mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-09-22 13:39:35 +00:00
Merge branch 'ENG-2661' into feat/acme-and-external-ca
This commit is contained in:
@@ -0,0 +1,44 @@
|
||||
import { Knex } from "knex";
|
||||
|
||||
import { TableName } from "../schemas";
|
||||
|
||||
export async function up(knex: Knex): Promise<void> {
|
||||
if (await knex.schema.hasTable(TableName.Certificate)) {
|
||||
const hasProjectIdColumn = await knex.schema.hasColumn(TableName.Certificate, "projectId");
|
||||
if (!hasProjectIdColumn) {
|
||||
await knex.schema.alterTable(TableName.Certificate, (t) => {
|
||||
t.string("projectId", 36).nullable();
|
||||
t.foreign("projectId").references("id").inTable(TableName.Project).onDelete("CASCADE");
|
||||
});
|
||||
|
||||
await knex.raw(`
|
||||
UPDATE "${TableName.Certificate}" cert
|
||||
SET "projectId" = ca."projectId"
|
||||
FROM "${TableName.CertificateAuthority}" ca
|
||||
WHERE cert."caId" = ca.id
|
||||
`);
|
||||
|
||||
await knex.schema.alterTable(TableName.Certificate, (t) => {
|
||||
t.string("projectId").notNullable().alter();
|
||||
});
|
||||
}
|
||||
|
||||
await knex.schema.alterTable(TableName.Certificate, (t) => {
|
||||
t.uuid("caId").nullable().alter();
|
||||
t.uuid("caCertId").nullable().alter();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export async function down(knex: Knex): Promise<void> {
|
||||
if (await knex.schema.hasTable(TableName.Certificate)) {
|
||||
if (await knex.schema.hasColumn(TableName.Certificate, "projectId")) {
|
||||
await knex.schema.alterTable(TableName.Certificate, (t) => {
|
||||
t.dropForeign("projectId");
|
||||
t.dropColumn("projectId");
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Altering back to notNullable for caId and caCertId will fail
|
||||
}
|
||||
@@ -11,7 +11,7 @@ export const CertificatesSchema = z.object({
|
||||
id: z.string().uuid(),
|
||||
createdAt: z.date(),
|
||||
updatedAt: z.date(),
|
||||
caId: z.string().uuid(),
|
||||
caId: z.string().uuid().nullable().optional(),
|
||||
status: z.string(),
|
||||
serialNumber: z.string(),
|
||||
friendlyName: z.string(),
|
||||
@@ -21,10 +21,11 @@ export const CertificatesSchema = z.object({
|
||||
revokedAt: z.date().nullable().optional(),
|
||||
revocationReason: z.number().nullable().optional(),
|
||||
altNames: z.string().nullable().optional(),
|
||||
caCertId: z.string().uuid(),
|
||||
caCertId: z.string().uuid().nullable().optional(),
|
||||
certificateTemplateId: z.string().uuid().nullable().optional(),
|
||||
keyUsages: z.string().array().nullable().optional(),
|
||||
extendedKeyUsages: z.string().array().nullable().optional(),
|
||||
projectId: z.string(),
|
||||
pkiSubscriberId: z.string().uuid().nullable().optional()
|
||||
});
|
||||
|
||||
|
||||
@@ -235,6 +235,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",
|
||||
@@ -1812,6 +1813,15 @@ interface IssueCert {
|
||||
};
|
||||
}
|
||||
|
||||
interface ImportCert {
|
||||
type: EventType.IMPORT_CERT;
|
||||
metadata: {
|
||||
certId: string;
|
||||
cn: string;
|
||||
serialNumber: string;
|
||||
};
|
||||
}
|
||||
|
||||
interface SignCert {
|
||||
type: EventType.SIGN_CERT;
|
||||
metadata: {
|
||||
@@ -2987,6 +2997,7 @@ export type Event =
|
||||
| ImportCaCert
|
||||
| GetCaCrls
|
||||
| IssueCert
|
||||
| ImportCert
|
||||
| SignCert
|
||||
| GetCaCertificateTemplates
|
||||
| GetCert
|
||||
|
||||
@@ -1669,6 +1669,19 @@ export const CERTIFICATES = {
|
||||
certificateChain: "The certificate chain of the certificate.",
|
||||
serialNumberRes: "The serial number of the certificate.",
|
||||
privateKey: "The private key of the certificate."
|
||||
},
|
||||
IMPORT: {
|
||||
projectSlug: "Slug of the project to import the certificate into.",
|
||||
certificatePem: "The PEM-encoded leaf certificate.",
|
||||
privateKeyPem: "The PEM-encoded private key corresponding to the certificate.",
|
||||
chainPem: "The PEM-encoded chain of intermediate certificates.",
|
||||
friendlyName: "A friendly name for the certificate.",
|
||||
pkiCollectionId: "The ID of the PKI collection to add the certificate to.",
|
||||
|
||||
certificate: "The issued certificate.",
|
||||
certificateChain: "The certificate chain of the issued certificate.",
|
||||
privateKey: "The private key of the issued certificate.",
|
||||
serialNumber: "The serial number of the issued certificate."
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -847,7 +847,9 @@ export const registerRoutes = async (
|
||||
certificateAuthoritySecretDAL,
|
||||
projectDAL,
|
||||
kmsService,
|
||||
permissionService
|
||||
permissionService,
|
||||
pkiCollectionDAL,
|
||||
pkiCollectionItemDAL
|
||||
});
|
||||
|
||||
const sshCertificateAuthorityService = sshCertificateAuthorityServiceFactory({
|
||||
|
||||
@@ -39,7 +39,7 @@ export const registerCertRouter = async (server: FastifyZodProvider) => {
|
||||
}
|
||||
},
|
||||
handler: async (req) => {
|
||||
const { cert, ca } = await server.services.certificate.getCert({
|
||||
const { cert } = await server.services.certificate.getCert({
|
||||
serialNumber: req.params.serialNumber,
|
||||
actor: req.permission.type,
|
||||
actorId: req.permission.id,
|
||||
@@ -49,7 +49,7 @@ export const registerCertRouter = async (server: FastifyZodProvider) => {
|
||||
|
||||
await server.services.auditLog.createAuditLog({
|
||||
...req.auditLogInfo,
|
||||
projectId: ca.projectId,
|
||||
projectId: cert.projectId,
|
||||
event: {
|
||||
type: EventType.GET_CERT,
|
||||
metadata: {
|
||||
@@ -86,7 +86,7 @@ export const registerCertRouter = async (server: FastifyZodProvider) => {
|
||||
}
|
||||
},
|
||||
handler: async (req, reply) => {
|
||||
const { ca, cert, certPrivateKey } = await server.services.certificate.getCertPrivateKey({
|
||||
const { cert, certPrivateKey } = await server.services.certificate.getCertPrivateKey({
|
||||
serialNumber: req.params.serialNumber,
|
||||
actor: req.permission.type,
|
||||
actorId: req.permission.id,
|
||||
@@ -96,7 +96,7 @@ export const registerCertRouter = async (server: FastifyZodProvider) => {
|
||||
|
||||
await server.services.auditLog.createAuditLog({
|
||||
...req.auditLogInfo,
|
||||
projectId: ca.projectId,
|
||||
projectId: cert.projectId,
|
||||
event: {
|
||||
type: EventType.GET_CERT_PRIVATE_KEY,
|
||||
metadata: {
|
||||
@@ -131,14 +131,14 @@ export const registerCertRouter = async (server: FastifyZodProvider) => {
|
||||
response: {
|
||||
200: z.object({
|
||||
certificate: z.string().trim().describe(CERTIFICATES.GET_CERT.certificate),
|
||||
certificateChain: z.string().trim().nullish().describe(CERTIFICATES.GET_CERT.certificateChain),
|
||||
certificateChain: z.string().trim().nullable().describe(CERTIFICATES.GET_CERT.certificateChain),
|
||||
privateKey: z.string().trim().describe(CERTIFICATES.GET_CERT.privateKey),
|
||||
serialNumber: z.string().trim().describe(CERTIFICATES.GET_CERT.serialNumberRes)
|
||||
})
|
||||
}
|
||||
},
|
||||
handler: async (req, reply) => {
|
||||
const { certificate, certificateChain, serialNumber, cert, ca, privateKey } =
|
||||
const { certificate, certificateChain, serialNumber, cert, privateKey } =
|
||||
await server.services.certificate.getCertBundle({
|
||||
serialNumber: req.params.serialNumber,
|
||||
actor: req.permission.type,
|
||||
@@ -149,7 +149,7 @@ export const registerCertRouter = async (server: FastifyZodProvider) => {
|
||||
|
||||
await server.services.auditLog.createAuditLog({
|
||||
...req.auditLogInfo,
|
||||
projectId: ca.projectId,
|
||||
projectId: cert.projectId,
|
||||
event: {
|
||||
type: EventType.GET_CERT_BUNDLE,
|
||||
metadata: {
|
||||
@@ -284,6 +284,68 @@ export const registerCertRouter = async (server: FastifyZodProvider) => {
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "POST",
|
||||
url: "/import-certificate",
|
||||
config: {
|
||||
rateLimit: writeLimit
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||
schema: {
|
||||
hide: false,
|
||||
tags: [ApiDocsTags.PkiCertificates],
|
||||
description: "Import certificate",
|
||||
body: z.object({
|
||||
projectSlug: z.string().trim().min(1).describe(CERTIFICATES.IMPORT.projectSlug),
|
||||
|
||||
certificatePem: z.string().trim().min(1).describe(CERTIFICATES.IMPORT.certificatePem),
|
||||
privateKeyPem: z.string().trim().min(1).describe(CERTIFICATES.IMPORT.privateKeyPem),
|
||||
chainPem: z.string().trim().min(1).describe(CERTIFICATES.IMPORT.chainPem),
|
||||
|
||||
friendlyName: z.string().trim().optional().describe(CERTIFICATES.IMPORT.friendlyName),
|
||||
pkiCollectionId: z.string().trim().optional().describe(CERTIFICATES.IMPORT.pkiCollectionId)
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
certificate: z.string().trim().describe(CERTIFICATES.IMPORT.certificate),
|
||||
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, 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
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
certificate,
|
||||
certificateChain,
|
||||
privateKey,
|
||||
serialNumber
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "POST",
|
||||
url: "/sign-certificate",
|
||||
@@ -474,7 +536,7 @@ export const registerCertRouter = async (server: FastifyZodProvider) => {
|
||||
}
|
||||
},
|
||||
handler: async (req) => {
|
||||
const { deletedCert, ca } = await server.services.certificate.deleteCert({
|
||||
const { deletedCert } = await server.services.certificate.deleteCert({
|
||||
serialNumber: req.params.serialNumber,
|
||||
actor: req.permission.type,
|
||||
actorId: req.permission.id,
|
||||
@@ -484,7 +546,7 @@ export const registerCertRouter = async (server: FastifyZodProvider) => {
|
||||
|
||||
await server.services.auditLog.createAuditLog({
|
||||
...req.auditLogInfo,
|
||||
projectId: ca.projectId,
|
||||
projectId: deletedCert.projectId,
|
||||
event: {
|
||||
type: EventType.DELETE_CERT,
|
||||
metadata: {
|
||||
@@ -518,13 +580,13 @@ export const registerCertRouter = async (server: FastifyZodProvider) => {
|
||||
response: {
|
||||
200: z.object({
|
||||
certificate: z.string().trim().describe(CERTIFICATES.GET_CERT.certificate),
|
||||
certificateChain: z.string().trim().nullish().describe(CERTIFICATES.GET_CERT.certificateChain),
|
||||
certificateChain: z.string().trim().nullable().describe(CERTIFICATES.GET_CERT.certificateChain),
|
||||
serialNumber: z.string().trim().describe(CERTIFICATES.GET_CERT.serialNumberRes)
|
||||
})
|
||||
}
|
||||
},
|
||||
handler: async (req) => {
|
||||
const { certificate, certificateChain, serialNumber, cert, ca } = await server.services.certificate.getCertBody({
|
||||
const { certificate, certificateChain, serialNumber, cert } = await server.services.certificate.getCertBody({
|
||||
serialNumber: req.params.serialNumber,
|
||||
actor: req.permission.type,
|
||||
actorId: req.permission.id,
|
||||
@@ -534,7 +596,7 @@ export const registerCertRouter = async (server: FastifyZodProvider) => {
|
||||
|
||||
await server.services.auditLog.createAuditLog({
|
||||
...req.auditLogInfo,
|
||||
projectId: ca.projectId,
|
||||
projectId: cert.projectId,
|
||||
event: {
|
||||
type: EventType.GET_CERT_BODY,
|
||||
metadata: {
|
||||
|
||||
@@ -5,7 +5,7 @@ import * as x509 from "@peculiar/x509";
|
||||
import { BadRequestError, NotFoundError } from "@app/lib/errors";
|
||||
|
||||
import { getProjectKmsCertificateKeyId } from "../project/project-fns";
|
||||
import { CrlReason, TBuildCertificateChainDTO, TGetCertificateCredentialsDTO } from "./certificate-types";
|
||||
import { CrlReason, TGetCertificateCredentialsDTO } from "./certificate-types";
|
||||
|
||||
export const revocationReasonToCrlCode = (crlReason: CrlReason) => {
|
||||
switch (crlReason) {
|
||||
@@ -52,6 +52,9 @@ export const constructPemChainFromCerts = (certificates: x509.X509Certificate[])
|
||||
.join("\n")
|
||||
.trim();
|
||||
|
||||
export const splitPemChain = (pemText: string) =>
|
||||
pemText.match(/-----BEGIN CERTIFICATE-----[^-]+-----END CERTIFICATE-----/g) || [];
|
||||
|
||||
/**
|
||||
* Return the public and private key of certificate
|
||||
* Note: credentials are returned as PEM strings
|
||||
@@ -95,29 +98,3 @@ export const getCertificateCredentials = async ({
|
||||
throw new BadRequestError({ message: `Failed to process private key for certificate with ID '${certId}'` });
|
||||
}
|
||||
};
|
||||
|
||||
// If the certificate was generated after ~05/01/25 it will have a encryptedCertificateChain attached to it's body
|
||||
// Otherwise we'll fallback to manually building the chain
|
||||
export const buildCertificateChain = async ({
|
||||
caCert,
|
||||
caCertChain,
|
||||
encryptedCertificateChain,
|
||||
kmsService,
|
||||
kmsId
|
||||
}: TBuildCertificateChainDTO) => {
|
||||
if (!encryptedCertificateChain && (!caCert || !caCertChain)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
let certificateChain = `${caCert}\n${caCertChain}`.trim();
|
||||
|
||||
if (encryptedCertificateChain) {
|
||||
const kmsDecryptor = await kmsService.decryptWithKmsKey({ kmsId });
|
||||
const decryptedCertChain = await kmsDecryptor({
|
||||
cipherTextBlob: encryptedCertificateChain
|
||||
});
|
||||
certificateChain = decryptedCertChain.toString();
|
||||
}
|
||||
|
||||
return certificateChain;
|
||||
};
|
||||
|
||||
@@ -1,19 +1,23 @@
|
||||
import { ForbiddenError } from "@casl/ability";
|
||||
import * as x509 from "@peculiar/x509";
|
||||
import { createPrivateKey, createPublicKey, sign, verify } from "crypto";
|
||||
|
||||
import { ActionProjectType } from "@app/db/schemas";
|
||||
import { ActionProjectType, ProjectType } from "@app/db/schemas";
|
||||
import { TCertificateAuthorityCrlDALFactory } from "@app/ee/services/certificate-authority-crl/certificate-authority-crl-dal";
|
||||
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
|
||||
import {
|
||||
ProjectPermissionCertificateActions,
|
||||
ProjectPermissionSub
|
||||
} from "@app/ee/services/permission/project-permission";
|
||||
import { BadRequestError, NotFoundError } from "@app/lib/errors";
|
||||
import { TCertificateBodyDALFactory } from "@app/services/certificate/certificate-body-dal";
|
||||
import { TCertificateDALFactory } from "@app/services/certificate/certificate-dal";
|
||||
import { TCertificateAuthorityCertDALFactory } from "@app/services/certificate-authority/certificate-authority-cert-dal";
|
||||
import { TCertificateAuthorityDALFactory } from "@app/services/certificate-authority/certificate-authority-dal";
|
||||
import { TCertificateAuthoritySecretDALFactory } from "@app/services/certificate-authority/certificate-authority-secret-dal";
|
||||
import { TKmsServiceFactory } from "@app/services/kms/kms-service";
|
||||
import { TPkiCollectionDALFactory } from "@app/services/pki-collection/pki-collection-dal";
|
||||
import { TPkiCollectionItemDALFactory } from "@app/services/pki-collection/pki-collection-item-dal";
|
||||
import { TProjectDALFactory } from "@app/services/project/project-dal";
|
||||
import { getProjectKmsCertificateKeyId } from "@app/services/project/project-fns";
|
||||
|
||||
@@ -21,12 +25,16 @@ import { expandInternalCa, getCaCertChain, rebuildCaCrl } from "../certificate-a
|
||||
import { buildCertificateChain, getCertificateCredentials, revocationReasonToCrlCode } from "./certificate-fns";
|
||||
import { TCertificateSecretDALFactory } from "./certificate-secret-dal";
|
||||
import {
|
||||
CertExtendedKeyUsage,
|
||||
CertExtendedKeyUsageOIDToName,
|
||||
CertKeyUsage,
|
||||
CertStatus,
|
||||
TDeleteCertDTO,
|
||||
TGetCertBodyDTO,
|
||||
TGetCertBundleDTO,
|
||||
TGetCertDTO,
|
||||
TGetCertPrivateKeyDTO,
|
||||
TImportCertDTO,
|
||||
TRevokeCertDTO
|
||||
} from "./certificate-types";
|
||||
|
||||
@@ -38,7 +46,12 @@ type TCertificateServiceFactoryDep = {
|
||||
certificateAuthorityCertDAL: Pick<TCertificateAuthorityCertDALFactory, "findById">;
|
||||
certificateAuthorityCrlDAL: Pick<TCertificateAuthorityCrlDALFactory, "update">;
|
||||
certificateAuthoritySecretDAL: Pick<TCertificateAuthoritySecretDALFactory, "findOne">;
|
||||
projectDAL: Pick<TProjectDALFactory, "findOne" | "updateById" | "findById" | "transaction">;
|
||||
pkiCollectionDAL: Pick<TPkiCollectionDALFactory, "findById">;
|
||||
pkiCollectionItemDAL: Pick<TPkiCollectionItemDALFactory, "create">;
|
||||
projectDAL: Pick<
|
||||
TProjectDALFactory,
|
||||
"findProjectBySlug" | "findOne" | "updateById" | "findById" | "transaction" | "getProjectFromSplitId"
|
||||
>;
|
||||
kmsService: Pick<TKmsServiceFactory, "generateKmsKey" | "encryptWithKmsKey" | "decryptWithKmsKey">;
|
||||
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission">;
|
||||
};
|
||||
@@ -53,6 +66,8 @@ export const certificateServiceFactory = ({
|
||||
certificateAuthorityCertDAL,
|
||||
certificateAuthorityCrlDAL,
|
||||
certificateAuthoritySecretDAL,
|
||||
pkiCollectionDAL,
|
||||
pkiCollectionItemDAL,
|
||||
projectDAL,
|
||||
kmsService,
|
||||
permissionService
|
||||
@@ -67,7 +82,7 @@ export const certificateServiceFactory = ({
|
||||
const { permission } = await permissionService.getProjectPermission({
|
||||
actor,
|
||||
actorId,
|
||||
projectId: ca.projectId,
|
||||
projectId: cert.projectId,
|
||||
actorAuthMethod,
|
||||
actorOrgId,
|
||||
actionProjectType: ActionProjectType.CertificateManager
|
||||
@@ -100,7 +115,7 @@ export const certificateServiceFactory = ({
|
||||
const { permission } = await permissionService.getProjectPermission({
|
||||
actor,
|
||||
actorId,
|
||||
projectId: ca.projectId,
|
||||
projectId: cert.projectId,
|
||||
actorAuthMethod,
|
||||
actorOrgId,
|
||||
actionProjectType: ActionProjectType.CertificateManager
|
||||
@@ -113,7 +128,7 @@ export const certificateServiceFactory = ({
|
||||
|
||||
const { certPrivateKey } = await getCertificateCredentials({
|
||||
certId: cert.id,
|
||||
projectId: ca.projectId,
|
||||
projectId: cert.projectId,
|
||||
certificateSecretDAL,
|
||||
projectDAL,
|
||||
kmsService
|
||||
@@ -136,7 +151,7 @@ export const certificateServiceFactory = ({
|
||||
const { permission } = await permissionService.getProjectPermission({
|
||||
actor,
|
||||
actorId,
|
||||
projectId: ca.projectId,
|
||||
projectId: cert.projectId,
|
||||
actorAuthMethod,
|
||||
actorOrgId,
|
||||
actionProjectType: ActionProjectType.CertificateManager
|
||||
@@ -224,7 +239,7 @@ export const certificateServiceFactory = ({
|
||||
const { permission } = await permissionService.getProjectPermission({
|
||||
actor,
|
||||
actorId,
|
||||
projectId: ca.projectId,
|
||||
projectId: cert.projectId,
|
||||
actorAuthMethod,
|
||||
actorOrgId,
|
||||
actionProjectType: ActionProjectType.CertificateManager
|
||||
@@ -238,7 +253,7 @@ export const certificateServiceFactory = ({
|
||||
const certBody = await certificateBodyDAL.findOne({ certId: cert.id });
|
||||
|
||||
const certificateManagerKeyId = await getProjectKmsCertificateKeyId({
|
||||
projectId: ca.projectId,
|
||||
projectId: cert.projectId,
|
||||
projectDAL,
|
||||
kmsService
|
||||
});
|
||||
@@ -252,21 +267,26 @@ export const certificateServiceFactory = ({
|
||||
|
||||
const certObj = new x509.X509Certificate(decryptedCert);
|
||||
|
||||
const { caCert, caCertChain } = await getCaCertChain({
|
||||
caCertId: cert.caCertId,
|
||||
certificateAuthorityDAL,
|
||||
certificateAuthorityCertDAL,
|
||||
projectDAL,
|
||||
kmsService
|
||||
});
|
||||
let certificateChain = null;
|
||||
|
||||
const certificateChain = await buildCertificateChain({
|
||||
caCert,
|
||||
caCertChain,
|
||||
kmsId: certificateManagerKeyId,
|
||||
kmsService,
|
||||
encryptedCertificateChain: certBody.encryptedCertificateChain || undefined
|
||||
});
|
||||
// On newer certs the certBody.encryptedCertificateChain column will always exist.
|
||||
// Older certs will have a caCertId which will be used as a fallback mechanism for structuring the chain.
|
||||
if (certBody.encryptedCertificateChain) {
|
||||
const decryptedCertChain = await kmsDecryptor({
|
||||
cipherTextBlob: certBody.encryptedCertificateChain
|
||||
});
|
||||
certificateChain = decryptedCertChain.toString();
|
||||
} else if (cert.caCertId) {
|
||||
const { caCert, caCertChain } = await getCaCertChain({
|
||||
caCertId: cert.caCertId,
|
||||
certificateAuthorityDAL,
|
||||
certificateAuthorityCertDAL,
|
||||
projectDAL,
|
||||
kmsService
|
||||
});
|
||||
|
||||
certificateChain = `${caCert}\n${caCertChain}`.trim();
|
||||
}
|
||||
|
||||
return {
|
||||
certificate: certObj.toString("pem"),
|
||||
@@ -288,7 +308,7 @@ export const certificateServiceFactory = ({
|
||||
const { permission } = await permissionService.getProjectPermission({
|
||||
actor,
|
||||
actorId,
|
||||
projectId: ca.projectId,
|
||||
projectId: cert.projectId,
|
||||
actorAuthMethod,
|
||||
actorOrgId,
|
||||
actionProjectType: ActionProjectType.CertificateManager
|
||||
@@ -306,7 +326,7 @@ export const certificateServiceFactory = ({
|
||||
const certBody = await certificateBodyDAL.findOne({ certId: cert.id });
|
||||
|
||||
const certificateManagerKeyId = await getProjectKmsCertificateKeyId({
|
||||
projectId: ca.projectId,
|
||||
projectId: cert.projectId,
|
||||
projectDAL,
|
||||
kmsService
|
||||
});
|
||||
@@ -321,25 +341,30 @@ export const certificateServiceFactory = ({
|
||||
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 = null;
|
||||
|
||||
const certificateChain = await buildCertificateChain({
|
||||
caCert,
|
||||
caCertChain,
|
||||
kmsId: certificateManagerKeyId,
|
||||
kmsService,
|
||||
encryptedCertificateChain: certBody.encryptedCertificateChain || undefined
|
||||
});
|
||||
// On newer certs the certBody.encryptedCertificateChain column will always exist.
|
||||
// Older certs will have a caCertId which will be used as a fallback mechanism for structuring the chain.
|
||||
if (certBody.encryptedCertificateChain) {
|
||||
const decryptedCertChain = await kmsDecryptor({
|
||||
cipherTextBlob: certBody.encryptedCertificateChain
|
||||
});
|
||||
certificateChain = decryptedCertChain.toString();
|
||||
} else if (cert.caCertId) {
|
||||
const { caCert, caCertChain } = await getCaCertChain({
|
||||
caCertId: cert.caCertId,
|
||||
certificateAuthorityDAL,
|
||||
certificateAuthorityCertDAL,
|
||||
projectDAL,
|
||||
kmsService
|
||||
});
|
||||
|
||||
certificateChain = `${caCert}\n${caCertChain}`.trim();
|
||||
}
|
||||
|
||||
const { certPrivateKey } = await getCertificateCredentials({
|
||||
certId: cert.id,
|
||||
projectId: ca.projectId,
|
||||
projectId: cert.projectId,
|
||||
certificateSecretDAL,
|
||||
projectDAL,
|
||||
kmsService
|
||||
@@ -361,6 +386,7 @@ export const certificateServiceFactory = ({
|
||||
deleteCert,
|
||||
revokeCert,
|
||||
getCertBody,
|
||||
importCert,
|
||||
getCertBundle
|
||||
};
|
||||
};
|
||||
|
||||
@@ -78,6 +78,17 @@ export type TGetCertBodyDTO = {
|
||||
serialNumber: string;
|
||||
} & Omit<TProjectPermission, "projectId">;
|
||||
|
||||
export type TImportCertDTO = {
|
||||
projectSlug: string;
|
||||
|
||||
friendlyName?: string;
|
||||
pkiCollectionId?: string;
|
||||
|
||||
certificatePem: string;
|
||||
privateKeyPem: string;
|
||||
chainPem: string;
|
||||
} & Omit<TProjectPermission, "projectId">;
|
||||
|
||||
export type TGetCertPrivateKeyDTO = {
|
||||
serialNumber: string;
|
||||
} & Omit<TProjectPermission, "projectId">;
|
||||
@@ -93,11 +104,3 @@ export type TGetCertificateCredentialsDTO = {
|
||||
projectDAL: Pick<TProjectDALFactory, "findOne" | "updateById" | "transaction">;
|
||||
kmsService: Pick<TKmsServiceFactory, "decryptWithKmsKey" | "generateKmsKey">;
|
||||
};
|
||||
|
||||
export type TBuildCertificateChainDTO = {
|
||||
caCert?: string;
|
||||
caCertChain?: string;
|
||||
encryptedCertificateChain?: Buffer;
|
||||
kmsService: Pick<TKmsServiceFactory, "decryptWithKmsKey">;
|
||||
kmsId: string;
|
||||
};
|
||||
|
||||
@@ -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` });
|
||||
|
||||
@@ -611,7 +611,8 @@ export const pkiSubscriberServiceFactory = ({
|
||||
notBefore: notBeforeDate,
|
||||
notAfter: notAfterDate,
|
||||
keyUsages: selectedKeyUsages,
|
||||
extendedKeyUsages: selectedExtendedKeyUsages
|
||||
extendedKeyUsages: selectedExtendedKeyUsages,
|
||||
projectId
|
||||
},
|
||||
tx
|
||||
);
|
||||
|
||||
@@ -969,13 +969,9 @@ export const projectServiceFactory = ({
|
||||
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 })
|
||||
},
|
||||
|
||||
@@ -68,6 +68,7 @@ export const eventToNameMap: { [K in EventType]: string } = {
|
||||
[EventType.IMPORT_CA_CERT]: "Import CA certificate",
|
||||
[EventType.GET_CA_CRL]: "Get CA CRL",
|
||||
[EventType.ISSUE_CERT]: "Issue certificate",
|
||||
[EventType.IMPORT_CERT]: "Import certificate",
|
||||
[EventType.GET_CERT]: "Get certificate",
|
||||
[EventType.DELETE_CERT]: "Delete certificate",
|
||||
[EventType.REVOKE_CERT]: "Revoke certificate",
|
||||
|
||||
@@ -81,6 +81,7 @@ export enum EventType {
|
||||
IMPORT_CA_CERT = "import-certificate-authority-cert",
|
||||
GET_CA_CRL = "get-certificate-authority-crl",
|
||||
ISSUE_CERT = "issue-cert",
|
||||
IMPORT_CERT = "import-cert",
|
||||
GET_CERT = "get-cert",
|
||||
DELETE_CERT = "delete-cert",
|
||||
REVOKE_CERT = "revoke-cert",
|
||||
|
||||
@@ -583,6 +583,14 @@ interface IssueCert {
|
||||
serialNumber: string;
|
||||
};
|
||||
}
|
||||
interface ImportCert {
|
||||
type: EventType.IMPORT_CERT;
|
||||
metadata: {
|
||||
certId: string;
|
||||
cn: string;
|
||||
serialNumber: string;
|
||||
};
|
||||
}
|
||||
|
||||
interface GetCert {
|
||||
type: EventType.GET_CERT;
|
||||
@@ -895,6 +903,7 @@ export type Event =
|
||||
| ImportCaCert
|
||||
| GetCaCrl
|
||||
| IssueCert
|
||||
| ImportCert
|
||||
| GetCert
|
||||
| DeleteCert
|
||||
| RevokeCert
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
export { useDeleteCert, useRevokeCert } from "./mutations";
|
||||
export { useDeleteCert, useImportCertificate, useRevokeCert } from "./mutations";
|
||||
export { useGetCert, useGetCertBody } from "./queries";
|
||||
|
||||
@@ -4,7 +4,13 @@ import { apiRequest } from "@app/config/request";
|
||||
|
||||
import { pkiSubscriberKeys } from "../pkiSubscriber/queries";
|
||||
import { workspaceKeys } from "../workspace";
|
||||
import { TCertificate, TDeleteCertDTO, TRevokeCertDTO } from "./types";
|
||||
import {
|
||||
TCertificate,
|
||||
TDeleteCertDTO,
|
||||
TImportCertificateDTO,
|
||||
TImportCertificateResponse,
|
||||
TRevokeCertDTO
|
||||
} from "./types";
|
||||
|
||||
export const useDeleteCert = () => {
|
||||
const queryClient = useQueryClient();
|
||||
@@ -49,3 +55,21 @@ export const useRevokeCert = () => {
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
export const useImportCertificate = () => {
|
||||
const queryClient = useQueryClient();
|
||||
return useMutation<TImportCertificateResponse, object, TImportCertificateDTO>({
|
||||
mutationFn: async (body) => {
|
||||
const { data } = await apiRequest.post<TImportCertificateResponse>(
|
||||
"/api/v1/pki/certificates/import-certificate",
|
||||
body
|
||||
);
|
||||
return data;
|
||||
},
|
||||
onSuccess: (_, { projectSlug }) => {
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: workspaceKeys.forWorkspaceCertificates(projectSlug)
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
@@ -25,3 +25,21 @@ export type TRevokeCertDTO = {
|
||||
serialNumber: string;
|
||||
revocationReason: string;
|
||||
};
|
||||
|
||||
export type TImportCertificateDTO = {
|
||||
projectSlug: string;
|
||||
|
||||
certificatePem: string;
|
||||
privateKeyPem: string;
|
||||
chainPem: string;
|
||||
|
||||
pkiCollectionId?: string;
|
||||
friendlyName?: string;
|
||||
};
|
||||
|
||||
export type TImportCertificateResponse = {
|
||||
certificate: string;
|
||||
certificateChain: string;
|
||||
privateKey: string;
|
||||
serialNumber: string;
|
||||
};
|
||||
|
||||
@@ -0,0 +1,244 @@
|
||||
import { useState } from "react";
|
||||
import { Controller, useForm } from "react-hook-form";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { z } from "zod";
|
||||
|
||||
import { createNotification } from "@app/components/notifications";
|
||||
import {
|
||||
Button,
|
||||
FormControl,
|
||||
Input,
|
||||
Modal,
|
||||
ModalContent,
|
||||
Select,
|
||||
SelectItem,
|
||||
TextArea
|
||||
} from "@app/components/v2";
|
||||
import { useWorkspace } from "@app/context";
|
||||
import { useGetCert, useImportCertificate, useListWorkspacePkiCollections } from "@app/hooks/api";
|
||||
import { UsePopUpState } from "@app/hooks/usePopUp";
|
||||
|
||||
import { CertificateContent } from "./CertificateContent";
|
||||
|
||||
const schema = z.object({
|
||||
certificatePem: z.string().trim().min(1, "Certificate PEM is required"),
|
||||
privateKeyPem: z.string().trim().min(1, "Private Key PEM is required"),
|
||||
chainPem: z.string().trim().min(1, "Certificate Chain PEM is required"),
|
||||
|
||||
friendlyName: z.string(),
|
||||
collectionId: z.string().optional()
|
||||
});
|
||||
|
||||
export type FormData = z.infer<typeof schema>;
|
||||
|
||||
type Props = {
|
||||
popUp: UsePopUpState<["certificateImport"]>;
|
||||
handlePopUpToggle: (
|
||||
popUpName: keyof UsePopUpState<["certificateImport"]>,
|
||||
state?: boolean
|
||||
) => void;
|
||||
};
|
||||
|
||||
type TCertificateDetails = {
|
||||
serialNumber: string;
|
||||
certificate: string;
|
||||
certificateChain: string;
|
||||
privateKey: string;
|
||||
};
|
||||
|
||||
export const CertificateImportModal = ({ popUp, handlePopUpToggle }: Props) => {
|
||||
const [certificateDetails, setCertificateDetails] = useState<TCertificateDetails | null>(null);
|
||||
const { currentWorkspace } = useWorkspace();
|
||||
const { data: cert } = useGetCert(
|
||||
(popUp?.certificateImport?.data as { serialNumber: string })?.serialNumber || ""
|
||||
);
|
||||
|
||||
const { data } = useListWorkspacePkiCollections({
|
||||
workspaceId: currentWorkspace?.id || ""
|
||||
});
|
||||
|
||||
const { mutateAsync: importCertificate } = useImportCertificate();
|
||||
|
||||
const {
|
||||
control,
|
||||
handleSubmit,
|
||||
reset,
|
||||
formState: { isSubmitting }
|
||||
} = useForm<FormData>({
|
||||
resolver: zodResolver(schema)
|
||||
});
|
||||
|
||||
const onFormSubmit = async ({
|
||||
certificatePem,
|
||||
privateKeyPem,
|
||||
chainPem,
|
||||
friendlyName,
|
||||
collectionId
|
||||
}: FormData) => {
|
||||
try {
|
||||
if (!currentWorkspace?.slug) return;
|
||||
|
||||
const { serialNumber, certificate, certificateChain, privateKey } = await importCertificate({
|
||||
projectSlug: currentWorkspace.slug,
|
||||
|
||||
certificatePem,
|
||||
privateKeyPem,
|
||||
chainPem,
|
||||
|
||||
friendlyName,
|
||||
pkiCollectionId: collectionId
|
||||
});
|
||||
|
||||
reset();
|
||||
|
||||
setCertificateDetails({
|
||||
serialNumber,
|
||||
certificate,
|
||||
certificateChain,
|
||||
privateKey
|
||||
});
|
||||
|
||||
createNotification({
|
||||
text: "Successfully imported certificate",
|
||||
type: "success"
|
||||
});
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
createNotification({
|
||||
text: "Failed to import certificate",
|
||||
type: "error"
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal
|
||||
isOpen={popUp?.certificateImport?.isOpen}
|
||||
onOpenChange={(isOpen) => {
|
||||
handlePopUpToggle("certificateImport", isOpen);
|
||||
reset();
|
||||
setCertificateDetails(null);
|
||||
}}
|
||||
>
|
||||
<ModalContent title={`${cert ? "View" : "Import"} Certificate`}>
|
||||
{!certificateDetails ? (
|
||||
<form onSubmit={handleSubmit(onFormSubmit)}>
|
||||
<Controller
|
||||
control={control}
|
||||
name="collectionId"
|
||||
render={({ field: { onChange, ...field }, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Certificate Collection"
|
||||
errorText={error?.message}
|
||||
isError={Boolean(error)}
|
||||
isOptional
|
||||
className="mt-4"
|
||||
>
|
||||
<Select
|
||||
defaultValue={field.value}
|
||||
{...field}
|
||||
onValueChange={(e) => onChange(e)}
|
||||
className="w-full"
|
||||
isDisabled={Boolean(cert)}
|
||||
>
|
||||
{(data?.collections || []).map(({ id, name }) => (
|
||||
<SelectItem value={id} key={`pki-collection-${id}`}>
|
||||
{name}
|
||||
</SelectItem>
|
||||
))}
|
||||
</Select>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
defaultValue=""
|
||||
name="friendlyName"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Friendly Name"
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
>
|
||||
<Input {...field} placeholder="My Certificate" isDisabled={Boolean(cert)} />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
defaultValue=""
|
||||
name="certificatePem"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Leaf Certificate PEM"
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
isRequired
|
||||
>
|
||||
<TextArea {...field} isDisabled={Boolean(cert)} />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
defaultValue=""
|
||||
name="privateKeyPem"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Private Key PEM"
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
isRequired
|
||||
>
|
||||
<TextArea {...field} isDisabled={Boolean(cert)} />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
defaultValue=""
|
||||
name="chainPem"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Certificate Chain PEM"
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
isRequired
|
||||
>
|
||||
<TextArea {...field} isDisabled={Boolean(cert)} />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
{!cert && (
|
||||
<div className="mt-4 flex items-center">
|
||||
<Button
|
||||
className="mr-4"
|
||||
size="sm"
|
||||
type="submit"
|
||||
isLoading={isSubmitting}
|
||||
isDisabled={isSubmitting}
|
||||
>
|
||||
Create
|
||||
</Button>
|
||||
<Button
|
||||
colorSchema="secondary"
|
||||
variant="plain"
|
||||
onClick={() => handlePopUpToggle("certificateImport", false)}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</form>
|
||||
) : (
|
||||
<CertificateContent
|
||||
serialNumber={certificateDetails.serialNumber}
|
||||
certificate={certificateDetails.certificate}
|
||||
certificateChain={certificateDetails.certificateChain}
|
||||
privateKey={certificateDetails.privateKey}
|
||||
/>
|
||||
)}
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
@@ -1,4 +1,4 @@
|
||||
import { faPlus } from "@fortawesome/free-solid-svg-icons";
|
||||
import { faArrowRight, faPlus } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
|
||||
import { createNotification } from "@app/components/notifications";
|
||||
@@ -13,6 +13,7 @@ import { useDeleteCert } from "@app/hooks/api";
|
||||
import { usePopUp } from "@app/hooks/usePopUp";
|
||||
|
||||
import { CertificateCertModal } from "./CertificateCertModal";
|
||||
import { CertificateImportModal } from "./CertificateImportModal";
|
||||
import { CertificateModal } from "./CertificateModal";
|
||||
import { CertificateRevocationModal } from "./CertificateRevocationModal";
|
||||
import { CertificatesTable } from "./CertificatesTable";
|
||||
@@ -23,6 +24,7 @@ export const CertificatesSection = () => {
|
||||
|
||||
const { popUp, handlePopUpOpen, handlePopUpClose, handlePopUpToggle } = usePopUp([
|
||||
"certificate",
|
||||
"certificateImport",
|
||||
"certificateCert",
|
||||
"deleteCertificate",
|
||||
"revokeCertificate"
|
||||
@@ -58,20 +60,31 @@ export const CertificatesSection = () => {
|
||||
a={ProjectPermissionSub.Certificates}
|
||||
>
|
||||
{(isAllowed) => (
|
||||
<Button
|
||||
colorSchema="primary"
|
||||
type="submit"
|
||||
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
||||
onClick={() => handlePopUpOpen("certificate")}
|
||||
isDisabled={!isAllowed}
|
||||
>
|
||||
Issue
|
||||
</Button>
|
||||
<div className="flex gap-2">
|
||||
<Button
|
||||
variant="outline_bg"
|
||||
leftIcon={<FontAwesomeIcon icon={faArrowRight} />}
|
||||
onClick={() => handlePopUpOpen("certificateImport")}
|
||||
isDisabled={!isAllowed}
|
||||
>
|
||||
Import
|
||||
</Button>
|
||||
<Button
|
||||
colorSchema="primary"
|
||||
type="submit"
|
||||
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
||||
onClick={() => handlePopUpOpen("certificate")}
|
||||
isDisabled={!isAllowed}
|
||||
>
|
||||
Issue
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</ProjectPermissionCan>
|
||||
</div>
|
||||
<CertificatesTable handlePopUpOpen={handlePopUpOpen} />
|
||||
<CertificateModal popUp={popUp} handlePopUpToggle={handlePopUpToggle} />
|
||||
<CertificateImportModal popUp={popUp} handlePopUpToggle={handlePopUpToggle} />
|
||||
<CertificateCertModal popUp={popUp} handlePopUpToggle={handlePopUpToggle} />
|
||||
<CertificateRevocationModal popUp={popUp} handlePopUpToggle={handlePopUpToggle} />
|
||||
<DeleteActionModal
|
||||
|
||||
Reference in New Issue
Block a user