Skip to content

Implement ITlsHandshakeFeature in IIS #48957

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 5 commits into from
Jun 26, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 0 additions & 34 deletions src/Servers/HttpSys/src/RequestProcessing/Request.cs
Original file line number Diff line number Diff line change
Expand Up @@ -343,41 +343,7 @@ private AspNetCore.HttpSys.Internal.SocketAddress LocalEndPoint
private void GetTlsHandshakeResults()
{
var handshake = RequestContext.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://learn.microsoft.com/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 Prmember is obsolete
#pragma warning disable SYSLIB0039 // TLS 1.0 and 1.1 are obsolete
if ((Protocol & SslProtocols.Tls) != 0)
{
Protocol |= SslProtocols.Tls;
}
if ((Protocol & SslProtocols.Tls11) != 0)
{
Protocol |= SslProtocols.Tls11;
}
#pragma warning restore SYSLIB0039
if ((Protocol & SslProtocols.Tls12) != 0)
{
Protocol |= SslProtocols.Tls12;
}
if ((Protocol & SslProtocols.Tls13) != 0)
{
Protocol |= SslProtocols.Tls13;
}

CipherAlgorithm = handshake.CipherType;
CipherStrength = (int)handshake.CipherStrength;
HashAlgorithm = handshake.HashType;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -611,4 +611,35 @@ http_response_set_need_goaway(
pHttpResponse->SetNeedGoAway();
return 0;
}

EXTERN_C __declspec(dllexport)
HRESULT
http_query_request_property(
_In_ HTTP_OPAQUE_ID requestId,
_In_ HTTP_REQUEST_PROPERTY propertyId,
_In_reads_bytes_opt_(qualifierSize) PVOID pQualifier,
_In_ ULONG qualifierSize,
_Out_writes_bytes_to_opt_(outputBufferSize, *pcbBytesReturned) PVOID pOutput,
_In_ ULONG outputBufferSize,
_Out_opt_ PULONG pcbBytesReturned,
_In_ LPOVERLAPPED pOverlapped
)
{
IHttpServer3* httpServer3;
HRESULT hr = HttpGetExtendedInterface<IHttpServer, IHttpServer3>(g_pHttpServer, g_pHttpServer, &httpServer3);
if (FAILED(hr))
{
return hr;
}

return httpServer3->QueryRequestProperty(
requestId,
propertyId,
pQualifier,
qualifierSize,
pOutput,
outputBufferSize,
pcbBytesReturned,
pOverlapped);
}
// End of export
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,9 @@
using System.Collections;
using System.Diagnostics;
using System.IO.Pipelines;
using System.Net.Security;
using System.Runtime.InteropServices;
using System.Security.Authentication;
using System.Security.Claims;
using System.Security.Cryptography.X509Certificates;
using Microsoft.AspNetCore.Connections.Features;
Expand All @@ -28,6 +30,7 @@ internal partial class IISHttpContext : IFeatureCollection,
IHttpAuthenticationFeature,
IServerVariablesFeature,
ITlsConnectionFeature,
ITlsHandshakeFeature,
IHttpBodyControlFeature,
IHttpMaxRequestBodySizeFeature,
IHttpResponseTrailersFeature,
Expand Down Expand Up @@ -406,6 +409,24 @@ unsafe X509Certificate2? ITlsConnectionFeature.ClientCertificate
}
}

SslProtocols ITlsHandshakeFeature.Protocol => Protocol;

TlsCipherSuite? ITlsHandshakeFeature.NegotiatedCipherSuite => NegotiatedCipherSuite;

string ITlsHandshakeFeature.HostName => SniHostName;

CipherAlgorithmType ITlsHandshakeFeature.CipherAlgorithm => CipherAlgorithm;

int ITlsHandshakeFeature.CipherStrength => CipherStrength;

HashAlgorithmType ITlsHandshakeFeature.HashAlgorithm => HashAlgorithm;

int ITlsHandshakeFeature.HashStrength => HashStrength;

ExchangeAlgorithmType ITlsHandshakeFeature.KeyExchangeAlgorithm => KeyExchangeAlgorithm;

int ITlsHandshakeFeature.KeyExchangeStrength => KeyExchangeStrength;

IEnumerator<KeyValuePair<Type, object>> IEnumerable<KeyValuePair<Type, object>>.GetEnumerator() => FastEnumerable().GetEnumerator();

IEnumerator IEnumerable.GetEnumerator() => FastEnumerable().GetEnumerator();
Expand Down Expand Up @@ -446,6 +467,11 @@ unsafe X509Certificate2? ITlsConnectionFeature.ClientCertificate
return AdvancedHttp2FeaturesSupported() ? this : null;
}

internal ITlsHandshakeFeature? GetTlsHandshakeFeature()
{
return IsHttps ? this : null;
}

IHeaderDictionary IHttpResponseTrailersFeature.Trailers
{
get => ResponseTrailers ??= HttpResponseTrailers;
Expand Down
16 changes: 16 additions & 0 deletions src/Servers/IIS/IIS/src/Core/IISHttpContext.Features.cs
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ internal partial class IISHttpContext
private static readonly Type IResponseCookiesFeatureType = typeof(global::Microsoft.AspNetCore.Http.Features.IResponseCookiesFeature);
private static readonly Type IItemsFeatureType = typeof(global::Microsoft.AspNetCore.Http.Features.IItemsFeature);
private static readonly Type ITlsConnectionFeatureType = typeof(global::Microsoft.AspNetCore.Http.Features.ITlsConnectionFeature);
private static readonly Type ITlsHandshakeFeatureType = typeof(global::Microsoft.AspNetCore.Connections.Features.ITlsHandshakeFeature);
private static readonly Type IHttpWebSocketFeatureType = typeof(global::Microsoft.AspNetCore.Http.Features.IHttpWebSocketFeature);
private static readonly Type ISessionFeatureType = typeof(global::Microsoft.AspNetCore.Http.Features.ISessionFeature);
private static readonly Type IHttpBodyControlFeatureType = typeof(global::Microsoft.AspNetCore.Http.Features.IHttpBodyControlFeature);
Expand Down Expand Up @@ -48,6 +49,7 @@ internal partial class IISHttpContext
private object? _currentIResponseCookiesFeature;
private object? _currentIItemsFeature;
private object? _currentITlsConnectionFeature;
private object? _currentITlsHandshakeFeature;
private object? _currentIHttpWebSocketFeature;
private object? _currentISessionFeature;
private object? _currentIHttpBodyControlFeature;
Expand Down Expand Up @@ -75,6 +77,7 @@ private void Initialize()
_currentIServerVariablesFeature = this;
_currentIHttpMaxRequestBodySizeFeature = this;
_currentITlsConnectionFeature = this;
_currentITlsHandshakeFeature = GetTlsHandshakeFeature();
_currentIHttpResponseTrailersFeature = GetResponseTrailersFeature();
_currentIHttpResetFeature = GetResetFeature();
_currentIConnectionLifetimeNotificationFeature = this;
Expand Down Expand Up @@ -146,6 +149,10 @@ private void Initialize()
{
return _currentITlsConnectionFeature;
}
if (key == ITlsHandshakeFeatureType)
{
return _currentITlsHandshakeFeature;
}
if (key == IHttpWebSocketFeatureType)
{
return _currentIHttpWebSocketFeature;
Expand Down Expand Up @@ -277,6 +284,11 @@ internal void FastFeatureSet(Type key, object? feature)
_currentITlsConnectionFeature = feature;
return;
}
if (key == ITlsHandshakeFeatureType)
{
_currentITlsHandshakeFeature = feature;
return;
}
if (key == IHttpWebSocketFeatureType)
{
_currentIHttpWebSocketFeature = feature;
Expand Down Expand Up @@ -400,6 +412,10 @@ private IEnumerable<KeyValuePair<Type, object>> FastEnumerable()
{
yield return new KeyValuePair<Type, object>(ITlsConnectionFeatureType, _currentITlsConnectionFeature);
}
if (_currentITlsHandshakeFeature != null)
{
yield return new KeyValuePair<Type, object>(ITlsHandshakeFeatureType, _currentITlsHandshakeFeature);
}
if (_currentIHttpWebSocketFeature != null)
{
yield return new KeyValuePair<Type, object>(IHttpWebSocketFeatureType, _currentIHttpWebSocketFeature);
Expand Down
55 changes: 54 additions & 1 deletion src/Servers/IIS/IIS/src/Core/IISHttpContext.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,9 @@
using System.Diagnostics.CodeAnalysis;
using System.IO.Pipelines;
using System.Net;
using System.Net.Security;
using System.Runtime.InteropServices;
using System.Security.Authentication;
using System.Security.Claims;
using System.Security.Principal;
using System.Text;
Expand Down Expand Up @@ -88,6 +90,7 @@ internal unsafe IISHttpContext(

private int PauseWriterThreshold => _options.MaxRequestBodyBufferSize;
private int ResumeWriterTheshold => PauseWriterThreshold / 2;
private bool IsHttps => SslStatus != SslStatus.Insecure;

public Version HttpVersion { get; set; } = default!;
public string Scheme { get; set; } = default!;
Expand All @@ -111,6 +114,16 @@ internal unsafe IISHttpContext(
public Stream ResponseBody { get; set; } = default!;
public PipeWriter? ResponsePipeWrapper { get; set; }

public SslProtocols Protocol { get; private set; }
public TlsCipherSuite? NegotiatedCipherSuite { get; private set; }
public string SniHostName { get; private set; } = default!;
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; }

protected IAsyncIOEngine? AsyncIO { get; set; }

public IHeaderDictionary RequestHeaders { get; set; } = default!;
Expand Down Expand Up @@ -139,7 +152,7 @@ protected void InitializeContext()
RawTarget = GetRawUrl() ?? string.Empty;
// TODO version is slow.
HttpVersion = GetVersion();
Scheme = SslStatus != SslStatus.Insecure ? Constants.HttpsScheme : Constants.HttpScheme;
Scheme = IsHttps ? Constants.HttpsScheme : Constants.HttpScheme;
KnownMethod = VerbId;
StatusCode = 200;

Expand Down Expand Up @@ -253,6 +266,12 @@ protected void InitializeContext()
// Request headers can be modified by the app, read these first.
RequestCanHaveBody = CheckRequestCanHaveBody();

SniHostName = string.Empty;
if (IsHttps)
{
GetTlsHandshakeResults();
}

if (_options.ForwardWindowsAuthentication)
{
WindowsUser = GetWindowsPrincipal();
Expand Down Expand Up @@ -371,6 +390,40 @@ private bool CheckRequestCanHaveBody()
return RequestHeaders.ContentLength.GetValueOrDefault() > 0;
}

private void GetTlsHandshakeResults()
{
var handshake = this.GetTlsHandshake();
Protocol = handshake.Protocol;
CipherAlgorithm = handshake.CipherType;
CipherStrength = (int)handshake.CipherStrength;
HashAlgorithm = handshake.HashType;
HashStrength = (int)handshake.HashStrength;
KeyExchangeAlgorithm = handshake.KeyExchangeType;
KeyExchangeStrength = (int)handshake.KeyExchangeStrength;

var sni = GetClientSni();
SniHostName = sni.Hostname;
}

private unsafe HttpApiTypes.HTTP_REQUEST_PROPERTY_SNI GetClientSni()
{
var buffer = new byte[HttpApiTypes.SniPropertySizeInBytes];
fixed (byte* pBuffer = buffer)
{
var statusCode = NativeMethods.HttpQueryRequestProperty(
RequestId,
HttpApiTypes.HTTP_REQUEST_PROPERTY.HttpRequestPropertySni,
qualifier: null,
qualifierSize: 0,
(void*)pBuffer,
(uint)buffer.Length,
bytesReturned: null,
IntPtr.Zero);

return statusCode == NativeMethods.HR_OK ? Marshal.PtrToStructure<HttpApiTypes.HTTP_REQUEST_PROPERTY_SNI>((IntPtr)pBuffer) : default;
}
}

private async Task InitializeResponse(bool flushHeaders)
{
await FireOnStarting();
Expand Down
16 changes: 16 additions & 0 deletions src/Servers/IIS/IIS/src/NativeMethods.cs
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,17 @@ private static unsafe partial int register_callbacks(NativeSafeHandle pInProcess
[LibraryImport(AspNetCoreModuleDll)]
private static partial int http_get_application_properties(out IISConfigurationData iiConfigData);

[LibraryImport(AspNetCoreModuleDll)]
private static unsafe partial int http_query_request_property(
ulong requestId,
HttpApiTypes.HTTP_REQUEST_PROPERTY propertyId,
void* qualifier,
uint qualifierSize,
void* output,
uint outputSize,
uint* bytesReturned,
IntPtr overlapped);

[LibraryImport(AspNetCoreModuleDll)]
private static partial int http_get_server_variable(
NativeSafeHandle pInProcessHandler,
Expand Down Expand Up @@ -231,6 +242,11 @@ internal static IISConfigurationData HttpGetApplicationProperties()
return iisConfigurationData;
}

public static unsafe int HttpQueryRequestProperty(ulong requestId, HttpApiTypes.HTTP_REQUEST_PROPERTY propertyId, void* qualifier, uint qualifierSize, void* output, uint outputSize, uint* bytesReturned, IntPtr overlapped)
{
return http_query_request_property(requestId, propertyId, qualifier, qualifierSize, output, outputSize, bytesReturned, overlapped);
}

public static bool HttpTryGetServerVariable(NativeSafeHandle pInProcessHandler, string variableName, out string value)
{
return http_get_server_variable(pInProcessHandler, variableName, out value) == 0;
Expand Down
75 changes: 75 additions & 0 deletions src/Servers/IIS/IIS/test/IIS.Tests/TlsHandshakeFeatureTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System.Security.Authentication;
using Microsoft.AspNetCore.Connections.Features;
using Microsoft.AspNetCore.Testing;
using Xunit;

namespace Microsoft.AspNetCore.Server.IIS.FunctionalTests;

[SkipIfHostableWebCoreNotAvailable]
[MinimumOSVersion(OperatingSystems.Windows, WindowsVersions.Win8, SkipReason = "https://github.com/aspnet/IISIntegration/issues/866")]
[SkipOnHelix("Unsupported queue", Queues = "Windows.Amd64.VS2022.Pre.Open;")]
public class TlsHandshakeFeatureTests : StrictTestServerTests
{
[ConditionalFact]
public async Task SetsTlsHandshakeFeatureForHttps()
{
ITlsHandshakeFeature tlsHandshakeFeature = null;
using (var testServer = await TestServer.CreateHttps(ctx =>
{
tlsHandshakeFeature = ctx.Features.Get<ITlsHandshakeFeature>();
return Task.CompletedTask;
}, LoggerFactory))
{
await testServer.HttpClient.GetStringAsync("/");
}

Assert.NotNull(tlsHandshakeFeature);

var protocol = tlsHandshakeFeature.Protocol;
Assert.True(protocol > SslProtocols.None, "Protocol: " + protocol);
Assert.True(Enum.IsDefined(typeof(SslProtocols), protocol), "Defined: " + protocol); // Mapping is required, make sure it's current

var cipherAlgorithm = tlsHandshakeFeature.CipherAlgorithm;
Assert.True(cipherAlgorithm > CipherAlgorithmType.Null, "Cipher: " + cipherAlgorithm);

var cipherStrength = tlsHandshakeFeature.CipherStrength;
Assert.True(cipherStrength > 0, "CipherStrength: " + cipherStrength);

var hashAlgorithm = tlsHandshakeFeature.HashAlgorithm;
Assert.True(hashAlgorithm >= HashAlgorithmType.None, "HashAlgorithm: " + hashAlgorithm);

var hashStrength = tlsHandshakeFeature.HashStrength;
Assert.True(hashStrength >= 0, "HashStrength: " + hashStrength); // May be 0 for some algorithms

var keyExchangeAlgorithm = tlsHandshakeFeature.KeyExchangeAlgorithm;
Assert.True(keyExchangeAlgorithm >= ExchangeAlgorithmType.None, "KeyExchangeAlgorithm: " + keyExchangeAlgorithm);

var keyExchangeStrength = tlsHandshakeFeature.KeyExchangeStrength;
Assert.True(keyExchangeStrength >= 0, "KeyExchangeStrength: " + keyExchangeStrength);

if (Environment.OSVersion.Version > new Version(10, 0, 19043, 0))
{
var hostName = tlsHandshakeFeature.HostName;
Assert.Equal("localhost", hostName);
}
}

[ConditionalFact]
public async Task DoesNotSetTlsHandshakeFeatureForHttp()
{
ITlsHandshakeFeature tlsHandshakeFeature = null;
using (var testServer = await TestServer.Create(ctx =>
{
tlsHandshakeFeature = ctx.Features.Get<ITlsHandshakeFeature>();
return Task.CompletedTask;
}, LoggerFactory))
{
await testServer.HttpClient.GetStringAsync("/");
}

Assert.Null(tlsHandshakeFeature);
}
}
Loading