Skip to content

Preserve certificateMsgTLS13 original bytes for the TLS 1.3 transcript#8

Closed
sam-at-ava wants to merge 3 commits into
bogdanfinn:masterfrom
MeetAva:fix/certificate-tls13-original-bytes
Closed

Preserve certificateMsgTLS13 original bytes for the TLS 1.3 transcript#8
sam-at-ava wants to merge 3 commits into
bogdanfinn:masterfrom
MeetAva:fix/certificate-tls13-original-bytes

Conversation

@sam-at-ava

Copy link
Copy Markdown

Problem

TLS 1.3 handshakes fail at CertificateVerify with:

tls: invalid signature by the server certificate: crypto/rsa: verification error

against real production HTTPS endpoints — even though the certificate chain validates and the same server handshakes fine with crypto/tls and with a Chrome fingerprint. It's deterministic (not the proxy, not flaky) and reproducible on a direct connection.

Root cause

certificateMsgTLS13 does not implement handshakeMessageWithOriginalBytes. uTLS reads the server Certificate with readHandshake(nil) (to support RFC 8879 cert compression) and then hashes it into the transcript via transcriptMsg, whose fallback path re-marshals the parsed message with marshalCertificate(). That re-marshal is not guaranteed to reproduce the peer's byte encoding: only leaf OCSP/SCT are re-emitted (in a fixed order), any other per-certificate extension is dropped, and non-canonical length encodings are lost. When the server's Certificate doesn't round-trip exactly, the client transcript diverges from the server's, so the (correct) CertificateVerify signature fails against the client's wrong transcript hash.

Only the uncompressed-certificate path is affected; compressed certs already transcript their raw CompressedCertificate bytes (which is why a Chrome/brotli fingerprint dodges it).

Fix

clientHelloMsg, serverHelloMsg and certificateRequestMsgTLS13 already cache their original wire bytes for exactly this reason; certificateMsgTLS13 was the one TLS 1.3 transcript message left out. This PR captures original in unmarshal, returns it from originalBytes(), nils it in TestMarshalUnmarshal like the sibling messages, and adds a regression test (a Certificate whose leaf carries an extension marshalCertificate drops; asserts transcriptMsg hashes the original wire bytes, not the divergent re-marshal). go test ./... passes; the new test fails without the fix.

Also submitted upstream: refraction-networking/utls#409.

sam-at-ava and others added 3 commits July 21, 2026 11:38
certificateMsgTLS13 did not implement handshakeMessageWithOriginalBytes, so
transcriptMsg re-marshaled the parsed Certificate via marshalCertificate() to
feed the TLS 1.3 handshake transcript. That re-marshal is not byte-identical to
the server's encoding — leaf extensions other than OCSP/SCT are dropped, and
OCSP/SCT are re-emitted in a fixed order — so the client's transcript diverges
from the server's and CertificateVerify fails with:

    tls: invalid signature by the server certificate: crypto/rsa: verification error

clientHelloMsg, serverHelloMsg and certificateRequestMsgTLS13 already cache
their original wire bytes for exactly this reason; certificateMsgTLS13 was the
only TLS 1.3 transcript message missing it. Capture `original` in unmarshal and
return it from originalBytes(), matching the existing pattern. Only the
uncompressed-certificate path was affected; compressed certs already transcript
their raw CompressedCertificate bytes.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
certificateMsgTLS13 now caches its original wire bytes (like clientHelloMsg,
serverHelloMsg, certificateRequestMsgTLS13), so TestMarshalUnmarshal must nil
the field before DeepEqual, matching the existing cases.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@MeetAva MeetAva closed this by deleting the head repository Jul 22, 2026
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.

2 participants