0
0
mirror of https://github.com/XTLS/REALITY.git synced 2025-08-25 16:05:32 +00:00

crypto/tls: implement X25519MLKEM768

This makes three related changes that work particularly well together
and would require significant extra work to do separately: it replaces
X25519Kyber768Draft00 with X25519MLKEM768, it makes CurvePreferences
ordering crypto/tls-selected, and applies a preference to PQ key
exchange methods over key shares (to mitigate downgrades).

TestHandshakeServerUnsupportedKeyShare was removed because we are not
rejecting unsupported key shares anymore (nor do we select them, and
rejecting them actively is a MAY). It would have been nice to keep the
test to check we still continue successfully, but testClientHelloFailure
is broken in the face of any server-side behavior which requires writing
any other messages back to the client, or reading them.

Updates #69985
Fixes #69393

Change-Id: I58de76f5b8742a9bd4543fd7907c48e038507b19
Reviewed-on: https://go-review.googlesource.com/c/go/+/630775
Reviewed-by: Roland Shoemaker <roland@golang.org>
Reviewed-by: Dmitri Shuralyov <dmitshur@google.com>
Auto-Submit: Filippo Valsorda <filippo@golang.org>
LUCI-TryBot-Result: Go LUCI <golang-scoped@luci-project-accounts.iam.gserviceaccount.com>
This commit is contained in:
yuhan6665 2025-05-10 15:06:04 -04:00
parent c3f6b7dc5f
commit cedab7cc24
8 changed files with 125 additions and 131 deletions

View File

