Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 27 additions & 0 deletions u_common.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import (
"fmt"
"hash"
"log"
"slices"

"github.com/refraction-networking/utls/internal/helper"
"golang.org/x/crypto/cryptobyte"
Expand Down Expand Up @@ -572,9 +573,35 @@ func (chs *ClientHelloSpec) FromRaw(raw []byte, ctrlFlags ...bool) error {
}
}

// BoringSSL draws its GREASE ECH payload length afresh on every connection,
// while NSS derives one from the ClientHello and sends it every time, so a
// captured length is replayed as BoringSSL's draw only when the capture
// carries RFC 8701 GREASE values, which BoringSSL sends and NSS does not.
// The lengths alone cannot tell the two apart: Firefox's is 240 bytes, one
// of BoringSSL's four.
if chs.hasGREASE() {
for _, ext := range chs.Extensions {
if ech, ok := ext.(*GREASEEncryptedClientHelloExtension); ok {
ech.CandidatePayloadLens = slices.Clone(boringGREASEECHPayloadLens)
}
}
}

return nil
}

// hasGREASE reports whether the spec carries RFC 8701 GREASE values in its
// cipher suites or extensions.
func (chs *ClientHelloSpec) hasGREASE() bool {
if slices.ContainsFunc(chs.CipherSuites, isGREASEUint16) {
return true
}
return slices.ContainsFunc(chs.Extensions, func(ext TLSExtension) bool {
_, ok := ext.(*UtlsGREASEExtension)
return ok
})
}

// UnmarshalJSON unmarshals a ClientHello message in the form of JSON into a ClientHelloSpec.
func (chs *ClientHelloSpec) UnmarshalJSON(jsonB []byte) error {
var chsju ClientHelloSpecJSONUnmarshaler
Expand Down
15 changes: 12 additions & 3 deletions u_ech.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"fmt"
"io"
"math/big"
"slices"
"sync"

"github.com/refraction-networking/utls/dicttls"
Expand Down Expand Up @@ -49,7 +50,7 @@ type GREASEEncryptedClientHelloExtension struct {
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)
CandidatePayloadLens []uint16 // Pre-encryption. If empty, drawn from BoringSSL's 128, 160, 192 and 224 (+16 on the wire)
payload []byte // payload should be calculated ONCE and stored here, HRR will reuse this

initOnce sync.Once
Expand Down Expand Up @@ -153,7 +154,7 @@ func (g *GREASEEncryptedClientHelloExtension) init() error {

if len(g.payload) == 0 {
if len(g.CandidatePayloadLens) == 0 {
g.CandidatePayloadLens = []uint16{128}
g.CandidatePayloadLens = slices.Clone(boringGREASEECHPayloadLens)
}

// randomly pick one from the list
Expand Down Expand Up @@ -316,6 +317,14 @@ func (*UnimplementedECHExtension) mustEmbedUnimplementedECHExtension() {
panic("mustEmbedUnimplementedECHExtension() is not implemented")
}

// boringGREASEECHPayloadLens are the pre-encryption payload lengths BoringSSL
// draws a GREASE ECH payload from, uniformly on every connection: 32 bytes
// times a random integer in [4, 7], the size of a typical ClientHelloInner
// padded to a multiple of 32 (ssl/encrypted_client_hello.cc, setup_ech_grease).
// With the 16-byte AEAD tag the payload is 144, 176, 208 or 240 bytes on the
// wire.
var boringGREASEECHPayloadLens = []uint16{128, 160, 192, 224}

// BoringGREASEECH returns a GREASE scheme BoringSSL uses by default.
func BoringGREASEECH() *GREASEEncryptedClientHelloExtension {
return &GREASEEncryptedClientHelloExtension{
Expand All @@ -325,6 +334,6 @@ func BoringGREASEECH() *GREASEEncryptedClientHelloExtension {
AeadId: dicttls.AEAD_AES_128_GCM,
},
},
CandidatePayloadLens: []uint16{128, 160, 192, 224}, // +16: 144, 176, 208, 240
CandidatePayloadLens: slices.Clone(boringGREASEECHPayloadLens),
}
}
Loading