From a1b7589d8003b277138d18d830793edece22a876 Mon Sep 17 00:00:00 2001 From: David Galey Date: Tue, 30 Jun 2026 13:47:20 -0400 Subject: [PATCH 01/19] 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/19] 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/19] 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/19] 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/19] 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/19] 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/19] 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/19] 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/19] 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/19] 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/19] 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 3184808ea6bb32305a688a9d3ac095ae0ce63c5f Mon Sep 17 00:00:00 2001 From: David Galey Date: Tue, 22 Sep 2026 15:14:44 -0400 Subject: [PATCH 12/19] 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 13/19] 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 14/19] 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 15/19] 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 16/19] 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 17/19] 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 18/19] 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 19/19] 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; }