diff --git a/backend/src/@types/fastify.d.ts b/backend/src/@types/fastify.d.ts index e5c2e19e4..f181d1c23 100644 --- a/backend/src/@types/fastify.d.ts +++ b/backend/src/@types/fastify.d.ts @@ -36,6 +36,7 @@ import { ActorAuthMethod, ActorType } from "@app/services/auth/auth-type"; import { TAuthTokenServiceFactory } from "@app/services/auth-token/auth-token-service"; import { TCertificateServiceFactory } from "@app/services/certificate/certificate-service"; import { TCertificateAuthorityServiceFactory } from "@app/services/certificate-authority/certificate-authority-service"; +import { TCertificateEstServiceFactory } from "@app/services/certificate-est/certificate-est-service"; import { TCertificateTemplateServiceFactory } from "@app/services/certificate-template/certificate-template-service"; import { TGroupProjectServiceFactory } from "@app/services/group-project/group-project-service"; import { TIdentityServiceFactory } from "@app/services/identity/identity-service"; @@ -160,6 +161,7 @@ declare module "fastify" { certificateTemplate: TCertificateTemplateServiceFactory; certificateAuthority: TCertificateAuthorityServiceFactory; certificateAuthorityCrl: TCertificateAuthorityCrlServiceFactory; + certificateEst: TCertificateEstServiceFactory; pkiCollection: TPkiCollectionServiceFactory; secretScanning: TSecretScanningServiceFactory; license: TLicenseServiceFactory; diff --git a/backend/src/server/routes/est/certificate-est-router.ts b/backend/src/server/routes/est/certificate-est-router.ts index 145b6340a..948828cdc 100644 --- a/backend/src/server/routes/est/certificate-est-router.ts +++ b/backend/src/server/routes/est/certificate-est-router.ts @@ -1,6 +1,4 @@ -import * as x509 from "@peculiar/x509"; import bcrypt from "bcrypt"; -import { Certificate, ContentInfo, EncapsulatedContentInfo, SignedData } from "pkijs"; import { z } from "zod"; import { BadRequestError, UnauthorizedError } from "@app/lib/errors"; @@ -17,7 +15,7 @@ export const registerCertificateEstRouter = async (server: FastifyZodProvider) = } }); - // Authenticate EST client + // Authenticate EST client using Passphrase server.addHook("onRequest", async (req, res) => { const { authorization } = req.headers; if (!authorization) { @@ -47,64 +45,7 @@ export const registerCertificateEstRouter = async (server: FastifyZodProvider) = if (!estConfig.isEnabled) { throw new BadRequestError({ - message: "EST enrollment is disabled" - }); - } - - const sslClientCert = req.headers["x-ssl-client-cert"] as string; - const leafCertificate = decodeURIComponent(sslClientCert).match( - /-----BEGIN CERTIFICATE-----[\s\S]+?-----END CERTIFICATE-----/g - )?.[0]; - - if (!sslClientCert || !leafCertificate) { - throw new UnauthorizedError({ message: "Missing client certificate" }); - } - - const clientCertBody = leafCertificate - .replace("-----BEGIN CERTIFICATE-----", "") - .replace("-----END CERTIFICATE-----", "") - .replace(/\n/g, "") - .replace(/ /g, "") - .trim(); - - // validate SSL client cert against configured CA - const chainCerts = estConfig.caChain - .match(/-----BEGIN CERTIFICATE-----[\s\S]+?-----END CERTIFICATE-----/g) - ?.map((cert) => { - const processedBody = cert - .replace("-----BEGIN CERTIFICATE-----", "") - .replace("-----END CERTIFICATE-----", "") - .replace(/\n/g, "") - .replace(/ /g, "") - .trim(); - - const certificateBuffer = Buffer.from(processedBody, "base64"); - return new x509.X509Certificate(certificateBuffer); - }); - - if (!chainCerts) { - throw new BadRequestError({ message: "Failed to parse certificate chain" }); - } - - let isSslClientCertValid = true; - let certToVerify = new x509.X509Certificate(clientCertBody); - - for await (const issuerCert of chainCerts) { - if ( - await certToVerify.verify({ - publicKey: issuerCert.publicKey, - date: new Date() - }) - ) { - certToVerify = issuerCert; // Move to the next certificate in the chain - } else { - isSslClientCertValid = false; - } - } - - if (!isSslClientCertValid) { - throw new UnauthorizedError({ - message: "Invalid client certificate" + message: "EST is disabled" }); } @@ -137,42 +78,23 @@ export const registerCertificateEstRouter = async (server: FastifyZodProvider) = rateLimit: writeLimit }, schema: { - body: z.string(), + body: z.string().min(1), params: z.object({ - certificateTemplateId: z.string() + certificateTemplateId: z.string().min(1) }), response: { 200: z.object({}) } }, handler: async (req, res) => { - const { rawCertificate } = await server.services.certificateAuthority.signCertFromCa({ - isInternal: true, - certificateTemplateId: req.params.certificateTemplateId, - csr: req.body - }); - void res.header("Content-Type", "application/pkcs7-mime; smime-type=certs-only"); void res.header("Content-Transfer-Encoding", "base64"); - const cert = Certificate.fromBER(rawCertificate); - const cmsSigned = new SignedData({ - encapContentInfo: new EncapsulatedContentInfo({ - eContentType: "1.2.840.113549.1.7.1" // not encrypted and not compressed data - }), - certificates: [cert] + return server.services.certificateEst.simpleEnroll({ + csr: req.body, + certificateTemplateId: req.params.certificateTemplateId, + sslClientCert: req.headers["x-ssl-client-cert"] as string }); - - const cmsContent = new ContentInfo({ - contentType: "1.2.840.113549.1.7.2", // SignedData - // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment - content: cmsSigned.toSchema() - }); - - const derBuffer = cmsContent.toSchema().toBER(false); - const base64Pkcs7 = Buffer.from(derBuffer).toString("base64"); - - return base64Pkcs7; } }); }; diff --git a/backend/src/server/routes/index.ts b/backend/src/server/routes/index.ts index edef022b8..f8d004618 100644 --- a/backend/src/server/routes/index.ts +++ b/backend/src/server/routes/index.ts @@ -89,6 +89,7 @@ import { certificateAuthorityDALFactory } from "@app/services/certificate-author import { certificateAuthorityQueueFactory } from "@app/services/certificate-authority/certificate-authority-queue"; import { certificateAuthoritySecretDALFactory } from "@app/services/certificate-authority/certificate-authority-secret-dal"; import { certificateAuthorityServiceFactory } from "@app/services/certificate-authority/certificate-authority-service"; +import { certificateEstServiceFactory } from "@app/services/certificate-est/certificate-est-service"; import { certificateTemplateDALFactory } from "@app/services/certificate-template/certificate-template-dal"; import { certificateTemplateEstConfigDALFactory } from "@app/services/certificate-template/certificate-template-est-config-dal"; import { certificateTemplateServiceFactory } from "@app/services/certificate-template/certificate-template-service"; @@ -657,6 +658,11 @@ export const registerRoutes = async ( projectDAL }); + const certificateEstService = certificateEstServiceFactory({ + certificateAuthorityService, + certificateTemplateService + }); + const pkiAlertService = pkiAlertServiceFactory({ pkiAlertDAL, pkiCollectionDAL, @@ -1179,6 +1185,7 @@ export const registerRoutes = async ( certificateAuthority: certificateAuthorityService, certificateTemplate: certificateTemplateService, certificateAuthorityCrl: certificateAuthorityCrlService, + certificateEst: certificateEstService, pkiAlert: pkiAlertService, pkiCollection: pkiCollectionService, secretScanning: secretScanningService, diff --git a/backend/src/services/certificate-est/certificate-est-service.ts b/backend/src/services/certificate-est/certificate-est-service.ts new file mode 100644 index 000000000..da0131c0a --- /dev/null +++ b/backend/src/services/certificate-est/certificate-est-service.ts @@ -0,0 +1,127 @@ +import * as x509 from "@peculiar/x509"; +import { Certificate, ContentInfo, EncapsulatedContentInfo, SignedData } from "pkijs"; + +import { BadRequestError, UnauthorizedError } from "@app/lib/errors"; + +import { TCertificateAuthorityServiceFactory } from "../certificate-authority/certificate-authority-service"; +import { TCertificateTemplateServiceFactory } from "../certificate-template/certificate-template-service"; + +type TCertificateEstServiceFactoryDep = { + certificateAuthorityService: Pick; + certificateTemplateService: Pick; +}; + +export type TCertificateEstServiceFactory = ReturnType; + +export const certificateEstServiceFactory = ({ + certificateAuthorityService, + certificateTemplateService +}: TCertificateEstServiceFactoryDep) => { + const simpleEnroll = async ({ + csr, + certificateTemplateId, + sslClientCert + }: { + csr: string; + certificateTemplateId: string; + sslClientCert: string; + }) => { + /* We first have to assert that the client certificate provided can be traced back to the attached + CA chain in the EST configuration + */ + const estConfig = await certificateTemplateService.getEstConfiguration({ + isInternal: true, + certificateTemplateId + }); + + if (!estConfig.isEnabled) { + throw new BadRequestError({ + message: "EST is disabled" + }); + } + + const leafCertificate = decodeURIComponent(sslClientCert).match( + /-----BEGIN CERTIFICATE-----[\s\S]+?-----END CERTIFICATE-----/g + )?.[0]; + + if (!sslClientCert || !leafCertificate) { + throw new UnauthorizedError({ message: "Missing client certificate" }); + } + + const clientCertBody = leafCertificate + .replace("-----BEGIN CERTIFICATE-----", "") + .replace("-----END CERTIFICATE-----", "") + .replace(/\n/g, "") + .replace(/ /g, "") + .trim(); + + // validate SSL client cert against configured CA + const chainCerts = estConfig.caChain + .match(/-----BEGIN CERTIFICATE-----[\s\S]+?-----END CERTIFICATE-----/g) + ?.map((cert) => { + const processedBody = cert + .replace("-----BEGIN CERTIFICATE-----", "") + .replace("-----END CERTIFICATE-----", "") + .replace(/\n/g, "") + .replace(/ /g, "") + .trim(); + + const certificateBuffer = Buffer.from(processedBody, "base64"); + return new x509.X509Certificate(certificateBuffer); + }); + + if (!chainCerts) { + throw new BadRequestError({ message: "Failed to parse certificate chain" }); + } + + let isSslClientCertValid = true; + let certToVerify = new x509.X509Certificate(clientCertBody); + + for await (const issuerCert of chainCerts) { + if ( + await certToVerify.verify({ + publicKey: issuerCert.publicKey, + date: new Date() + }) + ) { + certToVerify = issuerCert; // Move to the next certificate in the chain + } else { + isSslClientCertValid = false; + } + } + + if (!isSslClientCertValid) { + throw new UnauthorizedError({ + message: "Invalid client certificate" + }); + } + + const { rawCertificate } = await certificateAuthorityService.signCertFromCa({ + isInternal: true, + certificateTemplateId, + csr + }); + + const cert = Certificate.fromBER(rawCertificate); + const cmsSigned = new SignedData({ + encapContentInfo: new EncapsulatedContentInfo({ + eContentType: "1.2.840.113549.1.7.1" // not encrypted and not compressed data + }), + certificates: [cert] + }); + + const cmsContent = new ContentInfo({ + contentType: "1.2.840.113549.1.7.2", // SignedData + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment + content: cmsSigned.toSchema() + }); + + const derBuffer = cmsContent.toSchema().toBER(false); + const base64Pkcs7 = Buffer.from(derBuffer).toString("base64"); + + return base64Pkcs7; + }; + return { + simpleEnroll + }; +};