Browse Source

Implement ITlsHandshakeFeature for HttpSys (#7284)

Chris Ross 7 years ago
parent
commit
06d7fe73a9

+ 23 - 0
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)

+ 0 - 0
src/Servers/HttpSys/src/Http503VerbosityLevel .cs → src/Servers/HttpSys/src/Http503VerbosityLevel.cs


+ 1 - 0
src/Servers/HttpSys/src/Microsoft.AspNetCore.Server.HttpSys.csproj

@@ -16,6 +16,7 @@
 
   <ItemGroup>
     <Reference Include="Microsoft.AspNetCore.Authentication.Core" />
+    <Reference Include="Microsoft.AspNetCore.Connections.Abstractions" />
     <Reference Include="Microsoft.AspNetCore.Hosting" />
     <Reference Include="Microsoft.Net.Http.Headers" />
     <Reference Include="Microsoft.Win32.Registry" />

+ 60 - 0
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.

+ 2 - 0
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 },

+ 49 - 17
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<ITlsConnectionFeature>();
                 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<ITlsConnectionFeature>();
                 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<ITlsHandshakeFeature>();
+                    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);
             }
         }

+ 5 - 0
src/Servers/HttpSys/test/FunctionalTests/Microsoft.AspNetCore.Server.HttpSys.FunctionalTests.csproj

@@ -10,4 +10,9 @@
     <Reference Include="System.Net.Http.WinHttpHandler" />
   </ItemGroup>
 
+  <PropertyGroup>
+    <!--Imitate IIS Express so we can use it's cert bindings-->
+    <PackageTags>214124cd-d05b-4309-9af9-9caa44b2b74a</PackageTags>
+  </PropertyGroup>
+
 </Project>

+ 30 - 7
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<HttpSysOptions> configureOptions, RequestDelegate app)
         {
-            var server = CreatePump();
-            server.Features.Get<IServerAddressesFeature>().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<IServerAddressesFeature>().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);

+ 16 - 2
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]

+ 35 - 10
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)