Skip to content

Conversation

@rzikm
Copy link
Member

@rzikm rzikm commented Mar 17, 2025

Fixes #110804.

While at it, I also mad sure we are using SHA512 of the certificate during lookup as we do in other parts of the area.

Copilot AI review requested due to automatic review settings March 17, 2025 10:52
@rzikm
Copy link
Member Author

rzikm commented Mar 17, 2025

/azp run runtime-libraries-coreclr outerloop

@azure-pipelines
Copy link

Azure Pipelines successfully started running 1 pipeline(s).

@rzikm rzikm requested review from bartonjs and wfurt March 17, 2025 10:52
Copy link
Contributor

Copilot AI left a comment

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 fixes a null reference exception in SslStream.Protocol by ensuring that an invalid certificate conversion is handled correctly and improves the certificate lookup by switching from the thumbprint string to a SHA512-based hash.

  • Return early from the method when the certificate conversion fails (null certificate).
  • Update the certificate hash retrieval to use GetCertHash with SHA512.
  • Adjust the certificate store lookup to use the new FindByThumbprint API with the SHA512 hash.
Comments suppressed due to low confidence (2)

src/libraries/System.Net.Security/src/System/Net/Security/SslStream.Protocol.cs:197

  • With the updated null check returning early, please ensure there are unit tests covering cases where certificate conversion fails, so that the early return behavior is validated.
if (certEx is null)

src/libraries/System.Net.Security/src/System/Net/Security/SslStream.Protocol.cs:210

  • Verify that using GetCertHash with SHA512 produces the correct hash format needed for the FindByThumbprint lookup to avoid mismatches with certificate store entries.
byte[] certHash = certEx!.GetCertHash(HashAlgorithmName.SHA512);

@rzikm
Copy link
Member Author

rzikm commented Mar 17, 2025

/azp run runtime-libraries-coreclr outerloop

@azure-pipelines
Copy link

Azure Pipelines successfully started running 1 pipeline(s).

Copy link
Member

@ManickaP ManickaP left a comment

Choose a reason for hiding this comment

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

LGTM, thanks.

Copy link
Member

@wfurt wfurt left a comment

Choose a reason for hiding this comment

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

LGTM

}

string certHash = certEx!.Thumbprint;
byte[] certHash = certEx.GetCertHash(HashAlgorithmName.SHA512);
Copy link
Member

Choose a reason for hiding this comment

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

If I remember well the Thumbprint was cached inside X509 e.g. calling it again would not allocate new array. I'm wondering we can use stack here to avoid allocation...?

Copy link
Member Author

Choose a reason for hiding this comment

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

good catch!

Copy link
Member

Choose a reason for hiding this comment

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

f I remember well the Thumbprint was cached inside X509 e.g. calling it again would not allocate new array.

Thumbprint allocates a string every time it is accessed.

I'm wondering we can use stack here to avoid allocation.

Something like

Span<byte> thumbprint = stackalloc byte[SHA512.HashSizeInBytes];
bool ret = certEx.TryGetCertHash(HashAlgorithmName.SHA512, thumbprint, out int written);
Debug.Assert(ret && written == SHA512.HashSizeInBytes);

{
X509Certificate2Collection certs = store.Certificates;
X509Certificate2Collection found = certs.Find(X509FindType.FindByThumbprint, certHash, false);
X509Certificate2Collection found = certs.FindByThumbprint(HashAlgorithmName.SHA512, certHash);
Copy link
Member

Choose a reason for hiding this comment

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

Note that FindByThumbprint has different lifetime semantics than Find. Find would return an up-refed certificate, independent from the store. FindByThumbprint does not.

Since you iterate through all of the certs on line 261-264, found will get disposed.

Copy link
Member Author

Choose a reason for hiding this comment

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

I am a bit worried that this did not cause any of the existing tests to fail.

@rzikm
Copy link
Member Author

rzikm commented Mar 21, 2025

/ba-g wasm build failure is unrelated.

@rzikm
Copy link
Member Author

rzikm commented Mar 24, 2025

Since the change to SHA-512 requires using a different API for potential private key lookups for client certificate selection (in case the client certificate instance does not have private key attached, we check the cert store and try to look it up based on a cert fingerprint), I wanted to do some perf measurements to make sure we don't significantly regress that scenario.

When the private key is not attached, we iterate over all certs in the store on the C# side and manually calculate the SHA-512 hash, as opposed to passing SHA-1 to a Windows API which performs the search for us, thus there is regression in terms of memory allocated. But there does not seem to be measurable regression on time per handshake.

// * Summary *

BenchmarkDotNet v0.14.1-nightly.20250107.205, Windows 11 (10.0.26100.3476)
Intel Core i9-10900K CPU 3.70GHz, 1 CPU, 20 logical and 10 physical cores
.NET SDK 10.0.100-preview.3.25125.5
  [Host]     : .NET 10.0.0 (10.0.25.12411), X64 RyuJIT AVX2
  Job-UHHMWK : .NET 10.0.0 (42.42.42.42424), X64 RyuJIT AVX2
  Job-TRJCBK : .NET 10.0.0 (42.42.42.42424), X64 RyuJIT AVX2

PowerPlanMode=00000000-0000-0000-0000-000000000000  IterationTime=250ms  MaxIterationCount=20
MinIterationCount=15  WarmupCount=1

| Method                                 | Job        | Toolchain         | Mean     | Error     | StdDev    | Median   | Min      | Max      | Ratio | RatioSD | Allocated | Alloc Ratio |
|--------------------------------------- |----------- |------------------ |---------:|----------:|----------:|---------:|---------:|---------:|------:|--------:|----------:|------------:|
| DefaultMutualHandshakeContextIPv4Async | Job-UHHMWK | \main\corerun.exe | 6.951 ms | 0.5072 ms | 0.5841 ms | 6.787 ms | 6.004 ms | 8.025 ms |  1.01 |    0.12 |  12.71 KB |        1.00 |
| DefaultMutualHandshakeContextIPv4Async | Job-TRJCBK | \pr\corerun.exe   | 6.089 ms | 0.1173 ms | 0.1152 ms | 6.075 ms | 5.918 ms | 6.338 ms |  0.88 |    0.07 |  27.17 KB |        2.14 |
|                                        |            |                   |          |           |           |          |          |          |       |         |           |             |
| DefaultMutualHandshakeContextIPv6Async | Job-UHHMWK | \main\corerun.exe | 6.067 ms | 0.1138 ms | 0.1217 ms | 6.070 ms | 5.888 ms | 6.256 ms |  1.00 |    0.03 |  12.69 KB |        1.00 |
| DefaultMutualHandshakeContextIPv6Async | Job-TRJCBK | \pr\corerun.exe   | 6.056 ms | 0.1166 ms | 0.1145 ms | 6.047 ms | 5.915 ms | 6.223 ms |  1.00 |    0.03 |  27.15 KB |        2.14 |
|                                        |            |                   |          |           |           |          |          |          |       |         |           |             |
| DefaultMutualHandshakeIPv4Async        | Job-UHHMWK | \main\corerun.exe | 3.346 ms | 0.0622 ms | 0.0582 ms | 3.345 ms | 3.271 ms | 3.448 ms |  1.00 |    0.02 |  15.59 KB |        1.00 |
| DefaultMutualHandshakeIPv4Async        | Job-TRJCBK | \pr\corerun.exe   | 3.354 ms | 0.0623 ms | 0.0552 ms | 3.339 ms | 3.286 ms | 3.485 ms |  1.00 |    0.02 |  30.13 KB |        1.93 |
|                                        |            |                   |          |           |           |          |          |          |       |         |           |             |
| DefaultMutualHandshakeIPv6Async        | Job-UHHMWK | \main\corerun.exe | 3.320 ms | 0.0443 ms | 0.0415 ms | 3.329 ms | 3.260 ms | 3.394 ms |  1.00 |    0.02 |  15.67 KB |        1.00 |
| DefaultMutualHandshakeIPv6Async        | Job-TRJCBK | \pr\corerun.exe   | 3.545 ms | 0.1898 ms | 0.2185 ms | 3.492 ms | 3.301 ms | 3.968 ms |  1.07 |    0.07 |  30.03 KB |        1.92 |

The (I believe) more usual case where the X509Certificate2 instance contains the private key as well, is without regression

| Method                                 | Job        | Toolchain         | Mean     | Error     | StdDev    | Median   | Min      | Max      | Ratio | RatioSD | Allocated | Alloc Ratio |
|--------------------------------------- |----------- |------------------ |---------:|----------:|----------:|---------:|---------:|---------:|------:|--------:|----------:|------------:|
| DefaultMutualHandshakeContextIPv4Async | Job-HGGNSJ | \main\corerun.exe | 6.546 ms | 0.1251 ms | 0.1390 ms | 6.564 ms | 6.317 ms | 6.805 ms |  1.00 |    0.03 |   4.86 KB |        1.00 |
| DefaultMutualHandshakeContextIPv4Async | Job-WUNJJY | \pr\corerun.exe   | 6.288 ms | 0.1253 ms | 0.1393 ms | 6.275 ms | 6.086 ms | 6.521 ms |  0.96 |    0.03 |   4.87 KB |        1.00 |
|                                        |            |                   |          |           |           |          |          |          |       |         |           |             |
| DefaultMutualHandshakeContextIPv6Async | Job-HGGNSJ | \main\corerun.exe | 6.415 ms | 0.1489 ms | 0.1655 ms | 6.429 ms | 6.127 ms | 6.699 ms |  1.00 |    0.04 |   4.86 KB |        1.00 |
| DefaultMutualHandshakeContextIPv6Async | Job-WUNJJY | \pr\corerun.exe   | 6.297 ms | 0.1181 ms | 0.1104 ms | 6.299 ms | 6.128 ms | 6.486 ms |  0.98 |    0.03 |   4.85 KB |        1.00 |
|                                        |            |                   |          |           |           |          |          |          |       |         |           |             |
| DefaultMutualHandshakeIPv4Async        | Job-HGGNSJ | \main\corerun.exe | 3.553 ms | 0.0929 ms | 0.1070 ms | 3.521 ms | 3.418 ms | 3.817 ms |  1.00 |    0.04 |   7.81 KB |        1.00 |
| DefaultMutualHandshakeIPv4Async        | Job-WUNJJY | \pr\corerun.exe   | 3.546 ms | 0.0778 ms | 0.0896 ms | 3.529 ms | 3.418 ms | 3.708 ms |  1.00 |    0.04 |   7.81 KB |        1.00 |
|                                        |            |                   |          |           |           |          |          |          |       |         |           |             |
| DefaultMutualHandshakeIPv6Async        | Job-HGGNSJ | \main\corerun.exe | 3.469 ms | 0.0537 ms | 0.0476 ms | 3.458 ms | 3.392 ms | 3.550 ms |  1.00 |    0.02 |   7.81 KB |        1.00 |
| DefaultMutualHandshakeIPv6Async        | Job-WUNJJY | \pr\corerun.exe   | 3.529 ms | 0.0779 ms | 0.0800 ms | 3.530 ms | 3.409 ms | 3.669 ms |  1.02 |    0.03 |   7.81 KB |        1.00 |

Thus I think it is safe to proceed with the change.

@rzikm rzikm merged commit 669df89 into dotnet:main Mar 24, 2025
81 of 83 checks passed
@github-actions github-actions bot locked and limited conversation to collaborators Apr 24, 2025
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Possible null reference exception in SslStream.Protocol

4 participants