Implement MLKEM1024 for TLS

This adds the codepoint for ML-KEM-1024 from draft-ietf-tls-mlkem-04.

Change-Id: I897e50c6de7f00feaf0a9f8727d934fd0f8796cb
Bug: b:437414532
Reviewed-on: https://boringssl-review.googlesource.com/c/boringssl/+/81187
Reviewed-by: David Benjamin <davidben@google.com>
Commit-Queue: Lily Chen <chlily@google.com>
diff --git a/crypto/obj/obj_dat.h b/crypto/obj/obj_dat.h
index a857d53..344e7f2 100644
--- a/crypto/obj/obj_dat.h
+++ b/crypto/obj/obj_dat.h
@@ -15,7 +15,7 @@
 // This file is generated by crypto/obj/objects.go.
 
 
-#define NUM_NID 966
+#define NUM_NID 967
 
 static const uint8_t kObjectData[] = {
     /* NID_rsadsi */
@@ -8742,6 +8742,7 @@
     {"X25519Kyber768Draft00", "X25519Kyber768Draft00",
      NID_X25519Kyber768Draft00, 0, NULL, 0},
     {"X25519MLKEM768", "X25519MLKEM768", NID_X25519MLKEM768, 0, NULL, 0},
+    {"MLKEM1024", "MLKEM1024", NID_MLKEM1024, 0, NULL, 0},
 };
 
 static const uint16_t kNIDsInShortNameOrder[] = {
@@ -8864,6 +8865,7 @@
     114 /* MD5-SHA1 */,
     95 /* MDC2 */,
     911 /* MGF1 */,
+    966 /* MLKEM1024 */,
     388 /* Mail */,
     57 /* Netscape */,
     366 /* Nonce */,
@@ -9755,6 +9757,7 @@
     647 /* International Organizations */,
     142 /* Invalidity Date */,
     504 /* MIME MHS */,
+    966 /* MLKEM1024 */,
     388 /* Mail */,
     383 /* Management */,
     417 /* Microsoft CSP Name */,
diff --git a/crypto/obj/obj_mac.num b/crypto/obj/obj_mac.num
index 6e2a2ae..82d0a3d 100644
--- a/crypto/obj/obj_mac.num
+++ b/crypto/obj/obj_mac.num
@@ -953,3 +953,4 @@
 hkdf		963
 X25519Kyber768Draft00		964
 X25519MLKEM768		965
+MLKEM1024		966
diff --git a/crypto/obj/objects.txt b/crypto/obj/objects.txt
index 6553407..a38969b 100644
--- a/crypto/obj/objects.txt
+++ b/crypto/obj/objects.txt
@@ -1336,6 +1336,9 @@
  : X25519Kyber768Draft00
  : X25519MLKEM768
 
+# NIDs for post quantum (pure) KEMs in TLS (no corresponding OIDs).
+ : MLKEM1024
+
 # See RFC 8410.
 1 3 101 110 : X25519
 1 3 101 111 : X448
diff --git a/include/openssl/nid.h b/include/openssl/nid.h
index e1eed62..7f018ce 100644
--- a/include/openssl/nid.h
+++ b/include/openssl/nid.h
@@ -4216,6 +4216,9 @@
 #define SN_X25519MLKEM768 "X25519MLKEM768"
 #define NID_X25519MLKEM768 965
 
+#define SN_MLKEM1024 "MLKEM1024"
+#define NID_MLKEM1024 966
+
 
 #if defined(__cplusplus)
 } /* extern C */
diff --git a/include/openssl/ssl.h b/include/openssl/ssl.h
index 7fd857e..51417d4 100644
--- a/include/openssl/ssl.h
+++ b/include/openssl/ssl.h
@@ -2528,6 +2528,7 @@
 #define SSL_GROUP_X25519 29
 #define SSL_GROUP_X25519_MLKEM768 0x11ec
 #define SSL_GROUP_X25519_KYBER768_DRAFT00 0x6399
+#define SSL_GROUP_MLKEM1024 0x0202
 
 // SSL_CTX_set1_group_ids sets the preferred groups for |ctx| to |group_ids|.
 // Each element of |group_ids| should be one of the |SSL_GROUP_*| constants. It
diff --git a/ssl/extensions.cc b/ssl/extensions.cc
index b177e72..7be8a3c 100644
--- a/ssl/extensions.cc
+++ b/ssl/extensions.cc
@@ -102,6 +102,7 @@
   switch (id) {
     case SSL_GROUP_X25519_KYBER768_DRAFT00:
     case SSL_GROUP_X25519_MLKEM768:
+    case SSL_GROUP_MLKEM1024:
       return true;
     default:
       return false;
diff --git a/ssl/ssl_key_share.cc b/ssl/ssl_key_share.cc
index 856d0d6..c2c01e6 100644
--- a/ssl/ssl_key_share.cc
+++ b/ssl/ssl_key_share.cc
@@ -375,6 +375,66 @@
   MLKEM768_private_key mlkem_private_key_;
 };
 
+// draft-ietf-tls-mlkem-04
+class MLKEM1024KeyShare : public SSLKeyShare {
+ public:
+  MLKEM1024KeyShare() = default;
+
+  uint16_t GroupID() const override { return SSL_GROUP_MLKEM1024; }
+
+  bool Generate(CBB *out_public_key) override {
+    uint8_t public_key[MLKEM1024_PUBLIC_KEY_BYTES];
+    MLKEM1024_generate_key(public_key, /*optional_out_seed=*/nullptr,
+                           &private_key_);
+    return CBB_add_bytes(out_public_key, public_key, sizeof(public_key));
+  }
+
+  bool Encap(CBB *out_ciphertext, Array<uint8_t> *out_secret,
+             uint8_t *out_alert, Span<const uint8_t> peer_key) override {
+    MLKEM1024_public_key peer_pub;
+    CBS peer_pub_cbs;
+    CBS_init(&peer_pub_cbs, peer_key.data(), peer_key.size());
+    if (!MLKEM1024_parse_public_key(&peer_pub, &peer_pub_cbs)) {
+      *out_alert = SSL_AD_ILLEGAL_PARAMETER;
+      OPENSSL_PUT_ERROR(SSL, SSL_R_BAD_ECPOINT);
+      return false;
+    }
+
+    Array<uint8_t> secret;
+    if (!secret.InitForOverwrite(MLKEM_SHARED_SECRET_BYTES)) {
+      return false;
+    }
+    uint8_t ciphertext[MLKEM1024_CIPHERTEXT_BYTES];
+    MLKEM1024_encap(ciphertext, secret.data(), &peer_pub);
+    if (!CBB_add_bytes(out_ciphertext, ciphertext, sizeof(ciphertext))) {
+      return false;
+    }
+    *out_secret = std::move(secret);
+    return true;
+  }
+
+  bool Decap(Array<uint8_t> *out_secret, uint8_t *out_alert,
+             Span<const uint8_t> ciphertext) override {
+    Array<uint8_t> secret;
+    if (!secret.InitForOverwrite(MLKEM_SHARED_SECRET_BYTES)) {
+      *out_alert = SSL_AD_INTERNAL_ERROR;
+      return false;
+    }
+
+    if (!MLKEM1024_decap(secret.data(), ciphertext.data(), ciphertext.size(),
+                         &private_key_)) {
+      *out_alert = SSL_AD_ILLEGAL_PARAMETER;
+      OPENSSL_PUT_ERROR(SSL, SSL_R_BAD_ECPOINT);
+      return false;
+    }
+    *out_secret = std::move(secret);
+    return true;
+  }
+
+ private:
+  MLKEM1024_private_key private_key_;
+};
+
 constexpr NamedGroup kNamedGroups[] = {
     {NID_X9_62_prime256v1, SSL_GROUP_SECP256R1, "P-256", "prime256v1"},
     {NID_secp384r1, SSL_GROUP_SECP384R1, "P-384", "secp384r1"},
@@ -383,6 +443,7 @@
     {NID_X25519Kyber768Draft00, SSL_GROUP_X25519_KYBER768_DRAFT00,
      "X25519Kyber768Draft00", ""},
     {NID_X25519MLKEM768, SSL_GROUP_X25519_MLKEM768, "X25519MLKEM768", ""},
+    {NID_MLKEM1024, SSL_GROUP_MLKEM1024, "MLKEM1024", ""},
 };
 
 }  // namespace
