mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-05 19:26:38 +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 { 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[] => {
|
||||||
|
|||||||
+180
-103
@@ -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>
|
||||||
);
|
);
|
||||||
|
|||||||
Reference in New Issue
Block a user