Skip to content

quic: support client certificate authentication on QUIC listeners#45981

Open
bpalermo wants to merge 6 commits into
envoyproxy:mainfrom
bpalermo:quic-server-mtls
Open

quic: support client certificate authentication on QUIC listeners#45981
bpalermo wants to merge 6 commits into
envoyproxy:mainfrom
bpalermo:quic-server-mtls

Conversation

@bpalermo

@bpalermo bpalermo commented Jul 5, 2026

Copy link
Copy Markdown

Commit Message

quic: support client certificate authentication on QUIC listeners

Additional Description

Part 3 of re-introducing QUIC client certificate authentication. Fixes #23809; supersedes the
stale #40017 (and #39766). Stacked on #45978 (connection info) and #45980 (upstream client
certs) — the first two commits are theirs; this PR will be rebased as they land. Only the last
two commits are new here.

This honors require_client_certificate in the QUIC downstream TLS context, previously rejected
at configuration load time with "TLS Client Authentication is not supported over QUIC".

How it works (directly addressing the review questions on #39766 about where verification
happens and how it wires into Envoy's async cert validators):

  • EnvoyQuicServerSession::GetSSLConfig() maps the filter chain's require_client_certificate
    to quic::ClientCertMode::kRequire, making QUICHE send a CertificateRequest and require the
    certificate during the handshake.
  • Verification happens per-connection in EnvoyTlsServerHandshaker::VerifyCertChain (QUICHE's
    TlsHandshaker::VerifyCertChain is virtual). This handshaker already exists for session-ticket
    support and pins the exact filter chain's ServerContextImpl at connection creation, so no
    filter-chain re-lookup is needed (unlike quic: enable client certificate authentication support #40017, which re-found the filter chain with a
    synthetic localhost peer address at the crypto-config level). The chain is validated via the
    existing customVerifyCertChainForQuic(..., is_server=true, ...), so default, SPIFFE and
    custom cert validators behave exactly like TCP TLS.
  • Async validators are fully supported: PendingQUIC_PENDING, and completion resumes the
    server handshake with a packet flusher attached (mirroring
    TlsServerHandshaker::AdvanceHandshakeFromCallback; QUICHE's own proof-verify completion path
    assumes a client connection). A pending validation is cancelled if the connection goes away
    first.
  • OnProofVerifyDetailsAvailable marks the connection's SSL info validated, so
    peerCertificateValidated(), XFCC and RBAC principals reflect the handshake result (peer cert
    fields come from quic: populate peer certificate details in QUIC connection info #45978).
  • The Envoy crypto stream factory now creates EnvoyTlsServerHandshaker whenever the filter
    chain requires client certificates, independent of the
    envoy.reloadable_features.quic_session_ticket_support runtime flag (resumption stays disabled
    in that case since the ticket callback was not installed on the SSL context). The non-mTLS,
    flag-off path is unchanged.
  • Fail closed for third-party crypto-stream factories: a new
    supportsClientCertificateAuthentication() capability on
    EnvoyQuicCryptoServerStreamFactoryInterface (default false, true for the Envoy factory).
    Connections on filter chains requiring client certs are rejected when the configured factory
    can't validate them, because QUICHE's proof_verifier_ == nullptr path would otherwise accept
    the requested certificate without any validation.

Decisions to confirm with maintainers:

  1. kRequire only. A validation context without require_client_certificate does NOT map to
    kRequest (TCP requests-and-validates-if-presented in that case): doing so would change the
    wire behavior of accepted, working configs. Documented in the proto comment; can be a
    runtime-guarded follow-up.
  2. No runtime guard: pure config-acceptance widening — every affected config is rejected at
    load today, so the config itself is the opt-in. Happy to add a guard around the config-load
    acceptance if preferred.
  3. Resumption/0-RTT with client certs currently follows BoringSSL/QUICHE semantics. Session
    resumption after mTLS and 0-RTT+mTLS interactions may deserve explicit handling (e.g.
    disabling early data on mTLS filter chains) — input welcome.
  4. No CertificateRequest CA list is sent initially (SSL_set0_client_CAs needs
    CRYPTO_BUFFER plumbing); most clients select certificates without CA hints. Known gap.
  5. The dispatcher-level fail-closed close (vs. attempting config-time rejection, which isn't
    possible because the crypto-stream factory is per-listener while the requirement is
    per-filter-chain and filter chains update dynamically).

Risk Level

Medium — new handshake behavior is only reachable via configs that are rejected today; the
crypto-stream gating restructure preserves the existing non-mTLS paths.

Testing

New test/integration/quic_mtls_integration_test.cc with real QUIC handshakes: valid client
cert (200 + XFCC Hash/Subject forwarded), no client cert (handshake rejected,
PEER_DID_NOT_RETURN_A_CERTIFICATE), untrusted client cert (handshake rejected, unknown CA),
asynchronous validator (TimedCertValidator, pending → success), and no-mTLS regression. Unit
tests for config acceptance and requireClientCertificate(). Full //test/common/quic/...
suite passes.

Docs Changes

Proto docs for QuicDownstreamTransport describing the supported behavior and the kRequest
divergence from TCP.

Release Notes

Added a new-feature changelog fragment.

Platform Specific Features

N/A

bpalermo added 5 commits July 4, 2026 22:27
QuicSslConnectionInfo stubbed every peer certificate accessor with empty
values (TODO envoyproxy#23809), so peer certificate details of HTTP/3 connections
(e.g. upstream access log fields) were always empty.

The X509-based BoringSSL getters used by ConnectionInfoImplBase are not
usable on QUIC's CRYPTO_BUFFER-based SSL object, so the peer certificate
accessors in the base class are now routed through two protected virtual
hooks (peerCertificate/peerCertificateChain) whose default implementation
preserves the existing TCP TLS behavior. QuicSslConnectionInfo overrides
the hooks by converting the CRYPTO_BUFFER peer chain to X509 once and
caching it, and drops the stubs so all base class accessors work. Local
certificate accessors remain unsupported on QUIC.

This is also groundwork for QUIC client certificate authentication
(envoyproxy#23809): once client certs are requested, XFCC and RBAC principals need
these fields populated.

Signed-off-by: Bruno Palermo <b@palermo.dev>
The client certificates configured in a cluster's upstream TLS context
were silently not sent over HTTP/3: they are loaded into the Envoy
ClientContextImpl but were never installed on the QUICHE client SSL
context, so upstream QUIC servers requesting a client certificate got
none.

The certificate chain and private key are now installed on the QUICHE
SSL context (via SSL_CTX_set_chain_and_key, since QUICHE uses the
CRYPTO_BUFFER-based SSL method) when the crypto config is created or
refreshed. Client certificates using a private key provider are not
supported over QUIC and are skipped with a warning.

Guarded by envoy.reloadable_features.quic_upstream_client_certificates
(default true) since this changes the wire behavior of existing
accepted configurations.

Signed-off-by: Bruno Palermo <b@palermo.dev>
Honors require_client_certificate in the QUIC downstream TLS context
(previously rejected at config load time with 'TLS Client Authentication
is not supported over QUIC'). Fixes envoyproxy#23809.

- EnvoyQuicServerSession::GetSSLConfig() maps require_client_certificate
  to quic::ClientCertMode::kRequire per filter chain, making QUICHE send
  a CertificateRequest during the handshake.
- EnvoyTlsServerHandshaker overrides VerifyCertChain to validate the
  presented client chain against the pinned per-connection
  ServerContextImpl via customVerifyCertChainForQuic, with full support
  for asynchronous cert validators (Pending -> QUIC_PENDING ->
  ProofVerifierCallback), mirroring the client-side proof verifier.
- OnProofVerifyDetailsAvailable marks the connection SSL info validated
  so peerCertificateValidated() and XFCC reflect the handshake result.
- The Envoy crypto stream factory now creates EnvoyTlsServerHandshaker
  whenever client certs are required, independent of the session ticket
  runtime flag (resumption stays disabled in that case since the ticket
  callback was not installed).
- Third-party crypto stream factories fail closed: connections on filter
  chains requiring client certs are rejected unless the factory declares
  supportsClientCertificateAuthentication(), because QUICHE would
  otherwise accept the certificate without validation.

Signed-off-by: Bruno Palermo <b@palermo.dev>
Signed-off-by: Bruno Palermo <b@palermo.dev>
QUICHE's async proof-verify completion assumes a client connection and
resumes with a plain AdvanceHandshake(), which asserts server-side that
a packet flusher is attached. Route async validation completion through
the handshaker, attaching a ScopedPacketFlusher and notifying the
delegate, mirroring TlsServerHandshaker::AdvanceHandshakeFromCallback.
The pending callback is cancelled if the handshaker is destroyed first.

Also unsequence the transportSocketFactory() mock expectation in the
active QUIC listener test: the dispatcher now queries it before creating
the network filter chain for the client certificate fail-closed check.

Signed-off-by: Bruno Palermo <b@palermo.dev>
@repokitteh-read-only

Copy link
Copy Markdown

Hi @bpalermo, welcome and thank you for your contribution.

We will try to review your Pull Request as quickly as possible.

In the meantime, please take a look at the contribution guidelines if you have not done so already.

🐱

Caused by: #45981 was opened by bpalermo.

see: more, trace.

@repokitteh-read-only

Copy link
Copy Markdown

CC @envoyproxy/runtime-guard-changes: FYI only for changes made to (source/common/runtime/runtime_features.cc).
CC @envoyproxy/api-shepherds: Your approval is needed for changes made to (api/envoy/|docs/root/api-docs/).
envoyproxy/api-shepherds assignee is @markdroth
CC @envoyproxy/api-watchers: FYI only for changes made to (api/envoy/|docs/root/api-docs/).

🐱

Caused by: #45981 was opened by bpalermo.

see: more, trace.

When client certificates are required, QUIC does not resume sessions:
every connection performs a full handshake and re-validates the client
certificate (BoringSSL declines to resume a session carrying a client
credential in the QUIC configuration). This is stricter than TCP TLS and
means no client identity is ever attached to a resumed or 0-RTT
connection without a fresh validation.

Adds an integration test pinning that a second mTLS connection does a
full handshake (not resumption, not 0-RTT) and re-validates the client
certificate, and documents the behavior on QuicDownstreamTransport.

Signed-off-by: Bruno Palermo <b@palermo.dev>
@bpalermo bpalermo requested a deployment to external-contributors July 5, 2026 14:00 — with GitHub Actions Waiting
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

quic: require_client_certificate is ignored

2 participants