Skip to content

Commit fc88e18

Browse files
FiloSottileDanielMorsing
authored andcommitted
crypto/internal/fips140/entropy: add CPU jitter-based entropy source
Heavily inspired by the BoringSSL implementation. Change-Id: I6a6a6964b22826d54700c8b3d555054163cef5fe Co-authored-by: Daniel Morsing <[email protected]> Cq-Include-Trybots: luci.golang.try:gotip-linux-s390x,gotip-linux-ppc64_power10,gotip-linux-ppc64le_power10,gotip-linux-ppc64le_power8,gotip-linux-arm,gotip-darwin-arm64_15,gotip-windows-arm64,gotip-freebsd-amd64 Reviewed-on: https://go-review.googlesource.com/c/go/+/703015 LUCI-TryBot-Result: Go LUCI <[email protected]> Reviewed-by: Roland Shoemaker <[email protected]> Reviewed-by: Daniel McCarney <[email protected]> Auto-Submit: Filippo Valsorda <[email protected]> Reviewed-by: Cherry Mui <[email protected]>
1 parent db4fade commit fc88e18

File tree

10 files changed

+758
-15
lines changed

10 files changed

+758
-15
lines changed

src/crypto/internal/entropy/entropy.go

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,11 @@
33
// license that can be found in the LICENSE file.
44

55
// Package entropy provides the passive entropy source for the FIPS 140-3
6-
// module. It is only used in FIPS mode by [crypto/internal/fips140/drbg.Read].
6+
// module. It is only used in FIPS mode by [crypto/internal/fips140/drbg.Read]
7+
// from the FIPS 140-3 Go Cryptographic Module v1.0.0. Later versions of the
8+
// module have an internal CPU jitter-based entropy source.
79
//
8-
// This complies with IG 9.3.A, Additional Comment 12, which until January 1,
10+
// This complied with IG 9.3.A, Additional Comment 12, which until January 1,
911
// 2026 allows new modules to meet an [earlier version] of Resolution 2(b):
1012
// "A software module that contains an approved DRBG that receives a LOAD
1113
// command (or its logical equivalent) with entropy obtained from [...] inside

src/crypto/internal/fips140/drbg/rand.go

Lines changed: 49 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -9,21 +9,53 @@
99
package drbg
1010

1111
import (
12-
"crypto/internal/entropy"
1312
"crypto/internal/fips140"
13+
"crypto/internal/fips140/entropy"
1414
"crypto/internal/randutil"
1515
"crypto/internal/sysrand"
1616
"io"
1717
"sync"
18+
"sync/atomic"
1819
)
1920

