logging, finalizing some functions, and other tweaks

This commit is contained in:
x
2025-04-30 20:20:31 -04:00
parent 6a973be6f3
commit bda74ce13e
18 changed files with 190 additions and 165 deletions
@@ -209,6 +209,7 @@ export enum EventType {
IMPORT_CA_CERT = "import-certificate-authority-cert",
GET_CA_CRLS = "get-certificate-authority-crls",
ISSUE_CERT = "issue-cert",
IMPORT_CERT = "import-cert",
SIGN_CERT = "sign-cert",
GET_CA_CERTIFICATE_TEMPLATES = "get-ca-certificate-templates",
GET_CERT = "get-cert",
@@ -1666,6 +1667,15 @@ interface IssueCert {
};
}
interface ImportCert {
type: EventType.IMPORT_CERT;
metadata: {
certId: string;
cn: string;
serialNumber: string;
};
}
interface SignCert {
type: EventType.SIGN_CERT;
metadata: {
@@ -2685,6 +2695,7 @@ export type Event =
| ImportCaCert
| GetCaCrls
| IssueCert
| ImportCert
| SignCert
| GetCaCertificateTemplates
| GetCert
+3 -1
View File
@@ -805,7 +805,9 @@ export const registerRoutes = async (
certificateAuthoritySecretDAL,
projectDAL,
kmsService,
permissionService
permissionService,
pkiCollectionDAL,
pkiCollectionItemDAL
});
const certificateAuthorityQueue = certificateAuthorityQueueFactory({
@@ -192,8 +192,8 @@ export const registerCertRouter = async (server: FastifyZodProvider) => {
projectSlug: z.string().trim().min(1).describe(CERTIFICATES.IMPORT.projectSlug),
certificatePem: z.string().trim().min(1).describe(CERTIFICATES.IMPORT.certificatePem),
privateKeyPem: z.string().trim().optional().describe(CERTIFICATES.IMPORT.privateKeyPem),
chainPem: z.string().trim().optional().describe(CERTIFICATES.IMPORT.chainPem),
privateKeyPem: z.string().trim().describe(CERTIFICATES.IMPORT.privateKeyPem),
chainPem: z.string().trim().describe(CERTIFICATES.IMPORT.chainPem),
friendlyName: z.string().trim().optional().describe(CERTIFICATES.IMPORT.friendlyName),
pkiCollectionId: z.string().trim().optional().describe(CERTIFICATES.IMPORT.pkiCollectionId)
@@ -201,46 +201,35 @@ export const registerCertRouter = async (server: FastifyZodProvider) => {
response: {
200: z.object({
certificate: z.string().trim().describe(CERTIFICATES.IMPORT.certificate),
certificateChain: z.string().trim().optional().describe(CERTIFICATES.IMPORT.certificateChain),
privateKey: z.string().trim().optional().describe(CERTIFICATES.IMPORT.privateKey),
certificateChain: z.string().trim().describe(CERTIFICATES.IMPORT.certificateChain),
privateKey: z.string().trim().describe(CERTIFICATES.IMPORT.privateKey),
serialNumber: z.string().trim().describe(CERTIFICATES.IMPORT.serialNumber)
})
}
},
handler: async (req) => {
const { certificate, certificateChain, privateKey, serialNumber } = await server.services.certificate.importCert({
actor: req.permission.type,
actorId: req.permission.id,
actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId,
...req.body
const { certificate, certificateChain, privateKey, serialNumber, cert } =
await server.services.certificate.importCert({
actor: req.permission.type,
actorId: req.permission.id,
actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId,
...req.body
});
await server.services.auditLog.createAuditLog({
...req.auditLogInfo,
projectId: cert.projectId,
event: {
type: EventType.IMPORT_CERT,
metadata: {
certId: cert.id,
cn: cert.commonName,
serialNumber
}
}
});
// TODO(andrey): Add logs
// await server.services.auditLog.createAuditLog({
// ...req.auditLogInfo,
// projectId: ca.projectId,
// event: {
// type: EventType.ISSUE_CERT,
// metadata: {
// caId: ca.id,
// dn: ca.dn,
// serialNumber
// }
// }
// });
// await server.services.telemetry.sendPostHogEvents({
// event: PostHogEventTypes.IssueCert,
// distinctId: getTelemetryDistinctId(req),
// properties: {
// caId: req.body.caId,
// certificateTemplateId: req.body.certificateTemplateId,
// commonName: req.body.commonName,
// ...req.auditLogInfo
// }
// });
return {
certificate,
certificateChain,
@@ -1389,7 +1389,7 @@ export const certificateAuthorityServiceFactory = ({
notAfter: notAfterDate,
keyUsages: selectedKeyUsages,
extendedKeyUsages: selectedExtendedKeyUsages,
projectId: ca.projectId
projectId: (ca as TCertificateAuthorities).projectId
},
tx
);
@@ -1782,7 +1782,7 @@ export const certificateAuthorityServiceFactory = ({
notAfter: notAfterDate,
keyUsages: selectedKeyUsages,
extendedKeyUsages: selectedExtendedKeyUsages,
projectId: ca.projectId
projectId: (ca as TCertificateAuthorities).projectId
},
tx
);
@@ -46,3 +46,6 @@ export const constructPemChainFromCerts = (certificates: x509.X509Certificate[])
.map((cert) => cert.toString("pem"))
.join("\n")
.trim();
export const splitPemChain = (pemText: string) =>
pemText.match(/-----BEGIN CERTIFICATE-----[^-]+-----END CERTIFICATE-----/g) || [];
@@ -19,8 +19,11 @@ import { TProjectDALFactory } from "@app/services/project/project-dal";
import { getProjectKmsCertificateKeyId } from "@app/services/project/project-fns";
import { getCaCertChain, rebuildCaCrl } from "../certificate-authority/certificate-authority-fns";
import { revocationReasonToCrlCode } from "./certificate-fns";
import { revocationReasonToCrlCode, splitPemChain } from "./certificate-fns";
import {
CertExtendedKeyUsage,
CertExtendedKeyUsageOIDToName,
CertKeyUsage,
CertStatus,
TDeleteCertDTO,
TGetCertBodyDTO,
@@ -273,46 +276,54 @@ export const certificateServiceFactory = ({
}
// Parse the certificate
const certObj = new x509.X509Certificate(certificatePem);
const leafCert = new x509.X509Certificate(certificatePem);
// Verify the certificate chain
if (chainPem) {
const chainCert = new x509.X509Certificate(chainPem);
if (!(await certObj.verify({ publicKey: chainCert.publicKey }))) {
throw new BadRequestError({ message: "Certificate chain verification failed" });
}
const chainCerts = splitPemChain(chainPem).map((pem) => new x509.X509Certificate(pem));
if (chainCerts.length === 0) {
throw new BadRequestError({
message: "Certificate chain must contain at least one issuer certificate"
});
}
// If private key provided, verify it matches the certificate
if (privateKeyPem) {
try {
const message = Buffer.from("certificate-verification-test");
const chainValidationPromises = chainCerts.map((issuerCert) =>
leafCert.verify({ publicKey: issuerCert.publicKey }).catch(() => false)
);
const privateKey = createPrivateKey(privateKeyPem);
const publicKey = createPublicKey(certificatePem);
const results = await Promise.all(chainValidationPromises);
const signature = sign(null, message, privateKey);
const isValid = verify(null, message, publicKey, signature);
if (!results.some((result) => result === true)) {
throw new BadRequestError({ message: "Certificate chain verification failed" });
}
if (!isValid) {
throw new BadRequestError({ message: "Private key does not match certificate" });
}
} catch (err) {
throw new BadRequestError({ message: "Invalid private key format" });
// Verify private key matches the certificate
try {
const message = Buffer.from("certificate-verification-test");
const privateKey = createPrivateKey(privateKeyPem);
const publicKey = createPublicKey(certificatePem);
const signature = sign(null, message, privateKey);
const isValid = verify(null, message, publicKey, signature);
if (!isValid) {
throw new BadRequestError({ message: "Private key does not match certificate" });
}
} catch (err) {
throw new BadRequestError({ message: "Invalid private key format" });
}
// Get certificate attributes
const commonName = Array.from(certObj.subjectName.getField("CN")?.values() || [])[0] || "";
const commonName = Array.from(leafCert.subjectName.getField("CN")?.values() || [])[0] || "";
let altNames: undefined | string;
const sanExtension = certObj.extensions.find((ext) => ext.type === "2.5.29.17");
const sanExtension = leafCert.extensions.find((ext) => ext.type === "2.5.29.17");
if (sanExtension) {
const sanNames = new x509.GeneralNames(sanExtension.value);
altNames = sanNames.items.map((name) => name.value).join(", ");
}
const { serialNumber, notBefore, notAfter } = certObj;
const { serialNumber, notBefore, notAfter } = leafCert;
// Encrypt certificate for storage
const certificateManagerKeyId = await getProjectKmsCertificateKeyId({
@@ -328,50 +339,83 @@ export const certificateServiceFactory = ({
plainText: Buffer.from(certificatePem)
});
await certificateDAL.transaction(async (tx) => {
const cert = await certificateDAL.create(
{
status: CertStatus.ACTIVE,
friendlyName: friendlyName || commonName,
commonName,
altNames,
serialNumber,
notBefore,
notAfter,
projectId
// TODO(andrey): Add keyUsages and extendedKeyUsages
// keyUsages,
// extendedKeyUsages
},
tx
);
// Extract Key Usage
const keyUsagesExt = leafCert.getExtension("2.5.29.15") as x509.KeyUsagesExtension;
await certificateBodyDAL.create(
{
certId: cert.id,
encryptedCertificate
},
tx
let keyUsages: CertKeyUsage[] = [];
if (keyUsagesExt) {
keyUsages = Object.values(CertKeyUsage).filter(
// eslint-disable-next-line no-bitwise
(keyUsage) => (x509.KeyUsageFlags[keyUsage] & keyUsagesExt.usages) !== 0
);
}
if (collectionId) {
await pkiCollectionItemDAL.create(
// Extract Extended Key Usage
const extKeyUsageExt = leafCert.getExtension("2.5.29.37") as x509.ExtendedKeyUsageExtension;
let extendedKeyUsages: CertExtendedKeyUsage[] = [];
if (extKeyUsageExt) {
extendedKeyUsages = extKeyUsageExt.usages.map((ekuOid) => CertExtendedKeyUsageOIDToName[ekuOid as string]);
}
const cert = await certificateDAL.transaction(async (tx) => {
try {
const txCert = await certificateDAL.create(
{
pkiCollectionId: collectionId,
certId: cert.id
status: CertStatus.ACTIVE,
friendlyName: friendlyName || commonName,
commonName,
altNames,
serialNumber,
notBefore,
notAfter,
projectId,
keyUsages,
extendedKeyUsages
},
tx
);
}
return cert;
await certificateBodyDAL.create(
{
certId: txCert.id,
encryptedCertificate
},
tx
);
if (collectionId) {
await pkiCollectionItemDAL.create(
{
pkiCollectionId: collectionId,
certId: txCert.id
},
tx
);
}
return txCert;
} catch (error: unknown) {
if (
typeof error === "object" &&
error !== null &&
"error" in error &&
error.error &&
typeof error.error === "object" &&
"code" in error.error &&
error.error.code === "23505"
) {
throw new BadRequestError({ message: "Certificate serial already exists in your project" });
}
throw error;
}
});
return {
certificate: certificatePem,
certificateChain: chainPem,
privateKey: privateKeyPem,
serialNumber
serialNumber,
cert
};
};
@@ -81,6 +81,6 @@ export type TImportCertDTO = {
pkiCollectionId?: string;
certificatePem: string;
privateKeyPem?: string;
chainPem?: string;
privateKeyPem: string;
chainPem: string;
} & Omit<TProjectPermission, "projectId">;
@@ -269,14 +269,8 @@ export const pkiCollectionServiceFactory = ({
});
if (isCertAdded) throw new BadRequestError({ message: "Certificate already part of the PKI collection" });
// validate that there exists a certificate in same project as PKI collection
const cas = await certificateAuthorityDAL.find({ projectId: pkiCollection.projectId });
// TODO: consider making this more efficient
const [certificate] = await certificateDAL.find({
$in: {
caId: cas.map((ca) => ca.id)
},
projectId: pkiCollection.projectId,
id: itemId
});
if (!certificate) throw new NotFoundError({ message: `Certificate with ID '${itemId}' not found` });
@@ -929,13 +929,9 @@ export const projectServiceFactory = ({
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Certificates);
const cas = await certificateAuthorityDAL.find({ projectId });
const certificates = await certificateDAL.find(
{
$in: {
caId: cas.map((ca) => ca.id)
},
projectId,
...(friendlyName && { friendlyName }),
...(commonName && { commonName })
},
@@ -7,6 +7,7 @@ import { ProjectType, SecretsV2Schema, SecretType, TableName, TSecretsV2, TSecre
import { TKeyStoreFactory } from "@app/keystore/keystore";
import { getConfig } from "@app/lib/config/env";
import { generateCacheKeyFromData } from "@app/lib/crypto/cache";
import { applyJitter } from "@app/lib/dates";
import { BadRequestError, DatabaseError, NotFoundError } from "@app/lib/errors";
import {
buildFindFilter,
@@ -22,7 +23,6 @@ import type {
TFindSecretsByFolderIdsFilter,
TGetSecretsDTO
} from "@app/services/secret-v2-bridge/secret-v2-bridge-types";
import { applyJitter } from "@app/lib/dates";
export const SecretServiceCacheKeys = {
get productKey() {