diff --git a/.claude/settings.json b/.claude/settings.json
new file mode 100644
index 0000000..c64f0fd
--- /dev/null
+++ b/.claude/settings.json
@@ -0,0 +1,8 @@
+{
+ "permissions": {
+ "allow": [
+ "Bash(git fetch:*)",
+ "Bash(git checkout:*)"
+ ]
+ }
+}
diff --git a/README.md b/README.md
index b7fb319..f5e93ae 100644
--- a/README.md
+++ b/README.md
@@ -1,5 +1,5 @@
- CSCGlobal CA Gateway AnyCA Gateway REST Plugin
+ CSCGlobal CAPlugin AnyCA Gateway REST Plugin
@@ -38,10 +38,10 @@ This integration allows for the Synchronization, Enrollment, and Revocation of c
## Compatibility
-The CSCGlobal CA Gateway AnyCA Gateway REST plugin is compatible with the Keyfactor AnyCA Gateway REST 24.2.0 and later.
+The CSCGlobal CAPlugin AnyCA Gateway REST plugin is compatible with the Keyfactor AnyCA Gateway REST 24.2.0 and later.
## Support
-The CSCGlobal CA Gateway 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 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.
> 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.
@@ -53,7 +53,7 @@ This integration is tested and confirmed as working for Anygateway REST 24.2 and
1. Install the AnyCA Gateway REST per the [official Keyfactor documentation](https://software.keyfactor.com/Guides/AnyCAGatewayREST/Content/AnyCAGatewayREST/InstallIntroduction.htm).
-2. On the server hosting the AnyCA Gateway REST, download and unzip the latest [CSCGlobal CA Gateway AnyCA Gateway REST plugin](https://github.com/Keyfactor/cscglobal-caplugin/releases/latest) from GitHub.
+2. On the server hosting the AnyCA Gateway REST, download and unzip the latest [CSCGlobal CAPlugin AnyCA Gateway REST plugin](https://github.com/Keyfactor/cscglobal-caplugin/releases/latest) from GitHub.
3. Copy the unzipped directory (usually called `net6.0` or `net8.0`) to the Extensions directory:
@@ -64,11 +64,11 @@ This integration is tested and confirmed as working for Anygateway REST 24.2 and
Program Files\Keyfactor\AnyCA Gateway\AnyGatewayREST\net8.0\Extensions
```
- > The directory containing the CSCGlobal CA Gateway AnyCA Gateway REST plugin DLLs (`net6.0` or `net8.0`) can be named anything, as long as it is unique within the `Extensions` directory.
+ > The directory containing the CSCGlobal CAPlugin AnyCA Gateway REST plugin DLLs (`net6.0` or `net8.0`) can be named anything, as long as it is unique within the `Extensions` directory.
4. Restart the AnyCA Gateway REST service.
-5. Navigate to the AnyCA Gateway REST portal and verify that the Gateway recognizes the CSCGlobal CA Gateway plugin by hovering over the ⓘ symbol to the right of the Gateway on the top left of the portal.
+5. Navigate to the AnyCA Gateway REST portal and verify that the Gateway recognizes the CSCGlobal CAPlugin plugin by hovering over the ⓘ symbol to the right of the Gateway on the top left of the portal.
## Configuration
@@ -86,8 +86,8 @@ 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.
+ * **RenewalWindowDays** - Number of days before the annual order expiry within which a RenewOrReissue triggers a paid Renewal rather than a free Reissue. Default is 30.
2. PLEASE NOTE, AT THIS TIME THE RAPID_SSL TEMPLATE IS NOT SUPPORTED BY THE CSC API AND WILL NOT WORK WITH THIS INTEGRATION
@@ -311,6 +311,55 @@ This integration is tested and confirmed as working for Anygateway REST 24.2 and
* **Addtl Sans Comma Separated DCV Emails** - OPTIONAL: Additional SANs DCV Emails, comma separated
+## CA Connection Configuration
+
+When defining the Certificate Authority in the AnyCA Gateway REST portal, configure the following fields on the **CA Connection** tab:
+
+CONFIG ELEMENT | DESCRIPTION | DEFAULT
+---------------|-------------|--------
+CscGlobalUrl | The base URL for the CSCGlobal API (e.g. `https://apis.cscglobal.com`) | (required)
+ApiKey | Your CSCGlobal API key | (required)
+BearerToken | Your CSCGlobal Bearer token for authentication | (required)
+DefaultPageSize | Page size for API list requests | 100
+SyncFilterDays | Number of days from today used to filter certificates by expiration date during **incremental** sync. Only certificates expiring within this window are returned. Does not apply to full sync. | 5
+RenewalWindowDays | Number of days before the annual order expiry date within which a **RenewOrReissue** request triggers a paid **Renewal** rather than a free **Reissue**. See [Renewal vs. Reissue Logic](#renewal-vs-reissue-logic) below. | 30
+
+## Renewal vs. Reissue Logic
+
+CSC Global subscriptions are annual orders. When Keyfactor Command sends a **RenewOrReissue** request, the plugin must decide whether to submit a **Renewal** (a new paid order) or a **Reissue** (a free re-key under the existing active order).
+
+The decision is based on the **RenewalWindowDays** setting and works as follows:
+
+1. The plugin fetches the original certificate from CSC and reads its `orderDate`.
+2. It computes the **order expiry** as `orderDate + 1 year`.
+3. It calculates **days remaining** until the order expires.
+4. If `days remaining <= RenewalWindowDays`, the request is treated as a **Renewal** (new paid order).
+5. If `days remaining > RenewalWindowDays`, the request is treated as a **Reissue** (free under the active order).
+
+**Example with default RenewalWindowDays = 30:**
+
+```
+Order Date: 2025-04-08
+Order Expiry: 2026-04-08
+Today: 2026-03-15
+Days Left: 24
+
+24 <= 30 --> RENEWAL (new paid order)
+```
+
+```
+Order Date: 2025-04-08
+Order Expiry: 2026-04-08
+Today: 2025-09-01
+Days Left: 219
+
+219 > 30 --> REISSUE (free under active order)
+```
+
+**Fallback behavior:** If the plugin cannot retrieve the `orderDate` from CSC (e.g., API error or missing field), it falls back to checking the certificate's expiration date. If the certificate is already expired, it treats the request as a Renewal.
+
+**Note:** Both Renewal and Reissue submissions are asynchronous at CSC. The plugin returns a "pending" status and the issued certificate will appear in Keyfactor after the next sync cycle.
+
## License
diff --git a/cscglobal-caplugin/CSCGlobalCAPlugin.cs b/cscglobal-caplugin/CSCGlobalCAPlugin.cs
index e1af2f0..5449f64 100644
--- a/cscglobal-caplugin/CSCGlobalCAPlugin.cs
+++ b/cscglobal-caplugin/CSCGlobalCAPlugin.cs
@@ -35,68 +35,183 @@ public CSCGlobalCAPlugin()
private ICscGlobalClient CscGlobalClient { get; set; }
- public bool EnableTemplateSync { get; set; }
-
public int SyncFilterDays { get; set; }
+ public int RenewalWindowDays { get; set; }
+
//done
public void Initialize(IAnyCAPluginConfigProvider configProvider, ICertificateDataReader certificateDataReader)
{
+ using var flow = new FlowLogger(Logger, "Initialize");
Logger.MethodEntry(LogLevel.Debug);
+ Logger.LogTrace("Initialize called. configProvider is {Null}, certificateDataReader is {Null2}",
+ configProvider == null ? "NULL" : "present",
+ certificateDataReader == null ? "NULL" : "present");
+
+ flow.Step("ValidateInputs", () =>
+ {
+ if (configProvider == null)
+ throw new ArgumentNullException(nameof(configProvider), "configProvider cannot be null in Initialize");
+ if (certificateDataReader == null)
+ throw new ArgumentNullException(nameof(certificateDataReader), "certificateDataReader cannot be null in Initialize");
+ });
+
_certificateDataReader = certificateDataReader;
- CscGlobalClient = new CscGlobalClient(configProvider);
- var templateSync = configProvider.CAConnectionData["TemplateSync"].ToString();
- if (templateSync.ToUpper() == "ON") EnableTemplateSync = true;
- if (configProvider.CAConnectionData.ContainsKey(Constants.SyncFilterDays))
+ flow.Step("CreateCscGlobalClient", () =>
+ {
+ Logger.LogTrace("Creating CscGlobalClient from configProvider...");
+ CscGlobalClient = new CscGlobalClient(configProvider);
+ Logger.LogTrace("CscGlobalClient created successfully.");
+ });
+
+ flow.Step("ValidateConnectionData", () =>
{
- var syncFilterDaysStr = configProvider.CAConnectionData[Constants.SyncFilterDays]?.ToString();
- if (int.TryParse(syncFilterDaysStr, out var syncFilterDays))
+ if (configProvider.CAConnectionData == null)
{
- SyncFilterDays = syncFilterDays;
- Logger.LogDebug($"SyncFilterDays configured to {SyncFilterDays} days");
+ Logger.LogError("CAConnectionData is null. Cannot read configuration.");
+ throw new InvalidOperationException("CAConnectionData is null on configProvider.");
}
- }
+ Logger.LogTrace("CAConnectionData keys: {Keys}", string.Join(", ", configProvider.CAConnectionData.Keys));
+ });
+
+ flow.Step("ReadSyncFilterDays", () =>
+ {
+ if (configProvider.CAConnectionData.ContainsKey(Constants.SyncFilterDays))
+ {
+ var syncFilterDaysStr = configProvider.CAConnectionData[Constants.SyncFilterDays]?.ToString();
+ Logger.LogTrace("SyncFilterDays raw value: '{Value}'", syncFilterDaysStr ?? "(null)");
+ if (int.TryParse(syncFilterDaysStr, out var syncFilterDays))
+ {
+ SyncFilterDays = syncFilterDays;
+ Logger.LogDebug("SyncFilterDays configured to {Days} days", SyncFilterDays);
+ }
+ else
+ {
+ Logger.LogWarning("SyncFilterDays value '{Value}' could not be parsed as int, using default 0.", syncFilterDaysStr);
+ }
+ }
+ else
+ {
+ Logger.LogTrace("SyncFilterDays key not found in CAConnectionData, using default 0.");
+ }
+ });
+
+ flow.Step("ReadRenewalWindowDays", () =>
+ {
+ RenewalWindowDays = 30; // default
+ if (configProvider.CAConnectionData.TryGetValue(Constants.RenewalWindowDays, out var renewalWindowObj))
+ {
+ Logger.LogTrace("RenewalWindowDays raw value: '{Value}'", renewalWindowObj?.ToString() ?? "(null)");
+ if (int.TryParse(renewalWindowObj?.ToString(), out var renewalWindowDays) && renewalWindowDays > 0)
+ RenewalWindowDays = renewalWindowDays;
+ else
+ Logger.LogWarning("RenewalWindowDays value '{Value}' could not be parsed or was <= 0, using default 30.", renewalWindowObj);
+ }
+ else
+ {
+ Logger.LogTrace("RenewalWindowDays key not found in CAConnectionData, using default 30.");
+ }
+ Logger.LogDebug("RenewalWindowDays configured to {Days} days", RenewalWindowDays);
+ }, $"RenewalWindowDays={RenewalWindowDays}");
+
Logger.MethodExit(LogLevel.Debug);
}
//done
public async Task GetSingleRecord(string caRequestID)
{
+ using var flow = new FlowLogger(Logger, $"GetSingleRecord({caRequestID ?? "null"})");
+ Logger.MethodEntry(LogLevel.Debug);
+ Logger.LogTrace("GetSingleRecord called with caRequestID='{CaRequestId}'", caRequestID ?? "(null)");
+
+ flow.Step("ValidateInput", () =>
+ {
+ if (string.IsNullOrEmpty(caRequestID))
+ throw new ArgumentNullException(nameof(caRequestID), "caRequestID cannot be null or empty.");
+ if (caRequestID.Length < 36)
+ throw new ArgumentException($"caRequestID '{caRequestID}' is too short to extract a UUID (need at least 36 chars).", nameof(caRequestID));
+ });
+
try
{
- Logger.MethodEntry(LogLevel.Debug);
- 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))
- .Result;
+ var keyfactorCaId = caRequestID.Substring(0, 36);
+ flow.Step("ExtractUUID", $"keyfactorCaId={keyfactorCaId}");
- Logger.LogTrace($"Single Cert JSON: {JsonConvert.SerializeObject(certificateResponse)}");
+ CertificateResponse certificateResponse = null;
+ await flow.StepAsync("FetchCertFromCSC", async () =>
+ {
+ certificateResponse = await CscGlobalClient.SubmitGetCertificateAsync(keyfactorCaId);
+ });
- var fileContent =
- Encoding.ASCII.GetString(
- Convert.FromBase64String(certificateResponse?.Certificate ?? string.Empty));
+ if (certificateResponse == null)
+ {
+ flow.Fail("ParseResponse", "API returned null");
+ Logger.LogWarning("GetSingleRecord: SubmitGetCertificateAsync returned null for keyfactorCaId='{KeyfactorCaId}'", keyfactorCaId);
+ return new AnyCAPluginCertificate
+ {
+ CARequestID = keyfactorCaId,
+ Certificate = string.Empty,
+ Status = _requestManager.MapReturnStatus(null)
+ };
+ }
+
+ flow.Step("ParseResponse", $"Status={certificateResponse.Status ?? "(null)"}");
+ Logger.LogTrace("Single Cert JSON: {Json}", JsonConvert.SerializeObject(certificateResponse));
+
+ var rawCert = certificateResponse.Certificate ?? string.Empty;
+ string fileContent = string.Empty;
+ flow.Step("DecodeBase64", () =>
+ {
+ try
+ {
+ fileContent = Encoding.ASCII.GetString(Convert.FromBase64String(rawCert));
+ }
+ catch (FormatException fex)
+ {
+ Logger.LogError(fex, "GetSingleRecord: Failed to decode Base64 certificate content for keyfactorCaId='{KeyfactorCaId}'", keyfactorCaId);
+ fileContent = string.Empty;
+ }
+ }, $"length={rawCert.Length}");
- Logger.LogTrace($"File Content {fileContent}");
- var certData = fileContent?.Replace("\r\n", string.Empty);
+ var certData = fileContent.Replace("\r\n", string.Empty);
var certString = string.Empty;
if (!string.IsNullOrEmpty(certData))
- certString = GetEndEntityCertificate(certData);
- Logger.LogTrace($"Cert String Content {certString}");
+ {
+ flow.Step("ExtractLeafCert", () =>
+ {
+ certString = GetEndEntityCertificate(certData);
+ }, $"inputLength={certData.Length}");
+ }
+ else
+ {
+ flow.Skip("ExtractLeafCert", "certData empty after cleanup");
+ }
+
+ var mappedStatus = _requestManager.MapReturnStatus(certificateResponse.Status);
+ flow.Step("MapStatus", $"{certificateResponse.Status ?? "(null)"} -> {mappedStatus}");
Logger.MethodExit(LogLevel.Debug);
return new AnyCAPluginCertificate
{
CARequestID = keyfactorCaId,
- Certificate = certString,
- Status = _requestManager.MapReturnStatus(certificateResponse?.Status)
+ Certificate = certString ?? string.Empty,
+ Status = mappedStatus
};
}
+ catch (AggregateException ae)
+ {
+ var inner = ae.Flatten().InnerException;
+ flow.Fail("UNHANDLED", inner?.Message ?? ae.Message);
+ Logger.LogError(inner, "GetSingleRecord: AggregateException for caRequestID='{CaRequestId}': {Message}", caRequestID, inner?.Message ?? ae.Message);
+ throw new Exception($"Error Occurred getting single cert for '{caRequestID}': {inner?.Message ?? ae.Message}", inner ?? ae);
+ }
catch (Exception e)
{
- throw new Exception($"Error Occurred getting single cert {e.Message}");
+ flow.Fail("UNHANDLED", e.Message);
+ Logger.LogError(e, "GetSingleRecord: Exception for caRequestID='{CaRequestId}': {Message}", caRequestID, e.Message);
+ throw new Exception($"Error Occurred getting single cert for '{caRequestID}': {e.Message}", e);
}
}
@@ -104,31 +219,56 @@ public async Task GetSingleRecord(string caRequestID)
public async Task Synchronize(BlockingCollection blockingBuffer, DateTime? lastSync,
bool fullSync, CancellationToken cancelToken)
{
- Logger.LogTrace($"Full Sync? {fullSync.ToString()}");
+ var syncType = fullSync ? "Full" : "Incremental";
+ using var flow = new FlowLogger(Logger, $"Synchronize-{syncType}");
Logger.MethodEntry();
+ Logger.LogTrace("Synchronize called. fullSync={FullSync}, lastSync={LastSync}, blockingBuffer is {Null}",
+ fullSync, lastSync?.ToString("o") ?? "(null)",
+ blockingBuffer == null ? "NULL" : "present");
+
+ if (blockingBuffer == null)
+ throw new ArgumentNullException(nameof(blockingBuffer), "blockingBuffer cannot be null in Synchronize");
+
try
{
if (fullSync)
{
- Logger.LogDebug("Performing full sync - no date filter applied");
- await SyncCertificates(blockingBuffer, cancelToken, null);
+ flow.Step("DetermineFilter", "Full sync - no date filter");
+ await flow.StepAsync("FetchAndProcessCerts", async () =>
+ {
+ await SyncCertificates(blockingBuffer, cancelToken, null);
+ });
}
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);
+ flow.Step("DetermineFilter", $"Incremental, filterDays={filterDays}, cutoff={dateFilter}");
+ await flow.StepAsync("FetchAndProcessCerts", async () =>
+ {
+ await SyncCertificates(blockingBuffer, cancelToken, dateFilter);
+ });
}
+ flow.Step("CompleteAdding");
blockingBuffer.CompleteAdding();
}
+ catch (OperationCanceledException)
+ {
+ flow.Fail("Cancelled", "operation was cancelled");
+ Logger.LogWarning("Synchronize: operation was cancelled.");
+ if (!blockingBuffer.IsAddingCompleted)
+ blockingBuffer.CompleteAdding();
+ throw;
+ }
catch (Exception e)
{
- Logger.LogError($"Csc Global Synchronize Task failed! {LogHandler.FlattenException(e)}");
+ flow.Fail("SyncError", e.Message);
+ Logger.LogError(e, "Csc Global Synchronize Task failed! {FlatException}", LogHandler.FlattenException(e));
+ if (!blockingBuffer.IsAddingCompleted)
+ blockingBuffer.CompleteAdding();
Logger.MethodExit();
- blockingBuffer.CompleteAdding();
throw;
}
@@ -138,70 +278,182 @@ public async Task Synchronize(BlockingCollection blockin
private async Task SyncCertificates(BlockingCollection blockingBuffer,
CancellationToken cancelToken, string? dateFilter)
{
+ Logger.LogTrace("SyncCertificates: calling SubmitCertificateListRequestAsync with dateFilter='{DateFilter}'", dateFilter ?? "(null)");
var certs = await CscGlobalClient.SubmitCertificateListRequestAsync(dateFilter);
+ if (certs == null)
+ {
+ Logger.LogWarning("SyncCertificates: SubmitCertificateListRequestAsync returned null.");
+ return;
+ }
+
+ if (certs.Results == null)
+ {
+ Logger.LogWarning("SyncCertificates: certificate list response Results collection is null.");
+ return;
+ }
+
+ Logger.LogTrace("SyncCertificates: received {Count} certificate results.", certs.Results.Count);
+ var processedCount = 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);
- //Keyfactor sync only seems to work when there is a valid cert and I can only get Active valid certs from Csc Global
+ if (currentResponseItem == null)
+ {
+ Logger.LogTrace("SyncCertificates: skipping null result item.");
+ skippedCount++;
+ continue;
+ }
+
+ Logger.LogTrace("SyncCertificates: processing certificate UUID={Uuid}, Status='{Status}', CertificateType='{CertType}'",
+ currentResponseItem.Uuid ?? "(null)",
+ currentResponseItem.Status ?? "(null)",
+ currentResponseItem.CertificateType ?? "(null)");
+
+ var certStatus = _requestManager.MapReturnStatus(currentResponseItem.Status);
+ Logger.LogTrace("SyncCertificates: mapped status for UUID={Uuid}: {MappedStatus}", currentResponseItem.Uuid ?? "(null)", certStatus);
+
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 productId = _requestManager.MapCertificateTypeToProductId(currentResponseItem.CertificateType);
- var fileContent =
- PreparePemTextFromApi(
- currentResponseItem?.Certificate ?? string.Empty);
+ Logger.LogTrace("SyncCertificates: UUID={Uuid} qualifies for sync. CertificateType='{CertType}' -> ProductId='{ProductId}'",
+ currentResponseItem.Uuid, currentResponseItem.CertificateType ?? "(null)", productId);
+
+ string fileContent;
+ try
+ {
+ fileContent = PreparePemTextFromApi(currentResponseItem.Certificate ?? string.Empty);
+ }
+ catch (Exception ex)
+ {
+ Logger.LogError(ex, "SyncCertificates: PreparePemTextFromApi failed for UUID={Uuid}", currentResponseItem.Uuid);
+ skippedCount++;
+ continue;
+ }
if (fileContent.Length > 0)
{
- Logger.LogTrace($"File Content {fileContent}");
+ Logger.LogTrace("SyncCertificates: fileContent length={Length} for UUID={Uuid}", fileContent.Length, currentResponseItem.Uuid);
var certData = fileContent.Replace("\r\n", string.Empty);
- var certString = GetEndEntityCertificate(certData);
- if (certString.Length > 0)
+ string certString;
+ try
+ {
+ certString = GetEndEntityCertificate(certData);
+ }
+ catch (Exception ex)
+ {
+ Logger.LogError(ex, "SyncCertificates: GetEndEntityCertificate failed for UUID={Uuid}", currentResponseItem.Uuid);
+ skippedCount++;
+ continue;
+ }
+
+ if (!string.IsNullOrEmpty(certString))
+ {
blockingBuffer.Add(new AnyCAPluginCertificate
{
- CARequestID = $"{currentResponseItem?.Uuid}",
+ CARequestID = $"{currentResponseItem.Uuid}",
Certificate = certString,
Status = certStatus,
ProductID = productId
}, cancelToken);
+ processedCount++;
+ Logger.LogTrace("SyncCertificates: added UUID={Uuid} to buffer.", currentResponseItem.Uuid);
+ }
+ else
+ {
+ Logger.LogTrace("SyncCertificates: certString was empty for UUID={Uuid}, skipping.", currentResponseItem.Uuid);
+ skippedCount++;
+ }
}
+ else
+ {
+ Logger.LogTrace("SyncCertificates: fileContent was empty for UUID={Uuid}, skipping.", currentResponseItem.Uuid);
+ skippedCount++;
+ }
+ }
+ else
+ {
+ Logger.LogTrace("SyncCertificates: UUID={Uuid} status {Status} not eligible for sync, skipping.", currentResponseItem.Uuid, certStatus);
+ skippedCount++;
}
}
+
+ Logger.LogDebug("SyncCertificates: completed. Processed={Processed}, Skipped={Skipped}, Total={Total}",
+ processedCount, skippedCount, certs.Results.Count);
}
//done
public async Task Revoke(string caRequestID, string hexSerialNumber, uint revocationReason)
{
+ using var flow = new FlowLogger(Logger, $"Revoke({caRequestID ?? "null"})");
+ Logger.MethodEntry(LogLevel.Debug);
+ Logger.LogTrace("Revoke called with caRequestID='{CaRequestId}', hexSerialNumber='{SerialNumber}', revocationReason={Reason}",
+ caRequestID ?? "(null)", hexSerialNumber ?? "(null)", revocationReason);
+
+ flow.Step("ValidateInput", () =>
+ {
+ if (string.IsNullOrEmpty(caRequestID))
+ throw new ArgumentNullException(nameof(caRequestID), "caRequestID cannot be null or empty for Revoke.");
+ if (caRequestID.Length < 36)
+ throw new ArgumentException($"caRequestID '{caRequestID}' is too short to extract a UUID.", nameof(caRequestID));
+ });
+
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
+ var uuid = caRequestID.Substring(0, 36);
+ flow.Step("ExtractUUID", $"uuid={uuid}");
- Logger.LogTrace($"Revoke Response JSON: {JsonConvert.SerializeObject(revokeResponse)}");
- Logger.MethodExit(LogLevel.Debug);
+ RevokeResponse revokeResponse = null;
+ await flow.StepAsync("SubmitRevokeToCSC", async () =>
+ {
+ revokeResponse = await CscGlobalClient.SubmitRevokeCertificateAsync(uuid);
+ });
+
+ if (revokeResponse == null)
+ {
+ flow.Fail("ParseResponse", "API returned null");
+ throw new InvalidOperationException($"Revoke received null response for UUID '{uuid}'.");
+ }
+
+ Logger.LogTrace("Revoke Response JSON: {Json}", JsonConvert.SerializeObject(revokeResponse));
var revokeResult = _requestManager.GetRevokeResult(revokeResponse);
+ flow.Step("MapResult", $"result={revokeResult}");
if (revokeResult == (int)EndEntityStatus.FAILED)
- if (!string.IsNullOrEmpty(revokeResponse?.RegistrationError?.Description))
- throw new HttpRequestException(
- $"Revoke Failed with message {revokeResponse?.RegistrationError?.Description}");
+ {
+ var errorDesc = revokeResponse.RegistrationError?.Description;
+ flow.Fail("RevokeResult", errorDesc ?? "(no description)");
+ Logger.LogError("Revoke: failed for UUID='{Uuid}'. Error description: '{ErrorDesc}'",
+ uuid, errorDesc ?? "(no description)");
+ if (!string.IsNullOrEmpty(errorDesc))
+ throw new HttpRequestException($"Revoke Failed with message {errorDesc}");
+ }
+ Logger.MethodExit(LogLevel.Debug);
return revokeResult;
}
+ catch (AggregateException ae)
+ {
+ var inner = ae.Flatten().InnerException;
+ flow.Fail("UNHANDLED", inner?.Message ?? ae.Message);
+ Logger.LogError(inner, "Revoke: AggregateException for caRequestID='{CaRequestId}': {Message}", caRequestID, inner?.Message ?? ae.Message);
+ throw new Exception($"Revoke Failed for '{caRequestID}' with message {inner?.Message ?? ae.Message}", inner ?? ae);
+ }
+ catch (HttpRequestException)
+ {
+ throw; // already logged in flow above
+ }
catch (Exception e)
{
- throw new Exception($"Revoke Failed with message {e?.Message}");
+ flow.Fail("UNHANDLED", e.Message);
+ Logger.LogError(e, "Revoke: Exception for caRequestID='{CaRequestId}': {Message}", caRequestID, e.Message);
+ throw new Exception($"Revoke Failed for '{caRequestID}' with message {e.Message}", e);
}
}
@@ -209,128 +461,382 @@ await CscGlobalClient.SubmitRevokeCertificateAsync(caRequestID.Substring(0, 36))
public async Task Enroll(string csr, string subject, Dictionary san,
EnrollmentProductInfo productInfo, RequestFormat requestFormat, EnrollmentType enrollmentType)
{
+ using var flow = new FlowLogger(Logger, $"Enroll-{enrollmentType}");
Logger.MethodEntry(LogLevel.Debug);
+ Logger.LogTrace("Enroll called. enrollmentType={EnrollmentType}, subject='{Subject}', productId='{ProductId}', requestFormat={RequestFormat}",
+ enrollmentType, subject ?? "(null)",
+ productInfo?.ProductID ?? "(null)", requestFormat);
+ Logger.LogTrace("Enroll: csr is {CsrStatus}, san has {SanCount} entries, productInfo is {PiStatus}",
+ string.IsNullOrEmpty(csr) ? "empty/null" : $"present ({csr.Length} chars)",
+ san?.Count ?? 0,
+ productInfo == null ? "NULL" : "present");
+
+ flow.Step("ValidateInputs", () =>
+ {
+ if (productInfo == null)
+ throw new ArgumentNullException(nameof(productInfo), "productInfo cannot be null for Enroll.");
+ if (productInfo.ProductParameters == null)
+ throw new ArgumentNullException(nameof(productInfo), "productInfo.ProductParameters cannot be null for Enroll.");
+ if (string.IsNullOrEmpty(csr))
+ throw new ArgumentNullException(nameof(csr), "CSR cannot be null or empty for Enroll.");
+ });
+
+ Logger.LogTrace("Enroll: ProductParameters keys: [{Keys}]",
+ string.Join(", ", productInfo.ProductParameters.Keys));
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();
- switch (enrollmentType)
+ flow.Step("CheckPriorCertSN", () =>
{
- 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"))
+ if (productInfo.ProductParameters.ContainsKey("priorcertsn"))
+ {
+ 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)}");
+ priorSn = productInfo.ProductParameters["PriorCertSN"];
+ Logger.LogDebug("Enroll: Prior cert SN: '{PriorSn}'", priorSn ?? "(null)");
}
else
{
- return new EnrollmentResult
- {
- Status = 30, //failure
- StatusMessage = "You cannot renew an expired cert please perform an new enrollment."
- };
+ Logger.LogWarning("Enroll: 'priorcertsn' key exists but 'PriorCertSN' (case-sensitive) not found.");
}
+ }
+ }, string.IsNullOrEmpty(priorSn) ? "none" : $"SN={priorSn}");
- 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;
- }
+ string uUId;
+ List customFields = null;
+ await flow.StepAsync("FetchCustomFields", async () =>
+ {
+ customFields = await CscGlobalClient.SubmitGetCustomFields();
+ }, $"count={customFields?.Count ?? 0}");
- if (expirationDate < DateTime.Now) renewal = true;
- if (renewal)
- {
- //One click won't work for this implementation b/c we are missing enrollment params
+ if (customFields == null)
+ {
+ Logger.LogWarning("Enroll: SubmitGetCustomFields returned null, using empty list.");
+ customFields = new List();
+ }
+
+ try
+ {
+ switch (enrollmentType)
+ {
+ case EnrollmentType.New:
+ flow.Step("SelectPath", "New Enrollment");
+ IRegistrationResponse enrollmentResponse;
+ if (!productInfo.ProductParameters.ContainsKey("PriorCertSN"))
+ {
+ enrollmentRequest = null;
+ flow.Step("BuildRegistrationRequest", () =>
+ {
+ enrollmentRequest = _requestManager.GetRegistrationRequest(productInfo, csr, san, customFields);
+ });
+ Logger.LogTrace("Enrollment Request JSON: {Json}", JsonConvert.SerializeObject(enrollmentRequest));
+
+ RegistrationResponse regResponse = null;
+ await flow.StepAsync("SubmitRegistrationToCSC", async () =>
+ {
+ regResponse = await CscGlobalClient.SubmitRegistrationAsync(enrollmentRequest);
+ });
+ enrollmentResponse = regResponse;
+
+ if (enrollmentResponse == null)
+ {
+ flow.Fail("ParseResponse", "API returned null");
+ return new EnrollmentResult
+ {
+ Status = 30,
+ StatusMessage = "Enrollment failed: CSC API returned a null response."
+ };
+ }
+ flow.Step("ParseResponse", $"error={enrollmentResponse.RegistrationError != null}");
+ Logger.LogTrace("Enrollment Response JSON: {Json}", JsonConvert.SerializeObject(enrollmentResponse));
+ }
+ else
+ {
+ flow.Fail("RejectExpiredRenew", "PriorCertSN present on New enrollment");
+ return new EnrollmentResult
+ {
+ Status = 30,
+ StatusMessage = "You cannot renew an expired cert please perform an new enrollment."
+ };
+ }
+
+ var enrollResult = _requestManager.GetEnrollmentResult(enrollmentResponse);
+ flow.Step("MapResult", $"Status={enrollResult?.Status}, ID={enrollResult?.CARequestID ?? "(null)"}");
+ Logger.MethodExit(LogLevel.Debug);
+ return enrollResult;
+
+ case EnrollmentType.RenewOrReissue:
+ flow.Step("SelectPath", "RenewOrReissue");
+
+ if (string.IsNullOrEmpty(priorSn))
+ {
+ flow.Fail("ValidatePriorSN", "PriorCertSN is empty");
+ return new EnrollmentResult
+ {
+ Status = 30,
+ StatusMessage = "RenewOrReissue failed: PriorCertSN is required but was not provided."
+ };
+ }
+
+ string order_id = null;
+ await flow.StepAsync("LookupOrderId", async () =>
+ {
+ order_id = await _certificateDataReader.GetRequestIDBySerialNumber(priorSn);
+ }, $"orderId={order_id ?? "(null)"}");
+
+ if (string.IsNullOrEmpty(order_id))
+ {
+ flow.Fail("ValidateOrderId", $"no order found for SN={priorSn}");
+ return new EnrollmentResult
+ {
+ Status = 30,
+ StatusMessage = $"RenewOrReissue failed: could not find order ID for serial number '{priorSn}'."
+ };
+ }
+
+ if (order_id.Length < 36)
+ {
+ flow.Fail("ValidateOrderId", $"order_id too short ({order_id.Length} chars)");
+ return new EnrollmentResult
+ {
+ Status = 30,
+ StatusMessage = $"RenewOrReissue failed: order ID '{order_id}' is too short to extract a UUID."
+ };
+ }
+ flow.Step("ValidateOrderId", $"orderId={order_id}");
+
+ // Determine renew vs reissue based on order expiry window.
+ var renewal = false;
+ try
+ {
+ CertificateResponse liveCert = null;
+ await flow.StepAsync("FetchLiveCertForDecision", async () =>
+ {
+ liveCert = await CscGlobalClient.SubmitGetCertificateAsync(order_id[..36]);
+ });
+
+ if (liveCert != null && DateTime.TryParse(liveCert.OrderDate, out var orderDate))
+ {
+ var orderExpiry = orderDate.AddYears(1);
+ var daysUntilOrderExpiry = (orderExpiry - DateTime.UtcNow).TotalDays;
+ renewal = daysUntilOrderExpiry <= RenewalWindowDays;
+ flow.Step("ComputeRenewalDecision",
+ $"orderDate={liveCert.OrderDate}, expiry={orderExpiry:dd-MMM-yyyy}, daysLeft={(int)daysUntilOrderExpiry}, window={RenewalWindowDays}, isRenewal={renewal}");
+ }
+ else
+ {
+ flow.Skip("ComputeRenewalDecision", "orderDate unavailable, falling back to cert expiry");
+ var expirationDate = _certificateDataReader.GetExpirationDateByRequestId(order_id)
+ ?? (await GetSingleRecord(order_id)).RevocationDate;
+ renewal = expirationDate < DateTime.Now;
+ flow.Step("FallbackExpiryCheck", $"expirationDate={expirationDate?.ToString("o") ?? "(null)"}, isRenewal={renewal}");
+ }
+ }
+ catch (Exception ex)
+ {
+ flow.Fail("FetchLiveCertForDecision", $"falling back: {ex.Message}");
+ Logger.LogWarning(ex, "RenewOrReissue: failed to fetch live cert, falling back to cert expiry.");
+ try
+ {
+ var expirationDate = _certificateDataReader.GetExpirationDateByRequestId(order_id)
+ ?? (await GetSingleRecord(order_id)).RevocationDate;
+ renewal = expirationDate < DateTime.Now;
+ flow.Step("FallbackExpiryCheck", $"isRenewal={renewal}");
+ }
+ catch (Exception fallbackEx)
+ {
+ flow.Fail("FallbackExpiryCheck", fallbackEx.Message);
+ return new EnrollmentResult
+ {
+ Status = 30,
+ StatusMessage = $"RenewOrReissue failed: unable to determine renewal status for order '{order_id}'. {fallbackEx.Message}"
+ };
+ }
+ }
+
+ flow.Step("RenewalDecision", renewal ? "RENEWAL (paid order)" : "REISSUE (free under active order)");
+
+ if (renewal)
+ {
+ if (productInfo.ProductParameters.ContainsKey("Applicant Last Name"))
+ {
+ uUId = null;
+ await flow.StepAsync("LookupRenewalUUID", async () =>
+ {
+ uUId = await _certificateDataReader.GetRequestIDBySerialNumber(
+ productInfo.ProductParameters["PriorCertSN"]);
+ });
+
+ if (string.IsNullOrEmpty(uUId))
+ {
+ flow.Fail("ValidateRenewalUUID", "could not resolve PriorCertSN");
+ return new EnrollmentResult
+ {
+ Status = 30,
+ StatusMessage = "Renewal failed: could not resolve prior certificate serial number to a request ID."
+ };
+ }
+ flow.Step("ValidateRenewalUUID", $"uuid={uUId}");
+
+ RenewalRequest builtRenewRequest = null;
+ flow.Step("BuildRenewalRequest", () =>
+ {
+ builtRenewRequest = _requestManager.GetRenewalRequest(productInfo, uUId, csr, san, customFields);
+ });
+ renewRequest = builtRenewRequest;
+ Logger.LogTrace("Renewal Request JSON: {Json}", JsonConvert.SerializeObject(renewRequest));
+
+ RenewalResponse renewResponse = null;
+ await flow.StepAsync("SubmitRenewalToCSC", async () =>
+ {
+ renewResponse = await CscGlobalClient.SubmitRenewalAsync(renewRequest);
+ });
+
+ if (renewResponse == null)
+ {
+ flow.Fail("ParseRenewalResponse", "API returned null");
+ return new EnrollmentResult
+ {
+ Status = 30,
+ StatusMessage = "Renewal failed: CSC API returned a null response."
+ };
+ }
+
+ Logger.LogTrace("Renewal Response JSON: {Json}", JsonConvert.SerializeObject(renewResponse));
+ var renewResult = _requestManager.GetRenewResponse(renewResponse);
+ flow.Step("MapRenewalResult", $"Status={renewResult?.Status}, Message={renewResult?.StatusMessage ?? "(null)"}");
+ Logger.MethodExit(LogLevel.Debug);
+ return renewResult;
+ }
+
+ flow.Fail("MissingEnrollmentParams", "Applicant Last Name not present — one-click renew unavailable");
+ return new EnrollmentResult
+ {
+ Status = 30,
+ StatusMessage =
+ "One click Renew Is Not Available for this Certificate Type. Use the configure button instead."
+ };
+ }
+
+ // Reissue path
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 = Task.Run(async () => await CscGlobalClient.SubmitRenewalAsync(renewRequest))
- .Result;
- Logger.LogTrace($"Renewal Response JSON: {JsonConvert.SerializeObject(renewResponse)}");
+ string requestid = null;
+ await flow.StepAsync("LookupReissueRequestId", async () =>
+ {
+ requestid = await _certificateDataReader.GetRequestIDBySerialNumber(
+ productInfo.ProductParameters["PriorCertSN"]);
+ });
+
+ if (string.IsNullOrEmpty(requestid))
+ {
+ flow.Fail("ValidateReissueRequestId", "could not resolve PriorCertSN");
+ return new EnrollmentResult
+ {
+ Status = 30,
+ StatusMessage = "Reissue failed: could not resolve prior certificate serial number to a request ID."
+ };
+ }
+
+ if (requestid.Length < 36)
+ {
+ flow.Fail("ValidateReissueRequestId", $"requestid too short ({requestid.Length} chars)");
+ return new EnrollmentResult
+ {
+ Status = 30,
+ StatusMessage = $"Reissue failed: request ID '{requestid}' is too short to extract a UUID."
+ };
+ }
+
+ uUId = requestid.Substring(0, 36);
+ flow.Step("ExtractReissueUUID", $"uuid={uUId}");
+
+ ReissueRequest builtReissueRequest = null;
+ flow.Step("BuildReissueRequest", () =>
+ {
+ builtReissueRequest = _requestManager.GetReissueRequest(productInfo, uUId, csr, san, customFields);
+ });
+ reissueRequest = builtReissueRequest;
+ Logger.LogTrace("Reissue JSON: {Json}", JsonConvert.SerializeObject(reissueRequest));
+
+ ReissueResponse reissueResponse = null;
+ await flow.StepAsync("SubmitReissueToCSC", async () =>
+ {
+ reissueResponse = await CscGlobalClient.SubmitReissueAsync(reissueRequest);
+ });
+
+ if (reissueResponse == null)
+ {
+ flow.Fail("ParseReissueResponse", "API returned null");
+ return new EnrollmentResult
+ {
+ Status = 30,
+ StatusMessage = "Reissue failed: CSC API returned a null response."
+ };
+ }
+
+ Logger.LogTrace("Reissue Response JSON: {Json}", JsonConvert.SerializeObject(reissueResponse));
+ var reissueResult = _requestManager.GetReIssueResult(reissueResponse);
+ flow.Step("MapReissueResult", $"Status={reissueResult?.Status}, Message={reissueResult?.StatusMessage ?? "(null)"}");
Logger.MethodExit(LogLevel.Debug);
- return _requestManager.GetRenewResponse(renewResponse);
+ return reissueResult;
}
+ flow.Fail("MissingEnrollmentParams", "Applicant Last Name not present — one-click reissue unavailable");
return new EnrollmentResult
{
- Status = 30, //failure
+ Status = 30,
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."
- };
+ default:
+ flow.Fail("UnhandledType", $"enrollmentType={enrollmentType}");
+ return new EnrollmentResult
+ {
+ Status = 30,
+ StatusMessage = $"Enroll failed: unhandled enrollment type '{enrollmentType}'."
+ };
+ }
+ }
+ catch (AggregateException ae)
+ {
+ var inner = ae.Flatten().InnerException;
+ flow.Fail("UNHANDLED", inner?.Message ?? ae.Message);
+ Logger.LogError(inner, "Enroll: AggregateException during {EnrollmentType}: {Message}", enrollmentType, inner?.Message ?? ae.Message);
+ return new EnrollmentResult
+ {
+ Status = 30,
+ StatusMessage = $"Enrollment failed with error: {inner?.Message ?? ae.Message}"
+ };
+ }
+ catch (Exception ex)
+ {
+ flow.Fail("UNHANDLED", ex.Message);
+ Logger.LogError(ex, "Enroll: unhandled exception during {EnrollmentType}: {Message}", enrollmentType, ex.Message);
+ return new EnrollmentResult
+ {
+ Status = 30,
+ StatusMessage = $"Enrollment failed with error: {ex.Message}"
+ };
}
-
- Logger.MethodExit(LogLevel.Debug);
- return null;
}
//done
public async Task Ping()
{
Logger.MethodEntry();
+ Logger.LogTrace("Ping: CscGlobalClient is {Null}", CscGlobalClient == null ? "NULL" : "present");
try
{
Logger.LogInformation("Ping request received");
}
catch (Exception e)
{
- Logger.LogError($"There was an error contacting CSCGlobal: {e.Message}.");
+ Logger.LogError(e, "There was an error contacting CSCGlobal: {Message}", e.Message);
throw new Exception($"Error attempting to ping CSCGlobal: {e.Message}.", e);
}
@@ -340,19 +846,53 @@ public async Task Ping()
//do
public async Task ValidateCAConnectionInfo(Dictionary connectionInfo)
{
+ Logger.MethodEntry(LogLevel.Debug);
+ Logger.LogTrace("ValidateCAConnectionInfo called. connectionInfo is {Null}, keys=[{Keys}]",
+ connectionInfo == null ? "NULL" : "present",
+ connectionInfo != null ? string.Join(", ", connectionInfo.Keys) : "");
+
+ if (connectionInfo == null)
+ {
+ Logger.LogError("ValidateCAConnectionInfo: connectionInfo is null.");
+ throw new ArgumentNullException(nameof(connectionInfo), "connectionInfo cannot be null.");
+ }
+
+ Logger.MethodExit(LogLevel.Debug);
}
//do
public async Task ValidateProductInfo(EnrollmentProductInfo productInfo,
Dictionary connectionInfo)
{
+ Logger.MethodEntry(LogLevel.Debug);
+ Logger.LogTrace("ValidateProductInfo called. productInfo is {Null}, productId='{ProductId}'",
+ productInfo == null ? "NULL" : "present",
+ productInfo?.ProductID ?? "(null)");
+
+ if (productInfo == null)
+ {
+ Logger.LogError("ValidateProductInfo: productInfo is null.");
+ throw new ArgumentNullException(nameof(productInfo), "productInfo cannot be null.");
+ }
+
+ if (string.IsNullOrEmpty(productInfo.ProductID))
+ {
+ Logger.LogError("ValidateProductInfo: productInfo.ProductID is null or empty.");
+ throw new ArgumentException("ProductID cannot be null or empty.", nameof(productInfo));
+ }
+
var certType = ProductIDs.productIds.Find(x =>
x.Equals(productInfo.ProductID, StringComparison.InvariantCultureIgnoreCase));
- if (certType == null) throw new ArgumentException($"Cannot find {productInfo.ProductID}", "ProductId");
-
- Logger.LogInformation($"Validated {certType} ({certType})configured for AnyGateway");
+ if (certType == null)
+ {
+ Logger.LogError("ValidateProductInfo: cannot find product ID '{ProductId}'. Known IDs: [{KnownIds}]",
+ productInfo.ProductID, string.Join(", ", ProductIDs.productIds));
+ throw new ArgumentException($"Cannot find {productInfo.ProductID}", "ProductId");
+ }
+ Logger.LogInformation("Validated {CertType} configured for AnyGateway", certType);
+ Logger.MethodExit(LogLevel.Debug);
}
//done
@@ -388,19 +928,19 @@ 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.",
Hidden = false,
DefaultValue = "5",
Type = "Number"
+ },
+ [Constants.RenewalWindowDays] = new()
+ {
+ Comments = "Number of days before the annual order expiry within which a RenewOrReissue triggers a paid Renewal rather than a free Reissue. Default is 30.",
+ Hidden = false,
+ DefaultValue = "30",
+ Type = "Number"
}
};
}
diff --git a/cscglobal-caplugin/CSCGlobalCAPlugin.csproj b/cscglobal-caplugin/CSCGlobalCAPlugin.csproj
index 5118677..4d71ec5 100644
--- a/cscglobal-caplugin/CSCGlobalCAPlugin.csproj
+++ b/cscglobal-caplugin/CSCGlobalCAPlugin.csproj
@@ -3,7 +3,7 @@
true
- net6.0;net8.0
+ net6.0;net8.0;net10.0
Keyfactor.Extensions.CAPlugin.CSCGlobal
true
enable
@@ -23,6 +23,12 @@
+
+
+
+
+
+
diff --git a/cscglobal-caplugin/Client/CscGlobalClient.cs b/cscglobal-caplugin/Client/CscGlobalClient.cs
index 0a5c7c5..032f535 100644
--- a/cscglobal-caplugin/Client/CscGlobalClient.cs
+++ b/cscglobal-caplugin/Client/CscGlobalClient.cs
@@ -23,13 +23,58 @@ public sealed class CscGlobalClient : ICscGlobalClient
public CscGlobalClient(IAnyCAPluginConfigProvider config)
{
- Logger = LogHandler.GetClassLogger();
+ Logger = LogHandler.GetClassLogger();
+
+ if (config == null)
+ throw new ArgumentNullException(nameof(config), "config cannot be null in CscGlobalClient constructor.");
+
+ if (config.CAConnectionData == null)
+ throw new InvalidOperationException("CAConnectionData is null on config provider.");
+
+ Logger.LogTrace("CscGlobalClient: CAConnectionData keys=[{Keys}]", string.Join(", ", config.CAConnectionData.Keys));
+
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();
+ var rawUrl = config.CAConnectionData.ContainsKey(Constants.CscGlobalUrl)
+ ? config.CAConnectionData[Constants.CscGlobalUrl]?.ToString()
+ : null;
+ if (string.IsNullOrEmpty(rawUrl))
+ {
+ Logger.LogError("CscGlobalClient: CscGlobalUrl is missing or empty in CAConnectionData.");
+ throw new InvalidOperationException("CscGlobalUrl is required but was not configured.");
+ }
+
+ Logger.LogTrace("CscGlobalClient: BaseUrl='{BaseUrl}'", rawUrl);
+ BaseUrl = new Uri(rawUrl);
+
+ ApiKey = config.CAConnectionData[Constants.CscGlobalApiKey]?.ToString();
+ if (string.IsNullOrEmpty(ApiKey))
+ {
+ Logger.LogError("CscGlobalClient: ApiKey is empty or null.");
+ throw new InvalidOperationException("ApiKey is required but was not configured.");
+ }
+ Logger.LogTrace("CscGlobalClient: ApiKey is present (length={Length}).", ApiKey.Length);
+
+ if (!config.CAConnectionData.ContainsKey(Constants.BearerToken))
+ {
+ Logger.LogError("CscGlobalClient: BearerToken key not found in CAConnectionData.");
+ throw new InvalidOperationException("BearerToken is required but was not configured.");
+ }
+ Authorization = config.CAConnectionData[Constants.BearerToken]?.ToString();
+ if (string.IsNullOrEmpty(Authorization))
+ {
+ Logger.LogError("CscGlobalClient: BearerToken is empty or null.");
+ throw new InvalidOperationException("BearerToken is required but was empty.");
+ }
+ Logger.LogTrace("CscGlobalClient: BearerToken is present (length={Length}).", Authorization.Length);
+
RestClient = ConfigureRestClient();
+ Logger.LogTrace("CscGlobalClient: RestClient configured successfully.");
+ }
+ else
+ {
+ Logger.LogError("CscGlobalClient: ApiKey key '{Key}' not found in CAConnectionData. Client will not be functional.", Constants.CscGlobalApiKey);
+ throw new InvalidOperationException($"Required key '{Constants.CscGlobalApiKey}' not found in CAConnectionData.");
}
}
@@ -41,25 +86,42 @@ public CscGlobalClient(IAnyCAPluginConfigProvider config)
public async Task SubmitRegistrationAsync(
RegistrationRequest registerRequest)
{
+ Logger.LogTrace("SubmitRegistrationAsync: sending registration request...");
+ if (registerRequest == null)
+ throw new ArgumentNullException(nameof(registerRequest));
+
+ var requestJson = JsonConvert.SerializeObject(registerRequest);
+ Logger.LogTrace("SubmitRegistrationAsync: request JSON: {Json}", requestJson);
+
using (var resp = await RestClient.PostAsync("/dbs/api/v2/tls/registration", new StringContent(
- JsonConvert.SerializeObject(registerRequest), Encoding.ASCII, "application/json")))
+ requestJson, Encoding.ASCII, "application/json")))
{
- Logger.LogTrace(JsonConvert.SerializeObject(registerRequest));
+ var rawBody = await resp.Content.ReadAsStringAsync();
+ Logger.LogTrace("SubmitRegistrationAsync: HTTP {StatusCode}, body length={Length}", (int)resp.StatusCode, rawBody?.Length ?? 0);
+ Logger.LogTrace("SubmitRegistrationAsync: response body: {Body}", rawBody ?? "(null)");
+
var settings = new JsonSerializerSettings { NullValueHandling = NullValueHandling.Ignore };
- if (resp.StatusCode == HttpStatusCode.BadRequest) //Csc Sends Errors back in 400 Json Response
+ if (resp.StatusCode == HttpStatusCode.BadRequest)
{
- var errorResponse =
- JsonConvert.DeserializeObject(await resp.Content.ReadAsStringAsync(),
- settings);
+ Logger.LogWarning("SubmitRegistrationAsync: received 400 BadRequest.");
+ var errorResponse = JsonConvert.DeserializeObject(rawBody ?? "{}", settings);
+ Logger.LogTrace("SubmitRegistrationAsync: error description='{Desc}'", errorResponse?.Description ?? "(null)");
var response = new RegistrationResponse();
response.RegistrationError = errorResponse;
response.Result = null;
return response;
}
- var registrationResponse =
- JsonConvert.DeserializeObject(await resp.Content.ReadAsStringAsync(),
- settings);
+ if (!resp.IsSuccessStatusCode)
+ {
+ Logger.LogError("SubmitRegistrationAsync: unexpected HTTP {StatusCode}: {Body}", (int)resp.StatusCode, rawBody);
+ throw new HttpRequestException($"SubmitRegistrationAsync failed with HTTP {(int)resp.StatusCode}: {rawBody}");
+ }
+
+ var registrationResponse = JsonConvert.DeserializeObject(rawBody ?? "{}", settings);
+ Logger.LogTrace("SubmitRegistrationAsync: deserialized response. Result is {Null}, RegistrationError is {Null2}",
+ registrationResponse?.Result == null ? "null" : "present",
+ registrationResponse?.RegistrationError == null ? "null" : "present");
return registrationResponse;
}
}
@@ -67,31 +129,42 @@ public async Task SubmitRegistrationAsync(
public async Task SubmitRenewalAsync(
RenewalRequest renewalRequest)
{
+ Logger.LogTrace("SubmitRenewalAsync: sending renewal request...");
+ if (renewalRequest == null)
+ throw new ArgumentNullException(nameof(renewalRequest));
+
+ var requestJson = JsonConvert.SerializeObject(renewalRequest);
+ Logger.LogTrace("SubmitRenewalAsync: request JSON: {Json}", requestJson);
+
using (var resp = await RestClient.PostAsync("/dbs/api/v2/tls/renewal", new StringContent(
- JsonConvert.SerializeObject(renewalRequest), Encoding.ASCII, "application/json")))
+ requestJson, Encoding.ASCII, "application/json")))
{
- Logger.LogTrace(JsonConvert.SerializeObject(renewalRequest));
+ var rawBody = await resp.Content.ReadAsStringAsync();
+ Logger.LogTrace("SubmitRenewalAsync: HTTP {StatusCode}, body length={Length}", (int)resp.StatusCode, rawBody?.Length ?? 0);
+ Logger.LogTrace("SubmitRenewalAsync: response body: {Body}", rawBody ?? "(null)");
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();
- Logger.LogTrace("Logging Error Response Raw");
- Logger.LogTrace(rawErrorResponse);
- var errorResponse =
- JsonConvert.DeserializeObject(rawErrorResponse,
- settings);
+ if (resp.StatusCode == HttpStatusCode.BadRequest)
+ {
+ Logger.LogWarning("SubmitRenewalAsync: received 400 BadRequest.");
+ var errorResponse = JsonConvert.DeserializeObject(rawBody ?? "{}", settings);
+ Logger.LogTrace("SubmitRenewalAsync: error description='{Desc}'", errorResponse?.Description ?? "(null)");
var response = new RenewalResponse();
response.RegistrationError = errorResponse;
response.Result = null;
return response;
}
- var rawRenewResponse = await resp.Content.ReadAsStringAsync();
- Logger.LogTrace("Logging Success Response Raw");
- Logger.LogTrace(rawRenewResponse);
- var renewalResponse =
- JsonConvert.DeserializeObject(rawRenewResponse);
+ if (!resp.IsSuccessStatusCode)
+ {
+ Logger.LogError("SubmitRenewalAsync: unexpected HTTP {StatusCode}: {Body}", (int)resp.StatusCode, rawBody);
+ throw new HttpRequestException($"SubmitRenewalAsync failed with HTTP {(int)resp.StatusCode}: {rawBody}");
+ }
+
+ var renewalResponse = JsonConvert.DeserializeObject(rawBody ?? "{}");
+ Logger.LogTrace("SubmitRenewalAsync: deserialized response. Result is {Null}, RegistrationError is {Null2}",
+ renewalResponse?.Result == null ? "null" : "present",
+ renewalResponse?.RegistrationError == null ? "null" : "present");
return renewalResponse;
}
}
@@ -99,69 +172,145 @@ public async Task SubmitRenewalAsync(
public async Task SubmitReissueAsync(
ReissueRequest reissueRequest)
{
+ Logger.LogTrace("SubmitReissueAsync: sending reissue request...");
+ if (reissueRequest == null)
+ throw new ArgumentNullException(nameof(reissueRequest));
+
+ var requestJson = JsonConvert.SerializeObject(reissueRequest);
+ Logger.LogTrace("SubmitReissueAsync: request JSON: {Json}", requestJson);
+
using (var resp = await RestClient.PostAsync("/dbs/api/v2/tls/reissue", new StringContent(
- JsonConvert.SerializeObject(reissueRequest), Encoding.ASCII, "application/json")))
+ requestJson, Encoding.ASCII, "application/json")))
{
- Logger.LogTrace(JsonConvert.SerializeObject(reissueRequest));
+ var rawBody = await resp.Content.ReadAsStringAsync();
+ Logger.LogTrace("SubmitReissueAsync: HTTP {StatusCode}, body length={Length}", (int)resp.StatusCode, rawBody?.Length ?? 0);
+ Logger.LogTrace("SubmitReissueAsync: response body: {Body}", rawBody ?? "(null)");
var settings = new JsonSerializerSettings { NullValueHandling = NullValueHandling.Ignore };
- if (resp.StatusCode == HttpStatusCode.BadRequest) //Csc Sends Errors back in 400 Json Response
+ if (resp.StatusCode == HttpStatusCode.BadRequest)
{
- var errorResponse =
- JsonConvert.DeserializeObject(await resp.Content.ReadAsStringAsync(),
- settings);
+ Logger.LogWarning("SubmitReissueAsync: received 400 BadRequest.");
+ var errorResponse = JsonConvert.DeserializeObject(rawBody ?? "{}", settings);
+ Logger.LogTrace("SubmitReissueAsync: error description='{Desc}'", errorResponse?.Description ?? "(null)");
var response = new ReissueResponse();
response.RegistrationError = errorResponse;
response.Result = null;
return response;
}
- var reissueResponse =
- JsonConvert.DeserializeObject(await resp.Content.ReadAsStringAsync());
+ if (!resp.IsSuccessStatusCode)
+ {
+ Logger.LogError("SubmitReissueAsync: unexpected HTTP {StatusCode}: {Body}", (int)resp.StatusCode, rawBody);
+ throw new HttpRequestException($"SubmitReissueAsync failed with HTTP {(int)resp.StatusCode}: {rawBody}");
+ }
+
+ var reissueResponse = JsonConvert.DeserializeObject(rawBody ?? "{}");
+ Logger.LogTrace("SubmitReissueAsync: deserialized response. Result is {Null}, RegistrationError is {Null2}",
+ reissueResponse?.Result == null ? "null" : "present",
+ reissueResponse?.RegistrationError == null ? "null" : "present");
return reissueResponse;
}
}
public async Task SubmitGetCertificateAsync(string certificateId)
{
+ Logger.LogTrace("SubmitGetCertificateAsync: fetching certificate for id='{CertificateId}'", certificateId ?? "(null)");
+
+ if (string.IsNullOrEmpty(certificateId))
+ throw new ArgumentNullException(nameof(certificateId), "certificateId cannot be null or empty.");
+
using (var resp = await RestClient.GetAsync($"/dbs/api/v2/tls/certificate/{certificateId}"))
{
- resp.EnsureSuccessStatusCode();
- var getCertificateResponse =
- JsonConvert.DeserializeObject(await resp.Content.ReadAsStringAsync());
+ var rawBody = await resp.Content.ReadAsStringAsync();
+ Logger.LogTrace("SubmitGetCertificateAsync: HTTP {StatusCode}, body length={Length}", (int)resp.StatusCode, rawBody?.Length ?? 0);
+
+ if (!resp.IsSuccessStatusCode)
+ {
+ Logger.LogError("SubmitGetCertificateAsync: HTTP {StatusCode} for certificateId='{CertificateId}': {Body}",
+ (int)resp.StatusCode, certificateId, rawBody);
+ resp.EnsureSuccessStatusCode(); // will throw
+ }
+
+ Logger.LogTrace("SubmitGetCertificateAsync: response body: {Body}", rawBody ?? "(null)");
+ var getCertificateResponse = JsonConvert.DeserializeObject(rawBody ?? "{}");
+ Logger.LogTrace("SubmitGetCertificateAsync: deserialized. Status='{Status}', OrderDate='{OrderDate}', Certificate is {Null}",
+ getCertificateResponse?.Status ?? "(null)",
+ getCertificateResponse?.OrderDate ?? "(null)",
+ string.IsNullOrEmpty(getCertificateResponse?.Certificate) ? "empty/null" : "present");
return getCertificateResponse;
}
}
public async Task> SubmitGetCustomFields()
{
+ Logger.LogTrace("SubmitGetCustomFields: fetching custom fields...");
+
using (var resp = await RestClient.GetAsync("/dbs/api/v2/admin/customfields"))
{
- resp.EnsureSuccessStatusCode();
- var getCustomFieldsResponse =
- JsonConvert.DeserializeObject(await resp.Content.ReadAsStringAsync());
+ var rawBody = await resp.Content.ReadAsStringAsync();
+ Logger.LogTrace("SubmitGetCustomFields: HTTP {StatusCode}, body length={Length}", (int)resp.StatusCode, rawBody?.Length ?? 0);
+
+ if (!resp.IsSuccessStatusCode)
+ {
+ Logger.LogError("SubmitGetCustomFields: HTTP {StatusCode}: {Body}", (int)resp.StatusCode, rawBody);
+ resp.EnsureSuccessStatusCode(); // will throw
+ }
+
+ Logger.LogTrace("SubmitGetCustomFields: response body: {Body}", rawBody ?? "(null)");
+ var getCustomFieldsResponse = JsonConvert.DeserializeObject(rawBody ?? "{}");
+
+ if (getCustomFieldsResponse == null)
+ {
+ Logger.LogWarning("SubmitGetCustomFields: deserialized response is null, returning empty list.");
+ return new List();
+ }
+
+ if (getCustomFieldsResponse.CustomFields == null)
+ {
+ Logger.LogWarning("SubmitGetCustomFields: CustomFields property is null, returning empty list.");
+ return new List();
+ }
+
+ Logger.LogTrace("SubmitGetCustomFields: received {Count} custom fields.", getCustomFieldsResponse.CustomFields.Count);
return getCustomFieldsResponse.CustomFields;
}
}
public async Task SubmitRevokeCertificateAsync(string uuId)
{
+ Logger.LogTrace("SubmitRevokeCertificateAsync: revoking certificate UUID='{Uuid}'", uuId ?? "(null)");
+
+ if (string.IsNullOrEmpty(uuId))
+ throw new ArgumentNullException(nameof(uuId), "uuId cannot be null or empty.");
+
using (var resp = await RestClient.PutAsync($"/dbs/api/v2/tls/revoke/{uuId}", new StringContent("")))
{
+ var rawBody = await resp.Content.ReadAsStringAsync();
+ Logger.LogTrace("SubmitRevokeCertificateAsync: HTTP {StatusCode}, body length={Length}", (int)resp.StatusCode, rawBody?.Length ?? 0);
+ Logger.LogTrace("SubmitRevokeCertificateAsync: response body: {Body}", rawBody ?? "(null)");
+
var settings = new JsonSerializerSettings { NullValueHandling = NullValueHandling.Ignore };
- if (resp.StatusCode == HttpStatusCode.BadRequest) //Csc Sends Errors back in 400 Json Response
+ if (resp.StatusCode == HttpStatusCode.BadRequest)
{
- var errorResponse =
- JsonConvert.DeserializeObject(await resp.Content.ReadAsStringAsync(),
- settings);
+ Logger.LogWarning("SubmitRevokeCertificateAsync: received 400 BadRequest for UUID='{Uuid}'.", uuId);
+ var errorResponse = JsonConvert.DeserializeObject(rawBody ?? "{}", settings);
+ Logger.LogTrace("SubmitRevokeCertificateAsync: error description='{Desc}'", errorResponse?.Description ?? "(null)");
var response = new RevokeResponse();
response.RegistrationError = errorResponse;
response.RevokeSuccess = null;
return response;
}
- var getRevokeResponse =
- JsonConvert.DeserializeObject(await resp.Content.ReadAsStringAsync());
+ if (!resp.IsSuccessStatusCode)
+ {
+ Logger.LogError("SubmitRevokeCertificateAsync: unexpected HTTP {StatusCode} for UUID='{Uuid}': {Body}", (int)resp.StatusCode, uuId, rawBody);
+ throw new HttpRequestException($"SubmitRevokeCertificateAsync failed with HTTP {(int)resp.StatusCode}: {rawBody}");
+ }
+
+ var getRevokeResponse = JsonConvert.DeserializeObject(rawBody ?? "{}");
+ Logger.LogTrace("SubmitRevokeCertificateAsync: deserialized. RevokeSuccess is {Null}, RegistrationError is {Null2}",
+ getRevokeResponse?.RevokeSuccess == null ? "null" : "present",
+ getRevokeResponse?.RegistrationError == null ? "null" : "present");
return getRevokeResponse;
}
}
@@ -169,23 +318,37 @@ public async Task SubmitRevokeCertificateAsync(string uuId)
public async Task SubmitCertificateListRequestAsync(string? dateFilter = null)
{
Logger.MethodEntry(LogLevel.Debug);
+ Logger.LogTrace("SubmitCertificateListRequestAsync: dateFilter='{DateFilter}'", dateFilter ?? "(null)");
+
var filterQuery = "filter=status=in=(ACTIVE,REVOKED)";
if (!string.IsNullOrEmpty(dateFilter))
{
filterQuery += $";effectiveDate=ge={dateFilter}";
}
- Logger.LogTrace($"Certificate list filter query: {filterQuery}");
+ Logger.LogTrace("SubmitCertificateListRequestAsync: filter query: {FilterQuery}", filterQuery);
+
var resp = RestClient.GetAsync($"/dbs/api/v2/tls/certificate?{filterQuery}").Result;
+ var rawBody = await resp.Content.ReadAsStringAsync();
+ Logger.LogTrace("SubmitCertificateListRequestAsync: HTTP {StatusCode}, body length={Length}", (int)resp.StatusCode, rawBody?.Length ?? 0);
if (!resp.IsSuccessStatusCode)
{
- var responseMessage = resp.Content.ReadAsStringAsync().Result;
Logger.LogError(
- $"Failed Request to Keyfactor. Retrying request. Status Code {resp.StatusCode} | Message: {responseMessage}");
+ "SubmitCertificateListRequestAsync: failed request. StatusCode={StatusCode}, Body={Body}",
+ (int)resp.StatusCode, rawBody);
+ }
+
+ var certificateListResponse = JsonConvert.DeserializeObject(rawBody ?? "{}");
+
+ if (certificateListResponse == null)
+ {
+ Logger.LogWarning("SubmitCertificateListRequestAsync: deserialized response is null.");
+ return new CertificateListResponse();
}
- var certificateListResponse =
- JsonConvert.DeserializeObject(await resp.Content.ReadAsStringAsync());
+ Logger.LogTrace("SubmitCertificateListRequestAsync: Results count={Count}",
+ certificateListResponse.Results?.Count ?? 0);
+ Logger.MethodExit(LogLevel.Debug);
return certificateListResponse;
}
diff --git a/cscglobal-caplugin/Constants.cs b/cscglobal-caplugin/Constants.cs
index 4d6b4da..d588706 100644
--- a/cscglobal-caplugin/Constants.cs
+++ b/cscglobal-caplugin/Constants.cs
@@ -13,8 +13,8 @@ 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";
+ public static string RenewalWindowDays = "RenewalWindowDays";
}
public class ProductIDs
@@ -26,8 +26,8 @@ public class ProductIDs
"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 Trusted Secure Domain Validated Wildcard SSL",
+ "CSC Trusted Secure Domain Validated UC Certificate"
};
}
diff --git a/cscglobal-caplugin/FlowLogger.cs b/cscglobal-caplugin/FlowLogger.cs
new file mode 100644
index 0000000..5696fcd
--- /dev/null
+++ b/cscglobal-caplugin/FlowLogger.cs
@@ -0,0 +1,241 @@
+// 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;
+
+public enum FlowStepStatus
+{
+ Success,
+ Failed,
+ Skipped,
+ InProgress
+}
+
+public class FlowStep
+{
+ public string Name { get; set; }
+ public FlowStepStatus Status { get; set; }
+ public string Detail { get; set; }
+ public long ElapsedMs { get; set; }
+ public List Children { get; } = new();
+}
+
+///
+/// Tracks high-level operation flow and renders a visual step diagram to Trace logs.
+/// Usage:
+/// using var flow = new FlowLogger(logger, "Enroll-New");
+/// flow.Step("ParseCSR");
+/// flow.Step("ValidateCSR", () => { ... });
+/// flow.Fail("CreateOrder", "API returned 400");
+/// // flow renders automatically on Dispose
+///
+public sealed class FlowLogger : IDisposable
+{
+ private readonly ILogger _logger;
+ private readonly string _flowName;
+ private readonly Stopwatch _totalTimer;
+ private readonly List _steps = new();
+ private FlowStep _currentParent;
+ private bool _disposed;
+
+ public FlowLogger(ILogger logger, string flowName)
+ {
+ _logger = logger;
+ _flowName = flowName;
+ _totalTimer = Stopwatch.StartNew();
+ _logger.LogTrace("===== FLOW START: {FlowName} =====", _flowName);
+ }
+
+ /// Record a completed step.
+ public FlowLogger Step(string name, string detail = null)
+ {
+ var step = new FlowStep { Name = name, Status = FlowStepStatus.Success, Detail = detail };
+ AddStep(step);
+ _logger.LogTrace(" [{FlowName}] {StepName} ... OK{Detail}",
+ _flowName, name, detail != null ? $" ({detail})" : "");
+ return this;
+ }
+
+ /// Record a step that executes an action and times it.
+ public FlowLogger Step(string name, Action action, string detail = null)
+ {
+ var sw = Stopwatch.StartNew();
+ var step = new FlowStep { Name = name, Detail = detail };
+ try
+ {
+ _logger.LogTrace(" [{FlowName}] {StepName} ...", _flowName, name);
+ action();
+ sw.Stop();
+ step.Status = FlowStepStatus.Success;
+ step.ElapsedMs = sw.ElapsedMilliseconds;
+ AddStep(step);
+ _logger.LogTrace(" [{FlowName}] {StepName} ... OK ({Elapsed}ms){Detail}",
+ _flowName, name, sw.ElapsedMilliseconds, detail != null ? $" {detail}" : "");
+ }
+ catch (Exception ex)
+ {
+ sw.Stop();
+ step.Status = FlowStepStatus.Failed;
+ step.ElapsedMs = sw.ElapsedMilliseconds;
+ step.Detail = ex.Message;
+ AddStep(step);
+ _logger.LogTrace(" [{FlowName}] {StepName} ... FAILED ({Elapsed}ms): {Error}",
+ _flowName, name, sw.ElapsedMilliseconds, ex.Message);
+ throw;
+ }
+ return this;
+ }
+
+ /// Record an async step that executes and times it.
+ public async Task StepAsync(string name, Func action, string detail = null)
+ {
+ var sw = Stopwatch.StartNew();
+ var step = new FlowStep { Name = name, Detail = detail };
+ try
+ {
+ _logger.LogTrace(" [{FlowName}] {StepName} ...", _flowName, name);
+ await action();
+ sw.Stop();
+ step.Status = FlowStepStatus.Success;
+ step.ElapsedMs = sw.ElapsedMilliseconds;
+ AddStep(step);
+ _logger.LogTrace(" [{FlowName}] {StepName} ... OK ({Elapsed}ms){Detail}",
+ _flowName, name, sw.ElapsedMilliseconds, detail != null ? $" {detail}" : "");
+ }
+ catch (Exception ex)
+ {
+ sw.Stop();
+ step.Status = FlowStepStatus.Failed;
+ step.ElapsedMs = sw.ElapsedMilliseconds;
+ step.Detail = ex.Message;
+ AddStep(step);
+ _logger.LogTrace(" [{FlowName}] {StepName} ... FAILED ({Elapsed}ms): {Error}",
+ _flowName, name, sw.ElapsedMilliseconds, ex.Message);
+ throw;
+ }
+ return this;
+ }
+
+ /// Record a failed step without throwing.
+ public FlowLogger Fail(string name, string reason = null)
+ {
+ var step = new FlowStep { Name = name, Status = FlowStepStatus.Failed, Detail = reason };
+ AddStep(step);
+ _logger.LogTrace(" [{FlowName}] {StepName} ... FAILED{Reason}",
+ _flowName, name, reason != null ? $": {reason}" : "");
+ return this;
+ }
+
+ /// Record a skipped step.
+ public FlowLogger Skip(string name, string reason = null)
+ {
+ var step = new FlowStep { Name = name, Status = FlowStepStatus.Skipped, Detail = reason };
+ AddStep(step);
+ _logger.LogTrace(" [{FlowName}] {StepName} ... SKIPPED{Reason}",
+ _flowName, name, reason != null ? $": {reason}" : "");
+ return this;
+ }
+
+ /// Start a branch (group of child steps).
+ public FlowLogger Branch(string name)
+ {
+ var step = new FlowStep { Name = name, Status = FlowStepStatus.InProgress };
+ AddStep(step);
+ _currentParent = step;
+ _logger.LogTrace(" [{FlowName}] >> Branch: {BranchName}", _flowName, name);
+ return this;
+ }
+
+ /// End the current branch.
+ public FlowLogger EndBranch()
+ {
+ _currentParent = null;
+ return this;
+ }
+
+ private void AddStep(FlowStep step)
+ {
+ if (_currentParent != null)
+ _currentParent.Children.Add(step);
+ else
+ _steps.Add(step);
+ }
+
+ /// Render the visual flow diagram to Trace log.
+ private string RenderFlow()
+ {
+ var sb = new StringBuilder();
+ sb.AppendLine();
+ sb.AppendLine($" ===== FLOW: {_flowName} ({_totalTimer.ElapsedMilliseconds}ms total) =====");
+ sb.AppendLine();
+
+ for (var i = 0; i < _steps.Count; i++)
+ {
+ var step = _steps[i];
+ var icon = GetStatusIcon(step.Status);
+ var elapsed = step.ElapsedMs > 0 ? $" ({step.ElapsedMs}ms)" : "";
+ var detail = !string.IsNullOrEmpty(step.Detail) ? $" [{step.Detail}]" : "";
+
+ sb.AppendLine($" {icon} {step.Name}{elapsed}{detail}");
+
+ // Render children (branch)
+ if (step.Children.Count > 0)
+ {
+ for (var j = 0; j < step.Children.Count; j++)
+ {
+ var child = step.Children[j];
+ var childIcon = GetStatusIcon(child.Status);
+ var childElapsed = child.ElapsedMs > 0 ? $" ({child.ElapsedMs}ms)" : "";
+ var childDetail = !string.IsNullOrEmpty(child.Detail) ? $" [{child.Detail}]" : "";
+ var connector = j < step.Children.Count - 1 ? "| " : " ";
+ sb.AppendLine($" |");
+ sb.AppendLine($" +-- {childIcon} {child.Name}{childElapsed}{childDetail}");
+ }
+ }
+
+ // Connector between top-level steps
+ if (i < _steps.Count - 1)
+ {
+ sb.AppendLine(" |");
+ sb.AppendLine(" v");
+ }
+ }
+
+ sb.AppendLine();
+
+ // Final status line
+ var finalStatus = _steps.Count > 0 && _steps.Last().Status == FlowStepStatus.Failed
+ ? "FAILED" : _steps.Any(s => s.Status == FlowStepStatus.Failed) ? "PARTIAL FAILURE" : "SUCCESS";
+ sb.AppendLine($" ===== FLOW RESULT: {finalStatus} =====");
+
+ return sb.ToString();
+ }
+
+ private static string GetStatusIcon(FlowStepStatus status)
+ {
+ return status switch
+ {
+ FlowStepStatus.Success => "[OK]",
+ FlowStepStatus.Failed => "[FAIL]",
+ FlowStepStatus.Skipped => "[SKIP]",
+ FlowStepStatus.InProgress => "[...]",
+ _ => "[?]"
+ };
+ }
+
+ public void Dispose()
+ {
+ if (_disposed) return;
+ _disposed = true;
+ _totalTimer.Stop();
+ _logger.LogTrace(RenderFlow());
+ }
+}
diff --git a/cscglobal-caplugin/RequestManager.cs b/cscglobal-caplugin/RequestManager.cs
index aa19b1e..00e4de1 100644
--- a/cscglobal-caplugin/RequestManager.cs
+++ b/cscglobal-caplugin/RequestManager.cs
@@ -10,19 +10,48 @@
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));
private List GetCustomFields(EnrollmentProductInfo productInfo, List customFields)
{
+ Logger.LogTrace("GetCustomFields: productInfo is {Null}, customFields count={Count}",
+ productInfo == null ? "NULL" : "present",
+ customFields?.Count ?? 0);
+
var customFieldList = new List();
+ if (customFields == null || productInfo?.ProductParameters == null)
+ {
+ Logger.LogTrace("GetCustomFields: returning empty list (null customFields or ProductParameters).");
+ return customFieldList;
+ }
+
foreach (var field in customFields)
+ {
+ if (field == null)
+ {
+ Logger.LogTrace("GetCustomFields: skipping null field entry.");
+ continue;
+ }
+
+ Logger.LogTrace("GetCustomFields: checking field Label='{Label}', Mandatory={Mandatory}",
+ field.Label ?? "(null)", field.Mandatory);
+
+ if (string.IsNullOrEmpty(field.Label))
+ {
+ Logger.LogTrace("GetCustomFields: skipping field with null/empty label.");
+ continue;
+ }
+
if (productInfo.ProductParameters.ContainsKey(field.Label))
{
var newField = new CustomField
@@ -30,32 +59,60 @@ private List GetCustomFields(EnrollmentProductInfo productInfo, Lis
Name = field.Label,
Value = productInfo.ProductParameters[field.Label]
};
+ Logger.LogTrace("GetCustomFields: matched field '{Label}' = '{Value}'", field.Label, newField.Value ?? "(null)");
customFieldList.Add(newField);
}
else if (field.Mandatory)
{
+ Logger.LogError("GetCustomFields: mandatory field '{Label}' was not supplied. Available keys: [{Keys}]",
+ field.Label, string.Join(", ", productInfo.ProductParameters.Keys));
throw new Exception(
$"Custom field {field.Label} is marked as mandatory, but was not supplied in the request.");
}
+ else
+ {
+ Logger.LogTrace("GetCustomFields: optional field '{Label}' not found in ProductParameters, skipping.", field.Label);
+ }
+ }
+ Logger.LogTrace("GetCustomFields: returning {Count} custom fields.", customFieldList.Count);
return customFieldList;
}
public EnrollmentResult GetRenewResponse(RenewalResponse renewResponse)
{
+ Logger.LogTrace("GetRenewResponse: renewResponse is {Null}", renewResponse == null ? "NULL" : "present");
+
+ if (renewResponse == null)
+ {
+ Logger.LogError("GetRenewResponse: renewResponse is null.");
+ return new EnrollmentResult
+ {
+ Status = (int)EndEntityStatus.FAILED,
+ StatusMessage = "Renewal failed: received null response from CSC."
+ };
+ }
+
if (renewResponse.RegistrationError != null)
+ {
+ Logger.LogWarning("GetRenewResponse: RegistrationError present. Description='{Desc}'",
+ renewResponse.RegistrationError.Description ?? "(null)");
return new EnrollmentResult
{
- Status = (int)EndEntityStatus.FAILED, //failure
- CARequestID = renewResponse?.Result?.Status?.Uuid,
- StatusMessage = renewResponse.RegistrationError.Description
+ Status = (int)EndEntityStatus.FAILED,
+ CARequestID = renewResponse.Result?.Status?.Uuid,
+ StatusMessage = renewResponse.RegistrationError.Description ?? "Renewal failed with unknown error."
};
+ }
+ var commonName = renewResponse.Result?.CommonName ?? "(unknown)";
+ var uuid = renewResponse.Result?.Status?.Uuid;
+ Logger.LogTrace("GetRenewResponse: renewal succeeded for CommonName='{CommonName}', UUID='{Uuid}'", commonName, uuid ?? "(null)");
return new EnrollmentResult
{
- Status = (int)EndEntityStatus.GENERATED, //success
-
- StatusMessage = $"Renewal Successfully Completed For {renewResponse.Result.CommonName}"
+ Status = (int)EndEntityStatus.EXTERNALVALIDATION,
+ CARequestID = uuid,
+ StatusMessage = $"Renewal Successfully Submitted For {commonName}. Certificate will be available after next sync."
};
}
@@ -64,77 +121,210 @@ public EnrollmentResult
GetEnrollmentResult(
IRegistrationResponse registrationResponse)
{
+ Logger.LogTrace("GetEnrollmentResult: registrationResponse is {Null}", registrationResponse == null ? "NULL" : "present");
+
+ if (registrationResponse == null)
+ {
+ Logger.LogError("GetEnrollmentResult: registrationResponse is null.");
+ return new EnrollmentResult
+ {
+ Status = (int)EndEntityStatus.FAILED,
+ StatusMessage = "Enrollment failed: received null response from CSC."
+ };
+ }
+
if (registrationResponse.RegistrationError != null)
+ {
+ Logger.LogWarning("GetEnrollmentResult: RegistrationError present. Description='{Desc}'",
+ registrationResponse.RegistrationError.Description ?? "(null)");
+ return new EnrollmentResult
+ {
+ Status = (int)EndEntityStatus.FAILED,
+ StatusMessage = registrationResponse.RegistrationError.Description ?? "Enrollment failed with unknown error."
+ };
+ }
+
+ if (registrationResponse.Result == null)
+ {
+ Logger.LogError("GetEnrollmentResult: Result is null but no RegistrationError present.");
return new EnrollmentResult
{
- Status = (int)EndEntityStatus.FAILED, //failure
- StatusMessage = registrationResponse.RegistrationError.Description
+ Status = (int)EndEntityStatus.FAILED,
+ StatusMessage = "Enrollment failed: response Result is null."
};
+ }
var cnames = new Dictionary();
if (registrationResponse.Result.DcvDetails != null && registrationResponse.Result.DcvDetails.Count > 0)
+ {
+ Logger.LogTrace("GetEnrollmentResult: processing {Count} DcvDetails.", registrationResponse.Result.DcvDetails.Count);
foreach (var dcv in registrationResponse.Result.DcvDetails)
{
+ if (dcv == null)
+ {
+ Logger.LogTrace("GetEnrollmentResult: skipping null DcvDetail.");
+ continue;
+ }
+
if (dcv.CName != null && !string.IsNullOrEmpty(dcv.CName.Name) && !string.IsNullOrEmpty(dcv.CName.Value))
{
- cnames.Add(dcv.CName.Name, dcv.CName.Value);
+ if (!cnames.ContainsKey(dcv.CName.Name))
+ {
+ Logger.LogTrace("GetEnrollmentResult: adding CName '{Name}'='{Value}'", dcv.CName.Name, dcv.CName.Value);
+ cnames.Add(dcv.CName.Name, dcv.CName.Value);
+ }
+ else
+ {
+ Logger.LogTrace("GetEnrollmentResult: duplicate CName key '{Name}', skipping.", dcv.CName.Name);
+ }
}
- if (string.IsNullOrEmpty(dcv.Email))
+ if (!string.IsNullOrEmpty(dcv.Email))
{
- cnames.Add(dcv.Email, dcv.Email);
+ if (!cnames.ContainsKey(dcv.Email))
+ {
+ Logger.LogTrace("GetEnrollmentResult: adding DCV email '{Email}'", dcv.Email);
+ cnames.Add(dcv.Email, dcv.Email);
+ }
+ else
+ {
+ Logger.LogTrace("GetEnrollmentResult: duplicate email key '{Email}', skipping.", dcv.Email);
+ }
}
}
-
+ }
+ else
+ {
+ Logger.LogTrace("GetEnrollmentResult: no DcvDetails to process.");
+ }
+
+ var uuid = registrationResponse.Result.Status?.Uuid;
+ var commonName = registrationResponse.Result.CommonName ?? "(unknown)";
+ Logger.LogTrace("GetEnrollmentResult: success. UUID='{Uuid}', CommonName='{CommonName}', cnames count={Count}",
+ uuid ?? "(null)", commonName, cnames.Count);
+
return new EnrollmentResult
{
- Status = (int)EndEntityStatus.EXTERNALVALIDATION, //success
- CARequestID = registrationResponse.Result.Status.Uuid,
+ Status = (int)EndEntityStatus.EXTERNALVALIDATION,
+ CARequestID = uuid,
StatusMessage =
- $"Order Successfully Created With Order Number {registrationResponse.Result.CommonName}",
+ $"Order Successfully Created With Order Number {commonName}",
EnrollmentContext = cnames.Count > 0 ? cnames : null
};
}
public int GetRevokeResult(IRevokeResponse revokeResponse)
{
+ Logger.LogTrace("GetRevokeResult: revokeResponse is {Null}", revokeResponse == null ? "NULL" : "present");
+
+ if (revokeResponse == null)
+ {
+ Logger.LogError("GetRevokeResult: revokeResponse is null, returning FAILED.");
+ return (int)EndEntityStatus.FAILED;
+ }
+
if (revokeResponse.RegistrationError != null)
+ {
+ Logger.LogWarning("GetRevokeResult: RegistrationError present. Description='{Desc}'",
+ revokeResponse.RegistrationError.Description ?? "(null)");
return (int)EndEntityStatus.FAILED;
+ }
+ Logger.LogTrace("GetRevokeResult: returning REVOKED.");
return (int)EndEntityStatus.REVOKED;
}
public EnrollmentResult GetReIssueResult(IReissueResponse reissueResponse)
{
+ Logger.LogTrace("GetReIssueResult: reissueResponse is {Null}", reissueResponse == null ? "NULL" : "present");
+
+ if (reissueResponse == null)
+ {
+ Logger.LogError("GetReIssueResult: reissueResponse is null.");
+ return new EnrollmentResult
+ {
+ Status = (int)EndEntityStatus.FAILED,
+ StatusMessage = "Reissue failed: received null response from CSC."
+ };
+ }
+
if (reissueResponse.RegistrationError != null)
+ {
+ Logger.LogWarning("GetReIssueResult: RegistrationError present. Description='{Desc}'",
+ reissueResponse.RegistrationError.Description ?? "(null)");
+ return new EnrollmentResult
+ {
+ Status = (int)EndEntityStatus.FAILED,
+ StatusMessage = reissueResponse.RegistrationError.Description ?? "Reissue failed with unknown error."
+ };
+ }
+
+ if (reissueResponse.Result == null)
+ {
+ Logger.LogError("GetReIssueResult: Result is null but no RegistrationError present.");
return new EnrollmentResult
{
- Status = (int)EndEntityStatus.FAILED, //failure
- StatusMessage = reissueResponse.RegistrationError.Description
+ Status = (int)EndEntityStatus.FAILED,
+ StatusMessage = "Reissue failed: response Result is null."
};
+ }
+
+ var uuid = reissueResponse.Result.Status?.Uuid;
+ var commonName = reissueResponse.Result.CommonName ?? "(unknown)";
+ Logger.LogTrace("GetReIssueResult: success. UUID='{Uuid}', CommonName='{CommonName}'", uuid ?? "(null)", commonName);
return new EnrollmentResult
{
- Status = (int)EndEntityStatus.GENERATED, //success
- CARequestID = reissueResponse.Result.Status.Uuid,
- StatusMessage = $"Reissue Successfully Completed For {reissueResponse.Result.CommonName}"
+ Status = (int)EndEntityStatus.EXTERNALVALIDATION,
+ CARequestID = uuid,
+ StatusMessage = $"Reissue Successfully Submitted For {commonName}. Certificate will be available after next sync."
};
}
public DomainControlValidation GetDomainControlValidation(string methodType, string[] emailAddress,
string domainName)
{
+ Logger.LogTrace("GetDomainControlValidation(array): methodType='{MethodType}', domainName='{DomainName}', emailAddress count={Count}",
+ methodType ?? "(null)", domainName ?? "(null)", emailAddress?.Length ?? 0);
+
+ if (emailAddress == null || emailAddress.Length == 0)
+ {
+ Logger.LogTrace("GetDomainControlValidation(array): no email addresses provided, returning null.");
+ return null;
+ }
+
foreach (var address in emailAddress)
{
- var email = new MailAddress(address);
- if (domainName.Contains(email.Host.Split('.')[0]))
- return new DomainControlValidation
+ if (string.IsNullOrEmpty(address))
+ {
+ Logger.LogTrace("GetDomainControlValidation(array): skipping null/empty email address.");
+ continue;
+ }
+
+ try
+ {
+ var email = new MailAddress(address);
+ var hostPart = email.Host?.Split('.')[0] ?? "";
+ Logger.LogTrace("GetDomainControlValidation(array): checking email='{Email}', hostPart='{HostPart}' against domain='{Domain}'",
+ address, hostPart, domainName);
+
+ if (!string.IsNullOrEmpty(domainName) && domainName.Contains(hostPart))
{
- MethodType = methodType,
- EmailAddress = email.ToString()
- };
+ Logger.LogTrace("GetDomainControlValidation(array): matched! Returning email='{Email}'", email.ToString());
+ return new DomainControlValidation
+ {
+ MethodType = methodType,
+ EmailAddress = email.ToString()
+ };
+ }
+ }
+ catch (FormatException fex)
+ {
+ Logger.LogWarning("GetDomainControlValidation(array): invalid email address '{Address}': {Message}", address, fex.Message);
+ }
}
+ Logger.LogTrace("GetDomainControlValidation(array): no matching email found, returning null.");
return null;
}
@@ -150,105 +340,190 @@ public DomainControlValidation GetDomainControlValidation(string methodType, str
public RegistrationRequest GetRegistrationRequest(EnrollmentProductInfo productInfo, string csr,
Dictionary sans, List customFields)
{
- //var cert = "-----BEGIN CERTIFICATE REQUEST-----\r\n";
- var cert = Pemify(csr);
- //cert = cert + "\r\n-----END CERTIFICATE REQUEST-----";
+ Logger.LogTrace("GetRegistrationRequest: building registration request. ProductID='{ProductId}'", productInfo?.ProductID ?? "(null)");
+ if (productInfo?.ProductParameters == null)
+ throw new ArgumentNullException(nameof(productInfo), "productInfo or ProductParameters cannot be null.");
+ if (string.IsNullOrEmpty(csr))
+ throw new ArgumentNullException(nameof(csr), "CSR cannot be null or empty.");
+ var cert = Pemify(csr);
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"];
+ Logger.LogTrace("GetRegistrationRequest: CSR encoded, length={Length}", encodedString.Length);
+
+ var commonNameValidationEmail = productInfo.ProductParameters.ContainsKey("CN DCV Email")
+ ? productInfo.ProductParameters["CN DCV Email"] : null;
+ var methodType = productInfo.ProductParameters.ContainsKey("Domain Control Validation Method")
+ ? productInfo.ProductParameters["Domain Control Validation Method"] : null;
var certificateType = GetCertificateType(productInfo.ProductID);
+ Logger.LogTrace("GetRegistrationRequest: cnDcvEmail='{Email}', methodType='{Method}', certType='{CertType}'",
+ commonNameValidationEmail ?? "(null)", methodType ?? "(null)", certificateType);
+
return new RegistrationRequest
{
Csr = encodedString,
- ServerSoftware = "-1", //Just default to other, user does not need to fill this in
+ 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 = productInfo.ProductParameters.ContainsKey("Term") ? productInfo.ProductParameters["Term"] : null,
+ ApplicantFirstName = productInfo.ProductParameters.ContainsKey("Applicant First Name") ? productInfo.ProductParameters["Applicant First Name"] : null,
+ ApplicantLastName = productInfo.ProductParameters.ContainsKey("Applicant Last Name") ? productInfo.ProductParameters["Applicant Last Name"] : null,
+ ApplicantEmailAddress = productInfo.ProductParameters.ContainsKey("Applicant Email Address") ? productInfo.ProductParameters["Applicant Email Address"] : null,
+ ApplicantPhoneNumber = productInfo.ProductParameters.ContainsKey("Applicant Phone") ? productInfo.ProductParameters["Applicant Phone"] : null,
DomainControlValidation = GetDomainControlValidation(methodType, commonNameValidationEmail),
Notifications = GetNotifications(productInfo),
- OrganizationContact = productInfo.ProductParameters["Organization Contact"],
- BusinessUnit = productInfo.ProductParameters["Business Unit"],
- ShowPrice = true, //User should not have to fill this out
+ OrganizationContact = productInfo.ProductParameters.ContainsKey("Organization Contact") ? productInfo.ProductParameters["Organization Contact"] : null,
+ BusinessUnit = productInfo.ProductParameters.ContainsKey("Business Unit") ? productInfo.ProductParameters["Business Unit"] : null,
+ ShowPrice = true,
CustomFields = GetCustomFields(productInfo, customFields),
SubjectAlternativeNames = certificateType == "2" ? GetSubjectAlternativeNames(productInfo, sans) : null,
EvCertificateDetails = certificateType == "3" ? GetEvCertificateDetails(productInfo) : null
};
}
+ // Maps Keyfactor product ID -> CSC API certificate type code (used for enrollment requests)
+ private static readonly Dictionary ProductIdToCodeMap = new(StringComparer.OrdinalIgnoreCase)
+ {
+ ["CSC TrustedSecure Premium Certificate"] = "0",
+ ["CSC TrustedSecure Premium Wildcard Certificate"] = "1",
+ ["CSC TrustedSecure UC Certificate"] = "2",
+ ["CSC TrustedSecure EV Certificate"] = "3",
+ ["CSC TrustedSecure Domain Validated SSL"] = "4",
+ ["CSC Trusted Secure Domain Validated SSL"] = "4",
+ ["CSC Trusted Secure Domain Validated Wildcard SSL"] = "5",
+ ["CSC Trusted Secure Domain Validated UC Certificate"] = "6",
+ };
+
+ // Reverse map: CSC API certificateType string -> Keyfactor product ID (used during sync)
+ // Note: CSC naming is inconsistent — first 4 types use "TrustedSecure" (no space),
+ // DV Wildcard and DV UC use "Trusted Secure" (with space),
+ // but CSC API returns DV SSL as "CSC Trusted Secure Domain Validated SSL" (with space)
+ // while the product ID is "CSC TrustedSecure Domain Validated SSL" (no space).
+ private static readonly Dictionary CodeToProductIdMap = new(StringComparer.OrdinalIgnoreCase)
+ {
+ // Premium
+ ["0"] = "CSC TrustedSecure Premium Certificate",
+ ["CSC TrustedSecure Premium Certificate"] = "CSC TrustedSecure Premium Certificate",
+ ["CSC Trusted Secure Premium Certificate"] = "CSC TrustedSecure Premium Certificate",
+ // Premium Wildcard
+ ["1"] = "CSC TrustedSecure Premium Wildcard Certificate",
+ ["CSC TrustedSecure Premium Wildcard Certificate"] = "CSC TrustedSecure Premium Wildcard Certificate",
+ ["CSC Trusted Secure Premium Wildcard Certificate"] = "CSC TrustedSecure Premium Wildcard Certificate",
+ // UC
+ ["2"] = "CSC TrustedSecure UC Certificate",
+ ["CSC TrustedSecure UC Certificate"] = "CSC TrustedSecure UC Certificate",
+ ["CSC Trusted Secure UC Certificate"] = "CSC TrustedSecure UC Certificate",
+ // EV
+ ["3"] = "CSC TrustedSecure EV Certificate",
+ ["CSC TrustedSecure EV Certificate"] = "CSC TrustedSecure EV Certificate",
+ ["CSC Trusted Secure EV Certificate"] = "CSC TrustedSecure EV Certificate",
+ // DV SSL — product ID has no space, but CSC API returns with space
+ ["4"] = "CSC TrustedSecure Domain Validated SSL",
+ ["CSC TrustedSecure Domain Validated SSL"] = "CSC TrustedSecure Domain Validated SSL",
+ ["CSC Trusted Secure Domain Validated SSL"] = "CSC TrustedSecure Domain Validated SSL",
+ // DV Wildcard — product ID has space (matches CSC API)
+ ["5"] = "CSC Trusted Secure Domain Validated Wildcard SSL",
+ ["CSC Trusted Secure Domain Validated Wildcard SSL"] = "CSC Trusted Secure Domain Validated Wildcard SSL",
+ ["CSC TrustedSecure Domain Validated Wildcard SSL"] = "CSC Trusted Secure Domain Validated Wildcard SSL",
+ // DV UC — product ID has space (matches CSC API)
+ ["6"] = "CSC Trusted Secure Domain Validated UC Certificate",
+ ["CSC Trusted Secure Domain Validated UC Certificate"] = "CSC Trusted Secure Domain Validated UC Certificate",
+ ["CSC TrustedSecure Domain Validated UC Certificate"] = "CSC Trusted Secure Domain Validated UC Certificate",
+ };
+
private string GetCertificateType(string productId)
{
- switch (productId)
+ Logger.LogTrace("GetCertificateType: productId='{ProductId}'", productId ?? "(null)");
+ if (!string.IsNullOrEmpty(productId) && ProductIdToCodeMap.TryGetValue(productId, out var code))
{
- case "CSC TrustedSecure Premium Certificate":
- return "0";
- case "CSC TrustedSecure EV Certificate":
- return "3";
- case "CSC TrustedSecure UC Certificate":
- return "2";
- case "CSC TrustedSecure Premium Wildcard Certificate":
- 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":
- return "4";
- case "CSC TrustedSecure Domain Validated Wildcard SSL":
- return "5";
- case "CSC TrustedSecure Domain Validated UC Certificate":
- return "6";
+ Logger.LogTrace("GetCertificateType: mapped '{ProductId}' -> '{Code}'", productId, code);
+ return code;
}
-
+ Logger.LogWarning("GetCertificateType: no mapping found for '{ProductId}', returning -1.", productId);
return "-1";
}
+ ///
+ /// Maps a CSC API certificateType value back to a Keyfactor product ID.
+ /// Handles numeric codes, descriptive strings, and passthrough of already-correct values.
+ ///
+ public string MapCertificateTypeToProductId(string cscCertificateType)
+ {
+ Logger.LogTrace("MapCertificateTypeToProductId: input='{CscCertType}'", cscCertificateType ?? "(null)");
+ if (!string.IsNullOrEmpty(cscCertificateType) && CodeToProductIdMap.TryGetValue(cscCertificateType, out var productId))
+ {
+ Logger.LogTrace("MapCertificateTypeToProductId: mapped '{CscCertType}' -> '{ProductId}'", cscCertificateType, productId);
+ return productId;
+ }
+ Logger.LogWarning("MapCertificateTypeToProductId: no mapping for '{CscCertType}', passing through as-is.", cscCertificateType);
+ return cscCertificateType ?? "CscGlobal";
+ }
+
public Notifications GetNotifications(EnrollmentProductInfo productInfo)
{
+ Logger.LogTrace("GetNotifications: building notifications.");
+ var emailsRaw = productInfo?.ProductParameters != null
+ && productInfo.ProductParameters.ContainsKey("Notification Email(s) Comma Separated")
+ ? productInfo.ProductParameters["Notification Email(s) Comma Separated"]
+ : null;
+
+ Logger.LogTrace("GetNotifications: raw notification emails='{Emails}'", emailsRaw ?? "(null)");
+
+ var emailList = !string.IsNullOrEmpty(emailsRaw)
+ ? emailsRaw.Split(',').Where(e => !string.IsNullOrWhiteSpace(e)).ToList()
+ : new List();
+
+ Logger.LogTrace("GetNotifications: parsed {Count} notification emails.", emailList.Count);
+
return new Notifications
{
Enabled = true,
- AdditionalNotificationEmails = productInfo.ProductParameters["Notification Email(s) Comma Separated"]
- .Split(',').ToList()
+ AdditionalNotificationEmails = emailList
};
}
public RenewalRequest GetRenewalRequest(EnrollmentProductInfo productInfo, string uUId, string csr,
Dictionary sans, List customFields)
{
- //var cert = "-----BEGIN CERTIFICATE REQUEST-----\r\n";
- var cert = Pemify(csr);
- //cert = cert + "\r\n-----END CERTIFICATE REQUEST-----";
+ Logger.LogTrace("GetRenewalRequest: building renewal request. UUID='{Uuid}', ProductID='{ProductId}'",
+ uUId ?? "(null)", productInfo?.ProductID ?? "(null)");
+
+ if (productInfo?.ProductParameters == null)
+ throw new ArgumentNullException(nameof(productInfo), "productInfo or ProductParameters cannot be null.");
+ if (string.IsNullOrEmpty(csr))
+ throw new ArgumentNullException(nameof(csr), "CSR cannot be null or empty.");
+ if (string.IsNullOrEmpty(uUId))
+ throw new ArgumentNullException(nameof(uUId), "uUId cannot be null or empty.");
+ var cert = Pemify(csr);
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 = productInfo.ProductParameters.ContainsKey("CN DCV Email")
+ ? productInfo.ProductParameters["CN DCV Email"] : null;
+ var methodType = productInfo.ProductParameters.ContainsKey("Domain Control Validation Method")
+ ? productInfo.ProductParameters["Domain Control Validation Method"] : null;
var certificateType = GetCertificateType(productInfo.ProductID);
+ Logger.LogTrace("GetRenewalRequest: cnDcvEmail='{Email}', methodType='{Method}', certType='{CertType}'",
+ commonNameValidationEmail ?? "(null)", methodType ?? "(null)", certificateType);
+
return new RenewalRequest
{
Uuid = uUId,
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 = productInfo.ProductParameters.ContainsKey("Term") ? productInfo.ProductParameters["Term"] : null,
+ ApplicantFirstName = productInfo.ProductParameters.ContainsKey("Applicant First Name") ? productInfo.ProductParameters["Applicant First Name"] : null,
+ ApplicantLastName = productInfo.ProductParameters.ContainsKey("Applicant Last Name") ? productInfo.ProductParameters["Applicant Last Name"] : null,
+ ApplicantEmailAddress = productInfo.ProductParameters.ContainsKey("Applicant Email Address") ? productInfo.ProductParameters["Applicant Email Address"] : null,
+ ApplicantPhoneNumber = productInfo.ProductParameters.ContainsKey("Applicant Phone") ? productInfo.ProductParameters["Applicant Phone"] : null,
DomainControlValidation = GetDomainControlValidation(methodType, commonNameValidationEmail),
Notifications = GetNotifications(productInfo),
- OrganizationContact = productInfo.ProductParameters["Organization Contact"],
- BusinessUnit = productInfo.ProductParameters["Business Unit"],
+ OrganizationContact = productInfo.ProductParameters.ContainsKey("Organization Contact") ? productInfo.ProductParameters["Organization Contact"] : null,
+ BusinessUnit = productInfo.ProductParameters.ContainsKey("Business Unit") ? productInfo.ProductParameters["Business Unit"] : null,
ShowPrice = true,
SubjectAlternativeNames = certificateType == "2" ? GetSubjectAlternativeNames(productInfo, sans) : null,
CustomFields = GetCustomFields(productInfo, customFields),
@@ -259,54 +534,107 @@ public RenewalRequest GetRenewalRequest(EnrollmentProductInfo productInfo, strin
private List GetSubjectAlternativeNames(EnrollmentProductInfo productInfo,
Dictionary sans)
{
+ Logger.LogTrace("GetSubjectAlternativeNames: building SANs.");
var subjectNameList = new List();
- var methodType = productInfo.ProductParameters["Domain Control Validation Method"];
- foreach (var v in sans["dnsname"])
+ if (sans == null || !sans.ContainsKey("dnsname"))
{
+ Logger.LogTrace("GetSubjectAlternativeNames: no 'dnsname' key in SANs dictionary, returning empty list.");
+ return subjectNameList;
+ }
+
+ var dnsNames = sans["dnsname"];
+ if (dnsNames == null || dnsNames.Length == 0)
+ {
+ Logger.LogTrace("GetSubjectAlternativeNames: 'dnsname' array is null or empty, returning empty list.");
+ return subjectNameList;
+ }
+
+ var methodType = productInfo?.ProductParameters != null
+ && productInfo.ProductParameters.ContainsKey("Domain Control Validation Method")
+ ? productInfo.ProductParameters["Domain Control Validation Method"]
+ : null;
+
+ Logger.LogTrace("GetSubjectAlternativeNames: processing {Count} DNS names, methodType='{MethodType}'",
+ dnsNames.Length, methodType ?? "(null)");
+
+ foreach (var v in dnsNames)
+ {
+ if (string.IsNullOrEmpty(v))
+ {
+ Logger.LogTrace("GetSubjectAlternativeNames: skipping null/empty DNS name.");
+ continue;
+ }
+
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")
+ Logger.LogTrace("GetSubjectAlternativeNames: processing domain='{Domain}'", domainName);
+
+ if (!string.IsNullOrEmpty(methodType) && methodType.ToUpper() == "EMAIL")
+ {
+ var emailsRaw = productInfo.ProductParameters.ContainsKey("Addtl Sans Comma Separated DVC Emails")
+ ? productInfo.ProductParameters["Addtl Sans Comma Separated DVC Emails"]
+ : null;
+ var emailAddresses = !string.IsNullOrEmpty(emailsRaw) ? emailsRaw.Split(',') : Array.Empty();
+ Logger.LogTrace("GetSubjectAlternativeNames: EMAIL validation, {Count} email addresses for domain='{Domain}'",
+ emailAddresses.Length, domainName);
san.DomainControlValidation = GetDomainControlValidation(methodType, emailAddresses, domainName);
- else //it is a CNAME validation so no email is needed
+ }
+ else
+ {
+ Logger.LogTrace("GetSubjectAlternativeNames: CNAME/other validation for domain='{Domain}'", domainName);
san.DomainControlValidation = GetDomainControlValidation(methodType, "");
+ }
subjectNameList.Add(san);
}
+ Logger.LogTrace("GetSubjectAlternativeNames: returning {Count} SANs.", subjectNameList.Count);
return subjectNameList;
}
public ReissueRequest GetReissueRequest(EnrollmentProductInfo productInfo, string uUId, string csr,
Dictionary sans, List customFields)
{
- //var cert = "-----BEGIN CERTIFICATE REQUEST-----\r\n";
- var cert = Pemify(csr);
- //cert = cert + "\r\n-----END CERTIFICATE REQUEST-----";
+ Logger.LogTrace("GetReissueRequest: building reissue request. UUID='{Uuid}', ProductID='{ProductId}'",
+ uUId ?? "(null)", productInfo?.ProductID ?? "(null)");
+
+ if (productInfo?.ProductParameters == null)
+ throw new ArgumentNullException(nameof(productInfo), "productInfo or ProductParameters cannot be null.");
+ if (string.IsNullOrEmpty(csr))
+ throw new ArgumentNullException(nameof(csr), "CSR cannot be null or empty.");
+ if (string.IsNullOrEmpty(uUId))
+ throw new ArgumentNullException(nameof(uUId), "uUId cannot be null or empty.");
+ var cert = Pemify(csr);
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 = productInfo.ProductParameters.ContainsKey("CN DCV Email")
+ ? productInfo.ProductParameters["CN DCV Email"] : null;
+ var methodType = productInfo.ProductParameters.ContainsKey("Domain Control Validation Method")
+ ? productInfo.ProductParameters["Domain Control Validation Method"] : null;
var certificateType = GetCertificateType(productInfo.ProductID);
+ Logger.LogTrace("GetReissueRequest: cnDcvEmail='{Email}', methodType='{Method}', certType='{CertType}'",
+ commonNameValidationEmail ?? "(null)", methodType ?? "(null)", certificateType);
+
return new ReissueRequest
{
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 = productInfo.ProductParameters.ContainsKey("Term") ? productInfo.ProductParameters["Term"] : null,
+ ApplicantFirstName = productInfo.ProductParameters.ContainsKey("Applicant First Name") ? productInfo.ProductParameters["Applicant First Name"] : null,
+ ApplicantLastName = productInfo.ProductParameters.ContainsKey("Applicant Last Name") ? productInfo.ProductParameters["Applicant Last Name"] : null,
+ ApplicantEmailAddress = productInfo.ProductParameters.ContainsKey("Applicant Email Address") ? productInfo.ProductParameters["Applicant Email Address"] : null,
+ ApplicantPhoneNumber = productInfo.ProductParameters.ContainsKey("Applicant Phone") ? productInfo.ProductParameters["Applicant Phone"] : null,
DomainControlValidation = GetDomainControlValidation(methodType, commonNameValidationEmail),
Notifications = GetNotifications(productInfo),
- OrganizationContact = productInfo.ProductParameters["Organization Contact"],
- BusinessUnit = productInfo.ProductParameters["Business Unit"],
+ OrganizationContact = productInfo.ProductParameters.ContainsKey("Organization Contact") ? productInfo.ProductParameters["Organization Contact"] : null,
+ BusinessUnit = productInfo.ProductParameters.ContainsKey("Business Unit") ? productInfo.ProductParameters["Business Unit"] : null,
ShowPrice = true,
SubjectAlternativeNames = certificateType == "2" ? GetSubjectAlternativeNames(productInfo, sans) : null,
CustomFields = GetCustomFields(productInfo, customFields),
@@ -316,15 +644,28 @@ public ReissueRequest GetReissueRequest(EnrollmentProductInfo productInfo, strin
private EvCertificateDetails GetEvCertificateDetails(EnrollmentProductInfo productInfo)
{
+ Logger.LogTrace("GetEvCertificateDetails: building EV details.");
+ var country = productInfo?.ProductParameters != null
+ && productInfo.ProductParameters.ContainsKey("Organization Country")
+ ? productInfo.ProductParameters["Organization Country"]
+ : null;
+ Logger.LogTrace("GetEvCertificateDetails: country='{Country}'", country ?? "(null)");
var evDetails = new EvCertificateDetails();
- evDetails.Country = productInfo.ProductParameters["Organization Country"];
+ evDetails.Country = country;
return evDetails;
}
public int MapReturnStatus(string cscGlobalStatus)
{
- var returnStatus = 0;
+ Logger.LogTrace("MapReturnStatus: input status='{Status}'", cscGlobalStatus ?? "(null)");
+
+ if (string.IsNullOrEmpty(cscGlobalStatus))
+ {
+ Logger.LogWarning("MapReturnStatus: status is null or empty, returning FAILED.");
+ return (int)EndEntityStatus.FAILED;
+ }
+ int returnStatus;
switch (cscGlobalStatus)
{
case "ACTIVE":
@@ -340,10 +681,12 @@ public int MapReturnStatus(string cscGlobalStatus)
returnStatus = (int)EndEntityStatus.REVOKED;
break;
default:
+ Logger.LogWarning("MapReturnStatus: unrecognized status '{Status}', returning FAILED.", cscGlobalStatus);
returnStatus = (int)EndEntityStatus.FAILED;
break;
}
+ Logger.LogTrace("MapReturnStatus: mapped '{Status}' to {Result}", cscGlobalStatus, returnStatus);
return returnStatus;
}
}
\ No newline at end of file
diff --git a/docsource/configuration.md b/docsource/configuration.md
index d8c196e..54c3210 100644
--- a/docsource/configuration.md
+++ b/docsource/configuration.md
@@ -10,6 +10,55 @@ This integration is tested and confirmed as working for Anygateway REST 24.2 and
The Root certificates for installation on the Anygateway server machine should be obtained from CSC.
+## CA Connection Configuration
+
+When defining the Certificate Authority in the AnyCA Gateway REST portal, configure the following fields on the **CA Connection** tab:
+
+CONFIG ELEMENT | DESCRIPTION | DEFAULT
+---------------|-------------|--------
+CscGlobalUrl | The base URL for the CSCGlobal API (e.g. `https://apis.cscglobal.com`) | (required)
+ApiKey | Your CSCGlobal API key | (required)
+BearerToken | Your CSCGlobal Bearer token for authentication | (required)
+DefaultPageSize | Page size for API list requests | 100
+SyncFilterDays | Number of days from today used to filter certificates by expiration date during **incremental** sync. Only certificates expiring within this window are returned. Does not apply to full sync. | 5
+RenewalWindowDays | Number of days before the annual order expiry date within which a **RenewOrReissue** request triggers a paid **Renewal** rather than a free **Reissue**. See [Renewal vs. Reissue Logic](#renewal-vs-reissue-logic) below. | 30
+
+## Renewal vs. Reissue Logic
+
+CSC Global subscriptions are annual orders. When Keyfactor Command sends a **RenewOrReissue** request, the plugin must decide whether to submit a **Renewal** (a new paid order) or a **Reissue** (a free re-key under the existing active order).
+
+The decision is based on the **RenewalWindowDays** setting and works as follows:
+
+1. The plugin fetches the original certificate from CSC and reads its `orderDate`.
+2. It computes the **order expiry** as `orderDate + 1 year`.
+3. It calculates **days remaining** until the order expires.
+4. If `days remaining <= RenewalWindowDays`, the request is treated as a **Renewal** (new paid order).
+5. If `days remaining > RenewalWindowDays`, the request is treated as a **Reissue** (free under the active order).
+
+**Example with default RenewalWindowDays = 30:**
+
+```
+Order Date: 2025-04-08
+Order Expiry: 2026-04-08
+Today: 2026-03-15
+Days Left: 24
+
+24 <= 30 --> RENEWAL (new paid order)
+```
+
+```
+Order Date: 2025-04-08
+Order Expiry: 2026-04-08
+Today: 2025-09-01
+Days Left: 219
+
+219 > 30 --> REISSUE (free under active order)
+```
+
+**Fallback behavior:** If the plugin cannot retrieve the `orderDate` from CSC (e.g., API error or missing field), it falls back to checking the certificate's expiration date. If the certificate is already expired, it treats the request as a Renewal.
+
+**Note:** Both Renewal and Reissue submissions are asynchronous at CSC. The plugin returns a "pending" status and the issued certificate will appear in Keyfactor after the next sync cycle.
+
## Certificate Template Creation Step
PLEASE NOTE, AT THIS TIME THE RAPID_SSL TEMPLATE IS NOT SUPPORTED BY THE CSC API AND WILL NOT WORK WITH THIS INTEGRATION
diff --git a/integration-manifest.json b/integration-manifest.json
index 2b4b8c4..71d13f4 100644
--- a/integration-manifest.json
+++ b/integration-manifest.json
@@ -29,13 +29,13 @@
"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."
+ },
+ {
+ "name": "RenewalWindowDays",
+ "description": "Number of days before the annual order expiry within which a RenewOrReissue triggers a paid Renewal rather than a free Reissue. Default is 30."
}
],
"enrollment_config": [
@@ -94,8 +94,8 @@
"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 Trusted Secure Domain Validated Wildcard SSL",
+ "CSC Trusted Secure Domain Validated UC Certificate"
]
}
}