diff --git a/README.md.tpl b/README.md.tpl
index 0211243..9ebc119 100644
--- a/README.md.tpl
+++ b/README.md.tpl
@@ -48,13 +48,19 @@ To begin the migration process, the DatabaseManagementConsole.exe.config will ne
The following sections will breakdown the required configurations for the AnyGatewayConfig.json file that will be imported to configure the AnyGateway.
### Templates
-The Template section will map the CA's SSL profile to an AD template. Currently the only required parameter is the MultiDomain flag. This flag lets Keyfactor know if the certificate can contain multiple domain names. Depending on the setting, the SAN entries of the request will change to support Sectigo Requirements.
+The Template section will map the CA's SSL profile to an AD template. The following parameters are accepted:
+* ```MultiDomain```
+REQUIRED. This flag lets Keyfactor know if the certificate can contain multiple domain names. Depending on the setting, the SAN entries of the request will change to support Sectigo Requirements.
+* ```Department```
+OPTIONAL. If your Sectigo account is using department-level products, put the appropriate department name here. Previous versions of the Sectigo gateway read this value from the OU field of the subject, which is now deprecated.
+
```json
"Templates": {
"SectigoEnterpriseSSLPro1yr": {
"ProductID": "3210", /*Sectigo EnterpriseSSL Pro - ID from Cert Manager*/
"Parameters": {
- "MultiDomain": "false"
+ "MultiDomain": "false",
+ "Department": "Department Name"
}
}
}
diff --git a/src/SectigoCAProxy/SectigoCAProxy.cs b/src/SectigoCAProxy/SectigoCAProxy.cs
index e7eea40..c779ef2 100644
--- a/src/SectigoCAProxy/SectigoCAProxy.cs
+++ b/src/SectigoCAProxy/SectigoCAProxy.cs
@@ -1,19 +1,24 @@
-// Copyright 2021 Keyfactor
-// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
-// Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions
-// and limitations under the License.
+// Copyright 2021 Keyfactor
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
+// Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions
+// and limitations under the License.
using CAProxy.AnyGateway;
using CAProxy.AnyGateway.Interfaces;
using CAProxy.AnyGateway.Models;
-using CAProxy.Common;
-using Common.Logging;
+using CAProxy.Common;
+
+using Common.Logging;
+
using CSS.Common.Logging;
-using CSS.PKI;
+using CSS.PKI;
+
using Keyfactor.AnyGateway.Sectigo.API;
-using Keyfactor.AnyGateway.Sectigo.Client;
-using Newtonsoft.Json;
+using Keyfactor.AnyGateway.Sectigo.Client;
+
+using Newtonsoft.Json;
+
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
@@ -24,769 +29,796 @@
using System.Threading.Tasks;
namespace Keyfactor.AnyGateway.Sectigo
-{
- ///
- /// Implementation of the for Secitgo Certificate Manager. This class contains the
- /// entry points for AnyGateway Synchronization, Revocation, and Enrollment functions.
- ///
- public class SectigoCAProxy : BaseCAConnector
- {
-
- ///
- /// CAConnection section of the imported AnyGateway configuration
- ///
- SectigoCAConfig Config { get; set; }
-
- ///
- /// API Implementation for AnyGateway to interact with the Sectigo Certificate Manager API
- ///
- SectigoApiClient Client { get; set; }
-
- ///
- /// Method to query, parse, and return certificates to be synchronized with the AnyGateway database
- ///
- /// Interface to access existing certificate data from the Gateway database
- /// A to queue certificates for synchronization
- /// Details about previous synchronization attempts
- ///
- public override void Synchronize(ICertificateDataReader certificateDataReader,
- BlockingCollection blockingBuffer,
- CertificateAuthoritySyncInfo certificateAuthoritySyncInfo,
- CancellationToken cancelToken)
- {
- Logger.MethodEntry(ILogExtensions.MethodLogLevel.Debug);
-
- Task producerTask = null;
- CancellationTokenSource newCancelToken = CancellationTokenSource.CreateLinkedTokenSource(cancelToken);
-
- try
- {
- var certsToAdd = new BlockingCollection(100);
- Logger.Info($"Begin Paging Certificate List");
- int pageSize = 25;
- if (Config.PageSize > 0)
- {
- pageSize = Config.PageSize;
- }
- producerTask = Client.CertificateListProducer(certsToAdd, newCancelToken.Token, Config.PageSize, Config.SyncFilter);
-
- foreach (Certificate certToAdd in certsToAdd.GetConsumingEnumerable())
- {
- if (cancelToken.IsCancellationRequested)
- {
- Logger.Warn($"Task was canceled. Stopping Synchronize task.");
- blockingBuffer.CompleteAdding();
- break;
- }
-
- if (producerTask.Exception != null)
- {
- Logger.Error($"Synchronize task failed with the following message: {producerTask.Exception.Flatten().Message}");
- throw producerTask.Exception.Flatten();
- }
-
- CAConnectorCertificate dbCert=null;
- //serial number is blank on certs that have not been issued (awaiting approval)
- if(!String.IsNullOrEmpty(certToAdd.SerialNumber))
- dbCert = certificateDataReader.GetCertificateRecord(CSS.Common.DataConversion.HexToBytes(certToAdd.SerialNumber));
-
-
- //are we syncing a reissued cert?
- //Reissued certs keep the same ID, but may have different data and cause index errors on sync
- //Removed reissued certs from enrollment, but may be some stragglers for legacy installs
- int syncReqId = 0;
- if (dbCert != null && dbCert.CARequestID.Contains('-'))
- {
- syncReqId = int.Parse(dbCert.CARequestID.Split('-')[0]);
- }
- else if (dbCert != null)
- {
- syncReqId = int.Parse(dbCert.CARequestID);
- }
-
- string certData = string.Empty;
- if (dbCert != null)
- {
- //we found an existing cert from the DB by serial number.
- //This should already be in the DB so no need to sync again unless status changes or
- //admin has forced a complete sync
- if (dbCert.Status == ConvertToKeyfactorStatus(certToAdd.status) && !Config.ForceCompleteSync)
- {
- Logger.Trace($"Certificate {certToAdd.CommonName} (Id: {certToAdd.Id}) already synced. Skipping.");
- continue;
- }
- var statusMessage = dbCert.Status == ConvertToKeyfactorStatus(certToAdd.status) ? "not changed" : "changed";
- var forcedMessage = Config.ForceCompleteSync ? "Complete sync forced by configuration. " : string.Empty;
-
- Logger.Trace($"Certificate {certToAdd.CommonName} status {statusMessage}.{forcedMessage} Syncing certificate.");
- certData = dbCert.Certificate;
- }
- else
- {
- //No certificate in the DB by SN. Need to download to get full certdata required for sync process
- Logger.Trace($"Attempt to Pickup Certificate {certToAdd.CommonName} (ID: {certToAdd.Id})");
- var certdataApi = Task.Run(async () => await Client.PickupCertificate(certToAdd.Id, certToAdd.CommonName)).Result;
- if(certdataApi!=null)
- certData = Convert.ToBase64String(certdataApi.GetRawCertData());
- }
-
- if (certToAdd == null || String.IsNullOrEmpty(certToAdd.SerialNumber) || String.IsNullOrEmpty(certToAdd.CommonName) || String.IsNullOrEmpty(certData))
- {
- Logger.Debug($"Certificate Data unavailable for {certToAdd.CommonName} (ID: {certToAdd.Id}). Skipping ");
- continue;
- }
-
- CAConnectorCertificate caCertToAdd = new CAConnectorCertificate
- {
- CARequestID = syncReqId == 0 ? certToAdd.Id.ToString() : syncReqId.ToString(),
- ProductID = certToAdd.CertType.id.ToString(),
- Certificate = certData,
- Status = ConvertToKeyfactorStatus(certToAdd.status),
- SubmissionDate = certToAdd.requested,
- ResolutionDate = certToAdd.approved,
- RevocationReason = ConvertToKeyfactorStatus(certToAdd.status) == 21 ? 0 : 0xffffff,
- RevocationDate = certToAdd.revoked ?? DateTime.UtcNow,
- };
-
- if (blockingBuffer.TryAdd(caCertToAdd, 50, cancelToken))
- {
- Logger.Debug($"Added {certToAdd.CommonName} (ID:{(syncReqId == 0 ? certToAdd.Id.ToString() : syncReqId.ToString())}) to queue for synchronization");
- }
- else
- {
- Logger.Debug($"Adding {certToAdd.CommonName} to queue was blocked. Retrying");
- }
- }
- Logger.Info($"Adding Certificates to Queue is Complete.");
- blockingBuffer.CompleteAdding();
- }
- catch (Exception ex)
- {
- //gracefully exit so any certs added to queue prior to failure will still sync
- Logger.Error($"Synchronize Task failed. {ex.Message} | {ex.StackTrace}");
- if (producerTask != null && !producerTask.IsCompleted)
- {
- newCancelToken.Cancel();
- }
-
- blockingBuffer.CompleteAdding();
- }
- Logger.MethodExit(ILogExtensions.MethodLogLevel.Debug);
- }
-
- ///
- /// Method to execute a single certificate sync after enrollment or revocation. Used to update status of a single record in Command
- ///
- /// The certificate's unique ID as defined by the syncronization/enroll methods
- ///
- public override CAConnectorCertificate GetSingleRecord(string caRequestID)
- {
- Logger.MethodEntry(ILogExtensions.MethodLogLevel.Debug);
-
- Logger.Trace($"Get Single Certificate Detail from Sectigo (sslId: {caRequestID})");
- int sslId = int.Parse(caRequestID.Split('-')[0]);
-
- var singleCert = Task.Run(async () => await Client.GetCertificate(sslId)).Result;
- Logger.Trace($"{singleCert.CommonName} ({singleCert.status}) retrieved from Sectigo.");
-
- //Pending external validation, cannot download certificate data from API
- if (ConvertToKeyfactorStatus(singleCert.status) == 13|| ConvertToKeyfactorStatus(singleCert.status) == 21)
- {
- return new CAConnectorCertificate()
- {
- CARequestID = caRequestID,
- ProductID = singleCert.CertType.id.ToString(),
- SubmissionDate = singleCert.requested,
- ResolutionDate = singleCert.approved,
- Status = ConvertToKeyfactorStatus(singleCert.status),
- RevocationReason = ConvertToKeyfactorStatus(singleCert.status) == 21 ? 0 : 0xffffff,
- RevocationDate = singleCert.revoked ?? DateTime.UtcNow
- };
-
- }
-
- var certData = PickupSingleCert(sslId, singleCert.CommonName);
- if (certData != null)
- {
- Logger.MethodExit(ILogExtensions.MethodLogLevel.Debug);
- return new CAConnectorCertificate()
- {
- CARequestID = caRequestID,
- Certificate = Convert.ToBase64String(certData.GetRawCertData()),
- ProductID = singleCert.CertType.id.ToString(),
- SubmissionDate = singleCert.requested,
- ResolutionDate = singleCert.approved,
- Status = ConvertToKeyfactorStatus(singleCert.status),
- RevocationReason = ConvertToKeyfactorStatus(singleCert.status) == 21 ? 0 : 0xffffff,
- RevocationDate = singleCert.revoked ?? DateTime.UtcNow
- };
-
- }
-
- throw new Exception("Failed to download certificate data from Sectigo.");
- }
-
-
- ///
- /// Method to execture a new, renew, or reissue enrollment request from Keyfactor Command
- ///
- /// Interface to access existing certificate data from the Gateway database
- /// A base64 endocded certificate signing request in
- /// The distingused name of the certificate being requested
- /// All supported (dns, ipv4, ipv6, ) san entries defined during enrollment
- /// Template details parsed from the Template configuration section
- ///
- ///
- ///
- public override EnrollmentResult Enroll(ICertificateDataReader certificateDataReader, string csr, string subject, Dictionary san, EnrollmentProductInfo productInfo, PKIConstants.X509.RequestFormat requestFormat, RequestUtilities.EnrollmentType enrollmentType)
- {
- Logger.MethodEntry(ILogExtensions.MethodLogLevel.Debug);
- Logger.Info($"Begin {enrollmentType} enrollment for {subject}");
- try
- {
- Logger.Debug("Parse Subject for Common Name, Organization, and Org Unit");
-
- string commonName = ParseSubject(subject, "CN=");
- Logger.Trace($"Common Name: {commonName}");
-
- string orgStr = ParseSubject(subject, "O=");
- Logger.Trace($"Organization: {orgStr}");
-
- string ouStr = ParseSubject(subject, "OU=");
- Logger.Trace($"Org Unit: {ouStr}");
-
- var fieldList = Task.Run(async () => await Client.ListCustomFields()).Result;
- var mandatoryFields = fieldList.CustomFields?.Where(f => f.mandatory);
-
- Logger.Debug("Check for mandatory custom fields");
- foreach (CustomField reqField in mandatoryFields)
- {
- Logger.Trace($"Checking product parameters for {reqField.name}");
- if (!productInfo.ProductParameters.ContainsKey(reqField.name))
- {
- Logger.MethodExit(ILogExtensions.MethodLogLevel.Debug);
- return new EnrollmentResult { Status = 30, StatusMessage = $"Template {productInfo.ProductID} or Enrollment Fields do not contain a mandatory custom field value for of {reqField.name}" };
- }
- }
- Logger.Debug($"Search for Organization by Name {orgStr}");
-
- int requstOrgId = 0;
- var org = Task.Run(async () => await GetOrganizationAsync(orgStr)).Result;
- if (org == null)
- {
- string err = $"Unable to find Organization by Name {orgStr} ";
- Logger.Error($"{err}");
- return new EnrollmentResult { Status = 30, StatusMessage = err };
- }
-
- Department ou = null;
- //API returned no CertType node or it was an empty string. This changed at some point with the Sectigo API.
- if (org.certTypes == null || org.certTypes.Count == 0)
- {
- Logger.Trace($"{orgStr} does not contain a valid certificate type configuration. Verify Org Unit");
- ou = org.departments.Where(x => x.name.ToLower().Equals(ouStr.ToLower())).FirstOrDefault();
-
- if (ou == null)
- {
- string err = $"{ouStr} does not exist as a department of {orgStr}. Please verify configuration";
- Logger.Error($"{err}");
- return new EnrollmentResult { Status = 30, StatusMessage = err };
- }
-
- Logger.Trace($"{ou} is valid. Apply {ou.id} to request");
- requstOrgId = ou.id;
- }
- else
- {
- Logger.Trace($"{orgStr} contain a valid certificate type configuration. Apply {org.id} to request");
- requstOrgId = org.id;
- }
-
-
- //Check if SAN matches the SUBJECT CN when multidomain = false (single domain cert).
- //If true, we need to send empty san array. if different, join array (remove if one?)
- bool isMultiDomain = bool.Parse(productInfo.ProductParameters["MultiDomain"]);
- string sanList = ParseSanList(san, isMultiDomain, commonName);
-
- var enrollmentProfile = Task.Run(async () => await GetProfile(int.Parse(productInfo.ProductID))).Result;
- if (enrollmentProfile != null)
- {
- Logger.Trace($"Found {enrollmentProfile.name} profile for enroll request");
- }
-
-
- int sslId;
- string priorSn = string.Empty;
- Certificate newCert = null;
-
- switch (enrollmentType)
- {
- case RequestUtilities.EnrollmentType.New:
- case RequestUtilities.EnrollmentType.Reissue:
- case RequestUtilities.EnrollmentType.Renew:
-
- EnrollRequest request = new EnrollRequest
- {
- csr = csr,
- orgId = requstOrgId,
- term = Task.Run(async () => await GetProfileTerm(int.Parse(productInfo.ProductID))).Result,
- certType = enrollmentProfile.id,
- //External requestor is expected to be an email. Use config to pull the enrollment field or send blank
- //sectigo will default to the account (API account) making the request.
- externalRequester = GetExternalRequestor(productInfo),
- numberServers = 1,
- serverType = -1,
- subjAltNames = sanList,//,
- comments = $"CERTIFICATE_REQUESTOR: {productInfo.ProductParameters["Keyfactor-Requester"]}"//this is how the current gateway passes this data
- };
-
- Logger.Debug($"Submit {enrollmentType} request for {subject}");
- sslId = Task.Run(async () => await Client.Enroll(request)).Result;
- newCert = Task.Run(async () => await Client.GetCertificate(sslId)).Result;
- Logger.Debug($"Enrolled for Certificate {newCert.CommonName} (ID: {newCert.Id}) | Status: {newCert.status}. Attempt to Pickup Certificate.");
- break;
-
- default:
- return new EnrollmentResult { Status = 30, StatusMessage = $"Unsupported enrollment type {enrollmentType}" };
- }
-
- return PickUpEnrolledCertificate(newCert);
- }
- catch (HttpRequestException httpEx)
- {
- Logger.Error($"Enrollment Failed due to a HTTP error: {httpEx.Message}");
- return new EnrollmentResult { Status = 30, StatusMessage = httpEx.Message };
-
- }
- catch (SectigoApiException apiEx)
- {
- Logger.Error($"Enrollment Failed due to an API error: {apiEx.Message}");
- return new EnrollmentResult { Status = 30, StatusMessage = apiEx.Message };
- }
- catch (Exception ex)
- {
- Logger.Error($"Enrollment Failed with the following error: {ex.Message}");
- Logger.Error($"Inner Exception Message: {ex.InnerException.Message}");
- return new EnrollmentResult { Status = 30, StatusMessage = ex.InnerException.Message };
- }
- }
-
- private string GetExternalRequestor(EnrollmentProductInfo productInfo)
- {
- if (!String.IsNullOrEmpty(Config.ExternalRequestorFieldName))
- {
- if (!String.IsNullOrEmpty(productInfo.ProductParameters[Config.ExternalRequestorFieldName]))
- {
- return productInfo.ProductParameters[Config.ExternalRequestorFieldName];
- }
- }
- return string.Empty;
- }
- public X509Certificate2 PickupSingleCert(int sslId, string subject)
- {
- Logger.MethodEntry(ILogExtensions.MethodLogLevel.Debug);
- int retryCounter = 0;
- Thread.Sleep(5 * 1000);//small static delay as an attempt to avoid retries all together
- while (retryCounter < Config.PickupRetries)
- {
- Logger.Debug($"Try number {retryCounter + 1} to pickup single certificate");
- var certificate = Task.Run(async () => await Client.PickupCertificate(sslId, subject)).Result;
- if (certificate != null && !String.IsNullOrEmpty(certificate.Subject))
- {
- Logger.Info($"Successfully picked up certificate { certificate.Subject}");
- Logger.MethodExit(ILogExtensions.MethodLogLevel.Debug);
- return certificate;
- }
- Thread.Sleep(Config.PickupDelayInSeconds * 1000);//convert seconds to ms for delay.
- retryCounter++;
- }
-
- Logger.MethodExit(ILogExtensions.MethodLogLevel.Debug);
- return null;
- }
-
- public EnrollmentResult PickUpEnrolledCertificate(Certificate sslCert)
- {
- if (sslCert.status.Equals("Issued", StringComparison.InvariantCultureIgnoreCase)||
- sslCert.status.Equals("Applied", StringComparison.InvariantCultureIgnoreCase))
- {
- return PickUpEnrolledCertificate(sslCert.Id, sslCert.CommonName);
- }
-
- Logger.Info($"Certificate {sslCert.CommonName} (ID: {sslCert.Id}) has not been issued. Certificate will be picked up during synchronization after approval.");
- return new EnrollmentResult
- {
- CARequestID = $"{sslCert.Id}",
- Status = (int)PKIConstants.Microsoft.RequestDisposition.EXTERNAL_VALIDATION,
- StatusMessage = "Certificate requires approval. Certificate will be picked up during synchronization after approval."
- };
- }
-
- public EnrollmentResult PickUpEnrolledCertificate(int sslId, string subject)
- {
- Logger.MethodEntry(ILogExtensions.MethodLogLevel.Debug);
- int retryCounter = 0;
- Thread.Sleep(5 * 1000);//small static delay as an attempt to avoid retries all together
- while (retryCounter < Config.PickupRetries)
- {
- Logger.Debug($"Try number {retryCounter + 1} to pickup enrolled certificate");
- var certificate = Task.Run(async () => await Client.PickupCertificate(sslId, subject)).Result;
- if (certificate != null && !String.IsNullOrEmpty(certificate.Subject))
- {
- Logger.Info($"Successfully enrolled for certificate { certificate.Subject}");
- Logger.MethodExit(ILogExtensions.MethodLogLevel.Debug);
- return new EnrollmentResult
- {
- CARequestID = $"{sslId}",
- Certificate = Convert.ToBase64String(certificate.GetRawCertData()),
- Status = (int)PKIConstants.Microsoft.RequestDisposition.ISSUED,
- StatusMessage = $"Successfully enrolled for certificate {certificate.Subject}"
- };
- }
- Thread.Sleep(Config.PickupDelayInSeconds * 1000);//convert seconds to ms for delay.
- retryCounter++;
- }
-
- Logger.MethodExit(ILogExtensions.MethodLogLevel.Debug);
- return new EnrollmentResult
- {
- CARequestID = $"{sslId}",
- Status = (int)PKIConstants.Microsoft.RequestDisposition.EXTERNAL_VALIDATION,
- StatusMessage = "Failed to pickup certificate. Check SCM portal to determine if addtional approval is required"
- };
- }
-
- ///
- /// Method that sets up configuration class and API client. Called before each sync, revocation, or enrollment.
- /// is the configuration that is currently saved in the AnyGateway database for the CA.
- ///
- ///
- public override void Initialize(ICAConnectorConfigProvider configProvider)
- {
- Logger.MethodEntry(ILogExtensions.MethodLogLevel.Debug);
-
- Config = JsonConvert.DeserializeObject(JsonConvert.SerializeObject(configProvider.CAConnectionData));
- if (Config.PageSize > 200)
- {
- Config.PageSize = 200;//max value allowed by API
- }
-
- Client = InitializeRestClient(configProvider.CAConnectionData, Logger);
-
- Logger.MethodExit(ILogExtensions.MethodLogLevel.Debug);
- }
-
- ///
- /// Method to revoke a certificate from the Sectigo CA
- ///
- ///
- ///
- ///
- ///
- public override int Revoke(string caRequestID, string hexSerialNumber, uint revocationReason)
- {
- Logger.MethodEntry(ILogExtensions.MethodLogLevel.Debug);
-
- var response = Task.Run(async () => await Client.RevokeSslCertificateById(int.Parse(caRequestID), RevokeReasonToString(revocationReason))).Result;
-
- Logger.MethodExit(ILogExtensions.MethodLogLevel.Debug);
- if (response)//will throw an exception if false
- {
- return 21;//revoked
- }
-
- return -1;
- }
- ///
- /// Method to respond to certutil -ping command
- ///
- public override void Ping()
- {
- Logger.MethodEntry(ILogExtensions.MethodLogLevel.Debug);
- Logger.MethodExit(ILogExtensions.MethodLogLevel.Debug);
- }
-
- ///
- /// Method to validate CAConnection section of configuration during import of the configuration JSON file
- ///
- ///
- public override void ValidateCAConnectionInfo(Dictionary connectionInfo)
- {
- Logger.MethodEntry(ILogExtensions.MethodLogLevel.Debug);
- //determine required fields
- //URL
- //Auth Type (Cert or UN PASSWORD)
- List errors = new List();
- errors.Add(ValidateConfigurationKey(connectionInfo, Constants.API_ENDPOINT_KEY));
- errors.Add(ValidateConfigurationKey(connectionInfo, Constants.AUTH_TYPE_KEY));
-
- Logger.MethodExit(ILogExtensions.MethodLogLevel.Debug);
- if (errors.Any(s => !String.IsNullOrEmpty(s)))
- {
- throw new Exception(String.Join("|", errors.All(s => !String.IsNullOrEmpty(s))));
- }
- }
-
- ///
- /// Method to validate product details of a template configurd in the configuration JSON file
- ///
- ///
- ///
-
- public override void ValidateProductInfo(EnrollmentProductInfo productInfo, Dictionary connectionInfo)
- {
- Logger.MethodEntry(ILogExtensions.MethodLogLevel.Debug);
- SectigoApiClient localClient = InitializeRestClient(connectionInfo, Logger);
-
- var profileList = Task.Run(async () => await localClient.ListSslProfiles()).Result;
- if (profileList.SslProfiles.Where(p => p.id == int.Parse(productInfo.ProductID)).Count() == 0)
- {
- Logger.MethodExit(ILogExtensions.MethodLogLevel.Debug);
- throw new Exception($"Unable to find SSl Profile with ID {productInfo.ProductID}");
- }
- Logger.MethodExit(ILogExtensions.MethodLogLevel.Debug);
-
- }
-
- private async Task GetOrganizationId(string orgName)
- {
- var orgList = await Client.ListOrganizations();
- return orgList.Organizations.Where(x => x.name.ToLower().Equals(orgName.ToLower())).FirstOrDefault().id;
- }
- private async Task GetOrganizationAsync(string orgName)
- {
- var orgList = await Client.ListOrganizations();
- return orgList.Organizations.Where(x => x.name.ToLower().Equals(orgName.ToLower())).FirstOrDefault();
- }
-
- private async Task GetProfileTerm(int profileId)
- {
- var profileList = await Client.ListSslProfiles();
- return profileList.SslProfiles.Where(x => x.id == profileId).FirstOrDefault().terms[0];
- }
-
- private async Task GetProfile(int profileId)
- {
- var profileList = await Client.ListSslProfiles();
- return profileList.SslProfiles.Where(x => x.id == profileId).FirstOrDefault();
- }
-
-
- #region Static Helpers
- private static string ParseSanList(Dictionary san, bool multiDomain, string commonName)
- {
- string sanList = string.Empty;
- List allSans = new List();
- foreach (var k in san.Keys)
- {
- allSans.AddRange(san[k].ToList());
- }
-
- if (!multiDomain)
- {
- if (allSans.Contains(commonName) && allSans.Count() > 1)
- {
- List sans = allSans.ToList();
- sans.Remove(commonName);
- sanList = string.Join(",", sans.ToArray());
- }
- }
- else
- {
- if (allSans.Contains(commonName))
- {
- List sans = allSans.ToList();
- sans.Remove(commonName);
- sanList = string.Join(",", sans.ToArray());
- }
- else
- {
- List sans = allSans.ToList();
- sanList = string.Join(",", sans.ToArray());
- }
- }
- return sanList;
- }
-
- private static string ParseSubject(string subject, string rdn)
- {
- string escapedSubject = subject.Replace("\\,", "|");
- string rdnString = escapedSubject.Split(',').ToList().Where(x => x.Contains(rdn)).FirstOrDefault();
-
- if (!string.IsNullOrEmpty(rdnString))
- {
- return rdnString.Replace(rdn, "").Replace("|", ",").Trim();
- }
- else
- {
-
- throw new Exception($"The request is missing a {rdn} value");
- }
- }
- ///
- /// Ensure the key is configured in the CAConnectionDetail section
- ///
- ///
- ///
- ///
- private static string ValidateConfigurationKey(Dictionary connectionInfo, string key)
- {
- if (!connectionInfo.TryGetValue(key, out object tempValue) && tempValue != null)
- {
- return $"{key} is a required configuration value";
- }
-
- return string.Empty;
- }
- private static int ConvertToKeyfactorStatus(string status)
- {
- switch (status.ToUpper())
- {
- case "ISSUED":
- case "ENROLLED - PENDING DOWNLOAD":
- case "APPROVED":
- case "APPLIED":
- case "DOWNLOADED":
- return 20;
- case "REQUESTED":
- case "AWAITING APPROVAL":
- case "NOT ENROLLED":
- return 13;
- case "REVOKED":
- return 21;
- case "ANY":
- default:
- return (int)CSS.PKI.PKIConstants.Microsoft.RequestDisposition.UNKNOWN;//unknown
- }
-
- }
-
- public static string RevokeReasonToString(UInt32 revokeType)
- {
- switch (revokeType)
- {
- case 1:
- return "Compromised Key";
- case 2:
- return "CA Compromised";
- case 3:
- return "Affiliation Changed";
- case 4:
- return "Superseded";
- case 5:
- return "Cessation of Operation";
- case 6:
- return "Certificate Hold";
- default:
- return "Unspecified";
- }
- }
-
- public static int RevokeStringToCode(string revokePhrase)
- {
- switch (revokePhrase.ToLower())
- {
- case "compromised key":
- return 1;
- case "ca compromised":
- return 2;
- case "affiliation changed":
- return 3;
- case "superseded":
- return 4;
- case "cessation of operation":
- return 5;
- case "certificate hold":
- return 6;
- default:
- return 0;
- }
- }
- private static SectigoApiClient InitializeRestClient(Dictionary connectionInfo, ILog logger)
- {
- logger.MethodEntry(ILogExtensions.MethodLogLevel.Debug);
- SectigoCAConfig localConfig = JsonConvert.DeserializeObject(JsonConvert.SerializeObject(connectionInfo));
- WebRequestHandler webRequestHandler = new WebRequestHandler();
-
- if (localConfig.AuthenticationType.ToLower() == "certificate")
- {
- webRequestHandler.ClientCertificateOptions = ClientCertificateOption.Manual;
- X509Certificate2 authCert = GetClientCertificate(connectionInfo, logger);
- if (authCert == null)
- {
- logger.MethodExit(ILogExtensions.MethodLogLevel.Debug);
- throw new Exception("AuthType set to Certificate, but no certificate found!");
- }
-
- webRequestHandler.ClientCertificates.Add(authCert);
- }
-
- HttpClient restClient = new HttpClient(webRequestHandler)
- {
- BaseAddress = new Uri(localConfig.ApiEndpoint)
- };
-
- restClient.DefaultRequestHeaders.Add(Constants.CUSTOMER_URI_KEY, localConfig.CustomerUri);
- restClient.DefaultRequestHeaders.Add(Constants.CUSTOMER_LOGIN_KEY, localConfig.Username);
- //Determine
-
- if (localConfig.AuthenticationType.ToLower() == "password")
- {
- restClient.DefaultRequestHeaders.Add(Constants.CUSTOMER_PASSWORD_KEY, localConfig.Password);
- }
-
- logger.MethodExit(ILogExtensions.MethodLogLevel.Debug);
- return new SectigoApiClient(restClient);
- }
- private static X509Certificate2 GetClientCertificate(Dictionary config, ILog logger)
- {
- logger.MethodEntry(ILogExtensions.MethodLogLevel.Debug);
- Dictionary caConnectionCertificateDetail = config["ClientCertificate"] as Dictionary;
-
- StoreName sn;
- StoreLocation sl;
- string thumbprint = (string)caConnectionCertificateDetail["Thumbprint"];
-
- if (String.IsNullOrEmpty(thumbprint) ||
- !Enum.TryParse((string)caConnectionCertificateDetail["StoreName"], out sn) ||
- !Enum.TryParse((string)caConnectionCertificateDetail["StoreLocation"], out sl))
- {
- throw new Exception("Unable to find client authentication certificate");
- }
-
- X509Certificate2Collection foundCerts;
- using (X509Store currentStore = new X509Store(sn, sl))
- {
- logger.Trace($"Search for client auth certificates with Thumprint {thumbprint} in the {sn}{sl} certificate store");
-
- currentStore.Open(OpenFlags.ReadOnly | OpenFlags.OpenExistingOnly);
- foundCerts = currentStore.Certificates.Find(X509FindType.FindByThumbprint, thumbprint, true);
- logger.Trace($"Found {foundCerts.Count} certificates in the {currentStore.Name} store");
- currentStore.Close();
- }
- if (foundCerts.Count > 1)
- {
- throw new Exception($"Multiple certificates with Thumprint {thumbprint} found in the {sn}{sl} certificate store");
- }
- logger.MethodExit(ILogExtensions.MethodLogLevel.Debug);
- if (foundCerts.Count > 0)
- return foundCerts[0];
-
- return null;
- }
- #endregion
-
- #region Obsolete Methods
- [Obsolete]
- public override EnrollmentResult Enroll(string csr, string subject, Dictionary san, EnrollmentProductInfo productInfo, CSS.PKI.PKIConstants.X509.RequestFormat requestFormat, RequestUtilities.EnrollmentType enrollmentType)
- {
- throw new NotImplementedException();
- }
- [Obsolete]
- public override void Synchronize(ICertificateDataReader certificateDataReader, BlockingCollection blockingBuffer, CertificateAuthoritySyncInfo certificateAuthoritySyncInfo, CancellationToken cancelToken, string logicalName)
- {
- throw new NotImplementedException();
- }
- #endregion
- }
-}
+{
+ ///
+ /// Implementation of the for Secitgo Certificate Manager. This class contains the
+ /// entry points for AnyGateway Synchronization, Revocation, and Enrollment functions.
+ ///
+ public class SectigoCAProxy : BaseCAConnector
+ {
+ ///
+ /// CAConnection section of the imported AnyGateway configuration
+ ///
+ private SectigoCAConfig Config { get; set; }
+
+ ///
+ /// API Implementation for AnyGateway to interact with the Sectigo Certificate Manager API
+ ///
+ private SectigoApiClient Client { get; set; }
+
+ ///
+ /// Method to query, parse, and return certificates to be synchronized with the AnyGateway database
+ ///
+ /// Interface to access existing certificate data from the Gateway database
+ /// A to queue certificates for synchronization
+ /// Details about previous synchronization attempts
+ ///
+ public override void Synchronize(ICertificateDataReader certificateDataReader,
+ BlockingCollection blockingBuffer,
+ CertificateAuthoritySyncInfo certificateAuthoritySyncInfo,
+ CancellationToken cancelToken)
+ {
+ Logger.MethodEntry(ILogExtensions.MethodLogLevel.Debug);
+
+ Task producerTask = null;
+ CancellationTokenSource newCancelToken = CancellationTokenSource.CreateLinkedTokenSource(cancelToken);
+
+ try
+ {
+ var certsToAdd = new BlockingCollection(100);
+ Logger.Info($"Begin Paging Certificate List");
+ int pageSize = 25;
+ if (Config.PageSize > 0)
+ {
+ pageSize = Config.PageSize;
+ }
+ producerTask = Client.CertificateListProducer(certsToAdd, newCancelToken.Token, Config.PageSize, Config.SyncFilter);
+
+ foreach (Certificate certToAdd in certsToAdd.GetConsumingEnumerable())
+ {
+ if (cancelToken.IsCancellationRequested)
+ {
+ Logger.Warn($"Task was canceled. Stopping Synchronize task.");
+ blockingBuffer.CompleteAdding();
+ break;
+ }
+
+ if (producerTask.Exception != null)
+ {
+ Logger.Error($"Synchronize task failed with the following message: {producerTask.Exception.Flatten().Message}");
+ throw producerTask.Exception.Flatten();
+ }
+
+ CAConnectorCertificate dbCert = null;
+ //serial number is blank on certs that have not been issued (awaiting approval)
+ if (!String.IsNullOrEmpty(certToAdd.SerialNumber))
+ dbCert = certificateDataReader.GetCertificateRecord(CSS.Common.DataConversion.HexToBytes(certToAdd.SerialNumber));
+
+ //are we syncing a reissued cert?
+ //Reissued certs keep the same ID, but may have different data and cause index errors on sync
+ //Removed reissued certs from enrollment, but may be some stragglers for legacy installs
+ int syncReqId = 0;
+ if (dbCert != null && dbCert.CARequestID.Contains('-'))
+ {
+ syncReqId = int.Parse(dbCert.CARequestID.Split('-')[0]);
+ }
+ else if (dbCert != null)
+ {
+ syncReqId = int.Parse(dbCert.CARequestID);
+ }
+
+ string certData = string.Empty;
+ if (dbCert != null)
+ {
+ //we found an existing cert from the DB by serial number.
+ //This should already be in the DB so no need to sync again unless status changes or
+ //admin has forced a complete sync
+ if (dbCert.Status == ConvertToKeyfactorStatus(certToAdd.status) && !Config.ForceCompleteSync)
+ {
+ Logger.Trace($"Certificate {certToAdd.CommonName} (Id: {certToAdd.Id}) already synced. Skipping.");
+ continue;
+ }
+ var statusMessage = dbCert.Status == ConvertToKeyfactorStatus(certToAdd.status) ? "not changed" : "changed";
+ var forcedMessage = Config.ForceCompleteSync ? "Complete sync forced by configuration. " : string.Empty;
+
+ Logger.Trace($"Certificate {certToAdd.CommonName} status {statusMessage}.{forcedMessage} Syncing certificate.");
+ certData = dbCert.Certificate;
+ }
+ else
+ {
+ //No certificate in the DB by SN. Need to download to get full certdata required for sync process
+ Logger.Trace($"Attempt to Pickup Certificate {certToAdd.CommonName} (ID: {certToAdd.Id})");
+ var certdataApi = Task.Run(async () => await Client.PickupCertificate(certToAdd.Id, certToAdd.CommonName)).Result;
+ if (certdataApi != null)
+ certData = Convert.ToBase64String(certdataApi.GetRawCertData());
+ }
+
+ if (certToAdd == null || String.IsNullOrEmpty(certToAdd.SerialNumber) || String.IsNullOrEmpty(certToAdd.CommonName) || String.IsNullOrEmpty(certData))
+ {
+ Logger.Debug($"Certificate Data unavailable for {certToAdd.CommonName} (ID: {certToAdd.Id}). Skipping ");
+ continue;
+ }
+
+ CAConnectorCertificate caCertToAdd = new CAConnectorCertificate
+ {
+ CARequestID = syncReqId == 0 ? certToAdd.Id.ToString() : syncReqId.ToString(),
+ ProductID = certToAdd.CertType.id.ToString(),
+ Certificate = certData,
+ Status = ConvertToKeyfactorStatus(certToAdd.status),
+ SubmissionDate = certToAdd.requested,
+ ResolutionDate = certToAdd.approved,
+ RevocationReason = ConvertToKeyfactorStatus(certToAdd.status) == 21 ? 0 : 0xffffff,
+ RevocationDate = certToAdd.revoked ?? DateTime.UtcNow,
+ };
+
+ if (blockingBuffer.TryAdd(caCertToAdd, 50, cancelToken))
+ {
+ Logger.Debug($"Added {certToAdd.CommonName} (ID:{(syncReqId == 0 ? certToAdd.Id.ToString() : syncReqId.ToString())}) to queue for synchronization");
+ }
+ else
+ {
+ Logger.Debug($"Adding {certToAdd.CommonName} to queue was blocked. Retrying");
+ }
+ }
+ Logger.Info($"Adding Certificates to Queue is Complete.");
+ blockingBuffer.CompleteAdding();
+ }
+ catch (Exception ex)
+ {
+ //gracefully exit so any certs added to queue prior to failure will still sync
+ Logger.Error($"Synchronize Task failed. {ex.Message} | {ex.StackTrace}");
+ if (producerTask != null && !producerTask.IsCompleted)
+ {
+ newCancelToken.Cancel();
+ }
+
+ blockingBuffer.CompleteAdding();
+ }
+ Logger.MethodExit(ILogExtensions.MethodLogLevel.Debug);
+ }
+
+ ///
+ /// Method to execute a single certificate sync after enrollment or revocation. Used to update status of a single record in Command
+ ///
+ /// The certificate's unique ID as defined by the syncronization/enroll methods
+ ///
+ public override CAConnectorCertificate GetSingleRecord(string caRequestID)
+ {
+ Logger.MethodEntry(ILogExtensions.MethodLogLevel.Debug);
+
+ Logger.Trace($"Get Single Certificate Detail from Sectigo (sslId: {caRequestID})");
+ int sslId = int.Parse(caRequestID.Split('-')[0]);
+
+ var singleCert = Task.Run(async () => await Client.GetCertificate(sslId)).Result;
+ Logger.Trace($"{singleCert.CommonName} ({singleCert.status}) retrieved from Sectigo.");
+
+ //Pending external validation, cannot download certificate data from API
+ if (ConvertToKeyfactorStatus(singleCert.status) == 13 || ConvertToKeyfactorStatus(singleCert.status) == 21)
+ {
+ return new CAConnectorCertificate()
+ {
+ CARequestID = caRequestID,
+ ProductID = singleCert.CertType.id.ToString(),
+ SubmissionDate = singleCert.requested,
+ ResolutionDate = singleCert.approved,
+ Status = ConvertToKeyfactorStatus(singleCert.status),
+ RevocationReason = ConvertToKeyfactorStatus(singleCert.status) == 21 ? 0 : 0xffffff,
+ RevocationDate = singleCert.revoked ?? DateTime.UtcNow
+ };
+ }
+
+ var certData = PickupSingleCert(sslId, singleCert.CommonName);
+ if (certData != null)
+ {
+ Logger.MethodExit(ILogExtensions.MethodLogLevel.Debug);
+ return new CAConnectorCertificate()
+ {
+ CARequestID = caRequestID,
+ Certificate = Convert.ToBase64String(certData.GetRawCertData()),
+ ProductID = singleCert.CertType.id.ToString(),
+ SubmissionDate = singleCert.requested,
+ ResolutionDate = singleCert.approved,
+ Status = ConvertToKeyfactorStatus(singleCert.status),
+ RevocationReason = ConvertToKeyfactorStatus(singleCert.status) == 21 ? 0 : 0xffffff,
+ RevocationDate = singleCert.revoked ?? DateTime.UtcNow
+ };
+ }
+
+ throw new Exception("Failed to download certificate data from Sectigo.");
+ }
+
+ ///
+ /// Method to execture a new, renew, or reissue enrollment request from Keyfactor Command
+ ///
+ /// Interface to access existing certificate data from the Gateway database
+ /// A base64 endocded certificate signing request in
+ /// The distingused name of the certificate being requested
+ /// All supported (dns, ipv4, ipv6, ) san entries defined during enrollment
+ /// Template details parsed from the Template configuration section
+ ///
+ ///
+ ///
+ public override EnrollmentResult Enroll(ICertificateDataReader certificateDataReader, string csr, string subject, Dictionary san, EnrollmentProductInfo productInfo, PKIConstants.X509.RequestFormat requestFormat, RequestUtilities.EnrollmentType enrollmentType)
+ {
+ Logger.MethodEntry(ILogExtensions.MethodLogLevel.Debug);
+ Logger.Info($"Begin {enrollmentType} enrollment for {subject}");
+ try
+ {
+ Logger.Debug("Parse Subject for Common Name, Organization, and Org Unit");
+
+ string commonName = ParseSubject(subject, "CN=");
+ Logger.Trace($"Common Name: {commonName}");
+
+ string orgStr = ParseSubject(subject, "O=");
+ Logger.Trace($"Organization: {orgStr}");
+
+ string ouStr = ParseSubject(subject, "OU=");
+
+ string department = productInfo.ProductParameters["Department"];
+ Logger.Trace($"Department: {department}");
+
+ var fieldList = Task.Run(async () => await Client.ListCustomFields()).Result;
+ var mandatoryFields = fieldList.CustomFields?.Where(f => f.mandatory);
+
+ Logger.Debug("Check for mandatory custom fields");
+ foreach (CustomField reqField in mandatoryFields)
+ {
+ Logger.Trace($"Checking product parameters for {reqField.name}");
+ if (!productInfo.ProductParameters.ContainsKey(reqField.name))
+ {
+ Logger.MethodExit(ILogExtensions.MethodLogLevel.Debug);
+ return new EnrollmentResult { Status = 30, StatusMessage = $"Template {productInfo.ProductID} or Enrollment Fields do not contain a mandatory custom field value for of {reqField.name}" };
+ }
+ }
+ Logger.Debug($"Search for Organization by Name {orgStr}");
+
+ int requstOrgId = 0;
+ var org = Task.Run(async () => await GetOrganizationAsync(orgStr)).Result;
+ if (org == null)
+ {
+ string err = $"Unable to find Organization by Name {orgStr} ";
+ Logger.Error($"{err}");
+ return new EnrollmentResult { Status = 30, StatusMessage = err };
+ }
+
+ Department dep = null;
+ //API returned no CertType node or it was an empty string. This changed at some point with the Sectigo API.
+ if (org.certTypes == null || org.certTypes.Count == 0)
+ {
+ Logger.Trace($"{orgStr} does not contain a valid certificate type configuration. Checking department.");
+ if (string.IsNullOrEmpty(department))
+ {
+ string err = $"No department specified, and organization {orgStr} does not contain valid certificate type configuration. Verify account and gateway configuration.";
+ Logger.Error($"{err}");
+ if (!string.IsNullOrEmpty(ouStr))
+ {
+ Logger.Error("NOTE: Organizational Unit subject field has been deprecated. Department names must now be specified in the gateway template configuration. See documentation for details.");
+ }
+ return new EnrollmentResult { Status = 30, StatusMessage = err };
+ }
+ dep = org.departments.Where(x => x.name.ToLower().Equals(department.ToLower())).FirstOrDefault();
+
+ if (dep == null)
+ {
+ string err = $"{department} does not exist as a department of {orgStr}. Please verify configuration";
+ Logger.Error($"{err}");
+ return new EnrollmentResult { Status = 30, StatusMessage = err };
+ }
+
+ Logger.Trace($"{dep} is valid. Apply {dep.id} to request");
+ requstOrgId = dep.id;
+ }
+ else
+ {
+ Logger.Trace($"{orgStr} contain a valid certificate type configuration. Apply {org.id} to request");
+ requstOrgId = org.id;
+ }
+
+ //Check if SAN matches the SUBJECT CN when multidomain = false (single domain cert).
+ //If true, we need to send empty san array. if different, join array (remove if one?)
+ bool isMultiDomain = bool.Parse(productInfo.ProductParameters["MultiDomain"]);
+ string sanList = ParseSanList(san, isMultiDomain, commonName);
+
+ var enrollmentProfile = Task.Run(async () => await GetProfile(int.Parse(productInfo.ProductID))).Result;
+ if (enrollmentProfile != null)
+ {
+ Logger.Trace($"Found {enrollmentProfile.name} profile for enroll request");
+ }
+
+ int sslId;
+ string priorSn = string.Empty;
+ Certificate newCert = null;
+
+ switch (enrollmentType)
+ {
+ case RequestUtilities.EnrollmentType.New:
+ case RequestUtilities.EnrollmentType.Reissue:
+ case RequestUtilities.EnrollmentType.Renew:
+
+ EnrollRequest request = new EnrollRequest
+ {
+ csr = csr,
+ orgId = requstOrgId,
+ term = Task.Run(async () => await GetProfileTerm(int.Parse(productInfo.ProductID))).Result,
+ certType = enrollmentProfile.id,
+ //External requestor is expected to be an email. Use config to pull the enrollment field or send blank
+ //sectigo will default to the account (API account) making the request.
+ externalRequester = GetExternalRequestor(productInfo),
+ numberServers = 1,
+ serverType = -1,
+ subjAltNames = sanList,//,
+ comments = $"CERTIFICATE_REQUESTOR: {productInfo.ProductParameters["Keyfactor-Requester"]}"//this is how the current gateway passes this data
+ };
+
+ Logger.Debug($"Submit {enrollmentType} request for {subject}");
+ sslId = Task.Run(async () => await Client.Enroll(request)).Result;
+ newCert = Task.Run(async () => await Client.GetCertificate(sslId)).Result;
+ Logger.Debug($"Enrolled for Certificate {newCert.CommonName} (ID: {newCert.Id}) | Status: {newCert.status}. Attempt to Pickup Certificate.");
+ break;
+
+ default:
+ return new EnrollmentResult { Status = 30, StatusMessage = $"Unsupported enrollment type {enrollmentType}" };
+ }
+
+ return PickUpEnrolledCertificate(newCert);
+ }
+ catch (HttpRequestException httpEx)
+ {
+ Logger.Error($"Enrollment Failed due to a HTTP error: {httpEx.Message}");
+ return new EnrollmentResult { Status = 30, StatusMessage = httpEx.Message };
+ }
+ catch (SectigoApiException apiEx)
+ {
+ Logger.Error($"Enrollment Failed due to an API error: {apiEx.Message}");
+ return new EnrollmentResult { Status = 30, StatusMessage = apiEx.Message };
+ }
+ catch (Exception ex)
+ {
+ Logger.Error($"Enrollment Failed with the following error: {ex.Message}");
+ Logger.Error($"Inner Exception Message: {ex.InnerException.Message}");
+ return new EnrollmentResult { Status = 30, StatusMessage = ex.InnerException.Message };
+ }
+ }
+
+ private string GetExternalRequestor(EnrollmentProductInfo productInfo)
+ {
+ if (!String.IsNullOrEmpty(Config.ExternalRequestorFieldName))
+ {
+ if (!String.IsNullOrEmpty(productInfo.ProductParameters[Config.ExternalRequestorFieldName]))
+ {
+ return productInfo.ProductParameters[Config.ExternalRequestorFieldName];
+ }
+ }
+ return string.Empty;
+ }
+
+ public X509Certificate2 PickupSingleCert(int sslId, string subject)
+ {
+ Logger.MethodEntry(ILogExtensions.MethodLogLevel.Debug);
+ int retryCounter = 0;
+ Thread.Sleep(5 * 1000);//small static delay as an attempt to avoid retries all together
+ while (retryCounter < Config.PickupRetries)
+ {
+ Logger.Debug($"Try number {retryCounter + 1} to pickup single certificate");
+ var certificate = Task.Run(async () => await Client.PickupCertificate(sslId, subject)).Result;
+ if (certificate != null && !String.IsNullOrEmpty(certificate.Subject))
+ {
+ Logger.Info($"Successfully picked up certificate { certificate.Subject}");
+ Logger.MethodExit(ILogExtensions.MethodLogLevel.Debug);
+ return certificate;
+ }
+ Thread.Sleep(Config.PickupDelayInSeconds * 1000);//convert seconds to ms for delay.
+ retryCounter++;
+ }
+
+ Logger.MethodExit(ILogExtensions.MethodLogLevel.Debug);
+ return null;
+ }
+
+ public EnrollmentResult PickUpEnrolledCertificate(Certificate sslCert)
+ {
+ if (sslCert.status.Equals("Issued", StringComparison.InvariantCultureIgnoreCase) ||
+ sslCert.status.Equals("Applied", StringComparison.InvariantCultureIgnoreCase))
+ {
+ return PickUpEnrolledCertificate(sslCert.Id, sslCert.CommonName);
+ }
+
+ Logger.Info($"Certificate {sslCert.CommonName} (ID: {sslCert.Id}) has not been issued. Certificate will be picked up during synchronization after approval.");
+ return new EnrollmentResult
+ {
+ CARequestID = $"{sslCert.Id}",
+ Status = (int)PKIConstants.Microsoft.RequestDisposition.EXTERNAL_VALIDATION,
+ StatusMessage = "Certificate requires approval. Certificate will be picked up during synchronization after approval."
+ };
+ }
+
+ public EnrollmentResult PickUpEnrolledCertificate(int sslId, string subject)
+ {
+ Logger.MethodEntry(ILogExtensions.MethodLogLevel.Debug);
+ int retryCounter = 0;
+ Thread.Sleep(5 * 1000);//small static delay as an attempt to avoid retries all together
+ while (retryCounter < Config.PickupRetries)
+ {
+ Logger.Debug($"Try number {retryCounter + 1} to pickup enrolled certificate");
+ var certificate = Task.Run(async () => await Client.PickupCertificate(sslId, subject)).Result;
+ if (certificate != null && !String.IsNullOrEmpty(certificate.Subject))
+ {
+ Logger.Info($"Successfully enrolled for certificate { certificate.Subject}");
+ Logger.MethodExit(ILogExtensions.MethodLogLevel.Debug);
+ return new EnrollmentResult
+ {
+ CARequestID = $"{sslId}",
+ Certificate = Convert.ToBase64String(certificate.GetRawCertData()),
+ Status = (int)PKIConstants.Microsoft.RequestDisposition.ISSUED,
+ StatusMessage = $"Successfully enrolled for certificate {certificate.Subject}"
+ };
+ }
+ Thread.Sleep(Config.PickupDelayInSeconds * 1000);//convert seconds to ms for delay.
+ retryCounter++;
+ }
+
+ Logger.MethodExit(ILogExtensions.MethodLogLevel.Debug);
+ return new EnrollmentResult
+ {
+ CARequestID = $"{sslId}",
+ Status = (int)PKIConstants.Microsoft.RequestDisposition.EXTERNAL_VALIDATION,
+ StatusMessage = "Failed to pickup certificate. Check SCM portal to determine if addtional approval is required"
+ };
+ }
+
+ ///
+ /// Method that sets up configuration class and API client. Called before each sync, revocation, or enrollment.
+ /// is the configuration that is currently saved in the AnyGateway database for the CA.
+ ///
+ ///
+ public override void Initialize(ICAConnectorConfigProvider configProvider)
+ {
+ Logger.MethodEntry(ILogExtensions.MethodLogLevel.Debug);
+
+ Config = JsonConvert.DeserializeObject(JsonConvert.SerializeObject(configProvider.CAConnectionData));
+ if (Config.PageSize > 200)
+ {
+ Config.PageSize = 200;//max value allowed by API
+ }
+
+ Client = InitializeRestClient(configProvider.CAConnectionData, Logger);
+
+ Logger.MethodExit(ILogExtensions.MethodLogLevel.Debug);
+ }
+
+ ///
+ /// Method to revoke a certificate from the Sectigo CA
+ ///
+ ///
+ ///
+ ///
+ ///
+ public override int Revoke(string caRequestID, string hexSerialNumber, uint revocationReason)
+ {
+ Logger.MethodEntry(ILogExtensions.MethodLogLevel.Debug);
+
+ var response = Task.Run(async () => await Client.RevokeSslCertificateById(int.Parse(caRequestID), RevokeReasonToString(revocationReason))).Result;
+
+ Logger.MethodExit(ILogExtensions.MethodLogLevel.Debug);
+ if (response)//will throw an exception if false
+ {
+ return 21;//revoked
+ }
+
+ return -1;
+ }
+
+ ///
+ /// Method to respond to certutil -ping command
+ ///
+ public override void Ping()
+ {
+ Logger.MethodEntry(ILogExtensions.MethodLogLevel.Debug);
+ Logger.MethodExit(ILogExtensions.MethodLogLevel.Debug);
+ }
+
+ ///
+ /// Method to validate CAConnection section of configuration during import of the configuration JSON file
+ ///
+ ///
+ public override void ValidateCAConnectionInfo(Dictionary connectionInfo)
+ {
+ Logger.MethodEntry(ILogExtensions.MethodLogLevel.Debug);
+ //determine required fields
+ //URL
+ //Auth Type (Cert or UN PASSWORD)
+ List errors = new List();
+ errors.Add(ValidateConfigurationKey(connectionInfo, Constants.API_ENDPOINT_KEY));
+ errors.Add(ValidateConfigurationKey(connectionInfo, Constants.AUTH_TYPE_KEY));
+
+ Logger.MethodExit(ILogExtensions.MethodLogLevel.Debug);
+ if (errors.Any(s => !String.IsNullOrEmpty(s)))
+ {
+ throw new Exception(String.Join("|", errors.All(s => !String.IsNullOrEmpty(s))));
+ }
+ }
+
+ ///
+ /// Method to validate product details of a template configurd in the configuration JSON file
+ ///
+ ///
+ ///
+
+ public override void ValidateProductInfo(EnrollmentProductInfo productInfo, Dictionary connectionInfo)
+ {
+ Logger.MethodEntry(ILogExtensions.MethodLogLevel.Debug);
+ SectigoApiClient localClient = InitializeRestClient(connectionInfo, Logger);
+
+ var profileList = Task.Run(async () => await localClient.ListSslProfiles()).Result;
+ if (profileList.SslProfiles.Where(p => p.id == int.Parse(productInfo.ProductID)).Count() == 0)
+ {
+ Logger.MethodExit(ILogExtensions.MethodLogLevel.Debug);
+ throw new Exception($"Unable to find SSl Profile with ID {productInfo.ProductID}");
+ }
+ Logger.MethodExit(ILogExtensions.MethodLogLevel.Debug);
+ }
+
+ private async Task GetOrganizationId(string orgName)
+ {
+ var orgList = await Client.ListOrganizations();
+ return orgList.Organizations.Where(x => x.name.ToLower().Equals(orgName.ToLower())).FirstOrDefault().id;
+ }
+
+ private async Task GetOrganizationAsync(string orgName)
+ {
+ var orgList = await Client.ListOrganizations();
+ return orgList.Organizations.Where(x => x.name.ToLower().Equals(orgName.ToLower())).FirstOrDefault();
+ }
+
+ private async Task GetProfileTerm(int profileId)
+ {
+ var profileList = await Client.ListSslProfiles();
+ return profileList.SslProfiles.Where(x => x.id == profileId).FirstOrDefault().terms[0];
+ }
+
+ private async Task GetProfile(int profileId)
+ {
+ var profileList = await Client.ListSslProfiles();
+ return profileList.SslProfiles.Where(x => x.id == profileId).FirstOrDefault();
+ }
+
+ #region Static Helpers
+
+ private static string ParseSanList(Dictionary san, bool multiDomain, string commonName)
+ {
+ string sanList = string.Empty;
+ List allSans = new List();
+ foreach (var k in san.Keys)
+ {
+ allSans.AddRange(san[k].ToList());
+ }
+
+ if (!multiDomain)
+ {
+ if (allSans.Contains(commonName) && allSans.Count() > 1)
+ {
+ List sans = allSans.ToList();
+ sans.Remove(commonName);
+ sanList = string.Join(",", sans.ToArray());
+ }
+ }
+ else
+ {
+ if (allSans.Contains(commonName))
+ {
+ List sans = allSans.ToList();
+ sans.Remove(commonName);
+ sanList = string.Join(",", sans.ToArray());
+ }
+ else
+ {
+ List sans = allSans.ToList();
+ sanList = string.Join(",", sans.ToArray());
+ }
+ }
+ return sanList;
+ }
+
+ private static string ParseSubject(string subject, string rdn)
+ {
+ string escapedSubject = subject.Replace("\\,", "|");
+ string rdnString = escapedSubject.Split(',').ToList().Where(x => x.Contains(rdn)).FirstOrDefault();
+
+ if (!string.IsNullOrEmpty(rdnString))
+ {
+ return rdnString.Replace(rdn, "").Replace("|", ",").Trim();
+ }
+ else
+ {
+ throw new Exception($"The request is missing a {rdn} value");
+ }
+ }
+
+ ///
+ /// Ensure the key is configured in the CAConnectionDetail section
+ ///
+ ///
+ ///
+ ///
+ private static string ValidateConfigurationKey(Dictionary connectionInfo, string key)
+ {
+ if (!connectionInfo.TryGetValue(key, out object tempValue) && tempValue != null)
+ {
+ return $"{key} is a required configuration value";
+ }
+
+ return string.Empty;
+ }
+
+ private static int ConvertToKeyfactorStatus(string status)
+ {
+ switch (status.ToUpper())
+ {
+ case "ISSUED":
+ case "ENROLLED - PENDING DOWNLOAD":
+ case "APPROVED":
+ case "APPLIED":
+ case "DOWNLOADED":
+ return 20;
+
+ case "REQUESTED":
+ case "AWAITING APPROVAL":
+ case "NOT ENROLLED":
+ return 13;
+
+ case "REVOKED":
+ return 21;
+
+ case "ANY":
+ default:
+ return (int)CSS.PKI.PKIConstants.Microsoft.RequestDisposition.UNKNOWN;//unknown
+ }
+ }
+
+ public static string RevokeReasonToString(UInt32 revokeType)
+ {
+ switch (revokeType)
+ {
+ case 1:
+ return "Compromised Key";
+
+ case 2:
+ return "CA Compromised";
+
+ case 3:
+ return "Affiliation Changed";
+
+ case 4:
+ return "Superseded";
+
+ case 5:
+ return "Cessation of Operation";
+
+ case 6:
+ return "Certificate Hold";
+
+ default:
+ return "Unspecified";
+ }
+ }
+
+ public static int RevokeStringToCode(string revokePhrase)
+ {
+ switch (revokePhrase.ToLower())
+ {
+ case "compromised key":
+ return 1;
+
+ case "ca compromised":
+ return 2;
+
+ case "affiliation changed":
+ return 3;
+
+ case "superseded":
+ return 4;
+
+ case "cessation of operation":
+ return 5;
+
+ case "certificate hold":
+ return 6;
+
+ default:
+ return 0;
+ }
+ }
+
+ private static SectigoApiClient InitializeRestClient(Dictionary connectionInfo, ILog logger)
+ {
+ logger.MethodEntry(ILogExtensions.MethodLogLevel.Debug);
+ SectigoCAConfig localConfig = JsonConvert.DeserializeObject(JsonConvert.SerializeObject(connectionInfo));
+ WebRequestHandler webRequestHandler = new WebRequestHandler();
+
+ if (localConfig.AuthenticationType.ToLower() == "certificate")
+ {
+ webRequestHandler.ClientCertificateOptions = ClientCertificateOption.Manual;
+ X509Certificate2 authCert = GetClientCertificate(connectionInfo, logger);
+ if (authCert == null)
+ {
+ logger.MethodExit(ILogExtensions.MethodLogLevel.Debug);
+ throw new Exception("AuthType set to Certificate, but no certificate found!");
+ }
+
+ webRequestHandler.ClientCertificates.Add(authCert);
+ }
+
+ HttpClient restClient = new HttpClient(webRequestHandler)
+ {
+ BaseAddress = new Uri(localConfig.ApiEndpoint)
+ };
+
+ restClient.DefaultRequestHeaders.Add(Constants.CUSTOMER_URI_KEY, localConfig.CustomerUri);
+ restClient.DefaultRequestHeaders.Add(Constants.CUSTOMER_LOGIN_KEY, localConfig.Username);
+ //Determine
+
+ if (localConfig.AuthenticationType.ToLower() == "password")
+ {
+ restClient.DefaultRequestHeaders.Add(Constants.CUSTOMER_PASSWORD_KEY, localConfig.Password);
+ }
+
+ logger.MethodExit(ILogExtensions.MethodLogLevel.Debug);
+ return new SectigoApiClient(restClient);
+ }
+
+ private static X509Certificate2 GetClientCertificate(Dictionary config, ILog logger)
+ {
+ logger.MethodEntry(ILogExtensions.MethodLogLevel.Debug);
+ Dictionary caConnectionCertificateDetail = config["ClientCertificate"] as Dictionary;
+
+ StoreName sn;
+ StoreLocation sl;
+ string thumbprint = (string)caConnectionCertificateDetail["Thumbprint"];
+
+ if (String.IsNullOrEmpty(thumbprint) ||
+ !Enum.TryParse((string)caConnectionCertificateDetail["StoreName"], out sn) ||
+ !Enum.TryParse((string)caConnectionCertificateDetail["StoreLocation"], out sl))
+ {
+ throw new Exception("Unable to find client authentication certificate");
+ }
+
+ X509Certificate2Collection foundCerts;
+ using (X509Store currentStore = new X509Store(sn, sl))
+ {
+ logger.Trace($"Search for client auth certificates with Thumprint {thumbprint} in the {sn}{sl} certificate store");
+
+ currentStore.Open(OpenFlags.ReadOnly | OpenFlags.OpenExistingOnly);
+ foundCerts = currentStore.Certificates.Find(X509FindType.FindByThumbprint, thumbprint, true);
+ logger.Trace($"Found {foundCerts.Count} certificates in the {currentStore.Name} store");
+ currentStore.Close();
+ }
+ if (foundCerts.Count > 1)
+ {
+ throw new Exception($"Multiple certificates with Thumprint {thumbprint} found in the {sn}{sl} certificate store");
+ }
+ logger.MethodExit(ILogExtensions.MethodLogLevel.Debug);
+ if (foundCerts.Count > 0)
+ return foundCerts[0];
+
+ return null;
+ }
+
+ #endregion Static Helpers
+
+ #region Obsolete Methods
+
+ [Obsolete]
+ public override EnrollmentResult Enroll(string csr, string subject, Dictionary san, EnrollmentProductInfo productInfo, CSS.PKI.PKIConstants.X509.RequestFormat requestFormat, RequestUtilities.EnrollmentType enrollmentType)
+ {
+ throw new NotImplementedException();
+ }
+
+ [Obsolete]
+ public override void Synchronize(ICertificateDataReader certificateDataReader, BlockingCollection blockingBuffer, CertificateAuthoritySyncInfo certificateAuthoritySyncInfo, CancellationToken cancelToken, string logicalName)
+ {
+ throw new NotImplementedException();
+ }
+
+ #endregion Obsolete Methods
+ }
+}
\ No newline at end of file