-
Notifications
You must be signed in to change notification settings - Fork 10.4k
feat: fetch TLS client hello message from HTTP.SYS #60806
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
Changes from 15 commits
d7953f6
aa3ae32
840cdac
c9431ec
1d0f0d8
3c4d6fc
0c31882
5d7d187
8cf4cea
6f5cfaf
7566aec
946440a
dc8ae7e
0ad12ca
e9c7ead
e170454
b830fca
e1bdba4
033e211
23bfa1b
c69797f
886de4f
3fe7871
8dc51f8
54a9c75
9eeaee3
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
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 | ||
captainsafia marked this conversation as resolved.
Show resolved
Hide resolved
|
||
{ | ||
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; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Doc comments here as in the class below? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Eh, these are all documented in the http.sys docs, e.g. https://learn.microsoft.com/en-us/windows/win32/api/http/ns-http-http_service_config_ssl_param There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can we point to that somewhere? Not sure how frequently users interact with this type but having some sort of reference even for us as code maintainers might be helpful. |
||
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 | ||
} |
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(); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What is this? There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 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 | ||
} | ||
} | ||
} |
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> |
Uh oh!
There was an error while loading. Please reload this page.