diff --git a/backend/src/db/migrations/20240607032218_certificate-mgmt.ts b/backend/src/db/migrations/20240607032218_certificate-mgmt.ts index 51cc4f923..92a9f1ae6 100644 --- a/backend/src/db/migrations/20240607032218_certificate-mgmt.ts +++ b/backend/src/db/migrations/20240607032218_certificate-mgmt.ts @@ -68,7 +68,7 @@ export async function up(knex: Knex): Promise { t.timestamps(true, true, true); t.uuid("caId").notNullable().unique(); t.foreign("caId").references("id").inTable(TableName.CertificateAuthority).onDelete("CASCADE"); - t.text("crl").notNullable(); // TODO: encrypt + t.binary("encryptedCrl").notNullable(); // TODO: encrypt t.integer("ttl").notNullable(); // in minutes // TODO: consider type (crl or delta) // TODO: rebuild interval diff --git a/backend/src/db/schemas/certificate-authority-crl.ts b/backend/src/db/schemas/certificate-authority-crl.ts index 2f30d0f91..fd03789d5 100644 --- a/backend/src/db/schemas/certificate-authority-crl.ts +++ b/backend/src/db/schemas/certificate-authority-crl.ts @@ -5,6 +5,8 @@ import { z } from "zod"; +import { zodBuffer } from "@app/lib/zod"; + import { TImmutableDBKeys } from "./models"; export const CertificateAuthorityCrlSchema = z.object({ @@ -12,7 +14,7 @@ export const CertificateAuthorityCrlSchema = z.object({ createdAt: z.date(), updatedAt: z.date(), caId: z.string().uuid(), - crl: z.string(), + encryptedCrl: zodBuffer, ttl: z.number() }); diff --git a/backend/src/server/routes/index.ts b/backend/src/server/routes/index.ts index ab5bfb3ad..1b9b138b0 100644 --- a/backend/src/server/routes/index.ts +++ b/backend/src/server/routes/index.ts @@ -537,6 +537,8 @@ export const registerRoutes = async ( certificateAuthorityDAL, certificateAuthoritySecretDAL, certificateDAL, + projectDAL, + kmsService, queueService }); diff --git a/backend/src/services/certificate-authority/certificate-authority-dal.ts b/backend/src/services/certificate-authority/certificate-authority-dal.ts index 2ec58afc1..fc3b63b83 100644 --- a/backend/src/services/certificate-authority/certificate-authority-dal.ts +++ b/backend/src/services/certificate-authority/certificate-authority-dal.ts @@ -13,17 +13,17 @@ export const certificateAuthorityDALFactory = (db: TDbClient) => { const result: { caId: string; parentCaId?: string; - certificate: Buffer; + encryptedCertificate: Buffer; }[] = await db .withRecursive("cte", (cte) => { void cte - .select("ca.id as caId", "ca.parentCaId", "cert.certificate") + .select("ca.id as caId", "ca.parentCaId", "cert.encryptedCertificate") .from({ ca: TableName.CertificateAuthority }) .leftJoin({ cert: TableName.CertificateAuthorityCert }, "ca.id", "cert.caId") .where("ca.id", caId) .unionAll((builder) => { void builder - .select("ca.id as caId", "ca.parentCaId", "cert.certificate") + .select("ca.id as caId", "ca.parentCaId", "cert.encryptedCertificate") .from({ ca: TableName.CertificateAuthority }) .leftJoin({ cert: TableName.CertificateAuthorityCert }, "ca.id", "cert.caId") .innerJoin("cte", "cte.parentCaId", "ca.id"); @@ -33,7 +33,7 @@ export const certificateAuthorityDALFactory = (db: TDbClient) => { .from("cte"); // Extract certificates and reverse the order to have the root CA at the end - const certChain: Buffer[] = result.map((row) => row.certificate); + const certChain: Buffer[] = result.map((row) => row.encryptedCertificate); return certChain; } catch (error) { throw new DatabaseError({ error, name: "BuildCertificateChain" }); diff --git a/backend/src/services/certificate-authority/certificate-authority-queue.ts b/backend/src/services/certificate-authority/certificate-authority-queue.ts index 5e270fd23..1331689e4 100644 --- a/backend/src/services/certificate-authority/certificate-authority-queue.ts +++ b/backend/src/services/certificate-authority/certificate-authority-queue.ts @@ -1,17 +1,20 @@ -// import * as x509 from "@peculiar/x509"; -// import crypto from "crypto"; +import * as x509 from "@peculiar/x509"; +import crypto from "crypto"; import { getConfig } from "@app/lib/config/env"; import { daysToMillisecond, secondsToMillis } from "@app/lib/dates"; -// import { BadRequestError } from "@app/lib/errors"; +import { BadRequestError } from "@app/lib/errors"; import { logger } from "@app/lib/logger"; import { QueueJobs, QueueName, TQueueServiceFactory } from "@app/queue"; import { TCertificateDALFactory } from "@app/services/certificate/certificate-dal"; +import { CertKeyAlgorithm, CertStatus } from "@app/services/certificate/certificate-types"; +import { TKmsServiceFactory } from "@app/services/kms/kms-service"; +import { TProjectDALFactory } from "@app/services/project/project-dal"; +import { getProjectKmsCertificateKeyId } from "@app/services/project/project-fns"; -// import { CertKeyAlgorithm, CertStatus } from "@app/services/certificate/certificate-types"; import { TCertificateAuthorityCrlDALFactory } from "./certificate-authority-crl-dal"; import { TCertificateAuthorityDALFactory } from "./certificate-authority-dal"; -// import { keyAlgorithmToAlgCfg } from "./certificate-authority-fns"; +import { keyAlgorithmToAlgCfg } from "./certificate-authority-fns"; import { TCertificateAuthoritySecretDALFactory } from "./certificate-authority-secret-dal"; import { TRotateCaCrlTriggerDTO } from "./certificate-authority-types"; @@ -21,15 +24,19 @@ type TCertificateAuthorityQueueFactoryDep = { certificateAuthorityCrlDAL: TCertificateAuthorityCrlDALFactory; certificateAuthoritySecretDAL: TCertificateAuthoritySecretDALFactory; certificateDAL: TCertificateDALFactory; + projectDAL: Pick; + kmsService: Pick; queueService: TQueueServiceFactory; }; export type TCertificateAuthorityQueueFactory = ReturnType; export const certificateAuthorityQueueFactory = ({ - // certificateAuthorityCrlDAL, - // certificateAuthorityDAL, - // certificateAuthoritySecretDAL, - // certificateDAL, + certificateAuthorityCrlDAL, + certificateAuthorityDAL, + certificateAuthoritySecretDAL, + certificateDAL, + projectDAL, + kmsService, queueService }: TCertificateAuthorityQueueFactoryDep) => { // TODO 1: auto-periodic rotation @@ -64,55 +71,69 @@ export const certificateAuthorityQueueFactory = ({ ); }; - // queueService.start(QueueName.CaCrlRotation, async (job) => { - // const { caId } = job.data; - // logger.info(`secretReminderQueue.process: [secretDocument=${caId}]`); + queueService.start(QueueName.CaCrlRotation, async (job) => { + const { caId } = job.data; + logger.info(`secretReminderQueue.process: [secretDocument=${caId}]`); - // const ca = await certificateAuthorityDAL.findById(caId); - // if (!ca) throw new BadRequestError({ message: "CA not found" }); + const ca = await certificateAuthorityDAL.findById(caId); + if (!ca) throw new BadRequestError({ message: "CA not found" }); - // const caKeys = await certificateAuthoritySecretDAL.findOne({ caId: ca.id }); + const caKeys = await certificateAuthoritySecretDAL.findOne({ caId: ca.id }); - // const alg = keyAlgorithmToAlgCfg(ca.keyAlgorithm as CertKeyAlgorithm); - // const skObj = crypto.createPrivateKey({ key: caKeys.sk, format: "pem", type: "pkcs8" }); - // const sk = await crypto.subtle.importKey("pkcs8", skObj.export({ format: "der", type: "pkcs8" }), alg, true, [ - // "sign" - // ]); + const alg = keyAlgorithmToAlgCfg(ca.keyAlgorithm as CertKeyAlgorithm); - // const revokedCerts = await certificateDAL.find({ - // caId: ca.id, - // status: CertStatus.REVOKED - // }); + const keyId = await getProjectKmsCertificateKeyId({ + projectId: ca.projectId, + projectDAL, + kmsService + }); - // const crl = await x509.X509CrlGenerator.create({ - // issuer: ca.dn, - // thisUpdate: new Date(), - // nextUpdate: new Date("2025/12/12"), // TODO: depends on configured rebuild interval - // 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 privateKey = await kmsService.decrypt({ + kmsId: keyId, + cipherTextBlob: caKeys.encryptedPrivateKey + }); - // const base64crl = crl.toString("base64"); - // const crlPem = `-----BEGIN X509 CRL-----\n${base64crl.match(/.{1,64}/g)?.join("\n")}\n-----END X509 CRL-----`; + 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" + ]); - // await certificateAuthorityCrlDAL.update( - // { - // caId: ca.id - // }, - // { - // crl: crlPem // TODO: encrypt - // } - // ); - // }); + 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"), // TODO: depends on configured rebuild interval + 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 + } + ); + }); queueService.listen(QueueName.CaCrlRotation, "failed", (job, err) => { logger.error(err, "Failed to rotate CA CRL %s", job?.id); diff --git a/backend/src/services/certificate-authority/certificate-authority-service.ts b/backend/src/services/certificate-authority/certificate-authority-service.ts index 748df8fe4..f7d170e5b 100644 --- a/backend/src/services/certificate-authority/certificate-authority-service.ts +++ b/backend/src/services/certificate-authority/certificate-authority-service.ts @@ -193,10 +193,15 @@ export const certificateAuthorityServiceFactory = ({ tx ); + const { cipherTextBlob: encryptedCrl } = await kmsService.encrypt({ + kmsId: keyId, + plainText: Buffer.alloc(0) + }); + await certificateAuthorityCrlDAL.create( { caId: ca.id, - crl: "", // TODO: encrypt + encryptedCrl, ttl: 60 // in minutes }, tx @@ -944,18 +949,23 @@ export const certificateAuthorityServiceFactory = ({ signingKey: sk }); - const base64crl = crl.toString("base64"); - const crlPem = `-----BEGIN X509 CRL-----\n${base64crl.match(/.{1,64}/g)?.join("\n")}\n-----END X509 CRL-----`; + const { cipherTextBlob: encryptedCrl } = await kmsService.encrypt({ + kmsId: keyId, + plainText: Buffer.from(new Uint8Array(crl.rawData)) + }); await certificateAuthorityCrlDAL.update( { caId: ca.id }, { - crl: crlPem // TODO: encrypt + encryptedCrl } ); + const base64crl = crl.toString("base64"); + const crlPem = `-----BEGIN X509 CRL-----\n${base64crl.match(/.{1,64}/g)?.join("\n")}\n-----END X509 CRL-----`; + return { crl: crlPem };