You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
On a resumed (abbreviated) TLS handshake the peer does not resend its certificate — its identity was established and validated during the original full handshake that produced the session ticket / session id. Common TLS stacks (OpenSSL, SChannel) do not re-run certificate verification on resumption.
Today SslStream still rebuilds the chain and invokes the user validation callback on every resumed session. This PR changes the default so that, on a resumed initial handshake, SslStream adopts the cached peer certificate for the RemoteCertificate property but skips the chain build and the user validation callback — matching the underlying TLS libraries.
The shortcut is gated to the initial handshake only: during renegotiation or TLS 1.3 post-handshake authentication the peer can present a new certificate, which is always fully validated.
Opt-out switch
A new AppContext switch restores the previous behavior:
When set, the peer certificate is re-validated on every successful resumption as before.
Cross-platform TlsResumed detection
The skip only fires when the backend can reliably report resumption:
Backend
Platform
Signal
SChannel
Windows
SECPKG_ATTR_SESSION_INFO / SSL_SESSION_RECONNECT
OpenSSL
Linux
SSL_session_reused
SecureTransport
Apple (default)
none available in current Apple SDK → always revalidates
Network Framework
Apple (opt-in)
none public → always revalidates
Conscrypt/Java
Android
none reliable → always revalidates
The Windows/Linux TlsResumed computation was previously #if DEBUG-only and is now unconditional. SecureTransport's SSLGetResumableSessionInfo has been removed from current Apple SDKs (it fails to compile as an undeclared function), so Apple — like Network Framework and Android — leaves TlsResumed unset and always revalidates on resumption (documented inline). The optimization therefore currently applies on Windows and Linux.
Performance
dotnet/performance ResumedHandshake benchmark, Windows x64, across a cert × protocol matrix. Revalidate = old behavior (RevalidateCertificateOnTlsResume=1); Skip = new default. Stabilized run (warmup 12 / 30 iterations / 500 ms, GCgen0size=256MB).
Cert
Protocol
Revalidate (mean)
Skip / new default (mean)
Time Δ
Alloc Δ
Contoso (full chain)
TLS 1.2
762.1 µs
628.4 µs
−17.5%
−0.99 KB
ECDSA P-256
TLS 1.2
850.2 µs
653.0 µs
−23.2%
−0.77 KB
RSA-2048
TLS 1.2
568.4 µs
490.3 µs
−13.7%
−0.77 KB
RSA-4096
TLS 1.2
711.1 µs
590.8 µs
−16.9%
−0.76 KB
Contoso (full chain)
TLS 1.3
2,965.6 µs
2,905.3 µs
−2.0%
−0.94 KB
ECDSA P-256
TLS 1.3
3,022.5 µs
2,780.4 µs
−8.0%
−0.75 KB
RSA-2048
TLS 1.3
2,625.7 µs
2,688.5 µs
+2.4%
−0.72 KB
RSA-4096
TLS 1.3
2,899.1 µs
2,863.2 µs
−1.2%
−0.84 KB
TLS 1.2: consistent ~14–23% reduction in handshake time plus ~0.76–0.99 KB fewer allocations per resumed handshake.
TLS 1.3: handshake time is within run-to-run noise (most of the resumed-handshake cost there is native SSPI/SChannel work, not managed cert/chain crypto), but allocations still drop ~0.72–0.94 KB consistently.
Notes / open questions
⚠️ This flips a security-relevant default. It likely needs design/API review and a breaking-change doc before merging, even prerelease-to-prerelease.
⚠️ The user certificate-validation callback silently stops firing on resumed initial handshakes under the new default — this needs to be an intentional, documented decision.
No new tests added yet; existing resume/validation/renegotiation suites pass with the new default on Windows.
Note
This PR (including its description) was created with the assistance of GitHub Copilot.
On a resumed (abbreviated) TLS handshake the peer does not resend its
certificate; its identity was established during the original full
handshake. Common TLS stacks (OpenSSL, SChannel) do not re-run
certificate verification on resumption. Match that behavior: by default
SslStream now adopts the cached peer certificate without rebuilding the
chain or invoking the user validation callback on a resumed session.
Add the opt-in System.Net.Security.RevalidateCertificateOnTlsResume
AppContext switch (env DOTNET_SYSTEM_NET_SECURITY_REVALIDATECERTIFICATEONTLSRESUME)
to restore the previous revalidate-on-resume behavior.
Wire up TlsResumed detection across platforms:
- Windows (SChannel) / Linux (OpenSSL): un-guard the existing TlsResumed
computation from DEBUG-only builds.
- Apple SecureTransport: new AppleCryptoNative_SslGetSessionResumed export
using SSLGetResumableSessionInfo, plumbed through Interop.Ssl and
SslConnectionInfo.OSX.
- Network Framework and Android expose no reliable resumption signal and
keep the safe fallback of always revalidating (documented in code).
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 5fc01286-cba3-4fbb-b19f-c323b7b4d964
Azure Pipelines:
Successfully started running 4 pipeline(s).
12 pipeline(s) were filtered out due to trigger conditions.
There may be pipelines that require an authorized user to comment /azp run to run.
The reason will be displayed to describe this comment to others. Learn more.
🟡 Changes recommended
The resumed-session validation skip can incorrectly apply during renegotiation/TLS 1.3 post-handshake auth, potentially accepting newly supplied certificates without validation.
Once you've addressed the issues Copilot identified, you can request another Copilot review.
Pull request overview
This PR changes SslStream certificate-validation behavior on resumed (abbreviated) TLS handshakes: when a backend can reliably detect resumption, it can skip rebuilding the certificate chain and skip invoking the user validation callback by default, with an AppContext/env switch to restore the prior behavior. It also makes TlsResumed detection available cross-platform (Windows/Linux unconditional, Apple SecureTransport via a new native export; Android/Network Framework keep the “safe fallback” of always revalidating due to lack of a reliable signal).
Changes:
Add System.Net.Security.RevalidateCertificateOnTlsResume / DOTNET_SYSTEM_NET_SECURITY_REVALIDATECERTIFICATEONTLSRESUME to opt back into revalidation on session resumption.
Detect TlsResumed on Windows/Linux unconditionally and on Apple SecureTransport via AppleCryptoNative_SslGetSessionResumed.
Skip managed chain build + user validation callback when TlsResumed is true and the opt-out switch is not enabled.
…Apple API
Address review feedback and fix Apple CI build:
- Only skip peer certificate revalidation on the *initial* resumed
handshake. During renegotiation / TLS 1.3 post-handshake auth the peer
can present a new certificate, which must always be validated. Thread
an isInitialHandshake flag (SslStream passes !_isRenego; the TlsSession
external-validation path passes false) into VerifyRemoteCertificateCore
and gate the resumption shortcut on it.
- Revert the SecureTransport TlsResumed wiring: SSLGetResumableSessionInfo
has been removed from current Apple SDKs and fails to compile ("call to
undeclared function") across all Apple targets. Apple now leaves
TlsResumed unset, matching the Network Framework and Android safe
fallback of always revalidating on resumption.
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 5fc01286-cba3-4fbb-b19f-c323b7b4d964
The reason will be displayed to describe this comment to others. Learn more.
🟡 Changes recommended
The new resumed-handshake fast-path needs targeted coverage for the new default + opt-out behavior and should address the resource-lifetime implications of skipping the user callback while still collecting chain intermediates.
Once you've addressed the issues Copilot identified, you can request another Copilot review.
…itch tests
On a resumed handshake the certificate validation callback is skipped by
default, so the outer VerifyRemoteCertificate no longer had anything adopt the
peer-sent intermediate certificates that GetRemoteCertificate appends to the
chain's ExtraStore. Thread a certificateValidationSkippedOnResume flag out of
VerifyRemoteCertificateCore so those intermediates are disposed even when a
RemoteCertificateValidationCallback is configured, avoiding leaked
X509Certificate2 handles across repeated resumptions.
Add a RemoteExecutor-based test that verifies the client validation callback is
not invoked on a resumed handshake by default and that setting
RevalidateCertificateOnTlsResume restores callback invocation on resumption.
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 5fc01286-cba3-4fbb-b19f-c323b7b4d964
The reason will be displayed to describe this comment to others. Learn more.
🔵 Needs a closer look
It changes security-relevant certificate validation semantics on resumed TLS handshakes and still needs broader test coverage (notably server-side client-cert validation) and careful human review for compatibility impact.
…validate test
Parametrize the revalidate-switch resume test over client-side (server cert) and server-side (client cert) validation so the shared skip default is exercised in both directions. When the environment cannot establish session resumption, signal a skip to the parent process via a marker file instead of hard-failing, matching the SkipTestException pattern used elsewhere in this file.
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 5fc01286-cba3-4fbb-b19f-c323b7b4d964
The reason will be displayed to describe this comment to others. Learn more.
🟡 Changes recommended
It changes a security-relevant default to skip certificate validation on resumed sessions (and introduces a Release-path perf regression on Windows via Enum.HasFlag boxing) which needs addressing/explicit opt-in alignment before approval.
Once you've addressed the issues Copilot identified, you can request another Copilot review.
Review details
Suppressed comments (2)
Previously missed (1) — in code that hasn't changed since the last review.
Using Enum.HasFlag here boxes the enum value and can allocate; this code now runs in Release for every handshake, so it’s worth avoiding the overhead. Prefer a simple bit-test against SSL_SESSION_RECONNECT.
This introduces a default-path validation bypass: on resumed initial handshakes, the user RemoteCertificateValidationCallback and chain build are skipped unless an AppContext switch opts out. This conflicts with the System.Net.Security guidance that certificate validation bypass must require explicit opt-in (i.e., keep the existing secure default and allow opting into the optimization).
Add explicit assertions after the field/type/property reflection lookups so a future rename of the internal members produces a clear diagnostic instead of a NullReferenceException.
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 5fc01286-cba3-4fbb-b19f-c323b7b4d964
TlsResumed is now evaluated on every handshake in Release builds, so replace the boxing Enum.HasFlag call with a direct bit-test against SSL_SESSION_RECONNECT to avoid the per-handshake allocation.
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 5fc01286-cba3-4fbb-b19f-c323b7b4d964
Addressed the Copilot review feedback in the latest commits:
Enum.HasFlag boxing on the Windows resume-detection path (SslConnectionInfo.Windows.cs): replaced with a direct bit-test against SSL_SESSION_RECONNECT in 92776e8, since TlsResumed is now evaluated on every handshake in Release. (The identical pre-existing pattern in CertificateValidationPal.IsLocalCertificateUsed is outside this PR's scope and only runs when a local cert is used, so I left it untouched.)
Test coverage: the revalidate-switch resume test now also exercises the server-side (client-cert) validation callback, and skips cleanly when the environment can't establish resumption.
Reflection robustness: added explicit assertions on the internal field/type/property lookups.
On the note about skipping certificate re-validation on resumption being a security-relevant default change: that is the intended behavior of this PR — matching what mainstream TLS stacks (OpenSSL, SChannel, etc.) do, which don't re-invoke user validation on an abbreviated handshake. The previous behavior remains available via the System.Net.Security.RevalidateCertificateOnTlsResume AppContext switch, and the performance rationale/measurements are in the PR description.
The reason will be displayed to describe this comment to others. Learn more.
🔵 Needs a closer look
It changes a security-sensitive default (skipping user certificate validation callbacks on resumed sessions) and warrants careful human review of compatibility and security implications across platforms and scenarios.
Review details
Suppressed comments (1)
Previously missed (1) — in code that hasn't changed since the last review.
The switch comment says the peer certificate is not re-validated on resumed TLS handshakes "by default", but the implementation only skips validation when resumption can be detected (currently Windows/SChannel and Linux/OpenSSL). On Apple/Android, TlsResumed remains false and validation still runs, so the comment is misleading.
Session resumption is only detected on Windows and Linux; on other platforms TlsResumed stays false and the peer certificate is always re-validated. Note this in the switch comment so the default behavior is not read as universal.
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 5fc01286-cba3-4fbb-b19f-c323b7b4d964
… assert server resume flag
Address review feedback on the resume revalidation test:
- Enumerate supported protocols dynamically (TLS 1.2 + TLS 1.3) via
SslProtocolSupport.EnumerateSupportedProtocols instead of hardcoding TLS 1.2,
so the test keeps covering the negotiated versions as older ones are retired.
- Assert that the server observes the same resumption state as the client
(IsResumed(server) == IsResumed(client)).
- PingPong after the handshake so the client consumes the post-handshake TLS 1.3
session ticket, which is required to actually exercise TLS 1.3 resumption.
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 5fc01286-cba3-4fbb-b19f-c323b7b4d964
The reason will be displayed to describe this comment to others. Learn more.
🔵 Needs a closer look
It changes security-relevant default certificate-validation semantics on resumed handshakes and needs careful human review for compatibility and security tradeoffs.
Review details
Suppressed comments (1)
Previously missed (1) — in code that hasn't changed since the last review.
The resumption fast-path depends on connectionInfo.TlsResumed, but connectionInfo may still be uninitialized during inline certificate validation (note the later connectionInfo.Protocol == 0 check before invoking the user callback). In that case TlsResumed will remain false and the new default will unexpectedly fall back to full revalidation + callback on resumed handshakes.
Consider populating connectionInfo (via SslStreamPal.QueryContextConnectionInfo) before checking TlsResumed, using the same Protocol == 0 guard used later for the callback path.
Same infra pattern as the previous run, on a different set of work items. The runtime leg failed only because the Monitor Helix Jobs task flagged failed work items, and the timeline reports server-side throttling again (The job is currently being throttled by the server).
The flagged work items this time are all WASM Mono System.Runtime.Tests (WasmTestOnChrome-MONO-ST-System.Runtime.Tests on browser-wasm linux/windows LibraryTests_Smoke_AOT) — unrelated to this PR, which only touches System.Net.Security. Across all 32 test runs the run-level failed count is 0: every first-attempt failure passed on retry.
No System.Net.Security test failed. This is transient Helix throttling / WASM flakiness, not a failure caused by this change, so I'm not retriggering CI to reclassify.
This changes the default certificate-validation behavior of the public SslStream contract: on Windows/Linux resumed handshakes, callers' RemoteCertificateValidationCallback is no longer invoked and hostname/trust checks are skipped. The repository's review requirements treat behavioral changes as requiring breaking-change documentation, but this PR adds only implementation comments and tests; please add the corresponding breaking-change entry (including the callback and opt-out-switch behavior) before merging.
// By default the peer certificate is not re-validated on a resumed (abbreviated) TLS
// handshake, matching the behavior of common TLS stacks (e.g. OpenSSL, SChannel) that
// do not re-run certificate verification when a session is resumed. This optimization
// only applies on platforms where session resumption can be detected (currently Windows
// and Linux); elsewhere the peer certificate is always re-validated. Enabling this
These queries are now executed for every completed handshake, including full handshakes; before this change the Windows/Linux resumption probes were DEBUG-only and therefore absent from Release builds. The performance data in the PR covers resumed handshakes but does not establish the cost on full handshakes, so this can regress the common non-resumed path. Please add a full-handshake comparison to the benchmark (or otherwise demonstrate that the added native query is negligible) before relying on the reported optimization.
[!NOTE] This review comment was created with assistance from GitHub Copilot.
The new isInitialHandshake guard is security-critical, but the added matrix only exercises initial resumed handshakes; it does not verify that a certificate presented during TLS 1.2 renegotiation or TLS 1.3 post-handshake authentication still invokes the callback and is chain-validated. Add a focused regression case for the excluded path (or extend the existing renegotiation/PHA coverage) so a future change cannot accidentally broaden this shortcut.
if (certificate != null &&
isInitialHandshake &&
connectionInfo.TlsResumed &&
!LocalAppContextSwitches.RevalidateCertificateOnTlsResume)
The new test verifies only the callback count. It does not verify the other observable behavior introduced by the fast path: that the resumed connection's RemoteCertificate is populated with the cached peer certificate. A regression that returned success while leaving this property null would still pass; add an assertion for the resumed client/server RemoteCertificate (including the mutual-auth case) while the connection is alive.
// Establish a resumed session and measure whether the callback runs on it.
bool measuredResume = false;
for (int i = 0; i < 5 && !measuredResume; i++)
{
ResetMeasuredCount();
Added needs-breaking-change-doc-created label because this PR has the breaking-change label.
When you commit this breaking change:
Create and link to this PR and the issue a matching issue in the dotnet/docs repo using the breaking change documentation template, then remove this needs-breaking-change-doc-created label.
Ask a committer to mail the .NET Breaking Change Notification DL.
Tagging @dotnet/compat for awareness of the breaking change.
Assert exact client and server certificate validation callback counts across full and resumed handshakes.
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 5fc01286-cba3-4fbb-b19f-c323b7b4d964
Disable resumption in tests that require certificate validation callbacks to run on every connection, and remove an unrelated client certificate from the changed-host resumption test.
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 5fc01286-cba3-4fbb-b19f-c323b7b4d964
This changes the observable security contract for RemoteCertificateValidationCallback: on Windows/Linux, a resumed initial handshake now succeeds without invoking a callback that callers may use for pinning or authorization. The only explanation is an internal comment, while the public options docs do not describe this behavior and no breaking-change documentation is included. Add the required user-facing breaking-change documentation and clearly document the callback/switch behavior before merging.
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
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.
Summary
On a resumed (abbreviated) TLS handshake the peer does not resend its certificate — its identity was established and validated during the original full handshake that produced the session ticket / session id. Common TLS stacks (OpenSSL, SChannel) do not re-run certificate verification on resumption.
Today
SslStreamstill rebuilds the chain and invokes the user validation callback on every resumed session. This PR changes the default so that, on a resumed initial handshake,SslStreamadopts the cached peer certificate for theRemoteCertificateproperty but skips the chain build and the user validation callback — matching the underlying TLS libraries.The shortcut is gated to the initial handshake only: during renegotiation or TLS 1.3 post-handshake authentication the peer can present a new certificate, which is always fully validated.
Opt-out switch
A new AppContext switch restores the previous behavior:
System.Net.Security.RevalidateCertificateOnTlsResume(config)DOTNET_SYSTEM_NET_SECURITY_REVALIDATECERTIFICATEONTLSRESUME=1(env)When set, the peer certificate is re-validated on every successful resumption as before.
Cross-platform
TlsResumeddetectionThe skip only fires when the backend can reliably report resumption:
SECPKG_ATTR_SESSION_INFO/SSL_SESSION_RECONNECTSSL_session_reusedThe Windows/Linux
TlsResumedcomputation was previously#if DEBUG-only and is now unconditional. SecureTransport'sSSLGetResumableSessionInfohas been removed from current Apple SDKs (it fails to compile as an undeclared function), so Apple — like Network Framework and Android — leavesTlsResumedunset and always revalidates on resumption (documented inline). The optimization therefore currently applies on Windows and Linux.Performance
dotnet/performance
ResumedHandshakebenchmark, Windows x64, across a cert × protocol matrix. Revalidate = old behavior (RevalidateCertificateOnTlsResume=1); Skip = new default. Stabilized run (warmup 12 / 30 iterations / 500 ms,GCgen0size=256MB).Notes / open questions
Note
This PR (including its description) was created with the assistance of GitHub Copilot.