Skip to content

Commit 8e1e81a

Browse files
authored
[HTTPS] Update certificate strategy for Mac OS (dotnet#20022)
* Fixes and improvements for dotnet-dev-certs: * Revamps the HTTPS developer certificate tool implementation. * It accumulated a lot of cruft during the past few years and that has made making changes harder. * Separated the CertificateManager implementation into different classes per platform. * This centralizes the decision point of choosing a platform in a single place. * Makes clear what the flow is for a given platform. * Isolates changes needed for a given platform in the future. * Moved CertificateManager to a singleton * No more statics! * Updates logging to use EventSource * We didn't have a good way of performing logging as the code is shared and must run in multiple contexts and the set of dependencies need to be kept to a minimum. * Adding ETW allow us to log/monitor the the tool execution and capture the logs with `dotnet trace` without having to invent our own logging. * We can decide to write an EventListener in `dotnet-dev-certs` to write the results to the console output. * Updates the way we handle the dev-cert in Mac OS to use the security tool to import the certificate into the store instead of using the certificate store.
1 parent 0062392 commit 8e1e81a

File tree

13 files changed

+1152
-916
lines changed

13 files changed

+1152
-916
lines changed

src/ProjectTemplates/test/Helpers/AspNetProcess.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -89,8 +89,8 @@ public AspNetProcess(
8989
internal void EnsureDevelopmentCertificates()
9090
{
9191
var now = DateTimeOffset.Now;
92-
var manager = new CertificateManager();
93-
var certificate = manager.CreateAspNetCoreHttpsDevelopmentCertificate(now, now.AddYears(1), "CN=localhost");
92+
var manager = CertificateManager.Instance;
93+
var certificate = manager.CreateAspNetCoreHttpsDevelopmentCertificate(now, now.AddYears(1));
9494
manager.ExportCertificate(certificate, path: _certificatePath, includePrivateKey: true, _certificatePassword);
9595
}
9696

src/Servers/Kestrel/Core/src/Internal/LoggerExtensions.cs

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,12 +34,28 @@ internal static class LoggerExtensions
3434
new EventId(3, "FailedToLoadDevelopmentCertificate"),
3535
"Failed to load the development https certificate at '{certificatePath}'.");
3636

37+
private static readonly Action<ILogger, Exception> _badDeveloperCertificateState =
38+
LoggerMessage.Define(
39+
LogLevel.Error,
40+
new EventId(4, "BadDeveloperCertificateState"),
41+
CoreStrings.BadDeveloperCertificateState);
42+
43+
private static readonly Action<ILogger, string, Exception> _developerCertificateFirstRun =
44+
LoggerMessage.Define<string>(
45+
LogLevel.Warning,
46+
new EventId(5, "DeveloperCertificateFirstRun"),
47+
"{Message}");
48+
3749
public static void LocatedDevelopmentCertificate(this ILogger logger, X509Certificate2 certificate) => _locatedDevelopmentCertificate(logger, certificate.Subject, certificate.Thumbprint, null);
3850

3951
public static void UnableToLocateDevelopmentCertificate(this ILogger logger) => _unableToLocateDevelopmentCertificate(logger, null);
4052

4153
public static void FailedToLocateDevelopmentCertificateFile(this ILogger logger, string certificatePath) => _failedToLocateDevelopmentCertificateFile(logger, certificatePath, null);
4254

4355
public static void FailedToLoadDevelopmentCertificate(this ILogger logger, string certificatePath) => _failedToLoadDevelopmentCertificate(logger, certificatePath, null);
56+
57+
public static void BadDeveloperCertificateState(this ILogger logger) => _badDeveloperCertificateState(logger, null);
58+
59+
public static void DeveloperCertificateFirstRun(this ILogger logger, string message) => _developerCertificateFirstRun(logger, message, null);
4460
}
4561
}

