Skip to content

WIP: DO NOT REVIEW Cache SslStream certificate hashes#130615

Draft
artl93 wants to merge 1 commit into
dotnet:mainfrom
artl93:artl93-sslstream-tls-perf-hunt
Draft

WIP: DO NOT REVIEW Cache SslStream certificate hashes#130615
artl93 wants to merge 1 commit into
dotnet:mainfrom
artl93:artl93-sslstream-tls-perf-hunt

Conversation

@artl93

@artl93 artl93 commented Jul 13, 2026

Copy link
Copy Markdown
Member

I am running an experiment - full analysis to follow. Contact @artl93

This draft investigates repeated SHA-512 certificate digest work during SslStream credential-cache probes when an SslStreamCertificateContext is reused.

Exact-parent component evidence (local Debug build only)

Local environment: Apple M5 Pro (18 logical CPUs), macOS 26.5.2 arm64, repo-built .NET 11 Debug libraries testhost. Parent 7aa830a03599a8255c2c4abf2947afc5b346cc6f; candidate d1518851d013212171c5255c85698c954296657c.

A frozen external harness invokes the real private server method SslStream.AcquireServerCredentials(ref byte[]?) after configuring reusable server authentication options. It does not merely compare direct and cached helper calls. Each revision used an isolated immutable testhost. The run alternated parent/candidate order across 8 process launches, with 1,000,000 warmup probes and 7 trials of 1,000,000 probes per launch (56 measured trials/revision).

Real server credential probe — Debug/Apple only Parent Candidate Difference
Elapsed, median of launch medians (SD) 1,278.0 ns (60.7) 119.3 ns (4.3) 10.71x
CPU, median of launch medians (SD) 1,278.6 ns (53.6) 120.1 ns (3.9) 10.64x
Allocation 88.000544 B 0.000544 B -88.0 B/probe

These magnitudes are not shipping/Release performance numbers. Attempts to create a coherent Release-libraries testhost in this checkout are currently blocked by pre-existing CoreLib/LibraryImport generation failures; the failed commands/logs are retained. The table is evidence for the repeated-work mechanism and Debug component impact only.

On Apple, AcquireServerCredentials returned cachedCreds == false in all trials. This platform path has a cheap/null PAL credential handle at this stage; native TLS security-context construction occurs later during the handshake. The timed method still performs certificate selection, hash acquisition, credential-cache key lookup, and that PAL call. The result should not be read as native credential-construction time. The client credential path is changed too, but this canonical benchmark measures only the server path.

A separate baseline trace attributed 19.29% inclusive samples to X509Certificate.GetCertHash -> HashData -> Interop.AppleCrypto.DigestOneShot in an isolated process that also included certificate/setup work. That percentage validates stack attribution only; it is not the hash share of AcquireServerCredentials and is not used to derive the 10.71x table.

The frozen harness is currently external to this PR. It self-identifies revision, source/binary hashes, loaded System.Net.Security.dll hash, runtime/OS, full command, warmups, iterations, and trials. Canonical identities:

  • Harness source: 37132680bc488327349e625a36cad7c000d308181390f2f89141e6ede6dc6deb
  • Harness binary: 6137092338f45c133d5dfc03c02945637914f79cf7c6c3bb7b0f353834e4bea9
  • Parent System.Net.Security.dll: 3b3a00a5ac8e52d098151eed0456723cd9527f61ade48313fd62f4245379d123
  • Candidate System.Net.Security.dll: 2f4ee6e2d5c3cccf4a9fba3091fabb0226ef815766dd6646fde5046e83232450

Raw harness/log publication is still pending; this remains WIP.

Why cache on SslStreamCertificateContext

The credential-cache key includes a SHA-512 digest of the selected certificate. A reusable certificate context represents the same target certificate across many connections, but the previous code recomputed that digest and allocated a fresh array on every client/server credential probe.

The change lazily stores an immutable { certificate handle, hash } cache entry on the context and publishes it atomically. Reuse requires the current native certificate handle to match. On a miss, the digest is computed and published only if the handle is unchanged before/after hashing. Disposal/reset changes the handle and preserves the existing CryptographicException behavior instead of returning stale data. The byte array remains internal and is never mutated. No public API changes.

This is managed cross-platform code used above Schannel, OpenSSL, Apple TLS, and Android. Local execution covers Apple only; all configured System.Net.Security target frameworks compile in the validated Debug build, and runtime behavior on other platforms depends on CI/platform validation.

Supporting handshake evidence — explicitly not a performance claim

Four interleaved launches per revision exercised TLS 1.2 with reused/fresh contexts, ALPN, mTLS, resumption enabled/disabled, and concurrency 1/16. All successful runs negotiated TLS 1.2; ALPN scenarios negotiated h2. The internal resumed-session flag was observable and recorded zero resumed sessions on both client and server, even when AllowTlsResume=true. TLS 1.3 timed out identically for parent/candidate on this Apple testhost.

Whole-handshake elapsed deltas ranged from -2.7% to +3.1%, and CPU deltas changed sign across scenarios. These results are noisy and do not establish an HTTPS or TLS-handshake speedup. No request-throughput claim is made. The supported local claim is limited to the Debug server credential-probe component.

Validation

Revision Validation Result
Parent Functional tests 4,972 total, 0 failed, 19 skipped
Parent Unit tests 132 total, 0 failed, 3 skipped
Candidate Product build, all System.Net.Security target frameworks (Debug) 0 warnings, 0 errors
Candidate Functional tests 4,979 total, 0 failed, 19 skipped
Candidate Unit tests 132 total, 0 failed, 3 skipped
Candidate Targeted hash/disposal/credential-reuse matrix 8 total, 0 failed

