From 06d7fe73a995000ce79007e03785ee77d4dbb508 Mon Sep 17 00:00:00 2001 From: Chris Ross Date: Wed, 13 Feb 2019 08:39:45 -0800 Subject: [PATCH] Implement ITlsHandshakeFeature for HttpSys (#7284) --- src/Servers/HttpSys/src/FeatureContext.cs | 23 +++++++ ...sityLevel .cs => Http503VerbosityLevel.cs} | 0 ...Microsoft.AspNetCore.Server.HttpSys.csproj | 1 + .../HttpSys/src/RequestProcessing/Request.cs | 60 +++++++++++++++++ .../HttpSys/src/StandardFeatureCollection.cs | 2 + .../test/FunctionalTests/HttpsTests.cs | 66 ++++++++++++++----- ...Core.Server.HttpSys.FunctionalTests.csproj | 5 ++ .../HttpSys/test/FunctionalTests/Utilities.cs | 37 +++++++++-- .../HttpSys/NativeInterop/HttpApiTypes.cs | 18 ++++- .../RequestProcessing/NativeRequestContext.cs | 45 ++++++++++--- 10 files changed, 221 insertions(+), 36 deletions(-) rename src/Servers/HttpSys/src/{Http503VerbosityLevel .cs => Http503VerbosityLevel.cs} (100%) diff --git a/src/Servers/HttpSys/src/FeatureContext.cs b/src/Servers/HttpSys/src/FeatureContext.cs index 27a38a84d8..9088f60590 100644 --- a/src/Servers/HttpSys/src/FeatureContext.cs +++ b/src/Servers/HttpSys/src/FeatureContext.cs @@ -6,10 +6,12 @@ using System.Collections.Generic; using System.Globalization; using System.IO; using System.Net; +using System.Security.Authentication; using System.Security.Claims; using System.Security.Cryptography.X509Certificates; using System.Threading; using System.Threading.Tasks; +using Microsoft.AspNetCore.Connections.Features; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http.Features; using Microsoft.AspNetCore.Http.Features.Authentication; @@ -24,6 +26,7 @@ namespace Microsoft.AspNetCore.Server.HttpSys IHttpResponseFeature, IHttpSendFileFeature, ITlsConnectionFeature, + ITlsHandshakeFeature, // ITlsTokenBindingFeature, TODO: https://github.com/aspnet/HttpSysServer/issues/231 IHttpBufferingFeature, IHttpRequestLifetimeFeature, @@ -336,6 +339,12 @@ namespace Microsoft.AspNetCore.Server.HttpSys { return Request.IsHttps ? this : null; } + + internal ITlsHandshakeFeature GetTlsHandshakeFeature() + { + return Request.IsHttps ? this : null; + } + /* TODO: https://github.com/aspnet/HttpSysServer/issues/231 byte[] ITlsTokenBindingFeature.GetProvidedTokenBindingId() => Request.GetProvidedTokenBindingId(); @@ -482,6 +491,20 @@ namespace Microsoft.AspNetCore.Server.HttpSys set => Request.MaxRequestBodySize = value; } + SslProtocols ITlsHandshakeFeature.Protocol => Request.Protocol; + + CipherAlgorithmType ITlsHandshakeFeature.CipherAlgorithm => Request.CipherAlgorithm; + + int ITlsHandshakeFeature.CipherStrength => Request.CipherStrength; + + HashAlgorithmType ITlsHandshakeFeature.HashAlgorithm => Request.HashAlgorithm; + + int ITlsHandshakeFeature.HashStrength => Request.HashStrength; + + ExchangeAlgorithmType ITlsHandshakeFeature.KeyExchangeAlgorithm => Request.KeyExchangeAlgorithm; + + int ITlsHandshakeFeature.KeyExchangeStrength => Request.KeyExchangeStrength; + internal async Task OnResponseStart() { if (_responseStarted) diff --git a/src/Servers/HttpSys/src/Http503VerbosityLevel .cs b/src/Servers/HttpSys/src/Http503VerbosityLevel.cs similarity index 100% rename from src/Servers/HttpSys/src/Http503VerbosityLevel .cs rename to src/Servers/HttpSys/src/Http503VerbosityLevel.cs diff --git a/src/Servers/HttpSys/src/Microsoft.AspNetCore.Server.HttpSys.csproj b/src/Servers/HttpSys/src/Microsoft.AspNetCore.Server.HttpSys.csproj index 9117a4d39b..2ccf8af84c 100644 --- a/src/Servers/HttpSys/src/Microsoft.AspNetCore.Server.HttpSys.csproj +++ b/src/Servers/HttpSys/src/Microsoft.AspNetCore.Server.HttpSys.csproj @@ -16,6 +16,7 @@ + diff --git a/src/Servers/HttpSys/src/RequestProcessing/Request.cs b/src/Servers/HttpSys/src/RequestProcessing/Request.cs index 3dedf347a9..9112dfaca9 100644 --- a/src/Servers/HttpSys/src/RequestProcessing/Request.cs +++ b/src/Servers/HttpSys/src/RequestProcessing/Request.cs @@ -5,6 +5,7 @@ using System; using System.Globalization; using System.IO; using System.Net; +using System.Security.Authentication; using System.Security.Cryptography.X509Certificates; using System.Security.Principal; using System.Threading; @@ -83,6 +84,11 @@ namespace Microsoft.AspNetCore.Server.HttpSys User = _nativeRequestContext.GetUser(); + if (IsHttps) + { + GetTlsHandshakeResults(); + } + // GetTlsTokenBindingInfo(); TODO: https://github.com/aspnet/HttpSysServer/issues/231 // Finished directly accessing the HTTP_REQUEST structure. @@ -232,6 +238,60 @@ namespace Microsoft.AspNetCore.Server.HttpSys internal WindowsPrincipal User { get; } + public SslProtocols Protocol { get; private set; } + + public CipherAlgorithmType CipherAlgorithm { get; private set; } + + public int CipherStrength { get; private set; } + + public HashAlgorithmType HashAlgorithm { get; private set; } + + public int HashStrength { get; private set; } + + public ExchangeAlgorithmType KeyExchangeAlgorithm { get; private set; } + + public int KeyExchangeStrength { get; private set; } + + private void GetTlsHandshakeResults() + { + var handshake = _nativeRequestContext.GetTlsHandshake(); + + Protocol = handshake.Protocol; + // The OS considers client and server TLS as different enum values. SslProtocols choose to combine those for some reason. + // We need to fill in the client bits so the enum shows the expected protocol. + // https://docs.microsoft.com/en-us/windows/desktop/api/schannel/ns-schannel-_secpkgcontext_connectioninfo + // Compare to https://referencesource.microsoft.com/#System/net/System/Net/SecureProtocols/_SslState.cs,8905d1bf17729de3 +#pragma warning disable CS0618 // Type or member is obsolete + if ((Protocol & SslProtocols.Ssl2) != 0) + { + Protocol |= SslProtocols.Ssl2; + } + if ((Protocol & SslProtocols.Ssl3) != 0) + { + Protocol |= SslProtocols.Ssl3; + } +#pragma warning restore CS0618 // Type or member is obsolete + if ((Protocol & SslProtocols.Tls) != 0) + { + Protocol |= SslProtocols.Tls; + } + if ((Protocol & SslProtocols.Tls11) != 0) + { + Protocol |= SslProtocols.Tls11; + } + if ((Protocol & SslProtocols.Tls12) != 0) + { + Protocol |= SslProtocols.Tls12; + } + + CipherAlgorithm = handshake.CipherType; + CipherStrength = (int)handshake.CipherStrength; + HashAlgorithm = handshake.HashType; + HashStrength = (int)handshake.HashStrength; + KeyExchangeAlgorithm = handshake.KeyExchangeType; + KeyExchangeStrength = (int)handshake.KeyExchangeStrength; + } + // Populates the client certificate. The result may be null if there is no client cert. // TODO: Does it make sense for this to be invoked multiple times (e.g. renegotiate)? Client and server code appear to // enable this, but it's unclear what Http.Sys would do. diff --git a/src/Servers/HttpSys/src/StandardFeatureCollection.cs b/src/Servers/HttpSys/src/StandardFeatureCollection.cs index 2adf51d1fc..69158b62d1 100644 --- a/src/Servers/HttpSys/src/StandardFeatureCollection.cs +++ b/src/Servers/HttpSys/src/StandardFeatureCollection.cs @@ -4,6 +4,7 @@ using System; using System.Collections; using System.Collections.Generic; +using Microsoft.AspNetCore.Connections.Features; using Microsoft.AspNetCore.Http.Features; using Microsoft.AspNetCore.Http.Features.Authentication; @@ -19,6 +20,7 @@ namespace Microsoft.AspNetCore.Server.HttpSys { typeof(IHttpResponseFeature), _identityFunc }, { typeof(IHttpSendFileFeature), _identityFunc }, { typeof(ITlsConnectionFeature), ctx => ctx.GetTlsConnectionFeature() }, + { typeof(ITlsHandshakeFeature), ctx => ctx.GetTlsHandshakeFeature() }, // { typeof(ITlsTokenBindingFeature), ctx => ctx.GetTlsTokenBindingFeature() }, TODO: https://github.com/aspnet/HttpSysServer/issues/231 { typeof(IHttpBufferingFeature), _identityFunc }, { typeof(IHttpRequestLifetimeFeature), _identityFunc }, diff --git a/src/Servers/HttpSys/test/FunctionalTests/HttpsTests.cs b/src/Servers/HttpSys/test/FunctionalTests/HttpsTests.cs index 47b02a3092..e0a59cbf9b 100644 --- a/src/Servers/HttpSys/test/FunctionalTests/HttpsTests.cs +++ b/src/Servers/HttpSys/test/FunctionalTests/HttpsTests.cs @@ -1,12 +1,16 @@ // Copyright (c) .NET Foundation. All rights reserved. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. +using System; using System.IO; using System.Net.Http; +using System.Security.Authentication; 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.Testing.xunit; using Xunit; @@ -15,40 +19,38 @@ namespace Microsoft.AspNetCore.Server.HttpSys { public class HttpsTests { - private const string Address = "https://localhost:9090/"; - - [ConditionalFact(Skip = "TODO: Add trait filtering support so these SSL tests don't get run on teamcity or the command line."), Trait("scheme", "https")] + [ConditionalFact] public async Task Https_200OK_Success() { - using (Utilities.CreateHttpsServer(httpContext => + using (Utilities.CreateDynamicHttpsServer(out var address, httpContext => { return Task.FromResult(0); })) { - string response = await SendRequestAsync(Address); + string response = await SendRequestAsync(address); Assert.Equal(string.Empty, response); } } - [ConditionalFact(Skip = "TODO: Add trait filtering support so these SSL tests don't get run on teamcity or the command line."), Trait("scheme", "https")] + [ConditionalFact] public async Task Https_SendHelloWorld_Success() { - using (Utilities.CreateHttpsServer(httpContext => + using (Utilities.CreateDynamicHttpsServer(out var address, httpContext => { byte[] body = Encoding.UTF8.GetBytes("Hello World"); httpContext.Response.ContentLength = body.Length; return httpContext.Response.Body.WriteAsync(body, 0, body.Length); })) { - string response = await SendRequestAsync(Address); + string response = await SendRequestAsync(address); Assert.Equal("Hello World", response); } } - [ConditionalFact(Skip = "TODO: Add trait filtering support so these SSL tests don't get run on teamcity or the command line."), Trait("scheme", "https")] + [ConditionalFact] public async Task Https_EchoHelloWorld_Success() { - using (Utilities.CreateHttpsServer(httpContext => + using (Utilities.CreateDynamicHttpsServer(out var address, httpContext => { string input = new StreamReader(httpContext.Request.Body).ReadToEnd(); Assert.Equal("Hello World", input); @@ -58,15 +60,15 @@ namespace Microsoft.AspNetCore.Server.HttpSys return Task.FromResult(0); })) { - string response = await SendRequestAsync(Address, "Hello World"); + string response = await SendRequestAsync(address, "Hello World"); Assert.Equal("Hello World", response); } } - [ConditionalFact(Skip = "TODO: Add trait filtering support so these SSL tests don't get run on teamcity or the command line."), Trait("scheme", "https")] + [ConditionalFact] public async Task Https_ClientCertNotSent_ClientCertNotPresent() { - using (Utilities.CreateHttpsServer(async httpContext => + using (Utilities.CreateDynamicHttpsServer(out var address, async httpContext => { var tls = httpContext.Features.Get(); Assert.NotNull(tls); @@ -75,15 +77,15 @@ namespace Microsoft.AspNetCore.Server.HttpSys Assert.Null(tls.ClientCertificate); })) { - string response = await SendRequestAsync(Address); + string response = await SendRequestAsync(address); Assert.Equal(string.Empty, response); } } - [ConditionalFact(Skip = "TODO: Add trait filtering support so these SSL tests don't get run on teamcity or the command line."), Trait("scheme", "https")] + [ConditionalFact(Skip = "Manual test only, client certs are not always available.")] public async Task Https_ClientCertRequested_ClientCertPresent() { - using (Utilities.CreateHttpsServer(async httpContext => + using (Utilities.CreateDynamicHttpsServer(out var address, async httpContext => { var tls = httpContext.Features.Get(); Assert.NotNull(tls); @@ -94,7 +96,37 @@ namespace Microsoft.AspNetCore.Server.HttpSys { X509Certificate2 cert = FindClientCert(); Assert.NotNull(cert); - string response = await SendRequestAsync(Address, cert); + string response = await SendRequestAsync(address, cert); + Assert.Equal(string.Empty, response); + } + } + + [ConditionalFact] + public async Task Https_SetsITlsHandshakeFeature() + { + using (Utilities.CreateDynamicHttpsServer(out var address, httpContext => + { + try + { + var tlsFeature = httpContext.Features.Get(); + Assert.NotNull(tlsFeature); + Assert.True(tlsFeature.Protocol > SslProtocols.None, "Protocol"); + Assert.True(Enum.IsDefined(typeof(SslProtocols), tlsFeature.Protocol), "Defined"); // Mapping is required, make sure it's current + Assert.True(tlsFeature.CipherAlgorithm > CipherAlgorithmType.Null, "Cipher"); + Assert.True(tlsFeature.CipherStrength > 0, "CipherStrength"); + Assert.True(tlsFeature.HashAlgorithm > HashAlgorithmType.None, "HashAlgorithm"); + 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"); + } + catch (Exception ex) + { + return httpContext.Response.WriteAsync(ex.ToString()); + } + return Task.FromResult(0); + })) + { + string response = await SendRequestAsync(address); Assert.Equal(string.Empty, response); } } diff --git a/src/Servers/HttpSys/test/FunctionalTests/Microsoft.AspNetCore.Server.HttpSys.FunctionalTests.csproj b/src/Servers/HttpSys/test/FunctionalTests/Microsoft.AspNetCore.Server.HttpSys.FunctionalTests.csproj index 1216b48683..fd830d6041 100644 --- a/src/Servers/HttpSys/test/FunctionalTests/Microsoft.AspNetCore.Server.HttpSys.FunctionalTests.csproj +++ b/src/Servers/HttpSys/test/FunctionalTests/Microsoft.AspNetCore.Server.HttpSys.FunctionalTests.csproj @@ -10,4 +10,9 @@ + + + 214124cd-d05b-4309-9af9-9caa44b2b74a + + diff --git a/src/Servers/HttpSys/test/FunctionalTests/Utilities.cs b/src/Servers/HttpSys/test/FunctionalTests/Utilities.cs index bd38eafd81..24869a8b80 100644 --- a/src/Servers/HttpSys/test/FunctionalTests/Utilities.cs +++ b/src/Servers/HttpSys/test/FunctionalTests/Utilities.cs @@ -23,7 +23,10 @@ namespace Microsoft.AspNetCore.Server.HttpSys // ports during dynamic port allocation. private const int BasePort = 5001; private const int MaxPort = 8000; + private const int BaseHttpsPort = 44300; + private const int MaxHttpsPort = 44399; private static int NextPort = BasePort; + private static int NextHttpsPort = BaseHttpsPort; private static object PortLock = new object(); internal static readonly TimeSpan DefaultTimeout = TimeSpan.FromSeconds(15); internal static readonly int WriteRetryLimit = 1000; @@ -148,17 +151,37 @@ namespace Microsoft.AspNetCore.Server.HttpSys throw new Exception("Failed to locate a free port."); } - internal static IServer CreateHttpsServer(RequestDelegate app) + internal static IServer CreateDynamicHttpsServer(out string baseAddress, RequestDelegate app) { - return CreateServer("https", "localhost", 9090, string.Empty, app); + return CreateDynamicHttpsServer("/", out var root, out baseAddress, options => { }, app); } - internal static IServer CreateServer(string scheme, string host, int port, string path, RequestDelegate app) + internal static IServer CreateDynamicHttpsServer(string basePath, out string root, out string baseAddress, Action configureOptions, RequestDelegate app) { - var server = CreatePump(); - server.Features.Get().Addresses.Add(UrlPrefix.Create(scheme, host, port, path).ToString()); - server.StartAsync(new DummyApplication(app), CancellationToken.None).Wait(); - return server; + lock (PortLock) + { + while (NextHttpsPort < MaxHttpsPort) + { + var port = NextHttpsPort++; + var prefix = UrlPrefix.Create("https", "localhost", port, basePath); + root = prefix.Scheme + "://" + prefix.Host + ":" + prefix.Port; + baseAddress = prefix.ToString(); + + var server = CreatePump(); + server.Features.Get().Addresses.Add(baseAddress); + configureOptions(server.Listener.Options); + try + { + server.StartAsync(new DummyApplication(app), CancellationToken.None).Wait(); + return server; + } + catch (HttpSysException) + { + } + } + NextHttpsPort = BaseHttpsPort; + } + throw new Exception("Failed to locate a free port."); } internal static Task WithTimeout(this Task task) => task.TimeoutAfter(DefaultTimeout); diff --git a/src/Shared/HttpSys/NativeInterop/HttpApiTypes.cs b/src/Shared/HttpSys/NativeInterop/HttpApiTypes.cs index f05d089b9f..324426c60d 100644 --- a/src/Shared/HttpSys/NativeInterop/HttpApiTypes.cs +++ b/src/Shared/HttpSys/NativeInterop/HttpApiTypes.cs @@ -1,9 +1,11 @@ -// Copyright (c) .NET Foundation. All rights reserved. +// Copyright (c) .NET Foundation. All rights reserved. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System; using System.Collections.Generic; using System.Runtime.InteropServices; +using System.Security.Authentication; + namespace Microsoft.AspNetCore.HttpSys.Internal { internal static unsafe class HttpApiTypes @@ -426,12 +428,24 @@ namespace Microsoft.AspNetCore.HttpSys.Internal internal char* pMutualAuthData; } + [StructLayout(LayoutKind.Sequential)] + internal struct HTTP_SSL_PROTOCOL_INFO + { + internal SslProtocols Protocol; + internal CipherAlgorithmType CipherType; + internal uint CipherStrength; + internal HashAlgorithmType HashType; + internal uint HashStrength; + internal ExchangeAlgorithmType KeyExchangeType; + internal uint KeyExchangeStrength; + } + [StructLayout(LayoutKind.Sequential)] internal struct HTTP_REQUEST_INFO { internal HTTP_REQUEST_INFO_TYPE InfoType; internal uint InfoLength; - internal HTTP_REQUEST_AUTH_INFO* pInfo; + internal void* pInfo; } [Flags] diff --git a/src/Shared/HttpSys/RequestProcessing/NativeRequestContext.cs b/src/Shared/HttpSys/RequestProcessing/NativeRequestContext.cs index ef15d80394..b19bb1b4a6 100644 --- a/src/Shared/HttpSys/RequestProcessing/NativeRequestContext.cs +++ b/src/Shared/HttpSys/RequestProcessing/NativeRequestContext.cs @@ -184,10 +184,13 @@ namespace Microsoft.AspNetCore.HttpSys.Internal { var info = &requestInfo[i]; if (info != null - && info->InfoType == HttpApiTypes.HTTP_REQUEST_INFO_TYPE.HttpRequestInfoTypeAuth - && info->pInfo->AuthStatus == HttpApiTypes.HTTP_AUTH_STATUS.HttpAuthStatusSuccess) + && info->InfoType == HttpApiTypes.HTTP_REQUEST_INFO_TYPE.HttpRequestInfoTypeAuth) { - return true; + var authInfo = (HttpApiTypes.HTTP_REQUEST_AUTH_INFO*)info->pInfo; + if (authInfo->AuthStatus == HttpApiTypes.HTTP_AUTH_STATUS.HttpAuthStatusSuccess) + { + return true; + } } } return false; @@ -202,22 +205,44 @@ namespace Microsoft.AspNetCore.HttpSys.Internal { var info = &requestInfo[i]; if (info != null - && info->InfoType == HttpApiTypes.HTTP_REQUEST_INFO_TYPE.HttpRequestInfoTypeAuth - && info->pInfo->AuthStatus == HttpApiTypes.HTTP_AUTH_STATUS.HttpAuthStatusSuccess) + && info->InfoType == HttpApiTypes.HTTP_REQUEST_INFO_TYPE.HttpRequestInfoTypeAuth) { - // Duplicates AccessToken - var identity = new WindowsIdentity(info->pInfo->AccessToken, GetAuthTypeFromRequest(info->pInfo->AuthType)); + var authInfo = (HttpApiTypes.HTTP_REQUEST_AUTH_INFO*)info->pInfo; + if (authInfo->AuthStatus == HttpApiTypes.HTTP_AUTH_STATUS.HttpAuthStatusSuccess) + { + // Duplicates AccessToken + var identity = new WindowsIdentity(authInfo->AccessToken, GetAuthTypeFromRequest(authInfo->AuthType)); - // Close the original - UnsafeNclNativeMethods.SafeNetHandles.CloseHandle(info->pInfo->AccessToken); + // Close the original + UnsafeNclNativeMethods.SafeNetHandles.CloseHandle(authInfo->AccessToken); - return new WindowsPrincipal(identity); + return new WindowsPrincipal(identity); + } } } return new WindowsPrincipal(WindowsIdentity.GetAnonymous()); // Anonymous / !IsAuthenticated } + internal HttpApiTypes.HTTP_SSL_PROTOCOL_INFO GetTlsHandshake() + { + var requestInfo = NativeRequestV2->pRequestInfo; + var infoCount = NativeRequestV2->RequestInfoCount; + + for (int i = 0; i < infoCount; i++) + { + var info = &requestInfo[i]; + if (info != null + && info->InfoType == HttpApiTypes.HTTP_REQUEST_INFO_TYPE.HttpRequestInfoTypeSslProtocol) + { + var authInfo = (HttpApiTypes.HTTP_SSL_PROTOCOL_INFO*)info->pInfo; + return *authInfo; + } + } + + return default; + } + private static string GetAuthTypeFromRequest(HttpApiTypes.HTTP_REQUEST_AUTH_TYPE input) { switch (input)