diff --git a/src/Http/Routing/src/Matching/HostMatcherPolicy.cs b/src/Http/Routing/src/Matching/HostMatcherPolicy.cs
index d0f29612e469..2a3047489ebf 100644
--- a/src/Http/Routing/src/Matching/HostMatcherPolicy.cs
+++ b/src/Http/Routing/src/Matching/HostMatcherPolicy.cs
@@ -118,7 +118,7 @@ public Task ApplyAsync(HttpContext httpContext, CandidateSet candidates)
else if (
host.StartsWith(WildcardPrefix) &&
- // Note that we only slice of the `*`. We want to match the leading `.` also.
+ // Note that we only slice off the `*`. We want to match the leading `.` also.
MemoryExtensions.EndsWith(requestHost, host.Slice(WildcardHost.Length), StringComparison.OrdinalIgnoreCase))
{
// Matches a suffix wildcard.
diff --git a/src/Servers/Kestrel/Core/src/CoreStrings.resx b/src/Servers/Kestrel/Core/src/CoreStrings.resx
index 49c2f5fb482c..1dbd5c043564 100644
--- a/src/Servers/Kestrel/Core/src/CoreStrings.resx
+++ b/src/Servers/Kestrel/Core/src/CoreStrings.resx
@@ -620,4 +620,16 @@ For more information on configuring HTTPS see https://go.microsoft.com/fwlink/?l
Unknown algorithm for certificate with public key type '{0}'.
+
+ Connection refused because no SNI configuration section was found for '{serverName}' in '{endpointName}'. To allow all connections, add a wildcard ('*') SNI section.
+
+
+ Connection refused because the client did not specify a server name, and no wildcard ('*') SNI configuration section was found in '{endpointName}'.
+
+
+ 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.
+
+
+ The non-HTTPS endpoint {endpointName} includes HTTPS-only configuration for {keyName}.
+
\ No newline at end of file
diff --git a/src/Servers/Kestrel/Core/src/HttpsConnectionAdapterOptions.cs b/src/Servers/Kestrel/Core/src/HttpsConnectionAdapterOptions.cs
index d801316b5f9c..9961101484ca 100644
--- a/src/Servers/Kestrel/Core/src/HttpsConnectionAdapterOptions.cs
+++ b/src/Servers/Kestrel/Core/src/HttpsConnectionAdapterOptions.cs
@@ -16,6 +16,8 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Https
///
public class HttpsConnectionAdapterOptions
{
+ internal static TimeSpan DefaultHandshakeTimeout = TimeSpan.FromSeconds(10);
+
private TimeSpan _handshakeTimeout;
///
@@ -24,7 +26,7 @@ public class HttpsConnectionAdapterOptions
public HttpsConnectionAdapterOptions()
{
ClientCertificateMode = ClientCertificateMode.NoCertificate;
- HandshakeTimeout = TimeSpan.FromSeconds(10);
+ HandshakeTimeout = DefaultHandshakeTimeout;
}
///
@@ -91,7 +93,7 @@ public void AllowAnyClientCertificate()
public Action OnAuthenticate { get; set; }
///
- /// Specifies the maximum amount of time allowed for the TLS/SSL handshake. This must be positive and finite.
+ /// Specifies the maximum amount of time allowed for the TLS/SSL handshake. This must be positive and finite. Defaults to 10 seconds.
///
public TimeSpan HandshakeTimeout
{
diff --git a/src/Servers/Kestrel/Core/src/Internal/Certificates/CertificateConfigLoader.cs b/src/Servers/Kestrel/Core/src/Internal/Certificates/CertificateConfigLoader.cs
new file mode 100644
index 000000000000..f2663b9fbcd1
--- /dev/null
+++ b/src/Servers/Kestrel/Core/src/Internal/Certificates/CertificateConfigLoader.cs
@@ -0,0 +1,178 @@
+// Copyright (c) Microsoft. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+using System;
+using System.IO;
+using System.Runtime.InteropServices;
+using System.Security.Cryptography;
+using System.Security.Cryptography.X509Certificates;
+using Microsoft.AspNetCore.Server.Kestrel.Https;
+using Microsoft.Extensions.Hosting;
+using Microsoft.Extensions.Logging;
+
+namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Certificates
+{
+ internal class CertificateConfigLoader : ICertificateConfigLoader
+ {
+ public CertificateConfigLoader(IHostEnvironment hostEnvironment, ILogger logger)
+ {
+ HostEnvironment = hostEnvironment;
+ Logger = logger;
+ }
+
+ public IHostEnvironment HostEnvironment { get; }
+ public ILogger Logger { get; }
+
+ public bool IsTestMock => false;
+
+ public X509Certificate2 LoadCertificate(CertificateConfig certInfo, string endpointName)
+ {
+ if (certInfo is null)
+ {
+ return null;
+ }
+
+ if (certInfo.IsFileCert && certInfo.IsStoreCert)
+ {
+ throw new InvalidOperationException(CoreStrings.FormatMultipleCertificateSources(endpointName));
+ }
+ else if (certInfo.IsFileCert)
+ {
+ var certificatePath = Path.Combine(HostEnvironment.ContentRootPath, certInfo.Path);
+ if (certInfo.KeyPath != null)
+ {
+ var certificateKeyPath = Path.Combine(HostEnvironment.ContentRootPath, certInfo.KeyPath);
+ var certificate = GetCertificate(certificatePath);
+
+ if (certificate != null)
+ {
+ certificate = LoadCertificateKey(certificate, certificateKeyPath, certInfo.Password);
+ }
+ else
+ {
+ Logger.FailedToLoadCertificate(certificateKeyPath);
+ }
+
+ if (certificate != null)
+ {
+ if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
+ {
+ return PersistKey(certificate);
+ }
+
+ return certificate;
+ }
+ else
+ {
+ Logger.FailedToLoadCertificateKey(certificateKeyPath);
+ }
+
+ throw new InvalidOperationException(CoreStrings.InvalidPemKey);
+ }
+
+ return new X509Certificate2(Path.Combine(HostEnvironment.ContentRootPath, certInfo.Path), certInfo.Password);
+ }
+ else if (certInfo.IsStoreCert)
+ {
+ return LoadFromStoreCert(certInfo);
+ }
+
+ return null;
+ }
+
+ private static X509Certificate2 PersistKey(X509Certificate2 fullCertificate)
+ {
+ // We need to force the key to be persisted.
+ // See https://github.com/dotnet/runtime/issues/23749
+ var certificateBytes = fullCertificate.Export(X509ContentType.Pkcs12, "");
+ return new X509Certificate2(certificateBytes, "", X509KeyStorageFlags.DefaultKeySet);
+ }
+
+ private static X509Certificate2 LoadCertificateKey(X509Certificate2 certificate, string keyPath, string password)
+ {
+ // OIDs for the certificate key types.
+ const string RSAOid = "1.2.840.113549.1.1.1";
+ const string DSAOid = "1.2.840.10040.4.1";
+ const string ECDsaOid = "1.2.840.10045.2.1";
+
+ var keyText = File.ReadAllText(keyPath);
+ return certificate.PublicKey.Oid.Value switch
+ {
+ RSAOid => AttachPemRSAKey(certificate, keyText, password),
+ ECDsaOid => AttachPemECDSAKey(certificate, keyText, password),
+ DSAOid => AttachPemDSAKey(certificate, keyText, password),
+ _ => throw new InvalidOperationException(string.Format(CoreStrings.UnrecognizedCertificateKeyOid, certificate.PublicKey.Oid.Value))
+ };
+ }
+
+ private static X509Certificate2 GetCertificate(string certificatePath)
+ {
+ if (X509Certificate2.GetCertContentType(certificatePath) == X509ContentType.Cert)
+ {
+ return new X509Certificate2(certificatePath);
+ }
+
+ return null;
+ }
+
+ private static X509Certificate2 AttachPemRSAKey(X509Certificate2 certificate, string keyText, string password)
+ {
+ using var rsa = RSA.Create();
+ if (password == null)
+ {
+ rsa.ImportFromPem(keyText);
+ }
+ else
+ {
+ rsa.ImportFromEncryptedPem(keyText, password);
+ }
+
+ return certificate.CopyWithPrivateKey(rsa);
+ }
+
+ private static X509Certificate2 AttachPemDSAKey(X509Certificate2 certificate, string keyText, string password)
+ {
+ using var dsa = DSA.Create();
+ if (password == null)
+ {
+ dsa.ImportFromPem(keyText);
+ }
+ else
+ {
+ dsa.ImportFromEncryptedPem(keyText, password);
+ }
+
+ return certificate.CopyWithPrivateKey(dsa);
+ }
+
+ private static X509Certificate2 AttachPemECDSAKey(X509Certificate2 certificate, string keyText, string password)
+ {
+ using var ecdsa = ECDsa.Create();
+ if (password == null)
+ {
+ ecdsa.ImportFromPem(keyText);
+ }
+ else
+ {
+ ecdsa.ImportFromEncryptedPem(keyText, password);
+ }
+
+ return certificate.CopyWithPrivateKey(ecdsa);
+ }
+
+ private static X509Certificate2 LoadFromStoreCert(CertificateConfig certInfo)
+ {
+ var subject = certInfo.Subject;
+ var storeName = string.IsNullOrEmpty(certInfo.Store) ? StoreName.My.ToString() : certInfo.Store;
+ var location = certInfo.Location;
+ var storeLocation = StoreLocation.CurrentUser;
+ if (!string.IsNullOrEmpty(location))
+ {
+ storeLocation = (StoreLocation)Enum.Parse(typeof(StoreLocation), location, ignoreCase: true);
+ }
+ var allowInvalid = certInfo.AllowInvalid ?? false;
+
+ return CertificateLoader.LoadFromStoreCert(subject, storeName, storeLocation, allowInvalid);
+ }
+ }
+}
diff --git a/src/Servers/Kestrel/Core/src/Internal/Certificates/ICertificateConfigLoader.cs b/src/Servers/Kestrel/Core/src/Internal/Certificates/ICertificateConfigLoader.cs
new file mode 100644
index 000000000000..53cf84f42b9c
--- /dev/null
+++ b/src/Servers/Kestrel/Core/src/Internal/Certificates/ICertificateConfigLoader.cs
@@ -0,0 +1,14 @@
+// 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.Security.Cryptography.X509Certificates;
+
+namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Certificates
+{
+ internal interface ICertificateConfigLoader
+ {
+ bool IsTestMock { get; }
+
+ X509Certificate2 LoadCertificate(CertificateConfig certInfo, string endpointName);
+ }
+}
diff --git a/src/Servers/Kestrel/Core/src/Internal/ConfigurationReader.cs b/src/Servers/Kestrel/Core/src/Internal/ConfigurationReader.cs
index 894cd0fa01ed..06ca82a8a947 100644
--- a/src/Servers/Kestrel/Core/src/Internal/ConfigurationReader.cs
+++ b/src/Servers/Kestrel/Core/src/Internal/ConfigurationReader.cs
@@ -20,6 +20,7 @@ internal class ConfigurationReader
private const string EndpointsKey = "Endpoints";
private const string UrlKey = "Url";
private const string ClientCertificateModeKey = "ClientCertificateMode";
+ private const string SniKey = "Sni";
private readonly IConfiguration _configuration;
@@ -50,9 +51,9 @@ private IDictionary ReadCertificates()
}
// "EndpointDefaults": {
- // "Protocols": "Http1AndHttp2",
- // "SslProtocols": [ "Tls11", "Tls12", "Tls13"],
- // "ClientCertificateMode" : "NoCertificate"
+ // "Protocols": "Http1AndHttp2",
+ // "SslProtocols": [ "Tls11", "Tls12", "Tls13"],
+ // "ClientCertificateMode" : "NoCertificate"
// }
private EndpointDefaults ReadEndpointDefaults()
{
@@ -61,7 +62,7 @@ private EndpointDefaults ReadEndpointDefaults()
{
Protocols = ParseProtocols(configSection[ProtocolsKey]),
SslProtocols = ParseSslProcotols(configSection.GetSection(SslProtocolsKey)),
- ClientCertificateMode = ParseClientCertificateMode(configSection[ClientCertificateModeKey])
+ ClientCertificateMode = ParseClientCertificateMode(configSection[ClientCertificateModeKey]),
};
}
@@ -73,14 +74,25 @@ private IEnumerable ReadEndpoints()
foreach (var endpointConfig in endpointsConfig)
{
// "EndpointName": {
- // "Url": "https://*:5463",
- // "Protocols": "Http1AndHttp2",
- // "SslProtocols": [ "Tls11", "Tls12", "Tls13"],
- // "Certificate": {
- // "Path": "testCert.pfx",
- // "Password": "testPassword"
- // },
- // "ClientCertificateMode" : "NoCertificate"
+ // "Url": "https://*:5463",
+ // "Protocols": "Http1AndHttp2",
+ // "SslProtocols": [ "Tls11", "Tls12", "Tls13"],
+ // "Certificate": {
+ // "Path": "testCert.pfx",
+ // "Password": "testPassword"
+ // },
+ // "ClientCertificateMode" : "NoCertificate",
+ // "Sni": {
+ // "a.example.org": {
+ // "Certificate": {
+ // "Path": "testCertA.pfx",
+ // "Password": "testPassword"
+ // }
+ // },
+ // "*.example.org": {
+ // "Protocols": "Http1",
+ // }
+ // }
// }
var url = endpointConfig[UrlKey];
@@ -97,7 +109,8 @@ private IEnumerable ReadEndpoints()
ConfigSection = endpointConfig,
Certificate = new CertificateConfig(endpointConfig.GetSection(CertificateKey)),
SslProtocols = ParseSslProcotols(endpointConfig.GetSection(SslProtocolsKey)),
- ClientCertificateMode = ParseClientCertificateMode(endpointConfig[ClientCertificateModeKey])
+ ClientCertificateMode = ParseClientCertificateMode(endpointConfig[ClientCertificateModeKey]),
+ Sni = ReadSni(endpointConfig.GetSection(SniKey), endpointConfig.Key),
};
endpoints.Add(endpoint);
@@ -106,7 +119,53 @@ private IEnumerable ReadEndpoints()
return endpoints;
}
- private ClientCertificateMode? ParseClientCertificateMode(string clientCertificateMode)
+ private static Dictionary ReadSni(IConfigurationSection sniConfig, string endpointName)
+ {
+ var sniDictionary = new Dictionary(0, StringComparer.OrdinalIgnoreCase);
+
+ foreach (var sniChild in sniConfig.GetChildren())
+ {
+ // "Sni": {
+ // "a.example.org": {
+ // "Protocols": "Http1",
+ // "SslProtocols": [ "Tls11", "Tls12", "Tls13"],
+ // "Certificate": {
+ // "Path": "testCertA.pfx",
+ // "Password": "testPassword"
+ // },
+ // "ClientCertificateMode" : "NoCertificate"
+ // },
+ // "*.example.org": {
+ // "Certificate": {
+ // "Path": "testCertWildcard.pfx",
+ // "Password": "testPassword"
+ // }
+ // }
+ // // The following should work once https://github.com/dotnet/runtime/issues/40218 is resolved
+ // "*": {}
+ // }
+
+ if (string.IsNullOrEmpty(sniChild.Key))
+ {
+ throw new InvalidOperationException(CoreStrings.FormatSniNameCannotBeEmpty(endpointName));
+ }
+
+ var sni = new SniConfig
+ {
+ Certificate = new CertificateConfig(sniChild.GetSection(CertificateKey)),
+ Protocols = ParseProtocols(sniChild[ProtocolsKey]),
+ SslProtocols = ParseSslProcotols(sniChild.GetSection(SslProtocolsKey)),
+ ClientCertificateMode = ParseClientCertificateMode(sniChild[ClientCertificateModeKey])
+ };
+
+ sniDictionary.Add(sniChild.Key, sni);
+ }
+
+ return sniDictionary;
+ }
+
+
+ private static ClientCertificateMode? ParseClientCertificateMode(string clientCertificateMode)
{
if (Enum.TryParse(clientCertificateMode, ignoreCase: true, out var result))
{
@@ -140,12 +199,35 @@ private IEnumerable ReadEndpoints()
return acc;
});
}
+
+ internal static void ThrowIfContainsHttpsOnlyConfiguration(EndpointConfig endpoint)
+ {
+ if (endpoint.Certificate.IsFileCert || endpoint.Certificate.IsStoreCert)
+ {
+ throw new InvalidOperationException(CoreStrings.FormatEndpointHasUnusedHttpsConfig(endpoint.Name, CertificateKey));
+ }
+
+ if (endpoint.ClientCertificateMode.HasValue)
+ {
+ throw new InvalidOperationException(CoreStrings.FormatEndpointHasUnusedHttpsConfig(endpoint.Name, ClientCertificateModeKey));
+ }
+
+ if (endpoint.SslProtocols.HasValue)
+ {
+ throw new InvalidOperationException(CoreStrings.FormatEndpointHasUnusedHttpsConfig(endpoint.Name, SslProtocolsKey));
+ }
+
+ if (endpoint.Sni.Count > 0)
+ {
+ throw new InvalidOperationException(CoreStrings.FormatEndpointHasUnusedHttpsConfig(endpoint.Name, SniKey));
+ }
+ }
}
// "EndpointDefaults": {
- // "Protocols": "Http1AndHttp2",
- // "SslProtocols": [ "Tls11", "Tls12", "Tls13"],
- // "ClientCertificateMode" : "NoCertificate"
+ // "Protocols": "Http1AndHttp2",
+ // "SslProtocols": [ "Tls11", "Tls12", "Tls13"],
+ // "ClientCertificateMode" : "NoCertificate"
// }
internal class EndpointDefaults
{
@@ -155,14 +237,25 @@ internal class EndpointDefaults
}
// "EndpointName": {
- // "Url": "https://*:5463",
- // "Protocols": "Http1AndHttp2",
- // "SslProtocols": [ "Tls11", "Tls12", "Tls13"],
- // "Certificate": {
- // "Path": "testCert.pfx",
- // "Password": "testPassword"
- // },
- // "ClientCertificateMode" : "NoCertificate"
+ // "Url": "https://*:5463",
+ // "Protocols": "Http1AndHttp2",
+ // "SslProtocols": [ "Tls11", "Tls12", "Tls13"],
+ // "Certificate": {
+ // "Path": "testCert.pfx",
+ // "Password": "testPassword"
+ // },
+ // "ClientCertificateMode" : "NoCertificate",
+ // "Sni": {
+ // "a.example.org": {
+ // "Certificate": {
+ // "Path": "testCertA.pfx",
+ // "Password": "testPasswordA"
+ // }
+ // },
+ // "*.example.org": {
+ // "Protocols": "Http1",
+ // }
+ // }
// }
internal class EndpointConfig
{
@@ -175,6 +268,7 @@ internal class EndpointConfig
public SslProtocols? SslProtocols { get; set; }
public CertificateConfig Certificate { get; set; }
public ClientCertificateMode? ClientCertificateMode { get; set; }
+ public Dictionary Sni { get; set; }
// Compare config sections because it's accessible to app developers via an Action callback.
// We cannot rely entirely on comparing config sections for equality, because KestrelConfigurationLoader.Reload() sets
@@ -196,19 +290,63 @@ obj is EndpointConfig other &&
Name == other.Name &&
Url == other.Url &&
(Protocols ?? ListenOptions.DefaultHttpProtocols) == (other.Protocols ?? ListenOptions.DefaultHttpProtocols) &&
- Certificate == other.Certificate &&
(SslProtocols ?? System.Security.Authentication.SslProtocols.None) == (other.SslProtocols ?? System.Security.Authentication.SslProtocols.None) &&
+ Certificate == other.Certificate &&
+ (ClientCertificateMode ?? Https.ClientCertificateMode.NoCertificate) == (other.ClientCertificateMode ?? Https.ClientCertificateMode.NoCertificate) &&
+ CompareSniDictionaries(Sni, other.Sni) &&
_configSectionClone == other._configSectionClone;
- public override int GetHashCode() => HashCode.Combine(Name, Url, Protocols ?? ListenOptions.DefaultHttpProtocols, Certificate, _configSectionClone);
+ public override int GetHashCode() => HashCode.Combine(Name, Url,
+ Protocols ?? ListenOptions.DefaultHttpProtocols, SslProtocols ?? System.Security.Authentication.SslProtocols.None,
+ Certificate, ClientCertificateMode ?? Https.ClientCertificateMode.NoCertificate, Sni.Count, _configSectionClone);
public static bool operator ==(EndpointConfig lhs, EndpointConfig rhs) => lhs is null ? rhs is null : lhs.Equals(rhs);
public static bool operator !=(EndpointConfig lhs, EndpointConfig rhs) => !(lhs == rhs);
+
+ private static bool CompareSniDictionaries(Dictionary lhs, Dictionary rhs)
+ {
+ if (lhs.Count != rhs.Count)
+ {
+ return false;
+ }
+
+ foreach (var (lhsName, lhsSniConfig) in lhs)
+ {
+ if (!rhs.TryGetValue(lhsName, out var rhsSniConfig) || lhsSniConfig != rhsSniConfig)
+ {
+ return false;
+ }
+ }
+
+ return true;
+ }
+ }
+
+ internal class SniConfig
+ {
+ public HttpProtocols? Protocols { get; set; }
+ public SslProtocols? SslProtocols { get; set; }
+ public CertificateConfig Certificate { get; set; }
+ public ClientCertificateMode? ClientCertificateMode { get; set; }
+
+ public override bool Equals(object obj) =>
+ obj is SniConfig other &&
+ (Protocols ?? ListenOptions.DefaultHttpProtocols) == (other.Protocols ?? ListenOptions.DefaultHttpProtocols) &&
+ (SslProtocols ?? System.Security.Authentication.SslProtocols.None) == (other.SslProtocols ?? System.Security.Authentication.SslProtocols.None) &&
+ Certificate == other.Certificate &&
+ (ClientCertificateMode ?? Https.ClientCertificateMode.NoCertificate) == (other.ClientCertificateMode ?? Https.ClientCertificateMode.NoCertificate);
+
+ public override int GetHashCode() => HashCode.Combine(
+ Protocols ?? ListenOptions.DefaultHttpProtocols, SslProtocols ?? System.Security.Authentication.SslProtocols.None,
+ Certificate, ClientCertificateMode ?? Https.ClientCertificateMode.NoCertificate);
+
+ public static bool operator ==(SniConfig lhs, SniConfig rhs) => lhs is null ? rhs is null : lhs.Equals(rhs);
+ public static bool operator !=(SniConfig lhs, SniConfig rhs) => !(lhs == rhs);
}
// "CertificateName": {
- // "Path": "testCert.pfx",
- // "Password": "testPassword"
+ // "Path": "testCert.pfx",
+ // "Password": "testPassword"
// }
internal class CertificateConfig
{
@@ -218,6 +356,11 @@ public CertificateConfig(IConfigurationSection configSection)
ConfigSection.Bind(this);
}
+ // For testing
+ internal CertificateConfig()
+ {
+ }
+
public IConfigurationSection ConfigSection { get; }
// File
@@ -244,13 +387,14 @@ public CertificateConfig(IConfigurationSection configSection)
public override bool Equals(object obj) =>
obj is CertificateConfig other &&
Path == other.Path &&
+ KeyPath == other.KeyPath &&
Password == other.Password &&
Subject == other.Subject &&
Store == other.Store &&
Location == other.Location &&
(AllowInvalid ?? false) == (other.AllowInvalid ?? false);
- public override int GetHashCode() => HashCode.Combine(Path, Password, Subject, Store, Location, AllowInvalid ?? false);
+ public override int GetHashCode() => HashCode.Combine(Path, KeyPath, Password, Subject, Store, Location, AllowInvalid ?? false);
public static bool operator ==(CertificateConfig lhs, CertificateConfig rhs) => lhs is null ? rhs is null : lhs.Equals(rhs);
public static bool operator !=(CertificateConfig lhs, CertificateConfig rhs) => !(lhs == rhs);
diff --git a/src/Servers/Kestrel/Core/src/Internal/HttpProtocolsFeature.cs b/src/Servers/Kestrel/Core/src/Internal/HttpProtocolsFeature.cs
new file mode 100644
index 000000000000..c29b2cc5013c
--- /dev/null
+++ b/src/Servers/Kestrel/Core/src/Internal/HttpProtocolsFeature.cs
@@ -0,0 +1,15 @@
+// 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.
+
+namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal
+{
+ internal class HttpProtocolsFeature
+ {
+ public HttpProtocolsFeature(HttpProtocols httpProtocols)
+ {
+ HttpProtocols = httpProtocols;
+ }
+
+ public HttpProtocols HttpProtocols { get; }
+ }
+}
diff --git a/src/Servers/Kestrel/Core/src/Internal/LoggerExtensions.cs b/src/Servers/Kestrel/Core/src/Internal/LoggerExtensions.cs
index a28c74ae872e..636dec7454f5 100644
--- a/src/Servers/Kestrel/Core/src/Internal/LoggerExtensions.cs
+++ b/src/Servers/Kestrel/Core/src/Internal/LoggerExtensions.cs
@@ -58,19 +58,20 @@ internal static class LoggerExtensions
new EventId(7, "MissingOrInvalidCertificateKeyFile"),
"The certificate key file at '{CertificateKeyFilePath}' can not be found, contains malformed data or does not contain a PEM encoded key in PKCS8 format.");
- public static void LocatedDevelopmentCertificate(this ILogger logger, X509Certificate2 certificate) => _locatedDevelopmentCertificate(logger, certificate.Subject, certificate.Thumbprint, null);
+ public static void LocatedDevelopmentCertificate(this ILogger logger, X509Certificate2 certificate) => _locatedDevelopmentCertificate(logger, certificate.Subject, certificate.Thumbprint, null);
- public static void UnableToLocateDevelopmentCertificate(this ILogger logger) => _unableToLocateDevelopmentCertificate(logger, null);
+ public static void UnableToLocateDevelopmentCertificate(this ILogger logger) => _unableToLocateDevelopmentCertificate(logger, null);
- public static void FailedToLocateDevelopmentCertificateFile(this ILogger logger, string certificatePath) => _failedToLocateDevelopmentCertificateFile(logger, certificatePath, null);
+ public static void FailedToLocateDevelopmentCertificateFile(this ILogger logger, string certificatePath) => _failedToLocateDevelopmentCertificateFile(logger, certificatePath, null);
- public static void FailedToLoadDevelopmentCertificate(this ILogger logger, string certificatePath) => _failedToLoadDevelopmentCertificate(logger, certificatePath, null);
+ public static void FailedToLoadDevelopmentCertificate(this ILogger logger, string certificatePath) => _failedToLoadDevelopmentCertificate(logger, certificatePath, null);
- public static void BadDeveloperCertificateState(this ILogger logger) => _badDeveloperCertificateState(logger, null);
+ public static void BadDeveloperCertificateState(this ILogger logger) => _badDeveloperCertificateState(logger, null);
- public static void DeveloperCertificateFirstRun(this ILogger logger, string message) => _developerCertificateFirstRun(logger, message, null);
+ public static void DeveloperCertificateFirstRun(this ILogger logger, string message) => _developerCertificateFirstRun(logger, message, null);
- public static void FailedToLoadCertificate(this ILogger logger, string certificatePath) => _failedToLoadCertificate(logger, certificatePath, null);
- public static void FailedToLoadCertificateKey(this ILogger logger, string certificateKeyPath) => _failedToLoadCertificateKey(logger, certificateKeyPath, null);
+ public static void FailedToLoadCertificate(this ILogger logger, string certificatePath) => _failedToLoadCertificate(logger, certificatePath, null);
+
+ public static void FailedToLoadCertificateKey(this ILogger logger, string certificateKeyPath) => _failedToLoadCertificateKey(logger, certificateKeyPath, null);
}
}
diff --git a/src/Servers/Kestrel/Core/src/Internal/SniOptionsSelector.cs b/src/Servers/Kestrel/Core/src/Internal/SniOptionsSelector.cs
new file mode 100644
index 000000000000..279ca5c65c38
--- /dev/null
+++ b/src/Servers/Kestrel/Core/src/Internal/SniOptionsSelector.cs
@@ -0,0 +1,219 @@
+// 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;
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.Linq;
+using System.Net.Security;
+using System.Security.Authentication;
+using System.Security.Cryptography.X509Certificates;
+using System.Threading;
+using System.Threading.Tasks;
+using Microsoft.AspNetCore.Connections;
+using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Certificates;
+using Microsoft.AspNetCore.Server.Kestrel.Https;
+using Microsoft.AspNetCore.Server.Kestrel.Https.Internal;
+using Microsoft.Extensions.Logging;
+
+namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal
+{
+ internal class SniOptionsSelector
+ {
+ private const string WildcardHost = "*";
+ private const string WildcardPrefix = "*.";
+
+ private readonly string _endpointName;
+
+ private readonly Func _fallbackServerCertificateSelector;
+ private readonly Action _onAuthenticateCallback;
+
+ private readonly Dictionary _exactNameOptions = new Dictionary(StringComparer.OrdinalIgnoreCase);
+ private readonly SortedList _wildcardPrefixOptions = new SortedList(LongestStringFirstComparer.Instance);
+ private readonly SniOptions _wildcardOptions;
+
+ public SniOptionsSelector(
+ string endpointName,
+ Dictionary sniDictionary,
+ ICertificateConfigLoader certifcateConfigLoader,
+ HttpsConnectionAdapterOptions fallbackHttpsOptions,
+ HttpProtocols fallbackHttpProtocols,
+ ILogger logger)
+ {
+ _endpointName = endpointName;
+
+ _fallbackServerCertificateSelector = fallbackHttpsOptions.ServerCertificateSelector;
+ _onAuthenticateCallback = fallbackHttpsOptions.OnAuthenticate;
+
+ foreach (var (name, sniConfig) in sniDictionary)
+ {
+ var sslOptions = new SslServerAuthenticationOptions
+ {
+ ServerCertificate = certifcateConfigLoader.LoadCertificate(sniConfig.Certificate, $"{endpointName}:Sni:{name}"),
+ EnabledSslProtocols = sniConfig.SslProtocols ?? fallbackHttpsOptions.SslProtocols,
+ CertificateRevocationCheckMode = fallbackHttpsOptions.CheckCertificateRevocation ? X509RevocationMode.Online : X509RevocationMode.NoCheck,
+ };
+
+ if (sslOptions.ServerCertificate is null)
+ {
+ if (fallbackHttpsOptions.ServerCertificate is null && _fallbackServerCertificateSelector is null)
+ {
+ throw new InvalidOperationException(CoreStrings.NoCertSpecifiedNoDevelopmentCertificateFound);
+ }
+
+ if (_fallbackServerCertificateSelector is null)
+ {
+ // Cache the fallback ServerCertificate since there's no fallback ServerCertificateSelector taking precedence.
+ sslOptions.ServerCertificate = fallbackHttpsOptions.ServerCertificate;
+ }
+ }
+
+ if (!certifcateConfigLoader.IsTestMock && sslOptions.ServerCertificate is X509Certificate2 cert2)
+ {
+ HttpsConnectionMiddleware.EnsureCertificateIsAllowedForServerAuth(cert2);
+ }
+
+ var clientCertificateMode = sniConfig.ClientCertificateMode ?? fallbackHttpsOptions.ClientCertificateMode;
+
+ if (clientCertificateMode != ClientCertificateMode.NoCertificate)
+ {
+ sslOptions.ClientCertificateRequired = true;
+ sslOptions.RemoteCertificateValidationCallback = (sender, certificate, chain, sslPolicyErrors) =>
+ HttpsConnectionMiddleware.RemoteCertificateValidationCallback(
+ clientCertificateMode, fallbackHttpsOptions.ClientCertificateValidation, certificate, chain, sslPolicyErrors);
+ }
+
+ var httpProtocols = sniConfig.Protocols ?? fallbackHttpProtocols;
+ httpProtocols = HttpsConnectionMiddleware.ValidateAndNormalizeHttpProtocols(httpProtocols, logger);
+ HttpsConnectionMiddleware.ConfigureAlpn(sslOptions, httpProtocols);
+
+ var sniOptions = new SniOptions
+ {
+ SslOptions = sslOptions,
+ HttpProtocols = httpProtocols,
+ };
+
+ if (name.Equals(WildcardHost, StringComparison.Ordinal))
+ {
+ _wildcardOptions = sniOptions;
+ }
+ else if (name.StartsWith(WildcardPrefix, StringComparison.Ordinal))
+ {
+ // Only slice off 1 character, the `*`. We want to match the leading `.` also.
+ _wildcardPrefixOptions.Add(name.Substring(1), sniOptions);
+ }
+ else
+ {
+ _exactNameOptions.Add(name, sniOptions);
+ }
+ }
+ }
+
+ public SslServerAuthenticationOptions GetOptions(ConnectionContext connection, string serverName)
+ {
+ SniOptions sniOptions = null;
+
+ if (!string.IsNullOrEmpty(serverName) && !_exactNameOptions.TryGetValue(serverName, out sniOptions))
+ {
+ foreach (var (suffix, options) in _wildcardPrefixOptions)
+ {
+ if (serverName.EndsWith(suffix, StringComparison.OrdinalIgnoreCase))
+ {
+ sniOptions = options;
+ break;
+ }
+ }
+ }
+
+ // Fully wildcarded ("*") options can be used even when given an empty server name.
+ sniOptions ??= _wildcardOptions;
+
+ if (sniOptions is null)
+ {
+ if (serverName is null)
+ {
+ // There was no ALPN
+ throw new AuthenticationException(CoreStrings.FormatSniNotConfiguredToAllowNoServerName(_endpointName));
+ }
+ else
+ {
+ throw new AuthenticationException(CoreStrings.FormatSniNotConfiguredForServerName(serverName, _endpointName));
+ }
+ }
+
+ connection.Features.Set(new HttpProtocolsFeature(sniOptions.HttpProtocols));
+
+ var sslOptions = sniOptions.SslOptions;
+
+ if (sslOptions.ServerCertificate is null)
+ {
+ Debug.Assert(_fallbackServerCertificateSelector != null,
+ "The cached SniOptions ServerCertificate can only be null if there's a fallback certificate selector.");
+
+ // If a ServerCertificateSelector doesn't return a cert, HttpsConnectionMiddleware doesn't fallback to the ServerCertificate.
+ sslOptions = CloneSslOptions(sslOptions);
+ var fallbackCertificate = _fallbackServerCertificateSelector(connection, serverName);
+
+ if (fallbackCertificate != null)
+ {
+ HttpsConnectionMiddleware.EnsureCertificateIsAllowedForServerAuth(fallbackCertificate);
+ }
+
+ sslOptions.ServerCertificate = fallbackCertificate;
+ }
+
+ if (_onAuthenticateCallback != null)
+ {
+ // From doc comments: "This is called after all of the other settings have already been applied."
+ sslOptions = CloneSslOptions(sslOptions);
+ _onAuthenticateCallback(connection, sslOptions);
+ }
+
+ return sslOptions;
+ }
+
+ public static ValueTask OptionsCallback(ConnectionContext connection, SslStream stream, SslClientHelloInfo clientHelloInfo, object state, CancellationToken cancellationToken)
+ {
+ var sniOptionsSelector = (SniOptionsSelector)state;
+ var options = sniOptionsSelector.GetOptions(connection, clientHelloInfo.ServerName);
+ return new ValueTask(options);
+ }
+
+ internal static SslServerAuthenticationOptions CloneSslOptions(SslServerAuthenticationOptions sslOptions) =>
+ new SslServerAuthenticationOptions
+ {
+ AllowRenegotiation = sslOptions.AllowRenegotiation,
+ ApplicationProtocols = sslOptions.ApplicationProtocols?.ToList(),
+ CertificateRevocationCheckMode = sslOptions.CertificateRevocationCheckMode,
+ CipherSuitesPolicy = sslOptions.CipherSuitesPolicy,
+ ClientCertificateRequired = sslOptions.ClientCertificateRequired,
+ EnabledSslProtocols = sslOptions.EnabledSslProtocols,
+ EncryptionPolicy = sslOptions.EncryptionPolicy,
+ RemoteCertificateValidationCallback = sslOptions.RemoteCertificateValidationCallback,
+ ServerCertificate = sslOptions.ServerCertificate,
+ ServerCertificateContext = sslOptions.ServerCertificateContext,
+ ServerCertificateSelectionCallback = sslOptions.ServerCertificateSelectionCallback,
+ };
+
+ private class SniOptions
+ {
+ public SslServerAuthenticationOptions SslOptions { get; set; }
+ public HttpProtocols HttpProtocols { get; set; }
+ }
+
+ private class LongestStringFirstComparer : IComparer
+ {
+ public static LongestStringFirstComparer Instance { get; } = new LongestStringFirstComparer();
+
+ private LongestStringFirstComparer()
+ {
+ }
+
+ public int Compare(string x, string y)
+ {
+ // Flip x and y to put the longest instead of the shortest string first in the SortedList.
+ return y.Length.CompareTo(x.Length);
+ }
+ }
+ }
+}
diff --git a/src/Servers/Kestrel/Core/src/KestrelConfigurationLoader.cs b/src/Servers/Kestrel/Core/src/KestrelConfigurationLoader.cs
index 86e63ddc6a73..cc353b292f9e 100644
--- a/src/Servers/Kestrel/Core/src/KestrelConfigurationLoader.cs
+++ b/src/Servers/Kestrel/Core/src/KestrelConfigurationLoader.cs
@@ -6,17 +6,16 @@
using System.IO;
using System.Linq;
using System.Net;
-using System.Runtime.InteropServices;
-using System.Security.Authentication;
using System.Security.Cryptography;
using System.Security.Cryptography.X509Certificates;
using Microsoft.AspNetCore.Certificates.Generation;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Server.Kestrel.Core;
using Microsoft.AspNetCore.Server.Kestrel.Core.Internal;
+using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Certificates;
using Microsoft.AspNetCore.Server.Kestrel.Https;
+using Microsoft.AspNetCore.Server.Kestrel.Https.Internal;
using Microsoft.Extensions.Configuration;
-using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
@@ -26,13 +25,24 @@ public class KestrelConfigurationLoader
{
private bool _loaded = false;
- internal KestrelConfigurationLoader(KestrelServerOptions options, IConfiguration configuration, bool reloadOnChange)
+ internal KestrelConfigurationLoader(
+ KestrelServerOptions options,
+ IConfiguration configuration,
+ IHostEnvironment hostEnvironment,
+ bool reloadOnChange,
+ ILogger logger,
+ ILogger httpsLogger)
{
Options = options ?? throw new ArgumentNullException(nameof(options));
Configuration = configuration ?? throw new ArgumentNullException(nameof(configuration));
+ HostEnvironment = hostEnvironment ?? throw new ArgumentNullException(nameof(hostEnvironment));
+ Logger = logger ?? throw new ArgumentNullException(nameof(logger));
+ HttpsLogger = httpsLogger ?? throw new ArgumentNullException(nameof(logger));
+
ReloadOnChange = reloadOnChange;
ConfigurationReader = new ConfigurationReader(configuration);
+ CertificateConfigLoader = new CertificateConfigLoader(hostEnvironment, logger);
}
public KestrelServerOptions Options { get; }
@@ -44,8 +54,14 @@ internal KestrelConfigurationLoader(KestrelServerOptions options, IConfiguration
///
internal bool ReloadOnChange { get; }
+ private IHostEnvironment HostEnvironment { get; }
+ private ILogger Logger { get; }
+ private ILogger HttpsLogger { get; }
+
private ConfigurationReader ConfigurationReader { get; set; }
+ private ICertificateConfigLoader CertificateConfigLoader { get; }
+
private IDictionary> EndpointConfigurations { get; }
= new Dictionary>(0, StringComparer.OrdinalIgnoreCase);
@@ -215,9 +231,9 @@ public KestrelConfigurationLoader HandleEndpoint(ulong handle, Action { })
var httpsOptions = new HttpsConnectionAdapterOptions();
+
if (https)
{
- httpsOptions.SslProtocols = ConfigurationReader.EndpointDefaults.SslProtocols ?? SslProtocols.None;
- httpsOptions.ClientCertificateMode = ConfigurationReader.EndpointDefaults.ClientCertificateMode ?? ClientCertificateMode.NoCertificate;
-
// Defaults
Options.ApplyHttpsDefaults(httpsOptions);
@@ -289,14 +325,24 @@ public void Load()
{
httpsOptions.SslProtocols = endpoint.SslProtocols.Value;
}
+ else
+ {
+ // Ensure endpoint is reloaded if it used the default protocol and the SslProtocols changed.
+ endpoint.SslProtocols = ConfigurationReader.EndpointDefaults.SslProtocols;
+ }
if (endpoint.ClientCertificateMode.HasValue)
{
httpsOptions.ClientCertificateMode = endpoint.ClientCertificateMode.Value;
}
+ else
+ {
+ // Ensure endpoint is reloaded if it used the default mode and the ClientCertificateMode changed.
+ endpoint.ClientCertificateMode = ConfigurationReader.EndpointDefaults.ClientCertificateMode;
+ }
- // Specified
- httpsOptions.ServerCertificate = LoadCertificate(endpoint.Certificate, endpoint.Name)
+ // A cert specified directly on the endpoint overrides any defaults.
+ httpsOptions.ServerCertificate = CertificateConfigLoader.LoadCertificate(endpoint.Certificate, endpoint.Name)
?? httpsOptions.ServerCertificate;
if (httpsOptions.ServerCertificate == null && httpsOptions.ServerCertificateSelector == null)
@@ -329,12 +375,20 @@ public void Load()
// EndpointDefaults or configureEndpoint may have added an https adapter.
if (https && !listenOptions.IsTls)
{
- if (httpsOptions.ServerCertificate == null && httpsOptions.ServerCertificateSelector == null)
+ if (endpoint.Sni.Count == 0)
{
- throw new InvalidOperationException(CoreStrings.NoCertSpecifiedNoDevelopmentCertificateFound);
- }
+ if (httpsOptions.ServerCertificate == null && httpsOptions.ServerCertificateSelector == null)
+ {
+ throw new InvalidOperationException(CoreStrings.NoCertSpecifiedNoDevelopmentCertificateFound);
+ }
- listenOptions.UseHttps(httpsOptions);
+ listenOptions.UseHttps(httpsOptions);
+ }
+ else
+ {
+ var sniOptionsSelector = new SniOptionsSelector(endpoint.Name, endpoint.Sni, CertificateConfigLoader, httpsOptions, listenOptions.Protocols, HttpsLogger);
+ listenOptions.UseHttps(SniOptionsSelector.OptionsCallback, sniOptionsSelector, httpsOptions.HandshakeTimeout);
+ }
}
listenOptions.EndpointConfig = endpoint;
@@ -346,11 +400,11 @@ public void Load()
return (endpointsToStop, endpointsToStart);
}
- private void LoadDefaultCert(ConfigurationReader configReader)
+ private void LoadDefaultCert()
{
- if (configReader.Certificates.TryGetValue("Default", out var defaultCertConfig))
+ if (ConfigurationReader.Certificates.TryGetValue("Default", out var defaultCertConfig))
{
- var defaultCert = LoadCertificate(defaultCertConfig, "Default");
+ var defaultCert = CertificateConfigLoader.LoadCertificate(defaultCertConfig, "Default");
if (defaultCert != null)
{
DefaultCertificateConfig = defaultCertConfig;
@@ -359,23 +413,22 @@ private void LoadDefaultCert(ConfigurationReader configReader)
}
else
{
- var logger = Options.ApplicationServices.GetRequiredService>();
- var (certificate, certificateConfig) = FindDeveloperCertificateFile(configReader, logger);
+ var (certificate, certificateConfig) = FindDeveloperCertificateFile();
if (certificate != null)
{
- logger.LocatedDevelopmentCertificate(certificate);
+ Logger.LocatedDevelopmentCertificate(certificate);
DefaultCertificateConfig = certificateConfig;
Options.DefaultCertificate = certificate;
}
}
}
- private (X509Certificate2, CertificateConfig) FindDeveloperCertificateFile(ConfigurationReader configReader, ILogger logger)
+ private (X509Certificate2, CertificateConfig) FindDeveloperCertificateFile()
{
string certificatePath = null;
try
{
- if (configReader.Certificates.TryGetValue("Development", out var certificateConfig) &&
+ if (ConfigurationReader.Certificates.TryGetValue("Development", out var certificateConfig) &&
certificateConfig.Path == null &&
certificateConfig.Password != null &&
TryGetCertificatePath(out certificatePath) &&
@@ -390,12 +443,12 @@ private void LoadDefaultCert(ConfigurationReader configReader)
}
else if (!string.IsNullOrEmpty(certificatePath))
{
- logger.FailedToLocateDevelopmentCertificateFile(certificatePath);
+ Logger.FailedToLocateDevelopmentCertificateFile(certificatePath);
}
}
catch (CryptographicException)
{
- logger.FailedToLoadDevelopmentCertificate(certificatePath);
+ Logger.FailedToLoadDevelopmentCertificate(certificatePath);
}
return (null, null);
@@ -421,163 +474,14 @@ private static bool IsDevelopmentCertificate(X509Certificate2 certificate)
private bool TryGetCertificatePath(out string path)
{
- var hostingEnvironment = Options.ApplicationServices.GetRequiredService();
- var appName = hostingEnvironment.ApplicationName;
-
// This will go away when we implement
// https://github.com/aspnet/Hosting/issues/1294
var appData = Environment.GetEnvironmentVariable("APPDATA");
var home = Environment.GetEnvironmentVariable("HOME");
var basePath = appData != null ? Path.Combine(appData, "ASP.NET", "https") : null;
basePath = basePath ?? (home != null ? Path.Combine(home, ".aspnet", "https") : null);
- path = basePath != null ? Path.Combine(basePath, $"{appName}.pfx") : null;
+ path = basePath != null ? Path.Combine(basePath, $"{HostEnvironment.ApplicationName}.pfx") : null;
return path != null;
}
-
- private X509Certificate2 LoadCertificate(CertificateConfig certInfo, string endpointName)
- {
- var logger = Options.ApplicationServices.GetRequiredService>();
- if (certInfo.IsFileCert && certInfo.IsStoreCert)
- {
- throw new InvalidOperationException(CoreStrings.FormatMultipleCertificateSources(endpointName));
- }
- else if (certInfo.IsFileCert)
- {
- var environment = Options.ApplicationServices.GetRequiredService();
- var certificatePath = Path.Combine(environment.ContentRootPath, certInfo.Path);
- if (certInfo.KeyPath != null)
- {
- var certificateKeyPath = Path.Combine(environment.ContentRootPath, certInfo.KeyPath);
- var certificate = GetCertificate(certificatePath);
-
- if (certificate != null)
- {
- certificate = LoadCertificateKey(certificate, certificateKeyPath, certInfo.Password);
- }
- else
- {
- logger.FailedToLoadCertificate(certificateKeyPath);
- }
-
- if (certificate != null)
- {
- if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
- {
- return PersistKey(certificate);
- }
-
- return certificate;
- }
- else
- {
- logger.FailedToLoadCertificateKey(certificateKeyPath);
- }
-
- throw new InvalidOperationException(CoreStrings.InvalidPemKey);
- }
-
- return new X509Certificate2(Path.Combine(environment.ContentRootPath, certInfo.Path), certInfo.Password);
- }
- else if (certInfo.IsStoreCert)
- {
- return LoadFromStoreCert(certInfo);
- }
- return null;
-
- static X509Certificate2 PersistKey(X509Certificate2 fullCertificate)
- {
- // We need to force the key to be persisted.
- // See https://github.com/dotnet/runtime/issues/23749
- var certificateBytes = fullCertificate.Export(X509ContentType.Pkcs12, "");
- return new X509Certificate2(certificateBytes, "", X509KeyStorageFlags.DefaultKeySet);
- }
-
- static X509Certificate2 LoadCertificateKey(X509Certificate2 certificate, string keyPath, string password)
- {
- // OIDs for the certificate key types.
- const string RSAOid = "1.2.840.113549.1.1.1";
- const string DSAOid = "1.2.840.10040.4.1";
- const string ECDsaOid = "1.2.840.10045.2.1";
-
- var keyText = File.ReadAllText(keyPath);
- return certificate.PublicKey.Oid.Value switch
- {
- RSAOid => AttachPemRSAKey(certificate, keyText, password),
- ECDsaOid => AttachPemECDSAKey(certificate, keyText, password),
- DSAOid => AttachPemDSAKey(certificate, keyText, password),
- _ => throw new InvalidOperationException(string.Format(CoreStrings.UnrecognizedCertificateKeyOid, certificate.PublicKey.Oid.Value))
- };
- }
-
- static X509Certificate2 GetCertificate(string certificatePath)
- {
- if (X509Certificate2.GetCertContentType(certificatePath) == X509ContentType.Cert)
- {
- return new X509Certificate2(certificatePath);
- }
-
- return null;
- }
- }
-
- private static X509Certificate2 AttachPemRSAKey(X509Certificate2 certificate, string keyText, string password)
- {
- using var rsa = RSA.Create();
- if (password == null)
- {
- rsa.ImportFromPem(keyText);
- }
- else
- {
- rsa.ImportFromEncryptedPem(keyText, password);
- }
-
- return certificate.CopyWithPrivateKey(rsa);
- }
-
- private static X509Certificate2 AttachPemDSAKey(X509Certificate2 certificate, string keyText, string password)
- {
- using var dsa = DSA.Create();
- if (password == null)
- {
- dsa.ImportFromPem(keyText);
- }
- else
- {
- dsa.ImportFromEncryptedPem(keyText, password);
- }
-
- return certificate.CopyWithPrivateKey(dsa);
- }
-
- private static X509Certificate2 AttachPemECDSAKey(X509Certificate2 certificate, string keyText, string password)
- {
- using var ecdsa = ECDsa.Create();
- if (password == null)
- {
- ecdsa.ImportFromPem(keyText);
- }
- else
- {
- ecdsa.ImportFromEncryptedPem(keyText, password);
- }
-
- return certificate.CopyWithPrivateKey(ecdsa);
- }
-
- private static X509Certificate2 LoadFromStoreCert(CertificateConfig certInfo)
- {
- var subject = certInfo.Subject;
- var storeName = string.IsNullOrEmpty(certInfo.Store) ? StoreName.My.ToString() : certInfo.Store;
- var location = certInfo.Location;
- var storeLocation = StoreLocation.CurrentUser;
- if (!string.IsNullOrEmpty(location))
- {
- storeLocation = (StoreLocation)Enum.Parse(typeof(StoreLocation), location, ignoreCase: true);
- }
- var allowInvalid = certInfo.AllowInvalid ?? false;
-
- return CertificateLoader.LoadFromStoreCert(subject, storeName, storeLocation, allowInvalid);
- }
}
}
diff --git a/src/Servers/Kestrel/Core/src/KestrelServerOptions.cs b/src/Servers/Kestrel/Core/src/KestrelServerOptions.cs
index 917c1c8a75f8..3fa65dd14653 100644
--- a/src/Servers/Kestrel/Core/src/KestrelServerOptions.cs
+++ b/src/Servers/Kestrel/Core/src/KestrelServerOptions.cs
@@ -12,9 +12,12 @@
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Server.Kestrel.Core.Internal;
using Microsoft.AspNetCore.Server.Kestrel.Https;
+using Microsoft.AspNetCore.Server.Kestrel.Https.Internal;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
+using Microsoft.Extensions.Options;
namespace Microsoft.AspNetCore.Server.Kestrel.Core
{
@@ -138,7 +141,7 @@ public void ConfigureEndpointDefaults(Action configureOptions)
internal void ApplyEndpointDefaults(ListenOptions listenOptions)
{
listenOptions.KestrelServerOptions = this;
- ConfigurationLoader?.ApplyConfigurationDefaults(listenOptions);
+ ConfigurationLoader?.ApplyEndpointDefaults(listenOptions);
EndpointDefaults(listenOptions);
}
@@ -153,6 +156,7 @@ public void ConfigureHttpsDefaults(Action configu
internal void ApplyHttpsDefaults(HttpsConnectionAdapterOptions httpsOptions)
{
+ ConfigurationLoader?.ApplyHttpsDefaults(httpsOptions);
HttpsDefaults(httpsOptions);
}
@@ -240,7 +244,16 @@ private void EnsureDefaultCert()
/// A for further endpoint configuration.
public KestrelConfigurationLoader Configure(IConfiguration config, bool reloadOnChange)
{
- var loader = new KestrelConfigurationLoader(this, config, reloadOnChange);
+ if (ApplicationServices is null)
+ {
+ throw new InvalidOperationException($"{nameof(ApplicationServices)} must not be null. This is normally set automatically via {nameof(IConfigureOptions)}.");
+ }
+
+ var hostEnvironment = ApplicationServices.GetRequiredService();
+ var logger = ApplicationServices.GetRequiredService>();
+ var httpsLogger = ApplicationServices.GetRequiredService>();
+
+ var loader = new KestrelConfigurationLoader(this, config, hostEnvironment, reloadOnChange, logger, httpsLogger);
ConfigurationLoader = loader;
return loader;
}
diff --git a/src/Servers/Kestrel/Core/src/ListenOptionsHttpsExtensions.cs b/src/Servers/Kestrel/Core/src/ListenOptionsHttpsExtensions.cs
index d664fa5ee0e9..a65c0937ac0c 100644
--- a/src/Servers/Kestrel/Core/src/ListenOptionsHttpsExtensions.cs
+++ b/src/Servers/Kestrel/Core/src/ListenOptionsHttpsExtensions.cs
@@ -3,8 +3,10 @@
using System;
using System.IO;
+using System.Net.Security;
using System.Security.Cryptography.X509Certificates;
using Microsoft.AspNetCore.Server.Kestrel.Core;
+using Microsoft.AspNetCore.Server.Kestrel.Core.Internal;
using Microsoft.AspNetCore.Server.Kestrel.Https;
using Microsoft.AspNetCore.Server.Kestrel.Https.Internal;
using Microsoft.Extensions.DependencyInjection;
@@ -36,7 +38,7 @@ public static class ListenOptionsHttpsExtensions
/// The .
public static ListenOptions UseHttps(this ListenOptions listenOptions, string fileName)
{
- var env = listenOptions.KestrelServerOptions.ApplicationServices.GetRequiredService();
+ var env = listenOptions.ApplicationServices.GetRequiredService();
return listenOptions.UseHttps(new X509Certificate2(Path.Combine(env.ContentRootPath, fileName)));
}
@@ -50,7 +52,7 @@ public static ListenOptions UseHttps(this ListenOptions listenOptions, string fi
/// The .
public static ListenOptions UseHttps(this ListenOptions listenOptions, string fileName, string password)
{
- var env = listenOptions.KestrelServerOptions.ApplicationServices.GetRequiredService();
+ var env = listenOptions.ApplicationServices.GetRequiredService();
return listenOptions.UseHttps(new X509Certificate2(Path.Combine(env.ContentRootPath, fileName), password));
}
@@ -65,7 +67,7 @@ public static ListenOptions UseHttps(this ListenOptions listenOptions, string fi
public static ListenOptions UseHttps(this ListenOptions listenOptions, string fileName, string password,
Action configureOptions)
{
- var env = listenOptions.KestrelServerOptions.ApplicationServices.GetRequiredService();
+ var env = listenOptions.ApplicationServices.GetRequiredService();
return listenOptions.UseHttps(new X509Certificate2(Path.Combine(env.ContentRootPath, fileName), password), configureOptions);
}
@@ -227,5 +229,27 @@ public static ListenOptions UseHttps(this ListenOptions listenOptions, HttpsConn
return listenOptions;
}
+
+ ///
+ /// Configure Kestrel to use HTTPS.
+ ///
+ /// The to configure.
+ /// Callback to configure HTTPS options.
+ /// State for the .
+ /// Specifies the maximum amount of time allowed for the TLS/SSL handshake. This must be positive and finite.
+ /// The .
+ internal static ListenOptions UseHttps(this ListenOptions listenOptions, HttpsOptionsCallback httpsOptionsCallback, object state, TimeSpan handshakeTimeout)
+ {
+ var loggerFactory = listenOptions.KestrelServerOptions?.ApplicationServices.GetRequiredService() ?? NullLoggerFactory.Instance;
+
+ listenOptions.IsTls = true;
+ listenOptions.Use(next =>
+ {
+ var middleware = new HttpsConnectionMiddleware(next, httpsOptionsCallback, state, handshakeTimeout, loggerFactory);
+ return middleware.OnConnectionAsync;
+ });
+
+ return listenOptions;
+ }
}
}
diff --git a/src/Servers/Kestrel/Core/src/Middleware/HttpConnectionMiddleware.cs b/src/Servers/Kestrel/Core/src/Middleware/HttpConnectionMiddleware.cs
index d5263f5c3741..3743b3d2fa0d 100644
--- a/src/Servers/Kestrel/Core/src/Middleware/HttpConnectionMiddleware.cs
+++ b/src/Servers/Kestrel/Core/src/Middleware/HttpConnectionMiddleware.cs
@@ -13,13 +13,13 @@ internal class HttpConnectionMiddleware
{
private readonly ServiceContext _serviceContext;
private readonly IHttpApplication _application;
- private readonly HttpProtocols _protocols;
+ private readonly HttpProtocols _endpointDefaultProtocols;
public HttpConnectionMiddleware(ServiceContext serviceContext, IHttpApplication application, HttpProtocols protocols)
{
_serviceContext = serviceContext;
_application = application;
- _protocols = protocols;
+ _endpointDefaultProtocols = protocols;
}
public Task OnConnectionAsync(ConnectionContext connectionContext)
@@ -30,7 +30,7 @@ public Task OnConnectionAsync(ConnectionContext connectionContext)
{
ConnectionId = connectionContext.ConnectionId,
ConnectionContext = connectionContext,
- Protocols = _protocols,
+ Protocols = connectionContext.Features.Get()?.HttpProtocols ?? _endpointDefaultProtocols,
ServiceContext = _serviceContext,
ConnectionFeatures = connectionContext.Features,
MemoryPool = memoryPoolFeature?.MemoryPool ?? System.Buffers.MemoryPool.Shared,
diff --git a/src/Servers/Kestrel/Core/src/Middleware/HttpsConnectionMiddleware.cs b/src/Servers/Kestrel/Core/src/Middleware/HttpsConnectionMiddleware.cs
index 3f908091fc50..eb8ab43591bf 100644
--- a/src/Servers/Kestrel/Core/src/Middleware/HttpsConnectionMiddleware.cs
+++ b/src/Servers/Kestrel/Core/src/Middleware/HttpsConnectionMiddleware.cs
@@ -2,6 +2,7 @@
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
+using System.Buffers;
using System.Collections.Generic;
using System.IO;
using System.IO.Pipelines;
@@ -23,15 +24,28 @@
namespace Microsoft.AspNetCore.Server.Kestrel.Https.Internal
{
+ internal delegate ValueTask HttpsOptionsCallback(ConnectionContext connection, SslStream stream, SslClientHelloInfo clientHelloInfo, object state, CancellationToken cancellationToken);
+
internal class HttpsConnectionMiddleware
{
private const string EnableWindows81Http2 = "Microsoft.AspNetCore.Server.Kestrel.EnableWindows81Http2";
+
+ private static readonly bool _isWindowsVersionIncompatibleWithHttp2 = IsWindowsVersionIncompatibleWithHttp2();
+
private readonly ConnectionDelegate _next;
+ private readonly TimeSpan _handshakeTimeout;
+ private readonly ILogger _logger;
+ private readonly Func _sslStreamFactory;
+
+ // The following fields are only set by HttpsConnectionAdapterOptions ctor.
private readonly HttpsConnectionAdapterOptions _options;
- private readonly ILogger _logger;
private readonly X509Certificate2 _serverCertificate;
private readonly Func _serverCertificateSelector;
+ // The following fields are only set by ServerOptionsSelectionCallback ctor.
+ private readonly HttpsOptionsCallback _httpsOptionsCallback;
+ private readonly object _httpsOptionsCallbackState;
+
public HttpsConnectionMiddleware(ConnectionDelegate next, HttpsConnectionAdapterOptions options)
: this(next, options, loggerFactory: NullLoggerFactory.Instance)
{
@@ -44,35 +58,27 @@ public HttpsConnectionMiddleware(ConnectionDelegate next, HttpsConnectionAdapter
throw new ArgumentNullException(nameof(options));
}
- _options = options;
- _logger = loggerFactory.CreateLogger();
-
- // This configuration will always fail per-request, preemptively fail it here. See HttpConnection.SelectProtocol().
- if (options.HttpProtocols == HttpProtocols.Http2)
- {
- if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
- {
- throw new NotSupportedException(CoreStrings.Http2NoTlsOsx);
- }
- else if (IsWindowsVersionIncompatible())
- {
- throw new NotSupportedException(CoreStrings.Http2NoTlsWin81);
- }
- }
- else if (options.HttpProtocols == HttpProtocols.Http1AndHttp2 && IsWindowsVersionIncompatible())
+ if (options.ServerCertificate == null && options.ServerCertificateSelector == null)
{
- _logger.Http2DefaultCiphersInsufficient();
- options.HttpProtocols = HttpProtocols.Http1;
+ throw new ArgumentException(CoreStrings.ServerCertificateRequired, nameof(options));
}
_next = next;
+ _handshakeTimeout = options.HandshakeTimeout;
+ _logger = loggerFactory.CreateLogger();
+
+ // Something similar to the following could allow us to remove more duplicate logic, but we need https://github.com/dotnet/runtime/issues/40402 to be fixed first.
+ //var sniOptionsSelector = new SniOptionsSelector("", new Dictionary { { "*", new SniConfig() } }, new NoopCertificateConfigLoader(), options, options.HttpProtocols, _logger);
+ //_httpsOptionsCallback = SniOptionsSelector.OptionsCallback;
+ //_httpsOptionsCallbackState = sniOptionsSelector;
+ //_sslStreamFactory = s => new SslStream(s);
+
+ _options = options;
+ _options.HttpProtocols = ValidateAndNormalizeHttpProtocols(_options.HttpProtocols, _logger);
+
// capture the certificate now so it can't be switched after validation
_serverCertificate = options.ServerCertificate;
_serverCertificateSelector = options.ServerCertificateSelector;
- if (_serverCertificate == null && _serverCertificateSelector == null)
- {
- throw new ArgumentException(CoreStrings.ServerCertificateRequired, nameof(options));
- }
// If a selector is provided then ignore the cert, it may be a default cert.
if (_serverCertificateSelector != null)
@@ -84,13 +90,33 @@ public HttpsConnectionMiddleware(ConnectionDelegate next, HttpsConnectionAdapter
{
EnsureCertificateIsAllowedForServerAuth(_serverCertificate);
}
+
+ var remoteCertificateValidationCallback = _options.ClientCertificateMode == ClientCertificateMode.NoCertificate ?
+ (RemoteCertificateValidationCallback)null : RemoteCertificateValidationCallback;
+
+ _sslStreamFactory = s => new SslStream(s, leaveInnerStreamOpen: false, userCertificateValidationCallback: remoteCertificateValidationCallback);
+ }
+
+ internal HttpsConnectionMiddleware(
+ ConnectionDelegate next,
+ HttpsOptionsCallback httpsOptionsCallback,
+ object httpsOptionsCallbackState,
+ TimeSpan handshakeTimeout,
+ ILoggerFactory loggerFactory)
+ {
+ _next = next;
+ _handshakeTimeout = handshakeTimeout;
+ _logger = loggerFactory.CreateLogger();
+
+ _httpsOptionsCallback = httpsOptionsCallback;
+ _httpsOptionsCallbackState = httpsOptionsCallbackState;
+ _sslStreamFactory = s => new SslStream(s);
}
public async Task OnConnectionAsync(ConnectionContext context)
{
await Task.Yield();
- bool certificateRequired;
if (context.Features.Get() != null)
{
await _next(context);
@@ -101,152 +127,54 @@ public async Task OnConnectionAsync(ConnectionContext context)
context.Features.Set(feature);
context.Features.Set(feature);
- var memoryPool = context.Features.Get()?.MemoryPool;
-
- var inputPipeOptions = new StreamPipeReaderOptions
- (
- pool: memoryPool,
- bufferSize: memoryPool.GetMinimumSegmentSize(),
- minimumReadSize: memoryPool.GetMinimumAllocSize(),
- leaveOpen: true
- );
-
- var outputPipeOptions = new StreamPipeWriterOptions
- (
- pool: memoryPool,
- leaveOpen: true
- );
-
- SslDuplexPipe sslDuplexPipe = null;
-
- if (_options.ClientCertificateMode == ClientCertificateMode.NoCertificate)
- {
- sslDuplexPipe = new SslDuplexPipe(context.Transport, inputPipeOptions, outputPipeOptions);
- certificateRequired = false;
- }
- else
- {
- sslDuplexPipe = new SslDuplexPipe(context.Transport, inputPipeOptions, outputPipeOptions, s => new SslStream(s,
- leaveInnerStreamOpen: false,
- userCertificateValidationCallback: (sender, certificate, chain, sslPolicyErrors) =>
- {
- if (certificate == null)
- {
- return _options.ClientCertificateMode != ClientCertificateMode.RequireCertificate;
- }
-
- if (_options.ClientCertificateValidation == null)
- {
- if (sslPolicyErrors != SslPolicyErrors.None)
- {
- return false;
- }
- }
-
- var certificate2 = ConvertToX509Certificate2(certificate);
- if (certificate2 == null)
- {
- return false;
- }
-
- if (_options.ClientCertificateValidation != null)
- {
- if (!_options.ClientCertificateValidation(certificate2, chain, sslPolicyErrors))
- {
- return false;
- }
- }
-
- return true;
- }));
-
- certificateRequired = true;
- }
-
+ var sslDuplexPipe = CreateSslDuplexPipe(context.Transport, context.Features.Get()?.MemoryPool);
var sslStream = sslDuplexPipe.Stream;
- using (var cancellationTokeSource = new CancellationTokenSource(_options.HandshakeTimeout))
+ try
{
- try
+ using var cancellationTokenSource = new CancellationTokenSource(_handshakeTimeout);
+ if (_httpsOptionsCallback is null)
{
- // Adapt to the SslStream signature
- ServerCertificateSelectionCallback selector = null;
- if (_serverCertificateSelector != null)
- {
- selector = (sender, name) =>
- {
- feature.HostName = name;
- context.Features.Set(sslStream);
- var cert = _serverCertificateSelector(context, name);
- if (cert != null)
- {
- EnsureCertificateIsAllowedForServerAuth(cert);
- }
- return cert;
- };
- }
-
- var sslOptions = new SslServerAuthenticationOptions
- {
- ServerCertificate = _serverCertificate,
- ServerCertificateSelectionCallback = selector,
- ClientCertificateRequired = certificateRequired,
- EnabledSslProtocols = _options.SslProtocols,
- CertificateRevocationCheckMode = _options.CheckCertificateRevocation ? X509RevocationMode.Online : X509RevocationMode.NoCheck,
- ApplicationProtocols = new List()
- };
-
- // This is order sensitive
- if ((_options.HttpProtocols & HttpProtocols.Http2) != 0)
- {
- sslOptions.ApplicationProtocols.Add(SslApplicationProtocol.Http2);
- // https://tools.ietf.org/html/rfc7540#section-9.2.1
- sslOptions.AllowRenegotiation = false;
- }
-
- if ((_options.HttpProtocols & HttpProtocols.Http1) != 0)
- {
- sslOptions.ApplicationProtocols.Add(SslApplicationProtocol.Http11);
- }
-
- _options.OnAuthenticate?.Invoke(context, sslOptions);
-
- KestrelEventSource.Log.TlsHandshakeStart(context, sslOptions);
-
- await sslStream.AuthenticateAsServerAsync(sslOptions, cancellationTokeSource.Token);
+ await DoOptionsBasedHandshakeAsync(context, sslStream, feature, cancellationTokenSource.Token);
}
- catch (OperationCanceledException)
+ else
{
- KestrelEventSource.Log.TlsHandshakeFailed(context.ConnectionId);
- KestrelEventSource.Log.TlsHandshakeStop(context, null);
-
- _logger.AuthenticationTimedOut();
- await sslStream.DisposeAsync();
- return;
+ var state = (this, context, feature);
+ await sslStream.AuthenticateAsServerAsync(ServerOptionsCallback, state, cancellationTokenSource.Token);
}
- catch (IOException ex)
- {
- KestrelEventSource.Log.TlsHandshakeFailed(context.ConnectionId);
- KestrelEventSource.Log.TlsHandshakeStop(context, null);
+ }
+ catch (OperationCanceledException)
+ {
+ KestrelEventSource.Log.TlsHandshakeFailed(context.ConnectionId);
+ KestrelEventSource.Log.TlsHandshakeStop(context, null);
- _logger.AuthenticationFailed(ex);
- await sslStream.DisposeAsync();
- return;
- }
- catch (AuthenticationException ex)
- {
- KestrelEventSource.Log.TlsHandshakeFailed(context.ConnectionId);
- KestrelEventSource.Log.TlsHandshakeStop(context, null);
+ _logger.AuthenticationTimedOut();
+ await sslStream.DisposeAsync();
+ return;
+ }
+ catch (IOException ex)
+ {
+ KestrelEventSource.Log.TlsHandshakeFailed(context.ConnectionId);
+ KestrelEventSource.Log.TlsHandshakeStop(context, null);
+
+ _logger.AuthenticationFailed(ex);
+ await sslStream.DisposeAsync();
+ return;
+ }
+ catch (AuthenticationException ex)
+ {
+ KestrelEventSource.Log.TlsHandshakeFailed(context.ConnectionId);
+ KestrelEventSource.Log.TlsHandshakeStop(context, null);
- _logger.AuthenticationFailed(ex);
+ _logger.AuthenticationFailed(ex);
- await sslStream.DisposeAsync();
- return;
- }
+ await sslStream.DisposeAsync();
+ return;
}
feature.ApplicationProtocol = sslStream.NegotiatedApplicationProtocol.Protocol;
context.Features.Set(feature);
+
feature.ClientCertificate = ConvertToX509Certificate2(sslStream.RemoteCertificate);
feature.CipherAlgorithm = sslStream.CipherAlgorithm;
feature.CipherStrength = sslStream.CipherStrength;
@@ -282,7 +210,135 @@ public async Task OnConnectionAsync(ConnectionContext context)
}
}
- private static void EnsureCertificateIsAllowedForServerAuth(X509Certificate2 certificate)
+ private Task DoOptionsBasedHandshakeAsync(ConnectionContext context, SslStream sslStream, Core.Internal.TlsConnectionFeature feature, CancellationToken cancellationToken)
+ {
+ // Adapt to the SslStream signature
+ ServerCertificateSelectionCallback selector = null;
+ if (_serverCertificateSelector != null)
+ {
+ selector = (sender, name) =>
+ {
+ feature.HostName = name;
+ context.Features.Set(sslStream);
+ var cert = _serverCertificateSelector(context, name);
+ if (cert != null)
+ {
+ EnsureCertificateIsAllowedForServerAuth(cert);
+ }
+ return cert;
+ };
+ }
+
+ var sslOptions = new SslServerAuthenticationOptions
+ {
+ ServerCertificate = _serverCertificate,
+ ServerCertificateSelectionCallback = selector,
+ ClientCertificateRequired = _options.ClientCertificateMode != ClientCertificateMode.NoCertificate,
+ EnabledSslProtocols = _options.SslProtocols,
+ CertificateRevocationCheckMode = _options.CheckCertificateRevocation ? X509RevocationMode.Online : X509RevocationMode.NoCheck,
+ };
+
+ ConfigureAlpn(sslOptions, _options.HttpProtocols);
+
+ _options.OnAuthenticate?.Invoke(context, sslOptions);
+
+ KestrelEventSource.Log.TlsHandshakeStart(context, sslOptions);
+
+ return sslStream.AuthenticateAsServerAsync(sslOptions, cancellationToken);
+ }
+
+ internal static void ConfigureAlpn(SslServerAuthenticationOptions serverOptions, HttpProtocols httpProtocols)
+ {
+ serverOptions.ApplicationProtocols = new List();
+
+ // This is order sensitive
+ if ((httpProtocols & HttpProtocols.Http2) != 0)
+ {
+ serverOptions.ApplicationProtocols.Add(SslApplicationProtocol.Http2);
+ // https://tools.ietf.org/html/rfc7540#section-9.2.1
+ serverOptions.AllowRenegotiation = false;
+ }
+
+ if ((httpProtocols & HttpProtocols.Http1) != 0)
+ {
+ serverOptions.ApplicationProtocols.Add(SslApplicationProtocol.Http11);
+ }
+ }
+
+ internal static bool RemoteCertificateValidationCallback(
+ ClientCertificateMode clientCertificateMode,
+ Func clientCertificateValidation,
+ X509Certificate certificate,
+ X509Chain chain,
+ SslPolicyErrors sslPolicyErrors)
+ {
+ if (certificate == null)
+ {
+ return clientCertificateMode != ClientCertificateMode.RequireCertificate;
+ }
+
+ if (clientCertificateValidation == null)
+ {
+ if (sslPolicyErrors != SslPolicyErrors.None)
+ {
+ return false;
+ }
+ }
+
+ var certificate2 = ConvertToX509Certificate2(certificate);
+ if (certificate2 == null)
+ {
+ return false;
+ }
+
+ if (clientCertificateValidation != null)
+ {
+ if (!clientCertificateValidation(certificate2, chain, sslPolicyErrors))
+ {
+ return false;
+ }
+ }
+
+ return true;
+ }
+
+ private bool RemoteCertificateValidationCallback(object sender, X509Certificate certificate, X509Chain chain, SslPolicyErrors sslPolicyErrors) =>
+ RemoteCertificateValidationCallback(_options.ClientCertificateMode, _options.ClientCertificateValidation, certificate, chain, sslPolicyErrors);
+
+ private SslDuplexPipe CreateSslDuplexPipe(IDuplexPipe transport, MemoryPool memoryPool)
+ {
+ var inputPipeOptions = new StreamPipeReaderOptions
+ (
+ pool: memoryPool,
+ bufferSize: memoryPool.GetMinimumSegmentSize(),
+ minimumReadSize: memoryPool.GetMinimumAllocSize(),
+ leaveOpen: true
+ );
+
+ var outputPipeOptions = new StreamPipeWriterOptions
+ (
+ pool: memoryPool,
+ leaveOpen: true
+ );
+
+ return new SslDuplexPipe(transport, inputPipeOptions, outputPipeOptions, _sslStreamFactory);
+ }
+
+ private static async ValueTask ServerOptionsCallback(SslStream sslStream, SslClientHelloInfo clientHelloInfo, object state, CancellationToken cancellationToken)
+ {
+ var (middleware, context, feature) = (ValueTuple)state;
+
+ feature.HostName = clientHelloInfo.ServerName;
+ context.Features.Set(sslStream);
+
+ var sslOptions = await middleware._httpsOptionsCallback(context, sslStream, clientHelloInfo, middleware._httpsOptionsCallbackState, cancellationToken);
+
+ KestrelEventSource.Log.TlsHandshakeStart(context, sslOptions);
+
+ return sslOptions;
+ }
+
+ internal static void EnsureCertificateIsAllowedForServerAuth(X509Certificate2 certificate)
{
if (!CertificateLoader.IsCertificateAllowedForServerAuth(certificate))
{
@@ -305,7 +361,30 @@ private static X509Certificate2 ConvertToX509Certificate2(X509Certificate certif
return new X509Certificate2(certificate);
}
- private static bool IsWindowsVersionIncompatible()
+ internal static HttpProtocols ValidateAndNormalizeHttpProtocols(HttpProtocols httpProtocols, ILogger logger)
+ {
+ // This configuration will always fail per-request, preemptively fail it here. See HttpConnection.SelectProtocol().
+ if (httpProtocols == HttpProtocols.Http2)
+ {
+ if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
+ {
+ throw new NotSupportedException(CoreStrings.Http2NoTlsOsx);
+ }
+ else if (_isWindowsVersionIncompatibleWithHttp2)
+ {
+ throw new NotSupportedException(CoreStrings.Http2NoTlsWin81);
+ }
+ }
+ else if (httpProtocols == HttpProtocols.Http1AndHttp2 && _isWindowsVersionIncompatibleWithHttp2)
+ {
+ logger.Http2DefaultCiphersInsufficient();
+ return HttpProtocols.Http1;
+ }
+
+ return httpProtocols;
+ }
+
+ private static bool IsWindowsVersionIncompatibleWithHttp2()
{
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
{
@@ -349,12 +428,12 @@ internal static class HttpsConnectionMiddlewareLoggerExtensions
eventId: new EventId(4, "Http2DefaultCiphersInsufficient"),
formatString: CoreStrings.Http2DefaultCiphersInsufficient);
- public static void AuthenticationFailed(this ILogger logger, Exception exception) => _authenticationFailed(logger, exception);
+ public static void AuthenticationFailed(this ILogger logger, Exception exception) => _authenticationFailed(logger, exception);
- public static void AuthenticationTimedOut(this ILogger logger) => _authenticationTimedOut(logger, null);
+ public static void AuthenticationTimedOut(this ILogger logger) => _authenticationTimedOut(logger, null);
- public static void HttpsConnectionEstablished(this ILogger logger, string connectionId, SslProtocols sslProtocol) => _httpsConnectionEstablished(logger, connectionId, sslProtocol, null);
+ public static void HttpsConnectionEstablished(this ILogger logger, string connectionId, SslProtocols sslProtocol) => _httpsConnectionEstablished(logger, connectionId, sslProtocol, null);
- public static void Http2DefaultCiphersInsufficient(this ILogger logger) => _http2DefaultCiphersInsufficient(logger, null);
+ public static void Http2DefaultCiphersInsufficient(this ILogger logger) => _http2DefaultCiphersInsufficient(logger, null);
}
}
diff --git a/src/Servers/Kestrel/Core/test/KestrelServerOptionsTests.cs b/src/Servers/Kestrel/Core/test/KestrelServerOptionsTests.cs
index 956859b27962..e96c2d1940ca 100644
--- a/src/Servers/Kestrel/Core/test/KestrelServerOptionsTests.cs
+++ b/src/Servers/Kestrel/Core/test/KestrelServerOptionsTests.cs
@@ -3,6 +3,11 @@
using System;
using System.Net;
+using Microsoft.AspNetCore.Server.Kestrel.Https.Internal;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Hosting;
+using Microsoft.Extensions.Logging;
+using Moq;
using Xunit;
namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
@@ -51,10 +56,36 @@ public void ConfigureEndpointDefaultsAppliesToNewEndpoints()
Assert.Equal(HttpProtocols.Http2, options.CodeBackedListenOptions[3].Protocols);
}
+ [Fact]
+ public void ConfigureThrowsInvalidOperationExceptionIfApplicationServicesIsNotSet()
+ {
+ var options = new KestrelServerOptions();
+ Assert.Throws(() => options.Configure());
+ }
+
+ [Fact]
+ public void ConfigureThrowsInvalidOperationExceptionIfApplicationServicesDoesntHaveRequiredServices()
+ {
+ var options = new KestrelServerOptions
+ {
+ ApplicationServices = new ServiceCollection().BuildServiceProvider()
+ };
+
+ Assert.Throws(() => options.Configure());
+ }
+
[Fact]
public void CanCallListenAfterConfigure()
{
var options = new KestrelServerOptions();
+
+ // Ensure configure doesn't throw because of missing services.
+ var serviceCollection = new ServiceCollection();
+ serviceCollection.AddSingleton(Mock.Of());
+ serviceCollection.AddSingleton(Mock.Of>());
+ serviceCollection.AddSingleton(Mock.Of>());
+ options.ApplicationServices = serviceCollection.BuildServiceProvider();
+
options.Configure();
// This is a regression test to verify the Listen* methods don't throw a NullReferenceException if called after Configure().
@@ -63,7 +94,7 @@ public void CanCallListenAfterConfigure()
}
[Fact]
- public void SettingRequestHeaderEncodingSelecterThrowsArgumentNullException()
+ public void SettingRequestHeaderEncodingSelecterToNullThrowsArgumentNullException()
{
var options = new KestrelServerOptions();
diff --git a/src/Servers/Kestrel/Core/test/KestrelServerTests.cs b/src/Servers/Kestrel/Core/test/KestrelServerTests.cs
index 4f41ddbe1900..dac5a74c5324 100644
--- a/src/Servers/Kestrel/Core/test/KestrelServerTests.cs
+++ b/src/Servers/Kestrel/Core/test/KestrelServerTests.cs
@@ -13,9 +13,11 @@
using Microsoft.AspNetCore.Hosting.Server.Features;
using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http;
using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure;
+using Microsoft.AspNetCore.Server.Kestrel.Https.Internal;
using Microsoft.AspNetCore.Testing;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Microsoft.Extensions.Primitives;
@@ -493,8 +495,9 @@ public async Task ReloadsOnConfigurationChangeWhenOptedIn()
mockLoggerFactory.Setup(m => m.CreateLogger(It.IsAny())).Returns(Mock.Of());
var serviceCollection = new ServiceCollection();
- serviceCollection.AddSingleton(mockLoggerFactory.Object);
+ serviceCollection.AddSingleton(Mock.Of());
serviceCollection.AddSingleton(Mock.Of>());
+ serviceCollection.AddSingleton(Mock.Of>());
var options = new KestrelServerOptions
{
@@ -629,8 +632,9 @@ public async Task DoesNotReloadOnConfigurationChangeByDefault()
mockLoggerFactory.Setup(m => m.CreateLogger(It.IsAny())).Returns(Mock.Of());
var serviceCollection = new ServiceCollection();
- serviceCollection.AddSingleton(mockLoggerFactory.Object);
+ serviceCollection.AddSingleton(Mock.Of());
serviceCollection.AddSingleton(Mock.Of>());
+ serviceCollection.AddSingleton(Mock.Of>());
var options = new KestrelServerOptions
{
diff --git a/src/Servers/Kestrel/Core/test/SniOptionsSelectorTests.cs b/src/Servers/Kestrel/Core/test/SniOptionsSelectorTests.cs
new file mode 100644
index 000000000000..a2c2be973b99
--- /dev/null
+++ b/src/Servers/Kestrel/Core/test/SniOptionsSelectorTests.cs
@@ -0,0 +1,778 @@
+// Copyright (c) Microsoft. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+using System;
+using System.Collections.Generic;
+using System.IO.Pipelines;
+using System.Linq;
+using System.Net.Security;
+using System.Runtime.InteropServices;
+using System.Security.Authentication;
+using System.Security.Cryptography.X509Certificates;
+using Microsoft.AspNetCore.Connections;
+using Microsoft.AspNetCore.Http.Features;
+using Microsoft.AspNetCore.Server.Kestrel.Core.Internal;
+using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Certificates;
+using Microsoft.AspNetCore.Server.Kestrel.Https;
+using Microsoft.AspNetCore.Server.Kestrel.Https.Internal;
+using Microsoft.AspNetCore.Testing;
+using Microsoft.Extensions.Logging;
+using Moq;
+using Xunit;
+
+namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
+{
+ public class SniOptionsSelectorTests
+ {
+ private static X509Certificate2 _x509Certificate2 = TestResources.GetTestCertificate();
+
+ [Fact]
+ public void PrefersExactMatchOverWildcardPrefixOverWildcardOnly()
+ {
+ var sniDictionary = new Dictionary
+ {
+ {
+ "www.example.org",
+ new SniConfig
+ {
+ Certificate = new CertificateConfig
+ {
+ Path = "Exact"
+ }
+ }
+ },
+ {
+ "*.example.org",
+ new SniConfig
+ {
+ Certificate = new CertificateConfig
+ {
+ Path = "WildcardPrefix"
+ }
+ }
+ },
+ {
+ "*",
+ new SniConfig
+ {
+ Certificate = new CertificateConfig
+ {
+ Path = "WildcardOnly"
+ }
+ }
+ }
+ };
+
+ var mockCertificateConfigLoader = new MockCertificateConfigLoader();
+ var pathDictionary = mockCertificateConfigLoader.CertToPathDictionary;
+
+ var sniOptionsSelector = new SniOptionsSelector(
+ "TestEndpointName",
+ sniDictionary,
+ mockCertificateConfigLoader,
+ fallbackHttpsOptions: new HttpsConnectionAdapterOptions(),
+ fallbackHttpProtocols: HttpProtocols.Http1AndHttp2,
+ logger: Mock.Of>());
+
+ var wwwSubdomainOptions = sniOptionsSelector.GetOptions(new MockConnectionContext(), "www.example.org");
+ Assert.Equal("Exact", pathDictionary[wwwSubdomainOptions.ServerCertificate]);
+
+ var baSubdomainOptions = sniOptionsSelector.GetOptions(new MockConnectionContext(), "b.a.example.org");
+ Assert.Equal("WildcardPrefix", pathDictionary[baSubdomainOptions.ServerCertificate]);
+
+ var aSubdomainOptions = sniOptionsSelector.GetOptions(new MockConnectionContext(), "a.example.org");
+ Assert.Equal("WildcardPrefix", pathDictionary[aSubdomainOptions.ServerCertificate]);
+
+ // "*.example.org" is preferred over "*", but "*.example.org" doesn't match "example.org".
+ // REVIEW: Are we OK with "example.org" matching "*" instead of "*.example.org"? It feels annoying to me to have to configure example.org twice.
+ // Unfortunately, the alternative would have "a.example.org" match "*.a.example.org" before "*.example.org", and that just seems wrong.
+ var noSubdomainOptions = sniOptionsSelector.GetOptions(new MockConnectionContext(), "example.org");
+ Assert.Equal("WildcardOnly", pathDictionary[noSubdomainOptions.ServerCertificate]);
+
+ var anotherTldOptions = sniOptionsSelector.GetOptions(new MockConnectionContext(), "dot.net");
+ Assert.Equal("WildcardOnly", pathDictionary[anotherTldOptions.ServerCertificate]);
+ }
+
+ [Fact]
+ public void PerfersLongerWildcardPrefixOverShorterWildcardPrefix()
+ {
+ var sniDictionary = new Dictionary
+ {
+ {
+ "*.a.example.org",
+ new SniConfig
+ {
+ Certificate = new CertificateConfig
+ {
+ Path = "Long"
+ }
+ }
+ },
+ {
+ "*.example.org",
+ new SniConfig
+ {
+ Certificate = new CertificateConfig
+ {
+ Path = "Short"
+ }
+ }
+ }
+ };
+
+ var mockCertificateConfigLoader = new MockCertificateConfigLoader();
+ var pathDictionary = mockCertificateConfigLoader.CertToPathDictionary;
+
+ var sniOptionsSelector = new SniOptionsSelector(
+ "TestEndpointName",
+ sniDictionary,
+ mockCertificateConfigLoader,
+ fallbackHttpsOptions: new HttpsConnectionAdapterOptions(),
+ fallbackHttpProtocols: HttpProtocols.Http1AndHttp2,
+ logger: Mock.Of>());
+
+ var baSubdomainOptions = sniOptionsSelector.GetOptions(new MockConnectionContext(), "b.a.example.org");
+ Assert.Equal("Long", pathDictionary[baSubdomainOptions.ServerCertificate]);
+
+ // "*.a.example.org" is preferred over "*.example.org", but "a.example.org" doesn't match "*.a.example.org".
+ var aSubdomainOptions = sniOptionsSelector.GetOptions(new MockConnectionContext(), "a.example.org");
+ Assert.Equal("Short", pathDictionary[aSubdomainOptions.ServerCertificate]);
+ }
+
+ [Fact]
+ public void ServerNameMatchingIsCaseInsensitive()
+ {
+ var sniDictionary = new Dictionary
+ {
+ {
+ "Www.Example.Org",
+ new SniConfig
+ {
+ Certificate = new CertificateConfig
+ {
+ Path = "Exact"
+ }
+ }
+ },
+ {
+ "*.Example.Org",
+ new SniConfig
+ {
+ Certificate = new CertificateConfig
+ {
+ Path = "WildcardPrefix"
+ }
+ }
+ }
+ };
+
+ var mockCertificateConfigLoader = new MockCertificateConfigLoader();
+ var pathDictionary = mockCertificateConfigLoader.CertToPathDictionary;
+
+ var sniOptionsSelector = new SniOptionsSelector(
+ "TestEndpointName",
+ sniDictionary,
+ mockCertificateConfigLoader,
+ fallbackHttpsOptions: new HttpsConnectionAdapterOptions(),
+ fallbackHttpProtocols: HttpProtocols.Http1AndHttp2,
+ logger: Mock.Of>());
+
+ var wwwSubdomainOptions = sniOptionsSelector.GetOptions(new MockConnectionContext(), "wWw.eXample.oRg");
+ Assert.Equal("Exact", pathDictionary[wwwSubdomainOptions.ServerCertificate]);
+
+ var baSubdomainOptions = sniOptionsSelector.GetOptions(new MockConnectionContext(), "B.a.eXample.oRg");
+ Assert.Equal("WildcardPrefix", pathDictionary[baSubdomainOptions.ServerCertificate]);
+
+ var aSubdomainOptions = sniOptionsSelector.GetOptions(new MockConnectionContext(), "A.eXample.oRg");
+ Assert.Equal("WildcardPrefix", pathDictionary[aSubdomainOptions.ServerCertificate]);
+ }
+
+ [Fact]
+ public void GetOptionsThrowsAnAuthenticationExceptionIfThereIsNoMatchingSniSection()
+ {
+ var sniOptionsSelector = new SniOptionsSelector(
+ "TestEndpointName",
+ new Dictionary(),
+ new MockCertificateConfigLoader(),
+ fallbackHttpsOptions: new HttpsConnectionAdapterOptions(),
+ fallbackHttpProtocols: HttpProtocols.Http1AndHttp2,
+ logger: Mock.Of>());
+
+ var authExWithServerName = Assert.Throws(() => sniOptionsSelector.GetOptions(new MockConnectionContext(), "example.org"));
+ Assert.Equal(CoreStrings.FormatSniNotConfiguredForServerName("example.org", "TestEndpointName"), authExWithServerName.Message);
+
+ var authExWithoutServerName = Assert.Throws(() => sniOptionsSelector.GetOptions(new MockConnectionContext(), null));
+ Assert.Equal(CoreStrings.FormatSniNotConfiguredToAllowNoServerName("TestEndpointName"), authExWithoutServerName.Message);
+ }
+
+ [Fact]
+ public void WildcardOnlyMatchesNullServerNameDueToNoAlpn()
+ {
+ var sniDictionary = new Dictionary
+ {
+ {
+ "*",
+ new SniConfig
+ {
+ Certificate = new CertificateConfig
+ {
+ Path = "WildcardOnly"
+ }
+ }
+ }
+ };
+
+ var mockCertificateConfigLoader = new MockCertificateConfigLoader();
+ var pathDictionary = mockCertificateConfigLoader.CertToPathDictionary;
+
+ var sniOptionsSelector = new SniOptionsSelector(
+ "TestEndpointName",
+ sniDictionary,
+ mockCertificateConfigLoader,
+ fallbackHttpsOptions: new HttpsConnectionAdapterOptions(),
+ fallbackHttpProtocols: HttpProtocols.Http1AndHttp2,
+ logger: Mock.Of>());
+
+ var options = sniOptionsSelector.GetOptions(new MockConnectionContext(), null);
+ Assert.Equal("WildcardOnly", pathDictionary[options.ServerCertificate]);
+ }
+
+ [Fact]
+ public void CachesSslServerAuthenticationOptions()
+ {
+ var sniDictionary = new Dictionary
+ {
+ {
+ "www.example.org",
+ new SniConfig
+ {
+ Certificate = new CertificateConfig()
+ }
+ }
+ };
+
+ var sniOptionsSelector = new SniOptionsSelector(
+ "TestEndpointName",
+ sniDictionary,
+ new MockCertificateConfigLoader(),
+ fallbackHttpsOptions: new HttpsConnectionAdapterOptions(),
+ fallbackHttpProtocols: HttpProtocols.Http1AndHttp2,
+ logger: Mock.Of>());
+
+ var options1 = sniOptionsSelector.GetOptions(new MockConnectionContext(), "www.example.org");
+ var options2 = sniOptionsSelector.GetOptions(new MockConnectionContext(), "www.example.org");
+ Assert.Same(options1, options2);
+ }
+
+ [Fact]
+ public void ClonesSslServerAuthenticationOptionsIfAnOnAuthenticateCallbackIsDefined()
+ {
+ var sniDictionary = new Dictionary
+ {
+ {
+ "www.example.org",
+ new SniConfig
+ {
+ Certificate = new CertificateConfig()
+ }
+ }
+ };
+
+ SslServerAuthenticationOptions lastSeenSslOptions = null;
+
+ var fallbackOptions = new HttpsConnectionAdapterOptions
+ {
+ OnAuthenticate = (context, sslOptions) =>
+ {
+ lastSeenSslOptions = sslOptions;
+ }
+ };
+
+ var sniOptionsSelector = new SniOptionsSelector(
+ "TestEndpointName",
+ sniDictionary,
+ new MockCertificateConfigLoader(),
+ fallbackOptions,
+ fallbackHttpProtocols: HttpProtocols.Http1AndHttp2,
+ logger: Mock.Of>());
+
+ var options1 = sniOptionsSelector.GetOptions(new MockConnectionContext(), "www.example.org");
+ Assert.Same(lastSeenSslOptions, options1);
+
+ var options2 = sniOptionsSelector.GetOptions(new MockConnectionContext(), "www.example.org");
+ Assert.Same(lastSeenSslOptions, options2);
+
+ Assert.NotSame(options1, options2);
+ }
+
+ [Fact]
+ public void ClonesSslServerAuthenticationOptionsIfTheFallbackServerCertificateSelectorIsUsed()
+ {
+ var sniDictionary = new Dictionary
+ {
+ {
+ "selector.example.org",
+ new SniConfig()
+ },
+ {
+ "config.example.org",
+ new SniConfig
+ {
+ Certificate = new CertificateConfig()
+ }
+ }
+ };
+
+ var selectorCertificate = _x509Certificate2;
+
+ var fallbackOptions = new HttpsConnectionAdapterOptions
+ {
+ ServerCertificate = new X509Certificate2(),
+ ServerCertificateSelector = (context, serverName) => selectorCertificate
+ };
+
+ var sniOptionsSelector = new SniOptionsSelector(
+ "TestEndpointName",
+ sniDictionary,
+ new MockCertificateConfigLoader(),
+ fallbackOptions,
+ fallbackHttpProtocols: HttpProtocols.Http1AndHttp2,
+ logger: Mock.Of>());
+
+ var selectorOptions1 = sniOptionsSelector.GetOptions(new MockConnectionContext(), "selector.example.org");
+ Assert.Same(selectorCertificate, selectorOptions1.ServerCertificate);
+
+ var selectorOptions2 = sniOptionsSelector.GetOptions(new MockConnectionContext(), "selector.example.org");
+ Assert.Same(selectorCertificate, selectorOptions2.ServerCertificate);
+
+ // The SslServerAuthenticationOptions were cloned because the cert came from the ServerCertificateSelector fallback.
+ Assert.NotSame(selectorOptions1, selectorOptions2);
+
+ var configOptions1 = sniOptionsSelector.GetOptions(new MockConnectionContext(), "config.example.org");
+ Assert.NotSame(selectorCertificate, configOptions1.ServerCertificate);
+
+ var configOptions2 = sniOptionsSelector.GetOptions(new MockConnectionContext(), "config.example.org");
+ Assert.NotSame(selectorCertificate, configOptions2.ServerCertificate);
+
+ // The SslServerAuthenticationOptions don't need to be cloned if a static cert is defined in config for the given server name.
+ Assert.Same(configOptions1, configOptions2);
+ }
+
+ [Fact]
+ public void ConstructorThrowsInvalidOperationExceptionIfNoCertificateDefiniedInConfigOrFallback()
+ {
+ var sniDictionary = new Dictionary
+ {
+ { "www.example.org", new SniConfig() }
+ };
+
+ var ex = Assert.Throws(
+ () => new SniOptionsSelector(
+ "TestEndpointName",
+ sniDictionary,
+ new MockCertificateConfigLoader(),
+ fallbackHttpsOptions: new HttpsConnectionAdapterOptions(),
+ fallbackHttpProtocols: HttpProtocols.Http1AndHttp2,
+ logger: Mock.Of>()));
+
+ Assert.Equal(CoreStrings.NoCertSpecifiedNoDevelopmentCertificateFound, ex.Message);
+ }
+
+ [Fact]
+ public void FallsBackToHttpsConnectionAdapterCertificate()
+ {
+ var sniDictionary = new Dictionary
+ {
+ { "www.example.org", new SniConfig() }
+ };
+
+ var fallbackOptions = new HttpsConnectionAdapterOptions
+ {
+ ServerCertificate = new X509Certificate2()
+ };
+
+ var sniOptionsSelector = new SniOptionsSelector(
+ "TestEndpointName",
+ sniDictionary,
+ new MockCertificateConfigLoader(),
+ fallbackOptions,
+ fallbackHttpProtocols: HttpProtocols.Http1AndHttp2,
+ logger: Mock.Of>());
+
+ var options = sniOptionsSelector.GetOptions(new MockConnectionContext(), "www.example.org");
+ Assert.Same(fallbackOptions.ServerCertificate, options.ServerCertificate);
+ }
+
+ [Fact]
+ public void FallsBackToHttpsConnectionAdapterServerCertificateSelectorOverServerCertificate()
+ {
+ var sniDictionary = new Dictionary
+ {
+ { "www.example.org", new SniConfig() }
+ };
+
+ var selectorCertificate = _x509Certificate2;
+
+ var fallbackOptions = new HttpsConnectionAdapterOptions
+ {
+ ServerCertificate = new X509Certificate2(),
+ ServerCertificateSelector = (context, serverName) => selectorCertificate
+ };
+
+ var sniOptionsSelector = new SniOptionsSelector(
+ "TestEndpointName",
+ sniDictionary,
+ new MockCertificateConfigLoader(),
+ fallbackOptions,
+ fallbackHttpProtocols: HttpProtocols.Http1AndHttp2,
+ logger: Mock.Of>());
+
+ var options = sniOptionsSelector.GetOptions(new MockConnectionContext(), "www.example.org");
+ Assert.Same(selectorCertificate, options.ServerCertificate);
+ }
+
+ [Fact]
+ public void PrefersHttpProtocolsDefinedInSniConfig()
+ {
+ var sniDictionary = new Dictionary
+ {
+ {
+ "www.example.org",
+ new SniConfig
+ {
+ Protocols = HttpProtocols.None,
+ Certificate = new CertificateConfig()
+ }
+ }
+ };
+
+ var sniOptionsSelector = new SniOptionsSelector(
+ "TestEndpointName",
+ sniDictionary,
+ new MockCertificateConfigLoader(),
+ new HttpsConnectionAdapterOptions(),
+ fallbackHttpProtocols: HttpProtocols.Http1,
+ logger: Mock.Of>());
+
+ var mockConnectionContext = new MockConnectionContext();
+ sniOptionsSelector.GetOptions(mockConnectionContext, "www.example.org");
+
+ var httpProtocolsFeature = mockConnectionContext.Features.Get();
+ Assert.NotNull(httpProtocolsFeature);
+ Assert.Equal(HttpProtocols.None, httpProtocolsFeature.HttpProtocols);
+ }
+
+ [Fact]
+ public void ConfiguresAlpnBasedOnConfiguredHttpProtocols()
+ {
+ var sniDictionary = new Dictionary
+ {
+ {
+ "www.example.org",
+ new SniConfig
+ {
+ // I'm not using Http1AndHttp2 or Http2 because I don't want to account for
+ // validation and normalization. Other tests cover that.
+ Protocols = HttpProtocols.Http1,
+ Certificate = new CertificateConfig()
+ }
+ }
+ };
+
+ var sniOptionsSelector = new SniOptionsSelector(
+ "TestEndpointName",
+ sniDictionary,
+ new MockCertificateConfigLoader(),
+ new HttpsConnectionAdapterOptions(),
+ fallbackHttpProtocols: HttpProtocols.None,
+ logger: Mock.Of>());
+
+ var options = sniOptionsSelector.GetOptions(new MockConnectionContext(), "www.example.org");
+ var alpnList = options.ApplicationProtocols;
+
+ Assert.NotNull(alpnList);
+ var protocol = Assert.Single(alpnList);
+ Assert.Equal(SslApplicationProtocol.Http11, protocol);
+ }
+
+ [Fact]
+ public void FallsBackToFallbackHttpProtocols()
+ {
+ var sniDictionary = new Dictionary
+ {
+ {
+ "www.example.org",
+ new SniConfig
+ {
+ Certificate = new CertificateConfig()
+ }
+ }
+ };
+
+ var sniOptionsSelector = new SniOptionsSelector(
+ "TestEndpointName",
+ sniDictionary,
+ new MockCertificateConfigLoader(),
+ new HttpsConnectionAdapterOptions(),
+ fallbackHttpProtocols: HttpProtocols.Http1,
+ logger: Mock.Of>());
+
+ var mockConnectionContext = new MockConnectionContext();
+ sniOptionsSelector.GetOptions(mockConnectionContext, "www.example.org");
+
+ var httpProtocolsFeature = mockConnectionContext.Features.Get();
+ Assert.NotNull(httpProtocolsFeature);
+ Assert.Equal(HttpProtocols.Http1, httpProtocolsFeature.HttpProtocols);
+ }
+
+ [Fact]
+ public void PrefersSslProtocolsDefinedInSniConfig()
+ {
+ var sniDictionary = new Dictionary
+ {
+ {
+ "www.example.org",
+ new SniConfig
+ {
+ SslProtocols = SslProtocols.Tls13 | SslProtocols.Tls11,
+ Certificate = new CertificateConfig()
+ }
+ }
+ };
+
+ var sniOptionsSelector = new SniOptionsSelector(
+ "TestEndpointName",
+ sniDictionary,
+ new MockCertificateConfigLoader(),
+ new HttpsConnectionAdapterOptions
+ {
+ SslProtocols = SslProtocols.Tls13
+ },
+ fallbackHttpProtocols: HttpProtocols.Http1AndHttp2,
+ logger: Mock.Of>());
+
+ var options = sniOptionsSelector.GetOptions(new MockConnectionContext(), "www.example.org");
+ Assert.Equal(SslProtocols.Tls13 | SslProtocols.Tls11, options.EnabledSslProtocols);
+ }
+
+ [Fact]
+ public void FallsBackToFallbackSslProtocols()
+ {
+ var sniDictionary = new Dictionary
+ {
+ {
+ "www.example.org",
+ new SniConfig
+ {
+ Certificate = new CertificateConfig()
+ }
+ }
+ };
+
+ var sniOptionsSelector = new SniOptionsSelector(
+ "TestEndpointName",
+ sniDictionary,
+ new MockCertificateConfigLoader(),
+ new HttpsConnectionAdapterOptions
+ {
+ SslProtocols = SslProtocols.Tls13
+ },
+ fallbackHttpProtocols: HttpProtocols.Http1AndHttp2,
+ logger: Mock.Of>());
+
+ var options = sniOptionsSelector.GetOptions(new MockConnectionContext(), "www.example.org");
+ Assert.Equal(SslProtocols.Tls13, options.EnabledSslProtocols);
+ }
+
+
+ [Fact]
+ public void PrefersClientCertificateModeDefinedInSniConfig()
+ {
+ var sniDictionary = new Dictionary
+ {
+ {
+ "www.example.org",
+ new SniConfig
+ {
+ ClientCertificateMode = ClientCertificateMode.RequireCertificate,
+ Certificate = new CertificateConfig()
+ }
+ }
+ };
+
+ var sniOptionsSelector = new SniOptionsSelector(
+ "TestEndpointName",
+ sniDictionary,
+ new MockCertificateConfigLoader(),
+ new HttpsConnectionAdapterOptions
+ {
+ ClientCertificateMode = ClientCertificateMode.AllowCertificate
+ },
+ fallbackHttpProtocols: HttpProtocols.Http1AndHttp2,
+ logger: Mock.Of>());
+
+ var options = sniOptionsSelector.GetOptions(new MockConnectionContext(), "www.example.org");
+
+ Assert.True(options.ClientCertificateRequired);
+
+ Assert.NotNull(options.RemoteCertificateValidationCallback);
+ // The RemoteCertificateValidationCallback should first check if the certificate is null and return false since it's required.
+ Assert.False(options.RemoteCertificateValidationCallback(sender: null, certificate: null, chain: null, SslPolicyErrors.None));
+ }
+
+ [Fact]
+ public void FallsBackToFallbackClientCertificateMode()
+ {
+ var sniDictionary = new Dictionary
+ {
+ {
+ "www.example.org",
+ new SniConfig
+ {
+ Certificate = new CertificateConfig()
+ }
+ }
+ };
+
+ var sniOptionsSelector = new SniOptionsSelector(
+ "TestEndpointName",
+ sniDictionary,
+ new MockCertificateConfigLoader(),
+ new HttpsConnectionAdapterOptions
+ {
+ ClientCertificateMode = ClientCertificateMode.AllowCertificate
+ },
+ fallbackHttpProtocols: HttpProtocols.Http1AndHttp2,
+ logger: Mock.Of>());
+
+ var options = sniOptionsSelector.GetOptions(new MockConnectionContext(), "www.example.org");
+
+ // Despite the confusing name, ClientCertificateRequired being true simply requests a certificate from the client, but doesn't require it.
+ Assert.True(options.ClientCertificateRequired);
+
+ Assert.NotNull(options.RemoteCertificateValidationCallback);
+ // The RemoteCertificateValidationCallback should see we're in the AllowCertificate mode and return true.
+ Assert.True(options.RemoteCertificateValidationCallback(sender: null, certificate: null, chain: null, SslPolicyErrors.None));
+ }
+
+ [Fact]
+ public void CloneSslOptionsClonesAllProperties()
+ {
+ var propertyNames = typeof(SslServerAuthenticationOptions).GetProperties().Select(property => property.Name).ToList();
+
+ CipherSuitesPolicy cipherSuitesPolicy = null;
+
+ if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
+ {
+ try
+ {
+ // The CipherSuitesPolicy ctor throws a PlatformNotSupportedException on Windows.
+ cipherSuitesPolicy = new CipherSuitesPolicy(new[] { TlsCipherSuite.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384 });
+ }
+ catch (PlatformNotSupportedException)
+ {
+ // The CipherSuitesPolicy ctor throws a PlatformNotSupportedException on Ubuntu 16.04.
+ // I don't know exactly which other distros/versions throw PNEs, but it isn't super relevant to this test,
+ // so let's just swallow this exception.
+ }
+ }
+
+ // Set options properties to non-default values to verify they're copied.
+ var options = new SslServerAuthenticationOptions
+ {
+ // Defaults to true
+ AllowRenegotiation = false,
+ // Defaults to null
+ ApplicationProtocols = new List { SslApplicationProtocol.Http2 },
+ // Defaults to X509RevocationMode.NoCheck
+ CertificateRevocationCheckMode = X509RevocationMode.Offline,
+ // Defaults to null
+ CipherSuitesPolicy = cipherSuitesPolicy,
+ // Defaults to false
+ ClientCertificateRequired = true,
+ // Defaults to SslProtocols.None
+ EnabledSslProtocols = SslProtocols.Tls13 | SslProtocols.Tls11,
+ // Defaults to EncryptionPolicy.RequireEncryption
+ EncryptionPolicy = EncryptionPolicy.NoEncryption,
+ // Defaults to null
+ RemoteCertificateValidationCallback = (sender, certificate, chain, sslPolicyErrors) => true,
+ // Defaults to null
+ ServerCertificate = new X509Certificate2(),
+ // Defaults to null
+ ServerCertificateContext = SslStreamCertificateContext.Create(_x509Certificate2, additionalCertificates: null, offline: true),
+ // Defaults to null
+ ServerCertificateSelectionCallback = (sender, serverName) => null,
+ };
+
+ var clonedOptions = SniOptionsSelector.CloneSslOptions(options);
+
+ Assert.NotSame(options, clonedOptions);
+
+ Assert.Equal(options.AllowRenegotiation, clonedOptions.AllowRenegotiation);
+ Assert.True(propertyNames.Remove(nameof(options.AllowRenegotiation)));
+
+ // Ensure the List is also cloned since it could be modified by a user callback.
+ Assert.NotSame(options.ApplicationProtocols, clonedOptions.ApplicationProtocols);
+ Assert.Equal(Assert.Single(options.ApplicationProtocols), Assert.Single(clonedOptions.ApplicationProtocols));
+ Assert.True(propertyNames.Remove(nameof(options.ApplicationProtocols)));
+
+ Assert.Equal(options.CertificateRevocationCheckMode, clonedOptions.CertificateRevocationCheckMode);
+ Assert.True(propertyNames.Remove(nameof(options.CertificateRevocationCheckMode)));
+
+ Assert.Same(options.CipherSuitesPolicy, clonedOptions.CipherSuitesPolicy);
+ Assert.True(propertyNames.Remove(nameof(options.CipherSuitesPolicy)));
+
+ Assert.Equal(options.ClientCertificateRequired, clonedOptions.ClientCertificateRequired);
+ Assert.True(propertyNames.Remove(nameof(options.ClientCertificateRequired)));
+
+ Assert.Equal(options.EnabledSslProtocols, clonedOptions.EnabledSslProtocols);
+ Assert.True(propertyNames.Remove(nameof(options.EnabledSslProtocols)));
+
+ Assert.Equal(options.EncryptionPolicy, clonedOptions.EncryptionPolicy);
+ Assert.True(propertyNames.Remove(nameof(options.EncryptionPolicy)));
+
+ Assert.Same(options.RemoteCertificateValidationCallback, clonedOptions.RemoteCertificateValidationCallback);
+ Assert.True(propertyNames.Remove(nameof(options.RemoteCertificateValidationCallback)));
+
+ // Technically the ServerCertificate could be reset/reimported, but I'm hoping this is uncommon. Trying to clone the certificate and/or context seems risky.
+ Assert.Same(options.ServerCertificate, clonedOptions.ServerCertificate);
+ Assert.True(propertyNames.Remove(nameof(options.ServerCertificate)));
+
+ Assert.Same(options.ServerCertificateContext, clonedOptions.ServerCertificateContext);
+ Assert.True(propertyNames.Remove(nameof(options.ServerCertificateContext)));
+
+ Assert.Same(options.ServerCertificateSelectionCallback, clonedOptions.ServerCertificateSelectionCallback);
+ Assert.True(propertyNames.Remove(nameof(options.ServerCertificateSelectionCallback)));
+
+ // Ensure we've checked every property. When new properties get added, we'll have to update this test along with the CloneSslOptions implementation.
+ Assert.Empty(propertyNames);
+ }
+
+ private class MockCertificateConfigLoader : ICertificateConfigLoader
+ {
+ public Dictionary