mirror of
https://github.com/shankar0123/certctl.git
synced 2026-06-08 15:18:53 +00:00
Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 03593d4304 | |||
| 87355c3efb |
@@ -70,7 +70,7 @@ certctl gives you a single pane of glass for every TLS certificate in your organ
|
||||
**Core capabilities:**
|
||||
|
||||
- **Full lifecycle automation** — issuance, renewal, deployment, and revocation with zero human intervention. Configurable renewal policies trigger jobs automatically based on expiration thresholds.
|
||||
- **CA-agnostic issuer connectors** — Local CA (self-signed + sub-CA for enterprise root chains), ACME v2 with HTTP-01, DNS-01, and DNS-PERSIST-01 challenges (Let's Encrypt, Sectigo, any ACME-compatible CA), Smallstep step-ca (native /sign API), and OpenSSL/Custom CA (delegate to any shell script). Pluggable interface — add your own CA in one file.
|
||||
- **CA-agnostic issuer connectors** — Local CA (self-signed + sub-CA for enterprise root chains), ACME v2 with HTTP-01, DNS-01, and DNS-PERSIST-01 challenges (Let's Encrypt, ZeroSSL, Sectigo, Google Trust Services, any ACME-compatible CA), External Account Binding (EAB) for CAs that require it (auto-fetched for ZeroSSL), Smallstep step-ca (native /sign API), and OpenSSL/Custom CA (delegate to any shell script). Pluggable interface — add your own CA in one file.
|
||||
- **Agent-side key generation** — agents generate ECDSA P-256 keys locally, store them with 0600 permissions, and submit only the CSR. Private keys never touch the control plane. This is the default mode, not an opt-in feature.
|
||||
- **Certificate discovery** — agents scan filesystems for existing PEM/DER certificates and report findings for triage. The network scanner probes TLS endpoints across CIDR ranges to find certificates you didn't know existed.
|
||||
- **Revocation infrastructure** — RFC 5280 revocation with all standard reason codes, DER-encoded X.509 CRL per issuer, embedded OCSP responder, and short-lived certificate exemption (certs under 1 hour skip CRL/OCSP).
|
||||
@@ -220,6 +220,8 @@ All server environment variables use the `CERTCTL_` prefix:
|
||||
| `CERTCTL_KEYGEN_MODE` | `agent` | Key generation mode: `agent` (production) or `server` (demo only) |
|
||||
| `CERTCTL_ACME_DIRECTORY_URL` | — | ACME directory URL (e.g., Let's Encrypt staging) |
|
||||
| `CERTCTL_ACME_EMAIL` | — | Contact email for ACME account registration |
|
||||
| `CERTCTL_ACME_EAB_KID` | — | External Account Binding Key ID (required by ZeroSSL, Google Trust Services, SSL.com) |
|
||||
| `CERTCTL_ACME_EAB_HMAC` | — | External Account Binding HMAC key (base64url-encoded) |
|
||||
| `CERTCTL_ACME_CHALLENGE_TYPE` | — | ACME challenge type: `http-01` (default), `dns-01`, or `dns-persist-01` |
|
||||
| `CERTCTL_CA_CERT_PATH` | — | Path to CA certificate for sub-CA mode |
|
||||
| `CERTCTL_CA_KEY_PATH` | — | Path to CA private key for sub-CA mode |
|
||||
|
||||
+4
-1
@@ -97,11 +97,14 @@ func main() {
|
||||
localCA := local.New(localCAConfig, logger)
|
||||
logger.Info("initialized Local CA issuer connector")
|
||||
|
||||
// Initialize ACME issuer connector (for Let's Encrypt, Sectigo, etc.)
|
||||
// Initialize ACME issuer connector (for Let's Encrypt, ZeroSSL, Sectigo, Google Trust Services, etc.)
|
||||
// Supports HTTP-01 (default), DNS-01 (for wildcards), and DNS-PERSIST-01 (standing record) challenge types.
|
||||
// EAB (External Account Binding) required by ZeroSSL, Google Trust Services, SSL.com.
|
||||
acmeConnector := acmeissuer.New(&acmeissuer.Config{
|
||||
DirectoryURL: os.Getenv("CERTCTL_ACME_DIRECTORY_URL"),
|
||||
Email: os.Getenv("CERTCTL_ACME_EMAIL"),
|
||||
EABKid: os.Getenv("CERTCTL_ACME_EAB_KID"),
|
||||
EABHmac: os.Getenv("CERTCTL_ACME_EAB_HMAC"),
|
||||
ChallengeType: os.Getenv("CERTCTL_ACME_CHALLENGE_TYPE"),
|
||||
DNSPresentScript: os.Getenv("CERTCTL_ACME_DNS_PRESENT_SCRIPT"),
|
||||
DNSCleanUpScript: os.Getenv("CERTCTL_ACME_DNS_CLEANUP_SCRIPT"),
|
||||
|
||||
+38
-2
@@ -1,5 +1,41 @@
|
||||
# Architecture Guide
|
||||
|
||||
## Contents
|
||||
|
||||
1. [Overview](#overview)
|
||||
2. [System Components](#system-components)
|
||||
- [Control Plane (Server)](#control-plane-server)
|
||||
- [Agents](#agents)
|
||||
- [Web Dashboard](#web-dashboard)
|
||||
- [PostgreSQL Database](#postgresql-database)
|
||||
3. [Data Flow: Certificate Lifecycle](#data-flow-certificate-lifecycle)
|
||||
- [Create Managed Certificate](#1-create-managed-certificate)
|
||||
- [Certificate Issuance](#2-certificate-issuance)
|
||||
- [Deploy Certificate to Target](#3-deploy-certificate-to-target)
|
||||
- [Revoke a Certificate](#35-revoke-a-certificate)
|
||||
- [Automatic Renewal](#4-automatic-renewal)
|
||||
4. [Connector Architecture](#connector-architecture)
|
||||
- [IssuerConnectorAdapter (Dependency Inversion)](#issuerconnectoradapter-dependency-inversion)
|
||||
- [Issuer Connector](#issuer-connector)
|
||||
- [Target Connector](#target-connector)
|
||||
- [Notifier Connector](#notifier-connector)
|
||||
- [EST Server (RFC 7030)](#est-server-rfc-7030)
|
||||
5. [Security Model](#security-model)
|
||||
- [Private Key Management](#private-key-management)
|
||||
- [Authentication](#authentication)
|
||||
- [Audit Trail](#audit-trail)
|
||||
- [API Audit Log](#api-audit-log)
|
||||
- [Logging](#logging)
|
||||
6. [API Design](#api-design)
|
||||
7. [MCP Server](#mcp-server)
|
||||
8. [CLI Tool](#cli-tool)
|
||||
9. [Deployment Topologies](#deployment-topologies)
|
||||
- [Docker Compose (Development / Small Deployments)](#docker-compose-development--small-deployments)
|
||||
- [Production (Kubernetes)](#production-kubernetes)
|
||||
10. [Discovery Data Flow (M18b + M21)](#discovery-data-flow-m18b--m21)
|
||||
11. [Testing Strategy](#testing-strategy)
|
||||
12. [What's Next](#whats-next)
|
||||
|
||||
## Overview
|
||||
|
||||
Certctl is a certificate management platform with a **decoupled control-plane and agent architecture**. The control plane orchestrates certificate issuance and renewal, while agents deployed across your infrastructure handle key generation, certificate deployment, and local validation — private keys never leave the infrastructure they were generated on.
|
||||
@@ -41,7 +77,7 @@ flowchart TB
|
||||
|
||||
subgraph "Issuer Backends"
|
||||
CA1["Local CA\n(crypto/x509, sub-CA)"]
|
||||
CA2["ACME\n(HTTP-01 + DNS-01 + DNS-PERSIST-01)"]
|
||||
CA2["ACME\n(HTTP-01 + DNS-01 + DNS-PERSIST-01)\n(EAB, ZeroSSL auto-EAB)"]
|
||||
CA3["step-ca\n(/sign API)"]
|
||||
CA4["OpenSSL / Custom CA\n(script-based)"]
|
||||
CA6["Vault PKI\n(planned)"]
|
||||
@@ -527,7 +563,7 @@ type Connector interface {
|
||||
}
|
||||
```
|
||||
|
||||
Built-in issuers: **Local CA** (self-signed or sub-CA mode using `crypto/x509`), **ACME v2** (HTTP-01, DNS-01, and DNS-PERSIST-01 challenges, compatible with Let's Encrypt, Sectigo, and any ACME-compliant CA), **step-ca** (Smallstep private CA via native /sign API with JWK provisioner auth), and **OpenSSL/Custom CA** (script-based signing delegating to user-provided shell scripts). The ACME connector uses `golang.org/x/crypto/acme`, generates an ECDSA P-256 account key, handles account registration with ToS acceptance, order creation, challenge solving (HTTP-01 via built-in server, DNS-01 via script-based hooks, DNS-PERSIST-01 via standing TXT records with auto-fallback to DNS-01), order finalization, and DER-to-PEM chain conversion. The interface also includes `GetCACertPEM(ctx)` for CA chain distribution (used by the EST server's `/cacerts` endpoint).
|
||||
Built-in issuers: **Local CA** (self-signed or sub-CA mode using `crypto/x509`), **ACME v2** (HTTP-01, DNS-01, and DNS-PERSIST-01 challenges, compatible with Let's Encrypt, ZeroSSL, Sectigo, Google Trust Services, and any ACME-compliant CA), **step-ca** (Smallstep private CA via native /sign API with JWK provisioner auth), and **OpenSSL/Custom CA** (script-based signing delegating to user-provided shell scripts). The ACME connector uses `golang.org/x/crypto/acme`, generates an ECDSA P-256 account key, handles account registration with ToS acceptance and optional External Account Binding (EAB) for CAs that require it (ZeroSSL, Google Trust Services, SSL.com), order creation, challenge solving (HTTP-01 via built-in server, DNS-01 via script-based hooks, DNS-PERSIST-01 via standing TXT records with auto-fallback to DNS-01), order finalization, and DER-to-PEM chain conversion. For ZeroSSL, EAB credentials are auto-fetched from ZeroSSL's public API when the directory URL is detected as ZeroSSL and no EAB credentials are provided — zero-friction onboarding with no dashboard visit required. The interface also includes `GetCACertPEM(ctx)` for CA chain distribution (used by the EST server's `/cacerts` endpoint).
|
||||
|
||||
### Target Connector
|
||||
|
||||
|
||||
@@ -2,6 +2,24 @@
|
||||
|
||||
NIST SP 800-57 Part 1 Rev 5 (May 2020) is the authoritative US government guidance on cryptographic key management. This document maps certctl's implementation to its recommendations. certctl follows NIST guidance where applicable; this guide documents the alignment and identifies gaps for future roadmap planning.
|
||||
|
||||
## Contents
|
||||
|
||||
1. [Key Generation (Section 6.1)](#key-generation-section-61)
|
||||
2. [Key Storage and Protection (Sections 6.3, 6.4)](#key-storage-and-protection-sections-63-64)
|
||||
3. [Cryptoperiods (Section 5.3, Table 1)](#cryptoperiods-section-53-table-1)
|
||||
4. [Key States and Transitions (Section 5.2)](#key-states-and-transitions-section-52)
|
||||
5. [Algorithm Recommendations (Section 5.1, SP 800-131A)](#algorithm-recommendations-section-51-sp-800-131a)
|
||||
6. [Key Distribution and Transport (Section 6.2)](#key-distribution-and-transport-section-62)
|
||||
7. [Revocation and Compromise (NIST SP 800-57 Part 3)](#revocation-and-compromise-nist-sp-800-57-part-3)
|
||||
8. [Alignment Summary Table](#alignment-summary-table)
|
||||
9. [Gaps and Remediation Roadmap](#gaps-and-remediation-roadmap)
|
||||
- [V2 (Current)](#v2-current)
|
||||
- [V3 (Planned: 2026)](#v3-planned-2026)
|
||||
- [V5 (Planned: 2027+)](#v5-planned-2027)
|
||||
- [Post-Quantum (2027+)](#post-quantum-2027)
|
||||
10. [References](#references)
|
||||
11. [Questions or Corrections?](#questions-or-corrections)
|
||||
|
||||
## Key Generation (Section 6.1)
|
||||
|
||||
certctl generates certificate keys on agent infrastructure using Go's `crypto/rand` for entropy, backed by `/dev/urandom` on Linux and `CryptGenRandom` on Windows. Key generation happens as follows:
|
||||
|
||||
@@ -4,6 +4,34 @@ This guide maps certctl's existing capabilities to PCI-DSS 4.0 requirements rele
|
||||
|
||||
Organizations subject to PCI-DSS typically need to demonstrate control over certificate issuance, renewal, rotation, revocation, and key management. Certctl automates the technical controls for certificate lifecycle; compliance depends on how you deploy, monitor, and audit it.
|
||||
|
||||
## Contents
|
||||
|
||||
1. [How to Use This Guide](#how-to-use-this-guide)
|
||||
2. [Requirement 4: Protect Data in Transit](#requirement-4-protect-data-in-transit)
|
||||
- [4.2.1 — Strong Cryptography for Transmission](#421--strong-cryptography-for-transmission)
|
||||
- [4.2.2 — Certificate Inventory and Validation](#422--certificate-inventory-and-validation)
|
||||
3. [Requirement 3: Protect Stored Cardholder Data (Key Management)](#requirement-3-protect-stored-cardholder-data-key-management)
|
||||
- [3.6 — Cryptographic Key Documentation](#36--cryptographic-key-documentation)
|
||||
- [3.7 — Key Lifecycle Procedures](#37--key-lifecycle-procedures)
|
||||
4. [Requirement 8: Identify and Authenticate](#requirement-8-identify-and-authenticate)
|
||||
- [8.3 — Strong Authentication](#83--strong-authentication)
|
||||
- [8.6 — Application Account Management](#86--application-account-management)
|
||||
5. [Requirement 10: Log and Monitor](#requirement-10-log-and-monitor)
|
||||
- [10.2 — Implement Automated Audit Logging](#102--implement-automated-audit-logging)
|
||||
- [10.3 — Protect Audit Trail](#103--protect-audit-trail)
|
||||
- [10.4 — Promptly Review and Address Audit Trail Exceptions](#104--promptly-review-and-address-audit-trail-exceptions)
|
||||
- [10.7 — Retain and Protect Audit Trail History](#107--retain-and-protect-audit-trail-history)
|
||||
6. [Requirement 6: Develop and Maintain Secure Systems and Applications](#requirement-6-develop-and-maintain-secure-systems-and-applications)
|
||||
- [6.3.1 — Security Coding Practices](#631--security-coding-practices)
|
||||
- [6.5.10 — Broken Authentication and Cryptography Prevention](#6510--broken-authentication-and-cryptography-prevention)
|
||||
7. [Requirement 7: Restrict Access by Business Need-to-Know](#requirement-7-restrict-access-by-business-need-to-know)
|
||||
- [7.2 — Implement Access Control](#72--implement-access-control)
|
||||
8. [Evidence Summary Table](#evidence-summary-table)
|
||||
9. [Operator Responsibilities](#operator-responsibilities)
|
||||
10. [V3 Enhancements for PCI-DSS](#v3-enhancements-for-pci-dss)
|
||||
11. [Next Steps for Compliance](#next-steps-for-compliance)
|
||||
12. [Questions?](#questions)
|
||||
|
||||
## How to Use This Guide
|
||||
|
||||
Your QSA will request evidence that your certificate and key management systems meet specific PCI-DSS 4.0 requirements. For each applicable requirement, this guide identifies:
|
||||
|
||||
@@ -14,6 +14,28 @@ Each section includes:
|
||||
- **V2 vs V3 status** — whether feature is in the free community edition (V2) or paid Pro edition (V3)
|
||||
- **Operator responsibility** — aspects your organization must handle outside of certctl
|
||||
|
||||
## Contents
|
||||
|
||||
1. [How to Use This Guide](#how-to-use-this-guide)
|
||||
2. [CC6: Logical and Physical Access Controls](#cc6-logical-and-physical-access-controls)
|
||||
- [CC6.1 — Logical Access Security](#cc61--logical-access-security)
|
||||
- [CC6.2 — Prior to Issuing System Credentials](#cc62--prior-to-issuing-system-credentials)
|
||||
- [CC6.3 — Authentication Policies](#cc63--authentication-policies)
|
||||
- [CC6.7 — Information Transmission Protection](#cc67--information-transmission-protection)
|
||||
3. [CC7: System Operations](#cc7-system-operations)
|
||||
- [CC7.1 — System Monitoring](#cc71--system-monitoring)
|
||||
- [CC7.2 — Anomaly Detection](#cc72--anomaly-detection)
|
||||
- [CC7.3 — Incident Response](#cc73--incident-response)
|
||||
- [CC7.4 — Identify and Develop Risk Mitigation Activities](#cc74--identify-and-develop-risk-mitigation-activities)
|
||||
4. [A1: Availability](#a1-availability)
|
||||
- [A1.1/A1.2 — Availability and Recovery](#a11a12--availability-and-recovery)
|
||||
5. [CC8: Change Management](#cc8-change-management)
|
||||
- [CC8.1 — Change Control](#cc81--change-control)
|
||||
6. [Evidence Summary Table](#evidence-summary-table)
|
||||
7. [What Requires Operator Action](#what-requires-operator-action)
|
||||
8. [V3 Enhancements](#v3-enhancements)
|
||||
9. [Conclusion](#conclusion)
|
||||
|
||||
## CC6: Logical and Physical Access Controls
|
||||
|
||||
### CC6.1 — Logical Access Security
|
||||
|
||||
@@ -2,6 +2,41 @@
|
||||
|
||||
If you've never worked with TLS certificates before, this guide will get you up to speed. By the end, you'll understand what certificates are, why they matter, and why the industry's move toward shorter certificate lifespans — down to 47 days by 2029 — makes automated lifecycle management essential.
|
||||
|
||||
## Contents
|
||||
|
||||
1. [What Is a TLS Certificate?](#what-is-a-tls-certificate)
|
||||
2. [Why Do Certificates Expire?](#why-do-certificates-expire)
|
||||
3. [The Cast of Characters](#the-cast-of-characters)
|
||||
- [Certificate Authority (CA)](#certificate-authority-ca)
|
||||
- [ACME Protocol](#acme-protocol)
|
||||
- [EST Protocol (Enrollment over Secure Transport)](#est-protocol-enrollment-over-secure-transport)
|
||||
- [Private Key](#private-key)
|
||||
- [Subject Alternative Names (SANs)](#subject-alternative-names-sans)
|
||||
- [Certificate Chain](#certificate-chain)
|
||||
4. [How certctl Works](#how-certctl-works)
|
||||
- [The Control Plane (Server)](#the-control-plane-server)
|
||||
- [Agents](#agents)
|
||||
- [Deployment Targets](#deployment-targets)
|
||||
5. [The Certificate Lifecycle](#the-certificate-lifecycle)
|
||||
6. [Why Not Just Use Certbot?](#why-not-just-use-certbot)
|
||||
7. [Key Concepts in certctl](#key-concepts-in-certctl)
|
||||
- [Teams and Owners](#teams-and-owners)
|
||||
- [Agent Groups](#agent-groups)
|
||||
- [Certificate Profiles](#certificate-profiles)
|
||||
- [Interactive Renewal Approval](#interactive-renewal-approval)
|
||||
- [Certificate Revocation](#certificate-revocation)
|
||||
- [Short-Lived Certificates](#short-lived-certificates)
|
||||
- [Policies](#policies)
|
||||
- [Jobs](#jobs)
|
||||
- [Audit Trail](#audit-trail)
|
||||
- [Notifications](#notifications)
|
||||
- [CLI](#cli)
|
||||
- [MCP Server (AI Integration)](#mcp-server-ai-integration)
|
||||
- [EST Enrollment (Device Certificates)](#est-enrollment-device-certificates)
|
||||
- [Certificate Discovery](#certificate-discovery)
|
||||
- [Observability](#observability)
|
||||
8. [What's Next](#whats-next)
|
||||
|
||||
## What Is a TLS Certificate?
|
||||
|
||||
When you visit `https://yourbank.com`, your browser checks a digital document called a **TLS certificate** before sending any data. That certificate proves two things: (1) you're really talking to yourbank.com and not an imposter, and (2) everything sent between you and the server is encrypted.
|
||||
|
||||
@@ -2,6 +2,49 @@
|
||||
|
||||
Connectors extend certctl to integrate with external systems for certificate issuance, deployment, and notifications. This guide covers the connector interfaces, built-in implementations, and how to build your own.
|
||||
|
||||
## Contents
|
||||
|
||||
1. [Overview](#overview)
|
||||
2. [Issuer Connector](#issuer-connector)
|
||||
- [Interface](#interface)
|
||||
- [Built-in: Local CA](#built-in-local-ca)
|
||||
- [Built-in: ACME v2 (Let's Encrypt, Sectigo, ZeroSSL)](#built-in-acme-v2-lets-encrypt-sectigo-zerossl)
|
||||
- [Built-in: step-ca (Smallstep Private CA)](#built-in-step-ca-smallstep-private-ca)
|
||||
- [OpenSSL / Custom CA](#openssl--custom-ca)
|
||||
- [Revocation Across Issuers](#revocation-across-issuers)
|
||||
- [EST Integration (GetCACertPEM)](#est-integration-getcacertpem)
|
||||
- [Planned Issuers](#planned-issuers)
|
||||
- [Building a Custom Issuer](#building-a-custom-issuer)
|
||||
3. [Target Connector](#target-connector)
|
||||
- [Interface](#interface-1)
|
||||
- [Built-in: NGINX](#built-in-nginx)
|
||||
- [Built-in: Apache httpd](#built-in-apache-httpd)
|
||||
- [Built-in: HAProxy](#built-in-haproxy)
|
||||
- [F5 BIG-IP (Interface Only)](#f5-big-ip-interface-only)
|
||||
- [IIS (Interface Only, Dual-Mode)](#iis-interface-only-dual-mode)
|
||||
4. [Notifier Connector](#notifier-connector)
|
||||
- [Interface](#interface-2)
|
||||
5. [Registering a Connector](#registering-a-connector)
|
||||
- [IssuerConnectorAdapter](#issuerconnectoradapter)
|
||||
- [Notifier Registration](#notifier-registration)
|
||||
6. [Testing Connectors](#testing-connectors)
|
||||
- [Unit Tests](#unit-tests)
|
||||
- [Integration Tests](#integration-tests)
|
||||
7. [Best Practices](#best-practices)
|
||||
8. [Agent Discovery Scanner](#agent-discovery-scanner)
|
||||
- [Configuration](#configuration)
|
||||
- [How It Works](#how-it-works)
|
||||
- [API Endpoints](#api-endpoints)
|
||||
- [Use Cases](#use-cases)
|
||||
9. [Network Certificate Scanner (M21)](#network-certificate-scanner-m21)
|
||||
- [Configuration](#configuration-1)
|
||||
- [Creating Scan Targets](#creating-scan-targets)
|
||||
- [How It Works](#how-it-works-1)
|
||||
- [API Endpoints](#api-endpoints-1)
|
||||
- [Scheduler Integration](#scheduler-integration)
|
||||
- [Use Cases](#use-cases-1)
|
||||
10. [What's Next](#whats-next)
|
||||
|
||||
## Overview
|
||||
|
||||
Three types of connectors:
|
||||
@@ -159,11 +202,35 @@ DNS-PERSIST-01 configuration:
|
||||
|
||||
The present script creates a TXT record at `_validation-persist.<domain>` with the value `letsencrypt.org; accounturi=https://acme-v02.api.letsencrypt.org/acme/acct/<your-id>`. This record is permanent — no cleanup script is needed.
|
||||
|
||||
ZeroSSL configuration (requires External Account Binding):
|
||||
```json
|
||||
{
|
||||
"directory_url": "https://acme.zerossl.com/v2/DV90",
|
||||
"email": "admin@example.com",
|
||||
"eab_kid": "your-zerossl-eab-kid",
|
||||
"eab_hmac": "your-zerossl-eab-hmac-base64url"
|
||||
}
|
||||
```
|
||||
|
||||
ZeroSSL, Google Trust Services, and SSL.com require External Account Binding (EAB) for ACME account registration. For most CAs, get your EAB credentials from the CA's dashboard and provide them via `eab_kid` and `eab_hmac`. The HMAC key must be base64url-encoded (no padding). CAs that don't require EAB (Let's Encrypt, Buypass) ignore these fields.
|
||||
|
||||
**ZeroSSL auto-EAB:** When the directory URL points to ZeroSSL and no EAB credentials are provided, certctl automatically fetches them from ZeroSSL's public API (`api.zerossl.com/acme/eab-credentials-email`) using your configured email address. No dashboard visit required — just set the directory URL and email, and it works. This is the same approach used by Caddy and acme.sh.
|
||||
|
||||
Minimal ZeroSSL configuration (auto-EAB):
|
||||
```json
|
||||
{
|
||||
"directory_url": "https://acme.zerossl.com/v2/DV90",
|
||||
"email": "admin@example.com"
|
||||
}
|
||||
```
|
||||
|
||||
DNS hook scripts receive these environment variables: `CERTCTL_DNS_DOMAIN` (domain being validated), `CERTCTL_DNS_FQDN` (full record name — `_acme-challenge.<domain>` for dns-01, `_validation-persist.<domain>` for dns-persist-01), `CERTCTL_DNS_VALUE` (TXT record value), `CERTCTL_DNS_TOKEN` (ACME challenge token). The present script must create the TXT record and exit 0; the cleanup script removes it (dns-01 only).
|
||||
|
||||
Environment variables for the default ACME connector:
|
||||
- `CERTCTL_ACME_DIRECTORY_URL` — ACME directory URL
|
||||
- `CERTCTL_ACME_EMAIL` — Contact email for account registration
|
||||
- `CERTCTL_ACME_EAB_KID` — External Account Binding Key ID (required by ZeroSSL, Google Trust Services, SSL.com)
|
||||
- `CERTCTL_ACME_EAB_HMAC` — External Account Binding HMAC key (base64url-encoded)
|
||||
- `CERTCTL_ACME_CHALLENGE_TYPE` — `http-01` (default), `dns-01`, or `dns-persist-01`
|
||||
- `CERTCTL_ACME_DNS_PRESENT_SCRIPT` — Path to DNS record creation script (dns-01 and dns-persist-01)
|
||||
- `CERTCTL_ACME_DNS_CLEANUP_SCRIPT` — Path to DNS record cleanup script (dns-01 only, not used by dns-persist-01)
|
||||
|
||||
@@ -5,6 +5,41 @@ This demo goes beyond browsing pre-loaded data. You'll create a team, register a
|
||||
**Time**: 15-20 minutes
|
||||
**Prerequisites**: certctl running via Docker Compose (see [Quick Start](quickstart.md))
|
||||
|
||||
## Contents
|
||||
|
||||
1. [Setup](#setup)
|
||||
2. [How the pieces fit together](#how-the-pieces-fit-together)
|
||||
3. [Alternative Issuers Reference](#alternative-issuers-reference)
|
||||
- [Sub-CA Mode](#sub-ca-mode-local-ca-chained-to-enterprise-root)
|
||||
- [ACME with ZeroSSL](#acme-with-zerossl-auto-eab)
|
||||
- [ACME with DNS-01 Challenges](#acme-with-dns-01-challenges-wildcard-certificates)
|
||||
- [ACME with DNS-PERSIST-01](#acme-with-dns-persist-01-zero-touch-renewals)
|
||||
- [step-ca (Smallstep Private CA)](#step-ca-smallstep-private-ca)
|
||||
- [OpenSSL / Custom CA](#openssl--custom-ca-script-based)
|
||||
4. [Part 1: Build the Organization Structure](#part-1-build-the-organization-structure)
|
||||
5. [Part 2: Verify the Issuer](#part-2-verify-the-issuer)
|
||||
6. [Part 3: Create a Managed Certificate](#part-3-create-a-managed-certificate)
|
||||
7. [Part 4: Trigger Certificate Renewal](#part-4-trigger-certificate-renewal)
|
||||
8. [Part 4.5: Manage Deployment Targets](#part-45-manage-deployment-targets)
|
||||
9. [Part 5: Deploy the Certificate](#part-5-deploy-the-certificate)
|
||||
10. [Part 6: View the Audit Trail](#part-6-view-the-audit-trail-immutable-api-audit-log)
|
||||
11. [Part 7: Check Notifications](#part-7-check-notifications)
|
||||
12. [Part 8: Create a Second Certificate and Compare](#part-8-create-a-second-certificate-and-compare)
|
||||
13. [Part 8.5: Revoke a Certificate](#part-85-revoke-a-certificate)
|
||||
14. [Part 9: Policy Violations](#part-9-policy-violations)
|
||||
15. [Part 9.5: Dashboard Stats and Metrics](#part-95-dashboard-stats-and-metrics)
|
||||
16. [Part 10: Certificate Profiles](#part-10-certificate-profiles)
|
||||
17. [Part 11: Agent Groups](#part-11-agent-groups)
|
||||
18. [Part 12: Interactive Approval Workflow](#part-12-interactive-approval-workflow)
|
||||
19. [Part 13: Advanced Query Features](#part-13-advanced-query-features)
|
||||
20. [Part 14: CLI Tool](#part-14-cli-tool-m16b)
|
||||
21. [Part 15: MCP Server for AI Integration](#part-15-mcp-server-for-ai-integration-m18a)
|
||||
22. [Part 16: Certificate Discovery](#part-16-certificate-discovery-m18b--m21)
|
||||
23. [End-to-End Architecture Summary](#end-to-end-architecture-summary)
|
||||
24. [Full Automated Script](#full-automated-script)
|
||||
25. [What to Show Stakeholders](#what-to-show-stakeholders)
|
||||
26. [Teardown](#teardown)
|
||||
|
||||
## Setup
|
||||
|
||||
Make sure certctl is running:
|
||||
@@ -62,6 +97,27 @@ docker compose -f deploy/docker-compose.yml restart server
|
||||
|
||||
The CA key can be RSA, ECDSA, or PKCS#8 format. The connector validates that the certificate has `IsCA=true` and `KeyUsageCertSign`.
|
||||
|
||||
### ACME with ZeroSSL (Auto-EAB)
|
||||
|
||||
ZeroSSL is a free ACME CA that requires External Account Binding (EAB) for account registration. certctl auto-fetches EAB credentials from ZeroSSL's public API when the directory URL is detected as ZeroSSL and no EAB credentials are provided — you just need an email address:
|
||||
|
||||
```bash
|
||||
# Minimal config — certctl auto-fetches EAB credentials from ZeroSSL
|
||||
export CERTCTL_ACME_DIRECTORY_URL="https://acme.zerossl.com/v2/DV90"
|
||||
export CERTCTL_ACME_EMAIL="ops@example.com"
|
||||
```
|
||||
|
||||
No dashboard visit, no manual EAB credential copy-paste. certctl calls `api.zerossl.com/acme/eab-credentials-email` with your email, gets back a KID + HMAC key, and uses them for ACME account registration automatically.
|
||||
|
||||
If you already have EAB credentials (e.g., from the ZeroSSL dashboard or for other CAs like Google Trust Services or SSL.com), you can provide them explicitly:
|
||||
|
||||
```bash
|
||||
export CERTCTL_ACME_DIRECTORY_URL="https://acme.zerossl.com/v2/DV90"
|
||||
export CERTCTL_ACME_EMAIL="ops@example.com"
|
||||
export CERTCTL_ACME_EAB_KID="your-key-id"
|
||||
export CERTCTL_ACME_EAB_HMAC="your-base64url-hmac-key"
|
||||
```
|
||||
|
||||
### ACME with DNS-01 Challenges (Wildcard Certificates)
|
||||
|
||||
For Let's Encrypt or other ACME providers with wildcard support:
|
||||
|
||||
@@ -6,6 +6,30 @@ This guide gets you running in 5 minutes and walks you through everything certct
|
||||
|
||||
New to certificates? Read the [Concepts Guide](concepts.md) first — it explains TLS, CAs, and private keys in plain language.
|
||||
|
||||
## Contents
|
||||
|
||||
1. [Prerequisites](#prerequisites)
|
||||
2. [Start Everything](#start-everything)
|
||||
3. [Open the Dashboard](#open-the-dashboard)
|
||||
4. [Explore the API](#explore-the-api)
|
||||
- [Core operations](#core-operations)
|
||||
- [Sorting, filtering, and pagination](#sorting-filtering-and-pagination)
|
||||
- [Stats and metrics](#stats-and-metrics)
|
||||
5. [Create Your First Certificate](#create-your-first-certificate)
|
||||
- [Revoke a certificate](#revoke-a-certificate)
|
||||
- [Interactive approval workflow](#interactive-approval-workflow)
|
||||
6. [Certificate Discovery](#certificate-discovery)
|
||||
- [Filesystem discovery (agent-based)](#filesystem-discovery-agent-based)
|
||||
- [Network discovery (agentless)](#network-discovery-agentless)
|
||||
- [Triage discovered certificates](#triage-discovered-certificates)
|
||||
7. [CLI Tool](#cli-tool)
|
||||
8. [MCP Server (AI Integration)](#mcp-server-ai-integration)
|
||||
9. [Demo Data Reference](#demo-data-reference)
|
||||
10. [Dashboard Demo Mode](#dashboard-demo-mode)
|
||||
11. [Presenting to Stakeholders](#presenting-to-stakeholders)
|
||||
12. [Tear Down](#tear-down)
|
||||
13. [What's Next](#whats-next)
|
||||
|
||||
## Prerequisites
|
||||
|
||||
You need **Docker** and **Docker Compose** installed. That's it.
|
||||
|
||||
@@ -2,6 +2,37 @@
|
||||
|
||||
Comprehensive manual testing playbook. Every test has a concrete command, an explanation of what it validates and why it matters, exact expected output, and an unambiguous pass/fail criterion.
|
||||
|
||||
## Contents
|
||||
|
||||
- [Prerequisites](#prerequisites)
|
||||
- [Part 1: Infrastructure & Deployment](#part-1-infrastructure--deployment)
|
||||
- [Part 2: Authentication & Security](#part-2-authentication--security)
|
||||
- [Part 3: Certificate Lifecycle (CRUD)](#part-3-certificate-lifecycle-crud)
|
||||
- [Part 4: Renewal Workflow](#part-4-renewal-workflow)
|
||||
- [Part 5: Revocation](#part-5-revocation)
|
||||
- [Part 6: Issuer Connectors](#part-6-issuer-connectors)
|
||||
- [Part 7: Target Connectors & Deployment](#part-7-target-connectors--deployment)
|
||||
- [Part 8: Agent Operations](#part-8-agent-operations)
|
||||
- [Part 9: Job System](#part-9-job-system)
|
||||
- [Part 10: Policies & Profiles](#part-10-policies--profiles)
|
||||
- [Part 11: Ownership, Teams & Agent Groups](#part-11-ownership-teams--agent-groups)
|
||||
- [Part 12: Notifications](#part-12-notifications)
|
||||
- [Part 13: Observability](#part-13-observability)
|
||||
- [Part 14: Audit Trail](#part-14-audit-trail)
|
||||
- [Part 15: Certificate Discovery (Filesystem + Network)](#part-15-certificate-discovery-filesystem--network)
|
||||
- [Part 16: Enhanced Query API](#part-16-enhanced-query-api)
|
||||
- [Part 17: CLI Tool](#part-17-cli-tool)
|
||||
- [Part 18: MCP Server](#part-18-mcp-server)
|
||||
- [Part 19: GUI Testing](#part-19-gui-testing)
|
||||
- [Part 20: Background Scheduler](#part-20-background-scheduler)
|
||||
- [Part 21: Error Handling](#part-21-error-handling)
|
||||
- [Part 22: Performance Spot Checks](#part-22-performance-spot-checks)
|
||||
- [Part 23: Structured Logging Verification](#part-23-structured-logging-verification)
|
||||
- [Part 24: Documentation Verification](#part-24-documentation-verification)
|
||||
- [Part 25: Regression Tests](#part-25-regression-tests)
|
||||
- [Part 26: EST Server (RFC 7030)](#part-26-est-server-rfc-7030)
|
||||
- [Release Sign-Off](#release-sign-off)
|
||||
|
||||
---
|
||||
|
||||
## Prerequisites
|
||||
@@ -1459,6 +1490,26 @@ curl -s -H "$AUTH" "$SERVER/api/v1/issuers/iss-acme-le" | jq '{id, type}'
|
||||
|
||||
---
|
||||
|
||||
**Test 6.2.3 — Configure ACME with External Account Binding (ZeroSSL)**
|
||||
|
||||
Edit `deploy/docker-compose.yml` to set EAB environment variables:
|
||||
- `CERTCTL_ACME_DIRECTORY_URL: https://acme.zerossl.com/v2/DV90`
|
||||
- `CERTCTL_ACME_EAB_KID: your-zerossl-kid`
|
||||
- `CERTCTL_ACME_EAB_HMAC: your-base64url-hmac-key`
|
||||
|
||||
Restart and verify the issuer accepts the config:
|
||||
|
||||
```bash
|
||||
curl -s -H "$AUTH" "$SERVER/api/v1/issuers/iss-acme-prod" | jq '{id, type}'
|
||||
```
|
||||
|
||||
**What:** Verifies that ACME issuers read External Account Binding credentials from environment variables.
|
||||
**Why:** ZeroSSL, Google Trust Services, and SSL.com require EAB for ACME account registration. Without EAB, account creation fails and no certificates can be issued from these CAs.
|
||||
**Expected:** HTTP 200. ACME issuer functional with EAB credentials loaded.
|
||||
**PASS if** HTTP 200 and issuer responds. **FAIL** if 500 or startup errors related to EAB.
|
||||
|
||||
---
|
||||
|
||||
## Part 7: Target Connectors & Deployment
|
||||
|
||||
**What this validates:** CRUD for deployment targets, including type-specific configuration for all 5 target types.
|
||||
|
||||
@@ -6,12 +6,16 @@ import (
|
||||
"crypto/elliptic"
|
||||
"crypto/rand"
|
||||
"crypto/x509"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"encoding/pem"
|
||||
"fmt"
|
||||
"io"
|
||||
"log/slog"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
@@ -201,6 +205,33 @@ func (c *Connector) ensureClient(ctx context.Context) error {
|
||||
acct := &acme.Account{
|
||||
Contact: []string{"mailto:" + c.config.Email},
|
||||
}
|
||||
|
||||
// Auto-fetch EAB credentials from ZeroSSL if directory URL is ZeroSSL and no EAB provided.
|
||||
// ZeroSSL offers a public endpoint that returns EAB credentials given an email address,
|
||||
// so users don't need to visit the ZeroSSL dashboard manually.
|
||||
if c.config.EABKid == "" && c.config.EABHmac == "" && isZeroSSL(c.config.DirectoryURL) {
|
||||
kid, hmac, eabErr := fetchZeroSSLEAB(ctx, c.config.Email)
|
||||
if eabErr != nil {
|
||||
return fmt.Errorf("failed to auto-fetch ZeroSSL EAB credentials: %w", eabErr)
|
||||
}
|
||||
c.config.EABKid = kid
|
||||
c.config.EABHmac = hmac
|
||||
c.logger.Info("auto-fetched EAB credentials from ZeroSSL", "eab_kid", kid)
|
||||
}
|
||||
|
||||
// External Account Binding (required by ZeroSSL, Google Trust Services, SSL.com, etc.)
|
||||
if c.config.EABKid != "" && c.config.EABHmac != "" {
|
||||
hmacKey, decodeErr := base64.RawURLEncoding.DecodeString(c.config.EABHmac)
|
||||
if decodeErr != nil {
|
||||
return fmt.Errorf("failed to decode EAB HMAC key (expected base64url): %w", decodeErr)
|
||||
}
|
||||
acct.ExternalAccountBinding = &acme.ExternalAccountBinding{
|
||||
KID: c.config.EABKid,
|
||||
Key: hmacKey,
|
||||
}
|
||||
c.logger.Info("using External Account Binding for ACME registration", "eab_kid", c.config.EABKid)
|
||||
}
|
||||
|
||||
_, err = c.client.Register(ctx, acct, acme.AcceptTOS)
|
||||
if err != nil {
|
||||
// Account may already exist, try to get it
|
||||
@@ -216,6 +247,67 @@ func (c *Connector) ensureClient(ctx context.Context) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// zeroSSLEABEndpoint is the ZeroSSL API endpoint for auto-generating EAB credentials.
|
||||
// Variable (not const) to allow test overrides.
|
||||
var zeroSSLEABEndpoint = "https://api.zerossl.com/acme/eab-credentials-email"
|
||||
|
||||
// isZeroSSL returns true if the ACME directory URL points to ZeroSSL.
|
||||
func isZeroSSL(directoryURL string) bool {
|
||||
return strings.Contains(strings.ToLower(directoryURL), "zerossl.com")
|
||||
}
|
||||
|
||||
// fetchZeroSSLEAB retrieves EAB credentials from ZeroSSL's public API endpoint.
|
||||
// ZeroSSL provides this so users don't need to visit the dashboard manually.
|
||||
// Returns (kid, hmac_key, error). The HMAC key is already base64url-encoded.
|
||||
func fetchZeroSSLEAB(ctx context.Context, email string) (string, string, error) {
|
||||
if email == "" {
|
||||
return "", "", fmt.Errorf("email is required for ZeroSSL EAB auto-fetch")
|
||||
}
|
||||
|
||||
form := url.Values{"email": {email}}
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodPost, zeroSSLEABEndpoint, strings.NewReader(form.Encode()))
|
||||
if err != nil {
|
||||
return "", "", fmt.Errorf("create request: %w", err)
|
||||
}
|
||||
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
||||
|
||||
client := &http.Client{Timeout: 15 * time.Second}
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return "", "", fmt.Errorf("request failed: %w", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return "", "", fmt.Errorf("read response: %w", err)
|
||||
}
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return "", "", fmt.Errorf("ZeroSSL API returned status %d: %s", resp.StatusCode, string(body))
|
||||
}
|
||||
|
||||
var result struct {
|
||||
Success bool `json:"success"`
|
||||
EABKid string `json:"eab_kid"`
|
||||
EABHmac string `json:"eab_hmac_key"`
|
||||
ErrorMsg string `json:"error"`
|
||||
}
|
||||
if err := json.Unmarshal(body, &result); err != nil {
|
||||
return "", "", fmt.Errorf("parse response: %w", err)
|
||||
}
|
||||
|
||||
if !result.Success || result.EABKid == "" || result.EABHmac == "" {
|
||||
errDetail := result.ErrorMsg
|
||||
if errDetail == "" {
|
||||
errDetail = string(body)
|
||||
}
|
||||
return "", "", fmt.Errorf("ZeroSSL EAB generation failed: %s", errDetail)
|
||||
}
|
||||
|
||||
return result.EABKid, result.EABHmac, nil
|
||||
}
|
||||
|
||||
// IssueCertificate submits a certificate issuance request to the ACME CA.
|
||||
//
|
||||
// Flow:
|
||||
|
||||
@@ -0,0 +1,264 @@
|
||||
package acme
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func testLogger() *slog.Logger {
|
||||
return slog.New(slog.NewTextHandler(os.Stderr, &slog.HandlerOptions{Level: slog.LevelError}))
|
||||
}
|
||||
|
||||
func TestValidateConfig_MissingDirectoryURL(t *testing.T) {
|
||||
c := New(nil, testLogger())
|
||||
cfg, _ := json.Marshal(map[string]string{"email": "test@example.com"})
|
||||
err := c.ValidateConfig(context.Background(), cfg)
|
||||
if err == nil || !strings.Contains(err.Error(), "directory_url is required") {
|
||||
t.Fatalf("expected directory_url error, got: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateConfig_MissingEmail(t *testing.T) {
|
||||
c := New(nil, testLogger())
|
||||
cfg, _ := json.Marshal(map[string]string{"directory_url": "https://example.com/directory"})
|
||||
err := c.ValidateConfig(context.Background(), cfg)
|
||||
if err == nil || !strings.Contains(err.Error(), "email is required") {
|
||||
t.Fatalf("expected email error, got: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateConfig_InvalidChallengeType(t *testing.T) {
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
fmt.Fprint(w, `{"newNonce":"","newAccount":"","newOrder":""}`)
|
||||
}))
|
||||
defer srv.Close()
|
||||
|
||||
c := New(nil, testLogger())
|
||||
cfg, _ := json.Marshal(map[string]string{
|
||||
"directory_url": srv.URL,
|
||||
"email": "test@example.com",
|
||||
"challenge_type": "invalid-challenge",
|
||||
})
|
||||
err := c.ValidateConfig(context.Background(), cfg)
|
||||
if err == nil || !strings.Contains(err.Error(), "invalid challenge_type") {
|
||||
t.Fatalf("expected invalid challenge_type error, got: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateConfig_Success(t *testing.T) {
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
fmt.Fprint(w, `{"newNonce":"","newAccount":"","newOrder":""}`)
|
||||
}))
|
||||
defer srv.Close()
|
||||
|
||||
c := New(nil, testLogger())
|
||||
cfg, _ := json.Marshal(map[string]string{
|
||||
"directory_url": srv.URL,
|
||||
"email": "test@example.com",
|
||||
})
|
||||
err := c.ValidateConfig(context.Background(), cfg)
|
||||
if err != nil {
|
||||
t.Fatalf("expected success, got: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateConfig_EABFieldsPreserved(t *testing.T) {
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
fmt.Fprint(w, `{"newNonce":"","newAccount":"","newOrder":""}`)
|
||||
}))
|
||||
defer srv.Close()
|
||||
|
||||
c := New(nil, testLogger())
|
||||
cfg, _ := json.Marshal(map[string]string{
|
||||
"directory_url": srv.URL,
|
||||
"email": "test@example.com",
|
||||
"eab_kid": "kid-12345",
|
||||
"eab_hmac": base64.RawURLEncoding.EncodeToString([]byte("test-hmac-key")),
|
||||
})
|
||||
err := c.ValidateConfig(context.Background(), cfg)
|
||||
if err != nil {
|
||||
t.Fatalf("expected success, got: %v", err)
|
||||
}
|
||||
if c.config.EABKid != "kid-12345" {
|
||||
t.Fatalf("expected EABKid to be preserved, got: %s", c.config.EABKid)
|
||||
}
|
||||
if c.config.EABHmac == "" {
|
||||
t.Fatal("expected EABHmac to be preserved")
|
||||
}
|
||||
}
|
||||
|
||||
func TestEnsureClient_EABDecodeError(t *testing.T) {
|
||||
c := New(&Config{
|
||||
DirectoryURL: "https://acme.example.com/directory",
|
||||
Email: "test@example.com",
|
||||
EABKid: "kid-12345",
|
||||
EABHmac: "!!!not-valid-base64url!!!",
|
||||
}, testLogger())
|
||||
|
||||
err := c.ensureClient(context.Background())
|
||||
if err == nil || !strings.Contains(err.Error(), "decode EAB HMAC") {
|
||||
t.Fatalf("expected EAB decode error, got: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestEnsureClient_EABBindingSet(t *testing.T) {
|
||||
// We can't fully mock the ACME protocol (JWS nonce exchange), but we can
|
||||
// verify that valid EAB credentials are decoded and attached to the account
|
||||
// without panicking. The ensureClient call will fail at the network level
|
||||
// (no real ACME server), but it must NOT fail at EAB decoding.
|
||||
hmacKey := base64.RawURLEncoding.EncodeToString([]byte("test-hmac-secret-key"))
|
||||
c := New(&Config{
|
||||
DirectoryURL: "https://127.0.0.1:1/directory", // unreachable — that's fine
|
||||
Email: "test@example.com",
|
||||
EABKid: "kid-zerossl-12345",
|
||||
EABHmac: hmacKey,
|
||||
}, testLogger())
|
||||
|
||||
err := c.ensureClient(context.Background())
|
||||
// Expected: network error (unreachable server), NOT an EAB decode error
|
||||
if err != nil && strings.Contains(err.Error(), "decode EAB HMAC") {
|
||||
t.Fatalf("EAB decode should not fail with valid base64url key, got: %v", err)
|
||||
}
|
||||
// We expect some error (network unreachable) — that's correct
|
||||
if err == nil {
|
||||
t.Log("ensureClient succeeded (unexpected but not a failure for this test)")
|
||||
}
|
||||
}
|
||||
|
||||
// --- ZeroSSL auto-EAB tests ---
|
||||
|
||||
func TestIsZeroSSL(t *testing.T) {
|
||||
tests := []struct {
|
||||
url string
|
||||
expect bool
|
||||
}{
|
||||
{"https://acme.zerossl.com/v2/DV90", true},
|
||||
{"https://ACME.ZEROSSL.COM/v2/DV90", true},
|
||||
{"https://acme-v02.api.letsencrypt.org/directory", false},
|
||||
{"https://acme.example.com/directory", false},
|
||||
{"", false},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
if got := isZeroSSL(tt.url); got != tt.expect {
|
||||
t.Errorf("isZeroSSL(%q) = %v, want %v", tt.url, got, tt.expect)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestFetchZeroSSLEAB_Success(t *testing.T) {
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodPost {
|
||||
t.Errorf("expected POST, got %s", r.Method)
|
||||
}
|
||||
if ct := r.Header.Get("Content-Type"); ct != "application/x-www-form-urlencoded" {
|
||||
t.Errorf("expected form content-type, got %s", ct)
|
||||
}
|
||||
if err := r.ParseForm(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if email := r.FormValue("email"); email != "test@example.com" {
|
||||
t.Errorf("expected email test@example.com, got %s", email)
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
fmt.Fprint(w, `{"success":true,"eab_kid":"kid_abc123","eab_hmac_key":"dGVzdC1obWFjLWtleQ"}`)
|
||||
}))
|
||||
defer srv.Close()
|
||||
|
||||
// Override the endpoint for testing
|
||||
origEndpoint := zeroSSLEABEndpoint
|
||||
defer func() { zeroSSLEABEndpoint = origEndpoint }()
|
||||
zeroSSLEABEndpoint = srv.URL
|
||||
|
||||
kid, hmac, err := fetchZeroSSLEAB(context.Background(), "test@example.com")
|
||||
if err != nil {
|
||||
t.Fatalf("expected success, got: %v", err)
|
||||
}
|
||||
if kid != "kid_abc123" {
|
||||
t.Errorf("expected kid_abc123, got %s", kid)
|
||||
}
|
||||
if hmac != "dGVzdC1obWFjLWtleQ" {
|
||||
t.Errorf("expected dGVzdC1obWFjLWtleQ, got %s", hmac)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFetchZeroSSLEAB_EmptyEmail(t *testing.T) {
|
||||
_, _, err := fetchZeroSSLEAB(context.Background(), "")
|
||||
if err == nil || !strings.Contains(err.Error(), "email is required") {
|
||||
t.Fatalf("expected email required error, got: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFetchZeroSSLEAB_APIError(t *testing.T) {
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
fmt.Fprint(w, `{"success":false,"error":"invalid email"}`)
|
||||
}))
|
||||
defer srv.Close()
|
||||
|
||||
origEndpoint := zeroSSLEABEndpoint
|
||||
defer func() { zeroSSLEABEndpoint = origEndpoint }()
|
||||
zeroSSLEABEndpoint = srv.URL
|
||||
|
||||
_, _, err := fetchZeroSSLEAB(context.Background(), "bad@example.com")
|
||||
if err == nil || !strings.Contains(err.Error(), "status 400") {
|
||||
t.Fatalf("expected API error, got: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFetchZeroSSLEAB_MissingCredentials(t *testing.T) {
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
fmt.Fprint(w, `{"success":false,"error":"rate limited"}`)
|
||||
}))
|
||||
defer srv.Close()
|
||||
|
||||
origEndpoint := zeroSSLEABEndpoint
|
||||
defer func() { zeroSSLEABEndpoint = origEndpoint }()
|
||||
zeroSSLEABEndpoint = srv.URL
|
||||
|
||||
_, _, err := fetchZeroSSLEAB(context.Background(), "test@example.com")
|
||||
if err == nil || !strings.Contains(err.Error(), "EAB generation failed") {
|
||||
t.Fatalf("expected EAB generation failed error, got: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestEnsureClient_ZeroSSLAutoEAB(t *testing.T) {
|
||||
// Mock ZeroSSL EAB endpoint
|
||||
eabSrv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
fmt.Fprint(w, `{"success":true,"eab_kid":"auto-kid-123","eab_hmac_key":"dGVzdC1obWFjLWtleQ"}`)
|
||||
}))
|
||||
defer eabSrv.Close()
|
||||
|
||||
origEndpoint := zeroSSLEABEndpoint
|
||||
defer func() { zeroSSLEABEndpoint = origEndpoint }()
|
||||
zeroSSLEABEndpoint = eabSrv.URL
|
||||
|
||||
// Use an unreachable ACME directory — we only care that auto-EAB fetch happens
|
||||
c := New(&Config{
|
||||
DirectoryURL: "https://acme.zerossl.com/v2/DV90",
|
||||
Email: "test@example.com",
|
||||
// EABKid and EABHmac intentionally empty — should auto-fetch
|
||||
}, testLogger())
|
||||
|
||||
err := c.ensureClient(context.Background())
|
||||
// Will fail at ACME protocol level (unreachable ZeroSSL directory), but
|
||||
// EAB credentials should have been auto-fetched and set on config
|
||||
if c.config.EABKid != "auto-kid-123" {
|
||||
t.Errorf("expected auto-fetched EABKid, got: %s (err: %v)", c.config.EABKid, err)
|
||||
}
|
||||
if c.config.EABHmac != "dGVzdC1obWFjLWtleQ" {
|
||||
t.Errorf("expected auto-fetched EABHmac, got: %s", c.config.EABHmac)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user