diff --git a/src/libraries/Common/tests/System/Net/Configuration.Certificates.Dynamic.cs b/src/libraries/Common/tests/System/Net/Configuration.Certificates.Dynamic.cs new file mode 100644 index 0000000000000..6becbe26b1acf --- /dev/null +++ b/src/libraries/Common/tests/System/Net/Configuration.Certificates.Dynamic.cs @@ -0,0 +1,176 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using System.Security.Cryptography; +using System.Security.Cryptography.X509Certificates; +using System.Security.Cryptography.X509Certificates.Tests.Common; +using Test.Cryptography; + +namespace System.Net.Test.Common +{ + public static partial class Configuration + { + public static partial class Certificates + { + private static readonly X509KeyUsageExtension s_eeKeyUsage = + new X509KeyUsageExtension( + X509KeyUsageFlags.DigitalSignature | X509KeyUsageFlags.KeyEncipherment | X509KeyUsageFlags.DataEncipherment, + critical: false); + + private static readonly X509EnhancedKeyUsageExtension s_tlsServerEku = + new X509EnhancedKeyUsageExtension( + new OidCollection + { + new Oid("1.3.6.1.5.5.7.3.1", null) + }, + false); + + private static readonly X509EnhancedKeyUsageExtension s_tlsClientEku = + new X509EnhancedKeyUsageExtension( + new OidCollection + { + new Oid("1.3.6.1.5.5.7.3.2", null) + }, + false); + + private static readonly X509BasicConstraintsExtension s_eeConstraints = + new X509BasicConstraintsExtension(false, false, 0, false); + + private static X509Certificate2 s_dynamicServerCertificate; + private static X509Certificate2Collection s_dynamicCaCertificates; + private static object certLock = new object(); + + + // These Get* methods make a copy of the certificates so that consumers own the lifetime of the + // certificates handed back. Consumers are expected to dispose of their certs when done with them. + + public static X509Certificate2 GetDynamicServerCerttificate(X509Certificate2Collection? chainCertificates) + { + lock (certLock) + { + if (s_dynamicServerCertificate == null) + { + CleanupCertificates(); + (s_dynamicServerCertificate, s_dynamicCaCertificates) = GenerateCertificates("localhost", nameof(Configuration) + nameof(Certificates)); + } + + chainCertificates?.AddRange(s_dynamicCaCertificates); + return new X509Certificate2(s_dynamicServerCertificate); + } + } + + public static void CleanupCertificates([CallerMemberName] string? testName = null, StoreName storeName = StoreName.CertificateAuthority) + { + string caName = $"O={testName}"; + try + { + using (X509Store store = new X509Store(storeName, StoreLocation.LocalMachine)) + { + store.Open(OpenFlags.ReadWrite); + foreach (X509Certificate2 cert in store.Certificates) + { + if (cert.Subject.Contains(caName)) + { + store.Remove(cert); + } + cert.Dispose(); + } + } + } + catch { }; + + try + { + using (X509Store store = new X509Store(storeName, StoreLocation.CurrentUser)) + { + store.Open(OpenFlags.ReadWrite); + foreach (X509Certificate2 cert in store.Certificates) + { + if (cert.Subject.Contains(caName)) + { + store.Remove(cert); + } + cert.Dispose(); + } + } + } + catch { }; + } + + private static X509ExtensionCollection BuildTlsServerCertExtensions(string serverName) + { + return BuildTlsCertExtensions(serverName, true); + } + + private static X509ExtensionCollection BuildTlsCertExtensions(string targetName, bool serverCertificate) + { + X509ExtensionCollection extensions = new X509ExtensionCollection(); + + SubjectAlternativeNameBuilder builder = new SubjectAlternativeNameBuilder(); + builder.AddDnsName(targetName); + builder.AddIpAddress(IPAddress.Loopback); + builder.AddIpAddress(IPAddress.IPv6Loopback); + extensions.Add(builder.Build()); + extensions.Add(s_eeConstraints); + extensions.Add(s_eeKeyUsage); + extensions.Add(serverCertificate ? s_tlsServerEku : s_tlsClientEku); + + return extensions; + } + + public static (X509Certificate2 certificate, X509Certificate2Collection) GenerateCertificates(string targetName, [CallerMemberName] string? testName = null, bool longChain = false, bool serverCertificate = true) + { + const int keySize = 2048; + if (PlatformDetection.IsWindows && testName != null) + { + CleanupCertificates(testName); + } + + X509Certificate2Collection chain = new X509Certificate2Collection(); + X509ExtensionCollection extensions = BuildTlsCertExtensions(targetName, serverCertificate); + + CertificateAuthority.BuildPrivatePki( + PkiOptions.IssuerRevocationViaCrl, + out RevocationResponder responder, + out CertificateAuthority root, + out CertificateAuthority[] intermediates, + out X509Certificate2 endEntity, + intermediateAuthorityCount: longChain ? 3 : 1, + subjectName: targetName, + testName: testName, + keySize: keySize, + extensions: extensions); + + // Walk the intermediates backwards so we build the chain collection as + // Issuer3 + // Issuer2 + // Issuer1 + // Root + for (int i = intermediates.Length - 1; i >= 0; i--) + { + CertificateAuthority authority = intermediates[i]; + + chain.Add(authority.CloneIssuerCert()); + authority.Dispose(); + } + + chain.Add(root.CloneIssuerCert()); + + responder.Dispose(); + root.Dispose(); + + if (PlatformDetection.IsWindows) + { + X509Certificate2 ephemeral = endEntity; + endEntity = new X509Certificate2(endEntity.Export(X509ContentType.Pfx)); + ephemeral.Dispose(); + } + + return (endEntity, chain); + } + + } + } +} diff --git a/src/libraries/Common/tests/System/Net/Http/Http2LoopbackConnection.cs b/src/libraries/Common/tests/System/Net/Http/Http2LoopbackConnection.cs index e9e72542b9fc0..8370a489db43a 100644 --- a/src/libraries/Common/tests/System/Net/Http/Http2LoopbackConnection.cs +++ b/src/libraries/Common/tests/System/Net/Http/Http2LoopbackConnection.cs @@ -54,7 +54,7 @@ public static async Task CreateAsync(SocketWrapper sock { var sslStream = new SslStream(stream, false, delegate { return true; }); - using (X509Certificate2 cert = Configuration.Certificates.GetServerCertificate()) + using (X509Certificate2 cert = httpOptions.Certificate ?? Configuration.Certificates.GetServerCertificate()) { #if !NETFRAMEWORK SslServerAuthenticationOptions options = new SslServerAuthenticationOptions(); diff --git a/src/libraries/Common/tests/System/Net/Http/Http2LoopbackServer.cs b/src/libraries/Common/tests/System/Net/Http/Http2LoopbackServer.cs index e6ac7ae626640..3d5802ab72295 100644 --- a/src/libraries/Common/tests/System/Net/Http/Http2LoopbackServer.cs +++ b/src/libraries/Common/tests/System/Net/Http/Http2LoopbackServer.cs @@ -221,6 +221,7 @@ private static Http2Options CreateOptions(GenericLoopbackOptions options) { http2Options.Address = options.Address; http2Options.UseSsl = options.UseSsl; + http2Options.Certificate = options.Certificate; http2Options.SslProtocols = options.SslProtocols; http2Options.ListenBacklog = options.ListenBacklog; } diff --git a/src/libraries/Common/tests/System/Net/Http/Http3LoopbackServer.cs b/src/libraries/Common/tests/System/Net/Http/Http3LoopbackServer.cs index 60328d0b9f13f..187490932cfa0 100644 --- a/src/libraries/Common/tests/System/Net/Http/Http3LoopbackServer.cs +++ b/src/libraries/Common/tests/System/Net/Http/Http3LoopbackServer.cs @@ -23,7 +23,7 @@ public Http3LoopbackServer(Http3Options options = null) { options ??= new Http3Options(); - _cert = Configuration.Certificates.GetServerCertificate(); + _cert = options.Certificate ?? Configuration.Certificates.GetServerCertificate(); var listenerOptions = new QuicListenerOptions() { @@ -130,6 +130,7 @@ private static Http3Options CreateOptions(GenericLoopbackOptions options) { http3Options.Address = options.Address; http3Options.UseSsl = options.UseSsl; + http3Options.Certificate = options.Certificate; http3Options.SslProtocols = options.SslProtocols; http3Options.ListenBacklog = options.ListenBacklog; } diff --git a/src/libraries/Common/tests/System/Net/Http/HttpAgnosticLoopbackServer.cs b/src/libraries/Common/tests/System/Net/Http/HttpAgnosticLoopbackServer.cs index 48626cb2de6f6..59c421db4991c 100644 --- a/src/libraries/Common/tests/System/Net/Http/HttpAgnosticLoopbackServer.cs +++ b/src/libraries/Common/tests/System/Net/Http/HttpAgnosticLoopbackServer.cs @@ -202,6 +202,7 @@ private static HttpAgnosticOptions CreateOptions(GenericLoopbackOptions options) if (options != null) { httpOptions.Address = options.Address; + httpOptions.Certificate = options.Certificate; httpOptions.UseSsl = options.UseSsl; httpOptions.SslProtocols = options.SslProtocols; httpOptions.ListenBacklog = options.ListenBacklog; diff --git a/src/libraries/Common/tests/System/Net/Http/LoopbackServer.cs b/src/libraries/Common/tests/System/Net/Http/LoopbackServer.cs index 4879258fab2c4..c559ffe762c00 100644 --- a/src/libraries/Common/tests/System/Net/Http/LoopbackServer.cs +++ b/src/libraries/Common/tests/System/Net/Http/LoopbackServer.cs @@ -1121,6 +1121,7 @@ private static LoopbackServer.Options CreateOptions(GenericLoopbackOptions optio { newOptions.Address = options.Address; newOptions.UseSsl = options.UseSsl; + newOptions.Certificate = options.Certificate; newOptions.SslProtocols = options.SslProtocols; newOptions.ListenBacklog = options.ListenBacklog; } diff --git a/src/libraries/System.Net.Http.Json/tests/FunctionalTests/System.Net.Http.Json.Functional.Tests.csproj b/src/libraries/System.Net.Http.Json/tests/FunctionalTests/System.Net.Http.Json.Functional.Tests.csproj index 40ae1eb7e9b28..144cbb55fd2b6 100644 --- a/src/libraries/System.Net.Http.Json/tests/FunctionalTests/System.Net.Http.Json.Functional.Tests.csproj +++ b/src/libraries/System.Net.Http.Json/tests/FunctionalTests/System.Net.Http.Json.Functional.Tests.csproj @@ -14,7 +14,6 @@ - diff --git a/src/libraries/System.Net.Http/tests/FunctionalTests/SocketsHttpHandlerTest.cs b/src/libraries/System.Net.Http/tests/FunctionalTests/SocketsHttpHandlerTest.cs index 17ae4c3d7ca88..9fc34df5e2cd8 100644 --- a/src/libraries/System.Net.Http/tests/FunctionalTests/SocketsHttpHandlerTest.cs +++ b/src/libraries/System.Net.Http/tests/FunctionalTests/SocketsHttpHandlerTest.cs @@ -24,6 +24,8 @@ namespace System.Net.Http.Functional.Tests { + using Configuration = System.Net.Test.Common.Configuration; + [ConditionalClass(typeof(PlatformDetection), nameof(PlatformDetection.IsNotBrowser))] public sealed class SocketsHttpHandler_HttpClientHandler_Asynchrony_Test : HttpClientHandler_Asynchrony_Test { @@ -3728,6 +3730,137 @@ public SocketsHttpHandler_RequestContentLengthMismatchTest_Http3(ITestOutputHelp protected override Version UseVersion => HttpVersion.Version30; } + [ConditionalClass(typeof(PlatformDetection), nameof(PlatformDetection.IsNotBrowser))] + public abstract class SocketsHttpHandler_SecurityTest : HttpClientHandlerTestBase + { + public SocketsHttpHandler_SecurityTest(ITestOutputHelper output) : base(output) { } + + [ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsNotWindows7))] + public async Task SslOptions_CustomTrust_Ok() + { + X509Certificate2Collection caCerts = new X509Certificate2Collection(); + X509Certificate2 certificate = Configuration.Certificates.GetDynamicServerCerttificate(caCerts); + + GenericLoopbackOptions options = new GenericLoopbackOptions() { UseSsl = true, Certificate = certificate }; + await LoopbackServerFactory.CreateClientAndServerAsync( + async uri => + { + using HttpClientHandler handler = CreateHttpClientHandler(allowAllCertificates: false); + var socketsHandler = (SocketsHttpHandler)GetUnderlyingSocketsHttpHandler(handler); + + var policy = new X509ChainPolicy() + { + RevocationMode = X509RevocationMode.NoCheck, + TrustMode = X509ChainTrustMode.CustomRootTrust, + }; + + policy.ExtraStore.AddRange(caCerts); + policy.CustomTrustStore.Add(caCerts[caCerts.Count - 1]); + socketsHandler.SslOptions = new SslClientAuthenticationOptions() { CertificateChainPolicy = policy }; + using HttpClient client = CreateHttpClient(handler); + client.DefaultVersionPolicy = HttpVersionPolicy.RequestVersionExact; + HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, uri) { Version = UseVersion, VersionPolicy = HttpVersionPolicy.RequestVersionExact }; + // This will drive SNI and name verification + request.Headers.Host = "localhost"; + HttpResponseMessage response = await client.SendAsync(request, HttpCompletionOption.ResponseContentRead); + Assert.Equal("foo", await response.Content.ReadAsStringAsync()); + }, + async server => + { + await server.AcceptConnectionSendResponseAndCloseAsync(content: "foo"); + }, options: options); + } + + [Fact] + public async Task SslOptions_InvalidName_Throws() + { + X509Certificate2Collection caCerts = new X509Certificate2Collection(); + using X509Certificate2 certificate = Configuration.Certificates.GetDynamicServerCerttificate(caCerts); + + GenericLoopbackOptions options = new GenericLoopbackOptions() { UseSsl = true, Certificate = certificate }; + await LoopbackServerFactory.CreateClientAndServerAsync( + async uri => + { + using HttpClientHandler handler = CreateHttpClientHandler(allowAllCertificates: false); + var socketsHandler = (SocketsHttpHandler)GetUnderlyingSocketsHttpHandler(handler); + using HttpClient client = CreateHttpClient(handler); + client.DefaultVersionPolicy = HttpVersionPolicy.RequestVersionExact; + HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, uri) { Version = UseVersion, VersionPolicy = HttpVersionPolicy.RequestVersionExact }; + // This will drive SNI and name verification + request.Headers.Host = Guid.NewGuid().ToString("N"); + + await Assert.ThrowsAsync(() => client.SendAsync(request, HttpCompletionOption.ResponseContentRead)); + }, + async server => + { + try + { + await server.AcceptConnectionSendResponseAndCloseAsync(content: "foo"); + } + catch { }; + }, options: options); + } + + [Fact] + public async Task SslOptions_CustomPolicy_IgnoresNameMismatch() + { + X509Certificate2Collection caCerts = new X509Certificate2Collection(); + X509Certificate2 certificate = Configuration.Certificates.GetDynamicServerCerttificate(caCerts); + + GenericLoopbackOptions options = new GenericLoopbackOptions() { UseSsl = true, Certificate = certificate }; + await LoopbackServerFactory.CreateClientAndServerAsync( + async uri => + { + using HttpClientHandler handler = CreateHttpClientHandler(allowAllCertificates: false); + var socketsHandler = (SocketsHttpHandler)GetUnderlyingSocketsHttpHandler(handler); + + var policy = new X509ChainPolicy() + { + RevocationMode = X509RevocationMode.NoCheck, + TrustMode = X509ChainTrustMode.CustomRootTrust, + VerificationFlags = X509VerificationFlags.IgnoreInvalidName, + }; + + policy.ExtraStore.AddRange(caCerts); + policy.CustomTrustStore.Add(caCerts[caCerts.Count -1]); + socketsHandler.SslOptions = new SslClientAuthenticationOptions() { CertificateChainPolicy = policy }; + + using HttpClient client = CreateHttpClient(handler); + client.DefaultVersionPolicy = HttpVersionPolicy.RequestVersionExact; + HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, uri) { Version = UseVersion, VersionPolicy = HttpVersionPolicy.RequestVersionExact }; + // This will drive SNI and name verification + request.Headers.Host = Guid.NewGuid().ToString("N"); + + HttpResponseMessage response = await client.SendAsync(request, HttpCompletionOption.ResponseContentRead); + Assert.Equal("foo", await response.Content.ReadAsStringAsync()); + }, + async server => + { + await server.AcceptConnectionSendResponseAndCloseAsync(content: "foo"); + }, options: options); + } + } + + public sealed class SocketsHttpHandler_SocketsHttpHandler_SecurityTest_Http11 : SocketsHttpHandler_SecurityTest + { + public SocketsHttpHandler_SocketsHttpHandler_SecurityTest_Http11(ITestOutputHelper output) : base(output) { } + protected override Version UseVersion => HttpVersion.Version11; + } + + [ConditionalClass(typeof(PlatformDetection), nameof(PlatformDetection.SupportsAlpn))] + public sealed class SocketsHttpHandler_SocketsHttpHandler_SecurityTest_Http2 : SocketsHttpHandler_SecurityTest + { + public SocketsHttpHandler_SocketsHttpHandler_SecurityTest_Http2(ITestOutputHelper output) : base(output) { } + protected override Version UseVersion => HttpVersion.Version20; + } + + [ConditionalClass(typeof(HttpClientHandlerTestBase), nameof(IsQuicSupported))] + public sealed class SocketsHttpHandler_SocketsHttpHandler_SecurityTest_Http3 : SocketsHttpHandler_SecurityTest + { + public SocketsHttpHandler_SocketsHttpHandler_SecurityTest_Http3(ITestOutputHelper output) : base(output) { } + protected override Version UseVersion => HttpVersion.Version30; + } + public class MySsl : SslStream { public MySsl(Stream stream) : base(stream) diff --git a/src/libraries/System.Net.Http/tests/FunctionalTests/System.Net.Http.Functional.Tests.csproj b/src/libraries/System.Net.Http/tests/FunctionalTests/System.Net.Http.Functional.Tests.csproj index aa28608696e7c..4cbf8ecec267a 100644 --- a/src/libraries/System.Net.Http/tests/FunctionalTests/System.Net.Http.Functional.Tests.csproj +++ b/src/libraries/System.Net.Http/tests/FunctionalTests/System.Net.Http.Functional.Tests.csproj @@ -78,6 +78,12 @@ Link="CommonTest\System\Security\Cryptography\PlatformSupport.cs" /> + + + + @@ -24,8 +25,6 @@ - - diff --git a/src/libraries/System.Net.Security/src/System/Net/Security/SslAuthenticationOptions.cs b/src/libraries/System.Net.Security/src/System/Net/Security/SslAuthenticationOptions.cs index 16197cf22bf64..6113cecff667c 100644 --- a/src/libraries/System.Net.Security/src/System/Net/Security/SslAuthenticationOptions.cs +++ b/src/libraries/System.Net.Security/src/System/Net/Security/SslAuthenticationOptions.cs @@ -41,7 +41,7 @@ internal void UpdateOptions(SslClientAuthenticationOptions sslClientAuthenticati // Common options. AllowRenegotiation = sslClientAuthenticationOptions.AllowRenegotiation; ApplicationProtocols = sslClientAuthenticationOptions.ApplicationProtocols; - CheckCertName = true; + CheckCertName = !(sslClientAuthenticationOptions.CertificateChainPolicy?.VerificationFlags.HasFlag(X509VerificationFlags.IgnoreInvalidName) == true); EnabledSslProtocols = FilterOutIncompatibleSslProtocols(sslClientAuthenticationOptions.EnabledSslProtocols); EncryptionPolicy = sslClientAuthenticationOptions.EncryptionPolicy; IsServer = false;