Skip to content

Commit d7bf603

Browse files
authored
Add support for SP800-108 CTR Key Derivation Function
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.
1 parent 32ea339 commit d7bf603

30 files changed

+3171
-1
lines changed

src/libraries/Common/src/Interop/Windows/BCrypt/Interop.BCryptAlgPseudoHandle.cs

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,15 @@ public enum BCryptAlgPseudoHandle : uint
2525
BCRYPT_PBKDF2_ALG_HANDLE = 0x00000331,
2626
}
2727

28-
internal static bool PseudoHandlesSupported { get; } = OperatingSystem.IsWindowsVersionAtLeast(10, 0, 0);
28+
internal static bool PseudoHandlesSupported { get; } =
29+
#if NET
30+
OperatingSystem.IsWindowsVersionAtLeast(10, 0, 0);
31+
#elif NETSTANDARD2_0_OR_GREATER
32+
RuntimeInformation.IsOSPlatform(OSPlatform.Windows) && Environment.OSVersion.Version.Major >= 10;
33+
#elif NETFRAMEWORK
34+
Environment.OSVersion.Version.Major >= 10;
35+
#else
36+
#error Unhandled platform targets
37+
#endif
2938
}
3039
}

src/libraries/Common/src/System/Security/Cryptography/SP800108HmacCounterKdf.cs

