Skip to content

Commit fdba8a9

Browse files
authored
Add versioning to dotnet-dev-certs (#10908)
1 parent 5b56de9 commit fdba8a9

File tree

4 files changed

+187
-239
lines changed

4 files changed

+187
-239
lines changed

src/Shared/CertificateGeneration/CertificateManager.cs

Lines changed: 40 additions & 106 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,10 @@ internal class CertificateManager
4242
private static readonly string MacOSTrustCertificateCommandLineArguments = "security add-trusted-cert -d -r trustRoot -k " + MacOSSystemKeyChain + " ";
4343
private const int UserCancelledErrorCode = 1223;
4444

45+
// Setting to 0 means we don't append the version byte,
46+
// which is what all machines currently have.
47+
public int AspNetHttpsCertificateVersion { get; set; } = 1;
48+
4549
public IList<X509Certificate2> ListCertificates(
4650
CertificatePurpose purpose,
4751
StoreName storeName,
@@ -83,7 +87,8 @@ public IList<X509Certificate2> ListCertificates(
8387
var validCertificates = matchingCertificates
8488
.Where(c => c.NotBefore <= now &&
8589
now <= c.NotAfter &&
86-
(!requireExportable || !RuntimeInformation.IsOSPlatform(OSPlatform.Windows) || IsExportable(c)))
90+
(!requireExportable || !RuntimeInformation.IsOSPlatform(OSPlatform.Windows) || IsExportable(c))
91+
&& MatchesVersion(c))
8792
.ToArray();
8893

8994
var invalidCertificates = matchingCertificates.Except(validCertificates);
@@ -117,6 +122,25 @@ public IList<X509Certificate2> ListCertificates(
117122
bool HasOid(X509Certificate2 certificate, string oid) =>
118123
certificate.Extensions.OfType<X509Extension>()
119124
.Any(e => string.Equals(oid, e.Oid.Value, StringComparison.Ordinal));
125+
126+
bool MatchesVersion(X509Certificate2 c)
127+
{
128+
var byteArray = c.Extensions.OfType<X509Extension>()
129+
.Where(e => string.Equals(AspNetHttpsOid, e.Oid.Value, StringComparison.Ordinal))
130+
.Single()
131+
.RawData;
132+
133+
if ((byteArray.Length == AspNetHttpsOidFriendlyName.Length && byteArray[0] == (byte)'A') || byteArray.Length == 0)
134+
{
135+
// No Version set, default to 0
136+
return 0 >= AspNetHttpsCertificateVersion;
137+
}
138+
else
139+
{
140+
// Version is in the only byte of the byte array.
141+
return byteArray[0] >= AspNetHttpsCertificateVersion;
142+
}
143+
}
120144
#if !XPLAT
121145
bool IsExportable(X509Certificate2 c) =>
122146
((c.GetRSAPrivateKey() is RSACryptoServiceProvider rsaPrivateKey &&
@@ -171,10 +195,22 @@ public X509Certificate2 CreateAspNetCoreHttpsDevelopmentCertificate(DateTimeOffs
171195
pathLengthConstraint: 0,
172196
critical: true);
173197

198+
byte[] bytePayload;
199+
200+
if (AspNetHttpsCertificateVersion != 0)
201+
{
202+
bytePayload = new byte[1];
203+
bytePayload[0] = (byte)AspNetHttpsCertificateVersion;
204+
}
205+
else
206+
{
207+
bytePayload = Encoding.ASCII.GetBytes(AspNetHttpsOidFriendlyName);
208+
}
209+
174210
var aspNetHttpsExtension = new X509Extension(
175211
new AsnEncodedData(
176212
new Oid(AspNetHttpsOid, AspNetHttpsOidFriendlyName),
177-
Encoding.ASCII.GetBytes(AspNetHttpsOidFriendlyName)),
213+
bytePayload),
178214
critical: false);
179215

180216
extensions.Add(basicConstraints);
@@ -633,7 +669,7 @@ private static void RemoveCertificateFromKeyChain(string keyChain, X509Certifica
633669
}
634670
}
635671

636-
public EnsureCertificateResult EnsureAspNetCoreHttpsDevelopmentCertificate(
672+
public DetailedEnsureCertificateResult EnsureAspNetCoreHttpsDevelopmentCertificate(
637673
DateTimeOffset notBefore,
638674
DateTimeOffset notAfter,
639675
string path = null,
@@ -645,109 +681,7 @@ public EnsureCertificateResult EnsureAspNetCoreHttpsDevelopmentCertificate(
645681
return EnsureValidCertificateExists(notBefore, notAfter, CertificatePurpose.HTTPS, path, trust, includePrivateKey, password, subject);
646682
}
647683

648-
public EnsureCertificateResult EnsureValidCertificateExists(
649-
DateTimeOffset notBefore,
650-
DateTimeOffset notAfter,
651-
CertificatePurpose purpose,
652-
string path = null,
653-
bool trust = false,
654-
bool includePrivateKey = false,
655-
string password = null,
656-
string subjectOverride = null)
657-
{
658-
if (purpose == CertificatePurpose.All)
659-
{
660-
throw new ArgumentException("The certificate must have a specific purpose.");
661-
}
662-
663-
var certificates = ListCertificates(purpose, StoreName.My, StoreLocation.CurrentUser, isValid: true).Concat(
664-
ListCertificates(purpose, StoreName.My, StoreLocation.LocalMachine, isValid: true));
665-
666-
certificates = subjectOverride == null ? certificates : certificates.Where(c => c.Subject == subjectOverride);
667-
668-
var result = EnsureCertificateResult.Succeeded;
669-
670-
X509Certificate2 certificate = null;
671-
if (certificates.Count() > 0)
672-
{
673-
certificate = certificates.FirstOrDefault();
674-
result = EnsureCertificateResult.ValidCertificatePresent;
675-
}
676-
else
677-
{
678-
try
679-
{
680-
switch (purpose)
681-
{
682-
case CertificatePurpose.All:
683-
throw new InvalidOperationException("The certificate must have a specific purpose.");
684-
case CertificatePurpose.HTTPS:
685-
certificate = CreateAspNetCoreHttpsDevelopmentCertificate(notBefore, notAfter, subjectOverride);
686-
break;
687-
default:
688-
throw new InvalidOperationException("The certificate must have a purpose.");
689-
}
690-
}
691-
catch
692-
{
693-
return EnsureCertificateResult.ErrorCreatingTheCertificate;
694-
}
695-
696-
try
697-
{
698-
certificate = SaveCertificateInStore(certificate, StoreName.My, StoreLocation.CurrentUser);
699-
}
700-
catch
701-
{
702-
return EnsureCertificateResult.ErrorSavingTheCertificateIntoTheCurrentUserPersonalStore;
703-
}
704-
}
705-
if (path != null)
706-
{
707-
try
708-
{
709-
ExportCertificate(certificate, path, includePrivateKey, password);
710-
}
711-
catch
712-
{
713-
return EnsureCertificateResult.ErrorExportingTheCertificate;
714-
}
715-
}
716-
717-
if ((RuntimeInformation.IsOSPlatform(OSPlatform.Windows) || RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) && trust)
718-
{
719-
try
720-
{
721-
TrustCertificate(certificate);
722-
}
723-
catch (UserCancelledTrustException)
724-
{
725-
return EnsureCertificateResult.UserCancelledTrustStep;
726-
}
727-
catch
728-
{
729-
return EnsureCertificateResult.FailedToTrustTheCertificate;
730-
}
731-
}
732-
733-
return result;
734-
}
735-
736-
// This is just to avoid breaking changes across repos.
737-
// Will be renamed back to EnsureAspNetCoreHttpsDevelopmentCertificate once updates are made elsewhere.
738-
public DetailedEnsureCertificateResult EnsureAspNetCoreHttpsDevelopmentCertificate2(
739-
DateTimeOffset notBefore,
740-
DateTimeOffset notAfter,
741-
string path = null,
742-
bool trust = false,
743-
bool includePrivateKey = false,
744-
string password = null,
745-
string subject = LocalhostHttpsDistinguishedName)
746-
{
747-
return EnsureValidCertificateExists2(notBefore, notAfter, CertificatePurpose.HTTPS, path, trust, includePrivateKey, password, subject);
748-
}
749-
750-
public DetailedEnsureCertificateResult EnsureValidCertificateExists2(
684+
public DetailedEnsureCertificateResult EnsureValidCertificateExists(
751685
DateTimeOffset notBefore,
752686
DateTimeOffset notAfter,
753687
CertificatePurpose purpose,

0 commit comments

Comments
 (0)