From 62a8b24228821a3b74c28ec59caeb47f9caaef7a Mon Sep 17 00:00:00 2001
From: David Galey
Date: Tue, 23 Jun 2026 09:47:10 -0400
Subject: [PATCH 01/15] Initial commit
---
globalsign-atlas-caplugin.sln | 25 +
.../APIProxy/AtlasBaseCalls.cs | 27 +
.../APIProxy/Certificate.cs | 40 ++
globalsign-atlas-caplugin/APIProxy/Enroll.cs | 156 +++++
globalsign-atlas-caplugin/APIProxy/Login.cs | 25 +
.../APIProxy/ValidationPolicy.cs | 238 +++++++
globalsign-atlas-caplugin/AtlasCAPlugin.cs | 435 ++++++++++++
globalsign-atlas-caplugin/AtlasConfig.cs | 29 +
.../Client/AtlasClient.cs | 620 ++++++++++++++++++
.../globalsign-atlas-caplugin.csproj | 17 +
10 files changed, 1612 insertions(+)
create mode 100644 globalsign-atlas-caplugin.sln
create mode 100644 globalsign-atlas-caplugin/APIProxy/AtlasBaseCalls.cs
create mode 100644 globalsign-atlas-caplugin/APIProxy/Certificate.cs
create mode 100644 globalsign-atlas-caplugin/APIProxy/Enroll.cs
create mode 100644 globalsign-atlas-caplugin/APIProxy/Login.cs
create mode 100644 globalsign-atlas-caplugin/APIProxy/ValidationPolicy.cs
create mode 100644 globalsign-atlas-caplugin/AtlasCAPlugin.cs
create mode 100644 globalsign-atlas-caplugin/AtlasConfig.cs
create mode 100644 globalsign-atlas-caplugin/Client/AtlasClient.cs
create mode 100644 globalsign-atlas-caplugin/globalsign-atlas-caplugin.csproj
diff --git a/globalsign-atlas-caplugin.sln b/globalsign-atlas-caplugin.sln
new file mode 100644
index 0000000..e86e735
--- /dev/null
+++ b/globalsign-atlas-caplugin.sln
@@ -0,0 +1,25 @@
+
+Microsoft Visual Studio Solution File, Format Version 12.00
+# Visual Studio Version 17
+VisualStudioVersion = 17.14.36705.20 d17.14
+MinimumVisualStudioVersion = 10.0.40219.1
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "globalsign-atlas-caplugin", "globalsign-atlas-caplugin\globalsign-atlas-caplugin.csproj", "{292B7A34-CFEC-4ABB-8BE6-31F87BC1651B}"
+EndProject
+Global
+ GlobalSection(SolutionConfigurationPlatforms) = preSolution
+ Debug|Any CPU = Debug|Any CPU
+ Release|Any CPU = Release|Any CPU
+ EndGlobalSection
+ GlobalSection(ProjectConfigurationPlatforms) = postSolution
+ {292B7A34-CFEC-4ABB-8BE6-31F87BC1651B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {292B7A34-CFEC-4ABB-8BE6-31F87BC1651B}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {292B7A34-CFEC-4ABB-8BE6-31F87BC1651B}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {292B7A34-CFEC-4ABB-8BE6-31F87BC1651B}.Release|Any CPU.Build.0 = Release|Any CPU
+ EndGlobalSection
+ GlobalSection(SolutionProperties) = preSolution
+ HideSolutionNode = FALSE
+ EndGlobalSection
+ GlobalSection(ExtensibilityGlobals) = postSolution
+ SolutionGuid = {D966B726-B709-498F-97E5-D53F6F7495A6}
+ EndGlobalSection
+EndGlobal
diff --git a/globalsign-atlas-caplugin/APIProxy/AtlasBaseCalls.cs b/globalsign-atlas-caplugin/APIProxy/AtlasBaseCalls.cs
new file mode 100644
index 0000000..90e305c
--- /dev/null
+++ b/globalsign-atlas-caplugin/APIProxy/AtlasBaseCalls.cs
@@ -0,0 +1,27 @@
+using Newtonsoft.Json;
+
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace Keyfactor.Extensions.CAPlugin.GlobalSign.Atlas.APIProxy
+{
+ public abstract class AtlasBaseRequest
+ {
+ [JsonIgnore]
+ public string Resource { get; internal set; }
+
+ [JsonIgnore]
+ public string Method { get; internal set; }
+
+ [JsonIgnore]
+ public string TargetURI { get; set; }
+
+ public string BuildParameters()
+ {
+ return "";
+ }
+ }
+}
diff --git a/globalsign-atlas-caplugin/APIProxy/Certificate.cs b/globalsign-atlas-caplugin/APIProxy/Certificate.cs
new file mode 100644
index 0000000..e5f5465
--- /dev/null
+++ b/globalsign-atlas-caplugin/APIProxy/Certificate.cs
@@ -0,0 +1,40 @@
+using Newtonsoft.Json;
+
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace Keyfactor.Extensions.CAPlugin.GlobalSign.Atlas.APIProxy
+{
+ public class CertificateResponse
+ {
+ [JsonProperty("certificate")]
+ public string Certificate { get; set; }
+
+ [JsonProperty("status")]
+ public string Status { get; set; }
+
+ [JsonProperty("description")]
+ public string Description { get; set; }
+ }
+
+ public class CertificateStatusResponse
+ {
+ [JsonProperty("not_after")]
+ public long NotAfter { get; set; }
+
+ [JsonProperty("not_before")]
+ public long NotBefore { get; set; }
+
+ [JsonProperty("serial_number")]
+ public string SerialNumber { get; set; }
+ }
+
+ public class CertificateDetailsResponse
+ {
+ public CertificateResponse Cert { get; set; }
+ public CertificateStatusResponse Status { get; set; }
+ }
+}
diff --git a/globalsign-atlas-caplugin/APIProxy/Enroll.cs b/globalsign-atlas-caplugin/APIProxy/Enroll.cs
new file mode 100644
index 0000000..031c3c7
--- /dev/null
+++ b/globalsign-atlas-caplugin/APIProxy/Enroll.cs
@@ -0,0 +1,156 @@
+using Keyfactor.PKI;
+
+using Newtonsoft.Json;
+
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace Keyfactor.Extensions.CAPlugin.GlobalSign.Atlas.APIProxy
+{
+ public class EnrollRequest : AtlasBaseRequest
+ {
+ public EnrollRequest()
+ {
+ this.Resource = "certificates";
+ this.Method = "POST";
+ }
+ }
+
+ public class EnrollResponse
+ {
+ public string SerialNumber { get; set; }
+ public string Cert { get; set; }
+ public PKI.Enums.EJBCA.EndEntityStatus Status { get; set; }
+ public string StatusMessage { get; set; }
+ }
+
+ public class Enroll
+ {
+ [JsonProperty("validity")]
+ public ValidityDates Validity { get; set; }
+
+ [JsonProperty("subject_dn")]
+ public Subject SubjectDN { get; set; }
+
+ [JsonProperty("san")]
+ public AlternateNames SANs { get; set; }
+
+ [JsonProperty("extended_key_usages")]
+ public string[] EKUs
+ { get { return EKUList.ToArray(); } }
+
+ [JsonIgnore]
+ public List EKUList { get; set; }
+
+ [JsonProperty("public_key")]
+ public string CSR { get; set; }
+
+ [JsonProperty("signature")]
+ public Signature Sig { get; set; }
+
+ public Enroll()
+ {
+ Validity = new ValidityDates();
+ SubjectDN = new Subject();
+ SANs = new AlternateNames();
+ Sig = new Signature();
+ EKUList = new List();
+ }
+ }
+
+ public class ValidityDates
+ {
+ [JsonProperty("not_before")]
+ public long NotBeforeInt
+ {
+ get
+ {
+ return ((DateTimeOffset)NotBefore).ToUnixTimeSeconds();
+ }
+ }
+
+ [JsonProperty("not_after")]
+ public long NotAfterInt
+ {
+ get
+ {
+ return ((DateTimeOffset)NotAfter).ToUnixTimeSeconds();
+ }
+ }
+
+ [JsonIgnore]
+ public DateTime NotBefore { get; set; }
+
+ [JsonIgnore]
+ public DateTime NotAfter { get; set; }
+ }
+
+ public class Subject
+ {
+ [JsonProperty("common_name")]
+ public string CommonName { get; set; }
+
+ [JsonProperty("country")]
+ public string Country { get; set; }
+
+ [JsonProperty("state")]
+ public string State { get; set; }
+
+ [JsonProperty("locality")]
+ public string Locality { get; set; }
+
+ [JsonProperty("organization")]
+ public string Organization { get; set; }
+
+ [JsonProperty("email")]
+ public string Email { get; set; }
+ }
+
+ public class AlternateNames
+ {
+ [JsonProperty("dns_names")]
+ public string[] DNS
+ { get { return DNSList.ToArray(); } }
+
+ [JsonIgnore]
+ public List DNSList { get; set; }
+
+ [JsonProperty("emails")]
+ public string[] Emails
+ { get { return EmailList.ToArray(); } }
+
+ [JsonIgnore]
+ public List EmailList { get; set; }
+
+ [JsonProperty("ip_addresses")]
+ public string[] IPs
+ { get { return IPList.ToArray(); } }
+
+ [JsonIgnore]
+ public List IPList { get; set; }
+
+ [JsonProperty("uris")]
+ public string[] URIs
+ { get { return URIList.ToArray(); } }
+
+ [JsonIgnore]
+ public List URIList { get; set; }
+
+ public AlternateNames()
+ {
+ DNSList = new List();
+ EmailList = new List();
+ IPList = new List();
+ URIList = new List();
+ }
+ }
+
+ public class Signature
+ {
+ [JsonProperty("hash_algorithm")]
+ public string HashAlgorithm { get; set; }
+ }
+}
diff --git a/globalsign-atlas-caplugin/APIProxy/Login.cs b/globalsign-atlas-caplugin/APIProxy/Login.cs
new file mode 100644
index 0000000..8b1bb6c
--- /dev/null
+++ b/globalsign-atlas-caplugin/APIProxy/Login.cs
@@ -0,0 +1,25 @@
+using Newtonsoft.Json;
+
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace Keyfactor.Extensions.CAPlugin.GlobalSign.Atlas.APIProxy
+{
+ public class LoginRequest
+ {
+ [JsonProperty("api_key")]
+ public string Key { get; set; }
+
+ [JsonProperty("api_secret")]
+ public string Secret { get; set; }
+ }
+
+ public class LoginResponse
+ {
+ [JsonProperty("access_token")]
+ public string Token { get; set; }
+ }
+}
diff --git a/globalsign-atlas-caplugin/APIProxy/ValidationPolicy.cs b/globalsign-atlas-caplugin/APIProxy/ValidationPolicy.cs
new file mode 100644
index 0000000..53e9bf5
--- /dev/null
+++ b/globalsign-atlas-caplugin/APIProxy/ValidationPolicy.cs
@@ -0,0 +1,238 @@
+using Newtonsoft.Json;
+
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace Keyfactor.Extensions.CAPlugin.GlobalSign.Atlas.APIProxy
+{
+ public class ValidationPolicyResponse
+ {
+ [JsonProperty("validity")]
+ public ValidityResponse Validity { get; set; }
+
+ [JsonProperty("subject_dn")]
+ public SubjectDNResponse SubjectDN { get; set; }
+
+ [JsonProperty("san")]
+ public SANResponse San { get; set; }
+
+ [JsonProperty("key_usages")]
+ public KeyUsageResponse KeyUsages { get; set; }
+
+ [JsonProperty("extended_key_usages")]
+ public EKUResponse EKUs { get; set; }
+
+ [JsonProperty("signature")]
+ public SignatureResponse Signature { get; set; }
+
+ [JsonProperty("public_key")]
+ public PublicKeyResponse PublicKey { get; set; }
+
+ [JsonProperty("public_key_signature")]
+ public string PublicKeySignature { get; set; }
+ }
+
+ public class ValidityResponse
+ {
+ [JsonProperty("secondsmin")]
+ public long SecondsMin { get; set; }
+
+ [JsonProperty("secondsmax")]
+ public long SecondsMax { get; set; }
+
+ [JsonProperty("not_before_negative_skew")]
+ public long NotBeforeNegativeSkew { get; set; }
+
+ [JsonProperty("not_before_positive_skew")]
+ public long NotBeforePositiveSkew { get; set; }
+ }
+
+ public class SubjectDNResponse
+ {
+ [JsonProperty("common_name")]
+ public SubjectPartResponse CommonName { get; set; }
+
+ [JsonProperty("given_name")]
+ public SubjectPartResponse GivenName { get; set; }
+
+ [JsonProperty("surname")]
+ public SubjectPartResponse Surname { get; set; }
+
+ [JsonProperty("organization")]
+ public SubjectPartResponse Organization { get; set; }
+
+ [JsonProperty("organization_identifier")]
+ public SubjectPartResponse OrganizationIdentifier { get; set; }
+
+ [JsonProperty("organizational_unit")]
+ public OUResponse OrganizationalUnit { get; set; }
+
+ [JsonProperty("country")]
+ public SubjectPartResponse Country { get; set; }
+
+ [JsonProperty("state")]
+ public SubjectPartResponse State { get; set; }
+
+ [JsonProperty("locality")]
+ public SubjectPartResponse Locality { get; set; }
+
+ [JsonProperty("postal_code")]
+ public SubjectPartResponse PostalCode { get; set; }
+
+ [JsonProperty("street_address")]
+ public SubjectPartResponse StreetAddress { get; set; }
+
+ [JsonProperty("email")]
+ public SubjectPartResponse Email { get; set; }
+
+ [JsonProperty("jurisdiction_of_incorporation_locality_name")]
+ public SubjectPartResponse IncorporationLocality { get; set; }
+
+ [JsonProperty("jurisdiction_of_incorporation_state_or_province_name")]
+ public SubjectPartResponse IncorporationState { get; set; }
+
+ [JsonProperty("jurisdiction_of_incorporation_country_name")]
+ public SubjectPartResponse IncorporationCountry { get; set; }
+
+ [JsonProperty("business_category")]
+ public SubjectPartResponse BusinessCategory { get; set; }
+
+ [JsonProperty("serial_number")]
+ public SubjectPartResponse SerialNumber { get; set; }
+ }
+
+ public class SubjectPartResponse
+ {
+ [JsonProperty("presence")]
+ public string Presence { get; set; }
+
+ [JsonProperty("format")]
+ public string Format { get; set; }
+
+ [JsonProperty("ignore_empty")]
+ public bool IgnoreEmpty { get; set; }
+ }
+
+ public class OUResponse
+ {
+ [JsonProperty("static")]
+ public bool Static { get; set; }
+
+ [JsonProperty("list")]
+ public List List { get; set; }
+
+ [JsonProperty("mincount")]
+ public int MinCount { get; set; }
+
+ [JsonProperty("maxcount")]
+ public int MaxCount { get; set; }
+
+ [JsonProperty("ignore_empty")]
+ public bool IgnoreEmpty { get; set; }
+ }
+
+ public class SANResponse
+ {
+ [JsonProperty("critical")]
+ public bool Critical { get; set; }
+
+ [JsonProperty("dns_names")]
+ public ListTypeResponse DNSNames { get; set; }
+
+ [JsonProperty("emails")]
+ public ListTypeResponse Emails { get; set; }
+
+ [JsonProperty("ip_addresses")]
+ public ListTypeResponse IPAddresses { get; set; }
+
+ [JsonProperty("uris")]
+ public ListTypeResponse URIs { get; set; }
+ }
+
+ public class ListTypeResponse
+ {
+ [JsonProperty("static")]
+ public bool Static { get; set; }
+
+ [JsonProperty("list")]
+ public List List { get; set; }
+
+ [JsonProperty("mincount")]
+ public int MinCount { get; set; }
+
+ [JsonProperty("maxcount")]
+ public int MaxCount { get; set; }
+ }
+
+ public class KeyUsageResponse
+ {
+ [JsonProperty("digital_signature")]
+ public string DigitalSignature { get; set; }
+
+ [JsonProperty("content_commitment")]
+ public string ContentCommitment { get; set; }
+
+ [JsonProperty("key_encipherment")]
+ public string KeyEncipherment { get; set; }
+
+ [JsonProperty("data_encipherment")]
+ public string DataEncipherment { get; set; }
+
+ [JsonProperty("key_agreement")]
+ public string KeyAgreement { get; set; }
+
+ [JsonProperty("key_certificate_sign")]
+ public string KeyCertificateSign { get; set; }
+
+ [JsonProperty("crl_sign")]
+ public string CRLSign { get; set; }
+
+ [JsonProperty("encipher_only")]
+ public string EncipherOnly { get; set; }
+
+ [JsonProperty("decipher_only")]
+ public string DecipherOnly { get; set; }
+ }
+
+ public class EKUResponse
+ {
+ [JsonProperty("critical")]
+ public bool Critical { get; set; }
+
+ [JsonProperty("ekus")]
+ public ListTypeResponse EKUs { get; set; }
+ }
+
+ public class SignatureResponse
+ {
+ [JsonProperty("algorithm")]
+ public AlgorithmResponse Algorithm { get; set; }
+
+ [JsonProperty("hash_algorithm")]
+ public AlgorithmResponse HashAlgorithm { get; set; }
+ }
+
+ public class AlgorithmResponse
+ {
+ [JsonProperty("presence")]
+ public string Presence { get; set; }
+
+ [JsonProperty("list")]
+ public List List { get; set; }
+ }
+
+ public class PublicKeyResponse
+ {
+ [JsonProperty("key_type")]
+ public string KeyType { get; set; }
+
+ [JsonProperty("allowed_lengths")]
+ public List AllowedLengths { get; set; }
+
+ [JsonProperty("key_format")]
+ public string KeyFormat { get; set; }
+ }
+}
diff --git a/globalsign-atlas-caplugin/AtlasCAPlugin.cs b/globalsign-atlas-caplugin/AtlasCAPlugin.cs
new file mode 100644
index 0000000..1c4217c
--- /dev/null
+++ b/globalsign-atlas-caplugin/AtlasCAPlugin.cs
@@ -0,0 +1,435 @@
+using Keyfactor.AnyGateway.Extensions;
+using Keyfactor.Extensions.CAPlugin.GlobalSign.Atlas.APIProxy;
+using Keyfactor.Extensions.CAPlugin.GlobalSign.Atlas.Client;
+using Keyfactor.Logging;
+using Keyfactor.PKI.Enums.EJBCA;
+using Keyfactor.PKI.PEM;
+
+using Microsoft.Extensions.Logging;
+
+using Newtonsoft.Json;
+
+using Org.BouncyCastle.Asn1.Cmp;
+using Org.BouncyCastle.Asn1.X509;
+
+using System.Collections.Concurrent;
+using System.Net.Http.Headers;
+using System.Security.Cryptography.X509Certificates;
+
+using static Org.BouncyCastle.Math.EC.ECCurve;
+
+namespace Keyfactor.Extensions.CAPlugin.GlobalSign.Atlas
+{
+ public class AtlasCAPlugin : IAnyCAPlugin
+ {
+ private AtlasConfig _config;
+ private readonly ILogger _logger;
+ private ICertificateDataReader _certDataReader;
+ private ICertificateResolver _certResolver;
+
+ public AtlasCAPlugin(ICertificateResolver certificateResolver = null)
+ {
+ _logger = LogHandler.GetClassLogger();
+ _certResolver = certificateResolver;
+ }
+
+ public void Initialize(IAnyCAPluginConfigProvider configProvider, ICertificateDataReader certificateDataReader)
+ {
+ _certDataReader = certificateDataReader;
+ string rawConfig = JsonConvert.SerializeObject(configProvider.CAConnectionData);
+ _config = JsonConvert.DeserializeObject(rawConfig);
+ }
+
+ public async Task Enroll(string csr, string subject, Dictionary san, EnrollmentProductInfo productInfo, RequestFormat requestFormat, EnrollmentType enrollmentType)
+ {
+ _logger.MethodEntry(LogLevel.Trace);
+ AtlasClient client = AtlasClient.InitializeClient(_config, _certResolver);
+ Enroll enrollData = new Enroll();
+ csr = PemUtilities.DERToPEM(Convert.FromBase64String(csr), PemUtilities.PemObjectType.CertRequest);
+ enrollData.CSR = csr;
+
+ var validation = client.GetValidationPolicy();
+ long days = 0;
+ if (productInfo.ProductParameters.ContainsKey("Lifetime"))
+ {
+ days = int.Parse(productInfo.ProductParameters["Lifetime"]);
+ _logger.LogTrace($"Verifying provided validity period of {days} days");
+ }
+ else
+ {
+ days = validation.Validity.SecondsMax / 60 / 60 / 24;
+ _logger.LogTrace($"No validity period provided. Using default maximum of {days} days");
+ }
+ long validitySeconds = days * 24 * 60 * 60;
+ if (validitySeconds > validation.Validity.SecondsMax || validitySeconds < validation.Validity.SecondsMin)
+ {
+ int minDays = Convert.ToInt32(Math.Ceiling(validation.Validity.SecondsMin / 60.0 / 60.0 / 24.0));
+ int maxDays = Convert.ToInt32(Math.Floor(validation.Validity.SecondsMax / 60.0 / 60.0 / 24.0));
+ string errMsg = $"Invalid validity period. Valid period is between {minDays} and {maxDays} days.";
+ _logger.LogError(errMsg);
+ throw new Exception(errMsg);
+ }
+ enrollData.Validity.NotBefore = DateTime.UtcNow;
+ enrollData.Validity.NotAfter = enrollData.Validity.NotBefore.AddDays(days);
+
+ X509Name subjectParsed = new X509Name(subject);
+ string subjectField, validationPresence;
+ subjectField = subjectParsed.GetValueList(X509Name.CN).Cast().LastOrDefault();
+ validationPresence = validation.SubjectDN.CommonName.Presence;
+ if (validationPresence.Equals("required", StringComparison.OrdinalIgnoreCase) || validationPresence.Equals("optional", StringComparison.OrdinalIgnoreCase))
+ {
+ if (validationPresence.Equals("required", StringComparison.OrdinalIgnoreCase) && subjectField == null)
+ {
+ _logger.LogError($"Common Name is required");
+ throw new Exception("Common Name is required");
+ }
+ enrollData.SubjectDN.CommonName = subjectField;
+ }
+ else if (subjectField != null)
+ {
+ _logger.LogWarning($"Validation Policy does not allow Common Name, skipping");
+ }
+ subjectField = subjectParsed.GetValueList(X509Name.C).Cast().LastOrDefault();
+ validationPresence = validation.SubjectDN.Country.Presence;
+ if (validationPresence.Equals("required", StringComparison.OrdinalIgnoreCase) || validationPresence.Equals("optional", StringComparison.OrdinalIgnoreCase))
+ {
+ if (validationPresence.Equals("required", StringComparison.OrdinalIgnoreCase) && subjectField == null)
+ {
+ _logger.LogError($"Country is required");
+ throw new Exception("Country is required");
+ }
+ enrollData.SubjectDN.Country = subjectField;
+ }
+ else if (subjectField != null)
+ {
+ _logger.LogWarning($"Validation Policy does not allow Country, skipping");
+ }
+ subjectField = subjectParsed.GetValueList(X509Name.E).Cast().LastOrDefault();
+ validationPresence = validation.SubjectDN.Email.Presence;
+ if (validationPresence.Equals("required", StringComparison.OrdinalIgnoreCase) || validationPresence.Equals("optional", StringComparison.OrdinalIgnoreCase))
+ {
+ if (validationPresence.Equals("required", StringComparison.OrdinalIgnoreCase) && subjectField == null)
+ {
+ _logger.LogError($"Email is required");
+ throw new Exception("Email is required");
+ }
+ enrollData.SubjectDN.Email = subjectField;
+ }
+ else if (subjectField != null)
+ {
+ _logger.LogWarning($"Validation Policy does not allow Email, skipping");
+ }
+ subjectField = subjectParsed.GetValueList(X509Name.L).Cast().LastOrDefault();
+ validationPresence = validation.SubjectDN.Locality.Presence;
+ if (validationPresence.Equals("required", StringComparison.OrdinalIgnoreCase) || validationPresence.Equals("optional", StringComparison.OrdinalIgnoreCase))
+ {
+ if (validationPresence.Equals("required", StringComparison.OrdinalIgnoreCase) && subjectField == null)
+ {
+ _logger.LogError($"Locality is required");
+ throw new Exception("Locality is required");
+ }
+ enrollData.SubjectDN.Locality = subjectField;
+ }
+ else if (subjectField != null)
+ {
+ _logger.LogWarning($"Validation Policy does not allow Locality, skipping");
+ }
+ subjectField = subjectParsed.GetValueList(X509Name.O).Cast().LastOrDefault();
+ validationPresence = validation.SubjectDN.Organization.Presence;
+ if (validationPresence.Equals("required", StringComparison.OrdinalIgnoreCase) || validationPresence.Equals("optional", StringComparison.OrdinalIgnoreCase))
+ {
+ if (validationPresence.Equals("required", StringComparison.OrdinalIgnoreCase) && subjectField == null)
+ {
+ _logger.LogError($"Organization is required");
+ throw new Exception("Organization is required");
+ }
+ enrollData.SubjectDN.Organization = subjectField;
+ }
+ else if (subjectField != null)
+ {
+ _logger.LogWarning($"Validation Policy does not allow Organization, skipping");
+ }
+ subjectField = subjectParsed.GetValueList(X509Name.ST).Cast().LastOrDefault();
+ validationPresence = validation.SubjectDN.State.Presence;
+ if (validationPresence.Equals("required", StringComparison.OrdinalIgnoreCase) || validationPresence.Equals("optional", StringComparison.OrdinalIgnoreCase))
+ {
+ if (validationPresence.Equals("required", StringComparison.OrdinalIgnoreCase) && subjectField == null)
+ {
+ _logger.LogError($"State is required");
+ throw new Exception("State is required");
+ }
+ enrollData.SubjectDN.State = subjectField;
+ }
+ else if (subjectField != null)
+ {
+ _logger.LogWarning($"Validation Policy does not allow State, skipping");
+ }
+
+ var sanDict = new Dictionary(san, StringComparer.OrdinalIgnoreCase);
+ if (!validation.San.DNSNames.Static)
+ {
+ if (sanDict.ContainsKey("dns"))
+ foreach (var dnsSan in sanDict["dns"])
+ enrollData.SANs.DNSList.Add(dnsSan);
+ }
+ else if (sanDict.ContainsKey("dns"))
+ {
+ _logger.LogWarning($"Validation Policy does not allow DNS SANs, skipping");
+ }
+
+ if (!validation.San.IPAddresses.Static)
+ {
+ if (sanDict.ContainsKey("ipaddress"))
+ foreach (var ipSan in sanDict["ipaddress"])
+ enrollData.SANs.IPList.Add(ipSan);
+ }
+ else if (sanDict.ContainsKey("ipaddress"))
+ {
+ _logger.LogWarning($"Validation Policy does not allow IP address SANs, skipping");
+ }
+
+ if (!validation.San.Emails.Static)
+ {
+ if (sanDict.ContainsKey("email"))
+ foreach (var emailSan in sanDict["email"])
+ enrollData.SANs.EmailList.Add(emailSan);
+ }
+ else if (sanDict.ContainsKey("email"))
+ {
+ _logger.LogWarning($"Validation Policy does not allow email SANs, skipping");
+ }
+
+ string keyUsage = ((productInfo.ProductParameters.ContainsKey("KeyUsage")) ? productInfo.ProductParameters["KeyUsage"] : "").ToLower();
+ if (!validation.EKUs.EKUs.Static)
+ {
+ if (string.IsNullOrEmpty(keyUsage))
+ {
+ keyUsage = "clientserver";
+ }
+ if (keyUsage.Contains("server"))
+ {
+ enrollData.EKUList.Add("1.3.6.1.5.5.7.3.1");
+ }
+ if (keyUsage.Contains("client"))
+ {
+ enrollData.EKUList.Add("1.3.6.1.5.5.7.3.2");
+ }
+ }
+ else if (!string.IsNullOrEmpty(keyUsage))
+ {
+ _logger.LogWarning($"Validation Policy does not allow EKUs, skipping");
+ }
+
+ if (validation.Signature.HashAlgorithm.Presence.Equals("required", StringComparison.OrdinalIgnoreCase) || validation.Signature.HashAlgorithm.Presence.Equals("optional", StringComparison.OrdinalIgnoreCase))
+ {
+ enrollData.Sig.HashAlgorithm = "SHA-256";
+ }
+
+ var response = client.RequestNewCertificate(enrollData, 5, 5);
+
+ if (response.Status != EndEntityStatus.GENERATED && response.Status != EndEntityStatus.INPROCESS)
+ {
+ throw new Exception($"Certificate was not retrieved. Status: {response.StatusMessage}");
+ }
+
+ EnrollmentResult result = new EnrollmentResult();
+ result.CARequestID = response.SerialNumber;
+ if (response.Status == EndEntityStatus.GENERATED)
+ {
+ result.Status = (int)EndEntityStatus.GENERATED;
+ result.StatusMessage = string.Format(response.StatusMessage, subject);
+ result.Certificate = Convert.ToBase64String(PemUtilities.PEMToDER(response.Cert));
+ }
+ else
+ {
+ result.Status = (int)EndEntityStatus.EXTERNALVALIDATION;
+ result.StatusMessage = $"Certificate request is pending or requires approval. Certificate will be picked up during synchronization once available."; ;
+ }
+ return result;
+ }
+
+ public Dictionary GetCAConnectorAnnotations()
+ {
+ return new Dictionary()
+ {
+ ["ApiKey"] = new PropertyConfigInfo()
+ {
+ Comments = "The API key for the Atlas credentials the gateway will use.",
+ Hidden = false,
+ DefaultValue = "",
+ Type = "String"
+ },
+ ["ApiSecret"] = new PropertyConfigInfo()
+ {
+ Comments = "The corresponding API secret value that matches with the ApiKey.",
+ Hidden = true,
+ DefaultValue = "",
+ Type = "String"
+ },
+ ["ClientCertificate"] = new PropertyConfigInfo()
+ {
+ Comments = "The client auth certificate to use with the Atlas API",
+ Hidden = false,
+ DefaultValue = "",
+ Type = "ClientCertificate"
+ },
+ ["SyncStartDate"] = new PropertyConfigInfo()
+ {
+ Comments = "The earliest date to go back when doing a full sync.",
+ Hidden = false,
+ DefaultValue = "2024-01-01",
+ Type = "String"
+ }
+ };
+ }
+
+ public List GetProductIds()
+ {
+ // Atlas does not use product IDs. Gateway framework requires a value, so just use 'certificate'
+ return new List() { "certificate" };
+ }
+
+ public async Task GetSingleRecord(string caRequestID)
+ {
+ AtlasClient client = AtlasClient.InitializeClient(_config, _certResolver);
+ var certResponse = client.GetCertificate(caRequestID);
+
+ return new AnyCAPluginCertificate
+ {
+ CARequestID = caRequestID,
+ Certificate = certResponse.Certificate,
+ Status = certResponse.Status.Equals("issued", StringComparison.OrdinalIgnoreCase) ? (int)EndEntityStatus.GENERATED :
+ certResponse.Status.Equals("revoked", StringComparison.OrdinalIgnoreCase) ? (int)EndEntityStatus.REVOKED : (int)EndEntityStatus.EXTERNALVALIDATION,
+ };
+ }
+
+ public Dictionary GetTemplateParameterAnnotations()
+ {
+ return new Dictionary()
+ {
+ ["Lifetime"] = new PropertyConfigInfo()
+ {
+ Comments = "The term length (in days) to use for enrollment.",
+ Hidden = false,
+ DefaultValue = 30,
+ Type = "Number"
+ },
+ ["KeyUsage"] = new PropertyConfigInfo()
+ {
+ Comments = "The key usage to use for enrolled certs. Valid values are 'client', 'server', and 'clientserver'.",
+ Hidden = false,
+ DefaultValue = "server",
+ Type = "String"
+ }
+ };
+ }
+
+ public async Task Ping()
+ {
+ try
+ {
+ AtlasClient client = AtlasClient.InitializeClient(_config, _certResolver);
+ _ = client.GetValidationPolicy();
+ }
+ catch (Exception ex)
+ {
+ _logger.LogError($"Error attempting to contact GlobalSign Atlas: {ex.Message}");
+ throw new Exception($"Error attempting to contact GlobalSign Atlas: {ex.Message}", ex);
+ }
+ }
+
+ public async Task Revoke(string caRequestID, string hexSerialNumber, uint revocationReason)
+ {
+ AtlasClient client = AtlasClient.InitializeClient(_config, _certResolver);
+ client.RevokeCertificate(caRequestID);
+
+ var certResponse = client.GetCertificate(caRequestID);
+ return certResponse.Status.Equals("issued", StringComparison.OrdinalIgnoreCase) ? (int)EndEntityStatus.GENERATED :
+ certResponse.Status.Equals("revoked", StringComparison.OrdinalIgnoreCase) ? (int)EndEntityStatus.REVOKED : (int)EndEntityStatus.EXTERNALVALIDATION;
+ }
+
+ public async Task Synchronize(BlockingCollection blockingBuffer, DateTime? lastSync, bool fullSync, CancellationToken cancelToken)
+ {
+ AtlasClient client = AtlasClient.InitializeClient(_config, _certResolver);
+ var certs = client.GetAllCertificates(lastSync, fullSync);
+
+ foreach (var cert in certs)
+ {
+ var anyCACert = new AnyCAPluginCertificate
+ {
+ CARequestID = cert.Status.SerialNumber,
+ Certificate = cert.Cert.Certificate,
+ Status = cert.Cert.Status.Equals("issued", StringComparison.OrdinalIgnoreCase) ? (int)EndEntityStatus.GENERATED :
+ cert.Cert.Status.Equals("revoked", StringComparison.OrdinalIgnoreCase) ? (int)EndEntityStatus.REVOKED : (int)EndEntityStatus.EXTERNALVALIDATION,
+ };
+ blockingBuffer.Add(anyCACert);
+ }
+ }
+
+ public async Task ValidateCAConnectionInfo(Dictionary connectionInfo)
+ {
+ _logger.LogTrace($"Validating CA Connection info");
+ List errors = new List();
+ string rawConfig = JsonConvert.SerializeObject(connectionInfo);
+ AtlasConfig testConfig = JsonConvert.DeserializeObject(rawConfig);
+
+ _logger.LogTrace($"Checking for API Key/Secret");
+ if (string.IsNullOrWhiteSpace(testConfig.ApiKey))
+ {
+ errors.Add($"The API Key is required");
+ }
+ if (string.IsNullOrWhiteSpace(testConfig.ApiSecret))
+ {
+ errors.Add($"The API Secret is required");
+ }
+
+ _logger.LogTrace($"Checking Sync Start Date");
+ if (string.IsNullOrWhiteSpace(testConfig.SyncStartDate))
+ {
+ errors.Add($"Sync Start Date is required");
+ }
+ else
+ {
+ if (!DateTime.TryParse(testConfig.SyncStartDate, out _))
+ {
+ errors.Add("The Sync Start Date could not be parsed");
+ }
+ }
+
+ _logger.LogTrace($"Checking Client Certificate data");
+ try
+ {
+ X509Certificate2 authCert = null;
+ if (!string.IsNullOrEmpty(testConfig.Certificate.ImportedCertificate))
+ {
+ authCert = new X509Certificate2(Convert.FromBase64String(testConfig.Certificate.ImportedCertificate), testConfig.Certificate.ImportedCertificatePassword);
+ }
+ else
+ {
+ authCert = _certResolver.ResolveCertificate(testConfig.Certificate);
+ }
+ if (authCert == null)
+ {
+ errors.Add($"Unable to load authentication certificate");
+ }
+
+ _logger.LogTrace($"Auth Certificate found. Cert Details: \nSerial Number: {authCert.GetSerialNumberString()}\nHas PK: {authCert.HasPrivateKey.ToString()}\nSubject: {authCert.Subject}");
+ }
+ catch (Exception ex)
+ {
+ errors.Add($"Unable to load authentication certificate: {ex.Message}");
+ }
+
+ if (errors.Any())
+ {
+ throw new Exception(string.Join("\n", errors));
+ }
+ _logger.LogTrace($"CA Connection validation complete");
+ }
+
+ public async Task ValidateProductInfo(EnrollmentProductInfo productInfo, Dictionary connectionInfo)
+ {
+ // Do nothing
+ }
+ }
+}
diff --git a/globalsign-atlas-caplugin/AtlasConfig.cs b/globalsign-atlas-caplugin/AtlasConfig.cs
new file mode 100644
index 0000000..6c3ff89
--- /dev/null
+++ b/globalsign-atlas-caplugin/AtlasConfig.cs
@@ -0,0 +1,29 @@
+using Keyfactor.AnyGateway.Extensions;
+
+using Newtonsoft.Json;
+
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace Keyfactor.Extensions.CAPlugin.GlobalSign.Atlas
+{
+ public class AtlasConfig
+ {
+ public AtlasConfig() { }
+
+ [JsonProperty("ApiKey")]
+ public string ApiKey { get; set; }
+
+ [JsonProperty("ApiSecret")]
+ public string ApiSecret { get; set; }
+
+ [JsonProperty("ClientCertificate")]
+ public ClientCertificate Certificate { get; set; }
+
+ [JsonProperty("SyncStartDate")]
+ public string SyncStartDate { get; set; }
+ }
+}
diff --git a/globalsign-atlas-caplugin/Client/AtlasClient.cs b/globalsign-atlas-caplugin/Client/AtlasClient.cs
new file mode 100644
index 0000000..468a76d
--- /dev/null
+++ b/globalsign-atlas-caplugin/Client/AtlasClient.cs
@@ -0,0 +1,620 @@
+using Keyfactor.AnyGateway.Extensions;
+using Keyfactor.Common.Exceptions;
+using Keyfactor.Extensions.CAPlugin.GlobalSign.Atlas.APIProxy;
+using Keyfactor.Logging;
+using Keyfactor.PKI.Enums.EJBCA;
+
+using Microsoft.Extensions.Logging;
+
+using Newtonsoft.Json;
+using Newtonsoft.Json.Linq;
+
+using Org.BouncyCastle.Crmf;
+
+using Org.BouncyCastle.Pqc.Crypto.Lms;
+
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Net;
+using System.Numerics;
+using System.Security.Cryptography.X509Certificates;
+using System.Text;
+using System.Threading.Tasks;
+
+using CertificateResponse = Keyfactor.Extensions.CAPlugin.GlobalSign.Atlas.APIProxy.CertificateResponse;
+
+namespace Keyfactor.Extensions.CAPlugin.GlobalSign.Atlas.Client
+{
+ public class AtlasClient
+ {
+ private static ILogger Logger => LogHandler.GetClassLogger();
+ private string _apiKey;
+ private string _apiSecret;
+ private X509Certificate2 _authCert;
+ private string _baseUrl;
+
+ private string _token;
+ private DateTime _tokenTime;
+
+ private DateTime _syncStart;
+
+ private class AtlasResponse
+ {
+ public bool Success { get; set; }
+ public string Response { get; set; }
+
+ public AtlasResponse()
+ {
+ Success = true;
+ Response = "";
+ }
+ }
+
+ public AtlasClient(string apiKey, string apiSecret, X509Certificate2 authCert, DateTime syncStart)
+ :this(apiKey, apiSecret, authCert, syncStart, "https://emea.api.hvca.globalsign.com:8443/v2/")
+ { }
+
+ public AtlasClient(string apiKey, string apiSecret, X509Certificate2 authCert, DateTime syncStart, string baseUrl)
+ {
+ _apiKey = apiKey;
+ _apiSecret = apiSecret;
+ _authCert = authCert;
+ _syncStart = syncStart;
+ _baseUrl = baseUrl;
+ if (!_baseUrl.EndsWith("/"))
+ _baseUrl += "/";
+ }
+
+ public static AtlasClient InitializeClient(AtlasConfig config, ICertificateResolver certResolver)
+ {
+ Logger.MethodEntry(LogLevel.Debug);
+ Logger.LogTrace($"Retrieving auth certificate");
+ X509Certificate2 authCert = null;
+ if (!string.IsNullOrEmpty(config.Certificate.ImportedCertificate))
+ {
+ authCert = new X509Certificate2(Convert.FromBase64String(config.Certificate.ImportedCertificate), config.Certificate.ImportedCertificatePassword);
+ }
+ else
+ {
+ authCert = certResolver.ResolveCertificate(config.Certificate);
+ }
+ if (authCert == null)
+ {
+ Logger.MethodExit(LogLevel.Debug);
+ throw new Exception("Unable to resolve auth certificate.");
+ }
+
+ Logger.LogTrace($"Auth Certificate found. Cert Details: \nSerial Number: {authCert.GetSerialNumberString()}\nHas PK: {authCert.HasPrivateKey.ToString()}\nSubject: {authCert.Subject}");
+
+ return new AtlasClient(config.ApiKey, config.ApiSecret, authCert, DateTime.Parse(config.SyncStartDate));
+ }
+
+ private void RefreshApiToken()
+ {
+ Logger.MethodEntry(LogLevel.Debug);
+ try
+ {
+ string targetUri = _baseUrl + "login/";
+ HttpWebRequest request = (HttpWebRequest)WebRequest.Create(targetUri);
+ request.Method = "POST";
+ request.ContentType = "application/json;charset=utf-8";
+ request.Headers["X-SSL-Client-Serial"] = _authCert.SerialNumber;
+ request.ClientCertificates.Add(_authCert);
+ var loginReq = new LoginRequest()
+ {
+ Key = _apiKey,
+ Secret = _apiSecret
+ };
+ string postBody = JsonConvert.SerializeObject(loginReq, Formatting.None, new JsonSerializerSettings { NullValueHandling = NullValueHandling.Ignore });
+ byte[] postBytes = Encoding.UTF8.GetBytes(postBody);
+
+ request.ContentLength = postBytes.Length;
+ Stream requestStream = request.GetRequestStream();
+ requestStream.Write(postBytes, 0, postBytes.Length);
+ requestStream.Close();
+
+ LoginResponse apiResponse = new LoginResponse();
+ _tokenTime = DateTime.UtcNow;
+
+ Logger.LogTrace($"Atlas Request: POST {targetUri}");
+ using (HttpWebResponse response = (HttpWebResponse)request.GetResponse())
+ {
+ apiResponse = JsonConvert.DeserializeObject(new StreamReader(response.GetResponseStream()).ReadToEnd());
+ }
+ _token = apiResponse.Token;
+ }
+ catch (Exception ex)
+ {
+ Logger.LogError($"Atlas response error: {ex.Message}");
+ throw new Exception($"Unable to establish connection to Atlas web service: {ex.Message}", ex);
+ }
+ }
+
+ public EnrollResponse RequestNewCertificate(Enroll request, int pickupDelay, int pickupRetries)
+ {
+ EnrollResponse enrollResponse = new EnrollResponse();
+ string certUrl = null;
+ try
+ {
+ string targetUri = _baseUrl + "certificates/";
+ string method = "POST";
+
+ Logger.LogTrace($"Requesting new certificate");
+ HttpWebRequest apiRequest = (HttpWebRequest)WebRequest.Create(targetUri);
+ apiRequest.Method = method;
+ apiRequest.ContentType = "application/json;charset=utf-8";
+ if (string.IsNullOrEmpty(_token) || _tokenTime.AddMinutes(10) < DateTime.UtcNow)
+ {
+ RefreshApiToken();
+ }
+ apiRequest.ClientCertificates.Add(_authCert);
+ apiRequest.Headers.Add("Authorization", "Bearer " + _token);
+ string postJson = JsonConvert.SerializeObject(request);
+ byte[] postBytes = Encoding.UTF8.GetBytes(postJson);
+ apiRequest.ContentLength = postBytes.Length;
+ Stream requestStream = apiRequest.GetRequestStream();
+ requestStream.Write(postBytes, 0, postBytes.Length);
+ requestStream.Close();
+
+ Logger.LogTrace($"Atlas Request: POST {targetUri}\n{postJson}");
+ using (HttpWebResponse apiResponse = (HttpWebResponse)apiRequest.GetResponse())
+ {
+ Logger.LogTrace($"Atlas API returned response {apiResponse.StatusCode}");
+ if (apiResponse.StatusCode == HttpStatusCode.Created)
+ {
+ certUrl = apiResponse.Headers["Location"];
+ enrollResponse.SerialNumber = certUrl.Substring(certUrl.LastIndexOf('/') + 1);
+ }
+ else
+ {
+ throw new Exception($"Unable to enroll for certificate, status code: {apiResponse.StatusCode}");
+ }
+ }
+
+ targetUri = _baseUrl + "certificates/" + enrollResponse.SerialNumber;
+ method = "GET";
+
+ Logger.LogTrace($"Enrollment successful, retrieving new certificate");
+ bool success = true;
+ int attempt = 0;
+ do
+ {
+ try
+ {
+ System.Threading.Thread.Sleep(pickupDelay * 1000);
+ apiRequest = (HttpWebRequest)WebRequest.Create(targetUri);
+ apiRequest.Method = method;
+ apiRequest.ContentType = "application/json;charset=utf-8";
+ if (string.IsNullOrEmpty(_token) || _tokenTime.AddMinutes(10) < DateTime.UtcNow)
+ {
+ RefreshApiToken();
+ }
+ apiRequest.ClientCertificates.Add(_authCert);
+ apiRequest.Headers.Add("Authorization", "Bearer " + _token);
+
+ Logger.LogTrace($"Atlas Request: GET {targetUri}");
+ using (HttpWebResponse apiResponse = (HttpWebResponse)apiRequest.GetResponse())
+ {
+ Logger.LogTrace($"Atlas API returned response {apiResponse.StatusCode}");
+ if (apiResponse.StatusCode == HttpStatusCode.OK)
+ {
+ Logger.LogTrace($"Certificate retrieved");
+ CertificateResponse certResponse = JsonConvert.DeserializeObject(new StreamReader(apiResponse.GetResponseStream()).ReadToEnd());
+ enrollResponse.Status = EndEntityStatus.GENERATED;
+ enrollResponse.Cert = certResponse.Certificate;
+ enrollResponse.StatusMessage = "Successfully enrolled for certificate {0}";
+ }
+ else if (apiResponse.StatusCode == HttpStatusCode.Accepted)
+ {
+ Logger.LogTrace($"Certificate request in process");
+ CertificateResponse certResponse = JsonConvert.DeserializeObject(new StreamReader(apiResponse.GetResponseStream()).ReadToEnd());
+
+ enrollResponse.Status = EndEntityStatus.INPROCESS;
+ enrollResponse.StatusMessage = certResponse.Description;
+ success = false;
+ attempt++;
+ }
+ else
+ {
+ success = false;
+ attempt++;
+ }
+ }
+ }
+ catch (WebException wex)
+ {
+ success = false;
+ if (wex.Response != null)
+ {
+ using (var errorResponse = (HttpWebResponse)wex.Response)
+ {
+ if (errorResponse.StatusCode == (HttpStatusCode)429 /*Too Many Requests*/)
+ {
+ Logger.LogInformation("Request was rate-limited. Trying again in 5 seconds");
+ System.Threading.Thread.Sleep(5000);
+ }
+ else
+ {
+ attempt++;
+ }
+ }
+ }
+ else
+ {
+ attempt++;
+ }
+ }
+ if (!success)
+ {
+ if (attempt <= pickupRetries)
+ {
+ Logger.LogWarning($"Unable to pickup certificate. Retrying... (Retry attempt {attempt} of {pickupRetries})");
+ }
+ else
+ {
+ Logger.LogError($"Failed to pickup enrolled certificate. Current disposition status: {enrollResponse.Status.ToString()}");
+ }
+ }
+ } while (success == false && attempt <= pickupRetries);
+ return enrollResponse;
+ }
+ catch (WebException wex)
+ {
+ if (wex.Response != null)
+ {
+ using (var errorResponse = (HttpWebResponse)wex.Response)
+ {
+ if (errorResponse.StatusCode == (HttpStatusCode)429 /*Too Many Requests*/)
+ {
+ Logger.LogInformation("Request was rate-limited. Trying again in 5 seconds");
+ System.Threading.Thread.Sleep(5000);
+ return RequestNewCertificate(request, pickupDelay, pickupRetries);
+ }
+ else
+ {
+ using (var stream = wex.Response.GetResponseStream())
+ using (var reader = new StreamReader(stream))
+ {
+ string errorString = reader.ReadToEnd();
+ Logger.LogError($"Atlas CA has returned an error from enrolling: '{((HttpWebResponse)wex.Response).StatusCode}: {errorString}");
+ throw new Exception(errorString, wex);
+ }
+ }
+ }
+ }
+ else
+ {
+ Logger.LogError($"Error enrolling for cert: {wex.Message}");
+ throw new Exception($"Error enrolling for cert", wex);
+ }
+ }
+ catch (Exception ex)
+ {
+ Logger.LogError($"Error enrolling for cert: {ex.Message}");
+ throw new Exception($"Error enrolling for cert", ex);
+ }
+ }
+
+ public CertificateResponse GetCertificate(string caRequestID)
+ {
+ try
+ {
+ string targetUri = _baseUrl + "certificates/" + caRequestID;
+ string method = "GET";
+
+ Logger.LogTrace($"Retrieving certificate");
+ HttpWebRequest apiRequest = (HttpWebRequest)WebRequest.Create(targetUri);
+ apiRequest.Method = method;
+ apiRequest.ContentType = "application/json;charset=utf-8";
+ if (string.IsNullOrEmpty(_token) || _tokenTime.AddMinutes(10) < DateTime.UtcNow)
+ {
+ RefreshApiToken();
+ }
+ apiRequest.ClientCertificates.Add(_authCert);
+ apiRequest.Headers.Add("Authorization", "Bearer " + _token);
+
+ Logger.LogTrace($"Atlas Request: GET {targetUri}");
+ using (HttpWebResponse apiResponse = (HttpWebResponse)apiRequest.GetResponse())
+ {
+ Logger.LogTrace($"Atlas API returned response {apiResponse.StatusCode}");
+ if (apiResponse.StatusCode == HttpStatusCode.OK)
+ {
+ CertificateResponse certResponse = JsonConvert.DeserializeObject(new StreamReader(apiResponse.GetResponseStream()).ReadToEnd());
+ return certResponse;
+ }
+ else if (apiResponse.StatusCode == HttpStatusCode.Accepted)
+ {
+ CertificateResponse certResponse = JsonConvert.DeserializeObject(new StreamReader(apiResponse.GetResponseStream()).ReadToEnd());
+
+ return certResponse;
+ }
+ else
+ {
+ throw new Exception($"Unable to retrieve certificate, status code: {apiResponse.StatusCode}");
+ }
+ }
+ }
+ catch (WebException wex)
+ {
+ if (wex.Response != null)
+ {
+ using (var errorResponse = (HttpWebResponse)wex.Response)
+ {
+ if (errorResponse.StatusCode == (HttpStatusCode)429 /*Too Many Requests*/)
+ {
+ Logger.LogInformation($"Request was rate-limited. Trying again in 5 seconds");
+ System.Threading.Thread.Sleep(5000);
+ return GetCertificate(caRequestID);
+ }
+ else
+ {
+ using (var stream = wex.Response.GetResponseStream())
+ using (var reader = new StreamReader(stream))
+ {
+ string errorString = reader.ReadToEnd();
+ Logger.LogError($"Atlas CA has returned an error from retrieving certificate: '{((HttpWebResponse)wex.Response).StatusCode}: {errorString}");
+ throw new Exception(errorString, wex);
+ }
+ }
+ }
+ }
+ else
+ {
+ Logger.LogError($"Error retrieving cert: {wex.Message}");
+ throw new Exception($"Error retrieving cert", wex);
+ }
+ }
+ catch (Exception ex)
+ {
+ Logger.LogError($"Error retrieving cert: {ex.Message}");
+ throw new Exception($"Error retrieving cert", ex);
+ }
+ }
+
+ public void RevokeCertificate(string caRequestID)
+ {
+ try
+ {
+ string targetUri = _baseUrl + "certificates/" + caRequestID;
+ string method = "DELETE";
+
+ Logger.LogTrace($"Attempting to revoke certificate");
+ HttpWebRequest apiRequest = (HttpWebRequest)WebRequest.Create(targetUri);
+ apiRequest.Method = method;
+ apiRequest.ContentType = "application/json;charset=utf-8";
+ if (string.IsNullOrEmpty(_token) || _tokenTime.AddMinutes(10) < DateTime.UtcNow)
+ {
+ RefreshApiToken();
+ }
+ apiRequest.ClientCertificates.Add(_authCert);
+ apiRequest.Headers.Add("Authorization", "Bearer " + _token);
+
+ Logger.LogTrace($"Atlas Request: DELETE {targetUri}");
+ using (HttpWebResponse apiResponse = (HttpWebResponse)apiRequest.GetResponse())
+ {
+ Logger.LogTrace($"Atlas API returned response {apiResponse.StatusCode}");
+ if (apiResponse.StatusCode == HttpStatusCode.NoContent)
+ {
+ Logger.LogTrace($"Certificate successfully revoked");
+ }
+ else
+ {
+ throw new Exception($"Unable to revoke certificate, status code: {apiResponse.StatusCode}");
+ }
+ }
+ }
+ catch (WebException wex)
+ {
+ if (wex.Response != null)
+ {
+ using (var errorResponse = (HttpWebResponse)wex.Response)
+ {
+ if (errorResponse.StatusCode == (HttpStatusCode)429 /*Too Many Requests*/)
+ {
+ Logger.LogInformation("Request was rate-limited. Trying again in 5 seconds.");
+ System.Threading.Thread.Sleep(5000);
+ RevokeCertificate(caRequestID);
+ }
+ else
+ {
+ using (var stream = wex.Response.GetResponseStream())
+ using (var reader = new StreamReader(stream))
+ {
+ string errorString = reader.ReadToEnd();
+ Logger.LogError($"Atlas CA has returned an error from revoking certificate: '{((HttpWebResponse)wex.Response).StatusCode}: {errorString}");
+ throw new Exception(errorString, wex);
+ }
+ }
+ }
+ }
+ else
+ {
+ Logger.LogError($"Error revoking cert: {wex.Message}");
+ throw new Exception($"Error revoking cert", wex);
+ }
+ }
+ catch (Exception ex)
+ {
+ Logger.LogError($"Error revoking cert: {ex.Message}");
+ throw new Exception($"Error revoking cert", ex);
+ }
+ }
+
+ public ValidationPolicyResponse GetValidationPolicy()
+ {
+ try
+ {
+ string targetUri = _baseUrl + "validationpolicy/";
+ string method = "GET";
+
+ Logger.LogTrace($"Retrieving validation policy");
+ HttpWebRequest apiRequest = (HttpWebRequest)WebRequest.Create(targetUri);
+ apiRequest.Method = method;
+ apiRequest.ContentType = "application/json;charset=utf-8";
+ if (string.IsNullOrEmpty(_token) || _tokenTime.AddMinutes(10) < DateTime.UtcNow)
+ {
+ RefreshApiToken();
+ }
+ apiRequest.ClientCertificates.Add(_authCert);
+ apiRequest.Headers.Add("Authorization", "Bearer " + _token);
+ Logger.LogTrace($"Atlas Request: GET {targetUri}");
+ using (HttpWebResponse apiResponse = (HttpWebResponse)apiRequest.GetResponse())
+ {
+ var fullResponse = new StreamReader(apiResponse.GetResponseStream()).ReadToEnd();
+ Logger.LogTrace($"Atlas API returned response {apiResponse.StatusCode}");
+ if (apiResponse.StatusCode == HttpStatusCode.OK)
+ {
+ ValidationPolicyResponse validationResponse = JsonConvert.DeserializeObject(fullResponse);
+ return validationResponse;
+ }
+ else
+ {
+ throw new Exception("Error retrieving validation policy");
+ }
+ }
+ }
+ catch (WebException wex)
+ {
+ if (wex.Response != null)
+ {
+ using (var errorResponse = (HttpWebResponse)wex.Response)
+ {
+ if (errorResponse.StatusCode == (HttpStatusCode)429 /*Too Many Requests*/)
+ {
+ Logger.LogInformation("Request was rate-limited. Trying again in 5 seconds.");
+ System.Threading.Thread.Sleep(5000);
+ return GetValidationPolicy();
+ }
+ else
+ {
+ using (var stream = wex.Response.GetResponseStream())
+ using (var reader = new StreamReader(stream))
+ {
+ string errorString = reader.ReadToEnd();
+ Logger.LogError($"Atlas CA has returned an error from retrieving validation policy: '{((HttpWebResponse)wex.Response).StatusCode}: {errorString}");
+ throw new Exception(errorString, wex);
+ }
+ }
+ }
+ }
+ else
+ {
+ Logger.LogError($"Error retrieving validation policy: {wex.Message}");
+ throw new Exception($"Error retrieving validation policy", wex);
+ }
+ }
+ catch (Exception ex)
+ {
+ Logger.LogError($"Error retrieving validation policy: {ex.Message}");
+ throw new Exception($"Error retrieving validation policy", ex);
+ }
+ }
+
+ public List GetAllCertificates(DateTime? lastIncrementalSync, bool doFullSync)
+ {
+ if (!lastIncrementalSync.HasValue)
+ {
+ lastIncrementalSync = _syncStart;
+ }
+ DateTime startTime = doFullSync ? _syncStart : (lastIncrementalSync.Value);
+ DateTime endTime = DateTime.UtcNow;
+ long startTimeTicks = ((DateTimeOffset)startTime).ToUnixTimeSeconds();
+ long endTimeTicks = ((DateTimeOffset)endTime).ToUnixTimeSeconds();
+
+ List certs = new List();
+
+ for (long i = startTimeTicks; i <= endTimeTicks; i += 1728000) // Sync 20 days at a time
+ {
+ int pagenum = 1;
+ bool morePages = false;
+ long toTicks = i + 1728000;
+ if (toTicks > endTimeTicks)
+ {
+ toTicks = endTimeTicks;
+ }
+ do
+ {
+ morePages = false;
+ string targetUri = _baseUrl + "stats/issued?page=" + pagenum + "&from=" + i + "&to=" + toTicks;
+ string method = "GET";
+
+ Logger.LogTrace($"Retrieving certificate list");
+ HttpWebRequest apiRequest = (HttpWebRequest)WebRequest.Create(targetUri);
+ apiRequest.Method = method;
+ apiRequest.ContentType = "application/json;charset=utf-8";
+ if (string.IsNullOrEmpty(_token) || _tokenTime.AddMinutes(10) < DateTime.UtcNow)
+ {
+ RefreshApiToken();
+ }
+ apiRequest.ClientCertificates.Add(_authCert);
+ apiRequest.Headers.Add("Authorization", "Bearer " + _token);
+ List certResponse;
+ try
+ {
+ Logger.LogTrace($"Atlas Request: GET {targetUri}");
+ using (HttpWebResponse apiResponse = (HttpWebResponse)apiRequest.GetResponse())
+ {
+ Logger.LogTrace($"Atlas API returned response {apiResponse.StatusCode}");
+ if (apiResponse.StatusCode == HttpStatusCode.OK)
+ {
+ certResponse = JsonConvert.DeserializeObject>(new StreamReader(apiResponse.GetResponseStream()).ReadToEnd());
+
+ var header = apiResponse.Headers["Links"];
+ if (header.Contains("next"))
+ {
+ morePages = true;
+ pagenum++;
+ }
+ }
+ else
+ {
+ throw new Exception($"Unable to retrieve certificate list, status code: {apiResponse.StatusCode}");
+ }
+ }
+ }
+ catch (WebException wex)
+ {
+ if (wex.Response != null)
+ {
+ using (var errorResponse = (HttpWebResponse)wex.Response)
+ {
+ if (errorResponse.StatusCode == (HttpStatusCode)429 /*Too Many Requests*/)
+ {
+ Logger.LogInformation("Request was rate-limited. Trying again in 5 seconds.");
+ System.Threading.Thread.Sleep(5000);
+ continue;
+ }
+ }
+ using (var stream = wex.Response.GetResponseStream())
+ using (var reader = new StreamReader(stream))
+ {
+ string errorString = reader.ReadToEnd();
+ Logger.LogError($"Atlas CA has returned an error from retrieving certificate: '{((HttpWebResponse)wex.Response).StatusCode}: {errorString}");
+ throw new Exception(errorString, wex);
+ }
+ }
+ else
+ {
+ Logger.LogError($"Error retrieving cert: {wex.Message}");
+ throw new Exception($"Error retrieving cert", wex);
+ }
+ }
+ catch (Exception ex)
+ {
+ Logger.LogError($"Error retrieving cert: {ex.Message}");
+ throw new Exception($"Error retrieving cert", ex);
+ }
+ foreach (var certStatus in certResponse)
+ {
+ CertificateDetailsResponse details = new CertificateDetailsResponse();
+ details.Status = certStatus;
+ details.Cert = GetCertificate(certStatus.SerialNumber);
+ certs.Add(details);
+ }
+ } while (morePages);
+ }
+ return certs;
+ }
+ }
+}
diff --git a/globalsign-atlas-caplugin/globalsign-atlas-caplugin.csproj b/globalsign-atlas-caplugin/globalsign-atlas-caplugin.csproj
new file mode 100644
index 0000000..0a24675
--- /dev/null
+++ b/globalsign-atlas-caplugin/globalsign-atlas-caplugin.csproj
@@ -0,0 +1,17 @@
+
+
+
+ net8.0
+ Keyfactor.Extensions.CAPlugin.GlobalSign.Atlas
+ enable
+ disable
+
+
+
+
+
+
+
+
+
+
From f1e7a708073ffc9885f82faf6db2a24cc3967aaf Mon Sep 17 00:00:00 2001
From: Dave Galey <89407235+dgaley@users.noreply.github.com>
Date: Tue, 23 Jun 2026 09:50:17 -0400
Subject: [PATCH 02/15] Create keyfactor-bootstrap-workflow-v3.yml
---
.../keyfactor-bootstrap-workflow-v3.yml | 20 +++++++++++++++++++
1 file changed, 20 insertions(+)
create mode 100644 .github/workflows/keyfactor-bootstrap-workflow-v3.yml
diff --git a/.github/workflows/keyfactor-bootstrap-workflow-v3.yml b/.github/workflows/keyfactor-bootstrap-workflow-v3.yml
new file mode 100644
index 0000000..64919a4
--- /dev/null
+++ b/.github/workflows/keyfactor-bootstrap-workflow-v3.yml
@@ -0,0 +1,20 @@
+name: Keyfactor Bootstrap Workflow
+
+on:
+ workflow_dispatch:
+ pull_request:
+ types: [opened, closed, synchronize, edited, reopened]
+ push:
+ create:
+ branches:
+ - 'release-*.*'
+
+jobs:
+ call-starter-workflow:
+ uses: keyfactor/actions/.github/workflows/starter.yml@v3
+ secrets:
+ token: ${{ secrets.V2BUILDTOKEN}}
+ APPROVE_README_PUSH: ${{ secrets.APPROVE_README_PUSH}}
+ gpg_key: ${{ secrets.KF_GPG_PRIVATE_KEY }}
+ gpg_pass: ${{ secrets.KF_GPG_PASSPHRASE }}
+ scan_token: ${{ secrets.SAST_TOKEN }}
From 06e15afa555a5cb491ce544ac94d4173d854c3f7 Mon Sep 17 00:00:00 2001
From: David Galey
Date: Tue, 23 Jun 2026 09:54:16 -0400
Subject: [PATCH 03/15] integration manifest and changelog
---
CHANGELOG.md | 2 ++
globalsign-atlas-caplugin.sln | 8 +++++++-
integration-manifest.json | 13 +++++++++++++
3 files changed, 22 insertions(+), 1 deletion(-)
create mode 100644 CHANGELOG.md
create mode 100644 integration-manifest.json
diff --git a/CHANGELOG.md b/CHANGELOG.md
new file mode 100644
index 0000000..03d82d1
--- /dev/null
+++ b/CHANGELOG.md
@@ -0,0 +1,2 @@
+### 1.0.0
+* Initial Release
diff --git a/globalsign-atlas-caplugin.sln b/globalsign-atlas-caplugin.sln
index e86e735..411dafc 100644
--- a/globalsign-atlas-caplugin.sln
+++ b/globalsign-atlas-caplugin.sln
@@ -1,10 +1,16 @@
Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17
-VisualStudioVersion = 17.14.36705.20 d17.14
+VisualStudioVersion = 17.14.36705.20
MinimumVisualStudioVersion = 10.0.40219.1
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "globalsign-atlas-caplugin", "globalsign-atlas-caplugin\globalsign-atlas-caplugin.csproj", "{292B7A34-CFEC-4ABB-8BE6-31F87BC1651B}"
EndProject
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{8EC462FD-D22E-90A8-E5CE-7E832BA40C5D}"
+ ProjectSection(SolutionItems) = preProject
+ CHANGELOG.md = CHANGELOG.md
+ integration-manifest.json = integration-manifest.json
+ EndProjectSection
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
diff --git a/integration-manifest.json b/integration-manifest.json
new file mode 100644
index 0000000..607b443
--- /dev/null
+++ b/integration-manifest.json
@@ -0,0 +1,13 @@
+{
+ "$schema": "https://keyfactor.github.io/v2/integration-manifest-schema.json",
+ "integration_type": "anyca-plugin",
+ "name": "GlobalSign Atlas AnyCA REST Gateway Plugin",
+ "status": "production",
+ "support_level": "kf-supported",
+ "link_github": true,
+ "update_catalog": true,
+ "description": "GlobalSign Atlas plugin for the AnyCA REST Gateway framework",
+ "gateway_framework": "24.2.0",
+ "release_dir": "globalsign-atlas-caplugin/bin/Release",
+ "release_project": "globalsign-atlas-caplugin/globalsign-atlas-caplugin.csproj",
+}
\ No newline at end of file
From 901c4c718716368c5797928adaec51cafd9b9d0d Mon Sep 17 00:00:00 2001
From: David Galey
Date: Tue, 23 Jun 2026 11:50:03 -0400
Subject: [PATCH 04/15] update manifest
---
integration-manifest.json | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/integration-manifest.json b/integration-manifest.json
index 607b443..59955fd 100644
--- a/integration-manifest.json
+++ b/integration-manifest.json
@@ -9,5 +9,5 @@
"description": "GlobalSign Atlas plugin for the AnyCA REST Gateway framework",
"gateway_framework": "24.2.0",
"release_dir": "globalsign-atlas-caplugin/bin/Release",
- "release_project": "globalsign-atlas-caplugin/globalsign-atlas-caplugin.csproj",
+ "release_project": "globalsign-atlas-caplugin/globalsign-atlas-caplugin.csproj"
}
\ No newline at end of file
From db25ff284bb08af28d31c58dd9406459fb777488 Mon Sep 17 00:00:00 2001
From: David Galey
Date: Tue, 23 Jun 2026 11:57:32 -0400
Subject: [PATCH 05/15] add manifest
---
.../globalsign-atlas-caplugin.csproj | 1 +
globalsign-atlas-caplugin/manifest.json | 10 ++++++++++
2 files changed, 11 insertions(+)
create mode 100644 globalsign-atlas-caplugin/manifest.json
diff --git a/globalsign-atlas-caplugin/globalsign-atlas-caplugin.csproj b/globalsign-atlas-caplugin/globalsign-atlas-caplugin.csproj
index 0a24675..2d244d6 100644
--- a/globalsign-atlas-caplugin/globalsign-atlas-caplugin.csproj
+++ b/globalsign-atlas-caplugin/globalsign-atlas-caplugin.csproj
@@ -5,6 +5,7 @@
Keyfactor.Extensions.CAPlugin.GlobalSign.Atlas
enable
disable
+ AtlasCAPlugin
diff --git a/globalsign-atlas-caplugin/manifest.json b/globalsign-atlas-caplugin/manifest.json
new file mode 100644
index 0000000..81357a6
--- /dev/null
+++ b/globalsign-atlas-caplugin/manifest.json
@@ -0,0 +1,10 @@
+{
+ "extensions": {
+ "Keyfactor.AnyGateway.Extensions.IAnyCAPlugin": {
+ "AtlasCAPlugin": {
+ "assemblypath": "AtlasCAPlugin.dll",
+ "TypeFullName": "Keyfactor.Extensions.CAPlugin.GlobalSign.Atlas.AtlasCAPlugin"
+ }
+ }
+ }
+}
From 9f1d6a7e876b800294a5fb2b68236f37e6cecc96 Mon Sep 17 00:00:00 2001
From: David Galey
Date: Tue, 23 Jun 2026 11:57:56 -0400
Subject: [PATCH 06/15] copy manifest to output
---
globalsign-atlas-caplugin/globalsign-atlas-caplugin.csproj | 6 ++++++
1 file changed, 6 insertions(+)
diff --git a/globalsign-atlas-caplugin/globalsign-atlas-caplugin.csproj b/globalsign-atlas-caplugin/globalsign-atlas-caplugin.csproj
index 2d244d6..62f74d2 100644
--- a/globalsign-atlas-caplugin/globalsign-atlas-caplugin.csproj
+++ b/globalsign-atlas-caplugin/globalsign-atlas-caplugin.csproj
@@ -15,4 +15,10 @@
+
+
+ Always
+
+
+
From ce0df68fd25f9d0330cd02fa25c7d2e3906d6deb Mon Sep 17 00:00:00 2001
From: Keyfactor
Date: Tue, 23 Jun 2026 15:59:57 +0000
Subject: [PATCH 07/15] Update generated docs
---
README.md | 105 ++++++++++++++++++++++++++++++++++++-
docsource/configuration.md | 24 +++++++++
integration-manifest.json | 37 ++++++++++++-
3 files changed, 164 insertions(+), 2 deletions(-)
create mode 100644 docsource/configuration.md
diff --git a/README.md b/README.md
index 45f8331..2185c80 100644
--- a/README.md
+++ b/README.md
@@ -1 +1,104 @@
-# globalsign-atlas-caplugin
\ No newline at end of file
+
+ GlobalSign Atlas AnyCA Gateway REST Plugin
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Support
+
+ ·
+
+ Requirements
+
+ ·
+
+ Installation
+
+ ·
+
+ License
+
+ ·
+
+ Related Integrations
+
+
+
+
+TODO Overview is a required section
+
+## Compatibility
+
+The GlobalSign Atlas AnyCA Gateway REST plugin is compatible with the Keyfactor AnyCA Gateway REST 24.2.0 and later.
+
+## Support
+The GlobalSign Atlas AnyCA Gateway REST plugin is supported by Keyfactor for Keyfactor customers. If you have a support issue, please open a support ticket with your Keyfactor representative. If you have a support issue, please open a support ticket via the Keyfactor Support Portal at https://support.keyfactor.com.
+
+> To report a problem or suggest a new feature, use the **[Issues](../../issues)** tab. If you want to contribute actual bug fixes or proposed enhancements, use the **[Pull requests](../../pulls)** tab.
+
+## Requirements
+
+TODO Requirements is a required section
+
+## Installation
+
+1. Install the AnyCA Gateway REST per the [official Keyfactor documentation](https://software.keyfactor.com/Guides/AnyCAGatewayREST/Content/AnyCAGatewayREST/InstallIntroduction.htm).
+
+2. On the server hosting the AnyCA Gateway REST, download and unzip the latest [GlobalSign Atlas AnyCA Gateway REST plugin](https://github.com/Keyfactor/globalsign-atlas-caplugin/releases/latest) from GitHub.
+
+3. Copy the unzipped directory (usually called `net6.0` or `net8.0`) to the Extensions directory:
+
+
+ ```shell
+ Depending on your AnyCA Gateway REST version, copy the unzipped directory to one of the following locations:
+ Program Files\Keyfactor\AnyCA Gateway\AnyGatewayREST\net6.0\Extensions
+ Program Files\Keyfactor\AnyCA Gateway\AnyGatewayREST\net8.0\Extensions
+ ```
+
+ > The directory containing the GlobalSign Atlas AnyCA Gateway REST plugin DLLs (`net6.0` or `net8.0`) can be named anything, as long as it is unique within the `Extensions` directory.
+
+4. Restart the AnyCA Gateway REST service.
+
+5. Navigate to the AnyCA Gateway REST portal and verify that the Gateway recognizes the GlobalSign Atlas plugin by hovering over the ⓘ symbol to the right of the Gateway on the top left of the portal.
+
+## Configuration
+
+1. Follow the [official AnyCA Gateway REST documentation](https://software.keyfactor.com/Guides/AnyCAGatewayREST/Content/AnyCAGatewayREST/AddCA-Gateway.htm) to define a new Certificate Authority, and use the notes below to configure the **Gateway Registration** and **CA Connection** tabs:
+
+ * **Gateway Registration**
+
+ TODO Gateway Registration is a required section
+
+ * **CA Connection**
+
+ Populate using the configuration fields collected in the [requirements](#requirements) section.
+
+ * **ApiKey** - The API key for the Atlas credentials the gateway will use.
+ * **ApiSecret** - The corresponding API secret value that matches with the ApiKey.
+ * **ClientCertificate** - The client auth certificate to use with the Atlas API
+ * **SyncStartDate** - The earliest date to go back when doing a full sync.
+
+2. TODO Certificate Template Creation Step is an optional section. If this section doesn't seem necessary on initial glance, please delete it. Refer to the docs on [Confluence](https://keyfactor.atlassian.net/wiki/x/SAAyHg) for more info
+
+3. Follow the [official Keyfactor documentation](https://software.keyfactor.com/Guides/AnyCAGatewayREST/Content/AnyCAGatewayREST/AddCA-Keyfactor.htm) to add each defined Certificate Authority to Keyfactor Command and import the newly defined Certificate Templates.
+
+4. TODO Custom Enrollment Parameter Creation Step is an optional section. If this section doesn't seem necessary on initial glance, please delete it. Refer to the docs on [Confluence](https://keyfactor.atlassian.net/wiki/x/SAAyHg) for more info
+
+
+
+## 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
diff --git a/docsource/configuration.md b/docsource/configuration.md
new file mode 100644
index 0000000..71fb0e2
--- /dev/null
+++ b/docsource/configuration.md
@@ -0,0 +1,24 @@
+## Overview
+
+TODO Overview is a required section
+
+## Requirements
+
+TODO Requirements is a required section
+
+## Gateway Registration
+
+TODO Gateway Registration is a required section
+
+## Certificate Template Creation Step
+
+TODO Certificate Template Creation Step is an optional section. If this section doesn't seem necessary on initial glance, please delete it. Refer to the docs on [Confluence](https://keyfactor.atlassian.net/wiki/x/SAAyHg) for more info
+
+## Custom Enrollment Parameter Creation Step
+
+TODO Custom Enrollment Parameter Creation Step is an optional section. If this section doesn't seem necessary on initial glance, please delete it. Refer to the docs on [Confluence](https://keyfactor.atlassian.net/wiki/x/SAAyHg) for more info
+
+## Mechanics
+
+TODO Mechanics is an optional section. If this section doesn't seem necessary on initial glance, please delete it. Refer to the docs on [Confluence](https://keyfactor.atlassian.net/wiki/x/SAAyHg) for more info
+
diff --git a/integration-manifest.json b/integration-manifest.json
index 59955fd..a51f121 100644
--- a/integration-manifest.json
+++ b/integration-manifest.json
@@ -9,5 +9,40 @@
"description": "GlobalSign Atlas plugin for the AnyCA REST Gateway framework",
"gateway_framework": "24.2.0",
"release_dir": "globalsign-atlas-caplugin/bin/Release",
- "release_project": "globalsign-atlas-caplugin/globalsign-atlas-caplugin.csproj"
+ "release_project": "globalsign-atlas-caplugin/globalsign-atlas-caplugin.csproj",
+ "about": {
+ "carest": {
+ "product_ids": [
+ "certificate"
+ ],
+ "ca_plugin_config": [
+ {
+ "name": "ApiKey",
+ "description": "The API key for the Atlas credentials the gateway will use."
+ },
+ {
+ "name": "ApiSecret",
+ "description": "The corresponding API secret value that matches with the ApiKey."
+ },
+ {
+ "name": "ClientCertificate",
+ "description": "The client auth certificate to use with the Atlas API"
+ },
+ {
+ "name": "SyncStartDate",
+ "description": "The earliest date to go back when doing a full sync."
+ }
+ ],
+ "enrollment_config": [
+ {
+ "name": "Lifetime",
+ "description": "The term length (in days) to use for enrollment."
+ },
+ {
+ "name": "KeyUsage",
+ "description": "The key usage to use for enrolled certs. Valid values are 'client', 'server', and 'clientserver'."
+ }
+ ]
+ }
+ }
}
\ No newline at end of file
From 88c451c86f0c699d9b06ab7b9483f2750749f34b Mon Sep 17 00:00:00 2001
From: David Galey
Date: Tue, 23 Jun 2026 12:59:01 -0400
Subject: [PATCH 08/15] net10
---
globalsign-atlas-caplugin/globalsign-atlas-caplugin.csproj | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/globalsign-atlas-caplugin/globalsign-atlas-caplugin.csproj b/globalsign-atlas-caplugin/globalsign-atlas-caplugin.csproj
index 62f74d2..65fcadc 100644
--- a/globalsign-atlas-caplugin/globalsign-atlas-caplugin.csproj
+++ b/globalsign-atlas-caplugin/globalsign-atlas-caplugin.csproj
@@ -1,7 +1,7 @@
- net8.0
+ net8.0;net10.0
Keyfactor.Extensions.CAPlugin.GlobalSign.Atlas
enable
disable
From 3b39b4aca84db31dc83c84ad937f7d57d943667d Mon Sep 17 00:00:00 2001
From: David Galey
Date: Thu, 25 Jun 2026 15:07:53 -0400
Subject: [PATCH 09/15] fix for csr encoding
---
globalsign-atlas-caplugin/AtlasCAPlugin.cs | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/globalsign-atlas-caplugin/AtlasCAPlugin.cs b/globalsign-atlas-caplugin/AtlasCAPlugin.cs
index 1c4217c..e16a7ae 100644
--- a/globalsign-atlas-caplugin/AtlasCAPlugin.cs
+++ b/globalsign-atlas-caplugin/AtlasCAPlugin.cs
@@ -45,7 +45,7 @@ public async Task Enroll(string csr, string subject, Dictionar
_logger.MethodEntry(LogLevel.Trace);
AtlasClient client = AtlasClient.InitializeClient(_config, _certResolver);
Enroll enrollData = new Enroll();
- csr = PemUtilities.DERToPEM(Convert.FromBase64String(csr), PemUtilities.PemObjectType.CertRequest);
+
enrollData.CSR = csr;
var validation = client.GetValidationPolicy();
From c93220692672577cd80438eb214e6bc649a22c3f Mon Sep 17 00:00:00 2001
From: Dave Galey <89407235+dgaley@users.noreply.github.com>
Date: Thu, 25 Jun 2026 15:25:33 -0400
Subject: [PATCH 10/15] Update keyfactor-bootstrap-workflow-v3.yml
---
.github/workflows/keyfactor-bootstrap-workflow-v3.yml | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/.github/workflows/keyfactor-bootstrap-workflow-v3.yml b/.github/workflows/keyfactor-bootstrap-workflow-v3.yml
index 64919a4..b72a703 100644
--- a/.github/workflows/keyfactor-bootstrap-workflow-v3.yml
+++ b/.github/workflows/keyfactor-bootstrap-workflow-v3.yml
@@ -11,7 +11,7 @@ on:
jobs:
call-starter-workflow:
- uses: keyfactor/actions/.github/workflows/starter.yml@v3
+ uses: keyfactor/actions/.github/workflows/starter.yml@v5
secrets:
token: ${{ secrets.V2BUILDTOKEN}}
APPROVE_README_PUSH: ${{ secrets.APPROVE_README_PUSH}}
From 581f9f57a3c23c582c65ebaa5dfce84d0d8b55e9 Mon Sep 17 00:00:00 2001
From: Dave Galey <89407235+dgaley@users.noreply.github.com>
Date: Thu, 25 Jun 2026 15:26:08 -0400
Subject: [PATCH 11/15] Update keyfactor-bootstrap-workflow-v3.yml
---
.github/workflows/keyfactor-bootstrap-workflow-v3.yml | 1 -
1 file changed, 1 deletion(-)
diff --git a/.github/workflows/keyfactor-bootstrap-workflow-v3.yml b/.github/workflows/keyfactor-bootstrap-workflow-v3.yml
index b72a703..45eec87 100644
--- a/.github/workflows/keyfactor-bootstrap-workflow-v3.yml
+++ b/.github/workflows/keyfactor-bootstrap-workflow-v3.yml
@@ -14,7 +14,6 @@ jobs:
uses: keyfactor/actions/.github/workflows/starter.yml@v5
secrets:
token: ${{ secrets.V2BUILDTOKEN}}
- APPROVE_README_PUSH: ${{ secrets.APPROVE_README_PUSH}}
gpg_key: ${{ secrets.KF_GPG_PRIVATE_KEY }}
gpg_pass: ${{ secrets.KF_GPG_PASSPHRASE }}
scan_token: ${{ secrets.SAST_TOKEN }}
From c7d4a8955d09c9c0c059123866c34c393bff809d Mon Sep 17 00:00:00 2001
From: "github-actions[bot]"
Date: Thu, 25 Jun 2026 19:26:35 +0000
Subject: [PATCH 12/15] docs: auto-generate README and documentation [skip ci]
---
README.md | 23 ++++++++++-------------
1 file changed, 10 insertions(+), 13 deletions(-)
diff --git a/README.md b/README.md
index 2185c80..12405fb 100644
--- a/README.md
+++ b/README.md
@@ -14,7 +14,7 @@
Support
-
+
·
Requirements
@@ -33,7 +33,6 @@
-
TODO Overview is a required section
## Compatibility
@@ -41,7 +40,7 @@ TODO Overview is a required section
The GlobalSign Atlas AnyCA Gateway REST plugin is compatible with the Keyfactor AnyCA Gateway REST 24.2.0 and later.
## Support
-The GlobalSign Atlas 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 GlobalSign Atlas 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.
@@ -55,16 +54,16 @@ TODO Requirements is a required section
2. On the server hosting the AnyCA Gateway REST, download and unzip the latest [GlobalSign Atlas AnyCA Gateway REST plugin](https://github.com/Keyfactor/globalsign-atlas-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 GlobalSign Atlas 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 GlobalSign Atlas 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.
@@ -82,10 +81,10 @@ TODO Requirements is a required section
Populate using the configuration fields collected in the [requirements](#requirements) section.
- * **ApiKey** - The API key for the Atlas credentials the gateway will use.
- * **ApiSecret** - The corresponding API secret value that matches with the ApiKey.
- * **ClientCertificate** - The client auth certificate to use with the Atlas API
- * **SyncStartDate** - The earliest date to go back when doing a full sync.
+ * **ApiKey** - The API key for the Atlas credentials the gateway will use.
+ * **ApiSecret** - The corresponding API secret value that matches with the ApiKey.
+ * **ClientCertificate** - The client auth certificate to use with the Atlas API
+ * **SyncStartDate** - The earliest date to go back when doing a full sync.
2. TODO Certificate Template Creation Step is an optional section. If this section doesn't seem necessary on initial glance, please delete it. Refer to the docs on [Confluence](https://keyfactor.atlassian.net/wiki/x/SAAyHg) for more info
@@ -93,12 +92,10 @@ TODO Requirements is a required section
4. TODO Custom Enrollment Parameter Creation Step is an optional section. If this section doesn't seem necessary on initial glance, please delete it. Refer to the docs on [Confluence](https://keyfactor.atlassian.net/wiki/x/SAAyHg) for more info
-
-
## 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).
From 8fbeb474858acea0d8823b94ffbac8b8a636d597 Mon Sep 17 00:00:00 2001
From: David Galey
Date: Mon, 29 Jun 2026 14:12:34 -0400
Subject: [PATCH 13/15] docs
---
docsource/configuration.md | 20 +++++++-------------
globalsign-atlas-caplugin.sln | 5 +++--
globalsign-atlas-caplugin/AtlasCAPlugin.cs | 1 +
3 files changed, 11 insertions(+), 15 deletions(-)
diff --git a/docsource/configuration.md b/docsource/configuration.md
index 71fb0e2..be92150 100644
--- a/docsource/configuration.md
+++ b/docsource/configuration.md
@@ -1,24 +1,18 @@
## Overview
-TODO Overview is a required section
+The GlobalSign Atlas AnyCA Gateway REST plugin extends the capabilities of GlobalSign Atlas to Keyfactor Command via the Keyfactor AnyCA Gateway REST. The plugin has the following capabilities:
+* SSl Certificate Synchronization
+* SSL Certificate Enrollment
+* SSL Certificate Revocation
## Requirements
-TODO Requirements is a required section
+To use the GlobalSign Atlas AnyCA Gateway, you must generate a set of API Credentials (key/secret) within the Atlas portal, as well as an mTLS certificate linked to those credentials.
## Gateway Registration
-TODO Gateway Registration is a required section
+In order to enroll for certificates the Keyfactor Command server must trust the trust chain. Once you configure your Root and/or Subordinate CA in your Atlas account, make sure to download and import the certificate chain into the Command Server certificate store
## Certificate Template Creation Step
-TODO Certificate Template Creation Step is an optional section. If this section doesn't seem necessary on initial glance, please delete it. Refer to the docs on [Confluence](https://keyfactor.atlassian.net/wiki/x/SAAyHg) for more info
-
-## Custom Enrollment Parameter Creation Step
-
-TODO Custom Enrollment Parameter Creation Step is an optional section. If this section doesn't seem necessary on initial glance, please delete it. Refer to the docs on [Confluence](https://keyfactor.atlassian.net/wiki/x/SAAyHg) for more info
-
-## Mechanics
-
-TODO Mechanics is an optional section. If this section doesn't seem necessary on initial glance, please delete it. Refer to the docs on [Confluence](https://keyfactor.atlassian.net/wiki/x/SAAyHg) for more info
-
+When defining templates, the product ID should just be defined as "certificate".
\ No newline at end of file
diff --git a/globalsign-atlas-caplugin.sln b/globalsign-atlas-caplugin.sln
index 411dafc..3257d35 100644
--- a/globalsign-atlas-caplugin.sln
+++ b/globalsign-atlas-caplugin.sln
@@ -1,13 +1,14 @@
Microsoft Visual Studio Solution File, Format Version 12.00
-# Visual Studio Version 17
-VisualStudioVersion = 17.14.36705.20
+# Visual Studio Version 18
+VisualStudioVersion = 18.7.11919.86 stable
MinimumVisualStudioVersion = 10.0.40219.1
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "globalsign-atlas-caplugin", "globalsign-atlas-caplugin\globalsign-atlas-caplugin.csproj", "{292B7A34-CFEC-4ABB-8BE6-31F87BC1651B}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{8EC462FD-D22E-90A8-E5CE-7E832BA40C5D}"
ProjectSection(SolutionItems) = preProject
CHANGELOG.md = CHANGELOG.md
+ docsource\configuration.md = docsource\configuration.md
integration-manifest.json = integration-manifest.json
EndProjectSection
EndProject
diff --git a/globalsign-atlas-caplugin/AtlasCAPlugin.cs b/globalsign-atlas-caplugin/AtlasCAPlugin.cs
index e16a7ae..ce04e1a 100644
--- a/globalsign-atlas-caplugin/AtlasCAPlugin.cs
+++ b/globalsign-atlas-caplugin/AtlasCAPlugin.cs
@@ -357,6 +357,7 @@ public async Task Synchronize(BlockingCollection blockin
{
var anyCACert = new AnyCAPluginCertificate
{
+ ProductID = "certificate",
CARequestID = cert.Status.SerialNumber,
Certificate = cert.Cert.Certificate,
Status = cert.Cert.Status.Equals("issued", StringComparison.OrdinalIgnoreCase) ? (int)EndEntityStatus.GENERATED :
From 9d11affd4e6d8e2fa6b28cbcbd535a8ba7572b76 Mon Sep 17 00:00:00 2001
From: "github-actions[bot]"
Date: Mon, 29 Jun 2026 18:13:06 +0000
Subject: [PATCH 14/15] docs: auto-generate README and documentation [skip ci]
---
README.md | 16 +++++++++++-----
1 file changed, 11 insertions(+), 5 deletions(-)
diff --git a/README.md b/README.md
index 12405fb..9c1605a 100644
--- a/README.md
+++ b/README.md
@@ -33,7 +33,10 @@
-TODO Overview is a required section
+The GlobalSign Atlas AnyCA Gateway REST plugin extends the capabilities of GlobalSign Atlas to Keyfactor Command via the Keyfactor AnyCA Gateway REST. The plugin has the following capabilities:
+* SSl Certificate Synchronization
+* SSL Certificate Enrollment
+* SSL Certificate Revocation
## Compatibility
@@ -46,7 +49,7 @@ The GlobalSign Atlas AnyCA Gateway REST plugin is supported by Keyfactor for Key
## Requirements
-TODO Requirements is a required section
+To use the GlobalSign Atlas AnyCA Gateway, you must generate a set of API Credentials (key/secret) within the Atlas portal, as well as an mTLS certificate linked to those credentials.
## Installation
@@ -75,7 +78,7 @@ TODO Requirements is a required section
* **Gateway Registration**
- TODO Gateway Registration is a required section
+ In order to enroll for certificates the Keyfactor Command server must trust the trust chain. Once you configure your Root and/or Subordinate CA in your Atlas account, make sure to download and import the certificate chain into the Command Server certificate store
* **CA Connection**
@@ -86,11 +89,14 @@ TODO Requirements is a required section
* **ClientCertificate** - The client auth certificate to use with the Atlas API
* **SyncStartDate** - The earliest date to go back when doing a full sync.
-2. TODO Certificate Template Creation Step is an optional section. If this section doesn't seem necessary on initial glance, please delete it. Refer to the docs on [Confluence](https://keyfactor.atlassian.net/wiki/x/SAAyHg) for more info
+2. When defining templates, the product ID should just be defined as "certificate".
3. Follow the [official Keyfactor documentation](https://software.keyfactor.com/Guides/AnyCAGatewayREST/Content/AnyCAGatewayREST/AddCA-Keyfactor.htm) to add each defined Certificate Authority to Keyfactor Command and import the newly defined Certificate Templates.
-4. TODO Custom Enrollment Parameter Creation Step is an optional section. If this section doesn't seem necessary on initial glance, please delete it. Refer to the docs on [Confluence](https://keyfactor.atlassian.net/wiki/x/SAAyHg) for more info
+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:
+
+ * **Lifetime** - The term length (in days) to use for enrollment.
+ * **KeyUsage** - The key usage to use for enrolled certs. Valid values are 'client', 'server', and 'clientserver'.
## License
From 86cdc1f9d726569ee95e4c620369d42c48ff28c4 Mon Sep 17 00:00:00 2001
From: David Galey
Date: Wed, 8 Jul 2026 12:36:25 -0400
Subject: [PATCH 15/15] add Enable flag
---
globalsign-atlas-caplugin/AtlasCAPlugin.cs | 13 +++++++++++++
globalsign-atlas-caplugin/AtlasConfig.cs | 3 +++
globalsign-atlas-caplugin/Client/AtlasClient.cs | 8 ++++++++
3 files changed, 24 insertions(+)
diff --git a/globalsign-atlas-caplugin/AtlasCAPlugin.cs b/globalsign-atlas-caplugin/AtlasCAPlugin.cs
index ce04e1a..c08c907 100644
--- a/globalsign-atlas-caplugin/AtlasCAPlugin.cs
+++ b/globalsign-atlas-caplugin/AtlasCAPlugin.cs
@@ -279,6 +279,13 @@ public Dictionary GetCAConnectorAnnotations()
Hidden = false,
DefaultValue = "2024-01-01",
Type = "String"
+ },
+ ["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.",
+ Hidden = false,
+ DefaultValue = true,
+ Type = "Boolean"
}
};
}
@@ -326,6 +333,12 @@ public Dictionary GetTemplateParameterAnnotations()
public async Task Ping()
{
+ if (!_config.Enabled)
+ {
+ _logger.LogWarning($"The CA is currently in the Disabled state. It must be Enabled to perform operations. Skipping connectivity test...");
+ _logger.MethodExit(LogLevel.Trace);
+ return;
+ }
try
{
AtlasClient client = AtlasClient.InitializeClient(_config, _certResolver);
diff --git a/globalsign-atlas-caplugin/AtlasConfig.cs b/globalsign-atlas-caplugin/AtlasConfig.cs
index 6c3ff89..2283063 100644
--- a/globalsign-atlas-caplugin/AtlasConfig.cs
+++ b/globalsign-atlas-caplugin/AtlasConfig.cs
@@ -25,5 +25,8 @@ public AtlasConfig() { }
[JsonProperty("SyncStartDate")]
public string SyncStartDate { get; set; }
+
+ [JsonProperty("Enabled")]
+ public bool Enabled { get; set; } = true;
}
}
diff --git a/globalsign-atlas-caplugin/Client/AtlasClient.cs b/globalsign-atlas-caplugin/Client/AtlasClient.cs
index 468a76d..a77ef94 100644
--- a/globalsign-atlas-caplugin/Client/AtlasClient.cs
+++ b/globalsign-atlas-caplugin/Client/AtlasClient.cs
@@ -22,6 +22,8 @@
using System.Text;
using System.Threading.Tasks;
+using static Org.BouncyCastle.Math.EC.ECCurve;
+
using CertificateResponse = Keyfactor.Extensions.CAPlugin.GlobalSign.Atlas.APIProxy.CertificateResponse;
namespace Keyfactor.Extensions.CAPlugin.GlobalSign.Atlas.Client
@@ -69,6 +71,12 @@ public AtlasClient(string apiKey, string apiSecret, X509Certificate2 authCert, D
public static AtlasClient InitializeClient(AtlasConfig config, ICertificateResolver certResolver)
{
Logger.MethodEntry(LogLevel.Debug);
+ if (!config.Enabled)
+ {
+ Logger.LogWarning($"The CA is currently in the Disabled state. It must be Enabled to perform operations. Skipping connectivity test...");
+ Logger.MethodExit(LogLevel.Trace);
+ throw new Exception($"The CA is currently in the Disabled state. It must be Enabled to perform operations.");
+ }
Logger.LogTrace($"Retrieving auth certificate");
X509Certificate2 authCert = null;
if (!string.IsNullOrEmpty(config.Certificate.ImportedCertificate))