v2.4.1
A dependency release: zssl moves from 992ee9d to b6a6553, 83 commits.
Nothing about zrk's interface changes — no new flags, no output changes, no
JSON fields added or renamed. What moves is the TLS underneath: a full
handshake costs about a third less CPU, and a post-handshake record carrying
two messages no longer fails the connection.
The handshake got cheaper
Measured here rather than inherited from zssl's benchmark, because zssl's
number is a library microbenchmark and zrk's question is what a request
costs.
Against a loopback TLS 1.3 server with a P-256 ECDSA leaf and full chain
verification on, in --disable-keepalive mode so every request pays for its
own handshake:
| user CPU per request | |
|---|---|
| 2.4.0 | 956.8 µs |
| 2.4.1 | 644.4 µs |
312 µs per handshake, a 32.7% cut. Three 20-second runs per build,
alternating, -R 400 -c 32 -t 2, about 22,500 requests each; user CPU is the
zrk process's own, so the server's cost is not in it.
Almost all of it is one change. zssl's verifyEcdsa ran on
std.crypto.sign.ecdsa and now calls libcrypto: on zssl's bench machine Zig's
P-256 verifier is 333.8 µs against libcrypto's 44.6 µs, which was two thirds of
a full handshake. The rest is zssl's sans-I/O boundary moving. zssl's own
figure for the pair is handshake_full going 495 µs to 175.26 µs, and the
312 µs measured here is what survives of that inside a real request.
Two limits on that number. It only appears where a handshake is per-request —
a keepalive run does one handshake per connection and does not move
measurably — and it only becomes throughput where zrk is the bottleneck rather
than the target. On the same loopback server a closed-loop ceiling went 659 to
896 req/s, but both runs logged errors because the Python server was saturated,
so read that as a direction and not as a number.
Servers with an RSA leaf see less of this: the RSA verification path was
already in libcrypto.
One record, more than one message
§5.1 lets a record hold several handshake messages, and both Go and OpenSSL
pack a NewSessionTicket with a KeyUpdate. The old zssl returned exactly one
Event per record and then refused a non-empty assembler, so the second
message came back UnexpectedMessage and zrk dropped the connection —
legal traffic from two of the most common servers there are, counted as an
error.
BoringSSL's BoGo runner found it as zssl's finding 1. handleRecord now
returns ?Event and a new drain yields the rest of what a record carried;
zrk's two record loops — the handshake and the application read — drain until
null. A run of KeyUpdate requests is answered once now instead of once per
message.
This one rests on zssl's gate rather than on a zrk reproduction. The OpenSSL
server on hand sends its tickets in separate records, so the packing that
triggers it was not reachable locally.
What else the bump carries
zssl was, at the old pin, a stack that interoperated with OpenSSL and
std.crypto.tls and said of itself that it "has not been adversarially
tested". At the new one it runs under three hostile corpora: BoGo (324
passing), tlsfuzzer (22 of 62 scripts, driving zssl's server half), and
TLS-Anvil (115 passing). Still no external audit.
The client-side fixes those gates produced, all of which zrk now has: extensions
we never offered are refused; §5.4's cap on the inner plaintext is enforced;
empty records and KeyUpdates are bounded rather than unbounded; a record
interleaved with a handshake fragment is refused; so is a length the message
type cannot have; a record refused at its header now sends the alert it owes;
duplicate extensions are refused as a pre-pass; the ChangeCipherSpec payload is
read instead of assumed; user_canceled is ignored and bounded while other
warning alerts are refused; and a ticket's lifetime is enforced against a
supplied clock.
What this release does not take up
zssl's client can answer a HelloRetryRequest now, but only when the embedder
supplies Config.retry_key_share_private — the library will not invent a key.
zrk does not supply one, so it still refuses a retry exactly as 2.4.0 did, and
a server that insists on a group zrk did not share still fails the handshake.
zrk offers x25519 only, so that remains reachable.
Likewise 0-RTT: zssl can offer early data now, and zrk offers none.
Both are zrk-side work and neither is in this release.
Verified
267 tests pass and zig fmt --check is clean. Live runs against example.com,
cloudflare.com and go.dev — HTTP/1.1 and h2, and a Go server for the
post-handshake path — completed with errs=0.
No tests were added. The change is a pin and its two call sites, and driving a
packed record through them needs a TLS peer harness zrk does not have; zssl
gates that behaviour on its own side, which is where the code is.