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 }