Skip to content

Commit

Permalink
Add support for SP800-108 CTR Key Derivation Function
Browse files Browse the repository at this point in the history
This introduces a class for producing values per the "KDF in Counter Mode" section in NIST Special Publication 800-108r1 (Recommendation for Key Derivation Using Pseudorandom Functions).

The `SP800108HmacCounterKdf` class is part of the inbox cryptography library on .NET 8.  Based on demonstrated need for older TFMs, the type is also being exposed via a NuGet package: Microsoft.Bcl.Cryptography.  This package may, in the future, contain other types that belong as part of inbox cryptography but have a demonstrated need to be available to older TFMs.
  • Loading branch information
vcsjones committed Feb 7, 2023
1 parent 32ea339 commit d7bf603
Show file tree
Hide file tree
Showing 30 changed files with 3,171 additions and 1 deletion.
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,15 @@ public enum BCryptAlgPseudoHandle : uint
BCRYPT_PBKDF2_ALG_HANDLE = 0x00000331,
}

internal static bool PseudoHandlesSupported { get; } = OperatingSystem.IsWindowsVersionAtLeast(10, 0, 0);
internal static bool PseudoHandlesSupported { get; } =
#if NET
OperatingSystem.IsWindowsVersionAtLeast(10, 0, 0);
#elif NETSTANDARD2_0_OR_GREATER
RuntimeInformation.IsOSPlatform(OSPlatform.Windows) && Environment.OSVersion.Version.Major >= 10;

This comment has been minimized.

Copy link
@digital-pccc
#elif NETFRAMEWORK
Environment.OSVersion.Version.Major >= 10;
#else
#error Unhandled platform targets
#endif
}
}

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

