mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-04 15:26:20 +00:00
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:
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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."
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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<TCertificateTemplateDALFactory, "getById" | "find">;
|
||||
certificateAuthorityQueue: TCertificateAuthorityQueueFactory; // TODO: Pick
|
||||
certificateDAL: Pick<TCertificateDALFactory, "transaction" | "create" | "find">;
|
||||
certificateSecretDAL: Pick<TCertificateSecretDALFactory, "create">;
|
||||
certificateBodyDAL: Pick<TCertificateBodyDALFactory, "create">;
|
||||
pkiCollectionDAL: Pick<TPkiCollectionDALFactory, "findById">;
|
||||
pkiCollectionItemDAL: Pick<TPkiCollectionItemDALFactory, "create">;
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -5,6 +5,6 @@ import { ormify } from "@app/lib/knex";
|
||||
export type TCertificateSecretDALFactory = ReturnType<typeof certificateSecretDALFactory>;
|
||||
|
||||
export const certificateSecretDALFactory = (db: TDbClient) => {
|
||||
const caSecretOrm = ormify(db, TableName.CertificateSecret);
|
||||
return caSecretOrm;
|
||||
const certSecretOrm = ormify(db, TableName.CertificateSecret);
|
||||
return certSecretOrm;
|
||||
};
|
||||
|
||||
@@ -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<TCertificateDALFactory, "findOne" | "deleteById" | "update" | "find">;
|
||||
@@ -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
|
||||
};
|
||||
};
|
||||
|
||||
@@ -78,6 +78,10 @@ export type TGetCertBodyDTO = {
|
||||
serialNumber: string;
|
||||
} & Omit<TProjectPermission, "projectId">;
|
||||
|
||||
export type TGetCertBundleDTO = {
|
||||
serialNumber: string;
|
||||
} & Omit<TProjectPermission, "projectId">;
|
||||
|
||||
export type TGetCertificateCredentialsDTO = {
|
||||
certId: string;
|
||||
projectId: string;
|
||||
|
||||
Reference in New Issue
Block a user