diff --git a/.github/workflows/keyfactor-bootstrap-workflow.yml b/.github/workflows/keyfactor-bootstrap-workflow.yml index 722cb97..6e592a3 100644 --- a/.github/workflows/keyfactor-bootstrap-workflow.yml +++ b/.github/workflows/keyfactor-bootstrap-workflow.yml @@ -11,7 +11,7 @@ on: jobs: call-starter-workflow: - uses: keyfactor/actions/.github/workflows/starter.yml@v4 + uses: keyfactor/actions/.github/workflows/starter.yml@v5 secrets: token: ${{ secrets.V2BUILDTOKEN}} scan_token: ${{ secrets.SAST_TOKEN }} diff --git a/CHANGELOG.md b/CHANGELOG.md index a571c73..acdf627 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -39,3 +39,15 @@ ### 2.4.1 * Fix for missing parameter errors + +### 2.5.0 +* Fix for checking if product is a DV cert to ignore organization checks +* Performance enhancements to reduce API calls +* Added null checks/error handling to catch bad certs on sync +* Improved rate limiting handling based on DigiCert guidance +* Improved handling of API error responses +* Fixes for sync filtering +* Incremental sync fix + +### 3.0.0 +* Add support for automated domain validation via DNS gateway plugins. diff --git a/README.md b/README.md index 6f735fc..fe7f318 100644 --- a/README.md +++ b/README.md @@ -14,7 +14,7 @@ Support - + · Requirements @@ -33,7 +33,6 @@

