diff --git a/src/libraries/Common/src/Interop/Windows/BCrypt/Cng.cs b/src/libraries/Common/src/Interop/Windows/BCrypt/Cng.cs index 68738f67a1323b..6d8324b7eef531 100644 --- a/src/libraries/Common/src/Interop/Windows/BCrypt/Cng.cs +++ b/src/libraries/Common/src/Interop/Windows/BCrypt/Cng.cs @@ -29,10 +29,11 @@ internal static class AlgorithmName public const string ECDsaP256 = "ECDSA_P256"; // BCRYPT_ECDSA_P256_ALGORITHM public const string ECDsaP384 = "ECDSA_P384"; // BCRYPT_ECDSA_P384_ALGORITHM public const string ECDsaP521 = "ECDSA_P521"; // BCRYPT_ECDSA_P521_ALGORITHM - public const string RSA = "RSA"; // BCRYPT_RSA_ALGORITHM + public const string HKDF = "HKDF"; // BCRYPT_HKDF_ALGORITHM public const string MD5 = "MD5"; // BCRYPT_MD5_ALGORITHM public const string MLDsa = "ML-DSA"; // BCRYPT_MLDSA_ALGORITHM public const string MLKem = "ML-KEM"; // BCRYPT_MLKEM_ALGORITHM + public const string RSA = "RSA"; // BCRYPT_RSA_ALGORITHM public const string Sha1 = "SHA1"; // BCRYPT_SHA1_ALGORITHM public const string Sha256 = "SHA256"; // BCRYPT_SHA256_ALGORITHM public const string Sha384 = "SHA384"; // BCRYPT_SHA384_ALGORITHM 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 1fd6bd17669038..aa58b261e6ab84 100644 --- a/src/libraries/Common/src/Interop/Windows/BCrypt/Interop.BCryptAlgPseudoHandle.cs +++ b/src/libraries/Common/src/Interop/Windows/BCrypt/Interop.BCryptAlgPseudoHandle.cs @@ -23,6 +23,7 @@ public enum BCryptAlgPseudoHandle : uint BCRYPT_HMAC_SHA384_ALG_HANDLE = 0x000000c1, BCRYPT_HMAC_SHA512_ALG_HANDLE = 0x000000d1, BCRYPT_PBKDF2_ALG_HANDLE = 0x00000331, + BCRYPT_HKDF_ALG_HANDLE = 0x00000391, BCRYPT_SHA3_256_ALG_HANDLE = 0x000003B1, BCRYPT_SHA3_384_ALG_HANDLE = 0x000003C1, BCRYPT_SHA3_512_ALG_HANDLE = 0x000003D1, diff --git a/src/libraries/Common/src/Interop/Windows/BCrypt/Interop.Blobs.cs b/src/libraries/Common/src/Interop/Windows/BCrypt/Interop.Blobs.cs index f919daf562998b..42289566281153 100644 --- a/src/libraries/Common/src/Interop/Windows/BCrypt/Interop.Blobs.cs +++ b/src/libraries/Common/src/Interop/Windows/BCrypt/Interop.Blobs.cs @@ -302,6 +302,7 @@ internal enum CngBufferDescriptors : int KDF_CONTEXT = 14, KDF_SALT = 15, KDF_ITERATION_COUNT = 16, + KDF_HKDF_INFO = 20, NCRYPTBUFFER_ECC_CURVE_NAME = 60, } 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 31fab862e9ce7c..3ef51bb89ca051 100644 --- a/src/libraries/System.Security.Cryptography/src/System.Security.Cryptography.csproj +++ b/src/libraries/System.Security.Cryptography/src/System.Security.Cryptography.csproj @@ -1988,7 +1988,7 @@ - + diff --git a/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/HKDF.Windows.cs b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/HKDF.Windows.cs new file mode 100644 index 00000000000000..60544defa60436 --- /dev/null +++ b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/HKDF.Windows.cs @@ -0,0 +1,239 @@ +// 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.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using Internal.Cryptography; +using Microsoft.Win32.SafeHandles; + +using BCryptAlgPseudoHandle = Interop.BCrypt.BCryptAlgPseudoHandle; +using BCryptBuffer = Interop.BCrypt.BCryptBuffer; +using BCryptBufferDesc = Interop.BCrypt.BCryptBufferDesc; +using BCryptOpenAlgorithmProviderFlags = Interop.BCrypt.BCryptOpenAlgorithmProviderFlags; +using BCRYPT_KEY_DATA_BLOB_HEADER = Interop.BCrypt.BCRYPT_KEY_DATA_BLOB_HEADER; +using CngBufferDescriptors = Interop.BCrypt.CngBufferDescriptors; +using NTSTATUS = Interop.BCrypt.NTSTATUS; + +namespace System.Security.Cryptography +{ + public static partial class HKDF + { + private static readonly bool s_hasCngImplementation = IsCngSupported(); + private const string BCRYPT_HKDF_SALT_AND_FINALIZE = "HkdfSaltAndFinalize"; + private const string BCRYPT_HKDF_PRK_AND_FINALIZE = "HkdfPrkAndFinalize"; + private const string BCRYPT_HKDF_HASH_ALGORITHM = "HkdfHashAlgorithm"; + + private static void ExtractCore( + HashAlgorithmName hashAlgorithmName, + ReadOnlySpan ikm, + ReadOnlySpan salt, + Span prk) + { + // Windows does not clearly have a way to perform just the Extract step from HKDF. We'll use managed for now. + HKDFManagedImplementation.Extract(hashAlgorithmName, ikm, salt, prk); + } + + private static void Expand( + HashAlgorithmName hashAlgorithmName, + int hashLength, + ReadOnlySpan prk, + Span output, + ReadOnlySpan info) + { + if (s_hasCngImplementation && !IsAlgorithmRequiringManagedFallback(hashAlgorithmName)) + { + CngDeriveKey( + hashAlgorithmName, + prk, + info, + salt: default, + output, + secretIsIkm: false); + } + else + { + HKDFManagedImplementation.Expand(hashAlgorithmName, hashLength, prk, output, info); + } + } + + private static void DeriveKeyCore( + HashAlgorithmName hashAlgorithmName, + int hashLength, + ReadOnlySpan ikm, + Span output, + ReadOnlySpan salt, + ReadOnlySpan info) + { + if (s_hasCngImplementation && !IsAlgorithmRequiringManagedFallback(hashAlgorithmName)) + { + CngDeriveKey( + hashAlgorithmName, + ikm, + info, + salt, + output, + secretIsIkm: true); + } + else + { + HKDFManagedImplementation.DeriveKey(hashAlgorithmName, hashLength, ikm, output, salt, info); + } + } + + private static bool IsAlgorithmRequiringManagedFallback(HashAlgorithmName hashAlgorithmName) + { + return hashAlgorithmName == HashAlgorithmName.MD5; + } + + private static void ThrowIfAlgorithmNotSupported(HashAlgorithmName hashAlgorithmName) + { + if ((hashAlgorithmName == HashAlgorithmName.SHA3_256 && !SHA3_256.IsSupported) || + (hashAlgorithmName == HashAlgorithmName.SHA3_384 && !SHA3_384.IsSupported) || + (hashAlgorithmName == HashAlgorithmName.SHA3_512 && !SHA3_512.IsSupported)) + { + throw new PlatformNotSupportedException(); + } + } + + private static bool IsCngSupported() + { + NTSTATUS openStatus = Interop.BCrypt.BCryptOpenAlgorithmProvider( + out SafeBCryptAlgorithmHandle handle, + Internal.NativeCrypto.BCryptNative.AlgorithmName.HKDF, + null, + BCryptOpenAlgorithmProviderFlags.None); + + handle.Dispose(); + + // HKDF was added in Windows 10 1803. + Debug.Assert(!OperatingSystem.IsWindowsVersionAtLeast(10, 0, 17134) || openStatus == NTSTATUS.STATUS_SUCCESS); + return openStatus == NTSTATUS.STATUS_SUCCESS; + } + + private static unsafe void CngDeriveKey( + HashAlgorithmName hashAlgorithmName, + ReadOnlySpan secret, + ReadOnlySpan info, + ReadOnlySpan salt, + Span destination, + bool secretIsIkm) + { + Debug.Assert(Interop.BCrypt.PseudoHandlesSupported); + Debug.Assert(hashAlgorithmName.Name is not null); + + ThrowIfAlgorithmNotSupported(hashAlgorithmName); + + byte[]? rented; + ReadOnlySpan infoBlob; + + if (destination.Overlaps(info)) + { + rented = CryptoPool.Rent(info.Length); + info.CopyTo(rented); + infoBlob = rented.AsSpan(0, info.Length); + } + else + { + rented = null; + infoBlob = info; + } + + SafeBCryptKeyHandle? keyHandle = null; + NTSTATUS status; + + try + { + fixed (byte* pSecret = &Helpers.GetNonNullPinnableReference(secret)) + { + status = Interop.BCrypt.BCryptGenerateSymmetricKey( + (nuint)BCryptAlgPseudoHandle.BCRYPT_HKDF_ALG_HANDLE, + out keyHandle, + pbKeyObject: IntPtr.Zero, + cbKeyObject: 0, + pSecret, + secret.Length, + dwFlags: 0); + + if (status != NTSTATUS.STATUS_SUCCESS) + { + throw Interop.BCrypt.CreateCryptographicException(status); + } + + Interop.BCrypt.BCryptSetSZProperty(keyHandle, BCRYPT_HKDF_HASH_ALGORITHM, hashAlgorithmName.Name); + + if (secretIsIkm) + { + fixed (byte* pSalt = &Helpers.GetNonNullPinnableReference(salt)) + { + status = Interop.BCrypt.BCryptSetProperty( + keyHandle, + BCRYPT_HKDF_SALT_AND_FINALIZE, + pSalt, + (uint)salt.Length, + dwFlags: 0); + } + } + else + { + Debug.Assert(salt.IsEmpty); + status = Interop.BCrypt.BCryptSetProperty( + keyHandle, + BCRYPT_HKDF_PRK_AND_FINALIZE, + null, + 0U, + dwFlags: 0); + } + + if (status != NTSTATUS.STATUS_SUCCESS) + { + throw Interop.BCrypt.CreateCryptographicException(status); + } + } + + fixed (byte* pDestination = destination) + fixed (byte* pInfoBlob = &Helpers.GetNonNullPinnableReference(infoBlob)) + { + BCryptBuffer infoBuffer = default; + infoBuffer.cbBuffer = infoBlob.Length; + infoBuffer.BufferType = CngBufferDescriptors.KDF_HKDF_INFO; + infoBuffer.pvBuffer = (IntPtr)pInfoBlob; + + BCryptBufferDesc bufferDesc = default; + bufferDesc.ulVersion = Interop.BCrypt.BCRYPTBUFFER_VERSION; + bufferDesc.cBuffers = 1; + bufferDesc.pBuffers = (IntPtr)(&infoBuffer); + + status = Interop.BCrypt.BCryptKeyDerivation( + keyHandle, + &bufferDesc, + pDestination, + destination.Length, + out uint resultLength, + dwFlags: 0); + + if (status != NTSTATUS.STATUS_SUCCESS) + { + throw Interop.BCrypt.CreateCryptographicException(status); + } + + if (destination.Length != resultLength) + { + Debug.Fail("HKDF resultLength != destination.Length"); + throw new CryptographicException(); + } + } + } + finally + { + if (rented is not null) + { + CryptoPool.Return(rented, clearSize: 0); // Info is not considered secret. + } + + keyHandle?.Dispose(); + } + } + } +}