Merge branch 'release/2.1' into dev

This commit is contained in:
Chris Ross (ASP.NET) 2018-04-04 13:44:56 -07:00
commit 953496a970
9 changed files with 294 additions and 19 deletions

View File

@ -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

View File

@ -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">

View File

@ -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>

View File

@ -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
}

View File

@ -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);
}

View File

@ -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;
}

View File

@ -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.

View File

@ -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()

View File

@ -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");
}
}
}