mirror of
https://github.com/refraction-networking/utls.git
synced 2025-03-31 10:37:36 +03:00
feat: parse GREASE ECH from raw (#276)
This commit is contained in:
parent
f8beb04616
commit
42e79cb29a
4 changed files with 194 additions and 6 deletions
|
@ -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 +
|
||||
|
|
82
u_ech.go
82
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{
|
||||
|
|
103
u_ech_test.go
Normal file
103
u_ech_test.go
Normal file
|
@ -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,
|
||||
},
|
||||
}
|
||||
)
|
|
@ -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:
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue