diff --git a/backend/package-lock.json b/backend/package-lock.json index 4cfc4c7b5..c07b9dce8 100644 --- a/backend/package-lock.json +++ b/backend/package-lock.json @@ -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", diff --git a/backend/package.json b/backend/package.json index e819aa7fe..e2b6fa685 100644 --- a/backend/package.json +++ b/backend/package.json @@ -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", diff --git a/backend/src/db/migrations/20240821212643_crl-ca-secret-binding.ts b/backend/src/db/migrations/20240821212643_crl-ca-secret-binding.ts new file mode 100644 index 000000000..eee243714 --- /dev/null +++ b/backend/src/db/migrations/20240821212643_crl-ca-secret-binding.ts @@ -0,0 +1,36 @@ +import { Knex } from "knex"; + +import { TableName } from "../schemas"; + +export async function up(knex: Knex): Promise { + 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 { + if (await knex.schema.hasTable(TableName.CertificateAuthorityCrl)) { + await knex.schema.alterTable(TableName.CertificateAuthorityCrl, (t) => { + t.dropColumn("caSecretId"); + }); + } +} diff --git a/backend/src/db/schemas/access-approval-requests-reviewers.ts b/backend/src/db/schemas/access-approval-requests-reviewers.ts index de9489288..a209df206 100644 --- a/backend/src/db/schemas/access-approval-requests-reviewers.ts +++ b/backend/src/db/schemas/access-approval-requests-reviewers.ts @@ -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(), diff --git a/backend/src/db/schemas/access-approval-requests.ts b/backend/src/db/schemas/access-approval-requests.ts index 5102c0eae..0b20202f5 100644 --- a/backend/src/db/schemas/access-approval-requests.ts +++ b/backend/src/db/schemas/access-approval-requests.ts @@ -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(), diff --git a/backend/src/db/schemas/certificate-authority-crl.ts b/backend/src/db/schemas/certificate-authority-crl.ts index 204a0c60c..3d63be5d8 100644 --- a/backend/src/db/schemas/certificate-authority-crl.ts +++ b/backend/src/db/schemas/certificate-authority-crl.ts @@ -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; diff --git a/backend/src/db/schemas/project-user-additional-privilege.ts b/backend/src/db/schemas/project-user-additional-privilege.ts index bd69f1484..e657fc945 100644 --- a/backend/src/db/schemas/project-user-additional-privilege.ts +++ b/backend/src/db/schemas/project-user-additional-privilege.ts @@ -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(), diff --git a/backend/src/ee/routes/v1/certificate-authority-crl-router.ts b/backend/src/ee/routes/v1/certificate-authority-crl-router.ts index 10792f508..468981c0e 100644 --- a/backend/src/ee/routes/v1/certificate-authority-crl-router.ts +++ b/backend/src/ee/routes/v1/certificate-authority-crl-router.ts @@ -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" - // }; - // } - // }); }; diff --git a/backend/src/ee/routes/v1/index.ts b/backend/src/ee/routes/v1/index.ts index 1892bd5e8..961e06949 100644 --- a/backend/src/ee/routes/v1/index.ts +++ b/backend/src/ee/routes/v1/index.ts @@ -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" } ); diff --git a/backend/src/ee/services/audit-log/audit-log-types.ts b/backend/src/ee/services/audit-log/audit-log-types.ts index 0d07d005b..4789200b9 100644 --- a/backend/src/ee/services/audit-log/audit-log-types.ts +++ b/backend/src/ee/services/audit-log/audit-log-types.ts @@ -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 diff --git a/backend/src/ee/services/certificate-authority-crl/certificate-authority-crl-service.ts b/backend/src/ee/services/certificate-authority-crl/certificate-authority-crl-service.ts index 2ef924ffb..43f897a27 100644 --- a/backend/src/ee/services/certificate-authority-crl/certificate-authority-crl-service.ts +++ b/backend/src/ee/services/certificate-authority-crl/certificate-authority-crl-service.ts @@ -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; - certificateAuthorityCrlDAL: Pick; + certificateAuthorityCrlDAL: Pick; projectDAL: Pick; kmsService: Pick; permissionService: Pick; - licenseService: Pick; + // licenseService: Pick; }; export type TCertificateAuthorityCrlServiceFactory = ReturnType; @@ -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 }; }; diff --git a/backend/src/ee/services/certificate-authority-crl/certificate-authority-crl-types.ts b/backend/src/ee/services/certificate-authority-crl/certificate-authority-crl-types.ts index fc31e9eef..9b82727e9 100644 --- a/backend/src/ee/services/certificate-authority-crl/certificate-authority-crl-types.ts +++ b/backend/src/ee/services/certificate-authority-crl/certificate-authority-crl-types.ts @@ -1,5 +1,7 @@ import { TProjectPermission } from "@app/lib/types"; -export type TGetCrl = { +export type TGetCrlById = string; + +export type TGetCaCrlsDTO = { caId: string; } & Omit; diff --git a/backend/src/lib/api-docs/constants.ts b/backend/src/lib/api-docs/constants.ts index f4d645165..2a823024a 100644 --- a/backend/src/lib/api-docs/constants.ts +++ b/backend/src/lib/api-docs/constants.ts @@ -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", diff --git a/backend/src/server/routes/index.ts b/backend/src/server/routes/index.ts index 2454e92cb..363688ac2 100644 --- a/backend/src/server/routes/index.ts +++ b/backend/src/server/routes/index.ts @@ -646,8 +646,8 @@ export const registerRoutes = async ( certificateAuthorityCrlDAL, projectDAL, kmsService, - permissionService, - licenseService + permissionService + // licenseService }); const certificateTemplateService = certificateTemplateServiceFactory({ diff --git a/backend/src/server/routes/v1/certificate-authority-router.ts b/backend/src/server/routes/v1/certificate-authority-router.ts index 35de2b953..429620410 100644 --- a/backend/src/server/routes/v1/certificate-authority-router.ts +++ b/backend/src/server/routes/v1/certificate-authority-router.ts @@ -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" + // }; + // } + // }); }; diff --git a/backend/src/services/certificate-authority/certificate-authority-fns.ts b/backend/src/services/certificate-authority/certificate-authority-fns.ts index b1fd87a26..7330f029b 100644 --- a/backend/src/services/certificate-authority/certificate-authority-fns.ts +++ b/backend/src/services/certificate-authority/certificate-authority-fns.ts @@ -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, diff --git a/backend/src/services/certificate-authority/certificate-authority-service.ts b/backend/src/services/certificate-authority/certificate-authority-service.ts index b2ee04c4c..dc39afe61 100644 --- a/backend/src/services/certificate-authority/certificate-authority-service.ts +++ b/backend/src/services/certificate-authority/certificate-authority-service.ts @@ -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, diff --git a/docs/api-reference/endpoints/certificate-authorities/crl.mdx b/docs/api-reference/endpoints/certificate-authorities/crl.mdx index a7b7755de..428c3377e 100644 --- a/docs/api-reference/endpoints/certificate-authorities/crl.mdx +++ b/docs/api-reference/endpoints/certificate-authorities/crl.mdx @@ -1,4 +1,4 @@ --- -title: "Retrieve CRL" -openapi: "GET /api/v1/pki/ca/{caId}/crl" +title: "List CRLs" +openapi: "GET /api/v1/pki/ca/{caId}/crls" --- diff --git a/docs/documentation/platform/pki/certificates.mdx b/docs/documentation/platform/pki/certificates.mdx index ab6f4df59..41a5cb8c9 100644 --- a/docs/documentation/platform/pki/certificates.mdx +++ b/docs/documentation/platform/pki/certificates.mdx @@ -151,18 +151,24 @@ In the following steps, we explore how to revoke a X.509 certificate under a CA In order to check the revocation status of a certificate, you can check it - against the CRL of a CA by selecting the **View CRL** option under the - issuing CA and downloading the CRL file. + against the CRL of a CA by heading to its Issuing CA and downloading the CRL. ![pki view crl](/images/platform/pki/ca-crl.png) - ![pki download crl](/images/platform/pki/ca-crl-modal.png) - To verify a certificate against the downloaded CRL with OpenSSL, you can use the following command: ```bash openssl verify -crl_check -CAfile chain.pem -CRLfile crl.pem cert.pem +``` + +Note that you can also obtain the CRL from the certificate itself by +referencing the CRL distribution point extension on the certificate itself. + +To check a certificate against the CRL distribution point specified within it with OpenSSL, you can use the following command: + +```bash +openssl verify -verbose -crl_check -crl_download -CAfile chain.pem cert.pem ``` @@ -197,21 +203,25 @@ openssl verify -crl_check -CAfile chain.pem -CRLfile crl.pem cert.pem In order to check the revocation status of a certificate, you can check it against the CRL of the issuing CA. - To obtain the CRL of the CA, make an API request to the [Get CRL](/api-reference/endpoints/certificate-authorities/crl) API endpoint. + To obtain the CRLs of the CA, make an API request to the [List CRLs](/api-reference/endpoints/certificate-authorities/crls) API endpoint. ### Sample request ```bash Request - curl --location --request GET 'https://app.infisical.com/api/v1/pki/ca//crl' \ + curl --location --request GET 'https://app.infisical.com/api/v1/pki/ca//crls' \ --header 'Authorization: Bearer ' ``` ### Sample response ```bash Response - { - crl: "..." - } + [ + { + id: "...", + crl: "..." + }, + ... + ] ``` To verify a certificate against the CRL with OpenSSL, you can use the following command: diff --git a/docs/documentation/platform/pki/private-ca.mdx b/docs/documentation/platform/pki/private-ca.mdx index aff6fae05..0baa13abb 100644 --- a/docs/documentation/platform/pki/private-ca.mdx +++ b/docs/documentation/platform/pki/private-ca.mdx @@ -327,10 +327,10 @@ the certificate back to the intermediate CA. 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. - + Yes. You may obtain a CSR from the Intermediate CA and use it to generate a - certificate from your external Root CA. The certificate, along with the Root - CA certificate, can be imported back to the Intermediate CA as part of the - CA installation step. + certificate from your external CA. The certificate, along with the external + CA certificate chain, can be imported back to the Intermediate CA as part of + the CA installation step. diff --git a/docs/images/platform/pki/ca-crl-modal.png b/docs/images/platform/pki/ca-crl-modal.png deleted file mode 100644 index af26b1aca..000000000 Binary files a/docs/images/platform/pki/ca-crl-modal.png and /dev/null differ diff --git a/docs/images/platform/pki/ca-crl.png b/docs/images/platform/pki/ca-crl.png index 4794034a1..efe7d3b4a 100644 Binary files a/docs/images/platform/pki/ca-crl.png and b/docs/images/platform/pki/ca-crl.png differ diff --git a/docs/mint.json b/docs/mint.json index 7b8fcdc42..b15949e57 100644 --- a/docs/mint.json +++ b/docs/mint.json @@ -692,7 +692,7 @@ "api-reference/endpoints/certificate-authorities/import-cert", "api-reference/endpoints/certificate-authorities/issue-cert", "api-reference/endpoints/certificate-authorities/sign-cert", - "api-reference/endpoints/certificate-authorities/crl" + "api-reference/endpoints/certificate-authorities/crls" ] }, { diff --git a/frontend/src/hooks/api/ca/index.tsx b/frontend/src/hooks/api/ca/index.tsx index 45e675d4b..ef9bf09f2 100644 --- a/frontend/src/hooks/api/ca/index.tsx +++ b/frontend/src/hooks/api/ca/index.tsx @@ -1,4 +1,4 @@ -export { CaRenewalType,CaStatus, CaType } from "./enums"; +export { CaRenewalType, CaStatus, CaType } from "./enums"; export { useCreateCa, useCreateCertificate, @@ -6,5 +6,6 @@ export { useImportCaCertificate, useRenewCa, useSignIntermediate, - useUpdateCa} from "./mutations"; -export { useGetCaById, useGetCaCert, useGetCaCerts, useGetCaCrl, useGetCaCsr } from "./queries"; + useUpdateCa +} from "./mutations"; +export { useGetCaById, useGetCaCert, useGetCaCerts, useGetCaCrls,useGetCaCsr } from "./queries"; diff --git a/frontend/src/hooks/api/ca/queries.tsx b/frontend/src/hooks/api/ca/queries.tsx index 278c335ca..5da16462b 100644 --- a/frontend/src/hooks/api/ca/queries.tsx +++ b/frontend/src/hooks/api/ca/queries.tsx @@ -7,6 +7,7 @@ import { TCertificateAuthority } from "./types"; export const caKeys = { getCaById: (caId: string) => [{ caId }, "ca"], getCaCerts: (caId: string) => [{ caId }, "ca-cert"], + getCaCrls: (caId: string) => [{ caId }, "ca-crls"], getCaCert: (caId: string) => [{ caId }, "ca-cert"], getCaCsr: (caId: string) => [{ caId }, "ca-csr"], 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({ - queryKey: caKeys.getCaCrl(caId), + queryKey: caKeys.getCaCrls(caId), queryFn: async () => { - const { - data: { crl } - } = await apiRequest.get<{ - crl: string; - }>(`/api/v1/pki/ca/${caId}/crl`); - return crl; + const { data } = await apiRequest.get< + { + id: string; + crl: string; + }[] + >(`/api/v1/pki/ca/${caId}/crls`); + return data; }, enabled: Boolean(caId) }); diff --git a/frontend/src/views/Project/CaPage/CaPage.tsx b/frontend/src/views/Project/CaPage/CaPage.tsx index e0bdaeb64..23f94570c 100644 --- a/frontend/src/views/Project/CaPage/CaPage.tsx +++ b/frontend/src/views/Project/CaPage/CaPage.tsx @@ -22,7 +22,12 @@ 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 { CaCertificatesSection, CaDetailsSection, CaRenewalModal } from "./components"; +import { + CaCertificatesSection, + CaCrlsSection, + CaDetailsSection, + CaRenewalModal +} from "./components"; export const CaPage = withProjectPermission( () => { @@ -118,7 +123,10 @@ export const CaPage = withProjectPermission(
- +
+ + +
)} diff --git a/frontend/src/views/Project/CaPage/components/CaCrlsSection/CaCrlsSection.tsx b/frontend/src/views/Project/CaPage/components/CaCrlsSection/CaCrlsSection.tsx new file mode 100644 index 000000000..6778a77a3 --- /dev/null +++ b/frontend/src/views/Project/CaPage/components/CaCrlsSection/CaCrlsSection.tsx @@ -0,0 +1,20 @@ +import { CaCrlsTable } from "./CaCrlsTable"; + +type Props = { + caId: string; +}; + +export const CaCrlsSection = ({ caId }: Props) => { + return ( +
+
+

+ CA Certificate Revocation Lists (CRLs) +

+
+
+ +
+
+ ); +}; diff --git a/frontend/src/views/Project/CaPage/components/CaCrlsSection/CaCrlsTable.tsx b/frontend/src/views/Project/CaPage/components/CaCrlsSection/CaCrlsTable.tsx new file mode 100644 index 000000000..3fcdd2018 --- /dev/null +++ b/frontend/src/views/Project/CaPage/components/CaCrlsSection/CaCrlsTable.tsx @@ -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 ( + + + + + + {/* */} + {/* */} + + + + {isLoading && } + {!isLoading && + caCrls?.map(({ id, crl }) => { + // const caCrlObj = new x509.X509Crl(crl); + return ( + + + {/* */} + {/* */} + + + ); + })} + +
Distribution Point URLThis UpdateNext Update +
+
+ {`${window.origin}/api/v1/pki/crl/${id}`} +
+
{format(new Date(caCrlObj.thisUpdate), "yyyy-MM-dd")} + {caCrlObj.nextUpdate + ? format(new Date(caCrlObj.nextUpdate), "yyyy-MM-dd") + : "-"} + + + { + e.stopPropagation(); + downloadTxtFile("crl.pem", crl); + }} + > + + + +
+ {!isLoading && !caCrls?.length && ( + + )} +
+ ); +}; diff --git a/frontend/src/views/Project/CaPage/components/CaCrlsSection/index.tsx b/frontend/src/views/Project/CaPage/components/CaCrlsSection/index.tsx new file mode 100644 index 000000000..a46d94d7b --- /dev/null +++ b/frontend/src/views/Project/CaPage/components/CaCrlsSection/index.tsx @@ -0,0 +1 @@ +export { CaCrlsSection } from "./CaCrlsSection"; diff --git a/frontend/src/views/Project/CaPage/components/index.tsx b/frontend/src/views/Project/CaPage/components/index.tsx index 78889e9b4..95a4868f2 100644 --- a/frontend/src/views/Project/CaPage/components/index.tsx +++ b/frontend/src/views/Project/CaPage/components/index.tsx @@ -1,3 +1,4 @@ export { CaCertificatesSection } from "./CaCertificatesSection/CaCertificatesSection"; +export { CaCrlsSection } from "./CaCrlsSection"; export { CaDetailsSection } from "./CaDetailsSection"; export { CaRenewalModal } from "./CaRenewalModal"; diff --git a/frontend/src/views/Project/CertificatesPage/components/CaTab/components/CaCrlModal.tsx b/frontend/src/views/Project/CertificatesPage/components/CaTab/components/CaCrlModal.tsx deleted file mode 100644 index 77e67f32e..000000000 --- a/frontend/src/views/Project/CertificatesPage/components/CaTab/components/CaCrlModal.tsx +++ /dev/null @@ -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 ( - { - handlePopUpToggle("caCrl", isOpen); - }} - > - -
- {crl && ( - <> - {/*
-

Manual CRL Rotation

- -
*/} -
-

Certificate Revocation List

-
- { - navigator.clipboard.writeText(crl); - setIsCrlCopied.on(); - }} - > - - - Copy - - - { - downloadTxtFile("crl.pem", crl); - }} - > - - - Download - - -
-
-
-

{crl}

-
- - )} -
-
-
- ); -}; diff --git a/frontend/src/views/Project/CertificatesPage/components/CaTab/components/CaSection.tsx b/frontend/src/views/Project/CertificatesPage/components/CaTab/components/CaSection.tsx index 86e617a56..7184abdb6 100644 --- a/frontend/src/views/Project/CertificatesPage/components/CaTab/components/CaSection.tsx +++ b/frontend/src/views/Project/CertificatesPage/components/CaTab/components/CaSection.tsx @@ -9,7 +9,6 @@ import { CaStatus, useDeleteCa, useUpdateCa } from "@app/hooks/api"; import { usePopUp } from "@app/hooks/usePopUp"; import { CaCertModal } from "./CaCertModal"; -import { CaCrlModal } from "./CaCrlModal"; import { CaInstallCertModal } from "./CaInstallCertModal"; import { CaModal } from "./CaModal"; import { CaTable } from "./CaTable"; @@ -25,7 +24,6 @@ export const CaSection = () => { "installCaCert", "deleteCa", "caStatus", // enable / disable - "caCrl", // enable / disable "upgradePlan" ] as const); @@ -95,7 +93,6 @@ export const CaSection = () => { - , data?: { caId?: string; @@ -59,7 +53,6 @@ type Props = { export const CaTable = ({ handlePopUpOpen }: Props) => { const router = useRouter(); - const { subscription } = useSubscription(); const { currentWorkspace } = useWorkspace(); const { data, isLoading } = useListWorkspaceCas({ projectSlug: currentWorkspace?.slug ?? "" @@ -162,38 +155,6 @@ export const CaTable = ({ handlePopUpOpen }: Props) => { )} )} - {ca.status !== CaStatus.PENDING_CERTIFICATE && ( - - {(isAllowed) => ( - { - 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={} - > - View CRL - - )} - - )} { {certificate.friendlyName} - - {label} - + {certificate.status === CertStatus.REVOKED ? ( + Revoked + ) : ( + {label} + )} {certificate.notBefore