Finish updating CRL impl

This commit is contained in:
Tuan Dang
2024-08-22 11:55:19 -07:00
parent 1515dd8a71
commit 0cb95f36ff
27 changed files with 378 additions and 273 deletions
+4 -4
View File
@@ -29,7 +29,7 @@
"@octokit/rest": "^20.0.2", "@octokit/rest": "^20.0.2",
"@octokit/webhooks-types": "^7.3.1", "@octokit/webhooks-types": "^7.3.1",
"@peculiar/asn1-schema": "^2.3.8", "@peculiar/asn1-schema": "^2.3.8",
"@peculiar/x509": "^1.10.0", "@peculiar/x509": "^1.12.1",
"@serdnam/pino-cloudwatch-transport": "^1.0.4", "@serdnam/pino-cloudwatch-transport": "^1.0.4",
"@sindresorhus/slugify": "1.1.0", "@sindresorhus/slugify": "1.1.0",
"@team-plain/typescript-sdk": "^4.6.1", "@team-plain/typescript-sdk": "^4.6.1",
@@ -8078,9 +8078,9 @@
} }
}, },
"node_modules/@peculiar/x509": { "node_modules/@peculiar/x509": {
"version": "1.10.0", "version": "1.12.1",
"resolved": "https://registry.npmjs.org/@peculiar/x509/-/x509-1.10.0.tgz", "resolved": "https://registry.npmjs.org/@peculiar/x509/-/x509-1.12.1.tgz",
"integrity": "sha512-gdH6H8gWjAYoM4Yr6wPnRbzU77nU7xq/jipqYyyv5/AHTrulN2Z5DlnOSq9jjKrB+Ya0D6YJ2cGGtwkWDK75jA==", "integrity": "sha512-2T9t2viNP9m20mky50igPTpn2ByhHl5NlT6wW4Tp4BejQaQ5XDNZgfsabYwYysLXhChABlgtTCpp2gM3JBZRKA==",
"dependencies": { "dependencies": {
"@peculiar/asn1-cms": "^2.3.8", "@peculiar/asn1-cms": "^2.3.8",
"@peculiar/asn1-csr": "^2.3.8", "@peculiar/asn1-csr": "^2.3.8",
+1 -1
View File
@@ -126,7 +126,7 @@
"@octokit/rest": "^20.0.2", "@octokit/rest": "^20.0.2",
"@octokit/webhooks-types": "^7.3.1", "@octokit/webhooks-types": "^7.3.1",
"@peculiar/asn1-schema": "^2.3.8", "@peculiar/asn1-schema": "^2.3.8",
"@peculiar/x509": "^1.10.0", "@peculiar/x509": "^1.12.1",
"@serdnam/pino-cloudwatch-transport": "^1.0.4", "@serdnam/pino-cloudwatch-transport": "^1.0.4",
"@sindresorhus/slugify": "1.1.0", "@sindresorhus/slugify": "1.1.0",
"@team-plain/typescript-sdk": "^4.6.1", "@team-plain/typescript-sdk": "^4.6.1",
@@ -0,0 +1,36 @@
import { Knex } from "knex";
import { TableName } from "../schemas";
export async function up(knex: Knex): Promise<void> {
if (await knex.schema.hasTable(TableName.CertificateAuthorityCrl)) {
const hasCaSecretIdColumn = await knex.schema.hasColumn(TableName.CertificateAuthorityCrl, "caSecretId");
if (!hasCaSecretIdColumn) {
await knex.schema.alterTable(TableName.CertificateAuthorityCrl, (t) => {
t.uuid("caSecretId").nullable();
t.foreign("caSecretId").references("id").inTable(TableName.CertificateAuthoritySecret).onDelete("CASCADE");
});
await knex.raw(`
UPDATE "${TableName.CertificateAuthorityCrl}" crl
SET "caSecretId" = (
SELECT sec.id
FROM "${TableName.CertificateAuthoritySecret}" sec
WHERE sec."caId" = crl."caId"
)
`);
await knex.schema.alterTable(TableName.CertificateAuthorityCrl, (t) => {
t.uuid("caSecretId").notNullable().alter();
});
}
}
}
export async function down(knex: Knex): Promise<void> {
if (await knex.schema.hasTable(TableName.CertificateAuthorityCrl)) {
await knex.schema.alterTable(TableName.CertificateAuthorityCrl, (t) => {
t.dropColumn("caSecretId");
});
}
}
@@ -9,6 +9,7 @@ import { TImmutableDBKeys } from "./models";
export const AccessApprovalRequestsReviewersSchema = z.object({ export const AccessApprovalRequestsReviewersSchema = z.object({
id: z.string().uuid(), id: z.string().uuid(),
member: z.string().uuid().nullable().optional(),
status: z.string(), status: z.string(),
requestId: z.string().uuid(), requestId: z.string().uuid(),
createdAt: z.date(), createdAt: z.date(),
@@ -11,6 +11,7 @@ export const AccessApprovalRequestsSchema = z.object({
id: z.string().uuid(), id: z.string().uuid(),
policyId: z.string().uuid(), policyId: z.string().uuid(),
privilegeId: z.string().uuid().nullable().optional(), privilegeId: z.string().uuid().nullable().optional(),
requestedBy: z.string().uuid().nullable().optional(),
isTemporary: z.boolean(), isTemporary: z.boolean(),
temporaryRange: z.string().nullable().optional(), temporaryRange: z.string().nullable().optional(),
permissions: z.unknown(), permissions: z.unknown(),
@@ -14,7 +14,8 @@ export const CertificateAuthorityCrlSchema = z.object({
createdAt: z.date(), createdAt: z.date(),
updatedAt: z.date(), updatedAt: z.date(),
caId: z.string().uuid(), caId: z.string().uuid(),
encryptedCrl: zodBuffer encryptedCrl: zodBuffer,
caSecretId: z.string().uuid()
}); });
export type TCertificateAuthorityCrl = z.infer<typeof CertificateAuthorityCrlSchema>; export type TCertificateAuthorityCrl = z.infer<typeof CertificateAuthorityCrlSchema>;
@@ -10,6 +10,7 @@ import { TImmutableDBKeys } from "./models";
export const ProjectUserAdditionalPrivilegeSchema = z.object({ export const ProjectUserAdditionalPrivilegeSchema = z.object({
id: z.string().uuid(), id: z.string().uuid(),
slug: z.string(), slug: z.string(),
projectMembershipId: z.string().uuid().nullable().optional(),
isTemporary: z.boolean().default(false), isTemporary: z.boolean().default(false),
temporaryMode: z.string().nullable().optional(), temporaryMode: z.string().nullable().optional(),
temporaryRange: z.string().nullable().optional(), temporaryRange: z.string().nullable().optional(),
@@ -1,86 +1,31 @@
/* eslint-disable @typescript-eslint/no-floating-promises */
import { z } from "zod"; import { z } from "zod";
import { EventType } from "@app/ee/services/audit-log/audit-log-types"; import { CA_CRLS } from "@app/lib/api-docs";
import { CERTIFICATE_AUTHORITIES } from "@app/lib/api-docs";
import { readLimit } from "@app/server/config/rateLimiter"; import { readLimit } from "@app/server/config/rateLimiter";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { AuthMode } from "@app/services/auth/auth-type";
export const registerCaCrlRouter = async (server: FastifyZodProvider) => { export const registerCaCrlRouter = async (server: FastifyZodProvider) => {
server.route({ server.route({
method: "GET", method: "GET",
url: "/:caId/crl", url: "/:crlId",
config: { config: {
rateLimit: readLimit rateLimit: readLimit
}, },
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
schema: { schema: {
description: "Get CRL of the CA", description: "Get CRL in DER format",
params: z.object({ params: z.object({
caId: z.string().trim().describe(CERTIFICATE_AUTHORITIES.GET_CRL.caId) crlId: z.string().trim().describe(CA_CRLS.GET.crlId)
}), }),
response: { response: {
200: z.object({ 200: z.instanceof(Buffer)
crl: z.string().describe(CERTIFICATE_AUTHORITIES.GET_CRL.crl)
})
} }
}, },
handler: async (req) => { handler: async (req, res) => {
const { crl, ca } = await server.services.certificateAuthorityCrl.getCaCrl({ const { crl } = await server.services.certificateAuthorityCrl.getCrlById(req.params.crlId);
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({ res.header("Content-Type", "application/pkix-crl");
...req.auditLogInfo,
projectId: ca.projectId,
event: {
type: EventType.GET_CA_CRL,
metadata: {
caId: ca.id,
dn: ca.dn
}
}
});
return { return Buffer.from(crl);
crl
};
} }
}); });
// server.route({
// method: "GET",
// url: "/:caId/crl/rotate",
// config: {
// rateLimit: writeLimit
// },
// onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
// schema: {
// description: "Rotate CRL of the CA",
// params: z.object({
// caId: z.string().trim()
// }),
// response: {
// 200: z.object({
// message: z.string()
// })
// }
// },
// handler: async (req) => {
// await server.services.certificateAuthority.rotateCaCrl({
// caId: req.params.caId,
// actor: req.permission.type,
// actorId: req.permission.id,
// actorAuthMethod: req.permission.authMethod,
// actorOrgId: req.permission.orgId
// });
// return {
// message: "Successfully rotated CA CRL"
// };
// }
// });
}; };
+1 -1
View File
@@ -61,7 +61,7 @@ export const registerV1EERoutes = async (server: FastifyZodProvider) => {
await server.register( await server.register(
async (pkiRouter) => { async (pkiRouter) => {
await pkiRouter.register(registerCaCrlRouter, { prefix: "/ca" }); await pkiRouter.register(registerCaCrlRouter, { prefix: "/crl" });
}, },
{ prefix: "/pki" } { prefix: "/pki" }
); );
@@ -137,7 +137,7 @@ export enum EventType {
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",
GET_CA_CRL = "get-certificate-authority-crl", GET_CA_CRLS = "get-certificate-authority-crls",
ISSUE_CERT = "issue-cert", ISSUE_CERT = "issue-cert",
SIGN_CERT = "sign-cert", SIGN_CERT = "sign-cert",
GET_CERT = "get-cert", GET_CERT = "get-cert",
@@ -1163,8 +1163,8 @@ interface ImportCaCert {
}; };
} }
interface GetCaCrl { interface GetCaCrls {
type: EventType.GET_CA_CRL; type: EventType.GET_CA_CRLS;
metadata: { metadata: {
caId: string; caId: string;
dn: string; dn: string;
@@ -1518,7 +1518,7 @@ export type Event =
| GetCaCert | GetCaCert
| SignIntermediate | SignIntermediate
| ImportCaCert | ImportCaCert
| GetCaCrl | GetCaCrls
| IssueCert | IssueCert
| SignCert | SignCert
| GetCert | GetCert
@@ -5,17 +5,17 @@ import { TCertificateAuthorityCrlDALFactory } from "@app/ee/services/certificate
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service"; import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission"; import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission";
import { BadRequestError } from "@app/lib/errors"; import { BadRequestError, NotFoundError } from "@app/lib/errors";
import { TCertificateAuthorityDALFactory } from "@app/services/certificate-authority/certificate-authority-dal"; import { TCertificateAuthorityDALFactory } from "@app/services/certificate-authority/certificate-authority-dal";
import { TKmsServiceFactory } from "@app/services/kms/kms-service"; import { TKmsServiceFactory } from "@app/services/kms/kms-service";
import { TProjectDALFactory } from "@app/services/project/project-dal"; 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 { TGetCrl } from "./certificate-authority-crl-types"; import { TGetCaCrlsDTO, TGetCrlById } from "./certificate-authority-crl-types";
type TCertificateAuthorityCrlServiceFactoryDep = { type TCertificateAuthorityCrlServiceFactoryDep = {
certificateAuthorityDAL: Pick<TCertificateAuthorityDALFactory, "findById">; certificateAuthorityDAL: Pick<TCertificateAuthorityDALFactory, "findById">;
certificateAuthorityCrlDAL: Pick<TCertificateAuthorityCrlDALFactory, "findOne">; certificateAuthorityCrlDAL: Pick<TCertificateAuthorityCrlDALFactory, "find" | "findById">;
projectDAL: Pick<TProjectDALFactory, "findOne" | "updateById" | "transaction">; projectDAL: Pick<TProjectDALFactory, "findOne" | "updateById" | "transaction">;
kmsService: Pick<TKmsServiceFactory, "decryptWithKmsKey" | "generateKmsKey">; kmsService: Pick<TKmsServiceFactory, "decryptWithKmsKey" | "generateKmsKey">;
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission">; permissionService: Pick<TPermissionServiceFactory, "getProjectPermission">;
@@ -33,9 +33,42 @@ export const certificateAuthorityCrlServiceFactory = ({
licenseService licenseService
}: TCertificateAuthorityCrlServiceFactoryDep) => { }: TCertificateAuthorityCrlServiceFactoryDep) => {
/** /**
* Return the Certificate Revocation List (CRL) for CA with id [caId] * Return CRL with id [crlId]
*/ */
const getCaCrl = async ({ caId, actorId, actorAuthMethod, actor, actorOrgId }: TGetCrl) => { const getCrlById = async (crlId: TGetCrlById) => {
const caCrl = await certificateAuthorityCrlDAL.findById(crlId);
if (!caCrl) throw new NotFoundError({ message: "CRL not found" });
const ca = await certificateAuthorityDAL.findById(caCrl.caId);
const keyId = await getProjectKmsCertificateKeyId({
projectId: ca.projectId,
projectDAL,
kmsService
});
const kmsDecryptor = await kmsService.decryptWithKmsKey({
kmsId: keyId
});
const decryptedCrl = await kmsDecryptor({ cipherTextBlob: caCrl.encryptedCrl });
const crl = new x509.X509Crl(decryptedCrl);
// const base64crl = crl.toString("base64");
// const crlPem = `-----BEGIN X509 CRL-----\n${base64crl.match(/.{1,64}/g)?.join("\n")}\n-----END X509 CRL-----`;
return {
ca,
caCrl,
crl: crl.rawData
};
};
/**
* Returns a list of CRL ids for CA with id [caId]
*/
const getCaCrls = async ({ caId, actorId, actorAuthMethod, actor, actorOrgId }: TGetCaCrlsDTO) => {
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" });
@@ -56,11 +89,10 @@ export const certificateAuthorityCrlServiceFactory = ({
if (!plan.caCrl) if (!plan.caCrl)
throw new BadRequestError({ throw new BadRequestError({
message: message:
"Failed to get CA certificate revocation list (CRL) due to plan restriction. Upgrade plan to get the CA CRL." "Failed to get CA certificate revocation lists (CRLs) due to plan restriction. Upgrade plan to get the CA CRL."
}); });
const caCrl = await certificateAuthorityCrlDAL.findOne({ caId: ca.id }); const caCrls = await certificateAuthorityCrlDAL.find({ caId: ca.id }, { sort: [["createdAt", "desc"]] });
if (!caCrl) throw new BadRequestError({ message: "CRL not found" });
const keyId = await getProjectKmsCertificateKeyId({ const keyId = await getProjectKmsCertificateKeyId({
projectId: ca.projectId, projectId: ca.projectId,
@@ -72,15 +104,23 @@ export const certificateAuthorityCrlServiceFactory = ({
kmsId: keyId kmsId: keyId
}); });
const decryptedCrl = await kmsDecryptor({ cipherTextBlob: caCrl.encryptedCrl }); const decryptedCrls = await Promise.all(
const crl = new x509.X509Crl(decryptedCrl); caCrls.map(async (caCrl) => {
const decryptedCrl = await kmsDecryptor({ cipherTextBlob: caCrl.encryptedCrl });
const crl = new x509.X509Crl(decryptedCrl);
const base64crl = crl.toString("base64"); const base64crl = crl.toString("base64");
const crlPem = `-----BEGIN X509 CRL-----\n${base64crl.match(/.{1,64}/g)?.join("\n")}\n-----END X509 CRL-----`; const crlPem = `-----BEGIN X509 CRL-----\n${base64crl.match(/.{1,64}/g)?.join("\n")}\n-----END X509 CRL-----`;
return {
id: caCrl.id,
crl: crlPem
};
})
);
return { return {
crl: crlPem, ca,
ca crls: decryptedCrls
}; };
}; };
@@ -166,7 +206,8 @@ export const certificateAuthorityCrlServiceFactory = ({
// }; // };
return { return {
getCaCrl getCrlById,
getCaCrls
// rotateCaCrl // rotateCaCrl
}; };
}; };
@@ -1,5 +1,7 @@
import { TProjectPermission } from "@app/lib/types"; import { TProjectPermission } from "@app/lib/types";
export type TGetCrl = { export type TGetCrlById = string;
export type TGetCaCrlsDTO = {
caId: string; caId: string;
} & Omit<TProjectPermission, "projectId">; } & Omit<TProjectPermission, "projectId">;
+11 -3
View File
@@ -1120,9 +1120,10 @@ export const CERTIFICATE_AUTHORITIES = {
certificateChain: "The certificate chain of the issued certificate", certificateChain: "The certificate chain of the issued certificate",
serialNumber: "The serial number of the issued certificate" serialNumber: "The serial number of the issued certificate"
}, },
GET_CRL: { GET_CRLS: {
caId: "The ID of the CA to get the certificate revocation list (CRL) for", caId: "The ID of the CA to get the certificate revocation lists (CRLs) for",
crl: "The certificate revocation list (CRL) of the CA" id: "The ID of certificate revocation list (CRL)",
crl: "The certificate revocation list (CRL)"
} }
}; };
@@ -1174,6 +1175,13 @@ export const CERTIFICATE_TEMPLATES = {
} }
}; };
export const CA_CRLS = {
GET: {
crlId: "The ID of the certificate revocation list (CRL) to get",
crl: "The certificate revocation list (CRL)"
}
};
export const ALERTS = { export const ALERTS = {
CREATE: { CREATE: {
projectId: "The ID of the project to create the alert in", projectId: "The ID of the project to create the alert in",
@@ -698,4 +698,83 @@ export const registerCaRouter = async (server: FastifyZodProvider) => {
}; };
} }
}); });
server.route({
method: "GET",
url: "/:caId/crls",
config: {
rateLimit: readLimit
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
schema: {
description: "Get list of CRLs of the CA",
params: z.object({
caId: z.string().trim().describe(CERTIFICATE_AUTHORITIES.GET_CRLS.caId)
}),
response: {
200: z.array(
z.object({
id: z.string().describe(CERTIFICATE_AUTHORITIES.GET_CRLS.id),
crl: z.string().describe(CERTIFICATE_AUTHORITIES.GET_CRLS.crl)
})
)
}
},
handler: async (req) => {
const { ca, crls } = await server.services.certificateAuthorityCrl.getCaCrls({
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_CRLS,
metadata: {
caId: ca.id,
dn: ca.dn
}
}
});
return crls;
}
});
// TODO: implement this endpoint in the future
// server.route({
// method: "GET",
// url: "/:caId/crl/rotate",
// config: {
// rateLimit: writeLimit
// },
// onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
// schema: {
// description: "Rotate CRLs of the CA",
// params: z.object({
// caId: z.string().trim()
// }),
// response: {
// 200: z.object({
// message: z.string()
// })
// }
// },
// handler: async (req) => {
// await server.services.certificateAuthority.rotateCaCrl({
// caId: req.params.caId,
// actor: req.permission.type,
// actorId: req.permission.id,
// actorAuthMethod: req.permission.authMethod,
// actorOrgId: req.permission.orgId
// });
// return {
// message: "Successfully rotated CA CRL"
// };
// }
// });
}; };
@@ -13,6 +13,13 @@ import {
TRebuildCaCrlDTO TRebuildCaCrlDTO
} from "./certificate-authority-types"; } from "./certificate-authority-types";
/* eslint-disable no-bitwise */
export const createSerialNumber = () => {
const randomBytes = crypto.randomBytes(32);
randomBytes[0] &= 0x7f; // ensure the first bit is 0
return randomBytes.toString("hex");
};
export const createDistinguishedName = (parts: TDNParts) => { export const createDistinguishedName = (parts: TDNParts) => {
const dnParts = []; const dnParts = [];
if (parts.country) dnParts.push(`C=${parts.country}`); if (parts.country) dnParts.push(`C=${parts.country}`);
@@ -284,12 +291,11 @@ export const rebuildCaCrl = async ({
thisUpdate: new Date(), thisUpdate: new Date(),
nextUpdate: new Date("2025/12/12"), nextUpdate: new Date("2025/12/12"),
entries: revokedCerts.map((revokedCert) => { entries: revokedCerts.map((revokedCert) => {
const revocationDate = new Date(revokedCert.revokedAt as Date);
return { return {
serialNumber: revokedCert.serialNumber, serialNumber: revokedCert.serialNumber,
revocationDate: new Date(revokedCert.revokedAt as Date), revocationDate,
reason: revokedCert.revocationReason as number, reason: revokedCert.revocationReason as number
invalidity: new Date("2022/01/01"),
issuer: ca.dn
}; };
}), }),
signingAlgorithm: alg, signingAlgorithm: alg,
@@ -8,6 +8,7 @@ import { z } from "zod";
import { TCertificateAuthorities, TCertificateTemplates } from "@app/db/schemas"; import { TCertificateAuthorities, TCertificateTemplates } from "@app/db/schemas";
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission"; import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission";
import { getConfig } from "@app/lib/config/env";
import { BadRequestError, NotFoundError } from "@app/lib/errors"; import { BadRequestError, NotFoundError } from "@app/lib/errors";
import { TCertificateBodyDALFactory } from "@app/services/certificate/certificate-body-dal"; import { TCertificateBodyDALFactory } from "@app/services/certificate/certificate-body-dal";
import { TCertificateDALFactory } from "@app/services/certificate/certificate-dal"; import { TCertificateDALFactory } from "@app/services/certificate/certificate-dal";
@@ -25,6 +26,7 @@ import { TCertificateAuthorityCertDALFactory } from "./certificate-authority-cer
import { TCertificateAuthorityDALFactory } from "./certificate-authority-dal"; import { TCertificateAuthorityDALFactory } from "./certificate-authority-dal";
import { import {
createDistinguishedName, createDistinguishedName,
createSerialNumber,
getCaCertChain, // TODO: consider rename getCaCertChain, // TODO: consider rename
getCaCertChains, getCaCertChains,
getCaCredentials, getCaCredentials,
@@ -147,7 +149,7 @@ export const certificateAuthorityServiceFactory = ({
? new Date(notAfter) ? new Date(notAfter)
: new Date(new Date().setFullYear(new Date().getFullYear() + 10)); : new Date(new Date().setFullYear(new Date().getFullYear() + 10));
const serialNumber = crypto.randomBytes(32).toString("hex"); const serialNumber = createSerialNumber();
const ca = await certificateAuthorityDAL.create( const ca = await certificateAuthorityDAL.create(
{ {
@@ -263,7 +265,8 @@ export const certificateAuthorityServiceFactory = ({
await certificateAuthorityCrlDAL.create( await certificateAuthorityCrlDAL.create(
{ {
caId: ca.id, caId: ca.id,
encryptedCrl encryptedCrl,
caSecretId: caSecret.id
}, },
tx tx
); );
@@ -433,7 +436,7 @@ export const certificateAuthorityServiceFactory = ({
// get latest CA certificate // get latest CA certificate
const caCert = await certificateAuthorityCertDAL.findById(ca.activeCaCertId); const caCert = await certificateAuthorityCertDAL.findById(ca.activeCaCertId);
const serialNumber = crypto.randomBytes(32).toString("hex"); const serialNumber = createSerialNumber();
const certificateManagerKmsId = await getProjectKmsCertificateKeyId({ const certificateManagerKmsId = await getProjectKmsCertificateKeyId({
projectId: ca.projectId, projectId: ca.projectId,
@@ -846,7 +849,7 @@ export const certificateAuthorityServiceFactory = ({
kmsService kmsService
}); });
const serialNumber = crypto.randomBytes(32).toString("hex"); const serialNumber = createSerialNumber();
const intermediateCert = await x509.X509CertificateGenerator.create({ const intermediateCert = await x509.X509CertificateGenerator.create({
serialNumber, serialNumber,
subject: csrObj.subject, subject: csrObj.subject,
@@ -1131,7 +1134,7 @@ export const certificateAuthorityServiceFactory = ({
attributes: [new x509.ChallengePasswordAttribute("password")] attributes: [new x509.ChallengePasswordAttribute("password")]
}); });
const { caPrivateKey } = await getCaCredentials({ const { caPrivateKey, caSecret } = await getCaCredentials({
caId: ca.id, caId: ca.id,
certificateAuthorityDAL, certificateAuthorityDAL,
certificateAuthoritySecretDAL, certificateAuthoritySecretDAL,
@@ -1139,9 +1142,15 @@ export const certificateAuthorityServiceFactory = ({
kmsService kmsService
}); });
const caCrl = await certificateAuthorityCrlDAL.findOne({ caSecretId: caSecret.id });
const appCfg = getConfig();
const distributionPointUrl = `${appCfg.SITE_URL}/api/v1/pki/crl/${caCrl.id}`;
const extensions: x509.Extension[] = [ const extensions: x509.Extension[] = [
new x509.KeyUsagesExtension(x509.KeyUsageFlags.digitalSignature | x509.KeyUsageFlags.keyEncipherment, true), new x509.KeyUsagesExtension(x509.KeyUsageFlags.digitalSignature | x509.KeyUsageFlags.keyEncipherment, true),
new x509.BasicConstraintsExtension(false), new x509.BasicConstraintsExtension(false),
new x509.CRLDistributionPointsExtension([distributionPointUrl]),
await x509.AuthorityKeyIdentifierExtension.create(caCertObj, false), await x509.AuthorityKeyIdentifierExtension.create(caCertObj, false),
await x509.SubjectKeyIdentifierExtension.create(csrObj.publicKey) await x509.SubjectKeyIdentifierExtension.create(csrObj.publicKey)
]; ];
@@ -1192,7 +1201,7 @@ export const certificateAuthorityServiceFactory = ({
); );
} }
const serialNumber = crypto.randomBytes(32).toString("hex"); const serialNumber = createSerialNumber();
const leafCert = await x509.X509CertificateGenerator.create({ const leafCert = await x509.X509CertificateGenerator.create({
serialNumber, serialNumber,
subject: csrObj.subject, subject: csrObj.subject,
@@ -1451,7 +1460,7 @@ export const certificateAuthorityServiceFactory = ({
); );
} }
const serialNumber = crypto.randomBytes(32).toString("hex"); const serialNumber = createSerialNumber();
const leafCert = await x509.X509CertificateGenerator.create({ const leafCert = await x509.X509CertificateGenerator.create({
serialNumber, serialNumber,
subject: csrObj.subject, subject: csrObj.subject,
+4 -3
View File
@@ -1,4 +1,4 @@
export { CaRenewalType,CaStatus, CaType } from "./enums"; export { CaRenewalType, CaStatus, CaType } from "./enums";
export { export {
useCreateCa, useCreateCa,
useCreateCertificate, useCreateCertificate,
@@ -6,5 +6,6 @@ export {
useImportCaCertificate, useImportCaCertificate,
useRenewCa, useRenewCa,
useSignIntermediate, useSignIntermediate,
useUpdateCa} from "./mutations"; useUpdateCa
export { useGetCaById, useGetCaCert, useGetCaCerts, useGetCaCrl, useGetCaCsr } from "./queries"; } from "./mutations";
export { useGetCaById, useGetCaCert, useGetCaCerts, useGetCaCrls,useGetCaCsr } from "./queries";
+10 -8
View File
@@ -7,6 +7,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"], getCaCerts: (caId: string) => [{ caId }, "ca-cert"],
getCaCrls: (caId: string) => [{ caId }, "ca-crls"],
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"]
@@ -73,16 +74,17 @@ export const useGetCaCsr = (caId: string) => {
}); });
}; };
export const useGetCaCrl = (caId: string) => { export const useGetCaCrls = (caId: string) => {
return useQuery({ return useQuery({
queryKey: caKeys.getCaCrl(caId), queryKey: caKeys.getCaCrls(caId),
queryFn: async () => { queryFn: async () => {
const { const { data } = await apiRequest.get<
data: { crl } {
} = await apiRequest.get<{ id: string;
crl: string; crl: string;
}>(`/api/v1/pki/ca/${caId}/crl`); }[]
return crl; >(`/api/v1/pki/ca/${caId}/crls`);
return data;
}, },
enabled: Boolean(caId) enabled: Boolean(caId)
}); });
+10 -3
View File
@@ -22,8 +22,12 @@ import { usePopUp } from "@app/hooks/usePopUp";
import { CaModal } from "@app/views/Project/CertificatesPage/components/CaTab/components/CaModal"; import { CaModal } from "@app/views/Project/CertificatesPage/components/CaTab/components/CaModal";
import { CaInstallCertModal } from "../CertificatesPage/components/CaTab/components/CaInstallCertModal"; import { CaInstallCertModal } from "../CertificatesPage/components/CaTab/components/CaInstallCertModal";
import { TabSections } from "../Types"; import {
import { CaCertificatesSection, CaDetailsSection, CaRenewalModal } from "./components"; CaCertificatesSection,
CaCrlsSection,
CaDetailsSection,
CaRenewalModal
} from "./components";
export const CaPage = withProjectPermission( export const CaPage = withProjectPermission(
() => { () => {
@@ -119,7 +123,10 @@ export const CaPage = withProjectPermission(
<div className="mr-4 w-96"> <div className="mr-4 w-96">
<CaDetailsSection caId={caId} handlePopUpOpen={handlePopUpOpen} /> <CaDetailsSection caId={caId} handlePopUpOpen={handlePopUpOpen} />
</div> </div>
<CaCertificatesSection caId={caId} /> <div className="w-full">
<CaCertificatesSection caId={caId} />
<CaCrlsSection caId={caId} />
</div>
</div> </div>
</div> </div>
)} )}
@@ -0,0 +1,20 @@
import { CaCrlsTable } from "./CaCrlsTable";
type Props = {
caId: string;
};
export const CaCrlsSection = ({ caId }: Props) => {
return (
<div className="mt-4 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 Certificate Revocation Lists (CRLs)
</h3>
</div>
<div className="py-4">
<CaCrlsTable caId={caId} />
</div>
</div>
);
};
@@ -0,0 +1,88 @@
import { faCertificate, faFileDownload } 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 {
EmptyState,
IconButton,
Table,
TableContainer,
TableSkeleton,
TBody,
Td,
Th,
THead,
Tooltip,
Tr
} from "@app/components/v2";
import { useGetCaCrls } from "@app/hooks/api";
type Props = {
caId: string;
};
export const CaCrlsTable = ({ caId }: Props) => {
const { data: caCrls, isLoading } = useGetCaCrls(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>Distribution Point URL</Th>
{/* <Th>This Update</Th> */}
{/* <Th>Next Update</Th> */}
<Th className="w-5" />
</Tr>
</THead>
<TBody>
{isLoading && <TableSkeleton columns={4} innerKey="ca-certificates" />}
{!isLoading &&
caCrls?.map(({ id, crl }) => {
// const caCrlObj = new x509.X509Crl(crl);
return (
<Tr key={`ca-crl-${id}`}>
<Td>
<div className="flex items-center">
{`${window.origin}/api/v1/pki/crl/${id}`}
</div>
</Td>
{/* <Td>{format(new Date(caCrlObj.thisUpdate), "yyyy-MM-dd")}</Td> */}
{/* <Td>
{caCrlObj.nextUpdate
? format(new Date(caCrlObj.nextUpdate), "yyyy-MM-dd")
: "-"}
</Td> */}
<Td>
<Tooltip content="Download CRL">
<IconButton
ariaLabel="copy icon"
variant="plain"
className="group relative"
onClick={(e) => {
e.stopPropagation();
downloadTxtFile("crl.pem", crl);
}}
>
<FontAwesomeIcon icon={faFileDownload} />
</IconButton>
</Tooltip>
</Td>
</Tr>
);
})}
</TBody>
</Table>
{!isLoading && !caCrls?.length && (
<EmptyState title="This CA does not have any CRLs" icon={faCertificate} />
)}
</TableContainer>
);
};
@@ -0,0 +1 @@
export { CaCrlsSection } from "./CaCrlsSection";
@@ -1,3 +1,4 @@
export { CaCertificatesSection } from "./CaCertificatesSection/CaCertificatesSection"; export { CaCertificatesSection } from "./CaCertificatesSection/CaCertificatesSection";
export { CaCrlsSection } from "./CaCrlsSection";
export { CaDetailsSection } from "./CaDetailsSection"; export { CaDetailsSection } from "./CaDetailsSection";
export { CaRenewalModal } from "./CaRenewalModal"; export { CaRenewalModal } from "./CaRenewalModal";
@@ -1,106 +0,0 @@
import { useEffect } from "react";
import { faCheck, faCopy, faDownload } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { IconButton, Modal, ModalContent } from "@app/components/v2";
import { useToggle } from "@app/hooks";
import { useGetCaCrl } from "@app/hooks/api";
import { UsePopUpState } from "@app/hooks/usePopUp";
type Props = {
popUp: UsePopUpState<["caCrl"]>;
handlePopUpToggle: (popUpName: keyof UsePopUpState<["caCrl"]>, state?: boolean) => void;
};
export const CaCrlModal = ({ popUp, handlePopUpToggle }: Props) => {
const [isCrlCopied, setIsCrlCopied] = useToggle(false);
const { data: crl } = useGetCaCrl((popUp?.caCrl?.data as { caId: string })?.caId || "");
useEffect(() => {
let timer: NodeJS.Timeout;
if (isCrlCopied) {
timer = setTimeout(() => setIsCrlCopied.off(), 2000);
}
return () => clearTimeout(timer);
}, [isCrlCopied]);
const downloadTxtFile = (filename: string, content: string) => {
const blob = new Blob([content], { type: "text/plain" });
const url = URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = url;
a.download = filename;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
};
return (
<Modal
isOpen={popUp?.caCrl?.isOpen}
onOpenChange={(isOpen) => {
handlePopUpToggle("caCrl", isOpen);
}}
>
<ModalContent title="CA Certificate Revocation List (CRL)">
<div>
{crl && (
<>
{/* <div className="mb-4 flex items-center justify-between">
<h2>Manual CRL Rotation</h2>
<Button
// isLoading={isLoading}
// isDisabled={!isAllowed}
colorSchema="primary"
variant="outline_bg"
type="submit"
// onClick={() => handleAssignment(username, !isPartOfGroup)}
onClick={() => {}}
>
Rotate
</Button>
</div> */}
<div className="mb-4 flex items-center justify-between">
<h2>Certificate Revocation List</h2>
<div className="flex">
<IconButton
ariaLabel="copy icon"
colorSchema="secondary"
className="group relative"
onClick={() => {
navigator.clipboard.writeText(crl);
setIsCrlCopied.on();
}}
>
<FontAwesomeIcon icon={isCrlCopied ? faCheck : faCopy} />
<span className="absolute -left-8 -top-20 hidden w-28 translate-y-full rounded-md bg-bunker-800 py-2 pl-3 text-center text-sm text-gray-400 group-hover:flex group-hover:animate-fadeIn">
Copy
</span>
</IconButton>
<IconButton
ariaLabel="copy icon"
colorSchema="secondary"
className="group relative ml-2"
onClick={() => {
downloadTxtFile("crl.pem", crl);
}}
>
<FontAwesomeIcon icon={faDownload} />
<span className="absolute -left-8 -top-20 hidden w-28 translate-y-full rounded-md bg-bunker-800 py-2 pl-3 text-center text-sm text-gray-400 group-hover:flex group-hover:animate-fadeIn">
Download
</span>
</IconButton>
</div>
</div>
<div className="mb-8 flex items-center justify-between rounded-md bg-white/[0.07] p-2 text-base text-gray-400">
<p className="mr-4 whitespace-pre-wrap break-all">{crl}</p>
</div>
</>
)}
</div>
</ModalContent>
</Modal>
);
};
@@ -9,7 +9,6 @@ import { CaStatus, useDeleteCa, useUpdateCa } from "@app/hooks/api";
import { usePopUp } from "@app/hooks/usePopUp"; import { usePopUp } from "@app/hooks/usePopUp";
import { CaCertModal } from "./CaCertModal"; import { CaCertModal } from "./CaCertModal";
import { CaCrlModal } from "./CaCrlModal";
import { CaInstallCertModal } from "./CaInstallCertModal"; import { CaInstallCertModal } from "./CaInstallCertModal";
import { CaModal } from "./CaModal"; import { CaModal } from "./CaModal";
import { CaTable } from "./CaTable"; import { CaTable } from "./CaTable";
@@ -25,7 +24,6 @@ export const CaSection = () => {
"installCaCert", "installCaCert",
"deleteCa", "deleteCa",
"caStatus", // enable / disable "caStatus", // enable / disable
"caCrl", // enable / disable
"upgradePlan" "upgradePlan"
] as const); ] as const);
@@ -95,7 +93,6 @@ export const CaSection = () => {
<CaModal popUp={popUp} handlePopUpToggle={handlePopUpToggle} /> <CaModal popUp={popUp} handlePopUpToggle={handlePopUpToggle} />
<CaInstallCertModal popUp={popUp} handlePopUpToggle={handlePopUpToggle} /> <CaInstallCertModal popUp={popUp} handlePopUpToggle={handlePopUpToggle} />
<CaCertModal popUp={popUp} handlePopUpToggle={handlePopUpToggle} /> <CaCertModal popUp={popUp} handlePopUpToggle={handlePopUpToggle} />
<CaCrlModal popUp={popUp} handlePopUpToggle={handlePopUpToggle} />
<CaTable handlePopUpOpen={handlePopUpOpen} /> <CaTable handlePopUpOpen={handlePopUpOpen} />
<DeleteActionModal <DeleteActionModal
isOpen={popUp.deleteCa.isOpen} isOpen={popUp.deleteCa.isOpen}
@@ -4,7 +4,6 @@ import {
faCertificate, faCertificate,
faEllipsis, faEllipsis,
faEye, faEye,
faFile,
faTrash faTrash
} from "@fortawesome/free-solid-svg-icons"; } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
@@ -29,12 +28,7 @@ import {
Tooltip, Tooltip,
Tr Tr
} from "@app/components/v2"; } from "@app/components/v2";
import { import { ProjectPermissionActions, ProjectPermissionSub, useWorkspace } from "@app/context";
ProjectPermissionActions,
ProjectPermissionSub,
useSubscription,
useWorkspace
} from "@app/context";
import { CaStatus, useListWorkspaceCas } from "@app/hooks/api"; import { CaStatus, useListWorkspaceCas } from "@app/hooks/api";
import { import {
caStatusToNameMap, caStatusToNameMap,
@@ -46,7 +40,7 @@ import { UsePopUpState } from "@app/hooks/usePopUp";
type Props = { type Props = {
handlePopUpOpen: ( handlePopUpOpen: (
popUpName: keyof UsePopUpState< popUpName: keyof UsePopUpState<
["installCaCert", "caCert", "ca", "deleteCa", "caStatus", "caCrl", "upgradePlan"] ["installCaCert", "caCert", "ca", "deleteCa", "caStatus", "upgradePlan"]
>, >,
data?: { data?: {
caId?: string; caId?: string;
@@ -59,7 +53,6 @@ type Props = {
export const CaTable = ({ handlePopUpOpen }: Props) => { export const CaTable = ({ handlePopUpOpen }: Props) => {
const router = useRouter(); const router = useRouter();
const { subscription } = useSubscription();
const { currentWorkspace } = useWorkspace(); const { currentWorkspace } = useWorkspace();
const { data, isLoading } = useListWorkspaceCas({ const { data, isLoading } = useListWorkspaceCas({
projectSlug: currentWorkspace?.slug ?? "" projectSlug: currentWorkspace?.slug ?? ""
@@ -162,38 +155,6 @@ export const CaTable = ({ handlePopUpOpen }: Props) => {
)} )}
</ProjectPermissionCan> </ProjectPermissionCan>
)} )}
{ca.status !== CaStatus.PENDING_CERTIFICATE && (
<ProjectPermissionCan
I={ProjectPermissionActions.Read}
a={ProjectPermissionSub.CertificateAuthorities}
>
{(isAllowed) => (
<DropdownMenuItem
className={twMerge(
!isAllowed &&
"pointer-events-none cursor-not-allowed opacity-50"
)}
onClick={(e) => {
e.stopPropagation();
if (!subscription?.caCrl) {
handlePopUpOpen("upgradePlan", {
description:
"You can use the certificate revocation list (CRL) feature if you upgrade your Infisical plan."
});
} else {
handlePopUpOpen("caCrl", {
caId: ca.id
});
}
}}
disabled={!isAllowed}
icon={<FontAwesomeIcon icon={faFile} />}
>
View CRL
</DropdownMenuItem>
)}
</ProjectPermissionCan>
)}
<ProjectPermissionCan <ProjectPermissionCan
I={ProjectPermissionActions.Read} I={ProjectPermissionActions.Read}
a={ProjectPermissionSub.CertificateAuthorities} a={ProjectPermissionSub.CertificateAuthorities}
@@ -32,6 +32,7 @@ import {
} from "@app/components/v2"; } from "@app/components/v2";
import { ProjectPermissionActions, ProjectPermissionSub, useWorkspace } from "@app/context"; import { ProjectPermissionActions, ProjectPermissionSub, useWorkspace } from "@app/context";
import { useListWorkspaceCertificates } from "@app/hooks/api"; import { useListWorkspaceCertificates } from "@app/hooks/api";
import { CertStatus } from "@app/hooks/api/certificates/enums";
import { UsePopUpState } from "@app/hooks/usePopUp"; import { UsePopUpState } from "@app/hooks/usePopUp";
import { getCertValidUntilBadgeDetails } from "./CertificatesTable.utils"; import { getCertValidUntilBadgeDetails } from "./CertificatesTable.utils";
@@ -82,9 +83,11 @@ export const CertificatesTable = ({ handlePopUpOpen }: Props) => {
<Tr className="h-10" key={`certificate-${certificate.id}`}> <Tr className="h-10" key={`certificate-${certificate.id}`}>
<Td>{certificate.friendlyName}</Td> <Td>{certificate.friendlyName}</Td>
<Td> <Td>
<Badge className="" variant={variant}> {certificate.status === CertStatus.REVOKED ? (
{label} <Badge variant="danger">Revoked</Badge>
</Badge> ) : (
<Badge variant={variant}>{label}</Badge>
)}
</Td> </Td>
<Td> <Td>
{certificate.notBefore {certificate.notBefore