diff --git a/src/Servers/Kestrel/Core/src/KestrelServerOptions.cs b/src/Servers/Kestrel/Core/src/KestrelServerOptions.cs index 3dbbdee155d650d98a06eea2561b0981c0122f5c..ba5c6151b4ebe59f78dd971fd0d9c1576ca690e0 100644 --- a/src/Servers/Kestrel/Core/src/KestrelServerOptions.cs +++ b/src/Servers/Kestrel/Core/src/KestrelServerOptions.cs @@ -290,7 +290,7 @@ public class KestrelServerOptions var logger = ApplicationServices!.GetRequiredService<ILogger<KestrelServer>>(); try { - DefaultCertificate = CertificateManager.Instance.ListCertificates(StoreName.My, StoreLocation.CurrentUser, isValid: true) + DefaultCertificate = CertificateManager.Instance.ListCertificates(StoreName.My, StoreLocation.CurrentUser, isValid: true, requireExportable: false) .FirstOrDefault(); if (DefaultCertificate != null) diff --git a/src/Shared/CertificateGeneration/CertificateManager.cs b/src/Shared/CertificateGeneration/CertificateManager.cs index c34d88f8c00c794f2b762ea87a9b446468467889..00478e6284ef622acde11f303eb147cdbddfae2d 100644 --- a/src/Shared/CertificateGeneration/CertificateManager.cs +++ b/src/Shared/CertificateGeneration/CertificateManager.cs @@ -78,7 +78,7 @@ internal abstract class CertificateManager { using var store = new X509Store(storeName, location); store.Open(OpenFlags.ReadOnly); - PopulateCertificatesFromStore(store, certificates); + PopulateCertificatesFromStore(store, certificates, requireExportable); IEnumerable<X509Certificate2> matchingCertificates = certificates; matchingCertificates = matchingCertificates .Where(c => HasOid(c, AspNetHttpsOid)); @@ -161,7 +161,7 @@ internal abstract class CertificateManager GetCertificateVersion(certificate) >= AspNetHttpsCertificateVersion; } - protected virtual void PopulateCertificatesFromStore(X509Store store, List<X509Certificate2> certificates) + protected virtual void PopulateCertificatesFromStore(X509Store store, List<X509Certificate2> certificates, bool requireExportable) { certificates.AddRange(store.Certificates.OfType<X509Certificate2>()); } diff --git a/src/Shared/CertificateGeneration/MacOSCertificateManager.cs b/src/Shared/CertificateGeneration/MacOSCertificateManager.cs index 343a93f00bcd8cd79b384ca967b6ad4f036a44df..30237e445f61092825bd28181305ffc3e2e03414 100644 --- a/src/Shared/CertificateGeneration/MacOSCertificateManager.cs +++ b/src/Shared/CertificateGeneration/MacOSCertificateManager.cs @@ -373,14 +373,14 @@ internal sealed class MacOSCertificateManager : CertificateManager return ListCertificates(StoreName.My, StoreLocation.CurrentUser, isValid: false); } - protected override void PopulateCertificatesFromStore(X509Store store, List<X509Certificate2> certificates) + protected override void PopulateCertificatesFromStore(X509Store store, List<X509Certificate2> certificates, bool requireExportable) { if (store.Name! == StoreName.My.ToString() && store.Location == StoreLocation.CurrentUser && Directory.Exists(MacOSUserHttpsCertificateLocation)) { var certsFromDisk = GetCertsFromDisk(); var certsFromStore = new List<X509Certificate2>(); - base.PopulateCertificatesFromStore(store, certsFromStore); + base.PopulateCertificatesFromStore(store, certsFromStore, requireExportable); // Certs created by pre-.NET 7. var onlyOnKeychain = certsFromStore.Except(certsFromDisk, ThumbprintComparer.Instance); @@ -388,10 +388,13 @@ internal sealed class MacOSCertificateManager : CertificateManager // Certs created (or "upgraded") by .NET 7+. // .NET 7+ installs the certificate on disk as well as on the user keychain (for backwards // compatibility with pre-.NET 7). - // Note that the actual certs we populate need to be the ones from the store location, and - // not the version from disk, since we may do other operations with these certs later (such - // as exporting) which would fail with crypto errors otherwise. - var onDiskAndKeychain = certsFromStore.Intersect(certsFromDisk, ThumbprintComparer.Instance); + // Note that if we require exportable certs, the actual certs we populate need to be the ones + // from the store location, and not the version from disk. If we don't require exportability, + // we favor the version of the cert that's on disk (avoiding unnecessary keychain access + // prompts). Intersect compares with the specified comparer and returns the matching elements + // from the first set. + var onDiskAndKeychain = requireExportable ? certsFromStore.Intersect(certsFromDisk, ThumbprintComparer.Instance) + : certsFromDisk.Intersect(certsFromStore, ThumbprintComparer.Instance); // The only times we can find a certificate on the keychain and a certificate on keychain+disk // are when the certificate on disk and keychain has expired and a pre-.NET 7 SDK has been @@ -403,7 +406,7 @@ internal sealed class MacOSCertificateManager : CertificateManager } else { - base.PopulateCertificatesFromStore(store, certificates); + base.PopulateCertificatesFromStore(store, certificates, requireExportable); } }