Skip to content

Commit 3c34c3a

Browse files
authored
[Https] Various improvements to the dev-certs tool (#25037)
* Add support for the trust option on Linux on the command-line tool and print a message when it's used pointing to docs. * Bump the certificate version to 2 to ensure that the certificate gets updated for 5.0 on Mac OS. * Ensure we always select the certificate with the highest available version to ensure that when we change the certificate in the future older runtimes pick up the new certificate. * Support exporting the certificate without key on PEM format.
1 parent 818279f commit 3c34c3a

File tree

3 files changed

+132
-38
lines changed

3 files changed

+132
-38
lines changed

src/Shared/CertificateGeneration/CertificateManager.cs

Lines changed: 21 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ namespace Microsoft.AspNetCore.Certificates.Generation
1515
{
1616
internal abstract class CertificateManager
1717
{
18+
internal const int CurrentAspNetCoreCertificateVersion = 2;
1819
internal const string AspNetHttpsOid = "1.3.6.1.4.1.311.84.1.1";
1920
internal const string AspNetHttpsOidFriendlyName = "ASP.NET Core HTTPS development certificate";
2021

@@ -45,7 +46,7 @@ public int AspNetHttpsCertificateVersion
4546

4647
public string Subject { get; }
4748

48-
public CertificateManager() : this(LocalhostHttpsDistinguishedName, 1)
49+
public CertificateManager() : this(LocalhostHttpsDistinguishedName, CurrentAspNetCoreCertificateVersion)
4950
{
5051
}
5152

@@ -86,10 +87,8 @@ public IList<X509Certificate2> ListCertificates(
8687
Log.CheckCertificatesValidity();
8788
var now = DateTimeOffset.Now;
8889
var validCertificates = matchingCertificates
89-
.Where(c => c.NotBefore <= now &&
90-
now <= c.NotAfter &&
91-
(!requireExportable || IsExportable(c))
92-
&& MatchesVersion(c))
90+
.Where(c => IsValidCertificate(c, now, requireExportable))
91+
.OrderByDescending(c => GetCertificateVersion(c))
9392
.ToArray();
9493

9594
var invalidCertificates = matchingCertificates.Except(validCertificates);
@@ -123,7 +122,7 @@ bool HasOid(X509Certificate2 certificate, string oid) =>
123122
certificate.Extensions.OfType<X509Extension>()
124123
.Any(e => string.Equals(oid, e.Oid.Value, StringComparison.Ordinal));
125124

126-
bool MatchesVersion(X509Certificate2 c)
125+
static byte GetCertificateVersion(X509Certificate2 c)
127126
{
128127
var byteArray = c.Extensions.OfType<X509Extension>()
129128
.Where(e => string.Equals(AspNetHttpsOid, e.Oid.Value, StringComparison.Ordinal))
@@ -133,14 +132,20 @@ bool MatchesVersion(X509Certificate2 c)
133132
if ((byteArray.Length == AspNetHttpsOidFriendlyName.Length && byteArray[0] == (byte)'A') || byteArray.Length == 0)
134133
{
135134
// No Version set, default to 0
136-
return 0 >= AspNetHttpsCertificateVersion;
135+
return 0b0;
137136
}
138137
else
139138
{
140139
// Version is in the only byte of the byte array.
141-
return byteArray[0] >= AspNetHttpsCertificateVersion;
140+
return byteArray[0];
142141
}
143142
}
143+
144+
bool IsValidCertificate(X509Certificate2 certificate, DateTimeOffset currentDate, bool requireExportable) =>
145+
certificate.NotBefore <= currentDate &&
146+
currentDate <= certificate.NotAfter &&
147+
(!requireExportable || IsExportable(certificate)) &&
148+
GetCertificateVersion(certificate) >= AspNetHttpsCertificateVersion;
144149
}
145150

146151
public IList<X509Certificate2> GetHttpsCertificates() =>
@@ -448,7 +453,14 @@ internal void ExportCertificate(X509Certificate2 certificate, string path, bool
448453
}
449454
else
450455
{
451-
bytes = certificate.Export(X509ContentType.Cert);
456+
if (format == CertificateKeyExportFormat.Pem)
457+
{
458+
bytes = Encoding.ASCII.GetBytes(PemEncoding.Write("CERTIFICATE", certificate.Export(X509ContentType.Cert)));
459+
}
460+
else
461+
{
462+
bytes = certificate.Export(X509ContentType.Cert);
463+
}
452464
}
453465
}
454466
catch (Exception e)

src/Tools/FirstRunCertGenerator/test/CertificateManagerTests.cs

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -191,6 +191,36 @@ public void EnsureCreateHttpsCertificate_CanExportTheCertInPemFormat()
191191
Assert.Equal(httpsCertificate.GetCertHashString(), exportedCertificate.GetCertHashString());
192192
}
193193

194+
[ConditionalFact]
195+
[SkipOnHelix("https://github.com/dotnet/aspnetcore/issues/6720", Queues = "OSX.1014.Amd64;OSX.1014.Amd64.Open")]
196+
public void EnsureCreateHttpsCertificate_CanExportTheCertInPemFormat_WithoutKey()
197+
{
198+
// Arrange
199+
const string CertificateName = nameof(EnsureCreateHttpsCertificate_DoesNotCreateACertificate_WhenThereIsAnExistingHttpsCertificates) + ".pem";
200+
201+
_fixture.CleanupCertificates();
202+
203+
var now = DateTimeOffset.UtcNow;
204+
now = new DateTimeOffset(now.Year, now.Month, now.Day, now.Hour, now.Minute, now.Second, 0, now.Offset);
205+
var creation = _manager.EnsureAspNetCoreHttpsDevelopmentCertificate(now, now.AddYears(1), path: null, trust: false, isInteractive: false);
206+
Output.WriteLine(creation.ToString());
207+
ListCertificates();
208+
209+
var httpsCertificate = _manager.ListCertificates(StoreName.My, StoreLocation.CurrentUser, isValid: false).Single(c => c.Subject == TestCertificateSubject);
210+
211+
// Act
212+
var result = _manager
213+
.EnsureAspNetCoreHttpsDevelopmentCertificate(now, now.AddYears(1), CertificateName, trust: false, includePrivateKey: false, password: null, keyExportFormat: CertificateKeyExportFormat.Pem, isInteractive: false);
214+
215+
// Assert
216+
Assert.Equal(EnsureCertificateResult.ValidCertificatePresent, result);
217+
Assert.True(File.Exists(CertificateName));
218+
219+
var exportedCertificate = new X509Certificate2(CertificateName);
220+
Assert.NotNull(exportedCertificate);
221+
Assert.False(exportedCertificate.HasPrivateKey);
222+
}
223+
194224
[ConditionalFact]
195225
[SkipOnHelix("https://github.com/dotnet/aspnetcore/issues/6720", Queues = "OSX.1014.Amd64;OSX.1014.Amd64.Open")]
196226
public void EnsureCreateHttpsCertificate_CanImport_ExportedPfx()
@@ -351,6 +381,44 @@ public void EnsureCreateHttpsCertificate_ReturnValidIfCertIsNewer()
351381
var httpsCertificateList = _manager.ListCertificates(StoreName.My, StoreLocation.CurrentUser, isValid: true);
352382
Assert.NotEmpty(httpsCertificateList);
353383
}
384+
385+
[ConditionalFact]
386+
[SkipOnHelix("https://github.com/dotnet/aspnetcore/issues/6720", Queues = "OSX.1014.Amd64;OSX.1014.Amd64.Open")]
387+
public void ListCertificates_AlwaysReturnsTheCertificate_WithHighestVersion()
388+
{
389+
_fixture.CleanupCertificates();
390+
391+
var now = DateTimeOffset.UtcNow;
392+
now = new DateTimeOffset(now.Year, now.Month, now.Day, now.Hour, now.Minute, now.Second, 0, now.Offset);
393+
_manager.AspNetHttpsCertificateVersion = 1;
394+
var creation = _manager.EnsureAspNetCoreHttpsDevelopmentCertificate(now, now.AddYears(1), path: null, trust: false, isInteractive: false);
395+
Output.WriteLine(creation.ToString());
396+
ListCertificates();
397+
398+
_manager.AspNetHttpsCertificateVersion = 2;
399+
creation = _manager.EnsureAspNetCoreHttpsDevelopmentCertificate(now, now.AddYears(1), path: null, trust: false, isInteractive: false);
400+
Output.WriteLine(creation.ToString());
401+
ListCertificates();
402+
403+
_manager.AspNetHttpsCertificateVersion = 1;
404+
var httpsCertificateList = _manager.ListCertificates(StoreName.My, StoreLocation.CurrentUser, isValid: true);
405+
Assert.Equal(2, httpsCertificateList.Count);
406+
407+
var firstCertificate = httpsCertificateList[0];
408+
var secondCertificate = httpsCertificateList[1];
409+
410+
Assert.Contains(
411+
firstCertificate.Extensions.OfType<X509Extension>(),
412+
e => e.Critical == false &&
413+
e.Oid.Value == "1.3.6.1.4.1.311.84.1.1" &&
414+
e.RawData[0] == 2);
415+
416+
Assert.Contains(
417+
secondCertificate.Extensions.OfType<X509Extension>(),
418+
e => e.Critical == false &&
419+
e.Oid.Value == "1.3.6.1.4.1.311.84.1.1" &&
420+
e.RawData[0] == 1);
421+
}
354422
}
355423

