Skip to content

Commit ae651c8

Browse files
BrennanConroyDeagleGrossadityamandaleeka
authored
fetch TLS client hello message from HTTP.SYS (#61494)
* setup for tls clinet hello exposure * correctly retry access * last minute changes * fix warnings * hook up tls client hello callback * fix warnings & publish API * minimal * only go via callback if options has callback set; remove unused * PR review * address PR comments x1 * TTL & evict approach * address comments 1 * periodic timer * address comments x3 * TryAdd * make a static field (just in case) * Cache updates * test * whitespace * clear array * appcontext * fb * bp changes * Update src/Servers/HttpSys/src/RequestProcessing/RequestContext.cs Co-authored-by: Aditya Mandaleeka <[email protected]> --------- Co-authored-by: Dmitrii Korolev <[email protected]> Co-authored-by: Aditya Mandaleeka <[email protected]>
1 parent f2880bb commit ae651c8

22 files changed

+845
-39
lines changed

AspNetCore.sln

+19
Original file line numberDiff line numberDiff line change
@@ -1784,6 +1784,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "NotReferencedInWasmCodePack
17841784
EndProject
17851785
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Components.WasmRemoteAuthentication", "src\Components\test\testassets\Components.WasmRemoteAuthentication\Components.WasmRemoteAuthentication.csproj", "{8A021D6D-7935-4AB3-BB47-38D4FF9B0D13}"
17861786
EndProject
1787+
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TlsFeaturesObserve", "src\Servers\HttpSys\samples\TlsFeaturesObserve\TlsFeaturesObserve.csproj", "{98C71EC8-1303-F55D-4032-E6728971770E}"
1788+
EndProject
17871789
Global
17881790
GlobalSection(SolutionConfigurationPlatforms) = preSolution
17891791
Debug|Any CPU = Debug|Any CPU
@@ -10753,6 +10755,22 @@ Global
1075310755
{8A021D6D-7935-4AB3-BB47-38D4FF9B0D13}.Release|x64.Build.0 = Release|Any CPU
1075410756
{8A021D6D-7935-4AB3-BB47-38D4FF9B0D13}.Release|x86.ActiveCfg = Release|Any CPU
1075510757
{8A021D6D-7935-4AB3-BB47-38D4FF9B0D13}.Release|x86.Build.0 = Release|Any CPU
10758+
{98C71EC8-1303-F55D-4032-E6728971770E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
10759+
{98C71EC8-1303-F55D-4032-E6728971770E}.Debug|Any CPU.Build.0 = Debug|Any CPU
10760+
{98C71EC8-1303-F55D-4032-E6728971770E}.Debug|arm64.ActiveCfg = Debug|Any CPU
10761+
{98C71EC8-1303-F55D-4032-E6728971770E}.Debug|arm64.Build.0 = Debug|Any CPU
10762+
{98C71EC8-1303-F55D-4032-E6728971770E}.Debug|x64.ActiveCfg = Debug|Any CPU
10763+
{98C71EC8-1303-F55D-4032-E6728971770E}.Debug|x64.Build.0 = Debug|Any CPU
10764+
{98C71EC8-1303-F55D-4032-E6728971770E}.Debug|x86.ActiveCfg = Debug|Any CPU
10765+
{98C71EC8-1303-F55D-4032-E6728971770E}.Debug|x86.Build.0 = Debug|Any CPU
10766+
{98C71EC8-1303-F55D-4032-E6728971770E}.Release|Any CPU.ActiveCfg = Release|Any CPU
10767+
{98C71EC8-1303-F55D-4032-E6728971770E}.Release|Any CPU.Build.0 = Release|Any CPU
10768+
{98C71EC8-1303-F55D-4032-E6728971770E}.Release|arm64.ActiveCfg = Release|Any CPU
10769+
{98C71EC8-1303-F55D-4032-E6728971770E}.Release|arm64.Build.0 = Release|Any CPU
10770+
{98C71EC8-1303-F55D-4032-E6728971770E}.Release|x64.ActiveCfg = Release|Any CPU
10771+
{98C71EC8-1303-F55D-4032-E6728971770E}.Release|x64.Build.0 = Release|Any CPU
10772+
{98C71EC8-1303-F55D-4032-E6728971770E}.Release|x86.ActiveCfg = Release|Any CPU
10773+
{98C71EC8-1303-F55D-4032-E6728971770E}.Release|x86.Build.0 = Release|Any CPU
1075610774
EndGlobalSection
1075710775
GlobalSection(SolutionProperties) = preSolution
1075810776
HideSolutionNode = FALSE
@@ -11634,6 +11652,7 @@ Global
1163411652
{F232B503-D412-45EE-8B31-EFD46B9FA302} = {AA5ABFBC-177C-421E-B743-005E0FD1248B}
1163511653
{433F91E4-E39D-4EB0-B798-2998B3969A2C} = {6126DCE4-9692-4EE2-B240-C65743572995}
1163611654
{8A021D6D-7935-4AB3-BB47-38D4FF9B0D13} = {6126DCE4-9692-4EE2-B240-C65743572995}
11655+
{98C71EC8-1303-F55D-4032-E6728971770E} = {49016328-4D32-46E4-A4D2-94686ED38EA2}
1163711656
EndGlobalSection
1163811657
GlobalSection(ExtensibilityGlobals) = postSolution
1163911658
SolutionGuid = {3E8720B3-DBDD-498C-B383-2CC32A054E8F}

src/Servers/HttpSys/HttpSysServer.slnf

+2-1
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@
3737
"src\\Servers\\HttpSys\\samples\\QueueSharing\\QueueSharing.csproj",
3838
"src\\Servers\\HttpSys\\samples\\SelfHostServer\\SelfHostServer.csproj",
3939
"src\\Servers\\HttpSys\\samples\\TestClient\\TestClient.csproj",
40+
"src\\Servers\\HttpSys\\samples\\TlsFeaturesObserve\\TlsFeaturesObserve.csproj",
4041
"src\\Servers\\HttpSys\\src\\Microsoft.AspNetCore.Server.HttpSys.csproj",
4142
"src\\Servers\\HttpSys\\test\\FunctionalTests\\Microsoft.AspNetCore.Server.HttpSys.FunctionalTests.csproj",
4243
"src\\Servers\\HttpSys\\test\\NonHelixTests\\Microsoft.AspNetCore.Server.HttpSys.NonHelixTests.csproj",
@@ -53,4 +54,4 @@
5354
"src\\WebEncoders\\src\\Microsoft.Extensions.WebEncoders.csproj"
5455
]
5556
}
56-
}
57+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
using System.Net;
5+
using System.Runtime.InteropServices;
6+
7+
namespace TlsFeaturesObserve.HttpSys;
8+
9+
internal static class HttpSysConfigurator
10+
{
11+
const uint HTTP_INITIALIZE_CONFIG = 0x00000002;
12+
const uint ERROR_ALREADY_EXISTS = 183;
13+
14+
static readonly HTTPAPI_VERSION HttpApiVersion = new HTTPAPI_VERSION(1, 0);
15+
16+
internal static void ConfigureCacheTlsClientHello()
17+
{
18+
// Arbitrarily chosen port, but must match the port used in the web server. Via UrlPrefixes or launchsettings.
19+
var ipPort = new IPEndPoint(new IPAddress([0, 0, 0, 0]), 6000);
20+
var certThumbprint = "" /* your cert thumbprint here */;
21+
var appId = Guid.NewGuid();
22+
var sslCertStoreName = "My";
23+
24+
CallHttpApi(() => SetConfiguration(ipPort, certThumbprint, appId, sslCertStoreName));
25+
}
26+
27+
static void SetConfiguration(IPEndPoint ipPort, string certThumbprint, Guid appId, string sslCertStoreName)
28+
{
29+
var sockAddrHandle = CreateSockaddrStructure(ipPort);
30+
var pIpPort = sockAddrHandle.AddrOfPinnedObject();
31+
var httpServiceConfigSslKey = new HTTP_SERVICE_CONFIG_SSL_KEY(pIpPort);
32+
33+
var hash = GetHash(certThumbprint);
34+
var handleHash = GCHandle.Alloc(hash, GCHandleType.Pinned);
35+
var configSslParam = new HTTP_SERVICE_CONFIG_SSL_PARAM
36+
{
37+
AppId = appId,
38+
DefaultFlags = 0x00008000 /* HTTP_SERVICE_CONFIG_SSL_FLAG_ENABLE_CACHE_CLIENT_HELLO */,
39+
DefaultRevocationFreshnessTime = 0,
40+
DefaultRevocationUrlRetrievalTimeout = 15,
41+
pSslCertStoreName = sslCertStoreName,
42+
pSslHash = handleHash.AddrOfPinnedObject(),
43+
SslHashLength = hash.Length,
44+
pDefaultSslCtlIdentifier = null,
45+
pDefaultSslCtlStoreName = sslCertStoreName
46+
};
47+
48+
var configSslSet = new HTTP_SERVICE_CONFIG_SSL_SET
49+
{
50+
ParamDesc = configSslParam,
51+
KeyDesc = httpServiceConfigSslKey
52+
};
53+
54+
var pInputConfigInfo = Marshal.AllocCoTaskMem(
55+
Marshal.SizeOf(typeof(HTTP_SERVICE_CONFIG_SSL_SET)));
56+
Marshal.StructureToPtr(configSslSet, pInputConfigInfo, false);
57+
58+
var status = HttpSetServiceConfiguration(nint.Zero,
59+
HTTP_SERVICE_CONFIG_ID.HttpServiceConfigSSLCertInfo,
60+
pInputConfigInfo,
61+
Marshal.SizeOf(configSslSet),
62+
nint.Zero);
63+
64+
if (status == ERROR_ALREADY_EXISTS || status == 0) // already present or success
65+
{
66+
Console.WriteLine($"HttpServiceConfiguration is correct");
67+
}
68+
else
69+
{
70+
Console.WriteLine("Failed to HttpSetServiceConfiguration: " + status);
71+
}
72+
}
73+
74+
static byte[] GetHash(string thumbprint)
75+
{
76+
var length = thumbprint.Length;
77+
var bytes = new byte[length / 2];
78+
for (var i = 0; i < length; i += 2)
79+
{
80+
bytes[i / 2] = Convert.ToByte(thumbprint.Substring(i, 2), 16);
81+
}
82+
83+
return bytes;
84+
}
85+
86+
static GCHandle CreateSockaddrStructure(IPEndPoint ipEndPoint)
87+
{
88+
var socketAddress = ipEndPoint.Serialize();
89+
90+
// use an array of bytes instead of the sockaddr structure
91+
var sockAddrStructureBytes = new byte[socketAddress.Size];
92+
var sockAddrHandle = GCHandle.Alloc(sockAddrStructureBytes, GCHandleType.Pinned);
93+
for (var i = 0; i < socketAddress.Size; ++i)
94+
{
95+
sockAddrStructureBytes[i] = socketAddress[i];
96+
}
97+
return sockAddrHandle;
98+
}
99+
100+
static void CallHttpApi(Action body)
101+
{
102+
const uint flags = HTTP_INITIALIZE_CONFIG;
103+
var retVal = HttpInitialize(HttpApiVersion, flags, IntPtr.Zero);
104+
body();
105+
}
106+
107+
// disabled warning since it is just a sample
108+
#pragma warning disable SYSLIB1054 // Use 'LibraryImportAttribute' instead of 'DllImportAttribute' to generate P/Invoke marshalling code at compile time
109+
[DllImport("httpapi.dll", SetLastError = true)]
110+
private static extern uint HttpInitialize(
111+
HTTPAPI_VERSION version,
112+
uint flags,
113+
IntPtr pReserved);
114+
115+
[DllImport("httpapi.dll", SetLastError = true)]
116+
public static extern uint HttpSetServiceConfiguration(
117+
nint serviceIntPtr,
118+
HTTP_SERVICE_CONFIG_ID configId,
119+
nint pConfigInformation,
120+
int configInformationLength,
121+
nint pOverlapped);
122+
#pragma warning restore SYSLIB1054 // Use 'LibraryImportAttribute' instead of 'DllImportAttribute' to generate P/Invoke marshalling code at compile time
123+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
using System;
5+
using System.Collections.Generic;
6+
using System.Runtime.InteropServices;
7+
using System.Text;
8+
9+
namespace TlsFeaturesObserve.HttpSys;
10+
11+
// Http.Sys types from https://learn.microsoft.com/windows/win32/api/http/
12+
13+
[StructLayout(LayoutKind.Sequential, Pack = 2)]
14+
public struct HTTPAPI_VERSION
15+
{
16+
public ushort HttpApiMajorVersion;
17+
public ushort HttpApiMinorVersion;
18+
19+
public HTTPAPI_VERSION(ushort majorVersion, ushort minorVersion)
20+
{
21+
HttpApiMajorVersion = majorVersion;
22+
HttpApiMinorVersion = minorVersion;
23+
}
24+
}
25+
26+
public enum HTTP_SERVICE_CONFIG_ID
27+
{
28+
HttpServiceConfigIPListenList = 0,
29+
HttpServiceConfigSSLCertInfo,
30+
HttpServiceConfigUrlAclInfo,
31+
HttpServiceConfigMax
32+
}
33+
34+
[StructLayout(LayoutKind.Sequential)]
35+
public struct HTTP_SERVICE_CONFIG_SSL_SET
36+
{
37+
public HTTP_SERVICE_CONFIG_SSL_KEY KeyDesc;
38+
public HTTP_SERVICE_CONFIG_SSL_PARAM ParamDesc;
39+
}
40+
41+
[StructLayout(LayoutKind.Sequential)]
42+
public struct HTTP_SERVICE_CONFIG_SSL_KEY
43+
{
44+
public IntPtr pIpPort;
45+
46+
public HTTP_SERVICE_CONFIG_SSL_KEY(IntPtr pIpPort)
47+
{
48+
this.pIpPort = pIpPort;
49+
}
50+
}
51+
52+
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
53+
public struct HTTP_SERVICE_CONFIG_SSL_PARAM
54+
{
55+
public int SslHashLength;
56+
public IntPtr pSslHash;
57+
public Guid AppId;
58+
[MarshalAs(UnmanagedType.LPWStr)]
59+
public string pSslCertStoreName;
60+
public CertCheckModes DefaultCertCheckMode;
61+
public int DefaultRevocationFreshnessTime;
62+
public int DefaultRevocationUrlRetrievalTimeout;
63+
[MarshalAs(UnmanagedType.LPWStr)]
64+
public string pDefaultSslCtlIdentifier;
65+
[MarshalAs(UnmanagedType.LPWStr)]
66+
public string pDefaultSslCtlStoreName;
67+
public uint DefaultFlags; // HTTP_SERVICE_CONFIG_SSL_FLAG
68+
}
69+
70+
[Flags]
71+
public enum CertCheckModes : uint
72+
{
73+
/// <summary>
74+
/// Enables the client certificate revocation check.
75+
/// </summary>
76+
None = 0,
77+
78+
/// <summary>
79+
/// Client certificate is not to be verified for revocation.
80+
/// </summary>
81+
DoNotVerifyCertificateRevocation = 1,
82+
83+
/// <summary>
84+
/// Only cached certificate is to be used the revocation check.
85+
/// </summary>
86+
VerifyRevocationWithCachedCertificateOnly = 2,
87+
88+
/// <summary>
89+
/// The RevocationFreshnessTime setting is enabled.
90+
/// </summary>
91+
EnableRevocationFreshnessTime = 4,
92+
93+
/// <summary>
94+
/// No usage check is to be performed.
95+
/// </summary>
96+
NoUsageCheck = 0x10000
97+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
using System.Reflection;
5+
using System.Runtime.InteropServices;
6+
using Microsoft.AspNetCore.Hosting;
7+
using Microsoft.AspNetCore.Http.Features;
8+
using Microsoft.AspNetCore.Server.HttpSys;
9+
using Microsoft.Extensions.Hosting;
10+
using TlsFeatureObserve;
11+
using TlsFeaturesObserve.HttpSys;
12+
13+
HttpSysConfigurator.ConfigureCacheTlsClientHello();
14+
CreateHostBuilder(args).Build().Run();
15+
16+
static IHostBuilder CreateHostBuilder(string[] args) =>
17+
Host.CreateDefaultBuilder(args)
18+
.ConfigureWebHost(webBuilder =>
19+
{
20+
webBuilder.UseStartup<Startup>()
21+
.UseHttpSys(options =>
22+
{
23+
// If you want to use https locally: https://stackoverflow.com/a/51841893
24+
options.UrlPrefixes.Add("https://*:6000"); // HTTPS
25+
26+
options.Authentication.Schemes = AuthenticationSchemes.None;
27+
options.Authentication.AllowAnonymous = true;
28+
29+
var property = typeof(HttpSysOptions).GetProperty("TlsClientHelloBytesCallback", BindingFlags.NonPublic | BindingFlags.Instance);
30+
var delegateType = property.PropertyType; // Get the exact delegate type
31+
32+
// Create a delegate of the correct type
33+
var callbackDelegate = Delegate.CreateDelegate(delegateType, typeof(Holder).GetMethod(nameof(Holder.ProcessTlsClientHello), BindingFlags.Static | BindingFlags.Public));
34+
35+
property?.SetValue(options, callbackDelegate);
36+
});
37+
});
38+
39+
public static class Holder
40+
{
41+
public static void ProcessTlsClientHello(IFeatureCollection features, ReadOnlySpan<byte> tlsClientHelloBytes)
42+
{
43+
var httpConnectionFeature = features.Get<IHttpConnectionFeature>();
44+
45+
var myTlsFeature = new MyTlsFeature(
46+
connectionId: httpConnectionFeature.ConnectionId,
47+
tlsClientHelloLength: tlsClientHelloBytes.Length);
48+
49+
features.Set<IMyTlsFeature>(myTlsFeature);
50+
}
51+
}
52+
53+
public interface IMyTlsFeature
54+
{
55+
string ConnectionId { get; }
56+
int TlsClientHelloLength { get; }
57+
}
58+
59+
public class MyTlsFeature : IMyTlsFeature
60+
{
61+
public string ConnectionId { get; }
62+
public int TlsClientHelloLength { get; }
63+
64+
public MyTlsFeature(string connectionId, int tlsClientHelloLength)
65+
{
66+
ConnectionId = connectionId;
67+
TlsClientHelloLength = tlsClientHelloLength;
68+
}
69+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
{
2+
"profiles": {
3+
"TlsFeaturesObserve": {
4+
"commandName": "Project",
5+
"launchBrowser": true,
6+
"applicationUrl": "http://localhost:5000",
7+
"nativeDebugging": true
8+
}
9+
}
10+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
using System;
5+
using Microsoft.AspNetCore.Builder;
6+
using Microsoft.AspNetCore.Connections.Features;
7+
using Microsoft.AspNetCore.Hosting;
8+
using Microsoft.AspNetCore.Http;
9+
using Microsoft.AspNetCore.Http.Features;
10+
using Microsoft.AspNetCore.Server.HttpSys;
11+
using Microsoft.Extensions.DependencyInjection;
12+
using Microsoft.Extensions.Logging;
13+
14+
namespace TlsFeatureObserve;
15+
16+
public class Startup
17+
{
18+
public void Configure(IApplicationBuilder app)
19+
{
20+
app.Run(async (HttpContext context) =>
21+
{
22+
context.Response.ContentType = "text/plain";
23+
24+
var tlsFeature = context.Features.Get<IMyTlsFeature>();
25+
await context.Response.WriteAsync("TlsClientHello data: " + $"connectionId={tlsFeature?.ConnectionId}; length={tlsFeature?.TlsClientHelloLength}");
26+
});
27+
}
28+
}

0 commit comments

Comments
 (0)