/* eslint-disable no-bitwise */ import { ForbiddenError, subject } from "@casl/ability"; import * as x509 from "@peculiar/x509"; import slugify from "@sindresorhus/slugify"; import { ActionProjectType, TableName, TCertificateAuthorities, TCertificateTemplates } from "@app/db/schemas"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service-types"; import { ProjectPermissionActions, ProjectPermissionCertificateActions, ProjectPermissionPkiTemplateActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission"; import { extractX509CertFromChain } from "@app/lib/certificates/extract-certificate"; import { getConfig } from "@app/lib/config/env"; import { crypto } from "@app/lib/crypto/cryptography"; import { BadRequestError, NotFoundError } from "@app/lib/errors"; import { ms } from "@app/lib/ms"; import { alphaNumericNanoId } from "@app/lib/nanoid"; 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, CertSignatureAlgorithm, CertStatus, TAltNameMapping } 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, TCertificateAuthorityWithAssociatedCa } from "../certificate-authority-dal"; import { CaStatus, InternalCaType } from "../certificate-authority-enums"; import { createDistinguishedName, createSerialNumber, expandInternalCa, getCaCertChain, // TODO: consider rename getCaCertChains, getCaCredentials, keyAlgorithmToAlgCfg, parseDistinguishedName, signatureAlgorithmToAlgCfg } from "../certificate-authority-fns"; import { TCertificateAuthorityQueueFactory } from "../certificate-authority-queue"; import { TCertificateAuthoritySecretDALFactory } from "../certificate-authority-secret-dal"; import { validateAndMapAltNameType } from "../certificate-authority-validators"; import { TInternalCertificateAuthorityDALFactory } from "./internal-certificate-authority-dal"; import { TCreateCaDTO, TDeleteCaDTO, TGetCaCertDTO, TGetCaCertificateTemplatesDTO, TGetCaCertsDTO, TGetCaCsrDTO, TGetCaDTO, TImportCertToCaDTO, TIssueCertFromCaDTO, TRenewCaCertDTO, TSignCertFromCaDTO, TSignIntermediateDTO, TUpdateCaDTO } from "./internal-certificate-authority-types"; type TInternalCertificateAuthorityServiceFactoryDep = { certificateAuthorityDAL: Pick< TCertificateAuthorityDALFactory, | "transaction" | "create" | "findById" | "updateById" | "deleteById" | "findOne" | "findByIdWithAssociatedCa" | "findWithAssociatedCa" >; internalCertificateAuthorityDAL: Pick< TInternalCertificateAuthorityDALFactory, "transaction" | "create" | "findById" | "updateById" | "deleteById" | "findOne" | "update" >; 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; kmsService: Pick; permissionService: Pick; }; export type TInternalCertificateAuthorityServiceFactory = ReturnType; export const internalCertificateAuthorityServiceFactory = ({ certificateAuthorityDAL, certificateAuthorityCertDAL, certificateAuthoritySecretDAL, certificateAuthorityCrlDAL, certificateTemplateDAL, certificateDAL, certificateBodyDAL, certificateSecretDAL, pkiCollectionDAL, pkiCollectionItemDAL, internalCertificateAuthorityDAL, projectDAL, kmsService, permissionService }: TInternalCertificateAuthorityServiceFactoryDep) => { const createCa = async ({ type, friendlyName, commonName, organization, ou, country, province, locality, notBefore, notAfter, maxPathLength, keyAlgorithm, enableDirectIssuance, name, ...dto }: TCreateCaDTO) => { let projectId: string; if (!dto.isInternal) { const project = await projectDAL.findProjectBySlug(dto.projectSlug, dto.actorOrgId); if (!project) throw new NotFoundError({ message: `Project with slug '${dto.projectSlug}' not found` }); projectId = project.id; const { permission } = await permissionService.getProjectPermission({ actor: dto.actor, actorId: dto.actorId, projectId, actorAuthMethod: dto.actorAuthMethod, actorOrgId: dto.actorOrgId, actionProjectType: ActionProjectType.CertificateManager }); ForbiddenError.from(permission).throwUnlessCan( ProjectPermissionActions.Create, ProjectPermissionSub.CertificateAuthorities ); } else { projectId = dto.projectId; } const dn = createDistinguishedName({ commonName, organization, ou, country, province, locality }); const alg = keyAlgorithmToAlgCfg(keyAlgorithm); const keys = await crypto.nativeCrypto.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, enableDirectIssuance, name: name || slugify(`${(friendlyName || dn).slice(0, 16)}-${alphaNumericNanoId(8)}`), status: type === InternalCaType.ROOT ? CaStatus.ACTIVE : CaStatus.PENDING_CERTIFICATE }, tx ); const internalCa = await internalCertificateAuthorityDAL.create( { caId: ca.id, type, organization, ou, country, province, locality, friendlyName: friendlyName || dn, commonName, dn, keyAlgorithm, ...(type === InternalCaType.ROOT && { maxPathLength, notBefore: notBeforeDate, notAfter: notAfterDate, serialNumber }) }, 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 = crypto.nativeCrypto.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 === InternalCaType.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 || maxPathLength === null ? 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 internalCertificateAuthorityDAL.updateById( internalCa.id, { activeCaCertId: caCert.id }, tx ); } // create empty CRL const crl = await x509.X509CrlGenerator.create({ issuer: internalCa.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 certificateAuthorityDAL.findByIdWithAssociatedCa(ca.id, tx); }); return expandInternalCa(newCa); }; /** * Return CA with id [caId] */ const getCaById = async ({ caId, actorId, actorAuthMethod, actor, actorOrgId }: TGetCaDTO) => { 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, actorId, projectId: ca.projectId, actorAuthMethod, actorOrgId, actionProjectType: ActionProjectType.CertificateManager }); ForbiddenError.from(permission).throwUnlessCan( ProjectPermissionActions.Read, ProjectPermissionSub.CertificateAuthorities ); return expandInternalCa(ca); }; /** * Update CA with id [caId]. * Note: Used to enable/disable CA */ const updateCaById = async ({ caId, status, enableDirectIssuance, name, ...dto }: TUpdateCaDTO) => { const ca = await certificateAuthorityDAL.findByIdWithAssociatedCa(caId); if (!ca.internalCa) 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( ProjectPermissionActions.Edit, ProjectPermissionSub.CertificateAuthorities ); } const updatedCa = await certificateAuthorityDAL.transaction(async (tx) => { if (enableDirectIssuance !== undefined || status !== undefined || name !== undefined) { await certificateAuthorityDAL.updateById(ca.id, { enableDirectIssuance, status, name }, tx); } return certificateAuthorityDAL.findByIdWithAssociatedCa(caId, tx); }); return expandInternalCa(updatedCa); }; /** * Delete CA with id [caId] */ const deleteCaById = async ({ caId, actorId, actorAuthMethod, actor, actorOrgId }: TDeleteCaDTO) => { 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, actorId, projectId: ca.projectId, actorAuthMethod, actorOrgId, actionProjectType: ActionProjectType.CertificateManager }); ForbiddenError.from(permission).throwUnlessCan( ProjectPermissionActions.Delete, ProjectPermissionSub.CertificateAuthorities ); await certificateAuthorityDAL.deleteById(ca.id); return expandInternalCa(ca); }; /** * Return certificate signing request (CSR) made with CA with id [caId] */ const getCaCsr = async ({ caId, actorId, actorAuthMethod, actor, actorOrgId }: TGetCaCsrDTO) => { 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, actorId, projectId: ca.projectId, actorAuthMethod, actorOrgId, actionProjectType: ActionProjectType.CertificateManager }); ForbiddenError.from(permission).throwUnlessCan( ProjectPermissionActions.Create, ProjectPermissionSub.CertificateAuthorities ); if (ca.internalCa.type === InternalCaType.ROOT) throw new BadRequestError({ message: "Root CA cannot generate CSR" }); const { caPrivateKey, caPublicKey } = await getCaCredentials({ caId, certificateAuthorityDAL, certificateAuthoritySecretDAL, projectDAL, kmsService }); const alg = keyAlgorithmToAlgCfg(ca.internalCa.keyAlgorithm as CertKeyAlgorithm); const csrObj = await x509.Pkcs10CertificateRequestGenerator.create({ name: ca.internalCa.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: expandInternalCa(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.findByIdWithAssociatedCa(caId); if (!ca.internalCa) throw new NotFoundError({ message: `CA with ID '${caId}' not found` }); if (!ca.internalCa.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.internalCa.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.internalCa.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.internalCa.type) { case InternalCaType.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.internalCa.dn, serialNumber, notBefore: notBeforeDate, notAfter: new Date(notAfter), signingAlgorithm: alg, keys: { privateKey: caPrivateKey, publicKey: caPublicKey }, extensions: [ new x509.BasicConstraintsExtension( true, ca.internalCa.maxPathLength === -1 || !ca.internalCa.maxPathLength ? undefined : ca.internalCa.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 internalCertificateAuthorityDAL.transaction(async (tx) => { const newCaCert = await certificateAuthorityCertDAL.create( { caId: ca.id, encryptedCertificate, encryptedCertificateChain, version: caCert.version + 1, caSecretId: caSecret.id }, tx ); await internalCertificateAuthorityDAL.update( { caId: ca.id }, { activeCaCertId: newCaCert.id, notBefore: notBeforeDate, notAfter: new Date(notAfter) }, tx ); }); certificate = cert.toString("pem"); break; } case InternalCaType.INTERMEDIATE: { if (!ca.internalCa.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.findByIdWithAssociatedCa(ca.internalCa.parentCaId); const { caPrivateKey: parentCaPrivateKey } = await getCaCredentials({ caId: parentCa.id, certificateAuthorityDAL, certificateAuthoritySecretDAL, projectDAL, kmsService }); if (!parentCa.internalCa) { throw new BadRequestError({ message: "Parent CA not found" }); } // get latest parent CA certificate if (!parentCa.internalCa.activeCaCertId) throw new BadRequestError({ message: "Parent CA does not have a certificate installed" }); const parentCaCert = await certificateAuthorityCertDAL.findById(parentCa.internalCa.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.internalCa.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.internalCa.maxPathLength === -1 || !ca.internalCa.maxPathLength ? undefined : ca.internalCa.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.internalCa.activeCaCertId, certificateAuthorityDAL, certificateAuthorityCertDAL, projectDAL, kmsService }); certificateChain = `${parentCaCertificate}\n${parentCaCertChain}`.trim(); const { cipherTextBlob: encryptedCertificateChain } = await kmsEncryptor({ plainText: Buffer.from(certificateChain) }); await internalCertificateAuthorityDAL.transaction(async (tx) => { const newCaCert = await certificateAuthorityCertDAL.create( { caId: ca.id, encryptedCertificate, encryptedCertificateChain, version: caCert.version + 1, caSecretId: caSecret.id }, tx ); await internalCertificateAuthorityDAL.update( { caId: 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: { ...ca, ...ca.internalCa } }; }; const getCaCerts = async ({ caId, actorId, actorAuthMethod, actor, actorOrgId }: TGetCaCertsDTO) => { 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, 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: expandInternalCa(ca), caCerts: caCertChains }; }; /** * Return current certificate and certificate chain for CA */ const getCaCert = async ({ caId, actorId, actorAuthMethod, actor, actorOrgId }: TGetCaCertDTO) => { const ca = await certificateAuthorityDAL.findByIdWithAssociatedCa(caId); if (!ca.internalCa) throw new NotFoundError({ message: `CA with ID '${caId}' not found` }); if (!ca.internalCa.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.internalCa.activeCaCertId, certificateAuthorityDAL, certificateAuthorityCertDAL, projectDAL, kmsService }); return { certificate: caCert, certificateChain: caCertChain, serialNumber, ca: expandInternalCa(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.findByIdWithAssociatedCa(caId); if (!ca.internalCa) 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.internalCa.activeCaCertId) throw new BadRequestError({ message: "CA does not have a certificate installed" }); 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" }); } const alg = keyAlgorithmToAlgCfg(ca.internalCa.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.internalCa.activeCaCertId, certificateAuthorityDAL, certificateAuthorityCertDAL, projectDAL, kmsService }); return { certificate: intermediateCert.toString("pem"), issuingCaCertificate, certificateChain: `${issuingCaCertificate}\n${caCertChain}`.trim(), serialNumber: intermediateCert.serialNumber, ca: expandInternalCa(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.findByIdWithAssociatedCa(caId); if (!ca.internalCa) 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.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 * internal CA. */ throw new BadRequestError({ message: "Cannot import certificate to intermediate CA chained to internal parent CA" }); } 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; // 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 parentSerialNumber = parentCertObj.serialNumber; const [parentCa] = await certificateAuthorityDAL.findWithAssociatedCa({ [`${TableName.CertificateAuthority}.projectId` as "projectId"]: ca.projectId, [`${TableName.InternalCertificateAuthority}.serialNumber` as "serialNumber"]: parentSerialNumber }); 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.nativeCrypto.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 }); await internalCertificateAuthorityDAL.update( { caId: ca.id }, { 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: expandInternalCa(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, signatureAlgorithm, keyAlgorithm }: TIssueCertFromCaDTO) => { let ca: TCertificateAuthorityWithAssociatedCa | undefined; let certificateTemplate: TCertificateTemplates | undefined; let collectionId = pkiCollectionId; if (caId) { ca = await certificateAuthorityDAL.findByIdWithAssociatedCa(caId); } else if (certificateTemplateId) { certificateTemplate = await certificateTemplateDAL.getById(certificateTemplateId); if (!certificateTemplate) { throw new NotFoundError({ message: `Certificate template with ID '${certificateTemplateId}' not found` }); } if (!collectionId) { collectionId = certificateTemplate.pkiCollectionId as string; } ca = await certificateAuthorityDAL.findByIdWithAssociatedCa(certificateTemplate.caId); } if (!ca) { throw new NotFoundError({ message: `Internal CA with ID '${caId}' not found` }); } if (!ca?.internalCa?.id) { throw new NotFoundError({ message: `Internal 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.internalCa.activeCaCertId) throw new BadRequestError({ message: "CA does not have a certificate installed" }); if (!ca.enableDirectIssuance && !certificateTemplate) { throw new BadRequestError({ message: "Certificate template or subscriber is required for issuance" }); } 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" }); } // 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 effectiveKeyAlgorithm = (keyAlgorithm as CertKeyAlgorithm) || (ca.internalCa.keyAlgorithm as CertKeyAlgorithm); const keyGenAlg = keyAlgorithmToAlgCfg(effectiveKeyAlgorithm); const leafKeys = await crypto.nativeCrypto.subtle.generateKey(keyGenAlg, true, ["sign", "verify"]); if (signatureAlgorithm) { const caKeyAlgorithm = ca.internalCa.keyAlgorithm; const requestedKeyType = signatureAlgorithm.split("-")[0]; const isRsaCa = caKeyAlgorithm.startsWith(CertKeyAlgorithm.RSA_2048.split("_")[0]); const isEcdsaCa = caKeyAlgorithm.startsWith(CertKeyAlgorithm.ECDSA_P256.split("_")[0]); if ( (requestedKeyType === CertSignatureAlgorithm.RSA_SHA256.split("-")[0] && !isRsaCa) || (requestedKeyType === CertSignatureAlgorithm.ECDSA_SHA256.split("-")[0] && !isEcdsaCa) ) { // eslint-disable-next-line no-nested-ternary const supportedType = isRsaCa ? CertSignatureAlgorithm.RSA_SHA256.split("-")[0] : isEcdsaCa ? CertSignatureAlgorithm.ECDSA_SHA256.split("-")[0] : "unknown"; throw new BadRequestError({ message: `Requested signature algorithm ${signatureAlgorithm} is not compatible with CA key algorithm ${caKeyAlgorithm}. CA can only sign with ${supportedType}-based signature algorithms.` }); } } // Determine signing algorithm for certificate signing const signingAlg = signatureAlgorithm ? signatureAlgorithmToAlgCfg(signatureAlgorithm, ca.internalCa.keyAlgorithm as CertKeyAlgorithm) : keyAlgorithmToAlgCfg(ca.internalCa.keyAlgorithm as CertKeyAlgorithm); const csrObj = await x509.Pkcs10CertificateRequestGenerator.create({ name: `CN=${commonName}`, keys: leafKeys, signingAlgorithm: keyGenAlg, 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, signatureAlgorithm: signingAlg }); 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: TAltNameMapping[] = []; if (altNames) { altNamesArray = altNames .split(",") .map((name) => name.trim()) .map((altName): TAltNameMapping => { const altNameType = validateAndMapAltNameType(altName); if (!altNameType) { throw new Error(`Invalid altName: ${altName}`); } return altNameType; }); 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: signingAlg, extensions }); const skLeafObj = crypto.nativeCrypto.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, projectId: ca!.projectId }, 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: expandInternalCa(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: TCertificateAuthorityWithAssociatedCa | undefined; let certificateTemplate: TCertificateTemplates | undefined; const { caId, certificateTemplateId, csr, pkiCollectionId, friendlyName, commonName, altNames, ttl, notBefore, notAfter, keyUsages, extendedKeyUsages, signatureAlgorithm, keyAlgorithm } = dto; let collectionId = pkiCollectionId; if (caId) { ca = await certificateAuthorityDAL.findByIdWithAssociatedCa(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.findByIdWithAssociatedCa(certificateTemplate.caId); } if (!ca) { throw new NotFoundError({ message: `Internal CA with ID '${caId}' not found` }); } if (!ca?.internalCa?.id) { throw new NotFoundError({ message: `Internal 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.internalCa.activeCaCertId) throw new BadRequestError({ message: "CA does not have a certificate installed" }); if (!ca.enableDirectIssuance && !certificateTemplate) { throw new BadRequestError({ message: "Certificate template or subscriber is required for issuance" }); } 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" }); } // 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" }); } if (signatureAlgorithm) { const caKeyAlgorithm = ca.internalCa.keyAlgorithm; const requestedKeyType = signatureAlgorithm.split("-")[0]; // Get the first part (RSA, ECDSA) const isRsaCa = caKeyAlgorithm.startsWith(CertKeyAlgorithm.RSA_2048.split("_")[0]); const isEcdsaCa = caKeyAlgorithm.startsWith(CertKeyAlgorithm.ECDSA_P256.split("_")[0]); if ( (requestedKeyType === CertSignatureAlgorithm.RSA_SHA256.split("-")[0] && !isRsaCa) || (requestedKeyType === CertSignatureAlgorithm.ECDSA_SHA256.split("-")[0] && !isEcdsaCa) ) { // eslint-disable-next-line no-nested-ternary const supportedType = isRsaCa ? CertSignatureAlgorithm.RSA_SHA256.split("-")[0] : isEcdsaCa ? CertSignatureAlgorithm.ECDSA_SHA256.split("-")[0] : "unknown"; throw new BadRequestError({ message: `Requested signature algorithm ${signatureAlgorithm} is not compatible with CA key algorithm ${caKeyAlgorithm}. CA can only sign with ${supportedType}-based signature algorithms.` }); } } const effectiveKeyAlgorithm = (keyAlgorithm || ca.internalCa.keyAlgorithm) as CertKeyAlgorithm; const alg = signatureAlgorithm ? signatureAlgorithmToAlgCfg(signatureAlgorithm, effectiveKeyAlgorithm) : keyAlgorithmToAlgCfg(ca.internalCa.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: TAltNameMapping[] = []; if (altNames) { altNamesArray = altNames .split(",") .map((name) => name.trim()) .map((altName): TAltNameMapping => { const altNameType = validateAndMapAltNameType(altName); if (!altNameType) { throw new Error(`Invalid altName: ${altName}`); } return altNameType; }); const altNamesExtension = new x509.SubjectAlternativeNameExtension(altNamesArray, false); extensions.push(altNamesExtension); } 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" || value.type === "url" || value.type === "ip" ) .map((name): TAltNameMapping => { const altNameType = validateAndMapAltNameType(name.value); if (!altNameType) { throw new Error(`Invalid altName from CSR: ${name.value}`); } return altNameType; }); 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)) }); const { caCert: issuingCaCertificate, caCertChain } = await getCaCertChain({ caCertId: ca.internalCa.activeCaCertId, 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 || csrObj.subject, commonName: cn, altNames: altNamesFromCsr || altNames, serialNumber, notBefore: notBeforeDate, notAfter: notAfterDate, keyUsages: selectedKeyUsages, extendedKeyUsages: selectedExtendedKeyUsages, projectId: ca!.projectId }, tx ); await certificateBodyDAL.create( { certId: cert.id, encryptedCertificate, encryptedCertificateChain }, tx ); if (collectionId) { await pkiCollectionItemDAL.create( { pkiCollectionId: collectionId, certId: cert.id }, tx ); } return cert; }); return { certificate: leafCert, certificateChain: certificateChainPem, issuingCaCertificate, serialNumber, ca: expandInternalCa(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.findByIdWithAssociatedCa(caId); if (!ca?.internalCa?.id) throw new NotFoundError({ message: `Internal CA with ID '${caId}' not found` }); const { permission } = await permissionService.getProjectPermission({ actor, actorId, projectId: ca.projectId, actorAuthMethod, actorOrgId, actionProjectType: ActionProjectType.CertificateManager }); const certificateTemplates = await certificateTemplateDAL.find({ caId }); return { certificateTemplates: certificateTemplates.filter((el) => permission.can( ProjectPermissionPkiTemplateActions.Read, subject(ProjectPermissionSub.CertificateTemplates, { name: el.name }) ) ), ca: expandInternalCa(ca) }; }; return { createCa, getCaById, updateCaById, deleteCaById, getCaCsr, renewCaCert, getCaCerts, getCaCert, getCaCertById, signIntermediate, importCertToCa, issueCertFromCa, signCertFromCa, getCaCertificateTemplates }; };