crypto/internal/fips140test: add ctr DRBG ACVP tests

Adds ACVP test coverage for the SP 800-90A rev 1 ctrDRBG algorithm based
on the NIST spec:
  https://pages.nist.gov/ACVP/draft-vassilev-acvp-drbg.html#section-7.2

The implementation in our FIPS module is a minimal implementation
tailored to the specific needs of stdlib crypto. As a result we
customize the ACVP capability registration so that:

* predResistanceEnabled is false
* only mode AES-256 is supported
* for that mode,
  * derFuncEnabled is false
  * persoStringLen is 0 to disable personalization
  * additionalInputLen is 384 to match the [48]byte argument in our API

Other capability values are chosen based on Table 4's ctrDRBG AES-256
w/o `derFuncEnabled` row:
  https://pages.nist.gov/ACVP/draft-vassilev-acvp-drbg.html#section-7.4

We do enable reseed in the capability, necessitating two acvptool
commands: one that expects only 6 args and doesn't reseed
("ctrDRBG/AES-256"), and one that expects 8 args and does
("ctrDRBG-reseed/AES-256").

Updates #69642

Change-Id: I0f01a2f9496f45b130ee7d10916708093236f473
Reviewed-on: https://go-review.googlesource.com/c/go/+/639795
Reviewed-by: Roland Shoemaker <roland@golang.org>
Reviewed-by: Dmitri Shuralyov <dmitshur@google.com>
Reviewed-by: Filippo Valsorda <filippo@golang.org>
Auto-Submit: Filippo Valsorda <filippo@golang.org>
LUCI-TryBot-Result: Go LUCI <golang-scoped@luci-project-accounts.iam.gserviceaccount.com>
This commit is contained in:
Daniel McCarney 2025-01-02 15:09:39 -05:00 committed by Gopher Robot
parent 283296195b
commit 0bc57a3e7f
3 changed files with 158 additions and 34 deletions

View file

@ -44,6 +44,8 @@
{"algorithm":"hmacDRBG","revision":"1.0","predResistanceEnabled":[false],"reseedImplemented":false,"capabilities":[{"mode":"SHA3-384","derFuncEnabled":false,"entropyInputLen":[256],"nonceLen":[128],"persoStringLen":[256],"additionalInputLen":[0],"returnedBitsLen":384}]}, {"algorithm":"hmacDRBG","revision":"1.0","predResistanceEnabled":[false],"reseedImplemented":false,"capabilities":[{"mode":"SHA3-384","derFuncEnabled":false,"entropyInputLen":[256],"nonceLen":[128],"persoStringLen":[256],"additionalInputLen":[0],"returnedBitsLen":384}]},
{"algorithm":"hmacDRBG","revision":"1.0","predResistanceEnabled":[false],"reseedImplemented":false,"capabilities":[{"mode":"SHA3-512","derFuncEnabled":false,"entropyInputLen":[256],"nonceLen":[128],"persoStringLen":[256],"additionalInputLen":[0],"returnedBitsLen":512}]}, {"algorithm":"hmacDRBG","revision":"1.0","predResistanceEnabled":[false],"reseedImplemented":false,"capabilities":[{"mode":"SHA3-512","derFuncEnabled":false,"entropyInputLen":[256],"nonceLen":[128],"persoStringLen":[256],"additionalInputLen":[0],"returnedBitsLen":512}]},
{"algorithm":"ctrDRBG","revision":"1.0","predResistanceEnabled":[false],"reseedImplemented":true,"capabilities":[{"mode":"AES-256","derFuncEnabled":false,"entropyInputLen":[384],"nonceLen":[0],"persoStringLen":[0],"additionalInputLen":[384],"returnedBitsLen":128}]},
{"algorithm":"EDDSA","mode":"keyGen","revision":"1.0","curve":["ED-25519"]}, {"algorithm":"EDDSA","mode":"keyGen","revision":"1.0","curve":["ED-25519"]},
{"algorithm":"EDDSA","mode":"keyVer","revision":"1.0","curve":["ED-25519"]}, {"algorithm":"EDDSA","mode":"keyVer","revision":"1.0","curve":["ED-25519"]},
{"algorithm":"EDDSA","mode":"sigGen","revision":"1.0","pure":true,"preHash":true,"contextLength":[{"min":0,"max":255,"increment":1}],"curve":["ED-25519"]}, {"algorithm":"EDDSA","mode":"sigGen","revision":"1.0","pure":true,"preHash":true,"contextLength":[{"min":0,"max":255,"increment":1}],"curve":["ED-25519"]},

View file

