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/SniOptionsSelector.cs b/src/Servers/Kestrel/Core/src/Internal/SniOptionsSelector.cs
index b12923981084..3f78bb13b208 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);
@@ -94,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))
{
@@ -112,7 +113,7 @@ public SniOptionsSelector(
}
}
- public SslServerAuthenticationOptions GetOptions(ConnectionContext connection, string serverName)
+ public (SslServerAuthenticationOptions, ClientCertificateMode) GetOptions(ConnectionContext connection, string serverName)
{
SniOptions? sniOptions = null;
@@ -172,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) =>
@@ -200,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 e5631645790d..869b6c98dc1e 100644
--- a/src/Servers/Kestrel/Core/src/Internal/TlsConnectionFeature.cs
+++ b/src/Servers/Kestrel/Core/src/Internal/TlsConnectionFeature.cs
@@ -10,6 +10,7 @@
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
{
@@ -25,6 +26,7 @@ internal class TlsConnectionFeature : ITlsConnectionFeature, ITlsApplicationProt
private int? _hashStrength;
private ExchangeAlgorithmType? _keyExchangeAlgorithm;
private int? _keyExchangeStrength;
+ private Task? _clientCertTask;
public TlsConnectionFeature(SslStream sslStream)
{
@@ -36,6 +38,8 @@ public TlsConnectionFeature(SslStream sslStream)
_sslStream = sslStream;
}
+ internal ClientCertificateMode ClientCertificateMode { get; set; }
+
public X509Certificate2? ClientCertificate
{
get
@@ -99,7 +103,27 @@ public int KeyExchangeStrength
public Task GetClientCertificateAsync(CancellationToken cancellationToken)
{
- return Task.FromResult(ClientCertificate);
+ // Only try once per connection
+ if (_clientCertTask != null)
+ {
+ return _clientCertTask;
+ }
+
+ if (ClientCertificate != null
+ || 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)
+ {
+ return _clientCertTask = Task.FromResult(ClientCertificate);
+ }
+
+ return _clientCertTask = GetClientCertificateAsyncCore(cancellationToken);
+ }
+
+ 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 ff2b007e66a4..1aeb0fd460b0 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;
@@ -256,12 +259,16 @@ 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.
- HttpsOptionsCallback adaptedCallback = (connection, stream, clientHelloInfo, state, cancellationToken) =>
- serverOptionsSelectionCallback(stream, clientHelloInfo, state, cancellationToken);
+ return listenOptions.UseHttps(GetTlsOptionsAsync, state, handshakeTimeout);
- return listenOptions.UseHttps(adaptedCallback, state, handshakeTimeout);
+ 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);
+ }
}
///
diff --git a/src/Servers/Kestrel/Core/src/Middleware/HttpsConnectionMiddleware.cs b/src/Servers/Kestrel/Core/src/Middleware/HttpsConnectionMiddleware.cs
index c46aff747b7f..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
{
@@ -148,6 +148,8 @@ public async Task OnConnectionAsync(ConnectionContext context)
var sslStream = sslDuplexPipe.Stream;
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);
@@ -321,7 +323,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,
};
@@ -424,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/src/PublicAPI.Unshipped.txt b/src/Servers/Kestrel/Core/src/PublicAPI.Unshipped.txt
index cf79ecc855dd..8495f35e086c 100644
--- a/src/Servers/Kestrel/Core/src/PublicAPI.Unshipped.txt
+++ b/src/Servers/Kestrel/Core/src/PublicAPI.Unshipped.txt
@@ -95,6 +95,7 @@
*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!
diff --git a/src/Servers/Kestrel/Core/test/SniOptionsSelectorTests.cs b/src/Servers/Kestrel/Core/test/SniOptionsSelectorTests.cs
index bbe148069d99..c4d20185d92a 100644
--- a/src/Servers/Kestrel/Core/test/SniOptionsSelectorTests.cs
+++ b/src/Servers/Kestrel/Core/test/SniOptionsSelectorTests.cs
@@ -73,22 +73,22 @@ public void PrefersExactMatchOverWildcardPrefixOverWildcardOnly()
fallbackHttpProtocols: HttpProtocols.Http1AndHttp2,
logger: Mock.Of>());
- var wwwSubdomainOptions = sniOptionsSelector.GetOptions(new MockConnectionContext(), "www.example.org");
+ 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");
+ 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");
+ 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");
+ var (noSubdomainOptions, _) = sniOptionsSelector.GetOptions(new MockConnectionContext(), "example.org");
Assert.Equal("WildcardOnly", pathDictionary[noSubdomainOptions.ServerCertificate]);
- var anotherTldOptions = sniOptionsSelector.GetOptions(new MockConnectionContext(), "dot.net");
+ var (anotherTldOptions, _) = sniOptionsSelector.GetOptions(new MockConnectionContext(), "dot.net");
Assert.Equal("WildcardOnly", pathDictionary[anotherTldOptions.ServerCertificate]);
}
@@ -130,11 +130,11 @@ public void PerfersLongerWildcardPrefixOverShorterWildcardPrefix()
fallbackHttpProtocols: HttpProtocols.Http1AndHttp2,
logger: Mock.Of>());
- var baSubdomainOptions = sniOptionsSelector.GetOptions(new MockConnectionContext(), "b.a.example.org");
+ 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");
+ var (aSubdomainOptions, _) = sniOptionsSelector.GetOptions(new MockConnectionContext(), "a.example.org");
Assert.Equal("Short", pathDictionary[aSubdomainOptions.ServerCertificate]);
}
@@ -176,13 +176,13 @@ public void ServerNameMatchingIsCaseInsensitive()
fallbackHttpProtocols: HttpProtocols.Http1AndHttp2,
logger: Mock.Of>());
- var wwwSubdomainOptions = sniOptionsSelector.GetOptions(new MockConnectionContext(), "wWw.eXample.oRg");
+ 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");
+ 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");
+ var (aSubdomainOptions, _) = sniOptionsSelector.GetOptions(new MockConnectionContext(), "A.eXample.oRg");
Assert.Equal("WildcardPrefix", pathDictionary[aSubdomainOptions.ServerCertificate]);
}
@@ -232,7 +232,7 @@ public void WildcardOnlyMatchesNullServerNameDueToNoAlpn()
fallbackHttpProtocols: HttpProtocols.Http1AndHttp2,
logger: Mock.Of>());
- var options = sniOptionsSelector.GetOptions(new MockConnectionContext(), null);
+ var (options, _) = sniOptionsSelector.GetOptions(new MockConnectionContext(), null);
Assert.Equal("WildcardOnly", pathDictionary[options.ServerCertificate]);
}
@@ -258,8 +258,8 @@ 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");
+ var (options1, _) = sniOptionsSelector.GetOptions(new MockConnectionContext(), "www.example.org");
+ var (options2, _) = sniOptionsSelector.GetOptions(new MockConnectionContext(), "www.example.org");
Assert.Same(options1, options2);
}
@@ -295,10 +295,10 @@ public void ClonesSslServerAuthenticationOptionsIfAnOnAuthenticateCallbackIsDefi
fallbackHttpProtocols: HttpProtocols.Http1AndHttp2,
logger: Mock.Of>());
- var options1 = sniOptionsSelector.GetOptions(new MockConnectionContext(), "www.example.org");
+ var (options1, _) = sniOptionsSelector.GetOptions(new MockConnectionContext(), "www.example.org");
Assert.Same(lastSeenSslOptions, options1);
- var options2 = sniOptionsSelector.GetOptions(new MockConnectionContext(), "www.example.org");
+ var (options2, _) = sniOptionsSelector.GetOptions(new MockConnectionContext(), "www.example.org");
Assert.Same(lastSeenSslOptions, options2);
Assert.NotSame(options1, options2);
@@ -338,19 +338,19 @@ public void ClonesSslServerAuthenticationOptionsIfTheFallbackServerCertificateSe
fallbackHttpProtocols: HttpProtocols.Http1AndHttp2,
logger: Mock.Of>());
- var selectorOptions1 = sniOptionsSelector.GetOptions(new MockConnectionContext(), "selector.example.org");
+ var (selectorOptions1, _) = sniOptionsSelector.GetOptions(new MockConnectionContext(), "selector.example.org");
Assert.Same(selectorCertificate, selectorOptions1.ServerCertificate);
- var selectorOptions2 = sniOptionsSelector.GetOptions(new MockConnectionContext(), "selector.example.org");
+ 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, selectorOptions2);
- var configOptions1 = sniOptionsSelector.GetOptions(new MockConnectionContext(), "config.example.org");
+ var (configOptions1, _) = sniOptionsSelector.GetOptions(new MockConnectionContext(), "config.example.org");
Assert.NotSame(selectorCertificate, configOptions1.ServerCertificate);
- var configOptions2 = sniOptionsSelector.GetOptions(new MockConnectionContext(), "config.example.org");
+ 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.
@@ -397,7 +397,7 @@ public void FallsBackToHttpsConnectionAdapterCertificate()
fallbackHttpProtocols: HttpProtocols.Http1AndHttp2,
logger: Mock.Of>());
- var options = sniOptionsSelector.GetOptions(new MockConnectionContext(), "www.example.org");
+ var (options, _) = sniOptionsSelector.GetOptions(new MockConnectionContext(), "www.example.org");
Assert.Same(fallbackOptions.ServerCertificate, options.ServerCertificate);
}
@@ -425,7 +425,7 @@ public void FallsBackToHttpsConnectionAdapterServerCertificateSelectorOverServer
fallbackHttpProtocols: HttpProtocols.Http1AndHttp2,
logger: Mock.Of>());
- var options = sniOptionsSelector.GetOptions(new MockConnectionContext(), "www.example.org");
+ var (options, _) = sniOptionsSelector.GetOptions(new MockConnectionContext(), "www.example.org");
Assert.Same(selectorCertificate, options.ServerCertificate);
}
@@ -485,7 +485,7 @@ public void ConfiguresAlpnBasedOnConfiguredHttpProtocols()
fallbackHttpProtocols: HttpProtocols.None,
logger: Mock.Of>());
- var options = sniOptionsSelector.GetOptions(new MockConnectionContext(), "www.example.org");
+ var (options, _) = sniOptionsSelector.GetOptions(new MockConnectionContext(), "www.example.org");
var alpnList = options.ApplicationProtocols;
Assert.NotNull(alpnList);
@@ -549,7 +549,7 @@ public void PrefersSslProtocolsDefinedInSniConfig()
fallbackHttpProtocols: HttpProtocols.Http1AndHttp2,
logger: Mock.Of>());
- var options = sniOptionsSelector.GetOptions(new MockConnectionContext(), "www.example.org");
+ var (options, _) = sniOptionsSelector.GetOptions(new MockConnectionContext(), "www.example.org");
Assert.Equal(SslProtocols.Tls13 | SslProtocols.Tls11, options.EnabledSslProtocols);
}
@@ -578,7 +578,7 @@ public void FallsBackToFallbackSslProtocols()
fallbackHttpProtocols: HttpProtocols.Http1AndHttp2,
logger: Mock.Of>());
- var options = sniOptionsSelector.GetOptions(new MockConnectionContext(), "www.example.org");
+ var (options, _) = sniOptionsSelector.GetOptions(new MockConnectionContext(), "www.example.org");
Assert.Equal(SslProtocols.Tls13, options.EnabledSslProtocols);
}
@@ -592,7 +592,7 @@ public void PrefersClientCertificateModeDefinedInSniConfig()
"www.example.org",
new SniConfig
{
- ClientCertificateMode = ClientCertificateMode.RequireCertificate,
+ ClientCertificateMode = ClientCertificateMode.DelayCertificate,
Certificate = new CertificateConfig()
}
}
@@ -609,13 +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.True(options.ClientCertificateRequired);
+ Assert.Equal(ClientCertificateMode.DelayCertificate, certMode);
+ Assert.False(options.ClientCertificateRequired);
Assert.NotNull(options.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));
+ // The RemoteCertificateValidationCallback should first check if the certificate is null and return true since it's optional.
+ Assert.True(options.RemoteCertificateValidationCallback(sender: null, certificate: null, chain: null, SslPolicyErrors.None));
}
[Fact]
@@ -643,8 +644,9 @@ 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, certMode);
// Despite the confusing name, ClientCertificateRequired being true simply requests a certificate from the client, but doesn't require it.
Assert.True(options.ClientCertificateRequired);
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 dabd15ccf9e7..2b489a1a4d7c 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,8 @@ public void Configure(IApplicationBuilder app, ILoggerFactory loggerFactory)
{
var logger = loggerFactory.CreateLogger("Default");
+ app.UseClientCertBuffering();
+
// Add an exception handler that prevents throwing due to large request body size
app.Use(async (context, next) =>
{
@@ -43,12 +46,16 @@ public void Configure(IApplicationBuilder app, ILoggerFactory loggerFactory)
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;
@@ -80,6 +87,7 @@ public static Task Main(string[] args)
options.ConfigureHttpsDefaults(httpsOptions =>
{
httpsOptions.SslProtocols = SslProtocols.Tls12;
+ httpsOptions.ClientCertificateMode = ClientCertificateMode.DelayCertificate;
});
options.Listen(IPAddress.Loopback, basePort, listenOptions =>
@@ -92,6 +100,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();
});
diff --git a/src/Servers/Kestrel/test/InMemory.FunctionalTests/HttpsConnectionMiddlewareTests.cs b/src/Servers/Kestrel/test/InMemory.FunctionalTests/HttpsConnectionMiddlewareTests.cs
index 61fdb0586b0f..62d0beecc511 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;
@@ -69,7 +70,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>();
@@ -480,13 +481,270 @@ 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("localhost");
+ await stream.AuthenticateAsClientAsync(Guid.NewGuid().ToString());
await AssertConnectionResult(stream, true);
}
}
}
+ [Fact]
+ public async Task RenegotiateForClientCertificateOnHttp1DisabledByDefault()
+ {
+ 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.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.
+ // 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);
+ }
+
+ [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 CanRenegotiateForClientCertificate(HttpProtocols httpProtocols)
+ {
+ void ConfigureListenOptions(ListenOptions listenOptions)
+ {
+ listenOptions.Protocols = httpProtocols;
+ 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.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.
+ // 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);
+ }
+
+ [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.
+ // 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);
+ }
+
+ [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.
+ // Use a random host name to avoid the TLS session resumption cache.
+ var stream = new SslStream(connection.Stream);
+ var clientOptions = new SslClientAuthenticationOptions()
+ {
+ TargetHost = Guid.NewGuid().ToString(),
+ 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.")]
+ 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.
+ // 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);
+ }
+
+ [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.
+ // 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);
+ }
+
[Fact]
public async Task HttpsSchemePassedToRequestFeature()
{
@@ -854,9 +1112,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;
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();