namespace System.Security.Cryptography
{
internal abstract class SP800108HmacCounterKdfImplementationBase : IDisposable
{
internal abstract void DeriveBytes(ReadOnlySpan<byte> label, ReadOnlySpan<byte> context, Span<byte> destination);
internal abstract void DeriveBytes(byte[] label, byte[] context, Span<byte> destination);
internal abstract void DeriveBytes(ReadOnlySpan<char> label, ReadOnlySpan<char> context, Span<byte> destination);

public abstract void Dispose();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,211 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using Microsoft.Win32.SafeHandles;
using System.Diagnostics;

using BCryptBuffer = Interop.BCrypt.BCryptBuffer;
using CngBufferDescriptors = Interop.BCrypt.CngBufferDescriptors;
using NTSTATUS = Interop.BCrypt.NTSTATUS;

namespace System.Security.Cryptography
{
internal sealed partial class SP800108HmacCounterKdfImplementationCng : SP800108HmacCounterKdfImplementationBase
{
private const string BCRYPT_SP800108_CTR_HMAC_ALGORITHM = "SP800_108_CTR_HMAC";
private const nuint BCRYPT_SP800108_CTR_HMAC_ALG_HANDLE = 0x00000341;
private const int CharToBytesStackBufferSize = 256;

// A cached algorithm handle. On Windows 10 this is null if we are using a psuedo handle.
private static readonly SafeBCryptAlgorithmHandle? s_sp800108CtrHmacAlgorithmHandle = OpenAlgorithmHandle();

private readonly SafeBCryptKeyHandle _keyHandle;
private readonly HashAlgorithmName _hashAlgorithm;

public override void Dispose()
{
_keyHandle.Dispose();
}

internal override void DeriveBytes(byte[] label, byte[] context, Span<byte> destination)
{
DeriveBytes(new ReadOnlySpan<byte>(label), new ReadOnlySpan<byte>(context), destination);
}

internal override unsafe void DeriveBytes(ReadOnlySpan<byte> label, ReadOnlySpan<byte> context, Span<byte> destination)
{
if (destination.Length == 0)
{
return;
}

Debug.Assert(destination.Length <= 0x1FFFFFFF);
Debug.Assert(_hashAlgorithm.Name is not null);

fixed (byte* pLabel = label)
fixed (byte* pContext = context)
fixed (byte* pDestination = destination)
fixed (char* pHashAlgorithm = _hashAlgorithm.Name)
{
const int BCryptBufferLength = 3;
BCryptBuffer* buffers = stackalloc BCryptBuffer[BCryptBufferLength];

buffers[0].BufferType = CngBufferDescriptors.KDF_LABEL;
buffers[0].pvBuffer = (IntPtr)pLabel;
buffers[0].cbBuffer = label.Length;
buffers[1].BufferType = CngBufferDescriptors.KDF_CONTEXT;
buffers[1].pvBuffer = (IntPtr)pContext;
buffers[1].cbBuffer = context.Length;
buffers[2].BufferType = CngBufferDescriptors.KDF_HASH_ALGORITHM;
buffers[2].pvBuffer = (IntPtr)pHashAlgorithm;
buffers[2].cbBuffer = (_hashAlgorithm.Name.Length + 1) * 2; // +1 for the null terminator.

Interop.BCrypt.BCryptBufferDesc bufferDesc;
bufferDesc.ulVersion = Interop.BCrypt.BCRYPTBUFFER_VERSION;
bufferDesc.cBuffers = BCryptBufferLength;
bufferDesc.pBuffers = (IntPtr)buffers;

NTSTATUS deriveStatus = Interop.BCrypt.BCryptKeyDerivation(
_keyHandle,
&bufferDesc,
pDestination,
destination.Length,
out uint resultLength,
dwFlags: 0);

if (deriveStatus != NTSTATUS.STATUS_SUCCESS)
{
throw Interop.BCrypt.CreateCryptographicException(deriveStatus);
}

if (destination.Length != resultLength)
{
Debug.Fail("BCryptKeyDerivation resultLength != destination.Length");
throw new CryptographicException();
}
}
}

internal override void DeriveBytes(ReadOnlySpan<char> label, ReadOnlySpan<char> context, Span<byte> destination)
{
using (Utf8DataEncoding labelData = new Utf8DataEncoding(label, stackalloc byte[CharToBytesStackBufferSize]))
using (Utf8DataEncoding contextData = new Utf8DataEncoding(context, stackalloc byte[CharToBytesStackBufferSize]))
{
DeriveBytes(labelData.Utf8Bytes, contextData.Utf8Bytes, destination);
}
}

internal static void DeriveBytesOneShot(
ReadOnlySpan<byte> key,
HashAlgorithmName hashAlgorithm,
ReadOnlySpan<byte> label,
ReadOnlySpan<byte> context,
Span<byte> destination)
{
Debug.Assert(destination.Length <= 0x1FFFFFFF);

using (SP800108HmacCounterKdfImplementationCng kdf = new SP800108HmacCounterKdfImplementationCng(key, hashAlgorithm))
{
kdf.DeriveBytes(label, context, destination);
}
}

internal static void DeriveBytesOneShot(
ReadOnlySpan<byte> key,
HashAlgorithmName hashAlgorithm,
ReadOnlySpan<char> label,
ReadOnlySpan<char> context,
Span<byte> destination)
{
if (destination.Length == 0)
{
return;
}

using (Utf8DataEncoding labelData = new Utf8DataEncoding(label, stackalloc byte[CharToBytesStackBufferSize]))
using (Utf8DataEncoding contextData = new Utf8DataEncoding(context, stackalloc byte[CharToBytesStackBufferSize]))
{
DeriveBytesOneShot(key, hashAlgorithm, labelData.Utf8Bytes, contextData.Utf8Bytes, destination);
}
}

private static unsafe SafeBCryptKeyHandle CreateSymmetricKey(byte* symmetricKey, int symmetricKeyLength)
{
NTSTATUS generateKeyStatus;
SafeBCryptKeyHandle keyHandle;

if (s_sp800108CtrHmacAlgorithmHandle is not null)
{
generateKeyStatus = Interop.BCrypt.BCryptGenerateSymmetricKey(
s_sp800108CtrHmacAlgorithmHandle,
out keyHandle,
pbKeyObject: IntPtr.Zero,
cbKeyObject: 0,
symmetricKey,
symmetricKeyLength,
dwFlags: 0);
}
else
{
generateKeyStatus = Interop.BCrypt.BCryptGenerateSymmetricKey(
BCRYPT_SP800108_CTR_HMAC_ALG_HANDLE,
out keyHandle,
pbKeyObject: IntPtr.Zero,
cbKeyObject: 0,
symmetricKey,
symmetricKeyLength,
dwFlags: 0);
}

if (generateKeyStatus != NTSTATUS.STATUS_SUCCESS)
{
keyHandle.Dispose();
throw Interop.BCrypt.CreateCryptographicException(generateKeyStatus);
}

Debug.Assert(!keyHandle.IsInvalid);

return keyHandle;
}

// Returns null if the platform is Windows 10+ and psuedo handles should be used.
private static SafeBCryptAlgorithmHandle? OpenAlgorithmHandle()
{
if (!Interop.BCrypt.PseudoHandlesSupported)
{
NTSTATUS openStatus = Interop.BCrypt.BCryptOpenAlgorithmProvider(
out SafeBCryptAlgorithmHandle sp800108CtrHmacAlgorithmHandle,
BCRYPT_SP800108_CTR_HMAC_ALGORITHM,
null,
Interop.BCrypt.BCryptOpenAlgorithmProviderFlags.None);

if (openStatus != NTSTATUS.STATUS_SUCCESS)
{
sp800108CtrHmacAlgorithmHandle.Dispose();
throw Interop.BCrypt.CreateCryptographicException(openStatus);
}

return sp800108CtrHmacAlgorithmHandle;
}

return null;
}

private static int GetHashBlockSize(string hashAlgorithmName)
{
// Block sizes per NIST FIPS pub 180-4.
switch (hashAlgorithmName)
{
case HashAlgorithmNames.SHA1:
case HashAlgorithmNames.SHA256:
return 512 / 8;
case HashAlgorithmNames.SHA384:
case HashAlgorithmNames.SHA512:
return 1024 / 8;
default:
Debug.Fail($"Unexpected hash algorithm '{hashAlgorithmName}'");
throw new CryptographicException();
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System.Buffers.Binary;
using System.Diagnostics;
using System.Threading;
using System.Runtime.Versioning;

#pragma warning disable CA1513

namespace System.Security.Cryptography
{
#if !NET7_0_OR_GREATER && NET
[UnsupportedOSPlatform("browser")]
#endif
internal sealed partial class SP800108HmacCounterKdfImplementationManaged : SP800108HmacCounterKdfImplementationBase
{
private byte[] _key;
private int _keyReferenceCount = 1;
private int _disposed;
private readonly HashAlgorithmName _hashAlgorithm;

internal override void DeriveBytes(ReadOnlySpan<byte> label, ReadOnlySpan<byte> context, Span<byte> destination)
{
byte[] key = IncrementAndAcquireKey();

try
{
DeriveBytesOneShot(key, _hashAlgorithm, label, context, destination);
}
finally
{
ReleaseKey();
}
}

internal override void DeriveBytes(ReadOnlySpan<char> label, ReadOnlySpan<char> context, Span<byte> destination)
{
byte[] key = IncrementAndAcquireKey();

try
{
DeriveBytesOneShot(key, _hashAlgorithm, label, context, destination);
}
finally
{
ReleaseKey();
}
}

internal override void DeriveBytes(byte[] label, byte[] context, Span<byte> destination)
{
byte[] key = IncrementAndAcquireKey();

try
{
DeriveBytesOneShot(key, _hashAlgorithm, label, context, destination);
}
finally
{
ReleaseKey();
}
}

public override void Dispose()
{
if (Interlocked.Exchange(ref _disposed, 1) == 0)
{
ReleaseKey();
}
}

private byte[] IncrementAndAcquireKey()
{
while (true)
{
int current = Volatile.Read(ref _keyReferenceCount);

if (current == 0)
{
throw new ObjectDisposedException(nameof(SP800108HmacCounterKdfImplementationManaged));
}

Debug.Assert(current > 0);
int incrementedCount = checked(current + 1);

if (Interlocked.CompareExchange(ref _keyReferenceCount, incrementedCount, current) == current)
{
return _key;
}
}
}

public void ReleaseKey()
{
int newReferenceCount = Interlocked.Decrement(ref _keyReferenceCount);
Debug.Assert(newReferenceCount >= 0, newReferenceCount.ToString());

if (newReferenceCount == 0)
{
ZeroKey();
}
}

private void ZeroKey()
{
CryptographicOperations.ZeroMemory(_key);
_key = null!;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System.Text;

namespace System.Security.Cryptography
{
internal readonly ref struct Utf8DataEncoding
{
internal static Encoding ThrowingUtf8Encoding { get; } = new UTF8Encoding(false, true);

private readonly byte[]? _rented;
private readonly Span<byte> _buffer;

internal Utf8DataEncoding(ReadOnlySpan<char> data, Span<byte> stackBuffer)
{
int maxLength = ThrowingUtf8Encoding.GetMaxByteCount(data.Length);
_buffer = (uint)maxLength <= stackBuffer.Length ?
stackBuffer :
(_rented = CryptoPool.Rent(maxLength));

int written = ThrowingUtf8Encoding.GetBytes(data, _buffer);
_buffer = _buffer.Slice(0, written);
}

internal ReadOnlySpan<byte> Utf8Bytes => _buffer;

internal void Dispose()
{
CryptographicOperations.ZeroMemory(_buffer);

if (_rented is not null)
{
CryptoPool.Return(_rented, clearSize: 0);
}
}
}

}
Loading

0 comments on commit d7bf603

Please sign in to comment.