Skip to content

Commit 1377377

Browse files
committed
feature: Added WithSelfTests and WithZeroization
1 parent b882816 commit 1377377

12 files changed

Lines changed: 580 additions & 49 deletions

CHANGELOG/CHANGELOG-1.x.md

Lines changed: 21 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,24 @@ Date format: `YYYY-MM-DD`
1212

1313
### Added
1414
### Changed
15-
- **debt:** Upgraded all dependencies to their latest stable versions.
16-
- **risk:** Add retry logic to signature verification make target to handle transient network errors.
15+
### Deprecated
16+
### Removed
17+
### Fixed
18+
### Security
19+
20+
---
21+
22+
## [1.15.0] - 2025-12-29
23+
24+
25+
### Added
26+
- **feature:** Added `WithSelfTests` option to enable FIPS 140-2 Known Answer Test (KAT) self-tests at DRBG initialization using NIST CAVP vectors.
27+
- **feature:** Added `WithZeroization` option to enable secure erasure of old key material during key rotation for forward secrecy (FIPS 140-2 §4.7.6 compliance).
28+
29+
### Changed
30+
- **risk:** Added 64 KB maximum request size limit per DRBG call to comply with NIST SP 800-90A §10.2.1.
31+
- **risk:** Clamped user-configured reseed interval to a maximum of 2^48 requests per NIST SP 800-90A §10.2.1.1.
32+
- **debt:** Updated NIST SP 800-90A specification mapping table in documentation to reflect new FIPS compliance features (max request size, reseed interval cap, KATs, and key zeroization).
1733

1834
### Deprecated
1935
### Removed
@@ -193,7 +209,7 @@ Date format: `YYYY-MM-DD`
193209

194210
### Added
195211
### Changed
196-
- **feature:** Prediction resistance mode (`WithPredictionResistance`), enabling NIST SP 800-90A Section 9.3 compliance by reseeding before every output.
212+
- **feature:** Prediction resistance mode (`WithPredictionResistance`), enabling NIST SP 800-90A §9.3 compliance by reseeding before every output.
197213
- **feature:** Automatic interval-based reseeding (`WithReseedInterval`) and reseed-on-request-count (`WithReseedRequests`) options.
198214
- **feature:** Added `ReadWithAdditionalInput([]byte)` method for supplying per-call additional input as permitted by the NIST SP 800-90A standard.
199215
- **feature:** Added `Reseed([]byte)` method to manually reseed a DRBG instance with new entropy.
@@ -264,7 +280,8 @@ Date format: `YYYY-MM-DD`
264280
### Fixed
265281
### Security
266282

267-
[Unreleased]: https://github.com/sixafter/aes-ctr-drbg/compare/v1.14.5...HEAD
283+
[Unreleased]: https://github.com/sixafter/aes-ctr-drbg/compare/v1.15.0...HEAD
284+
[1.15.0]: https://github.com/sixafter/aes-ctr-drbg/compare/v1.14.5...v1.15.0
268285
[1.14.5]: https://github.com/sixafter/aes-ctr-drbg/compare/v1.14.1...v1.14.5
269286
[1.14.1]: https://github.com/sixafter/aes-ctr-drbg/compare/v1.14.0...v1.14.1
270287
[1.14.0]: https://github.com/sixafter/aes-ctr-drbg/compare/v1.13.0...v1.14.0

Makefile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ GO_WORK_FILE := ./go.work
2020
FUZZTIME ?= 25s
2121

2222
.PHONY: all
23-
all: deps vendor update vendor tidy clean test
23+
all: deps update tidy vendor clean test
2424

2525
.PHONY: deps
2626
deps: ## Get the dependencies and vendor

README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -117,10 +117,10 @@ For a detailed mapping between the implementation and NIST SP 800-90A requiremen
117117
To verify the integrity of the release tarball, you can use Cosign to check the signature and checksums. Follow these steps:
118118

