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,