Skip to content

Commit bb9df87

Browse files
authored
Implement HKDF with CryptoKit on Apple
1 parent f978d76 commit bb9df87

File tree

10 files changed

+484
-12
lines changed

10 files changed

+484
-12
lines changed
Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
using System;
5+
using System.Diagnostics;
6+
using System.Runtime.CompilerServices;
7+
using System.Runtime.InteropServices;
8+
using System.Runtime.InteropServices.Swift;
9+
using System.Runtime.Versioning;
10+
using System.Security.Cryptography;
11+
using System.Security.Cryptography.Apple;
12+
using Swift.Runtime;
13+
14+
#pragma warning disable CS3016 // Arrays as attribute arguments are not CLS Compliant
15+
16+
internal static partial class Interop
17+
{
18+
internal static partial class AppleCrypto
19+
{
20+
internal static unsafe void HkdfExpand(
21+
HashAlgorithmName hashAlgorithm,
22+
ReadOnlySpan<byte> prk,
23+
ReadOnlySpan<byte> info,
24+
Span<byte> destination)
25+
{
26+
Debug.Assert(!destination.IsEmpty);
27+
Debug.Assert(!prk.IsEmpty);
28+
29+
PAL_HashAlgorithm algorithm = PalAlgorithmFromAlgorithmName(hashAlgorithm);
30+
31+
int ret = AppleCryptoNative_HKDFExpand(
32+
algorithm,
33+
prk,
34+
prk.Length,
35+
info,
36+
info.Length,
37+
destination,
38+
destination.Length);
39+
40+
if (ret < 0)
41+
{
42+
throw new CryptographicException();
43+
}
44+
45+
Debug.Assert(ret == destination.Length);
46+
}
47+
48+
internal static unsafe void HKDFExtract(
49+
HashAlgorithmName hashAlgorithm,
50+
ReadOnlySpan<byte> ikm,
51+
ReadOnlySpan<byte> salt,
52+
Span<byte> destination)
53+
{
54+
Debug.Assert(!destination.IsEmpty);
55+
56+
PAL_HashAlgorithm algorithm = PalAlgorithmFromAlgorithmName(hashAlgorithm);
57+
58+
int ret = AppleCryptoNative_HKDFExtract(
59+
algorithm,
60+
ikm,
61+
ikm.Length,
62+
salt,
63+
salt.Length,
64+
destination,
65+
destination.Length);
66+
67+
if (ret < 0)
68+
{
69+
throw new CryptographicException();
70+
}
71+
72+
Debug.Assert(ret == destination.Length);
73+
}
74+
75+
internal static unsafe void HKDFDeriveKey(
76+
HashAlgorithmName hashAlgorithm,
77+
ReadOnlySpan<byte> ikm,
78+
ReadOnlySpan<byte> salt,
79+
ReadOnlySpan<byte> info,
80+
Span<byte> destination)
81+
{
82+
Debug.Assert(!destination.IsEmpty);
83+
84+
PAL_HashAlgorithm algorithm = PalAlgorithmFromAlgorithmName(hashAlgorithm);
85+
86+
int ret = AppleCryptoNative_HKDFDeriveKey(
87+
algorithm,
88+
ikm,
89+
ikm.Length,
90+
salt,
91+
salt.Length,
92+
info,
93+
info.Length,
94+
destination,
95+
destination.Length);
96+
97+
if (ret < 0)
98+
{
99+
throw new CryptographicException();
100+
}
101+
102+
Debug.Assert(ret == destination.Length);
103+
}
104+
105+
[LibraryImport(Libraries.AppleCryptoNative)]
106+
[UnmanagedCallConv(CallConvs = [ typeof(CallConvSwift) ])]
107+
private static partial int AppleCryptoNative_HKDFExpand(
108+
PAL_HashAlgorithm hashAlgorithm,
109+
ReadOnlySpan<byte> prkPtr,
110+
int prkLength,
111+
ReadOnlySpan<byte> infoPtr,
112+
int infoLength,
113+
Span<byte> destinationPtr,
114+
int destinationLength);
115+
116+
[LibraryImport(Libraries.AppleCryptoNative)]
117+
[UnmanagedCallConv(CallConvs = [ typeof(CallConvSwift) ])]
118+
private static partial int AppleCryptoNative_HKDFExtract(
119+
PAL_HashAlgorithm hashAlgorithm,
120+
ReadOnlySpan<byte> ikmPtr,
121+
int ikmLength,
122+
ReadOnlySpan<byte> saltPtr,
123+
int saltLength,
124+
Span<byte> destinationPtr,
125+
int destinationLength);
126+
127+
[LibraryImport(Libraries.AppleCryptoNative)]
128+
[UnmanagedCallConv(CallConvs = [ typeof(CallConvSwift) ])]
129+
private static partial int AppleCryptoNative_HKDFDeriveKey(
130+
PAL_HashAlgorithm hashAlgorithm,
131+
ReadOnlySpan<byte> ikmPtr,
132+
int ikmLength,
133+
ReadOnlySpan<byte> saltPtr,
134+
int saltLength,
135+
ReadOnlySpan<byte> infoPtr,
136+
int infoLength,
137+
Span<byte> destinationPtr,
138+
int destinationLength);
139+
}
140+
}

