From 6b95bb0ceba66c92cc3a079517ee2a42fe813e90 Mon Sep 17 00:00:00 2001 From: Sheen Capadngan Date: Wed, 14 May 2025 04:08:57 +0800 Subject: [PATCH] misc: continued migration to new ca structure --- .../certificate-authority-crl-service.ts | 14 +- .../certificate-est-service.ts | 18 +- backend/src/server/routes/index.ts | 2 +- backend/src/server/routes/sanitizedSchemas.ts | 6 + .../routes/v1/certificate-authority-router.ts | 12 +- .../server/routes/v1/certificate-router.ts | 4 +- .../src/server/routes/v2/project-router.ts | 5 +- .../certificate-authority-dal.ts | 91 +- .../certificate-authority-fns.ts | 27 +- .../certificate-authority-queue.ts | 10 +- .../certificate-authority-service.ts | 1904 +---------------- .../certificate-authority-types.ts | 4 +- .../internal-certificate-authority-service.ts | 144 +- .../certificate-template-dal.ts | 14 +- .../certificate/certificate-service.ts | 28 +- .../pki-subscriber/pki-subscriber-service.ts | 37 +- .../src/services/project/project-service.ts | 18 +- 17 files changed, 291 insertions(+), 2047 deletions(-) diff --git a/backend/src/ee/services/certificate-authority-crl/certificate-authority-crl-service.ts b/backend/src/ee/services/certificate-authority-crl/certificate-authority-crl-service.ts index b8f4ce663..0856c957c 100644 --- a/backend/src/ee/services/certificate-authority-crl/certificate-authority-crl-service.ts +++ b/backend/src/ee/services/certificate-authority-crl/certificate-authority-crl-service.ts @@ -7,6 +7,7 @@ import { TPermissionServiceFactory } from "@app/ee/services/permission/permissio import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission"; import { NotFoundError } from "@app/lib/errors"; import { TCertificateAuthorityDALFactory } from "@app/services/certificate-authority/certificate-authority-dal"; +import { expandInternalCa } from "@app/services/certificate-authority/certificate-authority-fns"; import { TKmsServiceFactory } from "@app/services/kms/kms-service"; import { TProjectDALFactory } from "@app/services/project/project-dal"; import { getProjectKmsCertificateKeyId } from "@app/services/project/project-fns"; @@ -14,7 +15,7 @@ import { getProjectKmsCertificateKeyId } from "@app/services/project/project-fns import { TGetCaCrlsDTO, TGetCrlById } from "./certificate-authority-crl-types"; type TCertificateAuthorityCrlServiceFactoryDep = { - certificateAuthorityDAL: Pick; + certificateAuthorityDAL: Pick; certificateAuthorityCrlDAL: Pick; projectDAL: Pick; kmsService: Pick; @@ -37,7 +38,8 @@ export const certificateAuthorityCrlServiceFactory = ({ const caCrl = await certificateAuthorityCrlDAL.findById(crlId); if (!caCrl) throw new NotFoundError({ message: `CRL with ID '${crlId}' not found` }); - const ca = await certificateAuthorityDAL.findById(caCrl.caId); + const ca = await certificateAuthorityDAL.findByIdWithAssociatedCa(caCrl.caId); + if (!ca?.internalCa) throw new NotFoundError({ message: `CA with ID '${caCrl.caId}' not found` }); const keyId = await getProjectKmsCertificateKeyId({ projectId: ca.projectId, @@ -54,7 +56,7 @@ export const certificateAuthorityCrlServiceFactory = ({ const crl = new x509.X509Crl(decryptedCrl); return { - ca, + ca: expandInternalCa(ca), caCrl, crl: crl.rawData }; @@ -64,8 +66,8 @@ export const certificateAuthorityCrlServiceFactory = ({ * Returns a list of CRL ids for CA with id [caId] */ const getCaCrls = async ({ caId, actorId, actorAuthMethod, actor, actorOrgId }: TGetCaCrlsDTO) => { - const ca = await certificateAuthorityDAL.findById(caId); - if (!ca) throw new NotFoundError({ message: `CA with ID '${caId}' not found` }); + const ca = await certificateAuthorityDAL.findByIdWithAssociatedCa(caId); + if (!ca?.internalCa) throw new NotFoundError({ message: `CA with ID '${caId}' not found` }); const { permission } = await permissionService.getProjectPermission({ actor, @@ -108,7 +110,7 @@ export const certificateAuthorityCrlServiceFactory = ({ ); return { - ca, + ca: expandInternalCa(ca), crls: decryptedCrls }; }; diff --git a/backend/src/ee/services/certificate-est/certificate-est-service.ts b/backend/src/ee/services/certificate-est/certificate-est-service.ts index 627cc58c6..59d963558 100644 --- a/backend/src/ee/services/certificate-est/certificate-est-service.ts +++ b/backend/src/ee/services/certificate-est/certificate-est-service.ts @@ -6,7 +6,7 @@ import { isCertChainValid } from "@app/services/certificate/certificate-fns"; import { TCertificateAuthorityCertDALFactory } from "@app/services/certificate-authority/certificate-authority-cert-dal"; import { TCertificateAuthorityDALFactory } from "@app/services/certificate-authority/certificate-authority-dal"; import { getCaCertChain, getCaCertChains } from "@app/services/certificate-authority/certificate-authority-fns"; -import { TCertificateAuthorityServiceFactory } from "@app/services/certificate-authority/certificate-authority-service"; +import { TInternalCertificateAuthorityServiceFactory } from "@app/services/certificate-authority/internal-certificate-authority-service"; import { TCertificateTemplateDALFactory } from "@app/services/certificate-template/certificate-template-dal"; import { TCertificateTemplateServiceFactory } from "@app/services/certificate-template/certificate-template-service"; import { TKmsServiceFactory } from "@app/services/kms/kms-service"; @@ -16,10 +16,10 @@ import { TLicenseServiceFactory } from "../license/license-service"; import { convertRawCertsToPkcs7 } from "./certificate-est-fns"; type TCertificateEstServiceFactoryDep = { - certificateAuthorityService: Pick; + internalCertificateAuthorityService: Pick; certificateTemplateService: Pick; certificateTemplateDAL: Pick; - certificateAuthorityDAL: Pick; + certificateAuthorityDAL: Pick; certificateAuthorityCertDAL: Pick; projectDAL: Pick; kmsService: Pick; @@ -29,7 +29,7 @@ type TCertificateEstServiceFactoryDep = { export type TCertificateEstServiceFactory = ReturnType; export const certificateEstServiceFactory = ({ - certificateAuthorityService, + internalCertificateAuthorityService, certificateTemplateService, certificateTemplateDAL, certificateAuthorityCertDAL, @@ -127,7 +127,7 @@ export const certificateEstServiceFactory = ({ }); } - const { certificate } = await certificateAuthorityService.signCertFromCa({ + const { certificate } = await internalCertificateAuthorityService.signCertFromCa({ isInternal: true, certificateTemplateId, csr @@ -188,7 +188,7 @@ export const certificateEstServiceFactory = ({ } } - const { certificate } = await certificateAuthorityService.signCertFromCa({ + const { certificate } = await internalCertificateAuthorityService.signCertFromCa({ isInternal: true, certificateTemplateId, csr @@ -227,15 +227,15 @@ export const certificateEstServiceFactory = ({ }); } - const ca = await certificateAuthorityDAL.findById(certTemplate.caId); - if (!ca) { + const ca = await certificateAuthorityDAL.findByIdWithAssociatedCa(certTemplate.caId); + if (!ca?.internalCa) { throw new NotFoundError({ message: `Certificate Authority with ID '${certTemplate.caId}' not found` }); } const { caCert, caCertChain } = await getCaCertChain({ - caCertId: ca.activeCaCertId as string, + caCertId: ca.internalCa.activeCaCertId as string, certificateAuthorityDAL, certificateAuthorityCertDAL, projectDAL, diff --git a/backend/src/server/routes/index.ts b/backend/src/server/routes/index.ts index b0d0d6fc8..717c3fdc1 100644 --- a/backend/src/server/routes/index.ts +++ b/backend/src/server/routes/index.ts @@ -959,7 +959,7 @@ export const registerRoutes = async ( }); const certificateEstService = certificateEstServiceFactory({ - certificateAuthorityService, + internalCertificateAuthorityService, certificateTemplateService, certificateTemplateDAL, certificateAuthorityCertDAL, diff --git a/backend/src/server/routes/sanitizedSchemas.ts b/backend/src/server/routes/sanitizedSchemas.ts index da300981c..260d6220e 100644 --- a/backend/src/server/routes/sanitizedSchemas.ts +++ b/backend/src/server/routes/sanitizedSchemas.ts @@ -1,9 +1,11 @@ import { z } from "zod"; import { + CertificateAuthoritiesSchema, DynamicSecretsSchema, IdentityProjectAdditionalPrivilegeSchema, IntegrationAuthsSchema, + InternalCertificateAuthoritiesSchema, ProjectRolesSchema, ProjectsSchema, SecretApprovalPoliciesSchema, @@ -271,3 +273,7 @@ export const SanitizedTagSchema = SecretTagsSchema.pick({ }).extend({ name: z.string() }); + +export const InternalCertificateAuthorityResponseSchema = CertificateAuthoritiesSchema.merge( + InternalCertificateAuthoritiesSchema +); diff --git a/backend/src/server/routes/v1/certificate-authority-router.ts b/backend/src/server/routes/v1/certificate-authority-router.ts index 7160e1068..8193a2b40 100644 --- a/backend/src/server/routes/v1/certificate-authority-router.ts +++ b/backend/src/server/routes/v1/certificate-authority-router.ts @@ -1,7 +1,7 @@ /* eslint-disable @typescript-eslint/no-floating-promises */ import { z } from "zod"; -import { CertificateAuthoritiesSchema, CertificateTemplatesSchema } from "@app/db/schemas"; +import { CertificateTemplatesSchema } from "@app/db/schemas"; import { EventType } from "@app/ee/services/audit-log/audit-log-types"; import { ApiDocsTags, CERTIFICATE_AUTHORITIES } from "@app/lib/api-docs"; import { ms } from "@app/lib/ms"; @@ -17,6 +17,8 @@ import { } from "@app/services/certificate-authority/certificate-authority-validators"; import { PostHogEventTypes } from "@app/services/telemetry/telemetry-types"; +import { InternalCertificateAuthorityResponseSchema } from "../sanitizedSchemas"; + export const registerCaRouter = async (server: FastifyZodProvider) => { server.route({ method: "POST", @@ -68,7 +70,7 @@ export const registerCaRouter = async (server: FastifyZodProvider) => { ), response: { 200: z.object({ - ca: CertificateAuthoritiesSchema + ca: InternalCertificateAuthorityResponseSchema }) } }, @@ -115,7 +117,7 @@ export const registerCaRouter = async (server: FastifyZodProvider) => { }), response: { 200: z.object({ - ca: CertificateAuthoritiesSchema + ca: InternalCertificateAuthorityResponseSchema }) } }, @@ -198,7 +200,7 @@ export const registerCaRouter = async (server: FastifyZodProvider) => { }), response: { 200: z.object({ - ca: CertificateAuthoritiesSchema + ca: InternalCertificateAuthorityResponseSchema }) } }, @@ -247,7 +249,7 @@ export const registerCaRouter = async (server: FastifyZodProvider) => { }), response: { 200: z.object({ - ca: CertificateAuthoritiesSchema + ca: InternalCertificateAuthorityResponseSchema }) } }, diff --git a/backend/src/server/routes/v1/certificate-router.ts b/backend/src/server/routes/v1/certificate-router.ts index dad1d9a80..067055587 100644 --- a/backend/src/server/routes/v1/certificate-router.ts +++ b/backend/src/server/routes/v1/certificate-router.ts @@ -242,7 +242,7 @@ export const registerCertRouter = async (server: FastifyZodProvider) => { }, handler: async (req) => { const { certificate, certificateChain, issuingCaCertificate, privateKey, serialNumber, ca } = - await server.services.certificateAuthority.issueCertFromCa({ + await server.services.internalCertificateAuthority.issueCertFromCa({ actor: req.permission.type, actorId: req.permission.id, actorAuthMethod: req.permission.authMethod, @@ -355,7 +355,7 @@ export const registerCertRouter = async (server: FastifyZodProvider) => { }, handler: async (req) => { const { certificate, certificateChain, issuingCaCertificate, serialNumber, ca, commonName } = - await server.services.certificateAuthority.signCertFromCa({ + await server.services.internalCertificateAuthority.signCertFromCa({ isInternal: false, actor: req.permission.type, actorId: req.permission.id, diff --git a/backend/src/server/routes/v2/project-router.ts b/backend/src/server/routes/v2/project-router.ts index 3d92bfb1a..ed578eb6f 100644 --- a/backend/src/server/routes/v2/project-router.ts +++ b/backend/src/server/routes/v2/project-router.ts @@ -1,7 +1,6 @@ import { z } from "zod"; import { - CertificateAuthoritiesSchema, CertificatesSchema, PkiAlertsSchema, PkiCollectionsSchema, @@ -28,7 +27,7 @@ import { sanitizedPkiSubscriber } from "@app/services/pki-subscriber/pki-subscri import { ProjectFilterType } from "@app/services/project/project-types"; import { PostHogEventTypes } from "@app/services/telemetry/telemetry-types"; -import { SanitizedProjectSchema } from "../sanitizedSchemas"; +import { InternalCertificateAuthorityResponseSchema, SanitizedProjectSchema } from "../sanitizedSchemas"; const projectWithEnv = SanitizedProjectSchema.extend({ _id: z.string(), @@ -366,7 +365,7 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => { }), response: { 200: z.object({ - cas: z.array(CertificateAuthoritiesSchema) + cas: z.array(InternalCertificateAuthorityResponseSchema) }) } }, diff --git a/backend/src/services/certificate-authority/certificate-authority-dal.ts b/backend/src/services/certificate-authority/certificate-authority-dal.ts index 35a390226..27eba2390 100644 --- a/backend/src/services/certificate-authority/certificate-authority-dal.ts +++ b/backend/src/services/certificate-authority/certificate-authority-dal.ts @@ -1,12 +1,16 @@ import { Knex } from "knex"; import { TDbClient } from "@app/db"; -import { CertificateAuthoritiesSchema, TableName } from "@app/db/schemas"; +import { CertificateAuthoritiesSchema, TableName, TCertificateAuthorities } from "@app/db/schemas"; import { DatabaseError } from "@app/lib/errors"; -import { ormify, selectAllTableCols } from "@app/lib/knex"; +import { ormify, selectAllTableCols, TFindOpt } from "@app/lib/knex"; export type TCertificateAuthorityDALFactory = ReturnType; +export type TCertificateAuthorityWithAssociatedCa = Awaited< + ReturnType +>; + export const certificateAuthorityDALFactory = (db: TDbClient) => { const caOrm = ormify(db, TableName.CertificateAuthority); @@ -109,8 +113,91 @@ export const certificateAuthorityDALFactory = (db: TDbClient) => { } }; + const findWithAssociatedCa = async ( + filter: Parameters<(typeof caOrm)["find"]>[0] & { dn?: string }, + { offset, limit, sort = [["createdAt", "desc"]] }: TFindOpt = {}, + tx?: Knex + ) => { + try { + const query = (tx || db.replicaNode())(TableName.CertificateAuthority) + .leftJoin( + TableName.InternalCertificateAuthority, + `${TableName.CertificateAuthority}.id`, + `${TableName.InternalCertificateAuthority}.certificateAuthorityId` + ) + .where(filter) + .select(selectAllTableCols(TableName.CertificateAuthority)) + .select( + db.ref("id").withSchema(TableName.InternalCertificateAuthority).as("internalCaId"), + db.ref("parentCaId").withSchema(TableName.InternalCertificateAuthority).as("internalParentCaId"), + db.ref("type").withSchema(TableName.InternalCertificateAuthority).as("internalType"), + db.ref("status").withSchema(TableName.InternalCertificateAuthority).as("internalStatus"), + db.ref("friendlyName").withSchema(TableName.InternalCertificateAuthority).as("internalFriendlyName"), + db.ref("organization").withSchema(TableName.InternalCertificateAuthority).as("internalOrganization"), + db.ref("ou").withSchema(TableName.InternalCertificateAuthority).as("internalOu"), + db.ref("country").withSchema(TableName.InternalCertificateAuthority).as("internalCountry"), + db.ref("province").withSchema(TableName.InternalCertificateAuthority).as("internalProvince"), + db.ref("locality").withSchema(TableName.InternalCertificateAuthority).as("internalLocality"), + db.ref("commonName").withSchema(TableName.InternalCertificateAuthority).as("internalCommonName"), + db.ref("dn").withSchema(TableName.InternalCertificateAuthority).as("internalDn"), + db.ref("serialNumber").withSchema(TableName.InternalCertificateAuthority).as("internalSerialNumber"), + db.ref("maxPathLength").withSchema(TableName.InternalCertificateAuthority).as("internalMaxPathLength"), + db.ref("keyAlgorithm").withSchema(TableName.InternalCertificateAuthority).as("internalKeyAlgorithm"), + db.ref("notBefore").withSchema(TableName.InternalCertificateAuthority).as("internalNotBefore"), + db.ref("notAfter").withSchema(TableName.InternalCertificateAuthority).as("internalNotAfter"), + db.ref("activeCaCertId").withSchema(TableName.InternalCertificateAuthority).as("internalActiveCaCertId"), + db + .ref("certificateAuthorityId") + .withSchema(TableName.InternalCertificateAuthority) + .as("internalCertificateAuthorityId") + ); + + if (limit) void query.limit(limit); + if (offset) void query.offset(offset); + if (sort) { + void query.orderBy( + sort.map(([column, order, nulls]) => ({ + column, + order, + nulls + })) + ); + } + + return (await query).map((ca) => ({ + ...CertificateAuthoritiesSchema.parse(ca), + internalCa: ca + ? { + id: ca.internalCaId, + parentCaId: ca.internalParentCaId, + type: ca.internalType, + status: ca.internalStatus, + friendlyName: ca.internalFriendlyName, + organization: ca.internalOrganization, + ou: ca.internalOu, + country: ca.internalCountry, + province: ca.internalProvince, + locality: ca.internalLocality, + commonName: ca.internalCommonName, + dn: ca.internalDn, + serialNumber: ca.internalSerialNumber, + maxPathLength: ca.internalMaxPathLength, + keyAlgorithm: ca.internalKeyAlgorithm, + notBefore: ca.internalNotBefore, + notAfter: ca.internalNotAfter, + activeCaCertId: ca.internalActiveCaCertId, + certificateAuthorityId: ca.internalCertificateAuthorityId + } + : undefined + })); + } catch (error) { + throw new DatabaseError({ error, name: "Find - Certificate Authority" }); + } + }; + return { ...caOrm, + findWithAssociatedCa, buildCertificateChain, findByIdWithAssociatedCa }; diff --git a/backend/src/services/certificate-authority/certificate-authority-fns.ts b/backend/src/services/certificate-authority/certificate-authority-fns.ts index 1ee13428f..f6f6cee66 100644 --- a/backend/src/services/certificate-authority/certificate-authority-fns.ts +++ b/backend/src/services/certificate-authority/certificate-authority-fns.ts @@ -113,8 +113,8 @@ export const getCaCredentials = async ({ projectDAL, kmsService }: TGetCaCredentialsDTO) => { - const ca = await certificateAuthorityDAL.findById(caId); - if (!ca) throw new NotFoundError({ message: `CA with ID '${caId}' not found` }); + const ca = await certificateAuthorityDAL.findByIdWithAssociatedCa(caId); + if (!ca?.internalCa) throw new NotFoundError({ message: `CA with ID '${caId}' not found` }); const caSecret = await certificateAuthoritySecretDAL.findOne({ caId }); if (!caSecret) throw new NotFoundError({ message: `CA secret for CA with ID '${caId}' not found` }); @@ -132,7 +132,7 @@ export const getCaCredentials = async ({ cipherTextBlob: caSecret.encryptedPrivateKey }); - const alg = keyAlgorithmToAlgCfg(ca.keyAlgorithm as CertKeyAlgorithm); + const alg = keyAlgorithmToAlgCfg(ca.internalCa.keyAlgorithm as CertKeyAlgorithm); const skObj = crypto.createPrivateKey({ key: decryptedPrivateKey, format: "der", type: "pkcs8" }); const caPrivateKey = await crypto.subtle.importKey( "pkcs8", @@ -256,12 +256,12 @@ export const rebuildCaCrl = async ({ certificateDAL, kmsService }: TRebuildCaCrlDTO) => { - const ca = await certificateAuthorityDAL.findById(caId); - if (!ca) throw new NotFoundError({ message: `CA with ID '${caId}' not found` }); + const ca = await certificateAuthorityDAL.findByIdWithAssociatedCa(caId); + if (!ca?.internalCa) throw new NotFoundError({ message: `CA with ID '${caId}' not found` }); const caSecret = await certificateAuthoritySecretDAL.findOne({ caId: ca.id }); - const alg = keyAlgorithmToAlgCfg(ca.keyAlgorithm as CertKeyAlgorithm); + const alg = keyAlgorithmToAlgCfg(ca.internalCa.keyAlgorithm as CertKeyAlgorithm); const keyId = await getProjectKmsCertificateKeyId({ projectId: ca.projectId, @@ -288,7 +288,7 @@ export const rebuildCaCrl = async ({ }); const crl = await x509.X509CrlGenerator.create({ - issuer: ca.dn, + issuer: ca.internalCa.dn, thisUpdate: new Date(), nextUpdate: new Date("2025/12/12"), entries: revokedCerts.map((revokedCert) => { @@ -322,7 +322,12 @@ export const rebuildCaCrl = async ({ export const expandInternalCa = ( ca: Awaited> -) => ({ - ...ca, - ...ca.internalCa -}); +) => { + if (!ca.internalCa) { + throw new Error("Internal CA must be defined"); + } + return { + ...ca.internalCa, + ...ca + } as const; +}; diff --git a/backend/src/services/certificate-authority/certificate-authority-queue.ts b/backend/src/services/certificate-authority/certificate-authority-queue.ts index 8c6d3906d..efa741692 100644 --- a/backend/src/services/certificate-authority/certificate-authority-queue.ts +++ b/backend/src/services/certificate-authority/certificate-authority-queue.ts @@ -75,12 +75,12 @@ export const certificateAuthorityQueueFactory = ({ const { caId } = job.data; logger.info(`secretReminderQueue.process: [secretDocument=${caId}]`); - const ca = await certificateAuthorityDAL.findById(caId); - if (!ca) throw new NotFoundError({ message: `CA with ID '${caId}' not found` }); + const ca = await certificateAuthorityDAL.findByIdWithAssociatedCa(caId); + if (!ca.internalCa) throw new NotFoundError({ message: `CA with ID '${caId}' not found` }); const caSecret = await certificateAuthoritySecretDAL.findOne({ caId: ca.id }); - const alg = keyAlgorithmToAlgCfg(ca.keyAlgorithm as CertKeyAlgorithm); + const alg = keyAlgorithmToAlgCfg(ca.internalCa.keyAlgorithm as CertKeyAlgorithm); const keyId = await getProjectKmsCertificateKeyId({ projectId: ca.projectId, @@ -106,7 +106,7 @@ export const certificateAuthorityQueueFactory = ({ }); const crl = await x509.X509CrlGenerator.create({ - issuer: ca.dn, + issuer: ca.internalCa.dn, thisUpdate: new Date(), nextUpdate: new Date("2025/12/12"), // TODO: depends on configured rebuild interval entries: revokedCerts.map((revokedCert) => { @@ -115,7 +115,7 @@ export const certificateAuthorityQueueFactory = ({ revocationDate: new Date(revokedCert.revokedAt as Date), reason: revokedCert.revocationReason as number, invalidity: new Date("2022/01/01"), - issuer: ca.dn + issuer: ca.internalCa?.dn }; }), signingAlgorithm: alg, diff --git a/backend/src/services/certificate-authority/certificate-authority-service.ts b/backend/src/services/certificate-authority/certificate-authority-service.ts index d504e38ed..23674d70f 100644 --- a/backend/src/services/certificate-authority/certificate-authority-service.ts +++ b/backend/src/services/certificate-authority/certificate-authority-service.ts @@ -1,1905 +1,5 @@ -/* eslint-disable no-bitwise */ -import { ForbiddenError } from "@casl/ability"; -import * as x509 from "@peculiar/x509"; -import crypto, { KeyObject } from "crypto"; -import { z } from "zod"; - -import { ActionProjectType, ProjectType, TCertificateAuthorities, TCertificateTemplates } from "@app/db/schemas"; -import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service"; -import { - ProjectPermissionActions, - ProjectPermissionCertificateActions, - ProjectPermissionSub -} from "@app/ee/services/permission/project-permission"; -import { extractX509CertFromChain } from "@app/lib/certificates/extract-certificate"; -import { getConfig } from "@app/lib/config/env"; -import { BadRequestError, NotFoundError } from "@app/lib/errors"; -import { ms } from "@app/lib/ms"; -import { isFQDN } from "@app/lib/validator/validate-url"; -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"; - -import { TCertificateAuthorityCrlDALFactory } from "../../ee/services/certificate-authority-crl/certificate-authority-crl-dal"; -import { TCertificateSecretDALFactory } from "../certificate/certificate-secret-dal"; -import { - CertExtendedKeyUsage, - CertExtendedKeyUsageOIDToName, - CertKeyAlgorithm, - CertKeyUsage, - CertStatus -} from "../certificate/certificate-types"; -import { TCertificateTemplateDALFactory } from "../certificate-template/certificate-template-dal"; -import { validateCertificateDetailsAgainstTemplate } from "../certificate-template/certificate-template-fns"; -import { TCertificateAuthorityCertDALFactory } from "./certificate-authority-cert-dal"; -import { TCertificateAuthorityDALFactory } from "./certificate-authority-dal"; -import { - createDistinguishedName, - createSerialNumber, - getCaCertChain, // TODO: consider rename - getCaCertChains, - getCaCredentials, - keyAlgorithmToAlgCfg, - parseDistinguishedName -} from "./certificate-authority-fns"; -import { TCertificateAuthorityQueueFactory } from "./certificate-authority-queue"; -import { TCertificateAuthoritySecretDALFactory } from "./certificate-authority-secret-dal"; -import { - CaStatus, - CaType, - TCreateCaDTO, - TDeleteCaDTO, - TGetCaCertDTO, - TGetCaCertificateTemplatesDTO, - TGetCaCertsDTO, - TGetCaCsrDTO, - TGetCaDTO, - TImportCertToCaDTO, - TIssueCertFromCaDTO, - TRenewCaCertDTO, - TSignCertFromCaDTO, - TSignIntermediateDTO, - TUpdateCaDTO -} from "./certificate-authority-types"; - -type TCertificateAuthorityServiceFactoryDep = { - certificateAuthorityDAL: Pick< - TCertificateAuthorityDALFactory, - "transaction" | "create" | "findById" | "updateById" | "deleteById" | "findOne" - >; - certificateAuthorityCertDAL: Pick< - TCertificateAuthorityCertDALFactory, - "create" | "findOne" | "transaction" | "find" | "findById" - >; - certificateAuthoritySecretDAL: Pick; - certificateAuthorityCrlDAL: Pick; - certificateTemplateDAL: Pick; - certificateAuthorityQueue: TCertificateAuthorityQueueFactory; // TODO: Pick - certificateDAL: Pick; - certificateSecretDAL: Pick; - certificateBodyDAL: Pick; - pkiCollectionDAL: Pick; - pkiCollectionItemDAL: Pick; - projectDAL: Pick< - TProjectDALFactory, - "findProjectBySlug" | "findOne" | "updateById" | "findById" | "transaction" | "getProjectFromSplitId" - >; - kmsService: Pick; - permissionService: Pick; -}; +type TCertificateAuthorityServiceFactoryDep = {}; export type TCertificateAuthorityServiceFactory = ReturnType; -export const certificateAuthorityServiceFactory = ({ - certificateAuthorityDAL, - certificateAuthorityCertDAL, - certificateAuthoritySecretDAL, - certificateAuthorityCrlDAL, - certificateTemplateDAL, - certificateDAL, - certificateBodyDAL, - certificateSecretDAL, - pkiCollectionDAL, - pkiCollectionItemDAL, - projectDAL, - kmsService, - permissionService -}: TCertificateAuthorityServiceFactoryDep) => { - /** - * Generates new root or intermediate CA - */ - const createCa = async ({ - projectSlug, - type, - friendlyName, - commonName, - organization, - ou, - country, - province, - locality, - notBefore, - notAfter, - maxPathLength, - keyAlgorithm, - requireTemplateForIssuance, - actorId, - actorAuthMethod, - actor, - actorOrgId - }: TCreateCaDTO) => { - const project = await projectDAL.findProjectBySlug(projectSlug, actorOrgId); - if (!project) throw new NotFoundError({ message: `Project with slug '${projectSlug}' not found` }); - let projectId = project.id; - - const certManagerProjectFromSplit = await projectDAL.getProjectFromSplitId( - projectId, - ProjectType.CertificateManager - ); - if (certManagerProjectFromSplit) { - projectId = certManagerProjectFromSplit.id; - } - - const { permission } = await permissionService.getProjectPermission({ - actor, - actorId, - projectId, - actorAuthMethod, - actorOrgId, - actionProjectType: ActionProjectType.CertificateManager - }); - - ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionActions.Create, - ProjectPermissionSub.CertificateAuthorities - ); - - const dn = createDistinguishedName({ - commonName, - organization, - ou, - country, - province, - locality - }); - - const alg = keyAlgorithmToAlgCfg(keyAlgorithm); - const keys = await crypto.subtle.generateKey(alg, true, ["sign", "verify"]); - - const newCa = await certificateAuthorityDAL.transaction(async (tx) => { - const notBeforeDate = notBefore ? new Date(notBefore) : new Date(); - - // if undefined, set [notAfterDate] to 10 years from now - const notAfterDate = notAfter - ? new Date(notAfter) - : new Date(new Date().setFullYear(new Date().getFullYear() + 10)); - - const serialNumber = createSerialNumber(); - - const ca = await certificateAuthorityDAL.create( - { - projectId, - type, - organization, - ou, - country, - province, - locality, - friendlyName: friendlyName || dn, - commonName, - status: type === CaType.ROOT ? CaStatus.ACTIVE : CaStatus.PENDING_CERTIFICATE, - dn, - keyAlgorithm, - ...(type === CaType.ROOT && { - maxPathLength, - notBefore: notBeforeDate, - notAfter: notAfterDate, - serialNumber - }), - requireTemplateForIssuance - }, - tx - ); - - const certificateManagerKmsId = await getProjectKmsCertificateKeyId({ - projectId, - projectDAL, - kmsService - }); - const kmsEncryptor = await kmsService.encryptWithKmsKey({ - kmsId: certificateManagerKmsId - }); - - // https://nodejs.org/api/crypto.html#static-method-keyobjectfromkey - const skObj = KeyObject.from(keys.privateKey); - - const { cipherTextBlob: encryptedPrivateKey } = await kmsEncryptor({ - plainText: skObj.export({ - type: "pkcs8", - format: "der" - }) - }); - - const caSecret = await certificateAuthoritySecretDAL.create( - { - caId: ca.id, - encryptedPrivateKey - }, - tx - ); - - if (type === CaType.ROOT) { - // note: create self-signed cert only applicable for root CA - const cert = await x509.X509CertificateGenerator.createSelfSigned({ - name: dn, - serialNumber, - notBefore: notBeforeDate, - notAfter: notAfterDate, - signingAlgorithm: alg, - keys, - extensions: [ - new x509.BasicConstraintsExtension(true, maxPathLength === -1 ? undefined : maxPathLength, true), - // eslint-disable-next-line no-bitwise - new x509.KeyUsagesExtension(x509.KeyUsageFlags.keyCertSign | x509.KeyUsageFlags.cRLSign, true), - await x509.SubjectKeyIdentifierExtension.create(keys.publicKey) - ] - }); - - const { cipherTextBlob: encryptedCertificate } = await kmsEncryptor({ - plainText: Buffer.from(new Uint8Array(cert.rawData)) - }); - - const { cipherTextBlob: encryptedCertificateChain } = await kmsEncryptor({ - plainText: Buffer.alloc(0) - }); - - const caCert = await certificateAuthorityCertDAL.create( - { - caId: ca.id, - encryptedCertificate, - encryptedCertificateChain, - version: 1, - caSecretId: caSecret.id - }, - tx - ); - - await certificateAuthorityDAL.updateById( - ca.id, - { - activeCaCertId: caCert.id - }, - tx - ); - } - - // create empty CRL - const crl = await x509.X509CrlGenerator.create({ - issuer: ca.dn, - thisUpdate: new Date(), - nextUpdate: new Date("2025/12/12"), // TODO: change - entries: [], - signingAlgorithm: alg, - signingKey: keys.privateKey - }); - - const { cipherTextBlob: encryptedCrl } = await kmsEncryptor({ - plainText: Buffer.from(new Uint8Array(crl.rawData)) - }); - - await certificateAuthorityCrlDAL.create( - { - caId: ca.id, - encryptedCrl, - caSecretId: caSecret.id - }, - tx - ); - - return ca; - }); - - return newCa; - }; - - /** - * Return CA with id [caId] - */ - const getCaById = async ({ caId, actorId, actorAuthMethod, actor, actorOrgId }: TGetCaDTO) => { - const ca = await certificateAuthorityDAL.findById(caId); - if (!ca) throw new NotFoundError({ message: `CA with ID '${caId}' not found` }); - - const { permission } = await permissionService.getProjectPermission({ - actor, - actorId, - projectId: ca.projectId, - actorAuthMethod, - actorOrgId, - actionProjectType: ActionProjectType.CertificateManager - }); - ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionActions.Read, - ProjectPermissionSub.CertificateAuthorities - ); - - return ca; - }; - - /** - * Update CA with id [caId]. - * Note: Used to enable/disable CA - */ - const updateCaById = async ({ - caId, - status, - requireTemplateForIssuance, - actorId, - actorAuthMethod, - actor, - actorOrgId - }: TUpdateCaDTO) => { - const ca = await certificateAuthorityDAL.findById(caId); - if (!ca) throw new NotFoundError({ message: `CA with ID '${caId}' not found` }); - - const { permission } = await permissionService.getProjectPermission({ - actor, - actorId, - projectId: ca.projectId, - actorAuthMethod, - actorOrgId, - actionProjectType: ActionProjectType.CertificateManager - }); - - ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionActions.Edit, - ProjectPermissionSub.CertificateAuthorities - ); - - const updatedCa = await certificateAuthorityDAL.updateById(caId, { status, requireTemplateForIssuance }); - - return updatedCa; - }; - - /** - * Delete CA with id [caId] - */ - const deleteCaById = async ({ caId, actorId, actorAuthMethod, actor, actorOrgId }: TDeleteCaDTO) => { - const ca = await certificateAuthorityDAL.findById(caId); - if (!ca) throw new NotFoundError({ message: `CA with ID '${caId}' not found` }); - - const { permission } = await permissionService.getProjectPermission({ - actor, - actorId, - projectId: ca.projectId, - actorAuthMethod, - actorOrgId, - actionProjectType: ActionProjectType.CertificateManager - }); - - ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionActions.Delete, - ProjectPermissionSub.CertificateAuthorities - ); - - const deletedCa = await certificateAuthorityDAL.deleteById(caId); - - return deletedCa; - }; - - /** - * Return certificate signing request (CSR) made with CA with id [caId] - */ - const getCaCsr = async ({ caId, actorId, actorAuthMethod, actor, actorOrgId }: TGetCaCsrDTO) => { - const ca = await certificateAuthorityDAL.findById(caId); - if (!ca) throw new NotFoundError({ message: `CA with ID '${caId}' not found` }); - - const { permission } = await permissionService.getProjectPermission({ - actor, - actorId, - projectId: ca.projectId, - actorAuthMethod, - actorOrgId, - actionProjectType: ActionProjectType.CertificateManager - }); - - ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionActions.Create, - ProjectPermissionSub.CertificateAuthorities - ); - - if (ca.type === CaType.ROOT) throw new BadRequestError({ message: "Root CA cannot generate CSR" }); - - const { caPrivateKey, caPublicKey } = await getCaCredentials({ - caId, - certificateAuthorityDAL, - certificateAuthoritySecretDAL, - projectDAL, - kmsService - }); - - const alg = keyAlgorithmToAlgCfg(ca.keyAlgorithm as CertKeyAlgorithm); - - const csrObj = await x509.Pkcs10CertificateRequestGenerator.create({ - name: ca.dn, - keys: { - privateKey: caPrivateKey, - publicKey: caPublicKey - }, - signingAlgorithm: alg, - extensions: [ - // eslint-disable-next-line no-bitwise - new x509.KeyUsagesExtension( - x509.KeyUsageFlags.keyCertSign | - x509.KeyUsageFlags.cRLSign | - x509.KeyUsageFlags.digitalSignature | - x509.KeyUsageFlags.keyEncipherment - ) - ], - attributes: [new x509.ChallengePasswordAttribute("password")] - }); - - return { - csr: csrObj.toString("pem"), - ca - }; - }; - - /** - * Renew certificate for CA with id [caId] - * Note 1: This CA renewal method is only applicable to CAs with internal parent CAs - * Note 2: Currently implements CA renewal with same key-pair only - */ - const renewCaCert = async ({ caId, notAfter, actorId, actorAuthMethod, actor, actorOrgId }: TRenewCaCertDTO) => { - const ca = await certificateAuthorityDAL.findById(caId); - if (!ca) throw new NotFoundError({ message: `CA with ID '${caId}' not found` }); - - if (!ca.activeCaCertId) throw new BadRequestError({ message: "CA does not have a certificate installed" }); - - const { permission } = await permissionService.getProjectPermission({ - actor, - actorId, - projectId: ca.projectId, - actorAuthMethod, - actorOrgId, - actionProjectType: ActionProjectType.CertificateManager - }); - - ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionActions.Create, - ProjectPermissionSub.CertificateAuthorities - ); - - if (ca.status === CaStatus.DISABLED) throw new BadRequestError({ message: "CA is disabled" }); - - // get latest CA certificate - const caCert = await certificateAuthorityCertDAL.findById(ca.activeCaCertId); - - const serialNumber = createSerialNumber(); - - const certificateManagerKmsId = await getProjectKmsCertificateKeyId({ - projectId: ca.projectId, - projectDAL, - kmsService - }); - - const kmsEncryptor = await kmsService.encryptWithKmsKey({ - kmsId: certificateManagerKmsId - }); - - const { caPrivateKey, caPublicKey, caSecret } = await getCaCredentials({ - caId: ca.id, - certificateAuthorityDAL, - certificateAuthoritySecretDAL, - projectDAL, - kmsService - }); - - const alg = keyAlgorithmToAlgCfg(ca.keyAlgorithm as CertKeyAlgorithm); - - const kmsDecryptor = await kmsService.decryptWithKmsKey({ - kmsId: certificateManagerKmsId - }); - const decryptedCaCert = await kmsDecryptor({ - cipherTextBlob: caCert.encryptedCertificate - }); - - const caCertObj = new x509.X509Certificate(decryptedCaCert); - - let certificate = ""; - let certificateChain = ""; - - switch (ca.type) { - case CaType.ROOT: { - if (new Date(notAfter) <= new Date(caCertObj.notAfter)) { - throw new BadRequestError({ - message: - "New Root CA certificate must have notAfter date that is greater than the current certificate notAfter date" - }); - } - - const notBeforeDate = new Date(); - const cert = await x509.X509CertificateGenerator.createSelfSigned({ - name: ca.dn, - serialNumber, - notBefore: notBeforeDate, - notAfter: new Date(notAfter), - signingAlgorithm: alg, - keys: { - privateKey: caPrivateKey, - publicKey: caPublicKey - }, - extensions: [ - new x509.BasicConstraintsExtension( - true, - ca.maxPathLength === -1 || !ca.maxPathLength ? undefined : ca.maxPathLength, - true - ), - // eslint-disable-next-line no-bitwise - new x509.KeyUsagesExtension(x509.KeyUsageFlags.keyCertSign | x509.KeyUsageFlags.cRLSign, true), - await x509.SubjectKeyIdentifierExtension.create(caPublicKey) - ] - }); - - const { cipherTextBlob: encryptedCertificate } = await kmsEncryptor({ - plainText: Buffer.from(new Uint8Array(cert.rawData)) - }); - - const { cipherTextBlob: encryptedCertificateChain } = await kmsEncryptor({ - plainText: Buffer.alloc(0) - }); - - await certificateAuthorityDAL.transaction(async (tx) => { - const newCaCert = await certificateAuthorityCertDAL.create( - { - caId: ca.id, - encryptedCertificate, - encryptedCertificateChain, - version: caCert.version + 1, - caSecretId: caSecret.id - }, - tx - ); - - await certificateAuthorityDAL.updateById( - ca.id, - { - activeCaCertId: newCaCert.id, - notBefore: notBeforeDate, - notAfter: new Date(notAfter) - }, - tx - ); - }); - - certificate = cert.toString("pem"); - break; - } - case CaType.INTERMEDIATE: { - if (!ca.parentCaId) { - // TODO: look into optimal way to support renewal of intermediate CA with external parent CA - throw new BadRequestError({ - message: "Failed to renew intermediate CA certificate with external parent CA" - }); - } - - const parentCa = await certificateAuthorityDAL.findById(ca.parentCaId); - const { caPrivateKey: parentCaPrivateKey } = await getCaCredentials({ - caId: parentCa.id, - certificateAuthorityDAL, - certificateAuthoritySecretDAL, - projectDAL, - kmsService - }); - - // get latest parent CA certificate - 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 - }); - - const parentCaCertObj = new x509.X509Certificate(decryptedParentCaCert); - - if (new Date(notAfter) <= new Date(caCertObj.notAfter)) { - throw new BadRequestError({ - message: - "New Intermediate CA certificate must have notAfter date that is greater than the current certificate notAfter date" - }); - } - - if (new Date(notAfter) > new Date(parentCaCertObj.notAfter)) { - throw new BadRequestError({ - message: - "New Intermediate CA certificate must have notAfter date that is equal to or smaller than the notAfter date of the parent CA certificate current certificate notAfter date" - }); - } - - const csrObj = await x509.Pkcs10CertificateRequestGenerator.create({ - name: ca.dn, - keys: { - privateKey: caPrivateKey, - publicKey: caPublicKey - }, - signingAlgorithm: alg, - extensions: [ - // eslint-disable-next-line no-bitwise - new x509.KeyUsagesExtension( - x509.KeyUsageFlags.keyCertSign | - x509.KeyUsageFlags.cRLSign | - x509.KeyUsageFlags.digitalSignature | - x509.KeyUsageFlags.keyEncipherment - ) - ], - attributes: [new x509.ChallengePasswordAttribute("password")] - }); - - const notBeforeDate = new Date(); - const intermediateCert = await x509.X509CertificateGenerator.create({ - serialNumber, - subject: csrObj.subject, - issuer: parentCaCertObj.subject, - notBefore: notBeforeDate, - notAfter: new Date(notAfter), - signingKey: parentCaPrivateKey, - publicKey: csrObj.publicKey, - signingAlgorithm: alg, - extensions: [ - new x509.KeyUsagesExtension( - x509.KeyUsageFlags.keyCertSign | - x509.KeyUsageFlags.cRLSign | - x509.KeyUsageFlags.digitalSignature | - x509.KeyUsageFlags.keyEncipherment, - true - ), - new x509.BasicConstraintsExtension( - true, - ca.maxPathLength === -1 || !ca.maxPathLength ? undefined : ca.maxPathLength, - true - ), - await x509.AuthorityKeyIdentifierExtension.create(parentCaCertObj, false), - await x509.SubjectKeyIdentifierExtension.create(csrObj.publicKey) - ] - }); - - const { cipherTextBlob: encryptedCertificate } = await kmsEncryptor({ - plainText: Buffer.from(new Uint8Array(intermediateCert.rawData)) - }); - - const { caCert: parentCaCertificate, caCertChain: parentCaCertChain } = await getCaCertChain({ - caCertId: parentCa.activeCaCertId, - certificateAuthorityDAL, - certificateAuthorityCertDAL, - projectDAL, - kmsService - }); - - certificateChain = `${parentCaCertificate}\n${parentCaCertChain}`.trim(); - - const { cipherTextBlob: encryptedCertificateChain } = await kmsEncryptor({ - plainText: Buffer.from(certificateChain) - }); - - await certificateAuthorityDAL.transaction(async (tx) => { - const newCaCert = await certificateAuthorityCertDAL.create( - { - caId: ca.id, - encryptedCertificate, - encryptedCertificateChain, - version: caCert.version + 1, - caSecretId: caSecret.id - }, - tx - ); - - await certificateAuthorityDAL.updateById( - ca.id, - { - activeCaCertId: newCaCert.id, - notBefore: notBeforeDate, - notAfter: new Date(notAfter) - }, - tx - ); - }); - - certificate = intermediateCert.toString("pem"); - break; - } - default: { - throw new BadRequestError({ - message: "Unrecognized CA type" - }); - } - } - - return { - certificate, - certificateChain, - serialNumber, - ca - }; - }; - - const getCaCerts = async ({ caId, actorId, actorAuthMethod, actor, actorOrgId }: TGetCaCertsDTO) => { - const ca = await certificateAuthorityDAL.findById(caId); - if (!ca) throw new NotFoundError({ message: `CA with ID '${caId}' not found` }); - - const { permission } = await permissionService.getProjectPermission({ - actor, - actorId, - projectId: ca.projectId, - actorAuthMethod, - actorOrgId, - actionProjectType: ActionProjectType.CertificateManager - }); - - ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionActions.Read, - ProjectPermissionSub.CertificateAuthorities - ); - - const caCertChains = await getCaCertChains({ - caId, - certificateAuthorityDAL, - certificateAuthorityCertDAL, - projectDAL, - kmsService - }); - - return { - ca, - caCerts: caCertChains - }; - }; - - /** - * Return current certificate and certificate chain for CA - */ - const getCaCert = async ({ caId, actorId, actorAuthMethod, actor, actorOrgId }: TGetCaCertDTO) => { - const ca = await certificateAuthorityDAL.findById(caId); - if (!ca) throw new NotFoundError({ message: `CA with ID '${caId}' not found` }); - if (!ca.activeCaCertId) throw new BadRequestError({ message: "CA does not have a certificate installed" }); - - const { permission } = await permissionService.getProjectPermission({ - actor, - actorId, - projectId: ca.projectId, - actorAuthMethod, - actorOrgId, - actionProjectType: ActionProjectType.CertificateManager - }); - - ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionActions.Read, - ProjectPermissionSub.CertificateAuthorities - ); - - const { caCert, caCertChain, serialNumber } = await getCaCertChain({ - caCertId: ca.activeCaCertId, - certificateAuthorityDAL, - certificateAuthorityCertDAL, - projectDAL, - kmsService - }); - - return { - certificate: caCert, - certificateChain: caCertChain, - serialNumber, - ca - }; - }; - - /** - * Return CA certificate object by ID - */ - const getCaCertById = async ({ caId, caCertId }: { caId: string; caCertId: string }) => { - const caCert = await certificateAuthorityCertDAL.findOne({ - caId, - id: caCertId - }); - - if (!caCert) { - throw new NotFoundError({ message: `Ca certificate with ID '${caCertId}' not found for CA with ID '${caId}'` }); - } - - const ca = await certificateAuthorityDAL.findById(caId); - const keyId = await getProjectKmsCertificateKeyId({ - projectId: ca.projectId, - projectDAL, - kmsService - }); - - const kmsDecryptor = await kmsService.decryptWithKmsKey({ - kmsId: keyId - }); - - const decryptedCaCert = await kmsDecryptor({ - cipherTextBlob: caCert.encryptedCertificate - }); - - const caCertObj = new x509.X509Certificate(decryptedCaCert); - - return caCertObj; - }; - - /** - * Issue certificate to be imported back in for intermediate CA - */ - const signIntermediate = async ({ - caId, - actorId, - actorAuthMethod, - actor, - actorOrgId, - csr, - notBefore, - notAfter, - maxPathLength - }: TSignIntermediateDTO) => { - const appCfg = getConfig(); - const ca = await certificateAuthorityDAL.findById(caId); - if (!ca) throw new NotFoundError({ message: "CA not found" }); - - const { permission } = await permissionService.getProjectPermission({ - actor, - actorId, - projectId: ca.projectId, - actorAuthMethod, - actorOrgId, - actionProjectType: ActionProjectType.CertificateManager - }); - - ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionActions.Create, - ProjectPermissionSub.CertificateAuthorities - ); - - 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.findById(ca.activeCaCertId); - - if (ca.notAfter && new Date() > new Date(ca.notAfter)) { - throw new BadRequestError({ message: "CA is expired" }); - } - - const alg = keyAlgorithmToAlgCfg(ca.keyAlgorithm as CertKeyAlgorithm); - - const certificateManagerKmsId = await getProjectKmsCertificateKeyId({ - projectId: ca.projectId, - projectDAL, - kmsService - }); - const kmsDecryptor = await kmsService.decryptWithKmsKey({ - kmsId: certificateManagerKmsId - }); - - const decryptedCaCert = await kmsDecryptor({ - cipherTextBlob: caCert.encryptedCertificate - }); - - const caCertObj = new x509.X509Certificate(decryptedCaCert); - const csrObj = new x509.Pkcs10CertificateRequest(csr); - - // check path length constraint - const caPathLength = caCertObj.getExtension(x509.BasicConstraintsExtension)?.pathLength; - if (caPathLength !== undefined) { - if (caPathLength === 0) - throw new BadRequestError({ - message: "Failed to issue intermediate certificate due to CA path length constraint" - }); - if (maxPathLength >= caPathLength || (maxPathLength === -1 && caPathLength !== -1)) - throw new BadRequestError({ - message: "The requested path length constraint exceeds the CA's allowed path length" - }); - } - - const notBeforeDate = notBefore ? new Date(notBefore) : new Date(); - const notAfterDate = new Date(notAfter); - - const caCertNotBeforeDate = new Date(caCertObj.notBefore); - const caCertNotAfterDate = new Date(caCertObj.notAfter); - - // check not before constraint - if (notBeforeDate < caCertNotBeforeDate) { - throw new BadRequestError({ message: "notBefore date is before CA certificate's notBefore date" }); - } - - if (notBeforeDate > notAfterDate) throw new BadRequestError({ message: "notBefore date is after notAfter date" }); - - // check not after constraint - if (notAfterDate > caCertNotAfterDate) { - throw new BadRequestError({ message: "notAfter date is after CA certificate's notAfter date" }); - } - - const { caPrivateKey, caSecret } = await getCaCredentials({ - caId: ca.id, - certificateAuthorityDAL, - certificateAuthoritySecretDAL, - projectDAL, - kmsService - }); - - const serialNumber = createSerialNumber(); - - const caCrl = await certificateAuthorityCrlDAL.findOne({ caSecretId: caSecret.id }); - const distributionPointUrl = `${appCfg.SITE_URL}/api/v1/pki/crl/${caCrl.id}/der`; - - const caIssuerUrl = `${appCfg.SITE_URL}/api/v1/pki/ca/${ca.id}/certificates/${caCert.id}/der`; - const intermediateCert = await x509.X509CertificateGenerator.create({ - serialNumber, - subject: csrObj.subject, - issuer: caCertObj.subject, - notBefore: notBeforeDate, - notAfter: notAfterDate, - signingKey: caPrivateKey, - publicKey: csrObj.publicKey, - signingAlgorithm: alg, - extensions: [ - new x509.KeyUsagesExtension( - x509.KeyUsageFlags.keyCertSign | - x509.KeyUsageFlags.cRLSign | - x509.KeyUsageFlags.digitalSignature | - x509.KeyUsageFlags.keyEncipherment, - true - ), - new x509.BasicConstraintsExtension(true, maxPathLength === -1 ? undefined : maxPathLength, true), - await x509.AuthorityKeyIdentifierExtension.create(caCertObj, false), - await x509.SubjectKeyIdentifierExtension.create(csrObj.publicKey), - new x509.CRLDistributionPointsExtension([distributionPointUrl]), - new x509.AuthorityInfoAccessExtension({ - caIssuers: new x509.GeneralName("url", caIssuerUrl) - }) - ] - }); - - const { caCert: issuingCaCertificate, caCertChain } = await getCaCertChain({ - caCertId: ca.activeCaCertId, - certificateAuthorityDAL, - certificateAuthorityCertDAL, - projectDAL, - kmsService - }); - - return { - certificate: intermediateCert.toString("pem"), - issuingCaCertificate, - certificateChain: `${issuingCaCertificate}\n${caCertChain}`.trim(), - serialNumber: intermediateCert.serialNumber, - ca - }; - }; - - /** - * Import certificate for CA with id [caId]. - * Note: Can be used to import an external certificate and certificate chain - * to be into an installed or uninstalled CA. - */ - const importCertToCa = async ({ - caId, - actorId, - actorAuthMethod, - actor, - actorOrgId, - certificate, - certificateChain - }: TImportCertToCaDTO) => { - const ca = await certificateAuthorityDAL.findById(caId); - if (!ca) throw new NotFoundError({ message: `CA with ID '${caId}' not found` }); - - const { permission } = await permissionService.getProjectPermission({ - actor, - actorId, - projectId: ca.projectId, - actorAuthMethod, - actorOrgId, - actionProjectType: ActionProjectType.CertificateManager - }); - - ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionActions.Create, - ProjectPermissionSub.CertificateAuthorities - ); - - if (ca.parentCaId) { - /** - * re-evaluate in the future if we should allow users to import a new CA certificate for an intermediate - * CA chained to an internal parent CA. Doing so would allow users to re-chain the CA to a different - * internal CA. - */ - throw new BadRequestError({ - message: "Cannot import certificate to intermediate CA chained to internal parent CA" - }); - } - - const caCert = ca.activeCaCertId ? await certificateAuthorityCertDAL.findById(ca.activeCaCertId) : undefined; - - const certObj = new x509.X509Certificate(certificate); - const maxPathLength = certObj.getExtension(x509.BasicConstraintsExtension)?.pathLength; - - // validate imported certificate and certificate chain - const certificates = extractX509CertFromChain(certificateChain)?.map((cert) => new x509.X509Certificate(cert)); - - if (!certificates) throw new BadRequestError({ message: "Failed to parse certificate chain" }); - - const chain = new x509.X509ChainBuilder({ - certificates - }); - - const chainItems = await chain.build(certObj); - - // chain.build() implicitly verifies the chain - if (chainItems.length !== certificates.length + 1) - throw new BadRequestError({ message: "Invalid certificate chain" }); - - const parentCertObj = chainItems[1]; - const parentCertSubject = parentCertObj.subject; - - const parentCa = await certificateAuthorityDAL.findOne({ - projectId: ca.projectId, - dn: parentCertSubject - }); - - const certificateManagerKmsId = await getProjectKmsCertificateKeyId({ - projectId: ca.projectId, - projectDAL, - kmsService - }); - const kmsEncryptor = await kmsService.encryptWithKmsKey({ - kmsId: certificateManagerKmsId - }); - - const { cipherTextBlob: encryptedCertificate } = await kmsEncryptor({ - plainText: Buffer.from(new Uint8Array(certObj.rawData)) - }); - - const { cipherTextBlob: encryptedCertificateChain } = await kmsEncryptor({ - plainText: Buffer.from(certificateChain) - }); - - // TODO: validate that latest key-pair of CA is used to sign the certificate - // once renewal with new key pair is supported - const { caSecret, caPublicKey } = await getCaCredentials({ - caId: ca.id, - certificateAuthorityDAL, - certificateAuthoritySecretDAL, - projectDAL, - kmsService - }); - - const isCaAndCertPublicKeySame = Buffer.from(await crypto.subtle.exportKey("spki", caPublicKey)).equals( - Buffer.from(certObj.publicKey.rawData) - ); - - if (!isCaAndCertPublicKeySame) { - throw new BadRequestError({ message: "CA and certificate public key do not match" }); - } - - await certificateAuthorityCertDAL.transaction(async (tx) => { - const newCaCert = await certificateAuthorityCertDAL.create( - { - caId: ca.id, - encryptedCertificate, - encryptedCertificateChain, - version: caCert ? caCert.version + 1 : 1, - caSecretId: caSecret.id - }, - tx - ); - - await certificateAuthorityDAL.updateById( - ca.id, - { - status: CaStatus.ACTIVE, - maxPathLength: maxPathLength === undefined ? -1 : maxPathLength, - notBefore: new Date(certObj.notBefore), - notAfter: new Date(certObj.notAfter), - serialNumber: certObj.serialNumber, - parentCaId: parentCa?.id, - activeCaCertId: newCaCert.id - }, - tx - ); - }); - - return { ca }; - }; - - /** - * Return new leaf certificate issued by CA with id [caId] and private key. - * Note: private key and CSR are generated within Infisical. - */ - const issueCertFromCa = async ({ - caId, - certificateTemplateId, - pkiCollectionId, - friendlyName, - commonName, - altNames, - ttl, - notBefore, - notAfter, - actorId, - actorAuthMethod, - actor, - actorOrgId, - keyUsages, - extendedKeyUsages - }: TIssueCertFromCaDTO) => { - let ca: TCertificateAuthorities | undefined; - let certificateTemplate: TCertificateTemplates | undefined; - let collectionId = pkiCollectionId; - - if (caId) { - ca = await certificateAuthorityDAL.findById(caId); - } else if (certificateTemplateId) { - certificateTemplate = await certificateTemplateDAL.getById(certificateTemplateId); - if (!certificateTemplate) { - throw new NotFoundError({ - message: `Certificate template with ID '${certificateTemplateId}' not found` - }); - } - - collectionId = certificateTemplate.pkiCollectionId as string; - ca = await certificateAuthorityDAL.findById(certificateTemplate.caId); - } - - if (!ca) { - throw new NotFoundError({ message: `CA with ID '${caId}' not found` }); - } - - const { permission } = await permissionService.getProjectPermission({ - actor, - actorId, - projectId: ca.projectId, - actorAuthMethod, - actorOrgId, - actionProjectType: ActionProjectType.CertificateManager - }); - - ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionCertificateActions.Create, - ProjectPermissionSub.Certificates - ); - - if (ca.status !== CaStatus.ACTIVE) throw new BadRequestError({ message: "CA is not active" }); - if (!ca.activeCaCertId) throw new BadRequestError({ message: "CA does not have a certificate installed" }); - if (ca.requireTemplateForIssuance && !certificateTemplate) { - throw new BadRequestError({ message: "Certificate template is required for issuance" }); - } - 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 (collectionId) { - const pkiCollection = await pkiCollectionDAL.findById(collectionId); - 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, - kmsService - }); - const kmsDecryptor = await kmsService.decryptWithKmsKey({ - kmsId: certificateManagerKmsId - }); - - const decryptedCaCert = await kmsDecryptor({ - cipherTextBlob: caCert.encryptedCertificate - }); - - const caCertObj = new x509.X509Certificate(decryptedCaCert); - - const notBeforeDate = notBefore ? new Date(notBefore) : new Date(); - - let notAfterDate = new Date(new Date().setFullYear(new Date().getFullYear() + 1)); - if (notAfter) { - notAfterDate = new Date(notAfter); - } else if (ttl) { - notAfterDate = new Date(new Date().getTime() + ms(ttl)); - } - - const caCertNotBeforeDate = new Date(caCertObj.notBefore); - const caCertNotAfterDate = new Date(caCertObj.notAfter); - - // check not before constraint - if (notBeforeDate < caCertNotBeforeDate) { - throw new BadRequestError({ message: "notBefore date is before CA certificate's notBefore date" }); - } - - if (notBeforeDate > notAfterDate) throw new BadRequestError({ message: "notBefore date is after notAfter date" }); - - // check not after constraint - if (notAfterDate > caCertNotAfterDate) { - throw new BadRequestError({ message: "notAfter date is after CA certificate's notAfter date" }); - } - - const alg = keyAlgorithmToAlgCfg(ca.keyAlgorithm as CertKeyAlgorithm); - const leafKeys = await crypto.subtle.generateKey(alg, true, ["sign", "verify"]); - - const csrObj = await x509.Pkcs10CertificateRequestGenerator.create({ - name: `CN=${commonName}`, - keys: leafKeys, - signingAlgorithm: alg, - extensions: [ - // eslint-disable-next-line no-bitwise - new x509.KeyUsagesExtension(x509.KeyUsageFlags.digitalSignature | x509.KeyUsageFlags.keyEncipherment) - ], - attributes: [new x509.ChallengePasswordAttribute("password")] - }); - - const { caPrivateKey, caSecret } = await getCaCredentials({ - caId: ca.id, - certificateAuthorityDAL, - certificateAuthoritySecretDAL, - projectDAL, - kmsService - }); - - const caCrl = await certificateAuthorityCrlDAL.findOne({ caSecretId: caSecret.id }); - const appCfg = getConfig(); - - const distributionPointUrl = `${appCfg.SITE_URL}/api/v1/pki/crl/${caCrl.id}/der`; - const caIssuerUrl = `${appCfg.SITE_URL}/api/v1/pki/ca/${ca.id}/certificates/${caCert.id}/der`; - - const extensions: x509.Extension[] = [ - new x509.BasicConstraintsExtension(false), - new x509.CRLDistributionPointsExtension([distributionPointUrl]), - await x509.AuthorityKeyIdentifierExtension.create(caCertObj, false), - await x509.SubjectKeyIdentifierExtension.create(csrObj.publicKey), - new x509.AuthorityInfoAccessExtension({ - caIssuers: new x509.GeneralName("url", caIssuerUrl) - }), - new x509.CertificatePolicyExtension(["2.5.29.32.0"]) // anyPolicy - ]; - - // handle key usages - let selectedKeyUsages: CertKeyUsage[] = keyUsages ?? []; - if (keyUsages === undefined && !certificateTemplate) { - selectedKeyUsages = [CertKeyUsage.DIGITAL_SIGNATURE, CertKeyUsage.KEY_ENCIPHERMENT]; - } - - if (keyUsages === undefined && certificateTemplate) { - selectedKeyUsages = (certificateTemplate.keyUsages ?? []) as CertKeyUsage[]; - } - - if (keyUsages?.length && certificateTemplate) { - const validKeyUsages = certificateTemplate.keyUsages || []; - if (keyUsages.some((keyUsage) => !validKeyUsages.includes(keyUsage))) { - throw new BadRequestError({ - message: "Invalid key usage value based on template policy" - }); - } - selectedKeyUsages = keyUsages; - } - - const keyUsagesBitValue = selectedKeyUsages.reduce((accum, keyUsage) => accum | x509.KeyUsageFlags[keyUsage], 0); - if (keyUsagesBitValue) { - extensions.push(new x509.KeyUsagesExtension(keyUsagesBitValue, true)); - } - - // handle extended key usages - let selectedExtendedKeyUsages: CertExtendedKeyUsage[] = extendedKeyUsages ?? []; - if (extendedKeyUsages === undefined && certificateTemplate) { - selectedExtendedKeyUsages = (certificateTemplate.extendedKeyUsages ?? []) as CertExtendedKeyUsage[]; - } - - if (extendedKeyUsages?.length && certificateTemplate) { - const validExtendedKeyUsages = certificateTemplate.extendedKeyUsages || []; - if (extendedKeyUsages.some((eku) => !validExtendedKeyUsages.includes(eku))) { - throw new BadRequestError({ - message: "Invalid extended key usage value based on template policy" - }); - } - selectedExtendedKeyUsages = extendedKeyUsages; - } - - if (selectedExtendedKeyUsages.length) { - extensions.push( - new x509.ExtendedKeyUsageExtension( - selectedExtendedKeyUsages.map((eku) => x509.ExtendedKeyUsage[eku]), - true - ) - ); - } - - let altNamesArray: { - type: "email" | "dns"; - value: string; - }[] = []; - - if (altNames) { - altNamesArray = altNames - .split(",") - .map((name) => name.trim()) - .map((altName) => { - // check if the altName is a valid email - if (z.string().email().safeParse(altName).success) { - return { - type: "email", - value: altName - }; - } - - // check if the altName is a valid hostname - if (isFQDN(altName, { allow_wildcard: true })) { - return { - type: "dns", - value: altName - }; - } - - // If altName is neither a valid email nor a valid hostname, throw an error or handle it accordingly - throw new Error(`Invalid altName: ${altName}`); - }); - - const altNamesExtension = new x509.SubjectAlternativeNameExtension(altNamesArray, false); - extensions.push(altNamesExtension); - } - - if (certificateTemplate) { - validateCertificateDetailsAgainstTemplate( - { - commonName, - notBeforeDate, - notAfterDate, - altNames: altNamesArray.map((entry) => entry.value) - }, - certificateTemplate - ); - } - - const serialNumber = createSerialNumber(); - const leafCert = await x509.X509CertificateGenerator.create({ - serialNumber, - subject: csrObj.subject, - issuer: caCertObj.subject, - notBefore: notBeforeDate, - notAfter: notAfterDate, - signingKey: caPrivateKey, - publicKey: csrObj.publicKey, - signingAlgorithm: alg, - extensions - }); - - const skLeafObj = KeyObject.from(leafKeys.privateKey); - const skLeaf = skLeafObj.export({ format: "pem", type: "pkcs8" }) as string; - - const kmsEncryptor = await kmsService.encryptWithKmsKey({ - kmsId: certificateManagerKmsId - }); - 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( - { - caId: (ca as TCertificateAuthorities).id, - caCertId: caCert.id, - certificateTemplateId: certificateTemplate?.id, - status: CertStatus.ACTIVE, - friendlyName: friendlyName || commonName, - commonName, - altNames, - serialNumber, - notBefore: notBeforeDate, - notAfter: notAfterDate, - keyUsages: selectedKeyUsages, - extendedKeyUsages: selectedExtendedKeyUsages - }, - tx - ); - - await certificateBodyDAL.create( - { - certId: cert.id, - encryptedCertificate, - encryptedCertificateChain - }, - tx - ); - - await certificateSecretDAL.create( - { - certId: cert.id, - encryptedPrivateKey - }, - tx - ); - - if (collectionId) { - await pkiCollectionItemDAL.create( - { - pkiCollectionId: collectionId, - certId: cert.id - }, - tx - ); - } - - return cert; - }); - - return { - certificate: leafCert.toString("pem"), - certificateChain: certificateChainPem, - issuingCaCertificate, - privateKey: skLeaf, - serialNumber, - ca - }; - }; - - /** - * Return new leaf certificate issued by CA with id [caId]. - * Note: CSR is generated externally and submitted to Infisical. - */ - const signCertFromCa = async (dto: TSignCertFromCaDTO) => { - const appCfg = getConfig(); - let ca: TCertificateAuthorities | undefined; - let certificateTemplate: TCertificateTemplates | undefined; - - const { - caId, - certificateTemplateId, - csr, - pkiCollectionId, - friendlyName, - commonName, - altNames, - ttl, - notBefore, - notAfter, - keyUsages, - extendedKeyUsages - } = dto; - - let collectionId = pkiCollectionId; - - if (caId) { - ca = await certificateAuthorityDAL.findById(caId); - } else if (certificateTemplateId) { - certificateTemplate = await certificateTemplateDAL.getById(certificateTemplateId); - if (!certificateTemplate) { - throw new NotFoundError({ - message: `Certificate template with ID '${certificateTemplateId}' not found` - }); - } - - collectionId = certificateTemplate.pkiCollectionId as string; - ca = await certificateAuthorityDAL.findById(certificateTemplate.caId); - } - - if (!ca) { - throw new NotFoundError({ message: `CA with ID '${caId}' not found` }); - } - - if (!dto.isInternal) { - const { permission } = await permissionService.getProjectPermission({ - actor: dto.actor, - actorId: dto.actorId, - projectId: ca.projectId, - actorAuthMethod: dto.actorAuthMethod, - actorOrgId: dto.actorOrgId, - actionProjectType: ActionProjectType.CertificateManager - }); - - ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionCertificateActions.Create, - ProjectPermissionSub.Certificates - ); - } - - if (ca.status !== CaStatus.ACTIVE) throw new BadRequestError({ message: "CA is not active" }); - if (!ca.activeCaCertId) throw new BadRequestError({ message: "CA does not have a certificate installed" }); - if (ca.requireTemplateForIssuance && !certificateTemplate) { - throw new BadRequestError({ message: "Certificate template is required for issuance" }); - } - - 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 with ID '${pkiCollectionId}' not found` }); - if (pkiCollection.projectId !== ca.projectId) throw new BadRequestError({ message: "Invalid PKI collection" }); - } - - const certificateManagerKmsId = await getProjectKmsCertificateKeyId({ - projectId: ca.projectId, - projectDAL, - kmsService - }); - - const kmsDecryptor = await kmsService.decryptWithKmsKey({ - kmsId: certificateManagerKmsId - }); - - const decryptedCaCert = await kmsDecryptor({ - cipherTextBlob: caCert.encryptedCertificate - }); - - const caCertObj = new x509.X509Certificate(decryptedCaCert); - - const notBeforeDate = notBefore ? new Date(notBefore) : new Date(); - - let notAfterDate = new Date(new Date().setFullYear(new Date().getFullYear() + 1)); - if (notAfter) { - notAfterDate = new Date(notAfter); - } else if (ttl) { - notAfterDate = new Date(new Date().getTime() + ms(ttl)); - } else if (certificateTemplate?.ttl) { - notAfterDate = new Date(new Date().getTime() + ms(certificateTemplate.ttl)); - } - - const caCertNotBeforeDate = new Date(caCertObj.notBefore); - const caCertNotAfterDate = new Date(caCertObj.notAfter); - - // check not before constraint - if (notBeforeDate < caCertNotBeforeDate) { - throw new BadRequestError({ message: "notBefore date is before CA certificate's notBefore date" }); - } - - if (notBeforeDate > notAfterDate) throw new BadRequestError({ message: "notBefore date is after notAfter date" }); - - // check not after constraint - if (notAfterDate > caCertNotAfterDate) { - throw new BadRequestError({ message: "notAfter date is after CA certificate's notAfter date" }); - } - - const alg = keyAlgorithmToAlgCfg(ca.keyAlgorithm as CertKeyAlgorithm); - - const csrObj = new x509.Pkcs10CertificateRequest(csr); - - const dn = parseDistinguishedName(csrObj.subject); - const cn = commonName || dn.commonName; - - if (!cn) - throw new BadRequestError({ - message: "A common name (CN) is required in the CSR or as a parameter to this endpoint" - }); - - const { caPrivateKey, caSecret } = await getCaCredentials({ - caId: ca.id, - certificateAuthorityDAL, - certificateAuthoritySecretDAL, - projectDAL, - kmsService - }); - - const caCrl = await certificateAuthorityCrlDAL.findOne({ caSecretId: caSecret.id }); - const distributionPointUrl = `${appCfg.SITE_URL}/api/v1/pki/crl/${caCrl.id}/der`; - - const caIssuerUrl = `${appCfg.SITE_URL}/api/v1/pki/ca/${ca.id}/certificates/${caCert.id}/der`; - const extensions: x509.Extension[] = [ - new x509.BasicConstraintsExtension(false), - await x509.AuthorityKeyIdentifierExtension.create(caCertObj, false), - await x509.SubjectKeyIdentifierExtension.create(csrObj.publicKey), - new x509.CRLDistributionPointsExtension([distributionPointUrl]), - new x509.AuthorityInfoAccessExtension({ - caIssuers: new x509.GeneralName("url", caIssuerUrl) - }), - new x509.CertificatePolicyExtension(["2.5.29.32.0"]) // anyPolicy - ]; - - // handle key usages - const csrKeyUsageExtension = csrObj.getExtension("2.5.29.15") as x509.KeyUsagesExtension; - let csrKeyUsages: CertKeyUsage[] = []; - if (csrKeyUsageExtension) { - csrKeyUsages = Object.values(CertKeyUsage).filter( - (keyUsage) => (x509.KeyUsageFlags[keyUsage] & csrKeyUsageExtension.usages) !== 0 - ); - } - - let selectedKeyUsages: CertKeyUsage[] = keyUsages ?? []; - if (keyUsages === undefined && !certificateTemplate) { - if (csrKeyUsageExtension) { - selectedKeyUsages = csrKeyUsages; - } else { - selectedKeyUsages = [CertKeyUsage.DIGITAL_SIGNATURE, CertKeyUsage.KEY_ENCIPHERMENT]; - } - } - - if (keyUsages === undefined && certificateTemplate) { - if (csrKeyUsageExtension) { - const validKeyUsages = certificateTemplate.keyUsages || []; - if (csrKeyUsages.some((keyUsage) => !validKeyUsages.includes(keyUsage))) { - throw new BadRequestError({ - message: "Invalid key usage value based on template policy" - }); - } - selectedKeyUsages = csrKeyUsages; - } else { - selectedKeyUsages = (certificateTemplate.keyUsages ?? []) as CertKeyUsage[]; - } - } - - if (keyUsages?.length && certificateTemplate) { - const validKeyUsages = certificateTemplate.keyUsages || []; - if (keyUsages.some((keyUsage) => !validKeyUsages.includes(keyUsage))) { - throw new BadRequestError({ - message: "Invalid key usage value based on template policy" - }); - } - selectedKeyUsages = keyUsages; - } - - const keyUsagesBitValue = selectedKeyUsages.reduce((accum, keyUsage) => accum | x509.KeyUsageFlags[keyUsage], 0); - if (keyUsagesBitValue) { - extensions.push(new x509.KeyUsagesExtension(keyUsagesBitValue, true)); - } - - // handle extended key usages - const csrExtendedKeyUsageExtension = csrObj.getExtension("2.5.29.37") as x509.ExtendedKeyUsageExtension; - let csrExtendedKeyUsages: CertExtendedKeyUsage[] = []; - if (csrExtendedKeyUsageExtension) { - csrExtendedKeyUsages = csrExtendedKeyUsageExtension.usages.map( - (ekuOid) => CertExtendedKeyUsageOIDToName[ekuOid as string] - ); - } - - let selectedExtendedKeyUsages: CertExtendedKeyUsage[] = extendedKeyUsages ?? []; - if (extendedKeyUsages === undefined && !certificateTemplate && csrExtendedKeyUsageExtension) { - selectedExtendedKeyUsages = csrExtendedKeyUsages; - } - - if (extendedKeyUsages === undefined && certificateTemplate) { - if (csrExtendedKeyUsageExtension) { - const validExtendedKeyUsages = certificateTemplate.extendedKeyUsages || []; - if (csrExtendedKeyUsages.some((eku) => !validExtendedKeyUsages.includes(eku))) { - throw new BadRequestError({ - message: "Invalid extended key usage value based on template policy" - }); - } - selectedExtendedKeyUsages = csrExtendedKeyUsages; - } else { - selectedExtendedKeyUsages = (certificateTemplate.extendedKeyUsages ?? []) as CertExtendedKeyUsage[]; - } - } - - if (extendedKeyUsages?.length && certificateTemplate) { - const validExtendedKeyUsages = certificateTemplate.extendedKeyUsages || []; - if (extendedKeyUsages.some((keyUsage) => !validExtendedKeyUsages.includes(keyUsage))) { - throw new BadRequestError({ - message: "Invalid extended key usage value based on template policy" - }); - } - selectedExtendedKeyUsages = extendedKeyUsages; - } - - if (selectedExtendedKeyUsages.length) { - extensions.push( - new x509.ExtendedKeyUsageExtension( - selectedExtendedKeyUsages.map((eku) => x509.ExtendedKeyUsage[eku]), - true - ) - ); - } - - let altNamesFromCsr: string = ""; - let altNamesArray: { - type: "email" | "dns"; - value: string; - }[] = []; - if (altNames) { - altNamesArray = altNames - .split(",") - .map((name) => name.trim()) - .map((altName) => { - // check if the altName is a valid email - if (z.string().email().safeParse(altName).success) { - return { - type: "email", - value: altName - }; - } - - // check if the altName is a valid hostname - if (isFQDN(altName, { allow_wildcard: true })) { - return { - type: "dns", - value: altName - }; - } - - // If altName is neither a valid email nor a valid hostname, throw an error or handle it accordingly - throw new Error(`Invalid altName: ${altName}`); - }); - } else { - // attempt to read from CSR if altNames is not explicitly provided - const sanExtension = csrObj.extensions.find((ext) => ext.type === "2.5.29.17"); - if (sanExtension) { - const sanNames = new x509.GeneralNames(sanExtension.value); - - altNamesArray = sanNames.items - .filter((value) => value.type === "email" || value.type === "dns") - .map((name) => ({ - type: name.type as "email" | "dns", - value: name.value - })); - - altNamesFromCsr = sanNames.items.map((item) => item.value).join(","); - } - } - - if (altNamesArray.length) { - const altNamesExtension = new x509.SubjectAlternativeNameExtension(altNamesArray, false); - extensions.push(altNamesExtension); - } - - if (certificateTemplate) { - validateCertificateDetailsAgainstTemplate( - { - commonName: cn, - notBeforeDate, - notAfterDate, - altNames: altNamesArray.map((entry) => entry.value) - }, - certificateTemplate - ); - } - - const serialNumber = createSerialNumber(); - const leafCert = await x509.X509CertificateGenerator.create({ - serialNumber, - subject: csrObj.subject, - issuer: caCertObj.subject, - notBefore: notBeforeDate, - notAfter: notAfterDate, - signingKey: caPrivateKey, - publicKey: csrObj.publicKey, - signingAlgorithm: alg, - extensions - }); - - const kmsEncryptor = await kmsService.encryptWithKmsKey({ - kmsId: certificateManagerKmsId - }); - const { cipherTextBlob: encryptedCertificate } = await kmsEncryptor({ - plainText: Buffer.from(new Uint8Array(leafCert.rawData)) - }); - - await certificateDAL.transaction(async (tx) => { - const cert = await certificateDAL.create( - { - caId: (ca as TCertificateAuthorities).id, - caCertId: caCert.id, - certificateTemplateId: certificateTemplate?.id, - status: CertStatus.ACTIVE, - friendlyName: friendlyName || csrObj.subject, - commonName: cn, - altNames: altNamesFromCsr || altNames, - serialNumber, - notBefore: notBeforeDate, - notAfter: notAfterDate, - keyUsages: selectedKeyUsages, - extendedKeyUsages: selectedExtendedKeyUsages - }, - tx - ); - - await certificateBodyDAL.create( - { - certId: cert.id, - encryptedCertificate - }, - tx - ); - - if (collectionId) { - await pkiCollectionItemDAL.create( - { - pkiCollectionId: collectionId, - certId: cert.id - }, - tx - ); - } - - return cert; - }); - - const { caCert: issuingCaCertificate, caCertChain } = await getCaCertChain({ - caCertId: ca.activeCaCertId, - certificateAuthorityDAL, - certificateAuthorityCertDAL, - projectDAL, - kmsService - }); - - return { - certificate: leafCert, - certificateChain: `${issuingCaCertificate}\n${caCertChain}`.trim(), - issuingCaCertificate, - serialNumber, - ca, - commonName: cn - }; - }; - - /** - * Return list of certificate templates for CA with id [caId]. - */ - const getCaCertificateTemplates = async ({ - caId, - actorId, - actorAuthMethod, - actor, - actorOrgId - }: TGetCaCertificateTemplatesDTO) => { - const ca = await certificateAuthorityDAL.findById(caId); - if (!ca) throw new NotFoundError({ message: `CA with ID '${caId}' not found` }); - - const { permission } = await permissionService.getProjectPermission({ - actor, - actorId, - projectId: ca.projectId, - actorAuthMethod, - actorOrgId, - actionProjectType: ActionProjectType.CertificateManager - }); - - ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionActions.Read, - ProjectPermissionSub.CertificateTemplates - ); - - const certificateTemplates = await certificateTemplateDAL.find({ caId }); - - return { - certificateTemplates, - ca - }; - }; - - return { - createCa, - getCaById, - updateCaById, - deleteCaById, - getCaCsr, - renewCaCert, - getCaCerts, - getCaCert, - getCaCertById, - signIntermediate, - importCertToCa, - issueCertFromCa, - signCertFromCa, - getCaCertificateTemplates - }; -}; +export const certificateAuthorityServiceFactory = ({}: TCertificateAuthorityServiceFactoryDep) => {}; diff --git a/backend/src/services/certificate-authority/certificate-authority-types.ts b/backend/src/services/certificate-authority/certificate-authority-types.ts index e2f523348..b5b75780c 100644 --- a/backend/src/services/certificate-authority/certificate-authority-types.ts +++ b/backend/src/services/certificate-authority/certificate-authority-types.ts @@ -148,7 +148,7 @@ export type TDNParts = { export type TGetCaCredentialsDTO = { caId: string; - certificateAuthorityDAL: Pick; + certificateAuthorityDAL: Pick; certificateAuthoritySecretDAL: Pick; projectDAL: Pick; kmsService: Pick; @@ -172,7 +172,7 @@ export type TGetCaCertChainDTO = { export type TRebuildCaCrlDTO = { caId: string; - certificateAuthorityDAL: Pick; + certificateAuthorityDAL: Pick; certificateAuthorityCrlDAL: Pick; certificateAuthoritySecretDAL: Pick; projectDAL: Pick; diff --git a/backend/src/services/certificate-authority/internal-certificate-authority-service.ts b/backend/src/services/certificate-authority/internal-certificate-authority-service.ts index bdb651878..b44508e4a 100644 --- a/backend/src/services/certificate-authority/internal-certificate-authority-service.ts +++ b/backend/src/services/certificate-authority/internal-certificate-authority-service.ts @@ -4,7 +4,13 @@ import * as x509 from "@peculiar/x509"; import crypto, { KeyObject } from "crypto"; import { z } from "zod"; -import { ActionProjectType, ProjectType, TCertificateAuthorities, TCertificateTemplates } from "@app/db/schemas"; +import { + ActionProjectType, + ProjectType, + TableName, + TCertificateAuthorities, + TCertificateTemplates +} from "@app/db/schemas"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service"; import { ProjectPermissionActions, @@ -36,7 +42,7 @@ import { import { TCertificateTemplateDALFactory } from "../certificate-template/certificate-template-dal"; import { validateCertificateDetailsAgainstTemplate } from "../certificate-template/certificate-template-fns"; import { TCertificateAuthorityCertDALFactory } from "./certificate-authority-cert-dal"; -import { TCertificateAuthorityDALFactory } from "./certificate-authority-dal"; +import { TCertificateAuthorityDALFactory, TCertificateAuthorityWithAssociatedCa } from "./certificate-authority-dal"; import { createDistinguishedName, createSerialNumber, @@ -71,7 +77,14 @@ import { TInternalCertificateAuthorityDALFactory } from "./internal-certificate- type TInternalCertificateAuthorityServiceFactoryDep = { certificateAuthorityDAL: Pick< TCertificateAuthorityDALFactory, - "transaction" | "create" | "findById" | "updateById" | "deleteById" | "findOne" | "findByIdWithAssociatedCa" + | "transaction" + | "create" + | "findById" + | "updateById" + | "deleteById" + | "findOne" + | "findByIdWithAssociatedCa" + | "findWithAssociatedCa" >; internalCertificateAuthorityDAL: Pick< TInternalCertificateAuthorityDALFactory, @@ -371,8 +384,19 @@ export const internalCertificateAuthorityServiceFactory = ({ ); const updatedCa = await certificateAuthorityDAL.transaction(async (tx) => { - await internalCertificateAuthorityDAL.updateById(caId, { status }, tx); - await certificateAuthorityDAL.updateById(ca.id, { disableDirectIssuance: requireTemplateForIssuance }, tx); + if (status) { + await internalCertificateAuthorityDAL.update( + { + certificateAuthorityId: ca.id + }, + { status }, + tx + ); + } + + if (requireTemplateForIssuance) { + await certificateAuthorityDAL.updateById(ca.id, { disableDirectIssuance: requireTemplateForIssuance }, tx); + } return certificateAuthorityDAL.findByIdWithAssociatedCa(caId, tx); }); @@ -401,15 +425,9 @@ export const internalCertificateAuthorityServiceFactory = ({ ProjectPermissionSub.CertificateAuthorities ); - return certificateAuthorityDAL.transaction(async (tx) => { - const deletedInternalCa = await internalCertificateAuthorityDAL.deleteById(caId, tx); - const deletedCa = await certificateAuthorityDAL.deleteById(ca.id, tx); + await certificateAuthorityDAL.deleteById(ca.id); - return { - ...deletedCa, - ...deletedInternalCa - }; - }); + return expandInternalCa(ca); }; /** @@ -466,7 +484,7 @@ export const internalCertificateAuthorityServiceFactory = ({ return { csr: csrObj.toString("pem"), - ca + ca: expandInternalCa(ca) }; }; @@ -884,8 +902,8 @@ export const internalCertificateAuthorityServiceFactory = ({ maxPathLength }: TSignIntermediateDTO) => { const appCfg = getConfig(); - const ca = await certificateAuthorityDAL.findById(caId); - if (!ca) throw new NotFoundError({ message: "CA not found" }); + const ca = await certificateAuthorityDAL.findByIdWithAssociatedCa(caId); + if (!ca.internalCa) throw new NotFoundError({ message: "CA not found" }); const { permission } = await permissionService.getProjectPermission({ actor, @@ -901,16 +919,17 @@ export const internalCertificateAuthorityServiceFactory = ({ ProjectPermissionSub.CertificateAuthorities ); - 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" }); + if (ca.internalCa.status === CaStatus.DISABLED) throw new BadRequestError({ message: "CA is disabled" }); + if (!ca.internalCa.activeCaCertId) + throw new BadRequestError({ message: "CA does not have a certificate installed" }); - const caCert = await certificateAuthorityCertDAL.findById(ca.activeCaCertId); + const caCert = await certificateAuthorityCertDAL.findById(ca.internalCa.activeCaCertId); - if (ca.notAfter && new Date() > new Date(ca.notAfter)) { + if (ca.internalCa.notAfter && new Date() > new Date(ca.internalCa.notAfter)) { throw new BadRequestError({ message: "CA is expired" }); } - const alg = keyAlgorithmToAlgCfg(ca.keyAlgorithm as CertKeyAlgorithm); + const alg = keyAlgorithmToAlgCfg(ca.internalCa.keyAlgorithm as CertKeyAlgorithm); const certificateManagerKmsId = await getProjectKmsCertificateKeyId({ projectId: ca.projectId, @@ -1001,7 +1020,7 @@ export const internalCertificateAuthorityServiceFactory = ({ }); const { caCert: issuingCaCertificate, caCertChain } = await getCaCertChain({ - caCertId: ca.activeCaCertId, + caCertId: ca.internalCa.activeCaCertId, certificateAuthorityDAL, certificateAuthorityCertDAL, projectDAL, @@ -1013,7 +1032,7 @@ export const internalCertificateAuthorityServiceFactory = ({ issuingCaCertificate, certificateChain: `${issuingCaCertificate}\n${caCertChain}`.trim(), serialNumber: intermediateCert.serialNumber, - ca + ca: expandInternalCa(ca) }; }; @@ -1031,8 +1050,8 @@ export const internalCertificateAuthorityServiceFactory = ({ certificate, certificateChain }: TImportCertToCaDTO) => { - const ca = await certificateAuthorityDAL.findById(caId); - if (!ca) throw new NotFoundError({ message: `CA with ID '${caId}' not found` }); + const ca = await certificateAuthorityDAL.findByIdWithAssociatedCa(caId); + if (!ca.internalCa) throw new NotFoundError({ message: `CA with ID '${caId}' not found` }); const { permission } = await permissionService.getProjectPermission({ actor, @@ -1048,7 +1067,7 @@ export const internalCertificateAuthorityServiceFactory = ({ ProjectPermissionSub.CertificateAuthorities ); - if (ca.parentCaId) { + if (ca.internalCa.parentCaId) { /** * re-evaluate in the future if we should allow users to import a new CA certificate for an intermediate * CA chained to an internal parent CA. Doing so would allow users to re-chain the CA to a different @@ -1059,7 +1078,9 @@ export const internalCertificateAuthorityServiceFactory = ({ }); } - const caCert = ca.activeCaCertId ? await certificateAuthorityCertDAL.findById(ca.activeCaCertId) : undefined; + const caCert = ca.internalCa.activeCaCertId + ? await certificateAuthorityCertDAL.findById(ca.internalCa.activeCaCertId) + : undefined; const certObj = new x509.X509Certificate(certificate); const maxPathLength = certObj.getExtension(x509.BasicConstraintsExtension)?.pathLength; @@ -1082,9 +1103,9 @@ export const internalCertificateAuthorityServiceFactory = ({ const parentCertObj = chainItems[1]; const parentCertSubject = parentCertObj.subject; - const parentCa = await certificateAuthorityDAL.findOne({ - projectId: ca.projectId, - dn: parentCertSubject + const [parentCa] = await certificateAuthorityDAL.findWithAssociatedCa({ + [`${TableName.CertificateAuthority}.projectId` as "projectId"]: ca.projectId, + [`${TableName.InternalCertificateAuthority}.dn` as "dn"]: parentCertSubject }); const certificateManagerKmsId = await getProjectKmsCertificateKeyId({ @@ -1134,8 +1155,10 @@ export const internalCertificateAuthorityServiceFactory = ({ tx ); - await certificateAuthorityDAL.updateById( - ca.id, + await internalCertificateAuthorityDAL.update( + { + certificateAuthorityId: ca.id + }, { status: CaStatus.ACTIVE, maxPathLength: maxPathLength === undefined ? -1 : maxPathLength, @@ -1149,7 +1172,7 @@ export const internalCertificateAuthorityServiceFactory = ({ ); }); - return { ca }; + return { ca: expandInternalCa(ca) }; }; /** @@ -1173,12 +1196,12 @@ export const internalCertificateAuthorityServiceFactory = ({ keyUsages, extendedKeyUsages }: TIssueCertFromCaDTO) => { - let ca: TCertificateAuthorities | undefined; + let ca: TCertificateAuthorityWithAssociatedCa | undefined; let certificateTemplate: TCertificateTemplates | undefined; let collectionId = pkiCollectionId; if (caId) { - ca = await certificateAuthorityDAL.findById(caId); + ca = await certificateAuthorityDAL.findByIdWithAssociatedCa(caId); } else if (certificateTemplateId) { certificateTemplate = await certificateTemplateDAL.getById(certificateTemplateId); if (!certificateTemplate) { @@ -1188,10 +1211,10 @@ export const internalCertificateAuthorityServiceFactory = ({ } collectionId = certificateTemplate.pkiCollectionId as string; - ca = await certificateAuthorityDAL.findById(certificateTemplate.caId); + ca = await certificateAuthorityDAL.findByIdWithAssociatedCa(certificateTemplate.caId); } - if (!ca) { + if (!ca?.internalCa) { throw new NotFoundError({ message: `CA with ID '${caId}' not found` }); } @@ -1209,14 +1232,16 @@ export const internalCertificateAuthorityServiceFactory = ({ ProjectPermissionSub.Certificates ); - if (ca.status !== CaStatus.ACTIVE) throw new BadRequestError({ message: "CA is not active" }); - if (!ca.activeCaCertId) throw new BadRequestError({ message: "CA does not have a certificate installed" }); - if (ca.requireTemplateForIssuance && !certificateTemplate) { + if (ca.internalCa.status !== CaStatus.ACTIVE) throw new BadRequestError({ message: "CA is not active" }); + if (!ca.internalCa.activeCaCertId) + throw new BadRequestError({ message: "CA does not have a certificate installed" }); + if (ca.disableDirectIssuance && !certificateTemplate) { throw new BadRequestError({ message: "Certificate template is required for issuance" }); } - const caCert = await certificateAuthorityCertDAL.findById(ca.activeCaCertId); - if (ca.notAfter && new Date() > new Date(ca.notAfter)) { + const caCert = await certificateAuthorityCertDAL.findById(ca.internalCa.activeCaCertId); + + if (ca.internalCa.notAfter && new Date() > new Date(ca.internalCa.notAfter)) { throw new BadRequestError({ message: "CA is expired" }); } @@ -1266,7 +1291,7 @@ export const internalCertificateAuthorityServiceFactory = ({ throw new BadRequestError({ message: "notAfter date is after CA certificate's notAfter date" }); } - const alg = keyAlgorithmToAlgCfg(ca.keyAlgorithm as CertKeyAlgorithm); + const alg = keyAlgorithmToAlgCfg(ca.internalCa.keyAlgorithm as CertKeyAlgorithm); const leafKeys = await crypto.subtle.generateKey(alg, true, ["sign", "verify"]); const csrObj = await x509.Pkcs10CertificateRequestGenerator.create({ @@ -1496,7 +1521,7 @@ export const internalCertificateAuthorityServiceFactory = ({ issuingCaCertificate, privateKey: skLeaf, serialNumber, - ca + ca: expandInternalCa(ca) }; }; @@ -1506,7 +1531,7 @@ export const internalCertificateAuthorityServiceFactory = ({ */ const signCertFromCa = async (dto: TSignCertFromCaDTO) => { const appCfg = getConfig(); - let ca: TCertificateAuthorities | undefined; + let ca: TCertificateAuthorityWithAssociatedCa | undefined; let certificateTemplate: TCertificateTemplates | undefined; const { @@ -1527,7 +1552,7 @@ export const internalCertificateAuthorityServiceFactory = ({ let collectionId = pkiCollectionId; if (caId) { - ca = await certificateAuthorityDAL.findById(caId); + ca = await certificateAuthorityDAL.findByIdWithAssociatedCa(caId); } else if (certificateTemplateId) { certificateTemplate = await certificateTemplateDAL.getById(certificateTemplateId); if (!certificateTemplate) { @@ -1537,10 +1562,10 @@ export const internalCertificateAuthorityServiceFactory = ({ } collectionId = certificateTemplate.pkiCollectionId as string; - ca = await certificateAuthorityDAL.findById(certificateTemplate.caId); + ca = await certificateAuthorityDAL.findByIdWithAssociatedCa(certificateTemplate.caId); } - if (!ca) { + if (!ca?.internalCa) { throw new NotFoundError({ message: `CA with ID '${caId}' not found` }); } @@ -1560,15 +1585,16 @@ export const internalCertificateAuthorityServiceFactory = ({ ); } - if (ca.status !== CaStatus.ACTIVE) throw new BadRequestError({ message: "CA is not active" }); - if (!ca.activeCaCertId) throw new BadRequestError({ message: "CA does not have a certificate installed" }); - if (ca.requireTemplateForIssuance && !certificateTemplate) { + if (ca.internalCa.status !== CaStatus.ACTIVE) throw new BadRequestError({ message: "CA is not active" }); + if (!ca.internalCa.activeCaCertId) + throw new BadRequestError({ message: "CA does not have a certificate installed" }); + if (ca.disableDirectIssuance && !certificateTemplate) { throw new BadRequestError({ message: "Certificate template is required for issuance" }); } - const caCert = await certificateAuthorityCertDAL.findById(ca.activeCaCertId); + const caCert = await certificateAuthorityCertDAL.findById(ca.internalCa.activeCaCertId); - if (ca.notAfter && new Date() > new Date(ca.notAfter)) { + if (ca.internalCa.notAfter && new Date() > new Date(ca.internalCa.notAfter)) { throw new BadRequestError({ message: "CA is expired" }); } @@ -1621,7 +1647,7 @@ export const internalCertificateAuthorityServiceFactory = ({ throw new BadRequestError({ message: "notAfter date is after CA certificate's notAfter date" }); } - const alg = keyAlgorithmToAlgCfg(ca.keyAlgorithm as CertKeyAlgorithm); + const alg = keyAlgorithmToAlgCfg(ca.internalCa.keyAlgorithm as CertKeyAlgorithm); const csrObj = new x509.Pkcs10CertificateRequest(csr); @@ -1874,7 +1900,7 @@ export const internalCertificateAuthorityServiceFactory = ({ }); const { caCert: issuingCaCertificate, caCertChain } = await getCaCertChain({ - caCertId: ca.activeCaCertId, + caCertId: ca.internalCa.activeCaCertId, certificateAuthorityDAL, certificateAuthorityCertDAL, projectDAL, @@ -1886,7 +1912,7 @@ export const internalCertificateAuthorityServiceFactory = ({ certificateChain: `${issuingCaCertificate}\n${caCertChain}`.trim(), issuingCaCertificate, serialNumber, - ca, + ca: expandInternalCa(ca), commonName: cn }; }; @@ -1901,8 +1927,8 @@ export const internalCertificateAuthorityServiceFactory = ({ actor, actorOrgId }: TGetCaCertificateTemplatesDTO) => { - const ca = await certificateAuthorityDAL.findById(caId); - if (!ca) throw new NotFoundError({ message: `CA with ID '${caId}' not found` }); + const ca = await certificateAuthorityDAL.findByIdWithAssociatedCa(caId); + if (!ca?.internalCa) throw new NotFoundError({ message: `CA with ID '${caId}' not found` }); const { permission } = await permissionService.getProjectPermission({ actor, @@ -1922,7 +1948,7 @@ export const internalCertificateAuthorityServiceFactory = ({ return { certificateTemplates, - ca + ca: expandInternalCa(ca) }; }; diff --git a/backend/src/services/certificate-template/certificate-template-dal.ts b/backend/src/services/certificate-template/certificate-template-dal.ts index c500833d1..5429b2b4a 100644 --- a/backend/src/services/certificate-template/certificate-template-dal.ts +++ b/backend/src/services/certificate-template/certificate-template-dal.ts @@ -19,10 +19,15 @@ export const certificateTemplateDALFactory = (db: TDbClient) => { `${TableName.CertificateAuthority}.id`, `${TableName.CertificateTemplate}.caId` ) + .join( + TableName.InternalCertificateAuthority, + `${TableName.InternalCertificateAuthority}.certificateAuthorityId`, + `${TableName.CertificateAuthority}.id` + ) .where(`${TableName.CertificateAuthority}.projectId`, "=", projectId) .select(selectAllTableCols(TableName.CertificateTemplate)) .select( - db.ref("friendlyName").as("caName").withSchema(TableName.CertificateAuthority), + db.ref("friendlyName").as("caName").withSchema(TableName.InternalCertificateAuthority), db.ref("projectId").withSchema(TableName.CertificateAuthority) ); @@ -41,11 +46,16 @@ export const certificateTemplateDALFactory = (db: TDbClient) => { `${TableName.CertificateTemplate}.caId` ) .join(TableName.Project, `${TableName.Project}.id`, `${TableName.CertificateAuthority}.projectId`) + .join( + TableName.InternalCertificateAuthority, + `${TableName.InternalCertificateAuthority}.certificateAuthorityId`, + `${TableName.CertificateAuthority}.id` + ) .where(`${TableName.CertificateTemplate}.id`, "=", id) .select(selectAllTableCols(TableName.CertificateTemplate)) .select( db.ref("projectId").withSchema(TableName.CertificateAuthority), - db.ref("friendlyName").as("caName").withSchema(TableName.CertificateAuthority), + db.ref("friendlyName").as("caName").withSchema(TableName.InternalCertificateAuthority), db.ref("orgId").withSchema(TableName.Project) ) .first(); diff --git a/backend/src/services/certificate/certificate-service.ts b/backend/src/services/certificate/certificate-service.ts index 73a8caed7..f10ec1c4d 100644 --- a/backend/src/services/certificate/certificate-service.ts +++ b/backend/src/services/certificate/certificate-service.ts @@ -17,7 +17,7 @@ import { TKmsServiceFactory } from "@app/services/kms/kms-service"; import { TProjectDALFactory } from "@app/services/project/project-dal"; import { getProjectKmsCertificateKeyId } from "@app/services/project/project-fns"; -import { getCaCertChain, rebuildCaCrl } from "../certificate-authority/certificate-authority-fns"; +import { expandInternalCa, getCaCertChain, rebuildCaCrl } from "../certificate-authority/certificate-authority-fns"; import { buildCertificateChain, getCertificateCredentials, revocationReasonToCrlCode } from "./certificate-fns"; import { TCertificateSecretDALFactory } from "./certificate-secret-dal"; import { @@ -34,7 +34,7 @@ type TCertificateServiceFactoryDep = { certificateDAL: Pick; certificateSecretDAL: Pick; certificateBodyDAL: Pick; - certificateAuthorityDAL: Pick; + certificateAuthorityDAL: Pick; certificateAuthorityCertDAL: Pick; certificateAuthorityCrlDAL: Pick; certificateAuthoritySecretDAL: Pick; @@ -62,7 +62,7 @@ export const certificateServiceFactory = ({ */ const getCert = async ({ serialNumber, actorId, actorAuthMethod, actor, actorOrgId }: TGetCertDTO) => { const cert = await certificateDAL.findOne({ serialNumber }); - const ca = await certificateAuthorityDAL.findById(cert.caId); + const ca = await certificateAuthorityDAL.findByIdWithAssociatedCa(cert.caId); const { permission } = await permissionService.getProjectPermission({ actor, @@ -80,7 +80,7 @@ export const certificateServiceFactory = ({ return { cert, - ca + ca: expandInternalCa(ca) }; }; @@ -95,7 +95,7 @@ export const certificateServiceFactory = ({ actorOrgId }: TGetCertPrivateKeyDTO) => { const cert = await certificateDAL.findOne({ serialNumber }); - const ca = await certificateAuthorityDAL.findById(cert.caId); + const ca = await certificateAuthorityDAL.findByIdWithAssociatedCa(cert.caId); const { permission } = await permissionService.getProjectPermission({ actor, @@ -120,7 +120,7 @@ export const certificateServiceFactory = ({ }); return { - ca, + ca: expandInternalCa(ca), cert, certPrivateKey }; @@ -131,7 +131,7 @@ export const certificateServiceFactory = ({ */ const deleteCert = async ({ serialNumber, actorId, actorAuthMethod, actor, actorOrgId }: TDeleteCertDTO) => { const cert = await certificateDAL.findOne({ serialNumber }); - const ca = await certificateAuthorityDAL.findById(cert.caId); + const ca = await certificateAuthorityDAL.findByIdWithAssociatedCa(cert.caId); const { permission } = await permissionService.getProjectPermission({ actor, @@ -151,7 +151,7 @@ export const certificateServiceFactory = ({ return { deletedCert, - ca + ca: expandInternalCa(ca) }; }; @@ -169,7 +169,7 @@ export const certificateServiceFactory = ({ actorOrgId }: TRevokeCertDTO) => { const cert = await certificateDAL.findOne({ serialNumber }); - const ca = await certificateAuthorityDAL.findById(cert.caId); + const ca = await certificateAuthorityDAL.findByIdWithAssociatedCa(cert.caId); const { permission } = await permissionService.getProjectPermission({ actor, @@ -210,7 +210,7 @@ export const certificateServiceFactory = ({ kmsService }); - return { revokedAt, cert, ca }; + return { revokedAt, cert, ca: expandInternalCa(ca) }; }; /** @@ -219,7 +219,7 @@ export const certificateServiceFactory = ({ */ const getCertBody = async ({ serialNumber, actorId, actorAuthMethod, actor, actorOrgId }: TGetCertBodyDTO) => { const cert = await certificateDAL.findOne({ serialNumber }); - const ca = await certificateAuthorityDAL.findById(cert.caId); + const ca = await certificateAuthorityDAL.findByIdWithAssociatedCa(cert.caId); const { permission } = await permissionService.getProjectPermission({ actor, @@ -273,7 +273,7 @@ export const certificateServiceFactory = ({ certificateChain, serialNumber: certObj.serialNumber, cert, - ca + ca: expandInternalCa(ca) }; }; @@ -283,7 +283,7 @@ export const certificateServiceFactory = ({ */ const getCertBundle = async ({ serialNumber, actorId, actorAuthMethod, actor, actorOrgId }: TGetCertBundleDTO) => { const cert = await certificateDAL.findOne({ serialNumber }); - const ca = await certificateAuthorityDAL.findById(cert.caId); + const ca = await certificateAuthorityDAL.findByIdWithAssociatedCa(cert.caId); const { permission } = await permissionService.getProjectPermission({ actor, @@ -351,7 +351,7 @@ export const certificateServiceFactory = ({ privateKey: certPrivateKey, serialNumber, cert, - ca + ca: expandInternalCa(ca) }; }; diff --git a/backend/src/services/pki-subscriber/pki-subscriber-service.ts b/backend/src/services/pki-subscriber/pki-subscriber-service.ts index 5b15786b1..7df3209e4 100644 --- a/backend/src/services/pki-subscriber/pki-subscriber-service.ts +++ b/backend/src/services/pki-subscriber/pki-subscriber-service.ts @@ -29,6 +29,7 @@ import { TCertificateAuthorityCertDALFactory } from "@app/services/certificate-a import { TCertificateAuthorityDALFactory } from "@app/services/certificate-authority/certificate-authority-dal"; import { createSerialNumber, + expandInternalCa, getCaCertChain, getCaCredentials, keyAlgorithmToAlgCfg, @@ -57,7 +58,7 @@ type TPkiSubscriberServiceFactoryDep = { TPkiSubscriberDALFactory, "create" | "findById" | "updateById" | "deleteById" | "transaction" | "find" | "findOne" >; - certificateAuthorityDAL: Pick; + certificateAuthorityDAL: Pick; certificateAuthorityCertDAL: Pick; certificateAuthoritySecretDAL: Pick; certificateAuthorityCrlDAL: Pick; @@ -266,8 +267,8 @@ export const pkiSubscriberServiceFactory = ({ if (!subscriber) throw new NotFoundError({ message: `PKI subscriber named '${subscriberName}' not found` }); if (!subscriber.caId) throw new BadRequestError({ message: "Subscriber does not have an assigned issuing CA" }); - const ca = await certificateAuthorityDAL.findById(subscriber.caId); - if (!ca) throw new NotFoundError({ message: `CA with ID '${subscriber.caId}' not found` }); + const ca = await certificateAuthorityDAL.findByIdWithAssociatedCa(subscriber.caId); + if (!ca?.internalCa) throw new NotFoundError({ message: `CA with ID '${subscriber.caId}' not found` }); const { permission } = await permissionService.getProjectPermission({ actor, @@ -287,12 +288,13 @@ export const pkiSubscriberServiceFactory = ({ if (subscriber.status !== PkiSubscriberStatus.ACTIVE) throw new BadRequestError({ message: "Subscriber is not active" }); - if (ca.status !== CaStatus.ACTIVE) throw new BadRequestError({ message: "CA is not active" }); - if (!ca.activeCaCertId) throw new BadRequestError({ message: "CA does not have a certificate installed" }); - if (ca.requireTemplateForIssuance) { + if (ca.internalCa?.status !== CaStatus.ACTIVE) throw new BadRequestError({ message: "CA is not active" }); + if (!ca.internalCa?.activeCaCertId) + throw new BadRequestError({ message: "CA does not have a certificate installed" }); + if (ca.disableDirectIssuance) { throw new BadRequestError({ message: "Certificate template is required for issuance" }); } - const caCert = await certificateAuthorityCertDAL.findById(ca.activeCaCertId); + const caCert = await certificateAuthorityCertDAL.findById(ca.internalCa.activeCaCertId); const certificateManagerKmsId = await getProjectKmsCertificateKeyId({ projectId: ca.projectId, @@ -323,7 +325,7 @@ export const pkiSubscriberServiceFactory = ({ throw new BadRequestError({ message: "notAfter date is after CA certificate's notAfter date" }); } - const alg = keyAlgorithmToAlgCfg(ca.keyAlgorithm as CertKeyAlgorithm); + const alg = keyAlgorithmToAlgCfg(ca.internalCa.keyAlgorithm as CertKeyAlgorithm); const leafKeys = await crypto.subtle.generateKey(alg, true, ["sign", "verify"]); const csrObj = await x509.Pkcs10CertificateRequestGenerator.create({ @@ -500,8 +502,8 @@ export const pkiSubscriberServiceFactory = ({ if (!subscriber) throw new NotFoundError({ message: `PKI subscriber named '${subscriberName}' not found` }); if (!subscriber.caId) throw new BadRequestError({ message: "Subscriber does not have an assigned issuing CA" }); - const ca = await certificateAuthorityDAL.findById(subscriber.caId); - if (!ca) throw new NotFoundError({ message: `CA with ID '${subscriber.caId}' not found` }); + const ca = await certificateAuthorityDAL.findByIdWithAssociatedCa(subscriber.caId); + if (!ca?.internalCa) throw new NotFoundError({ message: `CA with ID '${subscriber.caId}' not found` }); const { permission } = await permissionService.getProjectPermission({ actor, @@ -521,12 +523,13 @@ export const pkiSubscriberServiceFactory = ({ if (subscriber.status !== PkiSubscriberStatus.ACTIVE) throw new BadRequestError({ message: "Subscriber is not active" }); - if (ca.status !== CaStatus.ACTIVE) throw new BadRequestError({ message: "CA is not active" }); - if (!ca.activeCaCertId) throw new BadRequestError({ message: "CA does not have a certificate installed" }); - if (ca.requireTemplateForIssuance) { + if (ca.internalCa?.status !== CaStatus.ACTIVE) throw new BadRequestError({ message: "CA is not active" }); + if (!ca.internalCa?.activeCaCertId) + throw new BadRequestError({ message: "CA does not have a certificate installed" }); + if (ca.disableDirectIssuance) { throw new BadRequestError({ message: "Certificate template is required for issuance" }); } - const caCert = await certificateAuthorityCertDAL.findById(ca.activeCaCertId); + const caCert = await certificateAuthorityCertDAL.findById(ca.internalCa.activeCaCertId); const certificateManagerKmsId = await getProjectKmsCertificateKeyId({ projectId: ca.projectId, @@ -557,7 +560,7 @@ export const pkiSubscriberServiceFactory = ({ throw new BadRequestError({ message: "notAfter date is after CA certificate's notAfter date" }); } - const alg = keyAlgorithmToAlgCfg(ca.keyAlgorithm as CertKeyAlgorithm); + const alg = keyAlgorithmToAlgCfg(ca.internalCa.keyAlgorithm as CertKeyAlgorithm); const csrObj = new x509.Pkcs10CertificateRequest(csr); @@ -691,7 +694,7 @@ export const pkiSubscriberServiceFactory = ({ }); const { caCert: issuingCaCertificate, caCertChain } = await getCaCertChain({ - caCertId: ca.activeCaCertId, + caCertId: ca.internalCa.activeCaCertId, certificateAuthorityDAL, certificateAuthorityCertDAL, projectDAL, @@ -740,7 +743,7 @@ export const pkiSubscriberServiceFactory = ({ certificateChain: `${issuingCaCertificate}\n${caCertChain}`.trim(), issuingCaCertificate, serialNumber, - ca, + ca: expandInternalCa(ca), commonName: subscriber.commonName, subscriber }; diff --git a/backend/src/services/project/project-service.ts b/backend/src/services/project/project-service.ts index 8cfa20697..d58f08ca1 100644 --- a/backend/src/services/project/project-service.ts +++ b/backend/src/services/project/project-service.ts @@ -6,6 +6,7 @@ import { ProjectMembershipRole, ProjectType, ProjectVersion, + TableName, TProjectEnvironments } from "@app/db/schemas"; import { TLicenseServiceFactory } from "@app/ee/services/license/license-service"; @@ -41,6 +42,7 @@ import { TPkiSubscriberDALFactory } from "@app/services/pki-subscriber/pki-subsc import { ActorType } from "../auth/auth-type"; import { TCertificateDALFactory } from "../certificate/certificate-dal"; import { TCertificateAuthorityDALFactory } from "../certificate-authority/certificate-authority-dal"; +import { expandInternalCa } from "../certificate-authority/certificate-authority-fns"; import { TCertificateTemplateDALFactory } from "../certificate-template/certificate-template-dal"; import { TGroupProjectDALFactory } from "../group-project/group-project-dal"; import { TIdentityOrgDALFactory } from "../identity/identity-org-dal"; @@ -149,7 +151,7 @@ type TProjectServiceFactoryDep = { >; projectUserMembershipRoleDAL: Pick; pkiSubscriberDAL: Pick; - certificateAuthorityDAL: Pick; + certificateAuthorityDAL: Pick; certificateDAL: Pick; certificateTemplateDAL: Pick; pkiAlertDAL: Pick; @@ -913,17 +915,19 @@ export const projectServiceFactory = ({ ProjectPermissionSub.CertificateAuthorities ); - const cas = await certificateAuthorityDAL.find( + const cas = await certificateAuthorityDAL.findWithAssociatedCa( { - projectId, - ...(status && { status }), - ...(friendlyName && { friendlyName }), - ...(commonName && { commonName }) + [`${TableName.CertificateAuthority}.projectId` as "projectId"]: projectId, + ...(status && { [`${TableName.InternalCertificateAuthority}.status` as "status"]: status }), + ...(friendlyName && { + [`${TableName.InternalCertificateAuthority}.friendlyName` as "friendlyName"]: friendlyName + }), + ...(commonName && { [`${TableName.InternalCertificateAuthority}.commonName` as "commonName"]: commonName }) }, { offset, limit, sort: [["updatedAt", "desc"]] } ); - return cas; + return cas.map((ca) => expandInternalCa(ca)); }; /**