Merge pull request #4846 from Infisical/PKI-9

PKI: add support to export certs in PKCS12 format
This commit is contained in:
carlosmonastyrski
2025-11-11 13:58:53 -03:00
committed by GitHub
18 changed files with 577 additions and 25 deletions

View File

@@ -58,6 +58,7 @@
"@sindresorhus/slugify": "1.1.0",
"@slack/oauth": "^3.0.2",
"@slack/web-api": "^7.8.0",
"@types/node-forge": "^1.3.14",
"@ucast/mongo2js": "^1.3.4",
"acme-client": "^5.4.0",
"ajv": "^8.12.0",
@@ -97,6 +98,7 @@
"ms": "^2.1.3",
"mysql2": "^3.9.8",
"nanoid": "^3.3.8",
"node-forge": "^1.3.1",
"nodemailer": "^6.9.9",
"oci-sdk": "^2.108.0",
"odbc": "^2.4.9",
@@ -15272,6 +15274,15 @@
"form-data": "^4.0.0"
}
},
"node_modules/@types/node-forge": {
"version": "1.3.14",
"resolved": "https://registry.npmjs.org/@types/node-forge/-/node-forge-1.3.14.tgz",
"integrity": "sha512-mhVF2BnD4BO+jtOp7z1CdzaK4mbuK0LLQYAvdOLqHTavxFNq4zA1EmYkpnFjP8HOUzedfQkRnp0E2ulSAYSzAw==",
"license": "MIT",
"dependencies": {
"@types/node": "*"
}
},
"node_modules/@types/node/node_modules/undici-types": {
"version": "6.21.0",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
@@ -33526,18 +33537,6 @@
"url": "https://opencollective.com/vitest"
}
},
"node_modules/vite-node/node_modules/@types/node": {
"version": "24.9.1",
"resolved": "https://registry.npmjs.org/@types/node/-/node-24.9.1.tgz",
"integrity": "sha512-QoiaXANRkSXK6p0Duvt56W208du4P9Uye9hWLWgGMDTEoKPhuenzNcC4vGUmrNkiOKTlIrBoyNQYNpSwfEZXSg==",
"dev": true,
"license": "MIT",
"optional": true,
"peer": true,
"dependencies": {
"undici-types": "~7.16.0"
}
},
"node_modules/vite-node/node_modules/debug": {
"version": "4.4.3",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
@@ -33569,15 +33568,6 @@
"url": "https://github.com/sponsors/jonschlinkert"
}
},
"node_modules/vite-node/node_modules/undici-types": {
"version": "7.16.0",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz",
"integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==",
"dev": true,
"license": "MIT",
"optional": true,
"peer": true
},
"node_modules/vite-node/node_modules/vite": {
"version": "7.1.12",
"resolved": "https://registry.npmjs.org/vite/-/vite-7.1.12.tgz",

View File

@@ -185,6 +185,7 @@
"@sindresorhus/slugify": "1.1.0",
"@slack/oauth": "^3.0.2",
"@slack/web-api": "^7.8.0",
"@types/node-forge": "^1.3.14",
"@ucast/mongo2js": "^1.3.4",
"acme-client": "^5.4.0",
"ajv": "^8.12.0",
@@ -224,6 +225,7 @@
"ms": "^2.1.3",
"mysql2": "^3.9.8",
"nanoid": "^3.3.8",
"node-forge": "^1.3.1",
"nodemailer": "^6.9.9",
"oci-sdk": "^2.108.0",
"odbc": "^2.4.9",

View File

@@ -323,6 +323,7 @@ export enum EventType {
GET_CERT_BODY = "get-cert-body",
GET_CERT_PRIVATE_KEY = "get-cert-private-key",
GET_CERT_BUNDLE = "get-cert-bundle",
EXPORT_CERT_PKCS12 = "export-cert-pkcs12",
CREATE_PKI_ALERT = "create-pki-alert",
GET_PKI_ALERT = "get-pki-alert",
UPDATE_PKI_ALERT = "update-pki-alert",
@@ -2315,6 +2316,14 @@ interface GetCertBundle {
serialNumber: string;
};
}
interface GetCertPkcs12 {
type: EventType.EXPORT_CERT_PKCS12;
metadata: {
certId: string;
cn: string;
serialNumber: string;
};
}
interface CreatePkiAlert {
type: EventType.CREATE_PKI_ALERT;
@@ -4252,6 +4261,7 @@ export type Event =
| GetCertBody
| GetCertPrivateKey
| GetCertBundle
| GetCertPkcs12
| CreatePkiAlert
| GetPkiAlert
| UpdatePkiAlert

View File

@@ -1,4 +1,5 @@
/* eslint-disable @typescript-eslint/no-floating-promises */
import RE2 from "re2";
import { z } from "zod";
import { CertificatesSchema } from "@app/db/schemas";
@@ -616,4 +617,64 @@ export const registerCertRouter = async (server: FastifyZodProvider) => {
};
}
});
server.route({
method: "POST",
url: "/:serialNumber/pkcs12",
config: {
rateLimit: writeLimit
},
onRequest: verifyAuth([AuthMode.JWT]),
schema: {
hide: true,
tags: [ApiDocsTags.PkiCertificates],
description: "Download certificate in PKCS12 format",
params: z.object({
serialNumber: z.string().trim().describe(CERTIFICATES.GET.serialNumber)
}),
body: z.object({
password: z
.string()
.min(6, "Password must be at least 6 characters long")
.describe("Password for the keystore (minimum 6 characters)"),
alias: z.string().min(1, "Alias is required").describe("Alias for the certificate in the keystore")
}),
response: {
200: z.any().describe("PKCS12 keystore as binary data")
}
},
handler: async (req, reply) => {
const { pkcs12Data, cert } = await server.services.certificate.getCertPkcs12({
serialNumber: req.params.serialNumber,
password: req.body.password,
alias: req.body.alias,
actor: req.permission.type,
actorId: req.permission.id,
actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId
});
await server.services.auditLog.createAuditLog({
...req.auditLogInfo,
projectId: cert.projectId,
event: {
type: EventType.EXPORT_CERT_PKCS12,
metadata: {
certId: cert.id,
cn: cert.commonName,
serialNumber: cert.serialNumber
}
}
});
addNoCacheHeaders(reply);
reply.header("Content-Type", "application/octet-stream");
reply.header(
"Content-Disposition",
`attachment; filename="certificate-${req.params.serialNumber.replace(new RE2("[^\\w.-]", "g"), "_")}.p12"`
);
return pkcs12Data;
}
});
};

