A cross-stack difference in what ReplaceCertificates can do, which matters because the same operational runbook works on one stack and silently does nothing on the other.
On TCP, anchors given as TlsOptions.ClientCaPath are re-read from that path on every rotation. That is deliberate and documented, and it is how an issuer is revoked without a restart — RotationTests pins both directions ("trust anchors are re-read, so who may connect can be changed", "an anchor file emptied under a rotation stops admitting anyone").
On QUIC the verifier is built once, in iq_engine_new_mtls, and every certificate generation points at that same verifier. QuicEngine.ReplaceCertificates marshals only certificate and key paths — no CA — so anchors cannot be rotated at all. Both behaviours are now documented, but the gap is real: an operator who revokes an issuer by editing the bundle and rotating gets what they asked for on TCP and a no-op on QUIC, with nothing said.
Two ways out, and the choice is a design decision rather than a fix:
- extend
ReplaceCertificates (or add a sibling) to rebuild the verifier, which means the same build-then-publish care the certificate generations already get, since picotls keeps reading the verifier for the life of a connection;
- or leave it and make the asymmetry loud — the docs now state it, but nothing refuses or warns.
Related: tests/Ioxide.Tests.Http/CrossStackParityTests.cs exists for exactly this class of divergence.
A cross-stack difference in what
ReplaceCertificatescan do, which matters because the same operational runbook works on one stack and silently does nothing on the other.On TCP, anchors given as
TlsOptions.ClientCaPathare re-read from that path on every rotation. That is deliberate and documented, and it is how an issuer is revoked without a restart —RotationTestspins both directions ("trust anchors are re-read, so who may connect can be changed", "an anchor file emptied under a rotation stops admitting anyone").On QUIC the verifier is built once, in
iq_engine_new_mtls, and every certificate generation points at that same verifier.QuicEngine.ReplaceCertificatesmarshals only certificate and key paths — no CA — so anchors cannot be rotated at all. Both behaviours are now documented, but the gap is real: an operator who revokes an issuer by editing the bundle and rotating gets what they asked for on TCP and a no-op on QUIC, with nothing said.Two ways out, and the choice is a design decision rather than a fix:
ReplaceCertificates(or add a sibling) to rebuild the verifier, which means the same build-then-publish care the certificate generations already get, since picotls keeps reading the verifier for the life of a connection;Related:
tests/Ioxide.Tests.Http/CrossStackParityTests.csexists for exactly this class of divergence.