diff --git a/auth.go b/auth.go index 7c5675c6..77266306 100644 --- a/auth.go +++ b/auth.go @@ -15,6 +15,9 @@ import ( "fmt" "hash" "io" + + circlPki "github.com/cloudflare/circl/pki" + circlSign "github.com/cloudflare/circl/sign" ) // verifyHandshakeSignature verifies a signature against pre-hashed @@ -55,7 +58,20 @@ func verifyHandshakeSignature(sigType uint8, pubkey crypto.PublicKey, hashFunc c return err } default: - return errors.New("internal error: unknown signature type") + // [UTLS SECTION BEGINS] + // Ported from cloudflare/go + scheme := circlSchemeBySigType(sigType) + if scheme == nil { + return errors.New("internal error: unknown signature type") + } + pubKey, ok := pubkey.(circlSign.PublicKey) + if !ok { + return fmt.Errorf("expected a %s public key, got %T", scheme.Name(), pubkey) + } + if !scheme.Verify(pubKey, signed, sig, nil) { + return fmt.Errorf("%s verification failure", scheme.Name()) + } + // [UTLS SECTION ENDS] } return nil } @@ -106,7 +122,18 @@ func typeAndHashFromSignatureScheme(signatureAlgorithm SignatureScheme) (sigType case Ed25519: sigType = signatureEd25519 default: - return 0, 0, fmt.Errorf("unsupported signature algorithm: %v", signatureAlgorithm) + // [UTLS SECTION BEGINS] + // Ported from cloudflare/go + scheme := circlPki.SchemeByTLSID(uint(signatureAlgorithm)) + if scheme == nil { + return 0, 0, fmt.Errorf("unsupported signature algorithm: %v", signatureAlgorithm) + } + sigType = sigTypeByCirclScheme(scheme) + if sigType == 0 { + return 0, 0, fmt.Errorf("circl scheme %s not supported", + scheme.Name()) + } + // [UTLS SECTION ENDS] } switch signatureAlgorithm { case PKCS1WithSHA1, ECDSAWithSHA1: @@ -120,7 +147,14 @@ func typeAndHashFromSignatureScheme(signatureAlgorithm SignatureScheme) (sigType case Ed25519: hash = directSigning default: - return 0, 0, fmt.Errorf("unsupported signature algorithm: %v", signatureAlgorithm) + // [UTLS SECTION BEGINS] + // Ported from cloudflare/go + scheme := circlPki.SchemeByTLSID(uint(signatureAlgorithm)) + if scheme == nil { + return 0, 0, fmt.Errorf("unsupported signature algorithm: %v", signatureAlgorithm) + } + hash = directSigning + // [UTLS SECTION ENDS] } return sigType, hash, nil } @@ -140,6 +174,11 @@ func legacyTypeAndHashFromPublicKey(pub crypto.PublicKey) (sigType uint8, hash c // full signature, and not even OpenSSL bothers with the // complexity, so we can't even test it properly. return 0, 0, fmt.Errorf("tls: Ed25519 public keys are not supported before TLS 1.2") + // [UTLS SECTION BEGINS] + // Ported from cloudflare/go + case circlSign.PublicKey: + return 0, 0, fmt.Errorf("tls: circl public keys are not supported before TLS 1.2") + // [UTLS SECTION ENDS] default: return 0, 0, fmt.Errorf("tls: unsupported public key: %T", pub) } @@ -210,6 +249,16 @@ func signatureSchemesForCertificate(version uint16, cert *Certificate) []Signatu } case ed25519.PublicKey: sigAlgs = []SignatureScheme{Ed25519} + // [UTLS SECTION BEGINS] + // Ported from cloudflare/go + case circlSign.PublicKey: + scheme := pub.Scheme() + tlsScheme, ok := scheme.(circlPki.TLSScheme) + if !ok { + return nil + } + sigAlgs = []SignatureScheme{SignatureScheme(tlsScheme.TLSIdentifier())} + // [UTLS SECTION ENDS] default: return nil } diff --git a/auth_test.go b/auth_test.go index c23d93f3..54cf15de 100644 --- a/auth_test.go +++ b/auth_test.go @@ -7,6 +7,8 @@ package tls import ( "crypto" "testing" + + circlPki "github.com/cloudflare/circl/pki" ) func TestSignatureSelection(t *testing.T) { @@ -161,7 +163,7 @@ func TestSupportedSignatureAlgorithms(t *testing.T) { if sigType == 0 { t.Errorf("%v: missing signature type", sigAlg) } - if hash == 0 && sigAlg != Ed25519 { + if hash == 0 && sigAlg != Ed25519 && circlPki.SchemeByTLSID(uint(sigAlg)) == nil { // [UTLS] ported from cloudflare/go t.Errorf("%v: missing hash", sigAlg) } } diff --git a/cfkem.go b/cfkem.go new file mode 100644 index 00000000..8d440e4c --- /dev/null +++ b/cfkem.go @@ -0,0 +1,101 @@ +// Copyright 2022 Cloudflare, Inc. All rights reserved. Use of this source code +// is governed by a BSD-style license that can be found in the LICENSE file. +// +// Glue to add Circl's (post-quantum) hybrid KEMs. +// +// To enable set CurvePreferences with the desired scheme as the first element: +// +// import ( +// "crypto/tls" +// +// [...] +// +// config.CurvePreferences = []tls.CurveID{ +// tls.X25519Kyber768Draft00, +// tls.X25519, +// tls.P256, +// } + +package tls + +import ( + "fmt" + "io" + + "crypto/ecdh" + + "github.com/cloudflare/circl/kem" + "github.com/cloudflare/circl/kem/hybrid" +) + +// Either *ecdh.PrivateKey or *kemPrivateKey +type clientKeySharePrivate interface{} + +type kemPrivateKey struct { + secretKey kem.PrivateKey + curveID CurveID +} + +var ( + X25519Kyber512Draft00 = CurveID(0xfe30) + X25519Kyber768Draft00 = CurveID(0x6399) + X25519Kyber768Draft00Old = CurveID(0xfe31) + P256Kyber768Draft00 = CurveID(0xfe32) + invalidCurveID = CurveID(0) +) + +// Extract CurveID from clientKeySharePrivate +func clientKeySharePrivateCurveID(ks clientKeySharePrivate) CurveID { + switch v := ks.(type) { + case *kemPrivateKey: + return v.curveID + case *ecdh.PrivateKey: + ret, ok := curveIDForCurve(v.Curve()) + if !ok { + panic("cfkem: internal error: unknown curve") + } + return ret + default: + panic("cfkem: internal error: unknown clientKeySharePrivate") + } +} + +// Returns scheme by CurveID if supported by Circl +func curveIdToCirclScheme(id CurveID) kem.Scheme { + switch id { + case X25519Kyber512Draft00: + return hybrid.Kyber512X25519() + case X25519Kyber768Draft00, X25519Kyber768Draft00Old: + return hybrid.Kyber768X25519() + case P256Kyber768Draft00: + return hybrid.P256Kyber768Draft00() + } + return nil +} + +// Generate a new shared secret and encapsulates it for the packed +// public key in ppk using randomness from rnd. +func encapsulateForKem(scheme kem.Scheme, rnd io.Reader, ppk []byte) ( + ct, ss []byte, alert alert, err error) { + pk, err := scheme.UnmarshalBinaryPublicKey(ppk) + if err != nil { + return nil, nil, alertIllegalParameter, fmt.Errorf("unpack pk: %w", err) + } + seed := make([]byte, scheme.EncapsulationSeedSize()) + if _, err := io.ReadFull(rnd, seed); err != nil { + return nil, nil, alertInternalError, fmt.Errorf("random: %w", err) + } + ct, ss, err = scheme.EncapsulateDeterministically(pk, seed) + return ct, ss, alertIllegalParameter, err +} + +// Generate a new keypair using randomness from rnd. +func generateKemKeyPair(scheme kem.Scheme, curveID CurveID, rnd io.Reader) ( + kem.PublicKey, *kemPrivateKey, error) { + seed := make([]byte, scheme.SeedSize()) + if _, err := io.ReadFull(rnd, seed); err != nil { + return nil, nil, err + } + pk, sk := scheme.DeriveKeyPair(seed) + return pk, &kemPrivateKey{sk, curveID}, nil +} diff --git a/cfkem_test.go b/cfkem_test.go new file mode 100644 index 00000000..7043d5f0 --- /dev/null +++ b/cfkem_test.go @@ -0,0 +1,107 @@ +// Copyright 2022 Cloudflare, Inc. All rights reserved. Use of this source code +// is governed by a BSD-style license that can be found in the LICENSE file. + +package tls + +import ( + "context" + "fmt" + "testing" +) + +func testHybridKEX(t *testing.T, curveID CurveID, clientPQ, serverPQ, + clientTLS12, serverTLS12 bool) { + // var clientSelectedKEX *CurveID + // var retry bool + + clientConfig := testConfig.Clone() + if clientPQ { + clientConfig.CurvePreferences = []CurveID{curveID, X25519} + } + // clientCFEventHandler := func(ev CFEvent) { + // switch e := ev.(type) { + // case CFEventTLSNegotiatedNamedKEX: + // clientSelectedKEX = &e.KEX + // case CFEventTLS13HRR: + // retry = true + // } + // } + if clientTLS12 { + clientConfig.MaxVersion = VersionTLS12 + } + + serverConfig := testConfig.Clone() + if serverPQ { + serverConfig.CurvePreferences = []CurveID{curveID, X25519} + } else { + serverConfig.CurvePreferences = []CurveID{X25519} + } + if serverTLS12 { + serverConfig.MaxVersion = VersionTLS12 + } + + c, s := localPipe(t) + done := make(chan error) + defer c.Close() + + go func() { + defer s.Close() + done <- Server(s, serverConfig).Handshake() + }() + + cli := Client(c, clientConfig) + // cCtx := context.WithValue(context.Background(), CFEventHandlerContextKey{}, clientCFEventHandler) + clientErr := cli.HandshakeContext(context.Background()) + serverErr := <-done + if clientErr != nil { + t.Errorf("client error: %s", clientErr) + } + if serverErr != nil { + t.Errorf("server error: %s", serverErr) + } + + // var expectedKEX CurveID + // var expectedRetry bool + + // if clientPQ && serverPQ && !clientTLS12 && !serverTLS12 { + // expectedKEX = curveID + // } else { + // expectedKEX = X25519 + // } + // if !clientTLS12 && clientPQ && !serverPQ { + // expectedRetry = true + // } + + // if expectedRetry != retry { + // t.Errorf("Expected retry=%v, got retry=%v", expectedRetry, retry) + // } + + // if clientSelectedKEX == nil { + // t.Error("No KEX happened?") + // } else if *clientSelectedKEX != expectedKEX { + // t.Errorf("failed to negotiate: expected %d, got %d", + // expectedKEX, *clientSelectedKEX) + // } +} + +func TestHybridKEX(t *testing.T) { + run := func(curveID CurveID, clientPQ, serverPQ, clientTLS12, serverTLS12 bool) { + t.Run(fmt.Sprintf("%#04x serverPQ:%v clientPQ:%v serverTLS12:%v clientTLS12:%v", uint16(curveID), + serverPQ, clientPQ, serverTLS12, clientTLS12), func(t *testing.T) { + testHybridKEX(t, curveID, clientPQ, serverPQ, clientTLS12, serverTLS12) + }) + } + for _, curveID := range []CurveID{ + X25519Kyber512Draft00, + X25519Kyber768Draft00, + X25519Kyber768Draft00Old, + P256Kyber768Draft00, + } { + run(curveID, true, true, false, false) + run(curveID, true, false, false, false) + run(curveID, false, true, false, false) + run(curveID, true, true, true, false) + run(curveID, true, true, false, true) + run(curveID, true, true, true, true) + } +} diff --git a/common.go b/common.go index 7c6eaf05..a636dde1 100644 --- a/common.go +++ b/common.go @@ -189,6 +189,7 @@ const ( signatureRSAPSS signatureECDSA signatureEd25519 + signatureEdDilithium3 ) // directSigning is a standard Hash value that signals that no pre-hashing @@ -780,6 +781,11 @@ type Config struct { // its key share in TLS 1.3. This may change in the future. CurvePreferences []CurveID + // PQSignatureSchemesEnabled controls whether additional post-quantum + // signature schemes are supported for peer certificates. For available + // signature schemes, see tls_cf.go. + PQSignatureSchemesEnabled bool // [UTLS] ported from cloudflare/go + // DynamicRecordSizingDisabled disables adaptive sizing of TLS records. // When true, the largest possible TLS record size is always used. When // false, the size of TLS records may be adjusted in an attempt to @@ -885,6 +891,7 @@ func (c *Config) Clone() *Config { MinVersion: c.MinVersion, MaxVersion: c.MaxVersion, CurvePreferences: c.CurvePreferences, + PQSignatureSchemesEnabled: c.PQSignatureSchemesEnabled, // [UTLS] DynamicRecordSizingDisabled: c.DynamicRecordSizingDisabled, Renegotiation: c.Renegotiation, KeyLogWriter: c.KeyLogWriter, diff --git a/generate_cert.go b/generate_cert.go index cd4bfc51..dce68f7c 100644 --- a/generate_cert.go +++ b/generate_cert.go @@ -25,6 +25,9 @@ import ( "os" "strings" "time" + + circlSign "github.com/cloudflare/circl/sign" + circlSchemes "github.com/cloudflare/circl/sign/schemes" ) var ( @@ -35,6 +38,7 @@ var ( rsaBits = flag.Int("rsa-bits", 2048, "Size of RSA key to generate. Ignored if --ecdsa-curve is set") ecdsaCurve = flag.String("ecdsa-curve", "", "ECDSA curve to use to generate a key. Valid values are P224, P256 (recommended), P384, P521") ed25519Key = flag.Bool("ed25519", false, "Generate an Ed25519 key") + circlKey = flag.String("circl", "", "Generate a key supported by Circl") // [UTLS] ported from cloudflare/go ) func publicKey(priv any) any { @@ -45,6 +49,11 @@ func publicKey(priv any) any { return &k.PublicKey case ed25519.PrivateKey: return k.Public().(ed25519.PublicKey) + // [UTLS SECTION BEGINS] + // Ported from cloudflare/go + case circlSign.PrivateKey: + return k.Public() + // [UTLS SECTION ENDS] default: return nil } @@ -63,6 +72,15 @@ func main() { case "": if *ed25519Key { _, priv, err = ed25519.GenerateKey(rand.Reader) + // [UTLS SECTION BEGINS] + // Ported from cloudflare/go + } else if *circlKey != "" { + scheme := circlSchemes.ByName(*circlKey) + if scheme == nil { + log.Fatalf("No such Circl scheme: %s", *circlKey) + } + _, priv, err = scheme.GenerateKey() + // [UTLS SECTION ENDS] } else { priv, err = rsa.GenerateKey(rand.Reader, *rsaBits) } diff --git a/go.mod b/go.mod index 7be48a12..cef850a9 100644 --- a/go.mod +++ b/go.mod @@ -9,6 +9,7 @@ retract ( require ( github.com/andybalholm/brotli v1.0.5 + github.com/cloudflare/circl v1.3.3 github.com/gaukas/godicttls v0.0.4 github.com/klauspost/compress v1.16.7 github.com/quic-go/quic-go v0.37.4 diff --git a/go.sum b/go.sum index b807fd5a..00b89e11 100644 --- a/go.sum +++ b/go.sum @@ -1,5 +1,7 @@ github.com/andybalholm/brotli v1.0.5 h1:8uQZIdzKmjc/iuPu7O2ioW48L81FgatrcpfFmiq/cCs= github.com/andybalholm/brotli v1.0.5/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig= +github.com/cloudflare/circl v1.3.3 h1:fE/Qz0QdIGqeWfnwq0RE0R7MI51s0M2E4Ga9kq5AEMs= +github.com/cloudflare/circl v1.3.3/go.mod h1:5XYMA4rFBvNIrhs50XuiBJ15vF2pZn4nnUKZrLbUZFA= github.com/gaukas/godicttls v0.0.4 h1:NlRaXb3J6hAnTmWdsEKb9bcSBD6BvcIjdGdeb0zfXbk= github.com/gaukas/godicttls v0.0.4/go.mod h1:l6EenT4TLWgTdwslVb4sEMOCf7Bv0JAK67deKr9/NCI= github.com/go-logr/logr v1.2.4 h1:g01GSCwiDw2xSZfjJ2/T9M+S6pFdcNtFYsp+Y43HYDQ= diff --git a/handshake_client.go b/handshake_client.go index 088baa50..a0671ed9 100644 --- a/handshake_client.go +++ b/handshake_client.go @@ -21,6 +21,8 @@ import ( "net" "strings" "time" + + circlSign "github.com/cloudflare/circl/sign" ) type clientHandshakeState struct { @@ -39,7 +41,7 @@ type clientHandshakeState struct { var testingOnlyForceClientHelloSignatureAlgorithms []SignatureScheme -func (c *Conn) makeClientHello() (*clientHelloMsg, *ecdh.PrivateKey, error) { +func (c *Conn) makeClientHello() (*clientHelloMsg, clientKeySharePrivate, error) { config := c.config // [UTLS SECTION START] @@ -130,13 +132,13 @@ func (c *Conn) makeClientHello() (*clientHelloMsg, *ecdh.PrivateKey, error) { } if hello.vers >= VersionTLS12 { - hello.supportedSignatureAlgorithms = supportedSignatureAlgorithms() + hello.supportedSignatureAlgorithms = config.supportedSignatureAlgorithms() // [UTLS] ported from cloudflare/go } if testingOnlyForceClientHelloSignatureAlgorithms != nil { hello.supportedSignatureAlgorithms = testingOnlyForceClientHelloSignatureAlgorithms } - var key *ecdh.PrivateKey + var secret clientKeySharePrivate // [UTLS] if hello.supportedVersions[0] == VersionTLS13 { // Reset the list of ciphers when the client only supports TLS 1.3. if len(hello.supportedVersions) == 1 { @@ -149,14 +151,31 @@ func (c *Conn) makeClientHello() (*clientHelloMsg, *ecdh.PrivateKey, error) { } curveID := config.curvePreferences()[0] - if _, ok := curveForCurveID(curveID); !ok { - return nil, nil, errors.New("tls: CurvePreferences includes unsupported curve") - } - key, err = generateECDHEKey(config.rand(), curveID) - if err != nil { - return nil, nil, err + // [UTLS SECTION BEGINS] + // Ported from cloudflare/go with modifications to preserve crypto/tls compatibility + if scheme := curveIdToCirclScheme(curveID); scheme != nil { + pk, sk, err := generateKemKeyPair(scheme, curveID, config.rand()) + if err != nil { + return nil, nil, fmt.Errorf("generateKemKeyPair %s: %w", scheme.Name(), err) + } + packedPk, err := pk.MarshalBinary() + if err != nil { + return nil, nil, fmt.Errorf("pack circl public key %s: %w", scheme.Name(), err) + } + hello.keyShares = []keyShare{{group: curveID, data: packedPk}} + secret = sk + } else { + if _, ok := curveForCurveID(curveID); !ok { + return nil, nil, errors.New("tls: CurvePreferences includes unsupported curve") + } + key, err := generateECDHEKey(config.rand(), curveID) + if err != nil { + return nil, nil, err + } + hello.keyShares = []keyShare{{group: curveID, data: key.PublicKey().Bytes()}} + secret = key } - hello.keyShares = []keyShare{{group: curveID, data: key.PublicKey().Bytes()}} + // [UTLS SECTION ENDS] } if c.quic != nil { @@ -170,7 +189,7 @@ func (c *Conn) makeClientHello() (*clientHelloMsg, *ecdh.PrivateKey, error) { hello.quicTransportParameters = p } - return hello, key, nil + return hello, secret, nil } func (c *Conn) clientHandshake(ctx context.Context) (err error) { @@ -182,7 +201,7 @@ func (c *Conn) clientHandshake(ctx context.Context) (err error) { // need to be reset. c.didResume = false - hello, ecdheKey, err := c.makeClientHello() + hello, keySharePrivate, err := c.makeClientHello() if err != nil { return err } @@ -256,12 +275,20 @@ func (c *Conn) clientHandshake(ctx context.Context) (err error) { ctx: ctx, serverHello: serverHello, hello: hello, - ecdheKey: ecdheKey, + // ecdheKey: ecdheKey, session: session, earlySecret: earlySecret, binderKey: binderKey, - keySharesEcdheParams: make(KeySharesEcdheParameters, 2), // [uTLS] + keySharesParams: NewKeySharesParameters(), // [uTLS] + } + + if ecdheKey, ok := keySharePrivate.(*ecdh.PrivateKey); ok { + hs.ecdheKey = ecdheKey + } else if kemKey, ok := keySharePrivate.(*kemPrivateKey); ok { + hs.kemKey = kemKey + } else { + return fmt.Errorf("tls: unknown key share type %T", keySharePrivate) } // In TLS 1.3, session tickets are delivered after the handshake. @@ -1019,7 +1046,7 @@ func (c *Conn) verifyServerCertificate(certificates [][]byte) error { } switch certs[0].PublicKey.(type) { - case *rsa.PublicKey, *ecdsa.PublicKey, ed25519.PublicKey: + case *rsa.PublicKey, *ecdsa.PublicKey, ed25519.PublicKey, circlSign.PublicKey: // [UTLS] ported from cloudflare/go break default: c.sendAlert(alertUnsupportedCertificate) diff --git a/handshake_client_tls13.go b/handshake_client_tls13.go index 7e307c15..52c71390 100644 --- a/handshake_client_tls13.go +++ b/handshake_client_tls13.go @@ -15,37 +15,79 @@ import ( "fmt" "hash" "time" + + "github.com/cloudflare/circl/kem" ) // [uTLS SECTION START] -type KeySharesEcdheParameters map[CurveID]*ecdh.PrivateKey +// KeySharesParameters serves as a in-memory storage for generated keypairs by UTLS when generating +// ClientHello. It is used to store both ecdhe and kem keypairs. +type KeySharesParameters struct { + ecdhePrivKeymap map[CurveID]*ecdh.PrivateKey + ecdhePubKeymap map[CurveID]*ecdh.PublicKey + + // based on cloudflare/go + kemPrivKeymap map[CurveID]kem.PrivateKey + kemPubKeymap map[CurveID]kem.PublicKey +} + +func NewKeySharesParameters() *KeySharesParameters { + return &KeySharesParameters{ + ecdhePrivKeymap: make(map[CurveID]*ecdh.PrivateKey), + ecdhePubKeymap: make(map[CurveID]*ecdh.PublicKey), + + kemPrivKeymap: make(map[CurveID]kem.PrivateKey), + kemPubKeymap: make(map[CurveID]kem.PublicKey), + } +} -func (keymap KeySharesEcdheParameters) AddEcdheParams(curveID CurveID, ecdheKey *ecdh.PrivateKey) { - keymap[curveID] = ecdheKey +func (ksp *KeySharesParameters) AddEcdheKeypair(curveID CurveID, ecdheKey *ecdh.PrivateKey, ecdhePubKey *ecdh.PublicKey) { + ksp.ecdhePrivKeymap[curveID] = ecdheKey + ksp.ecdhePubKeymap[curveID] = ecdhePubKey } -func (keymap KeySharesEcdheParameters) GetEcdheParams(curveID CurveID) (ecdheKey *ecdh.PrivateKey, ok bool) { - ecdheKey, ok = keymap[curveID] + +func (ksp *KeySharesParameters) GetEcdheKey(curveID CurveID) (ecdheKey *ecdh.PrivateKey, ok bool) { + ecdheKey, ok = ksp.ecdhePrivKeymap[curveID] return } -func (keymap KeySharesEcdheParameters) GetPublicEcdheParams(curveID CurveID) (params *ecdh.PrivateKey, ok bool) { - params, ok = keymap[curveID] + +func (ksp *KeySharesParameters) GetEcdhePubkey(curveID CurveID) (params *ecdh.PublicKey, ok bool) { + params, ok = ksp.ecdhePubKeymap[curveID] return } -// [uTLS SECTION END] +func (ksp *KeySharesParameters) AddKemKeypair(curveID CurveID, kemKey kem.PrivateKey, kemPubKey kem.PublicKey) { + if curveIdToCirclScheme(curveID) != nil { // only store for circl schemes + ksp.kemPrivKeymap[curveID] = kemKey + ksp.kemPubKeymap[curveID] = kemPubKey + } +} -type clientHandshakeStateTLS13 struct { - c *Conn - ctx context.Context - serverHello *serverHelloMsg - hello *clientHelloMsg - ecdheKey *ecdh.PrivateKey +func (ksp *KeySharesParameters) GetKemKey(curveID CurveID) (kemKey kem.PrivateKey, ok bool) { + kemKey, ok = ksp.kemPrivKeymap[curveID] + return +} + +func (ksp *KeySharesParameters) GetKemPubkey(curveID CurveID) (params kem.PublicKey, ok bool) { + params, ok = ksp.kemPubKeymap[curveID] + return +} - keySharesEcdheParams KeySharesEcdheParameters // [uTLS] +// [uTLS SECTION END] - session *SessionState - earlySecret []byte - binderKey []byte +type clientHandshakeStateTLS13 struct { + c *Conn + ctx context.Context + serverHello *serverHelloMsg + hello *clientHelloMsg + ecdheKey *ecdh.PrivateKey + kemKey *kemPrivateKey // [uTLS] ported from cloudflare/go + keySharesParams *KeySharesParameters // [uTLS] support both ecdhe and kem + + session *SessionState + earlySecret []byte + binderKey []byte + selectedGroup CurveID certReq *certificateRequestMsgTLS13 usingPSK bool @@ -75,15 +117,23 @@ func (hs *clientHandshakeStateTLS13) handshake() error { } // [uTLS SECTION START] - // set echdheParams to what we received from server - if ecdheKey, ok := hs.keySharesEcdheParams.GetEcdheParams(hs.serverHello.serverShare.group); ok { + if ecdheKey, ok := hs.keySharesParams.GetEcdheKey(hs.serverHello.serverShare.group); ok { hs.ecdheKey = ecdheKey + hs.kemKey = nil // unset kemKey if any + } + // set kemParams to what we received from server + if kemKey, ok := hs.keySharesParams.GetKemKey(hs.serverHello.serverShare.group); ok { + hs.kemKey = &kemPrivateKey{ + secretKey: kemKey, + curveID: hs.serverHello.serverShare.group, + } + hs.ecdheKey = nil // unset ecdheKey if any } // [uTLS SECTION END] // Consistency check on the presence of a keyShare and its parameters. - if hs.ecdheKey == nil || len(hs.hello.keyShares) < 1 { // [uTLS] + if (hs.ecdheKey == nil && hs.kemKey == nil) || len(hs.hello.keyShares) < 1 { // [uTLS] // keyshares "< 1" instead of "!= 1", as uTLS may send multiple return c.sendAlert(alertInternalError) } @@ -268,21 +318,55 @@ func (hs *clientHandshakeStateTLS13) processHelloRetryRequest() error { c.sendAlert(alertIllegalParameter) return errors.New("tls: server selected unsupported group") } - if sentID, _ := curveIDForCurve(hs.ecdheKey.Curve()); sentID == curveID { - c.sendAlert(alertIllegalParameter) - return errors.New("tls: server sent an unnecessary HelloRetryRequest key_share") - } - if _, ok := curveForCurveID(curveID); !ok { + + // [UTLS SECTION BEGINS] + // ported from cloudflare/go, slightly modified to maintain compatibility with crypto/tls upstream + if hs.ecdheKey != nil { + if sentID, _ := curveIDForCurve(hs.ecdheKey.Curve()); sentID == curveID { + c.sendAlert(alertIllegalParameter) + return errors.New("tls: server sent an unnecessary HelloRetryRequest key_share") + } + } else if hs.kemKey != nil { + if clientKeySharePrivateCurveID(hs.kemKey) == curveID { + c.sendAlert(alertIllegalParameter) + return errors.New("tls: server sent an unnecessary HelloRetryRequest key_share") + } + } else { c.sendAlert(alertInternalError) - return errors.New("tls: CurvePreferences includes unsupported curve") + return errors.New("tls: ecdheKey and kemKey are both nil") } - key, err := generateECDHEKey(c.config.rand(), curveID) - if err != nil { - c.sendAlert(alertInternalError) - return err + + if scheme := curveIdToCirclScheme(curveID); scheme != nil { + pk, sk, err := generateKemKeyPair(scheme, curveID, c.config.rand()) + if err != nil { + c.sendAlert(alertInternalError) + return fmt.Errorf("HRR generateKemKeyPair %s: %w", + scheme.Name(), err) + } + packedPk, err := pk.MarshalBinary() + if err != nil { + c.sendAlert(alertInternalError) + return fmt.Errorf("HRR pack circl public key %s: %w", + scheme.Name(), err) + } + hs.kemKey = sk + hs.ecdheKey = nil // unset ecdheKey if any + hs.hello.keyShares = []keyShare{{group: curveID, data: packedPk}} + } else { + if _, ok := curveForCurveID(curveID); !ok { + c.sendAlert(alertInternalError) + return errors.New("tls: CurvePreferences includes unsupported curve") + } + key, err := generateECDHEKey(c.config.rand(), curveID) + if err != nil { + c.sendAlert(alertInternalError) + return err + } + hs.ecdheKey = key + hs.kemKey = nil // unset kemKey if any + hs.hello.keyShares = []keyShare{{group: curveID, data: key.PublicKey().Bytes()}} } - hs.ecdheKey = key - hs.hello.keyShares = []keyShare{{group: curveID, data: key.PublicKey().Bytes()}} + // [UTLS SECTION ENDS] } hs.hello.raw = nil @@ -430,10 +514,22 @@ func (hs *clientHandshakeStateTLS13) processServerHello() error { c.sendAlert(alertIllegalParameter) return errors.New("tls: server did not send a key share") } - if sentID, _ := curveIDForCurve(hs.ecdheKey.Curve()); hs.serverHello.serverShare.group != sentID { + + // [UTLS SECTION BEGINS] + var supportedGroupCompatible bool + if hs.ecdheKey != nil { // if we did send ECDHE KeyShare + if sentID, _ := curveIDForCurve(hs.ecdheKey.Curve()); hs.serverHello.serverShare.group == sentID { // and server selected ECDHE KeyShare + supportedGroupCompatible = true + } + } + if hs.kemKey != nil && clientKeySharePrivateCurveID(hs.kemKey) == hs.serverHello.serverShare.group { // we did send KEM KeyShare and server selected KEM KeyShare + supportedGroupCompatible = true + } + if !supportedGroupCompatible { // none matched c.sendAlert(alertIllegalParameter) return errors.New("tls: server selected unsupported group") } + // [UTLS SECTION ENDS] if !hs.serverHello.selectedIdentityPresent { return nil @@ -469,16 +565,38 @@ func (hs *clientHandshakeStateTLS13) processServerHello() error { func (hs *clientHandshakeStateTLS13) establishHandshakeKeys() error { c := hs.c - peerKey, err := hs.ecdheKey.Curve().NewPublicKey(hs.serverHello.serverShare.data) - if err != nil { - c.sendAlert(alertIllegalParameter) - return errors.New("tls: invalid server key share") + // [UTLS SECTION BEGINS] + // ported from cloudflare/go, slightly modified to maintain compatibility with crypto/tls upstream + var sharedKey []byte + var err error + + if hs.ecdheKey != nil { + if ecdheCurveID, _ := curveIDForCurve(hs.ecdheKey.Curve()); ecdheCurveID == hs.serverHello.serverShare.group { + peerKey, err := hs.ecdheKey.Curve().NewPublicKey(hs.serverHello.serverShare.data) + if err != nil { + c.sendAlert(alertIllegalParameter) + return errors.New("tls: invalid server key share") + } + sharedKey, err = hs.ecdheKey.ECDH(peerKey) + if err != nil { + c.sendAlert(alertIllegalParameter) + return errors.New("tls: invalid server key share") + } + } } - sharedKey, err := hs.ecdheKey.ECDH(peerKey) - if err != nil { - c.sendAlert(alertIllegalParameter) - return errors.New("tls: invalid server key share") + if sharedKey == nil && hs.kemKey != nil && clientKeySharePrivateCurveID(hs.kemKey) == hs.serverHello.serverShare.group { + sk := hs.kemKey.secretKey + sharedKey, err = sk.Scheme().Decapsulate(sk, hs.serverHello.serverShare.data) + if err != nil { + c.sendAlert(alertIllegalParameter) + return fmt.Errorf("%s decaps: %w", sk.Scheme().Name(), err) + } + } + if sharedKey == nil { + c.sendAlert(alertInternalError) + return errors.New("tls: ecdheKey and circlKey are both nil") } + // [UTLS SECTION ENDS] earlySecret := hs.earlySecret if !hs.usingPSK { @@ -680,7 +798,7 @@ func (hs *clientHandshakeStateTLS13) readServerCertificate() error { } // See RFC 8446, Section 4.4.3. - if !isSupportedSignatureAlgorithm(certVerify.signatureAlgorithm, supportedSignatureAlgorithms()) { + if !isSupportedSignatureAlgorithm(certVerify.signatureAlgorithm, c.config.supportedSignatureAlgorithms()) { // [UTLS] ported from cloudflare/go c.sendAlert(alertIllegalParameter) return errors.New("tls: certificate used with invalid signature algorithm") } diff --git a/handshake_server.go b/handshake_server.go index dcbda213..c29e9a32 100644 --- a/handshake_server.go +++ b/handshake_server.go @@ -17,6 +17,8 @@ import ( "hash" "io" "time" + + circlSign "github.com/cloudflare/circl/sign" ) // serverHandshakeState contains details of a server handshake in progress. @@ -593,7 +595,7 @@ func (hs *serverHandshakeState) doFullHandshake() error { } if c.vers >= VersionTLS12 { certReq.hasSignatureAlgorithm = true - certReq.supportedSignatureAlgorithms = supportedSignatureAlgorithms() + certReq.supportedSignatureAlgorithms = c.config.supportedSignatureAlgorithms() // [UTLS] ported from cloudflare/go } // An empty list of certificateAuthorities signals to @@ -917,7 +919,7 @@ func (c *Conn) processCertsFromClient(certificate Certificate) error { if len(certs) > 0 { switch certs[0].PublicKey.(type) { - case *ecdsa.PublicKey, *rsa.PublicKey, ed25519.PublicKey: + case *ecdsa.PublicKey, *rsa.PublicKey, ed25519.PublicKey, circlSign.PublicKey: // [UTLS] ported from cloudflare/go default: c.sendAlert(alertUnsupportedCertificate) return fmt.Errorf("tls: client certificate contains an unsupported public key of type %T", certs[0].PublicKey) diff --git a/handshake_server_tls13.go b/handshake_server_tls13.go index 07b1a385..43192b3d 100644 --- a/handshake_server_tls13.go +++ b/handshake_server_tls13.go @@ -12,6 +12,7 @@ import ( "crypto/rsa" "encoding/binary" "errors" + "fmt" "hash" "io" "time" @@ -33,6 +34,7 @@ type serverHandshakeStateTLS13 struct { suite *cipherSuiteTLS13 cert *Certificate sigAlg SignatureScheme + selectedGroup CurveID earlySecret []byte sharedKey []byte handshakeSecret []byte @@ -211,23 +213,31 @@ GroupSelection: clientKeyShare = &hs.clientHello.keyShares[0] } - if _, ok := curveForCurveID(selectedGroup); !ok { + if _, ok := curveForCurveID(selectedGroup); selectedGroup != X25519 && curveIdToCirclScheme(selectedGroup) == nil && !ok { c.sendAlert(alertInternalError) return errors.New("tls: CurvePreferences includes unsupported curve") } - key, err := generateECDHEKey(c.config.rand(), selectedGroup) - if err != nil { - c.sendAlert(alertInternalError) - return err - } - hs.hello.serverShare = keyShare{group: selectedGroup, data: key.PublicKey().Bytes()} - peerKey, err := key.Curve().NewPublicKey(clientKeyShare.data) - if err != nil { - c.sendAlert(alertIllegalParameter) - return errors.New("tls: invalid client key share") + if kem := curveIdToCirclScheme(selectedGroup); kem != nil { + ct, ss, alert, err := encapsulateForKem(kem, c.config.rand(), clientKeyShare.data) + if err != nil { + c.sendAlert(alert) + return fmt.Errorf("%s encap: %w", kem.Name(), err) + } + hs.hello.serverShare = keyShare{group: selectedGroup, data: ct} + hs.sharedKey = ss + } else { + key, err := generateECDHEKey(c.config.rand(), selectedGroup) + if err != nil { + c.sendAlert(alertInternalError) + return err + } + hs.hello.serverShare = keyShare{group: selectedGroup, data: key.PublicKey().Bytes()} + peerKey, err := key.Curve().NewPublicKey(clientKeyShare.data) + if err == nil { + hs.sharedKey, _ = key.ECDH(peerKey) + } } - hs.sharedKey, err = key.ECDH(peerKey) - if err != nil { + if hs.sharedKey == nil { c.sendAlert(alertIllegalParameter) return errors.New("tls: invalid client key share") } @@ -670,7 +680,7 @@ func (hs *serverHandshakeStateTLS13) sendServerCertificate() error { certReq := new(certificateRequestMsgTLS13) certReq.ocspStapling = true certReq.scts = true - certReq.supportedSignatureAlgorithms = supportedSignatureAlgorithms() + certReq.supportedSignatureAlgorithms = c.config.supportedSignatureAlgorithms() // [UTLS] ported from cloudflare/go if c.config.ClientCAs != nil { certReq.certificateAuthorities = c.config.ClientCAs.Subjects() } @@ -932,7 +942,7 @@ func (hs *serverHandshakeStateTLS13) readClientCertificate() error { } // See RFC 8446, Section 4.4.3. - if !isSupportedSignatureAlgorithm(certVerify.signatureAlgorithm, supportedSignatureAlgorithms()) { + if !isSupportedSignatureAlgorithm(certVerify.signatureAlgorithm, c.config.supportedSignatureAlgorithms()) { // [UTLS] ported from cloudflare/go c.sendAlert(alertIllegalParameter) return errors.New("tls: client certificate used with invalid signature algorithm") } diff --git a/key_agreement.go b/key_agreement.go index 2c8c5b8d..3c733454 100644 --- a/key_agreement.go +++ b/key_agreement.go @@ -130,7 +130,7 @@ func md5SHA1Hash(slices [][]byte) []byte { // the sigType (for earlier TLS versions). For Ed25519 signatures, which don't // do pre-hashing, it returns the concatenation of the slices. func hashForServerKeyExchange(sigType uint8, hashFunc crypto.Hash, version uint16, slices ...[]byte) []byte { - if sigType == signatureEd25519 { + if sigType == signatureEd25519 || circlSchemeBySigType(sigType) != nil { // [UTLS] ported from cloudflare/go var signed []byte for _, slice := range slices { signed = append(signed, slice...) @@ -169,7 +169,7 @@ type ecdheKeyAgreement struct { func (ka *ecdheKeyAgreement) generateServerKeyExchange(config *Config, cert *Certificate, clientHello *clientHelloMsg, hello *serverHelloMsg) (*serverKeyExchangeMsg, error) { var curveID CurveID for _, c := range clientHello.supportedCurves { - if config.supportsCurve(c) { + if config.supportsCurve(c) && curveIdToCirclScheme(c) == nil { curveID = c break } diff --git a/prf.go b/prf.go index 20bac96e..c2a024f4 100644 --- a/prf.go +++ b/prf.go @@ -225,11 +225,11 @@ func (h finishedHash) serverSum(masterSecret []byte) []byte { // hashForClientCertificate returns the handshake messages so far, pre-hashed if // necessary, suitable for signing by a TLS client certificate. func (h finishedHash) hashForClientCertificate(sigType uint8, hashAlg crypto.Hash) []byte { - if (h.version >= VersionTLS12 || sigType == signatureEd25519) && h.buffer == nil { + if (h.version >= VersionTLS12 || sigType == signatureEd25519 || circlSchemeBySigType(sigType) != nil) && h.buffer == nil { // [UTLS] ported from cloudflare/go panic("tls: handshake hash for a client certificate requested after discarding the handshake buffer") } - if sigType == signatureEd25519 { + if sigType == signatureEd25519 || circlSchemeBySigType(sigType) != nil { // [UTLS] ported from cloudflare/go return h.buffer } diff --git a/tls.go b/tls.go index b529c705..c4f6f390 100644 --- a/tls.go +++ b/tls.go @@ -25,6 +25,8 @@ import ( "net" "os" "strings" + + circlSign "github.com/cloudflare/circl/sign" ) // Server returns a new TLS server side connection @@ -326,6 +328,20 @@ func X509KeyPair(certPEMBlock, keyPEMBlock []byte) (Certificate, error) { if !bytes.Equal(priv.Public().(ed25519.PublicKey), pub) { return fail(errors.New("tls: private key does not match public key")) } + // [UTLS SECTION BEGINS] + // Ported from cloudflare/go + case circlSign.PublicKey: + priv, ok := cert.PrivateKey.(circlSign.PrivateKey) + if !ok { + return fail(errors.New("tls: private key type does not match public key type")) + } + pkBytes, err := priv.Public().(circlSign.PublicKey).MarshalBinary() + pkBytes2, err2 := pub.MarshalBinary() + + if err != nil || err2 != nil || !bytes.Equal(pkBytes, pkBytes2) { + return fail(errors.New("tls: private key does not match public key")) + } + // [UTLS SECTION ENDS] default: return fail(errors.New("tls: unknown public key algorithm")) } @@ -342,7 +358,7 @@ func parsePrivateKey(der []byte) (crypto.PrivateKey, error) { } if key, err := x509.ParsePKCS8PrivateKey(der); err == nil { switch key := key.(type) { - case *rsa.PrivateKey, *ecdsa.PrivateKey, ed25519.PrivateKey: + case *rsa.PrivateKey, *ecdsa.PrivateKey, ed25519.PrivateKey, circlSign.PrivateKey: return key, nil default: return nil, errors.New("tls: found unknown private key type in PKCS#8 wrapping") diff --git a/tls_cf.go b/tls_cf.go new file mode 100644 index 00000000..8160be09 --- /dev/null +++ b/tls_cf.go @@ -0,0 +1,66 @@ +// Copyright 2021 Cloudflare, Inc. All rights reserved. Use of this source code +// is governed by a BSD-style license that can be found in the LICENSE file. + +package tls + +import ( + circlPki "github.com/cloudflare/circl/pki" + circlSign "github.com/cloudflare/circl/sign" + "github.com/cloudflare/circl/sign/eddilithium3" +) + +// To add a signature scheme from Circl +// +// 1. make sure it implements TLSScheme and CertificateScheme, +// 2. follow the instructions in crypto/x509/x509_cf.go +// 3. add a signature to the iota in common.go +// 4. add row in the circlSchemes lists below + +var circlSchemes = [...]struct { + sigType uint8 + scheme circlSign.Scheme +}{ + {signatureEdDilithium3, eddilithium3.Scheme()}, +} + +func circlSchemeBySigType(sigType uint8) circlSign.Scheme { + for _, cs := range circlSchemes { + if cs.sigType == sigType { + return cs.scheme + } + } + return nil +} + +func sigTypeByCirclScheme(scheme circlSign.Scheme) uint8 { + for _, cs := range circlSchemes { + if cs.scheme == scheme { + return cs.sigType + } + } + return 0 +} + +var supportedSignatureAlgorithmsWithCircl []SignatureScheme + +// supportedSignatureAlgorithms returns enabled signature schemes. PQ signature +// schemes are only included when tls.Config#PQSignatureSchemesEnabled is set +// and FIPS-only mode is not enabled. +func (c *Config) supportedSignatureAlgorithms() []SignatureScheme { + // If FIPS-only mode is requested, do not add other algos. + if needFIPS() { + return supportedSignatureAlgorithms() + } + if c != nil && c.PQSignatureSchemesEnabled { + return supportedSignatureAlgorithmsWithCircl + } + return defaultSupportedSignatureAlgorithms +} + +func init() { + supportedSignatureAlgorithmsWithCircl = append([]SignatureScheme{}, defaultSupportedSignatureAlgorithms...) + for _, cs := range circlSchemes { + supportedSignatureAlgorithmsWithCircl = append(supportedSignatureAlgorithmsWithCircl, + SignatureScheme(cs.scheme.(circlPki.TLSScheme).TLSIdentifier())) + } +} diff --git a/tls_test.go b/tls_test.go index f4e282f1..923fb296 100644 --- a/tls_test.go +++ b/tls_test.go @@ -866,6 +866,8 @@ func TestCloneNonFuncFields(t *testing.T) { f.Set(reflect.ValueOf([]uint16{1, 2})) case "CurvePreferences": f.Set(reflect.ValueOf([]CurveID{CurveP256})) + case "PQSignatureSchemesEnabled": // [UTLS] ported from cloudflare/go + f.Set(reflect.ValueOf(true)) case "Renegotiation": f.Set(reflect.ValueOf(RenegotiateOnceAsClient)) case "mutex", "autoSessionTicketKeys", "sessionTicketKeys": diff --git a/u_common.go b/u_common.go index 776272d0..1b30df42 100644 --- a/u_common.go +++ b/u_common.go @@ -582,9 +582,17 @@ var ( HelloChrome_102 = ClientHelloID{helloChrome, "102", nil, nil} HelloChrome_106_Shuffle = ClientHelloID{helloChrome, "106", nil, nil} // beta: shuffler enabled starting from 106 - // Chrome with PSK: Chrome start sending this ClientHello after doing TLS 1.3 handshake with the same server. - HelloChrome_100_PSK = ClientHelloID{helloChrome, "100_PSK", nil, nil} // beta: PSK extension added. uTLS doesn't fully support PSK. Use at your own risk. - HelloChrome_112_PSK_Shuf = ClientHelloID{helloChrome, "112_PSK", nil, nil} // beta: PSK extension added. uTLS doesn't fully support PSK. Use at your own risk. + // Chrome w/ PSK: Chrome start sending this ClientHello after doing TLS 1.3 handshake with the same server. + // Beta: PSK extension added. However, uTLS doesn't ship with full PSK support. + // Use at your own discretion. + HelloChrome_100_PSK = ClientHelloID{helloChrome, "100_PSK", nil, nil} + HelloChrome_112_PSK_Shuf = ClientHelloID{helloChrome, "112_PSK", nil, nil} + HelloChrome_114_Padding_PSK_Shuf = ClientHelloID{helloChrome, "114_PSK", nil, nil} + + // Chrome w/ Post-Quantum Key Agreement + // Beta: PQ extension added. However, uTLS doesn't ship with full PQ support. Use at your own discretion. + HelloChrome_115_PQ = ClientHelloID{helloChrome, "115_PQ", nil, nil} + HelloChrome_115_PQ_PSK = ClientHelloID{helloChrome, "115_PQ_PSK", nil, nil} HelloIOS_Auto = HelloIOS_14 HelloIOS_11_1 = ClientHelloID{helloIOS, "111", nil, nil} // legacy "111" means 11.1 diff --git a/u_conn.go b/u_conn.go index ef79b58d..cbb4fa41 100644 --- a/u_conn.go +++ b/u_conn.go @@ -9,6 +9,7 @@ import ( "bytes" "context" "crypto/cipher" + "crypto/ecdh" "encoding/binary" "errors" "fmt" @@ -23,6 +24,7 @@ type UConn struct { Extensions []TLSExtension ClientHelloID ClientHelloID + pskExtension []*FakePreSharedKeyExtension ClientHelloBuilt bool HandshakeState PubClientHandshakeState @@ -42,13 +44,13 @@ type UConn struct { // UClient returns a new uTLS client, with behavior depending on clientHelloID. // Config CAN be nil, but make sure to eventually specify ServerName. -func UClient(conn net.Conn, config *Config, clientHelloID ClientHelloID) *UConn { +func UClient(conn net.Conn, config *Config, clientHelloID ClientHelloID, pskExtension ...*FakePreSharedKeyExtension) *UConn { if config == nil { config = &Config{} } tlsConn := Conn{conn: conn, config: config, isClient: true} handshakeState := PubClientHandshakeState{C: &tlsConn, Hello: &PubClientHelloMsg{}} - uconn := UConn{Conn: &tlsConn, ClientHelloID: clientHelloID, HandshakeState: handshakeState} + uconn := UConn{Conn: &tlsConn, ClientHelloID: clientHelloID, pskExtension: pskExtension, HandshakeState: handshakeState} uconn.HandshakeState.uconn = &uconn uconn.handshakeFn = uconn.clientHandshake return &uconn @@ -76,13 +78,19 @@ func (uconn *UConn) BuildHandshakeState() error { } // use default Golang ClientHello. - hello, ecdheKey, err := uconn.makeClientHello() + hello, keySharePrivate, err := uconn.makeClientHello() if err != nil { return err } uconn.HandshakeState.Hello = hello.getPublicPtr() - uconn.HandshakeState.State13.EcdheKey = ecdheKey + if ecdheKey, ok := keySharePrivate.(*ecdh.PrivateKey); ok { + uconn.HandshakeState.State13.EcdheKey = ecdheKey + } else if kemKey, ok := keySharePrivate.(*kemPrivateKey); ok { + uconn.HandshakeState.State13.KEMKey = kemKey.ToPublic() + } else { + return fmt.Errorf("uTLS: unknown keySharePrivate type: %T", keySharePrivate) + } uconn.HandshakeState.C = uconn.Conn } else { if !uconn.ClientHelloBuilt { diff --git a/u_handshake_client.go b/u_handshake_client.go index 7ff80d86..7b9a98f5 100644 --- a/u_handshake_client.go +++ b/u_handshake_client.go @@ -7,7 +7,6 @@ package tls import ( "bytes" "compress/zlib" - "crypto/ecdh" "errors" "fmt" "io" @@ -164,7 +163,7 @@ func (hs *clientHandshakeStateTLS13) utlsReadServerParameters(encryptedExtension return nil } -func (c *Conn) makeClientHelloForApplyPreset() (*clientHelloMsg, *ecdh.PrivateKey, error) { +func (c *Conn) makeClientHelloForApplyPreset() (*clientHelloMsg, clientKeySharePrivate, error) { config := c.config // [UTLS SECTION START] @@ -261,7 +260,7 @@ func (c *Conn) makeClientHelloForApplyPreset() (*clientHelloMsg, *ecdh.PrivateKe hello.supportedSignatureAlgorithms = testingOnlyForceClientHelloSignatureAlgorithms } - var key *ecdh.PrivateKey + var secret clientKeySharePrivate // [UTLS] if hello.supportedVersions[0] == VersionTLS13 { // Reset the list of ciphers when the client only supports TLS 1.3. if len(hello.supportedVersions) == 1 { @@ -273,15 +272,32 @@ func (c *Conn) makeClientHelloForApplyPreset() (*clientHelloMsg, *ecdh.PrivateKe hello.cipherSuites = append(hello.cipherSuites, defaultCipherSuitesTLS13NoAES...) } - curveID := config.curvePreferences()[0] - if _, ok := curveForCurveID(curveID); !ok { - return nil, nil, errors.New("tls: CurvePreferences includes unsupported curve") - } - key, err = generateECDHEKey(config.rand(), curveID) - if err != nil { - return nil, nil, err - } - hello.keyShares = []keyShare{{group: curveID, data: key.PublicKey().Bytes()}} + // curveID := config.curvePreferences()[0] + // // [UTLS SECTION BEGINS] + // // Ported from cloudflare/go with modifications to preserve crypto/tls compatibility + // if scheme := curveIdToCirclScheme(curveID); scheme != nil { + // pk, sk, err := generateKemKeyPair(scheme, curveID, config.rand()) + // if err != nil { + // return nil, nil, fmt.Errorf("generateKemKeyPair %s: %w", scheme.Name(), err) + // } + // packedPk, err := pk.MarshalBinary() + // if err != nil { + // return nil, nil, fmt.Errorf("pack circl public key %s: %w", scheme.Name(), err) + // } + // hello.keyShares = []keyShare{{group: curveID, data: packedPk}} + // secret = sk + // } else { + // if _, ok := curveForCurveID(curveID); !ok { + // return nil, nil, errors.New("tls: CurvePreferences includes unsupported curve") + // } + // key, err := generateECDHEKey(config.rand(), curveID) + // if err != nil { + // return nil, nil, err + // } + // hello.keyShares = []keyShare{{group: curveID, data: key.PublicKey().Bytes()}} + // secret = key + // } + // // [UTLS SECTION ENDS] } // [UTLS] We don't need this, since it is not ready yet @@ -296,5 +312,5 @@ func (c *Conn) makeClientHelloForApplyPreset() (*clientHelloMsg, *ecdh.PrivateKe // hello.quicTransportParameters = p // } - return hello, key, nil + return hello, secret, nil } diff --git a/u_parrots.go b/u_parrots.go index 7e2660f9..070e0059 100644 --- a/u_parrots.go +++ b/u_parrots.go @@ -5,6 +5,7 @@ package tls import ( + "crypto/ecdh" "crypto/sha256" "encoding/binary" "errors" @@ -15,11 +16,24 @@ import ( "strconv" ) +var ErrUnknownClientHelloID = errors.New("tls: unknown ClientHelloID") +var ErrNotPSKClientHelloID = errors.New("tls: ClientHello does not contain pre_shared_key extension") +var ErrPSKExtensionExpected = errors.New("tls: pre_shared_key extension expected when fetching preset ClientHelloSpec") + // UTLSIdToSpec converts a ClientHelloID to a corresponding ClientHelloSpec. // // Exported internal function utlsIdToSpec per request. -func UTLSIdToSpec(id ClientHelloID) (ClientHelloSpec, error) { - return utlsIdToSpec(id) +func UTLSIdToSpec(id ClientHelloID, pskExtension ...*FakePreSharedKeyExtension) (ClientHelloSpec, error) { + if len(pskExtension) > 1 { + return ClientHelloSpec{}, errors.New("tls: at most one FakePreSharedKeyExtensions is allowed") + } + + chs, err := utlsIdToSpec(id) + if err != nil && errors.Is(err, ErrUnknownClientHelloID) { + chs, err = utlsIdToSpecWithPSK(id, pskExtension...) + } + + return chs, err } func utlsIdToSpec(id ClientHelloID) (ClientHelloSpec, error) { @@ -508,7 +522,7 @@ func utlsIdToSpec(id ClientHelloID) (ClientHelloSpec, error) { &UtlsPaddingExtension{GetPaddingLen: BoringPaddingStyle}, }, }, nil - case HelloChrome_100_PSK: + case HelloChrome_106_Shuffle: return ClientHelloSpec{ CipherSuites: []uint16{ GREASE_PLACEHOLDER, @@ -531,7 +545,7 @@ func utlsIdToSpec(id ClientHelloID) (ClientHelloSpec, error) { CompressionMethods: []byte{ 0x00, // compressionNone }, - Extensions: []TLSExtension{ + Extensions: ShuffleChromeTLSExtensions([]TLSExtension{ &UtlsGREASEExtension{}, &SNIExtension{}, &ExtendedMasterSecretExtension{}, @@ -576,27 +590,83 @@ func utlsIdToSpec(id ClientHelloID) (ClientHelloSpec, error) { }}, &ApplicationSettingsExtension{SupportedProtocols: []string{"h2"}}, &UtlsGREASEExtension{}, - &FakePreSharedKeyExtension{}, + &UtlsPaddingExtension{GetPaddingLen: BoringPaddingStyle}, + }), + }, nil + // Chrome w/ Post-Quantum Key Agreement + case HelloChrome_115_PQ: + return ClientHelloSpec{ + CipherSuites: []uint16{ + GREASE_PLACEHOLDER, + TLS_AES_128_GCM_SHA256, + TLS_AES_256_GCM_SHA384, + TLS_CHACHA20_POLY1305_SHA256, + TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256, + TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256, + TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384, + TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384, + TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305, + TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305, + TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA, + TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA, + TLS_RSA_WITH_AES_128_GCM_SHA256, + TLS_RSA_WITH_AES_256_GCM_SHA384, + TLS_RSA_WITH_AES_128_CBC_SHA, + TLS_RSA_WITH_AES_256_CBC_SHA, + }, + CompressionMethods: []byte{ + 0x00, // compressionNone }, + Extensions: ShuffleChromeTLSExtensions([]TLSExtension{ + &UtlsGREASEExtension{}, + &SNIExtension{}, + &ExtendedMasterSecretExtension{}, + &RenegotiationInfoExtension{Renegotiation: RenegotiateOnceAsClient}, + &SupportedCurvesExtension{[]CurveID{ + GREASE_PLACEHOLDER, + X25519Kyber768Draft00, + X25519, + CurveP256, + CurveP384, + }}, + &SupportedPointsExtension{SupportedPoints: []byte{ + 0x00, // pointFormatUncompressed + }}, + &SessionTicketExtension{}, + &ALPNExtension{AlpnProtocols: []string{"h2", "http/1.1"}}, + &StatusRequestExtension{}, + &SignatureAlgorithmsExtension{SupportedSignatureAlgorithms: []SignatureScheme{ + ECDSAWithP256AndSHA256, + PSSWithSHA256, + PKCS1WithSHA256, + ECDSAWithP384AndSHA384, + PSSWithSHA384, + PKCS1WithSHA384, + PSSWithSHA512, + PKCS1WithSHA512, + }}, + &SCTExtension{}, + &KeyShareExtension{[]KeyShare{ + {Group: CurveID(GREASE_PLACEHOLDER), Data: []byte{0}}, + {Group: X25519Kyber768Draft00}, + {Group: X25519}, + }}, + &PSKKeyExchangeModesExtension{[]uint8{ + PskModeDHE, + }}, + &SupportedVersionsExtension{[]uint16{ + GREASE_PLACEHOLDER, + VersionTLS13, + VersionTLS12, + }}, + &UtlsCompressCertExtension{[]CertCompressionAlgo{ + CertCompressionBrotli, + }}, + &ApplicationSettingsExtension{SupportedProtocols: []string{"h2"}}, + &UtlsGREASEExtension{}, + &UtlsPaddingExtension{GetPaddingLen: BoringPaddingStyle}, + }), }, nil - case HelloChrome_106_Shuffle: - chs, err := utlsIdToSpec(HelloChrome_102) - if err != nil { - return chs, err - } - - // Chrome 107 started shuffling the order of extensions - shuffleExtensions(&chs) - return chs, err - case HelloChrome_112_PSK_Shuf: - chs, err := utlsIdToSpec(HelloChrome_100_PSK) - if err != nil { - return chs, err - } - - // Chrome 112 started shuffling the order of extensions - shuffleExtensions(&chs) - return chs, err case HelloFirefox_55, HelloFirefox_56: return ClientHelloSpec{ TLSVersMax: VersionTLS12, @@ -1930,53 +2000,337 @@ func utlsIdToSpec(id ClientHelloID) (ClientHelloSpec, error) { // Use empty values as they can be filled later by UConn.ApplyPreset or manually. return generateRandomizedSpec(&id, "", nil, nil) } - return ClientHelloSpec{}, errors.New("ClientHello ID " + id.Str() + " is unknown") + return ClientHelloSpec{}, fmt.Errorf("%w: %s", ErrUnknownClientHelloID, id.Str()) } } -func shuffleExtensions(chs *ClientHelloSpec) error { - // Shuffle extensions to avoid fingerprinting -- introduced in Chrome 106 - var err error = nil +func utlsIdToSpecWithPSK(id ClientHelloID, pskExtension ...*FakePreSharedKeyExtension) (ClientHelloSpec, error) { + switch id { + case HelloChrome_100_PSK, HelloChrome_112_PSK_Shuf, HelloChrome_114_Padding_PSK_Shuf, HelloChrome_115_PQ_PSK: + if len(pskExtension) == 0 || pskExtension[0] == nil { + return ClientHelloSpec{}, fmt.Errorf("%w: %s", ErrPSKExtensionExpected, id.Str()) + } + } - // unshufCheck checks: - // - if the exts[idx] is a GREASE extension, then it should not be shuffled - // - if the exts[idx] is a padding/pre_shared_key extension, then it should be the - // last extension in the list and should not be shuffled - var unshufCheck = func(idx int, exts []TLSExtension) (donotshuf bool, userErr error) { + switch id { + case HelloChrome_100_PSK: + return ClientHelloSpec{ + CipherSuites: []uint16{ + GREASE_PLACEHOLDER, + TLS_AES_128_GCM_SHA256, + TLS_AES_256_GCM_SHA384, + TLS_CHACHA20_POLY1305_SHA256, + TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256, + TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256, + TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384, + TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384, + TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305, + TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305, + TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA, + TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA, + TLS_RSA_WITH_AES_128_GCM_SHA256, + TLS_RSA_WITH_AES_256_GCM_SHA384, + TLS_RSA_WITH_AES_128_CBC_SHA, + TLS_RSA_WITH_AES_256_CBC_SHA, + }, + CompressionMethods: []byte{ + 0x00, // compressionNone + }, + Extensions: []TLSExtension{ + &UtlsGREASEExtension{}, + &SNIExtension{}, + &ExtendedMasterSecretExtension{}, + &RenegotiationInfoExtension{Renegotiation: RenegotiateOnceAsClient}, + &SupportedCurvesExtension{[]CurveID{ + GREASE_PLACEHOLDER, + X25519, + CurveP256, + CurveP384, + }}, + &SupportedPointsExtension{SupportedPoints: []byte{ + 0x00, // pointFormatUncompressed + }}, + &SessionTicketExtension{}, + &ALPNExtension{AlpnProtocols: []string{"h2", "http/1.1"}}, + &StatusRequestExtension{}, + &SignatureAlgorithmsExtension{SupportedSignatureAlgorithms: []SignatureScheme{ + ECDSAWithP256AndSHA256, + PSSWithSHA256, + PKCS1WithSHA256, + ECDSAWithP384AndSHA384, + PSSWithSHA384, + PKCS1WithSHA384, + PSSWithSHA512, + PKCS1WithSHA512, + }}, + &SCTExtension{}, + &KeyShareExtension{[]KeyShare{ + {Group: CurveID(GREASE_PLACEHOLDER), Data: []byte{0}}, + {Group: X25519}, + }}, + &PSKKeyExchangeModesExtension{[]uint8{ + PskModeDHE, + }}, + &SupportedVersionsExtension{[]uint16{ + GREASE_PLACEHOLDER, + VersionTLS13, + VersionTLS12, + }}, + &UtlsCompressCertExtension{[]CertCompressionAlgo{ + CertCompressionBrotli, + }}, + &ApplicationSettingsExtension{SupportedProtocols: []string{"h2"}}, + &UtlsGREASEExtension{}, + pskExtension[0], + }, + }, nil + case HelloChrome_112_PSK_Shuf: + return ClientHelloSpec{ + CipherSuites: []uint16{ + GREASE_PLACEHOLDER, + TLS_AES_128_GCM_SHA256, + TLS_AES_256_GCM_SHA384, + TLS_CHACHA20_POLY1305_SHA256, + TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256, + TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256, + TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384, + TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384, + TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305, + TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305, + TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA, + TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA, + TLS_RSA_WITH_AES_128_GCM_SHA256, + TLS_RSA_WITH_AES_256_GCM_SHA384, + TLS_RSA_WITH_AES_128_CBC_SHA, + TLS_RSA_WITH_AES_256_CBC_SHA, + }, + CompressionMethods: []byte{ + 0x00, // compressionNone + }, + Extensions: ShuffleChromeTLSExtensions([]TLSExtension{ + &UtlsGREASEExtension{}, + &SNIExtension{}, + &ExtendedMasterSecretExtension{}, + &RenegotiationInfoExtension{Renegotiation: RenegotiateOnceAsClient}, + &SupportedCurvesExtension{[]CurveID{ + GREASE_PLACEHOLDER, + X25519, + CurveP256, + CurveP384, + }}, + &SupportedPointsExtension{SupportedPoints: []byte{ + 0x00, // pointFormatUncompressed + }}, + &SessionTicketExtension{}, + &ALPNExtension{AlpnProtocols: []string{"h2", "http/1.1"}}, + &StatusRequestExtension{}, + &SignatureAlgorithmsExtension{SupportedSignatureAlgorithms: []SignatureScheme{ + ECDSAWithP256AndSHA256, + PSSWithSHA256, + PKCS1WithSHA256, + ECDSAWithP384AndSHA384, + PSSWithSHA384, + PKCS1WithSHA384, + PSSWithSHA512, + PKCS1WithSHA512, + }}, + &SCTExtension{}, + &KeyShareExtension{[]KeyShare{ + {Group: CurveID(GREASE_PLACEHOLDER), Data: []byte{0}}, + {Group: X25519}, + }}, + &PSKKeyExchangeModesExtension{[]uint8{ + PskModeDHE, + }}, + &SupportedVersionsExtension{[]uint16{ + GREASE_PLACEHOLDER, + VersionTLS13, + VersionTLS12, + }}, + &UtlsCompressCertExtension{[]CertCompressionAlgo{ + CertCompressionBrotli, + }}, + &ApplicationSettingsExtension{SupportedProtocols: []string{"h2"}}, + &UtlsGREASEExtension{}, + pskExtension[0], + }), + }, nil + case HelloChrome_114_Padding_PSK_Shuf: + return ClientHelloSpec{ + CipherSuites: []uint16{ + GREASE_PLACEHOLDER, + TLS_AES_128_GCM_SHA256, + TLS_AES_256_GCM_SHA384, + TLS_CHACHA20_POLY1305_SHA256, + TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256, + TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256, + TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384, + TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384, + TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305, + TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305, + TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA, + TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA, + TLS_RSA_WITH_AES_128_GCM_SHA256, + TLS_RSA_WITH_AES_256_GCM_SHA384, + TLS_RSA_WITH_AES_128_CBC_SHA, + TLS_RSA_WITH_AES_256_CBC_SHA, + }, + CompressionMethods: []byte{ + 0x00, // compressionNone + }, + Extensions: ShuffleChromeTLSExtensions([]TLSExtension{ + &UtlsGREASEExtension{}, + &SNIExtension{}, + &ExtendedMasterSecretExtension{}, + &RenegotiationInfoExtension{Renegotiation: RenegotiateOnceAsClient}, + &SupportedCurvesExtension{[]CurveID{ + GREASE_PLACEHOLDER, + X25519, + CurveP256, + CurveP384, + }}, + &SupportedPointsExtension{SupportedPoints: []byte{ + 0x00, // pointFormatUncompressed + }}, + &SessionTicketExtension{}, + &ALPNExtension{AlpnProtocols: []string{"h2", "http/1.1"}}, + &StatusRequestExtension{}, + &SignatureAlgorithmsExtension{SupportedSignatureAlgorithms: []SignatureScheme{ + ECDSAWithP256AndSHA256, + PSSWithSHA256, + PKCS1WithSHA256, + ECDSAWithP384AndSHA384, + PSSWithSHA384, + PKCS1WithSHA384, + PSSWithSHA512, + PKCS1WithSHA512, + }}, + &SCTExtension{}, + &KeyShareExtension{[]KeyShare{ + {Group: CurveID(GREASE_PLACEHOLDER), Data: []byte{0}}, + {Group: X25519}, + }}, + &PSKKeyExchangeModesExtension{[]uint8{ + PskModeDHE, + }}, + &SupportedVersionsExtension{[]uint16{ + GREASE_PLACEHOLDER, + VersionTLS13, + VersionTLS12, + }}, + &UtlsCompressCertExtension{[]CertCompressionAlgo{ + CertCompressionBrotli, + }}, + &ApplicationSettingsExtension{SupportedProtocols: []string{"h2"}}, + &UtlsGREASEExtension{}, + &UtlsPaddingExtension{GetPaddingLen: BoringPaddingStyle}, + pskExtension[0], + }), + }, nil + // Chrome w/ Post-Quantum Key Agreement + case HelloChrome_115_PQ_PSK: + return ClientHelloSpec{ + CipherSuites: []uint16{ + GREASE_PLACEHOLDER, + TLS_AES_128_GCM_SHA256, + TLS_AES_256_GCM_SHA384, + TLS_CHACHA20_POLY1305_SHA256, + TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256, + TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256, + TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384, + TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384, + TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305, + TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305, + TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA, + TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA, + TLS_RSA_WITH_AES_128_GCM_SHA256, + TLS_RSA_WITH_AES_256_GCM_SHA384, + TLS_RSA_WITH_AES_128_CBC_SHA, + TLS_RSA_WITH_AES_256_CBC_SHA, + }, + CompressionMethods: []byte{ + 0x00, // compressionNone + }, + Extensions: ShuffleChromeTLSExtensions([]TLSExtension{ + &UtlsGREASEExtension{}, + &SNIExtension{}, + &ExtendedMasterSecretExtension{}, + &RenegotiationInfoExtension{Renegotiation: RenegotiateOnceAsClient}, + &SupportedCurvesExtension{[]CurveID{ + GREASE_PLACEHOLDER, + X25519Kyber768Draft00, + X25519, + CurveP256, + CurveP384, + }}, + &SupportedPointsExtension{SupportedPoints: []byte{ + 0x00, // pointFormatUncompressed + }}, + &SessionTicketExtension{}, + &ALPNExtension{AlpnProtocols: []string{"h2", "http/1.1"}}, + &StatusRequestExtension{}, + &SignatureAlgorithmsExtension{SupportedSignatureAlgorithms: []SignatureScheme{ + ECDSAWithP256AndSHA256, + PSSWithSHA256, + PKCS1WithSHA256, + ECDSAWithP384AndSHA384, + PSSWithSHA384, + PKCS1WithSHA384, + PSSWithSHA512, + PKCS1WithSHA512, + }}, + &SCTExtension{}, + &KeyShareExtension{[]KeyShare{ + {Group: CurveID(GREASE_PLACEHOLDER), Data: []byte{0}}, + {Group: X25519Kyber768Draft00}, + {Group: X25519}, + }}, + &PSKKeyExchangeModesExtension{[]uint8{ + PskModeDHE, + }}, + &SupportedVersionsExtension{[]uint16{ + GREASE_PLACEHOLDER, + VersionTLS13, + VersionTLS12, + }}, + &UtlsCompressCertExtension{[]CertCompressionAlgo{ + CertCompressionBrotli, + }}, + &ApplicationSettingsExtension{SupportedProtocols: []string{"h2"}}, + &UtlsGREASEExtension{}, + pskExtension[0], + }), + }, nil + } + + return ClientHelloSpec{}, fmt.Errorf("%w: %s", ErrUnknownClientHelloID, id.Str()) +} + +// ShuffleChromeTLSExtensions shuffles the extensions in the ClientHelloSpec to avoid ossification. +// It shuffles every extension except GREASE, padding and pre_shared_key extensions. +// +// This feature was first introduced by Chrome 106. +func ShuffleChromeTLSExtensions(exts []TLSExtension) []TLSExtension { + // unshufCheck checks if the exts[idx] is a GREASE/padding/pre_shared_key extension, + // and returns true on success. For these extensions are considered positionally invariant. + var skipShuf = func(idx int, exts []TLSExtension) bool { switch exts[idx].(type) { - case *UtlsGREASEExtension: - donotshuf = true - case *UtlsPaddingExtension, *FakePreSharedKeyExtension: - donotshuf = true - if idx != len(chs.Extensions)-1 { - userErr = errors.New("UtlsPaddingExtension or FakePreSharedKeyExtension must be the last extension") - } + case *UtlsGREASEExtension, *UtlsPaddingExtension, *FakePreSharedKeyExtension: + return true default: - donotshuf = false + return false } - return } // Shuffle other extensions - rand.Shuffle(len(chs.Extensions), func(i, j int) { - if unshuf, shuferr := unshufCheck(i, chs.Extensions); unshuf { - if shuferr != nil { - err = shuferr - } - return + rand.Shuffle(len(exts), func(i, j int) { + if skipShuf(i, exts) || skipShuf(j, exts) { + return // do not shuffle some of the extensions } - - if unshuf, shuferr := unshufCheck(j, chs.Extensions); unshuf { - if shuferr != nil { - err = shuferr - } - return - } - - chs.Extensions[i], chs.Extensions[j] = chs.Extensions[j], chs.Extensions[i] + exts[i], exts[j] = exts[j], exts[i] }) - return err + return exts } func (uconn *UConn) applyPresetByID(id ClientHelloID) (err error) { @@ -1993,7 +2347,7 @@ func (uconn *UConn) applyPresetByID(id ClientHelloID) (err error) { return nil default: - spec, err = utlsIdToSpec(id) + spec, err = UTLSIdToSpec(id, uconn.pskExtension...) if err != nil { return err } @@ -2013,13 +2367,17 @@ func (uconn *UConn) ApplyPreset(p *ClientHelloSpec) error { return err } - privateHello, ecdheKey, err := uconn.makeClientHelloForApplyPreset() + privateHello, clientKeySharePrivate, err := uconn.makeClientHelloForApplyPreset() if err != nil { return err } uconn.HandshakeState.Hello = privateHello.getPublicPtr() - uconn.HandshakeState.State13.EcdheKey = ecdheKey - uconn.HandshakeState.State13.KeySharesEcdheParams = make(KeySharesEcdheParameters, 2) + if ecdheKey, ok := clientKeySharePrivate.(*ecdh.PrivateKey); ok { + uconn.HandshakeState.State13.EcdheKey = ecdheKey + } else if kemKey, ok := clientKeySharePrivate.(*kemPrivateKey); ok { + uconn.HandshakeState.State13.KEMKey = kemKey.ToPublic() + } + uconn.HandshakeState.State13.KeySharesParams = NewKeySharesParameters() hello := uconn.HandshakeState.Hello session := uconn.HandshakeState.Session @@ -2119,17 +2477,37 @@ func (uconn *UConn) ApplyPreset(p *ClientHelloSpec) error { continue } - ecdheKey, err := generateECDHEKey(uconn.config.rand(), curveID) - if err != nil { - return fmt.Errorf("unsupported Curve in KeyShareExtension: %v."+ - "To mimic it, fill the Data(key) field manually", curveID) - } - uconn.HandshakeState.State13.KeySharesEcdheParams.AddEcdheParams(curveID, ecdheKey) - ext.KeyShares[i].Data = ecdheKey.PublicKey().Bytes() - if !preferredCurveIsSet { - // only do this once for the first non-grease curve - uconn.HandshakeState.State13.EcdheKey = ecdheKey - preferredCurveIsSet = true + if scheme := curveIdToCirclScheme(curveID); scheme != nil { + pk, sk, err := generateKemKeyPair(scheme, curveID, uconn.config.rand()) + if err != nil { + return fmt.Errorf("HRR generateKemKeyPair %s: %w", + scheme.Name(), err) + } + packedPk, err := pk.MarshalBinary() + if err != nil { + return fmt.Errorf("HRR pack circl public key %s: %w", + scheme.Name(), err) + } + uconn.HandshakeState.State13.KeySharesParams.AddKemKeypair(curveID, sk.secretKey, pk) + ext.KeyShares[i].Data = packedPk + if !preferredCurveIsSet { + // only do this once for the first non-grease curve + uconn.HandshakeState.State13.KEMKey = sk.ToPublic() + preferredCurveIsSet = true + } + } else { + ecdheKey, err := generateECDHEKey(uconn.config.rand(), curveID) + if err != nil { + return fmt.Errorf("unsupported Curve in KeyShareExtension: %v."+ + "To mimic it, fill the Data(key) field manually", curveID) + } + uconn.HandshakeState.State13.KeySharesParams.AddEcdheKeypair(curveID, ecdheKey, ecdheKey.PublicKey()) + ext.KeyShares[i].Data = ecdheKey.PublicKey().Bytes() + if !preferredCurveIsSet { + // only do this once for the first non-grease curve + uconn.HandshakeState.State13.EcdheKey = ecdheKey + preferredCurveIsSet = true + } } } case *SupportedVersionsExtension: diff --git a/u_public.go b/u_public.go index 3699b80a..55caa431 100644 --- a/u_public.go +++ b/u_public.go @@ -10,6 +10,8 @@ import ( "crypto/x509" "hash" "time" + + "github.com/cloudflare/circl/kem" ) // ClientHandshakeState includes both TLS 1.3-only and TLS 1.2-only states, @@ -36,16 +38,17 @@ type PubClientHandshakeState struct { // TLS 1.3 only type TLS13OnlyState struct { - Suite *PubCipherSuiteTLS13 - EcdheKey *ecdh.PrivateKey - KeySharesEcdheParams KeySharesEcdheParameters - EarlySecret []byte - BinderKey []byte - CertReq *CertificateRequestMsgTLS13 - UsingPSK bool - SentDummyCCS bool - Transcript hash.Hash - TrafficSecret []byte // client_application_traffic_secret_0 + Suite *PubCipherSuiteTLS13 + EcdheKey *ecdh.PrivateKey + KeySharesParams *KeySharesParameters + KEMKey *KemPrivateKey + EarlySecret []byte + BinderKey []byte + CertReq *CertificateRequestMsgTLS13 + UsingPSK bool + SentDummyCCS bool + Transcript hash.Hash + TrafficSecret []byte // client_application_traffic_secret_0 } // TLS 1.2 and before only @@ -59,11 +62,12 @@ func (chs *PubClientHandshakeState) toPrivate13() *clientHandshakeStateTLS13 { return nil } else { return &clientHandshakeStateTLS13{ - c: chs.C, - serverHello: chs.ServerHello.getPrivatePtr(), - hello: chs.Hello.getPrivatePtr(), - ecdheKey: chs.State13.EcdheKey, - keySharesEcdheParams: chs.State13.KeySharesEcdheParams, + c: chs.C, + serverHello: chs.ServerHello.getPrivatePtr(), + hello: chs.Hello.getPrivatePtr(), + ecdheKey: chs.State13.EcdheKey, + keySharesParams: chs.State13.KeySharesParams, + kemKey: chs.State13.KEMKey.ToPrivate(), session: chs.Session, earlySecret: chs.State13.EarlySecret, @@ -87,16 +91,17 @@ func (chs13 *clientHandshakeStateTLS13) toPublic13() *PubClientHandshakeState { return nil } else { tls13State := TLS13OnlyState{ - KeySharesEcdheParams: chs13.keySharesEcdheParams, - EcdheKey: chs13.ecdheKey, - EarlySecret: chs13.earlySecret, - BinderKey: chs13.binderKey, - CertReq: chs13.certReq.toPublic(), - UsingPSK: chs13.usingPSK, - SentDummyCCS: chs13.sentDummyCCS, - Suite: chs13.suite.toPublic(), - TrafficSecret: chs13.trafficSecret, - Transcript: chs13.transcript, + KeySharesParams: chs13.keySharesParams, + EcdheKey: chs13.ecdheKey, + KEMKey: chs13.kemKey.ToPublic(), + EarlySecret: chs13.earlySecret, + BinderKey: chs13.binderKey, + CertReq: chs13.certReq.toPublic(), + UsingPSK: chs13.usingPSK, + SentDummyCCS: chs13.sentDummyCCS, + Suite: chs13.suite.toPublic(), + TrafficSecret: chs13.trafficSecret, + Transcript: chs13.transcript, } return &PubClientHandshakeState{ C: chs13.c, @@ -734,3 +739,30 @@ func (TKS TicketKeys) ToPrivate() []ticketKey { } return tks } + +type KemPrivateKey struct { + SecretKey kem.PrivateKey + CurveID CurveID +} + +func (kpk *KemPrivateKey) ToPrivate() *kemPrivateKey { + if kpk == nil { + return nil + } else { + return &kemPrivateKey{ + secretKey: kpk.SecretKey, + curveID: kpk.CurveID, + } + } +} + +func (kpk *kemPrivateKey) ToPublic() *KemPrivateKey { + if kpk == nil { + return nil + } else { + return &KemPrivateKey{ + SecretKey: kpk.secretKey, + CurveID: kpk.curveID, + } + } +}