From 931119f6ea035a81a69eee1bdffca32d82eb5912 Mon Sep 17 00:00:00 2001 From: Tuan Dang Date: Sun, 9 Jun 2024 17:46:16 -0400 Subject: [PATCH] Link cert mgmt to kms --- backend/src/@types/knex.d.ts | 14 +- ....ts => 20240607032218_certificate-mgmt.ts} | 42 ++- .../db/schemas/certificate-authority-certs.ts | 6 +- .../schemas/certificate-authority-secret.ts | 27 ++ .../db/schemas/certificate-authority-sk.ts | 23 -- backend/src/db/schemas/certificate-certs.ts | 6 +- backend/src/db/schemas/index.ts | 2 +- backend/src/db/schemas/models.ts | 2 +- backend/src/db/schemas/projects.ts | 3 +- backend/src/server/routes/index.ts | 11 +- .../certificate-authority-dal.ts | 4 +- .../certificate-authority-queue.ts | 106 ++++---- .../certificate-authority-secret-dal.ts | 10 + .../certificate-authority-service.ts | 243 +++++++++++++++--- .../certificate-authority-sk-dal.ts | 10 - .../certificate/certificate-service.ts | 30 ++- backend/src/services/kms/kms-service.ts | 21 +- backend/src/services/kms/kms-types.ts | 3 + backend/src/services/project/project-fns.ts | 44 ++++ 19 files changed, 434 insertions(+), 173 deletions(-) rename backend/src/db/migrations/{20240530163136_certificate-mgmt.ts => 20240607032218_certificate-mgmt.ts} (81%) create mode 100644 backend/src/db/schemas/certificate-authority-secret.ts delete mode 100644 backend/src/db/schemas/certificate-authority-sk.ts create mode 100644 backend/src/services/certificate-authority/certificate-authority-secret-dal.ts delete mode 100644 backend/src/services/certificate-authority/certificate-authority-sk-dal.ts diff --git a/backend/src/@types/knex.d.ts b/backend/src/@types/knex.d.ts index 1b242b29e..e04ef8a28 100644 --- a/backend/src/@types/knex.d.ts +++ b/backend/src/@types/knex.d.ts @@ -41,9 +41,9 @@ import { TCertificateAuthorityCrl, TCertificateAuthorityCrlInsert, TCertificateAuthorityCrlUpdate, - TCertificateAuthoritySk, - TCertificateAuthoritySkInsert, - TCertificateAuthoritySkUpdate, + TCertificateAuthoritySecret, + TCertificateAuthoritySecretInsert, + TCertificateAuthoritySecretUpdate, TCertificateCerts, TCertificateCertsInsert, TCertificateCertsUpdate, @@ -288,10 +288,10 @@ declare module "knex/types/tables" { TCertificateAuthorityCertsInsert, TCertificateAuthorityCertsUpdate >; - [TableName.CertificateAuthoritySk]: Knex.CompositeTableType< - TCertificateAuthoritySk, - TCertificateAuthoritySkInsert, - TCertificateAuthoritySkUpdate + [TableName.CertificateAuthoritySecret]: Knex.CompositeTableType< + TCertificateAuthoritySecret, + TCertificateAuthoritySecretInsert, + TCertificateAuthoritySecretUpdate >; [TableName.CertificateAuthorityCrl]: Knex.CompositeTableType< TCertificateAuthorityCrl, diff --git a/backend/src/db/migrations/20240530163136_certificate-mgmt.ts b/backend/src/db/migrations/20240607032218_certificate-mgmt.ts similarity index 81% rename from backend/src/db/migrations/20240530163136_certificate-mgmt.ts rename to backend/src/db/migrations/20240607032218_certificate-mgmt.ts index f97549066..51cc4f923 100644 --- a/backend/src/db/migrations/20240530163136_certificate-mgmt.ts +++ b/backend/src/db/migrations/20240607032218_certificate-mgmt.ts @@ -4,6 +4,16 @@ import { TableName } from "../schemas"; import { createOnUpdateTrigger, dropOnUpdateTrigger } from "../utils"; export async function up(knex: Knex): Promise { + if (await knex.schema.hasTable(TableName.Project)) { + const doesProjectCertificateKeyIdExist = await knex.schema.hasColumn(TableName.Project, "kmsCertificateKeyId"); + await knex.schema.alterTable(TableName.Project, (t) => { + if (!doesProjectCertificateKeyIdExist) { + t.uuid("kmsCertificateKeyId").nullable(); + t.foreign("kmsCertificateKeyId").references("id").inTable(TableName.KmsKey); + } + }); + } + if (!(await knex.schema.hasTable(TableName.CertificateAuthority))) { await knex.schema.createTable(TableName.CertificateAuthority, (t) => { t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid()); @@ -35,22 +45,20 @@ export async function up(knex: Knex): Promise { await knex.schema.createTable(TableName.CertificateAuthorityCert, (t) => { t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid()); t.timestamps(true, true, true); - t.uuid("caId").notNullable().unique(); // TODO: consider that cert can be rotated so may be multiple / non-unique + t.uuid("caId").notNullable().unique(); t.foreign("caId").references("id").inTable(TableName.CertificateAuthority).onDelete("CASCADE"); - t.text("certificate").notNullable(); // TODO: encrypt - t.text("certificateChain").notNullable(); // TODO: encrypt + t.binary("encryptedCertificate").notNullable(); + t.binary("encryptedCertificateChain").notNullable(); }); } - // TODO: consider renaming this to CertificateAuthoritySecret - if (!(await knex.schema.hasTable(TableName.CertificateAuthoritySk))) { - await knex.schema.createTable(TableName.CertificateAuthoritySk, (t) => { + if (!(await knex.schema.hasTable(TableName.CertificateAuthoritySecret))) { + await knex.schema.createTable(TableName.CertificateAuthoritySecret, (t) => { t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid()); t.timestamps(true, true, true); t.uuid("caId").notNullable().unique(); t.foreign("caId").references("id").inTable(TableName.CertificateAuthority).onDelete("CASCADE"); - t.text("pk").notNullable(); // TODO: encrypt - t.text("sk").notNullable(); // TODO: encrypt + t.binary("encryptedPrivateKey").notNullable(); }); } @@ -89,19 +97,27 @@ export async function up(knex: Knex): Promise { t.timestamps(true, true, true); t.uuid("certId").notNullable().unique(); t.foreign("certId").references("id").inTable(TableName.Certificate).onDelete("CASCADE"); - t.text("certificate").notNullable(); // TODO: encrypt - t.text("certificateChain").notNullable(); // TODO: encrypt + t.binary("encryptedCertificate").notNullable(); + t.binary("encryptedCertificateChain").notNullable(); }); } await createOnUpdateTrigger(knex, TableName.CertificateAuthority); await createOnUpdateTrigger(knex, TableName.CertificateAuthorityCert); - await createOnUpdateTrigger(knex, TableName.CertificateAuthoritySk); + await createOnUpdateTrigger(knex, TableName.CertificateAuthoritySecret); await createOnUpdateTrigger(knex, TableName.Certificate); await createOnUpdateTrigger(knex, TableName.CertificateCert); } export async function down(knex: Knex): Promise { + // project + if (await knex.schema.hasTable(TableName.Project)) { + const doesProjectCertificateKeyIdExist = await knex.schema.hasColumn(TableName.Project, "kmsCertificateKeyId"); + await knex.schema.alterTable(TableName.Project, (t) => { + if (doesProjectCertificateKeyIdExist) t.dropColumn("kmsCertificateKeyId"); + }); + } + // certificates await knex.schema.dropTableIfExists(TableName.CertificateCert); await dropOnUpdateTrigger(knex, TableName.CertificateCert); @@ -110,8 +126,8 @@ export async function down(knex: Knex): Promise { await dropOnUpdateTrigger(knex, TableName.Certificate); // certificate authorities - await knex.schema.dropTableIfExists(TableName.CertificateAuthoritySk); - await dropOnUpdateTrigger(knex, TableName.CertificateAuthoritySk); + await knex.schema.dropTableIfExists(TableName.CertificateAuthoritySecret); + await dropOnUpdateTrigger(knex, TableName.CertificateAuthoritySecret); await knex.schema.dropTableIfExists(TableName.CertificateAuthorityCrl); await dropOnUpdateTrigger(knex, TableName.CertificateAuthorityCrl); diff --git a/backend/src/db/schemas/certificate-authority-certs.ts b/backend/src/db/schemas/certificate-authority-certs.ts index ec3d6b8e1..96ad54f00 100644 --- a/backend/src/db/schemas/certificate-authority-certs.ts +++ b/backend/src/db/schemas/certificate-authority-certs.ts @@ -5,6 +5,8 @@ import { z } from "zod"; +import { zodBuffer } from "@app/lib/zod"; + import { TImmutableDBKeys } from "./models"; export const CertificateAuthorityCertsSchema = z.object({ @@ -12,8 +14,8 @@ export const CertificateAuthorityCertsSchema = z.object({ createdAt: z.date(), updatedAt: z.date(), caId: z.string().uuid(), - certificate: z.string(), - certificateChain: z.string() + encryptedCertificate: zodBuffer, + encryptedCertificateChain: zodBuffer }); export type TCertificateAuthorityCerts = z.infer; diff --git a/backend/src/db/schemas/certificate-authority-secret.ts b/backend/src/db/schemas/certificate-authority-secret.ts new file mode 100644 index 000000000..36ab1c506 --- /dev/null +++ b/backend/src/db/schemas/certificate-authority-secret.ts @@ -0,0 +1,27 @@ +// Code generated by automation script, DO NOT EDIT. +// Automated by pulling database and generating zod schema +// To update. Just run npm run generate:schema +// Written by akhilmhdh. + +import { z } from "zod"; + +import { zodBuffer } from "@app/lib/zod"; + +import { TImmutableDBKeys } from "./models"; + +export const CertificateAuthoritySecretSchema = z.object({ + id: z.string().uuid(), + createdAt: z.date(), + updatedAt: z.date(), + caId: z.string().uuid(), + encryptedPrivateKey: zodBuffer +}); + +export type TCertificateAuthoritySecret = z.infer; +export type TCertificateAuthoritySecretInsert = Omit< + z.input, + TImmutableDBKeys +>; +export type TCertificateAuthoritySecretUpdate = Partial< + Omit, TImmutableDBKeys> +>; diff --git a/backend/src/db/schemas/certificate-authority-sk.ts b/backend/src/db/schemas/certificate-authority-sk.ts deleted file mode 100644 index b3eca38e2..000000000 --- a/backend/src/db/schemas/certificate-authority-sk.ts +++ /dev/null @@ -1,23 +0,0 @@ -// Code generated by automation script, DO NOT EDIT. -// Automated by pulling database and generating zod schema -// To update. Just run npm run generate:schema -// Written by akhilmhdh. - -import { z } from "zod"; - -import { TImmutableDBKeys } from "./models"; - -export const CertificateAuthoritySkSchema = z.object({ - id: z.string().uuid(), - createdAt: z.date(), - updatedAt: z.date(), - caId: z.string().uuid(), - pk: z.string(), - sk: z.string() -}); - -export type TCertificateAuthoritySk = z.infer; -export type TCertificateAuthoritySkInsert = Omit, TImmutableDBKeys>; -export type TCertificateAuthoritySkUpdate = Partial< - Omit, TImmutableDBKeys> ->; diff --git a/backend/src/db/schemas/certificate-certs.ts b/backend/src/db/schemas/certificate-certs.ts index 213cdbf61..e30fb04ff 100644 --- a/backend/src/db/schemas/certificate-certs.ts +++ b/backend/src/db/schemas/certificate-certs.ts @@ -5,6 +5,8 @@ import { z } from "zod"; +import { zodBuffer } from "@app/lib/zod"; + import { TImmutableDBKeys } from "./models"; export const CertificateCertsSchema = z.object({ @@ -12,8 +14,8 @@ export const CertificateCertsSchema = z.object({ createdAt: z.date(), updatedAt: z.date(), certId: z.string().uuid(), - certificate: z.string(), - certificateChain: z.string() + encryptedCertificate: zodBuffer, + encryptedCertificateChain: zodBuffer }); export type TCertificateCerts = z.infer; diff --git a/backend/src/db/schemas/index.ts b/backend/src/db/schemas/index.ts index 14ac6cc24..d8c5e4eca 100644 --- a/backend/src/db/schemas/index.ts +++ b/backend/src/db/schemas/index.ts @@ -11,7 +11,7 @@ export * from "./backup-private-key"; export * from "./certificate-authorities"; export * from "./certificate-authority-certs"; export * from "./certificate-authority-crl"; -export * from "./certificate-authority-sk"; +export * from "./certificate-authority-secret"; export * from "./certificate-certs"; export * from "./certificate-secrets"; export * from "./certificates"; diff --git a/backend/src/db/schemas/models.ts b/backend/src/db/schemas/models.ts index 642cf8b88..de478755a 100644 --- a/backend/src/db/schemas/models.ts +++ b/backend/src/db/schemas/models.ts @@ -4,7 +4,7 @@ export enum TableName { Users = "users", CertificateAuthority = "certificate_authorities", CertificateAuthorityCert = "certificate_authority_certs", - CertificateAuthoritySk = "certificate_authority_sk", + CertificateAuthoritySecret = "certificate_authority_secret", CertificateAuthorityCrl = "certificate_authority_crl", Certificate = "certificates", CertificateCert = "certificate_certs", diff --git a/backend/src/db/schemas/projects.ts b/backend/src/db/schemas/projects.ts index 3965e24c0..ea85e28f7 100644 --- a/backend/src/db/schemas/projects.ts +++ b/backend/src/db/schemas/projects.ts @@ -16,7 +16,8 @@ export const ProjectsSchema = z.object({ createdAt: z.date(), updatedAt: z.date(), version: z.number().default(1), - upgradeStatus: z.string().nullable().optional() + upgradeStatus: z.string().nullable().optional(), + kmsCertificateKeyId: z.string().uuid().nullable().optional() }); export type TProjects = z.infer; diff --git a/backend/src/server/routes/index.ts b/backend/src/server/routes/index.ts index 64d603286..ab5bfb3ad 100644 --- a/backend/src/server/routes/index.ts +++ b/backend/src/server/routes/index.ts @@ -78,8 +78,8 @@ import { certificateAuthorityCertDALFactory } from "@app/services/certificate-au import { certificateAuthorityCrlDALFactory } from "@app/services/certificate-authority/certificate-authority-crl-dal"; import { certificateAuthorityDALFactory } from "@app/services/certificate-authority/certificate-authority-dal"; import { certificateAuthorityQueueFactory } from "@app/services/certificate-authority/certificate-authority-queue"; +import { certificateAuthoritySecretDALFactory } from "@app/services/certificate-authority/certificate-authority-secret-dal"; import { certificateAuthorityServiceFactory } from "@app/services/certificate-authority/certificate-authority-service"; -import { certificateAuthoritySkDALFactory } from "@app/services/certificate-authority/certificate-authority-sk-dal"; import { groupProjectDALFactory } from "@app/services/group-project/group-project-dal"; import { groupProjectMembershipRoleDALFactory } from "@app/services/group-project/group-project-membership-role-dal"; import { groupProjectServiceFactory } from "@app/services/group-project/group-project-service"; @@ -517,7 +517,7 @@ export const registerRoutes = async ( const certificateAuthorityDAL = certificateAuthorityDALFactory(db); const certificateAuthorityCertDAL = certificateAuthorityCertDALFactory(db); - const certificateAuthoritySkDAL = certificateAuthoritySkDALFactory(db); + const certificateAuthoritySecretDAL = certificateAuthoritySecretDALFactory(db); const certificateAuthorityCrlDAL = certificateAuthorityCrlDALFactory(db); const certificateDAL = certificateDALFactory(db); @@ -527,13 +527,15 @@ export const registerRoutes = async ( certificateDAL, certificateCertDAL, certificateAuthorityDAL, + projectDAL, + kmsService, permissionService }); const certificateAuthorityQueue = certificateAuthorityQueueFactory({ certificateAuthorityCrlDAL, certificateAuthorityDAL, - certificateAuthoritySkDAL, + certificateAuthoritySecretDAL, certificateDAL, queueService }); @@ -541,12 +543,13 @@ export const registerRoutes = async ( const certificateAuthorityService = certificateAuthorityServiceFactory({ certificateAuthorityDAL, certificateAuthorityCertDAL, - certificateAuthoritySkDAL, + certificateAuthoritySecretDAL, certificateAuthorityCrlDAL, certificateAuthorityQueue, certificateDAL, certificateCertDAL, projectDAL, + kmsService, permissionService }); diff --git a/backend/src/services/certificate-authority/certificate-authority-dal.ts b/backend/src/services/certificate-authority/certificate-authority-dal.ts index 3ad70bbdc..2ec58afc1 100644 --- a/backend/src/services/certificate-authority/certificate-authority-dal.ts +++ b/backend/src/services/certificate-authority/certificate-authority-dal.ts @@ -13,7 +13,7 @@ export const certificateAuthorityDALFactory = (db: TDbClient) => { const result: { caId: string; parentCaId?: string; - certificate: string; + certificate: Buffer; }[] = await db .withRecursive("cte", (cte) => { void cte @@ -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: string[] = result.map((row) => row.certificate); + const certChain: Buffer[] = result.map((row) => row.certificate); 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 231314656..5e270fd23 100644 --- a/backend/src/services/certificate-authority/certificate-authority-queue.ts +++ b/backend/src/services/certificate-authority/certificate-authority-queue.ts @@ -1,35 +1,35 @@ -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 { 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 { TCertificateAuthoritySkDALFactory } from "./certificate-authority-sk-dal"; +// import { keyAlgorithmToAlgCfg } from "./certificate-authority-fns"; +import { TCertificateAuthoritySecretDALFactory } from "./certificate-authority-secret-dal"; import { TRotateCaCrlTriggerDTO } from "./certificate-authority-types"; type TCertificateAuthorityQueueFactoryDep = { // TODO: Pick certificateAuthorityDAL: TCertificateAuthorityDALFactory; certificateAuthorityCrlDAL: TCertificateAuthorityCrlDALFactory; - certificateAuthoritySkDAL: TCertificateAuthoritySkDALFactory; + certificateAuthoritySecretDAL: TCertificateAuthoritySecretDALFactory; certificateDAL: TCertificateDALFactory; queueService: TQueueServiceFactory; }; export type TCertificateAuthorityQueueFactory = ReturnType; export const certificateAuthorityQueueFactory = ({ - certificateAuthorityCrlDAL, - certificateAuthorityDAL, - certificateAuthoritySkDAL, - certificateDAL, + // certificateAuthorityCrlDAL, + // certificateAuthorityDAL, + // certificateAuthoritySecretDAL, + // certificateDAL, queueService }: TCertificateAuthorityQueueFactoryDep) => { // TODO 1: auto-periodic rotation @@ -64,55 +64,55 @@ 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 certificateAuthoritySkDAL.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 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 revokedCerts = await certificateDAL.find({ - caId: ca.id, - status: CertStatus.REVOKED - }); + // 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 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 base64crl = crl.toString("base64"); - const crlPem = `-----BEGIN X509 CRL-----\n${base64crl.match(/.{1,64}/g)?.join("\n")}\n-----END X509 CRL-----`; + // const base64crl = crl.toString("base64"); + // const crlPem = `-----BEGIN X509 CRL-----\n${base64crl.match(/.{1,64}/g)?.join("\n")}\n-----END X509 CRL-----`; - await certificateAuthorityCrlDAL.update( - { - caId: ca.id - }, - { - crl: crlPem // TODO: encrypt - } - ); - }); + // await certificateAuthorityCrlDAL.update( + // { + // caId: ca.id + // }, + // { + // crl: crlPem // TODO: encrypt + // } + // ); + // }); 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-secret-dal.ts b/backend/src/services/certificate-authority/certificate-authority-secret-dal.ts new file mode 100644 index 000000000..2ade72e7e --- /dev/null +++ b/backend/src/services/certificate-authority/certificate-authority-secret-dal.ts @@ -0,0 +1,10 @@ +import { TDbClient } from "@app/db"; +import { TableName } from "@app/db/schemas"; +import { ormify } from "@app/lib/knex"; + +export type TCertificateAuthoritySecretDALFactory = ReturnType; + +export const certificateAuthoritySecretDALFactory = (db: TDbClient) => { + const caSecretOrm = ormify(db, TableName.CertificateAuthoritySecret); + return caSecretOrm; +}; diff --git a/backend/src/services/certificate-authority/certificate-authority-service.ts b/backend/src/services/certificate-authority/certificate-authority-service.ts index ca72c2656..748df8fe4 100644 --- a/backend/src/services/certificate-authority/certificate-authority-service.ts +++ b/backend/src/services/certificate-authority/certificate-authority-service.ts @@ -8,7 +8,9 @@ import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services import { BadRequestError } from "@app/lib/errors"; import { TCertificateCertDALFactory } from "@app/services/certificate/certificate-cert-dal"; 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 { getProjectKmsCertificateKeyId } from "@app/services/project/project-fns"; import { CertKeyAlgorithm, CertStatus } from "../certificate/certificate-types"; import { TCertificateAuthorityCertDALFactory } from "./certificate-authority-cert-dal"; @@ -16,7 +18,7 @@ import { TCertificateAuthorityCrlDALFactory } from "./certificate-authority-crl- import { TCertificateAuthorityDALFactory } from "./certificate-authority-dal"; import { createDistinguishedName, keyAlgorithmToAlgCfg } from "./certificate-authority-fns"; import { TCertificateAuthorityQueueFactory } from "./certificate-authority-queue"; -import { TCertificateAuthoritySkDALFactory } from "./certificate-authority-sk-dal"; +import { TCertificateAuthoritySecretDALFactory } from "./certificate-authority-secret-dal"; import { CaStatus, CaType, @@ -39,25 +41,29 @@ type TCertificateAuthorityServiceFactoryDep = { "transaction" | "create" | "findById" | "updateById" | "deleteById" | "findOne" | "buildCertificateChain" >; certificateAuthorityCertDAL: Pick; - certificateAuthoritySkDAL: Pick; + certificateAuthoritySecretDAL: Pick; certificateAuthorityCrlDAL: Pick; certificateAuthorityQueue: TCertificateAuthorityQueueFactory; // TODO: Pick certificateDAL: Pick; certificateCertDAL: Pick; - projectDAL: Pick; + projectDAL: Pick; + kmsService: Pick; permissionService: Pick; }; export type TCertificateAuthorityServiceFactory = ReturnType; +// TODO: reconsider build cert chain due to imported chains + export const certificateAuthorityServiceFactory = ({ certificateAuthorityDAL, certificateAuthorityCertDAL, - certificateAuthoritySkDAL, + certificateAuthoritySecretDAL, certificateAuthorityCrlDAL, certificateDAL, certificateCertDAL, projectDAL, + kmsService, permissionService }: TCertificateAuthorityServiceFactoryDep) => { /** @@ -109,12 +115,6 @@ export const certificateAuthorityServiceFactory = ({ const alg = keyAlgorithmToAlgCfg(keyAlgorithm); const keys = await crypto.subtle.generateKey(alg, true, ["sign", "verify"]); - // https://nodejs.org/api/crypto.html#static-method-keyobjectfromkey - const skObj = KeyObject.from(keys.privateKey); - const sk = skObj.export({ format: "pem", type: "pkcs8" }) as string; - const pkObj = KeyObject.from(keys.publicKey); - const pk = pkObj.export({ format: "pem", type: "spki" }) as string; - const newCa = await certificateAuthorityDAL.transaction(async (tx) => { const notBeforeDate = notBefore ? new Date(notBefore) : new Date(); @@ -149,6 +149,12 @@ export const certificateAuthorityServiceFactory = ({ // TODO: create CRL + const keyId = await getProjectKmsCertificateKeyId({ + projectId: project.id, + projectDAL, + kmsService + }); + if (type === CaType.ROOT) { // note: self-signed cert only applicable for root CA @@ -167,12 +173,22 @@ export const certificateAuthorityServiceFactory = ({ await x509.SubjectKeyIdentifierExtension.create(keys.publicKey) ] }); - const certificate = cert.toString("pem"); + + const { cipherTextBlob: encryptedCertificate } = await kmsService.encrypt({ + kmsId: keyId, + plainText: Buffer.from(new Uint8Array(cert.rawData)) + }); + + const { cipherTextBlob: encryptedCertificateChain } = await kmsService.encrypt({ + kmsId: keyId, + plainText: Buffer.alloc(0) + }); + await certificateAuthorityCertDAL.create( { caId: ca.id, - certificate, // TODO: encrypt - certificateChain: "" // TODO: encrypt + encryptedCertificate, + encryptedCertificateChain }, tx ); @@ -187,11 +203,21 @@ export const certificateAuthorityServiceFactory = ({ ); } - await certificateAuthoritySkDAL.create( + // https://nodejs.org/api/crypto.html#static-method-keyobjectfromkey + const skObj = KeyObject.from(keys.privateKey); + + const { cipherTextBlob: encryptedPrivateKey } = await kmsService.encrypt({ + kmsId: keyId, + plainText: skObj.export({ + type: "pkcs8", + format: "der" + }) + }); + + await certificateAuthoritySecretDAL.create( { caId: ca.id, - pk, // TODO: encrypt - sk // TODO: encrypt + encryptedPrivateKey }, tx ); @@ -290,16 +316,27 @@ export const certificateAuthorityServiceFactory = ({ const caCert = await certificateAuthorityCertDAL.findOne({ caId: ca.id }); if (caCert) throw new BadRequestError({ message: "CA already has a certificate installed" }); - const caKeys = await certificateAuthoritySkDAL.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 pkObj = crypto.createPublicKey({ key: caKeys.pk, format: "pem", type: "spki" }); + const keyId = await getProjectKmsCertificateKeyId({ + projectId: ca.projectId, + projectDAL, + kmsService + }); + const privateKey = await kmsService.decrypt({ + kmsId: keyId, + cipherTextBlob: caKeys.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 pkObj = crypto.createPublicKey(skObj); + const pk = await crypto.subtle.importKey("spki", pkObj.export({ format: "der", type: "spki" }), alg, true, [ "verify" ]); @@ -344,11 +381,28 @@ export const certificateAuthorityServiceFactory = ({ ); const caCert = await certificateAuthorityCertDAL.findOne({ caId: ca.id }); - const certObj = new x509.X509Certificate(caCert.certificate); + + const keyId = await getProjectKmsCertificateKeyId({ + projectId: ca.projectId, + projectDAL, + kmsService + }); + + const decryptedCaCert = await kmsService.decrypt({ + kmsId: keyId, + cipherTextBlob: caCert.encryptedCertificate + }); + + const certObj = new x509.X509Certificate(decryptedCaCert); + + const decryptedChain = await kmsService.decrypt({ + kmsId: keyId, + cipherTextBlob: caCert.encryptedCertificateChain + }); return { - certificate: caCert.certificate, - certificateChain: caCert.certificateChain, + certificate: certObj.toString("pem"), + certificateChain: decryptedChain.toString("utf-8"), serialNumber: certObj.serialNumber }; }; @@ -388,15 +442,31 @@ export const certificateAuthorityServiceFactory = ({ const alg = keyAlgorithmToAlgCfg(ca.keyAlgorithm as CertKeyAlgorithm); - const caCert = await certificateAuthorityCertDAL.findOne({ caId: ca.id }); - const caKeys = await certificateAuthoritySkDAL.findOne({ caId: ca.id }); + const keyId = await getProjectKmsCertificateKeyId({ + projectId: ca.projectId, + projectDAL, + kmsService + }); - const skObj = crypto.createPrivateKey({ key: caKeys.sk, format: "pem", type: "pkcs8" }); + const caCert = await certificateAuthorityCertDAL.findOne({ caId: ca.id }); + const caKeys = await certificateAuthoritySecretDAL.findOne({ caId: ca.id }); + + const privateKey = await kmsService.decrypt({ + kmsId: keyId, + cipherTextBlob: caKeys.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 certObj = new x509.X509Certificate(caCert.certificate); + const decryptedCaCert = await kmsService.decrypt({ + kmsId: keyId, + cipherTextBlob: caCert.encryptedCertificate + }); + + const certObj = new x509.X509Certificate(decryptedCaCert); const csrObj = new x509.Pkcs10CertificateRequest(csr); // check path length constraint @@ -455,11 +525,22 @@ export const certificateAuthorityServiceFactory = ({ }); const chain = await certificateAuthorityDAL.buildCertificateChain(caId); + const decryptedChain = await Promise.all( + chain.map(async (c) => { + const decryptedCaChainCert = await kmsService.decrypt({ + kmsId: keyId, + cipherTextBlob: c + }); + + const chainCertObj = new x509.X509Certificate(decryptedCaChainCert); + return chainCertObj.toString("pem"); + }) + ); return { certificate: intermediateCert.toString("pem"), - issuingCaCertificate: caCert.certificate, - certificateChain: chain.join("\n"), + issuingCaCertificate: certObj.toString("pem"), + certificateChain: decryptedChain.join("\n"), serialNumber: intermediateCert.serialNumber }; }; @@ -520,12 +601,28 @@ export const certificateAuthorityServiceFactory = ({ dn: parentCertSubject }); + const keyId = await getProjectKmsCertificateKeyId({ + projectId: ca.projectId, + projectDAL, + kmsService + }); + + const { cipherTextBlob: encryptedCertificate } = await kmsService.encrypt({ + kmsId: keyId, + plainText: Buffer.from(new Uint8Array(certObj.rawData)) + }); + + const { cipherTextBlob: encryptedCertificateChain } = await kmsService.encrypt({ + kmsId: keyId, + plainText: Buffer.from(certificateChain) + }); + await certificateAuthorityCertDAL.transaction(async (tx) => { await certificateAuthorityCertDAL.create( { caId: ca.id, - certificate, // TODO: encrypt - certificateChain // TODO: encrypt + encryptedCertificate, + encryptedCertificateChain }, tx ); @@ -569,18 +666,34 @@ export const certificateAuthorityServiceFactory = ({ ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Create, ProjectPermissionSub.Certificates); + if (ca.status === CaStatus.DISABLED) throw new BadRequestError({ message: "CA is disabled" }); + const caCert = await certificateAuthorityCertDAL.findOne({ caId: ca.id }); if (!caCert) throw new BadRequestError({ message: "CA does not have a certificate installed" }); - if (ca.status === CaStatus.DISABLED) throw new BadRequestError({ message: "CA is disabled" }); + const keyId = await getProjectKmsCertificateKeyId({ + projectId: ca.projectId, + projectDAL, + kmsService + }); - const caCertObj = new x509.X509Certificate(caCert.certificate); + const decryptedCaCert = await kmsService.decrypt({ + kmsId: keyId, + cipherTextBlob: caCert.encryptedCertificate + }); + + const caCertObj = new x509.X509Certificate(decryptedCaCert); const alg = keyAlgorithmToAlgCfg(ca.keyAlgorithm as CertKeyAlgorithm); - const caKeys = await certificateAuthoritySkDAL.findOne({ caId: ca.id }); + const caKeys = await certificateAuthoritySecretDAL.findOne({ caId: ca.id }); - const caSkObj = crypto.createPrivateKey({ key: caKeys.sk, format: "pem", type: "pkcs8" }); + const privateKey = await kmsService.decrypt({ + kmsId: keyId, + cipherTextBlob: caKeys.encryptedPrivateKey + }); + + const caSkObj = crypto.createPrivateKey({ key: privateKey, format: "der", type: "pkcs8" }); const caSk = await crypto.subtle.importKey("pkcs8", caSkObj.export({ format: "der", type: "pkcs8" }), alg, true, [ "sign" ]); @@ -646,6 +759,28 @@ export const certificateAuthorityServiceFactory = ({ const chain = await certificateAuthorityDAL.buildCertificateChain(caId); + const { cipherTextBlob: encryptedCertificate } = await kmsService.encrypt({ + kmsId: keyId, + plainText: Buffer.from(new Uint8Array(leafCert.rawData)) + }); + + const decryptedChain = await Promise.all( + chain.map(async (c) => { + const decryptedCaChainCert = await kmsService.decrypt({ + kmsId: keyId, + cipherTextBlob: c + }); + + const certObj = new x509.X509Certificate(decryptedCaChainCert); + return certObj.toString("pem"); + }) + ); + + const { cipherTextBlob: encryptedCertificateChain } = await kmsService.encrypt({ + kmsId: keyId, + plainText: Buffer.from(decryptedChain.join("\n")) + }); + await certificateDAL.transaction(async (tx) => { const cert = await certificateDAL.create( { @@ -662,8 +797,8 @@ export const certificateAuthorityServiceFactory = ({ await certificateCertDAL.create( { certId: cert.id, - certificate: leafCert.toString("pem"), // TODO: encrypt - certificateChain: chain.join("\n") // TODO: encrypt + encryptedCertificate, + encryptedCertificateChain }, tx ); @@ -673,8 +808,8 @@ export const certificateAuthorityServiceFactory = ({ return { certificate: leafCert.toString("pem"), - certificateChain: chain.join("\n"), - issuingCaCertificate: caCert.certificate, + certificateChain: decryptedChain.join("\n"), + issuingCaCertificate: caCertObj.toString("pem"), privateKey: skLeaf, serialNumber }; @@ -700,10 +835,22 @@ export const certificateAuthorityServiceFactory = ({ ProjectPermissionSub.CertificateAuthorities ); - const caKeys = await certificateAuthoritySkDAL.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 keyId = await getProjectKmsCertificateKeyId({ + projectId: ca.projectId, + projectDAL, + kmsService + }); + + const privateKey = await kmsService.decrypt({ + kmsId: keyId, + cipherTextBlob: caKeys.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" ]); @@ -755,10 +902,22 @@ export const certificateAuthorityServiceFactory = ({ ProjectPermissionSub.CertificateAuthorities ); - const caKeys = await certificateAuthoritySkDAL.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 keyId = await getProjectKmsCertificateKeyId({ + projectId: ca.projectId, + projectDAL, + kmsService + }); + + const privateKey = await kmsService.decrypt({ + kmsId: keyId, + cipherTextBlob: caKeys.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" ]); diff --git a/backend/src/services/certificate-authority/certificate-authority-sk-dal.ts b/backend/src/services/certificate-authority/certificate-authority-sk-dal.ts deleted file mode 100644 index 6c9333243..000000000 --- a/backend/src/services/certificate-authority/certificate-authority-sk-dal.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { TDbClient } from "@app/db"; -import { TableName } from "@app/db/schemas"; -import { ormify } from "@app/lib/knex"; - -export type TCertificateAuthoritySkDALFactory = ReturnType; - -export const certificateAuthoritySkDALFactory = (db: TDbClient) => { - const caSkOrm = ormify(db, TableName.CertificateAuthoritySk); - return caSkOrm; -}; diff --git a/backend/src/services/certificate/certificate-service.ts b/backend/src/services/certificate/certificate-service.ts index 50baaa1bc..992b2296d 100644 --- a/backend/src/services/certificate/certificate-service.ts +++ b/backend/src/services/certificate/certificate-service.ts @@ -6,6 +6,9 @@ import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services import { TCertificateCertDALFactory } from "@app/services/certificate/certificate-cert-dal"; import { TCertificateDALFactory } from "@app/services/certificate/certificate-dal"; import { TCertificateAuthorityDALFactory } from "@app/services/certificate-authority/certificate-authority-dal"; +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 { revocationReasonToCrlCode } from "./certificate-fns"; import { CertStatus, TDeleteCertDTO, TGetCertCertDTO, TGetCertDTO, TRevokeCertDTO } from "./certificate-types"; @@ -14,6 +17,8 @@ type TCertificateServiceFactoryDep = { certificateDAL: Pick; certificateCertDAL: Pick; certificateAuthorityDAL: Pick; + projectDAL: Pick; + kmsService: Pick; permissionService: Pick; }; @@ -23,6 +28,8 @@ export const certificateServiceFactory = ({ certificateDAL, certificateCertDAL, certificateAuthorityDAL, + projectDAL, + kmsService, permissionService }: TCertificateServiceFactoryDep) => { const getCert = async ({ serialNumber, actorId, actorAuthMethod, actor, actorOrgId }: TGetCertDTO) => { @@ -113,11 +120,28 @@ export const certificateServiceFactory = ({ ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Certificates); const certCert = await certificateCertDAL.findOne({ certId: cert.id }); - const certObj = new x509.X509Certificate(certCert.certificate); + + const keyId = await getProjectKmsCertificateKeyId({ + projectId: ca.projectId, + projectDAL, + kmsService + }); + + const decryptedCert = await kmsService.decrypt({ + kmsId: keyId, + cipherTextBlob: certCert.encryptedCertificate + }); + + const decryptedChain = await kmsService.decrypt({ + kmsId: keyId, + cipherTextBlob: certCert.encryptedCertificateChain + }); + + const certObj = new x509.X509Certificate(decryptedCert); return { - certificate: certCert.certificate, - certificateChain: certCert.certificateChain, + certificate: certObj.toString("pem"), + certificateChain: decryptedChain.toString("utf-8"), serialNumber: certObj.serialNumber }; }; diff --git a/backend/src/services/kms/kms-service.ts b/backend/src/services/kms/kms-service.ts index 24d901867..10ce7edb9 100644 --- a/backend/src/services/kms/kms-service.ts +++ b/backend/src/services/kms/kms-service.ts @@ -29,19 +29,22 @@ export const kmsServiceFactory = ({ kmsDAL, kmsRootConfigDAL, keyStore }: TKmsSe let ROOT_ENCRYPTION_KEY = Buffer.alloc(0); // this is used symmetric encryption - const generateKmsKey = async ({ scopeId, scopeType, isReserved = true }: TGenerateKMSDTO) => { + const generateKmsKey = async ({ scopeId, scopeType, isReserved = true, tx }: TGenerateKMSDTO) => { const cipher = symmetricCipherService(SymmetricEncryption.AES_GCM_256); const kmsKeyMaterial = randomSecureBytes(32); const encryptedKeyMaterial = cipher.encrypt(kmsKeyMaterial, ROOT_ENCRYPTION_KEY); - const { encryptedKey, ...doc } = await kmsDAL.create({ - version: 1, - encryptedKey: encryptedKeyMaterial, - encryptionAlgorithm: SymmetricEncryption.AES_GCM_256, - isReserved, - orgId: scopeType === "org" ? scopeId : undefined, - projectId: scopeType === "project" ? scopeId : undefined - }); + const { encryptedKey, ...doc } = await kmsDAL.create( + { + version: 1, + encryptedKey: encryptedKeyMaterial, + encryptionAlgorithm: SymmetricEncryption.AES_GCM_256, + isReserved, + orgId: scopeType === "org" ? scopeId : undefined, + projectId: scopeType === "project" ? scopeId : undefined + }, + tx + ); return doc; }; diff --git a/backend/src/services/kms/kms-types.ts b/backend/src/services/kms/kms-types.ts index 96ad25f6e..63fdaf484 100644 --- a/backend/src/services/kms/kms-types.ts +++ b/backend/src/services/kms/kms-types.ts @@ -1,7 +1,10 @@ +import { Knex } from "knex"; + export type TGenerateKMSDTO = { scopeType: "project" | "org"; scopeId: string; isReserved?: boolean; + tx?: Knex; }; export type TEncryptWithKmsDTO = { diff --git a/backend/src/services/project/project-fns.ts b/backend/src/services/project/project-fns.ts index 3ac75248d..c236faf41 100644 --- a/backend/src/services/project/project-fns.ts +++ b/backend/src/services/project/project-fns.ts @@ -1,6 +1,9 @@ import crypto from "crypto"; import { decryptAsymmetric, encryptAsymmetric } from "@app/lib/crypto"; +import { BadRequestError } from "@app/lib/errors"; +import { TKmsServiceFactory } from "@app/services/kms/kms-service"; +import { TProjectDALFactory } from "@app/services/project/project-dal"; import { AddUserToWsDTO } from "./project-types"; @@ -49,3 +52,44 @@ export const createProjectKey = ({ publicKey, privateKey, plainProjectKey }: TCr return { key: encryptedProjectKey, iv: encryptedProjectKeyIv }; }; + +export const getProjectKmsCertificateKeyId = async ({ + projectId, + projectDAL, + kmsService +}: { + projectId: string; + projectDAL: Pick; + kmsService: Pick; +}) => { + const keyId = await projectDAL.transaction(async (tx) => { + const project = await projectDAL.findOne({ id: projectId }, tx); + if (!project) { + throw new BadRequestError({ message: "Project not found" }); + } + + if (!project.kmsCertificateKeyId) { + // create default kms key for certificate service + const key = await kmsService.generateKmsKey({ + scopeId: projectId, + scopeType: "project", + isReserved: true, + tx + }); + + await projectDAL.updateById( + projectId, + { + kmsCertificateKeyId: key.id + }, + tx + ); + + return key.id; + } + + return project.kmsCertificateKeyId; + }); + + return keyId; +};