|
| 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