Skip to content

Commit f192bcf

Browse files
committed
Add notion of minimum cert version
Add new SAN for dev cert + json output for the tool
1 parent e3d974c commit f192bcf

File tree

6 files changed

+136
-33
lines changed

6 files changed

+136
-33
lines changed

src/Shared/CertificateGeneration/CertificateManager.cs

Lines changed: 48 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -9,22 +9,31 @@
99
using System.Security.Cryptography;
1010
using System.Security.Cryptography.X509Certificates;
1111
using System.Text;
12+
using System.Text.Json;
13+
using System.Text.Json.Nodes;
14+
using System.Text.Json.Serialization;
1215

1316
#nullable enable
1417

1518
namespace Microsoft.AspNetCore.Certificates.Generation;
1619

1720
internal abstract class CertificateManager
1821
{
19-
internal const int CurrentAspNetCoreCertificateVersion = 2;
22+
internal const int CurrentAspNetCoreCertificateVersion = 3;
23+
internal const int CurrentMinimumAspNetCoreCertificateVersion = 3;
2024

2125
// OID used for HTTPS certs
2226
internal const string AspNetHttpsOid = "1.3.6.1.4.1.311.84.1.1";
2327
internal const string AspNetHttpsOidFriendlyName = "ASP.NET Core HTTPS development certificate";
2428

2529
private const string ServerAuthenticationEnhancedKeyUsageOid = "1.3.6.1.5.5.7.3.1";
2630
private const string ServerAuthenticationEnhancedKeyUsageOidFriendlyName = "Server Authentication";
31+
32+
// dns names of the host from a container
33+
private const string LocalHostDockerHttpsDnsName = "host.docker.internal";
34+
private const string ContainersDockerHttpsDnsName = "host.containers.internal";
2735

36+
// main cert subject
2837
private const string LocalhostHttpsDnsName = "localhost";
2938
internal const string LocalhostHttpsDistinguishedName = "CN=" + LocalhostHttpsDnsName;
3039

@@ -49,6 +58,13 @@ public int AspNetHttpsCertificateVersion
4958
internal set;
5059
}
5160

61+
public int MinimumAspNetHttpsCertificateVersion
62+
{
63+
get;
64+
// For testing purposes only
65+
internal set;
66+
}
67+
5268
public string Subject { get; }
5369

5470
public CertificateManager() : this(LocalhostHttpsDistinguishedName, CurrentAspNetCoreCertificateVersion)
@@ -57,9 +73,16 @@ public CertificateManager() : this(LocalhostHttpsDistinguishedName, CurrentAspNe
5773

5874
// For testing purposes only
5975
internal CertificateManager(string subject, int version)
76+
: this(subject, version, version)
77+
{
78+
}
79+
80+
// For testing purposes only
81+
internal CertificateManager(string subject, int generatedVersion, int minimumVersion)
6082
{
6183
Subject = subject;
62-
AspNetHttpsCertificateVersion = version;
84+
AspNetHttpsCertificateVersion = generatedVersion;
85+
MinimumAspNetHttpsCertificateVersion = minimumVersion;
6386
}
6487

6588
/// <remarks>
@@ -147,30 +170,30 @@ bool HasOid(X509Certificate2 certificate, string oid) =>
147170
certificate.Extensions.OfType<X509Extension>()
148171
.Any(e => string.Equals(oid, e.Oid?.Value, StringComparison.Ordinal));
149172

150-
static byte GetCertificateVersion(X509Certificate2 c)
151-
{
152-
var byteArray = c.Extensions.OfType<X509Extension>()
153-
.Where(e => string.Equals(AspNetHttpsOid, e.Oid?.Value, StringComparison.Ordinal))
154-
.Single()
155-
.RawData;
156-
157-
if ((byteArray.Length == AspNetHttpsOidFriendlyName.Length && byteArray[0] == (byte)'A') || byteArray.Length == 0)
158-
{
159-
// No Version set, default to 0
160-
return 0b0;
161-
}
162-
else
163-
{
164-
// Version is in the only byte of the byte array.
165-
return byteArray[0];
166-
}
167-
}
168-
169173
bool IsValidCertificate(X509Certificate2 certificate, DateTimeOffset currentDate, bool requireExportable) =>
170174
certificate.NotBefore <= currentDate &&
171175
currentDate <= certificate.NotAfter &&
172176
(!requireExportable || IsExportable(certificate)) &&
173-
GetCertificateVersion(certificate) >= AspNetHttpsCertificateVersion;
177+
GetCertificateVersion(certificate) >= MinimumAspNetHttpsCertificateVersion;
178+
}
179+
180+
private static byte GetCertificateVersion(X509Certificate2 c)
181+
{
182+
var byteArray = c.Extensions.OfType<X509Extension>()
183+
.Where(e => string.Equals(AspNetHttpsOid, e.Oid?.Value, StringComparison.Ordinal))
184+
.Single()
185+
.RawData;
186+
187+
if ((byteArray.Length == AspNetHttpsOidFriendlyName.Length && byteArray[0] == (byte)'A') || byteArray.Length == 0)
188+
{
189+
// No Version set, default to 0
190+
return 0b0;
191+
}
192+
else
193+
{
194+
// Version is in the only byte of the byte array.
195+
return byteArray[0];
196+
}
174197
}
175198

