From 5292fed7909a8fe35c6a0de40ffca25868e79db5 Mon Sep 17 00:00:00 2001 From: David Fowler Date: Sat, 29 Aug 2020 16:14:00 -0700 Subject: [PATCH] Restore legacy behavior for certs without private keys (#25344) * Restore legacy behavior for certs without private keys - When trying to use an SSL certificate without a private key, SslStream would try to find another certificate in the cert store matching the thumbprint. Now that we're using the SslStreamCertificateContext, that behavor is no longer included so we need to restore it in Kestrel. * Handle cert store failing to open --- src/Servers/Kestrel/Core/src/CoreStrings.resx | 12 ++ .../Middleware/HttpsConnectionMiddleware.cs | 132 +++++++++++++++++- 2 files changed, 141 insertions(+), 3 deletions(-) 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); + } } }