mirror of
https://github.com/shankar0123/certctl.git
synced 2026-06-07 18:31:37 +00:00
Bundle 9: Local-issuer hardening — 5 findings closed + 1 partial
Closes H-010 + L-002 + L-003 + L-012 + L-014 from
comprehensive-audit-2026-04-25; partial-closes M-028 (the local.go:682
elliptic.Marshal site only).
H-010 (CWE-1257) — local-issuer coverage 68.3% -> 86.7%
* internal/connector/issuer/local/bundle9_coverage_test.go (NEW)
Adds ~30 subtests across CSR-acceptance failure paths, parsePrivateKey
four-format coverage, resolveEKUsAndKeyUsage all-EKU + fallback,
hashPublicKey RSA + ECDSA P-256/P-384/P-521 + unsupported curve,
ecdsaToECDH byte-identical round-trip pin, loadCAFromDisk
expired/non-CA/missing/happy, validateCSRUnicode all rejection arms,
marshalPrivateKeyAndZeroize / ensureKeyDirSecure all branches,
ValidateConfig 5 arms, MaxTTLSeconds cap.
* .github/workflows/ci.yml — flips local-issuer floor 60% -> 85% hard
with explicit "add tests, do not lower the gate" comment.
L-002 (CWE-226) — agent + local-CA private-key zeroization
* internal/connector/issuer/local/keymem.go (NEW)
* cmd/agent/keymem.go (NEW)
marshalPrivateKeyAndZeroize wraps x509.MarshalECPrivateKey with
defer clear(der). Agent additionally defer clear(privKeyPEM) on the
encoded buffer. Bounds heap-resident exposure of the private scalar
to the duration of PEM-encode + os.WriteFile.
L-003 (CWE-732) — 0700 key-directory hardening
* internal/connector/issuer/local/keystore.go (NEW)
* cmd/agent/keymem.go (NEW)
ensureKeyDirSecure / ensureAgentKeyDirSecure create dir tree at 0700,
accept owner-only modes, chmod-tighten permissive leaves with
re-stat verification, refuse empty/root/dot. Wired ahead of every
os.WriteFile(keyPath, ..., 0600) site in cmd/agent/main.go.
L-012 (CWE-1007 + CWE-176) — Unicode safety in CN/SAN
* internal/validation/unicode.go (NEW)
* internal/validation/unicode_test.go (NEW, 8 test functions)
ValidateUnicodeSafe rejects RTL/LTR overrides U+202A..U+202E +
U+2066..U+2069, zero-width U+200B..U+200D + U+2060 + U+FEFF,
control chars <0x20 + 0x7F..0x9F, and per-DNS-label
Latin+non-Latin-letter mixes (Cyrillic-а-in-apple homograph).
Pure-IDN labels allowed. Errors cite codepoint + byte offset.
Wired into IssueCertificate + RenewCertificate via
validateCSRUnicode covering CSR Subject CommonName + DNSNames +
EmailAddresses + request-side additional SANs.
L-014 — CA-key-in-process threat-model documentation
* internal/connector/issuer/local/local.go file-header doc comment
Documents what the bundled defense-in-depth measures DO and DO NOT
protect against; directs operators with stricter requirements to
HSM/PKCS#11/cloud-KMS-backed signing (V3 Pro KMS-issuance roadmap
entry as the source-of-truth fix).
M-028 (CWE-477) PARTIAL — 1 of 6 SA1019 sites
* internal/connector/issuer/local/local.go::ecdsaToECDH (NEW helper)
Replaces deprecated elliptic.Marshal(k.Curve, k.X, k.Y) inside
hashPublicKey with crypto/ecdh.PublicKey.Bytes(). Dispatches on
Curve.Params().Name to avoid importing crypto/elliptic for sentinel
comparisons. Supports P-256/P-384/P-521; P-224 returns
unsupported-curve error and the caller falls back to a stable X+Y
big.Int.Bytes() hash (so SKI generation never panics).
* TestHashPublicKey_ECDSA_RoundTripPin — byte-identical regression
oracle that pins the new output to the legacy elliptic.Marshal
output across all three supported curves (with explicit
//nolint:staticcheck on the SA1019 reference). Migration cannot
silently change the SubjectKeyId of every previously-issued cert.
* 5 SA1019 sites still open (test-file middleware.NewAuth × 3 +
scep.go csr.Attributes).
Audit deliverables updated:
* cowork/comprehensive-audit-2026-04-25/audit-report.md — score
20/55 -> 25/55 closed (High 6/9 -> 7/9; Low 4/19 -> 8/19).
* cowork/comprehensive-audit-2026-04-25/findings.yaml — H-010 +
L-002 + L-003 + L-012 + L-014 status open -> closed; M-028 status
open -> partial_closed; closure notes cite the Bundle-9 mechanism.
* certctl/CHANGELOG.md — Bundle-9 section under [unreleased].
This commit is contained in:
+13
-10
@@ -655,16 +655,19 @@ jobs:
|
||||
# the global-run number visible here for trend-watching but not
|
||||
# gating because 0% is a measurement artifact, not a regression.
|
||||
echo "PKCS7 package coverage (global run, informational): ${PKCS7_COV}%"
|
||||
# Bundle-7 / H-005 / H-010: local-issuer SOFT gate. Local
|
||||
# `go test -cover ./internal/connector/issuer/local/...` scoped to
|
||||
# that package reported 68.3% at Bundle-7 close, but the global
|
||||
# run averages per-function and produces a slightly lower number
|
||||
# (~64.6%). Floor set at 60% to absorb that measurement variance
|
||||
# without false-failing CI. H-010 lifts this to 85% once the
|
||||
# missing CSR-validation + CA-cert-loading + key-rotation tests
|
||||
# land.
|
||||
if [ "$(echo "$LOCAL_ISSUER_COV < 60" | bc -l)" -eq 1 ]; then
|
||||
echo "::error::Local-issuer coverage ${LOCAL_ISSUER_COV}% is below 60% transitional floor (H-010 will raise to 85%)"
|
||||
# Bundle-9 / H-010 closure: local-issuer HARD gate at 85%. The
|
||||
# transitional 60% floor (Bundle-7) was an explicit promise in the
|
||||
# CI config that H-010 would raise it once CSR-validation + CA-
|
||||
# cert-loading + key-rotation + key-encoding pin tests landed.
|
||||
# Bundle-9 ships those tests (bundle9_coverage_test.go) and lifts
|
||||
# the package-scoped run to ~86.7%; the global run averages a few
|
||||
# points lower (per-function arithmetic), so the gate is set to 85
|
||||
# with the live `go test -cover` number being the source of truth.
|
||||
# If this gate trips, the fix is to add tests, NOT to lower the
|
||||
# floor — every percentage point under 85 is a regression on the
|
||||
# H-010 closure invariant.
|
||||
if [ "$(echo "$LOCAL_ISSUER_COV < 85" | bc -l)" -eq 1 ]; then
|
||||
echo "::error::Local-issuer coverage ${LOCAL_ISSUER_COV}% is below 85% (H-010 closure floor — add tests, do not lower the gate)"
|
||||
exit 1
|
||||
fi
|
||||
echo "Coverage thresholds passed!"
|
||||
|
||||
Reference in New Issue
Block a user