diff --git a/alert.go b/alert.go index 33022cd2..aba46055 100644 --- a/alert.go +++ b/alert.go @@ -58,6 +58,7 @@ const ( alertUnknownPSKIdentity alert = 115 alertCertificateRequired alert = 116 alertNoApplicationProtocol alert = 120 + alertECHRequired alert = 121 ) var alertText = map[alert]string{ @@ -94,6 +95,7 @@ var alertText = map[alert]string{ alertUnknownPSKIdentity: "unknown PSK identity", alertCertificateRequired: "certificate required", alertNoApplicationProtocol: "no application protocol", + alertECHRequired: "ECH required", } func (e alert) String() string { diff --git a/common.go b/common.go index f61a22ef..7f0cc87d 100644 --- a/common.go +++ b/common.go @@ -306,6 +306,11 @@ type ConnectionState struct { // ekm is a closure exposed via ExportKeyingMaterial. ekm func(label string, context []byte, length int) ([]byte, error) + + // ECHRetryConfigs contains the ECH retry configurations sent by the server in + // EncryptedExtensions message. It is only populated if the server sent the + // ech extension in EncryptedExtensions message. + ECHRetryConfigs []ECHConfig // [uTLS] } // ExportKeyingMaterial returns length bytes of exported key material in a new @@ -836,6 +841,17 @@ type Config struct { // autoSessionTicketKeys is like sessionTicketKeys but is owned by the // auto-rotation logic. See Config.ticketKeys. autoSessionTicketKeys []ticketKey + + // ECHConfigs contains the ECH configurations to be used by the ECH + // extension if any. + // It could either be distributed by the server in EncryptedExtensions + // message or out-of-band. + // + // If ECHConfigs is nil and an ECH extension is present, GREASEd ECH + // extension will be sent. + // + // If GREASE ECH extension is present, this field will be ignored. + ECHConfigs []ECHConfig // [uTLS] } const ( @@ -921,6 +937,7 @@ func (c *Config) Clone() *Config { autoSessionTicketKeys: c.autoSessionTicketKeys, PreferSkipResumptionOnNilExtension: c.PreferSkipResumptionOnNilExtension, // [UTLS] + ECHConfigs: c.ECHConfigs, // [uTLS] } } diff --git a/dicttls/hpke_aead_identifiers.go b/dicttls/hpke_aead_identifiers.go new file mode 100644 index 00000000..c4f82314 --- /dev/null +++ b/dicttls/hpke_aead_identifiers.go @@ -0,0 +1,19 @@ +package dicttls + +// source: https://www.iana.org/assignments/hpke/hpke.xhtml +// last updated: December 2023 + +const ( + AEAD_AES_128_GCM uint16 = 0x0001 // NIST Special Publication 800-38D + AEAD_AES_256_GCM uint16 = 0x0002 // NIST Special Publication 800-38D + AEAD_CHACHA20_POLY1305 uint16 = 0x0003 // RFC 8439 + AEAD_EXPORT_ONLY uint16 = 0xFFFF // RFC 9180 +) + +var DictAEADIdentifierValueIndexed = map[uint16]string{ + 0x0000: "Reserved", // RFC 9180 + 0x0001: "AES-128-GCM", + 0x0002: "AES-256-GCM", + 0x0003: "ChaCha20Poly1305", + 0xFFFF: "Export-only", // RFC 9180 +} diff --git a/dicttls/kdf_identifiers.go b/dicttls/hpke_kdf_identifiers.go similarity index 55% rename from dicttls/kdf_identifiers.go rename to dicttls/hpke_kdf_identifiers.go index d7e07cdf..7471943f 100644 --- a/dicttls/kdf_identifiers.go +++ b/dicttls/hpke_kdf_identifiers.go @@ -1,19 +1,24 @@ package dicttls -// source: https://www.iana.org/assignments/tls-parameters/tls-kdf-ids.csv -// last updated: March 2023 +// source: https://www.iana.org/assignments/hpke/hpke.xhtml +// last updated: December 2023 const ( HKDF_SHA256 uint16 = 0x0001 HKDF_SHA384 uint16 = 0x0002 + HKDF_SHA512 uint16 = 0x0003 ) var DictKDFIdentifierValueIndexed = map[uint16]string{ + 0x0000: "Reserved", // RFC 9180 0x0001: "HKDF_SHA256", 0x0002: "HKDF_SHA384", + 0x0003: "HKDF_SHA512", } var DictKDFIdentifierNameIndexed = map[string]uint16{ + "Reserved": 0x0000, // RFC 9180 "HKDF_SHA256": 0x0001, "HKDF_SHA384": 0x0002, + "HKDF_SHA512": 0x0003, } diff --git a/dicttls/hpke_kem_identifiers.go b/dicttls/hpke_kem_identifiers.go new file mode 100644 index 00000000..213e7f8c --- /dev/null +++ b/dicttls/hpke_kem_identifiers.go @@ -0,0 +1,53 @@ +package dicttls + +// source: https://www.iana.org/assignments/hpke/hpke.xhtml +// last updated: December 2023 + +const ( + DHKEM_P256_HKDF_SHA256 uint16 = 0x0010 // RFC 5869 + DHKEM_P384_HKDF_SHA384 uint16 = 0x0011 // RFC 5869 + DHKEM_P521_HKDF_SHA512 uint16 = 0x0012 // RFC 5869 + DHKEM_CP256_HKDF_SHA256 uint16 = 0x0013 // RFC 6090 + DHKEM_CP384_HKDF_SHA384 uint16 = 0x0014 // RFC 6090 + DHKEM_CP521_HKDF_SHA512 uint16 = 0x0015 // RFC 6090 + DHKEM_SECP256K1_HKDF_SHA256 uint16 = 0x0016 // draft-wahby-cfrg-hpke-kem-secp256k1-01 + + DHKEM_X25519_HKDF_SHA256 uint16 = 0x0020 // RFC 7748 + DHKEM_X448_HKDF_SHA512 uint16 = 0x0021 // RFC 7748 + + X25519_KYBER768_DRAFT00 uint16 = 0x0030 // draft-westerbaan-cfrg-hpke-xyber768d00-02 +) + +var DictKEMIdentifierValueIndexed = map[uint16]string{ + 0x0000: "Reserved", // RFC 9180 + + 0x0010: "DHKEM(P-256, HKDF-SHA256)", + 0x0011: "DHKEM(P-384, HKDF-SHA384)", + 0x0012: "DHKEM(P-521, HKDF-SHA512)", + 0x0013: "DHKEM(CP-256, HKDF-SHA256)", + 0x0014: "DHKEM(CP-384, HKDF-SHA384)", + 0x0015: "DHKEM(CP-521, HKDF-SHA512)", + 0x0016: "DHKEM(secp256k1, HKDF-SHA256)", + + 0x0020: "DHKEM(X25519, HKDF-SHA256)", + 0x0021: "DHKEM(X448, HKDF-SHA512)", + + 0x0030: "X25519Kyber768Draft00", +} + +var DictKEMIdentifierNameIndexed = map[string]uint16{ + "Reserved": 0x0000, // RFC 9180 + + "DHKEM(P-256, HKDF-SHA256)": 0x0010, + "DHKEM(P-384, HKDF-SHA384)": 0x0011, + "DHKEM(P-521, HKDF-SHA512)": 0x0012, + "DHKEM(CP-256, HKDF-SHA256)": 0x0013, + "DHKEM(CP-384, HKDF-SHA384)": 0x0014, + "DHKEM(CP-521, HKDF-SHA512)": 0x0015, + "DHKEM(secp256k1, HKDF-SHA256)": 0x0016, + + "DHKEM(X25519, HKDF-SHA256)": 0x0020, + "DHKEM(X448, HKDF-SHA512)": 0x0021, + + "X25519Kyber768Draft00": 0x0030, +} diff --git a/dicttls/kem_identifiers.go b/dicttls/kem_identifiers.go deleted file mode 100644 index 02c7d226..00000000 --- a/dicttls/kem_identifiers.go +++ /dev/null @@ -1,35 +0,0 @@ -package dicttls - -// source: https://www.rfc-editor.org/rfc/rfc9180 -// last updated: December 2023 - -const ( - DHKEM_P256_HKDF_SHA256 uint16 = 0x0010 // RFC 5869 - DHKEM_P384_HKDF_SHA384 uint16 = 0x0011 // RFC 5869 - DHKEM_P521_HKDF_SHA512 uint16 = 0x0012 // RFC 5869 - - DHKEM_X25519_HKDF_SHA256 uint16 = 0x0020 // RFC 7748 - DHKEM_X448_HKDF_SHA512 uint16 = 0x0021 // RFC 7748 -) - -var DictKEMIdentifierValueIndexed = map[uint16]string{ - 0x0000: "Reserved", // RFC 9180 - - 0x0010: "DHKEM_P256_HKDF_SHA256", - 0x0011: "DHKEM_P384_HKDF_SHA384", - 0x0012: "DHKEM_P521_HKDF_SHA512", - - 0x0020: "DHKEM_X25519_HKDF_SHA256", - 0x0021: "DHKEM_X448_HKDF_SHA512", -} - -var DictKEMIdentifierNameIndexed = map[string]uint16{ - "Reserved": 0x0000, // RFC 9180 - - "DHKEM_P256_HKDF_SHA256": 0x0010, - "DHKEM_P384_HKDF_SHA384": 0x0011, - "DHKEM_P521_HKDF_SHA512": 0x0012, - - "DHKEM_X25519_HKDF_SHA256": 0x0020, - "DHKEM_X448_HKDF_SHA512": 0x0021, -} diff --git a/examples/ech/main.go b/examples/ech/main.go new file mode 100644 index 00000000..3b9e2054 --- /dev/null +++ b/examples/ech/main.go @@ -0,0 +1,127 @@ +package main + +import ( + "bufio" + "errors" + "fmt" + "io" + "log" + "net" + "net/http" + "net/url" + "os" + "time" + + tls "github.com/refraction-networking/utls" + "golang.org/x/net/http2" +) + +var ( + dialTimeout = time.Duration(15) * time.Second +) + +// var requestHostname = "crypto.cloudflare.com" // speaks http2 and TLS 1.3 and ECH and PQ +// var requestAddr = "crypto.cloudflare.com:443" +// var requestPath = "/cdn-cgi/trace" + +// var requestHostname = "tls-ech.dev" // speaks http2 and TLS 1.3 and ECH and PQ +// var requestAddr = "tls-ech.dev:443" +// var requestPath = "/" + +var requestHostname = "defo.ie" // speaks http2 and TLS 1.3 and ECH and PQ +var requestAddr = "defo.ie:443" +var requestPath = "/ech-check.php" + +// var requestHostname = "client.tlsfingerprint.io" // speaks http2 and TLS 1.3 and ECH and PQ +// var requestAddr = "client.tlsfingerprint.io:443" +// var requestPath = "/" + +func HttpGetCustom(hostname string, addr string) (*http.Response, error) { + klw, err := os.OpenFile("./sslkeylogging.log", os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0600) + if err != nil { + return nil, fmt.Errorf("os.OpenFile error: %+v", err) + } + config := tls.Config{ + ServerName: hostname, + KeyLogWriter: klw, + } + dialConn, err := net.DialTimeout("tcp", addr, dialTimeout) + if err != nil { + return nil, fmt.Errorf("net.DialTimeout error: %+v", err) + } + uTlsConn := tls.UClient(dialConn, &config, tls.HelloCustom) + defer uTlsConn.Close() + + // do not use this particular spec in production + // make sure to generate a separate copy of ClientHelloSpec for every connection + spec, err := tls.UTLSIdToSpec(tls.HelloChrome_120) + // spec, err := tls.UTLSIdToSpec(tls.HelloFirefox_120) + if err != nil { + return nil, fmt.Errorf("tls.UTLSIdToSpec error: %+v", err) + } + + err = uTlsConn.ApplyPreset(&spec) + if err != nil { + return nil, fmt.Errorf("uTlsConn.Handshake() error: %+v", err) + } + + err = uTlsConn.Handshake() + if err != nil { + return nil, fmt.Errorf("uTlsConn.Handshake() error: %+v", err) + } + + return httpGetOverConn(uTlsConn, uTlsConn.ConnectionState().NegotiatedProtocol) +} + +func httpGetOverConn(conn net.Conn, alpn string) (*http.Response, error) { + req := &http.Request{ + Method: "GET", + URL: &url.URL{Scheme: "https", Host: requestHostname, Path: requestPath}, + Header: make(http.Header), + Host: requestHostname, + } + + switch alpn { + case "h2": + log.Println("HTTP/2 enabled") + req.Proto = "HTTP/2.0" + req.ProtoMajor = 2 + req.ProtoMinor = 0 + + tr := http2.Transport{} + cConn, err := tr.NewClientConn(conn) + if err != nil { + return nil, err + } + return cConn.RoundTrip(req) + case "http/1.1", "": + log.Println("Using HTTP/1.1") + req.Proto = "HTTP/1.1" + req.ProtoMajor = 1 + req.ProtoMinor = 1 + + err := req.Write(conn) + if err != nil { + return nil, err + } + return http.ReadResponse(bufio.NewReader(conn), req) + default: + return nil, fmt.Errorf("unsupported ALPN: %v", alpn) + } +} + +func main() { + resp, err := HttpGetCustom(requestHostname, requestAddr) + if err != nil { + panic(err) + } + fmt.Printf("Response: %+v\n", resp) + // read from resp.Body + body := make([]byte, 65535) + n, err := resp.Body.Read(body) + if err != nil && !errors.Is(err, io.EOF) { + panic(err) + } + + fmt.Printf("Body: %s\n", body[:n]) +} diff --git a/examples/old/examples.go b/examples/old/examples.go index b17a7e93..fc7ef2e7 100644 --- a/examples/old/examples.go +++ b/examples/old/examples.go @@ -49,7 +49,7 @@ func HttpGetByHelloID(hostname string, addr string, helloID tls.ClientHelloID) ( return nil, fmt.Errorf("uTlsConn.Handshake() error: %+v", err) } - return httpGetOverConn(uTlsConn, uTlsConn.HandshakeState.ServerHello.AlpnProtocol) + return httpGetOverConn(uTlsConn, uTlsConn.ConnectionState().NegotiatedProtocol) } // this example generates a randomized fingeprint, then re-uses it in a follow-up connection @@ -80,7 +80,7 @@ func HttpGetConsistentRandomized(hostname string, addr string) (*http.Response, return nil, fmt.Errorf("uTlsConn.Handshake() error: %+v", err) } - return httpGetOverConn(uTlsConn2, uTlsConn2.HandshakeState.ServerHello.AlpnProtocol) + return httpGetOverConn(uTlsConn2, uTlsConn2.ConnectionState().NegotiatedProtocol) } func HttpGetExplicitRandom(hostname string, addr string) (*http.Response, error) { @@ -112,7 +112,7 @@ func HttpGetExplicitRandom(hostname string, addr string) (*http.Response, error) fmt.Printf("#> ClientHello Random:\n%s", hex.Dump(uTlsConn.HandshakeState.Hello.Random)) fmt.Printf("#> ServerHello Random:\n%s", hex.Dump(uTlsConn.HandshakeState.ServerHello.Random)) - return httpGetOverConn(uTlsConn, uTlsConn.HandshakeState.ServerHello.AlpnProtocol) + return httpGetOverConn(uTlsConn, uTlsConn.ConnectionState().NegotiatedProtocol) } // Note that the server will reject the fake ticket(unless you set up your server to accept them) and do full handshake @@ -152,7 +152,7 @@ func HttpGetTicket(hostname string, addr string) (*http.Response, error) { fmt.Println("#> This is how client hello with session ticket looked:") fmt.Print(hex.Dump(uTlsConn.HandshakeState.Hello.Raw)) - return httpGetOverConn(uTlsConn, uTlsConn.HandshakeState.ServerHello.AlpnProtocol) + return httpGetOverConn(uTlsConn, uTlsConn.ConnectionState().NegotiatedProtocol) } // Note that the server will reject the fake ticket(unless you set up your server to accept them) and do full handshake @@ -183,7 +183,7 @@ func HttpGetTicketHelloID(hostname string, addr string, helloID tls.ClientHelloI fmt.Println("#> This is how client hello with session ticket looked:") fmt.Print(hex.Dump(uTlsConn.HandshakeState.Hello.Raw)) - return httpGetOverConn(uTlsConn, uTlsConn.HandshakeState.ServerHello.AlpnProtocol) + return httpGetOverConn(uTlsConn, uTlsConn.ConnectionState().NegotiatedProtocol) } func HttpGetCustom(hostname string, addr string) (*http.Response, error) { @@ -253,7 +253,7 @@ func HttpGetCustom(hostname string, addr string) (*http.Response, error) { return nil, fmt.Errorf("uTlsConn.Handshake() error: %+v", err) } - return httpGetOverConn(uTlsConn, uTlsConn.HandshakeState.ServerHello.AlpnProtocol) + return httpGetOverConn(uTlsConn, uTlsConn.ConnectionState().NegotiatedProtocol) } var roller *tls.Roller @@ -277,7 +277,7 @@ func HttpGetGoogleWithRoller() (*http.Response, error) { return nil, err } - return httpGetOverConn(c, c.HandshakeState.ServerHello.AlpnProtocol) + return httpGetOverConn(c, c.ConnectionState().NegotiatedProtocol) } func forgeConn() { diff --git a/go.mod b/go.mod index 458928cd..90a7162c 100644 --- a/go.mod +++ b/go.mod @@ -9,7 +9,7 @@ retract ( require ( github.com/andybalholm/brotli v1.0.5 - github.com/cloudflare/circl v1.3.3 + github.com/cloudflare/circl v1.3.6 github.com/klauspost/compress v1.16.7 github.com/quic-go/quic-go v0.37.4 golang.org/x/crypto v0.14.0 diff --git a/go.sum b/go.sum index ef6e9f8b..067e720f 100644 --- a/go.sum +++ b/go.sum @@ -1,7 +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/cloudflare/circl v1.3.6 h1:/xbKIqSHbZXHwkhbrhrt2YOHIwYJlXH94E3tI/gDlUg= +github.com/cloudflare/circl v1.3.6/go.mod h1:5XYMA4rFBvNIrhs50XuiBJ15vF2pZn4nnUKZrLbUZFA= github.com/go-logr/logr v1.2.4 h1:g01GSCwiDw2xSZfjJ2/T9M+S6pFdcNtFYsp+Y43HYDQ= github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 h1:tfuBGBXKqDEevZMzYi5KSi8KkcZtzBcTgAUUtapy0OI= github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= diff --git a/tls_test.go b/tls_test.go index 0b54319a..3d6f0bea 100644 --- a/tls_test.go +++ b/tls_test.go @@ -874,6 +874,8 @@ func TestCloneNonFuncFields(t *testing.T) { continue // these are unexported fields that are handled separately case "ApplicationSettings": // [UTLS] ALPS (Application Settings) f.Set(reflect.ValueOf(map[string][]byte{"a": {1}})) + case "ECHConfigs": // [UTLS] ECH (Encrypted Client Hello) Configs + f.Set(reflect.ValueOf([]ECHConfig{{Version: 1}})) default: t.Errorf("all fields must be accounted for, but saw unknown field %q", fn) } diff --git a/u_common.go b/u_common.go index 1757c895..ad3de305 100644 --- a/u_common.go +++ b/u_common.go @@ -35,9 +35,11 @@ const ( extensionNextProtoNeg uint16 = 13172 // not IANA assigned. Removed by crypto/tls since Nov 2019 utlsExtensionPadding uint16 = 21 - utlsExtensionCompressCertificate uint16 = 27 // https://datatracker.ietf.org/doc/html/rfc8879#section-7.1 - utlsExtensionApplicationSettings uint16 = 17513 // not IANA assigned - utlsFakeExtensionCustom uint16 = 1234 // not IANA assigned, for ALPS + utlsExtensionCompressCertificate uint16 = 27 // https://datatracker.ietf.org/doc/html/rfc8879#section-7.1 + utlsExtensionApplicationSettings uint16 = 17513 // not IANA assigned + utlsFakeExtensionCustom uint16 = 1234 // not IANA assigned, for ALPS + utlsExtensionECH uint16 = 0xfe0d // draft-ietf-tls-esni-17 + utlsExtensionECHOuterExtensions uint16 = 0xfd00 // draft-ietf-tls-esni-17 // extensions with 'fake' prefix break connection, if server echoes them back fakeExtensionEncryptThenMAC uint16 = 22 @@ -593,6 +595,7 @@ var ( HelloFirefox_99 = ClientHelloID{helloFirefox, "99", nil, nil} HelloFirefox_102 = ClientHelloID{helloFirefox, "102", nil, nil} HelloFirefox_105 = ClientHelloID{helloFirefox, "105", nil, nil} + HelloFirefox_120 = ClientHelloID{helloFirefox, "120", nil, nil} HelloChrome_Auto = HelloChrome_106_Shuffle HelloChrome_58 = ClientHelloID{helloChrome, "58", nil, nil} @@ -618,6 +621,9 @@ var ( HelloChrome_115_PQ = ClientHelloID{helloChrome, "115_PQ", nil, nil} HelloChrome_115_PQ_PSK = ClientHelloID{helloChrome, "115_PQ_PSK", nil, nil} + // Chrome w/ Post-Quantum Key Agreement and Encrypted ClientHello + HelloChrome_120 = ClientHelloID{helloChrome, "120", nil, nil} + HelloIOS_Auto = HelloIOS_14 HelloIOS_11_1 = ClientHelloID{helloIOS, "111", nil, nil} // legacy "111" means 11.1 HelloIOS_12_1 = ClientHelloID{helloIOS, "12.1", nil, nil} diff --git a/u_conn.go b/u_conn.go index 89e21fc2..2f1f1498 100644 --- a/u_conn.go +++ b/u_conn.go @@ -48,6 +48,9 @@ type UConn struct { // algorithms, as specified in the ClientHello. This is only relevant client-side, for the // server certificate. All other forms of certificate compression are unsupported. certCompressionAlgs []CertCompressionAlgo + + // ech extension is a shortcut to the ECH extension in the Extensions slice if there is one. + ech ECHExtension } // UClient returns a new uTLS client, with behavior depending on clientHelloID. @@ -616,13 +619,19 @@ func (uconn *UConn) ApplyConfig() error { } func (uconn *UConn) MarshalClientHello() error { + if uconn.ech != nil { + if err := uconn.ech.Configure(uconn.config.ECHConfigs); err != nil { + return err + } + return uconn.ech.MarshalClientHello(uconn) + } hello := uconn.HandshakeState.Hello headerLength := 2 + 32 + 1 + len(hello.SessionId) + 2 + len(hello.CipherSuites)*2 + 1 + len(hello.CompressionMethods) extensionsLen := 0 - var paddingExt *UtlsPaddingExtension + var paddingExt *UtlsPaddingExtension // reference to padding extension, if present for _, ext := range uconn.Extensions { if pe, ok := ext.(*UtlsPaddingExtension); !ok { // If not padding - just add length of extension to total length @@ -859,6 +868,7 @@ func (c *Conn) utlsHandshakeMessageType(msgType byte) (handshakeMessage, error) // Extending (*Conn).connectionStateLocked() func (c *Conn) utlsConnectionStateLocked(state *ConnectionState) { state.PeerApplicationSettings = c.utls.peerApplicationSettings + state.ECHRetryConfigs = c.utls.echRetryConfigs } type utlsConnExtraFields struct { @@ -867,5 +877,8 @@ type utlsConnExtraFields struct { peerApplicationSettings []byte localApplicationSettings []byte + // Encrypted Client Hello (ECH) + echRetryConfigs []ECHConfig + sessionController *sessionController } diff --git a/u_ech.go b/u_ech.go new file mode 100644 index 00000000..e5ce83a9 --- /dev/null +++ b/u_ech.go @@ -0,0 +1,236 @@ +package tls + +import ( + "crypto/rand" + "errors" + "fmt" + "io" + "math/big" + "sync" + + "github.com/cloudflare/circl/hpke" +) + +// Unstable API: This is a work in progress and may change in the future. Using +// it in your application may cause your application to break when updating to +// a new version of uTLS. + +const ( + OuterClientHello byte = 0x00 + InnerClientHello byte = 0x01 +) + +type EncryptedClientHelloExtension interface { + // TLSExtension must be implemented by all EncryptedClientHelloExtension implementations. + TLSExtension + + // Configure configures the EncryptedClientHelloExtension with the given slice of ECHConfig. + Configure([]ECHConfig) error + + // MarshalClientHello is called by (*UConn).MarshalClientHello() when an ECH extension + // is present to allow the ECH extension to take control of the generation of the + // entire ClientHello message. + MarshalClientHello(*UConn) error + + mustEmbedUnimplementedECHExtension() +} + +type ECHExtension = EncryptedClientHelloExtension // alias + +// type guard: GREASEEncryptedClientHelloExtension must implement EncryptedClientHelloExtension +var ( + _ EncryptedClientHelloExtension = (*GREASEEncryptedClientHelloExtension)(nil) + + _ EncryptedClientHelloExtension = (*UnimplementedECHExtension)(nil) +) + +type GREASEEncryptedClientHelloExtension struct { + CandidateCipherSuites []HPKESymmetricCipherSuite + cipherSuite HPKESymmetricCipherSuite // randomly picked from CandidateCipherSuites or generated if empty + CandidateConfigIds []uint8 + configId uint8 // randomly picked from CandidateConfigIds or generated if empty + EncapsulatedKey []byte // if empty, will generate random bytes + CandidatePayloadLens []uint16 // Pre-encryption. If 0, will pick 128(+16=144) + payload []byte // payload should be calculated ONCE and stored here, HRR will reuse this + + initOnce sync.Once + + UnimplementedECHExtension +} + +type GREASEECHExtension = GREASEEncryptedClientHelloExtension // alias + +// init initializes the GREASEEncryptedClientHelloExtension with random values if they are not set. +// +// Based on cloudflare/go's echGenerateGreaseExt() +func (g *GREASEEncryptedClientHelloExtension) init() error { + var initErr error + g.initOnce.Do(func() { + // Set the config_id field to a random byte. + // + // Note: must not reuse this extension unless for HRR. It is required + // to generate new random bytes for config_id for each new ClientHello, + // but reuse the same config_id for HRR. + if len(g.CandidateConfigIds) == 0 { + var b []byte = make([]byte, 1) + _, err := rand.Read(b[:]) + if err != nil { + initErr = fmt.Errorf("error generating random byte for config_id: %w", err) + return + } + g.configId = b[0] + } else { + // randomly pick one from the list + rndIndex, err := rand.Int(rand.Reader, big.NewInt(int64(len(g.CandidateConfigIds)))) + if err != nil { + initErr = fmt.Errorf("error generating random index for config_id: %w", err) + return + } + g.configId = g.CandidateConfigIds[rndIndex.Int64()] + } + + // Set the cipher_suite field to a supported HpkeSymmetricCipherSuite. + // The selection SHOULD vary to exercise all supported configurations, + // but MAY be held constant for successive connections to the same server + // in the same session. + if len(g.CandidateCipherSuites) == 0 { + _, kdf, aead := defaultHPKESuite.Params() + g.cipherSuite = HPKESymmetricCipherSuite{uint16(kdf), uint16(aead)} + } else { + // randomly pick one from the list + rndIndex, err := rand.Int(rand.Reader, big.NewInt(int64(len(g.CandidateCipherSuites)))) + if err != nil { + initErr = fmt.Errorf("error generating random index for cipher_suite: %w", err) + return + } + g.cipherSuite = HPKESymmetricCipherSuite{ + g.CandidateCipherSuites[rndIndex.Int64()].KdfId, + g.CandidateCipherSuites[rndIndex.Int64()].AeadId, + } + // aead = hpke.AEAD(g.cipherSuite.AeadId) + } + + if len(g.EncapsulatedKey) == 0 { + // use default random key from cloudflare/go + kem := hpke.KEM_X25519_HKDF_SHA256 + + pk, err := kem.Scheme().UnmarshalBinaryPublicKey(dummyX25519PublicKey) + if err != nil { + initErr = fmt.Errorf("tls: grease ech: failed to parse dummy public key: %w", err) + return + } + sender, err := defaultHPKESuite.NewSender(pk, nil) + if err != nil { + initErr = fmt.Errorf("tls: grease ech: failed to create sender: %w", err) + return + } + + g.EncapsulatedKey, _, err = sender.Setup(rand.Reader) + if err != nil { + initErr = fmt.Errorf("tls: grease ech: failed to setup encapsulated key: %w", err) + return + } + } + + if len(g.payload) == 0 { + if len(g.CandidatePayloadLens) == 0 { + g.CandidatePayloadLens = []uint16{128} + } + + // randomly pick one from the list + rndIndex, err := rand.Int(rand.Reader, big.NewInt(int64(len(g.CandidatePayloadLens)))) + if err != nil { + initErr = fmt.Errorf("error generating random index for payload length: %w", err) + return + } + + initErr = g.randomizePayload(g.CandidatePayloadLens[rndIndex.Int64()]) + } + }) + + return initErr +} + +func (g *GREASEEncryptedClientHelloExtension) randomizePayload(encodedHelloInnerLen uint16) error { + if len(g.payload) != 0 { + return errors.New("tls: grease ech: regenerating payload is forbidden") + } + + aead := hpke.AEAD(g.cipherSuite.AeadId) + g.payload = make([]byte, int(aead.CipherLen(uint(encodedHelloInnerLen)))) + _, err := rand.Read(g.payload) + if err != nil { + return fmt.Errorf("tls: generating grease ech payload: %w", err) + } + return nil +} + +// For ECH extensions, writeToUConn simply points the ech field in UConn to the extension. +func (g *GREASEEncryptedClientHelloExtension) writeToUConn(uconn *UConn) error { + // uconn.ech = g // don't do this, so we don't intercept the MarshalClientHello() call + return nil +} + +func (g *GREASEEncryptedClientHelloExtension) Len() int { + g.init() + return 2 + 2 + 1 /* ClientHello Type */ + 4 /* CipherSuite */ + 1 /* Config ID */ + 2 + len(g.EncapsulatedKey) + 2 + len(g.payload) +} + +func (g *GREASEEncryptedClientHelloExtension) Read(b []byte) (int, error) { + if len(b) < g.Len() { + return 0, io.ErrShortBuffer + } + + b[0] = byte(utlsExtensionECH >> 8) + b[1] = byte(utlsExtensionECH & 0xFF) + b[2] = byte((g.Len() - 4) >> 8) + b[3] = byte((g.Len() - 4) & 0xFF) + b[4] = OuterClientHello + b[5] = byte(g.cipherSuite.KdfId >> 8) + b[6] = byte(g.cipherSuite.KdfId & 0xFF) + b[7] = byte(g.cipherSuite.AeadId >> 8) + b[8] = byte(g.cipherSuite.AeadId & 0xFF) + b[9] = g.configId + b[10] = byte(len(g.EncapsulatedKey) >> 8) + b[11] = byte(len(g.EncapsulatedKey) & 0xFF) + copy(b[12:], g.EncapsulatedKey) + b[12+len(g.EncapsulatedKey)] = byte(len(g.payload) >> 8) + b[12+len(g.EncapsulatedKey)+1] = byte(len(g.payload) & 0xFF) + copy(b[12+len(g.EncapsulatedKey)+2:], g.payload) + + return g.Len(), io.EOF +} + +func (*GREASEEncryptedClientHelloExtension) Configure([]ECHConfig) error { + return errors.New("tls: grease ech: Configure() is not implemented") +} + +func (*GREASEEncryptedClientHelloExtension) MarshalClientHello(*UConn) error { + return errors.New("tls: grease ech: MarshalClientHello() is not implemented, use (*UConn).MarshalClientHello() instead") +} + +type UnimplementedECHExtension struct{} + +func (*UnimplementedECHExtension) writeToUConn(_ *UConn) error { + return errors.New("tls: unimplemented ECHExtension") +} + +func (*UnimplementedECHExtension) Len() int { + return 0 +} + +func (*UnimplementedECHExtension) Read(_ []byte) (int, error) { + return 0, errors.New("tls: unimplemented ECHExtension") +} + +func (*UnimplementedECHExtension) Configure([]ECHConfig) error { + return errors.New("tls: unimplemented ECHExtension") +} + +func (*UnimplementedECHExtension) MarshalClientHello(*UConn) error { + return errors.New("tls: unimplemented ECHExtension") +} + +func (*UnimplementedECHExtension) mustEmbedUnimplementedECHExtension() { + panic("mustEmbedUnimplementedECHExtension() is not implemented") +} diff --git a/u_ech_config.go b/u_ech_config.go new file mode 100644 index 00000000..633c7b12 --- /dev/null +++ b/u_ech_config.go @@ -0,0 +1,135 @@ +package tls + +import ( + "errors" + "fmt" + + "github.com/cloudflare/circl/hpke" + "golang.org/x/crypto/cryptobyte" +) + +type ECHConfigContents struct { + KeyConfig HPKEKeyConfig + MaximumNameLength uint8 + PublicName []byte + // Extensions []TLSExtension // ignored for now + rawExtensions []byte +} + +func UnmarshalECHConfigContents(contents []byte) (ECHConfigContents, error) { + var ( + contentCryptobyte = cryptobyte.String(contents) + config ECHConfigContents + ) + + // Parse KeyConfig + var t cryptobyte.String + if !contentCryptobyte.ReadUint8(&config.KeyConfig.ConfigId) || + !contentCryptobyte.ReadUint16(&config.KeyConfig.KemId) || + !contentCryptobyte.ReadUint16LengthPrefixed(&t) || + !t.ReadBytes(&config.KeyConfig.rawPublicKey, len(t)) || + !contentCryptobyte.ReadUint16LengthPrefixed(&t) || + len(t)%4 != 0 { + return config, errors.New("error parsing KeyConfig") + } + + // Parse all CipherSuites in KeyConfig + config.KeyConfig.CipherSuites = nil + for !t.Empty() { + var kdfId, aeadId uint16 + if !t.ReadUint16(&kdfId) || !t.ReadUint16(&aeadId) { + // This indicates an internal bug. + panic("internal error while parsing contents.cipher_suites") + } + config.KeyConfig.CipherSuites = append(config.KeyConfig.CipherSuites, HPKESymmetricCipherSuite{kdfId, aeadId}) + } + + if !contentCryptobyte.ReadUint8(&config.MaximumNameLength) || + !contentCryptobyte.ReadUint8LengthPrefixed(&t) || + !t.ReadBytes(&config.PublicName, len(t)) || + !contentCryptobyte.ReadUint16LengthPrefixed(&t) || + !t.ReadBytes(&config.rawExtensions, len(t)) || + !contentCryptobyte.Empty() { + return config, errors.New("error parsing ECHConfigContents") + } + return config, nil +} + +func (echcc *ECHConfigContents) ParsePublicKey() error { + var err error + kem := hpke.KEM(echcc.KeyConfig.KemId) + if !kem.IsValid() { + return errors.New("invalid KEM") + } + echcc.KeyConfig.PublicKey, err = kem.Scheme().UnmarshalBinaryPublicKey(echcc.KeyConfig.rawPublicKey) + if err != nil { + return fmt.Errorf("error parsing public key: %s", err) + } + return nil +} + +type ECHConfig struct { + Version uint16 + Length uint16 + Contents ECHConfigContents + + raw []byte +} + +// UnmarshalECHConfigs parses a sequence of ECH configurations. +// +// Ported from cloudflare/go +func UnmarshalECHConfigs(raw []byte) ([]ECHConfig, error) { + var ( + err error + config ECHConfig + t, contents cryptobyte.String + ) + configs := make([]ECHConfig, 0) + s := cryptobyte.String(raw) + if !s.ReadUint16LengthPrefixed(&t) || !s.Empty() { + return configs, errors.New("error parsing configs") + } + raw = raw[2:] +ConfigsLoop: + for !t.Empty() { + l := len(t) + if !t.ReadUint16(&config.Version) || + !t.ReadUint16LengthPrefixed(&contents) { + return nil, errors.New("error parsing config") + } + config.Length = uint16(len(contents)) + n := l - len(t) + config.raw = raw[:n] + raw = raw[n:] + + if config.Version != utlsExtensionECH { + continue ConfigsLoop + } + + /**** cloudflare/go original ****/ + // if !readConfigContents(&contents, &config) { + // return nil, errors.New("error parsing config contents") + // } + + config.Contents, err = UnmarshalECHConfigContents(contents) + if err != nil { + return nil, fmt.Errorf("error parsing config contents: %s", err) + } + + /**** cloudflare/go original ****/ + // kem := hpke.KEM(config.kemId) + // if !kem.IsValid() { + // continue ConfigsLoop + // } + // config.pk, err = kem.Scheme().UnmarshalBinaryPublicKey(config.rawPublicKey) + // if err != nil { + // return nil, fmt.Errorf("error parsing public key: %s", err) + // } + + config.Contents.ParsePublicKey() // parse the bytes into a public key + + configs = append(configs, config) + } + return configs, nil +} diff --git a/u_handshake_client.go b/u_handshake_client.go index 7b9a98f5..ba40a862 100644 --- a/u_handshake_client.go +++ b/u_handshake_client.go @@ -141,6 +141,7 @@ func (hs *clientHandshakeStateTLS13) sendClientEncryptedExtensions() error { func (hs *clientHandshakeStateTLS13) utlsReadServerParameters(encryptedExtensions *encryptedExtensionsMsg) error { hs.c.utls.hasApplicationSettings = encryptedExtensions.utls.hasApplicationSettings hs.c.utls.peerApplicationSettings = encryptedExtensions.utls.applicationSettings + hs.c.utls.echRetryConfigs = encryptedExtensions.utls.echRetryConfigs if hs.c.utls.hasApplicationSettings { if hs.uconn.vers < VersionTLS13 { @@ -160,6 +161,23 @@ func (hs *clientHandshakeStateTLS13) utlsReadServerParameters(encryptedExtension } } + if len(hs.c.utls.echRetryConfigs) > 0 { + if hs.uconn.vers < VersionTLS13 { + return errors.New("tls: server sent ECH retry configs at invalid version") + } + + // find ECH extension in ClientHello + var echIncluded bool + for _, ext := range hs.uconn.Extensions { + if _, ok := ext.(ECHExtension); ok { + echIncluded = true + } + } + if !echIncluded { + return errors.New("tls: server sent ECH retry configs without client sending ECH extension") + } + } + return nil } diff --git a/u_handshake_messages.go b/u_handshake_messages.go index e7ebb151..1c9f460c 100644 --- a/u_handshake_messages.go +++ b/u_handshake_messages.go @@ -56,6 +56,7 @@ func (m *utlsCompressedCertificateMsg) unmarshal(data []byte) bool { type utlsEncryptedExtensionsMsgExtraFields struct { hasApplicationSettings bool applicationSettings []byte + echRetryConfigs []ECHConfig customExtension []byte } @@ -64,6 +65,12 @@ func (m *encryptedExtensionsMsg) utlsUnmarshal(extension uint16, extData cryptob case utlsExtensionApplicationSettings: m.utls.hasApplicationSettings = true m.utls.applicationSettings = []byte(extData) + case utlsExtensionECH: + var err error + m.utls.echRetryConfigs, err = UnmarshalECHConfigs([]byte(extData)) + if err != nil { + return false + } } return true // success/unknown extension } diff --git a/u_hpke.go b/u_hpke.go new file mode 100644 index 00000000..08d5eb4f --- /dev/null +++ b/u_hpke.go @@ -0,0 +1,62 @@ +package tls + +import ( + "errors" + "fmt" + + "github.com/cloudflare/circl/hpke" + "github.com/cloudflare/circl/kem" +) + +type HPKERawPublicKey = []byte +type HPKE_KEM_ID = uint16 // RFC 9180 +type HPKE_KDF_ID = uint16 // RFC 9180 +type HPKE_AEAD_ID = uint16 // RFC 9180 + +type HPKESymmetricCipherSuite struct { + KdfId HPKE_KDF_ID + AeadId HPKE_AEAD_ID +} + +type HPKEKeyConfig struct { + ConfigId uint8 + KemId HPKE_KEM_ID + PublicKey kem.PublicKey + rawPublicKey HPKERawPublicKey + CipherSuites []HPKESymmetricCipherSuite +} + +var defaultHPKESuite hpke.Suite + +func init() { + var err error + defaultHPKESuite, err = hpkeAssembleSuite( + uint16(hpke.KEM_X25519_HKDF_SHA256), + uint16(hpke.KDF_HKDF_SHA256), + uint16(hpke.AEAD_AES128GCM), + ) + if err != nil { + panic(fmt.Sprintf("hpke: mandatory-to-implement cipher suite not supported: %s", err)) + } +} + +func hpkeAssembleSuite(kemId, kdfId, aeadId uint16) (hpke.Suite, error) { + kem := hpke.KEM(kemId) + if !kem.IsValid() { + return hpke.Suite{}, errors.New("KEM is not supported") + } + kdf := hpke.KDF(kdfId) + if !kdf.IsValid() { + return hpke.Suite{}, errors.New("KDF is not supported") + } + aead := hpke.AEAD(aeadId) + if !aead.IsValid() { + return hpke.Suite{}, errors.New("AEAD is not supported") + } + return hpke.NewSuite(kem, kdf, aead), nil +} + +var dummyX25519PublicKey = []byte{ + 143, 38, 37, 36, 12, 6, 229, 30, 140, 27, 167, 73, 26, 100, 203, 107, 216, + 81, 163, 222, 52, 211, 54, 210, 46, 37, 78, 216, 157, 97, 241, 244, +} diff --git a/u_parrots.go b/u_parrots.go index d7bd8675..7165f6a8 100644 --- a/u_parrots.go +++ b/u_parrots.go @@ -14,6 +14,8 @@ import ( "math/rand" "sort" "strconv" + + "github.com/refraction-networking/utls/dicttls" ) var ErrUnknownClientHelloID = errors.New("tls: unknown ClientHelloID") @@ -656,6 +658,96 @@ func utlsIdToSpec(id ClientHelloID) (ClientHelloSpec, error) { &UtlsPaddingExtension{GetPaddingLen: BoringPaddingStyle}, }), }, nil + // Chrome w/ Post-Quantum Key Agreement and ECH + case HelloChrome_120: + 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"}}, + &GREASEEncryptedClientHelloExtension{ + CandidateCipherSuites: []HPKESymmetricCipherSuite{ + { + KdfId: dicttls.HKDF_SHA256, + AeadId: dicttls.AEAD_AES_128_GCM, + }, + { + KdfId: dicttls.HKDF_SHA256, + AeadId: dicttls.AEAD_AES_256_GCM, + }, + { + KdfId: dicttls.HKDF_SHA256, + AeadId: dicttls.AEAD_CHACHA20_POLY1305, + }, + }, + CandidatePayloadLens: []uint16{128, 160}, // +16: 144, 176 + }, + &UtlsGREASEExtension{}, + }), + }, nil case HelloFirefox_55, HelloFirefox_56: return ClientHelloSpec{ TLSVersMax: VersionTLS12, @@ -1043,6 +1135,121 @@ func utlsIdToSpec(id ClientHelloID) (ClientHelloSpec, error) { }, }, }, nil + case HelloFirefox_120: + return ClientHelloSpec{ + TLSVersMin: VersionTLS12, + TLSVersMax: VersionTLS13, + CipherSuites: []uint16{ + TLS_AES_128_GCM_SHA256, + TLS_CHACHA20_POLY1305_SHA256, + TLS_AES_256_GCM_SHA384, + TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256, + TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256, + TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305, + TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305, + TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384, + TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384, + TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA, + TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA, + 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: []uint8{ + 0x0, // no compression + }, + Extensions: []TLSExtension{ + &SNIExtension{}, + &ExtendedMasterSecretExtension{}, + &RenegotiationInfoExtension{ + Renegotiation: RenegotiateOnceAsClient, + }, + &SupportedCurvesExtension{ + Curves: []CurveID{ + X25519, + CurveP256, + CurveP384, + CurveP521, + 256, + 257, + }, + }, + &SupportedPointsExtension{ + SupportedPoints: []uint8{ + 0x0, // uncompressed + }, + }, + &ALPNExtension{ + AlpnProtocols: []string{ + "h2", + "http/1.1", + }, + }, + &StatusRequestExtension{}, + &FakeDelegatedCredentialsExtension{ + SupportedSignatureAlgorithms: []SignatureScheme{ + ECDSAWithP256AndSHA256, + ECDSAWithP384AndSHA384, + ECDSAWithP521AndSHA512, + ECDSAWithSHA1, + }, + }, + &KeyShareExtension{ + KeyShares: []KeyShare{ + { + Group: X25519, + }, + { + Group: CurveP256, + }, + }, + }, + &SupportedVersionsExtension{ + Versions: []uint16{ + VersionTLS13, + VersionTLS12, + }, + }, + &SignatureAlgorithmsExtension{ + SupportedSignatureAlgorithms: []SignatureScheme{ + ECDSAWithP256AndSHA256, + ECDSAWithP384AndSHA384, + ECDSAWithP521AndSHA512, + PSSWithSHA256, + PSSWithSHA384, + PSSWithSHA512, + PKCS1WithSHA256, + PKCS1WithSHA384, + PKCS1WithSHA512, + ECDSAWithSHA1, + PKCS1WithSHA1, + }, + }, + &FakeRecordSizeLimitExtension{ + Limit: 0x4001, + }, + &GREASEEncryptedClientHelloExtension{ + CandidateCipherSuites: []HPKESymmetricCipherSuite{ + { + KdfId: dicttls.HKDF_SHA256, + AeadId: dicttls.AEAD_AES_128_GCM, + }, + { + KdfId: dicttls.HKDF_SHA256, + AeadId: dicttls.AEAD_AES_256_GCM, + }, + { + KdfId: dicttls.HKDF_SHA256, + AeadId: dicttls.AEAD_CHACHA20_POLY1305, + }, + }, + CandidatePayloadLens: []uint16{128, 223}, // +16: 144, 239 + }, + }, + }, nil case HelloIOS_11_1: return ClientHelloSpec{ TLSVersMax: VersionTLS12,