feat: add GREASEEncryptedClientHelloExtension ()

* 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:
Gaukas Wang 2023-12-13 19:50:50 -07:00 committed by GitHub
parent 9521fba944
commit b4de442d02
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
19 changed files with 925 additions and 51 deletions

View file

@ -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 {

View file

@ -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]
}
}

View 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
}

View file

@ -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,
}

View 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,
}

View file

@ -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
View 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])
}

View file

@ -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
View file

@ -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
View file

@ -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=

View file

@ -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)
}

View file

@ -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}

View file

@ -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
View 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
View 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
}

View file

@ -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
}

View file

@ -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
View 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,
}

View file

@ -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,