@ -34,6 +34,8 @@
{"Wrapper": "go", "In": "vectors/hmacDRBG.bz2", "Out": "expected/hmacDRBG.bz2"}, {"Wrapper": "go", "In": "vectors/hmacDRBG.bz2", "Out": "expected/hmacDRBG.bz2"},
{"Wrapper": "go", "In": "vectors/ctrDRBG.bz2", "Out": "expected/ctrDRBG.bz2"},
{"Wrapper": "go", "In": "vectors/EDDSA.bz2", "Out": "expected/EDDSA.bz2"}, {"Wrapper": "go", "In": "vectors/EDDSA.bz2", "Out": "expected/EDDSA.bz2"},
{"Wrapper": "go", "In": "vectors/ECDSA.bz2", "Out": "expected/ECDSA.bz2"}, {"Wrapper": "go", "In": "vectors/ECDSA.bz2", "Out": "expected/ECDSA.bz2"},
@ -46,5 +48,6 @@
{"Wrapper": "go", "In": "vectors/TLS-v1.2.bz2", "Out": "expected/TLS-v1.2.bz2"}, {"Wrapper": "go", "In": "vectors/TLS-v1.2.bz2", "Out": "expected/TLS-v1.2.bz2"},
{"Wrapper": "go", "In": "vectors/TLS-v1.3.bz2", "Out": "expected/TLS-v1.3.bz2"}, {"Wrapper": "go", "In": "vectors/TLS-v1.3.bz2", "Out": "expected/TLS-v1.3.bz2"},
{"Wrapper": "go", "In": "vectors/kdf-components.bz2", "Out": "expected/kdf-components.bz2"} {"Wrapper": "go", "In": "vectors/kdf-components.bz2", "Out": "expected/kdf-components.bz2"}
] ]

View file

