mirror of
https://github.com/shankar0123/certctl.git
synced 2026-06-07 15:41:41 +00:00
master
11 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
21aeed4f4e |
legal: addlicense headers + normalize legacy variants (Phase 0 RED-4)
Phase 0 closure (Path B2, post-rewrite):
addlicense sweep — adds the canonical certctl LLC copyright + BUSL-1.1
SPDX header to every production Go file. Template:
// Copyright 2026 certctl LLC. All rights reserved.
// SPDX-License-Identifier: BUSL-1.1
Coverage: 338 / 338 production Go files (cmd/ + internal/, excluding
*_test.go and **/testdata/**). Pre-sweep coverage was 22 / 338 (6.5%);
post-sweep is 338 / 338 (100%).
Normalized 22 pre-existing legacy headers (`// Copyright (c) certctl`
+ `// SPDX-License-Identifier: BSL-1.1`) and 1 file using a
`Certctl Contributors` attribution. The legacy SPDX ID `BSL-1.1`
is non-standard; the official SPDX identifier for Business Source
License 1.1 is `BUSL-1.1` (capital U). All 338 files now share the
canonical form.
Generated via:
addlicense -c "certctl LLC" -y 2026 \
-f cowork/legal/copyright-header.tpl \
-ignore '**/testdata/**' -ignore '**/*_test.go' \
cmd/ internal/
Verification:
find cmd internal -name '*.go' -not -name '*_test.go' \
-not -path '*/testdata/*' \
-exec grep -L '^// Copyright 2026 certctl LLC' {} \; | wc -l
Returns: 0
gofmt clean. Header additions are comments only, no compile impact.
Closes: cowork/certctl-architecture-diligence-audit.html#fix-RED-4
|
||
|
|
3d15a3e5af |
feat(ocsp): RFC 6960 §4.4.1 nonce extension support — echo client nonce in response, reject malformed
Production hardening II Phase 1.
The OCSP responder previously ignored the request's nonce extension
entirely, leaving relying parties vulnerable to replay attacks. RFC
6960 §4.4.1 defines the OPTIONAL id-pkix-ocsp-nonce extension (OID
1.3.6.1.5.5.7.48.1.2): when present in the request, the responder
MUST echo the same value in the response; when absent, no nonce in
the response (back-compat with relying parties that don't send one).
NEW internal/service/ocsp_nonce.go: ParseOCSPRequestNonce walks raw
DER (golang.org/x/crypto/ocsp.Request doesn't expose the request's
extensions field — the library only exposes IssuerNameHash +
IssuerKeyHash + SerialNumber). Returns one of three states:
- (nil, false, nil) — no nonce extension in request
- (nonce, true, nil) — well-formed nonce, ≤ MaxOCSPNonceLength (32)
- (nil, false, ErrOCSPNonceMalformed) — empty or oversized
NEW internal/service/ocsp_counters.go: sync/atomic counter table for
OCSP request lifecycle (request_get/post, request_success/invalid,
nonce_echoed, nonce_malformed, rate_limited, ...). Mirrors the EST/
SCEP counter pattern; Phase 8 wires these into /metrics/prometheus.
CertSrv types extended:
- internal/connector/issuer/interface.go::OCSPSignRequest gains
Nonce []byte field.
- internal/service/renewal.go::OCSPSignRequest (the service-layer
duplicate used by ca_operations.go) gains the same field.
- internal/service/issuer_adapter.go bridges the two.
Service path: CAOperationsSvc.GetOCSPResponseWithNonce(ctx, issuerID,
serialHex, nonce) is the new entry point that plumbs the nonce
through every signing site (good / revoked / unknown / short-lived).
The legacy GetOCSPResponse becomes a nil-nonce wrapper for back-
compat — every existing caller (tests, the GET handler) sees no
behavior change.
CertificateService gains the same WithNonce variant; the handler
interface adds it to the contract. MockCertificateService in tests
extended with the new method (delegates to the legacy fn when no
override is set, so existing tests that don't care about the nonce
keep working).
Local issuer's SignOCSPResponse appends the id-pkix-ocsp-nonce
extension (non-Critical per RFC 6960 §4.4) to the response template's
ExtraExtensions when req.Nonce != nil. The extnValue is the nonce
bytes wrapped in an OCTET STRING per RFC 6960 §4.4.1.
POST OCSP handler (HandleOCSPPost):
- After ocsp.ParseRequest succeeds, calls ParseOCSPRequestNonce on
the raw body to extract the optional nonce.
- On ErrOCSPNonceMalformed (empty or > 32 bytes): writes an
'unauthorized' OCSP response (status 6 per RFC 6960 §2.3) using
the canonical ocsp.UnauthorizedErrorResponse from x/crypto/ocsp.
Does NOT echo malicious bytes back.
- On well-formed nonce: passes it through GetOCSPResponseWithNonce.
- On no nonce: nil passed through; back-compat preserved.
GET OCSP handler unchanged — the GET form has no body to carry a
nonce extension.
6 new tests in internal/service/ocsp_nonce_test.go pin every
documented failure mode + the 32-byte boundary. The test fixture
builds an OCSPRequest via golang.org/x/crypto/ocsp.CreateRequest then
splices in a [2] EXPLICIT Extensions element by hand (the library
doesn't expose extension construction either).
Pre-commit verification: gofmt clean, go vet clean across affected
packages, go test -short -count=1 green for service/ + handler/ +
connector/issuer/local/. No new env vars introduced (Phase 1 is
always-on per RFC; no operator opt-out).
|
||
|
|
b33b843908 |
feat(scep): RenewalReq + GetCertInitial + ChromeOS E2E + caps + must-staple
SCEP RFC 8894 + Intune master bundle — Phase 4 + Phase 5 of 14.
Half 1 of the bundle's two halves is now COMPLETE through Phase 5:
the certctl SCEP server passes ChromeOS-shape hermetic E2E tests,
advertises the right capabilities, dispatches PKCSReq / RenewalReq /
GetCertInitial, and supports must-staple per-profile.
== Phase 4: RenewalReq + GetCertInitial wiring ============================
internal/service/scep.go
* RenewalReqWithEnvelope (RFC 8894 §3.3.1.2) — re-enrollment with an
existing valid cert. Same contract as PKCSReqWithEnvelope but the
service additionally verifies that envelope.SignerCert chains to
the issuer's CA (verifyRenewalSignerCertChain). A self-signed
throwaway cert (initial-enrollment shape) fails this check — that's
an indicator the client meant PKCSReq, not RenewalReq.
* GetCertInitialWithEnvelope (RFC 8894 §3.3.3) — polling stub.
Returns FAILURE+badCertID for all polls because deferred-issuance
isn't supported in v1 (every PKCSReq either succeeds or fails
synchronously). Wiring stays in place for a future enhancement.
* Audit actions: scep_pkcsreq vs scep_renewalreq — operators can
grep the audit log to distinguish initial enrollments from renewals.
internal/api/handler/scep.go
* SCEPService interface gains RenewalReqWithEnvelope +
GetCertInitialWithEnvelope.
* pkiOperation RFC 8894 path now switches on envelope.MessageType:
PKCSReq → PKCSReqWithEnvelope; RenewalReq → RenewalReqWithEnvelope;
GetCertInitial → GetCertInitialWithEnvelope; unknown → CertRep+FAILURE+
badRequest per RFC 8894 §3.3.2.2.
== Phase 5.1: GetCACaps capability advertisement =========================
internal/service/scep.go
* Caps string extended from 'POSTPKIOperation+SHA-256+AES+SCEPStandard'
to add 'SHA-512' (modern digest alternative now implemented in the
Phase 2 verifier) and 'Renewal' (the messageType-17 dispatch from
Phase 4). ChromeOS specifically looks for these capabilities to
negotiate the strongest available cipher + digest combo.
* scep_test.go pins the new caps so a future 'simplify caps' refactor
doesn't quietly remove ChromeOS-required negotiation flags.
== Phase 5.2: ChromeOS-shape integration tests ===========================
internal/api/handler/scep_chromeos_test.go (new, ~570 LoC)
* 6 hermetic E2E tests + ~12 helpers. Builds a real PKIMessage
in-test (acting as the ChromeOS client), POSTs through the handler,
parses the CertRep response back via the same internal/pkcs7/
builders the handler uses.
* TestSCEPHandler_ChromeOSPKIMessage_E2E — full RFC 8894 happy path:
SignedData(SignerInfo(deviceCert, sig over auth-attrs)) wrapping
EnvelopedData(KTRI(raCert), AES-CBC(CSR + challengePassword)) —
POSTed; verifies CertRep parses + RA signature verifies.
* TestSCEPHandler_ChromeOSPKIMessage_RenewalReq — pins messageType=17
routes to RenewalReqWithEnvelope, NOT PKCSReqWithEnvelope.
* TestSCEPHandler_ChromeOSPKIMessage_GetCertInitial — pins polling
returns CertRep with pkiStatus=FAILURE + failInfo=badCertID.
* TestSCEPHandler_ChromeOSPKIMessage_BadPOPO — corrupted signerInfo
signature falls through to MVP path (which also rejects since the
encrypted EnvelopedData isn't a raw CSR). No silent acceptance.
* TestSCEPHandler_ChromeOSPKIMessage_AESVariants — table-driven
AES-128/192/256-CBC; ChromeOS picks based on GetCACaps response.
* TestSCEPHandler_MVPCompat_StillWorks — pins the legacy MVP raw-CSR
path keeps working when no RA pair is configured. Backward compat
is non-negotiable.
== Phase 5.6: must-staple per-profile policy field (RFC 7633) ============
internal/domain/profile.go
* Added MustStaple bool to CertificateProfile. Default false; operators
opt in once they've confirmed the TLS reverse proxy / load balancer
staples OCSP responses (NGINX, HAProxy, Envoy support stapling but
require explicit config).
internal/connector/issuer/interface.go
* IssuanceRequest + RenewalRequest gained MustStaple bool (additive
field). Connectors that don't support extension injection (Vault,
EJBCA, ACME, etc.) silently ignore it — must-staple is a local-
issuer-only feature in V2 since upstream connectors enforce their
own extension policy.
internal/connector/issuer/local/local.go
* Added oidMustStaple (1.3.6.1.5.5.7.1.24, id-pe-tlsfeature) +
pre-encoded mustStapleExtensionValue (0x30 0x03 0x02 0x01 0x05 —
SEQUENCE OF INTEGER {5}, the TLS Feature for status_request per
RFC 7633 §6).
* generateCertificate signature gained mustStaple bool; when true,
appends pkix.Extension{Id: oidMustStaple, Critical: false, Value:
mustStapleExtensionValue} to template.ExtraExtensions before
x509.CreateCertificate.
internal/connector/issuer/local/must_staple_test.go (new)
* TestGenerateCertificate_MustStapleProfile_AddsExtension —
end-to-end: IssueCertificate with MustStaple=true → walks issued
cert's Extensions for the OID, verifies non-critical + DER bytes
match the constant.
* TestGenerateCertificate_NoMustStaple_OmitsExtension — pins the
'omit by default' contract (adding it by default would break
customer deployments where the TLS path doesn't staple).
* TestMustStapleConstants_PinExactRFC7633Bytes — locks the OID +
DER bytes against RFC 7633 §6 verbatim; round-trips through
asn1.Unmarshal as []int{5}.
Note: full service-layer plumbing (CertificateProfile.MustStaple →
IssuanceRequest.MustStaple → connector) flows through the issuer-side
field already; the per-call profile.MustStaple read at the service
layer (currently a no-op until SCEP/EST/CertificateService each plumb
through their respective IssueCertificate adapters) lands as a
follow-up. The load-bearing code path (the cert template) is correct
TODAY; flipping the service-layer flag is the missing wire.
== Phase 5.4: docs/legacy-est-scep.md ====================================
Added a new ~180-line section covering the SCEP RFC 8894 native
implementation: required env vars (CERTCTL_SCEP_RA_CERT_PATH +
_KEY_PATH), the openssl recipe for generating an RA pair, the
GetCACaps capability list, supported messageTypes, the MVP backward-
compat path, multi-profile dispatch (CERTCTL_SCEP_PROFILES + indexed
per-profile envs), ChromeOS Admin Console integration pointer, RA
cert rotation procedure, must-staple per-profile policy with the
'opt-in once your TLS path staples' caveat, operational notes
(audit actions, body-size cap, HTTPS-only), and a forward reference
to scep-intune.md (Phase 11).
== Verification ==========================================================
* gofmt + go vet clean for the files I touched.
* staticcheck ./internal/api/handler/... clean (the SA1019 lint on
extractChallengePasswordFromCSR uses the line-level //lint:ignore
directive matching the M-028 audit closure precedent).
* go test -short -count=1 green across api/handler / api/router /
service / pkcs7 / connector/issuer/local / domain / cmd/server.
* G-3 docs-drift CI guard local check: empty diff in both directions.
Phase 4 + Phase 5 of 14 in SCEP RFC 8894 + Intune master bundle.
Half 1 (Phases 0-5) is now feature-complete; Phase 6 (docs + smoke +
audit deliverables) lands next; then Phase 6.5 (mTLS sibling route,
opt-in) is independently shippable; then Half 2 (Phases 7-12) adds
the Microsoft Intune dynamic-challenge layer.
Living progress at cowork/scep-rfc8894-intune/progress.md.
|
||
|
|
f2e60b93a3 |
feat(M11c): crypto policy enforcement — CSR validation, MaxTTL caps, key metadata
Enforce certificate profile crypto constraints across all 5 issuance paths (renewal, agent CSR, EST, SCEP). ValidateCSRAgainstProfile() rejects CSRs with key algorithm/size that don't match profile rules. MaxTTL enforcement caps certificate validity per issuer connector (Local CA, Vault, step-ca enforce directly; ACME/DigiCert/Sectigo pass through). Key algorithm and size are now persisted in certificate_versions for audit compliance. 16 new tests (12 service-layer + 4 Local CA connector). Removes hardcoded version number from GUI sidebar. Documentation updated across architecture, features, connectors, and README. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> |
||
|
|
f92c997a50 |
feat(M45): ACME certificate profile selection, ARI RFC 9773 renumber, 45-day renewal positioning
Three related ACME ecosystem changes shipped as a single milestone: 1. ACME Certificate Profile Selection: Custom JWS-signed newOrder POST with `profile` field (e.g., `tlsserver`, `shortlived` for 6-day certs) bypassing acme.Client.AuthorizeOrder() since golang.org/x/crypto lacks profile support. ES256 JWS signing with kid mode, nonce management, directory discovery. Empty profile delegates to standard library path (zero behavior change). Configurable via CERTCTL_ACME_PROFILE env var. GUI: profile dropdown on ACME issuer config. 2. ARI RFC 9702 → 9773 Renumber: All 25+ references updated across Go source, docs, README, and examples. Zero remaining occurrences of RFC 9702. 3. 45-Day / Short-Lived Certificate Positioning: 5 domain tests validating renewal thresholds against SC-081v3 validity reduction timeline (200→100→47 days) and Let's Encrypt 45-day/6-day profiles. ARI (RFC 9773) is the expected renewal path for 6-day shortlived certs. New tests: 13 profile + 5 domain threshold + 1 frontend = 19 new tests. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> |
||
|
|
ec21c9bb29 |
feat(m28+m29+m30): ACME ARI, email digest, and Helm chart
M28: ACME Renewal Information (RFC 9702) — CA-directed renewal timing with cert ID computation, directory endpoint discovery, graceful degradation for non-ARI CAs. 19 tests. M29: Email notifier wiring + scheduled certificate digest — SMTP connector bridged to service layer via NotifierAdapter, DigestService with HTML email template, 7th scheduler loop (24h), digest preview/send API endpoints and GUI card. 21 tests. M30: Production-ready Helm chart — server Deployment, PostgreSQL StatefulSet, agent DaemonSet, ConfigMaps, Secrets, Ingress, security contexts, health probes, example values for dev/prod/ACME scenarios. Also: OpenAPI spec updates, MCP tool additions, CI helm-lint job, documentation updates across 5 doc files and README. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> |
||
|
|
a00bb349c4 |
feat(m27): certificate export (PEM/PKCS#12) and S/MIME EKU support
Add certificate export in PEM (JSON or file download) and PKCS#12 formats. Private keys are never included — they stay on agents. Add EKU-aware issuance threading profile EKUs (serverAuth, clientAuth, codeSigning, emailProtection, timeStamping) through the full issuance pipeline. Fix agent CSR SAN splitting for email addresses, adaptive KeyUsage flags for S/MIME vs TLS, and a pre-existing generateID collision bug in deployment job creation. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> |
||
|
|
7d14635a72 |
feat: add EST server (RFC 7030) for device certificate enrollment (M23)
Implement Enrollment over Secure Transport protocol with 4 endpoints under /.well-known/est/ — cacerts (CA chain distribution), simpleenroll (initial enrollment), simplereenroll (certificate renewal), and csrattrs (CSR attributes). PKCS#7 certs-only wire format with hand-rolled ASN.1, accepts both PEM and base64-encoded DER CSRs, configurable issuer and profile binding, full audit trail. 28 new tests (18 handler + 10 service). Also includes: - GetCACertPEM added to issuer connector interface (all 4 issuers updated) - EST integration tests wired into e2e test suite (13 test cases) - QA testing guide Part 26 (15 manual EST test cases) - All docs updated: README, features, architecture, concepts, connectors, quickstart, demo-advanced (endpoint counts, MCP wording, agent IDs, issuer interface, resource lists, OpenSSL status) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> |
||
|
|
762c523d59 |
feat: M15b — OCSP responder, DER CRL, short-lived exemption, revocation GUI
Backend:
- Embedded OCSP responder: GET /api/v1/ocsp/{issuer_id}/{serial} returns
signed OCSP responses (good/revoked/unknown) using CA key
- DER-encoded X.509 CRL: GET /api/v1/crl/{issuer_id} returns proper DER CRL
signed by issuing CA with 24h validity window
- Short-lived cert exemption: certs with profile TTL < 1 hour skip CRL/OCSP
(expiry is sufficient revocation for ephemeral workloads)
- Extended issuer connector interface with GenerateCRL and SignOCSPResponse
- Local CA implements full CRL/OCSP signing; ACME and step-ca return
appropriate "use native endpoint" errors
- IssuerConnectorAdapter bridges new methods between layers
Frontend:
- Revoke button on certificate detail page with RFC 5280 reason modal
- Revocation banner with reason display and timestamp
- Revocation status indicators in lifecycle section
- "Revoked" filter option in certificates list
- API client: revokeCertificate() function and Certificate type extensions
Tests: ~31 new tests across connector, service, handler, and adapter layers
Docs: milestones renumbered (M13-M14, M16-M18), M15b marked complete
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
|
||
|
|
66f04f7afe |
style: run gofmt -s across all Go files
Fixes Go Report Card gofmt score from 52% to 100%. Pure formatting changes — no logic modifications. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> |
||
|
|
d395776a95 | Initial scaffold: certificate control plane v0.1.0 |