diff --git a/backend/src/db/migrations/20240813141341_certificate-authority-est-configuration.ts b/backend/src/db/migrations/20240813141341_certificate-authority-est-configuration.ts index fe83b5e06..db0164d3e 100644 --- a/backend/src/db/migrations/20240813141341_certificate-authority-est-configuration.ts +++ b/backend/src/db/migrations/20240813141341_certificate-authority-est-configuration.ts @@ -1,6 +1,7 @@ import { Knex } from "knex"; import { TableName } from "../schemas"; +import { createOnUpdateTrigger, dropOnUpdateTrigger } from "../utils"; export async function up(knex: Knex): Promise { const hasEstConfigTable = await knex.schema.hasTable(TableName.CertificateAuthorityEstConfig); @@ -14,9 +15,12 @@ export async function up(knex: Knex): Promise { tb.boolean("isEnabled"); tb.timestamps(true, true, true); }); + + await createOnUpdateTrigger(knex, TableName.CertificateAuthorityEstConfig); } } export async function down(knex: Knex): Promise { await knex.schema.dropTableIfExists(TableName.CertificateAuthorityEstConfig); + await dropOnUpdateTrigger(knex, TableName.CertificateAuthorityEstConfig); } diff --git a/backend/src/server/routes/est/certificate-est-router.ts b/backend/src/server/routes/est/certificate-est-router.ts index 453a260f5..fdbc3babe 100644 --- a/backend/src/server/routes/est/certificate-est-router.ts +++ b/backend/src/server/routes/est/certificate-est-router.ts @@ -1,4 +1,5 @@ import * as x509 from "@peculiar/x509"; +import bcrypt from "bcrypt"; import { Certificate, ContentInfo, EncapsulatedContentInfo, SignedData } from "pkijs"; import { z } from "zod"; @@ -39,41 +40,27 @@ export const registerCertificateEstRouter = async (server: FastifyZodProvider) = const urlFragments = req.url.split("/"); const certificateAuthorityId = urlFragments.slice(-2)[0]; + const caEstConfig = await server.services.certificateAuthority.getCaEstConfiguration({ + isInternal: true, + caId: certificateAuthorityId + }); - const hardcodedCertificateChain = ` - -----BEGIN CERTIFICATE----- - MIIEYzCCA0ugAwIBAgIUbxMrGIZnxNcX2kuYpGOFqix9P80wDQYJKoZIhvcNAQEL - BQAwaTELMAkGA1UEBhMCUEgxDTALBgNVBAgMBENlYnUxDTALBgNVBAcMBENlYnUx - EjAQBgNVBAoMCUluZmlzaWNhbDEUMBIGA1UECwwLRW5naW5lZXJpbmcxEjAQBgNV - BAMMCWxvY2FsaG9zdDAeFw0yNDA4MTIxMzM4MTNaFw0yNTA4MTIxMzM4MTNaMGkx - CzAJBgNVBAYTAlBIMQ0wCwYDVQQIDARDZWJ1MQ0wCwYDVQQHDARDZWJ1MRIwEAYD - VQQKDAlJbmZpc2ljYWwxFDASBgNVBAsMC0VuZ2luZWVyaW5nMRIwEAYDVQQDDAls - b2NhbGhvc3QwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDqssBBMfzr - 1DDRIxl8TcCHmQU+qhmw8ACkoNN0b+vD0USVv4SC1ABKtYQBBDvBOtQulqc4yTRw - A3Q0y3XUR+pyCFb5PcTG8ZFUZ7ewrrHrdExd0enY/R3eDPAb6H7hokDS10Sr5BRR - Oow109yzX7ipbw+kYSOOLTF1gX+ewbfpcGNylJNOvFNcu4V64Qg5NXp2Lo4o/VTj - IY9yxgVjep8utC/klughk3/EUqfyZ8/9BHyYj3KWDj7VpZNU4o506ZkYsCOPESe1 - SMl8z4s4bEkfTd6+9SetKkwmCbRpZE5iS0XV0lrySK7AGwKHPuJ5RYj0WZp5O/SK - 1zC0azN787T3AgMBAAGjggEBMIH+MB0GA1UdDgQWBBT25nGrtg4VmDaXscjwEv/B - CSFd2jCBpgYDVR0jBIGeMIGbgBT25nGrtg4VmDaXscjwEv/BCSFd2qFtpGswaTEL - MAkGA1UEBhMCUEgxDTALBgNVBAgMBENlYnUxDTALBgNVBAcMBENlYnUxEjAQBgNV - BAoMCUluZmlzaWNhbDEUMBIGA1UECwwLRW5naW5lZXJpbmcxEjAQBgNVBAMMCWxv - Y2FsaG9zdIIUbxMrGIZnxNcX2kuYpGOFqix9P80wDwYDVR0TAQH/BAUwAwEB/zAO - BgNVHQ8BAf8EBAMCBaAwEwYDVR0lBAwwCgYIKwYBBQUHAwIwDQYJKoZIhvcNAQEL - BQADggEBABPV6jpVHvnvp6cAPewL6SSN20KGdNX3MCpLIxPhz8dbGnc2SWMaR0Eo - GqAYvUgG0xpEWCTZ7RDtfrU7vt6+PnFpP2z0a4YToF24/tdAOMAUQ2AedULAb8UP - gwHDeZKKYhs7kscApO0VgYJgjqFe2Kjlt0zzVcMj0qrwgdDUFTNWGOdQy1ghmStc - nBw2xVppG0QAyIWnvxqPva+czHhMd8bmLR44VCuzO5xS5B/AUk7BeNBLuEEfM3DR - quZ0PRwgsaY/WND3ux93FaSiqfn5y9uZdJkqfJcPL6SKRms6v6da4Rh/DyFcWQFW - iwIeUl1cXagVKziyr4Ch5U5dnp+y8Es= - -----END CERTIFICATE----- - `; + if (!caEstConfig.isEnabled) { + throw new BadRequestError({ + message: "EST enrollment is disabled" + }); + } const sslClientCert = req.headers["x-ssl-client-cert"] as string; - if (!sslClientCert) { + 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 = decodeURIComponent(sslClientCert) + + const clientCertBody = leafCertificate .replace("-----BEGIN CERTIFICATE-----", "") .replace("-----END CERTIFICATE-----", "") .replace(/\n/g, "") @@ -81,7 +68,7 @@ export const registerCertificateEstRouter = async (server: FastifyZodProvider) = .trim(); // validate SSL client cert against configured CA - const chainCerts = hardcodedCertificateChain + const chainCerts = caEstConfig.caChain .match(/-----BEGIN CERTIFICATE-----[\s\S]+?-----END CERTIFICATE-----/g) ?.map((cert) => { const processedBody = cert @@ -126,8 +113,21 @@ export const registerCertificateEstRouter = async (server: FastifyZodProvider) = throw new UnauthorizedError({ message: "Missing HTTP credentials" }); } + // expected format is user:password const basicCredential = atob(rawCredential); - // compare with EST configuration here + const password = basicCredential.split(":").pop(); + if (!password) { + throw new BadRequestError({ + message: "No password provided" + }); + } + + const isPasswordValid = await bcrypt.compare(password, caEstConfig.hashedPassphrase); + if (!isPasswordValid) { + throw new UnauthorizedError({ + message: "Invalid credentials" + }); + } }); server.route({ diff --git a/backend/src/server/routes/v1/certificate-authority-router.ts b/backend/src/server/routes/v1/certificate-authority-router.ts index 1306dd657..a5df73bbf 100644 --- a/backend/src/server/routes/v1/certificate-authority-router.ts +++ b/backend/src/server/routes/v1/certificate-authority-router.ts @@ -810,6 +810,7 @@ export const registerCaRouter = async (server: FastifyZodProvider) => { }, handler: async (req) => { const caEstConfig = await server.services.certificateAuthority.getCaEstConfiguration({ + isInternal: false, caId: req.params.caId, actor: req.permission.type, actorId: req.permission.id, diff --git a/backend/src/services/certificate-authority/certificate-authority-service.ts b/backend/src/services/certificate-authority/certificate-authority-service.ts index 89223babb..dfbb895f9 100644 --- a/backend/src/services/certificate-authority/certificate-authority-service.ts +++ b/backend/src/services/certificate-authority/certificate-authority-service.ts @@ -1535,30 +1535,28 @@ export const certificateAuthorityServiceFactory = ({ return estConfig; }; - const getCaEstConfiguration = async ({ - caId, - actorId, - actorAuthMethod, - actor, - actorOrgId - }: TGetCaEstConfigurationDTO) => { - const ca = await certificateAuthorityDAL.findById(caId); + const getCaEstConfiguration = async (dto: TGetCaEstConfigurationDTO) => { + const ca = await certificateAuthorityDAL.findById(dto.caId); if (!ca) { throw new NotFoundError({ message: "CA not found" }); } - const { permission } = await permissionService.getProjectPermission( - actor, - actorId, - ca.projectId, - actorAuthMethod, - actorOrgId - ); + if (!dto.isInternal) { + const { permission } = await permissionService.getProjectPermission( + dto.actor, + dto.actorId, + ca.projectId, + dto.actorAuthMethod, + dto.actorOrgId + ); - ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionActions.Edit, - ProjectPermissionSub.CertificateAuthorities - ); + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionActions.Edit, + ProjectPermissionSub.CertificateAuthorities + ); + } + + const { caId } = dto; const caEstConfig = await certificateAuthorityEstConfigDAL.findOne({ caId @@ -1587,7 +1585,8 @@ export const certificateAuthorityServiceFactory = ({ return { caId, isEnabled: caEstConfig.isEnabled, - caChain: decryptedCaChain.toString() + caChain: decryptedCaChain.toString(), + hashedPassphrase: caEstConfig.hashedPassphrase }; }; diff --git a/backend/src/services/certificate-authority/certificate-authority-types.ts b/backend/src/services/certificate-authority/certificate-authority-types.ts index cbe552b08..2f0d789bb 100644 --- a/backend/src/services/certificate-authority/certificate-authority-types.ts +++ b/backend/src/services/certificate-authority/certificate-authority-types.ts @@ -181,6 +181,12 @@ export type TUpdateCaEstConfigurationDTO = { isEnabled?: boolean; } & Omit; -export type TGetCaEstConfigurationDTO = { - caId: string; -} & Omit; +export type TGetCaEstConfigurationDTO = + | { + isInternal: true; + caId: string; + } + | ({ + isInternal: false; + caId: string; + } & Omit); diff --git a/frontend/src/views/Project/CaPage/components/CaEnrollmentModal.tsx b/frontend/src/views/Project/CaPage/components/CaEnrollmentModal.tsx index a9bbf4d91..df03a6b11 100644 --- a/frontend/src/views/Project/CaPage/components/CaEnrollmentModal.tsx +++ b/frontend/src/views/Project/CaPage/components/CaEnrollmentModal.tsx @@ -155,7 +155,7 @@ export const CaEnrollmentModal = ({ popUp, handlePopUpToggle }: Props) => { >