Skip to content

Commit 9b25dae

Browse files
committed
New UseHttps overload with per connection callback #33452
1 parent 5577c0d commit 9b25dae

10 files changed

+223
-47
lines changed

src/Servers/Kestrel/Core/src/HttpsConnectionAdapterOptions.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -93,7 +93,8 @@ public void AllowAnyClientCertificate()
9393
public Action<ConnectionContext, SslServerAuthenticationOptions>? OnAuthenticate { get; set; }
9494

9595
/// <summary>
96-
/// Specifies the maximum amount of time allowed for the TLS/SSL handshake. This must be positive and finite. Defaults to 10 seconds.
96+
/// Specifies the maximum amount of time allowed for the TLS/SSL handshake. This must be positive
97+
/// or <see cref="Timeout.InfiniteTimeSpan"/>. Defaults to 10 seconds.
9798
/// </summary>
9899
public TimeSpan HandshakeTimeout
99100
{

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

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -176,11 +176,12 @@ public SniOptionsSelector(
176176
return (sslOptions, sniOptions.ClientCertificateMode);
177177
}
178178

179-
public static ValueTask<(SslServerAuthenticationOptions, ClientCertificateMode)> OptionsCallback(ConnectionContext connection, SslStream stream, SslClientHelloInfo clientHelloInfo, object state, CancellationToken cancellationToken)
179+
public static ValueTask<SslServerAuthenticationOptions> OptionsCallback(TlsHandshakeCallbackContext callbackContext)
180180
{
181-
var sniOptionsSelector = (SniOptionsSelector)state;
182-
var (options, clientCertificateMode) = sniOptionsSelector.GetOptions(connection, clientHelloInfo.ServerName);
183-
return new ValueTask<(SslServerAuthenticationOptions, ClientCertificateMode)>((options, clientCertificateMode));
181+
var sniOptionsSelector = (SniOptionsSelector)callbackContext.State!;
182+
var (options, clientCertificateMode) = sniOptionsSelector.GetOptions(callbackContext.Connection, callbackContext.ClientHelloInfo.ServerName);
183+
callbackContext.AllowDelayedClientCertificateNegotation = clientCertificateMode == ClientCertificateMode.DelayCertificate;
184+
return new ValueTask<SslServerAuthenticationOptions>(options);
184185
}
185186

186187
internal static SslServerAuthenticationOptions CloneSslOptions(SslServerAuthenticationOptions sslOptions) =>

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ public TlsConnectionFeature(SslStream sslStream)
3838
_sslStream = sslStream;
3939
}
4040

41-
internal ClientCertificateMode ClientCertificateMode { get; set; }
41+
internal bool AllowDelayedClientCertificateNegotation { get; set; }
4242

4343
public X509Certificate2? ClientCertificate
4444
{
@@ -114,7 +114,7 @@ public int KeyExchangeStrength
114114
}
115115

116116
if (ClientCertificate != null
117-
|| ClientCertificateMode != ClientCertificateMode.DelayCertificate
117+
|| !AllowDelayedClientCertificateNegotation
118118
// Delayed client cert negotiation is not allowed on HTTP/2 (or HTTP/3, but that's implemented elsewhere).
119119
|| _sslStream.NegotiatedApplicationProtocol == SslApplicationProtocol.Http2)
120120
{

src/Servers/Kestrel/Core/src/KestrelConfigurationLoader.cs

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -400,8 +400,16 @@ public void Load()
400400
}
401401
else
402402
{
403-
var sniOptionsSelector = new SniOptionsSelector(endpoint.Name, endpoint.Sni, CertificateConfigLoader, httpsOptions, listenOptions.Protocols, HttpsLogger);
404-
listenOptions.UseHttps(SniOptionsSelector.OptionsCallback, sniOptionsSelector, httpsOptions.HandshakeTimeout);
403+
var sniOptionsSelector = new SniOptionsSelector(endpoint.Name, endpoint.Sni, CertificateConfigLoader,
404+
httpsOptions, listenOptions.Protocols, HttpsLogger);
405+
var tlsCallbackOptions = new TlsHandshakeCallbackOptions()
406+
{
407+
OnConnection = SniOptionsSelector.OptionsCallback,
408+
HandshakeTimeout = httpsOptions.HandshakeTimeout,
409+
OnConnectionState = sniOptionsSelector,
410+
};
411+
412+
listenOptions.UseHttps(tlsCallbackOptions);
405413
}
406414
}
407415

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

