Преглед на файлове

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
David Fowler преди 5 години
родител
ревизия
5292fed790
променени са 2 файла, в които са добавени 141 реда и са изтрити 3 реда
  1. 12 0
      src/Servers/Kestrel/Core/src/CoreStrings.resx
  2. 129 3
      src/Servers/Kestrel/Core/src/Middleware/HttpsConnectionMiddleware.cs

+ 12 - 0
src/Servers/Kestrel/Core/src/CoreStrings.resx

@@ -632,4 +632,16 @@ For more information on configuring HTTPS see https://go.microsoft.com/fwlink/?l
   <data name="EndpointHasUnusedHttpsConfig" xml:space="preserve">
     <value>The non-HTTPS endpoint {endpointName} includes HTTPS-only configuration for {keyName}.</value>
   </data>
+  <data name="FoundCertWithPrivateKey" xml:space="preserve">
+    <value>Found certificate with private key and thumbprint {Thumbprint} in certificate store {StoreName}.</value>
+  </data>
+  <data name="LocatingCertWithPrivateKey" xml:space="preserve">
+    <value>Searching for certificate with private key and thumbprint {Thumbprint} in the certificate store.</value>
+  </data>
+  <data name="FailedToLocateCertificateFromStore" xml:space="preserve">
+    <value>Failure to locate certificate from store.</value>
+  </data>
+  <data name="FailedToOpenCertStore" xml:space="preserve">
+    <value>Failed to open certificate store {StoreName}.</value>
+  </data>
 </root>

+ 129 - 3
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<ILogger, Exception> _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<ILogger, string, Exception> _locatingCertWithPrivateKey =
+            LoggerMessage.Define<string>(
+                logLevel: LogLevel.Debug,
+                eventId: new EventId(5, "LocateCertWithPrivateKey"),
+                formatString: CoreStrings.LocatingCertWithPrivateKey);
+
+        private static readonly Action<ILogger, string, string, Exception> _foundCertWithPrivateKey =
+            LoggerMessage.Define<string, string>(
+                logLevel: LogLevel.Debug,
+                eventId: new EventId(6, "FoundCertWithPrivateKey"),
+                formatString: CoreStrings.FoundCertWithPrivateKey);
+
+        private static readonly Action<ILogger, Exception> _failedToFindCertificateInStore =
+            LoggerMessage.Define(
+                logLevel: LogLevel.Debug,
+                eventId: new EventId(7, "FailToLocateCertificate"),
+                formatString: CoreStrings.FailedToLocateCertificateFromStore);
+
+
+        private static readonly Action<ILogger, string, Exception> _failedToOpenCertificateStore =
+            LoggerMessage.Define<string>(
+                logLevel: LogLevel.Debug,
+                eventId: new EventId(8, "FailToOpenStore"),
+                formatString: CoreStrings.FailedToOpenCertStore);
+
         public static void AuthenticationFailed(this ILogger<HttpsConnectionMiddleware> logger, Exception exception) => _authenticationFailed(logger, exception);
 
         public static void AuthenticationTimedOut(this ILogger<HttpsConnectionMiddleware> logger) => _authenticationTimedOut(logger, null);
@@ -441,5 +549,23 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Https.Internal
         public static void HttpsConnectionEstablished(this ILogger<HttpsConnectionMiddleware> logger, string connectionId, SslProtocols sslProtocol) => _httpsConnectionEstablished(logger, connectionId, sslProtocol, null);
 
         public static void Http2DefaultCiphersInsufficient(this ILogger<HttpsConnectionMiddleware> logger) => _http2DefaultCiphersInsufficient(logger, null);
+
+        public static void LocatingCertWithPrivateKey(this ILogger<HttpsConnectionMiddleware> logger, X509Certificate2 certificate) => _locatingCertWithPrivateKey(logger, certificate.Thumbprint, null);
+
+        public static void FoundCertWithPrivateKey(this ILogger<HttpsConnectionMiddleware> 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<HttpsConnectionMiddleware> logger, Exception exception) => _failedToFindCertificateInStore(logger, exception);
+
+        public static void FailedToOpenStore(this ILogger<HttpsConnectionMiddleware> logger, StoreLocation storeLocation, Exception exception)
+        {
+            var storeLocationString = storeLocation == StoreLocation.LocalMachine ? nameof(StoreLocation.LocalMachine) : nameof(StoreLocation.CurrentUser);
+
+            _failedToOpenCertificateStore(logger, storeLocationString, exception);
+        }
     }
 }