src/libraries/Common/src/Interop/OSX/System.Security.Cryptography.Native.Apple/Interop.PAL_HashAlgorithm.cs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
// Licensed to the .NET Foundation under one or more agreements.
22
// The .NET Foundation licenses this file to you under the MIT license.
33

4+
using System;
5+
using System.Security.Cryptography;
6+
47
internal static partial class Interop
58
{
69
internal static partial class AppleCrypto
@@ -14,5 +17,13 @@ internal enum PAL_HashAlgorithm
1417
Sha384,
1518
Sha512,
1619
}
20+
21+
private static PAL_HashAlgorithm PalAlgorithmFromAlgorithmName(HashAlgorithmName hashAlgorithmName) =>
22+
hashAlgorithmName == HashAlgorithmName.MD5 ? PAL_HashAlgorithm.Md5 :
23+
hashAlgorithmName == HashAlgorithmName.SHA1 ? PAL_HashAlgorithm.Sha1 :
24+
hashAlgorithmName == HashAlgorithmName.SHA256 ? PAL_HashAlgorithm.Sha256 :
25+
hashAlgorithmName == HashAlgorithmName.SHA384 ? PAL_HashAlgorithm.Sha384 :
26+
hashAlgorithmName == HashAlgorithmName.SHA512 ? PAL_HashAlgorithm.Sha512 :
27+
throw new CryptographicException(SR.Cryptography_UnknownHashAlgorithm, hashAlgorithmName.Name);
1728
}
1829
}

src/libraries/Common/src/Interop/OSX/System.Security.Cryptography.Native.Apple/Interop.RSA.cs

Lines changed: 0 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -348,13 +348,5 @@ ref MemoryMarshal.GetReference(source),
348348

349349
return ProcessPrimitiveResponse(returnValue, cfData, cfError, destination, out bytesWritten);
350350
}
351-
352-
private static PAL_HashAlgorithm PalAlgorithmFromAlgorithmName(HashAlgorithmName hashAlgorithmName) =>
353-
hashAlgorithmName == HashAlgorithmName.MD5 ? PAL_HashAlgorithm.Md5 :
354-
hashAlgorithmName == HashAlgorithmName.SHA1 ? PAL_HashAlgorithm.Sha1 :
355-
hashAlgorithmName == HashAlgorithmName.SHA256 ? PAL_HashAlgorithm.Sha256 :
356-
hashAlgorithmName == HashAlgorithmName.SHA384 ? PAL_HashAlgorithm.Sha384 :
357-
hashAlgorithmName == HashAlgorithmName.SHA512 ? PAL_HashAlgorithm.Sha512 :
358-
throw new CryptographicException(SR.Cryptography_UnknownHashAlgorithm, hashAlgorithmName.Name);
359351
}
360352
}

