-
Notifications
You must be signed in to change notification settings - Fork 4.8k
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
Use BCrypt for ephemeral RSA on Windows #76277
Conversation
Tagging subscribers to this area: @dotnet/area-system-security, @vcsjones Issue DetailsWindows CNG has two different libraries: bcrypt.dll ( NCrypt's flexibility (to also work with persisted keys) comes at a cost. All key operations are done out-of-process (in lsass.exe), and that requires an (L)RPC call for every operation. It also means there are simply more moving parts, and thus more room for error. With this change we will use BCrypt for RSA operations on Windows from For keys created from RSA.Create() a verification operation currently looks like
After this change it is instead
Removing all of those other indirections removes about 40us from a 50us operation (on my primary devbox). As part of making some code be shared between RSACng and RSABCrypt, some allocating code changed to pooling code and some array code got spanified. Might contributes to #25979, either way it improves perf.
|
Perf Table
Benchmark Code private static RSA _rsa = RSA.Create();
private static byte[] _sig = _rsa.SignHash(new byte[256 / 8], HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1);
private static byte[] _enc = _rsa.Encrypt(new byte[3], RSAEncryptionPadding.OaepSHA256);
private static X509Certificate2 s_cert = new X509Certificate2(Convert.FromBase64String(@"
MIIE7DCCA9SgAwIBAgITMwAAALARrwqL0Duf3QABAAAAsDANBgkqhkiG9w0BAQUF
ADB5MQswCQYDVQQGEwJVUzETMBEGA1UECBMKV2FzaGluZ3RvbjEQMA4GA1UEBxMH
UmVkbW9uZDEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMSMwIQYDVQQD
ExpNaWNyb3NvZnQgQ29kZSBTaWduaW5nIFBDQTAeFw0xMzAxMjQyMjMzMzlaFw0x
NDA0MjQyMjMzMzlaMIGDMQswCQYDVQQGEwJVUzETMBEGA1UECBMKV2FzaGluZ3Rv
bjEQMA4GA1UEBxMHUmVkbW9uZDEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0
aW9uMQ0wCwYDVQQLEwRNT1BSMR4wHAYDVQQDExVNaWNyb3NvZnQgQ29ycG9yYXRp
b24wggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDor1yiIA34KHy8BXt/
re7rdqwoUz8620B9s44z5lc/pVEVNFSlz7SLqT+oN+EtUO01Fk7vTXrbE3aIsCzw
WVyp6+HXKXXkG4Unm/P4LZ5BNisLQPu+O7q5XHWTFlJLyjPFN7Dz636o9UEVXAhl
HSE38Cy6IgsQsRCddyKFhHxPuRuQsPWj/ov0DJpOoPXJCiHiquMBNkf9L4JqgQP1
qTXclFed+0vUDoLbOI8S/uPWenSIZOFixCUuKq6dGB8OHrbCryS0DlC83hyTXEmm
ebW22875cHsoAYS4KinPv6kFBeHgD3FN/a1cI4Mp68fFSsjoJ4TTfsZDC5UABbFP
ZXHFAgMBAAGjggFgMIIBXDATBgNVHSUEDDAKBggrBgEFBQcDAzAdBgNVHQ4EFgQU
WXGmWjNN2pgHgP+EHr6H+XIyQfIwUQYDVR0RBEowSKRGMEQxDTALBgNVBAsTBE1P
UFIxMzAxBgNVBAUTKjMxNTk1KzRmYWYwYjcxLWFkMzctNGFhMy1hNjcxLTc2YmMw
NTIzNDRhZDAfBgNVHSMEGDAWgBTLEejK0rQWWAHJNy4zFha5TJoKHzBWBgNVHR8E
TzBNMEugSaBHhkVodHRwOi8vY3JsLm1pY3Jvc29mdC5jb20vcGtpL2NybC9wcm9k
dWN0cy9NaWNDb2RTaWdQQ0FfMDgtMzEtMjAxMC5jcmwwWgYIKwYBBQUHAQEETjBM
MEoGCCsGAQUFBzAChj5odHRwOi8vd3d3Lm1pY3Jvc29mdC5jb20vcGtpL2NlcnRz
L01pY0NvZFNpZ1BDQV8wOC0zMS0yMDEwLmNydDANBgkqhkiG9w0BAQUFAAOCAQEA
MdduKhJXM4HVncbr+TrURE0Inu5e32pbt3nPApy8dmiekKGcC8N/oozxTbqVOfsN
4OGb9F0kDxuNiBU6fNutzrPJbLo5LEV9JBFUJjANDf9H6gMH5eRmXSx7nR2pEPoc
sHTyT2lrnqkkhNrtlqDfc6TvahqsS2Ke8XzAFH9IzU2yRPnwPJNtQtjofOYXoJto
aAko+QKX7xEDumdSrcHps3Om0mPNSuI+5PNO/f+h4LsCEztdIN5VP6OukEAxOHUo
XgSpRm3m9Xp5QL0fzehF1a7iXT71dcfmZmNgzNWahIeNJDD37zTQYx2xQmdKDku/
Og7vtpU6pzjkJZIIpohmgg=="));
using (RSA rsa = RSA.Create())
{
rsa.ExportParameters(false);
}
}
[Benchmark]
public void RSASign()
{
Span<byte> hash = stackalloc byte[256 >> 3];
Span<byte> destination = stackalloc byte[_rsa.KeySize >> 3];
_rsa.TrySignHash(hash, destination, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1, out _);
}
[Benchmark]
public void RSAVerify()
{
Span<byte> hash = stackalloc byte[256 >> 3];
_rsa.VerifyHash(hash, _sig, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1);
}
[Benchmark]
public void RSAEncrypt()
{
Span<byte> source = stackalloc byte[3];
Span<byte> destination = stackalloc byte[_rsa.KeySize >> 3];
_rsa.TryEncrypt(source, destination, RSAEncryptionPadding.OaepSHA256, out _);
}
[Benchmark]
public void RSADecrypt()
{
Span<byte> destination = stackalloc byte[_rsa.KeySize >> 3];
_rsa.TryDecrypt(_enc, destination, RSAEncryptionPadding.OaepSHA256, out _);
}
[Benchmark]
public void RSAVerifyFromCert()
{
using (RSA rsa = s_cert.GetRSAPublicKey())
{
Span<byte> sig = stackalloc byte[rsa.KeySize >> 3];
ReadOnlySpan<byte> hash = sig.Slice(0, 256 >> 3);
rsa.VerifyHash(hash, sig, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1);
}
} |
src/libraries/Common/src/Interop/Windows/BCrypt/Interop.BCryptEncryptDecrypt.RSA.cs
Outdated
Show resolved
Hide resolved
src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/RSABCrypt.cs
Outdated
Show resolved
Hide resolved
src/libraries/Common/src/Interop/Windows/BCrypt/Interop.BCryptGenerateKeyPair.cs
Show resolved
Hide resolved
src/libraries/Common/src/Interop/Windows/BCrypt/Interop.BCryptOpenAlgorithmProvider.cs
Show resolved
Hide resolved
src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/RSABCrypt.cs
Outdated
Show resolved
Hide resolved
src/libraries/Common/src/Interop/Windows/BCrypt/Interop.BCryptVerifySignature.cs
Outdated
Show resolved
Hide resolved
There's a decent amount of new p/invoke in here. Perhaps it would be worthwhile to run tests with |
All of the finalizations of SafeBCryptKeyHandle come via tests not disposing, or via X509Certificate2.CreateFromPem. Internally it looks OK. |
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.
Looks good. Had some questions / suggestions but they are not things I would complain about if it got merged as-is.
Windows CNG has two different libraries: bcrypt.dll (
BCrypt*
functions) for in-memory/ephemeral operations, and ncrypt.dll (NCrypt*
functions) for persisted key operations. Since the NCrypt functions can also operate on ephemeral keys our cryptographic operations have generally been done in terms of NCrypt.NCrypt's flexibility (to also work with persisted keys) comes at a cost. All key operations are done out-of-process (in lsass.exe), and that requires an (L)RPC call for every operation. It also means there are simply more moving parts, and thus more room for error.
With this change we will use BCrypt for RSA operations on Windows from
RSA.Create()
andcert.GetRSAPublicKey()
. ECDSA/ECDH/DSA can any/all be changed to follow suit later.For keys created from RSA.Create() a verification operation currently looks like
After this change it is instead
Removing all of those other indirections removes about 40us from a 50us operation (on my primary devbox).
As part of making some code be shared between RSACng and RSABCrypt, some allocating code changed to pooling code and some array code got spanified.
Might contributes to #25979, either way it improves perf.