[Platform] Provide a better error message when the developer certificate can't be used (#16659)
Improves the error message Kestrel gives when the developer certificate key is not available for some reason.
This commit is contained in:
parent
8acff52cf0
commit
3ceca46c5b
|
|
@ -617,4 +617,7 @@ For more information on configuring HTTPS see https://go.microsoft.com/fwlink/?l
|
|||
<data name="Http2TellClientToCalmDown" xml:space="preserve">
|
||||
<value>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.</value>
|
||||
</data>
|
||||
<data name="BadDeveloperCertificateState" xml:space="preserve">
|
||||
<value>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.</value>
|
||||
</data>
|
||||
</root>
|
||||
|
|
|
|||
|
|
@ -142,8 +142,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core
|
|||
var logger = ApplicationServices.GetRequiredService<ILogger<KestrelServer>>();
|
||||
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)
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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<IOException>(() =>
|
||||
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()
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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<X509Certificate2> ListCertificates(
|
||||
public static bool IsHttpsDevelopmentCertificate(X509Certificate2 certificate) =>
|
||||
certificate.Extensions.OfType<X509Extension>()
|
||||
.Any(e => string.Equals(AspNetHttpsOid, e.Oid.Value, StringComparison.Ordinal));
|
||||
|
||||
public static IList<X509Certificate2> 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<X509Extension> extensions,
|
||||
|
|
|
|||
|
|
@ -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<X509Extension>(),
|
||||
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));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
Loading…
Reference in New Issue