src/libraries/System.Security.Cryptography/src/System.Security.Cryptography.csproj

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1332,6 +1332,8 @@
13321332
Link="Common\Interop\OSX\System.Security.Cryptography.Native.Apple\Interop.Ecc.cs" />
13331333
<Compile Include="$(CommonPath)Interop\OSX\System.Security.Cryptography.Native.Apple\Interop.Err.cs"
13341334
Link="Common\Interop\OSX\System.Security.Cryptography.Native.Apple\Interop.Err.cs" />
1335+
<Compile Include="$(CommonPath)Interop\OSX\System.Security.Cryptography.Native.Apple\Interop.HKDF.cs"
1336+
Link="Common\Interop\OSX\System.Security.Cryptography.Native.Apple\Interop.HKDF.cs" />
13351337
<Compile Include="$(CommonPath)Interop\OSX\System.Security.Cryptography.Native.Apple\Interop.Hmac.cs"
13361338
Link="Common\Interop\OSX\System.Security.Cryptography.Native.Apple\Interop.Hmac.cs" />
13371339
<Compile Include="$(CommonPath)Interop\OSX\System.Security.Cryptography.Native.Apple\Interop.KeyAgree.cs"
@@ -1404,7 +1406,7 @@
14041406
<Compile Include="System\Security\Cryptography\ECDsa.Create.AppleCrypto.cs" />
14051407
<Compile Include="System\Security\Cryptography\HashAlgorithmNames.Apple.cs" />
14061408
<Compile Include="System\Security\Cryptography\HashProviderDispenser.Apple.cs" />
1407-
<Compile Include="System\Security\Cryptography\HKDF.Managed.cs" />
1409+
<Compile Include="System\Security\Cryptography\HKDF.Apple.cs" />
14081410
<Compile Include="System\Security\Cryptography\LiteHash.Apple.cs" />
14091411
<Compile Include="System\Security\Cryptography\LiteHash.Kmac.Unsupported.cs" />
14101412
<Compile Include="System\Security\Cryptography\MLDsaOpenSsl.NotSupported.cs" />
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
using System.Diagnostics;
5+
6+
namespace System.Security.Cryptography
7+
{
8+
public static partial class HKDF
9+
{
10+
private static readonly bool s_hasCryptoKitImplementation =
11+
OperatingSystem.IsMacOS() ||
12+
OperatingSystem.IsMacCatalyst() ||
13+
OperatingSystem.IsIOSVersionAtLeast(14) ||
14+
OperatingSystem.IsTvOSVersionAtLeast(14);
15+
16+
private static void Extract(
17+
HashAlgorithmName hashAlgorithmName,
18+
int hashLength,
19+
ReadOnlySpan<byte> ikm,
20+
ReadOnlySpan<byte> salt,
21+
Span<byte> prk)
22+
{
23+
ThrowForUnsupportedHashAlgorithm(hashAlgorithmName);
24+
25+
if (s_hasCryptoKitImplementation)
26+
{
27+
Interop.AppleCrypto.HKDFExtract(hashAlgorithmName, ikm, salt, prk);
28+
}
29+
else
30+
{
31+
HKDFManagedImplementation.Extract(hashAlgorithmName, hashLength, ikm, salt, prk);
32+
}
33+
}
34+
35+
private static void Expand(
36+
HashAlgorithmName hashAlgorithmName,
37+
int hashLength,
38+
ReadOnlySpan<byte> prk,
39+
Span<byte> output,
40+
ReadOnlySpan<byte> info)
41+
{
42+
ThrowForUnsupportedHashAlgorithm(hashAlgorithmName);
43+
44+
if (s_hasCryptoKitImplementation)
45+
{
46+
Interop.AppleCrypto.HkdfExpand(hashAlgorithmName, prk, info, output);
47+
}
48+
else
49+
{
50+
HKDFManagedImplementation.Expand(hashAlgorithmName, hashLength, prk, output, info);
51+
}
52+
}
53+
54+
private static void DeriveKeyCore(
55+
HashAlgorithmName hashAlgorithmName,
56+
int hashLength,
57+
ReadOnlySpan<byte> ikm,
58+
Span<byte> output,
59+
ReadOnlySpan<byte> salt,
60+
ReadOnlySpan<byte> info)
61+
{
62+
ThrowForUnsupportedHashAlgorithm(hashAlgorithmName);
63+
64+
if (s_hasCryptoKitImplementation)
65+
{
66+
Interop.AppleCrypto.HKDFDeriveKey(hashAlgorithmName, ikm, salt, info, output);
67+
}
68+
else
69+
{
70+
HKDFManagedImplementation.DeriveKey(hashAlgorithmName, hashLength, ikm, output, salt, info);
71+
}
72+
}
73+
74+
private static void ThrowForUnsupportedHashAlgorithm(HashAlgorithmName hashAlgorithmName)
75+
{
76+
if (hashAlgorithmName == HashAlgorithmName.SHA3_256 || hashAlgorithmName == HashAlgorithmName.SHA3_384 ||
77+
hashAlgorithmName == HashAlgorithmName.SHA3_512)
78+
{
79+
throw new PlatformNotSupportedException();
80+
}
81+
82+
// Unknown algorithms are handled outside of this as a CryptographicException. SHA-3 is known, it's just
83+
// not supported.
84+
}
85+
}
86+
}

