diff --git a/.github/workflows/keyfactor-bootstrap-workflow.yml b/.github/workflows/keyfactor-bootstrap-workflow.yml
index 6d8de53..722cb97 100644
--- a/.github/workflows/keyfactor-bootstrap-workflow.yml
+++ b/.github/workflows/keyfactor-bootstrap-workflow.yml
@@ -11,9 +11,9 @@ on:
jobs:
call-starter-workflow:
- uses: keyfactor/actions/.github/workflows/starter.yml@v2
+ uses: keyfactor/actions/.github/workflows/starter.yml@v4
secrets:
token: ${{ secrets.V2BUILDTOKEN}}
- APPROVE_README_PUSH: ${{ secrets.APPROVE_README_PUSH}}
+ scan_token: ${{ secrets.SAST_TOKEN }}
gpg_key: ${{ secrets.KF_GPG_PRIVATE_KEY }}
gpg_pass: ${{ secrets.KF_GPG_PASSPHRASE }}
diff --git a/CHANGELOG.md b/CHANGELOG.md
index af82282..ea5bfe9 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,6 +1,12 @@
-### 2.0.0
-* Initial Public Release
+### 2.0.0
+* Initial Public Release
-### 2.0.1
-* Add configuration fields to support sync filtering
-* Bug fixes around SAN processing
+### 2.0.1
+* Add configuration fields to support sync filtering
+* Bug fixes around SAN processing
+
+### 2.1.0
+* 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
diff --git a/README.md b/README.md
index 0b77ee8..573438d 100644
--- a/README.md
+++ b/README.md
@@ -1,87 +1,125 @@
+
+ DigiCert CertCentral Gateway AnyCA Gateway REST Plugin
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Support
+
+ ·
+
+ Requirements
+
+ ·
+
+ Installation
+
+ ·
+
+ License
+
+ ·
+
+ Related Integrations
+
+
+
+
+The Digicert CertCentral AnyCA REST plugin extends the capabilities of Digicert's CertCentral product to Keyfactor Command via the Keyfactor AnyCA Gateway REST. The plugin represents a fully featured AnyCA REST Plugin with the following capabilies:
+* SSL Certificate Synchronization
+* SSL Certificate Enrollment
+* SSL Certificate Revocation
+
+## Compatibility
+
+The DigiCert CertCentral Gateway 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.
+
+> 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.
+
+## Requirements
+
+An API Key within your Digicert account that has the necessary permissions to enroll, approve, and revoke certificates.
-# DigiCert CertCentral AnyCA REST Gateway Plugin
-
-DigiCert CertCentral plugin for the AnyCA REST Gateway framework
-
-#### Integration status: Production - Ready for use in production environments.
-
-## About the Keyfactor
-
-
-
-## Support for DigiCert CertCentral AnyCA REST Gateway Plugin
+## Installation
-DigiCert CertCentral AnyCA REST Gateway Plugin is supported by Keyfactor for Keyfactor customers. If you have a support issue, please open a support ticket via the Keyfactor Support Portal at https://support.keyfactor.com
+1. Install the AnyCA Gateway REST per the [official Keyfactor documentation](https://software.keyfactor.com/Guides/AnyCAGatewayREST/Content/AnyCAGatewayREST/InstallIntroduction.htm).
-###### 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.
+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.
----
+3. Copy the unzipped directory (usually called `net6.0` or `net8.0`) to the Extensions directory:
----
+ ```shell
+ Depending on your AnyCA Gateway REST version, copy the unzipped directory to one of the following locations:
+ Program Files\Keyfactor\AnyCA Gateway\AnyGatewayREST\net6.0\Extensions
+ Program Files\Keyfactor\AnyCA Gateway\AnyGatewayREST\net8.0\Extensions
+ ```
+ > 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.
+4. Restart the AnyCA Gateway REST service.
-
-# Introduction
+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.
-This AnyCA REST Gateway plug-in enables issuance, revocation, and synchronization of certificates from DigiCert's CertCentral offering.
-# Prerequisites
+## Configuration
-## Prerequisite: Certificate Chain
+1. Follow the [official AnyCA Gateway REST documentation](https://software.keyfactor.com/Guides/AnyCAGatewayREST/Content/AnyCAGatewayREST/AddCA-Gateway.htm) to define a new Certificate Authority, and use the notes below to configure the **Gateway Registration** and **CA Connection** tabs:
-In order to request certificates from the Keyfactor AnyGateway, the Keyfactor Command server must trust the certificate chain of trust. To ensure trust is established, download your Root and/or Subordinate CA certificates from DigiCert and import them into the appropriate local certificate stores on the Keyfactor AnyGateway and Command servers. More information can be found in the [AnyCA Gateway REST Install Guide](https://software.keyfactor.com/Guides/AnyCAGatewayREST/Content/AnyCAGatewayREST/Preparing.htm)
+ * **Gateway Registration**
-## Installation
-1. Download latest successful build from [GitHub Releases](../../releases/latest)
+ In order to enroll for certificates the Keyfactor Command server must trust the trust chain. Once you identify your Root and/or Subordinate CA in your Digicert account, make sure to download and import the certificate chain into the Command Server certificate store
-2. Extract the .zip file, and from it, copy DigicertCAPlugin.dll and DigicertCAPlugin.deps.json to the 'C:\Program Files\Keyfactor\AnyCA Gateway\AnyGatewayREST\net6.0\Extensions' directory
+ * **CA Connection**
-3. Within the 'C:\Program Files\Keyfactor\AnyCA Gateway\AnyGatewayREST\net6.0\Extensions\Connectors' folder, update the manifest.json file to contain the following:
+ Populate using the configuration fields collected in the [requirements](#requirements) section.
-```json
-{
- "extensions": {
- "Keyfactor.AnyGateway.Extensions.IAnyCAPlugin": {
- "CertCentralCAPlugin": {
- "assemblypath": "../DigicertCAPlugin.dll",
- "TypeFullName": "Keyfactor.Extensions.CAPlugin.DigiCert.CertCentralCAPlugin"
- }
- }
- }
-}
-```
+ * **APIKey** - API Key for connecting to DigiCert
+ * **DivisionId** - Division ID to use for retrieving product details (only if account is configured with per-divison product settings)
+ * **Region** - The geographic region that your DigiCert CertCentral account is in. Valid options are US and EU.
+ * **RevokeCertificateOnly** - Default DigiCert behavior on revocation requests is to revoke the entire order. If this value is changed to 'true', revocation requests will instead just revoke the individual certificate.
+ * **SyncCAFilter** - If you list one or more CA IDs here (comma-separated), the sync process will only sync records from those CAs. If you want to sync all CA IDs, leave this field empty.
+ * **SyncDivisionFilter** - If you list one or more Divison IDs (also known as Container IDs) here (comma-separated), the sync process will filter records to only return orders from those divisions. If you want to sync all divisions, leave this field empty. Note that this has no relationship to the value of the DivisionId config field.
+ * **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.
-NOTE: If the Connectors folder and/or the manifest.json file do not exist, they must be manually created
+2. Note for SMIME product types (Secure Email types): The template configuration fields provided for those are not required to be filled out in the gateway config. Many of those values would change on a per-enrollment basis. The way to handle that is to create Enrollment fields in Command with the same name (for example: CommonNameIndicator) and then any values populated in those fields will override any static values provided in the configuration.
-4. Restart the AnyCA Gateway service
+3. Follow the [official Keyfactor documentation](https://software.keyfactor.com/Guides/AnyCAGatewayREST/Content/AnyCAGatewayREST/AddCA-Keyfactor.htm) to add each defined Certificate Authority to Keyfactor Command and import the newly defined Certificate Templates.
-5. Navigate to the AnyCA Gateway REST portal and verify that the Gateway recognizes the DigiCert plugin by hovering over the ⓘ symbol to the right of the Gateway on the top left of the portal. CAPlugin Type should now be listed as CertCentralCA.
+4. In Keyfactor Command (v12.3+), for each imported Certificate Template, follow the [official documentation](https://software.keyfactor.com/Core-OnPrem/Current/Content/ReferenceGuide/Configuring%20Template%20Options.htm) to define enrollment fields for each of the following parameters:
+ * **LifetimeDays** - OPTIONAL: The number of days of validity to use when requesting certs. If not provided, default is 365.
+ * **CACertId** - OPTIONAL: ID of issuing CA to use by DigiCert. If not provided, the default for your account will be used.
+ * **Organization-Name** - OPTIONAL: For requests that will not have a subject (such as ACME) you can use this field to provide the organization name. Value supplied here will override any CSR values, so do not include this field if you want the organization from the CSR to be used.
+ * **RenewalWindowDays** - OPTIONAL: The number of days from certificate expiration that the gateway should do a renewal rather than a reissue. If not provided, default is 90.
+ * **CertType** - OPTIONAL: The type of cert to enroll for. Valid values are 'ssl' and 'client'. The value provided here must be consistant with the ProductID. If not provided, default is 'ssl'. Ignored for secure_email_* product types.
+ * **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.
+ * **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
-## Configuration
-
-1. Follow the [official Keyfactor AnyCA Gateway REST documentation](https://software.keyfactor.com/Guides/AnyCAGatewayREST/Content/AnyCAGatewayREST/AddCA-Gateway.htm#Add_or_Edit_a_Certificate_Authority) to define a new Certificate Authority, using the following information to configure the CA Connection section:
-SETTING | REQUIRED? | DESCRIPTION
---|--|--
-Enabled | Yes | Enables the DigiCert gateway functionality. Should almost always be set to 'true'
-APIKey | Yes | The API key the Gateway should use to communicate with the DigiCert API. Can be generated from the DigiCert portal.
-Region | No | The geographic region associated with your DigiCert account. Valid values are US and EU. Default if not provided is US.
-DivisionId | No | If your CertCentral account has multiple divisions AND uses any custom per-division product settings, provide a division ID for the gateway to use for product type lookups.
-RevokeCertificateOnly | No | If set to 'true', revoke operations will only revoke the individual certificate in question rather than the entire DigiCert order. Default if not provided is 'false'.
-SyncCAFilter | No | If you list one or more DigiCert issuing CA IDs here (comma-separated if more than one), the sync process will only return certs issued by one of those CAs. Leave this option empty to sync all certs from all CAs.
-FilterExpiredOrders | No | If set to 'true', syncing will not return certs that are expired more than a specified number of days. The number of days is specified by the SyncExpirationDays config option. Default value is 'false'.
-SyncExpirationDays | No | Only used if FilterExpiredOrders is 'true', otherwise ignored. Sets the number of days a cert has to be expired for the sync process to no longer sync it. For example, a value of 30 means sync will continue to return certs that have expired within the past 30 days, but not ones older than that. Default value is 0, meaning sync would not return any certs expired before the current day.
-2. After saving the CA configuration, Follow the [official AnyCA Gateway REST documentation](https://software.keyfactor.com/Guides/AnyCAGatewayREST/Content/AnyCAGatewayREST/AddCP-Gateway.htm#Certificate_Profile) to define one or more Certificate Profiles.
-3. Edit your newly configured CA, and you should now be able to modify the Templates tab. You need at least one template for each product type you wish to be able to enroll for. It is recommended to include the product type in the template name to make them easier to identify. Use the following information to configure the parameters for each template:
+## License
-SETTING | REQUIRED? | DESCRIPTION
---|--|--
-LifetimeDays | No | The number of days of validity to use when requesting certs. Default if not provided is 365. NOTE FOR RENEWALS: If the value of LifetimeDays is evenly divisible by 365, the expiration day and month of the new cert will be set to the same values as the old cert if possible, to avoid renewal date drift.
-CACertId | No | The ID of the issuing CA to be used by DigiCert. If not specified, the default for your account will be used.
-Organization-Name | No | If specified, this value will override any organization name provided in the subject of the cert request on enrollment. Useful for requests (such as ACME) that contain no subject.
-RenewalWindowDays | No | The number of days from expiration that the gateway should do a reissue rather than a renewal. Default if not provided is 90, meaning any renewal request for certs that expire in more than 90 days will be treated as a reissue request.
+Apache License 2.0, see [LICENSE](LICENSE).
+## Related Integrations
+See all [Keyfactor Any CA Gateways (REST)](https://github.com/orgs/Keyfactor/repositories?q=anycagateway).
\ No newline at end of file
diff --git a/digicert-certcentral-caplugin.sln b/digicert-certcentral-caplugin.sln
index 5425d02..f3f6414 100644
--- a/digicert-certcentral-caplugin.sln
+++ b/digicert-certcentral-caplugin.sln
@@ -8,7 +8,9 @@ EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{A6058317-1B5E-4E7C-9669-B3A6C0E605BB}"
ProjectSection(SolutionItems) = preProject
CHANGELOG.md = CHANGELOG.md
+ docsource\configuration.md = docsource\configuration.md
integration-manifest.json = integration-manifest.json
+ .github\workflows\keyfactor-bootstrap-workflow.yml = .github\workflows\keyfactor-bootstrap-workflow.yml
readme_source.md = readme_source.md
EndProjectSection
EndProject
diff --git a/digicert-certcentral-caplugin/API/ListCertificateOrders.cs b/digicert-certcentral-caplugin/API/ListCertificateOrders.cs
index b9d670e..7a62f6d 100644
--- a/digicert-certcentral-caplugin/API/ListCertificateOrders.cs
+++ b/digicert-certcentral-caplugin/API/ListCertificateOrders.cs
@@ -29,6 +29,7 @@ public ListCertificateOrdersRequest(bool ignoreExpired = false)
public bool ignoreExpired { get; set; }
public int expiredWindow { get; set; } = 0;
+ public string divID { get; set; } = string.Empty;
public new string BuildParameters()
{
@@ -37,6 +38,10 @@ 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))
+ {
+ sbParamters.Append("&filters[container_id]=").Append(this.divID);
+ }
if (ignoreExpired)
{
DateTime cutoffDate = DateTime.Today.AddDays(-1 - expiredWindow);
diff --git a/digicert-certcentral-caplugin/API/OrderCertificate.cs b/digicert-certcentral-caplugin/API/OrderCertificate.cs
index 9f82ea8..71746db 100644
--- a/digicert-certcentral-caplugin/API/OrderCertificate.cs
+++ b/digicert-certcentral-caplugin/API/OrderCertificate.cs
@@ -84,6 +84,9 @@ public class CertificateRequest
[JsonProperty("dns_names")]
public List DNSNames { get; set; }
+ [JsonProperty("emails")]
+ public List Emails { get; set; }
+
[JsonProperty("csr")]
public string CSR { get; set; }
diff --git a/digicert-certcentral-caplugin/API/OrderSmimeCertificate.cs b/digicert-certcentral-caplugin/API/OrderSmimeCertificate.cs
new file mode 100644
index 0000000..cf7010c
--- /dev/null
+++ b/digicert-certcentral-caplugin/API/OrderSmimeCertificate.cs
@@ -0,0 +1,142 @@
+using Keyfactor.Extensions.CAPlugin.DigiCert.Models;
+using Microsoft.VisualBasic;
+using Newtonsoft.Json;
+
+using System;
+using System.Collections.Generic;
+using System.ComponentModel;
+using System.Linq;
+using System.Security.Cryptography.X509Certificates;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace Keyfactor.Extensions.CAPlugin.DigiCert.API
+{
+ public class OrderSmimeRequest : CertCentralBaseRequest
+ {
+ public OrderSmimeRequest(CertCentralCertType certType)
+ {
+ Resource = "services/v2/order/certificate/" + certType.ProductCode;
+ Method = "POST";
+ CertType = certType;
+ Certificate = new SmimeCertificateRequest();
+ Certificate.Individual = new SmimeIndividual();
+ Certificate.UsageDesignation = new SmimeUsage();
+ Subject = new SmimeSubject();
+ CustomExpirationDate = null;
+ }
+
+ [JsonIgnore]
+ public CertCentralCertType CertType { get; set; }
+
+ [JsonProperty("certificate")]
+ public SmimeCertificateRequest Certificate { get; set; }
+
+ [JsonProperty("organization")]
+ private IdInformation Organization { get; set; } // Set via SetOrganization method
+
+ [JsonProperty("validity_years")]
+ public int ValidityYears { get; set; }
+
+ [JsonProperty("custom_expiration_date")] //YYYY-MM-DD
+ public DateTime? CustomExpirationDate { get; set; }
+
+ [JsonProperty("comments")]
+ public string Comments { get; set; }
+
+ [JsonProperty("disable_renewal_notifications")]
+ public bool DisableRenewalNotifications { get; set; }
+
+ [DefaultValue(0)]
+ [JsonProperty("renewal_of_order_id")]
+ public int RenewalOfOrderId { get; set; }
+
+ [JsonProperty("dcv_method")]
+ public string DCVMethod { get; set; }
+
+ [JsonProperty("container")]
+ public CertificateOrderContainer Container { get; set; }
+
+ [JsonProperty("custom_fields")]
+ public List CustomFields { get; set; }
+
+ [JsonProperty("skip_approval")]
+ public bool SkipApproval { get; set; }
+
+ [JsonProperty("subject")]
+ public SmimeSubject Subject { get; set; }
+
+ public void SetOrganization(int? organizationId)
+ {
+ if (organizationId.HasValue)
+ {
+ Organization = new IdInformation()
+ {
+ Id = organizationId.Value.ToString()
+ };
+ }
+ else
+ {
+ Organization = null;
+ }
+ }
+ }
+
+ public class SmimeSubject
+ {
+ [JsonProperty("include_pseudonym")]
+ public bool IncludePseudonym { get; set; }
+
+ [JsonProperty("include_email")]
+ public bool IncludeEmail { get; set; }
+
+ [JsonProperty("include_given_name_surname")]
+ public bool IncludeGivenName { get; set; }
+
+ }
+
+ public class SmimeCertificateRequest
+ {
+ [JsonProperty("emails")]
+ public List Emails { get; set; }
+
+ [JsonProperty("csr")]
+ public string CSR { get; set; }
+
+ [JsonProperty("signature_hash")]
+ public string SignatureHash { get; set; }
+
+ [JsonProperty("ca_cert_id")]
+ public string CACertID { get; set; }
+
+ [JsonProperty("common_name_indicator")]
+ public string CommonNameIndicator { get; set; }
+
+ [JsonProperty("individual")]
+ public SmimeIndividual Individual { get; set; }
+
+ [JsonProperty("usage_designation")]
+ public SmimeUsage UsageDesignation { get; set; }
+
+ [JsonProperty("profile_type")]
+ public string ProfileType { get; set; }
+ }
+
+ public class SmimeIndividual
+ {
+ [JsonProperty("first_name")]
+ public string FirstName { get; set; }
+
+ [JsonProperty("last_name")]
+ public string LastName { get; set; }
+
+ [JsonProperty("pseudonym")]
+ public string Pseudonym { get; set; }
+ }
+
+ public class SmimeUsage
+ {
+ [JsonProperty("primary_usage")]
+ public string PrimaryUsage { get; set; }
+ }
+}
diff --git a/digicert-certcentral-caplugin/API/ViewCertificateOrder.cs b/digicert-certcentral-caplugin/API/ViewCertificateOrder.cs
index a61f326..51ff61a 100644
--- a/digicert-certcentral-caplugin/API/ViewCertificateOrder.cs
+++ b/digicert-certcentral-caplugin/API/ViewCertificateOrder.cs
@@ -78,6 +78,9 @@ public ViewCertificateOrderResponse()
[JsonProperty("product")]
public Product product { get; set; }
+ [JsonProperty("container")]
+ public Container container { get; set; }
+
[JsonProperty("organization_contact")]
public Contact organization_contact { get; set; }
diff --git a/digicert-certcentral-caplugin/CertCentralCAPlugin.cs b/digicert-certcentral-caplugin/CertCentralCAPlugin.cs
index 9d44d6f..616f24c 100644
--- a/digicert-certcentral-caplugin/CertCentralCAPlugin.cs
+++ b/digicert-certcentral-caplugin/CertCentralCAPlugin.cs
@@ -11,6 +11,7 @@
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.VisualBasic;
+using Microsoft.Win32.SafeHandles;
using Newtonsoft.Json;
@@ -18,6 +19,7 @@
using System.Collections.Concurrent;
using System.Runtime.InteropServices;
+using System.Security.AccessControl;
using static Keyfactor.PKI.PKIConstants.Microsoft;
@@ -59,22 +61,25 @@ public async Task Enroll(string csr, string subject, Dictionar
{
_logger.MethodEntry(LogLevel.Trace);
- _logger.LogDebug($"Enrolling for certificate with subject {subject}");
- foreach (var sanlist in san)
+ string sans = string.Join(";", san.Select(s => string.Format("{0}:{1}", s.Key, string.Join(",", s.Value))));
+ string paramsList = string.Join(";", productInfo.ProductParameters.Select(x => string.Format("{0}={1}", x.Key, x.Value)));
+ _logger.LogTrace($"Attempting to enroll for certificate with:\nSubject: {subject}\nSANs: {sans}\nParams: {paramsList}\nCSR: {csr}");
+
+ if (Constants.ProductTypes.SMIME_CERT.Contains(productInfo.ProductID, StringComparer.OrdinalIgnoreCase))
{
- string sans = string.Join(",", sanlist.Value);
- _logger.LogDebug($"SANs type \"{sanlist.Key}\": {sans}");
+ return EnrollForSmimeCert(csr, subject, san, productInfo, enrollmentType);
}
OrderResponse orderResponse = new OrderResponse();
CertCentralCertType certType = CertCentralCertType.GetAllTypes(_config).FirstOrDefault(x => x.ProductCode.Equals(productInfo.ProductID));
OrderRequest orderRequest = new OrderRequest(certType);
- //var days = (productInfo.ProductParameters.ContainsKey("LifetimeDays") && !st) ? int.Parse(productInfo.ProductParameters["LifetimeDays"]) : 365;
+ string typeOfCert = (productInfo.ProductParameters.ContainsKey(CertCentralConstants.Config.CERT_TYPE)) ? productInfo.ProductParameters[CertCentralConstants.Config.CERT_TYPE].ToLower() : "ssl";
+
var days = 365;
- if (productInfo.ProductParameters.ContainsKey("LifetimeDays") && !string.IsNullOrEmpty(productInfo.ProductParameters["LifetimeDays"]))
+ if (productInfo.ProductParameters.ContainsKey(CertCentralConstants.Config.LIFETIME) && !string.IsNullOrEmpty(productInfo.ProductParameters[CertCentralConstants.Config.LIFETIME]))
{
- days = int.Parse(productInfo.ProductParameters["LifetimeDays"]);
+ days = int.Parse(productInfo.ProductParameters[CertCentralConstants.Config.LIFETIME]);
}
int validityYears = 0;
DateTime? customExpirationDate = null;
@@ -90,16 +95,31 @@ public async Task Enroll(string csr, string subject, Dictionar
break;
}
+ // Convert to case-insensitive dictionary
+ Dictionary sandict = new Dictionary(StringComparer.OrdinalIgnoreCase);
+ foreach (var s in san)
+ {
+ sandict.Add(s.Key, s.Value);
+ }
+
List dnsNames = new List();
- if (san.ContainsKey("Dns"))
+ List emails = new List();
+ if (sandict.ContainsKey("Dns"))
{
dnsNames = new List(san["Dns"]);
}
-
- if (san.ContainsKey("dnsname"))
+ if (sandict.ContainsKey("dnsname"))
{
dnsNames = new List(san["dnsname"]);
}
+ if (sandict.ContainsKey("Email"))
+ {
+ emails = new List(san["Email"]);
+ }
+ if (sandict.ContainsKey("Rfc822Name"))
+ {
+ emails = new List(san["Rfc822Name"]);
+ }
X509Name subjectParsed = null;
string commonName = null, organization = null, orgUnit = null;
@@ -114,13 +134,24 @@ public async Task Enroll(string csr, string subject, Dictionar
if (commonName == null)
{
- if (dnsNames.Count > 0)
+ if (typeOfCert.Equals("ssl") && dnsNames.Count > 0)
{
commonName = dnsNames[0];
}
+ else if (typeOfCert.Equals("client") && emails.Count > 0)
+ {
+ commonName = emails[0];
+ }
else
{
- throw new Exception("No Common Name or DNS SAN provided, unable to enroll");
+ throw new Exception("No Common Name or SAN provided, unable to enroll");
+ }
+ }
+ else
+ {
+ if (typeOfCert.Equals("client") && emails.Count == 0)
+ {
+ emails.Add(commonName);
}
}
@@ -180,6 +211,13 @@ public async Task Enroll(string csr, string subject, Dictionar
}
}
+ // Get Division ID (if present)
+ if (productInfo.ProductParameters.ContainsKey(CertCentralConstants.Config.ENROLL_DIVISION_ID) && !string.IsNullOrEmpty(productInfo.ProductParameters[CertCentralConstants.Config.ENROLL_DIVISION_ID]))
+ {
+ orderRequest.Container = new CertificateOrderContainer();
+ orderRequest.Container.Id = int.Parse(productInfo.ProductParameters[CertCentralConstants.Config.ENROLL_DIVISION_ID]);
+ }
+
// Get CA Cert ID (if present)
string caCertId = null;
if (productInfo.ProductParameters.ContainsKey("CACertId") && !string.IsNullOrEmpty(productInfo.ProductParameters["CACertId"]))
@@ -190,7 +228,14 @@ public async Task Enroll(string csr, string subject, Dictionar
orderRequest.Certificate.CommonName = commonName;
orderRequest.Certificate.CSR = csr;
orderRequest.Certificate.SignatureHash = signatureHash;
- orderRequest.Certificate.DNSNames = dnsNames;
+ if (typeOfCert.Equals("ssl"))
+ {
+ orderRequest.Certificate.DNSNames = dnsNames;
+ }
+ else if (typeOfCert.Equals("client"))
+ {
+ orderRequest.Certificate.Emails = emails;
+ }
orderRequest.Certificate.CACertID = caCertId;
orderRequest.SetOrganization(organizationId);
if (!string.IsNullOrEmpty(orgUnit))
@@ -353,7 +398,7 @@ public Dictionary GetCAConnectorAnnotations()
Comments = "Division ID to use for retrieving product details (only if account is configured with per-divison product settings)",
Hidden = false,
DefaultValue = "",
- Type = "Number"
+ Type = "String"
},
[CertCentralConstants.Config.REGION] = new PropertyConfigInfo()
{
@@ -377,6 +422,13 @@ public Dictionary GetCAConnectorAnnotations()
DefaultValue = "",
Type = "String"
},
+ [CertCentralConstants.Config.SYNC_DIV_FILTER] = new PropertyConfigInfo()
+ {
+ Comments = "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.",
+ 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.",
@@ -524,6 +576,62 @@ public Dictionary GetTemplateParameterAnnotations()
Hidden = false,
DefaultValue = 90,
Type = "Number"
+ },
+ [CertCentralConstants.Config.CERT_TYPE] = new PropertyConfigInfo()
+ {
+ Comments = "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.",
+ Hidden = false,
+ DefaultValue = "ssl",
+ Type = "String"
+ },
+ [CertCentralConstants.Config.ENROLL_DIVISION_ID] = new PropertyConfigInfo()
+ {
+ Comments = "OPTIONAL: The division (container) ID to use for enrollments against this template.",
+ Hidden = false,
+ DefaultValue = "",
+ Type = "String"
+ },
+ [CertCentralConstants.Config.COMMON_NAME_INDICATOR] = new PropertyConfigInfo()
+ {
+ Comments = "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",
+ Hidden = false,
+ DefaultValue = "",
+ Type = "String"
+ },
+ [CertCentralConstants.Config.PROFILE_TYPE] = new PropertyConfigInfo()
+ {
+ Comments = "Optional for secure_email_* types, ignored otherwise. Valid values are: strict, multipurpose. Default value is strict.",
+ Hidden = false,
+ DefaultValue = "strict",
+ Type = "String"
+ },
+ [CertCentralConstants.Config.FIRST_NAME] = new PropertyConfigInfo()
+ {
+ Comments = "Required for secure_email_* types if CommonNameIndicator is given_name_surname, ignored otherwise.",
+ Hidden = false,
+ DefaultValue = "",
+ Type = "String"
+ },
+ [CertCentralConstants.Config.LAST_NAME] = new PropertyConfigInfo()
+ {
+ Comments = "Required for secure_email_* types if CommonNameIndicator is given_name_surname, ignored otherwise.",
+ Hidden = false,
+ DefaultValue = "",
+ Type = "String"
+ },
+ [CertCentralConstants.Config.PSEUDONYM] = new PropertyConfigInfo()
+ {
+ Comments = "Required for secure_email_* types if CommonNameIndicator is pseudonym, ignored otherwise.",
+ Hidden = false,
+ DefaultValue = "",
+ Type = "String"
+ },
+ [CertCentralConstants.Config.SMIME_USAGE] = new PropertyConfigInfo()
+ {
+ Comments = "Required for secure_email_* types, ignored otherwise. The primary usage of the certificate. Valid values are: signing, key_management, dual_use",
+ Hidden = false,
+ DefaultValue = "",
+ Type = "String"
}
};
}
@@ -659,10 +767,16 @@ public async Task Synchronize(BlockingCollection blockin
caList.ForEach(c => c.ToUpper());
+ List divFilters = null;
+ if (!string.IsNullOrEmpty(_config.SyncDivisionFilter))
+ {
+ divFilters = new List();
+ divFilters.AddRange(_config.SyncDivisionFilter.Split(','));
+ }
if (fullSync)
{
- bool ignoreExpired = false; int expiredWindow = 0;
+ bool ignoreExpired = false; int expiredWindow = 0;
if (_config.FilterExpiredOrders.HasValue && _config.FilterExpiredOrders.Value)
{
ignoreExpired = true;
@@ -671,50 +785,73 @@ public async Task Synchronize(BlockingCollection blockin
expiredWindow = _config.SyncExpirationDays.Value;
}
}
+
long time = DateTime.Now.Ticks;
long starttime = time;
_logger.LogDebug($"SYNC: Starting sync at time {time}");
- ListCertificateOrdersResponse ordersResponse = client.ListAllCertificateOrders(ignoreExpired, expiredWindow);
- if (ordersResponse.Status == CertCentralBaseResponse.StatusType.ERROR)
+ List allOrders = new List();
+ if (divFilters != null)
{
- 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
- {
- _logger.LogDebug($"SYNC: Found {ordersResponse.orders.Count} records");
- foreach (var orderDetails in ordersResponse.orders)
+ foreach (string div in divFilters)
{
- List orderCerts = new List();
- try
+ ListCertificateOrdersResponse ordersResponse = client.ListAllCertificateOrders(ignoreExpired, expiredWindow, div);
+ if (ordersResponse.Status == CertCentralBaseResponse.StatusType.ERROR)
{
- cancelToken.ThrowIfCancellationRequested();
- string caReqId = orderDetails.id + "-" + orderDetails.certificate.id;
- _logger.LogDebug($"SYNC: Retrieving certs for order id {orderDetails.id}");
- orderCerts = GetAllConnectorCertsForOrder(caReqId, caList);
- if (orderCerts == null || orderCerts.Count == 0)
- {
- continue;
- }
- _logger.LogDebug($"SYNC: Retrieved {orderCerts.Count} certs at time {DateTime.Now.Ticks}");
+ 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");
}
- catch
+ else
{
- skippedOrders.Add(orderDetails.id.ToString());
- _logger.LogWarning($"An error occurred attempting to sync order '{orderDetails.id}'. This order will be skipped.");
- continue;
+ allOrders.AddRange(ordersResponse.orders);
}
-
- foreach (var cert in orderCerts)
+ }
+ }
+ 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);
+ }
+ }
+ _logger.LogDebug($"SYNC: Found {allOrders.Count} records");
+ foreach (var orderDetails in allOrders)
+ {
+ List orderCerts = new List();
+ try
+ {
+ cancelToken.ThrowIfCancellationRequested();
+ string caReqId = orderDetails.id + "-" + orderDetails.certificate.id;
+ _logger.LogDebug($"SYNC: Retrieving certs for order id {orderDetails.id}");
+ orderCerts = GetAllConnectorCertsForOrder(caReqId, caList, divFilters);
+ if (orderCerts == null || orderCerts.Count == 0)
{
- certCount++;
- blockingBuffer.Add(cert);
+ continue;
}
+ _logger.LogDebug($"SYNC: Retrieved {orderCerts.Count} certs at time {DateTime.Now.Ticks}");
+ }
+ catch
+ {
+ skippedOrders.Add(orderDetails.id.ToString());
+ _logger.LogWarning($"An error occurred attempting to sync order '{orderDetails.id}'. This order will be skipped.");
+ continue;
+ }
+ foreach (var cert in orderCerts)
+ {
+ certCount++;
+ blockingBuffer.Add(cert);
}
- _logger.LogDebug($"SYNC: Complete after {DateTime.Now.Ticks - starttime} ticks");
+
}
+ _logger.LogDebug($"SYNC: Complete after {DateTime.Now.Ticks - starttime} ticks");
}
else
{
@@ -735,7 +872,7 @@ public async Task Synchronize(BlockingCollection blockin
{
cancelToken.ThrowIfCancellationRequested();
string caReqId = order.order_id + "-" + order.certificate_id;
- orderCerts = GetAllConnectorCertsForOrder(caReqId, caList);
+ orderCerts = GetAllConnectorCertsForOrder(caReqId, caList, divFilters);
if (orderCerts == null || orderCerts.Count > 0)
{
continue;
@@ -886,13 +1023,17 @@ public async Task ValidateProductInfo(EnrollmentProductInfo productInfo, Diction
detailsRequest.ContainerId = null;
if (connectionInfo.ContainsKey(CertCentralConstants.Config.DIVISION_ID))
{
- if (int.TryParse($"{connectionInfo[CertCentralConstants.Config.DIVISION_ID]}", out int divId))
- {
- detailsRequest.ContainerId = divId;
- }
- else
+ string div = (string)connectionInfo[CertCentralConstants.Config.DIVISION_ID];
+ if (!string.IsNullOrWhiteSpace(div))
{
- throw new AnyCAValidationException($"Unable to parse division ID '{connectionInfo[CertCentralConstants.Config.DIVISION_ID]}'. Check that this is a valid division ID.");
+ if (int.TryParse($"{div}", out int divId))
+ {
+ detailsRequest.ContainerId = divId;
+ }
+ else
+ {
+ throw new AnyCAValidationException($"Unable to parse division ID '{div}'. Check that this is a valid division ID.");
+ }
}
}
@@ -901,6 +1042,18 @@ public async Task ValidateProductInfo(EnrollmentProductInfo productInfo, Diction
{
throw new AnyCAValidationException($"Validation of '{productId}' failed for the following reasons: {string.Join(" ", details.Errors.Select(x => x.message))}.");
}
+
+ if (!Constants.ProductTypes.SMIME_CERT.Contains(productInfo.ProductID, StringComparer.OrdinalIgnoreCase))
+ {
+ if (connectionInfo.ContainsKey(CertCentralConstants.Config.CERT_TYPE))
+ {
+ var typeOfCert = (string)connectionInfo[CertCentralConstants.Config.CERT_TYPE];
+ if (!(typeOfCert.Equals("ssl") || typeOfCert.Equals("client")))
+ {
+ throw new AnyCAValidationException("Invalid Cert Type specified. Valid options are 'ssl' or 'client'");
+ }
+ }
+ }
_logger.MethodExit(LogLevel.Trace);
}
@@ -918,6 +1071,12 @@ private async Task NewCertificate(CertCentralClient client, Or
return await ExtractEnrollmentResult(client, client.OrderCertificate(request), commonName);
}
+ private async Task NewSmimeCertificate(CertCentralClient client, OrderSmimeRequest request)
+ {
+ _logger.LogTrace("Attempting to enroll for a certificate.");
+ return await ExtractSmimeEnrollmentResult(client, client.OrderSmimeCertificate(request));
+ }
+
///
/// Gets the enrollment result from an object.
@@ -1042,6 +1201,63 @@ private async Task ExtractEnrollmentResult(CertCentralClient c
};
}
+ private async Task ExtractSmimeEnrollmentResult(CertCentralClient client, OrderResponse orderResponse)
+ {
+ int status = 0;
+ string statusMessage = null;
+ string certificate = null;
+ string caRequestID = null;
+
+ if (orderResponse.Status == CertCentralBaseResponse.StatusType.ERROR)
+ {
+ _logger.LogError($"Error from CertCentral client: {orderResponse.Errors.First().code}: {orderResponse.Errors.First().message}");
+
+ status = (int)EndEntityStatus.FAILED;
+ statusMessage = orderResponse.Errors[0].message;
+ }
+ else if (orderResponse.Status == CertCentralBaseResponse.StatusType.SUCCESS)
+ {
+ uint orderID = (uint)orderResponse.OrderId;
+ ViewCertificateOrderResponse certificateOrderResponse = client.ViewCertificateOrder(new ViewCertificateOrderRequest(orderID));
+ if (certificateOrderResponse.Status == CertCentralBaseResponse.StatusType.ERROR)
+ {
+ string errorMessage = $"Order {orderID} was not found for rejection in CertCentral database";
+ _logger.LogInformation(errorMessage);
+ throw new Exception(errorMessage);
+ }
+
+ status = GetCertificateStatusFromCA(certificateOrderResponse.status, (int)orderID);
+
+ ViewCertificateOrderResponse order = client.ViewCertificateOrder(new ViewCertificateOrderRequest((uint)orderResponse.OrderId));
+
+ // We don't worry about failures here, since the sync will update the cert if we can't get it right now for some reason.
+ if (order.Status != CertCentralBaseResponse.StatusType.ERROR)
+ {
+ caRequestID = $"{order.id}-{order.certificate.id}";
+ try
+ {
+ AnyCAPluginCertificate connCert = await GetSingleRecord($"{order.id}-{order.certificate.id}");
+ certificate = connCert.Certificate;
+ status = connCert.Status;
+ statusMessage = $"Post-submission approval of order {order.id} returned success";
+ }
+ catch (Exception getRecordEx)
+ {
+ _logger.LogWarning($"Unable to retrieve certificate {order.certificate.id} for order {order.id}: {getRecordEx.Message}");
+ status = (int)EndEntityStatus.INPROCESS;
+ statusMessage = $"Post-submission approval of order {order.id} was successful, but pickup failed";
+ }
+ }
+ }
+ return new EnrollmentResult
+ {
+ CARequestID = caRequestID,
+ Certificate = certificate,
+ Status = status,
+ StatusMessage = statusMessage
+ };
+ }
+
///
/// Convert DigiCert status string into a EndEntityStatus code
///
@@ -1268,6 +1484,27 @@ private async Task Renew(CertCentralClient client, OrderReques
return await ExtractEnrollmentResult(client, client.OrderCertificate(request), commonName);
}
+ private async Task RenewSmime(CertCentralClient client, OrderSmimeRequest request, EnrollmentProductInfo enrollmentProductInfo, string caRequestId)
+ {
+ CheckProductExistence(enrollmentProductInfo.ProductID);
+
+ int orderId = 0;
+ _logger.LogTrace("Parsing the order ID from the database certificate.");
+ try
+ {
+ orderId = int.Parse(caRequestId.Split('-').First());
+ }
+ catch (Exception e)
+ {
+ throw new Exception($"There was an error parsing the order ID from the certificate: {e.Message}", e);
+ }
+
+ request.RenewalOfOrderId = orderId;
+
+ _logger.LogTrace($"Attempting to renew certificate with order id {orderId}.");
+ return await ExtractSmimeEnrollmentResult(client, client.OrderSmimeCertificate(request));
+ }
+
string FormatSyncDate(DateTime? syncTime)
{
string date = syncTime.Value.Year + "-" + syncTime.Value.Month + "-" + syncTime.Value.Day;
@@ -1280,7 +1517,7 @@ string FormatSyncDate(DateTime? syncTime)
///
///
///
- private List GetAllConnectorCertsForOrder(string caRequestID, List caFilterIds)
+ private List GetAllConnectorCertsForOrder(string caRequestID, List caFilterIds, List divIds)
{
_logger.MethodEntry(LogLevel.Trace);
// Split ca request id into order and cert id
@@ -1298,6 +1535,11 @@ private List GetAllConnectorCertsForOrder(string caReque
_logger.LogTrace($"Found order ID {orderId} that does not match SyncCAFilter. CA ID: {orderResponse.certificate.ca_cert.Id} Skipping...");
return null;
}
+ if (divIds != null && divIds.Count > 0 && !divIds.Contains(orderResponse.container.Id.ToString()))
+ {
+ _logger.LogTrace($"Found order ID {orderId} that does not match Division filter. Division ID: {orderResponse.container.Id.ToString()} Skipping...");
+ return null;
+ }
var orderCerts = GetAllCertsForOrder(orderId);
@@ -1313,7 +1555,7 @@ private List GetAllConnectorCertsForOrder(string caReque
if (status == (int)EndEntityStatus.GENERATED || status == (int)EndEntityStatus.REVOKED)
{
// We have a status where there may be a cert to download, try to download it
- CertificateChainResponse certificateChainResponse = client.GetCertificateChain(new CertificateChainRequest(certId));
+ CertificateChainResponse certificateChainResponse = client.GetCertificateChain(new CertificateChainRequest($"{cert.certificate_id}"));
if (certificateChainResponse.Status == CertCentralBaseResponse.StatusType.SUCCESS)
{
certificate = certificateChainResponse.Intermediates[0].PEM;
@@ -1403,7 +1645,7 @@ private List GetAllCertsForOrder(int orderId)
// instead just retrieves multiple copies of the primary certificate on that order. Since the gateway database must have unique certificates
// (serial number column is unique), we work around this by only syncing the primary cert in these cases. Other orders that correctly retrieve the
// reissued/duplicate certificates will pass this check.
- if (!serNums.Contains(req))
+ if (!serNums.Contains(cert.serialNum))
{
reqIds.Add(req);
retCerts.Add(cert);
@@ -1417,5 +1659,296 @@ private List GetAllCertsForOrder(int orderId)
}
return retCerts;
}
+
+ private EnrollmentResult EnrollForSmimeCert(string csr, string subject, Dictionary san, EnrollmentProductInfo productInfo, EnrollmentType enrollmentType)
+ {
+ _logger.MethodEntry(LogLevel.Debug);
+
+ //Validate fields
+ string cnIndic = "";
+ if (productInfo.ProductParameters.ContainsKey(CertCentralConstants.Config.COMMON_NAME_INDICATOR))
+ {
+ cnIndic = productInfo.ProductParameters[CertCentralConstants.Config.COMMON_NAME_INDICATOR].ToString();
+ }
+ List validIndicators = new List() { "email_address", "given_name_surname", "pseudonym", "organization_name" };
+ // Valid value only required for secure_email_sponsor and secure_email_organization
+ if (!validIndicators.Contains(cnIndic, StringComparer.OrdinalIgnoreCase))
+ {
+ if (!productInfo.ProductID.Equals("secure_email_mailbox"))
+ {
+ throw new Exception($"Invalid CommonNameIndicator provided. Valid values are: {string.Join(',', validIndicators)}");
+ }
+ }
+
+ if (productInfo.ProductParameters.ContainsKey(CertCentralConstants.Config.PROFILE_TYPE))
+ {
+ string profile = productInfo.ProductParameters[CertCentralConstants.Config.PROFILE_TYPE].ToString();
+
+ // Only validate if value provided
+ if (!string.IsNullOrEmpty(profile))
+ {
+ List validProfiles = new List() { "strict", "multipurpose" };
+ if (!validProfiles.Contains(profile, StringComparer.OrdinalIgnoreCase))
+ {
+ throw new Exception($"Invalid profile type provided. Valid values are: strict, multipurpose");
+ }
+ }
+ }
+
+ if (cnIndic.Equals("given_name_surname", StringComparison.OrdinalIgnoreCase))
+ {
+ string first = "", last = "";
+ if (productInfo.ProductParameters.ContainsKey(CertCentralConstants.Config.FIRST_NAME))
+ {
+ first = productInfo.ProductParameters[CertCentralConstants.Config.FIRST_NAME].ToString();
+ }
+ if (productInfo.ProductParameters.ContainsKey(CertCentralConstants.Config.LAST_NAME))
+ {
+ last = productInfo.ProductParameters[CertCentralConstants.Config.LAST_NAME].ToString();
+ }
+
+ if (string.IsNullOrEmpty(first) || string.IsNullOrEmpty(last))
+ {
+ throw new Exception($"Both First and Last Name are required fields when CommonNameIndicator is given_name_surname");
+ }
+ }
+ else if (cnIndic.Equals("pseudonym", StringComparison.OrdinalIgnoreCase))
+ {
+ string pseu = "";
+ if (productInfo.ProductParameters.ContainsKey(CertCentralConstants.Config.PSEUDONYM))
+ {
+ pseu = productInfo.ProductParameters[CertCentralConstants.Config.PSEUDONYM].ToString();
+ }
+
+ if (string.IsNullOrEmpty(pseu))
+ {
+ throw new Exception($"Pseudonym is required when CommonNameIndicator is pseudonym");
+ }
+ }
+
+ string usage = "";
+ if (productInfo.ProductParameters.ContainsKey(CertCentralConstants.Config.SMIME_USAGE))
+ {
+ usage = productInfo.ProductParameters[CertCentralConstants.Config.SMIME_USAGE].ToString();
+ }
+ if (string.IsNullOrEmpty(usage))
+ {
+ throw new AnyCAValidationException($"UsageDesignation is required.");
+ }
+ else
+ {
+ List validUsage = new List() { "signing", "key_management", "dual_use" };
+ if (!validUsage.Contains(usage, StringComparer.OrdinalIgnoreCase))
+ {
+ throw new Exception($"Invalid UsageDesignation. Valid values are: {string.Join(',', validUsage)}");
+ }
+ }
+
+ CertCentralCertType certType = CertCentralCertType.GetAllTypes(_config).FirstOrDefault(x => x.ProductCode.Equals(productInfo.ProductID));
+ OrderSmimeRequest orderRequest = new OrderSmimeRequest(certType);
+
+ var days = 365;
+ if (productInfo.ProductParameters.ContainsKey(CertCentralConstants.Config.LIFETIME) && !string.IsNullOrEmpty(productInfo.ProductParameters[CertCentralConstants.Config.LIFETIME]))
+ {
+ days = int.Parse(productInfo.ProductParameters[CertCentralConstants.Config.LIFETIME]);
+ }
+ int validityYears = 0;
+ DateTime? customExpirationDate = null;
+ switch (days)
+ {
+ case 365:
+ case 730:
+ case 1095:
+ validityYears = days / 365;
+ break;
+ default:
+ customExpirationDate = DateTime.Now.AddDays(days);
+ break;
+ }
+
+ X509Name subjectParsed = null;
+ string commonName = null, organization = null;
+ try
+ {
+ subjectParsed = new X509Name(subject);
+ commonName = subjectParsed.GetValueList(X509Name.CN).Cast().LastOrDefault();
+ organization = subjectParsed.GetValueList(X509Name.O).Cast().LastOrDefault();
+ }
+ catch (Exception) { }
+
+ var cnIndicator = productInfo.ProductParameters[Constants.Config.COMMON_NAME_INDICATOR];
+ _logger.LogTrace($"Using Common Name Indicator: {cnIndicator}");
+ List emails = new List();
+ if (productInfo.ProductID.Equals("secure_email_mailbox", StringComparison.OrdinalIgnoreCase))
+ {
+ cnIndicator = "email_address";
+ }
+ if (cnIndicator.Equals("email_address", StringComparison.OrdinalIgnoreCase))
+ {
+ emails.Add(commonName);
+ }
+
+ // Convert to case-insensitive dictionary
+ Dictionary sandict = new Dictionary(StringComparer.OrdinalIgnoreCase);
+ foreach (var s in san)
+ {
+ sandict.Add(s.Key, s.Value);
+ }
+
+
+ if (sandict.ContainsKey("Email"))
+ {
+ emails.AddRange(san["Email"]);
+ }
+ if (sandict.ContainsKey("Rfc822Name"))
+ {
+ emails.AddRange(san["Rfc822Name"]);
+ }
+
+ if (productInfo.ProductParameters.TryGetValue(CertCentralConstants.RequestAttributes.ORGANIZATION_NAME, out string orgName))
+ {
+ // If org name is provided as a parameter, it overrides whatever is in the CSR
+ if (!string.IsNullOrEmpty(orgName))
+ {
+ organization = orgName;
+ }
+ }
+
+ CertCentralClient client = CertCentralClientUtilities.BuildCertCentralClient(_config);
+ int? organizationId = null;
+ if (organization == null)
+ {
+ throw new Exception("No organization provided in either subject or attributes, unable to enroll");
+ }
+
+ ListOrganizationsResponse organizations = client.ListOrganizations(new ListOrganizationsRequest());
+ if (organizations.Status == CertCentralBaseResponse.StatusType.ERROR)
+ {
+ _logger.LogError($"Error from CertCentral client: {organizations.Errors.First().message}");
+ }
+
+ Organization org = organizations.Organizations.FirstOrDefault(x => x.Name.Equals(organization, StringComparison.OrdinalIgnoreCase));
+ if (org != null)
+ {
+ organizationId = org.Id;
+ }
+ else
+ {
+ throw new Exception($"Organization '{organization}' is invalid for this account, please check name");
+ }
+
+ // Process metadata fields
+ orderRequest.CustomFields = new List();
+ var metadataResponse = client.ListMetadata(new ListMetadataRequest());
+ if (metadataResponse.MetadataFields != null && metadataResponse.MetadataFields.Count > 0)
+ {
+ var metadata = metadataResponse.MetadataFields.Where(m => m.Active).ToList();
+ _logger.LogTrace($"Found {metadata.Count()} active metadata fields in the account");
+ foreach (var field in metadata)
+ {
+ // See if the field has been provided in the request
+ if (productInfo.ProductParameters.TryGetValue(field.Label, out string fieldValue))
+ {
+ _logger.LogTrace($"Found {field.Label} in the request, adding...");
+ orderRequest.CustomFields.Add(new MetadataField() { MetadataId = field.Id, Value = fieldValue });
+ }
+ }
+ }
+
+ // Get Division ID (if present)
+ if (productInfo.ProductParameters.ContainsKey(CertCentralConstants.Config.ENROLL_DIVISION_ID) && !string.IsNullOrEmpty(productInfo.ProductParameters[CertCentralConstants.Config.ENROLL_DIVISION_ID]))
+ {
+ orderRequest.Container = new CertificateOrderContainer();
+ orderRequest.Container.Id = int.Parse(productInfo.ProductParameters[CertCentralConstants.Config.ENROLL_DIVISION_ID]);
+ _logger.LogTrace($"Using division ID: {orderRequest.Container.Id}");
+ }
+
+ // Get CA Cert ID (if present)
+ string caCertId = null;
+ if (productInfo.ProductParameters.ContainsKey("CACertId") && !string.IsNullOrEmpty(productInfo.ProductParameters["CACertId"]))
+ {
+ caCertId = (string)productInfo.ProductParameters["CACertId"];
+ _logger.LogTrace($"Using CACertID: {caCertId}");
+ }
+
+ if (customExpirationDate != null)
+ {
+ orderRequest.CustomExpirationDate = customExpirationDate;
+ }
+ else
+ {
+ orderRequest.ValidityYears = validityYears;
+ }
+ orderRequest.Certificate.Emails = emails;
+ orderRequest.Certificate.CSR = csr;
+ orderRequest.Certificate.SignatureHash = certType.signatureAlgorithm;
+ orderRequest.Certificate.CACertID = caCertId;
+ orderRequest.SetOrganization(organizationId);
+ string profileType = "strict";
+ if (productInfo.ProductParameters.ContainsKey(Constants.Config.PROFILE_TYPE))
+ {
+ profileType = productInfo.ProductParameters[Constants.Config.PROFILE_TYPE];
+ }
+ orderRequest.Certificate.ProfileType = profileType;
+ orderRequest.Certificate.CommonNameIndicator = cnIndicator;
+ if (productInfo.ProductID.Equals("secure_email_sponsor", StringComparison.OrdinalIgnoreCase))
+ {
+ if (cnIndicator.Equals("given_name_surname", StringComparison.OrdinalIgnoreCase))
+ {
+ orderRequest.Certificate.Individual.FirstName = productInfo.ProductParameters[Constants.Config.FIRST_NAME];
+ orderRequest.Certificate.Individual.LastName = productInfo.ProductParameters[Constants.Config.LAST_NAME];
+ }
+ else if (cnIndicator.Equals("pseudonym", StringComparison.OrdinalIgnoreCase))
+ {
+ orderRequest.Certificate.Individual.Pseudonym = productInfo.ProductParameters[Constants.Config.PSEUDONYM];
+ }
+ }
+ orderRequest.Certificate.UsageDesignation.PrimaryUsage = productInfo.ProductParameters[Constants.Config.SMIME_USAGE];
+
+ if (cnIndicator.Equals("email_address", StringComparison.OrdinalIgnoreCase))
+ {
+ orderRequest.Subject.IncludeEmail = true;
+ }
+ else if (cnIndicator.Equals("given_name_surname", StringComparison.OrdinalIgnoreCase))
+ {
+ orderRequest.Subject.IncludeGivenName = true;
+ }
+ else if (cnIndicator.Equals("pseudonym", StringComparison.OrdinalIgnoreCase))
+ {
+ orderRequest.Subject.IncludePseudonym = true;
+ }
+
+ orderRequest.DCVMethod = "email";
+
+ string priorCertSnString = null;
+ string priorCertReqID = null;
+ if (enrollmentType == EnrollmentType.RenewOrReissue)
+ {
+ enrollmentType = EnrollmentType.Renew;
+ }
+
+ if (enrollmentType == EnrollmentType.Renew)
+ {
+ priorCertReqID = _certificateDataReader.GetRequestIDBySerialNumber(priorCertSnString).Result;
+ if (string.IsNullOrEmpty(priorCertReqID))
+ {
+ throw new Exception($"No certificate with serial number '{priorCertSnString}' could be found.");
+ }
+ }
+
+ orderRequest.SkipApproval = true;
+
+ switch (enrollmentType)
+ {
+ case EnrollmentType.New:
+ return NewSmimeCertificate(client, orderRequest).Result;
+
+ case EnrollmentType.Renew:
+ return RenewSmime(client, orderRequest, productInfo, priorCertReqID).Result;
+
+ default:
+ throw new Exception($"The enrollment type '{enrollmentType}' is invalid for this cert type.");
+ }
+ }
}
-}
\ No newline at end of file
+}
diff --git a/digicert-certcentral-caplugin/CertCentralConfig.cs b/digicert-certcentral-caplugin/CertCentralConfig.cs
index 0bed1ea..fa3b354 100644
--- a/digicert-certcentral-caplugin/CertCentralConfig.cs
+++ b/digicert-certcentral-caplugin/CertCentralConfig.cs
@@ -36,5 +36,6 @@ public List SyncCAs
public bool? FilterExpiredOrders { get; set; }
public int? SyncExpirationDays { get; set; }
+ public string SyncDivisionFilter { get; set; }
}
}
diff --git a/digicert-certcentral-caplugin/Client/CertCentralClient.cs b/digicert-certcentral-caplugin/Client/CertCentralClient.cs
index c700a3c..2138f52 100644
--- a/digicert-certcentral-caplugin/Client/CertCentralClient.cs
+++ b/digicert-certcentral-caplugin/Client/CertCentralClient.cs
@@ -295,7 +295,32 @@ public ListMetadataResponse ListMetadata(ListMetadataRequest request)
public OrderResponse OrderCertificate(OrderRequest request)
{
- CertCentralResponse response = Request(request, JsonConvert.SerializeObject(request, Formatting.None, new JsonSerializerSettings { NullValueHandling = NullValueHandling.Ignore }));
+ string jsonRequest = JsonConvert.SerializeObject(request, Formatting.None, new JsonSerializerSettings { NullValueHandling = NullValueHandling.Ignore });
+
+ Logger.LogTrace($"Order request:\n{jsonRequest}");
+
+ CertCentralResponse response = Request(request, jsonRequest);
+
+ OrderResponse orderResponse = new OrderResponse();
+ if (!response.Success)
+ {
+ Errors errors = JsonConvert.DeserializeObject(response.Response);
+ orderResponse.Status = CertCentralBaseResponse.StatusType.ERROR;
+ orderResponse.Errors = errors.errors;
+ }
+ else
+ orderResponse = JsonConvert.DeserializeObject(response.Response);
+
+ return orderResponse;
+ }
+
+ public OrderResponse OrderSmimeCertificate(OrderSmimeRequest request)
+ {
+ string jsonRequest = JsonConvert.SerializeObject(request, Formatting.None, new JsonSerializerSettings { NullValueHandling = NullValueHandling.Ignore });
+
+ Logger.LogTrace($"Order request:\n{jsonRequest}");
+
+ CertCentralResponse response = Request(request, jsonRequest);
OrderResponse orderResponse = new OrderResponse();
if (!response.Success)
@@ -312,7 +337,10 @@ public OrderResponse OrderCertificate(OrderRequest request)
public OrderResponse ReissueCertificate(ReissueRequest request)
{
- CertCentralResponse response = Request(request, JsonConvert.SerializeObject(request, Formatting.None, new JsonSerializerSettings { NullValueHandling = NullValueHandling.Ignore }));
+ string jsonRequest = JsonConvert.SerializeObject(request, Formatting.None, new JsonSerializerSettings { NullValueHandling = NullValueHandling.Ignore });
+ Logger.LogTrace($"Reissue request:\n{jsonRequest}");
+
+ CertCentralResponse response = Request(request, jsonRequest);
OrderResponse reissueResponse = new OrderResponse();
if (!response.Success)
@@ -473,7 +501,7 @@ public DownloadCertificateByFormatResponse DownloadCertificateByFormat(DownloadC
return dlCertificateRequestResponse;
}
- public ListCertificateOrdersResponse ListAllCertificateOrders(bool ignoreExpired = false, int expiredWindow = 0)
+ public ListCertificateOrdersResponse ListAllCertificateOrders(bool ignoreExpired = false, int expiredWindow = 0, string divId = "")
{
int batch = 1000;
ListCertificateOrdersResponse totalResponse = new ListCertificateOrdersResponse();
@@ -485,7 +513,8 @@ public ListCertificateOrdersResponse ListAllCertificateOrders(bool ignoreExpired
limit = batch,
offset = totalResponse.orders.Count,
ignoreExpired = ignoreExpired,
- expiredWindow = expiredWindow
+ expiredWindow = expiredWindow,
+ divID = divId
};
CertCentralResponse response = Request(request, request.BuildParameters());
diff --git a/digicert-certcentral-caplugin/Constants.cs b/digicert-certcentral-caplugin/Constants.cs
index 609ec3f..57e4ed2 100644
--- a/digicert-certcentral-caplugin/Constants.cs
+++ b/digicert-certcentral-caplugin/Constants.cs
@@ -28,8 +28,17 @@ public class Config
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 FILTER_EXPIRED = "FilterExpiredOrders";
public const string SYNC_EXPIRATION_DAYS = "SyncExpirationDays";
+ public const string CERT_TYPE = "CertType";
+ public const string ENROLL_DIVISION_ID = "EnrollDivisionId";
+ public const string COMMON_NAME_INDICATOR = "CommonNameIndicator";
+ public const string PROFILE_TYPE = "ProfileType";
+ public const string FIRST_NAME = "FirstName";
+ public const string LAST_NAME = "LastName";
+ public const string PSEUDONYM = "Pseudonym";
+ public const string SMIME_USAGE = "UsageDesignation";
}
public class RequestAttributes
@@ -41,6 +50,7 @@ public class RequestAttributes
public class ProductTypes
{
public const string DV_SSL_CERT = "dv_ssl_certificate";
+ public static readonly List SMIME_CERT = new List() { "secure_email_mailbox", "secure_email_sponsor", "secure_email_organization" };
}
}
}
diff --git a/digicert-certcentral-caplugin/digicert-certcentral-caplugin.csproj b/digicert-certcentral-caplugin/digicert-certcentral-caplugin.csproj
index c09a0b8..9edfa7f 100644
--- a/digicert-certcentral-caplugin/digicert-certcentral-caplugin.csproj
+++ b/digicert-certcentral-caplugin/digicert-certcentral-caplugin.csproj
@@ -1,7 +1,7 @@
- net6.0
+ net6.0;net8.0
Keyfactor.Extensions.CAPlugin.DigiCert
enable
disable
@@ -16,4 +16,10 @@
+
+
+ Always
+
+
+
diff --git a/digicert-certcentral-caplugin/manifest.json b/digicert-certcentral-caplugin/manifest.json
new file mode 100644
index 0000000..da8348d
--- /dev/null
+++ b/digicert-certcentral-caplugin/manifest.json
@@ -0,0 +1,10 @@
+{
+ "extensions": {
+ "Keyfactor.AnyGateway.Extensions.IAnyCAPlugin": {
+ "DigicertCAPlugin": {
+ "assemblypath": "DigicertCAPlugin.dll",
+ "TypeFullName": "Keyfactor.Extensions.CAPlugin.DigiCert.CertCentralCAPlugin"
+ }
+ }
+ }
+}
diff --git a/docsource/configuration.md b/docsource/configuration.md
new file mode 100644
index 0000000..3292f12
--- /dev/null
+++ b/docsource/configuration.md
@@ -0,0 +1,20 @@
+## Overview
+
+The Digicert CertCentral AnyCA REST plugin extends the capabilities of Digicert's CertCentral product to Keyfactor Command via the Keyfactor AnyCA Gateway REST. The plugin represents a fully featured AnyCA REST Plugin with the following capabilies:
+* SSL Certificate Synchronization
+* SSL Certificate Enrollment
+* SSL Certificate Revocation
+
+
+## Requirements
+
+An API Key within your Digicert account that has the necessary permissions to enroll, approve, and revoke certificates.
+
+## Gateway Registration
+
+In order to enroll for certificates the Keyfactor Command server must trust the trust chain. Once you identify your Root and/or Subordinate CA in your Digicert account, make sure to download and import the certificate chain into the Command Server certificate store
+
+## Certificate Template Creation Step
+
+Note for SMIME product types (Secure Email types): The template configuration fields provided for those are not required to be filled out in the gateway config. Many of those values would change on a per-enrollment basis. The way to handle that is to create Enrollment fields in Command with the same name (for example: CommonNameIndicator) and then any values populated in those fields will override any static values provided in the configuration.
+
diff --git a/integration-manifest.json b/integration-manifest.json
index 62f97bf..ef10ed9 100644
--- a/integration-manifest.json
+++ b/integration-manifest.json
@@ -1,12 +1,106 @@
{
- "$schema": "https://keyfactor.github.io/integration-manifest-schema.json",
- "integration_type": "anyca-plugin",
- "name": "DigiCert CertCentral AnyCA REST Gateway Plugin",
- "status": "production",
- "support_level": "kf-supported",
- "link_github": true,
- "update_catalog": true,
- "description": "DigiCert CertCentral plugin for the AnyCA REST Gateway framework",
- "gateway_framework": "24.2.0",
- "release_dir": "digicert-certcentral-caplugin/bin/Release/net6.0"
-}
+ "$schema": "https://keyfactor.github.io/v2/integration-manifest-schema.json",
+ "integration_type": "anyca-plugin",
+ "name": "DigiCert CertCentral AnyCA REST Gateway Plugin",
+ "status": "production",
+ "support_level": "kf-supported",
+ "link_github": true,
+ "update_catalog": true,
+ "description": "DigiCert CertCentral plugin for the AnyCA REST Gateway framework",
+ "gateway_framework": "24.2.0",
+ "release_dir": "digicert-certcentral-caplugin/bin/Release",
+ "release_project": "digicert-certcentral-caplugin/digicert-certcentral-caplugin.csproj",
+ "about": {
+ "carest": {
+ "product_ids": [],
+ "ca_plugin_config": [
+ {
+ "name": "APIKey",
+ "description": "API Key for connecting to DigiCert"
+ },
+ {
+ "name": "DivisionId",
+ "description": "Division ID to use for retrieving product details (only if account is configured with per-divison product settings)"
+ },
+ {
+ "name": "Region",
+ "description": "The geographic region that your DigiCert CertCentral account is in. Valid options are US and EU."
+ },
+ {
+ "name": "RevokeCertificateOnly",
+ "description": "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."
+ },
+ {
+ "name": "SyncCAFilter",
+ "description": "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."
+ },
+ {
+ "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": "FilterExpiredOrders",
+ "description": "If set to 'true', syncing will apply a filter to not return orders that are expired for longer than specified in SyncExpirationDays."
+ },
+ {
+ "name": "SyncExpirationDays",
+ "description": "If FilterExpiredOrders is set to true, this setting determines how many days in the past to still return expired orders. For example, a value of 30 means the sync will return any certs that expired within the past 30 days. A value of 0 means the sync will not return any certs that expired before the current day. This value is ignored if FilterExpiredOrders is false."
+ },
+ {
+ "name": "Enabled",
+ "description": "Flag to Enable or Disable gateway functionality. Disabling is primarily used to allow creation of the CA prior to configuration information being available."
+ }
+ ],
+ "enrollment_config": [
+ {
+ "name": "LifetimeDays",
+ "description": "OPTIONAL: The number of days of validity to use when requesting certs. If not provided, default is 365."
+ },
+ {
+ "name": "CACertId",
+ "description": "OPTIONAL: ID of issuing CA to use by DigiCert. If not provided, the default for your account will be used."
+ },
+ {
+ "name": "Organization-Name",
+ "description": "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."
+ },
+ {
+ "name": "RenewalWindowDays",
+ "description": "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."
+ },
+ {
+ "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": "EnrollDivisionId",
+ "description": "OPTIONAL: The division (container) ID to use for enrollments against this template."
+ },
+ {
+ "name": "CommonNameIndicator",
+ "description": "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"
+ },
+ {
+ "name": "ProfileType",
+ "description": "Optional for secure_email_* types, ignored otherwise. Valid values are: strict, multipurpose. Default value is strict."
+ },
+ {
+ "name": "FirstName",
+ "description": "Required for secure_email_* types if CommonNameIndicator is given_name_surname, ignored otherwise."
+ },
+ {
+ "name": "LastName",
+ "description": "Required for secure_email_* types if CommonNameIndicator is given_name_surname, ignored otherwise."
+ },
+ {
+ "name": "Pseudonym",
+ "description": "Required for secure_email_* types if CommonNameIndicator is pseudonym, ignored otherwise."
+ },
+ {
+ "name": "UsageDesignation",
+ "description": "Required for secure_email_* types, ignored otherwise. The primary usage of the certificate. Valid values are: signing, key_management, dual_use"
+ }
+ ]
+ }
+ }
+}
\ No newline at end of file
diff --git a/readme_source.md b/readme_source.md
index f436883..22daa49 100644
--- a/readme_source.md
+++ b/readme_source.md
@@ -59,3 +59,17 @@ LifetimeDays | No | The number of days of validity to use when requesting certs.
CACertId | No | The ID of the issuing CA to be used by DigiCert. If not specified, the default for your account will be used.
Organization-Name | No | If specified, this value will override any organization name provided in the subject of the cert request on enrollment. Useful for requests (such as ACME) that contain no subject.
RenewalWindowDays | No | The number of days from expiration that the gateway should do a reissue rather than a renewal. Default if not provided is 90, meaning any renewal request for certs that expire in more than 90 days will be treated as a reissue request.
+CertType | No | Allows you to specify whether the certs of this template are ssl or client certs. Valid values: ssl, client. If not provided, defaults to ssl.
+EnrollDivisionId | No | The division (container) ID to use for enrollments against this template.
+
+NOTE FOR SMIME CERTIFICATES:
+For product IDs secure_email_mailbox, secure_email_sponsor, and secure_email_organization, the following settings apply. Note that while they CAN be configured at the template level in the config portal, since most of these values might be enrollment specific, they can also be configured in Command as Enrollment Fields with the same name.
+
+SETTING | DESCRIPTION
+--|--
+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.
+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