Lines changed: 611 additions & 0 deletions
Large diffs are not rendered by default.
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
namespace System.Security.Cryptography
5+
{
6+
internal abstract class SP800108HmacCounterKdfImplementationBase : IDisposable
7+
{
8+
internal abstract void DeriveBytes(ReadOnlySpan<byte> label, ReadOnlySpan<byte> context, Span<byte> destination);
9+
internal abstract void DeriveBytes(byte[] label, byte[] context, Span<byte> destination);
10+
internal abstract void DeriveBytes(ReadOnlySpan<char> label, ReadOnlySpan<char> context, Span<byte> destination);
11+
12+
public abstract void Dispose();
13+
}
14+
}
Lines changed: 211 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,211 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
using Microsoft.Win32.SafeHandles;
5+
using System.Diagnostics;
6+
7+
using BCryptBuffer = Interop.BCrypt.BCryptBuffer;
8+
using CngBufferDescriptors = Interop.BCrypt.CngBufferDescriptors;
9+
using NTSTATUS = Interop.BCrypt.NTSTATUS;
10+
11+
namespace System.Security.Cryptography
12+
{
13+
internal sealed partial class SP800108HmacCounterKdfImplementationCng : SP800108HmacCounterKdfImplementationBase
14+
{
15+
private const string BCRYPT_SP800108_CTR_HMAC_ALGORITHM = "SP800_108_CTR_HMAC";
16+
private const nuint BCRYPT_SP800108_CTR_HMAC_ALG_HANDLE = 0x00000341;
17+
private const int CharToBytesStackBufferSize = 256;
18+
19+
// A cached algorithm handle. On Windows 10 this is null if we are using a psuedo handle.
20+
private static readonly SafeBCryptAlgorithmHandle? s_sp800108CtrHmacAlgorithmHandle = OpenAlgorithmHandle();
21+
22+
private readonly SafeBCryptKeyHandle _keyHandle;
23+
private readonly HashAlgorithmName _hashAlgorithm;
24+
25+
public override void Dispose()
26+
{
27+
_keyHandle.Dispose();
28+
}
29+
30+
internal override void DeriveBytes(byte[] label, byte[] context, Span<byte> destination)
31+
{
32+
DeriveBytes(new ReadOnlySpan<byte>(label), new ReadOnlySpan<byte>(context), destination);
33+
}
34+
35+
internal override unsafe void DeriveBytes(ReadOnlySpan<byte> label, ReadOnlySpan<byte> context, Span<byte> destination)
36+
{
37+
if (destination.Length == 0)
38+
{
39+
return;
40+
}
41+
42+
Debug.Assert(destination.Length <= 0x1FFFFFFF);
43+
Debug.Assert(_hashAlgorithm.Name is not null);
44+
45+
fixed (byte* pLabel = label)
46+
fixed (byte* pContext = context)
47+
fixed (byte* pDestination = destination)
48+
fixed (char* pHashAlgorithm = _hashAlgorithm.Name)
49+
{
50+
const int BCryptBufferLength = 3;
51+
BCryptBuffer* buffers = stackalloc BCryptBuffer[BCryptBufferLength];
52+
53+
buffers[0].BufferType = CngBufferDescriptors.KDF_LABEL;
54+
buffers[0].pvBuffer = (IntPtr)pLabel;
55+
buffers[0].cbBuffer = label.Length;
56+
buffers[1].BufferType = CngBufferDescriptors.KDF_CONTEXT;
57+
buffers[1].pvBuffer = (IntPtr)pContext;
58+
buffers[1].cbBuffer = context.Length;
59+
buffers[2].BufferType = CngBufferDescriptors.KDF_HASH_ALGORITHM;
60+
buffers[2].pvBuffer = (IntPtr)pHashAlgorithm;
61+
buffers[2].cbBuffer = (_hashAlgorithm.Name.Length + 1) * 2; // +1 for the null terminator.
62+
63+
Interop.BCrypt.BCryptBufferDesc bufferDesc;
64+
bufferDesc.ulVersion = Interop.BCrypt.BCRYPTBUFFER_VERSION;
65+
bufferDesc.cBuffers = BCryptBufferLength;
66+
bufferDesc.pBuffers = (IntPtr)buffers;
67+
68+
NTSTATUS deriveStatus = Interop.BCrypt.BCryptKeyDerivation(
69+
_keyHandle,
70+
&bufferDesc,
71+
pDestination,
72+
destination.Length,
73+
out uint resultLength,
74+
dwFlags: 0);
75+
76+
if (deriveStatus != NTSTATUS.STATUS_SUCCESS)
77+
{
78+
throw Interop.BCrypt.CreateCryptographicException(deriveStatus);
79+
}
80+
81+
if (destination.Length != resultLength)
82+
{
83+
Debug.Fail("BCryptKeyDerivation resultLength != destination.Length");
84+
throw new CryptographicException();
85+
}
86+
}
87+
}
88+
89+
internal override void DeriveBytes(ReadOnlySpan<char> label, ReadOnlySpan<char> context, Span<byte> destination)
90+
{
91+
using (Utf8DataEncoding labelData = new Utf8DataEncoding(label, stackalloc byte[CharToBytesStackBufferSize]))
92+
using (Utf8DataEncoding contextData = new Utf8DataEncoding(context, stackalloc byte[CharToBytesStackBufferSize]))
93+
{
94+
DeriveBytes(labelData.Utf8Bytes, contextData.Utf8Bytes, destination);
95+
}
96+
}
97+
98+
internal static void DeriveBytesOneShot(
99+
ReadOnlySpan<byte> key,
100+
HashAlgorithmName hashAlgorithm,
101+
ReadOnlySpan<byte> label,
102+
ReadOnlySpan<byte> context,
103+
Span<byte> destination)
104+
{
105+
Debug.Assert(destination.Length <= 0x1FFFFFFF);
106+
107+
using (SP800108HmacCounterKdfImplementationCng kdf = new SP800108HmacCounterKdfImplementationCng(key, hashAlgorithm))
108+
{
109+
kdf.DeriveBytes(label, context, destination);
110+
}
111+
}
112+
113+
internal static void DeriveBytesOneShot(
114+
ReadOnlySpan<byte> key,
115+
HashAlgorithmName hashAlgorithm,
116+
ReadOnlySpan<char> label,
117+
ReadOnlySpan<char> context,
118+
Span<byte> destination)
119+
{
120+
if (destination.Length == 0)
121+
{
122+
return;
123+
}
124+
125+
using (Utf8DataEncoding labelData = new Utf8DataEncoding(label, stackalloc byte[CharToBytesStackBufferSize]))
126+
using (Utf8DataEncoding contextData = new Utf8DataEncoding(context, stackalloc byte[CharToBytesStackBufferSize]))
127+
{
128+
DeriveBytesOneShot(key, hashAlgorithm, labelData.Utf8Bytes, contextData.Utf8Bytes, destination);
129+
}
130+
}
131+
132+
private static unsafe SafeBCryptKeyHandle CreateSymmetricKey(byte* symmetricKey, int symmetricKeyLength)
133+
{
134+
NTSTATUS generateKeyStatus;
135+
SafeBCryptKeyHandle keyHandle;
136+
137+
if (s_sp800108CtrHmacAlgorithmHandle is not null)
138+
{
139+
generateKeyStatus = Interop.BCrypt.BCryptGenerateSymmetricKey(
140+
s_sp800108CtrHmacAlgorithmHandle,
141+
out keyHandle,
142+
pbKeyObject: IntPtr.Zero,
143+
cbKeyObject: 0,
144+
symmetricKey,
145+
symmetricKeyLength,
146+
dwFlags: 0);
147+
}
148+
else
149+
{
150+
generateKeyStatus = Interop.BCrypt.BCryptGenerateSymmetricKey(
151+
BCRYPT_SP800108_CTR_HMAC_ALG_HANDLE,
152+
out keyHandle,
153+
pbKeyObject: IntPtr.Zero,
154+
cbKeyObject: 0,
155+
symmetricKey,
156+
symmetricKeyLength,
157+
dwFlags: 0);
158+
}
159+
160+
if (generateKeyStatus != NTSTATUS.STATUS_SUCCESS)
161+
{
162+
keyHandle.Dispose();
163+
throw Interop.BCrypt.CreateCryptographicException(generateKeyStatus);
164+
}
165+
166+
Debug.Assert(!keyHandle.IsInvalid);
167+
168+
return keyHandle;
169+
}
170+
171+
// Returns null if the platform is Windows 10+ and psuedo handles should be used.
172+
private static SafeBCryptAlgorithmHandle? OpenAlgorithmHandle()
173+
{
174+
if (!Interop.BCrypt.PseudoHandlesSupported)
175+
{
176+
NTSTATUS openStatus = Interop.BCrypt.BCryptOpenAlgorithmProvider(
177+
out SafeBCryptAlgorithmHandle sp800108CtrHmacAlgorithmHandle,
178+
BCRYPT_SP800108_CTR_HMAC_ALGORITHM,
179+
null,
180+
Interop.BCrypt.BCryptOpenAlgorithmProviderFlags.None);
181+
182+
if (openStatus != NTSTATUS.STATUS_SUCCESS)
183+
{
184+
sp800108CtrHmacAlgorithmHandle.Dispose();
185+
throw Interop.BCrypt.CreateCryptographicException(openStatus);
186+
}
187+
188+
return sp800108CtrHmacAlgorithmHandle;
189+
}
190+
191+
return null;
192+
}
193+
194+
private static int GetHashBlockSize(string hashAlgorithmName)
195+
{
196+
// Block sizes per NIST FIPS pub 180-4.
197+
switch (hashAlgorithmName)
198+
{
199+
case HashAlgorithmNames.SHA1:
200+
case HashAlgorithmNames.SHA256:
201+
return 512 / 8;
202+
case HashAlgorithmNames.SHA384:
203+
case HashAlgorithmNames.SHA512:
204+
return 1024 / 8;
205+
default:
206+
Debug.Fail($"Unexpected hash algorithm '{hashAlgorithmName}'");
207+
throw new CryptographicException();
208+
}
209+
}
210+
}
211+
}
Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
using System.Buffers.Binary;
5+
using System.Diagnostics;
6+
using System.Threading;
7+
using System.Runtime.Versioning;
8+
9+
#pragma warning disable CA1513
10+
11+
namespace System.Security.Cryptography
12+
{
13+
#if !NET7_0_OR_GREATER && NET
14+
[UnsupportedOSPlatform("browser")]
15+
#endif
16+
internal sealed partial class SP800108HmacCounterKdfImplementationManaged : SP800108HmacCounterKdfImplementationBase
17+
{
18+
private byte[] _key;
19+
private int _keyReferenceCount = 1;
20+
private int _disposed;
21+
private readonly HashAlgorithmName _hashAlgorithm;
22+
23+
internal override void DeriveBytes(ReadOnlySpan<byte> label, ReadOnlySpan<byte> context, Span<byte> destination)
24+
{
25+
byte[] key = IncrementAndAcquireKey();
26+
27+
try
28+
{
29+
DeriveBytesOneShot(key, _hashAlgorithm, label, context, destination);
30+
}
31+
finally
32+
{
33+
ReleaseKey();
34+
}
35+
}
36+
37+
internal override void DeriveBytes(ReadOnlySpan<char> label, ReadOnlySpan<char> context, Span<byte> destination)
38+
{
39+
byte[] key = IncrementAndAcquireKey();
40+
41+
try
42+
{
43+
DeriveBytesOneShot(key, _hashAlgorithm, label, context, destination);
44+
}
45+
finally
46+
{
47+
ReleaseKey();
48+
}
49+
}
50+
51+
internal override void DeriveBytes(byte[] label, byte[] context, Span<byte> destination)
52+
{
53+
byte[] key = IncrementAndAcquireKey();
54+
55+
try
56+
{
57+
DeriveBytesOneShot(key, _hashAlgorithm, label, context, destination);
58+
}
59+
finally
60+
{
61+
ReleaseKey();
62+
}
63+
}
64+
65+
public override void Dispose()
66+
{
67+
if (Interlocked.Exchange(ref _disposed, 1) == 0)
68+
{
69+
ReleaseKey();
70+
}
71+
}
72+
73+
private byte[] IncrementAndAcquireKey()
74+
{
75+
while (true)
76+
{
77+
int current = Volatile.Read(ref _keyReferenceCount);
78+
79+
if (current == 0)
80+
{
81+
throw new ObjectDisposedException(nameof(SP800108HmacCounterKdfImplementationManaged));
82+
}
83+
84+
Debug.Assert(current > 0);
85+
int incrementedCount = checked(current + 1);
86+
87+
if (Interlocked.CompareExchange(ref _keyReferenceCount, incrementedCount, current) == current)
88+
{
89+
return _key;
90+
}
91+
}
92+
}
93+
94+
public void ReleaseKey()
95+
{
96+
int newReferenceCount = Interlocked.Decrement(ref _keyReferenceCount);
97+
Debug.Assert(newReferenceCount >= 0, newReferenceCount.ToString());
98+
99+
if (newReferenceCount == 0)
100+
{
101+
ZeroKey();
102+
}
103+
}
104+
105+
private void ZeroKey()
106+
{
107+
CryptographicOperations.ZeroMemory(_key);
108+
_key = null!;
109+
}
110+
}
111+
}
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
using System.Text;
5+
6+
namespace System.Security.Cryptography
7+
{
8+
internal readonly ref struct Utf8DataEncoding
9+
{
10+
internal static Encoding ThrowingUtf8Encoding { get; } = new UTF8Encoding(false, true);
11+
12+
private readonly byte[]? _rented;
13+
private readonly Span<byte> _buffer;
14+
15+
internal Utf8DataEncoding(ReadOnlySpan<char> data, Span<byte> stackBuffer)
16+
{
17+
int maxLength = ThrowingUtf8Encoding.GetMaxByteCount(data.Length);
18+
_buffer = (uint)maxLength <= stackBuffer.Length ?
19+
stackBuffer :
20+
(_rented = CryptoPool.Rent(maxLength));
21+
22+
int written = ThrowingUtf8Encoding.GetBytes(data, _buffer);
23+
_buffer = _buffer.Slice(0, written);
24+
}
25+
26+
internal ReadOnlySpan<byte> Utf8Bytes => _buffer;
27+
28+
internal void Dispose()
29+
{
30+
CryptographicOperations.ZeroMemory(_buffer);
31+
32+
if (_rented is not null)
33+
{
34+
CryptoPool.Return(_rented, clearSize: 0);
35+
}
36+
}
37+
}
38+
39+
}

0 commit comments

Comments
 (0)