h3: accept client trust anchors as PEM text, not only as a path - #192
Merged
Conversation
The TCP side takes its client CA either way - ClientCaPath or ClientCaPem - because TlsService parses the text into the store itself. QUIC took only a path: iq_engine_new_mtls handed it to X509_STORE_load_locations, which cannot read memory. So a host keeping its bundle in a secrets store had to write it to disk for HTTP/3 alone, and anything that had only the PEM string got mutual TLS on HTTP/1.1 and HTTP/2 and none whatsoever on HTTP/3, without a word said about it. The shim builds the same store from a BIO when given text, adding each block with X509_STORE_add_cert - the same anchors reached the other way. A block already in the store counts rather than failing, since a bundle naming one anchor twice is not an error. Both sources null still skips the whole block, so a server that never asked for mutual TLS gets the handshake it had, byte for byte. QuicEngine refuses both sources at once instead of quietly preferring one, which is what TlsService already does on the TCP side. Covered by the h3 mutual-TLS suite: a trusted client is admitted and named, and one signed by another CA is refused at the crypto layer, with the anchors loaded from text both times - the second test matters, since a store that verified nothing would pass the first. Playground/Http3/MutualTls takes PLAYGROUND_CLIENT_CA_PEM for the same. The native library was rebuilt for this. The two benchable h3 samples land within 1.5% of their recorded baselines, which is noise.
Named for the pull request that carries it: mutual TLS over HTTP/3 accepts trust anchors as PEM text, which the TCP side already did. A minor rather than a patch because QuicEngine's constructor grew a parameter, and because a caller that only ever had the PEM string was silently getting no client verification on HTTP/3 - anyone in that position wants to know the version they move to is different in kind, not degree.
MDA2AV
force-pushed
the
fix/h3-client-ca-pem
branch
from
August 17, 2026 15:38
69c5074 to
dabcbd4
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Mutual TLS over HTTP/3 could only be configured with a client CA file. The TCP side has always taken either form:
QUIC took only the path.
iq_engine_new_mtlshanded it straight toX509_STORE_load_locations, which cannot read memory, so there was nowhere for the text form to go.The consequence was worse than an inconvenience. A caller holding only the PEM string - a host that keeps its bundle in a secrets store rather than on disk - got mutual TLS on HTTP/1.1 and HTTP/2 and none at all on HTTP/3. Nothing was logged, because from the shim's point of view no client CA had been configured. On an endpoint serving
Protocols.Allthat is the same origin enforcing client certificates on two transports and waving everyone through on the third.What changed
iq_engine_new_mtlstakesclient_ca_pemalongsideclient_ca_pem_path, and builds the sameX509_STOREfrom aBIOwhen given text, adding each block withX509_STORE_add_cert. A block already present counts rather than failing - a bundle naming one anchor twice is not an error. With both sources null the whole block is skipped exactly as before, so a server that never asked for mutual TLS gets the handshake it had, byte for byte.QuicEnginerefuses both sources at once instead of quietly preferring one, matching whatTlsServicealready does for TCP.Testing
Three tests added to the h3 mutual-TLS suite, anchors loaded from text in each:
Full suites, all green: E2E 54, Tls 23, Chaos 47, Unit 31, Http 28 - 183 passed, 0 failed.
Also exercised through
Playground/Http3/MutualTls, which now takesPLAYGROUND_CLIENT_CA_PEM:the client CA PEM text held no usable certificatePerformance
The native library was rebuilt (ngtcp2 + picotls + shim), so the benchable h3 samples were re-measured against their recorded baselines at the same reactor count:
96137b9Http3/Nghttp3BufferedHttp3/BufferedNoise in both directions. The added code runs once, at engine construction, and not on any data path.
No bench result is committed here: the run was made on a dirty tree, and recording that as
latest.jsonis precisely what 9b4579e set out to stop.