diff --git a/src/Servers/Kestrel/Core/src/CoreStrings.resx b/src/Servers/Kestrel/Core/src/CoreStrings.resx
index 1dbd5c0435..293d84df90 100644
--- a/src/Servers/Kestrel/Core/src/CoreStrings.resx
+++ b/src/Servers/Kestrel/Core/src/CoreStrings.resx
@@ -632,4 +632,16 @@ For more information on configuring HTTPS see https://go.microsoft.com/fwlink/?l
The non-HTTPS endpoint {endpointName} includes HTTPS-only configuration for {keyName}.
+
+ Found certificate with private key and thumbprint {Thumbprint} in certificate store {StoreName}.
+
+
+ Searching for certificate with private key and thumbprint {Thumbprint} in the certificate store.
+
+
+ Failure to locate certificate from store.
+
+
+ Failed to open certificate store {StoreName}.
+
\ No newline at end of file
diff --git a/src/Servers/Kestrel/Core/src/Middleware/HttpsConnectionMiddleware.cs b/src/Servers/Kestrel/Core/src/Middleware/HttpsConnectionMiddleware.cs
index b131323693..6626fd04a0 100644
--- a/src/Servers/Kestrel/Core/src/Middleware/HttpsConnectionMiddleware.cs
+++ b/src/Servers/Kestrel/Core/src/Middleware/HttpsConnectionMiddleware.cs
@@ -4,11 +4,14 @@
using System;
using System.Buffers;
using System.Collections.Generic;
+using System.Diagnostics;
using System.IO;
using System.IO.Pipelines;
using System.Net.Security;
using System.Runtime.InteropServices;
+using System.Security;
using System.Security.Authentication;
+using System.Security.Cryptography;
using System.Security.Cryptography.X509Certificates;
using System.Threading;
using System.Threading.Tasks;
@@ -91,9 +94,18 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Https.Internal
{
EnsureCertificateIsAllowedForServerAuth(_serverCertificate);
+ var certificate = _serverCertificate;
+ if (!certificate.HasPrivateKey)
+ {
+ // SslStream historically has logic to deal with certificate missing private keys.
+ // By resolving the SslStreamCertificateContext eagerly, we circumvent this logic so
+ // try to resolve the certificate from the store if there's no private key in the cert.
+ certificate = LocateCertificateWithPrivateKey(certificate);
+ }
+
// This might be do blocking IO but it'll resolve the certificate chain up front before any connections are
// made to the server
- _serverCertificateContext = SslStreamCertificateContext.Create(_serverCertificate, additionalCertificates: null);
+ _serverCertificateContext = SslStreamCertificateContext.Create(certificate, additionalCertificates: null);
}
var remoteCertificateValidationCallback = _options.ClientCertificateMode == ClientCertificateMode.NoCertificate ?
@@ -215,6 +227,78 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Https.Internal
}
}
+ // This logic is replicated from https://github.com/dotnet/runtime/blob/02b24db7cada5d5806c5cc513e61e44fb2a41944/src/libraries/System.Net.Security/src/System/Net/Security/SecureChannel.cs#L195-L262
+ // but with public APIs
+ private X509Certificate2 LocateCertificateWithPrivateKey(X509Certificate2 certificate)
+ {
+ Debug.Assert(!certificate.HasPrivateKey, "This should only be called with certificates that don't have a private key");
+
+ _logger.LocatingCertWithPrivateKey(certificate);
+
+ X509Store OpenStore(StoreLocation storeLocation)
+ {
+ try
+ {
+ var store = new X509Store(StoreName.My, storeLocation);
+ store.Open(OpenFlags.ReadOnly);
+ return store;
+ }
+ catch (Exception exception)
+ {
+ if (exception is CryptographicException || exception is SecurityException)
+ {
+ _logger.FailedToOpenStore(storeLocation, exception);
+ return null;
+ }
+
+ throw;
+ }
+ }
+
+ try
+ {
+ var store = OpenStore(StoreLocation.LocalMachine);
+
+ if (store != null)
+ {
+ using (store)
+ {
+ var certs = store.Certificates.Find(X509FindType.FindByThumbprint, certificate.Thumbprint, validOnly: false);
+
+ if (certs.Count > 0 && certs[0].HasPrivateKey)
+ {
+ _logger.FoundCertWithPrivateKey(certs[0], StoreLocation.LocalMachine);
+ return certs[0];
+ }
+ }
+ }
+
+ store = OpenStore(StoreLocation.CurrentUser);
+
+ if (store != null)
+ {
+ using (store)
+ {
+ var certs = store.Certificates.Find(X509FindType.FindByThumbprint, certificate.Thumbprint, validOnly: false);
+
+ if (certs.Count > 0 && certs[0].HasPrivateKey)
+ {
+ _logger.FoundCertWithPrivateKey(certs[0], StoreLocation.CurrentUser);
+ return certs[0];
+ }
+ }
+ }
+ }
+ catch (CryptographicException ex)
+ {
+ // Log as debug since this error is expected an swallowed
+ _logger.FailedToFindCertificateInStore(ex);
+ }
+
+ // Return the cert, and it will fail later
+ return certificate;
+ }
+
private Task DoOptionsBasedHandshakeAsync(ConnectionContext context, SslStream sslStream, Core.Internal.TlsConnectionFeature feature, CancellationToken cancellationToken)
{
// Adapt to the SslStream signature
@@ -396,7 +480,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Https.Internal
{
var enableHttp2OnWindows81 = AppContext.TryGetSwitch(EnableWindows81Http2, out var enabled) && enabled;
if (Environment.OSVersion.Version < new Version(6, 3) // Missing ALPN support
- // Win8.1 and 2012 R2 don't support the right cipher configuration by default.
+ // Win8.1 and 2012 R2 don't support the right cipher configuration by default.
|| (Environment.OSVersion.Version < new Version(10, 0) && !enableHttp2OnWindows81))
{
return true;
@@ -409,7 +493,6 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Https.Internal
internal static class HttpsConnectionMiddlewareLoggerExtensions
{
-
private static readonly Action _authenticationFailed =
LoggerMessage.Define(
logLevel: LogLevel.Debug,
@@ -434,6 +517,31 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Https.Internal
eventId: new EventId(4, "Http2DefaultCiphersInsufficient"),
formatString: CoreStrings.Http2DefaultCiphersInsufficient);
+ private static readonly Action _locatingCertWithPrivateKey =
+ LoggerMessage.Define(
+ logLevel: LogLevel.Debug,
+ eventId: new EventId(5, "LocateCertWithPrivateKey"),
+ formatString: CoreStrings.LocatingCertWithPrivateKey);
+
+ private static readonly Action _foundCertWithPrivateKey =
+ LoggerMessage.Define(
+ logLevel: LogLevel.Debug,
+ eventId: new EventId(6, "FoundCertWithPrivateKey"),
+ formatString: CoreStrings.FoundCertWithPrivateKey);
+
+ private static readonly Action _failedToFindCertificateInStore =
+ LoggerMessage.Define(
+ logLevel: LogLevel.Debug,
+ eventId: new EventId(7, "FailToLocateCertificate"),
+ formatString: CoreStrings.FailedToLocateCertificateFromStore);
+
+
+ private static readonly Action _failedToOpenCertificateStore =
+ LoggerMessage.Define(
+ logLevel: LogLevel.Debug,
+ eventId: new EventId(8, "FailToOpenStore"),
+ formatString: CoreStrings.FailedToOpenCertStore);
+
public static void AuthenticationFailed(this ILogger logger, Exception exception) => _authenticationFailed(logger, exception);
public static void AuthenticationTimedOut(this ILogger logger) => _authenticationTimedOut(logger, null);
@@ -441,5 +549,23 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Https.Internal
public static void HttpsConnectionEstablished(this ILogger logger, string connectionId, SslProtocols sslProtocol) => _httpsConnectionEstablished(logger, connectionId, sslProtocol, null);
public static void Http2DefaultCiphersInsufficient(this ILogger logger) => _http2DefaultCiphersInsufficient(logger, null);
+
+ public static void LocatingCertWithPrivateKey(this ILogger logger, X509Certificate2 certificate) => _locatingCertWithPrivateKey(logger, certificate.Thumbprint, null);
+
+ public static void FoundCertWithPrivateKey(this ILogger logger, X509Certificate2 certificate, StoreLocation storeLocation)
+ {
+ var storeLocationString = storeLocation == StoreLocation.LocalMachine ? nameof(StoreLocation.LocalMachine) : nameof(StoreLocation.CurrentUser);
+
+ _foundCertWithPrivateKey(logger, certificate.Thumbprint, storeLocationString, null);
+ }
+
+ public static void FailedToFindCertificateInStore(this ILogger logger, Exception exception) => _failedToFindCertificateInStore(logger, exception);
+
+ public static void FailedToOpenStore(this ILogger logger, StoreLocation storeLocation, Exception exception)
+ {
+ var storeLocationString = storeLocation == StoreLocation.LocalMachine ? nameof(StoreLocation.LocalMachine) : nameof(StoreLocation.CurrentUser);
+
+ _failedToOpenCertificateStore(logger, storeLocationString, exception);
+ }
}
}