feat: integrated to est routes

This commit is contained in:
Sheen Capadngan
2024-08-14 20:52:21 +08:00
parent 5ae33b9f3b
commit 146c4284a2
6 changed files with 66 additions and 56 deletions
@@ -1,6 +1,7 @@
import { Knex } from "knex"; import { Knex } from "knex";
import { TableName } from "../schemas"; import { TableName } from "../schemas";
import { createOnUpdateTrigger, dropOnUpdateTrigger } from "../utils";
export async function up(knex: Knex): Promise<void> { export async function up(knex: Knex): Promise<void> {
const hasEstConfigTable = await knex.schema.hasTable(TableName.CertificateAuthorityEstConfig); const hasEstConfigTable = await knex.schema.hasTable(TableName.CertificateAuthorityEstConfig);
@@ -14,9 +15,12 @@ export async function up(knex: Knex): Promise<void> {
tb.boolean("isEnabled"); tb.boolean("isEnabled");
tb.timestamps(true, true, true); tb.timestamps(true, true, true);
}); });
await createOnUpdateTrigger(knex, TableName.CertificateAuthorityEstConfig);
} }
} }
export async function down(knex: Knex): Promise<void> { export async function down(knex: Knex): Promise<void> {
await knex.schema.dropTableIfExists(TableName.CertificateAuthorityEstConfig); await knex.schema.dropTableIfExists(TableName.CertificateAuthorityEstConfig);
await dropOnUpdateTrigger(knex, TableName.CertificateAuthorityEstConfig);
} }
@@ -1,4 +1,5 @@
import * as x509 from "@peculiar/x509"; import * as x509 from "@peculiar/x509";
import bcrypt from "bcrypt";
import { Certificate, ContentInfo, EncapsulatedContentInfo, SignedData } from "pkijs"; import { Certificate, ContentInfo, EncapsulatedContentInfo, SignedData } from "pkijs";
import { z } from "zod"; import { z } from "zod";
@@ -39,41 +40,27 @@ export const registerCertificateEstRouter = async (server: FastifyZodProvider) =
const urlFragments = req.url.split("/"); const urlFragments = req.url.split("/");
const certificateAuthorityId = urlFragments.slice(-2)[0]; const certificateAuthorityId = urlFragments.slice(-2)[0];
const caEstConfig = await server.services.certificateAuthority.getCaEstConfiguration({
isInternal: true,
caId: certificateAuthorityId
});
const hardcodedCertificateChain = ` if (!caEstConfig.isEnabled) {
-----BEGIN CERTIFICATE----- throw new BadRequestError({
MIIEYzCCA0ugAwIBAgIUbxMrGIZnxNcX2kuYpGOFqix9P80wDQYJKoZIhvcNAQEL message: "EST enrollment is disabled"
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-----
`;
const sslClientCert = req.headers["x-ssl-client-cert"] as string; 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" }); throw new UnauthorizedError({ message: "Missing client certificate" });
} }
const clientCertBody = decodeURIComponent(sslClientCert)
const clientCertBody = leafCertificate
.replace("-----BEGIN CERTIFICATE-----", "") .replace("-----BEGIN CERTIFICATE-----", "")
.replace("-----END CERTIFICATE-----", "") .replace("-----END CERTIFICATE-----", "")
.replace(/\n/g, "") .replace(/\n/g, "")
@@ -81,7 +68,7 @@ export const registerCertificateEstRouter = async (server: FastifyZodProvider) =
.trim(); .trim();
// validate SSL client cert against configured CA // validate SSL client cert against configured CA
const chainCerts = hardcodedCertificateChain const chainCerts = caEstConfig.caChain
.match(/-----BEGIN CERTIFICATE-----[\s\S]+?-----END CERTIFICATE-----/g) .match(/-----BEGIN CERTIFICATE-----[\s\S]+?-----END CERTIFICATE-----/g)
?.map((cert) => { ?.map((cert) => {
const processedBody = cert const processedBody = cert
@@ -126,8 +113,21 @@ export const registerCertificateEstRouter = async (server: FastifyZodProvider) =
throw new UnauthorizedError({ message: "Missing HTTP credentials" }); throw new UnauthorizedError({ message: "Missing HTTP credentials" });
} }
// expected format is user:password
const basicCredential = atob(rawCredential); 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({ server.route({
@@ -810,6 +810,7 @@ export const registerCaRouter = async (server: FastifyZodProvider) => {
}, },
handler: async (req) => { handler: async (req) => {
const caEstConfig = await server.services.certificateAuthority.getCaEstConfiguration({ const caEstConfig = await server.services.certificateAuthority.getCaEstConfiguration({
isInternal: false,
caId: req.params.caId, caId: req.params.caId,
actor: req.permission.type, actor: req.permission.type,
actorId: req.permission.id, actorId: req.permission.id,
@@ -1535,30 +1535,28 @@ export const certificateAuthorityServiceFactory = ({
return estConfig; return estConfig;
}; };
const getCaEstConfiguration = async ({ const getCaEstConfiguration = async (dto: TGetCaEstConfigurationDTO) => {
caId, const ca = await certificateAuthorityDAL.findById(dto.caId);
actorId,
actorAuthMethod,
actor,
actorOrgId
}: TGetCaEstConfigurationDTO) => {
const ca = await certificateAuthorityDAL.findById(caId);
if (!ca) { if (!ca) {
throw new NotFoundError({ message: "CA not found" }); throw new NotFoundError({ message: "CA not found" });
} }
const { permission } = await permissionService.getProjectPermission( if (!dto.isInternal) {
actor, const { permission } = await permissionService.getProjectPermission(
actorId, dto.actor,
ca.projectId, dto.actorId,
actorAuthMethod, ca.projectId,
actorOrgId dto.actorAuthMethod,
); dto.actorOrgId
);
ForbiddenError.from(permission).throwUnlessCan( ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Edit, ProjectPermissionActions.Edit,
ProjectPermissionSub.CertificateAuthorities ProjectPermissionSub.CertificateAuthorities
); );
}
const { caId } = dto;
const caEstConfig = await certificateAuthorityEstConfigDAL.findOne({ const caEstConfig = await certificateAuthorityEstConfigDAL.findOne({
caId caId
@@ -1587,7 +1585,8 @@ export const certificateAuthorityServiceFactory = ({
return { return {
caId, caId,
isEnabled: caEstConfig.isEnabled, isEnabled: caEstConfig.isEnabled,
caChain: decryptedCaChain.toString() caChain: decryptedCaChain.toString(),
hashedPassphrase: caEstConfig.hashedPassphrase
}; };
}; };
@@ -181,6 +181,12 @@ export type TUpdateCaEstConfigurationDTO = {
isEnabled?: boolean; isEnabled?: boolean;
} & Omit<TProjectPermission, "projectId">; } & Omit<TProjectPermission, "projectId">;
export type TGetCaEstConfigurationDTO = { export type TGetCaEstConfigurationDTO =
caId: string; | {
} & Omit<TProjectPermission, "projectId">; isInternal: true;
caId: string;
}
| ({
isInternal: false;
caId: string;
} & Omit<TProjectPermission, "projectId">);
@@ -155,7 +155,7 @@ export const CaEnrollmentModal = ({ popUp, handlePopUpToggle }: Props) => {
> >
<TextArea <TextArea
{...field} {...field}
className="border-none bg-mineshaft-900 text-gray-400" className="min-h-[15rem] border-none bg-mineshaft-900 text-gray-400"
reSize="none" reSize="none"
/> />
</FormControl> </FormControl>