Skip to content

Commit a31d83b

Browse files
authored
Implement ITlsHandshakeFeature in IIS (#48957)
1 parent f12c72b commit a31d83b

File tree

9 files changed

+280
-45
lines changed

9 files changed

+280
-45
lines changed

src/Servers/HttpSys/src/RequestProcessing/Request.cs

Lines changed: 0 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -343,41 +343,7 @@ private AspNetCore.HttpSys.Internal.SocketAddress LocalEndPoint
343343
private void GetTlsHandshakeResults()
344344
{
345345
var handshake = RequestContext.GetTlsHandshake();
346-
347346
Protocol = handshake.Protocol;
348-
// The OS considers client and server TLS as different enum values. SslProtocols choose to combine those for some reason.
349-
// We need to fill in the client bits so the enum shows the expected protocol.
350-
// https://learn.microsoft.com/windows/desktop/api/schannel/ns-schannel-_secpkgcontext_connectioninfo
351-
// Compare to https://referencesource.microsoft.com/#System/net/System/Net/SecureProtocols/_SslState.cs,8905d1bf17729de3
352-
#pragma warning disable CS0618 // Type or member is obsolete
353-
if ((Protocol & SslProtocols.Ssl2) != 0)
354-
{
355-
Protocol |= SslProtocols.Ssl2;
356-
}
357-
if ((Protocol & SslProtocols.Ssl3) != 0)
358-
{
359-
Protocol |= SslProtocols.Ssl3;
360-
}
361-
#pragma warning restore CS0618 // Type or Prmember is obsolete
362-
#pragma warning disable SYSLIB0039 // TLS 1.0 and 1.1 are obsolete
363-
if ((Protocol & SslProtocols.Tls) != 0)
364-
{
365-
Protocol |= SslProtocols.Tls;
366-
}
367-
if ((Protocol & SslProtocols.Tls11) != 0)
368-
{
369-
Protocol |= SslProtocols.Tls11;
370-
}
371-
#pragma warning restore SYSLIB0039
372-
if ((Protocol & SslProtocols.Tls12) != 0)
373-
{
374-
Protocol |= SslProtocols.Tls12;
375-
}
376-
if ((Protocol & SslProtocols.Tls13) != 0)
377-
{
378-
Protocol |= SslProtocols.Tls13;
379-
}
380-
381347
CipherAlgorithm = handshake.CipherType;
382348
CipherStrength = (int)handshake.CipherStrength;
383349
HashAlgorithm = handshake.HashType;

src/Servers/IIS/AspNetCoreModuleV2/InProcessRequestHandler/managedexports.cpp

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -611,4 +611,35 @@ http_response_set_need_goaway(
611611
pHttpResponse->SetNeedGoAway();
612612
return 0;
613613
}
614+
615+
EXTERN_C __declspec(dllexport)
616+
HRESULT
617+
http_query_request_property(
618+
_In_ HTTP_OPAQUE_ID requestId,
619+
_In_ HTTP_REQUEST_PROPERTY propertyId,
620+
_In_reads_bytes_opt_(qualifierSize) PVOID pQualifier,
621+
_In_ ULONG qualifierSize,
622+
_Out_writes_bytes_to_opt_(outputBufferSize, *pcbBytesReturned) PVOID pOutput,
623+
_In_ ULONG outputBufferSize,
624+
_Out_opt_ PULONG pcbBytesReturned,
625+
_In_ LPOVERLAPPED pOverlapped
626+
)
627+
{
628+
IHttpServer3* httpServer3;
629+
HRESULT hr = HttpGetExtendedInterface<IHttpServer, IHttpServer3>(g_pHttpServer, g_pHttpServer, &httpServer3);
630+
if (FAILED(hr))
631+
{
632+
return hr;
633+
}
634+
635+
return httpServer3->QueryRequestProperty(
636+
requestId,
637+
propertyId,
638+
pQualifier,
639+
qualifierSize,
640+
pOutput,
641+
outputBufferSize,
642+
pcbBytesReturned,
643+
pOverlapped);
644+
}
614645
// End of export

src/Servers/IIS/IIS/src/Core/IISHttpContext.FeatureCollection.cs

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,9 @@
44
using System.Collections;
55
using System.Diagnostics;
66
using System.IO.Pipelines;
7+
using System.Net.Security;
78
using System.Runtime.InteropServices;
9+
using System.Security.Authentication;
810
using System.Security.Claims;
911
using System.Security.Cryptography.X509Certificates;
1012
using Microsoft.AspNetCore.Connections.Features;
@@ -28,6 +30,7 @@ internal partial class IISHttpContext : IFeatureCollection,
2830
IHttpAuthenticationFeature,
2931
IServerVariablesFeature,
3032
ITlsConnectionFeature,
33+
ITlsHandshakeFeature,
3134
IHttpBodyControlFeature,
3235
IHttpMaxRequestBodySizeFeature,
3336
IHttpResponseTrailersFeature,
@@ -406,6 +409,24 @@ unsafe X509Certificate2? ITlsConnectionFeature.ClientCertificate
406409
}
407410
}
408411

