Skip to content

New UseHttps overload with per connection callback #33953

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Jul 9, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,8 @@ public void AllowAnyClientCertificate()
public Action<ConnectionContext, SslServerAuthenticationOptions>? OnAuthenticate { get; set; }

/// <summary>
/// Specifies the maximum amount of time allowed for the TLS/SSL handshake. This must be positive and finite. Defaults to 10 seconds.
/// Specifies the maximum amount of time allowed for the TLS/SSL handshake. This must be positive
/// or <see cref="Timeout.InfiniteTimeSpan"/>. Defaults to 10 seconds.
/// </summary>
public TimeSpan HandshakeTimeout
{
Expand Down
9 changes: 5 additions & 4 deletions src/Servers/Kestrel/Core/src/Internal/SniOptionsSelector.cs
Original file line number Diff line number Diff line change
Expand Up @@ -176,11 +176,12 @@ public SniOptionsSelector(
return (sslOptions, sniOptions.ClientCertificateMode);
}

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

internal static SslServerAuthenticationOptions CloneSslOptions(SslServerAuthenticationOptions sslOptions) =>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ public TlsConnectionFeature(SslStream sslStream)
_sslStream = sslStream;
}

internal ClientCertificateMode ClientCertificateMode { get; set; }
internal bool AllowDelayedClientCertificateNegotation { get; set; }

public X509Certificate2? ClientCertificate
{
Expand Down Expand Up @@ -114,7 +114,7 @@ public int KeyExchangeStrength
}

if (ClientCertificate != null
|| ClientCertificateMode != ClientCertificateMode.DelayCertificate
|| !AllowDelayedClientCertificateNegotation
// Delayed client cert negotiation is not allowed on HTTP/2 (or HTTP/3, but that's implemented elsewhere).
|| _sslStream.NegotiatedApplicationProtocol == SslApplicationProtocol.Http2)
{
Expand Down
12 changes: 10 additions & 2 deletions src/Servers/Kestrel/Core/src/KestrelConfigurationLoader.cs
Original file line number Diff line number Diff line change
Expand Up @@ -400,8 +400,16 @@ public void Load()
}
else
{
var sniOptionsSelector = new SniOptionsSelector(endpoint.Name, endpoint.Sni, CertificateConfigLoader, httpsOptions, listenOptions.Protocols, HttpsLogger);
listenOptions.UseHttps(SniOptionsSelector.OptionsCallback, sniOptionsSelector, httpsOptions.HandshakeTimeout);
var sniOptionsSelector = new SniOptionsSelector(endpoint.Name, endpoint.Sni, CertificateConfigLoader,
httpsOptions, listenOptions.Protocols, HttpsLogger);
var tlsCallbackOptions = new TlsHandshakeCallbackOptions()
{
OnConnection = SniOptionsSelector.OptionsCallback,
HandshakeTimeout = httpsOptions.HandshakeTimeout,
OnConnectionState = sniOptionsSelector,
};

listenOptions.UseHttps(tlsCallbackOptions);
}
}

Expand Down
38 changes: 20 additions & 18 deletions src/Servers/Kestrel/Core/src/ListenOptionsHttpsExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,6 @@
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;
Expand Down Expand Up @@ -259,34 +256,39 @@ public static ListenOptions UseHttps(this ListenOptions listenOptions, ServerOpt
/// <returns>The <see cref="ListenOptions"/>.</returns>
public static ListenOptions UseHttps(this ListenOptions listenOptions, ServerOptionsSelectionCallback serverOptionsSelectionCallback, object state, TimeSpan handshakeTimeout)
{
// 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);

async ValueTask<(SslServerAuthenticationOptions, ClientCertificateMode)> GetTlsOptionsAsync(
ConnectionContext connection, SslStream stream, SslClientHelloInfo clientHelloInfo, object state, CancellationToken cancellationToken)
return listenOptions.UseHttps(new TlsHandshakeCallbackOptions()
{
var tlsOptions = await serverOptionsSelectionCallback(stream, clientHelloInfo, state, cancellationToken);
return new (tlsOptions, ClientCertificateMode.DelayCertificate);
}
OnConnection = context => serverOptionsSelectionCallback(context.SslStream, context.ClientHelloInfo, context.State, context.CancellationToken),
HandshakeTimeout = handshakeTimeout,
OnConnectionState = state,
});
}

