feat(federation): Step-CA client service for grant certs (FED-M2-04)
- 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>
This commit is contained in:
@@ -6,6 +6,10 @@
|
||||
# On the first run (no /home/step/config/ca.json present) this script:
|
||||
# 1. Initialises Step-CA with a JWK provisioner named "mosaic-fed".
|
||||
# 2. Writes the CA configuration to the persistent volume at /home/step.
|
||||
# 3. Copies the federation X.509 template into the CA config directory.
|
||||
# 4. Patches the mosaic-fed provisioner entry in ca.json to reference the
|
||||
# template via options.x509.templateFile (using jq — must be installed
|
||||
# in the container image).
|
||||
#
|
||||
# On subsequent runs (config already exists) this script skips init and
|
||||
# starts the CA directly.
|
||||
@@ -18,15 +22,16 @@
|
||||
# Prod: mounted from a Docker secret at /run/secrets/ca_password.
|
||||
#
|
||||
# OID template:
|
||||
# infra/step-ca/templates/federation.tpl is copied into the CA config
|
||||
# directory so the JWK provisioner can reference it. The template
|
||||
# skeleton is wired in M2-04 when the CA service lands the SAN-bearing
|
||||
# CSR work.
|
||||
# infra/step-ca/templates/federation.tpl emits custom OID extensions:
|
||||
# 1.3.6.1.4.1.99999.1 — mosaic_grant_id
|
||||
# 1.3.6.1.4.1.99999.2 — mosaic_subject_user_id
|
||||
|
||||
set -e
|
||||
|
||||
CA_CONFIG="/home/step/config/ca.json"
|
||||
PASSWORD_FILE="/run/secrets/ca_password"
|
||||
TEMPLATE_SRC="/etc/step-ca-templates/federation.tpl"
|
||||
TEMPLATE_DEST="/home/step/templates/federation.tpl"
|
||||
|
||||
if [ ! -f "${CA_CONFIG}" ]; then
|
||||
echo "[step-ca init] First boot detected — initialising Mosaic Federation CA..."
|
||||
@@ -43,12 +48,37 @@ if [ ! -f "${CA_CONFIG}" ]; then
|
||||
|
||||
echo "[step-ca init] CA initialised."
|
||||
|
||||
# Copy the X.509 template into the Step-CA config directory so the
|
||||
# provisioner can reference it in M2-04.
|
||||
if [ -f "/etc/step-ca-templates/federation.tpl" ]; then
|
||||
# Copy the X.509 template into the Step-CA config directory.
|
||||
if [ -f "${TEMPLATE_SRC}" ]; then
|
||||
mkdir -p /home/step/templates
|
||||
cp /etc/step-ca-templates/federation.tpl /home/step/templates/federation.tpl
|
||||
echo "[step-ca init] Federation X.509 template copied to /home/step/templates/."
|
||||
cp "${TEMPLATE_SRC}" "${TEMPLATE_DEST}"
|
||||
echo "[step-ca init] Federation X.509 template copied to ${TEMPLATE_DEST}."
|
||||
else
|
||||
echo "[step-ca init] WARNING: Template source ${TEMPLATE_SRC} not found — skipping copy."
|
||||
fi
|
||||
|
||||
# Wire the template into the mosaic-fed provisioner via jq.
|
||||
# This is idempotent: the block only runs once (first boot).
|
||||
#
|
||||
# jq filter: find the provisioner entry with name "mosaic-fed" and set
|
||||
# .options.x509.templateFile to the absolute path of the template.
|
||||
# All other provisioners and config keys are left unchanged.
|
||||
if [ -f "${TEMPLATE_DEST}" ] && command -v jq > /dev/null 2>&1; then
|
||||
echo "[step-ca init] Patching mosaic-fed provisioner with X.509 template..."
|
||||
TEMP_CONFIG="${CA_CONFIG}.tmp"
|
||||
jq --arg tpl "${TEMPLATE_DEST}" '
|
||||
.authority.provisioners |= map(
|
||||
if .name == "mosaic-fed" then
|
||||
.options.x509.templateFile = $tpl
|
||||
else
|
||||
.
|
||||
end
|
||||
)
|
||||
' "${CA_CONFIG}" > "${TEMP_CONFIG}" && mv "${TEMP_CONFIG}" "${CA_CONFIG}"
|
||||
echo "[step-ca init] Provisioner patched."
|
||||
elif ! command -v jq > /dev/null 2>&1; then
|
||||
echo "[step-ca init] WARNING: jq not found — skipping provisioner template patch."
|
||||
echo "[step-ca init] Install jq in the step-ca image to enable automatic template wiring."
|
||||
fi
|
||||
|
||||
echo "[step-ca init] Startup complete."
|
||||
|
||||
@@ -5,41 +5,49 @@
|
||||
{{- /*
|
||||
Mosaic Federation X.509 Certificate Template
|
||||
============================================
|
||||
This template is used by the "mosaic-fed" JWK provisioner to sign
|
||||
federation client certificates.
|
||||
Provisioner: mosaic-fed (JWK)
|
||||
Implemented: FED-M2-04
|
||||
|
||||
Custom OID extensions (per PRD §6):
|
||||
1.3.6.1.4.1.99999.1 — mosaic.federation.grantId (UUID string)
|
||||
1.3.6.1.4.1.99999.2 — mosaic.federation.subjectUserId (UUID string)
|
||||
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.
|
||||
|
||||
TODO (M2-04): Wire actual OID extensions below once the CA service
|
||||
(apps/gateway/src/federation/ca.service.ts) lands the SAN-bearing CSR
|
||||
work and the template can be exercised end-to-end.
|
||||
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
|
||||
|
||||
Expected final shape of the extensions block (placeholder — not yet
|
||||
activated):
|
||||
|
||||
"extensions": [
|
||||
{
|
||||
"id": "1.3.6.1.4.1.99999.1",
|
||||
"critical": false,
|
||||
"value": {{ toJson (first .Token.mosaic_grant_id) }}
|
||||
},
|
||||
{
|
||||
"id": "1.3.6.1.4.1.99999.2",
|
||||
"critical": false,
|
||||
"value": {{ toJson (first .Token.mosaic_subject_user_id) }}
|
||||
}
|
||||
],
|
||||
|
||||
The provisioner must pass these values in the ACME/JWK token payload
|
||||
(token claims `mosaic_grant_id` and `mosaic_subject_user_id`) when
|
||||
submitting the CSR. M2-04 owns that work.
|
||||
*/ -}}
|
||||
|
||||
"extensions": [
|
||||
{
|
||||
"id": "1.3.6.1.4.1.99999.1",
|
||||
"critical": false,
|
||||
"value": "{{ printf "\x0c\x24%s" .Token.mosaic_grant_id | b64enc }}"
|
||||
},
|
||||
{
|
||||
"id": "1.3.6.1.4.1.99999.2",
|
||||
"critical": false,
|
||||
"value": "{{ printf "\x0c\x24%s" .Token.mosaic_subject_user_id | b64enc }}"
|
||||
}
|
||||
],
|
||||
|
||||
"keyUsage": ["digitalSignature"],
|
||||
"extKeyUsage": ["clientAuth"],
|
||||
"basicConstraints": {
|
||||
|
||||
Reference in New Issue
Block a user