From fc725c42956adcfbd714b7b99cda0b57463c26fe Mon Sep 17 00:00:00 2001
From: Brian Hill
Date: Wed, 9 Sep 2026 14:23:21 -0400
Subject: [PATCH 01/36] 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 02/36] 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 03/36] 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 04/36] 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 05/36] 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 06/36] 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 07/36] 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 08/36] 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 09/36] 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;
}
From fa1b23e2aa8d854a2cce1fae8772640feac9b6a4 Mon Sep 17 00:00:00 2001
From: Brian Hill
Date: Mon, 14 Sep 2026 15:47:06 -0400
Subject: [PATCH 10/36] Sync all cert request statuses back to Command, not
just issued/revoked
Previously the CSC Global list request filtered to status=in=(ACTIVE,REVOKED)
and SyncCertificates only forwarded GENERATED/REVOKED records, so any
pending or failed request was never reported back to Command on any
sync cycle. Command eventually treats a request it hasn't seen in
recent syncs as outdated and tries to prune it, which can trigger an
internal Command cleanup bug for requests with no staged private key.
Now every request CSC Global returns is forwarded to Command each
sync, with certificate content attached only when actually issued or
revoked.
---
cscglobal-caplugin/CSCGlobalCAPlugin.cs | 66 ++++++++++----------
cscglobal-caplugin/Client/CscGlobalClient.cs | 10 +--
2 files changed, 37 insertions(+), 39 deletions(-)
diff --git a/cscglobal-caplugin/CSCGlobalCAPlugin.cs b/cscglobal-caplugin/CSCGlobalCAPlugin.cs
index f5ab830..91bd8a5 100644
--- a/cscglobal-caplugin/CSCGlobalCAPlugin.cs
+++ b/cscglobal-caplugin/CSCGlobalCAPlugin.cs
@@ -178,21 +178,25 @@ private async Task SyncCertificates(BlockingCollection b
}
var queuedCount = 0;
- var skippedCount = 0;
+ var queuedWithoutCertCount = 0;
foreach (var currentResponseItem in certs.Results)
{
cancelToken.ThrowIfCancellationRequested();
Logger.LogTrace($"Took Certificate ID {currentResponseItem?.Uuid} from Queue");
var certStatus = _requestManager.MapReturnStatus(currentResponseItem?.Status);
- //Keyfactor sync only seems to work when there is a valid cert and I can only get Active valid certs from Csc Global
- if (certStatus == Convert.ToInt32(EndEntityStatus.GENERATED) ||
- certStatus == Convert.ToInt32(EndEntityStatus.REVOKED))
- {
- //One click renewal/reissue won't work for this implementation so there is an option to disable it by not syncing back template
- var productId = "CscGlobal";
- if (EnableTemplateSync) productId = currentResponseItem?.CertificateType;
+ //Every known request is always reported back to Command, even without a certificate,
+ //so Command never considers a still-pending or failed request "outdated" and tries to
+ //prune it (which can hit an internal Command bug for requests with no staged private key).
+ var productId = "CscGlobal";
+ if (EnableTemplateSync) productId = currentResponseItem?.CertificateType;
+
+ var certString = string.Empty;
+ var hasIssuedOrRevokedCert = certStatus == Convert.ToInt32(EndEntityStatus.GENERATED) ||
+ certStatus == Convert.ToInt32(EndEntityStatus.REVOKED);
+ if (hasIssuedOrRevokedCert)
+ {
var fileContent =
PreparePemTextFromApi(
currentResponseItem?.Certificate ?? string.Empty);
@@ -201,39 +205,33 @@ private async Task SyncCertificates(BlockingCollection b
{
Logger.LogTrace($"File Content {fileContent}");
var certData = fileContent.Replace("\r\n", string.Empty);
- var certString = GetEndEntityCertificate(certData);
- if (certString.Length > 0)
- {
- blockingBuffer.Add(new AnyCAPluginCertificate
- {
- CARequestID = $"{currentResponseItem?.Uuid}",
- Certificate = certString,
- 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++;
+ certString = GetEndEntityCertificate(certData);
}
+
+ if (string.IsNullOrEmpty(certString))
+ Logger.LogWarning($"Could not extract end-entity certificate for {currentResponseItem?.Uuid} (status {currentResponseItem?.Status}); syncing status only");
}
else
{
- Logger.LogTrace($"Skipping Certificate ID {currentResponseItem?.Uuid} - status {currentResponseItem?.Status} is not eligible for sync");
- skippedCount++;
+ Logger.LogTrace($"Certificate ID {currentResponseItem?.Uuid} - status {currentResponseItem?.Status} has no certificate content yet; syncing status only");
}
+
+ blockingBuffer.Add(new AnyCAPluginCertificate
+ {
+ CARequestID = $"{currentResponseItem?.Uuid}",
+ Certificate = certString,
+ Status = certStatus,
+ ProductID = productId
+ }, cancelToken);
+
+ if (string.IsNullOrEmpty(certString))
+ queuedWithoutCertCount++;
+ else
+ queuedCount++;
}
- flow.Step("QueueCertificates", $"Queued {queuedCount}, skipped {skippedCount}");
- Logger.LogInformation($"Sync queued {queuedCount} certificate(s), skipped {skippedCount}");
+ flow.Step("QueueCertificates", $"Queued {queuedCount} with certificates, {queuedWithoutCertCount} status-only");
+ Logger.LogInformation($"Sync queued {queuedCount} certificate(s) with content, {queuedWithoutCertCount} status-only record(s)");
}
//done
diff --git a/cscglobal-caplugin/Client/CscGlobalClient.cs b/cscglobal-caplugin/Client/CscGlobalClient.cs
index ca692e2..e036d10 100644
--- a/cscglobal-caplugin/Client/CscGlobalClient.cs
+++ b/cscglobal-caplugin/Client/CscGlobalClient.cs
@@ -254,11 +254,11 @@ public async Task SubmitRevokeCertificateAsync(string uuId)
public async Task SubmitCertificateListRequestAsync(string? dateFilter = null)
{
Logger.MethodEntry(LogLevel.Debug);
- var filterQuery = "filter=status=in=(ACTIVE,REVOKED)";
- if (!string.IsNullOrEmpty(dateFilter))
- {
- filterQuery += $";effectiveDate=ge={dateFilter}";
- }
+ // Intentionally not filtering by status here: Command's sync considers any request that
+ // isn't returned in a sync cycle as "outdated" and tries to prune it, which can hit an
+ // internal Command bug for requests with no staged private key. Returning every status
+ // (not just ACTIVE/REVOKED) keeps every known request visible to Command on every sync.
+ var filterQuery = !string.IsNullOrEmpty(dateFilter) ? $"filter=effectiveDate=ge={dateFilter}" : string.Empty;
Logger.LogTrace($"Certificate list filter query: {filterQuery}");
var resp = RestClient.GetAsync($"/dbs/api/v2/tls/certificate?{filterQuery}").Result;
From 11e27470111e7503f50518b97d012876a4e15add Mon Sep 17 00:00:00 2001
From: Brian Hill
Date: Tue, 15 Sep 2026 13:03:13 -0400
Subject: [PATCH 11/36] Revert "Sync all cert request statuses back to Command,
not just issued/revoked"
This reverts commit fa1b23e2aa8d854a2cce1fae8772640feac9b6a4.
---
cscglobal-caplugin/CSCGlobalCAPlugin.cs | 66 ++++++++++----------
cscglobal-caplugin/Client/CscGlobalClient.cs | 10 +--
2 files changed, 39 insertions(+), 37 deletions(-)
diff --git a/cscglobal-caplugin/CSCGlobalCAPlugin.cs b/cscglobal-caplugin/CSCGlobalCAPlugin.cs
index 91bd8a5..f5ab830 100644
--- a/cscglobal-caplugin/CSCGlobalCAPlugin.cs
+++ b/cscglobal-caplugin/CSCGlobalCAPlugin.cs
@@ -178,25 +178,21 @@ private async Task SyncCertificates(BlockingCollection b
}
var queuedCount = 0;
- var queuedWithoutCertCount = 0;
+ var skippedCount = 0;
foreach (var currentResponseItem in certs.Results)
{
cancelToken.ThrowIfCancellationRequested();
Logger.LogTrace($"Took Certificate ID {currentResponseItem?.Uuid} from Queue");
var certStatus = _requestManager.MapReturnStatus(currentResponseItem?.Status);
- //Every known request is always reported back to Command, even without a certificate,
- //so Command never considers a still-pending or failed request "outdated" and tries to
- //prune it (which can hit an internal Command bug for requests with no staged private key).
- var productId = "CscGlobal";
- if (EnableTemplateSync) productId = currentResponseItem?.CertificateType;
-
- var certString = string.Empty;
- var hasIssuedOrRevokedCert = certStatus == Convert.ToInt32(EndEntityStatus.GENERATED) ||
- certStatus == Convert.ToInt32(EndEntityStatus.REVOKED);
-
- if (hasIssuedOrRevokedCert)
+ //Keyfactor sync only seems to work when there is a valid cert and I can only get Active valid certs from Csc Global
+ if (certStatus == Convert.ToInt32(EndEntityStatus.GENERATED) ||
+ certStatus == Convert.ToInt32(EndEntityStatus.REVOKED))
{
+ //One click renewal/reissue won't work for this implementation so there is an option to disable it by not syncing back template
+ var productId = "CscGlobal";
+ if (EnableTemplateSync) productId = currentResponseItem?.CertificateType;
+
var fileContent =
PreparePemTextFromApi(
currentResponseItem?.Certificate ?? string.Empty);
@@ -205,33 +201,39 @@ private async Task SyncCertificates(BlockingCollection b
{
Logger.LogTrace($"File Content {fileContent}");
var certData = fileContent.Replace("\r\n", string.Empty);
- certString = GetEndEntityCertificate(certData);
+ var certString = GetEndEntityCertificate(certData);
+ if (certString.Length > 0)
+ {
+ blockingBuffer.Add(new AnyCAPluginCertificate
+ {
+ CARequestID = $"{currentResponseItem?.Uuid}",
+ Certificate = certString,
+ 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++;
}
-
- if (string.IsNullOrEmpty(certString))
- Logger.LogWarning($"Could not extract end-entity certificate for {currentResponseItem?.Uuid} (status {currentResponseItem?.Status}); syncing status only");
}
else
{
- Logger.LogTrace($"Certificate ID {currentResponseItem?.Uuid} - status {currentResponseItem?.Status} has no certificate content yet; syncing status only");
+ Logger.LogTrace($"Skipping Certificate ID {currentResponseItem?.Uuid} - status {currentResponseItem?.Status} is not eligible for sync");
+ skippedCount++;
}
-
- blockingBuffer.Add(new AnyCAPluginCertificate
- {
- CARequestID = $"{currentResponseItem?.Uuid}",
- Certificate = certString,
- Status = certStatus,
- ProductID = productId
- }, cancelToken);
-
- if (string.IsNullOrEmpty(certString))
- queuedWithoutCertCount++;
- else
- queuedCount++;
}
- flow.Step("QueueCertificates", $"Queued {queuedCount} with certificates, {queuedWithoutCertCount} status-only");
- Logger.LogInformation($"Sync queued {queuedCount} certificate(s) with content, {queuedWithoutCertCount} status-only record(s)");
+ flow.Step("QueueCertificates", $"Queued {queuedCount}, skipped {skippedCount}");
+ Logger.LogInformation($"Sync queued {queuedCount} certificate(s), skipped {skippedCount}");
}
//done
diff --git a/cscglobal-caplugin/Client/CscGlobalClient.cs b/cscglobal-caplugin/Client/CscGlobalClient.cs
index e036d10..ca692e2 100644
--- a/cscglobal-caplugin/Client/CscGlobalClient.cs
+++ b/cscglobal-caplugin/Client/CscGlobalClient.cs
@@ -254,11 +254,11 @@ public async Task SubmitRevokeCertificateAsync(string uuId)
public async Task SubmitCertificateListRequestAsync(string? dateFilter = null)
{
Logger.MethodEntry(LogLevel.Debug);
- // Intentionally not filtering by status here: Command's sync considers any request that
- // isn't returned in a sync cycle as "outdated" and tries to prune it, which can hit an
- // internal Command bug for requests with no staged private key. Returning every status
- // (not just ACTIVE/REVOKED) keeps every known request visible to Command on every sync.
- var filterQuery = !string.IsNullOrEmpty(dateFilter) ? $"filter=effectiveDate=ge={dateFilter}" : string.Empty;
+ var filterQuery = "filter=status=in=(ACTIVE,REVOKED)";
+ if (!string.IsNullOrEmpty(dateFilter))
+ {
+ filterQuery += $";effectiveDate=ge={dateFilter}";
+ }
Logger.LogTrace($"Certificate list filter query: {filterQuery}");
var resp = RestClient.GetAsync($"/dbs/api/v2/tls/certificate?{filterQuery}").Result;
From 41a2e2261b7f4172bf775abec0c031bea6d9fd0b Mon Sep 17 00:00:00 2001
From: Brian Hill
Date: Tue, 15 Sep 2026 14:41:40 -0400
Subject: [PATCH 12/36] Map CSC Global's current product names back to plugin
ProductIDs during sync
CSC Global renamed its product line at some point; its sync/list API
now returns current names (e.g. "CSC TrustedSecure DV") while this
plugin's ProductIDs/GetCertificateType still use the legacy names it
shipped with (e.g. "CSC TrustedSecure Domain Validated SSL"). Syncing
a cert back set ProductID directly from CSC's current name, which
never matched Command's Certificate Profile/Template mappings (keyed
on the legacy names) for 7 of the 10 product types. Add a reverse
lookup so synced certificates resolve to the same ProductID used
during enrollment.
---
cscglobal-caplugin/CSCGlobalCAPlugin.cs | 2 +-
cscglobal-caplugin/RequestManager.cs | 30 +++++++++++++++++++++++++
2 files changed, 31 insertions(+), 1 deletion(-)
diff --git a/cscglobal-caplugin/CSCGlobalCAPlugin.cs b/cscglobal-caplugin/CSCGlobalCAPlugin.cs
index f5ab830..ac23102 100644
--- a/cscglobal-caplugin/CSCGlobalCAPlugin.cs
+++ b/cscglobal-caplugin/CSCGlobalCAPlugin.cs
@@ -191,7 +191,7 @@ private async Task SyncCertificates(BlockingCollection b
{
//One click renewal/reissue won't work for this implementation so there is an option to disable it by not syncing back template
var productId = "CscGlobal";
- if (EnableTemplateSync) productId = currentResponseItem?.CertificateType;
+ if (EnableTemplateSync) productId = _requestManager.MapCertificateTypeToProductId(currentResponseItem?.CertificateType);
var fileContent =
PreparePemTextFromApi(
diff --git a/cscglobal-caplugin/RequestManager.cs b/cscglobal-caplugin/RequestManager.cs
index b580e71..b69f6b4 100644
--- a/cscglobal-caplugin/RequestManager.cs
+++ b/cscglobal-caplugin/RequestManager.cs
@@ -340,6 +340,36 @@ private string GetCertificateType(string productId)
return "-1";
}
+ // CSC Global's API returns its current product names in sync/list responses (e.g. "CSC TrustedSecure DV"),
+ // which differ from the legacy names this plugin's ProductIDs/GetCertificateType use as Command's ProductID
+ // (e.g. "CSC TrustedSecure Domain Validated SSL"). Map back to our legacy names so Command's Certificate
+ // Profile/Template mapping (keyed on our ProductID) can resolve synced certificates.
+ private static readonly Dictionary CscCertificateTypeToProductId = new(StringComparer.OrdinalIgnoreCase)
+ {
+ ["CSC TrustedSecure OV"] = "CSC TrustedSecure Premium Certificate",
+ ["CSC TrustedSecure OV Wildcard"] = "CSC TrustedSecure Premium Wildcard Certificate",
+ ["CSC TrustedSecure OV, Multiple Names"] = "CSC TrustedSecure UC Certificate",
+ ["CSC TrustedSecure EV"] = "CSC TrustedSecure EV Certificate",
+ ["CSC TrustedSecure DV"] = "CSC TrustedSecure Domain Validated SSL",
+ ["CSC TrustedSecure DV Wildcard"] = "CSC TrustedSecure Domain Validated Wildcard SSL",
+ ["CSC TrustedSecure DV, Multiple Names"] = "CSC TrustedSecure Domain Validated UC Certificate",
+ ["CSC TrustedSecure EV, Multiple Names"] = "CSC TrustedSecure EV, Multiple Names",
+ ["CSC TrustedSecure OV Wildcard, Multiple Names"] = "CSC TrustedSecure OV Wildcard, Multiple Names",
+ ["CSC TrustedSecure DV Wildcard, Multiple Names"] = "CSC TrustedSecure DV Wildcard, Multiple Names"
+ };
+
+ public string MapCertificateTypeToProductId(string cscCertificateType)
+ {
+ if (string.IsNullOrEmpty(cscCertificateType))
+ return cscCertificateType;
+
+ if (CscCertificateTypeToProductId.TryGetValue(cscCertificateType, out var productId))
+ return productId;
+
+ Logger.LogWarning($"Unrecognized CSC Global certificate type '{cscCertificateType}' returned during sync; using it as-is for ProductID");
+ return cscCertificateType;
+ }
+
private static readonly HashSet MultiNameCertificateTypes = new() { "2", "7", "8", "9" };
private static readonly HashSet EvCertificateTypes = new() { "3", "7" };
From 3d4f832ce859b3b9200371146b1ff7e8b81e2caf Mon Sep 17 00:00:00 2001
From: Brian Hill
Date: Tue, 15 Sep 2026 15:48:33 -0400
Subject: [PATCH 13/36] Rename all CSC certificate products to match CSC's
current naming
Drop the legacy product-name compatibility layer (GetCertificateType's
duplicate/old-name cases, the CscCertificateTypeToProductId sync mapping)
and use CSC's certificate type names directly everywhere: CSC TrustedSecure
OV/OV Wildcard/OV Multiple Names/EV/DV/DV Wildcard/DV Multiple Names plus
the three new EV/OV Wildcard/DV Wildcard Multiple Names products. There is
no template mapping persisted in the gateway database, so no migration is
needed, but existing Command Certificate Templates must have their
Template Short Name updated to the new names.
---
CHANGELOG.md | 1 +
cscglobal-caplugin/CSCGlobalCAPlugin.cs | 2 +-
cscglobal-caplugin/Constants.cs | 14 ++---
cscglobal-caplugin/RequestManager.cs | 54 ++++---------------
docsource/configuration.md | 71 ++++++++++++-------------
integration-manifest.json | 14 ++---
6 files changed, 60 insertions(+), 96 deletions(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 367a89c..aa4a9f3 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +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
+- BREAKING: Renamed all certificate template product IDs to match CSC's current certificate type names (e.g. "CSC TrustedSecure Premium Certificate" is now "CSC TrustedSecure OV", "CSC TrustedSecure Domain Validated SSL" is now "CSC TrustedSecure DV"). Existing Certificate Templates in Command must have their Template Short Name updated to match.
v.1.1.1
- Added Incremental Sync that goes back X Number of days
diff --git a/cscglobal-caplugin/CSCGlobalCAPlugin.cs b/cscglobal-caplugin/CSCGlobalCAPlugin.cs
index ac23102..e88d7d4 100644
--- a/cscglobal-caplugin/CSCGlobalCAPlugin.cs
+++ b/cscglobal-caplugin/CSCGlobalCAPlugin.cs
@@ -191,7 +191,7 @@ private async Task SyncCertificates(BlockingCollection b
{
//One click renewal/reissue won't work for this implementation so there is an option to disable it by not syncing back template
var productId = "CscGlobal";
- if (EnableTemplateSync) productId = _requestManager.MapCertificateTypeToProductId(currentResponseItem?.CertificateType);
+ if (EnableTemplateSync) productId = currentResponseItem?.CertificateType ?? productId;
var fileContent =
PreparePemTextFromApi(
diff --git a/cscglobal-caplugin/Constants.cs b/cscglobal-caplugin/Constants.cs
index fd79d7a..e8dc28a 100644
--- a/cscglobal-caplugin/Constants.cs
+++ b/cscglobal-caplugin/Constants.cs
@@ -21,13 +21,13 @@ public class ProductIDs
{
public static List productIds = new List()
{
- "CSC TrustedSecure Premium Certificate",
- "CSC TrustedSecure EV Certificate",
- "CSC TrustedSecure UC Certificate",
- "CSC TrustedSecure Premium Wildcard Certificate",
- "CSC TrustedSecure Domain Validated SSL",
- "CSC TrustedSecure Domain Validated Wildcard SSL",
- "CSC TrustedSecure Domain Validated UC Certificate",
+ "CSC TrustedSecure OV",
+ "CSC TrustedSecure OV Wildcard",
+ "CSC TrustedSecure OV, Multiple Names",
+ "CSC TrustedSecure EV",
+ "CSC TrustedSecure DV",
+ "CSC TrustedSecure DV Wildcard",
+ "CSC TrustedSecure DV, Multiple Names",
"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 b69f6b4..d689e66 100644
--- a/cscglobal-caplugin/RequestManager.cs
+++ b/cscglobal-caplugin/RequestManager.cs
@@ -308,25 +308,19 @@ private string GetCertificateType(string productId)
{
switch (productId)
{
- case "CSC TrustedSecure Premium Certificate":
+ case "CSC TrustedSecure OV":
return "0";
- case "CSC TrustedSecure EV Certificate":
- return "3";
- case "CSC TrustedSecure UC Certificate":
- return "2";
- case "CSC TrustedSecure Premium Wildcard Certificate":
+ case "CSC TrustedSecure OV Wildcard":
return "1";
- case "CSC Trusted Secure Domain Validated SSL":
- return "4";
- case "CSC Trusted Secure Domain Validated Wildcard SSL":
- return "5";
- case "CSC Trusted Secure Domain Validated UC Certificate":
- return "6";
- case "CSC TrustedSecure Domain Validated SSL":
+ case "CSC TrustedSecure OV, Multiple Names":
+ return "2";
+ case "CSC TrustedSecure EV":
+ return "3";
+ case "CSC TrustedSecure DV":
return "4";
- case "CSC TrustedSecure Domain Validated Wildcard SSL":
+ case "CSC TrustedSecure DV Wildcard":
return "5";
- case "CSC TrustedSecure Domain Validated UC Certificate":
+ case "CSC TrustedSecure DV, Multiple Names":
return "6";
case "CSC TrustedSecure EV, Multiple Names":
return "7";
@@ -340,36 +334,6 @@ private string GetCertificateType(string productId)
return "-1";
}
- // CSC Global's API returns its current product names in sync/list responses (e.g. "CSC TrustedSecure DV"),
- // which differ from the legacy names this plugin's ProductIDs/GetCertificateType use as Command's ProductID
- // (e.g. "CSC TrustedSecure Domain Validated SSL"). Map back to our legacy names so Command's Certificate
- // Profile/Template mapping (keyed on our ProductID) can resolve synced certificates.
- private static readonly Dictionary CscCertificateTypeToProductId = new(StringComparer.OrdinalIgnoreCase)
- {
- ["CSC TrustedSecure OV"] = "CSC TrustedSecure Premium Certificate",
- ["CSC TrustedSecure OV Wildcard"] = "CSC TrustedSecure Premium Wildcard Certificate",
- ["CSC TrustedSecure OV, Multiple Names"] = "CSC TrustedSecure UC Certificate",
- ["CSC TrustedSecure EV"] = "CSC TrustedSecure EV Certificate",
- ["CSC TrustedSecure DV"] = "CSC TrustedSecure Domain Validated SSL",
- ["CSC TrustedSecure DV Wildcard"] = "CSC TrustedSecure Domain Validated Wildcard SSL",
- ["CSC TrustedSecure DV, Multiple Names"] = "CSC TrustedSecure Domain Validated UC Certificate",
- ["CSC TrustedSecure EV, Multiple Names"] = "CSC TrustedSecure EV, Multiple Names",
- ["CSC TrustedSecure OV Wildcard, Multiple Names"] = "CSC TrustedSecure OV Wildcard, Multiple Names",
- ["CSC TrustedSecure DV Wildcard, Multiple Names"] = "CSC TrustedSecure DV Wildcard, Multiple Names"
- };
-
- public string MapCertificateTypeToProductId(string cscCertificateType)
- {
- if (string.IsNullOrEmpty(cscCertificateType))
- return cscCertificateType;
-
- if (CscCertificateTypeToProductId.TryGetValue(cscCertificateType, out var productId))
- return productId;
-
- Logger.LogWarning($"Unrecognized CSC Global certificate type '{cscCertificateType}' returned during sync; using it as-is for ProductID");
- return cscCertificateType;
- }
-
private static readonly HashSet MultiNameCertificateTypes = new() { "2", "7", "8", "9" };
private static readonly HashSet EvCertificateTypes = new() { "3", "7" };
diff --git a/docsource/configuration.md b/docsource/configuration.md
index 63cc066..20053a4 100644
--- a/docsource/configuration.md
+++ b/docsource/configuration.md
@@ -20,16 +20,16 @@ If a field value is specified as both an Enrollment Field in Command and in the
CONFIG ELEMENT | DESCRIPTION
----------------------------|------------------
-Template Short Name | CSC TrustedSecure Premium Certificate
-Template Display Name | CSC TrustedSecure Premium Certificate
-Friendly Name | CSC TrustedSecure Premium Certificate
+Template Short Name | CSC TrustedSecure OV
+Template Display Name | CSC TrustedSecure OV
+Friendly Name | CSC TrustedSecure OV
Keys Size | 2048
Enforce RFC 2818 Compliance | True
CSR Enrollment | True
Pfx Enrollment | True
-**CSC TrustedSecure Premium Certificate - Enrollment Fields**
+**CSC TrustedSecure OV - Enrollment Fields**
NAME | DATA TYPE | VALUES
-----|--------------|-----------------
@@ -44,20 +44,20 @@ 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**
+**CSC TrustedSecure OV Wildcard - 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
+Template Short Name | CSC TrustedSecure OV Wildcard
+Template Display Name | CSC TrustedSecure OV Wildcard
+Friendly Name | CSC TrustedSecure OV Wildcard
Keys Size | 2048
Enforce RFC 2818 Compliance | True
CSR Enrollment | True
Pfx Enrollment | True
-**CSC TrustedSecure EV Certificate - Enrollment Fields**
+**CSC TrustedSecure OV Wildcard - Enrollment Fields**
NAME | DATA TYPE | VALUES
-----|--------------|-----------------
@@ -71,22 +71,21 @@ 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**
+**CSC TrustedSecure OV, Multiple Names - 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
+Template Short Name | CSC TrustedSecure OV, Multiple Names
+Template Display Name | CSC TrustedSecure OV, Multiple Names
+Friendly Name | CSC TrustedSecure OV, Multiple Names
Keys Size | 2048
Enforce RFC 2818 Compliance | True
CSR Enrollment | True
Pfx Enrollment | True
-**CSC TrustedSecure UC Certificate - Enrollment Fields**
+**CSC TrustedSecure OV, Multiple Names - Enrollment Fields**
NAME | DATA TYPE | VALUES
-----|--------------|-----------------
@@ -101,22 +100,21 @@ 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**
+**CSC TrustedSecure EV - 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
+Template Short Name | CSC TrustedSecure EV
+Template Display Name | CSC TrustedSecure EV
+Friendly Name | CSC TrustedSecure EV
Keys Size | 2048
Enforce RFC 2818 Compliance | True
CSR Enrollment | True
Pfx Enrollment | True
-**CSC TrustedSecure Premium Wildcard Certificate - Enrollment Fields**
+**CSC TrustedSecure EV - Enrollment Fields**
NAME | DATA TYPE | VALUES
-----|--------------|-----------------
@@ -130,21 +128,22 @@ 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 Domain Validated SSL - Details Tab**
+**CSC TrustedSecure DV - 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
+Template Short Name | CSC TrustedSecure DV
+Template Display Name | CSC TrustedSecure DV
+Friendly Name | CSC TrustedSecure DV
Keys Size | 2048
Enforce RFC 2818 Compliance | True
CSR Enrollment | True
Pfx Enrollment | True
-**CSC TrustedSecure Domain Validated SSL - Enrollment Fields**
+**CSC TrustedSecure DV - Enrollment Fields**
NAME | DATA TYPE | VALUES
-----|--------------|-----------------
@@ -159,20 +158,20 @@ 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**
+**CSC TrustedSecure DV Wildcard - 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
+Template Short Name | CSC TrustedSecure DV Wildcard
+Template Display Name | CSC TrustedSecure DV Wildcard
+Friendly Name | CSC TrustedSecure DV Wildcard
Keys Size | 2048
Enforce RFC 2818 Compliance | True
CSR Enrollment | True
Pfx Enrollment | True
-**CSC TrustedSecure Domain Validated Wildcard SSL - Enrollment Fields**
+**CSC TrustedSecure DV Wildcard - Enrollment Fields**
NAME | DATA TYPE | VALUES
-----|--------------|-----------------
@@ -187,20 +186,20 @@ 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**
+**CSC TrustedSecure DV, Multiple Names - 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
+Template Short Name | CSC TrustedSecure DV, Multiple Names
+Template Display Name | CSC TrustedSecure DV, Multiple Names
+Friendly Name | CSC TrustedSecure DV, Multiple Names
Keys Size | 2048
Enforce RFC 2818 Compliance | True
CSR Enrollment | True
Pfx Enrollment | True
-**CSC TrustedSecure Domain Validated UC Certificate - Enrollment Fields**
+**CSC TrustedSecure DV, Multiple Names - Enrollment Fields**
NAME | DATA TYPE | VALUES
-----|--------------|-----------------
diff --git a/integration-manifest.json b/integration-manifest.json
index 756b19a..44f826d 100644
--- a/integration-manifest.json
+++ b/integration-manifest.json
@@ -89,13 +89,13 @@
}
],
"product_ids": [
- "CSC TrustedSecure Premium Certificate",
- "CSC TrustedSecure EV Certificate",
- "CSC TrustedSecure UC Certificate",
- "CSC TrustedSecure Premium Wildcard Certificate",
- "CSC TrustedSecure Domain Validated SSL",
- "CSC TrustedSecure Domain Validated Wildcard SSL",
- "CSC TrustedSecure Domain Validated UC Certificate",
+ "CSC TrustedSecure OV",
+ "CSC TrustedSecure OV Wildcard",
+ "CSC TrustedSecure OV, Multiple Names",
+ "CSC TrustedSecure EV",
+ "CSC TrustedSecure DV",
+ "CSC TrustedSecure DV Wildcard",
+ "CSC TrustedSecure DV, Multiple Names",
"CSC TrustedSecure EV, Multiple Names",
"CSC TrustedSecure OV Wildcard, Multiple Names",
"CSC TrustedSecure DV Wildcard, Multiple Names"
From 08681db3964fd8b84a25509c482da3d8d832d1d9 Mon Sep 17 00:00:00 2001
From: "github-actions[bot]"
Date: Tue, 15 Sep 2026 19:49:10 +0000
Subject: [PATCH 14/36] docs: auto-generate README and documentation [skip ci]
---
README.md | 71 +++++++++++++++++++++++++++----------------------------
1 file changed, 35 insertions(+), 36 deletions(-)
diff --git a/README.md b/README.md
index 3ffe2b0..be687f8 100644
--- a/README.md
+++ b/README.md
@@ -96,16 +96,16 @@ If a field value is specified as both an Enrollment Field in Command and in the
CONFIG ELEMENT | DESCRIPTION
----------------------------|------------------
-Template Short Name | CSC TrustedSecure Premium Certificate
-Template Display Name | CSC TrustedSecure Premium Certificate
-Friendly Name | CSC TrustedSecure Premium Certificate
+Template Short Name | CSC TrustedSecure OV
+Template Display Name | CSC TrustedSecure OV
+Friendly Name | CSC TrustedSecure OV
Keys Size | 2048
Enforce RFC 2818 Compliance | True
CSR Enrollment | True
Pfx Enrollment | True
-**CSC TrustedSecure Premium Certificate - Enrollment Fields**
+**CSC TrustedSecure OV - Enrollment Fields**
NAME | DATA TYPE | VALUES
-----|--------------|-----------------
@@ -120,20 +120,20 @@ 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**
+**CSC TrustedSecure OV Wildcard - 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
+Template Short Name | CSC TrustedSecure OV Wildcard
+Template Display Name | CSC TrustedSecure OV Wildcard
+Friendly Name | CSC TrustedSecure OV Wildcard
Keys Size | 2048
Enforce RFC 2818 Compliance | True
CSR Enrollment | True
Pfx Enrollment | True
-**CSC TrustedSecure EV Certificate - Enrollment Fields**
+**CSC TrustedSecure OV Wildcard - Enrollment Fields**
NAME | DATA TYPE | VALUES
-----|--------------|-----------------
@@ -147,22 +147,21 @@ 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**
+**CSC TrustedSecure OV, Multiple Names - 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
+Template Short Name | CSC TrustedSecure OV, Multiple Names
+Template Display Name | CSC TrustedSecure OV, Multiple Names
+Friendly Name | CSC TrustedSecure OV, Multiple Names
Keys Size | 2048
Enforce RFC 2818 Compliance | True
CSR Enrollment | True
Pfx Enrollment | True
-**CSC TrustedSecure UC Certificate - Enrollment Fields**
+**CSC TrustedSecure OV, Multiple Names - Enrollment Fields**
NAME | DATA TYPE | VALUES
-----|--------------|-----------------
@@ -177,22 +176,21 @@ 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**
+**CSC TrustedSecure EV - 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
+Template Short Name | CSC TrustedSecure EV
+Template Display Name | CSC TrustedSecure EV
+Friendly Name | CSC TrustedSecure EV
Keys Size | 2048
Enforce RFC 2818 Compliance | True
CSR Enrollment | True
Pfx Enrollment | True
-**CSC TrustedSecure Premium Wildcard Certificate - Enrollment Fields**
+**CSC TrustedSecure EV - Enrollment Fields**
NAME | DATA TYPE | VALUES
-----|--------------|-----------------
@@ -206,21 +204,22 @@ 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 Domain Validated SSL - Details Tab**
+**CSC TrustedSecure DV - 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
+Template Short Name | CSC TrustedSecure DV
+Template Display Name | CSC TrustedSecure DV
+Friendly Name | CSC TrustedSecure DV
Keys Size | 2048
Enforce RFC 2818 Compliance | True
CSR Enrollment | True
Pfx Enrollment | True
-**CSC TrustedSecure Domain Validated SSL - Enrollment Fields**
+**CSC TrustedSecure DV - Enrollment Fields**
NAME | DATA TYPE | VALUES
-----|--------------|-----------------
@@ -235,20 +234,20 @@ 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**
+**CSC TrustedSecure DV Wildcard - 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
+Template Short Name | CSC TrustedSecure DV Wildcard
+Template Display Name | CSC TrustedSecure DV Wildcard
+Friendly Name | CSC TrustedSecure DV Wildcard
Keys Size | 2048
Enforce RFC 2818 Compliance | True
CSR Enrollment | True
Pfx Enrollment | True
-**CSC TrustedSecure Domain Validated Wildcard SSL - Enrollment Fields**
+**CSC TrustedSecure DV Wildcard - Enrollment Fields**
NAME | DATA TYPE | VALUES
-----|--------------|-----------------
@@ -263,20 +262,20 @@ 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**
+**CSC TrustedSecure DV, Multiple Names - 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
+Template Short Name | CSC TrustedSecure DV, Multiple Names
+Template Display Name | CSC TrustedSecure DV, Multiple Names
+Friendly Name | CSC TrustedSecure DV, Multiple Names
Keys Size | 2048
Enforce RFC 2818 Compliance | True
CSR Enrollment | True
Pfx Enrollment | True
-**CSC TrustedSecure Domain Validated UC Certificate - Enrollment Fields**
+**CSC TrustedSecure DV, Multiple Names - Enrollment Fields**
NAME | DATA TYPE | VALUES
-----|--------------|-----------------
From f3319784b0f565753878fe9f6716ad6565b444fa Mon Sep 17 00:00:00 2001
From: Brian Hill
Date: Tue, 15 Sep 2026 16:13:41 -0400
Subject: [PATCH 15/36] Always sync certificate template mapping, remove
TemplateSync setting
TemplateSync defaulted to off and was never turned on, so template sync
never ran. There is no reason to keep it opt-in, so always resolve
synced certificates back to their CSC certificate type/template and
drop the gateway registration setting.
---
CHANGELOG.md | 1 +
cscglobal-caplugin/CSCGlobalCAPlugin.cs | 19 +------------------
cscglobal-caplugin/Constants.cs | 1 -
integration-manifest.json | 4 ----
4 files changed, 2 insertions(+), 23 deletions(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index aa4a9f3..ed77d06 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,6 +1,7 @@
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
- BREAKING: Renamed all certificate template product IDs to match CSC's current certificate type names (e.g. "CSC TrustedSecure Premium Certificate" is now "CSC TrustedSecure OV", "CSC TrustedSecure Domain Validated SSL" is now "CSC TrustedSecure DV"). Existing Certificate Templates in Command must have their Template Short Name updated to match.
+- Removed the TemplateSync gateway registration setting. Certificate sync now always maps synced certificates back to their CSC certificate type/template.
v.1.1.1
- Added Incremental Sync that goes back X Number of days
diff --git a/cscglobal-caplugin/CSCGlobalCAPlugin.cs b/cscglobal-caplugin/CSCGlobalCAPlugin.cs
index e88d7d4..21149d2 100644
--- a/cscglobal-caplugin/CSCGlobalCAPlugin.cs
+++ b/cscglobal-caplugin/CSCGlobalCAPlugin.cs
@@ -35,8 +35,6 @@ public CSCGlobalCAPlugin()
private ICscGlobalClient CscGlobalClient { get; set; }
- public bool EnableTemplateSync { get; set; }
-
public int SyncFilterDays { get; set; }
//done
@@ -47,12 +45,6 @@ public void Initialize(IAnyCAPluginConfigProvider configProvider, ICertificateDa
_certificateDataReader = certificateDataReader ?? throw new ArgumentNullException(nameof(certificateDataReader));
CscGlobalClient = new CscGlobalClient(configProvider);
- 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))
{
var syncFilterDaysStr = configProvider.CAConnectionData[Constants.SyncFilterDays]?.ToString();
@@ -189,9 +181,7 @@ private async Task SyncCertificates(BlockingCollection b
if (certStatus == Convert.ToInt32(EndEntityStatus.GENERATED) ||
certStatus == Convert.ToInt32(EndEntityStatus.REVOKED))
{
- //One click renewal/reissue won't work for this implementation so there is an option to disable it by not syncing back template
- var productId = "CscGlobal";
- if (EnableTemplateSync) productId = currentResponseItem?.CertificateType ?? productId;
+ var productId = currentResponseItem?.CertificateType ?? "CscGlobal";
var fileContent =
PreparePemTextFromApi(
@@ -552,13 +542,6 @@ public Dictionary GetCAConnectorAnnotations()
DefaultValue = "100",
Type = "String"
},
- [Constants.TemplateSync] = new()
- {
- Comments = "Enable template sync.",
- Hidden = false,
- DefaultValue = "false",
- Type = "Bool"
- },
[Constants.SyncFilterDays] = new()
{
Comments = "Number of days from today to filter certificates by expiration date during incremental sync.",
diff --git a/cscglobal-caplugin/Constants.cs b/cscglobal-caplugin/Constants.cs
index e8dc28a..557dee4 100644
--- a/cscglobal-caplugin/Constants.cs
+++ b/cscglobal-caplugin/Constants.cs
@@ -13,7 +13,6 @@ public class Constants
public static string CscGlobalApiKey = "ApiKey";
public static string BearerToken = "BearerToken";
public static string DefaultPageSize = "DefaultPageSize";
- public static string TemplateSync = "TemplateSync";
public static string SyncFilterDays = "SyncFilterDays";
}
diff --git a/integration-manifest.json b/integration-manifest.json
index 44f826d..17315d8 100644
--- a/integration-manifest.json
+++ b/integration-manifest.json
@@ -29,10 +29,6 @@
"name": "DefaultPageSize",
"description": "Default page size for use with the API. Default is 100"
},
- {
- "name": "TemplateSync",
- "description": "Enable template sync."
- },
{
"name": "SyncFilterDays",
"description": "Number of days from today to filter certificates by expiration date during incremental sync."
From 10f090f5a3465d89d4338b57cedba49c7ff22439 Mon Sep 17 00:00:00 2001
From: "github-actions[bot]"
Date: Tue, 15 Sep 2026 20:14:27 +0000
Subject: [PATCH 16/36] docs: auto-generate README and documentation [skip ci]
---
README.md | 1 -
1 file changed, 1 deletion(-)
diff --git a/README.md b/README.md
index be687f8..77cc6c6 100644
--- a/README.md
+++ b/README.md
@@ -85,7 +85,6 @@ This integration is tested and confirmed as working for Anygateway REST 24.2 and
* **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
From fd22d7eae806c7df636018cc1bb1226411cc2543 Mon Sep 17 00:00:00 2001
From: Brian Hill
Date: Tue, 15 Sep 2026 16:15:31 -0400
Subject: [PATCH 17/36] Log the resolved ProductID when queueing a synced
certificate
---
cscglobal-caplugin/CSCGlobalCAPlugin.cs | 1 +
1 file changed, 1 insertion(+)
diff --git a/cscglobal-caplugin/CSCGlobalCAPlugin.cs b/cscglobal-caplugin/CSCGlobalCAPlugin.cs
index 21149d2..1cc9455 100644
--- a/cscglobal-caplugin/CSCGlobalCAPlugin.cs
+++ b/cscglobal-caplugin/CSCGlobalCAPlugin.cs
@@ -194,6 +194,7 @@ private async Task SyncCertificates(BlockingCollection b
var certString = GetEndEntityCertificate(certData);
if (certString.Length > 0)
{
+ Logger.LogTrace($"Queueing Certificate ID {currentResponseItem?.Uuid} with ProductID '{productId}'");
blockingBuffer.Add(new AnyCAPluginCertificate
{
CARequestID = $"{currentResponseItem?.Uuid}",
From 34d0542449982ddd71994231ca2a67fe6a55ae34 Mon Sep 17 00:00:00 2001
From: Brian Hill
Date: Tue, 15 Sep 2026 16:29:58 -0400
Subject: [PATCH 18/36] Leave ProductID null when CSC returns no
certificateType during sync
Matches the pattern used in hydrantid-caplugin: pass the resolved value
through as-is rather than substituting a fallback string that looks
like a real product name.
---
cscglobal-caplugin/CSCGlobalCAPlugin.cs | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/cscglobal-caplugin/CSCGlobalCAPlugin.cs b/cscglobal-caplugin/CSCGlobalCAPlugin.cs
index 1cc9455..d2ae007 100644
--- a/cscglobal-caplugin/CSCGlobalCAPlugin.cs
+++ b/cscglobal-caplugin/CSCGlobalCAPlugin.cs
@@ -181,7 +181,7 @@ private async Task SyncCertificates(BlockingCollection b
if (certStatus == Convert.ToInt32(EndEntityStatus.GENERATED) ||
certStatus == Convert.ToInt32(EndEntityStatus.REVOKED))
{
- var productId = currentResponseItem?.CertificateType ?? "CscGlobal";
+ var productId = currentResponseItem?.CertificateType;
var fileContent =
PreparePemTextFromApi(
@@ -194,7 +194,7 @@ private async Task SyncCertificates(BlockingCollection b
var certString = GetEndEntityCertificate(certData);
if (certString.Length > 0)
{
- Logger.LogTrace($"Queueing Certificate ID {currentResponseItem?.Uuid} with ProductID '{productId}'");
+ Logger.LogTrace($"Queueing Certificate ID {currentResponseItem?.Uuid} with ProductID '{productId ?? "(null)"}'");
blockingBuffer.Add(new AnyCAPluginCertificate
{
CARequestID = $"{currentResponseItem?.Uuid}",
From 71bf6343d7b55b5ba0ff47d19f4f905c2f31f6a8 Mon Sep 17 00:00:00 2001
From: Brian Hill
Date: Tue, 15 Sep 2026 16:57:01 -0400
Subject: [PATCH 19/36] Add xUnit test suite (158 tests, ~96% line coverage)
Add a CSCGlobalCAPlugin.Tests project modeled on hydrantid-caplugin's
test setup (xUnit, Moq, coverlet). Covers RequestManager, FlowLogger,
CscGlobalClient (via a fake HttpMessageHandler), and CSCGlobalCAPlugin's
orchestration logic (Synchronize, Enroll, Revoke, GetSingleRecord,
GetEndEntityCertificate, annotations).
Rework needed for testability:
- CSCGlobalCAPlugin now resolves its ICscGlobalClient through an
internal ClientFactory delegate instead of constructing one eagerly
in Initialize, so tests can substitute a mock.
- CscGlobalClient gained an internal constructor overload that accepts
an HttpMessageHandler, so HTTP calls can be intercepted without a
real network client.
- Added AssemblyInfo.cs with InternalsVisibleTo for the test project.
Bugs found and fixed while writing tests:
- MultiNameCertificateTypes was missing certificate type "6" (CSC
TrustedSecure DV, Multiple Names), so that product never got
SubjectAlternativeNames populated despite its name.
- The prior-cert-serial-number extraction in Enroll() gated on the
lowercase key "priorcertsn" but read the properly-cased
"PriorCertSN", so priorSn was effectively never populated and
Renew/Reissue enrollments always failed with "no prior certificate
serial number was supplied".
Also fixed a blocking .Result call in
CscGlobalClient.SubmitCertificateListRequestAsync to use await.
---
.../CSCGlobalCAPlugin.Tests.csproj | 30 +
.../CSCGlobalCAPluginTests.cs | 866 ++++++++++++++++++
.../CscGlobalClientTests.cs | 385 ++++++++
cscglobal-caplugin.Tests/FlowLoggerTests.cs | 194 ++++
.../RequestManagerTests.cs | 547 +++++++++++
cscglobal-caplugin.Tests/coverlet.runsettings | 12 +
cscglobal-caplugin.sln | 8 +
cscglobal-caplugin/AssemblyInfo.cs | 10 +
cscglobal-caplugin/CSCGlobalCAPlugin.cs | 11 +-
cscglobal-caplugin/Client/CscGlobalClient.cs | 15 +-
cscglobal-caplugin/RequestManager.cs | 2 +-
11 files changed, 2070 insertions(+), 10 deletions(-)
create mode 100644 cscglobal-caplugin.Tests/CSCGlobalCAPlugin.Tests.csproj
create mode 100644 cscglobal-caplugin.Tests/CSCGlobalCAPluginTests.cs
create mode 100644 cscglobal-caplugin.Tests/CscGlobalClientTests.cs
create mode 100644 cscglobal-caplugin.Tests/FlowLoggerTests.cs
create mode 100644 cscglobal-caplugin.Tests/RequestManagerTests.cs
create mode 100644 cscglobal-caplugin.Tests/coverlet.runsettings
create mode 100644 cscglobal-caplugin/AssemblyInfo.cs
diff --git a/cscglobal-caplugin.Tests/CSCGlobalCAPlugin.Tests.csproj b/cscglobal-caplugin.Tests/CSCGlobalCAPlugin.Tests.csproj
new file mode 100644
index 0000000..b65ca5b
--- /dev/null
+++ b/cscglobal-caplugin.Tests/CSCGlobalCAPlugin.Tests.csproj
@@ -0,0 +1,30 @@
+
+
+
+ net8.0
+ enable
+ enable
+ false
+ true
+ CSCGlobalCAPlugin.Tests
+
+
+
+
+
+
+
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+ all
+
+
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+ all
+
+
+
+
+
+
+
+
diff --git a/cscglobal-caplugin.Tests/CSCGlobalCAPluginTests.cs b/cscglobal-caplugin.Tests/CSCGlobalCAPluginTests.cs
new file mode 100644
index 0000000..a2e40eb
--- /dev/null
+++ b/cscglobal-caplugin.Tests/CSCGlobalCAPluginTests.cs
@@ -0,0 +1,866 @@
+// 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.
+
+using System.Collections.Concurrent;
+using System.Security.Cryptography;
+using System.Security.Cryptography.X509Certificates;
+using System.Text;
+using Keyfactor.AnyGateway.Extensions;
+using Keyfactor.Extensions.CAPlugin.CSCGlobal;
+using Keyfactor.Extensions.CAPlugin.CSCGlobal.Client.Models;
+using Keyfactor.Extensions.CAPlugin.CSCGlobal.Interfaces;
+using Keyfactor.PKI.Enums.EJBCA;
+using Moq;
+using Xunit;
+
+namespace CscGlobalCAPluginTests;
+
+public class CSCGlobalCAPluginTests
+{
+ private sealed class FakeConfigProvider : IAnyCAPluginConfigProvider
+ {
+ public Dictionary CAConnectionData { get; set; } = new();
+ }
+
+ private static Dictionary ValidConnectionData() => new()
+ {
+ [Constants.CscGlobalUrl] = "https://api.csc.test",
+ [Constants.CscGlobalApiKey] = "test-api-key",
+ [Constants.BearerToken] = "test-bearer-token"
+ };
+
+ private static CSCGlobalCAPlugin MakePlugin(Mock? client = null,
+ Mock? certDataReader = null, Dictionary? connectionData = null)
+ {
+ var plugin = new CSCGlobalCAPlugin();
+ plugin.Initialize(new FakeConfigProvider { CAConnectionData = connectionData ?? ValidConnectionData() },
+ (certDataReader ?? new Mock()).Object);
+ if (client != null)
+ plugin.ClientFactory = _ => client.Object;
+ return plugin;
+ }
+
+ private static EnrollmentProductInfo ProductInfo(string productId = "CSC TrustedSecure OV",
+ Dictionary? parameters = null) => new EnrollmentProductInfo
+ {
+ ProductID = productId,
+ ProductParameters = parameters ?? new Dictionary()
+ };
+
+ private static (X509Certificate2 Cert, string Pem) MakeSelfSignedCert(string cn = "test.example.com", bool isCa = false)
+ {
+ using var rsa = RSA.Create(2048);
+ var req = new CertificateRequest($"CN={cn}", rsa, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1);
+ req.CertificateExtensions.Add(new X509BasicConstraintsExtension(isCa, false, 0, true));
+ var cert = req.CreateSelfSigned(DateTimeOffset.UtcNow.AddDays(-1), DateTimeOffset.UtcNow.AddDays(365));
+ var pem = "-----BEGIN CERTIFICATE-----\n" +
+ Convert.ToBase64String(cert.RawData, Base64FormattingOptions.InsertLineBreaks) +
+ "\n-----END CERTIFICATE-----\n";
+ return (cert, pem);
+ }
+
+ private static string ToApiBase64(string pemText) => Convert.ToBase64String(Encoding.UTF8.GetBytes(pemText));
+
+ // ---------------------------------------------------------------------
+ // Initialize
+ // ---------------------------------------------------------------------
+
+ [Fact]
+ public void Initialize_NullConfigProvider_Throws()
+ {
+ var plugin = new CSCGlobalCAPlugin();
+ Assert.Throws(() => plugin.Initialize(null!, Mock.Of()));
+ }
+
+ [Fact]
+ public void Initialize_NullCertDataReader_Throws()
+ {
+ var plugin = new CSCGlobalCAPlugin();
+ Assert.Throws(() =>
+ plugin.Initialize(new FakeConfigProvider { CAConnectionData = ValidConnectionData() }, null!));
+ }
+
+ [Fact]
+ public void Initialize_ValidSyncFilterDays_ParsesValue()
+ {
+ var data = ValidConnectionData();
+ data[Constants.SyncFilterDays] = "10";
+ var plugin = new CSCGlobalCAPlugin();
+ plugin.Initialize(new FakeConfigProvider { CAConnectionData = data }, Mock.Of());
+ Assert.Equal(10, plugin.SyncFilterDays);
+ }
+
+ [Fact]
+ public void Initialize_UnparsableSyncFilterDays_LeavesDefault()
+ {
+ var data = ValidConnectionData();
+ data[Constants.SyncFilterDays] = "not-a-number";
+ var plugin = new CSCGlobalCAPlugin();
+ plugin.Initialize(new FakeConfigProvider { CAConnectionData = data }, Mock.Of());
+ Assert.Equal(0, plugin.SyncFilterDays);
+ }
+
+ [Fact]
+ public void Initialize_MissingSyncFilterDays_LeavesDefault()
+ {
+ var plugin = new CSCGlobalCAPlugin();
+ plugin.Initialize(new FakeConfigProvider { CAConnectionData = ValidConnectionData() }, Mock.Of());
+ Assert.Equal(0, plugin.SyncFilterDays);
+ }
+
+ // ---------------------------------------------------------------------
+ // GetSingleRecord
+ // ---------------------------------------------------------------------
+
+ [Fact]
+ public async Task GetSingleRecord_ShortCaRequestId_ThrowsWrappedException()
+ {
+ var plugin = MakePlugin();
+ await Assert.ThrowsAsync(() => plugin.GetSingleRecord("too-short"));
+ }
+
+ [Fact]
+ public async Task GetSingleRecord_NullCaRequestId_ThrowsWrappedException()
+ {
+ var plugin = MakePlugin();
+ await Assert.ThrowsAsync(() => plugin.GetSingleRecord(null!));
+ }
+
+ [Fact]
+ public async Task GetSingleRecord_ValidId_ReturnsMappedCertificate()
+ {
+ var uuid = Guid.NewGuid().ToString();
+ var (cert, pem) = MakeSelfSignedCert();
+ var mockClient = new Mock();
+ mockClient.Setup(c => c.SubmitGetCertificateAsync(uuid)).ReturnsAsync(new CertificateResponse
+ {
+ Certificate = Convert.ToBase64String(Encoding.ASCII.GetBytes(pem)),
+ Status = "ACTIVE"
+ });
+
+ var plugin = MakePlugin(mockClient);
+ var result = await plugin.GetSingleRecord(uuid);
+
+ Assert.Equal(uuid, result.CARequestID);
+ Assert.Equal((int)EndEntityStatus.GENERATED, result.Status);
+ Assert.Equal(Convert.ToBase64String(cert.RawData), result.Certificate);
+ }
+
+ [Fact]
+ public async Task GetSingleRecord_ClientThrows_WrapsException()
+ {
+ var uuid = Guid.NewGuid().ToString();
+ var mockClient = new Mock();
+ mockClient.Setup(c => c.SubmitGetCertificateAsync(uuid)).ThrowsAsync(new InvalidOperationException("boom"));
+
+ var plugin = MakePlugin(mockClient);
+ await Assert.ThrowsAsync(() => plugin.GetSingleRecord(uuid));
+ }
+
+ // ---------------------------------------------------------------------
+ // Synchronize
+ // ---------------------------------------------------------------------
+
+ [Fact]
+ public async Task Synchronize_FullSync_QueuesActiveAndRevokedOnly()
+ {
+ var (cert, pem) = MakeSelfSignedCert();
+ var apiCert = ToApiBase64(pem);
+
+ var mockClient = new Mock();
+ mockClient.Setup(c => c.SubmitCertificateListRequestAsync(null)).ReturnsAsync(new CertificateListResponse
+ {
+ Results = new List
+ {
+ new CertificateResponse { Uuid = "cert-1", Status = "ACTIVE", CertificateType = "CSC TrustedSecure OV", Certificate = apiCert },
+ new CertificateResponse { Uuid = "cert-2", Status = "REVOKED", CertificateType = "CSC TrustedSecure DV", Certificate = apiCert },
+ new CertificateResponse { Uuid = "cert-3", Status = "Pending", CertificateType = "CSC TrustedSecure OV", Certificate = apiCert }
+ }
+ });
+
+ var plugin = MakePlugin(mockClient);
+ var buffer = new BlockingCollection();
+
+ await plugin.Synchronize(buffer, null, true, CancellationToken.None);
+
+ var items = buffer.ToList();
+ Assert.Equal(2, items.Count);
+ Assert.Contains(items, i => i.CARequestID == "cert-1" && i.ProductID == "CSC TrustedSecure OV");
+ Assert.Contains(items, i => i.CARequestID == "cert-2" && i.ProductID == "CSC TrustedSecure DV");
+ }
+
+ [Fact]
+ public async Task Synchronize_IncrementalSync_UsesConfiguredFilterDays()
+ {
+ var mockClient = new Mock();
+ string? capturedFilter = null;
+ mockClient.Setup(c => c.SubmitCertificateListRequestAsync(It.IsAny()))
+ .Callback(f => capturedFilter = f)
+ .ReturnsAsync(new CertificateListResponse { Results = new List() });
+
+ var data = ValidConnectionData();
+ data[Constants.SyncFilterDays] = "10";
+ var plugin = MakePlugin(mockClient, connectionData: data);
+ var buffer = new BlockingCollection();
+
+ await plugin.Synchronize(buffer, DateTime.UtcNow, false, CancellationToken.None);
+
+ Assert.NotNull(capturedFilter);
+ Assert.Equal(DateTime.Today.Subtract(TimeSpan.FromDays(10)).ToString("yyyy/MM/dd"), capturedFilter);
+ }
+
+ [Fact]
+ public async Task Synchronize_IncrementalSync_DefaultsToFiveDaysWhenUnset()
+ {
+ var mockClient = new Mock();
+ string? capturedFilter = null;
+ mockClient.Setup(c => c.SubmitCertificateListRequestAsync(It.IsAny()))
+ .Callback(f => capturedFilter = f)
+ .ReturnsAsync(new CertificateListResponse { Results = new List() });
+
+ var plugin = MakePlugin(mockClient);
+ var buffer = new BlockingCollection();
+
+ await plugin.Synchronize(buffer, DateTime.UtcNow, false, CancellationToken.None);
+
+ Assert.Equal(DateTime.Today.Subtract(TimeSpan.FromDays(5)).ToString("yyyy/MM/dd"), capturedFilter);
+ }
+
+ [Fact]
+ public async Task Synchronize_NullResultsCollection_CompletesWithoutQueueing()
+ {
+ var mockClient = new Mock();
+ mockClient.Setup(c => c.SubmitCertificateListRequestAsync(It.IsAny()))
+ .ReturnsAsync(new CertificateListResponse { Results = null });
+
+ var plugin = MakePlugin(mockClient);
+ var buffer = new BlockingCollection();
+
+ await plugin.Synchronize(buffer, null, true, CancellationToken.None);
+
+ Assert.Empty(buffer.ToList());
+ Assert.True(buffer.IsAddingCompleted);
+ }
+
+ [Fact]
+ public async Task Synchronize_EmptyCertificateContent_SkipsRecord()
+ {
+ var mockClient = new Mock();
+ mockClient.Setup(c => c.SubmitCertificateListRequestAsync(It.IsAny()))
+ .ReturnsAsync(new CertificateListResponse
+ {
+ Results = new List
+ {
+ new CertificateResponse { Uuid = "cert-empty", Status = "ACTIVE", Certificate = "" }
+ }
+ });
+
+ var plugin = MakePlugin(mockClient);
+ var buffer = new BlockingCollection();
+
+ await plugin.Synchronize(buffer, null, true, CancellationToken.None);
+
+ Assert.Empty(buffer.ToList());
+ }
+
+ [Fact]
+ public async Task Synchronize_UnparsableCertificateContent_SkipsRecord()
+ {
+ var mockClient = new Mock();
+ mockClient.Setup(c => c.SubmitCertificateListRequestAsync(It.IsAny()))
+ .ReturnsAsync(new CertificateListResponse
+ {
+ Results = new List
+ {
+ new CertificateResponse
+ {
+ Uuid = "cert-bad",
+ Status = "ACTIVE",
+ Certificate = ToApiBase64("not a valid pem block at all")
+ }
+ }
+ });
+
+ var plugin = MakePlugin(mockClient);
+ var buffer = new BlockingCollection();
+
+ await plugin.Synchronize(buffer, null, true, CancellationToken.None);
+
+ Assert.Empty(buffer.ToList());
+ }
+
+ [Fact]
+ public async Task Synchronize_MissingProductIdFromCsc_LeavesProductIdNull()
+ {
+ var (_, pem) = MakeSelfSignedCert();
+ var mockClient = new Mock();
+ mockClient.Setup(c => c.SubmitCertificateListRequestAsync(It.IsAny()))
+ .ReturnsAsync(new CertificateListResponse
+ {
+ Results = new List
+ {
+ new CertificateResponse { Uuid = "cert-1", Status = "ACTIVE", CertificateType = null, Certificate = ToApiBase64(pem) }
+ }
+ });
+
+ var plugin = MakePlugin(mockClient);
+ var buffer = new BlockingCollection();
+
+ await plugin.Synchronize(buffer, null, true, CancellationToken.None);
+
+ var item = Assert.Single(buffer.ToList());
+ Assert.Null(item.ProductID);
+ }
+
+ [Fact]
+ public async Task Synchronize_CancellationRequested_ThrowsAndCompletesBuffer()
+ {
+ var (_, pem) = MakeSelfSignedCert();
+ var mockClient = new Mock();
+ mockClient.Setup(c => c.SubmitCertificateListRequestAsync(It.IsAny()))
+ .ReturnsAsync(new CertificateListResponse
+ {
+ Results = new List
+ {
+ new CertificateResponse { Uuid = "cert-1", Status = "ACTIVE", Certificate = ToApiBase64(pem) }
+ }
+ });
+
+ var plugin = MakePlugin(mockClient);
+ var buffer = new BlockingCollection();
+ using var cts = new CancellationTokenSource();
+ cts.Cancel();
+
+ await Assert.ThrowsAsync(() =>
+ plugin.Synchronize(buffer, null, true, cts.Token));
+
+ Assert.True(buffer.IsAddingCompleted);
+ }
+
+ [Fact]
+ public async Task Synchronize_ClientThrows_CompletesBufferAndRethrows()
+ {
+ var mockClient = new Mock();
+ mockClient.Setup(c => c.SubmitCertificateListRequestAsync(It.IsAny()))
+ .ThrowsAsync(new InvalidOperationException("network down"));
+
+ var plugin = MakePlugin(mockClient);
+ var buffer = new BlockingCollection();
+
+ await Assert.ThrowsAsync(() =>
+ plugin.Synchronize(buffer, null, true, CancellationToken.None));
+
+ Assert.True(buffer.IsAddingCompleted);
+ }
+
+ // ---------------------------------------------------------------------
+ // Revoke
+ // ---------------------------------------------------------------------
+
+ [Fact]
+ public async Task Revoke_ShortCaRequestId_ThrowsWrappedException()
+ {
+ var plugin = MakePlugin();
+ await Assert.ThrowsAsync(() => plugin.Revoke("short", "AB12", 0));
+ }
+
+ [Fact]
+ public async Task Revoke_Success_ReturnsRevokedStatus()
+ {
+ var uuid = Guid.NewGuid().ToString();
+ var mockClient = new Mock();
+ mockClient.Setup(c => c.SubmitRevokeCertificateAsync(uuid)).ReturnsAsync(new RevokeResponse());
+
+ var plugin = MakePlugin(mockClient);
+ var status = await plugin.Revoke(uuid, "AB12", 0);
+
+ Assert.Equal((int)EndEntityStatus.REVOKED, status);
+ }
+
+ [Fact]
+ public async Task Revoke_ErrorWithDescription_ThrowsWrappedException()
+ {
+ var uuid = Guid.NewGuid().ToString();
+ var mockClient = new Mock();
+ mockClient.Setup(c => c.SubmitRevokeCertificateAsync(uuid)).ReturnsAsync(new RevokeResponse
+ {
+ RegistrationError = new RegistrationError { Description = "already revoked" }
+ });
+
+ var plugin = MakePlugin(mockClient);
+ await Assert.ThrowsAsync(() => plugin.Revoke(uuid, "AB12", 0));
+ }
+
+ [Fact]
+ public async Task Revoke_FailedWithNoErrorDescription_ReturnsFailedStatus()
+ {
+ var uuid = Guid.NewGuid().ToString();
+ var mockClient = new Mock();
+ mockClient.Setup(c => c.SubmitRevokeCertificateAsync(uuid)).ReturnsAsync((RevokeResponse)null!);
+
+ var plugin = MakePlugin(mockClient);
+ var status = await plugin.Revoke(uuid, "AB12", 0);
+
+ Assert.Equal((int)EndEntityStatus.FAILED, status);
+ }
+
+ // ---------------------------------------------------------------------
+ // Enroll
+ // ---------------------------------------------------------------------
+
+ [Fact]
+ public async Task Enroll_NullProductInfo_ThrowsArgumentNullException()
+ {
+ var plugin = MakePlugin();
+ await Assert.ThrowsAsync(() =>
+ plugin.Enroll("csr", "subject", new Dictionary(), null!, RequestFormat.PKCS10, EnrollmentType.New));
+ }
+
+ [Fact]
+ public async Task Enroll_New_Success_ReturnsExternalValidation()
+ {
+ var mockClient = new Mock();
+ mockClient.Setup(c => c.SubmitGetCustomFields()).ReturnsAsync(new List());
+ mockClient.Setup(c => c.SubmitRegistrationAsync(It.IsAny())).ReturnsAsync(new RegistrationResponse
+ {
+ Result = new Result { CommonName = "order-1", Status = new Status { Uuid = "uuid-1" } }
+ });
+
+ var plugin = MakePlugin(mockClient);
+ var result = await plugin.Enroll("csr", "CN=test", new Dictionary(), ProductInfo(), RequestFormat.PKCS10, EnrollmentType.New);
+
+ Assert.Equal((int)EndEntityStatus.EXTERNALVALIDATION, result!.Status);
+ Assert.Equal("uuid-1", result.CARequestID);
+ }
+
+ [Fact]
+ public async Task Enroll_New_CscReturnsError_ReturnsFailedStatus()
+ {
+ var mockClient = new Mock();
+ mockClient.Setup(c => c.SubmitGetCustomFields()).ReturnsAsync(new List());
+ mockClient.Setup(c => c.SubmitRegistrationAsync(It.IsAny())).ReturnsAsync(new RegistrationResponse
+ {
+ RegistrationError = new RegistrationError { Description = "duplicate order" }
+ });
+
+ var plugin = MakePlugin(mockClient);
+ var result = await plugin.Enroll("csr", "CN=test", new Dictionary(), ProductInfo(),
+ RequestFormat.PKCS10, EnrollmentType.New);
+
+ Assert.Equal((int)EndEntityStatus.FAILED, result!.Status);
+ Assert.Equal("duplicate order", result.StatusMessage);
+ }
+
+ [Fact]
+ public async Task Enroll_NewWithPriorCertSn_ReturnsFailureWithoutCallingClient()
+ {
+ var mockClient = new Mock();
+ mockClient.Setup(c => c.SubmitGetCustomFields()).ReturnsAsync(new List());
+
+ var plugin = MakePlugin(mockClient);
+ var productInfo = ProductInfo(parameters: new Dictionary { ["PriorCertSN"] = "ABC123" });
+
+ var result = await plugin.Enroll("csr", "CN=test", new Dictionary(), productInfo, RequestFormat.PKCS10, EnrollmentType.New);
+
+ Assert.Equal((int)EndEntityStatus.INPROCESS, result!.Status);
+ mockClient.Verify(c => c.SubmitRegistrationAsync(It.IsAny()), Times.Never);
+ }
+
+ [Fact]
+ public async Task Enroll_RenewOrReissue_MissingPriorCertSn_ReturnsFailure()
+ {
+ var mockClient = new Mock();
+ mockClient.Setup(c => c.SubmitGetCustomFields()).ReturnsAsync(new List());
+
+ var plugin = MakePlugin(mockClient);
+ var result = await plugin.Enroll("csr", "CN=test", new Dictionary(), ProductInfo(),
+ RequestFormat.PKCS10, EnrollmentType.RenewOrReissue);
+
+ Assert.Equal((int)EndEntityStatus.INPROCESS, result!.Status);
+ Assert.Contains("no prior certificate serial number", result.StatusMessage);
+ }
+
+ [Fact]
+ public async Task Enroll_RenewOrReissue_NoRequestIdFoundForSerial_ReturnsFailure()
+ {
+ var mockClient = new Mock();
+ mockClient.Setup(c => c.SubmitGetCustomFields()).ReturnsAsync(new List());
+ var certDataReader = new Mock();
+ certDataReader.Setup(r => r.GetRequestIDBySerialNumber("ABC123")).ReturnsAsync(string.Empty);
+
+ var plugin = MakePlugin(mockClient, certDataReader);
+ var productInfo = ProductInfo(parameters: new Dictionary { ["PriorCertSN"] = "ABC123" });
+
+ var result = await plugin.Enroll("csr", "CN=test", new Dictionary(), productInfo,
+ RequestFormat.PKCS10, EnrollmentType.RenewOrReissue);
+
+ Assert.Equal((int)EndEntityStatus.INPROCESS, result!.Status);
+ Assert.Contains("no prior request found", result.StatusMessage);
+ }
+
+ [Fact]
+ public async Task Enroll_RenewOrReissue_NullExpirationDate_FallsBackToGetSingleRecordThenReissues()
+ {
+ var orderUuid = Guid.NewGuid().ToString();
+ var (_, pem) = MakeSelfSignedCert();
+ var mockClient = new Mock();
+ mockClient.Setup(c => c.SubmitGetCustomFields()).ReturnsAsync(new List());
+ mockClient.Setup(c => c.SubmitGetCertificateAsync(orderUuid)).ReturnsAsync(new CertificateResponse
+ {
+ Certificate = Convert.ToBase64String(Encoding.ASCII.GetBytes(pem)),
+ Status = "ACTIVE"
+ });
+ mockClient.Setup(c => c.SubmitReissueAsync(It.IsAny())).ReturnsAsync(new ReissueResponse
+ {
+ Result = new Result { CommonName = "reissued.example.com" }
+ });
+
+ var certDataReader = new Mock();
+ certDataReader.Setup(r => r.GetRequestIDBySerialNumber("ABC123")).ReturnsAsync(orderUuid);
+ certDataReader.Setup(r => r.GetExpirationDateByRequestId(orderUuid)).Returns((DateTime?)null);
+
+ var plugin = MakePlugin(mockClient, certDataReader);
+ var productInfo = ProductInfo(parameters: new Dictionary
+ {
+ ["PriorCertSN"] = "ABC123",
+ ["Applicant Last Name"] = "Doe"
+ });
+
+ var result = await plugin.Enroll("csr", "CN=test", new Dictionary(), productInfo,
+ RequestFormat.PKCS10, EnrollmentType.RenewOrReissue);
+
+ Assert.Equal((int)EndEntityStatus.GENERATED, result!.Status);
+ mockClient.Verify(c => c.SubmitReissueAsync(It.IsAny()), Times.Once);
+ }
+
+ [Fact]
+ public async Task Enroll_Renewal_ExpiredCertWithApplicantLastName_Succeeds()
+ {
+ var uuid = Guid.NewGuid().ToString();
+ var mockClient = new Mock();
+ mockClient.Setup(c => c.SubmitGetCustomFields()).ReturnsAsync(new List());
+ mockClient.Setup(c => c.SubmitRenewalAsync(It.IsAny())).ReturnsAsync(new RenewalResponse
+ {
+ Result = new Result { CommonName = "renewed.example.com" }
+ });
+
+ var certDataReader = new Mock();
+ certDataReader.Setup(r => r.GetRequestIDBySerialNumber("ABC123")).ReturnsAsync(uuid);
+ certDataReader.Setup(r => r.GetExpirationDateByRequestId(uuid)).Returns(DateTime.Now.AddDays(-1));
+
+ var plugin = MakePlugin(mockClient, certDataReader);
+ var productInfo = ProductInfo(parameters: new Dictionary
+ {
+ ["PriorCertSN"] = "ABC123",
+ ["Applicant Last Name"] = "Doe"
+ });
+
+ var result = await plugin.Enroll("csr", "CN=test", new Dictionary(), productInfo,
+ RequestFormat.PKCS10, EnrollmentType.RenewOrReissue);
+
+ Assert.Equal((int)EndEntityStatus.GENERATED, result!.Status);
+ }
+
+ [Fact]
+ public async Task Enroll_Renewal_ExpiredCertMissingApplicantLastName_ReturnsFailure()
+ {
+ var uuid = Guid.NewGuid().ToString();
+ var mockClient = new Mock();
+ mockClient.Setup(c => c.SubmitGetCustomFields()).ReturnsAsync(new List());
+
+ var certDataReader = new Mock();
+ certDataReader.Setup(r => r.GetRequestIDBySerialNumber("ABC123")).ReturnsAsync(uuid);
+ certDataReader.Setup(r => r.GetExpirationDateByRequestId(uuid)).Returns(DateTime.Now.AddDays(-1));
+
+ var plugin = MakePlugin(mockClient, certDataReader);
+ var productInfo = ProductInfo(parameters: new Dictionary { ["PriorCertSN"] = "ABC123" });
+
+ var result = await plugin.Enroll("csr", "CN=test", new Dictionary(), productInfo,
+ RequestFormat.PKCS10, EnrollmentType.RenewOrReissue);
+
+ Assert.Equal((int)EndEntityStatus.INPROCESS, result!.Status);
+ Assert.Contains("One click Renew Is Not Available", result.StatusMessage);
+ }
+
+ [Fact]
+ public async Task Enroll_Reissue_ValidCertWithApplicantLastName_Succeeds()
+ {
+ var uuid = Guid.NewGuid().ToString();
+ var mockClient = new Mock();
+ mockClient.Setup(c => c.SubmitGetCustomFields()).ReturnsAsync(new List());
+ mockClient.Setup(c => c.SubmitReissueAsync(It.IsAny())).ReturnsAsync(new ReissueResponse
+ {
+ Result = new Result { CommonName = "reissued.example.com", Status = new Status { Uuid = uuid } }
+ });
+
+ var certDataReader = new Mock();
+ certDataReader.Setup(r => r.GetRequestIDBySerialNumber("ABC123")).ReturnsAsync(uuid);
+ certDataReader.Setup(r => r.GetExpirationDateByRequestId(uuid)).Returns(DateTime.Now.AddDays(30));
+
+ var plugin = MakePlugin(mockClient, certDataReader);
+ var productInfo = ProductInfo(parameters: new Dictionary
+ {
+ ["PriorCertSN"] = "ABC123",
+ ["Applicant Last Name"] = "Doe"
+ });
+
+ var result = await plugin.Enroll("csr", "CN=test", new Dictionary(), productInfo,
+ RequestFormat.PKCS10, EnrollmentType.RenewOrReissue);
+
+ Assert.Equal((int)EndEntityStatus.GENERATED, result!.Status);
+ }
+
+ [Fact]
+ public async Task Enroll_Reissue_MissingApplicantLastName_ReturnsFailure()
+ {
+ var uuid = Guid.NewGuid().ToString();
+ var mockClient = new Mock();
+ mockClient.Setup(c => c.SubmitGetCustomFields()).ReturnsAsync(new List());
+
+ var certDataReader = new Mock();
+ certDataReader.Setup(r => r.GetRequestIDBySerialNumber("ABC123")).ReturnsAsync(uuid);
+ certDataReader.Setup(r => r.GetExpirationDateByRequestId(uuid)).Returns(DateTime.Now.AddDays(30));
+
+ var plugin = MakePlugin(mockClient, certDataReader);
+ var productInfo = ProductInfo(parameters: new Dictionary { ["PriorCertSN"] = "ABC123" });
+
+ var result = await plugin.Enroll("csr", "CN=test", new Dictionary(), productInfo,
+ RequestFormat.PKCS10, EnrollmentType.RenewOrReissue);
+
+ Assert.Equal((int)EndEntityStatus.INPROCESS, result!.Status);
+ Assert.Contains("One click Renew Is Not Available", result.StatusMessage);
+ }
+
+ [Fact]
+ public async Task Enroll_Reissue_RequestIdTooShort_ReturnsFailure()
+ {
+ var mockClient = new Mock();
+ mockClient.Setup(c => c.SubmitGetCustomFields()).ReturnsAsync(new List());
+
+ var certDataReader = new Mock();
+ certDataReader.Setup(r => r.GetRequestIDBySerialNumber("ABC123")).ReturnsAsync("short-id");
+ certDataReader.Setup(r => r.GetExpirationDateByRequestId("short-id")).Returns(DateTime.Now.AddDays(30));
+
+ var plugin = MakePlugin(mockClient, certDataReader);
+ var productInfo = ProductInfo(parameters: new Dictionary
+ {
+ ["PriorCertSN"] = "ABC123",
+ ["Applicant Last Name"] = "Doe"
+ });
+
+ var result = await plugin.Enroll("csr", "CN=test", new Dictionary(), productInfo,
+ RequestFormat.PKCS10, EnrollmentType.RenewOrReissue);
+
+ Assert.Equal((int)EndEntityStatus.INPROCESS, result!.Status);
+ Assert.Contains("no prior request found", result.StatusMessage);
+ }
+
+ [Fact]
+ public async Task Enroll_UnhandledEnrollmentType_ReturnsNull()
+ {
+ var mockClient = new Mock();
+ mockClient.Setup(c => c.SubmitGetCustomFields()).ReturnsAsync(new List());
+
+ var plugin = MakePlugin(mockClient);
+ var result = await plugin.Enroll("csr", "CN=test", new Dictionary(), ProductInfo(),
+ RequestFormat.PKCS10, EnrollmentType.Renew);
+
+ Assert.Null(result);
+ }
+
+ [Fact]
+ public async Task Enroll_ClientThrows_Rethrows()
+ {
+ var mockClient = new Mock();
+ mockClient.Setup(c => c.SubmitGetCustomFields()).ThrowsAsync(new InvalidOperationException("boom"));
+
+ var plugin = MakePlugin(mockClient);
+ await Assert.ThrowsAsync(() =>
+ plugin.Enroll("csr", "CN=test", new Dictionary(), ProductInfo(), RequestFormat.PKCS10, EnrollmentType.New));
+ }
+
+ // ---------------------------------------------------------------------
+ // Ping / ValidateCAConnectionInfo / ValidateProductInfo
+ // ---------------------------------------------------------------------
+
+ [Fact]
+ public async Task Ping_DoesNotThrow()
+ {
+ var plugin = MakePlugin();
+ await plugin.Ping();
+ }
+
+ [Fact]
+ public async Task ValidateCAConnectionInfo_NullConnectionInfo_DoesNotThrow()
+ {
+ var plugin = MakePlugin();
+ await plugin.ValidateCAConnectionInfo(null!);
+ }
+
+ [Fact]
+ public async Task ValidateCAConnectionInfo_WithConnectionInfo_DoesNotThrow()
+ {
+ var plugin = MakePlugin();
+ await plugin.ValidateCAConnectionInfo(new Dictionary { ["Key"] = "Value" });
+ }
+
+ [Fact]
+ public async Task ValidateProductInfo_KnownProduct_DoesNotThrow()
+ {
+ var plugin = MakePlugin();
+ await plugin.ValidateProductInfo(ProductInfo("CSC TrustedSecure OV"), new Dictionary());
+ }
+
+ [Fact]
+ public async Task ValidateProductInfo_KnownProductDifferentCase_DoesNotThrow()
+ {
+ var plugin = MakePlugin();
+ await plugin.ValidateProductInfo(ProductInfo("csc trustedsecure ov"), new Dictionary());
+ }
+
+ [Fact]
+ public async Task ValidateProductInfo_UnknownProduct_Throws()
+ {
+ var plugin = MakePlugin();
+ await Assert.ThrowsAsync(() =>
+ plugin.ValidateProductInfo(ProductInfo("Not A Real Product"), new Dictionary()));
+ }
+
+ // ---------------------------------------------------------------------
+ // GetCAConnectorAnnotations / GetTemplateParameterAnnotations / GetProductIds
+ // ---------------------------------------------------------------------
+
+ [Fact]
+ public void GetCAConnectorAnnotations_ContainsExpectedKeys()
+ {
+ var plugin = MakePlugin();
+ var annotations = plugin.GetCAConnectorAnnotations();
+
+ Assert.Equal(5, annotations.Count);
+ Assert.Contains(Constants.CscGlobalUrl, annotations.Keys);
+ Assert.Contains(Constants.CscGlobalApiKey, annotations.Keys);
+ Assert.Contains(Constants.BearerToken, annotations.Keys);
+ Assert.Contains(Constants.DefaultPageSize, annotations.Keys);
+ Assert.Contains(Constants.SyncFilterDays, annotations.Keys);
+ Assert.True(annotations[Constants.CscGlobalApiKey].Hidden);
+ }
+
+ [Fact]
+ public void GetTemplateParameterAnnotations_ContainsExpectedKeys()
+ {
+ var plugin = MakePlugin();
+ var annotations = plugin.GetTemplateParameterAnnotations();
+
+ Assert.Equal(12, annotations.Count);
+ Assert.Contains(EnrollmentConfigConstants.Term, annotations.Keys);
+ Assert.Contains(EnrollmentConfigConstants.AdditionalSansCommaSeparatedDcvEmails, annotations.Keys);
+ }
+
+ [Fact]
+ public void GetProductIds_ReturnsFullList()
+ {
+ var plugin = MakePlugin();
+ var ids = plugin.GetProductIds();
+
+ Assert.Equal(10, ids.Count);
+ Assert.Contains("CSC TrustedSecure OV", ids);
+ Assert.Contains("CSC TrustedSecure DV Wildcard, Multiple Names", ids);
+ }
+
+ // ---------------------------------------------------------------------
+ // GetEndEntityCertificate
+ // ---------------------------------------------------------------------
+
+ [Fact]
+ public void GetEndEntityCertificate_EmptyInput_ReturnsEmptyString()
+ {
+ var plugin = MakePlugin();
+ Assert.Equal(string.Empty, plugin.GetEndEntityCertificate(""));
+ Assert.Equal(string.Empty, plugin.GetEndEntityCertificate(" "));
+ Assert.Equal(string.Empty, plugin.GetEndEntityCertificate(null!));
+ }
+
+ [Fact]
+ public void GetEndEntityCertificate_NoValidPemBlocks_ReturnsEmptyString()
+ {
+ var plugin = MakePlugin();
+ Assert.Equal(string.Empty, plugin.GetEndEntityCertificate("this is not a certificate"));
+ }
+
+ [Fact]
+ public void GetEndEntityCertificate_SingleLeafCert_ReturnsBase64Der()
+ {
+ var (cert, pem) = MakeSelfSignedCert();
+ var plugin = MakePlugin();
+
+ var result = plugin.GetEndEntityCertificate(pem);
+
+ Assert.Equal(Convert.ToBase64String(cert.RawData), result);
+ }
+
+ [Fact]
+ public void GetEndEntityCertificate_MalformedBase64Block_SkipsBlockReturnsEmpty()
+ {
+ var pem = "-----BEGIN CERTIFICATE-----\nNOT-VALID-BASE64!!!\n-----END CERTIFICATE-----\n";
+ var plugin = MakePlugin();
+
+ Assert.Equal(string.Empty, plugin.GetEndEntityCertificate(pem));
+ }
+
+ [Fact]
+ public void GetEndEntityCertificate_EmptyBlockContent_Skipped()
+ {
+ var pem = "-----BEGIN CERTIFICATE-----\n\n-----END CERTIFICATE-----\n";
+ var plugin = MakePlugin();
+
+ Assert.Equal(string.Empty, plugin.GetEndEntityCertificate(pem));
+ }
+
+ [Fact]
+ public void GetEndEntityCertificate_ValidBase64ButNotACertificate_SkipsBlockReturnsEmpty()
+ {
+ var notACert = Convert.ToBase64String(Encoding.UTF8.GetBytes("this is not a certificate, just text"));
+ var pem = $"-----BEGIN CERTIFICATE-----\n{notACert}\n-----END CERTIFICATE-----\n";
+ var plugin = MakePlugin();
+
+ Assert.Equal(string.Empty, plugin.GetEndEntityCertificate(pem));
+ }
+
+ [Fact]
+ public void GetEndEntityCertificate_TwoIndependentLeafCerts_ReturnsOneOfThem()
+ {
+ var (certA, pemA) = MakeSelfSignedCert("a.example.com");
+ var (certB, pemB) = MakeSelfSignedCert("b.example.com");
+ var plugin = MakePlugin();
+
+ var result = plugin.GetEndEntityCertificate(pemA + pemB);
+
+ Assert.True(result == Convert.ToBase64String(certA.RawData) || result == Convert.ToBase64String(certB.RawData));
+ }
+
+ [Fact]
+ public void GetEndEntityCertificate_LeafAndCaChain_ReturnsLeafOnly()
+ {
+ using var rsaCa = RSA.Create(2048);
+ var caReq = new CertificateRequest("CN=Test CA", rsaCa, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1);
+ caReq.CertificateExtensions.Add(new X509BasicConstraintsExtension(true, false, 0, true));
+ var caCert = caReq.CreateSelfSigned(DateTimeOffset.UtcNow.AddDays(-1), DateTimeOffset.UtcNow.AddDays(365));
+
+ using var rsaLeaf = RSA.Create(2048);
+ var leafReq = new CertificateRequest("CN=leaf.example.com", rsaLeaf, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1);
+ leafReq.CertificateExtensions.Add(new X509BasicConstraintsExtension(false, false, 0, true));
+ var leafCert = leafReq.Create(caCert, DateTimeOffset.UtcNow.AddDays(-1), DateTimeOffset.UtcNow.AddDays(365),
+ Guid.NewGuid().ToByteArray());
+
+ string ToPemBlock(X509Certificate2 c) => "-----BEGIN CERTIFICATE-----\n" +
+ Convert.ToBase64String(c.RawData, Base64FormattingOptions.InsertLineBreaks) +
+ "\n-----END CERTIFICATE-----\n";
+
+ var chainPem = ToPemBlock(caCert) + ToPemBlock(leafCert);
+ var plugin = MakePlugin();
+
+ var result = plugin.GetEndEntityCertificate(chainPem);
+
+ Assert.Equal(Convert.ToBase64String(leafCert.RawData), result);
+ }
+}
diff --git a/cscglobal-caplugin.Tests/CscGlobalClientTests.cs b/cscglobal-caplugin.Tests/CscGlobalClientTests.cs
new file mode 100644
index 0000000..2879dbe
--- /dev/null
+++ b/cscglobal-caplugin.Tests/CscGlobalClientTests.cs
@@ -0,0 +1,385 @@
+// 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.
+
+using System.Net;
+using System.Net.Http;
+using System.Text;
+using Keyfactor.AnyGateway.Extensions;
+using Keyfactor.Extensions.CAPlugin.CSCGlobal;
+using Keyfactor.Extensions.CAPlugin.CSCGlobal.Client;
+using Keyfactor.Extensions.CAPlugin.CSCGlobal.Client.Models;
+using Xunit;
+
+namespace CscGlobalCAPluginTests;
+
+public class CscGlobalClientTests
+{
+ private sealed class FakeConfigProvider : IAnyCAPluginConfigProvider
+ {
+ public Dictionary CAConnectionData { get; set; } = new();
+ }
+
+ private sealed class FakeHttpMessageHandler : HttpMessageHandler
+ {
+ private readonly Func _responder;
+
+ public FakeHttpMessageHandler(Func responder)
+ {
+ _responder = responder;
+ }
+
+ public HttpRequestMessage? LastRequest { get; private set; }
+ public string? LastRequestBody { get; private set; }
+
+ protected override Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
+ {
+ LastRequest = request;
+ LastRequestBody = request.Content?.ReadAsStringAsync(cancellationToken).GetAwaiter().GetResult();
+ return Task.FromResult(_responder(request));
+ }
+ }
+
+ private static HttpResponseMessage JsonResponse(HttpStatusCode status, string json) =>
+ new HttpResponseMessage(status) { Content = new StringContent(json, Encoding.UTF8, "application/json") };
+
+ private static IAnyCAPluginConfigProvider ValidConfig() => new FakeConfigProvider
+ {
+ CAConnectionData = new Dictionary
+ {
+ [Constants.CscGlobalUrl] = "https://api.csc.test",
+ [Constants.CscGlobalApiKey] = "test-api-key",
+ [Constants.BearerToken] = "test-bearer-token"
+ }
+ };
+
+ private static CscGlobalClient MakeClient(Func responder,
+ out FakeHttpMessageHandler handler, IAnyCAPluginConfigProvider? config = null)
+ {
+ handler = new FakeHttpMessageHandler(responder);
+ return new CscGlobalClient(config ?? ValidConfig(), handler);
+ }
+
+ // ---------------------------------------------------------------------
+ // Constructor validation
+ // ---------------------------------------------------------------------
+
+ [Fact]
+ public void Constructor_NullConfig_Throws()
+ {
+ Assert.Throws(() => new CscGlobalClient(null!));
+ }
+
+ [Fact]
+ public async Task Constructor_NullConnectionData_DoesNotThrowButClientIsInert()
+ {
+ var config = new FakeConfigProvider { CAConnectionData = null! };
+ var client = new CscGlobalClient(config);
+
+ await Assert.ThrowsAsync(() => client.SubmitGetCustomFields());
+ }
+
+ [Fact]
+ public async Task Constructor_MissingApiKeyEntry_DoesNotThrowButClientIsInert()
+ {
+ var config = new FakeConfigProvider { CAConnectionData = new Dictionary() };
+ var client = new CscGlobalClient(config);
+
+ await Assert.ThrowsAsync(() => client.SubmitGetCustomFields());
+ }
+
+ // ---------------------------------------------------------------------
+ // SubmitRegistrationAsync
+ // ---------------------------------------------------------------------
+
+ [Fact]
+ public async Task SubmitRegistrationAsync_Success_ReturnsParsedResponse()
+ {
+ var client = MakeClient(_ => JsonResponse(HttpStatusCode.OK,
+ "{\"result\":{\"commonName\":\"order-1\",\"price\":{\"currency\":\"USD\",\"total\":99.5}," +
+ "\"dcvDetails\":[{\"domainName\":\"example.com\",\"actionNeeded\":\"N\"}]}}"), out var handler);
+
+ var response = await client.SubmitRegistrationAsync(new RegistrationRequest());
+
+ Assert.NotNull(response.Result);
+ Assert.Equal("order-1", response.Result.CommonName);
+ Assert.Equal("USD", response.Result.Price.Currency);
+ Assert.Equal(99.5m, response.Result.Price.Total);
+ Assert.Equal("example.com", response.Result.DcvDetails[0].DomainName);
+ Assert.Equal("N", response.Result.DcvDetails[0].ActionNeeded);
+ Assert.Contains("/dbs/api/v2/tls/registration", handler.LastRequest!.RequestUri!.ToString());
+ }
+
+ [Fact]
+ public async Task SubmitRegistrationAsync_BadRequest_ReturnsRegistrationError()
+ {
+ var client = MakeClient(_ => JsonResponse(HttpStatusCode.BadRequest,
+ "{\"description\":\"invalid csr\"}"), out _);
+
+ var response = await client.SubmitRegistrationAsync(new RegistrationRequest());
+
+ Assert.NotNull(response.RegistrationError);
+ Assert.Equal("invalid csr", response.RegistrationError.Description);
+ Assert.Null(response.Result);
+ }
+
+ [Fact]
+ public async Task SubmitRegistrationAsync_OtherError_Throws()
+ {
+ var client = MakeClient(_ => JsonResponse(HttpStatusCode.InternalServerError, "boom"), out _);
+
+ await Assert.ThrowsAsync(() => client.SubmitRegistrationAsync(new RegistrationRequest()));
+ }
+
+ [Fact]
+ public async Task SubmitRegistrationAsync_UnparsableSuccessBody_Throws()
+ {
+ var client = MakeClient(_ => JsonResponse(HttpStatusCode.OK, "null"), out _);
+
+ await Assert.ThrowsAsync(() => client.SubmitRegistrationAsync(new RegistrationRequest()));
+ }
+
+ // ---------------------------------------------------------------------
+ // SubmitRenewalAsync
+ // ---------------------------------------------------------------------
+
+ [Fact]
+ public async Task SubmitRenewalAsync_Success_ReturnsParsedResponse()
+ {
+ var client = MakeClient(_ => JsonResponse(HttpStatusCode.OK,
+ "{\"result\":{\"commonName\":\"renew-1\"}}"), out var handler);
+
+ var response = await client.SubmitRenewalAsync(new RenewalRequest());
+
+ Assert.Equal("renew-1", response.Result.CommonName);
+ Assert.Contains("/dbs/api/v2/tls/renewal", handler.LastRequest!.RequestUri!.ToString());
+ }
+
+ [Fact]
+ public async Task SubmitRenewalAsync_BadRequest_ReturnsRegistrationError()
+ {
+ var client = MakeClient(_ => JsonResponse(HttpStatusCode.BadRequest, "{\"description\":\"nope\"}"), out _);
+
+ var response = await client.SubmitRenewalAsync(new RenewalRequest());
+
+ Assert.Equal("nope", response.RegistrationError.Description);
+ }
+
+ [Fact]
+ public async Task SubmitRenewalAsync_OtherError_Throws()
+ {
+ var client = MakeClient(_ => JsonResponse(HttpStatusCode.InternalServerError, "boom"), out _);
+
+ await Assert.ThrowsAsync(() => client.SubmitRenewalAsync(new RenewalRequest()));
+ }
+
+ [Fact]
+ public async Task SubmitRenewalAsync_UnparsableSuccessBody_Throws()
+ {
+ var client = MakeClient(_ => JsonResponse(HttpStatusCode.OK, "null"), out _);
+
+ await Assert.ThrowsAsync(() => client.SubmitRenewalAsync(new RenewalRequest()));
+ }
+
+ // ---------------------------------------------------------------------
+ // SubmitReissueAsync
+ // ---------------------------------------------------------------------
+
+ [Fact]
+ public async Task SubmitReissueAsync_Success_ReturnsParsedResponse()
+ {
+ var client = MakeClient(_ => JsonResponse(HttpStatusCode.OK,
+ "{\"result\":{\"commonName\":\"reissue-1\"}}"), out var handler);
+
+ var response = await client.SubmitReissueAsync(new ReissueRequest());
+
+ Assert.Equal("reissue-1", response.Result.CommonName);
+ Assert.Contains("/dbs/api/v2/tls/reissue", handler.LastRequest!.RequestUri!.ToString());
+ }
+
+ [Fact]
+ public async Task SubmitReissueAsync_BadRequest_ReturnsRegistrationError()
+ {
+ var client = MakeClient(_ => JsonResponse(HttpStatusCode.BadRequest, "{\"description\":\"denied\"}"), out _);
+
+ var response = await client.SubmitReissueAsync(new ReissueRequest());
+
+ Assert.Equal("denied", response.RegistrationError.Description);
+ }
+
+ [Fact]
+ public async Task SubmitReissueAsync_OtherError_Throws()
+ {
+ var client = MakeClient(_ => JsonResponse(HttpStatusCode.InternalServerError, "boom"), out _);
+
+ await Assert.ThrowsAsync(() => client.SubmitReissueAsync(new ReissueRequest()));
+ }
+
+ [Fact]
+ public async Task SubmitReissueAsync_UnparsableSuccessBody_Throws()
+ {
+ var client = MakeClient(_ => JsonResponse(HttpStatusCode.OK, "null"), out _);
+
+ await Assert.ThrowsAsync(() => client.SubmitReissueAsync(new ReissueRequest()));
+ }
+
+ // ---------------------------------------------------------------------
+ // SubmitGetCertificateAsync
+ // ---------------------------------------------------------------------
+
+ [Fact]
+ public async Task SubmitGetCertificateAsync_Success_ReturnsParsedResponse()
+ {
+ var client = MakeClient(_ => JsonResponse(HttpStatusCode.OK, "{\"certificate\":\"abc123\"}"), out var handler);
+
+ var response = await client.SubmitGetCertificateAsync("cert-uuid");
+
+ Assert.Equal("abc123", response.Certificate);
+ Assert.Contains("/dbs/api/v2/tls/certificate/cert-uuid", handler.LastRequest!.RequestUri!.ToString());
+ }
+
+ [Fact]
+ public async Task SubmitGetCertificateAsync_Failure_Throws()
+ {
+ var client = MakeClient(_ => JsonResponse(HttpStatusCode.NotFound, "not found"), out _);
+
+ await Assert.ThrowsAsync(() => client.SubmitGetCertificateAsync("missing-uuid"));
+ }
+
+ [Fact]
+ public async Task SubmitGetCertificateAsync_UnparsableSuccessBody_Throws()
+ {
+ var client = MakeClient(_ => JsonResponse(HttpStatusCode.OK, "null"), out _);
+
+ await Assert.ThrowsAsync(() => client.SubmitGetCertificateAsync("cert-uuid"));
+ }
+
+ // ---------------------------------------------------------------------
+ // SubmitGetCustomFields
+ // ---------------------------------------------------------------------
+
+ [Fact]
+ public async Task SubmitGetCustomFields_Success_ReturnsList()
+ {
+ var client = MakeClient(_ => JsonResponse(HttpStatusCode.OK,
+ "{\"customFields\":[{\"label\":\"Field1\",\"mandatory\":true}]}"), out var handler);
+
+ var fields = await client.SubmitGetCustomFields();
+
+ Assert.Single(fields);
+ Assert.Equal("Field1", fields[0].Label);
+ Assert.Contains("/dbs/api/v2/admin/customfields", handler.LastRequest!.RequestUri!.ToString());
+ }
+
+ [Fact]
+ public async Task SubmitGetCustomFields_NullCustomFieldsArray_ReturnsEmptyList()
+ {
+ var client = MakeClient(_ => JsonResponse(HttpStatusCode.OK, "{}"), out _);
+
+ var fields = await client.SubmitGetCustomFields();
+
+ Assert.Empty(fields);
+ }
+
+ [Fact]
+ public async Task SubmitGetCustomFields_Failure_Throws()
+ {
+ var client = MakeClient(_ => JsonResponse(HttpStatusCode.InternalServerError, "boom"), out _);
+
+ await Assert.ThrowsAsync(() => client.SubmitGetCustomFields());
+ }
+
+ [Fact]
+ public async Task SubmitGetCustomFields_UnparsableBody_Throws()
+ {
+ var client = MakeClient(_ => JsonResponse(HttpStatusCode.OK, "null"), out _);
+
+ await Assert.ThrowsAsync(() => client.SubmitGetCustomFields());
+ }
+
+ // ---------------------------------------------------------------------
+ // SubmitRevokeCertificateAsync
+ // ---------------------------------------------------------------------
+
+ [Fact]
+ public async Task SubmitRevokeCertificateAsync_Success_ReturnsParsedResponse()
+ {
+ var client = MakeClient(_ => JsonResponse(HttpStatusCode.OK,
+ "{\"revokeSuccess\":{\"commonName\":\"revoked.example.com\",\"certificateType\":\"4\",\"status\":\"REVOKED\"}}"), out var handler);
+
+ var response = await client.SubmitRevokeCertificateAsync("revoke-uuid");
+
+ Assert.Equal("revoked.example.com", response.RevokeSuccess.CommonName);
+ Assert.Equal("4", response.RevokeSuccess.CertificateType);
+ Assert.Equal("REVOKED", response.RevokeSuccess.Status);
+ Assert.Contains("/dbs/api/v2/tls/revoke/revoke-uuid", handler.LastRequest!.RequestUri!.ToString());
+ }
+
+ [Fact]
+ public async Task SubmitRevokeCertificateAsync_BadRequest_ReturnsRegistrationError()
+ {
+ var client = MakeClient(_ => JsonResponse(HttpStatusCode.BadRequest, "{\"description\":\"already revoked\"}"), out _);
+
+ var response = await client.SubmitRevokeCertificateAsync("revoke-uuid");
+
+ Assert.Equal("already revoked", response.RegistrationError.Description);
+ }
+
+ [Fact]
+ public async Task SubmitRevokeCertificateAsync_OtherError_Throws()
+ {
+ var client = MakeClient(_ => JsonResponse(HttpStatusCode.InternalServerError, "boom"), out _);
+
+ await Assert.ThrowsAsync(() => client.SubmitRevokeCertificateAsync("revoke-uuid"));
+ }
+
+ [Fact]
+ public async Task SubmitRevokeCertificateAsync_UnparsableSuccessBody_Throws()
+ {
+ var client = MakeClient(_ => JsonResponse(HttpStatusCode.OK, "null"), out _);
+
+ await Assert.ThrowsAsync(() => client.SubmitRevokeCertificateAsync("revoke-uuid"));
+ }
+
+ // ---------------------------------------------------------------------
+ // SubmitCertificateListRequestAsync
+ // ---------------------------------------------------------------------
+
+ [Fact]
+ public async Task SubmitCertificateListRequestAsync_NoDateFilter_Success()
+ {
+ var client = MakeClient(_ => JsonResponse(HttpStatusCode.OK,
+ "{\"meta\":{\"numResults\":1},\"results\":[{\"uuid\":\"cert-1\"}]}"), out var handler);
+
+ var response = await client.SubmitCertificateListRequestAsync();
+
+ Assert.Single(response.Results);
+ Assert.Equal(1, response.Meta.NumResults);
+ Assert.DoesNotContain("effectiveDate", handler.LastRequest!.RequestUri!.ToString());
+ }
+
+ [Fact]
+ public async Task SubmitCertificateListRequestAsync_WithDateFilter_IncludesFilterInQuery()
+ {
+ var client = MakeClient(_ => JsonResponse(HttpStatusCode.OK, "{\"results\":[]}"), out var handler);
+
+ await client.SubmitCertificateListRequestAsync("2026/01/01");
+
+ Assert.Contains("effectiveDate=ge=2026/01/01", handler.LastRequest!.RequestUri!.ToString());
+ }
+
+ [Fact]
+ public async Task SubmitCertificateListRequestAsync_Failure_Throws()
+ {
+ var client = MakeClient(_ => JsonResponse(HttpStatusCode.InternalServerError, "boom"), out _);
+
+ await Assert.ThrowsAsync(() => client.SubmitCertificateListRequestAsync());
+ }
+
+ [Fact]
+ public async Task SubmitCertificateListRequestAsync_UnparsableBody_Throws()
+ {
+ var client = MakeClient(_ => JsonResponse(HttpStatusCode.OK, "null"), out _);
+
+ await Assert.ThrowsAsync(() => client.SubmitCertificateListRequestAsync());
+ }
+}
diff --git a/cscglobal-caplugin.Tests/FlowLoggerTests.cs b/cscglobal-caplugin.Tests/FlowLoggerTests.cs
new file mode 100644
index 0000000..15b7f5e
--- /dev/null
+++ b/cscglobal-caplugin.Tests/FlowLoggerTests.cs
@@ -0,0 +1,194 @@
+// 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.
+
+using Keyfactor.Extensions.CAPlugin.CSCGlobal;
+using Microsoft.Extensions.Logging;
+using Moq;
+using Xunit;
+
+namespace CscGlobalCAPluginTests;
+
+public class FlowLoggerTests
+{
+ private static Mock NewLoggerMock()
+ {
+ var mock = new Mock();
+ mock.Setup(l => l.IsEnabled(It.IsAny())).Returns(true);
+ return mock;
+ }
+
+ [Fact]
+ public void Constructor_NullLogger_Throws()
+ {
+ Assert.Throws(() => new FlowLogger(null!, "Flow"));
+ }
+
+ [Fact]
+ public void Constructor_NullFlowName_Throws()
+ {
+ Assert.Throws(() => new FlowLogger(NewLoggerMock().Object, null!));
+ }
+
+ [Fact]
+ public void Step_NoDetail_DoesNotThrowAndMarksNoFailure()
+ {
+ using var flow = new FlowLogger(NewLoggerMock().Object, "Flow");
+ flow.Step("StepOne");
+ Assert.False(flow.HasFailures);
+ }
+
+ [Fact]
+ public void Step_WithDetail_DoesNotThrow()
+ {
+ using var flow = new FlowLogger(NewLoggerMock().Object, "Flow");
+ flow.Step("StepOne", "some detail");
+ Assert.False(flow.HasFailures);
+ }
+
+ [Fact]
+ public void Step_Action_Success_RecordsSuccess()
+ {
+ using var flow = new FlowLogger(NewLoggerMock().Object, "Flow");
+ var ran = false;
+ flow.Step("Action", () => ran = true);
+ Assert.True(ran);
+ Assert.False(flow.HasFailures);
+ }
+
+ [Fact]
+ public void Step_Action_Throws_RecordsFailureAndRethrows()
+ {
+ using var flow = new FlowLogger(NewLoggerMock().Object, "Flow");
+ Assert.Throws(() =>
+ flow.Step("Action", () => throw new InvalidOperationException("boom")));
+ Assert.True(flow.HasFailures);
+ }
+
+ [Fact]
+ public void Step_ActionWithDetail_Success()
+ {
+ using var flow = new FlowLogger(NewLoggerMock().Object, "Flow");
+ flow.Step("Action", () => { }, "detail");
+ Assert.False(flow.HasFailures);
+ }
+
+ [Fact]
+ public async Task StepAsync_NoReturnValue_Success()
+ {
+ using var flow = new FlowLogger(NewLoggerMock().Object, "Flow");
+ await flow.StepAsync("AsyncStep", () => Task.CompletedTask);
+ Assert.False(flow.HasFailures);
+ }
+
+ [Fact]
+ public async Task StepAsync_NoReturnValue_Throws_RecordsFailureAndRethrows()
+ {
+ using var flow = new FlowLogger(NewLoggerMock().Object, "Flow");
+ await Assert.ThrowsAsync(() =>
+ flow.StepAsync("AsyncStep", () => throw new InvalidOperationException("boom")));
+ Assert.True(flow.HasFailures);
+ }
+
+ [Fact]
+ public async Task StepAsync_WithReturnValue_Success_ReturnsResult()
+ {
+ using var flow = new FlowLogger(NewLoggerMock().Object, "Flow");
+ var result = await flow.StepAsync("AsyncStep", () => Task.FromResult(42));
+ Assert.Equal(42, result);
+ Assert.False(flow.HasFailures);
+ }
+
+ [Fact]
+ public async Task StepAsync_WithReturnValue_Throws_RecordsFailureAndRethrows()
+ {
+ using var flow = new FlowLogger(NewLoggerMock().Object, "Flow");
+ await Assert.ThrowsAsync(() =>
+ flow.StepAsync("AsyncStep", () => throw new InvalidOperationException("boom")));
+ Assert.True(flow.HasFailures);
+ }
+
+ [Fact]
+ public void StepFunc_Success_ReturnsResult()
+ {
+ using var flow = new FlowLogger(NewLoggerMock().Object, "Flow");
+ var result = flow.Step("Func", () => 99);
+ Assert.Equal(99, result);
+ Assert.False(flow.HasFailures);
+ }
+
+ [Fact]
+ public void StepFunc_Throws_RecordsFailureAndRethrows()
+ {
+ using var flow = new FlowLogger(NewLoggerMock().Object, "Flow");
+ Assert.Throws(() =>
+ flow.Step("Func", () => throw new InvalidOperationException("boom")));
+ Assert.True(flow.HasFailures);
+ }
+
+ [Fact]
+ public void Fail_RecordsFailure()
+ {
+ using var flow = new FlowLogger(NewLoggerMock().Object, "Flow");
+ flow.Fail("StepOne", "reason");
+ Assert.True(flow.HasFailures);
+ }
+
+ [Fact]
+ public void Skip_DoesNotRecordFailure()
+ {
+ using var flow = new FlowLogger(NewLoggerMock().Object, "Flow");
+ flow.Skip("StepOne", "not applicable");
+ Assert.False(flow.HasFailures);
+ }
+
+ [Fact]
+ public void Branch_EndBranch_RoundTrips()
+ {
+ using var flow = new FlowLogger(NewLoggerMock().Object, "Flow");
+ flow.Branch("Inner");
+ flow.Step("NestedStep");
+ flow.EndBranch();
+ Assert.False(flow.HasFailures);
+ }
+
+ [Fact]
+ public void EndBranch_WithoutBranch_DoesNotThrow()
+ {
+ using var flow = new FlowLogger(NewLoggerMock().Object, "Flow");
+ flow.EndBranch();
+ }
+
+ [Fact]
+ public void GetSummary_IncludesAllStepKindsAndCounts()
+ {
+ using var flow = new FlowLogger(NewLoggerMock().Object, "Flow");
+ flow.Step("Ok");
+ flow.Skip("Skipped", "n/a");
+ flow.Fail("Failed", "bad");
+
+ var summary = flow.GetSummary();
+
+ Assert.Contains("FAILED", summary);
+ Assert.Contains("Steps: 3 total, 1 ok, 1 failed, 1 skipped", summary);
+ Assert.Contains("[OK]", summary);
+ Assert.Contains("[FAIL]", summary);
+ Assert.Contains("[SKIP]", summary);
+ }
+
+ [Fact]
+ public void GetSummary_NoFailures_ReportsOk()
+ {
+ using var flow = new FlowLogger(NewLoggerMock().Object, "Flow");
+ flow.Step("Ok");
+ Assert.Contains("[OK]", flow.GetSummary());
+ Assert.DoesNotContain("FAILED", flow.GetSummary());
+ }
+
+ [Fact]
+ public void Dispose_DoesNotThrow()
+ {
+ var flow = new FlowLogger(NewLoggerMock().Object, "Flow");
+ flow.Step("Ok");
+ flow.Dispose();
+ }
+}
diff --git a/cscglobal-caplugin.Tests/RequestManagerTests.cs b/cscglobal-caplugin.Tests/RequestManagerTests.cs
new file mode 100644
index 0000000..fa1437b
--- /dev/null
+++ b/cscglobal-caplugin.Tests/RequestManagerTests.cs
@@ -0,0 +1,547 @@
+// 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.
+
+using Keyfactor.AnyGateway.Extensions;
+using Keyfactor.Extensions.CAPlugin.CSCGlobal;
+using Keyfactor.Extensions.CAPlugin.CSCGlobal.Client.Models;
+using Keyfactor.PKI.Enums.EJBCA;
+using Xunit;
+
+namespace CscGlobalCAPluginTests;
+
+public class RequestManagerTests
+{
+ private const string SampleCsr = "sample-csr-body";
+
+ private static EnrollmentProductInfo ProductInfo(string productId, Dictionary? parameters = null) =>
+ new EnrollmentProductInfo
+ {
+ ProductID = productId,
+ ProductParameters = parameters ?? new Dictionary()
+ };
+
+ private static RequestManager Manager => new RequestManager();
+
+ // ---------------------------------------------------------------------
+ // GetRenewResponse
+ // ---------------------------------------------------------------------
+
+ [Fact]
+ public void GetRenewResponse_NullResponse_ReturnsFailed()
+ {
+ var result = Manager.GetRenewResponse(null);
+ Assert.Equal((int)EndEntityStatus.FAILED, result.Status);
+ Assert.Contains("no response", result.StatusMessage);
+ }
+
+ [Fact]
+ public void GetRenewResponse_RegistrationError_ReturnsFailedWithDescription()
+ {
+ var response = new RenewalResponse
+ {
+ RegistrationError = new RegistrationError { Description = "boom" },
+ Result = new Result { Status = new Status { Uuid = "abc-123" } }
+ };
+
+ var result = Manager.GetRenewResponse(response);
+
+ Assert.Equal((int)EndEntityStatus.FAILED, result.Status);
+ Assert.Equal("boom", result.StatusMessage);
+ Assert.Equal("abc-123", result.CARequestID);
+ }
+
+ [Fact]
+ public void GetRenewResponse_NullResult_ReturnsFailed()
+ {
+ var response = new RenewalResponse { Result = null };
+ var result = Manager.GetRenewResponse(response);
+ Assert.Equal((int)EndEntityStatus.FAILED, result.Status);
+ Assert.Contains("no result", result.StatusMessage);
+ }
+
+ [Fact]
+ public void GetRenewResponse_Success_ReturnsGenerated()
+ {
+ var response = new RenewalResponse { Result = new Result { CommonName = "renewed.example.com" } };
+ var result = Manager.GetRenewResponse(response);
+ Assert.Equal((int)EndEntityStatus.GENERATED, result.Status);
+ Assert.Contains("renewed.example.com", result.StatusMessage);
+ }
+
+ // ---------------------------------------------------------------------
+ // GetEnrollmentResult
+ // ---------------------------------------------------------------------
+
+ [Fact]
+ public void GetEnrollmentResult_NullResponse_ReturnsFailed()
+ {
+ var result = Manager.GetEnrollmentResult(null);
+ Assert.Equal((int)EndEntityStatus.FAILED, result.Status);
+ }
+
+ [Fact]
+ public void GetEnrollmentResult_RegistrationError_ReturnsFailed()
+ {
+ var response = new RegistrationResponse { RegistrationError = new RegistrationError { Description = "bad request" } };
+ var result = Manager.GetEnrollmentResult(response);
+ Assert.Equal((int)EndEntityStatus.FAILED, result.Status);
+ Assert.Equal("bad request", result.StatusMessage);
+ }
+
+ [Fact]
+ public void GetEnrollmentResult_NullResult_ReturnsFailed()
+ {
+ var response = new RegistrationResponse { Result = null };
+ var result = Manager.GetEnrollmentResult(response);
+ Assert.Equal((int)EndEntityStatus.FAILED, result.Status);
+ }
+
+ [Fact]
+ public void GetEnrollmentResult_SuccessNoDcvDetails_ReturnsExternalValidationWithNullContext()
+ {
+ var response = new RegistrationResponse
+ {
+ Result = new Result { CommonName = "order-1", Status = new Status { Uuid = "uuid-1" } }
+ };
+
+ var result = Manager.GetEnrollmentResult(response);
+
+ Assert.Equal((int)EndEntityStatus.EXTERNALVALIDATION, result.Status);
+ Assert.Equal("uuid-1", result.CARequestID);
+ Assert.Null(result.EnrollmentContext);
+ }
+
+ [Fact]
+ public void GetEnrollmentResult_WithCNameAndEmailDcvDetails_PopulatesEnrollmentContext()
+ {
+ var response = new RegistrationResponse
+ {
+ Result = new Result
+ {
+ CommonName = "order-2",
+ Status = new Status { Uuid = "uuid-2" },
+ DcvDetails = new List
+ {
+ new DcvDetail { CName = new CName { Name = "_dnsauth.example.com", Value = "token" } },
+ new DcvDetail { Email = "admin@example.com" },
+ // Duplicate email key should not throw and should not be added twice.
+ new DcvDetail { Email = "admin@example.com" },
+ // Entry with neither CName nor Email contributes nothing.
+ new DcvDetail()
+ }
+ }
+ };
+
+ var result = Manager.GetEnrollmentResult(response);
+
+ Assert.NotNull(result.EnrollmentContext);
+ Assert.Equal("token", result.EnrollmentContext["_dnsauth.example.com"]);
+ Assert.Equal("admin@example.com", result.EnrollmentContext["admin@example.com"]);
+ Assert.Equal(2, result.EnrollmentContext.Count);
+ }
+
+ // ---------------------------------------------------------------------
+ // GetRevokeResult
+ // ---------------------------------------------------------------------
+
+ [Fact]
+ public void GetRevokeResult_NullResponse_ReturnsFailed()
+ {
+ Assert.Equal((int)EndEntityStatus.FAILED, Manager.GetRevokeResult(null));
+ }
+
+ [Fact]
+ public void GetRevokeResult_RegistrationError_ReturnsFailed()
+ {
+ var response = new RevokeResponse { RegistrationError = new RegistrationError { Description = "nope" } };
+ Assert.Equal((int)EndEntityStatus.FAILED, Manager.GetRevokeResult(response));
+ }
+
+ [Fact]
+ public void GetRevokeResult_Success_ReturnsRevoked()
+ {
+ var response = new RevokeResponse();
+ Assert.Equal((int)EndEntityStatus.REVOKED, Manager.GetRevokeResult(response));
+ }
+
+ // ---------------------------------------------------------------------
+ // GetReIssueResult
+ // ---------------------------------------------------------------------
+
+ [Fact]
+ public void GetReIssueResult_NullResponse_ReturnsFailed()
+ {
+ var result = Manager.GetReIssueResult(null);
+ Assert.Equal((int)EndEntityStatus.FAILED, result.Status);
+ }
+
+ [Fact]
+ public void GetReIssueResult_RegistrationError_ReturnsFailed()
+ {
+ var response = new ReissueResponse { RegistrationError = new RegistrationError { Description = "rejected" } };
+ var result = Manager.GetReIssueResult(response);
+ Assert.Equal((int)EndEntityStatus.FAILED, result.Status);
+ Assert.Equal("rejected", result.StatusMessage);
+ }
+
+ [Fact]
+ public void GetReIssueResult_NullResult_ReturnsFailed()
+ {
+ var response = new ReissueResponse { Result = null };
+ var result = Manager.GetReIssueResult(response);
+ Assert.Equal((int)EndEntityStatus.FAILED, result.Status);
+ }
+
+ [Fact]
+ public void GetReIssueResult_Success_ReturnsGenerated()
+ {
+ var response = new ReissueResponse
+ {
+ Result = new Result { CommonName = "reissued.example.com", Status = new Status { Uuid = "uuid-3" } }
+ };
+ var result = Manager.GetReIssueResult(response);
+ Assert.Equal((int)EndEntityStatus.GENERATED, result.Status);
+ Assert.Equal("uuid-3", result.CARequestID);
+ }
+
+ // ---------------------------------------------------------------------
+ // GetDomainControlValidation (email-list overload)
+ // ---------------------------------------------------------------------
+
+ [Fact]
+ public void GetDomainControlValidation_EmptyDomainName_ReturnsNull()
+ {
+ var result = Manager.GetDomainControlValidation("EMAIL", new[] { "admin@example.com" }, "");
+ Assert.Null(result);
+ }
+
+ [Fact]
+ public void GetDomainControlValidation_NullEmailArray_ReturnsNull()
+ {
+ var result = Manager.GetDomainControlValidation("EMAIL", null!, "example.com");
+ Assert.Null(result);
+ }
+
+ [Fact]
+ public void GetDomainControlValidation_MalformedEmailSkipped_NoMatchReturnsNull()
+ {
+ var result = Manager.GetDomainControlValidation("EMAIL", new[] { "not-an-email", " " }, "example.com");
+ Assert.Null(result);
+ }
+
+ [Fact]
+ public void GetDomainControlValidation_MatchingHostFound_ReturnsValidation()
+ {
+ var result = Manager.GetDomainControlValidation("EMAIL", new[] { "not-an-email", "admin@example.com" }, "www.example.com");
+ Assert.NotNull(result);
+ Assert.Equal("EMAIL", result.MethodType);
+ Assert.Contains("admin@example.com", result.EmailAddress);
+ }
+
+ [Fact]
+ public void GetDomainControlValidation_NoMatchingHost_ReturnsNull()
+ {
+ var result = Manager.GetDomainControlValidation("EMAIL", new[] { "admin@other.com" }, "www.example.com");
+ Assert.Null(result);
+ }
+
+ // ---------------------------------------------------------------------
+ // GetDomainControlValidation (single-email overload)
+ // ---------------------------------------------------------------------
+
+ [Fact]
+ public void GetDomainControlValidation_SingleEmail_ReturnsValidationVerbatim()
+ {
+ var result = Manager.GetDomainControlValidation("CNAME", "admin@example.com");
+ Assert.Equal("CNAME", result.MethodType);
+ Assert.Equal("admin@example.com", result.EmailAddress);
+ }
+
+ // ---------------------------------------------------------------------
+ // MapReturnStatus
+ // ---------------------------------------------------------------------
+
+ public static IEnumerable