From 21c91ef4e6fb8172dc39aa0e00eb9937d0f76aff Mon Sep 17 00:00:00 2001 From: wfurt Date: Thu, 24 Sep 2026 18:12:52 -0700 Subject: [PATCH] Prototype TLS signature family selection API Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Security/TlsSignatureAlgorithmHelper.cs | 180 ++++++++++++++++++ .../src/System.Net.Quic.csproj | 1 + .../src/System/Net/Quic/QuicListener.cs | 12 +- .../FunctionalTests/QuicListenerTests.cs | 20 ++ .../ref/System.Net.Security.cs | 12 ++ .../src/System.Net.Security.csproj | 2 + .../System/Net/Security/SslClientHelloInfo.cs | 63 ++++++ .../src/System/Net/Security/SslStream.IO.cs | 10 +- .../src/System/Net/Security/TlsFrameHelper.cs | 12 +- .../src/System/Net/Security/TlsSession.cs | 12 +- .../ServerAsyncAuthenticateTest.cs | 5 + .../System.Net.Security.Tests.csproj | 2 + .../FunctionalTests/TlsFrameHelperTests.cs | 161 +++++++++++++++- .../tests/FunctionalTests/TlsSessionTests.cs | 3 + .../System.Net.Security.Unit.Tests.csproj | 2 + 15 files changed, 489 insertions(+), 8 deletions(-) create mode 100644 src/libraries/Common/src/System/Net/Security/TlsSignatureAlgorithmHelper.cs diff --git a/src/libraries/Common/src/System/Net/Security/TlsSignatureAlgorithmHelper.cs b/src/libraries/Common/src/System/Net/Security/TlsSignatureAlgorithmHelper.cs new file mode 100644 index 00000000000000..b2e3bdb07a82cd --- /dev/null +++ b/src/libraries/Common/src/System/Net/Security/TlsSignatureAlgorithmHelper.cs @@ -0,0 +1,180 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Buffers.Binary; + +namespace System.Net.Security +{ + internal static class TlsSignatureAlgorithmHelper + { + private const ushort SignatureAlgorithmsExtension = 13; + + internal static bool TryGetFamiliesFromClientHello( + ReadOnlySpan clientHello, + out TlsSignatureAlgorithmFamilies signatureAlgorithmFamilies) + { + // https://www.rfc-editor.org/rfc/rfc8446.html#section-4.1.2 + const int HandshakeHeaderLength = 4; + const int ClientHelloFixedLength = 2 + 32; + + signatureAlgorithmFamilies = TlsSignatureAlgorithmFamilies.None; + + if (clientHello.Length < HandshakeHeaderLength + ClientHelloFixedLength || + clientHello[0] != 1) + { + return false; + } + + int helloLength = (clientHello[1] << 16) | (clientHello[2] << 8) | clientHello[3]; + if (helloLength != clientHello.Length - HandshakeHeaderLength) + { + return false; + } + + ReadOnlySpan hello = clientHello.Slice(HandshakeHeaderLength + ClientHelloFixedLength); + if (!TrySkipOpaque1(ref hello) || + !TrySkipOpaque2(ref hello) || + !TrySkipOpaque1(ref hello)) + { + return false; + } + + if (hello.IsEmpty) + { + return true; + } + + if (hello.Length < sizeof(ushort)) + { + return false; + } + + int extensionsLength = BinaryPrimitives.ReadUInt16BigEndian(hello); + hello = hello.Slice(sizeof(ushort)); + if (extensionsLength != hello.Length) + { + return false; + } + + while (hello.Length >= 2 * sizeof(ushort)) + { + ushort extensionType = BinaryPrimitives.ReadUInt16BigEndian(hello); + int extensionLength = BinaryPrimitives.ReadUInt16BigEndian(hello.Slice(sizeof(ushort))); + hello = hello.Slice(2 * sizeof(ushort)); + + if (extensionLength > hello.Length) + { + return false; + } + + if (extensionType == SignatureAlgorithmsExtension) + { + return TryGetFamiliesFromExtension( + hello.Slice(0, extensionLength), + out signatureAlgorithmFamilies); + } + + hello = hello.Slice(extensionLength); + } + + return hello.IsEmpty; + } + + internal static bool TryGetFamiliesFromExtension( + ReadOnlySpan extensionData, + out TlsSignatureAlgorithmFamilies signatureAlgorithmFamilies) + { + // https://www.rfc-editor.org/rfc/rfc8446.html#section-4.2.3 + signatureAlgorithmFamilies = TlsSignatureAlgorithmFamilies.None; + + if (extensionData.Length < sizeof(ushort)) + { + return false; + } + + int signatureAlgorithmsLength = BinaryPrimitives.ReadUInt16BigEndian(extensionData); + ReadOnlySpan signatureAlgorithms = extensionData.Slice(sizeof(ushort)); + + if (signatureAlgorithmsLength == 0 || + signatureAlgorithmsLength != signatureAlgorithms.Length || + (signatureAlgorithmsLength & 1) != 0) + { + return false; + } + + while (!signatureAlgorithms.IsEmpty) + { + ushort signatureScheme = BinaryPrimitives.ReadUInt16BigEndian(signatureAlgorithms); + signatureAlgorithms = signatureAlgorithms.Slice(sizeof(ushort)); + + signatureAlgorithmFamilies |= signatureScheme switch + { + 0x0201 or // rsa_pkcs1_sha1 + 0x0401 or // rsa_pkcs1_sha256 + 0x0501 or // rsa_pkcs1_sha384 + 0x0601 or // rsa_pkcs1_sha512 + 0x0804 or // rsa_pss_rsae_sha256 + 0x0805 or // rsa_pss_rsae_sha384 + 0x0806 or // rsa_pss_rsae_sha512 + 0x0809 or // rsa_pss_pss_sha256 + 0x080A or // rsa_pss_pss_sha384 + 0x080B => // rsa_pss_pss_sha512 + TlsSignatureAlgorithmFamilies.Rsa, + + 0x0203 or // ecdsa_sha1 + 0x0403 or // ecdsa_secp256r1_sha256 + 0x0503 or // ecdsa_secp384r1_sha384 + 0x0603 or // ecdsa_secp521r1_sha512 + 0x081A or // ecdsa_brainpoolP256r1tls13_sha256 + 0x081B or // ecdsa_brainpoolP384r1tls13_sha384 + 0x081C => // ecdsa_brainpoolP512r1tls13_sha512 + TlsSignatureAlgorithmFamilies.ECDsa, + + 0x0807 or // ed25519 + 0x0808 => // ed448 + TlsSignatureAlgorithmFamilies.EdDsa, + + 0x0904 or // mldsa44 + 0x0905 or // mldsa65 + 0x0906 => // mldsa87 + TlsSignatureAlgorithmFamilies.MLDsa, + + >= 0x0911 and <= 0x091C => + TlsSignatureAlgorithmFamilies.SlhDsa, + + _ => TlsSignatureAlgorithmFamilies.None, + }; + } + + return true; + } + + private static bool TrySkipOpaque1(ref ReadOnlySpan data) + { + if (data.IsEmpty || data.Length < data[0] + 1) + { + return false; + } + + data = data.Slice(data[0] + 1); + return true; + } + + private static bool TrySkipOpaque2(ref ReadOnlySpan data) + { + if (data.Length < sizeof(ushort)) + { + return false; + } + + int length = BinaryPrimitives.ReadUInt16BigEndian(data); + if (data.Length < sizeof(ushort) + length) + { + return false; + } + + data = data.Slice(sizeof(ushort) + length); + return true; + } + } +} diff --git a/src/libraries/System.Net.Quic/src/System.Net.Quic.csproj b/src/libraries/System.Net.Quic/src/System.Net.Quic.csproj index 50097783501999..6d0c194cf3260b 100644 --- a/src/libraries/System.Net.Quic/src/System.Net.Quic.csproj +++ b/src/libraries/System.Net.Quic/src/System.Net.Quic.csproj @@ -34,6 +34,7 @@ + diff --git a/src/libraries/System.Net.Quic/src/System/Net/Quic/QuicListener.cs b/src/libraries/System.Net.Quic/src/System/Net/Quic/QuicListener.cs index c17c357319923c..8d19973773ab87 100644 --- a/src/libraries/System.Net.Quic/src/System/Net/Quic/QuicListener.cs +++ b/src/libraries/System.Net.Quic/src/System/Net/Quic/QuicListener.cs @@ -343,7 +343,17 @@ private unsafe int HandleEventNewConnection(ref NEW_CONNECTION_DATA data) NetEventSource.Info(this, $"{this} New inbound connection {connection}."); } - SslClientHelloInfo clientHello = new SslClientHelloInfo(data.Info->ServerNameLength > 0 ? Encoding.UTF8.GetString((byte*)data.Info->ServerName, data.Info->ServerNameLength) : "", SslProtocols.Tls13); + ReadOnlySpan cryptoBuffer = new ReadOnlySpan( + data.Info->CryptoBuffer, + checked((int)data.Info->CryptoBufferLength)); + TlsSignatureAlgorithmHelper.TryGetFamiliesFromClientHello( + cryptoBuffer, + out TlsSignatureAlgorithmFamilies signatureAlgorithmFamilies); + + SslClientHelloInfo clientHello = new SslClientHelloInfo( + data.Info->ServerNameLength > 0 ? Encoding.UTF8.GetString((byte*)data.Info->ServerName, data.Info->ServerNameLength) : "", + SslProtocols.Tls13, + signatureAlgorithmFamilies); // Kicks off the rest of the handshake in the background, the process itself will enqueue the result in the accept queue. StartConnectionHandshake(connection, clientHello); diff --git a/src/libraries/System.Net.Quic/tests/FunctionalTests/QuicListenerTests.cs b/src/libraries/System.Net.Quic/tests/FunctionalTests/QuicListenerTests.cs index 74df2c638ebd07..0e1ca2b9de50ab 100644 --- a/src/libraries/System.Net.Quic/tests/FunctionalTests/QuicListenerTests.cs +++ b/src/libraries/System.Net.Quic/tests/FunctionalTests/QuicListenerTests.cs @@ -215,6 +215,26 @@ public async Task AcceptConnectionAsync_ClientCancels_FiresOptionCallbackCancell Assert.Equal(SR.Format(SR.net_quic_handshake_timeout, clientOptions.HandshakeTimeout), exception.Message); } + [Fact] + public async Task ConnectionOptionsCallback_ReceivesSignatureAlgorithmFamilies() + { + TlsSignatureAlgorithmFamilies observedSignatureAlgorithmFamilies = TlsSignatureAlgorithmFamilies.None; + QuicListenerOptions listenerOptions = CreateQuicListenerOptions(); + listenerOptions.ConnectionOptionsCallback = (_, hello, _) => + { + observedSignatureAlgorithmFamilies = hello.SignatureAlgorithmFamilies; + return ValueTask.FromResult(CreateQuicServerOptions()); + }; + + (QuicConnection clientConnection, QuicConnection serverConnection) = + await CreateConnectedQuicConnection(clientOptions: null, listenerOptions); + await using (clientConnection) + await using (serverConnection) + { + Assert.True((observedSignatureAlgorithmFamilies & TlsSignatureAlgorithmFamilies.Rsa) != 0); + } + } + [Fact] public async Task AcceptConnectionAsync_ListenerDisposed_Throws() { diff --git a/src/libraries/System.Net.Security/ref/System.Net.Security.cs b/src/libraries/System.Net.Security/ref/System.Net.Security.cs index ccbcb839dde5b2..ec01042dc33e97 100644 --- a/src/libraries/System.Net.Security/ref/System.Net.Security.cs +++ b/src/libraries/System.Net.Security/ref/System.Net.Security.cs @@ -221,7 +221,9 @@ public readonly partial struct SslClientHelloInfo private readonly object _dummy; private readonly int _dummyPrimitive; public SslClientHelloInfo(string serverName, System.Security.Authentication.SslProtocols sslProtocols) { throw null; } + public SslClientHelloInfo(string serverName, System.Security.Authentication.SslProtocols sslProtocols, System.Net.Security.TlsSignatureAlgorithmFamilies signatureAlgorithmFamilies) { throw null; } public string ServerName { get { throw null; } } + public System.Net.Security.TlsSignatureAlgorithmFamilies SignatureAlgorithmFamilies { get { throw null; } } public System.Security.Authentication.SslProtocols SslProtocols { get { throw null; } } } public partial class SslServerAuthenticationOptions @@ -742,6 +744,16 @@ public TlsBufferSession() { } public System.Net.Security.TlsOperationStatus DrainPendingOutput(System.Span destination, out int bytesWritten) { throw null; } public System.Net.Security.TlsOperationStatus RequestClientCertificate(System.Span destination, out int bytesWritten) { throw null; } } + [System.FlagsAttribute] + public enum TlsSignatureAlgorithmFamilies + { + None = 0, + Rsa = 1, + ECDsa = 2, + EdDsa = 4, + MLDsa = 8, + SlhDsa = 16, + } [System.Diagnostics.CodeAnalysis.ExperimentalAttribute("SYSLIB5007", UrlFormat = "https://aka.ms/dotnet-warnings/{0}")] public sealed partial class TlsSocketSession : System.Net.Security.TlsSession { diff --git a/src/libraries/System.Net.Security/src/System.Net.Security.csproj b/src/libraries/System.Net.Security/src/System.Net.Security.csproj index 1e80c61d76527c..5aa232ca982ab2 100644 --- a/src/libraries/System.Net.Security/src/System.Net.Security.csproj +++ b/src/libraries/System.Net.Security/src/System.Net.Security.csproj @@ -92,6 +92,8 @@ + diff --git a/src/libraries/System.Net.Security/src/System/Net/Security/SslClientHelloInfo.cs b/src/libraries/System.Net.Security/src/System/Net/Security/SslClientHelloInfo.cs index 13d6ce7a462e2a..1fcd4f22a0424f 100644 --- a/src/libraries/System.Net.Security/src/System/Net/Security/SslClientHelloInfo.cs +++ b/src/libraries/System.Net.Security/src/System/Net/Security/SslClientHelloInfo.cs @@ -5,6 +5,43 @@ namespace System.Net.Security { + /// + /// Specifies families of TLS signature algorithms. + /// + [Flags] + public enum TlsSignatureAlgorithmFamilies + { + /// + /// No signature algorithm family is specified. + /// + None = 0, + + /// + /// The RSA signature algorithm family. + /// + Rsa = 1 << 0, + + /// + /// The ECDSA signature algorithm family. + /// + ECDsa = 1 << 1, + + /// + /// The EdDSA signature algorithm family. + /// + EdDsa = 1 << 2, + + /// + /// The ML-DSA signature algorithm family. + /// + MLDsa = 1 << 3, + + /// + /// The SLH-DSA signature algorithm family. + /// + SlhDsa = 1 << 4, + } + /// /// This struct contains information from received TLS Client Hello frame. /// @@ -13,10 +50,36 @@ public readonly struct SslClientHelloInfo public readonly string ServerName { get; } public readonly SslProtocols SslProtocols { get; } + /// + /// Gets the signature algorithm families advertised by the client. + /// + /// + /// This property indicates broad compatibility with a server certificate's public key. + /// It does not guarantee that every certificate or certificate chain using an advertised + /// family will be accepted by the client. The value is + /// when this information is unavailable or the client advertises no recognized family. + /// + public readonly TlsSignatureAlgorithmFamilies SignatureAlgorithmFamilies { get; } + public SslClientHelloInfo(string serverName, SslProtocols sslProtocols) + : this(serverName, sslProtocols, TlsSignatureAlgorithmFamilies.None) + { + } + + /// + /// Initializes a new instance of the struct. + /// + /// The server name requested by the client. + /// A bitwise combination of the enumeration values that specifies the TLS protocols advertised by the client. + /// A bitwise combination of the enumeration values that specifies the signature algorithm families advertised by the client. + public SslClientHelloInfo( + string serverName, + SslProtocols sslProtocols, + TlsSignatureAlgorithmFamilies signatureAlgorithmFamilies) { ServerName = serverName; SslProtocols = sslProtocols; + SignatureAlgorithmFamilies = signatureAlgorithmFamilies; } } } diff --git a/src/libraries/System.Net.Security/src/System/Net/Security/SslStream.IO.cs b/src/libraries/System.Net.Security/src/System/Net/Security/SslStream.IO.cs index bc79976d479d5b..29571068b567e4 100644 --- a/src/libraries/System.Net.Security/src/System/Net/Security/SslStream.IO.cs +++ b/src/libraries/System.Net.Security/src/System/Net/Security/SslStream.IO.cs @@ -487,8 +487,9 @@ private async ValueTask ReceiveHandshakeFrameAsync(Cancellation if (_sslAuthenticationOptions.ServerOptionDelegate != null) { - // We need to process supported versions extension to pass it to user callback. - options |= TlsFrameHelper.ProcessingOptions.Versions; + // Process ClientHello information exposed to the user callback. + options |= TlsFrameHelper.ProcessingOptions.Versions | + TlsFrameHelper.ProcessingOptions.SignatureAlgorithms; } // Process SNI from Client Hello message @@ -508,7 +509,10 @@ private async ValueTask ReceiveHandshakeFrameAsync(Cancellation if (_sslAuthenticationOptions.ServerOptionDelegate != null) { SslServerAuthenticationOptions userOptions = - await _sslAuthenticationOptions.ServerOptionDelegate(this, new SslClientHelloInfo(_sslAuthenticationOptions.TargetHost, _lastFrame.SupportedVersions), + await _sslAuthenticationOptions.ServerOptionDelegate(this, new SslClientHelloInfo( + _sslAuthenticationOptions.TargetHost, + _lastFrame.SupportedVersions, + _lastFrame.SignatureAlgorithmFamilies), _sslAuthenticationOptions.UserState, cancellationToken).ConfigureAwait(false); _sslAuthenticationOptions.UpdateOptions(userOptions); } diff --git a/src/libraries/System.Net.Security/src/System/Net/Security/TlsFrameHelper.cs b/src/libraries/System.Net.Security/src/System/Net/Security/TlsFrameHelper.cs index 8ede2f76f0e3fe..afa806b98f0c76 100644 --- a/src/libraries/System.Net.Security/src/System/Net/Security/TlsFrameHelper.cs +++ b/src/libraries/System.Net.Security/src/System/Net/Security/TlsFrameHelper.cs @@ -78,6 +78,7 @@ internal enum ExtensionType : ushort TrustedCaKeys = 3, TruncatedHmac = 4, CertificateStatusRequest = 5, + SignatureAlgorithms = 13, ApplicationProtocols = 16, SupportedVersions = 43 } @@ -102,6 +103,7 @@ public enum ProcessingOptions ApplicationProtocol = 0x2, Versions = 0x4, RawApplicationProtocol = 0x8, + SignatureAlgorithms = 0x10, } [Flags] @@ -118,6 +120,7 @@ public struct TlsFrameInfo public TlsFrameHeader Header; public TlsHandshakeType HandshakeType; public SslProtocols SupportedVersions; + public TlsSignatureAlgorithmFamilies SignatureAlgorithmFamilies; public string TargetName; public ApplicationProtocolInfo ApplicationProtocols; public TlsAlertDescription AlertDescription; @@ -129,7 +132,7 @@ public override string ToString() { if (HandshakeType == TlsHandshakeType.ClientHello) { - return $"{Header.Version}:{HandshakeType}[{Header.Length}] TargetName='{TargetName}' SupportedVersion='{SupportedVersions}' ApplicationProtocols='{ApplicationProtocols}'"; + return $"{Header.Version}:{HandshakeType}[{Header.Length}] TargetName='{TargetName}' SupportedVersion='{SupportedVersions}' ApplicationProtocols='{ApplicationProtocols}' SignatureAlgorithmFamilies='{SignatureAlgorithmFamilies}'"; } else if (HandshakeType == TlsHandshakeType.ServerHello) { @@ -536,6 +539,13 @@ private static bool TryParseHelloExtensions(ReadOnlySpan extensions, ref T info.SupportedVersions |= versions; } + else if (extensionType == ExtensionType.SignatureAlgorithms && (options & ProcessingOptions.SignatureAlgorithms) != 0) + { + if (TlsSignatureAlgorithmHelper.TryGetFamiliesFromExtension(extensionData, out TlsSignatureAlgorithmFamilies signatureAlgorithmFamilies)) + { + info.SignatureAlgorithmFamilies |= signatureAlgorithmFamilies; + } + } else if (extensionType == ExtensionType.ApplicationProtocols && (options & (ProcessingOptions.ApplicationProtocol | ProcessingOptions.RawApplicationProtocol)) != 0) { diff --git a/src/libraries/System.Net.Security/src/System/Net/Security/TlsSession.cs b/src/libraries/System.Net.Security/src/System/Net/Security/TlsSession.cs index efc182fb6b3c53..6d487cdda33a97 100644 --- a/src/libraries/System.Net.Security/src/System/Net/Security/TlsSession.cs +++ b/src/libraries/System.Net.Security/src/System/Net/Security/TlsSession.cs @@ -1674,7 +1674,12 @@ private void ThrowIfContextNotSet() { frameLength = 0; TlsFrameHelper.TlsFrameInfo frameInfo = default; - if (!TlsFrameHelper.TryGetFrameInfo(input, ref frameInfo)) + if (!TlsFrameHelper.TryGetFrameInfo( + input, + ref frameInfo, + TlsFrameHelper.ProcessingOptions.ServerName | + TlsFrameHelper.ProcessingOptions.Versions | + TlsFrameHelper.ProcessingOptions.SignatureAlgorithms)) { return null; } @@ -1685,7 +1690,10 @@ private void ThrowIfContextNotSet() } frameLength = frameInfo.Header.Length; - return new SslClientHelloInfo(frameInfo.TargetName ?? string.Empty, frameInfo.SupportedVersions); + return new SslClientHelloInfo( + frameInfo.TargetName ?? string.Empty, + frameInfo.SupportedVersions, + frameInfo.SignatureAlgorithmFamilies); } // Server-side SNI + certificate selection. Parses the ClientHello to diff --git a/src/libraries/System.Net.Security/tests/FunctionalTests/ServerAsyncAuthenticateTest.cs b/src/libraries/System.Net.Security/tests/FunctionalTests/ServerAsyncAuthenticateTest.cs index 949c0486e67f8e..0ab4e76ea00e2f 100644 --- a/src/libraries/System.Net.Security/tests/FunctionalTests/ServerAsyncAuthenticateTest.cs +++ b/src/libraries/System.Net.Security/tests/FunctionalTests/ServerAsyncAuthenticateTest.cs @@ -126,6 +126,7 @@ public async Task ServerAsyncAuthenticate_SniSetVersion_Success(SslProtocols ver { Assert.Equal(server, stream); Assert.Equal(clientOptions.TargetHost, clientHelloInfo.ServerName); + Assert.True((clientHelloInfo.SignatureAlgorithmFamilies & TlsSignatureAlgorithmFamilies.Rsa) != 0); return new ValueTask(serverOptions); }, null, CancellationToken.None); @@ -153,6 +154,7 @@ public async Task ServerAsyncAuthenticate_SniCallback_ReceivesSupportedVersionsF clientOptions.RemoteCertificateValidationCallback = (sender, certificate, chain, sslPolicyErrors) => true; SslProtocols observedProtocols = SslProtocols.None; + TlsSignatureAlgorithmFamilies observedSignatureAlgorithmFamilies = TlsSignatureAlgorithmFamilies.None; (SslStream client, SslStream server) = TestHelper.GetConnectedSslStreams(); using (client) @@ -163,6 +165,7 @@ public async Task ServerAsyncAuthenticate_SniCallback_ReceivesSupportedVersionsF (stream, clientHelloInfo, userState, cancellationToken) => { observedProtocols = clientHelloInfo.SslProtocols; + observedSignatureAlgorithmFamilies = clientHelloInfo.SignatureAlgorithmFamilies; return new ValueTask(serverOptions); }, null, CancellationToken.None); @@ -172,6 +175,8 @@ public async Task ServerAsyncAuthenticate_SniCallback_ReceivesSupportedVersionsF Assert.True((observedProtocols & SslProtocols.Tls13) == SslProtocols.Tls13, $"Expected SslClientHelloInfo.SslProtocols to include Tls13, got '{observedProtocols}'."); + Assert.True((observedSignatureAlgorithmFamilies & TlsSignatureAlgorithmFamilies.Rsa) != 0, + $"Expected SslClientHelloInfo.SignatureAlgorithmFamilies to include Rsa, got '{observedSignatureAlgorithmFamilies}'."); } private async Task FailedTask() diff --git a/src/libraries/System.Net.Security/tests/FunctionalTests/System.Net.Security.Tests.csproj b/src/libraries/System.Net.Security/tests/FunctionalTests/System.Net.Security.Tests.csproj index 8cca9ec340c6a2..7b216a7da12beb 100644 --- a/src/libraries/System.Net.Security/tests/FunctionalTests/System.Net.Security.Tests.csproj +++ b/src/libraries/System.Net.Security/tests/FunctionalTests/System.Net.Security.Tests.csproj @@ -115,6 +115,8 @@ Link="Common\System\Net\Security\SslClientAuthenticationOptionsExtensions.cs" /> + diff --git a/src/libraries/System.Net.Security/tests/FunctionalTests/TlsFrameHelperTests.cs b/src/libraries/System.Net.Security/tests/FunctionalTests/TlsFrameHelperTests.cs index acb041e2e6ba9f..1111a4bb643a0d 100644 --- a/src/libraries/System.Net.Security/tests/FunctionalTests/TlsFrameHelperTests.cs +++ b/src/libraries/System.Net.Security/tests/FunctionalTests/TlsFrameHelperTests.cs @@ -57,7 +57,8 @@ private void InvalidClientHello(byte[] clientHello, int id, bool shouldPass) private const TlsFrameHelper.ProcessingOptions AllExtensions = TlsFrameHelper.ProcessingOptions.ServerName | TlsFrameHelper.ProcessingOptions.ApplicationProtocol | - TlsFrameHelper.ProcessingOptions.Versions; + TlsFrameHelper.ProcessingOptions.Versions | + TlsFrameHelper.ProcessingOptions.SignatureAlgorithms; [Fact] public void TlsFrameHelper_ValidData_Ok() @@ -69,6 +70,7 @@ public void TlsFrameHelper_ValidData_Ok() Assert.Equal(208, info.Header.Length); Assert.Equal(SslProtocols.Tls12, info.SupportedVersions); Assert.Equal(TlsFrameHelper.ApplicationProtocolInfo.None, info.ApplicationProtocols); + Assert.Equal(TlsSignatureAlgorithmFamilies.Rsa | TlsSignatureAlgorithmFamilies.ECDsa, info.SignatureAlgorithmFamilies); } [Fact] @@ -82,6 +84,7 @@ public void TlsFrameHelper_Tls12ClientHello_Ok() Assert.Equal(SslProtocols.Tls | SslProtocols.Tls12, info.SupportedVersions); #pragma warning restore SYSLIB0039 Assert.Equal(TlsFrameHelper.ApplicationProtocolInfo.Http11 | TlsFrameHelper.ApplicationProtocolInfo.Http2, info.ApplicationProtocols); + Assert.Equal(TlsSignatureAlgorithmFamilies.Rsa | TlsSignatureAlgorithmFamilies.ECDsa, info.SignatureAlgorithmFamilies); } [Fact] @@ -95,6 +98,117 @@ public void TlsFrameHelper_Tls13ClientHello_Ok() Assert.Equal(SslProtocols.Tls | SslProtocols.Tls11 | SslProtocols.Tls12 | SslProtocols.Tls13, info.SupportedVersions); #pragma warning restore SYSLIB0039 Assert.Equal(TlsFrameHelper.ApplicationProtocolInfo.Other, info.ApplicationProtocols); + Assert.Equal(TlsSignatureAlgorithmFamilies.Rsa | TlsSignatureAlgorithmFamilies.ECDsa, info.SignatureAlgorithmFamilies); + } + + public static IEnumerable SignatureAlgorithmFamilyData() + { + yield return new object[] { 0x0201, TlsSignatureAlgorithmFamilies.Rsa }; + yield return new object[] { 0x0401, TlsSignatureAlgorithmFamilies.Rsa }; + yield return new object[] { 0x0501, TlsSignatureAlgorithmFamilies.Rsa }; + yield return new object[] { 0x0601, TlsSignatureAlgorithmFamilies.Rsa }; + yield return new object[] { 0x0804, TlsSignatureAlgorithmFamilies.Rsa }; + yield return new object[] { 0x0805, TlsSignatureAlgorithmFamilies.Rsa }; + yield return new object[] { 0x0806, TlsSignatureAlgorithmFamilies.Rsa }; + yield return new object[] { 0x0809, TlsSignatureAlgorithmFamilies.Rsa }; + yield return new object[] { 0x080A, TlsSignatureAlgorithmFamilies.Rsa }; + yield return new object[] { 0x080B, TlsSignatureAlgorithmFamilies.Rsa }; + yield return new object[] { 0x0203, TlsSignatureAlgorithmFamilies.ECDsa }; + yield return new object[] { 0x0403, TlsSignatureAlgorithmFamilies.ECDsa }; + yield return new object[] { 0x0503, TlsSignatureAlgorithmFamilies.ECDsa }; + yield return new object[] { 0x0603, TlsSignatureAlgorithmFamilies.ECDsa }; + yield return new object[] { 0x081A, TlsSignatureAlgorithmFamilies.ECDsa }; + yield return new object[] { 0x081B, TlsSignatureAlgorithmFamilies.ECDsa }; + yield return new object[] { 0x081C, TlsSignatureAlgorithmFamilies.ECDsa }; + yield return new object[] { 0x0807, TlsSignatureAlgorithmFamilies.EdDsa }; + yield return new object[] { 0x0808, TlsSignatureAlgorithmFamilies.EdDsa }; + yield return new object[] { 0x0904, TlsSignatureAlgorithmFamilies.MLDsa }; + yield return new object[] { 0x0905, TlsSignatureAlgorithmFamilies.MLDsa }; + yield return new object[] { 0x0906, TlsSignatureAlgorithmFamilies.MLDsa }; + + for (int scheme = 0x0911; scheme <= 0x091C; scheme++) + { + yield return new object[] { scheme, TlsSignatureAlgorithmFamilies.SlhDsa }; + } + } + + [Theory] + [MemberData(nameof(SignatureAlgorithmFamilyData))] + public void TlsFrameHelper_ClientHelloSignatureAlgorithm_MapsFamily(int signatureScheme, TlsSignatureAlgorithmFamilies expected) + { + byte[] clientHello = CreateClientHelloWithSignatureAlgorithms(checked((ushort)signatureScheme)); + TlsFrameHelper.TlsFrameInfo info = default; + + Assert.True(TlsFrameHelper.TryGetFrameInfo(clientHello, ref info, AllExtensions)); + Assert.Equal(expected, info.SignatureAlgorithmFamilies); + } + + [Fact] + public void TlsFrameHelper_ClientHelloSignatureAlgorithms_CombinesFamiliesAndIgnoresUnknownSchemes() + { + byte[] clientHello = CreateClientHelloWithSignatureAlgorithms( + 0x0804, + 0x0403, + 0x0807, + 0x0904, + 0x0911, + 0x0A0A); + TlsFrameHelper.TlsFrameInfo info = default; + + Assert.True(TlsFrameHelper.TryGetFrameInfo(clientHello, ref info, AllExtensions)); + Assert.Equal( + TlsSignatureAlgorithmFamilies.Rsa | + TlsSignatureAlgorithmFamilies.ECDsa | + TlsSignatureAlgorithmFamilies.EdDsa | + TlsSignatureAlgorithmFamilies.MLDsa | + TlsSignatureAlgorithmFamilies.SlhDsa, + info.SignatureAlgorithmFamilies); + } + + [Fact] + public void TlsSignatureAlgorithmHelper_HandshakeClientHello_ParsesFamilies() + { + byte[] clientHello = CreateClientHelloWithSignatureAlgorithms(0x0804, 0x0403, 0x0904); + + Assert.True(TlsSignatureAlgorithmHelper.TryGetFamiliesFromClientHello( + clientHello.AsSpan(TlsFrameHelper.HeaderSize), + out TlsSignatureAlgorithmFamilies signatureAlgorithmFamilies)); + Assert.Equal( + TlsSignatureAlgorithmFamilies.Rsa | + TlsSignatureAlgorithmFamilies.ECDsa | + TlsSignatureAlgorithmFamilies.MLDsa, + signatureAlgorithmFamilies); + } + + public static IEnumerable InvalidSignatureAlgorithmsData() + { + yield return new object[] { Array.Empty() }; + yield return new object[] { new byte[] { 0x00 } }; + yield return new object[] { new byte[] { 0x00, 0x00 } }; + yield return new object[] { new byte[] { 0x00, 0x01, 0x04 } }; + yield return new object[] { new byte[] { 0x00, 0x02, 0x04 } }; + yield return new object[] { new byte[] { 0x00, 0x04, 0x04, 0x03 } }; + } + + [Theory] + [MemberData(nameof(InvalidSignatureAlgorithmsData))] + public void TlsFrameHelper_InvalidClientHelloSignatureAlgorithms_IgnoresExtension(byte[] extensionData) + { + byte[] clientHello = CreateClientHelloWithSignatureAlgorithmsExtension(extensionData); + TlsFrameHelper.TlsFrameInfo info = default; + + Assert.True(TlsFrameHelper.TryGetFrameInfo(clientHello, ref info, AllExtensions)); + Assert.Equal(TlsSignatureAlgorithmFamilies.None, info.SignatureAlgorithmFamilies); + } + + [Fact] + public void TlsFrameHelper_UnknownClientHelloSignatureAlgorithms_ReturnsNone() + { + byte[] clientHello = CreateClientHelloWithSignatureAlgorithms(0x0A0A); + TlsFrameHelper.TlsFrameInfo info = default; + + Assert.True(TlsFrameHelper.TryGetFrameInfo(clientHello, ref info, AllExtensions)); + Assert.Equal(TlsSignatureAlgorithmFamilies.None, info.SignatureAlgorithmFamilies); } [Fact] @@ -207,6 +321,51 @@ private static byte[] CreateClientHello(int hostNameLength) return clientHello; } + private static byte[] CreateClientHelloWithSignatureAlgorithms(params ushort[] signatureSchemes) + { + byte[] extensionData = new byte[sizeof(ushort) + (signatureSchemes.Length * sizeof(ushort))]; + BinaryPrimitives.WriteUInt16BigEndian(extensionData, checked((ushort)(extensionData.Length - sizeof(ushort)))); + + for (int i = 0; i < signatureSchemes.Length; i++) + { + BinaryPrimitives.WriteUInt16BigEndian( + extensionData.AsSpan(sizeof(ushort) + (i * sizeof(ushort))), + signatureSchemes[i]); + } + + return CreateClientHelloWithSignatureAlgorithmsExtension(extensionData); + } + + private static byte[] CreateClientHelloWithSignatureAlgorithmsExtension(byte[] extensionData) + { + const int ExtensionDataOffset = 56; + byte[] clientHello = new byte[ExtensionDataOffset + extensionData.Length]; + + clientHello[0] = (byte)TlsContentType.Handshake; + clientHello[1] = 3; + clientHello[2] = 3; + BinaryPrimitives.WriteUInt16BigEndian(clientHello.AsSpan(3), checked((ushort)(clientHello.Length - TlsFrameHelper.HeaderSize))); + clientHello[5] = (byte)TlsHandshakeType.ClientHello; + int handshakeLength = clientHello.Length - 9; + clientHello[6] = (byte)(handshakeLength >> 16); + clientHello[7] = (byte)(handshakeLength >> 8); + clientHello[8] = (byte)handshakeLength; + clientHello[9] = 3; + clientHello[10] = 3; + clientHello[43] = 0; + BinaryPrimitives.WriteUInt16BigEndian(clientHello.AsSpan(44), 2); + clientHello[46] = 0x13; + clientHello[47] = 0x01; + clientHello[48] = 1; + clientHello[49] = 0; + BinaryPrimitives.WriteUInt16BigEndian(clientHello.AsSpan(50), checked((ushort)(extensionData.Length + 4))); + BinaryPrimitives.WriteUInt16BigEndian(clientHello.AsSpan(52), (ushort)ExtensionType.SignatureAlgorithms); + BinaryPrimitives.WriteUInt16BigEndian(clientHello.AsSpan(54), checked((ushort)extensionData.Length)); + extensionData.CopyTo(clientHello.AsSpan(ExtensionDataOffset)); + + return clientHello; + } + private static byte[] s_validClientHello = new byte[] { // SslPlainText.(ContentType+ProtocolVersion) 0x16, 0x03, 0x03, diff --git a/src/libraries/System.Net.Security/tests/FunctionalTests/TlsSessionTests.cs b/src/libraries/System.Net.Security/tests/FunctionalTests/TlsSessionTests.cs index db6fc5e1eef6f6..d1d3425e9b4bda 100644 --- a/src/libraries/System.Net.Security/tests/FunctionalTests/TlsSessionTests.cs +++ b/src/libraries/System.Net.Security/tests/FunctionalTests/TlsSessionTests.cs @@ -122,6 +122,7 @@ public async Task ServerSession_DeferredOptions_SelectedFromSni_Succeeds() int factoryCalls = 0; string? observedSni = null; + TlsSignatureAlgorithmFamilies observedSignatureAlgorithmFamilies = TlsSignatureAlgorithmFamilies.None; (Stream clientStream, Stream serverStream) = TestHelper.GetConnectedStreams(); using (clientStream) @@ -147,6 +148,7 @@ public async Task ServerSession_DeferredOptions_SelectedFromSni_Succeeds() { factoryCalls++; observedSni = hello.ServerName; + observedSignatureAlgorithmFamilies = hello.SignatureAlgorithmFamilies; return hostCtx; }); @@ -156,6 +158,7 @@ public async Task ServerSession_DeferredOptions_SelectedFromSni_Succeeds() Assert.True(clientSsl.IsAuthenticated); Assert.Equal(1, factoryCalls); Assert.Equal(serverName, observedSni); + Assert.True((observedSignatureAlgorithmFamilies & TlsSignatureAlgorithmFamilies.Rsa) != 0); } } diff --git a/src/libraries/System.Net.Security/tests/UnitTests/System.Net.Security.Unit.Tests.csproj b/src/libraries/System.Net.Security/tests/UnitTests/System.Net.Security.Unit.Tests.csproj index ba8c55e67f50bc..82e22be59530a9 100644 --- a/src/libraries/System.Net.Security/tests/UnitTests/System.Net.Security.Unit.Tests.csproj +++ b/src/libraries/System.Net.Security/tests/UnitTests/System.Net.Security.Unit.Tests.csproj @@ -193,6 +193,8 @@ Link="ProductionCode\Common\System\Net\TlsAlertType.cs" /> +