diff --git a/src/crypto/internal/entropy/entropy.go b/src/crypto/internal/entropy/entropy.go index 5319e9e47a7..73fd5298007 100644 --- a/src/crypto/internal/entropy/entropy.go +++ b/src/crypto/internal/entropy/entropy.go @@ -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 diff --git a/src/crypto/internal/fips140/drbg/rand.go b/src/crypto/internal/fips140/drbg/rand.go index c1a3ea0ae65..3ccb018e326 100644 --- a/src/crypto/internal/fips140/drbg/rand.go +++ b/src/crypto/internal/fips140/drbg/rand.go @@ -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 index 00000000000..273f05c817a --- /dev/null +++ b/src/crypto/internal/fips140/entropy/entropy.go @@ -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 index 00000000000..ec23cfc9ad3 --- /dev/null +++ b/src/crypto/internal/fips140/entropy/sha384.go @@ -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) +} diff --git a/src/crypto/internal/fips140/fips140.go b/src/crypto/internal/fips140/fips140.go index d03219b540e..76054b00684 100644 --- a/src/crypto/internal/fips140/fips140.go +++ b/src/crypto/internal/fips140/fips140.go @@ -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", diff --git a/src/crypto/internal/fips140deps/fipsdeps_test.go b/src/crypto/internal/fips140deps/fipsdeps_test.go index 2c3bc8184e7..97552dc1ce1 100644 --- a/src/crypto/internal/fips140deps/fipsdeps_test.go +++ b/src/crypto/internal/fips140deps/fipsdeps_test.go @@ -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 index 00000000000..eea37b772e4 --- /dev/null +++ b/src/crypto/internal/fips140deps/time/time.go @@ -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 index 00000000000..410ede4ee91 --- /dev/null +++ b/src/crypto/internal/fips140deps/time/time_windows.go @@ -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 index 00000000000..76c24289520 --- /dev/null +++ b/src/crypto/internal/fips140test/entropy_test.go @@ -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] + } +} diff --git a/src/go/build/deps_test.go b/src/go/build/deps_test.go index d50a98d34c9..c76b254b23f 100644 --- a/src/go/build/deps_test.go +++ b/src/go/build/deps_test.go @@ -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