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 {
}
if (typeof keyUsages === "string") {
return keyUsages
.split(",")
.map((usage) => usage.trim())
.filter((usage): usage is CertKeyUsageType => validKeyUsages.includes(usage as CertKeyUsageType));
}
return []; return [];
}
return raw.map((u) => normalize(u)).filter((u): u is CertKeyUsageType => u !== null);
}; };
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 {
}
if (typeof extendedKeyUsages === "string") {
return extendedKeyUsages
.split(",")
.map((usage) => usage.trim())
.filter((usage): usage is CertExtendedKeyUsageType =>
validExtendedKeyUsages.includes(usage as CertExtendedKeyUsageType)
);
}
return []; return [];
}
return raw.map((u) => normalize(u)).filter((u): u is CertExtendedKeyUsageType => u !== null);
}; };
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,47 +44,112 @@ 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() !== "";
}
return true;
};
const handleExport = () => {
if ((certificateId || serialNumber) && isFormValid()) {
const options: ExportOptions = {}; const options: ExportOptions = {};
if (selectedFormat === "pkcs12") { if (data.format === "pkcs12") {
options.pkcs12 = pkcs12Options; options.pkcs12 = {
password: data.pkcs12Password!,
alias: data.pkcs12Alias!
};
} }
onFormatSelected( onFormatSelected(
selectedFormat, data.format,
{ {
certificateId, certificateId,
serialNumber serialNumber
@@ -89,7 +157,6 @@ export const CertificateExportModal = ({ popUp, handlePopUpToggle, onFormatSelec
options options
); );
handlePopUpToggle("certificateExport", false); handlePopUpToggle("certificateExport", false);
}
}; };
return ( return (
@@ -100,59 +167,67 @@ export const CertificateExportModal = ({ popUp, handlePopUpToggle, onFormatSelec
}} }}
> >
<ModalContent title="Export Certificate"> <ModalContent title="Export Certificate">
<form onSubmit={handleSubmit(onFormSubmit)}>
<div className="space-y-4"> <div className="space-y-4">
<p className="text-sm text-gray-400">Choose the format for exporting your certificate</p> <p className="text-sm text-gray-400">
Choose the format for exporting your certificate
</p>
<Controller
control={control}
name="format"
render={({ field, fieldState: { error } }) => (
<FormControl <FormControl
label="Export Format" label="Export Format"
helperText={ helperText={
selectedFormat === "pem" field.value === "pem"
? "Privacy Enhanced Mail - Text-based certificate format" ? "Privacy Enhanced Mail - Text-based certificate format"
: "PKCS12 format - Binary keystore format compatible with Java applications" : "PKCS12 format - Binary keystore format compatible with Java applications"
} }
isError={Boolean(error)}
errorText={error?.message}
> >
<Select <Select className="w-full" value={field.value} onValueChange={field.onChange}>
className="w-full"
value={selectedFormat}
onValueChange={(value) => setSelectedFormat(value as CertificateExportFormat)}
>
<SelectItem value="pem">PEM Format</SelectItem> <SelectItem value="pem">PEM Format</SelectItem>
<SelectItem value="pkcs12">PKCS12 Format</SelectItem> <SelectItem value="pkcs12">PKCS12 Format</SelectItem>
</Select> </Select>
</FormControl> </FormControl>
)}
/>
{selectedFormat === "pkcs12" && ( {selectedFormat === "pkcs12" && (
<> <>
<Controller
control={control}
name="pkcs12Password"
render={({ field, fieldState: { error } }) => (
<FormControl <FormControl
label="Keystore Password" label="Keystore Password"
helperText={ helperText="Password to protect the PKCS12 keystore (minimum 6 characters)"
pkcs12Options.password.length > 0 && pkcs12Options.password.length < 6 isError={Boolean(error)}
? undefined errorText={error?.message}
: "Password to protect the PKCS12 keystore (minimum 6 characters)" isRequired
}
isError={pkcs12Options.password.length > 0 && pkcs12Options.password.length < 6}
errorText="Password must be at least 6 characters long"
> >
<Input <Input {...field} placeholder="Enter keystore password" type="password" />
placeholder="Enter keystore password"
value={pkcs12Options.password}
onChange={(e) =>
setPkcs12Options((prev) => ({ ...prev, password: e.target.value }))
}
type="password"
/>
</FormControl> </FormControl>
)}
/>
<Controller
control={control}
name="pkcs12Alias"
render={({ field, fieldState: { error } }) => (
<FormControl <FormControl
label="Certificate Alias" label="Certificate Alias"
helperText="Friendly name for the certificate in the keystore" helperText="Friendly name for the certificate in the keystore"
isError={Boolean(error)}
errorText={error?.message}
isRequired
> >
<Input <Input {...field} placeholder="Enter certificate alias" />
placeholder="Enter certificate alias"
value={pkcs12Options.alias}
onChange={(e) => setPkcs12Options((prev) => ({ ...prev, alias: e.target.value }))}
/>
</FormControl> </FormControl>
)}
/>
</> </>
)} )}
@@ -164,15 +239,17 @@ export const CertificateExportModal = ({ popUp, handlePopUpToggle, onFormatSelec
Cancel Cancel
</Button> </Button>
<Button <Button
type="submit"
colorSchema="primary" colorSchema="primary"
leftIcon={<FontAwesomeIcon icon={faDownload} />} leftIcon={<FontAwesomeIcon icon={faDownload} />}
onClick={handleExport} disabled={!(certificateId || serialNumber)}
disabled={!(certificateId || serialNumber) || !isFormValid()} isLoading={isSubmitting}
> >
Export {selectedFormat.toUpperCase()} Export {selectedFormat.toUpperCase()}
</Button> </Button>
</div> </div>
</div> </div>
</form>
</ModalContent> </ModalContent>
</Modal> </Modal>
); );