Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 10 additions & 1 deletion src/Servers/HttpSys/samples/TlsFeaturesObserve/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -30,6 +31,7 @@
{
var connectionFeature = context.Features.GetRequiredFeature<IHttpConnectionFeature>();
var httpSysPropFeature = context.Features.GetRequiredFeature<IHttpSysRequestPropertyFeature>();
var tlsHandshakeFeature = context.Features.GetRequiredFeature<ITlsHandshakeFeature>();

// first time invocation to find out required size
var success = httpSysPropFeature.TryGetTlsClientHello(Array.Empty<byte>(), out var bytesReturned);
Expand All @@ -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);
});

Expand Down
1 change: 1 addition & 0 deletions src/Servers/HttpSys/src/LoggerEventIds.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
2 changes: 2 additions & 0 deletions src/Servers/HttpSys/src/NativeInterop/HttpApi.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand All @@ -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;
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -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.Server.HttpSys.NativeInterop.Types;

// 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;
}
5 changes: 5 additions & 0 deletions src/Servers/HttpSys/src/RequestProcessing/Request.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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; }

Expand All @@ -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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
}
42 changes: 42 additions & 0 deletions src/Servers/HttpSys/src/RequestProcessing/RequestContext.cs
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
// 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;
using Microsoft.AspNetCore.HttpSys.Internal;
using Microsoft.AspNetCore.Server.HttpSys.NativeInterop.Types;
using Microsoft.AspNetCore.WebUtilities;
using Microsoft.Extensions.Logging;
using Windows.Win32;
Expand Down Expand Up @@ -219,6 +221,46 @@ internal void ForceCancelRequest()
}
}

/// <summary>
/// Gets TLS cipher suite used for the request, if supported by the OS and http.sys.
/// </summary>
/// <returns>
/// null, if query of TlsCipherSuite is not supported or the query failed.
/// TlsCipherSuite value, if query is successful.
/// </returns>
internal unsafe TlsCipherSuite? GetTlsCipherSuite()
{
if (!HttpApi.SupportsQueryTlsCipherInfo)
{
return default;
}

var requestId = PinsReleased ? Request.RequestId : RequestId;

SecPkgContext_CipherInfo cipherInfo = default;
uint bytesReturned = 0;

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)(&bytesReturned),
overlapped: IntPtr.Zero);

if (statusCode is ErrorCodes.ERROR_SUCCESS)
{
return (TlsCipherSuite)cipherInfo.dwCipherSuite;
}

// OS supports querying TlsCipherSuite, but request failed.
Log.QueryTlsCipherSuiteError(Logger, requestId, statusCode);
return null;
}

/// <summary>
/// Attempts to get the client hello message bytes from the http.sys.
/// If successful writes the bytes into <paramref name="destination"/>, and shows how many bytes were written in <paramref name="bytesReturned"/>.
Expand Down
30 changes: 30 additions & 0 deletions src/Servers/IIS/IIS/src/Core/IISHttpContext.cs
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
using Microsoft.AspNetCore.Http.Features;
using Microsoft.AspNetCore.HttpSys.Internal;
using Microsoft.AspNetCore.Server.IIS.Core.IO;
using Microsoft.AspNetCore.Server.IIS.Core.Native;
using Microsoft.AspNetCore.Shared;
using Microsoft.AspNetCore.WebUtilities;
using Microsoft.Extensions.Logging;
Expand Down Expand Up @@ -402,6 +403,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;
Expand All @@ -415,6 +418,33 @@ private void GetTlsHandshakeResults()
SniHostName = sni.Hostname.ToString();
}

private unsafe TlsCipherSuite? GetTlsCipherSuite()
{
var size = sizeof(SecPkgContext_CipherInfo);
var buffer = new byte[size];

fixed (byte* pBuffer = buffer)
{
var statusCode = NativeMethods.HttpQueryRequestProperty(
RequestId,
(HTTP_REQUEST_PROPERTY)14 /* HTTP_REQUEST_PROPERTY.HttpRequestPropertyTlsCipherInfo */,
qualifier: null,
qualifierSize: 0,
(void*)pBuffer,
(uint)buffer.Length,
bytesReturned: null,
IntPtr.Zero);

if (statusCode == NativeMethods.HR_OK)
{
var cipherInfo = Marshal.PtrToStructure<SecPkgContext_CipherInfo>((IntPtr)pBuffer);
return (TlsCipherSuite)cipherInfo.dwCipherSuite;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pretty sure you have tested it and it returned the expected result. But just to be sure, the .h file uses a DWORD for this property, so I would assume an uint in c#. It's mapped to an int right now and it's fine as it's the same size. However I am not sure about the cast from int to the TlsCipherSuite enum. Do you know if a memory representation of a "positive value" DWORD will be fine when represented as an int?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I asked Copilot and it told me both will be represented the same way below 2,147,483,647, so this is fine. (actual used max value is 53253 for TlsCipherSuite)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think you are right, but I simply found this type definition in the dotnet/runtime: SecPkgContext_CipherInfo

where field is defined with int:
https://github.com/dotnet/runtime/blob/eeb1eae5038a82243d4675c37b6449cac4030207/src/libraries/Common/src/Interop/Windows/SChannel/SecPkgContext_CipherInfo.cs#L17

Now I think it should be uint, but probably does not matter below 2,147,483,647 value as you have already explained.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

SChannel transfers it as a DWORD, but it's actually a ushort (it's only 2 bytes on the wire).

The .NET TlsCipherSuite enum is : ushort. So the cast is just as sketchy with uint as it is with int, though in reality they'll both always work.

If you wanted to be paranoid you could do it as a checked cast return checked((TlsCipherSuite)cipherInfo.dwCipherSuite);. That won't check if the enum value is defined (which is fine), but it will fail if it exceeds ushort.MaxValue (or if the interpreted dwCipherSuite value is negative).

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

added paranoid check :)

}

return default;
}
}

private unsafe HTTP_REQUEST_PROPERTY_SNI GetClientSni()
{
var buffer = new byte[HttpApiTypes.SniPropertySizeInBytes];
Expand Down
30 changes: 30 additions & 0 deletions src/Servers/IIS/IIS/src/Core/Native/SecPkgContext_CipherInfo.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
// 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.Server.IIS.Core.Native;

// 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;
}

Loading