Skip to content

Commit cbf4b7a

Browse files
danielewoodCopilot
andauthored
fix: default readonly trust to mozilla and fix export csr subjects (#195)
* fix: default readonly trust to mozilla and fix export csr subjects Use explicit Mozilla-first trust-store selection for scan, verify, inspect, and connect, stop exporting unmatched bundle directories, preserve CSR common names in PEM and JSON exports, and include the dependency updates from the repository pre-commit hooks. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix: address exporter review feedback Refactor exporter fallback to use an input struct, assign bundle names per certificate record so same-SKI renewals do not override configured bundles, and align changelog text with the selected trust-store behavior. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix: export exact matched cert for reused ski Carry the selected cert record through bundle export so same-SKI renewals do not substitute a newer certificate from a different bundle. This keeps exact-bundle export aligned with expiry filtering and prevents expired bundles like dnsstg from borrowing a valid zimperium leaf. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix: fail fast on invalid readonly trust stores * fix: address remaining PR review comments * fix: address remaining PR review feedback --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent 520c421 commit cbf4b7a

25 files changed

Lines changed: 1192 additions & 342 deletions

CHANGELOG.md

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -22,21 +22,23 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
2222
### Changed
2323

2424
- **Breaking:** Default `TrustStore` in `DefaultOptions()` changed from `"system"` to `"mozilla"` — pure-Go Mozilla root verification is used by default instead of macOS `SecTrustEvaluateWithError` syscalls, eliminating multi-minute hangs on large certificate stores
25-
- Parallelize trust verification in scan summary, dump-certs, and AIA resolution — mozilla checks run concurrently, system checks only run for certs mozilla didn't trust
25+
- Default `scan`, `verify`, `inspect`, and `connect` trust-store selection to Mozilla, and require an explicit `--trust-store system` when command trust reporting should come from host trust roots
26+
- Parallelize trust verification in scan summary, dump-certs, and AIA resolution for the selected trust store
2627
- Add `TrustStore` label to `VerifyChainTrustInput` and debug-log every trust verification call with subject, store, and result
2728
- Normalize all exported private key PEM output (`.key`, K8s `tls.key`, YAML `key`) to PKCS#8 (`PRIVATE KEY`) regardless of input format ([#167])
2829
- Bundle export warns when Kubernetes TLS secret contains an unencrypted private key alongside encrypted outputs ([#167])
2930
- Use browser Web Crypto API for PBKDF2 key derivation in WASM builds to avoid blocking the main thread during encrypted key export ([#167])
30-
- `verify` now checks both Mozilla and system trust stores by default and treats a certificate as trusted when any available anchor source succeeds ([`0ee41ad`])
31+
- `verify` now uses the selected built-in trust store plus any `--roots` file-backed anchors, and treats a certificate as trusted when any requested anchor source succeeds
3132
- `scan` now counts `untrusted_*` certificates as trusted by neither Mozilla nor system, and exposes per-store trust counts in JSON output ([`0ee41ad`])
3233
- Surface trust-source load warnings in `inspect`, `verify`, and `connect`, fail fast on invalid `verify` trust-store configuration, and stop reporting a synthetic `file` source when no file-backed roots were requested ([#171])
3334

34-
### Removed
35-
36-
- **Breaking:** Remove `verify --trust-store`; use the default Mozilla+system verification or `--roots` to add a file-backed trust source ([`0ee41ad`])
37-
3835
### Fixed
3936

37+
- Make `scan` summaries rely on the selected trust store while still retrying bundle exports against host trust roots after Mozilla unknown-authority failures
38+
- Stop assigning fallback bundle names to certificates that do not match any configured bundle entry, so stray export directories like `bundles/spf-console.zimperium.com/` are no longer generated
39+
- Preserve the certificate common name in generated bundle CSRs so exported requests include a subject CN alongside SANs
40+
- Include `subject.common_name` in generated CSR JSON output so `.csr.json` matches the exported CSR subject
41+
- Stop bundle export from retrying the system trust fallback for non-trust-store verification failures such as expired certificates
4042
- Make `connect` fail when the peer omits part of the trust path and validation only succeeds after local chain completion, instead of accepting the incomplete server-presented chain ([#190])
4143
- Use bundle folder name as Kubernetes secret `metadata.name` instead of the CN-derived prefix, so the secret name matches the export directory ([#178])
4244
- Validate `bundleName` in bundle config YAML against DNS-1123 rules at load time; invalid names now produce a fatal error with the file path and line number ([#178])

README.md

Lines changed: 30 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -142,47 +142,50 @@ Common passwords (`""`, `"password"`, `"changeit"`, `"keypassword"`) are always
142142
### Inspect Flags
143143

144144
<!-- certkit:flags:inspect -->
145-
| Flag | Default | Description |
146-
| ------------------------- | ------- | ----------------------------------------------- |
147-
| `--allow-private-network` | `false` | Allow AIA fetches to private/internal endpoints |
148-
| `--format` | `text` | Output format: text, json |
145+
| Flag | Default | Description |
146+
| ------------------------- | --------- | ----------------------------------------------- |
147+
| `--allow-private-network` | `false` | Allow AIA fetches to private/internal endpoints |
148+
| `--format` | `text` | Output format: text, json |
149+
| `--trust-store` | `mozilla` | Trust store: system, mozilla |
149150
<!-- /certkit:flags -->
150151

151152
JSON certificate records include `trust_anchors` and `trust_warnings`.
152153

153154
### Verify Flags
154155

155156
<!-- certkit:flags:verify -->
156-
| Flag | Default | Description |
157-
| ------------------------- | ------- | --------------------------------------------------------------------- |
158-
| `--allow-private-network` | `false` | Allow AIA/OCSP/CRL fetches to private/internal endpoints |
159-
| `--crl` | `false` | Check CRL distribution points for revocation |
160-
| `--diagnose` | `false` | Show diagnostics when chain verification fails |
161-
| `--expiry`, `-e` | | Check if cert expires within duration (e.g., 30d, 720h) |
162-
| `--format` | `text` | Output format: text, json |
163-
| `--key` | | Private key file to check against the certificate |
164-
| `--ocsp` | `false` | Check OCSP revocation status |
165-
| `--roots` | | Additional root certificates file (PEM, DER, PKCS#7, PKCS#12, or JKS) |
157+
| Flag | Default | Description |
158+
| ------------------------- | --------- | --------------------------------------------------------------------- |
159+
| `--allow-private-network` | `false` | Allow AIA/OCSP/CRL fetches to private/internal endpoints |
160+
| `--crl` | `false` | Check CRL distribution points for revocation |
161+
| `--diagnose` | `false` | Show diagnostics when chain verification fails |
162+
| `--expiry`, `-e` | | Check if cert expires within duration (e.g., 30d, 720h) |
163+
| `--format` | `text` | Output format: text, json |
164+
| `--key` | | Private key file to check against the certificate |
165+
| `--ocsp` | `false` | Check OCSP revocation status |
166+
| `--roots` | | Additional root certificates file (PEM, DER, PKCS#7, PKCS#12, or JKS) |
167+
| `--trust-store` | `mozilla` | Trust store: system, mozilla |
166168
<!-- /certkit:flags -->
167169

168-
Chain verification is always performed against both the embedded Mozilla roots and the host system trust store. Use `--roots` to add a file-backed trust source for private PKI, including pinned or legacy trust anchors loaded from PEM, DER, PKCS#7, PKCS#12, or JKS. When the input contains an embedded private key (PKCS#12, JKS), key match is checked automatically. Use `--ocsp` and/or `--crl` to check revocation status (requires network access and a valid chain).
170+
Chain verification uses the embedded Mozilla roots by default; use `--trust-store system` to switch to the host trust store. Use `--roots` to add a file-backed trust source for private PKI, including pinned or legacy trust anchors loaded from PEM, DER, PKCS#7, PKCS#12, or JKS. When the input contains an embedded private key (PKCS#12, JKS), key match is checked automatically. Use `--ocsp` and/or `--crl` to check revocation status (requires network access and a valid chain).
169171

170172
JSON output includes `trust_anchors` and `trust_warnings` for the leaf and displayed chain entries.
171173

172174
### Connect Flags
173175

174176
<!-- certkit:flags:connect -->
175-
| Flag | Default | Description |
176-
| ------------------------- | ------- | ----------------------------------------------------------------------------------- |
177-
| `--allow-private-network` | `false` | Allow AIA/OCSP/CRL fetches to private/internal endpoints |
178-
| `--ciphers` | `false` | Enumerate all supported cipher suites with security ratings |
179-
| `--crl` | `false` | Check CRL distribution points for revocation |
180-
| `--fips-140-2` | `false` | Apply conservative FIPS 140-2 heuristic checks to negotiated/offered TLS algorithms |
181-
| `--fips-140-3` | `false` | Apply conservative FIPS 140-3 heuristic checks to negotiated/offered TLS algorithms |
182-
| `--format` | `text` | Output format: text, json |
183-
| `--no-ocsp` | `false` | Disable automatic OCSP revocation check |
184-
| `--servername` | | Override SNI hostname (defaults to host) |
185-
| `--tls-version` | | Pin TLS version: 1.0, 1.1, 1.2, or 1.3 (default: auto) |
177+
| Flag | Default | Description |
178+
| ------------------------- | --------- | ----------------------------------------------------------------------------------- |
179+
| `--allow-private-network` | `false` | Allow AIA/OCSP/CRL fetches to private/internal endpoints |
180+
| `--ciphers` | `false` | Enumerate all supported cipher suites with security ratings |
181+
| `--crl` | `false` | Check CRL distribution points for revocation |
182+
| `--fips-140-2` | `false` | Apply conservative FIPS 140-2 heuristic checks to negotiated/offered TLS algorithms |
183+
| `--fips-140-3` | `false` | Apply conservative FIPS 140-3 heuristic checks to negotiated/offered TLS algorithms |
184+
| `--format` | `text` | Output format: text, json |
185+
| `--no-ocsp` | `false` | Disable automatic OCSP revocation check |
186+
| `--servername` | | Override SNI hostname (defaults to host) |
187+
| `--tls-version` | | Pin TLS version: 1.0, 1.1, 1.2, or 1.3 (default: auto) |
188+
| `--trust-store` | `mozilla` | Trust store: system, mozilla |
186189
<!-- /certkit:flags -->
187190

188191
Port defaults to 443 if not specified. OCSP revocation status is checked automatically (best-effort); use `--no-ocsp` to disable. Use `--verbose` for extended details (serial, key info, signature algorithm, key usage, EKU, extensions) plus a PEM-formatted copy of the server-sent certificate chain with `# Subject`, `# Issuer`, and validity headers.
@@ -267,6 +270,7 @@ Input format is auto-detected.
267270
| `--load-db` | | Load an existing database into memory before scanning |
268271
| `--max-file-size` | `10485760` | Skip files larger than this size in bytes (0 to disable) |
269272
| `--save-db` | | Save the in-memory database to disk after scanning |
273+
| `--trust-store` | `mozilla` | Trust store: system, mozilla |
270274
<!-- /certkit:flags -->
271275

272276
### Keygen Flags

bundle.go

Lines changed: 41 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -347,6 +347,7 @@ type CheckTrustAnchorsInput struct {
347347
Cert *x509.Certificate
348348
Intermediates *x509.CertPool
349349
FileRoots *x509.CertPool
350+
TrustStore string
350351
}
351352

352353
// CheckTrustAnchorsResult reports which trust sources validated a certificate
@@ -410,35 +411,55 @@ func verifyChainTrustChains(input VerifyChainTrustInput) ([][]*x509.Certificate,
410411
}
411412

412413
// CheckTrustAnchors reports which trust sources validate the certificate.
413-
// Results are returned in stable order: mozilla, system, file.
414+
// Results are returned in stable order: selected trust store, then file.
414415
func CheckTrustAnchors(input CheckTrustAnchorsInput) CheckTrustAnchorsResult {
415416
if input.Cert == nil {
416417
return CheckTrustAnchorsResult{Anchors: []string{}, Warnings: []string{}}
417418
}
418419

419420
result := CheckTrustAnchorsResult{
420-
Anchors: make([]string, 0, 3),
421+
Anchors: make([]string, 0, 2),
421422
Warnings: make([]string, 0, 2),
422423
}
423-
if mozillaPool, err := MozillaRootPool(); err != nil {
424-
result.Warnings = append(result.Warnings, fmt.Sprintf("mozilla trust source unavailable: %v", err))
425-
} else if VerifyChainTrust(VerifyChainTrustInput{
426-
Cert: input.Cert,
427-
Roots: mozillaPool,
428-
Intermediates: input.Intermediates,
429-
TrustStore: "mozilla",
430-
}) {
431-
result.Anchors = append(result.Anchors, "mozilla")
424+
checkRoots := func(name string, roots *x509.CertPool) {
425+
if VerifyChainTrust(VerifyChainTrustInput{
426+
Cert: input.Cert,
427+
Roots: roots,
428+
Intermediates: input.Intermediates,
429+
TrustStore: name,
430+
}) {
431+
result.Anchors = append(result.Anchors, name)
432+
}
432433
}
433-
if systemPool, err := SystemCertPoolCached(); err != nil {
434-
result.Warnings = append(result.Warnings, fmt.Sprintf("system trust source unavailable: %v", err))
435-
} else if VerifyChainTrust(VerifyChainTrustInput{
436-
Cert: input.Cert,
437-
Roots: systemPool,
438-
Intermediates: input.Intermediates,
439-
TrustStore: "system",
440-
}) {
441-
result.Anchors = append(result.Anchors, "system")
434+
435+
trustStore := input.TrustStore
436+
if trustStore == "" {
437+
trustStore = "mozilla"
438+
}
439+
switch trustStore {
440+
case "mozilla":
441+
mozillaPool, err := MozillaRootPool()
442+
if err != nil {
443+
result.Warnings = append(result.Warnings, fmt.Sprintf("mozilla trust source unavailable: %v", err))
444+
break
445+
}
446+
checkRoots("mozilla", mozillaPool)
447+
case "system":
448+
systemPool, err := SystemCertPoolCached()
449+
if err != nil {
450+
result.Warnings = append(result.Warnings, fmt.Sprintf("system trust source unavailable: %v", err))
451+
break
452+
}
453+
checkRoots("system", systemPool)
454+
case "custom":
455+
result.Warnings = append(result.Warnings, "custom trust store cannot be evaluated without an explicit roots pool")
456+
case "file":
457+
if input.FileRoots == nil {
458+
result.Warnings = append(result.Warnings, "file trust store cannot be evaluated without file roots")
459+
}
460+
default:
461+
result.Warnings = append(result.Warnings, fmt.Sprintf("unsupported trust store %q", trustStore))
462+
return result
442463
}
443464
if input.FileRoots != nil && VerifyChainTrust(VerifyChainTrustInput{
444465
Cert: input.Cert,

bundle_test.go

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -194,6 +194,50 @@ func TestCheckTrustAnchors_FileRoots(t *testing.T) {
194194
}
195195
}
196196

197+
func TestCheckTrustAnchors_CustomTrustStoreWarnsWithoutRootsPool(t *testing.T) {
198+
t.Parallel()
199+
200+
_, intermediates, leaf := buildChain(t, 3)
201+
intermediatePool := x509.NewCertPool()
202+
for _, intermediate := range intermediates {
203+
intermediatePool.AddCert(intermediate)
204+
}
205+
206+
result := CheckTrustAnchors(CheckTrustAnchorsInput{
207+
Cert: leaf,
208+
Intermediates: intermediatePool,
209+
TrustStore: "custom",
210+
})
211+
if len(result.Anchors) != 0 {
212+
t.Fatalf("CheckTrustAnchors() anchors = %v, want none", result.Anchors)
213+
}
214+
if got, want := strings.Join(result.Warnings, "; "), "custom trust store cannot be evaluated without an explicit roots pool"; got != want {
215+
t.Fatalf("CheckTrustAnchors() warnings = %q, want %q", got, want)
216+
}
217+
}
218+
219+
func TestCheckTrustAnchors_FileTrustStoreWarnsWithoutFileRoots(t *testing.T) {
220+
t.Parallel()
221+
222+
_, intermediates, leaf := buildChain(t, 3)
223+
intermediatePool := x509.NewCertPool()
224+
for _, intermediate := range intermediates {
225+
intermediatePool.AddCert(intermediate)
226+
}
227+
228+
result := CheckTrustAnchors(CheckTrustAnchorsInput{
229+
Cert: leaf,
230+
Intermediates: intermediatePool,
231+
TrustStore: "file",
232+
})
233+
if len(result.Anchors) != 0 {
234+
t.Fatalf("CheckTrustAnchors() anchors = %v, want none", result.Anchors)
235+
}
236+
if got, want := strings.Join(result.Warnings, "; "), "file trust store cannot be evaluated without file roots"; got != want {
237+
t.Fatalf("CheckTrustAnchors() warnings = %q, want %q", got, want)
238+
}
239+
}
240+
197241
func TestFormatTrustAnchors(t *testing.T) {
198242
t.Parallel()
199243

0 commit comments

Comments
 (0)