From c2bcb2645a02d7a5c18d0c34b46ceb2773638403 Mon Sep 17 00:00:00 2001 From: Gaukas Wang Date: Tue, 15 Aug 2023 17:29:24 -0600 Subject: [PATCH] new: Support TLS-PSK (TLS 1.3) (#228) * uTLS: X25519Kyber768Draft00 hybrid post-quantum key agreement by cloudflare/go (#222) * crypto/tls: Add hybrid post-quantum key agreement (#13) * import: client-side KEM from cloudflare/go * import: server-side KEM from cloudflare/go * fix: modify test to get rid of CFEvents. Note: uTLS does not promise any server-side functionality, and this change is made to be able to conduct unit tests which requires both side to be able to handle KEM Curves. Co-authored-by: Christopher Wood Co-Authored-By: Bas Westerbaan ---- Based on: * crypto/tls: Add hybrid post-quantum key agreement Adds X25519Kyber512Draft00, X25519Kyber768Draft00, and P256Kyber768Draft00 hybrid post-quantum key agreements with temporary group identifiers. The hybrid post-quantum key exchanges uses plain X{25519,448} instead of HPKE, which we assume will be more likely to be adopted. The order is chosen to match CECPQ2. Not enabled by default. Adds CFEvents to detect `HelloRetryRequest`s and to signal which key agreement was used. Co-authored-by: Christopher Wood [bas, 1.20.1: also adds P256Kyber768Draft00] [pwu, 1.20.4: updated circl to v1.3.3, moved code to cfevent.go] * crypto: add support for CIRCL signature schemes * only partially port the commit from cloudflare/go. We would stick to the official x509 at the cost of incompatibility. Co-Authored-By: Bas Westerbaan Co-Authored-By: Christopher Patton <3453007+cjpatton@users.noreply.github.com> Co-Authored-By: Peter Wu * crypto/tls: add new X25519Kyber768Draft00 code point Ported from cloudflare/go to support the upcoming new post-quantum keyshare. ---- * Point tls.X25519Kyber768Draft00 to the new 0x6399 identifier while the old 0xfe31 identifier is available as tls.X25519Kyber768Draft00Old. * Make sure that the kem.PrivateKey can always be mapped to the CurveID that was linked to it. This is needed since we now have two ID aliasing to the same scheme, and clients need to be able to detect whether the key share presented by the server actually matches the key share that the client originally sent. * Update tests, add the new identifier and remove unnecessary code. Link: https://mailarchive.ietf.org/arch/msg/tls/HAWpNpgptl--UZNSYuvsjB-Pc2k/ Link: https://datatracker.ietf.org/doc/draft-tls-westerbaan-xyber768d00/02/ Co-Authored-By: Peter Wu Co-Authored-By: Bas Westerbaan --------- Co-authored-by: Bas Westerbaan Co-authored-by: Christopher Patton <3453007+cjpatton@users.noreply.github.com> Co-authored-by: Peter Wu * new: enable PQ parrots (#225) * Redesign KeySharesEcdheParameters into KeySharesParameters which supports multiple types of keys. * Optimize program logic to prevent using unwanted keys * new: more parrots and safety update (#227) * new: PQ and other parrots Add new preset parrots: - HelloChrome_114_Padding_PSK_Shuf - HelloChrome_115_PQ - HelloChrome_115_PQ_PSK * new: ShuffleChromeTLSExtensions Implement a new function `ShuffleChromeTLSExtensions(exts []TLSExtension) []TLSExtension`. * update: include psk parameter for parrot-related functions Update following functions' prototype to accept an optional pskExtension (of type *FakePreSharedKeyExtension): - `UClient(conn net.Conn, config *Config, clientHelloID ClientHelloID)` => `UClient(conn net.Conn, config *Config, clientHelloID ClientHelloID, pskExtension ...*FakePreSharedKeyExtension)` - `UTLSIdToSpec(id ClientHelloID)` => `UTLSIdToSpec(id ClientHelloID, pskExtension ...*FakePreSharedKeyExtension)` * new: pre-defined error from UTLSIdToSpec Update UTLSIdToSpec to return more comprehensive errors by pre-defining them, allowing easier error comparing/unwrapping. * new: UtlsPreSharedKeyExtension In `u_pre_shared_key.go`, create `PreSharedKeyExtension` as an interface, with 3 implementations: - `UtlsPreSharedKeyExtension` implements full support for `pre_shared_key` less resuming after seeing HRR. - `FakePreSharedKeyExtension` uses CipherSuiteID, SessionSecret and Identities to calculate the corresponding binders and send them, without setting the internal states. Therefore if the server accepts the PSK and tries to resume, the connection fails. - `HardcodedPreSharedKeyExtension` allows user to hardcode Identities and Binders to be sent in the extension without setting the internal states. Therefore if the server accepts the PSK and tries to resume, the connection fails. TODO: Only one of FakePreSharedKeyExtension and HardcodedPreSharedKeyExtension should be kept, the other one should be just removed. We still need to learn more of the safety of hardcoding both Identities and Binders without recalculating the latter. --------- Co-authored-by: Bas Westerbaan Co-authored-by: Christopher Patton <3453007+cjpatton@users.noreply.github.com> Co-authored-by: Peter Wu --- auth.go | 55 +++- auth_test.go | 4 +- cfkem.go | 101 ++++++++ cfkem_test.go | 107 ++++++++ common.go | 7 + conn.go | 2 +- generate_cert.go | 18 ++ go.mod | 1 + go.sum | 2 + handshake_client.go | 67 +++-- handshake_client_tls13.go | 204 +++++++++++---- handshake_messages.go | 2 +- handshake_server.go | 6 +- handshake_server_tls13.go | 40 +-- key_agreement.go | 4 +- prf.go | 4 +- tls.go | 18 +- tls_cf.go | 66 +++++ tls_test.go | 2 + u_common.go | 22 +- u_conn.go | 80 ++++-- u_fingerprinter_test.go | 45 ++-- u_handshake_client.go | 42 ++- u_parrots.go | 524 ++++++++++++++++++++++++++++++++------ u_pre_shared_key.go | 503 ++++++++++++++++++++++++++++++++++++ u_public.go | 82 ++++-- u_tls_extensions.go | 176 +------------ 27 files changed, 1758 insertions(+), 426 deletions(-) create mode 100644 cfkem.go create mode 100644 cfkem_test.go create mode 100644 tls_cf.go create mode 100644 u_pre_shared_key.go 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/conn.go b/conn.go index b8053314..a61bd04f 100644 --- a/conn.go +++ b/conn.go @@ -96,7 +96,7 @@ type Conn struct { // clientProtocol is the negotiated ALPN protocol. clientProtocol string - utls utlsConnExtraFields // [UTLS】 used for extensive things such as ALPS + utls utlsConnExtraFields // [UTLS] used for extensive things such as ALPS, PSK, etc // input/output in, out halfConn 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..aa56de39 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. @@ -297,6 +324,12 @@ func (c *Conn) loadSession(hello *clientHelloMsg) ( hello.pskModes = []uint8{pskModeDHE} } + // [UTLS BEGINS] + if c.utls.session != nil { + return c.utls.session, c.utls.earlySecret, c.utls.binderKey, nil + } + // [UTLS ENDS] + // Session resumption is not allowed if renegotiating because // renegotiation is primarily used to allow a client to send a client // certificate, which would be skipped if session resumption occurred. @@ -434,6 +467,10 @@ func (c *Conn) loadSession(hello *clientHelloMsg) ( return nil, nil, nil, err } + c.utls.session = session // [uTLS] + c.utls.earlySecret = earlySecret // [uTLS] + c.utls.binderKey = binderKey // [uTLS] + return } @@ -1019,7 +1056,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_messages.go b/handshake_messages.go index 0c7ba94c..9520246f 100644 --- a/handshake_messages.go +++ b/handshake_messages.go @@ -319,7 +319,7 @@ func (m *clientHelloMsg) marshal() ([]byte, error) { } // marshalWithoutBinders returns the ClientHello through the -// FakePreSharedKeyExtension.identities field, according to RFC 8446, Section +// PreSharedKeyExtension.identities field, according to RFC 8446, Section // 4.2.11.2. Note that m.pskBinders must be set to slices of the correct length. func (m *clientHelloMsg) marshalWithoutBinders() ([]byte, error) { bindersLen := 2 // uint16 length prefix 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..2ef76517 100644 --- a/u_common.go +++ b/u_common.go @@ -247,13 +247,15 @@ func (chs *ClientHelloSpec) ReadTLSExtensions(b []byte, allowBluntMimicry bool) func (chs *ClientHelloSpec) AlwaysAddPadding() { alreadyHasPadding := false - for _, ext := range chs.Extensions { + for idx, ext := range chs.Extensions { if _, ok := ext.(*UtlsPaddingExtension); ok { alreadyHasPadding = true break } - if _, ok := ext.(*FakePreSharedKeyExtension); ok { - alreadyHasPadding = true // PSK must be last, so we don't need to add padding + if _, ok := ext.(PreSharedKeyExtension); ok { + alreadyHasPadding = true // PSK must be last, so we can't append padding after it + // instead we will insert padding before PSK + chs.Extensions = append(chs.Extensions[:idx], append([]TLSExtension{&UtlsPaddingExtension{GetPaddingLen: BoringPaddingStyle}}, chs.Extensions[idx:]...)...) break } } @@ -582,9 +584,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..2db59f84 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 []PreSharedKeyExtension 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 ...PreSharedKeyExtension) *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 { @@ -448,21 +456,20 @@ func (c *UConn) clientHandshake(ctx context.Context) (err error) { }() } - cacheKey := c.clientSessionCacheKey() - if c.config.ClientSessionCache != nil { - cs, ok := c.config.ClientSessionCache.Get(cacheKey) - if !sessionIsAlreadySet && ok { // uTLS: do not overwrite already set session - err = c.SetSessionState(cs) - if err != nil { - return - } - } - } - if _, err := c.writeHandshakeRecord(hello, nil); err != nil { return err } + if hello.earlyData { + suite := cipherSuiteTLS13ByID(session.cipherSuite) + transcript := suite.hash.New() + if err := transcriptMsg(hello, transcript); err != nil { + return err + } + earlyTrafficSecret := suite.deriveSecret(earlySecret, clientEarlyTrafficLabel, transcript) + c.quicSetWriteSecret(QUICEncryptionLevelEarly, suite.id, earlyTrafficSecret) + } + msg, err := c.readHandshake(nil) if err != nil { return err @@ -483,9 +490,11 @@ func (c *UConn) clientHandshake(ctx context.Context) (err error) { hs13 := c.HandshakeState.toPrivate13() hs13.serverHello = serverHello hs13.hello = hello + hs13.keySharesParams = NewKeySharesParameters() if !sessionIsAlreadySet { hs13.earlySecret = earlySecret hs13.binderKey = binderKey + hs13.session = session } hs13.ctx = ctx // In TLS 1.3, session tickets are delivered after the handshake. @@ -500,6 +509,7 @@ func (c *UConn) clientHandshake(ctx context.Context) (err error) { hs12.serverHello = serverHello hs12.hello = hello hs12.ctx = ctx + hs12.session = session err = hs12.handshake() if handshakeState := hs12.toPublic12(); handshakeState != nil { c.HandshakeState = *handshakeState @@ -507,17 +517,6 @@ func (c *UConn) clientHandshake(ctx context.Context) (err error) { if err != nil { return err } - - // If we had a successful handshake and hs.session is different from - // the one already cached - cache a new one. - if cacheKey != "" && hs12.session != nil && session != hs12.session { - hs12cs := &ClientSessionState{ - ticket: hs12.ticket, - session: hs12.session, - } - - c.config.ClientSessionCache.Put(cacheKey, hs12cs) - } return nil } @@ -590,7 +589,28 @@ func (uconn *UConn) MarshalClientHello() error { if len(uconn.Extensions) > 0 { binary.Write(bufferedWriter, binary.BigEndian, uint16(extensionsLen)) for _, ext := range uconn.Extensions { - bufferedWriter.ReadFrom(ext) + switch typedExt := ext.(type) { + case PreSharedKeyExtension: + // PSK extension is handled separately + err := bufferedWriter.Flush() + if err != nil { + return fmt.Errorf("bufferedWriter.Flush(): %w", err) + } + hello.Raw = helloBuffer.Bytes() + // prepare buffer + buf := make([]byte, typedExt.Len()) + n, err := typedExt.ReadWithRawHello(hello.Raw, buf) + if err != nil && !errors.Is(err, io.EOF) { + return fmt.Errorf("(*PreSharedKeyExtension).ReadWithRawHello(): %w", err) + } + if n != typedExt.Len() { + return errors.New("uconn: PreSharedKeyExtension: read wrong number of bytes") + } + bufferedWriter.Write(buf) + hello.PskBinders = typedExt.Binders() + default: + bufferedWriter.ReadFrom(ext) + } } } @@ -776,7 +796,13 @@ func (c *Conn) utlsConnectionStateLocked(state *ConnectionState) { } type utlsConnExtraFields struct { + // Application Settings (ALPS) hasApplicationSettings bool peerApplicationSettings []byte localApplicationSettings []byte + + // session resumption (PSK) + session *SessionState + earlySecret []byte + binderKey []byte } diff --git a/u_fingerprinter_test.go b/u_fingerprinter_test.go index ad140bf1..4a36dd93 100644 --- a/u_fingerprinter_test.go +++ b/u_fingerprinter_test.go @@ -373,6 +373,7 @@ func TestUTLSFingerprintClientHelloAlwaysAddPadding(t *testing.T) { } func TestUTLSFingerprintClientHelloKeepPSK(t *testing.T) { + t.Skipf("TestUTLSFingerprintClientHelloKeepPSK needs to be re-designed.") // TLSv1.3 Record Layer: Handshake Protocol: Client Hello // Content Type: Handshake (22) // Version: TLS 1.0 (0x0301) @@ -491,28 +492,28 @@ func TestUTLSFingerprintClientHelloKeepPSK(t *testing.T) { // Length: 267 // Pre-Shared Key extension - byteString := []byte("16030102400100023c03035cef5aa9122008e37f0f74d717cd4ae0f745daba4292e6fbca3cd5bf9123498f208c4aa23444084eeb70097efe0b8f6e3a56c717abd67505c950aab314de59bd8f00204a4a130113021303c02bc02fc02cc030cca9cca8c013c014009c009d002f0035010001d33a3a0000000000160014000011656467656170692e736c61636b2e636f6d00170000ff01000100000a000a0008dada001d00170018000b00020100002300000010000e000c02683208687474702f312e31000500050100000000000d0012001004030804040105030805050108060601001200000033002b0029dada000100001d0020e35e636d4e2dcd5f39309170285dab92dbe81fefe4926826cec1ef881321687e002d00020101002b000b0a2a2a0304030303020301001b00030200024a4a0001000029010b00e600e017fab59672c1966ae78fc4dacd7efb42e735de956e3f96d342bb8e63a5233ce21c92d6d75036601d74ccbc3ca0085f3ac2ebbd83da13501ac3c6d612bcb453fb206a39a8112d768bea1976d7c14e6de9aa0ee70ea732554d3c57d1a993f1044a46c1fb371811039ef30582cacf41bd497121d67793b8ee4df7a60d525f7df052fd66cda7f141bb553d9253816752d923ac7c71426179db4f26a7d42f0d65a2dd2dbaafb86fa17b2da23fd57c5064c76551cfda86304051231e4da9e697fedbcb5ae8cb2f6cb92f71164acf2edff5bccc1266cd648a53cc46262eabf40727bcb6958a3d1300212083e99d791672d39919dcb387f2fa7aeee938ec32ecf4b861306f7df4f9a8a746") - - helloBytes := make([]byte, hex.DecodedLen(len(byteString))) - _, err := hex.Decode(helloBytes, byteString) - if err != nil { - t.Errorf("got error: %v; expected to succeed", err) - return - } - - f := &Fingerprinter{} - generatedSpec, err := f.FingerprintClientHello(helloBytes) - if err != nil { - t.Errorf("got error: %v; expected to succeed", err) - return - } - - for _, ext := range generatedSpec.Extensions { - if _, ok := (ext).(*FakePreSharedKeyExtension); ok { - return - } - } - t.Errorf("generated ClientHelloSpec with KeepPSK does not include preshared key extension") + // byteString := []byte("16030102400100023c03035cef5aa9122008e37f0f74d717cd4ae0f745daba4292e6fbca3cd5bf9123498f208c4aa23444084eeb70097efe0b8f6e3a56c717abd67505c950aab314de59bd8f00204a4a130113021303c02bc02fc02cc030cca9cca8c013c014009c009d002f0035010001d33a3a0000000000160014000011656467656170692e736c61636b2e636f6d00170000ff01000100000a000a0008dada001d00170018000b00020100002300000010000e000c02683208687474702f312e31000500050100000000000d0012001004030804040105030805050108060601001200000033002b0029dada000100001d0020e35e636d4e2dcd5f39309170285dab92dbe81fefe4926826cec1ef881321687e002d00020101002b000b0a2a2a0304030303020301001b00030200024a4a0001000029010b00e600e017fab59672c1966ae78fc4dacd7efb42e735de956e3f96d342bb8e63a5233ce21c92d6d75036601d74ccbc3ca0085f3ac2ebbd83da13501ac3c6d612bcb453fb206a39a8112d768bea1976d7c14e6de9aa0ee70ea732554d3c57d1a993f1044a46c1fb371811039ef30582cacf41bd497121d67793b8ee4df7a60d525f7df052fd66cda7f141bb553d9253816752d923ac7c71426179db4f26a7d42f0d65a2dd2dbaafb86fa17b2da23fd57c5064c76551cfda86304051231e4da9e697fedbcb5ae8cb2f6cb92f71164acf2edff5bccc1266cd648a53cc46262eabf40727bcb6958a3d1300212083e99d791672d39919dcb387f2fa7aeee938ec32ecf4b861306f7df4f9a8a746") + + // helloBytes := make([]byte, hex.DecodedLen(len(byteString))) + // _, err := hex.Decode(helloBytes, byteString) + // if err != nil { + // t.Errorf("got error: %v; expected to succeed", err) + // return + // } + + // f := &Fingerprinter{} + // generatedSpec, err := f.FingerprintClientHello(helloBytes) + // if err != nil { + // t.Errorf("got error: %v; expected to succeed", err) + // return + // } + + // for _, ext := range generatedSpec.Extensions { + // if _, ok := (ext).(*PreSharedKeyExtension); ok { + // return + // } + // } + // t.Errorf("generated ClientHelloSpec with KeepPSK does not include preshared key extension") } func TestUTLSHandshakeClientFingerprintedSpecFromChrome_58(t *testing.T) { 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..75d4a49c 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 ...PreSharedKeyExtension) (ClientHelloSpec, error) { + if len(pskExtension) > 1 { + return ClientHelloSpec{}, errors.New("tls: at most one PreSharedKeyExtensions 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 ...PreSharedKeyExtension) (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, PreSharedKeyExtension: + 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 @@ -2095,7 +2453,7 @@ func (uconn *UConn) ApplyPreset(p *ClientHelloSpec) error { if cs != nil { session = cs.session } - // TODO: use uconn.loadSession(hello.getPrivateObj()) to support TLS 1.3 PSK-style resumption + // TLS 1.3 (PSK) resumption is handled by PreSharedKeyExtension in MarshalClientHello() } err := uconn.SetSessionState(cs) if err != nil { @@ -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_pre_shared_key.go b/u_pre_shared_key.go new file mode 100644 index 00000000..032c8bb1 --- /dev/null +++ b/u_pre_shared_key.go @@ -0,0 +1,503 @@ +package tls + +import ( + "encoding/json" + "errors" + "io" + + "golang.org/x/crypto/cryptobyte" +) + +type PreSharedKeyExtension interface { + TLSExtension + + // ReadWithRawHello is used to read the extension from the ClientHello + // instead of Read(), where the latter is used to read all other extensions. + // + // This is needed because the PSK extension needs to calculate the binder + // based on all previous parts of the ClientHello. + ReadWithRawHello(raw, b []byte) (int, error) + + // Binders returns the binders that were computed during the handshake. + // + // FakePreSharedKeyExtension will return nil to make sure utls DOES NOT + // actually do any session resumption. + Binders() [][]byte + + mustEmbedUnimplementedPreSharedKeyExtension() // this works like a type guard +} + +type UnimplementedPreSharedKeyExtension struct{} + +func (UnimplementedPreSharedKeyExtension) mustEmbedUnimplementedPreSharedKeyExtension() {} + +func (*UnimplementedPreSharedKeyExtension) writeToUConn(*UConn) error { + return errors.New("tls: writeToUConn is not implemented for the PreSharedKeyExtension") +} + +func (*UnimplementedPreSharedKeyExtension) Len() int { + panic("tls: Len is not implemented for the PreSharedKeyExtension") +} + +func (*UnimplementedPreSharedKeyExtension) Read([]byte) (int, error) { + return 0, errors.New("tls: Read is not implemented for the PreSharedKeyExtension") +} + +func (*UnimplementedPreSharedKeyExtension) ReadWithRawHello(raw, b []byte) (int, error) { + return 0, errors.New("tls: ReadWithRawHello is not implemented for the PreSharedKeyExtension") +} + +func (*UnimplementedPreSharedKeyExtension) Binders() [][]byte { + panic("tls: Binders is not implemented for the PreSharedKeyExtension") +} + +// UtlsPreSharedKeyExtension is an extension used to set the PSK extension in the +// ClientHello. +type UtlsPreSharedKeyExtension struct { + UnimplementedPreSharedKeyExtension + + SessionCacheOverride ClientSessionCache + + identities []pskIdentity + binders [][]byte + binderKey []byte // this will be used to compute the binder when hello message is ready + cipherSuite *cipherSuiteTLS13 + earlySecret []byte +} + +func (e *UtlsPreSharedKeyExtension) writeToUConn(uc *UConn) error { + err := e.preloadSession(uc) + if err != nil { + return err + } + + uc.HandshakeState.Hello.PskIdentities = pskIdentities(e.identities).ToPublic() + // uc.HandshakeState.Hello.PskBinders = e.binders + // uc.HandshakeState.Hello = hello.getPublicPtr() // write back to public hello + // uc.HandshakeState.State13.EarlySecret = e.earlySecret + // uc.HandshakeState.State13.BinderKey = e.binderKey + + return nil +} + +func (e *UtlsPreSharedKeyExtension) Len() int { + length := 4 // extension type + extension length + length += 2 // identities length + for _, identity := range e.identities { + length += 2 + len(identity.label) + 4 // identity length + identity + obfuscated ticket age + } + length += 2 // binders length + for _, binder := range e.binders { + length += len(binder) + 1 // binder length + binder + } + return length +} + +func (e *UtlsPreSharedKeyExtension) Read(b []byte) (int, error) { + return 0, errors.New("tls: PreSharedKeyExtension shouldn't be read, use ReadWithRawHello() instead") +} + +func (e *UtlsPreSharedKeyExtension) ReadWithRawHello(raw, b []byte) (int, error) { + if len(b) < e.Len() { + return 0, io.ErrShortBuffer + } + + b[0] = byte(extensionPreSharedKey >> 8) + b[1] = byte(extensionPreSharedKey) + b[2] = byte((e.Len() - 4) >> 8) + b[3] = byte(e.Len() - 4) + + // identities length + identitiesLength := 0 + for _, identity := range e.identities { + identitiesLength += 2 + len(identity.label) + 4 // identity length + identity + obfuscated ticket age + } + b[4] = byte(identitiesLength >> 8) + b[5] = byte(identitiesLength) + + // identities + offset := 6 + for _, identity := range e.identities { + b[offset] = byte(len(identity.label) >> 8) + b[offset+1] = byte(len(identity.label)) + offset += 2 + copy(b[offset:], identity.label) + offset += len(identity.label) + b[offset] = byte(identity.obfuscatedTicketAge >> 24) + b[offset+1] = byte(identity.obfuscatedTicketAge >> 16) + b[offset+2] = byte(identity.obfuscatedTicketAge >> 8) + b[offset+3] = byte(identity.obfuscatedTicketAge) + offset += 4 + } + + // concatenate ClientHello and PreSharedKeyExtension + rawHelloSoFar := append(raw, b[:offset]...) + transcript := e.cipherSuite.hash.New() + transcript.Write(rawHelloSoFar) + e.binders = [][]byte{e.cipherSuite.finishedHash(e.binderKey, transcript)} + + // binders length + bindersLength := 0 + for _, binder := range e.binders { + bindersLength += len(binder) + 1 // binder length + binder + } + b[offset] = byte(bindersLength >> 8) + b[offset+1] = byte(bindersLength) + offset += 2 + + // binders + for _, binder := range e.binders { + b[offset] = byte(len(binder)) + offset++ + copy(b[offset:], binder) + offset += len(binder) + } + + return e.Len(), io.EOF +} + +func (e *UtlsPreSharedKeyExtension) preloadSession(uc *UConn) error { + // var sessionCache ClientSessionCache + // must set either e.Session or uc.config.ClientSessionCache + if e.SessionCacheOverride != nil { + uc.config.ClientSessionCache = e.SessionCacheOverride + } + + // load Hello + hello := uc.HandshakeState.Hello.getPrivatePtr() + // try to use loadSession() + session, earlySecret, binderKey, err := uc.loadSession(hello) + if err != nil { + return err + } + if session != nil && session.version == VersionTLS13 && binderKey != nil { + e.identities = hello.pskIdentities + e.binders = hello.pskBinders + e.binderKey = binderKey + e.cipherSuite = cipherSuiteTLS13ByID(session.cipherSuite) + e.earlySecret = earlySecret + return nil + } else { + return errors.New("tls: session not compatible with TLS 1.3, PSK not possible") + } +} + +// Binders must be called after ReadWithRawHello +func (e *UtlsPreSharedKeyExtension) Binders() [][]byte { + return e.binders +} + +// FakePreSharedKeyExtension is an extension used to send the PSK extension in the +// ClientHello. +// +// However, it DOES NOT do any session resumption AND should not be used with a +// real/valid PSK Identity. +// +// TODO: Only one of FakePreSharedKeyExtension and HardcodedPreSharedKeyExtension should +// be kept, the other one should be just removed. We still need to learn more of the safety +// of hardcoding both Identities and Binders without recalculating the latter. +type FakePreSharedKeyExtension struct { + UnimplementedPreSharedKeyExtension + + CipherSuite uint16 `json:"cipher_suite"` // this is used to compute the binder + SessionSecret []byte `json:"session_secret"` // this is used to compute the binder + + Identities []PskIdentity `json:"identities"` + binders [][]byte +} + +func (e *FakePreSharedKeyExtension) writeToUConn(uc *UConn) error { + return nil // do nothing for this fake extension +} + +func (e *FakePreSharedKeyExtension) Len() int { + length := 4 // extension type + extension length + length += 2 // identities length + for _, identity := range e.Identities { + length += 2 + len(identity.Label) + 4 // identity length + identity + obfuscated ticket age + } + + cipherSuite := cipherSuiteTLS13ByID(e.CipherSuite) + if cipherSuite == nil { + panic("tls: cipher suite not supported by the PreSharedKeyExtension") + } + singleBinderSize := cipherSuite.hash.Size() + + length += 2 // binders length + for range e.Identities { // binders should be as long as the identities + length += singleBinderSize + 1 + } + return length +} + +func (e *FakePreSharedKeyExtension) Read(b []byte) (int, error) { + return 0, errors.New("tls: PreSharedKeyExtension shouldn't be read, use ReadWithRawHello() instead") +} + +func (e *FakePreSharedKeyExtension) ReadWithRawHello(raw, b []byte) (int, error) { + if len(b) < e.Len() { + return 0, io.ErrShortBuffer + } + + b[0] = byte(extensionPreSharedKey >> 8) + b[1] = byte(extensionPreSharedKey) + b[2] = byte((e.Len() - 4) >> 8) + b[3] = byte(e.Len() - 4) + + // identities length + identitiesLength := 0 + for _, identity := range e.Identities { + identitiesLength += 2 + len(identity.Label) + 4 // identity length + identity + obfuscated ticket age + } + b[4] = byte(identitiesLength >> 8) + b[5] = byte(identitiesLength) + + // identities + offset := 6 + for _, identity := range e.Identities { + b[offset] = byte(len(identity.Label) >> 8) + b[offset+1] = byte(len(identity.Label)) + offset += 2 + copy(b[offset:], identity.Label) + offset += len(identity.Label) + b[offset] = byte(identity.ObfuscatedTicketAge >> 24) + b[offset+1] = byte(identity.ObfuscatedTicketAge >> 16) + b[offset+2] = byte(identity.ObfuscatedTicketAge >> 8) + b[offset+3] = byte(identity.ObfuscatedTicketAge) + offset += 4 + } + + cipherSuite := cipherSuiteTLS13ByID(e.CipherSuite) + if cipherSuite == nil { + return 0, errors.New("tls: cipher suite not supported") + } + earlySecret := cipherSuite.extract(e.SessionSecret, nil) + binderKey := cipherSuite.deriveSecret(earlySecret, resumptionBinderLabel, nil) + + // concatenate ClientHello and PreSharedKeyExtension + rawHelloSoFar := append(raw, b[:offset]...) + transcript := cipherSuite.hash.New() + transcript.Write(rawHelloSoFar) + e.binders = [][]byte{cipherSuite.finishedHash(binderKey, transcript)} + + // binders length + bindersLength := 0 + for _, binder := range e.binders { + bindersLength += len(binder) + 1 // binder length + binder + } + b[offset] = byte(bindersLength >> 8) + b[offset+1] = byte(bindersLength) + offset += 2 + + // binders + for _, binder := range e.binders { + b[offset] = byte(len(binder)) + offset++ + copy(b[offset:], binder) + offset += len(binder) + } + + return e.Len(), io.EOF +} + +func (e *FakePreSharedKeyExtension) Binders() [][]byte { + return nil +} + +func (e *FakePreSharedKeyExtension) UnmarshalJSON(data []byte) error { + var pskAccepter struct { + CipherSuite uint16 `json:"cipher_suite"` + SessionSecret []byte `json:"session_secret"` + Identities []PskIdentity `json:"identities"` + } + + if err := json.Unmarshal(data, &pskAccepter); err != nil { + return err + } + + e.CipherSuite = pskAccepter.CipherSuite + e.SessionSecret = pskAccepter.SessionSecret + e.Identities = pskAccepter.Identities + return nil +} + +// HardcodedPreSharedKeyExtension is an extension used to set the PSK extension in the +// ClientHello. +// +// It does not compute binders based on ClientHello, but uses the binders specified instead. +// +// TODO: Only one of FakePreSharedKeyExtension and HardcodedPreSharedKeyExtension should +// be kept, the other one should be just removed. We still need to learn more of the safety +// of hardcoding both Identities and Binders without recalculating the latter. +type HardcodedPreSharedKeyExtension struct { + Identities []PskIdentity `json:"identities"` + Binders [][]byte `json:"binders"` +} + +func (e *HardcodedPreSharedKeyExtension) writeToUConn(uc *UConn) error { + if uc.config.ClientSessionCache == nil { + return nil // don't write the extension if there is no session cache + } + if session, ok := uc.config.ClientSessionCache.Get(uc.clientSessionCacheKey()); !ok || session == nil { + return nil // don't write the extension if there is no session cache available for this session + } + uc.HandshakeState.Hello.PskIdentities = e.Identities + uc.HandshakeState.Hello.PskBinders = e.Binders + return nil +} + +func (e *HardcodedPreSharedKeyExtension) Len() int { + length := 4 // extension type + extension length + length += 2 // identities length + for _, identity := range e.Identities { + length += 2 + len(identity.Label) + 4 // identity length + identity + obfuscated ticket age + } + length += 2 // binders length + for _, binder := range e.Binders { + length += len(binder) + } + return length +} + +func (e *HardcodedPreSharedKeyExtension) Read(b []byte) (int, error) { + return 0, errors.New("tls: PreSharedKeyExtension shouldn't be read, use ReadWithRawHello() instead") +} + +func (e *HardcodedPreSharedKeyExtension) ReadWithRawHello(raw, b []byte) (int, error) { + if len(b) < e.Len() { + return 0, io.ErrShortBuffer + } + + b[0] = byte(extensionPreSharedKey >> 8) + b[1] = byte(extensionPreSharedKey) + b[2] = byte((e.Len() - 4) >> 8) + b[3] = byte(e.Len() - 4) + + // identities length + identitiesLength := 0 + for _, identity := range e.Identities { + identitiesLength += 2 + len(identity.Label) + 4 // identity length + identity + obfuscated ticket age + } + b[4] = byte(identitiesLength >> 8) + b[5] = byte(identitiesLength) + + // identities + offset := 6 + for _, identity := range e.Identities { + b[offset] = byte(len(identity.Label) >> 8) + b[offset+1] = byte(len(identity.Label)) + offset += 2 + copy(b[offset:], identity.Label) + offset += len(identity.Label) + b[offset] = byte(identity.ObfuscatedTicketAge >> 24) + b[offset+1] = byte(identity.ObfuscatedTicketAge >> 16) + b[offset+2] = byte(identity.ObfuscatedTicketAge >> 8) + b[offset+3] = byte(identity.ObfuscatedTicketAge) + offset += 4 + } + + // binders length + bindersLength := 0 + for _, binder := range e.Binders { + bindersLength += len(binder) + 1 + } + b[offset] = byte(bindersLength >> 8) + b[offset+1] = byte(bindersLength) + offset += 2 + + // binders + for _, binder := range e.Binders { + b[offset] = byte(len(binder)) + offset++ + copy(b[offset:], binder) + offset += len(binder) + } + + return e.Len(), io.EOF +} + +func (e *HardcodedPreSharedKeyExtension) Write(b []byte) (n int, err error) { + fullLen := len(b) + s := cryptobyte.String(b) + + var identitiesLength uint16 + if !s.ReadUint16(&identitiesLength) { + return 0, errors.New("tls: invalid PSK extension") + } + + // identities + for identitiesLength > 0 { + var identityLength uint16 + if !s.ReadUint16(&identityLength) { + return 0, errors.New("tls: invalid PSK extension") + } + identitiesLength -= 2 + + if identityLength > identitiesLength { + return 0, errors.New("tls: invalid PSK extension") + } + + var identity []byte + if !s.ReadBytes(&identity, int(identityLength)) { + return 0, errors.New("tls: invalid PSK extension") + } + + identitiesLength -= identityLength // identity + + var obfuscatedTicketAge uint32 + if !s.ReadUint32(&obfuscatedTicketAge) { + return 0, errors.New("tls: invalid PSK extension") + } + + e.Identities = append(e.Identities, PskIdentity{ + Label: identity, + ObfuscatedTicketAge: obfuscatedTicketAge, + }) + + identitiesLength -= 4 // obfuscated ticket age + } + + var bindersLength uint16 + if !s.ReadUint16(&bindersLength) { + return 0, errors.New("tls: invalid PSK extension") + } + + // binders + for bindersLength > 0 { + var binderLength uint8 + if !s.ReadUint8(&binderLength) { + return 0, errors.New("tls: invalid PSK extension") + } + bindersLength -= 1 + + if uint16(binderLength) > bindersLength { + return 0, errors.New("tls: invalid PSK extension") + } + + var binder []byte + if !s.ReadBytes(&binder, int(binderLength)) { + return 0, errors.New("tls: invalid PSK extension") + } + + e.Binders = append(e.Binders, binder) + + bindersLength -= uint16(binderLength) + } + + return fullLen, nil +} + +func (e *HardcodedPreSharedKeyExtension) UnmarshalJSON(data []byte) error { + var pskAccepter struct { + PskIdentities []PskIdentity `json:"identities"` + PskBinders [][]byte `json:"binders"` + } + + if err := json.Unmarshal(data, &pskAccepter); err != nil { + return err + } + + e.Identities = pskAccepter.PskIdentities + e.Binders = pskAccepter.PskBinders + return nil +} 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, + } + } +} diff --git a/u_tls_extensions.go b/u_tls_extensions.go index 068332c0..52294e70 100644 --- a/u_tls_extensions.go +++ b/u_tls_extensions.go @@ -47,8 +47,8 @@ func ExtensionFromID(id uint16) TLSExtension { return &FakeDelegatedCredentialsExtension{} case extensionSessionTicket: return &SessionTicketExtension{} - case extensionPreSharedKey: - return &FakePreSharedKeyExtension{} + // case extensionPreSharedKey: + // return &HardcodedPreSharedKeyExtension{} // TODO: redesign how to create proper PSK from ID // case extensionEarlyData: // return &EarlyDataExtension{} case extensionSupportedVersions: @@ -1893,175 +1893,3 @@ func (e *FakeDelegatedCredentialsExtension) UnmarshalJSON(data []byte) error { } return nil } - -// FakePreSharedKeyExtension is an extension used to set the PSK extension in the -// ClientHello. -// -// Unfortunately, even when the PSK extension is set, there will be no PSK-based -// resumption since crypto/tls does not implement PSK. -type FakePreSharedKeyExtension struct { - PskIdentities []PskIdentity `json:"identities"` - PskBinders [][]byte `json:"binders"` -} - -func (e *FakePreSharedKeyExtension) writeToUConn(uc *UConn) error { - if uc.config.ClientSessionCache == nil { - return nil // don't write the extension if there is no session cache - } - if session, ok := uc.config.ClientSessionCache.Get(uc.clientSessionCacheKey()); !ok || session == nil { - return nil // don't write the extension if there is no session cache available for this session - } - uc.HandshakeState.Hello.PskIdentities = e.PskIdentities - uc.HandshakeState.Hello.PskBinders = e.PskBinders - return nil -} - -func (e *FakePreSharedKeyExtension) Len() int { - length := 4 // extension type + extension length - length += 2 // identities length - for _, identity := range e.PskIdentities { - length += 2 + len(identity.Label) + 4 // identity length + identity + obfuscated ticket age - } - length += 2 // binders length - for _, binder := range e.PskBinders { - length += len(binder) - } - return length -} - -func (e *FakePreSharedKeyExtension) Read(b []byte) (int, error) { - if len(b) < e.Len() { - return 0, io.ErrShortBuffer - } - - b[0] = byte(extensionPreSharedKey >> 8) - b[1] = byte(extensionPreSharedKey) - b[2] = byte((e.Len() - 4) >> 8) - b[3] = byte(e.Len() - 4) - - // identities length - identitiesLength := 0 - for _, identity := range e.PskIdentities { - identitiesLength += 2 + len(identity.Label) + 4 // identity length + identity + obfuscated ticket age - } - b[4] = byte(identitiesLength >> 8) - b[5] = byte(identitiesLength) - - // identities - offset := 6 - for _, identity := range e.PskIdentities { - b[offset] = byte(len(identity.Label) >> 8) - b[offset+1] = byte(len(identity.Label)) - offset += 2 - copy(b[offset:], identity.Label) - offset += len(identity.Label) - b[offset] = byte(identity.ObfuscatedTicketAge >> 24) - b[offset+1] = byte(identity.ObfuscatedTicketAge >> 16) - b[offset+2] = byte(identity.ObfuscatedTicketAge >> 8) - b[offset+3] = byte(identity.ObfuscatedTicketAge) - offset += 4 - } - - // binders length - bindersLength := 0 - for _, binder := range e.PskBinders { - bindersLength += len(binder) - } - b[offset] = byte(bindersLength >> 8) - b[offset+1] = byte(bindersLength) - offset += 2 - - // binders - for _, binder := range e.PskBinders { - copy(b[offset:], binder) - offset += len(binder) - } - - return e.Len(), io.EOF -} - -func (e *FakePreSharedKeyExtension) Write(b []byte) (n int, err error) { - fullLen := len(b) - s := cryptobyte.String(b) - - var identitiesLength uint16 - if !s.ReadUint16(&identitiesLength) { - return 0, errors.New("tls: invalid PSK extension") - } - - // identities - for identitiesLength > 0 { - var identityLength uint16 - if !s.ReadUint16(&identityLength) { - return 0, errors.New("tls: invalid PSK extension") - } - identitiesLength -= 2 - - if identityLength > identitiesLength { - return 0, errors.New("tls: invalid PSK extension") - } - - var identity []byte - if !s.ReadBytes(&identity, int(identityLength)) { - return 0, errors.New("tls: invalid PSK extension") - } - - identitiesLength -= identityLength // identity - - var obfuscatedTicketAge uint32 - if !s.ReadUint32(&obfuscatedTicketAge) { - return 0, errors.New("tls: invalid PSK extension") - } - - e.PskIdentities = append(e.PskIdentities, PskIdentity{ - Label: identity, - ObfuscatedTicketAge: obfuscatedTicketAge, - }) - - identitiesLength -= 4 // obfuscated ticket age - } - - var bindersLength uint16 - if !s.ReadUint16(&bindersLength) { - return 0, errors.New("tls: invalid PSK extension") - } - - // binders - for bindersLength > 0 { - var binderLength uint8 - if !s.ReadUint8(&binderLength) { - return 0, errors.New("tls: invalid PSK extension") - } - bindersLength -= 1 - - if uint16(binderLength) > bindersLength { - return 0, errors.New("tls: invalid PSK extension") - } - - var binder []byte - if !s.ReadBytes(&binder, int(binderLength)) { - return 0, errors.New("tls: invalid PSK extension") - } - - e.PskBinders = append(e.PskBinders, binder) - - bindersLength -= uint16(binderLength) - } - - return fullLen, nil -} - -func (e *FakePreSharedKeyExtension) UnmarshalJSON(data []byte) error { - var pskAccepter struct { - PskIdentities []PskIdentity `json:"identities"` - PskBinders [][]byte `json:"binders"` - } - - if err := json.Unmarshal(data, &pskAccepter); err != nil { - return err - } - - e.PskIdentities = pskAccepter.PskIdentities - e.PskBinders = pskAccepter.PskBinders - return nil -}