View File

@@ -1,4 +1,5 @@
import * as x509 from "@peculiar/x509";
import forge from "node-forge";
import RE2 from "re2";
import { crypto } from "@app/lib/crypto/cryptography";
@@ -104,3 +105,53 @@ export const getCertificateCredentials = async ({
throw new BadRequestError({ message: `Failed to process private key for certificate with ID '${certId}'` });
}
};
export const generatePkcs12FromCertificate = async ({
certificate,
certificateChain,
privateKey,
password,
alias
}: {
certificate: string;
certificateChain: string;
privateKey: string;
password: string;
alias: string;
}): Promise<Buffer> => {
try {
if (!password || password.trim() === "") {
throw new BadRequestError({ message: "Password is required for PKCS12 keystore generation" });
}
const cert = forge.pki.certificateFromPem(certificate);
const key = forge.pki.privateKeyFromPem(privateKey);
const chainCerts = [];
if (certificateChain) {
const chainPems = splitPemChain(certificateChain);
for (const chainPem of chainPems) {
try {
const chainCert = forge.pki.certificateFromPem(chainPem);
chainCerts.push(chainCert);
} catch (error) {
// Skip invalid certificates in chain
}
}
}
// Generate PKCS12 file
const p12Asn1 = forge.pkcs12.toPkcs12Asn1(key, [cert, ...chainCerts], password, {
algorithm: "aes256", // Modern AES-256 encryption
friendlyName: alias
});
const p12Der = forge.asn1.toDer(p12Asn1).getBytes();
return Buffer.from(p12Der, "binary");
} catch (error) {
throw new BadRequestError({
message: `Failed to generate PKCS12 keystore: ${error instanceof Error ? error.message : "Unknown error"}`
});
}
};

View File

