Move CRL rebuild to upon cert revocation temp

This commit is contained in:
Tuan Dang
2024-06-10 00:33:18 -04:00
parent b48325b4ba
commit 3079cd72df
9 changed files with 247 additions and 160 deletions
@@ -68,10 +68,7 @@ export async function up(knex: Knex): Promise<void> {
t.timestamps(true, true, true); t.timestamps(true, true, true);
t.uuid("caId").notNullable().unique(); t.uuid("caId").notNullable().unique();
t.foreign("caId").references("id").inTable(TableName.CertificateAuthority).onDelete("CASCADE"); t.foreign("caId").references("id").inTable(TableName.CertificateAuthority).onDelete("CASCADE");
t.binary("encryptedCrl").notNullable(); // TODO: encrypt t.binary("encryptedCrl").notNullable();
t.integer("ttl").notNullable(); // in minutes
// TODO: consider type (crl or delta)
// TODO: rebuild interval
}); });
} }
@@ -14,8 +14,7 @@ export const CertificateAuthorityCrlSchema = z.object({
createdAt: z.date(), createdAt: z.date(),
updatedAt: z.date(), updatedAt: z.date(),
caId: z.string().uuid(), caId: z.string().uuid(),
encryptedCrl: zodBuffer, encryptedCrl: zodBuffer
ttl: z.number()
}); });
export type TCertificateAuthorityCrl = z.infer<typeof CertificateAuthorityCrlSchema>; export type TCertificateAuthorityCrl = z.infer<typeof CertificateAuthorityCrlSchema>;
+2
View File
@@ -528,6 +528,8 @@ export const registerRoutes = async (
certificateCertDAL, certificateCertDAL,
certificateAuthorityDAL, certificateAuthorityDAL,
certificateAuthorityCertDAL, certificateAuthorityCertDAL,
certificateAuthorityCrlDAL,
certificateAuthoritySecretDAL,
projectDAL, projectDAL,
kmsService, kmsService,
permissionService permissionService
@@ -420,35 +420,35 @@ export const registerCaRouter = async (server: FastifyZodProvider) => {
} }
}); });
server.route({ // server.route({
method: "GET", // method: "GET",
url: "/:caId/crl/rotate", // url: "/:caId/crl/rotate",
config: { // config: {
rateLimit: writeLimit // rateLimit: writeLimit
}, // },
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), // onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
schema: { // schema: {
description: "Rotate CRL of the CA", // description: "Rotate CRL of the CA",
params: z.object({ // params: z.object({
caId: z.string().trim() // caId: z.string().trim()
}), // }),
response: { // response: {
200: z.object({ // 200: z.object({
message: z.string() // message: z.string()
}) // })
} // }
}, // },
handler: async (req) => { // handler: async (req) => {
await server.services.certificateAuthority.rotateCaCrl({ // await server.services.certificateAuthority.rotateCaCrl({
caId: req.params.caId, // caId: req.params.caId,
actor: req.permission.type, // actor: req.permission.type,
actorId: req.permission.id, // actorId: req.permission.id,
actorAuthMethod: req.permission.authMethod, // actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId // actorOrgId: req.permission.orgId
}); // });
return { // return {
message: "Successfully rotated CA CRL" // message: "Successfully rotated CA CRL"
}; // };
} // }
}); // });
}; };
@@ -1,5 +1,11 @@
import { CertKeyAlgorithm } from "../certificate/certificate-types"; import * as x509 from "@peculiar/x509";
import { TDNParts } from "./certificate-authority-types"; import crypto from "crypto";
import { BadRequestError } from "@app/lib/errors";
import { getProjectKmsCertificateKeyId } from "@app/services/project/project-fns";
import { CertKeyAlgorithm, CertStatus } from "../certificate/certificate-types";
import { TDNParts, TRebuildCaCrlDTO } from "./certificate-authority-types";
export const createDistinguishedName = (parts: TDNParts) => { export const createDistinguishedName = (parts: TDNParts) => {
const dnParts = []; const dnParts = [];
@@ -44,3 +50,72 @@ export const keyAlgorithmToAlgCfg = (keyAlgorithm: CertKeyAlgorithm) => {
} }
} }
}; };
export const rebuildCaCrl = async ({
caId,
certificateAuthorityDAL,
certificateAuthorityCrlDAL,
certificateAuthoritySecretDAL,
projectDAL,
certificateDAL,
kmsService
}: TRebuildCaCrlDTO) => {
const ca = await certificateAuthorityDAL.findById(caId);
if (!ca) throw new BadRequestError({ message: "CA not found" });
const caSecret = await certificateAuthoritySecretDAL.findOne({ caId: ca.id });
const alg = keyAlgorithmToAlgCfg(ca.keyAlgorithm as CertKeyAlgorithm);
const keyId = await getProjectKmsCertificateKeyId({
projectId: ca.projectId,
projectDAL,
kmsService
});
const privateKey = await kmsService.decrypt({
kmsId: keyId,
cipherTextBlob: caSecret.encryptedPrivateKey
});
const skObj = crypto.createPrivateKey({ key: privateKey, format: "der", type: "pkcs8" });
const sk = await crypto.subtle.importKey("pkcs8", skObj.export({ format: "der", type: "pkcs8" }), alg, true, [
"sign"
]);
const revokedCerts = await certificateDAL.find({
caId: ca.id,
status: CertStatus.REVOKED
});
const crl = await x509.X509CrlGenerator.create({
issuer: ca.dn,
thisUpdate: new Date(),
nextUpdate: new Date("2025/12/12"),
entries: revokedCerts.map((revokedCert) => {
return {
serialNumber: revokedCert.serialNumber,
revocationDate: new Date(revokedCert.revokedAt as Date),
reason: revokedCert.revocationReason as number,
invalidity: new Date("2022/01/01"),
issuer: ca.dn
};
}),
signingAlgorithm: alg,
signingKey: sk
});
const { cipherTextBlob: encryptedCrl } = await kmsService.encrypt({
kmsId: keyId,
plainText: Buffer.from(new Uint8Array(crl.rawData))
});
await certificateAuthorityCrlDAL.update(
{
caId: ca.id
},
{
encryptedCrl
}
);
};
@@ -30,7 +30,7 @@ import {
TGetCrl, TGetCrl,
TImportCertToCaDTO, TImportCertToCaDTO,
TIssueCertFromCaDTO, TIssueCertFromCaDTO,
TRotateCrlDTO, // TRotateCrlDTO,
TSignIntermediateDTO, TSignIntermediateDTO,
TUpdateCaDTO TUpdateCaDTO
} from "./certificate-authority-types"; } from "./certificate-authority-types";
@@ -145,8 +145,6 @@ export const certificateAuthorityServiceFactory = ({
tx tx
); );
// TODO: create CRL
const keyId = await getProjectKmsCertificateKeyId({ const keyId = await getProjectKmsCertificateKeyId({
projectId: project.id, projectId: project.id,
projectDAL, projectDAL,
@@ -154,8 +152,7 @@ export const certificateAuthorityServiceFactory = ({
}); });
if (type === CaType.ROOT) { if (type === CaType.ROOT) {
// note: self-signed cert only applicable for root CA // note: create self-signed cert only applicable for root CA
const cert = await x509.X509CertificateGenerator.createSelfSigned({ const cert = await x509.X509CertificateGenerator.createSelfSigned({
name: dn, name: dn,
serialNumber, serialNumber,
@@ -190,22 +187,31 @@ export const certificateAuthorityServiceFactory = ({
}, },
tx tx
); );
const { cipherTextBlob: encryptedCrl } = await kmsService.encrypt({
kmsId: keyId,
plainText: Buffer.alloc(0)
});
await certificateAuthorityCrlDAL.create(
{
caId: ca.id,
encryptedCrl,
ttl: 60 // in minutes
},
tx
);
} }
// create empty CRL
const crl = await x509.X509CrlGenerator.create({
issuer: ca.dn,
thisUpdate: new Date(),
nextUpdate: new Date("2025/12/12"), // TODO: change
entries: [],
signingAlgorithm: alg,
signingKey: keys.privateKey
});
const { cipherTextBlob: encryptedCrl } = await kmsService.encrypt({
kmsId: keyId,
plainText: Buffer.from(new Uint8Array(crl.rawData))
});
await certificateAuthorityCrlDAL.create(
{
caId: ca.id,
encryptedCrl
},
tx
);
// https://nodejs.org/api/crypto.html#static-method-keyobjectfromkey // https://nodejs.org/api/crypto.html#static-method-keyobjectfromkey
const skObj = KeyObject.from(keys.privateKey); const skObj = KeyObject.from(keys.privateKey);
@@ -817,9 +823,8 @@ export const certificateAuthorityServiceFactory = ({
ProjectPermissionSub.CertificateAuthorities ProjectPermissionSub.CertificateAuthorities
); );
const caSecret = await certificateAuthoritySecretDAL.findOne({ caId: ca.id }); const caCrl = await certificateAuthorityCrlDAL.findOne({ caId: ca.id });
if (!caCrl) throw new BadRequestError({ message: "CRL not found" });
const alg = keyAlgorithmToAlgCfg(ca.keyAlgorithm as CertKeyAlgorithm);
const keyId = await getProjectKmsCertificateKeyId({ const keyId = await getProjectKmsCertificateKeyId({
projectId: ca.projectId, projectId: ca.projectId,
@@ -827,37 +832,12 @@ export const certificateAuthorityServiceFactory = ({
kmsService kmsService
}); });
const privateKey = await kmsService.decrypt({ const decryptedCrl = await kmsService.decrypt({
kmsId: keyId, kmsId: keyId,
cipherTextBlob: caSecret.encryptedPrivateKey cipherTextBlob: caCrl.encryptedCrl
}); });
const skObj = crypto.createPrivateKey({ key: privateKey, format: "der", type: "pkcs8" }); const crl = new x509.X509Crl(decryptedCrl);
const sk = await crypto.subtle.importKey("pkcs8", skObj.export({ format: "der", type: "pkcs8" }), alg, true, [
"sign"
]);
const revokedCerts = await certificateDAL.find({
caId: ca.id,
status: CertStatus.REVOKED
});
const crl = await x509.X509CrlGenerator.create({
issuer: ca.dn,
thisUpdate: new Date(),
nextUpdate: new Date("2025/12/12"),
entries: revokedCerts.map((revokedCert) => {
return {
serialNumber: revokedCert.serialNumber,
revocationDate: new Date(revokedCert.revokedAt as Date),
reason: revokedCert.revocationReason as number,
invalidity: new Date("2022/01/01"),
issuer: ca.dn
};
}),
signingAlgorithm: alg,
signingKey: sk
});
const base64crl = crl.toString("base64"); const base64crl = crl.toString("base64");
const crlPem = `-----BEGIN X509 CRL-----\n${base64crl.match(/.{1,64}/g)?.join("\n")}\n-----END X509 CRL-----`; const crlPem = `-----BEGIN X509 CRL-----\n${base64crl.match(/.{1,64}/g)?.join("\n")}\n-----END X509 CRL-----`;
@@ -867,86 +847,86 @@ export const certificateAuthorityServiceFactory = ({
}; };
}; };
const rotateCaCrl = async ({ caId, actorId, actorAuthMethod, actor, actorOrgId }: TRotateCrlDTO) => { // const rotateCaCrl = async ({ caId, actorId, actorAuthMethod, actor, actorOrgId }: TRotateCrlDTO) => {
const ca = await certificateAuthorityDAL.findById(caId); // const ca = await certificateAuthorityDAL.findById(caId);
if (!ca) throw new BadRequestError({ message: "CA not found" }); // if (!ca) throw new BadRequestError({ message: "CA not found" });
const { permission } = await permissionService.getProjectPermission( // const { permission } = await permissionService.getProjectPermission(
actor, // actor,
actorId, // actorId,
ca.projectId, // ca.projectId,
actorAuthMethod, // actorAuthMethod,
actorOrgId // actorOrgId
); // );
ForbiddenError.from(permission).throwUnlessCan( // ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Read, // ProjectPermissionActions.Read,
ProjectPermissionSub.CertificateAuthorities // ProjectPermissionSub.CertificateAuthorities
); // );
const caSecret = await certificateAuthoritySecretDAL.findOne({ caId: ca.id }); // const caSecret = await certificateAuthoritySecretDAL.findOne({ caId: ca.id });
const alg = keyAlgorithmToAlgCfg(ca.keyAlgorithm as CertKeyAlgorithm); // const alg = keyAlgorithmToAlgCfg(ca.keyAlgorithm as CertKeyAlgorithm);
const keyId = await getProjectKmsCertificateKeyId({ // const keyId = await getProjectKmsCertificateKeyId({
projectId: ca.projectId, // projectId: ca.projectId,
projectDAL, // projectDAL,
kmsService // kmsService
}); // });
const privateKey = await kmsService.decrypt({ // const privateKey = await kmsService.decrypt({
kmsId: keyId, // kmsId: keyId,
cipherTextBlob: caSecret.encryptedPrivateKey // cipherTextBlob: caSecret.encryptedPrivateKey
}); // });
const skObj = crypto.createPrivateKey({ key: privateKey, format: "der", type: "pkcs8" }); // const skObj = crypto.createPrivateKey({ key: privateKey, format: "der", type: "pkcs8" });
const sk = await crypto.subtle.importKey("pkcs8", skObj.export({ format: "der", type: "pkcs8" }), alg, true, [ // const sk = await crypto.subtle.importKey("pkcs8", skObj.export({ format: "der", type: "pkcs8" }), alg, true, [
"sign" // "sign"
]); // ]);
const revokedCerts = await certificateDAL.find({ // const revokedCerts = await certificateDAL.find({
caId: ca.id, // caId: ca.id,
status: CertStatus.REVOKED // status: CertStatus.REVOKED
}); // });
const crl = await x509.X509CrlGenerator.create({ // const crl = await x509.X509CrlGenerator.create({
issuer: ca.dn, // issuer: ca.dn,
thisUpdate: new Date(), // thisUpdate: new Date(),
nextUpdate: new Date("2025/12/12"), // nextUpdate: new Date("2025/12/12"),
entries: revokedCerts.map((revokedCert) => { // entries: revokedCerts.map((revokedCert) => {
return { // return {
serialNumber: revokedCert.serialNumber, // serialNumber: revokedCert.serialNumber,
revocationDate: new Date(revokedCert.revokedAt as Date), // revocationDate: new Date(revokedCert.revokedAt as Date),
reason: revokedCert.revocationReason as number, // reason: revokedCert.revocationReason as number,
invalidity: new Date("2022/01/01"), // invalidity: new Date("2022/01/01"),
issuer: ca.dn // issuer: ca.dn
}; // };
}), // }),
signingAlgorithm: alg, // signingAlgorithm: alg,
signingKey: sk // signingKey: sk
}); // });
const { cipherTextBlob: encryptedCrl } = await kmsService.encrypt({ // const { cipherTextBlob: encryptedCrl } = await kmsService.encrypt({
kmsId: keyId, // kmsId: keyId,
plainText: Buffer.from(new Uint8Array(crl.rawData)) // plainText: Buffer.from(new Uint8Array(crl.rawData))
}); // });
await certificateAuthorityCrlDAL.update( // await certificateAuthorityCrlDAL.update(
{ // {
caId: ca.id // caId: ca.id
}, // },
{ // {
encryptedCrl // encryptedCrl
} // }
); // );
const base64crl = crl.toString("base64"); // const base64crl = crl.toString("base64");
const crlPem = `-----BEGIN X509 CRL-----\n${base64crl.match(/.{1,64}/g)?.join("\n")}\n-----END X509 CRL-----`; // const crlPem = `-----BEGIN X509 CRL-----\n${base64crl.match(/.{1,64}/g)?.join("\n")}\n-----END X509 CRL-----`;
return { // return {
crl: crlPem // crl: crlPem
}; // };
}; // };
return { return {
createCa, createCa,
@@ -958,7 +938,7 @@ export const certificateAuthorityServiceFactory = ({
signIntermediate, signIntermediate,
importCertToCa, importCertToCa,
issueCertFromCa, issueCertFromCa,
getCaCrl, getCaCrl
rotateCaCrl // rotateCaCrl
}; };
}; };
@@ -1,6 +1,12 @@
import { TProjectPermission } from "@app/lib/types"; import { TProjectPermission } from "@app/lib/types";
import { TCertificateDALFactory } from "@app/services/certificate/certificate-dal";
import { TKmsServiceFactory } from "@app/services/kms/kms-service";
import { TProjectDALFactory } from "@app/services/project/project-dal";
import { CertKeyAlgorithm } from "../certificate/certificate-types"; import { CertKeyAlgorithm } from "../certificate/certificate-types";
import { TCertificateAuthorityCrlDALFactory } from "./certificate-authority-crl-dal";
import { TCertificateAuthorityDALFactory } from "./certificate-authority-dal";
import { TCertificateAuthoritySecretDALFactory } from "./certificate-authority-secret-dal";
export enum CaType { export enum CaType {
ROOT = "root", ROOT = "root",
@@ -88,6 +94,16 @@ export type TDNParts = {
locality?: string; locality?: string;
}; };
export type TRebuildCaCrlDTO = {
caId: string;
certificateAuthorityDAL: Pick<TCertificateAuthorityDALFactory, "findById">;
certificateAuthorityCrlDAL: Pick<TCertificateAuthorityCrlDALFactory, "update">;
certificateAuthoritySecretDAL: Pick<TCertificateAuthoritySecretDALFactory, "findOne">;
projectDAL: Pick<TProjectDALFactory, "findOne" | "updateById" | "transaction">;
certificateDAL: Pick<TCertificateDALFactory, "find">;
kmsService: Pick<TKmsServiceFactory, "generateKmsKey" | "decrypt" | "encrypt">;
};
export type TRotateCaCrlTriggerDTO = { export type TRotateCaCrlTriggerDTO = {
caId: string; caId: string;
rotationIntervalDays: number; rotationIntervalDays: number;
@@ -6,19 +6,24 @@ import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services
import { TCertificateCertDALFactory } from "@app/services/certificate/certificate-cert-dal"; import { TCertificateCertDALFactory } from "@app/services/certificate/certificate-cert-dal";
import { TCertificateDALFactory } from "@app/services/certificate/certificate-dal"; import { TCertificateDALFactory } from "@app/services/certificate/certificate-dal";
import { TCertificateAuthorityCertDALFactory } from "@app/services/certificate-authority/certificate-authority-cert-dal"; import { TCertificateAuthorityCertDALFactory } from "@app/services/certificate-authority/certificate-authority-cert-dal";
import { TCertificateAuthorityCrlDALFactory } from "@app/services/certificate-authority/certificate-authority-crl-dal";
import { TCertificateAuthorityDALFactory } from "@app/services/certificate-authority/certificate-authority-dal"; import { TCertificateAuthorityDALFactory } from "@app/services/certificate-authority/certificate-authority-dal";
import { TCertificateAuthoritySecretDALFactory } from "@app/services/certificate-authority/certificate-authority-secret-dal";
import { TKmsServiceFactory } from "@app/services/kms/kms-service"; import { TKmsServiceFactory } from "@app/services/kms/kms-service";
import { TProjectDALFactory } from "@app/services/project/project-dal"; import { TProjectDALFactory } from "@app/services/project/project-dal";
import { getProjectKmsCertificateKeyId } from "@app/services/project/project-fns"; import { getProjectKmsCertificateKeyId } from "@app/services/project/project-fns";
import { rebuildCaCrl } from "../certificate-authority/certificate-authority-fns";
import { revocationReasonToCrlCode } from "./certificate-fns"; import { revocationReasonToCrlCode } from "./certificate-fns";
import { CertStatus, TDeleteCertDTO, TGetCertCertDTO, TGetCertDTO, TRevokeCertDTO } from "./certificate-types"; import { CertStatus, TDeleteCertDTO, TGetCertCertDTO, TGetCertDTO, TRevokeCertDTO } from "./certificate-types";
type TCertificateServiceFactoryDep = { type TCertificateServiceFactoryDep = {
certificateDAL: Pick<TCertificateDALFactory, "findOne" | "deleteById" | "update">; certificateDAL: Pick<TCertificateDALFactory, "findOne" | "deleteById" | "update" | "find">;
certificateCertDAL: Pick<TCertificateCertDALFactory, "findOne">; certificateCertDAL: Pick<TCertificateCertDALFactory, "findOne">;
certificateAuthorityDAL: Pick<TCertificateAuthorityDALFactory, "findById">; certificateAuthorityDAL: Pick<TCertificateAuthorityDALFactory, "findById">;
certificateAuthorityCertDAL: Pick<TCertificateAuthorityCertDALFactory, "findOne">; certificateAuthorityCertDAL: Pick<TCertificateAuthorityCertDALFactory, "findOne">;
certificateAuthorityCrlDAL: Pick<TCertificateAuthorityCrlDALFactory, "update">;
certificateAuthoritySecretDAL: Pick<TCertificateAuthoritySecretDALFactory, "findOne">;
projectDAL: Pick<TProjectDALFactory, "findOne" | "updateById" | "findById" | "transaction">; projectDAL: Pick<TProjectDALFactory, "findOne" | "updateById" | "findById" | "transaction">;
kmsService: Pick<TKmsServiceFactory, "generateKmsKey" | "encrypt" | "decrypt">; kmsService: Pick<TKmsServiceFactory, "generateKmsKey" | "encrypt" | "decrypt">;
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission">; permissionService: Pick<TPermissionServiceFactory, "getProjectPermission">;
@@ -31,6 +36,8 @@ export const certificateServiceFactory = ({
certificateCertDAL, certificateCertDAL,
certificateAuthorityDAL, certificateAuthorityDAL,
certificateAuthorityCertDAL, certificateAuthorityCertDAL,
certificateAuthorityCrlDAL,
certificateAuthoritySecretDAL,
projectDAL, projectDAL,
kmsService, kmsService,
permissionService permissionService
@@ -105,6 +112,17 @@ export const certificateServiceFactory = ({
} }
); );
// rebuild CRL (TODO: move to interval-based cron job)
await rebuildCaCrl({
caId: ca.id,
certificateAuthorityDAL,
certificateAuthorityCrlDAL,
certificateAuthoritySecretDAL,
projectDAL,
certificateDAL,
kmsService
});
return { revokedAt }; return { revokedAt };
}; };
+1 -1
View File
@@ -59,7 +59,7 @@ export const getProjectKmsCertificateKeyId = async ({
kmsService kmsService
}: { }: {
projectId: string; projectId: string;
projectDAL: Pick<TProjectDALFactory, "findOne" | "updateById" | "findById" | "transaction">; projectDAL: Pick<TProjectDALFactory, "findOne" | "updateById" | "transaction">;
kmsService: Pick<TKmsServiceFactory, "generateKmsKey">; kmsService: Pick<TKmsServiceFactory, "generateKmsKey">;
}) => { }) => {
const keyId = await projectDAL.transaction(async (tx) => { const keyId = await projectDAL.transaction(async (tx) => {