diff --git a/CHANGELOG.md b/CHANGELOG.md index f4d155a..2786a1f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,6 @@ +## 1.0.2 +* bug fix: _certDataReader is now initialized in the Initialize method + ## 1.0.1 * added retrieval of roles associated with enrolled certificates via metadata for Vault Enterprise users diff --git a/hashicorp-vault-cagateway/APIProxy/CertResponse.cs b/hashicorp-vault-cagateway/APIProxy/CertResponse.cs index 1546191..79daa7c 100644 --- a/hashicorp-vault-cagateway/APIProxy/CertResponse.cs +++ b/hashicorp-vault-cagateway/APIProxy/CertResponse.cs @@ -16,7 +16,10 @@ public class CertResponse public string Certificate { get; set; } [JsonPropertyName("revocation_time_rfc3339")] - public DateTime? RevocationTime { get; set; } + public string RevocationTime { get; set; } + + [JsonPropertyName("revocation_time")] + public int? RevocationTimestamp { get; set; } [JsonPropertyName("issuer_id")] public string IssuerId { get; set; } diff --git a/hashicorp-vault-cagateway/APIProxy/WrappedResponse.cs b/hashicorp-vault-cagateway/APIProxy/WrappedResponse.cs index 44035ae..80051a2 100644 --- a/hashicorp-vault-cagateway/APIProxy/WrappedResponse.cs +++ b/hashicorp-vault-cagateway/APIProxy/WrappedResponse.cs @@ -12,6 +12,9 @@ namespace Keyfactor.Extensions.CAPlugin.HashicorpVault.APIProxy { public class WrappedResponse { + [JsonPropertyName("request_id")] + public string RequestId { get; set; } + [JsonPropertyName("lease_id")] public string LeaseId { get; set; } @@ -30,6 +33,9 @@ public class WrappedResponse [JsonPropertyName("mount_point")] public string MountPoint { get; set; } + [JsonPropertyName("mount_type")] + public string MountType { get; set; } + [JsonPropertyName("mount_running_plugin_version")] public string PluginVersion { get; set; } diff --git a/hashicorp-vault-cagateway/Client/HashicorpVaultClient.cs b/hashicorp-vault-cagateway/Client/HashicorpVaultClient.cs index 8edb068..42e2ca7 100644 --- a/hashicorp-vault-cagateway/Client/HashicorpVaultClient.cs +++ b/hashicorp-vault-cagateway/Client/HashicorpVaultClient.cs @@ -131,9 +131,15 @@ public async Task GetCertificate(string certSerial) try { - var response = await _vaultHttp.GetAsync($"cert/{certSerial}"); + var response = await _vaultHttp.GetAsync>($"cert/{certSerial}"); + logger.LogTrace($"successfully received a response for certificate with serial number: {certSerial}"); - return response; + logger.LogTrace($"--response data--"); + logger.LogTrace($"cert string: {response.Data?.Certificate}"); + logger.LogTrace($"revocation time: {response.Data?.RevocationTime}"); + + + return response.Data; } catch (Exception ex) { @@ -152,9 +158,9 @@ public async Task RevokeCertificate(string serial) logger.LogTrace($"making request to revoke cert with serial: {serial}"); try { - var response = await _vaultHttp.PostAsync("revoke", new RevokeRequest(serial)); - logger.LogTrace($"successfully revoked cert with serial {serial}, revocation time: {response.RevocationTime}"); - return response; + var response = await _vaultHttp.PostAsync>("revoke", new RevokeRequest(serial)); + logger.LogTrace($"successfully revoked cert with serial {serial}, revocation time: {response.Data.RevocationTime}"); + return response.Data; } catch (Exception ex) { @@ -189,7 +195,7 @@ public async Task PingServer() } /// - /// Retreives all serial numbers for issued certificates + /// Retrieves all serial numbers for issued certificates /// /// a list of the certificate serial number strings public async Task> GetAllCertSerialNumbers() @@ -199,7 +205,7 @@ public async Task> GetAllCertSerialNumbers() try { var res = await _vaultHttp.GetAsync>("certs/?list=true"); - return res.Data.Entries; + return res.Data?.Entries; } catch (Exception ex) { @@ -215,8 +221,8 @@ private async Task> GetRevokedSerialNumbers() var keys = new List(); try { - var res = await _vaultHttp.GetAsync("certs/revoked"); - keys = res.Entries; + var res = await _vaultHttp.GetAsync>("certs/revoked"); + keys = res.Data?.Entries; } catch (Exception ex) { @@ -247,7 +253,7 @@ public async Task> GetRoleNamesAsync() } /// - /// Retreives the metadata for the certificate + /// Retrieves the metadata for the certificate /// /// /// @@ -275,7 +281,7 @@ public async Task GetCertMetadata(string certSerial) } catch (Exception ex) { - logger.LogError($"an error occurred when attempting to retreive the certificate metadata: {ex.Message}"); + logger.LogError($"an error occurred when attempting to retrieve the certificate metadata: {ex.Message}"); throw; } finally { logger.MethodExit(); } @@ -317,5 +323,7 @@ private static string ConvertSerialToTrackingId(string serialNumber) return serialNumber.Replace(":", "-"); } + + } } \ No newline at end of file diff --git a/hashicorp-vault-cagateway/Client/VaultHttp.cs b/hashicorp-vault-cagateway/Client/VaultHttp.cs index fda1eb2..867d9f3 100644 --- a/hashicorp-vault-cagateway/Client/VaultHttp.cs +++ b/hashicorp-vault-cagateway/Client/VaultHttp.cs @@ -14,6 +14,7 @@ using System.Collections.Generic; using System.Text.Json; using System.Text.Json.Serialization; +using System.Threading; using System.Threading.Tasks; namespace Keyfactor.Extensions.CAPlugin.HashicorpVault.Client @@ -36,12 +37,12 @@ public VaultHttp(string host, string mountPoint, string authToken, string nameSp _serializerOptions = new() { DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingDefault, - RespectNullableAnnotations = true, PropertyNameCaseInsensitive = true, - PreferredObjectCreationHandling = JsonObjectCreationHandling.Replace, + RespectNullableAnnotations = true, + PreferredObjectCreationHandling = JsonObjectCreationHandling.Replace }; - var restClientOptions = new RestClientOptions($"{host.TrimEnd('/')}/v1") { ThrowOnAnyError = true }; + var restClientOptions = new RestClientOptions($"{host.TrimEnd('/')}/v1") { ThrowOnAnyError = true }; _restClient = new RestClient(restClientOptions, configureSerialization: s => s.UseSystemTextJson(_serializerOptions)); _mountPoint = mountPoint.TrimStart('/').TrimEnd('/'); // remove leading and trailing slashes @@ -69,19 +70,32 @@ public VaultHttp(string host, string mountPoint, string authToken, string nameSp public async Task GetAsync(string path, Dictionary parameters = null) { logger.MethodEntry(); - logger.LogTrace($"preparing to send GET request to {path} with parameters {JsonSerializer.Serialize(parameters)}"); - logger.LogTrace($"will attempt to deserialize the response into a {typeof(T)}"); + logger.LogTrace($"preparing to send GET request to {_mountPoint}/{path} with parameters {JsonSerializer.Serialize(parameters)}"); + try { var request = new RestRequest($"{_mountPoint}/{path}", Method.Get); - if (parameters != null) { request.AddJsonBody(parameters); } + if (parameters != null && parameters.Keys.Count > 0) { request.AddJsonBody(parameters); } + var response = await _restClient.ExecuteGetAsync(request); + + logger.LogTrace($"raw response: {JsonSerializer.Serialize(response)}"); + + logger.LogTrace($"response content: {response.Content}"); + + logger.LogTrace($"response status: {response.StatusCode}"); - var response = await _restClient.ExecuteGetAsync(request); - logger.LogTrace($"raw response: {response.Content}"); + logger.LogTrace($"response error msg: {response.ErrorMessage}"); response.ThrowIfError(); + if (string.IsNullOrEmpty(response.Content)) throw new Exception(response.ErrorMessage ?? "no content returned from Vault"); - return response.Data; + logger.LogTrace($"deserializing the response into a {typeof(T)}"); + + var deserialized = JsonSerializer.Deserialize(response.Content, _serializerOptions); + + logger.LogTrace($"successfully deserialized the response"); + + return deserialized; } catch (Exception ex) { @@ -108,8 +122,8 @@ public async Task PostAsync(string path, dynamic parameters = default) var request = new RestRequest(resourcePath, Method.Post); if (parameters != null) { - string serializedParams = JsonSerializer.Serialize(parameters, _serializerOptions); - logger.LogTrace($"serialized parameters (from {parameters.GetType()?.Name}): {serializedParams}"); + string serializedParams = JsonSerializer.Serialize(parameters); + logger.LogTrace($"deserialized parameters (from {parameters.GetType()?.Name}): {serializedParams}"); request.AddJsonBody(serializedParams); } @@ -127,7 +141,7 @@ public async Task PostAsync(string path, dynamic parameters = default) if (response.StatusCode == System.Net.HttpStatusCode.BadRequest) { - errorResponse = JsonSerializer.Deserialize(response.Content!); + errorResponse = JsonSerializer.Deserialize(response.Content ?? "no content"); string allErrors = "(Bad Request)"; if (errorResponse?.Errors.Count > 0) { diff --git a/hashicorp-vault-cagateway/HashicorpVaultCAConnector.cs b/hashicorp-vault-cagateway/HashicorpVaultCAConnector.cs index 597cf95..f179e7e 100644 --- a/hashicorp-vault-cagateway/HashicorpVaultCAConnector.cs +++ b/hashicorp-vault-cagateway/HashicorpVaultCAConnector.cs @@ -18,6 +18,7 @@ using System.Text.Json; using System.Threading; using System.Threading.Tasks; +using System.Reflection; namespace Keyfactor.Extensions.CAPlugin.HashicorpVault { @@ -35,7 +36,7 @@ public HashicorpVaultCAConnector() _serializerOptions = new() { - DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingDefault, + DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, RespectNullableAnnotations = true, PropertyNameCaseInsensitive = true, PreferredObjectCreationHandling = JsonObjectCreationHandling.Replace, @@ -50,10 +51,22 @@ public void Initialize(IAnyCAPluginConfigProvider configProvider, ICertificateDa { logger.MethodEntry(LogLevel.Trace); string rawConfig = JsonSerializer.Serialize(configProvider.CAConnectionData); - logger.LogTrace($"serialized config: {rawConfig}"); _caConfig = JsonSerializer.Deserialize(rawConfig); logger.MethodExit(LogLevel.Trace); _client = new HashicorpVaultClient(_caConfig); + _certificateDataReader = certificateDataReader; + + Assembly targetAssembly = typeof(HashicorpVaultCAConnector).Assembly; + + // Get the AssemblyName object + AssemblyName assemblyName = targetAssembly?.GetName(); + + // Get the Version object + Version version = assemblyName?.Version; + + logger.LogTrace($"-- {assemblyName?.Name ?? "unknown"} v{version} --"); + + logger.LogTrace($"serialized config: {rawConfig}"); } /// @@ -157,7 +170,7 @@ public async Task GetSingleRecord(string caRequestID) CARequestID = caRequestID, Certificate = cert.Certificate, Status = revoked ? (int)EndEntityStatus.REVOKED : (int)EndEntityStatus.GENERATED, - RevocationDate = cert.RevocationTime + RevocationDate = cert.RevocationTime != null ? DateTime.Parse(cert.RevocationTime.ToString(), System.Globalization.CultureInfo.InvariantCulture, System.Globalization.DateTimeStyles.AdjustToUniversal | System.Globalization.DateTimeStyles.AssumeUniversal) : null }; return result; @@ -239,7 +252,7 @@ public async Task Synchronize(BlockingCollection blockin } catch (Exception ex) { - logger.LogError($"failed to retreive serial numbers: {LogHandler.FlattenException(ex)}"); + logger.LogError($"failed to retrieve serial numbers: {LogHandler.FlattenException(ex)}"); throw; } @@ -250,25 +263,26 @@ public async Task Synchronize(BlockingCollection blockin CertResponse certFromVault = null; var dbStatus = -1; - // first, retreive the details from Vault + // first, retrieve the details from Vault try { logger.LogTrace($"Calling GetCertificate on our client, passing serial number: {certSerial}"); certFromVault = await _client.GetCertificate(certSerial); + logger.LogTrace($"got cert from vault. Cert content length: {certFromVault.Certificate?.Length}"); } catch (Exception ex) { - logger.LogError($"Failed to retreive details for certificate with serial number {certSerial} from Vault. Errors: {LogHandler.FlattenException(ex)}"); + logger.LogError($"Failed to retrieve details for certificate with serial number {certSerial} from Vault. Errors: {LogHandler.FlattenException(ex)}"); throw; } logger.LogTrace($"converting {certSerial} to database trackingId"); - var trackingId = certSerial.Replace(":", "-"); // we store with '-'; hashi stores with ':' + var trackingId = certSerial.Replace(":", "-"); // we store with '-'; hashi stores with ':' // then, check for an existing local entry try { - logger.LogTrace($"attempting to retreive status of cert with tracking id {trackingId} from the database"); + logger.LogTrace($"attempting to retrieve status of cert with tracking id {trackingId} from the database"); dbStatus = await _certificateDataReader.GetStatusByRequestID(trackingId); } catch @@ -280,37 +294,41 @@ public async Task Synchronize(BlockingCollection blockin { logger.LogTrace($"adding cert with serial {trackingId} to the database. fullsync is {fullSync}, and the certificate {(dbStatus == -1 ? "does not yet exist" : "already exists")} in the database."); - logger.LogTrace("attempting to retreive the role name (productId) from the certificate metadata, if available"); + logger.LogTrace("attempting to retrieve the role name (productId) from the certificate metadata, if available"); var metaData = new MetadataResponse(); - + try { metaData = await _client.GetCertMetadata(certSerial); } - catch (Exception) + catch (Exception ex) { - logger.LogTrace("an error occurred when attempting to retreive the metadata, continuing.."); + logger.LogTrace($"an error occurred when attempting to retrieve the metadata, continuing.. {LogHandler.FlattenException(ex)}"); } + var newCert = new AnyCAPluginCertificate { CARequestID = trackingId, Certificate = certFromVault.Certificate, - Status = certFromVault.RevocationTime != null ? (int)EndEntityStatus.REVOKED : (int)EndEntityStatus.GENERATED, - RevocationDate = certFromVault.RevocationTime, + Status = !string.IsNullOrEmpty(certFromVault.RevocationTime) ? (int)EndEntityStatus.REVOKED : (int)EndEntityStatus.GENERATED, + RevocationDate = !string.IsNullOrEmpty(certFromVault.RevocationTime) ? DateTime.Parse(certFromVault.RevocationTime) : null }; - + // if we were able to get the role name from metadata, we include it - if (!string.IsNullOrEmpty(metaData?.Role)) + if (!string.IsNullOrEmpty(metaData?.Role)) { newCert.ProductID = metaData.Role; } try { - logger.LogTrace($"writing the result."); - blockingBuffer.Add(newCert); + logger.LogTrace($"writing the result.."); + logger.LogTrace($"certificate ID: {newCert.CARequestID}"); + logger.LogTrace($"certificate contents: {newCert.Certificate}"); + logger.LogTrace($"certificate status: {newCert.Status}"); + blockingBuffer.Add(newCert, cancelToken); logger.LogTrace($"successfully added certificate to the database."); } catch (Exception ex) @@ -321,8 +339,10 @@ public async Task Synchronize(BlockingCollection blockin } else // the cert exists in the database; just update the status if necessary { - var revoked = certFromVault.RevocationTime != null; + var revoked = !string.IsNullOrEmpty(certFromVault.RevocationTime); + logger.LogTrace($"revocationTime = {certFromVault.RevocationTime} so the cert will be marked as{(revoked ? "" : " not")} revoked."); var vaultStatus = revoked ? (int)EndEntityStatus.REVOKED : (int)EndEntityStatus.GENERATED; + if (vaultStatus != dbStatus) // if there is a mismatch, we need to update { var newCert = new AnyCAPluginCertificate @@ -330,13 +350,16 @@ public async Task Synchronize(BlockingCollection blockin CARequestID = trackingId, Certificate = certFromVault.Certificate, Status = vaultStatus, - RevocationDate = certFromVault.RevocationTime + RevocationDate = !string.IsNullOrEmpty(certFromVault.RevocationTime) ? DateTime.Parse(certFromVault.RevocationTime) : null, // ProductID is not available via the API after the initial issuance. we do not want to overwrite - }; + }; + + blockingBuffer.Add(newCert); } } count++; } + blockingBuffer.CompleteAdding(); logger.LogTrace($"Completed sync of {count} certificates"); logger.MethodExit(); } @@ -346,7 +369,7 @@ public async Task Synchronize(BlockingCollection blockin /// /// The information used to connect to the CA. public async Task ValidateCAConnectionInfo(Dictionary connectionInfo) - { + { logger.MethodEntry(); logger.LogTrace(message: $"Validating CA connection info: {JsonSerializer.Serialize(connectionInfo)}"); @@ -372,7 +395,7 @@ public async Task ValidateCAConnectionInfo(Dictionary connection // make sure an authentication mechanism is defined (either certificate or token) var token = connectionInfo[Constants.CAConfig.TOKEN] as string; - + //var cert = connectionInfo[Constants.CAConfig.CLIENTCERT] as string; var cert = string.Empty; // temporary until client cert auth into vault is implemented @@ -422,12 +445,12 @@ public async Task ValidateCAConnectionInfo(Dictionary connection _client = new HashicorpVaultClient(config); - // attempt an authenticated request to retreive role names + // attempt an authenticated request to retrieve role names try { logger.LogTrace("making an authenticated request to the Vault server to verify credentials (listing role names).."); var roleNames = await _client.GetRoleNamesAsync(); - logger.LogTrace($"successfule request: received a response containing {roleNames.Count} role names"); + logger.LogTrace($"successful request: received a response containing {roleNames?.Count} role names"); } catch (Exception ex) { @@ -465,7 +488,7 @@ public Task ValidateProductInfo(EnrollmentProductInfo productInfo, Dictionary GetProductIds() try { logger.LogTrace("requesting role names from vault.."); - var roleNames = _client.GetRoleNamesAsync().Result; + var roleNames = _client.GetRoleNamesAsync().GetAwaiter().GetResult(); + if (roleNames == null) + { + throw new Exception("no role names returned, or deserialization failed."); + } logger.LogTrace($"got {roleNames.Count} role names from vault:"); foreach (var name in roleNames) { diff --git a/hashicorp-vault-cagateway/Properties/launchSettings.json b/hashicorp-vault-cagateway/Properties/launchSettings.json deleted file mode 100644 index e6a1e54..0000000 --- a/hashicorp-vault-cagateway/Properties/launchSettings.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "profiles": { - "hashicorp-vault-caplugin": { - "commandName": "Project", - "remoteDebugEnabled": true, - "authenticationMode": "None", - "nativeDebugging": true - } - } -} \ No newline at end of file diff --git a/hashicorp-vault-cagateway/hashicorp-vault-caplugin.csproj b/hashicorp-vault-cagateway/hashicorp-vault-caplugin.csproj index 95991b8..9ed4103 100644 --- a/hashicorp-vault-cagateway/hashicorp-vault-caplugin.csproj +++ b/hashicorp-vault-cagateway/hashicorp-vault-caplugin.csproj @@ -33,14 +33,19 @@ - - - - - - - - + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + @@ -48,19 +53,4 @@ Always - - - - True - True - Resources.resx - - - - - - ResXFileCodeGenerator - Resources.Designer.cs - - \ No newline at end of file diff --git a/readme_source.md b/readme_source.md index 2b54786..836a7bb 100644 --- a/readme_source.md +++ b/readme_source.md @@ -95,7 +95,7 @@ Certificates issued for the Hashicorp Vault CA from within the Keyfactor Command 1. Create an entry for each of the PKI secrets engine roles you would like to use for issuing certificates from the Hashicorp Vault CA. 1. Navigate to the "Certificate Authorities" tab and click "Edit" 1. In the "Edit CA" window, navigate to the "Templates" tab. - 1. Create an association between each of the certificate profiles we just created with the PKI secrets engine roles retreived from Vault. + 1. Create an association between each of the certificate profiles we just created with the PKI secrets engine roles retrieved from Vault. ### Configure the CA in Keyfactor Command