H1: Replace HS256/HMAC signing with real JWK signing (ES256/RS256/ES384)
via jose SignJWT. Algorithm derived from JWK kty/crv. Provisioner
password dropped as signing input; kept only as optional env var for
PBES2-decrypt path at startup.
H2: Clamp cert TTL to 900s (15 min) in both DTO validator and issueCert().
Default changed to 300s (5 min). @Max reduced to 15*60.
H3: Real CSR validation via @peculiar/x509: parse PEM, verify self-
signature, reject weak keys (RSA<2048, bad EC curves), reject MD5/SHA-1.
New validateCsr() throws CaServiceError code INVALID_CSR on failure.
H4: Replace hardcoded \x24 DER length in federation.tpl with dynamic
printf "%c" (len ...) encoding. Add UUID-shape validation for grantId
and subjectUserId in buildOtt() with code INVALID_GRANT_ID.
H5: Load JWK into KeyObject once (lazy, cached). provisionerKeyJson raw
string not stored as class field. provisionerPassword not stored.
M1: Set JWT sub to CSR CN (extracted via @peculiar/x509) instead of URL.
M2: Add jti: crypto.randomUUID() to OTT claims.
M3: Drop top-level sha claim; keep only step.sha.
M4: extractSerial() throws CaServiceError code CERT_PARSE instead of
returning 'unknown' on failure.
M5: Set timeout: 5000 on https.RequestOptions + req.setTimeout(5000).
M6: OTT signature verified with jose.jwtVerify in tests. Added real P-256
CSR test via @peculiar/x509 generator. Added provisionerPassword
leak-check test.
M7: Constructor validates STEP_CA_URL must be https://.
Verification: typecheck ✓, 385 tests pass (16 new), lint ✓, format ✓.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Add CaService (@Injectable) that POSTs CSRs to step-ca /1.0/sign over
HTTPS with a pinned CA root cert; builds HS256 OTT with custom claims
mosaic_grant_id and mosaic_subject_user_id plus step.sha CSR fingerprint
- Add CaServiceError with cause + remediation for fail-loud contract
- Add IssueCertRequestDto and IssuedCertDto with class-validator decorators
- Add FederationModule exporting CaService; wire into AppModule
- Replace federation.tpl TODO placeholder with real step-ca Go template
emitting OID 1.3.6.1.4.1.99999.1 (grantId) and .2 (subjectUserId) as
DER UTF8String extensions (tag 0x0C, length 0x24, base64-encoded value)
- Update infra/step-ca/init.sh to patch mosaic-fed provisioner config with
templateFile path via jq on first boot (idempotent)
- Append OID assignment registry and CA env var table to docs/federation/SETUP.md
- 11 unit tests pass: happy path, certChain fallbacks, HTTP 401/4xx, malformed
CSR (no HTTP call), non-JSON response, connection error, JWT claim assertions
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>