Skip to content

fix: harden the tls/x509 parser against malformed peer input - #445

Merged
kacy merged 3 commits into
mainfrom
hardening/tls-parser-bounds
Jun 23, 2026
Merged

fix: harden the tls/x509 parser against malformed peer input#445
kacy merged 3 commits into
mainfrom
hardening/tls-parser-bounds

Conversation

@kacy

@kacy kacy commented Jun 23, 2026

Copy link
Copy Markdown
Owner

summary

first of a four-PR hardening pass (targeted, high-confidence). this one
covers the freshest, most exposed surface: the hand-written X.509/DER
parser and TLS handshake parsers that consume bytes from a remote peer
during the mTLS handshake. all changes are reject-the-malformed /
bound-the-unbounded — no behavior change for well-formed inputs.

security-critical crypto path → not marked auto-merge, matching the
Phase 8 posture.

what's in here

1. bounds-check every length-driven slice in the DER parser
the walker sliced body[pos .. pos + hdr.length] from attacker-controlled
length fields without checking the bound first in ~15 places. in
ReleaseSafe an out-of-bounds slice panics — so a malformed peer cert
could crash the handshake thread (remote DoS; not memory corruption —
Zig is bounds-checked). added a sliceChecked(buf, start, len) helper
(overflow-safe subtraction form) returning error.InvalidCert, and
routed every length-driven slice through it.

2. SAN handling fails closed
a cert with more SAN URIs than the cap silently dropped the rest — a
required identity URI past the cap would never match. raised the cap
8 → 32 and now reject on overflow instead of truncating.

3. bound the handshake read loops
both client (doHandshakeInner) and server (acceptClientAuthAndFinished)
reassembled the peer's handshake flight into a growing buffer with no cap
and no iteration limit — a malicious peer could stream records forever.
capped each side at 64 KB (real cert chains are a few KB).

4. fuzz-tls target
the TLS stack was the only attack-surface parser without a fuzz target.
zig build fuzz-tls drives x509_verify.parseDer + the handshake
message parsers on arbitrary bytes, asserting every returned slice stays
within the input and nothing panics.

tests

  • parseDer rejects truncations at every prefix length without panicking
    — slices the cert at every length and asserts an error, never a crash
  • parseDer rejects a cert whose inner length fields point past the buffer
    — perturbs each header/length byte; asserts no panic
  • fillSanUris fails closed when SAN count exceeds the cap
  • all 8 existing x509_verify round-trips + the mTLS handshake socketpair
    tests stay green

verification

  • YOQ_SKIP_SLOW_TESTS=1 zig build test — 2238 passed, 0 failed
  • zig build fuzz-tls — corpus pass green (43 cases incl. inline tests)
  • tools/panic_audit.sh clean — the bounds helper returns errors, never
    panics, so no new panic-shaped constructs
  • zig fmt --check clean

kacy added 3 commits June 23, 2026 01:24
the der walker sliced body[pos .. pos + hdr.length] from attacker-
controlled length fields without checking the bound first in ~15 spots.
in releasesafe an out-of-bounds slice panics — so a malformed peer cert
could crash the mtls handshake thread (remote dos).

add a sliceChecked(buf, start, len) helper that validates the bound
(overflow-safe subtraction form) and returns error.InvalidCert, and
route every length-driven slice in parseDer / parseTbsFields /
extractCnFromName / extractEcPointFromSpki / extractSanUrisFromExtensions
/ fillSanUris / extractFirstOid through it.

also fix SAN handling to fail closed: a cert with more SAN URIs than the
cap previously dropped the rest silently, so a required identity URI past
the cap would never match. raise the cap (8 -> 32) and reject on overflow
instead of truncating.

new tests: parseDer rejects every truncation prefix and single-byte
header corruptions without panicking; fillSanUris rejects an over-cap
SAN list. existing positive round-trips unchanged.
both the client (doHandshakeInner) and server (acceptClientAuthAndFinished)
reassemble the peer's handshake flight into a growing buffer with no cap
and no iteration limit — a malicious peer could stream handshake records
forever, exhausting memory. cap each side's flight at 64 KB (real cert
chains are a few KB) and fail the handshake past it.

also widen the san_buf call sites to x509_verify.max_san_uris now that
the cap moved to 32.
the tls stack was the only attack-surface parser without a fuzz target
(http/manifest/dns/cluster-msg/gossip-msg/wireguard already have one).
fuzz-tls drives x509_verify.parseDer and the handshake message parsers
on arbitrary bytes, asserting every returned slice stays within the
input and nothing panics.
@kacy
kacy merged commit 90ffc9a into main Jun 23, 2026
10 checks passed
@kacy
kacy deleted the hardening/tls-parser-bounds branch June 23, 2026 02:34
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