412+
SslProtocols ITlsHandshakeFeature.Protocol => Protocol;
413+
414+
TlsCipherSuite? ITlsHandshakeFeature.NegotiatedCipherSuite => NegotiatedCipherSuite;
415+
416+
string ITlsHandshakeFeature.HostName => SniHostName;
417+
418+
CipherAlgorithmType ITlsHandshakeFeature.CipherAlgorithm => CipherAlgorithm;
419+
420+
int ITlsHandshakeFeature.CipherStrength => CipherStrength;
421+
422+
HashAlgorithmType ITlsHandshakeFeature.HashAlgorithm => HashAlgorithm;
423+
424+
int ITlsHandshakeFeature.HashStrength => HashStrength;
425+
426+
ExchangeAlgorithmType ITlsHandshakeFeature.KeyExchangeAlgorithm => KeyExchangeAlgorithm;
427+
428+
int ITlsHandshakeFeature.KeyExchangeStrength => KeyExchangeStrength;
429+
409430
IEnumerator<KeyValuePair<Type, object>> IEnumerable<KeyValuePair<Type, object>>.GetEnumerator() => FastEnumerable().GetEnumerator();
410431

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

470+
internal ITlsHandshakeFeature? GetTlsHandshakeFeature()
471+
{
472+
return IsHttps ? this : null;
473+
}
474+
449475
IHeaderDictionary IHttpResponseTrailersFeature.Trailers
450476
{
451477
get => ResponseTrailers ??= HttpResponseTrailers;

src/Servers/IIS/IIS/src/Core/IISHttpContext.Features.cs

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ internal partial class IISHttpContext
2020
private static readonly Type IResponseCookiesFeatureType = typeof(global::Microsoft.AspNetCore.Http.Features.IResponseCookiesFeature);
2121
private static readonly Type IItemsFeatureType = typeof(global::Microsoft.AspNetCore.Http.Features.IItemsFeature);
2222
private static readonly Type ITlsConnectionFeatureType = typeof(global::Microsoft.AspNetCore.Http.Features.ITlsConnectionFeature);
23+
private static readonly Type ITlsHandshakeFeatureType = typeof(global::Microsoft.AspNetCore.Connections.Features.ITlsHandshakeFeature);
2324
private static readonly Type IHttpWebSocketFeatureType = typeof(global::Microsoft.AspNetCore.Http.Features.IHttpWebSocketFeature);
2425
private static readonly Type ISessionFeatureType = typeof(global::Microsoft.AspNetCore.Http.Features.ISessionFeature);
2526
private static readonly Type IHttpBodyControlFeatureType = typeof(global::Microsoft.AspNetCore.Http.Features.IHttpBodyControlFeature);
@@ -48,6 +49,7 @@ internal partial class IISHttpContext
4849
private object? _currentIResponseCookiesFeature;
4950
private object? _currentIItemsFeature;
5051
private object? _currentITlsConnectionFeature;
52+
private object? _currentITlsHandshakeFeature;
5153
private object? _currentIHttpWebSocketFeature;
5254
private object? _currentISessionFeature;
5355
private object? _currentIHttpBodyControlFeature;
@@ -75,6 +77,7 @@ private void Initialize()
7577
_currentIServerVariablesFeature = this;
7678
_currentIHttpMaxRequestBodySizeFeature = this;
7779
_currentITlsConnectionFeature = this;
80+
_currentITlsHandshakeFeature = GetTlsHandshakeFeature();
7881
_currentIHttpResponseTrailersFeature = GetResponseTrailersFeature();
7982
_currentIHttpResetFeature = GetResetFeature();
8083
_currentIConnectionLifetimeNotificationFeature = this;
@@ -146,6 +149,10 @@ private void Initialize()
146149
{
147150
return _currentITlsConnectionFeature;
148151
}
152+
if (key == ITlsHandshakeFeatureType)
153+
{
154+
return _currentITlsHandshakeFeature;
155+
}
149156
if (key == IHttpWebSocketFeatureType)
150157
{
151158
return _currentIHttpWebSocketFeature;
@@ -277,6 +284,11 @@ internal void FastFeatureSet(Type key, object? feature)
277284
_currentITlsConnectionFeature = feature;
278285
return;
279286
}
287+
if (key == ITlsHandshakeFeatureType)
288+
{
289+
_currentITlsHandshakeFeature = feature;
290+
return;
291+
}
280292
if (key == IHttpWebSocketFeatureType)
281293
{
282294
_currentIHttpWebSocketFeature = feature;
@@ -400,6 +412,10 @@ private IEnumerable<KeyValuePair<Type, object>> FastEnumerable()
400412
{
401413
yield return new KeyValuePair<Type, object>(ITlsConnectionFeatureType, _currentITlsConnectionFeature);
402414
}
415+
if (_currentITlsHandshakeFeature != null)
416+
{
417+
yield return new KeyValuePair<Type, object>(ITlsHandshakeFeatureType, _currentITlsHandshakeFeature);
418+
}
403419
if (_currentIHttpWebSocketFeature != null)
404420
{
405421
yield return new KeyValuePair<Type, object>(IHttpWebSocketFeatureType, _currentIHttpWebSocketFeature);

src/Servers/IIS/IIS/src/Core/IISHttpContext.cs

Lines changed: 54 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,9 @@
66
using System.Diagnostics.CodeAnalysis;
77
using System.IO.Pipelines;
88
using System.Net;
9+
using System.Net.Security;
910
using System.Runtime.InteropServices;
11+
using System.Security.Authentication;
1012
using System.Security.Claims;
1113
using System.Security.Principal;
1214
using System.Text;
@@ -88,6 +90,7 @@ internal unsafe IISHttpContext(
8890

8991
private int PauseWriterThreshold => _options.MaxRequestBodyBufferSize;
9092
private int ResumeWriterTheshold => PauseWriterThreshold / 2;
93+
private bool IsHttps => SslStatus != SslStatus.Insecure;
9194

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

117+
public SslProtocols Protocol { get; private set; }
118+
public TlsCipherSuite? NegotiatedCipherSuite { get; private set; }
119+
public string SniHostName { get; private set; } = default!;
120+
public CipherAlgorithmType CipherAlgorithm { get; private set; }
121+
public int CipherStrength { get; private set; }
122+
public HashAlgorithmType HashAlgorithm { get; private set; }
123+
public int HashStrength { get; private set; }
124+
public ExchangeAlgorithmType KeyExchangeAlgorithm { get; private set; }
125+
public int KeyExchangeStrength { get; private set; }
126+
114127
protected IAsyncIOEngine? AsyncIO { get; set; }
115128

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

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

269+
SniHostName = string.Empty;
270+
if (IsHttps)
271+
{
272+
GetTlsHandshakeResults();
273+
}
274+
256275
if (_options.ForwardWindowsAuthentication)
257276
{
258277
WindowsUser = GetWindowsPrincipal();
@@ -371,6 +390,40 @@ private bool CheckRequestCanHaveBody()
371390
return RequestHeaders.ContentLength.GetValueOrDefault() > 0;
372391
}
373392

393+
private void GetTlsHandshakeResults()
394+
{
395+
var handshake = this.GetTlsHandshake();
396+
Protocol = handshake.Protocol;
397+
CipherAlgorithm = handshake.CipherType;
398+
CipherStrength = (int)handshake.CipherStrength;
399+
HashAlgorithm = handshake.HashType;
400+
HashStrength = (int)handshake.HashStrength;
401+
KeyExchangeAlgorithm = handshake.KeyExchangeType;
402+
KeyExchangeStrength = (int)handshake.KeyExchangeStrength;
403+
404+
var sni = GetClientSni();
405+
SniHostName = sni.Hostname;
406+
}
407+
408+
private unsafe HttpApiTypes.HTTP_REQUEST_PROPERTY_SNI GetClientSni()
409+
{
410+
var buffer = new byte[HttpApiTypes.SniPropertySizeInBytes];
411+
fixed (byte* pBuffer = buffer)
412+
{
413+
var statusCode = NativeMethods.HttpQueryRequestProperty(
414+
RequestId,
415+
HttpApiTypes.HTTP_REQUEST_PROPERTY.HttpRequestPropertySni,
416+
qualifier: null,
417+
qualifierSize: 0,
418+
(void*)pBuffer,
419+
(uint)buffer.Length,
420+
bytesReturned: null,
421+
IntPtr.Zero);
422+
423+
return statusCode == NativeMethods.HR_OK ? Marshal.PtrToStructure<HttpApiTypes.HTTP_REQUEST_PROPERTY_SNI>((IntPtr)pBuffer) : default;
424+
}
425+
}
426+
374427
private async Task InitializeResponse(bool flushHeaders)
375428
{
376429
await FireOnStarting();

src/Servers/IIS/IIS/src/NativeMethods.cs

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,17 @@ private static unsafe partial int register_callbacks(NativeSafeHandle pInProcess
9090
[LibraryImport(AspNetCoreModuleDll)]
9191
private static partial int http_get_application_properties(out IISConfigurationData iiConfigData);
9292

93+
[LibraryImport(AspNetCoreModuleDll)]
94+
private static unsafe partial int http_query_request_property(
95+
ulong requestId,
96+
HttpApiTypes.HTTP_REQUEST_PROPERTY propertyId,
97+
void* qualifier,
98+
uint qualifierSize,
99+
void* output,
100+
uint outputSize,
101+
uint* bytesReturned,
102+
IntPtr overlapped);
103+
93104
[LibraryImport(AspNetCoreModuleDll)]
94105
private static partial int http_get_server_variable(
95106
NativeSafeHandle pInProcessHandler,
@@ -231,6 +242,11 @@ internal static IISConfigurationData HttpGetApplicationProperties()
231242
return iisConfigurationData;
232243
}
233244

245+
public static unsafe int HttpQueryRequestProperty(ulong requestId, HttpApiTypes.HTTP_REQUEST_PROPERTY propertyId, void* qualifier, uint qualifierSize, void* output, uint outputSize, uint* bytesReturned, IntPtr overlapped)
246+
{
247+
return http_query_request_property(requestId, propertyId, qualifier, qualifierSize, output, outputSize, bytesReturned, overlapped);
248+
}
249+
234250
public static bool HttpTryGetServerVariable(NativeSafeHandle pInProcessHandler, string variableName, out string value)
235251
{
236252
return http_get_server_variable(pInProcessHandler, variableName, out value) == 0;
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
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.Security.Authentication;
5+
using Microsoft.AspNetCore.Connections.Features;
6+
using Microsoft.AspNetCore.Testing;
7+
using Xunit;
8+
9+
namespace Microsoft.AspNetCore.Server.IIS.FunctionalTests;
10+
11+
[SkipIfHostableWebCoreNotAvailable]
12+
[MinimumOSVersion(OperatingSystems.Windows, WindowsVersions.Win8, SkipReason = "https://github.com/aspnet/IISIntegration/issues/866")]
13+
[SkipOnHelix("Unsupported queue", Queues = "Windows.Amd64.VS2022.Pre.Open;")]
14+
public class TlsHandshakeFeatureTests : StrictTestServerTests
15+
{
16+
[ConditionalFact]
17+
public async Task SetsTlsHandshakeFeatureForHttps()
18+
{
19+
ITlsHandshakeFeature tlsHandshakeFeature = null;
20+
using (var testServer = await TestServer.CreateHttps(ctx =>
21+
{
22+
tlsHandshakeFeature = ctx.Features.Get<ITlsHandshakeFeature>();
23+
return Task.CompletedTask;
24+
}, LoggerFactory))
25+
{
26+
await testServer.HttpClient.GetStringAsync("/");
27+
}
28+
29+
Assert.NotNull(tlsHandshakeFeature);
30+
31+
var protocol = tlsHandshakeFeature.Protocol;
32+
Assert.True(protocol > SslProtocols.None, "Protocol: " + protocol);
33+
Assert.True(Enum.IsDefined(typeof(SslProtocols), protocol), "Defined: " + protocol); // Mapping is required, make sure it's current
34+
35+
var cipherAlgorithm = tlsHandshakeFeature.CipherAlgorithm;
36+
Assert.True(cipherAlgorithm > CipherAlgorithmType.Null, "Cipher: " + cipherAlgorithm);
37+
38+
var cipherStrength = tlsHandshakeFeature.CipherStrength;
39+
Assert.True(cipherStrength > 0, "CipherStrength: " + cipherStrength);
40+
41+
var hashAlgorithm = tlsHandshakeFeature.HashAlgorithm;
42+
Assert.True(hashAlgorithm >= HashAlgorithmType.None, "HashAlgorithm: " + hashAlgorithm);
43+
44+
var hashStrength = tlsHandshakeFeature.HashStrength;
45+
Assert.True(hashStrength >= 0, "HashStrength: " + hashStrength); // May be 0 for some algorithms
46+
47+
var keyExchangeAlgorithm = tlsHandshakeFeature.KeyExchangeAlgorithm;
48+
Assert.True(keyExchangeAlgorithm >= ExchangeAlgorithmType.None, "KeyExchangeAlgorithm: " + keyExchangeAlgorithm);
49+
50+
var keyExchangeStrength = tlsHandshakeFeature.KeyExchangeStrength;
51+
Assert.True(keyExchangeStrength >= 0, "KeyExchangeStrength: " + keyExchangeStrength);
52+
53+
if (Environment.OSVersion.Version > new Version(10, 0, 19043, 0))
54+
{
55+
var hostName = tlsHandshakeFeature.HostName;
56+
Assert.Equal("localhost", hostName);
57+
}
58+
}
59+
60+
[ConditionalFact]
61+
public async Task DoesNotSetTlsHandshakeFeatureForHttp()
62+
{
63+
ITlsHandshakeFeature tlsHandshakeFeature = null;
64+
using (var testServer = await TestServer.Create(ctx =>
65+
{
66+
tlsHandshakeFeature = ctx.Features.Get<ITlsHandshakeFeature>();
67+
return Task.CompletedTask;
68+
}, LoggerFactory))
69+
{
70+
await testServer.HttpClient.GetStringAsync("/");
71+
}
72+
73+
Assert.Null(tlsHandshakeFeature);
74+
}
75+
}

0 commit comments

Comments
 (0)