Reproducible experiments behind the IAB workshop position paper "TLS 1.3 never tells the server what a resumed connection must re-check" (León Acosta, QuantaKrypto).
Each experiment asks one question: when a TLS 1.3 session is resumed, does the endpoint re-establish a condition that the original full handshake evaluated? Certificate expiry, trust-anchor membership, and client-authentication scope.
Every directory has a gen.sh that makes throwaway certificates and a run.sh that runs the
experiment and prints a verdict. No key material is committed; run gen.sh first.
cd mtls-openssl && ./gen.sh && ./run.sh # server side, expired client certificate
cd firefox-client && ./nonbrowser.sh # client side, expired server certificate
cd nginx-vhost-scope && ./gen.sh && ./run.sh # CVE-2025-23419 reproduction attempt
cd haproxy && ./gen.sh && ./run.sh # cross-frontend resumption
cd go-crypto-tls && go run . # expiry and trust-anchor membership
cd public-endpoints && python3 probe.py --dry-run # inspect the panel, sends nothing
cd public-endpoints && python3 probe.py # measure 14 public endpoints
cd public-endpoints/ticket-prompting && ./run.sh # the same panel, asked and not askedRequirements: OpenSSL 3.x (the tests were run on 3.6.3; the system LibreSSL will not do),
nginx, HAProxy, Go 1.24+, Python 3. On macOS, brew install openssl@3 nginx haproxy go.
Two rules are applied throughout, because breaking either produces a result that looks like evidence and is not.
Every experiment has a control. A test that only shows "the connection was accepted" cannot
distinguish a real finding from a server that was never enforcing anything. The server-side
OpenSSL harness must be run with -Verify 1 -verify_return_error; with -Verify 1 alone the
control connection passes and the whole experiment proves nothing.
Every negative result has a second control proving the question was actually posed. Where a test looks for a scope bypass and does not find one, it then replays the same ticket at the host that minted it. If that resumes, the ticket was valid and offerable, and the earlier full handshake was the implementation declining it. Without that step, "not vulnerable" and "broken test" are indistinguishable.
See RESULTS.md for the full write-up with versions, dates and raw output.
| provider | version | finding |
|---|---|---|
| OpenSSL server | 3.6.3 | resumption accepts an expired client certificate; full handshake rejects it |
| OpenSSL client | 3.6.3 | resumption accepts an expired server certificate; full handshake rejects it |
| nginx | 1.31.4 | cross-vhost client-auth scope not reproducible, CVE-2025-23419 fix holds |
| Go crypto/tls | 1.27.0 | re-establishes certificate expiry and trust-anchor membership on resumption |
| HAProxy | 3.4.3 | inconclusive: separate bind lines are already isolated, so this configuration does not exercise what the August 2026 series changes |
| 14 public endpoints | 27 Aug 2026 | 14/14 issue a ticket, 13/14 resume, 11/14 already negotiate post-quantum hybrid key exchange |
| the same panel, silent | 28 Aug 2026 | 8/14 issue a ticket when the connection sends nothing; all 14 do after one HEAD / |
mtls-openssl/ server accepting a resumed session carrying an expired client certificate
firefox-client/ client accepting a resumed session past server certificate expiry
nginx-vhost-scope/ two server{} blocks, one certificate, differing only in ssl_verify_client
haproxy/ two frontends, one certificate, differing only in verify required
go-crypto-tls/ one Go program: expiry, then trust-anchor removal, with baselines
public-endpoints/ 14 public hosts: chain, ticket issuance, resumption, PQ key exchange
ticket-prompting/ the same panel run twice, silent and with one HEAD: 8 of 14 against 14 of 14
Certificates generated by these scripts are throwaway, live for seconds to hours, and are deliberately not committed.