diff --git a/backend/package-lock.json b/backend/package-lock.json index 18b1548f1..ca586cbbb 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", @@ -8078,9 +8078,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 fbd02365b..5fecde116 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..11825722a 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 @@ -5,17 +5,17 @@ import { TCertificateAuthorityCrlDALFactory } from "@app/ee/services/certificate 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; @@ -33,9 +33,42 @@ export const certificateAuthorityCrlServiceFactory = ({ 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); + + // 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); if (!ca) throw new BadRequestError({ message: "CA not found" }); @@ -56,11 +89,10 @@ export const certificateAuthorityCrlServiceFactory = ({ 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." + "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 +104,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 +206,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/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 32dbc8fd0..7d0b4afa2 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, @@ -1131,7 +1134,7 @@ export const certificateAuthorityServiceFactory = ({ attributes: [new x509.ChallengePasswordAttribute("password")] }); - const { caPrivateKey } = await getCaCredentials({ + const { caPrivateKey, caSecret } = await getCaCredentials({ caId: ca.id, certificateAuthorityDAL, certificateAuthoritySecretDAL, @@ -1139,9 +1142,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) ]; @@ -1192,7 +1201,7 @@ export const certificateAuthorityServiceFactory = ({ ); } - const serialNumber = crypto.randomBytes(32).toString("hex"); + const serialNumber = createSerialNumber(); const leafCert = await x509.X509CertificateGenerator.create({ serialNumber, 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({ serialNumber, subject: csrObj.subject, 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 7edc99af9..23f94570c 100644 --- a/frontend/src/views/Project/CaPage/CaPage.tsx +++ b/frontend/src/views/Project/CaPage/CaPage.tsx @@ -22,8 +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 { TabSections } from "../Types"; -import { CaCertificatesSection, CaDetailsSection, CaRenewalModal } from "./components"; +import { + CaCertificatesSection, + CaCrlsSection, + CaDetailsSection, + CaRenewalModal +} from "./components"; export const CaPage = withProjectPermission( () => { @@ -119,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