Skip to content

[Platform] Provide a better error message when the developer certificate can't be used #16659

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 8 commits into from
Oct 31, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions src/Servers/Kestrel/Core/src/CoreStrings.resx
Original file line number Diff line number Diff line change
Expand Up @@ -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>
3 changes: 1 addition & 2 deletions src/Servers/Kestrel/Core/src/KestrelServerOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -142,8 +142,7 @@ private void EnsureDefaultCert()
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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
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;
Expand Down Expand Up @@ -208,12 +209,28 @@ private async Task InnerOnConnectionAsync(ConnectionContext context)
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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -364,7 +364,7 @@ public async Task ClientAttemptingToUseUnsupportedProtocolIsLoggedAsDebug()
new TestServiceContext(LoggerFactory),
listenOptions =>
{
listenOptions.UseHttps(TestResources.GetTestCertificate());
listenOptions.UseHttps(TestResources.GetTestCertificate("no_extensions.pfx"));
}))
{
using (var connection = server.CreateConnection())
Expand All @@ -383,6 +383,35 @@ await Assert.ThrowsAsync<IOException>(() =>
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()
{
Expand Down
35 changes: 33 additions & 2 deletions src/Shared/CertificateGeneration/CertificateManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -44,9 +44,13 @@ internal class CertificateManager

// 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,
Expand Down Expand Up @@ -228,6 +232,33 @@ public X509Certificate2 CreateAspNetCoreHttpsDevelopmentCertificate(DateTimeOffs
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,
Expand Down
30 changes: 15 additions & 15 deletions src/Tools/FirstRunCertGenerator/test/CertificateManagerTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ public void EnsureCreateHttpsCertificate_CreatesACertificate_WhenThereAreNoHttps
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);
Expand Down Expand Up @@ -94,7 +94,7 @@ public void EnsureCreateHttpsCertificate_CreatesACertificate_WhenThereAreNoHttps
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());

Expand Down Expand Up @@ -137,7 +137,7 @@ public void EnsureCreateHttpsCertificate_DoesNotCreateACertificate_WhenThereIsAn
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);
Expand All @@ -164,9 +164,9 @@ public void EnsureCreateHttpsCertificate_ReturnsExpiredCertificateIfVersionIsInc
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);
}

Expand All @@ -178,12 +178,12 @@ public void EnsureCreateHttpsCertificate_ReturnsExpiredCertificateForEmptyVersio

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);
}

Expand All @@ -195,10 +195,10 @@ public void EnsureCreateHttpsCertificate_ReturnsValidIfVersionIsZero()

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);
}

Expand All @@ -210,11 +210,11 @@ public void EnsureCreateHttpsCertificate_ReturnValidIfCertIsNewer()

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);
}

Expand All @@ -241,10 +241,10 @@ public void EnsureAspNetCoreHttpsDevelopmentCertificate_CanRemoveCertificates()

_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));
}
}
}
Expand Down
4 changes: 2 additions & 2 deletions src/Tools/dotnet-dev-certs/src/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -150,7 +150,7 @@ private static int CheckHttpsCertificate(CommandOption trust, IReporter reporter
{
var now = DateTimeOffset.Now;
var certificateManager = new CertificateManager();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

could be removed.

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.");
Expand All @@ -164,7 +164,7 @@ private static int CheckHttpsCertificate(CommandOption trust, IReporter reporter
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:
Expand Down