Skip to content

Commit f954244

Browse files
committed
Solidify fallback logic
1 parent 346bb2e commit f954244

File tree

6 files changed

+120
-36
lines changed

6 files changed

+120
-36
lines changed
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
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+
6+
namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal
7+
{
8+
internal class SniOptions
9+
{
10+
public SslServerAuthenticationOptions SslOptions { get; set; }
11+
public HttpProtocols HttpProtocols { get; set; }
12+
13+
// TODO: Reflection based test to ensure we clone everything!
14+
public SniOptions Clone()
15+
{
16+
return new SniOptions
17+
{
18+
SslOptions = new SslServerAuthenticationOptions
19+
{
20+
AllowRenegotiation = SslOptions.AllowRenegotiation,
21+
ApplicationProtocols = SslOptions.ApplicationProtocols,
22+
CertificateRevocationCheckMode = SslOptions.CertificateRevocationCheckMode,
23+
CipherSuitesPolicy = SslOptions.CipherSuitesPolicy,
24+
ClientCertificateRequired = SslOptions.ClientCertificateRequired,
25+
EnabledSslProtocols = SslOptions.EnabledSslProtocols,
26+
EncryptionPolicy = SslOptions.EncryptionPolicy,
27+
RemoteCertificateValidationCallback = SslOptions.RemoteCertificateValidationCallback,
28+
ServerCertificate = SslOptions.ServerCertificate,
29+
ServerCertificateContext = SslOptions.ServerCertificateContext,
30+
ServerCertificateSelectionCallback = SslOptions.ServerCertificateSelectionCallback,
31+
},
32+
HttpProtocols = HttpProtocols,
33+
};
34+
}
35+
}
36+
}

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

Lines changed: 63 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,11 @@
33

44
using System;
55
using System.Collections.Generic;
6+
using System.Diagnostics;
67
using System.Net.Security;
78
using System.Security.Authentication;
9+
using System.Security.Cryptography.X509Certificates;
10+
using Microsoft.AspNetCore.Connections;
811
using Microsoft.AspNetCore.Server.Kestrel.Https;
912
using Microsoft.AspNetCore.Server.Kestrel.Https.Internal;
1013
using Microsoft.Extensions.Logging;
@@ -17,9 +20,13 @@ internal class SniOptionsSelector
1720
private const string wildcardPrefix = "*.";
1821

1922
private readonly string _endpointName;
20-
private readonly Dictionary<string, SslServerAuthenticationOptions> _fullNameOptions = new Dictionary<string, SslServerAuthenticationOptions>(StringComparer.OrdinalIgnoreCase);
21-
private readonly List<(string, SslServerAuthenticationOptions)> _wildcardPrefixOptions = new List<(string, SslServerAuthenticationOptions)>();
22-
private readonly SslServerAuthenticationOptions _wildcardHostOptions = null;
23+
24+
private readonly Func<ConnectionContext, string, X509Certificate2> _fallbackServerCertificateSelector;
25+
private readonly Action<ConnectionContext, SslServerAuthenticationOptions> _onAuthenticateCallback;
26+
27+
private readonly Dictionary<string, SniOptions> _fullNameOptions = new Dictionary<string, SniOptions>(StringComparer.OrdinalIgnoreCase);
28+
private readonly List<(string, SniOptions)> _wildcardPrefixOptions = new List<(string, SniOptions)>();
29+
private readonly SniOptions _wildcardHostOptions = null;
2330

