From 868d0345d69750ec899ec940341bc208793a6ae6 Mon Sep 17 00:00:00 2001 From: Tuan Dang Date: Mon, 10 Jun 2024 22:37:20 -0700 Subject: [PATCH] Modularized repetitive ca + chain logic --- .../certificate-authority-fns.ts | 97 ++++++++- .../certificate-authority-service.ts | 189 +++++++++--------- .../certificate-authority-types.ts | 17 ++ .../certificate/certificate-service.ts | 38 ++-- 4 files changed, 227 insertions(+), 114 deletions(-) diff --git a/backend/src/services/certificate-authority/certificate-authority-fns.ts b/backend/src/services/certificate-authority/certificate-authority-fns.ts index 3b9dd7c47..cf42a058e 100644 --- a/backend/src/services/certificate-authority/certificate-authority-fns.ts +++ b/backend/src/services/certificate-authority/certificate-authority-fns.ts @@ -5,7 +5,7 @@ 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"; +import { TDNParts, TGetCaCertChainDTO, TGetCaCredentialsDTO, TRebuildCaCrlDTO } from "./certificate-authority-types"; export const createDistinguishedName = (parts: TDNParts) => { const dnParts = []; @@ -51,6 +51,101 @@ export const keyAlgorithmToAlgCfg = (keyAlgorithm: CertKeyAlgorithm) => { } }; +/** + * Return the public and private key of CA with id [caId] + * Note: credentials are returned as crypto.webcrypto.CryptoKey + * suitable for use with @peculiar/x509 module + */ +export const getCaCredentials = async ({ + caId, + certificateAuthorityDAL, + certificateAuthoritySecretDAL, + projectDAL, + kmsService +}: TGetCaCredentialsDTO) => { + const ca = await certificateAuthorityDAL.findById(caId); + if (!ca) throw new BadRequestError({ message: "CA not found" }); + + const caSecret = await certificateAuthoritySecretDAL.findOne({ caId }); + if (!caSecret) throw new BadRequestError({ message: "CA secret not found" }); + + const keyId = await getProjectKmsCertificateKeyId({ + projectId: ca.projectId, + projectDAL, + kmsService + }); + + const decryptedPrivateKey = await kmsService.decrypt({ + kmsId: keyId, + cipherTextBlob: caSecret.encryptedPrivateKey + }); + + const alg = keyAlgorithmToAlgCfg(ca.keyAlgorithm as CertKeyAlgorithm); + const skObj = crypto.createPrivateKey({ key: decryptedPrivateKey, format: "der", type: "pkcs8" }); + const caPrivateKey = await crypto.subtle.importKey( + "pkcs8", + skObj.export({ format: "der", type: "pkcs8" }), + alg, + true, + ["sign"] + ); + + const pkObj = crypto.createPublicKey(skObj); + const caPublicKey = await crypto.subtle.importKey("spki", pkObj.export({ format: "der", type: "spki" }), alg, true, [ + "verify" + ]); + + return { + caPrivateKey, + caPublicKey + }; +}; + +/** + * Return the decrypted pem-encoded certificate and certificate chain + * for CA with id [caId]. + */ +export const getCaCertChain = async ({ + caId, + certificateAuthorityDAL, + certificateAuthorityCertDAL, + projectDAL, + kmsService +}: TGetCaCertChainDTO) => { + const ca = await certificateAuthorityDAL.findById(caId); + if (!ca) throw new BadRequestError({ message: "CA not found" }); + + const caCert = await certificateAuthorityCertDAL.findOne({ caId: ca.id }); + + const keyId = await getProjectKmsCertificateKeyId({ + projectId: ca.projectId, + projectDAL, + kmsService + }); + + const decryptedCaCert = await kmsService.decrypt({ + kmsId: keyId, + cipherTextBlob: caCert.encryptedCertificate + }); + + const caCertObj = new x509.X509Certificate(decryptedCaCert); + + const decryptedChain = await kmsService.decrypt({ + kmsId: keyId, + cipherTextBlob: caCert.encryptedCertificateChain + }); + + return { + caCert: caCertObj.toString("pem"), + caCertChain: decryptedChain.toString("utf-8"), + serialNumber: caCertObj.serialNumber + }; +}; + +/** + * Rebuilds the certificate revocation list (CRL) + * for CA with id [caId] + */ export const rebuildCaCrl = async ({ caId, certificateAuthorityDAL, diff --git a/backend/src/services/certificate-authority/certificate-authority-service.ts b/backend/src/services/certificate-authority/certificate-authority-service.ts index 3373f8bd0..32839cccc 100644 --- a/backend/src/services/certificate-authority/certificate-authority-service.ts +++ b/backend/src/services/certificate-authority/certificate-authority-service.ts @@ -16,7 +16,12 @@ import { CertKeyAlgorithm, CertStatus } from "../certificate/certificate-types"; import { TCertificateAuthorityCertDALFactory } from "./certificate-authority-cert-dal"; import { TCertificateAuthorityCrlDALFactory } from "./certificate-authority-crl-dal"; import { TCertificateAuthorityDALFactory } from "./certificate-authority-dal"; -import { createDistinguishedName, keyAlgorithmToAlgCfg } from "./certificate-authority-fns"; +import { + createDistinguishedName, + getCaCertChain, + getCaCredentials, + keyAlgorithmToAlgCfg +} from "./certificate-authority-fns"; import { TCertificateAuthorityQueueFactory } from "./certificate-authority-queue"; import { TCertificateAuthoritySecretDALFactory } from "./certificate-authority-secret-dal"; import { @@ -65,7 +70,7 @@ export const certificateAuthorityServiceFactory = ({ permissionService }: TCertificateAuthorityServiceFactoryDep) => { /** - * Generates a new root or intermediate CA + * Generates new root or intermediate CA */ const createCa = async ({ projectSlug, @@ -237,6 +242,9 @@ export const certificateAuthorityServiceFactory = ({ return newCa; }; + /** + * Return CA with id [caId] + */ const getCaById = async ({ caId, actorId, actorAuthMethod, actor, actorOrgId }: TGetCaDTO) => { const ca = await certificateAuthorityDAL.findById(caId); if (!ca) throw new BadRequestError({ message: "CA not found" }); @@ -256,6 +264,10 @@ export const certificateAuthorityServiceFactory = ({ return ca; }; + /** + * Update CA with id [caId]. + * Note: Used to enable/disable CA + */ const updateCaById = async ({ caId, status, actorId, actorAuthMethod, actor, actorOrgId }: TUpdateCaDTO) => { const ca = await certificateAuthorityDAL.findById(caId); if (!ca) throw new BadRequestError({ message: "CA not found" }); @@ -278,6 +290,9 @@ export const certificateAuthorityServiceFactory = ({ return updatedCa; }; + /** + * Delete CA with id [caId] + */ const deleteCaById = async ({ caId, actorId, actorAuthMethod, actor, actorOrgId }: TDeleteCaDTO) => { const ca = await certificateAuthorityDAL.findById(caId); if (!ca) throw new BadRequestError({ message: "CA not found" }); @@ -301,7 +316,7 @@ export const certificateAuthorityServiceFactory = ({ }; /** - * Generates a CSR for a CA + * Return certificate signing request (CSR) made with CA with id [caId] */ const getCaCsr = async ({ caId, actorId, actorAuthMethod, actor, actorOrgId }: TGetCaCsrDTO) => { const ca = await certificateAuthorityDAL.findById(caId); @@ -325,33 +340,22 @@ export const certificateAuthorityServiceFactory = ({ const caCert = await certificateAuthorityCertDAL.findOne({ caId: ca.id }); if (caCert) throw new BadRequestError({ message: "CA already has a certificate installed" }); - const caSecret = await certificateAuthoritySecretDAL.findOne({ caId: ca.id }); - - const keyId = await getProjectKmsCertificateKeyId({ - projectId: ca.projectId, + const { caPrivateKey, caPublicKey } = await getCaCredentials({ + caId, + certificateAuthorityDAL, + certificateAuthoritySecretDAL, projectDAL, kmsService }); - const privateKey = await kmsService.decrypt({ - kmsId: keyId, - cipherTextBlob: caSecret.encryptedPrivateKey - }); - const alg = keyAlgorithmToAlgCfg(ca.keyAlgorithm as CertKeyAlgorithm); - 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" - ]); const csrObj = await x509.Pkcs10CertificateRequestGenerator.create({ name: ca.dn, - keys: { privateKey: sk, publicKey: pk }, + keys: { + privateKey: caPrivateKey, + publicKey: caPublicKey + }, signingAlgorithm: alg, extensions: [ // eslint-disable-next-line no-bitwise @@ -388,30 +392,18 @@ export const certificateAuthorityServiceFactory = ({ ProjectPermissionSub.CertificateAuthorities ); - const caCert = await certificateAuthorityCertDAL.findOne({ caId: ca.id }); - - const keyId = await getProjectKmsCertificateKeyId({ - projectId: ca.projectId, + const { caCert, caCertChain, serialNumber } = await getCaCertChain({ + caId, + certificateAuthorityDAL, + certificateAuthorityCertDAL, 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: certObj.toString("pem"), - certificateChain: decryptedChain.toString("utf-8"), - serialNumber: certObj.serialNumber + certificate: caCert, + certificateChain: caCertChain, + serialNumber }; }; @@ -456,18 +448,6 @@ export const certificateAuthorityServiceFactory = ({ }); const caCert = await certificateAuthorityCertDAL.findOne({ caId: ca.id }); - const caSecret = await certificateAuthoritySecretDAL.findOne({ caId: ca.id }); - - 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 decryptedCaCert = await kmsService.decrypt({ kmsId: keyId, cipherTextBlob: caCert.encryptedCertificate @@ -507,6 +487,14 @@ export const certificateAuthorityServiceFactory = ({ throw new BadRequestError({ message: "notAfter date is after CA certificate's notAfter date" }); } + const { caPrivateKey } = await getCaCredentials({ + caId: ca.id, + certificateAuthorityDAL, + certificateAuthoritySecretDAL, + projectDAL, + kmsService + }); + const serialNumber = crypto.randomBytes(32).toString("hex"); const intermediateCert = await x509.X509CertificateGenerator.create({ serialNumber, @@ -514,7 +502,7 @@ export const certificateAuthorityServiceFactory = ({ issuer: caCertObj.subject, notBefore: notBeforeDate, notAfter: notAfterDate, - signingKey: sk, + signingKey: caPrivateKey, publicKey: csrObj.publicKey, signingAlgorithm: alg, extensions: [ @@ -531,21 +519,27 @@ export const certificateAuthorityServiceFactory = ({ ] }); - const caCertChain = await kmsService.decrypt({ - kmsId: keyId, - cipherTextBlob: caCert.encryptedCertificateChain + const { caCert: issuingCaCertificate, caCertChain } = await getCaCertChain({ + caId, + certificateAuthorityDAL, + certificateAuthorityCertDAL, + projectDAL, + kmsService }); - const certificateChain = `${caCertObj.toString("pem")}\n${caCertChain.toString("utf-8")}`.trim(); - return { certificate: intermediateCert.toString("pem"), - issuingCaCertificate: caCertObj.toString("pem"), - certificateChain, + issuingCaCertificate, + certificateChain: `${issuingCaCertificate}\n${caCertChain}`.trim(), serialNumber: intermediateCert.serialNumber }; }; + /** + * Import certificate for (un-installed) CA with id [caId]. + * Note: Can be used to import an external certificate and certificate chain + * to be installed into the CA. + */ const importCertToCa = async ({ caId, actorId, @@ -643,6 +637,9 @@ export const certificateAuthorityServiceFactory = ({ }); }; + /** + * Return new leaf certificate issued by CA with id [caId] + */ const issueCertFromCa = async ({ caId, commonName, @@ -685,33 +682,6 @@ export const certificateAuthorityServiceFactory = ({ const caCertObj = new x509.X509Certificate(decryptedCaCert); - const caSecret = await certificateAuthoritySecretDAL.findOne({ caId: ca.id }); - - const privateKey = await kmsService.decrypt({ - kmsId: keyId, - cipherTextBlob: caSecret.encryptedPrivateKey - }); - - const alg = keyAlgorithmToAlgCfg(ca.keyAlgorithm as CertKeyAlgorithm); - - 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" - ]); - - const leafKeys = await crypto.subtle.generateKey(alg, true, ["sign", "verify"]); - - const csrObj = await x509.Pkcs10CertificateRequestGenerator.create({ - name: `CN=${commonName}`, - keys: leafKeys, - signingAlgorithm: alg, - extensions: [ - // eslint-disable-next-line no-bitwise - new x509.KeyUsagesExtension(x509.KeyUsageFlags.digitalSignature | x509.KeyUsageFlags.keyEncipherment) - ], - attributes: [new x509.ChallengePasswordAttribute("password")] - }); - const notBeforeDate = notBefore ? new Date(notBefore) : new Date(); let notAfterDate = new Date(new Date().setFullYear(new Date().getFullYear() + 1)); @@ -737,6 +707,28 @@ export const certificateAuthorityServiceFactory = ({ throw new BadRequestError({ message: "notAfter date is after CA certificate's notAfter date" }); } + const alg = keyAlgorithmToAlgCfg(ca.keyAlgorithm as CertKeyAlgorithm); + const leafKeys = await crypto.subtle.generateKey(alg, true, ["sign", "verify"]); + + const csrObj = await x509.Pkcs10CertificateRequestGenerator.create({ + name: `CN=${commonName}`, + keys: leafKeys, + signingAlgorithm: alg, + extensions: [ + // eslint-disable-next-line no-bitwise + new x509.KeyUsagesExtension(x509.KeyUsageFlags.digitalSignature | x509.KeyUsageFlags.keyEncipherment) + ], + attributes: [new x509.ChallengePasswordAttribute("password")] + }); + + const { caPrivateKey } = await getCaCredentials({ + caId: ca.id, + certificateAuthorityDAL, + certificateAuthoritySecretDAL, + projectDAL, + kmsService + }); + const serialNumber = crypto.randomBytes(32).toString("hex"); const leafCert = await x509.X509CertificateGenerator.create({ serialNumber, @@ -744,7 +736,7 @@ export const certificateAuthorityServiceFactory = ({ issuer: caCertObj.subject, notBefore: notBeforeDate, notAfter: notAfterDate, - signingKey: caSk, + signingKey: caPrivateKey, publicKey: csrObj.publicKey, signingAlgorithm: alg, extensions: [ @@ -763,11 +755,6 @@ export const certificateAuthorityServiceFactory = ({ plainText: Buffer.from(new Uint8Array(leafCert.rawData)) }); - const caCertChain = await kmsService.decrypt({ - kmsId: keyId, - cipherTextBlob: caCert.encryptedCertificateChain - }); - await certificateDAL.transaction(async (tx) => { const cert = await certificateDAL.create( { @@ -792,19 +779,25 @@ export const certificateAuthorityServiceFactory = ({ return cert; }); - const certificateChain = `${caCertObj.toString("pem")}\n${caCertChain.toString("utf-8")}`.trim(); + const { caCert: issuingCaCertificate, caCertChain } = await getCaCertChain({ + caId: ca.id, + certificateAuthorityDAL, + certificateAuthorityCertDAL, + projectDAL, + kmsService + }); return { certificate: leafCert.toString("pem"), - certificateChain, - issuingCaCertificate: caCertObj.toString("pem"), + certificateChain: `${issuingCaCertificate}\n${caCertChain}`.trim(), + issuingCaCertificate, privateKey: skLeaf, serialNumber }; }; /** - * Return the Certificate Revocation List (CRL) for the CA + * Return the Certificate Revocation List (CRL) for CA with id [caId] */ const getCaCrl = async ({ caId, actorId, actorAuthMethod, actor, actorOrgId }: TGetCrl) => { const ca = await certificateAuthorityDAL.findById(caId); diff --git a/backend/src/services/certificate-authority/certificate-authority-types.ts b/backend/src/services/certificate-authority/certificate-authority-types.ts index cfcbd061e..d9fa0f59d 100644 --- a/backend/src/services/certificate-authority/certificate-authority-types.ts +++ b/backend/src/services/certificate-authority/certificate-authority-types.ts @@ -4,6 +4,7 @@ import { TKmsServiceFactory } from "@app/services/kms/kms-service"; import { TProjectDALFactory } from "@app/services/project/project-dal"; import { CertKeyAlgorithm } from "../certificate/certificate-types"; +import { TCertificateAuthorityCertDALFactory } from "./certificate-authority-cert-dal"; import { TCertificateAuthorityCrlDALFactory } from "./certificate-authority-crl-dal"; import { TCertificateAuthorityDALFactory } from "./certificate-authority-dal"; import { TCertificateAuthoritySecretDALFactory } from "./certificate-authority-secret-dal"; @@ -94,6 +95,22 @@ export type TDNParts = { locality?: string; }; +export type TGetCaCredentialsDTO = { + caId: string; + certificateAuthorityDAL: Pick; + certificateAuthoritySecretDAL: Pick; + projectDAL: Pick; + kmsService: Pick; +}; + +export type TGetCaCertChainDTO = { + caId: string; + certificateAuthorityDAL: Pick; + certificateAuthorityCertDAL: Pick; + projectDAL: Pick; + kmsService: Pick; +}; + export type TRebuildCaCrlDTO = { caId: string; certificateAuthorityDAL: Pick; diff --git a/backend/src/services/certificate/certificate-service.ts b/backend/src/services/certificate/certificate-service.ts index ad151c529..3e9dc5160 100644 --- a/backend/src/services/certificate/certificate-service.ts +++ b/backend/src/services/certificate/certificate-service.ts @@ -13,7 +13,7 @@ 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 { getCaCertChain, rebuildCaCrl } from "../certificate-authority/certificate-authority-fns"; import { revocationReasonToCrlCode } from "./certificate-fns"; import { CertStatus, TDeleteCertDTO, TGetCertCertDTO, TGetCertDTO, TRevokeCertDTO } from "./certificate-types"; @@ -42,6 +42,9 @@ export const certificateServiceFactory = ({ kmsService, permissionService }: TCertificateServiceFactoryDep) => { + /** + * Return details for certificate with serial number [serialNumber] + */ const getCert = async ({ serialNumber, actorId, actorAuthMethod, actor, actorOrgId }: TGetCertDTO) => { const cert = await certificateDAL.findOne({ serialNumber }); const ca = await certificateAuthorityDAL.findById(cert.caId); @@ -59,6 +62,9 @@ export const certificateServiceFactory = ({ return cert; }; + /** + * Delete certificate with serial number [serialNumber] + */ const deleteCert = async ({ serialNumber, actorId, actorAuthMethod, actor, actorOrgId }: TDeleteCertDTO) => { const cert = await certificateDAL.findOne({ serialNumber }); const ca = await certificateAuthorityDAL.findById(cert.caId); @@ -77,6 +83,11 @@ export const certificateServiceFactory = ({ return deletedCert; }; + /** + * Revoke certificate with serial number [serialNumber]. + * Note: Revoking a certificate adds it to the certificate revocation list (CRL) + * of its issuing CA + */ const revokeCert = async ({ serialNumber, revocationReason, @@ -126,10 +137,13 @@ export const certificateServiceFactory = ({ return { revokedAt }; }; + /** + * Return certificate body and certificate chain for certificate with + * serial number [serialNumber] + */ const getCertCert = async ({ serialNumber, actorId, actorAuthMethod, actor, actorOrgId }: TGetCertCertDTO) => { const cert = await certificateDAL.findOne({ serialNumber }); const ca = await certificateAuthorityDAL.findById(cert.caId); - const caCert = await certificateAuthorityCertDAL.findOne({ caId: ca.id }); const { permission } = await permissionService.getProjectPermission( actor, @@ -154,25 +168,19 @@ export const certificateServiceFactory = ({ cipherTextBlob: certCert.encryptedCertificate }); - const caCertChain = await kmsService.decrypt({ - kmsId: keyId, - cipherTextBlob: caCert.encryptedCertificateChain - }); - const certObj = new x509.X509Certificate(decryptedCert); - const decryptedCaCert = await kmsService.decrypt({ - kmsId: keyId, - cipherTextBlob: caCert.encryptedCertificate + const { caCert, caCertChain } = await getCaCertChain({ + caId: ca.id, + certificateAuthorityDAL, + certificateAuthorityCertDAL, + projectDAL, + kmsService }); - const caCertObj = new x509.X509Certificate(decryptedCaCert); - - const certificateChain = `${caCertObj.toString("pem")}\n${caCertChain.toString("utf-8")}`.trim(); - return { certificate: certObj.toString("pem"), - certificateChain, + certificateChain: `${caCert}\n${caCertChain}`.trim(), serialNumber: certObj.serialNumber }; };