diff --git a/src/Servers/Kestrel/Core/src/CoreStrings.resx b/src/Servers/Kestrel/Core/src/CoreStrings.resx
index 0f49dedc81..cb3245b4fe 100644
--- a/src/Servers/Kestrel/Core/src/CoreStrings.resx
+++ b/src/Servers/Kestrel/Core/src/CoreStrings.resx
@@ -617,4 +617,7 @@ For more information on configuring HTTPS see https://go.microsoft.com/fwlink/?l
A new stream was refused because this connection has too many streams that haven't finished processing. This may happen if many streams are aborted but not yet cleaned up.
+
+ The ASP.NET Core developer certificate is in an invalid state. To fix this issue, run the following commands 'dotnet dev-certs https --clean' and 'dotnet dev-certs https' to remove all existing ASP.NET Core development certificates and create a new untrusted developer certificate. On macOS or Windows, use 'dotnet dev-certs https --trust' to trust the new certificate.
+
diff --git a/src/Servers/Kestrel/Core/src/KestrelServerOptions.cs b/src/Servers/Kestrel/Core/src/KestrelServerOptions.cs
index 7ac2acc27e..c383945c98 100644
--- a/src/Servers/Kestrel/Core/src/KestrelServerOptions.cs
+++ b/src/Servers/Kestrel/Core/src/KestrelServerOptions.cs
@@ -142,8 +142,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core
var logger = ApplicationServices.GetRequiredService>();
try
{
- var certificateManager = new CertificateManager();
- DefaultCertificate = certificateManager.ListCertificates(CertificatePurpose.HTTPS, StoreName.My, StoreLocation.CurrentUser, isValid: true)
+ DefaultCertificate = CertificateManager.ListCertificates(CertificatePurpose.HTTPS, StoreName.My, StoreLocation.CurrentUser, isValid: true)
.FirstOrDefault();
if (DefaultCertificate != null)
diff --git a/src/Servers/Kestrel/Core/src/Middleware/HttpsConnectionMiddleware.cs b/src/Servers/Kestrel/Core/src/Middleware/HttpsConnectionMiddleware.cs
index 4dce5d0348..6693b4c06b 100644
--- a/src/Servers/Kestrel/Core/src/Middleware/HttpsConnectionMiddleware.cs
+++ b/src/Servers/Kestrel/Core/src/Middleware/HttpsConnectionMiddleware.cs
@@ -11,6 +11,7 @@ using System.Security.Authentication;
using System.Security.Cryptography.X509Certificates;
using System.Threading;
using System.Threading.Tasks;
+using Microsoft.AspNetCore.Certificates.Generation;
using Microsoft.AspNetCore.Connections;
using Microsoft.AspNetCore.Connections.Features;
using Microsoft.AspNetCore.Http.Features;
@@ -208,12 +209,28 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Https.Internal
await sslStream.DisposeAsync();
return;
}
- catch (Exception ex) when (ex is IOException || ex is AuthenticationException)
+ catch (IOException ex)
{
_logger?.LogDebug(1, ex, CoreStrings.AuthenticationFailed);
await sslStream.DisposeAsync();
return;
}
+ catch (AuthenticationException ex)
+ {
+ if (_serverCertificate == null ||
+ !CertificateManager.IsHttpsDevelopmentCertificate(_serverCertificate) ||
+ CertificateManager.CheckDeveloperCertificateKey(_serverCertificate))
+ {
+ _logger?.LogDebug(1, ex, CoreStrings.AuthenticationFailed);
+ }
+ else
+ {
+ _logger?.LogError(2, ex, CoreStrings.BadDeveloperCertificateState);
+ }
+
+ await sslStream.DisposeAsync();
+ return;
+ }
}
feature.ApplicationProtocol = sslStream.NegotiatedApplicationProtocol.Protocol;
diff --git a/src/Servers/Kestrel/test/InMemory.FunctionalTests/HttpsTests.cs b/src/Servers/Kestrel/test/InMemory.FunctionalTests/HttpsTests.cs
index 453c8f5385..ab9ec94d6b 100644
--- a/src/Servers/Kestrel/test/InMemory.FunctionalTests/HttpsTests.cs
+++ b/src/Servers/Kestrel/test/InMemory.FunctionalTests/HttpsTests.cs
@@ -364,7 +364,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.InMemory.FunctionalTests
new TestServiceContext(LoggerFactory),
listenOptions =>
{
- listenOptions.UseHttps(TestResources.GetTestCertificate());
+ listenOptions.UseHttps(TestResources.GetTestCertificate("no_extensions.pfx"));
}))
{
using (var connection = server.CreateConnection())
@@ -383,6 +383,35 @@ namespace Microsoft.AspNetCore.Server.Kestrel.InMemory.FunctionalTests
Assert.Equal(LogLevel.Debug, loggerProvider.FilterLogger.LastLogLevel);
}
+ [Fact]
+ public async Task DevCertWithInvalidPrivateKeyProducesCustomWarning()
+ {
+ var loggerProvider = new HandshakeErrorLoggerProvider();
+ LoggerFactory.AddProvider(loggerProvider);
+
+ await using (var server = new TestServer(context => Task.CompletedTask,
+ new TestServiceContext(LoggerFactory),
+ listenOptions =>
+ {
+ listenOptions.UseHttps(TestResources.GetTestCertificate());
+ }))
+ {
+ using (var connection = server.CreateConnection())
+ using (var sslStream = new SslStream(connection.Stream, true, (sender, certificate, chain, errors) => true))
+ {
+ // SslProtocols.Tls is TLS 1.0 which isn't supported by Kestrel by default.
+ await Assert.ThrowsAsync(() =>
+ sslStream.AuthenticateAsClientAsync("127.0.0.1", clientCertificates: null,
+ enabledSslProtocols: SslProtocols.Tls,
+ checkCertificateRevocation: false));
+ }
+ }
+
+ await loggerProvider.FilterLogger.LogTcs.Task.DefaultTimeout();
+ Assert.Equal(2, loggerProvider.FilterLogger.LastEventId);
+ Assert.Equal(LogLevel.Error, loggerProvider.FilterLogger.LastLogLevel);
+ }
+
[Fact]
public async Task OnAuthenticate_SeesOtherSettings()
{
diff --git a/src/Shared/CertificateGeneration/CertificateManager.cs b/src/Shared/CertificateGeneration/CertificateManager.cs
index 14d26abbcd..09824fb127 100644
--- a/src/Shared/CertificateGeneration/CertificateManager.cs
+++ b/src/Shared/CertificateGeneration/CertificateManager.cs
@@ -44,9 +44,13 @@ namespace Microsoft.AspNetCore.Certificates.Generation
// Setting to 0 means we don't append the version byte,
// which is what all machines currently have.
- public int AspNetHttpsCertificateVersion { get; set; } = 1;
+ public static int AspNetHttpsCertificateVersion { get; set; } = 1;
- public IList ListCertificates(
+ public static bool IsHttpsDevelopmentCertificate(X509Certificate2 certificate) =>
+ certificate.Extensions.OfType()
+ .Any(e => string.Equals(AspNetHttpsOid, e.Oid.Value, StringComparison.Ordinal));
+
+ public static IList ListCertificates(
CertificatePurpose purpose,
StoreName storeName,
StoreLocation location,
@@ -228,6 +232,33 @@ namespace Microsoft.AspNetCore.Certificates.Generation
return certificate;
}
+ internal static bool CheckDeveloperCertificateKey(X509Certificate2 candidate)
+ {
+ // Tries to use the certificate key to validate it can't access it
+ try
+ {
+ var rsa = candidate.GetRSAPrivateKey();
+ if (rsa == null)
+ {
+ return false;
+ }
+
+ // Encrypting a random value is the ultimate test for a key validity.
+ // Windows and Mac OS both return HasPrivateKey = true if there is (or there has been) a private key associated
+ // with the certificate at some point.
+ var value = new byte[32];
+ RandomNumberGenerator.Fill(value);
+ rsa.Decrypt(rsa.Encrypt(value, RSAEncryptionPadding.Pkcs1), RSAEncryptionPadding.Pkcs1);
+
+ // Being able to encrypt and decrypt a payload is the strongest guarantee that the key is valid.
+ return true;
+ }
+ catch (Exception)
+ {
+ return false;
+ }
+ }
+
public X509Certificate2 CreateSelfSignedCertificate(
X500DistinguishedName subject,
IEnumerable extensions,
diff --git a/src/Tools/FirstRunCertGenerator/test/CertificateManagerTests.cs b/src/Tools/FirstRunCertGenerator/test/CertificateManagerTests.cs
index 1be6c5cfe7..a0f103342a 100644
--- a/src/Tools/FirstRunCertGenerator/test/CertificateManagerTests.cs
+++ b/src/Tools/FirstRunCertGenerator/test/CertificateManagerTests.cs
@@ -52,7 +52,7 @@ namespace Microsoft.AspNetCore.Certificates.Generation.Tests
Assert.NotNull(exportedCertificate);
Assert.False(exportedCertificate.HasPrivateKey);
- var httpsCertificates = _manager.ListCertificates(CertificatePurpose.HTTPS, StoreName.My, StoreLocation.CurrentUser, isValid: false);
+ var httpsCertificates = CertificateManager.ListCertificates(CertificatePurpose.HTTPS, StoreName.My, StoreLocation.CurrentUser, isValid: false);
var httpsCertificate = Assert.Single(httpsCertificates, c => c.Subject == TestCertificateSubject);
Assert.True(httpsCertificate.HasPrivateKey);
Assert.Equal(TestCertificateSubject, httpsCertificate.Subject);
@@ -94,7 +94,7 @@ namespace Microsoft.AspNetCore.Certificates.Generation.Tests
httpsCertificate.Extensions.OfType(),
e => e.Critical == false &&
e.Oid.Value == "1.3.6.1.4.1.311.84.1.1" &&
- e.RawData[0] == _manager.AspNetHttpsCertificateVersion);
+ e.RawData[0] == CertificateManager.AspNetHttpsCertificateVersion);
Assert.Equal(httpsCertificate.GetCertHashString(), exportedCertificate.GetCertHashString());
@@ -137,7 +137,7 @@ namespace Microsoft.AspNetCore.Certificates.Generation.Tests
now = new DateTimeOffset(now.Year, now.Month, now.Day, now.Hour, now.Minute, now.Second, 0, now.Offset);
_manager.EnsureAspNetCoreHttpsDevelopmentCertificate(now, now.AddYears(1), path: null, trust: false, subject: TestCertificateSubject);
- var httpsCertificate = _manager.ListCertificates(CertificatePurpose.HTTPS, StoreName.My, StoreLocation.CurrentUser, isValid: false).Single(c => c.Subject == TestCertificateSubject);
+ var httpsCertificate = CertificateManager.ListCertificates(CertificatePurpose.HTTPS, StoreName.My, StoreLocation.CurrentUser, isValid: false).Single(c => c.Subject == TestCertificateSubject);
// Act
var result = _manager.EnsureAspNetCoreHttpsDevelopmentCertificate(now, now.AddYears(1), CertificateName, trust: false, includePrivateKey: true, password: certificatePassword, subject: TestCertificateSubject);
@@ -164,9 +164,9 @@ namespace Microsoft.AspNetCore.Certificates.Generation.Tests
now = new DateTimeOffset(now.Year, now.Month, now.Day, now.Hour, now.Minute, now.Second, 0, now.Offset);
_manager.EnsureAspNetCoreHttpsDevelopmentCertificate(now, now.AddYears(1), path: null, trust: false, subject: TestCertificateSubject);
- _manager.AspNetHttpsCertificateVersion = 2;
+ CertificateManager.AspNetHttpsCertificateVersion = 2;
- var httpsCertificateList = _manager.ListCertificates(CertificatePurpose.HTTPS, StoreName.My, StoreLocation.CurrentUser, isValid: true);
+ var httpsCertificateList = CertificateManager.ListCertificates(CertificatePurpose.HTTPS, StoreName.My, StoreLocation.CurrentUser, isValid: true);
Assert.Empty(httpsCertificateList);
}
@@ -178,12 +178,12 @@ namespace Microsoft.AspNetCore.Certificates.Generation.Tests
DateTimeOffset now = DateTimeOffset.UtcNow;
now = new DateTimeOffset(now.Year, now.Month, now.Day, now.Hour, now.Minute, now.Second, 0, now.Offset);
- _manager.AspNetHttpsCertificateVersion = 0;
+ CertificateManager.AspNetHttpsCertificateVersion = 0;
_manager.EnsureAspNetCoreHttpsDevelopmentCertificate(now, now.AddYears(1), path: null, trust: false, subject: TestCertificateSubject);
- _manager.AspNetHttpsCertificateVersion = 1;
+ CertificateManager.AspNetHttpsCertificateVersion = 1;
- var httpsCertificateList = _manager.ListCertificates(CertificatePurpose.HTTPS, StoreName.My, StoreLocation.CurrentUser, isValid: true);
+ var httpsCertificateList = CertificateManager.ListCertificates(CertificatePurpose.HTTPS, StoreName.My, StoreLocation.CurrentUser, isValid: true);
Assert.Empty(httpsCertificateList);
}
@@ -195,10 +195,10 @@ namespace Microsoft.AspNetCore.Certificates.Generation.Tests
DateTimeOffset now = DateTimeOffset.UtcNow;
now = new DateTimeOffset(now.Year, now.Month, now.Day, now.Hour, now.Minute, now.Second, 0, now.Offset);
- _manager.AspNetHttpsCertificateVersion = 0;
+ CertificateManager.AspNetHttpsCertificateVersion = 0;
_manager.EnsureAspNetCoreHttpsDevelopmentCertificate(now, now.AddYears(1), path: null, trust: false, subject: TestCertificateSubject);
- var httpsCertificateList = _manager.ListCertificates(CertificatePurpose.HTTPS, StoreName.My, StoreLocation.CurrentUser, isValid: true);
+ var httpsCertificateList = CertificateManager.ListCertificates(CertificatePurpose.HTTPS, StoreName.My, StoreLocation.CurrentUser, isValid: true);
Assert.NotEmpty(httpsCertificateList);
}
@@ -210,11 +210,11 @@ namespace Microsoft.AspNetCore.Certificates.Generation.Tests
DateTimeOffset now = DateTimeOffset.UtcNow;
now = new DateTimeOffset(now.Year, now.Month, now.Day, now.Hour, now.Minute, now.Second, 0, now.Offset);
- _manager.AspNetHttpsCertificateVersion = 2;
+ CertificateManager.AspNetHttpsCertificateVersion = 2;
_manager.EnsureAspNetCoreHttpsDevelopmentCertificate(now, now.AddYears(1), path: null, trust: false, subject: TestCertificateSubject);
- _manager.AspNetHttpsCertificateVersion = 1;
- var httpsCertificateList = _manager.ListCertificates(CertificatePurpose.HTTPS, StoreName.My, StoreLocation.CurrentUser, isValid: true);
+ CertificateManager.AspNetHttpsCertificateVersion = 1;
+ var httpsCertificateList = CertificateManager.ListCertificates(CertificatePurpose.HTTPS, StoreName.My, StoreLocation.CurrentUser, isValid: true);
Assert.NotEmpty(httpsCertificateList);
}
@@ -241,10 +241,10 @@ namespace Microsoft.AspNetCore.Certificates.Generation.Tests
_manager.CleanupHttpsCertificates(TestCertificateSubject);
- Assert.Empty(_manager.ListCertificates(CertificatePurpose.HTTPS, StoreName.My, StoreLocation.CurrentUser, isValid: false).Where(c => c.Subject == TestCertificateSubject));
+ Assert.Empty(CertificateManager.ListCertificates(CertificatePurpose.HTTPS, StoreName.My, StoreLocation.CurrentUser, isValid: false).Where(c => c.Subject == TestCertificateSubject));
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
{
- Assert.Empty(_manager.ListCertificates(CertificatePurpose.HTTPS, StoreName.Root, StoreLocation.CurrentUser, isValid: false).Where(c => c.Subject == TestCertificateSubject));
+ Assert.Empty(CertificateManager.ListCertificates(CertificatePurpose.HTTPS, StoreName.Root, StoreLocation.CurrentUser, isValid: false).Where(c => c.Subject == TestCertificateSubject));
}
}
}
diff --git a/src/Tools/dotnet-dev-certs/src/Program.cs b/src/Tools/dotnet-dev-certs/src/Program.cs
index 6ecb1912cd..2c58ff4947 100644
--- a/src/Tools/dotnet-dev-certs/src/Program.cs
+++ b/src/Tools/dotnet-dev-certs/src/Program.cs
@@ -150,7 +150,7 @@ namespace Microsoft.AspNetCore.DeveloperCertificates.Tools
{
var now = DateTimeOffset.Now;
var certificateManager = new CertificateManager();
- var certificates = certificateManager.ListCertificates(CertificatePurpose.HTTPS, StoreName.My, StoreLocation.CurrentUser, isValid: true);
+ var certificates = CertificateManager.ListCertificates(CertificatePurpose.HTTPS, StoreName.My, StoreLocation.CurrentUser, isValid: true);
if (certificates.Count == 0)
{
reporter.Output("No valid certificate found.");
@@ -164,7 +164,7 @@ namespace Microsoft.AspNetCore.DeveloperCertificates.Tools
if (trust != null && trust.HasValue())
{
var store = RuntimeInformation.IsOSPlatform(OSPlatform.OSX) ? StoreName.My : StoreName.Root;
- var trustedCertificates = certificateManager.ListCertificates(CertificatePurpose.HTTPS, store, StoreLocation.CurrentUser, isValid: true);
+ var trustedCertificates = CertificateManager.ListCertificates(CertificatePurpose.HTTPS, store, StoreLocation.CurrentUser, isValid: true);
if (!certificates.Any(c => certificateManager.IsTrusted(c)))
{
reporter.Output($@"The following certificates were found, but none of them is trusted: