Files
stack/infra/step-ca/templates/federation.tpl
Jarvis 7524d6e919
All checks were successful
ci/woodpecker/push/ci Pipeline was successful
ci/woodpecker/pr/ci Pipeline was successful
fix(federation): address #494 review findings (FED-M2-04)
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>
2026-04-21 22:34:05 -05:00

57 lines
2.0 KiB
Smarty

{
"subject": {{ toJson .Subject }},
"sans": {{ toJson .SANs }},
{{- /*
Mosaic Federation X.509 Certificate Template
============================================
Provisioner: mosaic-fed (JWK)
Implemented: FED-M2-04
This template emits two custom OID extensions carrying Mosaic federation
identifiers. The OTT token (built by CaService.buildOtt) MUST include the
claims `mosaic_grant_id` and `mosaic_subject_user_id` as top-level JWT
claims. step-ca exposes them under `.Token.<claim>` in this template.
OID Registry (Mosaic Internal Arc 1.3.6.1.4.1.99999):
1.3.6.1.4.1.99999.1 mosaic_grant_id (UUID, 36 ASCII chars)
1.3.6.1.4.1.99999.2 mosaic_subject_user_id (UUID, 36 ASCII chars)
DER encoding for each extension value (ASN.1 UTF8String):
Tag = 0x0C (UTF8String)
Length = 0x24 (decimal 36 the fixed length of a UUID string)
Value = 36 ASCII bytes of the UUID
The `printf` below builds the raw TLV bytes then base64-encodes them.
step-ca expects the `value` field to be base64-encoded raw DER bytes.
Fail-loud contract:
If either claim is missing from the token the printf will produce a
zero-length UUID field, making the extension malformed. step-ca will
reject the certificate rather than issuing one without the required OIDs.
Silent OID stripping is NEVER tolerated.
Step-CA template reference:
https://smallstep.com/docs/step-ca/templates
*/ -}}
"extensions": [
{
"id": "1.3.6.1.4.1.99999.1",
"critical": false,
"value": "{{ printf "\x0c%c%s" (len .Token.mosaic_grant_id) .Token.mosaic_grant_id | b64enc }}"
},
{
"id": "1.3.6.1.4.1.99999.2",
"critical": false,
"value": "{{ printf "\x0c%c%s" (len .Token.mosaic_subject_user_id) .Token.mosaic_subject_user_id | b64enc }}"
}
],
"keyUsage": ["digitalSignature"],
"extKeyUsage": ["clientAuth"],
"basicConstraints": {
"isCA": false
}
}