Skip to content

Commit ed7ea40

Browse files
Look up trusted certs consistently on windows (#56701)
* Search for trusted certificates consistently on Windows 1. Don't use thumbprint so we don't get flagged for using SHA-1 2. Make TrustCertificateCore and RemoveCertificateFromTrustedRoots consistent * Add a note about our usage of Thumbprint on macOS * Clean up assumptions about root store * FindBySubjectName expects a string * Search by serial number to avoid having to parse subject name * Fix typo Co-authored-by: Martin Costello <martin@martincostello.com> * Call DisposeCertificates more consistently --------- Co-authored-by: Martin Costello <martin@martincostello.com>
1 parent 9503913 commit ed7ea40

File tree

3 files changed

+57
-16
lines changed

3 files changed

+57
-16
lines changed

src/Shared/CertificateGeneration/CertificateManager.cs

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -812,6 +812,50 @@ internal static string ToCertificateDescription(IEnumerable<X509Certificate2> ce
812812
internal static string GetDescription(X509Certificate2 c) =>
813813
$"{c.Thumbprint} - {c.Subject} - Valid from {c.NotBefore:u} to {c.NotAfter:u} - IsHttpsDevelopmentCertificate: {IsHttpsDevelopmentCertificate(c).ToString().ToLowerInvariant()} - IsExportable: {Instance.IsExportable(c).ToString().ToLowerInvariant()}";
814814

815+
/// <remarks>
816+
/// <see cref="X509Certificate.Equals(X509Certificate?)"/> is not adequate for security purposes.
817+
/// </remarks>
818+
internal static bool AreCertificatesEqual(X509Certificate2 cert1, X509Certificate2 cert2)
819+
{
820+
return cert1.RawDataMemory.Span.SequenceEqual(cert2.RawDataMemory.Span);
821+
}
822+
823+
/// <summary>
824+
/// Given a certificate, usually from the <see cref="StoreName.My"/> store, try to find the
825+
/// corresponding certificate in <paramref name="store"/> (usually the <see cref="StoreName.Root"/> store)."/>
826+
/// </summary>
827+
/// <param name="store">An open <see cref="X509Store"/>.</param>
828+
/// <param name="certificate">A certificate to search for.</param>
829+
/// <param name="foundCertificate">The certificate, if any, corresponding to <paramref name="certificate"/> in <paramref name="store"/>.</param>
830+
/// <returns>True if a corresponding certificate was found.</returns>
831+
/// <remarks><see cref="ListCertificates"/> has richer filtering and a lot of debugging output that's unhelpful here.</remarks>
832+
internal static bool TryFindCertificateInStore(X509Store store, X509Certificate2 certificate, [NotNullWhen(true)] out X509Certificate2? foundCertificate)
833+
{
834+
foundCertificate = null;
835+
836+
// We specifically don't search by thumbprint to avoid being flagged for using a SHA-1 hash.
837+
var certificatesWithSubjectName = store.Certificates.Find(X509FindType.FindBySerialNumber, certificate.SerialNumber, validOnly: false);
838+
if (certificatesWithSubjectName.Count == 0)
839+
{
840+
return false;
841+
}
842+
843+
var certificatesToDispose = new List<X509Certificate2>();
844+
foreach (var candidate in certificatesWithSubjectName.OfType<X509Certificate2>())
845+
{
846+
if (foundCertificate is null && AreCertificatesEqual(candidate, certificate))
847+
{
848+
foundCertificate = candidate;
849+
}
850+
else
851+
{
852+
certificatesToDispose.Add(candidate);
853+
}
854+
}
855+
DisposeCertificates(certificatesToDispose);
856+
return foundCertificate is not null;
857+
}
858+
815859
[EventSource(Name = "Dotnet-dev-certs")]
816860
public sealed class CertificateManagerEventSource : EventSource
817861
{

src/Shared/CertificateGeneration/MacOSCertificateManager.cs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,11 @@
1111

1212
namespace Microsoft.AspNetCore.Certificates.Generation;
1313

14+
/// <remarks>
15+
/// Normally, we avoid the use of <see cref="X509Certificate2.Thumbprint"/> because it's a SHA-1 hash and, therefore,
16+
/// not adequate for security applications. However, the MacOS security tool uses SHA-1 hashes for certificate
17+
/// identification, so we're stuck.
18+
/// </remarks>
1419
internal sealed class MacOSCertificateManager : CertificateManager
1520
{
1621
// User keychain. Guard with quotes when using in command lines since users may have set

src/Shared/CertificateGeneration/WindowsCertificateManager.cs

Lines changed: 8 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -71,26 +71,22 @@ protected override X509Certificate2 SaveCertificateCore(X509Certificate2 certifi
7171

7272
protected override void TrustCertificateCore(X509Certificate2 certificate)
7373
{
74-
using var publicCertificate = new X509Certificate2(certificate.Export(X509ContentType.Cert));
75-
76-
publicCertificate.FriendlyName = certificate.FriendlyName;
77-
7874
using var store = new X509Store(StoreName.Root, StoreLocation.CurrentUser);
79-
8075
store.Open(OpenFlags.ReadWrite);
81-
var existing = store.Certificates.Find(X509FindType.FindByThumbprint, publicCertificate.Thumbprint, validOnly: false);
82-
if (existing.Count > 0)
76+
77+
if (TryFindCertificateInStore(store, certificate, out _))
8378
{
8479
Log.WindowsCertificateAlreadyTrusted();
85-
DisposeCertificates(existing.OfType<X509Certificate2>());
8680
return;
8781
}
8882

8983
try
9084
{
9185
Log.WindowsAddCertificateToRootStore();
86+
87+
using var publicCertificate = X509CertificateLoader.LoadCertificate(certificate.Export(X509ContentType.Cert));
88+
publicCertificate.FriendlyName = certificate.FriendlyName;
9289
store.Add(publicCertificate);
93-
store.Close();
9490
}
9591
catch (CryptographicException exception) when (exception.HResult == UserCancelledErrorCode)
9692
{
@@ -102,14 +98,11 @@ protected override void TrustCertificateCore(X509Certificate2 certificate)
10298
protected override void RemoveCertificateFromTrustedRoots(X509Certificate2 certificate)
10399
{
104100
Log.WindowsRemoveCertificateFromRootStoreStart();
105-
using var store = new X509Store(StoreName.Root, StoreLocation.CurrentUser);
106101

102+
using var store = new X509Store(StoreName.Root, StoreLocation.CurrentUser);
107103
store.Open(OpenFlags.ReadWrite);
108-
var matching = store.Certificates
109-
.OfType<X509Certificate2>()
110-
.SingleOrDefault(c => c.SerialNumber == certificate.SerialNumber);
111104

112-
if (matching != null)
105+
if (TryFindCertificateInStore(store, certificate, out var matching))
113106
{
114107
store.Remove(matching);
115108
}
@@ -118,14 +111,13 @@ protected override void RemoveCertificateFromTrustedRoots(X509Certificate2 certi
118111
Log.WindowsRemoveCertificateFromRootStoreNotFound();
119112
}
120113

121-
store.Close();
122114
Log.WindowsRemoveCertificateFromRootStoreEnd();
123115
}
124116

125117
public override bool IsTrusted(X509Certificate2 certificate)
126118
{
127119
return ListCertificates(StoreName.Root, StoreLocation.CurrentUser, isValid: true, requireExportable: false)
128-
.Any(c => c.Thumbprint == certificate.Thumbprint);
120+
.Any(c => AreCertificatesEqual(c, certificate));
129121
}
130122

131123
protected override IList<X509Certificate2> GetCertificatesToRemove(StoreName storeName, StoreLocation storeLocation)

0 commit comments

Comments
 (0)