-
Notifications
You must be signed in to change notification settings - Fork 5.1k
[Perf] Cache key size in CngKey #89599
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
Tagging subscribers to this area: @dotnet/area-system-security, @bartonjs, @vcsjones Issue DetailsAn internal partner is reporting that they're seeing CNG signature validation latency show up as a bottleneck. The culprit seems to be that each signing & validation operation queries the runtime/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/RSACng.Key.cs Lines 20 to 26 in 5d7b16f
Lines 47 to 56 in 5d7b16f
The Since public class CngRunner
{
private RSACng _rsa;
private ECDsaCng _ecdsa;
private static byte[] _emptyDigest = new byte[256 / 8];
private byte[] _rsaSignedHash;
private byte[] _ecdsaSignedHash;
[GlobalSetup]
public void Setup()
{
_rsa = new RSACng(2048);
_ecdsa = new ECDsaCng(256);
_rsaSignedHash = _rsa.SignHash(_emptyDigest, HashAlgorithmName.SHA256, RSASignaturePadding.Pss);
if (!Rsa_VerifyHash()) { throw new CryptographicException("Self-test failed."); }
_ecdsaSignedHash = _ecdsa.SignHash(_emptyDigest);
if (!Ecdsa_VerifyHash()) { throw new CryptographicException("Self-test failed."); }
}
[Benchmark]
public bool Rsa_VerifyHash() => _rsa.VerifyHash(_emptyDigest, _rsaSignedHash, HashAlgorithmName.SHA256, RSASignaturePadding.Pss);
[Benchmark]
public bool Ecdsa_VerifyHash() => _ecdsa.VerifyHash(_emptyDigest, _ecdsaSignedHash);
}
Open discussionTechnically this is a behavioral change, since it means that the
|
...s/System.Security.Cryptography/src/System/Security/Cryptography/CngKey.StandardProperties.cs
Outdated
Show resolved
Hide resolved
Updated the PR to use -1 instead of 0 as a sentinel, plus moved some logic around to account for early return scenarios. I also added a unit test to ensure that all non-handle properties throw after the CngKey instance is disposed, even if the properties had been previously accessed and cached. This should help thwart behavioral regressions. Note to reviewers: I suggest hiding whitespace changes (https://github.com/dotnet/runtime/pull/89599/files?diff=unified&w=1) when reviewing the latest iteration, since some indentation changed within the KeySize property getter. Perf results from this build are nearly identical to the earlier run. I won't bother posting them. |
src/libraries/System.Security.Cryptography.Cng/tests/CngKeyTests.cs
Outdated
Show resolved
Hide resolved
src/libraries/System.Security.Cryptography.Cng/tests/CngKeyTests.cs
Outdated
Show resolved
Hide resolved
A bit of justification here: This isn't a big deal for one off usages, but any time there is a system that is caching an instance for use over and over, the LRPC call is a significant bottleneck, and limits the scale of usage to be what lsass can handle. This is a common situation in cloud services, so a change to cache the key size is worthwile. |
@brianrob out of curiosity, are you using RSACng directly? Or are you using RSA.Create? If the former, are you using CNG specific features that would exclude using RSA.Create? |
@vcsjones, I'm one of the folks that was analyzing perf data, but don't work on the service itself. I've sent mail asking them to reply here. |
Gotcha. I only ask because starting in .NET 8, |
Internal partner reports that they're using I've changed the benchmark to perform signing instead of verification, since private key operations still require bouncing through lsass. The perf gains are still measurable: around 100μs per op.
|
Sure. I’m not trying to argue against the change - we should take it regardless since it has already demonstrated some improvements elsewhere. Just noting that the internal partner is likely to get an even more significant performance improvement for .NET 8 once they start going through bcrypt. |
Really this shows that Windows should add similar caching. But even if they did this change would still save on the machinery of a P/Invoke. |
Well... the key size can change - before NCryptFinalize is called. We (mostly) don't have that problem here. But I'm not so sure how reasonable it would be for Windows to add caching with that in mind. |
Looks like CI is a bit clogged. I'll come back in a few hours and see how things have progressed before merging. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
An internal partner is reporting that they're seeing CNG signature validation latency show up as a bottleneck. The culprit seems to be that each signing & validation operation queries the
CngKey.KeySize
property.runtime/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/RSACng.Key.cs
Lines 20 to 26 in 5d7b16f
runtime/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/CngAlgorithmCore.cs
Lines 47 to 56 in 5d7b16f
The
CngKey.KeySize
property is not cached anywhere in the system, which means that each query to this property results in an LRPC call back into lsass or lsaiso. This context switch is what appears to be introducing the latency.Since
CngKey
instances can't change their key size once created, we can cache the key size on our end without requiring calls back through theNCrypt*
(lsass) layer. That significantly reduces the latency.Open discussion
Technically this is a behavioral change, since it means that the
CngKey.KeySize
property will remain accessible on an otherwise disposed instance, just as long as it has already been fetched at least once prior to disposal. I could easily add a disposed check (and unit test to validate this behavior) if we think preserving the earlier behavior is worthwhile.