Skip to content

Commit 1ae00e6

Browse files
authored
Add TLS channel binding token access to ITlsConnectionFeature (#67436)
1 parent 4855e70 commit 1ae00e6

23 files changed

Lines changed: 666 additions & 18 deletions

File tree

src/Http/Http.Features/src/ITlsConnectionFeature.cs

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
// Licensed to the .NET Foundation under one or more agreements.
22
// The .NET Foundation licenses this file to you under the MIT license.
33

4+
using System.Security.Authentication.ExtendedProtection;
45
using System.Security.Cryptography.X509Certificates;
56

67
namespace Microsoft.AspNetCore.Http.Features;
@@ -19,4 +20,35 @@ public interface ITlsConnectionFeature
1920
/// Asynchronously retrieves the client certificate, if any.
2021
/// </summary>
2122
Task<X509Certificate2?> GetClientCertificateAsync(CancellationToken cancellationToken);
23+
24+
/// <summary>
25+
/// Attempts to retrieve the RFC 5929 TLS channel binding token (CBT) bytes for the
26+
/// requested <paramref name="kind"/> from the current connection.
27+
/// </summary>
28+
/// <param name="kind">The kind of channel binding to retrieve.</param>
29+
/// <param name="channelBindingToken">
30+
/// When this method returns <see langword="true"/>, contains the channel binding token
31+
/// bytes; otherwise, an empty <see cref="ReadOnlyMemory{T}"/>.
32+
/// </param>
33+
/// <returns>
34+
/// <see langword="true"/> if the requested channel binding is available;
35+
/// <see langword="false"/> otherwise (for example: the connection is not TLS,
36+
/// the requested <paramref name="kind"/> is not supported by the server, or
37+
/// channel binding is not enabled in server configuration).
38+
/// </returns>
39+
/// <remarks>
40+
/// <para>
41+
/// Channel binding tokens let in-channel authentication protocols (such as
42+
/// Kerberos or NTLM negotiated over HTTPS) bind themselves cryptographically
43+
/// to the underlying TLS channel, mitigating authentication relay attacks.
44+
/// See <see href="https://datatracker.ietf.org/doc/html/rfc5929"/> for the
45+
/// specification and Microsoft's "Extended Protection for Authentication"
46+
/// documentation for the Windows usage model.
47+
/// </para>
48+
/// </remarks>
49+
bool TryGetChannelBindingBytes(ChannelBindingKind kind, out ReadOnlyMemory<byte> channelBindingToken)
50+
{
51+
channelBindingToken = default;
52+
return false;
53+
}
2254
}

src/Http/Http.Features/src/ITlsTokenBindingFeature.cs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
// Licensed to the .NET Foundation under one or more agreements.
22
// The .NET Foundation licenses this file to you under the MIT license.
33

4+
using Microsoft.AspNetCore.Shared;
5+
46
namespace Microsoft.AspNetCore.Http.Features;
57

68
/// <summary>
@@ -12,6 +14,7 @@ namespace Microsoft.AspNetCore.Http.Features;
1214
/// client's machine. See <see href="https://datatracker.ietf.org/doc/draft-popov-token-binding/"/>
1315
/// for more information.
1416
/// </remarks>
17+
[Obsolete(Obsoletions.TlsTokenBindingFeatureMessage, DiagnosticId = Obsoletions.TlsTokenBindingFeatureDiagId, UrlFormat = Obsoletions.AspNetCoreDeprecate010Url)]
1518
public interface ITlsTokenBindingFeature
1619
{
1720
/// <summary>

src/Http/Http.Features/src/Microsoft.AspNetCore.Http.Features.csproj

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,4 +16,8 @@
1616
<Reference Include="Microsoft.Net.Http.Headers" />
1717
</ItemGroup>
1818

19+
<ItemGroup>
20+
<Compile Include="$(SharedSourceRoot)Obsoletions.cs" LinkBase="Shared" />
21+
</ItemGroup>
22+
1923
</Project>
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
11
#nullable enable
2+
Microsoft.AspNetCore.Http.Features.ITlsConnectionFeature.TryGetChannelBindingBytes(System.Security.Authentication.ExtendedProtection.ChannelBindingKind kind, out System.ReadOnlyMemory<byte> channelBindingToken) -> bool

src/Security/Authentication/Cookies/src/CookieAuthenticationHandler.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -480,7 +480,9 @@ protected override async Task HandleChallengeAsync(AuthenticationProperties prop
480480

481481
private string? GetTlsTokenBinding()
482482
{
483+
#pragma warning disable ASPDEPR010 // ITlsTokenBindingFeature is obsolete; kept for back-compat inside cookie auth handler.
483484
var binding = Context.Features.Get<ITlsTokenBindingFeature>()?.GetProvidedTokenBindingId();
485+
#pragma warning restore ASPDEPR010
484486
return binding == null ? null : Convert.ToBase64String(binding);
485487
}
486488
}

src/Servers/HttpSys/samples/TlsFeaturesObserve/Program.cs

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,11 @@
22
// The .NET Foundation licenses this file to you under the MIT license.
33

44
using System.Buffers;
5+
using System.Buffers.Binary;
56
using System.Diagnostics;
67
using System.Reflection;
78
using System.Runtime.InteropServices;
9+
using System.Security.Authentication.ExtendedProtection;
810
using Microsoft.AspNetCore.Builder;
911
using Microsoft.AspNetCore.Connections.Features;
1012
using Microsoft.AspNetCore.Hosting;
@@ -23,10 +25,63 @@
2325
options.UrlPrefixes.Add("https://*:6000");
2426
options.Authentication.Schemes = AuthenticationSchemes.None;
2527
options.Authentication.AllowAnonymous = true;
28+
29+
// Expose the RFC 5929 TLS channel binding token
30+
options.HttpAuthenticationHardeningLevel = HttpAuthenticationHardeningLevel.Medium;
2631
});
2732

2833
var app = builder.Build();
2934

35+
// Example middleware using ITlsConnectionFeature.TryGetChannelBindingBytes to load the channel binding token bytes and parse them.
36+
app.Use(async (context, next) =>
37+
{
38+
var tlsFeature = context.Features.Get<ITlsConnectionFeature>();
39+
if (tlsFeature is not null && tlsFeature.TryGetChannelBindingBytes(ChannelBindingKind.Endpoint, out var bytes))
40+
{
41+
// Parse the SEC_CHANNEL_BINDINGS header so we can see the RFC 5929 (https://datatracker.ietf.org/doc/html/rfc5929)
42+
// application data ("tls-server-end-point:" + SHA-256 of the cert).
43+
var span = bytes.Span;
44+
if (span.Length < 32)
45+
{
46+
await context.Response.WriteAsync("\tITlsConnectionFeature.TryGetChannelBindingBytes(Endpoint) returned an unexpected payload (< 32 bytes).\n\n");
47+
await next(context);
48+
return;
49+
}
50+
51+
var appLen = BinaryPrimitives.ReadUInt32LittleEndian(span.Slice(24, 4));
52+
var appOff = BinaryPrimitives.ReadUInt32LittleEndian(span.Slice(28, 4));
53+
if (appOff + appLen > (uint)span.Length)
54+
{
55+
await context.Response.WriteAsync("\tITlsConnectionFeature.TryGetChannelBindingBytes(Endpoint) returned an unexpected payload (appdata outside buffer).\n\n");
56+
await next(context);
57+
return;
58+
}
59+
60+
var app = span.Slice((int)appOff, (int)appLen);
61+
var asciiPrefix = System.Text.Encoding.ASCII.GetString(app[..Math.Min(21, app.Length)]);
62+
var hash = Convert.ToHexString(app[Math.Min(21, app.Length)..]);
63+
64+
await context.Response.WriteAsync(
65+
$"""
66+
ITlsConnectionFeature.TryGetChannelBindingBytes(Endpoint)
67+
----------------------------------------------------------
68+
raw length = {bytes.Length} bytes (SEC_CHANNEL_BINDINGS header + appdata)
69+
appdata length = {appLen}
70+
appdata offset = {appOff}
71+
ASCII prefix = "{asciiPrefix}"
72+
cert hash (hex) = {hash}
73+
74+
75+
""");
76+
}
77+
else
78+
{
79+
await context.Response.WriteAsync("\tITlsConnectionFeature.TryGetChannelBindingBytes(Endpoint) returned false \n\n");
80+
}
81+
82+
await next(context);
83+
});
84+
3085
// Example middleware using TryGetTlsClientHello API to query TLS Client Hello raw bytes.
3186
app.Use(async (context, next) =>
3287
{

src/Servers/HttpSys/samples/TlsFeaturesObserve/TlsFeaturesObserve.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
<Reference Include="Microsoft.AspNetCore.Authentication.Abstractions" />
1414
<Reference Include="Microsoft.AspNetCore.Authentication.Core" />
1515
<Reference Include="Microsoft.AspNetCore.Authorization.Policy" />
16+
<Reference Include="Microsoft.AspNetCore.Cors" />
1617
<Reference Include="Microsoft.AspNetCore.Diagnostics" />
1718
<Reference Include="Microsoft.AspNetCore.Diagnostics.Abstractions" />
1819
<Reference Include="Microsoft.AspNetCore.HostFiltering" />
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
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+
namespace Microsoft.AspNetCore.Server.HttpSys;
5+
6+
/// <summary>
7+
/// Specifies the Http.Sys authentication hardening level that controls how strictly
8+
/// HTTP authentication (Kerberos/NTLM) is validated against the underlying TLS channel.
9+
/// </summary>
10+
/// <remarks>
11+
/// <para>
12+
/// Corresponds to the Win32
13+
/// <see href="https://learn.microsoft.com/windows/win32/api/http/ne-http-http_authentication_hardening_levels"><c>HTTP_AUTHENTICATION_HARDENING_LEVELS</c></see>
14+
/// enumeration applied to the URL group's <c>HttpServerChannelBindProperty</c>.
15+
/// </para>
16+
/// <para>
17+
/// When set to <see cref="Medium"/> or <see cref="Strict"/>, Http.Sys is also
18+
/// instructed to attach the per-request <c>HTTP_REQUEST_CHANNEL_BIND_STATUS</c>
19+
/// so the application can retrieve the RFC 5929 TLS channel binding token via
20+
/// <see cref="Microsoft.AspNetCore.Http.Features.ITlsConnectionFeature.TryGetChannelBindingBytes"/>.
21+
/// </para>
22+
/// </remarks>
23+
public enum HttpAuthenticationHardeningLevel
24+
{
25+
/// <summary>
26+
/// Http.Sys does not enforce channel binding validation and does not expose the RFC 5929 TLS channel binding token to the application.
27+
/// This matches the pre-hardening default behavior.
28+
/// </summary>
29+
Legacy = 0,
30+
31+
/// <summary>
32+
/// Http.Sys validates channel binding tokens when clients supply them but tolerates their absence.
33+
/// The per-request TLS channel binding token is exposed to the application.
34+
/// </summary>
35+
Medium = 1,
36+
37+
/// <summary>
38+
/// Http.Sys requires channel binding tokens on authenticated requests and rejects those without one.
39+
/// The per-request TLS channel binding token is exposed to the application.
40+
/// </summary>
41+
Strict = 2,
42+
}

src/Servers/HttpSys/src/HttpSysOptions.cs

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,21 @@ public string? RequestQueueName
120120
/// </summary>
121121
public bool EnableKernelResponseBuffering { get; set; }
122122

123+
/// <summary>
124+
/// Configures the Http.Sys authentication hardening level. Non-<see cref="HttpAuthenticationHardeningLevel.Legacy"/>
125+
/// values instruct Http.Sys to validate the RFC 5929 TLS channel binding token (CBT) against
126+
/// authenticated requests and to expose the per-request CBT to the application via
127+
/// <see cref="Microsoft.AspNetCore.Http.Features.ITlsConnectionFeature.TryGetChannelBindingBytes"/>.
128+
/// The default is <see cref="HttpAuthenticationHardeningLevel.Medium"/>.
129+
/// </summary>
130+
/// <remarks>
131+
/// Setting this to <see cref="HttpAuthenticationHardeningLevel.Medium"/> or
132+
/// <see cref="HttpAuthenticationHardeningLevel.Strict"/> both raises the hardening
133+
/// level and sets the <c>HTTP_CHANNEL_BIND_SECURE_CHANNEL_TOKEN</c> flag on the URL
134+
/// group's <c>HttpServerChannelBindProperty</c>.
135+
/// </remarks>
136+
public HttpAuthenticationHardeningLevel HttpAuthenticationHardeningLevel { get; set; } = HttpAuthenticationHardeningLevel.Medium;
137+
123138
/// <summary>
124139
/// Gets or sets the maximum number of concurrent connections to accept. Set `-1` for infinite.
125140
/// Set to `null` to use the registry's machine-wide setting.
@@ -276,5 +291,6 @@ internal void Apply(UrlGroup urlGroup, RequestQueue? requestQueue)
276291

277292
Authentication.SetUrlGroupSecurity(urlGroup);
278293
Timeouts.SetUrlGroupTimeouts(urlGroup);
294+
urlGroup.SetChannelBindingProperty(HttpAuthenticationHardeningLevel);
279295
}
280296
}

