Skip to content

Commit c28dc4c

Browse files
authored
Support client cert negotation (#33264)
* Support client cert negotation #23948 * Add UseHttps overload * Update http2 test * Clean up tests * POST tests * Use unique hosts * Sample (needs debugging) * Fix sample * Revert UseHttps overload * Cache tasks * ServerOptionsSelectionCallback test * Consolidate tests * Clean up sample * Cleanup * Fail off for SNI config * Make DelayCert work with SNI from config * Sni config tests * PR feedback * Http2 negative test
1 parent 3e3f51a commit c28dc4c

File tree

11 files changed

+534
-53
lines changed

11 files changed

+534
-53
lines changed

src/Servers/Kestrel/Core/src/ClientCertificateMode.cs

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,12 @@ public enum ClientCertificateMode
2121
/// <summary>
2222
/// A client certificate will be requested, and the client must provide a valid certificate for authentication to succeed.
2323
/// </summary>
24-
RequireCertificate
24+
RequireCertificate,
25+
26+
/// <summary>
27+
/// A client certificate is not required and will not be requested from clients at the start of the connection.
28+
/// It may be requested by the application later.
29+
/// </summary>
30+
DelayCertificate,
2531
}
2632
}

src/Servers/Kestrel/Core/src/Internal/SniOptionsSelector.cs

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,8 @@ public SniOptionsSelector(
8484

8585
if (clientCertificateMode != ClientCertificateMode.NoCertificate)
8686
{
87-
sslOptions.ClientCertificateRequired = true;
87+
sslOptions.ClientCertificateRequired = clientCertificateMode == ClientCertificateMode.AllowCertificate
88+
|| clientCertificateMode == ClientCertificateMode.RequireCertificate;
8889
sslOptions.RemoteCertificateValidationCallback = (sender, certificate, chain, sslPolicyErrors) =>
8990
HttpsConnectionMiddleware.RemoteCertificateValidationCallback(
9091
clientCertificateMode, fallbackHttpsOptions.ClientCertificateValidation, certificate, chain, sslPolicyErrors);
@@ -94,7 +95,7 @@ public SniOptionsSelector(
9495
httpProtocols = HttpsConnectionMiddleware.ValidateAndNormalizeHttpProtocols(httpProtocols, logger);
9596
HttpsConnectionMiddleware.ConfigureAlpn(sslOptions, httpProtocols);
9697

97-
var sniOptions = new SniOptions(sslOptions, httpProtocols);
98+
var sniOptions = new SniOptions(sslOptions, httpProtocols, clientCertificateMode);
9899

99100
if (name.Equals(WildcardHost, StringComparison.Ordinal))
100101
{
@@ -112,7 +113,7 @@ public SniOptionsSelector(
112113
}
113114
}
114115

115-
public SslServerAuthenticationOptions GetOptions(ConnectionContext connection, string serverName)
116+
public (SslServerAuthenticationOptions, ClientCertificateMode) GetOptions(ConnectionContext connection, string serverName)
116117
{
117118
SniOptions? sniOptions = null;
118119

@@ -172,14 +173,14 @@ public SslServerAuthenticationOptions GetOptions(ConnectionContext connection, s
172173
_onAuthenticateCallback(connection, sslOptions);
173174
}
174175

175-
return sslOptions;
176+
return (sslOptions, sniOptions.ClientCertificateMode);
176177
}
177178

178-
public static ValueTask<SslServerAuthenticationOptions> OptionsCallback(ConnectionContext connection, SslStream stream, SslClientHelloInfo clientHelloInfo, object state, CancellationToken cancellationToken)
179+
public static ValueTask<(SslServerAuthenticationOptions, ClientCertificateMode)> OptionsCallback(ConnectionContext connection, SslStream stream, SslClientHelloInfo clientHelloInfo, object state, CancellationToken cancellationToken)
179180
{
180181
var sniOptionsSelector = (SniOptionsSelector)state;
181-
var options = sniOptionsSelector.GetOptions(connection, clientHelloInfo.ServerName);
182-
return new ValueTask<SslServerAuthenticationOptions>(options);
182+
var (options, clientCertificateMode) = sniOptionsSelector.GetOptions(connection, clientHelloInfo.ServerName);
183+
return new ValueTask<(SslServerAuthenticationOptions, ClientCertificateMode)>((options, clientCertificateMode));
183184
}
184185

185186
internal static SslServerAuthenticationOptions CloneSslOptions(SslServerAuthenticationOptions sslOptions) =>
@@ -200,14 +201,16 @@ internal static SslServerAuthenticationOptions CloneSslOptions(SslServerAuthenti
200201

201202
private class SniOptions
202203
{
203-
public SniOptions(SslServerAuthenticationOptions sslOptions, HttpProtocols httpProtocols)
204+
public SniOptions(SslServerAuthenticationOptions sslOptions, HttpProtocols httpProtocols, ClientCertificateMode clientCertificateMode)
204205
{
205206
SslOptions = sslOptions;
206207
HttpProtocols = httpProtocols;
208+
ClientCertificateMode = clientCertificateMode;
207209
}
208210

209211
public SslServerAuthenticationOptions SslOptions { get; }
210212
public HttpProtocols HttpProtocols { get; }
213+
public ClientCertificateMode ClientCertificateMode { get; }
211214
}
212215

213216
private class LongestStringFirstComparer : IComparer<string>

src/Servers/Kestrel/Core/src/Internal/TlsConnectionFeature.cs

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
using Microsoft.AspNetCore.Connections.Features;
1111
using Microsoft.AspNetCore.Http.Features;
1212
using Microsoft.AspNetCore.Server.Kestrel.Core.Features;
13+
using Microsoft.AspNetCore.Server.Kestrel.Https;
1314

1415
namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal
1516
{
@@ -25,6 +26,7 @@ internal class TlsConnectionFeature : ITlsConnectionFeature, ITlsApplicationProt
2526
private int? _hashStrength;
2627
private ExchangeAlgorithmType? _keyExchangeAlgorithm;
2728
private int? _keyExchangeStrength;
29+
private Task<X509Certificate2?>? _clientCertTask;
2830

2931
public TlsConnectionFeature(SslStream sslStream)
3032
{
@@ -36,6 +38,8 @@ public TlsConnectionFeature(SslStream sslStream)
3638
_sslStream = sslStream;
3739
}
3840

41+
internal ClientCertificateMode ClientCertificateMode { get; set; }
42+
3943
public X509Certificate2? ClientCertificate
4044
{
4145
get
@@ -99,7 +103,27 @@ public int KeyExchangeStrength
99103

100104
public Task<X509Certificate2?> GetClientCertificateAsync(CancellationToken cancellationToken)
101105
{
102-
return Task.FromResult(ClientCertificate);
106+
// Only try once per connection
107+
if (_clientCertTask != null)
108+
{
109+
return _clientCertTask;
110+
}
111+
112+
if (ClientCertificate != null
113+
|| ClientCertificateMode != ClientCertificateMode.DelayCertificate
114+
// Delayed client cert negotiation is not allowed on HTTP/2 (or HTTP/3, but that's implemented elsewhere).
115+
|| _sslStream.NegotiatedApplicationProtocol == SslApplicationProtocol.Http2)
116+
{
117+
return _clientCertTask = Task.FromResult(ClientCertificate);
118+
}
119+
120+
return _clientCertTask = GetClientCertificateAsyncCore(cancellationToken);
121+
}
122+
123+
private async Task<X509Certificate2?> GetClientCertificateAsyncCore(CancellationToken cancellationToken)
124+
{
125+
await _sslStream.NegotiateClientCertificateAsync(cancellationToken);
126+
return ClientCertificate;
103127
}
104128

105129
private static X509Certificate2? ConvertToX509Certificate2(X509Certificate? certificate)

src/Servers/Kestrel/Core/src/ListenOptionsHttpsExtensions.cs

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,9 @@
55
using System.IO;
66
using System.Net.Security;
77
using System.Security.Cryptography.X509Certificates;
8+
using System.Threading;
9+
using System.Threading.Tasks;
10+
using Microsoft.AspNetCore.Connections;
811
using Microsoft.AspNetCore.Server.Kestrel.Core;
912
using Microsoft.AspNetCore.Server.Kestrel.Https;
1013
using Microsoft.AspNetCore.Server.Kestrel.Https.Internal;
@@ -256,12 +259,16 @@ public static ListenOptions UseHttps(this ListenOptions listenOptions, ServerOpt
256259
/// <returns>The <see cref="ListenOptions"/>.</returns>
257260
public static ListenOptions UseHttps(this ListenOptions listenOptions, ServerOptionsSelectionCallback serverOptionsSelectionCallback, object state, TimeSpan handshakeTimeout)
258261
{
259-
// HttpsOptionsCallback is an internal delegate that is just the ServerOptionsSelectionCallback + a ConnectionContext parameter.
262+
// HttpsOptionsCallback is an internal delegate that is the ServerOptionsSelectionCallback, a ConnectionContext, and the ClientCertificateMode.
260263
// Given that ConnectionContext will eventually be replaced by System.Net.Connections, it doesn't make much sense to make the HttpsOptionsCallback delegate public.
261-
HttpsOptionsCallback adaptedCallback = (connection, stream, clientHelloInfo, state, cancellationToken) =>
262-
serverOptionsSelectionCallback(stream, clientHelloInfo, state, cancellationToken);
264+
return listenOptions.UseHttps(GetTlsOptionsAsync, state, handshakeTimeout);
263265

264-
return listenOptions.UseHttps(adaptedCallback, state, handshakeTimeout);
266+
async ValueTask<(SslServerAuthenticationOptions, ClientCertificateMode)> GetTlsOptionsAsync(
267+
ConnectionContext connection, SslStream stream, SslClientHelloInfo clientHelloInfo, object state, CancellationToken cancellationToken)
268+
{
269+
var tlsOptions = await serverOptionsSelectionCallback(stream, clientHelloInfo, state, cancellationToken);
270+
return new (tlsOptions, ClientCertificateMode.DelayCertificate);
271+
}
265272
}
266273

267274
/// <summary>

src/Servers/Kestrel/Core/src/Middleware/HttpsConnectionMiddleware.cs

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@
2626

2727
namespace Microsoft.AspNetCore.Server.Kestrel.Https.Internal
2828
{
29-
internal delegate ValueTask<SslServerAuthenticationOptions> HttpsOptionsCallback(ConnectionContext connection, SslStream stream, SslClientHelloInfo clientHelloInfo, object state, CancellationToken cancellationToken);
29+
internal delegate ValueTask<(SslServerAuthenticationOptions, ClientCertificateMode)> HttpsOptionsCallback(ConnectionContext connection, SslStream stream, SslClientHelloInfo clientHelloInfo, object state, CancellationToken cancellationToken);
3030

3131
internal class HttpsConnectionMiddleware
3232
{
@@ -148,6 +148,8 @@ public async Task OnConnectionAsync(ConnectionContext context)
148148
var sslStream = sslDuplexPipe.Stream;
149149

150150
var feature = new Core.Internal.TlsConnectionFeature(sslStream);
151+
// Set the mode if options were used. If the callback is used it will set the mode later.
152+
feature.ClientCertificateMode = _options?.ClientCertificateMode ?? ClientCertificateMode.NoCertificate;
151153
context.Features.Set<ITlsConnectionFeature>(feature);
152154
context.Features.Set<ITlsHandshakeFeature>(feature);
153155
context.Features.Set<ITlsApplicationProtocolFeature>(feature);
@@ -321,7 +323,8 @@ private Task DoOptionsBasedHandshakeAsync(ConnectionContext context, SslStream s
321323
ServerCertificate = _serverCertificate,
322324
ServerCertificateContext = _serverCertificateContext,
323325
ServerCertificateSelectionCallback = selector,
324-
ClientCertificateRequired = _options.ClientCertificateMode != ClientCertificateMode.NoCertificate,
326+
ClientCertificateRequired = _options.ClientCertificateMode == ClientCertificateMode.AllowCertificate
327+
|| _options.ClientCertificateMode == ClientCertificateMode.RequireCertificate,
325328
EnabledSslProtocols = _options.SslProtocols,
326329
CertificateRevocationCheckMode = _options.CheckCertificateRevocation ? X509RevocationMode.Online : X509RevocationMode.NoCheck,
327330
};
@@ -424,7 +427,8 @@ private static async ValueTask<SslServerAuthenticationOptions> ServerOptionsCall
424427
feature.HostName = clientHelloInfo.ServerName;
425428
context.Features.Set(sslStream);
426429

427-
var sslOptions = await middleware._httpsOptionsCallback!(context, sslStream, clientHelloInfo, middleware._httpsOptionsCallbackState!, cancellationToken);
430+
var (sslOptions, clientCertificateMode) = await middleware._httpsOptionsCallback!(context, sslStream, clientHelloInfo, middleware._httpsOptionsCallbackState!, cancellationToken);
431+
feature.ClientCertificateMode = clientCertificateMode;
428432

429433
KestrelEventSource.Log.TlsHandshakeStart(context, sslOptions);
430434

src/Servers/Kestrel/Core/src/PublicAPI.Unshipped.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,7 @@
9595
*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
9696
*REMOVED*~static Microsoft.AspNetCore.Hosting.ListenOptionsHttpsExtensions.UseHttps(this Microsoft.AspNetCore.Server.Kestrel.Core.ListenOptions listenOptions, string fileName, string password, System.Action<Microsoft.AspNetCore.Server.Kestrel.Https.HttpsConnectionAdapterOptions> configureOptions) -> Microsoft.AspNetCore.Server.Kestrel.Core.ListenOptions
9797
*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
98+
Microsoft.AspNetCore.Server.Kestrel.Https.ClientCertificateMode.DelayCertificate = 3 -> Microsoft.AspNetCore.Server.Kestrel.Https.ClientCertificateMode
9899
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!
99100
static Microsoft.AspNetCore.Hosting.ListenOptionsHttpsExtensions.UseHttps(this Microsoft.AspNetCore.Server.Kestrel.Core.ListenOptions! listenOptions, System.Action<Microsoft.AspNetCore.Server.Kestrel.Https.HttpsConnectionAdapterOptions!>! configureOptions) -> Microsoft.AspNetCore.Server.Kestrel.Core.ListenOptions!
100101
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!

0 commit comments

Comments
 (0)