fix(federation): security hardening — OID verification, atomic activation, audit on failure (#501)
This commit was merged in pull request #501.
This commit is contained in:
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user