@ -26,6 +26,7 @@ import (
"crypto/internal/fips140" "crypto/internal/fips140"
"crypto/internal/fips140/aes" "crypto/internal/fips140/aes"
"crypto/internal/fips140/aes/gcm" "crypto/internal/fips140/aes/gcm"
"crypto/internal/fips140/drbg"
"crypto/internal/fips140/ecdh" "crypto/internal/fips140/ecdh"
"crypto/internal/fips140/ecdsa" "crypto/internal/fips140/ecdsa"
"crypto/internal/fips140/ed25519" "crypto/internal/fips140/ed25519"
@ -125,7 +126,9 @@ var (
// SSH KDF algorithm capabilities: // SSH KDF algorithm capabilities:
// https://pages.nist.gov/ACVP/draft-celi-acvp-kdf-ssh.html#section-7.2 // https://pages.nist.gov/ACVP/draft-celi-acvp-kdf-ssh.html#section-7.2
// ECDH algorithm capabilities: // ECDH algorithm capabilities:
// https://pages.nist.gov/ACVP/draft-hammett-acvp-kas-ssc-ecc.html // https://pages.nist.gov/ACVP/draft-hammett-acvp-kas-ssc-ecc.html#section-7.3
// HMAC DRBG and CTR DRBG algorithm capabilities:
// https://pages.nist.gov/ACVP/draft-vassilev-acvp-drbg.html#section-7.2
//go:embed acvp_capabilities.json //go:embed acvp_capabilities.json
capabilitiesJson []byte capabilitiesJson []byte
@ -259,6 +262,9 @@ var (
"ECDH/P-256": cmdEcdhAftVal(ecdh.P256()), "ECDH/P-256": cmdEcdhAftVal(ecdh.P256()),
"ECDH/P-384": cmdEcdhAftVal(ecdh.P384()), "ECDH/P-384": cmdEcdhAftVal(ecdh.P384()),
"ECDH/P-521": cmdEcdhAftVal(ecdh.P521()), "ECDH/P-521": cmdEcdhAftVal(ecdh.P521()),
"ctrDRBG/AES-256": cmdCtrDrbgAft(),
"ctrDRBG-reseed/AES-256": cmdCtrDrbgReseedAft(),
} }
) )
@ -1103,39 +1109,6 @@ func cmdMlKem1024DecapAft() command {
} }
} }
func cmdHmacDrbgAft(h func() fips140.Hash) command {
return command{
requiredArgs: 6, // Output length, entropy, personalization, ad1, ad2, nonce
handler: func(args [][]byte) ([][]byte, error) {
outLen := binary.LittleEndian.Uint32(args[0])
entropy := args[1]
personalization := args[2]
ad1 := args[3]
ad2 := args[4]
nonce := args[5]
// Our capabilities describe no additional data support.
if len(ad1) != 0 || len(ad2) != 0 {
return nil, errors.New("additional data not supported")
}
// Our capabilities describe no prediction resistance (requires reseed) and no reseed.
// So the test procedure is:
// * Instantiate DRBG
// * Generate but don't output
// * Generate output
// * Uninstantiate
// See Table 7 in draft-vassilev-acvp-drbg
out := make([]byte, outLen)
drbg := ecdsa.TestingOnlyNewDRBG(h, entropy, nonce, personalization)
drbg.Generate(out)
drbg.Generate(out)
return [][]byte{out}, nil
},
}
}
func lookupCurve(name string) (elliptic.Curve, error) { func lookupCurve(name string) (elliptic.Curve, error) {
var c elliptic.Curve var c elliptic.Curve
@ -1460,6 +1433,152 @@ func cmdEcdhAftVal[P ecdh.Point[P]](curve *ecdh.Curve[P]) command {
} }
} }
func cmdHmacDrbgAft(h func() fips140.Hash) command {
return command{
requiredArgs: 6, // Output length, entropy, personalization, ad1, ad2, nonce
handler: func(args [][]byte) ([][]byte, error) {
outLen := binary.LittleEndian.Uint32(args[0])
entropy := args[1]
personalization := args[2]
ad1 := args[3]
ad2 := args[4]
nonce := args[5]
// Our capabilities describe no additional data support.
if len(ad1) != 0 || len(ad2) != 0 {
return nil, errors.New("additional data not supported")
}
// Our capabilities describe no prediction resistance (requires reseed) and no reseed.
// So the test procedure is:
// * Instantiate DRBG
// * Generate but don't output
// * Generate output
// * Uninstantiate
// See Table 7 in draft-vassilev-acvp-drbg
out := make([]byte, outLen)
drbg := ecdsa.TestingOnlyNewDRBG(h, entropy, nonce, personalization)
drbg.Generate(out)
drbg.Generate(out)
return [][]byte{out}, nil
},
}
}
func cmdCtrDrbgAft() command {
return command{
requiredArgs: 6, // Output length, entropy, personalization, ad1, ad2, nonce
handler: func(args [][]byte) ([][]byte, error) {
return acvpCtrDrbg{
outLen: binary.LittleEndian.Uint32(args[0]),
entropy: args[1],
personalization: args[2],
ad1: args[3],
ad2: args[4],
nonce: args[5],
}.process()
},
}
}
func cmdCtrDrbgReseedAft() command {
return command{
requiredArgs: 8, // Output length, entropy, personalization, reseedAD, reseedEntropy, ad1, ad2, nonce
handler: func(args [][]byte) ([][]byte, error) {
return acvpCtrDrbg{
outLen: binary.LittleEndian.Uint32(args[0]),
entropy: args[1],
personalization: args[2],
reseedAd: args[3],
reseedEntropy: args[4],
ad1: args[5],
ad2: args[6],
nonce: args[7],
}.process()
},
}
}
type acvpCtrDrbg struct {
outLen uint32
entropy []byte
personalization []byte
ad1 []byte
ad2 []byte
nonce []byte
reseedAd []byte // May be empty for no reseed
reseedEntropy []byte // May be empty for no reseed
}
func (args acvpCtrDrbg) process() ([][]byte, error) {
// Our capability describes no personalization support.
if len(args.personalization) > 0 {
return nil, errors.New("personalization string not supported")
}
// Our capability describes no derivation function support, so the nonce
// should be empty.
if len(args.nonce) > 0 {
return nil, errors.New("unexpected nonce value")
}
// Our capability describes entropy input len of 384 bits.
entropy, err := require48Bytes(args.entropy)
if err != nil {
return nil, fmt.Errorf("entropy: %w", err)
}
// Our capability describes additional input len of 384 bits.
ad1, err := require48Bytes(args.ad1)
if err != nil {
return nil, fmt.Errorf("AD1: %w", err)
}
ad2, err := require48Bytes(args.ad2)
if err != nil {
return nil, fmt.Errorf("AD2: %w", err)
}
withReseed := len(args.reseedAd) > 0
var reseedAd, reseedEntropy *[48]byte
if withReseed {
// Ditto RE: entropy and additional data lengths for reseeding.
if reseedAd, err = require48Bytes(args.reseedAd); err != nil {
return nil, fmt.Errorf("reseed AD: %w", err)
}
if reseedEntropy, err = require48Bytes(args.reseedEntropy); err != nil {
return nil, fmt.Errorf("reseed entropy: %w", err)
}
}
// Our capabilities describe no prediction resistance and allow both
// reseed and no reseed, so the test procedure is:
// * Instantiate DRBG
// * Reseed (if enabled)
// * Generate but don't output
// * Generate output
// * Uninstantiate
// See Table 7 in draft-vassilev-acvp-drbg
out := make([]byte, args.outLen)
ctrDrbg := drbg.NewCounter(entropy)
if withReseed {
ctrDrbg.Reseed(reseedEntropy, reseedAd)
}
ctrDrbg.Generate(out, ad1)
ctrDrbg.Generate(out, ad2)
return [][]byte{out}, nil
}
// Verify input is 48 byte slice, and cast it to a pointer to a fixed-size array
// of 48 bytes, or return an error.
func require48Bytes(input []byte) (*[48]byte, error) {
if inputLen := len(input); inputLen != 48 {
return nil, fmt.Errorf("invalid length: %d", inputLen)
}
return (*[48]byte)(input), nil
}
func TestACVP(t *testing.T) { func TestACVP(t *testing.T) {
testenv.SkipIfShortAndSlow(t) testenv.SkipIfShortAndSlow(t)