Kestrel SNI from config (#24286)
This commit is contained in:
parent
c9064a9b95
commit
292cb9c5dd
|
|
@ -118,7 +118,7 @@ namespace Microsoft.AspNetCore.Routing.Matching
|
|||
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.
|
||||
|
|
|
|||
|
|
@ -620,4 +620,16 @@ For more information on configuring HTTPS see https://go.microsoft.com/fwlink/?l
|
|||
<data name="UnrecognizedCertificateKeyOid" xml:space="preserve">
|
||||
<value>Unknown algorithm for certificate with public key type '{0}'.</value>
|
||||
</data>
|
||||
<data name="SniNotConfiguredForServerName" xml:space="preserve">
|
||||
<value>Connection refused because no SNI configuration section was found for '{serverName}' in '{endpointName}'. To allow all connections, add a wildcard ('*') SNI section.</value>
|
||||
</data>
|
||||
<data name="SniNotConfiguredToAllowNoServerName" xml:space="preserve">
|
||||
<value>Connection refused because the client did not specify a server name, and no wildcard ('*') SNI configuration section was found in '{endpointName}'.</value>
|
||||
</data>
|
||||
<data name="SniNameCannotBeEmpty" xml:space="preserve">
|
||||
<value>The endpoint {endpointName} is invalid because an SNI configuration section has an empty string as its key. Use a wildcard ('*') SNI section to match all server names.</value>
|
||||
</data>
|
||||
<data name="EndpointHasUnusedHttpsConfig" xml:space="preserve">
|
||||
<value>The non-HTTPS endpoint {endpointName} includes HTTPS-only configuration for {keyName}.</value>
|
||||
</data>
|
||||
</root>
|
||||
|
|
@ -16,6 +16,8 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Https
|
|||
/// </summary>
|
||||
public class HttpsConnectionAdapterOptions
|
||||
{
|
||||
internal static TimeSpan DefaultHandshakeTimeout = TimeSpan.FromSeconds(10);
|
||||
|
||||
private TimeSpan _handshakeTimeout;
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -24,7 +26,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Https
|
|||
public HttpsConnectionAdapterOptions()
|
||||
{
|
||||
ClientCertificateMode = ClientCertificateMode.NoCertificate;
|
||||
HandshakeTimeout = TimeSpan.FromSeconds(10);
|
||||
HandshakeTimeout = DefaultHandshakeTimeout;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -91,7 +93,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Https
|
|||
public Action<ConnectionContext, SslServerAuthenticationOptions> OnAuthenticate { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
public TimeSpan HandshakeTimeout
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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<KestrelServer> logger)
|
||||
{
|
||||
HostEnvironment = hostEnvironment;
|
||||
Logger = logger;
|
||||
}
|
||||
|
||||
public IHostEnvironment HostEnvironment { get; }
|
||||
public ILogger<KestrelServer> 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -20,6 +20,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal
|
|||
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 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal
|
|||
}
|
||||
|
||||
// "EndpointDefaults": {
|
||||
// "Protocols": "Http1AndHttp2",
|
||||
// "SslProtocols": [ "Tls11", "Tls12", "Tls13"],
|
||||
// "ClientCertificateMode" : "NoCertificate"
|
||||
// "Protocols": "Http1AndHttp2",
|
||||
// "SslProtocols": [ "Tls11", "Tls12", "Tls13"],
|
||||
// "ClientCertificateMode" : "NoCertificate"
|
||||
// }
|
||||
private EndpointDefaults ReadEndpointDefaults()
|
||||
{
|
||||
|
|
@ -61,7 +62,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal
|
|||
{
|
||||
Protocols = ParseProtocols(configSection[ProtocolsKey]),
|
||||
SslProtocols = ParseSslProcotols(configSection.GetSection(SslProtocolsKey)),
|
||||
ClientCertificateMode = ParseClientCertificateMode(configSection[ClientCertificateModeKey])
|
||||
ClientCertificateMode = ParseClientCertificateMode(configSection[ClientCertificateModeKey]),
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -73,14 +74,25 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal
|
|||
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 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal
|
|||
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 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal
|
|||
return endpoints;
|
||||
}
|
||||
|
||||
private ClientCertificateMode? ParseClientCertificateMode(string clientCertificateMode)
|
||||
private static Dictionary<string, SniConfig> ReadSni(IConfigurationSection sniConfig, string endpointName)
|
||||
{
|
||||
var sniDictionary = new Dictionary<string, SniConfig>(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>(clientCertificateMode, ignoreCase: true, out var result))
|
||||
{
|
||||
|
|
@ -140,12 +199,35 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal
|
|||
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 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal
|
|||
}
|
||||
|
||||
// "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 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal
|
|||
public SslProtocols? SslProtocols { get; set; }
|
||||
public CertificateConfig Certificate { get; set; }
|
||||
public ClientCertificateMode? ClientCertificateMode { get; set; }
|
||||
public Dictionary<string, SniConfig> Sni { get; set; }
|
||||
|
||||
// Compare config sections because it's accessible to app developers via an Action<EndpointConfiguration> callback.
|
||||
// We cannot rely entirely on comparing config sections for equality, because KestrelConfigurationLoader.Reload() sets
|
||||
|
|
@ -196,19 +290,63 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal
|
|||
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<string, SniConfig> lhs, Dictionary<string, SniConfig> 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 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal
|
|||
ConfigSection.Bind(this);
|
||||
}
|
||||
|
||||
// For testing
|
||||
internal CertificateConfig()
|
||||
{
|
||||
}
|
||||
|
||||
public IConfigurationSection ConfigSection { get; }
|
||||
|
||||
// File
|
||||
|
|
@ -244,13 +387,14 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal
|
|||
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);
|
||||
|
|
|
|||
|
|
@ -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; }
|
||||
}
|
||||
}
|
||||
|
|
@ -58,19 +58,20 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal
|
|||
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<KestrelServer> 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<KestrelServer> logger) => _unableToLocateDevelopmentCertificate(logger, null);
|
||||
|
||||
public static void FailedToLocateDevelopmentCertificateFile(this ILogger logger, string certificatePath) => _failedToLocateDevelopmentCertificateFile(logger, certificatePath, null);
|
||||
public static void FailedToLocateDevelopmentCertificateFile(this ILogger<KestrelServer> 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<KestrelServer> logger, string certificatePath) => _failedToLoadDevelopmentCertificate(logger, certificatePath, null);
|
||||
|
||||
public static void BadDeveloperCertificateState(this ILogger logger) => _badDeveloperCertificateState(logger, null);
|
||||
public static void BadDeveloperCertificateState(this ILogger<KestrelServer> logger) => _badDeveloperCertificateState(logger, null);
|
||||
|
||||
public static void DeveloperCertificateFirstRun(this ILogger logger, string message) => _developerCertificateFirstRun(logger, message, null);
|
||||
public static void DeveloperCertificateFirstRun(this ILogger<KestrelServer> 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<KestrelServer> logger, string certificatePath) => _failedToLoadCertificate(logger, certificatePath, null);
|
||||
|
||||
public static void FailedToLoadCertificateKey(this ILogger<KestrelServer> logger, string certificateKeyPath) => _failedToLoadCertificateKey(logger, certificateKeyPath, null);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<ConnectionContext, string, X509Certificate2> _fallbackServerCertificateSelector;
|
||||
private readonly Action<ConnectionContext, SslServerAuthenticationOptions> _onAuthenticateCallback;
|
||||
|
||||
private readonly Dictionary<string, SniOptions> _exactNameOptions = new Dictionary<string, SniOptions>(StringComparer.OrdinalIgnoreCase);
|
||||
private readonly SortedList<string, SniOptions> _wildcardPrefixOptions = new SortedList<string, SniOptions>(LongestStringFirstComparer.Instance);
|
||||
private readonly SniOptions _wildcardOptions;
|
||||
|
||||
public SniOptionsSelector(
|
||||
string endpointName,
|
||||
Dictionary<string, SniConfig> sniDictionary,
|
||||
ICertificateConfigLoader certifcateConfigLoader,
|
||||
HttpsConnectionAdapterOptions fallbackHttpsOptions,
|
||||
HttpProtocols fallbackHttpProtocols,
|
||||
ILogger<HttpsConnectionMiddleware> 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<SslServerAuthenticationOptions> 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<SslServerAuthenticationOptions>(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<string>
|
||||
{
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -6,17 +6,16 @@ using System.Collections.Generic;
|
|||
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 @@ namespace Microsoft.AspNetCore.Server.Kestrel
|
|||
{
|
||||
private bool _loaded = false;
|
||||
|
||||
internal KestrelConfigurationLoader(KestrelServerOptions options, IConfiguration configuration, bool reloadOnChange)
|
||||
internal KestrelConfigurationLoader(
|
||||
KestrelServerOptions options,
|
||||
IConfiguration configuration,
|
||||
IHostEnvironment hostEnvironment,
|
||||
bool reloadOnChange,
|
||||
ILogger<KestrelServer> logger,
|
||||
ILogger<HttpsConnectionMiddleware> 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 @@ namespace Microsoft.AspNetCore.Server.Kestrel
|
|||
/// </summary>
|
||||
internal bool ReloadOnChange { get; }
|
||||
|
||||
private IHostEnvironment HostEnvironment { get; }
|
||||
private ILogger<KestrelServer> Logger { get; }
|
||||
private ILogger<HttpsConnectionMiddleware> HttpsLogger { get; }
|
||||
|
||||
private ConfigurationReader ConfigurationReader { get; set; }
|
||||
|
||||
private ICertificateConfigLoader CertificateConfigLoader { get; }
|
||||
|
||||
private IDictionary<string, Action<EndpointConfiguration>> EndpointConfigurations { get; }
|
||||
= new Dictionary<string, Action<EndpointConfiguration>>(0, StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
|
|
@ -215,9 +231,9 @@ namespace Microsoft.AspNetCore.Server.Kestrel
|
|||
return this;
|
||||
}
|
||||
|
||||
// Called from ApplyEndpointDefaults so it applies to even explicit Listen endpoints.
|
||||
// Called from KestrelServerOptions.ApplyEndpointDefaults so it applies to even explicit Listen endpoints.
|
||||
// Does not require a call to Load.
|
||||
internal void ApplyConfigurationDefaults(ListenOptions listenOptions)
|
||||
internal void ApplyEndpointDefaults(ListenOptions listenOptions)
|
||||
{
|
||||
var defaults = ConfigurationReader.EndpointDefaults;
|
||||
|
||||
|
|
@ -227,6 +243,23 @@ namespace Microsoft.AspNetCore.Server.Kestrel
|
|||
}
|
||||
}
|
||||
|
||||
// Called from KestrelServerOptions.ApplyHttpsDefaults so it applies to even explicit Listen endpoints.
|
||||
// Does not require a call to Load.
|
||||
internal void ApplyHttpsDefaults(HttpsConnectionAdapterOptions httpsOptions)
|
||||
{
|
||||
var defaults = ConfigurationReader.EndpointDefaults;
|
||||
|
||||
if (defaults.SslProtocols.HasValue)
|
||||
{
|
||||
httpsOptions.SslProtocols = defaults.SslProtocols.Value;
|
||||
}
|
||||
|
||||
if (defaults.ClientCertificateMode.HasValue)
|
||||
{
|
||||
httpsOptions.ClientCertificateMode = defaults.ClientCertificateMode.Value;
|
||||
}
|
||||
}
|
||||
|
||||
public void Load()
|
||||
{
|
||||
if (_loaded)
|
||||
|
|
@ -256,12 +289,17 @@ namespace Microsoft.AspNetCore.Server.Kestrel
|
|||
|
||||
ConfigurationReader = new ConfigurationReader(Configuration);
|
||||
|
||||
LoadDefaultCert(ConfigurationReader);
|
||||
LoadDefaultCert();
|
||||
|
||||
foreach (var endpoint in ConfigurationReader.Endpoints)
|
||||
{
|
||||
var listenOptions = AddressBinder.ParseAddress(endpoint.Url, out var https);
|
||||
|
||||
if (!https)
|
||||
{
|
||||
ConfigurationReader.ThrowIfContainsHttpsOnlyConfiguration(endpoint);
|
||||
}
|
||||
|
||||
Options.ApplyEndpointDefaults(listenOptions);
|
||||
|
||||
if (endpoint.Protocols.HasValue)
|
||||
|
|
@ -277,11 +315,9 @@ namespace Microsoft.AspNetCore.Server.Kestrel
|
|||
|
||||
// Compare to UseHttps(httpsOptions => { })
|
||||
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 @@ namespace Microsoft.AspNetCore.Server.Kestrel
|
|||
{
|
||||
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 @@ namespace Microsoft.AspNetCore.Server.Kestrel
|
|||
// 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 @@ namespace Microsoft.AspNetCore.Server.Kestrel
|
|||
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 @@ namespace Microsoft.AspNetCore.Server.Kestrel
|
|||
}
|
||||
else
|
||||
{
|
||||
var logger = Options.ApplicationServices.GetRequiredService<ILogger<KestrelServer>>();
|
||||
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<KestrelServer> 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 @@ namespace Microsoft.AspNetCore.Server.Kestrel
|
|||
}
|
||||
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 @@ namespace Microsoft.AspNetCore.Server.Kestrel
|
|||
|
||||
private bool TryGetCertificatePath(out string path)
|
||||
{
|
||||
var hostingEnvironment = Options.ApplicationServices.GetRequiredService<IHostEnvironment>();
|
||||
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<ILogger<KestrelConfigurationLoader>>();
|
||||
if (certInfo.IsFileCert && certInfo.IsStoreCert)
|
||||
{
|
||||
throw new InvalidOperationException(CoreStrings.FormatMultipleCertificateSources(endpointName));
|
||||
}
|
||||
else if (certInfo.IsFileCert)
|
||||
{
|
||||
var environment = Options.ApplicationServices.GetRequiredService<IHostEnvironment>();
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -12,9 +12,12 @@ using Microsoft.AspNetCore.Certificates.Generation;
|
|||
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 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core
|
|||
internal void ApplyEndpointDefaults(ListenOptions listenOptions)
|
||||
{
|
||||
listenOptions.KestrelServerOptions = this;
|
||||
ConfigurationLoader?.ApplyConfigurationDefaults(listenOptions);
|
||||
ConfigurationLoader?.ApplyEndpointDefaults(listenOptions);
|
||||
EndpointDefaults(listenOptions);
|
||||
}
|
||||
|
||||
|
|
@ -153,6 +156,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core
|
|||
|
||||
internal void ApplyHttpsDefaults(HttpsConnectionAdapterOptions httpsOptions)
|
||||
{
|
||||
ConfigurationLoader?.ApplyHttpsDefaults(httpsOptions);
|
||||
HttpsDefaults(httpsOptions);
|
||||
}
|
||||
|
||||
|
|
@ -240,7 +244,16 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core
|
|||
/// <returns>A <see cref="KestrelConfigurationLoader"/> for further endpoint configuration.</returns>
|
||||
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<KestrelServerOptions>)}.");
|
||||
}
|
||||
|
||||
var hostEnvironment = ApplicationServices.GetRequiredService<IHostEnvironment>();
|
||||
var logger = ApplicationServices.GetRequiredService<ILogger<KestrelServer>>();
|
||||
var httpsLogger = ApplicationServices.GetRequiredService<ILogger<HttpsConnectionMiddleware>>();
|
||||
|
||||
var loader = new KestrelConfigurationLoader(this, config, hostEnvironment, reloadOnChange, logger, httpsLogger);
|
||||
ConfigurationLoader = loader;
|
||||
return loader;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 @@ namespace Microsoft.AspNetCore.Hosting
|
|||
/// <returns>The <see cref="ListenOptions"/>.</returns>
|
||||
public static ListenOptions UseHttps(this ListenOptions listenOptions, string fileName)
|
||||
{
|
||||
var env = listenOptions.KestrelServerOptions.ApplicationServices.GetRequiredService<IHostEnvironment>();
|
||||
var env = listenOptions.ApplicationServices.GetRequiredService<IHostEnvironment>();
|
||||
return listenOptions.UseHttps(new X509Certificate2(Path.Combine(env.ContentRootPath, fileName)));
|
||||
}
|
||||
|
||||
|
|
@ -50,7 +52,7 @@ namespace Microsoft.AspNetCore.Hosting
|
|||
/// <returns>The <see cref="ListenOptions"/>.</returns>
|
||||
public static ListenOptions UseHttps(this ListenOptions listenOptions, string fileName, string password)
|
||||
{
|
||||
var env = listenOptions.KestrelServerOptions.ApplicationServices.GetRequiredService<IHostEnvironment>();
|
||||
var env = listenOptions.ApplicationServices.GetRequiredService<IHostEnvironment>();
|
||||
return listenOptions.UseHttps(new X509Certificate2(Path.Combine(env.ContentRootPath, fileName), password));
|
||||
}
|
||||
|
||||
|
|
@ -65,7 +67,7 @@ namespace Microsoft.AspNetCore.Hosting
|
|||
public static ListenOptions UseHttps(this ListenOptions listenOptions, string fileName, string password,
|
||||
Action<HttpsConnectionAdapterOptions> configureOptions)
|
||||
{
|
||||
var env = listenOptions.KestrelServerOptions.ApplicationServices.GetRequiredService<IHostEnvironment>();
|
||||
var env = listenOptions.ApplicationServices.GetRequiredService<IHostEnvironment>();
|
||||
return listenOptions.UseHttps(new X509Certificate2(Path.Combine(env.ContentRootPath, fileName), password), configureOptions);
|
||||
}
|
||||
|
||||
|
|
@ -227,5 +229,27 @@ namespace Microsoft.AspNetCore.Hosting
|
|||
|
||||
return listenOptions;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Configure Kestrel to use HTTPS.
|
||||
/// </summary>
|
||||
/// <param name="listenOptions">The <see cref="ListenOptions"/> to configure.</param>
|
||||
/// <param name="httpsOptionsCallback">Callback to configure HTTPS options.</param>
|
||||
/// <param name="state">State for the <see cref="ServerOptionsSelectionCallback" />.</param>
|
||||
/// <param name="handshakeTimeout">Specifies the maximum amount of time allowed for the TLS/SSL handshake. This must be positive and finite.</param>
|
||||
/// <returns>The <see cref="ListenOptions"/>.</returns>
|
||||
internal static ListenOptions UseHttps(this ListenOptions listenOptions, HttpsOptionsCallback httpsOptionsCallback, object state, TimeSpan handshakeTimeout)
|
||||
{
|
||||
var loggerFactory = listenOptions.KestrelServerOptions?.ApplicationServices.GetRequiredService<ILoggerFactory>() ?? NullLoggerFactory.Instance;
|
||||
|
||||
listenOptions.IsTls = true;
|
||||
listenOptions.Use(next =>
|
||||
{
|
||||
var middleware = new HttpsConnectionMiddleware(next, httpsOptionsCallback, state, handshakeTimeout, loggerFactory);
|
||||
return middleware.OnConnectionAsync;
|
||||
});
|
||||
|
||||
return listenOptions;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,13 +13,13 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal
|
|||
{
|
||||
private readonly ServiceContext _serviceContext;
|
||||
private readonly IHttpApplication<TContext> _application;
|
||||
private readonly HttpProtocols _protocols;
|
||||
private readonly HttpProtocols _endpointDefaultProtocols;
|
||||
|
||||
public HttpConnectionMiddleware(ServiceContext serviceContext, IHttpApplication<TContext> application, HttpProtocols protocols)
|
||||
{
|
||||
_serviceContext = serviceContext;
|
||||
_application = application;
|
||||
_protocols = protocols;
|
||||
_endpointDefaultProtocols = protocols;
|
||||
}
|
||||
|
||||
public Task OnConnectionAsync(ConnectionContext connectionContext)
|
||||
|
|
@ -30,7 +30,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal
|
|||
{
|
||||
ConnectionId = connectionContext.ConnectionId,
|
||||
ConnectionContext = connectionContext,
|
||||
Protocols = _protocols,
|
||||
Protocols = connectionContext.Features.Get<HttpProtocolsFeature>()?.HttpProtocols ?? _endpointDefaultProtocols,
|
||||
ServiceContext = _serviceContext,
|
||||
ConnectionFeatures = connectionContext.Features,
|
||||
MemoryPool = memoryPoolFeature?.MemoryPool ?? System.Buffers.MemoryPool<byte>.Shared,
|
||||
|
|
|
|||
|
|
@ -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 @@ using Microsoft.Extensions.Logging.Abstractions;
|
|||
|
||||
namespace Microsoft.AspNetCore.Server.Kestrel.Https.Internal
|
||||
{
|
||||
internal delegate ValueTask<SslServerAuthenticationOptions> 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<HttpsConnectionMiddleware> _logger;
|
||||
private readonly Func<Stream, SslStream> _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<ConnectionContext, string, X509Certificate2> _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,36 +58,28 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Https.Internal
|
|||
throw new ArgumentNullException(nameof(options));
|
||||
}
|
||||
|
||||
_options = options;
|
||||
_logger = loggerFactory.CreateLogger<HttpsConnectionMiddleware>();
|
||||
|
||||
// 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())
|
||||
{
|
||||
_logger.Http2DefaultCiphersInsufficient();
|
||||
options.HttpProtocols = HttpProtocols.Http1;
|
||||
}
|
||||
|
||||
_next = next;
|
||||
// capture the certificate now so it can't be switched after validation
|
||||
_serverCertificate = options.ServerCertificate;
|
||||
_serverCertificateSelector = options.ServerCertificateSelector;
|
||||
if (_serverCertificate == null && _serverCertificateSelector == null)
|
||||
if (options.ServerCertificate == null && options.ServerCertificateSelector == null)
|
||||
{
|
||||
throw new ArgumentException(CoreStrings.ServerCertificateRequired, nameof(options));
|
||||
}
|
||||
|
||||
_next = next;
|
||||
_handshakeTimeout = options.HandshakeTimeout;
|
||||
_logger = loggerFactory.CreateLogger<HttpsConnectionMiddleware>();
|
||||
|
||||
// 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<string, SniConfig> { { "*", 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 a selector is provided then ignore the cert, it may be a default cert.
|
||||
if (_serverCertificateSelector != null)
|
||||
{
|
||||
|
|
@ -84,13 +90,33 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Https.Internal
|
|||
{
|
||||
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<HttpsConnectionMiddleware>();
|
||||
|
||||
_httpsOptionsCallback = httpsOptionsCallback;
|
||||
_httpsOptionsCallbackState = httpsOptionsCallbackState;
|
||||
_sslStreamFactory = s => new SslStream(s);
|
||||
}
|
||||
|
||||
public async Task OnConnectionAsync(ConnectionContext context)
|
||||
{
|
||||
await Task.Yield();
|
||||
|
||||
bool certificateRequired;
|
||||
if (context.Features.Get<ITlsConnectionFeature>() != null)
|
||||
{
|
||||
await _next(context);
|
||||
|
|
@ -101,152 +127,54 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Https.Internal
|
|||
context.Features.Set<ITlsConnectionFeature>(feature);
|
||||
context.Features.Set<ITlsHandshakeFeature>(feature);
|
||||
|
||||
var memoryPool = context.Features.Get<IMemoryPoolFeature>()?.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<IMemoryPoolFeature>()?.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<SslApplicationProtocol>()
|
||||
};
|
||||
|
||||
// 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);
|
||||
_logger.AuthenticationFailed(ex);
|
||||
await sslStream.DisposeAsync();
|
||||
return;
|
||||
}
|
||||
catch (AuthenticationException ex)
|
||||
{
|
||||
KestrelEventSource.Log.TlsHandshakeFailed(context.ConnectionId);
|
||||
KestrelEventSource.Log.TlsHandshakeStop(context, null);
|
||||
|
||||
await sslStream.DisposeAsync();
|
||||
return;
|
||||
}
|
||||
_logger.AuthenticationFailed(ex);
|
||||
|
||||
await sslStream.DisposeAsync();
|
||||
return;
|
||||
}
|
||||
|
||||
feature.ApplicationProtocol = sslStream.NegotiatedApplicationProtocol.Protocol;
|
||||
context.Features.Set<ITlsApplicationProtocolFeature>(feature);
|
||||
|
||||
feature.ClientCertificate = ConvertToX509Certificate2(sslStream.RemoteCertificate);
|
||||
feature.CipherAlgorithm = sslStream.CipherAlgorithm;
|
||||
feature.CipherStrength = sslStream.CipherStrength;
|
||||
|
|
@ -282,7 +210,135 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Https.Internal
|
|||
}
|
||||
}
|
||||
|
||||
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<SslApplicationProtocol>();
|
||||
|
||||
// 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<X509Certificate2, X509Chain, SslPolicyErrors, bool> 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<byte> 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<SslServerAuthenticationOptions> ServerOptionsCallback(SslStream sslStream, SslClientHelloInfo clientHelloInfo, object state, CancellationToken cancellationToken)
|
||||
{
|
||||
var (middleware, context, feature) = (ValueTuple<HttpsConnectionMiddleware, ConnectionContext, Core.Internal.TlsConnectionFeature>)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 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Https.Internal
|
|||
return new X509Certificate2(certificate);
|
||||
}
|
||||
|
||||
private static bool IsWindowsVersionIncompatible()
|
||||
internal static HttpProtocols ValidateAndNormalizeHttpProtocols(HttpProtocols httpProtocols, ILogger<HttpsConnectionMiddleware> 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 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Https.Internal
|
|||
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<HttpsConnectionMiddleware> logger, Exception exception) => _authenticationFailed(logger, exception);
|
||||
|
||||
public static void AuthenticationTimedOut(this ILogger logger) => _authenticationTimedOut(logger, null);
|
||||
public static void AuthenticationTimedOut(this ILogger<HttpsConnectionMiddleware> 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<HttpsConnectionMiddleware> 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<HttpsConnectionMiddleware> logger) => _http2DefaultCiphersInsufficient(logger, null);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
|
|||
Assert.Equal(HttpProtocols.Http2, options.CodeBackedListenOptions[3].Protocols);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ConfigureThrowsInvalidOperationExceptionIfApplicationServicesIsNotSet()
|
||||
{
|
||||
var options = new KestrelServerOptions();
|
||||
Assert.Throws<InvalidOperationException>(() => options.Configure());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ConfigureThrowsInvalidOperationExceptionIfApplicationServicesDoesntHaveRequiredServices()
|
||||
{
|
||||
var options = new KestrelServerOptions
|
||||
{
|
||||
ApplicationServices = new ServiceCollection().BuildServiceProvider()
|
||||
};
|
||||
|
||||
Assert.Throws<InvalidOperationException>(() => 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<IHostEnvironment>());
|
||||
serviceCollection.AddSingleton(Mock.Of<ILogger<KestrelServer>>());
|
||||
serviceCollection.AddSingleton(Mock.Of<ILogger<HttpsConnectionMiddleware>>());
|
||||
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 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
|
|||
}
|
||||
|
||||
[Fact]
|
||||
public void SettingRequestHeaderEncodingSelecterThrowsArgumentNullException()
|
||||
public void SettingRequestHeaderEncodingSelecterToNullThrowsArgumentNullException()
|
||||
{
|
||||
var options = new KestrelServerOptions();
|
||||
|
||||
|
|
|
|||
|
|
@ -13,9 +13,11 @@ using Microsoft.AspNetCore.Hosting.Server;
|
|||
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 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
|
|||
mockLoggerFactory.Setup(m => m.CreateLogger(It.IsAny<string>())).Returns(Mock.Of<ILogger>());
|
||||
|
||||
var serviceCollection = new ServiceCollection();
|
||||
serviceCollection.AddSingleton(mockLoggerFactory.Object);
|
||||
serviceCollection.AddSingleton(Mock.Of<IHostEnvironment>());
|
||||
serviceCollection.AddSingleton(Mock.Of<ILogger<KestrelServer>>());
|
||||
serviceCollection.AddSingleton(Mock.Of<ILogger<HttpsConnectionMiddleware>>());
|
||||
|
||||
var options = new KestrelServerOptions
|
||||
{
|
||||
|
|
@ -629,8 +632,9 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
|
|||
mockLoggerFactory.Setup(m => m.CreateLogger(It.IsAny<string>())).Returns(Mock.Of<ILogger>());
|
||||
|
||||
var serviceCollection = new ServiceCollection();
|
||||
serviceCollection.AddSingleton(mockLoggerFactory.Object);
|
||||
serviceCollection.AddSingleton(Mock.Of<IHostEnvironment>());
|
||||
serviceCollection.AddSingleton(Mock.Of<ILogger<KestrelServer>>());
|
||||
serviceCollection.AddSingleton(Mock.Of<ILogger<HttpsConnectionMiddleware>>());
|
||||
|
||||
var options = new KestrelServerOptions
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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<string, SniConfig>
|
||||
{
|
||||
{
|
||||
"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<ILogger<HttpsConnectionMiddleware>>());
|
||||
|
||||
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<string, SniConfig>
|
||||
{
|
||||
{
|
||||
"*.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<ILogger<HttpsConnectionMiddleware>>());
|
||||
|
||||
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<string, SniConfig>
|
||||
{
|
||||
{
|
||||
"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<ILogger<HttpsConnectionMiddleware>>());
|
||||
|
||||
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<string, SniConfig>(),
|
||||
new MockCertificateConfigLoader(),
|
||||
fallbackHttpsOptions: new HttpsConnectionAdapterOptions(),
|
||||
fallbackHttpProtocols: HttpProtocols.Http1AndHttp2,
|
||||
logger: Mock.Of<ILogger<HttpsConnectionMiddleware>>());
|
||||
|
||||
var authExWithServerName = Assert.Throws<AuthenticationException>(() => sniOptionsSelector.GetOptions(new MockConnectionContext(), "example.org"));
|
||||
Assert.Equal(CoreStrings.FormatSniNotConfiguredForServerName("example.org", "TestEndpointName"), authExWithServerName.Message);
|
||||
|
||||
var authExWithoutServerName = Assert.Throws<AuthenticationException>(() => sniOptionsSelector.GetOptions(new MockConnectionContext(), null));
|
||||
Assert.Equal(CoreStrings.FormatSniNotConfiguredToAllowNoServerName("TestEndpointName"), authExWithoutServerName.Message);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void WildcardOnlyMatchesNullServerNameDueToNoAlpn()
|
||||
{
|
||||
var sniDictionary = new Dictionary<string, SniConfig>
|
||||
{
|
||||
{
|
||||
"*",
|
||||
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<ILogger<HttpsConnectionMiddleware>>());
|
||||
|
||||
var options = sniOptionsSelector.GetOptions(new MockConnectionContext(), null);
|
||||
Assert.Equal("WildcardOnly", pathDictionary[options.ServerCertificate]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CachesSslServerAuthenticationOptions()
|
||||
{
|
||||
var sniDictionary = new Dictionary<string, SniConfig>
|
||||
{
|
||||
{
|
||||
"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<ILogger<HttpsConnectionMiddleware>>());
|
||||
|
||||
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<string, SniConfig>
|
||||
{
|
||||
{
|
||||
"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<ILogger<HttpsConnectionMiddleware>>());
|
||||
|
||||
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<string, SniConfig>
|
||||
{
|
||||
{
|
||||
"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<ILogger<HttpsConnectionMiddleware>>());
|
||||
|
||||
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<string, SniConfig>
|
||||
{
|
||||
{ "www.example.org", new SniConfig() }
|
||||
};
|
||||
|
||||
var ex = Assert.Throws<InvalidOperationException>(
|
||||
() => new SniOptionsSelector(
|
||||
"TestEndpointName",
|
||||
sniDictionary,
|
||||
new MockCertificateConfigLoader(),
|
||||
fallbackHttpsOptions: new HttpsConnectionAdapterOptions(),
|
||||
fallbackHttpProtocols: HttpProtocols.Http1AndHttp2,
|
||||
logger: Mock.Of<ILogger<HttpsConnectionMiddleware>>()));
|
||||
|
||||
Assert.Equal(CoreStrings.NoCertSpecifiedNoDevelopmentCertificateFound, ex.Message);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void FallsBackToHttpsConnectionAdapterCertificate()
|
||||
{
|
||||
var sniDictionary = new Dictionary<string, SniConfig>
|
||||
{
|
||||
{ "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<ILogger<HttpsConnectionMiddleware>>());
|
||||
|
||||
var options = sniOptionsSelector.GetOptions(new MockConnectionContext(), "www.example.org");
|
||||
Assert.Same(fallbackOptions.ServerCertificate, options.ServerCertificate);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void FallsBackToHttpsConnectionAdapterServerCertificateSelectorOverServerCertificate()
|
||||
{
|
||||
var sniDictionary = new Dictionary<string, SniConfig>
|
||||
{
|
||||
{ "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<ILogger<HttpsConnectionMiddleware>>());
|
||||
|
||||
var options = sniOptionsSelector.GetOptions(new MockConnectionContext(), "www.example.org");
|
||||
Assert.Same(selectorCertificate, options.ServerCertificate);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void PrefersHttpProtocolsDefinedInSniConfig()
|
||||
{
|
||||
var sniDictionary = new Dictionary<string, SniConfig>
|
||||
{
|
||||
{
|
||||
"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<ILogger<HttpsConnectionMiddleware>>());
|
||||
|
||||
var mockConnectionContext = new MockConnectionContext();
|
||||
sniOptionsSelector.GetOptions(mockConnectionContext, "www.example.org");
|
||||
|
||||
var httpProtocolsFeature = mockConnectionContext.Features.Get<HttpProtocolsFeature>();
|
||||
Assert.NotNull(httpProtocolsFeature);
|
||||
Assert.Equal(HttpProtocols.None, httpProtocolsFeature.HttpProtocols);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ConfiguresAlpnBasedOnConfiguredHttpProtocols()
|
||||
{
|
||||
var sniDictionary = new Dictionary<string, SniConfig>
|
||||
{
|
||||
{
|
||||
"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<ILogger<HttpsConnectionMiddleware>>());
|
||||
|
||||
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<string, SniConfig>
|
||||
{
|
||||
{
|
||||
"www.example.org",
|
||||
new SniConfig
|
||||
{
|
||||
Certificate = new CertificateConfig()
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
var sniOptionsSelector = new SniOptionsSelector(
|
||||
"TestEndpointName",
|
||||
sniDictionary,
|
||||
new MockCertificateConfigLoader(),
|
||||
new HttpsConnectionAdapterOptions(),
|
||||
fallbackHttpProtocols: HttpProtocols.Http1,
|
||||
logger: Mock.Of<ILogger<HttpsConnectionMiddleware>>());
|
||||
|
||||
var mockConnectionContext = new MockConnectionContext();
|
||||
sniOptionsSelector.GetOptions(mockConnectionContext, "www.example.org");
|
||||
|
||||
var httpProtocolsFeature = mockConnectionContext.Features.Get<HttpProtocolsFeature>();
|
||||
Assert.NotNull(httpProtocolsFeature);
|
||||
Assert.Equal(HttpProtocols.Http1, httpProtocolsFeature.HttpProtocols);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void PrefersSslProtocolsDefinedInSniConfig()
|
||||
{
|
||||
var sniDictionary = new Dictionary<string, SniConfig>
|
||||
{
|
||||
{
|
||||
"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<ILogger<HttpsConnectionMiddleware>>());
|
||||
|
||||
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<string, SniConfig>
|
||||
{
|
||||
{
|
||||
"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<ILogger<HttpsConnectionMiddleware>>());
|
||||
|
||||
var options = sniOptionsSelector.GetOptions(new MockConnectionContext(), "www.example.org");
|
||||
Assert.Equal(SslProtocols.Tls13, options.EnabledSslProtocols);
|
||||
}
|
||||
|
||||
|
||||
[Fact]
|
||||
public void PrefersClientCertificateModeDefinedInSniConfig()
|
||||
{
|
||||
var sniDictionary = new Dictionary<string, SniConfig>
|
||||
{
|
||||
{
|
||||
"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<ILogger<HttpsConnectionMiddleware>>());
|
||||
|
||||
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<string, SniConfig>
|
||||
{
|
||||
{
|
||||
"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<ILogger<HttpsConnectionMiddleware>>());
|
||||
|
||||
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> { 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<SslApplicationProtocol> 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<object, string> CertToPathDictionary { get; } = new Dictionary<object, string>(ReferenceEqualityComparer.Instance);
|
||||
|
||||
public bool IsTestMock => true;
|
||||
|
||||
public X509Certificate2 LoadCertificate(CertificateConfig certInfo, string endpointName)
|
||||
{
|
||||
if (certInfo is null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var cert = new X509Certificate2();
|
||||
CertToPathDictionary.Add(cert, certInfo.Path);
|
||||
return cert;
|
||||
}
|
||||
}
|
||||
|
||||
private class MockConnectionContext : ConnectionContext
|
||||
{
|
||||
public override IDuplexPipe Transport { get => throw new NotImplementedException(); set => throw new NotImplementedException(); }
|
||||
public override string ConnectionId { get; set; } = "MockConnectionId";
|
||||
public override IFeatureCollection Features { get; } = new FeatureCollection();
|
||||
public override IDictionary<object, object> Items { get; set; } = new Dictionary<object, object>();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -5,6 +5,7 @@ using System;
|
|||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Security.Authentication;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Server.Kestrel.Core;
|
||||
using Microsoft.AspNetCore.Server.Kestrel.Core.Internal;
|
||||
using Microsoft.AspNetCore.Server.Kestrel.Https;
|
||||
|
|
@ -273,6 +274,65 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Tests
|
|||
Assert.Null(endpoint.SslProtocols);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ReadEndpointWithEmptySniSection_ReturnsEmptyCollection()
|
||||
{
|
||||
var config = new ConfigurationBuilder().AddInMemoryCollection(new[]
|
||||
{
|
||||
new KeyValuePair<string, string>("Endpoints:End1:Url", "http://*:5001"),
|
||||
}).Build();
|
||||
|
||||
var reader = new ConfigurationReader(config);
|
||||
|
||||
var endpoint = reader.Endpoints.First();
|
||||
Assert.NotNull(endpoint.Sni);
|
||||
Assert.False(endpoint.Sni.Any());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ReadEndpointWithEmptySniKey_Throws()
|
||||
{
|
||||
var config = new ConfigurationBuilder().AddInMemoryCollection(new[]
|
||||
{
|
||||
new KeyValuePair<string, string>("Endpoints:End1:Url", "http://*:5001"),
|
||||
new KeyValuePair<string, string>("Endpoints:End1:Sni::Protocols", "Http1"),
|
||||
}).Build();
|
||||
|
||||
var reader = new ConfigurationReader(config);
|
||||
var end1Ex = Assert.Throws<InvalidOperationException>(() => reader.Endpoints);
|
||||
|
||||
Assert.Equal(CoreStrings.FormatSniNameCannotBeEmpty("End1"), end1Ex.Message);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ReadEndpointWithSniConfigured_ReturnsCorrectValue()
|
||||
{
|
||||
var config = new ConfigurationBuilder().AddInMemoryCollection(new[]
|
||||
{
|
||||
new KeyValuePair<string, string>("Endpoints:End1:Url", "http://*:5001"),
|
||||
new KeyValuePair<string, string>("Endpoints:End1:Sni:*.example.org:Protocols", "Http1"),
|
||||
new KeyValuePair<string, string>("Endpoints:End1:Sni:*.example.org:SslProtocols:0", "Tls12"),
|
||||
new KeyValuePair<string, string>("Endpoints:End1:Sni:*.example.org:Certificate:Path", "/path/cert.pfx"),
|
||||
new KeyValuePair<string, string>("Endpoints:End1:Sni:*.example.org:Certificate:Password", "certpassword"),
|
||||
new KeyValuePair<string, string>("Endpoints:End1:SNI:*.example.org:ClientCertificateMode", "AllowCertificate"),
|
||||
}).Build();
|
||||
|
||||
var reader = new ConfigurationReader(config);
|
||||
|
||||
static void VerifySniConfig(SniConfig config)
|
||||
{
|
||||
Assert.NotNull(config);
|
||||
|
||||
Assert.Equal(HttpProtocols.Http1, config.Protocols);
|
||||
Assert.Equal(SslProtocols.Tls12, config.SslProtocols);
|
||||
Assert.Equal("/path/cert.pfx", config.Certificate.Path);
|
||||
Assert.Equal("certpassword", config.Certificate.Password);
|
||||
Assert.Equal(ClientCertificateMode.AllowCertificate, config.ClientCertificateMode);
|
||||
}
|
||||
|
||||
VerifySniConfig(reader.Endpoints.First().Sni["*.Example.org"]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ReadEndpointDefaultsWithSingleSslProtocolSet_ReturnsCorrectValue()
|
||||
{
|
||||
|
|
|
|||
|
|
@ -8,7 +8,6 @@ using System.Linq;
|
|||
using System.Security.Authentication;
|
||||
using System.Security.Cryptography;
|
||||
using System.Security.Cryptography.X509Certificates;
|
||||
using System.Text;
|
||||
using Microsoft.AspNetCore.Hosting;
|
||||
using Microsoft.AspNetCore.Server.Kestrel.Core;
|
||||
using Microsoft.AspNetCore.Server.Kestrel.Https;
|
||||
|
|
@ -433,6 +432,88 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Tests
|
|||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ConfigureEndpoint_ThrowsWhen_HttpsConfigIsDeclaredInNonHttpsEndpoints()
|
||||
{
|
||||
var serverOptions = CreateServerOptions();
|
||||
|
||||
var config = new ConfigurationBuilder().AddInMemoryCollection(new[]
|
||||
{
|
||||
new KeyValuePair<string, string>("Endpoints:End1:Url", "http://*:5001"),
|
||||
// We shouldn't need to specify a real cert, because KestrelConfigurationLoader should check whether the endpoint requires a cert before trying to load it.
|
||||
new KeyValuePair<string, string>("Endpoints:End1:Certificate:Path", "fakecert.pfx"),
|
||||
}).Build();
|
||||
|
||||
var ex = Assert.Throws<InvalidOperationException>(() => serverOptions.Configure(config).Load());
|
||||
Assert.Equal(CoreStrings.FormatEndpointHasUnusedHttpsConfig("End1", "Certificate"), ex.Message);
|
||||
|
||||
config = new ConfigurationBuilder().AddInMemoryCollection(new[]
|
||||
{
|
||||
new KeyValuePair<string, string>("Endpoints:End1:Url", "http://*:5001"),
|
||||
new KeyValuePair<string, string>("Endpoints:End1:Certificate:Subject", "example.org"),
|
||||
}).Build();
|
||||
|
||||
ex = Assert.Throws<InvalidOperationException>(() => serverOptions.Configure(config).Load());
|
||||
Assert.Equal(CoreStrings.FormatEndpointHasUnusedHttpsConfig("End1", "Certificate"), ex.Message);
|
||||
|
||||
config = new ConfigurationBuilder().AddInMemoryCollection(new[]
|
||||
{
|
||||
new KeyValuePair<string, string>("Endpoints:End1:Url", "http://*:5001"),
|
||||
new KeyValuePair<string, string>("Endpoints:End1:ClientCertificateMode", ClientCertificateMode.RequireCertificate.ToString()),
|
||||
}).Build();
|
||||
|
||||
ex = Assert.Throws<InvalidOperationException>(() => serverOptions.Configure(config).Load());
|
||||
Assert.Equal(CoreStrings.FormatEndpointHasUnusedHttpsConfig("End1", "ClientCertificateMode"), ex.Message);
|
||||
|
||||
config = new ConfigurationBuilder().AddInMemoryCollection(new[]
|
||||
{
|
||||
new KeyValuePair<string, string>("Endpoints:End1:Url", "http://*:5001"),
|
||||
new KeyValuePair<string, string>("Endpoints:End1:SslProtocols:0", SslProtocols.Tls13.ToString()),
|
||||
}).Build();
|
||||
|
||||
ex = Assert.Throws<InvalidOperationException>(() => serverOptions.Configure(config).Load());
|
||||
Assert.Equal(CoreStrings.FormatEndpointHasUnusedHttpsConfig("End1", "SslProtocols"), ex.Message);
|
||||
|
||||
config = new ConfigurationBuilder().AddInMemoryCollection(new[]
|
||||
{
|
||||
new KeyValuePair<string, string>("Endpoints:End1:Url", "http://*:5001"),
|
||||
new KeyValuePair<string, string>("Endpoints:End1:Sni:Protocols", HttpProtocols.Http1.ToString()),
|
||||
}).Build();
|
||||
|
||||
ex = Assert.Throws<InvalidOperationException>(() => serverOptions.Configure(config).Load());
|
||||
Assert.Equal(CoreStrings.FormatEndpointHasUnusedHttpsConfig("End1", "Sni"), ex.Message);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ConfigureEndpoint_DoesNotThrowWhen_HttpsConfigIsDeclaredInEndpointDefaults()
|
||||
{
|
||||
var serverOptions = CreateServerOptions();
|
||||
|
||||
var config = new ConfigurationBuilder().AddInMemoryCollection(new[]
|
||||
{
|
||||
new KeyValuePair<string, string>("Endpoints:End1:Url", "http://*:5001"),
|
||||
new KeyValuePair<string, string>("EndpointDefaults:ClientCertificateMode", ClientCertificateMode.RequireCertificate.ToString()),
|
||||
}).Build();
|
||||
|
||||
var (_, endpointsToStart) = serverOptions.Configure(config).Reload();
|
||||
var end1 = Assert.Single(endpointsToStart);
|
||||
Assert.NotNull(end1?.EndpointConfig);
|
||||
Assert.Null(end1.EndpointConfig.ClientCertificateMode);
|
||||
|
||||
serverOptions = CreateServerOptions();
|
||||
|
||||
config = new ConfigurationBuilder().AddInMemoryCollection(new[]
|
||||
{
|
||||
new KeyValuePair<string, string>("Endpoints:End1:Url", "http://*:5001"),
|
||||
new KeyValuePair<string, string>("EndpointDefaults:SslProtocols:0", SslProtocols.Tls13.ToString()),
|
||||
}).Build();
|
||||
|
||||
(_, endpointsToStart) = serverOptions.Configure(config).Reload();
|
||||
end1 = Assert.Single(endpointsToStart);
|
||||
Assert.NotNull(end1?.EndpointConfig);
|
||||
Assert.Null(end1.EndpointConfig.SslProtocols);
|
||||
}
|
||||
|
||||
[ConditionalTheory]
|
||||
[InlineData("http1", HttpProtocols.Http1)]
|
||||
// [InlineData("http2", HttpProtocols.Http2)] // Not supported due to missing ALPN support. https://github.com/dotnet/corefx/issues/33016
|
||||
|
|
@ -673,7 +754,6 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Tests
|
|||
Assert.True(ran1);
|
||||
}
|
||||
|
||||
|
||||
[Fact]
|
||||
public void DefaultEndpointConfigureSection_ConfigureHttpsDefaultsCanOverrideSslProtocols()
|
||||
{
|
||||
|
|
@ -748,6 +828,36 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Tests
|
|||
Assert.True(ran2);
|
||||
}
|
||||
|
||||
|
||||
[Fact]
|
||||
public void EndpointConfigureSection_CanConfigureSni()
|
||||
{
|
||||
var serverOptions = CreateServerOptions();
|
||||
var certPath = Path.Combine("shared", "TestCertificates", "https-ecdsa.pem");
|
||||
var keyPath = Path.Combine("shared", "TestCertificates", "https-ecdsa.key");
|
||||
|
||||
var config = new ConfigurationBuilder().AddInMemoryCollection(new[]
|
||||
{
|
||||
new KeyValuePair<string, string>("Endpoints:End1:Url", "https://*:5001"),
|
||||
new KeyValuePair<string, string>("Endpoints:End1:Sni:*.example.org:Protocols", HttpProtocols.None.ToString()),
|
||||
new KeyValuePair<string, string>("Endpoints:End1:Sni:*.example.org:SslProtocols:0", SslProtocols.Tls13.ToString()),
|
||||
new KeyValuePair<string, string>("Endpoints:End1:Sni:*.example.org:ClientCertificateMode", ClientCertificateMode.RequireCertificate.ToString()),
|
||||
new KeyValuePair<string, string>("Endpoints:End1:Sni:*.example.org:Certificate:Path", certPath),
|
||||
new KeyValuePair<string, string>("Endpoints:End1:Sni:*.example.org:Certificate:KeyPath", keyPath),
|
||||
}).Build();
|
||||
|
||||
var (_, endpointsToStart) = serverOptions.Configure(config).Reload();
|
||||
var end1 = Assert.Single(endpointsToStart);
|
||||
var (name, sniConfig) = Assert.Single(end1?.EndpointConfig?.Sni);
|
||||
|
||||
Assert.Equal("*.example.org", name);
|
||||
Assert.Equal(HttpProtocols.None, sniConfig.Protocols);
|
||||
Assert.Equal(SslProtocols.Tls13, sniConfig.SslProtocols);
|
||||
Assert.Equal(ClientCertificateMode.RequireCertificate, sniConfig.ClientCertificateMode);
|
||||
Assert.Equal(certPath, sniConfig.Certificate.Path);
|
||||
Assert.Equal(keyPath, sniConfig.Certificate.KeyPath);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void EndpointConfigureSection_CanOverrideClientCertificateModeFromConfigureHttpsDefaults()
|
||||
{
|
||||
|
|
@ -803,7 +913,6 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Tests
|
|||
Assert.True(ran1);
|
||||
}
|
||||
|
||||
|
||||
[Fact]
|
||||
public void DefaultEndpointConfigureSection_ConfigureHttpsDefaultsCanOverrideClientCertificateMode()
|
||||
{
|
||||
|
|
|
|||
|
|
@ -1,9 +1,17 @@
|
|||
{
|
||||
{
|
||||
"Kestrel": {
|
||||
"Endpoints": {
|
||||
"NamedEndpoint": { "Url": "http://*:6000" },
|
||||
"NamedHttpsEndpoint": {
|
||||
"Url": "https://*:6443",
|
||||
"Sni": {
|
||||
"localhost": {
|
||||
"Protocols": "Http1AndHttp2"
|
||||
},
|
||||
"*": {
|
||||
"SslProtocols": [ "Tls12", "Tls13" ]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -24,7 +24,7 @@ using Microsoft.AspNetCore.Testing;
|
|||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using Microsoft.Extensions.Logging.Testing;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Moq;
|
||||
using Xunit;
|
||||
|
||||
|
|
@ -69,9 +69,14 @@ namespace Microsoft.AspNetCore.Server.Kestrel.InMemory.FunctionalTests
|
|||
var env = new Mock<IHostEnvironment>();
|
||||
env.SetupGet(e => e.ContentRootPath).Returns(Directory.GetCurrentDirectory());
|
||||
|
||||
options.ApplicationServices = new ServiceCollection().AddSingleton(env.Object).AddLogging().BuildServiceProvider();
|
||||
var loader = new KestrelConfigurationLoader(options, configuration, reloadOnChange: false);
|
||||
var serviceProvider = new ServiceCollection().AddLogging().BuildServiceProvider();
|
||||
options.ApplicationServices = serviceProvider;
|
||||
|
||||
var logger = serviceProvider.GetRequiredService<ILogger<KestrelServer>>();
|
||||
var httpsLogger = serviceProvider.GetRequiredService<ILogger<HttpsConnectionMiddleware>>();
|
||||
var loader = new KestrelConfigurationLoader(options, configuration, env.Object, reloadOnChange: false, logger, httpsLogger);
|
||||
loader.Load();
|
||||
|
||||
void ConfigureListenOptions(ListenOptions listenOptions)
|
||||
{
|
||||
listenOptions.KestrelServerOptions = options;
|
||||
|
|
@ -160,6 +165,38 @@ namespace Microsoft.AspNetCore.Server.Kestrel.InMemory.FunctionalTests
|
|||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[QuarantinedTest("https://github.com/dotnet/runtime/issues/40402")]
|
||||
public async Task ClientCertificateRequiredConfiguredInCallbackContinuesWhenNoCertificate()
|
||||
{
|
||||
void ConfigureListenOptions(ListenOptions listenOptions)
|
||||
{
|
||||
listenOptions.UseHttps((connection, stream, clientHelloInfo, state, cancellationToken) =>
|
||||
new ValueTask<SslServerAuthenticationOptions>(new SslServerAuthenticationOptions
|
||||
{
|
||||
ServerCertificate = _x509Certificate2,
|
||||
// From the API Docs: "Note that this is only a request --
|
||||
// if no certificate is provided, the server still accepts the connection request."
|
||||
// Not to mention this is equivalent to the test above.
|
||||
ClientCertificateRequired = true,
|
||||
RemoteCertificateValidationCallback = (sender, certificate, chain, sslPolicyErrors) => true,
|
||||
CertificateRevocationCheckMode = X509RevocationMode.NoCheck
|
||||
}), state: null, HttpsConnectionAdapterOptions.DefaultHandshakeTimeout);
|
||||
}
|
||||
|
||||
await using (var server = new TestServer(context =>
|
||||
{
|
||||
var tlsFeature = context.Features.Get<ITlsConnectionFeature>();
|
||||
Assert.NotNull(tlsFeature);
|
||||
Assert.Null(tlsFeature.ClientCertificate);
|
||||
return context.Response.WriteAsync("hello world");
|
||||
}, new TestServiceContext(LoggerFactory), ConfigureListenOptions))
|
||||
{
|
||||
var result = await server.HttpClientSlim.GetStringAsync($"https://localhost:{server.Port}/", validateCertificate: false);
|
||||
Assert.Equal("hello world", result);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ThrowsWhenNoServerCertificateIsProvided()
|
||||
{
|
||||
|
|
|
|||
Loading…
Reference in New Issue