2431
public SniOptionsSelector(
2532
KestrelConfigurationLoader configLoader,
@@ -30,24 +37,30 @@ public SniOptionsSelector(
3037
{
3138
_endpointName = endpointConfig.Name;
3239

40+
_fallbackServerCertificateSelector = fallbackOptions.ServerCertificateSelector;
41+
_onAuthenticateCallback = fallbackOptions.OnAuthenticate;
42+
3343
foreach (var (name, sniConfig) in endpointConfig.SNI)
3444
{
35-
var sslServerOptions = new SslServerAuthenticationOptions();
36-
37-
sslServerOptions.ServerCertificate = configLoader.LoadCertificate(sniConfig.Certificate, endpointConfig.Name);
38-
39-
if (sslServerOptions.ServerCertificate is null &&
40-
fallbackOptions.ServerCertificate is null &&
41-
fallbackOptions.ServerCertificateSelector is null)
45+
var sslServerOptions = new SslServerAuthenticationOptions
4246
{
43-
throw new InvalidOperationException(CoreStrings.NoCertSpecifiedNoDevelopmentCertificateFound);
44-
}
47+
ServerCertificate = configLoader.LoadCertificate(sniConfig.Certificate, endpointConfig.Name),
48+
EnabledSslProtocols = sniConfig.SslProtocols ?? fallbackOptions.SslProtocols,
49+
};
4550

46-
sslServerOptions.EnabledSslProtocols = sniConfig.SslProtocols ?? fallbackOptions.SslProtocols;
51+
if (sslServerOptions.ServerCertificate is null)
52+
{
53+
if (fallbackOptions.ServerCertificate is null && fallbackOptions.ServerCertificateSelector is null)
54+
{
55+
throw new InvalidOperationException(CoreStrings.NoCertSpecifiedNoDevelopmentCertificateFound);
56+
}
4757

48-
var httpProtocols = sniConfig.Protocols ?? fallbackHttpProtocols;
49-
httpProtocols = HttpsConnectionMiddleware.ValidateAndNormalizeHttpProtocols(httpProtocols, logger);
50-
HttpsConnectionMiddleware.ConfigureAlpn(sslServerOptions, httpProtocols);
58+
if (fallbackOptions.ServerCertificateSelector is null)
59+
{
60+
// Cache the fallback ServerCertificate since there's no fallback ServerCertificateSelector taking precedence.
61+
sslServerOptions.ServerCertificate = fallbackOptions.ServerCertificate;
62+
}
63+
}
5164

5265
var clientCertificateMode = sniConfig.ClientCertificateMode ?? fallbackOptions.ClientCertificateMode;
5366

@@ -59,24 +72,34 @@ fallbackOptions.ServerCertificate is null &&
5972
clientCertificateMode, fallbackOptions.ClientCertificateValidation, certificate, chain, sslPolicyErrors);
6073
}
6174

75+
var httpProtocols = sniConfig.Protocols ?? fallbackHttpProtocols;
76+
httpProtocols = HttpsConnectionMiddleware.ValidateAndNormalizeHttpProtocols(httpProtocols, logger);
77+
HttpsConnectionMiddleware.ConfigureAlpn(sslServerOptions, httpProtocols);
78+
79+
var sniOptions = new SniOptions
80+
{
81+
SslOptions = sslServerOptions,
82+
HttpProtocols = httpProtocols,
83+
};
84+
6285
if (name.Equals(wildcardHost, StringComparison.Ordinal))
6386
{
64-
_wildcardHostOptions = sslServerOptions;
87+
_wildcardHostOptions = sniOptions;
6588
}
6689
else if (name.StartsWith(wildcardPrefix, StringComparison.Ordinal))
6790
{
68-
_wildcardPrefixOptions.Add((name, sslServerOptions));
91+
_wildcardPrefixOptions.Add((name, sniOptions));
6992
}
7093
else
7194
{
72-
_fullNameOptions[name] = sslServerOptions;
95+
_fullNameOptions[name] = sniOptions;
7396
}
7497
}
7598
}
7699

77-
public SslServerAuthenticationOptions GetOptions(string serverName)
100+
public SniOptions GetOptions(ConnectionContext connection, string serverName)
78101
{
79-
SslServerAuthenticationOptions options = null;
102+
SniOptions options = null;
80103

81104
if (!string.IsNullOrEmpty(serverName))
82105
{
@@ -116,6 +139,25 @@ public SslServerAuthenticationOptions GetOptions(string serverName)
116139
}
117140
}
118141

