Skip to content

Commit 8433b62

Browse files
ML-DSA certificate accessors for M.B.C. (#117907)
1 parent 09aa370 commit 8433b62

File tree

16 files changed

+1258
-859
lines changed

16 files changed

+1258
-859
lines changed

src/libraries/Common/src/Interop/Windows/Crypt32/Interop.CertSetCertificateContextProperty_CRYPT_KEY_PROV_INFO.cs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,5 +11,9 @@ internal static partial class Crypt32
1111
[LibraryImport(Libraries.Crypt32, SetLastError = true)]
1212
[return: MarshalAs(UnmanagedType.Bool)]
1313
internal static unsafe partial bool CertSetCertificateContextProperty(SafeCertContextHandle pCertContext, CertContextPropId dwPropId, CertSetPropertyFlags dwFlags, CRYPT_KEY_PROV_INFO* pvData);
14+
15+
[LibraryImport(Libraries.Crypt32, SetLastError = true)]
16+
[return: MarshalAs(UnmanagedType.Bool)]
17+
internal static unsafe partial bool CertSetCertificateContextProperty(nint pCertContext, CertContextPropId dwPropId, CertSetPropertyFlags dwFlags, CRYPT_KEY_PROV_INFO* pvData);
1418
}
1519
}

src/libraries/Common/src/Interop/Windows/Crypt32/Interop.CertSetCertificateContextProperty_SafeNCryptKeyHandle.cs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,5 +11,9 @@ internal static partial class Crypt32
1111
[LibraryImport(Libraries.Crypt32, SetLastError = true)]
1212
[return: MarshalAs(UnmanagedType.Bool)]
1313
internal static unsafe partial bool CertSetCertificateContextProperty(SafeCertContextHandle pCertContext, CertContextPropId dwPropId, CertSetPropertyFlags dwFlags, SafeNCryptKeyHandle keyHandle);
14+
15+
[LibraryImport(Libraries.Crypt32, SetLastError = true)]
16+
[return: MarshalAs(UnmanagedType.Bool)]
17+
internal static unsafe partial bool CertSetCertificateContextProperty(nint pCertContext, CertContextPropId dwPropId, CertSetPropertyFlags dwFlags, SafeNCryptKeyHandle keyHandle);
1418
}
1519
}

