fix(federation): security hardening — OID verification, atomic activation, audit on failure (#501)
Some checks failed
ci/woodpecker/push/ci Pipeline failed
ci/woodpecker/push/publish Pipeline failed

This commit was merged in pull request #501.
This commit is contained in:
2026-04-22 06:02:52 +00:00
parent 0ee5b14c68
commit fc1600b738
2 changed files with 212 additions and 116 deletions

View File

@@ -35,7 +35,7 @@ import * as crypto from 'node:crypto';
import * as fs from 'node:fs';
import * as https from 'node:https';
import { SignJWT, importJWK } from 'jose';
import { Pkcs10CertificateRequest } from '@peculiar/x509';
import { Pkcs10CertificateRequest, X509Certificate } from '@peculiar/x509';
import type { IssueCertRequestDto } from './ca.dto.js';
import { IssuedCertDto } from './ca.dto.js';
@@ -624,6 +624,51 @@ export class CaService {
const serialNumber = extractSerial(response.crt);
// CRIT-1: Verify the issued certificate contains both Mosaic OID extensions
// with the correct values. Step-CA's federation.tpl encodes each as an ASN.1
// UTF8String TLV: tag 0x0C + 1-byte length + UUID bytes. We skip 2 bytes
// (tag + length) to extract the raw UUID string.
const issuedCert = new X509Certificate(response.crt);
const decoder = new TextDecoder();
const grantIdExt = issuedCert.getExtension('1.3.6.1.4.1.99999.1');
if (!grantIdExt) {
throw new CaServiceError(
'Issued certificate is missing required Mosaic OID: mosaic_grant_id',
'The Step-CA federation.tpl template did not embed OID 1.3.6.1.4.1.99999.1. Check the provisioner template configuration.',
undefined,
'OID_MISSING',
);
}
const grantIdInCert = decoder.decode(grantIdExt.value.slice(2));
if (grantIdInCert !== req.grantId) {
throw new CaServiceError(
`Issued certificate mosaic_grant_id mismatch: expected ${req.grantId}, got ${grantIdInCert}`,
'The Step-CA issued a certificate with a different grant ID than requested. This may indicate a provisioner misconfiguration or a MITM.',
undefined,
'OID_MISMATCH',
);
}
const subjectUserIdExt = issuedCert.getExtension('1.3.6.1.4.1.99999.2');
if (!subjectUserIdExt) {
throw new CaServiceError(
'Issued certificate is missing required Mosaic OID: mosaic_subject_user_id',
'The Step-CA federation.tpl template did not embed OID 1.3.6.1.4.1.99999.2. Check the provisioner template configuration.',
undefined,
'OID_MISSING',
);
}
const subjectUserIdInCert = decoder.decode(subjectUserIdExt.value.slice(2));
if (subjectUserIdInCert !== req.subjectUserId) {
throw new CaServiceError(
`Issued certificate mosaic_subject_user_id mismatch: expected ${req.subjectUserId}, got ${subjectUserIdInCert}`,
'The Step-CA issued a certificate with a different subject user ID than requested. This may indicate a provisioner misconfiguration or a MITM.',
undefined,
'OID_MISMATCH',
);
}
this.logger.log(`Certificate issued — serial=${serialNumber} grantId=${req.grantId}`);
const result = new IssuedCertDto();