File: CertificateManager.cs
Web Access
Project: src\src\System.Private.ServiceModel\tests\Common\Infrastructure\Infrastructure.Common.csproj (Infrastructure.Common)
// 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.
 
using System;
using System.IO;
using System.Security.Cryptography; 
using System.Security.Cryptography.X509Certificates;
using System.Text;
 
namespace Infrastructure.Common
{
    // This class manages the adding and removal of certificates.
    // It also handles informing http.sys of which certificate to associate with SSL ports.
    internal static class CertificateManager
    {
        private static object s_certificateLock = new object();
        private static bool s_PlatformSpecificStoreLocationIsSet = false;
        private static StoreLocation s_PlatformSpecificRootStoreLocation = StoreLocation.LocalMachine;
 
        // The location from where to open StoreName.Root
        //
        // Since we don't have access to System.Runtime.InteropServices.IsOSPlatform API, we need to find a novel way to switch
        // the store we use on Linux
        //
        // For Linux: 
        // On Linux, opening stores from the LocalMachine store is not supported yet, so we toggle to use CurrentUser
        // Furthermore, we don't need to sudo in Linux to install a StoreName.Root : StoreLocation.CurrentUser. So this will allow
        // tests requiring certs to pass in Linux. 
        // See dotnet/corefx#3690
        //
        // For Windows: 
        // We don't want to use CurrentUser, as writing to StoreName.Root : StoreLocation.CurrentUser will result in a 
        // modal dialog box that isn't dismissable if the root cert hasn't already been installed previously. 
        // If the cert has already been installed, writing to StoreName.Root : StoreLocation.CurrentUser results in a no-op
        // 
        // In other words, on Windows, we can bypass the modal dialog box, but only if we install to StoreName.Root : StoreLocation.LocalMachine
        // To do this though means that we must run certificate-based tests elevated
        internal static StoreLocation PlatformSpecificRootStoreLocation
        {
            get
            {
                if (!s_PlatformSpecificStoreLocationIsSet)
                {
                    try
                    {
                        using (var store = new X509Store(StoreName.Root, StoreLocation.LocalMachine))
                        {
                            store.Open(OpenFlags.ReadWrite);
                        }
                    }
                    catch (PlatformNotSupportedException)
                    {
                        // Linux
                        s_PlatformSpecificRootStoreLocation = StoreLocation.CurrentUser; 
                    }
                    catch(CryptographicException)
                    {
                        // Linux
                        s_PlatformSpecificRootStoreLocation = StoreLocation.CurrentUser;
                    }
 
                    s_PlatformSpecificStoreLocationIsSet = true;
                }
                return s_PlatformSpecificRootStoreLocation;
            }
        }
 
        internal static string OSXCustomKeychainFilePath
        {
            get
            {
                return Path.Combine(Environment.CurrentDirectory, "wcfLocal.keychain");
            }
        }
 
        internal static string OSXCustomKeychainPassword
        {
            get
            {
                return "WCFKeychainFilePassword";
            }
        }
 
