crypto/tls: omit PSK in ECH outer client hello When using ECH, do not include the PSK extension in the outer hello. Including the PSK extension allows for a degradation in privacy, as an on-path attacker can harvest outer client hellos, and then construct new hellos using the PSK extension and arbitrary guessed SNI values, replaying them to the target server. If the server rejects the PSK, the handshake will continue, but if the PSK is accepted, the binder check will fail. Thanks to Coia Prant (github.com/rbqvq) for reporting this issue. Fixes CVE-2026-42505 Fixes #79282 Change-Id: Ib3a3c948106a57c1b07b9e61a58cbf757848be18 Reviewed-on: https://go-review.googlesource.com/c/go/+/775960 Auto-Submit: Roland Shoemaker <roland@golang.org> TryBot-Bypass: Roland Shoemaker <roland@golang.org> Reviewed-by: Daniel McCarney <daniel@binaryparadox.net> Reviewed-by: Carlos Amedee <carlos@golang.org>
diff --git a/src/crypto/tls/handshake_messages.go b/src/crypto/tls/handshake_messages.go index aa0b7db..511e073 100644 --- a/src/crypto/tls/handshake_messages.go +++ b/src/crypto/tls/handshake_messages.go
@@ -5,6 +5,7 @@ package tls import ( + "bytes" "errors" "fmt" "slices" @@ -317,7 +318,8 @@ }) }) } - if len(m.pskIdentities) > 0 { // pre_shared_key must be the last extension + // pre_shared_key must be the last extension + if len(m.pskIdentities) > 0 && (echInner || len(m.encryptedClientHello) == 0 || bytes.Equal(m.encryptedClientHello, []byte{byte(innerECHExt)})) { // RFC 8446, Section 4.2.11 exts.AddUint16(extensionPreSharedKey) exts.AddUint16LengthPrefixed(func(exts *cryptobyte.Builder) {
diff --git a/src/crypto/tls/handshake_messages_test.go b/src/crypto/tls/handshake_messages_test.go index aa4659a..d025ce8 100644 --- a/src/crypto/tls/handshake_messages_test.go +++ b/src/crypto/tls/handshake_messages_test.go
@@ -215,7 +215,16 @@ case 2: m.pskModes = []uint8{pskModeDHE, pskModePlain} } - for i := 0; i < rand.Intn(5); i++ { + // clientHelloMsg.marshal uses echInner == false. If m.encryptedClientHello > 0 and + // does not equal []byte{innerECHExt}, then the psk extension will be omitted, + // so either only emit empty encryptedClientHello and psk, encryptedClientHello with + // []byte{innerECHExt} and psk, or random encryptedClientHello and no psk. + if rand.Intn(10) > 5 { + m.encryptedClientHello = randomBytes(rand.Intn(50)+1, rand) + } else { + if rand.Intn(10) > 5 { + m.encryptedClientHello = []byte{byte(innerECHExt)} + } var psk pskIdentity psk.obfuscatedTicketAge = uint32(rand.Intn(500000)) psk.label = randomBytes(rand.Intn(500)+1, rand) @@ -228,9 +237,6 @@ if rand.Intn(10) > 5 { m.earlyData = true } - if rand.Intn(10) > 5 { - m.encryptedClientHello = randomBytes(rand.Intn(50)+1, rand) - } return reflect.ValueOf(m) } @@ -579,3 +585,88 @@ t.Fatal("Unmarshaled ServerHello with duplicate extensions") } } + +func TestECHRemoveOuterPSK(t *testing.T) { + r := rand.New(rand.NewSource(0)) + + for _, tc := range []struct { + name string + echInner bool + echExt []byte + expectRemoved bool + }{ + { + name: "echInner true", + echInner: true, + expectRemoved: false, + }, + { + name: "echInner true, no ech ext", + echInner: true, + expectRemoved: false, + }, + { + name: "echInner true, ech ext present", + echInner: true, + echExt: []byte{254}, + expectRemoved: false, + }, + { + name: "echInner true, ech ext present, inner ech sentinel", + echInner: true, + echExt: []byte{byte(innerECHExt)}, + expectRemoved: false, + }, + { + name: "echInner false, no ech ext", + echInner: false, + expectRemoved: false, + }, + { + name: "echInner false, ech ext present", + echInner: false, + echExt: []byte{254}, + expectRemoved: true, + }, + { + name: "echInner false, ech ext present, inner ech sentinel", + echInner: false, + echExt: []byte{byte(innerECHExt)}, + expectRemoved: false, + }, + } { + t.Run(tc.name, func(t *testing.T) { + ch := (&clientHelloMsg{}).Generate(r, 0).Interface().(*clientHelloMsg) + + ch.pskBinders = [][]byte{[]byte("test")} + ch.pskIdentities = []pskIdentity{{label: []byte("test")}} + ch.encryptedClientHello = tc.echExt + + b, err := ch.marshalMsg(tc.echInner) + if err != nil { + t.Fatal(err) + } + var rch clientHelloMsg + if !rch.unmarshal(b) { + t.Fatal("Failed to unmarshal ClientHello") + } + + if tc.expectRemoved { + if rch.pskIdentities != nil { + t.Error("expected PSK identities to be removed") + } + if rch.pskBinders != nil { + t.Error("expected PSK binders to be removed") + } + } else { + if rch.pskIdentities == nil { + t.Error("expected PSK identities to be present") + } + if rch.pskBinders == nil { + t.Error("expected PSK binders to be present") + } + } + }) + } + +}
diff --git a/src/crypto/tls/tls_test.go b/src/crypto/tls/tls_test.go index d1f229b..6fe4490 100644 --- a/src/crypto/tls/tls_test.go +++ b/src/crypto/tls/tls_test.go
@@ -2678,6 +2678,7 @@ clientConfig.RootCAs.AddCert(secretCert) clientConfig.RootCAs.AddCert(publicCert) clientConfig.EncryptedClientHelloConfigList = echConfigList + clientConfig.ClientSessionCache = NewLRUClientSessionCache(2) serverConfig.InsecureSkipVerify = false serverConfig.Time = nil serverConfig.MinVersion = VersionTLS13