From 0ac9ba172a6fc29e8c869cf2516609ca24c93cc6 Mon Sep 17 00:00:00 2001 From: Radek Zikmund Date: Tue, 6 Oct 2026 14:37:19 +0200 Subject: [PATCH 1/2] Isolate HttpClient certificate validation tests from TLS resumption Add default-preserving handler helper options and require fresh handshakes in certificate validation tests. Cover helper forwarding and repeated callbacks while preserving native handler behavior. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- ...ttpClientHandlerTest.ServerCertificates.cs | 55 ++++++++++++------- .../HttpClientHandlerTest.SslProtocols.cs | 2 +- ...ttpClientHandlerTestBase.WinHttpHandler.cs | 10 +++- .../HttpClientHandlerTest.Http3.cs | 2 +- ...lientHandlerTestBase.SocketsHttpHandler.cs | 16 +++++- .../FunctionalTests/SocketsHttpHandlerTest.cs | 28 +++++++++- 6 files changed, 83 insertions(+), 30 deletions(-) diff --git a/src/libraries/Common/tests/System/Net/Http/HttpClientHandlerTest.ServerCertificates.cs b/src/libraries/Common/tests/System/Net/Http/HttpClientHandlerTest.ServerCertificates.cs index 8bd55e52abe05d..c3fbf655195105 100644 --- a/src/libraries/Common/tests/System/Net/Http/HttpClientHandlerTest.ServerCertificates.cs +++ b/src/libraries/Common/tests/System/Net/Http/HttpClientHandlerTest.ServerCertificates.cs @@ -32,9 +32,11 @@ public HttpClientHandler_ServerCertificates_Test(ITestOutputHelper output) : bas // This enables customizing ServerCertificateCustomValidationCallback in WinHttpHandler variants: protected bool AllowAllCertificates { get; set; } = true; - protected new HttpClientHandler CreateHttpClientHandler() => CreateHttpClientHandler( + protected new HttpClientHandler CreateHttpClientHandler() => CreateHttpClientHandler(allowTlsResume: true); + protected new HttpClientHandler CreateHttpClientHandler(bool allowTlsResume) => CreateHttpClientHandler( useVersion: UseVersion, - allowAllCertificates: UseVersion >= HttpVersion20.Value && AllowAllCertificates); + allowAllCertificates: UseVersion >= HttpVersion20.Value && AllowAllCertificates, + allowTlsResume: allowTlsResume); protected override HttpClient CreateHttpClient() => CreateHttpClient(CreateHttpClientHandler()); [Fact] @@ -143,7 +145,7 @@ public static IEnumerable UseCallback_ValidCertificate_ExpectedValuesD [MemberData(nameof(UseCallback_ValidCertificate_ExpectedValuesDuringCallback_Urls))] public async Task UseCallback_ValidCertificate_ExpectedValuesDuringCallback(Configuration.Http.RemoteServer remoteServer, Uri url, bool checkRevocation) { - HttpClientHandler handler = CreateHttpClientHandler(); + HttpClientHandler handler = CreateHttpClientHandler(allowTlsResume: false); using (HttpClient client = CreateHttpClientForRemoteServer(remoteServer, handler)) { bool callbackCalled = false; @@ -186,7 +188,7 @@ public async Task UseCallback_ValidCertificate_ExpectedValuesDuringCallback(Conf [Fact] public async Task UseCallback_CallbackReturnsFailure_ThrowsException() { - HttpClientHandler handler = CreateHttpClientHandler(); + HttpClientHandler handler = CreateHttpClientHandler(allowTlsResume: false); using (HttpClient client = CreateHttpClient(handler)) { handler.ServerCertificateCustomValidationCallback = delegate { return false; }; @@ -198,7 +200,7 @@ public async Task UseCallback_CallbackReturnsFailure_ThrowsException() [Fact] public async Task UseCallback_CallbackThrowsException_ExceptionPropagatesAsBaseException() { - HttpClientHandler handler = CreateHttpClientHandler(); + HttpClientHandler handler = CreateHttpClientHandler(allowTlsResume: false); using (HttpClient client = CreateHttpClient(handler)) { var e = new DivideByZeroException(); @@ -221,7 +223,8 @@ public async Task UseCallback_CallbackThrowsException_ExceptionPropagatesAsBaseE [MemberData(nameof(CertificateValidationServers))] public async Task NoCallback_BadCertificate_ThrowsException(string url) { - using (HttpClient client = CreateHttpClient()) + using (HttpClientHandler handler = CreateHttpClientHandler(allowTlsResume: false)) + using (HttpClient client = CreateHttpClient(handler)) { await Assert.ThrowsAsync(() => client.GetAsync(url)); } @@ -245,7 +248,7 @@ public async Task NoCallback_RevokedCertificate_NoRevocationChecking_Succeeds() [Fact] public async Task NoCallback_RevokedCertificate_RevocationChecking_Fails() { - HttpClientHandler handler = CreateHttpClientHandler(); + HttpClientHandler handler = CreateHttpClientHandler(allowTlsResume: false); handler.CheckCertificateRevocationList = true; using (HttpClient client = CreateHttpClient(handler)) { @@ -261,7 +264,7 @@ public async Task NoCallback_RevokedCertificate_RevocationChecking_Fails() private async Task UseCallback_BadCertificate_ExpectedPolicyErrors_Helper(string url, string useHttp2String, SslPolicyErrors expectedErrors) { - HttpClientHandler handler = CreateHttpClientHandler(useHttp2String); + HttpClientHandler handler = CreateHttpClientHandler(useHttp2String, allowTlsResume: false); using (HttpClient client = CreateHttpClient(handler, useHttp2String)) { bool callbackCalled = false; @@ -318,18 +321,20 @@ public async Task UseCallback_BadCertificate_ExpectedPolicyErrors(string url, Ss } } - [Fact] - public async Task UseCallback_SelfSignedCertificate_ExpectedPolicyErrors() + [Theory] + [InlineData(1)] + [InlineData(3)] + public async Task UseCallback_SelfSignedCertificate_ExpectedPolicyErrors(int requestCount) { - using (HttpClientHandler handler = CreateHttpClientHandler()) + using (HttpClientHandler handler = CreateHttpClientHandler(allowTlsResume: false)) using (HttpClient client = CreateHttpClient(handler)) { - bool callbackCalled = false; - X509Certificate2 certificate = TestHelper.CreateServerSelfSignedCertificate(); + int callbackCount = 0; + using X509Certificate2 certificate = TestHelper.CreateServerSelfSignedCertificate(); handler.ServerCertificateCustomValidationCallback = (request, cert, chain, errors) => { - callbackCalled = true; + callbackCount++; Assert.NotNull(request); Assert.NotNull(cert); Assert.NotNull(chain); @@ -341,12 +346,23 @@ public async Task UseCallback_SelfSignedCertificate_ExpectedPolicyErrors() await LoopbackServer.CreateServerAsync(async (server, url) => { - await TestHelper.WhenAllCompletedOrAnyFailed( - server.AcceptConnectionSendResponseAndCloseAsync(), - client.GetAsync($"https://{certificate.GetNameInfo(X509NameType.SimpleName, false)}:{url.Port}/")); + for (int i = 0; i < requestCount; i++) + { +#if NETFRAMEWORK + // The loopback authentication overload disposes its certificate after each handshake. + options.Certificate = new X509Certificate2(certificate); +#endif + Task responseTask = client.GetAsync($"https://{certificate.GetNameInfo(X509NameType.SimpleName, false)}:{url.Port}/"); + await TestHelper.WhenAllCompletedOrAnyFailed( + server.AcceptConnectionSendResponseAndCloseAsync(), + responseTask); + using HttpResponseMessage response = await responseTask; + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + Assert.Equal(i + 1, callbackCount); + } }, options); - Assert.True(callbackCalled); + Assert.Equal(requestCount, callbackCount); } } @@ -411,7 +427,8 @@ await RemoteExecutor.Invoke(async (useVersionString, allowAllCertificatesString) var version = Version.Parse(useVersionString); using HttpClientHandler handler = CreateHttpClientHandler( useVersion: version, - allowAllCertificates: version >= HttpVersion20.Value && bool.Parse(allowAllCertificatesString)); + allowAllCertificates: version >= HttpVersion20.Value && bool.Parse(allowAllCertificatesString), + allowTlsResume: false); using HttpClient client = CreateHttpClient(handler, useVersionString); await Assert.ThrowsAsync(() => client.GetAsync(Url)); diff --git a/src/libraries/Common/tests/System/Net/Http/HttpClientHandlerTest.SslProtocols.cs b/src/libraries/Common/tests/System/Net/Http/HttpClientHandlerTest.SslProtocols.cs index 1bbe73dd0a3a04..48e8fb9901463e 100644 --- a/src/libraries/Common/tests/System/Net/Http/HttpClientHandlerTest.SslProtocols.cs +++ b/src/libraries/Common/tests/System/Net/Http/HttpClientHandlerTest.SslProtocols.cs @@ -103,7 +103,7 @@ public static IEnumerable GetAsync_AllowedSSLVersion_Succeeds_MemberDa public async Task GetAsync_AllowedSSLVersion_Succeeds(SslProtocols acceptedProtocol, bool requestOnlyThisProtocol) { int count = 0; - using (HttpClientHandler handler = CreateHttpClientHandler()) + using (HttpClientHandler handler = CreateHttpClientHandler(allowTlsResume: false)) using (HttpClient client = CreateHttpClient(handler)) { handler.ServerCertificateCustomValidationCallback = diff --git a/src/libraries/System.Net.Http.WinHttpHandler/tests/FunctionalTests/HttpClientHandlerTestBase.WinHttpHandler.cs b/src/libraries/System.Net.Http.WinHttpHandler/tests/FunctionalTests/HttpClientHandlerTestBase.WinHttpHandler.cs index 81773638085d93..ff841013d1e199 100644 --- a/src/libraries/System.Net.Http.WinHttpHandler/tests/FunctionalTests/HttpClientHandlerTestBase.WinHttpHandler.cs +++ b/src/libraries/System.Net.Http.WinHttpHandler/tests/FunctionalTests/HttpClientHandlerTestBase.WinHttpHandler.cs @@ -10,7 +10,8 @@ public abstract partial class HttpClientHandlerTestBase : FileCleanupTestBase { protected static bool IsWinHttpHandler => true; - protected static WinHttpClientHandler CreateHttpClientHandler(Version useVersion = null, bool allowAllCertificates = true) + // WinHTTP owns TLS and does not expose SslStream's resumption setting. + protected static WinHttpClientHandler CreateHttpClientHandler(Version useVersion = null, bool allowAllCertificates = true, bool allowTlsResume = true) { useVersion ??= HttpVersion.Version11; @@ -26,8 +27,11 @@ protected static WinHttpClientHandler CreateHttpClientHandler(Version useVersion protected WinHttpClientHandler CreateHttpClientHandler() => CreateHttpClientHandler(UseVersion); - protected static WinHttpClientHandler CreateHttpClientHandler(string useVersionString) => - CreateHttpClientHandler(Version.Parse(useVersionString)); + protected WinHttpClientHandler CreateHttpClientHandler(bool allowTlsResume) => + CreateHttpClientHandler(UseVersion, allowTlsResume: allowTlsResume); + + protected static WinHttpClientHandler CreateHttpClientHandler(string useVersionString, bool allowTlsResume = true) => + CreateHttpClientHandler(Version.Parse(useVersionString), allowTlsResume: allowTlsResume); protected static HttpRequestMessage CreateRequest(HttpMethod method, Uri uri, Version version, bool exactVersion = false) => new HttpRequestMessage(method, uri) diff --git a/src/libraries/System.Net.Http/tests/FunctionalTests/HttpClientHandlerTest.Http3.cs b/src/libraries/System.Net.Http/tests/FunctionalTests/HttpClientHandlerTest.Http3.cs index 1e415acb54e864..a4585c78e2561d 100644 --- a/src/libraries/System.Net.Http/tests/FunctionalTests/HttpClientHandlerTest.Http3.cs +++ b/src/libraries/System.Net.Http/tests/FunctionalTests/HttpClientHandlerTest.Http3.cs @@ -667,7 +667,7 @@ public async Task ServerCertificateCustomValidationCallback_Succeeds() HttpRequestMessage? callbackRequest = null; int invocationCount = 0; - var httpClientHandler = CreateHttpClientHandler(); + var httpClientHandler = CreateHttpClientHandler(allowTlsResume: false); httpClientHandler.ServerCertificateCustomValidationCallback = (request, _, _, _) => { callbackRequest = request; diff --git a/src/libraries/System.Net.Http/tests/FunctionalTests/HttpClientHandlerTestBase.SocketsHttpHandler.cs b/src/libraries/System.Net.Http/tests/FunctionalTests/HttpClientHandlerTestBase.SocketsHttpHandler.cs index f3df45379b81a4..d4689547dfad6f 100644 --- a/src/libraries/System.Net.Http/tests/FunctionalTests/HttpClientHandlerTestBase.SocketsHttpHandler.cs +++ b/src/libraries/System.Net.Http/tests/FunctionalTests/HttpClientHandlerTestBase.SocketsHttpHandler.cs @@ -46,7 +46,7 @@ public static bool IsHttp3Supported } } - protected static HttpClientHandler CreateHttpClientHandler(Version useVersion = null, bool allowAllCertificates = true) + protected static HttpClientHandler CreateHttpClientHandler(Version useVersion = null, bool allowAllCertificates = true, bool allowTlsResume = true) { useVersion ??= HttpVersion.Version11; @@ -58,6 +58,13 @@ protected static HttpClientHandler CreateHttpClientHandler(Version useVersion = handler.ServerCertificateCustomValidationCallback = TestHelper.AllowAllCertificates; } + // Native mobile handlers do not use SslStream or expose its resumption setting. + if (!allowTlsResume && PlatformDetection.IsNotBrowser && PlatformDetection.IsNotWasi && + GetUnderlyingSocketsHttpHandler(handler) is SocketsHttpHandler socketsHandler) + { + socketsHandler.SslOptions.AllowTlsResume = false; + } + return handler; } @@ -71,8 +78,11 @@ protected static Http3LoopbackServer CreateHttp3LoopbackServer(Http3Options opti protected HttpClientHandler CreateHttpClientHandler() => CreateHttpClientHandler(UseVersion); - protected static HttpClientHandler CreateHttpClientHandler(string useVersionString) => - CreateHttpClientHandler(Version.Parse(useVersionString)); + protected HttpClientHandler CreateHttpClientHandler(bool allowTlsResume) => + CreateHttpClientHandler(UseVersion, allowTlsResume: allowTlsResume); + + protected static HttpClientHandler CreateHttpClientHandler(string useVersionString, bool allowTlsResume = true) => + CreateHttpClientHandler(Version.Parse(useVersionString), allowTlsResume: allowTlsResume); protected static SocketsHttpHandler GetUnderlyingSocketsHttpHandler(HttpClientHandler handler) { diff --git a/src/libraries/System.Net.Http/tests/FunctionalTests/SocketsHttpHandlerTest.cs b/src/libraries/System.Net.Http/tests/FunctionalTests/SocketsHttpHandlerTest.cs index 21b42aa2c15656..fb66bb6c6f6955 100644 --- a/src/libraries/System.Net.Http/tests/FunctionalTests/SocketsHttpHandlerTest.cs +++ b/src/libraries/System.Net.Http/tests/FunctionalTests/SocketsHttpHandlerTest.cs @@ -470,6 +470,28 @@ await server.AcceptConnectionAsync(async connection => public sealed class SocketsHttpHandler_HttpClientHandler_ServerCertificates_Test : HttpClientHandler_ServerCertificates_Test { public SocketsHttpHandler_HttpClientHandler_ServerCertificates_Test(ITestOutputHelper output) : base(output) { } + + [Theory] + [InlineData(true)] + [InlineData(false)] + [PlatformSpecific(TestPlatforms.Windows | TestPlatforms.Linux | TestPlatforms.OSX)] + public void CreateHttpClientHandler_AllowTlsResume_ConfiguresSslOptions(bool allowTlsResume) + { + using HttpClientHandler defaultHandler = CreateHttpClientHandler(); + Assert.True(GetUnderlyingSocketsHttpHandler(defaultHandler).SslOptions.AllowTlsResume); + + using HttpClientHandler instanceHandler = CreateHttpClientHandler(allowTlsResume: allowTlsResume); + Assert.Equal(allowTlsResume, GetUnderlyingSocketsHttpHandler(instanceHandler).SslOptions.AllowTlsResume); + Assert.Null(instanceHandler.ServerCertificateCustomValidationCallback); + + using HttpClientHandler versionHandler = CreateHttpClientHandler(UseVersion, allowAllCertificates: false, allowTlsResume: allowTlsResume); + Assert.Equal(allowTlsResume, GetUnderlyingSocketsHttpHandler(versionHandler).SslOptions.AllowTlsResume); + Assert.Null(versionHandler.ServerCertificateCustomValidationCallback); + + using HttpClientHandler stringHandler = CreateHttpClientHandler(UseVersion.ToString(), allowTlsResume: allowTlsResume); + Assert.Equal(allowTlsResume, GetUnderlyingSocketsHttpHandler(stringHandler).SslOptions.AllowTlsResume); + Assert.NotNull(stringHandler.ServerCertificateCustomValidationCallback); + } } [SkipOnPlatform(TestPlatforms.Browser, "ResponseDrainTimeout is not supported on Browser")] @@ -6233,7 +6255,7 @@ await LoopbackServerFactory.CreateClientAndServerAsync( policy.ExtraStore.AddRange(_certificateSetup.ServerChain); policy.CustomTrustStore.Add(_certificateSetup.ServerChain[^1]); - socketsHandler.SslOptions = new SslClientAuthenticationOptions() { CertificateChainPolicy = policy }; + socketsHandler.SslOptions = new SslClientAuthenticationOptions() { CertificateChainPolicy = policy, AllowTlsResume = false }; using HttpClient client = CreateHttpClient(handler); client.DefaultVersionPolicy = HttpVersionPolicy.RequestVersionExact; HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, uri) { Version = UseVersion, VersionPolicy = HttpVersionPolicy.RequestVersionExact }; @@ -6266,7 +6288,7 @@ await LoopbackServerFactory.CreateClientAndServerAsync( policy.ExtraStore.AddRange(_certificateSetup.ServerChain); policy.CustomTrustStore.Add(_certificateSetup.ServerChain[^1]); - socketsHandler.SslOptions = new SslClientAuthenticationOptions() { CertificateChainPolicy = policy }; + socketsHandler.SslOptions = new SslClientAuthenticationOptions() { CertificateChainPolicy = policy, AllowTlsResume = false }; using HttpClient client = CreateHttpClient(handler); client.DefaultVersionPolicy = HttpVersionPolicy.RequestVersionExact; HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, uri) { Version = UseVersion, VersionPolicy = HttpVersionPolicy.RequestVersionExact }; @@ -6305,7 +6327,7 @@ await LoopbackServerFactory.CreateClientAndServerAsync( policy.ExtraStore.AddRange(_certificateSetup.ServerChain); policy.CustomTrustStore.Add(_certificateSetup.ServerChain[^1]); - socketsHandler.SslOptions = new SslClientAuthenticationOptions() { CertificateChainPolicy = policy }; + socketsHandler.SslOptions = new SslClientAuthenticationOptions() { CertificateChainPolicy = policy, AllowTlsResume = false }; using HttpClient client = CreateHttpClient(handler); client.DefaultVersionPolicy = HttpVersionPolicy.RequestVersionExact; From 2403bdc4c7299771ae0a9001baf6d6770a998cc0 Mon Sep 17 00:00:00 2001 From: Radek Zikmund Date: Tue, 6 Oct 2026 15:06:53 +0200 Subject: [PATCH 2/2] Isolate WebSocket certificate callback test from TLS resumption Allow loopback servers to disable TLS resumption while retaining the default. Require fresh validation in the ClientWebSocket callback test and cover successive secure connections. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .../tests/System/Net/Http/LoopbackServer.cs | 2 ++ .../tests/ClientWebSocketOptionsTests.cs | 36 +++++++++++-------- 2 files changed, 23 insertions(+), 15 deletions(-) diff --git a/src/libraries/Common/tests/System/Net/Http/LoopbackServer.cs b/src/libraries/Common/tests/System/Net/Http/LoopbackServer.cs index 3bb37e43bdb983..178cf4d53e035a 100644 --- a/src/libraries/Common/tests/System/Net/Http/LoopbackServer.cs +++ b/src/libraries/Common/tests/System/Net/Http/LoopbackServer.cs @@ -431,6 +431,7 @@ public static string GetConnectionCloseResponse(HttpStatusCode statusCode = Http public class Options : GenericLoopbackOptions { + public bool AllowTlsResume { get; set; } = true; public bool WebSocketEndpoint { get; set; } = false; public Func StreamWrapper { get; set; } public string Username { get; set; } @@ -483,6 +484,7 @@ public static async Task CreateAsync(SocketWrapper socket, Stream st #if !NETFRAMEWORK SslServerAuthenticationOptions sslOptions = new SslServerAuthenticationOptions() { + AllowTlsResume = httpOptions.AllowTlsResume, EnabledSslProtocols = httpOptions.SslProtocols, ServerCertificateContext = httpOptions.CertificateContext ?? SslStreamCertificateContext.Create(Configuration.Certificates.GetServerCertificate(), null), ClientCertificateRequired = true, diff --git a/src/libraries/System.Net.WebSockets.Client/tests/ClientWebSocketOptionsTests.cs b/src/libraries/System.Net.WebSockets.Client/tests/ClientWebSocketOptionsTests.cs index 704ceba0fbcb11..bf3100c5c54753 100644 --- a/src/libraries/System.Net.WebSockets.Client/tests/ClientWebSocketOptionsTests.cs +++ b/src/libraries/System.Net.WebSockets.Client/tests/ClientWebSocketOptionsTests.cs @@ -2,6 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.Net.Http; +using System.Net.Http.Functional.Tests; using System.Net.Security; using System.Net.Test.Common; using System.Security.Cryptography.X509Certificates; @@ -173,36 +174,41 @@ public void RemoteCertificateValidationCallback_Roundtrips() [OuterLoop("Connects to remote service")] [ConditionalTheory(typeof(ClientWebSocketOptionsTests), nameof(WebSocketsSupported))] - [InlineData(false)] - [InlineData(true)] + [InlineData(false, 1)] + [InlineData(true, 1)] + [InlineData(true, 3)] [SkipOnPlatform(TestPlatforms.Browser, "Certificates not supported on browser")] - public async Task RemoteCertificateValidationCallback_PassedRemoteCertificateInfo(bool secure) + public async Task RemoteCertificateValidationCallback_PassedRemoteCertificateInfo(bool secure, int connectionCount) { - bool callbackInvoked = false; + int callbackCount = 0; - await LoopbackServer.CreateClientAndServerAsync(async uri => + // Disable resumption on the server because ClientWebSocketOptions has no such setting. + await LoopbackServer.CreateServerAsync(async (server, uri) => { - using (var cws = new ClientWebSocket()) - using (var cts = new CancellationTokenSource(TimeOutMilliseconds)) + for (int i = 0; i < connectionCount; i++) { + using var cws = new ClientWebSocket(); + using var cts = new CancellationTokenSource(TimeOutMilliseconds); cws.Options.RemoteCertificateValidationCallback = (source, cert, chain, errors) => { Assert.NotNull(source); Assert.NotNull(cert); Assert.NotNull(chain); Assert.NotEqual(SslPolicyErrors.None, errors); - callbackInvoked = true; + callbackCount++; return true; }; - await cws.ConnectAsync(uri, cts.Token); + await TestHelper.WhenAllCompletedOrAnyFailed( + cws.ConnectAsync(uri, cts.Token), + server.AcceptConnectionAsync(async connection => + { + Assert.NotNull(await LoopbackHelper.WebSocketHandshakeAsync(connection)); + })); + Assert.Equal(secure ? i + 1 : 0, callbackCount); } - }, server => server.AcceptConnectionAsync(async connection => - { - Assert.NotNull(await LoopbackHelper.WebSocketHandshakeAsync(connection)); - }), - new LoopbackServer.Options { UseSsl = secure, WebSocketEndpoint = true }); + }, new LoopbackServer.Options { UseSsl = secure, WebSocketEndpoint = true, AllowTlsResume = false }); - Assert.Equal(secure, callbackInvoked); + Assert.Equal(secure ? connectionCount : 0, callbackCount); } [OuterLoop("Connects to remote service")]