//******************************************************************************************************
// 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
}
}