mirror of
https://github.com/shankar0123/certctl.git
synced 2026-06-07 12:31:29 +00:00
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.
This commit is contained in:
@@ -981,6 +981,104 @@ paths:
|
||||
"500":
|
||||
description: Trust anchor reload failed (the OLD pool is retained)
|
||||
|
||||
/api/v1/admin/est/profiles:
|
||||
get:
|
||||
tags: [EST]
|
||||
summary: Per-profile EST administration overview (admin)
|
||||
description: |
|
||||
Returns one snapshot per configured EST profile with always-present
|
||||
per-profile fields (path_id, issuer_id, profile_id, mtls_enabled,
|
||||
basic_auth_configured, server_keygen_enabled, counters) plus an
|
||||
optional trust-anchor sub-block when the profile has MTLS_ENABLED=true.
|
||||
|
||||
Counter labels: success_simpleenroll, success_simplereenroll,
|
||||
success_serverkeygen, auth_failed_basic, auth_failed_mtls,
|
||||
auth_failed_channel_binding, csr_invalid, csr_policy_violation,
|
||||
csr_signature_mismatch, rate_limited, issuer_error, internal_error.
|
||||
|
||||
Admin-gated (M-008 pattern). Non-admin Bearer callers get HTTP 403 —
|
||||
the snapshot reveals operator profile set, mTLS trust-anchor expiries,
|
||||
and auth-mode posture (sensitive operational metadata). EST RFC 7030
|
||||
hardening master bundle Phase 7.2.
|
||||
operationId: listESTProfiles
|
||||
responses:
|
||||
"200":
|
||||
description: Per-profile EST administration snapshot
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
profiles:
|
||||
type: array
|
||||
items:
|
||||
type: object
|
||||
profile_count:
|
||||
type: integer
|
||||
generated_at:
|
||||
type: string
|
||||
format: date-time
|
||||
"403":
|
||||
description: Admin access required
|
||||
"500":
|
||||
$ref: "#/components/responses/InternalError"
|
||||
|
||||
/api/v1/admin/est/reload-trust:
|
||||
post:
|
||||
tags: [EST]
|
||||
summary: Reload an EST profile's mTLS trust anchor (admin)
|
||||
description: |
|
||||
Triggers the same Reload that the SIGHUP watcher would run for
|
||||
the named EST profile. The body MUST be `{"path_id": "<pathID>"}`;
|
||||
an empty body targets the legacy `/.well-known/est` root profile
|
||||
(PathID="").
|
||||
|
||||
Returns 200 + `{"reloaded": true, ...}` on success; 404 when the
|
||||
path_id doesn't match any configured EST profile; 409 when the
|
||||
profile exists but mTLS is disabled on it (no trust anchor to
|
||||
reload); 500 when the underlying file fails to parse — in which
|
||||
case the holder retains the OLD pool so enrollment keeps working
|
||||
off the previous trust anchor while the operator fixes the file.
|
||||
|
||||
Admin-gated (M-008 pattern). EST RFC 7030 hardening master
|
||||
bundle Phase 7.2.
|
||||
operationId: reloadESTTrust
|
||||
requestBody:
|
||||
required: false
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
path_id:
|
||||
type: string
|
||||
description: EST profile PathID (empty string = legacy /.well-known/est root)
|
||||
responses:
|
||||
"200":
|
||||
description: Trust anchor reloaded
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
reloaded:
|
||||
type: boolean
|
||||
path_id:
|
||||
type: string
|
||||
reloaded_at:
|
||||
type: string
|
||||
format: date-time
|
||||
"400":
|
||||
description: Invalid JSON body
|
||||
"403":
|
||||
description: Admin access required
|
||||
"404":
|
||||
description: EST profile not found for the given path_id
|
||||
"409":
|
||||
description: EST profile exists but mTLS is disabled
|
||||
"500":
|
||||
description: Trust anchor reload failed (the OLD pool is retained)
|
||||
|
||||
/.well-known/pki/ocsp/{issuer_id}:
|
||||
post:
|
||||
tags: [CRL & OCSP]
|
||||
@@ -3700,6 +3798,71 @@ paths:
|
||||
"500":
|
||||
$ref: "#/components/responses/InternalError"
|
||||
|
||||
/.well-known/est/serverkeygen:
|
||||
post:
|
||||
tags: [EST]
|
||||
summary: EST server-driven key generation (RFC 7030 §4.4)
|
||||
description: |
|
||||
EST RFC 7030 §4.4 server-keygen endpoint. Server generates the
|
||||
keypair, issues the certificate with the new pubkey, and returns
|
||||
BOTH the cert (as `application/pkcs7-mime; smime-type=certs-only`)
|
||||
AND the corresponding private key (as `application/pkcs7-mime;
|
||||
smime-type=enveloped-data` — the private key is wrapped in CMS
|
||||
EnvelopedData encrypted to the client's CSR-supplied
|
||||
key-encipherment public key per RFC 7030 §4.4.2).
|
||||
|
||||
The two parts are returned as a `multipart/mixed` response body
|
||||
with a per-response random boundary. Standard EST clients
|
||||
(libest, openssl + smime) parse this multipart body natively.
|
||||
|
||||
Per-profile gate: this endpoint is registered for every EST
|
||||
profile but returns 404 unless the operator opted in via
|
||||
`CERTCTL_EST_PROFILE_<NAME>_SERVER_KEYGEN_ENABLED=true`. The
|
||||
per-profile gate constrains the attack surface — server-driven
|
||||
keygen requires the server to hold plaintext private keys
|
||||
briefly, a meaningful trust delta from device-driven keygen.
|
||||
|
||||
Auth modes match the simpleenroll endpoint: HTTP Basic when the
|
||||
per-profile enrollment-password is set, anonymous otherwise.
|
||||
The mTLS sibling route at /.well-known/est-mtls/<PathID>/serverkeygen
|
||||
is registered when the profile has MTLS_ENABLED=true.
|
||||
|
||||
EST RFC 7030 hardening master bundle Phase 5.
|
||||
operationId: estServerKeygen
|
||||
security: []
|
||||
requestBody:
|
||||
required: true
|
||||
description: Base64-encoded PKCS#10 CSR. The CSR's Subject + SANs
|
||||
drive the issued cert's identity. The CSR's pubkey MUST be RSA
|
||||
— that pubkey is the encryption target for the returned
|
||||
private key (CMS EnvelopedData uses RSA PKCS#1 v1.5 keyTrans).
|
||||
content:
|
||||
application/pkcs10:
|
||||
schema:
|
||||
type: string
|
||||
format: byte
|
||||
responses:
|
||||
"200":
|
||||
description: Multipart body with cert + EnvelopedData-wrapped key
|
||||
content:
|
||||
multipart/mixed:
|
||||
schema:
|
||||
type: string
|
||||
format: byte
|
||||
"400":
|
||||
description: |
|
||||
CSR malformed, CSR pubkey not RSA (RFC 7030 §4.4.2 requires
|
||||
an encryption mechanism), or unsupported keygen algorithm
|
||||
requested by the profile.
|
||||
"401":
|
||||
description: HTTP Basic auth failed (when enrollment-password is set)
|
||||
"404":
|
||||
description: Server-keygen not enabled for this profile
|
||||
"429":
|
||||
description: Per-(CN, source-IP) rate limit exceeded
|
||||
"500":
|
||||
$ref: "#/components/responses/InternalError"
|
||||
|
||||
# ─── SCEP (RFC 8894) ──────────────────────────────────────────────
|
||||
/scep:
|
||||
get:
|
||||
|
||||
Reference in New Issue
Block a user