Skip to content
Permalink

Comparing changes

Choose two branches to see what’s changed or to start a new pull request. If you need to, you can also or learn more about diff comparisons.

Open a pull request

Create a new pull request by comparing changes across two branches. If you need to, you can also . Learn more about diff comparisons here.
base repository: refraction-networking/utls
Failed to load repositories. Confirm that selected base ref is valid, then try again.
Loading
base: v1.6.0
Choose a base ref
...
head repository: refraction-networking/utls
Failed to load repositories. Confirm that selected head ref is valid, then try again.
Loading
compare: v1.6.1
Choose a head ref
  • 3 commits
  • 6 files changed
  • 2 contributors

Commits on Dec 19, 2023

  1. build(deps): bump golang.org/x/crypto from 0.14.0 to 0.17.0 (#273)

    Bumps [golang.org/x/crypto](https://github.com/golang/crypto) from 0.14.0 to 0.17.0.
    - [Commits](golang/crypto@v0.14.0...v0.17.0)
    
    ---
    updated-dependencies:
    - dependency-name: golang.org/x/crypto
      dependency-type: direct:production
    ...
    
    Signed-off-by: dependabot[bot] <support@github.com>
    Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
    dependabot[bot] authored Dec 19, 2023
    Copy the full SHA
    f8beb04 View commit details

Commits on Dec 22, 2023

  1. Copy the full SHA
    42e79cb View commit details

Commits on Jan 8, 2024

  1. build(deps): bump github.com/cloudflare/circl from 1.3.6 to 1.3.7 (#277)

    Bumps [github.com/cloudflare/circl](https://github.com/cloudflare/circl) from 1.3.6 to 1.3.7.
    - [Release notes](https://github.com/cloudflare/circl/releases)
    - [Commits](cloudflare/circl@v1.3.6...v1.3.7)
    
    ---
    updated-dependencies:
    - dependency-name: github.com/cloudflare/circl
      dependency-type: direct:production
    ...
    
    Signed-off-by: dependabot[bot] <support@github.com>
    Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
    dependabot[bot] authored Jan 8, 2024
    Copy the full SHA
    8b9a63f View commit details
Showing with 206 additions and 18 deletions.
  1. +4 −4 go.mod
  2. +8 −8 go.sum
  3. +8 −1 u_conn.go
  4. +79 −3 u_ech.go
  5. +103 −0 u_ech_test.go
  6. +4 −2 u_tls_extensions.go
8 changes: 4 additions & 4 deletions go.mod
Original file line number Diff line number Diff line change
@@ -9,12 +9,12 @@ retract (

require (
github.com/andybalholm/brotli v1.0.5
github.com/cloudflare/circl v1.3.6
github.com/cloudflare/circl v1.3.7
github.com/klauspost/compress v1.16.7
github.com/quic-go/quic-go v0.37.4
golang.org/x/crypto v0.14.0
golang.org/x/crypto v0.17.0
golang.org/x/net v0.17.0
golang.org/x/sys v0.13.0
golang.org/x/sys v0.15.0
)

require golang.org/x/text v0.13.0 // indirect
require golang.org/x/text v0.14.0 // indirect
16 changes: 8 additions & 8 deletions go.sum
Original file line number Diff line number Diff line change
@@ -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.6 h1:/xbKIqSHbZXHwkhbrhrt2YOHIwYJlXH94E3tI/gDlUg=
github.com/cloudflare/circl v1.3.6/go.mod h1:5XYMA4rFBvNIrhs50XuiBJ15vF2pZn4nnUKZrLbUZFA=
github.com/cloudflare/circl v1.3.7 h1:qlCDlTPz2n9fu58M0Nh1J/JzcFpfgkFHHX3O35r5vcU=
github.com/cloudflare/circl v1.3.7/go.mod h1:sRTcRWXGLrKw6yIGJ+l7amYJFfAXbZG0kBSc8r4zxgA=
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=
@@ -12,13 +12,13 @@ github.com/onsi/ginkgo/v2 v2.9.5 h1:+6Hr4uxzP4XIUyAkg61dWBw8lb/gc4/X5luuxN/EC+Q=
github.com/onsi/gomega v1.27.6 h1:ENqfyGeS5AX/rlXDd/ETokDz93u0YufY1Pgxuy/PvWE=
github.com/quic-go/quic-go v0.37.4 h1:ke8B73yMCWGq9MfrCCAw0Uzdm7GaViC3i39dsIdDlH4=
github.com/quic-go/quic-go v0.37.4/go.mod h1:YsbH1r4mSHPJcLF4k4zruUkLBqctEMBDR6VPvcYjIsU=
golang.org/x/crypto v0.14.0 h1:wBqGXzWJW6m1XrIKlAH0Hs1JJ7+9KBwnIO8v66Q9cHc=
golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4=
golang.org/x/crypto v0.17.0 h1:r8bRNjWL3GshPW3gkd+RpvzWrZAwPS49OmTGZ/uhM4k=
golang.org/x/crypto v0.17.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4=
golang.org/x/net v0.17.0 h1:pVaXccu2ozPjCXewfr1S7xza/zcXTity9cCdXQYSjIM=
golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE=
golang.org/x/sys v0.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE=
golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/text v0.13.0 h1:ablQoSUd0tRdKxZewP80B+BaqeKJuVhuRxj/dkrun3k=
golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
golang.org/x/sys v0.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc=
golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/tools v0.9.1 h1:8WMNJAz3zrtPmnYC7ISf5dEn3MT0gY7jBJfw27yrrLo=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
9 changes: 8 additions & 1 deletion u_conn.go
Original file line number Diff line number Diff line change
@@ -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 changes: 79 additions & 3 deletions u_ech.go
Original file line number Diff line number Diff line change
@@ -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 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
@@ -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: