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
This commit is contained in:
David Fowler 2020-08-29 16:14:00 -07:00 committed by GitHub
parent 9f0eefb4c1
commit 5292fed790
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 141 additions and 3 deletions

View File

@ -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>

View File

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