Skip to content

Commit e5ee028

Browse files
committed
[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.
1 parent 21831b5 commit e5ee028

File tree

7 files changed

+102
-23
lines changed

7 files changed

+102
-23
lines changed

src/Servers/Kestrel/Core/src/CoreStrings.resx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -617,4 +617,7 @@ For more information on configuring HTTPS see https://go.microsoft.com/fwlink/?l
617617
<data name="Http2TellClientToCalmDown" xml:space="preserve">
618618
<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>
619619
</data>
620+
<data name="BadDeveloperCertificateState" xml:space="preserve">
621+
<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>
622+
</data>
620623
</root>

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

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -142,8 +142,7 @@ private void EnsureDefaultCert()
142142
var logger = ApplicationServices.GetRequiredService<ILogger<KestrelServer>>();
143143
try
144144
{
145-
var certificateManager = new CertificateManager();
146-
DefaultCertificate = certificateManager.ListCertificates(CertificatePurpose.HTTPS, StoreName.My, StoreLocation.CurrentUser, isValid: true)
145+
DefaultCertificate = CertificateManager.ListCertificates(CertificatePurpose.HTTPS, StoreName.My, StoreLocation.CurrentUser, isValid: true)
147146
.FirstOrDefault();
148147

149148
if (DefaultCertificate != null)

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

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
using System.Security.Cryptography.X509Certificates;
1212
using System.Threading;
1313
using System.Threading.Tasks;
14+
using Microsoft.AspNetCore.Certificates.Generation;
1415
using Microsoft.AspNetCore.Connections;
1516
using Microsoft.AspNetCore.Connections.Features;
1617
using Microsoft.AspNetCore.Http.Features;
@@ -208,12 +209,28 @@ private async Task InnerOnConnectionAsync(ConnectionContext context)
208209
await sslStream.DisposeAsync();
209210
return;
210211
}
211-
catch (Exception ex) when (ex is IOException || ex is AuthenticationException)
212+
catch (IOException ex)
212213
{
213214
_logger?.LogDebug(1, ex, CoreStrings.AuthenticationFailed);
214215
await sslStream.DisposeAsync();
215216
return;
216217
}
218+
catch (AuthenticationException ex)
219+
{
220+
if (_serverCertificate == null ||
221+
!CertificateManager.IsHttpsDevelopmentCertificate(_serverCertificate) ||
222+
CertificateManager.CheckDeveloperCertificateKey(_serverCertificate))
223+
{
224+
_logger?.LogDebug(1, ex, CoreStrings.AuthenticationFailed);
225+
}
226+
else
227+
{
228+
_logger?.LogError(2, ex, CoreStrings.BadDeveloperCertificateState);
229+
}
230+
231+
await sslStream.DisposeAsync();
232+
return;
233+
}
217234
}
218235