142+
if (options.SslOptions.ServerCertificate is null)
143+
{
144+
Debug.Assert(_fallbackServerCertificateSelector != null,
145+
"The cached SniOptions ServerCertificate can only be null if there's a fallback certificate selector.");
146+
147+
// If a ServerCertificateSelector passed into HttpsConnectionMiddleware via HttpsConnectionAdapterOptions doesn't return a cert,
148+
// HttpsConnectionMiddleware doesn't fallback to the ServerCertificate, so we don't do that here either.
149+
options = options.Clone();
150+
options.SslOptions.ServerCertificate = _fallbackServerCertificateSelector(connection, serverName);
151+
}
152+
153+
if (_onAuthenticateCallback != null)
154+
{
155+
options = options.Clone();
156+
157+
// From doc comments: "This is called after all of the other settings have already been applied."
158+
_onAuthenticateCallback(connection, options.SslOptions);
159+
}
160+
119161
return options;
120162
}
121163
}

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

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
using System.Threading;
1515
using System.Threading.Tasks;
1616
using Microsoft.AspNetCore.Certificates.Generation;
17+
using Microsoft.AspNetCore.Connections;
1718
using Microsoft.AspNetCore.Hosting;
1819
using Microsoft.AspNetCore.Server.Kestrel.Core;
1920
using Microsoft.AspNetCore.Server.Kestrel.Core.Internal;
@@ -371,11 +372,11 @@ public void Load()
371372
return (endpointsToStop, endpointsToStart);
372373
}
373374

374-
private static ValueTask<SslServerAuthenticationOptions> ServerOptionsSelectionCallback(SslStream stream, SslClientHelloInfo clientHelloInfo, object state, CancellationToken cancellationToken)
375+
private static ValueTask<SslServerAuthenticationOptions> ServerOptionsSelectionCallback(ConnectionContext connection, SslStream stream, SslClientHelloInfo clientHelloInfo, object state, CancellationToken cancellationToken)
375376
{
376377
var sniOptionsSelector = (SniOptionsSelector)state;
377-
var options = sniOptionsSelector.GetOptions(clientHelloInfo.ServerName);
378-
return new ValueTask<SslServerAuthenticationOptions>(options);
378+
var options = sniOptionsSelector.GetOptions(connection, clientHelloInfo.ServerName);
379+
return new ValueTask<SslServerAuthenticationOptions>(options.SslOptions);
379380
}
380381

