blob: f5b2f53752a028242118ade71157ac53640c712e [file] [log] [blame]
// Copyright 2025 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// Package entropy implements a CPU jitter-based SP 800-90B entropy source.
package entropy
import (
"crypto/internal/fips140deps/time"
"errors"
"sync/atomic"
"unsafe"
)
// Version returns the version of the entropy source.
//
// This is independent of the FIPS 140-3 module version, in order to reuse the
// ESV certificate across module versions.
func Version() string {
return "v1.0.0"
}
// ScratchBuffer is a large buffer that will be written to using atomics, to
// generate noise from memory access timings. Its contents do not matter.
type ScratchBuffer [1 << 25]byte
// Seed returns a 384-bit seed with full entropy.
//
// memory is passed in to allow changing the allocation strategy without
// modifying the frozen and certified entropy source in this package.
//
// Seed returns an error if the entropy source startup health tests fail, which
// has a non-negligible chance of happening.
func Seed(memory *ScratchBuffer) ([48]byte, error) {
// Collect w = 1024 samples, each certified to provide no less than h = 0.5
// bits of entropy, for a total of hᵢₙ = w × h = 512 bits of entropy, over
// nᵢₙ = w × n = 8192 bits of input data.
var samples [1024]byte
if err := Samples(samples[:], memory); err != nil {
return [48]byte{}, err
}
// Use a vetted unkeyed conditioning component, SHA-384, with nw = 384 and
// nₒᵤₜ = 384. Per the formula in SP 800-90B Section 3.1.5.1.2, the output
// entropy hₒᵤₜ is:
//
// sage: n_in = 8192
// sage: n_out = 384
// sage: nw = 384
// sage: h_in = 512
// sage: P_high = 2^(-h_in)
// sage: P_low = (1 - P_high) / (2^n_in - 1)
// sage: n = min(n_out, nw)
// sage: ψ = 2^(n_in - n) * P_low + P_high
// sage: U = 2^(n_in - n) + sqrt(2 * n * 2^(n_in - n) * ln(2))
// sage: ω = U * P_low
// sage: h_out = -log(max(ψ, ω), 2)
// sage: h_out.n()
// 384.000000000000
//
// According to Implementation Guidance D.K, Resolution 19, since
//
// - the conditioning component is vetted,
// - hᵢₙ = 512 ≥ nₒᵤₜ + 64 = 448, and
// - nₒᵤₜ ≤ security strength of SHA-384 = 384 (per SP 800-107 Rev. 1, Table 1),
//
// we can claim the output has full entropy.
return SHA384(&samples), nil
}
// Samples starts a new entropy source, collects the requested number of
// samples, conducts startup health tests, and returns the samples or an error
// if the health tests fail.
//
// The health tests have a non-negligible chance of failing.
func Samples(samples []uint8, memory *ScratchBuffer) error {
if len(samples) < 1024 {
return errors.New("entropy: at least 1024 samples are required for startup health tests")
}
s := newSource(memory)
for range 4 {
// Warm up the source to avoid any initial bias.
_ = s.Sample()
}
for i := range samples {
samples[i] = s.Sample()
}
if err := RepetitionCountTest(samples); err != nil {
return err
}
if err := AdaptiveProportionTest(samples); err != nil {
return err
}
return nil
}
type source struct {
memory *ScratchBuffer
lcgState uint32
previous int64
}
func newSource(memory *ScratchBuffer) *source {
return &source{
memory: memory,
lcgState: uint32(time.HighPrecisionNow()),
previous: time.HighPrecisionNow(),
}
}
// touchMemory performs a write to memory at the given index.
//
// The memory slice is passed in and may be shared across sources e.g. to avoid
// the significant (~500µs) cost of zeroing a new allocation on every [Seed] call.
func touchMemory(memory *ScratchBuffer, idx uint32) {
idx = idx / 4 * 4 // align to 32 bits
u32 := (*uint32)(unsafe.Pointer(&memory[idx]))
last := atomic.LoadUint32(u32)
atomic.SwapUint32(u32, last+13)
}
func (s *source) Sample() uint8 {
// Perform a few memory accesses in an unpredictable pattern to expose the
// next measurement to as much system noise as possible.
memory, lcgState := s.memory, s.lcgState
if memory == nil { // remove the nil check from the inlined touchMemory calls
panic("entropy: nil memory buffer")
}
for range 64 {
lcgState = 1664525*lcgState + 1013904223
// Discard the lower bits, which tend to fall into short cycles.
idx := (lcgState >> 6) & (1<<25 - 1)
touchMemory(memory, idx)
}
s.lcgState = lcgState
t := time.HighPrecisionNow()
sample := t - s.previous
s.previous = t
// Reduce the symbol space to 256 values, assuming most of the entropy is in
// the least-significant bits, which represent the highest-resolution timing
// differences.
return uint8(sample)
}
// RepetitionCountTest implements the repetition count test from SP 800-90B
// Section 4.4.1. It returns an error if any symbol is repeated C = 41 or more
// times in a row.
//
// This C value is calculated from a target failure probability α = 2⁻²⁰ and a
// claimed min-entropy per symbol h = 0.5 bits, using the formula in SP 800-90B
// Section 4.4.1.
//
// sage: α = 2^-20
// sage: H = 0.5
// sage: 1 + ceil(-log(α, 2) / H)
// 41
func RepetitionCountTest(samples []uint8) error {
x := samples[0]
count := 1
for _, y := range samples[1:] {
if y == x {
count++
if count >= 41 {
return errors.New("entropy: repetition count health test failed")
}
} else {
x = y
count = 1
}
}
return nil
}
// AdaptiveProportionTest implements the adaptive proportion test from SP 800-90B
// Section 4.4.2. It returns an error if any symbol appears C = 410 or more
// times in the last W = 512 samples.
//
// This C value is calculated from a target failure probability α = 2⁻²⁰, a
// window size W = 512, and a claimed min-entropy per symbol h = 0.5 bits, using
// the formula in SP 800-90B Section 4.4.2, equivalent to the Microsoft Excel
// formula 1+CRITBINOM(W, power(2,(−H)),1−α).
//
// sage: from scipy.stats import binom
// sage: α = 2^-20
// sage: H = 0.5
// sage: W = 512
// sage: C = 1 + binom.ppf(1 - α, W, 2**(-H))
// sage: ceil(C)
// 410
func AdaptiveProportionTest(samples []uint8) error {
var counts [256]int
for i, x := range samples {
counts[x]++
if i >= 512 {
counts[samples[i-512]]--
}
if counts[x] >= 410 {
return errors.New("entropy: adaptive proportion health test failed")
}
}
return nil
}