Merge pull request #4992 from Infisical/fix/pki-renewals

fix: renewals for internal CAs and minor improvement on the export certificate modal
This commit is contained in:
carlosmonastyrski
2025-12-05 15:03:24 -03:00
committed by GitHub
2 changed files with 220 additions and 132 deletions
@@ -19,8 +19,10 @@ import { TCertificateBodyDALFactory } from "@app/services/certificate/certificat
import { TCertificateDALFactory } from "@app/services/certificate/certificate-dal"; import { TCertificateDALFactory } from "@app/services/certificate/certificate-dal";
import { TCertificateSecretDALFactory } from "@app/services/certificate/certificate-secret-dal"; import { TCertificateSecretDALFactory } from "@app/services/certificate/certificate-secret-dal";
import { import {
CertExtendedKeyUsage,
CertKeyAlgorithm, CertKeyAlgorithm,
CertKeyType, CertKeyType,
CertKeyUsage,
CertSignatureAlgorithm, CertSignatureAlgorithm,
CertStatus CertStatus
} from "@app/services/certificate/certificate-types"; } from "@app/services/certificate/certificate-types";
@@ -46,7 +48,9 @@ import { getProjectKmsCertificateKeyId } from "@app/services/project/project-fns
import { import {
CertExtendedKeyUsageType, CertExtendedKeyUsageType,
CertKeyUsageType, CertKeyUsageType,
CertSubjectAlternativeNameType CertSubjectAlternativeNameType,
mapLegacyExtendedKeyUsageToStandard,
mapLegacyKeyUsageToStandard
} from "../certificate-common/certificate-constants"; } from "../certificate-common/certificate-constants";
import { import {
extractAlgorithmsFromCSR, extractAlgorithmsFromCSR,
@@ -309,47 +313,54 @@ const extractCertificateFromBuffer = (certData: Buffer | { rawData: Buffer } | s
const parseKeyUsages = (keyUsages: unknown): CertKeyUsageType[] => { const parseKeyUsages = (keyUsages: unknown): CertKeyUsageType[] => {
if (!keyUsages) return []; 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)) { if (Array.isArray(keyUsages)) {
return keyUsages.filter( raw = keyUsages.filter((u): u is string => typeof u === "string");
(usage): usage is CertKeyUsageType => } else if (typeof keyUsages === "string") {
typeof usage === "string" && validKeyUsages.includes(usage as CertKeyUsageType) raw = keyUsages.split(",").map((u) => u.trim());
); } else {
return [];
} }
if (typeof keyUsages === "string") { return raw.map((u) => normalize(u)).filter((u): u is CertKeyUsageType => u !== null);
return keyUsages
.split(",")
.map((usage) => usage.trim())
.filter((usage): usage is CertKeyUsageType => validKeyUsages.includes(usage as CertKeyUsageType));
}
return [];
}; };
const parseExtendedKeyUsages = (extendedKeyUsages: unknown): CertExtendedKeyUsageType[] => { const parseExtendedKeyUsages = (extendedKeyUsages: unknown): CertExtendedKeyUsageType[] => {
if (!extendedKeyUsages) return []; 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)) { if (Array.isArray(extendedKeyUsages)) {
return extendedKeyUsages.filter( raw = extendedKeyUsages.filter((u): u is string => typeof u === "string");
(usage): usage is CertExtendedKeyUsageType => } else if (typeof extendedKeyUsages === "string") {
typeof usage === "string" && validExtendedKeyUsages.includes(usage as CertExtendedKeyUsageType) raw = extendedKeyUsages.split(",").map((u) => u.trim());
); } else {
return [];
} }
if (typeof extendedKeyUsages === "string") { return raw.map((u) => normalize(u)).filter((u): u is CertExtendedKeyUsageType => u !== null);
return extendedKeyUsages
.split(",")
.map((usage) => usage.trim())
.filter((usage): usage is CertExtendedKeyUsageType =>
validExtendedKeyUsages.includes(usage as CertExtendedKeyUsageType)
);
}
return [];
}; };
const convertEnumsToStringArray = <T extends string>(enumArray: T[]): string[] => { const convertEnumsToStringArray = <T extends string>(enumArray: T[]): string[] => {
@@ -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 { faDownload } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { zodResolver } from "@hookform/resolvers/zod";
import { z } from "zod";
import { import {
Button, Button,
@@ -41,55 +44,119 @@ export type ExportOptions = {
}; };
}; };
export const CertificateExportModal = ({ popUp, handlePopUpToggle, onFormatSelected }: Props) => { const exportFormSchema = z
const [selectedFormat, setSelectedFormat] = useState<CertificateExportFormat>("pem"); .object({
const [pkcs12Options, setPkcs12Options] = useState({ format: z.enum(["pem", "pkcs12"]),
password: "", pkcs12Password: z.string().optional(),
alias: "" 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<typeof exportFormSchema>;
export const CertificateExportModal = ({ popUp, handlePopUpToggle, onFormatSelected }: Props) => {
const { certificateId, serialNumber } = const { certificateId, serialNumber } =
(popUp?.certificateExport?.data as { (popUp?.certificateExport?.data as {
certificateId: string; certificateId: string;
serialNumber: string; serialNumber: string;
}) || {}; }) || {};
const {
control,
handleSubmit,
reset,
watch,
formState: { isSubmitting }
} = useForm<ExportFormData>({
resolver: zodResolver(exportFormSchema),
defaultValues: {
format: "pem",
pkcs12Password: "",
pkcs12Alias: ""
}
});
const selectedFormat = watch("format");
// Reset form whenever the modal opens // Reset form whenever the modal opens
useEffect(() => { useEffect(() => {
if (popUp?.certificateExport?.isOpen) { if (popUp?.certificateExport?.isOpen) {
setSelectedFormat("pem"); reset({
setPkcs12Options({ format: "pem",
password: "", pkcs12Password: "",
alias: "" pkcs12Alias: ""
}); });
} }
}, [popUp?.certificateExport?.isOpen]); }, [popUp?.certificateExport?.isOpen, reset]);
const isFormValid = () => { const onFormSubmit = (data: ExportFormData) => {
if (selectedFormat === "pkcs12") { if (!(certificateId || serialNumber)) return;
return pkcs12Options.password.length >= 6 && pkcs12Options.alias.trim() !== "";
const options: ExportOptions = {};
if (data.format === "pkcs12") {
options.pkcs12 = {
password: data.pkcs12Password!,
alias: data.pkcs12Alias!
};
} }
return true;
};
const handleExport = () => { onFormatSelected(
if ((certificateId || serialNumber) && isFormValid()) { data.format,
const options: ExportOptions = {}; {
certificateId,
if (selectedFormat === "pkcs12") { serialNumber
options.pkcs12 = pkcs12Options; },
} options
);
onFormatSelected( handlePopUpToggle("certificateExport", false);
selectedFormat,
{
certificateId,
serialNumber
},
options
);
handlePopUpToggle("certificateExport", false);
}
}; };
return ( return (
@@ -100,79 +167,89 @@ export const CertificateExportModal = ({ popUp, handlePopUpToggle, onFormatSelec
}} }}
> >
<ModalContent title="Export Certificate"> <ModalContent title="Export Certificate">
<div className="space-y-4"> <form onSubmit={handleSubmit(onFormSubmit)}>
<p className="text-sm text-gray-400">Choose the format for exporting your certificate</p> <div className="space-y-4">
<p className="text-sm text-gray-400">
Choose the format for exporting your certificate
</p>
<FormControl <Controller
label="Export Format" control={control}
helperText={ name="format"
selectedFormat === "pem" render={({ field, fieldState: { error } }) => (
? "Privacy Enhanced Mail - Text-based certificate format" <FormControl
: "PKCS12 format - Binary keystore format compatible with Java applications" label="Export Format"
} helperText={
> field.value === "pem"
<Select ? "Privacy Enhanced Mail - Text-based certificate format"
className="w-full" : "PKCS12 format - Binary keystore format compatible with Java applications"
value={selectedFormat}
onValueChange={(value) => setSelectedFormat(value as CertificateExportFormat)}
>
<SelectItem value="pem">PEM Format</SelectItem>
<SelectItem value="pkcs12">PKCS12 Format</SelectItem>
</Select>
</FormControl>
{selectedFormat === "pkcs12" && (
<>
<FormControl
label="Keystore Password"
helperText={
pkcs12Options.password.length > 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"
>
<Input
placeholder="Enter keystore password"
value={pkcs12Options.password}
onChange={(e) =>
setPkcs12Options((prev) => ({ ...prev, password: e.target.value }))
} }
type="password" isError={Boolean(error)}
/> errorText={error?.message}
</FormControl> >
<Select className="w-full" value={field.value} onValueChange={field.onChange}>
<SelectItem value="pem">PEM Format</SelectItem>
<SelectItem value="pkcs12">PKCS12 Format</SelectItem>
</Select>
</FormControl>
)}
/>
<FormControl {selectedFormat === "pkcs12" && (
label="Certificate Alias" <>
helperText="Friendly name for the certificate in the keystore" <Controller
control={control}
name="pkcs12Password"
render={({ field, fieldState: { error } }) => (
<FormControl
label="Keystore Password"
helperText="Password to protect the PKCS12 keystore (minimum 6 characters)"
isError={Boolean(error)}
errorText={error?.message}
isRequired
>
<Input {...field} placeholder="Enter keystore password" type="password" />
</FormControl>
)}
/>
<Controller
control={control}
name="pkcs12Alias"
render={({ field, fieldState: { error } }) => (
<FormControl
label="Certificate Alias"
helperText="Friendly name for the certificate in the keystore"
isError={Boolean(error)}
errorText={error?.message}
isRequired
>
<Input {...field} placeholder="Enter certificate alias" />
</FormControl>
)}
/>
</>
)}
<div className="flex justify-end space-x-2 pt-4">
<Button
variant="outline_bg"
onClick={() => handlePopUpToggle("certificateExport", false)}
> >
<Input Cancel
placeholder="Enter certificate alias" </Button>
value={pkcs12Options.alias} <Button
onChange={(e) => setPkcs12Options((prev) => ({ ...prev, alias: e.target.value }))} type="submit"
/> colorSchema="primary"
</FormControl> leftIcon={<FontAwesomeIcon icon={faDownload} />}
</> disabled={!(certificateId || serialNumber)}
)} isLoading={isSubmitting}
>
<div className="flex justify-end space-x-2 pt-4"> Export {selectedFormat.toUpperCase()}
<Button </Button>
variant="outline_bg" </div>
onClick={() => handlePopUpToggle("certificateExport", false)}
>
Cancel
</Button>
<Button
colorSchema="primary"
leftIcon={<FontAwesomeIcon icon={faDownload} />}
onClick={handleExport}
disabled={!(certificateId || serialNumber) || !isFormValid()}
>
Export {selectedFormat.toUpperCase()}
</Button>
</div> </div>
</div> </form>
</ModalContent> </ModalContent>
</Modal> </Modal>
); );