Commit Graph

3 Commits

Author SHA1 Message Date
shankar0123 43075a1b5c EST RFC 7030 hardening master bundle Phases 5-7: end-to-end serverkeygen
+ profile-driven csrattrs + admin observability with per-status
counters + reload-trust endpoint.

Phase 5 — RFC 7030 §4.4 server-driven key generation:
- internal/pkcs7/envelopeddata_builder.go is the inverse of the
  existing parser/decryptor: AES-256-CBC content cipher + RSA PKCS#1
  v1.5 keyTrans + per-call random IV. Round-trip pinned in test
  (BuildEnvelopedData → ParseEnvelopedData → Decrypt returns the
  original plaintext byte-for-byte).
- ESTService.SimpleServerKeygen runs the full §4.4 flow: parse client
  CSR → require RSA pubkey for keyTrans → resolve per-profile
  algorithm (RSA-2048 default; honors AllowedKeyAlgorithms) → in-
  memory keygen → re-build CSR with server pubkey → run existing
  issuer pipeline → marshal PKCS#8 → CMS-EnvelopedData wrap to a
  synthetic recipient cert wrapping the device's CSR-supplied pubkey
  → zeroize plaintext + PKCS#8 bytes → return CertPEM + ChainPEM
  + EncryptedKey. Typed sentinels ErrServerKeygenRequiresKey-
  Encipherment / ErrServerKeygenUnsupportedAlgorithm /
  ErrServerKeygenDisabled.
- ESTHandler.ServerKeygen + ServerKeygenMTLS emit RFC 7030 §4.4.2
  multipart/mixed with random per-response boundary; per-profile
  SetServerKeygenEnabled gate returns 404 when off (defense in depth
  even if the route was registered).
- New routes POST /.well-known/est/[<PathID>/]serverkeygen +
  /.well-known/est-mtls/<PathID>/serverkeygen; openapi.yaml +
  openapi-parity guard updated.

Phase 6 — Real csrattrs implementation:
- New CertificateProfile.RequiredCSRAttributes []string + migration
  000022_certificate_profiles_csrattrs.up.sql. The migration also
  lands the previously-unwired must_staple column (closes the 5.6
  follow-up loop where the field shipped at the domain + service
  layer but the postgres scan/insert/update never persisted it).
- domain.EKUStringToOID + AttributeStringToOID lookup tables: id-kp-*
  EKUs (RFC 5280 §4.2.1.12) + RFC 5280 DN attributes + RFC 2985
  PKCS#10 attributes + Microsoft Intune device-serial OID.
- ESTService.GetCSRAttrs replaces the v2.0.x nil/204 stub with a
  profile-derived SEQUENCE OF OID ASN.1 marshal. Unknown EKU /
  attribute strings dropped + warning-logged so a typo doesn't take
  down the entire endpoint.

Phase 7 — Admin observability + counters + reload-trust:
- internal/service/est_counters.go: estCounterTab (sync/atomic; 12
  named labels) + ESTStatsSnapshot per-profile shape +
  ESTService.Stats(now) zero-allocation accessor + ReloadTrust()
  SIGHUP-equivalent + SetESTAdminMetadata setter.
- Counter ticks wired into processEnrollment + SimpleServerKeygen at
  every success/failure leg.
- internal/api/handler/admin_est.go mirrors AdminSCEPIntune verbatim:
  Profiles + ReloadTrust handlers + AdminESTServiceImpl. Both
  endpoints admin-gated (M-008 triplet pinned + admin_est.go added
  to AdminGatedHandlers).
- New routes GET /api/v1/admin/est/profiles + POST /api/v1/admin/
  est/reload-trust; openapi.yaml documented; openapi-parity guard
  reproduced clean.
- cmd/server/main.go grows estServices map populated by the per-
  profile EST loop + handed to AdminEST. New MTLSTrust() +
  HasMTLSTrust() accessors on ESTHandler so main.go can pull the
  trust holder for the admin-metadata wire-up.
- Per-profile counter isolation regression test
  (internal/service/est_profile_counter_isolation_test.go) proves
  a future shared-counter refactor would fail at compile-time
  pointer-identity check.

Pre-commit verification (sandbox): gofmt clean, go vet clean
(excluding repository/postgres which the sandbox can't build —
disk-space testcontainers download), staticcheck clean across
cms/trustanchor/api/handler/api/router/scep/intune/ratelimit/
service/pkcs7/domain/cmd/server, go test -short -count=1 green
for every non-postgres package. G-3 docs-drift guard reproduced
locally clean (Phases 5-7 added zero new env vars; Phase 1
already documented per-profile SERVER_KEYGEN_ENABLED).

Spec preserved at cowork/est-rfc7030-hardening-prompt.md. Phases
8-13 (GUI ESTAdminPage / CLI+MCP / libest e2e / bulk revocation /
docs/est.md / release prep) remain — post-2.1.0 work.
2026-04-29 23:57:45 +00:00
shankar0123 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.
2026-04-29 13:16:09 +00:00
shankar0123 a579a84c7f feat: M11a — certificate profiles, crypto policy enforcement, short-lived cert expiry
Add certificate profiles as named enrollment templates that control allowed
key algorithms, max TTL, permitted EKUs, required SAN patterns, and optional
SPIFFE URI SANs. CSR submissions are validated against profile rules at
signing time (key type + minimum size). Short-lived certs (TTL < 1 hour)
auto-expire via a new scheduler loop — expiry acts as revocation, no
CRL/OCSP needed.

New files:
- Migration 000003: certificate_profiles table, FK columns on
  managed_certificates/renewal_policies, key metadata on certificate_versions
- domain/profile.go: CertificateProfile + KeyAlgorithmRule structs
- repository/postgres/profile.go: full CRUD with JSONB marshaling
- service/profile.go: ProfileService with validation + audit logging
- service/crypto_validation.go: CSR-against-profile validation (RSA/ECDSA/Ed25519)
- handler/profiles.go: 5 HTTP endpoints under /api/v1/profiles
- web/src/pages/ProfilesPage.tsx: profiles management page

Modified:
- renewal.go: CSR validation in CompleteAgentCSRRenewal, ExpireShortLivedCertificates
- scheduler.go: 30s short-lived expiry check loop
- certificate.go (repo): nullable profile FK, key metadata on versions
- main.go: profile repo/service/handler wiring, 8-param NewRenewalService
- router.go: 12-param RegisterHandlers with profile routes
- seed_demo.sql: 4 demo profiles (standard, mtls, short-lived, high-security)
- Frontend: types, API client, routing, sidebar nav

Tests: 40 new tests across handler (15), service (13), crypto validation (12)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-20 20:39:49 -04:00