diff --git a/.github/workflows/keyfactor-integrations-workflow.yml b/.github/workflows/keyfactor-integrations-workflow.yml index 088548d..b779b53 100644 --- a/.github/workflows/keyfactor-integrations-workflow.yml +++ b/.github/workflows/keyfactor-integrations-workflow.yml @@ -31,10 +31,3 @@ jobs: uses: Keyfactor/actions/.github/workflows/update-catalog.yml@main secrets: token: ${{ secrets.SDK_SYNC_PAT }} - - call-update-store-types-workflow: - needs: call-assign-from-json-workflow - if: needs.call-assign-from-json-workflow.outputs.integration_type == 'orchestrator' && (github.event_name == 'push' || github.event_name == 'workflow_dispatch') - uses: Keyfactor/actions/.github/workflows/update-store-types.yml@main - secrets: - token: ${{ secrets.UPDATE_STORE_TYPES }} diff --git a/Keyfactor.AnyGateway.ICAConnector.dll b/Keyfactor.AnyGateway.ICAConnector.dll new file mode 100644 index 0000000..aee346c Binary files /dev/null and b/Keyfactor.AnyGateway.ICAConnector.dll differ diff --git a/README.md b/README.md index 036f8e3..97dffe9 100644 --- a/README.md +++ b/README.md @@ -10,16 +10,13 @@ DigiCert CertCentral plugin for the AnyCA Gateway framework This repository contains an AnyGateway CA Connector, which is a plugin to the Keyfactor AnyGateway. AnyGateway CA Connectors allow Keyfactor Command to be used for inventory, issuance, and revocation of certificates from a third-party certificate authority. - - ## Support for digicert-certcentral-anycagateway -digicert-certcentral-anycagateway is open source and community supported, meaning that there is **no SLA** applicable for these tools. +digicert-certcentral-anycagateway is open source and community supported, meaning that there is no support guaranteed from Keyfactor Support for these tools. ###### 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. - --- @@ -55,7 +52,7 @@ In order to enroll for certificates the Keyfactor Command server must trust the ```json { "extensions": { - "Keyfactor.AnyGateway.Extensions.ICAConnector": { + "Keyfactor.AnyGateway.Extensions.IAnyCAPlugin": { "DigiCertCAConnector": { "assemblypath": "../DigiCertCAGateway.dll", "TypeFullName": "Keyfactor.Extensions.CAGateway.DigiCert.CertCentralCAConnector" diff --git a/digicert-certcentral-anycagateway/API/StatusChanges.cs b/digicert-certcentral-anycagateway/API/StatusChanges.cs index 0b9a2cf..eaced16 100644 --- a/digicert-certcentral-anycagateway/API/StatusChanges.cs +++ b/digicert-certcentral-anycagateway/API/StatusChanges.cs @@ -27,6 +27,9 @@ public class StatusOrder [JsonProperty("status")] public string status { get; set; } + + [JsonIgnore] + public string serialNum { get; set; } } public class StatusChangesResponse : CertCentralBaseResponse diff --git a/digicert-certcentral-anycagateway/CertCentralCAConnector.cs b/digicert-certcentral-anycagateway/CertCentralCAConnector.cs index 2881854..3f8a512 100644 --- a/digicert-certcentral-anycagateway/CertCentralCAConnector.cs +++ b/digicert-certcentral-anycagateway/CertCentralCAConnector.cs @@ -1,9 +1,11 @@ using Keyfactor.AnyGateway.Extensions; using Keyfactor.Common; +using Keyfactor.Common.Exceptions; using Keyfactor.Extensions.CAGateway.DigiCert.API; using Keyfactor.Extensions.CAGateway.DigiCert.Client; using Keyfactor.Extensions.CAGateway.DigiCert.Models; using Keyfactor.Logging; +using Keyfactor.PKI.Enums; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; @@ -22,7 +24,7 @@ namespace Keyfactor.Extensions.CAGateway.DigiCert { - public class CertCentralCAConnector : ICAConnector + public class CertCentralCAConnector : IAnyCAPlugin { private CertCentralConfig _config; private readonly ILogger _logger; @@ -34,13 +36,24 @@ public CertCentralCAConnector() { _logger = LogHandler.GetClassLogger(); } - public void Initialize(ICAConnectorConfigProvider configProvider, ICertificateDataReader certificateDataReader) + public void Initialize(IAnyCAPluginConfigProvider configProvider, ICertificateDataReader certificateDataReader) { _certificateDataReader = certificateDataReader; string rawConfig = JsonConvert.SerializeObject(configProvider.CAConnectionData); _config = JsonConvert.DeserializeObject(rawConfig); } + /// + /// Enroll for a certificate + /// + /// The CSR for the certificate request + /// The subject string + /// The list of SANs + /// Collection of product information and options. Includes both product-level config options as well as custom enrollment fields. + /// The format of the request + /// The type of enrollment (new, renew, reissue) + /// The result of the enrollment + /// public async Task Enroll(string csr, string subject, Dictionary san, EnrollmentProductInfo productInfo, RequestFormat requestFormat, EnrollmentType enrollmentType) { _logger.MethodEntry(LogLevel.Trace); @@ -48,7 +61,12 @@ public async Task Enroll(string csr, string subject, Dictionar CertCentralCertType certType = CertCentralCertType.GetAllTypes(_config).FirstOrDefault(x => x.ProductCode.Equals(productInfo.ProductID)); OrderRequest orderRequest = new OrderRequest(certType); - var days = (productInfo.ProductParameters.ContainsKey("LifetimeDays")) ? int.Parse(productInfo.ProductParameters["LifetimeDays"]) : 365; + //var days = (productInfo.ProductParameters.ContainsKey("LifetimeDays") && !st) ? int.Parse(productInfo.ProductParameters["LifetimeDays"]) : 365; + var days = 365; + if (productInfo.ProductParameters.ContainsKey("LifetimeDays") && !string.IsNullOrEmpty(productInfo.ProductParameters["LifetimeDays"])) + { + days = int.Parse(productInfo.ProductParameters["LifetimeDays"]); + } int validityYears = 0; DateTime? customExpirationDate = null; switch (days) @@ -95,7 +113,10 @@ public async Task Enroll(string csr, string subject, Dictionar 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 - organization = orgName; + if (!string.IsNullOrEmpty(orgName)) + { + organization = orgName; + } } string signatureHash = certType.signatureAlgorithm; @@ -146,8 +167,11 @@ public async Task Enroll(string csr, string subject, Dictionar } // Get CA Cert ID (if present) - string caCertId = (productInfo.ProductParameters.ContainsKey("CACertId")) ? (string)productInfo.ProductParameters["CACertId"] : null; - + string caCertId = null; + if (productInfo.ProductParameters.ContainsKey("CACertId") && !string.IsNullOrEmpty(productInfo.ProductParameters["CACertId"])) + { + caCertId = (string)productInfo.ProductParameters["CACertId"]; + } // Set up request orderRequest.Certificate.CommonName = commonName; orderRequest.Certificate.CSR = csr; @@ -203,30 +227,46 @@ public async Task Enroll(string csr, string subject, Dictionar orderRequest.ValidityYears = validityYears; } - var renewWindow = (productInfo.ProductParameters.ContainsKey(CertCentralConstants.Config.RENEWAL_WINDOW)) ? int.Parse(productInfo.ProductParameters[CertCentralConstants.Config.RENEWAL_WINDOW]) : 90; + var renewWindow = 90; + if (productInfo.ProductParameters.ContainsKey(CertCentralConstants.Config.RENEWAL_WINDOW) && !string.IsNullOrEmpty(productInfo.ProductParameters[CertCentralConstants.Config.RENEWAL_WINDOW])) + { + renewWindow = int.Parse(productInfo.ProductParameters[CertCentralConstants.Config.RENEWAL_WINDOW]); + } string priorCertSnString = null; string priorCertReqID = null; + + // Current gateway core leaves it up to the integration to determine if it is a renewal or a reissue if (enrollmentType == EnrollmentType.RenewOrReissue) { //// Determine if we're going to do a renew or a reissue. - //string priorCertSnString = productInfo.ProductParameters["PriorCertSN"]; - //_logger.LogTrace($"Attempting to retrieve the certificate with serial number {priorCertSnString}."); - //byte[] priorCertSn = DataConversion.HexToBytes(priorCertSnString); - //CAConnectorCertificate anyGatewayCertificate = _certificateDataReader.(priorCertSn); - //if (anyGatewayCertificate == null) - //{ - // throw new Exception($"No certificate with serial number '{priorCertSnString}' could be found."); - //} - enrollmentType = EnrollmentType.Renew; + priorCertSnString = productInfo.ProductParameters["PriorCertSN"]; + _logger.LogTrace($"Attempting to retrieve the certificate with serial number {priorCertSnString}."); + var reqId = _certificateDataReader.GetRequestIDBySerialNumber(priorCertSnString).Result; + if (string.IsNullOrEmpty(reqId)) + { + throw new Exception($"No certificate with serial number '{priorCertSnString}' could be found."); + } + var expDate = _certificateDataReader.GetExpirationDateByRequestId(reqId); + + var renewCutoff = DateTime.Now.AddDays(renewWindow * -1); + if (expDate > renewCutoff) + { + _logger.LogTrace($"Certificate with serial number {priorCertSnString} is within renewal window"); + enrollmentType = EnrollmentType.Renew; + } + else + { + _logger.LogTrace($"Certificate with serial number {priorCertSnString} is not within renewal window. Reissuing..."); + enrollmentType = EnrollmentType.Reissue; + } } // Check if the order has more validity in it (multi-year cert). If so, do a reissue instead of a renew if (enrollmentType == EnrollmentType.Renew) { // Get the old cert so we can properly construct the request. - priorCertSnString = productInfo.ProductParameters["PriorCertSN"]; - _logger.LogTrace($"Attempting to retrieve the certificate with serial number {priorCertSnString}."); + _logger.LogTrace($"Checking for additional order validity."); priorCertReqID = await _certificateDataReader.GetRequestIDBySerialNumber(priorCertSnString); if (string.IsNullOrEmpty(priorCertReqID)) { @@ -249,8 +289,13 @@ public async Task Enroll(string csr, string subject, Dictionar if (certOrder.order_valid_till.HasValue && certOrder.order_valid_till.Value.AddDays(renewWindow * -1) > DateTime.UtcNow) { + _logger.LogTrace($"Additional order validity found. Reissuing cert with new expiration."); enrollmentType = EnrollmentType.Reissue; } + else + { + _logger.LogTrace($"No additional order validity found. Renewing certificate."); + } } @@ -272,6 +317,10 @@ public async Task Enroll(string csr, string subject, Dictionar } } + /// + /// Get the annotations for the CA Connector-level configuration fields + /// + /// public Dictionary GetCAConnectorAnnotations() { return new Dictionary() @@ -280,29 +329,37 @@ public Dictionary GetCAConnectorAnnotations() { Comments = "API Key for connecting to DigiCert", Hidden = true, - DefaultValue = "" + DefaultValue = "", + Type = "String" }, [CertCentralConstants.Config.DIVISION_ID] = new PropertyConfigInfo() { Comments = "Division ID to use for retrieving product details (only if account is configured with per-divison product settings)", Hidden = false, - DefaultValue = "" + DefaultValue = "", + Type = "Number" }, [CertCentralConstants.Config.REGION] = new PropertyConfigInfo() { Comments = "The geographic region that your DigiCert CertCentral account is in. Valid options are US and EU.", Hidden = false, - DefaultValue = "US" + DefaultValue = "US", + Type = "String" }, [CertCentralConstants.Config.REVOKE_CERT] = new PropertyConfigInfo() { Comments = "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.", Hidden = false, - DefaultValue = "false" + DefaultValue = false, + Type = "Boolean" } }; } + /// + /// Get the list of valid product IDs from DigiCert + /// + /// public List GetProductIds() { try @@ -317,19 +374,27 @@ public List GetProductIds() // If we couldn't get the types, return an empty comment. if (productTypesResponse.Status != CertCentralBaseResponse.StatusType.SUCCESS) { - throw new Exception("Unable to retrieve product list"); + _logger.LogError($"Unable to retrieve product list: {productTypesResponse.Errors[0]}"); + return new List(); } - return productTypesResponse.Products.Select(x => x.NameId).ToList(); } + return productTypesResponse.Products.Select(x => x.NameId).ToList(); + } catch (Exception ex) { // Swallow exceptions and return an empty string. _logger.LogError($"Unable to retrieve product list: {ex.Message}"); - throw; + return new List(); } } - public async Task GetSingleRecord(string caRequestID) + + /// + /// Retrieve a single record from DigiCert + /// + /// The gateway request ID of the record to retrieve, in the format 'orderID-certID' + /// + public async Task GetSingleRecord(string caRequestID) { _logger.MethodEntry(LogLevel.Trace); // Split ca request id into order and cert id @@ -348,7 +413,7 @@ public async Task GetSingleRecord(string caRequestID) string certificate = null; int status = GetCertificateStatusFromCA(certToCheck.status, orderId); - if (status == (int)RequestDisposition.ISSUED || status == (int)RequestDisposition.REVOKED || status == (int)RequestDisposition.UNKNOWN) + 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)); @@ -362,7 +427,7 @@ public async Task GetSingleRecord(string caRequestID) } } _logger.MethodExit(LogLevel.Trace); - return new CAConnectorCertificate + return new AnyCAPluginCertificate { CARequestID = caRequestID, Certificate = certificate, @@ -372,6 +437,10 @@ public async Task GetSingleRecord(string caRequestID) }; } + /// + /// Get the annotations for the product-level configuration fields + /// + /// public Dictionary GetTemplateParameterAnnotations() { return new Dictionary() @@ -380,29 +449,38 @@ public Dictionary GetTemplateParameterAnnotations() { Comments = "OPTIONAL: The number of days of validity to use when requesting certs. If not provided, default is 365.", Hidden = false, - DefaultValue = "365" + DefaultValue = 365, + Type = "Number" }, [CertCentralConstants.Config.CA_CERT_ID] = new PropertyConfigInfo() { Comments = "OPTIONAL: ID of issuing CA to use by DigiCert. If not provided, the default for your account will be used.", Hidden = false, - DefaultValue = "" + DefaultValue = "", + Type = "String" }, [CertCentralConstants.RequestAttributes.ORGANIZATION_NAME] = new PropertyConfigInfo() { Comments = "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.", Hidden = false, - DefaultValue = "" + DefaultValue = "", + Type = "String" }, [CertCentralConstants.Config.RENEWAL_WINDOW] = new PropertyConfigInfo() { Comments = "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.", Hidden = false, - DefaultValue = "90" + DefaultValue = 90, + Type = "Number" } }; } + /// + /// Verify connectivity with the DigiCert web service + /// + /// + /// public async Task Ping() { _logger.MethodEntry(LogLevel.Trace); @@ -428,6 +506,15 @@ public async Task Ping() _logger.MethodExit(LogLevel.Trace); } + /// + /// Revoke either a single certificate or an order, depending on your configuration settings + /// + /// + /// + /// + /// + /// + /// public async Task Revoke(string caRequestID, string hexSerialNumber, uint revocationReason) { _logger.MethodEntry(LogLevel.Trace); @@ -474,10 +561,19 @@ public async Task Revoke(string caRequestID, string hexSerialNumber, uint r _logger.LogError(errMsg); throw new Exception(errMsg); } - return (int)RequestDisposition.REVOKED; + return (int)EndEntityStatus.REVOKED; } - public async Task Synchronize(BlockingCollection blockingBuffer, DateTime? lastSync, bool fullSync, CancellationToken cancelToken) + /// + /// Perform an inventory of DigiCert certs + /// + /// Buffer to return retrieved certs in + /// DateTime of the last sync performed + /// If true, return all certs from DigiCert. If false, only return certs that are new or changed status since the lastSync time. + /// + /// + /// + public async Task Synchronize(BlockingCollection blockingBuffer, DateTime? lastSync, bool fullSync, CancellationToken cancelToken) { _logger.MethodEntry(LogLevel.Trace); @@ -486,7 +582,7 @@ public async Task Synchronize(BlockingCollection blockin string lastSyncFormat = FormatSyncDate(lastSync); string todaySyncFormat = FormatSyncDate(utcDate); - List certs = new List(); + List certs = new List(); List certsToSync = new List(); _logger.LogDebug("Attempting to create a CertCentral client"); @@ -497,6 +593,9 @@ public async Task Synchronize(BlockingCollection blockin if (fullSync) { + long time = DateTime.Now.Ticks; + long starttime = time; + _logger.LogDebug($"SYNC: Starting sync at time {time}"); ListCertificateOrdersResponse ordersResponse = client.ListAllCertificateOrders(); if (ordersResponse.Status == CertCentralBaseResponse.StatusType.ERROR) { @@ -506,14 +605,17 @@ public async Task Synchronize(BlockingCollection blockin } else { + _logger.LogDebug($"SYNC: Found {ordersResponse.orders.Count} records"); foreach (var orderDetails in ordersResponse.orders) { - List orderCerts = new List(); + 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); + _logger.LogDebug($"SYNC: Retrieved {orderCerts.Count} certs at time {DateTime.Now.Ticks}"); } catch { @@ -529,6 +631,7 @@ public async Task Synchronize(BlockingCollection blockin } } + _logger.LogDebug($"SYNC: Complete after {DateTime.Now.Ticks - starttime} ticks"); } } else @@ -545,7 +648,7 @@ public async Task Synchronize(BlockingCollection blockin int orderCount = statusChangesResponse.orders.Count; foreach (var order in statusChangesResponse.orders) { - List orderCerts = new List(); + List orderCerts = new List(); try { cancelToken.ThrowIfCancellationRequested(); @@ -582,6 +685,11 @@ public async Task Synchronize(BlockingCollection blockin _logger.MethodExit(LogLevel.Trace); } + /// + /// Validate CA Connection-level configuration fields + /// + /// + /// public async Task ValidateCAConnectionInfo(Dictionary connectionInfo) { _logger.MethodEntry(LogLevel.Trace); @@ -616,7 +724,7 @@ public async Task ValidateCAConnectionInfo(Dictionary connection if (domains.Status == CertCentralBaseResponse.StatusType.ERROR) { _logger.LogError($"Error from CertCentral client: {domains.Errors[0].message}"); - errors.Add("Error grabbing DigiCert domains"); + errors.Add("Error grabbing DigiCert domains. See log file for details."); } _logger.MethodExit(LogLevel.Trace); // We cannot proceed if there are any errors. @@ -628,9 +736,17 @@ public async Task ValidateCAConnectionInfo(Dictionary connection private void ThrowValidationException(List errors) { - throw new ArgumentException(string.Join("\n", errors)); + string validationMsg = $"Validation errors:\n{string.Join("\n", errors)}"; + throw new AnyCAValidationException(validationMsg); } + /// + /// Validate product-level configuration fields + /// + /// + /// + /// + /// public async Task ValidateProductInfo(EnrollmentProductInfo productInfo, Dictionary connectionInfo) { _logger.MethodEntry(LogLevel.Trace); @@ -654,14 +770,14 @@ public async Task ValidateProductInfo(EnrollmentProductInfo productInfo, Diction CertificateTypesResponse productIdResponse = client.GetAllCertificateTypes(); if (productIdResponse.Status != CertCentralBaseResponse.StatusType.SUCCESS) { - throw new Exception($"The product types could not be retrieved from the server. The following errors occurred: {string.Join(" ", productIdResponse.Errors.Select(x => x.message))}"); + throw new AnyCAValidationException($"The product types could not be retrieved from the server. The following errors occurred: {string.Join(" ", productIdResponse.Errors.Select(x => x.message))}"); } // Get product and check if it exists. var product = productIdResponse.Products.FirstOrDefault(x => x.NameId.Equals(productId, StringComparison.InvariantCultureIgnoreCase)); if (product == null) { - throw new Exception($"The product ID '{productId}' does not exist. The following product IDs are valid: {string.Join(", ", productIdResponse.Products.Select(x => x.NameId))}"); + throw new AnyCAValidationException($"The product ID '{productId}' does not exist. The following product IDs are valid: {string.Join(", ", productIdResponse.Products.Select(x => x.NameId))}"); } // Get product ID details. @@ -670,14 +786,20 @@ public async Task ValidateProductInfo(EnrollmentProductInfo productInfo, Diction detailsRequest.ContainerId = null; if (connectionInfo.ContainsKey(CertCentralConstants.Config.DIVISION_ID)) { - int.TryParse((string)connectionInfo[CertCentralConstants.Config.DIVISION_ID], out int divId); - detailsRequest.ContainerId = divId; + if (int.TryParse($"{connectionInfo[CertCentralConstants.Config.DIVISION_ID]}", out int divId)) + { + detailsRequest.ContainerId = divId; + } + else + { + throw new AnyCAValidationException($"Unable to parse division ID '{connectionInfo[CertCentralConstants.Config.DIVISION_ID]}'. Check that this is a valid division ID."); + } } CertificateTypeDetailsResponse details = client.GetCertificateTypeDetails(detailsRequest); if (details.Errors.Any()) { - throw new Exception($"Validation of '{productId}' failed for the following reasons: {string.Join(" ", details.Errors.Select(x => x.message))}."); + throw new AnyCAValidationException($"Validation of '{productId}' failed for the following reasons: {string.Join(" ", details.Errors.Select(x => x.message))}."); } _logger.MethodExit(LogLevel.Trace); } @@ -702,7 +824,7 @@ private async Task NewCertificate(CertCentralClient client, Or /// private async Task ExtractEnrollmentResult(CertCentralClient client, OrderResponse orderResponse, string commonName) { - int status = (int)RequestDisposition.UNKNOWN; + int status = 0; string statusMessage = null; string certificate = null; string caRequestID = null; @@ -711,7 +833,7 @@ private async Task ExtractEnrollmentResult(CertCentralClient c { _logger.LogError($"Error from CertCentral client: {orderResponse.Errors.First().message}"); - status = (int)RequestDisposition.FAILED; + status = (int)EndEntityStatus.FAILED; statusMessage = orderResponse.Errors[0].message; } else if (orderResponse.Status == CertCentralBaseResponse.StatusType.SUCCESS) @@ -771,12 +893,12 @@ private async Task ExtractEnrollmentResult(CertCentralClient c caRequestID = orderResponse.OrderId.ToString(); if (updateStatusResponse.Errors.Any(x => x.code == "access_denied|invalid_approver")) { - status = (int)RequestDisposition.EXTERNAL_VALIDATION; + status = (int)EndEntityStatus.EXTERNALVALIDATION; statusMessage = errors; } else { - status = (int)RequestDisposition.FAILED; + status = (int)EndEntityStatus.FAILED; statusMessage = $"Approval of order '{orderResponse.OrderId}' failed. Check the gateway logs for more details."; } } @@ -790,7 +912,7 @@ private async Task ExtractEnrollmentResult(CertCentralClient c caRequestID = $"{order.id}-{order.certificate.id}"; try { - CAConnectorCertificate connCert = await GetSingleRecord($"{order.id}-{order.certificate.id}"); + 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"; @@ -798,7 +920,7 @@ private async Task ExtractEnrollmentResult(CertCentralClient c catch (Exception getRecordEx) { _logger.LogWarning($"Unable to retrieve certificate {order.certificate.id} for order {order.id}: {getRecordEx.Message}"); - status = (int)RequestDisposition.UNKNOWN; + status = (int)EndEntityStatus.INPROCESS; statusMessage = $"Post-submission approval of order {order.id} was successful, but pickup failed"; } } @@ -807,8 +929,7 @@ private async Task ExtractEnrollmentResult(CertCentralClient c else { _logger.LogWarning("The request disposition is for this enrollment could not be determined."); - status = (int)RequestDisposition.UNKNOWN; - statusMessage = "The request disposition could not be determined."; + throw new Exception($"The request disposition is for this enrollment could not be determined."); } } } @@ -821,6 +942,12 @@ private async Task ExtractEnrollmentResult(CertCentralClient c }; } + /// + /// Convert DigiCert status string into a EndEntityStatus code + /// + /// + /// + /// private int GetCertificateStatusFromCA(string status, int orderId) { switch (status) @@ -828,32 +955,43 @@ private int GetCertificateStatusFromCA(string status, int orderId) case "issued": case "approved": case "expired": - return (int)RequestDisposition.ISSUED; + return (int)EndEntityStatus.GENERATED; case "processing": case "reissue_pending": case "pending": // Pending from DigiCert means it will be issued after validation - return (int)RequestDisposition.EXTERNAL_VALIDATION; + case "waiting_pickup": + return (int)EndEntityStatus.EXTERNALVALIDATION; case "denied": - return (int)RequestDisposition.DENIED; + case "rejected": + case "canceled": + return (int)EndEntityStatus.FAILED; case "revoked": - return (int)RequestDisposition.REVOKED; + return (int)EndEntityStatus.REVOKED; case "needs_approval": // This indicates that the request has to be approved through DigiCert, which is a misconfiguration _logger.LogWarning($"Order {orderId} needs to be approved in the DigiCert portal prior to issuance"); - return (int)RequestDisposition.EXTERNAL_VALIDATION; + return (int)EndEntityStatus.EXTERNALVALIDATION; default: - _logger.LogWarning($"Order {orderId} has unexpected status {status}"); - return (int)RequestDisposition.UNKNOWN; + _logger.LogError($"Order {orderId} has unexpected status {status}"); + throw new Exception($"Order {orderId} has unknown status {status}"); } } + /// + /// Get the list of reissues for a given order + /// + /// + /// + /// + /// private List GetReissues(CertCentralClient digiClient, int orderId) { _logger.LogTrace($"Getting Reissues for order {orderId}"); + List reqIds = new List(); List reissueCerts = new List(); ListReissueResponse reissueResponse = digiClient.ListReissues(new ListReissueRequest(orderId)); if (reissueResponse.Status == CertCentralBaseResponse.StatusType.ERROR) @@ -870,7 +1008,8 @@ private List GetReissues(CertCentralClient digiClient, int orderId) { order_id = orderId, certificate_id = reissueCert.id, - status = reissueCert.status + status = reissueCert.status, + serialNum = reissueCert.serial_number }; reissueCerts.Add(reissueStatusOrder); } @@ -879,6 +1018,13 @@ private List GetReissues(CertCentralClient digiClient, int orderId) return reissueCerts; } + /// + /// Get the list of duplicate certs for a given order + /// + /// + /// + /// + /// private List GetDuplicates(CertCentralClient digiClient, int orderId) { _logger.LogTrace($"Getting Duplicates for order {orderId}"); @@ -898,7 +1044,8 @@ private List GetDuplicates(CertCentralClient digiClient, int orderI { order_id = orderId, certificate_id = dupeCert.id, - status = dupeCert.status + status = dupeCert.status, + serialNum = dupeCert.serial_number }; dupeCerts.Add(dupeStatusOrder); } @@ -977,6 +1124,11 @@ private async Task Reissue(CertCentralClient client, Enrollmen return await ExtractEnrollmentResult(client, client.ReissueCertificate(reissueRequest), commonName); } + /// + /// Verify that the given product ID is valid + /// + /// + /// private void CheckProductExistence(string productId) { // Check that the product type is still valid. @@ -1023,8 +1175,12 @@ string FormatSyncDate(DateTime? syncTime) return date + "+" + time; } - - private List GetAllConnectorCertsForOrder(string caRequestID) + /// + /// Get all of the certs for a given order, including reissues and duplicates, in CAConnectorCertificate form + /// + /// + /// + private List GetAllConnectorCertsForOrder(string caRequestID) { _logger.MethodEntry(LogLevel.Trace); // Split ca request id into order and cert id @@ -1039,38 +1195,52 @@ private List GetAllConnectorCertsForOrder(string caReque var orderCerts = GetAllCertsForOrder(orderId); - List certList = new List(); + List certList = new List(); foreach (var cert in orderCerts) { - string certificate = null; - string caReqId = cert.order_id + "-" + cert.certificate_id; - int status = GetCertificateStatusFromCA(cert.status, orderId); - if (status == (int)RequestDisposition.ISSUED || status == (int)RequestDisposition.REVOKED || status == (int)RequestDisposition.UNKNOWN) + try { - // We have a status where there may be a cert to download, try to download it - CertificateChainResponse certificateChainResponse = client.GetCertificateChain(new CertificateChainRequest(certId)); - if (certificateChainResponse.Status == CertCentralBaseResponse.StatusType.SUCCESS) + string certificate = null; + string caReqId = cert.order_id + "-" + cert.certificate_id; + int status = GetCertificateStatusFromCA(cert.status, orderId); + if (status == (int)EndEntityStatus.GENERATED || status == (int)EndEntityStatus.REVOKED) { - certificate = certificateChainResponse.Intermediates[0].PEM; + // We have a status where there may be a cert to download, try to download it + CertificateChainResponse certificateChainResponse = client.GetCertificateChain(new CertificateChainRequest(certId)); + if (certificateChainResponse.Status == CertCentralBaseResponse.StatusType.SUCCESS) + { + certificate = certificateChainResponse.Intermediates[0].PEM; + } + else + { + throw new Exception($"Unexpected error downloading certificate {certId} for order {orderId}: {certificateChainResponse.Errors.FirstOrDefault()?.message}"); + } } - else + var connCert = new AnyCAPluginCertificate { - _logger.LogWarning($"Unexpected error downloading certificate {certId} for order {orderId}: {certificateChainResponse.Errors.FirstOrDefault()?.message}"); - } + CARequestID = caReqId, + Certificate = certificate, + Status = status, + ProductID = orderResponse.product.name_id, + RevocationDate = GetRevocationDate(orderResponse) + }; + certList.Add(connCert); } - var connCert = new CAConnectorCertificate + catch (Exception ex) { - CARequestID = caReqId, - Certificate = certificate, - Status = status, - ProductID = orderResponse.product.name_id, - RevocationDate = GetRevocationDate(orderResponse) - }; - certList.Add(connCert); + _logger.LogWarning($"Error processing cert {cert.order_id}-{cert.certificate_id}: {ex.Message}. Skipping record."); + } } return certList; } + + /// + /// Get all of the certs for a given order, including reissues and duplicates, in StatusOrder form + /// + /// + /// + /// private List GetAllCertsForOrder(int orderId) { CertCentralClient client = CertCentralClientUtilities.BuildCertCentralClient(_config); @@ -1098,10 +1268,13 @@ private List GetAllCertsForOrder(int orderId) { order_id = orderId, certificate_id = orderResponse.certificate.id, - status = orderStatusString + status = orderStatusString, + serialNum = orderResponse.certificate.serial_number + }; + List orderCerts = new List + { + primary }; - List orderCerts = new List(); - orderCerts.Add(primary); if (reissueCerts?.Count > 0) { orderCerts.AddRange(reissueCerts); @@ -1110,7 +1283,33 @@ private List GetAllCertsForOrder(int orderId) { orderCerts.AddRange(dupeCerts); } - return orderCerts; + List retCerts = new List(); + List reqIds = new List(); + List serNums = new List(); + foreach (var cert in orderCerts) + { + string req = $"{cert.order_id}-{cert.certificate_id}"; + + // Listing reissues/duplicates can also return the primary certificate. This check insures that only one copy of the primary certificate gets added to the sync list. + if (!reqIds.Contains(req)) + { + // This is actually caused by an issue in the DigiCert API. For some orders (but not all), retrieving the reissued/duplicate certificates on an order + // 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)) + { + reqIds.Add(req); + retCerts.Add(cert); + serNums.Add(cert.serialNum); + } + else + { + _logger.LogWarning($"Duplicate certificate serial numbers found. Only one will be synced. Order ID: {cert.order_id}"); + } + } + } + return retCerts; } } } \ No newline at end of file diff --git a/digicert-certcentral-anycagateway/digicert-certcentral-anycagateway.csproj b/digicert-certcentral-anycagateway/digicert-certcentral-anycagateway.csproj index faeceef..c96dcd7 100644 --- a/digicert-certcentral-anycagateway/digicert-certcentral-anycagateway.csproj +++ b/digicert-certcentral-anycagateway/digicert-certcentral-anycagateway.csproj @@ -9,10 +9,10 @@ - - + + - + diff --git a/integration-manifest.json b/integration-manifest.json index f9c7984..e0ca91a 100644 --- a/integration-manifest.json +++ b/integration-manifest.json @@ -7,5 +7,6 @@ "link_github": false, "update_catalog": false, "description": "DigiCert CertCentral plugin for the AnyCA Gateway framework", - "gateway_framework": "1.0.0" + "gateway_framework": "1.0.0", + "release_dir": "digicert-certcentral-anycagateway/bin/Release/net6.0" } diff --git a/readme_source.md b/readme_source.md index 101b084..c0d1cd8 100644 --- a/readme_source.md +++ b/readme_source.md @@ -17,7 +17,7 @@ In order to enroll for certificates the Keyfactor Command server must trust the ```json { "extensions": { - "Keyfactor.AnyGateway.Extensions.ICAConnector": { + "Keyfactor.AnyGateway.Extensions.IAnyCAPlugin": { "DigiCertCAConnector": { "assemblypath": "../DigiCertCAGateway.dll", "TypeFullName": "Keyfactor.Extensions.CAGateway.DigiCert.CertCentralCAConnector"