Skip to content

Commit 292cb9c

Browse files
authored
Kestrel SNI from config (#24286)
1 parent c9064a9 commit 292cb9c

21 files changed

+2029
-397
lines changed

src/Http/Routing/src/Matching/HostMatcherPolicy.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -118,7 +118,7 @@ public Task ApplyAsync(HttpContext httpContext, CandidateSet candidates)
118118
else if (
119119
host.StartsWith(WildcardPrefix) &&
120120

121-
// Note that we only slice of the `*`. We want to match the leading `.` also.
121+
// Note that we only slice off the `*`. We want to match the leading `.` also.
122122
MemoryExtensions.EndsWith(requestHost, host.Slice(WildcardHost.Length), StringComparison.OrdinalIgnoreCase))
123123
{
124124
// Matches a suffix wildcard.

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

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -620,4 +620,16 @@ 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 section 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 refused because the client did not specify a server name, and no wildcard ('*') SNI configuration section was found in '{endpointName}'.</value>
628+
</data>
629+
<data name="SniNameCannotBeEmpty" xml:space="preserve">
630+
<value>The endpoint {endpointName} is invalid because an SNI configuration section has an empty string as its key. Use a wildcard ('*') SNI section to match all server names.</value>
631+
</data>
632+
<data name="EndpointHasUnusedHttpsConfig" xml:space="preserve">
633+
<value>The non-HTTPS endpoint {endpointName} includes HTTPS-only configuration for {keyName}.</value>
634+
</data>
623635
</root>

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

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Https
1616
/// </summary>
1717
public class HttpsConnectionAdapterOptions
1818
{
19+
internal static TimeSpan DefaultHandshakeTimeout = TimeSpan.FromSeconds(10);
20+
1921
private TimeSpan _handshakeTimeout;
2022

2123
/// <summary>
@@ -24,7 +26,7 @@ public class HttpsConnectionAdapterOptions
2426
public HttpsConnectionAdapterOptions()
2527
{
2628
ClientCertificateMode = ClientCertificateMode.NoCertificate;
27-
HandshakeTimeout = TimeSpan.FromSeconds(10);
29+
HandshakeTimeout = DefaultHandshakeTimeout;
2830
}
2931

3032
/// <summary>
@@ -91,7 +93,7 @@ public void AllowAnyClientCertificate()
9193
public Action<ConnectionContext, SslServerAuthenticationOptions> OnAuthenticate { get; set; }
9294

9395
/// <summary>
94-
/// Specifies the maximum amount of time allowed for the TLS/SSL handshake. This must be positive and finite.
96+
/// Specifies the maximum amount of time allowed for the TLS/SSL handshake. This must be positive and finite. Defaults to 10 seconds.
9597
/// </summary>
9698
public TimeSpan HandshakeTimeout
9799
{
Lines changed: 178 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,178 @@
1+
// Copyright (c) Microsoft. All rights reserved.
2+
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
3+
4+
using System;
5+
using System.IO;
6+
using System.Runtime.InteropServices;
7+
using System.Security.Cryptography;
8+
using System.Security.Cryptography.X509Certificates;
9+
using Microsoft.AspNetCore.Server.Kestrel.Https;
10+
using Microsoft.Extensions.Hosting;
11+
using Microsoft.Extensions.Logging;
12+
13+
namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Certificates
14+
{
15+
internal class CertificateConfigLoader : ICertificateConfigLoader
16+
{
17+
public CertificateConfigLoader(IHostEnvironment hostEnvironment, ILogger<KestrelServer> logger)
18+
{
19+
HostEnvironment = hostEnvironment;
20+
Logger = logger;
21+
}
22+
23+
public IHostEnvironment HostEnvironment { get; }
24+
public ILogger<KestrelServer> Logger { get; }
25+
26+
public bool IsTestMock => false;
27+
28+
public X509Certificate2 LoadCertificate(CertificateConfig certInfo, string endpointName)
29+
{
30+
if (certInfo is null)
31+
{
32+
return null;
33+
}
34+
35+
if (certInfo.IsFileCert && certInfo.IsStoreCert)
36+
{
37+
throw new InvalidOperationException(CoreStrings.FormatMultipleCertificateSources(endpointName));
38+
}
39+
else if (certInfo.IsFileCert)
40+
{
41+
var certificatePath = Path.Combine(HostEnvironment.ContentRootPath, certInfo.Path);
42+
if (certInfo.KeyPath != null)
43+
{
44+
var certificateKeyPath = Path.Combine(HostEnvironment.ContentRootPath, certInfo.KeyPath);
45+
var certificate = GetCertificate(certificatePath);
46+
47+
if (certificate != null)
48+
{
49+
certificate = LoadCertificateKey(certificate, certificateKeyPath, certInfo.Password);
50+
}
51+
else
52+
{
53+
Logger.FailedToLoadCertificate(certificateKeyPath);
54+
}
55+
56+
if (certificate != null)
57+
{
58+
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
59+
{
60+
return PersistKey(certificate);
61+
}
62+
63+
return certificate;
64+
}
65+
else
66+
{
67+
Logger.FailedToLoadCertificateKey(certificateKeyPath);
68+
}
69+
70+
throw new InvalidOperationException(CoreStrings.InvalidPemKey);
71+
}
72+
73+
return new X509Certificate2(Path.Combine(HostEnvironment.ContentRootPath, certInfo.Path), certInfo.Password);
74+
}
75+
else if (certInfo.IsStoreCert)
76+
{
77+
return LoadFromStoreCert(certInfo);
78+
}
79+
80+
return null;
81+
}
82+
83+
private static X509Certificate2 PersistKey(X509Certificate2 fullCertificate)
84+
{
85+
// We need to force the key to be persisted.
86+
// See https://github.com/dotnet/runtime/issues/23749
87+
var certificateBytes = fullCertificate.Export(X509ContentType.Pkcs12, "");
88+
return new X509Certificate2(certificateBytes, "", X509KeyStorageFlags.DefaultKeySet);
89+
}
90+
91+
private static X509Certificate2 LoadCertificateKey(X509Certificate2 certificate, string keyPath, string password)
92+
{
93+
// OIDs for the certificate key types.
94+
const string RSAOid = "1.2.840.113549.1.1.1";
95+
const string DSAOid = "1.2.840.10040.4.1";
96+
const string ECDsaOid = "1.2.840.10045.2.1";
97+
98+
var keyText = File.ReadAllText(keyPath);
99+
return certificate.PublicKey.Oid.Value switch
100+
{
101+
RSAOid => AttachPemRSAKey(certificate, keyText, password),
102+
ECDsaOid => AttachPemECDSAKey(certificate, keyText, password),
103+
DSAOid => AttachPemDSAKey(certificate, keyText, password),
104+
_ => throw new InvalidOperationException(string.Format(CoreStrings.UnrecognizedCertificateKeyOid, certificate.PublicKey.Oid.Value))
105+
};
106+
}
107+
108+
private static X509Certificate2 GetCertificate(string certificatePath)
109+
{
110+
if (X509Certificate2.GetCertContentType(certificatePath) == X509ContentType.Cert)
111+
{
112+
return new X509Certificate2(certificatePath);
113+
}
114+
115+
return null;
116+
}
117+
118+
private static X509Certificate2 AttachPemRSAKey(X509Certificate2 certificate, string keyText, string password)
119+
{
120+
using var rsa = RSA.Create();
121+
if (password == null)
122+
{
123+
rsa.ImportFromPem(keyText);
124+
}
125+
else
126+
{
127+
rsa.ImportFromEncryptedPem(keyText, password);
128+
}
129+
130+
return certificate.CopyWithPrivateKey(rsa);
131+
}
132+
133+
private static X509Certificate2 AttachPemDSAKey(X509Certificate2 certificate, string keyText, string password)
134+
{
135+
using var dsa = DSA.Create();
136+
if (password == null)
137+
{
138+
dsa.ImportFromPem(keyText);
139+
}
140+
else
141+
{
142+
dsa.ImportFromEncryptedPem(keyText, password);
143+
}
144+
145+
return certificate.CopyWithPrivateKey(dsa);
146+
}
147+
148+
private static X509Certificate2 AttachPemECDSAKey(X509Certificate2 certificate, string keyText, string password)
149+
{
150+
using var ecdsa = ECDsa.Create();
151+
if (password == null)
152+
{
153+
ecdsa.ImportFromPem(keyText);
154+
}
155+
else
156+
{
157+
ecdsa.ImportFromEncryptedPem(keyText, password);
158+
}
159+
160+
return certificate.CopyWithPrivateKey(ecdsa);
161+
}
162+
163+
private static X509Certificate2 LoadFromStoreCert(CertificateConfig certInfo)
164+
{
165+
var subject = certInfo.Subject;
166+
var storeName = string.IsNullOrEmpty(certInfo.Store) ? StoreName.My.ToString() : certInfo.Store;
167+
var location = certInfo.Location;
168+
var storeLocation = StoreLocation.CurrentUser;
169+
if (!string.IsNullOrEmpty(location))
170+
{
171+
storeLocation = (StoreLocation)Enum.Parse(typeof(StoreLocation), location, ignoreCase: true);
172+
}
173+
var allowInvalid = certInfo.AllowInvalid ?? false;
174+
175+
return CertificateLoader.LoadFromStoreCert(subject, storeName, storeLocation, allowInvalid);
176+
}
177+
}
178+
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
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.Security.Cryptography.X509Certificates;
5+
6+
namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Certificates
7+
{
8+
internal interface ICertificateConfigLoader
9+
{
10+
bool IsTestMock { get; }
11+
12+
X509Certificate2 LoadCertificate(CertificateConfig certInfo, string endpointName);
13+
}
14+
}

0 commit comments

Comments
 (0)