Skip to content

Commit 29af02d

Browse files
committed
Support SNI config in EndpointDefaults
1 parent 8b13043 commit 29af02d

15 files changed

+396
-269
lines changed

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

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@
88
using System.Threading;
99
using Microsoft.AspNetCore.Connections;
1010
using Microsoft.AspNetCore.Server.Kestrel.Core;
11-
using Microsoft.AspNetCore.Server.Kestrel.Https.Internal;
1211

1312
namespace Microsoft.AspNetCore.Server.Kestrel.Https
1413
{

src/Servers/Kestrel/Core/src/Internal/Certificates/CertificateConfigLoader.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@ public CertificateConfigLoader(IHostEnvironment hostEnvironment, ILogger<Kestrel
2323
public IHostEnvironment HostEnvironment { get; }
2424
public ILogger<KestrelServer> Logger { get; }
2525

26+
public bool SkipValidation => false;
27+
2628
public X509Certificate2 LoadCertificate(CertificateConfig certInfo, string endpointName)
2729
{
2830
if (certInfo is null)

src/Servers/Kestrel/Core/src/Internal/Certificates/ICertificateConfigLoader.cs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,9 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Certificates
77
{
88
internal interface ICertificateConfigLoader
99
{
10+
// SkipValidation is only true in tests.
11+
bool SkipValidation { get; }
12+
1013
X509Certificate2 LoadCertificate(CertificateConfig certInfo, string endpointName);
1114
}
1215
}

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

Lines changed: 63 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -16,12 +16,13 @@ internal class ConfigurationReader
1616
private const string CertificatesKey = "Certificates";
1717
private const string CertificateKey = "Certificate";
1818
private const string SslProtocolsKey = "SslProtocols";
19-
private const string EndpointDefaultsKey = "EndpointDefaults";
2019
private const string EndpointsKey = "Endpoints";
2120
private const string UrlKey = "Url";
2221
private const string ClientCertificateModeKey = "ClientCertificateMode";
2322
private const string SniKey = "Sni";
2423

24+
internal const string EndpointDefaultsKey = "EndpointDefaults";
25+
2526
private readonly IConfiguration _configuration;
2627

2728
private IDictionary<string, CertificateConfig> _certificates;
@@ -51,9 +52,20 @@ private IDictionary<string, CertificateConfig> ReadCertificates()
5152
}
5253

5354
// "EndpointDefaults": {
54-
// "Protocols": "Http1AndHttp2",
55-
// "SslProtocols": [ "Tls11", "Tls12", "Tls13"],
56-
// "ClientCertificateMode" : "NoCertificate"
55+
// "Protocols": "Http1AndHttp2",
56+
// "SslProtocols": [ "Tls11", "Tls12", "Tls13"],
57+
// "ClientCertificateMode" : "NoCertificate",
58+
// "Sni": {
59+
// "a.example.org": {
60+
// "Certificate": {
61+
// "Path": "testCertA.pfx",
62+
// "Password": "testPassword"
63+
// }
64+
// },
65+
// "*.example.org": {
66+
// "Protocols": "Http1",
67+
// }
68+
// }
5769
// }
5870
private EndpointDefaults ReadEndpointDefaults()
5971
{
@@ -62,7 +74,8 @@ private EndpointDefaults ReadEndpointDefaults()
6274
{
6375
Protocols = ParseProtocols(configSection[ProtocolsKey]),
6476
SslProtocols = ParseSslProcotols(configSection.GetSection(SslProtocolsKey)),
65-
ClientCertificateMode = ParseClientCertificateMode(configSection[ClientCertificateModeKey])
77+
ClientCertificateMode = ParseClientCertificateMode(configSection[ClientCertificateModeKey]),
78+
Sni = ReadSni(configSection.GetSection(SniKey), EndpointDefaultsKey)
6679
};
6780
}
6881

@@ -74,14 +87,25 @@ private IEnumerable<EndpointConfig> ReadEndpoints()
7487
foreach (var endpointConfig in endpointsConfig)
7588
{
7689
// "EndpointName": {
77-
// "Url": "https://*:5463",
78-
// "Protocols": "Http1AndHttp2",
79-
// "SslProtocols": [ "Tls11", "Tls12", "Tls13"],
80-
// "Certificate": {
81-
// "Path": "testCert.pfx",
82-
// "Password": "testPassword"
83-
// },
84-
// "ClientCertificateMode" : "NoCertificate"
90+
// "Url": "https://*:5463",
91+
// "Protocols": "Http1AndHttp2",
92+
// "SslProtocols": [ "Tls11", "Tls12", "Tls13"],
93+
// "Certificate": {
94+
// "Path": "testCert.pfx",
95+
// "Password": "testPassword"
96+
// },
97+
// "ClientCertificateMode" : "NoCertificate",
98+
// "Sni": {
99+
// "a.example.org": {
100+
// "Certificate": {
101+
// "Path": "testCertA.pfx",
102+
// "Password": "testPassword"
103+
// }
104+
// },
105+
// "*.example.org": {
106+
// "Protocols": "Http1",
107+
// }
108+
// }
85109
// }
86110

87111
var url = endpointConfig[UrlKey];
@@ -116,20 +140,22 @@ private Dictionary<string, SniConfig> ReadSni(IConfigurationSection sniConfig, s
116140
{
117141
// "Sni": {
118142
// "a.example.org": {
119-
// "Protocols": "Http1AndHttp2",
143+
// "Protocols": "Http1",
120144
// "SslProtocols": [ "Tls11", "Tls12", "Tls13"],
121145
// "Certificate": {
122-
// "Path": "testCert.pfx",
146+
// "Path": "testCertA.pfx",
123147
// "Password": "testPassword"
124148
// },
125149
// "ClientCertificateMode" : "NoCertificate"
126150
// },
127151
// "*.example.org": {
128152
// "Certificate": {
129-
// "Path": "testCert2.pfx",
153+
// "Path": "testCertWildcard.pfx",
130154
// "Password": "testPassword"
131155
// }
132156
// }
157+
// // The following should work once https://github.com/dotnet/runtime/issues/40218 is resolved
158+
// "*": {}
133159
// }
134160

135161
if (string.IsNullOrEmpty(sniChild.Key))
@@ -139,8 +165,8 @@ private Dictionary<string, SniConfig> ReadSni(IConfigurationSection sniConfig, s
139165

140166
var sni = new SniConfig
141167
{
142-
Protocols = ParseProtocols(sniChild[ProtocolsKey]),
143168
Certificate = new CertificateConfig(sniChild.GetSection(CertificateKey)),
169+
Protocols = ParseProtocols(sniChild[ProtocolsKey]),
144170
SslProtocols = ParseSslProcotols(sniChild.GetSection(SslProtocolsKey)),
145171
ClientCertificateMode = ParseClientCertificateMode(sniChild[ClientCertificateModeKey])
146172
};
@@ -189,44 +215,37 @@ private Dictionary<string, SniConfig> ReadSni(IConfigurationSection sniConfig, s
189215
}
190216

191217
// "EndpointDefaults": {
192-
// "Protocols": "Http1AndHttp2",
193-
// "SslProtocols": [ "Tls11", "Tls12", "Tls13"],
194-
// "ClientCertificateMode" : "NoCertificate"
218+
// "Protocols": "Http1AndHttp2",
219+
// "SslProtocols": [ "Tls11", "Tls12", "Tls13"],
220+
// "ClientCertificateMode" : "NoCertificate"
195221
// }
196222
internal class EndpointDefaults
197223
{
198224
public HttpProtocols? Protocols { get; set; }
199225
public SslProtocols? SslProtocols { get; set; }
200226
public ClientCertificateMode? ClientCertificateMode { get; set; }
227+
public Dictionary<string, SniConfig> Sni { get; set; }
201228
}
202229

203230
// "EndpointName": {
204-
// "Url": "https://*:5463",
205-
// "Protocols": "Http1AndHttp2",
206-
// "SslProtocols": [ "Tls11", "Tls12", "Tls13"],
207-
// "Certificate": {
208-
// "Path": "testCert.pfx",
209-
// "Password": "testPassword"
210-
// },
211-
// "ClientCertificateMode" : "NoCertificate"
231+
// "Url": "https://*:5463",
232+
// "Protocols": "Http1AndHttp2",
233+
// "SslProtocols": [ "Tls11", "Tls12", "Tls13"],
234+
// "Certificate": {
235+
// "Path": "testCert.pfx",
236+
// "Password": "testPassword"
237+
// },
238+
// "ClientCertificateMode" : "NoCertificate",
212239
// "Sni": {
213240
// "a.example.org": {
214-
// "Protocols": "Http1AndHttp2",
215-
// "SslProtocols": [ "Tls11", "Tls12", "Tls13"],
216-
// "Certificate": {
217-
// "Path": "testCert.pfx",
218-
// "Password": "testPassword"
219-
// },
220-
// "ClientCertificateMode" : "NoCertificate"
221-
// },
222-
// "*.example.org": {
223241
// "Certificate": {
224-
// "Path": "testCert2.pfx",
225-
// "Password": "testPassword"
242+
// "Path": "testCertA.pfx",
243+
// "Password": "testPasswordA"
226244
// }
227245
// },
228-
// // The following should work once https://github.com/dotnet/runtime/issues/40218 is resolved
229-
// "*": {}
246+
// "*.example.org": {
247+
// "Protocols": "Http1",
248+
// }
230249
// }
231250
// }
232251
internal class EndpointConfig
@@ -304,8 +323,8 @@ internal class SniConfig
304323
public override bool Equals(object obj) =>
305324
obj is SniConfig other &&
306325
(Protocols ?? ListenOptions.DefaultHttpProtocols) == (other.Protocols ?? ListenOptions.DefaultHttpProtocols) &&
307-
Certificate == other.Certificate &&
308326
(SslProtocols ?? System.Security.Authentication.SslProtocols.None) == (other.SslProtocols ?? System.Security.Authentication.SslProtocols.None) &&
327+
Certificate == other.Certificate &&
309328
(ClientCertificateMode ?? Https.ClientCertificateMode.NoCertificate) == (other.ClientCertificateMode ?? Https.ClientCertificateMode.NoCertificate);
310329

311330
public override int GetHashCode() => HashCode.Combine(
@@ -317,8 +336,8 @@ public override int GetHashCode() => HashCode.Combine(
317336
}
318337

319338
// "CertificateName": {
320-
// "Path": "testCert.pfx",
321-
// "Password": "testPassword"
339+
// "Path": "testCert.pfx",
340+
// "Password": "testPassword"
322341
// }
323342
internal class CertificateConfig
324343
{

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

Lines changed: 38 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@
88
using System.Net.Security;
99
using System.Security.Authentication;
1010
using System.Security.Cryptography.X509Certificates;
11+
using System.Threading;
12+
using System.Threading.Tasks;
1113
using Microsoft.AspNetCore.Connections;
1214
using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Certificates;
1315
using Microsoft.AspNetCore.Server.Kestrel.Https;
@@ -28,51 +30,58 @@ internal class SniOptionsSelector
2830

2931
private readonly Dictionary<string, SniOptions> _exactNameOptions = new Dictionary<string, SniOptions>(StringComparer.OrdinalIgnoreCase);
3032
private readonly SortedList<string, SniOptions> _wildcardPrefixOptions = new SortedList<string, SniOptions>(LongestStringFirstComparer.Instance);
31-
private readonly SniOptions _wildcardOptions = null;
33+
private readonly SniOptions _wildcardOptions;
3234

3335
public SniOptionsSelector(
36+
string endpointName,
37+
Dictionary<string, SniConfig> sniDictionary,
3438
ICertificateConfigLoader certifcateConfigLoader,
35-
EndpointConfig endpointConfig,
36-
HttpsConnectionAdapterOptions fallbackOptions,
39+
HttpsConnectionAdapterOptions fallbackHttpsOptions,
3740
HttpProtocols fallbackHttpProtocols,
3841
ILogger<HttpsConnectionMiddleware> logger)
3942
{
40-
_endpointName = endpointConfig.Name;
43+
_endpointName = endpointName;
4144

42-
_fallbackServerCertificateSelector = fallbackOptions.ServerCertificateSelector;
43-
_onAuthenticateCallback = fallbackOptions.OnAuthenticate;
45+
_fallbackServerCertificateSelector = fallbackHttpsOptions.ServerCertificateSelector;
46+
_onAuthenticateCallback = fallbackHttpsOptions.OnAuthenticate;
4447

45-
foreach (var (name, sniConfig) in endpointConfig.Sni)
48+
foreach (var (name, sniConfig) in sniDictionary)
4649
{
4750
var sslOptions = new SslServerAuthenticationOptions
4851
{
49-
ServerCertificate = certifcateConfigLoader.LoadCertificate(sniConfig.Certificate, $"{endpointConfig.Name}:Sni:{name}"),
50-
EnabledSslProtocols = sniConfig.SslProtocols ?? fallbackOptions.SslProtocols,
51-
CertificateRevocationCheckMode = fallbackOptions.CheckCertificateRevocation ? X509RevocationMode.Online : X509RevocationMode.NoCheck,
52+
ServerCertificate = certifcateConfigLoader.LoadCertificate(sniConfig.Certificate, $"{endpointName}:Sni:{name}"),
53+
EnabledSslProtocols = sniConfig.SslProtocols ?? fallbackHttpsOptions.SslProtocols,
54+
CertificateRevocationCheckMode = fallbackHttpsOptions.CheckCertificateRevocation ? X509RevocationMode.Online : X509RevocationMode.NoCheck,
5255
};
5356

5457
if (sslOptions.ServerCertificate is null)
5558
{
56-
if (fallbackOptions.ServerCertificate is null && _fallbackServerCertificateSelector is null)
59+
if (fallbackHttpsOptions.ServerCertificate is null && _fallbackServerCertificateSelector is null)
5760
{
5861
throw new InvalidOperationException(CoreStrings.NoCertSpecifiedNoDevelopmentCertificateFound);
5962
}
6063

6164
if (_fallbackServerCertificateSelector is null)
6265
{
6366
// Cache the fallback ServerCertificate since there's no fallback ServerCertificateSelector taking precedence.
64-
sslOptions.ServerCertificate = fallbackOptions.ServerCertificate;
67+
sslOptions.ServerCertificate = fallbackHttpsOptions.ServerCertificate;
6568
}
6669
}
6770

68-
var clientCertificateMode = sniConfig.ClientCertificateMode ?? fallbackOptions.ClientCertificateMode;
71+
// SkipValidation is only true in tests.
72+
if (!certifcateConfigLoader.SkipValidation && sslOptions.ServerCertificate is X509Certificate2 cert2)
73+
{
74+
HttpsConnectionMiddleware.EnsureCertificateIsAllowedForServerAuth(cert2);
75+
}
76+
77+
var clientCertificateMode = sniConfig.ClientCertificateMode ?? fallbackHttpsOptions.ClientCertificateMode;
6978

7079
if (clientCertificateMode != ClientCertificateMode.NoCertificate)
7180
{
7281
sslOptions.ClientCertificateRequired = true;
7382
sslOptions.RemoteCertificateValidationCallback = (sender, certificate, chain, sslPolicyErrors) =>
7483
HttpsConnectionMiddleware.RemoteCertificateValidationCallback(
75-
clientCertificateMode, fallbackOptions.ClientCertificateValidation, certificate, chain, sslPolicyErrors);
84+
clientCertificateMode, fallbackHttpsOptions.ClientCertificateValidation, certificate, chain, sslPolicyErrors);
7685
}
7786

7887
var httpProtocols = sniConfig.Protocols ?? fallbackHttpProtocols;
@@ -144,7 +153,14 @@ public SslServerAuthenticationOptions GetOptions(ConnectionContext connection, s
144153

145154
// If a ServerCertificateSelector doesn't return a cert, HttpsConnectionMiddleware doesn't fallback to the ServerCertificate.
146155
sslOptions = CloneSslOptions(sslOptions);
147-
sslOptions.ServerCertificate = _fallbackServerCertificateSelector(connection, serverName);
156+
var fallbackCertificate = _fallbackServerCertificateSelector(connection, serverName);
157+
158+
if (fallbackCertificate != null)
159+
{
160+
HttpsConnectionMiddleware.EnsureCertificateIsAllowedForServerAuth(fallbackCertificate);
161+
}
162+
163+
sslOptions.ServerCertificate = fallbackCertificate;
148164
}
149165

150166
if (_onAuthenticateCallback != null)
@@ -157,6 +173,13 @@ public SslServerAuthenticationOptions GetOptions(ConnectionContext connection, s
157173
return sslOptions;
158174
}
159175

176+
public static ValueTask<SslServerAuthenticationOptions> OptionsCallback(ConnectionContext connection, SslStream stream, SslClientHelloInfo clientHelloInfo, object state, CancellationToken cancellationToken)
177+
{
178+
var sniOptionsSelector = (SniOptionsSelector)state;
179+
var options = sniOptionsSelector.GetOptions(connection, clientHelloInfo.ServerName);
180+
return new ValueTask<SslServerAuthenticationOptions>(options);
181+
}
182+
160183
// TODO: Reflection based test to ensure we clone everything!
161184
// This won't catch issues related to mutable subproperties, but the existing subproperties look like they're mostly immutable.
162185
// The exception are the ApplicationProtocols list which we clone and the ServerCertificate because of methods like Import() and Reset() :(

0 commit comments

Comments
 (0)