Merge pull request #2239 from Infisical/ca-renewal

CA Renewal (Same Key Pair)
This commit is contained in:
Maidul Islam
2024-08-16 10:37:44 -04:00
committed by GitHub
36 changed files with 1819 additions and 83 deletions
@@ -0,0 +1,117 @@
import { Knex } from "knex";
import { TableName } from "../schemas";
export async function up(knex: Knex): Promise<void> {
if (await knex.schema.hasTable(TableName.CertificateAuthority)) {
const hasActiveCaCertIdColumn = await knex.schema.hasColumn(TableName.CertificateAuthority, "activeCaCertId");
if (!hasActiveCaCertIdColumn) {
await knex.schema.alterTable(TableName.CertificateAuthority, (t) => {
t.uuid("activeCaCertId").nullable();
t.foreign("activeCaCertId").references("id").inTable(TableName.CertificateAuthorityCert);
});
await knex.raw(`
UPDATE "${TableName.CertificateAuthority}" ca
SET "activeCaCertId" = cac.id
FROM "${TableName.CertificateAuthorityCert}" cac
WHERE ca.id = cac."caId"
`);
}
}
if (await knex.schema.hasTable(TableName.CertificateAuthorityCert)) {
const hasVersionColumn = await knex.schema.hasColumn(TableName.CertificateAuthorityCert, "version");
if (!hasVersionColumn) {
await knex.schema.alterTable(TableName.CertificateAuthorityCert, (t) => {
t.integer("version").nullable();
t.dropUnique(["caId"]);
});
await knex(TableName.CertificateAuthorityCert).update({ version: 1 }).whereNull("version");
await knex.schema.alterTable(TableName.CertificateAuthorityCert, (t) => {
t.integer("version").notNullable().alter();
});
}
const hasCaSecretIdColumn = await knex.schema.hasColumn(TableName.CertificateAuthorityCert, "caSecretId");
if (!hasCaSecretIdColumn) {
await knex.schema.alterTable(TableName.CertificateAuthorityCert, (t) => {
t.uuid("caSecretId").nullable();
t.foreign("caSecretId").references("id").inTable(TableName.CertificateAuthoritySecret).onDelete("CASCADE");
});
await knex.raw(`
UPDATE "${TableName.CertificateAuthorityCert}" cert
SET "caSecretId" = (
SELECT sec.id
FROM "${TableName.CertificateAuthoritySecret}" sec
WHERE sec."caId" = cert."caId"
)
`);
await knex.schema.alterTable(TableName.CertificateAuthorityCert, (t) => {
t.uuid("caSecretId").notNullable().alter();
});
}
}
if (await knex.schema.hasTable(TableName.CertificateAuthoritySecret)) {
await knex.schema.alterTable(TableName.CertificateAuthoritySecret, (t) => {
t.dropUnique(["caId"]);
});
}
if (await knex.schema.hasTable(TableName.Certificate)) {
await knex.schema.alterTable(TableName.Certificate, (t) => {
t.uuid("caCertId").nullable();
t.foreign("caCertId").references("id").inTable(TableName.CertificateAuthorityCert);
});
await knex.raw(`
UPDATE "${TableName.Certificate}" cert
SET "caCertId" = (
SELECT caCert.id
FROM "${TableName.CertificateAuthorityCert}" caCert
WHERE caCert."caId" = cert."caId"
)
`);
await knex.schema.alterTable(TableName.Certificate, (t) => {
t.uuid("caCertId").notNullable().alter();
});
}
}
export async function down(knex: Knex): Promise<void> {
if (await knex.schema.hasTable(TableName.CertificateAuthority)) {
if (await knex.schema.hasColumn(TableName.CertificateAuthority, "activeCaCertId")) {
await knex.schema.alterTable(TableName.CertificateAuthority, (t) => {
t.dropColumn("activeCaCertId");
});
}
}
if (await knex.schema.hasTable(TableName.CertificateAuthorityCert)) {
if (await knex.schema.hasColumn(TableName.CertificateAuthorityCert, "version")) {
await knex.schema.alterTable(TableName.CertificateAuthorityCert, (t) => {
t.dropColumn("version");
});
}
if (await knex.schema.hasColumn(TableName.CertificateAuthorityCert, "caSecretId")) {
await knex.schema.alterTable(TableName.CertificateAuthorityCert, (t) => {
t.dropColumn("caSecretId");
});
}
}
if (await knex.schema.hasTable(TableName.Certificate)) {
if (await knex.schema.hasColumn(TableName.Certificate, "caCertId")) {
await knex.schema.alterTable(TableName.Certificate, (t) => {
t.dropColumn("caCertId");
});
}
}
}
@@ -27,7 +27,8 @@ export const CertificateAuthoritiesSchema = z.object({
maxPathLength: z.number().nullable().optional(), maxPathLength: z.number().nullable().optional(),
keyAlgorithm: z.string(), keyAlgorithm: z.string(),
notBefore: z.date().nullable().optional(), notBefore: z.date().nullable().optional(),
notAfter: z.date().nullable().optional() notAfter: z.date().nullable().optional(),
activeCaCertId: z.string().uuid().nullable().optional()
}); });
export type TCertificateAuthorities = z.infer<typeof CertificateAuthoritiesSchema>; export type TCertificateAuthorities = z.infer<typeof CertificateAuthoritiesSchema>;
@@ -15,7 +15,9 @@ export const CertificateAuthorityCertsSchema = z.object({
updatedAt: z.date(), updatedAt: z.date(),
caId: z.string().uuid(), caId: z.string().uuid(),
encryptedCertificate: zodBuffer, encryptedCertificate: zodBuffer,
encryptedCertificateChain: zodBuffer encryptedCertificateChain: zodBuffer,
version: z.number(),
caSecretId: z.string().uuid()
}); });
export type TCertificateAuthorityCerts = z.infer<typeof CertificateAuthorityCertsSchema>; export type TCertificateAuthorityCerts = z.infer<typeof CertificateAuthorityCertsSchema>;
+2 -1
View File
@@ -20,7 +20,8 @@ export const CertificatesSchema = z.object({
notAfter: z.date(), notAfter: z.date(),
revokedAt: z.date().nullable().optional(), revokedAt: z.date().nullable().optional(),
revocationReason: z.number().nullable().optional(), revocationReason: z.number().nullable().optional(),
altNames: z.string().default("").nullable().optional() altNames: z.string().default("").nullable().optional(),
caCertId: z.string().uuid()
}); });
export type TCertificates = z.infer<typeof CertificatesSchema>; export type TCertificates = z.infer<typeof CertificatesSchema>;
@@ -130,7 +130,9 @@ export enum EventType {
GET_CA = "get-certificate-authority", GET_CA = "get-certificate-authority",
UPDATE_CA = "update-certificate-authority", UPDATE_CA = "update-certificate-authority",
DELETE_CA = "delete-certificate-authority", DELETE_CA = "delete-certificate-authority",
RENEW_CA = "renew-certificate-authority",
GET_CA_CSR = "get-certificate-authority-csr", GET_CA_CSR = "get-certificate-authority-csr",
GET_CA_CERTS = "get-certificate-authority-certs",
GET_CA_CERT = "get-certificate-authority-cert", GET_CA_CERT = "get-certificate-authority-cert",
SIGN_INTERMEDIATE = "sign-intermediate", SIGN_INTERMEDIATE = "sign-intermediate",
IMPORT_CA_CERT = "import-certificate-authority-cert", IMPORT_CA_CERT = "import-certificate-authority-cert",
@@ -1096,6 +1098,14 @@ interface DeleteCa {
}; };
} }
interface RenewCa {
type: EventType.RENEW_CA;
metadata: {
caId: string;
dn: string;
};
}
interface GetCaCsr { interface GetCaCsr {
type: EventType.GET_CA_CSR; type: EventType.GET_CA_CSR;
metadata: { metadata: {
@@ -1104,6 +1114,14 @@ interface GetCaCsr {
}; };
} }
interface GetCaCerts {
type: EventType.GET_CA_CERTS;
metadata: {
caId: string;
dn: string;
};
}
interface GetCaCert { interface GetCaCert {
type: EventType.GET_CA_CERT; type: EventType.GET_CA_CERT;
metadata: { metadata: {
@@ -1349,7 +1367,9 @@ export type Event =
| GetCa | GetCa
| UpdateCa | UpdateCa
| DeleteCa | DeleteCa
| RenewCa
| GetCaCsr | GetCaCsr
| GetCaCerts
| GetCaCert | GetCaCert
| SignIntermediate | SignIntermediate
| ImportCaCert | ImportCaCert
+15
View File
@@ -1049,12 +1049,27 @@ export const CERTIFICATE_AUTHORITIES = {
caId: "The ID of the CA to generate CSR from", caId: "The ID of the CA to generate CSR from",
csr: "The generated CSR from the CA" csr: "The generated CSR from the CA"
}, },
RENEW_CA_CERT: {
caId: "The ID of the CA to renew the CA certificate for",
type: "The type of behavior to use for the renewal operation. Currently Infisical is only able to renew a CA certificate with the same key pair.",
notAfter: "The expiry date and time for the renewed CA certificate in YYYY-MM-DDTHH:mm:ss.sssZ format",
certificate: "The renewed CA certificate body",
certificateChain: "The certificate chain of the CA",
serialNumber: "The serial number of the renewed CA certificate"
},
GET_CERT: { GET_CERT: {
caId: "The ID of the CA to get the certificate body and certificate chain from", caId: "The ID of the CA to get the certificate body and certificate chain from",
certificate: "The certificate body of the CA", certificate: "The certificate body of the CA",
certificateChain: "The certificate chain of the CA", certificateChain: "The certificate chain of the CA",
serialNumber: "The serial number of the CA certificate" serialNumber: "The serial number of the CA certificate"
}, },
GET_CA_CERTS: {
caId: "The ID of the CA to get the CA certificates for",
certificate: "The certificate body of the CA certificate",
certificateChain: "The certificate chain of the CA certificate",
serialNumber: "The serial number of the CA certificate",
version: "The version of the CA certificate. The version is incremented for each CA renewal operation."
},
SIGN_INTERMEDIATE: { SIGN_INTERMEDIATE: {
caId: "The ID of the CA to sign the intermediate certificate with", caId: "The ID of the CA to sign the intermediate certificate with",
csr: "The pem-encoded CSR to sign with the CA", csr: "The pem-encoded CSR to sign with the CA",
@@ -8,7 +8,7 @@ import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { AuthMode } from "@app/services/auth/auth-type"; import { AuthMode } from "@app/services/auth/auth-type";
import { CertKeyAlgorithm } from "@app/services/certificate/certificate-types"; import { CertKeyAlgorithm } from "@app/services/certificate/certificate-types";
import { CaStatus, CaType } from "@app/services/certificate-authority/certificate-authority-types"; import { CaRenewalType, CaStatus, CaType } from "@app/services/certificate-authority/certificate-authority-types";
import { import {
validateAltNamesField, validateAltNamesField,
validateCaDateField validateCaDateField
@@ -275,15 +275,118 @@ export const registerCaRouter = async (server: FastifyZodProvider) => {
} }
}); });
server.route({
method: "POST",
url: "/:caId/renew",
config: {
rateLimit: writeLimit
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
schema: {
description: "Perform CA certificate renewal",
params: z.object({
caId: z.string().trim().describe(CERTIFICATE_AUTHORITIES.RENEW_CA_CERT.caId)
}),
body: z.object({
type: z.nativeEnum(CaRenewalType).describe(CERTIFICATE_AUTHORITIES.RENEW_CA_CERT.type),
notAfter: validateCaDateField.describe(CERTIFICATE_AUTHORITIES.RENEW_CA_CERT.notAfter)
}),
response: {
200: z.object({
certificate: z.string().trim().describe(CERTIFICATE_AUTHORITIES.RENEW_CA_CERT.certificate),
certificateChain: z.string().trim().describe(CERTIFICATE_AUTHORITIES.RENEW_CA_CERT.certificateChain),
serialNumber: z.string().trim().describe(CERTIFICATE_AUTHORITIES.RENEW_CA_CERT.serialNumber)
})
}
},
handler: async (req) => {
const { certificate, certificateChain, serialNumber, ca } =
await server.services.certificateAuthority.renewCaCert({
caId: req.params.caId,
actor: req.permission.type,
actorId: req.permission.id,
actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId,
...req.body
});
await server.services.auditLog.createAuditLog({
...req.auditLogInfo,
projectId: ca.projectId,
event: {
type: EventType.RENEW_CA,
metadata: {
caId: ca.id,
dn: ca.dn
}
}
});
return {
certificate,
certificateChain,
serialNumber
};
}
});
server.route({ server.route({
method: "GET", method: "GET",
url: "/:caId/certificate", url: "/:caId/ca-certificates",
config: { config: {
rateLimit: readLimit rateLimit: readLimit
}, },
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
schema: { schema: {
description: "Get cert and cert chain of a CA", description: "Get list of past and current CA certificates for a CA",
params: z.object({
caId: z.string().trim().describe(CERTIFICATE_AUTHORITIES.GET_CA_CERTS.caId)
}),
response: {
200: z.array(
z.object({
certificate: z.string().describe(CERTIFICATE_AUTHORITIES.GET_CA_CERTS.certificate),
certificateChain: z.string().describe(CERTIFICATE_AUTHORITIES.GET_CA_CERTS.certificateChain),
serialNumber: z.string().describe(CERTIFICATE_AUTHORITIES.GET_CA_CERTS.serialNumber),
version: z.number().describe(CERTIFICATE_AUTHORITIES.GET_CA_CERTS.version)
})
)
}
},
handler: async (req) => {
const { caCerts, ca } = await server.services.certificateAuthority.getCaCerts({
caId: req.params.caId,
actor: req.permission.type,
actorId: req.permission.id,
actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId
});
await server.services.auditLog.createAuditLog({
...req.auditLogInfo,
projectId: ca.projectId,
event: {
type: EventType.GET_CA_CERTS,
metadata: {
caId: ca.id,
dn: ca.dn
}
}
});
return caCerts;
}
});
server.route({
method: "GET",
url: "/:caId/certificate", // TODO: consider updating endpoint structure considering CA certificates
config: {
rateLimit: readLimit
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
schema: {
description: "Get current CA cert and cert chain of a CA",
params: z.object({ params: z.object({
caId: z.string().trim().describe(CERTIFICATE_AUTHORITIES.GET_CERT.caId) caId: z.string().trim().describe(CERTIFICATE_AUTHORITIES.GET_CERT.caId)
}), }),
@@ -5,7 +5,13 @@ import { BadRequestError } from "@app/lib/errors";
import { getProjectKmsCertificateKeyId } from "@app/services/project/project-fns"; import { getProjectKmsCertificateKeyId } from "@app/services/project/project-fns";
import { CertKeyAlgorithm, CertStatus } from "../certificate/certificate-types"; import { CertKeyAlgorithm, CertStatus } from "../certificate/certificate-types";
import { TDNParts, TGetCaCertChainDTO, TGetCaCredentialsDTO, TRebuildCaCrlDTO } from "./certificate-authority-types"; import {
TDNParts,
TGetCaCertChainDTO,
TGetCaCertChainsDTO,
TGetCaCredentialsDTO,
TRebuildCaCrlDTO
} from "./certificate-authority-types";
export const createDistinguishedName = (parts: TDNParts) => { export const createDistinguishedName = (parts: TDNParts) => {
const dnParts = []; const dnParts = [];
@@ -89,6 +95,8 @@ export const keyAlgorithmToAlgCfg = (keyAlgorithm: CertKeyAlgorithm) => {
* Return the public and private key of CA with id [caId] * Return the public and private key of CA with id [caId]
* Note: credentials are returned as crypto.webcrypto.CryptoKey * Note: credentials are returned as crypto.webcrypto.CryptoKey
* suitable for use with @peculiar/x509 module * suitable for use with @peculiar/x509 module
*
* TODO: Update to get latest CA Secret once support for CA renewal with new key pair is added
*/ */
export const getCaCredentials = async ({ export const getCaCredentials = async ({
caId, caId,
@@ -132,26 +140,73 @@ export const getCaCredentials = async ({
]); ]);
return { return {
caSecret,
caPrivateKey, caPrivateKey,
caPublicKey caPublicKey
}; };
}; };
/** /**
* Return the decrypted pem-encoded certificate and certificate chain * Return the list of decrypted pem-encoded certificates and certificate chains
* for CA with id [caId]. * for CA with id [caId].
*/ */
export const getCaCertChain = async ({ export const getCaCertChains = async ({
caId, caId,
certificateAuthorityDAL, certificateAuthorityDAL,
certificateAuthorityCertDAL, certificateAuthorityCertDAL,
projectDAL, projectDAL,
kmsService kmsService
}: TGetCaCertChainDTO) => { }: TGetCaCertChainsDTO) => {
const ca = await certificateAuthorityDAL.findById(caId); const ca = await certificateAuthorityDAL.findById(caId);
if (!ca) throw new BadRequestError({ message: "CA not found" }); if (!ca) throw new BadRequestError({ message: "CA not found" });
const caCert = await certificateAuthorityCertDAL.findOne({ caId: ca.id }); const keyId = await getProjectKmsCertificateKeyId({
projectId: ca.projectId,
projectDAL,
kmsService
});
const kmsDecryptor = await kmsService.decryptWithKmsKey({
kmsId: keyId
});
const caCerts = await certificateAuthorityCertDAL.find({ caId: ca.id }, { sort: [["version", "asc"]] });
const decryptedChains = await Promise.all(
caCerts.map(async (caCert) => {
const decryptedCaCert = await kmsDecryptor({
cipherTextBlob: caCert.encryptedCertificate
});
const caCertObj = new x509.X509Certificate(decryptedCaCert);
const decryptedChain = await kmsDecryptor({
cipherTextBlob: caCert.encryptedCertificateChain
});
return {
certificate: caCertObj.toString("pem"),
certificateChain: decryptedChain.toString("utf-8"),
serialNumber: caCertObj.serialNumber,
version: caCert.version
};
})
);
return decryptedChains;
};
/**
* Return the decrypted pem-encoded certificate and certificate chain
* corresponding to CA certificate with id [caCertId].
*/
export const getCaCertChain = async ({
caCertId,
certificateAuthorityDAL,
certificateAuthorityCertDAL,
projectDAL,
kmsService
}: TGetCaCertChainDTO) => {
const caCert = await certificateAuthorityCertDAL.findById(caCertId);
if (!caCert) throw new BadRequestError({ message: "CA certificate not found" });
const ca = await certificateAuthorityDAL.findById(caCert.caId);
const keyId = await getProjectKmsCertificateKeyId({ const keyId = await getProjectKmsCertificateKeyId({
projectId: ca.projectId, projectId: ca.projectId,
@@ -20,7 +20,8 @@ import { TCertificateAuthorityCertDALFactory } from "./certificate-authority-cer
import { TCertificateAuthorityDALFactory } from "./certificate-authority-dal"; import { TCertificateAuthorityDALFactory } from "./certificate-authority-dal";
import { import {
createDistinguishedName, createDistinguishedName,
getCaCertChain, getCaCertChain, // TODO: consider rename
getCaCertChains,
getCaCredentials, getCaCredentials,
keyAlgorithmToAlgCfg, keyAlgorithmToAlgCfg,
parseDistinguishedName parseDistinguishedName
@@ -33,10 +34,12 @@ import {
TCreateCaDTO, TCreateCaDTO,
TDeleteCaDTO, TDeleteCaDTO,
TGetCaCertDTO, TGetCaCertDTO,
TGetCaCertsDTO,
TGetCaCsrDTO, TGetCaCsrDTO,
TGetCaDTO, TGetCaDTO,
TImportCertToCaDTO, TImportCertToCaDTO,
TIssueCertFromCaDTO, TIssueCertFromCaDTO,
TRenewCaCertDTO,
TSignCertFromCaDTO, TSignCertFromCaDTO,
TSignIntermediateDTO, TSignIntermediateDTO,
TUpdateCaDTO TUpdateCaDTO
@@ -48,7 +51,10 @@ type TCertificateAuthorityServiceFactoryDep = {
TCertificateAuthorityDALFactory, TCertificateAuthorityDALFactory,
"transaction" | "create" | "findById" | "updateById" | "deleteById" | "findOne" "transaction" | "create" | "findById" | "updateById" | "deleteById" | "findOne"
>; >;
certificateAuthorityCertDAL: Pick<TCertificateAuthorityCertDALFactory, "create" | "findOne" | "transaction">; certificateAuthorityCertDAL: Pick<
TCertificateAuthorityCertDALFactory,
"create" | "findOne" | "transaction" | "find" | "findById"
>;
certificateAuthoritySecretDAL: Pick<TCertificateAuthoritySecretDALFactory, "create" | "findOne">; certificateAuthoritySecretDAL: Pick<TCertificateAuthoritySecretDALFactory, "create" | "findOne">;
certificateAuthorityCrlDAL: Pick<TCertificateAuthorityCrlDALFactory, "create" | "findOne" | "update">; certificateAuthorityCrlDAL: Pick<TCertificateAuthorityCrlDALFactory, "create" | "findOne" | "update">;
certificateAuthorityQueue: TCertificateAuthorityQueueFactory; // TODO: Pick certificateAuthorityQueue: TCertificateAuthorityQueueFactory; // TODO: Pick
@@ -165,6 +171,24 @@ export const certificateAuthorityServiceFactory = ({
kmsId: certificateManagerKmsId kmsId: certificateManagerKmsId
}); });
// https://nodejs.org/api/crypto.html#static-method-keyobjectfromkey
const skObj = KeyObject.from(keys.privateKey);
const { cipherTextBlob: encryptedPrivateKey } = await kmsEncryptor({
plainText: skObj.export({
type: "pkcs8",
format: "der"
})
});
const caSecret = await certificateAuthoritySecretDAL.create(
{
caId: ca.id,
encryptedPrivateKey
},
tx
);
if (type === CaType.ROOT) { if (type === CaType.ROOT) {
// note: create self-signed cert only applicable for root CA // note: create self-signed cert only applicable for root CA
const cert = await x509.X509CertificateGenerator.createSelfSigned({ const cert = await x509.X509CertificateGenerator.createSelfSigned({
@@ -191,11 +215,21 @@ export const certificateAuthorityServiceFactory = ({
plainText: Buffer.alloc(0) plainText: Buffer.alloc(0)
}); });
await certificateAuthorityCertDAL.create( const caCert = await certificateAuthorityCertDAL.create(
{ {
caId: ca.id, caId: ca.id,
encryptedCertificate, encryptedCertificate,
encryptedCertificateChain encryptedCertificateChain,
version: 1,
caSecretId: caSecret.id
},
tx
);
await certificateAuthorityDAL.updateById(
ca.id,
{
activeCaCertId: caCert.id
}, },
tx tx
); );
@@ -223,24 +257,6 @@ export const certificateAuthorityServiceFactory = ({
tx tx
); );
// https://nodejs.org/api/crypto.html#static-method-keyobjectfromkey
const skObj = KeyObject.from(keys.privateKey);
const { cipherTextBlob: encryptedPrivateKey } = await kmsEncryptor({
plainText: skObj.export({
type: "pkcs8",
format: "der"
})
});
await certificateAuthoritySecretDAL.create(
{
caId: ca.id,
encryptedPrivateKey
},
tx
);
return ca; return ca;
}); });
@@ -341,9 +357,7 @@ export const certificateAuthorityServiceFactory = ({
); );
if (ca.type === CaType.ROOT) throw new BadRequestError({ message: "Root CA cannot generate CSR" }); if (ca.type === CaType.ROOT) throw new BadRequestError({ message: "Root CA cannot generate CSR" });
if (ca.activeCaCertId) throw new BadRequestError({ message: "CA already has a certificate installed" });
const caCert = await certificateAuthorityCertDAL.findOne({ caId: ca.id });
if (caCert) throw new BadRequestError({ message: "CA already has a certificate installed" });
const { caPrivateKey, caPublicKey } = await getCaCredentials({ const { caPrivateKey, caPublicKey } = await getCaCredentials({
caId, caId,
@@ -381,9 +395,283 @@ export const certificateAuthorityServiceFactory = ({
}; };
/** /**
* Return certificate and certificate chain for CA * Renew certificate for CA with id [caId]
* Note: Currently implements CA renewal with same key-pair only
*/ */
const getCaCert = async ({ caId, actorId, actorAuthMethod, actor, actorOrgId }: TGetCaCertDTO) => { const renewCaCert = async ({ caId, notAfter, actorId, actorAuthMethod, actor, actorOrgId }: TRenewCaCertDTO) => {
const ca = await certificateAuthorityDAL.findById(caId);
if (!ca) throw new BadRequestError({ message: "CA not found" });
if (!ca.activeCaCertId) throw new BadRequestError({ message: "CA does not have a certificate installed" });
const { permission } = await permissionService.getProjectPermission(
actor,
actorId,
ca.projectId,
actorAuthMethod,
actorOrgId
);
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Create,
ProjectPermissionSub.CertificateAuthorities
);
if (ca.status === CaStatus.DISABLED) throw new BadRequestError({ message: "CA is disabled" });
// get latest CA certificate
const caCert = await certificateAuthorityCertDAL.findById(ca.activeCaCertId);
const serialNumber = crypto.randomBytes(32).toString("hex");
const certificateManagerKmsId = await getProjectKmsCertificateKeyId({
projectId: ca.projectId,
projectDAL,
kmsService
});
const kmsEncryptor = await kmsService.encryptWithKmsKey({
kmsId: certificateManagerKmsId
});
const { caPrivateKey, caPublicKey, caSecret } = await getCaCredentials({
caId: ca.id,
certificateAuthorityDAL,
certificateAuthoritySecretDAL,
projectDAL,
kmsService
});
const alg = keyAlgorithmToAlgCfg(ca.keyAlgorithm as CertKeyAlgorithm);
const kmsDecryptor = await kmsService.decryptWithKmsKey({
kmsId: certificateManagerKmsId
});
const decryptedCaCert = await kmsDecryptor({
cipherTextBlob: caCert.encryptedCertificate
});
const caCertObj = new x509.X509Certificate(decryptedCaCert);
let certificate = "";
let certificateChain = "";
switch (ca.type) {
case CaType.ROOT: {
if (new Date(notAfter) <= new Date(caCertObj.notAfter)) {
throw new BadRequestError({
message:
"New Root CA certificate must have notAfter date that is greater than the current certificate notAfter date"
});
}
const notBeforeDate = new Date();
const cert = await x509.X509CertificateGenerator.createSelfSigned({
name: ca.dn,
serialNumber,
notBefore: notBeforeDate,
notAfter: new Date(notAfter),
signingAlgorithm: alg,
keys: {
privateKey: caPrivateKey,
publicKey: caPublicKey
},
extensions: [
new x509.BasicConstraintsExtension(
true,
ca.maxPathLength === -1 || !ca.maxPathLength ? undefined : ca.maxPathLength,
true
),
new x509.ExtendedKeyUsageExtension(["1.2.3.4.5.6.7", "2.3.4.5.6.7.8"], true),
// eslint-disable-next-line no-bitwise
new x509.KeyUsagesExtension(x509.KeyUsageFlags.keyCertSign | x509.KeyUsageFlags.cRLSign, true),
await x509.SubjectKeyIdentifierExtension.create(caPublicKey)
]
});
const { cipherTextBlob: encryptedCertificate } = await kmsEncryptor({
plainText: Buffer.from(new Uint8Array(cert.rawData))
});
const { cipherTextBlob: encryptedCertificateChain } = await kmsEncryptor({
plainText: Buffer.alloc(0)
});
await certificateAuthorityDAL.transaction(async (tx) => {
const newCaCert = await certificateAuthorityCertDAL.create(
{
caId: ca.id,
encryptedCertificate,
encryptedCertificateChain,
version: caCert.version + 1,
caSecretId: caSecret.id
},
tx
);
await certificateAuthorityDAL.updateById(
ca.id,
{
activeCaCertId: newCaCert.id,
notBefore: notBeforeDate,
notAfter: new Date(notAfter)
},
tx
);
});
certificate = cert.toString("pem");
break;
}
case CaType.INTERMEDIATE: {
if (!ca.parentCaId) {
// TODO: look into optimal way to support renewal of intermediate CA with external parent CA
throw new BadRequestError({
message: "Failed to renew intermediate CA certificate with external parent CA"
});
}
const parentCa = await certificateAuthorityDAL.findById(ca.parentCaId);
const { caPrivateKey: parentCaPrivateKey } = await getCaCredentials({
caId: parentCa.id,
certificateAuthorityDAL,
certificateAuthoritySecretDAL,
projectDAL,
kmsService
});
// get latest parent CA certificate
if (!parentCa.activeCaCertId)
throw new BadRequestError({ message: "Parent CA does not have a certificate installed" });
const parentCaCert = await certificateAuthorityCertDAL.findById(parentCa.activeCaCertId);
const decryptedParentCaCert = await kmsDecryptor({
cipherTextBlob: parentCaCert.encryptedCertificate
});
const parentCaCertObj = new x509.X509Certificate(decryptedParentCaCert);
if (new Date(notAfter) <= new Date(caCertObj.notAfter)) {
throw new BadRequestError({
message:
"New Intermediate CA certificate must have notAfter date that is greater than the current certificate notAfter date"
});
}
if (new Date(notAfter) > new Date(parentCaCertObj.notAfter)) {
throw new BadRequestError({
message:
"New Intermediate CA certificate must have notAfter date that is equal to or smaller than the notAfter date of the parent CA certificate current certificate notAfter date"
});
}
const csrObj = await x509.Pkcs10CertificateRequestGenerator.create({
name: ca.dn,
keys: {
privateKey: caPrivateKey,
publicKey: caPublicKey
},
signingAlgorithm: alg,
extensions: [
// eslint-disable-next-line no-bitwise
new x509.KeyUsagesExtension(
x509.KeyUsageFlags.keyCertSign |
x509.KeyUsageFlags.cRLSign |
x509.KeyUsageFlags.digitalSignature |
x509.KeyUsageFlags.keyEncipherment
)
],
attributes: [new x509.ChallengePasswordAttribute("password")]
});
const notBeforeDate = new Date();
const intermediateCert = await x509.X509CertificateGenerator.create({
serialNumber,
subject: csrObj.subject,
issuer: parentCaCertObj.subject,
notBefore: notBeforeDate,
notAfter: new Date(notAfter),
signingKey: parentCaPrivateKey,
publicKey: csrObj.publicKey,
signingAlgorithm: alg,
extensions: [
new x509.KeyUsagesExtension(
x509.KeyUsageFlags.keyCertSign |
x509.KeyUsageFlags.cRLSign |
x509.KeyUsageFlags.digitalSignature |
x509.KeyUsageFlags.keyEncipherment,
true
),
new x509.BasicConstraintsExtension(
true,
ca.maxPathLength === -1 || !ca.maxPathLength ? undefined : ca.maxPathLength,
true
),
await x509.AuthorityKeyIdentifierExtension.create(parentCaCertObj, false),
await x509.SubjectKeyIdentifierExtension.create(csrObj.publicKey)
]
});
const { cipherTextBlob: encryptedCertificate } = await kmsEncryptor({
plainText: Buffer.from(new Uint8Array(intermediateCert.rawData))
});
const { caCert: parentCaCertificate, caCertChain: parentCaCertChain } = await getCaCertChain({
caCertId: parentCa.activeCaCertId,
certificateAuthorityDAL,
certificateAuthorityCertDAL,
projectDAL,
kmsService
});
certificateChain = `${parentCaCertificate}\n${parentCaCertChain}`.trim();
const { cipherTextBlob: encryptedCertificateChain } = await kmsEncryptor({
plainText: Buffer.from(certificateChain)
});
await certificateAuthorityDAL.transaction(async (tx) => {
const newCaCert = await certificateAuthorityCertDAL.create(
{
caId: ca.id,
encryptedCertificate,
encryptedCertificateChain,
version: caCert.version + 1,
caSecretId: caSecret.id
},
tx
);
await certificateAuthorityDAL.updateById(
ca.id,
{
activeCaCertId: newCaCert.id,
notBefore: notBeforeDate,
notAfter: new Date(notAfter)
},
tx
);
});
certificate = intermediateCert.toString("pem");
break;
}
default: {
throw new BadRequestError({
message: "Unrecognized CA type"
});
}
}
return {
certificate,
certificateChain,
serialNumber,
ca
};
};
const getCaCerts = async ({ caId, actorId, actorAuthMethod, actor, actorOrgId }: TGetCaCertsDTO) => {
const ca = await certificateAuthorityDAL.findById(caId); const ca = await certificateAuthorityDAL.findById(caId);
if (!ca) throw new BadRequestError({ message: "CA not found" }); if (!ca) throw new BadRequestError({ message: "CA not found" });
@@ -400,7 +688,7 @@ export const certificateAuthorityServiceFactory = ({
ProjectPermissionSub.CertificateAuthorities ProjectPermissionSub.CertificateAuthorities
); );
const { caCert, caCertChain, serialNumber } = await getCaCertChain({ const caCertChains = await getCaCertChains({
caId, caId,
certificateAuthorityDAL, certificateAuthorityDAL,
certificateAuthorityCertDAL, certificateAuthorityCertDAL,
@@ -408,6 +696,41 @@ export const certificateAuthorityServiceFactory = ({
kmsService kmsService
}); });
return {
ca,
caCerts: caCertChains
};
};
/**
* Return current certificate and certificate chain for CA
*/
const getCaCert = async ({ caId, actorId, actorAuthMethod, actor, actorOrgId }: TGetCaCertDTO) => {
const ca = await certificateAuthorityDAL.findById(caId);
if (!ca) throw new BadRequestError({ message: "CA not found" });
if (!ca.activeCaCertId) throw new BadRequestError({ message: "CA does not have a certificate installed" });
const { permission } = await permissionService.getProjectPermission(
actor,
actorId,
ca.projectId,
actorAuthMethod,
actorOrgId
);
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Read,
ProjectPermissionSub.CertificateAuthorities
);
const { caCert, caCertChain, serialNumber } = await getCaCertChain({
caCertId: ca.activeCaCertId,
certificateAuthorityDAL,
certificateAuthorityCertDAL,
projectDAL,
kmsService
});
return { return {
certificate: caCert, certificate: caCert,
certificateChain: caCertChain, certificateChain: caCertChain,
@@ -447,6 +770,13 @@ export const certificateAuthorityServiceFactory = ({
); );
if (ca.status === CaStatus.DISABLED) throw new BadRequestError({ message: "CA is disabled" }); if (ca.status === CaStatus.DISABLED) throw new BadRequestError({ message: "CA is disabled" });
if (!ca.activeCaCertId) throw new BadRequestError({ message: "CA does not have a certificate installed" });
const caCert = await certificateAuthorityCertDAL.findById(ca.activeCaCertId);
if (ca.notAfter && new Date() > new Date(ca.notAfter)) {
throw new BadRequestError({ message: "CA is expired" });
}
const alg = keyAlgorithmToAlgCfg(ca.keyAlgorithm as CertKeyAlgorithm); const alg = keyAlgorithmToAlgCfg(ca.keyAlgorithm as CertKeyAlgorithm);
@@ -459,7 +789,6 @@ export const certificateAuthorityServiceFactory = ({
kmsId: certificateManagerKmsId kmsId: certificateManagerKmsId
}); });
const caCert = await certificateAuthorityCertDAL.findOne({ caId: ca.id });
const decryptedCaCert = await kmsDecryptor({ const decryptedCaCert = await kmsDecryptor({
cipherTextBlob: caCert.encryptedCertificate cipherTextBlob: caCert.encryptedCertificate
}); });
@@ -531,7 +860,7 @@ export const certificateAuthorityServiceFactory = ({
}); });
const { caCert: issuingCaCertificate, caCertChain } = await getCaCertChain({ const { caCert: issuingCaCertificate, caCertChain } = await getCaCertChain({
caId, caCertId: ca.activeCaCertId,
certificateAuthorityDAL, certificateAuthorityDAL,
certificateAuthorityCertDAL, certificateAuthorityCertDAL,
projectDAL, projectDAL,
@@ -577,8 +906,7 @@ export const certificateAuthorityServiceFactory = ({
ProjectPermissionSub.CertificateAuthorities ProjectPermissionSub.CertificateAuthorities
); );
const caCert = await certificateAuthorityCertDAL.findOne({ caId: ca.id }); if (ca.activeCaCertId) throw new BadRequestError({ message: "CA has already imported a certificate" });
if (caCert) throw new BadRequestError({ message: "CA has already imported a certificate" });
const certObj = new x509.X509Certificate(certificate); const certObj = new x509.X509Certificate(certificate);
const maxPathLength = certObj.getExtension(x509.BasicConstraintsExtension)?.pathLength; const maxPathLength = certObj.getExtension(x509.BasicConstraintsExtension)?.pathLength;
@@ -625,12 +953,32 @@ export const certificateAuthorityServiceFactory = ({
plainText: Buffer.from(certificateChain) plainText: Buffer.from(certificateChain)
}); });
// TODO: validate that latest key-pair of CA is used to sign the certificate
// once renewal with new key pair is supported
const { caSecret, caPublicKey } = await getCaCredentials({
caId: ca.id,
certificateAuthorityDAL,
certificateAuthoritySecretDAL,
projectDAL,
kmsService
});
const isCaAndCertPublicKeySame = Buffer.from(await crypto.subtle.exportKey("spki", caPublicKey)).equals(
Buffer.from(certObj.publicKey.rawData)
);
if (!isCaAndCertPublicKeySame) {
throw new BadRequestError({ message: "CA and certificate public key do not match" });
}
await certificateAuthorityCertDAL.transaction(async (tx) => { await certificateAuthorityCertDAL.transaction(async (tx) => {
await certificateAuthorityCertDAL.create( const newCaCert = await certificateAuthorityCertDAL.create(
{ {
caId: ca.id, caId: ca.id,
encryptedCertificate, encryptedCertificate,
encryptedCertificateChain encryptedCertificateChain,
version: 1,
caSecretId: caSecret.id
}, },
tx tx
); );
@@ -643,7 +991,8 @@ export const certificateAuthorityServiceFactory = ({
notBefore: new Date(certObj.notBefore), notBefore: new Date(certObj.notBefore),
notAfter: new Date(certObj.notAfter), notAfter: new Date(certObj.notAfter),
serialNumber: certObj.serialNumber, serialNumber: certObj.serialNumber,
parentCaId: parentCa?.id parentCaId: parentCa?.id,
activeCaCertId: newCaCert.id
}, },
tx tx
); );
@@ -683,9 +1032,12 @@ export const certificateAuthorityServiceFactory = ({
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Create, ProjectPermissionSub.Certificates); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Create, ProjectPermissionSub.Certificates);
if (ca.status === CaStatus.DISABLED) throw new BadRequestError({ message: "CA is disabled" }); if (ca.status === CaStatus.DISABLED) throw new BadRequestError({ message: "CA is disabled" });
if (!ca.activeCaCertId) throw new BadRequestError({ message: "CA does not have a certificate installed" });
const caCert = await certificateAuthorityCertDAL.findById(ca.activeCaCertId);
const caCert = await certificateAuthorityCertDAL.findOne({ caId: ca.id }); if (ca.notAfter && new Date() > new Date(ca.notAfter)) {
if (!caCert) throw new BadRequestError({ message: "CA does not have a certificate installed" }); throw new BadRequestError({ message: "CA is expired" });
}
const certificateManagerKmsId = await getProjectKmsCertificateKeyId({ const certificateManagerKmsId = await getProjectKmsCertificateKeyId({
projectId: ca.projectId, projectId: ca.projectId,
@@ -814,6 +1166,7 @@ export const certificateAuthorityServiceFactory = ({
const cert = await certificateDAL.create( const cert = await certificateDAL.create(
{ {
caId: ca.id, caId: ca.id,
caCertId: caCert.id,
status: CertStatus.ACTIVE, status: CertStatus.ACTIVE,
friendlyName: friendlyName || commonName, friendlyName: friendlyName || commonName,
commonName, commonName,
@@ -837,7 +1190,7 @@ export const certificateAuthorityServiceFactory = ({
}); });
const { caCert: issuingCaCertificate, caCertChain } = await getCaCertChain({ const { caCert: issuingCaCertificate, caCertChain } = await getCaCertChain({
caId: ca.id, caCertId: caCert.id,
certificateAuthorityDAL, certificateAuthorityDAL,
certificateAuthorityCertDAL, certificateAuthorityCertDAL,
projectDAL, projectDAL,
@@ -886,9 +1239,13 @@ export const certificateAuthorityServiceFactory = ({
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Create, ProjectPermissionSub.Certificates); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Create, ProjectPermissionSub.Certificates);
if (ca.status === CaStatus.DISABLED) throw new BadRequestError({ message: "CA is disabled" }); if (ca.status === CaStatus.DISABLED) throw new BadRequestError({ message: "CA is disabled" });
if (!ca.activeCaCertId) throw new BadRequestError({ message: "CA does not have a certificate installed" });
const caCert = await certificateAuthorityCertDAL.findOne({ caId: ca.id }); const caCert = await certificateAuthorityCertDAL.findById(ca.activeCaCertId);
if (!caCert) throw new BadRequestError({ message: "CA does not have a certificate installed" });
if (ca.notAfter && new Date() > new Date(ca.notAfter)) {
throw new BadRequestError({ message: "CA is expired" });
}
const certificateManagerKmsId = await getProjectKmsCertificateKeyId({ const certificateManagerKmsId = await getProjectKmsCertificateKeyId({
projectId: ca.projectId, projectId: ca.projectId,
@@ -1013,6 +1370,7 @@ export const certificateAuthorityServiceFactory = ({
const cert = await certificateDAL.create( const cert = await certificateDAL.create(
{ {
caId: ca.id, caId: ca.id,
caCertId: caCert.id,
status: CertStatus.ACTIVE, status: CertStatus.ACTIVE,
friendlyName: friendlyName || csrObj.subject, friendlyName: friendlyName || csrObj.subject,
commonName: cn, commonName: cn,
@@ -1036,7 +1394,7 @@ export const certificateAuthorityServiceFactory = ({
}); });
const { caCert: issuingCaCertificate, caCertChain } = await getCaCertChain({ const { caCert: issuingCaCertificate, caCertChain } = await getCaCertChain({
caId: ca.id, caCertId: ca.activeCaCertId,
certificateAuthorityDAL, certificateAuthorityDAL,
certificateAuthorityCertDAL, certificateAuthorityCertDAL,
projectDAL, projectDAL,
@@ -1058,6 +1416,8 @@ export const certificateAuthorityServiceFactory = ({
updateCaById, updateCaById,
deleteCaById, deleteCaById,
getCaCsr, getCaCsr,
renewCaCert,
getCaCerts,
getCaCert, getCaCert,
signIntermediate, signIntermediate,
importCertToCa, importCertToCa,
@@ -20,6 +20,10 @@ export enum CaStatus {
PENDING_CERTIFICATE = "pending-certificate" PENDING_CERTIFICATE = "pending-certificate"
} }
export enum CaRenewalType {
EXISTING = "existing"
}
export type TCreateCaDTO = { export type TCreateCaDTO = {
projectSlug: string; projectSlug: string;
type: CaType; type: CaType;
@@ -53,6 +57,16 @@ export type TGetCaCsrDTO = {
caId: string; caId: string;
} & Omit<TProjectPermission, "projectId">; } & Omit<TProjectPermission, "projectId">;
export type TRenewCaCertDTO = {
caId: string;
notAfter: string;
type: CaRenewalType;
} & Omit<TProjectPermission, "projectId">;
export type TGetCaCertsDTO = {
caId: string;
} & Omit<TProjectPermission, "projectId">;
export type TGetCaCertDTO = { export type TGetCaCertDTO = {
caId: string; caId: string;
} & Omit<TProjectPermission, "projectId">; } & Omit<TProjectPermission, "projectId">;
@@ -109,10 +123,18 @@ export type TGetCaCredentialsDTO = {
kmsService: Pick<TKmsServiceFactory, "decryptWithKmsKey" | "generateKmsKey">; kmsService: Pick<TKmsServiceFactory, "decryptWithKmsKey" | "generateKmsKey">;
}; };
export type TGetCaCertChainDTO = { export type TGetCaCertChainsDTO = {
caId: string; caId: string;
certificateAuthorityDAL: Pick<TCertificateAuthorityDALFactory, "findById">; certificateAuthorityDAL: Pick<TCertificateAuthorityDALFactory, "findById">;
certificateAuthorityCertDAL: Pick<TCertificateAuthorityCertDALFactory, "findOne">; certificateAuthorityCertDAL: Pick<TCertificateAuthorityCertDALFactory, "find">;
projectDAL: Pick<TProjectDALFactory, "findOne" | "updateById" | "transaction">;
kmsService: Pick<TKmsServiceFactory, "decryptWithKmsKey" | "generateKmsKey">;
};
export type TGetCaCertChainDTO = {
caCertId: string;
certificateAuthorityDAL: Pick<TCertificateAuthorityDALFactory, "findById">;
certificateAuthorityCertDAL: Pick<TCertificateAuthorityCertDALFactory, "findById">;
projectDAL: Pick<TProjectDALFactory, "findOne" | "updateById" | "transaction">; projectDAL: Pick<TProjectDALFactory, "findOne" | "updateById" | "transaction">;
kmsService: Pick<TKmsServiceFactory, "decryptWithKmsKey" | "generateKmsKey">; kmsService: Pick<TKmsServiceFactory, "decryptWithKmsKey" | "generateKmsKey">;
}; };
@@ -21,7 +21,7 @@ type TCertificateServiceFactoryDep = {
certificateDAL: Pick<TCertificateDALFactory, "findOne" | "deleteById" | "update" | "find">; certificateDAL: Pick<TCertificateDALFactory, "findOne" | "deleteById" | "update" | "find">;
certificateBodyDAL: Pick<TCertificateBodyDALFactory, "findOne">; certificateBodyDAL: Pick<TCertificateBodyDALFactory, "findOne">;
certificateAuthorityDAL: Pick<TCertificateAuthorityDALFactory, "findById">; certificateAuthorityDAL: Pick<TCertificateAuthorityDALFactory, "findById">;
certificateAuthorityCertDAL: Pick<TCertificateAuthorityCertDALFactory, "findOne">; certificateAuthorityCertDAL: Pick<TCertificateAuthorityCertDALFactory, "findById">;
certificateAuthorityCrlDAL: Pick<TCertificateAuthorityCrlDALFactory, "update">; certificateAuthorityCrlDAL: Pick<TCertificateAuthorityCrlDALFactory, "update">;
certificateAuthoritySecretDAL: Pick<TCertificateAuthoritySecretDALFactory, "findOne">; certificateAuthoritySecretDAL: Pick<TCertificateAuthoritySecretDALFactory, "findOne">;
projectDAL: Pick<TProjectDALFactory, "findOne" | "updateById" | "findById" | "transaction">; projectDAL: Pick<TProjectDALFactory, "findOne" | "updateById" | "findById" | "transaction">;
@@ -180,7 +180,7 @@ export const certificateServiceFactory = ({
const certObj = new x509.X509Certificate(decryptedCert); const certObj = new x509.X509Certificate(decryptedCert);
const { caCert, caCertChain } = await getCaCertChain({ const { caCert, caCertChain } = await getCaCertChain({
caId: ca.id, caCertId: cert.caCertId,
certificateAuthorityDAL, certificateAuthorityDAL,
certificateAuthorityCertDAL, certificateAuthorityCertDAL,
projectDAL, projectDAL,
@@ -0,0 +1,4 @@
---
title: "List CA certificates"
openapi: "GET /api/v1/pki/ca/{caId}/ca-certificates"
---
@@ -0,0 +1,4 @@
---
title: "Renew"
openapi: "POST /api/v1/pki/ca/{caId}/renew"
---
+50 -1
View File
@@ -36,7 +36,7 @@ A typical workflow for setting up a Private CA hierarchy consists of the followi
intermediate certificate back to the intermediate CA as part of Step 2. intermediate certificate back to the intermediate CA as part of Step 2.
</Note> </Note>
## Guide ## Guide to Creating a CA Hierarchy
In the following steps, we explore how to create a simple Private CA hierarchy In the following steps, we explore how to create a simple Private CA hierarchy
consisting of a root CA and an intermediate CA. consisting of a root CA and an intermediate CA.
@@ -240,6 +240,51 @@ consisting of a root CA and an intermediate CA.
</Tab> </Tab>
</Tabs> </Tabs>
## Guide to CA Renewal
In the following steps, we explore how to renew a CA certificate via same key pair.
<Tabs>
<Tab title="Infisical UI">
Head to the CA Page of the CA you wish you renew and press **Renew CA** on
the left side. ![pki ca renewal
page](/images/platform/pki/ca-renewal-page.png) Input a new **Valid Until**
date to be used for the renewed CA certificate and press **Renew** to renew
the CA. ![pki ca renewal. modal](/images/platform/pki/ca-renewal-modal.png)
<Note>
The new **Valid Until** date must be within the validity period of the
parent CA.
</Note>
</Tab>
<Tab title="API">
To renew a CA certificate, make an API request to the [Renew CA](/api-reference/endpoints/certificate-authorities/renew) API endpoint, specifying the new `notAfter` date for the CA.
### Sample request
```bash Request
curl --location --request POST 'https://app.infisical.com/api/v1/pki/ca/<ca-id>/renew' \
--header 'Authorization: Bearer <access-token>' \
--header 'Content-Type: application/json' \
--data-raw '{
"type": "existing",
"notAfter": "2029-06-12"
}'
```
### Sample response
```bash Response
{
certificate: "...",
certificateChain: "...",
serialNumber: "..."
}
```
</Tab>
</Tabs>
## FAQ ## FAQ
<AccordionGroup> <AccordionGroup>
@@ -247,4 +292,8 @@ consisting of a root CA and an intermediate CA.
Infisical supports `RSA 2048`, `RSA 4096`, `ECDSA P-256`, `ECDSA P-384` key Infisical supports `RSA 2048`, `RSA 4096`, `ECDSA P-256`, `ECDSA P-384` key
algorithms specified at the time of creating a CA. algorithms specified at the time of creating a CA.
</Accordion> </Accordion>
<Accordion title="Does Infisical support CA renewal via new key pair">
At the moment, Infisical only supports CA renewal via same key pair. We
anticipate supporting CA renewal via new key pair in the coming month.
</Accordion>
</AccordionGroup> </AccordionGroup>
Binary file not shown.

After

Width:  |  Height:  |  Size: 408 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 584 KiB

+2
View File
@@ -682,6 +682,8 @@
"api-reference/endpoints/certificate-authorities/read", "api-reference/endpoints/certificate-authorities/read",
"api-reference/endpoints/certificate-authorities/update", "api-reference/endpoints/certificate-authorities/update",
"api-reference/endpoints/certificate-authorities/delete", "api-reference/endpoints/certificate-authorities/delete",
"api-reference/endpoints/certificate-authorities/renew",
"api-reference/endpoints/certificate-authorities/list-ca-certs",
"api-reference/endpoints/certificate-authorities/csr", "api-reference/endpoints/certificate-authorities/csr",
"api-reference/endpoints/certificate-authorities/cert", "api-reference/endpoints/certificate-authorities/cert",
"api-reference/endpoints/certificate-authorities/sign-intermediate", "api-reference/endpoints/certificate-authorities/sign-intermediate",
+194
View File
@@ -22,6 +22,7 @@
"@headlessui/react": "^1.7.7", "@headlessui/react": "^1.7.7",
"@hookform/resolvers": "^2.9.10", "@hookform/resolvers": "^2.9.10",
"@octokit/rest": "^19.0.7", "@octokit/rest": "^19.0.7",
"@peculiar/x509": "^1.11.0",
"@radix-ui/react-accordion": "^1.1.2", "@radix-ui/react-accordion": "^1.1.2",
"@radix-ui/react-alert-dialog": "^1.0.5", "@radix-ui/react-alert-dialog": "^1.0.5",
"@radix-ui/react-checkbox": "^1.0.4", "@radix-ui/react-checkbox": "^1.0.4",
@@ -4520,6 +4521,149 @@
"@octokit/openapi-types": "^18.0.0" "@octokit/openapi-types": "^18.0.0"
} }
}, },
"node_modules/@peculiar/asn1-cms": {
"version": "2.3.13",
"resolved": "https://registry.npmjs.org/@peculiar/asn1-cms/-/asn1-cms-2.3.13.tgz",
"integrity": "sha512-joqu8A7KR2G85oLPq+vB+NFr2ro7Ls4ol13Zcse/giPSzUNN0n2k3v8kMpf6QdGUhI13e5SzQYN8AKP8sJ8v4w==",
"dependencies": {
"@peculiar/asn1-schema": "^2.3.13",
"@peculiar/asn1-x509": "^2.3.13",
"@peculiar/asn1-x509-attr": "^2.3.13",
"asn1js": "^3.0.5",
"tslib": "^2.6.2"
}
},
"node_modules/@peculiar/asn1-csr": {
"version": "2.3.13",
"resolved": "https://registry.npmjs.org/@peculiar/asn1-csr/-/asn1-csr-2.3.13.tgz",
"integrity": "sha512-+JtFsOUWCw4zDpxp1LbeTYBnZLlGVOWmHHEhoFdjM5yn4wCn+JiYQ8mghOi36M2f6TPQ17PmhNL6/JfNh7/jCA==",
"dependencies": {
"@peculiar/asn1-schema": "^2.3.13",
"@peculiar/asn1-x509": "^2.3.13",
"asn1js": "^3.0.5",
"tslib": "^2.6.2"
}
},
"node_modules/@peculiar/asn1-ecc": {
"version": "2.3.13",
"resolved": "https://registry.npmjs.org/@peculiar/asn1-ecc/-/asn1-ecc-2.3.13.tgz",
"integrity": "sha512-3dF2pQcrN/WJEMq+9qWLQ0gqtn1G81J4rYqFl6El6QV367b4IuhcRv+yMA84tNNyHOJn9anLXV5radnpPiG3iA==",
"dependencies": {
"@peculiar/asn1-schema": "^2.3.13",
"@peculiar/asn1-x509": "^2.3.13",
"asn1js": "^3.0.5",
"tslib": "^2.6.2"
}
},
"node_modules/@peculiar/asn1-pfx": {
"version": "2.3.13",
"resolved": "https://registry.npmjs.org/@peculiar/asn1-pfx/-/asn1-pfx-2.3.13.tgz",
"integrity": "sha512-fypYxjn16BW+5XbFoY11Rm8LhZf6euqX/C7BTYpqVvLem1GvRl7A+Ro1bO/UPwJL0z+1mbvXEnkG0YOwbwz2LA==",
"dependencies": {
"@peculiar/asn1-cms": "^2.3.13",
"@peculiar/asn1-pkcs8": "^2.3.13",
"@peculiar/asn1-rsa": "^2.3.13",
"@peculiar/asn1-schema": "^2.3.13",
"asn1js": "^3.0.5",
"tslib": "^2.6.2"
}
},
"node_modules/@peculiar/asn1-pkcs8": {
"version": "2.3.13",
"resolved": "https://registry.npmjs.org/@peculiar/asn1-pkcs8/-/asn1-pkcs8-2.3.13.tgz",
"integrity": "sha512-VP3PQzbeSSjPjKET5K37pxyf2qCdM0dz3DJ56ZCsol3FqAXGekb4sDcpoL9uTLGxAh975WcdvUms9UcdZTuGyQ==",
"dependencies": {
"@peculiar/asn1-schema": "^2.3.13",
"@peculiar/asn1-x509": "^2.3.13",
"asn1js": "^3.0.5",
"tslib": "^2.6.2"
}
},
"node_modules/@peculiar/asn1-pkcs9": {
"version": "2.3.13",
"resolved": "https://registry.npmjs.org/@peculiar/asn1-pkcs9/-/asn1-pkcs9-2.3.13.tgz",
"integrity": "sha512-rIwQXmHpTo/dgPiWqUgby8Fnq6p1xTJbRMxCiMCk833kQCeZrC5lbSKg6NDnJTnX2kC6IbXBB9yCS2C73U2gJg==",
"dependencies": {
"@peculiar/asn1-cms": "^2.3.13",
"@peculiar/asn1-pfx": "^2.3.13",
"@peculiar/asn1-pkcs8": "^2.3.13",
"@peculiar/asn1-schema": "^2.3.13",
"@peculiar/asn1-x509": "^2.3.13",
"@peculiar/asn1-x509-attr": "^2.3.13",
"asn1js": "^3.0.5",
"tslib": "^2.6.2"
}
},
"node_modules/@peculiar/asn1-rsa": {
"version": "2.3.13",
"resolved": "https://registry.npmjs.org/@peculiar/asn1-rsa/-/asn1-rsa-2.3.13.tgz",
"integrity": "sha512-wBNQqCyRtmqvXkGkL4DR3WxZhHy8fDiYtOjTeCd7SFE5F6GBeafw3EJ94PX/V0OJJrjQ40SkRY2IZu3ZSyBqcg==",
"dependencies": {
"@peculiar/asn1-schema": "^2.3.13",
"@peculiar/asn1-x509": "^2.3.13",
"asn1js": "^3.0.5",
"tslib": "^2.6.2"
}
},
"node_modules/@peculiar/asn1-schema": {
"version": "2.3.13",
"resolved": "https://registry.npmjs.org/@peculiar/asn1-schema/-/asn1-schema-2.3.13.tgz",
"integrity": "sha512-3Xq3a01WkHRZL8X04Zsfg//mGaA21xlL4tlVn4v2xGT0JStiztATRkMwa5b+f/HXmY2smsiLXYK46Gwgzvfg3g==",
"dependencies": {
"asn1js": "^3.0.5",
"pvtsutils": "^1.3.5",
"tslib": "^2.6.2"
}
},
"node_modules/@peculiar/asn1-x509": {
"version": "2.3.13",
"resolved": "https://registry.npmjs.org/@peculiar/asn1-x509/-/asn1-x509-2.3.13.tgz",
"integrity": "sha512-PfeLQl2skXmxX2/AFFCVaWU8U6FKW1Db43mgBhShCOFS1bVxqtvusq1hVjfuEcuSQGedrLdCSvTgabluwN/M9A==",
"dependencies": {
"@peculiar/asn1-schema": "^2.3.13",
"asn1js": "^3.0.5",
"ipaddr.js": "^2.1.0",
"pvtsutils": "^1.3.5",
"tslib": "^2.6.2"
}
},
"node_modules/@peculiar/asn1-x509-attr": {
"version": "2.3.13",
"resolved": "https://registry.npmjs.org/@peculiar/asn1-x509-attr/-/asn1-x509-attr-2.3.13.tgz",
"integrity": "sha512-WpEos6CcnUzJ6o2Qb68Z7Dz5rSjRGv/DtXITCNBtjZIRWRV12yFVci76SVfOX8sisL61QWMhpLKQibrG8pi2Pw==",
"dependencies": {
"@peculiar/asn1-schema": "^2.3.13",
"@peculiar/asn1-x509": "^2.3.13",
"asn1js": "^3.0.5",
"tslib": "^2.6.2"
}
},
"node_modules/@peculiar/asn1-x509/node_modules/ipaddr.js": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-2.2.0.tgz",
"integrity": "sha512-Ag3wB2o37wslZS19hZqorUnrnzSkpOVy+IiiDEiTqNubEYpYuHWIf6K4psgN2ZWKExS4xhVCrRVfb/wfW8fWJA==",
"engines": {
"node": ">= 10"
}
},
"node_modules/@peculiar/x509": {
"version": "1.11.0",
"resolved": "https://registry.npmjs.org/@peculiar/x509/-/x509-1.11.0.tgz",
"integrity": "sha512-8rdxE//tsWLb2Yo2TYO2P8gieStbrHK/huFMV5PPfwX8I5HmtOus+Ox6nTKrPA9o+WOPaa5xKenee+QdmHBd5g==",
"dependencies": {
"@peculiar/asn1-cms": "^2.3.8",
"@peculiar/asn1-csr": "^2.3.8",
"@peculiar/asn1-ecc": "^2.3.8",
"@peculiar/asn1-pkcs9": "^2.3.8",
"@peculiar/asn1-rsa": "^2.3.8",
"@peculiar/asn1-schema": "^2.3.8",
"@peculiar/asn1-x509": "^2.3.8",
"pvtsutils": "^1.3.5",
"reflect-metadata": "^0.2.2",
"tslib": "^2.6.2",
"tsyringe": "^4.8.0"
}
},
"node_modules/@pkgjs/parseargs": { "node_modules/@pkgjs/parseargs": {
"version": "0.11.0", "version": "0.11.0",
"resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz",
@@ -9870,6 +10014,19 @@
"integrity": "sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA==", "integrity": "sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA==",
"dev": true "dev": true
}, },
"node_modules/asn1js": {
"version": "3.0.5",
"resolved": "https://registry.npmjs.org/asn1js/-/asn1js-3.0.5.tgz",
"integrity": "sha512-FVnvrKJwpt9LP2lAMl8qZswRNm3T4q9CON+bxldk2iwk3FFpuwhx2FfinyitizWHsVYyaY+y5JzDR0rCMV5yTQ==",
"dependencies": {
"pvtsutils": "^1.3.2",
"pvutils": "^1.1.3",
"tslib": "^2.4.0"
},
"engines": {
"node": ">=12.0.0"
}
},
"node_modules/assert": { "node_modules/assert": {
"version": "2.1.0", "version": "2.1.0",
"resolved": "https://registry.npmjs.org/assert/-/assert-2.1.0.tgz", "resolved": "https://registry.npmjs.org/assert/-/assert-2.1.0.tgz",
@@ -20320,6 +20477,22 @@
"async-limiter": "~1.0.0" "async-limiter": "~1.0.0"
} }
}, },
"node_modules/pvtsutils": {
"version": "1.3.5",
"resolved": "https://registry.npmjs.org/pvtsutils/-/pvtsutils-1.3.5.tgz",
"integrity": "sha512-ARvb14YB9Nm2Xi6nBq1ZX6dAM0FsJnuk+31aUp4TrcZEdKUlSqOqsxJHUPJDNE3qiIp+iUPEIeR6Je/tgV7zsA==",
"dependencies": {
"tslib": "^2.6.1"
}
},
"node_modules/pvutils": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/pvutils/-/pvutils-1.1.3.tgz",
"integrity": "sha512-pMpnA0qRdFp32b1sJl1wOJNxZLQ2cbQx+k6tjNtZ8CpvVhNqEPRgivZ2WOUev2YMajecdH7ctUPDvEe87nariQ==",
"engines": {
"node": ">=6.0.0"
}
},
"node_modules/qs": { "node_modules/qs": {
"version": "6.11.2", "version": "6.11.2",
"resolved": "https://registry.npmjs.org/qs/-/qs-6.11.2.tgz", "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.2.tgz",
@@ -21186,6 +21359,11 @@
"redux": "^4" "redux": "^4"
} }
}, },
"node_modules/reflect-metadata": {
"version": "0.2.2",
"resolved": "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.2.2.tgz",
"integrity": "sha512-urBwgfrvVP/eAyXx4hluJivBKzuEbSQs9rKWCrCkbSxNv8mxPcUZKeuoF3Uy4mJl3Lwprp6yy5/39VWigZ4K6Q=="
},
"node_modules/reflect.getprototypeof": { "node_modules/reflect.getprototypeof": {
"version": "1.0.4", "version": "1.0.4",
"resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.4.tgz", "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.4.tgz",
@@ -23573,6 +23751,22 @@
"integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==",
"dev": true "dev": true
}, },
"node_modules/tsyringe": {
"version": "4.8.0",
"resolved": "https://registry.npmjs.org/tsyringe/-/tsyringe-4.8.0.tgz",
"integrity": "sha512-YB1FG+axdxADa3ncEtRnQCFq/M0lALGLxSZeVNbTU8NqhOVc51nnv2CISTcvc1kyv6EGPtXVr0v6lWeDxiijOA==",
"dependencies": {
"tslib": "^1.9.3"
},
"engines": {
"node": ">= 6.0.0"
}
},
"node_modules/tsyringe/node_modules/tslib": {
"version": "1.14.1",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz",
"integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg=="
},
"node_modules/tty-browserify": { "node_modules/tty-browserify": {
"version": "0.0.1", "version": "0.0.1",
"resolved": "https://registry.npmjs.org/tty-browserify/-/tty-browserify-0.0.1.tgz", "resolved": "https://registry.npmjs.org/tty-browserify/-/tty-browserify-0.0.1.tgz",
+1
View File
@@ -30,6 +30,7 @@
"@headlessui/react": "^1.7.7", "@headlessui/react": "^1.7.7",
"@hookform/resolvers": "^2.9.10", "@hookform/resolvers": "^2.9.10",
"@octokit/rest": "^19.0.7", "@octokit/rest": "^19.0.7",
"@peculiar/x509": "^1.11.0",
"@radix-ui/react-accordion": "^1.1.2", "@radix-ui/react-accordion": "^1.1.2",
"@radix-ui/react-alert-dialog": "^1.0.5", "@radix-ui/react-alert-dialog": "^1.0.5",
"@radix-ui/react-checkbox": "^1.0.4", "@radix-ui/react-checkbox": "^1.0.4",
+1
View File
@@ -1,5 +1,6 @@
export * from "./Accordion"; export * from "./Accordion";
export * from "./Alert"; export * from "./Alert";
export * from "./Badge";
export * from "./Button"; export * from "./Button";
export * from "./Card"; export * from "./Card";
export * from "./Checkbox"; export * from "./Checkbox";
+12 -1
View File
@@ -1,4 +1,4 @@
import { CaStatus,CaType } from "./enums"; import { CaStatus, CaType } from "./enums";
export const caTypeToNameMap: { [K in CaType]: string } = { export const caTypeToNameMap: { [K in CaType]: string } = {
[CaType.ROOT]: "Root", [CaType.ROOT]: "Root",
@@ -10,3 +10,14 @@ export const caStatusToNameMap: { [K in CaStatus]: string } = {
[CaStatus.DISABLED]: "Disabled", [CaStatus.DISABLED]: "Disabled",
[CaStatus.PENDING_CERTIFICATE]: "Pending Certificate" [CaStatus.PENDING_CERTIFICATE]: "Pending Certificate"
}; };
export const getStatusBadgeVariant = (status: CaStatus) => {
switch (status) {
case CaStatus.ACTIVE:
return "success";
case CaStatus.DISABLED:
return "danger";
default:
return "primary";
}
};
+4
View File
@@ -8,3 +8,7 @@ export enum CaStatus {
DISABLED = "disabled", DISABLED = "disabled",
PENDING_CERTIFICATE = "pending-certificate" PENDING_CERTIFICATE = "pending-certificate"
} }
export enum CaRenewalType {
EXISTING = "existing"
}
+4 -4
View File
@@ -1,10 +1,10 @@
export { CaStatus, CaType } from "./enums"; export { CaRenewalType,CaStatus, CaType } from "./enums";
export { export {
useCreateCa, useCreateCa,
useCreateCertificate, useCreateCertificate,
useDeleteCa, useDeleteCa,
useImportCaCertificate, useImportCaCertificate,
useRenewCa,
useSignIntermediate, useSignIntermediate,
useUpdateCa useUpdateCa} from "./mutations";
} from "./mutations"; export { useGetCaById, useGetCaCert, useGetCaCerts, useGetCaCrl, useGetCaCsr } from "./queries";
export { useGetCaById, useGetCaCert, useGetCaCrl,useGetCaCsr } from "./queries";
+27 -1
View File
@@ -3,6 +3,7 @@ import { useMutation, useQueryClient } from "@tanstack/react-query";
import { apiRequest } from "@app/config/request"; import { apiRequest } from "@app/config/request";
import { workspaceKeys } from "../workspace/queries"; import { workspaceKeys } from "../workspace/queries";
import { caKeys } from "./queries";
import { import {
TCertificateAuthority, TCertificateAuthority,
TCreateCaDTO, TCreateCaDTO,
@@ -11,6 +12,8 @@ import {
TDeleteCaDTO, TDeleteCaDTO,
TImportCaCertificateDTO, TImportCaCertificateDTO,
TImportCaCertificateResponse, TImportCaCertificateResponse,
TRenewCaDTO,
TRenewCaResponse,
TSignIntermediateDTO, TSignIntermediateDTO,
TSignIntermediateResponse, TSignIntermediateResponse,
TUpdateCaDTO TUpdateCaDTO
@@ -84,8 +87,10 @@ export const useImportCaCertificate = () => {
); );
return data; return data;
}, },
onSuccess: (_, { projectSlug }) => { onSuccess: (_, { caId, projectSlug }) => {
queryClient.invalidateQueries(workspaceKeys.getWorkspaceCas({ projectSlug })); queryClient.invalidateQueries(workspaceKeys.getWorkspaceCas({ projectSlug }));
queryClient.invalidateQueries(caKeys.getCaCerts(caId));
queryClient.invalidateQueries(caKeys.getCaCert(caId));
} }
}); });
}; };
@@ -106,3 +111,24 @@ export const useCreateCertificate = () => {
} }
}); });
}; };
export const useRenewCa = () => {
const queryClient = useQueryClient();
return useMutation<TRenewCaResponse, {}, TRenewCaDTO>({
mutationFn: async (body) => {
const { data } = await apiRequest.post<TRenewCaResponse>(
`/api/v1/pki/ca/${body.caId}/renew`,
body
);
return data;
},
onSuccess: (_, { caId, projectSlug }) => {
queryClient.invalidateQueries(workspaceKeys.getWorkspaceCas({ projectSlug }));
queryClient.invalidateQueries(caKeys.getCaById(caId));
queryClient.invalidateQueries(caKeys.getCaCert(caId));
queryClient.invalidateQueries(caKeys.getCaCerts(caId));
queryClient.invalidateQueries(caKeys.getCaCsr(caId));
queryClient.invalidateQueries(caKeys.getCaCrl(caId));
}
});
};
+20 -1
View File
@@ -6,6 +6,7 @@ import { TCertificateAuthority } from "./types";
export const caKeys = { export const caKeys = {
getCaById: (caId: string) => [{ caId }, "ca"], getCaById: (caId: string) => [{ caId }, "ca"],
getCaCerts: (caId: string) => [{ caId }, "ca-cert"],
getCaCert: (caId: string) => [{ caId }, "ca-cert"], getCaCert: (caId: string) => [{ caId }, "ca-cert"],
getCaCsr: (caId: string) => [{ caId }, "ca-csr"], getCaCsr: (caId: string) => [{ caId }, "ca-csr"],
getCaCrl: (caId: string) => [{ caId }, "ca-crl"] getCaCrl: (caId: string) => [{ caId }, "ca-crl"]
@@ -24,6 +25,24 @@ export const useGetCaById = (caId: string) => {
}); });
}; };
export const useGetCaCerts = (caId: string) => {
return useQuery({
queryKey: caKeys.getCaCerts(caId),
queryFn: async () => {
const { data } = await apiRequest.get<
{
certificate: string;
certificateChain: string;
serialNumber: string;
version: number;
}[]
>(`/api/v1/pki/ca/${caId}/ca-certificates`); // TODO: consider updating endpoint structure
return data;
},
enabled: Boolean(caId)
});
};
export const useGetCaCert = (caId: string) => { export const useGetCaCert = (caId: string) => {
return useQuery({ return useQuery({
queryKey: caKeys.getCaCert(caId), queryKey: caKeys.getCaCert(caId),
@@ -32,7 +51,7 @@ export const useGetCaCert = (caId: string) => {
certificate: string; certificate: string;
certificateChain: string; certificateChain: string;
serialNumber: string; serialNumber: string;
}>(`/api/v1/pki/ca/${caId}/certificate`); }>(`/api/v1/pki/ca/${caId}/certificate`); // TODO: consider updating endpoint structure
return data; return data;
}, },
enabled: Boolean(caId) enabled: Boolean(caId)
+15 -1
View File
@@ -1,5 +1,5 @@
import { CertKeyAlgorithm } from "../certificates/enums"; import { CertKeyAlgorithm } from "../certificates/enums";
import { CaStatus, CaType } from "./enums"; import { CaRenewalType, CaStatus, CaType } from "./enums";
export type TCertificateAuthority = { export type TCertificateAuthority = {
id: string; id: string;
@@ -19,6 +19,7 @@ export type TCertificateAuthority = {
notAfter?: string; notAfter?: string;
notBefore?: string; notBefore?: string;
keyAlgorithm: CertKeyAlgorithm; keyAlgorithm: CertKeyAlgorithm;
activeCaCertId?: string;
createdAt: string; createdAt: string;
updatedAt: string; updatedAt: string;
}; };
@@ -94,3 +95,16 @@ export type TCreateCertificateResponse = {
privateKey: string; privateKey: string;
serialNumber: string; serialNumber: string;
}; };
export type TRenewCaDTO = {
projectSlug: string;
caId: string;
type: CaRenewalType;
notAfter: string;
};
export type TRenewCaResponse = {
certificate: string;
certificateChain: string;
serialNumber: string;
};
@@ -0,0 +1,18 @@
/* eslint-disable @typescript-eslint/no-unused-vars */
import Head from "next/head";
import { CaPage } from "@app/views/Project/CaPage";
export default function Ca() {
return (
<>
<Head>
<title>Certificate Authority</title>
<link rel="icon" href="/infisical.ico" />
</Head>
<CaPage />
</>
);
}
Ca.requireAuth = true;
@@ -0,0 +1,145 @@
/* eslint-disable @typescript-eslint/no-unused-vars */
import { useRouter } from "next/router";
import { faChevronLeft, faEllipsis } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { twMerge } from "tailwind-merge";
import { createNotification } from "@app/components/notifications";
import { ProjectPermissionCan } from "@app/components/permissions";
import {
Button,
DeleteActionModal,
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
Tooltip
} from "@app/components/v2";
import { ProjectPermissionActions, ProjectPermissionSub, useWorkspace } from "@app/context";
import { withProjectPermission } from "@app/hoc";
import { useDeleteCa, useGetCaById } from "@app/hooks/api";
import { usePopUp } from "@app/hooks/usePopUp";
import { CaModal } from "@app/views/Project/CertificatesPage/components/CaTab/components/CaModal";
import { CaInstallCertModal } from "../CertificatesPage/components/CaTab/components/CaInstallCertModal";
import { TabSections } from "../Types";
import { CaCertificatesSection, CaDetailsSection, CaRenewalModal } from "./components";
export const CaPage = withProjectPermission(
() => {
const router = useRouter();
const caId = router.query.caId as string;
const { data } = useGetCaById(caId);
const { currentWorkspace } = useWorkspace();
const projectId = currentWorkspace?.id || "";
const { mutateAsync: deleteCa } = useDeleteCa();
const { popUp, handlePopUpOpen, handlePopUpClose, handlePopUpToggle } = usePopUp([
"ca",
"deleteCa",
"installCaCert",
"renewCa"
] as const);
const onRemoveCaSubmit = async (caIdToDelete: string) => {
try {
if (!currentWorkspace?.slug) return;
await deleteCa({ caId: caIdToDelete, projectSlug: currentWorkspace.slug });
await createNotification({
text: "Successfully deleted CA",
type: "success"
});
handlePopUpClose("deleteCa");
router.push(`/project/${projectId}/certificates`);
} catch (err) {
console.error(err);
createNotification({
text: "Failed to delete CA",
type: "error"
});
}
};
return (
<div className="container mx-auto flex flex-col justify-between bg-bunker-800 text-white">
{data && (
<div className="mx-auto mb-6 w-full max-w-7xl py-6 px-6">
<Button
variant="link"
type="submit"
leftIcon={<FontAwesomeIcon icon={faChevronLeft} />}
onClick={() => router.push(`/project/${projectId}/certificates`)}
className="mb-4"
>
Certificate Authorities
</Button>
<div className="mb-4 flex items-center justify-between">
<p className="text-3xl font-semibold text-white">{data.friendlyName}</p>
<DropdownMenu>
<DropdownMenuTrigger asChild className="rounded-lg">
<div className="hover:text-primary-400 data-[state=open]:text-primary-400">
<Tooltip content="More options">
<FontAwesomeIcon size="sm" icon={faEllipsis} />
</Tooltip>
</div>
</DropdownMenuTrigger>
<DropdownMenuContent align="start" className="p-1">
<ProjectPermissionCan
I={ProjectPermissionActions.Delete}
a={ProjectPermissionSub.CertificateAuthorities}
>
{(isAllowed) => (
<DropdownMenuItem
className={twMerge(
isAllowed
? "hover:!bg-red-500 hover:!text-white"
: "pointer-events-none cursor-not-allowed opacity-50"
)}
onClick={() =>
handlePopUpOpen("deleteCa", {
caId: data.id,
dn: data.dn
})
}
disabled={!isAllowed}
>
Delete CA
</DropdownMenuItem>
)}
</ProjectPermissionCan>
</DropdownMenuContent>
</DropdownMenu>
</div>
<div className="flex">
<div className="mr-4 w-96">
<CaDetailsSection caId={caId} handlePopUpOpen={handlePopUpOpen} />
</div>
<CaCertificatesSection caId={caId} />
</div>
</div>
)}
<CaModal popUp={popUp} handlePopUpToggle={handlePopUpToggle} />
<CaRenewalModal popUp={popUp} handlePopUpToggle={handlePopUpToggle} />
<CaInstallCertModal popUp={popUp} handlePopUpToggle={handlePopUpToggle} />
<DeleteActionModal
isOpen={popUp.deleteCa.isOpen}
title={`Are you sure want to remove the CA ${
(popUp?.deleteCa?.data as { dn: string })?.dn || ""
} from the project?`}
subTitle="This action will delete other CAs and certificates below it in your CA hierarchy."
onChange={(isOpen) => handlePopUpToggle("deleteCa", isOpen)}
deleteKey="confirm"
onDeleteApproved={() =>
onRemoveCaSubmit((popUp?.deleteCa?.data as { caId: string })?.caId)
}
/>
</div>
);
},
{ action: ProjectPermissionActions.Read, subject: ProjectPermissionSub.CertificateAuthorities }
);
@@ -0,0 +1,31 @@
// import { faPlus } from "@fortawesome/free-solid-svg-icons";
// import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
// import { IconButton } from "@app/components/v2";
import { CaCertificatesTable } from "./CaCertificatesTable";
type Props = {
caId: string;
};
export const CaCertificatesSection = ({ caId }: Props) => {
return (
<div className="w-full rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
<div className="flex items-center justify-between border-b border-mineshaft-400 pb-4">
<h3 className="text-lg font-semibold text-mineshaft-100">CA Certificates</h3>
{/* <IconButton
ariaLabel="copy icon"
variant="plain"
className="group relative"
onClick={() => {
// handlePopUpOpen("addIdentityToProject");
}}
>
<FontAwesomeIcon icon={faPlus} />
</IconButton> */}
</div>
<div className="py-4">
<CaCertificatesTable caId={caId} />
</div>
</div>
);
};
@@ -0,0 +1,133 @@
import { faCertificate, faEllipsis } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import * as x509 from "@peculiar/x509";
import { format } from "date-fns";
import FileSaver from "file-saver";
import { twMerge } from "tailwind-merge";
import { ProjectPermissionCan } from "@app/components/permissions";
import {
Badge,
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
EmptyState,
Table,
TableContainer,
TableSkeleton,
TBody,
Td,
Th,
THead,
Tr
} from "@app/components/v2";
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/context";
import { useGetCaCerts } from "@app/hooks/api";
type Props = {
caId: string;
};
export const CaCertificatesTable = ({ caId }: Props) => {
const { data: caCerts, isLoading } = useGetCaCerts(caId);
const downloadTxtFile = (filename: string, content: string) => {
const blob = new Blob([content], { type: "text/plain;charset=utf-8" });
FileSaver.saveAs(blob, filename);
};
return (
<TableContainer>
<Table>
<THead>
<Tr>
<Th>CA Certificate #</Th>
<Th>Not Before</Th>
<Th>Not After</Th>
<Th className="w-5" />
</Tr>
</THead>
<TBody>
{isLoading && <TableSkeleton columns={4} innerKey="ca-certificates" />}
{!isLoading &&
caCerts?.map((caCert, index) => {
const isLastItem = index === caCerts.length - 1;
const caCertObj = new x509.X509Certificate(caCert.certificate);
return (
<Tr key={`ca-cert=${caCert.serialNumber}`}>
<Td>
<div className="flex items-center">
CA Certificate {caCert.version}
{isLastItem && (
<Badge variant="success" className="ml-4">
Current
</Badge>
)}
</div>
</Td>
<Td>{format(new Date(caCertObj.notBefore), "yyyy-MM-dd")}</Td>
<Td>{format(new Date(caCertObj.notAfter), "yyyy-MM-dd")}</Td>
<Td>
<DropdownMenu>
<DropdownMenuTrigger asChild className="rounded-lg">
<div className="hover:text-primary-400 data-[state=open]:text-primary-400">
<FontAwesomeIcon size="sm" icon={faEllipsis} />
</div>
</DropdownMenuTrigger>
<DropdownMenuContent align="start" className="p-1">
<ProjectPermissionCan
I={ProjectPermissionActions.Edit}
a={ProjectPermissionSub.Identity}
>
{(isAllowed) => (
<DropdownMenuItem
className={twMerge(
!isAllowed && "pointer-events-none cursor-not-allowed opacity-50"
)}
onClick={(e) => {
e.stopPropagation();
downloadTxtFile("cert.pem", caCert.certificate);
}}
disabled={!isAllowed}
>
Download CA Certificate
</DropdownMenuItem>
)}
</ProjectPermissionCan>
<ProjectPermissionCan
I={ProjectPermissionActions.Delete}
a={ProjectPermissionSub.Identity}
>
{(isAllowed) => (
<DropdownMenuItem
className={twMerge(
!isAllowed && "pointer-events-none cursor-not-allowed opacity-50"
)}
onClick={(e) => {
e.stopPropagation();
downloadTxtFile("chain.pem", caCert.certificateChain);
}}
disabled={!isAllowed}
>
Download CA Certificate Chain
</DropdownMenuItem>
)}
</ProjectPermissionCan>
</DropdownMenuContent>
</DropdownMenu>
</Td>
</Tr>
);
})}
</TBody>
</Table>
{!isLoading && !caCerts?.length && (
<EmptyState
title="This CA does not have any CA certificates installed"
icon={faCertificate}
/>
)}
</TableContainer>
);
};
@@ -0,0 +1 @@
export { CaCertificatesSection } from "./CaCertificatesSection";
@@ -0,0 +1,167 @@
import { faCheck, faCopy } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { format } from "date-fns";
import { ProjectPermissionCan } from "@app/components/permissions";
import { Button, IconButton, Tooltip } from "@app/components/v2";
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/context";
import { useTimedReset } from "@app/hooks";
import { CaStatus, useGetCaById } from "@app/hooks/api";
import { caStatusToNameMap, caTypeToNameMap } from "@app/hooks/api/ca/constants";
import { certKeyAlgorithmToNameMap } from "@app/hooks/api/certificates/constants";
import { UsePopUpState } from "@app/hooks/usePopUp";
type Props = {
caId: string;
handlePopUpOpen: (
popUpName: keyof UsePopUpState<["ca", "renewCa", "installCaCert"]>,
data?: {}
) => void;
};
export const CaDetailsSection = ({ caId, handlePopUpOpen }: Props) => {
const [copyTextId, isCopyingId, setCopyTextId] = useTimedReset<string>({
initialState: "Copy ID to clipboard"
});
const [copyTextParentId, isCopyingParentId, setCopyTextParentId] = useTimedReset<string>({
initialState: "Copy ID to clipboard"
});
const { data: ca } = useGetCaById(caId);
return ca ? (
<div className="rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
<div className="flex items-center justify-between border-b border-mineshaft-400 pb-4">
<h3 className="text-lg font-semibold text-mineshaft-100">CA Details</h3>
</div>
<div className="pt-4">
<div className="mb-4">
<p className="text-sm font-semibold text-mineshaft-300">CA ID</p>
<div className="group flex align-top">
<p className="text-sm text-mineshaft-300">{ca.id}</p>
<div className="opacity-0 transition-opacity duration-300 group-hover:opacity-100">
<Tooltip content={copyTextId}>
<IconButton
ariaLabel="copy icon"
variant="plain"
className="group relative ml-2"
onClick={() => {
navigator.clipboard.writeText(ca.id);
setCopyTextId("Copied");
}}
>
<FontAwesomeIcon icon={isCopyingId ? faCheck : faCopy} />
</IconButton>
</Tooltip>
</div>
</div>
</div>
{ca.parentCaId && (
<div className="mb-4">
<p className="text-sm font-semibold text-mineshaft-300">Parent CA ID</p>
<div className="group flex align-top">
<p className="text-sm text-mineshaft-300">{ca.parentCaId}</p>
<div className="opacity-0 transition-opacity duration-300 group-hover:opacity-100">
<Tooltip content={copyTextParentId}>
<IconButton
ariaLabel="copy icon"
variant="plain"
className="group relative ml-2"
onClick={() => {
navigator.clipboard.writeText(ca.parentCaId as string);
setCopyTextParentId("Copied");
}}
>
<FontAwesomeIcon icon={isCopyingParentId ? faCheck : faCopy} />
</IconButton>
</Tooltip>
</div>
</div>
</div>
)}
<div className="mb-4">
<p className="text-sm font-semibold text-mineshaft-300">Friendly Name</p>
<p className="text-sm text-mineshaft-300">{ca.friendlyName}</p>
</div>
<div className="mb-4">
<p className="text-sm font-semibold text-mineshaft-300">CA Type</p>
<p className="text-sm text-mineshaft-300">{caTypeToNameMap[ca.type]}</p>
</div>
<div className="mb-4">
<p className="text-sm font-semibold text-mineshaft-300">Status</p>
<p className="text-sm text-mineshaft-300">{caStatusToNameMap[ca.status]}</p>
</div>
<div className="mb-4">
<p className="text-sm font-semibold text-mineshaft-300">Key Algorithm</p>
<p className="text-sm text-mineshaft-300">{certKeyAlgorithmToNameMap[ca.keyAlgorithm]}</p>
</div>
<div className="mb-4">
<p className="text-sm font-semibold text-mineshaft-300">Max Path Length</p>
<p className="text-sm text-mineshaft-300">{ca.maxPathLength ?? "-"}</p>
</div>
<div className="mb-4">
<p className="text-sm font-semibold text-mineshaft-300">Not Before</p>
<p className="text-sm text-mineshaft-300">
{ca.notBefore ? format(new Date(ca.notBefore), "yyyy-MM-dd") : "-"}
</p>
</div>
<div className="mb-4">
<p className="text-sm font-semibold text-mineshaft-300">Not After</p>
<p className="text-sm text-mineshaft-300">
{ca.notAfter ? format(new Date(ca.notAfter), "yyyy-MM-dd") : "-"}
</p>
</div>
{ca.status === CaStatus.ACTIVE && (
<ProjectPermissionCan
I={ProjectPermissionActions.Edit}
a={ProjectPermissionSub.CertificateAuthorities}
>
{(isAllowed) => {
return (
<Button
isDisabled={!isAllowed}
className="mt-4 w-full"
colorSchema="primary"
type="submit"
onClick={() => {
handlePopUpOpen("renewCa", {
caId
});
}}
>
Renew CA
</Button>
);
}}
</ProjectPermissionCan>
)}
{ca.status === CaStatus.PENDING_CERTIFICATE && (
<ProjectPermissionCan
I={ProjectPermissionActions.Create}
a={ProjectPermissionSub.CertificateAuthorities}
>
{(isAllowed) => {
return (
<Button
isDisabled={!isAllowed}
className="mt-4 w-full"
colorSchema="primary"
type="submit"
onClick={() => {
handlePopUpOpen("installCaCert", {
caId
});
}}
>
Install CA Certificate
</Button>
);
}}
</ProjectPermissionCan>
)}
</div>
</div>
) : (
<div />
);
};
@@ -0,0 +1,182 @@
// import { useEffect } from "react";
import { Controller, useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { z } from "zod";
import { createNotification } from "@app/components/notifications";
import {
Button,
FormControl,
Input,
Modal,
ModalContent,
Select,
SelectItem
} from "@app/components/v2";
import { useWorkspace } from "@app/context";
import {
CaRenewalType,
useRenewCa
// useGetCaById,
// CaType,
// CaStatus
} from "@app/hooks/api/ca";
import { UsePopUpState } from "@app/hooks/usePopUp";
const caRenewalTypes = [{ label: "Renew with same key pair", value: CaRenewalType.EXISTING }];
const isValidDate = (dateString: string) => {
const date = new Date(dateString);
return !Number.isNaN(date.getTime());
};
const schema = z
.object({
type: z.enum([CaRenewalType.EXISTING]),
notAfter: z.string().trim().refine(isValidDate, { message: "Invalid date format" })
})
.required();
export type FormData = z.infer<typeof schema>;
type Props = {
popUp: UsePopUpState<["renewCa"]>;
handlePopUpToggle: (popUpName: keyof UsePopUpState<["renewCa"]>, state?: boolean) => void;
};
export const CaRenewalModal = ({ popUp, handlePopUpToggle }: Props) => {
const { currentWorkspace } = useWorkspace();
const projectSlug = currentWorkspace?.slug || "";
const popUpData = popUp?.renewCa?.data as {
caId: string;
};
// const { data: ca } = useGetCaById(popUpData?.caId || "");
// const { data: parentCa } = useGetCaById(ca?.parentCaId || "");
const { mutateAsync: renewCa } = useRenewCa();
const {
control,
handleSubmit,
reset,
formState: { isSubmitting }
// setValue
} = useForm<FormData>({
resolver: zodResolver(schema),
defaultValues: {
type: CaRenewalType.EXISTING,
notAfter: "" // TODO: consider setting a default value
}
});
// useEffect(() => {
// if (ca && ca.status === CaStatus.ACTIVE) {
// const notBeforeDate = new Date(ca.notBefore as string);
// const notAfterDate = new Date(ca.notAfter as string);
// const newNotAfterDate = new Date(
// notAfterDate.getTime() + notAfterDate.getTime() - notBeforeDate.getTime()
// );
// setValue("notAfter", newNotAfterDate.toISOString().split("T")[0]);
// }
// }, [ca, parentCa]);
const onFormSubmit = async ({ type, notAfter }: FormData) => {
try {
if (!projectSlug || !popUpData.caId) return;
await renewCa({
projectSlug,
caId: popUpData.caId,
notAfter,
type
});
handlePopUpToggle("renewCa", false);
createNotification({
text: "Successfully renewed CA",
type: "success"
});
reset();
} catch (err) {
console.error(err);
}
};
return (
<Modal
isOpen={popUp?.renewCa?.isOpen}
onOpenChange={(isOpen) => {
handlePopUpToggle("renewCa", isOpen);
reset();
}}
>
<ModalContent title="Renew CA">
<form onSubmit={handleSubmit(onFormSubmit)}>
<Controller
control={control}
name="type"
defaultValue={CaRenewalType.EXISTING}
render={({ field: { onChange, ...field }, fieldState: { error } }) => (
<FormControl
label="CA Renewal Method"
errorText={error?.message}
isError={Boolean(error)}
>
<Select
defaultValue={field.value}
{...field}
onValueChange={(e) => onChange(e)}
className="w-full"
>
{caRenewalTypes.map(({ label, value }) => (
<SelectItem value={String(value || "")} key={label}>
{label}
</SelectItem>
))}
</Select>
</FormControl>
)}
/>
<Controller
control={control}
defaultValue=""
name="notAfter"
render={({ field, fieldState: { error } }) => (
<FormControl
label="Valid Until"
isError={Boolean(error)}
errorText={error?.message}
isRequired
>
<Input {...field} placeholder="YYYY-MM-DD" />
</FormControl>
)}
/>
<div className="flex items-center">
<Button
className="mr-4"
size="sm"
type="submit"
isLoading={isSubmitting}
isDisabled={isSubmitting}
>
Renew
</Button>
<Button
colorSchema="secondary"
variant="plain"
onClick={() => handlePopUpToggle("renewCa", false)}
>
Cancel
</Button>
</div>
</form>
</ModalContent>
</Modal>
);
};
@@ -0,0 +1,3 @@
export { CaCertificatesSection } from "./CaCertificatesSection/CaCertificatesSection";
export { CaDetailsSection } from "./CaDetailsSection";
export { CaRenewalModal } from "./CaRenewalModal";
@@ -0,0 +1 @@
export { CaPage } from "./CaPage";
@@ -1,3 +1,4 @@
import { useRouter } from "next/router";
import { import {
faBan, faBan,
faCertificate, faCertificate,
@@ -12,6 +13,7 @@ import { twMerge } from "tailwind-merge";
import { ProjectPermissionCan } from "@app/components/permissions"; import { ProjectPermissionCan } from "@app/components/permissions";
import { import {
Badge,
DropdownMenu, DropdownMenu,
DropdownMenuContent, DropdownMenuContent,
DropdownMenuItem, DropdownMenuItem,
@@ -31,9 +33,14 @@ import {
ProjectPermissionActions, ProjectPermissionActions,
ProjectPermissionSub, ProjectPermissionSub,
useSubscription, useSubscription,
useWorkspace} from "@app/context"; useWorkspace
} from "@app/context";
import { CaStatus, useListWorkspaceCas } from "@app/hooks/api"; import { CaStatus, useListWorkspaceCas } from "@app/hooks/api";
import { caStatusToNameMap, caTypeToNameMap } from "@app/hooks/api/ca/constants"; import {
caStatusToNameMap,
caTypeToNameMap,
getStatusBadgeVariant
} from "@app/hooks/api/ca/constants";
import { UsePopUpState } from "@app/hooks/usePopUp"; import { UsePopUpState } from "@app/hooks/usePopUp";
type Props = { type Props = {
@@ -51,11 +58,13 @@ type Props = {
}; };
export const CaTable = ({ handlePopUpOpen }: Props) => { export const CaTable = ({ handlePopUpOpen }: Props) => {
const router = useRouter();
const { subscription } = useSubscription(); const { subscription } = useSubscription();
const { currentWorkspace } = useWorkspace(); const { currentWorkspace } = useWorkspace();
const { data, isLoading } = useListWorkspaceCas({ const { data, isLoading } = useListWorkspaceCas({
projectSlug: currentWorkspace?.slug ?? "" projectSlug: currentWorkspace?.slug ?? ""
}); });
return ( return (
<div> <div>
<TableContainer> <TableContainer>
@@ -76,11 +85,26 @@ export const CaTable = ({ handlePopUpOpen }: Props) => {
data.length > 0 && data.length > 0 &&
data.map((ca) => { data.map((ca) => {
return ( return (
<Tr className="h-10" key={`ca-${ca.id}`}> <Tr
className="h-10 cursor-pointer transition-colors duration-100 hover:bg-mineshaft-700"
key={`ca-${ca.id}`}
onClick={() => router.push(`/project/${currentWorkspace?.id}/ca/${ca.id}`)}
>
<Td>{ca.friendlyName}</Td> <Td>{ca.friendlyName}</Td>
<Td>{caStatusToNameMap[ca.status]}</Td> <Td>
<Badge variant={getStatusBadgeVariant(ca.status)}>
{caStatusToNameMap[ca.status]}
</Badge>
</Td>
<Td>{caTypeToNameMap[ca.type]}</Td> <Td>{caTypeToNameMap[ca.type]}</Td>
<Td>{ca.notAfter ? format(new Date(ca.notAfter), "yyyy-MM-dd") : "-"}</Td> <Td>
<div className="flex items-center ">
<p>{ca.notAfter ? format(new Date(ca.notAfter), "yyyy-MM-dd") : "-"}</p>
{/* <Badge variant="danger" className="ml-4">
Expires Soon
</Badge> */}
</div>
</Td>
<Td className="flex justify-end"> <Td className="flex justify-end">
<DropdownMenu> <DropdownMenu>
<DropdownMenuTrigger asChild className="rounded-lg"> <DropdownMenuTrigger asChild className="rounded-lg">
@@ -102,7 +126,8 @@ export const CaTable = ({ handlePopUpOpen }: Props) => {
!isAllowed && !isAllowed &&
"pointer-events-none cursor-not-allowed opacity-50" "pointer-events-none cursor-not-allowed opacity-50"
)} )}
onClick={async () => { onClick={(e) => {
e.stopPropagation();
handlePopUpOpen("installCaCert", { handlePopUpOpen("installCaCert", {
caId: ca.id caId: ca.id
}); });
@@ -110,7 +135,7 @@ export const CaTable = ({ handlePopUpOpen }: Props) => {
disabled={!isAllowed} disabled={!isAllowed}
icon={<FontAwesomeIcon icon={faCertificate} />} icon={<FontAwesomeIcon icon={faCertificate} />}
> >
Install Certificate Install CA Certificate
</DropdownMenuItem> </DropdownMenuItem>
)} )}
</ProjectPermissionCan> </ProjectPermissionCan>
@@ -126,7 +151,8 @@ export const CaTable = ({ handlePopUpOpen }: Props) => {
!isAllowed && !isAllowed &&
"pointer-events-none cursor-not-allowed opacity-50" "pointer-events-none cursor-not-allowed opacity-50"
)} )}
onClick={async () => { onClick={(e) => {
e.stopPropagation();
handlePopUpOpen("caCert", { handlePopUpOpen("caCert", {
caId: ca.id caId: ca.id
}); });
@@ -150,7 +176,8 @@ export const CaTable = ({ handlePopUpOpen }: Props) => {
!isAllowed && !isAllowed &&
"pointer-events-none cursor-not-allowed opacity-50" "pointer-events-none cursor-not-allowed opacity-50"
)} )}
onClick={async () => { onClick={(e) => {
e.stopPropagation();
if (!subscription?.caCrl) { if (!subscription?.caCrl) {
handlePopUpOpen("upgradePlan", { handlePopUpOpen("upgradePlan", {
description: description:
@@ -179,11 +206,12 @@ export const CaTable = ({ handlePopUpOpen }: Props) => {
className={twMerge( className={twMerge(
!isAllowed && "pointer-events-none cursor-not-allowed opacity-50" !isAllowed && "pointer-events-none cursor-not-allowed opacity-50"
)} )}
onClick={async () => onClick={(e) => {
e.stopPropagation();
handlePopUpOpen("ca", { handlePopUpOpen("ca", {
caId: ca.id caId: ca.id
}) });
} }}
disabled={!isAllowed} disabled={!isAllowed}
icon={<FontAwesomeIcon icon={faEye} />} icon={<FontAwesomeIcon icon={faEye} />}
> >
@@ -202,15 +230,16 @@ export const CaTable = ({ handlePopUpOpen }: Props) => {
!isAllowed && !isAllowed &&
"pointer-events-none cursor-not-allowed opacity-50" "pointer-events-none cursor-not-allowed opacity-50"
)} )}
onClick={async () => onClick={(e) => {
e.stopPropagation();
handlePopUpOpen("caStatus", { handlePopUpOpen("caStatus", {
caId: ca.id, caId: ca.id,
status: status:
ca.status === CaStatus.ACTIVE ca.status === CaStatus.ACTIVE
? CaStatus.DISABLED ? CaStatus.DISABLED
: CaStatus.ACTIVE : CaStatus.ACTIVE
}) });
} }}
disabled={!isAllowed} disabled={!isAllowed}
icon={<FontAwesomeIcon icon={faBan} />} icon={<FontAwesomeIcon icon={faBan} />}
> >
@@ -228,12 +257,13 @@ export const CaTable = ({ handlePopUpOpen }: Props) => {
className={twMerge( className={twMerge(
!isAllowed && "pointer-events-none cursor-not-allowed opacity-50" !isAllowed && "pointer-events-none cursor-not-allowed opacity-50"
)} )}
onClick={async () => onClick={(e) => {
e.stopPropagation();
handlePopUpOpen("deleteCa", { handlePopUpOpen("deleteCa", {
caId: ca.id, caId: ca.id,
dn: ca.dn dn: ca.dn
}) });
} }}
disabled={!isAllowed} disabled={!isAllowed}
icon={<FontAwesomeIcon icon={faTrash} />} icon={<FontAwesomeIcon icon={faTrash} />}
> >