@@ -403,6 +464,8 @@
       return MakeUnique<X25519Kyber768KeyShare>();
     case SSL_GROUP_X25519_MLKEM768:
       return MakeUnique<X25519MLKEM768KeyShare>();
+    case SSL_GROUP_MLKEM1024:
+      return MakeUnique<MLKEM1024KeyShare>();
     default:
       return nullptr;
   }
diff --git a/ssl/ssl_test.cc b/ssl/ssl_test.cc
index 87501b1..975b14a 100644
--- a/ssl/ssl_test.cc
+++ b/ssl/ssl_test.cc
@@ -495,6 +495,14 @@
         "P-256:X25519MLKEM768",
         {SSL_GROUP_SECP256R1, SSL_GROUP_X25519_MLKEM768},
     },
+    {
+        "P-256:MLKEM1024",
+        {SSL_GROUP_SECP256R1, SSL_GROUP_MLKEM1024},
+    },
+    {
+        "MLKEM1024:X25519MLKEM768",
+        {SSL_GROUP_MLKEM1024, SSL_GROUP_X25519_MLKEM768},
+    },
 
     {
         "P-256:P-384:P-521:X25519",
diff --git a/ssl/test/fuzzer.h b/ssl/test/fuzzer.h
index 9360fee..7b5960d 100644
--- a/ssl/test/fuzzer.h
+++ b/ssl/test/fuzzer.h
@@ -451,8 +451,9 @@
 
     static const uint16_t kGroups[] = {
         SSL_GROUP_X25519_MLKEM768, SSL_GROUP_X25519_KYBER768_DRAFT00,
-        SSL_GROUP_X25519,          SSL_GROUP_SECP256R1,
-        SSL_GROUP_SECP384R1,       SSL_GROUP_SECP521R1};
+        SSL_GROUP_MLKEM1024,       SSL_GROUP_X25519,
+        SSL_GROUP_SECP256R1,       SSL_GROUP_SECP384R1,
+        SSL_GROUP_SECP521R1};
     if (!SSL_CTX_set1_group_ids(ctx_.get(), kGroups,
                                 OPENSSL_ARRAY_SIZE(kGroups))) {
       return false;
diff --git a/ssl/test/runner/common.go b/ssl/test/runner/common.go
index 36a9db2..4232de8 100644
--- a/ssl/test/runner/common.go
+++ b/ssl/test/runner/common.go
@@ -210,6 +210,7 @@
 	CurveX25519         CurveID = 29
 	CurveX25519MLKEM768 CurveID = 0x11ec
 	CurveX25519Kyber768 CurveID = 0x6399
+	CurveMLKEM1024      CurveID = 0x0202
 )
 
 // TLS Elliptic Curve Point Formats
@@ -2246,7 +2247,7 @@
 	return ret
 }
 
-var defaultCurvePreferences = []CurveID{CurveX25519MLKEM768, CurveX25519Kyber768, CurveX25519, CurveP256, CurveP384, CurveP521}
+var defaultCurvePreferences = []CurveID{CurveX25519MLKEM768, CurveX25519Kyber768, CurveMLKEM1024, CurveX25519, CurveP256, CurveP384, CurveP521}
 
 func (c *Config) curvePreferences() []CurveID {
 	if c == nil || len(c.CurvePreferences) == 0 {
diff --git a/ssl/test/runner/curve_tests.go b/ssl/test/runner/curve_tests.go
index ca95c9d..099bb2a 100644
--- a/ssl/test/runner/curve_tests.go
+++ b/ssl/test/runner/curve_tests.go
@@ -28,13 +28,18 @@
 	{"P-521", CurveP521},
 	{"X25519", CurveX25519},
 	{"Kyber", CurveX25519Kyber768},
-	{"MLKEM", CurveX25519MLKEM768},
+	{"X25519MLKEM768", CurveX25519MLKEM768},
+	{"MLKEM1024", CurveMLKEM1024},
 }
 
 const bogusCurve = 0x1234
 
 func isPqGroup(r CurveID) bool {
-	return r == CurveX25519Kyber768 || r == CurveX25519MLKEM768
+	return r == CurveX25519Kyber768 || isMLKEMGroup(r)
+}
+
+func isMLKEMGroup(r CurveID) bool {
+	return r == CurveX25519MLKEM768 || r == CurveMLKEM1024
 }
 
 func isECDHGroup(r CurveID) bool {
@@ -195,7 +200,7 @@
 					})
 				}
 
