crypto/tls: add SecP256r1/SecP384r1MLKEM1024 hybrid post-quantum key exchanges

Fixes #71206

Change-Id: If3cf75261c56828b87ae6805bd2913f56a6a6964
Reviewed-on: https://go-review.googlesource.com/c/go/+/722140
Auto-Submit: Filippo Valsorda <filippo@golang.org>
Reviewed-by: Cherry Mui <cherryyz@google.com>
Reviewed-by: Roland Shoemaker <roland@golang.org>
LUCI-TryBot-Result: Go LUCI <golang-scoped@luci-project-accounts.iam.gserviceaccount.com>
This commit is contained in:
Filippo Valsorda 2025-11-19 17:32:42 +01:00 committed by Gopher Robot
parent a9093067ee
commit 1768cb40b8
16 changed files with 405 additions and 201 deletions

4
api/next/71206.txt Normal file
View file

@ -0,0 +1,4 @@
pkg crypto/tls, const SecP256r1MLKEM768 = 4587 #71206
pkg crypto/tls, const SecP256r1MLKEM768 CurveID #71206
pkg crypto/tls, const SecP384r1MLKEM1024 = 4589 #71206
pkg crypto/tls, const SecP384r1MLKEM1024 CurveID #71206

View file

@ -168,6 +168,10 @@ allows malformed hostnames containing colons outside of a bracketed IPv6 address
The default `urlstrictcolons=1` rejects URLs such as `http://localhost:1:2` or `http://::1/`.
Colons are permitted as part of a bracketed IPv6 address, such as `http://[::1]/`.
Go 1.26 enabled two additional post-quantum key exchange mechanisms:
SecP256r1MLKEM768 and SecP384r1MLKEM1024. The default can be reverted using the
[`tlssecpmlkem` setting](/pkg/crypto/tls/#Config.CurvePreferences).
Go 1.26 added a new `tracebacklabels` setting that controls the inclusion of
goroutine labels set through the the `runtime/pprof` package. Setting `tracebacklabels=1`
includes these key/value pairs in the goroutine status header of runtime

View file

@ -0,0 +1,3 @@
The hybrid [SecP256r1MLKEM768] and [SecP384r1MLKEM1024] post-quantum key
exchanges are now enabled by default. They can be disabled by setting
[Config.CurvePreferences] or with the `tlssecpmlkem=0` GODEBUG setting.

View file

@ -219,7 +219,9 @@
24,
25,
29,
4588
4587,
4588,
4589
],
"ErrorMap": {
":ECH_REJECTED:": ["tls: server rejected ECH"]

View file

@ -145,19 +145,31 @@ const (
type CurveID uint16
const (
CurveP256 CurveID = 23
CurveP384 CurveID = 24
CurveP521 CurveID = 25
X25519 CurveID = 29
X25519MLKEM768 CurveID = 4588
CurveP256 CurveID = 23
CurveP384 CurveID = 24
CurveP521 CurveID = 25
X25519 CurveID = 29
X25519MLKEM768 CurveID = 4588
SecP256r1MLKEM768 CurveID = 4587
SecP384r1MLKEM1024 CurveID = 4589
)
func isTLS13OnlyKeyExchange(curve CurveID) bool {
return curve == X25519MLKEM768
switch curve {
case X25519MLKEM768, SecP256r1MLKEM768, SecP384r1MLKEM1024:
return true
default:
return false
}
}
func isPQKeyExchange(curve CurveID) bool {
return curve == X25519MLKEM768
switch curve {
case X25519MLKEM768, SecP256r1MLKEM768, SecP384r1MLKEM1024:
return true
default:
return false
}
}
// TLS 1.3 Key Share. See RFC 8446, Section 4.2.8.
@ -787,6 +799,11 @@ type Config struct {
// From Go 1.24, the default includes the [X25519MLKEM768] hybrid
// post-quantum key exchange. To disable it, set CurvePreferences explicitly
// or use the GODEBUG=tlsmlkem=0 environment variable.
//
// From Go 1.26, the default includes the [SecP256r1MLKEM768] and
// [SecP256r1MLKEM768] hybrid post-quantum key exchanges, too. To disable
// them, set CurvePreferences explicitly or use either the
// GODEBUG=tlsmlkem=0 or the GODEBUG=tlssecpmlkem=0 environment variable.
CurvePreferences []CurveID
// DynamicRecordSizingDisabled disables adaptive sizing of TLS records.

View file

@ -72,16 +72,19 @@ func _() {
_ = x[CurveP521-25]
_ = x[X25519-29]
_ = x[X25519MLKEM768-4588]
_ = x[SecP256r1MLKEM768-4587]
_ = x[SecP384r1MLKEM1024-4589]
}
const (
_CurveID_name_0 = "CurveP256CurveP384CurveP521"
_CurveID_name_1 = "X25519"
_CurveID_name_2 = "X25519MLKEM768"
_CurveID_name_2 = "SecP256r1MLKEM768X25519MLKEM768SecP384r1MLKEM1024"
)
var (
_CurveID_index_0 = [...]uint8{0, 9, 18, 27}
_CurveID_index_2 = [...]uint8{0, 17, 31, 49}
)
func (i CurveID) String() string {
@ -91,8 +94,9 @@ func (i CurveID) String() string {
return _CurveID_name_0[_CurveID_index_0[i]:_CurveID_index_0[i+1]]
case i == 29:
return _CurveID_name_1
case i == 4588:
return _CurveID_name_2
case 4587 <= i && i <= 4589:
i -= 4587
return _CurveID_name_2[_CurveID_index_2[i]:_CurveID_index_2[i+1]]
default:
return "CurveID(" + strconv.FormatInt(int64(i), 10) + ")"
}

View file

@ -14,14 +14,24 @@ import (
// them to apply local policies.
var tlsmlkem = godebug.New("tlsmlkem")
var tlssecpmlkem = godebug.New("tlssecpmlkem")
// defaultCurvePreferences is the default set of supported key exchanges, as
// well as the preference order.
func defaultCurvePreferences() []CurveID {
if tlsmlkem.Value() == "0" {
switch {
// tlsmlkem=0 restores the pre-Go 1.24 default.
case tlsmlkem.Value() == "0":
return []CurveID{X25519, CurveP256, CurveP384, CurveP521}
// tlssecpmlkem=0 restores the pre-Go 1.26 default.
case tlssecpmlkem.Value() == "0":
return []CurveID{X25519MLKEM768, X25519, CurveP256, CurveP384, CurveP521}
default:
return []CurveID{
X25519MLKEM768, SecP256r1MLKEM768, SecP384r1MLKEM1024,
X25519, CurveP256, CurveP384, CurveP521,
}
}
return []CurveID{X25519MLKEM768, X25519, CurveP256, CurveP384, CurveP521}
}
// defaultSupportedSignatureAlgorithms returns the signature and hash algorithms that

View file

@ -32,6 +32,8 @@ var (
}
allowedCurvePreferencesFIPS = []CurveID{
X25519MLKEM768,
SecP256r1MLKEM768,
SecP384r1MLKEM1024,
CurveP256,
CurveP384,
CurveP521,

View file

@ -43,11 +43,15 @@ func isTLS13CipherSuite(id uint16) bool {
}
func generateKeyShare(group CurveID) keyShare {
key, err := generateECDHEKey(rand.Reader, group)
ke, err := keyExchangeForCurveID(group)
if err != nil {
panic(err)
}
return keyShare{group: group, data: key.PublicKey().Bytes()}
_, shares, err := ke.keyShares(rand.Reader)
if err != nil {
panic(err)
}
return shares[0]
}
func TestFIPSServerProtocolVersion(t *testing.T) {
@ -132,7 +136,7 @@ func isFIPSCurve(id CurveID) bool {
switch id {
case CurveP256, CurveP384, CurveP521:
return true
case X25519MLKEM768:
case X25519MLKEM768, SecP256r1MLKEM768, SecP384r1MLKEM1024:
// Only for the native module.
return !boring.Enabled
case X25519:

View file

@ -11,7 +11,6 @@ import (
"crypto/ecdsa"
"crypto/ed25519"
"crypto/hpke"
"crypto/internal/fips140/mlkem"
"crypto/internal/fips140/tls13"
"crypto/rsa"
"crypto/subtle"
@ -142,43 +141,21 @@ func (c *Conn) makeClientHello() (*clientHelloMsg, *keySharePrivateKeys, *echCli
if len(hello.supportedCurves) == 0 {
return nil, nil, nil, errors.New("tls: no supported elliptic curves for ECDHE")
}
// Since the order is fixed, the first one is always the one to send a
// key share for. All the PQ hybrids sort first, and produce a fallback
// ECDH share.
curveID := hello.supportedCurves[0]
keyShareKeys = &keySharePrivateKeys{curveID: curveID}
// Note that if X25519MLKEM768 is supported, it will be first because
// the preference order is fixed.
if curveID == X25519MLKEM768 {
keyShareKeys.ecdhe, err = generateECDHEKey(config.rand(), X25519)
if err != nil {
return nil, nil, nil, err
}
seed := make([]byte, mlkem.SeedSize)
if _, err := io.ReadFull(config.rand(), seed); err != nil {
return nil, nil, nil, err
}
keyShareKeys.mlkem, err = mlkem.NewDecapsulationKey768(seed)
if err != nil {
return nil, nil, nil, err
}
mlkemEncapsulationKey := keyShareKeys.mlkem.EncapsulationKey().Bytes()
x25519EphemeralKey := keyShareKeys.ecdhe.PublicKey().Bytes()
hello.keyShares = []keyShare{
{group: X25519MLKEM768, data: append(mlkemEncapsulationKey, x25519EphemeralKey...)},
}
// If both X25519MLKEM768 and X25519 are supported, we send both key
// shares (as a fallback) and we reuse the same X25519 ephemeral
// key, as allowed by draft-ietf-tls-hybrid-design-09, Section 3.2.
if slices.Contains(hello.supportedCurves, X25519) {
hello.keyShares = append(hello.keyShares, keyShare{group: X25519, data: x25519EphemeralKey})
}
} else {
if _, ok := curveForCurveID(curveID); !ok {
return nil, nil, nil, errors.New("tls: CurvePreferences includes unsupported curve")
}
keyShareKeys.ecdhe, err = generateECDHEKey(config.rand(), curveID)
if err != nil {
return nil, nil, nil, err
}
hello.keyShares = []keyShare{{group: curveID, data: keyShareKeys.ecdhe.PublicKey().Bytes()}}
ke, err := keyExchangeForCurveID(curveID)
if err != nil {
return nil, nil, nil, errors.New("tls: CurvePreferences includes unsupported curve")
}
keyShareKeys, hello.keyShares, err = ke.keyShares(config.rand())
if err != nil {
return nil, nil, nil, err
}
// Only send the fallback ECDH share if the corresponding CurveID is enabled.
if len(hello.keyShares) == 2 && !slices.Contains(hello.supportedCurves, hello.keyShares[1].group) {
hello.keyShares = hello.keyShares[:1]
}
}

View file

@ -10,7 +10,6 @@ import (
"crypto"
"crypto/hkdf"
"crypto/hmac"
"crypto/internal/fips140/mlkem"
"crypto/internal/fips140/tls13"
"crypto/rsa"
"crypto/subtle"
@ -320,22 +319,18 @@ func (hs *clientHandshakeStateTLS13) processHelloRetryRequest() error {
c.sendAlert(alertIllegalParameter)
return errors.New("tls: server sent an unnecessary HelloRetryRequest key_share")
}
// Note: we don't support selecting X25519MLKEM768 in a HRR, because it
// is currently first in preference order, so if it's enabled we'll
// always send a key share for it.
//
// This will have to change once we support multiple hybrid KEMs.
if _, ok := curveForCurveID(curveID); !ok {
ke, err := keyExchangeForCurveID(curveID)
if err != nil {
c.sendAlert(alertInternalError)
return errors.New("tls: CurvePreferences includes unsupported curve")
}
key, err := generateECDHEKey(c.config.rand(), curveID)
hs.keyShareKeys, hello.keyShares, err = ke.keyShares(c.config.rand())
if err != nil {
c.sendAlert(alertInternalError)
return err
}
hs.keyShareKeys = &keySharePrivateKeys{curveID: curveID, ecdhe: key}
hello.keyShares = []keyShare{{group: curveID, data: key.PublicKey().Bytes()}}
// Do not send the fallback ECDH key share in a HRR response.
hello.keyShares = hello.keyShares[:1]
}
if len(hello.pskIdentities) > 0 {
@ -475,36 +470,16 @@ func (hs *clientHandshakeStateTLS13) processServerHello() error {
func (hs *clientHandshakeStateTLS13) establishHandshakeKeys() error {
c := hs.c
ecdhePeerData := hs.serverHello.serverShare.data
if hs.serverHello.serverShare.group == X25519MLKEM768 {
if len(ecdhePeerData) != mlkem.CiphertextSize768+x25519PublicKeySize {
c.sendAlert(alertIllegalParameter)
return errors.New("tls: invalid server X25519MLKEM768 key share")
}
ecdhePeerData = hs.serverHello.serverShare.data[mlkem.CiphertextSize768:]
ke, err := keyExchangeForCurveID(hs.serverHello.serverShare.group)
if err != nil {
c.sendAlert(alertInternalError)
return err
}
peerKey, err := hs.keyShareKeys.ecdhe.Curve().NewPublicKey(ecdhePeerData)
sharedKey, err := ke.clientSharedSecret(hs.keyShareKeys, hs.serverHello.serverShare.data)
if err != nil {
c.sendAlert(alertIllegalParameter)
return errors.New("tls: invalid server key share")
}
sharedKey, err := hs.keyShareKeys.ecdhe.ECDH(peerKey)
if err != nil {
c.sendAlert(alertIllegalParameter)
return errors.New("tls: invalid server key share")
}
if hs.serverHello.serverShare.group == X25519MLKEM768 {
if hs.keyShareKeys.mlkem == nil {
return c.sendAlert(alertInternalError)
}
ciphertext := hs.serverHello.serverShare.data[:mlkem.CiphertextSize768]
mlkemShared, err := hs.keyShareKeys.mlkem.Decapsulate(ciphertext)
if err != nil {
c.sendAlert(alertIllegalParameter)
return errors.New("tls: invalid X25519MLKEM768 server key share")
}
sharedKey = append(mlkemShared, sharedKey...)
}
c.curveID = hs.serverHello.serverShare.group
earlySecret := hs.earlySecret

View file

@ -11,7 +11,6 @@ import (
"crypto/hkdf"
"crypto/hmac"
"crypto/hpke"
"crypto/internal/fips140/mlkem"
"crypto/internal/fips140/tls13"
"crypto/rsa"
"crypto/tls/internal/fips140tls"
@ -246,55 +245,16 @@ func (hs *serverHandshakeStateTLS13) processClientHello() error {
}
c.curveID = selectedGroup
ecdhGroup := selectedGroup
ecdhData := clientKeyShare.data
if selectedGroup == X25519MLKEM768 {
ecdhGroup = X25519
if len(ecdhData) != mlkem.EncapsulationKeySize768+x25519PublicKeySize {
c.sendAlert(alertIllegalParameter)
return errors.New("tls: invalid X25519MLKEM768 client key share")
}
ecdhData = ecdhData[mlkem.EncapsulationKeySize768:]
}
if _, ok := curveForCurveID(ecdhGroup); !ok {
ke, err := keyExchangeForCurveID(selectedGroup)
if err != nil {
c.sendAlert(alertInternalError)
return errors.New("tls: CurvePreferences includes unsupported curve")
}
key, err := generateECDHEKey(c.config.rand(), ecdhGroup)
if err != nil {
c.sendAlert(alertInternalError)
return err
}
hs.hello.serverShare = keyShare{group: selectedGroup, data: key.PublicKey().Bytes()}
peerKey, err := key.Curve().NewPublicKey(ecdhData)
hs.sharedKey, hs.hello.serverShare, err = ke.serverSharedSecret(c.config.rand(), clientKeyShare.data)
if err != nil {
c.sendAlert(alertIllegalParameter)
return errors.New("tls: invalid client key share")
}
hs.sharedKey, err = key.ECDH(peerKey)
if err != nil {
c.sendAlert(alertIllegalParameter)
return errors.New("tls: invalid client key share")
}
if selectedGroup == X25519MLKEM768 {
k, err := mlkem.NewEncapsulationKey768(clientKeyShare.data[:mlkem.EncapsulationKeySize768])
if err != nil {
c.sendAlert(alertIllegalParameter)
return errors.New("tls: invalid X25519MLKEM768 client key share")
}
mlkemSharedSecret, ciphertext := k.Encapsulate()
// draft-kwiatkowski-tls-ecdhe-mlkem-02, Section 3.1.3: "For
// X25519MLKEM768, the shared secret is the concatenation of the ML-KEM
// shared secret and the X25519 shared secret. The shared secret is 64
// bytes (32 bytes for each part)."
hs.sharedKey = append(mlkemSharedSecret, hs.sharedKey...)
// draft-kwiatkowski-tls-ecdhe-mlkem-02, Section 3.1.2: "When the
// X25519MLKEM768 group is negotiated, the server's key exchange value
// is the concatenation of an ML-KEM ciphertext returned from
// encapsulation to the client's encapsulation key, and the server's
// ephemeral X25519 share."
hs.hello.serverShare.data = append(ciphertext, hs.hello.serverShare.data...)
}
selectedProto, err := negotiateALPN(c.config.NextProtos, hs.clientHello.alpnProtocols, c.quic != nil)
if err != nil {

View file

@ -159,17 +159,17 @@ func hashForServerKeyExchange(sigType uint8, hashFunc crypto.Hash, version uint1
type ecdheKeyAgreement struct {
version uint16
isRSA bool
key *ecdh.PrivateKey
// ckx and preMasterSecret are generated in processServerKeyExchange
// and returned in generateClientKeyExchange.
ckx *clientKeyExchangeMsg
preMasterSecret []byte
// curveID and signatureAlgorithm are set by processServerKeyExchange and
// generateServerKeyExchange.
// curveID, signatureAlgorithm, and key are set by processServerKeyExchange
// and generateServerKeyExchange.
curveID CurveID
signatureAlgorithm SignatureScheme
key *ecdh.PrivateKey
}
func (ka *ecdheKeyAgreement) generateServerKeyExchange(config *Config, cert *Certificate, clientHello *clientHelloMsg, hello *serverHelloMsg) (*serverKeyExchangeMsg, error) {
@ -380,3 +380,29 @@ func (ka *ecdheKeyAgreement) generateClientKeyExchange(config *Config, clientHel
return ka.preMasterSecret, ka.ckx, nil
}
// generateECDHEKey returns a PrivateKey that implements Diffie-Hellman
// according to RFC 8446, Section 4.2.8.2.
func generateECDHEKey(rand io.Reader, curveID CurveID) (*ecdh.PrivateKey, error) {
curve, ok := curveForCurveID(curveID)
if !ok {
return nil, errors.New("tls: internal error: unsupported curve")
}
return curve.GenerateKey(rand)
}
func curveForCurveID(id CurveID) (ecdh.Curve, bool) {
switch id {
case X25519:
return ecdh.X25519(), true
case CurveP256:
return ecdh.P256(), true
case CurveP384:
return ecdh.P384(), true
case CurveP521:
return ecdh.P521(), true
default:
return nil, false
}
}

View file

@ -5,10 +5,11 @@
package tls
import (
"crypto"
"crypto/ecdh"
"crypto/hmac"
"crypto/internal/fips140/mlkem"
"crypto/internal/fips140/tls13"
"crypto/mlkem"
"errors"
"hash"
"io"
@ -50,35 +51,202 @@ func (c *cipherSuiteTLS13) exportKeyingMaterial(s *tls13.MasterSecret, transcrip
}
type keySharePrivateKeys struct {
curveID CurveID
ecdhe *ecdh.PrivateKey
mlkem *mlkem.DecapsulationKey768
ecdhe *ecdh.PrivateKey
mlkem crypto.Decapsulator
}
const x25519PublicKeySize = 32
// A keyExchange implements a TLS 1.3 KEM.
type keyExchange interface {
// keyShares generates one or two key shares.
//
// The first one will match the id, the second (if present) reuses the
// traditional component of the requested hybrid, as allowed by
// draft-ietf-tls-hybrid-design-09, Section 3.2.
keyShares(rand io.Reader) (*keySharePrivateKeys, []keyShare, error)
// generateECDHEKey returns a PrivateKey that implements Diffie-Hellman
// according to RFC 8446, Section 4.2.8.2.
func generateECDHEKey(rand io.Reader, curveID CurveID) (*ecdh.PrivateKey, error) {
curve, ok := curveForCurveID(curveID)
if !ok {
return nil, errors.New("tls: internal error: unsupported curve")
// serverSharedSecret computes the shared secret and the server's key share.
serverSharedSecret(rand io.Reader, clientKeyShare []byte) ([]byte, keyShare, error)
// clientSharedSecret computes the shared secret given the server's key
// share and the keys generated by keyShares.
clientSharedSecret(priv *keySharePrivateKeys, serverKeyShare []byte) ([]byte, error)
}
func keyExchangeForCurveID(id CurveID) (keyExchange, error) {
newMLKEMPrivateKey768 := func(b []byte) (crypto.Decapsulator, error) {
return mlkem.NewDecapsulationKey768(b)
}
newMLKEMPrivateKey1024 := func(b []byte) (crypto.Decapsulator, error) {
return mlkem.NewDecapsulationKey1024(b)
}
newMLKEMPublicKey768 := func(b []byte) (crypto.Encapsulator, error) {
return mlkem.NewEncapsulationKey768(b)
}
newMLKEMPublicKey1024 := func(b []byte) (crypto.Encapsulator, error) {
return mlkem.NewEncapsulationKey1024(b)
}
return curve.GenerateKey(rand)
}
func curveForCurveID(id CurveID) (ecdh.Curve, bool) {
switch id {
case X25519:
return ecdh.X25519(), true
return &ecdhKeyExchange{id, ecdh.X25519()}, nil
case CurveP256:
return ecdh.P256(), true
return &ecdhKeyExchange{id, ecdh.P256()}, nil
case CurveP384:
return ecdh.P384(), true
return &ecdhKeyExchange{id, ecdh.P384()}, nil
case CurveP521:
return ecdh.P521(), true
return &ecdhKeyExchange{id, ecdh.P521()}, nil
case X25519MLKEM768:
return &hybridKeyExchange{id, ecdhKeyExchange{X25519, ecdh.X25519()},
32, mlkem.EncapsulationKeySize768, mlkem.CiphertextSize768,
newMLKEMPrivateKey768, newMLKEMPublicKey768}, nil
case SecP256r1MLKEM768:
return &hybridKeyExchange{id, ecdhKeyExchange{CurveP256, ecdh.P256()},
65, mlkem.EncapsulationKeySize768, mlkem.CiphertextSize768,
newMLKEMPrivateKey768, newMLKEMPublicKey768}, nil
case SecP384r1MLKEM1024:
return &hybridKeyExchange{id, ecdhKeyExchange{CurveP384, ecdh.P384()},
97, mlkem.EncapsulationKeySize1024, mlkem.CiphertextSize1024,
newMLKEMPrivateKey1024, newMLKEMPublicKey1024}, nil
default:
return nil, false
return nil, errors.New("tls: unsupported key exchange")
}
}
type ecdhKeyExchange struct {
id CurveID
curve ecdh.Curve
}
func (ke *ecdhKeyExchange) keyShares(rand io.Reader) (*keySharePrivateKeys, []keyShare, error) {
priv, err := ke.curve.GenerateKey(rand)
if err != nil {
return nil, nil, err
}
return &keySharePrivateKeys{ecdhe: priv}, []keyShare{{ke.id, priv.PublicKey().Bytes()}}, nil
}
func (ke *ecdhKeyExchange) serverSharedSecret(rand io.Reader, clientKeyShare []byte) ([]byte, keyShare, error) {
key, err := ke.curve.GenerateKey(rand)
if err != nil {
return nil, keyShare{}, err
}
peerKey, err := ke.curve.NewPublicKey(clientKeyShare)
if err != nil {
return nil, keyShare{}, err
}
sharedKey, err := key.ECDH(peerKey)
if err != nil {
return nil, keyShare{}, err
}
return sharedKey, keyShare{ke.id, key.PublicKey().Bytes()}, nil
}
func (ke *ecdhKeyExchange) clientSharedSecret(priv *keySharePrivateKeys, serverKeyShare []byte) ([]byte, error) {
peerKey, err := ke.curve.NewPublicKey(serverKeyShare)
if err != nil {
return nil, err
}
sharedKey, err := priv.ecdhe.ECDH(peerKey)
if err != nil {
return nil, err
}
return sharedKey, nil
}
type hybridKeyExchange struct {
id CurveID
ecdh ecdhKeyExchange
ecdhElementSize int
mlkemPublicKeySize int
mlkemCiphertextSize int
newMLKEMPrivateKey func([]byte) (crypto.Decapsulator, error)
newMLKEMPublicKey func([]byte) (crypto.Encapsulator, error)
}
func (ke *hybridKeyExchange) keyShares(rand io.Reader) (*keySharePrivateKeys, []keyShare, error) {
priv, ecdhShares, err := ke.ecdh.keyShares(rand)
if err != nil {
return nil, nil, err
}
seed := make([]byte, mlkem.SeedSize)
if _, err := io.ReadFull(rand, seed); err != nil {
return nil, nil, err
}
priv.mlkem, err = ke.newMLKEMPrivateKey(seed)
if err != nil {
return nil, nil, err
}
var shareData []byte
// For X25519MLKEM768, the ML-KEM-768 encapsulation key comes first.
// For SecP256r1MLKEM768 and SecP384r1MLKEM1024, the ECDH share comes first.
// See draft-ietf-tls-ecdhe-mlkem-02, Section 4.1.
if ke.id == X25519MLKEM768 {
shareData = append(priv.mlkem.Encapsulator().Bytes(), ecdhShares[0].data...)
} else {
shareData = append(ecdhShares[0].data, priv.mlkem.Encapsulator().Bytes()...)
}
return priv, []keyShare{{ke.id, shareData}, ecdhShares[0]}, nil
}
func (ke *hybridKeyExchange) serverSharedSecret(rand io.Reader, clientKeyShare []byte) ([]byte, keyShare, error) {
if len(clientKeyShare) != ke.ecdhElementSize+ke.mlkemPublicKeySize {
return nil, keyShare{}, errors.New("tls: invalid client key share length for hybrid key exchange")
}
var ecdhShareData, mlkemShareData []byte
if ke.id == X25519MLKEM768 {
mlkemShareData = clientKeyShare[:ke.mlkemPublicKeySize]
ecdhShareData = clientKeyShare[ke.mlkemPublicKeySize:]
} else {
ecdhShareData = clientKeyShare[:ke.ecdhElementSize]
mlkemShareData = clientKeyShare[ke.ecdhElementSize:]
}
ecdhSharedSecret, ks, err := ke.ecdh.serverSharedSecret(rand, ecdhShareData)
if err != nil {
return nil, keyShare{}, err
}
mlkemPeerKey, err := ke.newMLKEMPublicKey(mlkemShareData)
if err != nil {
return nil, keyShare{}, err
}
mlkemSharedSecret, mlkemKeyShare := mlkemPeerKey.Encapsulate()
var sharedKey []byte
if ke.id == X25519MLKEM768 {
sharedKey = append(mlkemSharedSecret, ecdhSharedSecret...)
ks.data = append(mlkemKeyShare, ks.data...)
} else {
sharedKey = append(ecdhSharedSecret, mlkemSharedSecret...)
ks.data = append(ks.data, mlkemKeyShare...)
}
ks.group = ke.id
return sharedKey, ks, nil
}
func (ke *hybridKeyExchange) clientSharedSecret(priv *keySharePrivateKeys, serverKeyShare []byte) ([]byte, error) {
if len(serverKeyShare) != ke.ecdhElementSize+ke.mlkemCiphertextSize {
return nil, errors.New("tls: invalid server key share length for hybrid key exchange")
}
var ecdhShareData, mlkemShareData []byte
if ke.id == X25519MLKEM768 {
mlkemShareData = serverKeyShare[:ke.mlkemCiphertextSize]
ecdhShareData = serverKeyShare[ke.mlkemCiphertextSize:]
} else {
ecdhShareData = serverKeyShare[:ke.ecdhElementSize]
mlkemShareData = serverKeyShare[ke.ecdhElementSize:]
}
ecdhSharedSecret, err := ke.ecdh.clientSharedSecret(priv, ecdhShareData)
if err != nil {
return nil, err
}
mlkemSharedSecret, err := priv.mlkem.Decapsulate(mlkemShareData)
if err != nil {
return nil, err
}
var sharedKey []byte
if ke.id == X25519MLKEM768 {
sharedKey = append(mlkemSharedSecret, ecdhSharedSecret...)
} else {
sharedKey = append(ecdhSharedSecret, mlkemSharedSecret...)
}
return sharedKey, nil
}

View file

@ -11,6 +11,7 @@ import (
"crypto/ecdh"
"crypto/ecdsa"
"crypto/elliptic"
"crypto/internal/boring"
"crypto/rand"
"crypto/tls/internal/fips140tls"
"crypto/x509"
@ -1964,84 +1965,134 @@ func testVerifyCertificates(t *testing.T, version uint16) {
}
func TestHandshakeMLKEM(t *testing.T) {
skipFIPS(t) // No X25519MLKEM768 in FIPS
if boring.Enabled && fips140tls.Required() {
t.Skip("ML-KEM not supported in BoringCrypto FIPS mode")
}
defaultWithPQ := []CurveID{X25519MLKEM768, SecP256r1MLKEM768, SecP384r1MLKEM1024,
X25519, CurveP256, CurveP384, CurveP521}
defaultWithoutPQ := []CurveID{X25519, CurveP256, CurveP384, CurveP521}
var tests = []struct {
name string
clientConfig func(*Config)
serverConfig func(*Config)
preparation func(*testing.T)
expectClientSupport bool
expectMLKEM bool
expectHRR bool
name string
clientConfig func(*Config)
serverConfig func(*Config)
preparation func(*testing.T)
expectClient []CurveID
expectSelected CurveID
expectHRR bool
}{
{
name: "Default",
expectClientSupport: true,
expectMLKEM: true,
expectHRR: false,
name: "Default",
expectClient: defaultWithPQ,
expectSelected: X25519MLKEM768,
},
{
name: "ClientCurvePreferences",
clientConfig: func(config *Config) {
config.CurvePreferences = []CurveID{X25519}
},
expectClientSupport: false,
expectClient: []CurveID{X25519},
expectSelected: X25519,
},
{
name: "ServerCurvePreferencesX25519",
serverConfig: func(config *Config) {
config.CurvePreferences = []CurveID{X25519}
},
expectClientSupport: true,
expectMLKEM: false,
expectHRR: false,
expectClient: defaultWithPQ,
expectSelected: X25519,
},
{
name: "ServerCurvePreferencesHRR",
serverConfig: func(config *Config) {
config.CurvePreferences = []CurveID{CurveP256}
},
expectClientSupport: true,
expectMLKEM: false,
expectHRR: true,
expectClient: defaultWithPQ,
expectSelected: CurveP256,
expectHRR: true,
},
{
name: "SecP256r1MLKEM768-Only",
clientConfig: func(config *Config) {
config.CurvePreferences = []CurveID{SecP256r1MLKEM768}
},
expectClient: []CurveID{SecP256r1MLKEM768},
expectSelected: SecP256r1MLKEM768,
},
{
name: "SecP256r1MLKEM768-HRR",
serverConfig: func(config *Config) {
config.CurvePreferences = []CurveID{SecP256r1MLKEM768, CurveP256}
},
expectClient: defaultWithPQ,
expectSelected: SecP256r1MLKEM768,
expectHRR: true,
},
{
name: "SecP384r1MLKEM1024",
clientConfig: func(config *Config) {
config.CurvePreferences = []CurveID{SecP384r1MLKEM1024, CurveP384}
},
expectClient: []CurveID{SecP384r1MLKEM1024, CurveP384},
expectSelected: SecP384r1MLKEM1024,
},
{
name: "CurveP256NoHRR",
clientConfig: func(config *Config) {
config.CurvePreferences = []CurveID{SecP256r1MLKEM768, CurveP256}
},
serverConfig: func(config *Config) {
config.CurvePreferences = []CurveID{CurveP256}
},
expectClient: []CurveID{SecP256r1MLKEM768, CurveP256},
expectSelected: CurveP256,
},
{
name: "ClientMLKEMOnly",
clientConfig: func(config *Config) {
config.CurvePreferences = []CurveID{X25519MLKEM768}
},
expectClientSupport: true,
expectMLKEM: true,
expectClient: []CurveID{X25519MLKEM768},
expectSelected: X25519MLKEM768,
},
{
name: "ClientSortedCurvePreferences",
clientConfig: func(config *Config) {
config.CurvePreferences = []CurveID{CurveP256, X25519MLKEM768}
},
expectClientSupport: true,
expectMLKEM: true,
expectClient: []CurveID{X25519MLKEM768, CurveP256},
expectSelected: X25519MLKEM768,
},
{
name: "ClientTLSv12",
clientConfig: func(config *Config) {
config.MaxVersion = VersionTLS12
},
expectClientSupport: false,
expectClient: defaultWithoutPQ,
expectSelected: X25519,
},
{
name: "ServerTLSv12",
serverConfig: func(config *Config) {
config.MaxVersion = VersionTLS12
},
expectClientSupport: true,
expectMLKEM: false,
expectClient: defaultWithPQ,
expectSelected: X25519,
},
{
name: "GODEBUG",
name: "GODEBUG tlsmlkem=0",
preparation: func(t *testing.T) {
t.Setenv("GODEBUG", "tlsmlkem=0")
},
expectClientSupport: false,
expectClient: defaultWithoutPQ,
expectSelected: X25519,
},
{
name: "GODEBUG tlssecpmlkem=0",
preparation: func(t *testing.T) {
t.Setenv("GODEBUG", "tlssecpmlkem=0")
},
expectClient: []CurveID{X25519MLKEM768, X25519, CurveP256, CurveP384, CurveP521},
expectSelected: X25519MLKEM768,
},
}
@ -2049,6 +2100,9 @@ func TestHandshakeMLKEM(t *testing.T) {
baseConfig.CurvePreferences = nil
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
if fips140tls.Required() && test.expectSelected == X25519 {
t.Skip("X25519 not supported in FIPS mode")
}
if test.preparation != nil {
test.preparation(t)
} else {
@ -2059,10 +2113,12 @@ func TestHandshakeMLKEM(t *testing.T) {
test.serverConfig(serverConfig)
}
serverConfig.GetConfigForClient = func(hello *ClientHelloInfo) (*Config, error) {
if !test.expectClientSupport && slices.Contains(hello.SupportedCurves, X25519MLKEM768) {
return nil, errors.New("client supports X25519MLKEM768")
} else if test.expectClientSupport && !slices.Contains(hello.SupportedCurves, X25519MLKEM768) {
return nil, errors.New("client does not support X25519MLKEM768")
expectClient := slices.Clone(test.expectClient)
expectClient = slices.DeleteFunc(expectClient, func(c CurveID) bool {
return fips140tls.Required() && c == X25519
})
if !slices.Equal(hello.SupportedCurves, expectClient) {
t.Errorf("got client curves %v, expected %v", hello.SupportedCurves, expectClient)
}
return nil, nil
}
@ -2074,20 +2130,11 @@ func TestHandshakeMLKEM(t *testing.T) {
if err != nil {
t.Fatal(err)
}
if test.expectMLKEM {
if ss.CurveID != X25519MLKEM768 {
t.Errorf("got CurveID %v (server), expected %v", ss.CurveID, X25519MLKEM768)
}
if cs.CurveID != X25519MLKEM768 {
t.Errorf("got CurveID %v (client), expected %v", cs.CurveID, X25519MLKEM768)
}
} else {
if ss.CurveID == X25519MLKEM768 {
t.Errorf("got CurveID %v (server), expected not X25519MLKEM768", ss.CurveID)
}
if cs.CurveID == X25519MLKEM768 {
t.Errorf("got CurveID %v (client), expected not X25519MLKEM768", cs.CurveID)
}
if ss.CurveID != test.expectSelected {
t.Errorf("server selected curve %v, expected %v", ss.CurveID, test.expectSelected)
}
if cs.CurveID != test.expectSelected {
t.Errorf("client selected curve %v, expected %v", cs.CurveID, test.expectSelected)
}
if test.expectHRR {
if !ss.HelloRetryRequest {

View file

@ -64,6 +64,7 @@ var All = []Info{
{Name: "tlsmaxrsasize", Package: "crypto/tls"},
{Name: "tlsmlkem", Package: "crypto/tls", Changed: 24, Old: "0", Opaque: true},
{Name: "tlsrsakex", Package: "crypto/tls", Changed: 22, Old: "1"},
{Name: "tlssecpmlkem", Package: "crypto/tls", Changed: 26, Old: "0", Opaque: true},
{Name: "tlssha1", Package: "crypto/tls", Changed: 25, Old: "1"},
{Name: "tlsunsafeekm", Package: "crypto/tls", Changed: 22, Old: "1"},
{Name: "updatemaxprocs", Package: "runtime", Changed: 25, Old: "0"},