From d7bf603064bbbf86e37f70b7ddf44d85f3c31b1d Mon Sep 17 00:00:00 2001 From: Kevin Jones Date: Tue, 7 Feb 2023 12:25:31 -0500 Subject: [PATCH] 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. --- .../BCrypt/Interop.BCryptAlgPseudoHandle.cs | 11 +- .../Cryptography/SP800108HmacCounterKdf.cs | 611 ++++++++++++++++++ ...P800108HmacCounterKdfImplementationBase.cs | 14 + ...SP800108HmacCounterKdfImplementationCng.cs | 211 ++++++ ...0108HmacCounterKdfImplementationManaged.cs | 111 ++++ .../Security/Cryptography/Utf8DataEncoding.cs | 39 ++ ...800108HmacCounterKdfTests.ArgValidation.cs | 326 ++++++++++ .../SP800108HmacCounterKdfTests.Functional.cs | 357 ++++++++++ .../SP800108HmacCounterKdfTests.Helpers.cs | 169 +++++ ...P800108HmacCounterKdfTests.ThreadSafety.cs | 200 ++++++ .../Microsoft.Bcl.Cryptography.sln | 67 ++ .../Microsoft.Bcl.Cryptography.Forwards.cs | 4 + .../ref/Microsoft.Bcl.Cryptography.cs | 27 + .../ref/Microsoft.Bcl.Cryptography.csproj | 17 + .../src/Microsoft.Bcl.Cryptography.csproj | 79 +++ .../src/Resources/Strings.resx | 72 +++ .../Cryptography/HashAlgorithmNames.cs | 13 + .../Security/Cryptography/NetStandardShims.cs | 34 + .../Cryptography/SP800108HmacCounterKdf.cs | 100 +++ ...SP800108HmacCounterKdfImplementationCng.cs | 113 ++++ ...0108HmacCounterKdfImplementationManaged.cs | 203 ++++++ .../Microsoft.Bcl.Cryptography.Tests.csproj | 22 + .../ref/System.Security.Cryptography.cs | 18 + .../src/Resources/Strings.resx | 6 + .../src/System.Security.Cryptography.csproj | 29 + .../SP800108HmacCounterKdf.Managed.cs | 54 ++ .../SP800108HmacCounterKdf.Windows.cs | 84 +++ ...SP800108HmacCounterKdfImplementationCng.cs | 74 +++ ...0108HmacCounterKdfImplementationManaged.cs | 99 +++ .../System.Security.Cryptography.Tests.csproj | 8 + 30 files changed, 3171 insertions(+), 1 deletion(-) create mode 100644 src/libraries/Common/src/System/Security/Cryptography/SP800108HmacCounterKdf.cs create mode 100644 src/libraries/Common/src/System/Security/Cryptography/SP800108HmacCounterKdfImplementationBase.cs create mode 100644 src/libraries/Common/src/System/Security/Cryptography/SP800108HmacCounterKdfImplementationCng.cs create mode 100644 src/libraries/Common/src/System/Security/Cryptography/SP800108HmacCounterKdfImplementationManaged.cs create mode 100644 src/libraries/Common/src/System/Security/Cryptography/Utf8DataEncoding.cs create mode 100644 src/libraries/Common/tests/System/Security/Cryptography/SP800108HmacCounterKdfTests.ArgValidation.cs create mode 100644 src/libraries/Common/tests/System/Security/Cryptography/SP800108HmacCounterKdfTests.Functional.cs create mode 100644 src/libraries/Common/tests/System/Security/Cryptography/SP800108HmacCounterKdfTests.Helpers.cs create mode 100644 src/libraries/Common/tests/System/Security/Cryptography/SP800108HmacCounterKdfTests.ThreadSafety.cs create mode 100644 src/libraries/Microsoft.Bcl.Cryptography/Microsoft.Bcl.Cryptography.sln create mode 100644 src/libraries/Microsoft.Bcl.Cryptography/ref/Microsoft.Bcl.Cryptography.Forwards.cs create mode 100644 src/libraries/Microsoft.Bcl.Cryptography/ref/Microsoft.Bcl.Cryptography.cs create mode 100644 src/libraries/Microsoft.Bcl.Cryptography/ref/Microsoft.Bcl.Cryptography.csproj create mode 100644 src/libraries/Microsoft.Bcl.Cryptography/src/Microsoft.Bcl.Cryptography.csproj create mode 100644 src/libraries/Microsoft.Bcl.Cryptography/src/Resources/Strings.resx create mode 100644 src/libraries/Microsoft.Bcl.Cryptography/src/System/Security/Cryptography/HashAlgorithmNames.cs create mode 100644 src/libraries/Microsoft.Bcl.Cryptography/src/System/Security/Cryptography/NetStandardShims.cs create mode 100644 src/libraries/Microsoft.Bcl.Cryptography/src/System/Security/Cryptography/SP800108HmacCounterKdf.cs create mode 100644 src/libraries/Microsoft.Bcl.Cryptography/src/System/Security/Cryptography/SP800108HmacCounterKdfImplementationCng.cs create mode 100644 src/libraries/Microsoft.Bcl.Cryptography/src/System/Security/Cryptography/SP800108HmacCounterKdfImplementationManaged.cs create mode 100644 src/libraries/Microsoft.Bcl.Cryptography/tests/Microsoft.Bcl.Cryptography.Tests.csproj create mode 100644 src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/SP800108HmacCounterKdf.Managed.cs create mode 100644 src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/SP800108HmacCounterKdf.Windows.cs create mode 100644 src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/SP800108HmacCounterKdfImplementationCng.cs create mode 100644 src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/SP800108HmacCounterKdfImplementationManaged.cs diff --git a/src/libraries/Common/src/Interop/Windows/BCrypt/Interop.BCryptAlgPseudoHandle.cs b/src/libraries/Common/src/Interop/Windows/BCrypt/Interop.BCryptAlgPseudoHandle.cs index 433e6f515dc..57c508bf709 100644 --- a/src/libraries/Common/src/Interop/Windows/BCrypt/Interop.BCryptAlgPseudoHandle.cs +++ b/src/libraries/Common/src/Interop/Windows/BCrypt/Interop.BCryptAlgPseudoHandle.cs @@ -25,6 +25,15 @@ internal static partial class Interop 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; +#elif NETFRAMEWORK + Environment.OSVersion.Version.Major >= 10; +#else +#error Unhandled platform targets +#endif } } diff --git a/src/libraries/Common/src/System/Security/Cryptography/SP800108HmacCounterKdf.cs b/src/libraries/Common/src/System/Security/Cryptography/SP800108HmacCounterKdf.cs new file mode 100644 index 00000000000..6598136dfe7 --- /dev/null +++ b/src/libraries/Common/src/System/Security/Cryptography/SP800108HmacCounterKdf.cs @@ -0,0 +1,611 @@ +// 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 System.Runtime.Versioning; +using System.Text; + +#pragma warning disable CA1510 + +namespace System.Security.Cryptography +{ + /// + /// NIST SP 800-108 HMAC CTR Key-Based Key Derivation (KBKDF) + /// + /// + /// + /// This implements NIST SP 800-108 HMAC in counter mode. The implemented KDF assumes the form of + /// PRF (KI, [i]2 || Label || 0x00 || Context || [L]2) where [i]2 and [L]2 are encoded as + /// unsigned 32-bit integers, big endian. + /// + /// + /// All members of this class are thread safe. If the instance is disposed of while other threads are using + /// the instance, those threads will either receive an or produce a valid + /// derived key. + /// + /// + public sealed partial class SP800108HmacCounterKdf : IDisposable + { + // The maximum amount of data that we can produce with the PRF is 0x1FFFFFFF. + // This is because of L[2]. From SP 800-108 r1: + // L – An integer specifying the requested length (in bits) of the derived keying material KOUT. + // As an unsigned 32-bit interger (see r), L needs to become L[2] by multiplying by 8 (bytes to bits). + // We can't encode more than 0x1FFFFFFF as bits without overflowing. + // Windows' BCryptKeyDerivation cannot fullfill a request larger than 0x1FFFFFFF, either. + private const int MaxPrfOutputSize = (int)(uint.MaxValue / 8); + + private readonly SP800108HmacCounterKdfImplementationBase _implementation; + + private static partial SP800108HmacCounterKdfImplementationBase CreateImplementation( + ReadOnlySpan key, + HashAlgorithmName hashAlgorithm); + + /// + /// Initializes a new instance of using a specified key and HMAC algorithm. + /// + /// The key-derivation key. + /// The HMAC algorithm. + /// + /// has a which is . + /// + /// + /// has a which is empty. + /// + /// + /// is not a known or supported hash algorithm. + /// + /// + /// The current platform does not have a supported implementation of HMAC. + /// + public SP800108HmacCounterKdf(ReadOnlySpan key, HashAlgorithmName hashAlgorithm) + { + CheckHashAlgorithm(hashAlgorithm); + _implementation = CreateImplementation(key, hashAlgorithm); + } + + /// + /// Initializes a new instance of using a specified key and HMAC algorithm. + /// + /// The key-derivation key. + /// The HMAC algorithm. + /// + /// + /// has a which is . + /// + /// -or- + /// + /// is . + /// + /// + /// + /// has a which is empty. + /// + /// + /// is not a known or supported hash algorithm. + /// + /// + /// The current platform does not have a supported implementation of HMAC. + /// + public SP800108HmacCounterKdf(byte[] key, HashAlgorithmName hashAlgorithm) + { + // This constructor doesn't defer to the span constructor because SP800108HmacCounterKdfImplementationCng + // has a constructor for byte[] key to avoid a byte[]->span->byte[] conversion. + + if (key is null) + throw new ArgumentNullException(nameof(key)); + + CheckHashAlgorithm(hashAlgorithm); + _implementation = CreateImplementation(key, hashAlgorithm); + } + + /// + /// Derives a key of a specified length. + /// + /// The key-derivation key. + /// The HMAC algorithm. + /// The label that identifies the purpose for the derived key. + /// The context containing information related to the derived key. + /// The length of the derived key, in bytes. + /// An array containing the derived key. + /// + /// + /// is . + /// + /// -or- + /// + /// is . + /// + /// -or- + /// + /// is . + /// + /// -or- + /// + /// has a which is . + /// + /// + /// + /// has a which is empty. + /// + /// + /// is negative or larger than the maximum number of bytes + /// that can be derived. + /// + /// + /// is not a known or supported hash algorithm. + /// + /// + /// The current platform does not have a supported implementation of HMAC. + /// + public static byte[] DeriveBytes(byte[] key, HashAlgorithmName hashAlgorithm, byte[] label, byte[] context, int derivedKeyLengthInBytes) + { + if (key is null) + throw new ArgumentNullException(nameof(key)); + + if (label is null) + throw new ArgumentNullException(nameof(label)); + + if (context is null) + throw new ArgumentNullException(nameof(context)); + + CheckPrfOutputLength(derivedKeyLengthInBytes, nameof(derivedKeyLengthInBytes)); + CheckHashAlgorithm(hashAlgorithm); + + // Don't call to the Span overload so that we don't go from array->span->array for the key in the .NET Standard + // build, which prefers to use arrays for the key. + return DeriveBytesCore(key, hashAlgorithm, label, context, derivedKeyLengthInBytes); + } + + /// + /// Derives a key of a specified length. + /// + /// The key-derivation key. + /// The HMAC algorithm. + /// The label that identifies the purpose for the derived key. + /// The context containing information related to the derived key. + /// The length of the derived key, in bytes. + /// An array containing the derived key. + /// + /// + /// is . + /// + /// -or- + /// + /// is . + /// + /// -or- + /// + /// is . + /// + /// -or- + /// + /// has a which is . + /// + /// + /// + /// has a which is empty. + /// + /// + /// is negative or larger than the maximum number of bytes + /// that can be derived. + /// + /// + /// is not a known or supported hash algorithm. + /// + /// + /// or contains text that cannot be converted to UTF8. + /// + /// + /// and will be converted to bytes using the UTF8 encoding. + /// for other encodings, perform the conversion using the desired encoding and use an overload which accepts the + /// label and context as a sequence of bytes. + /// + /// + /// The current platform does not have a supported implementation of HMAC. + /// + public static byte[] DeriveBytes(byte[] key, HashAlgorithmName hashAlgorithm, string label, string context, int derivedKeyLengthInBytes) + { + if (key is null) + throw new ArgumentNullException(nameof(key)); + + if (label is null) + throw new ArgumentNullException(nameof(label)); + + if (context is null) + throw new ArgumentNullException(nameof(context)); + + CheckPrfOutputLength(derivedKeyLengthInBytes, nameof(derivedKeyLengthInBytes)); + CheckHashAlgorithm(hashAlgorithm); + + byte[] result = new byte[derivedKeyLengthInBytes]; + DeriveBytesCore(key, hashAlgorithm, label.AsSpan(), context.AsSpan(), result); + return result; + } + + /// + /// Derives a key of a specified length. + /// + /// The key-derivation key. + /// The HMAC algorithm. + /// The label that identifies the purpose for the derived key. + /// The context containing information related to the derived key. + /// The length of the derived key, in bytes. + /// An array containing the derived key. + /// + /// has a which is . + /// + /// + /// has a which is empty. + /// + /// + /// is negative or larger than the maximum number of bytes + /// that can be derived. + /// + /// + /// is not a known or supported hash algorithm. + /// + /// + /// The current platform does not have a supported implementation of HMAC. + /// + public static byte[] DeriveBytes(ReadOnlySpan key, HashAlgorithmName hashAlgorithm, ReadOnlySpan label, ReadOnlySpan context, int derivedKeyLengthInBytes) + { + CheckPrfOutputLength(derivedKeyLengthInBytes, nameof(derivedKeyLengthInBytes)); + + byte[] result = new byte[derivedKeyLengthInBytes]; + DeriveBytes(key, hashAlgorithm, label, context, result); + return result; + } + + /// + /// Fills a buffer with a derived key. + /// + /// The key-derivation key. + /// The HMAC algorithm. + /// The label that identifies the purpose for the derived key. + /// The context containing information related to the derived key. + /// The buffer which will receive the derived key. + /// + /// has a which is . + /// + /// + /// has a which is empty. + /// + /// + /// is larger than the maximum number of bytes that can be derived. + /// + /// + /// is not a known or supported hash algorithm. + /// + /// + /// The current platform does not have a supported implementation of HMAC. + /// + public static void DeriveBytes(ReadOnlySpan key, HashAlgorithmName hashAlgorithm, ReadOnlySpan label, ReadOnlySpan context, Span destination) + { + CheckHashAlgorithm(hashAlgorithm); + CheckPrfOutputLength(destination.Length, nameof(destination)); + DeriveBytesCore(key, hashAlgorithm, label, context, destination); + } + + /// + /// Derives a key of a specified length. + /// + /// The key-derivation key. + /// The HMAC algorithm. + /// The label that identifies the purpose for the derived key. + /// The context containing information related to the derived key. + /// The length of the derived key, in bytes. + /// An array containing the derived key. + /// + /// has a which is . + /// + /// + /// has a which is empty. + /// + /// + /// is negative or larger than the maximum number of bytes + /// that can be derived. + /// + /// + /// is not a known or supported hash algorithm. + /// + /// + /// or contains text that cannot be converted to UTF8. + /// + /// + /// The current platform does not have a supported implementation of HMAC. + /// + /// + /// and will be converted to bytes using the UTF8 encoding. + /// for other encodings, perform the conversion using the desired encoding and use an overload which accepts the + /// label and context as a sequence of bytes. + /// + public static byte[] DeriveBytes(ReadOnlySpan key, HashAlgorithmName hashAlgorithm, ReadOnlySpan label, ReadOnlySpan context, int derivedKeyLengthInBytes) + { + CheckPrfOutputLength(derivedKeyLengthInBytes, nameof(derivedKeyLengthInBytes)); + + byte[] result = new byte[derivedKeyLengthInBytes]; + DeriveBytes(key, hashAlgorithm, label, context, result); + return result; + } + + /// + /// Fills a buffer with a derived key. + /// + /// The key-derivation key. + /// The HMAC algorithm. + /// The label that identifies the purpose for the derived key. + /// The context containing information related to the derived key. + /// The buffer which will receive the derived key. + /// + /// has a which is . + /// + /// + /// has a which is empty. + /// + /// + /// is larger than the maximum number of bytes that can be derived. + /// + /// + /// is not a known or supported hash algorithm. + /// + /// + /// or contains text that cannot be converted to UTF8. + /// + /// + /// The current platform does not have a supported implementation of HMAC. + /// + /// + /// and will be converted to bytes using the UTF8 encoding. + /// for other encodings, perform the conversion using the desired encoding and use an overload which accepts the + /// label and context as a sequence of bytes. + /// + public static void DeriveBytes(ReadOnlySpan key, HashAlgorithmName hashAlgorithm, ReadOnlySpan label, ReadOnlySpan context, Span destination) + { + CheckHashAlgorithm(hashAlgorithm); + CheckPrfOutputLength(destination.Length, nameof(destination)); + DeriveBytesCore(key, hashAlgorithm, label, context, destination); + } + + /// + /// Derives a key of a specified length. + /// + /// The label that identifies the purpose for the derived key. + /// The context containing information related to the derived key. + /// The length of the derived key, in bytes. + /// An array containing the derived key. + /// + /// + /// is . + /// + /// -or- + /// + /// is . + /// + /// + /// + /// is negative or larger than the maximum number of bytes + /// that can be derived. + /// + public byte[] DeriveKey(byte[] label, byte[] context, int derivedKeyLengthInBytes) + { + if (label is null) + throw new ArgumentNullException(nameof(label)); + + if (context is null) + throw new ArgumentNullException(nameof(context)); + + CheckPrfOutputLength(derivedKeyLengthInBytes, nameof(derivedKeyLengthInBytes)); + + byte[] result = new byte[derivedKeyLengthInBytes]; + DeriveKeyCore(label, context, result); + return result; + } + + /// + /// Derives a key of a specified length. + /// + /// The label that identifies the purpose for the derived key. + /// The context containing information related to the derived key. + /// The length of the derived key, in bytes. + /// An array containing the derived key. + /// + /// is negative or larger than the maximum number of bytes + /// that can be derived. + /// + public byte[] DeriveKey(ReadOnlySpan label, ReadOnlySpan context, int derivedKeyLengthInBytes) + { + CheckPrfOutputLength(derivedKeyLengthInBytes, nameof(derivedKeyLengthInBytes)); + + byte[] result = new byte[derivedKeyLengthInBytes]; + DeriveKey(label, context, result); + return result; + } + + /// + /// Fills a buffer with a derived key. + /// + /// The label that identifies the purpose for the derived key. + /// The context containing information related to the derived key. + /// The buffer which will receive the derived key. + /// + /// + /// is . + /// + /// -or- + /// + /// is . + /// + /// + /// + /// is larger than the maximum number of bytes that can be derived. + /// + public void DeriveKey(ReadOnlySpan label, ReadOnlySpan context, Span destination) + { + CheckPrfOutputLength(destination.Length, nameof(destination)); + DeriveKeyCore(label, context, destination); + } + + /// + /// Derives a key of a specified length. + /// + /// The label that identifies the purpose for the derived key. + /// The context containing information related to the derived key. + /// The length of the derived key, in bytes. + /// An array containing the derived key. + /// + /// is negative or larger than the maximum number of bytes + /// that can be derived. + /// + /// + /// or contains text that cannot be converted to UTF8. + /// + /// + /// and will be converted to bytes using the UTF8 encoding. + /// for other encodings, perform the conversion using the desired encoding and use an overload which accepts the + /// label and context as a sequence of bytes. + /// + public byte[] DeriveKey(ReadOnlySpan label, ReadOnlySpan context, int derivedKeyLengthInBytes) + { + CheckPrfOutputLength(derivedKeyLengthInBytes, nameof(derivedKeyLengthInBytes)); + + byte[] result = new byte[derivedKeyLengthInBytes]; + DeriveKeyCore(label, context, result); + return result; + } + + /// + /// Fills a buffer with a derived key. + /// + /// The label that identifies the purpose for the derived key. + /// The context containing information related to the derived key. + /// The buffer which will receive the derived key. + /// + /// is larger than the maximum number of bytes that can be derived. + /// + /// + /// or contains text that cannot be converted to UTF8. + /// + /// + /// and will be converted to bytes using the UTF8 encoding. + /// for other encodings, perform the conversion using the desired encoding and use an overload which accepts the + /// label and context as a sequence of bytes. + /// + public void DeriveKey(ReadOnlySpan label, ReadOnlySpan context, Span destination) + { + CheckPrfOutputLength(destination.Length, nameof(destination)); + DeriveKeyCore(label, context, destination); + } + + /// + /// Derives a key of a specified length. + /// + /// The label that identifies the purpose for the derived key. + /// The context containing information related to the derived key. + /// The length of the derived key, in bytes. + /// An array containing the derived key. + /// + /// + /// is . + /// + /// -or- + /// + /// is . + /// + /// + /// + /// is negative or larger than the maximum number of bytes + /// that can be derived. + /// + /// + /// or contains text that cannot be converted to UTF8. + /// + /// + /// and will be converted to bytes using the UTF8 encoding. + /// for other encodings, perform the conversion using the desired encoding and use an overload which accepts the + /// label and context as a sequence of bytes. + /// + public byte[] DeriveKey(string label, string context, int derivedKeyLengthInBytes) + { + if (label is null) + throw new ArgumentNullException(nameof(label)); + + if (context is null) + throw new ArgumentNullException(nameof(context)); + + return DeriveKey(label.AsSpan(), context.AsSpan(), derivedKeyLengthInBytes); + } + + /// + /// Releases all resources used by the current instance of . + /// + public void Dispose() + { + _implementation.Dispose(); + } + + private static void CheckHashAlgorithm(HashAlgorithmName hashAlgorithm) + { + string? hashAlgorithmName = hashAlgorithm.Name; + + switch (hashAlgorithmName) + { + case null: + throw new ArgumentNullException(nameof(hashAlgorithm)); + case "": + throw new ArgumentException(SR.Argument_EmptyString, nameof(hashAlgorithm)); + case HashAlgorithmNames.SHA1: + case HashAlgorithmNames.SHA256: + case HashAlgorithmNames.SHA384: + case HashAlgorithmNames.SHA512: + break; + default: + throw new CryptographicException(SR.Format(SR.Cryptography_UnknownHashAlgorithm, hashAlgorithmName)); + } + } + + private static partial byte[] DeriveBytesCore( + byte[] key, + HashAlgorithmName hashAlgorithm, + byte[] label, + byte[] context, + int derivedKeyLengthInBytes); + + private static partial void DeriveBytesCore( + ReadOnlySpan key, + HashAlgorithmName hashAlgorithm, + ReadOnlySpan label, + ReadOnlySpan context, + Span destination); + + private static partial void DeriveBytesCore( + ReadOnlySpan key, + HashAlgorithmName hashAlgorithm, + ReadOnlySpan label, + ReadOnlySpan context, + Span destination); + + private void DeriveKeyCore(ReadOnlySpan label, ReadOnlySpan context, Span destination) + { + _implementation.DeriveBytes(label, context, destination); + } + + private void DeriveKeyCore(ReadOnlySpan label, ReadOnlySpan context, Span destination) + { + _implementation.DeriveBytes(label, context, destination); + } + + private static void CheckPrfOutputLength(int length, string paramName) + { + if (length > MaxPrfOutputSize) + { + throw new ArgumentOutOfRangeException(paramName, SR.ArgumentOutOfRange_KOut_Too_Large); + } + + if (length < 0) + { + throw new ArgumentOutOfRangeException(paramName, SR.ArgumentOutOfRange_NeedNonNegNum); + } + } + } +} diff --git a/src/libraries/Common/src/System/Security/Cryptography/SP800108HmacCounterKdfImplementationBase.cs b/src/libraries/Common/src/System/Security/Cryptography/SP800108HmacCounterKdfImplementationBase.cs new file mode 100644 index 00000000000..698c59d54c3 --- /dev/null +++ b/src/libraries/Common/src/System/Security/Cryptography/SP800108HmacCounterKdfImplementationBase.cs @@ -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 label, ReadOnlySpan context, Span destination); + internal abstract void DeriveBytes(byte[] label, byte[] context, Span destination); + internal abstract void DeriveBytes(ReadOnlySpan label, ReadOnlySpan context, Span destination); + + public abstract void Dispose(); + } +} diff --git a/src/libraries/Common/src/System/Security/Cryptography/SP800108HmacCounterKdfImplementationCng.cs b/src/libraries/Common/src/System/Security/Cryptography/SP800108HmacCounterKdfImplementationCng.cs new file mode 100644 index 00000000000..ca8f72c6142 --- /dev/null +++ b/src/libraries/Common/src/System/Security/Cryptography/SP800108HmacCounterKdfImplementationCng.cs @@ -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 destination) + { + DeriveBytes(new ReadOnlySpan(label), new ReadOnlySpan(context), destination); + } + + internal override unsafe void DeriveBytes(ReadOnlySpan label, ReadOnlySpan context, Span 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 label, ReadOnlySpan context, Span 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 key, + HashAlgorithmName hashAlgorithm, + ReadOnlySpan label, + ReadOnlySpan context, + Span destination) + { + Debug.Assert(destination.Length <= 0x1FFFFFFF); + + using (SP800108HmacCounterKdfImplementationCng kdf = new SP800108HmacCounterKdfImplementationCng(key, hashAlgorithm)) + { + kdf.DeriveBytes(label, context, destination); + } + } + + internal static void DeriveBytesOneShot( + ReadOnlySpan key, + HashAlgorithmName hashAlgorithm, + ReadOnlySpan label, + ReadOnlySpan context, + Span 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(); + } + } + } +} diff --git a/src/libraries/Common/src/System/Security/Cryptography/SP800108HmacCounterKdfImplementationManaged.cs b/src/libraries/Common/src/System/Security/Cryptography/SP800108HmacCounterKdfImplementationManaged.cs new file mode 100644 index 00000000000..c009b69ebd9 --- /dev/null +++ b/src/libraries/Common/src/System/Security/Cryptography/SP800108HmacCounterKdfImplementationManaged.cs @@ -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 label, ReadOnlySpan context, Span destination) + { + byte[] key = IncrementAndAcquireKey(); + + try + { + DeriveBytesOneShot(key, _hashAlgorithm, label, context, destination); + } + finally + { + ReleaseKey(); + } + } + + internal override void DeriveBytes(ReadOnlySpan label, ReadOnlySpan context, Span destination) + { + byte[] key = IncrementAndAcquireKey(); + + try + { + DeriveBytesOneShot(key, _hashAlgorithm, label, context, destination); + } + finally + { + ReleaseKey(); + } + } + + internal override void DeriveBytes(byte[] label, byte[] context, Span 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!; + } + } +} diff --git a/src/libraries/Common/src/System/Security/Cryptography/Utf8DataEncoding.cs b/src/libraries/Common/src/System/Security/Cryptography/Utf8DataEncoding.cs new file mode 100644 index 00000000000..c550f0fdfca --- /dev/null +++ b/src/libraries/Common/src/System/Security/Cryptography/Utf8DataEncoding.cs @@ -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 _buffer; + + internal Utf8DataEncoding(ReadOnlySpan data, Span 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 Utf8Bytes => _buffer; + + internal void Dispose() + { + CryptographicOperations.ZeroMemory(_buffer); + + if (_rented is not null) + { + CryptoPool.Return(_rented, clearSize: 0); + } + } + } + +} diff --git a/src/libraries/Common/tests/System/Security/Cryptography/SP800108HmacCounterKdfTests.ArgValidation.cs b/src/libraries/Common/tests/System/Security/Cryptography/SP800108HmacCounterKdfTests.ArgValidation.cs new file mode 100644 index 00000000000..a492b4b452f --- /dev/null +++ b/src/libraries/Common/tests/System/Security/Cryptography/SP800108HmacCounterKdfTests.ArgValidation.cs @@ -0,0 +1,326 @@ +// 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; +using Xunit; + +namespace System.Security.Cryptography.Tests +{ + public static partial class SP800108HmacCounterKdfTests + { + [Fact] + public static void DeriveBytes_Allocating_ArrayBytes_ArgValidation() + { + Assert.Throws("key", () => + SP800108HmacCounterKdf.DeriveBytes(key: (byte[])null, HashAlgorithmName.SHA256, s_labelBytes, s_contextBytes, 42)); + + Assert.Throws("label", () => + SP800108HmacCounterKdf.DeriveBytes(s_kdk, HashAlgorithmName.SHA256, label: (byte[])null, s_contextBytes, 42)); + + Assert.Throws("context", () => + SP800108HmacCounterKdf.DeriveBytes(s_kdk, HashAlgorithmName.SHA256, s_labelBytes, context: (byte[])null, 42)); + + Assert.Throws("hashAlgorithm", () => + SP800108HmacCounterKdf.DeriveBytes(s_kdk, s_nullHash, s_labelBytes, s_contextBytes, 42)); + + Assert.Throws("hashAlgorithm", () => + SP800108HmacCounterKdf.DeriveBytes(s_kdk, s_emptyHash, s_labelBytes, s_contextBytes, 42)); + + CryptographicException ex = Assert.Throws(() => + SP800108HmacCounterKdf.DeriveBytes(s_kdk, s_unknownHash, s_labelBytes, s_contextBytes, 42)); + Assert.Contains(s_unknownHash.Name, ex.Message); + + Assert.Throws("derivedKeyLengthInBytes", () => + SP800108HmacCounterKdf.DeriveBytes(s_kdk, HashAlgorithmName.SHA256, s_labelBytes, s_contextBytes, -1)); + + Assert.Throws("derivedKeyLengthInBytes", () => + SP800108HmacCounterKdf.DeriveBytes(s_kdk, HashAlgorithmName.SHA256, s_labelBytes, s_contextBytes, 0x20000000)); + } + + [Fact] + public static void DeriveBytes_Allocating_String_ArgValidation() + { + Assert.Throws("key", () => + SP800108HmacCounterKdf.DeriveBytes(key: (byte[])null, HashAlgorithmName.SHA256, Label, Context, 42)); + + Assert.Throws("label", () => + SP800108HmacCounterKdf.DeriveBytes(s_kdk, HashAlgorithmName.SHA256, label: (string)null, Context, 42)); + + Assert.Throws("context", () => + SP800108HmacCounterKdf.DeriveBytes(s_kdk, HashAlgorithmName.SHA256, Label, context: (string)null, 42)); + + Assert.Throws("hashAlgorithm", () => + SP800108HmacCounterKdf.DeriveBytes(s_kdk, s_nullHash, Label, Context, 42)); + + Assert.Throws("hashAlgorithm", () => + SP800108HmacCounterKdf.DeriveBytes(s_kdk, s_emptyHash, Label, Context, 42)); + + CryptographicException ex = Assert.Throws(() => + SP800108HmacCounterKdf.DeriveBytes(s_kdk, s_unknownHash, Label, Context, 42)); + Assert.Contains(s_unknownHash.Name, ex.Message); + + Assert.Throws("derivedKeyLengthInBytes", () => + SP800108HmacCounterKdf.DeriveBytes(s_kdk, HashAlgorithmName.SHA256, Label, Context, -1)); + + Assert.Throws("derivedKeyLengthInBytes", () => + SP800108HmacCounterKdf.DeriveBytes(s_kdk, HashAlgorithmName.SHA256, Label, Context, 0x20000000)); + } + + [Fact] + public static void DeriveBytes_Allocating_SpanBytes_ArgValidation() + { + Assert.Throws("hashAlgorithm", () => + SP800108HmacCounterKdf.DeriveBytes(s_kdk.AsSpan(), s_nullHash, s_labelBytes.AsSpan(), s_contextBytes.AsSpan(), 42)); + + Assert.Throws("hashAlgorithm", () => + SP800108HmacCounterKdf.DeriveBytes(s_kdk.AsSpan(), s_emptyHash, s_labelBytes.AsSpan(), s_contextBytes.AsSpan(), 42)); + + CryptographicException ex = Assert.Throws(() => + SP800108HmacCounterKdf.DeriveBytes(s_kdk.AsSpan(), s_unknownHash, s_labelBytes.AsSpan(), s_contextBytes.AsSpan(), 42)); + Assert.Contains(s_unknownHash.Name, ex.Message); + + Assert.Throws("derivedKeyLengthInBytes", () => + SP800108HmacCounterKdf.DeriveBytes(s_kdk.AsSpan(), HashAlgorithmName.SHA256, s_labelBytes.AsSpan(), s_contextBytes.AsSpan(), -1)); + + Assert.Throws("derivedKeyLengthInBytes", () => + SP800108HmacCounterKdf.DeriveBytes(s_kdk.AsSpan(), HashAlgorithmName.SHA256, s_labelBytes.AsSpan(), s_contextBytes.AsSpan(), 0x20000000)); + } + + [Fact] + public static void DeriveBytes_BufferFill_SpanBytes_ArgValidation() + { + byte[] destination = new byte[42]; + + Assert.Throws("hashAlgorithm", () => + SP800108HmacCounterKdf.DeriveBytes(s_kdk, s_nullHash, s_labelBytes, s_contextBytes, destination)); + + Assert.Throws("hashAlgorithm", () => + SP800108HmacCounterKdf.DeriveBytes(s_kdk, s_emptyHash, s_labelBytes, s_contextBytes, destination)); + + CryptographicException ex = Assert.Throws(() => + SP800108HmacCounterKdf.DeriveBytes(s_kdk, s_unknownHash, s_labelBytes, s_contextBytes, destination)); + Assert.Contains(s_unknownHash.Name, ex.Message); + + Assert.Throws("destination", () => + SP800108HmacCounterKdf.DeriveBytes(s_kdk, HashAlgorithmName.SHA256, s_labelBytes, s_contextBytes, GetOversizedSpan())); + } + + [Fact] + public static void DeriveBytes_Allocating_SpanChars_ArgValidation() + { + Assert.Throws("hashAlgorithm", () => + SP800108HmacCounterKdf.DeriveBytes(s_kdk, s_nullHash, Label.AsSpan(), Context.AsSpan(), 42)); + + Assert.Throws("hashAlgorithm", () => + SP800108HmacCounterKdf.DeriveBytes(s_kdk, s_emptyHash, Label.AsSpan(), Context.AsSpan(), 42)); + + CryptographicException ex = Assert.Throws(() => + SP800108HmacCounterKdf.DeriveBytes(s_kdk, s_unknownHash, Label.AsSpan(), Context.AsSpan(), 42)); + Assert.Contains(s_unknownHash.Name, ex.Message); + + Assert.Throws("derivedKeyLengthInBytes", () => + SP800108HmacCounterKdf.DeriveBytes(s_kdk, HashAlgorithmName.SHA256, Label.AsSpan(), Context.AsSpan(), -1)); + + Assert.Throws("derivedKeyLengthInBytes", () => + SP800108HmacCounterKdf.DeriveBytes(s_kdk, HashAlgorithmName.SHA256, Label.AsSpan(), Context.AsSpan(), 0x20000000)); + } + + [Fact] + public static void DeriveBytes_BufferFill_SpanChars_ArgValidation() + { + byte[] destination = new byte[42]; + + Assert.Throws("hashAlgorithm", () => + SP800108HmacCounterKdf.DeriveBytes(s_kdk, s_nullHash, Label.AsSpan(), Context.AsSpan(), destination)); + + Assert.Throws("hashAlgorithm", () => + SP800108HmacCounterKdf.DeriveBytes(s_kdk, s_emptyHash, Label.AsSpan(), Context.AsSpan(), destination)); + + CryptographicException ex = Assert.Throws(() => + SP800108HmacCounterKdf.DeriveBytes(s_kdk, s_unknownHash, Label.AsSpan(), Context.AsSpan(), destination)); + Assert.Contains(s_unknownHash.Name, ex.Message); + + Assert.Throws("destination", () => + SP800108HmacCounterKdf.DeriveBytes(s_kdk, HashAlgorithmName.SHA256, Label.AsSpan(), Context.AsSpan(), GetOversizedSpan())); + } + + [Fact] + public static void Ctor_KeyArray_ArgValidation() + { + Assert.Throws("key", () => + new SP800108HmacCounterKdf((byte[])null, HashAlgorithmName.SHA256)); + + Assert.Throws("hashAlgorithm", () => + new SP800108HmacCounterKdf(s_kdk, s_nullHash)); + + Assert.Throws("hashAlgorithm", () => + new SP800108HmacCounterKdf(s_kdk, s_emptyHash)); + + CryptographicException ex = Assert.Throws(() => + new SP800108HmacCounterKdf(s_kdk, s_unknownHash)); + Assert.Contains(s_unknownHash.Name, ex.Message); + } + + [Fact] + public static void Ctor_KeySpan_ArgValidation() + { + Assert.Throws("hashAlgorithm", () => + new SP800108HmacCounterKdf(s_kdk.AsSpan(), s_nullHash)); + + Assert.Throws("hashAlgorithm", () => + new SP800108HmacCounterKdf(s_kdk.AsSpan(), s_emptyHash)); + + CryptographicException ex = Assert.Throws(() => + new SP800108HmacCounterKdf(s_kdk.AsSpan(), s_unknownHash)); + Assert.Contains(s_unknownHash.Name, ex.Message); + } + + [Fact] + public static void DeriveKey_Allocating_ArrayBytes_ArgValidation() + { + using SP800108HmacCounterKdf kdf = new SP800108HmacCounterKdf(s_kdk, HashAlgorithmName.SHA256); + + Assert.Throws("label", () => + kdf.DeriveKey((byte[])null, s_contextBytes, 42)); + + Assert.Throws("context", () => + kdf.DeriveKey(s_labelBytes, (byte[])null, 42)); + + Assert.Throws("derivedKeyLengthInBytes", () => + kdf.DeriveKey(s_labelBytes, s_contextBytes, -1)); + + Assert.Throws("derivedKeyLengthInBytes", () => + kdf.DeriveKey(s_labelBytes, s_contextBytes, 0x20000000)); + } + + [Fact] + public static void DeriveKey_Allocating_SpanBytes_ArgValidation() + { + using SP800108HmacCounterKdf kdf = new SP800108HmacCounterKdf(s_kdk, HashAlgorithmName.SHA256); + + Assert.Throws("derivedKeyLengthInBytes", () => + kdf.DeriveKey(s_labelBytes.AsSpan(), s_contextBytes.AsSpan(), -1)); + + Assert.Throws("derivedKeyLengthInBytes", () => + kdf.DeriveKey(s_labelBytes.AsSpan(), s_contextBytes.AsSpan(), 0x20000000)); + } + + [Fact] + public static void DeriveKey_BufferFill_SpanBytes_ArgValidation() + { + using SP800108HmacCounterKdf kdf = new SP800108HmacCounterKdf(s_kdk, HashAlgorithmName.SHA256); + + Assert.Throws("destination", () => + kdf.DeriveKey(s_labelBytes.AsSpan(), s_contextBytes.AsSpan(), GetOversizedSpan())); + } + + [Fact] + public static void DeriveKey_Allocating_SpanChars_ArgValidation() + { + using SP800108HmacCounterKdf kdf = new SP800108HmacCounterKdf(s_kdk, HashAlgorithmName.SHA256); + + Assert.Throws("derivedKeyLengthInBytes", () => + kdf.DeriveKey(Label.AsSpan(), Context.AsSpan(), -1)); + + Assert.Throws("derivedKeyLengthInBytes", () => + kdf.DeriveKey(Label.AsSpan(), Context.AsSpan(), 0x20000000)); + } + + [Fact] + public static void DeriveKey_BufferFill_SpanChars_ArgValidation() + { + using SP800108HmacCounterKdf kdf = new SP800108HmacCounterKdf(s_kdk, HashAlgorithmName.SHA256); + + Assert.Throws("destination", () => + kdf.DeriveKey(Label.AsSpan(), Context.AsSpan(), GetOversizedSpan())); + } + + [Fact] + public static void DeriveKey_Allocating_String_ArgValidation() + { + using SP800108HmacCounterKdf kdf = new SP800108HmacCounterKdf(s_kdk, HashAlgorithmName.SHA256); + + Assert.Throws("derivedKeyLengthInBytes", () => + kdf.DeriveKey(Label, Context, -1)); + + Assert.Throws("derivedKeyLengthInBytes", () => + kdf.DeriveKey(Label, Context, 0x20000000)); + } + + [Fact] + public static void DeriveKey_Allocating_String_InvalidUTF8() + { + using SP800108HmacCounterKdf kdf = new SP800108HmacCounterKdf(s_kdk, HashAlgorithmName.SHA256); + + Assert.Throws(() => + kdf.DeriveKey("\uD800", Context, 42)); + + Assert.Throws(() => + kdf.DeriveKey(Label, "\uD800", 42)); + } + + [Fact] + public static void DeriveKey_BufferFill_SpanChars_InvalidUTF8() + { + using SP800108HmacCounterKdf kdf = new SP800108HmacCounterKdf(s_kdk, HashAlgorithmName.SHA256); + byte[] derivedKey = new byte[42]; + + Assert.Throws(() => + kdf.DeriveKey("\uD800".AsSpan(), Context.AsSpan(), derivedKey)); + + Assert.Throws(() => + kdf.DeriveKey(Label.AsSpan(), "\uD800".AsSpan(), derivedKey)); + } + + [Fact] + public static void DeriveKey_Allocating_SpanChars_InvalidUTF8() + { + using SP800108HmacCounterKdf kdf = new SP800108HmacCounterKdf(s_kdk, HashAlgorithmName.SHA256); + + Assert.Throws(() => + kdf.DeriveKey("\uD800".AsSpan(), Context.AsSpan(), 42)); + + Assert.Throws(() => + kdf.DeriveKey(Label.AsSpan(), "\uD800".AsSpan(), 42)); + } + + [Fact] + public static void DeriveBytes_BufferFill_SpanChars_InvalidUTF8() + { + byte[] destination = new byte[42]; + + Assert.Throws(() => + SP800108HmacCounterKdf.DeriveBytes(s_kdk, HashAlgorithmName.SHA256, Label.AsSpan(), "\uD800".AsSpan(), destination)); + + Assert.Throws(() => + SP800108HmacCounterKdf.DeriveBytes(s_kdk, HashAlgorithmName.SHA256, "\uD800".AsSpan(), Context.AsSpan(), destination)); + } + + [Fact] + public static void DeriveBytes_Allocating_SpanChars_InvalidUTF8() + { + Assert.Throws(() => + SP800108HmacCounterKdf.DeriveBytes(s_kdk, HashAlgorithmName.SHA256, Label.AsSpan(), "\uD800".AsSpan(), 42)); + + Assert.Throws(() => + SP800108HmacCounterKdf.DeriveBytes(s_kdk, HashAlgorithmName.SHA256, "\uD800".AsSpan(), Context.AsSpan(), 42)); + } + + [Fact] + public static void DeriveBytes_Allocating_String_InvalidUTF8() + { + Assert.Throws(() => + SP800108HmacCounterKdf.DeriveBytes(s_kdk, HashAlgorithmName.SHA256, Label, "\uD800", 42)); + + Assert.Throws(() => + SP800108HmacCounterKdf.DeriveBytes(s_kdk, HashAlgorithmName.SHA256, "\uD800", Context, 42)); + } + + private unsafe static Span GetOversizedSpan() + { + // This creates an very large span over some address space. The memory in this span should never be read + // or written to; it should only be used for length checking of the span for argument validation. + return new Span((void*)0, 0x20000000); + } + } +} diff --git a/src/libraries/Common/tests/System/Security/Cryptography/SP800108HmacCounterKdfTests.Functional.cs b/src/libraries/Common/tests/System/Security/Cryptography/SP800108HmacCounterKdfTests.Functional.cs new file mode 100644 index 00000000000..338ffc7a2a6 --- /dev/null +++ b/src/libraries/Common/tests/System/Security/Cryptography/SP800108HmacCounterKdfTests.Functional.cs @@ -0,0 +1,357 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Collections.Generic; +using System.Globalization; +using Xunit; + +namespace System.Security.Cryptography.Tests +{ + public static partial class SP800108HmacCounterKdfTests + { + [Theory] + [InlineData("V47WmHzPSkdC2vkLAomIjCzZlDOAetll3yJLcSvon7LJFjJpEN+KnSNp+gIpeydKMsENkflbrIZ/3s6GkEaH")] + [InlineData("mVaFM4deXLl610CmnCteNzxgbM/VkmKznAlPauHcDBn0le06uOjAKLHx0LfoU2/Ttq9nd78Y6Nk6wArmdwJgJg==")] + [InlineData("GaHPeqdUxriFpjRtkYQYWr5/iqneD/+hPhVJQt4rXblxSpB1UUqGqL00DMU/FJkX0iMCfqUjQXtXyfks+p++Ev4=")] + public static void AspNetCoreTestVectors_Basic(string expectedBase64) + { + // These tests are from the dotnet/aspnetcore repo. + byte[] expected = Convert.FromBase64String(expectedBase64); + VerifyKbkdf(expected, s_kdk, HashAlgorithmName.SHA512, Label.ToCharArray(), Context.ToCharArray()); + } + + [Theory] + [InlineData("rt2hM6kkQ8hAXmkHx0TU4o3Q+S7fie6b3S1LAq107k++P9v8uSYA2G+WX3pJf9ZkpYrTKD7WUIoLkgA1R9lk")] + [InlineData("RKiXmHSrWq5gkiRSyNZWNJrMR0jDyYHJMt9odOayRAE5wLSX2caINpQmfzTH7voJQi3tbn5MmD//dcspghfBiw==")] + [InlineData("KedXO0zAIZ3AfnPqY1NnXxpC3HDHIxefG4bwD3g6nWYEc5+q7pjbam71Yqj0zgHMNC9Z7BX3wS1/tajFocRWZUk=")] + public static void AspNetCoreTestVectors_LargeKdk(string expectedBase64) + { + // These tests are from the dotnet/aspnetcore repo. + // Win32 BCryptKeyDerivation doesn't perform RFC 2104, section 2 key adjustment for the KDK. + // We do this for CNG so that there is no functional limit on the KDK size. + byte[] kdk = new byte[50000]; + + for (int i = 0; i < kdk.Length; i++) + { + kdk[i] = (byte)i; + } + + byte[] expected = Convert.FromBase64String(expectedBase64); + VerifyKbkdf(expected, kdk, HashAlgorithmName.SHA512, Label.ToCharArray(), Context.ToCharArray()); + } + + [Theory] + [MemberData(nameof(GetRfc8009TestVectors))] + public static void Rfc8009Tests(byte[] kdk, byte[] expected, HashAlgorithmName hashAlgorithm) + { + VerifyKbkdf(expected, kdk, hashAlgorithm, "prf".ToCharArray(), "test".ToCharArray()); + } + + [Theory] + [InlineData(new byte[] { 0xcf, 0x4b, 0xfe, 0x4f, 0x85, 0xa1, 0x0b, 0xad }, nameof(HashAlgorithmName.SHA1))] + [InlineData(new byte[] { 0x00, 0x26, 0x4b, 0xbb, 0x14, 0x97, 0x40, 0x54 }, nameof(HashAlgorithmName.SHA256))] + [InlineData(new byte[] { 0xc7, 0x10, 0x27, 0x87, 0xd8, 0x96, 0xbc, 0x89 }, nameof(HashAlgorithmName.SHA384))] + [InlineData(new byte[] { 0xdb, 0x3a, 0x18, 0xd9, 0x6c, 0x4a, 0xd4, 0x1e }, nameof(HashAlgorithmName.SHA512))] + public static void SymCryptTestVectors(byte[] expected, string hashAlgorithm) + { + // These test vectors come from https://github.com/microsoft/SymCrypt. + // See sp800_108_hmacsha1.c, sp800_108_hmacsha256.c, and sp800_108_hmacsha512.c. + byte[] symCryptKey = new byte[] + { + 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, + 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, + }; + + byte[] key = symCryptKey.AsSpan(0, 8).ToArray(); + byte[] label = "Label"u8.ToArray(); + byte[] context = symCryptKey.AsSpan(16, 16).ToArray(); + + HashAlgorithmName hashAlgorithmName = new HashAlgorithmName(hashAlgorithm); + VerifyKbkdfBytes(expected, key, hashAlgorithmName, label, context); + } + + [Theory] + [InlineData(new byte[] { }, "label!", "context?", new byte[] { 0xb6, 0xff, 0x26, 0x61, 0xea, 0x43, 0x76, 0xd2 })] + [InlineData(new byte[] { 0xFE }, "", "context?", new byte[] { 0xed, 0xdf, 0x50, 0x06, 0x3c, 0x26, 0x3e, 0xd9 })] + [InlineData(new byte[] { 0xFE }, "label!", "", new byte[] { 0x98, 0x83, 0x67, 0x41, 0x3f, 0x2d, 0x90, 0x72 })] + [InlineData(new byte[] { }, "", "", new byte[] { 0x18, 0x0f, 0xf7, 0xa2, 0xbc, 0x8d, 0x6e, 0x98 })] + [InlineData(new byte[] { }, "", "", new byte[] { })] + public static void EmptyTests(byte[] key, string label, string context, byte[] expected) + { + VerifyKbkdf(expected, key, HashAlgorithmName.SHA256, label.ToCharArray(), context.ToCharArray()); + } + + [Theory] + [InlineData(nameof(HashAlgorithmName.SHA1), 512 / 8 - 1, new byte[] { 0xc9, 0x0f, 0x9d, 0x91, 0x85, 0xe5, 0xeb, 0x9b })] + [InlineData(nameof(HashAlgorithmName.SHA1), 512 / 8, new byte[] { 0x7b, 0xdb, 0x38, 0x28, 0xc0, 0x9f, 0x49, 0x05 })] + [InlineData(nameof(HashAlgorithmName.SHA1), 512 / 8 + 1, new byte[] { 0x6c, 0x3a, 0xba, 0x28, 0x38, 0xad, 0x51, 0x2c })] + [InlineData(nameof(HashAlgorithmName.SHA256), 512 / 8 - 1, new byte[] { 0x88, 0xaa, 0xc7, 0xee, 0x05, 0x65, 0xfd, 0xda })] + [InlineData(nameof(HashAlgorithmName.SHA256), 512 / 8, new byte[] { 0x3d, 0xdc, 0x7d, 0xec, 0x0a, 0xfd, 0x7a, 0xc0 })] + [InlineData(nameof(HashAlgorithmName.SHA256), 512 / 8 + 1, new byte[] { 0x47, 0x95, 0x00, 0xd5, 0x55, 0x1f, 0xb3, 0x85 })] + [InlineData(nameof(HashAlgorithmName.SHA384), 1024 / 8 - 1, new byte[] { 0x84, 0xd8, 0xfd, 0x33, 0x4f, 0x07, 0x81, 0x9b })] + [InlineData(nameof(HashAlgorithmName.SHA384), 1024 / 8, new byte[] { 0x6c, 0xa2, 0x5d, 0x4f, 0x61, 0x2d, 0x0f, 0x20 })] + [InlineData(nameof(HashAlgorithmName.SHA384), 1024 / 8 + 1, new byte[] { 0xe4, 0x0e, 0xbd, 0x41, 0x14, 0xe6, 0x80, 0x59 })] + [InlineData(nameof(HashAlgorithmName.SHA512), 1024 / 8 - 1, new byte[] { 0xa4, 0xe5, 0x24, 0xe8, 0x56, 0x2b, 0x48, 0xa4 })] + [InlineData(nameof(HashAlgorithmName.SHA512), 1024 / 8, new byte[] { 0xba, 0xf6, 0xed, 0xa7, 0x3a, 0xf7, 0x12, 0x27 })] + [InlineData(nameof(HashAlgorithmName.SHA512), 1024 / 8 + 1, new byte[] { 0x34, 0xdf, 0x2d, 0x21, 0xfd, 0xf1, 0x0e, 0x13 })] + public static void Kdk_HmacBlockBoundarySizes(string hashAlgorithmName, int kdkSize, byte[] expected) + { + // We do HMAC key adjust for the CNG implementation when the kdk exceeds the block size of the HMAC algorithm. + // This tests one byte below, at, and above the block size for each HMAC algorithm. + // Verified against OpenSSL 3. Example command used below. Adjust the digest and the seq upper boundary as needed. + // Note that OpenSSL calls the label "salt" and the context "info". + // + // openssl kdf -keylen 8 -kdfopt mac:HMAC -kdfopt digest:SHA1 \ + // -kdfopt hexkey:$(seq 1 63 | awk '{ printf "%02x", $1 }') -kdfopt salt:icecream \ + // -kdfopt info:sandwiches -binary KBKDF | xxd -i + + byte[] kdk = new byte[kdkSize]; + + for (int i = 0; i < kdkSize; i++) + { + kdk[i] = (byte)checked(i + 1); + } + + HashAlgorithmName alg = new HashAlgorithmName(hashAlgorithmName); + VerifyKbkdf(expected, kdk, alg, "icecream".ToCharArray(), "sandwiches".ToCharArray()); + } + + [Theory] + [MemberData(nameof(GetOutputLengthBoundaries))] + public static void OutputLength_AroundPrfOutputBoundaries(string hashAlgorithmName, byte[] expected) + { + byte[] kdk = new byte[] { 1, 2, 3, 4, 5, 6, 7, 8 }; + HashAlgorithmName alg = new HashAlgorithmName(hashAlgorithmName); + VerifyKbkdf(expected, kdk, alg, "mustard".ToCharArray(), "ketchup".ToCharArray()); + } + + [Fact] + public static void MultipleDisposes_NoThrow() + { + SP800108HmacCounterKdf kdf = new SP800108HmacCounterKdf(s_kdk, HashAlgorithmName.SHA256); + kdf.Dispose(); + + Assert.Throws(() => kdf.DeriveKey(s_labelBytes, s_contextBytes, 42)); + kdf.Dispose(); + } + + public static IEnumerable GetOutputLengthBoundaries() + { + // All outputs assume a kdk of 1, 2, 3, 4, 5, 6, 7, 8 with a label of "mustard" and context of "ketchup". + // Outputs were generated from the "openssl kdf" command. + + // HMACSHA1 output size is 20 bytes + yield return new object[] + { + nameof(HashAlgorithmName.SHA1), + new byte[39] + { + 0x05, 0xed, 0x32, 0x78, 0xfe, 0x72, 0x9e, 0x7a, 0x1b, 0x49, 0x2c, 0x1b, 0x40, 0x3b, 0x31, 0xd8, + 0xef, 0x74, 0xfc, 0xe6, 0x1f, 0x75, 0x4b, 0xa5, 0x47, 0x8e, 0xb9, 0x1d, 0x9e, 0x0c, 0x4f, 0x03, + 0xf7, 0x92, 0x68, 0x8a, 0x94, 0x0c, 0xea, + }, + }; + + yield return new object[] + { + nameof (HashAlgorithmName.SHA1), + new byte[40] + { + 0xa5, 0x4a, 0x74, 0xe5, 0x01, 0x0c, 0x69, 0x0e, 0xc8, 0x9e, 0x24, 0x95, 0x94, 0x6a, 0x57, 0x06, + 0xe8, 0x88, 0x63, 0x3b, 0xae, 0x1d, 0x13, 0xd8, 0x9d, 0x80, 0x4c, 0xcd, 0x60, 0x2b, 0x15, 0x3a, + 0xb9, 0xd3, 0x77, 0xc7, 0xc4, 0xb7, 0x2a, 0xe6, + } + }; + + yield return new object[] + { + nameof(HashAlgorithmName.SHA1), + new byte[41] + { + 0x44, 0xdc, 0x8a, 0x94, 0x18, 0xd1, 0x3a, 0x35, 0x39, 0xb8, 0xfb, 0x33, 0x0d, 0xf8, 0x1d, 0x3d, + 0x95, 0xa3, 0x3a, 0xfb, 0x0c, 0x7f, 0x54, 0x20, 0x32, 0x75, 0x3c, 0x9c, 0xf0, 0xe2, 0xce, 0x4c, + 0xe0, 0x98, 0xbe, 0x6b, 0x61, 0x19, 0xd0, 0x79, 0xea, + } + }; + + // HMACSHA256 output size is 32 bytes + yield return new object[] + { + nameof(HashAlgorithmName.SHA256), + new byte[63] + { + 0x86, 0x02, 0xc7, 0x0a, 0x80, 0xf3, 0xc9, 0x46, 0xb4, 0x10, 0xa8, 0xde, 0x36, 0x03, 0x5a, 0xb2, + 0x29, 0x48, 0x16, 0xc6, 0x97, 0x99, 0xa4, 0x48, 0x85, 0x55, 0x34, 0xbc, 0xa6, 0x29, 0x68, 0x88, + 0x8a, 0xd3, 0x42, 0x42, 0x22, 0x55, 0xa1, 0xf9, 0xaf, 0x1b, 0xb4, 0xfb, 0x69, 0xd3, 0x9d, 0x2e, + 0x89, 0x4f, 0x0e, 0x19, 0x5a, 0x98, 0xce, 0x5c, 0x7e, 0xfd, 0xba, 0xc7, 0x51, 0x18, 0xdf, + }, + }; + + yield return new object[] + { + nameof (HashAlgorithmName.SHA256), + new byte[64] + { + 0xcc, 0x06, 0x77, 0xaf, 0x45, 0xae, 0x66, 0x3f, 0x58, 0x8f, 0xd4, 0xa5, 0x6f, 0x31, 0xef, 0xd7, + 0x29, 0x9e, 0x45, 0xb1, 0xac, 0x5b, 0x27, 0xb4, 0x10, 0xc1, 0xaf, 0x63, 0xe9, 0xbb, 0xb2, 0xe6, + 0x76, 0x65, 0xbf, 0x2d, 0xd3, 0x14, 0xfb, 0xdf, 0xab, 0x6b, 0x95, 0xf4, 0x67, 0x53, 0xe0, 0xd7, + 0x46, 0xd3, 0x4c, 0x72, 0xf3, 0x08, 0x95, 0x37, 0xbf, 0xa4, 0x67, 0xb0, 0xe0, 0x00, 0x46, 0x79, + } + }; + + yield return new object[] + { + nameof(HashAlgorithmName.SHA256), + new byte[65] + { + 0x2b, 0x57, 0x00, 0xe5, 0xef, 0xda, 0x41, 0x7b, 0x75, 0x04, 0xe8, 0x37, 0x7a, 0x7f, 0xfd, 0x30, + 0xd6, 0x56, 0x07, 0x69, 0x00, 0x75, 0x7b, 0xb9, 0x64, 0x15, 0x51, 0xac, 0x88, 0x55, 0x87, 0x24, + 0xcc, 0xb9, 0x8b, 0xb2, 0x55, 0xcc, 0x02, 0xda, 0xf1, 0x4e, 0xc9, 0xa2, 0x40, 0x95, 0xfb, 0xff, + 0xa0, 0x57, 0x73, 0x51, 0x66, 0x4d, 0x65, 0xd4, 0xc9, 0xc0, 0xe6, 0xf4, 0x40, 0xf4, 0x30, 0x17, + 0x7b, + } + }; + + // HMACSHA384 output size is 48 bytes + yield return new object[] + { + nameof(HashAlgorithmName.SHA384), + new byte[95] + { + 0x82, 0x1d, 0x9b, 0x3c, 0x7f, 0xad, 0xd4, 0x1b, 0x91, 0xdc, 0x6e, 0x4f, 0xf5, 0xd8, 0xf7, 0xc8, + 0x33, 0x18, 0xc8, 0xf8, 0x23, 0x3f, 0x5d, 0xf4, 0x95, 0x32, 0x81, 0x72, 0x96, 0xbd, 0xb9, 0xcc, + 0xc1, 0x91, 0x0c, 0x5b, 0x5c, 0x86, 0x2c, 0x0d, 0x5b, 0xe4, 0xfb, 0xc6, 0x70, 0xc4, 0x20, 0xd6, + 0x9c, 0xfd, 0x67, 0x56, 0x86, 0x16, 0xd6, 0xf8, 0x05, 0x86, 0x5c, 0xa0, 0x64, 0x5f, 0x72, 0xe0, + 0xa5, 0x52, 0x5d, 0x72, 0xe8, 0x5e, 0x07, 0xf1, 0xf5, 0xcf, 0xf9, 0x63, 0x85, 0xc2, 0x77, 0x87, + 0x89, 0x75, 0x9d, 0xd2, 0xc6, 0x2b, 0xf3, 0x23, 0x73, 0xd9, 0x1d, 0x01, 0x17, 0x9c, 0x01, + }, + }; + + yield return new object[] + { + nameof (HashAlgorithmName.SHA384), + new byte[96] + { + 0xe4, 0x30, 0x8b, 0x7e, 0x5b, 0x64, 0xcd, 0xd7, 0x3d, 0x27, 0xd9, 0x3a, 0x9e, 0xee, 0xcc, 0xc6, + 0x79, 0xa7, 0x39, 0xca, 0x91, 0xb8, 0x93, 0xcd, 0xe8, 0xb8, 0xb7, 0x8a, 0x48, 0xad, 0xb4, 0x3d, + 0x3a, 0x02, 0xb9, 0xba, 0x81, 0x81, 0x01, 0x5f, 0xef, 0x8a, 0xc1, 0xcd, 0x6b, 0xae, 0x99, 0xb9, + 0xfd, 0xaf, 0x28, 0x18, 0xcf, 0x48, 0xa1, 0xfa, 0x57, 0xce, 0x0a, 0x79, 0x1f, 0xbf, 0xc8, 0x7f, + 0xd8, 0x34, 0x34, 0x16, 0x27, 0xc1, 0x12, 0x6e, 0x4c, 0x8c, 0x62, 0xc6, 0x11, 0x01, 0xb8, 0xb8, + 0xa5, 0x06, 0xc8, 0x4a, 0x2f, 0xf2, 0x91, 0x08, 0x1a, 0x02, 0x5e, 0x72, 0x48, 0xd1, 0x11, 0x6a, + + } + }; + + yield return new object[] + { + nameof(HashAlgorithmName.SHA384), + new byte[97] + { + 0xa1, 0x01, 0x9f, 0x83, 0x55, 0x8f, 0x4a, 0x67, 0x2f, 0xbf, 0xf7, 0x7b, 0xbb, 0xfd, 0x22, 0x35, + 0x35, 0x97, 0x01, 0x53, 0x5d, 0x9f, 0x7e, 0xa4, 0xbf, 0xb9, 0xfd, 0xdf, 0x7d, 0x86, 0x8f, 0x86, + 0x98, 0x9d, 0x87, 0x21, 0x41, 0x75, 0x4e, 0x2c, 0xaf, 0x10, 0x25, 0x40, 0xe8, 0x26, 0xb8, 0x4d, + 0x37, 0xe2, 0x43, 0x5f, 0x2b, 0x30, 0x03, 0xde, 0xb3, 0x1e, 0x31, 0x8c, 0x59, 0x2d, 0x26, 0xd5, + 0x08, 0xe4, 0x55, 0x46, 0x0d, 0x76, 0xe7, 0x58, 0xe5, 0xa9, 0xaf, 0xe8, 0xe7, 0x7c, 0xa3, 0x05, + 0x61, 0x7b, 0xcb, 0x98, 0x6e, 0xaa, 0xea, 0xd0, 0x7e, 0x52, 0x20, 0xf8, 0xfe, 0x49, 0x02, 0x08, + 0xee, + } + }; + + // HMACSHA512 output size is 64 bytes + yield return new object[] + { + nameof(HashAlgorithmName.SHA512), + new byte[127] + { + 0x3d, 0x72, 0x04, 0x07, 0x9c, 0x28, 0x34, 0x33, 0x7a, 0x2f, 0x8e, 0x22, 0x00, 0x1f, 0x1f, 0x22, + 0xd9, 0x24, 0x38, 0xd1, 0x56, 0xa4, 0x03, 0x46, 0x89, 0x09, 0xde, 0x13, 0xb3, 0x9c, 0xa9, 0x24, + 0x0d, 0x3e, 0x4c, 0x97, 0x7f, 0xd5, 0x9f, 0x86, 0x45, 0x36, 0xcd, 0xc8, 0x0f, 0x44, 0x9a, 0xad, + 0xe3, 0xba, 0xfc, 0x93, 0x08, 0x21, 0xa8, 0xfd, 0xde, 0xef, 0x4f, 0xe3, 0xaa, 0xb3, 0xcf, 0xc1, + 0x81, 0x1b, 0x44, 0xf9, 0xae, 0xf6, 0x73, 0xc7, 0xf0, 0x71, 0xd6, 0x14, 0x8e, 0x18, 0x5d, 0x43, + 0xa1, 0xfb, 0x09, 0x11, 0x24, 0x84, 0x56, 0x9c, 0x97, 0x6e, 0x2e, 0x5a, 0xcd, 0xd3, 0xa5, 0xfb, + 0x81, 0x33, 0x6a, 0x3d, 0x95, 0xa5, 0xd9, 0xcd, 0x04, 0x36, 0x76, 0xc2, 0x4c, 0xed, 0x65, 0x81, + 0x6f, 0x8c, 0xec, 0xfd, 0xde, 0xdd, 0x3c, 0xd9, 0x1a, 0xe1, 0xf1, 0x02, 0x7e, 0xb8, 0x3a, + }, + }; + + yield return new object[] + { + nameof (HashAlgorithmName.SHA512), + new byte[128] + { + 0x0e, 0x6d, 0x21, 0x43, 0xca, 0xa7, 0x88, 0x13, 0x70, 0x0e, 0xc5, 0x7b, 0x5e, 0x5a, 0x41, 0x21, + 0x03, 0x07, 0x30, 0x35, 0x10, 0xe9, 0x42, 0x12, 0x80, 0x64, 0x10, 0x71, 0x5d, 0x41, 0xb9, 0xf5, + 0x3a, 0xb2, 0xcd, 0xf8, 0x71, 0x52, 0x01, 0xf8, 0xc5, 0x27, 0x65, 0xc0, 0x6b, 0x31, 0x35, 0xfc, + 0x0d, 0x38, 0xbb, 0xf4, 0xc2, 0xeb, 0x9a, 0x85, 0x3f, 0x16, 0xf0, 0x25, 0x40, 0x33, 0x57, 0xc1, + 0x08, 0x25, 0xcf, 0x31, 0x10, 0xbf, 0x78, 0x3a, 0x37, 0x64, 0x13, 0xa7, 0x2f, 0xd8, 0x32, 0x2b, + 0x93, 0x1f, 0x0b, 0x6d, 0x6c, 0x6c, 0x45, 0x9f, 0x6a, 0xdb, 0x97, 0x8b, 0x33, 0x7d, 0x31, 0xa8, + 0xd9, 0x92, 0xe4, 0x50, 0x38, 0x25, 0x22, 0x09, 0x98, 0x11, 0xce, 0x55, 0xf8, 0x6d, 0x27, 0x36, + 0x5f, 0xab, 0xca, 0x4b, 0x54, 0x78, 0x11, 0xf9, 0xaf, 0x57, 0xfa, 0x02, 0x31, 0x27, 0x52, 0x69, + } + }; + + yield return new object[] + { + nameof(HashAlgorithmName.SHA512), + new byte[129] + { + 0x9e, 0xd0, 0xad, 0x78, 0x5d, 0xb3, 0x50, 0xce, 0x0a, 0x1b, 0xa2, 0xd1, 0x1d, 0xb8, 0x43, 0xc0, + 0xc1, 0x0f, 0x2f, 0x13, 0xbe, 0x6b, 0x56, 0x53, 0x4e, 0x9f, 0x18, 0x76, 0xcf, 0xf2, 0xf0, 0xa1, + 0xa6, 0xe8, 0x57, 0x2b, 0x05, 0xf2, 0x2e, 0x3e, 0xbf, 0xfe, 0x5c, 0x20, 0x93, 0x5e, 0x73, 0xca, + 0x23, 0xda, 0x63, 0x24, 0xdf, 0x6c, 0xb7, 0x5c, 0xe7, 0xe9, 0x6e, 0x2a, 0x0c, 0x3a, 0xa9, 0xb7, + 0x65, 0x25, 0x8a, 0x8c, 0xf0, 0xae, 0x0e, 0x53, 0x84, 0x82, 0x8b, 0xa6, 0xd9, 0xed, 0x3d, 0xfe, + 0x6f, 0x1d, 0xbf, 0x36, 0x7c, 0xfd, 0xa2, 0xf9, 0x22, 0x79, 0x79, 0x54, 0x5f, 0xed, 0x3b, 0xb9, + 0xab, 0x84, 0x75, 0xba, 0xb0, 0x12, 0x22, 0x6a, 0x07, 0xee, 0x35, 0xc4, 0x9a, 0xca, 0xd8, 0x28, + 0x3c, 0x0b, 0xcd, 0x85, 0xbb, 0x6e, 0x7b, 0x0e, 0xaa, 0xcb, 0xf9, 0xb1, 0xa4, 0xcd, 0x65, 0x87, + 0x61, + } + }; + } + + public static IEnumerable GetRfc8009TestVectors() + { + // See RFC 8009 Appendix A "Sample pseudorandom function (PRF) invocations" + // section. These vectors are also used by OpenSSL 3 for KBKDF. + // Section 5 of RFC 8009 defines KDF-HMAC-SHA2 as always using "prf" as the label. + // The appendix defines the context as "test". + // (kdk, expected, hmac algorithm) + yield return new object[] + { + new byte[] + { + 0x37, 0x05, 0xD9, 0x60, 0x80, 0xC1, 0x77, 0x28, + 0xA0, 0xE8, 0x00, 0xEA, 0xB6, 0xE0, 0xD2, 0x3C, + }, + new byte[] + { + 0x9D, 0x18, 0x86, 0x16, 0xF6, 0x38, 0x52, 0xFE, + 0x86, 0x91, 0x5B, 0xB8, 0x40, 0xB4, 0xA8, 0x86, + 0xFF, 0x3E, 0x6B, 0xB0, 0xF8, 0x19, 0xB4, 0x9B, + 0x89, 0x33, 0x93, 0xD3, 0x93, 0x85, 0x42, 0x95, + }, + HashAlgorithmName.SHA256, + }; + + yield return new object[] + { + new byte[] + { + 0x6D, 0x40, 0x4D, 0x37, 0xFA, 0xF7, 0x9F, 0x9D, + 0xF0, 0xD3, 0x35, 0x68, 0xD3, 0x20, 0x66, 0x98, + 0x00, 0xEB, 0x48, 0x36, 0x47, 0x2E, 0xA8, 0xA0, + 0x26, 0xD1, 0x6B, 0x71, 0x82, 0x46, 0x0C, 0x52, + }, + new byte[] + { + 0x98, 0x01, 0xF6, 0x9A, 0x36, 0x8C, 0x2B, 0xF6, + 0x75, 0xE5, 0x95, 0x21, 0xE1, 0x77, 0xD9, 0xA0, + 0x7F, 0x67, 0xEF, 0xE1, 0xCF, 0xDE, 0x8D, 0x3C, + 0x8D, 0x6F, 0x6A, 0x02, 0x56, 0xE3, 0xB1, 0x7D, + 0xB3, 0xC1, 0xB6, 0x2A, 0xD1, 0xB8, 0x55, 0x33, + 0x60, 0xD1, 0x73, 0x67, 0xEB, 0x15, 0x14, 0xD2, + }, + HashAlgorithmName.SHA384, + }; + } + } +} diff --git a/src/libraries/Common/tests/System/Security/Cryptography/SP800108HmacCounterKdfTests.Helpers.cs b/src/libraries/Common/tests/System/Security/Cryptography/SP800108HmacCounterKdfTests.Helpers.cs new file mode 100644 index 00000000000..527d06849de --- /dev/null +++ b/src/libraries/Common/tests/System/Security/Cryptography/SP800108HmacCounterKdfTests.Helpers.cs @@ -0,0 +1,169 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Collections.Generic; +using System.Globalization; +using System.Threading; +using Xunit; + +namespace System.Security.Cryptography.Tests +{ + public static partial class SP800108HmacCounterKdfTests + { + private const string Label = "label"; + private const string Context = "contextHeadercontext"; + private static readonly byte[] s_labelBytes = "label"u8.ToArray(); + private static readonly byte[] s_kdk = "kdk"u8.ToArray(); + private static readonly byte[] s_contextBytes = "contextHeadercontext"u8.ToArray(); + private static readonly HashAlgorithmName s_unknownHash = HashAlgorithmName.MD5; + private static readonly HashAlgorithmName s_nullHash = new HashAlgorithmName(null); + private static readonly HashAlgorithmName s_emptyHash = new HashAlgorithmName(""); + + private static void VerifyKbkdfBytes(byte[] expected, byte[] key, HashAlgorithmName hashAlgorithm, byte[] labelBytes, byte[] contextBytes) + { + byte[] result; + + using (SP800108HmacCounterKdf kdf = new SP800108HmacCounterKdf(key, hashAlgorithm)) + { + result = kdf.DeriveKey(labelBytes, contextBytes, expected.Length); + Assert.Equal(expected, result); + } + + using (SP800108HmacCounterKdf kdf = new SP800108HmacCounterKdf(key, hashAlgorithm)) + { + result = kdf.DeriveKey(new ReadOnlySpan(labelBytes), new ReadOnlySpan(contextBytes), expected.Length); + Assert.Equal(expected, result); + } + + using (SP800108HmacCounterKdf kdf = new SP800108HmacCounterKdf(key, hashAlgorithm)) + { + result = new byte[expected.Length]; + kdf.DeriveKey(new ReadOnlySpan(labelBytes), new ReadOnlySpan(contextBytes), result); + Assert.Equal(expected, result); + } + + result = SP800108HmacCounterKdf.DeriveBytes( + key, + hashAlgorithm, + labelBytes, + contextBytes, + expected.Length); + Assert.Equal(expected, result); + + result = SP800108HmacCounterKdf.DeriveBytes( + new ReadOnlySpan(key), + hashAlgorithm, + new ReadOnlySpan(labelBytes), + new ReadOnlySpan(contextBytes), + expected.Length); + Assert.Equal(expected, result); + + result = new byte[expected.Length]; + SP800108HmacCounterKdf.DeriveBytes( + new ReadOnlySpan(key), + hashAlgorithm, + new ReadOnlySpan(labelBytes), + new ReadOnlySpan(contextBytes), + result); + Assert.Equal(expected, result); + } + + private static void VerifyKbkdf(byte[] expected, byte[] key, HashAlgorithmName hashAlgorithm, char[] label, char[] context) + { + // The actual implementation uses a stricter UTF8 encoding/decoding but we know our test data does not contain + // invalid UTF8. + byte[] labelBytes = System.Text.Encoding.UTF8.GetBytes(label); + byte[] contextBytes = System.Text.Encoding.UTF8.GetBytes(context); + byte[] result; + + VerifyKbkdfBytes(expected, key, hashAlgorithm, labelBytes, contextBytes); + + using (SP800108HmacCounterKdf kdf = new SP800108HmacCounterKdf(key, hashAlgorithm)) + { + result = kdf.DeriveKey(new ReadOnlySpan(label), new ReadOnlySpan(context), expected.Length); + Assert.Equal(expected, result); + } + + using (SP800108HmacCounterKdf kdf = new SP800108HmacCounterKdf(key, hashAlgorithm)) + { + result = new byte[expected.Length]; + kdf.DeriveKey(new ReadOnlySpan(label), new ReadOnlySpan(context), result); + Assert.Equal(expected, result); + } + + using (SP800108HmacCounterKdf kdf = new SP800108HmacCounterKdf(key, hashAlgorithm)) + { + result = kdf.DeriveKey(new string(label), new string(context), expected.Length); + Assert.Equal(expected, result); + } + + result = SP800108HmacCounterKdf.DeriveBytes( + key, + hashAlgorithm, + new string(label), + new string(context), + expected.Length); + Assert.Equal(expected, result); + + result = SP800108HmacCounterKdf.DeriveBytes( + new ReadOnlySpan(key), + hashAlgorithm, + new ReadOnlySpan(label), + new ReadOnlySpan(context), + expected.Length); + Assert.Equal(expected, result); + + result = new byte[expected.Length]; + SP800108HmacCounterKdf.DeriveBytes( + new ReadOnlySpan(key), + hashAlgorithm, + new ReadOnlySpan(label), + new ReadOnlySpan(context), + result); + Assert.Equal(expected, result); + } + + private static void RaceCalls(byte[] expected1, byte[] expected2, bool isDisposing, Func call1, Func call2) + { + const int Iterations = 1_000; + + void ThreadCallback(object state) + { + (Func act, byte[] expected) = ((Func, byte[]))state; + byte[][] results = new byte[Iterations][]; + + for (int i = 0; i < Iterations; i++) + { + // defer asserting until after the loop so that the assert doesn't dominate the work of the + // threads interacting. + try + { + results[i] = act(i); + } + catch (ObjectDisposedException) when (isDisposing) + { + results[i] = null; + } + } + + for (int i = 0; i < Iterations; i++) + { + byte[] result = results[i]; + + if (result is not null) + { + Assert.Equal(expected, result); + } + } + } + + Thread t1 = new Thread(ThreadCallback); + Thread t2 = new Thread(ThreadCallback); + + t1.Start((call1, expected1)); + t2.Start((call2, expected2)); + t1.Join(); + t2.Join(); + } + } +} diff --git a/src/libraries/Common/tests/System/Security/Cryptography/SP800108HmacCounterKdfTests.ThreadSafety.cs b/src/libraries/Common/tests/System/Security/Cryptography/SP800108HmacCounterKdfTests.ThreadSafety.cs new file mode 100644 index 00000000000..cf1339eb4da --- /dev/null +++ b/src/libraries/Common/tests/System/Security/Cryptography/SP800108HmacCounterKdfTests.ThreadSafety.cs @@ -0,0 +1,200 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Collections.Generic; +using Xunit; + +namespace System.Security.Cryptography.Tests +{ + public static partial class SP800108HmacCounterKdfTests + { + [Fact] + [ActiveIssue("https://github.com/dotnet/runtime/issues/68162", TestPlatforms.Browser)] // wasm threading support + public static void Race_ReusingOneInstance_Allocating() + { + using (SP800108HmacCounterKdf kdf = new("kdf"u8, HashAlgorithmName.SHA256)) + { + RaceCalls( + new byte[] { 0xDC, 0xD6, 0x23, 0xE8, 0x59, 0xB8, 0x4B, 0x95, 0xBF, 0x44, 0x32, 0x6E, 0x2B, 0xA6, 0x34, 0xF0 }, + new byte[] { 0x92, 0xB0, 0xD7, 0xDA, 0x2C, 0xB1, 0xAA, 0x8C, 0xD5, 0xDF, 0x97, 0x9E, 0x61, 0xA3, 0x57, 0xD6 }, + isDisposing: false, + _ => kdf.DeriveKey("label"u8, "context"u8, derivedKeyLengthInBytes: 16), + _ => kdf.DeriveKey("label"u8, "bananas"u8, derivedKeyLengthInBytes: 16)); + } + + using (SP800108HmacCounterKdf kdf = new("kdf"u8, HashAlgorithmName.SHA256)) + { + RaceCalls( + new byte[] { 0xDC, 0xD6, 0x23, 0xE8, 0x59, 0xB8, 0x4B, 0x95, 0xBF, 0x44, 0x32, 0x6E, 0x2B, 0xA6, 0x34, 0xF0 }, + new byte[] { 0x92, 0xB0, 0xD7, 0xDA, 0x2C, 0xB1, 0xAA, 0x8C, 0xD5, 0xDF, 0x97, 0x9E, 0x61, 0xA3, 0x57, 0xD6 }, + isDisposing: false, + _ => kdf.DeriveKey("label"u8.ToArray(), "context"u8.ToArray(), derivedKeyLengthInBytes: 16), + _ => kdf.DeriveKey("label"u8.ToArray(), "bananas"u8.ToArray(), derivedKeyLengthInBytes: 16)); + } + + using (SP800108HmacCounterKdf kdf = new("kdf"u8, HashAlgorithmName.SHA256)) + { + RaceCalls( + new byte[] { 0xDC, 0xD6, 0x23, 0xE8, 0x59, 0xB8, 0x4B, 0x95, 0xBF, 0x44, 0x32, 0x6E, 0x2B, 0xA6, 0x34, 0xF0 }, + new byte[] { 0x92, 0xB0, 0xD7, 0xDA, 0x2C, 0xB1, 0xAA, 0x8C, 0xD5, 0xDF, 0x97, 0x9E, 0x61, 0xA3, 0x57, 0xD6 }, + isDisposing: false, + _ => kdf.DeriveKey("label", "context", derivedKeyLengthInBytes: 16), + _ => kdf.DeriveKey("label", "bananas", derivedKeyLengthInBytes: 16)); + } + + using (SP800108HmacCounterKdf kdf = new("kdf"u8, HashAlgorithmName.SHA256)) + { + RaceCalls( + new byte[] { 0xDC, 0xD6, 0x23, 0xE8, 0x59, 0xB8, 0x4B, 0x95, 0xBF, 0x44, 0x32, 0x6E, 0x2B, 0xA6, 0x34, 0xF0 }, + new byte[] { 0x92, 0xB0, 0xD7, 0xDA, 0x2C, 0xB1, 0xAA, 0x8C, 0xD5, 0xDF, 0x97, 0x9E, 0x61, 0xA3, 0x57, 0xD6 }, + isDisposing: false, + _ => kdf.DeriveKey("label".AsSpan(), "context".AsSpan(), derivedKeyLengthInBytes: 16), + _ => kdf.DeriveKey("label".AsSpan(), "bananas".AsSpan(), derivedKeyLengthInBytes: 16)); + } + } + + [Fact] + [ActiveIssue("https://github.com/dotnet/runtime/issues/68162", TestPlatforms.Browser)] // wasm threading support + public static void Race_ReusingOneInstance_Buffering() + { + using (SP800108HmacCounterKdf kdf = new("kdf"u8, HashAlgorithmName.SHA256)) + { + RaceCalls( + new byte[] { 0xDC, 0xD6, 0x23, 0xE8, 0x59, 0xB8, 0x4B, 0x95, 0xBF, 0x44, 0x32, 0x6E, 0x2B, 0xA6, 0x34, 0xF0 }, + new byte[] { 0x92, 0xB0, 0xD7, 0xDA, 0x2C, 0xB1, 0xAA, 0x8C, 0xD5, 0xDF, 0x97, 0x9E, 0x61, 0xA3, 0x57, 0xD6 }, + isDisposing: false, + _ => { + byte[] result = new byte[16]; + kdf.DeriveKey("label"u8, "context"u8, result); + return result; + }, + _ => { + byte[] result = new byte[16]; + kdf.DeriveKey("label"u8, "bananas"u8, result); + return result; + }); + } + + using (SP800108HmacCounterKdf kdf = new("kdf"u8, HashAlgorithmName.SHA256)) + { + RaceCalls( + new byte[] { 0xDC, 0xD6, 0x23, 0xE8, 0x59, 0xB8, 0x4B, 0x95, 0xBF, 0x44, 0x32, 0x6E, 0x2B, 0xA6, 0x34, 0xF0 }, + new byte[] { 0x92, 0xB0, 0xD7, 0xDA, 0x2C, 0xB1, 0xAA, 0x8C, 0xD5, 0xDF, 0x97, 0x9E, 0x61, 0xA3, 0x57, 0xD6 }, + isDisposing: false, + _ => { + byte[] result = new byte[16]; + kdf.DeriveKey("label"u8.ToArray(), "context"u8.ToArray(), result); + return result; + }, + _ => { + byte[] result = new byte[16]; + kdf.DeriveKey("label"u8.ToArray(), "bananas"u8.ToArray(), result); + return result; + }); + } + + using (SP800108HmacCounterKdf kdf = new("kdf"u8, HashAlgorithmName.SHA256)) + { + RaceCalls( + new byte[] { 0xDC, 0xD6, 0x23, 0xE8, 0x59, 0xB8, 0x4B, 0x95, 0xBF, 0x44, 0x32, 0x6E, 0x2B, 0xA6, 0x34, 0xF0 }, + new byte[] { 0x92, 0xB0, 0xD7, 0xDA, 0x2C, 0xB1, 0xAA, 0x8C, 0xD5, 0xDF, 0x97, 0x9E, 0x61, 0xA3, 0x57, 0xD6 }, + isDisposing: false, + _ => { + return kdf.DeriveKey("label", "context", 16); + }, + _ => { + return kdf.DeriveKey("label", "bananas", 16); + }); + } + + using (SP800108HmacCounterKdf kdf = new("kdf"u8, HashAlgorithmName.SHA256)) + { + RaceCalls( + new byte[] { 0xDC, 0xD6, 0x23, 0xE8, 0x59, 0xB8, 0x4B, 0x95, 0xBF, 0x44, 0x32, 0x6E, 0x2B, 0xA6, 0x34, 0xF0 }, + new byte[] { 0x92, 0xB0, 0xD7, 0xDA, 0x2C, 0xB1, 0xAA, 0x8C, 0xD5, 0xDF, 0x97, 0x9E, 0x61, 0xA3, 0x57, 0xD6 }, + isDisposing: false, + _ => { + byte[] result = new byte[16]; + kdf.DeriveKey("label".AsSpan(), "context".AsSpan(), result); + return result; + }, + _ => { + byte[] result = new byte[16]; + kdf.DeriveKey("label".AsSpan(), "bananas".AsSpan(), result); + return result; + }); + } + } + + [Fact] + [ActiveIssue("https://github.com/dotnet/runtime/issues/68162", TestPlatforms.Browser)] // wasm threading support + public static void Race_UseAndDisposeOneInstance_Allocating() + { + SP800108HmacCounterKdf kdf; + + kdf = new SP800108HmacCounterKdf("kdf"u8, HashAlgorithmName.SHA256); + RaceCalls( + new byte[] { 0xDC, 0xD6, 0x23, 0xE8, 0x59, 0xB8, 0x4B, 0x95, 0xBF, 0x44, 0x32, 0x6E, 0x2B, 0xA6, 0x34, 0xF0 }, + new byte[] { 0x92, 0xB0, 0xD7, 0xDA, 0x2C, 0xB1, 0xAA, 0x8C, 0xD5, 0xDF, 0x97, 0x9E, 0x61, 0xA3, 0x57, 0xD6 }, + isDisposing: true, + _ => kdf.DeriveKey("label"u8, "context"u8, derivedKeyLengthInBytes: 16), + i => { + if (i == 50) + { + kdf.Dispose(); + kdf.Dispose(); + } + + return null; + }); + + kdf = new SP800108HmacCounterKdf("kdf"u8, HashAlgorithmName.SHA256); + RaceCalls( + new byte[] { 0xDC, 0xD6, 0x23, 0xE8, 0x59, 0xB8, 0x4B, 0x95, 0xBF, 0x44, 0x32, 0x6E, 0x2B, 0xA6, 0x34, 0xF0 }, + new byte[] { 0x92, 0xB0, 0xD7, 0xDA, 0x2C, 0xB1, 0xAA, 0x8C, 0xD5, 0xDF, 0x97, 0x9E, 0x61, 0xA3, 0x57, 0xD6 }, + isDisposing: true, + _ => kdf.DeriveKey("label"u8.ToArray(), "context"u8.ToArray(), derivedKeyLengthInBytes: 16), + i => { + if (i == 50) + { + kdf.Dispose(); + kdf.Dispose(); + } + + return null; + }); + + kdf = new SP800108HmacCounterKdf("kdf"u8, HashAlgorithmName.SHA256); + RaceCalls( + new byte[] { 0xDC, 0xD6, 0x23, 0xE8, 0x59, 0xB8, 0x4B, 0x95, 0xBF, 0x44, 0x32, 0x6E, 0x2B, 0xA6, 0x34, 0xF0 }, + new byte[] { 0x92, 0xB0, 0xD7, 0xDA, 0x2C, 0xB1, 0xAA, 0x8C, 0xD5, 0xDF, 0x97, 0x9E, 0x61, 0xA3, 0x57, 0xD6 }, + isDisposing: true, + _ => kdf.DeriveKey("label", "context", derivedKeyLengthInBytes: 16), + i => { + if (i == 50) + { + kdf.Dispose(); + kdf.Dispose(); + } + + return null; + }); + + kdf = new SP800108HmacCounterKdf("kdf"u8, HashAlgorithmName.SHA256); + RaceCalls( + new byte[] { 0xDC, 0xD6, 0x23, 0xE8, 0x59, 0xB8, 0x4B, 0x95, 0xBF, 0x44, 0x32, 0x6E, 0x2B, 0xA6, 0x34, 0xF0 }, + new byte[] { 0x92, 0xB0, 0xD7, 0xDA, 0x2C, 0xB1, 0xAA, 0x8C, 0xD5, 0xDF, 0x97, 0x9E, 0x61, 0xA3, 0x57, 0xD6 }, + isDisposing: true, + _ => kdf.DeriveKey("label".AsSpan(), "context".AsSpan(), derivedKeyLengthInBytes: 16), + i => { + if (i == 50) + { + kdf.Dispose(); + kdf.Dispose(); + } + + return null; + }); + } + } +} diff --git a/src/libraries/Microsoft.Bcl.Cryptography/Microsoft.Bcl.Cryptography.sln b/src/libraries/Microsoft.Bcl.Cryptography/Microsoft.Bcl.Cryptography.sln new file mode 100644 index 00000000000..da578eba0cd --- /dev/null +++ b/src/libraries/Microsoft.Bcl.Cryptography/Microsoft.Bcl.Cryptography.sln @@ -0,0 +1,67 @@ +Microsoft Visual Studio Solution File, Format Version 12.00 +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "ref", "ref", "{7E9F6DE1-771B-4E25-A603-EC43D0291C8B}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.Bcl.Cryptography", "ref\Microsoft.Bcl.Cryptography.csproj", "{63655B2E-6A06-4E48-9F01-D0B910063165}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{98708A22-7268-4EDB-AE37-70AA958A772A}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.Bcl.Cryptography", "src\Microsoft.Bcl.Cryptography.csproj", "{B0716D7E-B824-4866-A1ED-DF31BA2970B9}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{8C3BD4AD-1A56-4204-9826-F8B74251D19F}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "TestUtilities", "..\Common\tests\TestUtilities\TestUtilities.csproj", "{1AEF7C7B-5A86-4A5E-9F8B-3933F7624751}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.Bcl.Cryptography.Tests", "tests\Microsoft.Bcl.Cryptography.Tests.csproj", "{E66D17AB-BBAF-4F2B-AC9C-8E89BDCC6191}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "gen", "gen", "{34897637-11A1-48A4-AF1F-E11463A61D0B}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.Interop.SourceGeneration", "..\System.Runtime.InteropServices\gen\Microsoft.Interop.SourceGeneration\Microsoft.Interop.SourceGeneration.csproj", "{A4C2BDDC-1AFB-45A8-9E9B-4AD7396A4DF2}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "LibraryImportGenerator", "..\System.Runtime.InteropServices\gen\LibraryImportGenerator\LibraryImportGenerator.csproj", "{E9271403-BEF5-46E9-B68B-16EF69AA7149}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {63655B2E-6A06-4E48-9F01-D0B910063165}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {63655B2E-6A06-4E48-9F01-D0B910063165}.Debug|Any CPU.Build.0 = Debug|Any CPU + {63655B2E-6A06-4E48-9F01-D0B910063165}.Release|Any CPU.ActiveCfg = Release|Any CPU + {63655B2E-6A06-4E48-9F01-D0B910063165}.Release|Any CPU.Build.0 = Release|Any CPU + {B0716D7E-B824-4866-A1ED-DF31BA2970B9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {B0716D7E-B824-4866-A1ED-DF31BA2970B9}.Debug|Any CPU.Build.0 = Debug|Any CPU + {B0716D7E-B824-4866-A1ED-DF31BA2970B9}.Release|Any CPU.ActiveCfg = Release|Any CPU + {B0716D7E-B824-4866-A1ED-DF31BA2970B9}.Release|Any CPU.Build.0 = Release|Any CPU + {1AEF7C7B-5A86-4A5E-9F8B-3933F7624751}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {1AEF7C7B-5A86-4A5E-9F8B-3933F7624751}.Debug|Any CPU.Build.0 = Debug|Any CPU + {1AEF7C7B-5A86-4A5E-9F8B-3933F7624751}.Release|Any CPU.ActiveCfg = Release|Any CPU + {1AEF7C7B-5A86-4A5E-9F8B-3933F7624751}.Release|Any CPU.Build.0 = Release|Any CPU + {E66D17AB-BBAF-4F2B-AC9C-8E89BDCC6191}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {E66D17AB-BBAF-4F2B-AC9C-8E89BDCC6191}.Debug|Any CPU.Build.0 = Debug|Any CPU + {E66D17AB-BBAF-4F2B-AC9C-8E89BDCC6191}.Release|Any CPU.ActiveCfg = Release|Any CPU + {E66D17AB-BBAF-4F2B-AC9C-8E89BDCC6191}.Release|Any CPU.Build.0 = Release|Any CPU + {A4C2BDDC-1AFB-45A8-9E9B-4AD7396A4DF2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {A4C2BDDC-1AFB-45A8-9E9B-4AD7396A4DF2}.Debug|Any CPU.Build.0 = Debug|Any CPU + {A4C2BDDC-1AFB-45A8-9E9B-4AD7396A4DF2}.Release|Any CPU.ActiveCfg = Release|Any CPU + {A4C2BDDC-1AFB-45A8-9E9B-4AD7396A4DF2}.Release|Any CPU.Build.0 = Release|Any CPU + {E9271403-BEF5-46E9-B68B-16EF69AA7149}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {E9271403-BEF5-46E9-B68B-16EF69AA7149}.Debug|Any CPU.Build.0 = Debug|Any CPU + {E9271403-BEF5-46E9-B68B-16EF69AA7149}.Release|Any CPU.ActiveCfg = Release|Any CPU + {E9271403-BEF5-46E9-B68B-16EF69AA7149}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(NestedProjects) = preSolution + {63655B2E-6A06-4E48-9F01-D0B910063165} = {7E9F6DE1-771B-4E25-A603-EC43D0291C8B} + {B0716D7E-B824-4866-A1ED-DF31BA2970B9} = {98708A22-7268-4EDB-AE37-70AA958A772A} + {1AEF7C7B-5A86-4A5E-9F8B-3933F7624751} = {8C3BD4AD-1A56-4204-9826-F8B74251D19F} + {E66D17AB-BBAF-4F2B-AC9C-8E89BDCC6191} = {8C3BD4AD-1A56-4204-9826-F8B74251D19F} + {A4C2BDDC-1AFB-45A8-9E9B-4AD7396A4DF2} = {34897637-11A1-48A4-AF1F-E11463A61D0B} + {E9271403-BEF5-46E9-B68B-16EF69AA7149} = {34897637-11A1-48A4-AF1F-E11463A61D0B} + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {EAA35B12-9858-4428-8510-F09B19933FB9} + EndGlobalSection +EndGlobal diff --git a/src/libraries/Microsoft.Bcl.Cryptography/ref/Microsoft.Bcl.Cryptography.Forwards.cs b/src/libraries/Microsoft.Bcl.Cryptography/ref/Microsoft.Bcl.Cryptography.Forwards.cs new file mode 100644 index 00000000000..5f6e0fbf895 --- /dev/null +++ b/src/libraries/Microsoft.Bcl.Cryptography/ref/Microsoft.Bcl.Cryptography.Forwards.cs @@ -0,0 +1,4 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +[assembly: System.Runtime.CompilerServices.TypeForwardedTo(typeof(System.Security.Cryptography.SP800108HmacCounterKdf))] diff --git a/src/libraries/Microsoft.Bcl.Cryptography/ref/Microsoft.Bcl.Cryptography.cs b/src/libraries/Microsoft.Bcl.Cryptography/ref/Microsoft.Bcl.Cryptography.cs new file mode 100644 index 00000000000..83dc7a9f3fa --- /dev/null +++ b/src/libraries/Microsoft.Bcl.Cryptography/ref/Microsoft.Bcl.Cryptography.cs @@ -0,0 +1,27 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// ------------------------------------------------------------------------------ +// Changes to this file must follow the https://aka.ms/api-review process. +// ------------------------------------------------------------------------------ + +namespace System.Security.Cryptography +{ + public sealed partial class SP800108HmacCounterKdf : System.IDisposable + { + public SP800108HmacCounterKdf(byte[] key, System.Security.Cryptography.HashAlgorithmName hashAlgorithm) { } + public SP800108HmacCounterKdf(System.ReadOnlySpan key, System.Security.Cryptography.HashAlgorithmName hashAlgorithm) { } + public static byte[] DeriveBytes(byte[] key, System.Security.Cryptography.HashAlgorithmName hashAlgorithm, byte[] label, byte[] context, int derivedKeyLengthInBytes) { throw null; } + public static byte[] DeriveBytes(byte[] key, System.Security.Cryptography.HashAlgorithmName hashAlgorithm, string label, string context, int derivedKeyLengthInBytes) { throw null; } + public static byte[] DeriveBytes(System.ReadOnlySpan key, System.Security.Cryptography.HashAlgorithmName hashAlgorithm, System.ReadOnlySpan label, System.ReadOnlySpan context, int derivedKeyLengthInBytes) { throw null; } + public static void DeriveBytes(System.ReadOnlySpan key, System.Security.Cryptography.HashAlgorithmName hashAlgorithm, System.ReadOnlySpan label, System.ReadOnlySpan context, System.Span destination) { } + public static byte[] DeriveBytes(System.ReadOnlySpan key, System.Security.Cryptography.HashAlgorithmName hashAlgorithm, System.ReadOnlySpan label, System.ReadOnlySpan context, int derivedKeyLengthInBytes) { throw null; } + public static void DeriveBytes(System.ReadOnlySpan key, System.Security.Cryptography.HashAlgorithmName hashAlgorithm, System.ReadOnlySpan label, System.ReadOnlySpan context, System.Span destination) { } + public byte[] DeriveKey(byte[] label, byte[] context, int derivedKeyLengthInBytes) { throw null; } + public byte[] DeriveKey(System.ReadOnlySpan label, System.ReadOnlySpan context, int derivedKeyLengthInBytes) { throw null; } + public void DeriveKey(System.ReadOnlySpan label, System.ReadOnlySpan context, System.Span destination) { } + public byte[] DeriveKey(System.ReadOnlySpan label, System.ReadOnlySpan context, int derivedKeyLengthInBytes) { throw null; } + public void DeriveKey(System.ReadOnlySpan label, System.ReadOnlySpan context, System.Span destination) { } + public byte[] DeriveKey(string label, string context, int derivedKeyLengthInBytes) { throw null; } + public void Dispose() { } + } +} diff --git a/src/libraries/Microsoft.Bcl.Cryptography/ref/Microsoft.Bcl.Cryptography.csproj b/src/libraries/Microsoft.Bcl.Cryptography/ref/Microsoft.Bcl.Cryptography.csproj new file mode 100644 index 00000000000..8e799cf5606 --- /dev/null +++ b/src/libraries/Microsoft.Bcl.Cryptography/ref/Microsoft.Bcl.Cryptography.csproj @@ -0,0 +1,17 @@ + + + $(NetCoreAppCurrent);netstandard2.0;$(NetFrameworkMinimum) + + + + + + + + + + + + + + diff --git a/src/libraries/Microsoft.Bcl.Cryptography/src/Microsoft.Bcl.Cryptography.csproj b/src/libraries/Microsoft.Bcl.Cryptography/src/Microsoft.Bcl.Cryptography.csproj new file mode 100644 index 00000000000..ba7b0ce6b63 --- /dev/null +++ b/src/libraries/Microsoft.Bcl.Cryptography/src/Microsoft.Bcl.Cryptography.csproj @@ -0,0 +1,79 @@ + + + $(NetCoreAppCurrent);netstandard2.0;$(NetFrameworkMinimum) + true + true + + true + Provides support for some cryptographic primitives for .NET Framework and .NET Standard. + +Commonly Used Types: +System.Security.Cryptography.SP800108HmacCounterKdf + + + + true + true + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/libraries/Microsoft.Bcl.Cryptography/src/Resources/Strings.resx b/src/libraries/Microsoft.Bcl.Cryptography/src/Resources/Strings.resx new file mode 100644 index 00000000000..85b7fab4f32 --- /dev/null +++ b/src/libraries/Microsoft.Bcl.Cryptography/src/Resources/Strings.resx @@ -0,0 +1,72 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + Non-negative number required. + + + The number of bytes requested is too large. The number of bytes produced by SP800108HmacCounterKdf cannot exceed 536,870,911 bytes. + + + The value cannot be an empty string. + + + '{0}' is not a known hash algorithm. + + diff --git a/src/libraries/Microsoft.Bcl.Cryptography/src/System/Security/Cryptography/HashAlgorithmNames.cs b/src/libraries/Microsoft.Bcl.Cryptography/src/System/Security/Cryptography/HashAlgorithmNames.cs new file mode 100644 index 00000000000..8f10590302e --- /dev/null +++ b/src/libraries/Microsoft.Bcl.Cryptography/src/System/Security/Cryptography/HashAlgorithmNames.cs @@ -0,0 +1,13 @@ +// 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 static class HashAlgorithmNames + { + internal const string SHA1 = nameof(SHA1); + internal const string SHA256 = nameof(SHA256); + internal const string SHA384 = nameof(SHA384); + internal const string SHA512 = nameof(SHA512); + } +} diff --git a/src/libraries/Microsoft.Bcl.Cryptography/src/System/Security/Cryptography/NetStandardShims.cs b/src/libraries/Microsoft.Bcl.Cryptography/src/System/Security/Cryptography/NetStandardShims.cs new file mode 100644 index 00000000000..63cd4cd5e09 --- /dev/null +++ b/src/libraries/Microsoft.Bcl.Cryptography/src/System/Security/Cryptography/NetStandardShims.cs @@ -0,0 +1,34 @@ +// 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; +using System.Runtime.CompilerServices; + +namespace System.Security.Cryptography +{ + internal static class NetStandardShims + { + internal static unsafe int GetBytes(this Encoding encoding, ReadOnlySpan str, Span destination) + { + if (str.IsEmpty) + { + return 0; + } + + fixed (char* pStr = str) + fixed (byte* pDestination = destination) + { + return encoding.GetBytes(pStr, str.Length, pDestination, destination.Length); + } + } + } + + internal static class CryptographicOperations + { + [MethodImpl(MethodImplOptions.NoInlining | MethodImplOptions.NoOptimization)] + internal static void ZeroMemory(Span buffer) + { + buffer.Clear(); + } + } +} diff --git a/src/libraries/Microsoft.Bcl.Cryptography/src/System/Security/Cryptography/SP800108HmacCounterKdf.cs b/src/libraries/Microsoft.Bcl.Cryptography/src/System/Security/Cryptography/SP800108HmacCounterKdf.cs new file mode 100644 index 00000000000..c4850f1508a --- /dev/null +++ b/src/libraries/Microsoft.Bcl.Cryptography/src/System/Security/Cryptography/SP800108HmacCounterKdf.cs @@ -0,0 +1,100 @@ +// 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 System.Runtime.Versioning; +using System.Text; + +namespace System.Security.Cryptography +{ + public sealed partial class SP800108HmacCounterKdf : IDisposable + { + private static readonly bool s_useCngKeyDerivation = IsWindows8OrGreater(); + + private static partial SP800108HmacCounterKdfImplementationBase CreateImplementation( + ReadOnlySpan key, + HashAlgorithmName hashAlgorithm) + { + if (s_useCngKeyDerivation) + { + return new SP800108HmacCounterKdfImplementationCng(key, hashAlgorithm); + } + else + { + return new SP800108HmacCounterKdfImplementationManaged(key, hashAlgorithm); + } + } + + private static partial byte[] DeriveBytesCore( + byte[] key, + HashAlgorithmName hashAlgorithm, + byte[] label, + byte[] context, + int derivedKeyLengthInBytes) + { + byte[] result = new byte[derivedKeyLengthInBytes]; + + if (s_useCngKeyDerivation) + { + SP800108HmacCounterKdfImplementationCng.DeriveBytesOneShot(key, hashAlgorithm, label, context, result); + } + else + { + SP800108HmacCounterKdfImplementationManaged.DeriveBytesOneShot(key, hashAlgorithm, label, context, result); + } + + return result; + } + + private static partial void DeriveBytesCore( + ReadOnlySpan key, + HashAlgorithmName hashAlgorithm, + ReadOnlySpan label, + ReadOnlySpan context, + Span destination) + { + if (s_useCngKeyDerivation) + { + SP800108HmacCounterKdfImplementationCng.DeriveBytesOneShot(key, hashAlgorithm, label, context, destination); + } + else + { + SP800108HmacCounterKdfImplementationManaged.DeriveBytesOneShot(key, hashAlgorithm, label, context, destination); + } + } + + private static partial void DeriveBytesCore( + ReadOnlySpan key, + HashAlgorithmName hashAlgorithm, + ReadOnlySpan label, + ReadOnlySpan context, + Span destination) + { + if (s_useCngKeyDerivation) + { + SP800108HmacCounterKdfImplementationCng.DeriveBytesOneShot(key, hashAlgorithm, label, context, destination); + } + else + { + SP800108HmacCounterKdfImplementationManaged.DeriveBytesOneShot(key, hashAlgorithm, label, context, destination); + } + } + + private static bool IsWindows8OrGreater() + { +#if NET + return OperatingSystem.IsWindowsVersionAtLeast(6, 2); +#elif NETSTANDARD + bool isWindows = RuntimeInformation.IsOSPlatform(OSPlatform.Windows); + Version version = Environment.OSVersion.Version; + return isWindows && (version.Major > 6 || (version.Major == 6 && version.Minor >= 2)); +#elif NETFRAMEWORK + Version version = Environment.OSVersion.Version; + return version.Major > 6 || (version.Major == 6 && version.Minor >= 2); +#else +#error Unhandled platform target +#endif + } + } +} diff --git a/src/libraries/Microsoft.Bcl.Cryptography/src/System/Security/Cryptography/SP800108HmacCounterKdfImplementationCng.cs b/src/libraries/Microsoft.Bcl.Cryptography/src/System/Security/Cryptography/SP800108HmacCounterKdfImplementationCng.cs new file mode 100644 index 00000000000..8252fc31463 --- /dev/null +++ b/src/libraries/Microsoft.Bcl.Cryptography/src/System/Security/Cryptography/SP800108HmacCounterKdfImplementationCng.cs @@ -0,0 +1,113 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Diagnostics; + +namespace System.Security.Cryptography +{ + internal sealed partial class SP800108HmacCounterKdfImplementationCng + { + internal unsafe SP800108HmacCounterKdfImplementationCng(ReadOnlySpan key, HashAlgorithmName hashAlgorithm) + { + Debug.Assert(hashAlgorithm.Name is not null); + + scoped ReadOnlySpan symmetricKeyMaterial; + scoped Span clearSpan = default; + int symmetricKeyMaterialLength; + int hashAlgorithmBlockSize = GetHashBlockSize(hashAlgorithm.Name); + + if (key.Length > hashAlgorithmBlockSize) + { + byte[] keyArray = new byte[key.Length]; + + fixed (byte* pKeyArray = keyArray) + { + key.CopyTo(keyArray); + clearSpan = HashOneShot(hashAlgorithm, keyArray); + CryptographicOperations.ZeroMemory(keyArray); + } + + symmetricKeyMaterial = clearSpan; + symmetricKeyMaterialLength = symmetricKeyMaterial.Length; + } + else if (!key.IsEmpty) + { + symmetricKeyMaterial = key; + symmetricKeyMaterialLength = key.Length; + } + else + { + // CNG requires a non-null pointer even when the length is zero. + symmetricKeyMaterial = stackalloc byte[] { 0 }; + symmetricKeyMaterialLength = 0; + } + + try + { + fixed (byte* pSymmetricKeyMaterial = symmetricKeyMaterial) + { + _keyHandle = CreateSymmetricKey(pSymmetricKeyMaterial, symmetricKeyMaterialLength); + } + } + finally + { + CryptographicOperations.ZeroMemory(clearSpan); + } + + _hashAlgorithm = hashAlgorithm; + } + + // For .NET Standard / .NET Framework, provide a byte overload so that we don't go from array->span->array + // when we need to adjust keys that are too large. + internal unsafe SP800108HmacCounterKdfImplementationCng(byte[] key, HashAlgorithmName hashAlgorithm) + { + Debug.Assert(hashAlgorithm.Name is not null); + + scoped ReadOnlySpan symmetricKeyMaterial; + scoped Span clearSpan = default; + int symmetricKeyMaterialLength; + int hashAlgorithmBlockSize = GetHashBlockSize(hashAlgorithm.Name); + + if (key.Length > hashAlgorithmBlockSize) + { + clearSpan = HashOneShot(hashAlgorithm, key); + symmetricKeyMaterial = clearSpan; + symmetricKeyMaterialLength = symmetricKeyMaterial.Length; + } + else if (key.Length > 0) + { + symmetricKeyMaterial = key; + symmetricKeyMaterialLength = key.Length; + } + else + { + // CNG requires a non-null pointer even when the length is zero. + symmetricKeyMaterial = stackalloc byte[] { 0 }; + symmetricKeyMaterialLength = 0; + } + + try + { + fixed (byte* pSymmetricKeyMaterial = symmetricKeyMaterial) + { + _keyHandle = CreateSymmetricKey(pSymmetricKeyMaterial, symmetricKeyMaterialLength); + } + } + finally + { + CryptographicOperations.ZeroMemory(clearSpan); + } + + _hashAlgorithm = hashAlgorithm; + } + + private static byte[] HashOneShot(HashAlgorithmName hashAlgorithm, byte[] data) + { + using (IncrementalHash hash = IncrementalHash.CreateHash(hashAlgorithm)) + { + hash.AppendData(data); + return hash.GetHashAndReset(); + } + } + } +} diff --git a/src/libraries/Microsoft.Bcl.Cryptography/src/System/Security/Cryptography/SP800108HmacCounterKdfImplementationManaged.cs b/src/libraries/Microsoft.Bcl.Cryptography/src/System/Security/Cryptography/SP800108HmacCounterKdfImplementationManaged.cs new file mode 100644 index 00000000000..60e438fd551 --- /dev/null +++ b/src/libraries/Microsoft.Bcl.Cryptography/src/System/Security/Cryptography/SP800108HmacCounterKdfImplementationManaged.cs @@ -0,0 +1,203 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Diagnostics; +using System.Text; + +namespace System.Security.Cryptography +{ + internal sealed partial class SP800108HmacCounterKdfImplementationManaged + { + public SP800108HmacCounterKdfImplementationManaged(ReadOnlySpan key, HashAlgorithmName hashAlgorithm) + { + _key = key.ToArray(); + _hashAlgorithm = hashAlgorithm; + } + + internal static unsafe void DeriveBytesOneShot( + ReadOnlySpan key, + HashAlgorithmName hashAlgorithm, + ReadOnlySpan label, + ReadOnlySpan context, + Span destination) + { + if (destination.IsEmpty) + { + return; + } + + // IncrementalHash needs an array of the correct size, so we can't rent for the key. + byte[] keyBuffer = new byte[key.Length]; + byte[] labelBuffer = CryptoPool.Rent(label.Length); + byte[] contextBuffer = CryptoPool.Rent(context.Length); + + // Fixed to prevent GC moves. + fixed (byte* pKeyBuffer = keyBuffer) + { + try + { + key.CopyTo(keyBuffer); + label.CopyTo(labelBuffer); + context.CopyTo(contextBuffer); + + DeriveBytesOneShot( + keyBuffer, + hashAlgorithm, + labelBuffer, + label.Length, + contextBuffer, + context.Length, + destination); + } + finally + { + CryptographicOperations.ZeroMemory(keyBuffer); + CryptoPool.Return(labelBuffer, clearSize: label.Length); + CryptoPool.Return(contextBuffer, clearSize: context.Length); + } + } + } + + internal static void DeriveBytesOneShot( + byte[] key, + HashAlgorithmName hashAlgorithm, + byte[] label, + byte[] context, + Span destination) + { + DeriveBytesOneShot(key, hashAlgorithm, label, label.Length, context, context.Length, destination); + } + + internal static unsafe void DeriveBytesOneShot( + ReadOnlySpan key, + HashAlgorithmName hashAlgorithm, + ReadOnlySpan label, + ReadOnlySpan context, + Span destination) + { + if (destination.Length == 0) + { + return; + } + + // The netstandard implementation needs arrays for all inputs, so always rent and don't perform + // the UTF8 encoding on the stack since that will just end up renting again anyway. + + Encoding utf8ThrowingEncoding = Utf8DataEncoding.ThrowingUtf8Encoding; + byte[] labelBuffer = CryptoPool.Rent(utf8ThrowingEncoding.GetMaxByteCount(label.Length)); + byte[] contextBuffer = CryptoPool.Rent(utf8ThrowingEncoding.GetMaxByteCount(context.Length)); + int labelWritten = 0; + int contextWritten = 0; + + byte[] keyBuffer = new byte[key.Length]; + + fixed (byte* pKeyBuffer = keyBuffer) + { + try + { + labelWritten = utf8ThrowingEncoding.GetBytes(label, labelBuffer); + contextWritten = utf8ThrowingEncoding.GetBytes(context, contextBuffer); + key.CopyTo(keyBuffer); + + DeriveBytesOneShot( + keyBuffer, + hashAlgorithm, + labelBuffer, + labelWritten, + contextBuffer, + contextWritten, + destination); + } + finally + { + CryptographicOperations.ZeroMemory(keyBuffer); + CryptoPool.Return(labelBuffer, labelWritten); + CryptoPool.Return(contextBuffer, contextWritten); + } + } + } + + private static void DeriveBytesOneShot( + byte[] key, + HashAlgorithmName hashAlgorithm, + byte[] label, + int labelLength, + byte[] context, + int contextLength, + Span destination) + { + if (destination.Length == 0) + { + return; + } + + Debug.Assert(destination.Length <= 0x1FFFFFFF); + + // Do everything as checked. Over/underflows are never expected. + checked + { + // The KDF is defined as K(i) := PRF (KI, [i]2 || Label || 0x00 || Context || [L]2) + // We know L is already less than 0x1FFFFFFF. h = ceil(L / h) where H is the hash length in bits. + // So we don't expect i to overflow. + using (IncrementalHash hash = IncrementalHash.CreateHMAC(hashAlgorithm, key)) + { + // We use this rented buffer for three things. The first two uints for i and L, and last byte + // for the zero separator. So this is + // uint(L) | uint(i) | byte(0) + // This lets us use a single rent for the L, i, and 0x00. + const int LOffset = 0; + const int LLength = sizeof(uint); + const int IOffset = LLength + LOffset; + const int ILength = sizeof(uint); + const int ZeroOffset = IOffset + ILength; + const int ZeroLength = sizeof(byte); + const int RentSize = ZeroOffset + ZeroLength; + + byte[]? rentedBuffer = null; + + try + { + rentedBuffer = CryptoPool.Rent(RentSize); + + WriteUInt32BigEndian((uint)destination.Length * 8U, rentedBuffer.AsSpan(LOffset, LLength)); + rentedBuffer[ZeroOffset] = 0; + + for (uint i = 1; !destination.IsEmpty; i++) + { + WriteUInt32BigEndian(i, rentedBuffer.AsSpan(IOffset, ILength)); + hash.AppendData(rentedBuffer, IOffset, ILength); + hash.AppendData(label, 0, labelLength); + hash.AppendData(rentedBuffer, ZeroOffset, ZeroLength); + hash.AppendData(context, 0, contextLength); + hash.AppendData(rentedBuffer, LOffset, LLength); + + byte[] hmac = hash.GetHashAndReset(); + int needed = Math.Min(destination.Length, hmac.Length); + hmac.AsSpan(0, needed).CopyTo(destination); + destination = destination.Slice(needed); + + // Best effort to zero out the key material. + CryptographicOperations.ZeroMemory(hmac); + } + } + finally + { + if (rentedBuffer is not null) + { + CryptoPool.Return(rentedBuffer, clearSize: RentSize); + } + } + } + } + } + + private static void WriteUInt32BigEndian(uint value, Span destination) + { + Debug.Assert(destination.Length == sizeof(uint)); + destination[0] = (byte)(value >> 24); + destination[1] = (byte)(value >> 16); + destination[2] = (byte)(value >> 8); + destination[3] = (byte)(value); + } + } +} diff --git a/src/libraries/Microsoft.Bcl.Cryptography/tests/Microsoft.Bcl.Cryptography.Tests.csproj b/src/libraries/Microsoft.Bcl.Cryptography/tests/Microsoft.Bcl.Cryptography.Tests.csproj new file mode 100644 index 00000000000..02ee9aae4af --- /dev/null +++ b/src/libraries/Microsoft.Bcl.Cryptography/tests/Microsoft.Bcl.Cryptography.Tests.csproj @@ -0,0 +1,22 @@ + + + true + $(NetCoreAppCurrent);$(NetFrameworkMinimum) + + + + + + + + + + + + + + diff --git a/src/libraries/System.Security.Cryptography/ref/System.Security.Cryptography.cs b/src/libraries/System.Security.Cryptography/ref/System.Security.Cryptography.cs index 41682a4330e..914f180f5df 100644 --- a/src/libraries/System.Security.Cryptography/ref/System.Security.Cryptography.cs +++ b/src/libraries/System.Security.Cryptography/ref/System.Security.Cryptography.cs @@ -2340,6 +2340,24 @@ namespace System.Security.Cryptography [System.Diagnostics.CodeAnalysis.RequiresUnreferencedCodeAttribute("CreateFormatter is not trim compatible because the algorithm implementation referenced by FormatterAlgorithm might be removed.")] public virtual System.Security.Cryptography.AsymmetricSignatureFormatter CreateFormatter(System.Security.Cryptography.AsymmetricAlgorithm key) { throw null; } } + public sealed partial class SP800108HmacCounterKdf : System.IDisposable + { + public SP800108HmacCounterKdf(byte[] key, System.Security.Cryptography.HashAlgorithmName hashAlgorithm) { } + public SP800108HmacCounterKdf(System.ReadOnlySpan key, System.Security.Cryptography.HashAlgorithmName hashAlgorithm) { } + public static byte[] DeriveBytes(byte[] key, System.Security.Cryptography.HashAlgorithmName hashAlgorithm, byte[] label, byte[] context, int derivedKeyLengthInBytes) { throw null; } + public static byte[] DeriveBytes(byte[] key, System.Security.Cryptography.HashAlgorithmName hashAlgorithm, string label, string context, int derivedKeyLengthInBytes) { throw null; } + public static byte[] DeriveBytes(System.ReadOnlySpan key, System.Security.Cryptography.HashAlgorithmName hashAlgorithm, System.ReadOnlySpan label, System.ReadOnlySpan context, int derivedKeyLengthInBytes) { throw null; } + public static void DeriveBytes(System.ReadOnlySpan key, System.Security.Cryptography.HashAlgorithmName hashAlgorithm, System.ReadOnlySpan label, System.ReadOnlySpan context, System.Span destination) { } + public static byte[] DeriveBytes(System.ReadOnlySpan key, System.Security.Cryptography.HashAlgorithmName hashAlgorithm, System.ReadOnlySpan label, System.ReadOnlySpan context, int derivedKeyLengthInBytes) { throw null; } + public static void DeriveBytes(System.ReadOnlySpan key, System.Security.Cryptography.HashAlgorithmName hashAlgorithm, System.ReadOnlySpan label, System.ReadOnlySpan context, System.Span destination) { } + public byte[] DeriveKey(byte[] label, byte[] context, int derivedKeyLengthInBytes) { throw null; } + public byte[] DeriveKey(System.ReadOnlySpan label, System.ReadOnlySpan context, int derivedKeyLengthInBytes) { throw null; } + public void DeriveKey(System.ReadOnlySpan label, System.ReadOnlySpan context, System.Span destination) { } + public byte[] DeriveKey(System.ReadOnlySpan label, System.ReadOnlySpan context, int derivedKeyLengthInBytes) { throw null; } + public void DeriveKey(System.ReadOnlySpan label, System.ReadOnlySpan context, System.Span destination) { } + public byte[] DeriveKey(string label, string context, int derivedKeyLengthInBytes) { throw null; } + public void Dispose() { } + } public abstract partial class SymmetricAlgorithm : System.IDisposable { protected int BlockSizeValue; diff --git a/src/libraries/System.Security.Cryptography/src/Resources/Strings.resx b/src/libraries/System.Security.Cryptography/src/Resources/Strings.resx index 6ce867ef2d1..f31af3b55e7 100644 --- a/src/libraries/System.Security.Cryptography/src/Resources/Strings.resx +++ b/src/libraries/System.Security.Cryptography/src/Resources/Strings.resx @@ -93,6 +93,9 @@ Destination is too short. + + The value cannot be an empty string. + The method cannot be called with an invalid or closed SafeHandle. @@ -159,6 +162,9 @@ Index was out of range. Must be non-negative and less than or equal to the size of the collection. + + The number of bytes requested is too large. The number of bytes produced by SP800108HmacCounterKdf cannot exceed 536,870,911 bytes. + Non-negative number required. diff --git a/src/libraries/System.Security.Cryptography/src/System.Security.Cryptography.csproj b/src/libraries/System.Security.Cryptography/src/System.Security.Cryptography.csproj index aa1ef1c7568..af007a24ece 100644 --- a/src/libraries/System.Security.Cryptography/src/System.Security.Cryptography.csproj +++ b/src/libraries/System.Security.Cryptography/src/System.Security.Cryptography.csproj @@ -261,6 +261,12 @@ Link="Common\System\Security\Cryptography\RSAKeyFormatHelper.Encrypted.cs" /> + + + + @@ -583,6 +591,8 @@ + + @@ -719,6 +729,8 @@ Link="Common\System\Security\Cryptography\ECOpenSsl.ImportExport.cs" /> + Common\System\Security\Cryptography\Asn1\DigestInfoAsn.xml @@ -821,6 +833,8 @@ + + @@ -937,6 +951,8 @@ Link="Common\System\Security\Cryptography\ECDsaAndroid.cs" /> + Common\System\Security\Cryptography\Asn1\DigestInfoAsn.xml @@ -1027,6 +1043,8 @@ + + @@ -1172,6 +1190,8 @@ Link="Common\System\Security\Cryptography\ECDsaSecurityTransforms.cs" /> + @@ -1200,6 +1220,8 @@ + + @@ -1721,6 +1743,10 @@ Link="Common\System\Security\Cryptography\RSACng.ImportExport.cs" /> + + @@ -1797,6 +1823,9 @@ + + + diff --git a/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/SP800108HmacCounterKdf.Managed.cs b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/SP800108HmacCounterKdf.Managed.cs new file mode 100644 index 00000000000..bcef2ff6466 --- /dev/null +++ b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/SP800108HmacCounterKdf.Managed.cs @@ -0,0 +1,54 @@ +// 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 System.Runtime.Versioning; +using System.Text; + +namespace System.Security.Cryptography +{ + public sealed partial class SP800108HmacCounterKdf : IDisposable + { + private static partial SP800108HmacCounterKdfImplementationBase CreateImplementation( + ReadOnlySpan key, + HashAlgorithmName hashAlgorithm) + { + return new SP800108HmacCounterKdfImplementationManaged(key, hashAlgorithm); + } + + private static partial byte[] DeriveBytesCore( + byte[] key, + HashAlgorithmName hashAlgorithm, + byte[] label, + byte[] context, + int derivedKeyLengthInBytes) + { + byte[] result = new byte[derivedKeyLengthInBytes]; + + SP800108HmacCounterKdfImplementationManaged.DeriveBytesOneShot(key, hashAlgorithm, label, context, result); + + return result; + } + + private static partial void DeriveBytesCore( + ReadOnlySpan key, + HashAlgorithmName hashAlgorithm, + ReadOnlySpan label, + ReadOnlySpan context, + Span destination) + { + SP800108HmacCounterKdfImplementationManaged.DeriveBytesOneShot(key, hashAlgorithm, label, context, destination); + } + + private static partial void DeriveBytesCore( + ReadOnlySpan key, + HashAlgorithmName hashAlgorithm, + ReadOnlySpan label, + ReadOnlySpan context, + Span destination) + { + SP800108HmacCounterKdfImplementationManaged.DeriveBytesOneShot(key, hashAlgorithm, label, context, destination); + } + } +} diff --git a/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/SP800108HmacCounterKdf.Windows.cs b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/SP800108HmacCounterKdf.Windows.cs new file mode 100644 index 00000000000..9ad1afc5f10 --- /dev/null +++ b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/SP800108HmacCounterKdf.Windows.cs @@ -0,0 +1,84 @@ +// 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 System.Runtime.Versioning; +using System.Text; + +namespace System.Security.Cryptography +{ + public sealed partial class SP800108HmacCounterKdf : IDisposable + { + private static readonly bool s_isWindows8OrGreater = OperatingSystem.IsWindowsVersionAtLeast(6, 2); + + private static partial SP800108HmacCounterKdfImplementationBase CreateImplementation( + ReadOnlySpan key, + HashAlgorithmName hashAlgorithm) + { + if (s_isWindows8OrGreater) + { + return new SP800108HmacCounterKdfImplementationCng(key, hashAlgorithm); + } + else + { + return new SP800108HmacCounterKdfImplementationManaged(key, hashAlgorithm); + } + } + + private static partial byte[] DeriveBytesCore( + byte[] key, + HashAlgorithmName hashAlgorithm, + byte[] label, + byte[] context, + int derivedKeyLengthInBytes) + { + byte[] result = new byte[derivedKeyLengthInBytes]; + + if (s_isWindows8OrGreater) + { + SP800108HmacCounterKdfImplementationCng.DeriveBytesOneShot(key, hashAlgorithm, label, context, result); + } + else + { + SP800108HmacCounterKdfImplementationManaged.DeriveBytesOneShot(key, hashAlgorithm, label, context, result); + } + + return result; + } + + private static partial void DeriveBytesCore( + ReadOnlySpan key, + HashAlgorithmName hashAlgorithm, + ReadOnlySpan label, + ReadOnlySpan context, + Span destination) + { + if (s_isWindows8OrGreater) + { + SP800108HmacCounterKdfImplementationCng.DeriveBytesOneShot(key, hashAlgorithm, label, context, destination); + } + else + { + SP800108HmacCounterKdfImplementationManaged.DeriveBytesOneShot(key, hashAlgorithm, label, context, destination); + } + } + + private static partial void DeriveBytesCore( + ReadOnlySpan key, + HashAlgorithmName hashAlgorithm, + ReadOnlySpan label, + ReadOnlySpan context, + Span destination) + { + if (s_isWindows8OrGreater) + { + SP800108HmacCounterKdfImplementationCng.DeriveBytesOneShot(key, hashAlgorithm, label, context, destination); + } + else + { + SP800108HmacCounterKdfImplementationManaged.DeriveBytesOneShot(key, hashAlgorithm, label, context, destination); + } + } + } +} diff --git a/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/SP800108HmacCounterKdfImplementationCng.cs b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/SP800108HmacCounterKdfImplementationCng.cs new file mode 100644 index 00000000000..75025e598ff --- /dev/null +++ b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/SP800108HmacCounterKdfImplementationCng.cs @@ -0,0 +1,74 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Diagnostics; + +namespace System.Security.Cryptography +{ + internal sealed partial class SP800108HmacCounterKdfImplementationCng + { + internal unsafe SP800108HmacCounterKdfImplementationCng(ReadOnlySpan key, HashAlgorithmName hashAlgorithm) + { + Debug.Assert(hashAlgorithm.Name is not null); + + scoped ReadOnlySpan symmetricKeyMaterial; + scoped Span clearSpan = default; + int symmetricKeyMaterialLength; + int hashAlgorithmBlockSize = GetHashBlockSize(hashAlgorithm.Name); + + if (key.Length > hashAlgorithmBlockSize) + { + Span buffer = stackalloc byte[512 / 8]; // Largest supported digest is SHA512. + symmetricKeyMaterialLength = HashOneShot(hashAlgorithm, key, buffer); + clearSpan = buffer.Slice(0, symmetricKeyMaterialLength); + symmetricKeyMaterial = clearSpan; + } + else if (!key.IsEmpty) + { + symmetricKeyMaterial = key; + symmetricKeyMaterialLength = key.Length; + } + else + { + // CNG requires a non-null pointer even when the length is zero. + symmetricKeyMaterial = stackalloc byte[] { 0 }; + symmetricKeyMaterialLength = 0; + } + + try + { + fixed (byte* pSymmetricKeyMaterial = symmetricKeyMaterial) + { + _keyHandle = CreateSymmetricKey(pSymmetricKeyMaterial, symmetricKeyMaterialLength); + } + } + finally + { + CryptographicOperations.ZeroMemory(clearSpan); + } + + _hashAlgorithm = hashAlgorithm; + } + + [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Security", "CA5350", Justification = "Weak algorithms are used as instructed by the caller")] + private static int HashOneShot(HashAlgorithmName hashAlgorithm, ReadOnlySpan data, Span destination) + { + Debug.Assert(hashAlgorithm.Name is not null); + + switch (hashAlgorithm.Name) + { + case HashAlgorithmNames.SHA1: + return SHA1.HashData(data, destination); + case HashAlgorithmNames.SHA256: + return SHA256.HashData(data, destination); + case HashAlgorithmNames.SHA384: + return SHA384.HashData(data, destination); + case HashAlgorithmNames.SHA512: + return SHA512.HashData(data, destination); + default: + Debug.Fail($"Unexpected hash algorithm '{hashAlgorithm.Name}'"); + throw new CryptographicException(); + } + } + } +} diff --git a/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/SP800108HmacCounterKdfImplementationManaged.cs b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/SP800108HmacCounterKdfImplementationManaged.cs new file mode 100644 index 00000000000..017bcdaf07f --- /dev/null +++ b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/SP800108HmacCounterKdfImplementationManaged.cs @@ -0,0 +1,99 @@ +// 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; + +namespace System.Security.Cryptography +{ + internal sealed partial class SP800108HmacCounterKdfImplementationManaged + { + private const int CharToBytesStackBufferSize = 256; + + public SP800108HmacCounterKdfImplementationManaged(ReadOnlySpan key, HashAlgorithmName hashAlgorithm) + { + // Use the POH if we can so the key doesn't get moved around by the GC. + _key = GC.AllocateArray(key.Length, pinned: true); + key.CopyTo(_key); + _hashAlgorithm = hashAlgorithm; + } + + internal static void DeriveBytesOneShot( + ReadOnlySpan key, + HashAlgorithmName hashAlgorithm, + ReadOnlySpan label, + ReadOnlySpan context, + Span destination) + { + if (destination.Length == 0) + { + return; + } + + Debug.Assert(destination.Length <= 0x1FFFFFFF); + + // Do everything as checked. Over/underflows are never expected. + checked + { + // The KDF is defined as K(i) := PRF (KI, [i]2 || Label || 0x00 || Context || [L]2) + // We know L is already less than 0x1FFFFFFF. h = ceil(L / h) where H is the hash length in bits. + // So we don't expect i to overflow. + using (IncrementalHash hash = IncrementalHash.CreateHMAC(hashAlgorithm, key)) + { + Span iBuffer = stackalloc byte[sizeof(uint)]; + Span lBuffer = stackalloc byte[sizeof(uint)]; + ReadOnlySpan zero = stackalloc byte[] { 0 }; + Span hmacBuffer = stackalloc byte[512 / 8]; // Largest HMAC supported is SHA512 + int hmacBufferWritten = 0; + + BinaryPrimitives.WriteUInt32BigEndian(lBuffer, (uint)destination.Length * 8U); + + for (uint i = 1; !destination.IsEmpty; i++) + { + BinaryPrimitives.WriteUInt32BigEndian(iBuffer, i); + hash.AppendData(iBuffer); + hash.AppendData(label); + hash.AppendData(zero); + hash.AppendData(context); + hash.AppendData(lBuffer); + + if (destination.Length >= hash.HashLengthInBytes) + { + int written = hash.GetHashAndReset(destination); + destination = destination.Slice(written); + } + else + { + hmacBufferWritten = hash.GetHashAndReset(hmacBuffer); + Debug.Assert(hmacBufferWritten > destination.Length); + hmacBuffer.Slice(0, destination.Length).CopyTo(destination); + destination = default; + } + } + + // Get derived key material off the stack, if any. + CryptographicOperations.ZeroMemory(hmacBuffer.Slice(0, hmacBufferWritten)); + } + } + } + + internal static void DeriveBytesOneShot( + ReadOnlySpan key, + HashAlgorithmName hashAlgorithm, + ReadOnlySpan label, + ReadOnlySpan context, + Span 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); + } + } + } +} diff --git a/src/libraries/System.Security.Cryptography/tests/System.Security.Cryptography.Tests.csproj b/src/libraries/System.Security.Cryptography/tests/System.Security.Cryptography.Tests.csproj index 3165257e702..bfbb59022d5 100644 --- a/src/libraries/System.Security.Cryptography/tests/System.Security.Cryptography.Tests.csproj +++ b/src/libraries/System.Security.Cryptography/tests/System.Security.Cryptography.Tests.csproj @@ -39,6 +39,14 @@ Link="ProductionCode\Common\System\Net\MultiArrayBuffer.cs" /> + + + +