Heavily inspired by the BoringSSL implementation.
Change-Id: I6a6a6964b22826d54700c8b3d555054163cef5fe
Co-authored-by: Daniel Morsing <daniel.morsing@gmail.com>
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 <golang-scoped@luci-project-accounts.iam.gserviceaccount.com>
Reviewed-by: Roland Shoemaker <roland@golang.org>
Reviewed-by: Daniel McCarney <daniel@binaryparadox.net>
Auto-Submit: Filippo Valsorda <filippo@golang.org>
Reviewed-by: Cherry Mui <cherryyz@google.com>
// license that can be found in the LICENSE file.
// Package entropy provides the passive entropy source for the FIPS 140-3
-// module. It is only used in FIPS mode by [crypto/internal/fips140/drbg.Read].
+// module. It is only used in FIPS mode by [crypto/internal/fips140/drbg.Read]
+// from the FIPS 140-3 Go Cryptographic Module v1.0.0. Later versions of the
+// module have an internal CPU jitter-based entropy source.
//
-// This complies with IG 9.3.A, Additional Comment 12, which until January 1,
+// This complied with IG 9.3.A, Additional Comment 12, which until January 1,
// 2026 allows new modules to meet an [earlier version] of Resolution 2(b):
// "A software module that contains an approved DRBG that receives a LOAD
// command (or its logical equivalent) with entropy obtained from [...] inside
package drbg
import (
- "crypto/internal/entropy"
"crypto/internal/fips140"
+ "crypto/internal/fips140/entropy"
"crypto/internal/randutil"
"crypto/internal/sysrand"
"io"
"sync"
+ "sync/atomic"
)
-var drbgs = sync.Pool{
+// memory is a scratch buffer that is accessed between samples by the entropy
+// source to expose it to memory access timings.
+//
+// We reuse it and share it between Seed calls to avoid the significant (~500µs)
+// cost of zeroing a new allocation every time. The entropy source accesses it
+// using atomics (and doesn't care about its contents).
+//
+// It should end up in the .noptrbss section, and become backed by physical pages
+// at first use. This ensures that programs that do not use the FIPS 140-3 module
+// do not incur any memory use or initialization penalties.
+var memory entropy.ScratchBuffer
+
+func getEntropy() *[SeedSize]byte {
+ var retries int
+ seed, err := entropy.Seed(&memory)
+ for err != nil {
+ // The CPU jitter-based SP 800-90B entropy source has a non-negligible
+ // chance of failing the startup health tests.
+ //
+ // Each time it does, it enters a permanent failure state, and we
+ // restart it anew. This is not expected to happen more than a few times
+ // in a row.
+ if retries++; retries > 100 {
+ panic("fips140/drbg: failed to obtain initial entropy")
+ }
+ seed, err = entropy.Seed(&memory)
+ }
+ return &seed
+}
+
+// getEntropy is very slow (~500µs), so we don't want it on the hot path.
+// We keep both a persistent DRBG instance and a pool of additional instances.
+// Occasional uses will use drbgInstance, even if the pool was emptied since the
+// last use. Frequent concurrent uses will fill the pool and use it.
+var drbgInstance atomic.Pointer[Counter]
+var drbgPool = sync.Pool{
New: func() any {
- var c *Counter
- entropy.Depleted(func(seed *[48]byte) {
- c = NewCounter(seed)
- })
- return c
+ return NewCounter(getEntropy())
},
}
additionalInput := new([SeedSize]byte)
sysrand.Read(additionalInput[:16])
- drbg := drbgs.Get().(*Counter)
- defer drbgs.Put(drbg)
+ drbg := drbgInstance.Swap(nil)
+ if drbg == nil {
+ drbg = drbgPool.Get().(*Counter)
+ }
+ defer func() {
+ if !drbgInstance.CompareAndSwap(nil, drbg) {
+ drbgPool.Put(drbg)
+ }
+ }()
for len(b) > 0 {
size := min(len(b), maxRequestSize)
// Section 9.3.2: if Generate reports a reseed is required, the
// additional input is passed to Reseed along with the entropy and
// then nulled before the next Generate call.
- entropy.Depleted(func(seed *[48]byte) {
- drbg.Reseed(seed, additionalInput)
- })
+ drbg.Reseed(getEntropy(), additionalInput)
additionalInput = nil
continue
}
--- /dev/null
+// 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
+ _ = memory[0] // hoist the nil check out of touchMemory
+ 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
+}
--- /dev/null
+// 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
+
+import "math/bits"
+
+// This file includes a SHA-384 implementation to insulate the entropy source
+// from any changes in the FIPS 140-3 module's crypto/internal/fips140/sha512
+// package. We only support 1024-byte inputs.
+
+func SHA384(p *[1024]byte) [48]byte {
+ h := [8]uint64{
+ 0xcbbb9d5dc1059ed8,
+ 0x629a292a367cd507,
+ 0x9159015a3070dd17,
+ 0x152fecd8f70e5939,
+ 0x67332667ffc00b31,
+ 0x8eb44a8768581511,
+ 0xdb0c2e0d64f98fa7,
+ 0x47b5481dbefa4fa4,
+ }
+
+ sha384Block(&h, (*[128]byte)(p[0:128]))
+ sha384Block(&h, (*[128]byte)(p[128:256]))
+ sha384Block(&h, (*[128]byte)(p[256:384]))
+ sha384Block(&h, (*[128]byte)(p[384:512]))
+ sha384Block(&h, (*[128]byte)(p[512:640]))
+ sha384Block(&h, (*[128]byte)(p[640:768]))
+ sha384Block(&h, (*[128]byte)(p[768:896]))
+ sha384Block(&h, (*[128]byte)(p[896:1024]))
+
+ var padlen [128]byte
+ padlen[0] = 0x80
+ bePutUint64(padlen[112+8:], 1024*8)
+ sha384Block(&h, &padlen)
+
+ var digest [48]byte
+ bePutUint64(digest[0:], h[0])
+ bePutUint64(digest[8:], h[1])
+ bePutUint64(digest[16:], h[2])
+ bePutUint64(digest[24:], h[3])
+ bePutUint64(digest[32:], h[4])
+ bePutUint64(digest[40:], h[5])
+ return digest
+}
+
+var _K = [...]uint64{
+ 0x428a2f98d728ae22,
+ 0x7137449123ef65cd,
+ 0xb5c0fbcfec4d3b2f,
+ 0xe9b5dba58189dbbc,
+ 0x3956c25bf348b538,
+ 0x59f111f1b605d019,
+ 0x923f82a4af194f9b,
+ 0xab1c5ed5da6d8118,
+ 0xd807aa98a3030242,
+ 0x12835b0145706fbe,
+ 0x243185be4ee4b28c,
+ 0x550c7dc3d5ffb4e2,
+ 0x72be5d74f27b896f,
+ 0x80deb1fe3b1696b1,
+ 0x9bdc06a725c71235,
+ 0xc19bf174cf692694,
+ 0xe49b69c19ef14ad2,
+ 0xefbe4786384f25e3,
+ 0x0fc19dc68b8cd5b5,
+ 0x240ca1cc77ac9c65,
+ 0x2de92c6f592b0275,
+ 0x4a7484aa6ea6e483,
+ 0x5cb0a9dcbd41fbd4,
+ 0x76f988da831153b5,
+ 0x983e5152ee66dfab,
+ 0xa831c66d2db43210,
+ 0xb00327c898fb213f,
+ 0xbf597fc7beef0ee4,
+ 0xc6e00bf33da88fc2,
+ 0xd5a79147930aa725,
+ 0x06ca6351e003826f,
+ 0x142929670a0e6e70,
+ 0x27b70a8546d22ffc,
+ 0x2e1b21385c26c926,
+ 0x4d2c6dfc5ac42aed,
+ 0x53380d139d95b3df,
+ 0x650a73548baf63de,
+ 0x766a0abb3c77b2a8,
+ 0x81c2c92e47edaee6,
+ 0x92722c851482353b,
+ 0xa2bfe8a14cf10364,
+ 0xa81a664bbc423001,
+ 0xc24b8b70d0f89791,
+ 0xc76c51a30654be30,
+ 0xd192e819d6ef5218,
+ 0xd69906245565a910,
+ 0xf40e35855771202a,
+ 0x106aa07032bbd1b8,
+ 0x19a4c116b8d2d0c8,
+ 0x1e376c085141ab53,
+ 0x2748774cdf8eeb99,
+ 0x34b0bcb5e19b48a8,
+ 0x391c0cb3c5c95a63,
+ 0x4ed8aa4ae3418acb,
+ 0x5b9cca4f7763e373,
+ 0x682e6ff3d6b2b8a3,
+ 0x748f82ee5defb2fc,
+ 0x78a5636f43172f60,
+ 0x84c87814a1f0ab72,
+ 0x8cc702081a6439ec,
+ 0x90befffa23631e28,
+ 0xa4506cebde82bde9,
+ 0xbef9a3f7b2c67915,
+ 0xc67178f2e372532b,
+ 0xca273eceea26619c,
+ 0xd186b8c721c0c207,
+ 0xeada7dd6cde0eb1e,
+ 0xf57d4f7fee6ed178,
+ 0x06f067aa72176fba,
+ 0x0a637dc5a2c898a6,
+ 0x113f9804bef90dae,
+ 0x1b710b35131c471b,
+ 0x28db77f523047d84,
+ 0x32caab7b40c72493,
+ 0x3c9ebe0a15c9bebc,
+ 0x431d67c49c100d4c,
+ 0x4cc5d4becb3e42b6,
+ 0x597f299cfc657e2a,
+ 0x5fcb6fab3ad6faec,
+ 0x6c44198c4a475817,
+}
+
+func sha384Block(dh *[8]uint64, p *[128]byte) {
+ var w [80]uint64
+ for i := range 80 {
+ if i < 16 {
+ w[i] = beUint64(p[i*8:])
+ } else {
+ v1 := w[i-2]
+ t1 := bits.RotateLeft64(v1, -19) ^ bits.RotateLeft64(v1, -61) ^ (v1 >> 6)
+ v2 := w[i-15]
+ t2 := bits.RotateLeft64(v2, -1) ^ bits.RotateLeft64(v2, -8) ^ (v2 >> 7)
+
+ w[i] = t1 + w[i-7] + t2 + w[i-16]
+ }
+ }
+
+ a, b, c, d, e, f, g, h := dh[0], dh[1], dh[2], dh[3], dh[4], dh[5], dh[6], dh[7]
+
+ for i := range 80 {
+ t1 := h + (bits.RotateLeft64(e, -14) ^ bits.RotateLeft64(e, -18) ^
+ bits.RotateLeft64(e, -41)) + ((e & f) ^ (^e & g)) + _K[i] + w[i]
+ t2 := (bits.RotateLeft64(a, -28) ^ bits.RotateLeft64(a, -34) ^
+ bits.RotateLeft64(a, -39)) + ((a & b) ^ (a & c) ^ (b & c))
+
+ h = g
+ g = f
+ f = e
+ e = d + t1
+ d = c
+ c = b
+ b = a
+ a = t1 + t2
+ }
+
+ dh[0] += a
+ dh[1] += b
+ dh[2] += c
+ dh[3] += d
+ dh[4] += e
+ dh[5] += f
+ dh[6] += g
+ dh[7] += h
+}
+
+func beUint64(b []byte) uint64 {
+ _ = b[7] // bounds check hint to compiler; see golang.org/issue/14808
+ return uint64(b[7]) | uint64(b[6])<<8 | uint64(b[5])<<16 | uint64(b[4])<<24 |
+ uint64(b[3])<<32 | uint64(b[2])<<40 | uint64(b[1])<<48 | uint64(b[0])<<56
+}
+
+func bePutUint64(b []byte, v uint64) {
+ _ = b[7] // early bounds check to guarantee safety of writes below
+ b[0] = byte(v >> 56)
+ b[1] = byte(v >> 48)
+ b[2] = byte(v >> 40)
+ b[3] = byte(v >> 32)
+ b[4] = byte(v >> 24)
+ b[5] = byte(v >> 16)
+ b[6] = byte(v >> 8)
+ b[7] = byte(v)
+}
}
// See EnableFIPS in cmd/internal/obj/fips.go for commentary.
+ // Also, js/wasm and windows/386 don't have good enough timers
+ // for the CPU jitter entropy source.
switch {
case runtime.GOARCH == "wasm",
runtime.GOOS == "windows" && runtime.GOARCH == "386",
}
}
- // Ensure that all packages except check and check's dependencies import check.
+ // Ensure that all packages except check, check's dependencies, and the
+ // entropy source (which is used only from .../fips140/drbg) import check.
for pkg := range allPackages {
switch pkg {
case "crypto/internal/fips140/check":
case "crypto/internal/fips140/sha3":
case "crypto/internal/fips140/sha256":
case "crypto/internal/fips140/sha512":
+ case "crypto/internal/fips140/entropy":
default:
if !importCheck[pkg] {
t.Errorf("package %s does not import crypto/internal/fips140/check", pkg)
--- /dev/null
+// 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.
+
+//go:build !windows
+
+package time
+
+import "time"
+
+var start = time.Now()
+
+// HighPrecisionNow returns a high-resolution timestamp suitable for measuring
+// small time differences. It uses the time package's monotonic clock.
+//
+// Its unit, epoch, and resolution are unspecified, and may change, but can be
+// assumed to be sufficiently precise to measure time differences on the order
+// of tens to hundreds of nanoseconds.
+func HighPrecisionNow() int64 {
+ return int64(time.Since(start))
+}
--- /dev/null
+// 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 time
+
+import "internal/syscall/windows"
+
+// HighPrecisionNow returns a high-resolution timestamp suitable for measuring
+// small time differences. It uses Windows' QueryPerformanceCounter.
+//
+// Its unit, epoch, and resolution are unspecified, and may change, but can be
+// assumed to be sufficiently precise to measure time differences on the order
+// of tens to hundreds of nanoseconds.
+func HighPrecisionNow() int64 {
+ return windows.QueryPerformanceCounter()
+}
--- /dev/null
+// 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.
+
+//go:build !fips140v1.0
+
+package fipstest
+
+import (
+ "bytes"
+ "crypto/internal/cryptotest"
+ "crypto/internal/fips140/drbg"
+ "crypto/internal/fips140/entropy"
+ "crypto/sha256"
+ "crypto/sha512"
+ "encoding/hex"
+ "flag"
+ "fmt"
+ "internal/testenv"
+ "io/fs"
+ "os"
+ "path/filepath"
+ "runtime"
+ "strings"
+ "testing"
+ "time"
+)
+
+var flagEntropySamples = flag.String("entropy-samples", "", "store entropy samples with the provided `suffix`")
+var flagNISTSP80090B = flag.Bool("nist-sp800-90b", false, "run NIST SP 800-90B tests (requires docker)")
+
+func TestEntropySamples(t *testing.T) {
+ cryptotest.MustSupportFIPS140(t)
+
+ var seqSamples [1_000_000]uint8
+ samplesOrTryAgain(t, seqSamples[:])
+ seqSamplesName := fmt.Sprintf("entropy_samples_sequential_%s_%s_%s_%s_%s.bin", entropy.Version(),
+ runtime.GOOS, runtime.GOARCH, *flagEntropySamples, time.Now().Format("20060102T150405Z"))
+ if *flagEntropySamples != "" {
+ if err := os.WriteFile(seqSamplesName, seqSamples[:], 0644); err != nil {
+ t.Fatalf("failed to write samples to %q: %v", seqSamplesName, err)
+ }
+ t.Logf("wrote %s", seqSamplesName)
+ }
+
+ var restartSamples [1000][1000]uint8
+ for i := range restartSamples {
+ var samples [1024]uint8
+ samplesOrTryAgain(t, samples[:])
+ copy(restartSamples[i][:], samples[:])
+ }
+ restartSamplesName := fmt.Sprintf("entropy_samples_restart_%s_%s_%s_%s_%s.bin", entropy.Version(),
+ runtime.GOOS, runtime.GOARCH, *flagEntropySamples, time.Now().Format("20060102T150405Z"))
+ if *flagEntropySamples != "" {
+ f, err := os.Create(restartSamplesName)
+ if err != nil {
+ t.Fatalf("failed to create %q: %v", restartSamplesName, err)
+ }
+ for i := range restartSamples {
+ if _, err := f.Write(restartSamples[i][:]); err != nil {
+ t.Fatalf("failed to write samples to %q: %v", restartSamplesName, err)
+ }
+ }
+ if err := f.Close(); err != nil {
+ t.Fatalf("failed to close %q: %v", restartSamplesName, err)
+ }
+ t.Logf("wrote %s", restartSamplesName)
+ }
+
+ if *flagNISTSP80090B {
+ if *flagEntropySamples == "" {
+ t.Fatalf("-nist-sp800-90b requires -entropy-samples to be set too")
+ }
+
+ // Check if the nist-sp800-90b docker image is already present,
+ // and build it otherwise.
+ if err := testenv.Command(t,
+ "docker", "image", "inspect", "nist-sp800-90b",
+ ).Run(); err != nil {
+ t.Logf("building nist-sp800-90b docker image")
+ dockerfile := filepath.Join(t.TempDir(), "Dockerfile.SP800-90B_EntropyAssessment")
+ if err := os.WriteFile(dockerfile, []byte(NISTSP80090BDockerfile), 0644); err != nil {
+ t.Fatalf("failed to write Dockerfile: %v", err)
+ }
+ out, err := testenv.Command(t,
+ "docker", "build", "-t", "nist-sp800-90b", "-f", dockerfile, "/var/empty",
+ ).CombinedOutput()
+ if err != nil {
+ t.Fatalf("failed to build nist-sp800-90b docker image: %v\n%s", err, out)
+ }
+ }
+
+ pwd, err := os.Getwd()
+ if err != nil {
+ t.Fatalf("failed to get current working directory: %v", err)
+ }
+ t.Logf("running ea_non_iid analysis")
+ out, err := testenv.Command(t,
+ "docker", "run", "--rm", "-v", fmt.Sprintf("%s:%s", pwd, pwd), "-w", pwd,
+ "nist-sp800-90b", "ea_non_iid", seqSamplesName, "8",
+ ).CombinedOutput()
+ if err != nil {
+ t.Fatalf("ea_non_iid failed: %v\n%s", err, out)
+ }
+ t.Logf("\n%s", out)
+
+ H_I := string(out)
+ H_I = strings.TrimSpace(H_I[strings.LastIndexByte(H_I, ' ')+1:])
+ t.Logf("running ea_restart analysis with H_I = %s", H_I)
+ out, err = testenv.Command(t,
+ "docker", "run", "--rm", "-v", fmt.Sprintf("%s:%s", pwd, pwd), "-w", pwd,
+ "nist-sp800-90b", "ea_restart", restartSamplesName, "8", H_I,
+ ).CombinedOutput()
+ if err != nil {
+ t.Fatalf("ea_restart failed: %v\n%s", err, out)
+ }
+ t.Logf("\n%s", out)
+ }
+}
+
+var NISTSP80090BDockerfile = `
+FROM ubuntu:24.04
+RUN apt-get update && apt-get install -y build-essential git \
+ libbz2-dev libdivsufsort-dev libjsoncpp-dev libgmp-dev libmpfr-dev libssl-dev \
+ && rm -rf /var/lib/apt/lists/*
+RUN git clone --depth 1 https://github.com/usnistgov/SP800-90B_EntropyAssessment.git
+RUN cd SP800-90B_EntropyAssessment && git checkout 8924f158c97e7b805e0f95247403ad4c44b9cd6f
+WORKDIR ./SP800-90B_EntropyAssessment/cpp/
+RUN make all
+RUN cd selftest && ./selftest
+RUN cp ea_non_iid ea_restart /usr/local/bin/
+`
+
+var memory entropy.ScratchBuffer
+
+// samplesOrTryAgain calls entropy.Samples up to 10 times until it succeeds.
+// Samples has a non-negligible chance of failing the health tests, as required
+// by SP 800-90B.
+func samplesOrTryAgain(t *testing.T, samples []uint8) {
+ t.Helper()
+ for range 10 {
+ if err := entropy.Samples(samples, &memory); err != nil {
+ t.Logf("entropy.Samples() failed: %v", err)
+ continue
+ }
+ return
+ }
+ t.Fatal("entropy.Samples() failed 10 times in a row")
+}
+
+func TestEntropySHA384(t *testing.T) {
+ var input [1024]uint8
+ for i := range input {
+ input[i] = uint8(i)
+ }
+ want := sha512.Sum384(input[:])
+ got := entropy.SHA384(&input)
+ if got != want {
+ t.Errorf("SHA384() = %x, want %x", got, want)
+ }
+}
+
+func TestEntropyRepetitionCountTest(t *testing.T) {
+ good := bytes.Repeat(append(bytes.Repeat([]uint8{42}, 40), 1), 100)
+ if err := entropy.RepetitionCountTest(good); err != nil {
+ t.Errorf("RepetitionCountTest(good) = %v, want nil", err)
+ }
+
+ bad := bytes.Repeat([]uint8{0}, 40)
+ bad = append(bad, bytes.Repeat([]uint8{1}, 40)...)
+ bad = append(bad, bytes.Repeat([]uint8{42}, 41)...)
+ bad = append(bad, bytes.Repeat([]uint8{2}, 40)...)
+ if err := entropy.RepetitionCountTest(bad); err == nil {
+ t.Error("RepetitionCountTest(bad) = nil, want error")
+ }
+
+ bad = bytes.Repeat([]uint8{42}, 41)
+ if err := entropy.RepetitionCountTest(bad); err == nil {
+ t.Error("RepetitionCountTest(bad) = nil, want error")
+ }
+}
+
+func TestEntropyAdaptiveProportionTest(t *testing.T) {
+ good := bytes.Repeat([]uint8{0}, 409)
+ good = append(good, bytes.Repeat([]uint8{1}, 512-409)...)
+ good = append(good, bytes.Repeat([]uint8{0}, 409)...)
+ if err := entropy.AdaptiveProportionTest(good); err != nil {
+ t.Errorf("AdaptiveProportionTest(good) = %v, want nil", err)
+ }
+
+ // These fall out of the window.
+ bad := bytes.Repeat([]uint8{1}, 100)
+ bad = append(bad, bytes.Repeat([]uint8{1, 2, 3, 4, 5, 6}, 100)...)
+ // These are in the window.
+ bad = append(bad, bytes.Repeat([]uint8{42}, 410)...)
+ if err := entropy.AdaptiveProportionTest(bad[:len(bad)-1]); err != nil {
+ t.Errorf("AdaptiveProportionTest(bad[:len(bad)-1]) = %v, want nil", err)
+ }
+ if err := entropy.AdaptiveProportionTest(bad); err == nil {
+ t.Error("AdaptiveProportionTest(bad) = nil, want error")
+ }
+}
+
+func TestEntropyUnchanged(t *testing.T) {
+ testenv.MustHaveSource(t)
+
+ h := sha256.New()
+ root := os.DirFS("../fips140/entropy")
+ if err := fs.WalkDir(root, ".", func(path string, d fs.DirEntry, err error) error {
+ if err != nil {
+ return err
+ }
+ if d.IsDir() {
+ return nil
+ }
+ data, err := fs.ReadFile(root, path)
+ if err != nil {
+ return err
+ }
+ t.Logf("Hashing %s (%d bytes)", path, len(data))
+ fmt.Fprintf(h, "%s %d\n", path, len(data))
+ h.Write(data)
+ return nil
+ }); err != nil {
+ t.Fatalf("WalkDir: %v", err)
+ }
+
+ // The crypto/internal/fips140/entropy package is certified as a FIPS 140-3
+ // entropy source through the Entropy Source Validation program,
+ // independently of the FIPS 140-3 module. It must not change even across
+ // FIPS 140-3 module versions, in order to reuse the ESV certificate.
+ exp := "35976eb8a11678c79777da07aaab5511d4325701f837777df205f6e7b20c6821"
+ if got := hex.EncodeToString(h.Sum(nil)); got != exp {
+ t.Errorf("hash of crypto/internal/fips140/entropy = %s, want %s", got, exp)
+ }
+}
+
+func TestEntropyRace(t *testing.T) {
+ // Check that concurrent calls to Seed don't trigger the race detector.
+ for range 2 {
+ go func() {
+ _, _ = entropy.Seed(&memory)
+ }()
+ }
+ // Same, with the higher-level DRBG. More concurrent calls to hit the Pool.
+ for range 16 {
+ go func() {
+ var b [64]byte
+ drbg.Read(b[:])
+ }()
+ }
+}
+
+var sink byte
+
+func BenchmarkEntropySeed(b *testing.B) {
+ for b.Loop() {
+ seed, err := entropy.Seed(&memory)
+ if err != nil {
+ b.Fatalf("entropy.Seed() failed: %v", err)
+ }
+ sink ^= seed[0]
+ }
+}
internal/byteorder < crypto/internal/fips140deps/byteorder;
internal/cpu, internal/goarch < crypto/internal/fips140deps/cpu;
internal/godebug < crypto/internal/fips140deps/godebug;
+ time, internal/syscall/windows < crypto/internal/fips140deps/time;
+
+ crypto/internal/fips140deps/time, errors, math/bits, sync/atomic, unsafe
+ < crypto/internal/fips140/entropy;
STR, hash,
crypto/internal/impl,
crypto/internal/entropy,
crypto/internal/randutil,
+ crypto/internal/fips140/entropy,
crypto/internal/fips140deps/byteorder,
crypto/internal/fips140deps/cpu,
crypto/internal/fips140deps/godebug