Skip to content

Commit 885eef2

Browse files
committed
Cache SslServerAuthenticationOptions and certs
1 parent 5ffb1ee commit 885eef2

File tree

3 files changed

+149
-79
lines changed

3 files changed

+149
-79
lines changed

src/Servers/Kestrel/Core/src/CoreStrings.resx

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -620,4 +620,10 @@ For more information on configuring HTTPS see https://go.microsoft.com/fwlink/?l
620620
<data name="UnrecognizedCertificateKeyOid" xml:space="preserve">
621621
<value>Unknown algorithm for certificate with public key type '{0}'.</value>
622622
</data>
623+
<data name="SniNotConfiguredForServerName" xml:space="preserve">
624+
<value>Connection refused because no SNI configuration was found for '{serverName}' in '{endpointName}'. To allow all connections, add a wildcard ('*') SNI section.</value>
625+
</data>
626+
<data name="SniNotConfiguredToAllowNoServerName" xml:space="preserve">
627+
<value>Connection refulsed because the client did not specify a server name, and no wildcard ('*') SNI configuration was found in '{endpointName}'.</value>
628+
</data>
623629
</root>
Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
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;
5+
using System.Collections.Generic;
6+
using System.Net.Security;
7+
using System.Security.Authentication;
8+
using Microsoft.AspNetCore.Server.Kestrel.Https;
9+
using Microsoft.AspNetCore.Server.Kestrel.Https.Internal;
10+
11+
namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal
12+
{
13+
internal class SniOptionsSelector
14+
{
15+
private const string wildcardHost = "*";
16+
private const string wildcardPrefix = "*.";
17+
18+
private readonly string _endpointName;
19+
private readonly Dictionary<string, SslServerAuthenticationOptions> _fullNameOptions = new Dictionary<string, SslServerAuthenticationOptions>(StringComparer.OrdinalIgnoreCase);
20+
private readonly List<(string, SslServerAuthenticationOptions)> _wildcardPrefixOptions = new List<(string, SslServerAuthenticationOptions)>();
21+
private readonly SslServerAuthenticationOptions _wildcardHostOptions = null;
22+
23+
public SniOptionsSelector(
24+
KestrelConfigurationLoader configLoader,
25+
EndpointConfig endpointConfig,
26+
HttpsConnectionAdapterOptions fallbackOptions,
27+
HttpProtocols fallbackHttpProtocols)
28+
{
29+
_endpointName = endpointConfig.Name;
30+
31+
foreach (var (name, sniConfig) in endpointConfig.SNI)
32+
{
33+
var sslServerOptions = new SslServerAuthenticationOptions();
34+
35+
sslServerOptions.ServerCertificate = configLoader.LoadCertificate(sniConfig.Certificate, endpointConfig.Name)
36+
?? configLoader.LoadEndpointOrDefaultCertificate(fallbackOptions, endpointConfig);
37+
38+
if (sslServerOptions.ServerCertificate is null)
39+
{
40+
throw new InvalidOperationException(CoreStrings.NoCertSpecifiedNoDevelopmentCertificateFound);
41+
}
42+
43+
sslServerOptions.EnabledSslProtocols = sniConfig.SslProtocols ?? fallbackOptions.SslProtocols;
44+
HttpsConnectionMiddleware.ConfigureAlpn(sslServerOptions, sniConfig.Protocols ?? fallbackHttpProtocols);
45+
46+
var clientCertificateMode = sniConfig.ClientCertificateMode ?? fallbackOptions.ClientCertificateMode;
47+
48+
if (clientCertificateMode != ClientCertificateMode.NoCertificate)
49+
{
50+
sslServerOptions.ClientCertificateRequired = true;
51+
sslServerOptions.RemoteCertificateValidationCallback = (sender, certificate, chain, sslPolicyErrors) =>
52+
HttpsConnectionMiddleware.RemoteCertificateValidationCallback(
53+
clientCertificateMode, fallbackOptions.ClientCertificateValidation, certificate, chain, sslPolicyErrors);
54+
}
55+
56+
if (name.Equals(wildcardHost, StringComparison.Ordinal))
57+
{
58+
_wildcardHostOptions = sslServerOptions;
59+
}
60+
else if (name.StartsWith(wildcardPrefix, StringComparison.Ordinal))
61+
{
62+
_wildcardPrefixOptions.Add((name, sslServerOptions));
63+
}
64+
else
65+
{
66+
_fullNameOptions[name] = sslServerOptions;
67+
}
68+
}
69+
}
70+
71+
public SslServerAuthenticationOptions GetOptions(string serverName)
72+
{
73+
SslServerAuthenticationOptions options = null;
74+
75+
if (!string.IsNullOrEmpty(serverName))
76+
{
77+
if (_fullNameOptions.TryGetValue(serverName, out options))
78+
{
79+
return options;
80+
}
81+
82+
var matchedNameLength = 0;
83+
ReadOnlySpan<char> serverNameSpan = serverName;
84+
85+
foreach (var (nameCandidate, optionsCandidate) in _wildcardPrefixOptions)
86+
{
87+
ReadOnlySpan<char> nameCandidateSpan = nameCandidate;
88+
89+
// Note that we only slice off the `*`. We want to match the leading `.` also.
90+
if (serverNameSpan.EndsWith(nameCandidateSpan.Slice(wildcardHost.Length), StringComparison.OrdinalIgnoreCase)
91+
&& nameCandidateSpan.Length > matchedNameLength)
92+
{
93+
matchedNameLength = nameCandidateSpan.Length;
94+
options = optionsCandidate;
95+
}
96+
}
97+
}
98+
99+
options ??= _wildcardHostOptions;
100+
101+
if (options is null)
102+
{
103+
if (serverName is null)
104+
{
105+
throw new AuthenticationException(CoreStrings.FormatSniNotConfiguredToAllowNoServerName(_endpointName));
106+
}
107+
else
108+
{
109+
throw new AuthenticationException(CoreStrings.FormatSniNotConfiguredForServerName(serverName, _endpointName));
110+
}
111+
}
112+
113+
return options;
114+
}
115+
}
116+
}

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

