From c498f03cb487d2bfa1726982bb85abbd4bef0a96 Mon Sep 17 00:00:00 2001 From: "Chris Ross (ASP.NET)" Date: Wed, 13 Jun 2018 11:53:19 -0700 Subject: [PATCH] Expose Tls details as a feature. #2661 Limit Http/2 to TLS 1.2 #2251 Bootstrap functional tests #2238 --- build/dependencies.props | 69 +++++----- .../Features/ITlsHandshakeFeature.cs | 24 ++++ src/Kestrel.Core/CoreStrings.resx | 3 + .../Internal/Http2/Http2Connection.cs | 83 ++++++++---- .../Internal/Http2/Http2FrameWriter.cs | 14 ++ .../Internal/HttpsConnectionAdapter.cs | 9 ++ .../Internal/TlsConnectionFeature.cs | 18 ++- .../Properties/CoreStrings.Designer.cs | 14 ++ .../Internal/LibuvOutputConsumer.cs | 3 +- .../Internal/SocketConnection.cs | 4 +- .../Http2ConnectionTests.cs | 2 + .../Http2/HandshakeTests.cs | 102 ++++++++++++++ .../Http2/PipeReaderFactory.cs | 49 +++++++ .../Kestrel.FunctionalTests/Http2/TlsTests.cs | 126 ++++++++++++++++++ .../HttpsConnectionAdapterTests.cs | 34 ++++- .../TestHelpers/TestServer.cs | 29 ++-- ...rel.Transport.Libuv.FunctionalTests.csproj | 1 + .../TransportSelector.cs | 2 +- ...l.Transport.Sockets.FunctionalTests.csproj | 1 + .../TransportSelector.cs | 2 +- 20 files changed, 504 insertions(+), 85 deletions(-) create mode 100644 src/Connections.Abstractions/Features/ITlsHandshakeFeature.cs create mode 100644 test/Kestrel.FunctionalTests/Http2/HandshakeTests.cs create mode 100644 test/Kestrel.FunctionalTests/Http2/PipeReaderFactory.cs create mode 100644 test/Kestrel.FunctionalTests/Http2/TlsTests.cs diff --git a/build/dependencies.props b/build/dependencies.props index 29db07dbd5..b31aece010 100644 --- a/build/dependencies.props +++ b/build/dependencies.props @@ -5,47 +5,48 @@ 0.10.13 - 2.2.0-preview1-34411 - 2.2.0-preview1-17081 + 2.2.0-preview1-34484 + 2.2.0-preview1-17087 1.10.0 - 2.2.0-preview1-34411 - 2.2.0-preview1-34411 - 2.2.0-preview1-34411 - 2.2.0-preview1-34411 - 2.2.0-preview1-34411 - 2.2.0-preview1-34411 - 2.2.0-preview1-34411 - 2.2.0-preview1-34411 - 2.2.0-preview1-34411 - 2.2.0-preview1-34411 - 2.2.0-preview1-34411 - 2.2.0-preview1-34411 - 2.2.0-preview1-34411 - 2.2.0-preview1-34411 - 2.2.0-preview1-34411 - 2.2.0-preview1-34411 - 2.2.0-preview1-34411 - 2.2.0-preview1-34411 - 2.2.0-preview1-34411 - 2.2.0-preview1-34411 - 2.2.0-preview1-34411 - 2.2.0-preview1-34411 - 2.2.0-preview1-34411 + 2.2.0-preview1-34484 + 2.2.0-preview1-34484 + 2.2.0-preview1-34484 + 2.2.0-preview1-34484 + 2.2.0-preview1-34484 + 2.2.0-preview1-34484 + 2.2.0-preview1-34484 + 2.2.0-preview1-34484 + 2.2.0-preview1-34484 + 2.2.0-preview1-34484 + 2.2.0-preview1-34484 + 2.2.0-preview1-34484 + 2.2.0-preview1-34484 + 2.2.0-preview1-34484 + 2.2.0-preview1-34484 + 2.2.0-preview1-34484 + 2.2.0-preview1-34484 + 2.2.0-preview1-34484 + 2.2.0-preview1-34484 + 2.2.0-preview1-34484 + 2.2.0-preview1-34484 + 2.2.0-preview1-34484 + 2.2.0-preview1-34484 2.0.0 2.1.0 - 2.2.0-preview1-26606-01 - 2.2.0-preview1-34411 + 2.2.0-preview1-26614-02 + 2.2.0-preview1-34484 15.6.1 4.7.49 2.0.3 11.0.2 - 4.6.0-preview1-26605-01 - 4.6.0-preview1-26605-01 - 4.6.0-preview1-26605-01 - 4.6.0-preview1-26605-01 - 4.6.0-preview1-26605-01 - 4.6.0-preview1-26605-01 - 4.6.0-preview1-26605-01 + 4.6.0-preview1-26613-07 + 4.6.0-preview1-26613-07 + 4.6.0-preview1-26613-07 + 4.6.0-preview1-26613-07 + 4.6.0-preview1-26613-07 + 4.6.0-preview1-26613-07 + 4.6.0-preview1-26613-07 + 4.6.0-preview1-26613-07 1.3.7 0.8.0 2.3.1 diff --git a/src/Connections.Abstractions/Features/ITlsHandshakeFeature.cs b/src/Connections.Abstractions/Features/ITlsHandshakeFeature.cs new file mode 100644 index 0000000000..b408323b30 --- /dev/null +++ b/src/Connections.Abstractions/Features/ITlsHandshakeFeature.cs @@ -0,0 +1,24 @@ +// 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.Authentication; + +namespace Microsoft.AspNetCore.Connections.Features +{ + public interface ITlsHandshakeFeature + { + SslProtocols Protocol { get; } + + CipherAlgorithmType CipherAlgorithm { get; } + + int CipherStrength { get; } + + HashAlgorithmType HashAlgorithm { get; } + + int HashStrength { get; } + + ExchangeAlgorithmType KeyExchangeAlgorithm { get; } + + int KeyExchangeStrength { get; } + } +} diff --git a/src/Kestrel.Core/CoreStrings.resx b/src/Kestrel.Core/CoreStrings.resx index 0230dfca6b..d0448aa7d4 100644 --- a/src/Kestrel.Core/CoreStrings.resx +++ b/src/Kestrel.Core/CoreStrings.resx @@ -518,4 +518,7 @@ For more information on configuring HTTPS see https://go.microsoft.com/fwlink/?l The received frame size of {size} exceeds the limit {limit}. + + Tls 1.2 or later must be used for HTTP/2. {protocol} was negotiated. + \ No newline at end of file diff --git a/src/Kestrel.Core/Internal/Http2/Http2Connection.cs b/src/Kestrel.Core/Internal/Http2/Http2Connection.cs index 58646cca12..2614ec5a18 100644 --- a/src/Kestrel.Core/Internal/Http2/Http2Connection.cs +++ b/src/Kestrel.Core/Internal/Http2/Http2Connection.cs @@ -3,14 +3,15 @@ using System; using System.Buffers; -using System.Collections; using System.Collections.Concurrent; using System.IO.Pipelines; +using System.Security.Authentication; using System.Text; using System.Threading.Tasks; +using Microsoft.AspNetCore.Connections; +using Microsoft.AspNetCore.Connections.Features; using Microsoft.AspNetCore.Hosting.Server; using Microsoft.AspNetCore.Http.Features; -using Microsoft.AspNetCore.Connections; using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http; using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2.HPack; using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure; @@ -115,31 +116,11 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2 try { - while (!_stopping) - { - var result = await Input.ReadAsync(); - var readableBuffer = result.Buffer; - var consumed = readableBuffer.Start; - var examined = readableBuffer.End; + ValidateTlsRequirements(); - try - { - if (!readableBuffer.IsEmpty) - { - if (ParsePreface(readableBuffer, out consumed, out examined)) - { - break; - } - } - else if (result.IsCompleted) - { - return; - } - } - finally - { - Input.AdvanceTo(consumed, examined); - } + if (!await TryReadPrefaceAsync()) + { + return; } if (!_stopping) @@ -213,6 +194,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2 } await _frameWriter.WriteGoAwayAsync(_highestOpenedStreamId, errorCode); + _frameWriter.Complete(); } finally { @@ -222,6 +204,55 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2 } } + // https://tools.ietf.org/html/rfc7540#section-9.2 + // Some of these could not be checked in advance. Fail before using the connection. + private void ValidateTlsRequirements() + { + var tlsFeature = ConnectionFeatures.Get(); + if (tlsFeature == null) + { + // Not using TLS at all. + return; + } + + if (tlsFeature.Protocol < SslProtocols.Tls12) + { + throw new Http2ConnectionErrorException(CoreStrings.FormatHttp2ErrorMinTlsVersion(tlsFeature.Protocol), Http2ErrorCode.INADEQUATE_SECURITY); + } + } + + private async Task TryReadPrefaceAsync() + { + while (!_stopping) + { + var result = await Input.ReadAsync(); + var readableBuffer = result.Buffer; + var consumed = readableBuffer.Start; + var examined = readableBuffer.End; + + try + { + if (!readableBuffer.IsEmpty) + { + if (ParsePreface(readableBuffer, out consumed, out examined)) + { + return true; + } + } + if (result.IsCompleted) + { + return false; + } + } + finally + { + Input.AdvanceTo(consumed, examined); + } + } + + return false; + } + private bool ParsePreface(ReadOnlySequence readableBuffer, out SequencePosition consumed, out SequencePosition examined) { consumed = readableBuffer.Start; diff --git a/src/Kestrel.Core/Internal/Http2/Http2FrameWriter.cs b/src/Kestrel.Core/Internal/Http2/Http2FrameWriter.cs index e31d4b7f4f..df20e02638 100644 --- a/src/Kestrel.Core/Internal/Http2/Http2FrameWriter.cs +++ b/src/Kestrel.Core/Internal/Http2/Http2FrameWriter.cs @@ -32,6 +32,20 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2 _outputReader = outputPipeReader; } + public void Complete() + { + lock (_writeLock) + { + if (_completed) + { + return; + } + + _completed = true; + _outputWriter.Complete(); + } + } + public void Abort(Exception ex) { lock (_writeLock) diff --git a/src/Kestrel.Core/Internal/HttpsConnectionAdapter.cs b/src/Kestrel.Core/Internal/HttpsConnectionAdapter.cs index 24f97b0820..4eaeffff5c 100644 --- a/src/Kestrel.Core/Internal/HttpsConnectionAdapter.cs +++ b/src/Kestrel.Core/Internal/HttpsConnectionAdapter.cs @@ -10,6 +10,7 @@ using System.Security.Cryptography.X509Certificates; using System.Threading; using System.Threading.Tasks; using Microsoft.AspNetCore.Connections; +using Microsoft.AspNetCore.Connections.Features; using Microsoft.AspNetCore.Http.Features; using Microsoft.AspNetCore.Server.Kestrel.Core; using Microsoft.AspNetCore.Server.Kestrel.Core.Adapter.Internal; @@ -77,6 +78,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Https.Internal bool certificateRequired; var feature = new TlsConnectionFeature(); context.Features.Set(feature); + context.Features.Set(feature); if (_options.ClientCertificateMode == ClientCertificateMode.NoCertificate) { @@ -210,6 +212,13 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Https.Internal #error TFMs need to be updated #endif feature.ClientCertificate = ConvertToX509Certificate2(sslStream.RemoteCertificate); + feature.CipherAlgorithm = sslStream.CipherAlgorithm; + feature.CipherStrength = sslStream.CipherStrength; + feature.HashAlgorithm = sslStream.HashAlgorithm; + feature.HashStrength = sslStream.HashStrength; + feature.KeyExchangeAlgorithm = sslStream.KeyExchangeAlgorithm; + feature.KeyExchangeStrength = sslStream.KeyExchangeStrength; + feature.Protocol = sslStream.SslProtocol; return new HttpsAdaptedConnection(sslStream); } diff --git a/src/Kestrel.Core/Internal/TlsConnectionFeature.cs b/src/Kestrel.Core/Internal/TlsConnectionFeature.cs index a914024131..97cd9b426f 100644 --- a/src/Kestrel.Core/Internal/TlsConnectionFeature.cs +++ b/src/Kestrel.Core/Internal/TlsConnectionFeature.cs @@ -2,20 +2,36 @@ // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System; +using System.Security.Authentication; using System.Security.Cryptography.X509Certificates; using System.Threading; using System.Threading.Tasks; +using Microsoft.AspNetCore.Connections.Features; using Microsoft.AspNetCore.Http.Features; using Microsoft.AspNetCore.Server.Kestrel.Core.Features; namespace Microsoft.AspNetCore.Server.Kestrel.Https.Internal { - internal class TlsConnectionFeature : ITlsConnectionFeature, ITlsApplicationProtocolFeature + internal class TlsConnectionFeature : ITlsConnectionFeature, ITlsApplicationProtocolFeature, ITlsHandshakeFeature { public X509Certificate2 ClientCertificate { get; set; } public ReadOnlyMemory ApplicationProtocol { get; set; } + public SslProtocols Protocol { get; set; } + + public CipherAlgorithmType CipherAlgorithm { get; set; } + + public int CipherStrength { get; set; } + + public HashAlgorithmType HashAlgorithm { get; set; } + + public int HashStrength { get; set; } + + public ExchangeAlgorithmType KeyExchangeAlgorithm { get; set; } + + public int KeyExchangeStrength { get; set; } + public Task GetClientCertificateAsync(CancellationToken cancellationToken) { return Task.FromResult(ClientCertificate); diff --git a/src/Kestrel.Core/Properties/CoreStrings.Designer.cs b/src/Kestrel.Core/Properties/CoreStrings.Designer.cs index 520d93cc2f..070fe9c45d 100644 --- a/src/Kestrel.Core/Properties/CoreStrings.Designer.cs +++ b/src/Kestrel.Core/Properties/CoreStrings.Designer.cs @@ -1876,6 +1876,20 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core internal static string FormatHttp2ErrorFrameOverLimit(object size, object limit) => string.Format(CultureInfo.CurrentCulture, GetString("Http2ErrorFrameOverLimit", "size", "limit"), size, limit); + /// + /// Tls 1.2 or later must be used for HTTP/2. {protocol} was negotiated. + /// + internal static string Http2ErrorMinTlsVersion + { + get => GetString("Http2ErrorMinTlsVersion"); + } + + /// + /// Tls 1.2 or later must be used for HTTP/2. {protocol} was negotiated. + /// + internal static string FormatHttp2ErrorMinTlsVersion(object protocol) + => string.Format(CultureInfo.CurrentCulture, GetString("Http2ErrorMinTlsVersion", "protocol"), protocol); + private static string GetString(string name, params string[] formatterNames) { var value = _resourceManager.GetString(name); diff --git a/src/Kestrel.Transport.Libuv/Internal/LibuvOutputConsumer.cs b/src/Kestrel.Transport.Libuv/Internal/LibuvOutputConsumer.cs index 4666ae198e..6049245537 100644 --- a/src/Kestrel.Transport.Libuv/Internal/LibuvOutputConsumer.cs +++ b/src/Kestrel.Transport.Libuv/Internal/LibuvOutputConsumer.cs @@ -89,7 +89,8 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Transport.Libuv.Internal writeReq = null; } } - else if (result.IsCompleted) + + if (result.IsCompleted) { break; } diff --git a/src/Kestrel.Transport.Sockets/Internal/SocketConnection.cs b/src/Kestrel.Transport.Sockets/Internal/SocketConnection.cs index 79fb522949..5e2d768913 100644 --- a/src/Kestrel.Transport.Sockets/Internal/SocketConnection.cs +++ b/src/Kestrel.Transport.Sockets/Internal/SocketConnection.cs @@ -245,13 +245,13 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Transport.Sockets.Internal { var result = await Output.ReadAsync(); - var buffer = result.Buffer; - if (result.IsCanceled) { break; } + var buffer = result.Buffer; + var end = buffer.End; var isCompleted = result.IsCompleted; if (!buffer.IsEmpty) diff --git a/test/Kestrel.Core.Tests/Http2ConnectionTests.cs b/test/Kestrel.Core.Tests/Http2ConnectionTests.cs index 549ec6a007..6459b6acdd 100644 --- a/test/Kestrel.Core.Tests/Http2ConnectionTests.cs +++ b/test/Kestrel.Core.Tests/Http2ConnectionTests.cs @@ -22,6 +22,7 @@ using Microsoft.AspNetCore.Server.Kestrel.Transport.Abstractions.Internal; using Microsoft.AspNetCore.Testing; using Microsoft.Net.Http.Headers; using Xunit; +using Microsoft.AspNetCore.Http.Features; namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests { @@ -274,6 +275,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests _connectionContext = new Http2ConnectionContext { + ConnectionFeatures = new FeatureCollection(), ServiceContext = new TestServiceContext() { Log = new TestKestrelTrace(_logger) diff --git a/test/Kestrel.FunctionalTests/Http2/HandshakeTests.cs b/test/Kestrel.FunctionalTests/Http2/HandshakeTests.cs new file mode 100644 index 0000000000..3b3a047372 --- /dev/null +++ b/test/Kestrel.FunctionalTests/Http2/HandshakeTests.cs @@ -0,0 +1,102 @@ +// 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. + +#if NETCOREAPP2_2 + +using System; +using System.Linq; +using System.Net; +using System.Net.Http; +using System.Net.Security; +using System.Runtime.InteropServices; +using System.Security.Cryptography.X509Certificates; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Server.Kestrel.Core; +using Microsoft.AspNetCore.Server.Kestrel.Core.Features; +using Microsoft.AspNetCore.Testing; +using Microsoft.AspNetCore.Testing.xunit; +using Microsoft.Extensions.Logging.Testing; +using Xunit; + +namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests.Http2 +{ + [OSSkipCondition(OperatingSystems.MacOSX, SkipReason = "Missing SslStream ALPN support: https://github.com/dotnet/corefx/issues/30492")] + [OSSkipCondition(OperatingSystems.Linux, SkipReason = "Curl requires a custom install to support HTTP/2, see https://askubuntu.com/questions/884899/how-do-i-install-curl-with-http2-support")] + [MinimumOSVersion(OperatingSystems.Windows, WindowsVersions.Win10)] + public class HandshakeTests : LoggedTest + { + private static X509Certificate2 _x509Certificate2 = TestResources.GetTestCertificate(); + + public HttpClient Client { get; set; } + + public HandshakeTests() + { + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + // We don't want the default SocketsHttpHandler, it doesn't support HTTP/2 yet. + Client = new HttpClient(new WinHttpHandler() + { + ServerCertificateValidationCallback = HttpClientHandler.DangerousAcceptAnyServerCertificateValidator + }); + } + } + + [ConditionalFact] + public async Task TlsAlpnHandshakeSelectsHttp2From1and2() + { + using (var server = new TestServer(context => + { + var tlsFeature = context.Features.Get(); + Assert.NotNull(tlsFeature); + Assert.True(SslApplicationProtocol.Http2.Protocol.Span.SequenceEqual(tlsFeature.ApplicationProtocol.Span), + "ALPN: " + tlsFeature.ApplicationProtocol.Length); + + return context.Response.WriteAsync("hello world " + context.Request.Protocol); + }, new TestServiceContext(LoggerFactory), + kestrelOptions => + { + kestrelOptions.Listen(IPAddress.Loopback, 0, listenOptions => + { + listenOptions.Protocols = HttpProtocols.Http1AndHttp2; + listenOptions.UseHttps(_x509Certificate2); + }); + })) + { + var result = await Client.GetStringAsync($"https://localhost:{server.Port}/"); + Assert.Equal("hello world HTTP/2", result); + } + } + + [ConditionalFact] + public async Task TlsAlpnHandshakeSelectsHttp2() + { + using (var server = new TestServer(context => + { + var tlsFeature = context.Features.Get(); + Assert.NotNull(tlsFeature); + Assert.True(SslApplicationProtocol.Http2.Protocol.Span.SequenceEqual(tlsFeature.ApplicationProtocol.Span), + "ALPN: " + tlsFeature.ApplicationProtocol.Length); + + return context.Response.WriteAsync("hello world " + context.Request.Protocol); + }, new TestServiceContext(LoggerFactory), + kestrelOptions => + { + kestrelOptions.Listen(IPAddress.Loopback, 0, listenOptions => + { + listenOptions.Protocols = HttpProtocols.Http2; + listenOptions.UseHttps(_x509Certificate2); + }); + })) + { + var result = await Client.GetStringAsync($"https://localhost:{server.Port}/"); + Assert.Equal("hello world HTTP/2", result); + } + } + } +} +#elif NET461 // No ALPN support +#else +#error TFMs need updating +#endif \ No newline at end of file diff --git a/test/Kestrel.FunctionalTests/Http2/PipeReaderFactory.cs b/test/Kestrel.FunctionalTests/Http2/PipeReaderFactory.cs new file mode 100644 index 0000000000..f3e2c90332 --- /dev/null +++ b/test/Kestrel.FunctionalTests/Http2/PipeReaderFactory.cs @@ -0,0 +1,49 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System.Threading; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Server.Kestrel.Core.Adapter.Internal; + +namespace System.IO.Pipelines +{ + internal class PipeReaderFactory + { + private static readonly Action _cancelReader = state => ((PipeReader)state).CancelPendingRead(); + + public static PipeReader CreateFromStream(PipeOptions options, Stream stream, CancellationToken cancellationToken) + { + if (!stream.CanRead) + { + throw new NotSupportedException(); + } + + var pipe = new Pipe(options); + _ = CopyToAsync(stream, pipe, cancellationToken); + + return pipe.Reader; + } + + private static async Task CopyToAsync(Stream stream, Pipe pipe, CancellationToken cancellationToken) + { + // We manually register for cancellation here in case the Stream implementation ignores it + using (var registration = cancellationToken.Register(_cancelReader, pipe.Reader)) + { + try + { + await stream.CopyToAsync(new RawStream(null, pipe.Writer), bufferSize: 4096, cancellationToken); + } + catch (OperationCanceledException) + { + // Ignore the cancellation signal (the pipe reader is already wired up for cancellation when the token trips) + } + catch (Exception ex) + { + pipe.Writer.Complete(ex); + return; + } + pipe.Writer.Complete(); + } + } + } +} \ No newline at end of file diff --git a/test/Kestrel.FunctionalTests/Http2/TlsTests.cs b/test/Kestrel.FunctionalTests/Http2/TlsTests.cs new file mode 100644 index 0000000000..081b9c8c1c --- /dev/null +++ b/test/Kestrel.FunctionalTests/Http2/TlsTests.cs @@ -0,0 +1,126 @@ +// 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. + +#if NETCOREAPP2_2 + +using System.Collections.Generic; +using System.IO; +using System.IO.Pipelines; +using System.Net; +using System.Net.Security; +using System.Security.Authentication; +using System.Security.Cryptography.X509Certificates; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Server.Kestrel.Core; +using Microsoft.AspNetCore.Server.Kestrel.Core.Features; +using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2; +using Microsoft.AspNetCore.Testing; +using Microsoft.AspNetCore.Testing.xunit; +using Microsoft.Extensions.Logging.Testing; +using Xunit; + +namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests.Http2 +{ + [OSSkipCondition(OperatingSystems.MacOSX, SkipReason = "Missing SslStream ALPN support: https://github.com/dotnet/corefx/issues/30492")] + public class TlsTests : LoggedTest + { + private static X509Certificate2 _x509Certificate2 = TestResources.GetTestCertificate(); + + [ConditionalFact] + public async Task TlsHandshakeRejectsTlsLessThan12() + { + using (var server = new TestServer(context => + { + var tlsFeature = context.Features.Get(); + Assert.NotNull(tlsFeature); + Assert.Equal(tlsFeature.ApplicationProtocol, SslApplicationProtocol.Http2.Protocol); + + return context.Response.WriteAsync("hello world " + context.Request.Protocol); + }, new TestServiceContext(LoggerFactory), + kestrelOptions => + { + kestrelOptions.Listen(IPAddress.Loopback, 0, listenOptions => + { + listenOptions.Protocols = HttpProtocols.Http2; + listenOptions.UseHttps(_x509Certificate2, httpsOptions => + { + httpsOptions.SslProtocols = SslProtocols.Tls11 | SslProtocols.Tls12; + }); + }); + })) + { + var connection = server.CreateConnection(); + var sslStream = new SslStream(connection.Stream); + await sslStream.AuthenticateAsClientAsync(new SslClientAuthenticationOptions() + { + TargetHost = "localhost", + RemoteCertificateValidationCallback = (_, __, ___, ____) => true, + ApplicationProtocols = new List() { SslApplicationProtocol.Http2, SslApplicationProtocol.Http11 }, + EnabledSslProtocols = SslProtocols.Tls11, // Intentionally less than the required 1.2 + }, CancellationToken.None); + + var reader = PipeReaderFactory.CreateFromStream(PipeOptions.Default, sslStream, CancellationToken.None); + await WaitForConnectionErrorAsync(reader, ignoreNonGoAwayFrames: false, expectedLastStreamId: 0, expectedErrorCode: Http2ErrorCode.INADEQUATE_SECURITY); + } + } + + private async Task WaitForConnectionErrorAsync(PipeReader reader, bool ignoreNonGoAwayFrames, int expectedLastStreamId, Http2ErrorCode expectedErrorCode) + { + var frame = await ReceiveFrameAsync(reader); + + if (ignoreNonGoAwayFrames) + { + while (frame.Type != Http2FrameType.GOAWAY) + { + frame = await ReceiveFrameAsync(reader); + } + } + + Assert.Equal(Http2FrameType.GOAWAY, frame.Type); + Assert.Equal(8, frame.Length); + Assert.Equal(0, frame.Flags); + Assert.Equal(0, frame.StreamId); + Assert.Equal(expectedLastStreamId, frame.GoAwayLastStreamId); + Assert.Equal(expectedErrorCode, frame.GoAwayErrorCode); + } + + private async Task ReceiveFrameAsync(PipeReader reader) + { + var frame = new Http2Frame(); + + while (true) + { + var result = await reader.ReadAsync(); + var buffer = result.Buffer; + var consumed = buffer.Start; + var examined = buffer.End; + + if (buffer.IsEmpty && result.IsCompleted) + { + throw new IOException("The reader completed without returning a frame."); + } + + try + { + // Assert.True(buffer.Length > 0); + + if (Http2FrameReader.ReadFrame(buffer, frame, 16_384, out consumed, out examined)) + { + return frame; + } + } + finally + { + reader.AdvanceTo(consumed, examined); + } + } + } + } +} +#elif NET461 // No ALPN support +#else +#error TFMs need updating +#endif \ No newline at end of file diff --git a/test/Kestrel.FunctionalTests/HttpsConnectionAdapterTests.cs b/test/Kestrel.FunctionalTests/HttpsConnectionAdapterTests.cs index 87be49ccec..d4c6c0c230 100644 --- a/test/Kestrel.FunctionalTests/HttpsConnectionAdapterTests.cs +++ b/test/Kestrel.FunctionalTests/HttpsConnectionAdapterTests.cs @@ -14,6 +14,7 @@ using System.Security.Cryptography.X509Certificates; using System.Text; using System.Threading; using System.Threading.Tasks; +using Microsoft.AspNetCore.Connections.Features; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http.Features; using Microsoft.AspNetCore.Server.Kestrel.Core; @@ -30,8 +31,6 @@ namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests private static X509Certificate2 _x509Certificate2 = TestResources.GetTestCertificate(); private static X509Certificate2 _x509Certificate2NoExt = TestResources.GetTestCertificate("no_extensions.pfx"); - // https://github.com/aspnet/KestrelHttpServer/issues/240 - // This test currently fails on mono because of an issue with SslStream. [Fact] public async Task CanReadAndWriteWithHttpsConnectionAdapter() { @@ -55,6 +54,37 @@ namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests } } + [Fact] + public async Task HandshakeDetailsAreAvailable() + { + var listenOptions = new ListenOptions(new IPEndPoint(IPAddress.Loopback, 0)) + { + ConnectionAdapters = + { + new HttpsConnectionAdapter(new HttpsConnectionAdapterOptions { ServerCertificate = _x509Certificate2 }) + } + }; + + using (var server = new TestServer(context => + { + var tlsFeature = context.Features.Get(); + Assert.NotNull(tlsFeature); + Assert.True(tlsFeature.Protocol > SslProtocols.None, "Protocol"); + Assert.True(tlsFeature.CipherAlgorithm > CipherAlgorithmType.Null, "Cipher"); + Assert.True(tlsFeature.CipherStrength > 0, "CipherStrength"); + Assert.True(tlsFeature.HashAlgorithm >= HashAlgorithmType.None, "HashAlgorithm"); // May be None on Linux. + Assert.True(tlsFeature.HashStrength >= 0, "HashStrength"); // May be 0 for some algorithms + Assert.True(tlsFeature.KeyExchangeAlgorithm > ExchangeAlgorithmType.None, "KeyExchangeAlgorithm"); + Assert.True(tlsFeature.KeyExchangeStrength >= 0, "KeyExchangeStrength"); // May be 0 on mac + + return context.Response.WriteAsync("hello world"); + }, new TestServiceContext(LoggerFactory), listenOptions)) + { + var result = await HttpClientSlim.GetStringAsync($"https://localhost:{server.Port}/", validateCertificate: false); + Assert.Equal("hello world", result); + } + } + [Fact] public async Task RequireCertificateFailsWhenNoCertificate() { diff --git a/test/Kestrel.FunctionalTests/TestHelpers/TestServer.cs b/test/Kestrel.FunctionalTests/TestHelpers/TestServer.cs index 7a54c418fa..9b290fdc37 100644 --- a/test/Kestrel.FunctionalTests/TestHelpers/TestServer.cs +++ b/test/Kestrel.FunctionalTests/TestHelpers/TestServer.cs @@ -45,15 +45,24 @@ namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests } public TestServer(RequestDelegate app, TestServiceContext context, ListenOptions listenOptions, Action configureServices) + : this(app, context, options => options.ListenOptions.Add(listenOptions), configureServices) + { + } + public TestServer(RequestDelegate app, TestServiceContext context, Action configureKestrel) + : this(app, context, configureKestrel, _ => { }) + { + } + + public TestServer(RequestDelegate app, TestServiceContext context, Action configureKestrel, Action configureServices) { _app = app; - _listenOptions = listenOptions; Context = context; _host = TransportSelector.GetWebHostBuilder() - .UseKestrel(o => + .UseKestrel(options => { - o.ListenOptions.Add(_listenOptions); + configureKestrel(options); + _listenOptions = options.ListenOptions.First(); }) .ConfigureServices(services => { @@ -70,7 +79,6 @@ namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests } return new KestrelServer(sp.GetRequiredService(), context); }); - RemoveDevCert(services); configureServices(services); }) .UseSetting(WebHostDefaults.ApplicationKey, typeof(TestServer).GetTypeInfo().Assembly.FullName) @@ -79,19 +87,6 @@ namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests _host.Start(); } - public static void RemoveDevCert(IServiceCollection services) - { - // KestrelServerOptionsSetup would scan all system certificates on every test server creation - // making test runs very slow - foreach (var descriptor in services.ToArray()) - { - if (descriptor.ImplementationType == typeof(KestrelServerOptionsSetup)) - { - services.Remove(descriptor); - } - } - } - public IPEndPoint EndPoint => _listenOptions.IPEndPoint; public int Port => _listenOptions.IPEndPoint.Port; public AddressFamily AddressFamily => _listenOptions.IPEndPoint.AddressFamily; diff --git a/test/Kestrel.Transport.Libuv.FunctionalTests/Kestrel.Transport.Libuv.FunctionalTests.csproj b/test/Kestrel.Transport.Libuv.FunctionalTests/Kestrel.Transport.Libuv.FunctionalTests.csproj index 0a37ba9fbd..3f87ee3ccd 100644 --- a/test/Kestrel.Transport.Libuv.FunctionalTests/Kestrel.Transport.Libuv.FunctionalTests.csproj +++ b/test/Kestrel.Transport.Libuv.FunctionalTests/Kestrel.Transport.Libuv.FunctionalTests.csproj @@ -27,6 +27,7 @@ + diff --git a/test/Kestrel.Transport.Libuv.FunctionalTests/TransportSelector.cs b/test/Kestrel.Transport.Libuv.FunctionalTests/TransportSelector.cs index 98a41b7760..ca209ba6e7 100644 --- a/test/Kestrel.Transport.Libuv.FunctionalTests/TransportSelector.cs +++ b/test/Kestrel.Transport.Libuv.FunctionalTests/TransportSelector.cs @@ -11,7 +11,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests { public static IWebHostBuilder GetWebHostBuilder(Func> memoryPoolFactory = null) { - return new WebHostBuilder().UseLibuv(options => { options.MemoryPoolFactory = memoryPoolFactory ?? options.MemoryPoolFactory; }).ConfigureServices(TestServer.RemoveDevCert); + return new WebHostBuilder().UseLibuv(options => { options.MemoryPoolFactory = memoryPoolFactory ?? options.MemoryPoolFactory; }); } } } diff --git a/test/Kestrel.Transport.Sockets.FunctionalTests/Kestrel.Transport.Sockets.FunctionalTests.csproj b/test/Kestrel.Transport.Sockets.FunctionalTests/Kestrel.Transport.Sockets.FunctionalTests.csproj index dff7f9c2ea..82a8cab1e3 100644 --- a/test/Kestrel.Transport.Sockets.FunctionalTests/Kestrel.Transport.Sockets.FunctionalTests.csproj +++ b/test/Kestrel.Transport.Sockets.FunctionalTests/Kestrel.Transport.Sockets.FunctionalTests.csproj @@ -26,6 +26,7 @@ + diff --git a/test/Kestrel.Transport.Sockets.FunctionalTests/TransportSelector.cs b/test/Kestrel.Transport.Sockets.FunctionalTests/TransportSelector.cs index 9d5ea6ede5..3e3cfe3f6c 100644 --- a/test/Kestrel.Transport.Sockets.FunctionalTests/TransportSelector.cs +++ b/test/Kestrel.Transport.Sockets.FunctionalTests/TransportSelector.cs @@ -11,7 +11,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests { public static IWebHostBuilder GetWebHostBuilder(Func> memoryPoolFactory = null) { - return new WebHostBuilder().UseSockets(options => { options.MemoryPoolFactory = memoryPoolFactory ?? options.MemoryPoolFactory; }).ConfigureServices(TestServer.RemoveDevCert); + return new WebHostBuilder().UseSockets(options => { options.MemoryPoolFactory = memoryPoolFactory ?? options.MemoryPoolFactory; }); } } }