diff --git a/.github/workflows/keyfactor-bootstrap-workflow-v3.yml b/.github/workflows/keyfactor-bootstrap-workflow-v3.yml
new file mode 100644
index 0000000..45eec87
--- /dev/null
+++ b/.github/workflows/keyfactor-bootstrap-workflow-v3.yml
@@ -0,0 +1,19 @@
+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@v5
+ secrets:
+ token: ${{ secrets.V2BUILDTOKEN}}
+ gpg_key: ${{ secrets.KF_GPG_PRIVATE_KEY }}
+ gpg_pass: ${{ secrets.KF_GPG_PASSPHRASE }}
+ scan_token: ${{ secrets.SAST_TOKEN }}
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/README.md b/README.md
index 45f8331..9c1605a 100644
--- a/README.md
+++ b/README.md
@@ -1 +1,107 @@
-# globalsign-atlas-caplugin
\ No newline at end of file
+
+ GlobalSign Atlas AnyCA Gateway REST Plugin
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Support
+
+ ·
+
+ Requirements
+
+ ·
+
+ Installation
+
+ ·
+
+ License
+
+ ·
+
+ Related Integrations
+
+
+
+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
+
+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 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
+
+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
+
+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 `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\net8.0\Extensions
+ Program Files\Keyfactor\AnyCA Gateway\AnyGatewayREST\net10.0\Extensions
+ ```
+
+ > 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.
+
+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**
+
+ 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**
+
+ 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. 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. 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
+
+Apache License 2.0, see [LICENSE](LICENSE).
+
+## Related Integrations
+
+See all [Keyfactor Any CA Gateways (REST)](https://github.com/orgs/Keyfactor/repositories?q=anycagateway).
diff --git a/docsource/configuration.md b/docsource/configuration.md
new file mode 100644
index 0000000..be92150
--- /dev/null
+++ b/docsource/configuration.md
@@ -0,0 +1,18 @@
+## Overview
+
+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
+
+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
+
+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
+
+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
new file mode 100644
index 0000000..3257d35
--- /dev/null
+++ b/globalsign-atlas-caplugin.sln
@@ -0,0 +1,32 @@
+
+Microsoft Visual Studio Solution File, Format Version 12.00
+# 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
+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..c08c907
--- /dev/null
+++ b/globalsign-atlas-caplugin/AtlasCAPlugin.cs
@@ -0,0 +1,449 @@
+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();
+
+ 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"
+ },
+ ["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"
+ }
+ };
+ }
+
+ 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()
+ {
+ 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);
+ _ = 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
+ {
+ ProductID = "certificate",
+ 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..2283063
--- /dev/null
+++ b/globalsign-atlas-caplugin/AtlasConfig.cs
@@ -0,0 +1,32 @@
+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; }
+
+ [JsonProperty("Enabled")]
+ public bool Enabled { get; set; } = true;
+ }
+}
diff --git a/globalsign-atlas-caplugin/Client/AtlasClient.cs b/globalsign-atlas-caplugin/Client/AtlasClient.cs
new file mode 100644
index 0000000..a77ef94
--- /dev/null
+++ b/globalsign-atlas-caplugin/Client/AtlasClient.cs
@@ -0,0 +1,628 @@
+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 static Org.BouncyCastle.Math.EC.ECCurve;
+
+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);
+ 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))
+ {
+ 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..65fcadc
--- /dev/null
+++ b/globalsign-atlas-caplugin/globalsign-atlas-caplugin.csproj
@@ -0,0 +1,24 @@
+
+
+
+ net8.0;net10.0
+ Keyfactor.Extensions.CAPlugin.GlobalSign.Atlas
+ enable
+ disable
+ AtlasCAPlugin
+
+
+
+
+
+
+
+
+
+
+
+ Always
+
+
+
+
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"
+ }
+ }
+ }
+}
diff --git a/integration-manifest.json b/integration-manifest.json
new file mode 100644
index 0000000..a51f121
--- /dev/null
+++ b/integration-manifest.json
@@ -0,0 +1,48 @@
+{
+ "$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",
+ "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