Targeted coverage includes atomic concurrent publication through 16 simultaneous reused-context connections, protocol negotiation, ALPN, mTLS/client certificates, certificate validation, encrypted data transfer, cached hash identity, and disposed-certificate invalidation.

Rejected variants

  • Carrying a 64-byte inline hash through authentication state enlarged async state machines and regressed handshake allocation.
  • Enlarging the dictionary key to hold the hash inline offset much of the intended allocation saving.
  • Pooling temporary hash storage reduced one allocation but added complexity and did not beat context ownership.

Context-level caching is smaller, aligns lifetime with the reusable certificate context, and avoids repeated digest work and per-probe array allocation.

Note

This draft PR description was generated with GitHub Copilot. The PR intentionally remains WIP and should not be reviewed yet.

Avoid recomputing and reallocating the SHA-512 certificate hash for each TLS credential cache lookup when a certificate context is reused.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

Copilot-Session: e9b4cc84-3f86-481e-9c92-03023269817c
@dotnet-policy-service

Copy link
Copy Markdown
Contributor

Tagging subscribers to this area: @dotnet/ncl, @bartonjs, @vcsjones
See info in area-owners.md if you want to be subscribed.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR introduces an internal SHA-512 certificate-hash cache on SslStreamCertificateContext and uses it during credential-cache probes in SslStream to avoid repeated X509Certificate.GetCertHash(HashAlgorithmName.SHA512) work when the same reusable context is used across connections.

Changes:

  • Add SslStreamCertificateContext.GetCertificateHash() which caches/publishes {cert-handle, hash} using Volatile for safe cross-thread reuse.
  • Update SslStream.Protocol credential acquisition paths to prefer the cached hash when the selected certificate is the context’s target certificate.
  • Add functional tests for concurrent authentication using a reused certificate context and for hash caching behavior (via reflection).
Show a summary per file
File Description
src/libraries/System.Net.Security/tests/FunctionalTests/SslStreamCredentialCacheTest.cs Adds a concurrent multi-connection theory that reuses SslStreamCertificateContext (optionally with ALPN and mutual TLS).
src/libraries/System.Net.Security/tests/FunctionalTests/SslStreamCertificateContextTests.cs Adds a reflection-based test for GetCertificateHash() reuse and disposed-certificate behavior.
src/libraries/System.Net.Security/src/System/Net/Security/SslStreamCertificateContext.cs Implements cached certificate hash storage on the reusable context.
src/libraries/System.Net.Security/src/System/Net/Security/SslStream.Protocol.cs Uses the cached hash for credential-cache keying instead of re-hashing each probe.

Copilot's findings

  • Files reviewed: 4/4 changed files
  • Comments generated: 2

Comment on lines +21 to +30
public static IEnumerable<object[]> CredentialReuseData()
{
foreach (SslProtocols protocol in SslProtocolSupport.EnumerateSupportedProtocols(SslProtocols.Tls12 | SslProtocols.Tls13, true))
{
yield return new object[] { protocol, false, false };
yield return new object[] { protocol, true, false };
yield return new object[] { protocol, true, true };
}
}


TargetInvocationException exception = Assert.Throws<TargetInvocationException>(
() => getCertificateHash.Invoke(context, null));
Assert.IsAssignableFrom<CryptographicException>(exception.InnerException);
@artl93

artl93 commented Jul 13, 2026

Copy link
Copy Markdown
Member Author

Still WIP — DO NOT REVIEW.

The exact-parent publication pass is complete, but this draft is intentionally not ready for review.

  • Canonical local evidence now exercises the real server AcquireServerCredentials probe path against immutable parent/candidate Debug testhosts: 8 interleaved launches per revision, 1,000,000 warmups, 56 measured 1,000,000-operation trials per revision.
  • Debug/Apple result: 1,278.0 ns → 119.3 ns elapsed, 1,278.6 ns → 120.1 ns CPU, and 88.000544 B → 0.000544 B per repeated server probe.
  • This is not a Release/shipping number. Release testhost attempts are blocked locally by pre-existing CoreLib/LibraryImport generation failures; those failed logs are preserved.
  • Whole-handshake results remain mixed and noisy (-2.7% to +3.1% elapsed). There is no TLS-handshake, HTTPS, request-throughput, or client-path performance claim.
  • Actual TLS resumption was observable but remained zero on client and server. TLS 1.3 timed out on this Apple testhost.
  • Local parent/candidate full tests and candidate targeted tests pass. Cross-platform CI is still in progress.

Frozen harness identity:

  • source SHA-256 37132680bc488327349e625a36cad7c000d308181390f2f89141e6ede6dc6deb
  • binary SHA-256 6137092338f45c133d5dfc03c02945637914f79cf7c6c3bb7b0f353834e4bea9
  • parent System.Net.Security.dll 3b3a00a5ac8e52d098151eed0456723cd9527f61ade48313fd62f4245379d123
  • candidate System.Net.Security.dll 2f4ee6e2d5c3cccf4a9fba3091fabb0226ef815766dd6646fde5046e83232450
  • canonical parent JSONL 469aa7c96fe34ab69db7e3e896a2964f1ba75c392faddc9bbfd523a2e0f068fb
  • canonical candidate JSONL c40959d4b6f40e5c91584d27faf2d596ffb8a51d62a9e7b7e36519033bb46627

Raw artifact publication and CI review remain outstanding. Please do not review yet.

Note

This WIP status comment was generated with GitHub Copilot.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants