Skip to content
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

feat: fetch TLS client hello message from HTTP.SYS #60806

Open
wants to merge 25 commits into
base: main
Choose a base branch
from
Open
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
1 change: 1 addition & 0 deletions AspNetCore.slnx
Original file line number Diff line number Diff line change
Expand Up @@ -868,6 +868,7 @@
<Project Path="src/Servers/HttpSys/samples/QueueSharing/QueueSharing.csproj" />
<Project Path="src/Servers/HttpSys/samples/SelfHostServer/SelfHostServer.csproj" />
<Project Path="src/Servers/HttpSys/samples/TestClient/TestClient.csproj" />
<Project Path="src/Servers/HttpSys/samples/TlsFeaturesObserve/TlsFeaturesObserve.csproj" />
</Folder>
<Folder Name="/src/Servers/HttpSys/test/" Id="c087a640-2922-1045-1264-5231983559f6">
<Project Path="src/Servers/HttpSys/test/FunctionalTests/Microsoft.AspNetCore.Server.HttpSys.FunctionalTests.csproj" />
Expand Down
1 change: 1 addition & 0 deletions src/Servers/HttpSys/HttpSysServer.slnf
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@
"src\\Servers\\HttpSys\\samples\\QueueSharing\\QueueSharing.csproj",
"src\\Servers\\HttpSys\\samples\\SelfHostServer\\SelfHostServer.csproj",
"src\\Servers\\HttpSys\\samples\\TestClient\\TestClient.csproj",
"src\\Servers\\HttpSys\\samples\\TlsFeaturesObserve\\TlsFeaturesObserve.csproj",
"src\\Servers\\HttpSys\\src\\Microsoft.AspNetCore.Server.HttpSys.csproj",
"src\\Servers\\HttpSys\\test\\FunctionalTests\\Microsoft.AspNetCore.Server.HttpSys.FunctionalTests.csproj",
"src\\Servers\\HttpSys\\test\\NonHelixTests\\Microsoft.AspNetCore.Server.HttpSys.NonHelixTests.csproj",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System;
using System.Net;
using System.Net.Sockets;
using System.Runtime.InteropServices;
using System.Text;
using Microsoft.AspNetCore.Http;

namespace TlsFeaturesObserve.HttpSys;

