Silent no-op when all explicitly-configured ClientCertificates are ineligible for client authentication — consider a louder diagnostic
- Dominant language
- C#
- Stars
- 18.3k
- Forks
- 5.6k
- PR merge metrics
- PR metrics pending
Description
### Description
When `HttpClientHandler.ClientCertificates` (or the `SslStream` certificate collection path) contains only certificates that fail the eligibility check in `CertificateHelper.GetEligibleClientCertificate` — e.g. certificates whose EKU lacks Client Authentication (`1.3.6.1.5.5.7.3.2`) — .NET presents **no client certificate** during the TLS handshake. The only signal is an Info-level `NetEventSource` trace ("No eligible client certificate found."), which is invisible unless you're already tracing with `dotnet-trace`/an `EventListener`.
**I'm not asking to change the selection behavior.** #121982 settled that (and having now lived through the consequences, I think the conclusion there is right — a serverAuth-only certificate *shouldn't* be auto-selected for client auth). This issue is only about observability when that filtering results in nothing being sent despite explicit configuration.
I think this is about to affect many more people: per the Chrome Root Program policy (§4.2.1, effective June 15 2026), public CAs no longer include the clientAuth EKU in publicly-trusted TLS certificates (see the DigiCert sunset notice referenced in #121982). Many organizations use public CA certificates as mTLS client certificates against partner APIs. As those certificates renew over the coming months, each renewal silently converts a working mTLS integration into "no certificate sent" — with no code change and no .NET-side error.
### Reproduction Steps
1. Obtain a certificate + private key whose EKU extension is present but contains only Server Authentication (`1.3.6.1.5.5.7.3.1`). Any post-June-2026 publicly-issued TLS certificate has this shape, or create one:
```bash
openssl req -x509 -newkey rsa:2048 -keyout client.key -out client.pem -days 30 -nodes \
-subj "/CN=repro-client" -addext "extendedKeyUsage=serverAuth" -addext "keyUsage=digitalSignature"
```
2. Point an `HttpClient` at any TLS endpoint that requires a client certificate (we hit this against an AWS API Gateway custom domain with mTLS enabled; any mTLS-enforcing server reproduces it):
```csharp
using var ephemeral = X509Certificate2.CreateFromPemFile("client.pem", "client.key");
var cert = X509CertificateLoader.LoadPkcs12(ephemeral.Export(X509ContentType.Pfx), null);
var handler = new HttpClientHandler();
handler.ClientCertificates.Add(cert);
using var client = new HttpClient(handler);
var response = await client.PostAsync("https:///", new StringContent("{}"));
```
3. Observe the request fail with a server-dependent transport error (see Actual behavior). Remove the EKU extension (or add clientAuth) from the certificate and the same code presents the certificate and succeeds.
### Expected behavior
When the developer has **explicitly** populated `ClientCertificates` (clear intent to perform client authentication) and eligibility filtering eliminates **every** provided certificate, there is some observable signal at normal diagnostic levels that the certificates were considered and discarded — for example a Warning-level `NetEventSource` event stating "N certificates provided, 0 eligible" with the per-certificate skip reason (missing clientAuth EKU / missing DigitalSignature KU / no private key).
### Actual behavior
The handshake proceeds with no client certificate and no error, warning, or exception from .NET. The observable failure comes from the *server* and is server-dependent; in our case (AWS API Gateway mTLS, TLS 1.3) it was a connection reset **after the request was sent**:
```
System.Net.Http.HttpRequestException: An error occurred while sending the request.
---> System.IO.IOException: Unable to read data from the transport connection: Connection reset by peer.
---> System.Net.Sockets.SocketException (104): Connection reset by peer
```
To me, this alludes to a network/firewall problem, not an authentication problem. Diagnosis is further complicated because `openssl s_client` and `curl` send the configured certificate regardless of EKU (and many servers, including ours, don't inspect the EKU), so manual verification of the same certificate against the same endpoint **succeeds** — actively pointing the investigation away from the certificate.
### Regression?
No — the eligibility filtering is longstanding behavior and was reaffirmed as by-design in #121982. What has changed is the environment: since the June 2026 CA policy change, routine certificate renewals now flip previously-eligible certificates into ineligible ones, so the silent path is being hit far more often, typically with no code change on the affected application.
### Known Workarounds
`LocalCertificateSelectionCallback` bypasses the eligibility filter and sends whatever it returns (the "do what I say" path confirmed in #121982):
```csharp
var handler = new SocketsHttpHandler();
handler.SslOptions.LocalCertificateSelectionCallback = (_, _, _, _, _) => cert;
```
This resolved our outage. The workaround is fine once you know it exists — the problem is that nothing tells you that you need it.
### Configuration
Observed on .NET 10 on Linux (containerized, where the affected production service runs) and Windows 11 x64 (standalone repro). Not OS- or arch-specific as far as we can tell; the relevant shared code path (`CertificateHelper.GetEligibleClientCertificate`) is present in current `main`.
### Other information
Real-world impact, for calibration: we run a shipping integration platform, and a partner carrier (mTLS on an AWS API Gateway endpoint) published renewed client certificates in June 2026 — same CA, same subject, but `serverAuth`-only where the previous certificate had `serverAuth + clientAuth`. We rotated following our runbook; every existing check passed (key/cert modulus match, expiry, successful calls to the partner's non-mTLS endpoints). The result was a production outage, reported by our customer before we detected it. Once we knew where to look, the fix took minutes; getting to "where to look" cost a full evening of packet-level investigation and side-by-side repros. A single warning would have made it a 30-second log read.
If a diagnostic event is deemed too heavy, a remarks/callout in the `HttpClientHandler.ClientCertificates` and `SslClientAuthenticationOptions.ClientCertificates` docs describing the filtering, the EKU requirement, and the callback escape hatch — ideally referencing the 2026 CA change — would capture most of the value; I'd be glad to contribute that docs PR. And per @bartonjs's note in #121982 that a "do what I say in Manual mode" case could be raised if someone had a concrete scenario: this is that scenario, though a diagnostic alone would have been enough for us.
Related: #121982, #94152.
Contributor guide
Research direction
Start with CertificateHelper.GetEligibleClientCertificate and the HttpClientHandler.ClientCertificates and SslClientAuthenticationOptions.ClientCertificates entry points; reproduce the server-dependent failure with an explicitly configured serverAuth-only certificate. Compare the existing NetEventSource message with the behavior discussed in #121982. Done means the explicit all-ineligible path has a useful diagnostic or documented explanation without changing certificate selection behavior.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- csharp
- Domain
- authentication, networking, security
- Issue type
- Feature
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Quiet
- Clarity
- Mostly clear
- Newbie friendliness
- 48/100