Merge to fips-20250107: Check hashes when parsing test-only, semi-expand ML-KEM private keys

ML-KEM has two private key formats: a short seed, and an internal
"semi-expanded" format that is only used in unit and ACVP testing. Only
the seed form is reachable from outside the library. Seeds simplify a
lot of questions, including making it impossible for components of the
private key to be inconsistent with each other.

The semi-expanded form has a redundancy in that it carries both the
public key and the hash. Matching what
https://boringssl-review.googlesource.com/c/boringssl/+/82991 did for
ML-DSA, check the hash is correct as part of parsing. This somewhat
defeats the point of having the hash listed explicitly. (It's an
optimization for systems that parse a trusted key over and over.) But
since we've now concluded this wasn't a great private key format in the
first place, this codepath is test-only anyway. That means there is no
optimization potential and it is better to include the check to avoid
having to think about it.

See https://boringssl-review.googlesource.com/c/boringssl/+/93247
(cherry picked from commit c8eb36bae607541c5c6bc518e60ba6ec44e3d10a)
Change-Id: I8b5c42bf38b7f7564dc7a125c23d9aa2fa7e568d
Reviewed-on: https://boringssl-review.googlesource.com/c/boringssl/+/100807
Reviewed-by: David Benjamin <davidben@google.com>
diff --git a/crypto/fipsmodule/mlkem/mlkem.cc.inc b/crypto/fipsmodule/mlkem/mlkem.cc.inc
index 07ed846..c4e5ae4 100644
--- a/crypto/fipsmodule/mlkem/mlkem.cc.inc
+++ b/crypto/fipsmodule/mlkem/mlkem.cc.inc
@@ -818,10 +818,12 @@
   mlkem_decap_no_self_test(out_shared_secret, ciphertext, priv);
 }
 
-// mlkem_parse_public_key_no_hash parses |in| into |pub| but doesn't calculate
-// the value of |pub->public_key_hash|.
+// mlkem_parse_public_key_with_trailing_data parses |in| into |pub| but leaves
+// trailing data in |in| for the caller.
 template <int RANK>
-int mlkem_parse_public_key_no_hash(struct public_key<RANK> *pub, CBS *in) {
+int mlkem_parse_public_key_with_trailing_data(struct public_key<RANK> *pub,
+                                              CBS *in) {
+  CBS orig_in = *in;
   CBS t_bytes;
   if (!CBS_get_bytes(in, &t_bytes, encoded_vector_size(RANK)) ||
       !vector_decode(&pub->t, CBS_data(&t_bytes), kLog2Prime) ||
@@ -829,28 +831,31 @@
     return 0;
   }
   matrix_expand(&pub->m, pub->rho);
+  size_t pub_key_len = CBS_len(&orig_in) - CBS_len(in);
+  assert(pub_key_len == encoded_public_key_size(RANK));
+  hash_h(pub->public_key_hash, CBS_data(&orig_in), pub_key_len);
   return 1;
 }
 
 template <int RANK>
 int mlkem_parse_public_key(struct public_key<RANK> *pub, CBS *in) {
-  CBS orig_in = *in;
-  if (!mlkem_parse_public_key_no_hash(pub, in) ||  //
+  if (!mlkem_parse_public_key_with_trailing_data(pub, in) ||  //
       CBS_len(in) != 0) {
     return 0;
   }
-  hash_h(pub->public_key_hash, CBS_data(&orig_in), CBS_len(&orig_in));
   return 1;
 }
 
 template <int RANK>
 int mlkem_parse_private_key(struct private_key<RANK> *priv, CBS *in) {
-  CBS s_bytes;
+  CBS s_bytes, public_key_hash;
   if (!CBS_get_bytes(in, &s_bytes, encoded_vector_size(RANK)) ||
       !vector_decode(&priv->s, CBS_data(&s_bytes), kLog2Prime) ||
-      !mlkem_parse_public_key_no_hash(&priv->pub, in) ||
-      !CBS_copy_bytes(in, priv->pub.public_key_hash,
-                      sizeof(priv->pub.public_key_hash)) ||
+      !mlkem_parse_public_key_with_trailing_data(&priv->pub, in) ||
+      // We compute the public key hash ourselves, but check it matched.
+      !CBS_get_bytes(in, &public_key_hash, sizeof(priv->pub.public_key_hash)) ||
+      !CBS_mem_equal(&public_key_hash, priv->pub.public_key_hash,
+                     sizeof(priv->pub.public_key_hash)) ||
       !CBS_copy_bytes(in, priv->fo_failure_secret,
                       sizeof(priv->fo_failure_secret)) ||
       CBS_len(in) != 0) {
diff --git a/crypto/mlkem/mlkem_test.cc b/crypto/mlkem/mlkem_test.cc
index 96d652d..401ff09 100644
--- a/crypto/mlkem/mlkem_test.cc
+++ b/crypto/mlkem/mlkem_test.cc
@@ -172,18 +172,32 @@
       Marshal(MARSHAL_PRIVATE, priv.get()));
   EXPECT_EQ(encoded_private_key.size(), size_t{PRIVATE_KEY_BYTES});
 
-  OPENSSL_memcpy(first_two_bytes, encoded_private_key.data(),
-                 sizeof(first_two_bytes));
-  OPENSSL_memset(encoded_private_key.data(), 0xff, sizeof(first_two_bytes));
+  // Parsing should fail if a coefficient is out of range.
+  {
+    std::vector<uint8_t> invalid = encoded_private_key;
+    invalid[0] = 0xff;
+    invalid[1] = 0xff;
+    CBS cbs;
+    CBS_init(&cbs, invalid.data(), invalid.size());
+    auto priv2 = std::make_unique<PRIVATE_KEY>();
+    ASSERT_FALSE(PARSE_PRIVATE(priv2.get(), &cbs));
+  }
+
+  // Parsing should fail if the public key hash is wrong.
+  {
+    std::vector<uint8_t> invalid = encoded_private_key;
+    // The final 64 bytes of the semi-expanded format are the 32-byte hash and
+    // the 32-byte FO (Fujisaki-Okamoto) failure secret. Flip a bit in the hash.
+    invalid[invalid.size() - 33] ^= 1;
+    CBS cbs;
+    CBS_init(&cbs, invalid.data(), invalid.size());
+    auto priv2 = std::make_unique<PRIVATE_KEY>();
+    ASSERT_FALSE(PARSE_PRIVATE(priv2.get(), &cbs));
+  }
+
   CBS cbs;
   CBS_init(&cbs, encoded_private_key.data(), encoded_private_key.size());
   auto priv2 = std::make_unique<PRIVATE_KEY>();
-  // Parsing should fail because the first coefficient is >= kPrime.
-  ASSERT_FALSE(PARSE_PRIVATE(priv2.get(), &cbs));
-
-  OPENSSL_memcpy(encoded_private_key.data(), first_two_bytes,
-                 sizeof(first_two_bytes));
-  CBS_init(&cbs, encoded_private_key.data(), encoded_private_key.size());
   ASSERT_TRUE(PARSE_PRIVATE(priv2.get(), &cbs));
   EXPECT_EQ(Bytes(Declassified(encoded_private_key)),
             Bytes(Declassified(Marshal(MARSHAL_PRIVATE, priv2.get()))));