From e47577491051c9ae75369d134932980867be1403 Mon Sep 17 00:00:00 2001 From: x Date: Wed, 30 Apr 2025 00:33:46 -0400 Subject: [PATCH] made certificates store PK and chain in relation to the main table, added /bundle endpoints, new audit log and permission entries --- .../ee/services/audit-log/audit-log-types.ts | 22 ++++ .../services/permission/project-permission.ts | 9 ++ backend/src/lib/api-docs/constants.ts | 3 +- backend/src/server/routes/index.ts | 4 + .../server/routes/v1/certificate-router.ts | 62 +++++++++- .../certificate-authority-service.ts | 41 +++++-- .../services/certificate/certificate-fns.ts | 3 +- .../certificate/certificate-secret-dal.ts | 4 +- .../certificate/certificate-service.ts | 111 +++++++++++++++++- .../services/certificate/certificate-types.ts | 4 + 10 files changed, 239 insertions(+), 24 deletions(-) diff --git a/backend/src/ee/services/audit-log/audit-log-types.ts b/backend/src/ee/services/audit-log/audit-log-types.ts index b6527f3f4..93d3f03c3 100644 --- a/backend/src/ee/services/audit-log/audit-log-types.ts +++ b/backend/src/ee/services/audit-log/audit-log-types.ts @@ -215,6 +215,8 @@ export enum EventType { DELETE_CERT = "delete-cert", REVOKE_CERT = "revoke-cert", GET_CERT_BODY = "get-cert-body", + GET_CERT_PRIVATE_KEY = "get-cert-private-key", + GET_CERT_BUNDLE = "get-cert-bundle", CREATE_PKI_ALERT = "create-pki-alert", GET_PKI_ALERT = "get-pki-alert", UPDATE_PKI_ALERT = "update-pki-alert", @@ -1719,6 +1721,24 @@ interface GetCertBody { }; } +interface GetCertPrivateKey { + type: EventType.GET_CERT_PRIVATE_KEY; + metadata: { + certId: string; + cn: string; + serialNumber: string; + }; +} + +interface GetCertBundle { + type: EventType.GET_CERT_BUNDLE; + metadata: { + certId: string; + cn: string; + serialNumber: string; + }; +} + interface CreatePkiAlert { type: EventType.CREATE_PKI_ALERT; metadata: { @@ -2691,6 +2711,8 @@ export type Event = | DeleteCert | RevokeCert | GetCertBody + | GetCertPrivateKey + | GetCertBundle | CreatePkiAlert | GetPkiAlert | UpdatePkiAlert diff --git a/backend/src/ee/services/permission/project-permission.ts b/backend/src/ee/services/permission/project-permission.ts index 8e6645073..7d659e99b 100644 --- a/backend/src/ee/services/permission/project-permission.ts +++ b/backend/src/ee/services/permission/project-permission.ts @@ -129,6 +129,7 @@ export enum ProjectPermissionSub { Identity = "identity", CertificateAuthorities = "certificate-authorities", Certificates = "certificates", + CertificatePrivateKey = "certificate-private-key", CertificateTemplates = "certificate-templates", SshCertificateAuthorities = "ssh-certificate-authorities", SshCertificates = "ssh-certificates", @@ -232,6 +233,7 @@ export type ProjectPermissionSet = ] | [ProjectPermissionActions, ProjectPermissionSub.CertificateAuthorities] | [ProjectPermissionActions, ProjectPermissionSub.Certificates] + | [ProjectPermissionActions, ProjectPermissionSub.CertificatePrivateKey] | [ProjectPermissionActions, ProjectPermissionSub.CertificateTemplates] | [ProjectPermissionActions, ProjectPermissionSub.SshCertificateAuthorities] | [ProjectPermissionActions, ProjectPermissionSub.SshCertificates] @@ -480,6 +482,12 @@ const GeneralPermissionSchema = [ "Describe what action an entity can take." ) }), + z.object({ + subject: z.literal(ProjectPermissionSub.CertificatePrivateKey).describe("The entity this permission pertains to."), + action: CASL_ACTION_SCHEMA_NATIVE_ENUM(ProjectPermissionActions).describe( + "Describe what action an entity can take." + ) + }), z.object({ subject: z.literal(ProjectPermissionSub.CertificateTemplates).describe("The entity this permission pertains to."), action: CASL_ACTION_SCHEMA_NATIVE_ENUM(ProjectPermissionActions).describe( @@ -681,6 +689,7 @@ const buildAdminPermissionRules = () => { ProjectPermissionSub.IpAllowList, ProjectPermissionSub.CertificateAuthorities, ProjectPermissionSub.Certificates, + ProjectPermissionSub.CertificatePrivateKey, ProjectPermissionSub.CertificateTemplates, ProjectPermissionSub.PkiAlerts, ProjectPermissionSub.PkiCollections, diff --git a/backend/src/lib/api-docs/constants.ts b/backend/src/lib/api-docs/constants.ts index 19ee7e331..c8fb3820c 100644 --- a/backend/src/lib/api-docs/constants.ts +++ b/backend/src/lib/api-docs/constants.ts @@ -1580,7 +1580,8 @@ export const CERTIFICATES = { serialNumber: "The serial number of the certificate to get the certificate body and certificate chain for.", certificate: "The certificate body of the certificate.", certificateChain: "The certificate chain of the certificate.", - serialNumberRes: "The serial number of the certificate." + serialNumberRes: "The serial number of the certificate.", + privateKey: "The private key of the certificate." } }; diff --git a/backend/src/server/routes/index.ts b/backend/src/server/routes/index.ts index 8ceeba648..f9a2223ee 100644 --- a/backend/src/server/routes/index.ts +++ b/backend/src/server/routes/index.ts @@ -260,6 +260,7 @@ import { registerSecretScannerGhApp } from "../plugins/secret-scanner"; import { registerV1Routes } from "./v1"; import { registerV2Routes } from "./v2"; import { registerV3Routes } from "./v3"; +import { certificateSecretDALFactory } from "@app/services/certificate/certificate-secret-dal"; const histogram = monitorEventLoopDelay({ resolution: 20 }); histogram.enable(); @@ -791,6 +792,7 @@ export const registerRoutes = async ( const certificateDAL = certificateDALFactory(db); const certificateBodyDAL = certificateBodyDALFactory(db); + const certificateSecretDAL = certificateSecretDALFactory(db); const pkiAlertDAL = pkiAlertDALFactory(db); const pkiCollectionDAL = pkiCollectionDALFactory(db); @@ -799,6 +801,7 @@ export const registerRoutes = async ( const certificateService = certificateServiceFactory({ certificateDAL, certificateBodyDAL, + certificateSecretDAL, certificateAuthorityDAL, certificateAuthorityCertDAL, certificateAuthorityCrlDAL, @@ -858,6 +861,7 @@ export const registerRoutes = async ( certificateAuthorityQueue, certificateDAL, certificateBodyDAL, + certificateSecretDAL, pkiCollectionDAL, pkiCollectionItemDAL, projectDAL, diff --git a/backend/src/server/routes/v1/certificate-router.ts b/backend/src/server/routes/v1/certificate-router.ts index 42c515795..d026868e9 100644 --- a/backend/src/server/routes/v1/certificate-router.ts +++ b/backend/src/server/routes/v1/certificate-router.ts @@ -64,7 +64,7 @@ export const registerCertRouter = async (server: FastifyZodProvider) => { } }); - // TODO(andrey): In the future add support for other formats outside of PEM. Adding a "format" query param may be best. + // TODO: In the future add support for other formats outside of PEM (such as DER). Adding a "format" query param may be best. server.route({ method: "GET", url: "/:serialNumber/private-key", @@ -96,7 +96,7 @@ export const registerCertRouter = async (server: FastifyZodProvider) => { ...req.auditLogInfo, projectId: ca.projectId, event: { - type: EventType.GET_CERT, + type: EventType.GET_CERT_PRIVATE_KEY, metadata: { certId: cert.id, cn: cert.commonName, @@ -109,6 +109,62 @@ export const registerCertRouter = async (server: FastifyZodProvider) => { } }); + // TODO: In the future add support for other formats outside of PEM (such as DER). Adding a "format" query param may be best. + server.route({ + method: "GET", + url: "/:serialNumber/bundle", + config: { + rateLimit: readLimit + }, + onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), + schema: { + hide: false, + tags: [ApiDocsTags.PkiCertificates], + description: "Get certificate bundle including the certificate, chain, and private key.", + params: z.object({ + serialNumber: z.string().trim().describe(CERTIFICATES.GET_CERT.serialNumber) + }), + response: { + 200: z.object({ + certificate: z.string().trim().describe(CERTIFICATES.GET_CERT.certificate), + certificateChain: z.string().trim().describe(CERTIFICATES.GET_CERT.certificateChain), + privateKey: z.string().trim().describe(CERTIFICATES.GET_CERT.certificateChain), + serialNumber: z.string().trim().describe(CERTIFICATES.GET_CERT.serialNumberRes) + }) + } + }, + handler: async (req) => { + const { certificate, certificateChain, serialNumber, cert, ca, privateKey } = + await server.services.certificate.getCertBundle({ + serialNumber: req.params.serialNumber, + actor: req.permission.type, + actorId: req.permission.id, + actorAuthMethod: req.permission.authMethod, + actorOrgId: req.permission.orgId + }); + + await server.services.auditLog.createAuditLog({ + ...req.auditLogInfo, + projectId: ca.projectId, + event: { + type: EventType.GET_CERT_BUNDLE, + metadata: { + certId: cert.id, + cn: cert.commonName, + serialNumber: cert.serialNumber + } + } + }); + + return { + certificate, + certificateChain, + serialNumber, + privateKey + }; + } + }); + server.route({ method: "POST", url: "/issue-certificate", @@ -474,7 +530,7 @@ export const registerCertRouter = async (server: FastifyZodProvider) => { ...req.auditLogInfo, projectId: ca.projectId, event: { - type: EventType.DELETE_CERT, + type: EventType.GET_CERT_BODY, metadata: { certId: cert.id, cn: cert.commonName, diff --git a/backend/src/services/certificate-authority/certificate-authority-service.ts b/backend/src/services/certificate-authority/certificate-authority-service.ts index 499a25741..4c40d9ff4 100644 --- a/backend/src/services/certificate-authority/certificate-authority-service.ts +++ b/backend/src/services/certificate-authority/certificate-authority-service.ts @@ -21,6 +21,7 @@ import { TProjectDALFactory } from "@app/services/project/project-dal"; import { getProjectKmsCertificateKeyId } from "@app/services/project/project-fns"; import { TCertificateAuthorityCrlDALFactory } from "../../ee/services/certificate-authority-crl/certificate-authority-crl-dal"; +import { TCertificateSecretDALFactory } from "../certificate/certificate-secret-dal"; import { CertExtendedKeyUsage, CertExtendedKeyUsageOIDToName, @@ -75,6 +76,7 @@ type TCertificateAuthorityServiceFactoryDep = { certificateTemplateDAL: Pick; certificateAuthorityQueue: TCertificateAuthorityQueueFactory; // TODO: Pick certificateDAL: Pick; + certificateSecretDAL: Pick; certificateBodyDAL: Pick; pkiCollectionDAL: Pick; pkiCollectionItemDAL: Pick; @@ -96,6 +98,7 @@ export const certificateAuthorityServiceFactory = ({ certificateTemplateDAL, certificateDAL, certificateBodyDAL, + certificateSecretDAL, pkiCollectionDAL, pkiCollectionItemDAL, projectDAL, @@ -1373,6 +1376,23 @@ export const certificateAuthorityServiceFactory = ({ const { cipherTextBlob: encryptedCertificate } = await kmsEncryptor({ plainText: Buffer.from(new Uint8Array(leafCert.rawData)) }); + const { cipherTextBlob: encryptedPrivateKey } = await kmsEncryptor({ + plainText: Buffer.from(skLeaf) + }); + + const { caCert: issuingCaCertificate, caCertChain } = await getCaCertChain({ + caCertId: caCert.id, + certificateAuthorityDAL, + certificateAuthorityCertDAL, + projectDAL, + kmsService + }); + + const certificateChainPem = `${issuingCaCertificate}\n${caCertChain}`.trim(); + + const { cipherTextBlob: encryptedCertificateChain } = await kmsEncryptor({ + plainText: Buffer.from(certificateChainPem) + }); await certificateDAL.transaction(async (tx) => { const cert = await certificateDAL.create( @@ -1396,7 +1416,16 @@ export const certificateAuthorityServiceFactory = ({ await certificateBodyDAL.create( { certId: cert.id, - encryptedCertificate + encryptedCertificate, + encryptedCertificateChain + }, + tx + ); + + await certificateSecretDAL.create( + { + certId: cert.id, + encryptedPrivateKey }, tx ); @@ -1414,17 +1443,9 @@ export const certificateAuthorityServiceFactory = ({ return cert; }); - const { caCert: issuingCaCertificate, caCertChain } = await getCaCertChain({ - caCertId: caCert.id, - certificateAuthorityDAL, - certificateAuthorityCertDAL, - projectDAL, - kmsService - }); - return { certificate: leafCert.toString("pem"), - certificateChain: `${issuingCaCertificate}\n${caCertChain}`.trim(), + certificateChain: certificateChainPem, issuingCaCertificate, privateKey: skLeaf, serialNumber, diff --git a/backend/src/services/certificate/certificate-fns.ts b/backend/src/services/certificate/certificate-fns.ts index 55b731bd7..6f9963db7 100644 --- a/backend/src/services/certificate/certificate-fns.ts +++ b/backend/src/services/certificate/certificate-fns.ts @@ -72,7 +72,6 @@ export const getCertificateCredentials = async ({ projectDAL, kmsService }); - const kmsDecryptor = await kmsService.decryptWithKmsKey({ kmsId: keyId }); @@ -80,7 +79,7 @@ export const getCertificateCredentials = async ({ cipherTextBlob: certificateSecret.encryptedPrivateKey }); - const skObj = crypto.createPrivateKey({ key: decryptedPrivateKey, format: "der", type: "pkcs8" }); + const skObj = crypto.createPrivateKey({ key: decryptedPrivateKey, format: "pem", type: "pkcs8" }); const certPrivateKey = skObj.export({ format: "pem", type: "pkcs8" }).toString(); const pkObj = crypto.createPublicKey(skObj); diff --git a/backend/src/services/certificate/certificate-secret-dal.ts b/backend/src/services/certificate/certificate-secret-dal.ts index d7f3e43ae..c1493eceb 100644 --- a/backend/src/services/certificate/certificate-secret-dal.ts +++ b/backend/src/services/certificate/certificate-secret-dal.ts @@ -5,6 +5,6 @@ import { ormify } from "@app/lib/knex"; export type TCertificateSecretDALFactory = ReturnType; export const certificateSecretDALFactory = (db: TDbClient) => { - const caSecretOrm = ormify(db, TableName.CertificateSecret); - return caSecretOrm; + const certSecretOrm = ormify(db, TableName.CertificateSecret); + return certSecretOrm; }; diff --git a/backend/src/services/certificate/certificate-service.ts b/backend/src/services/certificate/certificate-service.ts index d72235781..3568c0ca1 100644 --- a/backend/src/services/certificate/certificate-service.ts +++ b/backend/src/services/certificate/certificate-service.ts @@ -17,7 +17,14 @@ import { getProjectKmsCertificateKeyId } from "@app/services/project/project-fns import { getCaCertChain, rebuildCaCrl } from "../certificate-authority/certificate-authority-fns"; import { getCertificateCredentials, revocationReasonToCrlCode } from "./certificate-fns"; import { TCertificateSecretDALFactory } from "./certificate-secret-dal"; -import { CertStatus, TDeleteCertDTO, TGetCertBodyDTO, TGetCertDTO, TRevokeCertDTO } from "./certificate-types"; +import { + CertStatus, + TDeleteCertDTO, + TGetCertBodyDTO, + TGetCertBundleDTO, + TGetCertDTO, + TRevokeCertDTO +} from "./certificate-types"; type TCertificateServiceFactoryDep = { certificateDAL: Pick; @@ -86,11 +93,13 @@ export const certificateServiceFactory = ({ actionProjectType: ActionProjectType.CertificateManager }); - // TODO(andrey): Update permission for privateKey fetching. Should be very strict. - ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Certificates); + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionActions.Read, + ProjectPermissionSub.CertificatePrivateKey + ); const { certPrivateKey } = await getCertificateCredentials({ - certId: ca.id, + certId: cert.id, projectId: ca.projectId, certificateSecretDAL, projectDAL, @@ -229,20 +238,110 @@ export const certificateServiceFactory = ({ kmsService }); + let certificateChain = `${caCert}\n${caCertChain}`.trim(); + + // If the certificate was generated after ~05/01/25 it will have a encryptedCertificateChain attached to it's body + if (certBody.encryptedCertificateChain) { + const decryptedCertChain = await kmsDecryptor({ + cipherTextBlob: certBody.encryptedCertificateChain + }); + const certChainObj = new x509.X509Certificate(decryptedCertChain); + certificateChain = certChainObj.toString("pem"); + } + return { certificate: certObj.toString("pem"), - certificateChain: `${caCert}\n${caCertChain}`.trim(), + certificateChain, serialNumber: certObj.serialNumber, cert, ca }; }; + /** + * Return certificate body and certificate chain for certificate with + * serial number [serialNumber] + */ + const getCertBundle = async ({ serialNumber, actorId, actorAuthMethod, actor, actorOrgId }: TGetCertBundleDTO) => { + const cert = await certificateDAL.findOne({ serialNumber }); + const ca = await certificateAuthorityDAL.findById(cert.caId); + + const { permission } = await permissionService.getProjectPermission({ + actor, + actorId, + projectId: ca.projectId, + actorAuthMethod, + actorOrgId, + actionProjectType: ActionProjectType.CertificateManager + }); + + ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Certificates); + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionActions.Read, + ProjectPermissionSub.CertificatePrivateKey + ); + + const certBody = await certificateBodyDAL.findOne({ certId: cert.id }); + + const certificateManagerKeyId = await getProjectKmsCertificateKeyId({ + projectId: ca.projectId, + projectDAL, + kmsService + }); + + const kmsDecryptor = await kmsService.decryptWithKmsKey({ + kmsId: certificateManagerKeyId + }); + const decryptedCert = await kmsDecryptor({ + cipherTextBlob: certBody.encryptedCertificate + }); + + const certObj = new x509.X509Certificate(decryptedCert); + const certificate = certObj.toString("pem"); + + const { caCert, caCertChain } = await getCaCertChain({ + caCertId: cert.caCertId, + certificateAuthorityDAL, + certificateAuthorityCertDAL, + projectDAL, + kmsService + }); + + let certificateChain = `${caCert}\n${caCertChain}`.trim(); + + // If the certificate was generated after ~05/01/25 it will have a encryptedCertificateChain attached to it's body + if (certBody.encryptedCertificateChain) { + const decryptedCertChain = await kmsDecryptor({ + cipherTextBlob: certBody.encryptedCertificateChain + }); + const certChainObj = new x509.X509Certificate(decryptedCertChain); + certificateChain = certChainObj.toString("pem"); + } + + const { certPrivateKey } = await getCertificateCredentials({ + certId: cert.id, + projectId: ca.projectId, + certificateSecretDAL, + projectDAL, + kmsService + }); + + return { + certificate, + certificateChain, + privateKey: certPrivateKey, + serialNumber, + cert, + ca + }; + }; + return { getCert, getCertPrivateKey, deleteCert, revokeCert, - getCertBody + getCertBody, + getCertBundle }; }; diff --git a/backend/src/services/certificate/certificate-types.ts b/backend/src/services/certificate/certificate-types.ts index 48454f803..a36671030 100644 --- a/backend/src/services/certificate/certificate-types.ts +++ b/backend/src/services/certificate/certificate-types.ts @@ -78,6 +78,10 @@ export type TGetCertBodyDTO = { serialNumber: string; } & Omit; +export type TGetCertBundleDTO = { + serialNumber: string; +} & Omit; + export type TGetCertificateCredentialsDTO = { certId: string; projectId: string;