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