Skip to content

Constant-time secret comparison + stdlib-reuse audit (Tier 1+2)#28

Merged
endel merged 3 commits into
mainfrom
fix/constant-time-secret-compare
May 18, 2026
Merged

Constant-time secret comparison + stdlib-reuse audit (Tier 1+2)#28
endel merged 3 commits into
mainfrom
fix/constant-time-secret-compare

Conversation

@endel
Copy link
Copy Markdown
Owner

@endel endel commented May 18, 2026

Summary

Whole-codebase audit for functionality that duplicates / misuses Zig's standard library. The headline is security, not tidiness.

Tier 1 — Security (commit de72983)

Six authentication secrets were compared with std.mem.eql, which short-circuits on the first differing byte — a timing side-channel. Zig's stdlib already provides the correct primitive; this switches them to std.crypto.timing_safe.eql. Pre-existing; unrelated to PR #27. Every QUIC/TLS reference stack (BoringSSL, rustls, quic-go, quiche) compares these in constant time — this closes a conformance + security gap.

Site Secret Spec
quic/stateless_reset.zig stateless reset token RFC 9000 §10.3
quic/packet.zig Retry integrity AEAD tag RFC 9001 §5.8
quic/tls13.zig ×2 Finished verify_data (client+server) RFC 8446 §4.4.4
quic/tls13.zig PSK binder HMAC RFC 8446 §4.2.11.2
http1/tls.zig Finished verify_data RFC 8446 §4.4.4

Length is public (fixed by spec/cipher-suite), so a public length check precedes the constant-time array compare; wrong-length rejection semantics preserved. Two of the six (http1/tls.zig, packet.zig) were missed by a file-scoped pass and only caught by the whole-codebase sweep.

Tier 2 — stdlib reuse (commit 12917aa)

  • stream.zig: two hand-rolled descending insertion sorts → std.sort.insertion (stable; same equal-key semantics).
  • transport_params.zig: bigToNative(u16, @bitCast([2]u8))std.mem.readInt(u16, &bytes, .big).

Deliberately not changed (evaluated)

qlog.zig hand-rolled JSON (justified: streaming JSON-SEQ, no allocation — std.json is DOM/heavier), ack_handler.zig thin ArrayList wrappers (named-type readability; removal ripples for marginal gain), sys.zig/io_compat.zig (deliberate Zig 0.16 shims), sockaddr.zig (POSIX syscall adapters), MoQ/QPACK varints + Huffman (RFC-specified, no stdlib equivalent). The codebase is otherwise disciplined.

Verification

  • zig build test --summary all: 522/522 at every commit
  • zig build: client/server clean
  • Real-Chrome WebTransport full TLS 1.3 handshake (exercises server-side Finished verification) + bidi/datagram echo via Puppeteer, post-Tier-1 and post-Tier-2

🤖 Generated with Claude Code

endel added 3 commits May 18, 2026 10:04
RFC 9218 §4.2 default is non-incremental (sequential). Flip the
SendStream default and have WebTransport bidi/uni opens explicitly
set incremental=true, since WT streams have no defined inter-stream
ordering and round-robin interleaving matches the quicperf path.

Browser demo: allow empty cert hash to rely on CA trust, and use
datagrams.createWritable() fallback for the Safari 26.4 API change.

Assisted-by: Claude Opus 4.7
Six authentication secrets were compared with std.mem.eql, which
short-circuits on the first differing byte — a timing side-channel.
Zig's stdlib already provides the correct primitive
(std.crypto.timing_safe.eql); use it directly. Every QUIC/TLS
reference stack (BoringSSL, rustls, quic-go, quiche) compares
these in constant time; this closes a conformance + security gap.
Pre-existing, unrelated to the std.crypto.tls dedup branch.

Sites (length is public/spec-fixed, only the value is secret, so
a public length check precedes the constant-time array compare):

- quic/stateless_reset.zig  stateless reset token  (RFC 9000 §10.3)
- quic/packet.zig           Retry integrity tag    (RFC 9001 §5.8)
- quic/tls13.zig  ×2        Finished verify_data   (RFC 8446 §4.4.4)
- quic/tls13.zig            PSK binder HMAC        (RFC 8446 §4.2.11.2)
- http1/tls.zig             Finished verify_data   (RFC 8446 §4.4.4)

Wrong-length rejection semantics preserved. Tests 522/522,
client/server build clean, and a real-Chrome WebTransport full
TLS 1.3 handshake (exercising server-side Finished verification)
+ bidi/datagram echo verified via Puppeteer.

Assisted-by: Claude Opus 4.7
Tier 2 of the stdlib-reuse audit (clear, low-risk):

- stream.zig: two hand-rolled descending insertion sorts for
  send_order scheduling replaced with std.sort.insertion (stable;
  equal send_order keeps arrival order, matching prior semantics).
- transport_params.zig: preferred_address port decode used
  std.mem.bigToNative(u16, @bitcast([2]u8)) -- spell it as the
  authoritative std.mem.readInt(u16, &bytes, .big).

(Skipped as borderline: the symmetric write-side
toBytes(nativeToBig(...)) -- already pure-stdlib, and switching
hinges on the writer type exposing writeInt; not worth the churn.)

Tests 522/522, client/server build clean.

Assisted-by: Claude Opus 4.7
@endel endel merged commit a80e10e into main May 18, 2026
2 checks passed
@endel endel deleted the fix/constant-time-secret-compare branch May 18, 2026 17:38
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