From a76c620dfc8b7939f6f2bab241d5ed1da3b12f90 Mon Sep 17 00:00:00 2001 From: Brian Hill Date: Thu, 15 Jan 2026 14:55:22 -0500 Subject: [PATCH 1/6] saas fixes --- AcmeCaPlugin/AcmeCaPluginConfig.cs | 16 ++++ AcmeCaPlugin/AcmeClientConfig.cs | 4 + AcmeCaPlugin/Clients/Acme/AccountManager.cs | 27 +++++- .../Clients/Acme/AcmeClientManager.cs | 2 +- .../Clients/DNS/DnsProviderFactory.cs | 1 + AcmeCaPlugin/Clients/DNS/GoogleDnsProvider.cs | 17 ++-- docsource/configuration.md | 82 ++++++++++++++++--- 7 files changed, 129 insertions(+), 20 deletions(-) diff --git a/AcmeCaPlugin/AcmeCaPluginConfig.cs b/AcmeCaPlugin/AcmeCaPluginConfig.cs index 0118de2..647a3d4 100644 --- a/AcmeCaPlugin/AcmeCaPluginConfig.cs +++ b/AcmeCaPlugin/AcmeCaPluginConfig.cs @@ -60,6 +60,13 @@ public static Dictionary GetPluginAnnotations() DefaultValue = "", Type = "String" }, + ["Google_ServiceAccountKeyJson"] = new PropertyConfigInfo() + { + Comments = "Google Cloud DNS: Service account JSON key content (alternative to file path for containerized deployments)", + Hidden = true, + DefaultValue = "", + Type = "Secret" + }, ["Google_ProjectId"] = new PropertyConfigInfo() { Comments = "Google Cloud DNS: Project ID only if using Google DNS (Optional)", @@ -68,6 +75,15 @@ public static Dictionary GetPluginAnnotations() Type = "String" }, + // Container Deployment + ["AccountStoragePath"] = new PropertyConfigInfo() + { + Comments = "Path for ACME account storage. Defaults to %APPDATA%\\AcmeAccounts on Windows or ./AcmeAccounts in containers.", + Hidden = false, + DefaultValue = "", + Type = "String" + }, + // Cloudflare DNS ["Cloudflare_ApiToken"] = new PropertyConfigInfo() { diff --git a/AcmeCaPlugin/AcmeClientConfig.cs b/AcmeCaPlugin/AcmeClientConfig.cs index 93963a8..2d72735 100644 --- a/AcmeCaPlugin/AcmeClientConfig.cs +++ b/AcmeCaPlugin/AcmeClientConfig.cs @@ -15,6 +15,7 @@ public class AcmeClientConfig // Google Cloud DNS public string Google_ServiceAccountKeyPath { get; set; } = null; + public string Google_ServiceAccountKeyJson { get; set; } = null; public string Google_ProjectId { get; set; } = null; // Cloudflare DNS @@ -34,5 +35,8 @@ public class AcmeClientConfig //IBM NS1 DNS Ns1_ApiKey public string Ns1_ApiKey { get; set; } = null; + // Container Deployment Support + public string AccountStoragePath { get; set; } = null; + } } diff --git a/AcmeCaPlugin/Clients/Acme/AccountManager.cs b/AcmeCaPlugin/Clients/Acme/AccountManager.cs index 5345368..07ed3fb 100644 --- a/AcmeCaPlugin/Clients/Acme/AccountManager.cs +++ b/AcmeCaPlugin/Clients/Acme/AccountManager.cs @@ -39,13 +39,32 @@ class AccountManager #region Constructor - public AccountManager(ILogger log, string passphrase = null) + public AccountManager(ILogger log, string passphrase = null, string storagePath = null) { _log = log; _passphrase = passphrase; - _basePath = Path.Combine( - Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), - "AcmeAccounts"); + + if (!string.IsNullOrWhiteSpace(storagePath)) + { + // Use the explicitly configured path + _basePath = storagePath; + } + else + { + // Default: Use platform-appropriate path + var appDataPath = Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData); + if (string.IsNullOrEmpty(appDataPath)) + { + // In containers, APPDATA may not be set; use current directory + _basePath = Path.Combine(Directory.GetCurrentDirectory(), "AcmeAccounts"); + } + else + { + _basePath = Path.Combine(appDataPath, "AcmeAccounts"); + } + } + + _log.LogDebug("Account storage path configured: {BasePath}", _basePath); } #endregion diff --git a/AcmeCaPlugin/Clients/Acme/AcmeClientManager.cs b/AcmeCaPlugin/Clients/Acme/AcmeClientManager.cs index 5cbb1b8..e12e6fa 100644 --- a/AcmeCaPlugin/Clients/Acme/AcmeClientManager.cs +++ b/AcmeCaPlugin/Clients/Acme/AcmeClientManager.cs @@ -65,7 +65,7 @@ public AcmeClientManager(ILogger log, AcmeClientConfig config, HttpClient httpCl _email = config.Email; _eabKid = config.EabKid; _eabHmac = config.EabHmacKey; - _accountManager = new AccountManager(log,config.SignerEncryptionPhrase); + _accountManager = new AccountManager(log, config.SignerEncryptionPhrase, config.AccountStoragePath); _log.LogDebug("AcmeClientManager initialized for directory: {DirectoryUrl}", _directoryUrl); } diff --git a/AcmeCaPlugin/Clients/DNS/DnsProviderFactory.cs b/AcmeCaPlugin/Clients/DNS/DnsProviderFactory.cs index 011a528..2b84df7 100644 --- a/AcmeCaPlugin/Clients/DNS/DnsProviderFactory.cs +++ b/AcmeCaPlugin/Clients/DNS/DnsProviderFactory.cs @@ -15,6 +15,7 @@ public static IDnsProvider Create(AcmeClientConfig config, ILogger logger) case "google": return new GoogleDnsProvider( config.Google_ServiceAccountKeyPath, + config.Google_ServiceAccountKeyJson, config.Google_ProjectId ); diff --git a/AcmeCaPlugin/Clients/DNS/GoogleDnsProvider.cs b/AcmeCaPlugin/Clients/DNS/GoogleDnsProvider.cs index c82de75..951630f 100644 --- a/AcmeCaPlugin/Clients/DNS/GoogleDnsProvider.cs +++ b/AcmeCaPlugin/Clients/DNS/GoogleDnsProvider.cs @@ -9,7 +9,7 @@ /// /// Google Cloud DNS provider implementation for managing DNS TXT records. -/// Supports explicit Service Account key or Workload Identity (Application Default Credentials). +/// Supports explicit Service Account key (file or JSON), or Workload Identity (Application Default Credentials). /// public class GoogleDnsProvider : IDnsProvider { @@ -18,19 +18,26 @@ public class GoogleDnsProvider : IDnsProvider /// /// Initializes a new instance of the GoogleDnsProvider class. - /// If serviceAccountKeyPath is null or empty, uses Application Default Credentials. + /// Credential resolution order: JSON key > File path > Application Default Credentials. /// /// Path to the Service Account JSON key file (optional) + /// Service Account JSON key as a string (optional, for containerized deployments) /// Google Cloud project ID containing the DNS zones - public GoogleDnsProvider(string? serviceAccountKeyPath, string projectId) + public GoogleDnsProvider(string? serviceAccountKeyPath, string? serviceAccountKeyJson, string projectId) { _projectId = projectId; GoogleCredential credential; - if (!string.IsNullOrWhiteSpace(serviceAccountKeyPath)) + if (!string.IsNullOrWhiteSpace(serviceAccountKeyJson)) { - Console.WriteLine("✅ Using explicit Service Account JSON key."); + // JSON key provided directly (for container deployments) + Console.WriteLine("✅ Using Service Account JSON key from configuration."); + credential = GoogleCredential.FromJson(serviceAccountKeyJson); + } + else if (!string.IsNullOrWhiteSpace(serviceAccountKeyPath)) + { + Console.WriteLine("✅ Using Service Account JSON key from file."); credential = GoogleCredential.FromFile(serviceAccountKeyPath); } else diff --git a/docsource/configuration.md b/docsource/configuration.md index 56b652b..339e91c 100644 --- a/docsource/configuration.md +++ b/docsource/configuration.md @@ -65,7 +65,7 @@ This plugin automates DNS-01 challenges using pluggable DNS provider implementat | Provider | Auth Methods Supported | Config Keys Required | |--------------|-----------------------------------------------|--------------------------------------------------------| -| Google DNS | Service Account Key or ADC | `Google_ServiceAccountKeyPath`, `Google_ProjectId` | +| Google DNS | Service Account Key (file or JSON), or ADC | `Google_ServiceAccountKeyPath`, `Google_ServiceAccountKeyJson`, `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` | @@ -87,8 +87,9 @@ This logic is handled by the `DnsVerificationHelper` class and ensures a high-co Each provider supports multiple credential strategies: -- **Google DNS**: - - ✅ **Service Account Key** (via `Google_ServiceAccountKeyPath`) +- **Google DNS**: + - ✅ **Service Account Key File** (via `Google_ServiceAccountKeyPath`) + - ✅ **Service Account Key JSON** (via `Google_ServiceAccountKeyJson` - paste JSON directly) - ✅ **Application Default Credentials** (e.g., GCP Workload Identity or developer auth) - **AWS Route 53**: @@ -229,12 +230,17 @@ This ACME Gateway implementation uses a local file-based store to persist ACME a
📁 Account Directory Structure -Each account is saved in its own directory within: +Each account is saved in its own directory within the configured storage path: ``` -%APPDATA%\AcmeAccounts\{host}_{accountId} +{AccountStoragePath}\{host}_{accountId} ``` +**Default paths:** +- **Windows:** `%APPDATA%\AcmeAccounts\{host}_{accountId}` +- **Containers (when APPDATA unavailable):** `./AcmeAccounts\{host}_{accountId}` +- **Custom:** Set `AccountStoragePath` in the Gateway configuration + 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 @@ -344,10 +350,10 @@ This section outlines all required ports, file access, permissions, and validati | 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 | +| `%APPDATA%\AcmeAccounts\` or `AccountStoragePath` | Base path for ACME account storage (configurable) | +| `{base}\{account_id}\Registration_v2` | Contains serialized ACME account metadata | +| `{base}\{account_id}\Signer_v2` | Contains the encrypted private signer key | +| `{base}\default_{host}.txt` | Stores the default account pointer for a given directory | #### File Access & Permissions @@ -357,7 +363,8 @@ This section outlines all required ports, file access, permissions, and validati | 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. +- Ensure the service account under which the orchestrator runs has read/write access to the configured base path. +- For containers, mount a persistent volume to the `AccountStoragePath` to preserve accounts across restarts.
@@ -384,6 +391,61 @@ This section outlines all required ports, file access, permissions, and validati +--- + +### Container Deployment + +This section covers configuration options specific to containerized deployments (Docker, Kubernetes, etc.). + +
+📁 Configurable Account Storage Path + +By default, the plugin stores ACME accounts in `%APPDATA%\AcmeAccounts` on Windows. In containerized environments, use the `AccountStoragePath` configuration option: + +| Environment | Recommended Path | +|-------------|------------------| +| Docker/Kubernetes | `/data/AcmeAccounts` (mounted volume) | +| Windows Container | `C:\AcmeData\AcmeAccounts` | + +If `AccountStoragePath` is not set and `%APPDATA%` is unavailable, the plugin defaults to `./AcmeAccounts` relative to the working directory. + +
+ +
+🌐 Google Cloud DNS in Containers + +For Google Cloud DNS in container environments, you have three authentication options: + +1. **Workload Identity (GKE)**: No explicit credentials needed; uses pod identity. +2. **JSON key in config**: Paste the service account JSON directly into `Google_ServiceAccountKeyJson`. +3. **Mounted JSON file**: Mount the service account key file and set `Google_ServiceAccountKeyPath`. + +
+ +
+☸️ Kubernetes Deployment Considerations + +When deploying in Kubernetes: + +1. **Persistent Storage**: Use a PersistentVolumeClaim for `AccountStoragePath` to preserve ACME accounts across pod restarts. +2. **Cloud Provider Identity**: Leverage Workload Identity (GKE), IAM Roles for Service Accounts (EKS), or Pod Identity (AKS) for DNS provider authentication. + +**Example PersistentVolumeClaim:** +```yaml +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + name: acme-accounts +spec: + accessModes: + - ReadWriteOnce + resources: + requests: + storage: 100Mi +``` + +
+ ## Gateway Registration From b222ef502f8707e46f52303ff81f3bdf242bf687 Mon Sep 17 00:00:00 2001 From: Keyfactor Date: Thu, 15 Jan 2026 19:57:25 +0000 Subject: [PATCH 2/6] Update generated docs --- README.md | 84 ++++++++++++++++++++++++++++++++++----- integration-manifest.json | 8 ++++ 2 files changed, 82 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index cf34864..f8b9dc0 100644 --- a/README.md +++ b/README.md @@ -104,7 +104,7 @@ This plugin automates DNS-01 challenges using pluggable DNS provider implementat | Provider | Auth Methods Supported | Config Keys Required | |--------------|-----------------------------------------------|--------------------------------------------------------| -| Google DNS | Service Account Key or ADC | `Google_ServiceAccountKeyPath`, `Google_ProjectId` | +| Google DNS | Service Account Key (file or JSON), or ADC | `Google_ServiceAccountKeyPath`, `Google_ServiceAccountKeyJson`, `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` | @@ -126,8 +126,9 @@ This logic is handled by the `DnsVerificationHelper` class and ensures a high-co Each provider supports multiple credential strategies: -- **Google DNS**: - - ✅ **Service Account Key** (via `Google_ServiceAccountKeyPath`) +- **Google DNS**: + - ✅ **Service Account Key File** (via `Google_ServiceAccountKeyPath`) + - ✅ **Service Account Key JSON** (via `Google_ServiceAccountKeyJson` - paste JSON directly) - ✅ **Application Default Credentials** (e.g., GCP Workload Identity or developer auth) - **AWS Route 53**: @@ -268,12 +269,17 @@ This ACME Gateway implementation uses a local file-based store to persist ACME a
📁 Account Directory Structure -Each account is saved in its own directory within: +Each account is saved in its own directory within the configured storage path: ``` -%APPDATA%\AcmeAccounts\{host}_{accountId} +{AccountStoragePath}\{host}_{accountId} ``` +**Default paths:** +- **Windows:** `%APPDATA%\AcmeAccounts\{host}_{accountId}` +- **Containers (when APPDATA unavailable):** `./AcmeAccounts\{host}_{accountId}` +- **Custom:** Set `AccountStoragePath` in the Gateway configuration + 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 @@ -383,10 +389,10 @@ This section outlines all required ports, file access, permissions, and validati | 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 | +| `%APPDATA%\AcmeAccounts\` or `AccountStoragePath` | Base path for ACME account storage (configurable) | +| `{base}\{account_id}\Registration_v2` | Contains serialized ACME account metadata | +| `{base}\{account_id}\Signer_v2` | Contains the encrypted private signer key | +| `{base}\default_{host}.txt` | Stores the default account pointer for a given directory | #### File Access & Permissions @@ -396,7 +402,8 @@ This section outlines all required ports, file access, permissions, and validati | 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. +- Ensure the service account under which the orchestrator runs has read/write access to the configured base path. +- For containers, mount a persistent volume to the `AccountStoragePath` to preserve accounts across restarts.
@@ -423,6 +430,61 @@ This section outlines all required ports, file access, permissions, and validati +--- + +### Container Deployment + +This section covers configuration options specific to containerized deployments (Docker, Kubernetes, etc.). + +
+📁 Configurable Account Storage Path + +By default, the plugin stores ACME accounts in `%APPDATA%\AcmeAccounts` on Windows. In containerized environments, use the `AccountStoragePath` configuration option: + +| Environment | Recommended Path | +|-------------|------------------| +| Docker/Kubernetes | `/data/AcmeAccounts` (mounted volume) | +| Windows Container | `C:\AcmeData\AcmeAccounts` | + +If `AccountStoragePath` is not set and `%APPDATA%` is unavailable, the plugin defaults to `./AcmeAccounts` relative to the working directory. + +
+ +
+🌐 Google Cloud DNS in Containers + +For Google Cloud DNS in container environments, you have three authentication options: + +1. **Workload Identity (GKE)**: No explicit credentials needed; uses pod identity. +2. **JSON key in config**: Paste the service account JSON directly into `Google_ServiceAccountKeyJson`. +3. **Mounted JSON file**: Mount the service account key file and set `Google_ServiceAccountKeyPath`. + +
+ +
+☸️ Kubernetes Deployment Considerations + +When deploying in Kubernetes: + +1. **Persistent Storage**: Use a PersistentVolumeClaim for `AccountStoragePath` to preserve ACME accounts across pod restarts. +2. **Cloud Provider Identity**: Leverage Workload Identity (GKE), IAM Roles for Service Accounts (EKS), or Pod Identity (AKS) for DNS provider authentication. + +**Example PersistentVolumeClaim:** +```yaml +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + name: acme-accounts +spec: + accessModes: + - ReadWriteOnce + resources: + requests: + storage: 100Mi +``` + +
+ ## Installation 1. Install the AnyCA Gateway REST per the [official Keyfactor documentation](https://software.keyfactor.com/Guides/AnyCAGatewayREST/Content/AnyCAGatewayREST/InstallIntroduction.htm). @@ -546,7 +608,9 @@ This section outlines all required ports, file access, permissions, and validati * **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_ServiceAccountKeyJson** - Google Cloud DNS: Service account JSON key content (alternative to file path for containerized deployments) * **Google_ProjectId** - Google Cloud DNS: Project ID only if using Google DNS (Optional) + * **AccountStoragePath** - Path for ACME account storage. Defaults to %APPDATA%\AcmeAccounts on Windows or ./AcmeAccounts in containers. * **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) diff --git a/integration-manifest.json b/integration-manifest.json index 9be0a09..3c40df1 100644 --- a/integration-manifest.json +++ b/integration-manifest.json @@ -41,10 +41,18 @@ "name": "Google_ServiceAccountKeyPath", "description": "Google Cloud DNS: Path to service account JSON key file only if using Google DNS (Optional)" }, + { + "name": "Google_ServiceAccountKeyJson", + "description": "Google Cloud DNS: Service account JSON key content (alternative to file path for containerized deployments)" + }, { "name": "Google_ProjectId", "description": "Google Cloud DNS: Project ID only if using Google DNS (Optional)" }, + { + "name": "AccountStoragePath", + "description": "Path for ACME account storage. Defaults to %APPDATA%\\AcmeAccounts on Windows or ./AcmeAccounts in containers." + }, { "name": "Cloudflare_ApiToken", "description": "Cloudflare DNS: API Token only if using Cloudflare DNS (Optional)" From e1c7a908c412e3acc9ea84b55bd17ba7fd71a909 Mon Sep 17 00:00:00 2001 From: Brian Hill Date: Thu, 22 Jan 2026 10:36:14 -0500 Subject: [PATCH 3/6] fixed CAID length issue --- AcmeCaPlugin/AcmeCaPlugin.cs | 44 +++++++++++++++++++++++++++++++++--- 1 file changed, 41 insertions(+), 3 deletions(-) diff --git a/AcmeCaPlugin/AcmeCaPlugin.cs b/AcmeCaPlugin/AcmeCaPlugin.cs index fa1a5cf..c64b8f9 100644 --- a/AcmeCaPlugin/AcmeCaPlugin.cs +++ b/AcmeCaPlugin/AcmeCaPlugin.cs @@ -262,6 +262,9 @@ public async Task Enroll( // Create order var order = await acmeClient.CreateOrderAsync(identifiers, null); + _logger.LogInformation("Order created. OrderUrl: {OrderUrl}, Status: {Status}", + order.OrderUrl, order.Payload?.Status); + // Store pending order immediately var accountId = accountDetails.Kid.Split('/').Last(); @@ -271,26 +274,33 @@ public async Task Enroll( // Finalize with original CSR bytes order = await acmeClient.FinalizeOrderAsync(order, csrBytes); + // Extract order identifier (path only) for database storage + var orderIdentifier = ExtractOrderIdentifier(order.OrderUrl); + // If order is valid immediately, download cert if (order.Payload?.Status == "valid" && !string.IsNullOrEmpty(order.Payload.Certificate)) { var certBytes = await acmeClient.GetCertificateAsync(order); var certPem = EncodeToPem(certBytes, "CERTIFICATE"); + _logger.LogInformation("✅ Enrollment completed successfully. OrderUrl: {OrderUrl}, CARequestID: {OrderId}, Status: GENERATED", + order.OrderUrl, orderIdentifier); + return new EnrollmentResult { - CARequestID = order.Payload.Finalize, + CARequestID = orderIdentifier, Certificate = certPem, Status = (int)EndEntityStatus.GENERATED }; } else { - _logger.LogInformation("⏳ Order not valid yet — will be synced later. Status: {Status}", order.Payload?.Status); + _logger.LogInformation("⏳ Order not valid yet — will be synced later. OrderUrl: {OrderUrl}, CARequestID: {OrderId}, Status: {Status}", + order.OrderUrl, orderIdentifier, order.Payload?.Status); // Order stays saved for next sync return new EnrollmentResult { - CARequestID = order.Payload.Finalize, + CARequestID = orderIdentifier, Status = (int)EndEntityStatus.FAILED, StatusMessage = "Could not retrieve order in allowed time." }; @@ -314,6 +324,34 @@ public async Task Enroll( + /// + /// Extracts the order path from the full ACME order URL for use as a unique identifier. + /// This removes the scheme, host, and port, keeping only the path portion. + /// + /// Full order URL (e.g., https://dv.acme-v02.api.pki.goog/order/ABC123) + /// Order path without leading slash (e.g., "order/ABC123") + /// + /// Input: "https://dv.acme-v02.api.pki.goog/order/IlYl06mPl5VcAQpx3pzR6w" + /// Output: "order/IlYl06mPl5VcAQpx3pzR6w" + /// + private static string ExtractOrderIdentifier(string orderUrl) + { + if (string.IsNullOrWhiteSpace(orderUrl)) + return orderUrl; + + try + { + var uri = new Uri(orderUrl); + // Remove leading slash and return the path + return uri.AbsolutePath.TrimStart('/'); + } + catch (Exception) + { + // If URL parsing fails, return the original (shouldn't happen with valid ACME URLs) + return orderUrl; + } + } + /// /// Extracts the domain name from X.509 subject string /// From 698640d36ff6b9b707ace251f57a2e65edf4c733 Mon Sep 17 00:00:00 2001 From: Brian Hill Date: Thu, 22 Jan 2026 14:41:07 -0500 Subject: [PATCH 4/6] .net 10 build --- AcmeCaPlugin/AcmeCaPlugin.csproj | 2 +- TestProgram/TestProgram.csproj | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/AcmeCaPlugin/AcmeCaPlugin.csproj b/AcmeCaPlugin/AcmeCaPlugin.csproj index 1adf8a5..d4df242 100644 --- a/AcmeCaPlugin/AcmeCaPlugin.csproj +++ b/AcmeCaPlugin/AcmeCaPlugin.csproj @@ -1,6 +1,6 @@ - net6.0;net8.0 + net6.0;net8.0;net10.0 disable disable true diff --git a/TestProgram/TestProgram.csproj b/TestProgram/TestProgram.csproj index 127834b..95a3c2c 100644 --- a/TestProgram/TestProgram.csproj +++ b/TestProgram/TestProgram.csproj @@ -2,7 +2,7 @@ Exe - net6.0;net8.0 + net6.0;net8.0;net10.0 enable enable From a27abb5ca251fd5b18db173a371f59e139dcb344 Mon Sep 17 00:00:00 2001 From: Brian Hill Date: Tue, 27 Jan 2026 08:54:51 -0500 Subject: [PATCH 5/6] updated git ignore --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 3e759b7..67cc688 100644 --- a/.gitignore +++ b/.gitignore @@ -328,3 +328,4 @@ ASALocalRun/ # MFractors (Xamarin productivity tool) working folder .mfractor/ +/.claude From e1ccac87239719f7462075c6e97a35efd482a881 Mon Sep 17 00:00:00 2001 From: Brian Hill Date: Tue, 27 Jan 2026 13:04:30 -0500 Subject: [PATCH 6/6] project fixes --- AcmeCaPlugin/AcmeCaPlugin.csproj | 37 +++++++++++++++++--------------- TestProgram/TestProgram.csproj | 6 +++++- 2 files changed, 25 insertions(+), 18 deletions(-) diff --git a/AcmeCaPlugin/AcmeCaPlugin.csproj b/AcmeCaPlugin/AcmeCaPlugin.csproj index d4df242..e2a323d 100644 --- a/AcmeCaPlugin/AcmeCaPlugin.csproj +++ b/AcmeCaPlugin/AcmeCaPlugin.csproj @@ -9,23 +9,26 @@ AcmeCaPlugin - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + diff --git a/TestProgram/TestProgram.csproj b/TestProgram/TestProgram.csproj index 95a3c2c..0f93419 100644 --- a/TestProgram/TestProgram.csproj +++ b/TestProgram/TestProgram.csproj @@ -8,10 +8,14 @@ - + + + + +