ps/Modules/Alkami.Ops.Common/Cryptography/CertificateHelper.cs

713 lines
30 KiB
C#
Raw Permalink Normal View History

2023-05-30 22:51:22 -07:00
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Security.AccessControl;
using System.Security.Cryptography;
using System.Security.Cryptography.X509Certificates;
using System.Security.Principal;
using Alkami.Ops.Common.Exceptions;
using Alkami.Ops.Common.Extensions;
using Alkami.Ops.Common.NativeMethods;
namespace Alkami.Ops.Common.Cryptography
{
public static class CertificateHelper
{
private const int CertStoreProvSystem = 10;
private const int CertSystemStoreCurrentUser = (1 << 16);
private const int CertSystemStoreLocalMachine = (2 << 16);
/// <summary>
/// Finds a Certificate By Thumbprint
/// </summary>
/// <param name="thumbPrint"></param>
/// <param name="storeName"></param>
/// <param name="location"></param>
/// <param name="hostName"></param>
/// <param name="machineName"></param>
/// <returns></returns>
/// <exception cref="System.ArgumentNullException"></exception>
/// <exception cref="System.ArgumentException"></exception>
/// <exception cref="System.InvalidOperationException"></exception>
/// <exception cref="System.Security.Cryptography.CryptographicException"></exception>
/// <exception cref="System.Security.SecurityException"></exception>
public static X509Certificate2 FindCertificateByThumbprint(string thumbPrint, StoreName storeName,
StoreLocation location, string hostName, string machineName = null)
{
var searchLocal =
string.Equals(hostName, Environment.MachineName, StringComparison.OrdinalIgnoreCase) ||
string.Equals(machineName, Environment.MachineName, StringComparison.OrdinalIgnoreCase) ||
string.Equals(hostName, "localhost", StringComparison.OrdinalIgnoreCase);
if (searchLocal)
{
return SearchLocalStore(thumbPrint, storeName, location, X509FindType.FindByThumbprint);
}
return SearchRemoteStoreByThumbprint(thumbPrint, storeName, location, hostName);
}
/// <summary>
/// Finds the WMSvc Certificate for the local machine
/// </summary>
/// <returns></returns>
public static X509Certificate2 FindIISIssuedCertificate()
{
var certificates = GetAllCertificates(StoreName.My, StoreLocation.LocalMachine, string.Empty);
foreach (var certificate in certificates)
{
var friendlyName = certificate.FriendlyName;
if (friendlyName.StartsWith("WMSVC", StringComparison.OrdinalIgnoreCase))
{
return certificate;
}
}
return null;
}
/// <summary>
/// Searches for Certificates by Subject or Subject Alternate Name
/// </summary>
/// <remarks>
/// Additionally searches for a "wildcard" match, such as *.dev.alkamitech.com
/// </remarks>
/// <param name="subjectOrSAN"></param>
/// <param name="storeName"></param>
/// <param name="storeLocation"></param>
/// <param name="machineName"></param>
/// <returns></returns>
public static X509Certificate2 FindCertificatebySubjectOrSAN(string subjectOrSAN, StoreName storeName, StoreLocation storeLocation, string machineName = null)
{
var segments = subjectOrSAN.Split('.');
segments[0] = "*";
var wildcardUri = string.Join(".", segments);
// If all we got is a *, we won't search for wildcards, since it would be unreliable
var searchWildCard = (wildcardUri != "*");
var certificates = GetAllCertificates(storeName, storeLocation, machineName);
foreach (var certificate in certificates)
{
var subjectAlternateNames = certificate.Extensions.OfType<X509Extension>().FirstOrDefault(e => e.Oid.FriendlyName == "Subject Alternative Name");
var subject = certificate.GetNameInfo(X509NameType.SimpleName, false);
if (subjectAlternateNames != null)
{
var asnData = new AsnEncodedData(subjectAlternateNames.Oid, subjectAlternateNames.RawData);
if (asnData.Format(true).IndexOf(subjectOrSAN, StringComparison.OrdinalIgnoreCase) >= 0 ||
(searchWildCard && asnData.Format(true).IndexOf(wildcardUri, StringComparison.OrdinalIgnoreCase) >= 0))
{
return certificate;
}
}
if ((subject != null) && (subject.IndexOf(subjectOrSAN, StringComparison.OrdinalIgnoreCase) >= 0 ||
(searchWildCard && subject.IndexOf(wildcardUri, StringComparison.OrdinalIgnoreCase) >= 0)))
{
return certificate;
}
}
return null;
}
/// <summary>
/// Loads a Certificate in to the Specified Store
/// </summary>
/// <param name="certificate"></param>
/// <param name="storeName"></param>
/// <param name="location"></param>
/// <exception cref="System.ArgumentException"></exception>
/// <exception cref="System.Security.Cryptography.CryptographicException"></exception>
/// <exception cref="System.Security.SecurityException"></exception>
public static void LoadCertificateToStore(X509Certificate2 certificate, StoreName storeName, StoreLocation location)
{
var store = new X509Store(storeName, location);
try
{
store.Open(OpenFlags.ReadWrite);
store.Add(certificate);
}
finally
{
store.Close();
}
}
/// <summary>
/// Loads a Certificate in to the Specified Store
/// </summary>
/// <param name="certificatePath"></param>
/// <param name="storeName"></param>
/// <param name="location"></param>
/// <param name="password"></param>
/// <exception cref="System.ArgumentException"></exception>
/// <exception cref="System.Security.Cryptography.CryptographicException"></exception>
/// <exception cref="System.Security.SecurityException"></exception>
public static void LoadCertificateToStore(string certificatePath, StoreName storeName, StoreLocation location, string password = null)
{
X509KeyStorageFlags flags;
if (location == StoreLocation.LocalMachine)
{
flags = X509KeyStorageFlags.Exportable | X509KeyStorageFlags.MachineKeySet | X509KeyStorageFlags.PersistKeySet;
}
else
{
flags = X509KeyStorageFlags.Exportable | X509KeyStorageFlags.UserKeySet | X509KeyStorageFlags.PersistKeySet;
}
var certificate = !string.IsNullOrEmpty(password)
? new X509Certificate2(certificatePath, password, flags)
: new X509Certificate2(certificatePath);
LoadCertificateToStore(certificate, storeName, location);
}
/// <summary>
/// Loads the Certificate, discarding the private key, to a store
/// </summary>
/// <param name="certificatePath"></param>
/// <param name="storeName"></param>
/// <param name="location"></param>
public static void LoadCertificateFromPFXToStore(string certificatePath, StoreName storeName, StoreLocation location)
{
var certificateOnly = new X509Certificate2(certificatePath);
LoadCertificateToStore(certificateOnly, storeName, location);
}
/// <summary>
/// Removes a certificate or certificates from a store by name or thumbprint
/// </summary>
/// <param name="certificateNameOrThumbprint"></param>
/// <param name="storeName"></param>
/// <param name="location"></param>
public static void RemoveCertificateFromStore(string certificateNameOrThumbprint, StoreName storeName, StoreLocation location)
{
var store = new X509Store(storeName, location);
try
{
store.Open(OpenFlags.ReadWrite);
var certificateCollection = store.Certificates.Find(
certificateNameOrThumbprint.IsBase64String()
? X509FindType.FindByThumbprint
: X509FindType.FindBySubjectName,
certificateNameOrThumbprint, false);
store.RemoveRange(certificateCollection);
}
finally
{
store.Close();
}
}
/// <summary>
/// Validates a certificate chain and effective and expiration dates
/// </summary>
/// <param name="certificateNameOrThumbprint"></param>
/// <param name="storeName"></param>
/// <param name="location"></param>
public static void ValidateCertificate(string certificateNameOrThumbprint, StoreName storeName, StoreLocation location)
{
var store = new X509Store(storeName, location);
try
{
store.Open(OpenFlags.ReadWrite);
var certificateCollection = store.Certificates.Find(
certificateNameOrThumbprint.IsBase64String()
? X509FindType.FindByThumbprint
: X509FindType.FindBySubjectName,
certificateNameOrThumbprint, false);
foreach (var certificate in certificateCollection)
{
DateTime effectiveDate;
if (!DateTime.TryParse(certificate.GetEffectiveDateString(), out effectiveDate))
{
throw new InvalidCertificateException("Could not read certificate effective date", certificate.FriendlyName,
certificate.Thumbprint);
}
DateTime expirationDate;
if (!DateTime.TryParse(certificate.GetExpirationDateString(), out expirationDate))
{
throw new InvalidCertificateException("Could not read certificate expiration date", certificate.FriendlyName,
certificate.Thumbprint);
}
if (effectiveDate > DateTime.Now)
{
var message = $"Certificate effective date {effectiveDate:yyyy-mm-dd hh:mm:ss} is not yet in effect";
throw new InvalidCertificateException(message, certificate.FriendlyName, certificate.Thumbprint, effectiveDate, expirationDate);
}
if (expirationDate < DateTime.Now)
{
var message = $"Certificate expiration date {expirationDate:yyyy-mm-dd hh:mm:ss} has already passed";
throw new InvalidCertificateException(message, certificate.FriendlyName, certificate.Thumbprint, effectiveDate, expirationDate);
}
if (!certificate.Verify())
{
throw new InvalidCertificateException("Certificate chain validation failed", certificate.FriendlyName, certificate.Thumbprint,
effectiveDate, expirationDate);
}
}
}
finally
{
store.Close();
}
}
/// <summary>
/// Grants rights to private key files on disk
/// </summary>
/// <param name="certificateNameOrThumbprint"></param>
/// <param name="storeName"></param>
/// <param name="location"></param>
/// <param name="user"></param>
/// <exception cref="System.ArgumentNullException"></exception>
/// <exception cref="System.ArgumentOutOfRangeException"></exception>
/// <exception cref="System.ArgumentException"></exception>
/// <exception cref="System.ComponentModel.Win32Exception"></exception>
/// <exception cref="System.FormatException"></exception>
/// <exception cref="System.InvalidOperationException"></exception>
/// <exception cref="System.IO.DirectoryNotFoundException"></exception>
/// <exception cref="System.IO.FileNotFoundException"></exception>
/// <exception cref="System.IO.PathTooLongException"></exception>
/// <exception cref="System.IO.IOException"></exception>
/// <exception cref="System.NotSupportedException"></exception>
/// <exception cref="System.ObjectDisposedException"></exception>
/// <exception cref="System.OverflowException"></exception>
/// <exception cref="System.PlatformNotSupportedException"></exception>
/// <exception cref="System.Security.Cryptography.CryptographicException"></exception>
/// <exception cref="System.Security.Cryptography.CryptographicUnexpectedOperationException"></exception>
/// <exception cref="System.Security.SecurityException"></exception>
/// <exception cref="System.SystemException"></exception>
/// <exception cref="System.UnauthorizedAccessException"></exception>
public static void GrantRightsToPrivateKeys(string certificateNameOrThumbprint, StoreName storeName, StoreLocation location, string user)
{
var cert = SearchLocalStore(certificateNameOrThumbprint, storeName, location, certificateNameOrThumbprint.IsBase64String()
? X509FindType.FindByThumbprint
: X509FindType.FindBySubjectName);
var rsaKey = cert.PrivateKey as RSACryptoServiceProvider;
if (rsaKey == null)
{
throw new PrivateKeyNotFoundException(
"The key file could not be found for the certificate",
cert.GetNameInfo(X509NameType.SimpleName, false),
cert.Thumbprint,
storeName.ToString(),
location.ToString()
);
}
var keyFilePath = FindKeyLocation(rsaKey.CspKeyContainerInfo.UniqueKeyContainerName);
var pkFile = new FileInfo(Path.Combine(keyFilePath, rsaKey.CspKeyContainerInfo.UniqueKeyContainerName));
GrantRightsToPrivateKeys(pkFile, user);
}
/// <summary>
/// Export a Certificate by Name or Thumbprint
/// </summary>
/// <param name="certificateNameOrThumbprint"></param>
/// <param name="storeName"></param>
/// <param name="location"></param>
/// <param name="exportPath"></param>
/// <param name="password"></param>
public static void ExportCertificate(string certificateNameOrThumbprint, StoreName storeName, StoreLocation location, string exportPath, string password = null)
{
var cert = SearchLocalStore(certificateNameOrThumbprint, storeName, location, certificateNameOrThumbprint.IsBase64String()
? X509FindType.FindByThumbprint
: X509FindType.FindBySubjectName);
ExportCertificate(cert, exportPath, password);
}
/// <summary>
/// Exports a specific certificate to the destination exportPath folder.
/// </summary>
/// <param name="certificate">The certificate to be exported.</param>
/// <param name="exportPath">The path of the folder to export the certificate to.</param>
/// <param name="password">Optional password for the certificate.</param>
public static void ExportCertificate(X509Certificate2 certificate, string exportPath, string password = null)
{
// Don't export expired certificates.
bool expired = DateTime.Now > certificate.NotAfter;
if (expired)
{
return;
}
bool hasPassword = !string.IsNullOrEmpty(password);
// Get the byte data of the certificate.
byte[] bytes;
string extension;
if (hasPassword)
{
if (!certificate.HasPrivateKey)
{
extension = ".cer";
bytes = certificate.Export(X509ContentType.Cert);
}
else
{
extension = ".pfx";
bytes = certificate.Export(X509ContentType.Pkcs12, password);
}
}
else
{
extension = ".cer";
bytes = certificate.Export(X509ContentType.Cert);
}
// Determine the name of the file by friendly name and thumbprint for uniqueness.
string certName = certificate.GetNameInfo(X509NameType.SimpleName, false).GetSanitizedFileName() + "-" + certificate.Thumbprint;
// Save the cert.
var exportFileName = Path.Combine(exportPath, certName + extension);
File.WriteAllBytes(exportFileName, bytes);
}
/// <summary>
/// Export All Certificates from a Specific Store
/// </summary>
/// <param name="storeName"></param>
/// <param name="location"></param>
/// <param name="exportPath"></param>
/// <param name="password"></param>
public static List<CertificateExportException> ExportAllCertificates(StoreName storeName, StoreLocation location, string exportPath, string password = null)
{
var store = new X509Store(storeName, location);
var exceptionList = new List<CertificateExportException>();
try
{
store.Open(OpenFlags.OpenExistingOnly | OpenFlags.ReadOnly);
foreach (var cert in store.Certificates)
{
try
{
ExportCertificate(cert, exportPath, password);
}
catch (Exception ex)
{
exceptionList.Add(new CertificateExportException("Could not Export Certificate Due to Errors",
cert.GetNameInfo(X509NameType.SimpleName, false), cert.SubjectName.Name,
cert.Thumbprint, ex.Message));
}
}
}
finally
{
store.Close();
}
return exceptionList;
}
/// <summary>
/// Returns all Certificates from the Specified Store as an X509Certificate2Collection
/// </summary>
/// <param name="storeName"></param>
/// <param name="location"></param>
/// <param name="machineName"></param>
/// <returns></returns>
public static X509Certificate2Collection GetAllCertificates(StoreName storeName, StoreLocation location, string machineName = null)
{
return (string.Equals(machineName, Environment.MachineName, StringComparison.OrdinalIgnoreCase) ||
string.IsNullOrEmpty(machineName))
? GetCertificatesFromLocalStore(storeName, location)
: GetCertificatesFromRemoteStore(storeName, location, machineName);
}
/// <summary>
/// Returns Certificates from the Specified Remote Store
/// </summary>
/// <param name="storeName"></param>
/// <param name="location"></param>
/// <param name="machineName"></param>
/// <returns></returns>
private static X509Certificate2Collection GetCertificatesFromRemoteStore(StoreName storeName, StoreLocation location, string machineName)
{
var safeHandle = IntPtr.Zero;
try
{
safeHandle = GetRemoteCertificateStore(storeName, location, machineName, safeHandle);
if (safeHandle == IntPtr.Zero)
{
return new X509Certificate2Collection();
}
var currentCertContext = IntPtr.Zero;
var store = new X509Store("temp");
store.Open(OpenFlags.ReadWrite);
do
{
currentCertContext = SafeNativeMethods.CertEnumCertificatesInStore(safeHandle, currentCertContext);
if (currentCertContext == IntPtr.Zero)
{
continue;
}
store.Add(new X509Certificate2(currentCertContext));
} while (currentCertContext != (IntPtr)0);
return store.Certificates;
}
finally
{
if (safeHandle != IntPtr.Zero)
{
SafeNativeMethods.CertCloseStore(safeHandle, 0);
}
}
}
/// <summary>
/// Returns Certificates from the Specified Local Store
/// </summary>
/// <param name="storeName"></param>
/// <param name="location"></param>
/// <returns></returns>
private static X509Certificate2Collection GetCertificatesFromLocalStore(StoreName storeName, StoreLocation location)
{
var store = new X509Store(storeName, location);
try
{
store.Open(OpenFlags.OpenExistingOnly | OpenFlags.ReadOnly);
return store.Certificates;
}
finally
{
store.Close();
}
}
/// <summary>
/// Grants rights to private key files on disk
/// </summary>
/// <param name="pkFile"></param>
/// <param name="user"></param>
/// <exception cref="System.ArgumentNullException"></exception>
/// <exception cref="System.ArgumentOutOfRangeException"></exception>
/// <exception cref="System.ArgumentException"></exception>
/// <exception cref="System.IO.IOException"></exception>
/// <exception cref="System.PlatformNotSupportedException"></exception>
/// <exception cref="System.Security.AccessControl.PrivilegeNotHeldException"></exception>
/// <exception cref="System.SystemException"></exception>
/// <exception cref="System.UnauthorizedAccessException"></exception>
public static void GrantRightsToPrivateKeys(FileInfo pkFile, string user)
{
if (!pkFile.Exists)
{
return;
}
var fs = pkFile.GetAccessControl();
var account = new NTAccount(user);
fs.AddAccessRule(new FileSystemAccessRule(account, FileSystemRights.FullControl, AccessControlType.Allow));
pkFile.SetAccessControl(fs);
}
/// <summary>
/// Safely Searches a Local Certificate Store
/// </summary>
/// <param name="name"></param>
/// <param name="storeName"></param>
/// <param name="location"></param>
/// <param name="findType"></param>
/// <returns></returns>
private static X509Certificate2 SearchLocalStore(string name, StoreName storeName, StoreLocation location, X509FindType findType)
{
var store = new X509Store(storeName, location);
try
{
store.Open(OpenFlags.OpenExistingOnly | OpenFlags.ReadOnly);
return store.Certificates.Find(findType, name, false)
.OfType<X509Certificate2>()
.FirstOrDefault();
}
finally
{
store.Close();
}
}
/// <summary>
/// Searches a Remote Store By Thumbprint
/// </summary>
/// <param name="thumbPrint"></param>
/// <param name="storeName"></param>
/// <param name="location"></param>
/// <param name="machineName"></param>
/// <param name="password"></param>
/// <returns>a X509Certificate2</returns>
private static X509Certificate2 SearchRemoteStoreByThumbprint(string thumbPrint, StoreName storeName, StoreLocation location, string machineName, string password = null)
{
var safeHandle = IntPtr.Zero;
try
{
safeHandle = GetRemoteCertificateStore(storeName, location, machineName, safeHandle);
if (safeHandle != IntPtr.Zero)
{
var currentCertContext = IntPtr.Zero;
do
{
currentCertContext = SafeNativeMethods.CertEnumCertificatesInStore(safeHandle,
currentCertContext);
if (currentCertContext == IntPtr.Zero)
{
continue;
}
if (string.IsNullOrEmpty(password))
{
var cert = new X509Certificate2(currentCertContext);
if (string.Equals(cert.Thumbprint, thumbPrint, StringComparison.InvariantCultureIgnoreCase))
{
return cert;
}
}
else
{
var store = new X509Store("temp");
store.Open(OpenFlags.ReadWrite);
store.Add(new X509Certificate2(currentCertContext));
foreach (var certificate in store.Certificates)
{
if (string.Equals(certificate.Thumbprint, thumbPrint, StringComparison.InvariantCultureIgnoreCase))
{
return certificate;
}
}
}
} while (currentCertContext != (IntPtr)0);
}
return null;
}
finally
{
if (safeHandle != IntPtr.Zero)
{
SafeNativeMethods.CertCloseStore(safeHandle, 0);
}
}
}
/// <summary>
/// Gets a Remote Certificate Store
/// </summary>
/// <param name="storeName"></param>
/// <param name="location"></param>
/// <param name="machineName"></param>
/// <param name="safeHandle"></param>
/// <returns>An Integer Pointer (IntPtr)</returns>
/// <exception cref="System.ArgumentNullException"></exception>
/// <exception cref="System.FormatException"></exception>
// ReSharper disable once RedundantAssignment
private static IntPtr GetRemoteCertificateStore(StoreName storeName, StoreLocation location, string machineName, IntPtr safeHandle)
{
if (location == StoreLocation.CurrentUser)
{
safeHandle = SafeNativeMethods.CertOpenStore(CertStoreProvSystem, 0, 0,
CertSystemStoreCurrentUser, $@"\\{machineName}\{storeName}");
}
else
{
safeHandle = SafeNativeMethods.CertOpenStore(CertStoreProvSystem, 0, 0,
CertSystemStoreLocalMachine, $@"\\{machineName}\{storeName}");
}
return safeHandle;
}
/// <summary>
/// Finds the Keyfile Location on Disk
/// </summary>
/// <param name="keyFileName"></param>
/// <returns></returns>
/// <exception cref="System.ArgumentNullException"></exception>
/// <exception cref="System.ArgumentException"></exception>
/// <exception cref="System.IO.DirectoryNotFoundException"></exception>
/// <exception cref="System.IO.PathTooLongException"></exception>
/// <exception cref="System.IO.IOException"></exception>
/// <exception cref="System.OverflowException"></exception>
/// <exception cref="System.PlatformNotSupportedException"></exception>
/// <exception cref="System.UnauthorizedAccessException"></exception>
public static string FindKeyLocation(string keyFileName)
{
var commonAppDataPath = Environment.GetFolderPath(Environment.SpecialFolder.CommonApplicationData);
var machineKeyPath = commonAppDataPath + @"\Microsoft\Crypto\RSA\MachineKeys";
var fileList = Directory.EnumerateFiles(machineKeyPath, keyFileName);
if (fileList.Any())
{
return machineKeyPath;
}
var appDataPath = Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData);
var rsaKeyPath = appDataPath + @"\Microsoft\Crypto\RSA\";
var directoryArray = Directory.GetDirectories(rsaKeyPath);
if (directoryArray.Length > 0)
{
foreach (var directory in directoryArray)
{
var matchingDirectories = Directory.EnumerateFiles(directory, keyFileName);
if (matchingDirectories.Any())
{
return directory;
}
}
}
var cryptoKeyPath = appDataPath + @"\Microsoft\Crypto\Keys\";
var keyPath = Path.Combine(cryptoKeyPath, keyFileName);
if(File.Exists(keyPath))
{
return cryptoKeyPath;
}
return string.Empty;
}
}
}