// Copyright (c) .NET Foundation. All rights reserved. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System; using System.Collections.Generic; using System.IO; using System.Net.Security; using System.Security.Cryptography.X509Certificates; using System.Threading; using System.Threading.Tasks; using Microsoft.AspNetCore.Connections; using Microsoft.AspNetCore.Http.Features; using Microsoft.AspNetCore.Server.Kestrel.Core; using Microsoft.AspNetCore.Server.Kestrel.Core.Adapter.Internal; using Microsoft.AspNetCore.Server.Kestrel.Core.Features; using Microsoft.Extensions.Logging; namespace Microsoft.AspNetCore.Server.Kestrel.Https.Internal { public class HttpsConnectionAdapter : IConnectionAdapter { private static readonly ClosedAdaptedConnection _closedAdaptedConnection = new ClosedAdaptedConnection(); private readonly HttpsConnectionAdapterOptions _options; private readonly X509Certificate2 _serverCertificate; private readonly Func _serverCertificateSelector; private readonly ILogger _logger; public HttpsConnectionAdapter(HttpsConnectionAdapterOptions options) : this(options, loggerFactory: null) { } public HttpsConnectionAdapter(HttpsConnectionAdapterOptions options, ILoggerFactory loggerFactory) { if (options == null) { throw new ArgumentNullException(nameof(options)); } // capture the certificate now so it can't be switched after validation _serverCertificate = options.ServerCertificate; _serverCertificateSelector = options.ServerCertificateSelector; if (_serverCertificate == null && _serverCertificateSelector == null) { throw new ArgumentException(CoreStrings.ServerCertificateRequired, nameof(options)); } // If a selector is provided then ignore the cert, it may be a default cert. if (_serverCertificateSelector != null) { // SslStream doesn't allow both. _serverCertificate = null; } else { EnsureCertificateIsAllowedForServerAuth(_serverCertificate); } _options = options; _logger = loggerFactory?.CreateLogger(nameof(HttpsConnectionAdapter)); } public bool IsHttps => true; public Task OnConnectionAsync(ConnectionAdapterContext context) { // Don't trust SslStream not to block. return Task.Run(() => InnerOnConnectionAsync(context)); } private async Task InnerOnConnectionAsync(ConnectionAdapterContext context) { SslStream sslStream; bool certificateRequired; var feature = new TlsConnectionFeature(); context.Features.Set(feature); if (_options.ClientCertificateMode == ClientCertificateMode.NoCertificate) { sslStream = new SslStream(context.ConnectionStream); certificateRequired = false; } else { sslStream = new SslStream(context.ConnectionStream, leaveInnerStreamOpen: false, userCertificateValidationCallback: (sender, certificate, chain, sslPolicyErrors) => { if (certificate == null) { return _options.ClientCertificateMode != ClientCertificateMode.RequireCertificate; } if (_options.ClientCertificateValidation == null) { if (sslPolicyErrors != SslPolicyErrors.None) { return false; } } var certificate2 = ConvertToX509Certificate2(certificate); if (certificate2 == null) { return false; } if (_options.ClientCertificateValidation != null) { if (!_options.ClientCertificateValidation(certificate2, chain, sslPolicyErrors)) { return false; } } return true; }); certificateRequired = true; } var timeoutFeature = context.Features.Get(); timeoutFeature.SetTimeout(_options.HandshakeTimeout); try { #if NETCOREAPP2_1 // Adapt to the SslStream signature ServerCertificateSelectionCallback selector = null; if (_serverCertificateSelector != null) { selector = (sender, name) => { context.Features.Set(sslStream); var cert = _serverCertificateSelector(context.ConnectionContext, name); if (cert != null) { EnsureCertificateIsAllowedForServerAuth(cert); } return cert; }; } var sslOptions = new SslServerAuthenticationOptions() { ServerCertificate = _serverCertificate, ServerCertificateSelectionCallback = selector, ClientCertificateRequired = certificateRequired, EnabledSslProtocols = _options.SslProtocols, CertificateRevocationCheckMode = _options.CheckCertificateRevocation ? X509RevocationMode.Online : X509RevocationMode.NoCheck, ApplicationProtocols = new List() }; // This is order sensitive if ((_options.HttpProtocols & HttpProtocols.Http2) != 0) { sslOptions.ApplicationProtocols.Add(SslApplicationProtocol.Http2); } if ((_options.HttpProtocols & HttpProtocols.Http1) != 0) { sslOptions.ApplicationProtocols.Add(SslApplicationProtocol.Http11); } await sslStream.AuthenticateAsServerAsync(sslOptions, CancellationToken.None); #else var serverCert = _serverCertificate; if (_serverCertificateSelector != null) { context.Features.Set(sslStream); serverCert = _serverCertificateSelector(context.ConnectionContext, null); if (serverCert != null) { EnsureCertificateIsAllowedForServerAuth(serverCert); } } await sslStream.AuthenticateAsServerAsync(serverCert, certificateRequired, _options.SslProtocols, _options.CheckCertificateRevocation); #endif } catch (OperationCanceledException) { _logger?.LogDebug(2, CoreStrings.AuthenticationTimedOut); sslStream.Dispose(); return _closedAdaptedConnection; } catch (IOException ex) { _logger?.LogDebug(1, ex, CoreStrings.AuthenticationFailed); sslStream.Dispose(); return _closedAdaptedConnection; } finally { timeoutFeature.CancelTimeout(); } #if NETCOREAPP2_1 feature.ApplicationProtocol = sslStream.NegotiatedApplicationProtocol.Protocol; context.Features.Set(feature); #endif feature.ClientCertificate = ConvertToX509Certificate2(sslStream.RemoteCertificate); return new HttpsAdaptedConnection(sslStream); } private static void EnsureCertificateIsAllowedForServerAuth(X509Certificate2 certificate) { if (!CertificateLoader.IsCertificateAllowedForServerAuth(certificate)) { throw new InvalidOperationException(CoreStrings.FormatInvalidServerCertificateEku(certificate.Thumbprint)); } } private static X509Certificate2 ConvertToX509Certificate2(X509Certificate certificate) { if (certificate == null) { return null; } if (certificate is X509Certificate2 cert2) { return cert2; } return new X509Certificate2(certificate); } private class HttpsAdaptedConnection : IAdaptedConnection { private readonly SslStream _sslStream; public HttpsAdaptedConnection(SslStream sslStream) { _sslStream = sslStream; } public Stream ConnectionStream => _sslStream; public void Dispose() { _sslStream.Dispose(); } } private class ClosedAdaptedConnection : IAdaptedConnection { public Stream ConnectionStream { get; } = new ClosedStream(); public void Dispose() { } } } }