219236
feature.ApplicationProtocol = sslStream.NegotiatedApplicationProtocol.Protocol;

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

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -364,7 +364,7 @@ public async Task ClientAttemptingToUseUnsupportedProtocolIsLoggedAsDebug()
364364
new TestServiceContext(LoggerFactory),
365365
listenOptions =>
366366
{
367-
listenOptions.UseHttps(TestResources.GetTestCertificate());
367+
listenOptions.UseHttps(TestResources.GetTestCertificate("no_extensions.pfx"));
368368
}))
369369
{
370370
using (var connection = server.CreateConnection())
@@ -383,6 +383,35 @@ await Assert.ThrowsAsync<IOException>(() =>
383383
Assert.Equal(LogLevel.Debug, loggerProvider.FilterLogger.LastLogLevel);
384384
}
385385

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

src/Shared/CertificateGeneration/CertificateManager.cs

Lines changed: 33 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -44,9 +44,13 @@ internal class CertificateManager
4444

4545
// Setting to 0 means we don't append the version byte,
4646
// which is what all machines currently have.
47-
public int AspNetHttpsCertificateVersion { get; set; } = 1;
47+
public static int AspNetHttpsCertificateVersion { get; set; } = 1;
4848

49-
public IList<X509Certificate2> ListCertificates(
49+
public static bool IsHttpsDevelopmentCertificate(X509Certificate2 certificate) =>
50+
certificate.Extensions.OfType<X509Extension>()
51+
.Any(e => string.Equals(AspNetHttpsOid, e.Oid.Value, StringComparison.Ordinal));
52+
53+
public static IList<X509Certificate2> ListCertificates(
5054
CertificatePurpose purpose,
5155
StoreName storeName,
5256
StoreLocation location,
@@ -228,6 +232,33 @@ public X509Certificate2 CreateAspNetCoreHttpsDevelopmentCertificate(DateTimeOffs
228232
return certificate;
229233
}
230234

235+
internal static bool CheckDeveloperCertificateKey(X509Certificate2 candidate)
236+
{
237+
// Tries to use the certificate key to validate it can't access it
238+
try
239+
{
240+
var rsa = candidate.GetRSAPrivateKey();
241+
if (rsa == null)
242+
{
243+
return false;
244+
}
245+
246+
// Encrypting a random value is the ultimate test for a key validity.
247+
// Windows and Mac OS both return HasPrivateKey = true if there is (or there has been) a private key associated
248+
// with the certificate at some point.
249+
var value = new byte[32];
250+
RandomNumberGenerator.Fill(value);
251+
rsa.Decrypt(rsa.Encrypt(value, RSAEncryptionPadding.Pkcs1), RSAEncryptionPadding.Pkcs1);
252+
253+
// Being able to encrypt and decrypt a payload is the strongest guarantee that the key is valid.
254+
return true;
255+
}
256+
catch (Exception)
257+
{
258+
return false;
259+
}
260+
}
261+
231262
public X509Certificate2 CreateSelfSignedCertificate(
232263
X500DistinguishedName subject,
233264
IEnumerable<X509Extension> extensions,

src/Tools/FirstRunCertGenerator/test/CertificateManagerTests.cs

Lines changed: 15 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ public void EnsureCreateHttpsCertificate_CreatesACertificate_WhenThereAreNoHttps
5252
Assert.NotNull(exportedCertificate);
5353
Assert.False(exportedCertificate.HasPrivateKey);
5454

55-
var httpsCertificates = _manager.ListCertificates(CertificatePurpose.HTTPS, StoreName.My, StoreLocation.CurrentUser, isValid: false);
55+
var httpsCertificates = CertificateManager.ListCertificates(CertificatePurpose.HTTPS, StoreName.My, StoreLocation.CurrentUser, isValid: false);
5656
var httpsCertificate = Assert.Single(httpsCertificates, c => c.Subject == TestCertificateSubject);
5757
Assert.True(httpsCertificate.HasPrivateKey);
5858
Assert.Equal(TestCertificateSubject, httpsCertificate.Subject);
@@ -94,7 +94,7 @@ public void EnsureCreateHttpsCertificate_CreatesACertificate_WhenThereAreNoHttps
9494
httpsCertificate.Extensions.OfType<X509Extension>(),
9595
e => e.Critical == false &&
9696
e.Oid.Value == "1.3.6.1.4.1.311.84.1.1" &&
97-
e.RawData[0] == _manager.AspNetHttpsCertificateVersion);
97+
e.RawData[0] == CertificateManager.AspNetHttpsCertificateVersion);
9898

9999
Assert.Equal(httpsCertificate.GetCertHashString(), exportedCertificate.GetCertHashString());
100100

@@ -137,7 +137,7 @@ public void EnsureCreateHttpsCertificate_DoesNotCreateACertificate_WhenThereIsAn
137137
now = new DateTimeOffset(now.Year, now.Month, now.Day, now.Hour, now.Minute, now.Second, 0, now.Offset);
138138
_manager.EnsureAspNetCoreHttpsDevelopmentCertificate(now, now.AddYears(1), path: null, trust: false, subject: TestCertificateSubject);
139139

140-
var httpsCertificate = _manager.ListCertificates(CertificatePurpose.HTTPS, StoreName.My, StoreLocation.CurrentUser, isValid: false).Single(c => c.Subject == TestCertificateSubject);
140+
var httpsCertificate = CertificateManager.ListCertificates(CertificatePurpose.HTTPS, StoreName.My, StoreLocation.CurrentUser, isValid: false).Single(c => c.Subject == TestCertificateSubject);
141141

142142
// Act
143143
var result = _manager.EnsureAspNetCoreHttpsDevelopmentCertificate(now, now.AddYears(1), CertificateName, trust: false, includePrivateKey: true, password: certificatePassword, subject: TestCertificateSubject);
@@ -164,9 +164,9 @@ public void EnsureCreateHttpsCertificate_ReturnsExpiredCertificateIfVersionIsInc
164164
now = new DateTimeOffset(now.Year, now.Month, now.Day, now.Hour, now.Minute, now.Second, 0, now.Offset);
165165
_manager.EnsureAspNetCoreHttpsDevelopmentCertificate(now, now.AddYears(1), path: null, trust: false, subject: TestCertificateSubject);
166166

167-
_manager.AspNetHttpsCertificateVersion = 2;
167+
CertificateManager.AspNetHttpsCertificateVersion = 2;
168168

169-
var httpsCertificateList = _manager.ListCertificates(CertificatePurpose.HTTPS, StoreName.My, StoreLocation.CurrentUser, isValid: true);
169+
var httpsCertificateList = CertificateManager.ListCertificates(CertificatePurpose.HTTPS, StoreName.My, StoreLocation.CurrentUser, isValid: true);
170170
Assert.Empty(httpsCertificateList);
171171
}
172172

@@ -178,12 +178,12 @@ public void EnsureCreateHttpsCertificate_ReturnsExpiredCertificateForEmptyVersio
178178

179179
DateTimeOffset now = DateTimeOffset.UtcNow;
180180
now = new DateTimeOffset(now.Year, now.Month, now.Day, now.Hour, now.Minute, now.Second, 0, now.Offset);
181-
_manager.AspNetHttpsCertificateVersion = 0;
181+
CertificateManager.AspNetHttpsCertificateVersion = 0;
182182
_manager.EnsureAspNetCoreHttpsDevelopmentCertificate(now, now.AddYears(1), path: null, trust: false, subject: TestCertificateSubject);
183183

184-
_manager.AspNetHttpsCertificateVersion = 1;
184+
CertificateManager.AspNetHttpsCertificateVersion = 1;
185185

186-
var httpsCertificateList = _manager.ListCertificates(CertificatePurpose.HTTPS, StoreName.My, StoreLocation.CurrentUser, isValid: true);
186+
var httpsCertificateList = CertificateManager.ListCertificates(CertificatePurpose.HTTPS, StoreName.My, StoreLocation.CurrentUser, isValid: true);
187187
Assert.Empty(httpsCertificateList);
188188
}
189189

