Merge branch 'release/2.1' into dev
This commit is contained in:
commit
953496a970
|
|
@ -12,6 +12,7 @@ using Microsoft.AspNetCore.Builder;
|
|||
using Microsoft.AspNetCore.Hosting;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Server.Kestrel.Core;
|
||||
using Microsoft.AspNetCore.Server.Kestrel.Https.Internal;
|
||||
using Microsoft.AspNetCore.Server.Kestrel.Transport.Abstractions.Internal;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
|
@ -105,10 +106,23 @@ namespace SampleApp
|
|||
listenOptions.UseHttps(StoreName.My, "localhost", allowInvalid: true);
|
||||
});
|
||||
|
||||
options.ListenAnyIP(basePort + 5, listenOptions =>
|
||||
{
|
||||
listenOptions.UseHttps(httpsOptions =>
|
||||
{
|
||||
var localhostCert = CertificateLoader.LoadFromStoreCert("localhost", "My", StoreLocation.CurrentUser, allowInvalid: true);
|
||||
httpsOptions.ServerCertificateSelector = (features, name) =>
|
||||
{
|
||||
// Here you would check the name, select an appropriate cert, and provide a fallback or fail for null names.
|
||||
return localhostCert;
|
||||
};
|
||||
});
|
||||
});
|
||||
|
||||
options
|
||||
.Configure()
|
||||
.Endpoint(IPAddress.Loopback, basePort + 5)
|
||||
.LocalhostEndpoint(basePort + 6)
|
||||
.Endpoint(IPAddress.Loopback, basePort + 6)
|
||||
.LocalhostEndpoint(basePort + 7)
|
||||
.Load();
|
||||
|
||||
options
|
||||
|
|
|
|||
|
|
@ -477,7 +477,7 @@
|
|||
<data name="PositiveTimeSpanRequired1" xml:space="preserve">
|
||||
<value>Value must be a positive TimeSpan.</value>
|
||||
</data>
|
||||
<data name="ServiceCertificateRequired" xml:space="preserve">
|
||||
<data name="ServerCertificateRequired" xml:space="preserve">
|
||||
<value>The server certificate parameter is required.</value>
|
||||
</data>
|
||||
<data name="BindingToDefaultAddresses" xml:space="preserve">
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ using System.Net.Security;
|
|||
using System.Security.Authentication;
|
||||
using System.Security.Cryptography.X509Certificates;
|
||||
using System.Threading;
|
||||
using Microsoft.AspNetCore.Http.Features;
|
||||
using Microsoft.AspNetCore.Server.Kestrel.Core;
|
||||
|
||||
namespace Microsoft.AspNetCore.Server.Kestrel.Https
|
||||
|
|
@ -29,7 +30,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Https
|
|||
|
||||
/// <summary>
|
||||
/// <para>
|
||||
/// Specifies the server certificate used to authenticate HTTPS connections.
|
||||
/// Specifies the server certificate used to authenticate HTTPS connections. This is ignored if ServerCertificateSelector is set.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// If the server certificate has an Extended Key Usage extension, the usages must include Server Authentication (OID 1.3.6.1.5.5.7.3.1).
|
||||
|
|
@ -37,6 +38,17 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Https
|
|||
/// </summary>
|
||||
public X509Certificate2 ServerCertificate { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// <para>
|
||||
/// A callback that will be invoked to dynamically select a server certificate. This is higher priority than ServerCertificate.
|
||||
/// If SNI is not avialable then the name parameter will be null.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// If the server certificate has an Extended Key Usage extension, the usages must include Server Authentication (OID 1.3.6.1.5.5.7.3.1).
|
||||
/// </para>
|
||||
/// </summary>
|
||||
public Func<IFeatureCollection, string, X509Certificate2> ServerCertificateSelector { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Specifies the client certificate requirements for a HTTPS connection. Defaults to <see cref="ClientCertificateMode.NoCertificate"/>.
|
||||
/// </summary>
|
||||
|
|
|
|||
|
|
@ -22,6 +22,8 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Https.Internal
|
|||
|
||||
private readonly HttpsConnectionAdapterOptions _options;
|
||||
private readonly X509Certificate2 _serverCertificate;
|
||||
private readonly Func<IFeatureCollection, string, X509Certificate2> _serverCertificateSelector;
|
||||
|
||||
private readonly ILogger _logger;
|
||||
|
||||
public HttpsConnectionAdapter(HttpsConnectionAdapterOptions options)
|
||||
|
|
@ -36,15 +38,24 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Https.Internal
|
|||
throw new ArgumentNullException(nameof(options));
|
||||
}
|
||||
|
||||
if (options.ServerCertificate == null)
|
||||
// capture the certificate now so it can't be switched after validation
|
||||
_serverCertificate = options.ServerCertificate;
|
||||
_serverCertificateSelector = options.ServerCertificateSelector;
|
||||
if (_serverCertificate == null && _serverCertificateSelector == null)
|
||||
{
|
||||
throw new ArgumentException(CoreStrings.ServiceCertificateRequired, nameof(options));
|
||||
throw new ArgumentException(CoreStrings.ServerCertificateRequired, nameof(options));
|
||||
}
|
||||
|
||||
// capture the certificate now so it can be switched after validation
|
||||
_serverCertificate = options.ServerCertificate;
|
||||
|
||||
EnsureCertificateIsAllowedForServerAuth(_serverCertificate);
|
||||
// If a selector is provided then ignore the cert, it may be a default cert.
|
||||
if (_serverCertificateSelector != null)
|
||||
{
|
||||
// SslStream doesn't allow both.
|
||||
_serverCertificate = null;
|
||||
}
|
||||
else
|
||||
{
|
||||
EnsureCertificateIsAllowedForServerAuth(_serverCertificate);
|
||||
}
|
||||
|
||||
_options = options;
|
||||
_logger = loggerFactory?.CreateLogger(nameof(HttpsConnectionAdapter));
|
||||
|
|
@ -115,9 +126,26 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Https.Internal
|
|||
try
|
||||
{
|
||||
#if NETCOREAPP2_1
|
||||
// Adapt to the SslStream signature
|
||||
ServerCertificateSelectionCallback selector = null;
|
||||
if (_serverCertificateSelector != null)
|
||||
{
|
||||
selector = (sender, name) =>
|
||||
{
|
||||
context.Features.Set(sslStream);
|
||||
var cert = _serverCertificateSelector(context.Features, 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,
|
||||
|
|
@ -137,7 +165,17 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Https.Internal
|
|||
|
||||
await sslStream.AuthenticateAsServerAsync(sslOptions, CancellationToken.None);
|
||||
#else
|
||||
await sslStream.AuthenticateAsServerAsync(_serverCertificate, certificateRequired,
|
||||
var serverCert = _serverCertificate;
|
||||
if (_serverCertificateSelector != null)
|
||||
{
|
||||
context.Features.Set(sslStream);
|
||||
serverCert = _serverCertificateSelector(context.Features, null);
|
||||
if (serverCert != null)
|
||||
{
|
||||
EnsureCertificateIsAllowedForServerAuth(serverCert);
|
||||
}
|
||||
}
|
||||
await sslStream.AuthenticateAsServerAsync(serverCert, certificateRequired,
|
||||
_options.SslProtocols, _options.CheckCertificateRevocation);
|
||||
#endif
|
||||
}
|
||||
|
|
|
|||
|
|
@ -236,7 +236,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel
|
|||
// EndpointDefaults or configureEndpoint may have added an https adapter.
|
||||
if (https && !listenOptions.ConnectionAdapters.Any(f => f.IsHttps))
|
||||
{
|
||||
if (httpsOptions.ServerCertificate == null)
|
||||
if (httpsOptions.ServerCertificate == null && httpsOptions.ServerCertificateSelector == null)
|
||||
{
|
||||
throw new InvalidOperationException(CoreStrings.NoCertSpecifiedNoDevelopmentCertificateFound);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -179,7 +179,7 @@ namespace Microsoft.AspNetCore.Hosting
|
|||
listenOptions.KestrelServerOptions.ApplyHttpsDefaults(options);
|
||||
configureOptions(options);
|
||||
|
||||
if (options.ServerCertificate == null)
|
||||
if (options.ServerCertificate == null && options.ServerCertificateSelector == null)
|
||||
{
|
||||
throw new InvalidOperationException(CoreStrings.NoCertSpecifiedNoDevelopmentCertificateFound);
|
||||
}
|
||||
|
|
@ -192,7 +192,7 @@ namespace Microsoft.AspNetCore.Hosting
|
|||
var options = new HttpsConnectionAdapterOptions();
|
||||
listenOptions.KestrelServerOptions.ApplyHttpsDefaults(options);
|
||||
|
||||
if (options.ServerCertificate == null)
|
||||
if (options.ServerCertificate == null && options.ServerCertificateSelector == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1693,16 +1693,16 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core
|
|||
/// <summary>
|
||||
/// The server certificate parameter is required.
|
||||
/// </summary>
|
||||
internal static string ServiceCertificateRequired
|
||||
internal static string ServerCertificateRequired
|
||||
{
|
||||
get => GetString("ServiceCertificateRequired");
|
||||
get => GetString("ServerCertificateRequired");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The server certificate parameter is required.
|
||||
/// </summary>
|
||||
internal static string FormatServiceCertificateRequired()
|
||||
=> GetString("ServiceCertificateRequired");
|
||||
internal static string FormatServerCertificateRequired()
|
||||
=> GetString("ServerCertificateRequired");
|
||||
|
||||
/// <summary>
|
||||
/// No listening endpoints were configured. Binding to {address0} and {address1} by default.
|
||||
|
|
|
|||
|
|
@ -26,7 +26,8 @@ namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests
|
|||
{
|
||||
public class HttpsConnectionAdapterTests
|
||||
{
|
||||
private static X509Certificate2 _x509Certificate2 = new X509Certificate2(TestResources.TestCertificatePath, "testPassword");
|
||||
private static X509Certificate2 _x509Certificate2 = TestResources.GetTestCertificate();
|
||||
private static X509Certificate2 _x509Certificate2NoExt = TestResources.GetTestCertificate("no_extensions.pfx");
|
||||
private readonly ITestOutputHelper _output;
|
||||
|
||||
public HttpsConnectionAdapterTests(ITestOutputHelper output)
|
||||
|
|
@ -148,6 +149,211 @@ namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests
|
|||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task UsesProvidedServerCertificateSelector()
|
||||
{
|
||||
var selectorCalled = 0;
|
||||
var serviceContext = new TestServiceContext();
|
||||
var listenOptions = new ListenOptions(new IPEndPoint(IPAddress.Loopback, 0))
|
||||
{
|
||||
ConnectionAdapters =
|
||||
{
|
||||
new HttpsConnectionAdapter(new HttpsConnectionAdapterOptions
|
||||
{
|
||||
ServerCertificateSelector = (features, name) =>
|
||||
{
|
||||
Assert.NotNull(features);
|
||||
Assert.NotNull(features.Get<SslStream>());
|
||||
#if NETCOREAPP2_1
|
||||
Assert.Equal("localhost", name);
|
||||
#else
|
||||
Assert.Null(name);
|
||||
#endif
|
||||
selectorCalled++;
|
||||
return _x509Certificate2;
|
||||
}
|
||||
})
|
||||
}
|
||||
};
|
||||
using (var server = new TestServer(context => Task.CompletedTask, serviceContext, listenOptions))
|
||||
{
|
||||
using (var client = new TcpClient())
|
||||
{
|
||||
// SslStream is used to ensure the certificate is actually passed to the server
|
||||
// HttpClient might not send the certificate because it is invalid or it doesn't match any
|
||||
// of the certificate authorities sent by the server in the SSL handshake.
|
||||
var stream = await OpenSslStream(client, server);
|
||||
await stream.AuthenticateAsClientAsync("localhost", new X509CertificateCollection(), SslProtocols.Tls12 | SslProtocols.Tls11, false);
|
||||
Assert.True(stream.RemoteCertificate.Equals(_x509Certificate2));
|
||||
Assert.Equal(1, selectorCalled);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task UsesProvidedServerCertificateSelectorEachTime()
|
||||
{
|
||||
var selectorCalled = 0;
|
||||
var serviceContext = new TestServiceContext();
|
||||
var listenOptions = new ListenOptions(new IPEndPoint(IPAddress.Loopback, 0))
|
||||
{
|
||||
ConnectionAdapters =
|
||||
{
|
||||
new HttpsConnectionAdapter(new HttpsConnectionAdapterOptions
|
||||
{
|
||||
ServerCertificateSelector = (features, name) =>
|
||||
{
|
||||
Assert.NotNull(features);
|
||||
Assert.NotNull(features.Get<SslStream>());
|
||||
#if NETCOREAPP2_1
|
||||
Assert.Equal("localhost", name);
|
||||
#else
|
||||
Assert.Null(name);
|
||||
#endif
|
||||
selectorCalled++;
|
||||
if (selectorCalled == 1)
|
||||
{
|
||||
return _x509Certificate2;
|
||||
}
|
||||
return _x509Certificate2NoExt;
|
||||
}
|
||||
})
|
||||
}
|
||||
};
|
||||
using (var server = new TestServer(context => Task.CompletedTask, serviceContext, listenOptions))
|
||||
{
|
||||
using (var client = new TcpClient())
|
||||
{
|
||||
// SslStream is used to ensure the certificate is actually passed to the server
|
||||
// HttpClient might not send the certificate because it is invalid or it doesn't match any
|
||||
// of the certificate authorities sent by the server in the SSL handshake.
|
||||
var stream = await OpenSslStream(client, server);
|
||||
await stream.AuthenticateAsClientAsync("localhost", new X509CertificateCollection(), SslProtocols.Tls12 | SslProtocols.Tls11, false);
|
||||
Assert.True(stream.RemoteCertificate.Equals(_x509Certificate2));
|
||||
Assert.Equal(1, selectorCalled);
|
||||
}
|
||||
using (var client = new TcpClient())
|
||||
{
|
||||
// SslStream is used to ensure the certificate is actually passed to the server
|
||||
// HttpClient might not send the certificate because it is invalid or it doesn't match any
|
||||
// of the certificate authorities sent by the server in the SSL handshake.
|
||||
var stream = await OpenSslStream(client, server);
|
||||
await stream.AuthenticateAsClientAsync("localhost", new X509CertificateCollection(), SslProtocols.Tls12 | SslProtocols.Tls11, false);
|
||||
Assert.True(stream.RemoteCertificate.Equals(_x509Certificate2NoExt));
|
||||
Assert.Equal(2, selectorCalled);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task UsesProvidedServerCertificateSelectorValidatesEkus()
|
||||
{
|
||||
var selectorCalled = 0;
|
||||
var serviceContext = new TestServiceContext();
|
||||
var listenOptions = new ListenOptions(new IPEndPoint(IPAddress.Loopback, 0))
|
||||
{
|
||||
ConnectionAdapters =
|
||||
{
|
||||
new HttpsConnectionAdapter(new HttpsConnectionAdapterOptions
|
||||
{
|
||||
ServerCertificateSelector = (features, name) =>
|
||||
{
|
||||
selectorCalled++;
|
||||
return TestResources.GetTestCertificate("eku.code_signing.pfx");
|
||||
}
|
||||
})
|
||||
}
|
||||
};
|
||||
using (var server = new TestServer(context => Task.CompletedTask, serviceContext, listenOptions))
|
||||
{
|
||||
using (var client = new TcpClient())
|
||||
{
|
||||
// SslStream is used to ensure the certificate is actually passed to the server
|
||||
// HttpClient might not send the certificate because it is invalid or it doesn't match any
|
||||
// of the certificate authorities sent by the server in the SSL handshake.
|
||||
var stream = await OpenSslStream(client, server);
|
||||
await Assert.ThrowsAsync<IOException>(() =>
|
||||
stream.AuthenticateAsClientAsync("localhost", new X509CertificateCollection(), SslProtocols.Tls12 | SslProtocols.Tls11, false));
|
||||
Assert.Equal(1, selectorCalled);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task UsesProvidedServerCertificateSelectorOverridesServerCertificate()
|
||||
{
|
||||
var selectorCalled = 0;
|
||||
var serviceContext = new TestServiceContext();
|
||||
var listenOptions = new ListenOptions(new IPEndPoint(IPAddress.Loopback, 0))
|
||||
{
|
||||
ConnectionAdapters =
|
||||
{
|
||||
new HttpsConnectionAdapter(new HttpsConnectionAdapterOptions
|
||||
{
|
||||
ServerCertificate = _x509Certificate2NoExt,
|
||||
ServerCertificateSelector = (features, name) =>
|
||||
{
|
||||
Assert.NotNull(features);
|
||||
Assert.NotNull(features.Get<SslStream>());
|
||||
#if NETCOREAPP2_1
|
||||
Assert.Equal("localhost", name);
|
||||
#else
|
||||
Assert.Null(name);
|
||||
#endif
|
||||
selectorCalled++;
|
||||
return _x509Certificate2;
|
||||
}
|
||||
})
|
||||
}
|
||||
};
|
||||
using (var server = new TestServer(context => Task.CompletedTask, serviceContext, listenOptions))
|
||||
{
|
||||
using (var client = new TcpClient())
|
||||
{
|
||||
// SslStream is used to ensure the certificate is actually passed to the server
|
||||
// HttpClient might not send the certificate because it is invalid or it doesn't match any
|
||||
// of the certificate authorities sent by the server in the SSL handshake.
|
||||
var stream = await OpenSslStream(client, server);
|
||||
await stream.AuthenticateAsClientAsync("localhost", new X509CertificateCollection(), SslProtocols.Tls12 | SslProtocols.Tls11, false);
|
||||
Assert.True(stream.RemoteCertificate.Equals(_x509Certificate2));
|
||||
Assert.Equal(1, selectorCalled);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task UsesProvidedServerCertificateSelectorFailsIfYouReturnNull()
|
||||
{
|
||||
var selectorCalled = 0;
|
||||
var serviceContext = new TestServiceContext();
|
||||
var listenOptions = new ListenOptions(new IPEndPoint(IPAddress.Loopback, 0))
|
||||
{
|
||||
ConnectionAdapters =
|
||||
{
|
||||
new HttpsConnectionAdapter(new HttpsConnectionAdapterOptions
|
||||
{
|
||||
ServerCertificateSelector = (features, name) =>
|
||||
{
|
||||
selectorCalled++;
|
||||
return null;
|
||||
}
|
||||
})
|
||||
}
|
||||
};
|
||||
using (var server = new TestServer(context => Task.CompletedTask, serviceContext, listenOptions))
|
||||
{
|
||||
using (var client = new TcpClient())
|
||||
{
|
||||
// SslStream is used to ensure the certificate is actually passed to the server
|
||||
// HttpClient might not send the certificate because it is invalid or it doesn't match any
|
||||
// of the certificate authorities sent by the server in the SSL handshake.
|
||||
var stream = await OpenSslStream(client, server);
|
||||
await Assert.ThrowsAsync<IOException>(() =>
|
||||
stream.AuthenticateAsClientAsync("localhost", new X509CertificateCollection(), SslProtocols.Tls12 | SslProtocols.Tls11, false));
|
||||
Assert.Equal(1, selectorCalled);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task CertificatePassedToHttpContext()
|
||||
|
|
|
|||
|
|
@ -17,5 +17,10 @@ namespace Microsoft.AspNetCore.Testing
|
|||
{
|
||||
return new X509Certificate2(TestCertificatePath, "testPassword");
|
||||
}
|
||||
|
||||
public static X509Certificate2 GetTestCertificate(string certName)
|
||||
{
|
||||
return new X509Certificate2(GetCertPath(certName), "testPassword");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue