diff --git a/CHANGELOG.md b/CHANGELOG.md
index ea5bfe9..4f81506 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -9,4 +9,30 @@
* Add support for enrolling for client certs
* Option to filter sync by division ID
* Option to provide division ID for enrollment
-* Add support for secure_email_* SMIME product types
\ No newline at end of file
+* Add support for secure_email_* SMIME product types
+
+### 2.1.1
+* Add configuration flag to support adding client auth EKU to ssl cert requests
+ * NOTE: This is a temporary feature which is planned for loss of support by Digicert in May 2026
+* For smime certs, use profile type defined on the product as the default if not supplied, rather than just defaulting to 'strict'
+* Hotfix for data type conversion
+
+### 2.1.2
+* Hotfix for incremental sync to default to a 6 day window if no previous incremental sync has run
+* Workaround for DigiCert API issue where retrieving the PEM data of multiple certificates in the same order can occasionally return duplicate data rather than the correct cert
+* Remove caching of product ID lookups from DigiCert account
+
+### 2.2.0
+* Add support for duplicating certs
+
+### 2.2.1
+* Properly mark 'needs_approval' status as Pending rather than Failed
+
+### 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
diff --git a/README.md b/README.md
index 573438d..6f735fc 100644
--- a/README.md
+++ b/README.md
@@ -1,5 +1,5 @@
- DigiCert CertCentral Gateway AnyCA Gateway REST Plugin
+ DigiCert CertCentral AnyCA Gateway REST Plugin
@@ -41,10 +41,10 @@ The Digicert CertCentral AnyCA REST plugin extends the capabilities of Digicert'
## Compatibility
-The DigiCert CertCentral Gateway 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 24.2.0 and later.
## Support
-The DigiCert CertCentral Gateway AnyCA Gateway REST plugin is supported by Keyfactor for Keyfactor customers. If you have a support issue, please open a support ticket with your Keyfactor representative. If you have a support issue, please open a support ticket via the Keyfactor Support Portal at https://support.keyfactor.com.
+The 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.
> 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.
@@ -56,7 +56,7 @@ An API Key within your Digicert account that has the necessary permissions to en
1. Install the AnyCA Gateway REST per the [official Keyfactor documentation](https://software.keyfactor.com/Guides/AnyCAGatewayREST/Content/AnyCAGatewayREST/InstallIntroduction.htm).
-2. On the server hosting the AnyCA Gateway REST, download and unzip the latest [DigiCert CertCentral Gateway AnyCA Gateway REST plugin](https://github.com/Keyfactor/digicert-certcentral-caplugin/releases/latest) from GitHub.
+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:
@@ -67,11 +67,11 @@ An API Key within your Digicert account that has the necessary permissions to en
Program Files\Keyfactor\AnyCA Gateway\AnyGatewayREST\net8.0\Extensions
```
- > The directory containing the DigiCert CertCentral Gateway AnyCA Gateway REST plugin DLLs (`net6.0` or `net8.0`) can be named anything, as long as it is unique within the `Extensions` directory.
+ > The directory containing the 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.
4. Restart the AnyCA Gateway REST service.
-5. Navigate to the AnyCA Gateway REST portal and verify that the Gateway recognizes the DigiCert CertCentral Gateway plugin by hovering over the ⓘ symbol to the right of the Gateway on the top left of the portal.
+5. Navigate to the AnyCA Gateway REST portal and verify that the Gateway recognizes the DigiCert CertCentral plugin by hovering over the ⓘ symbol to the right of the Gateway on the top left of the portal.
## Configuration
@@ -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.
@@ -106,15 +107,22 @@ 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: 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. Default value is strict.
+ * **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
diff --git a/digicert-certcentral-caplugin/API/Duplicate.cs b/digicert-certcentral-caplugin/API/Duplicate.cs
new file mode 100644
index 0000000..6eeba91
--- /dev/null
+++ b/digicert-certcentral-caplugin/API/Duplicate.cs
@@ -0,0 +1,70 @@
+using Keyfactor.Extensions.CAPlugin.DigiCert.Models;
+using Newtonsoft.Json;
+
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace Keyfactor.Extensions.CAPlugin.DigiCert.API
+{
+ [Serializable]
+ public class DuplicateRequest : CertCentralBaseRequest
+ {
+ public DuplicateRequest(uint orderId)
+ {
+ Method = "POST";
+ OrderId = orderId;
+ Resource = $"services/v2/order/certificate/{OrderId}/duplicate";
+ Certificate = new CertificateDuplicateRequest();
+ }
+
+ [JsonProperty("certificate")]
+ public CertificateDuplicateRequest Certificate { get; set; }
+
+ [JsonProperty("order_id")]
+ public uint OrderId { get; set; }
+
+ [JsonProperty("skip_approval")]
+ public bool SkipApproval { get; set; }
+ }
+
+ public class CertificateDuplicateRequest
+ {
+ [JsonProperty("common_name")]
+ public string CommonName { get; set; }
+
+ [JsonProperty("dns_names")]
+ public List DnsNames { get; set; }
+
+ [JsonProperty("csr")]
+ public string CSR { get; set; }
+
+ [JsonProperty("server_platform")]
+ public Server_platform ServerPlatform { get; set; }
+
+ [JsonProperty("signature_hash")]
+ public string SignatureHash { get; set; }
+
+ [JsonProperty("ca_cert_id")]
+ public string CACertID { get; set; }
+ }
+
+ public class DuplicateResponse : CertCentralBaseResponse
+ {
+ public DuplicateResponse()
+ {
+ Requests = new List();
+ }
+
+ [JsonProperty("id")]
+ public int OrderId { get; set; }
+
+ [JsonProperty("requests")]
+ public List Requests { get; set; }
+
+ [JsonProperty("certificate_chain")]
+ public List CertificateChain { get; set; }
+ }
+}
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/API/OrderCertificate.cs b/digicert-certcentral-caplugin/API/OrderCertificate.cs
index 71746db..1a30852 100644
--- a/digicert-certcentral-caplugin/API/OrderCertificate.cs
+++ b/digicert-certcentral-caplugin/API/OrderCertificate.cs
@@ -101,6 +101,9 @@ public class CertificateRequest
[JsonProperty("ca_cert_id")]
public string CACertID { get; set; }
+
+ [JsonProperty("profile_option")]
+ public string ProfileOption { get; set; }
}
public class CertificateOrderContainer
diff --git a/digicert-certcentral-caplugin/CertCentralCAPlugin.cs b/digicert-certcentral-caplugin/CertCentralCAPlugin.cs
index 616f24c..cb1a503 100644
--- a/digicert-certcentral-caplugin/CertCentralCAPlugin.cs
+++ b/digicert-certcentral-caplugin/CertCentralCAPlugin.cs
@@ -5,6 +5,7 @@
using Keyfactor.Extensions.CAPlugin.DigiCert.Client;
using Keyfactor.Extensions.CAPlugin.DigiCert.Models;
using Keyfactor.Logging;
+using Keyfactor.PKI;
using Keyfactor.PKI.Enums;
using Keyfactor.PKI.Enums.EJBCA;
@@ -16,6 +17,7 @@
using Newtonsoft.Json;
using Org.BouncyCastle.Asn1.X509;
+using Org.BouncyCastle.Pqc.Crypto.Falcon;
using System.Collections.Concurrent;
using System.Runtime.InteropServices;
@@ -125,12 +127,15 @@ public async Task Enroll(string csr, string subject, Dictionar
string commonName = null, organization = null, orgUnit = null;
try
{
- subjectParsed = new X509Name(subject);
+ subjectParsed = new X509Name(true, PKIConstants.X509.OIDLookup, subject);
commonName = subjectParsed.GetValueList(X509Name.CN).Cast().LastOrDefault();
organization = subjectParsed.GetValueList(X509Name.O).Cast().LastOrDefault();
orgUnit = subjectParsed.GetValueList(X509Name.OU).Cast().LastOrDefault();
}
- catch (Exception) { }
+ catch (Exception exc)
+ {
+ _logger.LogInformation($"Error while parsing subject. This might be expected. Error message: {exc.Message}");
+ }
if (commonName == null)
{
@@ -294,33 +299,80 @@ public async Task Enroll(string csr, string subject, Dictionar
string priorCertSnString = null;
string priorCertReqID = null;
- // Current gateway core leaves it up to the integration to determine if it is a renewal or a reissue
+ 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]);
+ 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");
+ }
+ if (clientAuth)
+ {
+ orderRequest.Certificate.ProfileOption = "server_client_auth_eku";
+ _logger.LogWarning($"{CertCentralConstants.Config.INCLUDE_CLIENT_AUTH}: Ability to include client auth EKU in SSL certs is currently planned to cease in March 2027. Make sure any workflows that depend on this feature are updated before then to avoid interruptions.");
+ }
+ else if (kdc)
+ {
+ orderRequest.Certificate.ProfileOption = "kdc_smart_card";
+ }
+ else if (intel)
+ {
+ orderRequest.Certificate.ProfileOption = "intel_vpro_eku";
+ }
+ }
+
+ bool dupe = false;
+ // Current gateway core leaves it up to the integration to determine if it is a renewal, a reissue, or a duplicate
if (enrollmentType == EnrollmentType.RenewOrReissue)
{
- //// Determine if we're going to do a renew or a reissue.
+ //// Determine if we're going to do a renew, reissue, or duplicate.
priorCertSnString = productInfo.ProductParameters["PriorCertSN"];
_logger.LogTrace($"Attempting to retrieve the certificate with serial number {priorCertSnString}.");
- var reqId = _certificateDataReader.GetRequestIDBySerialNumber(priorCertSnString).Result;
- if (string.IsNullOrEmpty(reqId))
+ priorCertReqID = await _certificateDataReader.GetRequestIDBySerialNumber(priorCertSnString);
+ if (string.IsNullOrEmpty(priorCertReqID))
{
throw new Exception($"No certificate with serial number '{priorCertSnString}' could be found.");
}
- var expDate = _certificateDataReader.GetExpirationDateByRequestId(reqId);
- var renewCutoff = DateTime.Now.AddDays(renewWindow * -1);
-
- if (expDate > renewCutoff)
+ if (productInfo.ProductParameters.ContainsKey(CertCentralConstants.Config.DUPLICATE))
+ {
+ string dupStr = productInfo.ProductParameters[CertCentralConstants.Config.DUPLICATE].ToString();
+ if (!bool.TryParse(dupStr, out dupe))
+ {
+ _logger.LogError($"Could not parse 'Duplicate' field as true or false. Check configuration. Value: {dupStr}");
+ throw new Exception($"Could not parse 'Duplicate' field as true or false. Check configuration");
+ }
+ }
+ if (!dupe)
{
- _logger.LogTrace($"Certificate with serial number {priorCertSnString} is within renewal window");
- enrollmentType = EnrollmentType.Renew;
+ var expDate = _certificateDataReader.GetExpirationDateByRequestId(priorCertReqID);
+
+ var renewCutoff = DateTime.Now.AddDays(renewWindow * -1);
+
+ if (expDate > renewCutoff)
+ {
+ _logger.LogTrace($"Certificate with serial number {priorCertSnString} is within renewal window");
+ enrollmentType = EnrollmentType.Renew;
+ }
+ else
+ {
+ _logger.LogTrace($"Certificate with serial number {priorCertSnString} is not within renewal window. Reissuing...");
+ enrollmentType = EnrollmentType.Reissue;
+ }
}
else
{
- _logger.LogTrace($"Certificate with serial number {priorCertSnString} is not within renewal window. Reissuing...");
- enrollmentType = EnrollmentType.Reissue;
+ _logger.LogTrace($"'Duplicate' flag set, performing duplication");
}
}
+ if (dupe)
+ {
+ return await Duplicate(client, productInfo, priorCertReqID, commonName, csr, dnsNames, signatureHash, caCertId);
+ }
+
// Check if the order has more validity in it (multi-year cert). If so, do a reissue instead of a renew
if (enrollmentType == EnrollmentType.Renew)
{
@@ -429,6 +481,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.",
@@ -584,6 +643,27 @@ public Dictionary GetTemplateParameterAnnotations()
DefaultValue = "ssl",
Type = "String"
},
+ [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: 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. 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"
+ },
[CertCentralConstants.Config.ENROLL_DIVISION_ID] = new PropertyConfigInfo()
{
Comments = "OPTIONAL: The division (container) ID to use for enrollments against this template.",
@@ -600,9 +680,9 @@ public Dictionary GetTemplateParameterAnnotations()
},
[CertCentralConstants.Config.PROFILE_TYPE] = new PropertyConfigInfo()
{
- Comments = "Optional for secure_email_* types, ignored otherwise. Valid values are: strict, multipurpose. Default value is strict.",
+ Comments = "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.",
Hidden = false,
- DefaultValue = "strict",
+ DefaultValue = "",
Type = "String"
},
[CertCentralConstants.Config.FIRST_NAME] = new PropertyConfigInfo()
@@ -747,8 +827,14 @@ public async Task Synchronize(BlockingCollection blockin
{
_logger.MethodEntry(LogLevel.Trace);
- lastSync = lastSync.HasValue ? lastSync.Value.AddHours(-7) : DateTime.MinValue; // DigiCert issue with treating the timezone as mountain time. -7 to accomodate DST
+ // DigiCert issue with treating the timezone as mountain time. -7 hours to accomodate DST
+ // If no last sync, use a 6 day window for the sync range (only relevant for incremental syncs)
+ lastSync = lastSync.HasValue ? lastSync.Value.AddHours(-7) : DateTime.UtcNow.AddDays(-5);
DateTime? utcDate = DateTime.UtcNow.AddDays(1);
+ if ((utcDate.Value - lastSync.Value).Days > 6)
+ {
+ lastSync = DateTime.UtcNow.AddDays(-5);
+ }
string lastSyncFormat = FormatSyncDate(lastSync);
string todaySyncFormat = FormatSyncDate(utcDate);
@@ -767,12 +853,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)
{
@@ -790,37 +881,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)
{
@@ -830,7 +904,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;
@@ -872,7 +946,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;
@@ -1020,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 (connectionInfo.ContainsKey(CertCentralConstants.Config.DIVISION_ID))
{
- string div = (string)connectionInfo[CertCentralConstants.Config.DIVISION_ID];
+ string div = connectionInfo[CertCentralConstants.Config.DIVISION_ID].ToString();
if (!string.IsNullOrWhiteSpace(div))
{
if (int.TryParse($"{div}", out int divId))
@@ -1045,15 +1120,34 @@ public async Task ValidateProductInfo(EnrollmentProductInfo productInfo, Diction
if (!Constants.ProductTypes.SMIME_CERT.Contains(productInfo.ProductID, StringComparer.OrdinalIgnoreCase))
{
- if (connectionInfo.ContainsKey(CertCentralConstants.Config.CERT_TYPE))
+ if (productInfo.ProductParameters.ContainsKey(CertCentralConstants.Config.CERT_TYPE))
{
- var typeOfCert = (string)connectionInfo[CertCentralConstants.Config.CERT_TYPE];
+ var typeOfCert = (string)productInfo.ProductParameters[CertCentralConstants.Config.CERT_TYPE];
if (!(typeOfCert.Equals("ssl") || typeOfCert.Equals("client")))
{
throw new AnyCAValidationException("Invalid Cert Type specified. Valid options are 'ssl' or 'client'");
}
}
}
+
+ 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]);
+ }
+ if (productInfo.ProductParameters.ContainsKey(CertCentralConstants.Config.INCLUDE_KDC))
+ {
+ kdc = Convert.ToBoolean(productInfo.ProductParameters[CertCentralConstants.Config.INCLUDE_KDC]);
+ }
+ if (productInfo.ProductParameters.ContainsKey(CertCentralConstants.Config.INCLUDE_INTEL))
+ {
+ 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);
}
@@ -1135,12 +1229,12 @@ private async Task ExtractEnrollmentResult(CertCentralClient c
else // We should really only get here if there is a misconfiguration (e.g. set up for approval in DigiCert)
{
_logger.LogWarning($"Order {orderResponse.OrderId} did not return a CertificateId. Manual intervention may be required");
- if (orderResponse.Requests.Any(x => x.Status == CertCentralConstants.Status.PENDING))
+ if (orderResponse.Requests.Any(x => x.Status == CertCentralConstants.Status.PENDING || x.Status == CertCentralConstants.Status.NEEDS_APPROVAL))
{
_logger.LogTrace($"Attempting to approve order '{orderResponse.OrderId}'.");
// Attempt to update the request status.
- int requestId = int.Parse(orderResponse.Requests.FirstOrDefault(x => x.Status == CertCentralConstants.Status.PENDING).Id);
+ int requestId = int.Parse(orderResponse.Requests.FirstOrDefault(x => x.Status == CertCentralConstants.Status.PENDING || x.Status == CertCentralConstants.Status.NEEDS_APPROVAL).Id);
UpdateRequestStatusRequest updateStatusRequest = new UpdateRequestStatusRequest(requestId, CertCentralConstants.Status.APPROVED);
UpdateRequestStatusResponse updateStatusResponse = client.UpdateRequestStatus(updateStatusRequest);
@@ -1157,7 +1251,7 @@ private async Task ExtractEnrollmentResult(CertCentralClient c
}
else
{
- status = (int)EndEntityStatus.FAILED;
+ status = (int)EndEntityStatus.EXTERNALVALIDATION;
statusMessage = $"Approval of order '{orderResponse.OrderId}' failed. Check the gateway logs for more details.";
}
}
@@ -1440,6 +1534,46 @@ private async Task Reissue(CertCentralClient client, Enrollmen
return await ExtractEnrollmentResult(client, client.ReissueCertificate(reissueRequest), commonName);
}
+ ///
+ /// Duplicates a certificate.
+ ///
+ /// The client used to contact DigiCert.
+ /// 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)
+ {
+ CheckProductExistence(enrollmentProductInfo.ProductID);
+
+ // Get order ID
+ _logger.LogTrace("Attempting to parse the order ID from the AnyGateway certificate.");
+ uint orderId = 0;
+ try
+ {
+ orderId = uint.Parse(caRequestId.Split('-').First());
+ }
+ catch (Exception e)
+ {
+ throw new Exception($"There was an error parsing the order ID from the certificate: {e.Message}", e);
+ }
+
+ // Duplicate certificate.
+ DuplicateRequest duplicateRequest = new DuplicateRequest(orderId)
+ {
+ Certificate = new CertificateDuplicateRequest
+ {
+ CommonName = commonName,
+ CSR = csr,
+ DnsNames = dnsNames,
+ SignatureHash = signatureHash,
+ CACertID = caCertId
+ }
+ };
+
+ _logger.LogTrace("Attempting to duplicate certificate.");
+ return await ExtractEnrollmentResult(client, client.DuplicateCertificate(duplicateRequest), commonName);
+ }
+
///
/// Verify that the given product ID is valid
///
@@ -1517,7 +1651,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
@@ -1540,10 +1674,15 @@ 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);
List certList = new List();
+ List pemList = new List();
foreach (var cert in orderCerts)
{
@@ -1565,6 +1704,13 @@ private List GetAllConnectorCertsForOrder(string caReque
throw new Exception($"Unexpected error downloading certificate {certId} for order {orderId}: {certificateChainResponse.Errors.FirstOrDefault()?.message}");
}
}
+ //Another check for duplicate PEMs to get arround issue with DigiCert API returning incorrect data sometimes on reissued/duplicate certs
+ if (pemList.Contains(certificate))
+ {
+ _logger.LogWarning($"Found duplicate PEM for ID {caReqId}. Skipping...");
+ continue;
+ }
+ pemList.Add(certificate);
var connCert = new AnyCAPluginCertificate
{
CARequestID = caReqId,
@@ -1680,9 +1826,10 @@ private EnrollmentResult EnrollForSmimeCert(string csr, string subject, Dictiona
}
}
+ string profile = null;
if (productInfo.ProductParameters.ContainsKey(CertCentralConstants.Config.PROFILE_TYPE))
{
- string profile = productInfo.ProductParameters[CertCentralConstants.Config.PROFILE_TYPE].ToString();
+ profile = productInfo.ProductParameters[CertCentralConstants.Config.PROFILE_TYPE].ToString();
// Only validate if value provided
if (!string.IsNullOrEmpty(profile))
@@ -1693,6 +1840,10 @@ private EnrollmentResult EnrollForSmimeCert(string csr, string subject, Dictiona
throw new Exception($"Invalid profile type provided. Valid values are: strict, multipurpose");
}
}
+ else
+ {
+ profile = null;
+ }
}
if (cnIndic.Equals("given_name_surname", StringComparison.OrdinalIgnoreCase))
@@ -1884,12 +2035,11 @@ private EnrollmentResult EnrollForSmimeCert(string csr, string subject, Dictiona
orderRequest.Certificate.SignatureHash = certType.signatureAlgorithm;
orderRequest.Certificate.CACertID = caCertId;
orderRequest.SetOrganization(organizationId);
- string profileType = "strict";
- if (productInfo.ProductParameters.ContainsKey(Constants.Config.PROFILE_TYPE))
+ //If profile type is not provided, use the default on the digicert product configuration
+ if (!string.IsNullOrEmpty(profile))
{
- profileType = productInfo.ProductParameters[Constants.Config.PROFILE_TYPE];
- }
- orderRequest.Certificate.ProfileType = profileType;
+ orderRequest.Certificate.ProfileType = profile;
+ }
orderRequest.Certificate.CommonNameIndicator = cnIndicator;
if (productInfo.ProductID.Equals("secure_email_sponsor", StringComparison.OrdinalIgnoreCase))
{
@@ -1929,6 +2079,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))
{
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 2138f52..753f54f 100644
--- a/digicert-certcentral-caplugin/Client/CertCentralClient.cs
+++ b/digicert-certcentral-caplugin/Client/CertCentralClient.cs
@@ -357,6 +357,28 @@ public OrderResponse ReissueCertificate(ReissueRequest request)
return reissueResponse;
}
+ public OrderResponse DuplicateCertificate(DuplicateRequest request)
+ {
+ string jsonRequest = JsonConvert.SerializeObject(request, Formatting.None, new JsonSerializerSettings { NullValueHandling = NullValueHandling.Ignore });
+ Logger.LogTrace($"Duplicate request:\n{jsonRequest}");
+
+ CertCentralResponse response = Request(request, jsonRequest);
+
+ OrderResponse duplicateResponse = new OrderResponse();
+ if (!response.Success)
+ {
+ Errors errors = JsonConvert.DeserializeObject(response.Response);
+ duplicateResponse.Status = CertCentralBaseResponse.StatusType.ERROR;
+ duplicateResponse.Errors = errors.errors;
+ }
+ else
+ {
+ duplicateResponse = JsonConvert.DeserializeObject(response.Response);
+ }
+
+ return duplicateResponse;
+ }
+
public RevokeCertificateResponse RevokeCertificate(RevokeCertificateRequest request)
{
CertCentralResponse response = Request(request, JsonConvert.SerializeObject(request, Formatting.None, new JsonSerializerSettings { NullValueHandling = NullValueHandling.Ignore }));
@@ -501,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();
@@ -514,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 57e4ed2..a01d8ca 100644
--- a/digicert-certcentral-caplugin/Constants.cs
+++ b/digicert-certcentral-caplugin/Constants.cs
@@ -25,13 +25,18 @@ public class Config
public const string LIFETIME = "LifetimeDays";
public const string CA_CERT_ID = "CACertId";
public const string RENEWAL_WINDOW = "RenewalWindowDays";
+ public const string DUPLICATE = "Duplicate";
public const string REVOKE_CERT = "RevokeCertificateOnly";
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";
+ 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";
diff --git a/digicert-certcentral-caplugin/Models/CertCentralCertType.cs b/digicert-certcentral-caplugin/Models/CertCentralCertType.cs
index ce3882d..7b65c0f 100644
--- a/digicert-certcentral-caplugin/Models/CertCentralCertType.cs
+++ b/digicert-certcentral-caplugin/Models/CertCentralCertType.cs
@@ -16,7 +16,6 @@ public class CertCentralCertType
#region Private Fields
private static readonly ILogger Logger = LogHandler.GetClassLogger();
- private static List _allTypes;
#endregion Private Fields
@@ -62,12 +61,7 @@ public class CertCentralCertType
///
public static List GetAllTypes(CertCentralConfig config)
{
- if (_allTypes == null || !_allTypes.Any())
- {
- _allTypes = RetrieveCertCentralCertTypes(config);
- }
-
- return _allTypes;
+ return RetrieveCertCentralCertTypes(config);
}
///
diff --git a/digicert-certcentral-caplugin/digicert-certcentral-caplugin.csproj b/digicert-certcentral-caplugin/digicert-certcentral-caplugin.csproj
index 9edfa7f..7510b07 100644
--- a/digicert-certcentral-caplugin/digicert-certcentral-caplugin.csproj
+++ b/digicert-certcentral-caplugin/digicert-certcentral-caplugin.csproj
@@ -1,4 +1,4 @@
-
+
net6.0;net8.0
@@ -6,6 +6,8 @@
enable
disable
DigicertCAPlugin
+ 2.1.2
+ 2.1.2
diff --git a/docsource/configuration.md b/docsource/configuration.md
index 3292f12..0b4b863 100644
--- a/docsource/configuration.md
+++ b/docsource/configuration.md
@@ -18,3 +18,7 @@ In order to enroll for certificates the Keyfactor Command server must trust the
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.
+## 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.
+
diff --git a/integration-manifest.json b/integration-manifest.json
index ef10ed9..a30cc75 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."
@@ -72,6 +76,18 @@
"name": "CertType",
"description": "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."
},
+ {
+ "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: 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. 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",
"description": "OPTIONAL: The division (container) ID to use for enrollments against this template."
@@ -82,7 +98,7 @@
},
{
"name": "ProfileType",
- "description": "Optional for secure_email_* types, ignored otherwise. Valid values are: strict, multipurpose. Default value is strict."
+ "description": "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."
},
{
"name": "FirstName",