-				if curve.id == CurveX25519MLKEM768 && testType == serverTest {
+				if isMLKEMGroup(curve.id) && testType == serverTest {
 					testCases = append(testCases, testCase{
 						testType: testType,
 						name:     "CurveTest-Invalid-MLKEMEncapKeyNotReduced-" + suffix,
@@ -583,97 +588,103 @@
 		},
 	})
 
-	// If ML-KEM is offered, both X25519 and ML-KEM should have a key-share.
-	testCases = append(testCases, testCase{
-		name: "NotJustMLKEMKeyShare",
-		config: Config{
-			MinVersion: VersionTLS13,
-			Bugs: ProtocolBugs{
-				ExpectedKeyShares: []CurveID{CurveX25519MLKEM768, CurveX25519},
-			},
-		},
-		flags: []string{
-			"-curves", strconv.Itoa(int(CurveX25519MLKEM768)),
-			"-curves", strconv.Itoa(int(CurveX25519)),
-			"-expect-curve-id", strconv.Itoa(int(CurveX25519MLKEM768)),
-		},
-	})
+	for _, curve := range testCurves {
+		if !isMLKEMGroup(curve.id) {
+			continue
+		}
 
-	// ... and the other way around
-	testCases = append(testCases, testCase{
-		name: "MLKEMKeyShareIncludedSecond",
-		config: Config{
-			MinVersion: VersionTLS13,
-			Bugs: ProtocolBugs{
-				ExpectedKeyShares: []CurveID{CurveX25519, CurveX25519MLKEM768},
+		// If ML-KEM is offered, both X25519 and ML-KEM should have a key-share.
+		testCases = append(testCases, testCase{
+			name: "NotJustMLKEMKeyShare-" + curve.name,
+			config: Config{
+				MinVersion: VersionTLS13,
+				Bugs: ProtocolBugs{
+					ExpectedKeyShares: []CurveID{curve.id, CurveX25519},
+				},
 			},
-		},
-		flags: []string{
-			"-curves", strconv.Itoa(int(CurveX25519)),
-			"-curves", strconv.Itoa(int(CurveX25519MLKEM768)),
-			"-expect-curve-id", strconv.Itoa(int(CurveX25519)),
-		},
-	})
-
-	// ... and even if there's another curve in the middle because it's the
-	// first classical and first post-quantum "curves" that get key shares
-	// included.
-	testCases = append(testCases, testCase{
-		name: "MLKEMKeyShareIncludedThird",
-		config: Config{
-			MinVersion: VersionTLS13,
-			Bugs: ProtocolBugs{
-				ExpectedKeyShares: []CurveID{CurveX25519, CurveX25519MLKEM768},
+			flags: []string{
+				"-curves", strconv.Itoa(int(curve.id)),
+				"-curves", strconv.Itoa(int(CurveX25519)),
+				"-expect-curve-id", strconv.Itoa(int(curve.id)),
 			},
-		},
-		flags: []string{
-			"-curves", strconv.Itoa(int(CurveX25519)),
-			"-curves", strconv.Itoa(int(CurveP256)),
-			"-curves", strconv.Itoa(int(CurveX25519MLKEM768)),
-			"-expect-curve-id", strconv.Itoa(int(CurveX25519)),
-		},
-	})
+		})
 
-	// If ML-KEM is the only configured curve, the key share is sent.
-	testCases = append(testCases, testCase{
-		name: "JustConfiguringMLKEMWorks",
-		config: Config{
-			MinVersion: VersionTLS13,
-			Bugs: ProtocolBugs{
-				ExpectedKeyShares: []CurveID{CurveX25519MLKEM768},
+		// ... and the other way around
+		testCases = append(testCases, testCase{
+			name: "MLKEMKeyShareIncludedSecond-" + curve.name,
+			config: Config{
+				MinVersion: VersionTLS13,
+				Bugs: ProtocolBugs{
+					ExpectedKeyShares: []CurveID{CurveX25519, curve.id},
+				},
 			},
-		},
-		flags: []string{
-			"-curves", strconv.Itoa(int(CurveX25519MLKEM768)),
-			"-expect-curve-id", strconv.Itoa(int(CurveX25519MLKEM768)),
-		},
-	})
-
-	// If both ML-KEM and Kyber are configured, only the preferred one's
-	// key share should be sent.
-	testCases = append(testCases, testCase{
-		name: "BothMLKEMAndKyber",
-		config: Config{
-			MinVersion: VersionTLS13,
-			Bugs: ProtocolBugs{
-				ExpectedKeyShares: []CurveID{CurveX25519MLKEM768},
+			flags: []string{
+				"-curves", strconv.Itoa(int(CurveX25519)),
+				"-curves", strconv.Itoa(int(curve.id)),
+				"-expect-curve-id", strconv.Itoa(int(CurveX25519)),
 			},
-		},
-		flags: []string{
-			"-curves", strconv.Itoa(int(CurveX25519MLKEM768)),
-			"-curves", strconv.Itoa(int(CurveX25519Kyber768)),
-			"-expect-curve-id", strconv.Itoa(int(CurveX25519MLKEM768)),
-		},
-	})
+		})
 
-	// As a server, ML-KEM is not yet supported by default.
+		// ... and even if there's another curve in the middle because it's the
+		// first classical and first post-quantum "curves" that get key shares
+		// included.
+		testCases = append(testCases, testCase{
+			name: "MLKEMKeyShareIncludedThird-" + curve.name,
+			config: Config{
+				MinVersion: VersionTLS13,
+				Bugs: ProtocolBugs{
+					ExpectedKeyShares: []CurveID{CurveX25519, curve.id},
+				},
+			},
+			flags: []string{
+				"-curves", strconv.Itoa(int(CurveX25519)),
+				"-curves", strconv.Itoa(int(CurveP256)),
+				"-curves", strconv.Itoa(int(curve.id)),
+				"-expect-curve-id", strconv.Itoa(int(CurveX25519)),
+			},
+		})
+
+		// If ML-KEM is the only configured curve, the key share is sent.
+		testCases = append(testCases, testCase{
+			name: "JustConfiguringMLKEMWorks-" + curve.name,
+			config: Config{
+				MinVersion: VersionTLS13,
+				Bugs: ProtocolBugs{
+					ExpectedKeyShares: []CurveID{curve.id},
+				},
+			},
+			flags: []string{
+				"-curves", strconv.Itoa(int(curve.id)),
+				"-expect-curve-id", strconv.Itoa(int(curve.id)),
+			},
+		})
+
+		// If both ML-KEM and Kyber are configured, only the preferred one's
+		// key share should be sent.
+		testCases = append(testCases, testCase{
+			name: "BothMLKEMAndKyber-" + curve.name,
+			config: Config{
+				MinVersion: VersionTLS13,
+				Bugs: ProtocolBugs{
+					ExpectedKeyShares: []CurveID{curve.id},
+				},
+			},
+			flags: []string{
+				"-curves", strconv.Itoa(int(curve.id)),
+				"-curves", strconv.Itoa(int(CurveX25519Kyber768)),
+				"-expect-curve-id", strconv.Itoa(int(curve.id)),
+			},
+		})
+	}
+
+	// As a server, ML-KEMs and Kyber are not yet supported by default.
 	testCases = append(testCases, testCase{
 		testType: serverTest,
 		name:     "PostQuantumNotEnabledByDefaultForAServer",
 		config: Config{
 			MinVersion:       VersionTLS13,
-			CurvePreferences: []CurveID{CurveX25519MLKEM768, CurveX25519Kyber768, CurveX25519},
-			DefaultCurves:    []CurveID{CurveX25519MLKEM768, CurveX25519Kyber768},
+			CurvePreferences: []CurveID{CurveX25519MLKEM768, CurveMLKEM1024, CurveX25519Kyber768, CurveX25519},
+			DefaultCurves:    []CurveID{CurveX25519MLKEM768, CurveMLKEM1024, CurveX25519Kyber768},
 		},
 		flags: []string{
 			"-server-preference",
@@ -681,6 +692,23 @@
 		},
 	})
 
+	// If two ML-KEMs are configured, only the preferred one's
+	// key share should be sent.
+	testCases = append(testCases, testCase{
+		name: "TwoMLKEMs",
+		config: Config{
+			MinVersion: VersionTLS13,
+			Bugs: ProtocolBugs{
+				ExpectedKeyShares: []CurveID{CurveMLKEM1024},
+			},
+		},
+		flags: []string{
+			"-curves", strconv.Itoa(int(CurveMLKEM1024)),
+			"-curves", strconv.Itoa(int(CurveX25519MLKEM768)),
+			"-expect-curve-id", strconv.Itoa(int(CurveMLKEM1024)),
+		},
+	})
+
 	// In TLS 1.2, the curve list is also used to signal ECDSA curves.
 	testCases = append(testCases, testCase{
 		testType: serverTest,
diff --git a/ssl/test/runner/key_agreement.go b/ssl/test/runner/key_agreement.go
index f2372ea..d91ce39 100644
--- a/ssl/test/runner/key_agreement.go
+++ b/ssl/test/runner/key_agreement.go
@@ -426,6 +426,47 @@
 	return m.decapKey.Decapsulate(ciphertext)
 }
 
+// mlkem1024KEM implements ML-KEM-1024
+type mlkem1024KEM struct {
+	decapKey *mlkem.DecapsulationKey1024
+}
+
+func (e *mlkem1024KEM) encapsulationKeySize() int {
+	return mlkem.EncapsulationKeySize1024
+}
+
+func (e *mlkem1024KEM) ciphertextSize() int {
+	return mlkem.CiphertextSize1024
+}
+
+func (m *mlkem1024KEM) generate(config *Config) (publicKey []byte, err error) {
+	m.decapKey, err = mlkem.GenerateKey1024()
+	if err != nil {
+		return
+	}
+	publicKey = m.decapKey.EncapsulationKey().Bytes()
+	if config.Bugs.MLKEMEncapKeyNotReduced {
+		// Set the first 12 bits so that the first word is definitely
+		// not reduced.
+		publicKey[0] |= 0xff
+		publicKey[1] |= 0xf
+	}
+	return
+}
+
+func (m *mlkem1024KEM) encap(config *Config, peerKey []byte) (ciphertext []byte, secret []byte, err error) {
+	key, err := mlkem.NewEncapsulationKey1024(peerKey)
+	if err != nil {
+		return nil, nil, err
+	}
+	secret, ciphertext = key.Encapsulate()
+	return
+}
+
+func (m *mlkem1024KEM) decap(config *Config, ciphertext []byte) (secret []byte, err error) {
+	return m.decapKey.Decapsulate(ciphertext)
+}
+
 // concatKEM concatenates two kemImplementations.
 type concatKEM struct {
 	kem1, kem2 kemImplementation
@@ -535,6 +576,9 @@
 	case CurveX25519MLKEM768:
 		// draft-ietf-tls-ecdhe-mlkem-00
 		kem = &concatKEM{kem1: &mlkem768KEM{}, kem2: &ecdhKEM{curve: ecdh.X25519()}}
+	case CurveMLKEM1024:
+		// draft-ietf-tls-mlkem-04
+		kem = &mlkem1024KEM{}
 	default:
 		return nil, false
 	}