diff --git a/src/libraries/Common/src/Microsoft/Win32/SafeHandles/SafeCertContextHandleWithKeyContainerDeletion.cs b/src/libraries/Common/src/Microsoft/Win32/SafeHandles/SafeCertContextHandleWithKeyContainerDeletion.cs
index 7488f624b90c4..59a84bc923097 100644
--- a/src/libraries/Common/src/Microsoft/Win32/SafeHandles/SafeCertContextHandleWithKeyContainerDeletion.cs
+++ b/src/libraries/Common/src/Microsoft/Win32/SafeHandles/SafeCertContextHandleWithKeyContainerDeletion.cs
@@ -50,10 +50,16 @@ internal static void DeleteKeyContainer(SafeCertContextHandle pCertContext)
string providerName = Marshal.PtrToStringUni((IntPtr)(pProvInfo->pwszProvName))!;
string keyContainerName = Marshal.PtrToStringUni((IntPtr)(pProvInfo->pwszContainerName))!;
+ CngKeyOpenOptions openOpts = CngKeyOpenOptions.None;
+
+ if ((pProvInfo->dwFlags & Interop.Crypt32.CryptAcquireContextFlags.CRYPT_MACHINE_KEYSET) != 0)
+ {
+ openOpts = CngKeyOpenOptions.MachineKey;
+ }
try
{
- using (CngKey cngKey = CngKey.Open(keyContainerName, new CngProvider(providerName)))
+ using (CngKey cngKey = CngKey.Open(keyContainerName, new CngProvider(providerName), openOpts))
{
cngKey.Delete();
}
diff --git a/src/libraries/System.Security.Cryptography/tests/System.Security.Cryptography.Tests.csproj b/src/libraries/System.Security.Cryptography/tests/System.Security.Cryptography.Tests.csproj
index 13cda33052457..9ec855e32bd34 100644
--- a/src/libraries/System.Security.Cryptography/tests/System.Security.Cryptography.Tests.csproj
+++ b/src/libraries/System.Security.Cryptography/tests/System.Security.Cryptography.Tests.csproj
@@ -513,7 +513,8 @@
Link="Common\Interop\Windows\Crypt32\Interop.MsgEncodingType.cs" />
-
+
+
diff --git a/src/libraries/System.Security.Cryptography/tests/X509Certificates/X509FilesystemTests.Windows.cs b/src/libraries/System.Security.Cryptography/tests/X509Certificates/X509FilesystemTests.Windows.cs
new file mode 100644
index 0000000000000..463dded0ba5b3
--- /dev/null
+++ b/src/libraries/System.Security.Cryptography/tests/X509Certificates/X509FilesystemTests.Windows.cs
@@ -0,0 +1,532 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Runtime.CompilerServices;
+using System.Security.Cryptography.Pkcs;
+using System.Security.Principal;
+using System.Threading;
+using Test.Cryptography;
+using Xunit;
+
+namespace System.Security.Cryptography.X509Certificates.Tests
+{
+ [Collection("X509Filesystem")]
+ public static class X509FilesystemTests
+ {
+ // Microsoft Strong Cryptographic Provider
+ private static readonly AsnEncodedData s_capiCsp = new AsnEncodedData(
+ new Oid("1.3.6.1.4.1.311.17.1", null),
+ (
+ "1E4E004D006900630072006F0073006F006600740020005300740072006F006E" +
+ "0067002000430072007900700074006F00670072006100700068006900630020" +
+ "00500072006F00760069006400650072"
+ ).HexToByteArray());
+
+ private static readonly AsnEncodedData s_machineKey = new AsnEncodedData(
+ new Oid("1.3.6.1.4.1.311.17.2", null),
+ [0x05, 0x00]);
+
+ private static readonly Pkcs12LoaderLimits s_cspPreservingLimits = new Pkcs12LoaderLimits
+ {
+ PreserveStorageProvider = true,
+ };
+
+ // 6 random keys that will used across all of the tests in this file
+ private const int KeyGenKeySize = 2048;
+ private static readonly RSA[] s_keys =
+ {
+ RSA.Create(KeyGenKeySize), RSA.Create(KeyGenKeySize), RSA.Create(KeyGenKeySize),
+ RSA.Create(KeyGenKeySize), RSA.Create(KeyGenKeySize), RSA.Create(KeyGenKeySize),
+ };
+
+ [Theory]
+ [InlineData(X509KeyStorageFlags.DefaultKeySet)]
+ [InlineData(X509KeyStorageFlags.DefaultKeySet, true)]
+ [InlineData(X509KeyStorageFlags.UserKeySet)]
+ [InlineData(X509KeyStorageFlags.UserKeySet, true)]
+ [InlineData(X509KeyStorageFlags.MachineKeySet)]
+ [InlineData(X509KeyStorageFlags.MachineKeySet, true)]
+ public static void AllFilesDeleted_MultiplePrivateKey_Ctor(X509KeyStorageFlags storageFlags, bool capi = false)
+ {
+ AllFilesDeletedTest(
+ storageFlags,
+ capi,
+ multiPrivate: true,
+ static (bytes, pwd, flags) => new X509Certificate2(bytes, pwd, flags));
+ }
+
+ [Theory]
+ [InlineData(X509KeyStorageFlags.DefaultKeySet)]
+ [InlineData(X509KeyStorageFlags.DefaultKeySet, true)]
+ [InlineData(X509KeyStorageFlags.UserKeySet)]
+ [InlineData(X509KeyStorageFlags.UserKeySet, true)]
+ [InlineData(X509KeyStorageFlags.MachineKeySet)]
+ [InlineData(X509KeyStorageFlags.MachineKeySet, true)]
+ public static void AllFilesDeleted_SinglePrivateKey_Ctor(X509KeyStorageFlags storageFlags, bool capi = false)
+ {
+ AllFilesDeletedTest(
+ storageFlags,
+ capi,
+ multiPrivate: false,
+ static (bytes, pwd, flags) => new X509Certificate2(bytes, pwd, flags));
+ }
+
+ [Theory]
+ [InlineData(X509KeyStorageFlags.DefaultKeySet)]
+ [InlineData(X509KeyStorageFlags.DefaultKeySet, true)]
+ [InlineData(X509KeyStorageFlags.UserKeySet)]
+ [InlineData(X509KeyStorageFlags.UserKeySet, true)]
+ [InlineData(X509KeyStorageFlags.MachineKeySet)]
+ [InlineData(X509KeyStorageFlags.MachineKeySet, true)]
+ public static void AllFilesDeleted_MultiplePrivateKey_CollImport(X509KeyStorageFlags storageFlags, bool capi = false)
+ {
+ AllFilesDeletedTest(
+ storageFlags,
+ capi,
+ multiPrivate: true,
+ Cert.Import);
+ }
+
+ [Theory]
+ [InlineData(X509KeyStorageFlags.DefaultKeySet)]
+ [InlineData(X509KeyStorageFlags.DefaultKeySet, true)]
+ [InlineData(X509KeyStorageFlags.UserKeySet)]
+ [InlineData(X509KeyStorageFlags.UserKeySet, true)]
+ [InlineData(X509KeyStorageFlags.MachineKeySet)]
+ [InlineData(X509KeyStorageFlags.MachineKeySet, true)]
+ public static void AllFilesDeleted_SinglePrivateKey_CollImport(X509KeyStorageFlags storageFlags, bool capi = false)
+ {
+ AllFilesDeletedTest(
+ storageFlags,
+ capi,
+ multiPrivate: false,
+ Cert.Import);
+ }
+
+ [Theory]
+ [InlineData(X509KeyStorageFlags.DefaultKeySet)]
+ [InlineData(X509KeyStorageFlags.DefaultKeySet, true)]
+ [InlineData(X509KeyStorageFlags.UserKeySet)]
+ [InlineData(X509KeyStorageFlags.UserKeySet, true)]
+ [InlineData(X509KeyStorageFlags.MachineKeySet)]
+ [InlineData(X509KeyStorageFlags.MachineKeySet, true)]
+ public static void AllFilesDeleted_MultiplePrivateKey_SingleLoader(X509KeyStorageFlags storageFlags, bool capi = false)
+ {
+ AllFilesDeletedTest(
+ storageFlags,
+ capi,
+ multiPrivate: true,
+ static (bytes, pwd, flags) => X509CertificateLoader.LoadPkcs12(bytes, pwd, flags));
+ }
+
+ [Theory]
+ [InlineData(X509KeyStorageFlags.DefaultKeySet)]
+ [InlineData(X509KeyStorageFlags.DefaultKeySet, true)]
+ [InlineData(X509KeyStorageFlags.UserKeySet)]
+ [InlineData(X509KeyStorageFlags.UserKeySet, true)]
+ [InlineData(X509KeyStorageFlags.MachineKeySet)]
+ [InlineData(X509KeyStorageFlags.MachineKeySet, true)]
+ public static void AllFilesDeleted_SinglePrivateKey_SingleLoader(X509KeyStorageFlags storageFlags, bool capi = false)
+ {
+ AllFilesDeletedTest(
+ storageFlags,
+ capi,
+ multiPrivate: false,
+ static (bytes, pwd, flags) => X509CertificateLoader.LoadPkcs12(bytes, pwd, flags));
+ }
+
+ [Theory]
+ [InlineData(X509KeyStorageFlags.DefaultKeySet)]
+ [InlineData(X509KeyStorageFlags.DefaultKeySet, true)]
+ [InlineData(X509KeyStorageFlags.UserKeySet)]
+ [InlineData(X509KeyStorageFlags.UserKeySet, true)]
+ [InlineData(X509KeyStorageFlags.MachineKeySet)]
+ [InlineData(X509KeyStorageFlags.MachineKeySet, true)]
+ public static void AllFilesDeleted_MultiplePrivateKey_CollLoader(X509KeyStorageFlags storageFlags, bool capi = false)
+ {
+ AllFilesDeletedTest(
+ storageFlags,
+ capi,
+ multiPrivate: true,
+ static (bytes, pwd, flags) => new ImportedCollection(
+ X509CertificateLoader.LoadPkcs12Collection(bytes, pwd, flags)));
+ }
+
+ [Theory]
+ [InlineData(X509KeyStorageFlags.DefaultKeySet)]
+ [InlineData(X509KeyStorageFlags.DefaultKeySet, true)]
+ [InlineData(X509KeyStorageFlags.UserKeySet)]
+ [InlineData(X509KeyStorageFlags.UserKeySet, true)]
+ [InlineData(X509KeyStorageFlags.MachineKeySet)]
+ [InlineData(X509KeyStorageFlags.MachineKeySet, true)]
+ public static void AllFilesDeleted_SinglePrivateKey_CollLoader(X509KeyStorageFlags storageFlags, bool capi = false)
+ {
+ AllFilesDeletedTest(
+ storageFlags,
+ capi,
+ multiPrivate: false,
+ static (bytes, pwd, flags) => new ImportedCollection(
+ X509CertificateLoader.LoadPkcs12Collection(bytes, pwd, flags)));
+ }
+
+ [Theory]
+ [InlineData(X509KeyStorageFlags.DefaultKeySet)]
+ [InlineData(X509KeyStorageFlags.DefaultKeySet, true)]
+ [InlineData(X509KeyStorageFlags.UserKeySet)]
+ [InlineData(X509KeyStorageFlags.UserKeySet, true)]
+ [InlineData(X509KeyStorageFlags.MachineKeySet)]
+ [InlineData(X509KeyStorageFlags.MachineKeySet, true)]
+ public static void AllFilesDeleted_MultiplePrivateKey_SingleLoader_KeepCsp(X509KeyStorageFlags storageFlags, bool capi = false)
+ {
+ AllFilesDeletedTest(
+ storageFlags,
+ capi,
+ multiPrivate: true,
+ static (bytes, pwd, flags) =>
+ X509CertificateLoader.LoadPkcs12(bytes, pwd, flags, s_cspPreservingLimits));
+ }
+
+ [Theory]
+ [InlineData(X509KeyStorageFlags.DefaultKeySet)]
+ [InlineData(X509KeyStorageFlags.DefaultKeySet, true)]
+ [InlineData(X509KeyStorageFlags.UserKeySet)]
+ [InlineData(X509KeyStorageFlags.UserKeySet, true)]
+ [InlineData(X509KeyStorageFlags.MachineKeySet)]
+ [InlineData(X509KeyStorageFlags.MachineKeySet, true)]
+ public static void AllFilesDeleted_SinglePrivateKey_SingleLoader_KeepCsp(X509KeyStorageFlags storageFlags, bool capi = false)
+ {
+ AllFilesDeletedTest(
+ storageFlags,
+ capi,
+ multiPrivate: false,
+ static (bytes, pwd, flags) =>
+ X509CertificateLoader.LoadPkcs12(bytes, pwd, flags, s_cspPreservingLimits));
+ }
+
+ [Theory]
+ [InlineData(X509KeyStorageFlags.DefaultKeySet)]
+ [InlineData(X509KeyStorageFlags.DefaultKeySet, true)]
+ [InlineData(X509KeyStorageFlags.UserKeySet)]
+ [InlineData(X509KeyStorageFlags.UserKeySet, true)]
+ [InlineData(X509KeyStorageFlags.MachineKeySet)]
+ [InlineData(X509KeyStorageFlags.MachineKeySet, true)]
+ public static void AllFilesDeleted_MultiplePrivateKey_CollLoader_KeepCsp(X509KeyStorageFlags storageFlags, bool capi = false)
+ {
+ AllFilesDeletedTest(
+ storageFlags,
+ capi,
+ multiPrivate: true,
+ static (bytes, pwd, flags) => new ImportedCollection(
+ X509CertificateLoader.LoadPkcs12Collection(bytes, pwd, flags, s_cspPreservingLimits)));
+ }
+
+ [Theory]
+ [InlineData(X509KeyStorageFlags.DefaultKeySet)]
+ [InlineData(X509KeyStorageFlags.DefaultKeySet, true)]
+ [InlineData(X509KeyStorageFlags.UserKeySet)]
+ [InlineData(X509KeyStorageFlags.UserKeySet, true)]
+ [InlineData(X509KeyStorageFlags.MachineKeySet)]
+ [InlineData(X509KeyStorageFlags.MachineKeySet, true)]
+ public static void AllFilesDeleted_SinglePrivateKey_CollLoader_KeepCsp(X509KeyStorageFlags storageFlags, bool capi = false)
+ {
+ AllFilesDeletedTest(
+ storageFlags,
+ capi,
+ multiPrivate: false,
+ static (bytes, pwd, flags) => new ImportedCollection(
+ X509CertificateLoader.LoadPkcs12Collection(bytes, pwd, flags, s_cspPreservingLimits)));
+ }
+
+ private static void AllFilesDeletedTest(
+ X509KeyStorageFlags storageFlags,
+ bool capi,
+ bool multiPrivate,
+ Func importer,
+ [CallerMemberName] string? name = null)
+ {
+ const X509KeyStorageFlags NonDefaultKeySet =
+ X509KeyStorageFlags.UserKeySet |
+ X509KeyStorageFlags.MachineKeySet;
+
+ bool defaultKeySet = (storageFlags & NonDefaultKeySet) == 0;
+ int certAndKeyCount = multiPrivate ? s_keys.Length : 1;
+
+ byte[] pfx = MakePfx(certAndKeyCount, capi, name);
+
+ EnsureNoKeysGained(
+ (Bytes: pfx, Flags: storageFlags, Importer: importer),
+ static state => state.Importer(state.Bytes, "", state.Flags));
+
+ // When importing for DefaultKeySet, try both 010101 and 101010
+ // intermixing of machine and user keys so that single key import
+ // gets both a machine key and a user key.
+ if (defaultKeySet)
+ {
+ pfx = MakePfx(certAndKeyCount, capi, name, 1);
+
+ EnsureNoKeysGained(
+ (Bytes: pfx, Flags: storageFlags, Importer: importer),
+ static state => state.Importer(state.Bytes, "", state.Flags));
+ }
+ }
+
+ private static byte[] MakePfx(
+ int certAndKeyCount,
+ bool capi,
+ [CallerMemberName] string? name = null,
+ int machineKeySkew = 0)
+ {
+ Pkcs12SafeContents keys = new Pkcs12SafeContents();
+ Pkcs12SafeContents certs = new Pkcs12SafeContents();
+ DateTimeOffset notBefore = DateTimeOffset.UtcNow.AddMinutes(-5);
+ DateTimeOffset notAfter = notBefore.AddMinutes(10);
+
+ PbeParameters pbeParams = new PbeParameters(
+ PbeEncryptionAlgorithm.TripleDes3KeyPkcs12,
+ HashAlgorithmName.SHA1,
+ 1);
+
+ Span indices = [0, 1, 2, 3, 4, 5];
+ RandomNumberGenerator.Shuffle(indices);
+
+ for (int i = 0; i < s_keys.Length; i++)
+ {
+ RSA key = s_keys[indices[i]];
+
+ CertificateRequest req = new CertificateRequest(
+ $"CN={name}.{i}",
+ key,
+ HashAlgorithmName.SHA256,
+ RSASignaturePadding.Pkcs1);
+
+ using (X509Certificate2 cert = req.CreateSelfSigned(notBefore, notAfter))
+ {
+ Pkcs12CertBag certBag = certs.AddCertificate(cert);
+
+ if (i < certAndKeyCount)
+ {
+ Pkcs12ShroudedKeyBag keyBag = keys.AddShroudedKey(key, "", pbeParams);
+
+ if (capi)
+ {
+ keyBag.Attributes.Add(s_capiCsp);
+ }
+
+ if (int.IsEvenInteger(i + machineKeySkew))
+ {
+ keyBag.Attributes.Add(s_machineKey);
+ }
+
+ byte keyId = checked((byte)i);
+ Pkcs9LocalKeyId localKeyId = new Pkcs9LocalKeyId(new ReadOnlySpan(ref keyId));
+ keyBag.Attributes.Add(localKeyId);
+ certBag.Attributes.Add(localKeyId);
+ }
+ }
+ }
+
+ Pkcs12Builder builder = new Pkcs12Builder();
+ builder.AddSafeContentsEncrypted(certs, "", pbeParams);
+ builder.AddSafeContentsUnencrypted(keys);
+ builder.SealWithMac("", HashAlgorithmName.SHA1, 1);
+ return builder.Encode();
+ }
+
+ private static void EnsureNoKeysGained(TState state, Func importer)
+ {
+ const int ERROR_ACCESS_DENIED = (unchecked((int)0x80010005));
+
+ // In the good old days, before we had threads or parallel processes, these tests would be easy:
+ // * Read the directory listing(s)
+ // * Import a thing
+ // * See what new things were added
+ // * Dispose the thing
+ // * See that the new things went away
+ //
+ // But, since files can be created by tests on other threads, or even by other processes,
+ // recheck the directory a few times (MicroRetryCount) after sleeping (SleepMs).
+ //
+ // Sadly, that's not sufficient, because an extra file gained during that window could itself
+ // be leaked, or be intentionally persisted beyond the recheck interval. So, instead of failing,
+ // try again from the beginning. If we get parallel leaked on MacroRetryCount times in a row
+ // we'll still false-fail, but unless a majority of the tests in the process are leaking keys,
+ // it's unlikely.
+ //
+ // Before changing these constants to bigger numbers, consider the combinatorics. Failure will
+ // sleep (MacroRetryCount * (MicroRetryCount - 1) * SleepMs) ms, and also involves non-zero work.
+ // Failing 29 tests at (3, 5, 1000) adds about 6 minutes to the test run compared to success.
+
+ const int MacroRetryCount = 3;
+ const int MicroRetryCount = 5;
+ const int SleepMs = 1000;
+
+ KeyPaths keyPaths = KeyPaths.GetKeyPaths();
+ HashSet gainedFiles = null;
+
+ for (int macro = 0; macro < MacroRetryCount; macro++)
+ {
+ List keysBefore = new(keyPaths.EnumerateAllKeys());
+
+ IDisposable imported = null;
+
+ try
+ {
+ imported = importer(state);
+ }
+ catch (CryptographicException ex) when (ex.HResult == ERROR_ACCESS_DENIED)
+ {
+ }
+
+ imported?.Dispose();
+
+ gainedFiles = new HashSet(keyPaths.EnumerateAllKeys());
+ gainedFiles.ExceptWith(keysBefore);
+
+ for (int micro = 0; micro < MicroRetryCount; micro++)
+ {
+ if (gainedFiles.Count == 0)
+ {
+ return;
+ }
+
+ HashSet thisTry = new(keyPaths.EnumerateAllKeys());
+ gainedFiles.IntersectWith(thisTry);
+
+ if (gainedFiles.Count != 0 && micro < MicroRetryCount - 1)
+ {
+ Thread.Sleep(SleepMs);
+ }
+ }
+ }
+
+ Assert.Empty(keyPaths.MapPaths(gainedFiles));
+ }
+
+ private sealed class KeyPaths
+ {
+ private static volatile KeyPaths s_instance;
+
+ private string _capiUserDsa;
+ private string _capiUserRsa;
+ private string _capiMachineDsa;
+ private string _capiMachineRsa;
+ private string _cngUser;
+ private string _cngMachine;
+
+ private KeyPaths()
+ {
+ }
+
+ internal IEnumerable MapPaths(IEnumerable paths)
+ {
+ foreach (string path in paths)
+ {
+ yield return
+ Replace(path, _cngUser, "CNG-USER") ??
+ Replace(path, _capiUserRsa, "CAPI-USER-RSA") ??
+ Replace(path, _cngMachine, "CNG-MACH") ??
+ Replace(path, _capiMachineRsa, "CAPI-MACH-RSA") ??
+ Replace(path, _capiUserDsa, "CAPI-USER-DSS") ??
+ Replace(path, _capiMachineDsa, "CAPI-MACH-DSS") ??
+ path;
+ }
+
+ static string Replace(string path, string prefix, string ifMatched)
+ {
+ if (path.StartsWith(prefix))
+ {
+ return path.Replace(prefix, ifMatched);
+ }
+
+ return null;
+ }
+ }
+
+ internal IEnumerable EnumerateCapiUserKeys()
+ {
+ return EnumerateFiles(_capiUserRsa).Concat(EnumerateFiles(_capiUserDsa));
+ }
+
+ internal IEnumerable EnumerateCapiMachineKeys()
+ {
+ return EnumerateFiles(_capiMachineRsa).Concat(EnumerateFiles(_capiMachineDsa));
+ }
+
+ internal IEnumerable EnumerateCngUserKeys()
+ {
+ return EnumerateFiles(_cngUser);
+ }
+
+ internal IEnumerable EnumerateCngMachineKeys()
+ {
+ return EnumerateFiles(_cngMachine);
+ }
+
+ internal IEnumerable EnumerateUserKeys()
+ {
+ return EnumerateCapiUserKeys().Concat(EnumerateCngUserKeys());
+ }
+
+ internal IEnumerable EnumerateMachineKeys()
+ {
+ return EnumerateCapiMachineKeys().Concat(EnumerateCngMachineKeys());
+ }
+
+ internal IEnumerable EnumerateAllKeys()
+ {
+ return EnumerateUserKeys().Concat(EnumerateMachineKeys());
+ }
+
+ private static IEnumerable EnumerateFiles(string directory)
+ {
+ try
+ {
+ return Directory.EnumerateFiles(directory);
+ }
+ catch (DirectoryNotFoundException)
+ {
+ }
+
+ return [];
+ }
+
+ internal static KeyPaths GetKeyPaths()
+ {
+ if (s_instance is not null)
+ {
+ return s_instance;
+ }
+
+ // https://learn.microsoft.com/en-us/windows/win32/seccng/key-storage-and-retrieval
+ WindowsIdentity identity = WindowsIdentity.GetCurrent();
+ string userSid = identity.User!.ToString();
+
+ string userKeyBase = Path.Join(
+ Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData),
+ "Microsoft",
+ "Crypto");
+
+ string machineKeyBase = Path.Join(
+ Environment.GetFolderPath(Environment.SpecialFolder.CommonApplicationData),
+ "Microsoft",
+ "Crypto");
+
+ KeyPaths paths = new()
+ {
+ _capiUserDsa = Path.Join(userKeyBase, "DSS", userSid),
+ _capiUserRsa = Path.Join(userKeyBase, "RSA", userSid),
+ _capiMachineDsa = Path.Join(machineKeyBase, "DSS", "MachineKeys"),
+ _capiMachineRsa = Path.Join(machineKeyBase, "RSA", "MachineKeys"),
+ _cngUser = Path.Join(userKeyBase, "Keys"),
+ _cngMachine = Path.Join(machineKeyBase, "Keys"),
+ };
+
+ s_instance = paths;
+ return s_instance;
+ }
+ }
+ }
+}