From 52eee75e7d5bfb7731f1e251daf5e59e3e6eca00 Mon Sep 17 00:00:00 2001 From: David Galey Date: Wed, 8 Oct 2025 11:29:12 -0400 Subject: [PATCH 01/12] fix for smime profile type --- .../CertCentralCAPlugin.cs | 20 +++++++++++-------- .../digicert-certcentral-caplugin.csproj | 4 +++- 2 files changed, 15 insertions(+), 9 deletions(-) diff --git a/digicert-certcentral-caplugin/CertCentralCAPlugin.cs b/digicert-certcentral-caplugin/CertCentralCAPlugin.cs index 616f24c..2e6775f 100644 --- a/digicert-certcentral-caplugin/CertCentralCAPlugin.cs +++ b/digicert-certcentral-caplugin/CertCentralCAPlugin.cs @@ -600,7 +600,7 @@ 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", Type = "String" @@ -1023,7 +1023,7 @@ public async Task ValidateProductInfo(EnrollmentProductInfo productInfo, Diction 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)) @@ -1680,9 +1680,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 +1694,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 +1889,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)) { diff --git a/digicert-certcentral-caplugin/digicert-certcentral-caplugin.csproj b/digicert-certcentral-caplugin/digicert-certcentral-caplugin.csproj index 9edfa7f..84289d0 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.1 + 2.1.1 From 70fe240b0dbe6d3d73fa94f5c1b47208cedf1a58 Mon Sep 17 00:00:00 2001 From: David Galey Date: Wed, 8 Oct 2025 14:14:32 -0400 Subject: [PATCH 02/12] template parameter to include client auth eku --- .../API/OrderCertificate.cs | 3 +++ digicert-certcentral-caplugin/CertCentralCAPlugin.cs | 12 ++++++++++++ digicert-certcentral-caplugin/Constants.cs | 1 + 3 files changed, 16 insertions(+) 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 2e6775f..ffc2c28 100644 --- a/digicert-certcentral-caplugin/CertCentralCAPlugin.cs +++ b/digicert-certcentral-caplugin/CertCentralCAPlugin.cs @@ -294,6 +294,11 @@ public async Task Enroll(string csr, string subject, Dictionar string priorCertSnString = null; string priorCertReqID = null; + if (typeOfCert.Equals("ssl") && Convert.ToBoolean(productInfo.ProductParameters[CertCentralConstants.Config.INCLUDE_CLIENT_AUTH])) + { + orderRequest.Certificate.ProfileOption = "server_client_auth_eku"; + } + // Current gateway core leaves it up to the integration to determine if it is a renewal or a reissue if (enrollmentType == EnrollmentType.RenewOrReissue) { @@ -584,6 +589,13 @@ 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: This feature is currently planned to be removed by DigiCert in May 2026.", + 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.", diff --git a/digicert-certcentral-caplugin/Constants.cs b/digicert-certcentral-caplugin/Constants.cs index 57e4ed2..c1a3bd3 100644 --- a/digicert-certcentral-caplugin/Constants.cs +++ b/digicert-certcentral-caplugin/Constants.cs @@ -32,6 +32,7 @@ public class Config 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 ENROLL_DIVISION_ID = "EnrollDivisionId"; public const string COMMON_NAME_INDICATOR = "CommonNameIndicator"; public const string PROFILE_TYPE = "ProfileType"; From f9e1564695895cbe7b44ad0560b7e4f48cc7c91a Mon Sep 17 00:00:00 2001 From: Keyfactor Date: Wed, 8 Oct 2025 18:17:47 +0000 Subject: [PATCH 03/12] Update generated docs --- README.md | 3 ++- integration-manifest.json | 6 +++++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 573438d..a731753 100644 --- a/README.md +++ b/README.md @@ -106,9 +106,10 @@ 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 May 2026. * **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. diff --git a/integration-manifest.json b/integration-manifest.json index ef10ed9..e211f33 100644 --- a/integration-manifest.json +++ b/integration-manifest.json @@ -72,6 +72,10 @@ "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: This feature is currently planned to be removed by DigiCert in May 2026." + }, { "name": "EnrollDivisionId", "description": "OPTIONAL: The division (container) ID to use for enrollments against this template." @@ -82,7 +86,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", From 844a7e1a68ee93811e8b83d601398f3469f7841f Mon Sep 17 00:00:00 2001 From: David Galey Date: Mon, 13 Oct 2025 13:12:48 -0400 Subject: [PATCH 04/12] changelog and logging --- CHANGELOG.md | 8 +++++++- digicert-certcentral-caplugin/CertCentralCAPlugin.cs | 1 + 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ea5bfe9..d32a6cc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,4 +9,10 @@ * 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 \ No newline at end of file diff --git a/digicert-certcentral-caplugin/CertCentralCAPlugin.cs b/digicert-certcentral-caplugin/CertCentralCAPlugin.cs index ffc2c28..86da38c 100644 --- a/digicert-certcentral-caplugin/CertCentralCAPlugin.cs +++ b/digicert-certcentral-caplugin/CertCentralCAPlugin.cs @@ -297,6 +297,7 @@ public async Task Enroll(string csr, string subject, Dictionar if (typeOfCert.Equals("ssl") && Convert.ToBoolean(productInfo.ProductParameters[CertCentralConstants.Config.INCLUDE_CLIENT_AUTH])) { 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 May 2026. Make sure any workflows that depend on this feature are updated before then to avoid interruptions."); } // Current gateway core leaves it up to the integration to determine if it is a renewal or a reissue From a4dfbe2b08214adaf26748241f416932088eb962 Mon Sep 17 00:00:00 2001 From: David Galey Date: Thu, 6 Nov 2025 11:29:17 -0500 Subject: [PATCH 05/12] check for duplicate PEMs --- digicert-certcentral-caplugin/CertCentralCAPlugin.cs | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/digicert-certcentral-caplugin/CertCentralCAPlugin.cs b/digicert-certcentral-caplugin/CertCentralCAPlugin.cs index 86da38c..7969f7b 100644 --- a/digicert-certcentral-caplugin/CertCentralCAPlugin.cs +++ b/digicert-certcentral-caplugin/CertCentralCAPlugin.cs @@ -615,7 +615,7 @@ public Dictionary GetTemplateParameterAnnotations() { 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() @@ -1557,6 +1557,7 @@ private List GetAllConnectorCertsForOrder(string caReque var orderCerts = GetAllCertsForOrder(orderId); List certList = new List(); + List pemList = new List(); foreach (var cert in orderCerts) { @@ -1578,6 +1579,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, From cd8fd907f434927676b5834b974a63bc7f63ed4f Mon Sep 17 00:00:00 2001 From: David Galey Date: Tue, 18 Nov 2025 14:26:45 -0500 Subject: [PATCH 06/12] change default start sync date for first incremental sync --- digicert-certcentral-caplugin/CertCentralCAPlugin.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/digicert-certcentral-caplugin/CertCentralCAPlugin.cs b/digicert-certcentral-caplugin/CertCentralCAPlugin.cs index 7969f7b..7f191dc 100644 --- a/digicert-certcentral-caplugin/CertCentralCAPlugin.cs +++ b/digicert-certcentral-caplugin/CertCentralCAPlugin.cs @@ -760,7 +760,9 @@ 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 7 days in the past as the starting point (only relevant for incremental syncs) + lastSync = lastSync.HasValue ? lastSync.Value.AddHours(-7) : DateTime.UtcNow.AddDays(-7); DateTime? utcDate = DateTime.UtcNow.AddDays(1); string lastSyncFormat = FormatSyncDate(lastSync); string todaySyncFormat = FormatSyncDate(utcDate); From fe7e05d53e3150f1d854dc3d6362dc21d4cd7b28 Mon Sep 17 00:00:00 2001 From: David Galey Date: Tue, 18 Nov 2025 14:42:47 -0500 Subject: [PATCH 07/12] removing caching of product type list --- .../Models/CertCentralCertType.cs | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) 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); } /// From d195faf8ab3d065682d7b5b92b3964bbbde79fc3 Mon Sep 17 00:00:00 2001 From: David Galey Date: Wed, 19 Nov 2025 13:15:09 -0500 Subject: [PATCH 08/12] change default incremental sync range --- digicert-certcentral-caplugin/CertCentralCAPlugin.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/digicert-certcentral-caplugin/CertCentralCAPlugin.cs b/digicert-certcentral-caplugin/CertCentralCAPlugin.cs index 7f191dc..69820c2 100644 --- a/digicert-certcentral-caplugin/CertCentralCAPlugin.cs +++ b/digicert-certcentral-caplugin/CertCentralCAPlugin.cs @@ -761,8 +761,8 @@ public async Task Synchronize(BlockingCollection blockin _logger.MethodEntry(LogLevel.Trace); // DigiCert issue with treating the timezone as mountain time. -7 hours to accomodate DST - // If no last sync, use 7 days in the past as the starting point (only relevant for incremental syncs) - lastSync = lastSync.HasValue ? lastSync.Value.AddHours(-7) : DateTime.UtcNow.AddDays(-7); + // 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); string lastSyncFormat = FormatSyncDate(lastSync); string todaySyncFormat = FormatSyncDate(utcDate); From 1b14604978ff1b827f20799db4a2d4625471eae3 Mon Sep 17 00:00:00 2001 From: David Galey Date: Wed, 19 Nov 2025 13:18:44 -0500 Subject: [PATCH 09/12] version --- .../digicert-certcentral-caplugin.csproj | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/digicert-certcentral-caplugin/digicert-certcentral-caplugin.csproj b/digicert-certcentral-caplugin/digicert-certcentral-caplugin.csproj index 84289d0..7510b07 100644 --- a/digicert-certcentral-caplugin/digicert-certcentral-caplugin.csproj +++ b/digicert-certcentral-caplugin/digicert-certcentral-caplugin.csproj @@ -6,8 +6,8 @@ enable disable DigicertCAPlugin - 2.1.1 - 2.1.1 + 2.1.2 + 2.1.2 From a64934c1db82dd9954457675bb79765cb6578ad3 Mon Sep 17 00:00:00 2001 From: David Galey Date: Wed, 19 Nov 2025 13:21:33 -0500 Subject: [PATCH 10/12] changelog --- CHANGELOG.md | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d32a6cc..126e723 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,4 +15,9 @@ * 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 \ No newline at end of file +* 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 \ No newline at end of file From 208fecedc7c2a2b63d6e78d4892587669339087c Mon Sep 17 00:00:00 2001 From: David Galey Date: Tue, 2 Dec 2025 12:40:21 -0500 Subject: [PATCH 11/12] shorten incremental sync if it is too long --- digicert-certcentral-caplugin/CertCentralCAPlugin.cs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/digicert-certcentral-caplugin/CertCentralCAPlugin.cs b/digicert-certcentral-caplugin/CertCentralCAPlugin.cs index 69820c2..c2fdb76 100644 --- a/digicert-certcentral-caplugin/CertCentralCAPlugin.cs +++ b/digicert-certcentral-caplugin/CertCentralCAPlugin.cs @@ -764,6 +764,10 @@ public async Task Synchronize(BlockingCollection blockin // 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); From d53424133750b7be2b1c7d6010227b176438c4c5 Mon Sep 17 00:00:00 2001 From: Dave Galey <89407235+dgaley@users.noreply.github.com> Date: Tue, 17 Feb 2026 15:41:04 -0500 Subject: [PATCH 12/12] feat: release v2.2.0 * add duplicate support * Update generated docs --------- Co-authored-by: Keyfactor --- CHANGELOG.md | 5 +- README.md | 4 + .../API/Duplicate.cs | 70 +++++++++++++++ .../CertCentralCAPlugin.cs | 88 ++++++++++++++++--- .../Client/CertCentralClient.cs | 22 +++++ digicert-certcentral-caplugin/Constants.cs | 1 + docsource/configuration.md | 4 + 7 files changed, 181 insertions(+), 13 deletions(-) create mode 100644 digicert-certcentral-caplugin/API/Duplicate.cs diff --git a/CHANGELOG.md b/CHANGELOG.md index 126e723..3d223a9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,4 +20,7 @@ ### 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 \ No newline at end of file +* Remove caching of product ID lookups from DigiCert account + +### 2.2.0 +* Add support for duplicating certs \ No newline at end of file diff --git a/README.md b/README.md index a731753..267b649 100644 --- a/README.md +++ b/README.md @@ -116,6 +116,10 @@ An API Key within your Digicert account that has the necessary permissions to en * **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/CertCentralCAPlugin.cs b/digicert-certcentral-caplugin/CertCentralCAPlugin.cs index c2fdb76..28f3311 100644 --- a/digicert-certcentral-caplugin/CertCentralCAPlugin.cs +++ b/digicert-certcentral-caplugin/CertCentralCAPlugin.cs @@ -16,6 +16,7 @@ using Newtonsoft.Json; using Org.BouncyCastle.Asn1.X509; +using Org.BouncyCastle.Pqc.Crypto.Falcon; using System.Collections.Concurrent; using System.Runtime.InteropServices; @@ -300,33 +301,56 @@ public async Task Enroll(string csr, string subject, Dictionar _logger.LogWarning($"{CertCentralConstants.Config.INCLUDE_CLIENT_AUTH}: Ability to include client auth EKU in SSL certs is currently planned to cease in May 2026. Make sure any workflows that depend on this feature are updated before then to avoid interruptions."); } - // Current gateway core leaves it up to the integration to determine if it is a renewal or a reissue + 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) { @@ -1459,6 +1483,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 /// diff --git a/digicert-certcentral-caplugin/Client/CertCentralClient.cs b/digicert-certcentral-caplugin/Client/CertCentralClient.cs index 2138f52..fd9af77 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 })); diff --git a/digicert-certcentral-caplugin/Constants.cs b/digicert-certcentral-caplugin/Constants.cs index c1a3bd3..2379499 100644 --- a/digicert-certcentral-caplugin/Constants.cs +++ b/digicert-certcentral-caplugin/Constants.cs @@ -25,6 +25,7 @@ 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"; 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. +