mirror of
https://github.com/golang/go.git
synced 2025-12-08 06:10:04 +00:00
crypto/internal/fips140/entropy: add CPU jitter-based entropy source
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>
This commit is contained in:
parent
db4fade759
commit
fc88e18b4a
10 changed files with 758 additions and 15 deletions
|
|
@ -3,9 +3,11 @@
|
||||||
// license that can be found in the LICENSE file.
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
// Package entropy provides the passive entropy source for the FIPS 140-3
|
// 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):
|
// 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
|
// "A software module that contains an approved DRBG that receives a LOAD
|
||||||
// command (or its logical equivalent) with entropy obtained from [...] inside
|
// command (or its logical equivalent) with entropy obtained from [...] inside
|
||||||
|
|
|
||||||
|
|
@ -9,21 +9,53 @@
|
||||||
package drbg
|
package drbg
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"crypto/internal/entropy"
|
|
||||||
"crypto/internal/fips140"
|
"crypto/internal/fips140"
|
||||||
|
"crypto/internal/fips140/entropy"
|
||||||
"crypto/internal/randutil"
|
"crypto/internal/randutil"
|
||||||
"crypto/internal/sysrand"
|
"crypto/internal/sysrand"
|
||||||
"io"
|
"io"
|
||||||
"sync"
|
"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 {
|
New: func() any {
|
||||||
var c *Counter
|
return NewCounter(getEntropy())
|
||||||
entropy.Depleted(func(seed *[48]byte) {
|
|
||||||
c = NewCounter(seed)
|
|
||||||
})
|
|
||||||
return c
|
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -44,8 +76,15 @@ func Read(b []byte) {
|
||||||
additionalInput := new([SeedSize]byte)
|
additionalInput := new([SeedSize]byte)
|
||||||
sysrand.Read(additionalInput[:16])
|
sysrand.Read(additionalInput[:16])
|
||||||
|
|
||||||
drbg := drbgs.Get().(*Counter)
|
drbg := drbgInstance.Swap(nil)
|
||||||
defer drbgs.Put(drbg)
|
if drbg == nil {
|
||||||
|
drbg = drbgPool.Get().(*Counter)
|
||||||
|
}
|
||||||
|
defer func() {
|
||||||
|
if !drbgInstance.CompareAndSwap(nil, drbg) {
|
||||||
|
drbgPool.Put(drbg)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
for len(b) > 0 {
|
for len(b) > 0 {
|
||||||
size := min(len(b), maxRequestSize)
|
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
|
// Section 9.3.2: if Generate reports a reseed is required, the
|
||||||
// additional input is passed to Reseed along with the entropy and
|
// additional input is passed to Reseed along with the entropy and
|
||||||
// then nulled before the next Generate call.
|
// then nulled before the next Generate call.
|
||||||
entropy.Depleted(func(seed *[48]byte) {
|
drbg.Reseed(getEntropy(), additionalInput)
|
||||||
drbg.Reseed(seed, additionalInput)
|
|
||||||
})
|
|
||||||
additionalInput = nil
|
additionalInput = nil
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
|
||||||
202
src/crypto/internal/fips140/entropy/entropy.go
Normal file
202
src/crypto/internal/fips140/entropy/entropy.go
Normal file
|
|
@ -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
|
||||||
|
}
|
||||||
191
src/crypto/internal/fips140/entropy/sha384.go
Normal file
191
src/crypto/internal/fips140/entropy/sha384.go
Normal file
|
|
@ -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)
|
||||||
|
}
|
||||||
|
|
@ -48,6 +48,8 @@ func Supported() error {
|
||||||
}
|
}
|
||||||
|
|
||||||
// See EnableFIPS in cmd/internal/obj/fips.go for commentary.
|
// 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 {
|
switch {
|
||||||
case runtime.GOARCH == "wasm",
|
case runtime.GOARCH == "wasm",
|
||||||
runtime.GOOS == "windows" && runtime.GOARCH == "386",
|
runtime.GOOS == "windows" && runtime.GOARCH == "386",
|
||||||
|
|
|
||||||
|
|
@ -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 {
|
for pkg := range allPackages {
|
||||||
switch pkg {
|
switch pkg {
|
||||||
case "crypto/internal/fips140/check":
|
case "crypto/internal/fips140/check":
|
||||||
|
|
@ -99,6 +100,7 @@ func TestImports(t *testing.T) {
|
||||||
case "crypto/internal/fips140/sha3":
|
case "crypto/internal/fips140/sha3":
|
||||||
case "crypto/internal/fips140/sha256":
|
case "crypto/internal/fips140/sha256":
|
||||||
case "crypto/internal/fips140/sha512":
|
case "crypto/internal/fips140/sha512":
|
||||||
|
case "crypto/internal/fips140/entropy":
|
||||||
default:
|
default:
|
||||||
if !importCheck[pkg] {
|
if !importCheck[pkg] {
|
||||||
t.Errorf("package %s does not import crypto/internal/fips140/check", pkg)
|
t.Errorf("package %s does not import crypto/internal/fips140/check", pkg)
|
||||||
|
|
|
||||||
21
src/crypto/internal/fips140deps/time/time.go
Normal file
21
src/crypto/internal/fips140deps/time/time.go
Normal file
|
|
@ -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))
|
||||||
|
}
|
||||||
17
src/crypto/internal/fips140deps/time/time_windows.go
Normal file
17
src/crypto/internal/fips140deps/time/time_windows.go
Normal file
|
|
@ -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()
|
||||||
|
}
|
||||||
264
src/crypto/internal/fips140test/entropy_test.go
Normal file
264
src/crypto/internal/fips140test/entropy_test.go
Normal file
|
|
@ -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]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -485,11 +485,16 @@ var depsRules = `
|
||||||
internal/byteorder < crypto/internal/fips140deps/byteorder;
|
internal/byteorder < crypto/internal/fips140deps/byteorder;
|
||||||
internal/cpu, internal/goarch < crypto/internal/fips140deps/cpu;
|
internal/cpu, internal/goarch < crypto/internal/fips140deps/cpu;
|
||||||
internal/godebug < crypto/internal/fips140deps/godebug;
|
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,
|
STR, hash,
|
||||||
crypto/internal/impl,
|
crypto/internal/impl,
|
||||||
crypto/internal/entropy,
|
crypto/internal/entropy,
|
||||||
crypto/internal/randutil,
|
crypto/internal/randutil,
|
||||||
|
crypto/internal/fips140/entropy,
|
||||||
crypto/internal/fips140deps/byteorder,
|
crypto/internal/fips140deps/byteorder,
|
||||||
crypto/internal/fips140deps/cpu,
|
crypto/internal/fips140deps/cpu,
|
||||||
crypto/internal/fips140deps/godebug
|
crypto/internal/fips140deps/godebug
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue