Skip to content

Commit

Permalink
Use BCrypt for ephemeral RSA on Windows
Browse files Browse the repository at this point in the history
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()` and `cert.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

* Virtual invoke to RSAWrapper.VerifyHash
* Maybe-devirtualized invoke to RSACng.VerifyHash
* P/Invoke to NCryptVerifySignature
* "Virtual" invoke to MSKSPVerifySignature (or whatever it's really called)
* LRPC call
* Find key in the MRU ring
* Effectively a call to BCryptVerifySignature

After this change it is instead

* Virtual invoke to RSABCrypt.VerifyHash
* P/Invoke to BCryptVerifySignature

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.
  • Loading branch information
bartonjs authored Sep 30, 2022
1 parent 90d9034 commit fbc2232
Show file tree
Hide file tree
Showing 22 changed files with 1,381 additions and 360 deletions.
Original file line number Diff line number Diff line change
@@ -1,10 +1,8 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System;
using System.Diagnostics;
using System.Collections.Concurrent;
using System.Security.Cryptography;

using Microsoft.Win32.SafeHandles;

Expand All @@ -31,30 +29,12 @@ public static unsafe SafeBCryptAlgorithmHandle GetCachedBCryptAlgorithmHandle(st
return result.Handle;
}

NTSTATUS ntStatus = Interop.BCrypt.BCryptOpenAlgorithmProvider(out SafeBCryptAlgorithmHandle handle, key.hashAlgorithmId, null, key.flags);
if (ntStatus != NTSTATUS.STATUS_SUCCESS)
{
Exception e = Interop.BCrypt.CreateCryptographicException(ntStatus);
handle.Dispose();
throw e;
}

int hashSize;
ntStatus = Interop.BCrypt.BCryptGetProperty(
handle,
Interop.BCrypt.BCryptPropertyStrings.BCRYPT_HASH_LENGTH,
&hashSize,
sizeof(int),
out int cbHashSize,
0);
if (ntStatus != NTSTATUS.STATUS_SUCCESS)
{
Exception e = Interop.BCrypt.CreateCryptographicException(ntStatus);
handle.Dispose();
throw e;
}
SafeBCryptAlgorithmHandle handle = BCryptOpenAlgorithmProvider(
key.hashAlgorithmId,
null,
key.flags);

Debug.Assert(cbHashSize == sizeof(int));
int hashSize = BCryptGetDWordProperty(handle, BCryptPropertyStrings.BCRYPT_HASH_LENGTH);
Debug.Assert(hashSize > 0);

if (!s_handles.TryAdd(key, (handle, hashSize)))
Expand Down
5 changes: 4 additions & 1 deletion src/libraries/Common/src/Interop/Windows/BCrypt/Cng.cs
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,10 @@ public enum OpenAlgorithmProviderFlags : int
public const string BCRYPT_CHAIN_MODE_CFB = "ChainingModeCFB";
public const string BCRYPT_CHAIN_MODE_CCM = "ChainingModeCCM";

public static SafeAlgorithmHandle BCryptOpenAlgorithmProvider(string pszAlgId, string? pszImplementation, OpenAlgorithmProviderFlags dwFlags)
public static SafeAlgorithmHandle BCryptOpenAlgorithmProvider(
string pszAlgId,
string? pszImplementation = null,
OpenAlgorithmProviderFlags dwFlags = 0)
{
SafeAlgorithmHandle hAlgorithm;
NTSTATUS ntStatus = Interop.BCryptOpenAlgorithmProvider(out hAlgorithm, pszAlgId, pszImplementation, (int)dwFlags);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,205 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System;
using System.Runtime.InteropServices;

using Microsoft.Win32.SafeHandles;

internal static partial class Interop
{
internal static partial class BCrypt
{
[Flags]
private enum BCryptEncryptFlags : uint
{
BCRYPT_PAD_PKCS1 = 2,
BCRYPT_PAD_OAEP = 4,
}

[LibraryImport(Libraries.BCrypt)]
private static unsafe partial NTSTATUS BCryptEncrypt(
SafeBCryptKeyHandle hKey,
byte* pbInput,
int cbInput,
void* paddingInfo,
byte* pbIV,
int cbIV,
byte* pbOutput,
int cbOutput,
out int cbResult,
BCryptEncryptFlags dwFlags);

[LibraryImport(Libraries.BCrypt)]
private static unsafe partial NTSTATUS BCryptDecrypt(
SafeBCryptKeyHandle hKey,
byte* pbInput,
int cbInput,
void* paddingInfo,
byte* pbIV,
int cbIV,
byte* pbOutput,
int cbOutput,
out int cbResult,
BCryptEncryptFlags dwFlags);

private static unsafe int BCryptEncryptRsa(
SafeBCryptKeyHandle key,
ReadOnlySpan<byte> source,
Span<byte> destination,
void* pPaddingInfo,
BCryptEncryptFlags dwFlags)
{
// BCryptEncrypt does not accept null/0, only non-null/0.
Span<byte> notNull = stackalloc byte[1];
scoped ReadOnlySpan<byte> effectiveSource;

if (source.IsEmpty)
{
effectiveSource = notNull.Slice(1);
}
else
{
effectiveSource = source;
}

NTSTATUS status;
int written;

fixed (byte* pSource = &MemoryMarshal.GetReference(effectiveSource))
fixed (byte* pDest = &MemoryMarshal.GetReference(destination))
{
status = BCryptEncrypt(
key,
pSource,
source.Length,
pPaddingInfo,
null,
0,
pDest,
destination.Length,
out written,
dwFlags);
}

if (status != NTSTATUS.STATUS_SUCCESS)
{
throw CreateCryptographicException(status);
}

return written;
}

private static unsafe bool BCryptDecryptRsa(
SafeBCryptKeyHandle key,
ReadOnlySpan<byte> source,
Span<byte> destination,
void* pPaddingInfo,
BCryptEncryptFlags dwFlags,
out int bytesWritten)
{
NTSTATUS status;
int written;

fixed (byte* pSource = &MemoryMarshal.GetReference(source))
fixed (byte* pDest = &MemoryMarshal.GetReference(destination))
{
status = BCryptDecrypt(
key,
pSource,
source.Length,
pPaddingInfo,
null,
0,
pDest,
destination.Length,
out written,
dwFlags);
}

if (status == NTSTATUS.STATUS_SUCCESS)
{
bytesWritten = written;
return true;
}

if (status == NTSTATUS.STATUS_BUFFER_TOO_SMALL)
{
bytesWritten = 0;
return false;
}

throw CreateCryptographicException(status);
}

internal static unsafe int BCryptEncryptPkcs1(
SafeBCryptKeyHandle key,
ReadOnlySpan<byte> source,
Span<byte> destination)
{
return BCryptEncryptRsa(
key,
source,
destination,
null,
BCryptEncryptFlags.BCRYPT_PAD_PKCS1);
}

internal static unsafe int BCryptEncryptOaep(
SafeBCryptKeyHandle key,
ReadOnlySpan<byte> source,
Span<byte> destination,
string? hashAlgorithmName)
{
fixed (char* pHashAlg = hashAlgorithmName)
{
BCRYPT_OAEP_PADDING_INFO paddingInfo = default;
paddingInfo.pszAlgId = (IntPtr)pHashAlg;

return BCryptEncryptRsa(
key,
source,
destination,
&paddingInfo,
BCryptEncryptFlags.BCRYPT_PAD_OAEP);
}
}

internal static unsafe bool BCryptDecryptPkcs1(
SafeBCryptKeyHandle key,
ReadOnlySpan<byte> source,
Span<byte> destination,
out int bytesWritten)
{
return BCryptDecryptRsa(
key,
source,
destination,
null,
BCryptEncryptFlags.BCRYPT_PAD_PKCS1,
out bytesWritten);
}

internal static unsafe bool BCryptDecryptOaep(
SafeBCryptKeyHandle key,
ReadOnlySpan<byte> source,
Span<byte> destination,
string? hashAlgorithmName,
out int bytesWritten)
{
fixed (char* pHashAlg = hashAlgorithmName)
{
BCRYPT_OAEP_PADDING_INFO paddingInfo = default;
paddingInfo.pszAlgId = (IntPtr)pHashAlg;

return BCryptDecryptRsa(
key,
source,
destination,
&paddingInfo,
BCryptEncryptFlags.BCRYPT_PAD_OAEP,
out bytesWritten);
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@
// The .NET Foundation licenses this file to you under the MIT license.

using System;
using System.Diagnostics;
using System.Runtime.InteropServices;
using System.Security.Cryptography;

using Microsoft.Win32.SafeHandles;

Expand All @@ -12,6 +12,34 @@ internal static partial class Interop
internal static partial class BCrypt
{
[LibraryImport(Libraries.BCrypt, StringMarshalling = StringMarshalling.Utf16)]
internal static partial NTSTATUS BCryptExportKey(SafeBCryptKeyHandle hKey, IntPtr hExportKey, string pszBlobType, byte[]? pbOutput, int cbOutput, out int pcbResult, int dwFlags);
private static partial NTSTATUS BCryptExportKey(
SafeBCryptKeyHandle hKey,
IntPtr hExportKey,
string pszBlobType,
byte[]? pbOutput,
int cbOutput,
out int pcbResult,
int dwFlags);

internal static ArraySegment<byte> BCryptExportKey(SafeBCryptKeyHandle key, string blobType)
{
int numBytesNeeded;
NTSTATUS ntStatus = BCryptExportKey(key, IntPtr.Zero, blobType, null, 0, out numBytesNeeded, 0);

if (ntStatus != NTSTATUS.STATUS_SUCCESS)
{
throw CreateCryptographicException(ntStatus);
}

byte[] rented = CryptoPool.Rent(numBytesNeeded);
ntStatus = BCryptExportKey(key, IntPtr.Zero, blobType, rented, numBytesNeeded, out numBytesNeeded, 0);

if (ntStatus != NTSTATUS.STATUS_SUCCESS)
{
throw CreateCryptographicException(ntStatus);
}

return new ArraySegment<byte>(rented, 0, numBytesNeeded);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System;
using System.Runtime.InteropServices;

using Microsoft.Win32.SafeHandles;

internal static partial class Interop
{
internal static partial class BCrypt
{
[LibraryImport(Libraries.BCrypt)]
private static unsafe partial NTSTATUS BCryptFinalizeKeyPair(
SafeBCryptKeyHandle hKey,
uint dwFlags);

internal static void BCryptFinalizeKeyPair(SafeBCryptKeyHandle key)
{
NTSTATUS status = BCryptFinalizeKeyPair(key, 0);

if (status != NTSTATUS.STATUS_SUCCESS)
{
throw CreateCryptographicException(status);
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System.Runtime.InteropServices;

using Microsoft.Win32.SafeHandles;

internal static partial class Interop
{
internal static partial class BCrypt
{
[LibraryImport(Libraries.BCrypt)]
private static unsafe partial NTSTATUS BCryptGenerateKeyPair(
SafeBCryptAlgorithmHandle hAlgorithm,
out SafeBCryptKeyHandle phKey,
int dwLength,
uint dwFlags);

internal static SafeBCryptKeyHandle BCryptGenerateKeyPair(
SafeBCryptAlgorithmHandle hAlgorithm,
int keyLength)
{
NTSTATUS status = BCryptGenerateKeyPair(
hAlgorithm,
out SafeBCryptKeyHandle hKey,
keyLength,
0);

if (status != NTSTATUS.STATUS_SUCCESS)
{
hKey.Dispose();
throw CreateCryptographicException(status);
}

return hKey;
}
}
}
Loading

0 comments on commit fbc2232

Please sign in to comment.