From a8b037fcc948ce907361564c093f8c24021a2c22 Mon Sep 17 00:00:00 2001 From: Chris R Date: Thu, 3 Jun 2021 14:32:26 -0700 Subject: [PATCH 01/19] Support client cert negotation #23948 --- .../Core/src/Internal/TlsConnectionFeature.cs | 14 ++- .../Middleware/HttpsConnectionMiddleware.cs | 5 +- .../HttpsConnectionMiddlewareTests.cs | 100 +++++++++++++++++- 3 files changed, 112 insertions(+), 7 deletions(-) diff --git a/src/Servers/Kestrel/Core/src/Internal/TlsConnectionFeature.cs b/src/Servers/Kestrel/Core/src/Internal/TlsConnectionFeature.cs index e5631645790d..0d7d1738a755 100644 --- a/src/Servers/Kestrel/Core/src/Internal/TlsConnectionFeature.cs +++ b/src/Servers/Kestrel/Core/src/Internal/TlsConnectionFeature.cs @@ -25,6 +25,7 @@ internal class TlsConnectionFeature : ITlsConnectionFeature, ITlsApplicationProt private int? _hashStrength; private ExchangeAlgorithmType? _keyExchangeAlgorithm; private int? _keyExchangeStrength; + private bool _renegotiated; public TlsConnectionFeature(SslStream sslStream) { @@ -97,9 +98,18 @@ public int KeyExchangeStrength set => _keyExchangeStrength = value; } - public Task GetClientCertificateAsync(CancellationToken cancellationToken) + public async Task GetClientCertificateAsync(CancellationToken cancellationToken) { - return Task.FromResult(ClientCertificate); + if (ClientCertificate != null || _renegotiated + // Delayed client cert negotiation is not allowed on HTTP/2. + || _sslStream.NegotiatedApplicationProtocol == SslApplicationProtocol.Http2) + { + return ClientCertificate; + } + + _renegotiated = true; // Only try once + await _sslStream.NegotiateClientCertificateAsync(cancellationToken); + return ClientCertificate; } private static X509Certificate2? ConvertToX509Certificate2(X509Certificate? certificate) diff --git a/src/Servers/Kestrel/Core/src/Middleware/HttpsConnectionMiddleware.cs b/src/Servers/Kestrel/Core/src/Middleware/HttpsConnectionMiddleware.cs index c46aff747b7f..e8446b8d7a4a 100644 --- a/src/Servers/Kestrel/Core/src/Middleware/HttpsConnectionMiddleware.cs +++ b/src/Servers/Kestrel/Core/src/Middleware/HttpsConnectionMiddleware.cs @@ -112,10 +112,7 @@ public HttpsConnectionMiddleware(ConnectionDelegate next, HttpsConnectionAdapter _serverCertificateContext = SslStreamCertificateContext.Create(certificate, additionalCertificates: null); } - var remoteCertificateValidationCallback = _options.ClientCertificateMode == ClientCertificateMode.NoCertificate ? - (RemoteCertificateValidationCallback?)null : RemoteCertificateValidationCallback; - - _sslStreamFactory = s => new SslStream(s, leaveInnerStreamOpen: false, userCertificateValidationCallback: remoteCertificateValidationCallback); + _sslStreamFactory = s => new SslStream(s, leaveInnerStreamOpen: false, RemoteCertificateValidationCallback); } internal HttpsConnectionMiddleware( diff --git a/src/Servers/Kestrel/test/InMemory.FunctionalTests/HttpsConnectionMiddlewareTests.cs b/src/Servers/Kestrel/test/InMemory.FunctionalTests/HttpsConnectionMiddlewareTests.cs index 61fdb0586b0f..19a6893868fe 100644 --- a/src/Servers/Kestrel/test/InMemory.FunctionalTests/HttpsConnectionMiddlewareTests.cs +++ b/src/Servers/Kestrel/test/InMemory.FunctionalTests/HttpsConnectionMiddlewareTests.cs @@ -69,7 +69,7 @@ public async Task CanReadAndWriteWithHttpsConnectionMiddlewareWithPemCertificate var env = new Mock(); env.SetupGet(e => e.ContentRootPath).Returns(Directory.GetCurrentDirectory()); - var serviceProvider = new ServiceCollection().AddLogging().BuildServiceProvider(); + var serviceProvider = new ServiceCollection().AddLogging().BuildServiceProvider(); options.ApplicationServices = serviceProvider; var logger = serviceProvider.GetRequiredService>(); @@ -487,6 +487,104 @@ void ConfigureListenOptions(ListenOptions listenOptions) } } + [Fact] + public async Task CanRenegotiateForClientCertificateOnHttp1() + { + void ConfigureListenOptions(ListenOptions listenOptions) + { + listenOptions.Protocols = HttpProtocols.Http1; + listenOptions.UseHttps(options => + { + options.ServerCertificate = _x509Certificate2; + options.ClientCertificateMode = ClientCertificateMode.NoCertificate; + options.AllowAnyClientCertificate(); + }); + } + + await using var server = new TestServer(async context => + { + var tlsFeature = context.Features.Get(); + Assert.NotNull(tlsFeature); + Assert.Null(tlsFeature.ClientCertificate); + Assert.Null(context.Connection.ClientCertificate); + + var clientCert = await context.Connection.GetClientCertificateAsync(); + Assert.NotNull(clientCert); + Assert.NotNull(tlsFeature.ClientCertificate); + Assert.NotNull(context.Connection.ClientCertificate); + + await context.Response.WriteAsync("hello world"); + }, new TestServiceContext(LoggerFactory), ConfigureListenOptions); + + using var connection = server.CreateConnection(); + // SslStream is used to ensure the certificate is actually passed to the server + // HttpClient might not send the certificate because it is invalid or it doesn't match any + // of the certificate authorities sent by the server in the SSL handshake. + var stream = new SslStream(connection.Stream); + var clientOptions = new SslClientAuthenticationOptions() + { + TargetHost = "localhost", + EnabledSslProtocols = SslProtocols.Tls | SslProtocols.Tls11 | SslProtocols.Tls12, + }; + clientOptions.RemoteCertificateValidationCallback = (sender, certificate, chain, sslPolicyErrors) => true; + clientOptions.LocalCertificateSelectionCallback + = (sender, targetHost, localCertificates, remoteCertificate, acceptableIssuers) => _x509Certificate2; + + await stream.AuthenticateAsClientAsync(clientOptions); + await AssertConnectionResult(stream, true); + } + + [Fact] + // Turning on HTTP/2 disables renegotiation. + // TODO: Tomas is changing it so AllowRenegotiation only applies to the remote, + // NegotiateClientCertificateAsync call be called locally and it's up to us to prevent that + // on HTTP/2. + public async Task ClientCertificateRenegotationDisabledOnHttp1WithHttp2() + { + void ConfigureListenOptions(ListenOptions listenOptions) + { + listenOptions.Protocols = HttpProtocols.Http1AndHttp2; + listenOptions.UseHttps(options => + { + options.ServerCertificate = _x509Certificate2; + options.ClientCertificateMode = ClientCertificateMode.NoCertificate; + options.AllowAnyClientCertificate(); + }); + } + + await using var server = new TestServer(async context => + { + var tlsFeature = context.Features.Get(); + Assert.NotNull(tlsFeature); + Assert.Null(tlsFeature.ClientCertificate); + Assert.Null(context.Connection.ClientCertificate); + + var ex = await Assert.ThrowsAsync(() => context.Connection.GetClientCertificateAsync()); + Assert.Equal("The remote party requested renegotiation when AllowRenegotiation was set to false.", ex.Message); + Assert.Null(tlsFeature.ClientCertificate); + Assert.Null(context.Connection.ClientCertificate); + + await context.Response.WriteAsync("hello world"); + }, new TestServiceContext(LoggerFactory), ConfigureListenOptions); + + using var connection = server.CreateConnection(); + // SslStream is used to ensure the certificate is actually passed to the server + // HttpClient might not send the certificate because it is invalid or it doesn't match any + // of the certificate authorities sent by the server in the SSL handshake. + var stream = new SslStream(connection.Stream); + var clientOptions = new SslClientAuthenticationOptions() + { + TargetHost = "localhost", + EnabledSslProtocols = SslProtocols.Tls | SslProtocols.Tls11 | SslProtocols.Tls12, + }; + clientOptions.RemoteCertificateValidationCallback = (sender, certificate, chain, sslPolicyErrors) => true; + clientOptions.LocalCertificateSelectionCallback + = (sender, targetHost, localCertificates, remoteCertificate, acceptableIssuers) => _x509Certificate2; + + await stream.AuthenticateAsClientAsync(clientOptions); + await AssertConnectionResult(stream, true); + } + [Fact] public async Task HttpsSchemePassedToRequestFeature() { From 7aa1bd014e78f67f0c78b0043be67d572ccec5bd Mon Sep 17 00:00:00 2001 From: Chris R Date: Fri, 4 Jun 2021 15:33:43 -0700 Subject: [PATCH 02/19] Add UseHttps overload --- .../Kestrel/Core/src/ClientCertificateMode.cs | 8 +- .../Core/src/Internal/TlsConnectionFeature.cs | 9 +- .../Core/src/KestrelConfigurationLoader.cs | 2 +- .../Core/src/ListenOptionsHttpsExtensions.cs | 26 ++++- .../Middleware/HttpsConnectionMiddleware.cs | 14 ++- .../Kestrel/Core/src/PublicAPI.Unshipped.txt | 2 + .../HttpsConnectionMiddlewareTests.cs | 103 +++++++++++++++++- 7 files changed, 149 insertions(+), 15 deletions(-) diff --git a/src/Servers/Kestrel/Core/src/ClientCertificateMode.cs b/src/Servers/Kestrel/Core/src/ClientCertificateMode.cs index caff5e041ac8..afa129aba3f3 100644 --- a/src/Servers/Kestrel/Core/src/ClientCertificateMode.cs +++ b/src/Servers/Kestrel/Core/src/ClientCertificateMode.cs @@ -21,6 +21,12 @@ public enum ClientCertificateMode /// /// A client certificate will be requested, and the client must provide a valid certificate for authentication to succeed. /// - RequireCertificate + RequireCertificate, + + /// + /// A client certificate is not required and will not be requested from clients at the start of the connection. + /// It may be requested by the application later. + /// + DelayCertificate, } } diff --git a/src/Servers/Kestrel/Core/src/Internal/TlsConnectionFeature.cs b/src/Servers/Kestrel/Core/src/Internal/TlsConnectionFeature.cs index 0d7d1738a755..c0b814345fe3 100644 --- a/src/Servers/Kestrel/Core/src/Internal/TlsConnectionFeature.cs +++ b/src/Servers/Kestrel/Core/src/Internal/TlsConnectionFeature.cs @@ -10,12 +10,14 @@ using Microsoft.AspNetCore.Connections.Features; using Microsoft.AspNetCore.Http.Features; using Microsoft.AspNetCore.Server.Kestrel.Core.Features; +using Microsoft.AspNetCore.Server.Kestrel.Https; namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal { internal class TlsConnectionFeature : ITlsConnectionFeature, ITlsApplicationProtocolFeature, ITlsHandshakeFeature { private readonly SslStream _sslStream; + private readonly ClientCertificateMode _clientCertificateMode; private X509Certificate2? _clientCert; private ReadOnlyMemory? _applicationProtocol; private SslProtocols? _protocol; @@ -27,7 +29,7 @@ internal class TlsConnectionFeature : ITlsConnectionFeature, ITlsApplicationProt private int? _keyExchangeStrength; private bool _renegotiated; - public TlsConnectionFeature(SslStream sslStream) + public TlsConnectionFeature(SslStream sslStream, ClientCertificateMode clientCertificateMode) { if (sslStream is null) { @@ -35,6 +37,7 @@ public TlsConnectionFeature(SslStream sslStream) } _sslStream = sslStream; + _clientCertificateMode = clientCertificateMode; } public X509Certificate2? ClientCertificate @@ -100,7 +103,9 @@ public int KeyExchangeStrength public async Task GetClientCertificateAsync(CancellationToken cancellationToken) { - if (ClientCertificate != null || _renegotiated + if (ClientCertificate != null + || _clientCertificateMode != ClientCertificateMode.DelayCertificate + || _renegotiated // Delayed client cert negotiation is not allowed on HTTP/2. || _sslStream.NegotiatedApplicationProtocol == SslApplicationProtocol.Http2) { diff --git a/src/Servers/Kestrel/Core/src/KestrelConfigurationLoader.cs b/src/Servers/Kestrel/Core/src/KestrelConfigurationLoader.cs index e32b04fd6d93..22b78d8d96be 100644 --- a/src/Servers/Kestrel/Core/src/KestrelConfigurationLoader.cs +++ b/src/Servers/Kestrel/Core/src/KestrelConfigurationLoader.cs @@ -401,7 +401,7 @@ public void Load() else { var sniOptionsSelector = new SniOptionsSelector(endpoint.Name, endpoint.Sni, CertificateConfigLoader, httpsOptions, listenOptions.Protocols, HttpsLogger); - listenOptions.UseHttps(SniOptionsSelector.OptionsCallback, sniOptionsSelector, httpsOptions.HandshakeTimeout); + listenOptions.UseHttps(SniOptionsSelector.OptionsCallback, sniOptionsSelector, httpsOptions.HandshakeTimeout, ClientCertificateMode.NoCertificate); } } diff --git a/src/Servers/Kestrel/Core/src/ListenOptionsHttpsExtensions.cs b/src/Servers/Kestrel/Core/src/ListenOptionsHttpsExtensions.cs index ff2b007e66a4..09fcee838ed4 100644 --- a/src/Servers/Kestrel/Core/src/ListenOptionsHttpsExtensions.cs +++ b/src/Servers/Kestrel/Core/src/ListenOptionsHttpsExtensions.cs @@ -254,14 +254,31 @@ public static ListenOptions UseHttps(this ListenOptions listenOptions, ServerOpt /// State for the . /// Specifies the maximum amount of time allowed for the TLS/SSL handshake. This must be positive and finite. /// The . - public static ListenOptions UseHttps(this ListenOptions listenOptions, ServerOptionsSelectionCallback serverOptionsSelectionCallback, object state, TimeSpan handshakeTimeout) + public static ListenOptions UseHttps(this ListenOptions listenOptions, ServerOptionsSelectionCallback serverOptionsSelectionCallback, + object state, TimeSpan handshakeTimeout) + { + return listenOptions.UseHttps(serverOptionsSelectionCallback, state, handshakeTimeout, ClientCertificateMode.NoCertificate); + } + + /// + /// Configure Kestrel to use HTTPS. This does not use default certificates or other defaults specified via config or + /// . + /// + /// The to configure. + /// Callback to configure HTTPS options. + /// State for the . + /// Specifies the maximum amount of time allowed for the TLS/SSL handshake. This must be positive and finite. + /// The mode used for accepting client certificates. + /// The . + public static ListenOptions UseHttps(this ListenOptions listenOptions, ServerOptionsSelectionCallback serverOptionsSelectionCallback, + object state, TimeSpan handshakeTimeout, ClientCertificateMode clientCertificateMode) { // HttpsOptionsCallback is an internal delegate that is just the ServerOptionsSelectionCallback + a ConnectionContext parameter. // Given that ConnectionContext will eventually be replaced by System.Net.Connections, it doesn't make much sense to make the HttpsOptionsCallback delegate public. HttpsOptionsCallback adaptedCallback = (connection, stream, clientHelloInfo, state, cancellationToken) => serverOptionsSelectionCallback(stream, clientHelloInfo, state, cancellationToken); - return listenOptions.UseHttps(adaptedCallback, state, handshakeTimeout); + return listenOptions.UseHttps(adaptedCallback, state, handshakeTimeout, clientCertificateMode); } /// @@ -271,15 +288,16 @@ public static ListenOptions UseHttps(this ListenOptions listenOptions, ServerOpt /// Callback to configure HTTPS options. /// State for the . /// Specifies the maximum amount of time allowed for the TLS/SSL handshake. This must be positive and finite. + /// The mode used for accepting client certificates. /// The . - internal static ListenOptions UseHttps(this ListenOptions listenOptions, HttpsOptionsCallback httpsOptionsCallback, object state, TimeSpan handshakeTimeout) + internal static ListenOptions UseHttps(this ListenOptions listenOptions, HttpsOptionsCallback httpsOptionsCallback, object state, TimeSpan handshakeTimeout, ClientCertificateMode clientCertificateMode) { var loggerFactory = listenOptions.KestrelServerOptions?.ApplicationServices.GetRequiredService() ?? NullLoggerFactory.Instance; listenOptions.IsTls = true; listenOptions.Use(next => { - var middleware = new HttpsConnectionMiddleware(next, httpsOptionsCallback, state, handshakeTimeout, loggerFactory); + var middleware = new HttpsConnectionMiddleware(next, httpsOptionsCallback, state, handshakeTimeout, loggerFactory, clientCertificateMode); return middleware.OnConnectionAsync; }); diff --git a/src/Servers/Kestrel/Core/src/Middleware/HttpsConnectionMiddleware.cs b/src/Servers/Kestrel/Core/src/Middleware/HttpsConnectionMiddleware.cs index e8446b8d7a4a..ec943c0221f5 100644 --- a/src/Servers/Kestrel/Core/src/Middleware/HttpsConnectionMiddleware.cs +++ b/src/Servers/Kestrel/Core/src/Middleware/HttpsConnectionMiddleware.cs @@ -38,6 +38,7 @@ internal class HttpsConnectionMiddleware private readonly TimeSpan _handshakeTimeout; private readonly ILogger _logger; private readonly Func _sslStreamFactory; + private readonly ClientCertificateMode _clientCertificateMode; // The following fields are only set by HttpsConnectionAdapterOptions ctor. private readonly HttpsConnectionAdapterOptions? _options; @@ -112,7 +113,12 @@ public HttpsConnectionMiddleware(ConnectionDelegate next, HttpsConnectionAdapter _serverCertificateContext = SslStreamCertificateContext.Create(certificate, additionalCertificates: null); } - _sslStreamFactory = s => new SslStream(s, leaveInnerStreamOpen: false, RemoteCertificateValidationCallback); + _clientCertificateMode = _options.ClientCertificateMode; + + var remoteCertificateValidationCallback = _clientCertificateMode == ClientCertificateMode.NoCertificate ? + (RemoteCertificateValidationCallback?)null : RemoteCertificateValidationCallback; + + _sslStreamFactory = s => new SslStream(s, leaveInnerStreamOpen: false, userCertificateValidationCallback: remoteCertificateValidationCallback); } internal HttpsConnectionMiddleware( @@ -120,7 +126,8 @@ internal HttpsConnectionMiddleware( HttpsOptionsCallback httpsOptionsCallback, object httpsOptionsCallbackState, TimeSpan handshakeTimeout, - ILoggerFactory loggerFactory) + ILoggerFactory loggerFactory, + ClientCertificateMode clientCertificateMode) { _next = next; _handshakeTimeout = handshakeTimeout; @@ -128,6 +135,7 @@ internal HttpsConnectionMiddleware( _httpsOptionsCallback = httpsOptionsCallback; _httpsOptionsCallbackState = httpsOptionsCallbackState; + _clientCertificateMode = clientCertificateMode; _sslStreamFactory = s => new SslStream(s); } @@ -144,7 +152,7 @@ public async Task OnConnectionAsync(ConnectionContext context) context.Features.Get()?.MemoryPool ?? MemoryPool.Shared); var sslStream = sslDuplexPipe.Stream; - var feature = new Core.Internal.TlsConnectionFeature(sslStream); + var feature = new Core.Internal.TlsConnectionFeature(sslStream, _clientCertificateMode); context.Features.Set(feature); context.Features.Set(feature); context.Features.Set(feature); diff --git a/src/Servers/Kestrel/Core/src/PublicAPI.Unshipped.txt b/src/Servers/Kestrel/Core/src/PublicAPI.Unshipped.txt index cf79ecc855dd..2b8067f5226e 100644 --- a/src/Servers/Kestrel/Core/src/PublicAPI.Unshipped.txt +++ b/src/Servers/Kestrel/Core/src/PublicAPI.Unshipped.txt @@ -95,10 +95,12 @@ *REMOVED*~static Microsoft.AspNetCore.Hosting.ListenOptionsHttpsExtensions.UseHttps(this Microsoft.AspNetCore.Server.Kestrel.Core.ListenOptions listenOptions, string fileName, string password) -> Microsoft.AspNetCore.Server.Kestrel.Core.ListenOptions *REMOVED*~static Microsoft.AspNetCore.Hosting.ListenOptionsHttpsExtensions.UseHttps(this Microsoft.AspNetCore.Server.Kestrel.Core.ListenOptions listenOptions, string fileName, string password, System.Action configureOptions) -> Microsoft.AspNetCore.Server.Kestrel.Core.ListenOptions *REMOVED*~static Microsoft.AspNetCore.Server.Kestrel.Https.CertificateLoader.LoadFromStoreCert(string subject, string storeName, System.Security.Cryptography.X509Certificates.StoreLocation storeLocation, bool allowInvalid) -> System.Security.Cryptography.X509Certificates.X509Certificate2 +Microsoft.AspNetCore.Server.Kestrel.Https.ClientCertificateMode.DelayCertificate = 3 -> Microsoft.AspNetCore.Server.Kestrel.Https.ClientCertificateMode static Microsoft.AspNetCore.Hosting.ListenOptionsHttpsExtensions.UseHttps(this Microsoft.AspNetCore.Server.Kestrel.Core.ListenOptions! listenOptions, Microsoft.AspNetCore.Server.Kestrel.Https.HttpsConnectionAdapterOptions! httpsOptions) -> Microsoft.AspNetCore.Server.Kestrel.Core.ListenOptions! static Microsoft.AspNetCore.Hosting.ListenOptionsHttpsExtensions.UseHttps(this Microsoft.AspNetCore.Server.Kestrel.Core.ListenOptions! listenOptions, System.Action! configureOptions) -> Microsoft.AspNetCore.Server.Kestrel.Core.ListenOptions! static Microsoft.AspNetCore.Hosting.ListenOptionsHttpsExtensions.UseHttps(this Microsoft.AspNetCore.Server.Kestrel.Core.ListenOptions! listenOptions, System.Net.Security.ServerOptionsSelectionCallback! serverOptionsSelectionCallback, object! state) -> Microsoft.AspNetCore.Server.Kestrel.Core.ListenOptions! static Microsoft.AspNetCore.Hosting.ListenOptionsHttpsExtensions.UseHttps(this Microsoft.AspNetCore.Server.Kestrel.Core.ListenOptions! listenOptions, System.Net.Security.ServerOptionsSelectionCallback! serverOptionsSelectionCallback, object! state, System.TimeSpan handshakeTimeout) -> Microsoft.AspNetCore.Server.Kestrel.Core.ListenOptions! +static Microsoft.AspNetCore.Hosting.ListenOptionsHttpsExtensions.UseHttps(this Microsoft.AspNetCore.Server.Kestrel.Core.ListenOptions! listenOptions, System.Net.Security.ServerOptionsSelectionCallback! serverOptionsSelectionCallback, object! state, System.TimeSpan handshakeTimeout, Microsoft.AspNetCore.Server.Kestrel.Https.ClientCertificateMode clientCertificateMode) -> Microsoft.AspNetCore.Server.Kestrel.Core.ListenOptions! static Microsoft.AspNetCore.Hosting.ListenOptionsHttpsExtensions.UseHttps(this Microsoft.AspNetCore.Server.Kestrel.Core.ListenOptions! listenOptions, System.Security.Cryptography.X509Certificates.X509Certificate2! serverCertificate) -> Microsoft.AspNetCore.Server.Kestrel.Core.ListenOptions! static Microsoft.AspNetCore.Hosting.ListenOptionsHttpsExtensions.UseHttps(this Microsoft.AspNetCore.Server.Kestrel.Core.ListenOptions! listenOptions, string! fileName, string? password) -> Microsoft.AspNetCore.Server.Kestrel.Core.ListenOptions! static Microsoft.AspNetCore.Hosting.ListenOptionsHttpsExtensions.UseHttps(this Microsoft.AspNetCore.Server.Kestrel.Core.ListenOptions! listenOptions, string! fileName, string? password, System.Action! configureOptions) -> Microsoft.AspNetCore.Server.Kestrel.Core.ListenOptions! diff --git a/src/Servers/Kestrel/test/InMemory.FunctionalTests/HttpsConnectionMiddlewareTests.cs b/src/Servers/Kestrel/test/InMemory.FunctionalTests/HttpsConnectionMiddlewareTests.cs index 19a6893868fe..03b06970c567 100644 --- a/src/Servers/Kestrel/test/InMemory.FunctionalTests/HttpsConnectionMiddlewareTests.cs +++ b/src/Servers/Kestrel/test/InMemory.FunctionalTests/HttpsConnectionMiddlewareTests.cs @@ -488,7 +488,7 @@ void ConfigureListenOptions(ListenOptions listenOptions) } [Fact] - public async Task CanRenegotiateForClientCertificateOnHttp1() + public async Task RenegotiateForClientCertificateOnHttp1DisabledByDefault() { void ConfigureListenOptions(ListenOptions listenOptions) { @@ -501,6 +501,54 @@ void ConfigureListenOptions(ListenOptions listenOptions) }); } + await using var server = new TestServer(async context => + { + var tlsFeature = context.Features.Get(); + Assert.NotNull(tlsFeature); + Assert.Null(tlsFeature.ClientCertificate); + Assert.Null(context.Connection.ClientCertificate); + + var clientCert = await context.Connection.GetClientCertificateAsync(); + Assert.Null(clientCert); + Assert.Null(tlsFeature.ClientCertificate); + Assert.Null(context.Connection.ClientCertificate); + + await context.Response.WriteAsync("hello world"); + }, new TestServiceContext(LoggerFactory), ConfigureListenOptions); + + using var connection = server.CreateConnection(); + // SslStream is used to ensure the certificate is actually passed to the server + // HttpClient might not send the certificate because it is invalid or it doesn't match any + // of the certificate authorities sent by the server in the SSL handshake. + var stream = new SslStream(connection.Stream); + var clientOptions = new SslClientAuthenticationOptions() + { + TargetHost = "localhost", + EnabledSslProtocols = SslProtocols.Tls | SslProtocols.Tls11 | SslProtocols.Tls12, + }; + clientOptions.RemoteCertificateValidationCallback = (sender, certificate, chain, sslPolicyErrors) => true; + clientOptions.LocalCertificateSelectionCallback + = (sender, targetHost, localCertificates, remoteCertificate, acceptableIssuers) => _x509Certificate2; + + await stream.AuthenticateAsClientAsync(clientOptions); + await AssertConnectionResult(stream, true); + } + + [ConditionalFact] + [OSSkipCondition(OperatingSystems.MacOSX | OperatingSystems.Linux, SkipReason = "Not supported yet.")] + public async Task CanRenegotiateForClientCertificateOnHttp1() + { + void ConfigureListenOptions(ListenOptions listenOptions) + { + listenOptions.Protocols = HttpProtocols.Http1; + listenOptions.UseHttps(options => + { + options.ServerCertificate = _x509Certificate2; + options.ClientCertificateMode = ClientCertificateMode.DelayCertificate; + options.AllowAnyClientCertificate(); + }); + } + await using var server = new TestServer(async context => { var tlsFeature = context.Features.Get(); @@ -534,7 +582,54 @@ void ConfigureListenOptions(ListenOptions listenOptions) await AssertConnectionResult(stream, true); } - [Fact] + [ConditionalFact] + [OSSkipCondition(OperatingSystems.MacOSX | OperatingSystems.Linux, SkipReason = "Not supported yet.")] + public async Task CanRenegotiateForClientCertificateOnHttp1CanReturnNoCert() + { + void ConfigureListenOptions(ListenOptions listenOptions) + { + listenOptions.Protocols = HttpProtocols.Http1; + listenOptions.UseHttps(options => + { + options.ServerCertificate = _x509Certificate2; + options.ClientCertificateMode = ClientCertificateMode.DelayCertificate; + options.AllowAnyClientCertificate(); + }); + } + + await using var server = new TestServer(async context => + { + var tlsFeature = context.Features.Get(); + Assert.NotNull(tlsFeature); + Assert.Null(tlsFeature.ClientCertificate); + Assert.Null(context.Connection.ClientCertificate); + + var clientCert = await context.Connection.GetClientCertificateAsync(); + Assert.Null(clientCert); + Assert.Null(tlsFeature.ClientCertificate); + Assert.Null(context.Connection.ClientCertificate); + + await context.Response.WriteAsync("hello world"); + }, new TestServiceContext(LoggerFactory), ConfigureListenOptions); + + using var connection = server.CreateConnection(); + // SslStream is used to ensure the certificate is actually passed to the server + // HttpClient might not send the certificate because it is invalid or it doesn't match any + // of the certificate authorities sent by the server in the SSL handshake. + var stream = new SslStream(connection.Stream); + var clientOptions = new SslClientAuthenticationOptions() + { + TargetHost = "localhost", + EnabledSslProtocols = SslProtocols.Tls | SslProtocols.Tls11 | SslProtocols.Tls12, + }; + clientOptions.RemoteCertificateValidationCallback = (sender, certificate, chain, sslPolicyErrors) => true; + + await stream.AuthenticateAsClientAsync(clientOptions); + await AssertConnectionResult(stream, true); + } + + [ConditionalFact] + [OSSkipCondition(OperatingSystems.MacOSX | OperatingSystems.Linux, SkipReason = "Not supported yet.")] // Turning on HTTP/2 disables renegotiation. // TODO: Tomas is changing it so AllowRenegotiation only applies to the remote, // NegotiateClientCertificateAsync call be called locally and it's up to us to prevent that @@ -547,7 +642,7 @@ void ConfigureListenOptions(ListenOptions listenOptions) listenOptions.UseHttps(options => { options.ServerCertificate = _x509Certificate2; - options.ClientCertificateMode = ClientCertificateMode.NoCertificate; + options.ClientCertificateMode = ClientCertificateMode.DelayCertificate; options.AllowAnyClientCertificate(); }); } @@ -582,7 +677,7 @@ void ConfigureListenOptions(ListenOptions listenOptions) = (sender, targetHost, localCertificates, remoteCertificate, acceptableIssuers) => _x509Certificate2; await stream.AuthenticateAsClientAsync(clientOptions); - await AssertConnectionResult(stream, true); + await AssertConnectionResult(stream, false); } [Fact] From 6317e8cd8084473ee35f8b0ad5e9790947590b5d Mon Sep 17 00:00:00 2001 From: Chris R Date: Fri, 4 Jun 2021 16:04:54 -0700 Subject: [PATCH 03/19] Update http2 test --- .../HttpsConnectionMiddlewareTests.cs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/Servers/Kestrel/test/InMemory.FunctionalTests/HttpsConnectionMiddlewareTests.cs b/src/Servers/Kestrel/test/InMemory.FunctionalTests/HttpsConnectionMiddlewareTests.cs index 03b06970c567..1e4622035e03 100644 --- a/src/Servers/Kestrel/test/InMemory.FunctionalTests/HttpsConnectionMiddlewareTests.cs +++ b/src/Servers/Kestrel/test/InMemory.FunctionalTests/HttpsConnectionMiddlewareTests.cs @@ -634,7 +634,7 @@ void ConfigureListenOptions(ListenOptions listenOptions) // TODO: Tomas is changing it so AllowRenegotiation only applies to the remote, // NegotiateClientCertificateAsync call be called locally and it's up to us to prevent that // on HTTP/2. - public async Task ClientCertificateRenegotationDisabledOnHttp1WithHttp2() + public async Task CanRenegotiateForClientCertificateOnHttp1WithHttp2() { void ConfigureListenOptions(ListenOptions listenOptions) { @@ -654,10 +654,10 @@ void ConfigureListenOptions(ListenOptions listenOptions) Assert.Null(tlsFeature.ClientCertificate); Assert.Null(context.Connection.ClientCertificate); - var ex = await Assert.ThrowsAsync(() => context.Connection.GetClientCertificateAsync()); - Assert.Equal("The remote party requested renegotiation when AllowRenegotiation was set to false.", ex.Message); - Assert.Null(tlsFeature.ClientCertificate); - Assert.Null(context.Connection.ClientCertificate); + var cert = await context.Connection.GetClientCertificateAsync(); + Assert.NotNull(cert); + Assert.NotNull(tlsFeature.ClientCertificate); + Assert.NotNull(context.Connection.ClientCertificate); await context.Response.WriteAsync("hello world"); }, new TestServiceContext(LoggerFactory), ConfigureListenOptions); @@ -677,7 +677,7 @@ void ConfigureListenOptions(ListenOptions listenOptions) = (sender, targetHost, localCertificates, remoteCertificate, acceptableIssuers) => _x509Certificate2; await stream.AuthenticateAsClientAsync(clientOptions); - await AssertConnectionResult(stream, false); + await AssertConnectionResult(stream, true); } [Fact] From 5ae7511ee0131638dd56ac6cd1210fe45fc6d503 Mon Sep 17 00:00:00 2001 From: Chris R Date: Fri, 4 Jun 2021 16:31:32 -0700 Subject: [PATCH 04/19] Clean up tests --- .../Middleware/HttpsConnectionMiddleware.cs | 3 +- .../HttpsConnectionMiddlewareTests.cs | 54 ++++--------------- 2 files changed, 13 insertions(+), 44 deletions(-) diff --git a/src/Servers/Kestrel/Core/src/Middleware/HttpsConnectionMiddleware.cs b/src/Servers/Kestrel/Core/src/Middleware/HttpsConnectionMiddleware.cs index ec943c0221f5..8413b11583ca 100644 --- a/src/Servers/Kestrel/Core/src/Middleware/HttpsConnectionMiddleware.cs +++ b/src/Servers/Kestrel/Core/src/Middleware/HttpsConnectionMiddleware.cs @@ -326,7 +326,8 @@ private Task DoOptionsBasedHandshakeAsync(ConnectionContext context, SslStream s ServerCertificate = _serverCertificate, ServerCertificateContext = _serverCertificateContext, ServerCertificateSelectionCallback = selector, - ClientCertificateRequired = _options.ClientCertificateMode != ClientCertificateMode.NoCertificate, + ClientCertificateRequired = _options.ClientCertificateMode == ClientCertificateMode.AllowCertificate + || _options.ClientCertificateMode == ClientCertificateMode.RequireCertificate, EnabledSslProtocols = _options.SslProtocols, CertificateRevocationCheckMode = _options.CheckCertificateRevocation ? X509RevocationMode.Online : X509RevocationMode.NoCheck, }; diff --git a/src/Servers/Kestrel/test/InMemory.FunctionalTests/HttpsConnectionMiddlewareTests.cs b/src/Servers/Kestrel/test/InMemory.FunctionalTests/HttpsConnectionMiddlewareTests.cs index 1e4622035e03..7100a638773d 100644 --- a/src/Servers/Kestrel/test/InMemory.FunctionalTests/HttpsConnectionMiddlewareTests.cs +++ b/src/Servers/Kestrel/test/InMemory.FunctionalTests/HttpsConnectionMiddlewareTests.cs @@ -520,17 +520,8 @@ void ConfigureListenOptions(ListenOptions listenOptions) // SslStream is used to ensure the certificate is actually passed to the server // HttpClient might not send the certificate because it is invalid or it doesn't match any // of the certificate authorities sent by the server in the SSL handshake. - var stream = new SslStream(connection.Stream); - var clientOptions = new SslClientAuthenticationOptions() - { - TargetHost = "localhost", - EnabledSslProtocols = SslProtocols.Tls | SslProtocols.Tls11 | SslProtocols.Tls12, - }; - clientOptions.RemoteCertificateValidationCallback = (sender, certificate, chain, sslPolicyErrors) => true; - clientOptions.LocalCertificateSelectionCallback - = (sender, targetHost, localCertificates, remoteCertificate, acceptableIssuers) => _x509Certificate2; - - await stream.AuthenticateAsClientAsync(clientOptions); + var stream = OpenSslStreamWithCert(connection.Stream); + await stream.AuthenticateAsClientAsync("localhost"); await AssertConnectionResult(stream, true); } @@ -568,17 +559,8 @@ void ConfigureListenOptions(ListenOptions listenOptions) // SslStream is used to ensure the certificate is actually passed to the server // HttpClient might not send the certificate because it is invalid or it doesn't match any // of the certificate authorities sent by the server in the SSL handshake. - var stream = new SslStream(connection.Stream); - var clientOptions = new SslClientAuthenticationOptions() - { - TargetHost = "localhost", - EnabledSslProtocols = SslProtocols.Tls | SslProtocols.Tls11 | SslProtocols.Tls12, - }; - clientOptions.RemoteCertificateValidationCallback = (sender, certificate, chain, sslPolicyErrors) => true; - clientOptions.LocalCertificateSelectionCallback - = (sender, targetHost, localCertificates, remoteCertificate, acceptableIssuers) => _x509Certificate2; - - await stream.AuthenticateAsClientAsync(clientOptions); + var stream = OpenSslStreamWithCert(connection.Stream); + await stream.AuthenticateAsClientAsync("localhost"); await AssertConnectionResult(stream, true); } @@ -616,19 +598,14 @@ void ConfigureListenOptions(ListenOptions listenOptions) // SslStream is used to ensure the certificate is actually passed to the server // HttpClient might not send the certificate because it is invalid or it doesn't match any // of the certificate authorities sent by the server in the SSL handshake. - var stream = new SslStream(connection.Stream); - var clientOptions = new SslClientAuthenticationOptions() - { - TargetHost = "localhost", - EnabledSslProtocols = SslProtocols.Tls | SslProtocols.Tls11 | SslProtocols.Tls12, - }; - clientOptions.RemoteCertificateValidationCallback = (sender, certificate, chain, sslPolicyErrors) => true; - - await stream.AuthenticateAsClientAsync(clientOptions); + var stream = new SslStream(connection.Stream, false, (sender, certificate, chain, errors) => true, + (sender, host, certificates, certificate, issuers) => null); + await stream.AuthenticateAsClientAsync("localhost"); await AssertConnectionResult(stream, true); } - [ConditionalFact] + // [ConditionalFact(Skip = "Depends on https://github.com/dotnet/runtime/pull/53719")] + [Fact] [OSSkipCondition(OperatingSystems.MacOSX | OperatingSystems.Linux, SkipReason = "Not supported yet.")] // Turning on HTTP/2 disables renegotiation. // TODO: Tomas is changing it so AllowRenegotiation only applies to the remote, @@ -666,17 +643,8 @@ void ConfigureListenOptions(ListenOptions listenOptions) // SslStream is used to ensure the certificate is actually passed to the server // HttpClient might not send the certificate because it is invalid or it doesn't match any // of the certificate authorities sent by the server in the SSL handshake. - var stream = new SslStream(connection.Stream); - var clientOptions = new SslClientAuthenticationOptions() - { - TargetHost = "localhost", - EnabledSslProtocols = SslProtocols.Tls | SslProtocols.Tls11 | SslProtocols.Tls12, - }; - clientOptions.RemoteCertificateValidationCallback = (sender, certificate, chain, sslPolicyErrors) => true; - clientOptions.LocalCertificateSelectionCallback - = (sender, targetHost, localCertificates, remoteCertificate, acceptableIssuers) => _x509Certificate2; - - await stream.AuthenticateAsClientAsync(clientOptions); + var stream = OpenSslStreamWithCert(connection.Stream); + await stream.AuthenticateAsClientAsync("localhost"); await AssertConnectionResult(stream, true); } From c1385e07fdf872dfe78d617c941ca5d9ab34d208 Mon Sep 17 00:00:00 2001 From: Chris R Date: Fri, 4 Jun 2021 17:13:13 -0700 Subject: [PATCH 05/19] POST tests --- .../HttpsConnectionMiddlewareTests.cs | 161 ++++++++++++++++-- 1 file changed, 148 insertions(+), 13 deletions(-) diff --git a/src/Servers/Kestrel/test/InMemory.FunctionalTests/HttpsConnectionMiddlewareTests.cs b/src/Servers/Kestrel/test/InMemory.FunctionalTests/HttpsConnectionMiddlewareTests.cs index 7100a638773d..935f48bde485 100644 --- a/src/Servers/Kestrel/test/InMemory.FunctionalTests/HttpsConnectionMiddlewareTests.cs +++ b/src/Servers/Kestrel/test/InMemory.FunctionalTests/HttpsConnectionMiddlewareTests.cs @@ -520,8 +520,17 @@ void ConfigureListenOptions(ListenOptions listenOptions) // SslStream is used to ensure the certificate is actually passed to the server // HttpClient might not send the certificate because it is invalid or it doesn't match any // of the certificate authorities sent by the server in the SSL handshake. - var stream = OpenSslStreamWithCert(connection.Stream); - await stream.AuthenticateAsClientAsync("localhost"); + var stream = new SslStream(connection.Stream); + var clientOptions = new SslClientAuthenticationOptions() + { + TargetHost = "localhost", + EnabledSslProtocols = SslProtocols.Tls | SslProtocols.Tls11 | SslProtocols.Tls12, + }; + clientOptions.RemoteCertificateValidationCallback = (sender, certificate, chain, sslPolicyErrors) => true; + clientOptions.LocalCertificateSelectionCallback + = (sender, targetHost, localCertificates, remoteCertificate, acceptableIssuers) => _x509Certificate2; + + await stream.AuthenticateAsClientAsync(clientOptions); await AssertConnectionResult(stream, true); } @@ -559,8 +568,17 @@ void ConfigureListenOptions(ListenOptions listenOptions) // SslStream is used to ensure the certificate is actually passed to the server // HttpClient might not send the certificate because it is invalid or it doesn't match any // of the certificate authorities sent by the server in the SSL handshake. - var stream = OpenSslStreamWithCert(connection.Stream); - await stream.AuthenticateAsClientAsync("localhost"); + var stream = new SslStream(connection.Stream); + var clientOptions = new SslClientAuthenticationOptions() + { + TargetHost = "localhost", + EnabledSslProtocols = SslProtocols.Tls | SslProtocols.Tls11 | SslProtocols.Tls12, + }; + clientOptions.RemoteCertificateValidationCallback = (sender, certificate, chain, sslPolicyErrors) => true; + clientOptions.LocalCertificateSelectionCallback + = (sender, targetHost, localCertificates, remoteCertificate, acceptableIssuers) => _x509Certificate2; + + await stream.AuthenticateAsClientAsync(clientOptions); await AssertConnectionResult(stream, true); } @@ -598,14 +616,19 @@ void ConfigureListenOptions(ListenOptions listenOptions) // SslStream is used to ensure the certificate is actually passed to the server // HttpClient might not send the certificate because it is invalid or it doesn't match any // of the certificate authorities sent by the server in the SSL handshake. - var stream = new SslStream(connection.Stream, false, (sender, certificate, chain, errors) => true, - (sender, host, certificates, certificate, issuers) => null); - await stream.AuthenticateAsClientAsync("localhost"); + var stream = new SslStream(connection.Stream); + var clientOptions = new SslClientAuthenticationOptions() + { + TargetHost = "localhost", + EnabledSslProtocols = SslProtocols.Tls | SslProtocols.Tls11 | SslProtocols.Tls12, + }; + clientOptions.RemoteCertificateValidationCallback = (sender, certificate, chain, sslPolicyErrors) => true; + + await stream.AuthenticateAsClientAsync(clientOptions); await AssertConnectionResult(stream, true); } - // [ConditionalFact(Skip = "Depends on https://github.com/dotnet/runtime/pull/53719")] - [Fact] + [ConditionalFact(Skip = "Depends on https://github.com/dotnet/runtime/pull/53719")] [OSSkipCondition(OperatingSystems.MacOSX | OperatingSystems.Linux, SkipReason = "Not supported yet.")] // Turning on HTTP/2 disables renegotiation. // TODO: Tomas is changing it so AllowRenegotiation only applies to the remote, @@ -643,11 +666,122 @@ void ConfigureListenOptions(ListenOptions listenOptions) // SslStream is used to ensure the certificate is actually passed to the server // HttpClient might not send the certificate because it is invalid or it doesn't match any // of the certificate authorities sent by the server in the SSL handshake. - var stream = OpenSslStreamWithCert(connection.Stream); - await stream.AuthenticateAsClientAsync("localhost"); + var stream = new SslStream(connection.Stream); + var clientOptions = new SslClientAuthenticationOptions() + { + TargetHost = "localhost", + EnabledSslProtocols = SslProtocols.Tls | SslProtocols.Tls11 | SslProtocols.Tls12, + }; + clientOptions.RemoteCertificateValidationCallback = (sender, certificate, chain, sslPolicyErrors) => true; + clientOptions.LocalCertificateSelectionCallback + = (sender, targetHost, localCertificates, remoteCertificate, acceptableIssuers) => _x509Certificate2; + + await stream.AuthenticateAsClientAsync(clientOptions); await AssertConnectionResult(stream, true); } + [ConditionalFact] + [OSSkipCondition(OperatingSystems.MacOSX | OperatingSystems.Linux, SkipReason = "Not supported yet.")] + public async Task RenegotiateForClientCertificateOnPostWithoutBufferingThrows() + { + void ConfigureListenOptions(ListenOptions listenOptions) + { + listenOptions.Protocols = HttpProtocols.Http1; + listenOptions.UseHttps(options => + { + options.ServerCertificate = _x509Certificate2; + options.ClientCertificateMode = ClientCertificateMode.DelayCertificate; + options.AllowAnyClientCertificate(); + }); + } + + // Under 4kb can sometimes work because it fits into Kestrel's header parsing buffer. + var expectedBody = new string('a', 1024 * 4); + + await using var server = new TestServer(async context => + { + var tlsFeature = context.Features.Get(); + Assert.NotNull(tlsFeature); + Assert.Null(tlsFeature.ClientCertificate); + Assert.Null(context.Connection.ClientCertificate); + + var ex = await Assert.ThrowsAsync(() => context.Connection.GetClientCertificateAsync()); + Assert.Equal("Received data during renegotiation.", ex.Message); + Assert.Null(tlsFeature.ClientCertificate); + Assert.Null(context.Connection.ClientCertificate); + }, new TestServiceContext(LoggerFactory), ConfigureListenOptions); + + using var connection = server.CreateConnection(); + // SslStream is used to ensure the certificate is actually passed to the server + // HttpClient might not send the certificate because it is invalid or it doesn't match any + // of the certificate authorities sent by the server in the SSL handshake. + var stream = new SslStream(connection.Stream); + var clientOptions = new SslClientAuthenticationOptions() + { + TargetHost = "localhost", + EnabledSslProtocols = SslProtocols.Tls | SslProtocols.Tls11 | SslProtocols.Tls12, + }; + clientOptions.RemoteCertificateValidationCallback = (sender, certificate, chain, sslPolicyErrors) => true; + clientOptions.LocalCertificateSelectionCallback + = (sender, targetHost, localCertificates, remoteCertificate, acceptableIssuers) => _x509Certificate2; + + await stream.AuthenticateAsClientAsync(clientOptions); + await AssertConnectionResult(stream, false, expectedBody); + } + + [ConditionalFact] + [OSSkipCondition(OperatingSystems.MacOSX | OperatingSystems.Linux, SkipReason = "Not supported yet.")] + public async Task CanRenegotiateForClientCertificateOnPostIfDrained() + { + void ConfigureListenOptions(ListenOptions listenOptions) + { + listenOptions.Protocols = HttpProtocols.Http1; + listenOptions.UseHttps(options => + { + options.ServerCertificate = _x509Certificate2; + options.ClientCertificateMode = ClientCertificateMode.DelayCertificate; + options.AllowAnyClientCertificate(); + }); + } + + var expectedBody = new string('a', 1024 * 4); + + await using var server = new TestServer(async context => + { + var tlsFeature = context.Features.Get(); + Assert.NotNull(tlsFeature); + Assert.Null(tlsFeature.ClientCertificate); + Assert.Null(context.Connection.ClientCertificate); + + // Read the body before requesting the client cert + var body = await new StreamReader(context.Request.Body).ReadToEndAsync(); + Assert.Equal(expectedBody, body); + + var clientCert = await context.Connection.GetClientCertificateAsync(); + Assert.NotNull(clientCert); + Assert.NotNull(tlsFeature.ClientCertificate); + Assert.NotNull(context.Connection.ClientCertificate); + await context.Response.WriteAsync("hello world"); + }, new TestServiceContext(LoggerFactory), ConfigureListenOptions); + + using var connection = server.CreateConnection(); + // SslStream is used to ensure the certificate is actually passed to the server + // HttpClient might not send the certificate because it is invalid or it doesn't match any + // of the certificate authorities sent by the server in the SSL handshake. + var stream = new SslStream(connection.Stream); + var clientOptions = new SslClientAuthenticationOptions() + { + TargetHost = "localhost", + EnabledSslProtocols = SslProtocols.Tls | SslProtocols.Tls11 | SslProtocols.Tls12, + }; + clientOptions.RemoteCertificateValidationCallback = (sender, certificate, chain, sslPolicyErrors) => true; + clientOptions.LocalCertificateSelectionCallback + = (sender, targetHost, localCertificates, remoteCertificate, acceptableIssuers) => _x509Certificate2; + + await stream.AuthenticateAsClientAsync(clientOptions); + await AssertConnectionResult(stream, true, expectedBody); + } + [Fact] public async Task HttpsSchemePassedToRequestFeature() { @@ -1015,9 +1149,10 @@ private static SslStream OpenSslStreamWithCert(Stream rawStream, X509Certificate (sender, host, certificates, certificate, issuers) => clientCertificate ?? _x509Certificate2); } - private static async Task AssertConnectionResult(SslStream stream, bool success) + private static async Task AssertConnectionResult(SslStream stream, bool success, string body = null) { - var request = Encoding.UTF8.GetBytes("GET / HTTP/1.0\r\n\r\n"); + var request = body == null ? Encoding.UTF8.GetBytes("GET / HTTP/1.0\r\n\r\n") + : Encoding.UTF8.GetBytes($"POST / HTTP/1.0\r\nContent-Length: {body.Length}\r\n\r\n{body}"); await stream.WriteAsync(request, 0, request.Length); var reader = new StreamReader(stream); string line = null; From 920b3f0fd50188fd5e2dbec52aedee195ce9e5df Mon Sep 17 00:00:00 2001 From: Chris R Date: Fri, 4 Jun 2021 18:29:59 -0700 Subject: [PATCH 06/19] Use unique hosts --- .../HttpsConnectionMiddlewareTests.cs | 69 ++++--------------- 1 file changed, 12 insertions(+), 57 deletions(-) diff --git a/src/Servers/Kestrel/test/InMemory.FunctionalTests/HttpsConnectionMiddlewareTests.cs b/src/Servers/Kestrel/test/InMemory.FunctionalTests/HttpsConnectionMiddlewareTests.cs index 935f48bde485..f25fab61d76a 100644 --- a/src/Servers/Kestrel/test/InMemory.FunctionalTests/HttpsConnectionMiddlewareTests.cs +++ b/src/Servers/Kestrel/test/InMemory.FunctionalTests/HttpsConnectionMiddlewareTests.cs @@ -481,7 +481,7 @@ void ConfigureListenOptions(ListenOptions listenOptions) // HttpClient might not send the certificate because it is invalid or it doesn't match any // of the certificate authorities sent by the server in the SSL handshake. var stream = OpenSslStreamWithCert(connection.Stream); - await stream.AuthenticateAsClientAsync("localhost"); + await stream.AuthenticateAsClientAsync(Guid.NewGuid().ToString()); await AssertConnectionResult(stream, true); } } @@ -520,17 +520,8 @@ void ConfigureListenOptions(ListenOptions listenOptions) // SslStream is used to ensure the certificate is actually passed to the server // HttpClient might not send the certificate because it is invalid or it doesn't match any // of the certificate authorities sent by the server in the SSL handshake. - var stream = new SslStream(connection.Stream); - var clientOptions = new SslClientAuthenticationOptions() - { - TargetHost = "localhost", - EnabledSslProtocols = SslProtocols.Tls | SslProtocols.Tls11 | SslProtocols.Tls12, - }; - clientOptions.RemoteCertificateValidationCallback = (sender, certificate, chain, sslPolicyErrors) => true; - clientOptions.LocalCertificateSelectionCallback - = (sender, targetHost, localCertificates, remoteCertificate, acceptableIssuers) => _x509Certificate2; - - await stream.AuthenticateAsClientAsync(clientOptions); + var stream = OpenSslStreamWithCert(connection.Stream); + await stream.AuthenticateAsClientAsync(Guid.NewGuid().ToString()); await AssertConnectionResult(stream, true); } @@ -568,17 +559,8 @@ void ConfigureListenOptions(ListenOptions listenOptions) // SslStream is used to ensure the certificate is actually passed to the server // HttpClient might not send the certificate because it is invalid or it doesn't match any // of the certificate authorities sent by the server in the SSL handshake. - var stream = new SslStream(connection.Stream); - var clientOptions = new SslClientAuthenticationOptions() - { - TargetHost = "localhost", - EnabledSslProtocols = SslProtocols.Tls | SslProtocols.Tls11 | SslProtocols.Tls12, - }; - clientOptions.RemoteCertificateValidationCallback = (sender, certificate, chain, sslPolicyErrors) => true; - clientOptions.LocalCertificateSelectionCallback - = (sender, targetHost, localCertificates, remoteCertificate, acceptableIssuers) => _x509Certificate2; - - await stream.AuthenticateAsClientAsync(clientOptions); + var stream = OpenSslStreamWithCert(connection.Stream); + await stream.AuthenticateAsClientAsync(Guid.NewGuid().ToString()); await AssertConnectionResult(stream, true); } @@ -619,7 +601,7 @@ void ConfigureListenOptions(ListenOptions listenOptions) var stream = new SslStream(connection.Stream); var clientOptions = new SslClientAuthenticationOptions() { - TargetHost = "localhost", + TargetHost = Guid.NewGuid().ToString(), EnabledSslProtocols = SslProtocols.Tls | SslProtocols.Tls11 | SslProtocols.Tls12, }; clientOptions.RemoteCertificateValidationCallback = (sender, certificate, chain, sslPolicyErrors) => true; @@ -666,17 +648,8 @@ void ConfigureListenOptions(ListenOptions listenOptions) // SslStream is used to ensure the certificate is actually passed to the server // HttpClient might not send the certificate because it is invalid or it doesn't match any // of the certificate authorities sent by the server in the SSL handshake. - var stream = new SslStream(connection.Stream); - var clientOptions = new SslClientAuthenticationOptions() - { - TargetHost = "localhost", - EnabledSslProtocols = SslProtocols.Tls | SslProtocols.Tls11 | SslProtocols.Tls12, - }; - clientOptions.RemoteCertificateValidationCallback = (sender, certificate, chain, sslPolicyErrors) => true; - clientOptions.LocalCertificateSelectionCallback - = (sender, targetHost, localCertificates, remoteCertificate, acceptableIssuers) => _x509Certificate2; - - await stream.AuthenticateAsClientAsync(clientOptions); + var stream = OpenSslStreamWithCert(connection.Stream); + await stream.AuthenticateAsClientAsync(Guid.NewGuid().ToString()); await AssertConnectionResult(stream, true); } @@ -715,17 +688,8 @@ void ConfigureListenOptions(ListenOptions listenOptions) // SslStream is used to ensure the certificate is actually passed to the server // HttpClient might not send the certificate because it is invalid or it doesn't match any // of the certificate authorities sent by the server in the SSL handshake. - var stream = new SslStream(connection.Stream); - var clientOptions = new SslClientAuthenticationOptions() - { - TargetHost = "localhost", - EnabledSslProtocols = SslProtocols.Tls | SslProtocols.Tls11 | SslProtocols.Tls12, - }; - clientOptions.RemoteCertificateValidationCallback = (sender, certificate, chain, sslPolicyErrors) => true; - clientOptions.LocalCertificateSelectionCallback - = (sender, targetHost, localCertificates, remoteCertificate, acceptableIssuers) => _x509Certificate2; - - await stream.AuthenticateAsClientAsync(clientOptions); + var stream = OpenSslStreamWithCert(connection.Stream); + await stream.AuthenticateAsClientAsync(Guid.NewGuid().ToString()); await AssertConnectionResult(stream, false, expectedBody); } @@ -768,17 +732,8 @@ void ConfigureListenOptions(ListenOptions listenOptions) // SslStream is used to ensure the certificate is actually passed to the server // HttpClient might not send the certificate because it is invalid or it doesn't match any // of the certificate authorities sent by the server in the SSL handshake. - var stream = new SslStream(connection.Stream); - var clientOptions = new SslClientAuthenticationOptions() - { - TargetHost = "localhost", - EnabledSslProtocols = SslProtocols.Tls | SslProtocols.Tls11 | SslProtocols.Tls12, - }; - clientOptions.RemoteCertificateValidationCallback = (sender, certificate, chain, sslPolicyErrors) => true; - clientOptions.LocalCertificateSelectionCallback - = (sender, targetHost, localCertificates, remoteCertificate, acceptableIssuers) => _x509Certificate2; - - await stream.AuthenticateAsClientAsync(clientOptions); + var stream = OpenSslStreamWithCert(connection.Stream); + await stream.AuthenticateAsClientAsync(Guid.NewGuid().ToString()); await AssertConnectionResult(stream, true, expectedBody); } From 6ff5d72edf2d71aaba83f80ec86ea4270eb25772 Mon Sep 17 00:00:00 2001 From: Chris R Date: Mon, 7 Jun 2021 16:34:52 -0700 Subject: [PATCH 07/19] Sample (needs debugging) --- .../samples/SampleApp/BufferingTlsFeature.cs | 50 +++++++++++++++++++ .../Kestrel/samples/SampleApp/Startup.cs | 29 +++++++++-- 2 files changed, 75 insertions(+), 4 deletions(-) create mode 100644 src/Servers/Kestrel/samples/SampleApp/BufferingTlsFeature.cs diff --git a/src/Servers/Kestrel/samples/SampleApp/BufferingTlsFeature.cs b/src/Servers/Kestrel/samples/SampleApp/BufferingTlsFeature.cs new file mode 100644 index 000000000000..24b10fc310c2 --- /dev/null +++ b/src/Servers/Kestrel/samples/SampleApp/BufferingTlsFeature.cs @@ -0,0 +1,50 @@ +// 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.Security.Cryptography.X509Certificates; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Connections.Features; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Http.Features; +using Microsoft.AspNetCore.WebUtilities; + +namespace SampleApp +{ + internal class BufferingTlsFeature : ITlsConnectionFeature + { + private ITlsConnectionFeature _tlsFeature; + private HttpContext _context; + + public BufferingTlsFeature(ITlsConnectionFeature tlsFeature, HttpContext context) + { + _tlsFeature = tlsFeature; + _context = context; + } + + public X509Certificate2 ClientCertificate + { + get => _tlsFeature.ClientCertificate; + set => _tlsFeature.ClientCertificate = value; + } + + public async Task GetClientCertificateAsync(CancellationToken cancellationToken) + { + // TODO: The buffering and draining don't have a size limit by default, they rely on the server's 30mb default request + // size limit. + if (!_context.Request.Body.CanSeek) + { + _context.Request.EnableBuffering(); + } + var body = _context.Request.Body; + await body.DrainAsync(cancellationToken); + body.Position = 0; + + // Negative caching, prevent buffering on future requests even if the client does not give a cert when prompted. + var connectionItems = _context.Features.Get(); + connectionItems.Items["tls.clientcert.negotiated"] = true; + + return await _tlsFeature.GetClientCertificateAsync(cancellationToken); + } + } +} diff --git a/src/Servers/Kestrel/samples/SampleApp/Startup.cs b/src/Servers/Kestrel/samples/SampleApp/Startup.cs index dabd15ccf9e7..d1d4182c22cb 100644 --- a/src/Servers/Kestrel/samples/SampleApp/Startup.cs +++ b/src/Servers/Kestrel/samples/SampleApp/Startup.cs @@ -11,6 +11,7 @@ using System.Security.Cryptography.X509Certificates; using System.Threading.Tasks; using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Connections.Features; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http.Features; @@ -27,6 +28,22 @@ public void Configure(IApplicationBuilder app, ILoggerFactory loggerFactory) { var logger = loggerFactory.CreateLogger("Default"); + app.Use((context, next) => + { + var tlsFeature = context.Features.Get(); + var bodyFeature = context.Features.Get(); + var connectionItems = context.Features.Get(); + + // Look for TLS connections that don't already have a client cert, and requests that could have a body. + if (tlsFeature != null && tlsFeature.ClientCertificate == null && bodyFeature.CanHaveBody + && !connectionItems.Items.TryGetValue("tls.clientcert.negotiated", out var _)) + { + context.Features.Set(new BufferingTlsFeature(tlsFeature, context)); + } + + return next(context); + }); + /* // Add an exception handler that prevents throwing due to large request body size app.Use(async (context, next) => { @@ -39,16 +56,20 @@ public void Configure(IApplicationBuilder app, ILoggerFactory loggerFactory) } catch (Microsoft.AspNetCore.Http.BadHttpRequestException ex) when (ex.StatusCode == StatusCodes.Status413RequestEntityTooLarge) { } }); - + */ app.Run(async context => { // Drain the request body - await context.Request.Body.CopyToAsync(Stream.Null); + // await context.Request.Body.CopyToAsync(Stream.Null); + + var cert = await context.Connection.GetClientCertificateAsync(); var connectionFeature = context.Connection; logger.LogDebug($"Peer: {connectionFeature.RemoteIpAddress?.ToString()}:{connectionFeature.RemotePort}" + $"{Environment.NewLine}" - + $"Sock: {connectionFeature.LocalIpAddress?.ToString()}:{connectionFeature.LocalPort}"); + + $"Sock: {connectionFeature.LocalIpAddress?.ToString()}:{connectionFeature.LocalPort}" + + $"{Environment.NewLine}" + + cert); var response = $"hello, world{Environment.NewLine}"; context.Response.ContentLength = response.Length; @@ -126,7 +147,7 @@ public static Task Main(string[] args) { ServerCertificate = localhostCert }); - }, state: null); + }, state: null, TimeSpan.FromSeconds(5), ClientCertificateMode.DelayCertificate); }); options From 97c7257fc230c88837bc48bc03d96ec5d8eb3bac Mon Sep 17 00:00:00 2001 From: Chris R Date: Mon, 7 Jun 2021 16:54:14 -0700 Subject: [PATCH 08/19] Fix sample --- src/Servers/Kestrel/samples/SampleApp/Startup.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/Servers/Kestrel/samples/SampleApp/Startup.cs b/src/Servers/Kestrel/samples/SampleApp/Startup.cs index d1d4182c22cb..9038aae46e46 100644 --- a/src/Servers/Kestrel/samples/SampleApp/Startup.cs +++ b/src/Servers/Kestrel/samples/SampleApp/Startup.cs @@ -101,6 +101,7 @@ public static Task Main(string[] args) options.ConfigureHttpsDefaults(httpsOptions => { httpsOptions.SslProtocols = SslProtocols.Tls12; + httpsOptions.ClientCertificateMode = ClientCertificateMode.DelayCertificate; }); options.Listen(IPAddress.Loopback, basePort, listenOptions => @@ -113,6 +114,7 @@ public static Task Main(string[] args) options.Listen(IPAddress.Loopback, basePort + 1, listenOptions => { + listenOptions.Protocols = Microsoft.AspNetCore.Server.Kestrel.Core.HttpProtocols.Http1; listenOptions.UseHttps(); listenOptions.UseConnectionLogging(); }); From c1d279949ca43b534791a2f4d06038a77690eb3b Mon Sep 17 00:00:00 2001 From: Chris R Date: Tue, 8 Jun 2021 10:31:41 -0700 Subject: [PATCH 09/19] Revert UseHttps overload --- .../Core/src/ListenOptionsHttpsExtensions.cs | 20 ++----------------- .../Kestrel/Core/src/PublicAPI.Unshipped.txt | 1 - .../samples/SampleApp/BufferingTlsFeature.cs | 4 ++-- .../Kestrel/samples/SampleApp/Startup.cs | 2 +- 4 files changed, 5 insertions(+), 22 deletions(-) diff --git a/src/Servers/Kestrel/Core/src/ListenOptionsHttpsExtensions.cs b/src/Servers/Kestrel/Core/src/ListenOptionsHttpsExtensions.cs index 09fcee838ed4..37b1728817bd 100644 --- a/src/Servers/Kestrel/Core/src/ListenOptionsHttpsExtensions.cs +++ b/src/Servers/Kestrel/Core/src/ListenOptionsHttpsExtensions.cs @@ -256,29 +256,13 @@ public static ListenOptions UseHttps(this ListenOptions listenOptions, ServerOpt /// The . public static ListenOptions UseHttps(this ListenOptions listenOptions, ServerOptionsSelectionCallback serverOptionsSelectionCallback, object state, TimeSpan handshakeTimeout) - { - return listenOptions.UseHttps(serverOptionsSelectionCallback, state, handshakeTimeout, ClientCertificateMode.NoCertificate); - } - - /// - /// Configure Kestrel to use HTTPS. This does not use default certificates or other defaults specified via config or - /// . - /// - /// The to configure. - /// Callback to configure HTTPS options. - /// State for the . - /// Specifies the maximum amount of time allowed for the TLS/SSL handshake. This must be positive and finite. - /// The mode used for accepting client certificates. - /// The . - public static ListenOptions UseHttps(this ListenOptions listenOptions, ServerOptionsSelectionCallback serverOptionsSelectionCallback, - object state, TimeSpan handshakeTimeout, ClientCertificateMode clientCertificateMode) { // HttpsOptionsCallback is an internal delegate that is just the ServerOptionsSelectionCallback + a ConnectionContext parameter. // Given that ConnectionContext will eventually be replaced by System.Net.Connections, it doesn't make much sense to make the HttpsOptionsCallback delegate public. HttpsOptionsCallback adaptedCallback = (connection, stream, clientHelloInfo, state, cancellationToken) => serverOptionsSelectionCallback(stream, clientHelloInfo, state, cancellationToken); - return listenOptions.UseHttps(adaptedCallback, state, handshakeTimeout, clientCertificateMode); + return listenOptions.UseHttps(adaptedCallback, state, handshakeTimeout, ClientCertificateMode.DelayCertificate); } /// @@ -288,7 +272,7 @@ public static ListenOptions UseHttps(this ListenOptions listenOptions, ServerOpt /// Callback to configure HTTPS options. /// State for the . /// Specifies the maximum amount of time allowed for the TLS/SSL handshake. This must be positive and finite. - /// The mode used for accepting client certificates. + /// /// The . internal static ListenOptions UseHttps(this ListenOptions listenOptions, HttpsOptionsCallback httpsOptionsCallback, object state, TimeSpan handshakeTimeout, ClientCertificateMode clientCertificateMode) { diff --git a/src/Servers/Kestrel/Core/src/PublicAPI.Unshipped.txt b/src/Servers/Kestrel/Core/src/PublicAPI.Unshipped.txt index 2b8067f5226e..8495f35e086c 100644 --- a/src/Servers/Kestrel/Core/src/PublicAPI.Unshipped.txt +++ b/src/Servers/Kestrel/Core/src/PublicAPI.Unshipped.txt @@ -100,7 +100,6 @@ static Microsoft.AspNetCore.Hosting.ListenOptionsHttpsExtensions.UseHttps(this M static Microsoft.AspNetCore.Hosting.ListenOptionsHttpsExtensions.UseHttps(this Microsoft.AspNetCore.Server.Kestrel.Core.ListenOptions! listenOptions, System.Action! configureOptions) -> Microsoft.AspNetCore.Server.Kestrel.Core.ListenOptions! static Microsoft.AspNetCore.Hosting.ListenOptionsHttpsExtensions.UseHttps(this Microsoft.AspNetCore.Server.Kestrel.Core.ListenOptions! listenOptions, System.Net.Security.ServerOptionsSelectionCallback! serverOptionsSelectionCallback, object! state) -> Microsoft.AspNetCore.Server.Kestrel.Core.ListenOptions! static Microsoft.AspNetCore.Hosting.ListenOptionsHttpsExtensions.UseHttps(this Microsoft.AspNetCore.Server.Kestrel.Core.ListenOptions! listenOptions, System.Net.Security.ServerOptionsSelectionCallback! serverOptionsSelectionCallback, object! state, System.TimeSpan handshakeTimeout) -> Microsoft.AspNetCore.Server.Kestrel.Core.ListenOptions! -static Microsoft.AspNetCore.Hosting.ListenOptionsHttpsExtensions.UseHttps(this Microsoft.AspNetCore.Server.Kestrel.Core.ListenOptions! listenOptions, System.Net.Security.ServerOptionsSelectionCallback! serverOptionsSelectionCallback, object! state, System.TimeSpan handshakeTimeout, Microsoft.AspNetCore.Server.Kestrel.Https.ClientCertificateMode clientCertificateMode) -> Microsoft.AspNetCore.Server.Kestrel.Core.ListenOptions! static Microsoft.AspNetCore.Hosting.ListenOptionsHttpsExtensions.UseHttps(this Microsoft.AspNetCore.Server.Kestrel.Core.ListenOptions! listenOptions, System.Security.Cryptography.X509Certificates.X509Certificate2! serverCertificate) -> Microsoft.AspNetCore.Server.Kestrel.Core.ListenOptions! static Microsoft.AspNetCore.Hosting.ListenOptionsHttpsExtensions.UseHttps(this Microsoft.AspNetCore.Server.Kestrel.Core.ListenOptions! listenOptions, string! fileName, string? password) -> Microsoft.AspNetCore.Server.Kestrel.Core.ListenOptions! static Microsoft.AspNetCore.Hosting.ListenOptionsHttpsExtensions.UseHttps(this Microsoft.AspNetCore.Server.Kestrel.Core.ListenOptions! listenOptions, string! fileName, string? password, System.Action! configureOptions) -> Microsoft.AspNetCore.Server.Kestrel.Core.ListenOptions! diff --git a/src/Servers/Kestrel/samples/SampleApp/BufferingTlsFeature.cs b/src/Servers/Kestrel/samples/SampleApp/BufferingTlsFeature.cs index 24b10fc310c2..5cb00e31972d 100644 --- a/src/Servers/Kestrel/samples/SampleApp/BufferingTlsFeature.cs +++ b/src/Servers/Kestrel/samples/SampleApp/BufferingTlsFeature.cs @@ -30,8 +30,8 @@ public X509Certificate2 ClientCertificate public async Task GetClientCertificateAsync(CancellationToken cancellationToken) { - // TODO: The buffering and draining don't have a size limit by default, they rely on the server's 30mb default request - // size limit. + // TODO: This doesn't set a size limit for the buffering or draining by default, it relies on the server's + // 30mb default request size limit. if (!_context.Request.Body.CanSeek) { _context.Request.EnableBuffering(); diff --git a/src/Servers/Kestrel/samples/SampleApp/Startup.cs b/src/Servers/Kestrel/samples/SampleApp/Startup.cs index 9038aae46e46..6111f0777fe8 100644 --- a/src/Servers/Kestrel/samples/SampleApp/Startup.cs +++ b/src/Servers/Kestrel/samples/SampleApp/Startup.cs @@ -149,7 +149,7 @@ public static Task Main(string[] args) { ServerCertificate = localhostCert }); - }, state: null, TimeSpan.FromSeconds(5), ClientCertificateMode.DelayCertificate); + }, state: null); }); options From ed544ffb1bc780ad46c8577688d027989e6ab761 Mon Sep 17 00:00:00 2001 From: Chris R Date: Tue, 8 Jun 2021 10:42:12 -0700 Subject: [PATCH 10/19] Cache tasks --- .../Core/src/Internal/TlsConnectionFeature.cs | 23 +++++++++++++------ 1 file changed, 16 insertions(+), 7 deletions(-) diff --git a/src/Servers/Kestrel/Core/src/Internal/TlsConnectionFeature.cs b/src/Servers/Kestrel/Core/src/Internal/TlsConnectionFeature.cs index c0b814345fe3..7f0cc304a7a9 100644 --- a/src/Servers/Kestrel/Core/src/Internal/TlsConnectionFeature.cs +++ b/src/Servers/Kestrel/Core/src/Internal/TlsConnectionFeature.cs @@ -27,7 +27,7 @@ internal class TlsConnectionFeature : ITlsConnectionFeature, ITlsApplicationProt private int? _hashStrength; private ExchangeAlgorithmType? _keyExchangeAlgorithm; private int? _keyExchangeStrength; - private bool _renegotiated; + private Task? _clientCertTask; public TlsConnectionFeature(SslStream sslStream, ClientCertificateMode clientCertificateMode) { @@ -101,20 +101,29 @@ public int KeyExchangeStrength set => _keyExchangeStrength = value; } - public async Task GetClientCertificateAsync(CancellationToken cancellationToken) + public Task GetClientCertificateAsync(CancellationToken cancellationToken) { + // Only try once per connection + if (_clientCertTask != null) + { + return _clientCertTask; + } + if (ClientCertificate != null || _clientCertificateMode != ClientCertificateMode.DelayCertificate - || _renegotiated // Delayed client cert negotiation is not allowed on HTTP/2. || _sslStream.NegotiatedApplicationProtocol == SslApplicationProtocol.Http2) { - return ClientCertificate; + return _clientCertTask = Task.FromResult(ClientCertificate); } - _renegotiated = true; // Only try once - await _sslStream.NegotiateClientCertificateAsync(cancellationToken); - return ClientCertificate; + return _clientCertTask = GetClientCertificateAsyncCore(cancellationToken); + + async Task GetClientCertificateAsyncCore(CancellationToken cancellationToken) + { + await _sslStream.NegotiateClientCertificateAsync(cancellationToken); + return ClientCertificate; + } } private static X509Certificate2? ConvertToX509Certificate2(X509Certificate? certificate) From 0d5103b9c30f0aff42d2b9be8a54d4b806699350 Mon Sep 17 00:00:00 2001 From: Chris R Date: Tue, 8 Jun 2021 10:58:44 -0700 Subject: [PATCH 11/19] ServerOptionsSelectionCallback test --- .../HttpsConnectionMiddlewareTests.cs | 42 +++++++++++++++++++ 1 file changed, 42 insertions(+) diff --git a/src/Servers/Kestrel/test/InMemory.FunctionalTests/HttpsConnectionMiddlewareTests.cs b/src/Servers/Kestrel/test/InMemory.FunctionalTests/HttpsConnectionMiddlewareTests.cs index f25fab61d76a..e5b2a0a9b315 100644 --- a/src/Servers/Kestrel/test/InMemory.FunctionalTests/HttpsConnectionMiddlewareTests.cs +++ b/src/Servers/Kestrel/test/InMemory.FunctionalTests/HttpsConnectionMiddlewareTests.cs @@ -11,6 +11,7 @@ using System.Security.Authentication; using System.Security.Cryptography.X509Certificates; using System.Text; +using System.Threading; using System.Threading.Tasks; using Microsoft.AspNetCore.Connections.Features; using Microsoft.AspNetCore.Hosting; @@ -564,6 +565,47 @@ void ConfigureListenOptions(ListenOptions listenOptions) await AssertConnectionResult(stream, true); } + [ConditionalFact] + [OSSkipCondition(OperatingSystems.MacOSX | OperatingSystems.Linux, SkipReason = "Not supported yet.")] + public async Task CanRenegotiateForServerOptionsSelectionCallback() + { + void ConfigureListenOptions(ListenOptions listenOptions) + { + listenOptions.UseHttps((SslStream stream, SslClientHelloInfo clientHelloInfo, object state, CancellationToken cancellationToken) => + { + return ValueTask.FromResult(new SslServerAuthenticationOptions() + { + ServerCertificate = _x509Certificate2, + ClientCertificateRequired = false, + RemoteCertificateValidationCallback = (_, _, _, _) => true, + }); + }, state: null); + } + + await using var server = new TestServer(async context => + { + var tlsFeature = context.Features.Get(); + Assert.NotNull(tlsFeature); + Assert.Null(tlsFeature.ClientCertificate); + Assert.Null(context.Connection.ClientCertificate); + + var clientCert = await context.Connection.GetClientCertificateAsync(); + Assert.NotNull(clientCert); + Assert.NotNull(tlsFeature.ClientCertificate); + Assert.NotNull(context.Connection.ClientCertificate); + + await context.Response.WriteAsync("hello world"); + }, new TestServiceContext(LoggerFactory), ConfigureListenOptions); + + using var connection = server.CreateConnection(); + // SslStream is used to ensure the certificate is actually passed to the server + // HttpClient might not send the certificate because it is invalid or it doesn't match any + // of the certificate authorities sent by the server in the SSL handshake. + var stream = OpenSslStreamWithCert(connection.Stream); + await stream.AuthenticateAsClientAsync(Guid.NewGuid().ToString()); + await AssertConnectionResult(stream, true); + } + [ConditionalFact] [OSSkipCondition(OperatingSystems.MacOSX | OperatingSystems.Linux, SkipReason = "Not supported yet.")] public async Task CanRenegotiateForClientCertificateOnHttp1CanReturnNoCert() From 9b6878e57090dd0d903dab7c51877159aff11067 Mon Sep 17 00:00:00 2001 From: Chris R Date: Tue, 8 Jun 2021 12:19:31 -0700 Subject: [PATCH 12/19] Consolidate tests --- .../HttpsConnectionMiddlewareTests.cs | 51 ++----------------- 1 file changed, 5 insertions(+), 46 deletions(-) diff --git a/src/Servers/Kestrel/test/InMemory.FunctionalTests/HttpsConnectionMiddlewareTests.cs b/src/Servers/Kestrel/test/InMemory.FunctionalTests/HttpsConnectionMiddlewareTests.cs index e5b2a0a9b315..462b9f9efcb3 100644 --- a/src/Servers/Kestrel/test/InMemory.FunctionalTests/HttpsConnectionMiddlewareTests.cs +++ b/src/Servers/Kestrel/test/InMemory.FunctionalTests/HttpsConnectionMiddlewareTests.cs @@ -526,13 +526,15 @@ void ConfigureListenOptions(ListenOptions listenOptions) await AssertConnectionResult(stream, true); } - [ConditionalFact] + [ConditionalTheory] + [InlineData(HttpProtocols.Http1)] + [InlineData(HttpProtocols.Http1AndHttp2)] // Make sure turning on Http/2 doesn't regress HTTP/1 [OSSkipCondition(OperatingSystems.MacOSX | OperatingSystems.Linux, SkipReason = "Not supported yet.")] - public async Task CanRenegotiateForClientCertificateOnHttp1() + public async Task CanRenegotiateForClientCertificate(HttpProtocols httpProtocols) { void ConfigureListenOptions(ListenOptions listenOptions) { - listenOptions.Protocols = HttpProtocols.Http1; + listenOptions.Protocols = httpProtocols; listenOptions.UseHttps(options => { options.ServerCertificate = _x509Certificate2; @@ -652,49 +654,6 @@ void ConfigureListenOptions(ListenOptions listenOptions) await AssertConnectionResult(stream, true); } - [ConditionalFact(Skip = "Depends on https://github.com/dotnet/runtime/pull/53719")] - [OSSkipCondition(OperatingSystems.MacOSX | OperatingSystems.Linux, SkipReason = "Not supported yet.")] - // Turning on HTTP/2 disables renegotiation. - // TODO: Tomas is changing it so AllowRenegotiation only applies to the remote, - // NegotiateClientCertificateAsync call be called locally and it's up to us to prevent that - // on HTTP/2. - public async Task CanRenegotiateForClientCertificateOnHttp1WithHttp2() - { - void ConfigureListenOptions(ListenOptions listenOptions) - { - listenOptions.Protocols = HttpProtocols.Http1AndHttp2; - listenOptions.UseHttps(options => - { - options.ServerCertificate = _x509Certificate2; - options.ClientCertificateMode = ClientCertificateMode.DelayCertificate; - options.AllowAnyClientCertificate(); - }); - } - - await using var server = new TestServer(async context => - { - var tlsFeature = context.Features.Get(); - Assert.NotNull(tlsFeature); - Assert.Null(tlsFeature.ClientCertificate); - Assert.Null(context.Connection.ClientCertificate); - - var cert = await context.Connection.GetClientCertificateAsync(); - Assert.NotNull(cert); - Assert.NotNull(tlsFeature.ClientCertificate); - Assert.NotNull(context.Connection.ClientCertificate); - - await context.Response.WriteAsync("hello world"); - }, new TestServiceContext(LoggerFactory), ConfigureListenOptions); - - using var connection = server.CreateConnection(); - // SslStream is used to ensure the certificate is actually passed to the server - // HttpClient might not send the certificate because it is invalid or it doesn't match any - // of the certificate authorities sent by the server in the SSL handshake. - var stream = OpenSslStreamWithCert(connection.Stream); - await stream.AuthenticateAsClientAsync(Guid.NewGuid().ToString()); - await AssertConnectionResult(stream, true); - } - [ConditionalFact] [OSSkipCondition(OperatingSystems.MacOSX | OperatingSystems.Linux, SkipReason = "Not supported yet.")] public async Task RenegotiateForClientCertificateOnPostWithoutBufferingThrows() From 3bfcdf080233d60e729d39d79726dbaf9a6f3f23 Mon Sep 17 00:00:00 2001 From: Chris R Date: Tue, 8 Jun 2021 13:25:27 -0700 Subject: [PATCH 13/19] Clean up sample --- .../samples/SampleApp/BufferingTlsFeature.cs | 50 ------------ .../SampleApp/ClientCertBufferingFeature.cs | 77 +++++++++++++++++++ .../Kestrel/samples/SampleApp/Startup.cs | 18 +---- 3 files changed, 79 insertions(+), 66 deletions(-) delete mode 100644 src/Servers/Kestrel/samples/SampleApp/BufferingTlsFeature.cs create mode 100644 src/Servers/Kestrel/samples/SampleApp/ClientCertBufferingFeature.cs diff --git a/src/Servers/Kestrel/samples/SampleApp/BufferingTlsFeature.cs b/src/Servers/Kestrel/samples/SampleApp/BufferingTlsFeature.cs deleted file mode 100644 index 5cb00e31972d..000000000000 --- a/src/Servers/Kestrel/samples/SampleApp/BufferingTlsFeature.cs +++ /dev/null @@ -1,50 +0,0 @@ -// 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.Security.Cryptography.X509Certificates; -using System.Threading; -using System.Threading.Tasks; -using Microsoft.AspNetCore.Connections.Features; -using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Http.Features; -using Microsoft.AspNetCore.WebUtilities; - -namespace SampleApp -{ - internal class BufferingTlsFeature : ITlsConnectionFeature - { - private ITlsConnectionFeature _tlsFeature; - private HttpContext _context; - - public BufferingTlsFeature(ITlsConnectionFeature tlsFeature, HttpContext context) - { - _tlsFeature = tlsFeature; - _context = context; - } - - public X509Certificate2 ClientCertificate - { - get => _tlsFeature.ClientCertificate; - set => _tlsFeature.ClientCertificate = value; - } - - public async Task GetClientCertificateAsync(CancellationToken cancellationToken) - { - // TODO: This doesn't set a size limit for the buffering or draining by default, it relies on the server's - // 30mb default request size limit. - if (!_context.Request.Body.CanSeek) - { - _context.Request.EnableBuffering(); - } - var body = _context.Request.Body; - await body.DrainAsync(cancellationToken); - body.Position = 0; - - // Negative caching, prevent buffering on future requests even if the client does not give a cert when prompted. - var connectionItems = _context.Features.Get(); - connectionItems.Items["tls.clientcert.negotiated"] = true; - - return await _tlsFeature.GetClientCertificateAsync(cancellationToken); - } - } -} diff --git a/src/Servers/Kestrel/samples/SampleApp/ClientCertBufferingFeature.cs b/src/Servers/Kestrel/samples/SampleApp/ClientCertBufferingFeature.cs new file mode 100644 index 000000000000..e14d20d1b684 --- /dev/null +++ b/src/Servers/Kestrel/samples/SampleApp/ClientCertBufferingFeature.cs @@ -0,0 +1,77 @@ +// 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.Security.Cryptography.X509Certificates; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Connections.Features; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Http.Features; +using Microsoft.AspNetCore.WebUtilities; + +namespace SampleApp +{ + internal static class ClientCertBufferingExtensions + { + // Buffers HTTP/1.x request bodies received over TLS (https) if a client certificate needs to be negotiated. + // This avoids the issue where POST data is received during the certificate negotiation: + // InvalidOperationException: Received data during renegotiation. + public static IApplicationBuilder UseClientCertBuffering(this IApplicationBuilder builder) + { + return builder.Use((context, next) => + { + var tlsFeature = context.Features.Get(); + var bodyFeature = context.Features.Get(); + var connectionItems = context.Features.Get(); + + // Look for TLS connections that don't already have a client cert, and requests that could have a body. + if (tlsFeature != null && tlsFeature.ClientCertificate == null && bodyFeature.CanHaveBody + && !connectionItems.Items.TryGetValue("tls.clientcert.negotiated", out var _)) + { + context.Features.Set(new ClientCertBufferingFeature(tlsFeature, context)); + } + + return next(context); + }); + } + } + + internal class ClientCertBufferingFeature : ITlsConnectionFeature + { + private ITlsConnectionFeature _tlsFeature; + private HttpContext _context; + + public ClientCertBufferingFeature(ITlsConnectionFeature tlsFeature, HttpContext context) + { + _tlsFeature = tlsFeature; + _context = context; + } + + public X509Certificate2 ClientCertificate + { + get => _tlsFeature.ClientCertificate; + set => _tlsFeature.ClientCertificate = value; + } + + public async Task GetClientCertificateAsync(CancellationToken cancellationToken) + { + // Note: This doesn't set its own size limit for the buffering or draining, it relies on the server's + // 30mb default request size limit. + if (!_context.Request.Body.CanSeek) + { + _context.Request.EnableBuffering(); + } + + var body = _context.Request.Body; + await body.DrainAsync(cancellationToken); + body.Position = 0; + + // Negative caching, prevent buffering on future requests even if the client does not give a cert when prompted. + var connectionItems = _context.Features.Get(); + connectionItems.Items["tls.clientcert.negotiated"] = true; + + return await _tlsFeature.GetClientCertificateAsync(cancellationToken); + } + } +} diff --git a/src/Servers/Kestrel/samples/SampleApp/Startup.cs b/src/Servers/Kestrel/samples/SampleApp/Startup.cs index 6111f0777fe8..2b489a1a4d7c 100644 --- a/src/Servers/Kestrel/samples/SampleApp/Startup.cs +++ b/src/Servers/Kestrel/samples/SampleApp/Startup.cs @@ -28,22 +28,8 @@ public void Configure(IApplicationBuilder app, ILoggerFactory loggerFactory) { var logger = loggerFactory.CreateLogger("Default"); - app.Use((context, next) => - { - var tlsFeature = context.Features.Get(); - var bodyFeature = context.Features.Get(); - var connectionItems = context.Features.Get(); - - // Look for TLS connections that don't already have a client cert, and requests that could have a body. - if (tlsFeature != null && tlsFeature.ClientCertificate == null && bodyFeature.CanHaveBody - && !connectionItems.Items.TryGetValue("tls.clientcert.negotiated", out var _)) - { - context.Features.Set(new BufferingTlsFeature(tlsFeature, context)); - } + app.UseClientCertBuffering(); - return next(context); - }); - /* // Add an exception handler that prevents throwing due to large request body size app.Use(async (context, next) => { @@ -56,7 +42,7 @@ public void Configure(IApplicationBuilder app, ILoggerFactory loggerFactory) } catch (Microsoft.AspNetCore.Http.BadHttpRequestException ex) when (ex.StatusCode == StatusCodes.Status413RequestEntityTooLarge) { } }); - */ + app.Run(async context => { // Drain the request body From 4f64234d52f106aa9a1f693f6e0ce61bc0d50130 Mon Sep 17 00:00:00 2001 From: Chris R Date: Tue, 8 Jun 2021 13:37:28 -0700 Subject: [PATCH 14/19] Cleanup --- .../Kestrel/Core/src/Internal/TlsConnectionFeature.cs | 2 +- .../Kestrel/Core/src/ListenOptionsHttpsExtensions.cs | 3 +-- .../HttpsConnectionMiddlewareTests.cs | 7 +++++++ 3 files changed, 9 insertions(+), 3 deletions(-) diff --git a/src/Servers/Kestrel/Core/src/Internal/TlsConnectionFeature.cs b/src/Servers/Kestrel/Core/src/Internal/TlsConnectionFeature.cs index 7f0cc304a7a9..b23016009586 100644 --- a/src/Servers/Kestrel/Core/src/Internal/TlsConnectionFeature.cs +++ b/src/Servers/Kestrel/Core/src/Internal/TlsConnectionFeature.cs @@ -111,7 +111,7 @@ public int KeyExchangeStrength if (ClientCertificate != null || _clientCertificateMode != ClientCertificateMode.DelayCertificate - // Delayed client cert negotiation is not allowed on HTTP/2. + // Delayed client cert negotiation is not allowed on HTTP/2 (or HTTP/3, but that's implemented elsewhere). || _sslStream.NegotiatedApplicationProtocol == SslApplicationProtocol.Http2) { return _clientCertTask = Task.FromResult(ClientCertificate); diff --git a/src/Servers/Kestrel/Core/src/ListenOptionsHttpsExtensions.cs b/src/Servers/Kestrel/Core/src/ListenOptionsHttpsExtensions.cs index 37b1728817bd..829fb93876ca 100644 --- a/src/Servers/Kestrel/Core/src/ListenOptionsHttpsExtensions.cs +++ b/src/Servers/Kestrel/Core/src/ListenOptionsHttpsExtensions.cs @@ -254,8 +254,7 @@ public static ListenOptions UseHttps(this ListenOptions listenOptions, ServerOpt /// State for the . /// Specifies the maximum amount of time allowed for the TLS/SSL handshake. This must be positive and finite. /// The . - public static ListenOptions UseHttps(this ListenOptions listenOptions, ServerOptionsSelectionCallback serverOptionsSelectionCallback, - object state, TimeSpan handshakeTimeout) + public static ListenOptions UseHttps(this ListenOptions listenOptions, ServerOptionsSelectionCallback serverOptionsSelectionCallback, object state, TimeSpan handshakeTimeout) { // HttpsOptionsCallback is an internal delegate that is just the ServerOptionsSelectionCallback + a ConnectionContext parameter. // Given that ConnectionContext will eventually be replaced by System.Net.Connections, it doesn't make much sense to make the HttpsOptionsCallback delegate public. diff --git a/src/Servers/Kestrel/test/InMemory.FunctionalTests/HttpsConnectionMiddlewareTests.cs b/src/Servers/Kestrel/test/InMemory.FunctionalTests/HttpsConnectionMiddlewareTests.cs index 462b9f9efcb3..62d0beecc511 100644 --- a/src/Servers/Kestrel/test/InMemory.FunctionalTests/HttpsConnectionMiddlewareTests.cs +++ b/src/Servers/Kestrel/test/InMemory.FunctionalTests/HttpsConnectionMiddlewareTests.cs @@ -481,6 +481,7 @@ void ConfigureListenOptions(ListenOptions listenOptions) // SslStream is used to ensure the certificate is actually passed to the server // HttpClient might not send the certificate because it is invalid or it doesn't match any // of the certificate authorities sent by the server in the SSL handshake. + // Use a random host name to avoid the TLS session resumption cache. var stream = OpenSslStreamWithCert(connection.Stream); await stream.AuthenticateAsClientAsync(Guid.NewGuid().ToString()); await AssertConnectionResult(stream, true); @@ -521,6 +522,7 @@ void ConfigureListenOptions(ListenOptions listenOptions) // SslStream is used to ensure the certificate is actually passed to the server // HttpClient might not send the certificate because it is invalid or it doesn't match any // of the certificate authorities sent by the server in the SSL handshake. + // Use a random host name to avoid the TLS session resumption cache. var stream = OpenSslStreamWithCert(connection.Stream); await stream.AuthenticateAsClientAsync(Guid.NewGuid().ToString()); await AssertConnectionResult(stream, true); @@ -562,6 +564,7 @@ void ConfigureListenOptions(ListenOptions listenOptions) // SslStream is used to ensure the certificate is actually passed to the server // HttpClient might not send the certificate because it is invalid or it doesn't match any // of the certificate authorities sent by the server in the SSL handshake. + // Use a random host name to avoid the TLS session resumption cache. var stream = OpenSslStreamWithCert(connection.Stream); await stream.AuthenticateAsClientAsync(Guid.NewGuid().ToString()); await AssertConnectionResult(stream, true); @@ -603,6 +606,7 @@ void ConfigureListenOptions(ListenOptions listenOptions) // SslStream is used to ensure the certificate is actually passed to the server // HttpClient might not send the certificate because it is invalid or it doesn't match any // of the certificate authorities sent by the server in the SSL handshake. + // Use a random host name to avoid the TLS session resumption cache. var stream = OpenSslStreamWithCert(connection.Stream); await stream.AuthenticateAsClientAsync(Guid.NewGuid().ToString()); await AssertConnectionResult(stream, true); @@ -642,6 +646,7 @@ void ConfigureListenOptions(ListenOptions listenOptions) // SslStream is used to ensure the certificate is actually passed to the server // HttpClient might not send the certificate because it is invalid or it doesn't match any // of the certificate authorities sent by the server in the SSL handshake. + // Use a random host name to avoid the TLS session resumption cache. var stream = new SslStream(connection.Stream); var clientOptions = new SslClientAuthenticationOptions() { @@ -689,6 +694,7 @@ void ConfigureListenOptions(ListenOptions listenOptions) // SslStream is used to ensure the certificate is actually passed to the server // HttpClient might not send the certificate because it is invalid or it doesn't match any // of the certificate authorities sent by the server in the SSL handshake. + // Use a random host name to avoid the TLS session resumption cache. var stream = OpenSslStreamWithCert(connection.Stream); await stream.AuthenticateAsClientAsync(Guid.NewGuid().ToString()); await AssertConnectionResult(stream, false, expectedBody); @@ -733,6 +739,7 @@ void ConfigureListenOptions(ListenOptions listenOptions) // SslStream is used to ensure the certificate is actually passed to the server // HttpClient might not send the certificate because it is invalid or it doesn't match any // of the certificate authorities sent by the server in the SSL handshake. + // Use a random host name to avoid the TLS session resumption cache. var stream = OpenSslStreamWithCert(connection.Stream); await stream.AuthenticateAsClientAsync(Guid.NewGuid().ToString()); await AssertConnectionResult(stream, true, expectedBody); From 54cf4de45c5cec8c6644cfc84c92454df4940621 Mon Sep 17 00:00:00 2001 From: Chris R Date: Tue, 8 Jun 2021 13:51:59 -0700 Subject: [PATCH 15/19] Fail off for SNI config --- src/Servers/Kestrel/Core/src/Internal/SniOptionsSelector.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Servers/Kestrel/Core/src/Internal/SniOptionsSelector.cs b/src/Servers/Kestrel/Core/src/Internal/SniOptionsSelector.cs index b12923981084..4ad1872991cb 100644 --- a/src/Servers/Kestrel/Core/src/Internal/SniOptionsSelector.cs +++ b/src/Servers/Kestrel/Core/src/Internal/SniOptionsSelector.cs @@ -84,7 +84,8 @@ public SniOptionsSelector( if (clientCertificateMode != ClientCertificateMode.NoCertificate) { - sslOptions.ClientCertificateRequired = true; + sslOptions.ClientCertificateRequired = clientCertificateMode == ClientCertificateMode.AllowCertificate + || clientCertificateMode == ClientCertificateMode.RequireCertificate; sslOptions.RemoteCertificateValidationCallback = (sender, certificate, chain, sslPolicyErrors) => HttpsConnectionMiddleware.RemoteCertificateValidationCallback( clientCertificateMode, fallbackHttpsOptions.ClientCertificateValidation, certificate, chain, sslPolicyErrors); From d1362e4042d6066d791a72d3a14a771522f6f00b Mon Sep 17 00:00:00 2001 From: Chris R Date: Tue, 8 Jun 2021 14:31:20 -0700 Subject: [PATCH 16/19] Make DelayCert work with SNI from config --- .../Core/src/Internal/SniOptionsSelector.cs | 16 +++-- .../Core/src/Internal/TlsConnectionFeature.cs | 8 +-- .../Core/src/KestrelConfigurationLoader.cs | 2 +- .../Core/src/ListenOptionsHttpsExtensions.cs | 18 ++++-- .../Middleware/HttpsConnectionMiddleware.cs | 18 +++--- .../Core/test/SniOptionsSelectorTests.cs | 64 +++++++++---------- 6 files changed, 66 insertions(+), 60 deletions(-) diff --git a/src/Servers/Kestrel/Core/src/Internal/SniOptionsSelector.cs b/src/Servers/Kestrel/Core/src/Internal/SniOptionsSelector.cs index 4ad1872991cb..3f78bb13b208 100644 --- a/src/Servers/Kestrel/Core/src/Internal/SniOptionsSelector.cs +++ b/src/Servers/Kestrel/Core/src/Internal/SniOptionsSelector.cs @@ -95,7 +95,7 @@ public SniOptionsSelector( httpProtocols = HttpsConnectionMiddleware.ValidateAndNormalizeHttpProtocols(httpProtocols, logger); HttpsConnectionMiddleware.ConfigureAlpn(sslOptions, httpProtocols); - var sniOptions = new SniOptions(sslOptions, httpProtocols); + var sniOptions = new SniOptions(sslOptions, httpProtocols, clientCertificateMode); if (name.Equals(WildcardHost, StringComparison.Ordinal)) { @@ -113,7 +113,7 @@ public SniOptionsSelector( } } - public SslServerAuthenticationOptions GetOptions(ConnectionContext connection, string serverName) + public (SslServerAuthenticationOptions, ClientCertificateMode) GetOptions(ConnectionContext connection, string serverName) { SniOptions? sniOptions = null; @@ -173,14 +173,14 @@ public SslServerAuthenticationOptions GetOptions(ConnectionContext connection, s _onAuthenticateCallback(connection, sslOptions); } - return sslOptions; + return (sslOptions, sniOptions.ClientCertificateMode); } - public static ValueTask OptionsCallback(ConnectionContext connection, SslStream stream, SslClientHelloInfo clientHelloInfo, object state, CancellationToken cancellationToken) + public static ValueTask<(SslServerAuthenticationOptions, ClientCertificateMode)> OptionsCallback(ConnectionContext connection, SslStream stream, SslClientHelloInfo clientHelloInfo, object state, CancellationToken cancellationToken) { var sniOptionsSelector = (SniOptionsSelector)state; - var options = sniOptionsSelector.GetOptions(connection, clientHelloInfo.ServerName); - return new ValueTask(options); + var (options, clientCertificateMode) = sniOptionsSelector.GetOptions(connection, clientHelloInfo.ServerName); + return new ValueTask<(SslServerAuthenticationOptions, ClientCertificateMode)>((options, clientCertificateMode)); } internal static SslServerAuthenticationOptions CloneSslOptions(SslServerAuthenticationOptions sslOptions) => @@ -201,14 +201,16 @@ internal static SslServerAuthenticationOptions CloneSslOptions(SslServerAuthenti private class SniOptions { - public SniOptions(SslServerAuthenticationOptions sslOptions, HttpProtocols httpProtocols) + public SniOptions(SslServerAuthenticationOptions sslOptions, HttpProtocols httpProtocols, ClientCertificateMode clientCertificateMode) { SslOptions = sslOptions; HttpProtocols = httpProtocols; + ClientCertificateMode = clientCertificateMode; } public SslServerAuthenticationOptions SslOptions { get; } public HttpProtocols HttpProtocols { get; } + public ClientCertificateMode ClientCertificateMode { get; } } private class LongestStringFirstComparer : IComparer diff --git a/src/Servers/Kestrel/Core/src/Internal/TlsConnectionFeature.cs b/src/Servers/Kestrel/Core/src/Internal/TlsConnectionFeature.cs index b23016009586..2c1f0379ccf9 100644 --- a/src/Servers/Kestrel/Core/src/Internal/TlsConnectionFeature.cs +++ b/src/Servers/Kestrel/Core/src/Internal/TlsConnectionFeature.cs @@ -17,7 +17,6 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal internal class TlsConnectionFeature : ITlsConnectionFeature, ITlsApplicationProtocolFeature, ITlsHandshakeFeature { private readonly SslStream _sslStream; - private readonly ClientCertificateMode _clientCertificateMode; private X509Certificate2? _clientCert; private ReadOnlyMemory? _applicationProtocol; private SslProtocols? _protocol; @@ -29,7 +28,7 @@ internal class TlsConnectionFeature : ITlsConnectionFeature, ITlsApplicationProt private int? _keyExchangeStrength; private Task? _clientCertTask; - public TlsConnectionFeature(SslStream sslStream, ClientCertificateMode clientCertificateMode) + public TlsConnectionFeature(SslStream sslStream) { if (sslStream is null) { @@ -37,9 +36,10 @@ public TlsConnectionFeature(SslStream sslStream, ClientCertificateMode clientCer } _sslStream = sslStream; - _clientCertificateMode = clientCertificateMode; } + internal ClientCertificateMode ClientCertificateMode { get; set; } + public X509Certificate2? ClientCertificate { get @@ -110,7 +110,7 @@ public int KeyExchangeStrength } if (ClientCertificate != null - || _clientCertificateMode != ClientCertificateMode.DelayCertificate + || ClientCertificateMode != ClientCertificateMode.DelayCertificate // Delayed client cert negotiation is not allowed on HTTP/2 (or HTTP/3, but that's implemented elsewhere). || _sslStream.NegotiatedApplicationProtocol == SslApplicationProtocol.Http2) { diff --git a/src/Servers/Kestrel/Core/src/KestrelConfigurationLoader.cs b/src/Servers/Kestrel/Core/src/KestrelConfigurationLoader.cs index 22b78d8d96be..e32b04fd6d93 100644 --- a/src/Servers/Kestrel/Core/src/KestrelConfigurationLoader.cs +++ b/src/Servers/Kestrel/Core/src/KestrelConfigurationLoader.cs @@ -401,7 +401,7 @@ public void Load() else { var sniOptionsSelector = new SniOptionsSelector(endpoint.Name, endpoint.Sni, CertificateConfigLoader, httpsOptions, listenOptions.Protocols, HttpsLogger); - listenOptions.UseHttps(SniOptionsSelector.OptionsCallback, sniOptionsSelector, httpsOptions.HandshakeTimeout, ClientCertificateMode.NoCertificate); + listenOptions.UseHttps(SniOptionsSelector.OptionsCallback, sniOptionsSelector, httpsOptions.HandshakeTimeout); } } diff --git a/src/Servers/Kestrel/Core/src/ListenOptionsHttpsExtensions.cs b/src/Servers/Kestrel/Core/src/ListenOptionsHttpsExtensions.cs index 829fb93876ca..91d327671a00 100644 --- a/src/Servers/Kestrel/Core/src/ListenOptionsHttpsExtensions.cs +++ b/src/Servers/Kestrel/Core/src/ListenOptionsHttpsExtensions.cs @@ -5,6 +5,9 @@ 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.Server.Kestrel.Core; using Microsoft.AspNetCore.Server.Kestrel.Https; using Microsoft.AspNetCore.Server.Kestrel.Https.Internal; @@ -258,10 +261,14 @@ public static ListenOptions UseHttps(this ListenOptions listenOptions, ServerOpt { // HttpsOptionsCallback is an internal delegate that is just the ServerOptionsSelectionCallback + a ConnectionContext parameter. // Given that ConnectionContext will eventually be replaced by System.Net.Connections, it doesn't make much sense to make the HttpsOptionsCallback delegate public. - HttpsOptionsCallback adaptedCallback = (connection, stream, clientHelloInfo, state, cancellationToken) => - serverOptionsSelectionCallback(stream, clientHelloInfo, state, cancellationToken); + return listenOptions.UseHttps(GetTlsOptionsAsync, state, handshakeTimeout); - return listenOptions.UseHttps(adaptedCallback, state, handshakeTimeout, ClientCertificateMode.DelayCertificate); + async ValueTask<(SslServerAuthenticationOptions, ClientCertificateMode)> GetTlsOptionsAsync( + ConnectionContext connection, SslStream stream, SslClientHelloInfo clientHelloInfo, object state, CancellationToken cancellationToken) + { + var tlsOptions = await serverOptionsSelectionCallback(stream, clientHelloInfo, state, cancellationToken); + return new (tlsOptions, ClientCertificateMode.DelayCertificate); + } } /// @@ -271,16 +278,15 @@ public static ListenOptions UseHttps(this ListenOptions listenOptions, ServerOpt /// Callback to configure HTTPS options. /// State for the . /// Specifies the maximum amount of time allowed for the TLS/SSL handshake. This must be positive and finite. - /// /// The . - internal static ListenOptions UseHttps(this ListenOptions listenOptions, HttpsOptionsCallback httpsOptionsCallback, object state, TimeSpan handshakeTimeout, ClientCertificateMode clientCertificateMode) + internal static ListenOptions UseHttps(this ListenOptions listenOptions, HttpsOptionsCallback httpsOptionsCallback, object state, TimeSpan handshakeTimeout) { var loggerFactory = listenOptions.KestrelServerOptions?.ApplicationServices.GetRequiredService() ?? NullLoggerFactory.Instance; listenOptions.IsTls = true; listenOptions.Use(next => { - var middleware = new HttpsConnectionMiddleware(next, httpsOptionsCallback, state, handshakeTimeout, loggerFactory, clientCertificateMode); + var middleware = new HttpsConnectionMiddleware(next, httpsOptionsCallback, state, handshakeTimeout, loggerFactory); return middleware.OnConnectionAsync; }); diff --git a/src/Servers/Kestrel/Core/src/Middleware/HttpsConnectionMiddleware.cs b/src/Servers/Kestrel/Core/src/Middleware/HttpsConnectionMiddleware.cs index 8413b11583ca..3cc706334528 100644 --- a/src/Servers/Kestrel/Core/src/Middleware/HttpsConnectionMiddleware.cs +++ b/src/Servers/Kestrel/Core/src/Middleware/HttpsConnectionMiddleware.cs @@ -26,7 +26,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Https.Internal { - internal delegate ValueTask HttpsOptionsCallback(ConnectionContext connection, SslStream stream, SslClientHelloInfo clientHelloInfo, object state, CancellationToken cancellationToken); + internal delegate ValueTask<(SslServerAuthenticationOptions, ClientCertificateMode)> HttpsOptionsCallback(ConnectionContext connection, SslStream stream, SslClientHelloInfo clientHelloInfo, object state, CancellationToken cancellationToken); internal class HttpsConnectionMiddleware { @@ -38,7 +38,6 @@ internal class HttpsConnectionMiddleware private readonly TimeSpan _handshakeTimeout; private readonly ILogger _logger; private readonly Func _sslStreamFactory; - private readonly ClientCertificateMode _clientCertificateMode; // The following fields are only set by HttpsConnectionAdapterOptions ctor. private readonly HttpsConnectionAdapterOptions? _options; @@ -113,9 +112,7 @@ public HttpsConnectionMiddleware(ConnectionDelegate next, HttpsConnectionAdapter _serverCertificateContext = SslStreamCertificateContext.Create(certificate, additionalCertificates: null); } - _clientCertificateMode = _options.ClientCertificateMode; - - var remoteCertificateValidationCallback = _clientCertificateMode == ClientCertificateMode.NoCertificate ? + var remoteCertificateValidationCallback = _options.ClientCertificateMode == ClientCertificateMode.NoCertificate ? (RemoteCertificateValidationCallback?)null : RemoteCertificateValidationCallback; _sslStreamFactory = s => new SslStream(s, leaveInnerStreamOpen: false, userCertificateValidationCallback: remoteCertificateValidationCallback); @@ -126,8 +123,7 @@ internal HttpsConnectionMiddleware( HttpsOptionsCallback httpsOptionsCallback, object httpsOptionsCallbackState, TimeSpan handshakeTimeout, - ILoggerFactory loggerFactory, - ClientCertificateMode clientCertificateMode) + ILoggerFactory loggerFactory) { _next = next; _handshakeTimeout = handshakeTimeout; @@ -135,7 +131,6 @@ internal HttpsConnectionMiddleware( _httpsOptionsCallback = httpsOptionsCallback; _httpsOptionsCallbackState = httpsOptionsCallbackState; - _clientCertificateMode = clientCertificateMode; _sslStreamFactory = s => new SslStream(s); } @@ -152,7 +147,9 @@ public async Task OnConnectionAsync(ConnectionContext context) context.Features.Get()?.MemoryPool ?? MemoryPool.Shared); var sslStream = sslDuplexPipe.Stream; - var feature = new Core.Internal.TlsConnectionFeature(sslStream, _clientCertificateMode); + var feature = new Core.Internal.TlsConnectionFeature(sslStream); + // Set the mode if options were used. If the callback is used it will set the mode later. + feature.ClientCertificateMode = _options?.ClientCertificateMode ?? ClientCertificateMode.NoCertificate; context.Features.Set(feature); context.Features.Set(feature); context.Features.Set(feature); @@ -430,7 +427,8 @@ private static async ValueTask ServerOptionsCall feature.HostName = clientHelloInfo.ServerName; context.Features.Set(sslStream); - var sslOptions = await middleware._httpsOptionsCallback!(context, sslStream, clientHelloInfo, middleware._httpsOptionsCallbackState!, cancellationToken); + var (sslOptions, clientCertificateMode) = await middleware._httpsOptionsCallback!(context, sslStream, clientHelloInfo, middleware._httpsOptionsCallbackState!, cancellationToken); + feature.ClientCertificateMode = clientCertificateMode; KestrelEventSource.Log.TlsHandshakeStart(context, sslOptions); diff --git a/src/Servers/Kestrel/Core/test/SniOptionsSelectorTests.cs b/src/Servers/Kestrel/Core/test/SniOptionsSelectorTests.cs index bbe148069d99..8e2414509b71 100644 --- a/src/Servers/Kestrel/Core/test/SniOptionsSelectorTests.cs +++ b/src/Servers/Kestrel/Core/test/SniOptionsSelectorTests.cs @@ -74,22 +74,22 @@ public void PrefersExactMatchOverWildcardPrefixOverWildcardOnly() logger: Mock.Of>()); var wwwSubdomainOptions = sniOptionsSelector.GetOptions(new MockConnectionContext(), "www.example.org"); - Assert.Equal("Exact", pathDictionary[wwwSubdomainOptions.ServerCertificate]); + Assert.Equal("Exact", pathDictionary[wwwSubdomainOptions.Item1.ServerCertificate]); var baSubdomainOptions = sniOptionsSelector.GetOptions(new MockConnectionContext(), "b.a.example.org"); - Assert.Equal("WildcardPrefix", pathDictionary[baSubdomainOptions.ServerCertificate]); + Assert.Equal("WildcardPrefix", pathDictionary[baSubdomainOptions.Item1.ServerCertificate]); var aSubdomainOptions = sniOptionsSelector.GetOptions(new MockConnectionContext(), "a.example.org"); - Assert.Equal("WildcardPrefix", pathDictionary[aSubdomainOptions.ServerCertificate]); + Assert.Equal("WildcardPrefix", pathDictionary[aSubdomainOptions.Item1.ServerCertificate]); // "*.example.org" is preferred over "*", but "*.example.org" doesn't match "example.org". // REVIEW: Are we OK with "example.org" matching "*" instead of "*.example.org"? It feels annoying to me to have to configure example.org twice. // Unfortunately, the alternative would have "a.example.org" match "*.a.example.org" before "*.example.org", and that just seems wrong. var noSubdomainOptions = sniOptionsSelector.GetOptions(new MockConnectionContext(), "example.org"); - Assert.Equal("WildcardOnly", pathDictionary[noSubdomainOptions.ServerCertificate]); + Assert.Equal("WildcardOnly", pathDictionary[noSubdomainOptions.Item1.ServerCertificate]); var anotherTldOptions = sniOptionsSelector.GetOptions(new MockConnectionContext(), "dot.net"); - Assert.Equal("WildcardOnly", pathDictionary[anotherTldOptions.ServerCertificate]); + Assert.Equal("WildcardOnly", pathDictionary[anotherTldOptions.Item1.ServerCertificate]); } [Fact] @@ -131,11 +131,11 @@ public void PerfersLongerWildcardPrefixOverShorterWildcardPrefix() logger: Mock.Of>()); var baSubdomainOptions = sniOptionsSelector.GetOptions(new MockConnectionContext(), "b.a.example.org"); - Assert.Equal("Long", pathDictionary[baSubdomainOptions.ServerCertificate]); + Assert.Equal("Long", pathDictionary[baSubdomainOptions.Item1.ServerCertificate]); // "*.a.example.org" is preferred over "*.example.org", but "a.example.org" doesn't match "*.a.example.org". var aSubdomainOptions = sniOptionsSelector.GetOptions(new MockConnectionContext(), "a.example.org"); - Assert.Equal("Short", pathDictionary[aSubdomainOptions.ServerCertificate]); + Assert.Equal("Short", pathDictionary[aSubdomainOptions.Item1.ServerCertificate]); } [Fact] @@ -177,13 +177,13 @@ public void ServerNameMatchingIsCaseInsensitive() logger: Mock.Of>()); var wwwSubdomainOptions = sniOptionsSelector.GetOptions(new MockConnectionContext(), "wWw.eXample.oRg"); - Assert.Equal("Exact", pathDictionary[wwwSubdomainOptions.ServerCertificate]); + Assert.Equal("Exact", pathDictionary[wwwSubdomainOptions.Item1.ServerCertificate]); var baSubdomainOptions = sniOptionsSelector.GetOptions(new MockConnectionContext(), "B.a.eXample.oRg"); - Assert.Equal("WildcardPrefix", pathDictionary[baSubdomainOptions.ServerCertificate]); + Assert.Equal("WildcardPrefix", pathDictionary[baSubdomainOptions.Item1.ServerCertificate]); var aSubdomainOptions = sniOptionsSelector.GetOptions(new MockConnectionContext(), "A.eXample.oRg"); - Assert.Equal("WildcardPrefix", pathDictionary[aSubdomainOptions.ServerCertificate]); + Assert.Equal("WildcardPrefix", pathDictionary[aSubdomainOptions.Item1.ServerCertificate]); } [Fact] @@ -233,7 +233,7 @@ public void WildcardOnlyMatchesNullServerNameDueToNoAlpn() logger: Mock.Of>()); var options = sniOptionsSelector.GetOptions(new MockConnectionContext(), null); - Assert.Equal("WildcardOnly", pathDictionary[options.ServerCertificate]); + Assert.Equal("WildcardOnly", pathDictionary[options.Item1.ServerCertificate]); } [Fact] @@ -260,7 +260,7 @@ public void CachesSslServerAuthenticationOptions() var options1 = sniOptionsSelector.GetOptions(new MockConnectionContext(), "www.example.org"); var options2 = sniOptionsSelector.GetOptions(new MockConnectionContext(), "www.example.org"); - Assert.Same(options1, options2); + Assert.Same(options1.Item1, options2.Item1); } [Fact] @@ -296,12 +296,12 @@ public void ClonesSslServerAuthenticationOptionsIfAnOnAuthenticateCallbackIsDefi logger: Mock.Of>()); var options1 = sniOptionsSelector.GetOptions(new MockConnectionContext(), "www.example.org"); - Assert.Same(lastSeenSslOptions, options1); + Assert.Same(lastSeenSslOptions, options1.Item1); var options2 = sniOptionsSelector.GetOptions(new MockConnectionContext(), "www.example.org"); - Assert.Same(lastSeenSslOptions, options2); + Assert.Same(lastSeenSslOptions, options2.Item1); - Assert.NotSame(options1, options2); + Assert.NotSame(options1.Item1, options2.Item1); } [Fact] @@ -339,22 +339,22 @@ public void ClonesSslServerAuthenticationOptionsIfTheFallbackServerCertificateSe logger: Mock.Of>()); var selectorOptions1 = sniOptionsSelector.GetOptions(new MockConnectionContext(), "selector.example.org"); - Assert.Same(selectorCertificate, selectorOptions1.ServerCertificate); + Assert.Same(selectorCertificate, selectorOptions1.Item1.ServerCertificate); var selectorOptions2 = sniOptionsSelector.GetOptions(new MockConnectionContext(), "selector.example.org"); - Assert.Same(selectorCertificate, selectorOptions2.ServerCertificate); + Assert.Same(selectorCertificate, selectorOptions2.Item1.ServerCertificate); // The SslServerAuthenticationOptions were cloned because the cert came from the ServerCertificateSelector fallback. - Assert.NotSame(selectorOptions1, selectorOptions2); + Assert.NotSame(selectorOptions1.Item1, selectorOptions2.Item1); var configOptions1 = sniOptionsSelector.GetOptions(new MockConnectionContext(), "config.example.org"); - Assert.NotSame(selectorCertificate, configOptions1.ServerCertificate); + Assert.NotSame(selectorCertificate, configOptions1.Item1.ServerCertificate); var configOptions2 = sniOptionsSelector.GetOptions(new MockConnectionContext(), "config.example.org"); - Assert.NotSame(selectorCertificate, configOptions2.ServerCertificate); + Assert.NotSame(selectorCertificate, configOptions2.Item1.ServerCertificate); // The SslServerAuthenticationOptions don't need to be cloned if a static cert is defined in config for the given server name. - Assert.Same(configOptions1, configOptions2); + Assert.Same(configOptions1.Item1, configOptions2.Item1); } [Fact] @@ -398,7 +398,7 @@ public void FallsBackToHttpsConnectionAdapterCertificate() logger: Mock.Of>()); var options = sniOptionsSelector.GetOptions(new MockConnectionContext(), "www.example.org"); - Assert.Same(fallbackOptions.ServerCertificate, options.ServerCertificate); + Assert.Same(fallbackOptions.ServerCertificate, options.Item1.ServerCertificate); } [Fact] @@ -426,7 +426,7 @@ public void FallsBackToHttpsConnectionAdapterServerCertificateSelectorOverServer logger: Mock.Of>()); var options = sniOptionsSelector.GetOptions(new MockConnectionContext(), "www.example.org"); - Assert.Same(selectorCertificate, options.ServerCertificate); + Assert.Same(selectorCertificate, options.Item1.ServerCertificate); } [Fact] @@ -486,7 +486,7 @@ public void ConfiguresAlpnBasedOnConfiguredHttpProtocols() logger: Mock.Of>()); var options = sniOptionsSelector.GetOptions(new MockConnectionContext(), "www.example.org"); - var alpnList = options.ApplicationProtocols; + var alpnList = options.Item1.ApplicationProtocols; Assert.NotNull(alpnList); var protocol = Assert.Single(alpnList); @@ -550,7 +550,7 @@ public void PrefersSslProtocolsDefinedInSniConfig() logger: Mock.Of>()); var options = sniOptionsSelector.GetOptions(new MockConnectionContext(), "www.example.org"); - Assert.Equal(SslProtocols.Tls13 | SslProtocols.Tls11, options.EnabledSslProtocols); + Assert.Equal(SslProtocols.Tls13 | SslProtocols.Tls11, options.Item1.EnabledSslProtocols); } [Fact] @@ -579,7 +579,7 @@ public void FallsBackToFallbackSslProtocols() logger: Mock.Of>()); var options = sniOptionsSelector.GetOptions(new MockConnectionContext(), "www.example.org"); - Assert.Equal(SslProtocols.Tls13, options.EnabledSslProtocols); + Assert.Equal(SslProtocols.Tls13, options.Item1.EnabledSslProtocols); } @@ -611,11 +611,11 @@ public void PrefersClientCertificateModeDefinedInSniConfig() var options = sniOptionsSelector.GetOptions(new MockConnectionContext(), "www.example.org"); - Assert.True(options.ClientCertificateRequired); + Assert.True(options.Item1.ClientCertificateRequired); - Assert.NotNull(options.RemoteCertificateValidationCallback); + Assert.NotNull(options.Item1.RemoteCertificateValidationCallback); // The RemoteCertificateValidationCallback should first check if the certificate is null and return false since it's required. - Assert.False(options.RemoteCertificateValidationCallback(sender: null, certificate: null, chain: null, SslPolicyErrors.None)); + Assert.False(options.Item1.RemoteCertificateValidationCallback(sender: null, certificate: null, chain: null, SslPolicyErrors.None)); } [Fact] @@ -646,11 +646,11 @@ public void FallsBackToFallbackClientCertificateMode() var options = sniOptionsSelector.GetOptions(new MockConnectionContext(), "www.example.org"); // Despite the confusing name, ClientCertificateRequired being true simply requests a certificate from the client, but doesn't require it. - Assert.True(options.ClientCertificateRequired); + Assert.True(options.Item1.ClientCertificateRequired); - Assert.NotNull(options.RemoteCertificateValidationCallback); + Assert.NotNull(options.Item1.RemoteCertificateValidationCallback); // The RemoteCertificateValidationCallback should see we're in the AllowCertificate mode and return true. - Assert.True(options.RemoteCertificateValidationCallback(sender: null, certificate: null, chain: null, SslPolicyErrors.None)); + Assert.True(options.Item1.RemoteCertificateValidationCallback(sender: null, certificate: null, chain: null, SslPolicyErrors.None)); } [Fact] From 81c605734d83645abbdbfee1a5a7a2b5ab46779c Mon Sep 17 00:00:00 2001 From: Chris R Date: Wed, 9 Jun 2021 14:14:50 -0700 Subject: [PATCH 17/19] Sni config tests --- .../Kestrel/Core/test/SniOptionsSelectorTests.cs | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/Servers/Kestrel/Core/test/SniOptionsSelectorTests.cs b/src/Servers/Kestrel/Core/test/SniOptionsSelectorTests.cs index 8e2414509b71..d830f81f842c 100644 --- a/src/Servers/Kestrel/Core/test/SniOptionsSelectorTests.cs +++ b/src/Servers/Kestrel/Core/test/SniOptionsSelectorTests.cs @@ -592,7 +592,7 @@ public void PrefersClientCertificateModeDefinedInSniConfig() "www.example.org", new SniConfig { - ClientCertificateMode = ClientCertificateMode.RequireCertificate, + ClientCertificateMode = ClientCertificateMode.DelayCertificate, Certificate = new CertificateConfig() } } @@ -611,11 +611,12 @@ public void PrefersClientCertificateModeDefinedInSniConfig() var options = sniOptionsSelector.GetOptions(new MockConnectionContext(), "www.example.org"); - Assert.True(options.Item1.ClientCertificateRequired); + Assert.Equal(ClientCertificateMode.DelayCertificate, options.Item2); + Assert.False(options.Item1.ClientCertificateRequired); Assert.NotNull(options.Item1.RemoteCertificateValidationCallback); - // The RemoteCertificateValidationCallback should first check if the certificate is null and return false since it's required. - Assert.False(options.Item1.RemoteCertificateValidationCallback(sender: null, certificate: null, chain: null, SslPolicyErrors.None)); + // The RemoteCertificateValidationCallback should first check if the certificate is null and return true since it's optional. + Assert.True(options.Item1.RemoteCertificateValidationCallback(sender: null, certificate: null, chain: null, SslPolicyErrors.None)); } [Fact] @@ -645,6 +646,7 @@ public void FallsBackToFallbackClientCertificateMode() var options = sniOptionsSelector.GetOptions(new MockConnectionContext(), "www.example.org"); + Assert.Equal(ClientCertificateMode.AllowCertificate, options.Item2); // Despite the confusing name, ClientCertificateRequired being true simply requests a certificate from the client, but doesn't require it. Assert.True(options.Item1.ClientCertificateRequired); From 0dd491168b059ac14d2b418e4a08c37c7083a75c Mon Sep 17 00:00:00 2001 From: Chris R Date: Tue, 15 Jun 2021 15:03:11 -0700 Subject: [PATCH 18/19] PR feedback --- .../Core/src/Internal/TlsConnectionFeature.cs | 10 +- .../Core/src/ListenOptionsHttpsExtensions.cs | 2 +- .../Core/test/SniOptionsSelectorTests.cs | 112 +++++++++--------- 3 files changed, 62 insertions(+), 62 deletions(-) diff --git a/src/Servers/Kestrel/Core/src/Internal/TlsConnectionFeature.cs b/src/Servers/Kestrel/Core/src/Internal/TlsConnectionFeature.cs index 2c1f0379ccf9..869b6c98dc1e 100644 --- a/src/Servers/Kestrel/Core/src/Internal/TlsConnectionFeature.cs +++ b/src/Servers/Kestrel/Core/src/Internal/TlsConnectionFeature.cs @@ -118,12 +118,12 @@ public int KeyExchangeStrength } return _clientCertTask = GetClientCertificateAsyncCore(cancellationToken); + } - async Task GetClientCertificateAsyncCore(CancellationToken cancellationToken) - { - await _sslStream.NegotiateClientCertificateAsync(cancellationToken); - return ClientCertificate; - } + private async Task GetClientCertificateAsyncCore(CancellationToken cancellationToken) + { + await _sslStream.NegotiateClientCertificateAsync(cancellationToken); + return ClientCertificate; } private static X509Certificate2? ConvertToX509Certificate2(X509Certificate? certificate) diff --git a/src/Servers/Kestrel/Core/src/ListenOptionsHttpsExtensions.cs b/src/Servers/Kestrel/Core/src/ListenOptionsHttpsExtensions.cs index 91d327671a00..1aeb0fd460b0 100644 --- a/src/Servers/Kestrel/Core/src/ListenOptionsHttpsExtensions.cs +++ b/src/Servers/Kestrel/Core/src/ListenOptionsHttpsExtensions.cs @@ -259,7 +259,7 @@ public static ListenOptions UseHttps(this ListenOptions listenOptions, ServerOpt /// The . public static ListenOptions UseHttps(this ListenOptions listenOptions, ServerOptionsSelectionCallback serverOptionsSelectionCallback, object state, TimeSpan handshakeTimeout) { - // HttpsOptionsCallback is an internal delegate that is just the ServerOptionsSelectionCallback + a ConnectionContext parameter. + // HttpsOptionsCallback is an internal delegate that is the ServerOptionsSelectionCallback, a ConnectionContext, and the ClientCertificateMode. // Given that ConnectionContext will eventually be replaced by System.Net.Connections, it doesn't make much sense to make the HttpsOptionsCallback delegate public. return listenOptions.UseHttps(GetTlsOptionsAsync, state, handshakeTimeout); diff --git a/src/Servers/Kestrel/Core/test/SniOptionsSelectorTests.cs b/src/Servers/Kestrel/Core/test/SniOptionsSelectorTests.cs index d830f81f842c..c776ba1aa85b 100644 --- a/src/Servers/Kestrel/Core/test/SniOptionsSelectorTests.cs +++ b/src/Servers/Kestrel/Core/test/SniOptionsSelectorTests.cs @@ -73,23 +73,23 @@ public void PrefersExactMatchOverWildcardPrefixOverWildcardOnly() fallbackHttpProtocols: HttpProtocols.Http1AndHttp2, logger: Mock.Of>()); - var wwwSubdomainOptions = sniOptionsSelector.GetOptions(new MockConnectionContext(), "www.example.org"); - Assert.Equal("Exact", pathDictionary[wwwSubdomainOptions.Item1.ServerCertificate]); + var (wwwSubdomainOptions, _) = sniOptionsSelector.GetOptions(new MockConnectionContext(), "www.example.org"); + Assert.Equal("Exact", pathDictionary[wwwSubdomainOptions.ServerCertificate]); - var baSubdomainOptions = sniOptionsSelector.GetOptions(new MockConnectionContext(), "b.a.example.org"); - Assert.Equal("WildcardPrefix", pathDictionary[baSubdomainOptions.Item1.ServerCertificate]); + var (baSubdomainOptions, _) = sniOptionsSelector.GetOptions(new MockConnectionContext(), "b.a.example.org"); + Assert.Equal("WildcardPrefix", pathDictionary[baSubdomainOptions.ServerCertificate]); - var aSubdomainOptions = sniOptionsSelector.GetOptions(new MockConnectionContext(), "a.example.org"); - Assert.Equal("WildcardPrefix", pathDictionary[aSubdomainOptions.Item1.ServerCertificate]); + var (aSubdomainOptions, _) = sniOptionsSelector.GetOptions(new MockConnectionContext(), "a.example.org"); + Assert.Equal("WildcardPrefix", pathDictionary[aSubdomainOptions.ServerCertificate]); // "*.example.org" is preferred over "*", but "*.example.org" doesn't match "example.org". // REVIEW: Are we OK with "example.org" matching "*" instead of "*.example.org"? It feels annoying to me to have to configure example.org twice. // Unfortunately, the alternative would have "a.example.org" match "*.a.example.org" before "*.example.org", and that just seems wrong. - var noSubdomainOptions = sniOptionsSelector.GetOptions(new MockConnectionContext(), "example.org"); - Assert.Equal("WildcardOnly", pathDictionary[noSubdomainOptions.Item1.ServerCertificate]); + var (noSubdomainOptions, _) = sniOptionsSelector.GetOptions(new MockConnectionContext(), "example.org"); + Assert.Equal("WildcardOnly", pathDictionary[noSubdomainOptions.ServerCertificate]); - var anotherTldOptions = sniOptionsSelector.GetOptions(new MockConnectionContext(), "dot.net"); - Assert.Equal("WildcardOnly", pathDictionary[anotherTldOptions.Item1.ServerCertificate]); + var (anotherTldOptions, _) = sniOptionsSelector.GetOptions(new MockConnectionContext(), "dot.net"); + Assert.Equal("WildcardOnly", pathDictionary[anotherTldOptions.ServerCertificate]); } [Fact] @@ -130,12 +130,12 @@ public void PerfersLongerWildcardPrefixOverShorterWildcardPrefix() fallbackHttpProtocols: HttpProtocols.Http1AndHttp2, logger: Mock.Of>()); - var baSubdomainOptions = sniOptionsSelector.GetOptions(new MockConnectionContext(), "b.a.example.org"); - Assert.Equal("Long", pathDictionary[baSubdomainOptions.Item1.ServerCertificate]); + var (baSubdomainOptions, _) = sniOptionsSelector.GetOptions(new MockConnectionContext(), "b.a.example.org"); + Assert.Equal("Long", pathDictionary[baSubdomainOptions.ServerCertificate]); // "*.a.example.org" is preferred over "*.example.org", but "a.example.org" doesn't match "*.a.example.org". - var aSubdomainOptions = sniOptionsSelector.GetOptions(new MockConnectionContext(), "a.example.org"); - Assert.Equal("Short", pathDictionary[aSubdomainOptions.Item1.ServerCertificate]); + var (aSubdomainOptions, _) = sniOptionsSelector.GetOptions(new MockConnectionContext(), "a.example.org"); + Assert.Equal("Short", pathDictionary[aSubdomainOptions.ServerCertificate]); } [Fact] @@ -176,14 +176,14 @@ public void ServerNameMatchingIsCaseInsensitive() fallbackHttpProtocols: HttpProtocols.Http1AndHttp2, logger: Mock.Of>()); - var wwwSubdomainOptions = sniOptionsSelector.GetOptions(new MockConnectionContext(), "wWw.eXample.oRg"); - Assert.Equal("Exact", pathDictionary[wwwSubdomainOptions.Item1.ServerCertificate]); + var (wwwSubdomainOptions, _) = sniOptionsSelector.GetOptions(new MockConnectionContext(), "wWw.eXample.oRg"); + Assert.Equal("Exact", pathDictionary[wwwSubdomainOptions.ServerCertificate]); - var baSubdomainOptions = sniOptionsSelector.GetOptions(new MockConnectionContext(), "B.a.eXample.oRg"); - Assert.Equal("WildcardPrefix", pathDictionary[baSubdomainOptions.Item1.ServerCertificate]); + var (baSubdomainOptions, _) = sniOptionsSelector.GetOptions(new MockConnectionContext(), "B.a.eXample.oRg"); + Assert.Equal("WildcardPrefix", pathDictionary[baSubdomainOptions.ServerCertificate]); - var aSubdomainOptions = sniOptionsSelector.GetOptions(new MockConnectionContext(), "A.eXample.oRg"); - Assert.Equal("WildcardPrefix", pathDictionary[aSubdomainOptions.Item1.ServerCertificate]); + var (aSubdomainOptions, _) = sniOptionsSelector.GetOptions(new MockConnectionContext(), "A.eXample.oRg"); + Assert.Equal("WildcardPrefix", pathDictionary[aSubdomainOptions.ServerCertificate]); } [Fact] @@ -232,8 +232,8 @@ public void WildcardOnlyMatchesNullServerNameDueToNoAlpn() fallbackHttpProtocols: HttpProtocols.Http1AndHttp2, logger: Mock.Of>()); - var options = sniOptionsSelector.GetOptions(new MockConnectionContext(), null); - Assert.Equal("WildcardOnly", pathDictionary[options.Item1.ServerCertificate]); + var (options, _) = sniOptionsSelector.GetOptions(new MockConnectionContext(), null); + Assert.Equal("WildcardOnly", pathDictionary[options.ServerCertificate]); } [Fact] @@ -258,9 +258,9 @@ public void CachesSslServerAuthenticationOptions() fallbackHttpProtocols: HttpProtocols.Http1AndHttp2, logger: Mock.Of>()); - var options1 = sniOptionsSelector.GetOptions(new MockConnectionContext(), "www.example.org"); - var options2 = sniOptionsSelector.GetOptions(new MockConnectionContext(), "www.example.org"); - Assert.Same(options1.Item1, options2.Item1); + var (options1, _) = sniOptionsSelector.GetOptions(new MockConnectionContext(), "www.example.org"); + var (options2, _) = sniOptionsSelector.GetOptions(new MockConnectionContext(), "www.example.org"); + Assert.Same(options1, options2); } [Fact] @@ -295,13 +295,13 @@ public void ClonesSslServerAuthenticationOptionsIfAnOnAuthenticateCallbackIsDefi fallbackHttpProtocols: HttpProtocols.Http1AndHttp2, logger: Mock.Of>()); - var options1 = sniOptionsSelector.GetOptions(new MockConnectionContext(), "www.example.org"); - Assert.Same(lastSeenSslOptions, options1.Item1); + var (options1, _) = sniOptionsSelector.GetOptions(new MockConnectionContext(), "www.example.org"); + Assert.Same(lastSeenSslOptions, options1); - var options2 = sniOptionsSelector.GetOptions(new MockConnectionContext(), "www.example.org"); - Assert.Same(lastSeenSslOptions, options2.Item1); + var (options2, _) = sniOptionsSelector.GetOptions(new MockConnectionContext(), "www.example.org"); + Assert.Same(lastSeenSslOptions, options2); - Assert.NotSame(options1.Item1, options2.Item1); + Assert.NotSame(options1, options2); } [Fact] @@ -338,23 +338,23 @@ public void ClonesSslServerAuthenticationOptionsIfTheFallbackServerCertificateSe fallbackHttpProtocols: HttpProtocols.Http1AndHttp2, logger: Mock.Of>()); - var selectorOptions1 = sniOptionsSelector.GetOptions(new MockConnectionContext(), "selector.example.org"); - Assert.Same(selectorCertificate, selectorOptions1.Item1.ServerCertificate); + var (selectorOptions1, _) = sniOptionsSelector.GetOptions(new MockConnectionContext(), "selector.example.org"); + Assert.Same(selectorCertificate, selectorOptions1.ServerCertificate); - var selectorOptions2 = sniOptionsSelector.GetOptions(new MockConnectionContext(), "selector.example.org"); - Assert.Same(selectorCertificate, selectorOptions2.Item1.ServerCertificate); + var (selectorOptions2, _) = sniOptionsSelector.GetOptions(new MockConnectionContext(), "selector.example.org"); + Assert.Same(selectorCertificate, selectorOptions2.ServerCertificate); // The SslServerAuthenticationOptions were cloned because the cert came from the ServerCertificateSelector fallback. - Assert.NotSame(selectorOptions1.Item1, selectorOptions2.Item1); + Assert.NotSame(selectorOptions1, selectorOptions2); - var configOptions1 = sniOptionsSelector.GetOptions(new MockConnectionContext(), "config.example.org"); - Assert.NotSame(selectorCertificate, configOptions1.Item1.ServerCertificate); + var (configOptions1, _) = sniOptionsSelector.GetOptions(new MockConnectionContext(), "config.example.org"); + Assert.NotSame(selectorCertificate, configOptions1.ServerCertificate); - var configOptions2 = sniOptionsSelector.GetOptions(new MockConnectionContext(), "config.example.org"); - Assert.NotSame(selectorCertificate, configOptions2.Item1.ServerCertificate); + var (configOptions2, _) = sniOptionsSelector.GetOptions(new MockConnectionContext(), "config.example.org"); + Assert.NotSame(selectorCertificate, configOptions2.ServerCertificate); // The SslServerAuthenticationOptions don't need to be cloned if a static cert is defined in config for the given server name. - Assert.Same(configOptions1.Item1, configOptions2.Item1); + Assert.Same(configOptions1, configOptions2); } [Fact] @@ -397,8 +397,8 @@ public void FallsBackToHttpsConnectionAdapterCertificate() fallbackHttpProtocols: HttpProtocols.Http1AndHttp2, logger: Mock.Of>()); - var options = sniOptionsSelector.GetOptions(new MockConnectionContext(), "www.example.org"); - Assert.Same(fallbackOptions.ServerCertificate, options.Item1.ServerCertificate); + var (options, _) = sniOptionsSelector.GetOptions(new MockConnectionContext(), "www.example.org"); + Assert.Same(fallbackOptions.ServerCertificate, options.ServerCertificate); } [Fact] @@ -425,8 +425,8 @@ public void FallsBackToHttpsConnectionAdapterServerCertificateSelectorOverServer fallbackHttpProtocols: HttpProtocols.Http1AndHttp2, logger: Mock.Of>()); - var options = sniOptionsSelector.GetOptions(new MockConnectionContext(), "www.example.org"); - Assert.Same(selectorCertificate, options.Item1.ServerCertificate); + var (options, _) = sniOptionsSelector.GetOptions(new MockConnectionContext(), "www.example.org"); + Assert.Same(selectorCertificate, options.ServerCertificate); } [Fact] @@ -578,8 +578,8 @@ public void FallsBackToFallbackSslProtocols() fallbackHttpProtocols: HttpProtocols.Http1AndHttp2, logger: Mock.Of>()); - var options = sniOptionsSelector.GetOptions(new MockConnectionContext(), "www.example.org"); - Assert.Equal(SslProtocols.Tls13, options.Item1.EnabledSslProtocols); + var (options, _) = sniOptionsSelector.GetOptions(new MockConnectionContext(), "www.example.org"); + Assert.Equal(SslProtocols.Tls13, options.EnabledSslProtocols); } @@ -609,14 +609,14 @@ public void PrefersClientCertificateModeDefinedInSniConfig() fallbackHttpProtocols: HttpProtocols.Http1AndHttp2, logger: Mock.Of>()); - var options = sniOptionsSelector.GetOptions(new MockConnectionContext(), "www.example.org"); + var (options, certMode) = sniOptionsSelector.GetOptions(new MockConnectionContext(), "www.example.org"); - Assert.Equal(ClientCertificateMode.DelayCertificate, options.Item2); - Assert.False(options.Item1.ClientCertificateRequired); + Assert.Equal(ClientCertificateMode.DelayCertificate, certMode); + Assert.False(options.ClientCertificateRequired); - Assert.NotNull(options.Item1.RemoteCertificateValidationCallback); + Assert.NotNull(options.RemoteCertificateValidationCallback); // The RemoteCertificateValidationCallback should first check if the certificate is null and return true since it's optional. - Assert.True(options.Item1.RemoteCertificateValidationCallback(sender: null, certificate: null, chain: null, SslPolicyErrors.None)); + Assert.True(options.RemoteCertificateValidationCallback(sender: null, certificate: null, chain: null, SslPolicyErrors.None)); } [Fact] @@ -644,15 +644,15 @@ public void FallsBackToFallbackClientCertificateMode() fallbackHttpProtocols: HttpProtocols.Http1AndHttp2, logger: Mock.Of>()); - var options = sniOptionsSelector.GetOptions(new MockConnectionContext(), "www.example.org"); + var (options, certMode) = sniOptionsSelector.GetOptions(new MockConnectionContext(), "www.example.org"); - Assert.Equal(ClientCertificateMode.AllowCertificate, options.Item2); + Assert.Equal(ClientCertificateMode.AllowCertificate, certMode); // Despite the confusing name, ClientCertificateRequired being true simply requests a certificate from the client, but doesn't require it. - Assert.True(options.Item1.ClientCertificateRequired); + Assert.True(options.ClientCertificateRequired); - Assert.NotNull(options.Item1.RemoteCertificateValidationCallback); + Assert.NotNull(options.RemoteCertificateValidationCallback); // The RemoteCertificateValidationCallback should see we're in the AllowCertificate mode and return true. - Assert.True(options.Item1.RemoteCertificateValidationCallback(sender: null, certificate: null, chain: null, SslPolicyErrors.None)); + Assert.True(options.RemoteCertificateValidationCallback(sender: null, certificate: null, chain: null, SslPolicyErrors.None)); } [Fact] From 5c786a745926ff808eeee4fb075df33e8d563e26 Mon Sep 17 00:00:00 2001 From: Chris R Date: Tue, 15 Jun 2021 15:36:58 -0700 Subject: [PATCH 19/19] Http2 negative test --- .../Core/test/SniOptionsSelectorTests.cs | 8 +- .../HttpClientHttp2InteropTests.cs | 89 +++++++++++++++++++ 2 files changed, 93 insertions(+), 4 deletions(-) diff --git a/src/Servers/Kestrel/Core/test/SniOptionsSelectorTests.cs b/src/Servers/Kestrel/Core/test/SniOptionsSelectorTests.cs index c776ba1aa85b..c4d20185d92a 100644 --- a/src/Servers/Kestrel/Core/test/SniOptionsSelectorTests.cs +++ b/src/Servers/Kestrel/Core/test/SniOptionsSelectorTests.cs @@ -485,8 +485,8 @@ public void ConfiguresAlpnBasedOnConfiguredHttpProtocols() fallbackHttpProtocols: HttpProtocols.None, logger: Mock.Of>()); - var options = sniOptionsSelector.GetOptions(new MockConnectionContext(), "www.example.org"); - var alpnList = options.Item1.ApplicationProtocols; + var (options, _) = sniOptionsSelector.GetOptions(new MockConnectionContext(), "www.example.org"); + var alpnList = options.ApplicationProtocols; Assert.NotNull(alpnList); var protocol = Assert.Single(alpnList); @@ -549,8 +549,8 @@ public void PrefersSslProtocolsDefinedInSniConfig() fallbackHttpProtocols: HttpProtocols.Http1AndHttp2, logger: Mock.Of>()); - var options = sniOptionsSelector.GetOptions(new MockConnectionContext(), "www.example.org"); - Assert.Equal(SslProtocols.Tls13 | SslProtocols.Tls11, options.Item1.EnabledSslProtocols); + var (options, _) = sniOptionsSelector.GetOptions(new MockConnectionContext(), "www.example.org"); + Assert.Equal(SslProtocols.Tls13 | SslProtocols.Tls11, options.EnabledSslProtocols); } [Fact] diff --git a/src/Servers/Kestrel/test/Interop.FunctionalTests/HttpClientHttp2InteropTests.cs b/src/Servers/Kestrel/test/Interop.FunctionalTests/HttpClientHttp2InteropTests.cs index cd762cba334b..a0c4a69e3e1a 100644 --- a/src/Servers/Kestrel/test/Interop.FunctionalTests/HttpClientHttp2InteropTests.cs +++ b/src/Servers/Kestrel/test/Interop.FunctionalTests/HttpClientHttp2InteropTests.cs @@ -15,6 +15,7 @@ using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http.Features; using Microsoft.AspNetCore.Server.Kestrel.Core; +using Microsoft.AspNetCore.Server.Kestrel.Https; using Microsoft.AspNetCore.Testing; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging.Testing; @@ -1591,6 +1592,94 @@ public async Task UrlEncoding(string scheme) await host.StopAsync().DefaultTimeout(); } + [ConditionalFact] + [OSSkipCondition(OperatingSystems.Linux | OperatingSystems.MacOSX, SkipReason = "Not supported yet")] + public async Task ClientCertificate_Required() + { + var hostBuilder = new HostBuilder() + .ConfigureWebHost(webHostBuilder => + { + webHostBuilder.UseKestrel(options => + { + options.Listen(IPAddress.Loopback, 0, listenOptions => + { + listenOptions.Protocols = HttpProtocols.Http2; + listenOptions.UseHttps(httpsOptions => + { + httpsOptions.ServerCertificate = TestResources.GetTestCertificate(); + httpsOptions.ClientCertificateMode = ClientCertificateMode.RequireCertificate; + httpsOptions.AllowAnyClientCertificate(); + }); + }); + }); + webHostBuilder.ConfigureServices(AddTestLogging) + .Configure(app => app.Run(async context => + { + Assert.NotNull(context.Connection.ClientCertificate); + Assert.NotNull(await context.Connection.GetClientCertificateAsync()); + await context.Response.WriteAsync("Hello World"); + })); + }); + using var host = await hostBuilder.StartAsync().DefaultTimeout(); + + var handler = new SocketsHttpHandler(); + handler.SslOptions.RemoteCertificateValidationCallback = (_, _, _, _) => true; + handler.SslOptions.LocalCertificateSelectionCallback = (_, _, _, _, _) => TestResources.GetTestCertificate(); + using var client = new HttpClient(handler); + client.DefaultRequestVersion = HttpVersion.Version20; + client.DefaultVersionPolicy = HttpVersionPolicy.RequestVersionExact; + var url = host.MakeUrl(Uri.UriSchemeHttps); + var response = await client.GetAsync(url).DefaultTimeout(); + + Assert.Equal(HttpVersion.Version20, response.Version); + Assert.Equal("Hello World", await response.Content.ReadAsStringAsync()); + await host.StopAsync().DefaultTimeout(); + } + + [ConditionalFact] + [OSSkipCondition(OperatingSystems.Linux | OperatingSystems.MacOSX, SkipReason = "Not supported yet")] + public async Task ClientCertificate_DelayedNotSupported() + { + var hostBuilder = new HostBuilder() + .ConfigureWebHost(webHostBuilder => + { + webHostBuilder.UseKestrel(options => + { + options.Listen(IPAddress.Loopback, 0, listenOptions => + { + listenOptions.Protocols = HttpProtocols.Http2; + listenOptions.UseHttps(httpsOptions => + { + httpsOptions.ServerCertificate = TestResources.GetTestCertificate(); + httpsOptions.ClientCertificateMode = ClientCertificateMode.DelayCertificate; + httpsOptions.AllowAnyClientCertificate(); + }); + }); + }); + webHostBuilder.ConfigureServices(AddTestLogging) + .Configure(app => app.Run(async context => + { + Assert.Null(context.Connection.ClientCertificate); + Assert.Null(await context.Connection.GetClientCertificateAsync()); + await context.Response.WriteAsync("Hello World"); + })); + }); + using var host = await hostBuilder.StartAsync().DefaultTimeout(); + + var handler = new SocketsHttpHandler(); + handler.SslOptions.RemoteCertificateValidationCallback = (_, _, _, _) => true; + handler.SslOptions.LocalCertificateSelectionCallback = (_, _, _, _, _) => TestResources.GetTestCertificate(); + using var client = new HttpClient(handler); + client.DefaultRequestVersion = HttpVersion.Version20; + client.DefaultVersionPolicy = HttpVersionPolicy.RequestVersionExact; + var url = host.MakeUrl(Uri.UriSchemeHttps); + var response = await client.GetAsync(url).DefaultTimeout(); + + Assert.Equal(HttpVersion.Version20, response.Version); + Assert.Equal("Hello World", await response.Content.ReadAsStringAsync()); + await host.StopAsync().DefaultTimeout(); + } + private static HttpClient CreateClient() { var handler = new HttpClientHandler();