119119
```sh
120-
# Fetch the latest release tag from GitHub API (e.g., "v1.14.0")
120+
# Fetch the latest release tag from GitHub API (e.g., "v1.15.0")
121121
TAG=$(curl -s https://api.github.com/repos/sixafter/aes-ctr-drbg/releases/latest | jq -r .tag_name)
122122

123-
# Remove leading "v" for filenames (e.g., "v1.14.0" -> "1.14.0")
123+
# Remove leading "v" for filenames (e.g., "v1.15.0" -> "1.15.0")
124124
VERSION=${TAG#v}
125125

126126
# ---------------------------------------------------------------------

aes_ctr_drbg.go

Lines changed: 44 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
//
88
// This package implements a cryptographically secure, pool-backed Deterministic Random Bit Generator
99
// (DRBG) following the NIST SP 800-90A AES-CTR-DRBG construction, specifically as defined in
10-
// Section 10.2.1 of NIST SP 800-90A Rev. 1 ("Recommendation for Random Number Generation Using
10+
// §10.2.1 of NIST SP 800-90A Rev. 1 ("Recommendation for Random Number Generation Using
1111
// Deterministic Random Bit Generators").
1212
//
1313
// Each generator instance uses an AES block cipher in counter (CTR) mode to produce cryptographically
@@ -18,14 +18,16 @@
1818
//
1919
// Reference:
2020
//
21-
// NIST Special Publication 800-90A Rev. 1, Section 10.2.1 (CTR_DRBG Construction)
21+
// NIST Special Publication 800-90A Rev. 1, §10.2.1 (CTR_DRBG Construction)
2222
// https://nvlpubs.nist.gov/nistpubs/SpecialPublications/NIST.SP.800-90Ar1.pdf
2323
package ctrdrbg
2424

2525
import (
2626
"crypto/aes"
2727
"crypto/cipher"
2828
"crypto/rand"
29+
"crypto/subtle"
30+
"errors"
2931
"fmt"
3032
"io"
3133
mrand "math/rand/v2"
@@ -35,6 +37,14 @@ import (
3537
"time"
3638
)
3739

40+
const (
41+
// MaxBytesPerRequest is the NIST SP 800-90A maximum bytes per request for CTR_DRBG (2^19 bits = 64 KB).
42+
MaxBytesPerRequest = 1 << 16
43+
)
44+
45+
// ErrRequestTooLarge is returned when a Read request exceeds the NIST SP 800-90A maximum (64 KB).
46+
var ErrRequestTooLarge = errors.New("ctrdrbg: request exceeds NIST SP 800-90A max_number_of_bits_per_request (64 KB)")
47+
3848
// Reader is a package-level, cryptographically secure random source suitable for high-concurrency applications.
3949
//
4050
// Reader is initialized at package load time via NewReader and is safe for concurrent use. If initialization fails
@@ -193,6 +203,13 @@ func NewReader(opts ...Option) (Interface, error) {
193203
opt(&cfg)
194204
}
195205

206+
// FIPS 140-2 §4.9.1: Run Known Answer Tests if enabled.
207+
if cfg.EnableSelfTests {
208+
if err := RunSelfTests(); err != nil {
209+
return nil, err
210+
}
211+
}
212+
196213
// Validate the configured key size is appropriate for AES.
197214
// Only 16, 24, or 32 bytes (AES-128, AES-192, AES-256) are supported.
198215
switch cfg.KeySize {
@@ -541,6 +558,11 @@ func (d *drbg) Read(b []byte) (int, error) {
541558
return 0, nil
542559
}
543560

561+
// NIST SP 800-90A §10.2.1: Validate max_number_of_bits_per_request (64 KB).
562+
if n > MaxBytesPerRequest {
563+
return 0, ErrRequestTooLarge
564+
}
565+
544566
d.reseedIfForked()
545567

546568
// Prediction Resistance
@@ -650,6 +672,11 @@ func (d *drbg) ReadWithAdditionalInput(b []byte, additionalInput []byte) (int, e
650672
return 0, nil
651673
}
652674

675+
// NIST SP 800-90A §10.2.1: Validate max_number_of_bits_per_request (64 KB).
676+
if n > MaxBytesPerRequest {
677+
return 0, ErrRequestTooLarge
678+
}
679+
653680
d.reseedIfForked()
654681

655682
// If PredictionResistance is enabled, always reseed from fresh entropy before output,
@@ -1074,8 +1101,18 @@ func (d *drbg) asyncRekey() {
10741101
copy(key[:], seed[:d.config.KeySize])
10751102
var v [16]byte
10761103
copy(v[:], seed[d.config.KeySize:])
1077-
block, err := aes.NewCipher(key[:d.config.KeySize])
1104+
var block cipher.Block
1105+
block, err = aes.NewCipher(key[:d.config.KeySize])
10781106
if err == nil {
1107+
// FIPS 140-2 §4.7.6: Zeroize old key material before replacement.
1108+
if d.config.EnableZeroization {
1109+
if oldState := d.state.Load(); oldState != nil {
1110+
// Use subtle.XORBytes to prevent compiler optimization.
1111+
subtle.XORBytes(oldState.key[:], oldState.key[:], oldState.key[:])
1112+
subtle.XORBytes(oldState.v[:], oldState.v[:], oldState.v[:])
1113+
}
1114+
}
1115+
10791116
// Store new cryptographic state atomically.
10801117
newState := &state{
10811118
block: block,
@@ -1087,6 +1124,10 @@ func (d *drbg) asyncRekey() {
10871124

10881125
// Reset the working counter (v) under mutex lock to ensure no overlap.
10891126
d.vMu.Lock()
1127+
// Zeroize old working counter before overwriting.
1128+
if d.config.EnableZeroization {
1129+
subtle.XORBytes(d.v[:], d.v[:], d.v[:])
1130+
}
10901131
copy(d.v[:], v[:])
10911132
d.vMu.Unlock()
10921133
return // Rekey complete.

aes_ctr_drbg_bench_test.go

Lines changed: 26 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -83,8 +83,9 @@ func BenchmarkDRBG_Read_Concurrent(b *testing.B) {
8383
}
8484

8585
// BenchmarkDRBG_Read_LargeSizes_Sequential benchmarks sequential Read performance for large buffer sizes.
86+
// Buffer sizes are capped at 64 KB per NIST SP 800-90A max_number_of_bits_per_request.
8687
func BenchmarkDRBG_Read_LargeSizes_Sequential(b *testing.B) {
87-
largeBufferSizes := []int{4096, 16384, 65536, 1048576}
88+
largeBufferSizes := []int{4096, 16384, 65536}
8889
for _, size := range largeBufferSizes {
8990
b.Run(fmt.Sprintf("Serial_Read_Large_%dBytes", size), func(b *testing.B) {
9091
buffer := make([]byte, size)
@@ -102,8 +103,9 @@ func BenchmarkDRBG_Read_LargeSizes_Sequential(b *testing.B) {
102103

103104
// BenchmarkDRBG_Read_LargeSizes_Concurrent benchmarks concurrent Read performance
104105
// for large buffer sizes and multiple goroutines.
106+
// Buffer sizes are capped at 64 KB per NIST SP 800-90A max_number_of_bits_per_request.
105107
func BenchmarkDRBG_Read_LargeSizes_Concurrent(b *testing.B) {
106-
largeBufferSizes := []int{4096, 16384, 65536, 1048576}
108+
largeBufferSizes := []int{4096, 16384, 65536}
107109
goroutineCounts := []int{2, 4, 8, 16, 32, 64, 128}
108110
for _, size := range largeBufferSizes {
109111
for _, gc := range goroutineCounts {
@@ -170,7 +172,9 @@ func BenchmarkDRBG_Read_VariableSizes_Concurrent(b *testing.B) {
170172

171173
// BenchmarkDRBG_Read_ExtremeSizes benchmarks Read performance for very large buffer sizes
172174
// (10MB, 50MB, 100MB) both serially and concurrently.
175+
// Reads are performed in 64 KB chunks per NIST SP 800-90A max_number_of_bits_per_request.
173176
func BenchmarkDRBG_Read_ExtremeSizes(b *testing.B) {
177+
const chunkSize = 65536 // 64 KB (NIST max)
174178
extremeBufferSizes := []int{10485760, 52428800, 104857600} // 10MB, 50MB, 100MB
175179
for _, size := range extremeBufferSizes {
176180
// Serial
@@ -179,9 +183,16 @@ func BenchmarkDRBG_Read_ExtremeSizes(b *testing.B) {
179183
b.ReportAllocs()
180184
b.ResetTimer()
181185
for i := 0; i < b.N; i++ {
182-
_, err := Reader.Read(buffer)
183-
if err != nil {
184-
b.Fatalf("Read failed: %v", err)
186+
// Read in 64 KB chunks
187+
for offset := 0; offset < size; offset += chunkSize {
188+
end := offset + chunkSize
189+
if end > size {
190+
end = size
191+
}
192+
_, err := Reader.Read(buffer[offset:end])
193+
if err != nil {
194+
b.Fatalf("Read failed: %v", err)
195+
}
185196
}
186197
}
187198
})
@@ -195,9 +206,16 @@ func BenchmarkDRBG_Read_ExtremeSizes(b *testing.B) {
195206
b.ResetTimer()
196207
b.RunParallel(func(pb *testing.PB) {
197208
for pb.Next() {
198-
_, err := Reader.Read(buffer)
199-
if err != nil {
200-
b.Fatalf("Read failed: %v", err)
209+
// Read in 64 KB chunks
210+
for offset := 0; offset < size; offset += chunkSize {
211+
end := offset + chunkSize
212+
if end > size {
213+
end = size
214+
}
215+
_, err := Reader.Read(buffer[offset:end])
216+
if err != nil {
217+
b.Fatalf("Read failed: %v", err)
218+
}
201219
}
202220
}
203221
})

aes_ctr_drbg_fuzz_test.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,8 @@ import (
1515

1616
// Fuzz_Reader_Read exercises the Reader.Read method using a variety of buffer sizes.
1717
// 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.
18+
// for all valid sizes in the range [0, 65536]. Sizes exceeding 65536 (64 KB) are skipped
19+
// as they exceed the NIST SP 800-90A max_number_of_bits_per_request limit.
1920
func Fuzz_Reader_Read(f *testing.F) {
2021
for _, sz := range []int{0, 1, 15, 16, 17, 32, 64, 1024, 4096} {
2122
f.Add(sz)

0 commit comments

Comments
 (0)