Skip to content

Commit 5898d17

Browse files
authored
risk: Implemented automatic fork detection (#14)
1 parent 57c051d commit 5898d17

9 files changed

Lines changed: 415 additions & 281 deletions

File tree

CHANGELOG/CHANGELOG-1.x.md

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,18 @@ Date format: `YYYY-MM-DD`
1717
### Fixed
1818
### Security
1919

20+
---
21+
## [1.5.0] - 2025-07-23
22+
23+
### Added
24+
### Changed
25+
- **risk:** Implemented automatic fork detection and random stream reseeding using process PID tracking. After a process fork, DRBG instances will securely reseed to prevent duplicate random streams in parent and child processes. This eliminates a longstanding CSPRNG risk in forked environments and aligns user-space DRBG safety with that of kernel-backed generators. See `ForkDetectionInterval` in [configuration](../config.go) for tuning and compliance.
26+
27+
### Deprecated
28+
### Removed
29+
### Fixed
30+
### Security
31+
2032
---
2133
## [1.4.0] - 2025-07-20
2234

@@ -93,7 +105,8 @@ Date format: `YYYY-MM-DD`
93105
### Fixed
94106
### Security
95107

96-
[Unreleased]: https://github.com/sixafter/aes-ctr-drbg/compare/v1.4.0...HEAD
108+
[Unreleased]: https://github.com/sixafter/aes-ctr-drbg/compare/v1.5.0...HEAD
109+
[1.5.0]: https://github.com/sixafter/aes-ctr-drbg/compare/v1.4.0...v1.5.0
97110
[1.4.0]: https://github.com/sixafter/aes-ctr-drbg/compare/v1.3.0...v1.4.0
98111
[1.3.0]: https://github.com/sixafter/aes-ctr-drbg/compare/v1.2.0...v1.3.0
99112
[1.2.0]: https://github.com/sixafter/aes-ctr-drbg/compare/v1.1.0...v1.2.0

README.md

Lines changed: 197 additions & 194 deletions
Large diffs are not rendered by default.

aes_ctr_drbg.go

Lines changed: 36 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,20 @@
66
// Package ctrdrbg provides a FIPS 140-2 aligned, high-performance AES-CTR-DRBG.
77
//
88
// This package implements a cryptographically secure, pool-backed Deterministic Random Bit Generator
9-
// (DRBG) following the NIST SP 800-90A AES-CTR-DRBG construction. Each generator instance uses an
10-
// AES block cipher in counter (CTR) mode to produce cryptographically secure pseudo-random bytes,
11-
// suitable for high-throughput, concurrent workloads.
9+
// (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
11+
// Deterministic Random Bit Generators").
12+
//
13+
// Each generator instance uses an AES block cipher in counter (CTR) mode to produce cryptographically
14+
// secure pseudo-random bytes, suitable for high-throughput, concurrent workloads.
1215
//
1316
// All cryptographic primitives are provided by the Go standard library. This implementation is designed
1417
// for environments requiring strong compliance, including support for Go's FIPS-140 mode (GODEBUG=fips140=on).
18+
//
19+
// Reference:
20+
//
21+
// NIST Special Publication 800-90A Rev. 1, Section 10.2.1 (CTR_DRBG Construction)
22+
// https://nvlpubs.nist.gov/nistpubs/SpecialPublications/NIST.SP.800-90Ar1.pdf
1523
package ctrdrbg
1624

1725
import (
@@ -21,6 +29,7 @@ import (
2129
"fmt"
2230
"io"
2331
mrand "math/rand/v2"
32+
"os"
2433
"sync"
2534
"sync/atomic"
2635
"time"
@@ -466,6 +475,25 @@ type drbg struct {
466475
// Uses atomic operations for concurrency safety.
467476
rekeying uint32
468477

478+
// pid caches the process identifier (PID) of the operating system process in which
479+
// this DRBG instance was most recently initialized or reseeded.
480+
//
481+
// Purpose:
482+
// - Enables robust detection of process-level forks (e.g., via fork(2) or similar system calls).
483+
// - After a fork, the child process receives a new, unique PID, but the DRBG instance initially
484+
// retains the PID from its parent process.
485+
// - By comparing the current process PID (os.Getpid()) to this cached value, the DRBG can reliably
486+
// detect fork events at runtime.
487+
// - When a fork is detected, the DRBG securely reseeds its cryptographic state, preventing random
488+
// stream duplication and ensuring forward and backward security in both parent and child processes.
489+
//
490+
// Security Rationale:
491+
// - Eliminates the risk of duplicated random streams following a process fork—a known pitfall of
492+
// userspace CSPRNGs in forking environments (Linux, macOS).
493+
// - Aligns the safety guarantees of userspace DRBGs with those provided by kernel-backed CSPRNGs,
494+
// such as Linux getrandom(2), which handle fork-safety internally.
495+
pid int
496+
469497
// encV is a persistent [16]byte working buffer used as a session-local counter
470498
// during output generation.
471499
//
@@ -511,6 +539,8 @@ func (d *drbg) Read(b []byte) (int, error) {
511539
return 0, nil
512540
}
513541

542+
d.reseedIfForked()
543+
514544
// Prediction Resistance
515545
if d.config.PredictionResistance {
516546
if err := d.reseed(nil); err != nil {
@@ -618,6 +648,8 @@ func (d *drbg) ReadWithAdditionalInput(b []byte, additionalInput []byte) (int, e
618648
return 0, nil
619649
}
620650

651+
d.reseedIfForked()
652+
621653
// If PredictionResistance is enabled, always reseed from fresh entropy before output,
622654
// ignoring any additional input per NIST SP 800-90A requirements.
623655
if d.config.PredictionResistance {
@@ -957,6 +989,7 @@ func newDRBG(cfg *Config) (*drbg, error) {
957989
zero: zero,
958990
usage: 0,
959991
rekeying: 0,
992+
pid: os.Getpid(),
960993
}
961994
d.state.Store(st)
962995

aes_ctr_drbg_test.go

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -594,3 +594,22 @@ func Test_DRBG_Reseed_RequestLimit(t *testing.T) {
594594
// Outputs before and after reseed should differ (with overwhelming probability)
595595
is.False(bytes.Equal(out1, out2), "Output after reseed should differ from before")
596596
}
597+
598+
// Test_DRBG_ForkDetectionInterval_Config checks that ForkDetectionInterval is settable and present in config.
599+
func Test_DRBG_ForkDetectionInterval_Config(t *testing.T) {
600+
t.Parallel()
601+
is := assert.New(t)
602+
603+
cfg := DefaultConfig()
604+
cfg.ForkDetectionInterval = 17
605+
d, err := newDRBG(&cfg)
606+
is.NoError(err)
607+
608+
is.Equal(uint64(17), d.config.ForkDetectionInterval, "ForkDetectionInterval should match config value")
609+
610+
// Also test via functional option path.
611+
rdr, err := NewReader(WithForkDetectionInterval(42))
612+
is.NoError(err)
613+
got := rdr.Config()
614+
is.Equal(uint64(42), got.ForkDetectionInterval, "ForkDetectionInterval should be set via option")
615+
}

config.go

Lines changed: 54 additions & 77 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,15 @@ type Config struct {
102102
// Zero disables reseed-on-request-count.
103103
ReseedRequests uint64
104104

105+
// ForkDetectionInterval controls how often fork detection is performed.
106+
//
107+
// If 0 (default), fork detection runs on every output request (max safety, fully compliant).
108+
// If >0, fork detection is performed once every N output requests (advanced tuning; reduces overhead
109+
// at the cost of a negligible window of risk).
110+
//
111+
// WARNING: Setting this above zero is NOT recommended for compliance-sensitive environments.
112+
ForkDetectionInterval uint64
113+
105114
// KeySize specifies the AES key length to use for this DRBG instance.
106115
//
107116
// Acceptable values:
@@ -188,35 +197,51 @@ const (
188197
defaultRekeyBackoff = 100 * time.Millisecond
189198
)
190199

191-
// DefaultConfig returns a Config struct populated with production-safe, recommended defaults.
200+
// DefaultConfig returns a Config struct populated with production-safe, NIST SP 800-90A Section 10.2.1-aligned defaults.
201+
//
202+
// This function provides a robust baseline configuration for AES-CTR-DRBG instances, suitable for general-purpose
203+
// cryptographic use and high-concurrency workloads. All parameters are selected to ensure strong security, compliance
204+
// with FIPS 140-2 and NIST SP 800-90A requirements, and operational reliability under diverse system conditions.
205+
//
206+
// The returned configuration enables fork-safety (via PID tracking), supports domain separation via personalization,
207+
// and is compatible with Go's FIPS-140 mode (GODEBUG=fips140=on).
192208
//
193209
// Defaults:
194-
// - KeySize: 32 bytes (AES-256)
195-
// - MaxBytesPerKey: 1 GiB (1 << 30)
196-
// - MaxInitRetries: 3
197-
// - MaxRekeyAttempts: 5
198-
// - MaxRekeyBackoff: 2 seconds
199-
// - RekeyBackoff: 100 milliseconds
200-
// - EnableKeyRotation: true
201-
// - Personalization: nil (no domain separation)
210+
// - KeySize: 32 bytes (AES-256, recommended by NIST for most use cases)
211+
// - MaxBytesPerKey: 1 GiB (1 << 30); triggers key rotation for forward secrecy
212+
// - MaxInitRetries: 3 attempts to initialize each DRBG pool entry
213+
// - MaxRekeyAttempts: 5 attempts per automatic key rotation
214+
// - MaxRekeyBackoff: 2 seconds (maximum exponential backoff between failed rekey attempts)
215+
// - RekeyBackoff: 100 milliseconds (initial backoff for rekey attempts)
216+
// - EnableKeyRotation: false (key rotation is disabled by default—**set to true for forward secrecy**)
217+
// - Personalization: nil (no domain separation unless set by the caller)
218+
// - UseZeroBuffer: false (random output generated directly into caller's buffer)
219+
// - DefaultBufferSize: 0 (no preallocation of zero-filled buffers)
220+
// - Shards: runtime.GOMAXPROCS(0) (number of internal DRBG pools matches available CPUs)
221+
// - PredictionResistance: false (prediction resistance is disabled; enable only if required by policy)
222+
// - ForkDetectionInterval: 0 (fork detection performed on every output request for maximum safety)
223+
//
224+
// NIST Reference:
225+
// - See NIST SP 800-90A, Section 10.2.1 (CTR DRBG) for cryptographic construction details.
202226
//
203227
// Example usage:
204228
//
205229
// cfg := ctrdrbg.DefaultConfig()
206230
func DefaultConfig() Config {
207231
return Config{
208-
KeySize: KeySize256,
209-
MaxBytesPerKey: defaultMaxBytes,
210-
MaxInitRetries: defaultInitRetries,
211-
MaxRekeyAttempts: defaultRekeyRetries,
212-
MaxRekeyBackoff: defaultMaxBackoff,
213-
RekeyBackoff: defaultRekeyBackoff,
214-
EnableKeyRotation: false,
215-
Personalization: nil,
216-
UseZeroBuffer: false,
217-
DefaultBufferSize: 0,
218-
Shards: runtime.GOMAXPROCS(0),
219-
PredictionResistance: false,
232+
KeySize: KeySize256,
233+
MaxBytesPerKey: defaultMaxBytes,
234+
MaxInitRetries: defaultInitRetries,
235+
MaxRekeyAttempts: defaultRekeyRetries,
236+
MaxRekeyBackoff: defaultMaxBackoff,
237+
RekeyBackoff: defaultRekeyBackoff,
238+
EnableKeyRotation: false,
239+
Personalization: nil,
240+
UseZeroBuffer: false,
241+
DefaultBufferSize: 0,
242+
Shards: runtime.GOMAXPROCS(0),
243+
PredictionResistance: false,
244+
ForkDetectionInterval: 0,
220245
}
221246
}
222247

@@ -236,65 +261,41 @@ type Option func(*Config)
236261
//
237262
// Acceptable values are KeySize128 (16 bytes), KeySize192 (24 bytes), or KeySize256 (32 bytes).
238263
// Any other value will cause NewReader to fail with an error at construction time.
239-
//
240-
// Example usage:
241-
//
242-
// r, err := ctrdrbg.NewReader(ctrdrbg.WithKeySize(ctrdrbg.KeySize192))
243264
func WithKeySize(k KeySize) Option { return func(cfg *Config) { cfg.KeySize = k } }
244265

245266
// WithMaxBytesPerKey returns an Option that sets the maximum number of bytes output per key before rekeying.
246267
//
247268
// This enforces a forward secrecy window: after MaxBytesPerKey random bytes are generated,
248269
// the DRBG automatically performs a rekey operation (if key rotation is enabled) to derive a new key
249270
// from fresh entropy. Lower this value to increase key rotation frequency for higher assurance.
250-
//
251-
// Example:
252-
//
253-
// r, err := ctrdrbg.NewReader(ctrdrbg.WithMaxBytesPerKey(1<<20)) // 1 MiB per key
254271
func WithMaxBytesPerKey(n uint64) Option { return func(cfg *Config) { cfg.MaxBytesPerKey = n } }
255272

256273
// WithMaxInitRetries returns an Option that sets the maximum number of attempts to initialize a DRBG instance
257274
// in the pool before failing.
258275
//
259276
// Increase this value if your system occasionally fails to gather entropy or encounters transient cryptographic errors
260277
// at startup.
261-
//
262-
// Example:
263-
//
264-
// r, err := ctrdrbg.NewReader(ctrdrbg.WithMaxInitRetries(10))
265278
func WithMaxInitRetries(n int) Option { return func(cfg *Config) { cfg.MaxInitRetries = n } }
266279

267280
// WithMaxRekeyAttempts returns an Option that sets the maximum number of attempts allowed for
268281
// asynchronous key rotation (rekey) in the DRBG.
269282
//
270283
// If all rekey attempts fail, the DRBG continues using the previous state. Exponential backoff is applied
271284
// between attempts (see WithMaxRekeyBackoff and WithRekeyBackoff).
272-
//
273-
// Example:
274-
//
275-
// r, err := ctrdrbg.NewReader(ctrdrbg.WithMaxRekeyAttempts(7))
276285
func WithMaxRekeyAttempts(n int) Option { return func(cfg *Config) { cfg.MaxRekeyAttempts = n } }
277286

278287
// WithMaxRekeyBackoff returns an Option that sets the maximum duration for exponential backoff between
279288
// failed rekey attempts.
280289
//
281290
// When a rekey attempt fails, the DRBG waits with exponentially increasing intervals, up to this maximum duration,
282291
// before retrying. If set to zero, a default (2s) is used.
283-
//
284-
// Example:
285-
//
286-
// r, err := ctrdrbg.NewReader(ctrdrbg.WithMaxRekeyBackoff(5 * time.Second))
287292
func WithMaxRekeyBackoff(d time.Duration) Option {
288293
return func(cfg *Config) { cfg.MaxRekeyBackoff = d }
289294
}
290295

291296
// WithRekeyBackoff returns an Option that sets the initial backoff duration before retrying a failed rekey operation.
292297
//
293298
// The first failure sleeps this duration, doubling for each subsequent failure, up to MaxRekeyBackoff.
294-
//
295-
// Example:
296-
//
297-
// r, err := ctrdrbg.NewReader(ctrdrbg.WithRekeyBackoff(250 * time.Millisecond))
298299
func WithRekeyBackoff(d time.Duration) Option {
299300
return func(cfg *Config) { cfg.RekeyBackoff = d }
300301
}
@@ -306,10 +307,6 @@ func WithRekeyBackoff(d time.Duration) Option {
306307
//
307308
// For most use cases, leave this enabled for forward secrecy and NIST alignment. Disable only for
308309
// compliance testing or special-purpose scenarios.
309-
//
310-
// Example:
311-
//
312-
// r, err := ctrdrbg.NewReader(ctrdrbg.WithEnableKeyRotation(true))
313310
func WithEnableKeyRotation(enable bool) Option {
314311
return func(cfg *Config) { cfg.EnableKeyRotation = enable }
315312
}
@@ -321,10 +318,6 @@ func WithEnableKeyRotation(enable bool) Option {
321318
// personalization values produce independent random streams, even if instantiated simultaneously.
322319
//
323320
// Use for tenant, application, service, or hardware isolation as required by your security model.
324-
//
325-
// Example:
326-
//
327-
// r, err := ctrdrbg.NewReader(ctrdrbg.WithPersonalization([]byte("my-tenant")))
328321
func WithPersonalization(p []byte) Option {
329322
return func(cfg *Config) { cfg.Personalization = p }
330323
}
@@ -336,10 +329,6 @@ func WithPersonalization(p []byte) Option {
336329
// for CTR-mode output. If disabled (false), output is written directly to the destination buffer.
337330
//
338331
// This option primarily affects performance tuning; it does not impact cryptographic security.
339-
//
340-
// Example:
341-
//
342-
// r, err := ctrdrbg.NewReader(ctrdrbg.WithUseZeroBuffer(true))
343332
func WithUseZeroBuffer(enable bool) Option {
344333
return func(cfg *Config) { cfg.UseZeroBuffer = enable }
345334
}
@@ -348,10 +337,6 @@ func WithUseZeroBuffer(enable bool) Option {
348337
// used for output if UseZeroBuffer is enabled.
349338
//
350339
// This can reduce allocations when large or repeated output requests are expected.
351-
//
352-
// Example:
353-
//
354-
// r, err := ctrdrbg.NewReader(ctrdrbg.WithDefaultBufferSize(4096))
355340
func WithDefaultBufferSize(n int) Option {
356341
return func(cfg *Config) { cfg.DefaultBufferSize = n }
357342
}
@@ -360,10 +345,6 @@ func WithDefaultBufferSize(n int) Option {
360345
//
361346
// Sharding improves parallelism and reduces contention under high concurrency, at the cost of increased memory use.
362347
// If n <= 0, the shard count defaults to runtime.GOMAXPROCS(0).
363-
//
364-
// Example:
365-
//
366-
// r, err := ctrdrbg.NewReader(ctrdrbg.WithShards(8))
367348
func WithShards(n int) Option {
368349
return func(cfg *Config) {
369350
if n <= 0 {
@@ -380,10 +361,6 @@ func WithShards(n int) Option {
380361
//
381362
// This mode increases system entropy usage and can impact performance in high-throughput scenarios.
382363
// Use only when required by compliance or application policy.
383-
//
384-
// Example:
385-
//
386-
// r, err := ctrdrbg.NewReader(ctrdrbg.WithPredictionResistance(true))
387364
func WithPredictionResistance(enable bool) Option {
388365
return func(cfg *Config) { cfg.PredictionResistance = enable }
389366
}
@@ -392,10 +369,6 @@ func WithPredictionResistance(enable bool) Option {
392369
//
393370
// When set to a non-zero value, the DRBG will reseed after this interval elapses, even if no key rotation
394371
// or manual reseed occurs. Set to zero to disable interval-based reseeding.
395-
//
396-
// Example:
397-
//
398-
// r, err := ctrdrbg.NewReader(ctrdrbg.WithReseedInterval(30 * time.Minute))
399372
func WithReseedInterval(d time.Duration) Option {
400373
return func(cfg *Config) { cfg.ReseedInterval = d }
401374
}
@@ -404,10 +377,14 @@ func WithReseedInterval(d time.Duration) Option {
404377
// allowed before forcing an automatic reseed from system entropy.
405378
//
406379
// Set to zero to disable reseed-on-request-count behavior.
407-
//
408-
// Example:
409-
//
410-
// r, err := ctrdrbg.NewReader(ctrdrbg.WithReseedRequests(1000))
411380
func WithReseedRequests(n uint64) Option {
412381
return func(cfg *Config) { cfg.ReseedRequests = n }
413382
}
383+
384+
// WithForkDetectionInterval sets the number of output requests between fork detection checks.
385+
//
386+
// WARNING: Setting this above zero introduces a window where a fork may not be detected immediately.
387+
// Only set for performance-tuned applications that do NOT require strict compliance!
388+
func WithForkDetectionInterval(n uint64) Option {
389+
return func(cfg *Config) { cfg.ForkDetectionInterval = n }
390+
}

config_test.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -203,3 +203,12 @@ func TestConfig_WithPredictionResistance(t *testing.T) {
203203
WithPredictionResistance(false)(&cfg)
204204
is.False(cfg.PredictionResistance, "WithPredictionResistance(false) should set PredictionResistance to false")
205205
}
206+
207+
func TestConfig_WithForkDetectionInterval(t *testing.T) {
208+
t.Parallel()
209+
is := assert.New(t)
210+
211+
cfg := DefaultConfig()
212+
WithForkDetectionInterval(42)(&cfg)
213+
is.Equal(uint64(42), cfg.ForkDetectionInterval, "WithForkDetectionInterval should set ForkDetectionInterval")
214+
}

0 commit comments

Comments
 (0)