From 996ce3b8cba17dbbc1d4fdb899c0bae23176ee66 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Wed, 28 Aug 2024 17:50:21 -0700 Subject: [PATCH] [release/9.0] Use OpenSSL 3's HKDF if it is available (#107085) * Split out managed implementation * Misc. cleanup * Introduce a PAL * Start a Unix implementation * Get DeriveKey working with OpenSSL * Extract and Expand individual calls * Fix build * Fix tests on Azure Linux * Use defined consts rather than magic strings --------- Co-authored-by: Kevin Jones --- .../Interop.EVP.Kdf.cs | 113 +++++++++++ .../Interop.EVP.KdfAlgs.cs | 3 + .../src/System.Security.Cryptography.csproj | 6 + .../Security/Cryptography/HKDF.Managed.cs | 39 ++++ .../Security/Cryptography/HKDF.OpenSsl.cs | 79 ++++++++ .../src/System/Security/Cryptography/HKDF.cs | 180 +++--------------- .../Cryptography/HKDFManagedImplementation.cs | 101 ++++++++++ .../System/Security/Cryptography/Helpers.cs | 40 ++++ .../tests/HKDFTests.cs | 4 +- .../entrypoints.c | 3 + .../opensslshim.h | 20 ++ .../osslcompat_30.h | 2 + .../pal_evp_kdf.c | 162 ++++++++++++++++ .../pal_evp_kdf.h | 32 ++++ 14 files changed, 633 insertions(+), 151 deletions(-) create mode 100644 src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/HKDF.Managed.cs create mode 100644 src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/HKDF.OpenSsl.cs create mode 100644 src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/HKDFManagedImplementation.cs diff --git a/src/libraries/Common/src/Interop/Unix/System.Security.Cryptography.Native/Interop.EVP.Kdf.cs b/src/libraries/Common/src/Interop/Unix/System.Security.Cryptography.Native/Interop.EVP.Kdf.cs index 082d496497289f..f3c971a1a2f956 100644 --- a/src/libraries/Common/src/Interop/Unix/System.Security.Cryptography.Native/Interop.EVP.Kdf.cs +++ b/src/libraries/Common/src/Interop/Unix/System.Security.Cryptography.Native/Interop.EVP.Kdf.cs @@ -13,6 +13,41 @@ internal static partial class Crypto [LibraryImport(Libraries.CryptoNative, EntryPoint = "CryptoNative_EvpKdfFree")] internal static partial void EvpKdfFree(IntPtr kdf); + [LibraryImport(Libraries.CryptoNative, EntryPoint = "CryptoNative_HkdfDeriveKey", StringMarshalling = StringMarshalling.Utf8)] + private static partial int CryptoNative_HkdfDeriveKey( + SafeEvpKdfHandle kdf, + ReadOnlySpan ikm, + int ikmLength, + string algorithm, + ReadOnlySpan salt, + int saltLength, + ReadOnlySpan info, + int infoLength, + Span destination, + int destinationLength); + + [LibraryImport(Libraries.CryptoNative, EntryPoint = "CryptoNative_HkdfExpand", StringMarshalling = StringMarshalling.Utf8)] + private static partial int CryptoNative_HkdfExpand( + SafeEvpKdfHandle kdf, + ReadOnlySpan prk, + int prkLength, + string algorithm, + ReadOnlySpan info, + int infoLength, + Span destination, + int destinationLength); + + [LibraryImport(Libraries.CryptoNative, EntryPoint = "CryptoNative_HkdfExtract", StringMarshalling = StringMarshalling.Utf8)] + private static partial int CryptoNative_HkdfExtract( + SafeEvpKdfHandle kdf, + ReadOnlySpan ikm, + int ikmLength, + string algorithm, + ReadOnlySpan salt, + int saltLength, + Span destination, + int destinationLength); + [LibraryImport(Libraries.CryptoNative, EntryPoint = "CryptoNative_KbkdfHmacOneShot", StringMarshalling = StringMarshalling.Utf8)] private static unsafe partial int CryptoNative_KbkdfHmacOneShot( SafeEvpKdfHandle kdf, @@ -26,6 +61,84 @@ private static unsafe partial int CryptoNative_KbkdfHmacOneShot( Span destination, int destinationLength); + internal static void HkdfDeriveKey( + SafeEvpKdfHandle kdf, + ReadOnlySpan ikm, + string algorithm, + ReadOnlySpan salt, + ReadOnlySpan info, + Span destination) + { + const int Success = 1; + int ret = CryptoNative_HkdfDeriveKey( + kdf, + ikm, + ikm.Length, + algorithm, + salt, + salt.Length, + info, + info.Length, + destination, + destination.Length); + + if (ret != Success) + { + Debug.Assert(ret == 0); + throw CreateOpenSslCryptographicException(); + } + } + + internal static void HkdfExpand( + SafeEvpKdfHandle kdf, + ReadOnlySpan prk, + string algorithm, + ReadOnlySpan info, + Span destination) + { + const int Success = 1; + int ret = CryptoNative_HkdfExpand( + kdf, + prk, + prk.Length, + algorithm, + info, + info.Length, + destination, + destination.Length); + + if (ret != Success) + { + Debug.Assert(ret == 0); + throw CreateOpenSslCryptographicException(); + } + } + + internal static void HkdfExtract( + SafeEvpKdfHandle kdf, + ReadOnlySpan ikm, + string algorithm, + ReadOnlySpan salt, + Span destination) + { + const int Success = 1; + int ret = CryptoNative_HkdfExtract( + kdf, + ikm, + ikm.Length, + algorithm, + salt, + salt.Length, + destination, + destination.Length); + + if (ret != Success) + { + Debug.Assert(ret == 0); + throw CreateOpenSslCryptographicException(); + } + } + internal static void KbkdfHmacOneShot( SafeEvpKdfHandle kdf, ReadOnlySpan key, diff --git a/src/libraries/Common/src/Interop/Unix/System.Security.Cryptography.Native/Interop.EVP.KdfAlgs.cs b/src/libraries/Common/src/Interop/Unix/System.Security.Cryptography.Native/Interop.EVP.KdfAlgs.cs index 0b4217e30d7910..1cd7598a9421f5 100644 --- a/src/libraries/Common/src/Interop/Unix/System.Security.Cryptography.Native/Interop.EVP.KdfAlgs.cs +++ b/src/libraries/Common/src/Interop/Unix/System.Security.Cryptography.Native/Interop.EVP.KdfAlgs.cs @@ -13,8 +13,10 @@ internal static partial class Crypto internal static partial class EvpKdfAlgs { private const string KbkdfAlgorithmName = "KBKDF"; + private const string HkdfAlgorithmName = "HKDF"; internal static SafeEvpKdfHandle? Kbkdf { get; } + internal static SafeEvpKdfHandle? Hkdf { get; } static EvpKdfAlgs() { @@ -24,6 +26,7 @@ static EvpKdfAlgs() // is called first. Property initializers happen before cctors, so instead set the property after the // initializer is run. Kbkdf = EvpKdfFetch(KbkdfAlgorithmName); + Hkdf = EvpKdfFetch(HkdfAlgorithmName); } [LibraryImport(Libraries.CryptoNative, EntryPoint = "CryptoNative_EvpKdfFetch", StringMarshalling = StringMarshalling.Utf8)] 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 ec6f682444aa29..c609292d4059f6 100644 --- a/src/libraries/System.Security.Cryptography/src/System.Security.Cryptography.csproj +++ b/src/libraries/System.Security.Cryptography/src/System.Security.Cryptography.csproj @@ -418,6 +418,7 @@ + @@ -668,6 +669,7 @@ + @@ -864,6 +866,7 @@ + @@ -1024,6 +1027,7 @@ + @@ -1154,6 +1158,7 @@ + @@ -1745,6 +1750,7 @@ + diff --git a/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/HKDF.Managed.cs b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/HKDF.Managed.cs new file mode 100644 index 00000000000000..e3af79fd8d41e9 --- /dev/null +++ b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/HKDF.Managed.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. + +namespace System.Security.Cryptography +{ + public static partial class HKDF + { + private static void Extract( + HashAlgorithmName hashAlgorithmName, + int hashLength, + ReadOnlySpan ikm, + ReadOnlySpan salt, + Span prk) + { + HKDFManagedImplementation.Extract(hashAlgorithmName, hashLength, ikm, salt, prk); + } + + private static void Expand( + HashAlgorithmName hashAlgorithmName, + int hashLength, + ReadOnlySpan prk, + Span output, + ReadOnlySpan info) + { + HKDFManagedImplementation.Expand(hashAlgorithmName, hashLength, prk, output, info); + } + + private static void DeriveKeyCore( + HashAlgorithmName hashAlgorithmName, + int hashLength, + ReadOnlySpan ikm, + Span output, + ReadOnlySpan salt, + ReadOnlySpan info) + { + HKDFManagedImplementation.DeriveKey(hashAlgorithmName, hashLength, ikm, output, salt, info); + } + } +} diff --git a/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/HKDF.OpenSsl.cs b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/HKDF.OpenSsl.cs new file mode 100644 index 00000000000000..9194f144be76d3 --- /dev/null +++ b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/HKDF.OpenSsl.cs @@ -0,0 +1,79 @@ +// 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 +{ + public static partial class HKDF + { + private static readonly bool s_hasOpenSslImplementation = Interop.Crypto.EvpKdfAlgs.Hkdf is not null; + + private static void Extract( + HashAlgorithmName hashAlgorithmName, + int hashLength, + ReadOnlySpan ikm, + ReadOnlySpan salt, + Span prk) + { + if (s_hasOpenSslImplementation) + { + Debug.Assert(Interop.Crypto.EvpKdfAlgs.Hkdf is not null); + Debug.Assert(hashAlgorithmName.Name is not null); + + Interop.Crypto.HkdfExtract(Interop.Crypto.EvpKdfAlgs.Hkdf, ikm, hashAlgorithmName.Name, salt, prk); + } + else + { + HKDFManagedImplementation.Extract(hashAlgorithmName, hashLength, ikm, salt, prk); + } + } + + private static void Expand( + HashAlgorithmName hashAlgorithmName, + int hashLength, + ReadOnlySpan prk, + Span output, + ReadOnlySpan info) + { + if (s_hasOpenSslImplementation) + { + Debug.Assert(Interop.Crypto.EvpKdfAlgs.Hkdf is not null); + Debug.Assert(hashAlgorithmName.Name is not null); + + Interop.Crypto.HkdfExpand(Interop.Crypto.EvpKdfAlgs.Hkdf, prk, hashAlgorithmName.Name, info, output); + } + 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_hasOpenSslImplementation) + { + Debug.Assert(Interop.Crypto.EvpKdfAlgs.Hkdf is not null); + Debug.Assert(hashAlgorithmName.Name is not null); + + Interop.Crypto.HkdfDeriveKey( + Interop.Crypto.EvpKdfAlgs.Hkdf, + ikm, + hashAlgorithmName.Name, + salt, + info, + output); + } + else + { + HKDFManagedImplementation.DeriveKey(hashAlgorithmName, hashLength, ikm, output, salt, info); + } + } + } +} diff --git a/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/HKDF.cs b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/HKDF.cs index 9b33aea297a77c..f167f18afc2466 100644 --- a/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/HKDF.cs +++ b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/HKDF.cs @@ -15,7 +15,7 @@ namespace System.Security.Cryptography /// phase to be skipped, and the master key to be used directly as the pseudorandom key. /// See RFC5869 for more information. /// - public static class HKDF + public static partial class HKDF { /// /// Performs the HKDF-Extract function. @@ -23,13 +23,16 @@ public static class HKDF /// /// The hash algorithm used for HMAC operations. /// The input keying material. - /// The optional salt value (a non-secret random value). If not provided it defaults to a byte array of zeros. + /// + /// The optional salt value (a non-secret random value). If not provided it defaults to a + /// byte array of the same length as the output of the specified hash algorithm. + /// /// The pseudo random key (prk). public static byte[] Extract(HashAlgorithmName hashAlgorithmName, byte[] ikm, byte[]? salt = null) { ArgumentNullException.ThrowIfNull(ikm); - int hashLength = HashLength(hashAlgorithmName); + int hashLength = Helpers.HashLength(hashAlgorithmName); byte[] prk = new byte[hashLength]; Extract(hashAlgorithmName, hashLength, ikm, salt, prk); @@ -47,7 +50,7 @@ public static byte[] Extract(HashAlgorithmName hashAlgorithmName, byte[] ikm, by /// The number of bytes written to the buffer. public static int Extract(HashAlgorithmName hashAlgorithmName, ReadOnlySpan ikm, ReadOnlySpan salt, Span prk) { - int hashLength = HashLength(hashAlgorithmName); + int hashLength = Helpers.HashLength(hashAlgorithmName); if (prk.Length < hashLength) { @@ -63,19 +66,15 @@ public static int Extract(HashAlgorithmName hashAlgorithmName, ReadOnlySpan ikm, ReadOnlySpan salt, Span prk) - { - Debug.Assert(HashLength(hashAlgorithmName) == hashLength); - int written = CryptographicOperations.HmacData(hashAlgorithmName, salt, ikm, prk); - Debug.Assert(written == prk.Length, $"Bytes written is {written} bytes which does not match output length ({prk.Length} bytes)"); - } - /// /// Performs the HKDF-Expand function /// See section 2.3 of RFC5869 /// /// The hash algorithm used for HMAC operations. - /// The pseudorandom key of at least bytes (usually the output from Expand step). + /// + /// The pseudorandom key that is at least as long as the output byte array of the specified hash + /// algorithm (usually the output from the Extract step). + /// /// The length of the output keying material. /// The optional context and application specific information. /// The output keying material. @@ -84,10 +83,14 @@ private static void Extract(HashAlgorithmName hashAlgorithmName, int hashLength, public static byte[] Expand(HashAlgorithmName hashAlgorithmName, byte[] prk, int outputLength, byte[]? info = null) { ArgumentNullException.ThrowIfNull(prk); - ArgumentOutOfRangeException.ThrowIfNegativeOrZero(outputLength); - int hashLength = HashLength(hashAlgorithmName); + int hashLength = Helpers.HashLength(hashAlgorithmName); + + if (prk.Length < hashLength) + { + throw new ArgumentException(SR.Format(SR.Cryptography_Prk_TooSmall, hashLength), nameof(prk)); + } // Constant comes from section 2.3 (the constraint on L in the Inputs section) int maxOkmLength = 255 * hashLength; @@ -105,17 +108,23 @@ public static byte[] Expand(HashAlgorithmName hashAlgorithmName, byte[] prk, int /// See section 2.3 of RFC5869 /// /// The hash algorithm used for HMAC operations. - /// The pseudorandom key of at least bytes (usually the output from Expand step). + /// + /// The pseudorandom key that is at least as long as the output byte array of the specified hash + /// algorithm (usually the output from the Extract step). + /// /// The destination buffer to receive the output keying material. /// The context and application specific information (can be an empty span). /// is empty, or is larger than the maximum allowed length. public static void Expand(HashAlgorithmName hashAlgorithmName, ReadOnlySpan prk, Span output, ReadOnlySpan info) { - int hashLength = HashLength(hashAlgorithmName); + int hashLength = Helpers.HashLength(hashAlgorithmName); if (output.Length == 0) throw new ArgumentException(SR.Argument_DestinationTooShort, nameof(output)); + if (prk.Length < hashLength) + throw new ArgumentException(SR.Format(SR.Cryptography_Prk_TooSmall, hashLength), nameof(prk)); + // Constant comes from section 2.3 (the constraint on L in the Inputs section) int maxOkmLength = 255 * hashLength; if (output.Length > maxOkmLength) @@ -124,83 +133,13 @@ public static void Expand(HashAlgorithmName hashAlgorithmName, ReadOnlySpan prk, Span output, ReadOnlySpan info) - { - Debug.Assert(HashLength(hashAlgorithmName) == hashLength); - - if (prk.Length < hashLength) - throw new ArgumentException(SR.Format(SR.Cryptography_Prk_TooSmall, hashLength), nameof(prk)); - - byte counter = 0; - var counterSpan = new Span(ref counter); - Span t = Span.Empty; - Span remainingOutput = output; - - const int MaxStackInfoBuffer = 64; - Span tempInfoBuffer = stackalloc byte[MaxStackInfoBuffer]; - scoped ReadOnlySpan infoBuffer; - byte[]? rentedTempInfoBuffer = null; - - if (output.Overlaps(info)) - { - if (info.Length > MaxStackInfoBuffer) - { - rentedTempInfoBuffer = CryptoPool.Rent(info.Length); - tempInfoBuffer = rentedTempInfoBuffer; - } - - tempInfoBuffer = tempInfoBuffer.Slice(0, info.Length); - info.CopyTo(tempInfoBuffer); - infoBuffer = tempInfoBuffer; - } - else - { - infoBuffer = info; - } - - using (IncrementalHash hmac = IncrementalHash.CreateHMAC(hashAlgorithmName, prk)) - { - for (int i = 1; ; i++) - { - hmac.AppendData(t); - hmac.AppendData(infoBuffer); - counter = (byte)i; - hmac.AppendData(counterSpan); - - if (remainingOutput.Length >= hashLength) - { - t = remainingOutput.Slice(0, hashLength); - remainingOutput = remainingOutput.Slice(hashLength); - GetHashAndReset(hmac, t); - } - else - { - if (remainingOutput.Length > 0) - { - Debug.Assert(hashLength <= 512 / 8, "hashLength is larger than expected, consider increasing this value or using regular allocation"); - Span lastChunk = stackalloc byte[hashLength]; - GetHashAndReset(hmac, lastChunk); - lastChunk.Slice(0, remainingOutput.Length).CopyTo(remainingOutput); - } - - break; - } - } - } - - if (rentedTempInfoBuffer is not null) - { - CryptoPool.Return(rentedTempInfoBuffer, clearSize: info.Length); - } - } - /// /// Performs the key derivation HKDF Expand and Extract functions /// /// The hash algorithm used for HMAC operations. /// The input keying material. /// The length of the output keying material. - /// The optional salt value (a non-secret random value). If not provided it defaults to a byte array of zeros. + /// The optional salt value (a non-secret random value). /// The optional context and application specific information. /// The output keying material. /// is . @@ -211,7 +150,7 @@ public static byte[] DeriveKey(HashAlgorithmName hashAlgorithmName, byte[] ikm, ArgumentOutOfRangeException.ThrowIfNegativeOrZero(outputLength); - int hashLength = HashLength(hashAlgorithmName); + int hashLength = Helpers.HashLength(hashAlgorithmName); Debug.Assert(hashLength <= 512 / 8, "hashLength is larger than expected, consider increasing this value or using regular allocation"); // Constant comes from section 2.3 (the constraint on L in the Inputs section) @@ -219,13 +158,8 @@ public static byte[] DeriveKey(HashAlgorithmName hashAlgorithmName, byte[] ikm, if (outputLength > maxOkmLength) throw new ArgumentOutOfRangeException(nameof(outputLength), SR.Format(SR.Cryptography_Okm_TooLarge, maxOkmLength)); - Span prk = stackalloc byte[hashLength]; - - Extract(hashAlgorithmName, hashLength, ikm, salt, prk); - byte[] result = new byte[outputLength]; - Expand(hashAlgorithmName, hashLength, prk, result, info); - + DeriveKeyCore(hashAlgorithmName, hashLength, ikm, result, salt, info); return result; } @@ -240,7 +174,7 @@ public static byte[] DeriveKey(HashAlgorithmName hashAlgorithmName, byte[] ikm, /// is empty, or is larger than the maximum allowed length. public static void DeriveKey(HashAlgorithmName hashAlgorithmName, ReadOnlySpan ikm, Span output, ReadOnlySpan salt, ReadOnlySpan info) { - int hashLength = HashLength(hashAlgorithmName); + int hashLength = Helpers.HashLength(hashAlgorithmName); if (output.Length == 0) throw new ArgumentException(SR.Argument_DestinationTooShort, nameof(output)); @@ -251,61 +185,7 @@ public static void DeriveKey(HashAlgorithmName hashAlgorithmName, ReadOnlySpan prk = stackalloc byte[hashLength]; - - Extract(hashAlgorithmName, hashLength, ikm, salt, prk); - Expand(hashAlgorithmName, hashLength, prk, output, info); - } - - private static void GetHashAndReset(IncrementalHash hmac, Span output) - { - if (!hmac.TryGetHashAndReset(output, out int bytesWritten)) - { - Debug.Fail("HMAC operation failed unexpectedly"); - throw new CryptographicException(SR.Arg_CryptographyException); - } - - Debug.Assert(bytesWritten == output.Length, $"Bytes written is {bytesWritten} bytes which does not match output length ({output.Length} bytes)"); - } - - private static int HashLength(HashAlgorithmName hashAlgorithmName) - { - if (hashAlgorithmName == HashAlgorithmName.SHA1) - { - return HMACSHA1.HashSizeInBytes; - } - else if (hashAlgorithmName == HashAlgorithmName.SHA256) - { - return HMACSHA256.HashSizeInBytes; - } - else if (hashAlgorithmName == HashAlgorithmName.SHA384) - { - return HMACSHA384.HashSizeInBytes; - } - else if (hashAlgorithmName == HashAlgorithmName.SHA512) - { - return HMACSHA512.HashSizeInBytes; - } - else if (hashAlgorithmName == HashAlgorithmName.SHA3_256) - { - return HMACSHA3_256.HashSizeInBytes; - } - else if (hashAlgorithmName == HashAlgorithmName.SHA3_384) - { - return HMACSHA3_384.HashSizeInBytes; - } - else if (hashAlgorithmName == HashAlgorithmName.SHA3_512) - { - return HMACSHA3_512.HashSizeInBytes; - } - else if (hashAlgorithmName == HashAlgorithmName.MD5) - { - return HMACMD5.HashSizeInBytes; - } - else - { - throw new ArgumentOutOfRangeException(nameof(hashAlgorithmName)); - } + DeriveKeyCore(hashAlgorithmName, hashLength, ikm, output, salt, info); } } } diff --git a/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/HKDFManagedImplementation.cs b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/HKDFManagedImplementation.cs new file mode 100644 index 00000000000000..e62cc4e33ecf54 --- /dev/null +++ b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/HKDFManagedImplementation.cs @@ -0,0 +1,101 @@ +// 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.Runtime.Versioning; +using Internal.Cryptography; + +namespace System.Security.Cryptography +{ + internal static class HKDFManagedImplementation + { + internal static void Extract(HashAlgorithmName hashAlgorithmName, int hashLength, ReadOnlySpan ikm, ReadOnlySpan salt, Span prk) + { + Debug.Assert(Helpers.HashLength(hashAlgorithmName) == hashLength); + int written = CryptographicOperations.HmacData(hashAlgorithmName, salt, ikm, prk); + Debug.Assert(written == prk.Length, $"Bytes written is {written} bytes which does not match output length ({prk.Length} bytes)"); + } + + internal static void Expand(HashAlgorithmName hashAlgorithmName, int hashLength, ReadOnlySpan prk, Span output, ReadOnlySpan info) + { + Debug.Assert(Helpers.HashLength(hashAlgorithmName) == hashLength); + + byte counter = 0; + var counterSpan = new Span(ref counter); + Span t = Span.Empty; + Span remainingOutput = output; + + const int MaxStackInfoBuffer = 64; + Span tempInfoBuffer = stackalloc byte[MaxStackInfoBuffer]; + scoped ReadOnlySpan infoBuffer; + byte[]? rentedTempInfoBuffer = null; + + if (output.Overlaps(info)) + { + if (info.Length > MaxStackInfoBuffer) + { + rentedTempInfoBuffer = CryptoPool.Rent(info.Length); + tempInfoBuffer = rentedTempInfoBuffer; + } + + tempInfoBuffer = tempInfoBuffer.Slice(0, info.Length); + info.CopyTo(tempInfoBuffer); + infoBuffer = tempInfoBuffer; + } + else + { + infoBuffer = info; + } + + using (IncrementalHash hmac = IncrementalHash.CreateHMAC(hashAlgorithmName, prk)) + { + for (int i = 1; ; i++) + { + hmac.AppendData(t); + hmac.AppendData(infoBuffer); + counter = (byte)i; + hmac.AppendData(counterSpan); + + if (remainingOutput.Length >= hashLength) + { + t = remainingOutput.Slice(0, hashLength); + remainingOutput = remainingOutput.Slice(hashLength); + GetHashAndReset(hmac, t); + } + else + { + if (remainingOutput.Length > 0) + { + Debug.Assert(hashLength <= 512 / 8, "hashLength is larger than expected, consider increasing this value or using regular allocation"); + Span lastChunk = stackalloc byte[hashLength]; + GetHashAndReset(hmac, lastChunk); + lastChunk.Slice(0, remainingOutput.Length).CopyTo(remainingOutput); + } + + break; + } + } + } + + if (rentedTempInfoBuffer is not null) + { + CryptoPool.Return(rentedTempInfoBuffer, clearSize: info.Length); + } + } + + internal static void DeriveKey(HashAlgorithmName hashAlgorithmName, int hashLength, ReadOnlySpan ikm, Span output, ReadOnlySpan salt, ReadOnlySpan info) + { + Span prk = stackalloc byte[hashLength]; + + Extract(hashAlgorithmName, hashLength, ikm, salt, prk); + Expand(hashAlgorithmName, hashLength, prk, output, info); + CryptographicOperations.ZeroMemory(prk); + } + + private static void GetHashAndReset(IncrementalHash hmac, Span output) + { + int written = hmac.GetHashAndReset(output); + Debug.Assert(written == output.Length, $"Bytes written is {written} bytes which does not match output length ({output.Length} bytes)"); + } + } +} diff --git a/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/Helpers.cs b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/Helpers.cs index 5a07b09c2cc17b..72319d5185af60 100644 --- a/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/Helpers.cs +++ b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/Helpers.cs @@ -347,5 +347,45 @@ internal static ReadOnlySpan ArrayToSpanOrThrow( return arg; } + + internal static int HashLength(HashAlgorithmName hashAlgorithmName) + { + if (hashAlgorithmName == HashAlgorithmName.SHA1) + { + return HMACSHA1.HashSizeInBytes; + } + else if (hashAlgorithmName == HashAlgorithmName.SHA256) + { + return HMACSHA256.HashSizeInBytes; + } + else if (hashAlgorithmName == HashAlgorithmName.SHA384) + { + return HMACSHA384.HashSizeInBytes; + } + else if (hashAlgorithmName == HashAlgorithmName.SHA512) + { + return HMACSHA512.HashSizeInBytes; + } + else if (hashAlgorithmName == HashAlgorithmName.SHA3_256) + { + return HMACSHA3_256.HashSizeInBytes; + } + else if (hashAlgorithmName == HashAlgorithmName.SHA3_384) + { + return HMACSHA3_384.HashSizeInBytes; + } + else if (hashAlgorithmName == HashAlgorithmName.SHA3_512) + { + return HMACSHA3_512.HashSizeInBytes; + } + else if (hashAlgorithmName == HashAlgorithmName.MD5) + { + return HMACMD5.HashSizeInBytes; + } + else + { + throw new ArgumentOutOfRangeException(nameof(hashAlgorithmName)); + } + } } } diff --git a/src/libraries/System.Security.Cryptography/tests/HKDFTests.cs b/src/libraries/System.Security.Cryptography/tests/HKDFTests.cs index 9c3537808b0a70..896325de4b371c 100644 --- a/src/libraries/System.Security.Cryptography/tests/HKDFTests.cs +++ b/src/libraries/System.Security.Cryptography/tests/HKDFTests.cs @@ -15,6 +15,7 @@ public abstract class HKDFTests protected abstract byte[] DeriveKey(HashAlgorithmName hash, byte[] ikm, int outputLength, byte[] salt, byte[] info); internal static bool MD5Supported => !PlatformDetection.IsBrowser && !PlatformDetection.IsAzureLinux; + internal static bool EmptyKeysSupported => !PlatformDetection.IsAzureLinux; [Theory] [MemberData(nameof(GetHkdfTestCases))] @@ -72,7 +73,7 @@ public void ExtractNonsensicalHash() () => Extract(new HashAlgorithmName("foo"), 20, ikm, salt)); } - [Fact] + [ConditionalFact(nameof(EmptyKeysSupported))] public void ExtractEmptyIkm() { byte[] salt = new byte[20]; @@ -206,6 +207,7 @@ public void DeriveKeyTamperInfoTests(HkdfTestCase test) [Theory] [MemberData(nameof(Sha3TestCases))] + [ActiveIssue("https://github.com/dotnet/runtime/issues/106489", typeof(PlatformDetection), nameof(PlatformDetection.IsAzureLinux))] public void Sha3Tests(HkdfTestCase test) { if (PlatformDetection.SupportsSha3) diff --git a/src/native/libs/System.Security.Cryptography.Native/entrypoints.c b/src/native/libs/System.Security.Cryptography.Native/entrypoints.c index 523796cd3cac59..de513a3c4152a5 100644 --- a/src/native/libs/System.Security.Cryptography.Native/entrypoints.c +++ b/src/native/libs/System.Security.Cryptography.Native/entrypoints.c @@ -222,6 +222,9 @@ static const Entry s_cryptoNative[] = DllImportEntry(CryptoNative_GetX509SubjectPublicKeyInfoDerSize) DllImportEntry(CryptoNative_GetX509Thumbprint) DllImportEntry(CryptoNative_GetX509Version) + DllImportEntry(CryptoNative_HkdfDeriveKey) + DllImportEntry(CryptoNative_HkdfExpand) + DllImportEntry(CryptoNative_HkdfExtract) DllImportEntry(CryptoNative_HmacCopy) DllImportEntry(CryptoNative_HmacCreate) DllImportEntry(CryptoNative_HmacCurrent) diff --git a/src/native/libs/System.Security.Cryptography.Native/opensslshim.h b/src/native/libs/System.Security.Cryptography.Native/opensslshim.h index 1ce54770235218..b85316556c0aac 100644 --- a/src/native/libs/System.Security.Cryptography.Native/opensslshim.h +++ b/src/native/libs/System.Security.Cryptography.Native/opensslshim.h @@ -104,6 +104,24 @@ void ERR_put_error(int32_t lib, int32_t func, int32_t reason, const char* file, c_static_assert(ERR_R_UNSUPPORTED == 0x8010C); #endif +#ifndef EVP_KDF_HKDF_MODE_EXTRACT_AND_EXPAND +#define EVP_KDF_HKDF_MODE_EXTRACT_AND_EXPAND 0 +#else +c_static_assert(EVP_KDF_HKDF_MODE_EXTRACT_AND_EXPAND == 0); +#endif + +#ifndef EVP_KDF_HKDF_MODE_EXTRACT_ONLY +#define EVP_KDF_HKDF_MODE_EXTRACT_ONLY 1 +#else +c_static_assert(EVP_KDF_HKDF_MODE_EXTRACT_ONLY == 1); +#endif + +#ifndef EVP_KDF_HKDF_MODE_EXPAND_ONLY +#define EVP_KDF_HKDF_MODE_EXPAND_ONLY 2 +#else +c_static_assert(EVP_KDF_HKDF_MODE_EXPAND_ONLY == 2); +#endif + #if defined FEATURE_DISTRO_AGNOSTIC_SSL || OPENSSL_VERSION_NUMBER >= OPENSSL_VERSION_3_0_RTM #include "apibridge_30_rev.h" #endif @@ -552,6 +570,7 @@ extern bool g_libSslUses32BitTime; LIGHTUP_FUNCTION(OSSL_STORE_open_ex) \ LIGHTUP_FUNCTION(OSSL_PARAM_construct_octet_string) \ LIGHTUP_FUNCTION(OSSL_PARAM_construct_utf8_string) \ + LIGHTUP_FUNCTION(OSSL_PARAM_construct_int) \ LIGHTUP_FUNCTION(OSSL_PARAM_construct_int32) \ LIGHTUP_FUNCTION(OSSL_PARAM_construct_end) \ REQUIRED_FUNCTION(PKCS8_PRIV_KEY_INFO_free) \ @@ -1104,6 +1123,7 @@ extern TYPEOF(OPENSSL_gmtime)* OPENSSL_gmtime_ptr; #define OSSL_STORE_open_ex OSSL_STORE_open_ex_ptr #define OSSL_PARAM_construct_octet_string OSSL_PARAM_construct_octet_string_ptr #define OSSL_PARAM_construct_utf8_string OSSL_PARAM_construct_utf8_string_ptr +#define OSSL_PARAM_construct_int OSSL_PARAM_construct_int_ptr #define OSSL_PARAM_construct_int32 OSSL_PARAM_construct_int32_ptr #define OSSL_PARAM_construct_end OSSL_PARAM_construct_end_ptr #define PKCS8_PRIV_KEY_INFO_free PKCS8_PRIV_KEY_INFO_free_ptr diff --git a/src/native/libs/System.Security.Cryptography.Native/osslcompat_30.h b/src/native/libs/System.Security.Cryptography.Native/osslcompat_30.h index 6baa8fa143347b..1609afd2a003bd 100644 --- a/src/native/libs/System.Security.Cryptography.Native/osslcompat_30.h +++ b/src/native/libs/System.Security.Cryptography.Native/osslcompat_30.h @@ -17,6 +17,7 @@ #define OSSL_KDF_PARAM_KEY "key" #define OSSL_KDF_PARAM_SALT "salt" #define OSSL_KDF_PARAM_INFO "info" +#define OSSL_KDF_PARAM_MODE "mode" #define OSSL_MAC_PARAM_KEY "key" #define OSSL_MAC_PARAM_CUSTOM "custom" @@ -76,6 +77,7 @@ EVP_PKEY_CTX *EVP_PKEY_CTX_new_from_pkey( OSSL_LIB_CTX *libctx, EVP_PKEY *pkey, const char *propquery); OSSL_PARAM OSSL_PARAM_construct_end(void); +OSSL_PARAM OSSL_PARAM_construct_int(const char *key, int *buf); OSSL_PARAM OSSL_PARAM_construct_int32(const char *key, int32_t *buf); OSSL_PARAM OSSL_PARAM_construct_octet_string(const char *key, void *buf, size_t bsize); OSSL_PARAM OSSL_PARAM_construct_utf8_string(const char *key, char *buf, size_t bsize); diff --git a/src/native/libs/System.Security.Cryptography.Native/pal_evp_kdf.c b/src/native/libs/System.Security.Cryptography.Native/pal_evp_kdf.c index 5b3705e8128419..a7af40330316a5 100644 --- a/src/native/libs/System.Security.Cryptography.Native/pal_evp_kdf.c +++ b/src/native/libs/System.Security.Cryptography.Native/pal_evp_kdf.c @@ -157,3 +157,165 @@ int32_t CryptoNative_KbkdfHmacOneShot( #endif return 0; } + +static int32_t HkdfCore( + EVP_KDF* kdf, + int operation, + uint8_t* key, + int32_t keyLength, + char* algorithm, + uint8_t* salt, + int32_t saltLength, + uint8_t* info, + int32_t infoLength, + uint8_t* destination, + int32_t destinationLength) +{ + assert(kdf); + assert(key != NULL || keyLength == 0); + assert(keyLength >= 0); + assert(algorithm); + assert(destination); + assert(destinationLength > 0); + assert(salt != NULL || saltLength == 0); + assert(info != NULL || infoLength == 0); + + ERR_clear_error(); + +#ifdef NEED_OPENSSL_3_0 + if (API_EXISTS(EVP_KDF_CTX_new)) + { + assert(API_EXISTS(EVP_KDF_CTX_free)); + assert(API_EXISTS(EVP_KDF_derive)); + assert(API_EXISTS(OSSL_PARAM_construct_utf8_string)); + assert(API_EXISTS(OSSL_PARAM_construct_octet_string)); + assert(API_EXISTS(OSSL_PARAM_construct_end)); + + EVP_KDF_CTX* ctx = EVP_KDF_CTX_new(kdf); + int32_t ret = 0; + + if (ctx == NULL) + { + goto cleanup; + } + + size_t keyLengthT = Int32ToSizeT(keyLength); + size_t destinationLengthT = Int32ToSizeT(destinationLength); + size_t saltLengthT = Int32ToSizeT(saltLength); + size_t infoLengthT = Int32ToSizeT(infoLength); + + OSSL_PARAM params[] = + { + OSSL_PARAM_construct_octet_string(OSSL_KDF_PARAM_KEY, (void*)key, keyLengthT), + OSSL_PARAM_construct_utf8_string(OSSL_KDF_PARAM_DIGEST, algorithm, 0), + OSSL_PARAM_construct_octet_string(OSSL_KDF_PARAM_SALT, (void*)salt, saltLengthT), + OSSL_PARAM_construct_octet_string(OSSL_KDF_PARAM_INFO, (void*)info, infoLengthT), + OSSL_PARAM_construct_int(OSSL_KDF_PARAM_MODE, &operation), + OSSL_PARAM_construct_end(), + }; + + if (EVP_KDF_derive(ctx, destination, destinationLengthT, params) <= 0) + { + goto cleanup; + } + + ret = 1; + +cleanup: + if (ctx != NULL) + { + EVP_KDF_CTX_free(ctx); + } + + return ret; + } +#else + (void)kdf; + (void)operation; + (void)key; + (void)keyLength; + (void)algorithm; + (void)salt; + (void)saltLength; + (void)info; + (void)infoLength; + (void)destination; + (void)destinationLength; + assert(0 && "Inconsistent EVP_KDF API availability."); +#endif + return 0; +} + +int32_t CryptoNative_HkdfDeriveKey( + EVP_KDF* kdf, + uint8_t* ikm, + int32_t ikmLength, + char* algorithm, + uint8_t* salt, + int32_t saltLength, + uint8_t* info, + int32_t infoLength, + uint8_t* destination, + int32_t destinationLength) +{ + return HkdfCore( + kdf, + EVP_KDF_HKDF_MODE_EXTRACT_AND_EXPAND, + ikm, + ikmLength, + algorithm, + salt, + saltLength, + info, + infoLength, + destination, + destinationLength); +} + +int32_t CryptoNative_HkdfExpand( + EVP_KDF* kdf, + uint8_t* prk, + int32_t prkLength, + char* algorithm, + uint8_t* info, + int32_t infoLength, + uint8_t* destination, + int32_t destinationLength) +{ + return HkdfCore( + kdf, + EVP_KDF_HKDF_MODE_EXPAND_ONLY, + prk, + prkLength, + algorithm, + NULL /* salt */, + 0 /* saltLength */, + info, + infoLength, + destination, + destinationLength); +} + +int32_t CryptoNative_HkdfExtract( + EVP_KDF* kdf, + uint8_t* ikm, + int32_t ikmLength, + char* algorithm, + uint8_t* salt, + int32_t saltLength, + uint8_t* destination, + int32_t destinationLength) +{ + return HkdfCore( + kdf, + EVP_KDF_HKDF_MODE_EXTRACT_ONLY, + ikm, + ikmLength, + algorithm, + salt, + saltLength, + NULL /* info */, + 0 /* infoLength */, + destination, + destinationLength); +} diff --git a/src/native/libs/System.Security.Cryptography.Native/pal_evp_kdf.h b/src/native/libs/System.Security.Cryptography.Native/pal_evp_kdf.h index 0841dc2b3553e1..165e9e59d7ad56 100644 --- a/src/native/libs/System.Security.Cryptography.Native/pal_evp_kdf.h +++ b/src/native/libs/System.Security.Cryptography.Native/pal_evp_kdf.h @@ -61,3 +61,35 @@ PALEXPORT int32_t CryptoNative_KbkdfHmacOneShot( int32_t contextLength, uint8_t* destination, int32_t destinationLength); + +PALEXPORT int32_t CryptoNative_HkdfDeriveKey( + EVP_KDF* kdf, + uint8_t* ikm, + int32_t ikmLength, + char* algorithm, + uint8_t* salt, + int32_t saltLength, + uint8_t* info, + int32_t infoLength, + uint8_t* destination, + int32_t destinationLength); + +PALEXPORT int32_t CryptoNative_HkdfExpand( + EVP_KDF* kdf, + uint8_t* prk, + int32_t prkLength, + char* algorithm, + uint8_t* info, + int32_t infoLength, + uint8_t* destination, + int32_t destinationLength); + +PALEXPORT int32_t CryptoNative_HkdfExtract( + EVP_KDF* kdf, + uint8_t* ikm, + int32_t ikmLength, + char* algorithm, + uint8_t* salt, + int32_t saltLength, + uint8_t* destination, + int32_t destinationLength);