ML-KEM: add ACVP support. Change-Id: I934bf60900ffe1f318e5929072844b26f4c35e44 Reviewed-on: https://boringssl-review.googlesource.com/c/boringssl/+/73667 Reviewed-by: Bob Beck <bbe@google.com> Commit-Queue: Adam Langley <agl@google.com>
diff --git a/util/fipstools/acvp/ACVP.md b/util/fipstools/acvp/ACVP.md index 9e1663a..1004c84 100644 --- a/util/fipstools/acvp/ACVP.md +++ b/util/fipstools/acvp/ACVP.md
@@ -133,6 +133,9 @@ | ML-DSA-XX/keyGen | Seed | Public key, private key | | ML-DSA-XX/sigGen | Private key, message, randomizer | Signature | | ML-DSA-XX/sigVer | Public key, message, signature | 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 | ¹ The iterated tests would result in excessive numbers of round trips if the module wrapper handled only basic operations. Thus some ACVP logic is pushed down for these tests so that the inner loop can be handled locally. Either read the NIST documentation ([block-ciphers](https://pages.nist.gov/ACVP/draft-celi-acvp-symmetric.html#name-monte-carlo-tests-for-block) [hashes](https://pages.nist.gov/ACVP/draft-celi-acvp-sha.html#name-monte-carlo-tests-for-sha-1)) to understand the iteration count and return values or, probably more fruitfully, see how these functions are handled in the `modulewrapper` directory.
diff --git a/util/fipstools/acvp/acvptool/subprocess/mlkem.go b/util/fipstools/acvp/acvptool/subprocess/mlkem.go new file mode 100644 index 0000000..e9fd45d --- /dev/null +++ b/util/fipstools/acvp/acvptool/subprocess/mlkem.go
@@ -0,0 +1,236 @@ +package subprocess + +import ( + "encoding/hex" + "encoding/json" + "fmt" + "strings" +) + +// Common top-level structure to parse mode +type mlkemTestVectorSet struct { + Algorithm string `json:"algorithm"` + Mode string `json:"mode"` + Revision string `json:"revision"` +} + +// Key generation specific structures +type mlkemKeyGenTestVectorSet struct { + Algorithm string `json:"algorithm"` + Mode string `json:"mode"` + Revision string `json:"revision"` + Groups []mlkemKeyGenTestGroup `json:"testGroups"` +} + +type mlkemKeyGenTestGroup struct { + ID uint64 `json:"tgId"` + TestType string `json:"testType"` + ParameterSet string `json:"parameterSet"` + Tests []mlkemKeyGenTest `json:"tests"` +} + +type mlkemKeyGenTest struct { + ID uint64 `json:"tcId"` + Z string `json:"z"` + D string `json:"d"` +} + +type mlkemKeyGenTestGroupResponse struct { + ID uint64 `json:"tgId"` + Tests []mlkemKeyGenTestResponse `json:"tests"` +} + +type mlkemKeyGenTestResponse struct { + ID uint64 `json:"tcId"` + EK string `json:"ek"` + DK string `json:"dk"` +} + +type mlkemEncapDecapTestVectorSet struct { + Algorithm string `json:"algorithm"` + Mode string `json:"mode"` + Revision string `json:"revision"` + Groups []mlkemEncapDecapTestGroup `json:"testGroups"` +} + +type mlkemEncapDecapTestGroup struct { + ID uint64 `json:"tgId"` + TestType string `json:"testType"` + ParameterSet string `json:"parameterSet"` + Function string `json:"function"` + DK string `json:"dk,omitempty"` + Tests []mlkemEncapDecapTest `json:"tests"` +} + +type mlkemEncapDecapTest struct { + ID uint64 `json:"tcId"` + EK string `json:"ek,omitempty"` + M string `json:"m,omitempty"` + C string `json:"c,omitempty"` +} + +type mlkemEncapDecapTestGroupResponse struct { + ID uint64 `json:"tgId"` + Tests []mlkemEncapDecapTestResponse `json:"tests"` +} + +type mlkemEncapDecapTestResponse struct { + ID uint64 `json:"tcId"` + C string `json:"c,omitempty"` + K string `json:"k,omitempty"` +} + +type mlkem struct{} + +func (m *mlkem) Process(vectorSet []byte, t Transactable) (any, error) { + var common mlkemTestVectorSet + if err := json.Unmarshal(vectorSet, &common); err != nil { + return nil, fmt.Errorf("failed to unmarshal vector set: %v", err) + } + + switch common.Mode { + case "keyGen": + return m.processKeyGen(vectorSet, t) + case "encapDecap": + return m.processEncapDecap(vectorSet, t) + default: + return nil, fmt.Errorf("unsupported ML-KEM mode: %q", common.Mode) + } +} + +func (m *mlkem) processKeyGen(vectorSet []byte, t Transactable) (any, error) { + var parsed mlkemKeyGenTestVectorSet + if err := json.Unmarshal(vectorSet, &parsed); err != nil { + return nil, fmt.Errorf("failed to unmarshal keyGen vector set: %v", err) + } + + var ret []mlkemKeyGenTestGroupResponse + + for _, group := range parsed.Groups { + response := mlkemKeyGenTestGroupResponse{ + ID: group.ID, + } + + if !strings.HasPrefix(group.ParameterSet, "ML-KEM-") { + return nil, fmt.Errorf("invalid parameter set: %s", group.ParameterSet) + } + cmdName := group.ParameterSet + "/keyGen" + + for _, test := range group.Tests { + // Concatenate d and z to form the seed + dBytes, err := hex.DecodeString(test.D) + if err != nil { + return nil, fmt.Errorf("failed to decode d in test case %d/%d: %s", + group.ID, test.ID, err) + } + zBytes, err := hex.DecodeString(test.Z) + if err != nil { + return nil, fmt.Errorf("failed to decode z in test case %d/%d: %s", + group.ID, test.ID, err) + } + + seed := make([]byte, len(dBytes)+len(zBytes)) + copy(seed, dBytes) + copy(seed[len(dBytes):], zBytes) + + result, err := t.Transact(cmdName, 2, seed) + if err != nil { + return nil, fmt.Errorf("key generation failed for test case %d/%d: %s", + group.ID, test.ID, err) + } + + response.Tests = append(response.Tests, mlkemKeyGenTestResponse{ + ID: test.ID, + EK: hex.EncodeToString(result[0]), + DK: hex.EncodeToString(result[1]), + }) + } + + ret = append(ret, response) + } + + return ret, nil +} + +func (m *mlkem) processEncapDecap(vectorSet []byte, t Transactable) (any, error) { + var parsed mlkemEncapDecapTestVectorSet + if err := json.Unmarshal(vectorSet, &parsed); err != nil { + return nil, fmt.Errorf("failed to unmarshal encapDecap vector set: %v", err) + } + + var ret []mlkemEncapDecapTestGroupResponse + + for _, group := range parsed.Groups { + response := mlkemEncapDecapTestGroupResponse{ + ID: group.ID, + } + + if !strings.HasPrefix(group.ParameterSet, "ML-KEM-") { + return nil, fmt.Errorf("invalid parameter set: %s", group.ParameterSet) + } + + switch group.Function { + case "encapsulation": + cmdName := group.ParameterSet + "/encap" + for _, test := range group.Tests { + ek, err := hex.DecodeString(test.EK) + if err != nil { + return nil, fmt.Errorf("failed to decode ek in test case %d/%d: %s", + group.ID, test.ID, err) + } + + m, err := hex.DecodeString(test.M) + if err != nil { + return nil, fmt.Errorf("failed to decode m in test case %d/%d: %s", + group.ID, test.ID, err) + } + + result, err := t.Transact(cmdName, 2, ek, m) + if err != nil { + return nil, fmt.Errorf("encapsulation failed for test case %d/%d: %s", + group.ID, test.ID, err) + } + + response.Tests = append(response.Tests, mlkemEncapDecapTestResponse{ + ID: test.ID, + C: hex.EncodeToString(result[0]), + K: hex.EncodeToString(result[1]), + }) + } + + case "decapsulation": + cmdName := group.ParameterSet + "/decap" + dk, err := hex.DecodeString(group.DK) + if err != nil { + return nil, fmt.Errorf("failed to decode dk in group %d: %s", + group.ID, err) + } + + for _, test := range group.Tests { + c, err := hex.DecodeString(test.C) + if err != nil { + return nil, fmt.Errorf("failed to decode c in test case %d/%d: %s", + group.ID, test.ID, err) + } + + result, err := t.Transact(cmdName, 1, dk, c) + if err != nil { + return nil, fmt.Errorf("decapsulation failed for test case %d/%d: %s", + group.ID, test.ID, err) + } + + response.Tests = append(response.Tests, mlkemEncapDecapTestResponse{ + ID: test.ID, + K: hex.EncodeToString(result[0]), + }) + } + + default: + return nil, fmt.Errorf("unsupported function: %s", group.Function) + } + + ret = append(ret, response) + } + + return ret, nil +}
diff --git a/util/fipstools/acvp/acvptool/subprocess/subprocess.go b/util/fipstools/acvp/acvptool/subprocess/subprocess.go index 1f5fc60..0913220 100644 --- a/util/fipstools/acvp/acvptool/subprocess/subprocess.go +++ b/util/fipstools/acvp/acvptool/subprocess/subprocess.go
@@ -145,6 +145,7 @@ "KAS-FFC-SSC": &kasDH{}, "PBKDF": &pbkdf{}, "ML-DSA": &mldsa{}, + "ML-KEM": &mlkem{}, } m.primitives["ECDSA"] = &ecdsa{"ECDSA", map[string]bool{"P-224": true, "P-256": true, "P-384": true, "P-521": true}, m.primitives} m.primitives["DetECDSA"] = &ecdsa{"DetECDSA", map[string]bool{"P-224": true, "P-256": true, "P-384": true, "P-521": true}, m.primitives}
diff --git a/util/fipstools/acvp/acvptool/test/expected/ML-KEM.bz2 b/util/fipstools/acvp/acvptool/test/expected/ML-KEM.bz2 new file mode 100644 index 0000000..8c9ee96 --- /dev/null +++ b/util/fipstools/acvp/acvptool/test/expected/ML-KEM.bz2 Binary files differ
diff --git a/util/fipstools/acvp/acvptool/test/tests.json b/util/fipstools/acvp/acvptool/test/tests.json index 1559b98..736e1d9 100644 --- a/util/fipstools/acvp/acvptool/test/tests.json +++ b/util/fipstools/acvp/acvptool/test/tests.json
@@ -26,6 +26,7 @@ {"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-KEM.bz2", "Out": "expected/ML-KEM.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-KEM.bz2 b/util/fipstools/acvp/acvptool/test/vectors/ML-KEM.bz2 new file mode 100644 index 0000000..962d017 --- /dev/null +++ b/util/fipstools/acvp/acvptool/test/vectors/ML-KEM.bz2 Binary files differ
diff --git a/util/fipstools/acvp/modulewrapper/modulewrapper.cc b/util/fipstools/acvp/modulewrapper/modulewrapper.cc index d42d127..d5d0f95 100644 --- a/util/fipstools/acvp/modulewrapper/modulewrapper.cc +++ b/util/fipstools/acvp/modulewrapper/modulewrapper.cc
@@ -982,6 +982,28 @@ "ML-DSA-65", "ML-DSA-87" ] + }, + { + "algorithm": "ML-KEM", + "mode": "keyGen", + "revision": "FIPS203", + "parameterSets": [ + "ML-KEM-768", + "ML-KEM-1024" + ] + }, + { + "algorithm": "ML-KEM", + "mode": "encapDecap", + "revision": "FIPS203", + "parameterSets": [ + "ML-KEM-768", + "ML-KEM-1024" + ], + "functions": [ + "encapsulation", + "decapsulation" + ] } ])"; return write_reply({Span<const uint8_t>( @@ -2259,6 +2281,85 @@ return write_reply({Span<const uint8_t>(&ok, sizeof(ok))}); } +template <typename PrivateKey, size_t PublicKeyBytes, + bcm_infallible (*KeyGen)(uint8_t *, PrivateKey *, const uint8_t *), + bcm_status (*MarshalPrivate)(CBB *, const PrivateKey *)> +static bool MLKEMKeyGen(const Span<const uint8_t> args[], + ReplyCallback write_reply) { + const Span<const uint8_t> seed = args[0]; + if (seed.size() != BCM_MLKEM_SEED_BYTES) { + LOG_ERROR("Bad seed size.\n"); + return false; + } + + auto priv = std::make_unique<PrivateKey>(); + uint8_t pub_key_bytes[PublicKeyBytes]; + KeyGen(pub_key_bytes, priv.get(), seed.data()); + + ScopedCBB cbb; + if (!CBB_init(cbb.get(), BCM_MLKEM768_PRIVATE_KEY_BYTES) || + !bcm_success(MarshalPrivate(cbb.get(), priv.get()))) { + LOG_ERROR("Failed to serialize private key.\n"); + return false; + } + + return write_reply( + {pub_key_bytes, MakeConstSpan(CBB_data(cbb.get()), CBB_len(cbb.get()))}); +} + +template <typename PublicKey, bcm_status (*ParsePublic)(PublicKey *, CBS *), + size_t CiphertextBytes, + bcm_infallible (*Encap)(uint8_t *, uint8_t *, const PublicKey *, + const uint8_t *)> +static bool MLKEMEncap(const Span<const uint8_t> args[], + ReplyCallback write_reply) { + const Span<const uint8_t> pub_key_bytes = args[0]; + const Span<const uint8_t> entropy = args[1]; + + if (entropy.size() != BCM_MLKEM_ENCAP_ENTROPY) { + LOG_ERROR("Bad entropy size.\n"); + return false; + } + + auto pub = std::make_unique<PublicKey>(); + CBS cbs = bssl::MakeConstSpan(pub_key_bytes); + if (!bcm_success(ParsePublic(pub.get(), &cbs)) || CBS_len(&cbs) != 0) { + LOG_ERROR("Failed to parse public key.\n"); + return false; + } + + uint8_t ciphertext[CiphertextBytes]; + uint8_t shared_secret[BCM_MLKEM_SHARED_SECRET_BYTES]; + Encap(ciphertext, shared_secret, pub.get(), entropy.data()); + + return write_reply({ciphertext, shared_secret}); +} + +template <typename PrivateKey, bcm_status (*ParsePrivate)(PrivateKey *, CBS *), + bcm_status (*Decap)(uint8_t *, const uint8_t *, size_t, + const PrivateKey *)> +static bool MLKEMDecap(const Span<const uint8_t> args[], + ReplyCallback write_reply) { + const Span<const uint8_t> priv_key_bytes = args[0]; + const Span<const uint8_t> ciphertext = args[1]; + + auto priv = std::make_unique<PrivateKey>(); + CBS cbs = bssl::MakeConstSpan(priv_key_bytes); + if (!bcm_success(ParsePrivate(priv.get(), &cbs))) { + LOG_ERROR("Failed to parse private key.\n"); + return false; + } + + uint8_t shared_secret[BCM_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}); +} + static constexpr struct { char name[kMaxNameLength + 1]; uint8_t num_expected_args; @@ -2372,6 +2473,28 @@ {"ML-DSA-87/sigVer", 3, MLDSASigVer<BCM_mldsa87_public_key, BCM_MLDSA87_SIGNATURE_BYTES, BCM_mldsa87_parse_public_key, BCM_mldsa87_verify_internal>}, + {"ML-KEM-768/keyGen", 1, + MLKEMKeyGen<BCM_mlkem768_private_key, BCM_MLKEM768_PUBLIC_KEY_BYTES, + BCM_mlkem768_generate_key_external_seed, + BCM_mlkem768_marshal_private_key>}, + {"ML-KEM-1024/keyGen", 1, + MLKEMKeyGen<BCM_mlkem1024_private_key, BCM_MLKEM1024_PUBLIC_KEY_BYTES, + BCM_mlkem1024_generate_key_external_seed, + BCM_mlkem1024_marshal_private_key>}, + {"ML-KEM-768/encap", 2, + MLKEMEncap<BCM_mlkem768_public_key, BCM_mlkem768_parse_public_key, + BCM_MLKEM768_CIPHERTEXT_BYTES, + BCM_mlkem768_encap_external_entropy>}, + {"ML-KEM-1024/encap", 2, + MLKEMEncap<BCM_mlkem1024_public_key, BCM_mlkem1024_parse_public_key, + BCM_MLKEM1024_CIPHERTEXT_BYTES, + BCM_mlkem1024_encap_external_entropy>}, + {"ML-KEM-768/decap", 2, + MLKEMDecap<BCM_mlkem768_private_key, BCM_mlkem768_parse_private_key, + BCM_mlkem768_decap>}, + {"ML-KEM-1024/decap", 2, + MLKEMDecap<BCM_mlkem1024_private_key, BCM_mlkem1024_parse_private_key, + BCM_mlkem1024_decap>}, }; Handler FindHandler(Span<const Span<const uint8_t>> args) {