Skip to content

fix(ech): verify the public name on rejection and give the callback the chain - #429

Open
fabifont wants to merge 2 commits into
refraction-networking:masterfrom
fabifont:fix/ech-rejection-verification
Open

fabifont wants to merge 2 commits into
refraction-networking:masterfrom
fabifont:fix/ech-rejection-verification

Conversation

@fabifont

Copy link
Copy Markdown

When the server rejects ECH, the client must authenticate it for the ECH configuration's public_name to obtain retry_configs (RFC 9849, section 6.1.6). verifyServerCertificate's echRejected branch instead verifies the presented certificate against the inner name in Config.ServerName. The two names almost always differ, so a correctly behaving client-facing server, whose certificate covers only the public name, always fails with a hostname mismatch and the *ECHRejectionError carrying retry_configs is never returned. crypto/tls verifies against c.serverName, which holds the public name; uTLS replaced it with the config name when InsecureServerNameToVerify was added. This is the same defect #407 fixes, with the same one-line change.

Separately, EncryptedClientHelloRejectionVerify is called before c.peerCertificates is set, so the callback receives a ConnectionState with an empty PeerCertificates and cannot verify the provider certificate as its documentation says. crypto/tls has the same ordering (golang/go#75242, open). The second commit records the chain before invoking the callback and documents what it receives; VerifiedChains stays empty since the callback replaces verification.

Both ClientHello builders set c.serverName to the public name (the Go path from the ECH config, the custom spec path through the SNI extension in ApplyPreset), and the new tests run each case against both. The existing TestTLS13ECHRejectionCallbacks did not catch either defect because it sets the inner name equal to the public name; it is left unchanged, with its ECHConfigList literal moved into a named constant.

Verified with Go 1.27.3: go test ./..., go test -race on the ECH tests, go vet, gofmt -s, go fix -diff, staticcheck and golangci-lint report nothing.

Refs #407

When the server rejects ECH it answers the outer ClientHello, so the
certificate it presents is for the ECH configuration's public_name, and
the client must verify it against that name to obtain retry_configs
(RFC 9849, section 6.1.6). verifyServerCertificate instead used the
inner name from Config.ServerName, which almost always differs, so a
correctly behaving client-facing server failed verification with a
hostname mismatch and *ECHRejectionError was never returned. crypto/tls
verifies against c.serverName, which holds the public name; uTLS had
replaced it with the config name while adding InsecureServerNameToVerify.

Both ClientHello builders set c.serverName to the public name: the Go
path from the ECH config, and the custom spec path through the SNI
extension in ApplyPreset. InsecureServerNameToVerify still overrides
the name when set.

Refs: refraction-networking#407
EncryptedClientHelloRejectionVerify exists so that a client can verify
the ECH provider's certificate itself when the server rejects ECH, but
it was invoked before the chain was recorded on the connection, so the
ConnectionState it received had an empty PeerCertificates and the
callback had nothing to verify. crypto/tls has the same ordering,
tracked in golang/go#75242.

Record the chain before calling the callback. VerifiedChains stays
empty, since the callback replaces verification, and the field's
documentation now states what the callback receives.

Refs: golang/go#75242
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant