File: Tcp\ClientCredentialTypeTests.OSX.cs
Web Access
Project: src\src\System.Private.ServiceModel\tests\Scenarios\Security\TransportSecurity\Security.TransportSecurity.IntegrationTests.csproj (Security.TransportSecurity.IntegrationTests)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
 
/* Peer certificate validation relies on two certificate stores. For a certificate to be
 * considered valid, it MUST exist in the TrustedPeople certificate store, and it MUST NOT
 * exist in the Disallowed certificate store. The certificate store model (keychains) in
 * OSX don't completely map to the Windows certificate store model. The concept of a
 * TrustedPeople store doesn't exist by default in OSX, but you can create a custom keychain
 * and define your own usage semantics to be equivalent. This test uses a custom keychain
 * which contains the public certificate of a trusted remote host. The built in Peer 
 * certificate validator has been copied below and modified to work with this custom keychain.
 * The class OSXPeerCertificateValidator contains this validator.
 * 
 * Our test infrastructure creates a custom keychain and installs a certificate into it. This
 * can be simply done with the following code:
 * 
 *     byte[] certBytes = File.ReadAllBytes(pathToCertificate);
 *     // Use X509KeyStorageFlags.Exportable if certificate contains private key
 *     var clientCertificate = new X509Certificate2(certBytes, "certificateFilePassword", X509KeyStorageFlags.DefaultKeySet);
 *     var keychainHandle = SafeKeychainHandle.Create(keychainFilePath, password);
 *     using (X509Store store = new X509Store(keychain.DangerousGetHandle()))
 *     {
 *         store.Add(clientCertificate);
 *     }
 * 
 * You can also create and install certificates into a custom keychain using OSX shell
 * utilities. The SafeKeychainHandle class exists as part of our test infrastructure but
 * can be copied standalone. It can be found at:
 * 
 * src/System.Private.ServiceModel/tests/Common/Infrastructure/SafeKeychainHandle.cs
 */
 
using Infrastructure.Common;
using System;
using Xunit;
using System.ServiceModel;
using System.Text;
using System.Security.Cryptography.X509Certificates;
using System.IdentityModel.Selectors;
using System.IO;
using System.ServiceModel.Security;
 
public partial class Tcp_ClientCredentialTypeTests : ConditionalWcfTest
{
    [WcfFact]
    [Condition(nameof(Root_Certificate_Installed),
           nameof(Client_Certificate_Installed),
           nameof(OSXPeer_Certificate_Installed),
           nameof(SSL_Available))]
    [OuterLoop]
    public static void NetTcp_SecModeTrans_CertValMode_OSXCustomStore_Succeeds_In_CustomStore()
    {
        EndpointAddress endpointAddress = null;
        string testString = "Hello";
        ChannelFactory<IWcfService> factory = null;
        IWcfService serviceProxy = null;
 
        try
        {
            // *** SETUP *** \\
            NetTcpBinding binding = new NetTcpBinding(SecurityMode.Transport);
            binding.Security.Transport.ClientCredentialType = TcpClientCredentialType.None;
 
            endpointAddress = new EndpointAddress(
                                new Uri(Endpoints.NetTcp_SecModeTrans_ClientCredTypeNone_ServerCertValModePeerTrust_Address));
 
            factory = new ChannelFactory<IWcfService>(binding, endpointAddress);
            factory.Credentials.ServiceCertificate.Authentication.CertificateValidationMode = X509CertificateValidationMode.Custom;
            factory.Credentials.ServiceCertificate.Authentication.CustomCertificateValidator = new OSXPeerCertificateValidator();
 
            serviceProxy = factory.CreateChannel();
 
            // *** EXECUTE *** \\
            string result = serviceProxy.Echo(testString);
 
            // *** VALIDATE *** \\
            Assert.Equal(testString, result);
 
            // *** CLEANUP *** \\
            ((ICommunicationObject)serviceProxy).Close();
            factory.Close();
        }
        finally
        {
            // *** ENSURE CLEANUP *** \\
            ScenarioTestHelpers.CloseCommunicationObjects((ICommunicationObject)serviceProxy, factory);
        }
    }
 
