diff --git a/src/libraries/Common/tests/System/Net/Configuration.Http.cs b/src/libraries/Common/tests/System/Net/Configuration.Http.cs index 15d426ce9bab42..23e7c7a011f179 100644 --- a/src/libraries/Common/tests/System/Net/Configuration.Http.cs +++ b/src/libraries/Common/tests/System/Net/Configuration.Http.cs @@ -37,9 +37,6 @@ public static partial class Http public static string TLSv11RemoteServer => GetValue("DOTNET_TEST_HTTPHOST_TLS11", "https://www.ssllabs.com:10302/"); public static string TLSv12RemoteServer => GetValue("DOTNET_TEST_HTTPHOST_TLS12", "https://www.ssllabs.com:10303/"); - public static string ExpiredCertRemoteServer => GetValue("DOTNET_TEST_HTTPHOST_EXPIREDCERT", "https://expired.badssl.com/"); - public static string WrongHostNameCertRemoteServer => GetValue("DOTNET_TEST_HTTPHOST_WRONGHOSTNAME", "https://wrong.host.badssl.com/"); - public static string SelfSignedCertRemoteServer => GetValue("DOTNET_TEST_HTTPHOST_SELFSIGNEDCERT", "https://self-signed.badssl.com/"); public static string RevokedCertRemoteServer => GetValue("DOTNET_TEST_HTTPHOST_REVOKEDCERT", "https://revoked.badssl.com/"); public static string EchoClientCertificateRemoteServer => GetValue("DOTNET_TEST_HTTPHOST_ECHOCLIENTCERT", "https://corefx-net-tls.azurewebsites.net/EchoClientCertificate.ashx"); diff --git a/src/libraries/Common/tests/System/Net/Http/HttpClientHandlerTest.AcceptAllCerts.cs b/src/libraries/Common/tests/System/Net/Http/HttpClientHandlerTest.AcceptAllCerts.cs index ec0e0395c6a80f..eacbc846604d82 100644 --- a/src/libraries/Common/tests/System/Net/Http/HttpClientHandlerTest.AcceptAllCerts.cs +++ b/src/libraries/Common/tests/System/Net/Http/HttpClientHandlerTest.AcceptAllCerts.cs @@ -4,23 +4,19 @@ using System.Net.Security; using System.Net.Test.Common; using System.Security.Authentication; +using System.Security.Cryptography.X509Certificates; using System.Threading.Tasks; -using Microsoft.DotNet.XUnitExtensions; using Xunit; using Xunit.Abstractions; namespace System.Net.Http.Functional.Tests { - using Configuration = System.Net.Test.Common.Configuration; - #if WINHTTPHANDLER_TEST using HttpClientHandler = System.Net.Http.WinHttpClientHandler; #endif public abstract class HttpClientHandler_DangerousAcceptAllCertificatesValidator_Test : HttpClientHandlerTestBase { - private static bool ClientSupportsDHECipherSuites => (!PlatformDetection.IsWindows || PlatformDetection.IsWindows10Version1607OrGreater); - public HttpClientHandler_DangerousAcceptAllCertificatesValidator_Test(ITestOutputHelper output) : base(output) { } #if !WINHTTPHANDLER_TEST @@ -86,22 +82,30 @@ await TestHelper.WhenAllCompletedOrAnyFailed( } } - public static readonly object[][] InvalidCertificateServers = + [Theory] + [InlineData(true, false)] + [InlineData(false, false)] + [InlineData(false, true)] + [InlineData(true, true)] + public async Task InvalidCertificates_CertificateValidationDisabled_Succeeds(bool expired, bool wrongHostName) { - new object[] { Configuration.Http.ExpiredCertRemoteServer }, - new object[] { Configuration.Http.SelfSignedCertRemoteServer }, - new object[] { Configuration.Http.WrongHostNameCertRemoteServer }, - }; + using X509Certificate2 certificate = TestHelper.CreateServerSelfSignedCertificate( + name: wrongHostName ? "not-localhost" : "localhost", expired: expired); - [OuterLoop] - [ConditionalTheory(typeof(HttpClientHandler_DangerousAcceptAllCertificatesValidator_Test), nameof(ClientSupportsDHECipherSuites))] - [MemberData(nameof(InvalidCertificateServers))] - public async Task InvalidCertificateServers_CertificateValidationDisabled_Succeeds(string url) - { using (HttpClientHandler handler = CreateHttpClientHandler(allowAllCertificates: true)) using (HttpClient client = CreateHttpClient(handler)) { - (await client.GetAsync(url)).Dispose(); + await LoopbackServerFactory.CreateClientAndServerAsync(async uri => + { + using HttpResponseMessage response = await client.GetAsync(new UriBuilder(uri) { Host = "localhost" }.Uri); + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + }, server => server.HandleRequestAsync(), + options: new GenericLoopbackOptions + { + UseSsl = true, + Certificate = certificate, + SslProtocols = SslProtocols.None, + }); } } } 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 c3fbf655195105..e41661190166b8 100644 --- a/src/libraries/Common/tests/System/Net/Http/HttpClientHandlerTest.ServerCertificates.cs +++ b/src/libraries/Common/tests/System/Net/Http/HttpClientHandlerTest.ServerCertificates.cs @@ -2,12 +2,17 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.Collections.Generic; +#if WINHTTPHANDLER_TEST +using System.ComponentModel; +#endif using System.Diagnostics; using System.IO; using System.Linq; using System.Net.Security; +using System.Net.Sockets; using System.Net.Test.Common; using System.Runtime.InteropServices; +using System.Security.Authentication; using System.Security.Authentication.ExtendedProtection; using System.Security.Cryptography.X509Certificates; using System.Threading.Tasks; @@ -26,7 +31,9 @@ namespace System.Net.Http.Functional.Tests public abstract partial class HttpClientHandler_ServerCertificates_Test : HttpClientHandlerTestBase { +#if WINHTTPHANDLER_TEST private static bool ClientSupportsDHECipherSuites => (!PlatformDetection.IsWindows || PlatformDetection.IsWindows10Version1607OrGreater); +#endif public HttpClientHandler_ServerCertificates_Test(ITestOutputHelper output) : base(output) { } @@ -211,25 +218,86 @@ public async Task UseCallback_CallbackThrowsException_ExceptionPropagatesAsBaseE } } - public static readonly object[][] CertificateValidationServers = + [Theory] + [InlineData(true, false)] + [InlineData(false, false)] + [InlineData(false, true)] + public async Task NoCallback_BadCertificate_ThrowsException(bool expired, bool wrongHostName) { - new object[] { Configuration.Http.ExpiredCertRemoteServer }, - new object[] { Configuration.Http.SelfSignedCertRemoteServer }, - new object[] { Configuration.Http.WrongHostNameCertRemoteServer }, - }; + using X509Certificate2 certificate = TestHelper.CreateServerSelfSignedCertificate( + name: wrongHostName ? "not-localhost" : "localhost", expired: expired); - [OuterLoop("Uses external servers")] - [ConditionalTheory(typeof(HttpClientHandler_ServerCertificates_Test), nameof(ClientSupportsDHECipherSuites))] - [MemberData(nameof(CertificateValidationServers))] - public async Task NoCallback_BadCertificate_ThrowsException(string url) - { using (HttpClientHandler handler = CreateHttpClientHandler(allowTlsResume: false)) using (HttpClient client = CreateHttpClient(handler)) { - await Assert.ThrowsAsync(() => client.GetAsync(url)); + bool customChainPolicy = ConfigureCertificateChainPolicy(handler, certificate, trustCertificate: expired || wrongHostName); + + await LoopbackServer.CreateClientAndServerAsync(async uri => + { + HttpRequestException exception = await Assert.ThrowsAsync( + () => client.GetAsync(new UriBuilder(uri) { Host = "localhost" }.Uri)); +#if WINHTTPHANDLER_TEST + const int ErrorWinHttpSecureFailure = 12175; + Assert.Equal(ErrorWinHttpSecureFailure, Assert.IsAssignableFrom(exception.InnerException).NativeErrorCode); +#else + if (customChainPolicy) + { + Assert.Equal(HttpRequestError.SecureConnectionError, exception.HttpRequestError); + Assert.IsType(exception.InnerException); + } +#endif + }, async server => + { + try + { + if (customChainPolicy || IsWinHttpHandler) + { + await server.AcceptConnectionAsync(_ => Task.CompletedTask); + } + else + { + // Native handlers may report generic errors, so acceptance must produce a successful response. + await server.HandleRequestAsync(); + } + } + catch (Exception e) when (e is AuthenticationException or IOException) + { + _output.WriteLine($"Expected certificate validation failure: {e}"); + } + }, + options: new LoopbackServer.Options + { + UseSsl = true, + Certificate = certificate, + SslProtocols = SslProtocols.None, + }); } } + [Fact] + public async Task LoopbackServer_AuthenticationFailure_DisposesStream() + { + bool streamDisposed = false; + var expectedException = new IOException("Simulated TLS transport failure."); + using var stream = new DelegateStream( + canReadFunc: () => true, + canWriteFunc: () => true, + readAsyncFunc: (_, _, _, _) => Task.FromException(expectedException), + writeAsyncFunc: (_, _, _, _) => Task.CompletedTask, + disposeFunc: disposing => streamDisposed |= disposing); + using var socket = new SocketWrapper(new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp)); + using X509Certificate2 certificate = TestHelper.CreateServerSelfSignedCertificate(); + var options = new LoopbackServer.Options { UseSsl = true, Certificate = certificate }; +#if !NETFRAMEWORK + options.CertificateContext = SslStreamCertificateContext.Create(certificate, null); +#endif + + IOException exception = await Assert.ThrowsAsync(() => LoopbackServer.Connection.CreateAsync(socket, stream, options)); + Assert.Same(expectedException, exception.GetBaseException()); + Assert.True(streamDisposed); + } + +#if WINHTTPHANDLER_TEST [OuterLoop("Uses external servers")] [ConditionalFact(typeof(HttpClientHandler_ServerCertificates_Test), nameof(ClientSupportsDHECipherSuites))] [ActiveIssue("https://github.com/dotnet/runtime/issues/106634", typeof(PlatformDetection), nameof(PlatformDetection.IsAlpine))] @@ -255,70 +323,85 @@ public async Task NoCallback_RevokedCertificate_RevocationChecking_Fails() await Assert.ThrowsAsync(() => client.GetAsync(Configuration.Http.RevokedCertRemoteServer)); } } +#endif - public static readonly object[][] CertificateValidationServersAndExpectedPolicies = + [Theory] + [InlineData(true, false, SslPolicyErrors.RemoteCertificateChainErrors)] + [InlineData(false, false, SslPolicyErrors.RemoteCertificateChainErrors)] + [InlineData(false, true, SslPolicyErrors.RemoteCertificateNameMismatch)] + public async Task UseCallback_BadCertificate_ExpectedPolicyErrors(bool expired, bool wrongHostName, SslPolicyErrors expectedErrors) { - new object[] { Configuration.Http.ExpiredCertRemoteServer, SslPolicyErrors.RemoteCertificateChainErrors }, - new object[] { Configuration.Http.WrongHostNameCertRemoteServer , SslPolicyErrors.RemoteCertificateNameMismatch}, - }; + using X509Certificate2 certificate = TestHelper.CreateServerSelfSignedCertificate( + name: wrongHostName ? "not-localhost" : "localhost", expired: expired); - private async Task UseCallback_BadCertificate_ExpectedPolicyErrors_Helper(string url, string useHttp2String, SslPolicyErrors expectedErrors) - { - HttpClientHandler handler = CreateHttpClientHandler(useHttp2String, allowTlsResume: false); - using (HttpClient client = CreateHttpClient(handler, useHttp2String)) + using (HttpClientHandler handler = CreateHttpClientHandler(allowTlsResume: false)) + using (HttpClient client = CreateHttpClient(handler)) { + bool customChainPolicy = ConfigureCertificateChainPolicy(handler, certificate, trustCertificate: expired || wrongHostName); + X509ChainStatusFlags expectedChainStatus = expired ? X509ChainStatusFlags.NotTimeValid : + wrongHostName ? X509ChainStatusFlags.NoError : X509ChainStatusFlags.UntrustedRoot; + + if (!customChainPolicy) + { + // Native handlers cannot trust the generated root without changing the OS certificate store. + expectedErrors |= SslPolicyErrors.RemoteCertificateChainErrors; + expectedChainStatus |= X509ChainStatusFlags.UntrustedRoot; + } + + string expectedThumbprint = certificate.Thumbprint; bool callbackCalled = false; handler.ServerCertificateCustomValidationCallback = (request, cert, chain, errors) => { - // https://github.com/dotnet/corefx/issues/21922#issuecomment-315555237 - X509ChainStatusFlags flags = chain.ChainStatus.Aggregate(X509ChainStatusFlags.NoError, (cur, status) => cur | status.Status); - if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX) && - flags == X509ChainStatusFlags.RevocationStatusUnknown) - { - expectedErrors |= SslPolicyErrors.RemoteCertificateChainErrors; - } - callbackCalled = true; Assert.NotNull(request); Assert.NotNull(cert); Assert.NotNull(chain); + Assert.Equal(expectedThumbprint, cert.Thumbprint); Assert.Equal(expectedErrors, errors); + Assert.Equal(expectedChainStatus, chain.ChainStatus.Aggregate( + X509ChainStatusFlags.NoError, (flags, status) => flags | status.Status)); return true; }; - using (HttpResponseMessage response = await client.GetAsync(url)) + await LoopbackServerFactory.CreateClientAndServerAsync(async uri => { + using HttpResponseMessage response = await client.GetAsync(new UriBuilder(uri) { Host = "localhost" }.Uri); Assert.Equal(HttpStatusCode.OK, response.StatusCode); - } + }, server => server.HandleRequestAsync(), + options: new GenericLoopbackOptions + { + UseSsl = true, + Certificate = certificate, + SslProtocols = SslProtocols.None, + }); Assert.True(callbackCalled); } } - [OuterLoop("Uses external servers")] - [Theory] - [MemberData(nameof(CertificateValidationServersAndExpectedPolicies))] - public async Task UseCallback_BadCertificate_ExpectedPolicyErrors(string url, SslPolicyErrors expectedErrors) + private static bool ConfigureCertificateChainPolicy(HttpClientHandler handler, X509Certificate2 certificate, bool trustCertificate) { - const int SEC_E_BUFFER_TOO_SMALL = unchecked((int)0x80090321); - - if (!ClientSupportsDHECipherSuites) +#if !WINHTTPHANDLER_TEST + if (GetUnderlyingSocketsHttpHandler(handler) is SocketsHttpHandler socketsHandler) { - return; - } + var policy = new X509ChainPolicy + { + TrustMode = X509ChainTrustMode.CustomRootTrust, + RevocationMode = X509RevocationMode.NoCheck, + DisableCertificateDownloads = true, + }; - try - { - await UseCallback_BadCertificate_ExpectedPolicyErrors_Helper(url, UseVersion.ToString(), expectedErrors); - } - catch (HttpRequestException e) when (e.InnerException?.GetType().Name == "WinHttpException" && - e.InnerException.HResult == SEC_E_BUFFER_TOO_SMALL && - !PlatformDetection.IsWindows10Version1607OrGreater) - { - // Testing on old Windows versions can hit https://github.com/dotnet/runtime/issues/17005 - // Ignore SEC_E_BUFFER_TOO_SMALL error on such cases. + if (trustCertificate) + { + policy.CustomTrustStore.Add(certificate); + } + + socketsHandler.SslOptions.CertificateChainPolicy = policy; + return true; } +#endif + return false; } [Theory] diff --git a/src/libraries/Common/tests/System/Net/Http/LoopbackServer.cs b/src/libraries/Common/tests/System/Net/Http/LoopbackServer.cs index 178cf4d53e035a..b27868b239f29d 100644 --- a/src/libraries/Common/tests/System/Net/Http/LoopbackServer.cs +++ b/src/libraries/Common/tests/System/Net/Http/LoopbackServer.cs @@ -481,26 +481,35 @@ public static async Task CreateAsync(SocketWrapper socket, Stream st if (httpOptions.UseSsl) { var sslStream = new SslStream(stream, false, delegate { return true; }); -#if !NETFRAMEWORK - SslServerAuthenticationOptions sslOptions = new SslServerAuthenticationOptions() + try { - AllowTlsResume = httpOptions.AllowTlsResume, - EnabledSslProtocols = httpOptions.SslProtocols, - ServerCertificateContext = httpOptions.CertificateContext ?? SslStreamCertificateContext.Create(Configuration.Certificates.GetServerCertificate(), null), - ClientCertificateRequired = true, - }; +#if !NETFRAMEWORK + SslServerAuthenticationOptions sslOptions = new SslServerAuthenticationOptions() + { + AllowTlsResume = httpOptions.AllowTlsResume, + EnabledSslProtocols = httpOptions.SslProtocols, + ServerCertificateContext = httpOptions.CertificateContext ?? SslStreamCertificateContext.Create(Configuration.Certificates.GetServerCertificate(), null), + ClientCertificateRequired = true, + }; - await sslStream.AuthenticateAsServerAsync(sslOptions, default).ConfigureAwait(false); + await sslStream.AuthenticateAsServerAsync(sslOptions, default).ConfigureAwait(false); #else - using (X509Certificate2 cert = httpOptions.Certificate ?? Configuration.Certificates.GetServerCertificate()) + using (X509Certificate2 cert = httpOptions.Certificate ?? Configuration.Certificates.GetServerCertificate()) + { + await sslStream.AuthenticateAsServerAsync( + cert, + clientCertificateRequired: true, // allowed but not required + enabledSslProtocols: httpOptions.SslProtocols, + checkCertificateRevocation: false).ConfigureAwait(false); + } +#endif + } + catch { - await sslStream.AuthenticateAsServerAsync( - cert, - clientCertificateRequired: true, // allowed but not required - enabledSslProtocols: httpOptions.SslProtocols, - checkCertificateRevocation: false).ConfigureAwait(false); + sslStream.Dispose(); + throw; } -#endif + stream = sslStream; } diff --git a/src/libraries/Common/tests/System/Net/Http/TestHelper.cs b/src/libraries/Common/tests/System/Net/Http/TestHelper.cs index 8ff0e437526bac..929b3b4db42715 100644 --- a/src/libraries/Common/tests/System/Net/Http/TestHelper.cs +++ b/src/libraries/Common/tests/System/Net/Http/TestHelper.cs @@ -109,7 +109,7 @@ public static byte[] GenerateRandomContent(int size) return data; } - public static X509Certificate2 CreateServerSelfSignedCertificate(string name = "localhost") + public static X509Certificate2 CreateServerSelfSignedCertificate(string name = "localhost", bool expired = false) { using (RSA root = RSA.Create()) { @@ -136,13 +136,16 @@ public static X509Certificate2 CreateServerSelfSignedCertificate(string name = " builder.AddIpAddress(IPAddress.IPv6Loopback); req.CertificateExtensions.Add(builder.Build()); - DateTimeOffset start = DateTimeOffset.UtcNow.AddMinutes(-5); + DateTimeOffset start = expired ? DateTimeOffset.UtcNow.AddMonths(-2) : DateTimeOffset.UtcNow.AddMinutes(-5); DateTimeOffset end = start.AddMonths(1); X509Certificate2 cert = req.CreateSelfSigned(start, end); if (PlatformDetection.IsWindows) { - cert = X509CertificateLoader.LoadPkcs12(cert.Export(X509ContentType.Pfx), (string?)null); + using (cert) + { + return X509CertificateLoader.LoadPkcs12(cert.Export(X509ContentType.Pfx), (string?)null); + } } return cert; diff --git a/src/libraries/System.Net.Http/tests/FunctionalTests/SocketsHttpHandlerTest.cs b/src/libraries/System.Net.Http/tests/FunctionalTests/SocketsHttpHandlerTest.cs index fb66bb6c6f6955..7b610a41e6b978 100644 --- a/src/libraries/System.Net.Http/tests/FunctionalTests/SocketsHttpHandlerTest.cs +++ b/src/libraries/System.Net.Http/tests/FunctionalTests/SocketsHttpHandlerTest.cs @@ -492,6 +492,77 @@ public void CreateHttpClientHandler_AllowTlsResume_ConfiguresSslOptions(bool all Assert.Equal(allowTlsResume, GetUnderlyingSocketsHttpHandler(stringHandler).SslOptions.AllowTlsResume); Assert.NotNull(stringHandler.ServerCertificateCustomValidationCallback); } + + [Theory] + [InlineData(false)] + [InlineData(true)] + [PlatformSpecific(TestPlatforms.Windows | TestPlatforms.Linux | TestPlatforms.OSX)] + public async Task NoCallback_RevokedCertificate_RevocationChecking_MatchesExpected(bool checkRevocation) + { + using Configuration.Certificates.PkiHolder pkiHolder = Configuration.Certificates.GenerateCertificates( + "localhost", forceRsaCertificate: true); + pkiHolder.Intermediates[^1].Revoke(pkiHolder.EndEntity, pkiHolder.EndEntity.NotBefore); + + using HttpClientHandler handler = CreateHttpClientHandler(allowTlsResume: false); + handler.CheckCertificateRevocationList = checkRevocation; + SocketsHttpHandler socketsHandler = GetUnderlyingSocketsHttpHandler(handler); + + var policy = new X509ChainPolicy + { + TrustMode = X509ChainTrustMode.CustomRootTrust, + RevocationMode = socketsHandler.SslOptions.CertificateRevocationCheckMode, + RevocationFlag = X509RevocationFlag.EndCertificateOnly, + DisableCertificateDownloads = true, + CustomTrustStore = { pkiHolder.IssuerChain[^1] }, + }; + policy.ExtraStore.AddRange(pkiHolder.IssuerChain); + socketsHandler.SslOptions.CertificateChainPolicy = policy; + + using HttpClient client = CreateHttpClient(handler); + await LoopbackServerFactory.CreateClientAndServerAsync(async uri => + { + if (checkRevocation) + { + HttpRequestException exception = await Assert.ThrowsAsync(() => client.GetAsync(uri)); + Assert.Equal(HttpRequestError.SecureConnectionError, exception.HttpRequestError); + Assert.IsType(exception.InnerException); + } + else + { + using HttpResponseMessage response = await client.GetAsync(uri); + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + } + }, async server => + { + try + { + if (checkRevocation) + { + await server.AcceptConnectionAsync(_ => Task.CompletedTask); + } + else + { + await server.HandleRequestAsync(); + } + } + catch (Exception e) when (checkRevocation && (e is AuthenticationException or IOException)) + { + _output.WriteLine($"Expected certificate validation failure: {e}"); + } + }, + options: new GenericLoopbackOptions + { + UseSsl = true, + Certificate = pkiHolder.EndEntity, + SslProtocols = SslProtocols.None, + }); + + using X509Chain chain = new X509Chain(); + chain.ChainPolicy = policy; + Assert.Equal(!checkRevocation, chain.Build(pkiHolder.EndEntity)); + Assert.Equal(checkRevocation ? X509ChainStatusFlags.Revoked : X509ChainStatusFlags.NoError, + chain.ChainStatus.Aggregate(X509ChainStatusFlags.NoError, (flags, status) => flags | status.Status)); + } } [SkipOnPlatform(TestPlatforms.Browser, "ResponseDrainTimeout is not supported on Browser")]