/* eslint-disable react/no-array-index-key */ /* eslint-disable no-nested-ternary */ import { useEffect, useState } from "react"; import { Controller, useForm } from "react-hook-form"; import { faQuestionCircle } from "@fortawesome/free-regular-svg-icons"; import { faPlus, faTrash } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { zodResolver } from "@hookform/resolvers/zod"; import { z } from "zod"; import { createNotification } from "@app/components/notifications"; import { Accordion, AccordionContent, AccordionItem, AccordionTrigger, Button, Checkbox, FormControl, FormLabel, IconButton, Input, Modal, ModalContent, Select, SelectItem, Tooltip } from "@app/components/v2"; import { useProject } from "@app/context"; import { useCreateCertificateV3, useGetCert } from "@app/hooks/api"; import { useListCertificateProfiles } from "@app/hooks/api/certificateProfiles"; import { certKeyAlgorithms, EXTENDED_KEY_USAGES_OPTIONS, KEY_USAGES_OPTIONS, SIGNATURE_ALGORITHMS_OPTIONS } from "@app/hooks/api/certificates/constants"; import { CertExtendedKeyUsage, CertKeyAlgorithm, CertKeyUsage } from "@app/hooks/api/certificates/enums"; import { useGetCertificateTemplateV2ById } from "@app/hooks/api/certificateTemplates/queries"; import { UsePopUpState } from "@app/hooks/usePopUp"; import { CertificateContent } from "./CertificateContent"; const schema = z.object({ profileId: z.string().min(1, "Profile is required"), friendlyName: z.string(), subjectAttributes: z .array( z.object({ type: z.enum(["common_name"]), value: z.string().min(1, "Value is required"), include: z.enum(["mandatory", "optional", "prohibit"]).optional() }) ) .min(1, "At least one subject attribute is required"), altNames: z .array( z.object({ type: z.enum(["dns", "ip", "email", "uri"]), value: z.string().min(1, "Value is required"), include: z.enum(["mandatory", "optional", "prohibit"]).optional() }) ) .default([]), ttl: z.string().trim().min(1, "TTL is required"), signatureAlgorithm: z.string().optional(), keyAlgorithm: z.string().optional(), keyUsages: z.object({ [CertKeyUsage.DIGITAL_SIGNATURE]: z.boolean().optional(), [CertKeyUsage.KEY_ENCIPHERMENT]: z.boolean().optional(), [CertKeyUsage.NON_REPUDIATION]: z.boolean().optional(), [CertKeyUsage.DATA_ENCIPHERMENT]: z.boolean().optional(), [CertKeyUsage.KEY_AGREEMENT]: z.boolean().optional(), [CertKeyUsage.KEY_CERT_SIGN]: z.boolean().optional(), [CertKeyUsage.CRL_SIGN]: z.boolean().optional(), [CertKeyUsage.ENCIPHER_ONLY]: z.boolean().optional(), [CertKeyUsage.DECIPHER_ONLY]: z.boolean().optional() }), extendedKeyUsages: z.object({ [CertExtendedKeyUsage.CLIENT_AUTH]: z.boolean().optional(), [CertExtendedKeyUsage.CODE_SIGNING]: z.boolean().optional(), [CertExtendedKeyUsage.EMAIL_PROTECTION]: z.boolean().optional(), [CertExtendedKeyUsage.OCSP_SIGNING]: z.boolean().optional(), [CertExtendedKeyUsage.SERVER_AUTH]: z.boolean().optional(), [CertExtendedKeyUsage.TIMESTAMPING]: z.boolean().optional() }) }); export type FormData = z.infer; type Props = { popUp: UsePopUpState<["certificateIssuance"]>; handlePopUpToggle: ( popUpName: keyof UsePopUpState<["certificateIssuance"]>, state?: boolean ) => void; }; type TCertificateDetails = { serialNumber: string; certificate: string; certificateChain: string; privateKey: string; }; export const CertificateIssuanceModal = ({ popUp, handlePopUpToggle }: Props) => { const [certificateDetails, setCertificateDetails] = useState(null); const [allowedKeyUsages, setAllowedKeyUsages] = useState([]); const [allowedExtendedKeyUsages, setAllowedExtendedKeyUsages] = useState([]); const [allowedSignatureAlgorithms, setAllowedSignatureAlgorithms] = useState([]); const [allowedKeyAlgorithms, setAllowedKeyAlgorithms] = useState([]); const { currentProject } = useProject(); const { data: cert } = useGetCert( (popUp?.certificateIssuance?.data as { serialNumber: string })?.serialNumber || "" ); const { data: profilesData } = useListCertificateProfiles({ projectId: currentProject?.id || "", includeMetrics: false }); const { mutateAsync: createCertificate } = useCreateCertificateV3(); const { control, handleSubmit, reset, watch, setValue, formState: { isSubmitting } } = useForm({ resolver: zodResolver(schema), defaultValues: { profileId: "", friendlyName: "", subjectAttributes: [{ type: "common_name", value: "" }], altNames: [], ttl: "30d", signatureAlgorithm: "", keyAlgorithm: "", keyUsages: {}, extendedKeyUsages: {} } }); const selectedProfileId = watch("profileId"); const selectedProfile = profilesData?.certificateProfiles?.find( (p) => p.id === selectedProfileId ); const { data: templateData } = useGetCertificateTemplateV2ById({ templateId: selectedProfile?.certificateTemplateId || "" }); useEffect(() => { if (templateData && selectedProfile) { if (templateData.signatureAlgorithm?.defaultAlgorithm) { let sigAlgValue = templateData.signatureAlgorithm.defaultAlgorithm; const sigAlgMap: Record = { "SHA256-RSA": "RSA-SHA256", "SHA384-RSA": "RSA-SHA384", "SHA512-RSA": "RSA-SHA512", "SHA256-ECDSA": "ECDSA-SHA256", "SHA384-ECDSA": "ECDSA-SHA384", "SHA512-ECDSA": "ECDSA-SHA512" }; if (sigAlgMap[sigAlgValue]) { sigAlgValue = sigAlgMap[sigAlgValue]; } setValue("signatureAlgorithm", sigAlgValue); } if (templateData.keyAlgorithm?.defaultKeyType) { let keyAlgValue = templateData.keyAlgorithm.defaultKeyType; const keyAlgMap: Record = { "RSA-2048": CertKeyAlgorithm.RSA_2048, "RSA-3072": CertKeyAlgorithm.RSA_3072, "RSA-4096": CertKeyAlgorithm.RSA_4096, "ECDSA-P256": CertKeyAlgorithm.ECDSA_P256, "ECDSA-P384": CertKeyAlgorithm.ECDSA_P384, [CertKeyAlgorithm.ECDSA_P256]: CertKeyAlgorithm.ECDSA_P256, [CertKeyAlgorithm.ECDSA_P384]: CertKeyAlgorithm.ECDSA_P384 }; if (keyAlgMap[keyAlgValue]) { keyAlgValue = keyAlgMap[keyAlgValue]; } else { keyAlgValue = templateData.keyAlgorithm.defaultKeyType; } setValue("keyAlgorithm", keyAlgValue); } if (templateData.signatureAlgorithm?.allowedAlgorithms) { const mappedSigAlgs = templateData.signatureAlgorithm.allowedAlgorithms.map( (alg: string) => { const sigAlgMap: Record = { "SHA256-RSA": "RSA-SHA256", "SHA384-RSA": "RSA-SHA384", "SHA512-RSA": "RSA-SHA512", "SHA256-ECDSA": "ECDSA-SHA256", "SHA384-ECDSA": "ECDSA-SHA384", "SHA512-ECDSA": "ECDSA-SHA512" }; return sigAlgMap[alg] || alg; } ); setAllowedSignatureAlgorithms(mappedSigAlgs); } if (templateData.keyAlgorithm?.allowedKeyTypes) { const mappedKeyAlgs = templateData.keyAlgorithm.allowedKeyTypes.map((alg: string) => { const keyAlgMap: Record = { "RSA-2048": CertKeyAlgorithm.RSA_2048, "RSA-3072": CertKeyAlgorithm.RSA_3072, "RSA-4096": CertKeyAlgorithm.RSA_4096, "ECDSA-P256": CertKeyAlgorithm.ECDSA_P256, "ECDSA-P384": CertKeyAlgorithm.ECDSA_P384 }; return keyAlgMap[alg] || alg; }); setAllowedKeyAlgorithms(mappedKeyAlgs); } const allAllowedKeyUsages: string[] = []; if (templateData.keyUsages?.requiredUsages?.all) { allAllowedKeyUsages.push(...templateData.keyUsages.requiredUsages.all); } if (templateData.keyUsages?.optionalUsages?.all) { allAllowedKeyUsages.push(...templateData.keyUsages.optionalUsages.all); } setAllowedKeyUsages([...new Set(allAllowedKeyUsages)]); const allAllowedExtendedKeyUsages: string[] = []; if (templateData.extendedKeyUsages?.requiredUsages?.all) { allAllowedExtendedKeyUsages.push(...templateData.extendedKeyUsages.requiredUsages.all); } if (templateData.extendedKeyUsages?.optionalUsages?.all) { allAllowedExtendedKeyUsages.push(...templateData.extendedKeyUsages.optionalUsages.all); } setAllowedExtendedKeyUsages([...new Set(allAllowedExtendedKeyUsages)]); if (templateData.attributes && Array.isArray(templateData.attributes)) { const subjectAttrs: Array<{ type: "common_name"; value: string; }> = []; templateData.attributes.forEach((attr) => { if ( (attr.include === "mandatory" || attr.include === "optional" || attr.include === "prohibit") && attr.value && attr.value.length > 0 ) { attr.value.forEach((val: string) => { subjectAttrs.push({ type: attr.type as any, value: val }); }); } }); if (subjectAttrs.length > 0) { setValue("subjectAttributes", subjectAttrs); } } if ( templateData.subjectAlternativeNames && Array.isArray(templateData.subjectAlternativeNames) ) { const templateSans: Array<{ type: "dns" | "ip" | "email" | "uri"; value: string }> = []; templateData.subjectAlternativeNames.forEach((sanPolicy) => { if ( (sanPolicy.include === "mandatory" || sanPolicy.include === "optional" || sanPolicy.include === "prohibit") && sanPolicy.value && sanPolicy.value.length > 0 ) { const typeMapping: Record = { dns_name: "dns", ip_address: "ip", email: "email", uri: "uri" }; const mappedType = typeMapping[sanPolicy.type]; if (mappedType) { sanPolicy.value.forEach((val: string) => { templateSans.push({ type: mappedType, value: val }); }); } } }); if (templateSans.length > 0) { setValue("altNames", templateSans); } } const resetKeyUsages = { [CertKeyUsage.DIGITAL_SIGNATURE]: false, [CertKeyUsage.KEY_ENCIPHERMENT]: false, [CertKeyUsage.NON_REPUDIATION]: false, [CertKeyUsage.DATA_ENCIPHERMENT]: false, [CertKeyUsage.KEY_AGREEMENT]: false, [CertKeyUsage.KEY_CERT_SIGN]: false, [CertKeyUsage.CRL_SIGN]: false, [CertKeyUsage.ENCIPHER_ONLY]: false, [CertKeyUsage.DECIPHER_ONLY]: false }; const resetExtendedKeyUsages = { [CertExtendedKeyUsage.CLIENT_AUTH]: false, [CertExtendedKeyUsage.CODE_SIGNING]: false, [CertExtendedKeyUsage.EMAIL_PROTECTION]: false, [CertExtendedKeyUsage.OCSP_SIGNING]: false, [CertExtendedKeyUsage.SERVER_AUTH]: false, [CertExtendedKeyUsage.TIMESTAMPING]: false }; const templateToEnumMap = { digital_signature: CertKeyUsage.DIGITAL_SIGNATURE, digitalSignature: CertKeyUsage.DIGITAL_SIGNATURE, key_encipherment: CertKeyUsage.KEY_ENCIPHERMENT, keyEncipherment: CertKeyUsage.KEY_ENCIPHERMENT, non_repudiation: CertKeyUsage.NON_REPUDIATION, nonRepudiation: CertKeyUsage.NON_REPUDIATION, data_encipherment: CertKeyUsage.DATA_ENCIPHERMENT, dataEncipherment: CertKeyUsage.DATA_ENCIPHERMENT, key_agreement: CertKeyUsage.KEY_AGREEMENT, keyAgreement: CertKeyUsage.KEY_AGREEMENT, key_cert_sign: CertKeyUsage.KEY_CERT_SIGN, keyCertSign: CertKeyUsage.KEY_CERT_SIGN, crl_sign: CertKeyUsage.CRL_SIGN, cRLSign: CertKeyUsage.CRL_SIGN, encipher_only: CertKeyUsage.ENCIPHER_ONLY, encipherOnly: CertKeyUsage.ENCIPHER_ONLY, decipher_only: CertKeyUsage.DECIPHER_ONLY, decipherOnly: CertKeyUsage.DECIPHER_ONLY, client_auth: CertExtendedKeyUsage.CLIENT_AUTH, clientAuth: CertExtendedKeyUsage.CLIENT_AUTH, server_auth: CertExtendedKeyUsage.SERVER_AUTH, serverAuth: CertExtendedKeyUsage.SERVER_AUTH, code_signing: CertExtendedKeyUsage.CODE_SIGNING, codeSigning: CertExtendedKeyUsage.CODE_SIGNING, email_protection: CertExtendedKeyUsage.EMAIL_PROTECTION, emailProtection: CertExtendedKeyUsage.EMAIL_PROTECTION, ocsp_signing: CertExtendedKeyUsage.OCSP_SIGNING, ocspSigning: CertExtendedKeyUsage.OCSP_SIGNING, time_stamping: CertExtendedKeyUsage.TIMESTAMPING, timestamping: CertExtendedKeyUsage.TIMESTAMPING, timeStamping: CertExtendedKeyUsage.TIMESTAMPING }; const currentKeyUsages = { ...resetKeyUsages }; if (templateData.keyUsages?.requiredUsages?.all) { templateData.keyUsages.requiredUsages.all.forEach((usage: string) => { const enumValue = (templateToEnumMap as any)[usage]; if (enumValue && enumValue in currentKeyUsages) { (currentKeyUsages as any)[enumValue] = true; } }); } const currentExtendedKeyUsages = { ...resetExtendedKeyUsages }; if (templateData.extendedKeyUsages?.requiredUsages?.all) { templateData.extendedKeyUsages.requiredUsages.all.forEach((usage: string) => { const enumValue = (templateToEnumMap as any)[usage]; if (enumValue && enumValue in currentExtendedKeyUsages) { (currentExtendedKeyUsages as any)[enumValue] = true; } }); } setValue("keyUsages", currentKeyUsages); setValue("extendedKeyUsages", currentExtendedKeyUsages); } }, [templateData, selectedProfile, setValue]); useEffect(() => { if (cert) { const subjectAttrs: Array<{ type: string; value: string }> = []; if (cert.commonName) subjectAttrs.push({ type: "common_name", value: cert.commonName }); reset({ profileId: "", friendlyName: cert.friendlyName, subjectAttributes: subjectAttrs.length > 0 ? (subjectAttrs as any) : [{ type: "common_name" as const, value: "" }], altNames: cert.altNames ? cert.altNames.split(",").map((name) => { const trimmed = name.trim(); if (trimmed.includes("@")) return { type: "email" as const, value: trimmed }; if (trimmed.match(/^\d+\.\d+\.\d+\.\d+$/)) return { type: "ip" as const, value: trimmed }; if (trimmed.startsWith("http")) return { type: "uri" as const, value: trimmed }; return { type: "dns" as const, value: trimmed }; }) : [], ttl: "", keyUsages: Object.fromEntries((cert.keyUsages || []).map((name) => [name, true])), extendedKeyUsages: Object.fromEntries( (cert.extendedKeyUsages || []).map((name) => [name, true]) ) }); } }, [cert, reset]); const onFormSubmit = async ({ profileId, friendlyName, subjectAttributes, altNames, ttl, signatureAlgorithm, keyAlgorithm, keyUsages, extendedKeyUsages }: FormData) => { try { if (!currentProject?.slug) return; const getAttributeValue = (type: string) => { const foundAttr = subjectAttributes.find((attr) => attr.type === type); return foundAttr?.value || ""; }; const { serialNumber, certificate, certificateChain, privateKey } = await createCertificate({ profileId, projectSlug: currentProject.slug, friendlyName, commonName: getAttributeValue("common_name"), altNames: altNames .filter((san) => san.value.trim()) .map((san) => san.value.trim()) .join(", "), ttl, signatureAlgorithm: (() => { const frontendToBackendSigAlg: Record = { "RSA-SHA256": "SHA256-RSA", "RSA-SHA384": "SHA384-RSA", "RSA-SHA512": "SHA512-RSA", "ECDSA-SHA256": "SHA256-ECDSA", "ECDSA-SHA384": "SHA384-ECDSA", "ECDSA-SHA512": "SHA512-ECDSA" }; return signatureAlgorithm ? frontendToBackendSigAlg[signatureAlgorithm] || signatureAlgorithm : undefined; })(), keyAlgorithm: (() => { const frontendToBackendKeyAlg: Record = { RSA_2048: "RSA-2048", RSA_3072: "RSA-3072", RSA_4096: "RSA-4096", EC_prime256v1: "ECDSA-P256", EC_secp384r1: "ECDSA-P384" }; return keyAlgorithm ? frontendToBackendKeyAlg[keyAlgorithm] || keyAlgorithm : undefined; })(), keyUsages: Object.entries(keyUsages) .filter(([, value]) => value) .map(([key]) => key as CertKeyUsage), extendedKeyUsages: Object.entries(extendedKeyUsages) .filter(([, value]) => value) .map(([key]) => key as CertExtendedKeyUsage) }); reset(); setCertificateDetails({ serialNumber, certificate, certificateChain, privateKey }); createNotification({ text: "Successfully created certificate", type: "success" }); } catch (err) { console.error(err); createNotification({ text: "Failed to create certificate", type: "error" }); } }; return ( { handlePopUpToggle("certificateIssuance", isOpen); setCertificateDetails(null); reset(); }} > {certificateDetails && ( )} {cert && (

Certificate Details

Serial Number: {cert.serialNumber}

Common Name: {cert.commonName}

Status: {cert.status}

)} {!cert && !certificateDetails && (
( Certificate profiles define the policies and enrollment methods for certificate issuance. The selected profile will enforce validation rules and determine the CA used for signing. } > } /> } errorText={error?.message} isError={Boolean(error)} isRequired > )} /> ( )} /> (
{value.map((attr, index) => (
{ const newValue = [...value]; newValue[index] = { ...attr, value: e.target.value }; onChange(newValue); }} placeholder="example.com" className="flex-1" /> {value.length > 1 && ( { const newValue = value.filter((_, i) => i !== index); onChange(newValue); }} > )}
))}
)} /> (
{value.map((san, index) => (
{ const newValue = [...value]; newValue[index] = { ...san, value: e.target.value }; onChange(newValue); }} placeholder={ san.type === "dns" ? "example.com or *.example.com" : san.type === "ip" ? "192.168.1.1" : san.type === "email" ? "admin@example.com" : "https://example.com" } className="flex-1" /> { const newValue = value.filter((_, i) => i !== index); onChange(newValue); }} >
))}
)} /> ( )} />
( )} />
( )} />
Key Usages
{KEY_USAGES_OPTIONS.filter(({ value }) => { if (allowedKeyUsages.length === 0) return true; const templateToEnumMap = { digital_signature: CertKeyUsage.DIGITAL_SIGNATURE, key_encipherment: CertKeyUsage.KEY_ENCIPHERMENT, non_repudiation: CertKeyUsage.NON_REPUDIATION, data_encipherment: CertKeyUsage.DATA_ENCIPHERMENT, key_agreement: CertKeyUsage.KEY_AGREEMENT, key_cert_sign: CertKeyUsage.KEY_CERT_SIGN, crl_sign: CertKeyUsage.CRL_SIGN, encipher_only: CertKeyUsage.ENCIPHER_ONLY, decipher_only: CertKeyUsage.DECIPHER_ONLY }; return allowedKeyUsages.some( (allowedUsage) => (templateToEnumMap as any)[allowedUsage] === value ); }).map(({ label, value }) => ( (
field.onChange(checked)} />
)} /> ))}
Extended Key Usages
{EXTENDED_KEY_USAGES_OPTIONS.filter(({ value }) => { if (allowedExtendedKeyUsages.length === 0) return true; const templateToEnumMap = { client_auth: CertExtendedKeyUsage.CLIENT_AUTH, server_auth: CertExtendedKeyUsage.SERVER_AUTH, code_signing: CertExtendedKeyUsage.CODE_SIGNING, email_protection: CertExtendedKeyUsage.EMAIL_PROTECTION, ocsp_signing: CertExtendedKeyUsage.OCSP_SIGNING, time_stamping: CertExtendedKeyUsage.TIMESTAMPING, timestamping: CertExtendedKeyUsage.TIMESTAMPING }; return allowedExtendedKeyUsages.some( (allowedUsage) => (templateToEnumMap as any)[allowedUsage] === value ); }).map(({ label, value }) => ( (
field.onChange(checked)} />
)} /> ))}
)}
); };