Closes the #2 acquisition-readiness blocker from the 2026-05-01 issuer
coverage audit. New() at ejbca.go:L79-L88 previously constructed an
http.Client with only Timeout set — no Transport, no TLSClientConfig.
When AuthMode=mtls (the default), the client never presented the
configured ClientCert/ClientKey. The OAuth2 path worked; mTLS always
failed authentication. Tests passed because they injected a pre-built
*http.Client via NewWithHTTPClient, a path the production factory never
took.
This commit:
- Rewrites New() to load ClientCertPath + ClientKeyPath via
tls.LoadX509KeyPair when AuthMode=mtls, configure
*http.Transport.TLSClientConfig with MinVersion: TLS 1.2 (compatibility
floor for on-prem EJBCA installs that may predate TLS 1.3), and return
(*Connector, error). Constructs a fresh *http.Transport — does NOT
clone http.DefaultTransport, which would leak mutation across the
package boundary.
- OAuth2 mode unchanged: returns a client with no transport
customization (the Bearer header path is wired in setAuthHeaders).
- Invalid auth_mode values return (nil, error) immediately rather than
falling through to the mtls default and erroring at cert load.
- Updates the factory call site at issuerfactory/factory.go for the
new signature; the factory's outer (issuer.Connector, error) shape
was already in place.
- Adds TestNew_MTLSWiresClientCert: calls production New() (NOT
NewWithHTTPClient) with real cert/key files generated via stdlib
crypto/x509, asserts httpClient.Transport.TLSClientConfig.Certificates
is non-empty. Includes an httptest TLS server with
ClientAuth: tls.RequireAndVerifyClientCert that proves the cert is
actually presented on the wire — not just stashed in a struct field.
- Adds TestNew_MTLSCertLoadFailure: missing-cert path returns an error
wrapping fs.ErrNotExist (verified via errors.Is).
- Adds TestNew_OAuth2NoTransportTuning: OAuth2 path leaves Transport
nil, ensuring no accidental mTLS bleedthrough.
- Adds TestNew_InvalidAuthMode: explicit guard that auth_mode values
other than "mtls"/"oauth2" return (nil, error) at New() time.
- Adds export_test.go with HTTPClientForTest helper so the external
ejbca_test package can inspect the connector's internal *http.Client
for the wiring assertions. Compile-only during `go test`; production
builds don't expose it.
- Adds mustNewForValidateConfig test helper (OAuth2 placeholder
connector) for the existing ValidateConfig-only tests; pre-fix they
used New(nil, ...) which is no longer valid because nil config falls
into the mTLS default branch that requires non-nil cert paths.
- Updates ejbca_stubs_test.go (internal package) for the new
(*Connector, error) signature; switches the dummy connector to
OAuth2 mode so Config{} doesn't error at New().
Out of scope (separate follow-ups, per the prompt's explicit fence):
- OAuth2 token refresh missing
- Config.Token plaintext at runtime (needs SecretRef abstraction)
- RevokeCertificate composite OrderID parsing (the issuerDN := "" line
at ejbca.go:L313)
Verified locally:
- gofmt clean
- go vet ./... clean
- staticcheck ./... clean
- golangci-lint run --timeout 5m ./... → 0 issues
- go test -short -count=1 ./internal/connector/issuer/ejbca/ green
- go test -short -count=1 ./internal/connector/issuerfactory/ green
- go test -short -count=1 ./internal/service/ green
- go build ./... success
Audit reference: cowork/issuer-coverage-audit-2026-05-01/RESULTS.md
Top-10 fix#2.
Mechanical reformat. The new 'gofmt drift' CI step (added in
ci-pipeline-cleanup Phase 4, commit 0f205a8) 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>