@ -149,13 +149,17 @@ const (
CurveP384 CurveID = 24 CurveP384 CurveID = 24
CurveP521 CurveID = 25 CurveP521 CurveID = 25
X25519 CurveID = 29 X25519 CurveID = 29
X25519MLKEM768 CurveID = 4588
// Experimental codepoint for X25519Kyber768Draft00, specified in
// draft-tls-westerbaan-xyber768d00-03. Not exported, as support might be
// removed in the future.
x25519Kyber768Draft00 CurveID = 0x6399 // X25519Kyber768Draft00
) )
func isTLS13OnlyKeyExchange(curve CurveID) bool {
return curve == X25519MLKEM768
}
func isPQKeyExchange(curve CurveID) bool {
return curve == X25519MLKEM768
}
// TLS 1.3 Key Share. See RFC 8446, Section 4.2.8. // TLS 1.3 Key Share. See RFC 8446, Section 4.2.8.
type keyShare struct { type keyShare struct {
group CurveID group CurveID
@ -419,9 +423,12 @@ type ClientHelloInfo struct {
// client is using SNI (see RFC 4366, Section 3.1). // client is using SNI (see RFC 4366, Section 3.1).
ServerName string ServerName string
// SupportedCurves lists the elliptic curves supported by the client. // SupportedCurves lists the key exchange mechanisms supported by the
// SupportedCurves is set only if the Supported Elliptic Curves // client. It was renamed to "supported groups" in TLS 1.3, see RFC 8446,
// Extension is being used (see RFC 4492, Section 5.1.1). // Section 4.2.7 and [CurveID].
//
// SupportedCurves may be nil in TLS 1.2 and lower if the Supported Elliptic
// Curves Extension is not being used (see RFC 4492, Section 5.1.1).
SupportedCurves []CurveID SupportedCurves []CurveID
// SupportedPoints lists the point formats supported by the client. // SupportedPoints lists the point formats supported by the client.
@ -775,14 +782,15 @@ type Config struct {
// which is currently TLS 1.3. // which is currently TLS 1.3.
MaxVersion uint16 MaxVersion uint16
// CurvePreferences contains the elliptic curves that will be used in // CurvePreferences contains a set of supported key exchange mechanisms.
// an ECDHE handshake, in preference order. If empty, the default will // The name refers to elliptic curves for legacy reasons, see [CurveID].
// be used. The client will use the first preference as the type for // The order of the list is ignored, and key exchange mechanisms are chosen
// its key share in TLS 1.3. This may change in the future. // from this list using an internal preference order. If empty, the default
// will be used.
// //
// From Go 1.23, the default includes the X25519Kyber768Draft00 hybrid // From Go 1.24, the default includes the [X25519MLKEM768] hybrid
// post-quantum key exchange. To disable it, set CurvePreferences explicitly // post-quantum key exchange. To disable it, set CurvePreferences explicitly
// or use the GODEBUG=tlskyber=0 environment variable. // or use the GODEBUG=tlsmlkem=0 environment variable.
CurvePreferences []CurveID CurvePreferences []CurveID
// DynamicRecordSizingDisabled disables adaptive sizing of TLS records. // DynamicRecordSizingDisabled disables adaptive sizing of TLS records.
@ -1198,23 +1206,19 @@ func supportedVersionsFromMax(maxVersion uint16) []uint16 {
func (c *Config) curvePreferences(version uint16) []CurveID { func (c *Config) curvePreferences(version uint16) []CurveID {
var curvePreferences []CurveID var curvePreferences []CurveID
if c != nil && len(c.CurvePreferences) != 0 {
curvePreferences = slices.Clone(c.CurvePreferences)
if fips140tls.Required() { if fips140tls.Required() {
return slices.DeleteFunc(curvePreferences, func(c CurveID) bool {
return !slices.Contains(defaultCurvePreferencesFIPS, c)
})
}
} else if fips140tls.Required() {
curvePreferences = slices.Clone(defaultCurvePreferencesFIPS) curvePreferences = slices.Clone(defaultCurvePreferencesFIPS)
} else { } else {
curvePreferences = defaultCurvePreferences() curvePreferences = defaultCurvePreferences()
} }
if version < VersionTLS13 { if c != nil && len(c.CurvePreferences) != 0 {
return slices.DeleteFunc(curvePreferences, func(c CurveID) bool { curvePreferences = slices.DeleteFunc(curvePreferences, func(x CurveID) bool {
return c == x25519Kyber768Draft00 return !slices.Contains(c.CurvePreferences, x)
}) })
} }
if version < VersionTLS13 {
curvePreferences = slices.DeleteFunc(curvePreferences, isTLS13OnlyKeyExchange)
}
return curvePreferences return curvePreferences
} }

View File

@ -71,13 +71,13 @@ func _() {
_ = x[CurveP384-24] _ = x[CurveP384-24]
_ = x[CurveP521-25] _ = x[CurveP521-25]
_ = x[X25519-29] _ = x[X25519-29]
_ = x[x25519Kyber768Draft00-25497] _ = x[X25519MLKEM768-4588]
} }
const ( const (
_CurveID_name_0 = "CurveP256CurveP384CurveP521" _CurveID_name_0 = "CurveP256CurveP384CurveP521"
_CurveID_name_1 = "X25519" _CurveID_name_1 = "X25519"
_CurveID_name_2 = "X25519Kyber768Draft00" _CurveID_name_2 = "X25519MLKEM768"
) )
var ( var (
@ -91,7 +91,7 @@ func (i CurveID) String() string {
return _CurveID_name_0[_CurveID_index_0[i]:_CurveID_index_0[i+1]] return _CurveID_name_0[_CurveID_index_0[i]:_CurveID_index_0[i+1]]
case i == 29: case i == 29:
return _CurveID_name_1 return _CurveID_name_1
case i == 25497: case i == 4588:
return _CurveID_name_2 return _CurveID_name_2
default: default:
return "CurveID(" + strconv.FormatInt(int64(i), 10) + ")" return "CurveID(" + strconv.FormatInt(int64(i), 10) + ")"

View File

@ -12,14 +12,15 @@ import (
// Defaults are collected in this file to allow distributions to more easily patch // Defaults are collected in this file to allow distributions to more easily patch
// them to apply local policies. // them to apply local policies.
//var tlskyber = godebug.New("tlskyber") //var tlsmlkem = godebug.New("tlsmlkem")
// defaultCurvePreferences is the default set of supported key exchanges, as
// well as the preference order.
func defaultCurvePreferences() []CurveID { func defaultCurvePreferences() []CurveID {
if false { if false {
return []CurveID{X25519, CurveP256, CurveP384, CurveP521} return []CurveID{X25519, CurveP256, CurveP384, CurveP521}
} }
// For now, x25519Kyber768Draft00 must always be followed by X25519. return []CurveID{X25519MLKEM768, X25519, CurveP256, CurveP384, CurveP521}
return []CurveID{x25519Kyber768Draft00, X25519, CurveP256, CurveP384, CurveP521}
} }
// defaultSupportedSignatureAlgorithms contains the signature and hash algorithms that // defaultSupportedSignatureAlgorithms contains the signature and hash algorithms that

View File

@ -20,6 +20,7 @@ import (
"hash" "hash"
"io" "io"
"net" "net"
"slices"
"strings" "strings"
"time" "time"
@ -156,7 +157,9 @@ func (c *Conn) makeClientHello() (*clientHelloMsg, *keySharePrivateKeys, *echCli
} }
curveID := hello.supportedCurves[0] curveID := hello.supportedCurves[0]
keyShareKeys = &keySharePrivateKeys{curveID: curveID} keyShareKeys = &keySharePrivateKeys{curveID: curveID}
if curveID == x25519Kyber768Draft00 { // 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) keyShareKeys.ecdhe, err = generateECDHEKey(config.rand(), X25519)
if err != nil { if err != nil {
return nil, nil, nil, err return nil, nil, nil, err
@ -165,18 +168,20 @@ func (c *Conn) makeClientHello() (*clientHelloMsg, *keySharePrivateKeys, *echCli
if _, err := io.ReadFull(config.rand(), seed); err != nil { if _, err := io.ReadFull(config.rand(), seed); err != nil {
return nil, nil, nil, err return nil, nil, nil, err
} }
keyShareKeys.kyber, err = mlkem.NewDecapsulationKey768(seed) keyShareKeys.mlkem, err = mlkem.NewDecapsulationKey768(seed)
if err != nil { if err != nil {
return nil, nil, nil, err return nil, nil, nil, err
} }
// For draft-tls-westerbaan-xyber768d00-03, we send both a hybrid mlkemEncapsulationKey := keyShareKeys.mlkem.EncapsulationKey().Bytes()
// and a standard X25519 key share, since most servers will only x25519EphemeralKey := keyShareKeys.ecdhe.PublicKey().Bytes()
// support the latter. We reuse the same X25519 ephemeral key for
// both, as allowed by draft-ietf-tls-hybrid-design-09, Section 3.2.
hello.keyShares = []keyShare{ hello.keyShares = []keyShare{
{group: x25519Kyber768Draft00, data: append(keyShareKeys.ecdhe.PublicKey().Bytes(), {group: X25519MLKEM768, data: append(mlkemEncapsulationKey, x25519EphemeralKey...)},
keyShareKeys.kyber.EncapsulationKey().Bytes()...)}, }
{group: X25519, data: keyShareKeys.ecdhe.PublicKey().Bytes()}, // 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 { } else {
if _, ok := curveForCurveID(curveID); !ok { if _, ok := curveForCurveID(curveID); !ok {
@ -704,7 +709,7 @@ func (hs *clientHandshakeState) doFullHandshake() error {
if ok { if ok {
err = keyAgreement.processServerKeyExchange(c.config, hs.hello, hs.serverHello, c.peerCertificates[0], skx) err = keyAgreement.processServerKeyExchange(c.config, hs.hello, hs.serverHello, c.peerCertificates[0], skx)
if err != nil { if err != nil {
c.sendAlert(alertUnexpectedMessage) c.sendAlert(alertIllegalParameter)
return err return err
} }
if len(skx.key) >= 3 && skx.key[0] == 3 /* named curve */ { if len(skx.key) >= 3 && skx.key[0] == 3 /* named curve */ {

View File

@ -322,12 +322,11 @@ func (hs *clientHandshakeStateTLS13) processHelloRetryRequest() error {
c.sendAlert(alertIllegalParameter) c.sendAlert(alertIllegalParameter)
return errors.New("tls: server sent an unnecessary HelloRetryRequest key_share") return errors.New("tls: server sent an unnecessary HelloRetryRequest key_share")
} }
// Note: we don't support selecting X25519Kyber768Draft00 in a HRR, // Note: we don't support selecting X25519MLKEM768 in a HRR, because it
// because we currently only support it at all when CurvePreferences is // is currently first in preference order, so if it's enabled we'll
// empty, which will cause us to also send a key share for it. // always send a key share for it.
// //
// This will have to change once we support selecting hybrid KEMs // This will have to change once we support multiple hybrid KEMs.
// without sending key shares for them.
if _, ok := curveForCurveID(curveID); !ok { if _, ok := curveForCurveID(curveID); !ok {
c.sendAlert(alertInternalError) c.sendAlert(alertInternalError)
return errors.New("tls: CurvePreferences includes unsupported curve") return errors.New("tls: CurvePreferences includes unsupported curve")
@ -480,12 +479,12 @@ func (hs *clientHandshakeStateTLS13) establishHandshakeKeys() error {
c := hs.c c := hs.c
ecdhePeerData := hs.serverHello.serverShare.data ecdhePeerData := hs.serverHello.serverShare.data
if hs.serverHello.serverShare.group == x25519Kyber768Draft00 { if hs.serverHello.serverShare.group == X25519MLKEM768 {
if len(ecdhePeerData) != x25519PublicKeySize+mlkem.CiphertextSize768 { if len(ecdhePeerData) != mlkem.CiphertextSize768+x25519PublicKeySize {
c.sendAlert(alertIllegalParameter) c.sendAlert(alertIllegalParameter)
return errors.New("tls: invalid server key share") return errors.New("tls: invalid server X25519MLKEM768 key share")
} }
ecdhePeerData = hs.serverHello.serverShare.data[:x25519PublicKeySize] ecdhePeerData = hs.serverHello.serverShare.data[mlkem.CiphertextSize768:]
} }
peerKey, err := hs.keyShareKeys.ecdhe.Curve().NewPublicKey(ecdhePeerData) peerKey, err := hs.keyShareKeys.ecdhe.Curve().NewPublicKey(ecdhePeerData)
if err != nil { if err != nil {
@ -497,17 +496,17 @@ func (hs *clientHandshakeStateTLS13) establishHandshakeKeys() error {
c.sendAlert(alertIllegalParameter) c.sendAlert(alertIllegalParameter)
return errors.New("tls: invalid server key share") return errors.New("tls: invalid server key share")
} }
if hs.serverHello.serverShare.group == x25519Kyber768Draft00 { if hs.serverHello.serverShare.group == X25519MLKEM768 {
if hs.keyShareKeys.kyber == nil { if hs.keyShareKeys.mlkem == nil {
return c.sendAlert(alertInternalError) return c.sendAlert(alertInternalError)
} }
ciphertext := hs.serverHello.serverShare.data[x25519PublicKeySize:] ciphertext := hs.serverHello.serverShare.data[:mlkem.CiphertextSize768]
kyberShared, err := kyberDecapsulate(hs.keyShareKeys.kyber, ciphertext) mlkemShared, err := hs.keyShareKeys.mlkem.Decapsulate(ciphertext)
if err != nil { if err != nil {
c.sendAlert(alertIllegalParameter) c.sendAlert(alertIllegalParameter)
return errors.New("tls: invalid Kyber server key share") return errors.New("tls: invalid X25519MLKEM768 server key share")
} }
sharedKey = append(sharedKey, kyberShared...) sharedKey = append(mlkemShared, sharedKey...)
} }
c.curveID = hs.serverHello.serverShare.group c.curveID = hs.serverHello.serverShare.group

View File

@ -690,7 +690,7 @@ func (hs *serverHandshakeState) doFullHandshake() error {
preMasterSecret, err := keyAgreement.processClientKeyExchange(c.config, hs.cert, ckx, c.vers) preMasterSecret, err := keyAgreement.processClientKeyExchange(c.config, hs.cert, ckx, c.vers)
if err != nil { if err != nil {
c.sendAlert(alertHandshakeFailure) c.sendAlert(alertIllegalParameter)
return err return err
} }
if hs.hello.extendedMasterSecret { if hs.hello.extendedMasterSecret {

View File

@ -22,6 +22,7 @@ import (
"io" "io"
"math/big" "math/big"
"slices" "slices"
"sort"
"time" "time"
"github.com/xtls/reality/fips140tls" "github.com/xtls/reality/fips140tls"
@ -248,36 +249,44 @@ func (hs *serverHandshakeStateTLS13) processClientHello() error {
hs.hello.cipherSuite = hs.suite.id hs.hello.cipherSuite = hs.suite.id
hs.transcript = hs.suite.hash.New() hs.transcript = hs.suite.hash.New()
// Pick the key exchange method in server preference order, but give // First, if a post-quantum key exchange is available, use one. See
// priority to key shares, to avoid a HelloRetryRequest round-trip. // draft-ietf-tls-key-share-prediction-01, Section 4 for why this must be
var selectedGroup CurveID // first.
var clientKeyShare *keyShare //
// Second, if the client sent a key share for a group we support, use that,
// to avoid a HelloRetryRequest round-trip.
//
// Finally, pick in our fixed preference order.
preferredGroups := c.config.curvePreferences(c.vers) preferredGroups := c.config.curvePreferences(c.vers)
for _, preferredGroup := range preferredGroups { preferredGroups = slices.DeleteFunc(preferredGroups, func(group CurveID) bool {
ki := slices.IndexFunc(hs.clientHello.keyShares, func(ks keyShare) bool { return !slices.Contains(hs.clientHello.supportedCurves, group)
return ks.group == preferredGroup
}) })
if ki != -1 { if len(preferredGroups) == 0 {
clientKeyShare = &hs.clientHello.keyShares[ki]
selectedGroup = clientKeyShare.group
if !slices.Contains(hs.clientHello.supportedCurves, selectedGroup) {
c.sendAlert(alertIllegalParameter)
return errors.New("tls: client sent key share for group it does not support")
}
break
}
}
if selectedGroup == 0 {
for _, preferredGroup := range preferredGroups {
if slices.Contains(hs.clientHello.supportedCurves, preferredGroup) {
selectedGroup = preferredGroup
break
}
}
}
if selectedGroup == 0 {
c.sendAlert(alertHandshakeFailure) c.sendAlert(alertHandshakeFailure)
return errors.New("tls: no ECDHE curve supported by both client and server") return errors.New("tls: no key exchanges supported by both client and server")
}
hasKeyShare := func(group CurveID) bool {
for _, ks := range hs.clientHello.keyShares {
if ks.group == group {
return true
}
}
return false
}
sort.SliceStable(preferredGroups, func(i, j int) bool {
return hasKeyShare(preferredGroups[i]) && !hasKeyShare(preferredGroups[j])
})
sort.SliceStable(preferredGroups, func(i, j int) bool {
return isPQKeyExchange(preferredGroups[i]) && !isPQKeyExchange(preferredGroups[j])
})
selectedGroup := preferredGroups[0]
var clientKeyShare *keyShare
for _, ks := range hs.clientHello.keyShares {
if ks.group == selectedGroup {
clientKeyShare = &ks
break
}
} }
if clientKeyShare == nil { if clientKeyShare == nil {
ks, err := hs.doHelloRetryRequest(selectedGroup) ks, err := hs.doHelloRetryRequest(selectedGroup)
@ -290,13 +299,13 @@ func (hs *serverHandshakeStateTLS13) processClientHello() error {
ecdhGroup := selectedGroup ecdhGroup := selectedGroup
ecdhData := clientKeyShare.data ecdhData := clientKeyShare.data
if selectedGroup == x25519Kyber768Draft00 { if selectedGroup == X25519MLKEM768 {
ecdhGroup = X25519 ecdhGroup = X25519
if len(ecdhData) != x25519PublicKeySize+mlkem.EncapsulationKeySize768 { if len(ecdhData) != mlkem.EncapsulationKeySize768+x25519PublicKeySize {
c.sendAlert(alertIllegalParameter) c.sendAlert(alertIllegalParameter)
return errors.New("tls: invalid Kyber client key share") return errors.New("tls: invalid X25519MLKEM768 client key share")
} }
ecdhData = ecdhData[:x25519PublicKeySize] ecdhData = ecdhData[mlkem.EncapsulationKeySize768:]
} }
if _, ok := curveForCurveID(ecdhGroup); !ok { if _, ok := curveForCurveID(ecdhGroup); !ok {
c.sendAlert(alertInternalError) c.sendAlert(alertInternalError)
@ -318,14 +327,24 @@ func (hs *serverHandshakeStateTLS13) processClientHello() error {
c.sendAlert(alertIllegalParameter) c.sendAlert(alertIllegalParameter)
return errors.New("tls: invalid client key share") return errors.New("tls: invalid client key share")
} }
if selectedGroup == x25519Kyber768Draft00 { if selectedGroup == X25519MLKEM768 {
ciphertext, kyberShared, err := kyberEncapsulate(clientKeyShare.data[x25519PublicKeySize:]) k, err := mlkem.NewEncapsulationKey768(clientKeyShare.data[:mlkem.EncapsulationKeySize768])
if err != nil { if err != nil {
c.sendAlert(alertIllegalParameter) c.sendAlert(alertIllegalParameter)
return errors.New("tls: invalid Kyber client key share") return errors.New("tls: invalid X25519MLKEM768 client key share")
} }
hs.sharedKey = append(hs.sharedKey, kyberShared...) ciphertext, mlkemSharedSecret := k.Encapsulate()
hs.hello.serverShare.data = append(hs.hello.serverShare.data, ciphertext...) // 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) selectedProto, err := negotiateALPN(c.config.NextProtos, hs.clientHello.alpnProtocols, c.quic != nil)

View File

@ -8,7 +8,6 @@ import (
"crypto/ecdh" "crypto/ecdh"
"crypto/hmac" "crypto/hmac"
"crypto/mlkem" "crypto/mlkem"
"crypto/sha3"
"errors" "errors"
"hash" "hash"
"io" "io"
@ -54,40 +53,7 @@ func (c *cipherSuiteTLS13) exportKeyingMaterial(s *tls13.MasterSecret, transcrip
type keySharePrivateKeys struct { type keySharePrivateKeys struct {
curveID CurveID curveID CurveID
ecdhe *ecdh.PrivateKey ecdhe *ecdh.PrivateKey
kyber *mlkem.DecapsulationKey768 mlkem *mlkem.DecapsulationKey768
}
// kyberDecapsulate implements decapsulation according to Kyber Round 3.
func kyberDecapsulate(dk *mlkem.DecapsulationKey768, c []byte) ([]byte, error) {
K, err := dk.Decapsulate(c)
if err != nil {
return nil, err
}
return kyberSharedSecret(c, K), nil
}
// kyberEncapsulate implements encapsulation according to Kyber Round 3.
func kyberEncapsulate(ek []byte) (c, ss []byte, err error) {
k, err := mlkem.NewEncapsulationKey768(ek)
if err != nil {
return nil, nil, err
}
c, ss = k.Encapsulate()
return c, kyberSharedSecret(c, ss), nil
}
func kyberSharedSecret(c, K []byte) []byte {
// Package mlkem implements ML-KEM, which compared to Kyber removed a
// final hashing step. Compute SHAKE-256(K || SHA3-256(c), 32) to match Kyber.
// See https://words.filippo.io/mlkem768/#bonus-track-using-a-ml-kem-implementation-as-kyber-v3.
h := sha3.NewSHAKE256()
h.Write(K)
ch := sha3.New256()
ch.Write(c)
h.Write(ch.Sum(nil))
out := make([]byte, 32)
h.Read(out)
return out
} }
const x25519PublicKeySize = 32 const x25519PublicKeySize = 32