src/Servers/HttpSys/src/NativeInterop/UrlGroup.cs

Lines changed: 28 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -12,14 +12,13 @@ namespace Microsoft.AspNetCore.Server.HttpSys;
1212

1313
internal sealed partial class UrlGroup : IDisposable
1414
{
15-
private static readonly int BindingInfoSize =
16-
Marshal.SizeOf<HTTP_BINDING_INFO>();
17-
private static readonly int QosInfoSize =
18-
Marshal.SizeOf<HTTP_QOS_SETTING_INFO>();
19-
private static readonly int RequestPropertyInfoSize =
20-
Marshal.SizeOf<HTTP_BINDING_INFO>();
21-
private static readonly int ChannelBindInfoSize =
22-
Marshal.SizeOf<HTTP_CHANNEL_BIND_INFO>();
15+
// See https://learn.microsoft.com/windows/win32/api/http/ns-http-http_channel_bind_info
16+
private const uint HTTP_CHANNEL_BIND_SECURE_CHANNEL_TOKEN = 0x00000008;
17+
18+
private static readonly int BindingInfoSize = Marshal.SizeOf<HTTP_BINDING_INFO>();
19+
private static readonly int QosInfoSize = Marshal.SizeOf<HTTP_QOS_SETTING_INFO>();
20+
private static readonly int RequestPropertyInfoSize = Marshal.SizeOf<HTTP_BINDING_INFO>();
21+
private static readonly int ChannelBindInfoSize = Marshal.SizeOf<HTTP_CHANNEL_BIND_INFO>();
2322

2423
private readonly ILogger _logger;
2524

@@ -44,17 +43,30 @@ internal unsafe UrlGroup(ServerSession serverSession, RequestQueue requestQueue,
4443

4544
Debug.Assert(urlGroupId != 0, "Invalid id returned by HttpCreateUrlGroup");
4645
Id = urlGroupId;
46+
}
4747

48-
if (AppContext.TryGetSwitch("Microsoft.AspNetCore.Server.HttpSys.EnableCBTHardening", out var enabled) && enabled)
48+
// Sets HttpServerChannelBindProperty with the requested hardening level and the HTTP_CHANNEL_BIND_SECURE_CHANNEL_TOKEN flag
49+
// so the per-request CBT is delivered to the app.
50+
internal unsafe void SetChannelBindingProperty(HttpAuthenticationHardeningLevel level)
51+
{
52+
var info = new HTTP_CHANNEL_BIND_INFO
4953
{
50-
var channelBindingSettings = new HTTP_CHANNEL_BIND_INFO
54+
Hardening = level switch
5155
{
52-
Hardening = HTTP_AUTHENTICATION_HARDENING_LEVELS.HttpAuthenticationHardeningMedium,
53-
ServiceNames = (HTTP_SERVICE_BINDING_BASE**)IntPtr.Zero,
54-
NumberOfServiceNames = 0,
55-
};
56-
SetProperty(HTTP_SERVER_PROPERTY.HttpServerChannelBindProperty, new(&channelBindingSettings), (uint)ChannelBindInfoSize);
57-
}
56+
HttpAuthenticationHardeningLevel.Strict => HTTP_AUTHENTICATION_HARDENING_LEVELS.HttpAuthenticationHardeningStrict,
57+
HttpAuthenticationHardeningLevel.Legacy => HTTP_AUTHENTICATION_HARDENING_LEVELS.HttpAuthenticationHardeningLegacy,
58+
HttpAuthenticationHardeningLevel.Medium or _ => HTTP_AUTHENTICATION_HARDENING_LEVELS.HttpAuthenticationHardeningMedium,
59+
},
60+
ServiceNames = (HTTP_SERVICE_BINDING_BASE**)IntPtr.Zero,
61+
NumberOfServiceNames = 0,
62+
63+
// optimize: Flags control if CBT is included in NativeRequest, and if legacy level is used, no point to load the CBT data.
64+
Flags = level == HttpAuthenticationHardeningLevel.Legacy
65+
? default
66+
: HTTP_CHANNEL_BIND_SECURE_CHANNEL_TOKEN,
67+
};
68+
69+
SetProperty(HTTP_SERVER_PROPERTY.HttpServerChannelBindProperty, new IntPtr(&info), (uint)ChannelBindInfoSize, throwOnError: false);
5870
}
5971

6072
internal ulong Id { get; private set; }

0 commit comments

Comments
 (0)