Chain { get; }
+
+ ///
+ /// FriendlyName
+ ///
+ string FriendlyName { get; }
+
+ ///
+ /// Main certificate hash
+ ///
+ byte[] GetHash();
+
+ ///
+ /// Main certificate thumbprint
+ ///
+ string Thumbprint { get; }
+ }
+}
\ No newline at end of file
diff --git a/AcmeCaPlugin/manifest.json b/AcmeCaPlugin/manifest.json
new file mode 100644
index 0000000..48e4401
--- /dev/null
+++ b/AcmeCaPlugin/manifest.json
@@ -0,0 +1,10 @@
+{
+ "extensions": {
+ "Keyfactor.AnyGateway.Extensions.IAnyCAPlugin": {
+ "AcmeCaPlugin": {
+ "assemblypath": "AcmeCaPlugin.dll",
+ "TypeFullName": "Keyfactor.Extensions.CAPlugin.Acme.AcmeCaPlugin"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/CHANGELOG.md b/CHANGELOG.md
index e69de29..4c9a3ca 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -0,0 +1,2 @@
+# v1.0.0
+* Initial Release. Support for Acme. Enroll and Revocation
diff --git a/LICENSE.txt b/LICENSE.txt
new file mode 100644
index 0000000..261eeb9
--- /dev/null
+++ b/LICENSE.txt
@@ -0,0 +1,201 @@
+ Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
+
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+ 1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+ 2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+ 3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+ 4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+ 6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+ 7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+ 8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+ 9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+ END OF TERMS AND CONDITIONS
+
+ APPENDIX: How to apply the Apache License to your work.
+
+ To apply the Apache License to your work, attach the following
+ boilerplate notice, with the fields enclosed by brackets "[]"
+ replaced with your own identifying information. (Don't include
+ the brackets!) The text should be enclosed in the appropriate
+ comment syntax for the file format. We also recommend that a
+ file or class name and description of purpose be included on the
+ same "printed page" as the copyright notice for easier
+ identification within third-party archives.
+
+ Copyright [yyyy] [name of copyright owner]
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
diff --git a/README.md b/README.md
index 16f2a3e..5762a55 100644
--- a/README.md
+++ b/README.md
@@ -1,93 +1,574 @@
-# cpr-cagateway-template
+
+ Acme AnyCA Gateway REST Plugin
+
-## Template for new CA Gateway integrations
+
+
+
+
+
+
+
-### Use this repository to create new integrations for new CA Gateway integration types.
+
+
+
+ Support
+
+ ·
+
+ Requirements
+
+ ·
+
+ Installation
+
+ ·
+
+ License
+
+ ·
+
+ Related Integrations
+
+
-1. [Use this repository](#using-the-repository)
-1. [Update the integration-manifest.json](#updating-the-integration-manifest.json)
-1. [Add Keyfactor Bootstrap Workflow (keyfactor-bootstrap-workflow.yml)](#add-bootstrap)
-1. [Create required branches](#create-required-branches)
-1. [Replace template files/folders](#replace-template-files-and-folders)
-1. [Create initial prerelease](#create-initial-prerelease)
----
+The **Keyfactor ACME CA Gateway Plugin** enables certificate enrollment using the [ACME protocol (RFC 8555)](https://datatracker.ietf.org/doc/html/rfc8555), providing automated certificate issuance via any compliant Certificate Authority. This plugin is designed for **enrollment-only workflows** — it **does not support synchronization or revocation** of certificates.
-#### Using the repository
-1. Select the ```Use this template``` button at the top of this page
-1. Update the repository name following [these guidelines](https://keyfactorinc.sharepoint.com/sites/IntegrationWiki/SitePages/GitHub-Processes.aspx#repository-naming-conventions)
- 1. All repositories must be in lower-case
- 1. General pattern: company-product-type
- 1. e.g. hashicorp-vault-orchestator
-1. Click the ```Create repository``` button
+### 🔧 What It Does
+This plugin allows Keyfactor Gateways to:
+- Submit CSRs to ACME-based CAs.
+- Complete domain validation via DNS-01 challenges.
+- Automatically retrieve and return signed certificates.
----
+Once a certificate is issued, the plugin returns the PEM-encoded certificate to the Gateway.
+
+### ✅ ACME Providers Tested
+This plugin has been tested and confirmed to work with the following ACME providers:
+- **Let's Encrypt**
+- **Google ACME (Certificate Authority Service)**
+- **ZeroSSL** (functional but known slowness may cause timeouts)
+- **Buypass**
-#### Updating the integration-manifest.json
+It is designed to be provider-agnostic and should work with any standards-compliant ACME server.
-*The following properties must be updated in the integration-manifest.json*
+### 🌐 Supported DNS Providers (Initial Release)
+DNS-01 challenge automation is supported through the following providers:
+- **Google Cloud DNS**
+- **AWS Route 53**
+- **Azure DNS**
+- **Cloudflare**
+- **NS1**
-Clone the repository locally, use vsdev.io, or the GitHub online editor to update the file.
+Additional DNS providers can be added by extending the included `IDnsProvider` interface.
-* "name": "Friendly name for the integration"
- * This will be used in the readme file generation and catalog entries
-* "description": "Brief description of the integration."
- * This will be used in the readme file generation
- * If the repository description is empty this value will be used for the repository description upon creating a release branch
-* "release_dir": "PATH\\\TO\\\BINARY\\\RELEASE\\\OUTPUT\\\FOLDER"
- * Path separators can be "\\\\" or "/"
- * Be sure to specify the release folder name. This can be found by running a Release build and noting the output folder
- * Example: "AzureAppGatewayOrchestrator\\bin\\Release"
-* "gateway_framework": "" string denoting the required command gateway framework version
---
-#### Add Bootstrap
-Add Keyfactor Bootstrap Workflow (keyfactor-bootstrap-workflow.yml). This can be copied directly from the workflow templates or through the Actions tab
-* Directly:
- 1. Create a file named ```.github\workflows\keyfactor-bootstrap-workflow.yml```
- 1. Copy the contents of [keyfactor/.github/workflow-templates/keyfactor-bootstrap-workflow.yml](https://raw.githubusercontent.com/Keyfactor/.github/main/workflow-templates/keyfactor-bootstrap-workflow.yml) into the file created in the previous step
-* Actions tab:
- 1. Navigate to the [Actions tab](./actions) in the new repository
- 1. Click the ```New workflow``` button
- 1. Find the ```Keyfactor Bootstrap Workflow``` and click the ```Configure``` button
- 1. Click the ```Commit changes...``` button on this screen and the next to add the bootstrap workflow to the main branch
-
-A new build will run the tasks of a *Push* trigger on the main branch
-
-*Ensure there are no errors during the workflow run in the Actions tab.*
+### 🔁 Enrollment Flow Summary
----
+```text
+1. Keyfactor Gateway submits CSR and SAN metadata to plugin.
+2. Plugin initializes ACME client and creates a new order.
+3. For each domain:
+ a. Retrieve DNS-01 challenge.
+ b. Use the configured DNS provider to publish challenge record.
+ c. Wait for DNS propagation and validate record.
+ d. Notify ACME provider to trigger validation.
+4. Once all challenges are valid, finalize the order using CSR.
+5. Download the signed certificate from ACME provider.
+6. Return PEM certificate to the Gateway.
+```
+
+The plugin uses a modular design that separates ACME communication logic and DNS challenge automation, allowing for future extensibility in both areas.
+
+> ⚠️ Revocation, certificate synchronization, and renewal tracking are intentionally **not implemented** in this plugin. All lifecycle tracking must be handled externally (e.g., via Keyfactor monitoring or Gateway automation).
+
+## Compatibility
+
+The Acme AnyCA Gateway REST plugin is compatible with the Keyfactor AnyCA Gateway REST 24.2 and later.
+
+## Support
+The Acme 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
+
+### DNS Providers
+
+This plugin automates DNS-01 challenges using pluggable DNS provider implementations. These providers create and remove TXT records to prove domain control to ACME servers.
+
+
+✅ Supported DNS Providers (Initial Release)
+
+| Provider | Auth Methods Supported | Config Keys Required |
+|--------------|-----------------------------------------------|--------------------------------------------------------|
+| Google DNS | Service Account Key or ADC | `Google_ServiceAccountKeyPath`, `Google_ProjectId` |
+| AWS Route 53 | Access Key/Secret or IAM Role | `AwsRoute53_AccessKey`, `AwsRoute53_SecretKey` |
+| Azure DNS | Client Secret or Managed Identity | `Azure_TenantId`, `Azure_ClientId`, `Azure_ClientSecret`, `Azure_SubscriptionId` |
+| Cloudflare | API Token | `Cloudflare_ApiToken` |
+| NS1 | API Key | `Ns1_ApiKey` |
+
+
+
+
+⏱ DNS Propagation Logic
+
+Before submitting ACME challenges, the plugin verifies DNS propagation using multiple public resolvers (Google, Cloudflare, OpenDNS, Quad9). A record must be visible on **at least 3 servers** to proceed, with up to **3 retries** spaced by 10 seconds.
+
+This logic is handled by the `DnsVerificationHelper` class and ensures a high-confidence validation before proceeding.
+
+
+
+
+🔑 Credential Flow
+
+Each provider supports multiple credential strategies:
+
+- **Google DNS**:
+ - ✅ **Service Account Key** (via `Google_ServiceAccountKeyPath`)
+ - ✅ **Application Default Credentials** (e.g., GCP Workload Identity or developer auth)
+
+- **AWS Route 53**:
+ - ✅ **Access/Secret Keys** (`AwsRoute53_AccessKey`, `AwsRoute53_SecretKey`)
+ - ✅ **IAM Role via EC2 Instance Metadata** (no explicit credentials)
+
+- **Azure DNS**:
+ - ✅ **Client Secret** (explicit `TenantId`, `ClientId`, `ClientSecret`)
+ - ✅ **Managed Identity** or environment-based credentials via `DefaultAzureCredential`
+
+- **Cloudflare**:
+ - ✅ **Bearer API Token** for zone-level DNS control
+
+- **NS1**:
+ - ✅ **API Key** passed in header `X-NSONE-Key`
+
+
+
+
+🧩 Adding New DNS Providers
+
+To add support for new DNS services:
+
+1. Implement the `IDnsProvider` interface:
+ ```csharp
+ public interface IDnsProvider
+ {
+ Task CreateRecordAsync(string recordName, string txtValue);
+ Task DeleteRecordAsync(string recordName);
+ }
+ ```
+
+2. Register the new provider in the `DnsProviderFactory`:
+ ```csharp
+ case "yourprovider":
+ return new YourCustomDnsProvider(config.YourProviderConfigValues...);
+ ```
+
+3. Use zone detection logic similar to `GoogleDnsProvider`, `AzureDnsProvider`, or `Ns1DnsProvider`.
+
+Each provider is instantiated dynamically based on the `DnsProvider` field in the `AcmeClientConfig`.
+
+> 🔁 This modular DNS system ensures challenge automation works across cloud providers and is easily extensible.
+
+
+
+
+🔒 CA-Level DNS Provider Binding
+
+Each ACME/DNS combination is supported **at the CA level**, meaning that only **one DNS provider** is configured per CA entry in Keyfactor. This ensures a clear and isolated challenge path for each ACME CA connector instance.
+
+If you need to support multiple DNS zones/providers (e.g., both AWS and Cloudflare), configure **separate CA entries**, each with its own DNS provider configuration.
+
+
+
+
+🚫 No Offline Challenge Retry (Initial Release)
+
+In this initial release, there is **no background or offline retry** for ACME challenges that timeout. If DNS propagation takes too long and the challenge is not verified in time, the certificate **request will fail immediately**.
+
+> ⚠️ However, in testing across all supported DNS providers and ACME services (e.g., Let's Encrypt, Google CAS, ZeroSSL, Buypass), propagation has been fast enough to avoid these timeouts in all observed cases.
-#### Create required branches
-1. Create a release branch from main: release-1.0
-1. Create a dev branch from the starting with the devops id in the format ab#\, e.g. ab#53535.
- 1. For the cleanest pull request merge, create the dev branch from the release branch.
- 1. Optionally, add a suffix to the branch name indicating initial release. e.g. ab#53535-initial-release
+
---
+### ACME Provider Configuration
-#### Replace template files and folders
-1. Replace the contents of readme_source.md
-1. Create a CHANGELOG.md file in the root of the repository indicating ```1.0: Initial release```
-1. Replace the SampleOrchestratorExtension.sln solution file and SampleOrchestratorExtension folder with your new orchestrator dotnet solution
-1. Push your updates to the dev branch (ab#xxxxx)
+Each ACME CA (Certificate Authority) has slightly different expectations for account creation and request handling. This plugin supports multiple providers and dynamically handles credentials based on your configuration.
+
+
+🧩 External Account Binding (EAB) Support
+
+Some providers **require** External Account Binding (EAB), which includes:
+- `eabKid`: External Account Binding Key ID
+- `eabHmacKey`: HMAC Key to sign the JWK thumbprint
+
+Others **do not require EAB**, and can create accounts automatically with just an email address.
+
+
+
+
+✅ Supported Providers & Credential Expectations
+
+| Provider | Directory URL | Requires EAB | Notes |
+|----------------|----------------------------------------------------------------|--------------|-----------------------------------------------------------------------|
+| Let's Encrypt | `https://acme-v02.api.letsencrypt.org/directory` | ❌ No | Free and public; account created using only an email address |
+| Buypass | `https://api.buypass.com/acme/directory` | ❌ No | Free and public; supports long-lived certs; no EAB required |
+| ZeroSSL | `https://acme.zerossl.com/v2/DV90/directory` | ✅ Yes | Requires EAB; keys available via [ZeroSSL Developer Portal](https://zerossl.com) |
+| Google CAS | `https://dv.acme-v02.api.pki.goog/directory` | ✅ Yes | Requires EAB; keys issued via [Google CAS UI](https://console.cloud.google.com) |
+
+> ⚠️ If a provider requires EAB and it is not supplied, the request will fail during account registration.
+
+
+
+
+📋 Configuration Fields (Per ACME Provider)
+
+These values are set in the Keyfactor Command Gateway Configuration UI for each ACME provider:
+
+| Field | Description | Required |
+|---------------|---------------------------------------------------|-----------------|
+| `directoryUrl`| The full ACME directory URL for the CA | ✅ Yes |
+| `email` | Account email address for ACME registration | ✅ Yes |
+| `eabKid` | External Account Binding Key ID (if applicable) | 🚫 Only if EAB |
+| `eabHmacKey` | HMAC key used to sign EAB binding (if applicable) | 🚫 Only if EAB |
+
+
+
+
+🔐 How to Get EAB Credentials
+
+- **ZeroSSL**:
+ Log into your account and go to **"ACME EAB Credentials"** in the developer section.
+
+- **Google CAS**:
+ Enable your CA Pool for ACME and generate EAB credentials under the **ACME Integration** tab in Google Cloud Console.
+
+
+
+
+⚙️ Plugin Behavior
+
+- If both `eabKid` and `eabHmacKey` are provided, they will be used to create the ACME account.
+- If either is omitted and the provider requires it, account creation will fail.
+- If neither is provided and the provider does not require EAB, the account will be created using only the email.
+
+Each provider is configured in the JSON config under `acmeProviders`, and only **one provider** is active per enrollment.
+
+
---
+### Account Storage and Signer Encryption
+
+This ACME Gateway implementation uses a local file-based store to persist ACME accounts and their associated cryptographic signers. Accounts are cached on disk using a structured format, and signers (private keys) can be encrypted with a passphrase for enhanced security.
+
+
+📁 Account Directory Structure
+
+Each account is saved in its own directory within:
+
+```
+%APPDATA%\AcmeAccounts\{host}_{accountId}
+```
+
+Where:
+- `{host}` is the ACME directory host with dots replaced by dashes (e.g., `acme-zerossl-com`)
+- `{accountId}` is the final segment of the account's KID URL
+
+
+
+
+📄 Files per Account
+
+- `Registration_v2`: Contains serialized `AccountDetails` in JSON format
+- `Signer_v2`: Contains encrypted or plaintext signer key material, depending on passphrase usage
+- `default_{host}.txt`: Tracks the default account for a given ACME directory host
+
+
+
+
+🔐 Encryption with Passphrase
+
+If the `SignerEncryptionPhrase` configuration value is set, the plugin encrypts signer files (`Signer_v2`) using AES with a PBKDF2-derived key and IV. The encrypted data includes a prepended salt and IV to support cross-platform decryption.
+
+```text
+[Salt (16 bytes)] [IV (16 bytes)] [AES-CBC encrypted signer JSON]
+```
+
+The encryption ensures that even if the account files are accessed on disk, the private keys remain unreadable without the configured passphrase.
+
+
+
+
+🔗 External Account Binding (EAB)
+
+For ACME providers requiring EAB (e.g., ZeroSSL, Google CAS), the gateway constructs a manually signed JWS payload containing:
+
+- Protected Header: `alg`, `kid`, `url`
+- Payload: Public JWK of the account signer
+- Signature: HMAC using `eabHmacKey`
+
+This JWS is included during account creation to bind the account to the pre-provisioned identity provided by the CA.
+
+
+
+
+⚙️ Algorithm Support
+
+- Signers support `ES256`, `ES384`, `ES512` (ECDSA) and `RS256`, `RS384`, `RS512` (RSA)
+- EAB HMAC support includes `HS256`, `HS384`, `HS512`
+
+If `ES256` key generation fails (e.g., due to platform constraints), the system automatically falls back to `RS256`.
+
+
+
+### Account Caching and Auto-Creation
+
+On startup or during enrollment/sync, the plugin:
+
+1. Attempts to load a cached account for the specified ACME directory.
+2. If no account is found, it automatically creates a new one, using EAB if configured.
+3. The new account is saved to disk and set as default for future use.
+
+
+🔗 External Account Binding (EAB)
+
+For ACME providers requiring EAB (e.g., ZeroSSL, Google CAS), the gateway constructs a manually signed JWS payload containing:
+
+- Protected Header: `alg`, `kid`, `url`
+- Payload: Public JWK of the account signer
+- Signature: HMAC using `eabHmacKey`
+
+This JWS is included during account creation to bind the account to the pre-provisioned identity provided by the CA.
+
+
+
+
+🔧 Algorithm Support
+
+- Signers support `ES256`, `ES384`, `ES512` (ECDSA) and `RS256`, `RS384`, `RS512` (RSA)
+- EAB HMAC support includes `HS256`, `HS384`, `HS512`
+
+If `ES256` key generation fails (e.g., due to platform constraints), the system automatically falls back to `RS256`.
+
+
+
+### Network and File System Requirements
+
+This section outlines all required ports, file access, permissions, and validation behaviors for operating the ACME Gateway Plugin in a Keyfactor Orchestrator environment.
+
+
+🔌 Port Usage
+
+#### Incoming Connections
+
+- **None.** This plugin does not expose any HTTP or network listeners.
+
+#### Outgoing Connections
+
+| Protocol | Port | Target | Purpose |
+|----------|------|------------------------------|-----------------------------------------------------|
+| HTTPS | 443 | ACME Directory URL | Connect to the ACME CA for account, challenge, and certificate operations |
+| HTTPS | 443 | DNS Provider APIs | Used for DNS-01 challenge automation (Google DNS, AWS, etc.) |
+
+
+
+
+💾 File System Requirements
+
+#### Directory Layout
+
+| Path | Purpose |
+|----------------------------------------------------|----------------------------------------------|
+| `%APPDATA%\AcmeAccounts\` | Default base path for ACME account storage |
+| `AcmeAccounts\{account_id}\Registration_v2` | Contains serialized ACME account metadata |
+| `AcmeAccounts\{account_id}\Signer_v2` | Contains the encrypted private signer key |
+| `AcmeAccounts\default_{host}.txt` | Stores the default account pointer for a given directory |
+
+#### File Access & Permissions
+
+| Path | Operation | Required Permission |
+|--------------------------|-----------|---------------------|
+| Account directory | Create | `Write` |
+| Account files | Read/Write| `Read`, `Write` |
+
+- Files may be optionally encrypted using AES if a passphrase is configured.
+- Ensure the service account under which the orchestrator runs has read/write access to `%APPDATA%` or the custom configured base path.
+
+
+
+
+👤 Windows Account Permissions
+
+- The orchestrator service account (usually `NT AUTHORITY\SYSTEM` or a custom `Network Service`) must have:
+ - File I/O permissions to read/write within the configured base directory.
+ - Network access to ACME CA endpoints and DNS APIs over HTTPS.
+ - DNS provider credentials (Cloudflare API token, Google credentials, etc.) stored securely.
+
+
+
+
+🌐 DNS Propagation Check Behavior
+
+- **Initial Release Behavior**:
+ - DNS challenge propagation is checked during the interactive enrollment phase only.
+ - If propagation takes too long (> 60s), the request will fail. No deferred background polling occurs.
+ - There is **no offline retry mechanism** (e.g., for sync jobs) to pick up completed validations that succeeded after a delay.
+
+- **Future Considerations**:
+ - Support for file-based or database-backed challenge persistence may be added to allow background sync to re-check and finalize challenge state.
+
+
+
+## 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 [Acme AnyCA Gateway REST plugin](https://github.com/Keyfactor/acme-provider-caplugin/releases/latest) from GitHub.
+
+3. Copy the unzipped directory (usually called `net6.0`) to the Extensions directory:
+
+ ```shell
+ Program Files\Keyfactor\AnyCA Gateway\AnyGatewayREST\net6.0\Extensions
+ ```
+
+ > The directory containing the Acme AnyCA Gateway REST plugin DLLs (`net6.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 Acme 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**
+
+ Each ACME CA issues certificates that chain to a specific intermediate and root certificate. For trust validation and proper integration with the Keyfactor Gateway, the following steps are required for **every ACME CA** used in your environment.
+
+ ---
+
+ ### 🔍 Retrieving Root and Intermediate Certificates
+
+ Here is how to obtain the root and intermediate CA certificates from supported ACME providers:
+
+ #### Let's Encrypt
+
+ - **Root**: ISRG Root X1
+ - **Intermediate**: R3
+
+ **How to Get:**
+ - Browse to: https://letsencrypt.org/certificates/
+ - Download both the **ISRG Root X1** and **R3 Intermediate Certificate (PEM format)**.
+
+ #### Google Certificate Authority Service (CAS)
+
+ - **Root** and **Intermediate** are custom per CA Pool.
+
+ **How to Get:**
+ 1. In the [Google Cloud Console](https://console.cloud.google.com/security/privateca), navigate to your CA pool.
+ 2. Click the CA name and go to the **Certificates** tab.
+ 3. Download the **root** and **intermediate** certificates for the issuing CA in PEM format.
+
+ #### ZeroSSL
+
+ - **Root**: USERTrust RSA Certification Authority
+ - **Intermediate**: ZeroSSL RSA Domain Secure Site CA
+
+ **How to Get:**
+ - Visit: https://zerossl.com
+ - Download the full certificate chain in PEM format.
+ - Extract individual certs if needed using OpenSSL or a text editor.
+
+ #### Buypass
+
+ - **Root**: Buypass Class 3 Root CA
+ - **Intermediate**: Buypass Class 3 CA 1 / G2 (depends on issuance)
+
+ **How to Get:**
+ - Go to: https://www.buypass.com
+ - Download both root and intermediate in PEM or DER format.
+
+ ---
+
+ ### 🧩 Installing Certificates on the Keyfactor Gateway Server
+
+ Once downloaded, the **root and intermediate certificates must be installed** in the proper Windows certificate stores on the Gateway server.
+
+ #### Steps:
+
+ 1. **Open** `certlm.msc` (Local Computer Certificates)
+ 2. Install the **Root CA certificate** into:
+ - `Trusted Root Certification Authorities` → `Certificates`
+ 3. Install the **Intermediate CA certificate** into:
+ - `Intermediate Certification Authorities` → `Certificates`
+
+ You can import certificates using the GUI or PowerShell:
+
+ ```powershell
+ Import-Certificate -FilePath "C:\path\to\intermediate.crt" -CertStoreLocation "Cert:\LocalMachine\CA"
+ Import-Certificate -FilePath "C:\path\to\root.crt" -CertStoreLocation "Cert:\LocalMachine\Root"
+ ```
+
+ ---
+
+ ### 🔑 Using the Intermediate Thumbprint
+
+ When registering a new CA in Keyfactor Command:
+
+ - You must specify the **thumbprint** of the Intermediate CA certificate.
+ - This is used to associate issued certificates with the correct issuing chain.
+
+ **How to Get the Thumbprint:**
+
+ 1. In `certlm.msc`, open the certificate under **Intermediate Certification Authorities**.
+ 2. Go to **Details** tab → Scroll to **Thumbprint**.
+ 3. Copy the hex string (ignore spaces).
+
+ ---
+
+ ⚠️ All certificate chains must be trusted by the Gateway OS. If the intermediate is missing or untrusted, issuance will fail or returned certificates may not chain properly.
+
+ * **CA Connection**
+
+ Populate using the configuration fields collected in the [requirements](#requirements) section.
+
+ * **DirectoryUrl** - ACME directory URL (e.g. Let's Encrypt, ZeroSSL, etc.)
+ * **Email** - Email for ACME account registration.
+ * **EabKid** - External Account Binding Key ID (optional)
+ * **EabHmacKey** - External Account Binding HMAC key (optional)
+ * **SignerEncryptionPhrase** - Used to encrypt singer information when account is saved to disk (optional)
+ * **DnsProvider** - DNS Provider to use for ACME DNS-01 challenges (options Google, Cloudflare, AwsRoute53, Azure, Ns1)
+ * **Google_ServiceAccountKeyPath** - Google Cloud DNS: Path to service account JSON key file only if using Google DNS (Optional)
+ * **Google_ProjectId** - Google Cloud DNS: Project ID only if using Google DNS (Optional)
+ * **Cloudflare_ApiToken** - Cloudflare DNS: API Token only if using Cloudflare DNS (Optional)
+ * **Azure_ClientId** - Azure DNS: ClientId only if using Azure DNS and Not Managed Itentity in Azure (Optional)
+ * **Azure_ClientSecret** - Azure DNS: ClientSecret only if using Azure DNS and Not Managed Itentity in Azure (Optional)
+ * **Azure_SubscriptionId** - Azure DNS: SubscriptionId only if using Azure DNS and Not Managed Itentity in Azure (Optional)
+ * **Azure_TenantId** - Azure DNS: TenantId only if using Azure DNS and Not Managed Itentity in Azure (Optional)
+ * **AwsRoute53_AccessKey** - Aws DNS: Access Key only if not using AWS DNS and default AWS Chain Creds on AWS (Optional)
+ * **AwsRoute53_SecretKey** - Aws DNS: Secret Key only if using AWS DNS and not using default AWS Chain Creds on AWS (Optional)
+ * **Ns1_ApiKey** - Ns1 DNS: Api Key only if Using Ns1 DNS (Optional)
+
+2. Define [Certificate Profiles](https://software.keyfactor.com/Guides/AnyCAGatewayREST/Content/AnyCAGatewayREST/AddCP-Gateway.htm) and [Certificate Templates](https://software.keyfactor.com/Guides/AnyCAGatewayREST/Content/AnyCAGatewayREST/AddCA-Gateway.htm) for the Certificate Authority as required. One Certificate Profile must be defined per Certificate Template. It's recommended that each Certificate Profile be named after the Product ID. The Acme plugin supports the following product IDs:
+
+ * **default**
+
+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.
+
+
+## Compatibility
-#### Create initial prerelease
-1. Create a pull request from the dev branch to the release-1.0 branch
+The Acme AnyCA Gateway REST plugin is compatible with the Keyfactor AnyCA Gateway REST 24.2 and later.
-----
+## License
-When the repository is ready for SE Demo, change the following property:
-* "status": "pilot"
+Apache License 2.0, see [LICENSE](LICENSE).
-When the integration has been approved by Support and Delivery teams, change the following property:
-* "status": "production"
+## Related Integrations
-If the repository is ready to be published in the public catalog, the following properties must be updated:
-* "update_catalog": true
-* "link_github": true
+See all [Keyfactor Any CA Gateways (REST)](https://github.com/orgs/Keyfactor/repositories?q=anycagateway).
\ No newline at end of file
diff --git a/TestProgram/Program.cs b/TestProgram/Program.cs
new file mode 100644
index 0000000..ebeedf0
--- /dev/null
+++ b/TestProgram/Program.cs
@@ -0,0 +1,450 @@
+using Microsoft.Extensions.Logging;
+using Keyfactor.Extensions.CAPlugin.Acme;
+using Keyfactor.AnyGateway.Extensions;
+using Keyfactor.Logging;
+using Keyfactor.PKI.Enums.EJBCA;
+using Org.BouncyCastle.Asn1.Pkcs;
+using Org.BouncyCastle.Asn1.X509;
+using Org.BouncyCastle.Asn1;
+using Org.BouncyCastle.Crypto;
+using Org.BouncyCastle.Security;
+using Org.BouncyCastle.OpenSsl;
+using Org.BouncyCastle.Pkcs;
+using System.Text;
+using System.Text.Json;
+using System.Collections.Concurrent;
+
+internal class Program
+{
+ private const string CONFIG_FILE_PATH = "c:\\acme\\config\\acme-config.json";
+
+ public static async Task Main()
+ {
+
+ // ================================
+ // 📌 === LOAD CONFIGURATION ===
+ // ================================
+ AcmeConfig config;
+ try
+ {
+ config = await LoadConfigurationAsync(CONFIG_FILE_PATH);
+ }
+ catch (Exception ex)
+ {
+ Console.WriteLine($"❌ Failed to load configuration: {ex.Message}");
+ Console.WriteLine($"Please ensure {CONFIG_FILE_PATH} exists and is properly formatted.");
+ return;
+ }
+
+ // ================================
+ // ✅ Setup logging + plugin
+ // ================================
+ using var loggerFactory = LoggerFactory.Create(builder =>
+ {
+ builder.AddSimpleConsole(options =>
+ {
+ options.SingleLine = true;
+ options.TimestampFormat = "[HH:mm:ss] ";
+ });
+ builder.SetMinimumLevel(LogLevel.Debug);
+ });
+
+ ILogger logger = LogHandler.GetClassLogger();
+ logger.LogInformation("🚀 Starting secure ACME test client...");
+
+ var selectedAcmeProvider = GetSelectedAcmeProvider(config);
+ logger.LogInformation($"📋 Using ACME Provider: {config.AcmeProvider}");
+ logger.LogInformation($"🌐 Directory URL: {selectedAcmeProvider.DirectoryUrl}");
+ logger.LogInformation($"📧 Email: {selectedAcmeProvider.Email}");
+ logger.LogInformation($"🔒 EAB Required: {(!string.IsNullOrEmpty(selectedAcmeProvider.EabKid) ? "Yes" : "No")}");
+ logger.LogInformation($"🌍 DNS Provider: {config.DnsProvider}");
+ logger.LogInformation($"🏷️ Domain: {config.Domain}");
+
+ // ✅ Convert to flat dictionary for AnyGateway
+ var configDict = BuildConfigurationDictionary(config);
+
+ var configProvider = new MockConfigProvider(configDict);
+ var plugin = new AcmeCaPlugin();
+ plugin.Initialize(configProvider, null);
+
+ if (config.RunEnroll)
+ {
+ // ================================
+ // ✅ Generate CSR dynamically
+ // ================================
+ string privateKeyPem;
+ string csrString = CsrHelper.GenerateCsrBase64(config.Domain, new List { config.Domain }, config.KeySize, out privateKeyPem);
+
+ logger.LogInformation($"Generated CSR (Base64): {csrString[..Math.Min(80, csrString.Length)]}...");
+ logger.LogInformation($"Generated Private Key PEM:\n{privateKeyPem[..Math.Min(200, privateKeyPem.Length)]}...");
+
+ var san = new Dictionary
+ {
+ { "dns", new[] { config.Domain } }
+ };
+
+ // ================================
+ // ✅ Run ACME enrollment
+ // ================================
+ var result = await plugin.Enroll(
+ csr: csrString,
+ subject: $"CN={config.Domain}",
+ san: san,
+ productInfo: new EnrollmentProductInfo { ProductID = "default" },
+ requestFormat: RequestFormat.PKCS10,
+ enrollmentType: EnrollmentType.New
+ );
+
+ logger.LogInformation("✅ Enrollment Result:");
+ logger.LogInformation($"Status: {(EndEntityStatus)result.Status}");
+ logger.LogInformation($"Certificate:\n{(string.IsNullOrEmpty(result.Certificate) ? "None" : result.Certificate[..Math.Min(result.Certificate.Length, 300)] + "...")}");
+ logger.LogInformation($"CA Request ID: {result.CARequestID}");
+
+ // ================================
+ // ✅ Save outputs to disk
+ // ================================
+ await File.WriteAllTextAsync($"{config.Domain}_privatekey.pem", privateKeyPem);
+ await File.WriteAllTextAsync($"{config.Domain}_certificate.pem", result.Certificate ?? "");
+
+ logger.LogInformation($"✅ Saved private key and certificate: {config.Domain}_*.pem");
+ }
+ else
+ {
+ // ================================
+ // ✅ Run Synchronize always (or stand-alone)
+ // ================================
+ using var cancelTokenSource = new CancellationTokenSource();
+ var buffer = new BlockingCollection();
+
+ logger.LogInformation("🔄 Running Synchronize to check for pending orders...");
+
+ await plugin.Synchronize(
+ buffer,
+ lastSync: null,
+ fullSync: true,
+ cancelToken: cancelTokenSource.Token);
+
+ foreach (var cert in buffer)
+ {
+ logger.LogInformation($"🔑 Synced Certificate: CARequestID={cert.CARequestID}, Status={(EndEntityStatus)cert.Status}");
+ if (!string.IsNullOrWhiteSpace(cert.Certificate))
+ {
+ var filename = $"{config.Domain}_synced_certificate.pem";
+ await File.WriteAllTextAsync(filename, cert.Certificate);
+ logger.LogInformation($"✅ Saved synced certificate to: {filename}");
+ }
+ }
+
+ logger.LogInformation("✅ Synchronize call completed.");
+ }
+ }
+
+ private static async Task LoadConfigurationAsync(string configPath)
+ {
+ if (!File.Exists(configPath))
+ {
+ // Create a sample configuration file
+ await CreateSampleConfigAsync(configPath);
+ throw new FileNotFoundException($"Configuration file not found. A sample configuration has been created at {configPath}. Please edit it with your actual values.");
+ }
+
+ var jsonString = await File.ReadAllTextAsync(configPath);
+ var options = new JsonSerializerOptions
+ {
+ PropertyNameCaseInsensitive = true,
+ AllowTrailingCommas = true,
+ ReadCommentHandling = JsonCommentHandling.Skip
+ };
+
+ return JsonSerializer.Deserialize(jsonString, options)
+ ?? throw new InvalidOperationException("Failed to deserialize configuration file.");
+ }
+
+ private static async Task CreateSampleConfigAsync(string configPath)
+ {
+ var sampleConfig = new AcmeConfig
+ {
+ RunEnroll = true,
+ Domain = "www.example.com",
+ KeySize = 4096,
+ AcmeProvider = "LetsEncrypt", // Options: LetsEncrypt, Buypass, ZeroSsl, GoogleCas, Custom
+ AcmeProviders = new AcmeProvidersSettings
+ {
+ LetsEncrypt = new AcmeProviderSettings
+ {
+ DirectoryUrl = "https://acme-v02.api.letsencrypt.org/directory",
+ Email = "your-email@example.com",
+ EabKid = null, // Not required for Let's Encrypt
+ EabHmacKey = null, // Not required for Let's Encrypt
+ Description = "Let's Encrypt Production Environment"
+ },
+ Buypass = new AcmeProviderSettings
+ {
+ DirectoryUrl = "https://api.buypass.com/acme/directory",
+ Email = "your-email@example.com",
+ EabKid = "your-buypass-eab-kid",
+ EabHmacKey = "your-buypass-eab-hmac-key",
+ Description = "Buypass ACME CA"
+ },
+ ZeroSsl = new AcmeProviderSettings
+ {
+ DirectoryUrl = "https://acme.zerossl.com/v2/DV90/directory",
+ Email = "your-email@example.com",
+ EabKid = "your-zerossl-eab-kid",
+ EabHmacKey = "your-zerossl-eab-hmac-key",
+ Description = "ZeroSSL ACME CA"
+ },
+ GoogleCas = new AcmeProviderSettings
+ {
+ DirectoryUrl = "https://dv.acme-v02.api.pki.goog/directory",
+ Email = "your-email@example.com",
+ EabKid = "your-google-cas-eab-kid",
+ EabHmacKey = "your-google-cas-eab-hmac-key",
+ Description = "Google Certificate Authority Service"
+ },
+ Custom = new AcmeProviderSettings
+ {
+ DirectoryUrl = "https://your-custom-acme-server.com/directory",
+ Email = "your-email@example.com",
+ EabKid = "your-custom-eab-kid",
+ EabHmacKey = "your-custom-eab-hmac-key",
+ Description = "Custom ACME Provider"
+ }
+ },
+ DnsProvider = "Google", // Options: Google, Cloudflare, AwsRoute53, Azure, Ns1
+ DnsProviderSettings = new DnsProviderSettings
+ {
+ Google = new GoogleDnsSettings
+ {
+ ServiceAccountKeyPath = "C:\\path\\to\\service-account.json",
+ ProjectId = "your-project-id"
+ },
+ Cloudflare = new CloudflareDnsSettings
+ {
+ ApiToken = "your-cloudflare-api-token"
+ },
+ AwsRoute53 = new AwsRoute53Settings
+ {
+ AccessKeyId = "your-aws-access-key",
+ SecretAccessKey = "your-aws-secret-key",
+ Region = "us-east-1"
+ },
+ Azure = new AzureDnsSettings
+ {
+ TenantId = "your-tenant-id",
+ ClientId = "your-client-id",
+ SubscriptionId = "your-subscription-id",
+ ClientSecret = "your-client-secret"
+ },
+ Ns1 = new Ns1DnsSettings
+ {
+ ApiKey = "your-ns1-api-key"
+ }
+ }
+ };
+
+ var options = new JsonSerializerOptions
+ {
+ WriteIndented = true,
+ PropertyNamingPolicy = JsonNamingPolicy.CamelCase
+ };
+
+ var jsonString = JsonSerializer.Serialize(sampleConfig, options);
+ await File.WriteAllTextAsync(configPath, jsonString);
+ }
+
+ private static Dictionary BuildConfigurationDictionary(AcmeConfig config)
+ {
+ // Get the selected ACME provider settings
+ var selectedAcmeProvider = GetSelectedAcmeProvider(config);
+
+ var dict = new Dictionary
+ {
+ ["DirectoryUrl"] = selectedAcmeProvider.DirectoryUrl,
+ ["Email"] = selectedAcmeProvider.Email,
+ ["EabKid"] = selectedAcmeProvider.EabKid ?? "",
+ ["EabHmacKey"] = selectedAcmeProvider.EabHmacKey ?? "",
+ ["DnsProvider"] = config.DnsProvider,
+ ["SignerEncryptionPhrase"] = config.SignerEncryptionPhrase
+ };
+
+ // Add DNS provider credentials based on selected provider
+ var dns = config.DnsProviderSettings;
+
+ if (dns.Google != null)
+ {
+ dict["Google_ServiceAccountKeyPath"] = dns.Google.ServiceAccountKeyPath ?? "";
+ dict["Google_ProjectId"] = dns.Google.ProjectId ?? "";
+ }
+
+ if (dns.Cloudflare != null)
+ {
+ dict["Cloudflare_ApiToken"] = dns.Cloudflare.ApiToken ?? "";
+ }
+
+ if (dns.AwsRoute53 != null)
+ {
+ dict["AwsRoute53_AccessKey"] = dns.AwsRoute53.AccessKeyId ?? "";
+ dict["AwsRoute53_SecretKey"] = dns.AwsRoute53.SecretAccessKey ?? "";
+ }
+
+ if (dns.Azure != null)
+ {
+ dict["Azure_ClientId"] = dns.Azure.ClientId ?? "";
+ dict["Azure_TenantId"] = dns.Azure.TenantId ?? "";
+ dict["Azure_SubscriptionId"] = dns.Azure.SubscriptionId ?? "";
+ dict["Azure_ClientSecret"] = dns.Azure.ClientSecret ?? "";
+ }
+
+ if (dns.Ns1 != null)
+ {
+ dict["Ns1_ApiKey"] = dns.Ns1.ApiKey ?? "";
+ }
+
+ return dict;
+ }
+
+ private static AcmeProviderSettings GetSelectedAcmeProvider(AcmeConfig config)
+ {
+ var providers = config.AcmeProviders;
+
+ return config.AcmeProvider.ToLower() switch
+ {
+ "letsencrypt" => providers.LetsEncrypt ?? throw new InvalidOperationException("Let's Encrypt configuration not found"),
+ "buypass" => providers.Buypass ?? throw new InvalidOperationException("Buypass configuration not found"),
+ "zerossl" => providers.ZeroSsl ?? throw new InvalidOperationException("ZeroSSL configuration not found"),
+ "googlecas" => providers.GoogleCas ?? throw new InvalidOperationException("Google CAS configuration not found"),
+ "custom" => providers.Custom ?? throw new InvalidOperationException("Custom ACME provider configuration not found"),
+ _ => throw new InvalidOperationException($"Unknown ACME provider: {config.AcmeProvider}")
+ };
+ }
+
+ // === Configuration Classes ===
+ public class AcmeConfig
+ {
+ public bool RunEnroll { get; set; }
+ public string Domain { get; set; } = "";
+ public int KeySize { get; set; }
+ public string AcmeProvider { get; set; } = "";
+ public AcmeProvidersSettings AcmeProviders { get; set; } = new();
+ public string DnsProvider { get; set; } = "";
+ public DnsProviderSettings DnsProviderSettings { get; set; } = new();
+ public string SignerEncryptionPhrase { get; set; } = "";
+ }
+
+ public class AcmeProvidersSettings
+ {
+ public AcmeProviderSettings? LetsEncrypt { get; set; }
+ public AcmeProviderSettings? Buypass { get; set; }
+ public AcmeProviderSettings? ZeroSsl { get; set; }
+ public AcmeProviderSettings? GoogleCas { get; set; }
+ public AcmeProviderSettings? Custom { get; set; }
+ }
+
+ public class AcmeProviderSettings
+ {
+ public string DirectoryUrl { get; set; } = "";
+ public string Email { get; set; } = "";
+ public string? EabKid { get; set; }
+ public string? EabHmacKey { get; set; }
+ public string? Description { get; set; }
+ }
+
+ public class DnsProviderSettings
+ {
+ public GoogleDnsSettings? Google { get; set; }
+ public CloudflareDnsSettings? Cloudflare { get; set; }
+ public AwsRoute53Settings? AwsRoute53 { get; set; }
+ public AzureDnsSettings? Azure { get; set; }
+ public Ns1DnsSettings? Ns1 { get; set; }
+ }
+
+ public class GoogleDnsSettings
+ {
+ public string? ServiceAccountKeyPath { get; set; }
+ public string? ProjectId { get; set; }
+ }
+
+ public class CloudflareDnsSettings
+ {
+ public string? ApiToken { get; set; }
+ }
+
+ public class AwsRoute53Settings
+ {
+ public string? AccessKeyId { get; set; }
+ public string? SecretAccessKey { get; set; }
+ public string Region { get; set; } = "us-east-1";
+ }
+
+ public class AzureDnsSettings
+ {
+ public string? TenantId { get; set; }
+ public string? ClientId { get; set; }
+ public string? SubscriptionId { get; set; }
+ public string? ClientSecret { get; set; }
+ }
+
+ public class Ns1DnsSettings
+ {
+ public string? ApiKey { get; set; }
+ }
+
+ // === Local config provider ===
+ private class MockConfigProvider : IAnyCAPluginConfigProvider
+ {
+ public MockConfigProvider(Dictionary config) =>
+ CAConnectionData = config;
+
+ public Dictionary CAConnectionData { get; }
+ public Dictionary CertificateAuthorityData => new();
+ public Dictionary Metadata => new();
+ }
+
+ // === CSR helper ===
+ public static class CsrHelper
+ {
+ public static string GenerateCsrBase64(string domainName, List sanNames, int keySize, out string privateKeyPem)
+ {
+ var keyPairGenerator = new Org.BouncyCastle.Crypto.Generators.RsaKeyPairGenerator();
+ keyPairGenerator.Init(new Org.BouncyCastle.Crypto.KeyGenerationParameters(new SecureRandom(), keySize));
+ AsymmetricCipherKeyPair keyPair = keyPairGenerator.GenerateKeyPair();
+
+ var subject = new X509Name($"CN={domainName}");
+
+ var sanBuilder = new GeneralNames(
+ sanNames.ConvertAll(name => new GeneralName(GeneralName.DnsName, name)).ToArray()
+ );
+ var extensionsGenerator = new X509ExtensionsGenerator();
+ extensionsGenerator.AddExtension(
+ X509Extensions.SubjectAlternativeName,
+ false,
+ sanBuilder
+ );
+ var extensions = extensionsGenerator.Generate();
+ var attrSet = new AttributePkcs(PkcsObjectIdentifiers.Pkcs9AtExtensionRequest, new DerSet(extensions));
+
+ var csr = new Pkcs10CertificationRequest(
+ "SHA256WITHRSA",
+ subject,
+ keyPair.Public,
+ new DerSet(attrSet),
+ keyPair.Private
+ );
+
+ byte[] csrDer = csr.GetDerEncoded();
+ string csrBase64 = Convert.ToBase64String(csrDer);
+
+ StringBuilder sb = new StringBuilder();
+ using (var writer = new StringWriter(sb))
+ {
+ var pemWriter = new PemWriter(writer);
+ pemWriter.WriteObject(keyPair.Private);
+ pemWriter.Writer.Flush();
+ privateKeyPem = sb.ToString();
+ }
+
+ return csrBase64;
+ }
+ }
+}
\ No newline at end of file
diff --git a/TestProgram/TestProgram.csproj b/TestProgram/TestProgram.csproj
new file mode 100644
index 0000000..127834b
--- /dev/null
+++ b/TestProgram/TestProgram.csproj
@@ -0,0 +1,21 @@
+
+
+
+ Exe
+ net6.0;net8.0
+ enable
+ enable
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/cagateway-template.sln b/cagateway-template.sln
deleted file mode 100644
index b953ecb..0000000
--- a/cagateway-template.sln
+++ /dev/null
@@ -1,32 +0,0 @@
-
-Microsoft Visual Studio Solution File, Format Version 12.00
-# Visual Studio Version 16
-VisualStudioVersion = 16.0.31729.503
-MinimumVisualStudioVersion = 10.0.40219.1
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "cagateway-template", "cagateway-template\cagateway-template.csproj", "{9D2D6ED9-4626-430C-879D-0FE0FEBED146}"
-EndProject
-Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{431498A1-F30A-4307-9FBF-B1D634326444}"
- ProjectSection(SolutionItems) = preProject
- CHANGELOG.md = CHANGELOG.md
- integration-manifest.json = integration-manifest.json
- readme_source.md = readme_source.md
- EndProjectSection
-EndProject
-Global
- GlobalSection(SolutionConfigurationPlatforms) = preSolution
- Debug|Any CPU = Debug|Any CPU
- Release|Any CPU = Release|Any CPU
- EndGlobalSection
- GlobalSection(ProjectConfigurationPlatforms) = postSolution
- {9D2D6ED9-4626-430C-879D-0FE0FEBED146}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {9D2D6ED9-4626-430C-879D-0FE0FEBED146}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {9D2D6ED9-4626-430C-879D-0FE0FEBED146}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {9D2D6ED9-4626-430C-879D-0FE0FEBED146}.Release|Any CPU.Build.0 = Release|Any CPU
- EndGlobalSection
- GlobalSection(SolutionProperties) = preSolution
- HideSolutionNode = FALSE
- EndGlobalSection
- GlobalSection(ExtensibilityGlobals) = postSolution
- SolutionGuid = {5D2E21F6-120F-4B71-A596-991879B03943}
- EndGlobalSection
-EndGlobal
diff --git a/cagateway-template/APIProxy/ProductNameBaseCall.cs b/cagateway-template/APIProxy/ProductNameBaseCall.cs
deleted file mode 100644
index 1b92523..0000000
--- a/cagateway-template/APIProxy/ProductNameBaseCall.cs
+++ /dev/null
@@ -1,27 +0,0 @@
-using Newtonsoft.Json;
-
-using System;
-using System.Collections.Generic;
-using System.Linq;
-using System.Text;
-using System.Threading.Tasks;
-
-namespace Keyfactor.Extensions.AnyGateway.Company.Product.APIProxy
-{
- public abstract class ProductNameBaseRequest
- {
- [JsonIgnore]
- public string Resource { get; internal set; }
-
- [JsonIgnore]
- public string Method { get; internal set; }
-
- [JsonIgnore]
- public string targetURI { get; set; }
-
- public string BuildParameters()
- {
- return "";
- }
- }
-}
\ No newline at end of file
diff --git a/cagateway-template/Client/ProductNameClient.cs b/cagateway-template/Client/ProductNameClient.cs
deleted file mode 100644
index cbd16ab..0000000
--- a/cagateway-template/Client/ProductNameClient.cs
+++ /dev/null
@@ -1,12 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.Linq;
-using System.Text;
-using System.Threading.Tasks;
-
-namespace Keyfactor.Extensions.AnyGateway.Company.Product.Client
-{
- public class ProductNameClient
- {
- }
-}
\ No newline at end of file
diff --git a/cagateway-template/Constants.cs b/cagateway-template/Constants.cs
deleted file mode 100644
index af6c50e..0000000
--- a/cagateway-template/Constants.cs
+++ /dev/null
@@ -1,13 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.Linq;
-using System.Text;
-using System.Threading.Tasks;
-
-namespace Keyfactor.Extensions.AnyGateway.Company.Product
-{
- public class Constants
- {
- //Define any constants needed here (mostly field names for config parameters)
- }
-}
\ No newline at end of file
diff --git a/cagateway-template/GatewayNameCAConnector.cs b/cagateway-template/GatewayNameCAConnector.cs
deleted file mode 100644
index abbe2ab..0000000
--- a/cagateway-template/GatewayNameCAConnector.cs
+++ /dev/null
@@ -1,193 +0,0 @@
-using CAProxy.AnyGateway;
-using CAProxy.AnyGateway.Interfaces;
-using CAProxy.AnyGateway.Models;
-using CAProxy.AnyGateway.Models.Configuration;
-using CAProxy.Common;
-
-using CSS.PKI;
-
-using System;
-using System.Collections.Concurrent;
-using System.Collections.Generic;
-using System.Linq;
-using System.Text;
-using System.Threading;
-using System.Threading.Tasks;
-
-using GatewayNameConstants = Keyfactor.Extensions.AnyGateway.Company.Product.Constants;
-
-namespace Keyfactor.Extensions.AnyGateway.Company.Product
-{
- public class GatewayNameCAConnector : BaseCAConnector, ICAConnectorConfigInfoProvider
- {
- #region Fields and Constructors
-
- ///
- /// Provides configuration information for the
- ///
- private ICAConnectorConfigProvider ConfigProvider { get; set; }
-
- //Define any additional private fields here
-
- #endregion Fields and Constructors
-
- #region ICAConnector Methods
-
- ///
- /// Initialize the
- ///
- /// The config provider contains information required to connect to the CA.
- public override void Initialize(ICAConnectorConfigProvider configProvider)
- {
- ConfigProvider = configProvider;
- }
-
- ///
- /// Enrolls for a certificate through the API.
- ///
- /// Reads certificate data from the database.
- /// The certificate request CSR in PEM format.
- /// The subject of the certificate request.
- /// Any SANs added to the request.
- /// Information about the CA product type.
- /// The format of the request.
- /// The type of the enrollment, i.e. new, renew, or reissue.
- ///
- public override EnrollmentResult Enroll(ICertificateDataReader certificateDataReader, string csr, string subject, Dictionary san, EnrollmentProductInfo productInfo, PKIConstants.X509.RequestFormat requestFormat, RequestUtilities.EnrollmentType enrollmentType)
- {
- throw new NotImplementedException();
- }
-
- ///
- /// Returns a single certificate record by its serial number.
- ///
- /// The CA request ID for the certificate.
- ///
- public override CAConnectorCertificate GetSingleRecord(string caRequestID)
- {
- throw new NotImplementedException();
- }
-
- ///
- /// Attempts to reach the CA over the network.
- ///
- public override void Ping()
- {
- throw new NotImplementedException();
- }
-
- ///
- /// Revokes a certificate by its serial number.
- ///
- /// The CA request ID.
- /// The hex-encoded serial number.
- /// The revocation reason.
- ///
- public override int Revoke(string caRequestID, string hexSerialNumber, uint revocationReason)
- {
- throw new NotImplementedException();
- }
-
- ///
- /// Synchronizes the gateway with the external CA
- ///
- /// Provides information about the gateway's certificate database.
- /// Buffer into which certificates are places from the CA.
- /// Information about the last CA sync.
- /// The cancellation token.
- public override void Synchronize(ICertificateDataReader certificateDataReader, BlockingCollection blockingBuffer, CertificateAuthoritySyncInfo certificateAuthoritySyncInfo, CancellationToken cancelToken)
- {
- throw new NotImplementedException();
- }
-
- ///
- /// Validates that the CA connection info is correct.
- ///
- /// The information used to connect to the CA.
- public override void ValidateCAConnectionInfo(Dictionary connectionInfo)
- {
- throw new NotImplementedException();
- }
-
- ///
- /// Validates that the product information for the CA is correct
- ///
- /// The product information.
- /// The CA connection information.
- public override void ValidateProductInfo(EnrollmentProductInfo productInfo, Dictionary connectionInfo)
- {
- throw new NotImplementedException();
- }
-
- [Obsolete]
- public override EnrollmentResult Enroll(string csr, string subject, Dictionary san, EnrollmentProductInfo productInfo, PKIConstants.X509.RequestFormat requestFormat, RequestUtilities.EnrollmentType enrollmentType)
- {
- throw new NotImplementedException();
- }
-
- [Obsolete]
- public override void Synchronize(ICertificateDataReader certificateDataReader, BlockingCollection blockingBuffer, CertificateAuthoritySyncInfo certificateAuthoritySyncInfo, CancellationToken cancelToken, string logicalName)
- {
- throw new NotImplementedException();
- }
-
- #endregion ICAConnector Methods
-
- #region ICAConnectorConfigInfoProvider Methods
-
- ///
- /// Returns the default CA connector section of the config file.
- ///
- ///
- public Dictionary GetDefaultCAConnectorConfig()
- {
- return new Dictionary()
- {
- };
- }
-
- ///
- /// Gets the default comment on the default product type.
- ///
- ///
- public string GetProductIDComment()
- {
- return "";
- }
-
- ///
- /// Gets annotations for the CA connector properties.
- ///
- ///
- public Dictionary GetCAConnectorAnnotations()
- {
- return new Dictionary();
- }
-
- ///
- /// Gets annotations for the template mapping parameters
- ///
- ///
- public Dictionary GetTemplateParameterAnnotations()
- {
- throw new NotImplementedException();
- }
-
- ///
- /// Gets default template map parameters for GlobalSign Atlas product types.
- ///
- ///
- public Dictionary GetDefaultTemplateParametersConfig()
- {
- throw new NotImplementedException();
- }
-
- #endregion ICAConnectorConfigInfoProvider Methods
-
- #region Helper Methods
-
- // All private helper methods go here
-
- #endregion Helper Methods
- }
-}
\ No newline at end of file
diff --git a/cagateway-template/Properties/AssemblyInfo.cs b/cagateway-template/Properties/AssemblyInfo.cs
deleted file mode 100644
index 8b68512..0000000
--- a/cagateway-template/Properties/AssemblyInfo.cs
+++ /dev/null
@@ -1,36 +0,0 @@
-using System.Reflection;
-using System.Runtime.CompilerServices;
-using System.Runtime.InteropServices;
-
-// General Information about an assembly is controlled through the following
-// set of attributes. Change these attribute values to modify the information
-// associated with an assembly.
-[assembly: AssemblyTitle("cagateway-template")]
-[assembly: AssemblyDescription("")]
-[assembly: AssemblyConfiguration("")]
-[assembly: AssemblyCompany("")]
-[assembly: AssemblyProduct("cagateway-template")]
-[assembly: AssemblyCopyright("Copyright © 2022")]
-[assembly: AssemblyTrademark("")]
-[assembly: AssemblyCulture("")]
-
-// Setting ComVisible to false makes the types in this assembly not visible
-// to COM components. If you need to access a type in this assembly from
-// COM, set the ComVisible attribute to true on that type.
-[assembly: ComVisible(false)]
-
-// The following GUID is for the ID of the typelib if this project is exposed to COM
-[assembly: Guid("9d2d6ed9-4626-430c-879d-0fe0febed146")]
-
-// Version information for an assembly consists of the following four values:
-//
-// Major Version
-// Minor Version
-// Build Number
-// Revision
-//
-// You can specify all the values or you can default the Build and Revision Numbers
-// by using the '*' as shown below:
-// [assembly: AssemblyVersion("1.0.*")]
-[assembly: AssemblyVersion("1.0.0.0")]
-[assembly: AssemblyFileVersion("1.0.0.0")]
diff --git a/cagateway-template/app.config b/cagateway-template/app.config
deleted file mode 100644
index ad48466..0000000
--- a/cagateway-template/app.config
+++ /dev/null
@@ -1,15 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/cagateway-template/cagateway-template.csproj b/cagateway-template/cagateway-template.csproj
deleted file mode 100644
index a71a7f5..0000000
--- a/cagateway-template/cagateway-template.csproj
+++ /dev/null
@@ -1,93 +0,0 @@
-
-
-
-
- Debug
- AnyCPU
- {9D2D6ED9-4626-430C-879D-0FE0FEBED146}
- Library
- Properties
- cagateway_template
- cagateway-template
- v4.7.2
- 512
- true
-
-
- true
- full
- false
- bin\Debug\
- DEBUG;TRACE
- prompt
- 4
-
-
- pdbonly
- true
- bin\Release\
- TRACE
- prompt
- 4
-
-
-
- ..\packages\BouncyCastle.1.8.9\lib\BouncyCastle.Crypto.dll
-
-
- ..\packages\Keyfactor.AnyGateway.SDK.21.3.2\lib\net462\CAProxy.AnyGateway.Core.dll
-
-
- ..\packages\Keyfactor.AnyGateway.SDK.21.3.2\lib\net462\CAProxy.Interfaces.dll
-
-
- ..\packages\Keyfactor.AnyGateway.SDK.21.3.2\lib\net462\CAProxyDAL.dll
-
-
- ..\packages\Common.Logging.3.4.1\lib\net40\Common.Logging.dll
-
-
- ..\packages\Common.Logging.Core.3.4.1\lib\net40\Common.Logging.Core.dll
-
-
- ..\packages\Keyfactor.AnyGateway.SDK.21.3.2\lib\net462\CommonCAProxy.dll
-
-
- ..\packages\CSS.Common.1.7.0\lib\net462\CSS.Common.dll
-
-
- ..\packages\CSS.PKI.2.13.0\lib\net462\CSS.PKI.dll
-
-
- ..\packages\Keyfactor.Logging.1.1.0\lib\netstandard2.0\Keyfactor.Logging.dll
-
-
- ..\packages\Microsoft.Extensions.Logging.Abstractions.5.0.0\lib\net461\Microsoft.Extensions.Logging.Abstractions.dll
-
-
- ..\packages\Newtonsoft.Json.12.0.3\lib\net45\Newtonsoft.Json.dll
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/cagateway-template/packages.config b/cagateway-template/packages.config
deleted file mode 100644
index 5fd12f1..0000000
--- a/cagateway-template/packages.config
+++ /dev/null
@@ -1,14 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/docsource/configuration.md b/docsource/configuration.md
new file mode 100644
index 0000000..56b652b
--- /dev/null
+++ b/docsource/configuration.md
@@ -0,0 +1,474 @@
+## Overview
+The **Keyfactor ACME CA Gateway Plugin** enables certificate enrollment using the [ACME protocol (RFC 8555)](https://datatracker.ietf.org/doc/html/rfc8555), providing automated certificate issuance via any compliant Certificate Authority. This plugin is designed for **enrollment-only workflows** — it **does not support synchronization or revocation** of certificates.
+
+### 🔧 What It Does
+This plugin allows Keyfactor Gateways to:
+- Submit CSRs to ACME-based CAs.
+- Complete domain validation via DNS-01 challenges.
+- Automatically retrieve and return signed certificates.
+
+Once a certificate is issued, the plugin returns the PEM-encoded certificate to the Gateway.
+
+### ✅ ACME Providers Tested
+This plugin has been tested and confirmed to work with the following ACME providers:
+- **Let's Encrypt**
+- **Google ACME (Certificate Authority Service)**
+- **ZeroSSL** (functional but known slowness may cause timeouts)
+- **Buypass**
+
+It is designed to be provider-agnostic and should work with any standards-compliant ACME server.
+
+### 🌐 Supported DNS Providers (Initial Release)
+DNS-01 challenge automation is supported through the following providers:
+- **Google Cloud DNS**
+- **AWS Route 53**
+- **Azure DNS**
+- **Cloudflare**
+- **NS1**
+
+Additional DNS providers can be added by extending the included `IDnsProvider` interface.
+
+---
+
+### 🔁 Enrollment Flow Summary
+
+```text
+1. Keyfactor Gateway submits CSR and SAN metadata to plugin.
+2. Plugin initializes ACME client and creates a new order.
+3. For each domain:
+ a. Retrieve DNS-01 challenge.
+ b. Use the configured DNS provider to publish challenge record.
+ c. Wait for DNS propagation and validate record.
+ d. Notify ACME provider to trigger validation.
+4. Once all challenges are valid, finalize the order using CSR.
+5. Download the signed certificate from ACME provider.
+6. Return PEM certificate to the Gateway.
+```
+
+The plugin uses a modular design that separates ACME communication logic and DNS challenge automation, allowing for future extensibility in both areas.
+
+> ⚠️ Revocation, certificate synchronization, and renewal tracking are intentionally **not implemented** in this plugin. All lifecycle tracking must be handled externally (e.g., via Keyfactor monitoring or Gateway automation).
+
+## Compatibility
+
+The Acme AnyCA Gateway REST plugin is compatible with the Keyfactor AnyCA Gateway REST 24.2 and later.
+
+
+## Requirements
+
+### DNS Providers
+
+This plugin automates DNS-01 challenges using pluggable DNS provider implementations. These providers create and remove TXT records to prove domain control to ACME servers.
+
+
+✅ Supported DNS Providers (Initial Release)
+
+| Provider | Auth Methods Supported | Config Keys Required |
+|--------------|-----------------------------------------------|--------------------------------------------------------|
+| Google DNS | Service Account Key or ADC | `Google_ServiceAccountKeyPath`, `Google_ProjectId` |
+| AWS Route 53 | Access Key/Secret or IAM Role | `AwsRoute53_AccessKey`, `AwsRoute53_SecretKey` |
+| Azure DNS | Client Secret or Managed Identity | `Azure_TenantId`, `Azure_ClientId`, `Azure_ClientSecret`, `Azure_SubscriptionId` |
+| Cloudflare | API Token | `Cloudflare_ApiToken` |
+| NS1 | API Key | `Ns1_ApiKey` |
+
+
+
+
+⏱ DNS Propagation Logic
+
+Before submitting ACME challenges, the plugin verifies DNS propagation using multiple public resolvers (Google, Cloudflare, OpenDNS, Quad9). A record must be visible on **at least 3 servers** to proceed, with up to **3 retries** spaced by 10 seconds.
+
+This logic is handled by the `DnsVerificationHelper` class and ensures a high-confidence validation before proceeding.
+
+
+
+
+🔑 Credential Flow
+
+Each provider supports multiple credential strategies:
+
+- **Google DNS**:
+ - ✅ **Service Account Key** (via `Google_ServiceAccountKeyPath`)
+ - ✅ **Application Default Credentials** (e.g., GCP Workload Identity or developer auth)
+
+- **AWS Route 53**:
+ - ✅ **Access/Secret Keys** (`AwsRoute53_AccessKey`, `AwsRoute53_SecretKey`)
+ - ✅ **IAM Role via EC2 Instance Metadata** (no explicit credentials)
+
+- **Azure DNS**:
+ - ✅ **Client Secret** (explicit `TenantId`, `ClientId`, `ClientSecret`)
+ - ✅ **Managed Identity** or environment-based credentials via `DefaultAzureCredential`
+
+- **Cloudflare**:
+ - ✅ **Bearer API Token** for zone-level DNS control
+
+- **NS1**:
+ - ✅ **API Key** passed in header `X-NSONE-Key`
+
+
+
+
+🧩 Adding New DNS Providers
+
+To add support for new DNS services:
+
+1. Implement the `IDnsProvider` interface:
+ ```csharp
+ public interface IDnsProvider
+ {
+ Task CreateRecordAsync(string recordName, string txtValue);
+ Task DeleteRecordAsync(string recordName);
+ }
+ ```
+
+2. Register the new provider in the `DnsProviderFactory`:
+ ```csharp
+ case "yourprovider":
+ return new YourCustomDnsProvider(config.YourProviderConfigValues...);
+ ```
+
+3. Use zone detection logic similar to `GoogleDnsProvider`, `AzureDnsProvider`, or `Ns1DnsProvider`.
+
+Each provider is instantiated dynamically based on the `DnsProvider` field in the `AcmeClientConfig`.
+
+> 🔁 This modular DNS system ensures challenge automation works across cloud providers and is easily extensible.
+
+
+
+
+🔒 CA-Level DNS Provider Binding
+
+Each ACME/DNS combination is supported **at the CA level**, meaning that only **one DNS provider** is configured per CA entry in Keyfactor. This ensures a clear and isolated challenge path for each ACME CA connector instance.
+
+If you need to support multiple DNS zones/providers (e.g., both AWS and Cloudflare), configure **separate CA entries**, each with its own DNS provider configuration.
+
+
+
+
+🚫 No Offline Challenge Retry (Initial Release)
+
+In this initial release, there is **no background or offline retry** for ACME challenges that timeout. If DNS propagation takes too long and the challenge is not verified in time, the certificate **request will fail immediately**.
+
+> ⚠️ However, in testing across all supported DNS providers and ACME services (e.g., Let's Encrypt, Google CAS, ZeroSSL, Buypass), propagation has been fast enough to avoid these timeouts in all observed cases.
+
+
+
+---
+
+### ACME Provider Configuration
+
+Each ACME CA (Certificate Authority) has slightly different expectations for account creation and request handling. This plugin supports multiple providers and dynamically handles credentials based on your configuration.
+
+
+🧩 External Account Binding (EAB) Support
+
+Some providers **require** External Account Binding (EAB), which includes:
+- `eabKid`: External Account Binding Key ID
+- `eabHmacKey`: HMAC Key to sign the JWK thumbprint
+
+Others **do not require EAB**, and can create accounts automatically with just an email address.
+
+
+
+
+✅ Supported Providers & Credential Expectations
+
+| Provider | Directory URL | Requires EAB | Notes |
+|----------------|----------------------------------------------------------------|--------------|-----------------------------------------------------------------------|
+| Let's Encrypt | `https://acme-v02.api.letsencrypt.org/directory` | ❌ No | Free and public; account created using only an email address |
+| Buypass | `https://api.buypass.com/acme/directory` | ❌ No | Free and public; supports long-lived certs; no EAB required |
+| ZeroSSL | `https://acme.zerossl.com/v2/DV90/directory` | ✅ Yes | Requires EAB; keys available via [ZeroSSL Developer Portal](https://zerossl.com) |
+| Google CAS | `https://dv.acme-v02.api.pki.goog/directory` | ✅ Yes | Requires EAB; keys issued via [Google CAS UI](https://console.cloud.google.com) |
+
+> ⚠️ If a provider requires EAB and it is not supplied, the request will fail during account registration.
+
+
+
+
+📋 Configuration Fields (Per ACME Provider)
+
+These values are set in the Keyfactor Command Gateway Configuration UI for each ACME provider:
+
+| Field | Description | Required |
+|---------------|---------------------------------------------------|-----------------|
+| `directoryUrl`| The full ACME directory URL for the CA | ✅ Yes |
+| `email` | Account email address for ACME registration | ✅ Yes |
+| `eabKid` | External Account Binding Key ID (if applicable) | 🚫 Only if EAB |
+| `eabHmacKey` | HMAC key used to sign EAB binding (if applicable) | 🚫 Only if EAB |
+
+
+
+
+🔐 How to Get EAB Credentials
+
+- **ZeroSSL**:
+ Log into your account and go to **"ACME EAB Credentials"** in the developer section.
+
+- **Google CAS**:
+ Enable your CA Pool for ACME and generate EAB credentials under the **ACME Integration** tab in Google Cloud Console.
+
+
+
+
+⚙️ Plugin Behavior
+
+- If both `eabKid` and `eabHmacKey` are provided, they will be used to create the ACME account.
+- If either is omitted and the provider requires it, account creation will fail.
+- If neither is provided and the provider does not require EAB, the account will be created using only the email.
+
+Each provider is configured in the JSON config under `acmeProviders`, and only **one provider** is active per enrollment.
+
+
+
+---
+
+### Account Storage and Signer Encryption
+
+This ACME Gateway implementation uses a local file-based store to persist ACME accounts and their associated cryptographic signers. Accounts are cached on disk using a structured format, and signers (private keys) can be encrypted with a passphrase for enhanced security.
+
+
+📁 Account Directory Structure
+
+Each account is saved in its own directory within:
+
+```
+%APPDATA%\AcmeAccounts\{host}_{accountId}
+```
+
+Where:
+- `{host}` is the ACME directory host with dots replaced by dashes (e.g., `acme-zerossl-com`)
+- `{accountId}` is the final segment of the account's KID URL
+
+
+
+
+📄 Files per Account
+
+- `Registration_v2`: Contains serialized `AccountDetails` in JSON format
+- `Signer_v2`: Contains encrypted or plaintext signer key material, depending on passphrase usage
+- `default_{host}.txt`: Tracks the default account for a given ACME directory host
+
+
+
+
+🔐 Encryption with Passphrase
+
+If the `SignerEncryptionPhrase` configuration value is set, the plugin encrypts signer files (`Signer_v2`) using AES with a PBKDF2-derived key and IV. The encrypted data includes a prepended salt and IV to support cross-platform decryption.
+
+```text
+[Salt (16 bytes)] [IV (16 bytes)] [AES-CBC encrypted signer JSON]
+```
+
+The encryption ensures that even if the account files are accessed on disk, the private keys remain unreadable without the configured passphrase.
+
+
+
+
+🔗 External Account Binding (EAB)
+
+For ACME providers requiring EAB (e.g., ZeroSSL, Google CAS), the gateway constructs a manually signed JWS payload containing:
+
+- Protected Header: `alg`, `kid`, `url`
+- Payload: Public JWK of the account signer
+- Signature: HMAC using `eabHmacKey`
+
+This JWS is included during account creation to bind the account to the pre-provisioned identity provided by the CA.
+
+
+
+
+⚙️ Algorithm Support
+
+- Signers support `ES256`, `ES384`, `ES512` (ECDSA) and `RS256`, `RS384`, `RS512` (RSA)
+- EAB HMAC support includes `HS256`, `HS384`, `HS512`
+
+If `ES256` key generation fails (e.g., due to platform constraints), the system automatically falls back to `RS256`.
+
+
+
+### Account Caching and Auto-Creation
+
+On startup or during enrollment/sync, the plugin:
+
+1. Attempts to load a cached account for the specified ACME directory.
+2. If no account is found, it automatically creates a new one, using EAB if configured.
+3. The new account is saved to disk and set as default for future use.
+
+
+🔗 External Account Binding (EAB)
+
+For ACME providers requiring EAB (e.g., ZeroSSL, Google CAS), the gateway constructs a manually signed JWS payload containing:
+
+- Protected Header: `alg`, `kid`, `url`
+- Payload: Public JWK of the account signer
+- Signature: HMAC using `eabHmacKey`
+
+This JWS is included during account creation to bind the account to the pre-provisioned identity provided by the CA.
+
+
+
+
+🔧 Algorithm Support
+
+- Signers support `ES256`, `ES384`, `ES512` (ECDSA) and `RS256`, `RS384`, `RS512` (RSA)
+- EAB HMAC support includes `HS256`, `HS384`, `HS512`
+
+If `ES256` key generation fails (e.g., due to platform constraints), the system automatically falls back to `RS256`.
+
+
+
+### Network and File System Requirements
+
+This section outlines all required ports, file access, permissions, and validation behaviors for operating the ACME Gateway Plugin in a Keyfactor Orchestrator environment.
+
+
+🔌 Port Usage
+
+#### Incoming Connections
+
+- **None.** This plugin does not expose any HTTP or network listeners.
+
+#### Outgoing Connections
+
+| Protocol | Port | Target | Purpose |
+|----------|------|------------------------------|-----------------------------------------------------|
+| HTTPS | 443 | ACME Directory URL | Connect to the ACME CA for account, challenge, and certificate operations |
+| HTTPS | 443 | DNS Provider APIs | Used for DNS-01 challenge automation (Google DNS, AWS, etc.) |
+
+
+
+
+💾 File System Requirements
+
+#### Directory Layout
+
+| Path | Purpose |
+|----------------------------------------------------|----------------------------------------------|
+| `%APPDATA%\AcmeAccounts\` | Default base path for ACME account storage |
+| `AcmeAccounts\{account_id}\Registration_v2` | Contains serialized ACME account metadata |
+| `AcmeAccounts\{account_id}\Signer_v2` | Contains the encrypted private signer key |
+| `AcmeAccounts\default_{host}.txt` | Stores the default account pointer for a given directory |
+
+#### File Access & Permissions
+
+| Path | Operation | Required Permission |
+|--------------------------|-----------|---------------------|
+| Account directory | Create | `Write` |
+| Account files | Read/Write| `Read`, `Write` |
+
+- Files may be optionally encrypted using AES if a passphrase is configured.
+- Ensure the service account under which the orchestrator runs has read/write access to `%APPDATA%` or the custom configured base path.
+
+
+
+
+👤 Windows Account Permissions
+
+- The orchestrator service account (usually `NT AUTHORITY\SYSTEM` or a custom `Network Service`) must have:
+ - File I/O permissions to read/write within the configured base directory.
+ - Network access to ACME CA endpoints and DNS APIs over HTTPS.
+ - DNS provider credentials (Cloudflare API token, Google credentials, etc.) stored securely.
+
+
+
+
+🌐 DNS Propagation Check Behavior
+
+- **Initial Release Behavior**:
+ - DNS challenge propagation is checked during the interactive enrollment phase only.
+ - If propagation takes too long (> 60s), the request will fail. No deferred background polling occurs.
+ - There is **no offline retry mechanism** (e.g., for sync jobs) to pick up completed validations that succeeded after a delay.
+
+- **Future Considerations**:
+ - Support for file-based or database-backed challenge persistence may be added to allow background sync to re-check and finalize challenge state.
+
+
+
+
+## Gateway Registration
+
+Each ACME CA issues certificates that chain to a specific intermediate and root certificate. For trust validation and proper integration with the Keyfactor Gateway, the following steps are required for **every ACME CA** used in your environment.
+
+---
+
+### 🔍 Retrieving Root and Intermediate Certificates
+
+Here is how to obtain the root and intermediate CA certificates from supported ACME providers:
+
+#### Let's Encrypt
+
+- **Root**: ISRG Root X1
+- **Intermediate**: R3
+
+**How to Get:**
+- Browse to: https://letsencrypt.org/certificates/
+- Download both the **ISRG Root X1** and **R3 Intermediate Certificate (PEM format)**.
+
+#### Google Certificate Authority Service (CAS)
+
+- **Root** and **Intermediate** are custom per CA Pool.
+
+**How to Get:**
+1. In the [Google Cloud Console](https://console.cloud.google.com/security/privateca), navigate to your CA pool.
+2. Click the CA name and go to the **Certificates** tab.
+3. Download the **root** and **intermediate** certificates for the issuing CA in PEM format.
+
+#### ZeroSSL
+
+- **Root**: USERTrust RSA Certification Authority
+- **Intermediate**: ZeroSSL RSA Domain Secure Site CA
+
+**How to Get:**
+- Visit: https://zerossl.com
+- Download the full certificate chain in PEM format.
+- Extract individual certs if needed using OpenSSL or a text editor.
+
+#### Buypass
+
+- **Root**: Buypass Class 3 Root CA
+- **Intermediate**: Buypass Class 3 CA 1 / G2 (depends on issuance)
+
+**How to Get:**
+- Go to: https://www.buypass.com
+- Download both root and intermediate in PEM or DER format.
+
+---
+
+### 🧩 Installing Certificates on the Keyfactor Gateway Server
+
+Once downloaded, the **root and intermediate certificates must be installed** in the proper Windows certificate stores on the Gateway server.
+
+#### Steps:
+
+1. **Open** `certlm.msc` (Local Computer Certificates)
+2. Install the **Root CA certificate** into:
+ - `Trusted Root Certification Authorities` → `Certificates`
+3. Install the **Intermediate CA certificate** into:
+ - `Intermediate Certification Authorities` → `Certificates`
+
+You can import certificates using the GUI or PowerShell:
+
+```powershell
+Import-Certificate -FilePath "C:\path\to\intermediate.crt" -CertStoreLocation "Cert:\LocalMachine\CA"
+Import-Certificate -FilePath "C:\path\to\root.crt" -CertStoreLocation "Cert:\LocalMachine\Root"
+```
+
+---
+
+### 🔑 Using the Intermediate Thumbprint
+
+When registering a new CA in Keyfactor Command:
+
+- You must specify the **thumbprint** of the Intermediate CA certificate.
+- This is used to associate issued certificates with the correct issuing chain.
+
+**How to Get the Thumbprint:**
+
+1. In `certlm.msc`, open the certificate under **Intermediate Certification Authorities**.
+2. Go to **Details** tab → Scroll to **Thumbprint**.
+3. Copy the hex string (ignore spaces).
+
+---
+
+⚠️ All certificate chains must be trusted by the Gateway OS. If the intermediate is missing or untrusted, issuance will fail or returned certificates may not chain properly.
+
diff --git a/integration-manifest.json b/integration-manifest.json
index 4beca57..9be0a09 100644
--- a/integration-manifest.json
+++ b/integration-manifest.json
@@ -1,12 +1,87 @@
{
- "$schema": "https://keyfactor.github.io/integration-manifest-schema.json",
- "integration_type": "ca-gateway",
- "name": "",
- "status": "prototype",
- "support_level": "community",
- "link_github": false,
- "update_catalog": false,
- "description": "",
- "gateway_framework": "10.x.x",
- "release_dir": "UPDATE-THIS-WITH-PATH-TO-BINARY-BUILD-FOLDER"
-}
+ "$schema": "https://keyfactor.github.io/v2/integration-manifest-schema.json",
+ "name": "Acme AnyCA REST plugin",
+ "release_dir": "AcmeCaPlugin/bin/Release",
+ "release_project": "AcmeCaPlugin/AcmeCaPlugin.csproj",
+ "description": "Enrollment Only AnyCA Gateway REST plugin that works with multiple ACME Providers and DNS Providers",
+ "status": "production",
+ "integration_type": "anyca-plugin",
+ "support_level": "kf-supported",
+ "link_github": true,
+ "update_catalog": true,
+ "gateway_framework": "24.2",
+ "about": {
+ "carest": {
+ "ca_plugin_config": [
+ {
+ "name": "DirectoryUrl",
+ "description": "ACME directory URL (e.g. Let's Encrypt, ZeroSSL, etc.)"
+ },
+ {
+ "name": "Email",
+ "description": "Email for ACME account registration."
+ },
+ {
+ "name": "EabKid",
+ "description": "External Account Binding Key ID (optional)"
+ },
+ {
+ "name": "EabHmacKey",
+ "description": "External Account Binding HMAC key (optional)"
+ },
+ {
+ "name": "SignerEncryptionPhrase",
+ "description": "Used to encrypt singer information when account is saved to disk (optional)"
+ },
+ {
+ "name": "DnsProvider",
+ "description": "DNS Provider to use for ACME DNS-01 challenges (options Google, Cloudflare, AwsRoute53, Azure, Ns1)"
+ },
+ {
+ "name": "Google_ServiceAccountKeyPath",
+ "description": "Google Cloud DNS: Path to service account JSON key file only if using Google DNS (Optional)"
+ },
+ {
+ "name": "Google_ProjectId",
+ "description": "Google Cloud DNS: Project ID only if using Google DNS (Optional)"
+ },
+ {
+ "name": "Cloudflare_ApiToken",
+ "description": "Cloudflare DNS: API Token only if using Cloudflare DNS (Optional)"
+ },
+ {
+ "name": "Azure_ClientId",
+ "description": "Azure DNS: ClientId only if using Azure DNS and Not Managed Itentity in Azure (Optional)"
+ },
+ {
+ "name": "Azure_ClientSecret",
+ "description": "Azure DNS: ClientSecret only if using Azure DNS and Not Managed Itentity in Azure (Optional)"
+ },
+ {
+ "name": "Azure_SubscriptionId",
+ "description": "Azure DNS: SubscriptionId only if using Azure DNS and Not Managed Itentity in Azure (Optional)"
+ },
+ {
+ "name": "Azure_TenantId",
+ "description": "Azure DNS: TenantId only if using Azure DNS and Not Managed Itentity in Azure (Optional)"
+ },
+ {
+ "name": "AwsRoute53_AccessKey",
+ "description": "Aws DNS: Access Key only if not using AWS DNS and default AWS Chain Creds on AWS (Optional)"
+ },
+ {
+ "name": "AwsRoute53_SecretKey",
+ "description": "Aws DNS: Secret Key only if using AWS DNS and not using default AWS Chain Creds on AWS (Optional)"
+ },
+ {
+ "name": "Ns1_ApiKey",
+ "description": "Ns1 DNS: Api Key only if Using Ns1 DNS (Optional)"
+ }
+ ],
+ "enrollment_config": [],
+ "product_ids": [
+ "default"
+ ]
+ }
+ }
+}
\ No newline at end of file
diff --git a/readme_source.md b/readme_source.md
deleted file mode 100644
index 757b425..0000000
--- a/readme_source.md
+++ /dev/null
@@ -1,130 +0,0 @@
-# Introduction
-This AnyGateway plug-in enables issuance, revocation, and synchronization of certificates from offering.
-# Prerequisites
-
-## Certificate Chain
-
-In order to enroll for certificates the Keyfactor Command server must trust the trust chain. Once you create your Root and/or Subordinate CA, make sure to import the certificate chain into the AnyGateway and Command Server certificate store
-
-
-# Install
-* Download latest successful build from [GitHub Releases](../../releases/latest)
-
-* Copy .dll to the Program Files\Keyfactor\Keyfactor AnyGateway directory
-
-* Update the CAProxyServer.config file
- * Update the CAConnection section to point at the DigiCertCAProxy class
- ```xml
-
- ```
-
-# Configuration
-The following sections will breakdown the required configurations for the AnyGatewayConfig.json file that will be imported to configure the AnyGateway.
-
-## Templates
-The Template section will map the CA's products to an AD template.
-* ```ProductID```
-This is the ID of the product to map to the specified template.
-
- ```json
- "Templates": {
- "WebServer": {
- "ProductID": "",
- "Parameters": {
- }
- }
-}
- ```
-
-## Security
-The security section does not change specifically for the CA Gateway. Refer to the AnyGateway Documentation for more detail.
-```json
- /*Grant permissions on the CA to users or groups in the local domain.
- READ: Enumerate and read contents of certificates.
- ENROLL: Request certificates from the CA.
- OFFICER: Perform certificate functions such as issuance and revocation. This is equivalent to "Issue and Manage" permission on the Microsoft CA.
- ADMINISTRATOR: Configure/reconfigure the gateway.
- Valid permission settings are "Allow", "None", and "Deny".*/
- "Security": {
- "Keyfactor\\Administrator": {
- "READ": "Allow",
- "ENROLL": "Allow",
- "OFFICER": "Allow",
- "ADMINISTRATOR": "Allow"
- },
- "Keyfactor\\gateway_test": {
- "READ": "Allow",
- "ENROLL": "Allow",
- "OFFICER": "Allow",
- "ADMINISTRATOR": "Allow"
- },
- "Keyfactor\\SVC_TimerService": {
- "READ": "Allow",
- "ENROLL": "Allow",
- "OFFICER": "Allow",
- "ADMINISTRATOR": "None"
- },
- "Keyfactor\\SVC_AppPool": {
- "READ": "Allow",
- "ENROLL": "Allow",
- "OFFICER": "Allow",
- "ADMINISTRATOR": "Allow"
- }
- }
-```
-## CerificateManagers
-The Certificate Managers section is optional.
- If configured, all users or groups granted OFFICER permissions under the Security section
- must be configured for at least one Template and one Requester.
- Uses "" to specify all templates. Uses "Everyone" to specify all requesters.
- Valid permission values are "Allow" and "Deny".
-```json
- "CertificateManagers":{
- "DOMAIN\\Username":{
- "Templates":{
- "MyTemplateShortName":{
- "Requesters":{
- "Everyone":"Allow",
- "DOMAIN\\Groupname":"Deny"
- }
- },
- "":{
- "Requesters":{
- "Everyone":"Allow"
- }
- }
- }
- }
- }
-```
-## CAConnection
-The CA Connection section will determine the API endpoint and configuration data used to connect to the API.
-
-
-```json
- "CAConnection": {
-
- },
-```
-## GatewayRegistration
-There are no specific Changes for the GatewayRegistration section. Refer to the AnyGateway Documentation for more detail.
-```json
- "GatewayRegistration": {
- "LogicalName": "CASandbox",
- "GatewayCertificate": {
- "StoreName": "CA",
- "StoreLocation": "LocalMachine",
- "Thumbprint": "0123456789abcdef"
- }
- }
-```
-
-## ServiceSettings
-There are no specific Changes for the ServiceSettings section. Refer to the AnyGateway Documentation for more detail.
-```json
- "ServiceSettings": {
- "ViewIdleMinutes": 8,
- "FullScanPeriodHours": 24,
- "PartialScanPeriodMinutes": 240
- }
-```