    public class OSXPeerCertificateValidator : X509CertificateValidator
    {
        public override void Validate(X509Certificate2 certificate)
        {
            if (certificate == null)
                throw new ArgumentNullException(nameof(certificate));
 
            Exception exception;
            if (!TryValidate(certificate, out exception))
                throw exception;
        }
 
        static bool StoreContainsCertificate(X509Store store, X509Certificate2 certificate)
        {
            X509Certificate2Collection certificates = null;
            try
            {
                certificates = store.Certificates.Find(X509FindType.FindByThumbprint, certificate.Thumbprint, false);
                return certificates.Count > 0;
            }
            finally
            {
                ResetAllCertificates(certificates);
            }
        }
 
        internal bool TryValidate(X509Certificate2 certificate, out Exception exception)
        {
            // Checklist
            // 1) time validity of cert
            // 2) in local trusted key chain (in place of Trusted People store)
            // 3) not in disallowed store
 
            DateTime now = DateTime.Now;
 
            if (now > certificate.NotAfter || now < certificate.NotBefore)
            {
                exception = new Exception($"The X.509 certificate ({GetCertificateId(certificate)}) usage time is invalid.  " +
                    $"The usage time '{now}' does not fall between NotBefore time '{certificate.NotBefore}' and NotAfter time '{certificate.NotAfter}'.");
                return false;
            }
 
            if (!File.Exists(ServiceUtilHelper.OSXCustomKeychainFilePath))
            {
                // The certificate can't be in a non-existent keychain file
                exception = new Exception($"The X.509 certificate {GetCertificateId(certificate)} is not in the keychain file {ServiceUtilHelper.OSXCustomKeychainFilePath}.");
                return false;
            }
 
            X509Store store;
            using (SafeKeychainHandle keychain = SafeKeychainHandle.Open(ServiceUtilHelper.OSXCustomKeychainFilePath, ServiceUtilHelper.OSXCustomKeychainPassword))
            {
                using (store = new X509Store(keychain.DangerousGetHandle()))
                {
                    if (!StoreContainsCertificate(store, certificate))
                    {
                        exception = new Exception($"The X.509 certificate {GetCertificateId(certificate)} is not in the keychain file {ServiceUtilHelper.OSXCustomKeychainFilePath}.");
                        return false;
                    }
                }
            }
 
            using (store = new X509Store(StoreName.Disallowed, StoreLocation.CurrentUser))
            {
                if (StoreContainsCertificate(store, certificate))
                {
                    exception = new Exception($"The {GetCertificateId(certificate)} X.509 certificate is in an untrusted certificate store.");
                    return false;
                }
            }
 
            exception = null;
            return true;
        }
 
        // This is a workaround, Since store.Certificates returns a full collection
        // of certs in store.  These are holding native resources.
        internal static void ResetAllCertificates(X509Certificate2Collection certificates)
        {
            if (certificates != null)
            {
                for (int i = 0; i < certificates.Count; ++i)
                {
                    ResetCertificate(certificates[i]);
                }
            }
        }
 
        internal static void ResetCertificate(X509Certificate2 certificate)
        {
            certificate.Dispose();
        }
 
        internal static string GetCertificateId(X509Certificate2 certificate)
        {
            StringBuilder str = new StringBuilder(256);
            AppendCertificateIdentityName(str, certificate);
            return str.ToString();
        }
 
        internal static void AppendCertificateIdentityName(StringBuilder str, X509Certificate2 certificate)
        {
            string value = certificate.SubjectName.Name;
            if (String.IsNullOrEmpty(value))
            {
                value = certificate.GetNameInfo(X509NameType.DnsName, false);
                if (String.IsNullOrEmpty(value))
                {
                    value = certificate.GetNameInfo(X509NameType.SimpleName, false);
                    if (String.IsNullOrEmpty(value))
                    {
                        value = certificate.GetNameInfo(X509NameType.EmailName, false);
                        if (String.IsNullOrEmpty(value))
                        {
                            value = certificate.GetNameInfo(X509NameType.UpnName, false);
                        }
                    }
                }
            }
            // Same format as X509Identity
            str.Append(string.IsNullOrEmpty(value) ? "<x509>" : value);
            str.Append("; ");
            str.Append(certificate.Thumbprint);
        }
    }
}