diff --git a/backend/src/db/migrations/20240607032218_certificate-mgmt.ts b/backend/src/db/migrations/20240607032218_certificate-mgmt.ts index d4eeb60c8..3edac7a72 100644 --- a/backend/src/db/migrations/20240607032218_certificate-mgmt.ts +++ b/backend/src/db/migrations/20240607032218_certificate-mgmt.ts @@ -68,10 +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.binary("encryptedCrl").notNullable(); // TODO: encrypt - t.integer("ttl").notNullable(); // in minutes - // TODO: consider type (crl or delta) - // TODO: rebuild interval + t.binary("encryptedCrl").notNullable(); }); } diff --git a/backend/src/db/schemas/certificate-authority-crl.ts b/backend/src/db/schemas/certificate-authority-crl.ts index fd03789d5..204a0c60c 100644 --- a/backend/src/db/schemas/certificate-authority-crl.ts +++ b/backend/src/db/schemas/certificate-authority-crl.ts @@ -14,8 +14,7 @@ export const CertificateAuthorityCrlSchema = z.object({ createdAt: z.date(), updatedAt: z.date(), caId: z.string().uuid(), - encryptedCrl: zodBuffer, - ttl: z.number() + encryptedCrl: zodBuffer }); export type TCertificateAuthorityCrl = z.infer; diff --git a/backend/src/server/routes/index.ts b/backend/src/server/routes/index.ts index dd1de1385..a803c6bc0 100644 --- a/backend/src/server/routes/index.ts +++ b/backend/src/server/routes/index.ts @@ -528,6 +528,8 @@ export const registerRoutes = async ( certificateCertDAL, certificateAuthorityDAL, certificateAuthorityCertDAL, + certificateAuthorityCrlDAL, + certificateAuthoritySecretDAL, projectDAL, kmsService, permissionService diff --git a/backend/src/server/routes/v1/certificate-authority-router.ts b/backend/src/server/routes/v1/certificate-authority-router.ts index 433c02cb8..2520850d3 100644 --- a/backend/src/server/routes/v1/certificate-authority-router.ts +++ b/backend/src/server/routes/v1/certificate-authority-router.ts @@ -420,35 +420,35 @@ export const registerCaRouter = async (server: FastifyZodProvider) => { } }); - server.route({ - method: "GET", - url: "/:caId/crl/rotate", - config: { - rateLimit: writeLimit - }, - onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), - schema: { - description: "Rotate CRL of the CA", - params: z.object({ - caId: z.string().trim() - }), - response: { - 200: z.object({ - message: z.string() - }) - } - }, - handler: async (req) => { - await server.services.certificateAuthority.rotateCaCrl({ - caId: req.params.caId, - actor: req.permission.type, - actorId: req.permission.id, - actorAuthMethod: req.permission.authMethod, - actorOrgId: req.permission.orgId - }); - return { - message: "Successfully rotated CA CRL" - }; - } - }); + // server.route({ + // method: "GET", + // url: "/:caId/crl/rotate", + // config: { + // rateLimit: writeLimit + // }, + // onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), + // schema: { + // description: "Rotate CRL of the CA", + // params: z.object({ + // caId: z.string().trim() + // }), + // response: { + // 200: z.object({ + // message: z.string() + // }) + // } + // }, + // handler: async (req) => { + // await server.services.certificateAuthority.rotateCaCrl({ + // caId: req.params.caId, + // actor: req.permission.type, + // actorId: req.permission.id, + // actorAuthMethod: req.permission.authMethod, + // actorOrgId: req.permission.orgId + // }); + // return { + // message: "Successfully rotated CA CRL" + // }; + // } + // }); }; diff --git a/backend/src/services/certificate-authority/certificate-authority-fns.ts b/backend/src/services/certificate-authority/certificate-authority-fns.ts index 393729672..3b9dd7c47 100644 --- a/backend/src/services/certificate-authority/certificate-authority-fns.ts +++ b/backend/src/services/certificate-authority/certificate-authority-fns.ts @@ -1,5 +1,11 @@ -import { CertKeyAlgorithm } from "../certificate/certificate-types"; -import { TDNParts } from "./certificate-authority-types"; +import * as x509 from "@peculiar/x509"; +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) => { 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 + } + ); +}; diff --git a/backend/src/services/certificate-authority/certificate-authority-service.ts b/backend/src/services/certificate-authority/certificate-authority-service.ts index b4b0bb340..3373f8bd0 100644 --- a/backend/src/services/certificate-authority/certificate-authority-service.ts +++ b/backend/src/services/certificate-authority/certificate-authority-service.ts @@ -30,7 +30,7 @@ import { TGetCrl, TImportCertToCaDTO, TIssueCertFromCaDTO, - TRotateCrlDTO, + // TRotateCrlDTO, TSignIntermediateDTO, TUpdateCaDTO } from "./certificate-authority-types"; @@ -145,8 +145,6 @@ export const certificateAuthorityServiceFactory = ({ tx ); - // TODO: create CRL - const keyId = await getProjectKmsCertificateKeyId({ projectId: project.id, projectDAL, @@ -154,8 +152,7 @@ export const certificateAuthorityServiceFactory = ({ }); 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({ name: dn, serialNumber, @@ -190,22 +187,31 @@ export const certificateAuthorityServiceFactory = ({ }, 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 const skObj = KeyObject.from(keys.privateKey); @@ -817,9 +823,8 @@ export const certificateAuthorityServiceFactory = ({ ProjectPermissionSub.CertificateAuthorities ); - const caSecret = await certificateAuthoritySecretDAL.findOne({ caId: ca.id }); - - const alg = keyAlgorithmToAlgCfg(ca.keyAlgorithm as CertKeyAlgorithm); + const caCrl = await certificateAuthorityCrlDAL.findOne({ caId: ca.id }); + if (!caCrl) throw new BadRequestError({ message: "CRL not found" }); const keyId = await getProjectKmsCertificateKeyId({ projectId: ca.projectId, @@ -827,37 +832,12 @@ export const certificateAuthorityServiceFactory = ({ kmsService }); - const privateKey = await kmsService.decrypt({ + const decryptedCrl = await kmsService.decrypt({ kmsId: keyId, - cipherTextBlob: caSecret.encryptedPrivateKey + cipherTextBlob: caCrl.encryptedCrl }); - 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 crl = new x509.X509Crl(decryptedCrl); const base64crl = crl.toString("base64"); 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 ca = await certificateAuthorityDAL.findById(caId); - if (!ca) throw new BadRequestError({ message: "CA not found" }); + // const rotateCaCrl = async ({ caId, actorId, actorAuthMethod, actor, actorOrgId }: TRotateCrlDTO) => { + // const ca = await certificateAuthorityDAL.findById(caId); + // if (!ca) throw new BadRequestError({ message: "CA not found" }); - const { permission } = await permissionService.getProjectPermission( - actor, - actorId, - ca.projectId, - actorAuthMethod, - actorOrgId - ); + // const { permission } = await permissionService.getProjectPermission( + // actor, + // actorId, + // ca.projectId, + // actorAuthMethod, + // actorOrgId + // ); - ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionActions.Read, - ProjectPermissionSub.CertificateAuthorities - ); + // ForbiddenError.from(permission).throwUnlessCan( + // ProjectPermissionActions.Read, + // 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({ - projectId: ca.projectId, - projectDAL, - kmsService - }); + // const keyId = await getProjectKmsCertificateKeyId({ + // projectId: ca.projectId, + // projectDAL, + // kmsService + // }); - const privateKey = await kmsService.decrypt({ - kmsId: keyId, - cipherTextBlob: caSecret.encryptedPrivateKey - }); + // 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 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 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 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)) - }); + // const { cipherTextBlob: encryptedCrl } = await kmsService.encrypt({ + // kmsId: keyId, + // plainText: Buffer.from(new Uint8Array(crl.rawData)) + // }); - await certificateAuthorityCrlDAL.update( - { - caId: ca.id - }, - { - encryptedCrl - } - ); + // await certificateAuthorityCrlDAL.update( + // { + // caId: ca.id + // }, + // { + // encryptedCrl + // } + // ); - 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-----`; - return { - crl: crlPem - }; - }; + // return { + // crl: crlPem + // }; + // }; return { createCa, @@ -958,7 +938,7 @@ export const certificateAuthorityServiceFactory = ({ signIntermediate, importCertToCa, issueCertFromCa, - getCaCrl, - rotateCaCrl + getCaCrl + // rotateCaCrl }; }; diff --git a/backend/src/services/certificate-authority/certificate-authority-types.ts b/backend/src/services/certificate-authority/certificate-authority-types.ts index 8d4746d77..cfcbd061e 100644 --- a/backend/src/services/certificate-authority/certificate-authority-types.ts +++ b/backend/src/services/certificate-authority/certificate-authority-types.ts @@ -1,6 +1,12 @@ 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 { TCertificateAuthorityCrlDALFactory } from "./certificate-authority-crl-dal"; +import { TCertificateAuthorityDALFactory } from "./certificate-authority-dal"; +import { TCertificateAuthoritySecretDALFactory } from "./certificate-authority-secret-dal"; export enum CaType { ROOT = "root", @@ -88,6 +94,16 @@ export type TDNParts = { locality?: string; }; +export type TRebuildCaCrlDTO = { + caId: string; + certificateAuthorityDAL: Pick; + certificateAuthorityCrlDAL: Pick; + certificateAuthoritySecretDAL: Pick; + projectDAL: Pick; + certificateDAL: Pick; + kmsService: Pick; +}; + export type TRotateCaCrlTriggerDTO = { caId: string; rotationIntervalDays: number; diff --git a/backend/src/services/certificate/certificate-service.ts b/backend/src/services/certificate/certificate-service.ts index bac710432..ad151c529 100644 --- a/backend/src/services/certificate/certificate-service.ts +++ b/backend/src/services/certificate/certificate-service.ts @@ -6,19 +6,24 @@ 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 { 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 { TCertificateAuthoritySecretDALFactory } from "@app/services/certificate-authority/certificate-authority-secret-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 { rebuildCaCrl } from "../certificate-authority/certificate-authority-fns"; import { revocationReasonToCrlCode } from "./certificate-fns"; import { CertStatus, TDeleteCertDTO, TGetCertCertDTO, TGetCertDTO, TRevokeCertDTO } from "./certificate-types"; type TCertificateServiceFactoryDep = { - certificateDAL: Pick; + certificateDAL: Pick; certificateCertDAL: Pick; certificateAuthorityDAL: Pick; certificateAuthorityCertDAL: Pick; + certificateAuthorityCrlDAL: Pick; + certificateAuthoritySecretDAL: Pick; projectDAL: Pick; kmsService: Pick; permissionService: Pick; @@ -31,6 +36,8 @@ export const certificateServiceFactory = ({ certificateCertDAL, certificateAuthorityDAL, certificateAuthorityCertDAL, + certificateAuthorityCrlDAL, + certificateAuthoritySecretDAL, projectDAL, kmsService, 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 }; }; diff --git a/backend/src/services/project/project-fns.ts b/backend/src/services/project/project-fns.ts index c236faf41..78c7b442f 100644 --- a/backend/src/services/project/project-fns.ts +++ b/backend/src/services/project/project-fns.ts @@ -59,7 +59,7 @@ export const getProjectKmsCertificateKeyId = async ({ kmsService }: { projectId: string; - projectDAL: Pick; + projectDAL: Pick; kmsService: Pick; }) => { const keyId = await projectDAL.transaction(async (tx) => {