internal static class HttpSysConfigurator
{
const uint HTTP_INITIALIZE_CONFIG = 0x00000002;
const uint ERROR_ALREADY_EXISTS = 183;

static readonly HTTPAPI_VERSION HttpApiVersion = new HTTPAPI_VERSION(1, 0);

internal static void ConfigureCacheTlsClientHello()
{
IPEndPoint ipPort = new IPEndPoint(new IPAddress([0, 0, 0, 0]), 6000);
string certThumbprint = "" /* your cert thumbprint here */;
Guid appId = Guid.NewGuid();
string sslCertStoreName = "My";

CallHttpApi(() => SetConfiguration(ipPort, certThumbprint, appId, sslCertStoreName));
}

static void SetConfiguration(IPEndPoint ipPort, string certThumbprint, Guid appId, string sslCertStoreName)
{
GCHandle sockAddrHandle = CreateSockaddrStructure(ipPort);
var pIpPort = sockAddrHandle.AddrOfPinnedObject();
var httpServiceConfigSslKey = new HTTP_SERVICE_CONFIG_SSL_KEY(pIpPort);

byte[] hash = GetHash(certThumbprint);
var handleHash = GCHandle.Alloc(hash, GCHandleType.Pinned);
var configSslParam = new HTTP_SERVICE_CONFIG_SSL_PARAM
{
AppId = appId,
DefaultFlags = 0x00008000 /* HTTP_SERVICE_CONFIG_SSL_FLAG_ENABLE_CACHE_CLIENT_HELLO */,
DefaultRevocationFreshnessTime = 0,
DefaultRevocationUrlRetrievalTimeout = 15,
pSslCertStoreName = sslCertStoreName,
pSslHash = handleHash.AddrOfPinnedObject(),
SslHashLength = hash.Length,
pDefaultSslCtlIdentifier = null,
pDefaultSslCtlStoreName = sslCertStoreName
};

var configSslSet = new HTTP_SERVICE_CONFIG_SSL_SET
{
ParamDesc = configSslParam,
KeyDesc = httpServiceConfigSslKey
};

var pInputConfigInfo = Marshal.AllocCoTaskMem(
Marshal.SizeOf(typeof(HTTP_SERVICE_CONFIG_SSL_SET)));
Marshal.StructureToPtr(configSslSet, pInputConfigInfo, false);

uint status = HttpSetServiceConfiguration(nint.Zero,
HTTP_SERVICE_CONFIG_ID.HttpServiceConfigSSLCertInfo,
pInputConfigInfo,
Marshal.SizeOf(configSslSet),
nint.Zero);

if (status == ERROR_ALREADY_EXISTS || status == 0) // already present or success
{
Console.WriteLine("HttpServiceConfiguration is correct");
}
else
{
Console.WriteLine("Failed to HttpSetServiceConfiguration: " + status);
}
}

static byte[] GetHash(string thumbprint)
{
int length = thumbprint.Length;
byte[] bytes = new byte[length / 2];
for (int i = 0; i < length; i += 2)
{
bytes[i / 2] = Convert.ToByte(thumbprint.Substring(i, 2), 16);
}

return bytes;
}

static GCHandle CreateSockaddrStructure(IPEndPoint ipEndPoint)
{
SocketAddress socketAddress = ipEndPoint.Serialize();

// use an array of bytes instead of the sockaddr structure
byte[] sockAddrStructureBytes = new byte[socketAddress.Size];
GCHandle sockAddrHandle = GCHandle.Alloc(sockAddrStructureBytes, GCHandleType.Pinned);
for (int i = 0; i < socketAddress.Size; ++i)
{
sockAddrStructureBytes[i] = socketAddress[i];
}
return sockAddrHandle;
}

static void CallHttpApi(Action body)
{
const uint flags = HTTP_INITIALIZE_CONFIG;
uint retVal = HttpInitialize(HttpApiVersion, flags, IntPtr.Zero);
body();
}

// disabled warning since it is just a sample
#pragma warning disable SYSLIB1054 // Use 'LibraryImportAttribute' instead of 'DllImportAttribute' to generate P/Invoke marshalling code at compile time
[DllImport("httpapi.dll", SetLastError = true)]
private static extern uint HttpInitialize(
HTTPAPI_VERSION version,
uint flags,
IntPtr pReserved);

[DllImport("httpapi.dll", SetLastError = true)]
public static extern uint HttpSetServiceConfiguration(
nint serviceIntPtr,
HTTP_SERVICE_CONFIG_ID configId,
nint pConfigInformation,
int configInformationLength,
nint pOverlapped);
#pragma warning restore SYSLIB1054 // Use 'LibraryImportAttribute' instead of 'DllImportAttribute' to generate P/Invoke marshalling code at compile time
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System;
using System.Collections.Generic;
using System.Runtime.InteropServices;
using System.Text;

namespace TlsFeaturesObserve.HttpSys;

[StructLayout(LayoutKind.Sequential, Pack = 2)]
public struct HTTPAPI_VERSION
{
public ushort HttpApiMajorVersion;
public ushort HttpApiMinorVersion;

public HTTPAPI_VERSION(ushort majorVersion, ushort minorVersion)
{
HttpApiMajorVersion = majorVersion;
HttpApiMinorVersion = minorVersion;
}
}

public enum HTTP_SERVICE_CONFIG_ID
{
HttpServiceConfigIPListenList = 0,
HttpServiceConfigSSLCertInfo,
HttpServiceConfigUrlAclInfo,
HttpServiceConfigMax
}

[StructLayout(LayoutKind.Sequential)]
public struct HTTP_SERVICE_CONFIG_SSL_SET
{
public HTTP_SERVICE_CONFIG_SSL_KEY KeyDesc;
public HTTP_SERVICE_CONFIG_SSL_PARAM ParamDesc;
}

[StructLayout(LayoutKind.Sequential)]
public struct HTTP_SERVICE_CONFIG_SSL_KEY
{
public IntPtr pIpPort;

public HTTP_SERVICE_CONFIG_SSL_KEY(IntPtr pIpPort)
{
this.pIpPort = pIpPort;
}
}

[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
public struct HTTP_SERVICE_CONFIG_SSL_PARAM
{
public int SslHashLength;
public IntPtr pSslHash;
public Guid AppId;
[MarshalAs(UnmanagedType.LPWStr)]
public string pSslCertStoreName;
public CertCheckModes DefaultCertCheckMode;
public int DefaultRevocationFreshnessTime;
public int DefaultRevocationUrlRetrievalTimeout;
[MarshalAs(UnmanagedType.LPWStr)]
public string pDefaultSslCtlIdentifier;
[MarshalAs(UnmanagedType.LPWStr)]
public string pDefaultSslCtlStoreName;
public uint DefaultFlags; // HTTP_SERVICE_CONFIG_SSL_FLAG
}

[Flags]
public enum CertCheckModes : uint
{
/// <summary>
/// Enables the client certificate revocation check.
/// </summary>
None = 0,

/// <summary>
/// Client certificate is not to be verified for revocation.
/// </summary>
DoNotVerifyCertificateRevocation = 1,

/// <summary>
/// Only cached certificate is to be used the revocation check.
/// </summary>
VerifyRevocationWithCachedCertificateOnly = 2,

/// <summary>
/// The RevocationFreshnessTime setting is enabled.
/// </summary>
EnableRevocationFreshnessTime = 4,

/// <summary>
/// No usage check is to be performed.
/// </summary>
NoUsageCheck = 0x10000
}
60 changes: 60 additions & 0 deletions src/Servers/HttpSys/samples/TlsFeaturesObserve/Program.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System.Reflection;
using System.Runtime.InteropServices;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http.Features;
using Microsoft.AspNetCore.Server.HttpSys;
using Microsoft.Extensions.Hosting;
using TlsFeatureObserve;
using TlsFeaturesObserve.HttpSys;

HttpSysConfigurator.ConfigureCacheTlsClientHello();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is this?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fetching TLS client hello feature from HTTP.SYS works only if a pre-configuration was done for ssl cert binding and netsh ssl config beforehand. User would need to set service configuration via HttpSetServiceConfiguration with HTTP_SERVICE_CONFIG_SSL_FLAG_ENABLE_CACHE_CLIENT_HELLO flag.

That is and should not be a part of ASP.NET app, but I added that for "sample app" for simplicity of reproduction.

CreateHostBuilder(args).Build().Run();

static IHostBuilder CreateHostBuilder(string[] args) =>
Host.CreateDefaultBuilder(args)
.ConfigureWebHost(webBuilder =>
{
webBuilder.UseStartup<Startup>()
.UseHttpSys(options =>
{
// If you want to use https locally: https://stackoverflow.com/a/51841893
options.UrlPrefixes.Add("https://*:6000"); // HTTPS

options.Authentication.Schemes = AuthenticationSchemes.None;
options.Authentication.AllowAnonymous = true;

options.TlsClientHelloBytesCallback = ProcessTlsClientHello;
});
});

static void ProcessTlsClientHello(IFeatureCollection features, ReadOnlySpan<byte> tlsClientHelloBytes)
{
var httpConnectionFeature = features.Get<IHttpConnectionFeature>();

var myTlsFeature = new MyTlsFeature(
connectionId: httpConnectionFeature.ConnectionId,
tlsClientHelloLength: tlsClientHelloBytes.Length);

features.Set<IMyTlsFeature>(myTlsFeature);
}

public interface IMyTlsFeature
{
string ConnectionId { get; }
int TlsClientHelloLength { get; }
}

public class MyTlsFeature : IMyTlsFeature
{
public string ConnectionId { get; }
public int TlsClientHelloLength { get; }

public MyTlsFeature(string connectionId, int tlsClientHelloLength)
{
ConnectionId = connectionId;
TlsClientHelloLength = tlsClientHelloLength;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"profiles": {
"TlsFeaturesObserve": {
"commandName": "Project",
"launchBrowser": true,
"applicationUrl": "http://localhost:5000",
"nativeDebugging": true
}
}
}
28 changes: 28 additions & 0 deletions src/Servers/HttpSys/samples/TlsFeaturesObserve/Startup.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Connections.Features;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Http.Features;
using Microsoft.AspNetCore.Server.HttpSys;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;

namespace TlsFeatureObserve;

public class Startup
{
public void Configure(IApplicationBuilder app)
{
app.Run(async (HttpContext context) =>
{
context.Response.ContentType = "text/plain";

var tlsFeature = context.Features.Get<IMyTlsFeature>();
await context.Response.WriteAsync("TlsClientHello data: " + $"connectionId={tlsFeature?.ConnectionId}; length={tlsFeature?.TlsClientHelloLength}");
});
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>$(DefaultNetCoreTargetFramework)</TargetFramework>
<OutputType>Exe</OutputType>
<ServerGarbageCollection>true</ServerGarbageCollection>
</PropertyGroup>

<ItemGroup>
<Reference Include="Microsoft.AspNetCore.Server.HttpSys" />
<Reference Include="Microsoft.Extensions.Hosting" />
<Reference Include="Microsoft.Extensions.Logging.Console" />
</ItemGroup>
</Project>
Loading
Loading