diff --git a/src/Servers/HttpSys/samples/TlsFeaturesObserve/Program.cs b/src/Servers/HttpSys/samples/TlsFeaturesObserve/Program.cs index ed61268897ef..1b9e03dc5706 100644 --- a/src/Servers/HttpSys/samples/TlsFeaturesObserve/Program.cs +++ b/src/Servers/HttpSys/samples/TlsFeaturesObserve/Program.cs @@ -6,6 +6,7 @@ using System.Reflection; using System.Runtime.InteropServices; using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Connections.Features; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http.Features; @@ -30,6 +31,7 @@ { var connectionFeature = context.Features.GetRequiredFeature(); var httpSysPropFeature = context.Features.GetRequiredFeature(); + var tlsHandshakeFeature = context.Features.GetRequiredFeature(); // first time invocation to find out required size var success = httpSysPropFeature.TryGetTlsClientHello(Array.Empty(), out var bytesReturned); @@ -41,7 +43,14 @@ success = httpSysPropFeature.TryGetTlsClientHello(bytes, out _); Debug.Assert(success); - await context.Response.WriteAsync($"[Response] connectionId={connectionFeature.ConnectionId}; tlsClientHello.length={bytesReturned}; tlsclienthello start={string.Join(' ', bytes.AsSpan(0, 30).ToArray())}"); + await context.Response.WriteAsync( + $""" + connectionId = {connectionFeature.ConnectionId}; + negotiated cipher suite = {tlsHandshakeFeature.NegotiatedCipherSuite}; + tlsClientHello.length = {bytesReturned}; + tlsclienthello start = {string.Join(' ', bytes.AsSpan(0, 30).ToArray())} + """); + await next(context); }); diff --git a/src/Servers/HttpSys/src/LoggerEventIds.cs b/src/Servers/HttpSys/src/LoggerEventIds.cs index e6d745f506be..a9f2c969c6bc 100644 --- a/src/Servers/HttpSys/src/LoggerEventIds.cs +++ b/src/Servers/HttpSys/src/LoggerEventIds.cs @@ -60,4 +60,5 @@ internal static class LoggerEventIds public const int AcceptObserveExpectationMismatch = 53; public const int RequestParsingError = 54; public const int TlsListenerError = 55; + public const int QueryTlsCipherSuiteError = 56; } diff --git a/src/Servers/HttpSys/src/NativeInterop/HttpApi.cs b/src/Servers/HttpSys/src/NativeInterop/HttpApi.cs index f5dfbc96a6cd..fbada3843642 100644 --- a/src/Servers/HttpSys/src/NativeInterop/HttpApi.cs +++ b/src/Servers/HttpSys/src/NativeInterop/HttpApi.cs @@ -70,6 +70,7 @@ internal static unsafe uint HttpSetRequestProperty(SafeHandle requestQueueHandle internal static bool SupportsReset { get; } internal static bool SupportsDelegation { get; } internal static bool SupportsClientHello { get; } + internal static bool SupportsQueryTlsCipherInfo { get; } internal static bool Supported { get; } static unsafe HttpApi() @@ -86,6 +87,7 @@ static unsafe HttpApi() SupportsTrailers = IsFeatureSupported(HTTP_FEATURE_ID.HttpFeatureResponseTrailers); SupportsDelegation = IsFeatureSupported(HTTP_FEATURE_ID.HttpFeatureDelegateEx); SupportsClientHello = IsFeatureSupported((HTTP_FEATURE_ID)11 /* HTTP_FEATURE_ID.HttpFeatureCacheTlsClientHello */) && HttpGetRequestPropertySupported; + SupportsQueryTlsCipherInfo = IsFeatureSupported((HTTP_FEATURE_ID)15 /* HTTP_FEATURE_ID.HttpFeatureQueryCipherInfo */) && HttpGetRequestPropertySupported; } } diff --git a/src/Servers/HttpSys/src/RequestProcessing/Request.cs b/src/Servers/HttpSys/src/RequestProcessing/Request.cs index 8e4babf7ca21..3d99fe8e718b 100644 --- a/src/Servers/HttpSys/src/RequestProcessing/Request.cs +++ b/src/Servers/HttpSys/src/RequestProcessing/Request.cs @@ -3,6 +3,7 @@ using System.Globalization; using System.Net; +using System.Net.Security; using System.Security; using System.Security.Authentication; using System.Security.Cryptography; @@ -334,6 +335,8 @@ private AspNetCore.HttpSys.Internal.SocketAddress LocalEndPoint public SslProtocols Protocol { get; private set; } + public TlsCipherSuite? NegotiatedCipherSuite { get; private set; } + [Obsolete(Obsoletions.RuntimeTlsCipherAlgorithmEnumsMessage, DiagnosticId = Obsoletions.RuntimeTlsCipherAlgorithmEnumsDiagId, UrlFormat = Obsoletions.RuntimeSharedUrlFormat)] public CipherAlgorithmType CipherAlgorithm { get; private set; } @@ -356,6 +359,8 @@ private void GetTlsHandshakeResults() { var handshake = RequestContext.GetTlsHandshake(); Protocol = (SslProtocols)handshake.Protocol; + + NegotiatedCipherSuite = RequestContext.GetTlsCipherSuite(); #pragma warning disable SYSLIB0058 // Type or member is obsolete CipherAlgorithm = (CipherAlgorithmType)handshake.CipherType; CipherStrength = (int)handshake.CipherStrength; diff --git a/src/Servers/HttpSys/src/RequestProcessing/RequestContext.FeatureCollection.cs b/src/Servers/HttpSys/src/RequestProcessing/RequestContext.FeatureCollection.cs index 1c80f92febc2..a66e44d1484c 100644 --- a/src/Servers/HttpSys/src/RequestProcessing/RequestContext.FeatureCollection.cs +++ b/src/Servers/HttpSys/src/RequestProcessing/RequestContext.FeatureCollection.cs @@ -5,6 +5,7 @@ using System.Globalization; using System.IO.Pipelines; using System.Net; +using System.Net.Security; using System.Security.Authentication; using System.Security.Claims; using System.Security.Cryptography.X509Certificates; @@ -593,6 +594,8 @@ bool IHttpBodyControlFeature.AllowSynchronousIO SslProtocols ITlsHandshakeFeature.Protocol => Request.Protocol; + TlsCipherSuite? ITlsHandshakeFeature.NegotiatedCipherSuite => Request.NegotiatedCipherSuite; + #pragma warning disable SYSLIB0058 // Type or member is obsolete CipherAlgorithmType ITlsHandshakeFeature.CipherAlgorithm => Request.CipherAlgorithm; diff --git a/src/Servers/HttpSys/src/RequestProcessing/RequestContext.Log.cs b/src/Servers/HttpSys/src/RequestProcessing/RequestContext.Log.cs index d7766698bc41..0fce6896778b 100644 --- a/src/Servers/HttpSys/src/RequestProcessing/RequestContext.Log.cs +++ b/src/Servers/HttpSys/src/RequestProcessing/RequestContext.Log.cs @@ -23,5 +23,8 @@ private static partial class Log [LoggerMessage(LoggerEventIds.RequestParsingError, LogLevel.Debug, "Failed to invoke QueryTlsClientHello; RequestId: {RequestId}; Win32 Error code: {Win32Error}", EventName = "TlsClientHelloRetrieveError")] public static partial void TlsClientHelloRetrieveError(ILogger logger, ulong requestId, uint win32Error); + + [LoggerMessage(LoggerEventIds.QueryTlsCipherSuiteError, LogLevel.Debug, "Failed to invoke QueryTlsCipherSuite; RequestId: {RequestId}; Win32 Error code: {Win32Error}", EventName = "QueryTlsCipherSuiteError")] + public static partial void QueryTlsCipherSuiteError(ILogger logger, ulong requestId, uint win32Error); } } diff --git a/src/Servers/HttpSys/src/RequestProcessing/RequestContext.cs b/src/Servers/HttpSys/src/RequestProcessing/RequestContext.cs index eba7d33ff3b8..5aefb9df1cf3 100644 --- a/src/Servers/HttpSys/src/RequestProcessing/RequestContext.cs +++ b/src/Servers/HttpSys/src/RequestProcessing/RequestContext.cs @@ -1,6 +1,7 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System.Net.Security; using System.Runtime.InteropServices; using System.Security.Principal; using Microsoft.AspNetCore.Http; @@ -219,6 +220,45 @@ internal void ForceCancelRequest() } } + /// + /// Gets TLS cipher suite used for the request, if supported by the OS and http.sys. + /// + /// + /// null, if query of TlsCipherSuite is not supported or the query failed. + /// TlsCipherSuite value, if query is successful. + /// + internal unsafe TlsCipherSuite? GetTlsCipherSuite() + { + if (!HttpApi.SupportsQueryTlsCipherInfo) + { + return default; + } + + var requestId = PinsReleased ? Request.RequestId : RequestId; + + SecPkgContext_CipherInfo cipherInfo = default; + + var statusCode = HttpApi.HttpGetRequestProperty( + requestQueueHandle: Server.RequestQueue.Handle, + requestId, + propertyId: (HTTP_REQUEST_PROPERTY)14 /* HTTP_REQUEST_PROPERTY.HttpRequestPropertyTlsCipherInfo */, + qualifier: null, + qualifierSize: 0, + output: &cipherInfo, + outputSize: (uint)sizeof(SecPkgContext_CipherInfo), + bytesReturned: IntPtr.Zero, + overlapped: IntPtr.Zero); + + if (statusCode is ErrorCodes.ERROR_SUCCESS) + { + return checked((TlsCipherSuite)cipherInfo.dwCipherSuite); + } + + // OS supports querying TlsCipherSuite, but request failed. + Log.QueryTlsCipherSuiteError(Logger, requestId, statusCode); + return null; + } + /// /// Attempts to get the client hello message bytes from the http.sys. /// If successful writes the bytes into , and shows how many bytes were written in . diff --git a/src/Servers/IIS/IIS/samples/NativeIISSample/Startup.cs b/src/Servers/IIS/IIS/samples/NativeIISSample/Startup.cs index 0ff3b86369c6..e3559fa5b1e0 100644 --- a/src/Servers/IIS/IIS/samples/NativeIISSample/Startup.cs +++ b/src/Servers/IIS/IIS/samples/NativeIISSample/Startup.cs @@ -6,6 +6,7 @@ using System.Threading.Tasks; using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Connections.Features; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Hosting.Server; using Microsoft.AspNetCore.Hosting.Server.Features; @@ -52,6 +53,19 @@ public void Configure(IApplicationBuilder app) await context.Response.WriteAsync("ClientCert: " + context.Connection.ClientCertificate + Environment.NewLine); await context.Response.WriteAsync(Environment.NewLine); + var handshakeFeature = context.Features.Get(); + if (handshakeFeature is not null) + { + await context.Response.WriteAsync(Environment.NewLine); + await context.Response.WriteAsync("TLS Information:" + Environment.NewLine); + await context.Response.WriteAsync($"Protocol: {handshakeFeature.Protocol}" + Environment.NewLine); + + if (handshakeFeature.NegotiatedCipherSuite.HasValue) + { + await context.Response.WriteAsync($"Cipher Suite: {handshakeFeature.NegotiatedCipherSuite.Value}" + Environment.NewLine); + } + } + await context.Response.WriteAsync("User: " + context.User.Identity.Name + Environment.NewLine); if (_authSchemeProvider != null) { diff --git a/src/Servers/IIS/IIS/src/Core/IISHttpContext.cs b/src/Servers/IIS/IIS/src/Core/IISHttpContext.cs index 3ddc9315cf66..4a1417ed1b52 100644 --- a/src/Servers/IIS/IIS/src/Core/IISHttpContext.cs +++ b/src/Servers/IIS/IIS/src/Core/IISHttpContext.cs @@ -402,6 +402,8 @@ private void GetTlsHandshakeResults() { var handshake = GetTlsHandshake(); Protocol = (SslProtocols)handshake.Protocol; + + NegotiatedCipherSuite = GetTlsCipherSuite(); #pragma warning disable SYSLIB0058 // Type or member is obsolete CipherAlgorithm = (CipherAlgorithmType)handshake.CipherType; CipherStrength = (int)handshake.CipherStrength; @@ -415,6 +417,28 @@ private void GetTlsHandshakeResults() SniHostName = sni.Hostname.ToString(); } + private unsafe TlsCipherSuite? GetTlsCipherSuite() + { + SecPkgContext_CipherInfo cipherInfo = default; + + var statusCode = NativeMethods.HttpQueryRequestProperty( + RequestId, + (HTTP_REQUEST_PROPERTY)14 /* HTTP_REQUEST_PROPERTY.HttpRequestPropertyTlsCipherInfo */, + qualifier: null, + qualifierSize: 0, + output: &cipherInfo, + outputSize: (uint)sizeof(SecPkgContext_CipherInfo), + bytesReturned: null, + overlapped: IntPtr.Zero); + + if (statusCode == NativeMethods.HR_OK) + { + return checked((TlsCipherSuite)cipherInfo.dwCipherSuite); + } + + return default; + } + private unsafe HTTP_REQUEST_PROPERTY_SNI GetClientSni() { var buffer = new byte[HttpApiTypes.SniPropertySizeInBytes]; diff --git a/src/Shared/HttpSys/NativeInterop/SecPkgContext_CipherInfo.cs b/src/Shared/HttpSys/NativeInterop/SecPkgContext_CipherInfo.cs new file mode 100644 index 000000000000..1bba1c1afcef --- /dev/null +++ b/src/Shared/HttpSys/NativeInterop/SecPkgContext_CipherInfo.cs @@ -0,0 +1,29 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Runtime.InteropServices; + +namespace Microsoft.AspNetCore.HttpSys.Internal; + +// From Schannel.h +[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)] +internal unsafe struct SecPkgContext_CipherInfo +{ + private const int SZ_ALG_MAX_SIZE = 64; + + private readonly int dwVersion; + private readonly int dwProtocol; + public readonly int dwCipherSuite; + private readonly int dwBaseCipherSuite; + private fixed char szCipherSuite[SZ_ALG_MAX_SIZE]; + private fixed char szCipher[SZ_ALG_MAX_SIZE]; + private readonly int dwCipherLen; + private readonly int dwCipherBlockLen; // in bytes + private fixed char szHash[SZ_ALG_MAX_SIZE]; + private readonly int dwHashLen; + private fixed char szExchange[SZ_ALG_MAX_SIZE]; + private readonly int dwMinExchangeLen; + private readonly int dwMaxExchangeLen; + private fixed char szCertificate[SZ_ALG_MAX_SIZE]; + private readonly int dwKeyType; +}