From 32388df1ad99d616377b5ec6ca4966d60f10b365 Mon Sep 17 00:00:00 2001 From: Krzysztof Wicher Date: Wed, 13 Nov 2019 16:23:19 -0800 Subject: [PATCH] HKDF implementation (dotnet/corefx#42567) * HKDF implementation * Fix CreateMacProvider on OSX * apply review feedback * improve error message in case of test failure Commit migrated from https://github.com/dotnet/corefx/commit/c14fc5636bdb9141f69eaeaf0e5812b80af525b1 --- .../src/Internal/Cryptography/HashProviderCng.cs | 11 +- .../Windows/BCrypt/Interop.BCryptCreateHash.cs | 7 +- .../ref/System.Security.Cryptography.Algorithms.cs | 9 + .../src/Internal/Cryptography/HMACCommon.cs | 35 +- .../Cryptography/HashProviderDispenser.OSX.cs | 6 +- .../Cryptography/HashProviderDispenser.Unix.cs | 7 +- .../Cryptography/HashProviderDispenser.Windows.cs | 4 +- .../src/Resources/Strings.resx | 6 + .../System.Security.Cryptography.Algorithms.csproj | 5 +- .../src/System/Security/Cryptography/HKDF.cs | 262 ++++++++++ .../Security/Cryptography/IncrementalHash.cs | 6 + .../tests/HKDFTests.cs | 554 +++++++++++++++++++++ ...m.Security.Cryptography.Algorithms.Tests.csproj | 1 + 13 files changed, 892 insertions(+), 21 deletions(-) create mode 100644 src/libraries/System.Security.Cryptography.Algorithms/src/System/Security/Cryptography/HKDF.cs create mode 100644 src/libraries/System.Security.Cryptography.Algorithms/tests/HKDFTests.cs diff --git a/src/libraries/Common/src/Internal/Cryptography/HashProviderCng.cs b/src/libraries/Common/src/Internal/Cryptography/HashProviderCng.cs index d94bc09..ce4e1f5 100644 --- a/src/libraries/Common/src/Internal/Cryptography/HashProviderCng.cs +++ b/src/libraries/Common/src/Internal/Cryptography/HashProviderCng.cs @@ -21,12 +21,16 @@ namespace Internal.Cryptography // // - "key" activates MAC hashing if present. If null, this HashProvider performs a regular old hash. // - public HashProviderCng(string hashAlgId, byte[] key) + public HashProviderCng(string hashAlgId, byte[] key) : this(hashAlgId, key, isHmac: key != null) + { + } + + internal HashProviderCng(string hashAlgId, ReadOnlySpan key, bool isHmac) { BCryptOpenAlgorithmProviderFlags dwFlags = BCryptOpenAlgorithmProviderFlags.None; - if (key != null) + if (isHmac) { - _key = key.CloneByteArray(); + _key = key.ToArray(); dwFlags |= BCryptOpenAlgorithmProviderFlags.BCRYPT_ALG_HANDLE_HMAC_FLAG; } @@ -63,7 +67,6 @@ namespace Internal.Cryptography throw Interop.BCrypt.CreateCryptographicException(ntStatus); _hashSize = hashSize; } - return; } public sealed override unsafe void AppendHashData(ReadOnlySpan source) diff --git a/src/libraries/Common/src/Interop/Windows/BCrypt/Interop.BCryptCreateHash.cs b/src/libraries/Common/src/Interop/Windows/BCrypt/Interop.BCryptCreateHash.cs index 09d5987..6a2fc0d 100644 --- a/src/libraries/Common/src/Interop/Windows/BCrypt/Interop.BCryptCreateHash.cs +++ b/src/libraries/Common/src/Interop/Windows/BCrypt/Interop.BCryptCreateHash.cs @@ -12,8 +12,13 @@ internal partial class Interop { internal partial class BCrypt { + internal static NTSTATUS BCryptCreateHash(SafeBCryptAlgorithmHandle hAlgorithm, out SafeBCryptHashHandle phHash, IntPtr pbHashObject, int cbHashObject, ReadOnlySpan secret, int cbSecret, BCryptCreateHashFlags dwFlags) + { + return BCryptCreateHash(hAlgorithm, out phHash, pbHashObject, cbHashObject, ref MemoryMarshal.GetReference(secret), cbSecret, dwFlags); + } + [DllImport(Libraries.BCrypt, CharSet = CharSet.Unicode)] - internal static extern NTSTATUS BCryptCreateHash(SafeBCryptAlgorithmHandle hAlgorithm, out SafeBCryptHashHandle phHash, IntPtr pbHashObject, int cbHashObject, [In, Out] byte[] pbSecret, int cbSecret, BCryptCreateHashFlags dwFlags); + private static extern NTSTATUS BCryptCreateHash(SafeBCryptAlgorithmHandle hAlgorithm, out SafeBCryptHashHandle phHash, IntPtr pbHashObject, int cbHashObject, ref byte pbSecret, int cbSecret, BCryptCreateHashFlags dwFlags); [Flags] internal enum BCryptCreateHashFlags : int diff --git a/src/libraries/System.Security.Cryptography.Algorithms/ref/System.Security.Cryptography.Algorithms.cs b/src/libraries/System.Security.Cryptography.Algorithms/ref/System.Security.Cryptography.Algorithms.cs index 06fec22..f60d0ed 100644 --- a/src/libraries/System.Security.Cryptography.Algorithms/ref/System.Security.Cryptography.Algorithms.cs +++ b/src/libraries/System.Security.Cryptography.Algorithms/ref/System.Security.Cryptography.Algorithms.cs @@ -332,6 +332,15 @@ namespace System.Security.Cryptography public byte[] X; public byte[] Y; } + public static class HKDF + { + public static byte[] Extract(HashAlgorithmName hashAlgorithmName, byte[] ikm, byte[] salt = null) { throw null; } + public static int Extract(HashAlgorithmName hashAlgorithmName, ReadOnlySpan ikm, ReadOnlySpan salt, Span prk) { throw null; } + public static byte[] Expand(HashAlgorithmName hashAlgorithmName, byte[] prk, int outputLength, byte[] info = null) { throw null; } + public static void Expand(HashAlgorithmName hashAlgorithmName, ReadOnlySpan prk, Span output, ReadOnlySpan info) { throw null; } + public static byte[] DeriveKey(HashAlgorithmName hashAlgorithmName, byte[] ikm, int outputLength, byte[] salt = null, byte[] info = null) { throw null; } + public static void DeriveKey(HashAlgorithmName hashAlgorithmName, ReadOnlySpan ikm, Span output, ReadOnlySpan salt, ReadOnlySpan info) { throw null; } + } public partial class HMACMD5 : System.Security.Cryptography.HMAC { public HMACMD5() { } diff --git a/src/libraries/System.Security.Cryptography.Algorithms/src/Internal/Cryptography/HMACCommon.cs b/src/libraries/System.Security.Cryptography.Algorithms/src/Internal/Cryptography/HMACCommon.cs index 8a6d400..63f3b8b 100644 --- a/src/libraries/System.Security.Cryptography.Algorithms/src/Internal/Cryptography/HMACCommon.cs +++ b/src/libraries/System.Security.Cryptography.Algorithms/src/Internal/Cryptography/HMACCommon.cs @@ -17,20 +17,45 @@ namespace Internal.Cryptography // internal sealed class HMACCommon { - public HMACCommon(string hashAlgorithmId, byte[] key, int blockSize) + public HMACCommon(string hashAlgorithmId, byte[] key, int blockSize) : this(hashAlgorithmId, blockSize) + { + ChangeKey(key); + } + + internal HMACCommon(string hashAlgorithmId, ReadOnlySpan key, int blockSize) : this(hashAlgorithmId, blockSize) + { + // note: will not set ActualKey if key size is smaller or equal than blockSize + // this is to avoid extra allocation. ActualKey can still be used if key is generated. + // Otherwise the ReadOnlySpan overload would actually be slower than byte array overload. + ChangeKey(key); + } + + private HMACCommon(string hashAlgorithmId, int blockSize) { Debug.Assert(!string.IsNullOrEmpty(hashAlgorithmId)); Debug.Assert(blockSize > 0 || blockSize == -1); _hashAlgorithmId = hashAlgorithmId; _blockSize = blockSize; - ChangeKey(key); } public int HashSizeInBits => _hMacProvider.HashSizeInBytes * 8; public void ChangeKey(byte[] key) { + ActualKey = ChangeKeyImpl(key) ?? key; + } + + internal void ChangeKey(ReadOnlySpan key) + { + // note: does not set key when it's smaller than blockSize + ActualKey = ChangeKeyImpl(key); + } + + private byte[] ChangeKeyImpl(ReadOnlySpan key) + { + byte[] modifiedKey = null; + // If _blockSize is -1 the key isn't going to be extractable by the object holder, // so there's no point in recalculating it in managed code. if (key.Length > _blockSize && _blockSize > 0) @@ -40,8 +65,8 @@ namespace Internal.Cryptography { _lazyHashProvider = HashProviderDispenser.CreateHashProvider(_hashAlgorithmId); } - _lazyHashProvider.AppendHashData(key, 0, key.Length); - key = _lazyHashProvider.FinalizeHashAndReset(); + _lazyHashProvider.AppendHashData(key); + modifiedKey = _lazyHashProvider.FinalizeHashAndReset(); } HashProvider oldHashProvider = _hMacProvider; @@ -49,7 +74,7 @@ namespace Internal.Cryptography oldHashProvider?.Dispose(true); _hMacProvider = HashProviderDispenser.CreateMacProvider(_hashAlgorithmId, key); - ActualKey = key; + return modifiedKey; } // The actual key used for hashing. This will not be the same as the original key passed to ChangeKey() if the original key exceeded the diff --git a/src/libraries/System.Security.Cryptography.Algorithms/src/Internal/Cryptography/HashProviderDispenser.OSX.cs b/src/libraries/System.Security.Cryptography.Algorithms/src/Internal/Cryptography/HashProviderDispenser.OSX.cs index 4768666..fb6025f 100644 --- a/src/libraries/System.Security.Cryptography.Algorithms/src/Internal/Cryptography/HashProviderDispenser.OSX.cs +++ b/src/libraries/System.Security.Cryptography.Algorithms/src/Internal/Cryptography/HashProviderDispenser.OSX.cs @@ -30,7 +30,7 @@ namespace Internal.Cryptography throw new CryptographicException(SR.Format(SR.Cryptography_UnknownHashAlgorithm, hashAlgorithmId)); } - public static HashProvider CreateMacProvider(string hashAlgorithmId, byte[] key) + public static HashProvider CreateMacProvider(string hashAlgorithmId, ReadOnlySpan key) { switch (hashAlgorithmId) { @@ -62,9 +62,9 @@ namespace Internal.Cryptography public override int HashSizeInBytes { get; } - internal AppleHmacProvider(Interop.AppleCrypto.PAL_HashAlgorithm algorithm, byte[] key) + internal AppleHmacProvider(Interop.AppleCrypto.PAL_HashAlgorithm algorithm, ReadOnlySpan key) { - _key = key.CloneByteArray(); + _key = key.ToArray(); int hashSizeInBytes = 0; _ctx = Interop.AppleCrypto.HmacCreate(algorithm, ref hashSizeInBytes); diff --git a/src/libraries/System.Security.Cryptography.Algorithms/src/Internal/Cryptography/HashProviderDispenser.Unix.cs b/src/libraries/System.Security.Cryptography.Algorithms/src/Internal/Cryptography/HashProviderDispenser.Unix.cs index ff8e91e..1c85f79 100644 --- a/src/libraries/System.Security.Cryptography.Algorithms/src/Internal/Cryptography/HashProviderDispenser.Unix.cs +++ b/src/libraries/System.Security.Cryptography.Algorithms/src/Internal/Cryptography/HashProviderDispenser.Unix.cs @@ -30,7 +30,7 @@ namespace Internal.Cryptography throw new CryptographicException(SR.Format(SR.Cryptography_UnknownHashAlgorithm, hashAlgorithmId)); } - public static unsafe HashProvider CreateMacProvider(string hashAlgorithmId, byte[] key) + public static unsafe HashProvider CreateMacProvider(string hashAlgorithmId, ReadOnlySpan key) { switch (hashAlgorithmId) { @@ -121,10 +121,9 @@ namespace Internal.Cryptography private readonly int _hashSize; private SafeHmacCtxHandle _hmacCtx; - public HmacHashProvider(IntPtr algorithmEvp, byte[] key) + public HmacHashProvider(IntPtr algorithmEvp, ReadOnlySpan key) { Debug.Assert(algorithmEvp != IntPtr.Zero); - Debug.Assert(key != null); _hashSize = Interop.Crypto.EvpMdSize(algorithmEvp); if (_hashSize <= 0 || _hashSize > Interop.Crypto.EVP_MAX_MD_SIZE) @@ -132,7 +131,7 @@ namespace Internal.Cryptography throw new CryptographicException(); } - _hmacCtx = Interop.Crypto.HmacCreate(ref MemoryMarshal.GetReference(new Span(key)), key.Length, algorithmEvp); + _hmacCtx = Interop.Crypto.HmacCreate(ref MemoryMarshal.GetReference(key), key.Length, algorithmEvp); Interop.Crypto.CheckValidOpenSslHandle(_hmacCtx); } diff --git a/src/libraries/System.Security.Cryptography.Algorithms/src/Internal/Cryptography/HashProviderDispenser.Windows.cs b/src/libraries/System.Security.Cryptography.Algorithms/src/Internal/Cryptography/HashProviderDispenser.Windows.cs index 0121303..e5ddba7 100644 --- a/src/libraries/System.Security.Cryptography.Algorithms/src/Internal/Cryptography/HashProviderDispenser.Windows.cs +++ b/src/libraries/System.Security.Cryptography.Algorithms/src/Internal/Cryptography/HashProviderDispenser.Windows.cs @@ -18,9 +18,9 @@ namespace Internal.Cryptography return new HashProviderCng(hashAlgorithmId, null); } - public static HashProvider CreateMacProvider(string hashAlgorithmId, byte[] key) + public static HashProvider CreateMacProvider(string hashAlgorithmId, ReadOnlySpan key) { - return new HashProviderCng(hashAlgorithmId, key); + return new HashProviderCng(hashAlgorithmId, key, isHmac: true); } } } diff --git a/src/libraries/System.Security.Cryptography.Algorithms/src/Resources/Strings.resx b/src/libraries/System.Security.Cryptography.Algorithms/src/Resources/Strings.resx index 5b97a59..593d64c 100644 --- a/src/libraries/System.Security.Cryptography.Algorithms/src/Resources/Strings.resx +++ b/src/libraries/System.Security.Cryptography.Algorithms/src/Resources/Strings.resx @@ -330,4 +330,10 @@ The input to WriteEncodedValue must represent a single encoded value with no trailing data. + + The pseudo-random key length must be {0} bytes. + + + Output keying material length can be at most {0} bytes (255 * hash length). + diff --git a/src/libraries/System.Security.Cryptography.Algorithms/src/System.Security.Cryptography.Algorithms.csproj b/src/libraries/System.Security.Cryptography.Algorithms/src/System.Security.Cryptography.Algorithms.csproj index d46e378..820621a 100644 --- a/src/libraries/System.Security.Cryptography.Algorithms/src/System.Security.Cryptography.Algorithms.csproj +++ b/src/libraries/System.Security.Cryptography.Algorithms/src/System.Security.Cryptography.Algorithms.csproj @@ -1,4 +1,4 @@ - + true $(DefineConstants);INTERNAL_ASYMMETRIC_IMPLEMENTATIONS @@ -45,6 +45,7 @@ + @@ -726,6 +727,6 @@ - + diff --git a/src/libraries/System.Security.Cryptography.Algorithms/src/System/Security/Cryptography/HKDF.cs b/src/libraries/System.Security.Cryptography.Algorithms/src/System/Security/Cryptography/HKDF.cs new file mode 100644 index 0000000..23edddc --- /dev/null +++ b/src/libraries/System.Security.Cryptography.Algorithms/src/System/Security/Cryptography/HKDF.cs @@ -0,0 +1,262 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Diagnostics; + +namespace System.Security.Cryptography +{ + /// + /// RFC5869 HMAC-based Extract-and-Expand Key Derivation (HKDF) + /// + /// + /// In situations where the input key material is already a uniformly random bitstring, the HKDF standard allows the Extract + /// 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 + { + /// + /// Performs the HKDF-Extract function. + /// See section 2.2 of RFC5869 + /// + /// 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 pseudo random key (prk). + public static byte[] Extract(HashAlgorithmName hashAlgorithmName, byte[] ikm, byte[] salt = null) + { + if (ikm == null) + throw new ArgumentNullException(nameof(ikm)); + + int hashLength = HashLength(hashAlgorithmName); + byte[] prk = new byte[hashLength]; + + Extract(hashAlgorithmName, hashLength, ikm, salt, prk); + return prk; + } + + /// + /// Performs the HKDF-Extract function. + /// See section 2.2 of RFC5869 + /// + /// The hash algorithm used for HMAC operations. + /// The input keying material. + /// The salt value (a non-secret random value). + /// The destination buffer to receive the pseudo-random key (prk). + /// The number of bytes written to the buffer. + public static int Extract(HashAlgorithmName hashAlgorithmName, ReadOnlySpan ikm, ReadOnlySpan salt, Span prk) + { + int hashLength = HashLength(hashAlgorithmName); + + if (prk.Length < hashLength) + { + throw new ArgumentException(SR.Format(SR.Cryptography_Prk_TooSmall, hashLength), nameof(prk)); + } + + if (prk.Length > hashLength) + { + prk = prk.Slice(0, hashLength); + } + + Extract(hashAlgorithmName, hashLength, ikm, salt, prk); + return hashLength; + } + + private static void Extract(HashAlgorithmName hashAlgorithmName, int hashLength, ReadOnlySpan ikm, ReadOnlySpan salt, Span prk) + { + Debug.Assert(HashLength(hashAlgorithmName) == hashLength); + + using (IncrementalHash hmac = IncrementalHash.CreateHMAC(hashAlgorithmName, salt)) + { + hmac.AppendData(ikm); + GetHashAndReset(hmac, prk); + } + } + + /// + /// 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 length of the output keying material. + /// The optional context and application specific information. + /// The output keying material. + public static byte[] Expand(HashAlgorithmName hashAlgorithmName, byte[] prk, int outputLength, byte[] info = null) + { + if (prk == null) + throw new ArgumentNullException(nameof(prk)); + + int hashLength = HashLength(hashAlgorithmName); + + // Constant comes from section 2.3 (the constraint on L in the Inputs section) + int maxOkmLength = 255 * hashLength; + if (outputLength <= 0 || outputLength > maxOkmLength) + throw new ArgumentOutOfRangeException(nameof(outputLength), SR.Format(SR.Cryptography_Okm_TooLarge, maxOkmLength)); + + byte[] result = new byte[outputLength]; + Expand(hashAlgorithmName, hashLength, prk, result, info); + + return result; + } + + /// + /// 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 destination buffer to receive the output keying material. + /// The context and application specific information (can be an empty span). + public static void Expand(HashAlgorithmName hashAlgorithmName, ReadOnlySpan prk, Span output, ReadOnlySpan info) + { + int hashLength = HashLength(hashAlgorithmName); + + // Constant comes from section 2.3 (the constraint on L in the Inputs section) + int maxOkmLength = 255 * hashLength; + if (output.Length > maxOkmLength) + throw new ArgumentException(SR.Format(SR.Cryptography_Okm_TooLarge, maxOkmLength), nameof(output)); + + Expand(hashAlgorithmName, hashLength, prk, output, info); + } + + private static void Expand(HashAlgorithmName hashAlgorithmName, int hashLength, 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)); + + Span counterSpan = stackalloc byte[1]; + ref byte counter = ref counterSpan[0]; + Span t = Span.Empty; + Span remainingOutput = output; + + using (IncrementalHash hmac = IncrementalHash.CreateHMAC(hashAlgorithmName, prk)) + { + for (int i = 1; ; i++) + { + hmac.AppendData(t); + hmac.AppendData(info); + 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; + } + } + } + } + + /// + /// 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 context and application specific information. + /// The output keying material. + public static byte[] DeriveKey(HashAlgorithmName hashAlgorithmName, byte[] ikm, int outputLength, byte[] salt = null, byte[] info = null) + { + if (ikm == null) + throw new ArgumentNullException(nameof(ikm)); + + int hashLength = 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) + int maxOkmLength = 255 * hashLength; + 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); + + return result; + } + + /// + /// Performs the key derivation HKDF Expand and Extract functions + /// + /// The hash algorithm used for HMAC operations. + /// The input keying material. + /// The output buffer representing output keying material. + /// The salt value (a non-secret random value). + /// The context and application specific information (can be an empty span). + public static void DeriveKey(HashAlgorithmName hashAlgorithmName, ReadOnlySpan ikm, Span output, ReadOnlySpan salt, ReadOnlySpan info) + { + int hashLength = HashLength(hashAlgorithmName); + + // Constant comes from section 2.3 (the constraint on L in the Inputs section) + int maxOkmLength = 255 * hashLength; + if (output.Length > maxOkmLength) + throw new ArgumentException(SR.Format(SR.Cryptography_Okm_TooLarge, maxOkmLength), nameof(output)); + + Debug.Assert(hashLength <= 512 / 8, "hashLength is larger than expected, consider increasing this value or using regular allocation"); + Span 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.Assert(false, "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 160 / 8; + } + else if (hashAlgorithmName == HashAlgorithmName.SHA256) + { + return 256 / 8; + } + else if (hashAlgorithmName == HashAlgorithmName.SHA384) + { + return 384 / 8; + } + else if (hashAlgorithmName == HashAlgorithmName.SHA512) + { + return 512 / 8; + } + else if (hashAlgorithmName == HashAlgorithmName.MD5) + { + return 128 / 8; + } + else + { + throw new ArgumentOutOfRangeException(nameof(hashAlgorithmName)); + } + } + } +} diff --git a/src/libraries/System.Security.Cryptography.Algorithms/src/System/Security/Cryptography/IncrementalHash.cs b/src/libraries/System.Security.Cryptography.Algorithms/src/System/Security/Cryptography/IncrementalHash.cs index a3c1553..709c560 100644 --- a/src/libraries/System.Security.Cryptography.Algorithms/src/System/Security/Cryptography/IncrementalHash.cs +++ b/src/libraries/System.Security.Cryptography.Algorithms/src/System/Security/Cryptography/IncrementalHash.cs @@ -209,6 +209,12 @@ namespace System.Security.Cryptography { if (key == null) throw new ArgumentNullException(nameof(key)); + + return CreateHMAC(hashAlgorithm, (ReadOnlySpan)key); + } + + internal static IncrementalHash CreateHMAC(HashAlgorithmName hashAlgorithm, ReadOnlySpan key) + { if (string.IsNullOrEmpty(hashAlgorithm.Name)) throw new ArgumentException(SR.Cryptography_HashAlgorithmNameNullOrEmpty, nameof(hashAlgorithm)); diff --git a/src/libraries/System.Security.Cryptography.Algorithms/tests/HKDFTests.cs b/src/libraries/System.Security.Cryptography.Algorithms/tests/HKDFTests.cs new file mode 100644 index 0000000..afd7b42 --- /dev/null +++ b/src/libraries/System.Security.Cryptography.Algorithms/tests/HKDFTests.cs @@ -0,0 +1,554 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Collections.Generic; +using System.Linq; +using Microsoft.DotNet.XUnitExtensions; +using Test.Cryptography; +using Xunit; + +namespace System.Security.Cryptography.Algorithms.Tests +{ + public abstract class HKDFTests + { + protected abstract byte[] Extract(HashAlgorithmName hash, int prkLength, byte[] ikm, byte[] salt); + protected abstract byte[] Expand(HashAlgorithmName hash, byte[] prk, int outputLength, byte[] info); + protected abstract byte[] DeriveKey(HashAlgorithmName hash, byte[] ikm, int outputLength, byte[] salt, byte[] info); + + [Theory] + [MemberData(nameof(GetRfc5869TestCases))] + public void Rfc5869ExtractTests(Rfc5869TestCase test) + { + byte[] prk = Extract(test.Hash, test.Prk.Length, test.Ikm, test.Salt); + Assert.Equal(test.Prk, prk); + } + + [Theory] + [MemberData(nameof(GetRfc5869TestCases))] + public void Rfc5869ExtractTamperHashTests(Rfc5869TestCase test) + { + byte[] prk = Extract(HashAlgorithmName.MD5, 128 / 8, test.Ikm, test.Salt); + Assert.NotEqual(test.Prk, prk); + } + + [Theory] + [MemberData(nameof(GetRfc5869TestCases))] + public void Rfc5869ExtractTamperIkmTests(Rfc5869TestCase test) + { + byte[] ikm = test.Ikm.ToArray(); + ikm[0] ^= 1; + byte[] prk = Extract(test.Hash, test.Prk.Length, ikm, test.Salt); + Assert.NotEqual(test.Prk, prk); + } + + [Theory] + [MemberData(nameof(GetRfc5869TestCasesWithNonEmptySalt))] + public void Rfc5869ExtractTamperSaltTests(Rfc5869TestCase test) + { + byte[] salt = test.Salt.ToArray(); + salt[0] ^= 1; + byte[] prk = Extract(test.Hash, test.Prk.Length, test.Ikm, salt); + Assert.NotEqual(test.Prk, prk); + } + + [Fact] + public void Rfc5869ExtractDefaultHash() + { + byte[] ikm = new byte[20]; + byte[] salt = new byte[20]; + AssertExtensions.Throws( + "hashAlgorithmName", + () => Extract(default(HashAlgorithmName), 20, ikm, salt)); + } + + [Fact] + public void Rfc5869ExtractNonsensicalHash() + { + byte[] ikm = new byte[20]; + byte[] salt = new byte[20]; + AssertExtensions.Throws( + "hashAlgorithmName", + () => Extract(new HashAlgorithmName("foo"), 20, ikm, salt)); + } + + [Fact] + public void Rfc5869ExtractEmptyIkm() + { + byte[] salt = new byte[20]; + byte[] ikm = Array.Empty(); + + // Ensure does not throw + byte[] prk = Extract(HashAlgorithmName.SHA1, 20, ikm, salt); + Assert.Equal("FBDB1D1B18AA6C08324B7D64B71FB76370690E1D", prk.ByteArrayToHex()); + } + + [Fact] + public void Rfc5869ExtractEmptySalt() + { + byte[] ikm = new byte[20]; + byte[] salt = Array.Empty(); + byte[] prk = Extract(HashAlgorithmName.SHA1, 20, ikm, salt); + Assert.Equal("A3CBF4A40F51A53E046F07397E52DF9286AE93A2", prk.ByteArrayToHex()); + } + + [Theory] + [MemberData(nameof(GetRfc5869TestCases))] + public void Rfc5869ExpandTests(Rfc5869TestCase test) + { + byte[] okm = Expand(test.Hash, test.Prk, test.Okm.Length, test.Info); + Assert.Equal(test.Okm, okm); + } + + [Fact] + public void Rfc5869ExpandDefaultHash() + { + byte[] prk = new byte[20]; + AssertExtensions.Throws( + "hashAlgorithmName", + () => Expand(default(HashAlgorithmName), prk, 20, null)); + } + + [Fact] + public void Rfc5869ExpandNonsensicalHash() + { + byte[] prk = new byte[20]; + AssertExtensions.Throws( + "hashAlgorithmName", + () => Expand(new HashAlgorithmName("foo"), prk, 20, null)); + } + + [Theory] + [MemberData(nameof(GetRfc5869TestCases))] + public void Rfc5869ExpandTamperPrkTests(Rfc5869TestCase test) + { + byte[] prk = test.Prk.ToArray(); + prk[0] ^= 1; + byte[] okm = Expand(test.Hash, prk, test.Okm.Length, test.Info); + Assert.NotEqual(test.Okm, okm); + } + + [Theory] + [MemberData(nameof(GetPrkTooShortTestCases))] + public void Rfc5869ExpandPrkTooShort(HashAlgorithmName hash, int prkSize) + { + byte[] prk = new byte[prkSize]; + AssertExtensions.Throws( + "prk", + () => Expand(hash, prk, 17, Array.Empty())); + } + + [Fact] + public void Rfc5869ExpandOkmMaxSize() + { + byte[] prk = new byte[20]; + + // Does not throw + byte[] okm = Expand(HashAlgorithmName.SHA1, prk, 20 * 255, Array.Empty()); + Assert.Equal(20 * 255, okm.Length); + } + + [Theory] + [MemberData(nameof(GetRfc5869TestCases))] + public void Rfc5869DeriveKeyTests(Rfc5869TestCase test) + { + byte[] okm = DeriveKey(test.Hash, test.Ikm, test.Okm.Length, test.Salt, test.Info); + Assert.Equal(test.Okm, okm); + } + + [Fact] + public void Rfc5869DeriveKeyDefaultHash() + { + byte[] ikm = new byte[20]; + AssertExtensions.Throws( + "hashAlgorithmName", + () => DeriveKey(default(HashAlgorithmName), ikm, 20, Array.Empty(), Array.Empty())); + } + + [Fact] + public void Rfc5869DeriveKeyNonSensicalHash() + { + byte[] ikm = new byte[20]; + AssertExtensions.Throws( + "hashAlgorithmName", + () => DeriveKey(new HashAlgorithmName("foo"), ikm, 20, Array.Empty(), Array.Empty())); + } + + [Theory] + [MemberData(nameof(GetRfc5869TestCases))] + public void Rfc5869DeriveKeyTamperIkmTests(Rfc5869TestCase test) + { + byte[] ikm = test.Ikm.ToArray(); + ikm[0] ^= 1; + byte[] okm = DeriveKey(test.Hash, ikm, test.Okm.Length, test.Salt, test.Info); + Assert.NotEqual(test.Okm, okm); + } + + [Theory] + [MemberData(nameof(GetRfc5869TestCasesWithNonEmptySalt))] + public void Rfc5869DeriveKeyTamperSaltTests(Rfc5869TestCase test) + { + byte[] salt = test.Salt.ToArray(); + salt[0] ^= 1; + byte[] okm = DeriveKey(test.Hash, test.Ikm, test.Okm.Length, salt, test.Info); + Assert.NotEqual(test.Okm, okm); + } + + [Theory] + [MemberData(nameof(GetRfc5869TestCasesWithNonEmptyInfo))] + public void Rfc5869DeriveKeyTamperInfoTests(Rfc5869TestCase test) + { + byte[] info = test.Info.ToArray(); + info[0] ^= 1; + byte[] okm = DeriveKey(test.Hash, test.Ikm, test.Okm.Length, test.Salt, info); + Assert.NotEqual(test.Okm, okm); + } + + public static IEnumerable GetRfc5869TestCases() + { + foreach (Rfc5869TestCase test in Rfc5869TestCases) + { + yield return new object[] { test }; + } + } + + public static IEnumerable GetRfc5869TestCasesWithNonEmptySalt() + { + foreach (Rfc5869TestCase test in Rfc5869TestCases) + { + if (test.Salt != null && test.Salt.Length != 0) + { + yield return new object[] { test }; + } + } + } + + public static IEnumerable GetRfc5869TestCasesWithNonEmptyInfo() + { + foreach (Rfc5869TestCase test in Rfc5869TestCases) + { + if (test.Info != null && test.Info.Length != 0) + { + yield return new object[] { test }; + } + } + } + + public static IEnumerable GetPrkTooShortTestCases() + { + yield return new object[] { HashAlgorithmName.SHA1, 0 }; + yield return new object[] { HashAlgorithmName.SHA1, 1 }; + yield return new object[] { HashAlgorithmName.SHA1, 160 / 8 - 1 }; + yield return new object[] { HashAlgorithmName.SHA256, 256 / 8 - 1 }; + yield return new object[] { HashAlgorithmName.SHA512, 512 / 8 - 1 }; + yield return new object[] { HashAlgorithmName.MD5, 128 / 8 - 1 }; + } + + private static Rfc5869TestCase[] Rfc5869TestCases { get; } = new Rfc5869TestCase[7] + { + new Rfc5869TestCase() + { + Name = "Basic test case with SHA-256", + Hash = HashAlgorithmName.SHA256, + Ikm = "0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b".HexToByteArray(), + Salt = "000102030405060708090a0b0c".HexToByteArray(), + Info = "f0f1f2f3f4f5f6f7f8f9".HexToByteArray(), + Prk = ( + "077709362c2e32df0ddc3f0dc47bba63" + + "90b6c73bb50f9c3122ec844ad7c2b3e5").HexToByteArray(), + Okm = ( + "3cb25f25faacd57a90434f64d0362f2a" + + "2d2d0a90cf1a5a4c5db02d56ecc4c5bf" + + "34007208d5b887185865").HexToByteArray(), + }, + new Rfc5869TestCase() + { + Name = "Test with SHA-256 and longer inputs/outputs", + Hash = HashAlgorithmName.SHA256, + Ikm = ( + "000102030405060708090a0b0c0d0e0f" + + "101112131415161718191a1b1c1d1e1f" + + "202122232425262728292a2b2c2d2e2f" + + "303132333435363738393a3b3c3d3e3f" + + "404142434445464748494a4b4c4d4e4f").HexToByteArray(), + Salt = ( + "606162636465666768696a6b6c6d6e6f" + + "707172737475767778797a7b7c7d7e7f" + + "808182838485868788898a8b8c8d8e8f" + + "909192939495969798999a9b9c9d9e9f" + + "a0a1a2a3a4a5a6a7a8a9aaabacadaeaf").HexToByteArray(), + Info = ( + "b0b1b2b3b4b5b6b7b8b9babbbcbdbebf" + + "c0c1c2c3c4c5c6c7c8c9cacbcccdcecf" + + "d0d1d2d3d4d5d6d7d8d9dadbdcdddedf" + + "e0e1e2e3e4e5e6e7e8e9eaebecedeeef" + + "f0f1f2f3f4f5f6f7f8f9fafbfcfdfeff").HexToByteArray(), + Prk = ( + "06a6b88c5853361a06104c9ceb35b45c" + + "ef760014904671014a193f40c15fc244").HexToByteArray(), + Okm = ( + "b11e398dc80327a1c8e7f78c596a4934" + + "4f012eda2d4efad8a050cc4c19afa97c" + + "59045a99cac7827271cb41c65e590e09" + + "da3275600c2f09b8367793a9aca3db71" + + "cc30c58179ec3e87c14c01d5c1f3434f" + + "1d87").HexToByteArray(), + }, + new Rfc5869TestCase() + { + Name = "Test with SHA-256 and zero-length salt/info", + Hash = HashAlgorithmName.SHA256, + Ikm = "0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b".HexToByteArray(), + Salt = Array.Empty(), + Info = Array.Empty(), + Prk = ( + "19ef24a32c717b167f33a91d6f648bdf" + + "96596776afdb6377ac434c1c293ccb04").HexToByteArray(), + Okm = ( + "8da4e775a563c18f715f802a063c5a31" + + "b8a11f5c5ee1879ec3454e5f3c738d2d" + + "9d201395faa4b61a96c8").HexToByteArray(), + }, + new Rfc5869TestCase() + { + Name = "Basic test case with SHA-1", + Hash = HashAlgorithmName.SHA1, + Ikm = "0b0b0b0b0b0b0b0b0b0b0b".HexToByteArray(), + Salt = "000102030405060708090a0b0c".HexToByteArray(), + Info = "f0f1f2f3f4f5f6f7f8f9".HexToByteArray(), + Prk = "9b6c18c432a7bf8f0e71c8eb88f4b30baa2ba243".HexToByteArray(), + Okm = ( + "085a01ea1b10f36933068b56efa5ad81" + + "a4f14b822f5b091568a9cdd4f155fda2" + + "c22e422478d305f3f896").HexToByteArray(), + }, + new Rfc5869TestCase() + { + Name = "Test with SHA-1 and longer inputs/outputs", + Hash = HashAlgorithmName.SHA1, + Ikm = ( + "000102030405060708090a0b0c0d0e0f" + + "101112131415161718191a1b1c1d1e1f" + + "202122232425262728292a2b2c2d2e2f" + + "303132333435363738393a3b3c3d3e3f" + + "404142434445464748494a4b4c4d4e4f").HexToByteArray(), + Salt = ( + "606162636465666768696a6b6c6d6e6f" + + "707172737475767778797a7b7c7d7e7f" + + "808182838485868788898a8b8c8d8e8f" + + "909192939495969798999a9b9c9d9e9f" + + "a0a1a2a3a4a5a6a7a8a9aaabacadaeaf").HexToByteArray(), + Info = ( + "b0b1b2b3b4b5b6b7b8b9babbbcbdbebf" + + "c0c1c2c3c4c5c6c7c8c9cacbcccdcecf" + + "d0d1d2d3d4d5d6d7d8d9dadbdcdddedf" + + "e0e1e2e3e4e5e6e7e8e9eaebecedeeef" + + "f0f1f2f3f4f5f6f7f8f9fafbfcfdfeff").HexToByteArray(), + Prk = "8adae09a2a307059478d309b26c4115a224cfaf6".HexToByteArray(), + Okm = ( + "0bd770a74d1160f7c9f12cd5912a06eb" + + "ff6adcae899d92191fe4305673ba2ffe" + + "8fa3f1a4e5ad79f3f334b3b202b2173c" + + "486ea37ce3d397ed034c7f9dfeb15c5e" + + "927336d0441f4c4300e2cff0d0900b52" + + "d3b4").HexToByteArray(), + }, + new Rfc5869TestCase() + { + Name = "Test with SHA-1 and zero-length salt/info", + Hash = HashAlgorithmName.SHA1, + Ikm = "0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b".HexToByteArray(), + Salt = Array.Empty(), + Info = Array.Empty(), + Prk = "da8c8a73c7fa77288ec6f5e7c297786aa0d32d01".HexToByteArray(), + Okm = ( + "0ac1af7002b3d761d1e55298da9d0506" + + "b9ae52057220a306e07b6b87e8df21d0" + + "ea00033de03984d34918").HexToByteArray(), + }, + new Rfc5869TestCase() + { + Name = "Test with SHA-1, salt not provided (defaults to HashLen zero octets), zero-length info", + Hash = HashAlgorithmName.SHA1, + Ikm = "0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c".HexToByteArray(), + Salt = null, + Info = Array.Empty(), + Prk = "2adccada18779e7c2077ad2eb19d3f3e731385dd".HexToByteArray(), + Okm = ( + "2c91117204d745f3500d636a62f64f0a" + + "b3bae548aa53d423b0d1f27ebba6f5e5" + + "673a081d70cce7acfc48").HexToByteArray(), + }, + }; + + public struct Rfc5869TestCase + { + public string Name { get; set; } + public HashAlgorithmName Hash { get; set; } + public byte[] Ikm { get; set; } + public byte[] Salt { get; set; } + public byte[] Info { get; set; } + public byte[] Prk { get; set; } + public byte[] Okm { get; set; } + + public override string ToString() => Name; + } + + public class HkdfByteArrayTests : HKDFTests + { + protected override byte[] Extract(HashAlgorithmName hash, int prkLength, byte[] ikm, byte[] salt) + { + return HKDF.Extract(hash, ikm, salt); + } + + protected override byte[] Expand(HashAlgorithmName hash, byte[] prk, int outputLength, byte[] info) + { + return HKDF.Expand(hash, prk, outputLength, info); + } + + protected override byte[] DeriveKey(HashAlgorithmName hash, byte[] ikm, int outputLength, byte[] salt, byte[] info) + { + return HKDF.DeriveKey(hash, ikm, outputLength, salt, info); + } + + [Fact] + public void Rfc5869ExtractNullIkm() + { + byte[] salt = new byte[20]; + AssertExtensions.Throws( + "ikm", + () => HKDF.Extract(HashAlgorithmName.SHA1, null, salt)); + } + + [Fact] + public void Rfc5869ExpandOkmMaxSizePlusOne() + { + byte[] prk = new byte[20]; + AssertExtensions.Throws( + "outputLength", + () => HKDF.Expand(HashAlgorithmName.SHA1, prk, 20 * 255 + 1, Array.Empty())); + } + + [Fact] + public void Rfc5869ExpandOkmPotentiallyOverflowingValue() + { + byte[] prk = new byte[20]; + AssertExtensions.Throws( + "outputLength", + () => HKDF.Expand(HashAlgorithmName.SHA1, prk, 8421505, Array.Empty())); + } + + [Fact] + public void Rfc5869DeriveKeyNullIkm() + { + AssertExtensions.Throws( + "ikm", + () => HKDF.DeriveKey(HashAlgorithmName.SHA1, null, 20, Array.Empty(), Array.Empty())); + } + + [Fact] + public void Rfc5869DeriveKeyOkmMaxSizePlusOne() + { + byte[] ikm = new byte[20]; + AssertExtensions.Throws( + "outputLength", + () => HKDF.DeriveKey(HashAlgorithmName.SHA1, ikm, 20 * 255 + 1, Array.Empty(), Array.Empty())); + } + + [Fact] + public void Rfc5869DeriveKeyOkmPotentiallyOverflowingValue() + { + byte[] ikm = new byte[20]; + AssertExtensions.Throws( + "outputLength", + () => HKDF.DeriveKey(HashAlgorithmName.SHA1, ikm, 8421505, Array.Empty(), Array.Empty())); + } + } + + public class HkdfSpanTests : HKDFTests + { + protected override byte[] Extract(HashAlgorithmName hash, int prkLength, byte[] ikm, byte[] salt) + { + byte[] prk = new byte[prkLength]; + Assert.Equal(prkLength, HKDF.Extract(hash, ikm, salt, prk)); + return prk; + } + + protected override byte[] Expand(HashAlgorithmName hash, byte[] prk, int outputLength, byte[] info) + { + byte[] output = new byte[outputLength]; + HKDF.Expand(hash, prk, output, info); + return output; + } + + protected override byte[] DeriveKey(HashAlgorithmName hash, byte[] ikm, int outputLength, byte[] salt, byte[] info) + { + byte[] output = new byte[outputLength]; + HKDF.DeriveKey(hash, ikm, output, salt, info); + return output; + } + + [Fact] + public void Rfc5869ExtractPrkTooLong() + { + byte[] prk = new byte[24]; + + for (int i = 0; i < 4; i++) + { + prk[20 + i] = (byte)(i + 5); + } + + byte[] ikm = new byte[20]; + byte[] salt = new byte[20]; + Assert.Equal(20, HKDF.Extract(HashAlgorithmName.SHA1, ikm, salt, prk)); + Assert.Equal("A3CBF4A40F51A53E046F07397E52DF9286AE93A2", prk.AsSpan(0, 20).ByteArrayToHex()); + + for (int i = 0; i < 4; i++) + { + // ensure we didn't modify anything further + Assert.Equal((byte)(i + 5), prk[20 + i]); + } + } + + [Fact] + public void Rfc5869OkmMaxSizePlusOne() + { + byte[] prk = new byte[20]; + byte[] okm = new byte[20 * 255 + 1]; + AssertExtensions.Throws( + "output", + () => HKDF.Expand(HashAlgorithmName.SHA1, prk, okm, Array.Empty())); + } + + [Fact] + public void Rfc5869OkmMaxSizePotentiallyOverflowingValue() + { + byte[] prk = new byte[20]; + byte[] okm = new byte[8421505]; + AssertExtensions.Throws( + "output", + () => HKDF.Expand(HashAlgorithmName.SHA1, prk, okm, Array.Empty())); + } + + [Fact] + public void Rfc5869DeriveKeySpanOkmMaxSizePlusOne() + { + byte[] ikm = new byte[20]; + byte[] okm = new byte[20 * 255 + 1]; + AssertExtensions.Throws( + "output", + () => HKDF.DeriveKey(HashAlgorithmName.SHA1, ikm, okm, Array.Empty(), Array.Empty())); + } + + [Fact] + public void Rfc5869DeriveKeySpanOkmPotentiallyOverflowingValue() + { + byte[] ikm = new byte[20]; + byte[] okm = new byte[8421505]; + AssertExtensions.Throws( + "output", + () => HKDF.DeriveKey(HashAlgorithmName.SHA1, ikm, okm, Array.Empty(), Array.Empty())); + } + } + } +} diff --git a/src/libraries/System.Security.Cryptography.Algorithms/tests/System.Security.Cryptography.Algorithms.Tests.csproj b/src/libraries/System.Security.Cryptography.Algorithms/tests/System.Security.Cryptography.Algorithms.Tests.csproj index a7109d0..ec3598c 100644 --- a/src/libraries/System.Security.Cryptography.Algorithms/tests/System.Security.Cryptography.Algorithms.Tests.csproj +++ b/src/libraries/System.Security.Cryptography.Algorithms/tests/System.Security.Cryptography.Algorithms.Tests.csproj @@ -268,5 +268,6 @@ + -- 2.7.4