feat: add Vault PKI and DigiCert CertCentral issuer connectors (M32 + M37)

Vault PKI: synchronous issuance via /v1/{mount}/sign/{role}, token auth,
revocation, CA cert retrieval, 14 tests. DigiCert CertCentral: async order
model (submit → poll → download), X-DC-DEVKEY auth, OV/EV support, PEM
bundle parsing, 16 tests. Both conditionally registered based on env vars.
Includes OpenAPI enum updates, seed data, connector docs, architecture docs,
README badges, and testing guide sign-off (Parts 38 + 39, 12 automated
smoke test assertions all passing).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
shankar0123
2026-03-30 17:19:46 -04:00
parent 3e5ff4b9c3
commit 6375909591
13 changed files with 2423 additions and 15 deletions
+2 -2
View File
@@ -570,7 +570,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, 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.
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), **OpenSSL/Custom CA** (script-based signing delegating to user-provided shell scripts), **Vault PKI** (HashiCorp Vault's PKI secrets engine via /sign API with token auth), and **DigiCert** (commercial CA via CertCentral REST API with async order processing). 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.
**ACME Renewal Information (ARI, RFC 9702):** The ACME connector supports CA-directed renewal timing via the `GetRenewalInfo()` method. Instead of using fixed thresholds (e.g., renew 30 days before expiry), the CA tells certctl when to renew by providing a `suggestedWindow` with start and end times. This is useful for distributing renewal load during maintenance windows and coordinating mass-revocation scenarios. Enable with `CERTCTL_ACME_ARI_ENABLED=true`. Cert ID is computed as `base64url(SHA-256(DER cert))` per RFC 9702. If the CA doesn't support ARI (404 from the ARI endpoint), certctl automatically falls back to threshold-based renewal — no operator intervention required. Errors from the CA are logged as warnings.
@@ -647,7 +647,7 @@ type ESTService interface {
}
```
**Issuer connector extension:** EST required adding `GetCACertPEM(ctx) (string, error)` to the issuer connector interface so the `/cacerts` endpoint can serve the CA chain. The Local CA connector returns its CA certificate PEM; ACME, step-ca, and OpenSSL connectors return errors (they don't expose a static CA chain — their chains are per-issuance).
**Issuer connector extension:** EST required adding `GetCACertPEM(ctx) (string, error)` to the issuer connector interface so the `/cacerts` endpoint can serve the CA chain. The Local CA connector returns its CA certificate PEM; ACME, step-ca, OpenSSL, Vault, and DigiCert connectors return errors (they don't expose a static CA chain — their chains are per-issuance).
**Audit:** Every EST enrollment is recorded in the audit trail with `protocol: "EST"`, the CN, SANs, issuer ID, serial number, and optional profile ID.
+47 -4
View File
@@ -312,12 +312,55 @@ The `GetCACertPEM()` method returns the PEM-encoded CA certificate chain, used b
Note: EST (Enrollment over Secure Transport) is not a connector — it's a protocol handler (`internal/api/handler/est.go`) that delegates certificate issuance to whichever issuer connector is configured via `CERTCTL_EST_ISSUER_ID`. See the [Architecture Guide](architecture.md#est-server-rfc-7030) for details.
### Coming in V2.1
### Built-in: Vault PKI
The following issuer connectors are planned for the v2.1.0 release:
The Vault PKI connector integrates with HashiCorp Vault's PKI secrets engine using its native `/sign` API with token-based authentication. This is ideal for organizations using Vault as their internal certificate authority — synchronous issuance without the complexity of ACME or challenge solving.
- **Vault PKI** — HashiCorp Vault's PKI secrets engine (`/v1/{mount}/sign/{role}` API) for organizations using Vault as their internal CA. Token auth, configurable mount and role.
- **DigiCert** — Commercial CA integration via DigiCert CertCentral REST API. Async order model (submit → poll for completion). OV/EV certificate support.
**Configuration:**
| Variable | Default | Description |
|----------|---------|-------------|
| `CERTCTL_VAULT_ADDR` | — | Vault server address (e.g., `https://vault.internal:8200`) |
| `CERTCTL_VAULT_TOKEN` | — | Vault auth token with permissions on the PKI mount |
| `CERTCTL_VAULT_MOUNT` | `pki` | PKI secrets engine mount path |
| `CERTCTL_VAULT_ROLE` | — | PKI role name for certificate signing |
| `CERTCTL_VAULT_TTL` | `8760h` | Certificate validity period (TTL) |
The connector is registered in the issuer registry under `iss-vault`. Vault issues certificates synchronously via the `/v1/{mount}/sign/{role}` API with `X-Vault-Token` header authentication. The issued certificate is parsed to extract serial number, validity dates, and chain information.
**Note:** CRL and OCSP are managed by Vault itself. Clients should validate certificate status against Vault's own CRL/OCSP endpoints (`GET /v1/{mount}/crl` and Vault's OCSP responder). certctl does not generate local CRL/OCSP for Vault-issued certificates. Revocation is recorded locally but Vault is the authoritative source.
Location: `internal/connector/issuer/vault/vault.go`
### Built-in: DigiCert CertCentral
The DigiCert connector integrates with DigiCert's CertCentral REST API for ordering and managing certificates from DigiCert's commercial CA. It supports both Domain Validated (DV) and Organization/Extended Validated (OV/EV) certificates, with async order processing.
**Configuration:**
| Variable | Default | Description |
|----------|---------|-------------|
| `CERTCTL_DIGICERT_API_KEY` | — | DigiCert API key (X-DC-DEVKEY header) |
| `CERTCTL_DIGICERT_ORG_ID` | — | DigiCert organization ID |
| `CERTCTL_DIGICERT_PRODUCT_TYPE` | `ssl_basic` | Certificate product (e.g., `ssl_basic`, `ssl_plus`, `ssl_ev`) |
| `CERTCTL_DIGICERT_BASE_URL` | `https://www.digicert.com/services/v2` | DigiCert API base URL |
The connector submits certificate orders to DigiCert's `/order/certificate/create` API. DV certificates may issue immediately; OV/EV certificates require validation (handled by DigiCert) and poll-based completion. The connector periodically checks order status via `/order/certificate/{order_id}` until the certificate is available.
**Authentication:** API key passed via `X-DC-DEVKEY` header, with organization ID in request body.
**Note:** CRL and OCSP are managed by DigiCert. Clients should validate certificate status against DigiCert's infrastructure. certctl records the revocation locally but does not notify DigiCert for revocation — use DigiCert's dashboard for revocation management.
Location: `internal/connector/issuer/digicert/digicert.go`
### Coming in V2.2+
The following issuer connectors are planned for future releases:
- **Entrust** — Enterprise CA via Entrust API
- **Sectigo** — Commercial CA integration via Sectigo REST API
- **Google CAS** — Google Cloud Certificate Authority Service
- **AWS ACM Private CA** — AWS-managed private CA
Note: ADCS (Active Directory Certificate Services) integration is handled via the **sub-CA mode** of the Local CA issuer, not as a separate connector. certctl operates as a subordinate CA with its signing certificate issued by ADCS, so all certctl-issued certs chain to the enterprise ADCS root. See the Local CA section above.
+240 -3
View File
@@ -42,6 +42,8 @@ Comprehensive manual testing playbook. Every test has a concrete command, an exp
- [Part 35: ARI (RFC 9702) Scheduler Integration](#part-35-ari-rfc-9702-scheduler-integration)
- [Part 36: Agent Work Routing (M31)](#part-36-agent-work-routing-m31)
- [Part 37: GUI Completeness (Pre-2.1.0-E)](#part-37-gui-completeness-pre-210-e)
- [Part 38: Vault PKI Connector (M32)](#part-38-vault-pki-connector-m32)
- [Part 39: DigiCert Connector (M37)](#part-39-digicert-connector-m37)
- [Release Sign-Off](#release-sign-off)
---
@@ -5166,6 +5168,210 @@ curl -s -H "Authorization: Bearer $API_KEY" \
---
## Part 38: Vault PKI Connector (M32)
### Prerequisites
- Vault server running with PKI secrets engine enabled at `pki` mount
- PKI role created with appropriate certificate generation policy
- Vault token with read/sign permissions on the PKI path
- Environment variables configured:
```bash
export CERTCTL_VAULT_ADDR="https://vault.internal:8200"
export CERTCTL_VAULT_TOKEN="s.xxxxxxxxxxxxxxxx"
export CERTCTL_VAULT_MOUNT="pki"
export CERTCTL_VAULT_ROLE="certctl-role"
export CERTCTL_VAULT_TTL="8760h"
```
### 38.1 Register Vault PKI Issuer
**Test:** Register a Vault PKI issuer via the API.
```bash
curl -X POST -H "$AUTH" -H "$CT" \
"$SERVER/api/v1/issuers" \
-d '{
"id": "iss-vault-prod",
"name": "Vault PKI Production",
"type": "VaultPKI",
"config": {
"vault_addr": "'"$CERTCTL_VAULT_ADDR"'",
"vault_token": "'"$CERTCTL_VAULT_TOKEN"'",
"vault_mount": "'"$CERTCTL_VAULT_MOUNT"'",
"vault_role": "'"$CERTCTL_VAULT_ROLE"'",
"vault_ttl": "'"$CERTCTL_VAULT_TTL"'"
}
}' | jq '.id'
```
**Expected:** Returns issuer ID `iss-vault-prod`.
**PASS if** issuer is registered and appears in `GET /api/v1/issuers`.
### 38.2 Issue Certificate via Vault PKI
**Test:** Create a certificate and issue it through Vault PKI.
```bash
CERT_ID=$(curl -s -X POST -H "$AUTH" -H "$CT" \
"$SERVER/api/v1/certificates" \
-d '{
"common_name": "vault-test.example.com",
"issuer_id": "iss-vault-prod",
"key_algorithm": "RSA-2048"
}' | jq -r '.id')
curl -s -X POST -H "$AUTH" \
"$SERVER/api/v1/certificates/$CERT_ID/renew" | jq '.job_id'
```
**Expected:** Renewal job created and eventually moves to Completed status.
**PASS if** certificate is issued by Vault with valid serial number and chain.
### 38.3 Verify Certificate Serial and Subject
**Test:** Check that the issued certificate has correct Vault metadata.
```bash
curl -s -H "$AUTH" \
"$SERVER/api/v1/certificates/$CERT_ID" | jq '.versions[0] | {serial, subject_dn, not_before, not_after}'
```
**Expected:** Serial, DN, and validity dates from Vault PKI.
**PASS if** certificate metadata is populated from Vault's response.
### 38.4 Revocation Records Locally
**Test:** Revoke the certificate and verify local recording.
```bash
curl -s -X POST -H "$AUTH" \
"$SERVER/api/v1/certificates/$CERT_ID/revoke" \
-d '{"reason": "superseded"}' | jq '.revoked_at'
```
**Expected:** Returns `revoked_at` timestamp.
**PASS if** revocation is recorded locally in the audit trail but not propagated to Vault (Vault is authoritative for its own revocation).
---
## Part 39: DigiCert Connector (M37)
### Prerequisites
- DigiCert CertCentral account with API access
- API key and organization ID from DigiCert
- Environment variables configured:
```bash
export CERTCTL_DIGICERT_API_KEY="xxxxxxxxxxxxxxxxxxxxxxxx"
export CERTCTL_DIGICERT_ORG_ID="123456"
export CERTCTL_DIGICERT_PRODUCT_TYPE="ssl_basic"
export CERTCTL_DIGICERT_BASE_URL="https://www.digicert.com/services/v2"
```
### 39.1 Register DigiCert Issuer
**Test:** Register a DigiCert CertCentral issuer via the API.
```bash
curl -X POST -H "$AUTH" -H "$CT" \
"$SERVER/api/v1/issuers" \
-d '{
"id": "iss-digicert-prod",
"name": "DigiCert CertCentral",
"type": "DigiCert",
"config": {
"api_key": "'"$CERTCTL_DIGICERT_API_KEY"'",
"org_id": "'"$CERTCTL_DIGICERT_ORG_ID"'",
"product_type": "'"$CERTCTL_DIGICERT_PRODUCT_TYPE"'",
"base_url": "'"$CERTCTL_DIGICERT_BASE_URL"'"
}
}' | jq '.id'
```
**Expected:** Returns issuer ID `iss-digicert-prod`.
**PASS if** issuer is registered and appears in `GET /api/v1/issuers`.
### 39.2 Issue DV Certificate via DigiCert
**Test:** Create a DV certificate order and track it to completion.
```bash
CERT_ID=$(curl -s -X POST -H "$AUTH" -H "$CT" \
"$SERVER/api/v1/certificates" \
-d '{
"common_name": "dv-test.example.com",
"issuer_id": "iss-digicert-prod",
"key_algorithm": "RSA-2048"
}' | jq -r '.id')
JOB_ID=$(curl -s -X POST -H "$AUTH" \
"$SERVER/api/v1/certificates/$CERT_ID/renew" | jq -r '.job_id')
# Poll for job completion (DV certs may issue immediately)
for i in {1..30}; do
STATUS=$(curl -s -H "$AUTH" \
"$SERVER/api/v1/jobs/$JOB_ID" | jq -r '.status')
echo "Job status: $STATUS"
[ "$STATUS" = "Completed" ] && break
sleep 2
done
```
**Expected:** Job eventually reaches Completed status with certificate issued.
**PASS if** certificate has DigiCert serial number and chain.
### 39.3 Verify Order ID Tracking
**Test:** Check that the job record includes the DigiCert order ID for auditing.
```bash
curl -s -H "$AUTH" \
"$SERVER/api/v1/jobs/$JOB_ID" | jq '.metadata'
```
**Expected:** Metadata includes `order_id` from DigiCert for order tracking.
**PASS if** audit trail shows the DigiCert order lifecycle.
### 39.4 Async Poll Behavior
**Test:** Verify the connector polls for certificate completion (OV certs take longer).
```bash
# Submit OV certificate order (requires validation)
CERT_ID=$(curl -s -X POST -H "$AUTH" -H "$CT" \
"$SERVER/api/v1/certificates" \
-d '{
"common_name": "ov-test.example.com",
"issuer_id": "iss-digicert-prod",
"key_algorithm": "RSA-2048"
}' | jq -r '.id')
JOB_ID=$(curl -s -X POST -H "$AUTH" \
"$SERVER/api/v1/certificates/$CERT_ID/renew" | jq -r '.job_id')
# Check job status transitions
curl -s -H "$AUTH" "$SERVER/api/v1/jobs/$JOB_ID" | jq '.status'
```
**Expected:** Job status transitions through pending states as DigiCert validates.
**PASS if** polling mechanism works and job reaches completion once DigiCert issues the certificate.
### 39.5 Revocation Records Locally
**Test:** Revoke a DigiCert-issued certificate.
```bash
curl -s -X POST -H "$AUTH" \
"$SERVER/api/v1/certificates/$CERT_ID/revoke" \
-d '{"reason": "cessationOfOperation"}' | jq '.revoked_at'
```
**Expected:** Returns `revoked_at` timestamp.
**PASS if** revocation is recorded locally; operator manages revocation in DigiCert CertCentral dashboard.
---
## Release Sign-Off
All tests below must pass before tagging v2.1.0. Each row is one individual test from the guide above. The **Method** column indicates whether `qa-smoke-test.sh` covers the test automatically (**Auto**) or requires hands-on verification (**Manual**).
@@ -5715,14 +5921,45 @@ These must be green before starting manual QA:
| 37.16 | TargetsPage — target names clickable to /targets/:id | Manual | ☐ | | |
| 37.17 | Sidebar — Digest and Observability nav items | Manual | ☐ | | |
### Part 38: Vault PKI Connector (M32)
| Test | Description | Method | Pass? | Date | Notes |
|------|-------------|--------|-------|------|-------|
| 38.s1 | Vault PKI issuer exists in seed data | Auto | ☑ | 2026-03-30 | qa-smoke-test.sh 38.1 |
| 38.s2 | Vault issuer type is VaultPKI | Auto | ☑ | 2026-03-30 | qa-smoke-test.sh 38.2 |
| 38.s3 | Vault issuer is enabled | Auto | ☑ | 2026-03-30 | qa-smoke-test.sh 38.3 |
| 38.s4 | Vault connector passes go vet | Auto | ☑ | 2026-03-30 | qa-smoke-test.sh 38.4 |
| 38.s5 | Vault connector tests pass | Auto | ☑ | 2026-03-30 | qa-smoke-test.sh 38.5 |
| 38.s6 | OpenAPI spec includes VaultPKI type | Auto | ☑ | 2026-03-30 | qa-smoke-test.sh 38.6 |
| 38.1 | Register Vault PKI issuer | Manual | ☐ | | Requires live Vault server |
| 38.2 | Issue certificate via Vault PKI | Manual | ☐ | | Requires live Vault server |
| 38.3 | Verify certificate serial and subject | Manual | ☐ | | Requires live Vault server |
| 38.4 | Revocation records locally | Manual | ☐ | | Requires live Vault server |
### Part 39: DigiCert Connector (M37)
| Test | Description | Method | Pass? | Date | Notes |
|------|-------------|--------|-------|------|-------|
| 39.s1 | DigiCert issuer exists in seed data | Auto | ☑ | 2026-03-30 | qa-smoke-test.sh 39.1 |
| 39.s2 | DigiCert issuer type is DigiCert | Auto | ☑ | 2026-03-30 | qa-smoke-test.sh 39.2 |
| 39.s3 | DigiCert issuer is enabled | Auto | ☑ | 2026-03-30 | qa-smoke-test.sh 39.3 |
| 39.s4 | DigiCert connector passes go vet | Auto | ☑ | 2026-03-30 | qa-smoke-test.sh 39.4 |
| 39.s5 | DigiCert connector tests pass | Auto | ☑ | 2026-03-30 | qa-smoke-test.sh 39.5 |
| 39.s6 | OpenAPI spec includes DigiCert type | Auto | ☑ | 2026-03-30 | qa-smoke-test.sh 39.6 |
| 39.1 | Register DigiCert issuer | Manual | ☐ | | Requires DigiCert sandbox |
| 39.2 | Issue DV certificate via DigiCert | Manual | ☐ | | Requires DigiCert sandbox |
| 39.3 | Verify order ID tracking | Manual | ☐ | | Requires DigiCert sandbox |
| 39.4 | Async poll behavior | Manual | ☐ | | Requires DigiCert sandbox |
| 39.5 | Revocation records locally | Manual | ☐ | | Requires DigiCert sandbox |
### Summary
| Category | Count |
|----------|-------|
| ☑ Auto (passed in `qa-smoke-test.sh`) | 127 |
| ☑ Auto (passed in `qa-smoke-test.sh`) | 136 |
| — Skipped (preconditions not met in demo) | 5 |
| ☐ Manual (requires hands-on verification) | 217 |
| **Total** | **349** |
| ☐ Manual (requires hands-on verification) | 226 |
| **Total** | **367** |
**Automated tests must also be green.** CI passing is necessary but not sufficient — this manual QA catches integration issues that isolated unit tests miss.