From eb243f03285ae23ce7ca78b0f3857cb6c2a2fd4e Mon Sep 17 00:00:00 2001 From: Tomas Weinfurt Date: Mon, 10 Aug 2020 12:02:08 -0700 Subject: [PATCH] fix certificate ctx on windows and macOS (#39818) * fix certificate ctx on windows * fix linux * fix osx pal * feedback from review * feedback from review --- .../Security/Pal.OSX/SafeDeleteSslContext.cs | 74 ++++----------- .../Pal.OSX/SafeFreeSslCredentials.cs | 22 ++--- .../src/System/Net/Security/SecureChannel.cs | 15 ++- .../SslStreamCertificateContext.Linux.cs | 8 ++ .../SslStreamCertificateContext.OSX.cs | 9 ++ .../SslStreamCertificateContext.Windows.cs | 92 +++++++++++++++++++ .../Security/SslStreamCertificateContext.cs | 18 ++-- .../System/Net/Security/SslStreamPal.OSX.cs | 4 +- .../System/Net/Security/SslStreamPal.Unix.cs | 4 +- .../Net/Security/SslStreamPal.Windows.cs | 6 +- .../SslStreamNetworkStreamTest.cs | 39 +++++--- .../tests/FunctionalTests/TestHelper.cs | 52 ++++++++++- 12 files changed, 236 insertions(+), 107 deletions(-) diff --git a/src/libraries/System.Net.Security/src/System/Net/Security/Pal.OSX/SafeDeleteSslContext.cs b/src/libraries/System.Net.Security/src/System/Net/Security/Pal.OSX/SafeDeleteSslContext.cs index ae52439d16293..06ecc8d9ed1d7 100644 --- a/src/libraries/System.Net.Security/src/System/Net/Security/Pal.OSX/SafeDeleteSslContext.cs +++ b/src/libraries/System.Net.Security/src/System/Net/Security/Pal.OSX/SafeDeleteSslContext.cs @@ -118,9 +118,9 @@ private static SafeSslHandle CreateSslContext(SafeFreeSslCredentials credential, SetProtocols(sslContext, credential.Protocols); } - if (credential.Certificate != null) + if (credential.CertificateContext != null) { - SetCertificate(sslContext, credential.Certificate); + SetCertificate(sslContext, credential.CertificateContext); } Interop.AppleCrypto.SslBreakOnServerAuth(sslContext, true); @@ -315,68 +315,34 @@ private static void SetProtocols(SafeSslHandle sslContext, SslProtocols protocol Interop.AppleCrypto.SslSetMaxProtocolVersion(sslContext, maxProtocolId); } - private static void SetCertificate(SafeSslHandle sslContext, X509Certificate2 certificate) + private static void SetCertificate(SafeSslHandle sslContext, SslStreamCertificateContext context) { Debug.Assert(sslContext != null, "sslContext != null"); - Debug.Assert(certificate != null, "certificate != null"); - Debug.Assert(certificate.HasPrivateKey, "certificate.HasPrivateKey"); - X509Chain chain = TLSCertificateExtensions.BuildNewChain( - certificate, - includeClientApplicationPolicy: false)!; - using (chain) - { - X509ChainElementCollection elements = chain.ChainElements; - - // We need to leave off the EE (first) and root (last) certificate from the intermediates. - X509Certificate2[] intermediateCerts = elements.Count < 3 - ? Array.Empty() - : new X509Certificate2[elements.Count - 2]; - - // Build an array which is [ - // SecIdentityRef for EE cert, - // SecCertificateRef for intermed0, - // SecCertificateREf for intermed1, - // ... - // ] - IntPtr[] ptrs = new IntPtr[intermediateCerts.Length + 1]; - - for (int i = 0; i < intermediateCerts.Length; i++) - { - X509Certificate2 intermediateCert = elements[i + 1].Certificate!; + IntPtr[] ptrs = new IntPtr[context!.IntermediateCertificates!.Length + 1]; - if (intermediateCert.HasPrivateKey) - { - // In the unlikely event that we get a certificate with a private key from - // a chain, clear it to the certificate. - // - // The current value of intermediateCert is still in elements, which will - // get Disposed at the end of this method. The new value will be - // in the intermediate certs array, which also gets serially Disposed. - intermediateCert = new X509Certificate2(intermediateCert.RawData); - } + for (int i = 0; i < context.IntermediateCertificates.Length; i++) + { + X509Certificate2 intermediateCert = context.IntermediateCertificates[i]; - intermediateCerts[i] = intermediateCert; - ptrs[i + 1] = intermediateCert.Handle; + if (intermediateCert.HasPrivateKey) + { + // In the unlikely event that we get a certificate with a private key from + // a chain, clear it to the certificate. + // + // The current value of intermediateCert is still in elements, which will + // get Disposed at the end of this method. The new value will be + // in the intermediate certs array, which also gets serially Disposed. + intermediateCert = new X509Certificate2(intermediateCert.RawData); } - ptrs[0] = certificate.Handle; - - Interop.AppleCrypto.SslSetCertificate(sslContext, ptrs); + ptrs[i + 1] = intermediateCert.Handle; + } - // The X509Chain created all new certs for us, so Dispose them. - // And since the intermediateCerts could have been new instances, Dispose them, too - for (int i = 0; i < elements.Count; i++) - { - elements[i].Certificate!.Dispose(); + ptrs[0] = context!.Certificate!.Handle; - if (i < intermediateCerts.Length) - { - intermediateCerts[i].Dispose(); - } - } - } + Interop.AppleCrypto.SslSetCertificate(sslContext, ptrs); } } } diff --git a/src/libraries/System.Net.Security/src/System/Net/Security/Pal.OSX/SafeFreeSslCredentials.cs b/src/libraries/System.Net.Security/src/System/Net/Security/Pal.OSX/SafeFreeSslCredentials.cs index 9a894cb811221..24a4e7be7843a 100644 --- a/src/libraries/System.Net.Security/src/System/Net/Security/Pal.OSX/SafeFreeSslCredentials.cs +++ b/src/libraries/System.Net.Security/src/System/Net/Security/Pal.OSX/SafeFreeSslCredentials.cs @@ -10,30 +10,22 @@ namespace System.Net { internal sealed class SafeFreeSslCredentials : SafeFreeCredentials { - public SafeFreeSslCredentials(X509Certificate certificate, SslProtocols protocols, EncryptionPolicy policy) + public SafeFreeSslCredentials(SslStreamCertificateContext? certificateContext, SslProtocols protocols, EncryptionPolicy policy) : base(IntPtr.Zero, true) { - Debug.Assert( - certificate == null || certificate is X509Certificate2, - "Only X509Certificate2 certificates are supported at this time"); - - X509Certificate2? cert = (X509Certificate2?)certificate; - - if (cert != null) + if (certificateContext != null) { - Debug.Assert(cert.HasPrivateKey, "cert.HasPrivateKey"); - // Make a defensive copy of the certificate. In some async cases the // certificate can have been disposed before being provided to the handshake. // // This meshes with the Unix (OpenSSL) PAL, because it extracts the private key // and cert handle (which get up-reffed) to match the API expectations. - cert = new X509Certificate2(cert); + certificateContext = certificateContext.Duplicate(); - Debug.Assert(cert.HasPrivateKey, "cert clone.HasPrivateKey"); + Debug.Assert(certificateContext.Certificate.HasPrivateKey, "cert clone.HasPrivateKey"); } - Certificate = cert; + CertificateContext = certificateContext; Protocols = protocols; Policy = policy; } @@ -42,13 +34,13 @@ public SafeFreeSslCredentials(X509Certificate certificate, SslProtocols protocol public SslProtocols Protocols { get; } - public X509Certificate2? Certificate { get; } + public SslStreamCertificateContext? CertificateContext { get; } public override bool IsInvalid => false; protected override bool ReleaseHandle() { - Certificate?.Dispose(); + CertificateContext?.Certificate.Dispose(); return true; } } diff --git a/src/libraries/System.Net.Security/src/System/Net/Security/SecureChannel.cs b/src/libraries/System.Net.Security/src/System/Net/Security/SecureChannel.cs index 4d20cea9c251b..c8cdcb780a26b 100644 --- a/src/libraries/System.Net.Security/src/System/Net/Security/SecureChannel.cs +++ b/src/libraries/System.Net.Security/src/System/Net/Security/SecureChannel.cs @@ -596,21 +596,27 @@ private bool AcquireClientCredentials(ref byte[]? thumbPrint) { if (NetEventSource.Log.IsEnabled()) NetEventSource.Log.UsingCachedCredential(this); - _credentialsHandle = cachedCredentialHandle; _selectedClientCertificate = clientCertificate; cachedCred = true; } else { - _credentialsHandle = SslStreamPal.AcquireCredentialsHandle(selectedCert!, _sslAuthenticationOptions.EnabledSslProtocols, _sslAuthenticationOptions.EncryptionPolicy, _sslAuthenticationOptions.IsServer); + if (selectedCert != null) + { + _sslAuthenticationOptions.CertificateContext = SslStreamCertificateContext.Create(selectedCert!); + } + + _credentialsHandle = SslStreamPal.AcquireCredentialsHandle(_sslAuthenticationOptions.CertificateContext, + _sslAuthenticationOptions.EnabledSslProtocols, _sslAuthenticationOptions.EncryptionPolicy, _sslAuthenticationOptions.IsServer); + thumbPrint = guessedThumbPrint; // Delay until here in case something above threw. _selectedClientCertificate = clientCertificate; } } finally { - if (selectedCert != null) + if (selectedCert != null && _sslAuthenticationOptions.CertificateContext != null) { _sslAuthenticationOptions.CertificateContext = SslStreamCertificateContext.Create(selectedCert); } @@ -710,7 +716,8 @@ private bool AcquireServerCredentials(ref byte[]? thumbPrint) } else { - _credentialsHandle = SslStreamPal.AcquireCredentialsHandle(selectedCert, _sslAuthenticationOptions.EnabledSslProtocols, _sslAuthenticationOptions.EncryptionPolicy, _sslAuthenticationOptions.IsServer); + _credentialsHandle = SslStreamPal.AcquireCredentialsHandle(_sslAuthenticationOptions.CertificateContext, _sslAuthenticationOptions.EnabledSslProtocols, + _sslAuthenticationOptions.EncryptionPolicy, _sslAuthenticationOptions.IsServer); thumbPrint = guessedThumbPrint; } diff --git a/src/libraries/System.Net.Security/src/System/Net/Security/SslStreamCertificateContext.Linux.cs b/src/libraries/System.Net.Security/src/System/Net/Security/SslStreamCertificateContext.Linux.cs index e4260aeaac788..f5e9dd2114d61 100644 --- a/src/libraries/System.Net.Security/src/System/Net/Security/SslStreamCertificateContext.Linux.cs +++ b/src/libraries/System.Net.Security/src/System/Net/Security/SslStreamCertificateContext.Linux.cs @@ -7,6 +7,14 @@ namespace System.Net.Security { public partial class SslStreamCertificateContext { + private const bool TrimRootCertificate = true; + + private SslStreamCertificateContext(X509Certificate2 target, X509Certificate2[] intermediates) + { + Certificate = target; + IntermediateCertificates = intermediates; + } + internal static SslStreamCertificateContext Create(X509Certificate2 target) => Create(target, null); } } diff --git a/src/libraries/System.Net.Security/src/System/Net/Security/SslStreamCertificateContext.OSX.cs b/src/libraries/System.Net.Security/src/System/Net/Security/SslStreamCertificateContext.OSX.cs index d616c9e8e6b9b..4579f15407fde 100644 --- a/src/libraries/System.Net.Security/src/System/Net/Security/SslStreamCertificateContext.OSX.cs +++ b/src/libraries/System.Net.Security/src/System/Net/Security/SslStreamCertificateContext.OSX.cs @@ -7,6 +7,15 @@ namespace System.Net.Security { public partial class SslStreamCertificateContext { + // No leaf, no root. + private const bool TrimRootCertificate = true; + + private SslStreamCertificateContext(X509Certificate2 target, X509Certificate2[] intermediates) + { + Certificate = target; + IntermediateCertificates = intermediates; + } + internal static SslStreamCertificateContext Create(X509Certificate2 target) { // On OSX we do not need to build chain unless we are asked for it. diff --git a/src/libraries/System.Net.Security/src/System/Net/Security/SslStreamCertificateContext.Windows.cs b/src/libraries/System.Net.Security/src/System/Net/Security/SslStreamCertificateContext.Windows.cs index 5906a13a68b73..0827eca16d572 100644 --- a/src/libraries/System.Net.Security/src/System/Net/Security/SslStreamCertificateContext.Windows.cs +++ b/src/libraries/System.Net.Security/src/System/Net/Security/SslStreamCertificateContext.Windows.cs @@ -7,10 +7,102 @@ namespace System.Net.Security { public partial class SslStreamCertificateContext { + // No leaf, include root. + private const bool TrimRootCertificate = false; + internal static SslStreamCertificateContext Create(X509Certificate2 target) { // On Windows we do not need to build chain unless we are asked for it. return new SslStreamCertificateContext(target, Array.Empty()); } + + private SslStreamCertificateContext(X509Certificate2 target, X509Certificate2[] intermediates) + { + if (intermediates.Length > 0) + { + using (X509Chain chain = new X509Chain()) + { + chain.ChainPolicy.VerificationFlags = X509VerificationFlags.AllFlags; + chain.ChainPolicy.RevocationMode = X509RevocationMode.NoCheck; + chain.ChainPolicy.DisableCertificateDownloads = true; + bool osCanBuildChain = chain.Build(target); + + int count = 0; + foreach (X509ChainStatus status in chain.ChainStatus) + { + if (status.Status.HasFlag(X509ChainStatusFlags.PartialChain) || status.Status.HasFlag(X509ChainStatusFlags.NotSignatureValid)) + { + osCanBuildChain = false; + break; + } + + count++; + } + + // OS failed to build the chain but we have at least some intermediates. + // We will try to add them to "Intermediate Certification Authorities" store. + if (!osCanBuildChain) + { + X509Store? store = new X509Store(StoreName.CertificateAuthority, StoreLocation.LocalMachine); + + try + { + store.Open(OpenFlags.ReadWrite); + } + catch + { + // If using system store fails, try to fall-back to user store. + store.Dispose(); + store = new X509Store(StoreName.CertificateAuthority, StoreLocation.CurrentUser); + try + { + store.Open(OpenFlags.ReadWrite); + } + catch + { + store.Dispose(); + store = null; + if (NetEventSource.IsEnabled) + { + NetEventSource.Error(this, $"Failed to open certificate store for intermediates."); + } + } + } + + if (store != null) + { + using (store) + { + // Add everything except the root + for (int index = count; index < intermediates.Length - 1; index++) + { + store.Add(intermediates[index]); + } + + osCanBuildChain = chain.Build(target); + foreach (X509ChainStatus status in chain.ChainStatus) + { + if (status.Status.HasFlag(X509ChainStatusFlags.PartialChain) || status.Status.HasFlag(X509ChainStatusFlags.NotSignatureValid)) + { + osCanBuildChain = false; + break; + } + } + + if (!osCanBuildChain) + { + // Add also root to Intermediate CA store so OS can complete building chain. + // (This does not make it trusted. + store.Add(intermediates[intermediates.Length - 1]); + } + } + } + } + } + } + + Certificate = target; + IntermediateCertificates = intermediates; + } } } diff --git a/src/libraries/System.Net.Security/src/System/Net/Security/SslStreamCertificateContext.cs b/src/libraries/System.Net.Security/src/System/Net/Security/SslStreamCertificateContext.cs index 621ab20079d51..4cba3232be530 100644 --- a/src/libraries/System.Net.Security/src/System/Net/Security/SslStreamCertificateContext.cs +++ b/src/libraries/System.Net.Security/src/System/Net/Security/SslStreamCertificateContext.cs @@ -18,7 +18,6 @@ public static SslStreamCertificateContext Create(X509Certificate2 target, X509Ce } X509Certificate2[] intermediates = Array.Empty(); - using (X509Chain chain = new X509Chain()) { if (additionalCertificates != null) @@ -32,11 +31,14 @@ public static SslStreamCertificateContext Create(X509Certificate2 target, X509Ce chain.ChainPolicy.VerificationFlags = X509VerificationFlags.AllFlags; chain.ChainPolicy.RevocationMode = X509RevocationMode.NoCheck; chain.ChainPolicy.DisableCertificateDownloads = offline; - chain.Build(target); + bool chainStatus = chain.Build(target); - // No leaf, no root. - int count = chain.ChainElements.Count - 2; + if (!chainStatus && NetEventSource.IsEnabled) + { + NetEventSource.Error(null, $"Failed to build chain for {target.Subject}"); + } + int count = chain.ChainElements.Count - (TrimRootCertificate ? 1 : 2); foreach (X509ChainStatus status in chain.ChainStatus) { if (status.Status.HasFlag(X509ChainStatusFlags.PartialChain)) @@ -48,7 +50,7 @@ public static SslStreamCertificateContext Create(X509Certificate2 target, X509Ce } // Count can be zero for a self-signed certificate, or a cert issued directly from a root. - if (count > 0) + if (count > 0 && chain.ChainElements.Count > 1) { intermediates = new X509Certificate2[count]; for (int i = 0; i < count; i++) @@ -70,10 +72,10 @@ public static SslStreamCertificateContext Create(X509Certificate2 target, X509Ce return new SslStreamCertificateContext(target, intermediates); } - private SslStreamCertificateContext(X509Certificate2 target, X509Certificate2[] intermediates) + internal SslStreamCertificateContext Duplicate() { - Certificate = target; - IntermediateCertificates = intermediates; + return new SslStreamCertificateContext(new X509Certificate2(Certificate), IntermediateCertificates); + } } } diff --git a/src/libraries/System.Net.Security/src/System/Net/Security/SslStreamPal.OSX.cs b/src/libraries/System.Net.Security/src/System/Net/Security/SslStreamPal.OSX.cs index 4f5a990698515..5f1c1bb346676 100644 --- a/src/libraries/System.Net.Security/src/System/Net/Security/SslStreamPal.OSX.cs +++ b/src/libraries/System.Net.Security/src/System/Net/Security/SslStreamPal.OSX.cs @@ -53,12 +53,12 @@ public static void VerifyPackageInfo() } public static SafeFreeCredentials AcquireCredentialsHandle( - X509Certificate certificate, + SslStreamCertificateContext? certificateContext, SslProtocols protocols, EncryptionPolicy policy, bool isServer) { - return new SafeFreeSslCredentials(certificate, protocols, policy); + return new SafeFreeSslCredentials(certificateContext, protocols, policy); } internal static byte[]? GetNegotiatedApplicationProtocol(SafeDeleteContext? context) diff --git a/src/libraries/System.Net.Security/src/System/Net/Security/SslStreamPal.Unix.cs b/src/libraries/System.Net.Security/src/System/Net/Security/SslStreamPal.Unix.cs index 4864d32b63c6e..aeae1edc9fe94 100644 --- a/src/libraries/System.Net.Security/src/System/Net/Security/SslStreamPal.Unix.cs +++ b/src/libraries/System.Net.Security/src/System/Net/Security/SslStreamPal.Unix.cs @@ -36,10 +36,10 @@ public static void VerifyPackageInfo() return HandshakeInternal(credential!, ref context, inputBuffer, ref outputBuffer, sslAuthenticationOptions); } - public static SafeFreeCredentials AcquireCredentialsHandle(X509Certificate? certificate, + public static SafeFreeCredentials AcquireCredentialsHandle(SslStreamCertificateContext? certificateContext, SslProtocols protocols, EncryptionPolicy policy, bool isServer) { - return new SafeFreeSslCredentials(certificate, protocols, policy); + return new SafeFreeSslCredentials(certificateContext?.Certificate, protocols, policy); } public static SecurityStatusPal EncryptMessage(SafeDeleteContext securityContext, ReadOnlyMemory input, int headerSize, int trailerSize, ref byte[] output, out int resultSize) diff --git a/src/libraries/System.Net.Security/src/System/Net/Security/SslStreamPal.Windows.cs b/src/libraries/System.Net.Security/src/System/Net/Security/SslStreamPal.Windows.cs index 5e72e6b553b0d..4c3c2c22db42e 100644 --- a/src/libraries/System.Net.Security/src/System/Net/Security/SslStreamPal.Windows.cs +++ b/src/libraries/System.Net.Security/src/System/Net/Security/SslStreamPal.Windows.cs @@ -110,12 +110,12 @@ public static SecurityStatusPal InitializeSecurityContext(ref SafeFreeCredential return SecurityStatusAdapterPal.GetSecurityStatusPalFromNativeInt(errorCode); } - public static SafeFreeCredentials AcquireCredentialsHandle(X509Certificate? certificate, SslProtocols protocols, EncryptionPolicy policy, bool isServer) + public static SafeFreeCredentials AcquireCredentialsHandle(SslStreamCertificateContext? certificateContext, SslProtocols protocols, EncryptionPolicy policy, bool isServer) { // New crypto API supports TLS1.3 but it does not allow to force NULL encryption. return !UseNewCryptoApi || policy == EncryptionPolicy.NoEncryption ? - AcquireCredentialsHandleSchannelCred(certificate, protocols, policy, isServer) : - AcquireCredentialsHandleSchCredentials(certificate, protocols, policy, isServer); + AcquireCredentialsHandleSchannelCred(certificateContext?.Certificate, protocols, policy, isServer) : + AcquireCredentialsHandleSchCredentials(certificateContext?.Certificate, protocols, policy, isServer); } // This is legacy crypto API used on .NET Framework and older Windows versions. diff --git a/src/libraries/System.Net.Security/tests/FunctionalTests/SslStreamNetworkStreamTest.cs b/src/libraries/System.Net.Security/tests/FunctionalTests/SslStreamNetworkStreamTest.cs index 28cb9233b6384..14d61a3d7407b 100644 --- a/src/libraries/System.Net.Security/tests/FunctionalTests/SslStreamNetworkStreamTest.cs +++ b/src/libraries/System.Net.Security/tests/FunctionalTests/SslStreamNetworkStreamTest.cs @@ -7,6 +7,7 @@ using System.Security.Authentication; using System.Security.Cryptography.X509Certificates; using System.Text; +using System.Threading; using System.Threading.Tasks; using Microsoft.DotNet.XUnitExtensions; using Xunit; @@ -15,14 +16,19 @@ namespace System.Net.Security.Tests { using Configuration = System.Net.Test.Common.Configuration; - public class SslStreamNetworkStreamTest + public class SslStreamNetworkStreamTest : IDisposable { private readonly X509Certificate2 _serverCert; - private readonly X509CertificateCollection _serverChain; + private readonly X509Certificate2Collection _serverChain; public SslStreamNetworkStreamTest() { - (_serverCert, _serverChain) = TestHelper.GenerateCertificates("localhost"); + (_serverCert, _serverChain) = TestHelper.GenerateCertificates("localhost", this.GetType().Name); + } + + public void Dispose() + { + TestHelper.CleanupCertificates(this.GetType().Name); } [Fact] @@ -269,14 +275,12 @@ public async Task SslStream_TargetHostName_Succeeds(bool useEmptyName) } [Fact] - [PlatformSpecific(TestPlatforms.AnyUnix)] public async Task SslStream_UntrustedCaWithCustomCallback_OK() { - var options = new SslClientAuthenticationOptions() { TargetHost = "localhost" }; - options.RemoteCertificateValidationCallback = + var clientOptions = new SslClientAuthenticationOptions() { TargetHost = "localhost" }; + clientOptions.RemoteCertificateValidationCallback = (sender, certificate, chain, sslPolicyErrors) => { - chain.ChainPolicy.ExtraStore.AddRange(_serverChain); chain.ChainPolicy.CustomTrustStore.Add(_serverChain[_serverChain.Count -1]); chain.ChainPolicy.TrustMode = X509ChainTrustMode.CustomRootTrust; @@ -286,14 +290,17 @@ public async Task SslStream_UntrustedCaWithCustomCallback_OK() return result; }; + var serverOptions = new SslServerAuthenticationOptions(); + serverOptions.ServerCertificateContext = SslStreamCertificateContext.Create(_serverCert, _serverChain); + (Stream clientStream, Stream serverStream) = TestHelper.GetConnectedStreams(); using (clientStream) using (serverStream) using (SslStream client = new SslStream(clientStream)) using (SslStream server = new SslStream(serverStream)) { - Task t1 = client.AuthenticateAsClientAsync(options, default); - Task t2 = server.AuthenticateAsServerAsync(_serverCert); + Task t1 = client.AuthenticateAsClientAsync(clientOptions, CancellationToken.None); + Task t2 = server.AuthenticateAsServerAsync(serverOptions, CancellationToken.None); await TestConfiguration.WhenAllOrAnyFailedWithTimeout(t1, t2); } @@ -306,13 +313,12 @@ public async Task SslStream_UntrustedCaWithCustomCallback_OK() public async Task SslStream_UntrustedCaWithCustomCallback_Throws(bool customCallback) { string errorMessage; - var options = new SslClientAuthenticationOptions() { TargetHost = "localhost" }; + var clientOptions = new SslClientAuthenticationOptions() { TargetHost = "localhost" }; if (customCallback) { - options.RemoteCertificateValidationCallback = + clientOptions.RemoteCertificateValidationCallback = (sender, certificate, chain, sslPolicyErrors) => { - chain.ChainPolicy.ExtraStore.AddRange(_serverChain); chain.ChainPolicy.CustomTrustStore.Add(_serverChain[_serverChain.Count -1]); chain.ChainPolicy.TrustMode = X509ChainTrustMode.CustomRootTrust; // This should work and we should be able to trust the chain. @@ -325,17 +331,20 @@ public async Task SslStream_UntrustedCaWithCustomCallback_Throws(bool customCall } else { - errorMessage = "PartialChain"; + errorMessage = "UntrustedRoot"; } + var serverOptions = new SslServerAuthenticationOptions(); + serverOptions.ServerCertificateContext = SslStreamCertificateContext.Create(_serverCert, _serverChain); + (Stream clientStream, Stream serverStream) = TestHelper.GetConnectedStreams(); using (clientStream) using (serverStream) using (SslStream client = new SslStream(clientStream)) using (SslStream server = new SslStream(serverStream)) { - Task t1 = client.AuthenticateAsClientAsync(options, default); - Task t2 = server.AuthenticateAsServerAsync(_serverCert); + Task t1 = client.AuthenticateAsClientAsync(clientOptions, CancellationToken.None); + Task t2 = server.AuthenticateAsServerAsync(serverOptions, CancellationToken.None); var e = await Assert.ThrowsAsync(() => t1); Assert.Contains(errorMessage, e.Message); diff --git a/src/libraries/System.Net.Security/tests/FunctionalTests/TestHelper.cs b/src/libraries/System.Net.Security/tests/FunctionalTests/TestHelper.cs index 4e9a9a8ea37a7..b16bcac29825d 100644 --- a/src/libraries/System.Net.Security/tests/FunctionalTests/TestHelper.cs +++ b/src/libraries/System.Net.Security/tests/FunctionalTests/TestHelper.cs @@ -72,14 +72,53 @@ internal static (VirtualNetworkStream ClientStream, VirtualNetworkStream ServerS return (new VirtualNetworkStream(vn, isServer: false), new VirtualNetworkStream(vn, isServer: true)); } - internal static (X509Certificate2 certificate, X509Certificate2Collection) GenerateCertificates(string name, string? testName = null) + internal static void CleanupCertificates(string testName) { - X509Certificate2Collection chain = new X509Certificate2Collection(); + string caName = $"O={testName}"; + try + { + using (X509Store store = new X509Store(StoreName.CertificateAuthority, StoreLocation.LocalMachine)) + { + store.Open(OpenFlags.ReadWrite); + foreach (X509Certificate2 cert in store.Certificates) + { + if (cert.Subject.Contains(caName)) + { + store.Remove(cert); + } + } + } + } + catch { }; + + try + { + using (X509Store store = new X509Store(StoreName.CertificateAuthority, StoreLocation.CurrentUser)) + { + store.Open(OpenFlags.ReadWrite); + foreach (X509Certificate2 cert in store.Certificates) + { + if (cert.Subject.Contains(caName)) + { + store.Remove(cert); + } + } + } + } + catch { }; + } + internal static (X509Certificate2 certificate, X509Certificate2Collection) GenerateCertificates(string targetName, string? testName = null) + { + if (PlatformDetection.IsWindows && testName != null) + { + CleanupCertificates(testName); + } + X509Certificate2Collection chain = new X509Certificate2Collection(); X509ExtensionCollection extensions = new X509ExtensionCollection(); SubjectAlternativeNameBuilder builder = new SubjectAlternativeNameBuilder(); - builder.AddDnsName(name); + builder.AddDnsName(targetName); extensions.Add(builder.Build()); extensions.Add(s_eeConstraints); extensions.Add(s_eeKeyUsage); @@ -91,7 +130,7 @@ internal static (X509Certificate2 certificate, X509Certificate2Collection) Gener out CertificateAuthority root, out CertificateAuthority intermediate, out X509Certificate2 endEntity, - subjectName: name, + subjectName: targetName, testName: testName, keySize: 2048, extensions: extensions); @@ -103,6 +142,11 @@ internal static (X509Certificate2 certificate, X509Certificate2Collection) Gener root.Dispose(); intermediate.Dispose(); + if (PlatformDetection.IsWindows) + { + endEntity = new X509Certificate2(endEntity.Export(X509ContentType.Pfx)); + } + return (endEntity, chain); } }