Skip to content

Preserve TLS 1.3 Certificate transcript bytes - #1

Open
hiroTamada wants to merge 3 commits into
masterfrom
hypeship/preserve-cert-transcript
Open

Preserve TLS 1.3 Certificate transcript bytes#1
hiroTamada wants to merge 3 commits into
masterfrom
hypeship/preserve-cert-transcript

Conversation

@hiroTamada

@hiroTamada hiroTamada commented Aug 11, 2026

Copy link
Copy Markdown

Summary

Preserve the exact wire bytes of TLS 1.3 Certificate messages for handshake transcript hashing.

uTLS currently parses and then re-marshals these messages before adding them to the transcript. Unknown per-certificate extensions can be dropped during that round trip, causing CertificateVerify to fail against an otherwise valid server signature.

This change:

  • retains the original Certificate message bytes during parsing;
  • makes transcriptMsg hash those bytes instead of a lossy re-marshal;
  • adds unit and full-handshake regression coverage for an unknown CertificateEntry extension;
  • renames the module and self-imports to github.com/kernel/utls so Kernel can depend on the fork directly.

Unknown extensions remain ignored semantically. Certificate and signature verification are not weakened.

Upstream

The protocol fix is also proposed in refraction-networking/utls#409. The temporary v1.8.2-kernel.1 release applies the same fix to upstream v1.8.2 for the current downstream pin.

Tests

  • go test ./...
  • the recorded TLS 1.3 handshake completes certificate verification and application-data exchange; the same fixture fails on unpatched v1.8.2 with tls: invalid signature by the server certificate

Note

Medium Risk
Changes TLS 1.3 handshake transcript construction (security-sensitive), but only to hash the peer’s bytes and fix verification failures; verification semantics are not relaxed.

Overview
Fixes TLS 1.3 handshakes that fail CertificateVerify when the server’s Certificate message includes per-entry extensions that are not re-emitted on marshal (only OCSP/SCT are preserved today).

certificateMsgTLS13 now keeps the exact received bytes and exposes originalBytes() so transcriptMsg hashes the peer encoding instead of a lossy re-marshal—matching the existing pattern for ClientHello, ServerHello, and CertificateRequest. Round-trip marshal/unmarshal tests clear the new original field like the other message types.

Regression coverage adds a wire fixture with an unknown leaf CertificateEntry extension plus unit tests for transcript hashing and a full client replay handshake.

The Go module and imports are renamed from github.com/refraction-networking/utls to github.com/kernel/utls so downstream can depend on this fork directly.

Reviewed by Cursor Bugbot for commit 7bc214f. Bugbot is set up for automated code reviews on this repo. Configure here.

sam-at-ava and others added 2 commits August 11, 2026 00:28
certificateMsgTLS13 does not implement handshakeMessageWithOriginalBytes, so
transcriptMsg re-marshals the parsed Certificate via marshalCertificate() when
feeding the TLS 1.3 handshake transcript. That re-marshal is not guaranteed to
be byte-identical to the peer's encoding: only leaf OCSP/SCT are re-emitted (in
a fixed order), and any other per-certificate extension or non-canonical length
encoding is lost. When a server's Certificate message doesn't round-trip
exactly, the client's transcript diverges from the server's and the handshake
fails at CertificateVerify with:

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

This is reproducible against real production HTTPS endpoints whose leaf
certificate message carries an extension beyond OCSP/SCT. It only affects the
uncompressed-certificate path; compressed certificates already transcript their
raw CompressedCertificate bytes.

clientHelloMsg, serverHelloMsg and certificateRequestMsgTLS13 already cache
their original wire bytes for exactly this reason (added in the June 2025
originalBytes work); certificateMsgTLS13 was the one TLS 1.3 transcript message
left out. Capture `original` in unmarshal and return it from originalBytes(),
and nil it in the TestMarshalUnmarshal round-trip like the sibling messages.

Adds a regression test that builds a Certificate whose leaf carries an extension
marshalCertificate drops, and asserts transcriptMsg hashes the original wire
bytes rather than the (divergent) re-marshal.
@hiroTamada
hiroTamada marked this pull request as ready for review August 11, 2026 00:32
@hiroTamada
hiroTamada requested review from rgarcia and ulziibay-kernel and removed request for rgarcia August 11, 2026 00:33
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.

3 participants