From ba2b883db088b0a07a3b82f1963f34098be9c148 Mon Sep 17 00:00:00 2001 From: Pavel Krymets Date: Mon, 2 Apr 2018 16:44:13 -0700 Subject: [PATCH 1/3] Reaction to *Memory changes (#2446) --- build/dependencies.props | 24 +++++++++---------- src/Directory.Build.props | 4 ++++ .../Internal/LibuvConnection.cs | 7 +++--- .../Internal/Networking/UvWriteReq.cs | 4 ++-- 4 files changed, 21 insertions(+), 18 deletions(-) diff --git a/build/dependencies.props b/build/dependencies.props index db44bbd005..bcef3e3761 100644 --- a/build/dependencies.props +++ b/build/dependencies.props @@ -7,8 +7,8 @@ 0.10.13 2.1.0-preview2-15749 1.10.0 - 2.1.0-preview2-30478 - 2.1.0-preview2-30478 + 2.1.0-a-preview2-mpr-16443 + 2.1.0-a-preview2-mpr-16443 2.1.0-preview2-30478 2.1.0-preview2-30478 2.1.0-preview2-30478 @@ -17,8 +17,8 @@ 2.1.0-preview2-30478 2.1.0-preview2-30478 2.1.0-preview2-30478 - 2.1.0-preview2-30478 - 2.1.0-preview2-30478 + 2.1.0-a-preview2-mpr-16443 + 2.1.0-a-preview2-mpr-16443 2.1.0-preview2-30478 2.1.0-preview2-30478 2.1.0-preview2-30478 @@ -28,18 +28,18 @@ 2.1.0-preview2-30478 2.1.0-preview2-30478 2.0.0 - 2.1.0-preview2-26326-03 + 2.1.0-preview3-26331-01 2.1.0-preview2-30478 15.6.1 4.7.49 11.0.2 - 4.5.0-preview2-26326-04 - 4.5.0-preview2-26326-04 - 4.5.0-preview2-26326-04 - 4.5.0-preview2-26326-04 - 4.5.0-preview2-26326-04 - 4.5.0-preview2-26326-04 - 4.5.0-preview2-26326-04 + 4.5.0-preview3-26331-02 + 4.5.0-preview3-26331-02 + 4.5.0-preview3-26331-02 + 4.5.0-preview3-26331-02 + 4.5.0-preview3-26331-02 + 4.5.0-preview3-26331-02 + 4.5.0-preview3-26331-02 0.8.0 2.3.1 2.4.0-beta.1.build3945 diff --git a/src/Directory.Build.props b/src/Directory.Build.props index a1676ede80..6b85b2cf04 100644 --- a/src/Directory.Build.props +++ b/src/Directory.Build.props @@ -6,4 +6,8 @@ + + + false + diff --git a/src/Kestrel.Transport.Libuv/Internal/LibuvConnection.cs b/src/Kestrel.Transport.Libuv/Internal/LibuvConnection.cs index b6295aa568..116b67e39c 100644 --- a/src/Kestrel.Transport.Libuv/Internal/LibuvConnection.cs +++ b/src/Kestrel.Transport.Libuv/Internal/LibuvConnection.cs @@ -106,7 +106,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Transport.Libuv.Internal private unsafe LibuvFunctions.uv_buf_t OnAlloc(UvStreamHandle handle, int suggestedSize) { var currentWritableBuffer = Input.GetMemory(MinAllocBufferSize); - _bufferHandle = currentWritableBuffer.Retain(true); + _bufferHandle = currentWritableBuffer.Pin(); return handle.Libuv.buf_init((IntPtr)_bufferHandle.Pointer, currentWritableBuffer.Length); } @@ -118,6 +118,8 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Transport.Libuv.Internal private void OnRead(UvStreamHandle handle, int status) { + // Cleanup state from last OnAlloc. This is safe even if OnAlloc wasn't called. + _bufferHandle.Dispose(); if (status == 0) { // EAGAIN/EWOULDBLOCK so just return the buffer. @@ -168,9 +170,6 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Transport.Libuv.Internal // Complete after aborting the connection Input.Complete(error); } - - // Cleanup state from last OnAlloc. This is safe even if OnAlloc wasn't called. - _bufferHandle.Dispose(); } private async Task ApplyBackpressureAsync(ValueTask flushTask) diff --git a/src/Kestrel.Transport.Libuv/Internal/Networking/UvWriteReq.cs b/src/Kestrel.Transport.Libuv/Internal/Networking/UvWriteReq.cs index 9a635ba5cc..e28c318616 100644 --- a/src/Kestrel.Transport.Libuv/Internal/Networking/UvWriteReq.cs +++ b/src/Kestrel.Transport.Libuv/Internal/Networking/UvWriteReq.cs @@ -94,7 +94,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Transport.Libuv.Internal.Networkin if (nBuffers == 1) { var memory = buffer.First; - var memoryHandle = memory.Retain(true); + var memoryHandle = memory.Pin(); _handles.Add(memoryHandle); // Fast path for single buffer @@ -108,7 +108,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Transport.Libuv.Internal.Networkin foreach (var memory in buffer) { // This won't actually pin the buffer since we're already using pinned memory - var memoryHandle = memory.Retain(true); + var memoryHandle = memory.Pin(); _handles.Add(memoryHandle); // create and pin each segment being written From 623c27ab013fcec0462345ce7c374c1397f38422 Mon Sep 17 00:00:00 2001 From: Stephen Halter Date: Mon, 2 Apr 2018 19:48:17 -0700 Subject: [PATCH 2/3] Dispose SocketAsyncEventArgs when we dispose the Socket (#2459) --- src/Kestrel.Transport.Sockets/Internal/SocketConnection.cs | 2 ++ src/Kestrel.Transport.Sockets/Internal/SocketReceiver.cs | 7 ++++++- src/Kestrel.Transport.Sockets/Internal/SocketSender.cs | 7 ++++++- 3 files changed, 14 insertions(+), 2 deletions(-) diff --git a/src/Kestrel.Transport.Sockets/Internal/SocketConnection.cs b/src/Kestrel.Transport.Sockets/Internal/SocketConnection.cs index 726d0372ca..59e5ff2e22 100644 --- a/src/Kestrel.Transport.Sockets/Internal/SocketConnection.cs +++ b/src/Kestrel.Transport.Sockets/Internal/SocketConnection.cs @@ -86,6 +86,8 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Transport.Sockets.Internal // Dispose the socket(should noop if already called) _socket.Dispose(); + _receiver.Dispose(); + _sender.Dispose(); } catch (Exception ex) { diff --git a/src/Kestrel.Transport.Sockets/Internal/SocketReceiver.cs b/src/Kestrel.Transport.Sockets/Internal/SocketReceiver.cs index 150978e473..2116c03cd5 100644 --- a/src/Kestrel.Transport.Sockets/Internal/SocketReceiver.cs +++ b/src/Kestrel.Transport.Sockets/Internal/SocketReceiver.cs @@ -7,7 +7,7 @@ using System.Net.Sockets; namespace Microsoft.AspNetCore.Server.Kestrel.Transport.Sockets.Internal { - public class SocketReceiver + public class SocketReceiver : IDisposable { private readonly Socket _socket; private readonly SocketAsyncEventArgs _eventArgs = new SocketAsyncEventArgs(); @@ -37,5 +37,10 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Transport.Sockets.Internal return _awaitable; } + + public void Dispose() + { + _eventArgs.Dispose(); + } } } diff --git a/src/Kestrel.Transport.Sockets/Internal/SocketSender.cs b/src/Kestrel.Transport.Sockets/Internal/SocketSender.cs index a8b0727c0a..26e22b664d 100644 --- a/src/Kestrel.Transport.Sockets/Internal/SocketSender.cs +++ b/src/Kestrel.Transport.Sockets/Internal/SocketSender.cs @@ -11,7 +11,7 @@ using System.Runtime.InteropServices; namespace Microsoft.AspNetCore.Server.Kestrel.Transport.Sockets.Internal { - public class SocketSender + public class SocketSender : IDisposable { private readonly Socket _socket; private readonly SocketAsyncEventArgs _eventArgs = new SocketAsyncEventArgs(); @@ -98,5 +98,10 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Transport.Sockets.Internal return _bufferList; } + + public void Dispose() + { + _eventArgs.Dispose(); + } } } From 9ea2c50068c77978f0df7b3c58368167503dfa50 Mon Sep 17 00:00:00 2001 From: "Chris Ross (ASP.NET)" Date: Fri, 23 Mar 2018 10:23:17 -0700 Subject: [PATCH 3/3] Add SNI support #2357 --- build/dependencies.props | 58 ++--- samples/SampleApp/Startup.cs | 18 +- src/Kestrel.Core/CoreStrings.resx | 2 +- .../HttpsConnectionAdapterOptions.cs | 14 +- .../Internal/HttpsConnectionAdapter.cs | 52 ++++- .../KestrelConfigurationLoader.cs | 2 +- .../ListenOptionsHttpsExtensions.cs | 4 +- .../Properties/CoreStrings.Designer.cs | 8 +- .../HttpsConnectionAdapterTests.cs | 208 +++++++++++++++++- test/shared/TestResources.cs | 5 + 10 files changed, 323 insertions(+), 48 deletions(-) diff --git a/build/dependencies.props b/build/dependencies.props index bcef3e3761..c3d5165f83 100644 --- a/build/dependencies.props +++ b/build/dependencies.props @@ -7,39 +7,39 @@ 0.10.13 2.1.0-preview2-15749 1.10.0 - 2.1.0-a-preview2-mpr-16443 - 2.1.0-a-preview2-mpr-16443 - 2.1.0-preview2-30478 - 2.1.0-preview2-30478 - 2.1.0-preview2-30478 - 2.1.0-preview2-30478 - 2.1.0-preview2-30478 - 2.1.0-preview2-30478 - 2.1.0-preview2-30478 - 2.1.0-preview2-30478 - 2.1.0-a-preview2-mpr-16443 - 2.1.0-a-preview2-mpr-16443 - 2.1.0-preview2-30478 - 2.1.0-preview2-30478 - 2.1.0-preview2-30478 - 2.1.0-preview2-30478 - 2.1.0-preview2-30478 - 2.1.0-preview2-30478 - 2.1.0-preview2-30478 - 2.1.0-preview2-30478 + 2.1.0-preview2-30554 + 2.1.0-preview2-30554 + 2.1.0-preview2-30554 + 2.1.0-preview2-30554 + 2.1.0-preview2-30554 + 2.1.0-preview2-30554 + 2.1.0-preview2-30554 + 2.1.0-preview2-30554 + 2.1.0-preview2-30554 + 2.1.0-preview2-30554 + 2.1.0-preview2-30554 + 2.1.0-preview2-30554 + 2.1.0-preview2-30554 + 2.1.0-preview2-30554 + 2.1.0-preview2-30554 + 2.1.0-preview2-30554 + 2.1.0-preview2-30554 + 2.1.0-preview2-30554 + 2.1.0-preview2-30554 + 2.1.0-preview2-30554 2.0.0 - 2.1.0-preview3-26331-01 - 2.1.0-preview2-30478 + 2.1.0-preview2-26403-06 + 2.1.0-preview2-30554 15.6.1 4.7.49 11.0.2 - 4.5.0-preview3-26331-02 - 4.5.0-preview3-26331-02 - 4.5.0-preview3-26331-02 - 4.5.0-preview3-26331-02 - 4.5.0-preview3-26331-02 - 4.5.0-preview3-26331-02 - 4.5.0-preview3-26331-02 + 4.5.0-preview2-26403-05 + 4.5.0-preview2-26403-05 + 4.5.0-preview2-26403-05 + 4.5.0-preview2-26403-05 + 4.5.0-preview2-26403-05 + 4.5.0-preview2-26403-05 + 4.5.0-preview2-26403-05 0.8.0 2.3.1 2.4.0-beta.1.build3945 diff --git a/samples/SampleApp/Startup.cs b/samples/SampleApp/Startup.cs index 02c66a20be..3a4f29a3fd 100644 --- a/samples/SampleApp/Startup.cs +++ b/samples/SampleApp/Startup.cs @@ -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 diff --git a/src/Kestrel.Core/CoreStrings.resx b/src/Kestrel.Core/CoreStrings.resx index 5eb49c1320..0ba93c9219 100644 --- a/src/Kestrel.Core/CoreStrings.resx +++ b/src/Kestrel.Core/CoreStrings.resx @@ -477,7 +477,7 @@ Value must be a positive TimeSpan. - + The server certificate parameter is required. diff --git a/src/Kestrel.Core/HttpsConnectionAdapterOptions.cs b/src/Kestrel.Core/HttpsConnectionAdapterOptions.cs index 6ada701707..760c29cfc1 100644 --- a/src/Kestrel.Core/HttpsConnectionAdapterOptions.cs +++ b/src/Kestrel.Core/HttpsConnectionAdapterOptions.cs @@ -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 /// /// - /// 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. /// /// /// 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 /// public X509Certificate2 ServerCertificate { get; set; } + /// + /// + /// 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. + /// + /// + /// 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). + /// + /// + public Func ServerCertificateSelector { get; set; } + /// /// Specifies the client certificate requirements for a HTTPS connection. Defaults to . /// diff --git a/src/Kestrel.Core/Internal/HttpsConnectionAdapter.cs b/src/Kestrel.Core/Internal/HttpsConnectionAdapter.cs index c71aedfedb..4107da5118 100644 --- a/src/Kestrel.Core/Internal/HttpsConnectionAdapter.cs +++ b/src/Kestrel.Core/Internal/HttpsConnectionAdapter.cs @@ -22,6 +22,8 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Https.Internal private readonly HttpsConnectionAdapterOptions _options; private readonly X509Certificate2 _serverCertificate; + private readonly Func _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 } diff --git a/src/Kestrel.Core/KestrelConfigurationLoader.cs b/src/Kestrel.Core/KestrelConfigurationLoader.cs index 71337a11d2..b6468374b2 100644 --- a/src/Kestrel.Core/KestrelConfigurationLoader.cs +++ b/src/Kestrel.Core/KestrelConfigurationLoader.cs @@ -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); } diff --git a/src/Kestrel.Core/ListenOptionsHttpsExtensions.cs b/src/Kestrel.Core/ListenOptionsHttpsExtensions.cs index 7cc267c5de..162ee59d40 100644 --- a/src/Kestrel.Core/ListenOptionsHttpsExtensions.cs +++ b/src/Kestrel.Core/ListenOptionsHttpsExtensions.cs @@ -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; } diff --git a/src/Kestrel.Core/Properties/CoreStrings.Designer.cs b/src/Kestrel.Core/Properties/CoreStrings.Designer.cs index f964f3db90..dab35d664d 100644 --- a/src/Kestrel.Core/Properties/CoreStrings.Designer.cs +++ b/src/Kestrel.Core/Properties/CoreStrings.Designer.cs @@ -1693,16 +1693,16 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core /// /// The server certificate parameter is required. /// - internal static string ServiceCertificateRequired + internal static string ServerCertificateRequired { - get => GetString("ServiceCertificateRequired"); + get => GetString("ServerCertificateRequired"); } /// /// The server certificate parameter is required. /// - internal static string FormatServiceCertificateRequired() - => GetString("ServiceCertificateRequired"); + internal static string FormatServerCertificateRequired() + => GetString("ServerCertificateRequired"); /// /// No listening endpoints were configured. Binding to {address0} and {address1} by default. diff --git a/test/Kestrel.FunctionalTests/HttpsConnectionAdapterTests.cs b/test/Kestrel.FunctionalTests/HttpsConnectionAdapterTests.cs index c08d02fb3e..43ff38ce26 100644 --- a/test/Kestrel.FunctionalTests/HttpsConnectionAdapterTests.cs +++ b/test/Kestrel.FunctionalTests/HttpsConnectionAdapterTests.cs @@ -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()); +#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()); +#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(() => + 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()); +#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(() => + stream.AuthenticateAsClientAsync("localhost", new X509CertificateCollection(), SslProtocols.Tls12 | SslProtocols.Tls11, false)); + Assert.Equal(1, selectorCalled); + } + } + } [Fact] public async Task CertificatePassedToHttpContext() diff --git a/test/shared/TestResources.cs b/test/shared/TestResources.cs index b8ae46d18f..3218a1eaca 100644 --- a/test/shared/TestResources.cs +++ b/test/shared/TestResources.cs @@ -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"); + } } }