mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-09-22 13:39:35 +00:00
Merge pull request #4846 from Infisical/PKI-9
PKI: add support to export certs in PKCS12 format
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
export { CertStatus } from "./enums";
|
||||
export {
|
||||
useDeleteCert,
|
||||
useDownloadCertPkcs12,
|
||||
useImportCertificate,
|
||||
useRenewCertificate,
|
||||
useRevokeCert,
|
||||
|
||||
@@ -7,6 +7,7 @@ import { projectKeys } from "../projects";
|
||||
import {
|
||||
TCertificate,
|
||||
TDeleteCertDTO,
|
||||
TDownloadPkcs12DTO,
|
||||
TImportCertificateDTO,
|
||||
TImportCertificateResponse,
|
||||
TRenewCertificateDTO,
|
||||
@@ -134,3 +135,42 @@ export const useUpdateRenewalConfig = () => {
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
export const useDownloadCertPkcs12 = () => {
|
||||
return useMutation<void, object, TDownloadPkcs12DTO>({
|
||||
mutationFn: async ({ serialNumber, projectSlug, password, alias }) => {
|
||||
try {
|
||||
const response = await apiRequest.post(
|
||||
`/api/v1/pki/certificates/${serialNumber}/pkcs12`,
|
||||
{
|
||||
password,
|
||||
alias
|
||||
},
|
||||
{
|
||||
params: { projectSlug },
|
||||
responseType: "arraybuffer"
|
||||
}
|
||||
);
|
||||
|
||||
// Create blob and trigger download
|
||||
const blob = new Blob([response.data], { type: "application/octet-stream" });
|
||||
const url = window.URL.createObjectURL(blob);
|
||||
const link = document.createElement("a");
|
||||
link.href = url;
|
||||
link.download = `certificate-${serialNumber}.p12`;
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
document.body.removeChild(link);
|
||||
window.URL.revokeObjectURL(url);
|
||||
} catch (error: any) {
|
||||
if (error.response?.data instanceof ArrayBuffer) {
|
||||
const decoder = new TextDecoder();
|
||||
const errorText = decoder.decode(error.response.data);
|
||||
const errorData = JSON.parse(errorText);
|
||||
throw new Error(errorData.message);
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
@@ -72,3 +72,10 @@ export type TUpdateRenewalConfigDTO = {
|
||||
enableAutoRenewal?: boolean;
|
||||
projectSlug: string;
|
||||
};
|
||||
|
||||
export type TDownloadPkcs12DTO = {
|
||||
serialNumber: string;
|
||||
projectSlug: string;
|
||||
password: string;
|
||||
alias: string;
|
||||
};
|
||||
|
||||
@@ -0,0 +1,163 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { faDownload } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
|
||||
import {
|
||||
Button,
|
||||
FormControl,
|
||||
Input,
|
||||
Modal,
|
||||
ModalContent,
|
||||
Select,
|
||||
SelectItem
|
||||
} from "@app/components/v2";
|
||||
import { UsePopUpState } from "@app/hooks/usePopUp";
|
||||
|
||||
type Props = {
|
||||
popUp: UsePopUpState<["certificateExport"]>;
|
||||
handlePopUpToggle: (
|
||||
popUpName: keyof UsePopUpState<["certificateExport"]>,
|
||||
state?: boolean
|
||||
) => void;
|
||||
onFormatSelected: (
|
||||
format: "pem" | "pkcs12",
|
||||
serialNumber: string,
|
||||
options?: ExportOptions
|
||||
) => void;
|
||||
};
|
||||
|
||||
export type CertificateExportFormat = "pem" | "pkcs12";
|
||||
|
||||
export type ExportOptions = {
|
||||
pkcs12?: {
|
||||
password: string;
|
||||
alias: string;
|
||||
};
|
||||
};
|
||||
|
||||
export const CertificateExportModal = ({ popUp, handlePopUpToggle, onFormatSelected }: Props) => {
|
||||
const [selectedFormat, setSelectedFormat] = useState<CertificateExportFormat>("pem");
|
||||
const [pkcs12Options, setPkcs12Options] = useState({
|
||||
password: "",
|
||||
alias: ""
|
||||
});
|
||||
|
||||
const serialNumber =
|
||||
(popUp?.certificateExport?.data as { serialNumber: string })?.serialNumber || "";
|
||||
|
||||
// Reset form whenever the modal opens
|
||||
useEffect(() => {
|
||||
if (popUp?.certificateExport?.isOpen) {
|
||||
setSelectedFormat("pem");
|
||||
setPkcs12Options({
|
||||
password: "",
|
||||
alias: ""
|
||||
});
|
||||
}
|
||||
}, [popUp?.certificateExport?.isOpen]);
|
||||
|
||||
const isFormValid = () => {
|
||||
if (selectedFormat === "pkcs12") {
|
||||
return pkcs12Options.password.length >= 6 && pkcs12Options.alias.trim() !== "";
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
const handleExport = () => {
|
||||
if (serialNumber && isFormValid()) {
|
||||
const options: ExportOptions = {};
|
||||
|
||||
if (selectedFormat === "pkcs12") {
|
||||
options.pkcs12 = pkcs12Options;
|
||||
}
|
||||
|
||||
onFormatSelected(selectedFormat, serialNumber, options);
|
||||
handlePopUpToggle("certificateExport", false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal
|
||||
isOpen={popUp?.certificateExport?.isOpen}
|
||||
onOpenChange={(isOpen) => {
|
||||
handlePopUpToggle("certificateExport", isOpen);
|
||||
}}
|
||||
>
|
||||
<ModalContent title="Export Certificate">
|
||||
<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 }))
|
||||
}
|
||||
type="password"
|
||||
/>
|
||||
</FormControl>
|
||||
|
||||
<FormControl
|
||||
label="Certificate Alias"
|
||||
helperText="Friendly name for the certificate in the keystore"
|
||||
>
|
||||
<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={!serialNumber || !isFormValid()}
|
||||
>
|
||||
Export {selectedFormat.toUpperCase()}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
@@ -9,10 +9,11 @@ import {
|
||||
ProjectPermissionSub,
|
||||
useProject
|
||||
} from "@app/context";
|
||||
import { useDeleteCert } from "@app/hooks/api";
|
||||
import { useDeleteCert, useDownloadCertPkcs12 } from "@app/hooks/api";
|
||||
import { usePopUp } from "@app/hooks/usePopUp";
|
||||
|
||||
import { CertificateCertModal } from "./CertificateCertModal";
|
||||
import { CertificateExportModal, ExportOptions } from "./CertificateExportModal";
|
||||
import { CertificateImportModal } from "./CertificateImportModal";
|
||||
import { CertificateIssuanceModal } from "./CertificateIssuanceModal";
|
||||
import { CertificateManagePkiSyncsModal } from "./CertificateManagePkiSyncsModal";
|
||||
@@ -24,11 +25,13 @@ import { CertificatesTable } from "./CertificatesTable";
|
||||
export const CertificatesSection = () => {
|
||||
const { currentProject } = useProject();
|
||||
const { mutateAsync: deleteCert } = useDeleteCert();
|
||||
const { mutateAsync: downloadCertPkcs12 } = useDownloadCertPkcs12();
|
||||
|
||||
const { popUp, handlePopUpOpen, handlePopUpClose, handlePopUpToggle } = usePopUp([
|
||||
"issueCertificate",
|
||||
"certificateImport",
|
||||
"certificateCert",
|
||||
"certificateExport",
|
||||
"deleteCertificate",
|
||||
"revokeCertificate",
|
||||
"manageRenewal",
|
||||
@@ -49,6 +52,45 @@ export const CertificatesSection = () => {
|
||||
handlePopUpClose("deleteCertificate");
|
||||
};
|
||||
|
||||
const handleCertificateExport = async (
|
||||
format: "pem" | "pkcs12",
|
||||
serialNumber: string,
|
||||
options?: ExportOptions
|
||||
) => {
|
||||
if (format === "pem") {
|
||||
handlePopUpOpen("certificateCert", { serialNumber });
|
||||
} else if (format === "pkcs12") {
|
||||
if (!currentProject?.slug) return;
|
||||
|
||||
if (!options?.pkcs12?.password || !options?.pkcs12?.alias) {
|
||||
createNotification({
|
||||
text: "Password and alias are required for PKCS12 export",
|
||||
type: "error"
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await downloadCertPkcs12({
|
||||
serialNumber,
|
||||
projectSlug: currentProject.slug,
|
||||
password: options.pkcs12.password,
|
||||
alias: options.pkcs12.alias
|
||||
});
|
||||
|
||||
createNotification({
|
||||
text: "PKCS12 certificate downloaded successfully",
|
||||
type: "success"
|
||||
});
|
||||
} catch (error: any) {
|
||||
createNotification({
|
||||
text: error?.message || "Failed to download PKCS12 certificate",
|
||||
type: "error"
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="mb-6 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
|
||||
<div className="mb-4 flex justify-between">
|
||||
@@ -84,6 +126,11 @@ export const CertificatesSection = () => {
|
||||
<CertificateIssuanceModal popUp={popUp} handlePopUpToggle={handlePopUpToggle} />
|
||||
<CertificateImportModal popUp={popUp} handlePopUpToggle={handlePopUpToggle} />
|
||||
<CertificateCertModal popUp={popUp} handlePopUpToggle={handlePopUpToggle} />
|
||||
<CertificateExportModal
|
||||
popUp={popUp}
|
||||
handlePopUpToggle={handlePopUpToggle}
|
||||
onFormatSelected={handleCertificateExport}
|
||||
/>
|
||||
<CertificateManageRenewalModal popUp={popUp} handlePopUpToggle={handlePopUpToggle} />
|
||||
<CertificateRenewalModal popUp={popUp} handlePopUpToggle={handlePopUpToggle} />
|
||||
<CertificateRevocationModal popUp={popUp} handlePopUpToggle={handlePopUpToggle} />
|
||||
|
||||
@@ -68,6 +68,7 @@ type Props = {
|
||||
"deleteCertificate",
|
||||
"revokeCertificate",
|
||||
"certificateCert",
|
||||
"certificateExport",
|
||||
"manageRenewal",
|
||||
"renewCertificate",
|
||||
"managePkiSyncs"
|
||||
@@ -275,7 +276,7 @@ export const CertificatesTable = ({ handlePopUpOpen }: Props) => {
|
||||
!isAllowed && "pointer-events-none cursor-not-allowed opacity-50"
|
||||
)}
|
||||
onClick={async () =>
|
||||
handlePopUpOpen("certificateCert", {
|
||||
handlePopUpOpen("certificateExport", {
|
||||
serialNumber: certificate.serialNumber
|
||||
})
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user