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:
32
backend/package-lock.json
generated
32
backend/package-lock.json
generated
@@ -58,6 +58,7 @@
|
|||||||
"@sindresorhus/slugify": "1.1.0",
|
"@sindresorhus/slugify": "1.1.0",
|
||||||
"@slack/oauth": "^3.0.2",
|
"@slack/oauth": "^3.0.2",
|
||||||
"@slack/web-api": "^7.8.0",
|
"@slack/web-api": "^7.8.0",
|
||||||
|
"@types/node-forge": "^1.3.14",
|
||||||
"@ucast/mongo2js": "^1.3.4",
|
"@ucast/mongo2js": "^1.3.4",
|
||||||
"acme-client": "^5.4.0",
|
"acme-client": "^5.4.0",
|
||||||
"ajv": "^8.12.0",
|
"ajv": "^8.12.0",
|
||||||
@@ -97,6 +98,7 @@
|
|||||||
"ms": "^2.1.3",
|
"ms": "^2.1.3",
|
||||||
"mysql2": "^3.9.8",
|
"mysql2": "^3.9.8",
|
||||||
"nanoid": "^3.3.8",
|
"nanoid": "^3.3.8",
|
||||||
|
"node-forge": "^1.3.1",
|
||||||
"nodemailer": "^6.9.9",
|
"nodemailer": "^6.9.9",
|
||||||
"oci-sdk": "^2.108.0",
|
"oci-sdk": "^2.108.0",
|
||||||
"odbc": "^2.4.9",
|
"odbc": "^2.4.9",
|
||||||
@@ -15272,6 +15274,15 @@
|
|||||||
"form-data": "^4.0.0"
|
"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": {
|
"node_modules/@types/node/node_modules/undici-types": {
|
||||||
"version": "6.21.0",
|
"version": "6.21.0",
|
||||||
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
|
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
|
||||||
@@ -33526,18 +33537,6 @@
|
|||||||
"url": "https://opencollective.com/vitest"
|
"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": {
|
"node_modules/vite-node/node_modules/debug": {
|
||||||
"version": "4.4.3",
|
"version": "4.4.3",
|
||||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
|
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
|
||||||
@@ -33569,15 +33568,6 @@
|
|||||||
"url": "https://github.com/sponsors/jonschlinkert"
|
"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": {
|
"node_modules/vite-node/node_modules/vite": {
|
||||||
"version": "7.1.12",
|
"version": "7.1.12",
|
||||||
"resolved": "https://registry.npmjs.org/vite/-/vite-7.1.12.tgz",
|
"resolved": "https://registry.npmjs.org/vite/-/vite-7.1.12.tgz",
|
||||||
|
|||||||
@@ -185,6 +185,7 @@
|
|||||||
"@sindresorhus/slugify": "1.1.0",
|
"@sindresorhus/slugify": "1.1.0",
|
||||||
"@slack/oauth": "^3.0.2",
|
"@slack/oauth": "^3.0.2",
|
||||||
"@slack/web-api": "^7.8.0",
|
"@slack/web-api": "^7.8.0",
|
||||||
|
"@types/node-forge": "^1.3.14",
|
||||||
"@ucast/mongo2js": "^1.3.4",
|
"@ucast/mongo2js": "^1.3.4",
|
||||||
"acme-client": "^5.4.0",
|
"acme-client": "^5.4.0",
|
||||||
"ajv": "^8.12.0",
|
"ajv": "^8.12.0",
|
||||||
@@ -224,6 +225,7 @@
|
|||||||
"ms": "^2.1.3",
|
"ms": "^2.1.3",
|
||||||
"mysql2": "^3.9.8",
|
"mysql2": "^3.9.8",
|
||||||
"nanoid": "^3.3.8",
|
"nanoid": "^3.3.8",
|
||||||
|
"node-forge": "^1.3.1",
|
||||||
"nodemailer": "^6.9.9",
|
"nodemailer": "^6.9.9",
|
||||||
"oci-sdk": "^2.108.0",
|
"oci-sdk": "^2.108.0",
|
||||||
"odbc": "^2.4.9",
|
"odbc": "^2.4.9",
|
||||||
|
|||||||
@@ -323,6 +323,7 @@ export enum EventType {
|
|||||||
GET_CERT_BODY = "get-cert-body",
|
GET_CERT_BODY = "get-cert-body",
|
||||||
GET_CERT_PRIVATE_KEY = "get-cert-private-key",
|
GET_CERT_PRIVATE_KEY = "get-cert-private-key",
|
||||||
GET_CERT_BUNDLE = "get-cert-bundle",
|
GET_CERT_BUNDLE = "get-cert-bundle",
|
||||||
|
EXPORT_CERT_PKCS12 = "export-cert-pkcs12",
|
||||||
CREATE_PKI_ALERT = "create-pki-alert",
|
CREATE_PKI_ALERT = "create-pki-alert",
|
||||||
GET_PKI_ALERT = "get-pki-alert",
|
GET_PKI_ALERT = "get-pki-alert",
|
||||||
UPDATE_PKI_ALERT = "update-pki-alert",
|
UPDATE_PKI_ALERT = "update-pki-alert",
|
||||||
@@ -2315,6 +2316,14 @@ interface GetCertBundle {
|
|||||||
serialNumber: string;
|
serialNumber: string;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
interface GetCertPkcs12 {
|
||||||
|
type: EventType.EXPORT_CERT_PKCS12;
|
||||||
|
metadata: {
|
||||||
|
certId: string;
|
||||||
|
cn: string;
|
||||||
|
serialNumber: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
interface CreatePkiAlert {
|
interface CreatePkiAlert {
|
||||||
type: EventType.CREATE_PKI_ALERT;
|
type: EventType.CREATE_PKI_ALERT;
|
||||||
@@ -4252,6 +4261,7 @@ export type Event =
|
|||||||
| GetCertBody
|
| GetCertBody
|
||||||
| GetCertPrivateKey
|
| GetCertPrivateKey
|
||||||
| GetCertBundle
|
| GetCertBundle
|
||||||
|
| GetCertPkcs12
|
||||||
| CreatePkiAlert
|
| CreatePkiAlert
|
||||||
| GetPkiAlert
|
| GetPkiAlert
|
||||||
| UpdatePkiAlert
|
| UpdatePkiAlert
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
/* eslint-disable @typescript-eslint/no-floating-promises */
|
/* eslint-disable @typescript-eslint/no-floating-promises */
|
||||||
|
import RE2 from "re2";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
import { CertificatesSchema } from "@app/db/schemas";
|
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;
|
||||||
|
}
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import * as x509 from "@peculiar/x509";
|
import * as x509 from "@peculiar/x509";
|
||||||
|
import forge from "node-forge";
|
||||||
import RE2 from "re2";
|
import RE2 from "re2";
|
||||||
|
|
||||||
import { crypto } from "@app/lib/crypto/cryptography";
|
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}'` });
|
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"}`
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|||||||
@@ -29,7 +29,12 @@ import { TProjectDALFactory } from "@app/services/project/project-dal";
|
|||||||
import { getProjectKmsCertificateKeyId } from "@app/services/project/project-fns";
|
import { getProjectKmsCertificateKeyId } from "@app/services/project/project-fns";
|
||||||
|
|
||||||
import { expandInternalCa, getCaCertChain, rebuildCaCrl } from "../certificate-authority/certificate-authority-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 { TCertificateSecretDALFactory } from "./certificate-secret-dal";
|
||||||
import {
|
import {
|
||||||
CertExtendedKeyUsage,
|
CertExtendedKeyUsage,
|
||||||
@@ -40,6 +45,7 @@ import {
|
|||||||
TGetCertBodyDTO,
|
TGetCertBodyDTO,
|
||||||
TGetCertBundleDTO,
|
TGetCertBundleDTO,
|
||||||
TGetCertDTO,
|
TGetCertDTO,
|
||||||
|
TGetCertPkcs12DTO,
|
||||||
TGetCertPrivateKeyDTO,
|
TGetCertPrivateKeyDTO,
|
||||||
TImportCertDTO,
|
TImportCertDTO,
|
||||||
TRevokeCertDTO
|
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 {
|
return {
|
||||||
getCert,
|
getCert,
|
||||||
getCertPrivateKey,
|
getCertPrivateKey,
|
||||||
@@ -663,6 +734,7 @@ export const certificateServiceFactory = ({
|
|||||||
revokeCert,
|
revokeCert,
|
||||||
getCertBody,
|
getCertBody,
|
||||||
importCert,
|
importCert,
|
||||||
getCertBundle
|
getCertBundle,
|
||||||
|
getCertPkcs12
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -119,6 +119,12 @@ export type TGetCertBundleDTO = {
|
|||||||
serialNumber: string;
|
serialNumber: string;
|
||||||
} & Omit<TProjectPermission, "projectId">;
|
} & Omit<TProjectPermission, "projectId">;
|
||||||
|
|
||||||
|
export type TGetCertPkcs12DTO = {
|
||||||
|
serialNumber: string;
|
||||||
|
password: string;
|
||||||
|
alias: string;
|
||||||
|
} & Omit<TProjectPermission, "projectId">;
|
||||||
|
|
||||||
export type TGetCertificateCredentialsDTO = {
|
export type TGetCertificateCredentialsDTO = {
|
||||||
certId: string;
|
certId: string;
|
||||||
projectId: string;
|
projectId: string;
|
||||||
|
|||||||
@@ -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 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.
|
- 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.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
<Tabs>
|
||||||
|
<Tab title="PEM Format">
|
||||||
|
<Steps>
|
||||||
|
<Step title="Exporting in PEM Format">
|
||||||
|
In the export modal, choose **PEM** as the format and click **Export**.
|
||||||
|
|
||||||
|

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

|
||||||
|
|
||||||
|
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:
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
- **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
|
## 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.
|
In the following steps, we explore how to revoke a X.509 certificate and obtain a Certificate Revocation List (CRL) for a CA.
|
||||||
|
|||||||
BIN
docs/images/platform/pki/certificate/cert-export-option.png
Normal file
BIN
docs/images/platform/pki/certificate/cert-export-option.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 305 KiB |
BIN
docs/images/platform/pki/certificate/cert-export-pem-modal.png
Normal file
BIN
docs/images/platform/pki/certificate/cert-export-pem-modal.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 539 KiB |
BIN
docs/images/platform/pki/certificate/cert-export-pem.png
Normal file
BIN
docs/images/platform/pki/certificate/cert-export-pem.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 281 KiB |
BIN
docs/images/platform/pki/certificate/cert-export-pkcs12.png
Normal file
BIN
docs/images/platform/pki/certificate/cert-export-pkcs12.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 324 KiB |
@@ -1,6 +1,7 @@
|
|||||||
export { CertStatus } from "./enums";
|
export { CertStatus } from "./enums";
|
||||||
export {
|
export {
|
||||||
useDeleteCert,
|
useDeleteCert,
|
||||||
|
useDownloadCertPkcs12,
|
||||||
useImportCertificate,
|
useImportCertificate,
|
||||||
useRenewCertificate,
|
useRenewCertificate,
|
||||||
useRevokeCert,
|
useRevokeCert,
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import { projectKeys } from "../projects";
|
|||||||
import {
|
import {
|
||||||
TCertificate,
|
TCertificate,
|
||||||
TDeleteCertDTO,
|
TDeleteCertDTO,
|
||||||
|
TDownloadPkcs12DTO,
|
||||||
TImportCertificateDTO,
|
TImportCertificateDTO,
|
||||||
TImportCertificateResponse,
|
TImportCertificateResponse,
|
||||||
TRenewCertificateDTO,
|
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;
|
enableAutoRenewal?: boolean;
|
||||||
projectSlug: string;
|
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,
|
ProjectPermissionSub,
|
||||||
useProject
|
useProject
|
||||||
} from "@app/context";
|
} from "@app/context";
|
||||||
import { useDeleteCert } from "@app/hooks/api";
|
import { useDeleteCert, useDownloadCertPkcs12 } from "@app/hooks/api";
|
||||||
import { usePopUp } from "@app/hooks/usePopUp";
|
import { usePopUp } from "@app/hooks/usePopUp";
|
||||||
|
|
||||||
import { CertificateCertModal } from "./CertificateCertModal";
|
import { CertificateCertModal } from "./CertificateCertModal";
|
||||||
|
import { CertificateExportModal, ExportOptions } from "./CertificateExportModal";
|
||||||
import { CertificateImportModal } from "./CertificateImportModal";
|
import { CertificateImportModal } from "./CertificateImportModal";
|
||||||
import { CertificateIssuanceModal } from "./CertificateIssuanceModal";
|
import { CertificateIssuanceModal } from "./CertificateIssuanceModal";
|
||||||
import { CertificateManagePkiSyncsModal } from "./CertificateManagePkiSyncsModal";
|
import { CertificateManagePkiSyncsModal } from "./CertificateManagePkiSyncsModal";
|
||||||
@@ -24,11 +25,13 @@ import { CertificatesTable } from "./CertificatesTable";
|
|||||||
export const CertificatesSection = () => {
|
export const CertificatesSection = () => {
|
||||||
const { currentProject } = useProject();
|
const { currentProject } = useProject();
|
||||||
const { mutateAsync: deleteCert } = useDeleteCert();
|
const { mutateAsync: deleteCert } = useDeleteCert();
|
||||||
|
const { mutateAsync: downloadCertPkcs12 } = useDownloadCertPkcs12();
|
||||||
|
|
||||||
const { popUp, handlePopUpOpen, handlePopUpClose, handlePopUpToggle } = usePopUp([
|
const { popUp, handlePopUpOpen, handlePopUpClose, handlePopUpToggle } = usePopUp([
|
||||||
"issueCertificate",
|
"issueCertificate",
|
||||||
"certificateImport",
|
"certificateImport",
|
||||||
"certificateCert",
|
"certificateCert",
|
||||||
|
"certificateExport",
|
||||||
"deleteCertificate",
|
"deleteCertificate",
|
||||||
"revokeCertificate",
|
"revokeCertificate",
|
||||||
"manageRenewal",
|
"manageRenewal",
|
||||||
@@ -49,6 +52,45 @@ export const CertificatesSection = () => {
|
|||||||
handlePopUpClose("deleteCertificate");
|
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 (
|
return (
|
||||||
<div className="mb-6 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
|
<div className="mb-6 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
|
||||||
<div className="mb-4 flex justify-between">
|
<div className="mb-4 flex justify-between">
|
||||||
@@ -84,6 +126,11 @@ export const CertificatesSection = () => {
|
|||||||
<CertificateIssuanceModal popUp={popUp} handlePopUpToggle={handlePopUpToggle} />
|
<CertificateIssuanceModal popUp={popUp} handlePopUpToggle={handlePopUpToggle} />
|
||||||
<CertificateImportModal popUp={popUp} handlePopUpToggle={handlePopUpToggle} />
|
<CertificateImportModal popUp={popUp} handlePopUpToggle={handlePopUpToggle} />
|
||||||
<CertificateCertModal popUp={popUp} handlePopUpToggle={handlePopUpToggle} />
|
<CertificateCertModal popUp={popUp} handlePopUpToggle={handlePopUpToggle} />
|
||||||
|
<CertificateExportModal
|
||||||
|
popUp={popUp}
|
||||||
|
handlePopUpToggle={handlePopUpToggle}
|
||||||
|
onFormatSelected={handleCertificateExport}
|
||||||
|
/>
|
||||||
<CertificateManageRenewalModal popUp={popUp} handlePopUpToggle={handlePopUpToggle} />
|
<CertificateManageRenewalModal popUp={popUp} handlePopUpToggle={handlePopUpToggle} />
|
||||||
<CertificateRenewalModal popUp={popUp} handlePopUpToggle={handlePopUpToggle} />
|
<CertificateRenewalModal popUp={popUp} handlePopUpToggle={handlePopUpToggle} />
|
||||||
<CertificateRevocationModal popUp={popUp} handlePopUpToggle={handlePopUpToggle} />
|
<CertificateRevocationModal popUp={popUp} handlePopUpToggle={handlePopUpToggle} />
|
||||||
|
|||||||
@@ -68,6 +68,7 @@ type Props = {
|
|||||||
"deleteCertificate",
|
"deleteCertificate",
|
||||||
"revokeCertificate",
|
"revokeCertificate",
|
||||||
"certificateCert",
|
"certificateCert",
|
||||||
|
"certificateExport",
|
||||||
"manageRenewal",
|
"manageRenewal",
|
||||||
"renewCertificate",
|
"renewCertificate",
|
||||||
"managePkiSyncs"
|
"managePkiSyncs"
|
||||||
@@ -275,7 +276,7 @@ export const CertificatesTable = ({ handlePopUpOpen }: Props) => {
|
|||||||
!isAllowed && "pointer-events-none cursor-not-allowed opacity-50"
|
!isAllowed && "pointer-events-none cursor-not-allowed opacity-50"
|
||||||
)}
|
)}
|
||||||
onClick={async () =>
|
onClick={async () =>
|
||||||
handlePopUpOpen("certificateCert", {
|
handlePopUpOpen("certificateExport", {
|
||||||
serialNumber: certificate.serialNumber
|
serialNumber: certificate.serialNumber
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user