From a1b7589d8003b277138d18d830793edece22a876 Mon Sep 17 00:00:00 2001
From: David Galey
Date: Tue, 30 Jun 2026 13:47:20 -0400
Subject: [PATCH 01/29] add product ID filter to sync
---
.../API/ListCertificateOrders.cs | 11 +++-
.../CertCentralCAPlugin.cs | 61 +++++++++----------
.../CertCentralConfig.cs | 15 +++++
.../Client/CertCentralClient.cs | 5 +-
digicert-certcentral-caplugin/Constants.cs | 1 +
5 files changed, 57 insertions(+), 36 deletions(-)
diff --git a/digicert-certcentral-caplugin/API/ListCertificateOrders.cs b/digicert-certcentral-caplugin/API/ListCertificateOrders.cs
index 7a62f6d..73714c8 100644
--- a/digicert-certcentral-caplugin/API/ListCertificateOrders.cs
+++ b/digicert-certcentral-caplugin/API/ListCertificateOrders.cs
@@ -29,7 +29,8 @@ public ListCertificateOrdersRequest(bool ignoreExpired = false)
public bool ignoreExpired { get; set; }
public int expiredWindow { get; set; } = 0;
- public string divID { get; set; } = string.Empty;
+ public List divIDs { get; set; } = new List();
+ public List productIDs { get; set; } = new List();
public new string BuildParameters()
{
@@ -38,9 +39,13 @@ public ListCertificateOrdersRequest(bool ignoreExpired = false)
sbParamters.Append("limit=").Append(this.limit.ToString());
sbParamters.Append("&offset=").Append(HttpUtility.UrlEncode(this.offset.ToString()));
- if (!string.IsNullOrEmpty(divID))
+ foreach (string divID in this.divIDs)
{
- sbParamters.Append("&filters[container_id]=").Append(this.divID);
+ sbParamters.Append("&filters[container_id]=").Append(divID);
+ }
+ foreach (string productID in productIDs)
+ {
+ sbParamters.Append("&filters[product_name_id]=").Append(productID);
}
if (ignoreExpired)
{
diff --git a/digicert-certcentral-caplugin/CertCentralCAPlugin.cs b/digicert-certcentral-caplugin/CertCentralCAPlugin.cs
index c71a6a2..0830e09 100644
--- a/digicert-certcentral-caplugin/CertCentralCAPlugin.cs
+++ b/digicert-certcentral-caplugin/CertCentralCAPlugin.cs
@@ -476,6 +476,13 @@ public Dictionary GetCAConnectorAnnotations()
DefaultValue = "",
Type = "String"
},
+ [CertCentralConstants.Config.SYNC_PROD_FILTER] = new PropertyConfigInfo()
+ {
+ Comments = "If you list one or more Product IDs here (comma-separated), the sync process will filter records to only return orders of those product types. Leave empty to sync all products.",
+ Hidden = false,
+ DefaultValue = "",
+ Type = "String"
+ },
[CertCentralConstants.Config.FILTER_EXPIRED] = new PropertyConfigInfo()
{
Comments = "If set to 'true', syncing will apply a filter to not return orders that are expired for longer than specified in SyncExpirationDays.",
@@ -834,12 +841,17 @@ public async Task Synchronize(BlockingCollection blockin
caList.ForEach(c => c.ToUpper());
- List divFilters = null;
+ List divFilters = new List();
if (!string.IsNullOrEmpty(_config.SyncDivisionFilter))
{
- divFilters = new List();
divFilters.AddRange(_config.SyncDivisionFilter.Split(','));
}
+ List productFilters = new List();
+ if (!string.IsNullOrEmpty(_config.SyncProductFilter))
+ {
+ _logger.LogTrace($"Sync Products: {_config.SyncProductFilter}");
+ productFilters = _config.SyncProducts;
+ }
if (fullSync)
{
@@ -857,37 +869,20 @@ public async Task Synchronize(BlockingCollection blockin
long starttime = time;
_logger.LogDebug($"SYNC: Starting sync at time {time}");
List allOrders = new List();
- if (divFilters != null)
+
+ ListCertificateOrdersResponse ordersResponse = client.ListAllCertificateOrders(ignoreExpired, expiredWindow, divFilters, productFilters);
+ if (ordersResponse.Status == CertCentralBaseResponse.StatusType.ERROR)
{
- foreach (string div in divFilters)
- {
- ListCertificateOrdersResponse ordersResponse = client.ListAllCertificateOrders(ignoreExpired, expiredWindow, div);
- if (ordersResponse.Status == CertCentralBaseResponse.StatusType.ERROR)
- {
- Error error = ordersResponse.Errors[0];
- _logger.LogError("Error in listing all certificate orders");
- throw new Exception($"DigiCert CertCentral web service returned {error.code} - {error.message} when retrieving all rows");
- }
- else
- {
- allOrders.AddRange(ordersResponse.orders);
- }
- }
+ Error error = ordersResponse.Errors[0];
+ _logger.LogError("Error in listing all certificate orders");
+ throw new Exception($"DigiCert CertCentral web service returned {error.code} - {error.message} when retrieving all rows");
}
else
{
- ListCertificateOrdersResponse ordersResponse = client.ListAllCertificateOrders(ignoreExpired, expiredWindow, null);
- if (ordersResponse.Status == CertCentralBaseResponse.StatusType.ERROR)
- {
- Error error = ordersResponse.Errors[0];
- _logger.LogError("Error in listing all certificate orders");
- throw new Exception($"DigiCert CertCentral web service returned {error.code} - {error.message} when retrieving all rows");
- }
- else
- {
- allOrders.AddRange(ordersResponse.orders);
- }
+ allOrders.AddRange(ordersResponse.orders);
}
+
+
_logger.LogDebug($"SYNC: Found {allOrders.Count} records");
foreach (var orderDetails in allOrders)
{
@@ -897,7 +892,7 @@ public async Task Synchronize(BlockingCollection blockin
cancelToken.ThrowIfCancellationRequested();
string caReqId = orderDetails.id + "-" + orderDetails.certificate.id;
_logger.LogDebug($"SYNC: Retrieving certs for order id {orderDetails.id}");
- orderCerts = GetAllConnectorCertsForOrder(caReqId, caList, divFilters);
+ orderCerts = GetAllConnectorCertsForOrder(caReqId, caList, divFilters, productFilters);
if (orderCerts == null || orderCerts.Count == 0)
{
continue;
@@ -939,7 +934,7 @@ public async Task Synchronize(BlockingCollection blockin
{
cancelToken.ThrowIfCancellationRequested();
string caReqId = order.order_id + "-" + order.certificate_id;
- orderCerts = GetAllConnectorCertsForOrder(caReqId, caList, divFilters);
+ orderCerts = GetAllConnectorCertsForOrder(caReqId, caList, divFilters, productFilters);
if (orderCerts == null || orderCerts.Count > 0)
{
continue;
@@ -1639,7 +1634,7 @@ string FormatSyncDate(DateTime? syncTime)
///
///
///
- private List GetAllConnectorCertsForOrder(string caRequestID, List caFilterIds, List divIds)
+ private List GetAllConnectorCertsForOrder(string caRequestID, List caFilterIds, List divIds, List productIds)
{
_logger.MethodEntry(LogLevel.Trace);
// Split ca request id into order and cert id
@@ -1662,6 +1657,10 @@ private List GetAllConnectorCertsForOrder(string caReque
_logger.LogTrace($"Found order ID {orderId} that does not match Division filter. Division ID: {orderResponse.container.Id.ToString()} Skipping...");
return null;
}
+ if (productIds != null && productIds.Count > 0 && !productIds.Contains(orderResponse.product.name_id.ToString()))
+ {
+ _logger.LogTrace($"Found order ID {orderId} that does not match Product filter. Product ID: {orderResponse.product.name_id.ToString()} Skipping...");
+ }
var orderCerts = GetAllCertsForOrder(orderId);
diff --git a/digicert-certcentral-caplugin/CertCentralConfig.cs b/digicert-certcentral-caplugin/CertCentralConfig.cs
index fa3b354..6909c2e 100644
--- a/digicert-certcentral-caplugin/CertCentralConfig.cs
+++ b/digicert-certcentral-caplugin/CertCentralConfig.cs
@@ -33,6 +33,21 @@ public List SyncCAs
}
}
}
+ public string SyncProductFilter { get; set; }
+ public List SyncProducts
+ {
+ get
+ {
+ if (!string.IsNullOrEmpty(SyncProductFilter))
+ {
+ return SyncProductFilter.Split(",").ToList();
+ }
+ else
+ {
+ return new List();
+ }
+ }
+ }
public bool? FilterExpiredOrders { get; set; }
public int? SyncExpirationDays { get; set; }
diff --git a/digicert-certcentral-caplugin/Client/CertCentralClient.cs b/digicert-certcentral-caplugin/Client/CertCentralClient.cs
index fd9af77..753f54f 100644
--- a/digicert-certcentral-caplugin/Client/CertCentralClient.cs
+++ b/digicert-certcentral-caplugin/Client/CertCentralClient.cs
@@ -523,7 +523,7 @@ public DownloadCertificateByFormatResponse DownloadCertificateByFormat(DownloadC
return dlCertificateRequestResponse;
}
- public ListCertificateOrdersResponse ListAllCertificateOrders(bool ignoreExpired = false, int expiredWindow = 0, string divId = "")
+ public ListCertificateOrdersResponse ListAllCertificateOrders(bool ignoreExpired, int expiredWindow, List divIds, List productIds)
{
int batch = 1000;
ListCertificateOrdersResponse totalResponse = new ListCertificateOrdersResponse();
@@ -536,7 +536,8 @@ public ListCertificateOrdersResponse ListAllCertificateOrders(bool ignoreExpired
offset = totalResponse.orders.Count,
ignoreExpired = ignoreExpired,
expiredWindow = expiredWindow,
- divID = divId
+ divIDs = divIds,
+ productIDs = productIds
};
CertCentralResponse response = Request(request, request.BuildParameters());
diff --git a/digicert-certcentral-caplugin/Constants.cs b/digicert-certcentral-caplugin/Constants.cs
index 183a7cd..19fe860 100644
--- a/digicert-certcentral-caplugin/Constants.cs
+++ b/digicert-certcentral-caplugin/Constants.cs
@@ -30,6 +30,7 @@ public class Config
public const string ENABLED = "Enabled";
public const string SYNC_CA_FILTER = "SyncCAFilter";
public const string SYNC_DIV_FILTER = "SyncDivisionFilter";
+ public const string SYNC_PROD_FILTER = "SyncProductFilter";
public const string FILTER_EXPIRED = "FilterExpiredOrders";
public const string SYNC_EXPIRATION_DAYS = "SyncExpirationDays";
public const string CERT_TYPE = "CertType";
From babebc35eeb27e2d87ae561aae2b0056c23af018 Mon Sep 17 00:00:00 2001
From: Keyfactor
Date: Tue, 30 Jun 2026 17:48:47 +0000
Subject: [PATCH 02/29] Update generated docs
---
README.md | 1 +
integration-manifest.json | 4 ++++
2 files changed, 5 insertions(+)
diff --git a/README.md b/README.md
index 6929905..4918513 100644
--- a/README.md
+++ b/README.md
@@ -91,6 +91,7 @@ An API Key within your Digicert account that has the necessary permissions to en
* **RevokeCertificateOnly** - Default DigiCert behavior on revocation requests is to revoke the entire order. If this value is changed to 'true', revocation requests will instead just revoke the individual certificate.
* **SyncCAFilter** - If you list one or more CA IDs here (comma-separated), the sync process will only sync records from those CAs. If you want to sync all CA IDs, leave this field empty.
* **SyncDivisionFilter** - If you list one or more Divison IDs (also known as Container IDs) here (comma-separated), the sync process will filter records to only return orders from those divisions. If you want to sync all divisions, leave this field empty. Note that this has no relationship to the value of the DivisionId config field.
+ * **SyncProductFilter** - If you list one or more Product IDs here (comma-separated), the sync process will filter records to only return orders of those product types. Leave empty to sync all products.
* **FilterExpiredOrders** - If set to 'true', syncing will apply a filter to not return orders that are expired for longer than specified in SyncExpirationDays.
* **SyncExpirationDays** - If FilterExpiredOrders is set to true, this setting determines how many days in the past to still return expired orders. For example, a value of 30 means the sync will return any certs that expired within the past 30 days. A value of 0 means the sync will not return any certs that expired before the current day. This value is ignored if FilterExpiredOrders is false.
* **Enabled** - Flag to Enable or Disable gateway functionality. Disabling is primarily used to allow creation of the CA prior to configuration information being available.
diff --git a/integration-manifest.json b/integration-manifest.json
index a854d2f..8cad8e9 100644
--- a/integration-manifest.json
+++ b/integration-manifest.json
@@ -38,6 +38,10 @@
"name": "SyncDivisionFilter",
"description": "If you list one or more Divison IDs (also known as Container IDs) here (comma-separated), the sync process will filter records to only return orders from those divisions. If you want to sync all divisions, leave this field empty. Note that this has no relationship to the value of the DivisionId config field."
},
+ {
+ "name": "SyncProductFilter",
+ "description": "If you list one or more Product IDs here (comma-separated), the sync process will filter records to only return orders of those product types. Leave empty to sync all products."
+ },
{
"name": "FilterExpiredOrders",
"description": "If set to 'true', syncing will apply a filter to not return orders that are expired for longer than specified in SyncExpirationDays."
From 9eaa6ea3a0617fc53aa3bdea04372ff9b0f4f42a Mon Sep 17 00:00:00 2001
From: David Galey
Date: Wed, 1 Jul 2026 13:36:57 -0400
Subject: [PATCH 03/29] add Intel vPro EKU support
---
.../CertCentralCAPlugin.cs | 20 +++++++++++++++----
digicert-certcentral-caplugin/Constants.cs | 1 +
2 files changed, 17 insertions(+), 4 deletions(-)
diff --git a/digicert-certcentral-caplugin/CertCentralCAPlugin.cs b/digicert-certcentral-caplugin/CertCentralCAPlugin.cs
index 0830e09..4b3b8b6 100644
--- a/digicert-certcentral-caplugin/CertCentralCAPlugin.cs
+++ b/digicert-certcentral-caplugin/CertCentralCAPlugin.cs
@@ -303,9 +303,10 @@ public async Task Enroll(string csr, string subject, Dictionar
{
bool clientAuth = Convert.ToBoolean(productInfo.ProductParameters[CertCentralConstants.Config.INCLUDE_CLIENT_AUTH]);
bool kdc = Convert.ToBoolean(productInfo.ProductParameters[CertCentralConstants.Config.INCLUDE_KDC]);
- if (clientAuth && kdc)
+ bool intel = Convert.ToBoolean(productInfo.ProductParameters[CertCentralConstants.Config.INCLUDE_INTEL]);
+ if ((clientAuth ? 1 : 0) + (kdc ? 1 : 0) + (intel ? 1 : 0) >= 2) //If more than one EKU option is selected
{
- throw new Exception($"Cannot enroll for cert with both Client Auth and KDC/SmartCardLogon EKU set to 'true'");
+ throw new Exception($"Cannot enroll for cert with more than one EKU option selected");
}
if (clientAuth)
{
@@ -316,6 +317,10 @@ public async Task Enroll(string csr, string subject, Dictionar
{
orderRequest.Certificate.ProfileOption = "kdc_smart_card";
}
+ else if (intel)
+ {
+ orderRequest.Certificate.ProfileOption = "intel_vpro_eku";
+ }
}
bool dupe = false;
@@ -640,14 +645,21 @@ public Dictionary GetTemplateParameterAnnotations()
},
[CertCentralConstants.Config.INCLUDE_CLIENT_AUTH] = new PropertyConfigInfo()
{
- Comments = "OPTIONAL for SSL certs, ignored otherwise. If set to 'true', SSL certs enrolled under this template will have the Client Authentication EKU added to the request. NOTE: This feature is currently planned to be removed by DigiCert in March 2027.",
+ Comments = "OPTIONAL for SSL certs, ignored otherwise. If set to 'true', SSL certs enrolled under this template will have the Client Authentication EKU added to the request. NOTE: Only one EKU option can be set for any given enrollment. NOTE: This feature is currently planned to be removed by DigiCert in March 2027.",
Hidden = false,
DefaultValue = false,
Type = "Boolean"
},
[CertCentralConstants.Config.INCLUDE_KDC] = new PropertyConfigInfo()
{
- Comments = "OPTIONAL for SSL certs, ignored otherwise. If set to 'true', SSL certs enrolled under this template will have the KDC/SmartCardLogon EKU added to the request.",
+ Comments = "OPTIONAL for SSL certs, ignored otherwise. If set to 'true', SSL certs enrolled under this template will have the KDC/SmartCardLogon EKU added to the request. NOTE: Only one EKU option can be set for any given enrollment.",
+ Hidden = false,
+ DefaultValue = false,
+ Type = "Boolean"
+ },
+ [CertCentralConstants.Config.INCLUDE_INTEL] = new PropertyConfigInfo()
+ {
+ Comments = "OPTIONAL for SSL certs, ignored otherwise. If set to 'true', SSL certs enrolled under this template will have the Intel vPro EKU added to the request. NOTE: Only one EKU option can be set for any given enrollment.",
Hidden = false,
DefaultValue = false,
Type = "Boolean"
diff --git a/digicert-certcentral-caplugin/Constants.cs b/digicert-certcentral-caplugin/Constants.cs
index 19fe860..a01d8ca 100644
--- a/digicert-certcentral-caplugin/Constants.cs
+++ b/digicert-certcentral-caplugin/Constants.cs
@@ -36,6 +36,7 @@ public class Config
public const string CERT_TYPE = "CertType";
public const string INCLUDE_CLIENT_AUTH = "IncludeClientAuthEKU";
public const string INCLUDE_KDC = "IncludeKDCSmartCardLogonEKU";
+ public const string INCLUDE_INTEL = "IncludeIntelvProEKU";
public const string ENROLL_DIVISION_ID = "EnrollDivisionId";
public const string COMMON_NAME_INDICATOR = "CommonNameIndicator";
public const string PROFILE_TYPE = "ProfileType";
From 1df8750b561095481b7904c3e2956c6e4a798df6 Mon Sep 17 00:00:00 2001
From: Keyfactor
Date: Wed, 1 Jul 2026 17:38:52 +0000
Subject: [PATCH 04/29] Update generated docs
---
README.md | 5 +++--
integration-manifest.json | 8 ++++++--
2 files changed, 9 insertions(+), 4 deletions(-)
diff --git a/README.md b/README.md
index 4918513..6f735fc 100644
--- a/README.md
+++ b/README.md
@@ -107,8 +107,9 @@ An API Key within your Digicert account that has the necessary permissions to en
* **Organization-Name** - OPTIONAL: For requests that will not have a subject (such as ACME) you can use this field to provide the organization name. Value supplied here will override any CSR values, so do not include this field if you want the organization from the CSR to be used.
* **RenewalWindowDays** - OPTIONAL: The number of days from certificate expiration that the gateway should do a renewal rather than a reissue. If not provided, default is 90.
* **CertType** - OPTIONAL: The type of cert to enroll for. Valid values are 'ssl' and 'client'. The value provided here must be consistant with the ProductID. If not provided, default is 'ssl'. Ignored for secure_email_* product types.
- * **IncludeClientAuthEKU** - OPTIONAL for SSL certs, ignored otherwise. If set to 'true', SSL certs enrolled under this template will have the Client Authentication EKU added to the request. NOTE: This feature is currently planned to be removed by DigiCert in March 2027.
- * **IncludeKDCSmartCardLogonEKU** - OPTIONAL for SSL certs, ignored otherwise. If set to 'true', SSL certs enrolled under this template will have the KDC/SmartCardLogon EKU added to the request.
+ * **IncludeClientAuthEKU** - OPTIONAL for SSL certs, ignored otherwise. If set to 'true', SSL certs enrolled under this template will have the Client Authentication EKU added to the request. NOTE: Only one EKU option can be set for any given enrollment. NOTE: This feature is currently planned to be removed by DigiCert in March 2027.
+ * **IncludeKDCSmartCardLogonEKU** - OPTIONAL for SSL certs, ignored otherwise. If set to 'true', SSL certs enrolled under this template will have the KDC/SmartCardLogon EKU added to the request. NOTE: Only one EKU option can be set for any given enrollment.
+ * **IncludeIntelvProEKU** - OPTIONAL for SSL certs, ignored otherwise. If set to 'true', SSL certs enrolled under this template will have the Intel vPro EKU added to the request. NOTE: Only one EKU option can be set for any given enrollment.
* **EnrollDivisionId** - OPTIONAL: The division (container) ID to use for enrollments against this template.
* **CommonNameIndicator** - Required for secure_email_sponsor and secure_email_organization products, ignored otherwise. Defines the source of the common name. Valid values are: email_address, given_name_surname, pseudonym, organization_name
* **ProfileType** - Optional for secure_email_* types, ignored otherwise. Valid values are: strict, multipurpose. Use 'multipurpose' if your cert includes any additional EKUs such as client auth. Default if not provided is dependent on product configuration within Digicert portal.
diff --git a/integration-manifest.json b/integration-manifest.json
index 8cad8e9..a30cc75 100644
--- a/integration-manifest.json
+++ b/integration-manifest.json
@@ -78,11 +78,15 @@
},
{
"name": "IncludeClientAuthEKU",
- "description": "OPTIONAL for SSL certs, ignored otherwise. If set to 'true', SSL certs enrolled under this template will have the Client Authentication EKU added to the request. NOTE: This feature is currently planned to be removed by DigiCert in March 2027."
+ "description": "OPTIONAL for SSL certs, ignored otherwise. If set to 'true', SSL certs enrolled under this template will have the Client Authentication EKU added to the request. NOTE: Only one EKU option can be set for any given enrollment. NOTE: This feature is currently planned to be removed by DigiCert in March 2027."
},
{
"name": "IncludeKDCSmartCardLogonEKU",
- "description": "OPTIONAL for SSL certs, ignored otherwise. If set to 'true', SSL certs enrolled under this template will have the KDC/SmartCardLogon EKU added to the request."
+ "description": "OPTIONAL for SSL certs, ignored otherwise. If set to 'true', SSL certs enrolled under this template will have the KDC/SmartCardLogon EKU added to the request. NOTE: Only one EKU option can be set for any given enrollment."
+ },
+ {
+ "name": "IncludeIntelvProEKU",
+ "description": "OPTIONAL for SSL certs, ignored otherwise. If set to 'true', SSL certs enrolled under this template will have the Intel vPro EKU added to the request. NOTE: Only one EKU option can be set for any given enrollment."
},
{
"name": "EnrollDivisionId",
From c543fcbf74fcd97c0b7d9deb43018f10eeeb6d09 Mon Sep 17 00:00:00 2001
From: David Galey
Date: Wed, 1 Jul 2026 13:53:40 -0400
Subject: [PATCH 05/29] fix for renewal of smime certs
---
digicert-certcentral-caplugin/CertCentralCAPlugin.cs | 1 +
1 file changed, 1 insertion(+)
diff --git a/digicert-certcentral-caplugin/CertCentralCAPlugin.cs b/digicert-certcentral-caplugin/CertCentralCAPlugin.cs
index 4b3b8b6..f40480f 100644
--- a/digicert-certcentral-caplugin/CertCentralCAPlugin.cs
+++ b/digicert-certcentral-caplugin/CertCentralCAPlugin.cs
@@ -2074,6 +2074,7 @@ private EnrollmentResult EnrollForSmimeCert(string csr, string subject, Dictiona
if (enrollmentType == EnrollmentType.Renew)
{
+ priorCertSnString = productInfo.ProductParameters["PriorCertSN"];
priorCertReqID = _certificateDataReader.GetRequestIDBySerialNumber(priorCertSnString).Result;
if (string.IsNullOrEmpty(priorCertReqID))
{
From c453bddfb0ab1067561acccd1a6961b1a56a1541 Mon Sep 17 00:00:00 2001
From: David Galey
Date: Wed, 1 Jul 2026 14:10:31 -0400
Subject: [PATCH 06/29] validation for intel vpro eku
---
digicert-certcentral-caplugin/CertCentralCAPlugin.cs | 10 +++++++---
1 file changed, 7 insertions(+), 3 deletions(-)
diff --git a/digicert-certcentral-caplugin/CertCentralCAPlugin.cs b/digicert-certcentral-caplugin/CertCentralCAPlugin.cs
index f40480f..caf6f1a 100644
--- a/digicert-certcentral-caplugin/CertCentralCAPlugin.cs
+++ b/digicert-certcentral-caplugin/CertCentralCAPlugin.cs
@@ -1129,7 +1129,7 @@ public async Task ValidateProductInfo(EnrollmentProductInfo productInfo, Diction
}
}
- bool clientAuth = false, kdc = false;
+ bool clientAuth = false, kdc = false, intel = false;
if (productInfo.ProductParameters.ContainsKey(CertCentralConstants.Config.INCLUDE_CLIENT_AUTH))
{
clientAuth = Convert.ToBoolean(productInfo.ProductParameters[CertCentralConstants.Config.INCLUDE_CLIENT_AUTH]);
@@ -1138,9 +1138,13 @@ public async Task ValidateProductInfo(EnrollmentProductInfo productInfo, Diction
{
kdc = Convert.ToBoolean(productInfo.ProductParameters[CertCentralConstants.Config.INCLUDE_KDC]);
}
- if (clientAuth && kdc)
+ if (productInfo.ProductParameters.ContainsKey(CertCentralConstants.Config.INCLUDE_INTEL))
{
- throw new AnyCAValidationException($"Unable to use both {CertCentralConstants.Config.INCLUDE_CLIENT_AUTH} and {CertCentralConstants.Config.INCLUDE_KDC} in the same certificate.");
+ intel = Convert.ToBoolean(productInfo.ProductParameters[CertCentralConstants.Config.INCLUDE_INTEL]);
+ }
+ if ((clientAuth ? 1 : 0) + (kdc ? 1 : 0) + (intel ? 1 : 0) >= 2) // If more than one EKU option is selected
+ {
+ throw new AnyCAValidationException($"Unable to use more than one of: {CertCentralConstants.Config.INCLUDE_CLIENT_AUTH}, {CertCentralConstants.Config.INCLUDE_KDC}, or {CertCentralConstants.Config.INCLUDE_INTEL} in the same certificate.");
}
_logger.MethodExit(LogLevel.Trace);
From 126fd30f1e7c62a86d3ef28d6d1d5889720b3883 Mon Sep 17 00:00:00 2001
From: David Galey
Date: Wed, 1 Jul 2026 14:36:49 -0400
Subject: [PATCH 07/29] fix for template validation
---
digicert-certcentral-caplugin/CertCentralCAPlugin.cs | 5 +++--
1 file changed, 3 insertions(+), 2 deletions(-)
diff --git a/digicert-certcentral-caplugin/CertCentralCAPlugin.cs b/digicert-certcentral-caplugin/CertCentralCAPlugin.cs
index caf6f1a..cb1a503 100644
--- a/digicert-certcentral-caplugin/CertCentralCAPlugin.cs
+++ b/digicert-certcentral-caplugin/CertCentralCAPlugin.cs
@@ -1094,10 +1094,11 @@ public async Task ValidateProductInfo(EnrollmentProductInfo productInfo, Diction
// Get product ID details.
CertificateTypeDetailsRequest detailsRequest = new CertificateTypeDetailsRequest(product.NameId);
+ // For pulling product ID details, we use the Connection-level Division ID rather than the enrollment-level one.
detailsRequest.ContainerId = null;
- if (productInfo.ProductParameters.ContainsKey(CertCentralConstants.Config.ENROLL_DIVISION_ID))
+ if (connectionInfo.ContainsKey(CertCentralConstants.Config.DIVISION_ID))
{
- string div = productInfo.ProductParameters[CertCentralConstants.Config.ENROLL_DIVISION_ID].ToString();
+ string div = connectionInfo[CertCentralConstants.Config.DIVISION_ID].ToString();
if (!string.IsNullOrWhiteSpace(div))
{
if (int.TryParse($"{div}", out int divId))
From a8e6c5e99d2b210da097afda663e727ee357e265 Mon Sep 17 00:00:00 2001
From: David Galey
Date: Thu, 2 Jul 2026 10:44:05 -0400
Subject: [PATCH 08/29] changelog
---
CHANGELOG.md | 6 ++++++
1 file changed, 6 insertions(+)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 6df6ad5..4f81506 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -30,3 +30,9 @@
### 2.3.0
* Add configuration flag to support adding KDC/SmartCardLogon EKU to ssl cert requests
+
+### 2.4.0
+* Add configuration flag to support Intel vPro EKU on ssl cert requests
+* Add ability to filter sync by product ID
+* Bug fix for SMIME cert renewal
+* Bug fix for template validation
From add1e9c11a86f2a6ca1d4c5ee3692a260bd88955 Mon Sep 17 00:00:00 2001
From: David Galey
Date: Tue, 21 Jul 2026 07:54:46 -0400
Subject: [PATCH 09/29] check for existance of template parameter fields
---
CHANGELOG.md | 5 +++-
.../CertCentralCAPlugin.cs | 28 +++++++++++++++++--
2 files changed, 29 insertions(+), 4 deletions(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 4f81506..a571c73 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -35,4 +35,7 @@
* Add configuration flag to support Intel vPro EKU on ssl cert requests
* Add ability to filter sync by product ID
* Bug fix for SMIME cert renewal
-* Bug fix for template validation
+* Bug fix for template validation
+
+### 2.4.1
+* Fix for missing parameter errors
diff --git a/digicert-certcentral-caplugin/CertCentralCAPlugin.cs b/digicert-certcentral-caplugin/CertCentralCAPlugin.cs
index cb1a503..176c2bd 100644
--- a/digicert-certcentral-caplugin/CertCentralCAPlugin.cs
+++ b/digicert-certcentral-caplugin/CertCentralCAPlugin.cs
@@ -301,9 +301,31 @@ public async Task Enroll(string csr, string subject, Dictionar
if (typeOfCert.Equals("ssl"))
{
- bool clientAuth = Convert.ToBoolean(productInfo.ProductParameters[CertCentralConstants.Config.INCLUDE_CLIENT_AUTH]);
- bool kdc = Convert.ToBoolean(productInfo.ProductParameters[CertCentralConstants.Config.INCLUDE_KDC]);
- bool intel = Convert.ToBoolean(productInfo.ProductParameters[CertCentralConstants.Config.INCLUDE_INTEL]);
+ bool clientAuth = false, kdc = false, intel = false;
+ if (productInfo.ProductParameters.TryGetValue(CertCentralConstants.Config.INCLUDE_CLIENT_AUTH, out string clientAuthValue))
+ {
+ if (!bool.TryParse(clientAuthValue, out clientAuth))
+ {
+ _logger.LogError($"Could not parse 'IncludeClientAuthEKU' field as true or false. Check configuration. Value: {clientAuthValue}");
+ throw new Exception($"Could not parse 'IncludeClientAuthEKU' field as true or false. Check configuration");
+ }
+ }
+ if (productInfo.ProductParameters.TryGetValue(CertCentralConstants.Config.INCLUDE_KDC, out string kdcValue))
+ {
+ if (!bool.TryParse(kdcValue, out kdc))
+ {
+ _logger.LogError($"Could not parse 'IncludeKDCSmartCardLogonEKU' field as true or false. Check configuration. Value: {kdcValue}");
+ throw new Exception($"Could not parse 'IncludeKDCSmartCardLogonEKU' field as true or false. Check configuration");
+ }
+ }
+ if (productInfo.ProductParameters.TryGetValue(CertCentralConstants.Config.INCLUDE_INTEL, out string intelValue))
+ {
+ if (!bool.TryParse(intelValue, out intel))
+ {
+ _logger.LogError($"Could not parse 'IncludeIntelvProEKU' field as true or false. Check configuration. Value: {intelValue}");
+ throw new Exception($"Could not parse 'IncludeIntelvProEKU' field as true or false. Check configuration");
+ }
+ }
if ((clientAuth ? 1 : 0) + (kdc ? 1 : 0) + (intel ? 1 : 0) >= 2) //If more than one EKU option is selected
{
throw new Exception($"Cannot enroll for cert with more than one EKU option selected");
From 368a70fd0d39c296355fee7a82f399d92139ebee Mon Sep 17 00:00:00 2001
From: Morgan Gangwere <470584+indrora@users.noreply.github.com>
Date: Wed, 22 Jul 2026 14:04:39 -0700
Subject: [PATCH 10/29] Merge 2.4.1 to main (#61)
* fix for smime profile type
* template parameter to include client auth eku
* Update generated docs
* changelog and logging
* check for duplicate PEMs
* change default start sync date for first incremental sync
* removing caching of product type list
* change default incremental sync range
* version
* changelog
* shorten incremental sync if it is too long
* feat: release v2.2.0
* add duplicate support
* Update generated docs
---------
Co-authored-by: Keyfactor
* Dev 2.2 (#47)
* add duplicate support
* Update generated docs
* treat needs_approval the same as pending on enrollments and don't return failure code
* Update generated docs
---------
Co-authored-by: Keyfactor
* Dev 2.3 (#54)
* improve BouncyCastle parsing
* add duplicate support
* Update generated docs
* Merge 2.2.0 to main
* fix for smime profile type
* template parameter to include client auth eku
* Update generated docs
* changelog and logging
* check for duplicate PEMs
* change default start sync date for first incremental sync
* removing caching of product type list
* change default incremental sync range
* version
* changelog
* shorten incremental sync if it is too long
* feat: release v2.2.0
* add duplicate support
* Update generated docs
---------
Co-authored-by: Keyfactor
---------
Co-authored-by: David Galey
Co-authored-by: Keyfactor
Co-authored-by: Dave Galey <89407235+dgaley@users.noreply.github.com>
Co-authored-by: Sean <1661003+spbsoluble@users.noreply.github.com>
* treat needs_approval the same as pending on enrollments and don't return failure code
* Update generated docs
* Merge 2.2.1 to main (#49)
* fix for smime profile type
* template parameter to include client auth eku
* Update generated docs
* changelog and logging
* check for duplicate PEMs
* change default start sync date for first incremental sync
* removing caching of product type list
* change default incremental sync range
* version
* changelog
* shorten incremental sync if it is too long
* feat: release v2.2.0
* add duplicate support
* Update generated docs
---------
Co-authored-by: Keyfactor
* Dev 2.2 (#47)
* add duplicate support
* Update generated docs
* treat needs_approval the same as pending on enrollments and don't return failure code
* Update generated docs
---------
Co-authored-by: Keyfactor
---------
Co-authored-by: David Galey
Co-authored-by: Keyfactor
Co-authored-by: Dave Galey <89407235+dgaley@users.noreply.github.com>
Co-authored-by: Sean <1661003+spbsoluble@users.noreply.github.com>
* Merge to main (#48)
* fix for smime profile type
* template parameter to include client auth eku
* Update generated docs
* changelog and logging
* check for duplicate PEMs
* change default start sync date for first incremental sync
* removing caching of product type list
* change default incremental sync range
* version
* changelog
* shorten incremental sync if it is too long
* add duplicate support
* Update generated docs
* treat needs_approval the same as pending on enrollments and don't return failure code
* Update generated docs
---------
Co-authored-by: David Galey
Co-authored-by: Keyfactor
Co-authored-by: Dave Galey <89407235+dgaley@users.noreply.github.com>
Co-authored-by: Sean <1661003+spbsoluble@users.noreply.github.com>
* Update CHANGELOG.md (#50)
* add option for kdc/smartcardlogon eku, fix template validation
* Update generated docs
* changelog
---------
Co-authored-by: Sean <1661003+spbsoluble@users.noreply.github.com>
Co-authored-by: Keyfactor
Co-authored-by: Morgan Gangwere <470584+indrora@users.noreply.github.com>
* Dev 2.4 (#57)
* add product ID filter to sync
* Update generated docs
* add Intel vPro EKU support
* Update generated docs
* fix for renewal of smime certs
* validation for intel vpro eku
* fix for template validation
* changelog
---------
Co-authored-by: Keyfactor
* Dev 2.4 (#60)
* add product ID filter to sync
* Update generated docs
* add Intel vPro EKU support
* Update generated docs
* fix for renewal of smime certs
* validation for intel vpro eku
* fix for template validation
* changelog
* check for existance of template parameter fields
---------
Co-authored-by: Keyfactor
---------
Co-authored-by: David Galey
Co-authored-by: Keyfactor
Co-authored-by: Dave Galey <89407235+dgaley@users.noreply.github.com>
Co-authored-by: Sean <1661003+spbsoluble@users.noreply.github.com>
---
CHANGELOG.md | 5 +++-
.../CertCentralCAPlugin.cs | 30 ++++++++++++++++---
2 files changed, 30 insertions(+), 5 deletions(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 4f81506..a571c73 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -35,4 +35,7 @@
* Add configuration flag to support Intel vPro EKU on ssl cert requests
* Add ability to filter sync by product ID
* Bug fix for SMIME cert renewal
-* Bug fix for template validation
+* Bug fix for template validation
+
+### 2.4.1
+* Fix for missing parameter errors
diff --git a/digicert-certcentral-caplugin/CertCentralCAPlugin.cs b/digicert-certcentral-caplugin/CertCentralCAPlugin.cs
index 5605eb9..176c2bd 100644
--- a/digicert-certcentral-caplugin/CertCentralCAPlugin.cs
+++ b/digicert-certcentral-caplugin/CertCentralCAPlugin.cs
@@ -301,9 +301,31 @@ public async Task Enroll(string csr, string subject, Dictionar
if (typeOfCert.Equals("ssl"))
{
- bool clientAuth = Convert.ToBoolean(productInfo.ProductParameters[CertCentralConstants.Config.INCLUDE_CLIENT_AUTH]);
- bool kdc = Convert.ToBoolean(productInfo.ProductParameters[CertCentralConstants.Config.INCLUDE_KDC]);
- bool intel = Convert.ToBoolean(productInfo.ProductParameters[CertCentralConstants.Config.INCLUDE_INTEL]);
+ bool clientAuth = false, kdc = false, intel = false;
+ if (productInfo.ProductParameters.TryGetValue(CertCentralConstants.Config.INCLUDE_CLIENT_AUTH, out string clientAuthValue))
+ {
+ if (!bool.TryParse(clientAuthValue, out clientAuth))
+ {
+ _logger.LogError($"Could not parse 'IncludeClientAuthEKU' field as true or false. Check configuration. Value: {clientAuthValue}");
+ throw new Exception($"Could not parse 'IncludeClientAuthEKU' field as true or false. Check configuration");
+ }
+ }
+ if (productInfo.ProductParameters.TryGetValue(CertCentralConstants.Config.INCLUDE_KDC, out string kdcValue))
+ {
+ if (!bool.TryParse(kdcValue, out kdc))
+ {
+ _logger.LogError($"Could not parse 'IncludeKDCSmartCardLogonEKU' field as true or false. Check configuration. Value: {kdcValue}");
+ throw new Exception($"Could not parse 'IncludeKDCSmartCardLogonEKU' field as true or false. Check configuration");
+ }
+ }
+ if (productInfo.ProductParameters.TryGetValue(CertCentralConstants.Config.INCLUDE_INTEL, out string intelValue))
+ {
+ if (!bool.TryParse(intelValue, out intel))
+ {
+ _logger.LogError($"Could not parse 'IncludeIntelvProEKU' field as true or false. Check configuration. Value: {intelValue}");
+ throw new Exception($"Could not parse 'IncludeIntelvProEKU' field as true or false. Check configuration");
+ }
+ }
if ((clientAuth ? 1 : 0) + (kdc ? 1 : 0) + (intel ? 1 : 0) >= 2) //If more than one EKU option is selected
{
throw new Exception($"Cannot enroll for cert with more than one EKU option selected");
@@ -1096,7 +1118,7 @@ public async Task ValidateProductInfo(EnrollmentProductInfo productInfo, Diction
// For pulling product ID details, we use the Connection-level Division ID rather than the enrollment-level one.
detailsRequest.ContainerId = null;
- if (productInfo.ProductParameters.ContainsKey(CertCentralConstants.Config.ENROLL_DIVISION_ID))
+ if (connectionInfo.ContainsKey(CertCentralConstants.Config.DIVISION_ID))
{
string div = connectionInfo[CertCentralConstants.Config.DIVISION_ID].ToString();
if (!string.IsNullOrWhiteSpace(div))
From d6e7a457cbbcd3f4554d5f92ab1a5a901c80d0e7 Mon Sep 17 00:00:00 2001
From: Dave Galey <89407235+dgaley@users.noreply.github.com>
Date: Tue, 18 Aug 2026 14:32:14 -0400
Subject: [PATCH 11/29] Update digicert-certcentral-caplugin.csproj
---
.../digicert-certcentral-caplugin.csproj | 2 --
1 file changed, 2 deletions(-)
diff --git a/digicert-certcentral-caplugin/digicert-certcentral-caplugin.csproj b/digicert-certcentral-caplugin/digicert-certcentral-caplugin.csproj
index 7510b07..8b1c555 100644
--- a/digicert-certcentral-caplugin/digicert-certcentral-caplugin.csproj
+++ b/digicert-certcentral-caplugin/digicert-certcentral-caplugin.csproj
@@ -6,8 +6,6 @@
enable
disable
DigicertCAPlugin
- 2.1.2
- 2.1.2
From 101128504d79f06f1fa28503892bd053a65594bf Mon Sep 17 00:00:00 2001
From: David Galey
Date: Thu, 10 Sep 2026 14:42:48 -0400
Subject: [PATCH 12/29] automated domain validation functionality
---
.../CertCentralCAPlugin.cs | 188 ++++++++++++++----
.../CertCentralConfig.cs | 2 +
.../Client/CertCentralClient.cs | 17 ++
digicert-certcentral-caplugin/Constants.cs | 2 +
.../digicert-certcentral-caplugin.csproj | 12 +-
5 files changed, 173 insertions(+), 48 deletions(-)
diff --git a/digicert-certcentral-caplugin/CertCentralCAPlugin.cs b/digicert-certcentral-caplugin/CertCentralCAPlugin.cs
index 176c2bd..3e0d5ee 100644
--- a/digicert-certcentral-caplugin/CertCentralCAPlugin.cs
+++ b/digicert-certcentral-caplugin/CertCentralCAPlugin.cs
@@ -20,6 +20,7 @@
using Org.BouncyCastle.Pqc.Crypto.Falcon;
using System.Collections.Concurrent;
+using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Security.AccessControl;
@@ -32,14 +33,15 @@ namespace Keyfactor.Extensions.CAPlugin.DigiCert
public class CertCentralCAPlugin : IAnyCAPlugin
{
private CertCentralConfig _config;
- private readonly ILogger _logger;
+ private readonly ILogger _logger = LogHandler.GetClassLogger();
private ICertificateDataReader _certificateDataReader;
+ private readonly IDomainValidatorFactory _domainValidatorFactory;
private Dictionary DCVTokens { get; } = new Dictionary();
- public CertCentralCAPlugin()
+ public CertCentralCAPlugin(IDomainValidatorFactory domainValidatorFactory)
{
- _logger = LogHandler.GetClassLogger();
+ _domainValidatorFactory = domainValidatorFactory;
}
public void Initialize(IAnyCAPluginConfigProvider configProvider, ICertificateDataReader certificateDataReader)
{
@@ -254,31 +256,26 @@ public async Task Enroll(string csr, string subject, Dictionar
string dcvMethod = "email";
- // AnyGateway Core does not currently support retreiving DCV tokens, the following code block can be uncommented once support is added.
-
- //if (productInfo.ProductParameters.TryGetValue(DigiCertConstants.RequestAttributes.DCV_METHOD, out string rawDCV))
- //{
- // Logger.Trace($"Parsing DCV method: {rawDCV}");
- // if (rawDCV.IndexOf("mail", StringComparison.OrdinalIgnoreCase) >= 0)
- // {
- // Logger.Trace("Selecting DCV method 'email'");
- // dcvMethod = "email";
- // }
- // else if (rawDCV.IndexOf("dns", StringComparison.OrdinalIgnoreCase) >= 0)
- // {
- // Logger.Trace("Selecting DCV method 'dns-txt-token'");
- // dcvMethod = "dns-txt-token";
- // }
- // else if (rawDCV.IndexOf("http", StringComparison.OrdinalIgnoreCase) >= 0)
- // {
- // Logger.Trace("Selecting DCV method 'http-token'");
- // dcvMethod = "http-token";
- // }
- // else
- // {
- // Logger.Warn($"Unexpected DCV method '{rawDCV}'. Falling back to default of 'email'");
- // }
- //}
+ if (string.Equals(_config.DnsValidationMethod, "email", StringComparison.OrdinalIgnoreCase))
+ {
+ _logger.LogTrace($"Selecting DCV method 'email'");
+ dcvMethod = "email";
+ }
+ else if (string.Equals(_config.DnsValidationMethod, "txt", StringComparison.OrdinalIgnoreCase))
+ {
+ _logger.LogTrace($"Selecting DCV method 'dns-txt-token'");
+ dcvMethod = "dns-txt-token";
+ }
+ else if (string.Equals(_config.DnsValidationMethod, "cname", StringComparison.OrdinalIgnoreCase))
+ {
+ _logger.LogTrace($"Selecting DCV method 'dns-cname-token'");
+ dcvMethod = "dns-cname-token";
+ }
+ else
+ {
+ _logger.LogWarning($"Unexpeted DCV method '{_config.DnsValidationMethod}'. Falling back to default of 'email'");
+ dcvMethod = "email";
+ }
orderRequest.DCVMethod = dcvMethod;
@@ -392,7 +389,7 @@ public async Task Enroll(string csr, string subject, Dictionar
if (dupe)
{
- return await Duplicate(client, productInfo, priorCertReqID, commonName, csr, dnsNames, signatureHash, caCertId);
+ return await Duplicate(client, productInfo, priorCertReqID, commonName, csr, dnsNames, signatureHash, caCertId, dcvMethod);
}
// Check if the order has more validity in it (multi-year cert). If so, do a reissue instead of a renew
@@ -439,13 +436,13 @@ public async Task Enroll(string csr, string subject, Dictionar
switch (enrollmentType)
{
case EnrollmentType.New:
- return await NewCertificate(client, orderRequest, commonName);
+ return await NewCertificate(client, orderRequest, commonName, dcvMethod);
case EnrollmentType.Reissue:
- return await Reissue(client, productInfo, priorCertReqID, commonName, csr, dnsNames, signatureHash, caCertId);
+ return await Reissue(client, productInfo, priorCertReqID, commonName, csr, dnsNames, signatureHash, caCertId, dcvMethod);
case EnrollmentType.Renew:
- return await Renew(client, orderRequest, productInfo, priorCertReqID, commonName);
+ return await Renew(client, orderRequest, productInfo, priorCertReqID, commonName, dcvMethod);
default:
throw new Exception($"The enrollment type '{enrollmentType}' is invalid for the DigiCert gateway.");
@@ -524,6 +521,26 @@ public Dictionary GetCAConnectorAnnotations()
DefaultValue = 30,
Type = "Number"
},
+ [CertCentralConstants.Config.DNS_VALIDATION_METHOD] = new PropertyConfigInfo()
+ {
+ Comments = "The DNS validation method to use. Default value is 'email'. Other valid values are 'txt' and 'cname' " +
+ "If using automated DNS validation, 'txt' is the preferred method.",
+ Hidden = false,
+ DefaultValue = "email",
+ Type = "String"
+ },
+ [CertCentralConstants.Config.DNS_VALIDATION_ENABLED] = new PropertyConfigInfo()
+ {
+ Comments = "Enable automated DNS (TXT or CNAME) domain control validation. When enabled, the plugin " +
+ "requests TXT-based validation from DigiCert and publishes the returned record via the " +
+ "DNS provider plugin resolved by the AnyCA Gateway. Requires a DNS provider plugin (e.g. Azure, " +
+ "Cloudflare, etc) to be deployed and configured on the gateway. When disabled, requests that require validation " +
+ "will be flagged as External Validation, and the validation token, if needed depending on the DNS Validation method, " +
+ "will be returned.",
+ Hidden = false,
+ DefaultValue = false,
+ Type = "Boolean"
+ },
[CertCentralConstants.Config.ENABLED] = new PropertyConfigInfo()
{
Comments = "Flag to Enable or Disable gateway functionality. Disabling is primarily used to allow creation of the CA prior to configuration information being available.",
@@ -1181,10 +1198,10 @@ public async Task ValidateProductInfo(EnrollmentProductInfo productInfo, Diction
/// The request to order a certificate.
/// The common name.
/// The containing the result of the enrollment request
- private async Task NewCertificate(CertCentralClient client, OrderRequest request, string commonName)
+ private async Task NewCertificate(CertCentralClient client, OrderRequest request, string commonName, string dcvMethod)
{
_logger.LogTrace("Attempting to enroll for a certificate.");
- return await ExtractEnrollmentResult(client, client.OrderCertificate(request), commonName);
+ return await ExtractEnrollmentResult(client, client.OrderCertificate(request), commonName, dcvMethod);
}
private async Task NewSmimeCertificate(CertCentralClient client, OrderSmimeRequest request)
@@ -1197,12 +1214,13 @@ private async Task NewSmimeCertificate(CertCentralClient clien
///
/// Gets the enrollment result from an object.
///
- private async Task ExtractEnrollmentResult(CertCentralClient client, OrderResponse orderResponse, string commonName)
+ private async Task ExtractEnrollmentResult(CertCentralClient client, OrderResponse orderResponse, string commonName, string dcvMethod)
{
int status = 0;
string statusMessage = null;
string certificate = null;
string caRequestID = null;
+ Dictionary context = new Dictionary();
if (orderResponse.Status == CertCentralBaseResponse.StatusType.ERROR)
{
@@ -1242,6 +1260,91 @@ private async Task ExtractEnrollmentResult(CertCentralClient c
_logger.LogTrace($"Certificate for order {orderResponse.OrderId} is being processed by DigiCert. Most likely a domain/organization requires further validation");
if (!string.IsNullOrEmpty(orderResponse.DCVRandomValue))
{
+ if (!_config.DnsValidationEnabled)
+ {
+ _logger.LogTrace($"Automated DNS validation not enabled. Returning DCV token in enrollment context");
+ context.Add(dcvMethod, orderResponse.DCVRandomValue);
+ }
+ else
+ {
+ if (_domainValidatorFactory == null)
+ {
+ _logger.LogError($"Automated DNS validation enabled by the AnyCA Gateway did not inject an IDomainValidatorFactory.");
+ throw new Exception($"DNS validation enabled but no DNS provider detected. Check your configuration");
+ }
+ string validType = "";
+ if (string.Equals(dcvMethod, "dns-txt-token"))
+ validType = "dns-01";
+ else if (string.Equals(dcvMethod, "dns-cname-token"))
+ validType = "cname";
+ else
+ throw new Exception($"For automated DNS validation, validation type must be either 'txt' or 'cname'");
+
+ List domains = new List();
+ domains.Add(certificateOrderResponse.certificate.common_name);
+ domains.AddRange(certificateOrderResponse.certificate.dns_names);
+
+ List errors = new List();
+ foreach (var dom in domains)
+ {
+ IDomainValidator validator;
+ try
+ {
+ validator = _domainValidatorFactory.ResolveDomainValidator(dom, validType);
+ }
+ catch (Exception ex)
+ {
+ errors.Add($"Failed to resolve DNS provider plugin for '{dom}' (validation type '{validType}'\nError: {ex.Message}");
+ continue;
+ }
+
+ DomainValidationResult result = null;
+ if (validType.Equals("dns-01"))
+ {
+ result = await validator.StageValidation(dom, orderResponse.DCVRandomValue, CancellationToken.None);
+ }
+ else
+ {
+ result = await validator.StageValidation("_dnsauth", $"{orderResponse.DCVRandomValue}.dcv.digicert.com", CancellationToken.None);
+ }
+
+ if (result == null || !result.Success)
+ {
+ var msg = result?.ErrorMessage ?? "unknown error";
+ errors.Add($"Failed to publish DNS validation record for '{dom}': {msg}");
+ }
+ else
+ {
+ _logger.LogInformation($"Published DNS validation record for '{dom}'");
+ }
+ }
+
+ var dcvcheck = client.DVCheckDCV(new DVCheckDCVRequest((int)orderID));
+ if (dcvcheck.Status == CertCentralBaseResponse.StatusType.ERROR)
+ {
+ if (errors.Count > 0)
+ {
+ _logger.LogError($"Domain Validation Errors:\n{string.Join('\n', errors)}");
+ string err = string.Join(';', errors);
+ statusMessage = err;
+ }
+ else
+ {
+ string msg = $"Domain validation(s) still pending. Certificate will be picked up on future sync.";
+ _logger.LogWarning(msg);
+ }
+ status = (int)EndEntityStatus.EXTERNALVALIDATION;
+ }
+ else
+ {
+ var certChain = client.GetCertificateChain(new CertificateChainRequest(orderResponse.CertificateId.Value.ToString()));
+ string certPem = certChain.Intermediates.SingleOrDefault(c => c.SubjectCommonName.Equals(commonName, StringComparison.OrdinalIgnoreCase))?.PEM;
+ certificate = certPem;
+ ViewCertificateOrderResponse newCertificateOrderResponse = client.ViewCertificateOrder(new ViewCertificateOrderRequest(orderID));
+
+ status = GetCertificateStatusFromCA(newCertificateOrderResponse.status, (int)orderID);
+ }
+ }
_logger.LogDebug($"Saving DCV token for order {orderResponse.OrderId}");
DCVTokens[orderResponse.OrderId] = orderResponse.DCVRandomValue;
}
@@ -1313,7 +1416,8 @@ private async Task ExtractEnrollmentResult(CertCentralClient c
CARequestID = caRequestID,
Certificate = certificate,
Status = status,
- StatusMessage = statusMessage
+ StatusMessage = statusMessage,
+ EnrollmentContext = context
};
}
@@ -1521,7 +1625,7 @@ private List GetDuplicates(CertCentralClient digiClient, int orderI
/// The .
/// Information about the DigiCert product this certificate uses.
///
- private async Task Reissue(CertCentralClient client, EnrollmentProductInfo enrollmentProductInfo, string caRequestId, string commonName, string csr, List dnsNames, string signatureHash, string caCertId)
+ private async Task Reissue(CertCentralClient client, EnrollmentProductInfo enrollmentProductInfo, string caRequestId, string commonName, string csr, List dnsNames, string signatureHash, string caCertId, string dcvMethod)
{
CheckProductExistence(enrollmentProductInfo.ProductID);
@@ -1553,7 +1657,7 @@ private async Task Reissue(CertCentralClient client, Enrollmen
};
_logger.LogTrace("Attempting to reissue certificate.");
- return await ExtractEnrollmentResult(client, client.ReissueCertificate(reissueRequest), commonName);
+ return await ExtractEnrollmentResult(client, client.ReissueCertificate(reissueRequest), commonName, dcvMethod);
}
///
@@ -1563,7 +1667,7 @@ private async Task Reissue(CertCentralClient client, Enrollmen
/// The .
/// Information about the DigiCert product this certificate uses.
///
- private async Task Duplicate(CertCentralClient client, EnrollmentProductInfo enrollmentProductInfo, string caRequestId, string commonName, string csr, List dnsNames, string signatureHash, string caCertId)
+ private async Task Duplicate(CertCentralClient client, EnrollmentProductInfo enrollmentProductInfo, string caRequestId, string commonName, string csr, List dnsNames, string signatureHash, string caCertId, string dcvMethod)
{
CheckProductExistence(enrollmentProductInfo.ProductID);
@@ -1593,7 +1697,7 @@ private async Task Duplicate(CertCentralClient client, Enrollm
};
_logger.LogTrace("Attempting to duplicate certificate.");
- return await ExtractEnrollmentResult(client, client.DuplicateCertificate(duplicateRequest), commonName);
+ return await ExtractEnrollmentResult(client, client.DuplicateCertificate(duplicateRequest), commonName, dcvMethod);
}
///
@@ -1619,7 +1723,7 @@ private void CheckProductExistence(string productId)
/// The .
/// Information about the DigiCert product this certificate uses.
///
- private async Task Renew(CertCentralClient client, OrderRequest request, EnrollmentProductInfo enrollmentProductInfo, string caRequestId, string commonName)
+ private async Task Renew(CertCentralClient client, OrderRequest request, EnrollmentProductInfo enrollmentProductInfo, string caRequestId, string commonName, string dcvMethod)
{
CheckProductExistence(enrollmentProductInfo.ProductID);
@@ -1637,7 +1741,7 @@ private async Task Renew(CertCentralClient client, OrderReques
request.RenewalOfOrderId = orderId;
_logger.LogTrace($"Attempting to renew certificate with order id {orderId}.");
- return await ExtractEnrollmentResult(client, client.OrderCertificate(request), commonName);
+ return await ExtractEnrollmentResult(client, client.OrderCertificate(request), commonName, dcvMethod);
}
private async Task RenewSmime(CertCentralClient client, OrderSmimeRequest request, EnrollmentProductInfo enrollmentProductInfo, string caRequestId)
diff --git a/digicert-certcentral-caplugin/CertCentralConfig.cs b/digicert-certcentral-caplugin/CertCentralConfig.cs
index 6909c2e..eba73a6 100644
--- a/digicert-certcentral-caplugin/CertCentralConfig.cs
+++ b/digicert-certcentral-caplugin/CertCentralConfig.cs
@@ -52,5 +52,7 @@ public List SyncProducts
public bool? FilterExpiredOrders { get; set; }
public int? SyncExpirationDays { get; set; }
public string SyncDivisionFilter { get; set; }
+ public bool DnsValidationEnabled { get; set; }
+ public string DnsValidationMethod { get; set; }
}
}
diff --git a/digicert-certcentral-caplugin/Client/CertCentralClient.cs b/digicert-certcentral-caplugin/Client/CertCentralClient.cs
index 753f54f..5ddcf8f 100644
--- a/digicert-certcentral-caplugin/Client/CertCentralClient.cs
+++ b/digicert-certcentral-caplugin/Client/CertCentralClient.cs
@@ -161,6 +161,23 @@ private CertCentralResponse Request(CertCentralBaseRequest request, string param
return oCertCertResponse;
}
+ public DVCheckDCVResponse CheckDCV(DVCheckDCVRequest request)
+ {
+ CertCentralResponse response = Request(request);
+
+ DVCheckDCVResponse checkDCVResponse = new DVCheckDCVResponse();
+ if (!response.Success)
+ {
+ Errors errors = JsonConvert.DeserializeObject(response.Response);
+ checkDCVResponse.Status = CertCentralBaseResponse.StatusType.ERROR;
+ checkDCVResponse.Errors = errors.errors;
+ }
+ else
+ {
+ checkDCVResponse = JsonConvert.DeserializeObject(response.Response);
+ }
+ return checkDCVResponse;
+ }
public ListOrganizationsResponse ListOrganizations(ListOrganizationsRequest request)
{
CertCentralResponse response = Request(request, request.BuildParameters());
diff --git a/digicert-certcentral-caplugin/Constants.cs b/digicert-certcentral-caplugin/Constants.cs
index a01d8ca..c729882 100644
--- a/digicert-certcentral-caplugin/Constants.cs
+++ b/digicert-certcentral-caplugin/Constants.cs
@@ -44,6 +44,8 @@ public class Config
public const string LAST_NAME = "LastName";
public const string PSEUDONYM = "Pseudonym";
public const string SMIME_USAGE = "UsageDesignation";
+ public const string DNS_VALIDATION_METHOD = "DnsValidationMethod";
+ public const string DNS_VALIDATION_ENABLED = "DnsValidationEnabled";
}
public class RequestAttributes
diff --git a/digicert-certcentral-caplugin/digicert-certcentral-caplugin.csproj b/digicert-certcentral-caplugin/digicert-certcentral-caplugin.csproj
index 7510b07..6828af9 100644
--- a/digicert-certcentral-caplugin/digicert-certcentral-caplugin.csproj
+++ b/digicert-certcentral-caplugin/digicert-certcentral-caplugin.csproj
@@ -1,7 +1,7 @@
- net6.0;net8.0
+ net10.0
Keyfactor.Extensions.CAPlugin.DigiCert
enable
disable
@@ -11,11 +11,11 @@
-
-
-
-
-
+
+
+
+
+
From 948abdfde337fa260fbf1583a568b45441dedf1b Mon Sep 17 00:00:00 2001
From: David Galey
Date: Fri, 18 Sep 2026 15:16:05 -0400
Subject: [PATCH 13/29] update integration manifest
---
integration-manifest.json | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/integration-manifest.json b/integration-manifest.json
index a30cc75..4f01696 100644
--- a/integration-manifest.json
+++ b/integration-manifest.json
@@ -8,7 +8,7 @@
"update_catalog": true,
"description": "DigiCert CertCentral plugin for the AnyCA REST Gateway framework",
"gateway_framework": "24.2.0",
- "release_dir": "digicert-certcentral-caplugin/bin/Release",
+ "release_dir": "digicert-certcentral-caplugin/bin/Release/10.0",
"release_project": "digicert-certcentral-caplugin/digicert-certcentral-caplugin.csproj",
"about": {
"carest": {
From 919526169299b0b79e1210b63747cef0e8d23d21 Mon Sep 17 00:00:00 2001
From: David Galey
Date: Fri, 18 Sep 2026 15:18:34 -0400
Subject: [PATCH 14/29] update gateway framework required version
---
integration-manifest.json | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/integration-manifest.json b/integration-manifest.json
index 4f01696..a7e2ccf 100644
--- a/integration-manifest.json
+++ b/integration-manifest.json
@@ -7,7 +7,7 @@
"link_github": true,
"update_catalog": true,
"description": "DigiCert CertCentral plugin for the AnyCA REST Gateway framework",
- "gateway_framework": "24.2.0",
+ "gateway_framework": "26.2.0",
"release_dir": "digicert-certcentral-caplugin/bin/Release/10.0",
"release_project": "digicert-certcentral-caplugin/digicert-certcentral-caplugin.csproj",
"about": {
From e5279ef834cf3938df495f765a41f692df8c7cc7 Mon Sep 17 00:00:00 2001
From: David Galey
Date: Fri, 18 Sep 2026 15:21:31 -0400
Subject: [PATCH 15/29] fix release dir
---
integration-manifest.json | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/integration-manifest.json b/integration-manifest.json
index a7e2ccf..4e1cf1e 100644
--- a/integration-manifest.json
+++ b/integration-manifest.json
@@ -8,7 +8,7 @@
"update_catalog": true,
"description": "DigiCert CertCentral plugin for the AnyCA REST Gateway framework",
"gateway_framework": "26.2.0",
- "release_dir": "digicert-certcentral-caplugin/bin/Release/10.0",
+ "release_dir": "digicert-certcentral-caplugin/bin/Release",
"release_project": "digicert-certcentral-caplugin/digicert-certcentral-caplugin.csproj",
"about": {
"carest": {
From 89066ddb620e4201ffea4c018f6f12eb3d8902d9 Mon Sep 17 00:00:00 2001
From: Morgan Gangwere <470584+indrora@users.noreply.github.com>
Date: Fri, 18 Sep 2026 12:35:54 -0700
Subject: [PATCH 16/29] Update digicert-certcentral-caplugin.csproj
---
.../digicert-certcentral-caplugin.csproj | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/digicert-certcentral-caplugin/digicert-certcentral-caplugin.csproj b/digicert-certcentral-caplugin/digicert-certcentral-caplugin.csproj
index 6828af9..261f0d2 100644
--- a/digicert-certcentral-caplugin/digicert-certcentral-caplugin.csproj
+++ b/digicert-certcentral-caplugin/digicert-certcentral-caplugin.csproj
@@ -1,7 +1,7 @@
- net10.0
+ net8.0;net10.0
Keyfactor.Extensions.CAPlugin.DigiCert
enable
disable
From 6f3d9909f823a144103227e78dde493630de198c Mon Sep 17 00:00:00 2001
From: Keyfactor
Date: Fri, 18 Sep 2026 19:37:48 +0000
Subject: [PATCH 17/29] Update generated docs
---
README.md | 4 +++-
integration-manifest.json | 8 ++++++++
2 files changed, 11 insertions(+), 1 deletion(-)
diff --git a/README.md b/README.md
index 6f735fc..7e781ae 100644
--- a/README.md
+++ b/README.md
@@ -41,7 +41,7 @@ The Digicert CertCentral AnyCA REST plugin extends the capabilities of Digicert'
## Compatibility
-The DigiCert CertCentral AnyCA Gateway REST plugin is compatible with the Keyfactor AnyCA Gateway REST 24.2.0 and later.
+The DigiCert CertCentral AnyCA Gateway REST plugin is compatible with the Keyfactor AnyCA Gateway REST 26.2.0 and later.
## Support
The DigiCert CertCentral 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.
@@ -94,6 +94,8 @@ An API Key within your Digicert account that has the necessary permissions to en
* **SyncProductFilter** - If you list one or more Product IDs here (comma-separated), the sync process will filter records to only return orders of those product types. Leave empty to sync all products.
* **FilterExpiredOrders** - If set to 'true', syncing will apply a filter to not return orders that are expired for longer than specified in SyncExpirationDays.
* **SyncExpirationDays** - If FilterExpiredOrders is set to true, this setting determines how many days in the past to still return expired orders. For example, a value of 30 means the sync will return any certs that expired within the past 30 days. A value of 0 means the sync will not return any certs that expired before the current day. This value is ignored if FilterExpiredOrders is false.
+ * **DnsValidationMethod** - The DNS validation method to use. Default value is 'email'. Other valid values are 'txt' and 'cname' If using automated DNS validation, 'txt' is the preferred method.
+ * **DnsValidationEnabled** - Enable automated DNS (TXT or CNAME) domain control validation. When enabled, the plugin requests TXT-based validation from DigiCert and publishes the returned record via the DNS provider plugin resolved by the AnyCA Gateway. Requires a DNS provider plugin (e.g. Azure, Cloudflare, etc) to be deployed and configured on the gateway. When disabled, requests that require validation will be flagged as External Validation, and the validation token, if needed depending on the DNS Validation method, will be returned.
* **Enabled** - Flag to Enable or Disable gateway functionality. Disabling is primarily used to allow creation of the CA prior to configuration information being available.
2. Note for SMIME product types (Secure Email types): The template configuration fields provided for those are not required to be filled out in the gateway config. Many of those values would change on a per-enrollment basis. The way to handle that is to create Enrollment fields in Command with the same name (for example: CommonNameIndicator) and then any values populated in those fields will override any static values provided in the configuration.
diff --git a/integration-manifest.json b/integration-manifest.json
index 4e1cf1e..3ef42f7 100644
--- a/integration-manifest.json
+++ b/integration-manifest.json
@@ -50,6 +50,14 @@
"name": "SyncExpirationDays",
"description": "If FilterExpiredOrders is set to true, this setting determines how many days in the past to still return expired orders. For example, a value of 30 means the sync will return any certs that expired within the past 30 days. A value of 0 means the sync will not return any certs that expired before the current day. This value is ignored if FilterExpiredOrders is false."
},
+ {
+ "name": "DnsValidationMethod",
+ "description": "The DNS validation method to use. Default value is 'email'. Other valid values are 'txt' and 'cname' If using automated DNS validation, 'txt' is the preferred method."
+ },
+ {
+ "name": "DnsValidationEnabled",
+ "description": "Enable automated DNS (TXT or CNAME) domain control validation. When enabled, the plugin requests TXT-based validation from DigiCert and publishes the returned record via the DNS provider plugin resolved by the AnyCA Gateway. Requires a DNS provider plugin (e.g. Azure, Cloudflare, etc) to be deployed and configured on the gateway. When disabled, requests that require validation will be flagged as External Validation, and the validation token, if needed depending on the DNS Validation method, will be returned."
+ },
{
"name": "Enabled",
"description": "Flag to Enable or Disable gateway functionality. Disabling is primarily used to allow creation of the CA prior to configuration information being available."
From 18b708303dc3a71ba79412362a32bbec52b6ae48 Mon Sep 17 00:00:00 2001
From: Dave Galey <89407235+dgaley@users.noreply.github.com>
Date: Fri, 18 Sep 2026 22:34:13 -0400
Subject: [PATCH 18/29] Update keyfactor-bootstrap-workflow.yml
---
.github/workflows/keyfactor-bootstrap-workflow.yml | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/.github/workflows/keyfactor-bootstrap-workflow.yml b/.github/workflows/keyfactor-bootstrap-workflow.yml
index 722cb97..6e592a3 100644
--- a/.github/workflows/keyfactor-bootstrap-workflow.yml
+++ b/.github/workflows/keyfactor-bootstrap-workflow.yml
@@ -11,7 +11,7 @@ on:
jobs:
call-starter-workflow:
- uses: keyfactor/actions/.github/workflows/starter.yml@v4
+ uses: keyfactor/actions/.github/workflows/starter.yml@v5
secrets:
token: ${{ secrets.V2BUILDTOKEN}}
scan_token: ${{ secrets.SAST_TOKEN }}
From ce4141a4d08d233627de23a24483bc67f20b4500 Mon Sep 17 00:00:00 2001
From: "github-actions[bot]"
Date: Sat, 19 Sep 2026 02:34:51 +0000
Subject: [PATCH 19/29] docs: auto-generate README and documentation [skip ci]
---
README.md | 69 ++++++++++++++++++++++++++-----------------------------
1 file changed, 33 insertions(+), 36 deletions(-)
diff --git a/README.md b/README.md
index 7e781ae..39e3518 100644
--- a/README.md
+++ b/README.md
@@ -14,7 +14,7 @@
Support
-
+
·
Requirements
@@ -33,7 +33,6 @@
-
The Digicert CertCentral AnyCA REST plugin extends the capabilities of Digicert's CertCentral product to Keyfactor Command via the Keyfactor AnyCA Gateway REST. The plugin represents a fully featured AnyCA REST Plugin with the following capabilies:
* SSL Certificate Synchronization
* SSL Certificate Enrollment
@@ -44,7 +43,7 @@ The Digicert CertCentral AnyCA REST plugin extends the capabilities of Digicert'
The DigiCert CertCentral AnyCA Gateway REST plugin is compatible with the Keyfactor AnyCA Gateway REST 26.2.0 and later.
## Support
-The DigiCert CertCentral 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 DigiCert CertCentral AnyCA Gateway REST plugin is supported by Keyfactor for Keyfactor customers. If you have a support issue, please open a support ticket via the Keyfactor Support Portal at https://support.keyfactor.com.
> To report a problem or suggest a new feature, use the **[Issues](../../issues)** tab. If you want to contribute actual bug fixes or proposed enhancements, use the **[Pull requests](../../pulls)** tab.
@@ -58,16 +57,16 @@ An API Key within your Digicert account that has the necessary permissions to en
2. On the server hosting the AnyCA Gateway REST, download and unzip the latest [DigiCert CertCentral AnyCA Gateway REST plugin](https://github.com/Keyfactor/digicert-certcentral-caplugin/releases/latest) from GitHub.
-3. Copy the unzipped directory (usually called `net6.0` or `net8.0`) to the Extensions directory:
+3. Copy the unzipped directory (usually called `net8.0` or `net10.0`) to the Extensions directory:
```shell
Depending on your AnyCA Gateway REST version, copy the unzipped directory to one of the following locations:
- Program Files\Keyfactor\AnyCA Gateway\AnyGatewayREST\net6.0\Extensions
Program Files\Keyfactor\AnyCA Gateway\AnyGatewayREST\net8.0\Extensions
+ Program Files\Keyfactor\AnyCA Gateway\AnyGatewayREST\net10.0\Extensions
```
- > The directory containing the DigiCert CertCentral 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 DigiCert CertCentral AnyCA Gateway REST plugin DLLs (`net8.0` or `net10.0`) can be named anything, as long as it is unique within the `Extensions` directory.
4. Restart the AnyCA Gateway REST service.
@@ -85,18 +84,18 @@ An API Key within your Digicert account that has the necessary permissions to en
Populate using the configuration fields collected in the [requirements](#requirements) section.
- * **APIKey** - API Key for connecting to DigiCert
- * **DivisionId** - Division ID to use for retrieving product details (only if account is configured with per-divison product settings)
- * **Region** - The geographic region that your DigiCert CertCentral account is in. Valid options are US and EU.
- * **RevokeCertificateOnly** - Default DigiCert behavior on revocation requests is to revoke the entire order. If this value is changed to 'true', revocation requests will instead just revoke the individual certificate.
- * **SyncCAFilter** - If you list one or more CA IDs here (comma-separated), the sync process will only sync records from those CAs. If you want to sync all CA IDs, leave this field empty.
- * **SyncDivisionFilter** - If you list one or more Divison IDs (also known as Container IDs) here (comma-separated), the sync process will filter records to only return orders from those divisions. If you want to sync all divisions, leave this field empty. Note that this has no relationship to the value of the DivisionId config field.
- * **SyncProductFilter** - If you list one or more Product IDs here (comma-separated), the sync process will filter records to only return orders of those product types. Leave empty to sync all products.
- * **FilterExpiredOrders** - If set to 'true', syncing will apply a filter to not return orders that are expired for longer than specified in SyncExpirationDays.
- * **SyncExpirationDays** - If FilterExpiredOrders is set to true, this setting determines how many days in the past to still return expired orders. For example, a value of 30 means the sync will return any certs that expired within the past 30 days. A value of 0 means the sync will not return any certs that expired before the current day. This value is ignored if FilterExpiredOrders is false.
- * **DnsValidationMethod** - The DNS validation method to use. Default value is 'email'. Other valid values are 'txt' and 'cname' If using automated DNS validation, 'txt' is the preferred method.
- * **DnsValidationEnabled** - Enable automated DNS (TXT or CNAME) domain control validation. When enabled, the plugin requests TXT-based validation from DigiCert and publishes the returned record via the DNS provider plugin resolved by the AnyCA Gateway. Requires a DNS provider plugin (e.g. Azure, Cloudflare, etc) to be deployed and configured on the gateway. When disabled, requests that require validation will be flagged as External Validation, and the validation token, if needed depending on the DNS Validation method, will be returned.
- * **Enabled** - Flag to Enable or Disable gateway functionality. Disabling is primarily used to allow creation of the CA prior to configuration information being available.
+ * **APIKey** - API Key for connecting to DigiCert
+ * **DivisionId** - Division ID to use for retrieving product details (only if account is configured with per-divison product settings)
+ * **Region** - The geographic region that your DigiCert CertCentral account is in. Valid options are US and EU.
+ * **RevokeCertificateOnly** - Default DigiCert behavior on revocation requests is to revoke the entire order. If this value is changed to 'true', revocation requests will instead just revoke the individual certificate.
+ * **SyncCAFilter** - If you list one or more CA IDs here (comma-separated), the sync process will only sync records from those CAs. If you want to sync all CA IDs, leave this field empty.
+ * **SyncDivisionFilter** - If you list one or more Divison IDs (also known as Container IDs) here (comma-separated), the sync process will filter records to only return orders from those divisions. If you want to sync all divisions, leave this field empty. Note that this has no relationship to the value of the DivisionId config field.
+ * **SyncProductFilter** - If you list one or more Product IDs here (comma-separated), the sync process will filter records to only return orders of those product types. Leave empty to sync all products.
+ * **FilterExpiredOrders** - If set to 'true', syncing will apply a filter to not return orders that are expired for longer than specified in SyncExpirationDays.
+ * **SyncExpirationDays** - If FilterExpiredOrders is set to true, this setting determines how many days in the past to still return expired orders. For example, a value of 30 means the sync will return any certs that expired within the past 30 days. A value of 0 means the sync will not return any certs that expired before the current day. This value is ignored if FilterExpiredOrders is false.
+ * **DnsValidationMethod** - The DNS validation method to use. Default value is 'email'. Other valid values are 'txt' and 'cname' If using automated DNS validation, 'txt' is the preferred method.
+ * **DnsValidationEnabled** - Enable automated DNS (TXT or CNAME) domain control validation. When enabled, the plugin requests TXT-based validation from DigiCert and publishes the returned record via the DNS provider plugin resolved by the AnyCA Gateway. Requires a DNS provider plugin (e.g. Azure, Cloudflare, etc) to be deployed and configured on the gateway. When disabled, requests that require validation will be flagged as External Validation, and the validation token, if needed depending on the DNS Validation method, will be returned.
+ * **Enabled** - Flag to Enable or Disable gateway functionality. Disabling is primarily used to allow creation of the CA prior to configuration information being available.
2. Note for SMIME product types (Secure Email types): The template configuration fields provided for those are not required to be filled out in the gateway config. Many of those values would change on a per-enrollment basis. The way to handle that is to create Enrollment fields in Command with the same name (for example: CommonNameIndicator) and then any values populated in those fields will override any static values provided in the configuration.
@@ -104,32 +103,30 @@ An API Key within your Digicert account that has the necessary permissions to en
4. In Keyfactor Command (v12.3+), for each imported Certificate Template, follow the [official documentation](https://software.keyfactor.com/Core-OnPrem/Current/Content/ReferenceGuide/Configuring%20Template%20Options.htm) to define enrollment fields for each of the following parameters:
- * **LifetimeDays** - OPTIONAL: The number of days of validity to use when requesting certs. If not provided, default is 365.
- * **CACertId** - OPTIONAL: ID of issuing CA to use by DigiCert. If not provided, the default for your account will be used.
- * **Organization-Name** - OPTIONAL: For requests that will not have a subject (such as ACME) you can use this field to provide the organization name. Value supplied here will override any CSR values, so do not include this field if you want the organization from the CSR to be used.
- * **RenewalWindowDays** - OPTIONAL: The number of days from certificate expiration that the gateway should do a renewal rather than a reissue. If not provided, default is 90.
- * **CertType** - OPTIONAL: The type of cert to enroll for. Valid values are 'ssl' and 'client'. The value provided here must be consistant with the ProductID. If not provided, default is 'ssl'. Ignored for secure_email_* product types.
- * **IncludeClientAuthEKU** - OPTIONAL for SSL certs, ignored otherwise. If set to 'true', SSL certs enrolled under this template will have the Client Authentication EKU added to the request. NOTE: Only one EKU option can be set for any given enrollment. NOTE: This feature is currently planned to be removed by DigiCert in March 2027.
- * **IncludeKDCSmartCardLogonEKU** - OPTIONAL for SSL certs, ignored otherwise. If set to 'true', SSL certs enrolled under this template will have the KDC/SmartCardLogon EKU added to the request. NOTE: Only one EKU option can be set for any given enrollment.
- * **IncludeIntelvProEKU** - OPTIONAL for SSL certs, ignored otherwise. If set to 'true', SSL certs enrolled under this template will have the Intel vPro EKU added to the request. NOTE: Only one EKU option can be set for any given enrollment.
- * **EnrollDivisionId** - OPTIONAL: The division (container) ID to use for enrollments against this template.
- * **CommonNameIndicator** - Required for secure_email_sponsor and secure_email_organization products, ignored otherwise. Defines the source of the common name. Valid values are: email_address, given_name_surname, pseudonym, organization_name
- * **ProfileType** - Optional for secure_email_* types, ignored otherwise. Valid values are: strict, multipurpose. Use 'multipurpose' if your cert includes any additional EKUs such as client auth. Default if not provided is dependent on product configuration within Digicert portal.
- * **FirstName** - Required for secure_email_* types if CommonNameIndicator is given_name_surname, ignored otherwise.
- * **LastName** - Required for secure_email_* types if CommonNameIndicator is given_name_surname, ignored otherwise.
- * **Pseudonym** - Required for secure_email_* types if CommonNameIndicator is pseudonym, ignored otherwise.
- * **UsageDesignation** - Required for secure_email_* types, ignored otherwise. The primary usage of the certificate. Valid values are: signing, key_management, dual_use
-
+ * **LifetimeDays** - OPTIONAL: The number of days of validity to use when requesting certs. If not provided, default is 365.
+ * **CACertId** - OPTIONAL: ID of issuing CA to use by DigiCert. If not provided, the default for your account will be used.
+ * **Organization-Name** - OPTIONAL: For requests that will not have a subject (such as ACME) you can use this field to provide the organization name. Value supplied here will override any CSR values, so do not include this field if you want the organization from the CSR to be used.
+ * **RenewalWindowDays** - OPTIONAL: The number of days from certificate expiration that the gateway should do a renewal rather than a reissue. If not provided, default is 90.
+ * **CertType** - OPTIONAL: The type of cert to enroll for. Valid values are 'ssl' and 'client'. The value provided here must be consistant with the ProductID. If not provided, default is 'ssl'. Ignored for secure_email_* product types.
+ * **IncludeClientAuthEKU** - OPTIONAL for SSL certs, ignored otherwise. If set to 'true', SSL certs enrolled under this template will have the Client Authentication EKU added to the request. NOTE: Only one EKU option can be set for any given enrollment. NOTE: This feature is currently planned to be removed by DigiCert in March 2027.
+ * **IncludeKDCSmartCardLogonEKU** - OPTIONAL for SSL certs, ignored otherwise. If set to 'true', SSL certs enrolled under this template will have the KDC/SmartCardLogon EKU added to the request. NOTE: Only one EKU option can be set for any given enrollment.
+ * **IncludeIntelvProEKU** - OPTIONAL for SSL certs, ignored otherwise. If set to 'true', SSL certs enrolled under this template will have the Intel vPro EKU added to the request. NOTE: Only one EKU option can be set for any given enrollment.
+ * **EnrollDivisionId** - OPTIONAL: The division (container) ID to use for enrollments against this template.
+ * **CommonNameIndicator** - Required for secure_email_sponsor and secure_email_organization products, ignored otherwise. Defines the source of the common name. Valid values are: email_address, given_name_surname, pseudonym, organization_name
+ * **ProfileType** - Optional for secure_email_* types, ignored otherwise. Valid values are: strict, multipurpose. Use 'multipurpose' if your cert includes any additional EKUs such as client auth. Default if not provided is dependent on product configuration within Digicert portal.
+ * **FirstName** - Required for secure_email_* types if CommonNameIndicator is given_name_surname, ignored otherwise.
+ * **LastName** - Required for secure_email_* types if CommonNameIndicator is given_name_surname, ignored otherwise.
+ * **Pseudonym** - Required for secure_email_* types if CommonNameIndicator is pseudonym, ignored otherwise.
+ * **UsageDesignation** - Required for secure_email_* types, ignored otherwise. The primary usage of the certificate. Valid values are: signing, key_management, dual_use
## Certificate Duplicates
DigiCert supports the ability to duplicate existing certificate orders. To take advantage of this functionality, in Keyfactor Command, under the enrollment pattern you're using, create an Enrollment Field named 'Duplicate' of type Multiple Choice, and the values 'False', 'True'. When performing a renew operation against that enrollment pattern, set the value to True to tell the gateway to duplicate instead of renew. The field will be ignored on new enrollments.
-
## License
Apache License 2.0, see [LICENSE](LICENSE).
## Related Integrations
-See all [Keyfactor Any CA Gateways (REST)](https://github.com/orgs/Keyfactor/repositories?q=anycagateway).
\ No newline at end of file
+See all [Keyfactor Any CA Gateways (REST)](https://github.com/orgs/Keyfactor/repositories?q=anycagateway).
From 3184808ea6bb32305a688a9d3ac095ae0ce63c5f Mon Sep 17 00:00:00 2001
From: David Galey
Date: Tue, 22 Sep 2026 15:14:44 -0400
Subject: [PATCH 20/29] properly check if cert is DV to ignore org check
---
CHANGELOG.md | 3 +++
.../CertCentralCAPlugin.cs | 16 +++++++++++++++-
2 files changed, 18 insertions(+), 1 deletion(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index a571c73..437bc33 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -39,3 +39,6 @@
### 2.4.1
* Fix for missing parameter errors
+
+### 2.5.0
+* Fix for checking if product is a DV cert to ignore organization checks
diff --git a/digicert-certcentral-caplugin/CertCentralCAPlugin.cs b/digicert-certcentral-caplugin/CertCentralCAPlugin.cs
index 176c2bd..8f4eb85 100644
--- a/digicert-certcentral-caplugin/CertCentralCAPlugin.cs
+++ b/digicert-certcentral-caplugin/CertCentralCAPlugin.cs
@@ -174,7 +174,21 @@ public async Task Enroll(string csr, string subject, Dictionar
CertCentralClient client = CertCentralClientUtilities.BuildCertCentralClient(_config);
int? organizationId = null;
// DV certs have no organization, so only do the org check if its a non-DV cert
- if (!string.Equals(productInfo.ProductID, CertCentralConstants.ProductTypes.DV_SSL_CERT, StringComparison.OrdinalIgnoreCase))
+
+ // Get product ID details.
+ CertificateTypeDetailsRequest detailsRequest = new CertificateTypeDetailsRequest(productInfo.ProductID);
+
+ // For pulling product ID details, we use the Connection-level Division ID rather than the template-level one.
+ detailsRequest.ContainerId = null;
+ if (_config.DivisionId.HasValue)
+ {
+ detailsRequest.ContainerId = _config.DivisionId.Value;
+ }
+
+ CertificateTypeDetailsResponse details = client.GetCertificateTypeDetails(detailsRequest);
+
+ // Only do org check if the product type is NOT the group dv_ssl_certificate (https://dev.digicert.com/certcentral-apis/services-api/glossary.html#product-identifiers)
+ if (!string.Equals(details.GroupName, CertCentralConstants.ProductTypes.DV_SSL_CERT, StringComparison.OrdinalIgnoreCase))
{
if (organization == null)
{
From dc2f8947222de74e375c89ca4f4f7ecbc107faa7 Mon Sep 17 00:00:00 2001
From: David Galey
Date: Tue, 22 Sep 2026 15:26:24 -0400
Subject: [PATCH 21/29] avoid duplicate API calls on sync
---
CHANGELOG.md | 1 +
digicert-certcentral-caplugin/CertCentralCAPlugin.cs | 10 ++++++----
2 files changed, 7 insertions(+), 4 deletions(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 437bc33..c338b40 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -42,3 +42,4 @@
### 2.5.0
* Fix for checking if product is a DV cert to ignore organization checks
+* Performance enhancements to reduce API calls
diff --git a/digicert-certcentral-caplugin/CertCentralCAPlugin.cs b/digicert-certcentral-caplugin/CertCentralCAPlugin.cs
index 8f4eb85..c6bf494 100644
--- a/digicert-certcentral-caplugin/CertCentralCAPlugin.cs
+++ b/digicert-certcentral-caplugin/CertCentralCAPlugin.cs
@@ -599,7 +599,7 @@ public async Task GetSingleRecord(string caRequestID)
CertCentralClient client = CertCentralClientUtilities.BuildCertCentralClient(_config);
ViewCertificateOrderResponse orderResponse = client.ViewCertificateOrder(new ViewCertificateOrderRequest((uint)orderId));
- var orderCerts = GetAllCertsForOrder(orderId);
+ var orderCerts = GetAllCertsForOrder(orderId, orderResponse);
StatusOrder certToCheck = orderCerts.Where(c => c.certificate_id == certIdInt).First();
@@ -1715,7 +1715,7 @@ private List GetAllConnectorCertsForOrder(string caReque
_logger.LogTrace($"Found order ID {orderId} that does not match Product filter. Product ID: {orderResponse.product.name_id.ToString()} Skipping...");
}
- var orderCerts = GetAllCertsForOrder(orderId);
+ var orderCerts = GetAllCertsForOrder(orderId, orderResponse);
List certList = new List();
List pemList = new List();
@@ -1771,10 +1771,12 @@ private List GetAllConnectorCertsForOrder(string caReque
///
///
///
- private List GetAllCertsForOrder(int orderId)
+ private List GetAllCertsForOrder(int orderId, ViewCertificateOrderResponse existingOrderResponse = null)
{
CertCentralClient client = CertCentralClientUtilities.BuildCertCentralClient(_config);
- ViewCertificateOrderResponse orderResponse = client.ViewCertificateOrder(new ViewCertificateOrderRequest((uint)orderId));
+
+ // If the caller provides an existing order response, reuse that to save the API call.
+ ViewCertificateOrderResponse orderResponse = existingOrderResponse ?? client.ViewCertificateOrder(new ViewCertificateOrderRequest((uint)orderId));
if (orderResponse.Status == CertCentralBaseResponse.StatusType.ERROR)
{
string errorMessage = String.Format("Request {0} was not found in CertCentral database or is not valid", orderId);
From 292d63041d11b15d6bb9d33c5a0810f6e0a30f93 Mon Sep 17 00:00:00 2001
From: David Galey
Date: Tue, 22 Sep 2026 15:34:13 -0400
Subject: [PATCH 22/29] error handling on sync to catch bad certs
---
CHANGELOG.md | 1 +
digicert-certcentral-caplugin/CertCentralCAPlugin.cs | 11 +++++++++--
2 files changed, 10 insertions(+), 2 deletions(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index c338b40..af1befe 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -43,3 +43,4 @@
### 2.5.0
* Fix for checking if product is a DV cert to ignore organization checks
* Performance enhancements to reduce API calls
+* Added null checks/error handling to catch bad certs on sync
diff --git a/digicert-certcentral-caplugin/CertCentralCAPlugin.cs b/digicert-certcentral-caplugin/CertCentralCAPlugin.cs
index c6bf494..425aedb 100644
--- a/digicert-certcentral-caplugin/CertCentralCAPlugin.cs
+++ b/digicert-certcentral-caplugin/CertCentralCAPlugin.cs
@@ -1733,6 +1733,10 @@ private List GetAllConnectorCertsForOrder(string caReque
CertificateChainResponse certificateChainResponse = client.GetCertificateChain(new CertificateChainRequest($"{cert.certificate_id}"));
if (certificateChainResponse.Status == CertCentralBaseResponse.StatusType.SUCCESS)
{
+ if (certificateChainResponse.Intermediates == null || certificateChainResponse.Intermediates.Count == 0)
+ {
+ throw new Exception($"DigiCert returned an empty certificate chain for certificate {cert.certificate_id} on order {orderId}.");
+ }
certificate = certificateChainResponse.Intermediates[0].PEM;
}
else
@@ -1741,12 +1745,15 @@ private List GetAllConnectorCertsForOrder(string caReque
}
}
//Another check for duplicate PEMs to get arround issue with DigiCert API returning incorrect data sometimes on reissued/duplicate certs
- if (pemList.Contains(certificate))
+ if (certificate != null && pemList.Contains(certificate))
{
_logger.LogWarning($"Found duplicate PEM for ID {caReqId}. Skipping...");
continue;
}
- pemList.Add(certificate);
+ if (certificate != null)
+ {
+ pemList.Add(certificate);
+ }
var connCert = new AnyCAPluginCertificate
{
CARequestID = caReqId,
From 98d0ccdbf1f095258e4ace48e6e0ac9bec010357 Mon Sep 17 00:00:00 2001
From: David Galey
Date: Tue, 22 Sep 2026 15:44:24 -0400
Subject: [PATCH 23/29] rate limit handling
---
CHANGELOG.md | 3 +-
.../Client/CertCentralClient.cs | 29 ++++++++++++++++---
2 files changed, 27 insertions(+), 5 deletions(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index af1befe..03af54e 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -43,4 +43,5 @@
### 2.5.0
* Fix for checking if product is a DV cert to ignore organization checks
* Performance enhancements to reduce API calls
-* Added null checks/error handling to catch bad certs on sync
+* Added null checks/error handling to catch bad certs on sync
+* Improved rate limiting handling based on DigiCert guidance
diff --git a/digicert-certcentral-caplugin/Client/CertCentralClient.cs b/digicert-certcentral-caplugin/Client/CertCentralClient.cs
index 753f54f..9f67ed9 100644
--- a/digicert-certcentral-caplugin/Client/CertCentralClient.cs
+++ b/digicert-certcentral-caplugin/Client/CertCentralClient.cs
@@ -72,8 +72,14 @@ private CertCentralResponse Request(CertCentralBaseRequest request)
}
private static int RequestIDCounter = 1;
+ private const int MaxRateLimitRetries = 3;
private CertCentralResponse Request(CertCentralBaseRequest request, string parameters)
+ {
+ return Request(request, parameters, 1);
+ }
+
+ private CertCentralResponse Request(CertCentralBaseRequest request, string parameters, int attempt)
{
//set in config files
//ServicePointManager.SecurityProtocol = SecurityProtocolType.Tls12;
@@ -129,10 +135,25 @@ private CertCentralResponse Request(CertCentralBaseRequest request, string param
{
if (errorResponse.StatusCode == (HttpStatusCode)429/*Too Many Requests*/)
{
- Logger.LogInformation($"Request ID: {reqID} was rate-limited. Trying again in 5 seconds");
- // TODO - Figure out how long to wait, then wait that long
- System.Threading.Thread.Sleep(5000);
- return Request(request, parameters);
+ // DigiCert's documented limits are 1000 requests / 3 minutes AND 100 requests / 5 seconds,
+ // rolling, per API key.
+ // Guidance is exponential backoff with a default maximum of 3 retries.
+ if (attempt >= MaxRateLimitRetries)
+ {
+ Logger.LogWarning($"Request ID: {reqID} was rate-limited by DigiCert and has exhausted {MaxRateLimitRetries} attempts. Giving up.");
+ using (var limitReader = new StreamReader(errorResponse.GetResponseStream()))
+ {
+ oCertCertResponse.Success = false;
+ oCertCertResponse.Response = limitReader.ReadToEnd();
+ }
+ }
+ else
+ {
+ int waitSeconds = 5 * (int)Math.Pow(2, attempt - 1);
+ Logger.LogInformation($"Request ID: {reqID} was rate-limited. Retry {attempt} of {MaxRateLimitRetries - 1} in {waitSeconds} seconds");
+ System.Threading.Thread.Sleep(waitSeconds * 1000);
+ return Request(request, parameters, attempt + 1);
+ }
}
else
{
From 3bd56c7813b7246a40c1e50623b5be15b95a1ccf Mon Sep 17 00:00:00 2001
From: David Galey
Date: Tue, 22 Sep 2026 16:20:07 -0400
Subject: [PATCH 24/29] improved API error handling
---
CHANGELOG.md | 1 +
.../Client/CertCentralClient.cs | 128 ++++++++++++------
2 files changed, 85 insertions(+), 44 deletions(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 03af54e..d2f1c45 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -45,3 +45,4 @@
* Performance enhancements to reduce API calls
* Added null checks/error handling to catch bad certs on sync
* Improved rate limiting handling based on DigiCert guidance
+* Improved handling of API error responses
diff --git a/digicert-certcentral-caplugin/Client/CertCentralClient.cs b/digicert-certcentral-caplugin/Client/CertCentralClient.cs
index 9f67ed9..1b4f6c6 100644
--- a/digicert-certcentral-caplugin/Client/CertCentralClient.cs
+++ b/digicert-certcentral-caplugin/Client/CertCentralClient.cs
@@ -6,6 +6,7 @@
using System;
using System.Collections.Generic;
+using System.Data.SqlTypes;
using System.Linq;
using System.Net;
using System.Security.Cryptography.X509Certificates;
@@ -64,6 +65,43 @@ public CertCentralResponse()
public bool Success { get; set; }
public string Response { get; set; }
+ public int StatusCode { get; set; }
+ }
+
+ ///
+ /// Turn a DigiCert response body into a non-empty list of Errors, without throwing
+ ///
+ ///
+ ///
+ ///
+ internal static List ParseErrors(string body, int statusCode)
+ {
+ string codeSuffix = statusCode > 0 ? $" (HTTP {statusCode})" : "";
+
+ if (string.IsNullOrWhiteSpace(body))
+ {
+ return new List { new Error { code = "empty_response_body", message = $"DigiCert returned no response body{codeSuffix}." } };
+ }
+
+ try
+ {
+ Errors parsed = JsonConvert.DeserializeObject(body);
+ if (parsed?.errors != null && parsed.errors.Count > 0)
+ {
+ return parsed.errors;
+ }
+ return new List { new Error { code = "unrecognized_error_response", message = $"DigiCert returned a response with no 'errors' array{codeSuffix} : {Truncate(body)}" } };
+ }
+ catch (JsonException)
+ {
+ return new List { new Error { code = "non_json_response", message = $"DigiCert returned a non-JSON response{codeSuffix}: {Truncate(body)}" } };
+ }
+ }
+
+ private static string Truncate(string s)
+ {
+ s = s.Replace("\r", " ").Replace("\n", " ").Trim();
+ return s.Length <= 500 ? s : s.Substring(0, 500) + "\u2026";
}
private CertCentralResponse Request(CertCentralBaseRequest request)
@@ -124,6 +162,7 @@ private CertCentralResponse Request(CertCentralBaseRequest request, string param
{
string respString = new StreamReader(objResponse.GetResponseStream()).ReadToEnd();
oCertCertResponse.Response = respString;
+ oCertCertResponse.StatusCode = (int)objResponse.StatusCode;
Logger.LogTrace($"CertCentral CA (Request ID: {reqID}) has returned Response '{objResponse.StatusCode}: {respString}");
}
}
@@ -162,6 +201,7 @@ private CertCentralResponse Request(CertCentralBaseRequest request, string param
string errorString = reader.ReadToEnd();
oCertCertResponse.Success = false;
oCertCertResponse.Response = errorString;
+ oCertCertResponse.StatusCode = (int)errorResponse.StatusCode;
Logger.LogTrace($"CertCentral CA (Request ID: {reqID}) has returned Response '{errorResponse.StatusCode}: {errorString}");
}
}
@@ -190,9 +230,9 @@ public ListOrganizationsResponse ListOrganizations(ListOrganizationsRequest requ
if (!response.Success)
{
- Errors errors = JsonConvert.DeserializeObject(response.Response);
+ List errors = ParseErrors(response.Response, response.StatusCode);
listOrganizationsResponse.Status = CertCentralBaseResponse.StatusType.ERROR;
- listOrganizationsResponse.Errors = errors.errors;
+ listOrganizationsResponse.Errors = errors;
}
else
listOrganizationsResponse = JsonConvert.DeserializeObject(response.Response);
@@ -208,9 +248,9 @@ public ListDomainsResponse ListDomains(ListDomainsRequest request)
if (!response.Success)
{
- Errors errors = JsonConvert.DeserializeObject(response.Response);
+ List errors= ParseErrors(response.Response, response.StatusCode);
listDomainsResponse.Status = CertCentralBaseResponse.StatusType.ERROR;
- listDomainsResponse.Errors = errors.errors;
+ listDomainsResponse.Errors = errors;
}
else
listDomainsResponse = JsonConvert.DeserializeObject(response.Response);
@@ -226,9 +266,9 @@ public ListContainersResponse ListContainers(ListContainersRequest request)
if (!response.Success)
{
- Errors errors = JsonConvert.DeserializeObject(response.Response);
+ List errors = ParseErrors(response.Response, response.StatusCode);
listContainersResponse.Status = CertCentralBaseResponse.StatusType.ERROR;
- listContainersResponse.Errors = errors.errors;
+ listContainersResponse.Errors = errors;
}
else
{
@@ -246,9 +286,9 @@ public ListDuplicatesResponse ListDuplicates(ListDuplicatesRequest duplicatesReq
if (!ccResponse.Success)
{
- Errors errors = JsonConvert.DeserializeObject(ccResponse.Response);
+ List errors = ParseErrors(ccResponse.Response, ccResponse.StatusCode);
duplicatesResponse.Status = CertCentralBaseResponse.StatusType.ERROR;
- duplicatesResponse.Errors = errors.errors;
+ duplicatesResponse.Errors = errors;
}
else
{
@@ -266,9 +306,9 @@ public ListReissueResponse ListReissues(ListReissueRequest reissueRequest)
if (!ccResponse.Success)
{
- Errors errors = JsonConvert.DeserializeObject(ccResponse.Response);
+ List errors = ParseErrors(ccResponse.Response, ccResponse.StatusCode);
reissueResponse.Status = CertCentralBaseResponse.StatusType.ERROR;
- reissueResponse.Errors = errors.errors;
+ reissueResponse.Errors = errors;
}
else
{
@@ -286,9 +326,9 @@ public ListRequestsResponse ListRequests(ListRequestsRequest request)
if (!response.Success)
{
- Errors errors = JsonConvert.DeserializeObject(response.Response);
+ List errors = ParseErrors(response.Response, response.StatusCode);
listRequestsResponse.Status = CertCentralBaseResponse.StatusType.ERROR;
- listRequestsResponse.Errors = errors.errors;
+ listRequestsResponse.Errors = errors;
}
else
listRequestsResponse = JsonConvert.DeserializeObject(response.Response);
@@ -304,9 +344,9 @@ public ListMetadataResponse ListMetadata(ListMetadataRequest request)
if (!response.Success)
{
- Errors errors = JsonConvert.DeserializeObject(response.Response);
+ List errors = ParseErrors(response.Response, response.StatusCode);
listMetadataResponse.Status = CertCentralBaseResponse.StatusType.ERROR;
- listMetadataResponse.Errors = errors.errors;
+ listMetadataResponse.Errors = errors;
}
else
listMetadataResponse = JsonConvert.DeserializeObject(response.Response);
@@ -325,9 +365,9 @@ public OrderResponse OrderCertificate(OrderRequest request)
OrderResponse orderResponse = new OrderResponse();
if (!response.Success)
{
- Errors errors = JsonConvert.DeserializeObject(response.Response);
+ List errors = ParseErrors(response.Response, response.StatusCode);
orderResponse.Status = CertCentralBaseResponse.StatusType.ERROR;
- orderResponse.Errors = errors.errors;
+ orderResponse.Errors = errors;
}
else
orderResponse = JsonConvert.DeserializeObject(response.Response);
@@ -346,9 +386,9 @@ public OrderResponse OrderSmimeCertificate(OrderSmimeRequest request)
OrderResponse orderResponse = new OrderResponse();
if (!response.Success)
{
- Errors errors = JsonConvert.DeserializeObject(response.Response);
+ List errors = ParseErrors(response.Response, response.StatusCode);
orderResponse.Status = CertCentralBaseResponse.StatusType.ERROR;
- orderResponse.Errors = errors.errors;
+ orderResponse.Errors = errors;
}
else
orderResponse = JsonConvert.DeserializeObject(response.Response);
@@ -366,9 +406,9 @@ public OrderResponse ReissueCertificate(ReissueRequest request)
OrderResponse reissueResponse = new OrderResponse();
if (!response.Success)
{
- Errors errors = JsonConvert.DeserializeObject(response.Response);
+ List errors = ParseErrors(response.Response, response.StatusCode);
reissueResponse.Status = CertCentralBaseResponse.StatusType.ERROR;
- reissueResponse.Errors = errors.errors;
+ reissueResponse.Errors = errors;
}
else
{
@@ -388,9 +428,9 @@ public OrderResponse DuplicateCertificate(DuplicateRequest request)
OrderResponse duplicateResponse = new OrderResponse();
if (!response.Success)
{
- Errors errors = JsonConvert.DeserializeObject(response.Response);
+ List errors = ParseErrors(response.Response, response.StatusCode);
duplicateResponse.Status = CertCentralBaseResponse.StatusType.ERROR;
- duplicateResponse.Errors = errors.errors;
+ duplicateResponse.Errors = errors;
}
else
{
@@ -407,9 +447,9 @@ public RevokeCertificateResponse RevokeCertificate(RevokeCertificateRequest requ
RevokeCertificateResponse revokeOrderResponse = new RevokeCertificateResponse();
if (!response.Success)
{
- Errors errors = JsonConvert.DeserializeObject(response.Response);
+ List errors = ParseErrors(response.Response, response.StatusCode);
revokeOrderResponse.Status = CertCentralBaseResponse.StatusType.ERROR;
- revokeOrderResponse.Errors = errors.errors;
+ revokeOrderResponse.Errors = errors;
}
else
revokeOrderResponse = JsonConvert.DeserializeObject(response.Response);
@@ -424,9 +464,9 @@ public RevokeCertificateResponse RevokeCertificate(RevokeCertificateByOrderReque
RevokeCertificateResponse revokeOrderResponse = new RevokeCertificateResponse();
if (!response.Success)
{
- Errors errors = JsonConvert.DeserializeObject(response.Response);
+ List errors = ParseErrors(response.Response, response.StatusCode);
revokeOrderResponse.Status = CertCentralBaseResponse.StatusType.ERROR;
- revokeOrderResponse.Errors = errors.errors;
+ revokeOrderResponse.Errors = errors;
}
else
revokeOrderResponse = JsonConvert.DeserializeObject(response.Response);
@@ -441,9 +481,9 @@ public UpdateRequestStatusResponse UpdateRequestStatus(UpdateRequestStatusReques
UpdateRequestStatusResponse updateRequestResponse = new UpdateRequestStatusResponse();
if (!response.Success)
{
- Errors errors = JsonConvert.DeserializeObject(response.Response);
+ List errors = ParseErrors(response.Response, response.StatusCode);
updateRequestResponse.Status = CertCentralBaseResponse.StatusType.ERROR;
- updateRequestResponse.Errors = errors.errors;
+ updateRequestResponse.Errors = errors;
}
else
{
@@ -464,9 +504,9 @@ public DVCheckDCVResponse DVCheckDCV(DVCheckDCVRequest request)
DVCheckDCVResponse checkDCVResponse = new DVCheckDCVResponse();
if (!response.Success)
{
- Errors errors = JsonConvert.DeserializeObject(response.Response);
+ List errors = ParseErrors(response.Response, response.StatusCode);
checkDCVResponse.Status = CertCentralBaseResponse.StatusType.ERROR;
- checkDCVResponse.Errors = errors.errors;
+ checkDCVResponse.Errors = errors;
}
else
{
@@ -482,9 +522,9 @@ public CertificateChainResponse GetCertificateChain(CertificateChainRequest requ
CertificateChainResponse chainResponse = new CertificateChainResponse();
if (!response.Success)
{
- Errors errors = JsonConvert.DeserializeObject(response.Response);
+ List errors = ParseErrors(response.Response, response.StatusCode);
chainResponse.Status = CertCentralBaseResponse.StatusType.ERROR;
- chainResponse.Errors = errors.errors;
+ chainResponse.Errors = errors;
}
else
{
@@ -500,9 +540,9 @@ public StatusChangesResponse StatusChanges(StatusChangesRequest request)
StatusChangesResponse statusChangeResponse = new StatusChangesResponse();
if (!certResponse.Success)
{
- Errors errors = JsonConvert.DeserializeObject(certResponse.Response);
+ List errors = ParseErrors(certResponse.Response, certResponse.StatusCode);
statusChangeResponse.Status = CertCentralBaseResponse.StatusType.ERROR;
- statusChangeResponse.Errors = errors.errors;
+ statusChangeResponse.Errors = errors;
}
else
{
@@ -517,9 +557,9 @@ public DownloadCertificateByFormatResponse DownloadCertificateByFormat(DownloadC
DownloadCertificateByFormatResponse dlCertificateRequestResponse = new DownloadCertificateByFormatResponse();
if (!response.Success)
{
- Errors errors = JsonConvert.DeserializeObject(response.Response);
+ List errors = ParseErrors(response.Response, response.StatusCode);
dlCertificateRequestResponse.Status = CertCentralBaseResponse.StatusType.ERROR;
- dlCertificateRequestResponse.Errors = errors.errors;
+ dlCertificateRequestResponse.Errors = errors;
}
else
{
@@ -566,9 +606,9 @@ public ListCertificateOrdersResponse ListAllCertificateOrders(bool ignoreExpired
ListCertificateOrdersResponse listCertificateResponse = new ListCertificateOrdersResponse();
if (!response.Success)
{
- Errors errors = JsonConvert.DeserializeObject(response.Response);
+ List errors = ParseErrors(response.Response, response.StatusCode);
listCertificateResponse.Status = CertCentralBaseResponse.StatusType.ERROR;
- listCertificateResponse.Errors = errors.errors;
+ listCertificateResponse.Errors = errors;
return listCertificateResponse;
}
@@ -592,9 +632,9 @@ public ViewCertificateOrderResponse ViewCertificateOrder(ViewCertificateOrderReq
if (!response.Success)
{
- Errors errors = JsonConvert.DeserializeObject(response.Response);
+ List errors = ParseErrors(response.Response, response.StatusCode);
viewCertResponse.Status = CertCentralBaseResponse.StatusType.ERROR;
- viewCertResponse.Errors = errors.errors;
+ viewCertResponse.Errors = errors;
}
else
{
@@ -617,9 +657,9 @@ public CertificateTypeDetailsResponse GetCertificateTypeDetails(CertificateTypeD
if (!response.Success)
{
- Errors errors = JsonConvert.DeserializeObject(response.Response);
+ List errors = ParseErrors(response.Response, response.StatusCode);
detailsResponse.Status = CertCentralBaseResponse.StatusType.ERROR;
- detailsResponse.Errors = errors.errors;
+ detailsResponse.Errors = errors;
}
else
{
@@ -641,9 +681,9 @@ public CertificateTypesResponse GetAllCertificateTypes()
if (!response.Success)
{
- Errors errors = JsonConvert.DeserializeObject(response.Response);
+ List errors = ParseErrors(response.Response, response.StatusCode);
allTypes.Status = CertCentralBaseResponse.StatusType.ERROR;
- allTypes.Errors = errors.errors;
+ allTypes.Errors = errors;
}
else
{
From 0665cb48d2b437546628a7ceb010e4e937b2ee7f Mon Sep 17 00:00:00 2001
From: David Galey
Date: Tue, 22 Sep 2026 16:23:56 -0400
Subject: [PATCH 25/29] add ToString to Error class
---
digicert-certcentral-caplugin/Models/Error.cs | 7 +++++++
1 file changed, 7 insertions(+)
diff --git a/digicert-certcentral-caplugin/Models/Error.cs b/digicert-certcentral-caplugin/Models/Error.cs
index e963096..1a7c1c6 100644
--- a/digicert-certcentral-caplugin/Models/Error.cs
+++ b/digicert-certcentral-caplugin/Models/Error.cs
@@ -15,6 +15,13 @@ public class Error
[JsonProperty("message")]
public string message { get; set; }
+
+ public override string ToString()
+ {
+ if (string.IsNullOrEmpty(code)) return message ?? string.Empty;
+ if (string.IsNullOrEmpty(message)) return code;
+ return $"{code}: {message}";
+ }
}
public class Errors
From fe98dbd125b1b2ded98d3707a6d953aef1b3e82a Mon Sep 17 00:00:00 2001
From: David Galey
Date: Tue, 22 Sep 2026 16:27:59 -0400
Subject: [PATCH 26/29] sync filter fixes
---
CHANGELOG.md | 1 +
digicert-certcentral-caplugin/CertCentralCAPlugin.cs | 3 ++-
2 files changed, 3 insertions(+), 1 deletion(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index d2f1c45..1b31671 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -46,3 +46,4 @@
* Added null checks/error handling to catch bad certs on sync
* Improved rate limiting handling based on DigiCert guidance
* Improved handling of API error responses
+* Fixes for sync filtering
diff --git a/digicert-certcentral-caplugin/CertCentralCAPlugin.cs b/digicert-certcentral-caplugin/CertCentralCAPlugin.cs
index 425aedb..ff0da9c 100644
--- a/digicert-certcentral-caplugin/CertCentralCAPlugin.cs
+++ b/digicert-certcentral-caplugin/CertCentralCAPlugin.cs
@@ -887,7 +887,7 @@ public async Task Synchronize(BlockingCollection blockin
_logger.LogTrace($"Sync CAs: {syncCAstring}");
List caList = _config.SyncCAs;
- caList.ForEach(c => c.ToUpper());
+ caList = caList.Select(c => c?.Trim().ToUpper()).Where(c => !string.IsNullOrEmpty(c)).ToList();
List divFilters = new List();
if (!string.IsNullOrEmpty(_config.SyncDivisionFilter))
@@ -1713,6 +1713,7 @@ private List GetAllConnectorCertsForOrder(string caReque
if (productIds != null && productIds.Count > 0 && !productIds.Contains(orderResponse.product.name_id.ToString()))
{
_logger.LogTrace($"Found order ID {orderId} that does not match Product filter. Product ID: {orderResponse.product.name_id.ToString()} Skipping...");
+ return null;
}
var orderCerts = GetAllCertsForOrder(orderId, orderResponse);
From 4744a512a146afb13344fee1902ada54a66b132a Mon Sep 17 00:00:00 2001
From: David Galey
Date: Tue, 22 Sep 2026 16:29:45 -0400
Subject: [PATCH 27/29] incremental sync fix
---
CHANGELOG.md | 1 +
digicert-certcentral-caplugin/CertCentralCAPlugin.cs | 2 +-
2 files changed, 2 insertions(+), 1 deletion(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 1b31671..31bb4a1 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -47,3 +47,4 @@
* Improved rate limiting handling based on DigiCert guidance
* Improved handling of API error responses
* Fixes for sync filtering
+* Incremental sync fix
diff --git a/digicert-certcentral-caplugin/CertCentralCAPlugin.cs b/digicert-certcentral-caplugin/CertCentralCAPlugin.cs
index ff0da9c..a5e2869 100644
--- a/digicert-certcentral-caplugin/CertCentralCAPlugin.cs
+++ b/digicert-certcentral-caplugin/CertCentralCAPlugin.cs
@@ -983,7 +983,7 @@ public async Task Synchronize(BlockingCollection blockin
cancelToken.ThrowIfCancellationRequested();
string caReqId = order.order_id + "-" + order.certificate_id;
orderCerts = GetAllConnectorCertsForOrder(caReqId, caList, divFilters, productFilters);
- if (orderCerts == null || orderCerts.Count > 0)
+ if (orderCerts == null || orderCerts.Count == 0)
{
continue;
}
From 26b35c15408e5ff58f7d62fc8ddae621e5b0f519 Mon Sep 17 00:00:00 2001
From: David Galey
Date: Tue, 22 Sep 2026 17:25:22 -0400
Subject: [PATCH 28/29] doc update
---
CHANGELOG.md | 3 +++
digicert-certcentral-caplugin/CertCentralCAPlugin.cs | 2 +-
docsource/configuration.md | 9 +++++++++
3 files changed, 13 insertions(+), 1 deletion(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 31bb4a1..acdf627 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -48,3 +48,6 @@
* Improved handling of API error responses
* Fixes for sync filtering
* Incremental sync fix
+
+### 3.0.0
+* Add support for automated domain validation via DNS gateway plugins.
diff --git a/digicert-certcentral-caplugin/CertCentralCAPlugin.cs b/digicert-certcentral-caplugin/CertCentralCAPlugin.cs
index 39280ce..9415e6a 100644
--- a/digicert-certcentral-caplugin/CertCentralCAPlugin.cs
+++ b/digicert-certcentral-caplugin/CertCentralCAPlugin.cs
@@ -546,7 +546,7 @@ public Dictionary GetCAConnectorAnnotations()
[CertCentralConstants.Config.DNS_VALIDATION_ENABLED] = new PropertyConfigInfo()
{
Comments = "Enable automated DNS (TXT or CNAME) domain control validation. When enabled, the plugin " +
- "requests TXT-based validation from DigiCert and publishes the returned record via the " +
+ "requests TXT-based or CNAME-based validation from DigiCert and publishes the returned record via the " +
"DNS provider plugin resolved by the AnyCA Gateway. Requires a DNS provider plugin (e.g. Azure, " +
"Cloudflare, etc) to be deployed and configured on the gateway. When disabled, requests that require validation " +
"will be flagged as External Validation, and the validation token, if needed depending on the DNS Validation method, " +
diff --git a/docsource/configuration.md b/docsource/configuration.md
index 0b4b863..7a9c66e 100644
--- a/docsource/configuration.md
+++ b/docsource/configuration.md
@@ -14,6 +14,15 @@ An API Key within your Digicert account that has the necessary permissions to en
In order to enroll for certificates the Keyfactor Command server must trust the trust chain. Once you identify your Root and/or Subordinate CA in your Digicert account, make sure to download and import the certificate chain into the Command Server certificate store
+### Automated DNS Domain Validation
+
+This plugin integrates with the AnyCA Gateway **DNS provider plugin framework** (`KeyfactorAnyGateway.IAnyCAPlugin` 3.3.0+). DNS provider plugins (Azure DNS, AWS Route53, Cloudflare, Google Cloud DNS, etc.) are deployed and configured **separately** on the gateway; this CA plugin does not bundle any DNS provider SDKs. The gateway injects an `IDomainValidatorFactory` that resolves the correct provider for each domain at enrollment time.
+
+DigiCert supports both **TXT** and **CNAME** records for DNS validation, the choice of which is provided by the appropriate configuration field. **TXT** records are the preferred method.
+
+`DnsValidationMethod` defines whether you wish to use TXT, CNAME, or email validation. Only TXT or CNAME will work with the automated validation.
+`DnsValidationEnabled` determines whether to use the automated validation. Make sure you have the necessary DNS plugins installed and configured before enabling. If `DnsValidationMethod` is set to either TXT or CNAME but `DnsValidationEnabled` is false, then unvalidated enrollment requests will get a status of External Validation, and the necessary TXT or CNAME token will be instead returned to the enrollment caller to be used to manually update the DNS record.
+
## Certificate Template Creation Step
Note for SMIME product types (Secure Email types): The template configuration fields provided for those are not required to be filled out in the gateway config. Many of those values would change on a per-enrollment basis. The way to handle that is to create Enrollment fields in Command with the same name (for example: CommonNameIndicator) and then any values populated in those fields will override any static values provided in the configuration.
From b3974c1541efa7b31adf21c62710566c89d9005b Mon Sep 17 00:00:00 2001
From: "github-actions[bot]"
Date: Tue, 22 Sep 2026 21:25:57 +0000
Subject: [PATCH 29/29] docs: auto-generate README and documentation [skip ci]
---
README.md | 9 +++++++++
1 file changed, 9 insertions(+)
diff --git a/README.md b/README.md
index 39e3518..fe7f318 100644
--- a/README.md
+++ b/README.md
@@ -79,6 +79,15 @@ An API Key within your Digicert account that has the necessary permissions to en
* **Gateway Registration**
In order to enroll for certificates the Keyfactor Command server must trust the trust chain. Once you identify your Root and/or Subordinate CA in your Digicert account, make sure to download and import the certificate chain into the Command Server certificate store
+
+ ### Automated DNS Domain Validation
+
+ This plugin integrates with the AnyCA Gateway **DNS provider plugin framework** (`KeyfactorAnyGateway.IAnyCAPlugin` 3.3.0+). DNS provider plugins (Azure DNS, AWS Route53, Cloudflare, Google Cloud DNS, etc.) are deployed and configured **separately** on the gateway; this CA plugin does not bundle any DNS provider SDKs. The gateway injects an `IDomainValidatorFactory` that resolves the correct provider for each domain at enrollment time.
+
+ DigiCert supports both **TXT** and **CNAME** records for DNS validation, the choice of which is provided by the appropriate configuration field. **TXT** records are the preferred method.
+
+ `DnsValidationMethod` defines whether you wish to use TXT, CNAME, or email validation. Only TXT or CNAME will work with the automated validation.
+ `DnsValidationEnabled` determines whether to use the automated validation. Make sure you have the necessary DNS plugins installed and configured before enabling. If `DnsValidationMethod` is set to either TXT or CNAME but `DnsValidationEnabled` is false, then unvalidated enrollment requests will get a status of External Validation, and the necessary TXT or CNAME token will be instead returned to the enrollment caller to be used to manually update the DNS record.
* **CA Connection**