diff --git a/crypto/obj/obj_dat.h b/crypto/obj/obj_dat.h index 71ef2d2bdc..f1b706391b 100644 --- a/crypto/obj/obj_dat.h +++ b/crypto/obj/obj_dat.h @@ -57,7 +57,7 @@ /* This file is generated by crypto/obj/objects.go. */ -#define NUM_NID 965 +#define NUM_NID 966 static const uint8_t kObjectData[] = { /* NID_rsadsi */ @@ -8783,6 +8783,7 @@ static const ASN1_OBJECT kObjects[NUM_NID] = { {"HKDF", "hkdf", NID_hkdf, 0, NULL, 0}, {"X25519Kyber768Draft00", "X25519Kyber768Draft00", NID_X25519Kyber768Draft00, 0, NULL, 0}, + {"X25519MLKEM768", "X25519MLKEM768", NID_X25519MLKEM768, 0, NULL, 0}, }; static const uint16_t kNIDsInShortNameOrder[] = { @@ -8981,6 +8982,7 @@ static const uint16_t kNIDsInShortNameOrder[] = { 458 /* UID */, 948 /* X25519 */, 964 /* X25519Kyber768Draft00 */, + 965 /* X25519MLKEM768 */, 961 /* X448 */, 11 /* X500 */, 378 /* X500algorithms */, @@ -9852,6 +9854,7 @@ static const uint16_t kNIDsInLongNameOrder[] = { 375 /* Trust Root */, 948 /* X25519 */, 964 /* X25519Kyber768Draft00 */, + 965 /* X25519MLKEM768 */, 961 /* X448 */, 12 /* X509 */, 402 /* X509v3 AC Targeting */, diff --git a/crypto/obj/obj_mac.num b/crypto/obj/obj_mac.num index a0519aceeb..6e2a2aed64 100644 --- a/crypto/obj/obj_mac.num +++ b/crypto/obj/obj_mac.num @@ -952,3 +952,4 @@ X448 961 sha512_256 962 hkdf 963 X25519Kyber768Draft00 964 +X25519MLKEM768 965 diff --git a/crypto/obj/objects.txt b/crypto/obj/objects.txt index 3ad32ea3d1..655340710c 100644 --- a/crypto/obj/objects.txt +++ b/crypto/obj/objects.txt @@ -1334,6 +1334,7 @@ secg-scheme 14 3 : dhSinglePass-cofactorDH-sha512kdf-scheme # NIDs for post quantum hybrid KEMs in TLS (no corresponding OIDs). : X25519Kyber768Draft00 + : X25519MLKEM768 # See RFC 8410. 1 3 101 110 : X25519 diff --git a/go.mod b/go.mod index b1cb93eb73..b87a3c1f19 100644 --- a/go.mod +++ b/go.mod @@ -1,13 +1,14 @@ module boringssl.googlesource.com/boringssl -go 1.21 +go 1.22 require ( - golang.org/x/crypto v0.25.0 + golang.org/x/crypto v0.26.0 golang.org/x/net v0.27.0 ) require ( - golang.org/x/sys v0.22.0 // indirect - golang.org/x/term v0.22.0 // indirect + filippo.io/mlkem768 v0.0.0-20240821141156-859a9b3f2ff6 // indirect + golang.org/x/sys v0.24.0 // indirect + golang.org/x/term v0.23.0 // indirect ) diff --git a/go.sum b/go.sum index 709415cdb8..7424815df0 100644 --- a/go.sum +++ b/go.sum @@ -1,8 +1,10 @@ -golang.org/x/crypto v0.25.0 h1:ypSNr+bnYL2YhwoMt2zPxHFmbAN1KZs/njMG3hxUp30= -golang.org/x/crypto v0.25.0/go.mod h1:T+wALwcMOSE0kXgUAnPAHqTLW+XHgcELELW8VaDgm/M= +filippo.io/mlkem768 v0.0.0-20240821141156-859a9b3f2ff6 h1:A7gTX0HxgkmTtCgRtpWlhIuMBBszxW/02MXv55wHk4U= +filippo.io/mlkem768 v0.0.0-20240821141156-859a9b3f2ff6/go.mod h1:IkpYfciLz5fI/S4/Z0NlhR4cpv6ubCMDnIwAe0XiojA= +golang.org/x/crypto v0.26.0 h1:RrRspgV4mU+YwB4FYnuBoKsUapNIL5cohGAmSH3azsw= +golang.org/x/crypto v0.26.0/go.mod h1:GY7jblb9wI+FOo5y8/S2oY4zWP07AkOJ4+jxCqdqn54= golang.org/x/net v0.27.0 h1:5K3Njcw06/l2y9vpGCSdcxWOYHOUk3dVNGDXN+FvAys= golang.org/x/net v0.27.0/go.mod h1:dDi0PyhWNoiUOrAS8uXv/vnScO4wnHQO4mj9fn/RytE= -golang.org/x/sys v0.22.0 h1:RI27ohtqKCnwULzJLqkv897zojh5/DwS/ENaMzUOaWI= -golang.org/x/sys v0.22.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/term v0.22.0 h1:BbsgPEJULsl2fV/AT3v15Mjva5yXKQDyKf+TbDz7QJk= -golang.org/x/term v0.22.0/go.mod h1:F3qCibpT5AMpCRfhfT53vVJwhLtIVHhB9XDjfFvnMI4= +golang.org/x/sys v0.24.0 h1:Twjiwq9dn6R1fQcyiK+wQyHWfaz/BJB+YIpzU/Cv3Xg= +golang.org/x/sys v0.24.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/term v0.23.0 h1:F6D4vR+EHoL9/sWAWgAR1H2DcHr4PareCbAaCo1RpuU= +golang.org/x/term v0.23.0/go.mod h1:DgV24QBUrK6jhZXl+20l6UWznPlwAHm1Q1mGHtydmSk= diff --git a/include/openssl/nid.h b/include/openssl/nid.h index 4dd8841b1e..02d78c6483 100644 --- a/include/openssl/nid.h +++ b/include/openssl/nid.h @@ -4255,6 +4255,9 @@ extern "C" { #define SN_X25519Kyber768Draft00 "X25519Kyber768Draft00" #define NID_X25519Kyber768Draft00 964 +#define SN_X25519MLKEM768 "X25519MLKEM768" +#define NID_X25519MLKEM768 965 + #if defined(__cplusplus) } /* extern C */ diff --git a/include/openssl/ssl.h b/include/openssl/ssl.h index 1dbc1e7dcb..a0797cd611 100644 --- a/include/openssl/ssl.h +++ b/include/openssl/ssl.h @@ -2548,6 +2548,7 @@ OPENSSL_EXPORT size_t SSL_CTX_get_num_tickets(const SSL_CTX *ctx); #define SSL_GROUP_SECP384R1 24 #define SSL_GROUP_SECP521R1 25 #define SSL_GROUP_X25519 29 +#define SSL_GROUP_X25519_MLKEM768 0x11ec #define SSL_GROUP_X25519_KYBER768_DRAFT00 0x6399 // SSL_CTX_set1_group_ids sets the preferred groups for |ctx| to |group_ids|. diff --git a/ssl/extensions.cc b/ssl/extensions.cc index 7f06dedbbe..30591a6a07 100644 --- a/ssl/extensions.cc +++ b/ssl/extensions.cc @@ -207,6 +207,7 @@ static bool tls1_check_duplicate_extensions(const CBS *cbs) { static bool is_post_quantum_group(uint16_t id) { switch (id) { case SSL_GROUP_X25519_KYBER768_DRAFT00: + case SSL_GROUP_X25519_MLKEM768: return true; default: return false; diff --git a/ssl/ssl_key_share.cc b/ssl/ssl_key_share.cc index 419724c624..923b8bb483 100644 --- a/ssl/ssl_key_share.cc +++ b/ssl/ssl_key_share.cc @@ -28,6 +28,7 @@ #include #include #include +#include #include #include #include @@ -192,6 +193,7 @@ class X25519KeyShare : public SSLKeyShare { uint8_t private_key_[32]; }; +// draft-tls-westerbaan-xyber768d00-03 class X25519Kyber768KeyShare : public SSLKeyShare { public: X25519Kyber768KeyShare() {} @@ -225,9 +227,7 @@ class X25519Kyber768KeyShare : public SSLKeyShare { uint8_t x25519_public_key[32]; X25519_keypair(x25519_public_key, x25519_private_key_); KYBER_public_key peer_kyber_pub; - CBS peer_key_cbs; - CBS peer_x25519_cbs; - CBS peer_kyber_cbs; + CBS peer_key_cbs, peer_x25519_cbs, peer_kyber_cbs; CBS_init(&peer_key_cbs, peer_key.data(), peer_key.size()); if (!CBS_get_bytes(&peer_key_cbs, &peer_x25519_cbs, 32) || !CBS_get_bytes(&peer_key_cbs, &peer_kyber_cbs, @@ -282,6 +282,97 @@ class X25519Kyber768KeyShare : public SSLKeyShare { KYBER_private_key kyber_private_key_; }; +// draft-kwiatkowski-tls-ecdhe-mlkem-01 +class X25519MLKEM768KeyShare : public SSLKeyShare { + public: + X25519MLKEM768KeyShare() {} + + uint16_t GroupID() const override { return SSL_GROUP_X25519_MLKEM768; } + + bool Generate(CBB *out) override { + uint8_t mlkem_public_key[MLKEM768_PUBLIC_KEY_BYTES]; + MLKEM768_generate_key(mlkem_public_key, /*optional_out_seed=*/nullptr, + &mlkem_private_key_); + + uint8_t x25519_public_key[X25519_PUBLIC_VALUE_LEN]; + X25519_keypair(x25519_public_key, x25519_private_key_); + + if (!CBB_add_bytes(out, mlkem_public_key, sizeof(mlkem_public_key)) || + !CBB_add_bytes(out, x25519_public_key, sizeof(x25519_public_key))) { + return false; + } + + return true; + } + + bool Encap(CBB *out_ciphertext, Array *out_secret, + uint8_t *out_alert, Span peer_key) override { + Array secret; + if (!secret.Init(MLKEM_SHARED_SECRET_BYTES + X25519_SHARED_KEY_LEN)) { + return false; + } + + MLKEM768_public_key peer_mlkem_pub; + uint8_t x25519_public_key[X25519_PUBLIC_VALUE_LEN]; + X25519_keypair(x25519_public_key, x25519_private_key_); + CBS peer_key_cbs, peer_mlkem_cbs, peer_x25519_cbs; + CBS_init(&peer_key_cbs, peer_key.data(), peer_key.size()); + if (!CBS_get_bytes(&peer_key_cbs, &peer_mlkem_cbs, + MLKEM768_PUBLIC_KEY_BYTES) || + !MLKEM768_parse_public_key(&peer_mlkem_pub, &peer_mlkem_cbs) || + !CBS_get_bytes(&peer_key_cbs, &peer_x25519_cbs, + X25519_PUBLIC_VALUE_LEN) || + CBS_len(&peer_key_cbs) != 0 || + !X25519(secret.data() + MLKEM_SHARED_SECRET_BYTES, x25519_private_key_, + CBS_data(&peer_x25519_cbs))) { + *out_alert = SSL_AD_DECODE_ERROR; + OPENSSL_PUT_ERROR(SSL, SSL_R_BAD_ECPOINT); + return false; + } + + uint8_t mlkem_ciphertext[MLKEM768_CIPHERTEXT_BYTES]; + MLKEM768_encap(mlkem_ciphertext, secret.data(), &peer_mlkem_pub); + + if (!CBB_add_bytes(out_ciphertext, mlkem_ciphertext, + sizeof(mlkem_ciphertext)) || + !CBB_add_bytes(out_ciphertext, x25519_public_key, + sizeof(x25519_public_key))) { + return false; + } + + *out_secret = std::move(secret); + return true; + } + + bool Decap(Array *out_secret, uint8_t *out_alert, + Span ciphertext) override { + *out_alert = SSL_AD_INTERNAL_ERROR; + + Array secret; + if (!secret.Init(MLKEM_SHARED_SECRET_BYTES + X25519_SHARED_KEY_LEN)) { + return false; + } + + if (ciphertext.size() != + MLKEM768_CIPHERTEXT_BYTES + X25519_PUBLIC_VALUE_LEN || + !MLKEM768_decap(secret.data(), ciphertext.data(), + MLKEM768_CIPHERTEXT_BYTES, &mlkem_private_key_) || + !X25519(secret.data() + MLKEM_SHARED_SECRET_BYTES, x25519_private_key_, + ciphertext.data() + MLKEM768_CIPHERTEXT_BYTES)) { + *out_alert = SSL_AD_DECODE_ERROR; + OPENSSL_PUT_ERROR(SSL, SSL_R_BAD_ECPOINT); + return false; + } + + *out_secret = std::move(secret); + return true; + } + + private: + uint8_t x25519_private_key_[32]; + MLKEM768_private_key mlkem_private_key_; +}; + constexpr NamedGroup kNamedGroups[] = { {NID_secp224r1, SSL_GROUP_SECP224R1, "P-224", "secp224r1"}, {NID_X9_62_prime256v1, SSL_GROUP_SECP256R1, "P-256", "prime256v1"}, @@ -290,6 +381,7 @@ constexpr NamedGroup kNamedGroups[] = { {NID_X25519, SSL_GROUP_X25519, "X25519", "x25519"}, {NID_X25519Kyber768Draft00, SSL_GROUP_X25519_KYBER768_DRAFT00, "X25519Kyber768Draft00", ""}, + {NID_X25519MLKEM768, SSL_GROUP_X25519_MLKEM768, "X25519MLKEM768", ""}, }; } // namespace @@ -312,6 +404,8 @@ UniquePtr SSLKeyShare::Create(uint16_t group_id) { return MakeUnique(); case SSL_GROUP_X25519_KYBER768_DRAFT00: return MakeUnique(); + case SSL_GROUP_X25519_MLKEM768: + return MakeUnique(); default: return nullptr; } diff --git a/ssl/ssl_test.cc b/ssl/ssl_test.cc index 1b71c97ba4..393b48e004 100644 --- a/ssl/ssl_test.cc +++ b/ssl/ssl_test.cc @@ -489,6 +489,10 @@ static const CurveTest kCurveTests[] = { "P-256:X25519Kyber768Draft00", { SSL_GROUP_SECP256R1, SSL_GROUP_X25519_KYBER768_DRAFT00 }, }, + { + "P-256:X25519MLKEM768", + { SSL_GROUP_SECP256R1, SSL_GROUP_X25519_MLKEM768 }, + }, { "P-256:P-384:P-521:X25519", diff --git a/ssl/test/fuzzer.h b/ssl/test/fuzzer.h index e6d2d0219d..ca7b55a77a 100644 --- a/ssl/test/fuzzer.h +++ b/ssl/test/fuzzer.h @@ -419,8 +419,9 @@ class TLSFuzzer { } static const uint16_t kGroups[] = { - SSL_GROUP_X25519_KYBER768_DRAFT00, SSL_GROUP_X25519, - SSL_GROUP_SECP256R1, SSL_GROUP_SECP384R1, SSL_GROUP_SECP521R1}; + SSL_GROUP_X25519_MLKEM768, SSL_GROUP_X25519_KYBER768_DRAFT00, + 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 5907a35cbc..2eedd6283a 100644 --- a/ssl/test/runner/common.go +++ b/ssl/test/runner/common.go @@ -158,6 +158,7 @@ const ( CurveP384 CurveID = 24 CurveP521 CurveID = 25 CurveX25519 CurveID = 29 + CurveX25519MLKEM768 CurveID = 0x11ec CurveX25519Kyber768 CurveID = 0x6399 ) @@ -1962,9 +1963,9 @@ type ProtocolBugs struct { // hello retry. FailIfHelloRetryRequested bool - // FailedIfKyberOffered will cause a server to reject a ClientHello if Kyber - // is supported. - FailIfKyberOffered bool + // FailIfPostQuantumOffered will cause a server to reject a ClientHello if + // post-quantum curves are supported. + FailIfPostQuantumOffered bool // ExpectKeyShares, if not nil, lists (in order) the curves that a ClientHello // should have key shares for. @@ -2067,7 +2068,7 @@ func (c *Config) maxVersion(isDTLS bool) uint16 { return ret } -var defaultCurvePreferences = []CurveID{CurveX25519Kyber768, CurveX25519, CurveP256, CurveP384, CurveP521} +var defaultCurvePreferences = []CurveID{CurveX25519MLKEM768, CurveX25519Kyber768, CurveX25519, CurveP256, CurveP384, CurveP521} func (c *Config) curvePreferences() []CurveID { if c == nil || len(c.CurvePreferences) == 0 { diff --git a/ssl/test/runner/handshake_server.go b/ssl/test/runner/handshake_server.go index 9e647df0cf..fa7eb9c86d 100644 --- a/ssl/test/runner/handshake_server.go +++ b/ssl/test/runner/handshake_server.go @@ -281,10 +281,10 @@ func (hs *serverHandshakeState) readClientHello() error { } } - if config.Bugs.FailIfKyberOffered { + if config.Bugs.FailIfPostQuantumOffered { for _, offeredCurve := range hs.clientHello.supportedCurves { if isPqGroup(offeredCurve) { - return errors.New("tls: X25519Kyber768 was offered") + return errors.New("tls: post-quantum group was offered") } } } diff --git a/ssl/test/runner/key_agreement.go b/ssl/test/runner/key_agreement.go index aff0820e3c..a4bbfa5961 100644 --- a/ssl/test/runner/key_agreement.go +++ b/ssl/test/runner/key_agreement.go @@ -16,8 +16,10 @@ import ( "fmt" "io" "math/big" + "slices" "boringssl.googlesource.com/boringssl/ssl/test/runner/kyber" + "filippo.io/mlkem768" "golang.org/x/crypto/curve25519" ) @@ -233,6 +235,9 @@ func (ka *rsaKeyAgreement) peerSignatureAlgorithm() signatureAlgorithm { // A kemImplementation is an instance of KEM-style construction for TLS. type kemImplementation interface { + encapsulationKeySize() int + ciphertextSize() int + // generate generates a keypair using rand. It returns the encoded public key. generate(rand io.Reader) (publicKey []byte, err error) @@ -253,6 +258,15 @@ type ecdhKEM struct { sendCompressed bool } +func (e *ecdhKEM) encapsulationKeySize() int { + fieldBytes := (e.curve.Params().BitSize + 7) / 8 + return 1 + 2*fieldBytes +} + +func (e *ecdhKEM) ciphertextSize() int { + return e.encapsulationKeySize() +} + func (e *ecdhKEM) generate(rand io.Reader) (publicKey []byte, err error) { var x, y *big.Int e.privateKey, x, y, err = elliptic.GenerateKey(e.curve, rand) @@ -300,6 +314,14 @@ type x25519KEM struct { setHighBit bool } +func (e *x25519KEM) encapsulationKeySize() int { + return curve25519.PointSize +} + +func (e *x25519KEM) ciphertextSize() int { + return curve25519.PointSize +} + func (e *x25519KEM) generate(rand io.Reader) (publicKey []byte, err error) { _, err = io.ReadFull(rand, e.privateKey[:]) if err != nil { @@ -341,53 +363,35 @@ func (e *x25519KEM) decap(ciphertext []byte) (secret []byte, err error) { return out[:], nil } -// kyberKEM implements Kyber combined with X25519. +// kyberKEM implements Kyber-768 type kyberKEM struct { - x25519PrivateKey [32]byte - kyberPrivateKey *kyber.PrivateKey + kyberPrivateKey *kyber.PrivateKey } -func (e *kyberKEM) generate(rand io.Reader) (publicKey []byte, err error) { - if _, err := io.ReadFull(rand, e.x25519PrivateKey[:]); err != nil { - return nil, err - } - var x25519Public [32]byte - curve25519.ScalarBaseMult(&x25519Public, &e.x25519PrivateKey) +func (e *kyberKEM) encapsulationKeySize() int { + return kyber.PublicKeySize +} + +func (e *kyberKEM) ciphertextSize() int { + return kyber.CiphertextSize +} +func (e *kyberKEM) generate(rand io.Reader) (publicKey []byte, err error) { var kyberEntropy [64]byte if _, err := io.ReadFull(rand, kyberEntropy[:]); err != nil { return nil, err } var kyberPublic *[kyber.PublicKeySize]byte e.kyberPrivateKey, kyberPublic = kyber.NewPrivateKey(&kyberEntropy) - - var ret []byte - ret = append(ret, x25519Public[:]...) - ret = append(ret, kyberPublic[:]...) - return ret, nil + return kyberPublic[:], nil } func (e *kyberKEM) encap(rand io.Reader, peerKey []byte) (ciphertext []byte, secret []byte, err error) { - if len(peerKey) != 32+kyber.PublicKeySize { + if len(peerKey) != kyber.PublicKeySize { return nil, nil, errors.New("tls: bad length Kyber offer") } - if _, err := io.ReadFull(rand, e.x25519PrivateKey[:]); err != nil { - return nil, nil, err - } - - var x25519Shared, x25519PeerKey, x25519Public [32]byte - copy(x25519PeerKey[:], peerKey) - curve25519.ScalarBaseMult(&x25519Public, &e.x25519PrivateKey) - curve25519.ScalarMult(&x25519Shared, &e.x25519PrivateKey, &x25519PeerKey) - - // Per RFC 7748, reject the all-zero value in constant time. - var zeros [32]byte - if subtle.ConstantTimeCompare(zeros[:], x25519Shared[:]) == 1 { - return nil, nil, errors.New("tls: X25519 value with wrong order") - } - - kyberPublicKey, ok := kyber.UnmarshalPublicKey((*[kyber.PublicKeySize]byte)(peerKey[32:])) + kyberPublicKey, ok := kyber.UnmarshalPublicKey((*[kyber.PublicKeySize]byte)(peerKey)) if !ok { return nil, nil, errors.New("tls: bad Kyber offer") } @@ -397,37 +401,105 @@ func (e *kyberKEM) encap(rand io.Reader, peerKey []byte) (ciphertext []byte, sec return nil, nil, err } kyberCiphertext := kyberPublicKey.Encap(kyberShared[:], &kyberEntropy) - - ciphertext = append(ciphertext, x25519Public[:]...) - ciphertext = append(ciphertext, kyberCiphertext[:]...) - secret = append(secret, x25519Shared[:]...) - secret = append(secret, kyberShared[:]...) - - return ciphertext, secret, nil + return kyberCiphertext[:], kyberShared[:], nil } func (e *kyberKEM) decap(ciphertext []byte) (secret []byte, err error) { - if len(ciphertext) != 32+kyber.CiphertextSize { + if len(ciphertext) != kyber.CiphertextSize { return nil, errors.New("tls: bad length Kyber reply") } - var x25519Shared, x25519PeerKey [32]byte - copy(x25519PeerKey[:], ciphertext) - curve25519.ScalarMult(&x25519Shared, &e.x25519PrivateKey, &x25519PeerKey) + var kyberShared [32]byte + e.kyberPrivateKey.Decap(kyberShared[:], (*[kyber.CiphertextSize]byte)(ciphertext)) + return kyberShared[:], nil +} - // Per RFC 7748, reject the all-zero value in constant time. - var zeros [32]byte - if subtle.ConstantTimeCompare(zeros[:], x25519Shared[:]) == 1 { - return nil, errors.New("tls: X25519 value with wrong order") +// mlkem768KEM implements ML-KEM-768 +type mlkem768KEM struct { + decapKey *mlkem768.DecapsulationKey +} + +func (e *mlkem768KEM) encapsulationKeySize() int { + return mlkem768.EncapsulationKeySize +} + +func (e *mlkem768KEM) ciphertextSize() int { + return mlkem768.CiphertextSize +} + +func (m *mlkem768KEM) generate(rand io.Reader) (publicKey []byte, err error) { + m.decapKey, err = mlkem768.GenerateKey() + if err != nil { + return } + return m.decapKey.EncapsulationKey(), nil +} - var kyberShared [32]byte - e.kyberPrivateKey.Decap(kyberShared[:], (*[kyber.CiphertextSize]byte)(ciphertext[32:])) +func (m *mlkem768KEM) encap(rand io.Reader, peerKey []byte) (ciphertext []byte, secret []byte, err error) { + return mlkem768.Encapsulate(peerKey) +} - secret = append(secret, x25519Shared[:]...) - secret = append(secret, kyberShared[:]...) +func (m *mlkem768KEM) decap(ciphertext []byte) (secret []byte, err error) { + return mlkem768.Decapsulate(m.decapKey, ciphertext) +} - return secret, nil +// concatKEM concatenates two kemImplementations. +type concatKEM struct { + kem1, kem2 kemImplementation +} + +func (c *concatKEM) encapsulationKeySize() int { + return c.kem1.encapsulationKeySize() + c.kem2.encapsulationKeySize() +} + +func (c *concatKEM) ciphertextSize() int { + return c.kem1.ciphertextSize() + c.kem2.ciphertextSize() +} + +func (c *concatKEM) generate(rand io.Reader) (publicKey []byte, err error) { + publicKey1, err := c.kem1.generate(rand) + if err != nil { + return nil, err + } + publicKey2, err := c.kem2.generate(rand) + if err != nil { + return nil, err + } + return slices.Concat(publicKey1, publicKey2), nil +} + +func (c *concatKEM) encap(rand io.Reader, peerKey []byte) (ciphertext []byte, secret []byte, err error) { + encapKeySize1 := c.kem1.encapsulationKeySize() + if len(peerKey) < encapKeySize1 { + return nil, nil, errors.New("tls: invalid peer key") + } + peerKey1, peerKey2 := peerKey[:encapKeySize1], peerKey[encapKeySize1:] + ciphertext1, secret1, err := c.kem1.encap(rand, peerKey1) + if err != nil { + return nil, nil, err + } + ciphertext2, secret2, err := c.kem2.encap(rand, peerKey2) + if err != nil { + return nil, nil, err + } + return slices.Concat(ciphertext1, ciphertext2), slices.Concat(secret1, secret2), nil +} + +func (c *concatKEM) decap(ciphertext []byte) (secret []byte, err error) { + ciphertextSize1 := c.kem1.ciphertextSize() + if len(ciphertext) < ciphertextSize1 { + return nil, errors.New("tls: invalid ciphertext") + } + ciphertext1, ciphertext2 := ciphertext[:ciphertextSize1], ciphertext[ciphertextSize1:] + secret1, err := c.kem1.decap(ciphertext1) + if err != nil { + return nil, err + } + secret2, err := c.kem2.decap(ciphertext2) + if err != nil { + return nil, err + } + return slices.Concat(secret1, secret2), nil } func kemForCurveID(id CurveID, config *Config) (kemImplementation, bool) { @@ -443,7 +515,11 @@ func kemForCurveID(id CurveID, config *Config) (kemImplementation, bool) { case CurveX25519: return &x25519KEM{setHighBit: config.Bugs.SetX25519HighBit}, true case CurveX25519Kyber768: - return &kyberKEM{}, true + // draft-tls-westerbaan-xyber768d00-03 + return &concatKEM{kem1: &x25519KEM{setHighBit: config.Bugs.SetX25519HighBit}, kem2: &kyberKEM{}}, true + case CurveX25519MLKEM768: + // draft-kwiatkowski-tls-ecdhe-mlkem-01 + return &concatKEM{kem1: &mlkem768KEM{}, kem2: &x25519KEM{setHighBit: config.Bugs.SetX25519HighBit}}, true default: return nil, false } diff --git a/ssl/test/runner/runner.go b/ssl/test/runner/runner.go index 16cebb23e8..6179ee0f5d 100644 --- a/ssl/test/runner/runner.go +++ b/ssl/test/runner/runner.go @@ -11756,12 +11756,13 @@ var testCurves = []struct { {"P-521", CurveP521}, {"X25519", CurveX25519}, {"Kyber", CurveX25519Kyber768}, + {"MLKEM", CurveX25519MLKEM768}, } const bogusCurve = 0x1234 func isPqGroup(r CurveID) bool { - return r == CurveX25519Kyber768 + return r == CurveX25519Kyber768 || r == CurveX25519MLKEM768 } func addCurveTests() { @@ -12225,78 +12226,100 @@ func addCurveTests() { }, }) - // Kyber should not be offered by a TLS < 1.3 client. - testCases = append(testCases, testCase{ - name: "KyberNotInTLS12", - config: Config{ - Bugs: ProtocolBugs{ - FailIfKyberOffered: true, + // Post-quantum groups require TLS 1.3. + for _, curve := range testCurves { + if !isPqGroup(curve.id) { + continue + } + + // Post-quantum groups should not be offered by a TLS 1.2 client. + testCases = append(testCases, testCase{ + name: "TLS12ClientShouldNotOffer-" + curve.name, + config: Config{ + Bugs: ProtocolBugs{ + FailIfPostQuantumOffered: true, + }, }, - }, - flags: []string{ - "-max-version", strconv.Itoa(VersionTLS12), - "-curves", strconv.Itoa(int(CurveX25519Kyber768)), - "-curves", strconv.Itoa(int(CurveX25519)), - }, - }) + flags: []string{ + "-max-version", strconv.Itoa(VersionTLS12), + "-curves", strconv.Itoa(int(curve.id)), + "-curves", strconv.Itoa(int(CurveX25519)), + }, + }) - // Kyber should not crash a TLS < 1.3 client if the server mistakenly - // selects it. - testCases = append(testCases, testCase{ - name: "KyberNotAcceptedByTLS12Client", - config: Config{ - Bugs: ProtocolBugs{ - SendCurve: CurveX25519Kyber768, + // Post-quantum groups should not be selected by a TLS 1.2 server. + testCases = append(testCases, testCase{ + testType: serverTest, + name: "TLS12ServerShouldNotSelect-" + curve.name, + flags: []string{ + "-max-version", strconv.Itoa(VersionTLS12), + "-curves", strconv.Itoa(int(curve.id)), + "-curves", strconv.Itoa(int(CurveX25519)), }, - }, - flags: []string{ - "-max-version", strconv.Itoa(VersionTLS12), - "-curves", strconv.Itoa(int(CurveX25519Kyber768)), - "-curves", strconv.Itoa(int(CurveX25519)), - }, - shouldFail: true, - expectedError: ":WRONG_CURVE:", - }) + expectations: connectionExpectations{ + curveID: CurveX25519, + }, + }) + + // If a TLS 1.2 server selects a post-quantum group anyway, the client + // should not accept it. + testCases = append(testCases, testCase{ + name: "ClientShouldNotAllowInTLS12-" + curve.name, + config: Config{ + MaxVersion: VersionTLS12, + Bugs: ProtocolBugs{ + SendCurve: curve.id, + }, + }, + flags: []string{ + "-curves", strconv.Itoa(int(curve.id)), + "-curves", strconv.Itoa(int(CurveX25519)), + }, + shouldFail: true, + expectedError: ":WRONG_CURVE:", + expectedLocalError: "remote error: illegal parameter", + }) + } - // Kyber should not be offered by default as a client. + // ML-KEM and Kyber should not be offered by default as a client. testCases = append(testCases, testCase{ - name: "KyberNotEnabledByDefaultInClients", + name: "PostQuantumNotEnabledByDefaultInClients", config: Config{ MinVersion: VersionTLS13, Bugs: ProtocolBugs{ - FailIfKyberOffered: true, + FailIfPostQuantumOffered: true, }, }, }) - // If Kyber is offered, both X25519 and Kyber should have a key-share. + // If ML-KEM is offered, both X25519 and ML-KEM should have a key-share. testCases = append(testCases, testCase{ - name: "NotJustKyberKeyShare", + name: "NotJustMLKEMKeyShare", config: Config{ MinVersion: VersionTLS13, Bugs: ProtocolBugs{ - ExpectedKeyShares: []CurveID{CurveX25519Kyber768, CurveX25519}, + ExpectedKeyShares: []CurveID{CurveX25519MLKEM768, CurveX25519}, }, }, flags: []string{ - "-curves", strconv.Itoa(int(CurveX25519Kyber768)), + "-curves", strconv.Itoa(int(CurveX25519MLKEM768)), "-curves", strconv.Itoa(int(CurveX25519)), - "-expect-curve-id", strconv.Itoa(int(CurveX25519Kyber768)), + "-expect-curve-id", strconv.Itoa(int(CurveX25519MLKEM768)), }, }) // ... and the other way around testCases = append(testCases, testCase{ - name: "KyberKeyShareIncludedSecond", + name: "MLKEMKeyShareIncludedSecond", config: Config{ MinVersion: VersionTLS13, Bugs: ProtocolBugs{ - ExpectedKeyShares: []CurveID{CurveX25519, CurveX25519Kyber768}, + ExpectedKeyShares: []CurveID{CurveX25519, CurveX25519MLKEM768}, }, }, flags: []string{ "-curves", strconv.Itoa(int(CurveX25519)), - "-curves", strconv.Itoa(int(CurveX25519Kyber768)), + "-curves", strconv.Itoa(int(CurveX25519MLKEM768)), "-expect-curve-id", strconv.Itoa(int(CurveX25519)), }, }) @@ -12305,44 +12328,61 @@ func addCurveTests() { // first classical and first post-quantum "curves" that get key shares // included. testCases = append(testCases, testCase{ - name: "KyberKeyShareIncludedThird", + name: "MLKEMKeyShareIncludedThird", config: Config{ MinVersion: VersionTLS13, Bugs: ProtocolBugs{ - ExpectedKeyShares: []CurveID{CurveX25519, CurveX25519Kyber768}, + ExpectedKeyShares: []CurveID{CurveX25519, CurveX25519MLKEM768}, }, }, flags: []string{ "-curves", strconv.Itoa(int(CurveX25519)), "-curves", strconv.Itoa(int(CurveP256)), - "-curves", strconv.Itoa(int(CurveX25519Kyber768)), + "-curves", strconv.Itoa(int(CurveX25519MLKEM768)), "-expect-curve-id", strconv.Itoa(int(CurveX25519)), }, }) - // If Kyber is the only configured curve, the key share is sent. + // 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}, + }, + }, + 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: "JustConfiguringKyberWorks", + name: "BothMLKEMAndKyber", config: Config{ MinVersion: VersionTLS13, Bugs: ProtocolBugs{ - ExpectedKeyShares: []CurveID{CurveX25519Kyber768}, + ExpectedKeyShares: []CurveID{CurveX25519MLKEM768}, }, }, flags: []string{ + "-curves", strconv.Itoa(int(CurveX25519MLKEM768)), "-curves", strconv.Itoa(int(CurveX25519Kyber768)), - "-expect-curve-id", strconv.Itoa(int(CurveX25519Kyber768)), + "-expect-curve-id", strconv.Itoa(int(CurveX25519MLKEM768)), }, }) - // As a server, Kyber is not yet supported by default. + // As a server, ML-KEM is not yet supported by default. testCases = append(testCases, testCase{ testType: serverTest, - name: "KyberNotEnabledByDefaultForAServer", + name: "PostQuantumNotEnabledByDefaultForAServer", config: Config{ MinVersion: VersionTLS13, - CurvePreferences: []CurveID{CurveX25519Kyber768, CurveX25519}, - DefaultCurves: []CurveID{CurveX25519Kyber768}, + CurvePreferences: []CurveID{CurveX25519MLKEM768, CurveX25519Kyber768, CurveX25519}, + DefaultCurves: []CurveID{CurveX25519MLKEM768, CurveX25519Kyber768}, }, flags: []string{ "-server-preference",