//****************************************************************************************************** // SelfSignedCertificateGenerator.cs - Gbtc // // Copyright © 2013, Grid Protection Alliance. All Rights Reserved. // // Licensed to the Grid Protection Alliance (GPA) under one or more contributor license agreements. See // the NOTICE file distributed with this work for additional information regarding copyright ownership. // The GPA licenses this file to you under the MIT License (MIT), the "License"; you may // not use this file except in compliance with the License. You may obtain a copy of the License at: // // http://www.opensource.org/licenses/MIT // // Unless agreed to in writing, the subject software distributed under the License is distributed on an // "AS-IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. Refer to the // License for the specific language governing permissions and limitations. // // Code Modification History: // ---------------------------------------------------------------------------------------------------- // 12/18/2013 - Stephen C. Wills // Generated original version of source code. // //****************************************************************************************************** using System; using System.Collections.Generic; using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using System.IO; using System.Linq; using System.Net; using System.Security; using System.Security.Cryptography; using System.Security.Cryptography.X509Certificates; using System.Text; using GSF.IO; namespace GSF.Net.Security { /// /// The CertificateGenerator searches certificate stores for existing certificates and creates /// self-signed certificates if no matching certificate exists in any accessible store. /// It then generates a certificate file at the specified certificate path. /// public class CertificateGenerator { #region [ Members ] // Fields private string m_issuer; private string[] m_subjectNames; private string m_certificatePath; private List m_debugLog; #endregion #region [ Constructors ] /// /// Creates a new instance of the class. /// public CertificateGenerator() { m_debugLog = new List(); } #endregion #region [ Properties ] /// /// Gets or sets the name of the entity issuing the certificate. /// public string Issuer { get { return (m_issuer = m_issuer ?? GetDefaultIssuer()); } set { m_issuer = value; } } /// /// Gets or sets the subject names (common names) /// of the entity that this certificate identifies. /// public string[] SubjectNames { get { return (m_subjectNames = m_subjectNames ?? GetDefaultSubjectNames()); } set { m_subjectNames = value; } } /// /// Gets or sets the path to the certificate file /// that is generated by this certificate generator. /// public string CertificatePath { get { return (m_certificatePath = m_certificatePath ?? GetDefaultCertificatePath()); } set { m_certificatePath = value; } } /// /// Gets a list of detailed log messages /// public List DebugLog { get { return new List(m_debugLog); } } #endregion #region [ Methods ] /// /// Generates the certificate. /// /// The certificate that was generated by this certificate generator. [SuppressMessage("Microsoft.Maintainability", "CA1502:AvoidExcessiveComplexity")] public X509Certificate2 GenerateCertificate() { List stores; List storedCertificates; X509Certificate2 certificate = null; string certificatePath; string commonNameList; byte[] certificateData; ProcessStartInfo processInfo; string makeCertPath; m_debugLog.Clear(); stores = new List() { new X509Store(StoreName.My, StoreLocation.LocalMachine), new X509Store(StoreName.My, StoreLocation.CurrentUser) }; // Attempt to get an existing certificate from the given certificate path certificatePath = FilePath.GetAbsolutePath(CertificatePath); if (File.Exists(certificatePath)) m_debugLog.Add($"Searching for existing certificate...found ({certificatePath})"); else m_debugLog.Add($"Searching for existing certificate...not found ({certificatePath})"); if (File.Exists(certificatePath)) { try { certificate = new X509Certificate2(certificatePath); } catch (CryptographicException ex) { string message = new StringBuilder() .AppendLine() .AppendLine("Error opening existing certificate:") .AppendLine(ex.ToString()) .AppendLine() .ToString(); m_debugLog.Add(message); } } try { TryOpenStores(stores, OpenFlags.ReadOnly); if (stores.Count == 0) m_debugLog.Add($"Opening certificate stores with readonly access...failed"); else if (stores.Count == 1) m_debugLog.Add($"Opening certificate stores with readonly access...opened {stores[0].Location} store"); else m_debugLog.Add($"Opening certificate stores with readonly access...success"); // If a valid certificate exists on the certificate path, // search the certificate stores to determine if we have // access to its private key if ((object)certificate != null) { bool canAccessPrivateKey; storedCertificates = stores.SelectMany(store => store.Certificates.Cast()).ToList(); FindMatchingCertificates(storedCertificates, certificate); canAccessPrivateKey = storedCertificates.Any(CanAccessPrivateKey); if (canAccessPrivateKey) m_debugLog.Add($"Attempting to access existing certificate's private key...success"); else m_debugLog.Add($"Attempting to access existing certificate's private key...failed"); if (canAccessPrivateKey) return certificate; } // Search the certificate stores for certificates // with a matching issuer and accessible private keys commonNameList = GetCommonNameList(); storedCertificates = stores.SelectMany(store => store.Certificates.Cast()).ToList(); certificate = storedCertificates.FirstOrDefault(storedCertificate => storedCertificate.Issuer.Equals(commonNameList) && CanAccessPrivateKey(storedCertificate)); if ((object)certificate != null) m_debugLog.Add($"Searching stores for a usable certificate with accessible private key...success"); else m_debugLog.Add($"Searching stores for a usable certificate with accessible private key...failed"); // If such a certificate exists, generate the certificate file and return the result if ((object)certificate != null) { using (FileStream certificateStream = File.OpenWrite(certificatePath)) { certificateData = certificate.Export(X509ContentType.Cert); certificateStream.Write(certificateData, 0, certificateData.Length); } return new X509Certificate2(certificatePath); } } finally { CloseStores(stores); } try { // Ensure that we can write to the certificate // stores before generating a new certificate TryOpenStores(stores, OpenFlags.ReadWrite); if (stores.Count == 0) m_debugLog.Add($"Opening certificate stores with read-write access...failed"); else if (stores.Count == 1) m_debugLog.Add($"Opening certificate stores with read-write access...opened {stores[0].Location} store"); else m_debugLog.Add($"Opening certificate stores with read-write access...success"); } finally { CloseStores(stores); } // Attempt to use makecert to create a new self-signed certificate makeCertPath = FilePath.GetAbsolutePath("makecert.exe"); if (File.Exists(makeCertPath)) m_debugLog.Add("Searching for makecert utility to generate a new certificate...success"); else m_debugLog.Add("Searching for makecert utility to generate a new certificate...failed"); if (File.Exists(makeCertPath)) { foreach (X509Store store in stores) { processInfo = new ProcessStartInfo(makeCertPath); processInfo.Arguments = string.Format("-r -pe -n \"{0}\" -ss My -sr {1} \"{2}\"", commonNameList, store.Location, certificatePath); processInfo.UseShellExecute = true; using (Process makeCertProcess = Process.Start(processInfo)) { makeCertProcess.WaitForExit(); } if (File.Exists(certificatePath)) m_debugLog.Add($"Attemping to generate a new certificate in the {store.Location} store... success"); else m_debugLog.Add($"Attemping to generate a new certificate in the {store.Location} store... failed"); if (File.Exists(certificatePath)) return new X509Certificate2(certificatePath); } } // All attempts to generate the certificate failed, so we must throw an exception throw new InvalidOperationException("Unable to generate the self-signed certificate."); } /// /// Exports the private key of the certificate that is currently in use to the given path, /// encrypting the file with the given password. /// /// The path to which the certificate will be exported. /// The password used to encrypt the certificate file. public void ExportCertificateWithPrivateKey(string exportPath, SecureString password) { // Attempt to get an existing certificate from the given certificate path string certificatePath = FilePath.GetAbsolutePath(CertificatePath); X509Certificate2 certificate = new X509Certificate2(certificatePath); using (X509Store store = new X509Store(StoreName.My, StoreLocation.CurrentUser)) { store.Open(OpenFlags.ReadOnly); List certificates = store.Certificates.Cast().ToList(); FindMatchingCertificates(certificates, certificate); X509Certificate2 pvkCertificate = certificates.FirstOrDefault(CanAccessPrivateKey); if ((object)pvkCertificate == null) throw new InvalidOperationException("Could not locate private key in the current user store."); byte[] certificateData = pvkCertificate.Export(X509ContentType.Pfx, password); File.Delete(exportPath); File.WriteAllBytes(exportPath, certificateData); store.Close(); } } /// /// Imports the given certificate into the current user store and then exports the public key. /// /// The file with the certificate and private key to be imported. /// The password used to encrypt the certificate file with the private key. public void ImportCertificateWithPrivateKey(string importPath, SecureString password) { X509KeyStorageFlags keyStorageFlags = X509KeyStorageFlags.Exportable | X509KeyStorageFlags.PersistKeySet; X509Certificate2 certificate = new X509Certificate2(importPath, password, keyStorageFlags); using (X509Store store = new X509Store(StoreName.My, StoreLocation.CurrentUser)) { store.Open(OpenFlags.ReadWrite); store.Add(certificate); store.Close(); } string certificatePath = FilePath.GetAbsolutePath(CertificatePath); byte[] certificateData = certificate.Export(X509ContentType.Cert); File.WriteAllBytes(certificatePath, certificateData); } // Gets the list of common names to be passed to // makecert when generating self-signed certificates. private string GetCommonNameList() { return string.Join(",", new string[] { Issuer }.Concat(SubjectNames).Distinct().Select(name => "CN=" + name)); } // Gets the default value for the issuer. private string GetDefaultIssuer() { return Dns.GetHostEntry(Dns.GetHostName()).HostName; } // Gets the default value for the subject names. // This uses a DNS lookup to determine the host name of the system and // all the possible IP addresses and aliases that the system may go by. private string[] GetDefaultSubjectNames() { IPHostEntry hostEntry = Dns.GetHostEntry(Dns.GetHostName()); return hostEntry.AddressList #if !MONO .Select(address => address.MapToIPv6()) #endif .Concat(hostEntry.AddressList) .Distinct() .Select(address => address.ToString()) .Concat(hostEntry.Aliases) .Concat(new string[] { Environment.MachineName, hostEntry.HostName }) .ToArray(); } // Gets the default path to which the certificate file will be generated. private string GetDefaultCertificatePath() { return string.Format("{0}.cer", Issuer); } // Attempts to open all the stores in the given list of stores. // After returning, the list of stores contains only the stores which could be opened. private void TryOpenStores(List stores, OpenFlags flags) { for (int i = stores.Count - 1; i >= 0; i--) { try { stores[i].Open(flags); } catch { stores.RemoveAt(i); } } } // Filters the given list of certificates down to only the certificates that match the given certificate. private void FindMatchingCertificates(List certificates, X509Certificate2 certificate) { byte[] hash = certificate.GetCertHash(); byte[] key = certificate.GetPublicKey(); for (int i = certificates.Count - 1; i >= 0; i--) { if (!hash.SequenceEqual(certificates[i].GetCertHash()) || !key.SequenceEqual(certificates[i].GetPublicKey())) certificates.RemoveAt(i); } } // Determines if the current application has access to the private key of the given certificate. private bool CanAccessPrivateKey(X509Certificate2 certificate) { try { // The point here is not only to check if the certificate has a private key, // but also to attempt to access its private key, since doing so might result // in a CryptographicException; certificate.HasPrivateKey will not work return ((object)certificate.PrivateKey != null); } catch (CryptographicException) { return false; } } // Closes all stores in the given list of stores. private void CloseStores(List stores) { foreach (X509Store store in stores) store.Close(); } #endregion } }