|
| 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