Lines changed: 27 additions & 79 deletions
Original file line numberDiff line numberDiff line change
@@ -281,7 +281,7 @@ public void Load()
281281

282282
// Compare to UseHttps(httpsOptions => { })
283283
var httpsOptions = new HttpsConnectionAdapterOptions();
284-
ServerOptionsSelectionCallback serverOptionsCallback = null;
284+
SniOptionsSelector sniOptionsSelector = null;
285285

286286
if (https)
287287
{
@@ -318,8 +318,7 @@ public void Load()
318318
}
319319
else
320320
{
321-
serverOptionsCallback = (stream, clientHelloInfo, state, cancellationToken) =>
322-
SniCallback(httpsOptions, listenOptions.Protocols, endpoint, clientHelloInfo);
321+
sniOptionsSelector = new SniOptionsSelector(this, endpoint, httpsOptions, listenOptions.Protocols);
323322
}
324323
}
325324

@@ -343,7 +342,7 @@ public void Load()
343342
// EndpointDefaults or configureEndpoint may have added an https adapter.
344343
if (https && !listenOptions.IsTls)
345344
{
346-
if (serverOptionsCallback is null)
345+
if (sniOptionsSelector is null)
347346
{
348347
if (httpsOptions.ServerCertificate == null && httpsOptions.ServerCertificateSelector == null)
349348
{
@@ -354,7 +353,7 @@ public void Load()
354353
}
355354
else
356355
{
357-
listenOptions.UseHttps(serverOptionsCallback);
356+
listenOptions.UseHttps(ServerOptionsSelectionCallback, sniOptionsSelector);
358357
}
359358
}
360359

@@ -367,80 +366,11 @@ public void Load()
367366
return (endpointsToStop, endpointsToStart);
368367
}
369368

