Security remediation for 6 findings from the M2 independent review. All changes are in apps/gateway/src/federation/.
CRIT-1: Added post-issuance OID verification in CaService.issueCert() — parses the returned cert with @peculiar/x509 and validates that OID 1.3.6.1.4.1.99999.1 (mosaic_grant_id) and 1.3.6.1.4.1.99999.2 (mosaic_subject_user_id) are present and match the request values. Based on infra/step-ca/templates/federation.tpl which uses ASN.1 UTF8String TLV encoding (0x0C + 1-byte length + UUID bytes). Throws CaServiceError on mismatch or absence.
CRIT-2: Added WHERE status=pending guard on the grant activation UPDATE in redeem() transaction using .returning() to detect no-op. Throws ConflictException if the grant was already activated. Also added WHERE state=pending guard on the federationPeers UPDATE.
HIGH-2: Removed 90-day silent fallback in extractCertNotAfter() — an unparseable cert now propagates as a 500 error rather than silently setting a wrong expiry date.
HIGH-4: Enrollment token is now logged with only its first 8 hex chars for correlation instead of the full 64-char plaintext token.
HIGH-5: Wrapped the redeem() body in try/catch; writes a best-effort failure audit row (outside transaction, .catch guarded) on any error path so all enrollment attempts are recorded in federation_audit_log regardless of success or failure.
MED-3: Added grant<->peer binding verification in createToken() — queries federationGrants to confirm the grantId peer matches dto.peerId before issuing a token, preventing cross-wiring attacks.
## Summary
Security remediation for 6 findings from the M2 independent review. All changes are in apps/gateway/src/federation/.
- **CRIT-1**: Added post-issuance OID verification in CaService.issueCert() — parses the returned cert with @peculiar/x509 and validates that OID 1.3.6.1.4.1.99999.1 (mosaic_grant_id) and 1.3.6.1.4.1.99999.2 (mosaic_subject_user_id) are present and match the request values. Based on infra/step-ca/templates/federation.tpl which uses ASN.1 UTF8String TLV encoding (0x0C + 1-byte length + UUID bytes). Throws CaServiceError on mismatch or absence.
- **CRIT-2**: Added WHERE status=pending guard on the grant activation UPDATE in redeem() transaction using .returning() to detect no-op. Throws ConflictException if the grant was already activated. Also added WHERE state=pending guard on the federationPeers UPDATE.
- **HIGH-2**: Removed 90-day silent fallback in extractCertNotAfter() — an unparseable cert now propagates as a 500 error rather than silently setting a wrong expiry date.
- **HIGH-4**: Enrollment token is now logged with only its first 8 hex chars for correlation instead of the full 64-char plaintext token.
- **HIGH-5**: Wrapped the redeem() body in try/catch; writes a best-effort failure audit row (outside transaction, .catch guarded) on any error path so all enrollment attempts are recorded in federation_audit_log regardless of success or failure.
- **MED-3**: Added grant<->peer binding verification in createToken() — queries federationGrants to confirm the grantId peer matches dto.peerId before issuing a token, preventing cross-wiring attacks.
## Test plan
- Typecheck: pnpm typecheck passes (38/38 tasks successful)
- Lint: pnpm lint passes
- Format: pnpm format:check passes
- No regressions: same 398 tests pass, same 5 pre-existing suite failures remain (jose/@peculiar/x509 not loaded in vitest context)
- Woodpecker CI pipeline expected green
Closes #461
CRIT-1: Add post-issuance OID verification in CaService.issueCert() — parses
the returned cert with @peculiar/x509 and validates that OIDs 1.3.6.1.4.1.99999.1
(mosaic_grant_id) and 1.3.6.1.4.1.99999.2 (mosaic_subject_user_id) are present
and match the request values. Throws CaServiceError on mismatch or absence.
CRIT-2: Guard grant activation in the redeem() transaction with
WHERE status='pending' (RETURNING to detect no-op). Throw ConflictException
if the grant was already activated. Also add WHERE state='pending' guard on
the federationPeers UPDATE.
HIGH-2: Remove 90-day silent fallback in extractCertNotAfter() — an unparseable
cert now propagates as a 500 error rather than silently setting a wrong expiry.
HIGH-4: Log only the first 8 hex chars of the enrollment token in the issueCert
failure error log — never log the full 64-char token.
HIGH-5: Wrap redeem() body in try/catch; write a best-effort failure audit row
(outside transaction, .catch(() => {}) guarded) on any error path so all
enrollment attempts are audited regardless of outcome.
MED-3: Verify grantId ↔ peerId binding in createToken() before inserting the
token — prevents cross-wiring a grant to an attacker-controlled peer.
Closes#461
Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Summary
Security remediation for 6 findings from the M2 independent review. All changes are in apps/gateway/src/federation/.
Test plan
Closes #461
CRIT-1: Add post-issuance OID verification in CaService.issueCert() — parses the returned cert with @peculiar/x509 and validates that OIDs 1.3.6.1.4.1.99999.1 (mosaic_grant_id) and 1.3.6.1.4.1.99999.2 (mosaic_subject_user_id) are present and match the request values. Throws CaServiceError on mismatch or absence. CRIT-2: Guard grant activation in the redeem() transaction with WHERE status='pending' (RETURNING to detect no-op). Throw ConflictException if the grant was already activated. Also add WHERE state='pending' guard on the federationPeers UPDATE. HIGH-2: Remove 90-day silent fallback in extractCertNotAfter() — an unparseable cert now propagates as a 500 error rather than silently setting a wrong expiry. HIGH-4: Log only the first 8 hex chars of the enrollment token in the issueCert failure error log — never log the full 64-char token. HIGH-5: Wrap redeem() body in try/catch; write a best-effort failure audit row (outside transaction, .catch(() => {}) guarded) on any error path so all enrollment attempts are audited regardless of outcome. MED-3: Verify grantId ↔ peerId binding in createToken() before inserting the token — prevents cross-wiring a grant to an attacker-controlled peer. Closes #461 Co-Authored-By: Claude Sonnet 4.6 <[email protected]>