@@ -195,10 +195,10 @@ public void EnsureCreateHttpsCertificate_ReturnsValidIfVersionIsZero()
195195

196196
DateTimeOffset now = DateTimeOffset.UtcNow;
197197
now = new DateTimeOffset(now.Year, now.Month, now.Day, now.Hour, now.Minute, now.Second, 0, now.Offset);
198-
_manager.AspNetHttpsCertificateVersion = 0;
198+
CertificateManager.AspNetHttpsCertificateVersion = 0;
199199
_manager.EnsureAspNetCoreHttpsDevelopmentCertificate(now, now.AddYears(1), path: null, trust: false, subject: TestCertificateSubject);
200200

201-
var httpsCertificateList = _manager.ListCertificates(CertificatePurpose.HTTPS, StoreName.My, StoreLocation.CurrentUser, isValid: true);
201+
var httpsCertificateList = CertificateManager.ListCertificates(CertificatePurpose.HTTPS, StoreName.My, StoreLocation.CurrentUser, isValid: true);
202202
Assert.NotEmpty(httpsCertificateList);
203203
}
204204

@@ -210,11 +210,11 @@ public void EnsureCreateHttpsCertificate_ReturnValidIfCertIsNewer()
210210

211211
DateTimeOffset now = DateTimeOffset.UtcNow;
212212
now = new DateTimeOffset(now.Year, now.Month, now.Day, now.Hour, now.Minute, now.Second, 0, now.Offset);
213-
_manager.AspNetHttpsCertificateVersion = 2;
213+
CertificateManager.AspNetHttpsCertificateVersion = 2;
214214
_manager.EnsureAspNetCoreHttpsDevelopmentCertificate(now, now.AddYears(1), path: null, trust: false, subject: TestCertificateSubject);
215215