370-
private ValueTask<SslServerAuthenticationOptions> SniCallback(
371-
HttpsConnectionAdapterOptions httpsOptions,
372-
HttpProtocols endpointDefaultHttpProtocols,
373-
EndpointConfig endpoint,
374-
SslClientHelloInfo clientHelloInfo)
369+
private static ValueTask<SslServerAuthenticationOptions> ServerOptionsSelectionCallback(SslStream stream, SslClientHelloInfo clientHelloInfo, object state, CancellationToken cancellationToken)
375370
{
376-
const string wildcardHost = "*";
377-
const string wildcardPrefix = "*.";
378-
379-
var sslServerOptions = new SslServerAuthenticationOptions();
380-
var serverName = clientHelloInfo.ServerName;
381-
SniConfig sniConfig = null;
382-
383-
if (!string.IsNullOrEmpty(serverName))
384-
{
385-
foreach (var (name, sniConfigCandidate) in endpoint.SNI)
386-
{
387-
if (name.Equals(serverName, StringComparison.OrdinalIgnoreCase))
388-
{
389-
sniConfig = sniConfigCandidate;
390-
break;
391-
}
392-
// Note that we only slice off the `*`. We want to match the leading `.` also.
393-
else if (name.StartsWith(wildcardPrefix, StringComparison.Ordinal)
394-
&& serverName.EndsWith(name.Substring(wildcardHost.Length), StringComparison.OrdinalIgnoreCase)
395-
&& name.Length > (sniConfig?.Name.Length ?? 0))
396-
{
397-
sniConfig = sniConfigCandidate;
398-
}
399-
else if (name.Equals(wildcardHost, StringComparison.Ordinal))
400-
{
401-
sniConfig ??= sniConfigCandidate;
402-
}
403-
}
404-
}
405-
406-
sslServerOptions.ServerCertificate = LoadCertificate(sniConfig?.Certificate, endpoint.Name) ?? LoadEndpointOrDefaultCertificate(httpsOptions, endpoint);
407-
408-
if (sslServerOptions.ServerCertificate is null)
409-
{
410-
throw new InvalidOperationException(CoreStrings.NoCertSpecifiedNoDevelopmentCertificateFound);
411-
}
412-
413-
sslServerOptions.EnabledSslProtocols = sniConfig?.SslProtocols ?? httpsOptions.SslProtocols;
414-
HttpsConnectionMiddleware.ConfigureAlpn(sslServerOptions, sniConfig?.Protocols ?? endpointDefaultHttpProtocols);
415-
416-
var clientCertificateMode = sniConfig?.ClientCertificateMode ?? httpsOptions.ClientCertificateMode;
417-
418-
if (clientCertificateMode != ClientCertificateMode.NoCertificate)
419-
{
420-
sslServerOptions.ClientCertificateRequired = true;
421-
sslServerOptions.RemoteCertificateValidationCallback = (sender, certificate, chain, sslPolicyErrors) =>
422-
HttpsConnectionMiddleware.RemoteCertificateValidationCallback(clientCertificateMode, httpsOptions.ClientCertificateValidation, certificate, chain, sslPolicyErrors);
423-
}
424-
425-
return new ValueTask<SslServerAuthenticationOptions>(sslServerOptions);
426-
}
427-
428-
private X509Certificate2 LoadEndpointOrDefaultCertificate(HttpsConnectionAdapterOptions httpsOptions, EndpointConfig endpoint)
429-
{
430-
// Specified
431-
httpsOptions.ServerCertificate = LoadCertificate(endpoint.Certificate, endpoint.Name)
432-
?? httpsOptions.ServerCertificate;
433-
434-
if (httpsOptions.ServerCertificate == null && httpsOptions.ServerCertificateSelector == null)
435-
{
436-
// Fallback
437-
Options.ApplyDefaultCert(httpsOptions);
438-
439-
// Ensure endpoint is reloaded if it used the default certificate and the certificate changed.
440-
endpoint.Certificate = DefaultCertificateConfig;
441-
}
442-
443-
return httpsOptions.ServerCertificate;
371+
var sniOptionsSelector = (SniOptionsSelector)state;
372+
var options = sniOptionsSelector.GetOptions(clientHelloInfo.ServerName);
373+
return new ValueTask<SslServerAuthenticationOptions>(options);
444374
}
445375

446376
private void LoadDefaultCert(ConfigurationReader configReader)
@@ -531,7 +461,7 @@ private bool TryGetCertificatePath(out string path)
531461
return path != null;
532462
}
533463

534-
private X509Certificate2 LoadCertificate(CertificateConfig certInfo, string endpointName)
464+
internal X509Certificate2 LoadCertificate(CertificateConfig certInfo, string endpointName)
535465
{
536466
if (certInfo is null)
537467
{
@@ -622,6 +552,24 @@ static X509Certificate2 GetCertificate(string certificatePath)
622552
}
623553
}
624554

555+
internal X509Certificate2 LoadEndpointOrDefaultCertificate(HttpsConnectionAdapterOptions httpsOptions, EndpointConfig endpoint)
556+
{
557+
// Specified
558+
httpsOptions.ServerCertificate = LoadCertificate(endpoint.Certificate, endpoint.Name)
559+
?? httpsOptions.ServerCertificate;
560+
561+
if (httpsOptions.ServerCertificate == null && httpsOptions.ServerCertificateSelector == null)
562+
{
563+
// Fallback
564+
Options.ApplyDefaultCert(httpsOptions);
565+
566+
// Ensure endpoint is reloaded if it used the default certificate and the certificate changed.
567+
endpoint.Certificate = DefaultCertificateConfig;
568+
}
569+
570+
return httpsOptions.ServerCertificate;
571+
}
572+
625573
private static X509Certificate2 AttachPemRSAKey(X509Certificate2 certificate, string keyText, string password)
626574
{
627575
using var rsa = RSA.Create();

0 commit comments

Comments
 (0)