From fc725c42956adcfbd714b7b99cda0b57463c26fe Mon Sep 17 00:00:00 2001 From: Brian Hill Date: Wed, 9 Sep 2026 14:23:21 -0400 Subject: [PATCH 1/9] Add support for EV, OV Wildcard, and DV Wildcard multi-name certificate products CSC TrustedSecure EV, Multiple Names (7), CSC TrustedSecure OV Wildcard, Multiple Names (8), and CSC TrustedSecure DV Wildcard, Multiple Names (9) per CSC's certificate type table. Type 7 requires both EvCertificateDetails and SubjectAlternativeNames since it is EV plus multi-name; 8 and 9 only need SubjectAlternativeNames. --- CHANGELOG.md | 3 + cscglobal-caplugin/Constants.cs | 5 +- cscglobal-caplugin/RequestManager.cs | 21 +++++-- docsource/configuration.md | 88 ++++++++++++++++++++++++++++ integration-manifest.json | 5 +- 5 files changed, 114 insertions(+), 8 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5c6124b..367a89c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,6 @@ +v1.2.0 +- Added support for CSC TrustedSecure EV, Multiple Names; CSC TrustedSecure OV Wildcard, Multiple Names; and CSC TrustedSecure DV Wildcard, Multiple Names certificate products + v.1.1.1 - Added Incremental Sync that goes back X Number of days - Fixed issue with parsing certain certificates that were in zip format diff --git a/cscglobal-caplugin/Constants.cs b/cscglobal-caplugin/Constants.cs index 4d6b4da..fd79d7a 100644 --- a/cscglobal-caplugin/Constants.cs +++ b/cscglobal-caplugin/Constants.cs @@ -27,7 +27,10 @@ public class ProductIDs "CSC TrustedSecure Premium Wildcard Certificate", "CSC TrustedSecure Domain Validated SSL", "CSC TrustedSecure Domain Validated Wildcard SSL", - "CSC TrustedSecure Domain Validated UC Certificate" + "CSC TrustedSecure Domain Validated UC Certificate", + "CSC TrustedSecure EV, Multiple Names", + "CSC TrustedSecure OV Wildcard, Multiple Names", + "CSC TrustedSecure DV Wildcard, Multiple Names" }; } diff --git a/cscglobal-caplugin/RequestManager.cs b/cscglobal-caplugin/RequestManager.cs index 776902c..94ce704 100644 --- a/cscglobal-caplugin/RequestManager.cs +++ b/cscglobal-caplugin/RequestManager.cs @@ -177,8 +177,8 @@ public RegistrationRequest GetRegistrationRequest(EnrollmentProductInfo productI BusinessUnit = productInfo.ProductParameters["Business Unit"], ShowPrice = true, //User should not have to fill this out CustomFields = GetCustomFields(productInfo, customFields), - SubjectAlternativeNames = certificateType == "2" ? GetSubjectAlternativeNames(productInfo, sans) : null, - EvCertificateDetails = certificateType == "3" ? GetEvCertificateDetails(productInfo) : null + SubjectAlternativeNames = MultiNameCertificateTypes.Contains(certificateType) ? GetSubjectAlternativeNames(productInfo, sans) : null, + EvCertificateDetails = EvCertificateTypes.Contains(certificateType) ? GetEvCertificateDetails(productInfo) : null }; } @@ -206,11 +206,20 @@ private string GetCertificateType(string productId) return "5"; case "CSC TrustedSecure Domain Validated UC Certificate": return "6"; + case "CSC TrustedSecure EV, Multiple Names": + return "7"; + case "CSC TrustedSecure OV Wildcard, Multiple Names": + return "8"; + case "CSC TrustedSecure DV Wildcard, Multiple Names": + return "9"; } return "-1"; } + private static readonly HashSet MultiNameCertificateTypes = new() { "2", "7", "8", "9" }; + private static readonly HashSet EvCertificateTypes = new() { "3", "7" }; + public Notifications GetNotifications(EnrollmentProductInfo productInfo) { return new Notifications @@ -250,9 +259,9 @@ public RenewalRequest GetRenewalRequest(EnrollmentProductInfo productInfo, strin OrganizationContact = productInfo.ProductParameters["Organization Contact"], BusinessUnit = productInfo.ProductParameters["Business Unit"], ShowPrice = true, - SubjectAlternativeNames = certificateType == "2" ? GetSubjectAlternativeNames(productInfo, sans) : null, + SubjectAlternativeNames = MultiNameCertificateTypes.Contains(certificateType) ? GetSubjectAlternativeNames(productInfo, sans) : null, CustomFields = GetCustomFields(productInfo, customFields), - EvCertificateDetails = certificateType == "3" ? GetEvCertificateDetails(productInfo) : null + EvCertificateDetails = EvCertificateTypes.Contains(certificateType) ? GetEvCertificateDetails(productInfo) : null }; } @@ -308,9 +317,9 @@ public ReissueRequest GetReissueRequest(EnrollmentProductInfo productInfo, strin OrganizationContact = productInfo.ProductParameters["Organization Contact"], BusinessUnit = productInfo.ProductParameters["Business Unit"], ShowPrice = true, - SubjectAlternativeNames = certificateType == "2" ? GetSubjectAlternativeNames(productInfo, sans) : null, + SubjectAlternativeNames = MultiNameCertificateTypes.Contains(certificateType) ? GetSubjectAlternativeNames(productInfo, sans) : null, CustomFields = GetCustomFields(productInfo, customFields), - EvCertificateDetails = certificateType == "3" ? GetEvCertificateDetails(productInfo) : null + EvCertificateDetails = EvCertificateTypes.Contains(certificateType) ? GetEvCertificateDetails(productInfo) : null }; } diff --git a/docsource/configuration.md b/docsource/configuration.md index d8c196e..63cc066 100644 --- a/docsource/configuration.md +++ b/docsource/configuration.md @@ -216,3 +216,91 @@ Notification Email(s) Comma Separated | String | N/A CN DCV Email | String | N/A Addtl Sans Comma Separated DCV Emails | String | N/A +**CSC TrustedSecure EV, Multiple Names - Details Tab** + +CONFIG ELEMENT | DESCRIPTION +----------------------------|------------------ +Template Short Name | CSC TrustedSecure EV, Multiple Names +Template Display Name | CSC TrustedSecure EV, Multiple Names +Friendly Name | CSC TrustedSecure EV, Multiple Names +Keys Size | 2048 +Enforce RFC 2818 Compliance | True +CSR Enrollment | True +Pfx Enrollment | True + + +**CSC TrustedSecure EV, Multiple Names - Enrollment Fields** + +NAME | DATA TYPE | VALUES +-----|--------------|----------------- +Term | Multiple Choice | 12,24 +Applicant First Name | String | N/A +Applicant Last Name | String | N/A +Applicant Email Address | String | N/A +Applicant Phone | String | N/A +Domain Control Validation Method | Multiple Choice | EMAIL +Organization Contact | Multiple Choice | Get From CSC Differs For Clients +Business Unit | Multiple Choice | Get From CSC Differs For Clients +Notification Email(s) Comma Separated | String | N/A +CN DCV Email | String | N/A +Organization Country | String | N/A +Addtl Sans Comma Separated DCV Emails | String | N/A + +**CSC TrustedSecure OV Wildcard, Multiple Names - Details Tab** + +CONFIG ELEMENT | DESCRIPTION +----------------------------|------------------ +Template Short Name | CSC TrustedSecure OV Wildcard, Multiple Names +Template Display Name | CSC TrustedSecure OV Wildcard, Multiple Names +Friendly Name | CSC TrustedSecure OV Wildcard, Multiple Names +Keys Size | 2048 +Enforce RFC 2818 Compliance | True +CSR Enrollment | True +Pfx Enrollment | True + + +**CSC TrustedSecure OV Wildcard, Multiple Names - Enrollment Fields** + +NAME | DATA TYPE | VALUES +-----|--------------|----------------- +Term | Multiple Choice | 12,24 +Applicant First Name | String | N/A +Applicant Last Name | String | N/A +Applicant Email Address | String | N/A +Applicant Phone | String | N/A +Domain Control Validation Method | Multiple Choice | EMAIL +Organization Contact | Multiple Choice | Get From CSC Differs For Clients +Business Unit | Multiple Choice | Get From CSC Differs For Clients +Notification Email(s) Comma Separated | String | N/A +CN DCV Email | String | N/A +Addtl Sans Comma Separated DCV Emails | String | N/A + +**CSC TrustedSecure DV Wildcard, Multiple Names - Details Tab** + +CONFIG ELEMENT | DESCRIPTION +----------------------------|------------------ +Template Short Name | CSC TrustedSecure DV Wildcard, Multiple Names +Template Display Name | CSC TrustedSecure DV Wildcard, Multiple Names +Friendly Name | CSC TrustedSecure DV Wildcard, Multiple Names +Keys Size | 2048 +Enforce RFC 2818 Compliance | True +CSR Enrollment | True +Pfx Enrollment | True + + +**CSC TrustedSecure DV Wildcard, Multiple Names - Enrollment Fields** + +NAME | DATA TYPE | VALUES +-----|--------------|----------------- +Term | Multiple Choice | 12,24 +Applicant First Name | String | N/A +Applicant Last Name | String | N/A +Applicant Email Address | String | N/A +Applicant Phone | String | N/A +Domain Control Validation Method | Multiple Choice | EMAIL +Organization Contact | Multiple Choice | Get From CSC Differs For Clients +Business Unit | Multiple Choice | Get From CSC Differs For Clients +Notification Email(s) Comma Separated | String | N/A +CN DCV Email | String | N/A +Addtl Sans Comma Separated DCV Emails | String | N/A + diff --git a/integration-manifest.json b/integration-manifest.json index 2b4b8c4..756b19a 100644 --- a/integration-manifest.json +++ b/integration-manifest.json @@ -95,7 +95,10 @@ "CSC TrustedSecure Premium Wildcard Certificate", "CSC TrustedSecure Domain Validated SSL", "CSC TrustedSecure Domain Validated Wildcard SSL", - "CSC TrustedSecure Domain Validated UC Certificate" + "CSC TrustedSecure Domain Validated UC Certificate", + "CSC TrustedSecure EV, Multiple Names", + "CSC TrustedSecure OV Wildcard, Multiple Names", + "CSC TrustedSecure DV Wildcard, Multiple Names" ] } } From 916c16b395357ab315ff4f5ad6b0818ab3337ce2 Mon Sep 17 00:00:00 2001 From: Keyfactor Date: Wed, 9 Sep 2026 18:24:58 +0000 Subject: [PATCH 2/9] Update generated docs --- README.md | 88 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 88 insertions(+) diff --git a/README.md b/README.md index c68aac4..f61fd6d 100644 --- a/README.md +++ b/README.md @@ -293,6 +293,94 @@ This integration is tested and confirmed as working for Anygateway REST 24.2 and CN DCV Email | String | N/A Addtl Sans Comma Separated DCV Emails | String | N/A + **CSC TrustedSecure EV, Multiple Names - Details Tab** + + CONFIG ELEMENT | DESCRIPTION + ----------------------------|------------------ + Template Short Name | CSC TrustedSecure EV, Multiple Names + Template Display Name | CSC TrustedSecure EV, Multiple Names + Friendly Name | CSC TrustedSecure EV, Multiple Names + Keys Size | 2048 + Enforce RFC 2818 Compliance | True + CSR Enrollment | True + Pfx Enrollment | True + + + **CSC TrustedSecure EV, Multiple Names - Enrollment Fields** + + NAME | DATA TYPE | VALUES + -----|--------------|----------------- + Term | Multiple Choice | 12,24 + Applicant First Name | String | N/A + Applicant Last Name | String | N/A + Applicant Email Address | String | N/A + Applicant Phone | String | N/A + Domain Control Validation Method | Multiple Choice | EMAIL + Organization Contact | Multiple Choice | Get From CSC Differs For Clients + Business Unit | Multiple Choice | Get From CSC Differs For Clients + Notification Email(s) Comma Separated | String | N/A + CN DCV Email | String | N/A + Organization Country | String | N/A + Addtl Sans Comma Separated DCV Emails | String | N/A + + **CSC TrustedSecure OV Wildcard, Multiple Names - Details Tab** + + CONFIG ELEMENT | DESCRIPTION + ----------------------------|------------------ + Template Short Name | CSC TrustedSecure OV Wildcard, Multiple Names + Template Display Name | CSC TrustedSecure OV Wildcard, Multiple Names + Friendly Name | CSC TrustedSecure OV Wildcard, Multiple Names + Keys Size | 2048 + Enforce RFC 2818 Compliance | True + CSR Enrollment | True + Pfx Enrollment | True + + + **CSC TrustedSecure OV Wildcard, Multiple Names - Enrollment Fields** + + NAME | DATA TYPE | VALUES + -----|--------------|----------------- + Term | Multiple Choice | 12,24 + Applicant First Name | String | N/A + Applicant Last Name | String | N/A + Applicant Email Address | String | N/A + Applicant Phone | String | N/A + Domain Control Validation Method | Multiple Choice | EMAIL + Organization Contact | Multiple Choice | Get From CSC Differs For Clients + Business Unit | Multiple Choice | Get From CSC Differs For Clients + Notification Email(s) Comma Separated | String | N/A + CN DCV Email | String | N/A + Addtl Sans Comma Separated DCV Emails | String | N/A + + **CSC TrustedSecure DV Wildcard, Multiple Names - Details Tab** + + CONFIG ELEMENT | DESCRIPTION + ----------------------------|------------------ + Template Short Name | CSC TrustedSecure DV Wildcard, Multiple Names + Template Display Name | CSC TrustedSecure DV Wildcard, Multiple Names + Friendly Name | CSC TrustedSecure DV Wildcard, Multiple Names + Keys Size | 2048 + Enforce RFC 2818 Compliance | True + CSR Enrollment | True + Pfx Enrollment | True + + + **CSC TrustedSecure DV Wildcard, Multiple Names - Enrollment Fields** + + NAME | DATA TYPE | VALUES + -----|--------------|----------------- + Term | Multiple Choice | 12,24 + Applicant First Name | String | N/A + Applicant Last Name | String | N/A + Applicant Email Address | String | N/A + Applicant Phone | String | N/A + Domain Control Validation Method | Multiple Choice | EMAIL + Organization Contact | Multiple Choice | Get From CSC Differs For Clients + Business Unit | Multiple Choice | Get From CSC Differs For Clients + Notification Email(s) Comma Separated | String | N/A + CN DCV Email | String | N/A + Addtl Sans Comma Separated DCV Emails | String | N/A + 3. Follow the [official Keyfactor documentation](https://software.keyfactor.com/Guides/AnyCAGatewayREST/Content/AnyCAGatewayREST/AddCA-Keyfactor.htm) to add each defined Certificate Authority to Keyfactor Command and import the newly defined Certificate Templates. 4. In Keyfactor Command (v12.3+), for each imported Certificate Template, follow the [official documentation](https://software.keyfactor.com/Core-OnPrem/Current/Content/ReferenceGuide/Configuring%20Template%20Options.htm) to define enrollment fields for each of the following parameters: From 9c3b8241900ebf8a7f102fce9ac716c55907f396 Mon Sep 17 00:00:00 2001 From: Brian Hill Date: Wed, 9 Sep 2026 16:37:07 -0400 Subject: [PATCH 3/9] Add FlowLogger and expand error/warning/info logging Introduce a step-oriented FlowLogger for tracing Enroll, Revoke, and Synchronize pipelines end to end, and add logging throughout RequestManager (previously had none) and CscGlobalClient (missing error/warning branches, wrong logger category on CscGlobalClient). --- cscglobal-caplugin/CSCGlobalCAPlugin.cs | 309 +++++++++++++------ cscglobal-caplugin/Client/CscGlobalClient.cs | 60 +++- cscglobal-caplugin/FlowLogger.cs | 267 ++++++++++++++++ cscglobal-caplugin/RequestManager.cs | 27 ++ 4 files changed, 555 insertions(+), 108 deletions(-) create mode 100644 cscglobal-caplugin/FlowLogger.cs diff --git a/cscglobal-caplugin/CSCGlobalCAPlugin.cs b/cscglobal-caplugin/CSCGlobalCAPlugin.cs index e1af2f0..edf44bd 100644 --- a/cscglobal-caplugin/CSCGlobalCAPlugin.cs +++ b/cscglobal-caplugin/CSCGlobalCAPlugin.cs @@ -47,6 +47,7 @@ public void Initialize(IAnyCAPluginConfigProvider configProvider, ICertificateDa CscGlobalClient = new CscGlobalClient(configProvider); var templateSync = configProvider.CAConnectionData["TemplateSync"].ToString(); if (templateSync.ToUpper() == "ON") EnableTemplateSync = true; + Logger.LogInformation($"Template sync is {(EnableTemplateSync ? "enabled" : "disabled")}"); if (configProvider.CAConnectionData.ContainsKey(Constants.SyncFilterDays)) { @@ -56,7 +57,13 @@ public void Initialize(IAnyCAPluginConfigProvider configProvider, ICertificateDa SyncFilterDays = syncFilterDays; Logger.LogDebug($"SyncFilterDays configured to {SyncFilterDays} days"); } + else + { + Logger.LogWarning($"Could not parse {Constants.SyncFilterDays} value '{syncFilterDaysStr}' as an integer; using default"); + } } + + Logger.LogInformation("CSCGlobalCAPlugin initialized successfully"); Logger.MethodExit(LogLevel.Debug); } @@ -96,6 +103,7 @@ public async Task GetSingleRecord(string caRequestID) } catch (Exception e) { + Logger.LogError(e, "Error occurred getting single cert for CA request ID {CaRequestID}: {Message}", caRequestID, e.Message); throw new Exception($"Error Occurred getting single cert {e.Message}"); } } @@ -106,27 +114,39 @@ public async Task Synchronize(BlockingCollection blockin { Logger.LogTrace($"Full Sync? {fullSync.ToString()}"); Logger.MethodEntry(); + using var flow = new FlowLogger(Logger, "Synchronize"); try { if (fullSync) { - Logger.LogDebug("Performing full sync - no date filter applied"); - await SyncCertificates(blockingBuffer, cancelToken, null); + Logger.LogInformation("Performing full sync - no date filter applied"); + flow.Step("DetermineSyncMode", "Full sync - no date filter applied"); + await SyncCertificates(blockingBuffer, cancelToken, null, flow); } else { var filterDays = SyncFilterDays > 0 ? SyncFilterDays : 5; var filterDate = DateTime.Today.Subtract(TimeSpan.FromDays(filterDays)); var dateFilter = filterDate.ToString("yyyy/MM/dd"); - Logger.LogDebug($"Performing incremental sync with expiration date filter: {dateFilter}"); - await SyncCertificates(blockingBuffer, cancelToken, dateFilter); + Logger.LogInformation($"Performing incremental sync with expiration date filter: {dateFilter}"); + flow.Step("DetermineSyncMode", $"Incremental sync with expiration date filter: {dateFilter}"); + await SyncCertificates(blockingBuffer, cancelToken, dateFilter, flow); } blockingBuffer.CompleteAdding(); + Logger.LogInformation("Csc Global Synchronize Task completed successfully"); + } + catch (OperationCanceledException) + { + Logger.LogWarning("Csc Global Synchronize Task was cancelled"); + flow.Fail("Synchronize", "Task was cancelled"); + blockingBuffer.CompleteAdding(); + throw; } catch (Exception e) { - Logger.LogError($"Csc Global Synchronize Task failed! {LogHandler.FlattenException(e)}"); + Logger.LogError(e, $"Csc Global Synchronize Task failed! {LogHandler.FlattenException(e)}"); + flow.Fail("Synchronize", e.Message); Logger.MethodExit(); blockingBuffer.CompleteAdding(); throw; @@ -136,10 +156,15 @@ public async Task Synchronize(BlockingCollection blockin } private async Task SyncCertificates(BlockingCollection blockingBuffer, - CancellationToken cancelToken, string? dateFilter) + CancellationToken cancelToken, string? dateFilter, FlowLogger flow) { - var certs = await CscGlobalClient.SubmitCertificateListRequestAsync(dateFilter); + var certs = await flow.StepAsync("SubmitCertificateListRequest", + () => CscGlobalClient.SubmitCertificateListRequestAsync(dateFilter)); + + Logger.LogInformation($"Retrieved {certs?.Results?.Count ?? 0} certificate(s) from CSC Global for sync"); + var queuedCount = 0; + var skippedCount = 0; foreach (var currentResponseItem in certs.Results) { cancelToken.ThrowIfCancellationRequested(); @@ -164,6 +189,7 @@ private async Task SyncCertificates(BlockingCollection b var certData = fileContent.Replace("\r\n", string.Empty); var certString = GetEndEntityCertificate(certData); if (certString.Length > 0) + { blockingBuffer.Add(new AnyCAPluginCertificate { CARequestID = $"{currentResponseItem?.Uuid}", @@ -171,36 +197,70 @@ private async Task SyncCertificates(BlockingCollection b Status = certStatus, ProductID = productId }, cancelToken); + queuedCount++; + } + else + { + Logger.LogWarning($"Could not extract end-entity certificate for {currentResponseItem?.Uuid}; skipping sync of this record"); + skippedCount++; + } + } + else + { + Logger.LogWarning($"No certificate content returned by CSC Global for {currentResponseItem?.Uuid}; skipping sync of this record"); + skippedCount++; } } + else + { + Logger.LogTrace($"Skipping Certificate ID {currentResponseItem?.Uuid} - status {currentResponseItem?.Status} is not eligible for sync"); + skippedCount++; + } } + + flow.Step("QueueCertificates", $"Queued {queuedCount}, skipped {skippedCount}"); + Logger.LogInformation($"Sync queued {queuedCount} certificate(s), skipped {skippedCount}"); } //done public async Task Revoke(string caRequestID, string hexSerialNumber, uint revocationReason) { + Logger.MethodEntry(LogLevel.Debug); + using var flow = new FlowLogger(Logger, "Revoke"); try { - Logger.LogTrace("Staring Revoke Method"); - var revokeResponse = - Task.Run(async () => - await CscGlobalClient.SubmitRevokeCertificateAsync(caRequestID.Substring(0, 36))).Result - ; //todo fix to use pipe delimiter + Logger.LogInformation($"Starting Revoke for CA request ID {caRequestID}, reason {revocationReason}"); + var uuid = caRequestID.Substring(0, 36); //todo fix to use pipe delimiter + + var revokeResponse = await flow.StepAsync("SubmitRevokeCertificate", + () => CscGlobalClient.SubmitRevokeCertificateAsync(uuid)); Logger.LogTrace($"Revoke Response JSON: {JsonConvert.SerializeObject(revokeResponse)}"); - Logger.MethodExit(LogLevel.Debug); var revokeResult = _requestManager.GetRevokeResult(revokeResponse); if (revokeResult == (int)EndEntityStatus.FAILED) + { if (!string.IsNullOrEmpty(revokeResponse?.RegistrationError?.Description)) + { + flow.Fail("SubmitRevokeCertificate", revokeResponse?.RegistrationError?.Description ?? "Unknown error"); throw new HttpRequestException( $"Revoke Failed with message {revokeResponse?.RegistrationError?.Description}"); + } + Logger.LogWarning($"Revoke returned a failed status for CA request ID {caRequestID} with no error description"); + } + else + { + Logger.LogInformation($"Revoke succeeded for CA request ID {caRequestID}"); + } + + Logger.MethodExit(LogLevel.Debug); return revokeResult; } catch (Exception e) { + Logger.LogError(e, $"Revoke Failed for CA request ID {caRequestID} with message {e?.Message}"); throw new Exception($"Revoke Failed with message {e?.Message}"); } } @@ -210,114 +270,153 @@ public async Task Enroll(string csr, string subject, Dictionar EnrollmentProductInfo productInfo, RequestFormat requestFormat, EnrollmentType enrollmentType) { Logger.MethodEntry(LogLevel.Debug); + Logger.LogInformation($"Starting Enroll for product {productInfo.ProductID}, enrollment type {enrollmentType}"); + using var flow = new FlowLogger(Logger, "Enroll"); - RegistrationRequest enrollmentRequest; - var priorSn = ""; - ReissueRequest reissueRequest; - RenewalRequest renewRequest; - if (productInfo.ProductParameters.ContainsKey("priorcertsn")) + try { - priorSn = productInfo.ProductParameters["PriorCertSN"]; - Logger.LogDebug($"Prior cert sn: {priorSn}"); - } + RegistrationRequest enrollmentRequest; + var priorSn = ""; + ReissueRequest reissueRequest; + RenewalRequest renewRequest; + if (productInfo.ProductParameters.ContainsKey("priorcertsn")) + { + priorSn = productInfo.ProductParameters["PriorCertSN"]; + Logger.LogDebug($"Prior cert sn: {priorSn}"); + } - string uUId; - var customFields = await CscGlobalClient.SubmitGetCustomFields(); + string uUId; + var customFields = await flow.StepAsync("SubmitGetCustomFields", () => CscGlobalClient.SubmitGetCustomFields()); - switch (enrollmentType) - { - case EnrollmentType.New: - Logger.LogTrace("Entering New Enrollment"); - //If they renewed an expired cert it gets here and this will not be supported - IRegistrationResponse enrollmentResponse; - if (!productInfo.ProductParameters.ContainsKey("PriorCertSN")) - { - enrollmentRequest = _requestManager.GetRegistrationRequest(productInfo, csr, san, customFields); - Logger.LogTrace($"Enrollment Request JSON: {JsonConvert.SerializeObject(enrollmentRequest)}"); - enrollmentResponse = - Task.Run(async () => await CscGlobalClient.SubmitRegistrationAsync(enrollmentRequest)) - .Result; - Logger.LogTrace($"Enrollment Response JSON: {JsonConvert.SerializeObject(enrollmentResponse)}"); - } - else - { - return new EnrollmentResult + switch (enrollmentType) + { + case EnrollmentType.New: + flow.Branch("New Enrollment"); + //If they renewed an expired cert it gets here and this will not be supported + IRegistrationResponse enrollmentResponse; + if (!productInfo.ProductParameters.ContainsKey("PriorCertSN")) { - Status = 30, //failure - StatusMessage = "You cannot renew an expired cert please perform an new enrollment." - }; - } + enrollmentRequest = _requestManager.GetRegistrationRequest(productInfo, csr, san, customFields); + Logger.LogTrace($"Enrollment Request JSON: {JsonConvert.SerializeObject(enrollmentRequest)}"); + enrollmentResponse = await flow.StepAsync("SubmitRegistration", + () => CscGlobalClient.SubmitRegistrationAsync(enrollmentRequest)); + Logger.LogTrace($"Enrollment Response JSON: {JsonConvert.SerializeObject(enrollmentResponse)}"); + } + else + { + Logger.LogWarning("Cannot renew an expired cert via new enrollment; a new enrollment must be performed instead"); + flow.Fail("New Enrollment", "Attempted to renew an expired cert via new enrollment"); + flow.EndBranch(); + return new EnrollmentResult + { + Status = 30, //failure + StatusMessage = "You cannot renew an expired cert please perform an new enrollment." + }; + } - Logger.MethodExit(LogLevel.Debug); - return _requestManager.GetEnrollmentResult(enrollmentResponse); - case EnrollmentType.RenewOrReissue: - Logger.LogTrace("Entering Renew Enrollment"); - //Logic to determine renew vs reissue - var renewal = false; - var order_id = await _certificateDataReader.GetRequestIDBySerialNumber(priorSn); - var expirationDate = _certificateDataReader.GetExpirationDateByRequestId(order_id); - if (expirationDate == null) - { - var localcert = await GetSingleRecord(order_id); - expirationDate = localcert.RevocationDate; - } + flow.EndBranch(); + var newResult = _requestManager.GetEnrollmentResult(enrollmentResponse); + LogEnrollmentOutcome(newResult, "New Enrollment"); + Logger.MethodExit(LogLevel.Debug); + return newResult; + case EnrollmentType.RenewOrReissue: + flow.Branch("Renew Or Reissue"); + //Logic to determine renew vs reissue + var renewal = false; + var order_id = await _certificateDataReader.GetRequestIDBySerialNumber(priorSn); + var expirationDate = _certificateDataReader.GetExpirationDateByRequestId(order_id); + if (expirationDate == null) + { + var localcert = await GetSingleRecord(order_id); + expirationDate = localcert.RevocationDate; + } - if (expirationDate < DateTime.Now) renewal = true; - if (renewal) - { + if (expirationDate < DateTime.Now) renewal = true; + if (renewal) + { + flow.Step("DetermineRenewOrReissue", "Renewal - cert is expired"); + //One click won't work for this implementation b/c we are missing enrollment params + if (productInfo.ProductParameters.ContainsKey("Applicant Last Name")) + { + //priorCert = _certificateDataReader.get( + //DataConversion.HexToBytes(productInfo.ProductParameters["PriorCertSN"])); + //uUId = priorCert.CARequestID.Substring(0, 36); //uUId is a GUID + uUId = await _certificateDataReader.GetRequestIDBySerialNumber( + productInfo.ProductParameters["PriorCertSN"]); + Logger.LogTrace($"Renew uUId: {uUId}"); + renewRequest = _requestManager.GetRenewalRequest(productInfo, uUId, csr, san, customFields); + Logger.LogTrace($"Renewal Request JSON: {JsonConvert.SerializeObject(renewRequest)}"); + var renewResponse = await flow.StepAsync("SubmitRenewal", + () => CscGlobalClient.SubmitRenewalAsync(renewRequest)); + Logger.LogTrace($"Renewal Response JSON: {JsonConvert.SerializeObject(renewResponse)}"); + flow.EndBranch(); + var renewResult = _requestManager.GetRenewResponse(renewResponse); + LogEnrollmentOutcome(renewResult, "Renewal"); + Logger.MethodExit(LogLevel.Debug); + return renewResult; + } + + Logger.LogWarning($"One click renew is not available for product {productInfo.ProductID}; missing required enrollment parameters"); + flow.Fail("Renewal", "One click renew is not available; missing Applicant Last Name"); + flow.EndBranch(); + return new EnrollmentResult + { + Status = 30, //failure + StatusMessage = + "One click Renew Is Not Available for this Certificate Type. Use the configure button instead." + }; + } + + flow.Step("DetermineRenewOrReissue", "Reissue - cert is still valid"); //One click won't work for this implementation b/c we are missing enrollment params if (productInfo.ProductParameters.ContainsKey("Applicant Last Name")) { - //priorCert = _certificateDataReader.get( - //DataConversion.HexToBytes(productInfo.ProductParameters["PriorCertSN"])); - //uUId = priorCert.CARequestID.Substring(0, 36); //uUId is a GUID - uUId = await _certificateDataReader.GetRequestIDBySerialNumber( + var requestid = await _certificateDataReader.GetRequestIDBySerialNumber( productInfo.ProductParameters["PriorCertSN"]); - Logger.LogTrace($"Renew uUId: {uUId}"); - renewRequest = _requestManager.GetRenewalRequest(productInfo, uUId, csr, san, customFields); - Logger.LogTrace($"Renewal Request JSON: {JsonConvert.SerializeObject(renewRequest)}"); - var renewResponse = Task.Run(async () => await CscGlobalClient.SubmitRenewalAsync(renewRequest)) - .Result; - Logger.LogTrace($"Renewal Response JSON: {JsonConvert.SerializeObject(renewResponse)}"); + uUId = requestid.Substring(0, 36); //uUId is a GUID + Logger.LogTrace($"Reissue uUId: {uUId}"); + reissueRequest = _requestManager.GetReissueRequest(productInfo, uUId, csr, san, customFields); + Logger.LogTrace($"Reissue JSON: {JsonConvert.SerializeObject(reissueRequest)}"); + var reissueResponse = await flow.StepAsync("SubmitReissue", + () => CscGlobalClient.SubmitReissueAsync(reissueRequest)); + Logger.LogTrace($"Reissue Response JSON: {JsonConvert.SerializeObject(reissueResponse)}"); + flow.EndBranch(); + var reissueResult = _requestManager.GetReIssueResult(reissueResponse); + LogEnrollmentOutcome(reissueResult, "Reissue"); Logger.MethodExit(LogLevel.Debug); - return _requestManager.GetRenewResponse(renewResponse); + return reissueResult; } + Logger.LogWarning($"One click reissue is not available for product {productInfo.ProductID}; missing required enrollment parameters"); + flow.Fail("Reissue", "One click reissue is not available; missing Applicant Last Name"); + flow.EndBranch(); return new EnrollmentResult { Status = 30, //failure StatusMessage = "One click Renew Is Not Available for this Certificate Type. Use the configure button instead." }; - } - - Logger.LogTrace("Entering Reissue Enrollment"); - //One click won't work for this implementation b/c we are missing enrollment params - if (productInfo.ProductParameters.ContainsKey("Applicant Last Name")) - { - var requestid = await _certificateDataReader.GetRequestIDBySerialNumber( - productInfo.ProductParameters["PriorCertSN"]); - uUId = requestid.Substring(0, 36); //uUId is a GUID - Logger.LogTrace($"Reissue uUId: {uUId}"); - reissueRequest = _requestManager.GetReissueRequest(productInfo, uUId, csr, san, customFields); - Logger.LogTrace($"Reissue JSON: {JsonConvert.SerializeObject(reissueRequest)}"); - var reissueResponse = Task.Run(async () => await CscGlobalClient.SubmitReissueAsync(reissueRequest)) - .Result; - Logger.LogTrace($"Reissue Response JSON: {JsonConvert.SerializeObject(reissueResponse)}"); - Logger.MethodExit(LogLevel.Debug); - return _requestManager.GetReIssueResult(reissueResponse); - } + } - return new EnrollmentResult - { - Status = 30, //failure - StatusMessage = - "One click Renew Is Not Available for this Certificate Type. Use the configure button instead." - }; + Logger.LogWarning($"Unhandled enrollment type {enrollmentType} for product {productInfo.ProductID}"); + Logger.MethodExit(LogLevel.Debug); + return null; } + catch (Exception e) + { + Logger.LogError(e, $"Enroll failed for product {productInfo.ProductID}: {e.Message}"); + flow.Fail("Enroll", e.Message); + throw; + } + } - Logger.MethodExit(LogLevel.Debug); - return null; + private void LogEnrollmentOutcome(EnrollmentResult result, string operationName) + { + if (result == null) return; + if (result.Status == (int)EndEntityStatus.FAILED) + Logger.LogError($"{operationName} failed: {result.StatusMessage}"); + else + Logger.LogInformation($"{operationName} succeeded: {result.StatusMessage}"); } //done @@ -330,7 +429,7 @@ public async Task Ping() } catch (Exception e) { - Logger.LogError($"There was an error contacting CSCGlobal: {e.Message}."); + Logger.LogError(e, $"There was an error contacting CSCGlobal: {e.Message}."); throw new Exception($"Error attempting to ping CSCGlobal: {e.Message}.", e); } @@ -340,19 +439,27 @@ public async Task Ping() //do public async Task ValidateCAConnectionInfo(Dictionary connectionInfo) { + Logger.MethodEntry(LogLevel.Debug); + Logger.LogDebug($"Validating CA connection info with {connectionInfo?.Count ?? 0} entries"); + Logger.MethodExit(LogLevel.Debug); } //do public async Task ValidateProductInfo(EnrollmentProductInfo productInfo, Dictionary connectionInfo) { + Logger.MethodEntry(LogLevel.Debug); var certType = ProductIDs.productIds.Find(x => x.Equals(productInfo.ProductID, StringComparison.InvariantCultureIgnoreCase)); - if (certType == null) throw new ArgumentException($"Cannot find {productInfo.ProductID}", "ProductId"); + if (certType == null) + { + Logger.LogError($"Cannot find product ID {productInfo.ProductID} in the list of supported CSC Global products"); + throw new ArgumentException($"Cannot find {productInfo.ProductID}", "ProductId"); + } Logger.LogInformation($"Validated {certType} ({certType})configured for AnyGateway"); - + Logger.MethodExit(LogLevel.Debug); } //done diff --git a/cscglobal-caplugin/Client/CscGlobalClient.cs b/cscglobal-caplugin/Client/CscGlobalClient.cs index 0a5c7c5..3429dc5 100644 --- a/cscglobal-caplugin/Client/CscGlobalClient.cs +++ b/cscglobal-caplugin/Client/CscGlobalClient.cs @@ -23,13 +23,18 @@ public sealed class CscGlobalClient : ICscGlobalClient public CscGlobalClient(IAnyCAPluginConfigProvider config) { - Logger = LogHandler.GetClassLogger(); + Logger = LogHandler.GetClassLogger(); if (config.CAConnectionData.ContainsKey(Constants.CscGlobalApiKey)) { BaseUrl = new Uri(config.CAConnectionData[Constants.CscGlobalUrl].ToString()); ApiKey = config.CAConnectionData[Constants.CscGlobalApiKey].ToString(); Authorization = config.CAConnectionData[Constants.BearerToken].ToString(); RestClient = ConfigureRestClient(); + Logger.LogDebug($"CscGlobalClient configured for base URL {BaseUrl}"); + } + else + { + Logger.LogError($"CA connection data is missing required key '{Constants.CscGlobalApiKey}'; client will not be able to call the CSC Global API"); } } @@ -41,6 +46,7 @@ public CscGlobalClient(IAnyCAPluginConfigProvider config) public async Task SubmitRegistrationAsync( RegistrationRequest registerRequest) { + Logger.MethodEntry(LogLevel.Debug); using (var resp = await RestClient.PostAsync("/dbs/api/v2/tls/registration", new StringContent( JsonConvert.SerializeObject(registerRequest), Encoding.ASCII, "application/json"))) { @@ -48,18 +54,23 @@ public async Task SubmitRegistrationAsync( var settings = new JsonSerializerSettings { NullValueHandling = NullValueHandling.Ignore }; if (resp.StatusCode == HttpStatusCode.BadRequest) //Csc Sends Errors back in 400 Json Response { + var rawErrorResponse = await resp.Content.ReadAsStringAsync(); var errorResponse = - JsonConvert.DeserializeObject(await resp.Content.ReadAsStringAsync(), - settings); + JsonConvert.DeserializeObject(rawErrorResponse, settings); + Logger.LogWarning($"Registration request rejected by CSC Global: {errorResponse?.Description ?? rawErrorResponse}"); var response = new RegistrationResponse(); response.RegistrationError = errorResponse; response.Result = null; return response; } + if (!resp.IsSuccessStatusCode) + Logger.LogError($"Registration request failed with status code {resp.StatusCode}"); + var registrationResponse = JsonConvert.DeserializeObject(await resp.Content.ReadAsStringAsync(), settings); + Logger.MethodExit(LogLevel.Debug); return registrationResponse; } } @@ -81,12 +92,16 @@ public async Task SubmitRenewalAsync( var errorResponse = JsonConvert.DeserializeObject(rawErrorResponse, settings); + Logger.LogWarning($"Renewal request rejected by CSC Global: {errorResponse?.Description ?? rawErrorResponse}"); var response = new RenewalResponse(); response.RegistrationError = errorResponse; response.Result = null; return response; } + if (!resp.IsSuccessStatusCode) + Logger.LogError($"Renewal request failed with status code {resp.StatusCode}"); + var rawRenewResponse = await resp.Content.ReadAsStringAsync(); Logger.LogTrace("Logging Success Response Raw"); Logger.LogTrace(rawRenewResponse); @@ -107,15 +122,19 @@ public async Task SubmitReissueAsync( var settings = new JsonSerializerSettings { NullValueHandling = NullValueHandling.Ignore }; if (resp.StatusCode == HttpStatusCode.BadRequest) //Csc Sends Errors back in 400 Json Response { + var rawErrorResponse = await resp.Content.ReadAsStringAsync(); var errorResponse = - JsonConvert.DeserializeObject(await resp.Content.ReadAsStringAsync(), - settings); + JsonConvert.DeserializeObject(rawErrorResponse, settings); + Logger.LogWarning($"Reissue request rejected by CSC Global: {errorResponse?.Description ?? rawErrorResponse}"); var response = new ReissueResponse(); response.RegistrationError = errorResponse; response.Result = null; return response; } + if (!resp.IsSuccessStatusCode) + Logger.LogError($"Reissue request failed with status code {resp.StatusCode}"); + var reissueResponse = JsonConvert.DeserializeObject(await resp.Content.ReadAsStringAsync()); return reissueResponse; @@ -124,44 +143,69 @@ public async Task SubmitReissueAsync( public async Task SubmitGetCertificateAsync(string certificateId) { + Logger.MethodEntry(LogLevel.Debug); + Logger.LogTrace($"Getting certificate with ID {certificateId}"); using (var resp = await RestClient.GetAsync($"/dbs/api/v2/tls/certificate/{certificateId}")) { + if (!resp.IsSuccessStatusCode) + { + var errorBody = await resp.Content.ReadAsStringAsync(); + Logger.LogError($"Failed to get certificate {certificateId}. Status code {resp.StatusCode} | Message: {errorBody}"); + } + resp.EnsureSuccessStatusCode(); var getCertificateResponse = JsonConvert.DeserializeObject(await resp.Content.ReadAsStringAsync()); + Logger.MethodExit(LogLevel.Debug); return getCertificateResponse; } } public async Task> SubmitGetCustomFields() { + Logger.MethodEntry(LogLevel.Debug); using (var resp = await RestClient.GetAsync("/dbs/api/v2/admin/customfields")) { + if (!resp.IsSuccessStatusCode) + { + var errorBody = await resp.Content.ReadAsStringAsync(); + Logger.LogError($"Failed to get custom fields. Status code {resp.StatusCode} | Message: {errorBody}"); + } + resp.EnsureSuccessStatusCode(); var getCustomFieldsResponse = JsonConvert.DeserializeObject(await resp.Content.ReadAsStringAsync()); + Logger.LogTrace($"Retrieved {getCustomFieldsResponse.CustomFields?.Count ?? 0} custom field(s)"); + Logger.MethodExit(LogLevel.Debug); return getCustomFieldsResponse.CustomFields; } } public async Task SubmitRevokeCertificateAsync(string uuId) { + Logger.MethodEntry(LogLevel.Debug); + Logger.LogTrace($"Revoking certificate with UUID {uuId}"); using (var resp = await RestClient.PutAsync($"/dbs/api/v2/tls/revoke/{uuId}", new StringContent(""))) { var settings = new JsonSerializerSettings { NullValueHandling = NullValueHandling.Ignore }; if (resp.StatusCode == HttpStatusCode.BadRequest) //Csc Sends Errors back in 400 Json Response { + var rawErrorResponse = await resp.Content.ReadAsStringAsync(); var errorResponse = - JsonConvert.DeserializeObject(await resp.Content.ReadAsStringAsync(), - settings); + JsonConvert.DeserializeObject(rawErrorResponse, settings); + Logger.LogWarning($"Revoke request rejected by CSC Global for UUID {uuId}: {errorResponse?.Description ?? rawErrorResponse}"); var response = new RevokeResponse(); response.RegistrationError = errorResponse; response.RevokeSuccess = null; return response; } + if (!resp.IsSuccessStatusCode) + Logger.LogError($"Revoke request for UUID {uuId} failed with status code {resp.StatusCode}"); + var getRevokeResponse = JsonConvert.DeserializeObject(await resp.Content.ReadAsStringAsync()); + Logger.MethodExit(LogLevel.Debug); return getRevokeResponse; } } @@ -186,6 +230,8 @@ public async Task SubmitCertificateListRequestAsync(str var certificateListResponse = JsonConvert.DeserializeObject(await resp.Content.ReadAsStringAsync()); + Logger.LogInformation($"Certificate list request returned {certificateListResponse?.Results?.Count ?? 0} result(s)"); + Logger.MethodExit(LogLevel.Debug); return certificateListResponse; } diff --git a/cscglobal-caplugin/FlowLogger.cs b/cscglobal-caplugin/FlowLogger.cs new file mode 100644 index 0000000..711cac9 --- /dev/null +++ b/cscglobal-caplugin/FlowLogger.cs @@ -0,0 +1,267 @@ +// Copyright 2021 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.Diagnostics; +using System.Text; +using Microsoft.Extensions.Logging; + +namespace Keyfactor.Extensions.CAPlugin.CSCGlobal; + +/// +/// Step-oriented trace logger for CA plugin pipelines. Wraps a sequence of named steps with +/// timings, branches, and outcomes into a single appended summary block that is attached +/// to on both success and failure, giving +/// operators a single scannable breadcrumb trail per plugin operation (Enroll, Revoke, Sync). +/// +public class FlowLogger : IDisposable +{ + private readonly ILogger _logger; + private readonly string _flowName; + private readonly Stopwatch _overallStopwatch; + private readonly List _steps = new(); + private readonly Stack _branchStack = new(); + + public FlowLogger(ILogger logger, string flowName) + { + _logger = logger ?? throw new ArgumentNullException(nameof(logger)); + _flowName = flowName ?? throw new ArgumentNullException(nameof(flowName)); + _overallStopwatch = Stopwatch.StartNew(); + _logger.LogTrace("[FLOW:{FlowName}] === BEGIN ===", _flowName); + } + + public void Step(string name, string detail = null) + { + var step = new FlowStep { Name = name, Detail = detail, Status = StepStatus.Success }; + _steps.Add(step); + var prefix = GetPrefix(); + if (detail != null) + _logger.LogTrace("[FLOW:{FlowName}] {Prefix}[OK] {StepName} - {Detail}", _flowName, prefix, name, detail); + else + _logger.LogTrace("[FLOW:{FlowName}] {Prefix}[OK] {StepName}", _flowName, prefix, name); + } + + public void Step(string name, Action action, string detail = null) + { + var sw = Stopwatch.StartNew(); + var step = new FlowStep { Name = name, Detail = detail }; + try + { + action(); + sw.Stop(); + step.Status = StepStatus.Success; + step.ElapsedMs = sw.ElapsedMilliseconds; + _steps.Add(step); + var prefix = GetPrefix(); + _logger.LogTrace("[FLOW:{FlowName}] {Prefix}[OK] {StepName} ({Elapsed}ms){DetailSuffix}", + _flowName, prefix, name, sw.ElapsedMilliseconds, FormatDetail(detail)); + } + catch (Exception ex) + { + sw.Stop(); + step.Status = StepStatus.Failed; + step.ElapsedMs = sw.ElapsedMilliseconds; + step.ErrorMessage = ex.Message; + _steps.Add(step); + var prefix = GetPrefix(); + _logger.LogTrace("[FLOW:{FlowName}] {Prefix}[FAIL] {StepName} ({Elapsed}ms) - {Error}", + _flowName, prefix, name, sw.ElapsedMilliseconds, ex.Message); + throw; + } + } + + public async Task StepAsync(string name, Func action, string detail = null) + { + var sw = Stopwatch.StartNew(); + var step = new FlowStep { Name = name, Detail = detail }; + try + { + await action(); + sw.Stop(); + step.Status = StepStatus.Success; + step.ElapsedMs = sw.ElapsedMilliseconds; + _steps.Add(step); + var prefix = GetPrefix(); + _logger.LogTrace("[FLOW:{FlowName}] {Prefix}[OK] {StepName} ({Elapsed}ms){DetailSuffix}", + _flowName, prefix, name, sw.ElapsedMilliseconds, FormatDetail(detail)); + } + catch (Exception ex) + { + sw.Stop(); + step.Status = StepStatus.Failed; + step.ElapsedMs = sw.ElapsedMilliseconds; + step.ErrorMessage = ex.Message; + _steps.Add(step); + var prefix = GetPrefix(); + _logger.LogTrace("[FLOW:{FlowName}] {Prefix}[FAIL] {StepName} ({Elapsed}ms) - {Error}", + _flowName, prefix, name, sw.ElapsedMilliseconds, ex.Message); + throw; + } + } + + public async Task StepAsync(string name, Func> action, string detail = null) + { + var sw = Stopwatch.StartNew(); + var step = new FlowStep { Name = name, Detail = detail }; + try + { + var result = await action(); + sw.Stop(); + step.Status = StepStatus.Success; + step.ElapsedMs = sw.ElapsedMilliseconds; + _steps.Add(step); + var prefix = GetPrefix(); + _logger.LogTrace("[FLOW:{FlowName}] {Prefix}[OK] {StepName} ({Elapsed}ms){DetailSuffix}", + _flowName, prefix, name, sw.ElapsedMilliseconds, FormatDetail(detail)); + return result; + } + catch (Exception ex) + { + sw.Stop(); + step.Status = StepStatus.Failed; + step.ElapsedMs = sw.ElapsedMilliseconds; + step.ErrorMessage = ex.Message; + _steps.Add(step); + var prefix = GetPrefix(); + _logger.LogTrace("[FLOW:{FlowName}] {Prefix}[FAIL] {StepName} ({Elapsed}ms) - {Error}", + _flowName, prefix, name, sw.ElapsedMilliseconds, ex.Message); + throw; + } + } + + public T Step(string name, Func action, string detail = null) + { + var sw = Stopwatch.StartNew(); + var step = new FlowStep { Name = name, Detail = detail }; + try + { + var result = action(); + sw.Stop(); + step.Status = StepStatus.Success; + step.ElapsedMs = sw.ElapsedMilliseconds; + _steps.Add(step); + var prefix = GetPrefix(); + _logger.LogTrace("[FLOW:{FlowName}] {Prefix}[OK] {StepName} ({Elapsed}ms){DetailSuffix}", + _flowName, prefix, name, sw.ElapsedMilliseconds, FormatDetail(detail)); + return result; + } + catch (Exception ex) + { + sw.Stop(); + step.Status = StepStatus.Failed; + step.ElapsedMs = sw.ElapsedMilliseconds; + step.ErrorMessage = ex.Message; + _steps.Add(step); + var prefix = GetPrefix(); + _logger.LogTrace("[FLOW:{FlowName}] {Prefix}[FAIL] {StepName} ({Elapsed}ms) - {Error}", + _flowName, prefix, name, sw.ElapsedMilliseconds, ex.Message); + throw; + } + } + + public void Fail(string name, string reason) + { + var step = new FlowStep { Name = name, Status = StepStatus.Failed, ErrorMessage = reason }; + _steps.Add(step); + var prefix = GetPrefix(); + _logger.LogTrace("[FLOW:{FlowName}] {Prefix}[FAIL] {StepName} - {Reason}", _flowName, prefix, name, reason); + } + + public void Skip(string name, string reason) + { + var step = new FlowStep { Name = name, Status = StepStatus.Skipped, Detail = reason }; + _steps.Add(step); + var prefix = GetPrefix(); + _logger.LogTrace("[FLOW:{FlowName}] {Prefix}[SKIP] {StepName} - {Reason}", _flowName, prefix, name, reason); + } + + public void Branch(string name) + { + _branchStack.Push(name); + var prefix = GetPrefix(); + _logger.LogTrace("[FLOW:{FlowName}] {Prefix}>> {BranchName}", _flowName, prefix, name); + } + + public void EndBranch() + { + if (_branchStack.Count > 0) + { + var name = _branchStack.Pop(); + var prefix = GetPrefix(); + _logger.LogTrace("[FLOW:{FlowName}] {Prefix}<< {BranchName}", _flowName, prefix, name); + } + } + + public bool HasFailures => _steps.Any(s => s.Status == StepStatus.Failed); + + public string GetSummary() + { + var hasFailures = HasFailures; + var overallStatus = hasFailures ? "FAILED" : "OK"; + var total = _steps.Count; + var succeeded = _steps.Count(s => s.Status == StepStatus.Success); + var failed = _steps.Count(s => s.Status == StepStatus.Failed); + var skipped = _steps.Count(s => s.Status == StepStatus.Skipped); + var elapsed = _overallStopwatch.ElapsedMilliseconds; + + var sb = new StringBuilder(); + sb.AppendLine($"Flow: {_flowName} [{overallStatus}] Total: {elapsed}ms"); + sb.AppendLine($"Steps: {total} total, {succeeded} ok, {failed} failed, {skipped} skipped"); + sb.AppendLine("----------------------------------------"); + foreach (var step in _steps) + { + var icon = step.Status == StepStatus.Success ? "[OK] " + : step.Status == StepStatus.Failed ? "[FAIL]" + : step.Status == StepStatus.Skipped ? "[SKIP]" + : "[...]"; + var time = step.ElapsedMs.HasValue ? $" ({step.ElapsedMs}ms)" : ""; + var detail = !string.IsNullOrEmpty(step.ErrorMessage) + ? $" - {step.ErrorMessage}" + : !string.IsNullOrEmpty(step.Detail) + ? $" - {step.Detail}" + : ""; + sb.AppendLine($" {icon} {step.Name}{time}{detail}"); + } + sb.Append("----------------------------------------"); + + return sb.ToString(); + } + + public void Dispose() + { + _overallStopwatch.Stop(); + var summary = GetSummary(); + _logger.LogTrace("[FLOW:{FlowName}] === END ===\n{Summary}", _flowName, summary); + } + + private string GetPrefix() + { + if (_branchStack.Count == 0) return ""; + return new string(' ', _branchStack.Count * 2) + "| "; + } + + private static string FormatDetail(string detail) + { + return string.IsNullOrEmpty(detail) ? "" : $" - {detail}"; + } + + private enum StepStatus + { + Success, + Failed, + Skipped, + InProgress + } + + private class FlowStep + { + public string Name { get; set; } + public string Detail { get; set; } + public StepStatus Status { get; set; } = StepStatus.InProgress; + public long? ElapsedMs { get; set; } + public string ErrorMessage { get; set; } + } +} diff --git a/cscglobal-caplugin/RequestManager.cs b/cscglobal-caplugin/RequestManager.cs index 94ce704..14859cd 100644 --- a/cscglobal-caplugin/RequestManager.cs +++ b/cscglobal-caplugin/RequestManager.cs @@ -10,12 +10,16 @@ using Keyfactor.AnyGateway.Extensions; using Keyfactor.Extensions.CAPlugin.CSCGlobal.Client.Models; using Keyfactor.Extensions.CAPlugin.CSCGlobal.Interfaces; +using Keyfactor.Logging; using Keyfactor.PKI.Enums.EJBCA; +using Microsoft.Extensions.Logging; namespace Keyfactor.Extensions.CAPlugin.CSCGlobal; public class RequestManager { + private readonly ILogger Logger = LogHandler.GetClassLogger(); + public static Func Pemify = ss => ss.Length <= 64 ? ss : ss.Substring(0, 64) + "\n" + Pemify(ss.Substring(64)); @@ -34,23 +38,29 @@ private List GetCustomFields(EnrollmentProductInfo productInfo, Lis } else if (field.Mandatory) { + Logger.LogError($"Custom field {field.Label} is marked as mandatory, but was not supplied in the request."); throw new Exception( $"Custom field {field.Label} is marked as mandatory, but was not supplied in the request."); } + Logger.LogTrace($"Mapped {customFieldList.Count} custom field(s) for request"); return customFieldList; } public EnrollmentResult GetRenewResponse(RenewalResponse renewResponse) { if (renewResponse.RegistrationError != null) + { + Logger.LogError($"Renewal failed: {renewResponse.RegistrationError.Description}"); return new EnrollmentResult { Status = (int)EndEntityStatus.FAILED, //failure CARequestID = renewResponse?.Result?.Status?.Uuid, StatusMessage = renewResponse.RegistrationError.Description }; + } + Logger.LogInformation($"Renewal successfully completed for {renewResponse.Result.CommonName}"); return new EnrollmentResult { Status = (int)EndEntityStatus.GENERATED, //success @@ -65,11 +75,14 @@ public EnrollmentResult IRegistrationResponse registrationResponse) { if (registrationResponse.RegistrationError != null) + { + Logger.LogError($"Enrollment failed: {registrationResponse.RegistrationError.Description}"); return new EnrollmentResult { Status = (int)EndEntityStatus.FAILED, //failure StatusMessage = registrationResponse.RegistrationError.Description }; + } var cnames = new Dictionary(); if (registrationResponse.Result.DcvDetails != null && registrationResponse.Result.DcvDetails.Count > 0) @@ -86,6 +99,7 @@ public EnrollmentResult } } + Logger.LogInformation($"Order successfully created with order number {registrationResponse.Result.CommonName}"); return new EnrollmentResult { Status = (int)EndEntityStatus.EXTERNALVALIDATION, //success @@ -99,7 +113,10 @@ public EnrollmentResult public int GetRevokeResult(IRevokeResponse revokeResponse) { if (revokeResponse.RegistrationError != null) + { + Logger.LogError($"Revoke failed: {revokeResponse.RegistrationError.Description}"); return (int)EndEntityStatus.FAILED; + } return (int)EndEntityStatus.REVOKED; } @@ -107,12 +124,16 @@ public int GetRevokeResult(IRevokeResponse revokeResponse) public EnrollmentResult GetReIssueResult(IReissueResponse reissueResponse) { if (reissueResponse.RegistrationError != null) + { + Logger.LogError($"Reissue failed: {reissueResponse.RegistrationError.Description}"); return new EnrollmentResult { Status = (int)EndEntityStatus.FAILED, //failure StatusMessage = reissueResponse.RegistrationError.Description }; + } + Logger.LogInformation($"Reissue successfully completed for {reissueResponse.Result.CommonName}"); return new EnrollmentResult { Status = (int)EndEntityStatus.GENERATED, //success @@ -135,6 +156,7 @@ public DomainControlValidation GetDomainControlValidation(string methodType, str }; } + Logger.LogWarning($"No matching DCV email address found for domain {domainName}"); return null; } @@ -150,6 +172,7 @@ public DomainControlValidation GetDomainControlValidation(string methodType, str public RegistrationRequest GetRegistrationRequest(EnrollmentProductInfo productInfo, string csr, Dictionary sans, List customFields) { + Logger.LogTrace($"Building registration request for product {productInfo.ProductID}"); //var cert = "-----BEGIN CERTIFICATE REQUEST-----\r\n"; var cert = Pemify(csr); //cert = cert + "\r\n-----END CERTIFICATE REQUEST-----"; @@ -214,6 +237,7 @@ private string GetCertificateType(string productId) return "9"; } + Logger.LogWarning($"Unrecognized product ID '{productId}'; defaulting certificate type to -1"); return "-1"; } @@ -233,6 +257,7 @@ public Notifications GetNotifications(EnrollmentProductInfo productInfo) public RenewalRequest GetRenewalRequest(EnrollmentProductInfo productInfo, string uUId, string csr, Dictionary sans, List customFields) { + Logger.LogTrace($"Building renewal request for product {productInfo.ProductID}, UUID {uUId}"); //var cert = "-----BEGIN CERTIFICATE REQUEST-----\r\n"; var cert = Pemify(csr); //cert = cert + "\r\n-----END CERTIFICATE REQUEST-----"; @@ -291,6 +316,7 @@ private List GetSubjectAlternativeNames(EnrollmentProduc public ReissueRequest GetReissueRequest(EnrollmentProductInfo productInfo, string uUId, string csr, Dictionary sans, List customFields) { + Logger.LogTrace($"Building reissue request for product {productInfo.ProductID}, UUID {uUId}"); //var cert = "-----BEGIN CERTIFICATE REQUEST-----\r\n"; var cert = Pemify(csr); //cert = cert + "\r\n-----END CERTIFICATE REQUEST-----"; @@ -349,6 +375,7 @@ public int MapReturnStatus(string cscGlobalStatus) returnStatus = (int)EndEntityStatus.REVOKED; break; default: + Logger.LogWarning($"Unrecognized CSC Global status '{cscGlobalStatus}'; mapping to FAILED"); returnStatus = (int)EndEntityStatus.FAILED; break; } From 2488f3544b14c8385e359e44d1bfd1be999a6448 Mon Sep 17 00:00:00 2001 From: Brian Hill <76450501+bhillkeyfactor@users.noreply.github.com> Date: Fri, 28 Aug 2026 09:20:06 -0400 Subject: [PATCH 4/9] Update keyfactor-bootstrap-workflow-v3.yml --- .github/workflows/keyfactor-bootstrap-workflow-v3.yml | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/.github/workflows/keyfactor-bootstrap-workflow-v3.yml b/.github/workflows/keyfactor-bootstrap-workflow-v3.yml index 042ba5a..0f3d3ae 100644 --- a/.github/workflows/keyfactor-bootstrap-workflow-v3.yml +++ b/.github/workflows/keyfactor-bootstrap-workflow-v3.yml @@ -11,10 +11,17 @@ on: jobs: call-starter-workflow: - uses: keyfactor/actions/.github/workflows/starter.yml@v3.1.2 + uses: keyfactor/actions/.github/workflows/starter.yml@v5 + 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 }} From bb6b6589c8939c208bda3232565b4968ff7b237f Mon Sep 17 00:00:00 2001 From: Brian Hill Date: Fri, 28 Aug 2026 09:09:03 -0400 Subject: [PATCH 5/9] Fix KeyNotFoundException when Addtl Sans Comma Separated DVC Emails is unset Only look up the optional additional-SAN-emails field when the domain control validation method is EMAIL, and use TryGetValue instead of the indexer so a missing/blank value no longer throws. --- cscglobal-caplugin/RequestManager.cs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/cscglobal-caplugin/RequestManager.cs b/cscglobal-caplugin/RequestManager.cs index 14859cd..8557672 100644 --- a/cscglobal-caplugin/RequestManager.cs +++ b/cscglobal-caplugin/RequestManager.cs @@ -301,9 +301,14 @@ private List GetSubjectAlternativeNames(EnrollmentProduc var domainName = v; var san = new SubjectAlternativeName(); san.DomainName = domainName; - var emailAddresses = productInfo.ProductParameters["Addtl Sans Comma Separated DVC Emails"].Split(','); if (methodType.ToUpper() == "EMAIL") + { + productInfo.ProductParameters.TryGetValue("Addtl Sans Comma Separated DVC Emails", out var addtlSansEmails); + var emailAddresses = string.IsNullOrWhiteSpace(addtlSansEmails) + ? Array.Empty() + : addtlSansEmails.Split(','); san.DomainControlValidation = GetDomainControlValidation(methodType, emailAddresses, domainName); + } else //it is a CNAME validation so no email is needed san.DomainControlValidation = GetDomainControlValidation(methodType, ""); From b9d0bdd1136b1df187c54d4b00b64634dce5b918 Mon Sep 17 00:00:00 2001 From: Brian Hill Date: Fri, 28 Aug 2026 10:51:09 -0400 Subject: [PATCH 6/9] Fix KeyNotFoundException in GetSubjectAlternativeNames when sans has no 'dnsname' key UC certificate enrollments with zero SANs supplied threw KeyNotFoundException from the sans["dnsname"] indexer. Use TryGetValue and treat a missing key as no SANs instead of throwing. --- cscglobal-caplugin/RequestManager.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/cscglobal-caplugin/RequestManager.cs b/cscglobal-caplugin/RequestManager.cs index 8557672..72f9272 100644 --- a/cscglobal-caplugin/RequestManager.cs +++ b/cscglobal-caplugin/RequestManager.cs @@ -296,7 +296,8 @@ private List GetSubjectAlternativeNames(EnrollmentProduc var subjectNameList = new List(); var methodType = productInfo.ProductParameters["Domain Control Validation Method"]; - foreach (var v in sans["dnsname"]) + sans.TryGetValue("dnsname", out var dnsNames); + foreach (var v in dnsNames ?? Array.Empty()) { var domainName = v; var san = new SubjectAlternativeName(); From 35e488e388c5d29e1ad498651c7b5a83cbc27a67 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Wed, 9 Sep 2026 20:49:56 +0000 Subject: [PATCH 7/9] docs: auto-generate README and documentation [skip ci] --- README.md | 623 +++++++++++++++++++++++++++--------------------------- 1 file changed, 310 insertions(+), 313 deletions(-) diff --git a/README.md b/README.md index f61fd6d..3ffe2b0 100644 --- a/README.md +++ b/README.md @@ -14,7 +14,7 @@ Support - + · Requirements @@ -33,7 +33,6 @@

- This integration allows for the Synchronization, Enrollment, and Revocation of certificates from the CSCGlobal. This is the AnyGateway REST version. ## Compatibility @@ -41,7 +40,7 @@ This integration allows for the Synchronization, Enrollment, and Revocation of c The CSCGlobal CAPlugin AnyCA Gateway REST plugin is compatible with the Keyfactor AnyCA Gateway REST 24.2.0 and later. ## Support -The CSCGlobal CAPlugin AnyCA Gateway REST plugin is supported by Keyfactor for Keyfactor customers. If you have a support issue, please open a support ticket with your Keyfactor representative. If you have a support issue, please open a support ticket via the Keyfactor Support Portal at https://support.keyfactor.com. +The CSCGlobal CAPlugin AnyCA Gateway REST plugin is supported by Keyfactor for Keyfactor customers. If you have a support issue, please open a support ticket via the Keyfactor Support Portal at https://support.keyfactor.com. > To report a problem or suggest a new feature, use the **[Issues](../../issues)** tab. If you want to contribute actual bug fixes or proposed enhancements, use the **[Pull requests](../../pulls)** tab. @@ -82,323 +81,321 @@ This integration is tested and confirmed as working for Anygateway REST 24.2 and Populate using the configuration fields collected in the [requirements](#requirements) section. - * **CscGlobalUrl** - CSCGlobal API URL - * **ApiKey** - CSCGlobal API Key - * **BearerToken** - CSCGlobal Bearer Token - * **DefaultPageSize** - Default page size for use with the API. Default is 100 - * **TemplateSync** - Enable template sync. - * **SyncFilterDays** - Number of days from today to filter certificates by expiration date during incremental sync. + * **CscGlobalUrl** - CSCGlobal API URL + * **ApiKey** - CSCGlobal API Key + * **BearerToken** - CSCGlobal Bearer Token + * **DefaultPageSize** - Default page size for use with the API. Default is 100 + * **TemplateSync** - Enable template sync. + * **SyncFilterDays** - Number of days from today to filter certificates by expiration date during incremental sync. 2. PLEASE NOTE, AT THIS TIME THE RAPID_SSL TEMPLATE IS NOT SUPPORTED BY THE CSC API AND WILL NOT WORK WITH THIS INTEGRATION - The following certificate templates are supported. Please set up the key sizes accordingly in the Certificate Profile menu of Anygateway REST, then enter the remaining details - and the Enrollment Fields for each Template accordingly using the Certificate Templates section in Command. If you would like to set up default values for enrollment parameters, you can do so the in the Certificate Template Menu of Anygateway REST. - If a field value is specified as both an Enrollment Field in Command and in the Certificate Template Menu in the REST Gateway, the value in the Enrollment Field will take precedence. - - CONFIG ELEMENT | DESCRIPTION - ----------------------------|------------------ - Template Short Name | CSC TrustedSecure Premium Certificate - Template Display Name | CSC TrustedSecure Premium Certificate - Friendly Name | CSC TrustedSecure Premium Certificate - Keys Size | 2048 - Enforce RFC 2818 Compliance | True - CSR Enrollment | True - Pfx Enrollment | True - - - **CSC TrustedSecure Premium Certificate - Enrollment Fields** - - NAME | DATA TYPE | VALUES - -----|--------------|----------------- - Term | Multiple Choice | 12,24 - Applicant First Name | String | N/A - Applicant Last Name | String | N/A - Applicant Email Address | String | N/A - Applicant Phone | String | N/A - Domain Control Validation Method | Multiple Choice | EMAIL - Organization Contact | Multiple Choice | Get From CSC Differs For Clients - Business Unit | Multiple Choice | Get From CSC Differs For Clients - Notification Email(s) Comma Separated | String | N/A - CN DCV Email | String | N/A - - **CSC TrustedSecure EV Certificate - Details Tab** - - CONFIG ELEMENT | DESCRIPTION - ----------------------------|------------------ - Template Short Name | CSC TrustedSecure EV Certificate - Template Display Name | CSC TrustedSecure EV Certificate - Friendly Name | CSC TrustedSecure EV Certificate - Keys Size | 2048 - Enforce RFC 2818 Compliance | True - CSR Enrollment | True - Pfx Enrollment | True - - - **CSC TrustedSecure EV Certificate - Enrollment Fields** - - NAME | DATA TYPE | VALUES - -----|--------------|----------------- - Term | Multiple Choice | 12,24 - Applicant First Name | String | N/A - Applicant Last Name | String | N/A - Applicant Email Address | String | N/A - Applicant Phone | String | N/A - Domain Control Validation Method | Multiple Choice | EMAIL - Organization Contact | Multiple Choice | Get From CSC Differs For Clients - Business Unit | Multiple Choice | Get From CSC Differs For Clients - Notification Email(s) Comma Separated | String | N/A - CN DCV Email | String | N/A - Organization Country | String | N/A - - **CSC TrustedSecure UC Certificate - Details Tab** - - CONFIG ELEMENT | DESCRIPTION - ----------------------------|------------------ - Template Short Name | CSC TrustedSecure UC Certificate - Template Display Name | CSC TrustedSecure UC Certificate - Friendly Name | CSC TrustedSecure UC Certificate - Keys Size | 2048 - Enforce RFC 2818 Compliance | True - CSR Enrollment | True - Pfx Enrollment | True - - - **CSC TrustedSecure UC Certificate - Enrollment Fields** - - NAME | DATA TYPE | VALUES - -----|--------------|----------------- - Term | Multiple Choice | 12,24 - Applicant First Name | String | N/A - Applicant Last Name | String | N/A - Applicant Email Address | String | N/A - Applicant Phone | String | N/A - Domain Control Validation Method | Multiple Choice | EMAIL - Organization Contact | Multiple Choice | Get From CSC Differs For Clients - Business Unit | Multiple Choice | Get From CSC Differs For Clients - Notification Email(s) Comma Separated | String | N/A - CN DCV Email | String | N/A - Addtl Sans Comma Separated DCV Emails | String | N/A - - - **CSC TrustedSecure Premium Wildcard Certificate - Details Tab** - - CONFIG ELEMENT | DESCRIPTION - ----------------------------|------------------ - Template Short Name | CSC TrustedSecure Premium Wildcard Certificate - Template Display Name | CSC TrustedSecure Premium Wildcard Certificate - Friendly Name | CSC TrustedSecure Premium Wildcard Certificate - Keys Size | 2048 - Enforce RFC 2818 Compliance | True - CSR Enrollment | True - Pfx Enrollment | True - - - **CSC TrustedSecure Premium Wildcard Certificate - Enrollment Fields** - - NAME | DATA TYPE | VALUES - -----|--------------|----------------- - Term | Multiple Choice | 12,24 - Applicant First Name | String | N/A - Applicant Last Name | String | N/A - Applicant Email Address | String | N/A - Applicant Phone | String | N/A - Domain Control Validation Method | Multiple Choice | EMAIL - Organization Contact | Multiple Choice | Get From CSC Differs For Clients - Business Unit | Multiple Choice | Get From CSC Differs For Clients - Notification Email(s) Comma Separated | String | N/A - CN DCV Email | String | N/A - - **CSC TrustedSecure Domain Validated SSL - Details Tab** - - CONFIG ELEMENT | DESCRIPTION - ----------------------------|------------------ - Template Short Name | CSC TrustedSecure Domain Validated SSL - Template Display Name | CSC TrustedSecure Domain Validated SSL - Friendly Name | CSC TrustedSecure Domain Validated SSL - Keys Size | 2048 - Enforce RFC 2818 Compliance | True - CSR Enrollment | True - Pfx Enrollment | True - - - **CSC TrustedSecure Domain Validated SSL - Enrollment Fields** - - NAME | DATA TYPE | VALUES - -----|--------------|----------------- - Term | Multiple Choice | 12,24 - Applicant First Name | String | N/A - Applicant Last Name | String | N/A - Applicant Email Address | String | N/A - Applicant Phone | String | N/A - Domain Control Validation Method | Multiple Choice | EMAIL - Organization Contact | Multiple Choice | Get From CSC Differs For Clients - Business Unit | Multiple Choice | Get From CSC Differs For Clients - Notification Email(s) Comma Separated | String | N/A - CN DCV Email | String | N/A - - **CSC TrustedSecure Domain Validated Wildcard SSL - Details Tab** - - CONFIG ELEMENT | DESCRIPTION - ----------------------------|------------------ - Template Short Name | CSC TrustedSecure Domain Validated Wildcard SSL - Template Display Name | CSC TrustedSecure Domain Validated Wildcard SSL - Friendly Name | CSC TrustedSecure Domain Validated Wildcard SSL - Keys Size | 2048 - Enforce RFC 2818 Compliance | True - CSR Enrollment | True - Pfx Enrollment | True - - - **CSC TrustedSecure Domain Validated Wildcard SSL - Enrollment Fields** - - NAME | DATA TYPE | VALUES - -----|--------------|----------------- - Term | Multiple Choice | 12,24 - Applicant First Name | String | N/A - Applicant Last Name | String | N/A - Applicant Email Address | String | N/A - Applicant Phone | String | N/A - Domain Control Validation Method | Multiple Choice | EMAIL - Organization Contact | Multiple Choice | Get From CSC Differs For Clients - Business Unit | Multiple Choice | Get From CSC Differs For Clients - Notification Email(s) Comma Separated | String | N/A - CN DCV Email | String | N/A - - **CSC TrustedSecure Domain Validated UC Certificate - Details Tab** - - CONFIG ELEMENT | DESCRIPTION - ----------------------------|------------------ - Template Short Name | CSC TrustedSecure Domain Validated UC Certificate - Template Display Name | CSC TrustedSecure Domain Validated UC Certificate - Friendly Name | CSC TrustedSecure Domain Validated UC Certificate - Keys Size | 2048 - Enforce RFC 2818 Compliance | True - CSR Enrollment | True - Pfx Enrollment | True - - - **CSC TrustedSecure Domain Validated UC Certificate - Enrollment Fields** - - NAME | DATA TYPE | VALUES - -----|--------------|----------------- - Term | Multiple Choice | 12,24 - Applicant First Name | String | N/A - Applicant Last Name | String | N/A - Applicant Email Address | String | N/A - Applicant Phone | String | N/A - Domain Control Validation Method | Multiple Choice | EMAIL - Organization Contact | Multiple Choice | Get From CSC Differs For Clients - Business Unit | Multiple Choice | Get From CSC Differs For Clients - Notification Email(s) Comma Separated | String | N/A - CN DCV Email | String | N/A - Addtl Sans Comma Separated DCV Emails | String | N/A - - **CSC TrustedSecure EV, Multiple Names - Details Tab** - - CONFIG ELEMENT | DESCRIPTION - ----------------------------|------------------ - Template Short Name | CSC TrustedSecure EV, Multiple Names - Template Display Name | CSC TrustedSecure EV, Multiple Names - Friendly Name | CSC TrustedSecure EV, Multiple Names - Keys Size | 2048 - Enforce RFC 2818 Compliance | True - CSR Enrollment | True - Pfx Enrollment | True - - - **CSC TrustedSecure EV, Multiple Names - Enrollment Fields** - - NAME | DATA TYPE | VALUES - -----|--------------|----------------- - Term | Multiple Choice | 12,24 - Applicant First Name | String | N/A - Applicant Last Name | String | N/A - Applicant Email Address | String | N/A - Applicant Phone | String | N/A - Domain Control Validation Method | Multiple Choice | EMAIL - Organization Contact | Multiple Choice | Get From CSC Differs For Clients - Business Unit | Multiple Choice | Get From CSC Differs For Clients - Notification Email(s) Comma Separated | String | N/A - CN DCV Email | String | N/A - Organization Country | String | N/A - Addtl Sans Comma Separated DCV Emails | String | N/A - - **CSC TrustedSecure OV Wildcard, Multiple Names - Details Tab** - - CONFIG ELEMENT | DESCRIPTION - ----------------------------|------------------ - Template Short Name | CSC TrustedSecure OV Wildcard, Multiple Names - Template Display Name | CSC TrustedSecure OV Wildcard, Multiple Names - Friendly Name | CSC TrustedSecure OV Wildcard, Multiple Names - Keys Size | 2048 - Enforce RFC 2818 Compliance | True - CSR Enrollment | True - Pfx Enrollment | True - - - **CSC TrustedSecure OV Wildcard, Multiple Names - Enrollment Fields** - - NAME | DATA TYPE | VALUES - -----|--------------|----------------- - Term | Multiple Choice | 12,24 - Applicant First Name | String | N/A - Applicant Last Name | String | N/A - Applicant Email Address | String | N/A - Applicant Phone | String | N/A - Domain Control Validation Method | Multiple Choice | EMAIL - Organization Contact | Multiple Choice | Get From CSC Differs For Clients - Business Unit | Multiple Choice | Get From CSC Differs For Clients - Notification Email(s) Comma Separated | String | N/A - CN DCV Email | String | N/A - Addtl Sans Comma Separated DCV Emails | String | N/A - - **CSC TrustedSecure DV Wildcard, Multiple Names - Details Tab** - - CONFIG ELEMENT | DESCRIPTION - ----------------------------|------------------ - Template Short Name | CSC TrustedSecure DV Wildcard, Multiple Names - Template Display Name | CSC TrustedSecure DV Wildcard, Multiple Names - Friendly Name | CSC TrustedSecure DV Wildcard, Multiple Names - Keys Size | 2048 - Enforce RFC 2818 Compliance | True - CSR Enrollment | True - Pfx Enrollment | True - - - **CSC TrustedSecure DV Wildcard, Multiple Names - Enrollment Fields** - - NAME | DATA TYPE | VALUES - -----|--------------|----------------- - Term | Multiple Choice | 12,24 - Applicant First Name | String | N/A - Applicant Last Name | String | N/A - Applicant Email Address | String | N/A - Applicant Phone | String | N/A - Domain Control Validation Method | Multiple Choice | EMAIL - Organization Contact | Multiple Choice | Get From CSC Differs For Clients - Business Unit | Multiple Choice | Get From CSC Differs For Clients - Notification Email(s) Comma Separated | String | N/A - CN DCV Email | String | N/A - Addtl Sans Comma Separated DCV Emails | String | N/A +The following certificate templates are supported. Please set up the key sizes accordingly in the Certificate Profile menu of Anygateway REST, then enter the remaining details +and the Enrollment Fields for each Template accordingly using the Certificate Templates section in Command. If you would like to set up default values for enrollment parameters, you can do so the in the Certificate Template Menu of Anygateway REST. +If a field value is specified as both an Enrollment Field in Command and in the Certificate Template Menu in the REST Gateway, the value in the Enrollment Field will take precedence. + +CONFIG ELEMENT | DESCRIPTION +----------------------------|------------------ +Template Short Name | CSC TrustedSecure Premium Certificate +Template Display Name | CSC TrustedSecure Premium Certificate +Friendly Name | CSC TrustedSecure Premium Certificate +Keys Size | 2048 +Enforce RFC 2818 Compliance | True +CSR Enrollment | True +Pfx Enrollment | True + + +**CSC TrustedSecure Premium Certificate - Enrollment Fields** + +NAME | DATA TYPE | VALUES +-----|--------------|----------------- +Term | Multiple Choice | 12,24 +Applicant First Name | String | N/A +Applicant Last Name | String | N/A +Applicant Email Address | String | N/A +Applicant Phone | String | N/A +Domain Control Validation Method | Multiple Choice | EMAIL +Organization Contact | Multiple Choice | Get From CSC Differs For Clients +Business Unit | Multiple Choice | Get From CSC Differs For Clients +Notification Email(s) Comma Separated | String | N/A +CN DCV Email | String | N/A + +**CSC TrustedSecure EV Certificate - Details Tab** + +CONFIG ELEMENT | DESCRIPTION +----------------------------|------------------ +Template Short Name | CSC TrustedSecure EV Certificate +Template Display Name | CSC TrustedSecure EV Certificate +Friendly Name | CSC TrustedSecure EV Certificate +Keys Size | 2048 +Enforce RFC 2818 Compliance | True +CSR Enrollment | True +Pfx Enrollment | True + + +**CSC TrustedSecure EV Certificate - Enrollment Fields** + +NAME | DATA TYPE | VALUES +-----|--------------|----------------- +Term | Multiple Choice | 12,24 +Applicant First Name | String | N/A +Applicant Last Name | String | N/A +Applicant Email Address | String | N/A +Applicant Phone | String | N/A +Domain Control Validation Method | Multiple Choice | EMAIL +Organization Contact | Multiple Choice | Get From CSC Differs For Clients +Business Unit | Multiple Choice | Get From CSC Differs For Clients +Notification Email(s) Comma Separated | String | N/A +CN DCV Email | String | N/A +Organization Country | String | N/A + +**CSC TrustedSecure UC Certificate - Details Tab** + +CONFIG ELEMENT | DESCRIPTION +----------------------------|------------------ +Template Short Name | CSC TrustedSecure UC Certificate +Template Display Name | CSC TrustedSecure UC Certificate +Friendly Name | CSC TrustedSecure UC Certificate +Keys Size | 2048 +Enforce RFC 2818 Compliance | True +CSR Enrollment | True +Pfx Enrollment | True + + +**CSC TrustedSecure UC Certificate - Enrollment Fields** + +NAME | DATA TYPE | VALUES +-----|--------------|----------------- +Term | Multiple Choice | 12,24 +Applicant First Name | String | N/A +Applicant Last Name | String | N/A +Applicant Email Address | String | N/A +Applicant Phone | String | N/A +Domain Control Validation Method | Multiple Choice | EMAIL +Organization Contact | Multiple Choice | Get From CSC Differs For Clients +Business Unit | Multiple Choice | Get From CSC Differs For Clients +Notification Email(s) Comma Separated | String | N/A +CN DCV Email | String | N/A +Addtl Sans Comma Separated DCV Emails | String | N/A + + +**CSC TrustedSecure Premium Wildcard Certificate - Details Tab** + +CONFIG ELEMENT | DESCRIPTION +----------------------------|------------------ +Template Short Name | CSC TrustedSecure Premium Wildcard Certificate +Template Display Name | CSC TrustedSecure Premium Wildcard Certificate +Friendly Name | CSC TrustedSecure Premium Wildcard Certificate +Keys Size | 2048 +Enforce RFC 2818 Compliance | True +CSR Enrollment | True +Pfx Enrollment | True + + +**CSC TrustedSecure Premium Wildcard Certificate - Enrollment Fields** + +NAME | DATA TYPE | VALUES +-----|--------------|----------------- +Term | Multiple Choice | 12,24 +Applicant First Name | String | N/A +Applicant Last Name | String | N/A +Applicant Email Address | String | N/A +Applicant Phone | String | N/A +Domain Control Validation Method | Multiple Choice | EMAIL +Organization Contact | Multiple Choice | Get From CSC Differs For Clients +Business Unit | Multiple Choice | Get From CSC Differs For Clients +Notification Email(s) Comma Separated | String | N/A +CN DCV Email | String | N/A + +**CSC TrustedSecure Domain Validated SSL - Details Tab** + +CONFIG ELEMENT | DESCRIPTION +----------------------------|------------------ +Template Short Name | CSC TrustedSecure Domain Validated SSL +Template Display Name | CSC TrustedSecure Domain Validated SSL +Friendly Name | CSC TrustedSecure Domain Validated SSL +Keys Size | 2048 +Enforce RFC 2818 Compliance | True +CSR Enrollment | True +Pfx Enrollment | True + + +**CSC TrustedSecure Domain Validated SSL - Enrollment Fields** + +NAME | DATA TYPE | VALUES +-----|--------------|----------------- +Term | Multiple Choice | 12,24 +Applicant First Name | String | N/A +Applicant Last Name | String | N/A +Applicant Email Address | String | N/A +Applicant Phone | String | N/A +Domain Control Validation Method | Multiple Choice | EMAIL +Organization Contact | Multiple Choice | Get From CSC Differs For Clients +Business Unit | Multiple Choice | Get From CSC Differs For Clients +Notification Email(s) Comma Separated | String | N/A +CN DCV Email | String | N/A + +**CSC TrustedSecure Domain Validated Wildcard SSL - Details Tab** + +CONFIG ELEMENT | DESCRIPTION +----------------------------|------------------ +Template Short Name | CSC TrustedSecure Domain Validated Wildcard SSL +Template Display Name | CSC TrustedSecure Domain Validated Wildcard SSL +Friendly Name | CSC TrustedSecure Domain Validated Wildcard SSL +Keys Size | 2048 +Enforce RFC 2818 Compliance | True +CSR Enrollment | True +Pfx Enrollment | True + + +**CSC TrustedSecure Domain Validated Wildcard SSL - Enrollment Fields** + +NAME | DATA TYPE | VALUES +-----|--------------|----------------- +Term | Multiple Choice | 12,24 +Applicant First Name | String | N/A +Applicant Last Name | String | N/A +Applicant Email Address | String | N/A +Applicant Phone | String | N/A +Domain Control Validation Method | Multiple Choice | EMAIL +Organization Contact | Multiple Choice | Get From CSC Differs For Clients +Business Unit | Multiple Choice | Get From CSC Differs For Clients +Notification Email(s) Comma Separated | String | N/A +CN DCV Email | String | N/A + +**CSC TrustedSecure Domain Validated UC Certificate - Details Tab** + +CONFIG ELEMENT | DESCRIPTION +----------------------------|------------------ +Template Short Name | CSC TrustedSecure Domain Validated UC Certificate +Template Display Name | CSC TrustedSecure Domain Validated UC Certificate +Friendly Name | CSC TrustedSecure Domain Validated UC Certificate +Keys Size | 2048 +Enforce RFC 2818 Compliance | True +CSR Enrollment | True +Pfx Enrollment | True + + +**CSC TrustedSecure Domain Validated UC Certificate - Enrollment Fields** + +NAME | DATA TYPE | VALUES +-----|--------------|----------------- +Term | Multiple Choice | 12,24 +Applicant First Name | String | N/A +Applicant Last Name | String | N/A +Applicant Email Address | String | N/A +Applicant Phone | String | N/A +Domain Control Validation Method | Multiple Choice | EMAIL +Organization Contact | Multiple Choice | Get From CSC Differs For Clients +Business Unit | Multiple Choice | Get From CSC Differs For Clients +Notification Email(s) Comma Separated | String | N/A +CN DCV Email | String | N/A +Addtl Sans Comma Separated DCV Emails | String | N/A + +**CSC TrustedSecure EV, Multiple Names - Details Tab** + +CONFIG ELEMENT | DESCRIPTION +----------------------------|------------------ +Template Short Name | CSC TrustedSecure EV, Multiple Names +Template Display Name | CSC TrustedSecure EV, Multiple Names +Friendly Name | CSC TrustedSecure EV, Multiple Names +Keys Size | 2048 +Enforce RFC 2818 Compliance | True +CSR Enrollment | True +Pfx Enrollment | True + + +**CSC TrustedSecure EV, Multiple Names - Enrollment Fields** + +NAME | DATA TYPE | VALUES +-----|--------------|----------------- +Term | Multiple Choice | 12,24 +Applicant First Name | String | N/A +Applicant Last Name | String | N/A +Applicant Email Address | String | N/A +Applicant Phone | String | N/A +Domain Control Validation Method | Multiple Choice | EMAIL +Organization Contact | Multiple Choice | Get From CSC Differs For Clients +Business Unit | Multiple Choice | Get From CSC Differs For Clients +Notification Email(s) Comma Separated | String | N/A +CN DCV Email | String | N/A +Organization Country | String | N/A +Addtl Sans Comma Separated DCV Emails | String | N/A + +**CSC TrustedSecure OV Wildcard, Multiple Names - Details Tab** + +CONFIG ELEMENT | DESCRIPTION +----------------------------|------------------ +Template Short Name | CSC TrustedSecure OV Wildcard, Multiple Names +Template Display Name | CSC TrustedSecure OV Wildcard, Multiple Names +Friendly Name | CSC TrustedSecure OV Wildcard, Multiple Names +Keys Size | 2048 +Enforce RFC 2818 Compliance | True +CSR Enrollment | True +Pfx Enrollment | True + + +**CSC TrustedSecure OV Wildcard, Multiple Names - Enrollment Fields** + +NAME | DATA TYPE | VALUES +-----|--------------|----------------- +Term | Multiple Choice | 12,24 +Applicant First Name | String | N/A +Applicant Last Name | String | N/A +Applicant Email Address | String | N/A +Applicant Phone | String | N/A +Domain Control Validation Method | Multiple Choice | EMAIL +Organization Contact | Multiple Choice | Get From CSC Differs For Clients +Business Unit | Multiple Choice | Get From CSC Differs For Clients +Notification Email(s) Comma Separated | String | N/A +CN DCV Email | String | N/A +Addtl Sans Comma Separated DCV Emails | String | N/A + +**CSC TrustedSecure DV Wildcard, Multiple Names - Details Tab** + +CONFIG ELEMENT | DESCRIPTION +----------------------------|------------------ +Template Short Name | CSC TrustedSecure DV Wildcard, Multiple Names +Template Display Name | CSC TrustedSecure DV Wildcard, Multiple Names +Friendly Name | CSC TrustedSecure DV Wildcard, Multiple Names +Keys Size | 2048 +Enforce RFC 2818 Compliance | True +CSR Enrollment | True +Pfx Enrollment | True + + +**CSC TrustedSecure DV Wildcard, Multiple Names - Enrollment Fields** + +NAME | DATA TYPE | VALUES +-----|--------------|----------------- +Term | Multiple Choice | 12,24 +Applicant First Name | String | N/A +Applicant Last Name | String | N/A +Applicant Email Address | String | N/A +Applicant Phone | String | N/A +Domain Control Validation Method | Multiple Choice | EMAIL +Organization Contact | Multiple Choice | Get From CSC Differs For Clients +Business Unit | Multiple Choice | Get From CSC Differs For Clients +Notification Email(s) Comma Separated | String | N/A +CN DCV Email | String | N/A +Addtl Sans Comma Separated DCV Emails | String | N/A 3. Follow the [official Keyfactor documentation](https://software.keyfactor.com/Guides/AnyCAGatewayREST/Content/AnyCAGatewayREST/AddCA-Keyfactor.htm) to add each defined Certificate Authority to Keyfactor Command and import the newly defined Certificate Templates. 4. In Keyfactor Command (v12.3+), for each imported Certificate Template, follow the [official documentation](https://software.keyfactor.com/Core-OnPrem/Current/Content/ReferenceGuide/Configuring%20Template%20Options.htm) to define enrollment fields for each of the following parameters: - * **Term** - OPTIONAL: Certificate term (e.g. 12 or 24 months) - * **Applicant First Name** - OPTIONAL: Applicant First Name - * **Applicant Last Name** - OPTIONAL: Applicant Last Name - * **Applicant Email Address** - OPTIONAL: Applicant Email Address - * **Applicant Phone** - OPTIONAL: Applicant Phone (+nn.nnnnnnnn) - * **Domain Control Validation Method** - OPTIONAL: Domain Control Validation Method (e.g. EMAIL) - * **Organization Contact** - OPTIONAL: Organization Contact (selected from CSC configuration) - * **Business Unit** - OPTIONAL: Business Unit (selected from CSC configuration) - * **Notification Email(s) Comma Separated** - OPTIONAL: Notification Email(s), comma separated - * **CN DCV Email** - OPTIONAL: CN DCV Email (e.g. admin@yourdomain.com) - * **Organization Country** - OPTIONAL: Organization Country - * **Addtl Sans Comma Separated DCV Emails** - OPTIONAL: Additional SANs DCV Emails, comma separated - - + * **Term** - OPTIONAL: Certificate term (e.g. 12 or 24 months) + * **Applicant First Name** - OPTIONAL: Applicant First Name + * **Applicant Last Name** - OPTIONAL: Applicant Last Name + * **Applicant Email Address** - OPTIONAL: Applicant Email Address + * **Applicant Phone** - OPTIONAL: Applicant Phone (+nn.nnnnnnnn) + * **Domain Control Validation Method** - OPTIONAL: Domain Control Validation Method (e.g. EMAIL) + * **Organization Contact** - OPTIONAL: Organization Contact (selected from CSC configuration) + * **Business Unit** - OPTIONAL: Business Unit (selected from CSC configuration) + * **Notification Email(s) Comma Separated** - OPTIONAL: Notification Email(s), comma separated + * **CN DCV Email** - OPTIONAL: CN DCV Email (e.g. admin@yourdomain.com) + * **Organization Country** - OPTIONAL: Organization Country + * **Addtl Sans Comma Separated DCV Emails** - OPTIONAL: Additional SANs DCV Emails, comma separated ## License @@ -406,4 +403,4 @@ Apache License 2.0, see [LICENSE](LICENSE). ## Related Integrations -See all [Keyfactor Any CA Gateways (REST)](https://github.com/orgs/Keyfactor/repositories?q=anycagateway). \ No newline at end of file +See all [Keyfactor Any CA Gateways (REST)](https://github.com/orgs/Keyfactor/repositories?q=anycagateway). From cba26dc36b3c08be371bb63d8487c787d62b62b7 Mon Sep 17 00:00:00 2001 From: Brian Hill Date: Wed, 9 Sep 2026 16:59:11 -0400 Subject: [PATCH 8/9] Throw on non-BadRequest HTTP failures in CscGlobalClient Registration/Renewal/Reissue/Revoke calls only handled CSC Global's 400 error-body case; any other failure status (e.g. 404) fell through to deserializing an empty/unexpected body, leaving Result and RegistrationError both null and causing a downstream NullReferenceException in RequestManager instead of a clear error. --- cscglobal-caplugin/Client/CscGlobalClient.cs | 24 ++++++++++++++++---- 1 file changed, 20 insertions(+), 4 deletions(-) diff --git a/cscglobal-caplugin/Client/CscGlobalClient.cs b/cscglobal-caplugin/Client/CscGlobalClient.cs index 3429dc5..f9b7fb2 100644 --- a/cscglobal-caplugin/Client/CscGlobalClient.cs +++ b/cscglobal-caplugin/Client/CscGlobalClient.cs @@ -65,7 +65,11 @@ public async Task SubmitRegistrationAsync( } if (!resp.IsSuccessStatusCode) - Logger.LogError($"Registration request failed with status code {resp.StatusCode}"); + { + var errorBody = await resp.Content.ReadAsStringAsync(); + Logger.LogError($"Registration request failed with status code {resp.StatusCode} | Message: {errorBody}"); + throw new HttpRequestException($"Registration request failed with status code {resp.StatusCode}: {errorBody}"); + } var registrationResponse = JsonConvert.DeserializeObject(await resp.Content.ReadAsStringAsync(), @@ -100,7 +104,11 @@ public async Task SubmitRenewalAsync( } if (!resp.IsSuccessStatusCode) - Logger.LogError($"Renewal request failed with status code {resp.StatusCode}"); + { + var errorBody = await resp.Content.ReadAsStringAsync(); + Logger.LogError($"Renewal request failed with status code {resp.StatusCode} | Message: {errorBody}"); + throw new HttpRequestException($"Renewal request failed with status code {resp.StatusCode}: {errorBody}"); + } var rawRenewResponse = await resp.Content.ReadAsStringAsync(); Logger.LogTrace("Logging Success Response Raw"); @@ -133,7 +141,11 @@ public async Task SubmitReissueAsync( } if (!resp.IsSuccessStatusCode) - Logger.LogError($"Reissue request failed with status code {resp.StatusCode}"); + { + var errorBody = await resp.Content.ReadAsStringAsync(); + Logger.LogError($"Reissue request failed with status code {resp.StatusCode} | Message: {errorBody}"); + throw new HttpRequestException($"Reissue request failed with status code {resp.StatusCode}: {errorBody}"); + } var reissueResponse = JsonConvert.DeserializeObject(await resp.Content.ReadAsStringAsync()); @@ -201,7 +213,11 @@ public async Task SubmitRevokeCertificateAsync(string uuId) } if (!resp.IsSuccessStatusCode) - Logger.LogError($"Revoke request for UUID {uuId} failed with status code {resp.StatusCode}"); + { + var errorBody = await resp.Content.ReadAsStringAsync(); + Logger.LogError($"Revoke request for UUID {uuId} failed with status code {resp.StatusCode} | Message: {errorBody}"); + throw new HttpRequestException($"Revoke request failed with status code {resp.StatusCode}: {errorBody}"); + } var getRevokeResponse = JsonConvert.DeserializeObject(await resp.Content.ReadAsStringAsync()); From 36e2c5ab43e2be7495038eb9cfc34d745bd9f769 Mon Sep 17 00:00:00 2001 From: Brian Hill Date: Wed, 9 Sep 2026 17:07:45 -0400 Subject: [PATCH 9/9] Harden CA plugin against null/short/missing inputs Add null and bounds guards throughout the enrollment, revoke, and sync paths: CSC Global responses (Result/RegistrationError/Status may be null even on a 200), optional ProductParameters lookups that were using the strict indexer, CARequestID substring truncation, and config/dictionary lookups in Initialize and the CscGlobalClient constructor. Replaces silent NREs with clear, logged failures. --- cscglobal-caplugin/CSCGlobalCAPlugin.cs | 81 +++++++-- cscglobal-caplugin/Client/CscGlobalClient.cs | 38 +++- cscglobal-caplugin/RequestManager.cs | 180 +++++++++++++++---- 3 files changed, 243 insertions(+), 56 deletions(-) diff --git a/cscglobal-caplugin/CSCGlobalCAPlugin.cs b/cscglobal-caplugin/CSCGlobalCAPlugin.cs index edf44bd..f5ab830 100644 --- a/cscglobal-caplugin/CSCGlobalCAPlugin.cs +++ b/cscglobal-caplugin/CSCGlobalCAPlugin.cs @@ -43,10 +43,14 @@ public CSCGlobalCAPlugin() public void Initialize(IAnyCAPluginConfigProvider configProvider, ICertificateDataReader certificateDataReader) { Logger.MethodEntry(LogLevel.Debug); - _certificateDataReader = certificateDataReader; + if (configProvider == null) throw new ArgumentNullException(nameof(configProvider)); + _certificateDataReader = certificateDataReader ?? throw new ArgumentNullException(nameof(certificateDataReader)); CscGlobalClient = new CscGlobalClient(configProvider); - var templateSync = configProvider.CAConnectionData["TemplateSync"].ToString(); - if (templateSync.ToUpper() == "ON") EnableTemplateSync = true; + + if (configProvider.CAConnectionData.TryGetValue("TemplateSync", out var templateSyncValue) && + templateSyncValue != null && + string.Equals(templateSyncValue.ToString(), "ON", StringComparison.OrdinalIgnoreCase)) + EnableTemplateSync = true; Logger.LogInformation($"Template sync is {(EnableTemplateSync ? "enabled" : "disabled")}"); if (configProvider.CAConnectionData.ContainsKey(Constants.SyncFilterDays)) @@ -73,7 +77,10 @@ public async Task GetSingleRecord(string caRequestID) try { Logger.MethodEntry(LogLevel.Debug); - var keyfactorCaId = caRequestID?.Substring(0, 36); //todo fix to use pipe delimiter + if (string.IsNullOrEmpty(caRequestID) || caRequestID.Length < 36) + throw new ArgumentException($"CA request ID '{caRequestID}' is missing or too short to contain a valid UUID", nameof(caRequestID)); + + var keyfactorCaId = caRequestID.Substring(0, 36); //todo fix to use pipe delimiter Logger.LogTrace($"Keyfactor Ca Id: {keyfactorCaId}"); var certificateResponse = Task.Run(async () => await CscGlobalClient.SubmitGetCertificateAsync(keyfactorCaId)) @@ -163,6 +170,13 @@ private async Task SyncCertificates(BlockingCollection b Logger.LogInformation($"Retrieved {certs?.Results?.Count ?? 0} certificate(s) from CSC Global for sync"); + if (certs?.Results == null) + { + Logger.LogWarning("Certificate list request returned no results collection; nothing to sync"); + flow.Step("QueueCertificates", "No results collection returned; nothing to sync"); + return; + } + var queuedCount = 0; var skippedCount = 0; foreach (var currentResponseItem in certs.Results) @@ -230,6 +244,9 @@ public async Task Revoke(string caRequestID, string hexSerialNumber, uint r try { Logger.LogInformation($"Starting Revoke for CA request ID {caRequestID}, reason {revocationReason}"); + if (string.IsNullOrEmpty(caRequestID) || caRequestID.Length < 36) + throw new ArgumentException($"CA request ID '{caRequestID}' is missing or too short to contain a valid UUID", nameof(caRequestID)); + var uuid = caRequestID.Substring(0, 36); //todo fix to use pipe delimiter var revokeResponse = await flow.StepAsync("SubmitRevokeCertificate", @@ -269,6 +286,8 @@ public async Task Revoke(string caRequestID, string hexSerialNumber, uint r public async Task Enroll(string csr, string subject, Dictionary san, EnrollmentProductInfo productInfo, RequestFormat requestFormat, EnrollmentType enrollmentType) { + if (productInfo == null) throw new ArgumentNullException(nameof(productInfo)); + Logger.MethodEntry(LogLevel.Debug); Logger.LogInformation($"Starting Enroll for product {productInfo.ProductID}, enrollment type {enrollmentType}"); using var flow = new FlowLogger(Logger, "Enroll"); @@ -279,9 +298,11 @@ public async Task Enroll(string csr, string subject, Dictionar var priorSn = ""; ReissueRequest reissueRequest; RenewalRequest renewRequest; - if (productInfo.ProductParameters.ContainsKey("priorcertsn")) + var productParameters = productInfo.ProductParameters ?? new Dictionary(); + if (productParameters.ContainsKey("priorcertsn")) { - priorSn = productInfo.ProductParameters["PriorCertSN"]; + productParameters.TryGetValue("PriorCertSN", out priorSn); + priorSn ??= ""; Logger.LogDebug($"Prior cert sn: {priorSn}"); } @@ -294,7 +315,7 @@ public async Task Enroll(string csr, string subject, Dictionar flow.Branch("New Enrollment"); //If they renewed an expired cert it gets here and this will not be supported IRegistrationResponse enrollmentResponse; - if (!productInfo.ProductParameters.ContainsKey("PriorCertSN")) + if (!productParameters.ContainsKey("PriorCertSN")) { enrollmentRequest = _requestManager.GetRegistrationRequest(productInfo, csr, san, customFields); Logger.LogTrace($"Enrollment Request JSON: {JsonConvert.SerializeObject(enrollmentRequest)}"); @@ -321,14 +342,38 @@ public async Task Enroll(string csr, string subject, Dictionar return newResult; case EnrollmentType.RenewOrReissue: flow.Branch("Renew Or Reissue"); + if (string.IsNullOrEmpty(priorSn)) + { + Logger.LogWarning($"Renew/Reissue requested for product {productInfo.ProductID} but no prior certificate serial number was supplied"); + flow.Fail("Renew Or Reissue", "Missing prior certificate serial number"); + flow.EndBranch(); + return new EnrollmentResult + { + Status = 30, //failure + StatusMessage = "Cannot renew or reissue: no prior certificate serial number was supplied." + }; + } + //Logic to determine renew vs reissue var renewal = false; var order_id = await _certificateDataReader.GetRequestIDBySerialNumber(priorSn); + if (string.IsNullOrEmpty(order_id)) + { + Logger.LogWarning($"Could not find a Keyfactor request ID for prior certificate serial number {priorSn}"); + flow.Fail("Renew Or Reissue", $"No request ID found for prior certificate serial number {priorSn}"); + flow.EndBranch(); + return new EnrollmentResult + { + Status = 30, //failure + StatusMessage = $"Cannot renew or reissue: no prior request found for serial number {priorSn}." + }; + } + var expirationDate = _certificateDataReader.GetExpirationDateByRequestId(order_id); if (expirationDate == null) { var localcert = await GetSingleRecord(order_id); - expirationDate = localcert.RevocationDate; + expirationDate = localcert?.RevocationDate; } if (expirationDate < DateTime.Now) renewal = true; @@ -336,13 +381,13 @@ public async Task Enroll(string csr, string subject, Dictionar { flow.Step("DetermineRenewOrReissue", "Renewal - cert is expired"); //One click won't work for this implementation b/c we are missing enrollment params - if (productInfo.ProductParameters.ContainsKey("Applicant Last Name")) + if (productParameters.ContainsKey("Applicant Last Name")) { //priorCert = _certificateDataReader.get( //DataConversion.HexToBytes(productInfo.ProductParameters["PriorCertSN"])); //uUId = priorCert.CARequestID.Substring(0, 36); //uUId is a GUID uUId = await _certificateDataReader.GetRequestIDBySerialNumber( - productInfo.ProductParameters["PriorCertSN"]); + productParameters.GetValueOrDefault("PriorCertSN", "")); Logger.LogTrace($"Renew uUId: {uUId}"); renewRequest = _requestManager.GetRenewalRequest(productInfo, uUId, csr, san, customFields); Logger.LogTrace($"Renewal Request JSON: {JsonConvert.SerializeObject(renewRequest)}"); @@ -369,10 +414,22 @@ public async Task Enroll(string csr, string subject, Dictionar flow.Step("DetermineRenewOrReissue", "Reissue - cert is still valid"); //One click won't work for this implementation b/c we are missing enrollment params - if (productInfo.ProductParameters.ContainsKey("Applicant Last Name")) + if (productParameters.ContainsKey("Applicant Last Name")) { var requestid = await _certificateDataReader.GetRequestIDBySerialNumber( - productInfo.ProductParameters["PriorCertSN"]); + productParameters.GetValueOrDefault("PriorCertSN", "")); + if (string.IsNullOrEmpty(requestid) || requestid.Length < 36) + { + Logger.LogWarning($"Could not find a valid Keyfactor request ID for prior certificate serial number for product {productInfo.ProductID}"); + flow.Fail("Reissue", "No valid request ID found for prior certificate serial number"); + flow.EndBranch(); + return new EnrollmentResult + { + Status = 30, //failure + StatusMessage = "Cannot reissue: no prior request found for the supplied certificate serial number." + }; + } + uUId = requestid.Substring(0, 36); //uUId is a GUID Logger.LogTrace($"Reissue uUId: {uUId}"); reissueRequest = _requestManager.GetReissueRequest(productInfo, uUId, csr, san, customFields); diff --git a/cscglobal-caplugin/Client/CscGlobalClient.cs b/cscglobal-caplugin/Client/CscGlobalClient.cs index f9b7fb2..ca692e2 100644 --- a/cscglobal-caplugin/Client/CscGlobalClient.cs +++ b/cscglobal-caplugin/Client/CscGlobalClient.cs @@ -24,6 +24,13 @@ public sealed class CscGlobalClient : ICscGlobalClient public CscGlobalClient(IAnyCAPluginConfigProvider config) { Logger = LogHandler.GetClassLogger(); + if (config == null) throw new ArgumentNullException(nameof(config)); + if (config.CAConnectionData == null) + { + Logger.LogError("CA connection data is null; client will not be able to call the CSC Global API"); + return; + } + if (config.CAConnectionData.ContainsKey(Constants.CscGlobalApiKey)) { BaseUrl = new Uri(config.CAConnectionData[Constants.CscGlobalUrl].ToString()); @@ -74,6 +81,9 @@ public async Task SubmitRegistrationAsync( var registrationResponse = JsonConvert.DeserializeObject(await resp.Content.ReadAsStringAsync(), settings); + if (registrationResponse == null) + throw new InvalidOperationException("Registration request succeeded but the response body could not be parsed"); + Logger.MethodExit(LogLevel.Debug); return registrationResponse; } @@ -115,6 +125,9 @@ public async Task SubmitRenewalAsync( Logger.LogTrace(rawRenewResponse); var renewalResponse = JsonConvert.DeserializeObject(rawRenewResponse); + if (renewalResponse == null) + throw new InvalidOperationException("Renewal request succeeded but the response body could not be parsed"); + return renewalResponse; } } @@ -149,6 +162,9 @@ public async Task SubmitReissueAsync( var reissueResponse = JsonConvert.DeserializeObject(await resp.Content.ReadAsStringAsync()); + if (reissueResponse == null) + throw new InvalidOperationException("Reissue request succeeded but the response body could not be parsed"); + return reissueResponse; } } @@ -168,6 +184,9 @@ public async Task SubmitGetCertificateAsync(string certific resp.EnsureSuccessStatusCode(); var getCertificateResponse = JsonConvert.DeserializeObject(await resp.Content.ReadAsStringAsync()); + if (getCertificateResponse == null) + throw new InvalidOperationException($"Get certificate request for {certificateId} succeeded but the response body could not be parsed"); + Logger.MethodExit(LogLevel.Debug); return getCertificateResponse; } @@ -187,9 +206,12 @@ public async Task> SubmitGetCustomFields() resp.EnsureSuccessStatusCode(); var getCustomFieldsResponse = JsonConvert.DeserializeObject(await resp.Content.ReadAsStringAsync()); + if (getCustomFieldsResponse == null) + throw new InvalidOperationException("Get custom fields request succeeded but the response body could not be parsed"); + Logger.LogTrace($"Retrieved {getCustomFieldsResponse.CustomFields?.Count ?? 0} custom field(s)"); Logger.MethodExit(LogLevel.Debug); - return getCustomFieldsResponse.CustomFields; + return getCustomFieldsResponse.CustomFields ?? new List(); } } @@ -221,6 +243,9 @@ public async Task SubmitRevokeCertificateAsync(string uuId) var getRevokeResponse = JsonConvert.DeserializeObject(await resp.Content.ReadAsStringAsync()); + if (getRevokeResponse == null) + throw new InvalidOperationException("Revoke request succeeded but the response body could not be parsed"); + Logger.MethodExit(LogLevel.Debug); return getRevokeResponse; } @@ -239,14 +264,17 @@ public async Task SubmitCertificateListRequestAsync(str if (!resp.IsSuccessStatusCode) { - var responseMessage = resp.Content.ReadAsStringAsync().Result; - Logger.LogError( - $"Failed Request to Keyfactor. Retrying request. Status Code {resp.StatusCode} | Message: {responseMessage}"); + var responseMessage = await resp.Content.ReadAsStringAsync(); + Logger.LogError($"Certificate list request failed. Status Code {resp.StatusCode} | Message: {responseMessage}"); + throw new HttpRequestException($"Certificate list request failed with status code {resp.StatusCode}: {responseMessage}"); } var certificateListResponse = JsonConvert.DeserializeObject(await resp.Content.ReadAsStringAsync()); - Logger.LogInformation($"Certificate list request returned {certificateListResponse?.Results?.Count ?? 0} result(s)"); + if (certificateListResponse == null) + throw new InvalidOperationException("Certificate list request succeeded but the response body could not be parsed"); + + Logger.LogInformation($"Certificate list request returned {certificateListResponse.Results?.Count ?? 0} result(s)"); Logger.MethodExit(LogLevel.Debug); return certificateListResponse; } diff --git a/cscglobal-caplugin/RequestManager.cs b/cscglobal-caplugin/RequestManager.cs index 72f9272..b580e71 100644 --- a/cscglobal-caplugin/RequestManager.cs +++ b/cscglobal-caplugin/RequestManager.cs @@ -23,9 +23,23 @@ public class RequestManager public static Func Pemify = ss => ss.Length <= 64 ? ss : ss.Substring(0, 64) + "\n" + Pemify(ss.Substring(64)); + private static string GetOptionalParam(EnrollmentProductInfo productInfo, string key) + { + return productInfo.ProductParameters != null && + productInfo.ProductParameters.TryGetValue(key, out var value) + ? value + : string.Empty; + } + private List GetCustomFields(EnrollmentProductInfo productInfo, List customFields) { var customFieldList = new List(); + if (customFields == null) + { + Logger.LogTrace("No custom field definitions supplied; skipping custom field mapping"); + return customFieldList; + } + foreach (var field in customFields) if (productInfo.ProductParameters.ContainsKey(field.Label)) { @@ -49,17 +63,37 @@ private List GetCustomFields(EnrollmentProductInfo productInfo, Lis public EnrollmentResult GetRenewResponse(RenewalResponse renewResponse) { + if (renewResponse == null) + { + Logger.LogError("Renewal failed: CSC Global returned no response"); + return new EnrollmentResult + { + Status = (int)EndEntityStatus.FAILED, //failure + StatusMessage = "CSC Global returned no response for the renewal request" + }; + } + if (renewResponse.RegistrationError != null) { Logger.LogError($"Renewal failed: {renewResponse.RegistrationError.Description}"); return new EnrollmentResult { Status = (int)EndEntityStatus.FAILED, //failure - CARequestID = renewResponse?.Result?.Status?.Uuid, + CARequestID = renewResponse.Result?.Status?.Uuid, StatusMessage = renewResponse.RegistrationError.Description }; } + if (renewResponse.Result == null) + { + Logger.LogError("Renewal failed: CSC Global reported success but returned no result"); + return new EnrollmentResult + { + Status = (int)EndEntityStatus.FAILED, //failure + StatusMessage = "CSC Global reported success but returned no result" + }; + } + Logger.LogInformation($"Renewal successfully completed for {renewResponse.Result.CommonName}"); return new EnrollmentResult { @@ -74,6 +108,16 @@ public EnrollmentResult GetEnrollmentResult( IRegistrationResponse registrationResponse) { + if (registrationResponse == null) + { + Logger.LogError("Enrollment failed: CSC Global returned no response"); + return new EnrollmentResult + { + Status = (int)EndEntityStatus.FAILED, //failure + StatusMessage = "CSC Global returned no response for the registration request" + }; + } + if (registrationResponse.RegistrationError != null) { Logger.LogError($"Enrollment failed: {registrationResponse.RegistrationError.Description}"); @@ -84,6 +128,16 @@ public EnrollmentResult }; } + if (registrationResponse.Result == null) + { + Logger.LogError("Enrollment failed: CSC Global reported success but returned no result"); + return new EnrollmentResult + { + Status = (int)EndEntityStatus.FAILED, //failure + StatusMessage = "CSC Global reported success but returned no result" + }; + } + var cnames = new Dictionary(); if (registrationResponse.Result.DcvDetails != null && registrationResponse.Result.DcvDetails.Count > 0) foreach (var dcv in registrationResponse.Result.DcvDetails) @@ -103,7 +157,7 @@ public EnrollmentResult return new EnrollmentResult { Status = (int)EndEntityStatus.EXTERNALVALIDATION, //success - CARequestID = registrationResponse.Result.Status.Uuid, + CARequestID = registrationResponse.Result.Status?.Uuid, StatusMessage = $"Order Successfully Created With Order Number {registrationResponse.Result.CommonName}", EnrollmentContext = cnames.Count > 0 ? cnames : null @@ -112,6 +166,12 @@ public EnrollmentResult public int GetRevokeResult(IRevokeResponse revokeResponse) { + if (revokeResponse == null) + { + Logger.LogError("Revoke failed: CSC Global returned no response"); + return (int)EndEntityStatus.FAILED; + } + if (revokeResponse.RegistrationError != null) { Logger.LogError($"Revoke failed: {revokeResponse.RegistrationError.Description}"); @@ -123,6 +183,16 @@ public int GetRevokeResult(IRevokeResponse revokeResponse) public EnrollmentResult GetReIssueResult(IReissueResponse reissueResponse) { + if (reissueResponse == null) + { + Logger.LogError("Reissue failed: CSC Global returned no response"); + return new EnrollmentResult + { + Status = (int)EndEntityStatus.FAILED, //failure + StatusMessage = "CSC Global returned no response for the reissue request" + }; + } + if (reissueResponse.RegistrationError != null) { Logger.LogError($"Reissue failed: {reissueResponse.RegistrationError.Description}"); @@ -133,11 +203,21 @@ public EnrollmentResult GetReIssueResult(IReissueResponse reissueResponse) }; } + if (reissueResponse.Result == null) + { + Logger.LogError("Reissue failed: CSC Global reported success but returned no result"); + return new EnrollmentResult + { + Status = (int)EndEntityStatus.FAILED, //failure + StatusMessage = "CSC Global reported success but returned no result" + }; + } + Logger.LogInformation($"Reissue successfully completed for {reissueResponse.Result.CommonName}"); return new EnrollmentResult { Status = (int)EndEntityStatus.GENERATED, //success - CARequestID = reissueResponse.Result.Status.Uuid, + CARequestID = reissueResponse.Result.Status?.Uuid, StatusMessage = $"Reissue Successfully Completed For {reissueResponse.Result.CommonName}" }; } @@ -145,10 +225,29 @@ public EnrollmentResult GetReIssueResult(IReissueResponse reissueResponse) public DomainControlValidation GetDomainControlValidation(string methodType, string[] emailAddress, string domainName) { - foreach (var address in emailAddress) + if (string.IsNullOrWhiteSpace(domainName)) + { + Logger.LogWarning("GetDomainControlValidation called with an empty domain name"); + return null; + } + + foreach (var address in emailAddress ?? Array.Empty()) { - var email = new MailAddress(address); - if (domainName.Contains(email.Host.Split('.')[0])) + if (string.IsNullOrWhiteSpace(address)) continue; + + MailAddress email; + try + { + email = new MailAddress(address.Trim()); + } + catch (FormatException fex) + { + Logger.LogWarning(fex, $"Skipping malformed DCV email address '{address}'"); + continue; + } + + var hostLabels = email.Host.Split('.'); + if (hostLabels.Length > 0 && domainName.Contains(hostLabels[0])) return new DomainControlValidation { MethodType = methodType, @@ -180,8 +279,8 @@ public RegistrationRequest GetRegistrationRequest(EnrollmentProductInfo productI var bytes = Encoding.UTF8.GetBytes(cert); var encodedString = Convert.ToBase64String(bytes); - var commonNameValidationEmail = productInfo.ProductParameters["CN DCV Email"]; - var methodType = productInfo.ProductParameters["Domain Control Validation Method"]; + var commonNameValidationEmail = GetOptionalParam(productInfo, "CN DCV Email"); + var methodType = GetOptionalParam(productInfo, "Domain Control Validation Method"); var certificateType = GetCertificateType(productInfo.ProductID); return new RegistrationRequest @@ -189,15 +288,15 @@ public RegistrationRequest GetRegistrationRequest(EnrollmentProductInfo productI Csr = encodedString, ServerSoftware = "-1", //Just default to other, user does not need to fill this in CertificateType = certificateType, - Term = productInfo.ProductParameters["Term"], - ApplicantFirstName = productInfo.ProductParameters["Applicant First Name"], - ApplicantLastName = productInfo.ProductParameters["Applicant Last Name"], - ApplicantEmailAddress = productInfo.ProductParameters["Applicant Email Address"], - ApplicantPhoneNumber = productInfo.ProductParameters["Applicant Phone"], + Term = GetOptionalParam(productInfo, "Term"), + ApplicantFirstName = GetOptionalParam(productInfo, "Applicant First Name"), + ApplicantLastName = GetOptionalParam(productInfo, "Applicant Last Name"), + ApplicantEmailAddress = GetOptionalParam(productInfo, "Applicant Email Address"), + ApplicantPhoneNumber = GetOptionalParam(productInfo, "Applicant Phone"), DomainControlValidation = GetDomainControlValidation(methodType, commonNameValidationEmail), Notifications = GetNotifications(productInfo), - OrganizationContact = productInfo.ProductParameters["Organization Contact"], - BusinessUnit = productInfo.ProductParameters["Business Unit"], + OrganizationContact = GetOptionalParam(productInfo, "Organization Contact"), + BusinessUnit = GetOptionalParam(productInfo, "Business Unit"), ShowPrice = true, //User should not have to fill this out CustomFields = GetCustomFields(productInfo, customFields), SubjectAlternativeNames = MultiNameCertificateTypes.Contains(certificateType) ? GetSubjectAlternativeNames(productInfo, sans) : null, @@ -246,11 +345,13 @@ private string GetCertificateType(string productId) public Notifications GetNotifications(EnrollmentProductInfo productInfo) { + var notificationEmails = GetOptionalParam(productInfo, "Notification Email(s) Comma Separated"); return new Notifications { Enabled = true, - AdditionalNotificationEmails = productInfo.ProductParameters["Notification Email(s) Comma Separated"] - .Split(',').ToList() + AdditionalNotificationEmails = string.IsNullOrWhiteSpace(notificationEmails) + ? new List() + : notificationEmails.Split(',').ToList() }; } @@ -264,8 +365,8 @@ public RenewalRequest GetRenewalRequest(EnrollmentProductInfo productInfo, strin var bytes = Encoding.UTF8.GetBytes(cert); var encodedString = Convert.ToBase64String(bytes); - var commonNameValidationEmail = productInfo.ProductParameters["CN DCV Email"]; - var methodType = productInfo.ProductParameters["Domain Control Validation Method"]; + var commonNameValidationEmail = GetOptionalParam(productInfo, "CN DCV Email"); + var methodType = GetOptionalParam(productInfo, "Domain Control Validation Method"); var certificateType = GetCertificateType(productInfo.ProductID); return new RenewalRequest @@ -274,15 +375,15 @@ public RenewalRequest GetRenewalRequest(EnrollmentProductInfo productInfo, strin Csr = encodedString, ServerSoftware = "-1", CertificateType = certificateType, - Term = productInfo.ProductParameters["Term"], - ApplicantFirstName = productInfo.ProductParameters["Applicant First Name"], - ApplicantLastName = productInfo.ProductParameters["Applicant Last Name"], - ApplicantEmailAddress = productInfo.ProductParameters["Applicant Email Address"], - ApplicantPhoneNumber = productInfo.ProductParameters["Applicant Phone"], + Term = GetOptionalParam(productInfo, "Term"), + ApplicantFirstName = GetOptionalParam(productInfo, "Applicant First Name"), + ApplicantLastName = GetOptionalParam(productInfo, "Applicant Last Name"), + ApplicantEmailAddress = GetOptionalParam(productInfo, "Applicant Email Address"), + ApplicantPhoneNumber = GetOptionalParam(productInfo, "Applicant Phone"), DomainControlValidation = GetDomainControlValidation(methodType, commonNameValidationEmail), Notifications = GetNotifications(productInfo), - OrganizationContact = productInfo.ProductParameters["Organization Contact"], - BusinessUnit = productInfo.ProductParameters["Business Unit"], + OrganizationContact = GetOptionalParam(productInfo, "Organization Contact"), + BusinessUnit = GetOptionalParam(productInfo, "Business Unit"), ShowPrice = true, SubjectAlternativeNames = MultiNameCertificateTypes.Contains(certificateType) ? GetSubjectAlternativeNames(productInfo, sans) : null, CustomFields = GetCustomFields(productInfo, customFields), @@ -294,9 +395,10 @@ private List GetSubjectAlternativeNames(EnrollmentProduc Dictionary sans) { var subjectNameList = new List(); - var methodType = productInfo.ProductParameters["Domain Control Validation Method"]; + var methodType = GetOptionalParam(productInfo, "Domain Control Validation Method"); - sans.TryGetValue("dnsname", out var dnsNames); + string[] dnsNames = null; + sans?.TryGetValue("dnsname", out dnsNames); foreach (var v in dnsNames ?? Array.Empty()) { var domainName = v; @@ -329,8 +431,8 @@ public ReissueRequest GetReissueRequest(EnrollmentProductInfo productInfo, strin var bytes = Encoding.UTF8.GetBytes(cert); var encodedString = Convert.ToBase64String(bytes); - var commonNameValidationEmail = productInfo.ProductParameters["CN DCV Email"]; - var methodType = productInfo.ProductParameters["Domain Control Validation Method"]; + var commonNameValidationEmail = GetOptionalParam(productInfo, "CN DCV Email"); + var methodType = GetOptionalParam(productInfo, "Domain Control Validation Method"); var certificateType = GetCertificateType(productInfo.ProductID); return new ReissueRequest @@ -338,16 +440,16 @@ public ReissueRequest GetReissueRequest(EnrollmentProductInfo productInfo, strin Uuid = uUId, Csr = encodedString, ServerSoftware = "-1", - CertificateType = GetCertificateType(productInfo.ProductID), - Term = productInfo.ProductParameters["Term"], - ApplicantFirstName = productInfo.ProductParameters["Applicant First Name"], - ApplicantLastName = productInfo.ProductParameters["Applicant Last Name"], - ApplicantEmailAddress = productInfo.ProductParameters["Applicant Email Address"], - ApplicantPhoneNumber = productInfo.ProductParameters["Applicant Phone"], + CertificateType = certificateType, + Term = GetOptionalParam(productInfo, "Term"), + ApplicantFirstName = GetOptionalParam(productInfo, "Applicant First Name"), + ApplicantLastName = GetOptionalParam(productInfo, "Applicant Last Name"), + ApplicantEmailAddress = GetOptionalParam(productInfo, "Applicant Email Address"), + ApplicantPhoneNumber = GetOptionalParam(productInfo, "Applicant Phone"), DomainControlValidation = GetDomainControlValidation(methodType, commonNameValidationEmail), Notifications = GetNotifications(productInfo), - OrganizationContact = productInfo.ProductParameters["Organization Contact"], - BusinessUnit = productInfo.ProductParameters["Business Unit"], + OrganizationContact = GetOptionalParam(productInfo, "Organization Contact"), + BusinessUnit = GetOptionalParam(productInfo, "Business Unit"), ShowPrice = true, SubjectAlternativeNames = MultiNameCertificateTypes.Contains(certificateType) ? GetSubjectAlternativeNames(productInfo, sans) : null, CustomFields = GetCustomFields(productInfo, customFields), @@ -358,7 +460,7 @@ public ReissueRequest GetReissueRequest(EnrollmentProductInfo productInfo, strin private EvCertificateDetails GetEvCertificateDetails(EnrollmentProductInfo productInfo) { var evDetails = new EvCertificateDetails(); - evDetails.Country = productInfo.ProductParameters["Organization Country"]; + evDetails.Country = GetOptionalParam(productInfo, "Organization Country"); return evDetails; }