Lines changed: 20 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,6 @@
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;
118
using Microsoft.AspNetCore.Server.Kestrel.Core;
129
using Microsoft.AspNetCore.Server.Kestrel.Https;
1310
using Microsoft.AspNetCore.Server.Kestrel.Https.Internal;
@@ -259,34 +256,39 @@ public static ListenOptions UseHttps(this ListenOptions listenOptions, ServerOpt
259256
/// <returns>The <see cref="ListenOptions"/>.</returns>
260257
public static ListenOptions UseHttps(this ListenOptions listenOptions, ServerOptionsSelectionCallback serverOptionsSelectionCallback, object state, TimeSpan handshakeTimeout)
261258
{
262-
// HttpsOptionsCallback is an internal delegate that is the ServerOptionsSelectionCallback, a ConnectionContext, and the ClientCertificateMode.
263-
// Given that ConnectionContext will eventually be replaced by System.Net.Connections, it doesn't make much sense to make the HttpsOptionsCallback delegate public.
264-
return listenOptions.UseHttps(GetTlsOptionsAsync, state, handshakeTimeout);
265-
266-
async ValueTask<(SslServerAuthenticationOptions, ClientCertificateMode)> GetTlsOptionsAsync(
267-
ConnectionContext connection, SslStream stream, SslClientHelloInfo clientHelloInfo, object state, CancellationToken cancellationToken)
259+
return listenOptions.UseHttps(new TlsHandshakeCallbackOptions()
268260
{
269-
var tlsOptions = await serverOptionsSelectionCallback(stream, clientHelloInfo, state, cancellationToken);
270-
return new (tlsOptions, ClientCertificateMode.DelayCertificate);
271-
}
261+
OnConnection = context => serverOptionsSelectionCallback(context.SslStream, context.ClientHelloInfo, context.State, context.CancellationToken),
262+
HandshakeTimeout = handshakeTimeout,
263+
OnConnectionState = state,
264+
});
272265
}
273266

274267
/// <summary>
275-
/// Configure Kestrel to use HTTPS.
268+
/// Configure Kestrel to use HTTPS. This does not use default certificates or other defaults specified via config or
269+
/// <see cref="KestrelServerOptions.ConfigureHttpsDefaults(Action{HttpsConnectionAdapterOptions})"/>.
276270
/// </summary>
277271
/// <param name="listenOptions">The <see cref="ListenOptions"/> to configure.</param>
278-
/// <param name="httpsOptionsCallback">Callback to configure HTTPS options.</param>
279-
/// <param name="state">State for the <paramref name="httpsOptionsCallback"/>.</param>
280-
/// <param name="handshakeTimeout">Specifies the maximum amount of time allowed for the TLS/SSL handshake. This must be positive and finite.</param>
272+
/// <param name="callbackOptions">Options for a per connection callback.</param>
281273
/// <returns>The <see cref="ListenOptions"/>.</returns>
282-
internal static ListenOptions UseHttps(this ListenOptions listenOptions, HttpsOptionsCallback httpsOptionsCallback, object state, TimeSpan handshakeTimeout)
274+
public static ListenOptions UseHttps(this ListenOptions listenOptions, TlsHandshakeCallbackOptions callbackOptions)
283275
{
276+
if (callbackOptions is null)
277+
{
278+
throw new ArgumentNullException(nameof(callbackOptions));
279+
}
280+
281+
if (callbackOptions.OnConnection is null)
282+
{
283+
throw new ArgumentException($"{nameof(TlsHandshakeCallbackOptions.OnConnection)} must not be null.");
284+
}
285+
284286
var loggerFactory = listenOptions.KestrelServerOptions?.ApplicationServices.GetRequiredService<ILoggerFactory>() ?? NullLoggerFactory.Instance;
285287

286288
listenOptions.IsTls = true;
287289
listenOptions.Use(next =>
288290
{
289-
var middleware = new HttpsConnectionMiddleware(next, httpsOptionsCallback, state, handshakeTimeout, loggerFactory);
291+
var middleware = new HttpsConnectionMiddleware(next, callbackOptions, loggerFactory);
290292
return middleware.OnConnectionAsync;
291293
});
292294

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

Lines changed: 20 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -26,8 +26,6 @@
2626

2727
namespace Microsoft.AspNetCore.Server.Kestrel.Https.Internal
2828
{
29-
internal delegate ValueTask<(SslServerAuthenticationOptions, ClientCertificateMode)> HttpsOptionsCallback(ConnectionContext connection, SslStream stream, SslClientHelloInfo clientHelloInfo, object state, CancellationToken cancellationToken);
30-
3129
internal class HttpsConnectionMiddleware
3230
{
3331
private const string EnableWindows81Http2 = "Microsoft.AspNetCore.Server.Kestrel.EnableWindows81Http2";
@@ -45,9 +43,9 @@ internal class HttpsConnectionMiddleware
4543
private readonly X509Certificate2? _serverCertificate;
4644
private readonly Func<ConnectionContext, string?, X509Certificate2?>? _serverCertificateSelector;
4745

48-
// The following fields are only set by ServerOptionsSelectionCallback ctor.
49-
private readonly HttpsOptionsCallback? _httpsOptionsCallback;
50-
private readonly object? _httpsOptionsCallbackState;
46+
// The following fields are only set by TlsHandshakeCallbackOptions ctor.
47+
private readonly Func<TlsHandshakeCallbackContext, ValueTask<SslServerAuthenticationOptions>>? _tlsCallbackOptions;
48+
private readonly object? _tlsCallbackOptionsState;
5149

5250
// Pool for cancellation tokens that cancel the handshake
5351
private readonly CancellationTokenSourcePool _ctsPool = new();
@@ -120,17 +118,15 @@ public HttpsConnectionMiddleware(ConnectionDelegate next, HttpsConnectionAdapter
120118

121119
internal HttpsConnectionMiddleware(
122120
ConnectionDelegate next,
123-
HttpsOptionsCallback httpsOptionsCallback,
124-
object httpsOptionsCallbackState,
125-
TimeSpan handshakeTimeout,
121+
TlsHandshakeCallbackOptions tlsCallbackOptions,
126122
ILoggerFactory loggerFactory)
127123
{
128124
_next = next;
129-
_handshakeTimeout = handshakeTimeout;
125+
_handshakeTimeout = tlsCallbackOptions.HandshakeTimeout;
130126
_logger = loggerFactory.CreateLogger<HttpsConnectionMiddleware>();
131127

132-
_httpsOptionsCallback = httpsOptionsCallback;
133-
_httpsOptionsCallbackState = httpsOptionsCallbackState;
128+
_tlsCallbackOptions = tlsCallbackOptions.OnConnection;
129+
_tlsCallbackOptionsState = tlsCallbackOptions.OnConnectionState;
134130
_sslStreamFactory = s => new SslStream(s);
135131
}
136132

@@ -149,7 +145,8 @@ public async Task OnConnectionAsync(ConnectionContext context)
149145

150146
var feature = new Core.Internal.TlsConnectionFeature(sslStream);
151147
// 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;
148+
feature.AllowDelayedClientCertificateNegotation =
149+
_options?.ClientCertificateMode == ClientCertificateMode.DelayCertificate;
153150
context.Features.Set<ITlsConnectionFeature>(feature);
154151
context.Features.Set<ITlsHandshakeFeature>(feature);
155152
context.Features.Set<ITlsApplicationProtocolFeature>(feature);
@@ -159,7 +156,7 @@ public async Task OnConnectionAsync(ConnectionContext context)
159156
using var cancellationTokenSource = _ctsPool.Rent();
160157
cancellationTokenSource.CancelAfter(_handshakeTimeout);
161158

162-
if (_httpsOptionsCallback is null)
159+
if (_tlsCallbackOptions is null)
163160
{
164161
await DoOptionsBasedHandshakeAsync(context, sslStream, feature, cancellationTokenSource.Token);
165162
}
@@ -426,9 +423,16 @@ private static async ValueTask<SslServerAuthenticationOptions> ServerOptionsCall
426423

427424
feature.HostName = clientHelloInfo.ServerName;
428425
context.Features.Set(sslStream);
429-
430-
var (sslOptions, clientCertificateMode) = await middleware._httpsOptionsCallback!(context, sslStream, clientHelloInfo, middleware._httpsOptionsCallbackState!, cancellationToken);
431-
feature.ClientCertificateMode = clientCertificateMode;
426+
var callbackContext = new TlsHandshakeCallbackContext()
427+
{
428+
Connection = context,
429+
SslStream = sslStream,
430+
State = middleware._tlsCallbackOptionsState,
431+
CancellationToken = cancellationToken,
432+
ClientHelloInfo = clientHelloInfo,
433+
};
434+
var sslOptions = await middleware._tlsCallbackOptions!(callbackContext);
435+
feature.AllowDelayedClientCertificateNegotation = callbackContext.AllowDelayedClientCertificateNegotation;
432436

433437
KestrelEventSource.Log.TlsHandshakeStart(context, sslOptions);
434438

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

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,7 +100,25 @@ Microsoft.AspNetCore.Server.Kestrel.Core.KestrelServerOptions.AllowAlternateSche
100100
Microsoft.AspNetCore.Server.Kestrel.Https.ClientCertificateMode.DelayCertificate = 3 -> Microsoft.AspNetCore.Server.Kestrel.Https.ClientCertificateMode
101101
Microsoft.AspNetCore.Server.Kestrel.Core.KestrelServerOptions.ResponseHeaderEncodingSelector.get -> System.Func<string!, System.Text.Encoding?>!
102102
Microsoft.AspNetCore.Server.Kestrel.Core.KestrelServerOptions.ResponseHeaderEncodingSelector.set -> void
103+
Microsoft.AspNetCore.Server.Kestrel.Https.TlsHandshakeCallbackContext
104+
Microsoft.AspNetCore.Server.Kestrel.Https.TlsHandshakeCallbackContext.AllowDelayedClientCertificateNegotation.get -> bool
105+
Microsoft.AspNetCore.Server.Kestrel.Https.TlsHandshakeCallbackContext.AllowDelayedClientCertificateNegotation.set -> void
106+
Microsoft.AspNetCore.Server.Kestrel.Https.TlsHandshakeCallbackContext.CancellationToken.get -> System.Threading.CancellationToken
107+
Microsoft.AspNetCore.Server.Kestrel.Https.TlsHandshakeCallbackContext.ClientHelloInfo.get -> System.Net.Security.SslClientHelloInfo
108+
Microsoft.AspNetCore.Server.Kestrel.Https.TlsHandshakeCallbackContext.Connection.get -> Microsoft.AspNetCore.Connections.ConnectionContext!
109+
Microsoft.AspNetCore.Server.Kestrel.Https.TlsHandshakeCallbackContext.SslStream.get -> System.Net.Security.SslStream!
110+
Microsoft.AspNetCore.Server.Kestrel.Https.TlsHandshakeCallbackContext.State.get -> object?
111+
Microsoft.AspNetCore.Server.Kestrel.Https.TlsHandshakeCallbackContext.TlsHandshakeCallbackContext() -> void
112+
Microsoft.AspNetCore.Server.Kestrel.Https.TlsHandshakeCallbackOptions
113+
Microsoft.AspNetCore.Server.Kestrel.Https.TlsHandshakeCallbackOptions.HandshakeTimeout.get -> System.TimeSpan
114+
Microsoft.AspNetCore.Server.Kestrel.Https.TlsHandshakeCallbackOptions.HandshakeTimeout.set -> void
115+
Microsoft.AspNetCore.Server.Kestrel.Https.TlsHandshakeCallbackOptions.OnConnection.get -> System.Func<Microsoft.AspNetCore.Server.Kestrel.Https.TlsHandshakeCallbackContext!, System.Threading.Tasks.ValueTask<System.Net.Security.SslServerAuthenticationOptions!>>!
116+
Microsoft.AspNetCore.Server.Kestrel.Https.TlsHandshakeCallbackOptions.OnConnection.set -> void
117+
Microsoft.AspNetCore.Server.Kestrel.Https.TlsHandshakeCallbackOptions.OnConnectionState.get -> object?
118+
Microsoft.AspNetCore.Server.Kestrel.Https.TlsHandshakeCallbackOptions.OnConnectionState.set -> void
119+
Microsoft.AspNetCore.Server.Kestrel.Https.TlsHandshakeCallbackOptions.TlsHandshakeCallbackOptions() -> void
103120
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!
121+
static Microsoft.AspNetCore.Hosting.ListenOptionsHttpsExtensions.UseHttps(this Microsoft.AspNetCore.Server.Kestrel.Core.ListenOptions! listenOptions, Microsoft.AspNetCore.Server.Kestrel.Https.TlsHandshakeCallbackOptions! callbackOptions) -> Microsoft.AspNetCore.Server.Kestrel.Core.ListenOptions!
104122
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!
105123
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!
106124
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!
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
// Copyright (c) .NET Foundation. All rights reserved.
2+
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
3+
4+
using System.Net.Security;
5+
using System.Threading;
6+
using Microsoft.AspNetCore.Connections;
7+
using Microsoft.AspNetCore.Http.Features;
8+
9+
namespace Microsoft.AspNetCore.Server.Kestrel.Https
10+
{
11+
/// <summary>
12+
/// Per connection state used to determine the TLS options.
13+
/// </summary>
14+
public class TlsHandshakeCallbackContext
15+
{
16+
// ServerOptionsSelectionCallback parameters
17+
18+
/// <summary>
19+
/// The TLS stream on which the authentication happens.
20+
/// </summary>
21+
public SslStream SslStream { get; internal set; } = default!;
22+
23+
/// <summary>
24+
/// Information from the Client Hello message.
25+
/// </summary>
26+
public SslClientHelloInfo ClientHelloInfo { get; internal set; }
27+
28+
/// <summary>
29+
/// The information that was passed when registering the callback.
30+
/// </summary>
31+
public object? State { get; internal set; }
32+
33+
/// <summary>
34+
/// The token to monitor for cancellation requests.
35+
/// </summary>
36+
public CancellationToken CancellationToken { get; internal set; }
37+
38+
// Kestrel specific
39+
40+
/// <summary>
41+
/// Information about an individual connection.
42+
/// </summary>
43+
public ConnectionContext Connection { get; internal set; } = default!;
44+
45+
/// <summary>
46+
/// Indicates if the application is allowed to request a client certificate after the handshake has completed.
47+
/// The default is false. See <see cref="ITlsConnectionFeature.GetClientCertificateAsync"/>
48+
/// </summary>
49+
public bool AllowDelayedClientCertificateNegotation { get; set; }
50+
}
51+
}

0 commit comments

Comments
 (0)