@@ -29,7 +29,12 @@ import { TProjectDALFactory } from "@app/services/project/project-dal";
import { getProjectKmsCertificateKeyId } from "@app/services/project/project-fns";
import { expandInternalCa, getCaCertChain, rebuildCaCrl } from "../certificate-authority/certificate-authority-fns";
import { getCertificateCredentials, revocationReasonToCrlCode, splitPemChain } from "./certificate-fns";
import {
generatePkcs12FromCertificate,
getCertificateCredentials,
revocationReasonToCrlCode,
splitPemChain
} from "./certificate-fns";
import { TCertificateSecretDALFactory } from "./certificate-secret-dal";
import {
CertExtendedKeyUsage,
@@ -40,6 +45,7 @@ import {
TGetCertBodyDTO,
TGetCertBundleDTO,
TGetCertDTO,
TGetCertPkcs12DTO,
TGetCertPrivateKeyDTO,
TImportCertDTO,
TRevokeCertDTO
@@ -656,6 +662,71 @@ export const certificateServiceFactory = ({
};
};
const getCertPkcs12 = async ({
serialNumber,
password,
alias,
actorId,
actorAuthMethod,
actor,
actorOrgId
}: TGetCertPkcs12DTO) => {
if (!password || password.trim() === "") {
throw new BadRequestError({ message: "Password is required for PKCS12 keystore generation" });
}
if (password.length < 6) {
throw new BadRequestError({
message: "Password must be at least 6 characters long for PKCS12 keystore security"
});
}
if (!alias || alias.trim() === "") {
throw new BadRequestError({ message: "Alias is required for PKCS12 keystore generation" });
}
const cert = await certificateDAL.findOne({ serialNumber });
const { permission } = await permissionService.getProjectPermission({
actor,
actorId,
projectId: cert.projectId,
actorAuthMethod,
actorOrgId,
actionProjectType: ActionProjectType.CertificateManager
});
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionCertificateActions.ReadPrivateKey,
ProjectPermissionSub.Certificates
);
// Get certificate bundle (certificate, chain, private key)
const { certificate, certificateChain, privateKey } = await getCertBundle({
serialNumber,
actor,
actorId,
actorAuthMethod,
actorOrgId
});
if (!privateKey) {
throw new BadRequestError({ message: "Certificate private key is required for PKCS12 export" });
}
const pkcs12Data = await generatePkcs12FromCertificate({
certificate,
certificateChain: certificateChain || "",
privateKey,
password,
alias
});
return {
pkcs12Data,
cert
};
};
return {
getCert,
getCertPrivateKey,
@@ -663,6 +734,7 @@ export const certificateServiceFactory = ({
revokeCert,
getCertBody,
importCert,
getCertBundle
getCertBundle,
getCertPkcs12
};
};

View File

@@ -119,6 +119,12 @@ export type TGetCertBundleDTO = {
serialNumber: string;
} & Omit<TProjectPermission, "projectId">;
export type TGetCertPkcs12DTO = {
serialNumber: string;
password: string;
alias: string;
} & Omit<TProjectPermission, "projectId">;
export type TGetCertificateCredentialsDTO = {
certId: string;
projectId: string;

View File

@@ -60,6 +60,107 @@ The following examples demonstrate different approaches to certificate renewal:
- Using the ACME enrollment method, you may use [cert-manager](https://cert-manager.io/) with Infisical to issue and renew certificates for Kubernetes workloads; cert-manager will pursue a client-driven approach and submit certificate requests upon certificate expiration for you, saving renewed certificates back to Kubernetes secrets.
- Using the API enrollment method, you may push and auto-renew certificates to AWS and Azure using [certificate syncs](/documentation/platform/pki/certificate-syncs/overview). Certificates issued over the API enrollment method, where key pairs are generated server-side, are also eligible for server-side auto-renewal; once renewed, certificates are automatically pushed back to their sync destination.
## Guide to Exporting Certificates
In the following steps, we explore how to export certificates from Infisical in different formats for use in your applications and infrastructure.
### Accessing the Export Certificate Modal
To export any certificate, first navigate to your project's certificate inventory and locate the certificate you want to export. Click on the **Export Certificate** option from the certificate's action menu.
![pki export certificate option](/images/platform/pki/certificate/cert-export-option.png)
<Tabs>
<Tab title="PEM Format">
<Steps>
<Step title="Exporting in PEM Format">
In the export modal, choose **PEM** as the format and click **Export**.
![pki export certificate pem](/images/platform/pki/certificate/cert-export-pem.png)
The PEM export modal will display the certificate details including:
- **Serial Number**: The unique identifier for the certificate
- **Certificate Body**: The X.509 certificate in PEM format
- **Certificate Chain**: The intermediate and root CA certificates
- **Private Key**: The private key associated with the certificate (if available)
![pki export certificate pem modal](/images/platform/pki/certificate/cert-export-pem-modal.png)
You can copy each component individually or use the **Copy All** button to copy the complete certificate bundle.
</Step>
<Step title="Using PEM Certificates">
PEM format certificates can be used directly with most web servers and applications:
- **Apache HTTP Server**: Configure SSL certificates in your virtual host
- **Nginx**: Use the certificate and private key files in your server configuration
- **Docker containers**: Mount certificate files for TLS-enabled applications
- **Load balancers**: Upload PEM certificates to AWS ALB, Azure Application Gateway, etc.
Example Nginx configuration:
```nginx
server {
listen 443 ssl;
server_name example.com;
ssl_certificate /path/to/certificate.pem;
ssl_certificate_key /path/to/private-key.pem;
}
```
</Step>
</Steps>
</Tab>
<Tab title="PKCS12 Format">
<Steps>
<Step title="Exporting in PKCS12 Format">
In the export modal, choose **PKCS12** as the format and provide the required configuration:
![pki export certificate pkcs12](/images/platform/pki/certificate/cert-export-pkcs12.png)
- **Password**: A secure password to protect the PKCS12 keystore
- **Alias**: A friendly name for the certificate within the keystore
Click **Export** to generate and download the `.p12` file containing the certificate, certificate chain, and private key.
</Step>
<Step title="Using PKCS12 Certificates">
PKCS12 files (`.p12` extension) are binary keystore files that contain the certificate, certificate chain, and private key in a single encrypted file:
- **Java applications**: Import directly into Java KeyStore (JKS) or use with SSL/TLS
- **Windows IIS**: Import the PKCS12 file for web server SSL configuration
- **Browser certificates**: Install client certificates for authentication
- **Mobile applications**: Deploy certificates to iOS and Android applications
To verify the contents of a PKCS12 file:
```bash
openssl pkcs12 -in certificate.p12 -nokeys -clcerts
```
To extract the private key:
```bash
openssl pkcs12 -in certificate.p12 -nocerts -out private-key.pem
```
<Info>
If you need to convert the PKCS12 file to Java KeyStore (JKS) format for applications running on Java 8 or earlier, use the following keytool command:
```bash
keytool -importkeystore \
-srckeystore certificate.p12 \
-srcstoretype PKCS12 \
-srcstorepass <p12-password> \
-destkeystore certificate.jks \
-deststoretype JKS \
-deststorepass <jks-password>
```
Replace `<p12-password>` with the password you used when exporting the PKCS12 file, and `<jks-password>` with your desired JKS keystore password.
The resulting `.jks` file can then be used with Java applications that require JKS format keystores.
</Info>
</Step>
</Steps>
</Tab>
</Tabs>
## Guide to Revoking Certificates
In the following steps, we explore how to revoke a X.509 certificate and obtain a Certificate Revocation List (CRL) for a CA.

Binary file not shown.

After

Width:  |  Height:  |  Size: 305 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 539 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 281 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 324 KiB

View File

@@ -1,6 +1,7 @@
export { CertStatus } from "./enums";
export {
useDeleteCert,
useDownloadCertPkcs12,
useImportCertificate,
useRenewCertificate,
useRevokeCert,

View File

@@ -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;
}
}
});
};

View File

@@ -72,3 +72,10 @@ export type TUpdateRenewalConfigDTO = {
enableAutoRenewal?: boolean;
projectSlug: string;
};
export type TDownloadPkcs12DTO = {
serialNumber: string;
projectSlug: string;
password: string;
alias: string;
};

View File

@@ -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>
);
};

View File

@@ -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} />

View File

@@ -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
})
}