acvptool: seed key format for ML-DSA sigGen and ML-KEM decap ACVP-Server v1.1.0.43 adds the FIPS204-tr1 and FIPS203-tr1 testing revisions. These add a keyFormats registration property that lets an implementation under test receive the ML-DSA sigGen private key or the ML-KEM decapsulation key as a seed instead of in expanded form. Test groups with keyFormat "seed" are dispatched to new module wrapper commands, ML-DSA-XX/sigGen/seed and ML-KEM-XX/decap/seed. The seed for ML-KEM is d followed by z, as in keyGen. Groups with expanded keys use the existing commands unchanged. The BoringSSL module wrapper implements the new commands with the existing from-seed functions. The new test bz2 vectors/expected were fetched from the NIST demo server and the answers validated there successfully before trimming. Change-Id: I0826c01074f1d08a15eae14b3935964f82e9a749 Reviewed-on: https://boringssl-review.googlesource.com/c/boringssl/+/100087 SLSA-Policy-Verified: SLSA Policy Verification Service <devtools-gerritcodereview-exitgate@google.com> Commit-Queue: David Benjamin <davidben@google.com> Reviewed-by: Adam Langley <agl@google.com> Reviewed-by: David Benjamin <davidben@google.com>
diff --git a/util/fipstools/acvp/ACVP.md b/util/fipstools/acvp/ACVP.md index eb3cdfb..0056b9f 100644 --- a/util/fipstools/acvp/ACVP.md +++ b/util/fipstools/acvp/ACVP.md
@@ -138,10 +138,12 @@ | PBKDF | HMAC name, key length (bits), salt, password, iteration count | Derived key | | ML-DSA-XX/keyGen | Seed | Public key, private key | | ML-DSA-XX/sigGen | Private key, message, randomizer, context, mu | Signature | +| ML-DSA-XX/sigGen/seed | Seed, message, randomizer, context, mu | Signature | | ML-DSA-XX/sigVer | Public key, message, signature, context, mu | Single-byte validity flag | | ML-KEM-XX/keyGen | Seed | Public key, private key | | ML-KEM-XX/encap | Public key, entropy | Ciphertext, shared secret | | ML-KEM-XX/decap | Private key, ciphertext | Shared secret | +| ML-KEM-XX/decap/seed | Seed (d ‖ z), ciphertext | Shared secret | | SLH-DSA-XX/keyGen | Seed | Private key, public key | | SLH-DSA-XX/sigGen | Private key, message, entropy or empty | Signature | | SLH-DSA-XX/sigVer | Public key, message, signature | Single-byte validity flag |
diff --git a/util/fipstools/acvp/acvptool/subprocess/mldsa.go b/util/fipstools/acvp/acvptool/subprocess/mldsa.go index 73ebe3e..2f020f0 100644 --- a/util/fipstools/acvp/acvptool/subprocess/mldsa.go +++ b/util/fipstools/acvp/acvptool/subprocess/mldsa.go
@@ -59,6 +59,7 @@ ID uint64 `json:"tgId"` TestType string `json:"testType"` ParameterSet string `json:"parameterSet"` + KeyFormat string `json:"keyFormat"` Deterministic bool `json:"deterministic"` SignatureInterface string `json:"signatureInterface"` Tests []mldsaSigGenTest `json:"tests"` @@ -68,6 +69,7 @@ ID uint64 `json:"tcId"` Message string `json:"message"` PrivateKey string `json:"sk"` + Seed string `json:"seed"` Randomizer string `json:"rnd"` Context string `json:"context"` Mu string `json:"mu"` @@ -200,12 +202,25 @@ return nil, fmt.Errorf("invalid parameter set: %s", group.ParameterSet) } cmdName := group.ParameterSet + "/sigGen" + if group.KeyFormat == "seed" { + cmdName = group.ParameterSet + "/sigGen/seed" + } for _, test := range group.Tests { - sk, err := hex.DecodeString(test.PrivateKey) - if err != nil { - return nil, fmt.Errorf("failed to decode private key in test case %d/%d: %s", - group.ID, test.ID, err) + var sk []byte + var err error + if group.KeyFormat == "seed" { + sk, err = hex.DecodeString(test.Seed) + if err != nil { + return nil, fmt.Errorf("failed to decode seed in test case %d/%d: %s", + group.ID, test.ID, err) + } + } else { + sk, err = hex.DecodeString(test.PrivateKey) + if err != nil { + return nil, fmt.Errorf("failed to decode private key in test case %d/%d: %s", + group.ID, test.ID, err) + } } msg, err := hex.DecodeString(test.Message)
diff --git a/util/fipstools/acvp/acvptool/subprocess/mlkem.go b/util/fipstools/acvp/acvptool/subprocess/mlkem.go index 883fd48..2d98b0f 100644 --- a/util/fipstools/acvp/acvptool/subprocess/mlkem.go +++ b/util/fipstools/acvp/acvptool/subprocess/mlkem.go
@@ -59,6 +59,7 @@ TestType string `json:"testType"` ParameterSet string `json:"parameterSet"` Function string `json:"function"` + KeyFormat string `json:"keyFormat"` Tests []mlkemEncapDecapTest `json:"tests"` } @@ -66,6 +67,8 @@ ID uint64 `json:"tcId"` EK string `json:"ek,omitempty"` DK string `json:"dk,omitempty"` + D string `json:"d,omitempty"` + Z string `json:"z,omitempty"` M string `json:"m,omitempty"` C string `json:"c,omitempty"` } @@ -213,12 +216,33 @@ case "decapsulation": cmdName := group.ParameterSet + "/decap" + if group.KeyFormat == "seed" { + cmdName = group.ParameterSet + "/decap/seed" + } for _, test := range group.Tests { - dk, err := decodeNonEmptyHex(test.DK) - if err != nil { - return nil, fmt.Errorf("failed to decode dk in test case %d/%d: %s", - group.ID, test.ID, err) + var key []byte + if group.KeyFormat == "seed" { + d, err := decodeNonEmptyHex(test.D) + if err != nil { + return nil, fmt.Errorf("failed to decode d in test case %d/%d: %s", + group.ID, test.ID, err) + } + z, err := decodeNonEmptyHex(test.Z) + if err != nil { + return nil, fmt.Errorf("failed to decode z in test case %d/%d: %s", + group.ID, test.ID, err) + } + key = make([]byte, 0, len(d)+len(z)) + key = append(key, d...) + key = append(key, z...) + } else { + var err error + key, err = decodeNonEmptyHex(test.DK) + if err != nil { + return nil, fmt.Errorf("failed to decode dk in test case %d/%d: %s", + group.ID, test.ID, err) + } } c, err := decodeNonEmptyHex(test.C) @@ -227,7 +251,7 @@ group.ID, test.ID, err) } - result, err := t.Transact(cmdName, 1, dk, c) + result, err := t.Transact(cmdName, 1, key, c) if err != nil { return nil, fmt.Errorf("decapsulation failed for test case %d/%d: %s", group.ID, test.ID, err)
diff --git a/util/fipstools/acvp/acvptool/test/expected/ML-DSA-seed.bz2 b/util/fipstools/acvp/acvptool/test/expected/ML-DSA-seed.bz2 new file mode 100644 index 0000000..5d62ffa --- /dev/null +++ b/util/fipstools/acvp/acvptool/test/expected/ML-DSA-seed.bz2 Binary files differ
diff --git a/util/fipstools/acvp/acvptool/test/expected/ML-KEM-seed.bz2 b/util/fipstools/acvp/acvptool/test/expected/ML-KEM-seed.bz2 new file mode 100644 index 0000000..80bb011 --- /dev/null +++ b/util/fipstools/acvp/acvptool/test/expected/ML-KEM-seed.bz2 Binary files differ
diff --git a/util/fipstools/acvp/acvptool/test/tests.json b/util/fipstools/acvp/acvptool/test/tests.json index af41452..589402c 100644 --- a/util/fipstools/acvp/acvptool/test/tests.json +++ b/util/fipstools/acvp/acvptool/test/tests.json
@@ -26,7 +26,9 @@ {"Wrapper": "modulewrapper", "In": "vectors/KAS-FFC-SSC.bz2"}, {"Wrapper": "testmodulewrapper", "In": "vectors/KDF.bz2"}, {"Wrapper": "modulewrapper", "In": "vectors/ML-DSA.bz2", "Out": "expected/ML-DSA.bz2"}, +{"Wrapper": "modulewrapper", "In": "vectors/ML-DSA-seed.bz2", "Out": "expected/ML-DSA-seed.bz2"}, {"Wrapper": "modulewrapper", "In": "vectors/ML-KEM.bz2", "Out": "expected/ML-KEM.bz2"}, +{"Wrapper": "modulewrapper", "In": "vectors/ML-KEM-seed.bz2", "Out": "expected/ML-KEM-seed.bz2"}, {"Wrapper": "modulewrapper", "In": "vectors/RSA.bz2", "Out": "expected/RSA.bz2"}, {"Wrapper": "modulewrapper", "In": "vectors/SHA-1.bz2", "Out": "expected/SHA-1.bz2"}, {"Wrapper": "modulewrapper", "In": "vectors/SHA2-224.bz2", "Out": "expected/SHA2-224.bz2"},
diff --git a/util/fipstools/acvp/acvptool/test/vectors/ML-DSA-seed.bz2 b/util/fipstools/acvp/acvptool/test/vectors/ML-DSA-seed.bz2 new file mode 100644 index 0000000..fb11f0e --- /dev/null +++ b/util/fipstools/acvp/acvptool/test/vectors/ML-DSA-seed.bz2 Binary files differ
diff --git a/util/fipstools/acvp/acvptool/test/vectors/ML-KEM-seed.bz2 b/util/fipstools/acvp/acvptool/test/vectors/ML-KEM-seed.bz2 new file mode 100644 index 0000000..cb49cd1 --- /dev/null +++ b/util/fipstools/acvp/acvptool/test/vectors/ML-KEM-seed.bz2 Binary files differ
diff --git a/util/fipstools/acvp/modulewrapper/modulewrapper.cc b/util/fipstools/acvp/modulewrapper/modulewrapper.cc index b3d5e75..83351b7 100644 --- a/util/fipstools/acvp/modulewrapper/modulewrapper.cc +++ b/util/fipstools/acvp/modulewrapper/modulewrapper.cc
@@ -2178,22 +2178,15 @@ } template <typename PrivateKey, size_t SignatureBytes, - bcm_status (*ParsePrivateKey)(PrivateKey *, CBS *), bcm_status (*SignInternal)(uint8_t *, const PrivateKey *, const uint8_t *, size_t, const uint8_t *, size_t, const uint8_t *, size_t, const uint8_t *), bcm_status (*SignMuInternal)(uint8_t *, const PrivateKey *, const uint8_t *, const uint8_t *)> -static bool MLDSASigGen(const Span<const uint8_t> args[], - ReplyCallback write_reply) { - CBS cbs = args[0]; - auto priv = std::make_unique<PrivateKey>(); - if (ParsePrivateKey(priv.get(), &cbs) != bcm_status::approved) { - LOG_ERROR("Failed to parse ML-DSA private key.\n"); - return false; - } - +static bool MLDSASigGenWithKey(const PrivateKey *priv, + const Span<const uint8_t> args[], + ReplyCallback write_reply) { const Span<const uint8_t> msg = args[1]; const Span<const uint8_t> randomizer = args[2]; const Span<const uint8_t> context = args[3]; @@ -2216,12 +2209,12 @@ uint8_t signature[SignatureBytes]; if (mu.size() != 0) { - if (SignMuInternal(signature, priv.get(), mu.data(), randomizer.data()) != + if (SignMuInternal(signature, priv, mu.data(), randomizer.data()) != bcm_status::approved) { LOG_ERROR("ML-DSA mu-signing failed.\n"); return false; } - } else if (SignInternal(signature, priv.get(), msg.data(), msg.size(), + } else if (SignInternal(signature, priv, msg.data(), msg.size(), // It's not just an empty context, the context // prefix is omitted too. nullptr, 0, nullptr, 0, @@ -2233,6 +2226,53 @@ return write_reply({signature}); } +template <typename PrivateKey, size_t SignatureBytes, + bcm_status (*ParsePrivateKey)(PrivateKey *, CBS *), + bcm_status (*SignInternal)(uint8_t *, const PrivateKey *, + const uint8_t *, size_t, const uint8_t *, + size_t, const uint8_t *, size_t, + const uint8_t *), + bcm_status (*SignMuInternal)(uint8_t *, const PrivateKey *, + const uint8_t *, const uint8_t *)> +static bool MLDSASigGen(const Span<const uint8_t> args[], + ReplyCallback write_reply) { + CBS cbs = args[0]; + auto priv = std::make_unique<PrivateKey>(); + if (ParsePrivateKey(priv.get(), &cbs) != bcm_status::approved) { + LOG_ERROR("Failed to parse ML-DSA private key.\n"); + return false; + } + + return MLDSASigGenWithKey<PrivateKey, SignatureBytes, SignInternal, + SignMuInternal>(priv.get(), args, write_reply); +} + +template <typename PrivateKey, size_t SignatureBytes, + bcm_status (*PrivateKeyFromSeed)(PrivateKey *, const uint8_t *), + bcm_status (*SignInternal)(uint8_t *, const PrivateKey *, + const uint8_t *, size_t, const uint8_t *, + size_t, const uint8_t *, size_t, + const uint8_t *), + bcm_status (*SignMuInternal)(uint8_t *, const PrivateKey *, + const uint8_t *, const uint8_t *)> +static bool MLDSASigGenSeed(const Span<const uint8_t> args[], + ReplyCallback write_reply) { + const Span<const uint8_t> seed = args[0]; + if (seed.size() != MLDSA_SEED_BYTES) { + LOG_ERROR("Bad seed size.\n"); + return false; + } + + auto priv = std::make_unique<PrivateKey>(); + if (PrivateKeyFromSeed(priv.get(), seed.data()) != bcm_status::approved) { + LOG_ERROR("ML-DSA private key from seed failed.\n"); + return false; + } + + return MLDSASigGenWithKey<PrivateKey, SignatureBytes, SignInternal, + SignMuInternal>(priv.get(), args, write_reply); +} + template <typename PublicKey, size_t SignatureBytes, bcm_status (*ParsePublicKey)(PublicKey *, CBS *), bcm_status (*VerifyInternal)(const PublicKey *, const uint8_t *, @@ -2362,6 +2402,31 @@ return write_reply({shared_secret}); } +template < + typename PrivateKey, + bcm_status (*PrivateKeyFromSeed)(PrivateKey *, const uint8_t *, size_t), + bcm_status (*Decap)(uint8_t *, const uint8_t *, size_t, const PrivateKey *)> +static bool MLKEMDecapSeed(const Span<const uint8_t> args[], + ReplyCallback write_reply) { + const Span<const uint8_t> seed = args[0]; + const Span<const uint8_t> ciphertext = args[1]; + + auto priv = std::make_unique<PrivateKey>(); + if (!bcm_success(PrivateKeyFromSeed(priv.get(), seed.data(), seed.size()))) { + LOG_ERROR("Failed to derive private key from seed.\n"); + return false; + } + + uint8_t shared_secret[MLKEM_SHARED_SECRET_BYTES]; + if (!bcm_success(Decap(shared_secret, ciphertext.data(), ciphertext.size(), + priv.get()))) { + LOG_ERROR("ML-KEM decapsulation failed.\n"); + return false; + } + + return write_reply({shared_secret}); +} + template <typename PublicKey, bcm_status (*ParsePublic)(PublicKey *, CBS *)> static bool MLKEMEncapKeyCheck(const Span<const uint8_t> args[], ReplyCallback write_reply) { @@ -2584,6 +2649,18 @@ MLDSASigGen<MLDSA87_private_key, MLDSA87_SIGNATURE_BYTES, BCM_mldsa87_parse_private_key, BCM_mldsa87_sign_internal, BCM_mldsa87_sign_mu_internal>}, + {"ML-DSA-44/sigGen/seed", 5, + MLDSASigGenSeed<MLDSA44_private_key, MLDSA44_SIGNATURE_BYTES, + BCM_mldsa44_private_key_from_seed_fips, + BCM_mldsa44_sign_internal, BCM_mldsa44_sign_mu_internal>}, + {"ML-DSA-65/sigGen/seed", 5, + MLDSASigGenSeed<MLDSA65_private_key, MLDSA65_SIGNATURE_BYTES, + BCM_mldsa65_private_key_from_seed_fips, + BCM_mldsa65_sign_internal, BCM_mldsa65_sign_mu_internal>}, + {"ML-DSA-87/sigGen/seed", 5, + MLDSASigGenSeed<MLDSA87_private_key, MLDSA87_SIGNATURE_BYTES, + BCM_mldsa87_private_key_from_seed_fips, + BCM_mldsa87_sign_internal, BCM_mldsa87_sign_mu_internal>}, {"ML-DSA-44/sigVer", 5, MLDSASigVer<MLDSA44_public_key, MLDSA44_SIGNATURE_BYTES, BCM_mldsa44_parse_public_key, BCM_mldsa44_verify_internal, @@ -2618,6 +2695,12 @@ {"ML-KEM-1024/decap", 2, MLKEMDecap<MLKEM1024_private_key, BCM_mlkem1024_parse_private_key, BCM_mlkem1024_decap>}, + {"ML-KEM-768/decap/seed", 2, + MLKEMDecapSeed<MLKEM768_private_key, BCM_mlkem768_private_key_from_seed, + BCM_mlkem768_decap>}, + {"ML-KEM-1024/decap/seed", 2, + MLKEMDecapSeed<MLKEM1024_private_key, BCM_mlkem1024_private_key_from_seed, + BCM_mlkem1024_decap>}, {"ML-KEM-768/encapKeyCheck", 1, MLKEMEncapKeyCheck<MLKEM768_public_key, BCM_mlkem768_parse_public_key>}, {"ML-KEM-1024/encapKeyCheck", 1,