356424
public class CertFixture : IDisposable

src/Tools/dotnet-dev-certs/src/Program.cs

Lines changed: 43 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -98,12 +98,9 @@ public static int Main(string[] args)
9898
CommandOptionType.SingleValue);
9999

100100
CommandOption trust = null;
101-
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows) || RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
102-
{
103-
trust = c.Option("-t|--trust",
104-
"Trust the certificate on the current platform",
105-
CommandOptionType.NoValue);
106-
}
101+
trust = c.Option("-t|--trust",
102+
"Trust the certificate on the current platform",
103+
CommandOptionType.NoValue);
107104

108105
var verbose = c.Option("-v|--verbose",
109106
"Display more debug information.",
@@ -292,24 +289,32 @@ private static int CheckHttpsCertificate(CommandOption trust, IReporter reporter
292289

293290
if (trust != null && trust.HasValue())
294291
{
295-
var store = RuntimeInformation.IsOSPlatform(OSPlatform.OSX) ? StoreName.My : StoreName.Root;
296-
var trustedCertificates = certificateManager.ListCertificates(store, StoreLocation.CurrentUser, isValid: true);
297-
if (!certificates.Any(c => certificateManager.IsTrusted(c)))
292+
if(!RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
298293
{
299-
reporter.Output($@"The following certificates were found, but none of them is trusted:
300-
{string.Join(Environment.NewLine, certificates.Select(c => $"{c.Subject} - {c.Thumbprint}"))}");
301-
return ErrorCertificateNotTrusted;
294+
var store = RuntimeInformation.IsOSPlatform(OSPlatform.OSX) ? StoreName.My : StoreName.Root;
295+
var trustedCertificates = certificateManager.ListCertificates(store, StoreLocation.CurrentUser, isValid: true);
296+
if (!certificates.Any(c => certificateManager.IsTrusted(c)))
297+
{
298+
reporter.Output($@"The following certificates were found, but none of them is trusted:
299+
{string.Join(Environment.NewLine, certificates.Select(c => $"{c.Subject} - {c.Thumbprint}"))}");
300+
return ErrorCertificateNotTrusted;
301+
}
302+
else
303+
{
304+
reporter.Output("A trusted certificate was found.");
305+
}
302306
}
303307
else
304308
{
305-
reporter.Output("A trusted certificate was found.");
309+
reporter.Warn("Checking the HTTPS development certificate trust status was requested. Checking whether the certificate is trusted or not is not supported on Linux distributions." +
310+
"For instructions on how to manually validate the certificate is trusted on your Linux distribution, go to https://aka.ms/dev-certs-trust");
306311
}
307312
}
308313

309314
return Success;
310315
}
311316

312-
private static int EnsureHttpsCertificate(CommandOption exportPath, CommandOption password, CommandOption noPassword, CommandOption trust, CommandOption keyFormat, IReporter reporter)
317+
private static int EnsureHttpsCertificate(CommandOption exportPath, CommandOption password, CommandOption noPassword, CommandOption trust, CommandOption exportFormat, IReporter reporter)
313318
{
314319
var now = DateTimeOffset.Now;
315320
var manager = CertificateManager.Instance;
@@ -332,36 +337,45 @@ private static int EnsureHttpsCertificate(CommandOption exportPath, CommandOptio
332337
}
333338
}
334339

335-
if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX) && trust?.HasValue() == true)
340+
if (trust?.HasValue() == true)
336341
{
337-
reporter.Warn("Trusting the HTTPS development certificate was requested. If the certificate is not " +
338-
"already trusted we will run the following command:" + Environment.NewLine +
339-
"'sudo security add-trusted-cert -d -r trustRoot -k /Library/Keychains/System.keychain <<certificate>>'" +
340-
Environment.NewLine + "This command might prompt you for your password to install the certificate " +
341-
"on the system keychain.");
342-
}
342+
if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
343+
{
344+
reporter.Warn("Trusting the HTTPS development certificate was requested. If the certificate is not " +
345+
"already trusted we will run the following command:" + Environment.NewLine +
346+
"'sudo security add-trusted-cert -d -r trustRoot -k /Library/Keychains/System.keychain <<certificate>>'" +
347+
Environment.NewLine + "This command might prompt you for your password to install the certificate " +
348+
"on the system keychain.");
349+
}
343350

344-
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows) && trust?.HasValue() == true)
345-
{
346-
reporter.Warn("Trusting the HTTPS development certificate was requested. A confirmation prompt will be displayed " +
347-
"if the certificate was not previously trusted. Click yes on the prompt to trust the certificate.");
351+
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
352+
{
353+
reporter.Warn("Trusting the HTTPS development certificate was requested. A confirmation prompt will be displayed " +
354+
"if the certificate was not previously trusted. Click yes on the prompt to trust the certificate.");
355+
}
356+
357+
if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
358+
{
359+
reporter.Warn("Trusting the HTTPS development certificate was requested. Trusting the certificate on Linux distributions automatically is not supported. " +
360+
"For instructions on how to manually trust the certificate on your Linux distribution, go to https://aka.ms/dev-certs-trust");
361+
}
348362
}
349363

