diff --git a/backend/src/services/certificate-v3/certificate-v3-service.ts b/backend/src/services/certificate-v3/certificate-v3-service.ts index c20412537..fd46f2c5f 100644 --- a/backend/src/services/certificate-v3/certificate-v3-service.ts +++ b/backend/src/services/certificate-v3/certificate-v3-service.ts @@ -19,8 +19,10 @@ import { TCertificateBodyDALFactory } from "@app/services/certificate/certificat import { TCertificateDALFactory } from "@app/services/certificate/certificate-dal"; import { TCertificateSecretDALFactory } from "@app/services/certificate/certificate-secret-dal"; import { + CertExtendedKeyUsage, CertKeyAlgorithm, CertKeyType, + CertKeyUsage, CertSignatureAlgorithm, CertStatus } from "@app/services/certificate/certificate-types"; @@ -46,7 +48,9 @@ import { getProjectKmsCertificateKeyId } from "@app/services/project/project-fns import { CertExtendedKeyUsageType, CertKeyUsageType, - CertSubjectAlternativeNameType + CertSubjectAlternativeNameType, + mapLegacyExtendedKeyUsageToStandard, + mapLegacyKeyUsageToStandard } from "../certificate-common/certificate-constants"; import { extractAlgorithmsFromCSR, @@ -309,47 +313,54 @@ const extractCertificateFromBuffer = (certData: Buffer | { rawData: Buffer } | s const parseKeyUsages = (keyUsages: unknown): CertKeyUsageType[] => { if (!keyUsages) return []; - const validKeyUsages = Object.values(CertKeyUsageType); + const validKeyUsages = [...Object.values(CertKeyUsageType), ...Object.values(CertKeyUsage)] as string[]; + + const normalize = (usage: string): CertKeyUsageType | null => { + if (validKeyUsages.includes(usage)) { + return mapLegacyKeyUsageToStandard(usage as CertKeyUsageType); + } + return null; + }; + + let raw: string[]; if (Array.isArray(keyUsages)) { - return keyUsages.filter( - (usage): usage is CertKeyUsageType => - typeof usage === "string" && validKeyUsages.includes(usage as CertKeyUsageType) - ); + raw = keyUsages.filter((u): u is string => typeof u === "string"); + } else if (typeof keyUsages === "string") { + raw = keyUsages.split(",").map((u) => u.trim()); + } else { + return []; } - if (typeof keyUsages === "string") { - return keyUsages - .split(",") - .map((usage) => usage.trim()) - .filter((usage): usage is CertKeyUsageType => validKeyUsages.includes(usage as CertKeyUsageType)); - } - - return []; + return raw.map((u) => normalize(u)).filter((u): u is CertKeyUsageType => u !== null); }; const parseExtendedKeyUsages = (extendedKeyUsages: unknown): CertExtendedKeyUsageType[] => { if (!extendedKeyUsages) return []; - const validExtendedKeyUsages = Object.values(CertExtendedKeyUsageType); + const validExtendedKeyUsages = [ + ...Object.values(CertExtendedKeyUsageType), + ...Object.values(CertExtendedKeyUsage) + ] as string[]; + + const normalize = (usage: string): CertExtendedKeyUsageType | null => { + if (validExtendedKeyUsages.includes(usage)) { + return mapLegacyExtendedKeyUsageToStandard(usage as CertExtendedKeyUsageType); + } + return null; + }; + + let raw: string[]; if (Array.isArray(extendedKeyUsages)) { - return extendedKeyUsages.filter( - (usage): usage is CertExtendedKeyUsageType => - typeof usage === "string" && validExtendedKeyUsages.includes(usage as CertExtendedKeyUsageType) - ); + raw = extendedKeyUsages.filter((u): u is string => typeof u === "string"); + } else if (typeof extendedKeyUsages === "string") { + raw = extendedKeyUsages.split(",").map((u) => u.trim()); + } else { + return []; } - if (typeof extendedKeyUsages === "string") { - return extendedKeyUsages - .split(",") - .map((usage) => usage.trim()) - .filter((usage): usage is CertExtendedKeyUsageType => - validExtendedKeyUsages.includes(usage as CertExtendedKeyUsageType) - ); - } - - return []; + return raw.map((u) => normalize(u)).filter((u): u is CertExtendedKeyUsageType => u !== null); }; const convertEnumsToStringArray = (enumArray: T[]): string[] => { diff --git a/frontend/src/pages/cert-manager/CertificatesPage/components/CertificateExportModal.tsx b/frontend/src/pages/cert-manager/CertificatesPage/components/CertificateExportModal.tsx index d16a1ac3f..8fc59e013 100644 --- a/frontend/src/pages/cert-manager/CertificatesPage/components/CertificateExportModal.tsx +++ b/frontend/src/pages/cert-manager/CertificatesPage/components/CertificateExportModal.tsx @@ -1,6 +1,9 @@ -import { useEffect, useState } from "react"; +import { useEffect } from "react"; +import { Controller, useForm } from "react-hook-form"; import { faDownload } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { z } from "zod"; import { Button, @@ -41,55 +44,119 @@ export type ExportOptions = { }; }; -export const CertificateExportModal = ({ popUp, handlePopUpToggle, onFormatSelected }: Props) => { - const [selectedFormat, setSelectedFormat] = useState("pem"); - const [pkcs12Options, setPkcs12Options] = useState({ - password: "", - alias: "" - }); +const exportFormSchema = z + .object({ + format: z.enum(["pem", "pkcs12"]), + pkcs12Password: z.string().optional(), + pkcs12Alias: z.string().optional() + }) + .refine( + (data) => { + if (data.format === "pkcs12") { + return data.pkcs12Password && data.pkcs12Alias && data.pkcs12Alias.trim() !== ""; + } + return true; + }, + { + message: "PKCS12 password and alias are required when using PKCS12 format", + path: ["pkcs12Password"] + } + ) + .refine( + (data) => { + if (data.format === "pkcs12") { + return data.pkcs12Password && data.pkcs12Password.length >= 6; + } + return true; + }, + { + message: "PKCS12 password must be 6 characters or longer", + path: ["pkcs12Password"] + } + ) + .refine( + (data) => { + if (data.format === "pkcs12" && data.pkcs12Password) { + return data.pkcs12Password.length >= 6; + } + return true; + }, + { + message: "Password must be at least 6 characters long", + path: ["pkcs12Password"] + } + ) + .refine( + (data) => { + if (data.format === "pkcs12") { + return data.pkcs12Alias && data.pkcs12Alias.trim() !== ""; + } + return true; + }, + { + message: "Certificate alias is required", + path: ["pkcs12Alias"] + } + ); +type ExportFormData = z.infer; + +export const CertificateExportModal = ({ popUp, handlePopUpToggle, onFormatSelected }: Props) => { const { certificateId, serialNumber } = (popUp?.certificateExport?.data as { certificateId: string; serialNumber: string; }) || {}; + const { + control, + handleSubmit, + reset, + watch, + formState: { isSubmitting } + } = useForm({ + resolver: zodResolver(exportFormSchema), + defaultValues: { + format: "pem", + pkcs12Password: "", + pkcs12Alias: "" + } + }); + + const selectedFormat = watch("format"); + // Reset form whenever the modal opens useEffect(() => { if (popUp?.certificateExport?.isOpen) { - setSelectedFormat("pem"); - setPkcs12Options({ - password: "", - alias: "" + reset({ + format: "pem", + pkcs12Password: "", + pkcs12Alias: "" }); } - }, [popUp?.certificateExport?.isOpen]); + }, [popUp?.certificateExport?.isOpen, reset]); - const isFormValid = () => { - if (selectedFormat === "pkcs12") { - return pkcs12Options.password.length >= 6 && pkcs12Options.alias.trim() !== ""; + const onFormSubmit = (data: ExportFormData) => { + if (!(certificateId || serialNumber)) return; + + const options: ExportOptions = {}; + + if (data.format === "pkcs12") { + options.pkcs12 = { + password: data.pkcs12Password!, + alias: data.pkcs12Alias! + }; } - return true; - }; - const handleExport = () => { - if ((certificateId || serialNumber) && isFormValid()) { - const options: ExportOptions = {}; - - if (selectedFormat === "pkcs12") { - options.pkcs12 = pkcs12Options; - } - - onFormatSelected( - selectedFormat, - { - certificateId, - serialNumber - }, - options - ); - handlePopUpToggle("certificateExport", false); - } + onFormatSelected( + data.format, + { + certificateId, + serialNumber + }, + options + ); + handlePopUpToggle("certificateExport", false); }; return ( @@ -100,79 +167,89 @@ export const CertificateExportModal = ({ popUp, handlePopUpToggle, onFormatSelec }} > -
-

Choose the format for exporting your certificate

+
+
+

+ Choose the format for exporting your certificate +

- - - - - {selectedFormat === "pkcs12" && ( - <> - 0 && pkcs12Options.password.length < 6 - ? undefined - : "Password to protect the PKCS12 keystore (minimum 6 characters)" - } - isError={pkcs12Options.password.length > 0 && pkcs12Options.password.length < 6} - errorText="Password must be at least 6 characters long" - > - - setPkcs12Options((prev) => ({ ...prev, password: e.target.value })) + ( + - + isError={Boolean(error)} + errorText={error?.message} + > + + + )} + /> - + ( + + + + )} + /> + + ( + + + + )} + /> + + )} + +
+ - + Cancel + + +
-
+
);