]> Cypherpunks repositories - gostls13.git/commitdiff
crypto/internal/fips140/entropy: add CPU jitter-based entropy source
authorFilippo Valsorda <filippo@golang.org>
Thu, 11 Sep 2025 19:04:05 +0000 (21:04 +0200)
committerGopher Robot <gobot@golang.org>
Mon, 29 Sep 2025 21:06:02 +0000 (14:06 -0700)
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>
src/crypto/internal/entropy/entropy.go
src/crypto/internal/fips140/drbg/rand.go
src/crypto/internal/fips140/entropy/entropy.go [new file with mode: 0644]
src/crypto/internal/fips140/entropy/sha384.go [new file with mode: 0644]
src/crypto/internal/fips140/fips140.go
src/crypto/internal/fips140deps/fipsdeps_test.go
src/crypto/internal/fips140deps/time/time.go [new file with mode: 0644]
src/crypto/internal/fips140deps/time/time_windows.go [new file with mode: 0644]
src/crypto/internal/fips140test/entropy_test.go [new file with mode: 0644]
src/go/build/deps_test.go

index 5319e9e47a7455ce0be87598e135c510564a331b..73fd5298007a11ba2628389957e0bd1a4bb8cf7d 100644 (file)
@@ -3,9 +3,11 @@
 // 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
index c1a3ea0ae658ff9b330527b38005189596527451..3ccb018e32604752fc857d915efa971596d73063 100644 (file)
@@ -9,21 +9,53 @@
 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())
        },
 }
 
@@ -44,8 +76,15 @@ func Read(b []byte) {
        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)
@@ -54,9 +93,7 @@ func Read(b []byte) {
                        // 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
                }
diff --git a/src/crypto/internal/fips140/entropy/entropy.go b/src/crypto/internal/fips140/entropy/entropy.go
new file mode 100644 (file)
index 0000000..273f05c
--- /dev/null
@@ -0,0 +1,202 @@
+// 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
+}
diff --git a/src/crypto/internal/fips140/entropy/sha384.go b/src/crypto/internal/fips140/entropy/sha384.go
new file mode 100644 (file)
index 0000000..ec23cfc
--- /dev/null
@@ -0,0 +1,191 @@
+// 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)
+}
index d03219b540e27fbd9c2b6b0f36ff4483e53c1486..76054b00684e2b4a3967a34971a53111ae865e78 100644 (file)
@@ -48,6 +48,8 @@ func Supported() error {
        }
 
        // 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",
index 2c3bc8184e71bc3fdb5414581f78ffbc74e505e7..97552dc1ce10f1ee0d3568c5bea8874436dfc2c2 100644 (file)
@@ -88,7 +88,8 @@ func TestImports(t *testing.T) {
                }
        }
 
-       // 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":
@@ -99,6 +100,7 @@ func TestImports(t *testing.T) {
                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)
diff --git a/src/crypto/internal/fips140deps/time/time.go b/src/crypto/internal/fips140deps/time/time.go
new file mode 100644 (file)
index 0000000..eea37b7
--- /dev/null
@@ -0,0 +1,21 @@
+// 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))
+}
diff --git a/src/crypto/internal/fips140deps/time/time_windows.go b/src/crypto/internal/fips140deps/time/time_windows.go
new file mode 100644 (file)
index 0000000..410ede4
--- /dev/null
@@ -0,0 +1,17 @@
+// 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()
+}
diff --git a/src/crypto/internal/fips140test/entropy_test.go b/src/crypto/internal/fips140test/entropy_test.go
new file mode 100644 (file)
index 0000000..76c2428
--- /dev/null
@@ -0,0 +1,264 @@
+// 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]
+       }
+}
index d50a98d34c9092c2efd2446599ea0a0d0436c463..c76b254b23ffc84608e9cd714907078590ed4bf1 100644 (file)
@@ -485,11 +485,16 @@ var depsRules = `
        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