diff --git a/.github/workflows/keyfactor-bootstrap-workflow-v3.yml b/.github/workflows/keyfactor-bootstrap-workflow-v3.yml deleted file mode 100644 index 64919a4..0000000 --- a/.github/workflows/keyfactor-bootstrap-workflow-v3.yml +++ /dev/null @@ -1,20 +0,0 @@ -name: Keyfactor Bootstrap Workflow - -on: - workflow_dispatch: - pull_request: - types: [opened, closed, synchronize, edited, reopened] - push: - create: - branches: - - 'release-*.*' - -jobs: - call-starter-workflow: - uses: keyfactor/actions/.github/workflows/starter.yml@v3 - secrets: - token: ${{ secrets.V2BUILDTOKEN}} - APPROVE_README_PUSH: ${{ secrets.APPROVE_README_PUSH}} - gpg_key: ${{ secrets.KF_GPG_PRIVATE_KEY }} - gpg_pass: ${{ secrets.KF_GPG_PASSPHRASE }} - scan_token: ${{ secrets.SAST_TOKEN }} diff --git a/.github/workflows/keyfactor-bootstrap-workflow.yml b/.github/workflows/keyfactor-bootstrap-workflow.yml index 6d8de53..bd5f384 100644 --- a/.github/workflows/keyfactor-bootstrap-workflow.yml +++ b/.github/workflows/keyfactor-bootstrap-workflow.yml @@ -11,9 +11,17 @@ on: jobs: call-starter-workflow: - uses: keyfactor/actions/.github/workflows/starter.yml@v2 + uses: keyfactor/actions/.github/workflows/starter.yml@v4 + with: + command_token_url: ${{ vars.COMMAND_TOKEN_URL }} + command_hostname: ${{ vars.COMMAND_HOSTNAME }} + command_base_api_path: ${{ vars.COMMAND_API_PATH }} secrets: token: ${{ secrets.V2BUILDTOKEN}} - APPROVE_README_PUSH: ${{ secrets.APPROVE_README_PUSH}} gpg_key: ${{ secrets.KF_GPG_PRIVATE_KEY }} gpg_pass: ${{ secrets.KF_GPG_PASSPHRASE }} + scan_token: ${{ secrets.SAST_TOKEN }} + entra_username: ${{ secrets.DOCTOOL_ENTRA_USERNAME }} + entra_password: ${{ secrets.DOCTOOL_ENTRA_PASSWD }} + command_client_id: ${{ secrets.COMMAND_CLIENT_ID }} + command_client_secret: ${{ secrets.COMMAND_CLIENT_SECRET }} \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index e69de29..2786a1f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -0,0 +1,8 @@ +## 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 + +## 1.0.0 +* initial release \ No newline at end of file diff --git a/hashicorp-vault-cagateway/APIProxy/CertResponse.cs b/hashicorp-vault-cagateway/APIProxy/CertResponse.cs index 305ec3f..79daa7c 100644 --- a/hashicorp-vault-cagateway/APIProxy/CertResponse.cs +++ b/hashicorp-vault-cagateway/APIProxy/CertResponse.cs @@ -1,4 +1,4 @@ -// Copyright 2024 Keyfactor +// Copyright 2025 Keyfactor // Licensed under the Apache License, Version 2.0 (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.apache.org/licenses/LICENSE-2.0 // Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, @@ -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/ErrorResponse.cs b/hashicorp-vault-cagateway/APIProxy/ErrorResponse.cs index 8da94ad..340ec90 100644 --- a/hashicorp-vault-cagateway/APIProxy/ErrorResponse.cs +++ b/hashicorp-vault-cagateway/APIProxy/ErrorResponse.cs @@ -1,4 +1,4 @@ -// Copyright 2024 Keyfactor +// Copyright 2025 Keyfactor // Licensed under the Apache License, Version 2.0 (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.apache.org/licenses/LICENSE-2.0 // Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, diff --git a/hashicorp-vault-cagateway/APIProxy/KeyedList.cs b/hashicorp-vault-cagateway/APIProxy/KeyedList.cs index 9fe9ecc..ba94423 100644 --- a/hashicorp-vault-cagateway/APIProxy/KeyedList.cs +++ b/hashicorp-vault-cagateway/APIProxy/KeyedList.cs @@ -1,4 +1,4 @@ -// Copyright 2024 Keyfactor +// Copyright 2025 Keyfactor // Licensed under the Apache License, Version 2.0 (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.apache.org/licenses/LICENSE-2.0 // Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, diff --git a/hashicorp-vault-cagateway/APIProxy/MetadataResponse.cs b/hashicorp-vault-cagateway/APIProxy/MetadataResponse.cs new file mode 100644 index 0000000..42481b2 --- /dev/null +++ b/hashicorp-vault-cagateway/APIProxy/MetadataResponse.cs @@ -0,0 +1,30 @@ +// Copyright 2025 Keyfactor +// Licensed under the Apache License, Version 2.0 (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.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions +// and limitations under the License. + +using System; +using System.Text.Json.Serialization; + +namespace Keyfactor.Extensions.CAPlugin.HashicorpVault.APIProxy +{ + public class MetadataResponse + { + [JsonPropertyName("issuer_id")] + public string IssuerId { get; set; } + + [JsonPropertyName("expiration")] + public DateTime? Expiration { get; set; } + + [JsonPropertyName("cert_metadata")] + public string CertMetadata { get; set; } + + [JsonPropertyName("role")] + public string Role { get; set; } + + [JsonPropertyName("serial_number")] + public string SerialNumber { get; set; } + } +} diff --git a/hashicorp-vault-cagateway/APIProxy/RevokeRequest.cs b/hashicorp-vault-cagateway/APIProxy/RevokeRequest.cs index 7a4421a..f974359 100644 --- a/hashicorp-vault-cagateway/APIProxy/RevokeRequest.cs +++ b/hashicorp-vault-cagateway/APIProxy/RevokeRequest.cs @@ -1,4 +1,4 @@ -// Copyright 2024 Keyfactor +// Copyright 2025 Keyfactor // Licensed under the Apache License, Version 2.0 (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.apache.org/licenses/LICENSE-2.0 // Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, diff --git a/hashicorp-vault-cagateway/APIProxy/RevokeResponse.cs b/hashicorp-vault-cagateway/APIProxy/RevokeResponse.cs index 6a1e144..aecfe80 100644 --- a/hashicorp-vault-cagateway/APIProxy/RevokeResponse.cs +++ b/hashicorp-vault-cagateway/APIProxy/RevokeResponse.cs @@ -1,4 +1,4 @@ -// Copyright 2024 Keyfactor +// Copyright 2025 Keyfactor // Licensed under the Apache License, Version 2.0 (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.apache.org/licenses/LICENSE-2.0 // Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, diff --git a/hashicorp-vault-cagateway/APIProxy/SealStatusResponse.cs b/hashicorp-vault-cagateway/APIProxy/SealStatusResponse.cs index e6cb1c3..9d86d04 100644 --- a/hashicorp-vault-cagateway/APIProxy/SealStatusResponse.cs +++ b/hashicorp-vault-cagateway/APIProxy/SealStatusResponse.cs @@ -1,4 +1,4 @@ -// Copyright 2024 Keyfactor +// Copyright 2025 Keyfactor // Licensed under the Apache License, Version 2.0 (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.apache.org/licenses/LICENSE-2.0 // Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, diff --git a/hashicorp-vault-cagateway/APIProxy/SignRequest.cs b/hashicorp-vault-cagateway/APIProxy/SignRequest.cs index f0360f2..1e65db2 100644 --- a/hashicorp-vault-cagateway/APIProxy/SignRequest.cs +++ b/hashicorp-vault-cagateway/APIProxy/SignRequest.cs @@ -1,4 +1,4 @@ -// Copyright 2024 Keyfactor +// Copyright 2025 Keyfactor // Licensed under the Apache License, Version 2.0 (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.apache.org/licenses/LICENSE-2.0 // Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, diff --git a/hashicorp-vault-cagateway/APIProxy/SignResponse.cs b/hashicorp-vault-cagateway/APIProxy/SignResponse.cs index 2aa29d7..882fbc9 100644 --- a/hashicorp-vault-cagateway/APIProxy/SignResponse.cs +++ b/hashicorp-vault-cagateway/APIProxy/SignResponse.cs @@ -1,4 +1,4 @@ -// Copyright 2024 Keyfactor +// Copyright 2025 Keyfactor // Licensed under the Apache License, Version 2.0 (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.apache.org/licenses/LICENSE-2.0 // Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, diff --git a/hashicorp-vault-cagateway/APIProxy/TokenLookupResponse.cs b/hashicorp-vault-cagateway/APIProxy/TokenLookupResponse.cs index 0b7d141..e72f5e8 100644 --- a/hashicorp-vault-cagateway/APIProxy/TokenLookupResponse.cs +++ b/hashicorp-vault-cagateway/APIProxy/TokenLookupResponse.cs @@ -1,4 +1,4 @@ -// Copyright 2024 Keyfactor +// Copyright 2025 Keyfactor // Licensed under the Apache License, Version 2.0 (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.apache.org/licenses/LICENSE-2.0 // Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, diff --git a/hashicorp-vault-cagateway/APIProxy/WrappedResponse.cs b/hashicorp-vault-cagateway/APIProxy/WrappedResponse.cs index 7725dd9..80051a2 100644 --- a/hashicorp-vault-cagateway/APIProxy/WrappedResponse.cs +++ b/hashicorp-vault-cagateway/APIProxy/WrappedResponse.cs @@ -1,4 +1,4 @@ -// Copyright 2024 Keyfactor +// Copyright 2025 Keyfactor // Licensed under the Apache License, Version 2.0 (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.apache.org/licenses/LICENSE-2.0 // Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, @@ -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 3ab133d..5ff461e 100644 --- a/hashicorp-vault-cagateway/Client/HashicorpVaultClient.cs +++ b/hashicorp-vault-cagateway/Client/HashicorpVaultClient.cs @@ -1,4 +1,4 @@ -// Copyright 2024 Keyfactor +// Copyright 2025 Keyfactor // Licensed under the Apache License, Version 2.0 (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.apache.org/licenses/LICENSE-2.0 // Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, @@ -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) { @@ -246,6 +252,41 @@ public async Task> GetRoleNamesAsync() finally { logger.MethodExit(); } } + /// + /// Retrieves the metadata for the certificate + /// + /// + /// + public async Task GetCertMetadata(string certSerial) + { + logger.MethodEntry(); + + try + { + var res = await _vaultHttp.GetAsync>($"cert-metadata/{certSerial}"); + var md = res?.Data; + if (md != null) + { + logger.LogTrace($"got response from cert-metadata"); + logger.LogTrace($"serial number: {md.SerialNumber}"); + logger.LogTrace($"issuer id: {md.IssuerId}"); + logger.LogTrace($"expiration: {md.Expiration}"); + logger.LogTrace($"metadata: {md.CertMetadata}"); + logger.LogTrace($"role: {md.Role}"); + } + else { + logger.LogTrace($"no metadata associated with cert {certSerial} could be found."); + } + return md; + } + catch (Exception ex) + { + logger.LogError($"an error occurred when attempting to retrieve the certificate metadata: {ex.Message}"); + throw; + } + finally { logger.MethodExit(); } + } + private void SetClientValuesFromConfigs(HashicorpVaultCAConfig caConfig, HashicorpVaultCATemplateConfig templateConfig) { logger.MethodEntry(); @@ -282,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 c1c5f99..e40e3f8 100644 --- a/hashicorp-vault-cagateway/Client/VaultHttp.cs +++ b/hashicorp-vault-cagateway/Client/VaultHttp.cs @@ -1,4 +1,4 @@ -// Copyright 2024 Keyfactor +// Copyright 2025 Keyfactor // Licensed under the Apache License, Version 2.0 (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.apache.org/licenses/LICENSE-2.0 // Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, @@ -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,18 +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($"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) { @@ -107,7 +122,7 @@ 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); + string serializedParams = JsonSerializer.Serialize(parameters); logger.LogTrace($"serialized parameters (from {parameters.GetType()?.Name}): {serializedParams}"); request.AddJsonBody(serializedParams); } @@ -126,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/Constants.cs b/hashicorp-vault-cagateway/Constants.cs index d82b13b..3a38d15 100644 --- a/hashicorp-vault-cagateway/Constants.cs +++ b/hashicorp-vault-cagateway/Constants.cs @@ -1,4 +1,4 @@ -// Copyright 2024 Keyfactor +// Copyright 2025 Keyfactor // Licensed under the Apache License, Version 2.0 (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.apache.org/licenses/LICENSE-2.0 // Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, diff --git a/hashicorp-vault-cagateway/HashicorpVaultCAConfig.cs b/hashicorp-vault-cagateway/HashicorpVaultCAConfig.cs index 0413b0c..5972502 100644 --- a/hashicorp-vault-cagateway/HashicorpVaultCAConfig.cs +++ b/hashicorp-vault-cagateway/HashicorpVaultCAConfig.cs @@ -1,4 +1,4 @@ -// Copyright 2024 Keyfactor +// Copyright 2025 Keyfactor // Licensed under the Apache License, Version 2.0 (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.apache.org/licenses/LICENSE-2.0 // Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, @@ -23,8 +23,8 @@ public class HashicorpVaultCAConfig [JsonPropertyName(Constants.CAConfig.NAMESPACE)] public string Namespace { get; set; } - [JsonPropertyName(Constants.CAConfig.CLIENTCERT)] - public AuthCert ClientCertificate { get; set; } + //[JsonPropertyName(Constants.CAConfig.CLIENTCERT)] + //public AuthCert ClientCertificate { get; set; } [JsonPropertyName(Constants.CAConfig.ENABLED)] public bool Enabled { get; set; } diff --git a/hashicorp-vault-cagateway/HashicorpVaultCAConnector.cs b/hashicorp-vault-cagateway/HashicorpVaultCAConnector.cs index e020e9e..21ce226 100644 --- a/hashicorp-vault-cagateway/HashicorpVaultCAConnector.cs +++ b/hashicorp-vault-cagateway/HashicorpVaultCAConnector.cs @@ -1,4 +1,4 @@ -// Copyright 2024 Keyfactor +// Copyright 2025 Keyfactor // Licensed under the Apache License, Version 2.0 (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.apache.org/licenses/LICENSE-2.0 // Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, @@ -18,6 +18,8 @@ using System.Text.Json; using System.Threading; using System.Threading.Tasks; +using System.Reflection; +using System.Runtime.ConstrainedExecution; namespace Keyfactor.Extensions.CAPlugin.HashicorpVault { @@ -35,7 +37,7 @@ public HashicorpVaultCAConnector() _serializerOptions = new() { - DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingDefault, + DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, RespectNullableAnnotations = true, PropertyNameCaseInsensitive = true, PreferredObjectCreationHandling = JsonObjectCreationHandling.Replace, @@ -50,10 +52,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 +171,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 +253,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,15 +264,16 @@ 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"); @@ -268,7 +283,7 @@ public async Task Synchronize(BlockingCollection blockin // 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,18 +295,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 retrieve the role name (productId) from the certificate metadata, if available"); + + var metaData = new MetadataResponse(); + + try + { + metaData = await _client.GetCertMetadata(certSerial); + } + catch (Exception ex) + { + 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)) + { + 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) @@ -302,8 +340,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 @@ -311,13 +351,16 @@ public async Task Synchronize(BlockingCollection blockin CARequestID = trackingId, Certificate = certFromVault.Certificate, Status = vaultStatus, - RevocationDate = certFromVault.RevocationTime + RevocationDate = certFromVault.RevocationTime != null ? DateTime.Parse(certFromVault.RevocationTime.ToString(), System.Globalization.CultureInfo.InvariantCulture, System.Globalization.DateTimeStyles.AdjustToUniversal | System.Globalization.DateTimeStyles.AssumeUniversal) : null // ProductID is not available via the API after the initial issuance. we do not want to overwrite }; + + blockingBuffer.Add(newCert, cancelToken); } } count++; } + blockingBuffer.CompleteAdding(); logger.LogTrace($"Completed sync of {count} certificates"); logger.MethodExit(); } @@ -329,6 +372,7 @@ public async Task Synchronize(BlockingCollection blockin public async Task ValidateCAConnectionInfo(Dictionary connectionInfo) { logger.MethodEntry(); + logger.LogTrace(message: $"Validating CA connection info: {JsonSerializer.Serialize(connectionInfo)}"); // first, we check to see if the CA Gateway is enabled in the configuration if (!(bool)connectionInfo[Constants.CAConfig.ENABLED]) @@ -352,7 +396,10 @@ 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 = connectionInfo[Constants.CAConfig.CLIENTCERT] as string; + + var cert = string.Empty; // temporary until client cert auth into vault is implemented if (string.IsNullOrEmpty(token) && string.IsNullOrEmpty(cert)) { @@ -399,12 +446,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) { @@ -422,6 +469,9 @@ public async Task ValidateCAConnectionInfo(Dictionary connection public Task ValidateProductInfo(EnrollmentProductInfo productInfo, Dictionary connectionInfo) { logger.MethodEntry(); + + logger.LogTrace($"validating product info: {JsonSerializer.Serialize(productInfo)}"); + List errors = new List(); HashicorpVaultCATemplateConfig templateConfig = null; @@ -429,7 +479,7 @@ public Task ValidateProductInfo(EnrollmentProductInfo productInfo, Dictionary(JsonSerializer.Serialize(productInfo)); + templateConfig = JsonSerializer.Deserialize(JsonSerializer.Serialize(productInfo.ProductParameters)); caConfig = JsonSerializer.Deserialize(JsonSerializer.Serialize(connectionInfo)); logger.LogTrace("successfully deserialized the product and CA config values."); } @@ -439,11 +489,6 @@ 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/HashicorpVaultCATemplateConfig.cs b/hashicorp-vault-cagateway/HashicorpVaultCATemplateConfig.cs index 76a5145..fadfe6f 100644 --- a/hashicorp-vault-cagateway/HashicorpVaultCATemplateConfig.cs +++ b/hashicorp-vault-cagateway/HashicorpVaultCATemplateConfig.cs @@ -1,4 +1,4 @@ -// Copyright 2024 Keyfactor +// Copyright 2025 Keyfactor // Licensed under the Apache License, Version 2.0 (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.apache.org/licenses/LICENSE-2.0 // Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, 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/integration-manifest.json b/integration-manifest.json index e1bd1b7..30206d5 100644 --- a/integration-manifest.json +++ b/integration-manifest.json @@ -9,6 +9,7 @@ "description": "Hashicorp Vault plugin for the AnyCA REST Gateway Framework", "gateway_framework": "24.2.0", "release_dir": "hashicorp-vault-cagateway/bin/Release", + "release_project": "hashicorp-vault-cagateway\\hashicorp-vault-caplugin.csproj", "about": { "carest": { "product_ids": [], diff --git a/readme_source.md b/readme_source.md index 20d51d2..836a7bb 100644 --- a/readme_source.md +++ b/readme_source.md @@ -23,6 +23,21 @@ Make sure the following information is available, as it will be needed to comple - An authentication token that has sufficient authority to perform operations on the PKI Secrets engine - PKI Secrets Engine Roles defined that will correspond to certificate templates to be used when signing certificates with the CA. +### Managing Certificates Issued Outside of Keyfactor Command + +:warning: Important! + +The role name used when issuing the certificate is required in order to store the associated details for the certificate within Keyfactor Command. +The Hashicorp Vault PKI Secrets Engine does not store the role name by default. + +Because of this, certificates generated outside of Keyfactor Command will _not_ have their associated data available within Command **unless the following two conditions are met**: +- The certificate was generated in an instance of Hashicorp Vault with the Enterprise License, to allow for certificate metadata to be stored. +- There is certificate metadata associated with the certificate + +The metadata value provided when the cert is issued outside of Command can be blank, it just needs to have been provided in order for the role used for issuance to be stored. +Certificates issued for the Hashicorp Vault CA from within the Keyfactor Command platform do not need to have metadata associated with it in order to view the certificate details. + + ### Steps 1. Install the AnyCA Gateway Rest per the [official Keyfactor documentation](https://software.keyfactor.com/Guides/AnyCAGatewayREST/Content/AnyCAGatewayREST/InstallIntroduction.htm). @@ -80,7 +95,7 @@ Make sure the following information is available, as it will be needed to comple 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