From 58f2c9135df3e665d2a1cf4cf3c09bad781cb1af Mon Sep 17 00:00:00 2001 From: JoeKF Date: Thu, 10 Oct 2024 10:32:43 -0400 Subject: [PATCH 01/19] work on enrollment --- .../Client/HashicorpVaultClient.cs | 4 +++- .../HashicorpVaultCAConnector.cs | 22 +++++++++++-------- 2 files changed, 16 insertions(+), 10 deletions(-) diff --git a/hashicorp-vault-cagateway/Client/HashicorpVaultClient.cs b/hashicorp-vault-cagateway/Client/HashicorpVaultClient.cs index 5f629be..04d67b5 100644 --- a/hashicorp-vault-cagateway/Client/HashicorpVaultClient.cs +++ b/hashicorp-vault-cagateway/Client/HashicorpVaultClient.cs @@ -19,7 +19,9 @@ public HashicorpVaultClient(string vaultServerUri) { }) } - public Secret SignCSR(string csr, string subject, Dictionary san, string roleName) + + + public async Secret SignCSR(string csr, string subject, Dictionary san, string roleName) { var reqOptions = new SignCertificatesRequestOptions(); diff --git a/hashicorp-vault-cagateway/HashicorpVaultCAConnector.cs b/hashicorp-vault-cagateway/HashicorpVaultCAConnector.cs index 40ea92b..af9476b 100644 --- a/hashicorp-vault-cagateway/HashicorpVaultCAConnector.cs +++ b/hashicorp-vault-cagateway/HashicorpVaultCAConnector.cs @@ -50,8 +50,8 @@ public override void Initialize(ICAConnectorConfigProvider configProvider) string rawConfig = JsonConvert.SerializeObject(configProvider.CAConnectionData); logger.LogTrace($"serialized config: {rawConfig}"); Config = JsonConvert.DeserializeObject(rawConfig); - _client = new HashicorpVaultClient(); - GatewayCertificate + _client = new HashicorpVaultClient(Config.Host); + logger.MethodExit(LogLevel.Trace); } @@ -79,23 +79,27 @@ public override EnrollmentResult Enroll(ICertificateDataReader certificateDataRe var vaultRole = Config.Role; var secretEnginePath = Config.EnginePath; - var res = Client. + var res = _client.SignCSR(csr, subject, san, Config.Role); return new EnrollmentResult() { - CARequestID = response.serial_number.Replace("-", "").Replace(":", ""), + CARequestID = res.Data.SerialNumber.Replace("-", "").Replace(":", ""), Status = (int)PKIConstants.Microsoft.RequestDisposition.ISSUED, StatusMessage = $"Successfully enrolled for certificate {subject}", - Certificate = response.certificate + Certificate = res.Data.CertificateContent }; - // throw new NotImplementedException(); } - catch (Exception ex) { - + catch (Exception ex) + { + logger.LogError($"Error when performing enrollment: {ex.Message}"); + throw; + } + finally + { + logger.MethodExit(LogLevel.Trace); } - logger.MethodExit(LogLevel.Trace); } /// From 4c877a7cd64bc832fbd71d5a451298e62336c8bd Mon Sep 17 00:00:00 2001 From: JoeKF Date: Thu, 10 Oct 2024 16:29:02 -0400 Subject: [PATCH 02/19] Enrollment code complete --- .../Client/HashicorpVaultClient.cs | 102 ++++++++++++++++-- .../HashicorpVaultCAConfig.cs | 18 +++- .../HashicorpVaultCAConnector.cs | 64 +++++++++-- 3 files changed, 161 insertions(+), 23 deletions(-) diff --git a/hashicorp-vault-cagateway/Client/HashicorpVaultClient.cs b/hashicorp-vault-cagateway/Client/HashicorpVaultClient.cs index 04d67b5..7ba5375 100644 --- a/hashicorp-vault-cagateway/Client/HashicorpVaultClient.cs +++ b/hashicorp-vault-cagateway/Client/HashicorpVaultClient.cs @@ -1,10 +1,17 @@ -using Org.BouncyCastle.Asn1.X509; +using CAProxy.AnyGateway.Models; +using Keyfactor.Logging; +using Microsoft.Extensions.Logging; +using Org.BouncyCastle.Asn1.X509; using System; using System.Collections.Generic; using System.Linq; +using System.Security.Cryptography.X509Certificates; using System.Text; using System.Threading.Tasks; using VaultSharp; +using VaultSharp.V1.AuthMethods; +using VaultSharp.V1.AuthMethods.Cert; +using VaultSharp.V1.AuthMethods.Token; using VaultSharp.V1.Commons; using VaultSharp.V1.SecretsEngines.PKI; @@ -13,15 +20,82 @@ namespace Keyfactor.Extensions.AnyGateway.HashicorpVault.Client public class HashicorpVaultClient { private VaultClient _vaultClient { get; set; } + private static readonly ILogger logger = Logging.LogHandler.GetClassLogger(); + private string _mountPoint { get; set; } - public HashicorpVaultClient(string vaultServerUri) { - _vaultClient = new VaultClient(new VaultClientSettings(vaultServerUri, n) { - }) - + public HashicorpVaultClient(HashicorpVaultCAConfig config) + { + logger.MethodEntry(); + X509Certificate2 clientCert = null; + IAuthMethodInfo authMethod = null; + _mountPoint = config.MountPoint; + if (config.Token != null) + { + logger.LogTrace("Token is present in config and will be used for authentication to Vault"); + authMethod = new TokenAuthMethodInfo(config.Token); + } + else + { + logger.LogTrace("No Token is present in the config. Checking for certificate info for authentication"); + + if (!string.IsNullOrEmpty(config.ClientCertificate?.Thumbprint)) + { + logger.LogTrace("Thumbprint is present in config. Retreiving cert for authentication from store"); + //Cert auth, cert in Windows store + string thumbprint = config.ClientCertificate.Thumbprint; + + if (!Enum.TryParse(config.ClientCertificate.StoreName, out StoreName sn) || !Enum.TryParse(config.ClientCertificate.StoreLocation, out StoreLocation sl)) + { + logger.LogError($"Both store name and store location values are needed to retreive the cert from the store. Values from configuration - StoreName: {config.ClientCertificate.StoreName}, StoreLocation: {config.ClientCertificate.StoreLocation}"); + throw new MissingFieldException("Unable to find client authentication certificate"); + } + + X509Certificate2Collection foundCerts; + using (X509Store currentStore = new X509Store(sn, sl)) + { + logger.LogTrace($"Search for client auth certificates with Thumbprint {thumbprint} in the {sn}{sl} certificate store"); + + currentStore.Open(OpenFlags.ReadOnly | OpenFlags.OpenExistingOnly); + foundCerts = currentStore.Certificates.Find(X509FindType.FindByThumbprint, thumbprint, true); + logger.LogTrace($"Found {foundCerts.Count} certificates in the {currentStore.Name} store"); + currentStore.Close(); + } + if (foundCerts.Count > 1) + { + throw new Exception($"Multiple certificates with Thumprint {thumbprint} found in the {sn}{sl} certificate store"); + } + if (foundCerts.Count > 0) + clientCert = foundCerts[0]; + } + else if (!string.IsNullOrEmpty(config.ClientCertificate.CertificatePath)) + { + logger.LogTrace($"CertificatePath is present in config. Will attempt to read cert from {config.ClientCertificate.CertificatePath}"); + //Cert auth, cert in pfx file + try + { + X509Certificate2 cert = new X509Certificate2(config.ClientCertificate.CertificatePath, config.ClientCertificate.CertificatePassword); + clientCert = cert; + } + catch (Exception ex) + { + throw new Exception($"Unable to open the client certificate file with the given password. Error: {ex.Message}"); + } + } + if (clientCert != null) + { + authMethod = new CertAuthMethodInfo(clientCert); + } + } + + if (authMethod == null) throw new MissingFieldException($"Neither token or certificate data are present in the configuration. Unable to configure Vault Authentication."); + + _vaultClient = new VaultClient(new VaultClientSettings(config.Host, authMethod)); + + logger.MethodExit(); } - public async Secret SignCSR(string csr, string subject, Dictionary san, string roleName) + public async Task> SignCSR(string csr, string subject, Dictionary san, string roleName) { var reqOptions = new SignCertificatesRequestOptions(); @@ -58,11 +132,21 @@ public async Secret SignCSR(string csr, string subject, D reqOptions.CommonName = commonName; reqOptions.SubjectAlternativeNames = string.Join(",", dnsNames); - //reqOptions.IPSubjectAlternativeNames - reqOptions.TimeToLive = + //reqOptions.IPSubjectAlternativeNames + try + { + var response = await _vaultClient.V1.Secrets.PKI.SignCertificateAsync(roleName, reqOptions, pkiBackendMountPoint: _mountPoint); + return response; + } + catch (Exception ex) + { + logger.LogError($"There was an error when submitting the request to Vault: {ex.Message}"); + logger.LogTrace($"provided parameters -- vaultUri: {_vaultClient.Settings.VaultServerUriWithPort}, mountPoint: {_mountPoint}, roleName: {roleName}, commonName: {reqOptions.CommonName}, SANs: {reqOptions.SubjectAlternativeNames}"); + throw; + } } - + // example using vaultsharp: diff --git a/hashicorp-vault-cagateway/HashicorpVaultCAConfig.cs b/hashicorp-vault-cagateway/HashicorpVaultCAConfig.cs index 2e4d89a..0c05732 100644 --- a/hashicorp-vault-cagateway/HashicorpVaultCAConfig.cs +++ b/hashicorp-vault-cagateway/HashicorpVaultCAConfig.cs @@ -14,13 +14,25 @@ public class HashicorpVaultCAConfig [JsonProperty("Host")] public string Host { get; set; } - [JsonProperty("EnginePath")] - public string EnginePath { get; set; } + [JsonProperty("MountPoint")] + public string MountPoint { get; set; } [JsonProperty("Role")] public string Role { get; set; } [JsonProperty("Token")] public string Token { get; set; } - } + + [JsonProperty("ClientCertificate")] + public AuthCert ClientCertificate { get; set; } + } + + public class AuthCert + { + public string StoreName { get; set; } + public string StoreLocation { get; set; } + public string Thumbprint { get; set; } + public string CertificatePath { get; set; } + public string CertificatePassword { get; set; } + } } \ No newline at end of file diff --git a/hashicorp-vault-cagateway/HashicorpVaultCAConnector.cs b/hashicorp-vault-cagateway/HashicorpVaultCAConnector.cs index af9476b..fe01fa6 100644 --- a/hashicorp-vault-cagateway/HashicorpVaultCAConnector.cs +++ b/hashicorp-vault-cagateway/HashicorpVaultCAConnector.cs @@ -1,5 +1,4 @@ using CAProxy.AnyGateway; -using CAProxy.AnyGateway.Configuration; using CAProxy.AnyGateway.Interfaces; using CAProxy.AnyGateway.Models; using CAProxy.AnyGateway.Models.Configuration; @@ -14,12 +13,8 @@ using System.Collections.Concurrent; using System.Collections.Generic; using System.Linq; -using System.Net; -using System.Runtime.ConstrainedExecution; -using System.Text; +using System.Security.Cryptography.X509Certificates; using System.Threading; -using System.Threading.Tasks; -using static CAProxy.AnyGateway.Constants; namespace Keyfactor.Extensions.AnyGateway.HashicorpVault { @@ -44,13 +39,61 @@ public class HashicorpVaultCAConnector : BaseCAConnector, ICAConnectorConfigInfo /// Initialize the /// /// The config provider contains information required to connect to the CA. - public override void Initialize(ICAConnectorConfigProvider configProvider) + public override async void Initialize(ICAConnectorConfigProvider configProvider) { logger.MethodEntry(LogLevel.Trace); string rawConfig = JsonConvert.SerializeObject(configProvider.CAConnectionData); logger.LogTrace($"serialized config: {rawConfig}"); Config = JsonConvert.DeserializeObject(rawConfig); - _client = new HashicorpVaultClient(Config.Host); + + X509Certificate2 clientCert = null; + + if (!string.IsNullOrEmpty(Config.ClientCertificate?.Thumbprint)) + { + //Cert auth, cert in Windows store + StoreName sn; + StoreLocation sl; + string thumbprint = Config.ClientCertificate.Thumbprint; + + if (string.IsNullOrEmpty(thumbprint) || + !Enum.TryParse(Config.ClientCertificate.StoreName, out sn) || + !Enum.TryParse(Config.ClientCertificate.StoreLocation, out sl)) + { + throw new Exception("Unable to find client authentication certificate"); + } + + X509Certificate2Collection foundCerts; + using (X509Store currentStore = new X509Store(sn, sl)) + { + logger.LogTrace($"Search for client auth certificates with Thumbprint {thumbprint} in the {sn}{sl} certificate store"); + + currentStore.Open(OpenFlags.ReadOnly | OpenFlags.OpenExistingOnly); + foundCerts = currentStore.Certificates.Find(X509FindType.FindByThumbprint, thumbprint, true); + logger.LogTrace($"Found {foundCerts.Count} certificates in the {currentStore.Name} store"); + currentStore.Close(); + } + if (foundCerts.Count > 1) + { + throw new Exception($"Multiple certificates with Thumbprint {thumbprint} found in the {sn}{sl} certificate store"); + } + if (foundCerts.Count > 0) + clientCert = foundCerts[0]; + } + else if (!string.IsNullOrEmpty(Config.ClientCertificate.CertificatePath)) + { + //Cert auth, cert in pfx file + try + { + X509Certificate2 cert = new X509Certificate2(Config.ClientCertificate.CertificatePath, Config.ClientCertificate.CertificatePassword); + clientCert = cert; + } + catch (Exception ex) + { + throw new Exception($"Unable to open the client certificate file with the given password. Error: {ex.Message}"); + } + } + + _client = new HashicorpVaultClient(Config); logger.MethodExit(LogLevel.Trace); } @@ -77,10 +120,9 @@ public override EnrollmentResult Enroll(ICertificateDataReader certificateDataRe Logger.Trace($"Common Name: {commonName}"); var vaultHost = Config.Host; var vaultRole = Config.Role; - var secretEnginePath = Config.EnginePath; - - var res = _client.SignCSR(csr, subject, san, Config.Role); + var secretEnginePath = Config.MountPoint; + var res = _client.SignCSR(csr, subject, san, Config.Role).Result; return new EnrollmentResult() { From 2486556527912c1ff1269e6d05a997b97b3ee5d9 Mon Sep 17 00:00:00 2001 From: Joseph VanWanzeele Date: Mon, 4 Nov 2024 17:02:04 -0500 Subject: [PATCH 03/19] Updated namespace, implemented revoke, getsinglerecord, and enroll. --- .../APIProxy/HashicorpVaultBaseCall.cs | 10 +- .../Client/HashicorpVaultClient.cs | 81 ++++- hashicorp-vault-cagateway/Constants.cs | 33 +- .../HashicorpVaultCAConfig.cs | 25 +- .../HashicorpVaultCAConnector.cs | 324 +++++++++++------- .../HashicorpVaultCATemplateConfig.cs | 19 + .../Properties/AssemblyInfo.cs | 36 -- .../hashicorp-vault-cagateway.csproj | 128 ------- .../hashicorp-vault-caplugin.csproj | 21 ++ ...ateway.sln => hashicorp-vault-caplugin.sln | 6 +- readme_source.md | 4 +- 11 files changed, 354 insertions(+), 333 deletions(-) create mode 100644 hashicorp-vault-cagateway/HashicorpVaultCATemplateConfig.cs delete mode 100644 hashicorp-vault-cagateway/Properties/AssemblyInfo.cs delete mode 100644 hashicorp-vault-cagateway/hashicorp-vault-cagateway.csproj create mode 100644 hashicorp-vault-cagateway/hashicorp-vault-caplugin.csproj rename hashicorp-vault-cagateway.sln => hashicorp-vault-caplugin.sln (87%) diff --git a/hashicorp-vault-cagateway/APIProxy/HashicorpVaultBaseCall.cs b/hashicorp-vault-cagateway/APIProxy/HashicorpVaultBaseCall.cs index c9383c7..a82fc69 100644 --- a/hashicorp-vault-cagateway/APIProxy/HashicorpVaultBaseCall.cs +++ b/hashicorp-vault-cagateway/APIProxy/HashicorpVaultBaseCall.cs @@ -1,14 +1,8 @@ using Newtonsoft.Json; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - -namespace Keyfactor.Extensions.AnyGateway.HashicorpVault.APIProxy +namespace Keyfactor.Extensions.CAPlugin.HashicorpVault { - public abstract class ProductNameBaseRequest + public abstract class ProductNameBaseRequest { [JsonIgnore] public string Resource { get; internal set; } diff --git a/hashicorp-vault-cagateway/Client/HashicorpVaultClient.cs b/hashicorp-vault-cagateway/Client/HashicorpVaultClient.cs index 7ba5375..6b72c08 100644 --- a/hashicorp-vault-cagateway/Client/HashicorpVaultClient.cs +++ b/hashicorp-vault-cagateway/Client/HashicorpVaultClient.cs @@ -1,12 +1,10 @@ -using CAProxy.AnyGateway.Models; -using Keyfactor.Logging; +using Keyfactor.Logging; using Microsoft.Extensions.Logging; using Org.BouncyCastle.Asn1.X509; using System; using System.Collections.Generic; using System.Linq; using System.Security.Cryptography.X509Certificates; -using System.Text; using System.Threading.Tasks; using VaultSharp; using VaultSharp.V1.AuthMethods; @@ -15,12 +13,12 @@ using VaultSharp.V1.Commons; using VaultSharp.V1.SecretsEngines.PKI; -namespace Keyfactor.Extensions.AnyGateway.HashicorpVault.Client +namespace Keyfactor.Extensions.CAPlugin.HashicorpVault { public class HashicorpVaultClient { private VaultClient _vaultClient { get; set; } - private static readonly ILogger logger = Logging.LogHandler.GetClassLogger(); + private static readonly ILogger logger = LogHandler.GetClassLogger(); private string _mountPoint { get; set; } public HashicorpVaultClient(HashicorpVaultCAConfig config) @@ -28,7 +26,7 @@ public HashicorpVaultClient(HashicorpVaultCAConfig config) logger.MethodEntry(); X509Certificate2 clientCert = null; IAuthMethodInfo authMethod = null; - _mountPoint = config.MountPoint; + _mountPoint = config.MountPoint ?? "pki"; if (config.Token != null) { logger.LogTrace("Token is present in config and will be used for authentication to Vault"); @@ -97,6 +95,7 @@ public HashicorpVaultClient(HashicorpVaultCAConfig config) public async Task> SignCSR(string csr, string subject, Dictionary san, string roleName) { + logger.MethodEntry(); var reqOptions = new SignCertificatesRequestOptions(); @@ -144,14 +143,76 @@ public async Task> SignCSR(string csr, string subj logger.LogTrace($"provided parameters -- vaultUri: {_vaultClient.Settings.VaultServerUriWithPort}, mountPoint: {_mountPoint}, roleName: {roleName}, commonName: {reqOptions.CommonName}, SANs: {reqOptions.SubjectAlternativeNames}"); throw; } + finally + { + logger.MethodExit(); + } } + public async Task> GetCertificate(string certSerial) + { + logger.MethodEntry(); + try + { + logger.LogTrace($"requesting the certificate with serial number: {certSerial}"); + var cert = await _vaultClient.V1.Secrets.PKI.ReadCertificateAsync(certSerial, _mountPoint); + logger.LogTrace($"successfully received a response for certificae with serial number: {cert.Data.SerialNumber}"); + return cert; + } + catch (Exception ex) + { + logger.LogError($"an error occurred attempting to retrieve certificate: {ex.Message}"); + throw; + } + finally + { + logger.MethodExit(); + } + } + + public async Task RevokeCertificate(string serial) + { + logger.MethodEntry(); + try + { + logger.LogTrace($"making request to revoke cert with serial: {serial}"); + var response = await _vaultClient.V1.Secrets.PKI.RevokeCertificateAsync(serial, _mountPoint); + logger.LogTrace($"successfully revoked cert with serial {serial}, revocation time: {response.Data.RevocationTime}"); + } + catch (Exception ex) + { + logger.LogError($"an error occurred when attempting to revoke the certificate: {ex.Message}"); + throw; + } + finally { logger.MethodExit(); } + } - // example using vaultsharp: - // var signCertificateRequestOptions = new SignCertificateRequestOptions { // initialize }; - // Secret certSecret = await vaultClient.V1.Secrets.PKI.SignCertificateAsync(pkiRoleName, signCertificateRequestOptions); - // string certificateContent = certSecret.Data.CertificateContent; + public async Task PingServer() + { + logger.MethodEntry(); + logger.LogTrace($"performing a system health check request to Vault"); + try + { + var res = await _vaultClient.V1.System.GetHealthStatusAsync(); + logger.LogTrace($"-- Got a response --"); + logger.LogTrace($"Vault version : {res.Version}"); + logger.LogTrace($"enterprise instance : {res.Enterprise}"); + logger.LogTrace($"initialized : {res.Initialized}"); + logger.LogTrace($"sealed : {res.Sealed}"); + logger.LogTrace($"server time UTC: {res.ServerTimeUtcUnixTimestamp}"); + return true; + } + catch (Exception ex) + { + logger.LogError($"Vault healthcheck failed with error: {ex.Message}"); + throw; + } + finally + { + logger.MethodExit(); + } + } } } \ No newline at end of file diff --git a/hashicorp-vault-cagateway/Constants.cs b/hashicorp-vault-cagateway/Constants.cs index 2eebf69..847a130 100644 --- a/hashicorp-vault-cagateway/Constants.cs +++ b/hashicorp-vault-cagateway/Constants.cs @@ -1,13 +1,24 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - -namespace Keyfactor.Extensions.AnyGateway.HashicorpVault +namespace Keyfactor.Extensions.CAPlugin.HashicorpVault { - public class Constants - { - //Define any constants needed here (mostly field names for config parameters) - } + public static class Constants + { + //Define any constants needed here (mostly field names for config parameters) + public static class CAConfig + { + public const string HOST = "Host"; + public const string MOUNTPOINT = "MountPoint"; + public const string TOKEN = "Token"; + public const string CLIENTCERT = "ClientCertificate"; + public const string NAMESPACE = "Namespace"; + public const string ENABLED = "Enabled"; + } + + public static class TemplateConfig + { + public const string ROLENAME = "RoleName"; + public const string TOKEN = "Token"; + public const string CLIENTCERT = "ClientCertificate"; + public const string NAMESPACE = "Namespace"; + } + } } \ No newline at end of file diff --git a/hashicorp-vault-cagateway/HashicorpVaultCAConfig.cs b/hashicorp-vault-cagateway/HashicorpVaultCAConfig.cs index 0c05732..5e64f0f 100644 --- a/hashicorp-vault-cagateway/HashicorpVaultCAConfig.cs +++ b/hashicorp-vault-cagateway/HashicorpVaultCAConfig.cs @@ -7,24 +7,27 @@ using Newtonsoft.Json; -namespace Keyfactor.Extensions.AnyGateway.HashicorpVault +namespace Keyfactor.Extensions.CAPlugin.HashicorpVault { - public class HashicorpVaultCAConfig - { - [JsonProperty("Host")] - public string Host { get; set; } + public class HashicorpVaultCAConfig + { + [JsonProperty("Host")] + public string Host { get; set; } - [JsonProperty("MountPoint")] - public string MountPoint { get; set; } + [JsonProperty("MountPoint")] + public string MountPoint { get; set; } - [JsonProperty("Role")] - public string Role { get; set; } + [JsonProperty("Token")] + public string Token { get; set; } - [JsonProperty("Token")] - public string Token { get; set; } + [JsonProperty("Namespace")] + public string Namespace { get; set; } [JsonProperty("ClientCertificate")] public AuthCert ClientCertificate { get; set; } + + [JsonProperty("Enabled")] + public bool Enabled { get; set; } } public class AuthCert diff --git a/hashicorp-vault-cagateway/HashicorpVaultCAConnector.cs b/hashicorp-vault-cagateway/HashicorpVaultCAConnector.cs index fe01fa6..2709111 100644 --- a/hashicorp-vault-cagateway/HashicorpVaultCAConnector.cs +++ b/hashicorp-vault-cagateway/HashicorpVaultCAConnector.cs @@ -1,100 +1,41 @@ -using CAProxy.AnyGateway; -using CAProxy.AnyGateway.Interfaces; -using CAProxy.AnyGateway.Models; -using CAProxy.AnyGateway.Models.Configuration; -using CAProxy.Common; -using CSS.Common.Logging; -using CSS.PKI; -using Keyfactor.Extensions.AnyGateway.HashicorpVault.Client; +using Keyfactor.AnyGateway.Extensions; using Keyfactor.Logging; +using Keyfactor.PKI.Enums.EJBCA; using Microsoft.Extensions.Logging; using Newtonsoft.Json; using System; using System.Collections.Concurrent; using System.Collections.Generic; using System.Linq; -using System.Security.Cryptography.X509Certificates; +using System.Runtime.CompilerServices; using System.Threading; +using System.Threading.Tasks; -namespace Keyfactor.Extensions.AnyGateway.HashicorpVault +namespace Keyfactor.Extensions.CAPlugin.HashicorpVault { - public class HashicorpVaultCAConnector : BaseCAConnector, ICAConnectorConfigInfoProvider + public class HashicorpVaultCAConnector : IAnyCAPlugin { - #region Fields and Constructors - - private static readonly ILogger logger = Logging.LogHandler.GetClassLogger(); - - /// - /// Provides configuration information for the - /// + private readonly ILogger logger; private HashicorpVaultCAConfig Config { get; set; } private HashicorpVaultClient _client { get; set; } - //Define any additional private fields here + private ICertificateDataReader _certificateDataReader; - #endregion Fields and Constructors - - #region ICAConnector Methods + public HashicorpVaultCAConnector() + { + logger = Logging.LogHandler.GetClassLogger(); + } /// /// Initialize the /// /// The config provider contains information required to connect to the CA. - public override async void Initialize(ICAConnectorConfigProvider configProvider) + public void Initialize(IAnyCAPluginConfigProvider configProvider, ICertificateDataReader certificateDataReader) { logger.MethodEntry(LogLevel.Trace); string rawConfig = JsonConvert.SerializeObject(configProvider.CAConnectionData); logger.LogTrace($"serialized config: {rawConfig}"); Config = JsonConvert.DeserializeObject(rawConfig); - - X509Certificate2 clientCert = null; - - if (!string.IsNullOrEmpty(Config.ClientCertificate?.Thumbprint)) - { - //Cert auth, cert in Windows store - StoreName sn; - StoreLocation sl; - string thumbprint = Config.ClientCertificate.Thumbprint; - - if (string.IsNullOrEmpty(thumbprint) || - !Enum.TryParse(Config.ClientCertificate.StoreName, out sn) || - !Enum.TryParse(Config.ClientCertificate.StoreLocation, out sl)) - { - throw new Exception("Unable to find client authentication certificate"); - } - - X509Certificate2Collection foundCerts; - using (X509Store currentStore = new X509Store(sn, sl)) - { - logger.LogTrace($"Search for client auth certificates with Thumbprint {thumbprint} in the {sn}{sl} certificate store"); - - currentStore.Open(OpenFlags.ReadOnly | OpenFlags.OpenExistingOnly); - foundCerts = currentStore.Certificates.Find(X509FindType.FindByThumbprint, thumbprint, true); - logger.LogTrace($"Found {foundCerts.Count} certificates in the {currentStore.Name} store"); - currentStore.Close(); - } - if (foundCerts.Count > 1) - { - throw new Exception($"Multiple certificates with Thumbprint {thumbprint} found in the {sn}{sl} certificate store"); - } - if (foundCerts.Count > 0) - clientCert = foundCerts[0]; - } - else if (!string.IsNullOrEmpty(Config.ClientCertificate.CertificatePath)) - { - //Cert auth, cert in pfx file - try - { - X509Certificate2 cert = new X509Certificate2(Config.ClientCertificate.CertificatePath, Config.ClientCertificate.CertificatePassword); - clientCert = cert; - } - catch (Exception ex) - { - throw new Exception($"Unable to open the client certificate file with the given password. Error: {ex.Message}"); - } - } - _client = new HashicorpVaultClient(Config); - logger.MethodExit(LogLevel.Trace); } @@ -109,25 +50,25 @@ public override async void Initialize(ICAConnectorConfigProvider configProvider) /// The format of the request. /// The type of the enrollment, i.e. new, renew, or reissue. /// - public override EnrollmentResult Enroll(ICertificateDataReader certificateDataReader, string csr, string subject, Dictionary san, EnrollmentProductInfo productInfo, PKIConstants.X509.RequestFormat requestFormat, RequestUtilities.EnrollmentType enrollmentType) + public async Task Enroll(string csr, string subject, Dictionary san, EnrollmentProductInfo productInfo, RequestFormat requestFormat, EnrollmentType enrollmentType) { logger.MethodEntry(LogLevel.Trace); - Logger.Info($"Begin {enrollmentType} enrollment for {subject}"); + logger.LogInformation($"Begin {enrollmentType} enrollment for {subject}"); try { - Logger.Debug("Parse subject for Common Name"); + logger.LogDebug("Parse subject for Common Name"); string commonName = ParseSubject(subject, "CN="); - Logger.Trace($"Common Name: {commonName}"); - var vaultHost = Config.Host; - var vaultRole = Config.Role; - var secretEnginePath = Config.MountPoint; + logger.LogTrace($"Common Name: {commonName}"); + //var vaultHost = Config.Host; + var vaultRole = productInfo.ProductParameters[Constants.TemplateConfig.ROLENAME]; + //var secretEnginePath = Config.MountPoint; - var res = _client.SignCSR(csr, subject, san, Config.Role).Result; + var res = await _client.SignCSR(csr, subject, san, vaultRole); return new EnrollmentResult() { - CARequestID = res.Data.SerialNumber.Replace("-", "").Replace(":", ""), - Status = (int)PKIConstants.Microsoft.RequestDisposition.ISSUED, + CARequestID = GetTrackingIdFromSerial(res.Data.SerialNumber), + Status = (int)EndEntityStatus.NEW, StatusMessage = $"Successfully enrolled for certificate {subject}", Certificate = res.Data.CertificateContent }; @@ -140,7 +81,7 @@ public override EnrollmentResult Enroll(ICertificateDataReader certificateDataRe } finally { - logger.MethodExit(LogLevel.Trace); + logger.MethodExit(); } } @@ -149,20 +90,50 @@ public override EnrollmentResult Enroll(ICertificateDataReader certificateDataRe /// /// The CA request ID for the certificate. /// - public override CAConnectorCertificate GetSingleRecord(string caRequestID) + public async Task GetSingleRecord(string caRequestID) { - // example using vaultsharp: - // var cert = await vaultClient.V1.Secrets.PKI.ReadCertificateAsync("17:67:16:b0:b9:45:58:c0:3a:29:e3:cb:d6:98:33:7a:a6:3b:66:c1", mountpoint); + logger.MethodEntry(); - throw new NotImplementedException(); + logger.LogTrace($"converting caRequestId {caRequestID} into a Vault style certificate serial number"); + var formattedSerial = GetSerialFromTrackingId(caRequestID); + logger.LogTrace($"converted serial number: {formattedSerial}"); + + try + { + var cert = await _client.GetCertificate(formattedSerial); + + var result = new AnyCAPluginCertificate + { + CARequestID = caRequestID, + Certificate = cert.Data.CertificateContent, + //TODO: get status. Not available in this version of VaultSharp. Pending issue https://github.com/rajanadar/VaultSharp/issues/366 + }; + + return result; + } + catch (Exception ex) + { + logger.LogError($"There was an error retrieving the certificate: {ex.Message}"); + throw; + } } /// /// Attempts to reach the CA over the network. /// - public override void Ping() + public async Task Ping() { - throw new NotImplementedException(); + logger.MethodEntry(); + logger.LogTrace("Attempting ping of Vault endpoint"); + try + { + var result = await _client.PingServer(); + } + catch (Exception ex) + { + logger.LogError($"Ping attempt failed with error: {ex.Message}"); + throw; + } } /// @@ -172,12 +143,24 @@ public override void Ping() /// The hex-encoded serial number. /// The revocation reason. /// - public override int Revoke(string caRequestID, string hexSerialNumber, uint revocationReason) + public async Task Revoke(string caRequestID, string hexSerialNumber, uint revocationReason) { - // example using vaultsharp: - // Secret revoke = await vaultClient.V1.Secrets.PKI.RevokeCertificateAsync(serialNumber); - - throw new NotImplementedException(); + logger.MethodEntry(); + try + { + var serial = GetSerialFromTrackingId(caRequestID); + await _client.RevokeCertificate(serial); + return (int)EndEntityStatus.REVOKED; + } + catch (Exception ex) + { + logger.LogError($"revocation failed with error: {ex.Message}"); + throw; + } + finally + { + logger.MethodExit(); + } } /// @@ -187,7 +170,7 @@ public override int Revoke(string caRequestID, string hexSerialNumber, uint revo /// Buffer into which certificates are places from the CA. /// Information about the last CA sync. /// The cancellation token. - public override void Synchronize(ICertificateDataReader certificateDataReader, BlockingCollection blockingBuffer, CertificateAuthoritySyncInfo certificateAuthoritySyncInfo, CancellationToken cancelToken) + public async Task Synchronize(BlockingCollection blockingBuffer, DateTime? lastSync, bool fullSync, CancellationToken cancelToken) { throw new NotImplementedException(); } @@ -196,7 +179,7 @@ public override void Synchronize(ICertificateDataReader certificateDataReader, B /// Validates that the CA connection info is correct. /// /// The information used to connect to the CA. - public override void ValidateCAConnectionInfo(Dictionary connectionInfo) + public async Task ValidateCAConnectionInfo(Dictionary connectionInfo) { throw new NotImplementedException(); } @@ -206,27 +189,11 @@ public override void ValidateCAConnectionInfo(Dictionary connect /// /// The product information. /// The CA connection information. - public override void ValidateProductInfo(EnrollmentProductInfo productInfo, Dictionary connectionInfo) + public async Task ValidateProductInfo(EnrollmentProductInfo productInfo, Dictionary connectionInfo) { throw new NotImplementedException(); } - [Obsolete] - public override EnrollmentResult Enroll(string csr, string subject, Dictionary san, EnrollmentProductInfo productInfo, PKIConstants.X509.RequestFormat requestFormat, RequestUtilities.EnrollmentType enrollmentType) - { - throw new NotImplementedException(); - } - - [Obsolete] - public override void Synchronize(ICertificateDataReader certificateDataReader, BlockingCollection blockingBuffer, CertificateAuthoritySyncInfo certificateAuthoritySyncInfo, CancellationToken cancelToken, string logicalName) - { - throw new NotImplementedException(); - } - - #endregion ICAConnector Methods - - #region ICAConnectorConfigInfoProvider Methods - /// /// Returns the default CA connector section of the config file. /// @@ -253,7 +220,51 @@ public string GetProductIDComment() /// public Dictionary GetCAConnectorAnnotations() { - return new Dictionary(); + return new Dictionary() + { + [Constants.CAConfig.HOST] = new PropertyConfigInfo() + { + Comments = "The client certificate information used to authenticate with Vault (if configured to use certificate authentication). This can be either a Windows cert store name and location (e.g. 'My' and 'LocalMachine' for the Local Computer personal cert store) and thumbprint, or a PFX file and password.", + Hidden = false, + DefaultValue = "https://