381382
private void LoadDefaultCert(ConfigurationReader configReader)

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

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -233,17 +233,17 @@ public static ListenOptions UseHttps(this ListenOptions listenOptions, HttpsConn
233233
/// Configure Kestrel to use HTTPS.
234234
/// </summary>
235235
/// <param name="listenOptions">The <see cref="ListenOptions"/> to configure.</param>
236-
/// <param name="serverOptionsCallback">Callback to configure HTTPS options.</param>
236+
/// <param name="httpsOptionsCallback">Callback to configure HTTPS options.</param>
237237
/// <param name="state">State for the <see cref="ServerOptionsSelectionCallback" />.</param>
238238
/// <returns>The <see cref="ListenOptions"/>.</returns>
239-
internal static ListenOptions UseHttps(this ListenOptions listenOptions, ServerOptionsSelectionCallback serverOptionsCallback, object state = null)
239+
internal static ListenOptions UseHttps(this ListenOptions listenOptions, HttpsOptionsCallback httpsOptionsCallback, object state = null)
240240
{
241241
var loggerFactory = listenOptions.KestrelServerOptions?.ApplicationServices.GetRequiredService<ILoggerFactory>() ?? NullLoggerFactory.Instance;
242242

243243
listenOptions.IsTls = true;
244244
listenOptions.Use(next =>
245245
{
246-
var middleware = new HttpsConnectionMiddleware(next, serverOptionsCallback, state, loggerFactory);
246+
var middleware = new HttpsConnectionMiddleware(next, httpsOptionsCallback, state, loggerFactory);
247247
return middleware.OnConnectionAsync;
248248
});
249249

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

Lines changed: 13 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -24,11 +24,14 @@
2424

2525
namespace Microsoft.AspNetCore.Server.Kestrel.Https.Internal
2626
{
27+
28+
internal delegate ValueTask<SslServerAuthenticationOptions> HttpsOptionsCallback(ConnectionContext connection, SslStream stream, SslClientHelloInfo clientHelloInfo, object state, CancellationToken cancellationToken);
29+
2730
internal class HttpsConnectionMiddleware
2831
{
2932
private const string EnableWindows81Http2 = "Microsoft.AspNetCore.Server.Kestrel.EnableWindows81Http2";
3033

31-
internal static TimeSpan DefaultHandshakeTimeout = TimeSpan.FromSeconds(10);
34+
internal static readonly TimeSpan DefaultHandshakeTimeout = TimeSpan.FromSeconds(10);
3235

3336
private readonly ConnectionDelegate _next;
3437
private readonly ILogger _logger;
@@ -40,8 +43,9 @@ internal class HttpsConnectionMiddleware
4043
private readonly Func<ConnectionContext, string, X509Certificate2> _serverCertificateSelector;
4144

4245
// The following fields are only set by ServerOptionsSelectionCallback ctor.
43-
private readonly ServerOptionsSelectionCallback _serverOptionsSelectionCallback;
44-
private readonly object _serverOptionsSelectionCallbackState;
46+
// If we ever expose this via a public API, we should really create a delegate type.
47+
private readonly HttpsOptionsCallback _httpsOptionsCallback;
48+
private readonly object _httpsOptionsCallbackState;
4549

4650
public HttpsConnectionMiddleware(ConnectionDelegate next, HttpsConnectionAdapterOptions options)
4751
: this(next, options, loggerFactory: NullLoggerFactory.Instance)
@@ -88,14 +92,14 @@ public HttpsConnectionMiddleware(ConnectionDelegate next, HttpsConnectionAdapter
8892

8993
internal HttpsConnectionMiddleware(
9094
ConnectionDelegate next,
91-
ServerOptionsSelectionCallback serverOptionsSelectionCallback,
92-
object serverOptionsSelectionCallbackState,
95+
HttpsOptionsCallback httpsOptionsCallback,
96+
object httpsOptionsCallbackState,
9397
ILoggerFactory loggerFactory)
9498
{
9599
_next = next;
96100
_logger = loggerFactory.CreateLogger<HttpsConnectionMiddleware>();
97-
_serverOptionsSelectionCallback = serverOptionsSelectionCallback;
98-
_serverOptionsSelectionCallbackState = serverOptionsSelectionCallbackState;
101+
_httpsOptionsCallback = httpsOptionsCallback;
102+
_httpsOptionsCallbackState = httpsOptionsCallbackState;
99103
_sslStreamFactory = s => new SslStream(s);
100104
}
101105

@@ -119,7 +123,7 @@ public async Task OnConnectionAsync(ConnectionContext context)
119123
try
120124
{
121125
using var cancellationTokenSource = new CancellationTokenSource(_options?.HandshakeTimeout ?? DefaultHandshakeTimeout);
122-
if (_serverOptionsSelectionCallback is null)
126+
if (_httpsOptionsCallback is null)
123127
{
124128
await DoOptionsBasedHandshakeAsync(context, sslStream, feature, cancellationTokenSource.Token);
125129
}
@@ -316,7 +320,7 @@ private static async ValueTask<SslServerAuthenticationOptions> ServerOptionsCall
316320

317321
feature.HostName = clientHelloInfo.ServerName;
318322

319-
var sslOptions = await middleware._serverOptionsSelectionCallback(stream, clientHelloInfo, middleware._serverOptionsSelectionCallbackState, cancellationToken);
323+
var sslOptions = await middleware._httpsOptionsCallback(context, stream, clientHelloInfo, middleware._httpsOptionsCallbackState, cancellationToken);
320324

321325
// REVIEW: Cache results? We don't do this for ServerCertificateSelectionCallback.
322326
if (sslOptions.ServerCertificate is X509Certificate2 cert)

src/Servers/Kestrel/samples/SampleApp/appsettings.Production.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
"Protocols": "Http1"
1010
},
1111
"*": {
12+
"SslProtocols": [ "Tls12", "Tls13" ]
1213
}
1314
}
1415
}

0 commit comments

Comments
 (0)