20-
var drbgs = sync.Pool{
21+
// memory is a scratch buffer that is accessed between samples by the entropy
22+
// source to expose it to memory access timings.
23+
//
24+
// We reuse it and share it between Seed calls to avoid the significant (~500µs)
25+
// cost of zeroing a new allocation every time. The entropy source accesses it
26+
// using atomics (and doesn't care about its contents).
27+
//
28+
// It should end up in the .noptrbss section, and become backed by physical pages
29+
// at first use. This ensures that programs that do not use the FIPS 140-3 module
30+
// do not incur any memory use or initialization penalties.
31+
var memory entropy.ScratchBuffer
32+
33+
func getEntropy() *[SeedSize]byte {
34+
var retries int
35+
seed, err := entropy.Seed(&memory)
36+
for err != nil {
37+
// The CPU jitter-based SP 800-90B entropy source has a non-negligible
38+
// chance of failing the startup health tests.
39+
//
40+
// Each time it does, it enters a permanent failure state, and we
41+
// restart it anew. This is not expected to happen more than a few times
42+
// in a row.
43+
if retries++; retries > 100 {
44+
panic("fips140/drbg: failed to obtain initial entropy")
45+
}
46+
seed, err = entropy.Seed(&memory)
47+
}
48+
return &seed
49+
}
50+
51+
// getEntropy is very slow (~500µs), so we don't want it on the hot path.
52+
// We keep both a persistent DRBG instance and a pool of additional instances.
53+
// Occasional uses will use drbgInstance, even if the pool was emptied since the
54+
// last use. Frequent concurrent uses will fill the pool and use it.
55+
var drbgInstance atomic.Pointer[Counter]
56+
var drbgPool = sync.Pool{
2157
New: func() any {
22-
var c *Counter
23-
entropy.Depleted(func(seed *[48]byte) {
24-
c = NewCounter(seed)
25-
})
26-
return c
58+
return NewCounter(getEntropy())
2759
},
2860
}
2961

@@ -44,8 +76,15 @@ func Read(b []byte) {
4476
additionalInput := new([SeedSize]byte)
4577
sysrand.Read(additionalInput[:16])
4678

47-
drbg := drbgs.Get().(*Counter)
48-
defer drbgs.Put(drbg)
79+
drbg := drbgInstance.Swap(nil)
80+
if drbg == nil {
81+
drbg = drbgPool.Get().(*Counter)
82+
}
83+
defer func() {
84+
if !drbgInstance.CompareAndSwap(nil, drbg) {
85+
drbgPool.Put(drbg)
86+
}
87+
}()
4988

5089
for len(b) > 0 {
5190
size := min(len(b), maxRequestSize)
@@ -54,9 +93,7 @@ func Read(b []byte) {
5493
// Section 9.3.2: if Generate reports a reseed is required, the
5594
// additional input is passed to Reseed along with the entropy and
5695
// then nulled before the next Generate call.
57-
entropy.Depleted(func(seed *[48]byte) {
58-
drbg.Reseed(seed, additionalInput)
59-
})
96+
drbg.Reseed(getEntropy(), additionalInput)
6097
additionalInput = nil
6198
continue
6299
}
Lines changed: 202 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,202 @@
1+
// Copyright 2025 The Go Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style
3+
// license that can be found in the LICENSE file.
4+
5+
// Package entropy implements a CPU jitter-based SP 800-90B entropy source.
6+
package entropy
7+
8+
import (
9+
"crypto/internal/fips140deps/time"
10+
"errors"
11+
"sync/atomic"
12+
"unsafe"
13+
)
14+
15+
// Version returns the version of the entropy source.
16+
//
17+
// This is independent of the FIPS 140-3 module version, in order to reuse the
18+
// ESV certificate across module versions.
19+
func Version() string {
20+
return "v1.0.0"
21+
}
22+
23+
// ScratchBuffer is a large buffer that will be written to using atomics, to
24+
// generate noise from memory access timings. Its contents do not matter.
25+
type ScratchBuffer [1 << 25]byte
26+
27+
// Seed returns a 384-bit seed with full entropy.
28+
//
29+
// memory is passed in to allow changing the allocation strategy without
30+
// modifying the frozen and certified entropy source in this package.
31+
//
32+
// Seed returns an error if the entropy source startup health tests fail, which
33+
// has a non-negligible chance of happening.
34+
func Seed(memory *ScratchBuffer) ([48]byte, error) {
35+
// Collect w = 1024 samples, each certified to provide no less than h = 0.5
36+
// bits of entropy, for a total of hᵢₙ = w × h = 512 bits of entropy, over
37+
// nᵢₙ = w × n = 8192 bits of input data.
38+
var samples [1024]byte
39+
if err := Samples(samples[:], memory); err != nil {
40+
return [48]byte{}, err
41+
}
42+
43+
// Use a vetted unkeyed conditioning component, SHA-384, with nw = 384 and
44+
// nₒᵤₜ = 384. Per the formula in SP 800-90B Section 3.1.5.1.2, the output
45+
// entropy hₒᵤₜ is:
46+
//
47+
// sage: n_in = 8192
48+
// sage: n_out = 384
49+
// sage: nw = 384
50+
// sage: h_in = 512
51+
// sage: P_high = 2^(-h_in)
52+
// sage: P_low = (1 - P_high) / (2^n_in - 1)
53+
// sage: n = min(n_out, nw)
54+
// sage: ψ = 2^(n_in - n) * P_low + P_high
55+
// sage: U = 2^(n_in - n) + sqrt(2 * n * 2^(n_in - n) * ln(2))
56+
// sage: ω = U * P_low
57+
// sage: h_out = -log(max(ψ, ω), 2)
58+
// sage: h_out.n()
59+
// 384.000000000000
60+
//
61+
// According to Implementation Guidance D.K, Resolution 19, since
62+
//
63+
// - the conditioning component is vetted,
64+
// - hᵢₙ = 512 ≥ nₒᵤₜ + 64 = 448, and
65+
// - nₒᵤₜ ≤ security strength of SHA-384 = 384 (per SP 800-107 Rev. 1, Table 1),
66+
//
67+
// we can claim the output has full entropy.
68+
return SHA384(&samples), nil
69+
}
70+
71+
// Samples starts a new entropy source, collects the requested number of
72+
// samples, conducts startup health tests, and returns the samples or an error
73+
// if the health tests fail.
74+
//
75+
// The health tests have a non-negligible chance of failing.
76+
func Samples(samples []uint8, memory *ScratchBuffer) error {
77+
if len(samples) < 1024 {
78+
return errors.New("entropy: at least 1024 samples are required for startup health tests")
79+
}
80+
s := newSource(memory)
81+
for range 4 {
82+
// Warm up the source to avoid any initial bias.
83+
_ = s.Sample()
84+
}
85+
for i := range samples {
86+
samples[i] = s.Sample()
87+
}
88+
if err := RepetitionCountTest(samples); err != nil {
89+
return err
90+
}
91+
if err := AdaptiveProportionTest(samples); err != nil {
92+
return err
93+
}
94+
return nil
95+
}
96+
97+
type source struct {
98+
memory *ScratchBuffer
99+
lcgState uint32
100+
previous int64
101+
}
102+
103+
func newSource(memory *ScratchBuffer) *source {
104+
return &source{
105+
memory: memory,
106+
lcgState: uint32(time.HighPrecisionNow()),
107+
previous: time.HighPrecisionNow(),
108+
}
109+
}
110+
111+
// touchMemory performs a write to memory at the given index.
112+
//
113+
// The memory slice is passed in and may be shared across sources e.g. to avoid
114+
// the significant (~500µs) cost of zeroing a new allocation on every [Seed] call.
115+
func touchMemory(memory *ScratchBuffer, idx uint32) {
116+
idx = idx / 4 * 4 // align to 32 bits
117+
u32 := (*uint32)(unsafe.Pointer(&memory[idx]))
118+
last := atomic.LoadUint32(u32)
119+
atomic.SwapUint32(u32, last+13)
120+
}
121+
122+
func (s *source) Sample() uint8 {
123+
// Perform a few memory accesses in an unpredictable pattern to expose the
124+
// next measurement to as much system noise as possible.
125+
memory, lcgState := s.memory, s.lcgState
126+
_ = memory[0] // hoist the nil check out of touchMemory
127+
for range 64 {
128+
lcgState = 1664525*lcgState + 1013904223
129+
// Discard the lower bits, which tend to fall into short cycles.
130+
idx := (lcgState >> 6) & (1<<25 - 1)
131+
touchMemory(memory, idx)
132+
}
133+
s.lcgState = lcgState
134+
135+
t := time.HighPrecisionNow()
136+
sample := t - s.previous
137+
s.previous = t
138+
139+
// Reduce the symbol space to 256 values, assuming most of the entropy is in
140+
// the least-significant bits, which represent the highest-resolution timing
141+
// differences.
142+
return uint8(sample)
143+
}
144+
145+
// RepetitionCountTest implements the repetition count test from SP 800-90B
146+
// Section 4.4.1. It returns an error if any symbol is repeated C = 41 or more
147+
// times in a row.
148+
//
149+
// This C value is calculated from a target failure probability α = 2⁻²⁰ and a
150+
// claimed min-entropy per symbol h = 0.5 bits, using the formula in SP 800-90B
151+
// Section 4.4.1.
152+
//
153+
// sage: α = 2^-20
154+
// sage: H = 0.5
155+
// sage: 1 + ceil(-log(α, 2) / H)
156+
// 41
157+
func RepetitionCountTest(samples []uint8) error {
158+
x := samples[0]
159+
count := 1
160+
for _, y := range samples[1:] {
161+
if y == x {
162+
count++
163+
if count >= 41 {
164+
return errors.New("entropy: repetition count health test failed")
165+
}
166+
} else {
167+
x = y
168+
count = 1
169+
}
170+
}
171+
return nil
172+
}
173+
174+
// AdaptiveProportionTest implements the adaptive proportion test from SP 800-90B
175+
// Section 4.4.2. It returns an error if any symbol appears C = 410 or more
176+
// times in the last W = 512 samples.
177+
//
178+
// This C value is calculated from a target failure probability α = 2⁻²⁰, a
179+
// window size W = 512, and a claimed min-entropy per symbol h = 0.5 bits, using
180+
// the formula in SP 800-90B Section 4.4.2, equivalent to the Microsoft Excel
181+
// formula 1+CRITBINOM(W, power(2,(−H)),1−α).
182+
//
183+
// sage: from scipy.stats import binom
184+
// sage: α = 2^-20
185+
// sage: H = 0.5
186+
// sage: W = 512
187+
// sage: C = 1 + binom.ppf(1 - α, W, 2**(-H))
188+
// sage: ceil(C)
189+
// 410
190+
func AdaptiveProportionTest(samples []uint8) error {
191+
var counts [256]int
192+
for i, x := range samples {
193+
counts[x]++
194+
if i >= 512 {
195+
counts[samples[i-512]]--
196+
}
197+
if counts[x] >= 410 {
198+
return errors.New("entropy: adaptive proportion health test failed")
199+
}
200+
}
201+
return nil
202+
}

0 commit comments

Comments
 (0)