crypto/tls: skip unsupported ECH config versions

When we encounter an ECHConfig structure with an unsupported version,
the RFC 9849 section 4 text indicates we MUST ignore it. The
parseECHConfig helper returns a skip boolean when this case is hit, but
previously processECHClientHello treated this as equivalent to a non-nil
error return, sending an alert and terminating the handshake.

Instead we should handle the nil error true skip case by continuing to
try the next available echKeys entry, ignoring the unsupported version
entry. If we exhaust all available echKeys without finding a supported
one, we will not accept ECH as expected.

Change-Id: Id0a21c48b472756ad27a028be4d8422c1e9dd3ef
Reviewed-on: https://go-review.googlesource.com/c/go/+/771461
LUCI-TryBot-Result: golang-scoped@luci-project-accounts.iam.gserviceaccount.com <golang-scoped@luci-project-accounts.iam.gserviceaccount.com>
Auto-Submit: Daniel McCarney <daniel@binaryparadox.net>
Reviewed-by: Carlos Amedee <carlos@golang.org>
Reviewed-by: Roland Shoemaker <roland@golang.org>
This commit is contained in:
Daniel McCarney 2026-04-28 10:56:31 -04:00 committed by Gopher Robot
parent be9da6ce60
commit 10b5baca54
2 changed files with 22 additions and 5 deletions

View file

@ -572,7 +572,7 @@ func (c *Conn) processECHClientHello(outer *clientHelloMsg, echKeys []EncryptedC
for _, echKey := range echKeys {
skip, config, err := parseECHConfig(echKey.Config)
if err != nil || skip {
if err != nil {
c.sendAlert(alertInternalError)
return nil, nil, fmt.Errorf("tls: invalid EncryptedClientHelloKey Config: %s", err)
}

View file

@ -2333,9 +2333,9 @@ func TestECH(t *testing.T) {
t.Fatal(err)
}
marshalECHConfig := func(id uint8, pubKey []byte, publicName string, maxNameLen uint8) []byte {
marshalECHConfig := func(version uint16, id uint8, pubKey []byte, publicName string, maxNameLen uint8) []byte {
builder := cryptobyte.NewBuilder(nil)
builder.AddUint16(extensionEncryptedClientHello)
builder.AddUint16(version)
builder.AddUint16LengthPrefixed(func(builder *cryptobyte.Builder) {
builder.AddUint8(id)
builder.AddUint16(0x0020 /* DHKEM(X25519, HKDF-SHA256) */)
@ -2361,7 +2361,7 @@ func TestECH(t *testing.T) {
t.Fatal(err)
}
echConfig := marshalECHConfig(123, echKey.PublicKey().Bytes(), "public.example", 32)
echConfig := marshalECHConfig(extensionEncryptedClientHello, 123, echKey.PublicKey().Bytes(), "public.example", 32)
builder := cryptobyte.NewBuilder(nil)
builder.AddUint16LengthPrefixed(func(builder *cryptobyte.Builder) {
@ -2426,12 +2426,29 @@ func TestECH(t *testing.T) {
if err != nil {
t.Fatal(err)
}
randConfig := marshalECHConfig(32, randKey.PublicKey().Bytes(), "random.example", 32)
randConfig := marshalECHConfig(extensionEncryptedClientHello, 32, randKey.PublicKey().Bytes(), "random.example", 32)
serverConfig.EncryptedClientHelloKeys = []EncryptedClientHelloKey{
{Config: randConfig, PrivateKey: randKey.Bytes(), SendAsRetry: true},
}
check()
// A server configured with an unsupported-version ECHConfig ahead of a
// usable one must skip the unusable entry (per RFC 9849 §4) and
// trial-decrypt against the next key, rather than aborting the handshake
// on the first entry.
unsupportedKey, err := ecdh.X25519().GenerateKey(rand.Reader)
if err != nil {
t.Fatal(err)
}
unsupportedConfig := marshalECHConfig(0xbadd, 99, unsupportedKey.PublicKey().Bytes(), "public.example", 32)
serverConfig.GetEncryptedClientHelloKeys = nil
serverConfig.EncryptedClientHelloKeys = []EncryptedClientHelloKey{
{Config: unsupportedConfig, PrivateKey: unsupportedKey.Bytes(), SendAsRetry: true},
{Config: echConfig, PrivateKey: echKey.Bytes(), SendAsRetry: true},
}
check()
}
func TestMessageSigner(t *testing.T) {