From 42e79cb29a740a7ff3feb4bc5139a394dd0f4449 Mon Sep 17 00:00:00 2001 From: Gaukas Wang Date: Fri, 22 Dec 2023 11:16:33 -0700 Subject: [PATCH] feat: parse GREASE ECH from raw (#276) --- u_conn.go | 9 +++- u_ech.go | 82 +++++++++++++++++++++++++++++++++-- u_ech_test.go | 103 ++++++++++++++++++++++++++++++++++++++++++++ u_tls_extensions.go | 6 ++- 4 files changed, 194 insertions(+), 6 deletions(-) create mode 100644 u_ech_test.go diff --git a/u_conn.go b/u_conn.go index 2f1f1498..d7e569a6 100644 --- a/u_conn.go +++ b/u_conn.go @@ -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 + diff --git a/u_ech.go b/u_ech.go index 9d3593c9..fad8dd02 100644 --- a/u_ech.go +++ b/u_ech.go @@ -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 @@ -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 @@ -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{ diff --git a/u_ech_test.go b/u_ech_test.go new file mode 100644 index 00000000..967ceb27 --- /dev/null +++ b/u_ech_test.go @@ -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, + }, + } +) diff --git a/u_tls_extensions.go b/u_tls_extensions.go index 4ccdb85f..aca175d1 100644 --- a/u_tls_extensions.go +++ b/u_tls_extensions.go @@ -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: @@ -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: