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..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 @@ -936,10 +936,23 @@ 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); + + // 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..ce727ea5ec5ec3 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; @@ -392,6 +392,54 @@ await TestConfiguration.WhenAllOrAnyFailedWithTimeout( } } + [Theory] + [ClassData(typeof(SslProtocolSupport.SupportedSslProtocolsTestData))] + public async Task SslStream_Tls13ResumptionWithClientCert_IsMutuallyAuthenticatedTrue( + SslProtocols protocol) + { + string targetHost = Guid.NewGuid().ToString("N"); + + var clientOptions = new SslClientAuthenticationOptions + { + TargetHost = targetHost, + ClientCertificates = new X509CertificateCollection { _clientCertificate }, + EnabledSslProtocols = protocol, + RemoteCertificateValidationCallback = AllowAnyCertificate, + }; + + var serverOptions = new SslServerAuthenticationOptions + { + ServerCertificate = _serverCertificate, + ClientCertificateRequired = true, + EnabledSslProtocols = protocol, + RemoteCertificateValidationCallback = AllowAnyCertificate, + }; + + for (int i = 0; i < 3; i++) + { + (SslStream client, SslStream server) = TestHelper.GetConnectedSslStreams(); + using (client) + using (server) + { + await TestConfiguration.WhenAllOrAnyFailedWithTimeout( + client.AuthenticateAsClientAsync(clientOptions), + server.AuthenticateAsServerAsync(serverOptions)); + + // 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 + 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(); + } + } + } + private static bool AllowAnyCertificate( object sender, X509Certificate certificate,