src/libraries/Common/src/System/Security/Cryptography/MLDsaImplementation.CreateCng.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,9 @@ namespace System.Security.Cryptography
77
{
88
internal sealed partial class MLDsaImplementation
99
{
10+
#if !SYSTEM_SECURITY_CRYPTOGRAPHY
1011
[SupportedOSPlatform("windows")]
12+
#endif
1113
internal CngKey CreateEphemeralCng()
1214
{
1315
string bcryptBlobType =
Lines changed: 333 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,333 @@
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+
using System.Runtime.InteropServices;
6+
using System.Runtime.Versioning;
7+
using Internal.Cryptography;
8+
using Microsoft.Win32.SafeHandles;
9+
10+
#if SYSTEM_SECURITY_CRYPTOGRAPHY
11+
using TCertificate = System.Security.Cryptography.X509Certificates.CertificatePal;
12+
#else
13+
using TCertificate = System.Security.Cryptography.X509Certificates.X509Certificate2;
14+
#endif
15+
16+
namespace System.Security.Cryptography.X509Certificates
17+
{
18+
internal static partial class CertificateHelpers
19+
{
20+
private static partial CryptographicException GetExceptionForLastError();
21+
22+
private static partial SafeNCryptKeyHandle CreateSafeNCryptKeyHandle(IntPtr handle, SafeHandle parentHandle);
23+
24+
private static partial TCertificate CopyFromRawBytes(TCertificate certificate);
25+
26+
private static partial int GuessKeySpec(CngProvider provider, string keyName, bool machineKey, CngAlgorithmGroup? algorithmGroup);
27+
28+
#if !SYSTEM_SECURITY_CRYPTOGRAPHY
29+
[SupportedOSPlatform("windows")]
30+
#endif
31+
internal static TCertificate CopyWithPrivateKey(TCertificate certificate, MLDsa privateKey)
32+
{
33+
if (privateKey is MLDsaCng mldsaCng)
34+
{
35+
CngKey key = mldsaCng.KeyNoDuplicate;
36+
37+
TCertificate? clone = CopyWithPersistedCngKey(certificate, key);
38+
39+
if (clone is not null)
40+
{
41+
return clone;
42+
}
43+
}
44+
45+
if (privateKey is MLDsaImplementation mldsaImplementation)
46+
{
47+
using (CngKey clonedKey = mldsaImplementation.CreateEphemeralCng())
48+
{
49+
return CopyWithEphemeralKey(certificate, clonedKey);
50+
}
51+
}
52+
53+
// MLDsaCng and third-party implementations can be copied by exporting the PKCS#8 and importing it into
54+
// a new MLDsaCng. An alternative to PKCS#8 would be to try the private seed and fall back to secret key,
55+
// but that potentially requires two calls and wouldn't allow implementations to do anything smarter internally.
56+
// Blobs may also be an option for MLDsaCng, but for now we will stick with PKCS#8.
57+
byte[] exportedPkcs8 = privateKey.ExportPkcs8PrivateKey();
58+
59+
using (PinAndClear.Track(exportedPkcs8))
60+
using (MLDsaCng clonedKey = MLDsaCng.ImportPkcs8PrivateKey(exportedPkcs8, out _))
61+
{
62+
CngKey clonedCngKey = clonedKey.KeyNoDuplicate;
63+
64+
if (clonedCngKey.AlgorithmGroup != CngAlgorithmGroup.MLDsa)
65+
{
66+
Debug.Fail($"{nameof(MLDsaCng)} should only give ML-DSA keys.");
67+
throw new CryptographicException();
68+
}
69+
70+
return CopyWithEphemeralKey(certificate, clonedCngKey);
71+
}
72+
}
73+
74+
[SupportedOSPlatform("windows")]
75+
internal static T? GetPrivateKey<T>(TCertificate certificate, Func<CspParameters, T> createCsp, Func<CngKey, T?> createCng)
76+
where T : class, IDisposable
77+
{
78+
using (SafeCertContextHandle certContext = Interop.Crypt32.CertDuplicateCertificateContext(certificate.Handle))
79+
{
80+
SafeNCryptKeyHandle? ncryptKey = TryAcquireCngPrivateKey(certContext, out CngKeyHandleOpenOptions cngHandleOptions);
81+
if (ncryptKey != null)
82+
{
83+
#if SYSTEM_SECURITY_CRYPTOGRAPHY
84+
CngKey cngKey = CngKey.OpenNoDuplicate(ncryptKey, cngHandleOptions);
85+
#else
86+
CngKey cngKey = CngKey.Open(ncryptKey, cngHandleOptions);
87+
#endif
88+
T? result = createCng(cngKey);
89+
90+
// Dispose of cngKey if its ownership did not transfer to the underlying algorithm.
91+
if (result is null)
92+
{
93+
cngKey.Dispose();
94+
}
95+
96+
return result;
97+
}
98+
99+
CspParameters? cspParameters = GetPrivateKeyCsp(certContext);
100+
if (cspParameters == null)
101+
return null;
102+
103+
if (cspParameters.ProviderType == 0)
104+
{
105+
// ProviderType being 0 signifies that this is actually a CNG key, not a CAPI key. Crypt32.dll stuffs the CNG Key Storage Provider
106+
// name into CRYPT_KEY_PROV_INFO->ProviderName, and the CNG key name into CRYPT_KEY_PROV_INFO->KeyContainerName.
107+
108+
string keyStorageProvider = cspParameters.ProviderName!;
109+
string keyName = cspParameters.KeyContainerName!;
110+
CngKey cngKey = CngKey.Open(keyName, new CngProvider(keyStorageProvider));
111+
return createCng(cngKey);
112+
}
113+
else
114+
{
115+
// ProviderType being non-zero signifies that this is a CAPI key.
116+
// We never want to stomp over certificate private keys.
117+
cspParameters.Flags |= CspProviderFlags.UseExistingKey;
118+
return createCsp(cspParameters);
119+
}
120+
}
121+
}
122+
123+
#if !SYSTEM_SECURITY_CRYPTOGRAPHY
124+
[SupportedOSPlatform("windows")]
125+
#endif
126+
private static SafeNCryptKeyHandle? TryAcquireCngPrivateKey(
127+
SafeCertContextHandle certificateContext,
128+
out CngKeyHandleOpenOptions handleOptions)
129+
{
130+
Debug.Assert(certificateContext != null);
131+
Debug.Assert(!certificateContext.IsClosed && !certificateContext.IsInvalid);
132+
133+
// If the certificate has a key handle without a key prov info, return the
134+
// ephemeral key
135+
if (!certificateContext.HasPersistedPrivateKey)
136+
{
137+
int cbData = IntPtr.Size;
138+
139+
if (Interop.Crypt32.CertGetCertificateContextProperty(
140+
certificateContext,
141+
Interop.Crypt32.CertContextPropId.CERT_NCRYPT_KEY_HANDLE_PROP_ID,
142+
out IntPtr privateKeyPtr,
143+
ref cbData))
144+
{
145+
handleOptions = CngKeyHandleOpenOptions.EphemeralKey;
146+
return CreateSafeNCryptKeyHandle(privateKeyPtr, certificateContext);
147+
}
148+
}
149+
150+
bool freeKey = true;
151+
SafeNCryptKeyHandle? privateKey = null;
152+
handleOptions = CngKeyHandleOpenOptions.None;
153+
try
154+
{
155+
if (!Interop.Crypt32.CryptAcquireCertificatePrivateKey(
156+
certificateContext,
157+
Interop.Crypt32.CryptAcquireCertificatePrivateKeyFlags.CRYPT_ACQUIRE_ONLY_NCRYPT_KEY_FLAG,
158+
IntPtr.Zero,
159+
out privateKey,
160+
out Interop.Crypt32.CryptKeySpec _,
161+
out freeKey))
162+
{
163+
164+
// The documentation for CryptAcquireCertificatePrivateKey says that freeKey
165+
// should already be false if "key acquisition fails", and it can be presumed
166+
// that privateKey was set to 0. But, just in case:
167+
freeKey = false;
168+
privateKey?.SetHandleAsInvalid();
169+
return null;
170+
}
171+
172+
// It is very unlikely that Windows will tell us !freeKey other than when reporting failure,
173+
// because we set neither CRYPT_ACQUIRE_CACHE_FLAG nor CRYPT_ACQUIRE_USE_PROV_INFO_FLAG, which are
174+
// currently the only two success situations documented. However, any !freeKey response means the
175+
// key's lifetime is tied to that of the certificate, so re-register the handle as a child handle
176+
// of the certificate.
177+
if (!freeKey && privateKey != null && !privateKey.IsInvalid)
178+
{
179+
SafeNCryptKeyHandle newKeyHandle = CreateSafeNCryptKeyHandle(privateKey.DangerousGetHandle(), certificateContext);
180+
privateKey.SetHandleAsInvalid();
181+
privateKey = newKeyHandle;
182+
freeKey = true;
183+
}
184+
185+
return privateKey;
186+
}
187+
catch
188+
{
189+
// If we aren't supposed to free the key, and we're not returning it,
190+
// just tell the SafeHandle to not free itself.
191+
if (privateKey != null && !freeKey)
192+
{
193+
privateKey.SetHandleAsInvalid();
194+
}
195+
196+
throw;
197+
}
198+
}
199+
200+
//
201+
// Returns the private key referenced by a store certificate. Note that despite the return type being declared "CspParameters",
202+
// the key can actually be a CNG key. To distinguish, examine the ProviderType property. If it is 0, this key is a CNG key with
203+
// the various properties of CspParameters being "repurposed" into storing CNG info.
204+
//
205+
// This is a behavior this method inherits directly from the Crypt32 CRYPT_KEY_PROV_INFO semantics.
206+
//
207+
// It would have been nice not to let this ugliness escape out of this helper method. But X509Certificate2.ToString() calls this
208+
// method too so we cannot just change it without breaking its output.
209+
//
210+
#if !SYSTEM_SECURITY_CRYPTOGRAPHY
211+
[SupportedOSPlatform("windows")]
212+
#endif
213+
internal static CspParameters? GetPrivateKeyCsp(SafeCertContextHandle hCertContext)
214+
{
215+
int cbData = 0;
216+
if (!Interop.Crypt32.CertGetCertificateContextProperty(hCertContext, Interop.Crypt32.CertContextPropId.CERT_KEY_PROV_INFO_PROP_ID, null, ref cbData))
217+
{
218+
#if NETFRAMEWORK
219+
int dwErrorCode = Marshal.GetHRForLastWin32Error();
220+
#else
221+
int dwErrorCode = Marshal.GetLastPInvokeError();
222+
#endif
223+
if (dwErrorCode == ErrorCode.CRYPT_E_NOT_FOUND)
224+
return null;
225+
throw dwErrorCode.ToCryptographicException();
226+
}
227+
228+
unsafe
229+
{
230+
byte[] privateKey = new byte[cbData];
231+
fixed (byte* pPrivateKey = privateKey)
232+
{
233+
if (!Interop.Crypt32.CertGetCertificateContextProperty(hCertContext, Interop.Crypt32.CertContextPropId.CERT_KEY_PROV_INFO_PROP_ID, privateKey, ref cbData))
234+
throw GetExceptionForLastError();
235+
Interop.Crypt32.CRYPT_KEY_PROV_INFO* pKeyProvInfo = (Interop.Crypt32.CRYPT_KEY_PROV_INFO*)pPrivateKey;
236+
237+
return new CspParameters
238+
{
239+
ProviderName = Marshal.PtrToStringUni((IntPtr)(pKeyProvInfo->pwszProvName)),
240+
KeyContainerName = Marshal.PtrToStringUni((IntPtr)(pKeyProvInfo->pwszContainerName)),
241+
ProviderType = pKeyProvInfo->dwProvType,
242+
KeyNumber = pKeyProvInfo->dwKeySpec,
243+
Flags = (pKeyProvInfo->dwFlags & Interop.Crypt32.CryptAcquireContextFlags.CRYPT_MACHINE_KEYSET) == Interop.Crypt32.CryptAcquireContextFlags.CRYPT_MACHINE_KEYSET ? CspProviderFlags.UseMachineKeyStore : 0,
244+
};
245+
}
246+
}
247+
}
248+
249+
#if !SYSTEM_SECURITY_CRYPTOGRAPHY
250+
[UnsupportedOSPlatform("browser")]
251+
#endif
252+
internal static unsafe TCertificate? CopyWithPersistedCngKey(TCertificate certificate, CngKey cngKey)
253+
{
254+
if (string.IsNullOrEmpty(cngKey.KeyName))
255+
{
256+
return null;
257+
}
258+
259+
// Make a new pal from bytes.
260+
TCertificate newCert = CopyFromRawBytes(certificate);
261+
262+
CngProvider provider = cngKey.Provider!;
263+
string keyName = cngKey.KeyName;
264+
bool machineKey = cngKey.IsMachineKey;
265+
266+
// CAPI RSA_SIGN keys won't open correctly under CNG without the key number being specified, so
267+
// check to see if we can figure out what key number it needs to re-open.
268+
int keySpec = GuessKeySpec(provider, keyName, machineKey, cngKey.AlgorithmGroup);
269+
270+
Interop.Crypt32.CRYPT_KEY_PROV_INFO keyProvInfo = default;
271+
272+
fixed (char* keyNamePtr = cngKey.KeyName)
273+
fixed (char* provNamePtr = cngKey.Provider!.Provider)
274+
{
275+
keyProvInfo.pwszContainerName = keyNamePtr;
276+
keyProvInfo.pwszProvName = provNamePtr;
277+
keyProvInfo.dwFlags = machineKey ? Interop.Crypt32.CryptAcquireContextFlags.CRYPT_MACHINE_KEYSET : 0;
278+
keyProvInfo.dwKeySpec = keySpec;
279+
280+
if (!Interop.Crypt32.CertSetCertificateContextProperty(
281+
newCert.Handle,
282+
Interop.Crypt32.CertContextPropId.CERT_KEY_PROV_INFO_PROP_ID,
283+
Interop.Crypt32.CertSetPropertyFlags.None,
284+
&keyProvInfo))
285+
{
286+
Exception e = GetExceptionForLastError();
287+
newCert.Dispose();
288+
throw e;
289+
}
290+
}
291+
292+
return newCert;
293+
}
294+
295+
#if !SYSTEM_SECURITY_CRYPTOGRAPHY
296+
[UnsupportedOSPlatform("browser")]
297+
#endif
298+
internal static TCertificate CopyWithEphemeralKey(TCertificate certificate, CngKey cngKey)
299+
{
300+
Debug.Assert(string.IsNullOrEmpty(cngKey.KeyName));
301+
302+
// Handle makes a copy of the handle. This is required given that CertSetCertificateContextProperty accepts a SafeHandle
303+
// and transfers ownership of the handle to the certificate. We can't transfer that ownership out of the cngKey, as it's
304+
// owned by the caller, so we make a copy.
305+
using (SafeNCryptKeyHandle handle = cngKey.Handle)
306+
{
307+
// Make a new certificate from bytes.
308+
TCertificate newCert = CopyFromRawBytes(certificate);
309+
try
310+
{
311+
if (!Interop.Crypt32.CertSetCertificateContextProperty(
312+
newCert.Handle,
313+
Interop.Crypt32.CertContextPropId.CERT_NCRYPT_KEY_HANDLE_PROP_ID,
314+
Interop.Crypt32.CertSetPropertyFlags.CERT_SET_PROPERTY_INHIBIT_PERSIST_FLAG,
315+
handle))
316+
{
317+
throw GetExceptionForLastError();
318+
}
319+
320+
// The value was transferred to the certificate.
321+
handle.SetHandleAsInvalid();
322+
323+
return newCert;
324+
}
325+
catch
326+
{
327+
newCert.Dispose();
328+
throw;
329+
}
330+
}
331+
}
332+
}
333+
}

0 commit comments

Comments
 (0)