src/Servers/Kestrel/Core/src/KestrelServerOptions.cs

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -162,11 +162,29 @@ private void EnsureDefaultCert()
162162
var logger = ApplicationServices.GetRequiredService<ILogger<KestrelServer>>();
163163
try
164164
{
165-
DefaultCertificate = CertificateManager.ListCertificates(CertificatePurpose.HTTPS, StoreName.My, StoreLocation.CurrentUser, isValid: true)
165+
DefaultCertificate = CertificateManager.Instance.ListCertificates(StoreName.My, StoreLocation.CurrentUser, isValid: true)
166166
.FirstOrDefault();
167167

168168
if (DefaultCertificate != null)
169169
{
170+
var status = CertificateManager.Instance.CheckCertificateState(DefaultCertificate, interactive: false);
171+
if (!status.Result)
172+
{
173+
// Display a warning indicating to the user that a prompt might appear and provide instructions on what to do in that
174+
// case. The underlying implementation of this check is specific to Mac OS and is handled within CheckCertificateState.
175+
// Kestrel must NEVER cause a UI prompt on a production system. We only attempt this here because Mac OS is not supported
176+
// in production.
177+
logger.DeveloperCertificateFirstRun(status.Message);
178+
179+
// Now that we've displayed a warning in the logs so that the user gets a notification that a prompt might appear, try
180+
// and access the certificate key, which might trigger a prompt.
181+
status = CertificateManager.Instance.CheckCertificateState(DefaultCertificate, interactive: true);
182+
if (!status.Result)
183+
{
184+
logger.BadDeveloperCertificateState();
185+
}
186+
}
187+
170188
logger.LocatedDevelopmentCertificate(DefaultCertificate);
171189
}
172190
else

src/Servers/Kestrel/Core/src/Middleware/HttpsConnectionMiddleware.cs

Lines changed: 1 addition & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -220,16 +220,7 @@ public async Task OnConnectionAsync(ConnectionContext context)
220220
}
221221
catch (AuthenticationException ex)
222222
{
223-
if (_serverCertificate == null ||
224-
!CertificateManager.IsHttpsDevelopmentCertificate(_serverCertificate) ||
225-
CertificateManager.CheckDeveloperCertificateKey(_serverCertificate))
226-
{
227-
_logger.LogDebug(1, ex, CoreStrings.AuthenticationFailed);
228-
}
229-
else
230-
{
231-
_logger.LogError(3, ex, CoreStrings.BadDeveloperCertificateState);
232-
}
223+
_logger.LogDebug(1, ex, CoreStrings.AuthenticationFailed);
233224

234225
await sslStream.DisposeAsync();
235226
return;

src/Servers/Kestrel/test/InMemory.FunctionalTests/HttpsTests.cs

Lines changed: 0 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -385,35 +385,6 @@ await Assert.ThrowsAnyAsync<Exception>(() =>
385385
Assert.Equal(LogLevel.Debug, loggerProvider.FilterLogger.LastLogLevel);
386386
}
387387

388-
[Fact]
389-
public async Task DevCertWithInvalidPrivateKeyProducesCustomWarning()
390-
{
391-
var loggerProvider = new HandshakeErrorLoggerProvider();
392-
LoggerFactory.AddProvider(loggerProvider);
393-
394-
await using (var server = new TestServer(context => Task.CompletedTask,
395-
new TestServiceContext(LoggerFactory),
396-
listenOptions =>
397-
{
398-
listenOptions.UseHttps(TestResources.GetTestCertificate("aspnetdevcert.pfx", "testPassword"));
399-
}))
400-
{
401-
using (var connection = server.CreateConnection())
402-
using (var sslStream = new SslStream(connection.Stream, true, (sender, certificate, chain, errors) => true))
403-
{
404-
// SslProtocols.Tls is TLS 1.0 which isn't supported by Kestrel by default.
405-
await Assert.ThrowsAnyAsync<Exception>(() =>
406-
sslStream.AuthenticateAsClientAsync("127.0.0.1", clientCertificates: null,
407-
enabledSslProtocols: SslProtocols.Tls,
408-
checkCertificateRevocation: false));
409-
}
410-
}
411-
412-
await loggerProvider.FilterLogger.LogTcs.Task.DefaultTimeout();
413-
Assert.Equal(3, loggerProvider.FilterLogger.LastEventId);
414-
Assert.Equal(LogLevel.Error, loggerProvider.FilterLogger.LastLogLevel);
415-
}
416-
417388
[Fact]
418389
public async Task OnAuthenticate_SeesOtherSettings()
419390
{

0 commit comments

Comments
 (0)