350364
var format = CertificateKeyExportFormat.Pfx;
351-
if (keyFormat.HasValue() && !Enum.TryParse(keyFormat.Value(), ignoreCase: true, out format))
365+
if (exportFormat.HasValue() && !Enum.TryParse(exportFormat.Value(), ignoreCase: true, out format))
352366
{
353-
reporter.Error($"Unknown key format '{keyFormat.Value()}'.");
367+
reporter.Error($"Unknown key format '{exportFormat.Value()}'.");
354368
return InvalidKeyExportFormat;
355369
}
356370

357371
var result = manager.EnsureAspNetCoreHttpsDevelopmentCertificate(
358372
now,
359373
now.Add(HttpsCertificateValidity),
360374
exportPath.Value(),
361-
trust == null ? false : trust.HasValue(),
375+
trust == null ? false : trust.HasValue() && !RuntimeInformation.IsOSPlatform(OSPlatform.Linux),
362376
password.HasValue() || (noPassword.HasValue() && format == CertificateKeyExportFormat.Pem),
363377
password.Value(),
364-
keyFormat.HasValue() ? format : CertificateKeyExportFormat.Pfx);
378+
exportFormat.HasValue() ? format : CertificateKeyExportFormat.Pfx);
365379

366380
switch (result)
367381
{

0 commit comments

Comments
 (0)