- 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 @@ -41,10 +40,10 @@ The Digicert CertCentral AnyCA REST plugin extends the capabilities of Digicert' ## Compatibility -The DigiCert CertCentral AnyCA Gateway REST plugin is compatible with the Keyfactor AnyCA Gateway REST 24.2.0 and later. +The DigiCert CertCentral AnyCA Gateway REST plugin is compatible with the Keyfactor AnyCA Gateway REST 26.2.0 and later. ## Support -The DigiCert CertCentral AnyCA Gateway REST plugin is supported by Keyfactor for Keyfactor customers. If you have a support issue, please open a support ticket with your Keyfactor representative. If you have a support issue, please open a support ticket via the Keyfactor Support Portal at https://support.keyfactor.com. +The DigiCert CertCentral AnyCA Gateway REST plugin is supported by Keyfactor for Keyfactor customers. If you have a support issue, please open a support ticket 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. @@ -58,16 +57,16 @@ An API Key within your Digicert account that has the necessary permissions to en 2. On the server hosting the AnyCA Gateway REST, download and unzip the latest [DigiCert CertCentral AnyCA Gateway REST plugin](https://github.com/Keyfactor/digicert-certcentral-caplugin/releases/latest) from GitHub. -3. Copy the unzipped directory (usually called `net6.0` or `net8.0`) to the Extensions directory: +3. Copy the unzipped directory (usually called `net8.0` or `net10.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 + Program Files\Keyfactor\AnyCA Gateway\AnyGatewayREST\net10.0\Extensions ``` - > The directory containing the DigiCert CertCentral AnyCA Gateway REST plugin DLLs (`net6.0` or `net8.0`) can be named anything, as long as it is unique within the `Extensions` directory. + > The directory containing the DigiCert CertCentral AnyCA Gateway REST plugin DLLs (`net8.0` or `net10.0`) can be named anything, as long as it is unique within the `Extensions` directory. 4. Restart the AnyCA Gateway REST service. @@ -80,21 +79,32 @@ An API Key within your Digicert account that has the necessary permissions to en * **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 + + ### Automated DNS Domain Validation + + This plugin integrates with the AnyCA Gateway **DNS provider plugin framework** (`KeyfactorAnyGateway.IAnyCAPlugin` 3.3.0+). DNS provider plugins (Azure DNS, AWS Route53, Cloudflare, Google Cloud DNS, etc.) are deployed and configured **separately** on the gateway; this CA plugin does not bundle any DNS provider SDKs. The gateway injects an `IDomainValidatorFactory` that resolves the correct provider for each domain at enrollment time. + + DigiCert supports both **TXT** and **CNAME** records for DNS validation, the choice of which is provided by the appropriate configuration field. **TXT** records are the preferred method. + + `DnsValidationMethod` defines whether you wish to use TXT, CNAME, or email validation. Only TXT or CNAME will work with the automated validation. + `DnsValidationEnabled` determines whether to use the automated validation. Make sure you have the necessary DNS plugins installed and configured before enabling. If `DnsValidationMethod` is set to either TXT or CNAME but `DnsValidationEnabled` is false, then unvalidated enrollment requests will get a status of External Validation, and the necessary TXT or CNAME token will be instead returned to the enrollment caller to be used to manually update the DNS record. * **CA Connection** Populate using the configuration fields collected in the [requirements](#requirements) section. - * **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. - * **SyncProductFilter** - If you list one or more Product IDs here (comma-separated), the sync process will filter records to only return orders of those product types. Leave empty to sync all products. - * **FilterExpiredOrders** - If set to 'true', syncing will apply a filter to not return orders that are expired for longer than specified in SyncExpirationDays. - * **SyncExpirationDays** - If FilterExpiredOrders is set to true, this setting determines how many days in the past to still return expired orders. For example, a value of 30 means the sync will return any certs that expired within the past 30 days. A value of 0 means the sync will not return any certs that expired before the current day. This value is ignored if FilterExpiredOrders is false. - * **Enabled** - Flag to Enable or Disable gateway functionality. Disabling is primarily used to allow creation of the CA prior to configuration information being available. + * **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. + * **SyncProductFilter** - If you list one or more Product IDs here (comma-separated), the sync process will filter records to only return orders of those product types. Leave empty to sync all products. + * **FilterExpiredOrders** - If set to 'true', syncing will apply a filter to not return orders that are expired for longer than specified in SyncExpirationDays. + * **SyncExpirationDays** - If FilterExpiredOrders is set to true, this setting determines how many days in the past to still return expired orders. For example, a value of 30 means the sync will return any certs that expired within the past 30 days. A value of 0 means the sync will not return any certs that expired before the current day. This value is ignored if FilterExpiredOrders is false. + * **DnsValidationMethod** - The DNS validation method to use. Default value is 'email'. Other valid values are 'txt' and 'cname' If using automated DNS validation, 'txt' is the preferred method. + * **DnsValidationEnabled** - Enable automated DNS (TXT or CNAME) domain control validation. When enabled, the plugin requests TXT-based validation from DigiCert and publishes the returned record via the DNS provider plugin resolved by the AnyCA Gateway. Requires a DNS provider plugin (e.g. Azure, Cloudflare, etc) to be deployed and configured on the gateway. When disabled, requests that require validation will be flagged as External Validation, and the validation token, if needed depending on the DNS Validation method, will be returned. + * **Enabled** - Flag to Enable or Disable gateway functionality. Disabling is primarily used to allow creation of the CA prior to configuration information being available. 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. @@ -102,32 +112,30 @@ An API Key within your Digicert account that has the necessary permissions to en 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. - * **IncludeClientAuthEKU** - OPTIONAL for SSL certs, ignored otherwise. If set to 'true', SSL certs enrolled under this template will have the Client Authentication EKU added to the request. NOTE: Only one EKU option can be set for any given enrollment. NOTE: This feature is currently planned to be removed by DigiCert in March 2027. - * **IncludeKDCSmartCardLogonEKU** - OPTIONAL for SSL certs, ignored otherwise. If set to 'true', SSL certs enrolled under this template will have the KDC/SmartCardLogon EKU added to the request. NOTE: Only one EKU option can be set for any given enrollment. - * **IncludeIntelvProEKU** - OPTIONAL for SSL certs, ignored otherwise. If set to 'true', SSL certs enrolled under this template will have the Intel vPro EKU added to the request. NOTE: Only one EKU option can be set for any given enrollment. - * **EnrollDivisionId** - OPTIONAL: The division (container) ID to use for enrollments against this template. - * **CommonNameIndicator** - Required for secure_email_sponsor and secure_email_organization products, ignored otherwise. Defines the source of the common name. Valid values are: email_address, given_name_surname, pseudonym, organization_name - * **ProfileType** - Optional for secure_email_* types, ignored otherwise. Valid values are: strict, multipurpose. Use 'multipurpose' if your cert includes any additional EKUs such as client auth. Default if not provided is dependent on product configuration within Digicert portal. - * **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 - + * **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. + * **IncludeClientAuthEKU** - OPTIONAL for SSL certs, ignored otherwise. If set to 'true', SSL certs enrolled under this template will have the Client Authentication EKU added to the request. NOTE: Only one EKU option can be set for any given enrollment. NOTE: This feature is currently planned to be removed by DigiCert in March 2027. + * **IncludeKDCSmartCardLogonEKU** - OPTIONAL for SSL certs, ignored otherwise. If set to 'true', SSL certs enrolled under this template will have the KDC/SmartCardLogon EKU added to the request. NOTE: Only one EKU option can be set for any given enrollment. + * **IncludeIntelvProEKU** - OPTIONAL for SSL certs, ignored otherwise. If set to 'true', SSL certs enrolled under this template will have the Intel vPro EKU added to the request. NOTE: Only one EKU option can be set for any given enrollment. + * **EnrollDivisionId** - OPTIONAL: The division (container) ID to use for enrollments against this template. + * **CommonNameIndicator** - Required for secure_email_sponsor and secure_email_organization products, ignored otherwise. Defines the source of the common name. Valid values are: email_address, given_name_surname, pseudonym, organization_name + * **ProfileType** - Optional for secure_email_* types, ignored otherwise. Valid values are: strict, multipurpose. Use 'multipurpose' if your cert includes any additional EKUs such as client auth. Default if not provided is dependent on product configuration within Digicert portal. + * **FirstName** - Required for secure_email_* types if CommonNameIndicator is given_name_surname, ignored otherwise. + * **LastName** - Required for secure_email_* types if CommonNameIndicator is given_name_surname, ignored otherwise. + * **Pseudonym** - Required for secure_email_* types if CommonNameIndicator is pseudonym, ignored otherwise. + * **UsageDesignation** - Required for secure_email_* types, ignored otherwise. The primary usage of the certificate. Valid values are: signing, key_management, dual_use ## Certificate Duplicates DigiCert supports the ability to duplicate existing certificate orders. To take advantage of this functionality, in Keyfactor Command, under the enrollment pattern you're using, create an Enrollment Field named 'Duplicate' of type Multiple Choice, and the values 'False', 'True'. When performing a renew operation against that enrollment pattern, set the value to True to tell the gateway to duplicate instead of renew. The field will be ignored on new enrollments. - ## License 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 +See all [Keyfactor Any CA Gateways (REST)](https://github.com/orgs/Keyfactor/repositories?q=anycagateway). diff --git a/digicert-certcentral-caplugin/CertCentralCAPlugin.cs b/digicert-certcentral-caplugin/CertCentralCAPlugin.cs index 176c2bd..9415e6a 100644 --- a/digicert-certcentral-caplugin/CertCentralCAPlugin.cs +++ b/digicert-certcentral-caplugin/CertCentralCAPlugin.cs @@ -20,6 +20,7 @@ using Org.BouncyCastle.Pqc.Crypto.Falcon; using System.Collections.Concurrent; +using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using System.Security.AccessControl; @@ -32,14 +33,15 @@ namespace Keyfactor.Extensions.CAPlugin.DigiCert public class CertCentralCAPlugin : IAnyCAPlugin { private CertCentralConfig _config; - private readonly ILogger _logger; + private readonly ILogger _logger = LogHandler.GetClassLogger(); private ICertificateDataReader _certificateDataReader; + private readonly IDomainValidatorFactory _domainValidatorFactory; private Dictionary DCVTokens { get; } = new Dictionary(); - public CertCentralCAPlugin() + public CertCentralCAPlugin(IDomainValidatorFactory domainValidatorFactory) { - _logger = LogHandler.GetClassLogger(); + _domainValidatorFactory = domainValidatorFactory; } public void Initialize(IAnyCAPluginConfigProvider configProvider, ICertificateDataReader certificateDataReader) { @@ -174,7 +176,21 @@ public async Task Enroll(string csr, string subject, Dictionar CertCentralClient client = CertCentralClientUtilities.BuildCertCentralClient(_config); int? organizationId = null; // DV certs have no organization, so only do the org check if its a non-DV cert - if (!string.Equals(productInfo.ProductID, CertCentralConstants.ProductTypes.DV_SSL_CERT, StringComparison.OrdinalIgnoreCase)) + + // Get product ID details. + CertificateTypeDetailsRequest detailsRequest = new CertificateTypeDetailsRequest(productInfo.ProductID); + + // For pulling product ID details, we use the Connection-level Division ID rather than the template-level one. + detailsRequest.ContainerId = null; + if (_config.DivisionId.HasValue) + { + detailsRequest.ContainerId = _config.DivisionId.Value; + } + + CertificateTypeDetailsResponse details = client.GetCertificateTypeDetails(detailsRequest); + + // Only do org check if the product type is NOT the group dv_ssl_certificate (https://dev.digicert.com/certcentral-apis/services-api/glossary.html#product-identifiers) + if (!string.Equals(details.GroupName, CertCentralConstants.ProductTypes.DV_SSL_CERT, StringComparison.OrdinalIgnoreCase)) { if (organization == null) { @@ -254,31 +270,26 @@ public async Task Enroll(string csr, string subject, Dictionar string dcvMethod = "email"; - // AnyGateway Core does not currently support retreiving DCV tokens, the following code block can be uncommented once support is added. - - //if (productInfo.ProductParameters.TryGetValue(DigiCertConstants.RequestAttributes.DCV_METHOD, out string rawDCV)) - //{ - // Logger.Trace($"Parsing DCV method: {rawDCV}"); - // if (rawDCV.IndexOf("mail", StringComparison.OrdinalIgnoreCase) >= 0) - // { - // Logger.Trace("Selecting DCV method 'email'"); - // dcvMethod = "email"; - // } - // else if (rawDCV.IndexOf("dns", StringComparison.OrdinalIgnoreCase) >= 0) - // { - // Logger.Trace("Selecting DCV method 'dns-txt-token'"); - // dcvMethod = "dns-txt-token"; - // } - // else if (rawDCV.IndexOf("http", StringComparison.OrdinalIgnoreCase) >= 0) - // { - // Logger.Trace("Selecting DCV method 'http-token'"); - // dcvMethod = "http-token"; - // } - // else - // { - // Logger.Warn($"Unexpected DCV method '{rawDCV}'. Falling back to default of 'email'"); - // } - //} + if (string.Equals(_config.DnsValidationMethod, "email", StringComparison.OrdinalIgnoreCase)) + { + _logger.LogTrace($"Selecting DCV method 'email'"); + dcvMethod = "email"; + } + else if (string.Equals(_config.DnsValidationMethod, "txt", StringComparison.OrdinalIgnoreCase)) + { + _logger.LogTrace($"Selecting DCV method 'dns-txt-token'"); + dcvMethod = "dns-txt-token"; + } + else if (string.Equals(_config.DnsValidationMethod, "cname", StringComparison.OrdinalIgnoreCase)) + { + _logger.LogTrace($"Selecting DCV method 'dns-cname-token'"); + dcvMethod = "dns-cname-token"; + } + else + { + _logger.LogWarning($"Unexpeted DCV method '{_config.DnsValidationMethod}'. Falling back to default of 'email'"); + dcvMethod = "email"; + } orderRequest.DCVMethod = dcvMethod; @@ -392,7 +403,7 @@ public async Task Enroll(string csr, string subject, Dictionar if (dupe) { - return await Duplicate(client, productInfo, priorCertReqID, commonName, csr, dnsNames, signatureHash, caCertId); + return await Duplicate(client, productInfo, priorCertReqID, commonName, csr, dnsNames, signatureHash, caCertId, dcvMethod); } // Check if the order has more validity in it (multi-year cert). If so, do a reissue instead of a renew @@ -439,13 +450,13 @@ public async Task Enroll(string csr, string subject, Dictionar switch (enrollmentType) { case EnrollmentType.New: - return await NewCertificate(client, orderRequest, commonName); + return await NewCertificate(client, orderRequest, commonName, dcvMethod); case EnrollmentType.Reissue: - return await Reissue(client, productInfo, priorCertReqID, commonName, csr, dnsNames, signatureHash, caCertId); + return await Reissue(client, productInfo, priorCertReqID, commonName, csr, dnsNames, signatureHash, caCertId, dcvMethod); case EnrollmentType.Renew: - return await Renew(client, orderRequest, productInfo, priorCertReqID, commonName); + return await Renew(client, orderRequest, productInfo, priorCertReqID, commonName, dcvMethod); default: throw new Exception($"The enrollment type '{enrollmentType}' is invalid for the DigiCert gateway."); @@ -524,6 +535,26 @@ public Dictionary GetCAConnectorAnnotations() DefaultValue = 30, Type = "Number" }, + [CertCentralConstants.Config.DNS_VALIDATION_METHOD] = new PropertyConfigInfo() + { + Comments = "The DNS validation method to use. Default value is 'email'. Other valid values are 'txt' and 'cname' " + + "If using automated DNS validation, 'txt' is the preferred method.", + Hidden = false, + DefaultValue = "email", + Type = "String" + }, + [CertCentralConstants.Config.DNS_VALIDATION_ENABLED] = new PropertyConfigInfo() + { + Comments = "Enable automated DNS (TXT or CNAME) domain control validation. When enabled, the plugin " + + "requests TXT-based or CNAME-based validation from DigiCert and publishes the returned record via the " + + "DNS provider plugin resolved by the AnyCA Gateway. Requires a DNS provider plugin (e.g. Azure, " + + "Cloudflare, etc) to be deployed and configured on the gateway. When disabled, requests that require validation " + + "will be flagged as External Validation, and the validation token, if needed depending on the DNS Validation method, " + + "will be returned.", + Hidden = false, + DefaultValue = false, + Type = "Boolean" + }, [CertCentralConstants.Config.ENABLED] = new PropertyConfigInfo() { Comments = "Flag to Enable or Disable gateway functionality. Disabling is primarily used to allow creation of the CA prior to configuration information being available.", @@ -585,7 +616,7 @@ public async Task GetSingleRecord(string caRequestID) CertCentralClient client = CertCentralClientUtilities.BuildCertCentralClient(_config); ViewCertificateOrderResponse orderResponse = client.ViewCertificateOrder(new ViewCertificateOrderRequest((uint)orderId)); - var orderCerts = GetAllCertsForOrder(orderId); + var orderCerts = GetAllCertsForOrder(orderId, orderResponse); StatusOrder certToCheck = orderCerts.Where(c => c.certificate_id == certIdInt).First(); @@ -873,7 +904,7 @@ public async Task Synchronize(BlockingCollection blockin _logger.LogTrace($"Sync CAs: {syncCAstring}"); List caList = _config.SyncCAs; - caList.ForEach(c => c.ToUpper()); + caList = caList.Select(c => c?.Trim().ToUpper()).Where(c => !string.IsNullOrEmpty(c)).ToList(); List divFilters = new List(); if (!string.IsNullOrEmpty(_config.SyncDivisionFilter)) @@ -969,7 +1000,7 @@ public async Task Synchronize(BlockingCollection blockin cancelToken.ThrowIfCancellationRequested(); string caReqId = order.order_id + "-" + order.certificate_id; orderCerts = GetAllConnectorCertsForOrder(caReqId, caList, divFilters, productFilters); - if (orderCerts == null || orderCerts.Count > 0) + if (orderCerts == null || orderCerts.Count == 0) { continue; } @@ -1181,10 +1212,10 @@ public async Task ValidateProductInfo(EnrollmentProductInfo productInfo, Diction /// The request to order a certificate. /// The common name. /// The containing the result of the enrollment request - private async Task NewCertificate(CertCentralClient client, OrderRequest request, string commonName) + private async Task NewCertificate(CertCentralClient client, OrderRequest request, string commonName, string dcvMethod) { _logger.LogTrace("Attempting to enroll for a certificate."); - return await ExtractEnrollmentResult(client, client.OrderCertificate(request), commonName); + return await ExtractEnrollmentResult(client, client.OrderCertificate(request), commonName, dcvMethod); } private async Task NewSmimeCertificate(CertCentralClient client, OrderSmimeRequest request) @@ -1197,12 +1228,13 @@ private async Task NewSmimeCertificate(CertCentralClient clien /// /// Gets the enrollment result from an object. /// - private async Task ExtractEnrollmentResult(CertCentralClient client, OrderResponse orderResponse, string commonName) + private async Task ExtractEnrollmentResult(CertCentralClient client, OrderResponse orderResponse, string commonName, string dcvMethod) { int status = 0; string statusMessage = null; string certificate = null; string caRequestID = null; + Dictionary context = new Dictionary(); if (orderResponse.Status == CertCentralBaseResponse.StatusType.ERROR) { @@ -1242,6 +1274,91 @@ private async Task ExtractEnrollmentResult(CertCentralClient c _logger.LogTrace($"Certificate for order {orderResponse.OrderId} is being processed by DigiCert. Most likely a domain/organization requires further validation"); if (!string.IsNullOrEmpty(orderResponse.DCVRandomValue)) { + if (!_config.DnsValidationEnabled) + { + _logger.LogTrace($"Automated DNS validation not enabled. Returning DCV token in enrollment context"); + context.Add(dcvMethod, orderResponse.DCVRandomValue); + } + else + { + if (_domainValidatorFactory == null) + { + _logger.LogError($"Automated DNS validation enabled by the AnyCA Gateway did not inject an IDomainValidatorFactory."); + throw new Exception($"DNS validation enabled but no DNS provider detected. Check your configuration"); + } + string validType = ""; + if (string.Equals(dcvMethod, "dns-txt-token")) + validType = "dns-01"; + else if (string.Equals(dcvMethod, "dns-cname-token")) + validType = "cname"; + else + throw new Exception($"For automated DNS validation, validation type must be either 'txt' or 'cname'"); + + List domains = new List(); + domains.Add(certificateOrderResponse.certificate.common_name); + domains.AddRange(certificateOrderResponse.certificate.dns_names); + + List errors = new List(); + foreach (var dom in domains) + { + IDomainValidator validator; + try + { + validator = _domainValidatorFactory.ResolveDomainValidator(dom, validType); + } + catch (Exception ex) + { + errors.Add($"Failed to resolve DNS provider plugin for '{dom}' (validation type '{validType}'\nError: {ex.Message}"); + continue; + } + + DomainValidationResult result = null; + if (validType.Equals("dns-01")) + { + result = await validator.StageValidation(dom, orderResponse.DCVRandomValue, CancellationToken.None); + } + else + { + result = await validator.StageValidation("_dnsauth", $"{orderResponse.DCVRandomValue}.dcv.digicert.com", CancellationToken.None); + } + + if (result == null || !result.Success) + { + var msg = result?.ErrorMessage ?? "unknown error"; + errors.Add($"Failed to publish DNS validation record for '{dom}': {msg}"); + } + else + { + _logger.LogInformation($"Published DNS validation record for '{dom}'"); + } + } + + var dcvcheck = client.DVCheckDCV(new DVCheckDCVRequest((int)orderID)); + if (dcvcheck.Status == CertCentralBaseResponse.StatusType.ERROR) + { + if (errors.Count > 0) + { + _logger.LogError($"Domain Validation Errors:\n{string.Join('\n', errors)}"); + string err = string.Join(';', errors); + statusMessage = err; + } + else + { + string msg = $"Domain validation(s) still pending. Certificate will be picked up on future sync."; + _logger.LogWarning(msg); + } + status = (int)EndEntityStatus.EXTERNALVALIDATION; + } + else + { + var certChain = client.GetCertificateChain(new CertificateChainRequest(orderResponse.CertificateId.Value.ToString())); + string certPem = certChain.Intermediates.SingleOrDefault(c => c.SubjectCommonName.Equals(commonName, StringComparison.OrdinalIgnoreCase))?.PEM; + certificate = certPem; + ViewCertificateOrderResponse newCertificateOrderResponse = client.ViewCertificateOrder(new ViewCertificateOrderRequest(orderID)); + + status = GetCertificateStatusFromCA(newCertificateOrderResponse.status, (int)orderID); + } + } _logger.LogDebug($"Saving DCV token for order {orderResponse.OrderId}"); DCVTokens[orderResponse.OrderId] = orderResponse.DCVRandomValue; } @@ -1313,7 +1430,8 @@ private async Task ExtractEnrollmentResult(CertCentralClient c CARequestID = caRequestID, Certificate = certificate, Status = status, - StatusMessage = statusMessage + StatusMessage = statusMessage, + EnrollmentContext = context }; } @@ -1521,7 +1639,7 @@ private List GetDuplicates(CertCentralClient digiClient, int orderI /// The . /// Information about the DigiCert product this certificate uses. /// - private async Task Reissue(CertCentralClient client, EnrollmentProductInfo enrollmentProductInfo, string caRequestId, string commonName, string csr, List dnsNames, string signatureHash, string caCertId) + private async Task Reissue(CertCentralClient client, EnrollmentProductInfo enrollmentProductInfo, string caRequestId, string commonName, string csr, List dnsNames, string signatureHash, string caCertId, string dcvMethod) { CheckProductExistence(enrollmentProductInfo.ProductID); @@ -1553,7 +1671,7 @@ private async Task Reissue(CertCentralClient client, Enrollmen }; _logger.LogTrace("Attempting to reissue certificate."); - return await ExtractEnrollmentResult(client, client.ReissueCertificate(reissueRequest), commonName); + return await ExtractEnrollmentResult(client, client.ReissueCertificate(reissueRequest), commonName, dcvMethod); } /// @@ -1563,7 +1681,7 @@ private async Task Reissue(CertCentralClient client, Enrollmen /// 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) + private async Task Duplicate(CertCentralClient client, EnrollmentProductInfo enrollmentProductInfo, string caRequestId, string commonName, string csr, List dnsNames, string signatureHash, string caCertId, string dcvMethod) { CheckProductExistence(enrollmentProductInfo.ProductID); @@ -1593,7 +1711,7 @@ private async Task Duplicate(CertCentralClient client, Enrollm }; _logger.LogTrace("Attempting to duplicate certificate."); - return await ExtractEnrollmentResult(client, client.DuplicateCertificate(duplicateRequest), commonName); + return await ExtractEnrollmentResult(client, client.DuplicateCertificate(duplicateRequest), commonName, dcvMethod); } /// @@ -1619,7 +1737,7 @@ private void CheckProductExistence(string productId) /// The . /// Information about the DigiCert product this certificate uses. /// - private async Task Renew(CertCentralClient client, OrderRequest request, EnrollmentProductInfo enrollmentProductInfo, string caRequestId, string commonName) + private async Task Renew(CertCentralClient client, OrderRequest request, EnrollmentProductInfo enrollmentProductInfo, string caRequestId, string commonName, string dcvMethod) { CheckProductExistence(enrollmentProductInfo.ProductID); @@ -1637,7 +1755,7 @@ private async Task Renew(CertCentralClient client, OrderReques request.RenewalOfOrderId = orderId; _logger.LogTrace($"Attempting to renew certificate with order id {orderId}."); - return await ExtractEnrollmentResult(client, client.OrderCertificate(request), commonName); + return await ExtractEnrollmentResult(client, client.OrderCertificate(request), commonName, dcvMethod); } private async Task RenewSmime(CertCentralClient client, OrderSmimeRequest request, EnrollmentProductInfo enrollmentProductInfo, string caRequestId) @@ -1699,9 +1817,10 @@ private List GetAllConnectorCertsForOrder(string caReque if (productIds != null && productIds.Count > 0 && !productIds.Contains(orderResponse.product.name_id.ToString())) { _logger.LogTrace($"Found order ID {orderId} that does not match Product filter. Product ID: {orderResponse.product.name_id.ToString()} Skipping..."); + return null; } - var orderCerts = GetAllCertsForOrder(orderId); + var orderCerts = GetAllCertsForOrder(orderId, orderResponse); List certList = new List(); List pemList = new List(); @@ -1719,6 +1838,10 @@ private List GetAllConnectorCertsForOrder(string caReque CertificateChainResponse certificateChainResponse = client.GetCertificateChain(new CertificateChainRequest($"{cert.certificate_id}")); if (certificateChainResponse.Status == CertCentralBaseResponse.StatusType.SUCCESS) { + if (certificateChainResponse.Intermediates == null || certificateChainResponse.Intermediates.Count == 0) + { + throw new Exception($"DigiCert returned an empty certificate chain for certificate {cert.certificate_id} on order {orderId}."); + } certificate = certificateChainResponse.Intermediates[0].PEM; } else @@ -1727,12 +1850,15 @@ private List GetAllConnectorCertsForOrder(string caReque } } //Another check for duplicate PEMs to get arround issue with DigiCert API returning incorrect data sometimes on reissued/duplicate certs - if (pemList.Contains(certificate)) + if (certificate != null && pemList.Contains(certificate)) { _logger.LogWarning($"Found duplicate PEM for ID {caReqId}. Skipping..."); continue; } - pemList.Add(certificate); + if (certificate != null) + { + pemList.Add(certificate); + } var connCert = new AnyCAPluginCertificate { CARequestID = caReqId, @@ -1757,10 +1883,12 @@ private List GetAllConnectorCertsForOrder(string caReque /// /// /// - private List GetAllCertsForOrder(int orderId) + private List GetAllCertsForOrder(int orderId, ViewCertificateOrderResponse existingOrderResponse = null) { CertCentralClient client = CertCentralClientUtilities.BuildCertCentralClient(_config); - ViewCertificateOrderResponse orderResponse = client.ViewCertificateOrder(new ViewCertificateOrderRequest((uint)orderId)); + + // If the caller provides an existing order response, reuse that to save the API call. + ViewCertificateOrderResponse orderResponse = existingOrderResponse ?? client.ViewCertificateOrder(new ViewCertificateOrderRequest((uint)orderId)); if (orderResponse.Status == CertCentralBaseResponse.StatusType.ERROR) { string errorMessage = String.Format("Request {0} was not found in CertCentral database or is not valid", orderId); diff --git a/digicert-certcentral-caplugin/CertCentralConfig.cs b/digicert-certcentral-caplugin/CertCentralConfig.cs index 6909c2e..eba73a6 100644 --- a/digicert-certcentral-caplugin/CertCentralConfig.cs +++ b/digicert-certcentral-caplugin/CertCentralConfig.cs @@ -52,5 +52,7 @@ public List SyncProducts public bool? FilterExpiredOrders { get; set; } public int? SyncExpirationDays { get; set; } public string SyncDivisionFilter { get; set; } + public bool DnsValidationEnabled { get; set; } + public string DnsValidationMethod { get; set; } } } diff --git a/digicert-certcentral-caplugin/Client/CertCentralClient.cs b/digicert-certcentral-caplugin/Client/CertCentralClient.cs index 753f54f..b16dc2d 100644 --- a/digicert-certcentral-caplugin/Client/CertCentralClient.cs +++ b/digicert-certcentral-caplugin/Client/CertCentralClient.cs @@ -6,6 +6,7 @@ using System; using System.Collections.Generic; +using System.Data.SqlTypes; using System.Linq; using System.Net; using System.Security.Cryptography.X509Certificates; @@ -64,6 +65,43 @@ public CertCentralResponse() public bool Success { get; set; } public string Response { get; set; } + public int StatusCode { get; set; } + } + + /// + /// Turn a DigiCert response body into a non-empty list of Errors, without throwing + /// + /// + /// + /// + internal static List ParseErrors(string body, int statusCode) + { + string codeSuffix = statusCode > 0 ? $" (HTTP {statusCode})" : ""; + + if (string.IsNullOrWhiteSpace(body)) + { + return new List { new Error { code = "empty_response_body", message = $"DigiCert returned no response body{codeSuffix}." } }; + } + + try + { + Errors parsed = JsonConvert.DeserializeObject(body); + if (parsed?.errors != null && parsed.errors.Count > 0) + { + return parsed.errors; + } + return new List { new Error { code = "unrecognized_error_response", message = $"DigiCert returned a response with no 'errors' array{codeSuffix} : {Truncate(body)}" } }; + } + catch (JsonException) + { + return new List { new Error { code = "non_json_response", message = $"DigiCert returned a non-JSON response{codeSuffix}: {Truncate(body)}" } }; + } + } + + private static string Truncate(string s) + { + s = s.Replace("\r", " ").Replace("\n", " ").Trim(); + return s.Length <= 500 ? s : s.Substring(0, 500) + "\u2026"; } private CertCentralResponse Request(CertCentralBaseRequest request) @@ -72,8 +110,14 @@ private CertCentralResponse Request(CertCentralBaseRequest request) } private static int RequestIDCounter = 1; + private const int MaxRateLimitRetries = 3; private CertCentralResponse Request(CertCentralBaseRequest request, string parameters) + { + return Request(request, parameters, 1); + } + + private CertCentralResponse Request(CertCentralBaseRequest request, string parameters, int attempt) { //set in config files //ServicePointManager.SecurityProtocol = SecurityProtocolType.Tls12; @@ -118,6 +162,7 @@ private CertCentralResponse Request(CertCentralBaseRequest request, string param { string respString = new StreamReader(objResponse.GetResponseStream()).ReadToEnd(); oCertCertResponse.Response = respString; + oCertCertResponse.StatusCode = (int)objResponse.StatusCode; Logger.LogTrace($"CertCentral CA (Request ID: {reqID}) has returned Response '{objResponse.StatusCode}: {respString}"); } } @@ -129,10 +174,25 @@ private CertCentralResponse Request(CertCentralBaseRequest request, string param { if (errorResponse.StatusCode == (HttpStatusCode)429/*Too Many Requests*/) { - Logger.LogInformation($"Request ID: {reqID} was rate-limited. Trying again in 5 seconds"); - // TODO - Figure out how long to wait, then wait that long - System.Threading.Thread.Sleep(5000); - return Request(request, parameters); + // DigiCert's documented limits are 1000 requests / 3 minutes AND 100 requests / 5 seconds, + // rolling, per API key. + // Guidance is exponential backoff with a default maximum of 3 retries. + if (attempt >= MaxRateLimitRetries) + { + Logger.LogWarning($"Request ID: {reqID} was rate-limited by DigiCert and has exhausted {MaxRateLimitRetries} attempts. Giving up."); + using (var limitReader = new StreamReader(errorResponse.GetResponseStream())) + { + oCertCertResponse.Success = false; + oCertCertResponse.Response = limitReader.ReadToEnd(); + } + } + else + { + int waitSeconds = 5 * (int)Math.Pow(2, attempt - 1); + Logger.LogInformation($"Request ID: {reqID} was rate-limited. Retry {attempt} of {MaxRateLimitRetries - 1} in {waitSeconds} seconds"); + System.Threading.Thread.Sleep(waitSeconds * 1000); + return Request(request, parameters, attempt + 1); + } } else { @@ -141,6 +201,7 @@ private CertCentralResponse Request(CertCentralBaseRequest request, string param string errorString = reader.ReadToEnd(); oCertCertResponse.Success = false; oCertCertResponse.Response = errorString; + oCertCertResponse.StatusCode = (int)errorResponse.StatusCode; Logger.LogTrace($"CertCentral CA (Request ID: {reqID}) has returned Response '{errorResponse.StatusCode}: {errorString}"); } } @@ -161,6 +222,23 @@ private CertCentralResponse Request(CertCentralBaseRequest request, string param return oCertCertResponse; } + public DVCheckDCVResponse CheckDCV(DVCheckDCVRequest request) + { + CertCentralResponse response = Request(request); + + DVCheckDCVResponse checkDCVResponse = new DVCheckDCVResponse(); + if (!response.Success) + { + Errors errors = JsonConvert.DeserializeObject(response.Response); + checkDCVResponse.Status = CertCentralBaseResponse.StatusType.ERROR; + checkDCVResponse.Errors = errors.errors; + } + else + { + checkDCVResponse = JsonConvert.DeserializeObject(response.Response); + } + return checkDCVResponse; + } public ListOrganizationsResponse ListOrganizations(ListOrganizationsRequest request) { CertCentralResponse response = Request(request, request.BuildParameters()); @@ -169,9 +247,9 @@ public ListOrganizationsResponse ListOrganizations(ListOrganizationsRequest requ if (!response.Success) { - Errors errors = JsonConvert.DeserializeObject(response.Response); + List errors = ParseErrors(response.Response, response.StatusCode); listOrganizationsResponse.Status = CertCentralBaseResponse.StatusType.ERROR; - listOrganizationsResponse.Errors = errors.errors; + listOrganizationsResponse.Errors = errors; } else listOrganizationsResponse = JsonConvert.DeserializeObject(response.Response); @@ -187,9 +265,9 @@ public ListDomainsResponse ListDomains(ListDomainsRequest request) if (!response.Success) { - Errors errors = JsonConvert.DeserializeObject(response.Response); + List errors= ParseErrors(response.Response, response.StatusCode); listDomainsResponse.Status = CertCentralBaseResponse.StatusType.ERROR; - listDomainsResponse.Errors = errors.errors; + listDomainsResponse.Errors = errors; } else listDomainsResponse = JsonConvert.DeserializeObject(response.Response); @@ -205,9 +283,9 @@ public ListContainersResponse ListContainers(ListContainersRequest request) if (!response.Success) { - Errors errors = JsonConvert.DeserializeObject(response.Response); + List errors = ParseErrors(response.Response, response.StatusCode); listContainersResponse.Status = CertCentralBaseResponse.StatusType.ERROR; - listContainersResponse.Errors = errors.errors; + listContainersResponse.Errors = errors; } else { @@ -225,9 +303,9 @@ public ListDuplicatesResponse ListDuplicates(ListDuplicatesRequest duplicatesReq if (!ccResponse.Success) { - Errors errors = JsonConvert.DeserializeObject(ccResponse.Response); + List errors = ParseErrors(ccResponse.Response, ccResponse.StatusCode); duplicatesResponse.Status = CertCentralBaseResponse.StatusType.ERROR; - duplicatesResponse.Errors = errors.errors; + duplicatesResponse.Errors = errors; } else { @@ -245,9 +323,9 @@ public ListReissueResponse ListReissues(ListReissueRequest reissueRequest) if (!ccResponse.Success) { - Errors errors = JsonConvert.DeserializeObject(ccResponse.Response); + List errors = ParseErrors(ccResponse.Response, ccResponse.StatusCode); reissueResponse.Status = CertCentralBaseResponse.StatusType.ERROR; - reissueResponse.Errors = errors.errors; + reissueResponse.Errors = errors; } else { @@ -265,9 +343,9 @@ public ListRequestsResponse ListRequests(ListRequestsRequest request) if (!response.Success) { - Errors errors = JsonConvert.DeserializeObject(response.Response); + List errors = ParseErrors(response.Response, response.StatusCode); listRequestsResponse.Status = CertCentralBaseResponse.StatusType.ERROR; - listRequestsResponse.Errors = errors.errors; + listRequestsResponse.Errors = errors; } else listRequestsResponse = JsonConvert.DeserializeObject(response.Response); @@ -283,9 +361,9 @@ public ListMetadataResponse ListMetadata(ListMetadataRequest request) if (!response.Success) { - Errors errors = JsonConvert.DeserializeObject(response.Response); + List errors = ParseErrors(response.Response, response.StatusCode); listMetadataResponse.Status = CertCentralBaseResponse.StatusType.ERROR; - listMetadataResponse.Errors = errors.errors; + listMetadataResponse.Errors = errors; } else listMetadataResponse = JsonConvert.DeserializeObject(response.Response); @@ -304,9 +382,9 @@ public OrderResponse OrderCertificate(OrderRequest request) OrderResponse orderResponse = new OrderResponse(); if (!response.Success) { - Errors errors = JsonConvert.DeserializeObject(response.Response); + List errors = ParseErrors(response.Response, response.StatusCode); orderResponse.Status = CertCentralBaseResponse.StatusType.ERROR; - orderResponse.Errors = errors.errors; + orderResponse.Errors = errors; } else orderResponse = JsonConvert.DeserializeObject(response.Response); @@ -325,9 +403,9 @@ public OrderResponse OrderSmimeCertificate(OrderSmimeRequest request) OrderResponse orderResponse = new OrderResponse(); if (!response.Success) { - Errors errors = JsonConvert.DeserializeObject(response.Response); + List errors = ParseErrors(response.Response, response.StatusCode); orderResponse.Status = CertCentralBaseResponse.StatusType.ERROR; - orderResponse.Errors = errors.errors; + orderResponse.Errors = errors; } else orderResponse = JsonConvert.DeserializeObject(response.Response); @@ -345,9 +423,9 @@ public OrderResponse ReissueCertificate(ReissueRequest request) OrderResponse reissueResponse = new OrderResponse(); if (!response.Success) { - Errors errors = JsonConvert.DeserializeObject(response.Response); + List errors = ParseErrors(response.Response, response.StatusCode); reissueResponse.Status = CertCentralBaseResponse.StatusType.ERROR; - reissueResponse.Errors = errors.errors; + reissueResponse.Errors = errors; } else { @@ -367,9 +445,9 @@ public OrderResponse DuplicateCertificate(DuplicateRequest request) OrderResponse duplicateResponse = new OrderResponse(); if (!response.Success) { - Errors errors = JsonConvert.DeserializeObject(response.Response); + List errors = ParseErrors(response.Response, response.StatusCode); duplicateResponse.Status = CertCentralBaseResponse.StatusType.ERROR; - duplicateResponse.Errors = errors.errors; + duplicateResponse.Errors = errors; } else { @@ -386,9 +464,9 @@ public RevokeCertificateResponse RevokeCertificate(RevokeCertificateRequest requ RevokeCertificateResponse revokeOrderResponse = new RevokeCertificateResponse(); if (!response.Success) { - Errors errors = JsonConvert.DeserializeObject(response.Response); + List errors = ParseErrors(response.Response, response.StatusCode); revokeOrderResponse.Status = CertCentralBaseResponse.StatusType.ERROR; - revokeOrderResponse.Errors = errors.errors; + revokeOrderResponse.Errors = errors; } else revokeOrderResponse = JsonConvert.DeserializeObject(response.Response); @@ -403,9 +481,9 @@ public RevokeCertificateResponse RevokeCertificate(RevokeCertificateByOrderReque RevokeCertificateResponse revokeOrderResponse = new RevokeCertificateResponse(); if (!response.Success) { - Errors errors = JsonConvert.DeserializeObject(response.Response); + List errors = ParseErrors(response.Response, response.StatusCode); revokeOrderResponse.Status = CertCentralBaseResponse.StatusType.ERROR; - revokeOrderResponse.Errors = errors.errors; + revokeOrderResponse.Errors = errors; } else revokeOrderResponse = JsonConvert.DeserializeObject(response.Response); @@ -420,9 +498,9 @@ public UpdateRequestStatusResponse UpdateRequestStatus(UpdateRequestStatusReques UpdateRequestStatusResponse updateRequestResponse = new UpdateRequestStatusResponse(); if (!response.Success) { - Errors errors = JsonConvert.DeserializeObject(response.Response); + List errors = ParseErrors(response.Response, response.StatusCode); updateRequestResponse.Status = CertCentralBaseResponse.StatusType.ERROR; - updateRequestResponse.Errors = errors.errors; + updateRequestResponse.Errors = errors; } else { @@ -443,9 +521,9 @@ public DVCheckDCVResponse DVCheckDCV(DVCheckDCVRequest request) DVCheckDCVResponse checkDCVResponse = new DVCheckDCVResponse(); if (!response.Success) { - Errors errors = JsonConvert.DeserializeObject(response.Response); + List errors = ParseErrors(response.Response, response.StatusCode); checkDCVResponse.Status = CertCentralBaseResponse.StatusType.ERROR; - checkDCVResponse.Errors = errors.errors; + checkDCVResponse.Errors = errors; } else { @@ -461,9 +539,9 @@ public CertificateChainResponse GetCertificateChain(CertificateChainRequest requ CertificateChainResponse chainResponse = new CertificateChainResponse(); if (!response.Success) { - Errors errors = JsonConvert.DeserializeObject(response.Response); + List errors = ParseErrors(response.Response, response.StatusCode); chainResponse.Status = CertCentralBaseResponse.StatusType.ERROR; - chainResponse.Errors = errors.errors; + chainResponse.Errors = errors; } else { @@ -479,9 +557,9 @@ public StatusChangesResponse StatusChanges(StatusChangesRequest request) StatusChangesResponse statusChangeResponse = new StatusChangesResponse(); if (!certResponse.Success) { - Errors errors = JsonConvert.DeserializeObject(certResponse.Response); + List errors = ParseErrors(certResponse.Response, certResponse.StatusCode); statusChangeResponse.Status = CertCentralBaseResponse.StatusType.ERROR; - statusChangeResponse.Errors = errors.errors; + statusChangeResponse.Errors = errors; } else { @@ -496,9 +574,9 @@ public DownloadCertificateByFormatResponse DownloadCertificateByFormat(DownloadC DownloadCertificateByFormatResponse dlCertificateRequestResponse = new DownloadCertificateByFormatResponse(); if (!response.Success) { - Errors errors = JsonConvert.DeserializeObject(response.Response); + List errors = ParseErrors(response.Response, response.StatusCode); dlCertificateRequestResponse.Status = CertCentralBaseResponse.StatusType.ERROR; - dlCertificateRequestResponse.Errors = errors.errors; + dlCertificateRequestResponse.Errors = errors; } else { @@ -545,9 +623,9 @@ public ListCertificateOrdersResponse ListAllCertificateOrders(bool ignoreExpired ListCertificateOrdersResponse listCertificateResponse = new ListCertificateOrdersResponse(); if (!response.Success) { - Errors errors = JsonConvert.DeserializeObject(response.Response); + List errors = ParseErrors(response.Response, response.StatusCode); listCertificateResponse.Status = CertCentralBaseResponse.StatusType.ERROR; - listCertificateResponse.Errors = errors.errors; + listCertificateResponse.Errors = errors; return listCertificateResponse; } @@ -571,9 +649,9 @@ public ViewCertificateOrderResponse ViewCertificateOrder(ViewCertificateOrderReq if (!response.Success) { - Errors errors = JsonConvert.DeserializeObject(response.Response); + List errors = ParseErrors(response.Response, response.StatusCode); viewCertResponse.Status = CertCentralBaseResponse.StatusType.ERROR; - viewCertResponse.Errors = errors.errors; + viewCertResponse.Errors = errors; } else { @@ -596,9 +674,9 @@ public CertificateTypeDetailsResponse GetCertificateTypeDetails(CertificateTypeD if (!response.Success) { - Errors errors = JsonConvert.DeserializeObject(response.Response); + List errors = ParseErrors(response.Response, response.StatusCode); detailsResponse.Status = CertCentralBaseResponse.StatusType.ERROR; - detailsResponse.Errors = errors.errors; + detailsResponse.Errors = errors; } else { @@ -620,9 +698,9 @@ public CertificateTypesResponse GetAllCertificateTypes() if (!response.Success) { - Errors errors = JsonConvert.DeserializeObject(response.Response); + List errors = ParseErrors(response.Response, response.StatusCode); allTypes.Status = CertCentralBaseResponse.StatusType.ERROR; - allTypes.Errors = errors.errors; + allTypes.Errors = errors; } else { diff --git a/digicert-certcentral-caplugin/Constants.cs b/digicert-certcentral-caplugin/Constants.cs index a01d8ca..c729882 100644 --- a/digicert-certcentral-caplugin/Constants.cs +++ b/digicert-certcentral-caplugin/Constants.cs @@ -44,6 +44,8 @@ public class Config public const string LAST_NAME = "LastName"; public const string PSEUDONYM = "Pseudonym"; public const string SMIME_USAGE = "UsageDesignation"; + public const string DNS_VALIDATION_METHOD = "DnsValidationMethod"; + public const string DNS_VALIDATION_ENABLED = "DnsValidationEnabled"; } public class RequestAttributes diff --git a/digicert-certcentral-caplugin/Models/Error.cs b/digicert-certcentral-caplugin/Models/Error.cs index e963096..1a7c1c6 100644 --- a/digicert-certcentral-caplugin/Models/Error.cs +++ b/digicert-certcentral-caplugin/Models/Error.cs @@ -15,6 +15,13 @@ public class Error [JsonProperty("message")] public string message { get; set; } + + public override string ToString() + { + if (string.IsNullOrEmpty(code)) return message ?? string.Empty; + if (string.IsNullOrEmpty(message)) return code; + return $"{code}: {message}"; + } } public class Errors diff --git a/digicert-certcentral-caplugin/digicert-certcentral-caplugin.csproj b/digicert-certcentral-caplugin/digicert-certcentral-caplugin.csproj index 7510b07..4ed0d61 100644 --- a/digicert-certcentral-caplugin/digicert-certcentral-caplugin.csproj +++ b/digicert-certcentral-caplugin/digicert-certcentral-caplugin.csproj @@ -1,21 +1,19 @@  - net6.0;net8.0 + net8.0;net10.0 Keyfactor.Extensions.CAPlugin.DigiCert enable disable DigicertCAPlugin - 2.1.2 - 2.1.2 - - - - - + + + + + diff --git a/docsource/configuration.md b/docsource/configuration.md index 0b4b863..7a9c66e 100644 --- a/docsource/configuration.md +++ b/docsource/configuration.md @@ -14,6 +14,15 @@ An API Key within your Digicert account that has the necessary permissions to en 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 +### Automated DNS Domain Validation + +This plugin integrates with the AnyCA Gateway **DNS provider plugin framework** (`KeyfactorAnyGateway.IAnyCAPlugin` 3.3.0+). DNS provider plugins (Azure DNS, AWS Route53, Cloudflare, Google Cloud DNS, etc.) are deployed and configured **separately** on the gateway; this CA plugin does not bundle any DNS provider SDKs. The gateway injects an `IDomainValidatorFactory` that resolves the correct provider for each domain at enrollment time. + +DigiCert supports both **TXT** and **CNAME** records for DNS validation, the choice of which is provided by the appropriate configuration field. **TXT** records are the preferred method. + +`DnsValidationMethod` defines whether you wish to use TXT, CNAME, or email validation. Only TXT or CNAME will work with the automated validation. +`DnsValidationEnabled` determines whether to use the automated validation. Make sure you have the necessary DNS plugins installed and configured before enabling. If `DnsValidationMethod` is set to either TXT or CNAME but `DnsValidationEnabled` is false, then unvalidated enrollment requests will get a status of External Validation, and the necessary TXT or CNAME token will be instead returned to the enrollment caller to be used to manually update the DNS record. + ## 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 a30cc75..3ef42f7 100644 --- a/integration-manifest.json +++ b/integration-manifest.json @@ -7,7 +7,7 @@ "link_github": true, "update_catalog": true, "description": "DigiCert CertCentral plugin for the AnyCA REST Gateway framework", - "gateway_framework": "24.2.0", + "gateway_framework": "26.2.0", "release_dir": "digicert-certcentral-caplugin/bin/Release", "release_project": "digicert-certcentral-caplugin/digicert-certcentral-caplugin.csproj", "about": { @@ -50,6 +50,14 @@ "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": "DnsValidationMethod", + "description": "The DNS validation method to use. Default value is 'email'. Other valid values are 'txt' and 'cname' If using automated DNS validation, 'txt' is the preferred method." + }, + { + "name": "DnsValidationEnabled", + "description": "Enable automated DNS (TXT or CNAME) domain control validation. When enabled, the plugin requests TXT-based validation from DigiCert and publishes the returned record via the DNS provider plugin resolved by the AnyCA Gateway. Requires a DNS provider plugin (e.g. Azure, Cloudflare, etc) to be deployed and configured on the gateway. When disabled, requests that require validation will be flagged as External Validation, and the validation token, if needed depending on the DNS Validation method, will be returned." + }, { "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."