Implement ITlsHandshakeFeature for HttpSys (#7284)

This commit is contained in:
Chris Ross 2019-02-13 08:39:45 -08:00 committed by GitHub
parent 3fd8a97af2
commit 06d7fe73a9
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 221 additions and 36 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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