Skip to content

Commit 665bf3d

Browse files
indroradgaleyKeyfactorspbsoluble
authored
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 <keyfactor@keyfactor.github.io> --------- Co-authored-by: David Galey <dgaley@keyfactor.com> Co-authored-by: Keyfactor <keyfactor@keyfactor.github.io> Co-authored-by: Dave Galey <89407235+dgaley@users.noreply.github.com> Co-authored-by: Sean <1661003+spbsoluble@users.noreply.github.com>
1 parent 5e15a5d commit 665bf3d

11 files changed

Lines changed: 248 additions & 33 deletions

CHANGELOG.md

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,4 +9,18 @@
99
* Add support for enrolling for client certs
1010
* Option to filter sync by division ID
1111
* Option to provide division ID for enrollment
12-
* Add support for secure_email_* SMIME product types
12+
* Add support for secure_email_* SMIME product types
13+
14+
### 2.1.1
15+
* Add configuration flag to support adding client auth EKU to ssl cert requests
16+
* NOTE: This is a temporary feature which is planned for loss of support by Digicert in May 2026
17+
* For smime certs, use profile type defined on the product as the default if not supplied, rather than just defaulting to 'strict'
18+
* Hotfix for data type conversion
19+
20+
### 2.1.2
21+
* Hotfix for incremental sync to default to a 6 day window if no previous incremental sync has run
22+
* 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
23+
* Remove caching of product ID lookups from DigiCert account
24+
25+
### 2.2.0
26+
* Add support for duplicating certs

README.md

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -106,15 +106,20 @@ An API Key within your Digicert account that has the necessary permissions to en
106106
* **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.
107107
* **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.
108108
* **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.
109+
* **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.
109110
* **EnrollDivisionId** - OPTIONAL: The division (container) ID to use for enrollments against this template.
110111
* **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
111-
* **ProfileType** - Optional for secure_email_* types, ignored otherwise. Valid values are: strict, multipurpose. Default value is strict.
112+
* **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.
112113
* **FirstName** - Required for secure_email_* types if CommonNameIndicator is given_name_surname, ignored otherwise.
113114
* **LastName** - Required for secure_email_* types if CommonNameIndicator is given_name_surname, ignored otherwise.
114115
* **Pseudonym** - Required for secure_email_* types if CommonNameIndicator is pseudonym, ignored otherwise.
115116
* **UsageDesignation** - Required for secure_email_* types, ignored otherwise. The primary usage of the certificate. Valid values are: signing, key_management, dual_use
116117

117118

119+
## Certificate Duplicates
120+
121+
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.
122+
118123
119124
## License
120125
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
using Keyfactor.Extensions.CAPlugin.DigiCert.Models;
2+
using Newtonsoft.Json;
3+
4+
using System;
5+
using System.Collections.Generic;
6+
using System.Linq;
7+
using System.Text;
8+
using System.Threading.Tasks;
9+
10+
namespace Keyfactor.Extensions.CAPlugin.DigiCert.API
11+
{
12+
[Serializable]
13+
public class DuplicateRequest : CertCentralBaseRequest
14+
{
15+
public DuplicateRequest(uint orderId)
16+
{
17+
Method = "POST";
18+
OrderId = orderId;
19+
Resource = $"services/v2/order/certificate/{OrderId}/duplicate";
20+
Certificate = new CertificateDuplicateRequest();
21+
}
22+
23+
[JsonProperty("certificate")]
24+
public CertificateDuplicateRequest Certificate { get; set; }
25+
26+
[JsonProperty("order_id")]
27+
public uint OrderId { get; set; }
28+
29+
[JsonProperty("skip_approval")]
30+
public bool SkipApproval { get; set; }
31+
}
32+
33+
public class CertificateDuplicateRequest
34+
{
35+
[JsonProperty("common_name")]
36+
public string CommonName { get; set; }
37+
38+
[JsonProperty("dns_names")]
39+
public List<string> DnsNames { get; set; }
40+
41+
[JsonProperty("csr")]
42+
public string CSR { get; set; }
43+
44+
[JsonProperty("server_platform")]
45+
public Server_platform ServerPlatform { get; set; }
46+
47+
[JsonProperty("signature_hash")]
48+
public string SignatureHash { get; set; }
49+
50+
[JsonProperty("ca_cert_id")]
51+
public string CACertID { get; set; }
52+
}
53+
54+
public class DuplicateResponse : CertCentralBaseResponse
55+
{
56+
public DuplicateResponse()
57+
{
58+
Requests = new List<Requests>();
59+
}
60+
61+
[JsonProperty("id")]
62+
public int OrderId { get; set; }
63+
64+
[JsonProperty("requests")]
65+
public List<Requests> Requests { get; set; }
66+
67+
[JsonProperty("certificate_chain")]
68+
public List<CertificateChainElement> CertificateChain { get; set; }
69+
}
70+
}

digicert-certcentral-caplugin/API/OrderCertificate.cs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,9 @@ public class CertificateRequest
101101

102102
[JsonProperty("ca_cert_id")]
103103
public string CACertID { get; set; }
104+
105+
[JsonProperty("profile_option")]
106+
public string ProfileOption { get; set; }
104107
}
105108

106109
public class CertificateOrderContainer

digicert-certcentral-caplugin/CertCentralCAPlugin.cs

Lines changed: 117 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
using Newtonsoft.Json;
1818

1919
using Org.BouncyCastle.Asn1.X509;
20+
using Org.BouncyCastle.Pqc.Crypto.Falcon;
2021

2122
using System.Collections.Concurrent;
2223
using System.Runtime.InteropServices;
@@ -298,33 +299,62 @@ public async Task<EnrollmentResult> Enroll(string csr, string subject, Dictionar
298299
string priorCertSnString = null;
299300
string priorCertReqID = null;
300301

301-
// Current gateway core leaves it up to the integration to determine if it is a renewal or a reissue
302+
if (typeOfCert.Equals("ssl") && Convert.ToBoolean(productInfo.ProductParameters[CertCentralConstants.Config.INCLUDE_CLIENT_AUTH]))
303+
{
304+
orderRequest.Certificate.ProfileOption = "server_client_auth_eku";
305+
_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.");
306+
}
307+
308+
bool dupe = false;
309+
// Current gateway core leaves it up to the integration to determine if it is a renewal, a reissue, or a duplicate
302310
if (enrollmentType == EnrollmentType.RenewOrReissue)
303311
{
304-
//// Determine if we're going to do a renew or a reissue.
312+
//// Determine if we're going to do a renew, reissue, or duplicate.
305313
priorCertSnString = productInfo.ProductParameters["PriorCertSN"];
306314
_logger.LogTrace($"Attempting to retrieve the certificate with serial number {priorCertSnString}.");
307-
var reqId = _certificateDataReader.GetRequestIDBySerialNumber(priorCertSnString).Result;
308-
if (string.IsNullOrEmpty(reqId))
315+
priorCertReqID = await _certificateDataReader.GetRequestIDBySerialNumber(priorCertSnString);
316+
if (string.IsNullOrEmpty(priorCertReqID))
309317
{
310318
throw new Exception($"No certificate with serial number '{priorCertSnString}' could be found.");
311319
}
312-
var expDate = _certificateDataReader.GetExpirationDateByRequestId(reqId);
313320

314-
var renewCutoff = DateTime.Now.AddDays(renewWindow * -1);
315-
316-
if (expDate > renewCutoff)
321+
if (productInfo.ProductParameters.ContainsKey(CertCentralConstants.Config.DUPLICATE))
322+
{
323+
string dupStr = productInfo.ProductParameters[CertCentralConstants.Config.DUPLICATE].ToString();
324+
if (!bool.TryParse(dupStr, out dupe))
325+
{
326+
_logger.LogError($"Could not parse 'Duplicate' field as true or false. Check configuration. Value: {dupStr}");
327+
throw new Exception($"Could not parse 'Duplicate' field as true or false. Check configuration");
328+
}
329+
}
330+
if (!dupe)
317331
{
318-
_logger.LogTrace($"Certificate with serial number {priorCertSnString} is within renewal window");
319-
enrollmentType = EnrollmentType.Renew;
332+
var expDate = _certificateDataReader.GetExpirationDateByRequestId(priorCertReqID);
333+
334+
var renewCutoff = DateTime.Now.AddDays(renewWindow * -1);
335+
336+
if (expDate > renewCutoff)
337+
{
338+
_logger.LogTrace($"Certificate with serial number {priorCertSnString} is within renewal window");
339+
enrollmentType = EnrollmentType.Renew;
340+
}
341+
else
342+
{
343+
_logger.LogTrace($"Certificate with serial number {priorCertSnString} is not within renewal window. Reissuing...");
344+
enrollmentType = EnrollmentType.Reissue;
345+
}
320346
}
321347
else
322348
{
323-
_logger.LogTrace($"Certificate with serial number {priorCertSnString} is not within renewal window. Reissuing...");
324-
enrollmentType = EnrollmentType.Reissue;
349+
_logger.LogTrace($"'Duplicate' flag set, performing duplication");
325350
}
326351
}
327352

353+
if (dupe)
354+
{
355+
return await Duplicate(client, productInfo, priorCertReqID, commonName, csr, dnsNames, signatureHash, caCertId);
356+
}
357+
328358
// Check if the order has more validity in it (multi-year cert). If so, do a reissue instead of a renew
329359
if (enrollmentType == EnrollmentType.Renew)
330360
{
@@ -588,6 +618,13 @@ public Dictionary<string, PropertyConfigInfo> GetTemplateParameterAnnotations()
588618
DefaultValue = "ssl",
589619
Type = "String"
590620
},
621+
[CertCentralConstants.Config.INCLUDE_CLIENT_AUTH] = new PropertyConfigInfo()
622+
{
623+
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.",
624+
Hidden = false,
625+
DefaultValue = false,
626+
Type = "Boolean"
627+
},
591628
[CertCentralConstants.Config.ENROLL_DIVISION_ID] = new PropertyConfigInfo()
592629
{
593630
Comments = "OPTIONAL: The division (container) ID to use for enrollments against this template.",
@@ -604,9 +641,9 @@ public Dictionary<string, PropertyConfigInfo> GetTemplateParameterAnnotations()
604641
},
605642
[CertCentralConstants.Config.PROFILE_TYPE] = new PropertyConfigInfo()
606643
{
607-
Comments = "Optional for secure_email_* types, ignored otherwise. Valid values are: strict, multipurpose. Default value is strict.",
644+
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.",
608645
Hidden = false,
609-
DefaultValue = "strict",
646+
DefaultValue = "",
610647
Type = "String"
611648
},
612649
[CertCentralConstants.Config.FIRST_NAME] = new PropertyConfigInfo()
@@ -751,8 +788,14 @@ public async Task Synchronize(BlockingCollection<AnyCAPluginCertificate> blockin
751788
{
752789
_logger.MethodEntry(LogLevel.Trace);
753790

754-
lastSync = lastSync.HasValue ? lastSync.Value.AddHours(-7) : DateTime.MinValue; // DigiCert issue with treating the timezone as mountain time. -7 to accomodate DST
791+
// DigiCert issue with treating the timezone as mountain time. -7 hours to accomodate DST
792+
// If no last sync, use a 6 day window for the sync range (only relevant for incremental syncs)
793+
lastSync = lastSync.HasValue ? lastSync.Value.AddHours(-7) : DateTime.UtcNow.AddDays(-5);
755794
DateTime? utcDate = DateTime.UtcNow.AddDays(1);
795+
if ((utcDate.Value - lastSync.Value).Days > 6)
796+
{
797+
lastSync = DateTime.UtcNow.AddDays(-5);
798+
}
756799
string lastSyncFormat = FormatSyncDate(lastSync);
757800
string todaySyncFormat = FormatSyncDate(utcDate);
758801

@@ -1027,7 +1070,7 @@ public async Task ValidateProductInfo(EnrollmentProductInfo productInfo, Diction
10271070
detailsRequest.ContainerId = null;
10281071
if (connectionInfo.ContainsKey(CertCentralConstants.Config.DIVISION_ID))
10291072
{
1030-
string div = (string)connectionInfo[CertCentralConstants.Config.DIVISION_ID];
1073+
string div = connectionInfo[CertCentralConstants.Config.DIVISION_ID].ToString();
10311074
if (!string.IsNullOrWhiteSpace(div))
10321075
{
10331076
if (int.TryParse($"{div}", out int divId))
@@ -1444,6 +1487,46 @@ private async Task<EnrollmentResult> Reissue(CertCentralClient client, Enrollmen
14441487
return await ExtractEnrollmentResult(client, client.ReissueCertificate(reissueRequest), commonName);
14451488
}
14461489

1490+
/// <summary>
1491+
/// Duplicates a certificate.
1492+
/// </summary>
1493+
/// <param name="client">The client used to contact DigiCert.</param>
1494+
/// <param name="request">The <see cref="OrderRequest"/>.</param>
1495+
/// <param name="enrollmentProductInfo">Information about the DigiCert product this certificate uses.</param>
1496+
/// <returns></returns>
1497+
private async Task<EnrollmentResult> Duplicate(CertCentralClient client, EnrollmentProductInfo enrollmentProductInfo, string caRequestId, string commonName, string csr, List<string> dnsNames, string signatureHash, string caCertId)
1498+
{
1499+
CheckProductExistence(enrollmentProductInfo.ProductID);
1500+
1501+
// Get order ID
1502+
_logger.LogTrace("Attempting to parse the order ID from the AnyGateway certificate.");
1503+
uint orderId = 0;
1504+
try
1505+
{
1506+
orderId = uint.Parse(caRequestId.Split('-').First());
1507+
}
1508+
catch (Exception e)
1509+
{
1510+
throw new Exception($"There was an error parsing the order ID from the certificate: {e.Message}", e);
1511+
}
1512+
1513+
// Duplicate certificate.
1514+
DuplicateRequest duplicateRequest = new DuplicateRequest(orderId)
1515+
{
1516+
Certificate = new CertificateDuplicateRequest
1517+
{
1518+
CommonName = commonName,
1519+
CSR = csr,
1520+
DnsNames = dnsNames,
1521+
SignatureHash = signatureHash,
1522+
CACertID = caCertId
1523+
}
1524+
};
1525+
1526+
_logger.LogTrace("Attempting to duplicate certificate.");
1527+
return await ExtractEnrollmentResult(client, client.DuplicateCertificate(duplicateRequest), commonName);
1528+
}
1529+
14471530
/// <summary>
14481531
/// Verify that the given product ID is valid
14491532
/// </summary>
@@ -1548,6 +1631,7 @@ private List<AnyCAPluginCertificate> GetAllConnectorCertsForOrder(string caReque
15481631
var orderCerts = GetAllCertsForOrder(orderId);
15491632

15501633
List<AnyCAPluginCertificate> certList = new List<AnyCAPluginCertificate>();
1634+
List<string> pemList = new List<string>();
15511635

15521636
foreach (var cert in orderCerts)
15531637
{
@@ -1569,6 +1653,13 @@ private List<AnyCAPluginCertificate> GetAllConnectorCertsForOrder(string caReque
15691653
throw new Exception($"Unexpected error downloading certificate {certId} for order {orderId}: {certificateChainResponse.Errors.FirstOrDefault()?.message}");
15701654
}
15711655
}
1656+
//Another check for duplicate PEMs to get arround issue with DigiCert API returning incorrect data sometimes on reissued/duplicate certs
1657+
if (pemList.Contains(certificate))
1658+
{
1659+
_logger.LogWarning($"Found duplicate PEM for ID {caReqId}. Skipping...");
1660+
continue;
1661+
}
1662+
pemList.Add(certificate);
15721663
var connCert = new AnyCAPluginCertificate
15731664
{
15741665
CARequestID = caReqId,
@@ -1684,9 +1775,10 @@ private EnrollmentResult EnrollForSmimeCert(string csr, string subject, Dictiona
16841775
}
16851776
}
16861777

1778+
string profile = null;
16871779
if (productInfo.ProductParameters.ContainsKey(CertCentralConstants.Config.PROFILE_TYPE))
16881780
{
1689-
string profile = productInfo.ProductParameters[CertCentralConstants.Config.PROFILE_TYPE].ToString();
1781+
profile = productInfo.ProductParameters[CertCentralConstants.Config.PROFILE_TYPE].ToString();
16901782

16911783
// Only validate if value provided
16921784
if (!string.IsNullOrEmpty(profile))
@@ -1697,6 +1789,10 @@ private EnrollmentResult EnrollForSmimeCert(string csr, string subject, Dictiona
16971789
throw new Exception($"Invalid profile type provided. Valid values are: strict, multipurpose");
16981790
}
16991791
}
1792+
else
1793+
{
1794+
profile = null;
1795+
}
17001796
}
17011797

17021798
if (cnIndic.Equals("given_name_surname", StringComparison.OrdinalIgnoreCase))
@@ -1888,12 +1984,11 @@ private EnrollmentResult EnrollForSmimeCert(string csr, string subject, Dictiona
18881984
orderRequest.Certificate.SignatureHash = certType.signatureAlgorithm;
18891985
orderRequest.Certificate.CACertID = caCertId;
18901986
orderRequest.SetOrganization(organizationId);
1891-
string profileType = "strict";
1892-
if (productInfo.ProductParameters.ContainsKey(Constants.Config.PROFILE_TYPE))
1987+
//If profile type is not provided, use the default on the digicert product configuration
1988+
if (!string.IsNullOrEmpty(profile))
18931989
{
1894-
profileType = productInfo.ProductParameters[Constants.Config.PROFILE_TYPE];
1895-
}
1896-
orderRequest.Certificate.ProfileType = profileType;
1990+
orderRequest.Certificate.ProfileType = profile;
1991+
}
18971992
orderRequest.Certificate.CommonNameIndicator = cnIndicator;
18981993
if (productInfo.ProductID.Equals("secure_email_sponsor", StringComparison.OrdinalIgnoreCase))
18991994
{

digicert-certcentral-caplugin/Client/CertCentralClient.cs

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -357,6 +357,28 @@ public OrderResponse ReissueCertificate(ReissueRequest request)
357357
return reissueResponse;
358358
}
359359

360+
public OrderResponse DuplicateCertificate(DuplicateRequest request)
361+
{
362+
string jsonRequest = JsonConvert.SerializeObject(request, Formatting.None, new JsonSerializerSettings { NullValueHandling = NullValueHandling.Ignore });
363+
Logger.LogTrace($"Duplicate request:\n{jsonRequest}");
364+
365+
CertCentralResponse response = Request(request, jsonRequest);
366+
367+
OrderResponse duplicateResponse = new OrderResponse();
368+
if (!response.Success)
369+
{
370+
Errors errors = JsonConvert.DeserializeObject<Errors>(response.Response);
371+
duplicateResponse.Status = CertCentralBaseResponse.StatusType.ERROR;
372+
duplicateResponse.Errors = errors.errors;
373+
}
374+
else
375+
{
376+
duplicateResponse = JsonConvert.DeserializeObject<OrderResponse>(response.Response);
377+
}
378+
379+
return duplicateResponse;
380+
}
381+
360382
public RevokeCertificateResponse RevokeCertificate(RevokeCertificateRequest request)
361383
{
362384
CertCentralResponse response = Request(request, JsonConvert.SerializeObject(request, Formatting.None, new JsonSerializerSettings { NullValueHandling = NullValueHandling.Ignore }));

0 commit comments

Comments
 (0)