Skip to content

Commit 6d7db42

Browse files
authored
risk: Added fuzz testing to enhance security (#6)
1 parent c78ca43 commit 6d7db42

4 files changed

Lines changed: 236 additions & 4 deletions

File tree

CHANGELOG/CHANGELOG-1.x.md

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,17 @@ Date format: `YYYY-MM-DD`
1616
### Removed
1717
### Fixed
1818

19+
---
20+
## [1.1.0] - 2025-07-18
21+
22+
### Added
23+
### Changed
24+
### Deprecated
25+
### Removed
26+
### Fixed
27+
### Security
28+
- **risk:** Added fuzz testing to the `aes-ctr-drbg` module to enhance security and robustness.
29+
1930
---
2031
## [1.0.1] - 2025-07-18
2132

@@ -39,7 +50,8 @@ Date format: `YYYY-MM-DD`
3950
### Fixed
4051
### Security
4152

42-
[Unreleased]: https://github.com/sixafter/aes-ctr-drbg/compare/v1.0.1...HEAD
53+
[Unreleased]: https://github.com/sixafter/aes-ctr-drbg/compare/v1.1.0...HEAD
54+
[1.1.0]: https://github.com/sixafter/aes-ctr-drbg/compare/v1.0.1...v1.1.0
4355
[1.0.1]: https://github.com/sixafter/aes-ctr-drbg/compare/v1.0.0...v1.0.1
4456
[1.0.0]: https://github.com/sixafter/aes-ctr-drbg/compare/80b4d9e2c5b6a5805bd11741af0eea3d5435889b...v1.0.0
4557

Makefile

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ GO_MOD=$(GO_CMD) mod
1717
GO_LINT_CMD=golangci-lint run
1818
GO_WORK=$(GO_CMD) work
1919
GO_WORK_FILE := ./go.work
20-
FUZZTIME ?= 10s
20+
FUZZTIME ?= 20s
2121

2222
.PHONY: all
2323
all: clean test
@@ -32,9 +32,9 @@ test: ## Execute unit tests
3232

3333
.PHONY: fuzz
3434
fuzz: ## Run each Go fuzz test individually (10s per test)
35-
@for fuzz in FuzzNewWithLength FuzzCustomAlphabet FuzzCustomGenerator FuzzRead; do \
35+
@for fuzz in Fuzz_Reader_Read Fuzz_Reader_Concurrent Fuzz_NewReader_AllOptions Fuzz_NewReader_Personalization Fuzz_NewReader_Buffers; do \
3636
echo "===> Fuzzing $$fuzz"; \
37-
$(GO_TEST) -v -run=^$$ -fuzz=$$fuzz -fuzztime=$(FUZZTIME) || exit $$?; \
37+
$(GO_TEST) -v -fuzz=$$fuzz -fuzztime=$(FUZZTIME) || exit $$?; \
3838
done
3939

4040
.PHONY: bench

aes_ctr_drbg.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -132,6 +132,10 @@ func NewReader(opts ...Option) (Interface, error) {
132132
return nil, fmt.Errorf("invalid key size %d bytes; must be 16, 24, or 32", cfg.KeySize)
133133
}
134134

135+
if cfg.MaxInitRetries < 1 {
136+
return nil, fmt.Errorf("invalid MaxInitRetries: must be >= 1")
137+
}
138+
135139
// Step 3: Create a sync.Pool to manage DRBG instances for concurrent access.
136140
// The pool's New function attempts to create a new DRBG, retrying up to MaxInitRetries times.
137141
// If all attempts fail, the function panics, making failure explicit and visible.

aes_ctr_drbg_fuzz_test.go

Lines changed: 216 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,216 @@
1+
// Copyright (c) 2024 Six After, Inc
2+
//
3+
// This source code is licensed under the Apache 2.0 License found in the
4+
// LICENSE file in the root directory of this source tree.
5+
6+
package ctrdrbg
7+
8+
import (
9+
"math/rand"
10+
"testing"
11+
"time"
12+
13+
"github.com/stretchr/testify/assert"
14+
)
15+
16+
// Fuzz_Reader_Read exercises the Reader.Read method using a variety of buffer sizes.
17+
// It ensures that Read does not return an error and produces the requested number of bytes
18+
// for all valid sizes in the range [0, 65536]. Invalid sizes outside this range are skipped.
19+
func Fuzz_Reader_Read(f *testing.F) {
20+
for _, sz := range []int{0, 1, 15, 16, 17, 32, 64, 1024, 4096} {
21+
f.Add(sz)
22+
}
23+
24+
f.Fuzz(func(t *testing.T, size int) {
25+
t.Parallel()
26+
is := assert.New(t)
27+
28+
if size < 0 || size > 65536 {
29+
return // don't allocate insane slices
30+
}
31+
32+
buf := make([]byte, size)
33+
n, err := Reader.Read(buf)
34+
is.NoError(err, "Reader.Read failed")
35+
is.Equal(size, n, "unexpected number of bytes read")
36+
})
37+
}
38+
39+
// Fuzz_Reader_Concurrent tests the thread-safety of Reader.Read under concurrent access.
40+
// For several buffer sizes, it spawns multiple goroutines that each perform a Read operation,
41+
// checking that no errors occur. Sizes outside the range [1, 16384] are skipped.
42+
func Fuzz_Reader_Concurrent(f *testing.F) {
43+
f.Add(16)
44+
f.Add(1024)
45+
f.Fuzz(func(t *testing.T, size int) {
46+
t.Parallel()
47+
is := assert.New(t)
48+
49+
if size < 1 || size > 16384 {
50+
return
51+
}
52+
const N = 8
53+
bufs := make([][]byte, N)
54+
errs := make(chan error, N)
55+
for i := range bufs {
56+
bufs[i] = make([]byte, size)
57+
go func(i int) {
58+
_, err := Reader.Read(bufs[i])
59+
errs <- err
60+
}(i)
61+
}
62+
for i := 0; i < N; i++ {
63+
err := <-errs
64+
is.NoError(err, "Concurrent Read failed")
65+
}
66+
})
67+
}
68+
69+
// Fuzz_NewReader_AllOptions exercises NewReader with a variety of option combinations and parameter values.
70+
// It fuzzes all tunable configuration fields, including key size, personalization, sharding, buffer size, and retry settings.
71+
// If the key size is invalid, it asserts an error is returned. For valid configs, it checks that Read succeeds.
72+
func Fuzz_NewReader_AllOptions(f *testing.F) {
73+
f.Add(uint64(32), int(3), int(5), int(0), int(1), int(16), true, []byte("seed"), int(16))
74+
f.Add(uint64(0), int(0), int(0), int(0), int(0), int(0), false, []byte(nil), int(1))
75+
f.Add(uint64(4096), int(10), int(10), int(5), int(32), int(0), true, []byte("p"), int(32))
76+
77+
f.Fuzz(func(t *testing.T,
78+
maxBytes uint64,
79+
maxInitRetries int,
80+
maxRekeyAttempts int,
81+
shards int,
82+
bufSize int,
83+
keySizeRaw int,
84+
zeroBuffer bool,
85+
personalization []byte,
86+
mode int,
87+
) {
88+
t.Parallel()
89+
is := assert.New(t)
90+
91+
// Defensive bounds for fuzz
92+
if maxBytes > 1<<32 {
93+
maxBytes = 1 << 32
94+
}
95+
if bufSize < 0 {
96+
bufSize = 0
97+
}
98+
if bufSize > 1<<24 {
99+
bufSize = 1 << 24
100+
}
101+
if shards < 0 {
102+
shards = 0
103+
}
104+
if shards > 64 {
105+
shards = 64
106+
}
107+
if maxInitRetries < 0 {
108+
maxInitRetries = 0
109+
}
110+
if maxInitRetries > 100 {
111+
maxInitRetries = 100
112+
}
113+
if maxRekeyAttempts < 0 {
114+
maxRekeyAttempts = 0
115+
}
116+
if maxRekeyAttempts > 100 {
117+
maxRekeyAttempts = 100
118+
}
119+
if len(personalization) > 128 {
120+
personalization = personalization[:128]
121+
}
122+
123+
// Choose a valid or invalid key size
124+
var keySize KeySize
125+
switch mode % 4 {
126+
case 0:
127+
keySize = KeySize128
128+
case 1:
129+
keySize = KeySize192
130+
case 2:
131+
keySize = KeySize256
132+
default:
133+
keySize = KeySize(keySizeRaw)
134+
}
135+
136+
opts := []Option{
137+
WithMaxBytesPerKey(maxBytes),
138+
WithMaxInitRetries(maxInitRetries),
139+
WithMaxRekeyAttempts(maxRekeyAttempts),
140+
WithShards(shards),
141+
WithDefaultBufferSize(bufSize),
142+
WithEnableKeyRotation(mode%2 == 0),
143+
WithKeySize(keySize),
144+
WithUseZeroBuffer(zeroBuffer),
145+
WithPersonalization(personalization),
146+
WithRekeyBackoff(time.Duration(rand.Intn(1000)) * time.Millisecond),
147+
WithMaxRekeyBackoff(time.Duration(rand.Intn(3000)) * time.Millisecond),
148+
}
149+
150+
r, err := NewReader(opts...)
151+
152+
if keySize != KeySize128 && keySize != KeySize192 && keySize != KeySize256 {
153+
is.Error(err, "expected error with invalid keysize")
154+
return
155+
}
156+
157+
// If we failed for any other reason (e.g. invalid config, entropy exhausted), just return.
158+
if err != nil {
159+
return
160+
}
161+
162+
buf := make([]byte, 32)
163+
n, err := r.Read(buf)
164+
is.NoError(err, "Read failed")
165+
is.Equal(32, n, "short read")
166+
})
167+
}
168+
169+
// Fuzz_NewReader_Personalization fuzzes NewReader with different personalization values to ensure
170+
// that the DRBG accepts arbitrary domain separation strings. If NewReader returns an error (e.g. invalid config),
171+
// the input is skipped. Otherwise, it asserts that a 16-byte read succeeds.
172+
func Fuzz_NewReader_Personalization(f *testing.F) {
173+
f.Add([]byte("p"))
174+
f.Add([]byte{})
175+
f.Add([]byte(nil))
176+
f.Add(make([]byte, 64))
177+
178+
f.Fuzz(func(t *testing.T, p []byte) {
179+
t.Parallel()
180+
is := assert.New(t)
181+
182+
r, err := NewReader(WithPersonalization(p))
183+
if err != nil {
184+
return // or t.Skip() to not count as "failure"
185+
}
186+
buf := make([]byte, 16)
187+
n, err := r.Read(buf)
188+
is.NoError(err, "read failed")
189+
is.Equal(16, n, "short read")
190+
})
191+
}
192+
193+
// Fuzz_NewReader_Buffers fuzzes NewReader with various buffer size configurations, verifying
194+
// that the reader is correctly initialized and produces the expected output. If initialization fails
195+
// (due to an unsupported buffer size, etc.), the input is skipped. Otherwise, it checks a 32-byte read.
196+
func Fuzz_NewReader_Buffers(f *testing.F) {
197+
for _, sz := range []int{0, 1, 16, 1024, 1 << 20, 1 << 23} {
198+
f.Add(sz)
199+
}
200+
f.Fuzz(func(t *testing.T, bufSize int) {
201+
t.Parallel()
202+
is := assert.New(t)
203+
204+
if bufSize < 0 || bufSize > 1<<24 {
205+
return
206+
}
207+
r, err := NewReader(WithDefaultBufferSize(bufSize))
208+
if err != nil {
209+
return // or t.Skip() to not count as "failure"
210+
}
211+
buf := make([]byte, 32)
212+
n, err := r.Read(buf)
213+
is.NoError(err, "read failed")
214+
is.Equal(32, n, "short read")
215+
})
216+
}

0 commit comments

Comments
 (0)