        // Adds the given certificate to the given store unless it is
        // already present.  Returns the  certificate either already in
        // the store or the one requested.
        public static X509Certificate2 AddToStoreIfNeeded(StoreName storeName,
                                                          StoreLocation storeLocation,
                                                          X509Certificate2 certificate)
        {
            X509Certificate2 resultCert = null;
            lock (s_certificateLock)
            {
                // Open the store as ReadOnly first, as it prevents the need for elevation if opening
                // a LocalMachine store
                using (X509Store store = new X509Store(storeName, storeLocation))
                {
                    store.Open(OpenFlags.ReadOnly);
                    resultCert = CertificateFromThumbprint(store, certificate.Thumbprint, validOnly: false);
                }
 
                // Not already in store.  We need to add it.
                if (resultCert == null)
                {
                    using (X509Store store = new X509Store(storeName, storeLocation))
                    {
                        try
                        {
                            store.Open(OpenFlags.ReadWrite);
                            store.Add(certificate);
                            resultCert = certificate;
                        }
                        catch (CryptographicException inner)
                        {
                            StringBuilder exceptionString = new StringBuilder();
                            exceptionString.AppendFormat("Error opening StoreName: '{0}' certificate store from StoreLocation '{1}' in ReadWrite mode ", storeName, storeLocation);
                            exceptionString.AppendFormat("while attempting to install cert with thumbprint '{1}'.", Environment.NewLine, certificate.Thumbprint);
                            exceptionString.AppendFormat("{0}This is usually due to permissions issues if writing to the LocalMachine location", Environment.NewLine);
                            exceptionString.AppendFormat("{0}Try running the test with elevated or superuser permissions.", Environment.NewLine);
 
                            throw new InvalidOperationException(exceptionString.ToString(), inner);
                        }
                    }
                }
            }
 
            return resultCert;
        }
 
        // Adds the given certificate to the given keychain unless it is
        // already present.  Returns the  certificate either already in
        // the store or the one requested.
        public static X509Certificate2 AddToOSXKeyChainIfNeeded(SafeKeychainHandle keychain,
                                                          X509Certificate2 certificate)
        {
            X509Certificate2 resultCert = null;
            lock (s_certificateLock)
            {
                using (X509Store store = new X509Store(keychain.DangerousGetHandle()))
                {
                    // No need to open X509Store as it is already opened with mode of MaxAllowed 
                    resultCert = CertificateFromThumbprint(store, certificate.Thumbprint, validOnly: false);
                    // Not already in store.  We need to add it.
                    if (resultCert == null)
                    {
                        try
                        {
                            // We don't need the private key and the X509Certificate2 instance wasn't created as exportable.
                            // This creates a new certificate instance with only the public key so that it can be added to keychain.
                            var publicOnly = new X509Certificate2(certificate.RawData);
                            store.Add(publicOnly);
                            resultCert = publicOnly;
                        }
                        catch (CryptographicException inner)
                        {
                            throw new InvalidOperationException($"Error while attempting to install cert with thumbprint '{certificate.Thumbprint}' into OSX custom keychain.", inner);
                        }
                    }
                }
            }
 
            return resultCert;
        }
 
        // Returns the certificate matching the given thumbprint from the given store.
        // Returns null if not found.
        private static X509Certificate2 CertificateFromThumbprint(X509Store store, string thumbprint, bool validOnly)
        {
            X509Certificate2Collection foundCertificates = store.Certificates.Find(X509FindType.FindByThumbprint, thumbprint, validOnly);
            return foundCertificates.Count == 0 ? null : foundCertificates[0];
        }
 
        private static X509Certificate2 CertificateFromThumbprint(StoreName storeName,
                                                                  StoreLocation storeLocation,
                                                                  string thumbprint,
                                                                  bool validOnly)
        {
            X509Certificate2 resultCert = null;
            using (X509Store store = new X509Store(storeName, storeLocation))
            {
                store.Open(OpenFlags.ReadOnly);
                resultCert = CertificateFromThumbprint(store, thumbprint, validOnly);
            }
 
            return resultCert;
        }
 
        private static X509Certificate2 KeychainCertificateFromThumbprint(string thumbprint, bool validOnly)
        {
            X509Certificate2 resultCert = null;
            using (SafeKeychainHandle handle = SafeKeychainHandle.Open(CertificateManager.OSXCustomKeychainFilePath, CertificateManager.OSXCustomKeychainPassword))
            {
                using (X509Store store = new X509Store(handle.DangerousGetHandle()))
                {
                    resultCert = CertificateFromThumbprint(store, thumbprint, validOnly);
                }
            }
 
            return resultCert;
        }
 
