mirror of
https://github.com/refraction-networking/utls.git
synced 2025-04-01 02:57:36 +03:00
feat: add GREASEEncryptedClientHelloExtension (#266)
* dicttls: update ECH-related entries * wip: GREASE ECH extension * new: GREASE ECH extension * fix: GREASE ECH Read must succeed with io.EOF * new: GREASE ECH multiple payload len * new: parse ECH in EncryptedExtensions * fix: ECHConfig Length always 0 * new: GREASE ECH parrots * new: (*Config).ECHConfigs Add (*Config).ECHConfigs for future full ECH extension. * new: add GREASE ECH example Add an incomplete example of using GREASE ECH extension (Chrome 120 parrot). * fix: invalid httpGetOverConn call fix a problem in old example where httpGetOverConn was called with uTlsConn.HandshakeState.ServerHello.AlpnProtocol, which will not be populated in case TLS 1.3 is used. * new: possible InnerClientHello length
This commit is contained in:
parent
9521fba944
commit
b4de442d02
19 changed files with 925 additions and 51 deletions
2
alert.go
2
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 {
|
||||
|
|
17
common.go
17
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]
|
||||
}
|
||||
}
|
||||
|
||||
|
|
19
dicttls/hpke_aead_identifiers.go
Normal file
19
dicttls/hpke_aead_identifiers.go
Normal file
|
@ -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
|
||||
}
|
|
@ -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,
|
||||
}
|
53
dicttls/hpke_kem_identifiers.go
Normal file
53
dicttls/hpke_kem_identifiers.go
Normal file
|
@ -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,
|
||||
}
|
|
@ -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,
|
||||
}
|
127
examples/ech/main.go
Normal file
127
examples/ech/main.go
Normal file
|
@ -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])
|
||||
}
|
|
@ -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() {
|
||||
|
|
2
go.mod
2
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
|
||||
|
|
4
go.sum
4
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=
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
12
u_common.go
12
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}
|
||||
|
|
15
u_conn.go
15
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
|
||||
}
|
||||
|
|
236
u_ech.go
Normal file
236
u_ech.go
Normal file
|
@ -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")
|
||||
}
|
135
u_ech_config.go
Normal file
135
u_ech_config.go
Normal file
|
@ -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
|
||||
}
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
62
u_hpke.go
Normal file
62
u_hpke.go
Normal file
|
@ -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,
|
||||
}
|
207
u_parrots.go
207
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,
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue