made certificates store PK and chain in relation to the main table, added /bundle endpoints, new audit log and permission entries

This commit is contained in:
x
2025-04-30 00:33:46 -04:00
parent e81c49500b
commit e475774910
10 changed files with 239 additions and 24 deletions
@@ -215,6 +215,8 @@ export enum EventType {
DELETE_CERT = "delete-cert", DELETE_CERT = "delete-cert",
REVOKE_CERT = "revoke-cert", REVOKE_CERT = "revoke-cert",
GET_CERT_BODY = "get-cert-body", 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", CREATE_PKI_ALERT = "create-pki-alert",
GET_PKI_ALERT = "get-pki-alert", GET_PKI_ALERT = "get-pki-alert",
UPDATE_PKI_ALERT = "update-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 { interface CreatePkiAlert {
type: EventType.CREATE_PKI_ALERT; type: EventType.CREATE_PKI_ALERT;
metadata: { metadata: {
@@ -2691,6 +2711,8 @@ export type Event =
| DeleteCert | DeleteCert
| RevokeCert | RevokeCert
| GetCertBody | GetCertBody
| GetCertPrivateKey
| GetCertBundle
| CreatePkiAlert | CreatePkiAlert
| GetPkiAlert | GetPkiAlert
| UpdatePkiAlert | UpdatePkiAlert
@@ -129,6 +129,7 @@ export enum ProjectPermissionSub {
Identity = "identity", Identity = "identity",
CertificateAuthorities = "certificate-authorities", CertificateAuthorities = "certificate-authorities",
Certificates = "certificates", Certificates = "certificates",
CertificatePrivateKey = "certificate-private-key",
CertificateTemplates = "certificate-templates", CertificateTemplates = "certificate-templates",
SshCertificateAuthorities = "ssh-certificate-authorities", SshCertificateAuthorities = "ssh-certificate-authorities",
SshCertificates = "ssh-certificates", SshCertificates = "ssh-certificates",
@@ -232,6 +233,7 @@ export type ProjectPermissionSet =
] ]
| [ProjectPermissionActions, ProjectPermissionSub.CertificateAuthorities] | [ProjectPermissionActions, ProjectPermissionSub.CertificateAuthorities]
| [ProjectPermissionActions, ProjectPermissionSub.Certificates] | [ProjectPermissionActions, ProjectPermissionSub.Certificates]
| [ProjectPermissionActions, ProjectPermissionSub.CertificatePrivateKey]
| [ProjectPermissionActions, ProjectPermissionSub.CertificateTemplates] | [ProjectPermissionActions, ProjectPermissionSub.CertificateTemplates]
| [ProjectPermissionActions, ProjectPermissionSub.SshCertificateAuthorities] | [ProjectPermissionActions, ProjectPermissionSub.SshCertificateAuthorities]
| [ProjectPermissionActions, ProjectPermissionSub.SshCertificates] | [ProjectPermissionActions, ProjectPermissionSub.SshCertificates]
@@ -480,6 +482,12 @@ const GeneralPermissionSchema = [
"Describe what action an entity can take." "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({ z.object({
subject: z.literal(ProjectPermissionSub.CertificateTemplates).describe("The entity this permission pertains to."), subject: z.literal(ProjectPermissionSub.CertificateTemplates).describe("The entity this permission pertains to."),
action: CASL_ACTION_SCHEMA_NATIVE_ENUM(ProjectPermissionActions).describe( action: CASL_ACTION_SCHEMA_NATIVE_ENUM(ProjectPermissionActions).describe(
@@ -681,6 +689,7 @@ const buildAdminPermissionRules = () => {
ProjectPermissionSub.IpAllowList, ProjectPermissionSub.IpAllowList,
ProjectPermissionSub.CertificateAuthorities, ProjectPermissionSub.CertificateAuthorities,
ProjectPermissionSub.Certificates, ProjectPermissionSub.Certificates,
ProjectPermissionSub.CertificatePrivateKey,
ProjectPermissionSub.CertificateTemplates, ProjectPermissionSub.CertificateTemplates,
ProjectPermissionSub.PkiAlerts, ProjectPermissionSub.PkiAlerts,
ProjectPermissionSub.PkiCollections, ProjectPermissionSub.PkiCollections,
+2 -1
View File
@@ -1580,7 +1580,8 @@ export const CERTIFICATES = {
serialNumber: "The serial number of the certificate to get the certificate body and certificate chain for.", serialNumber: "The serial number of the certificate to get the certificate body and certificate chain for.",
certificate: "The certificate body of the certificate.", certificate: "The certificate body of the certificate.",
certificateChain: "The certificate chain 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."
} }
}; };
+4
View File
@@ -260,6 +260,7 @@ import { registerSecretScannerGhApp } from "../plugins/secret-scanner";
import { registerV1Routes } from "./v1"; import { registerV1Routes } from "./v1";
import { registerV2Routes } from "./v2"; import { registerV2Routes } from "./v2";
import { registerV3Routes } from "./v3"; import { registerV3Routes } from "./v3";
import { certificateSecretDALFactory } from "@app/services/certificate/certificate-secret-dal";
const histogram = monitorEventLoopDelay({ resolution: 20 }); const histogram = monitorEventLoopDelay({ resolution: 20 });
histogram.enable(); histogram.enable();
@@ -791,6 +792,7 @@ export const registerRoutes = async (
const certificateDAL = certificateDALFactory(db); const certificateDAL = certificateDALFactory(db);
const certificateBodyDAL = certificateBodyDALFactory(db); const certificateBodyDAL = certificateBodyDALFactory(db);
const certificateSecretDAL = certificateSecretDALFactory(db);
const pkiAlertDAL = pkiAlertDALFactory(db); const pkiAlertDAL = pkiAlertDALFactory(db);
const pkiCollectionDAL = pkiCollectionDALFactory(db); const pkiCollectionDAL = pkiCollectionDALFactory(db);
@@ -799,6 +801,7 @@ export const registerRoutes = async (
const certificateService = certificateServiceFactory({ const certificateService = certificateServiceFactory({
certificateDAL, certificateDAL,
certificateBodyDAL, certificateBodyDAL,
certificateSecretDAL,
certificateAuthorityDAL, certificateAuthorityDAL,
certificateAuthorityCertDAL, certificateAuthorityCertDAL,
certificateAuthorityCrlDAL, certificateAuthorityCrlDAL,
@@ -858,6 +861,7 @@ export const registerRoutes = async (
certificateAuthorityQueue, certificateAuthorityQueue,
certificateDAL, certificateDAL,
certificateBodyDAL, certificateBodyDAL,
certificateSecretDAL,
pkiCollectionDAL, pkiCollectionDAL,
pkiCollectionItemDAL, pkiCollectionItemDAL,
projectDAL, projectDAL,
@@ -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({ server.route({
method: "GET", method: "GET",
url: "/:serialNumber/private-key", url: "/:serialNumber/private-key",
@@ -96,7 +96,7 @@ export const registerCertRouter = async (server: FastifyZodProvider) => {
...req.auditLogInfo, ...req.auditLogInfo,
projectId: ca.projectId, projectId: ca.projectId,
event: { event: {
type: EventType.GET_CERT, type: EventType.GET_CERT_PRIVATE_KEY,
metadata: { metadata: {
certId: cert.id, certId: cert.id,
cn: cert.commonName, 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({ server.route({
method: "POST", method: "POST",
url: "/issue-certificate", url: "/issue-certificate",
@@ -474,7 +530,7 @@ export const registerCertRouter = async (server: FastifyZodProvider) => {
...req.auditLogInfo, ...req.auditLogInfo,
projectId: ca.projectId, projectId: ca.projectId,
event: { event: {
type: EventType.DELETE_CERT, type: EventType.GET_CERT_BODY,
metadata: { metadata: {
certId: cert.id, certId: cert.id,
cn: cert.commonName, cn: cert.commonName,
@@ -21,6 +21,7 @@ import { TProjectDALFactory } from "@app/services/project/project-dal";
import { getProjectKmsCertificateKeyId } from "@app/services/project/project-fns"; import { getProjectKmsCertificateKeyId } from "@app/services/project/project-fns";
import { TCertificateAuthorityCrlDALFactory } from "../../ee/services/certificate-authority-crl/certificate-authority-crl-dal"; import { TCertificateAuthorityCrlDALFactory } from "../../ee/services/certificate-authority-crl/certificate-authority-crl-dal";
import { TCertificateSecretDALFactory } from "../certificate/certificate-secret-dal";
import { import {
CertExtendedKeyUsage, CertExtendedKeyUsage,
CertExtendedKeyUsageOIDToName, CertExtendedKeyUsageOIDToName,
@@ -75,6 +76,7 @@ type TCertificateAuthorityServiceFactoryDep = {
certificateTemplateDAL: Pick<TCertificateTemplateDALFactory, "getById" | "find">; certificateTemplateDAL: Pick<TCertificateTemplateDALFactory, "getById" | "find">;
certificateAuthorityQueue: TCertificateAuthorityQueueFactory; // TODO: Pick certificateAuthorityQueue: TCertificateAuthorityQueueFactory; // TODO: Pick
certificateDAL: Pick<TCertificateDALFactory, "transaction" | "create" | "find">; certificateDAL: Pick<TCertificateDALFactory, "transaction" | "create" | "find">;
certificateSecretDAL: Pick<TCertificateSecretDALFactory, "create">;
certificateBodyDAL: Pick<TCertificateBodyDALFactory, "create">; certificateBodyDAL: Pick<TCertificateBodyDALFactory, "create">;
pkiCollectionDAL: Pick<TPkiCollectionDALFactory, "findById">; pkiCollectionDAL: Pick<TPkiCollectionDALFactory, "findById">;
pkiCollectionItemDAL: Pick<TPkiCollectionItemDALFactory, "create">; pkiCollectionItemDAL: Pick<TPkiCollectionItemDALFactory, "create">;
@@ -96,6 +98,7 @@ export const certificateAuthorityServiceFactory = ({
certificateTemplateDAL, certificateTemplateDAL,
certificateDAL, certificateDAL,
certificateBodyDAL, certificateBodyDAL,
certificateSecretDAL,
pkiCollectionDAL, pkiCollectionDAL,
pkiCollectionItemDAL, pkiCollectionItemDAL,
projectDAL, projectDAL,
@@ -1373,6 +1376,23 @@ export const certificateAuthorityServiceFactory = ({
const { cipherTextBlob: encryptedCertificate } = await kmsEncryptor({ const { cipherTextBlob: encryptedCertificate } = await kmsEncryptor({
plainText: Buffer.from(new Uint8Array(leafCert.rawData)) 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) => { await certificateDAL.transaction(async (tx) => {
const cert = await certificateDAL.create( const cert = await certificateDAL.create(
@@ -1396,7 +1416,16 @@ export const certificateAuthorityServiceFactory = ({
await certificateBodyDAL.create( await certificateBodyDAL.create(
{ {
certId: cert.id, certId: cert.id,
encryptedCertificate encryptedCertificate,
encryptedCertificateChain
},
tx
);
await certificateSecretDAL.create(
{
certId: cert.id,
encryptedPrivateKey
}, },
tx tx
); );
@@ -1414,17 +1443,9 @@ export const certificateAuthorityServiceFactory = ({
return cert; return cert;
}); });
const { caCert: issuingCaCertificate, caCertChain } = await getCaCertChain({
caCertId: caCert.id,
certificateAuthorityDAL,
certificateAuthorityCertDAL,
projectDAL,
kmsService
});
return { return {
certificate: leafCert.toString("pem"), certificate: leafCert.toString("pem"),
certificateChain: `${issuingCaCertificate}\n${caCertChain}`.trim(), certificateChain: certificateChainPem,
issuingCaCertificate, issuingCaCertificate,
privateKey: skLeaf, privateKey: skLeaf,
serialNumber, serialNumber,
@@ -72,7 +72,6 @@ export const getCertificateCredentials = async ({
projectDAL, projectDAL,
kmsService kmsService
}); });
const kmsDecryptor = await kmsService.decryptWithKmsKey({ const kmsDecryptor = await kmsService.decryptWithKmsKey({
kmsId: keyId kmsId: keyId
}); });
@@ -80,7 +79,7 @@ export const getCertificateCredentials = async ({
cipherTextBlob: certificateSecret.encryptedPrivateKey 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 certPrivateKey = skObj.export({ format: "pem", type: "pkcs8" }).toString();
const pkObj = crypto.createPublicKey(skObj); const pkObj = crypto.createPublicKey(skObj);
@@ -5,6 +5,6 @@ import { ormify } from "@app/lib/knex";
export type TCertificateSecretDALFactory = ReturnType<typeof certificateSecretDALFactory>; export type TCertificateSecretDALFactory = ReturnType<typeof certificateSecretDALFactory>;
export const certificateSecretDALFactory = (db: TDbClient) => { export const certificateSecretDALFactory = (db: TDbClient) => {
const caSecretOrm = ormify(db, TableName.CertificateSecret); const certSecretOrm = ormify(db, TableName.CertificateSecret);
return caSecretOrm; return certSecretOrm;
}; };
@@ -17,7 +17,14 @@ import { getProjectKmsCertificateKeyId } from "@app/services/project/project-fns
import { getCaCertChain, rebuildCaCrl } from "../certificate-authority/certificate-authority-fns"; import { getCaCertChain, rebuildCaCrl } from "../certificate-authority/certificate-authority-fns";
import { getCertificateCredentials, revocationReasonToCrlCode } from "./certificate-fns"; import { getCertificateCredentials, revocationReasonToCrlCode } from "./certificate-fns";
import { TCertificateSecretDALFactory } from "./certificate-secret-dal"; 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 = { type TCertificateServiceFactoryDep = {
certificateDAL: Pick<TCertificateDALFactory, "findOne" | "deleteById" | "update" | "find">; certificateDAL: Pick<TCertificateDALFactory, "findOne" | "deleteById" | "update" | "find">;
@@ -86,11 +93,13 @@ export const certificateServiceFactory = ({
actionProjectType: ActionProjectType.CertificateManager actionProjectType: ActionProjectType.CertificateManager
}); });
// TODO(andrey): Update permission for privateKey fetching. Should be very strict. ForbiddenError.from(permission).throwUnlessCan(
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Certificates); ProjectPermissionActions.Read,
ProjectPermissionSub.CertificatePrivateKey
);
const { certPrivateKey } = await getCertificateCredentials({ const { certPrivateKey } = await getCertificateCredentials({
certId: ca.id, certId: cert.id,
projectId: ca.projectId, projectId: ca.projectId,
certificateSecretDAL, certificateSecretDAL,
projectDAL, projectDAL,
@@ -229,20 +238,110 @@ export const certificateServiceFactory = ({
kmsService 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 { return {
certificate: certObj.toString("pem"), certificate: certObj.toString("pem"),
certificateChain: `${caCert}\n${caCertChain}`.trim(), certificateChain,
serialNumber: certObj.serialNumber, serialNumber: certObj.serialNumber,
cert, cert,
ca 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 { return {
getCert, getCert,
getCertPrivateKey, getCertPrivateKey,
deleteCert, deleteCert,
revokeCert, revokeCert,
getCertBody getCertBody,
getCertBundle
}; };
}; };
@@ -78,6 +78,10 @@ export type TGetCertBodyDTO = {
serialNumber: string; serialNumber: string;
} & Omit<TProjectPermission, "projectId">; } & Omit<TProjectPermission, "projectId">;
export type TGetCertBundleDTO = {
serialNumber: string;
} & Omit<TProjectPermission, "projectId">;
export type TGetCertificateCredentialsDTO = { export type TGetCertificateCredentialsDTO = {
certId: string; certId: string;
projectId: string; projectId: string;