        // Retrieves a root certificate matching the given thumbprint from the root store
        public static X509Certificate2 RootCertificateFromThumprint(string thumbprint, bool validOnly)
        {
            return CertificateFromThumbprint(StoreName.Root, PlatformSpecificRootStoreLocation, thumbprint, validOnly);
        }
 
        // Retrieves a client certificate matching the given thumbprint from the certificate store
        public static X509Certificate2 ClientCertificateFromThumprint(string thumbprint, bool validOnly)
        {
            return CertificateFromThumbprint(StoreName.My, StoreLocation.CurrentUser, thumbprint, validOnly);
        }
 
        // Retrieves a server certificate matching the given thumbprint from the certificate store
        public static X509Certificate2 PeerCertificateFromThumprint(string thumbprint, bool validOnly)
        {
            return CertificateFromThumbprint(StoreName.TrustedPeople, StoreLocation.CurrentUser, thumbprint, validOnly);
        }
 
        // Retrieves a server certificate matching the given thumbprint from a OSX local Keychain store
        public static X509Certificate2 OSXLocalKeychainCertificateFromThumprint(string thumbprint, bool validOnly)
        {
            return KeychainCertificateFromThumbprint(thumbprint, validOnly);
        }
 
        // Install the certificate into the Root store and returns its thumbprint.
        // It will not install the certificate if it is already present in the store.
        // It returns the thumbprint of the certificate, regardless whether it was added or found.
        public static X509Certificate2 InstallCertificateToRootStore(X509Certificate2 certificate)
        {
            // See explanation of StoreLocation selection at PlatformSpecificRootStoreLocation
            certificate = AddToStoreIfNeeded(StoreName.Root, PlatformSpecificRootStoreLocation, certificate);
            return certificate;
        }
 
        // Install the certificate into the My store.
        // It will not install the certificate if it is already present in the store.
        // It returns the thumbprint of the certificate, regardless whether it was added or found.
        public static X509Certificate2 InstallCertificateToMyStore(X509Certificate2 certificate)
        {
            // Always install client certs to CurrentUser
            // StoreLocation.CurrentUser is supported on both Linux and Windows 
            // Furthermore, installing this cert to this location does not require sudo or admin elevation
            certificate = AddToStoreIfNeeded(StoreName.My, StoreLocation.CurrentUser, certificate);
 
            return certificate;
        }
 
        // Install the certificate into the TrustedPeople store.
        // It will not install the certificate if it is already present in the store.
        // It returns the thumbprint of the certificate, regardless whether it was added or found.
        public static X509Certificate2 InstallCertificateToTrustedPeopleStore(X509Certificate2 certificate)
        {
            // Always install certs to CurrentUser
            // StoreLocation.CurrentUser is supported on both Linux and Windows 
            // Furthermore, installing this cert to this location does not require sudo or admin elevation
            certificate = AddToStoreIfNeeded(StoreName.TrustedPeople, StoreLocation.CurrentUser, certificate);
 
            return certificate;
        }
 
        // Install the certificate into a custom keychain on OSX. The TrustedPeople store isn't supported
        // on OSX but a similar mechanism can be achieved by creating a custom keychain and using it in
        // the same way as the TrustedPeople store.
        // It will not install the certificate if it is already present in the store.
        // It returns the thumbprint of the certificate, regardless whether it was added or found.
        public static X509Certificate2 InstallCertificateToOSXKeychainStore(X509Certificate2 certificate)
        {
            SafeKeychainHandle keychain;
 
            if (!File.Exists(OSXCustomKeychainFilePath))
            {
                keychain = SafeKeychainHandle.Create(OSXCustomKeychainFilePath, OSXCustomKeychainPassword);
            }
            else
            {
                keychain = SafeKeychainHandle.Open(OSXCustomKeychainFilePath, OSXCustomKeychainPassword);
            }
 
            certificate = AddToOSXKeyChainIfNeeded(keychain, certificate);
 
            return certificate;
        }
    }
}