From 3988cfc7d853c7158b97099cf3ffd018cf7cf806 Mon Sep 17 00:00:00 2001 From: wfurt Date: Thu, 14 May 2026 21:22:43 -0700 Subject: [PATCH 1/4] improve accuracy of SslClientHelloInfo --- .../src/System/Net/Security/SslStream.IO.cs | 6 +++ .../ServerAsyncAuthenticateTest.cs | 38 +++++++++++++++++++ 2 files changed, 44 insertions(+) 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 a8faaae4fa9cbb..4659e32091c774 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 @@ -467,6 +467,12 @@ private async ValueTask ReceiveHandshakeFrameAsync(Cancellation options |= TlsFrameHelper.ProcessingOptions.RawApplicationProtocol; } + if (_sslAuthenticationOptions.ServerOptionDelegate != null) + { + // We need to process supported versions extension to pass it to user callback. + options |= TlsFrameHelper.ProcessingOptions.Versions; + } + // Process SNI from Client Hello message if (!TlsFrameHelper.TryGetFrameInfo(_buffer.EncryptedReadOnlySpan, ref _lastFrame, options)) { diff --git a/src/libraries/System.Net.Security/tests/FunctionalTests/ServerAsyncAuthenticateTest.cs b/src/libraries/System.Net.Security/tests/FunctionalTests/ServerAsyncAuthenticateTest.cs index ef59c802863a53..949c0486e67f8e 100644 --- a/src/libraries/System.Net.Security/tests/FunctionalTests/ServerAsyncAuthenticateTest.cs +++ b/src/libraries/System.Net.Security/tests/FunctionalTests/ServerAsyncAuthenticateTest.cs @@ -136,6 +136,44 @@ public async Task ServerAsyncAuthenticate_SniSetVersion_Success(SslProtocols ver } } + [ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.SupportsTls13))] + public async Task ServerAsyncAuthenticate_SniCallback_ReceivesSupportedVersionsFromExtension() + { + // Regression test: ensure that when a ServerOptionDelegate (SNI callback) is used, + // the supported_versions extension from the ClientHello is parsed and exposed via + // SslClientHelloInfo.SslProtocols. TLS 1.3 advertises its version only via that + // extension (the record-layer / legacy_version field still reports TLS 1.2), so + // without parsing the extension the callback would not see Tls13. + var serverOptions = new SslServerAuthenticationOptions() { ServerCertificate = _serverCertificate, EnabledSslProtocols = SslProtocols.Tls13 }; + var clientOptions = new SslClientAuthenticationOptions() + { + TargetHost = _serverCertificate.GetNameInfo(X509NameType.SimpleName, forIssuer: false), + EnabledSslProtocols = SslProtocols.Tls13, + }; + clientOptions.RemoteCertificateValidationCallback = (sender, certificate, chain, sslPolicyErrors) => true; + + SslProtocols observedProtocols = SslProtocols.None; + + (SslStream client, SslStream server) = TestHelper.GetConnectedSslStreams(); + using (client) + using (server) + { + Task t1 = client.AuthenticateAsClientAsync(clientOptions, CancellationToken.None); + Task t2 = server.AuthenticateAsServerAsync( + (stream, clientHelloInfo, userState, cancellationToken) => + { + observedProtocols = clientHelloInfo.SslProtocols; + return new ValueTask(serverOptions); + }, + null, CancellationToken.None); + + await TestConfiguration.WhenAllOrAnyFailedWithTimeout(t1, t2); + } + + Assert.True((observedProtocols & SslProtocols.Tls13) == SslProtocols.Tls13, + $"Expected SslClientHelloInfo.SslProtocols to include Tls13, got '{observedProtocols}'."); + } + private async Task FailedTask() { await Task.Yield(); From b19dd78d9337c3dd6145583918f0c14759f31417 Mon Sep 17 00:00:00 2001 From: wfurt Date: Thu, 14 May 2026 21:49:12 -0700 Subject: [PATCH 2/4] feedback --- .../src/System/Net/Security/SslStream.IO.cs | 27 ++++++++++--------- 1 file changed, 15 insertions(+), 12 deletions(-) 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 4659e32091c774..cdb8e61ddb8b12 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 @@ -457,20 +457,23 @@ private async ValueTask ReceiveHandshakeFrameAsync(Cancellation _sslAuthenticationOptions!.IsServer) // guard against malicious endpoints. We should not see ClientHello on client. #pragma warning restore CS0618 { - TlsFrameHelper.ProcessingOptions options = NetEventSource.Log.IsEnabled() ? - TlsFrameHelper.ProcessingOptions.All : - TlsFrameHelper.ProcessingOptions.ServerName; - if (OperatingSystem.IsMacOS() && _sslAuthenticationOptions.IsServer) - { - // macOS cannot process ALPN on server at the moment. - // We fallback to our own process similar to SNI bellow. - options |= TlsFrameHelper.ProcessingOptions.RawApplicationProtocol; - } + TlsFrameHelper.ProcessingOptions options = TlsFrameHelper.ProcessingOptions.All; - if (_sslAuthenticationOptions.ServerOptionDelegate != null) + if (!NetEventSource.Log.IsEnabled()) { - // We need to process supported versions extension to pass it to user callback. - options |= TlsFrameHelper.ProcessingOptions.Versions; + options = TlsFrameHelper.ProcessingOptions.ServerName; + if (OperatingSystem.IsMacOS() && _sslAuthenticationOptions.IsServer) + { + // macOS cannot process ALPN on server at the moment. + // We fallback to our own process similar to SNI bellow. + options |= TlsFrameHelper.ProcessingOptions.RawApplicationProtocol; + } + + if (_sslAuthenticationOptions.ServerOptionDelegate != null) + { + // We need to process supported versions extension to pass it to user callback. + options |= TlsFrameHelper.ProcessingOptions.Versions; + } } // Process SNI from Client Hello message From bcd4dd3cd8c0204d46ff9c9d0780e7e4bbfdcf4a Mon Sep 17 00:00:00 2001 From: wfurt Date: Thu, 14 May 2026 22:28:48 -0700 Subject: [PATCH 3/4] feedback --- .../src/System/Net/Security/SslStream.IO.cs | 30 ++++++++++--------- .../src/System/Net/Security/TlsFrameHelper.cs | 12 ++++---- .../FunctionalTests/TlsFrameHelperTests.cs | 17 +++++++---- 3 files changed, 32 insertions(+), 27 deletions(-) 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 cdb8e61ddb8b12..6f7780e764bc4a 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 @@ -457,23 +457,25 @@ private async ValueTask ReceiveHandshakeFrameAsync(Cancellation _sslAuthenticationOptions!.IsServer) // guard against malicious endpoints. We should not see ClientHello on client. #pragma warning restore CS0618 { - TlsFrameHelper.ProcessingOptions options = TlsFrameHelper.ProcessingOptions.All; + TlsFrameHelper.ProcessingOptions options = TlsFrameHelper.ProcessingOptions.ServerName; - if (!NetEventSource.Log.IsEnabled()) + if (OperatingSystem.IsMacOS() && _sslAuthenticationOptions.IsServer) { - options = TlsFrameHelper.ProcessingOptions.ServerName; - if (OperatingSystem.IsMacOS() && _sslAuthenticationOptions.IsServer) - { - // macOS cannot process ALPN on server at the moment. - // We fallback to our own process similar to SNI bellow. - options |= TlsFrameHelper.ProcessingOptions.RawApplicationProtocol; - } + // macOS cannot process ALPN on server at the moment. + // We fallback to our own process similar to SNI bellow. + // This will allocate. + options |= TlsFrameHelper.ProcessingOptions.RawApplicationProtocol; + } - if (_sslAuthenticationOptions.ServerOptionDelegate != null) - { - // We need to process supported versions extension to pass it to user callback. - options |= TlsFrameHelper.ProcessingOptions.Versions; - } + if (NetEventSource.Log.IsEnabled()) + { + options |= TlsFrameHelper.ProcessingOptions.ApplicationProtocol | TlsFrameHelper.ProcessingOptions.Versions; + } + + if (_sslAuthenticationOptions.ServerOptionDelegate != null) + { + // We need to process supported versions extension to pass it to user callback. + options |= TlsFrameHelper.ProcessingOptions.Versions; } // Process SNI from Client Hello message 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 67f83c1eeaa1e0..8744712e2ad7f9 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 @@ -99,11 +99,11 @@ internal static class TlsFrameHelper [Flags] public enum ProcessingOptions { - All = 0, ServerName = 0x1, ApplicationProtocol = 0x2, Versions = 0x4, RawApplicationProtocol = 0x8, + All = ServerName | ApplicationProtocol | Versions | RawApplicationProtocol, } [Flags] @@ -515,8 +515,7 @@ private static bool TryParseHelloExtensions(ReadOnlySpan extensions, ref T ReadOnlySpan extensionData = extensions.Slice(0, extensionLength); - if (extensionType == ExtensionType.ServerName && (options == ProcessingOptions.All || - (options & ProcessingOptions.ServerName) == ProcessingOptions.ServerName)) + if (extensionType == ExtensionType.ServerName && (options & ProcessingOptions.ServerName) != 0) { if (!TryGetSniFromServerNameList(extensionData, out string? sni)) { @@ -525,8 +524,7 @@ private static bool TryParseHelloExtensions(ReadOnlySpan extensions, ref T info.TargetName = sni!; } - else if (extensionType == ExtensionType.SupportedVersions && (options == ProcessingOptions.All || - (options & ProcessingOptions.Versions) == ProcessingOptions.Versions)) + else if (extensionType == ExtensionType.SupportedVersions && (options & ProcessingOptions.Versions) != 0) { if (!TryGetSupportedVersionsFromExtension(extensionData, out SslProtocols versions)) { @@ -535,8 +533,8 @@ private static bool TryParseHelloExtensions(ReadOnlySpan extensions, ref T info.SupportedVersions |= versions; } - else if (extensionType == ExtensionType.ApplicationProtocols && (options == ProcessingOptions.All || - (options.HasFlag(ProcessingOptions.ApplicationProtocol) || options.HasFlag(ProcessingOptions.RawApplicationProtocol)))) + else if (extensionType == ExtensionType.ApplicationProtocols && + (options & (ProcessingOptions.ApplicationProtocol | ProcessingOptions.RawApplicationProtocol)) != 0) { if (!TryGetApplicationProtocolsFromExtension(extensionData, out ApplicationProtocolInfo alpn)) { diff --git a/src/libraries/System.Net.Security/tests/FunctionalTests/TlsFrameHelperTests.cs b/src/libraries/System.Net.Security/tests/FunctionalTests/TlsFrameHelperTests.cs index be14f2e9ca45b2..5de96318a51057 100644 --- a/src/libraries/System.Net.Security/tests/FunctionalTests/TlsFrameHelperTests.cs +++ b/src/libraries/System.Net.Security/tests/FunctionalTests/TlsFrameHelperTests.cs @@ -39,11 +39,16 @@ private void InvalidClientHello(byte[] clientHello, int id, bool shouldPass) Assert.Null(ret); } + private const TlsFrameHelper.ProcessingOptions AllExtensions = + TlsFrameHelper.ProcessingOptions.ServerName | + TlsFrameHelper.ProcessingOptions.ApplicationProtocol | + TlsFrameHelper.ProcessingOptions.Versions; + [Fact] public void TlsFrameHelper_ValidData_Ok() { TlsFrameHelper.TlsFrameInfo info = default; - Assert.True(TlsFrameHelper.TryGetFrameInfo(s_validClientHello, ref info)); + Assert.True(TlsFrameHelper.TryGetFrameInfo(s_validClientHello, ref info, AllExtensions)); Assert.Equal(SslProtocols.Tls12, info.Header.Version); Assert.Equal(208, info.Header.Length); @@ -55,7 +60,7 @@ public void TlsFrameHelper_ValidData_Ok() public void TlsFrameHelper_Tls12ClientHello_Ok() { TlsFrameHelper.TlsFrameInfo info = default; - Assert.True(TlsFrameHelper.TryGetFrameInfo(s_Tls12ClientHello, ref info)); + Assert.True(TlsFrameHelper.TryGetFrameInfo(s_Tls12ClientHello, ref info, AllExtensions)); #pragma warning disable SYSLIB0039 Assert.Equal(SslProtocols.Tls, info.Header.Version); @@ -68,7 +73,7 @@ public void TlsFrameHelper_Tls12ClientHello_Ok() public void TlsFrameHelper_Tls13ClientHello_Ok() { TlsFrameHelper.TlsFrameInfo info = default; - Assert.True(TlsFrameHelper.TryGetFrameInfo(s_Tls13ClientHello, ref info)); + Assert.True(TlsFrameHelper.TryGetFrameInfo(s_Tls13ClientHello, ref info, AllExtensions)); #pragma warning disable SYSLIB0039 Assert.Equal(SslProtocols.Tls, info.Header.Version); @@ -81,7 +86,7 @@ public void TlsFrameHelper_Tls13ClientHello_Ok() public void TlsFrameHelper_Tls12ServerHello_Ok() { TlsFrameHelper.TlsFrameInfo info = default; - Assert.True(TlsFrameHelper.TryGetFrameInfo(s_Tls12ServerHello, ref info)); + Assert.True(TlsFrameHelper.TryGetFrameInfo(s_Tls12ServerHello, ref info, AllExtensions)); Assert.Equal(SslProtocols.Tls12, info.Header.Version); Assert.Equal(SslProtocols.Tls12, info.SupportedVersions); @@ -95,7 +100,7 @@ public void TlsFrameHelper_UnifiedClientHello_Ok() Assert.True(TlsFrameHelper.TryGetFrameHeader(s_UnifiedHello, ref info.Header)); Assert.Equal(75, info.Header.Length); - Assert.True(TlsFrameHelper.TryGetFrameInfo(s_UnifiedHello, ref info)); + Assert.True(TlsFrameHelper.TryGetFrameInfo(s_UnifiedHello, ref info, AllExtensions)); #pragma warning disable CS0618 // Ssl2 and Ssl3 are obsolete #pragma warning disable SYSLIB0039 // Tls is obsolete Assert.Equal(SslProtocols.Ssl2, info.Header.Version); @@ -111,7 +116,7 @@ public void TlsFrameHelper_UnifiedClientHello_Ok() public void TlsFrameHelper_TlsClientHelloNoExtensions_Ok() { TlsFrameHelper.TlsFrameInfo info = default; - Assert.True(TlsFrameHelper.TryGetFrameInfo(s_TlsClientHelloNoExtensions, ref info)); + Assert.True(TlsFrameHelper.TryGetFrameInfo(s_TlsClientHelloNoExtensions, ref info, AllExtensions)); Assert.Equal(SslProtocols.Tls12, info.Header.Version); Assert.Equal(SslProtocols.Tls12, info.SupportedVersions); Assert.Equal(TlsContentType.Handshake, info.Header.Type); From 9d749c6591994d0949c6b67c1e0e950e46f581d5 Mon Sep 17 00:00:00 2001 From: wfurt Date: Thu, 14 May 2026 22:53:32 -0700 Subject: [PATCH 4/4] frame --- .../src/System/Net/Security/TlsFrameHelper.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) 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 8744712e2ad7f9..8da2a047feb1c0 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 @@ -103,7 +103,6 @@ public enum ProcessingOptions ApplicationProtocol = 0x2, Versions = 0x4, RawApplicationProtocol = 0x8, - All = ServerName | ApplicationProtocol | Versions | RawApplicationProtocol, } [Flags] @@ -224,7 +223,7 @@ public static bool TryGetFrameHeader(ReadOnlySpan frame, ref TlsFrameHeade // It is OK to call it again if more data becomes available. // It is also possible to limit what information is processed. // If callback delegate is provided, it will be called on ALL extensions. - public static bool TryGetFrameInfo(ReadOnlySpan frame, ref TlsFrameInfo info, ProcessingOptions options = ProcessingOptions.All, HelloExtensionCallback? callback = null) + public static bool TryGetFrameInfo(ReadOnlySpan frame, ref TlsFrameInfo info, ProcessingOptions options = ProcessingOptions.ServerName, HelloExtensionCallback? callback = null) { const int HandshakeTypeOffset = 5; if (frame.Length < HeaderSize)