Merge pull request #2325 from Infisical/crl-update

CRL Distribution Point URLs + Support for Multiple CRLs per CA
This commit is contained in:
BlackMagiq
2024-08-22 23:56:55 -07:00
committed by GitHub
34 changed files with 411 additions and 299 deletions
+4 -4
View File
@@ -29,7 +29,7 @@
"@octokit/rest": "^20.0.2",
"@octokit/webhooks-types": "^7.3.1",
"@peculiar/asn1-schema": "^2.3.8",
"@peculiar/x509": "^1.10.0",
"@peculiar/x509": "^1.12.1",
"@serdnam/pino-cloudwatch-transport": "^1.0.4",
"@sindresorhus/slugify": "1.1.0",
"@team-plain/typescript-sdk": "^4.6.1",
@@ -5029,9 +5029,9 @@
}
},
"node_modules/@peculiar/x509": {
"version": "1.10.0",
"resolved": "https://registry.npmjs.org/@peculiar/x509/-/x509-1.10.0.tgz",
"integrity": "sha512-gdH6H8gWjAYoM4Yr6wPnRbzU77nU7xq/jipqYyyv5/AHTrulN2Z5DlnOSq9jjKrB+Ya0D6YJ2cGGtwkWDK75jA==",
"version": "1.12.1",
"resolved": "https://registry.npmjs.org/@peculiar/x509/-/x509-1.12.1.tgz",
"integrity": "sha512-2T9t2viNP9m20mky50igPTpn2ByhHl5NlT6wW4Tp4BejQaQ5XDNZgfsabYwYysLXhChABlgtTCpp2gM3JBZRKA==",
"dependencies": {
"@peculiar/asn1-cms": "^2.3.8",
"@peculiar/asn1-csr": "^2.3.8",
+1 -1
View File
@@ -126,7 +126,7 @@
"@octokit/rest": "^20.0.2",
"@octokit/webhooks-types": "^7.3.1",
"@peculiar/asn1-schema": "^2.3.8",
"@peculiar/x509": "^1.10.0",
"@peculiar/x509": "^1.12.1",
"@serdnam/pino-cloudwatch-transport": "^1.0.4",
"@sindresorhus/slugify": "1.1.0",
"@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({
id: z.string().uuid(),
member: z.string().uuid().nullable().optional(),
status: z.string(),
requestId: z.string().uuid(),
createdAt: z.date(),
@@ -11,6 +11,7 @@ export const AccessApprovalRequestsSchema = z.object({
id: z.string().uuid(),
policyId: z.string().uuid(),
privilegeId: z.string().uuid().nullable().optional(),
requestedBy: z.string().uuid().nullable().optional(),
isTemporary: z.boolean(),
temporaryRange: z.string().nullable().optional(),
permissions: z.unknown(),
@@ -14,7 +14,8 @@ export const CertificateAuthorityCrlSchema = z.object({
createdAt: z.date(),
updatedAt: z.date(),
caId: z.string().uuid(),
encryptedCrl: zodBuffer
encryptedCrl: zodBuffer,
caSecretId: z.string().uuid()
});
export type TCertificateAuthorityCrl = z.infer<typeof CertificateAuthorityCrlSchema>;
@@ -10,6 +10,7 @@ import { TImmutableDBKeys } from "./models";
export const ProjectUserAdditionalPrivilegeSchema = z.object({
id: z.string().uuid(),
slug: z.string(),
projectMembershipId: z.string().uuid().nullable().optional(),
isTemporary: z.boolean().default(false),
temporaryMode: 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 { EventType } from "@app/ee/services/audit-log/audit-log-types";
import { CERTIFICATE_AUTHORITIES } from "@app/lib/api-docs";
import { CA_CRLS } from "@app/lib/api-docs";
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) => {
server.route({
method: "GET",
url: "/:caId/crl",
url: "/:crlId",
config: {
rateLimit: readLimit
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
schema: {
description: "Get CRL of the CA",
description: "Get CRL in DER format",
params: z.object({
caId: z.string().trim().describe(CERTIFICATE_AUTHORITIES.GET_CRL.caId)
crlId: z.string().trim().describe(CA_CRLS.GET.crlId)
}),
response: {
200: z.object({
crl: z.string().describe(CERTIFICATE_AUTHORITIES.GET_CRL.crl)
})
200: z.instanceof(Buffer)
}
},
handler: async (req) => {
const { crl, ca } = await server.services.certificateAuthorityCrl.getCaCrl({
caId: req.params.caId,
actor: req.permission.type,
actorId: req.permission.id,
actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId
});
handler: async (req, res) => {
const { crl } = await server.services.certificateAuthorityCrl.getCrlById(req.params.crlId);
await server.services.auditLog.createAuditLog({
...req.auditLogInfo,
projectId: ca.projectId,
event: {
type: EventType.GET_CA_CRL,
metadata: {
caId: ca.id,
dn: ca.dn
}
}
});
res.header("Content-Type", "application/pkix-crl");
return {
crl
};
return Buffer.from(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(
async (pkiRouter) => {
await pkiRouter.register(registerCaCrlRouter, { prefix: "/ca" });
await pkiRouter.register(registerCaCrlRouter, { prefix: "/crl" });
},
{ prefix: "/pki" }
);
@@ -137,7 +137,7 @@ export enum EventType {
GET_CA_CERT = "get-certificate-authority-cert",
SIGN_INTERMEDIATE = "sign-intermediate",
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",
SIGN_CERT = "sign-cert",
GET_CERT = "get-cert",
@@ -1163,8 +1163,8 @@ interface ImportCaCert {
};
}
interface GetCaCrl {
type: EventType.GET_CA_CRL;
interface GetCaCrls {
type: EventType.GET_CA_CRLS;
metadata: {
caId: string;
dn: string;
@@ -1518,7 +1518,7 @@ export type Event =
| GetCaCert
| SignIntermediate
| ImportCaCert
| GetCaCrl
| GetCaCrls
| IssueCert
| SignCert
| GetCert
@@ -2,24 +2,24 @@ import { ForbiddenError } from "@casl/ability";
import * as x509 from "@peculiar/x509";
import { TCertificateAuthorityCrlDALFactory } from "@app/ee/services/certificate-authority-crl/certificate-authority-crl-dal";
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 { 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 { TKmsServiceFactory } from "@app/services/kms/kms-service";
import { TProjectDALFactory } from "@app/services/project/project-dal";
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 = {
certificateAuthorityDAL: Pick<TCertificateAuthorityDALFactory, "findById">;
certificateAuthorityCrlDAL: Pick<TCertificateAuthorityCrlDALFactory, "findOne">;
certificateAuthorityCrlDAL: Pick<TCertificateAuthorityCrlDALFactory, "find" | "findById">;
projectDAL: Pick<TProjectDALFactory, "findOne" | "updateById" | "transaction">;
kmsService: Pick<TKmsServiceFactory, "decryptWithKmsKey" | "generateKmsKey">;
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission">;
licenseService: Pick<TLicenseServiceFactory, "getPlan">;
// licenseService: Pick<TLicenseServiceFactory, "getPlan">;
};
export type TCertificateAuthorityCrlServiceFactory = ReturnType<typeof certificateAuthorityCrlServiceFactory>;
@@ -29,13 +29,42 @@ export const certificateAuthorityCrlServiceFactory = ({
certificateAuthorityCrlDAL,
projectDAL,
kmsService,
permissionService,
licenseService
permissionService // licenseService
}: 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);
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);
if (!ca) throw new BadRequestError({ message: "CA not found" });
@@ -52,15 +81,14 @@ export const certificateAuthorityCrlServiceFactory = ({
ProjectPermissionSub.CertificateAuthorities
);
const plan = await licenseService.getPlan(actorOrgId);
if (!plan.caCrl)
throw new BadRequestError({
message:
"Failed to get CA certificate revocation list (CRL) due to plan restriction. Upgrade plan to get the CA CRL."
});
// const plan = await licenseService.getPlan(actorOrgId);
// if (!plan.caCrl)
// throw new BadRequestError({
// message:
// "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 });
if (!caCrl) throw new BadRequestError({ message: "CRL not found" });
const caCrls = await certificateAuthorityCrlDAL.find({ caId: ca.id }, { sort: [["createdAt", "desc"]] });
const keyId = await getProjectKmsCertificateKeyId({
projectId: ca.projectId,
@@ -72,15 +100,23 @@ export const certificateAuthorityCrlServiceFactory = ({
kmsId: keyId
});
const decryptedCrl = await kmsDecryptor({ cipherTextBlob: caCrl.encryptedCrl });
const crl = new x509.X509Crl(decryptedCrl);
const decryptedCrls = await Promise.all(
caCrls.map(async (caCrl) => {
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-----`;
const base64crl = crl.toString("base64");
const crlPem = `-----BEGIN X509 CRL-----\n${base64crl.match(/.{1,64}/g)?.join("\n")}\n-----END X509 CRL-----`;
return {
id: caCrl.id,
crl: crlPem
};
})
);
return {
crl: crlPem,
ca
ca,
crls: decryptedCrls
};
};
@@ -166,7 +202,8 @@ export const certificateAuthorityCrlServiceFactory = ({
// };
return {
getCaCrl
getCrlById,
getCaCrls
// rotateCaCrl
};
};
@@ -1,5 +1,7 @@
import { TProjectPermission } from "@app/lib/types";
export type TGetCrl = {
export type TGetCrlById = string;
export type TGetCaCrlsDTO = {
caId: string;
} & Omit<TProjectPermission, "projectId">;
+11 -3
View File
@@ -1120,9 +1120,10 @@ export const CERTIFICATE_AUTHORITIES = {
certificateChain: "The certificate chain of the issued certificate",
serialNumber: "The serial number of the issued certificate"
},
GET_CRL: {
caId: "The ID of the CA to get the certificate revocation list (CRL) for",
crl: "The certificate revocation list (CRL) of the CA"
GET_CRLS: {
caId: "The ID of the CA to get the certificate revocation lists (CRLs) for",
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 = {
CREATE: {
projectId: "The ID of the project to create the alert in",
+2 -2
View File
@@ -646,8 +646,8 @@ export const registerRoutes = async (
certificateAuthorityCrlDAL,
projectDAL,
kmsService,
permissionService,
licenseService
permissionService
// licenseService
});
const certificateTemplateService = certificateTemplateServiceFactory({
@@ -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
} 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) => {
const dnParts = [];
if (parts.country) dnParts.push(`C=${parts.country}`);
@@ -284,12 +291,11 @@ export const rebuildCaCrl = async ({
thisUpdate: new Date(),
nextUpdate: new Date("2025/12/12"),
entries: revokedCerts.map((revokedCert) => {
const revocationDate = new Date(revokedCert.revokedAt as Date);
return {
serialNumber: revokedCert.serialNumber,
revocationDate: new Date(revokedCert.revokedAt as Date),
reason: revokedCert.revocationReason as number,
invalidity: new Date("2022/01/01"),
issuer: ca.dn
revocationDate,
reason: revokedCert.revocationReason as number
};
}),
signingAlgorithm: alg,
@@ -8,6 +8,7 @@ import { z } from "zod";
import { TCertificateAuthorities, TCertificateTemplates } from "@app/db/schemas";
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
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 { TCertificateBodyDALFactory } from "@app/services/certificate/certificate-body-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 {
createDistinguishedName,
createSerialNumber,
getCaCertChain, // TODO: consider rename
getCaCertChains,
getCaCredentials,
@@ -147,7 +149,7 @@ export const certificateAuthorityServiceFactory = ({
? new Date(notAfter)
: new Date(new Date().setFullYear(new Date().getFullYear() + 10));
const serialNumber = crypto.randomBytes(32).toString("hex");
const serialNumber = createSerialNumber();
const ca = await certificateAuthorityDAL.create(
{
@@ -263,7 +265,8 @@ export const certificateAuthorityServiceFactory = ({
await certificateAuthorityCrlDAL.create(
{
caId: ca.id,
encryptedCrl
encryptedCrl,
caSecretId: caSecret.id
},
tx
);
@@ -433,7 +436,7 @@ export const certificateAuthorityServiceFactory = ({
// get latest CA certificate
const caCert = await certificateAuthorityCertDAL.findById(ca.activeCaCertId);
const serialNumber = crypto.randomBytes(32).toString("hex");
const serialNumber = createSerialNumber();
const certificateManagerKmsId = await getProjectKmsCertificateKeyId({
projectId: ca.projectId,
@@ -846,7 +849,7 @@ export const certificateAuthorityServiceFactory = ({
kmsService
});
const serialNumber = crypto.randomBytes(32).toString("hex");
const serialNumber = createSerialNumber();
const intermediateCert = await x509.X509CertificateGenerator.create({
serialNumber,
subject: csrObj.subject,
@@ -1142,7 +1145,7 @@ export const certificateAuthorityServiceFactory = ({
attributes: [new x509.ChallengePasswordAttribute("password")]
});
const { caPrivateKey } = await getCaCredentials({
const { caPrivateKey, caSecret } = await getCaCredentials({
caId: ca.id,
certificateAuthorityDAL,
certificateAuthoritySecretDAL,
@@ -1150,9 +1153,15 @@ export const certificateAuthorityServiceFactory = ({
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[] = [
new x509.KeyUsagesExtension(x509.KeyUsageFlags.digitalSignature | x509.KeyUsageFlags.keyEncipherment, true),
new x509.BasicConstraintsExtension(false),
new x509.CRLDistributionPointsExtension([distributionPointUrl]),
await x509.AuthorityKeyIdentifierExtension.create(caCertObj, false),
await x509.SubjectKeyIdentifierExtension.create(csrObj.publicKey)
];
@@ -1203,7 +1212,7 @@ export const certificateAuthorityServiceFactory = ({
);
}
const serialNumber = crypto.randomBytes(32).toString("hex");
const serialNumber = createSerialNumber();
const leafCert = await x509.X509CertificateGenerator.create({
serialNumber,
subject: csrObj.subject,
@@ -1462,7 +1471,7 @@ export const certificateAuthorityServiceFactory = ({
);
}
const serialNumber = crypto.randomBytes(32).toString("hex");
const serialNumber = createSerialNumber();
const leafCert = await x509.X509CertificateGenerator.create({
serialNumber,
subject: csrObj.subject,