216-
_manager.AspNetHttpsCertificateVersion = 1;
217-
var httpsCertificateList = _manager.ListCertificates(CertificatePurpose.HTTPS, StoreName.My, StoreLocation.CurrentUser, isValid: true);
216+
CertificateManager.AspNetHttpsCertificateVersion = 1;
217+
var httpsCertificateList = CertificateManager.ListCertificates(CertificatePurpose.HTTPS, StoreName.My, StoreLocation.CurrentUser, isValid: true);
218218
Assert.NotEmpty(httpsCertificateList);
219219
}
220220

@@ -241,10 +241,10 @@ public void EnsureAspNetCoreHttpsDevelopmentCertificate_CanRemoveCertificates()
241241

242242
_manager.CleanupHttpsCertificates(TestCertificateSubject);
243243

244-
Assert.Empty(_manager.ListCertificates(CertificatePurpose.HTTPS, StoreName.My, StoreLocation.CurrentUser, isValid: false).Where(c => c.Subject == TestCertificateSubject));
244+
Assert.Empty(CertificateManager.ListCertificates(CertificatePurpose.HTTPS, StoreName.My, StoreLocation.CurrentUser, isValid: false).Where(c => c.Subject == TestCertificateSubject));
245245
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
246246
{
247-
Assert.Empty(_manager.ListCertificates(CertificatePurpose.HTTPS, StoreName.Root, StoreLocation.CurrentUser, isValid: false).Where(c => c.Subject == TestCertificateSubject));
247+
Assert.Empty(CertificateManager.ListCertificates(CertificatePurpose.HTTPS, StoreName.Root, StoreLocation.CurrentUser, isValid: false).Where(c => c.Subject == TestCertificateSubject));
248248
}
249249
}
250250
}

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -150,7 +150,7 @@ private static int CheckHttpsCertificate(CommandOption trust, IReporter reporter
150150
{
151151
var now = DateTimeOffset.Now;
152152
var certificateManager = new CertificateManager();
153-
var certificates = certificateManager.ListCertificates(CertificatePurpose.HTTPS, StoreName.My, StoreLocation.CurrentUser, isValid: true);
153+
var certificates = CertificateManager.ListCertificates(CertificatePurpose.HTTPS, StoreName.My, StoreLocation.CurrentUser, isValid: true);
154154
if (certificates.Count == 0)
155155
{
156156
reporter.Output("No valid certificate found.");
@@ -164,7 +164,7 @@ private static int CheckHttpsCertificate(CommandOption trust, IReporter reporter
164164
if (trust != null && trust.HasValue())
165165
{
166166
var store = RuntimeInformation.IsOSPlatform(OSPlatform.OSX) ? StoreName.My : StoreName.Root;
167-
var trustedCertificates = certificateManager.ListCertificates(CertificatePurpose.HTTPS, store, StoreLocation.CurrentUser, isValid: true);
167+
var trustedCertificates = CertificateManager.ListCertificates(CertificatePurpose.HTTPS, store, StoreLocation.CurrentUser, isValid: true);
168168
if (!certificates.Any(c => certificateManager.IsTrusted(c)))
169169
{
170170
reporter.Output($@"The following certificates were found, but none of them is trusted:

0 commit comments

Comments
 (0)