Phase 2 of the #5 acquisition-readiness fix from the 2026-05-01 issuer
coverage audit. Phase 1 (commit 593210f) shipped the shared asyncpoll
package and refactored DigiCert as the reference. This commit applies
the same pattern to the remaining three async-CA connectors and adds
the operator-facing docs.
Per-connector refactors:
- Sectigo (sectigo.go): GetOrderStatus now wraps pollEnrollmentOnce in
asyncpoll.Poll. The collectNotReady sentinel (cert approved by SCM
but not yet retrievable from the collect endpoint) maps to
StillPending and rides the backoff schedule rather than the prior
"return pending immediately" branch. Added isPermanentStatusError
helper to distinguish transient HTTP errors (5xx / 429 / network)
from permanent ones (4xx / parse failure) — the wrapped checkStatus
errors get triaged at the poll closure boundary.
- Entrust (entrust.go): GetOrderStatus wraps pollEnrollmentOnce. The
AWAITING_APPROVAL status maps to StillPending; operators using
approval-pending workflows where humans approve enrollments should
bump CERTCTL_ENTRUST_POLL_MAX_WAIT_SECONDS to 86400 (24h) so a
single scheduler tick can wait through the approval window. The
default 10-minute deadline matches the other three connectors.
- GlobalSign (globalsign.go): GetOrderStatus wraps pollCertificateOnce.
GlobalSign tracks orders by serial number rather than order ID, but
the polling shape is identical to the other three. Status-code
triage matches DigiCert: 4xx (not 429) is permanent, 5xx / 429 /
network is transient.
Per-connector Config field added:
- DigiCert.PollMaxWaitSeconds (env CERTCTL_DIGICERT_POLL_MAX_WAIT_SECONDS)
- Sectigo.PollMaxWaitSeconds (env CERTCTL_SECTIGO_POLL_MAX_WAIT_SECONDS)
- Entrust.PollMaxWaitSeconds (env CERTCTL_ENTRUST_POLL_MAX_WAIT_SECONDS)
- GlobalSign.PollMaxWaitSeconds (env CERTCTL_GLOBALSIGN_POLL_MAX_WAIT_SECONDS)
internal/config/config.go env-var loaders updated for all four. Default
is 600 seconds (10 minutes); zero falls back to the asyncpoll package
default.
Test-helper updates: every existing test that exercises the pending
branch (collectNotReady, AWAITING_APPROVAL, status="pending", etc.)
now sets PollMaxWaitSeconds=1 in its Config so the test doesn't block
on the production-default 10-minute deadline. Tests that exercise
permanent-error branches (404, 401, malformed JSON, etc.) continue
to return immediately.
Test sites updated:
- buildSectigoConnector helper + GetOrderStatus_CollectNotReady test
- buildEntrustConnector helper + GetOrderStatus_Pending test
- buildGlobalsignConnector helper + GetOrderStatus_Pending test +
the GetHTTPClient_NoMTLSCertPaths test (network failure now rides
the backoff schedule rather than returning immediately)
Documentation:
- docs/async-polling.md: new operator reference covering the backoff
schedule, status-code triage, the four env vars, failure modes, and
where the implementation lives. Audit blocker citation included.
- docs/connectors.md: per-issuer sections for DigiCert, Sectigo,
Entrust, GlobalSign each gain the PollMaxWaitSeconds env var row
and a cross-link to async-polling.md.
Lint cleanup: simplified the isPermanentStatusError branch to satisfy
staticcheck S1008 (single-line return for a final boolean check).
Verified locally:
- gofmt -l . clean
- go vet ./... clean
- staticcheck ./... clean
- golangci-lint run --timeout 5m ./... → 0 issues
- go test -short -count=1 across all 4 connector packages + config + asyncpoll: green
Audit reference: cowork/issuer-coverage-audit-2026-05-01/RESULTS.md
Top-10 fix#5 — Phase 2.
Mechanical reformat. The new 'gofmt drift' CI step (added in
ci-pipeline-cleanup Phase 4, commit 71b2245) surfaced 111 files
with accumulated gofmt drift across cmd/, internal/, and deploy/test/.
Each file's diff is gofmt-standard: whitespace adjustments, intra-
group import sorting (alphabetical by import path within blank-line-
separated groups), and struct-tag column alignment. No semantic
changes — verified via 'git diff --ignore-all-space' which shows only
the line-position deltas from import reordering.
The gate stays in place after this commit. Going forward it catches
gofmt drift at PR time.
Closes M-001 partially; M-002, M-003, and CI threshold raise #2 deferred.
Stubs coverage shipped across 8 issuer connectors via per-connector
<conn>_stubs_test.go (~50 LoC each) pinning the not-supported
issuer.Connector interface methods (GenerateCRL, SignOCSPResponse,
GetCACertPEM, GetRenewalInfo). Most CAs delegate CRL/OCSP/CA-cert
distribution to managed services, so these are documented stubs that
return errors. Pinning them ensures the stubs aren't silently replaced
with no-ops in a future refactor.
Coverage delta:
digicert: 79.3% -> 81.0% (+1.7pp)
ejbca: 75.8% -> 76.5% (+0.7pp)
entrust: 70.8% -> 70.8% (stubs already covered)
sectigo: 78.0% -> 79.4% (+1.4pp)
vault: 81.0% -> 84.1% (+3.1pp)
openssl: 76.9% -> 78.0% (+1.1pp)
googlecas: 81.0% -> 83.4% (+2.4pp)
globalsign: 75.9% -> 78.2% (+2.3pp)
(awsacmpca not included; its 0%-coverage hotspots are stubClient methods
structurally different from the others' interface stubs. Already at 83.5%.)
Why the gates aren't yet met: the stub functions are tiny (1-2 lines
each, mostly 'return nil, fmt.Errorf("not supported")'). Lifting each
connector to >=85% requires per-connector failure-mode test files
mirroring Bundle J's ACME pattern (httptest.Server + canned 401/403/
429+Retry-After/5xx/malformed responses against the actual API methods).
That's ~200-300 LoC x 9 connectors = ~2000-2700 LoC of bespoke per-CA
mock work; exceeds this session's budget. Tracked as follow-on
Bundle N.A-extended / N.B-extended.
Deferred sub-batches:
N.C (M-002 + M-003): internal/service (70.5%) + internal/api/handler
(79.4%) round-out NOT YET STARTED. Tracked as Bundle N.C-extended.
N.CI (CI threshold raise #2): prescribed raises require underlying
coverage at proposed floors first. Premature raise would fail CI
immediately. Tracked as Bundle N.CI-extended.
Verification:
go vet ./internal/connector/issuer/{8-pkgs}/... clean
gofmt -l clean
go test -short -count=1 PASS for all 8
Audit deliverables:
gap-backlog.md: M-001 partial-strikethrough with per-connector table
+ Bundle N closure-log entry covering all 4 sub-batch statuses
closure-plan.md: Bundle N [~] with per-sub-batch status breakdown
CHANGELOG.md: [unreleased] Bundle N entry
Add three new issuer connectors completing commercial and open-source CA
coverage. Entrust uses mTLS client certificate auth with sync/async
issuance. GlobalSign Atlas uses mTLS + API key/secret dual auth with
serial-based tracking. EJBCA supports dual auth (mTLS or OAuth2) for
self-hosted Keyfactor CAs.
Each connector implements the full issuer.Connector interface (9 methods),
includes httptest-based unit tests (~14 each), and follows established
patterns (injectable HTTP clients, RFC 5280 revocation reason mapping,
CRL/OCSP delegated to CA).
Also includes: issuer factory cases, env var seeding, config structs,
domain types, seed data (3 rows, all disabled), OpenAPI enum updates,
frontend issuer catalog entries with config fields, and full docs
(connectors.md, architecture.md, features.md, README).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>