src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/HKDF.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -171,7 +171,7 @@ public static byte[] DeriveKey(HashAlgorithmName hashAlgorithmName, byte[] ikm,
171171
/// <param name="output">The output buffer representing output keying material.</param>
172172
/// <param name="salt">The salt value (a non-secret random value).</param>
173173
/// <param name="info">The context and application specific information (can be an empty span).</param>
174-
/// <exception cref="ArgumentException"><paramref name="ikm"/> is empty, or is larger than the maximum allowed length.</exception>
174+
/// <exception cref="ArgumentException"><paramref name="output"/> is empty, or is larger than the maximum allowed length.</exception>
175175
public static void DeriveKey(HashAlgorithmName hashAlgorithmName, ReadOnlySpan<byte> ikm, Span<byte> output, ReadOnlySpan<byte> salt, ReadOnlySpan<byte> info)
176176
{
177177
int hashLength = Helpers.HashLength(hashAlgorithmName);

src/libraries/System.Security.Cryptography/tests/HKDFTests.cs

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ public abstract class HKDFTests
1919

2020
[Theory]
2121
[MemberData(nameof(GetHkdfTestCases))]
22+
[MemberData(nameof(SupplementalTestCases))]
2223
public void ExtractTests(HkdfTestCase test)
2324
{
2425
byte[] prk = Extract(test.Hash, test.Prk.Length, test.Ikm, test.Salt);
@@ -95,6 +96,7 @@ public void ExtractEmptySalt()
9596

9697
[Theory]
9798
[MemberData(nameof(GetHkdfTestCases))]
99+
[MemberData(nameof(SupplementalTestCases))]
98100
public void ExpandTests(HkdfTestCase test)
99101
{
100102
byte[] okm = Expand(test.Hash, test.Prk, test.Okm.Length, test.Info);
@@ -151,6 +153,7 @@ public void ExpandOkmMaxSize()
151153

152154
[Theory]
153155
[MemberData(nameof(GetHkdfTestCases))]
156+
[MemberData(nameof(SupplementalTestCases))]
154157
public void DeriveKeyTests(HkdfTestCase test)
155158
{
156159
byte[] okm = DeriveKey(test.Hash, test.Ikm, test.Okm.Length, test.Salt, test.Info);
@@ -409,6 +412,80 @@ public static IEnumerable<object[]> GetPrkTooShortTestCases()
409412
},
410413
};
411414

415+
public static IEnumerable<object[]> SupplementalTestCases
416+
{
417+
get
418+
{
419+
if (MD5Supported)
420+
{
421+
yield return new object[]
422+
{
423+
// Generated
424+
// openssl kdf -keylen 16 -kdfopt digest:MD5 -kdfopt hexkey:000102030405060708090A0B0C0D0E0F \
425+
// -kdfopt hexsalt:101112131415161718191A1B1C1D1E1F -kdfopt hexinfo:202122232425262728292A2B2C2D2E2F \
426+
// -binary HKDF | xxd -p
427+
new HkdfTestCase()
428+
{
429+
Name = "Test with MD5, salt, and info",
430+
Hash = HashAlgorithmName.MD5,
431+
Ikm = "000102030405060708090A0B0C0D0E0F".HexToByteArray(),
432+
Salt = "101112131415161718191A1B1C1D1E1F".HexToByteArray(),
433+
Info = "202122232425262728292A2B2C2D2E2F".HexToByteArray(),
434+
Okm = "5a25e9d9d27578f28a79a680fd9ce780".HexToByteArray(),
435+
436+
// Add -kdfopt mode:EXTRACT_ONLY to derive the PRK.
437+
Prk = "2d2d573fd48c9ad0be5e8214af0d7d64".HexToByteArray(),
438+
439+
}
440+
};
441+
}
442+
443+
yield return new object[]
444+
{
445+
// Generated
446+
// openssl kdf -keylen 16 -kdfopt digest:SHA384 -kdfopt hexkey:000102030405060708090A0B0C0D0E0F \
447+
// -kdfopt hexsalt:101112131415161718191A1B1C1D1E1F -kdfopt hexinfo:202122232425262728292A2B2C2D2E2F \
448+
// -binary HKDF | xxd -p
449+
new HkdfTestCase()
450+
{
451+
Name = "Test with SHA-2-384, salt, and info",
452+
Hash = HashAlgorithmName.SHA384,
453+
Ikm = "000102030405060708090A0B0C0D0E0F".HexToByteArray(),
454+
Salt = "101112131415161718191A1B1C1D1E1F".HexToByteArray(),
455+
Info = "202122232425262728292A2B2C2D2E2F".HexToByteArray(),
456+
Okm = "5b3a502e2ce5d366479afa17a4fe4aaa".HexToByteArray(),
457+
458+
// Add -kdfopt mode:EXTRACT_ONLY to derive the PRK.
459+
Prk = ("31ca88a527220f8271d78df4ce6c4d973f135ad37973b966" +
460+
"44b4d52d499d0a2b03d53c875b1176b089e1e6161ab6d92b").HexToByteArray(),
461+
462+
}
463+
};
464+
465+
yield return new object[]
466+
{
467+
// Generated
468+
// openssl kdf -keylen 16 -kdfopt digest:SHA512 -kdfopt hexkey:000102030405060708090A0B0C0D0E0F \
469+
// -kdfopt hexsalt:101112131415161718191A1B1C1D1E1F -kdfopt hexinfo:202122232425262728292A2B2C2D2E2F \
470+
// -binary HKDF | xxd -p
471+
new HkdfTestCase()
472+
{
473+
Name = "Test with SHA-2-512, salt, and info",
474+
Hash = HashAlgorithmName.SHA512,
475+
Ikm = "000102030405060708090A0B0C0D0E0F".HexToByteArray(),
476+
Salt = "101112131415161718191A1B1C1D1E1F".HexToByteArray(),
477+
Info = "202122232425262728292A2B2C2D2E2F".HexToByteArray(),
478+
Okm = "270b6ba8a5989e4c26c8d116930c14ab".HexToByteArray(),
479+
480+
// Add -kdfopt mode:EXTRACT_ONLY to derive the PRK.
481+
Prk = ("f6e6b1ddb24ea0f0ede0f533d1f350c86bf78966b0e5fd2af34dd00dae3901d6" +
482+
"279fe8111d6572e3cd05f2f0eeabb9144dc0da9437cdf37b0c6d7f3b1064ab2b").HexToByteArray(),
483+
484+
}
485+
};
486+
}
487+
}
488+
412489
public static IEnumerable<object[]> Sha3TestCases
413490
{
414491
// These cases were generated from the openssl kdf command.

0 commit comments

Comments
 (0)