diff --git a/backend/src/db/migrations/20240802181855_ca-cert-version.ts b/backend/src/db/migrations/20240802181855_ca-cert-version.ts index 62637cbab..b9c572ecd 100644 --- a/backend/src/db/migrations/20240802181855_ca-cert-version.ts +++ b/backend/src/db/migrations/20240802181855_ca-cert-version.ts @@ -4,16 +4,19 @@ import { TableName } from "../schemas"; export async function up(knex: Knex): Promise { if (await knex.schema.hasTable(TableName.CertificateAuthority)) { - const hasActiveCaCertVersionColumn = await knex.schema.hasColumn( - TableName.CertificateAuthority, - "activeCaCertVersion" - ); - if (!hasActiveCaCertVersionColumn) { + const hasActiveCaCertIdColumn = await knex.schema.hasColumn(TableName.CertificateAuthority, "activeCaCertId"); + if (!hasActiveCaCertIdColumn) { await knex.schema.alterTable(TableName.CertificateAuthority, (t) => { - t.integer("activeCaCertVersion").nullable(); + t.uuid("activeCaCertId").nullable(); + t.foreign("activeCaCertId").references("id").inTable(TableName.CertificateAuthorityCert); }); - await knex(TableName.CertificateAuthority).where("status", "active").update({ activeCaCertVersion: 1 }); + await knex.raw(` + UPDATE "${TableName.CertificateAuthority}" ca + SET "activeCaCertId" = cac.id + FROM "${TableName.CertificateAuthorityCert}" cac + WHERE ca.id = cac."caId" + `); } } @@ -54,18 +57,38 @@ export async function up(knex: Knex): Promise { } } - // if (await knex.schema.hasTable(TableName.CertificateAuthoritySecret)) { - // await knex.schema.alterTable(TableName.CertificateAuthoritySecret, (t) => { - // t.dropUnique(["caId"]); - // }); - // } + if (await knex.schema.hasTable(TableName.CertificateAuthoritySecret)) { + await knex.schema.alterTable(TableName.CertificateAuthoritySecret, (t) => { + t.dropUnique(["caId"]); + }); + } + + if (await knex.schema.hasTable(TableName.Certificate)) { + await knex.schema.alterTable(TableName.Certificate, (t) => { + t.uuid("caCertId").nullable(); + t.foreign("caCertId").references("id").inTable(TableName.CertificateAuthorityCert); + }); + + await knex.raw(` + UPDATE "${TableName.Certificate}" cert + SET "caCertId" = ( + SELECT caCert.id + FROM "${TableName.CertificateAuthorityCert}" caCert + WHERE caCert."caId" = cert."caId" + ) + `); + + await knex.schema.alterTable(TableName.Certificate, (t) => { + t.uuid("caCertId").notNullable().alter(); + }); + } } export async function down(knex: Knex): Promise { if (await knex.schema.hasTable(TableName.CertificateAuthority)) { - if (await knex.schema.hasColumn(TableName.CertificateAuthority, "activeCaCertVersion")) { + if (await knex.schema.hasColumn(TableName.CertificateAuthority, "activeCaCertId")) { await knex.schema.alterTable(TableName.CertificateAuthority, (t) => { - t.dropColumn("activeCaCertVersion"); + t.dropColumn("activeCaCertId"); }); } } @@ -83,4 +106,12 @@ export async function down(knex: Knex): Promise { }); } } + + if (await knex.schema.hasTable(TableName.Certificate)) { + if (await knex.schema.hasColumn(TableName.Certificate, "caCertId")) { + await knex.schema.alterTable(TableName.Certificate, (t) => { + t.dropColumn("caCertId"); + }); + } + } } diff --git a/backend/src/db/migrations/20240806173521_cert-alerting.ts b/backend/src/db/migrations/20240806173521_cert-alerting.ts index a946eb5ce..5ea98cda9 100644 --- a/backend/src/db/migrations/20240806173521_cert-alerting.ts +++ b/backend/src/db/migrations/20240806173521_cert-alerting.ts @@ -11,6 +11,7 @@ export async function up(knex: Knex): Promise { t.string("projectId").notNullable(); t.foreign("projectId").references("id").inTable(TableName.Project).onDelete("CASCADE"); t.string("name").notNullable(); + t.string("description").notNullable(); }); } diff --git a/backend/src/db/schemas/certificate-authorities.ts b/backend/src/db/schemas/certificate-authorities.ts index 9255b037e..e59a9225c 100644 --- a/backend/src/db/schemas/certificate-authorities.ts +++ b/backend/src/db/schemas/certificate-authorities.ts @@ -28,7 +28,7 @@ export const CertificateAuthoritiesSchema = z.object({ keyAlgorithm: z.string(), notBefore: z.date().nullable().optional(), notAfter: z.date().nullable().optional(), - activeCaCertVersion: z.number().nullable().optional() + activeCaCertId: z.string().uuid().nullable().optional() }); export type TCertificateAuthorities = z.infer; diff --git a/backend/src/db/schemas/certificates.ts b/backend/src/db/schemas/certificates.ts index 833396fb1..cb14d05f9 100644 --- a/backend/src/db/schemas/certificates.ts +++ b/backend/src/db/schemas/certificates.ts @@ -20,7 +20,8 @@ export const CertificatesSchema = z.object({ notAfter: z.date(), revokedAt: z.date().nullable().optional(), revocationReason: z.number().nullable().optional(), - altNames: z.string().default("").nullable().optional() + altNames: z.string().default("").nullable().optional(), + caCertId: z.string().uuid() }); export type TCertificates = z.infer; diff --git a/backend/src/db/schemas/pki-collections.ts b/backend/src/db/schemas/pki-collections.ts index 4470c513c..0f9a5675f 100644 --- a/backend/src/db/schemas/pki-collections.ts +++ b/backend/src/db/schemas/pki-collections.ts @@ -12,7 +12,8 @@ export const PkiCollectionsSchema = z.object({ createdAt: z.date(), updatedAt: z.date(), projectId: z.string(), - name: z.string() + name: z.string(), + description: z.string() }); export type TPkiCollections = z.infer; diff --git a/backend/src/lib/api-docs/constants.ts b/backend/src/lib/api-docs/constants.ts index 31079f318..e2a9bbd38 100644 --- a/backend/src/lib/api-docs/constants.ts +++ b/backend/src/lib/api-docs/constants.ts @@ -1089,6 +1089,7 @@ export const CERTIFICATE_AUTHORITIES = { ISSUE_CERT: { caId: "The ID of the CA to issue the certificate from", certificateTemplateId: "The ID of the certificate template to issue the certificate from", + pkiCollectionId: "The ID of the PKI collection to add the certificate to", friendlyName: "A friendly name for the certificate", commonName: "The common name (CN) for the certificate", altNames: @@ -1104,6 +1105,7 @@ export const CERTIFICATE_AUTHORITIES = { }, SIGN_CERT: { caId: "The ID of the CA to issue the certificate from", + pkiCollectionId: "The ID of the PKI collection to add the certificate to", csr: "The pem-encoded CSR to sign with the CA to be used for certificate issuance", friendlyName: "A friendly name for the certificate", commonName: "The common name (CN) for the certificate", @@ -1171,14 +1173,16 @@ export const ALERTS = { export const PKI_COLLECTIONS = { CREATE: { projectId: "The ID of the project to create the PKI collection in", - name: "The name of the PKI collection" + name: "The name of the PKI collection", + description: "A description for the PKI collection" }, GET: { collectionId: "The ID of the PKI collection to get" }, UPDATE: { collectionId: "The ID of the PKI collection to update", - name: "The name of the PKI collection to update to" + name: "The name of the PKI collection to update to", + description: "The description for the PKI collection to update to" }, DELETE: { collectionId: "The ID of the PKI collection to delete" diff --git a/backend/src/server/routes/index.ts b/backend/src/server/routes/index.ts index 76eedecdf..df405d25d 100644 --- a/backend/src/server/routes/index.ts +++ b/backend/src/server/routes/index.ts @@ -626,6 +626,8 @@ export const registerRoutes = async ( certificateAuthorityQueue, certificateDAL, certificateBodyDAL, + pkiCollectionDAL, + pkiCollectionItemDAL, projectDAL, kmsService, permissionService diff --git a/backend/src/server/routes/v1/certificate-authority-router.ts b/backend/src/server/routes/v1/certificate-authority-router.ts index bc5294f05..35de2b953 100644 --- a/backend/src/server/routes/v1/certificate-authority-router.ts +++ b/backend/src/server/routes/v1/certificate-authority-router.ts @@ -283,7 +283,7 @@ export const registerCaRouter = async (server: FastifyZodProvider) => { }, onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), schema: { - description: "Renew CA certificate for CA", + description: "Perform CA certificate renewal", params: z.object({ caId: z.string().trim().describe(CERTIFICATE_AUTHORITIES.RENEW_CA_CERT.caId) }), @@ -556,6 +556,7 @@ export const registerCaRouter = async (server: FastifyZodProvider) => { }), body: z .object({ + pkiCollectionId: z.string().trim().optional().describe(CERTIFICATE_AUTHORITIES.ISSUE_CERT.pkiCollectionId), friendlyName: z.string().trim().optional().describe(CERTIFICATE_AUTHORITIES.ISSUE_CERT.friendlyName), commonName: z.string().trim().min(1).describe(CERTIFICATE_AUTHORITIES.ISSUE_CERT.commonName), altNames: validateAltNamesField.describe(CERTIFICATE_AUTHORITIES.ISSUE_CERT.altNames), @@ -635,6 +636,7 @@ export const registerCaRouter = async (server: FastifyZodProvider) => { body: z .object({ csr: z.string().trim().min(1).describe(CERTIFICATE_AUTHORITIES.SIGN_CERT.csr), + pkiCollectionId: z.string().trim().optional().describe(CERTIFICATE_AUTHORITIES.SIGN_CERT.pkiCollectionId), friendlyName: z.string().trim().optional().describe(CERTIFICATE_AUTHORITIES.SIGN_CERT.friendlyName), commonName: z.string().trim().min(1).optional().describe(CERTIFICATE_AUTHORITIES.SIGN_CERT.commonName), altNames: validateAltNamesField.describe(CERTIFICATE_AUTHORITIES.SIGN_CERT.altNames), diff --git a/backend/src/server/routes/v1/pki-collection-router.ts b/backend/src/server/routes/v1/pki-collection-router.ts index 6a48b5e76..2f2add5c1 100644 --- a/backend/src/server/routes/v1/pki-collection-router.ts +++ b/backend/src/server/routes/v1/pki-collection-router.ts @@ -20,7 +20,8 @@ export const registerPkiCollectionRouter = async (server: FastifyZodProvider) => description: "Create PKI collection", body: z.object({ projectId: z.string().trim().describe(PKI_COLLECTIONS.CREATE.projectId), - name: z.string().trim().describe(PKI_COLLECTIONS.CREATE.name) + name: z.string().trim().describe(PKI_COLLECTIONS.CREATE.name), + description: z.string().trim().default("").describe(PKI_COLLECTIONS.CREATE.description) }), response: { 200: PkiCollectionsSchema @@ -104,7 +105,8 @@ export const registerPkiCollectionRouter = async (server: FastifyZodProvider) => collectionId: z.string().trim().describe(PKI_COLLECTIONS.UPDATE.collectionId) }), body: z.object({ - name: z.string().trim().optional().describe(PKI_COLLECTIONS.UPDATE.name) + name: z.string().trim().optional().describe(PKI_COLLECTIONS.UPDATE.name), + description: z.string().trim().optional().describe(PKI_COLLECTIONS.UPDATE.description) }), response: { 200: PkiCollectionsSchema diff --git a/backend/src/services/certificate-authority/certificate-authority-fns.ts b/backend/src/services/certificate-authority/certificate-authority-fns.ts index 84d8d263d..4e3df1173 100644 --- a/backend/src/services/certificate-authority/certificate-authority-fns.ts +++ b/backend/src/services/certificate-authority/certificate-authority-fns.ts @@ -195,19 +195,18 @@ export const getCaCertChains = async ({ /** * Return the decrypted pem-encoded certificate and certificate chain - * for CA with id [caId]. + * corresponding to CA certificate with id [caCertId]. */ export const getCaCertChain = async ({ - caId, + caCertId, certificateAuthorityDAL, certificateAuthorityCertDAL, projectDAL, kmsService }: TGetCaCertChainDTO) => { - const ca = await certificateAuthorityDAL.findById(caId); - if (!ca) throw new BadRequestError({ message: "CA not found" }); - - const caCert = await certificateAuthorityCertDAL.findOne({ caId: ca.id }); + const caCert = await certificateAuthorityCertDAL.findById(caCertId); + if (!caCert) throw new BadRequestError({ message: "CA certificate not found" }); + const ca = await certificateAuthorityDAL.findById(caCert.caId); const keyId = await getProjectKmsCertificateKeyId({ projectId: ca.projectId, diff --git a/backend/src/services/certificate-authority/certificate-authority-service.ts b/backend/src/services/certificate-authority/certificate-authority-service.ts index 1f1ffe9e1..dd122ef9c 100644 --- a/backend/src/services/certificate-authority/certificate-authority-service.ts +++ b/backend/src/services/certificate-authority/certificate-authority-service.ts @@ -12,6 +12,8 @@ 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 { 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"; @@ -54,13 +56,18 @@ type TCertificateAuthorityServiceFactoryDep = { TCertificateAuthorityDALFactory, "transaction" | "create" | "findById" | "updateById" | "deleteById" | "findOne" >; - certificateAuthorityCertDAL: Pick; + certificateAuthorityCertDAL: Pick< + TCertificateAuthorityCertDALFactory, + "create" | "findOne" | "transaction" | "find" | "findById" + >; certificateAuthoritySecretDAL: Pick; certificateAuthorityCrlDAL: Pick; certificateTemplateDAL: Pick; certificateAuthorityQueue: TCertificateAuthorityQueueFactory; // TODO: Pick certificateDAL: Pick; certificateBodyDAL: Pick; + pkiCollectionDAL: Pick; + pkiCollectionItemDAL: Pick; projectDAL: Pick; kmsService: Pick; permissionService: Pick; @@ -76,6 +83,8 @@ export const certificateAuthorityServiceFactory = ({ certificateTemplateDAL, certificateDAL, certificateBodyDAL, + pkiCollectionDAL, + pkiCollectionItemDAL, projectDAL, kmsService, permissionService @@ -158,8 +167,7 @@ export const certificateAuthorityServiceFactory = ({ maxPathLength, notBefore: notBeforeDate, notAfter: notAfterDate, - serialNumber, - activeCaCertVersion: 1 + serialNumber }) }, tx @@ -218,7 +226,7 @@ export const certificateAuthorityServiceFactory = ({ plainText: Buffer.alloc(0) }); - await certificateAuthorityCertDAL.create( + const caCert = await certificateAuthorityCertDAL.create( { caId: ca.id, encryptedCertificate, @@ -228,6 +236,14 @@ export const certificateAuthorityServiceFactory = ({ }, tx ); + + await certificateAuthorityDAL.updateById( + ca.id, + { + activeCaCertId: caCert.id + }, + tx + ); } // create empty CRL @@ -352,9 +368,7 @@ export const certificateAuthorityServiceFactory = ({ ); if (ca.type === CaType.ROOT) throw new BadRequestError({ message: "Root CA cannot generate CSR" }); - - const [caCert] = await certificateAuthorityCertDAL.find({ caId: ca.id }, { sort: [["version", "desc"]] }); - if (caCert) throw new BadRequestError({ message: "CA already has a certificate installed" }); + if (ca.activeCaCertId) throw new BadRequestError({ message: "CA already has a certificate installed" }); const { caPrivateKey, caPublicKey } = await getCaCredentials({ caId, @@ -399,6 +413,8 @@ export const certificateAuthorityServiceFactory = ({ const ca = await certificateAuthorityDAL.findById(caId); if (!ca) throw new BadRequestError({ message: "CA not found" }); + if (!ca.activeCaCertId) throw new BadRequestError({ message: "CA does not have a certificate installed" }); + const { permission } = await permissionService.getProjectPermission( actor, actorId, @@ -415,8 +431,7 @@ export const certificateAuthorityServiceFactory = ({ if (ca.status === CaStatus.DISABLED) throw new BadRequestError({ message: "CA is disabled" }); // get latest CA certificate - const [caCert] = await certificateAuthorityCertDAL.find({ caId: ca.id }, { sort: [["version", "desc"]] }); - if (!caCert) throw new BadRequestError({ message: "CA does not have a certificate installed" }); + const caCert = await certificateAuthorityCertDAL.findById(ca.activeCaCertId); const serialNumber = crypto.randomBytes(32).toString("hex"); @@ -494,13 +509,12 @@ export const certificateAuthorityServiceFactory = ({ }); await certificateAuthorityDAL.transaction(async (tx) => { - const newActiveCaCertVersion = caCert.version + 1; - await certificateAuthorityCertDAL.create( + const newCaCert = await certificateAuthorityCertDAL.create( { caId: ca.id, encryptedCertificate, encryptedCertificateChain, - version: newActiveCaCertVersion, + version: caCert.version + 1, caSecretId: caSecret.id }, tx @@ -509,7 +523,7 @@ export const certificateAuthorityServiceFactory = ({ await certificateAuthorityDAL.updateById( ca.id, { - activeCaCertVersion: newActiveCaCertVersion, + activeCaCertId: newCaCert.id, notBefore: notBeforeDate, notAfter: new Date(notAfter) }, @@ -538,10 +552,9 @@ export const certificateAuthorityServiceFactory = ({ }); // get latest parent CA certificate - const [parentCaCert] = await certificateAuthorityCertDAL.find( - { caId: parentCa.id }, - { sort: [["version", "desc"]] } - ); + if (!parentCa.activeCaCertId) + throw new BadRequestError({ message: "Parent CA does not have a certificate installed" }); + const parentCaCert = await certificateAuthorityCertDAL.findById(parentCa.activeCaCertId); const decryptedParentCaCert = await kmsDecryptor({ cipherTextBlob: parentCaCert.encryptedCertificate @@ -586,7 +599,7 @@ export const certificateAuthorityServiceFactory = ({ const intermediateCert = await x509.X509CertificateGenerator.create({ serialNumber, subject: csrObj.subject, - issuer: caCertObj.subject, + issuer: parentCaCertObj.subject, notBefore: notBeforeDate, notAfter: new Date(notAfter), signingKey: parentCaPrivateKey, @@ -605,7 +618,7 @@ export const certificateAuthorityServiceFactory = ({ ca.maxPathLength === -1 || !ca.maxPathLength ? undefined : ca.maxPathLength, true ), - await x509.AuthorityKeyIdentifierExtension.create(caCertObj, false), + await x509.AuthorityKeyIdentifierExtension.create(parentCaCertObj, false), await x509.SubjectKeyIdentifierExtension.create(csrObj.publicKey) ] }); @@ -615,7 +628,7 @@ export const certificateAuthorityServiceFactory = ({ }); const { caCert: parentCaCertificate, caCertChain: parentCaCertChain } = await getCaCertChain({ - caId: parentCa.id, + caCertId: parentCa.activeCaCertId, certificateAuthorityDAL, certificateAuthorityCertDAL, projectDAL, @@ -629,13 +642,12 @@ export const certificateAuthorityServiceFactory = ({ }); await certificateAuthorityDAL.transaction(async (tx) => { - const newActiveCaCertVersion = caCert.version + 1; - await certificateAuthorityCertDAL.create( + const newCaCert = await certificateAuthorityCertDAL.create( { caId: ca.id, encryptedCertificate, encryptedCertificateChain, - version: newActiveCaCertVersion, + version: caCert.version + 1, caSecretId: caSecret.id }, tx @@ -644,7 +656,7 @@ export const certificateAuthorityServiceFactory = ({ await certificateAuthorityDAL.updateById( ca.id, { - activeCaCertVersion: newActiveCaCertVersion, + activeCaCertId: newCaCert.id, notBefore: notBeforeDate, notAfter: new Date(notAfter) }, @@ -703,11 +715,11 @@ export const certificateAuthorityServiceFactory = ({ /** * Return current certificate and certificate chain for CA - * get latest?? ca cert */ const getCaCert = async ({ caId, actorId, actorAuthMethod, actor, actorOrgId }: TGetCaCertDTO) => { const ca = await certificateAuthorityDAL.findById(caId); if (!ca) throw new BadRequestError({ message: "CA not found" }); + if (!ca.activeCaCertId) throw new BadRequestError({ message: "CA does not have a certificate installed" }); const { permission } = await permissionService.getProjectPermission( actor, @@ -723,7 +735,7 @@ export const certificateAuthorityServiceFactory = ({ ); const { caCert, caCertChain, serialNumber } = await getCaCertChain({ - caId, + caCertId: ca.activeCaCertId, certificateAuthorityDAL, certificateAuthorityCertDAL, projectDAL, @@ -769,9 +781,9 @@ export const certificateAuthorityServiceFactory = ({ ); if (ca.status === CaStatus.DISABLED) throw new BadRequestError({ message: "CA is disabled" }); + if (!ca.activeCaCertId) throw new BadRequestError({ message: "CA does not have a certificate installed" }); - const [caCert] = await certificateAuthorityCertDAL.find({ caId: ca.id }, { sort: [["version", "desc"]] }); - if (!caCert) throw new BadRequestError({ message: "CA does not have a certificate installed" }); + const caCert = await certificateAuthorityCertDAL.findById(ca.activeCaCertId); if (ca.notAfter && new Date() > new Date(ca.notAfter)) { throw new BadRequestError({ message: "CA is expired" }); @@ -859,7 +871,7 @@ export const certificateAuthorityServiceFactory = ({ }); const { caCert: issuingCaCertificate, caCertChain } = await getCaCertChain({ - caId, + caCertId: ca.activeCaCertId, certificateAuthorityDAL, certificateAuthorityCertDAL, projectDAL, @@ -905,8 +917,7 @@ export const certificateAuthorityServiceFactory = ({ ProjectPermissionSub.CertificateAuthorities ); - const [caCert] = await certificateAuthorityCertDAL.find({ caId: ca.id }, { sort: [["version", "desc"]] }); - if (caCert) throw new BadRequestError({ message: "CA has already imported a certificate" }); + if (ca.activeCaCertId) throw new BadRequestError({ message: "CA has already imported a certificate" }); const certObj = new x509.X509Certificate(certificate); const maxPathLength = certObj.getExtension(x509.BasicConstraintsExtension)?.pathLength; @@ -972,7 +983,7 @@ export const certificateAuthorityServiceFactory = ({ } await certificateAuthorityCertDAL.transaction(async (tx) => { - await certificateAuthorityCertDAL.create( + const newCaCert = await certificateAuthorityCertDAL.create( { caId: ca.id, encryptedCertificate, @@ -991,7 +1002,8 @@ export const certificateAuthorityServiceFactory = ({ notBefore: new Date(certObj.notBefore), notAfter: new Date(certObj.notAfter), serialNumber: certObj.serialNumber, - parentCaId: parentCa?.id + parentCaId: parentCa?.id, + activeCaCertId: newCaCert.id }, tx ); @@ -1007,6 +1019,7 @@ export const certificateAuthorityServiceFactory = ({ const issueCertFromCa = async ({ caId, certificateTemplateId, + pkiCollectionId, friendlyName, commonName, altNames, @@ -1049,14 +1062,20 @@ export const certificateAuthorityServiceFactory = ({ ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Create, ProjectPermissionSub.Certificates); if (ca.status === CaStatus.DISABLED) throw new BadRequestError({ message: "CA is disabled" }); - - const [caCert] = await certificateAuthorityCertDAL.find({ caId: ca.id }, { sort: [["version", "desc"]] }); - if (!caCert) throw new BadRequestError({ message: "CA does not have a certificate installed" }); + if (!ca.activeCaCertId) throw new BadRequestError({ message: "CA does not have a certificate installed" }); + const caCert = await certificateAuthorityCertDAL.findById(ca.activeCaCertId); if (ca.notAfter && new Date() > new Date(ca.notAfter)) { throw new BadRequestError({ message: "CA is expired" }); } + // check PKI collection + if (pkiCollectionId) { + const pkiCollection = await pkiCollectionDAL.findById(pkiCollectionId); + if (!pkiCollection) throw new NotFoundError({ message: "PKI collection not found" }); + if (pkiCollection.projectId !== ca.projectId) throw new BadRequestError({ message: "Invalid PKI collection" }); + } + const certificateManagerKmsId = await getProjectKmsCertificateKeyId({ projectId: ca.projectId, projectDAL, @@ -1198,6 +1217,7 @@ export const certificateAuthorityServiceFactory = ({ const cert = await certificateDAL.create( { caId: ca.id, + caCertId: caCert.id, status: CertStatus.ACTIVE, friendlyName: friendlyName || commonName, commonName, @@ -1217,11 +1237,21 @@ export const certificateAuthorityServiceFactory = ({ tx ); + if (pkiCollectionId) { + await pkiCollectionItemDAL.create( + { + pkiCollectionId, + certId: cert.id + }, + tx + ); + } + return cert; }); const { caCert: issuingCaCertificate, caCertChain } = await getCaCertChain({ - caId: ca.id, + caCertId: caCert.id, certificateAuthorityDAL, certificateAuthorityCertDAL, projectDAL, @@ -1246,6 +1276,7 @@ export const certificateAuthorityServiceFactory = ({ caId, certificateTemplateId, csr, + pkiCollectionId, friendlyName, commonName, altNames, @@ -1288,14 +1319,21 @@ export const certificateAuthorityServiceFactory = ({ ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Create, ProjectPermissionSub.Certificates); if (ca.status === CaStatus.DISABLED) throw new BadRequestError({ message: "CA is disabled" }); + if (!ca.activeCaCertId) throw new BadRequestError({ message: "CA does not have a certificate installed" }); - const [caCert] = await certificateAuthorityCertDAL.find({ caId: ca.id }, { sort: [["version", "desc"]] }); - if (!caCert) throw new BadRequestError({ message: "CA does not have a certificate installed" }); + const caCert = await certificateAuthorityCertDAL.findById(ca.activeCaCertId); if (ca.notAfter && new Date() > new Date(ca.notAfter)) { throw new BadRequestError({ message: "CA is expired" }); } + // check PKI collection + if (pkiCollectionId) { + const pkiCollection = await pkiCollectionDAL.findById(pkiCollectionId); + if (!pkiCollection) throw new NotFoundError({ message: "PKI collection not found" }); + if (pkiCollection.projectId !== ca.projectId) throw new BadRequestError({ message: "Invalid PKI collection" }); + } + const certificateManagerKmsId = await getProjectKmsCertificateKeyId({ projectId: ca.projectId, projectDAL, @@ -1432,6 +1470,7 @@ export const certificateAuthorityServiceFactory = ({ const cert = await certificateDAL.create( { caId: ca.id, + caCertId: caCert.id, status: CertStatus.ACTIVE, friendlyName: friendlyName || csrObj.subject, commonName: cn, @@ -1451,11 +1490,21 @@ export const certificateAuthorityServiceFactory = ({ tx ); + if (pkiCollectionId) { + await pkiCollectionItemDAL.create( + { + pkiCollectionId, + certId: cert.id + }, + tx + ); + } + return cert; }); const { caCert: issuingCaCertificate, caCertChain } = await getCaCertChain({ - caId: ca.id, + caCertId: ca.activeCaCertId, certificateAuthorityDAL, certificateAuthorityCertDAL, projectDAL, diff --git a/backend/src/services/certificate-authority/certificate-authority-types.ts b/backend/src/services/certificate-authority/certificate-authority-types.ts index e04e0b8a9..c6bdeae30 100644 --- a/backend/src/services/certificate-authority/certificate-authority-types.ts +++ b/backend/src/services/certificate-authority/certificate-authority-types.ts @@ -88,6 +88,7 @@ export type TImportCertToCaDTO = { export type TIssueCertFromCaDTO = { caId?: string; certificateTemplateId?: string; + pkiCollectionId?: string; friendlyName?: string; commonName: string; altNames: string; @@ -100,6 +101,7 @@ export type TSignCertFromCaDTO = { caId?: string; csr: string; certificateTemplateId?: string; + pkiCollectionId?: string; friendlyName?: string; commonName?: string; altNames: string; @@ -134,9 +136,9 @@ export type TGetCaCertChainsDTO = { }; export type TGetCaCertChainDTO = { - caId: string; + caCertId: string; certificateAuthorityDAL: Pick; - certificateAuthorityCertDAL: Pick; + certificateAuthorityCertDAL: Pick; projectDAL: Pick; kmsService: Pick; }; diff --git a/backend/src/services/certificate/certificate-service.ts b/backend/src/services/certificate/certificate-service.ts index f05ed8a87..8dc2de901 100644 --- a/backend/src/services/certificate/certificate-service.ts +++ b/backend/src/services/certificate/certificate-service.ts @@ -21,7 +21,7 @@ type TCertificateServiceFactoryDep = { certificateDAL: Pick; certificateBodyDAL: Pick; certificateAuthorityDAL: Pick; - certificateAuthorityCertDAL: Pick; + certificateAuthorityCertDAL: Pick; certificateAuthorityCrlDAL: Pick; certificateAuthoritySecretDAL: Pick; projectDAL: Pick; @@ -180,7 +180,7 @@ export const certificateServiceFactory = ({ const certObj = new x509.X509Certificate(decryptedCert); const { caCert, caCertChain } = await getCaCertChain({ - caId: ca.id, + caCertId: cert.caCertId, certificateAuthorityDAL, certificateAuthorityCertDAL, projectDAL, diff --git a/backend/src/services/pki-collection/pki-collection-service.ts b/backend/src/services/pki-collection/pki-collection-service.ts index 87b1be2f9..f7c7ba399 100644 --- a/backend/src/services/pki-collection/pki-collection-service.ts +++ b/backend/src/services/pki-collection/pki-collection-service.ts @@ -40,6 +40,7 @@ export const pkiCollectionServiceFactory = ({ }: TPkiCollectionServiceFactoryDep) => { const createPkiCollection = async ({ name, + description, projectId, actorId, actorAuthMethod, @@ -61,7 +62,8 @@ export const pkiCollectionServiceFactory = ({ const pkiCollection = await pkiCollectionDAL.create({ projectId, - name + name, + description }); return pkiCollection; @@ -92,6 +94,7 @@ export const pkiCollectionServiceFactory = ({ const updatePkiCollection = async ({ collectionId, name, + description, actorId, actorAuthMethod, actor, @@ -110,7 +113,8 @@ export const pkiCollectionServiceFactory = ({ ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Edit, ProjectPermissionSub.PkiCollections); pkiCollection = await pkiCollectionDAL.updateById(collectionId, { - name + name, + description }); return pkiCollection; diff --git a/backend/src/services/pki-collection/pki-collection-types.ts b/backend/src/services/pki-collection/pki-collection-types.ts index 52d489eb2..e82ca31d0 100644 --- a/backend/src/services/pki-collection/pki-collection-types.ts +++ b/backend/src/services/pki-collection/pki-collection-types.ts @@ -2,6 +2,7 @@ import { TProjectPermission } from "@app/lib/types"; export type TCreatePkiCollectionDTO = { name: string; + description: string; } & TProjectPermission; export type TGetPkiCollectionByIdDTO = { @@ -11,6 +12,7 @@ export type TGetPkiCollectionByIdDTO = { export type TUpdatePkiCollectionDTO = { collectionId: string; name?: string; + description?: string; } & Omit; export type TDeletePkiCollectionDTO = { diff --git a/frontend/src/hooks/api/ca/mutations.tsx b/frontend/src/hooks/api/ca/mutations.tsx index 035911e4a..48652f3fd 100644 --- a/frontend/src/hooks/api/ca/mutations.tsx +++ b/frontend/src/hooks/api/ca/mutations.tsx @@ -124,6 +124,7 @@ export const useRenewCa = () => { }, onSuccess: (_, { caId, projectSlug }) => { queryClient.invalidateQueries(workspaceKeys.getWorkspaceCas({ projectSlug })); + queryClient.invalidateQueries(caKeys.getCaById(caId)); queryClient.invalidateQueries(caKeys.getCaCert(caId)); queryClient.invalidateQueries(caKeys.getCaCerts(caId)); queryClient.invalidateQueries(caKeys.getCaCsr(caId)); diff --git a/frontend/src/hooks/api/ca/types.ts b/frontend/src/hooks/api/ca/types.ts index 697e0e84c..e09ae16b8 100644 --- a/frontend/src/hooks/api/ca/types.ts +++ b/frontend/src/hooks/api/ca/types.ts @@ -19,6 +19,7 @@ export type TCertificateAuthority = { notAfter?: string; notBefore?: string; keyAlgorithm: CertKeyAlgorithm; + activeCaCertId?: string; createdAt: string; updatedAt: string; }; @@ -80,6 +81,7 @@ export type TCreateCertificateDTO = { projectSlug: string; caId?: string; certificateTemplateId?: string; + pkiCollectionId?: string; friendlyName?: string; commonName: string; altNames: string; // sans diff --git a/frontend/src/hooks/api/pkiCollections/types.ts b/frontend/src/hooks/api/pkiCollections/types.ts index 86c2471e9..98a98d520 100644 --- a/frontend/src/hooks/api/pkiCollections/types.ts +++ b/frontend/src/hooks/api/pkiCollections/types.ts @@ -1,6 +1,7 @@ export type TPkiCollection = { id: string; name: string; + description: string; projectId: string; createdAt: string; updatedAt: string; @@ -9,12 +10,14 @@ export type TPkiCollection = { export type TCreatePkiCollectionDTO = { projectId: string; name: string; + description: string; }; export type TUpdatePkiCollectionTO = { collectionId: string; projectId: string; name?: string; + description?: string; }; export type TDeletePkiCollectionDTO = { diff --git a/frontend/src/views/Project/CertificatesPage/components/CertificatesTab/CertificatesTab.tsx b/frontend/src/views/Project/CertificatesPage/components/CertificatesTab/CertificatesTab.tsx index f29a0bdb7..71576d39d 100644 --- a/frontend/src/views/Project/CertificatesPage/components/CertificatesTab/CertificatesTab.tsx +++ b/frontend/src/views/Project/CertificatesPage/components/CertificatesTab/CertificatesTab.tsx @@ -1,5 +1,6 @@ import { motion } from "framer-motion"; +import { PkiCollectionSection } from "../PkiAlertsTab/components"; import { CertificateTemplatesSection } from "./components/CertificateTemplatesSection"; import { CertificatesSection } from "./components"; @@ -12,6 +13,7 @@ export const CertificatesTab = () => { animate={{ opacity: 1, translateX: 0 }} exit={{ opacity: 0, translateX: 30 }} > + diff --git a/frontend/src/views/Project/CertificatesPage/components/CertificatesTab/components/CertificateModal.tsx b/frontend/src/views/Project/CertificatesPage/components/CertificatesTab/components/CertificateModal.tsx index c3275c547..40223085b 100644 --- a/frontend/src/views/Project/CertificatesPage/components/CertificatesTab/components/CertificateModal.tsx +++ b/frontend/src/views/Project/CertificatesPage/components/CertificatesTab/components/CertificateModal.tsx @@ -20,7 +20,8 @@ import { useGetCert, useGetCertTemplate, useListWorkspaceCas, - useListWorkspaceCertificateTemplates + useListWorkspaceCertificateTemplates, + useListWorkspacePkiCollections } from "@app/hooks/api"; import { caTypeToNameMap } from "@app/hooks/api/ca/constants"; import { UsePopUpState } from "@app/hooks/usePopUp"; @@ -30,6 +31,7 @@ import { CertificateContent } from "./CertificateContent"; const schema = z.object({ certificateTemplateId: z.string().optional(), caId: z.string(), + collectionId: z.string().optional(), friendlyName: z.string(), commonName: z.string().trim().min(1), altNames: z.string(), @@ -64,6 +66,10 @@ export const CertificateModal = ({ popUp, handlePopUpToggle }: Props) => { status: CaStatus.ACTIVE }); + const { data } = useListWorkspacePkiCollections({ + workspaceId: currentWorkspace?.id || "" + }); + const { data: templatesData } = useListWorkspaceCertificateTemplates({ workspaceId: currentWorkspace?.id || "" }); @@ -116,7 +122,14 @@ export const CertificateModal = ({ popUp, handlePopUpToggle }: Props) => { } }, [selectedCertTemplate, cert]); - const onFormSubmit = async ({ caId, friendlyName, commonName, altNames, ttl }: FormData) => { + const onFormSubmit = async ({ + caId, + friendlyName, + collectionId, + commonName, + altNames, + ttl + }: FormData) => { try { if (!currentWorkspace?.slug) return; @@ -124,6 +137,7 @@ export const CertificateModal = ({ popUp, handlePopUpToggle }: Props) => { caId: !selectedCertTemplate ? caId : undefined, certificateTemplateId: selectedCertTemplate ? selectedCertTemplateId : undefined, projectSlug: currentWorkspace.slug, + pkiCollectionId: collectionId, friendlyName, commonName, altNames, @@ -230,6 +244,32 @@ export const CertificateModal = ({ popUp, handlePopUpToggle }: Props) => { )} /> )} + ( + + + + )} + /> ; @@ -49,16 +50,18 @@ export const PkiCollectionModal = ({ popUp, handlePopUpToggle }: Props) => { useEffect(() => { if (pkiCollection) { reset({ - name: pkiCollection.name + name: pkiCollection.name, + description: pkiCollection.description }); } else { reset({ - name: "" + name: "", + description: "" }); } }, [pkiCollection]); - const onFormSubmit = async ({ name }: FormData) => { + const onFormSubmit = async ({ name, description }: FormData) => { try { if (!projectId) return; @@ -67,12 +70,14 @@ export const PkiCollectionModal = ({ popUp, handlePopUpToggle }: Props) => { await updatePkiCollection({ collectionId: pkiCollection.id, name, + description, projectId }); } else { // create const { id: createdId } = await createPkiCollection({ name, + description, projectId }); @@ -121,6 +126,19 @@ export const PkiCollectionModal = ({ popUp, handlePopUpToggle }: Props) => { )} /> + ( + + + + )} + />
-
+

Name

{pkiCollection.name}

+
+

Description

+

{pkiCollection.description}

+
) : (