/// <summary>
/// Configure Kestrel to use HTTPS.
/// Configure Kestrel to use HTTPS. This does not use default certificates or other defaults specified via config or
/// <see cref="KestrelServerOptions.ConfigureHttpsDefaults(Action{HttpsConnectionAdapterOptions})"/>.
/// </summary>
/// <param name="listenOptions">The <see cref="ListenOptions"/> to configure.</param>
/// <param name="httpsOptionsCallback">Callback to configure HTTPS options.</param>
/// <param name="state">State for the <paramref name="httpsOptionsCallback"/>.</param>
/// <param name="handshakeTimeout">Specifies the maximum amount of time allowed for the TLS/SSL handshake. This must be positive and finite.</param>
/// <param name="callbackOptions">Options for a per connection callback.</param>
/// <returns>The <see cref="ListenOptions"/>.</returns>
internal static ListenOptions UseHttps(this ListenOptions listenOptions, HttpsOptionsCallback httpsOptionsCallback, object state, TimeSpan handshakeTimeout)
public static ListenOptions UseHttps(this ListenOptions listenOptions, TlsHandshakeCallbackOptions callbackOptions)
{
if (callbackOptions is null)
{
throw new ArgumentNullException(nameof(callbackOptions));
}

if (callbackOptions.OnConnection is null)
{
throw new ArgumentException($"{nameof(TlsHandshakeCallbackOptions.OnConnection)} must not be null.");
}

var loggerFactory = listenOptions.KestrelServerOptions?.ApplicationServices.GetRequiredService<ILoggerFactory>() ?? NullLoggerFactory.Instance;

listenOptions.IsTls = true;
listenOptions.Use(next =>
{
var middleware = new HttpsConnectionMiddleware(next, httpsOptionsCallback, state, handshakeTimeout, loggerFactory);
var middleware = new HttpsConnectionMiddleware(next, callbackOptions, loggerFactory);
return middleware.OnConnectionAsync;
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,6 @@

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

internal class HttpsConnectionMiddleware
{
private const string EnableWindows81Http2 = "Microsoft.AspNetCore.Server.Kestrel.EnableWindows81Http2";
Expand All @@ -45,9 +43,9 @@ internal class HttpsConnectionMiddleware
private readonly X509Certificate2? _serverCertificate;
private readonly Func<ConnectionContext, string?, X509Certificate2?>? _serverCertificateSelector;

// The following fields are only set by ServerOptionsSelectionCallback ctor.
private readonly HttpsOptionsCallback? _httpsOptionsCallback;
private readonly object? _httpsOptionsCallbackState;
// The following fields are only set by TlsHandshakeCallbackOptions ctor.
private readonly Func<TlsHandshakeCallbackContext, ValueTask<SslServerAuthenticationOptions>>? _tlsCallbackOptions;
private readonly object? _tlsCallbackOptionsState;

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

internal HttpsConnectionMiddleware(
ConnectionDelegate next,
HttpsOptionsCallback httpsOptionsCallback,
object httpsOptionsCallbackState,
TimeSpan handshakeTimeout,
TlsHandshakeCallbackOptions tlsCallbackOptions,
ILoggerFactory loggerFactory)
{
_next = next;
_handshakeTimeout = handshakeTimeout;
_handshakeTimeout = tlsCallbackOptions.HandshakeTimeout;
_logger = loggerFactory.CreateLogger<HttpsConnectionMiddleware>();

_httpsOptionsCallback = httpsOptionsCallback;
_httpsOptionsCallbackState = httpsOptionsCallbackState;
_tlsCallbackOptions = tlsCallbackOptions.OnConnection;
_tlsCallbackOptionsState = tlsCallbackOptions.OnConnectionState;
_sslStreamFactory = s => new SslStream(s);
}

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

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;
feature.AllowDelayedClientCertificateNegotation =
_options?.ClientCertificateMode == ClientCertificateMode.DelayCertificate;
context.Features.Set<ITlsConnectionFeature>(feature);
context.Features.Set<ITlsHandshakeFeature>(feature);
context.Features.Set<ITlsApplicationProtocolFeature>(feature);
Expand All @@ -159,7 +156,7 @@ public async Task OnConnectionAsync(ConnectionContext context)
using var cancellationTokenSource = _ctsPool.Rent();
cancellationTokenSource.CancelAfter(_handshakeTimeout);

if (_httpsOptionsCallback is null)
if (_tlsCallbackOptions is null)
{
await DoOptionsBasedHandshakeAsync(context, sslStream, feature, cancellationTokenSource.Token);
}
Expand Down Expand Up @@ -426,9 +423,16 @@ private static async ValueTask<SslServerAuthenticationOptions> ServerOptionsCall

feature.HostName = clientHelloInfo.ServerName;
context.Features.Set(sslStream);

var (sslOptions, clientCertificateMode) = await middleware._httpsOptionsCallback!(context, sslStream, clientHelloInfo, middleware._httpsOptionsCallbackState!, cancellationToken);
feature.ClientCertificateMode = clientCertificateMode;
var callbackContext = new TlsHandshakeCallbackContext()
{
Connection = context,
SslStream = sslStream,
State = middleware._tlsCallbackOptionsState,
CancellationToken = cancellationToken,
ClientHelloInfo = clientHelloInfo,
};
var sslOptions = await middleware._tlsCallbackOptions!(callbackContext);
feature.AllowDelayedClientCertificateNegotation = callbackContext.AllowDelayedClientCertificateNegotation;

KestrelEventSource.Log.TlsHandshakeStart(context, sslOptions);

Expand Down
18 changes: 18 additions & 0 deletions src/Servers/Kestrel/Core/src/PublicAPI.Unshipped.txt
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,25 @@ Microsoft.AspNetCore.Server.Kestrel.Core.KestrelServerOptions.AllowAlternateSche
Microsoft.AspNetCore.Server.Kestrel.Https.ClientCertificateMode.DelayCertificate = 3 -> Microsoft.AspNetCore.Server.Kestrel.Https.ClientCertificateMode
Microsoft.AspNetCore.Server.Kestrel.Core.KestrelServerOptions.ResponseHeaderEncodingSelector.get -> System.Func<string!, System.Text.Encoding?>!
Microsoft.AspNetCore.Server.Kestrel.Core.KestrelServerOptions.ResponseHeaderEncodingSelector.set -> void
Microsoft.AspNetCore.Server.Kestrel.Https.TlsHandshakeCallbackContext
Microsoft.AspNetCore.Server.Kestrel.Https.TlsHandshakeCallbackContext.AllowDelayedClientCertificateNegotation.get -> bool
Microsoft.AspNetCore.Server.Kestrel.Https.TlsHandshakeCallbackContext.AllowDelayedClientCertificateNegotation.set -> void
Microsoft.AspNetCore.Server.Kestrel.Https.TlsHandshakeCallbackContext.CancellationToken.get -> System.Threading.CancellationToken
Microsoft.AspNetCore.Server.Kestrel.Https.TlsHandshakeCallbackContext.ClientHelloInfo.get -> System.Net.Security.SslClientHelloInfo
Microsoft.AspNetCore.Server.Kestrel.Https.TlsHandshakeCallbackContext.Connection.get -> Microsoft.AspNetCore.Connections.ConnectionContext!
Microsoft.AspNetCore.Server.Kestrel.Https.TlsHandshakeCallbackContext.SslStream.get -> System.Net.Security.SslStream!
Microsoft.AspNetCore.Server.Kestrel.Https.TlsHandshakeCallbackContext.State.get -> object?
Microsoft.AspNetCore.Server.Kestrel.Https.TlsHandshakeCallbackContext.TlsHandshakeCallbackContext() -> void
Microsoft.AspNetCore.Server.Kestrel.Https.TlsHandshakeCallbackOptions
Microsoft.AspNetCore.Server.Kestrel.Https.TlsHandshakeCallbackOptions.HandshakeTimeout.get -> System.TimeSpan
Microsoft.AspNetCore.Server.Kestrel.Https.TlsHandshakeCallbackOptions.HandshakeTimeout.set -> void
Microsoft.AspNetCore.Server.Kestrel.Https.TlsHandshakeCallbackOptions.OnConnection.get -> System.Func<Microsoft.AspNetCore.Server.Kestrel.Https.TlsHandshakeCallbackContext!, System.Threading.Tasks.ValueTask<System.Net.Security.SslServerAuthenticationOptions!>>!
Microsoft.AspNetCore.Server.Kestrel.Https.TlsHandshakeCallbackOptions.OnConnection.set -> void
Microsoft.AspNetCore.Server.Kestrel.Https.TlsHandshakeCallbackOptions.OnConnectionState.get -> object?
Microsoft.AspNetCore.Server.Kestrel.Https.TlsHandshakeCallbackOptions.OnConnectionState.set -> void
Microsoft.AspNetCore.Server.Kestrel.Https.TlsHandshakeCallbackOptions.TlsHandshakeCallbackOptions() -> void
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, Microsoft.AspNetCore.Server.Kestrel.Https.TlsHandshakeCallbackOptions! callbackOptions) -> Microsoft.AspNetCore.Server.Kestrel.Core.ListenOptions!
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!
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!
Expand Down
51 changes: 51 additions & 0 deletions src/Servers/Kestrel/Core/src/TlsHandshakeCallbackContext.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
// 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.Net.Security;
using System.Threading;
using Microsoft.AspNetCore.Connections;
using Microsoft.AspNetCore.Http.Features;

namespace Microsoft.AspNetCore.Server.Kestrel.Https
{
/// <summary>
/// Per connection state used to determine the TLS options.
/// </summary>
public class TlsHandshakeCallbackContext
{
// ServerOptionsSelectionCallback parameters

/// <summary>
/// The TLS stream on which the authentication happens.
/// </summary>
public SslStream SslStream { get; internal set; } = default!;

/// <summary>
/// Information from the Client Hello message.
/// </summary>
public SslClientHelloInfo ClientHelloInfo { get; internal set; }

/// <summary>
/// The information that was passed when registering the callback.
/// </summary>
public object? State { get; internal set; }

/// <summary>
/// The token to monitor for cancellation requests.
/// </summary>
public CancellationToken CancellationToken { get; internal set; }

// Kestrel specific

/// <summary>
/// Information about an individual connection.
/// </summary>
public ConnectionContext Connection { get; internal set; } = default!;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍 This resolves part of #31303 too.

The next major thing is to not break ALPN for HTTP/2 when this new UseHttps() overload is used. Exposing the ConfigureAlpn() is better than nothing, but I would prefer if it just works without needing to know to call that.


/// <summary>
/// Indicates if the application is allowed to request a client certificate after the handshake has completed.
/// The default is false. See <see cref="ITlsConnectionFeature.GetClientCertificateAsync"/>
/// </summary>
public bool AllowDelayedClientCertificateNegotation { get; set; }
}
}
Loading