Skip to content

Commit

Permalink
feat: parse GREASE ECH from raw (#276)
Browse files Browse the repository at this point in the history
  • Loading branch information
gaukas authored Dec 22, 2023
1 parent f8beb04 commit 42e79cb
Show file tree
Hide file tree
Showing 4 changed files with 194 additions and 6 deletions.
9 changes: 8 additions & 1 deletion u_conn.go
Original file line number Diff line number Diff line change
Expand Up @@ -619,12 +619,19 @@ func (uconn *UConn) ApplyConfig() error {
}

func (uconn *UConn) MarshalClientHello() error {
if uconn.ech != nil {
if len(uconn.config.ECHConfigs) > 0 && uconn.ech != nil {
if err := uconn.ech.Configure(uconn.config.ECHConfigs); err != nil {
return err
}
return uconn.ech.MarshalClientHello(uconn)
}

return uconn.MarshalClientHelloNoECH() // if no ECH pointer, just marshal normally
}

// MarshalClientHelloNoECH marshals ClientHello as if there was no
// ECH extension present.
func (uconn *UConn) MarshalClientHelloNoECH() error {
hello := uconn.HandshakeState.Hello
headerLength := 2 + 32 + 1 + len(hello.SessionId) +
2 + len(hello.CipherSuites)*2 +
Expand Down
82 changes: 79 additions & 3 deletions u_ech.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import (

"github.com/cloudflare/circl/hpke"
"github.com/refraction-networking/utls/dicttls"
"golang.org/x/crypto/cryptobyte"
)

// Unstable API: This is a work in progress and may change in the future. Using
Expand Down Expand Up @@ -166,17 +167,21 @@ func (g *GREASEEncryptedClientHelloExtension) randomizePayload(encodedHelloInner
return nil
}

// writeToUConn implements TLSExtension.
//
// 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
uconn.ech = g
return uconn.MarshalClientHelloNoECH()
}

// Len implements TLSExtension.
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)
}

// Read implements TLSExtension.
func (g *GREASEEncryptedClientHelloExtension) Read(b []byte) (int, error) {
if len(b) < g.Len() {
return 0, io.ErrShortBuffer
Expand All @@ -202,40 +207,111 @@ func (g *GREASEEncryptedClientHelloExtension) Read(b []byte) (int, error) {
return g.Len(), io.EOF
}

// Configure implements EncryptedClientHelloExtension.
func (*GREASEEncryptedClientHelloExtension) Configure([]ECHConfig) error {
return errors.New("tls: grease ech: Configure() is not implemented")
return nil // no-op, it is not possible to configure a GREASE extension for now
}

// MarshalClientHello implements EncryptedClientHelloExtension.
func (*GREASEEncryptedClientHelloExtension) MarshalClientHello(*UConn) error {
return errors.New("tls: grease ech: MarshalClientHello() is not implemented, use (*UConn).MarshalClientHello() instead")
}

// Write implements TLSExtensionWriter.
func (g *GREASEEncryptedClientHelloExtension) Write(b []byte) (int, error) {
fullLen := len(b)
extData := cryptobyte.String(b)

// Check the extension type, it must be OuterClientHello otherwise we are not
// parsing the correct extension
var chType uint8 // 0: outer, 1: inner
var ignored cryptobyte.String
if !extData.ReadUint8(&chType) || chType != 0 {
return fullLen, errors.New("bad Client Hello type, expected 0, got " + fmt.Sprintf("%d", chType))
}

// Parse the cipher suite
if !extData.ReadUint16(&g.cipherSuite.KdfId) || !extData.ReadUint16(&g.cipherSuite.AeadId) {
return fullLen, errors.New("bad cipher suite")
}
if g.cipherSuite.KdfId != dicttls.HKDF_SHA256 &&
g.cipherSuite.KdfId != dicttls.HKDF_SHA384 &&
g.cipherSuite.KdfId != dicttls.HKDF_SHA512 {
return fullLen, errors.New("bad KDF ID: " + fmt.Sprintf("%d", g.cipherSuite.KdfId))
}
if g.cipherSuite.AeadId != dicttls.AEAD_AES_128_GCM &&
g.cipherSuite.AeadId != dicttls.AEAD_AES_256_GCM &&
g.cipherSuite.AeadId != dicttls.AEAD_CHACHA20_POLY1305 {
return fullLen, errors.New("bad AEAD ID: " + fmt.Sprintf("%d", g.cipherSuite.AeadId))
}
g.CandidateCipherSuites = []HPKESymmetricCipherSuite{g.cipherSuite}

// GREASE the ConfigId
if !extData.ReadUint8(&g.configId) {
return fullLen, errors.New("bad config ID")
}
// we don't write to CandidateConfigIds because we don't really want to reuse the same config_id

// GREASE the EncapsulatedKey
if !extData.ReadUint16LengthPrefixed(&ignored) {
return fullLen, errors.New("bad encapsulated key")
}
g.EncapsulatedKey = make([]byte, len(ignored))
n, err := rand.Read(g.EncapsulatedKey)
if err != nil {
return fullLen, fmt.Errorf("tls: generating grease ech encapsulated key: %w", err)
}
if n != len(g.EncapsulatedKey) {
return fullLen, fmt.Errorf("tls: generating grease ech encapsulated key: short read for %d bytes", len(ignored)-n)
}

// GREASE the payload
if !extData.ReadUint16LengthPrefixed(&ignored) {
return fullLen, errors.New("bad payload")
}
aead := hpke.AEAD(g.cipherSuite.AeadId)
g.CandidatePayloadLens = []uint16{uint16(len(ignored) - int(aead.CipherLen(0)))}

return fullLen, nil
}

// UnimplementedECHExtension is a placeholder for an ECH extension that is not implemented.
// All implementations of EncryptedClientHelloExtension should embed this struct to ensure
// forward compatibility.
type UnimplementedECHExtension struct{}

// writeToUConn implements TLSExtension.
func (*UnimplementedECHExtension) writeToUConn(_ *UConn) error {
return errors.New("tls: unimplemented ECHExtension")
}

// Len implements TLSExtension.
func (*UnimplementedECHExtension) Len() int {
return 0
}

// Read implements TLSExtension.
func (*UnimplementedECHExtension) Read(_ []byte) (int, error) {
return 0, errors.New("tls: unimplemented ECHExtension")
}

// Configure implements EncryptedClientHelloExtension.
func (*UnimplementedECHExtension) Configure([]ECHConfig) error {
return errors.New("tls: unimplemented ECHExtension")
}

// MarshalClientHello implements EncryptedClientHelloExtension.
func (*UnimplementedECHExtension) MarshalClientHello(*UConn) error {
return errors.New("tls: unimplemented ECHExtension")
}

// mustEmbedUnimplementedECHExtension is a noop function but is required to
// ensure forward compatibility.
func (*UnimplementedECHExtension) mustEmbedUnimplementedECHExtension() {
panic("mustEmbedUnimplementedECHExtension() is not implemented")
}

// BoringGREASEECH returns a GREASE scheme BoringSSL uses by default.
func BoringGREASEECH() *GREASEEncryptedClientHelloExtension {
return &GREASEEncryptedClientHelloExtension{
CandidateCipherSuites: []HPKESymmetricCipherSuite{
Expand Down
103 changes: 103 additions & 0 deletions u_ech_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
package tls_test

import (
"errors"
"io"
"testing"

tls "github.com/refraction-networking/utls"
"github.com/refraction-networking/utls/dicttls"
)

func TestGREASEECHWrite(t *testing.T) {
for _, testsuite := range []rawECHTestSuite{rawECH_HKDFSHA256_AES128GCM} {

gech := &tls.GREASEEncryptedClientHelloExtension{}

n, err := gech.Write(testsuite.raw[4:]) // skip the first 4 bytes which are the extension type and length
if err != nil {
t.Fatalf("Failed to write GREASE ECH extension: %s", err)
}

if n != len(testsuite.raw[4:]) {
t.Fatalf("Failed to write all GREASE ECH extension bytes: %d != %d", n, len(testsuite.raw[4:]))
}

var gechBytes []byte = make([]byte, 1024)
n, err = gech.Read(gechBytes)
if err != nil && !errors.Is(err, io.EOF) {
t.Fatalf("Failed to read GREASE ECH extension: %s", err)
}

if n != len(testsuite.raw) {
t.Fatalf("GREASE ECH Read length mismatch: %d != %d", n, len(testsuite.raw))
}

// manually check fields in the GREASE ECH extension
if len(gech.CandidateCipherSuites) != 1 ||
gech.CandidateCipherSuites[0].KdfId != testsuite.kdfID ||
gech.CandidateCipherSuites[0].AeadId != testsuite.aeadID {
t.Fatalf("GREASE ECH Read cipher suite mismatch")
}

if len(gech.EncapsulatedKey) != int(testsuite.encapsulatedKeyLength) {
t.Fatalf("GREASE ECH Read encapsulated key length mismatch")
}

if len(gech.CandidatePayloadLens) != 1 || gech.CandidatePayloadLens[0] != testsuite.payloadLength {
t.Fatalf("GREASE ECH Read payload length mismatch")
}
}
}

type rawECHTestSuite struct {
kdfID uint16
aeadID uint16
encapsulatedKeyLength uint16
payloadLength uint16

raw []byte
}

var (
rawECH_HKDFSHA256_AES128GCM rawECHTestSuite = rawECHTestSuite{
kdfID: dicttls.HKDF_SHA256,
aeadID: dicttls.AEAD_AES_128_GCM,
encapsulatedKeyLength: 32,
payloadLength: 208 - 16,
raw: []byte{
0xfe, 0x0d, 0x00, 0xfa, 0x00, 0x00, 0x01, 0x00,
0x01, 0x77, 0x00, 0x20, 0x3d, 0x3e, 0xe0, 0xa6,
0x1f, 0x46, 0x4f, 0x89, 0x5f, 0x39, 0x4a, 0xfd,
0x6e, 0xbc, 0x7f, 0x4e, 0xe2, 0x5a, 0xdc, 0x4e,
0xda, 0x9a, 0x9f, 0x5f, 0x2b, 0xf5, 0x21, 0x0e,
0xc6, 0x33, 0x64, 0x32, 0x00, 0xd0, 0xae, 0xff,
0x25, 0xd6, 0x4a, 0x23, 0x3a, 0x13, 0x5b, 0xdc,
0xe4, 0xaf, 0x6c, 0xb8, 0xaf, 0x66, 0x57, 0xbd,
0x44, 0x2d, 0xca, 0xb6, 0xbb, 0xaf, 0xda, 0x8a,
0x6b, 0x12, 0xb2, 0x42, 0xf1, 0x3d, 0xf6, 0x26,
0xd4, 0x82, 0x30, 0x40, 0xd4, 0x53, 0x06, 0x7c,
0xf1, 0x10, 0xf3, 0x80, 0x16, 0x95, 0xa7, 0xfb,
0x08, 0x76, 0x82, 0x85, 0x86, 0xb4, 0x3a, 0x7b,
0xea, 0xfb, 0xaa, 0xc3, 0xe0, 0x51, 0xcf, 0x42,
0xf6, 0xa0, 0x15, 0x0e, 0x26, 0x4d, 0x37, 0x35,
0x95, 0x4d, 0xce, 0xf6, 0xd6, 0x58, 0x78, 0x67,
0x42, 0xd3, 0xc6, 0xac, 0xb5, 0xe9, 0x3e, 0xb6,
0x02, 0x87, 0x66, 0xb3, 0xb2, 0x56, 0x99, 0xb2,
0xdb, 0x8c, 0x3b, 0x04, 0xf1, 0x7c, 0x85, 0x5b,
0xc3, 0x93, 0x8e, 0xdb, 0x5d, 0x87, 0x66, 0xfb,
0x66, 0x54, 0xf3, 0xec, 0x25, 0xe5, 0x70, 0x3c,
0xd5, 0x0e, 0x8e, 0xd5, 0xd2, 0xbb, 0x24, 0x2b,
0xb5, 0x01, 0xa0, 0x5e, 0xba, 0x45, 0xaf, 0x68,
0x96, 0x8a, 0x83, 0x90, 0x20, 0x5b, 0x8c, 0x7d,
0x24, 0x00, 0x2f, 0x08, 0x7f, 0x29, 0x8c, 0x32,
0x5e, 0x57, 0xb5, 0x64, 0xaa, 0x0b, 0xf4, 0x42,
0x54, 0xdc, 0xe5, 0xd4, 0x08, 0xf4, 0x4d, 0x27,
0x5d, 0x90, 0x52, 0x32, 0x22, 0xc8, 0xb6, 0xd8,
0x80, 0xa6, 0x30, 0xa0, 0x20, 0x98, 0x2c, 0x0b,
0x3e, 0x55, 0x4a, 0x09, 0xa9, 0x09, 0xa4, 0x99,
0x89, 0x02, 0x6e, 0xab, 0xe3, 0xa1, 0xe9, 0xb8,
0x58, 0x20, 0xcc, 0xc8, 0xb0, 0x73,
},
}
)
6 changes: 4 additions & 2 deletions u_tls_extensions.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,8 @@ func ExtensionFromID(id uint16) TLSExtension {
return &FakeTokenBindingExtension{}
case utlsExtensionCompressCertificate:
return &UtlsCompressCertExtension{}
case fakeRecordSizeLimit:
return &FakeRecordSizeLimitExtension{}
case fakeExtensionDelegatedCredentials:
return &FakeDelegatedCredentialsExtension{}
case extensionSessionTicket:
Expand Down Expand Up @@ -73,8 +75,8 @@ func ExtensionFromID(id uint16) TLSExtension {
return &FakeChannelIDExtension{true}
case fakeExtensionChannelID:
return &FakeChannelIDExtension{}
case fakeRecordSizeLimit:
return &FakeRecordSizeLimitExtension{}
case utlsExtensionECH:
return &GREASEEncryptedClientHelloExtension{}
case extensionRenegotiationInfo:
return &RenegotiationInfoExtension{}
default:
Expand Down

0 comments on commit 42e79cb

Please sign in to comment.