176199
protected virtual void PopulateCertificatesFromStore(X509Store store, List<X509Certificate2> certificates, bool requireExportable)
@@ -487,7 +510,7 @@ public void CleanupHttpsCertificates()
487510
/// <remarks>Implementations may choose to throw, rather than returning <see cref="TrustLevel.None"/>.</remarks>
488511
protected abstract TrustLevel TrustCertificateCore(X509Certificate2 certificate);
489512

490-
protected abstract bool IsExportable(X509Certificate2 c);
513+
internal abstract bool IsExportable(X509Certificate2 c);
491514

492515
protected abstract void RemoveCertificateFromTrustedRoots(X509Certificate2 certificate);
493516

@@ -649,6 +672,8 @@ internal X509Certificate2 CreateAspNetCoreHttpsDevelopmentCertificate(DateTimeOf
649672
var extensions = new List<X509Extension>();
650673
var sanBuilder = new SubjectAlternativeNameBuilder();
651674
sanBuilder.AddDnsName(LocalhostHttpsDnsName);
675+
sanBuilder.AddDnsName(LocalHostDockerHttpsDnsName);
676+
sanBuilder.AddDnsName(ContainersDockerHttpsDnsName);
652677

653678
var keyUsage = new X509KeyUsageExtension(X509KeyUsageFlags.KeyEncipherment | X509KeyUsageFlags.DigitalSignature, critical: true);
654679
var enhancedKeyUsage = new X509EnhancedKeyUsageExtension(

src/Shared/CertificateGeneration/MacOSCertificateManager.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -302,7 +302,7 @@ private static bool IsCertOnKeychain(string keychain, X509Certificate2 certifica
302302
}
303303

304304
// We don't have a good way of checking on the underlying implementation if it is exportable, so just return true.
305-
protected override bool IsExportable(X509Certificate2 c) => true;
305+
internal override bool IsExportable(X509Certificate2 c) => true;
306306

307307
protected override X509Certificate2 SaveCertificateCore(X509Certificate2 certificate, StoreName storeName, StoreLocation storeLocation)
308308
{

src/Shared/CertificateGeneration/UnixCertificateManager.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -179,7 +179,7 @@ internal override void CorrectCertificateState(X509Certificate2 candidate)
179179
// This is about correcting storage, not trust.
180180
}
181181

182-
protected override bool IsExportable(X509Certificate2 c) => true;
182+
internal override bool IsExportable(X509Certificate2 c) => true;
183183

184184
protected override TrustLevel TrustCertificateCore(X509Certificate2 certificate)
185185
{

src/Shared/CertificateGeneration/WindowsCertificateManager.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ internal WindowsCertificateManager(string subject, int version)
2727
{
2828
}
2929

30-
protected override bool IsExportable(X509Certificate2 c)
30+
internal override bool IsExportable(X509Certificate2 c)
3131
{
3232
#if XPLAT
3333
// For the first run experience we don't need to know if the certificate can be exported.

src/Tools/FirstRunCertGenerator/test/CertificateManagerTests.cs

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -387,7 +387,7 @@ public void EnsureCreateHttpsCertificate_ReturnsExpiredCertificateIfVersionIsInc
387387
Output.WriteLine(creation.ToString());
388388
ListCertificates();
389389

390-
_manager.AspNetHttpsCertificateVersion = 2;
390+
_manager.MinimumAspNetHttpsCertificateVersion = 2;
391391

392392
var httpsCertificateList = _manager.ListCertificates(StoreName.My, StoreLocation.CurrentUser, isValid: true);
393393
Assert.Empty(httpsCertificateList);
@@ -419,7 +419,7 @@ public void EnsureCreateHttpsCertificate_ReturnsValidIfVersionIsZero()
419419

420420
var now = DateTimeOffset.UtcNow;
421421
now = new DateTimeOffset(now.Year, now.Month, now.Day, now.Hour, now.Minute, now.Second, 0, now.Offset);
422-
_manager.AspNetHttpsCertificateVersion = 0;
422+
_manager.MinimumAspNetHttpsCertificateVersion = 0;
423423
var creation = _manager.EnsureAspNetCoreHttpsDevelopmentCertificate(now, now.AddYears(1), path: null, trust: false, isInteractive: false);
424424
Output.WriteLine(creation.ToString());
425425
ListCertificates();
@@ -460,11 +460,12 @@ public void ListCertificates_AlwaysReturnsTheCertificate_WithHighestVersion()
460460
ListCertificates();
461461

462462
_manager.AspNetHttpsCertificateVersion = 2;
463+
_manager.MinimumAspNetHttpsCertificateVersion = 2;
463464
creation = _manager.EnsureAspNetCoreHttpsDevelopmentCertificate(now, now.AddYears(1), path: null, trust: false, isInteractive: false);
464465
Output.WriteLine(creation.ToString());
465466
ListCertificates();
466467

467-
_manager.AspNetHttpsCertificateVersion = 1;
468+
_manager.MinimumAspNetHttpsCertificateVersion = 1;
468469
var httpsCertificateList = _manager.ListCertificates(StoreName.My, StoreLocation.CurrentUser, isValid: true);
469470
Assert.Equal(2, httpsCertificateList.Count);
470471

@@ -532,6 +533,8 @@ public CertFixture()
532533

533534
internal void CleanupCertificates()
534535
{
536+
Manager.AspNetHttpsCertificateVersion = 1;
537+
Manager.MinimumAspNetHttpsCertificateVersion = 1;
535538
Manager.RemoveAllCertificates(StoreName.My, StoreLocation.CurrentUser);
536539
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows) || RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
537540
{

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

Lines changed: 79 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,13 @@
11
// Licensed to the .NET Foundation under one or more agreements.
22
// The .NET Foundation licenses this file to you under the MIT license.
33

4+
using System.Diagnostics;
45
using System.Linq;
56
using System.Runtime.InteropServices;
67
using System.Security.Cryptography.X509Certificates;
8+
using System.Text.Json;
9+
using System.Text.Json.Nodes;
10+
using System.Text.Json.Serialization;
711
using Microsoft.AspNetCore.Certificates.Generation;
812
using Microsoft.Extensions.CommandLineUtils;
913
using Microsoft.Extensions.Tools.Internal;
@@ -43,6 +47,8 @@ internal sealed class Program
4347

4448
public static readonly TimeSpan HttpsCertificateValidity = TimeSpan.FromDays(365);
4549

50+
private static bool _parsableOutput;
51+
4652
public static int Main(string[] args)
4753
{
4854
if (args.Contains("--debug"))
@@ -110,12 +116,18 @@ public static int Main(string[] args)
110116
"Display warnings and errors only.",
111117
CommandOptionType.NoValue);
112118

119+
var parsableOutput = c.Option("--parsable",
120+
"Produce a parsable output, to be used by other tools.",
121+
CommandOptionType.NoValue);
122+
113123
c.HelpOption("-h|--help");
114124

115125
c.OnExecute(() =>
116126
{
117127
var reporter = new ConsoleReporter(PhysicalConsole.Singleton, verbose.HasValue(), quiet.HasValue());
118128

129+
_parsableOutput = parsableOutput.HasValue();
130+
119131
if (verbose.HasValue())
120132
{
121133
var listener = new ReporterEventListener(reporter);
@@ -328,11 +340,18 @@ private static int CheckHttpsCertificate(CommandOption trust, CommandOption verb
328340

329341
private static void ReportCertificates(IReporter reporter, IReadOnlyList<X509Certificate2> certificates, string certificateState)
330342
{
331-
reporter.Output(certificates.Count switch
343+
if (_parsableOutput)
332344
{
333-
1 => $"A {certificateState} certificate was found: {CertificateManager.GetDescription(certificates[0])}",
334-
_ => $"{certificates.Count} {certificateState} certificates were found: {CertificateManager.ToCertificateDescription(certificates)}"
335-
});
345+
reporter.Output(JsonSerializer.Serialize(CertificateReport.FromX509Certificate2List(certificates)));
346+
}
347+
else
348+
{
349+
reporter.Output(certificates.Count switch
350+
{
351+
1 => $"A {certificateState} certificate was found: {CertificateManager.GetDescription(certificates[0])}",
352+
_ => $"{certificates.Count} {certificateState} certificates were found: {CertificateManager.ToCertificateDescription(certificates)}"
353+
});
354+
}
336355
}
337356

338357
private static int EnsureHttpsCertificate(CommandOption exportPath, CommandOption password, CommandOption noPassword, CommandOption trust, CommandOption exportFormat, IReporter reporter)
@@ -452,3 +471,59 @@ private static int EnsureHttpsCertificate(CommandOption exportPath, CommandOptio
452471
}
453472
}
454473
}
474+
475+
/// <summary>
476+
/// A Serializable friendly version of the cert report output
477+
/// </summary>
478+
internal class CertificateReport
479+
{
480+
public string Thumbprint { get; init; }
481+
public string Subject { get; init; }
482+
public List<string> X509SubjectAlternativeNameExtension { get; init; }
483+
public int Version { get; init; }
484+
public DateTime ValidityNotBefore { get; init; }
485+
public DateTime ValidityNotAfter { get; init; }
486+
public bool IsHttpsDevelopmentCertificate { get; init; }
487+
public bool IsExportable { get; init; }
488+
489+
public static CertificateReport FromX509Certificate2(X509Certificate2 cert)
490+
{
491+
return new CertificateReport
492+
{
493+
Thumbprint = cert.Thumbprint,
494+
Subject = cert.Subject,
495+
X509SubjectAlternativeNameExtension = GetSanExtension(cert),
496+
Version = cert.Version,
497+
ValidityNotBefore = cert.NotBefore,
498+
ValidityNotAfter = cert.NotAfter,
499+
IsHttpsDevelopmentCertificate = CertificateManager.IsHttpsDevelopmentCertificate(cert),
500+
IsExportable = CertificateManager.Instance.IsExportable(cert)
501+
};
502+
503+
static List<string> GetSanExtension(X509Certificate2 cert)
504+
{
505+
var dnsNames = new List<string>();
506+
foreach (var extension in cert.Extensions)
507+
{
508+
if (extension is X509SubjectAlternativeNameExtension sanExtension)
509+
{
510+
foreach (var dns in sanExtension.EnumerateDnsNames())
511+
{
512+
dnsNames.Add(dns);
513+
}
514+
}
515+
}
516+
return dnsNames;
517+
}
518+
}
519+
520+
public static List<CertificateReport> FromX509Certificate2List(IEnumerable<X509Certificate2> certs)
521+
{
522+
var list = new List<CertificateReport>();
523+
foreach (var cert in certs)
524+
{
525+
list.Add(FromX509Certificate2(cert));
526+
}
527+
return list;
528+
}
529+
}

0 commit comments

Comments
 (0)