mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-09-22 13:39:35 +00:00
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:
@@ -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 = <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 { 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<CertificateExportFormat>("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<typeof exportFormSchema>;
|
||||
|
||||
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<ExportFormData>({
|
||||
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
|
||||
}}
|
||||
>
|
||||
<ModalContent title="Export Certificate">
|
||||
<div className="space-y-4">
|
||||
<p className="text-sm text-gray-400">Choose the format for exporting your certificate</p>
|
||||
<form onSubmit={handleSubmit(onFormSubmit)}>
|
||||
<div className="space-y-4">
|
||||
<p className="text-sm text-gray-400">
|
||||
Choose the format for exporting your certificate
|
||||
</p>
|
||||
|
||||
<FormControl
|
||||
label="Export Format"
|
||||
helperText={
|
||||
selectedFormat === "pem"
|
||||
? "Privacy Enhanced Mail - Text-based certificate format"
|
||||
: "PKCS12 format - Binary keystore format compatible with Java applications"
|
||||
}
|
||||
>
|
||||
<Select
|
||||
className="w-full"
|
||||
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 }))
|
||||
<Controller
|
||||
control={control}
|
||||
name="format"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Export Format"
|
||||
helperText={
|
||||
field.value === "pem"
|
||||
? "Privacy Enhanced Mail - Text-based certificate format"
|
||||
: "PKCS12 format - Binary keystore format compatible with Java applications"
|
||||
}
|
||||
type="password"
|
||||
/>
|
||||
</FormControl>
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
>
|
||||
<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
|
||||
label="Certificate Alias"
|
||||
helperText="Friendly name for the certificate in the keystore"
|
||||
{selectedFormat === "pkcs12" && (
|
||||
<>
|
||||
<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
|
||||
placeholder="Enter certificate alias"
|
||||
value={pkcs12Options.alias}
|
||||
onChange={(e) => setPkcs12Options((prev) => ({ ...prev, alias: e.target.value }))}
|
||||
/>
|
||||
</FormControl>
|
||||
</>
|
||||
)}
|
||||
|
||||
<div className="flex justify-end space-x-2 pt-4">
|
||||
<Button
|
||||
variant="outline_bg"
|
||||
onClick={() => handlePopUpToggle("certificateExport", false)}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
colorSchema="primary"
|
||||
leftIcon={<FontAwesomeIcon icon={faDownload} />}
|
||||
onClick={handleExport}
|
||||
disabled={!(certificateId || serialNumber) || !isFormValid()}
|
||||
>
|
||||
Export {selectedFormat.toUpperCase()}
|
||||
</Button>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
type="submit"
|
||||
colorSchema="primary"
|
||||
leftIcon={<FontAwesomeIcon icon={faDownload} />}
|
||||
disabled={!(certificateId || serialNumber)}
|
||||
isLoading={isSubmitting}
|
||||
>
|
||||
Export {selectedFormat.toUpperCase()}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user