From 5b416fa2d079ce34d96c5ccea94696fa257643a8 Mon Sep 17 00:00:00 2001 From: Aliaksandr Kukrash Date: Fri, 16 Jan 2026 01:36:15 +0100 Subject: [PATCH 1/5] Is this is a fix for #123238? --- .../Interop.OpenSsl.cs | 18 ++++++- .../Interop.Ssl.cs | 7 +++ .../SslStreamMutualAuthenticationTest.cs | 48 ++++++++++++++++++- 3 files changed, 70 insertions(+), 3 deletions(-) diff --git a/src/libraries/Common/src/Interop/Unix/System.Security.Cryptography.Native/Interop.OpenSsl.cs b/src/libraries/Common/src/Interop/Unix/System.Security.Cryptography.Native/Interop.OpenSsl.cs index a0faa66b6464d4..880a645c70f910 100644 --- a/src/libraries/Common/src/Interop/Unix/System.Security.Cryptography.Native/Interop.OpenSsl.cs +++ b/src/libraries/Common/src/Interop/Unix/System.Security.Cryptography.Native/Interop.OpenSsl.cs @@ -936,10 +936,24 @@ private static unsafe int NewSessionCallback(IntPtr ssl, IntPtr session) Debug.Assert(ssl != IntPtr.Zero); Debug.Assert(session != IntPtr.Zero); - // remember if the session used a certificate, this information is used after - // session resumption, the pointer is not being dereferenced and the refcount + // Remember if the session used a certificate, this information is used after + // session resumption. The pointer is not being dereferenced and the refcount // is not going to be manipulated. IntPtr cert = Interop.Ssl.SslGetCertificate(ssl); + + // Fix for https://github.com/dotnet/runtime/issues/XXXXX + // In TLS 1.3, new session tickets can be issued on resumed connections. + // When resuming, no certificate is set on the SSL object, so inherit + // the cert info from the current (resuming) session. + if (cert == IntPtr.Zero && Interop.Ssl.SslSessionReused(ssl)) + { + IntPtr currentSession = Interop.Ssl.SslGetSession(ssl); + if (currentSession != IntPtr.Zero) + { + cert = Interop.Ssl.SslSessionGetData(currentSession); + } + } + Interop.Ssl.SslSessionSetData(session, cert); IntPtr ptr = Ssl.SslGetData(ssl); diff --git a/src/libraries/Common/src/Interop/Unix/System.Security.Cryptography.Native/Interop.Ssl.cs b/src/libraries/Common/src/Interop/Unix/System.Security.Cryptography.Native/Interop.Ssl.cs index a93bc22be021ac..25bb670f784939 100644 --- a/src/libraries/Common/src/Interop/Unix/System.Security.Cryptography.Native/Interop.Ssl.cs +++ b/src/libraries/Common/src/Interop/Unix/System.Security.Cryptography.Native/Interop.Ssl.cs @@ -160,9 +160,16 @@ internal static SafeSharedX509StackHandle SslGetPeerCertChain(SafeSslHandle ssl) [return: MarshalAs(UnmanagedType.Bool)] internal static partial bool SslSessionReused(SafeSslHandle ssl); + [LibraryImport(Libraries.CryptoNative, EntryPoint = "CryptoNative_SslSessionReused")] + [return: MarshalAs(UnmanagedType.Bool)] + internal static partial bool SslSessionReused(IntPtr ssl); + [LibraryImport(Libraries.CryptoNative, EntryPoint = "CryptoNative_SslGetSession")] internal static partial IntPtr SslGetSession(SafeSslHandle ssl); + [LibraryImport(Libraries.CryptoNative, EntryPoint = "CryptoNative_SslGetSession")] + internal static partial IntPtr SslGetSession(IntPtr ssl); + [LibraryImport(Libraries.CryptoNative, EntryPoint = "CryptoNative_SslGetClientCAList")] private static partial SafeSharedX509NameStackHandle SslGetClientCAList_private(SafeSslHandle ssl); diff --git a/src/libraries/System.Net.Security/tests/FunctionalTests/SslStreamMutualAuthenticationTest.cs b/src/libraries/System.Net.Security/tests/FunctionalTests/SslStreamMutualAuthenticationTest.cs index 435ecdc4774eda..56bef3de034a69 100644 --- a/src/libraries/System.Net.Security/tests/FunctionalTests/SslStreamMutualAuthenticationTest.cs +++ b/src/libraries/System.Net.Security/tests/FunctionalTests/SslStreamMutualAuthenticationTest.cs @@ -1,4 +1,4 @@ -// Licensed to the .NET Foundation under one or more agreements. +// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System.IO; @@ -410,5 +410,51 @@ private X509Certificate ClientCertSelectionCallback( { return _clientCertificate; } + + [ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.SupportsTls13))] + [PlatformSpecific(TestPlatforms.Linux)] // Regression test for TLS 1.3 session resumption on OpenSSL + public async Task SslStream_Tls13ResumptionWithClientCert_IsMutuallyAuthenticatedTrue() + { + // Use certificate context which enables session caching + var serverContext = SslStreamCertificateContext.Create(_serverCertificate, null); + var clientContext = SslStreamCertificateContext.Create(_clientCertificate, null); + + // Use a consistent target host to enable session resumption + string targetHost = Guid.NewGuid().ToString("N"); + + for (int i = 0; i < 3; i++) + { + (SslStream client, SslStream server) = TestHelper.GetConnectedSslStreams(); + using (client) + using (server) + { + var clientOptions = new SslClientAuthenticationOptions + { + TargetHost = targetHost, + ClientCertificateContext = clientContext, + EnabledSslProtocols = SslProtocols.Tls13, + RemoteCertificateValidationCallback = AllowAnyCertificate, + }; + + var serverOptions = new SslServerAuthenticationOptions + { + ServerCertificateContext = serverContext, + ClientCertificateRequired = true, + EnabledSslProtocols = SslProtocols.Tls13, + RemoteCertificateValidationCallback = AllowAnyCertificate, + }; + + await TestConfiguration.WhenAllOrAnyFailedWithTimeout( + client.AuthenticateAsClientAsync(clientOptions), + server.AuthenticateAsServerAsync(serverOptions)); + + // Regression test: all connections (including resumed ones) must report mutual auth + Assert.True(client.IsMutuallyAuthenticated, $"Client connection {i}: IsMutuallyAuthenticated should be true"); + Assert.True(server.IsMutuallyAuthenticated, $"Server connection {i}: IsMutuallyAuthenticated should be true"); + Assert.NotNull(client.LocalCertificate); + Assert.NotNull(server.RemoteCertificate); + } + } + } } } From 271e64a6222b26b456bc024c443475136d9fc088 Mon Sep 17 00:00:00 2001 From: Aliaksandr Kukrash Date: Sat, 17 Jan 2026 00:59:31 +0100 Subject: [PATCH 2/5] Remove issues reference comment Co-authored-by: Radek Zikmund <32671551+rzikm@users.noreply.github.com> --- .../Unix/System.Security.Cryptography.Native/Interop.OpenSsl.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/libraries/Common/src/Interop/Unix/System.Security.Cryptography.Native/Interop.OpenSsl.cs b/src/libraries/Common/src/Interop/Unix/System.Security.Cryptography.Native/Interop.OpenSsl.cs index 880a645c70f910..221e130108f419 100644 --- a/src/libraries/Common/src/Interop/Unix/System.Security.Cryptography.Native/Interop.OpenSsl.cs +++ b/src/libraries/Common/src/Interop/Unix/System.Security.Cryptography.Native/Interop.OpenSsl.cs @@ -941,7 +941,6 @@ private static unsafe int NewSessionCallback(IntPtr ssl, IntPtr session) // is not going to be manipulated. IntPtr cert = Interop.Ssl.SslGetCertificate(ssl); - // Fix for https://github.com/dotnet/runtime/issues/XXXXX // In TLS 1.3, new session tickets can be issued on resumed connections. // When resuming, no certificate is set on the SSL object, so inherit // the cert info from the current (resuming) session. From 75e9edd3c9635b0ac8f7859e74c616bc7d2385a5 Mon Sep 17 00:00:00 2001 From: Aliaksandr Kukrash Date: Sat, 17 Jan 2026 01:58:25 +0100 Subject: [PATCH 3/5] Simplify test --- .../SslStreamMutualAuthenticationTest.cs | 79 +++++++++---------- 1 file changed, 36 insertions(+), 43 deletions(-) diff --git a/src/libraries/System.Net.Security/tests/FunctionalTests/SslStreamMutualAuthenticationTest.cs b/src/libraries/System.Net.Security/tests/FunctionalTests/SslStreamMutualAuthenticationTest.cs index 56bef3de034a69..f9d7d6f9022625 100644 --- a/src/libraries/System.Net.Security/tests/FunctionalTests/SslStreamMutualAuthenticationTest.cs +++ b/src/libraries/System.Net.Security/tests/FunctionalTests/SslStreamMutualAuthenticationTest.cs @@ -392,63 +392,37 @@ await TestConfiguration.WhenAllOrAnyFailedWithTimeout( } } - private static bool AllowAnyCertificate( - object sender, - X509Certificate certificate, - X509Chain chain, - SslPolicyErrors sslPolicyErrors) - { - return true; - } - - private X509Certificate ClientCertSelectionCallback( - object sender, - string targetHost, - X509CertificateCollection localCertificates, - X509Certificate remoteCertificate, - string[] acceptableIssuers) - { - return _clientCertificate; - } - [ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.SupportsTls13))] - [PlatformSpecific(TestPlatforms.Linux)] // Regression test for TLS 1.3 session resumption on OpenSSL - public async Task SslStream_Tls13ResumptionWithClientCert_IsMutuallyAuthenticatedTrue() + public async Task SslStream_Tls13ResumedSessionsClientCollection_MutualAuthPreserved() { - // Use certificate context which enables session caching - var serverContext = SslStreamCertificateContext.Create(_serverCertificate, null); - var clientContext = SslStreamCertificateContext.Create(_clientCertificate, null); - - // Use a consistent target host to enable session resumption string targetHost = Guid.NewGuid().ToString("N"); + var clientOptions = new SslClientAuthenticationOptions + { + TargetHost = targetHost, + ClientCertificates = new X509CertificateCollection { _clientCertificate }, + EnabledSslProtocols = SslProtocols.Tls13, + RemoteCertificateValidationCallback = AllowAnyCertificate, + }; + + var serverOptions = new SslServerAuthenticationOptions + { + ServerCertificateContext = SslStreamCertificateContext.Create(_serverCertificate, null), + ClientCertificateRequired = true, + EnabledSslProtocols = SslProtocols.Tls13, + RemoteCertificateValidationCallback = AllowAnyCertificate, + }; + for (int i = 0; i < 3; i++) { (SslStream client, SslStream server) = TestHelper.GetConnectedSslStreams(); using (client) using (server) { - var clientOptions = new SslClientAuthenticationOptions - { - TargetHost = targetHost, - ClientCertificateContext = clientContext, - EnabledSslProtocols = SslProtocols.Tls13, - RemoteCertificateValidationCallback = AllowAnyCertificate, - }; - - var serverOptions = new SslServerAuthenticationOptions - { - ServerCertificateContext = serverContext, - ClientCertificateRequired = true, - EnabledSslProtocols = SslProtocols.Tls13, - RemoteCertificateValidationCallback = AllowAnyCertificate, - }; - await TestConfiguration.WhenAllOrAnyFailedWithTimeout( client.AuthenticateAsClientAsync(clientOptions), server.AuthenticateAsServerAsync(serverOptions)); - // Regression test: all connections (including resumed ones) must report mutual auth Assert.True(client.IsMutuallyAuthenticated, $"Client connection {i}: IsMutuallyAuthenticated should be true"); Assert.True(server.IsMutuallyAuthenticated, $"Server connection {i}: IsMutuallyAuthenticated should be true"); Assert.NotNull(client.LocalCertificate); @@ -456,5 +430,24 @@ await TestConfiguration.WhenAllOrAnyFailedWithTimeout( } } } + + private static bool AllowAnyCertificate( + object sender, + X509Certificate certificate, + X509Chain chain, + SslPolicyErrors sslPolicyErrors) + { + return true; + } + + private X509Certificate ClientCertSelectionCallback( + object sender, + string targetHost, + X509CertificateCollection localCertificates, + X509Certificate remoteCertificate, + string[] acceptableIssuers) + { + return _clientCertificate; + } } } From 56f6f3d12178a611d3f3c9b2c9ccbe78d81f3a31 Mon Sep 17 00:00:00 2001 From: Aliaksandr Kukrash Date: Sat, 17 Jan 2026 02:31:12 +0100 Subject: [PATCH 4/5] Simplify test case --- .../SslStreamMutualAuthenticationTest.cs | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/src/libraries/System.Net.Security/tests/FunctionalTests/SslStreamMutualAuthenticationTest.cs b/src/libraries/System.Net.Security/tests/FunctionalTests/SslStreamMutualAuthenticationTest.cs index f9d7d6f9022625..477d5c62823205 100644 --- a/src/libraries/System.Net.Security/tests/FunctionalTests/SslStreamMutualAuthenticationTest.cs +++ b/src/libraries/System.Net.Security/tests/FunctionalTests/SslStreamMutualAuthenticationTest.cs @@ -393,7 +393,8 @@ await TestConfiguration.WhenAllOrAnyFailedWithTimeout( } [ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.SupportsTls13))] - public async Task SslStream_Tls13ResumedSessionsClientCollection_MutualAuthPreserved() + [PlatformSpecific(TestPlatforms.Linux)] // Regression test for TLS 1.3 session resumption on OpenSSL + public async Task SslStream_Tls13ResumptionWithClientCert_IsMutuallyAuthenticatedTrue() { string targetHost = Guid.NewGuid().ToString("N"); @@ -407,7 +408,7 @@ public async Task SslStream_Tls13ResumedSessionsClientCollection_MutualAuthPrese var serverOptions = new SslServerAuthenticationOptions { - ServerCertificateContext = SslStreamCertificateContext.Create(_serverCertificate, null), + ServerCertificate = _serverCertificate, ClientCertificateRequired = true, EnabledSslProtocols = SslProtocols.Tls13, RemoteCertificateValidationCallback = AllowAnyCertificate, @@ -423,10 +424,17 @@ await TestConfiguration.WhenAllOrAnyFailedWithTimeout( client.AuthenticateAsClientAsync(clientOptions), server.AuthenticateAsServerAsync(serverOptions)); + // PingPong triggers TLS 1.3 new session ticket delivery + await TestHelper.PingPong(client, server); + + // Regression test: all connections (including resumed ones) must report mutual auth Assert.True(client.IsMutuallyAuthenticated, $"Client connection {i}: IsMutuallyAuthenticated should be true"); Assert.True(server.IsMutuallyAuthenticated, $"Server connection {i}: IsMutuallyAuthenticated should be true"); Assert.NotNull(client.LocalCertificate); Assert.NotNull(server.RemoteCertificate); + + await client.ShutdownAsync(); + await server.ShutdownAsync(); } } } From 3d94657c0406220ca93c8ed9e613c00fced583b0 Mon Sep 17 00:00:00 2001 From: Aliaksandr Kukrash Date: Sat, 17 Jan 2026 02:39:25 +0100 Subject: [PATCH 5/5] Improve test case --- .../SslStreamMutualAuthenticationTest.cs | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/src/libraries/System.Net.Security/tests/FunctionalTests/SslStreamMutualAuthenticationTest.cs b/src/libraries/System.Net.Security/tests/FunctionalTests/SslStreamMutualAuthenticationTest.cs index 477d5c62823205..ce727ea5ec5ec3 100644 --- a/src/libraries/System.Net.Security/tests/FunctionalTests/SslStreamMutualAuthenticationTest.cs +++ b/src/libraries/System.Net.Security/tests/FunctionalTests/SslStreamMutualAuthenticationTest.cs @@ -392,9 +392,10 @@ await TestConfiguration.WhenAllOrAnyFailedWithTimeout( } } - [ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.SupportsTls13))] - [PlatformSpecific(TestPlatforms.Linux)] // Regression test for TLS 1.3 session resumption on OpenSSL - public async Task SslStream_Tls13ResumptionWithClientCert_IsMutuallyAuthenticatedTrue() + [Theory] + [ClassData(typeof(SslProtocolSupport.SupportedSslProtocolsTestData))] + public async Task SslStream_Tls13ResumptionWithClientCert_IsMutuallyAuthenticatedTrue( + SslProtocols protocol) { string targetHost = Guid.NewGuid().ToString("N"); @@ -402,7 +403,7 @@ public async Task SslStream_Tls13ResumptionWithClientCert_IsMutuallyAuthenticate { TargetHost = targetHost, ClientCertificates = new X509CertificateCollection { _clientCertificate }, - EnabledSslProtocols = SslProtocols.Tls13, + EnabledSslProtocols = protocol, RemoteCertificateValidationCallback = AllowAnyCertificate, }; @@ -410,7 +411,7 @@ public async Task SslStream_Tls13ResumptionWithClientCert_IsMutuallyAuthenticate { ServerCertificate = _serverCertificate, ClientCertificateRequired = true, - EnabledSslProtocols = SslProtocols.Tls13, + EnabledSslProtocols = protocol, RemoteCertificateValidationCallback = AllowAnyCertificate, }; @@ -424,7 +425,7 @@ await TestConfiguration.WhenAllOrAnyFailedWithTimeout( client.AuthenticateAsClientAsync(clientOptions), server.AuthenticateAsServerAsync(serverOptions)); - // PingPong triggers TLS 1.3 new session ticket delivery + // PingPong triggers new session ticket delivery (TLS 1.3) await TestHelper.PingPong(client, server); // Regression test: all connections (including resumed ones) must report mutual auth