From 587a4a1120bbe425d0e985db596ffbc88091ad5f Mon Sep 17 00:00:00 2001 From: Tuan Dang Date: Mon, 5 Aug 2024 11:00:22 -0700 Subject: [PATCH 1/7] Make progress on ca cert versioning --- .../20240802181855_ca-cert-version.ts | 114 ++++++ .../src/db/schemas/certificate-authorities.ts | 3 +- .../db/schemas/certificate-authority-certs.ts | 4 +- backend/src/db/schemas/dynamic-secrets.ts | 10 +- backend/src/db/schemas/webhooks.ts | 15 +- .../ee/services/audit-log/audit-log-types.ts | 10 + backend/src/lib/api-docs/constants.ts | 8 + .../routes/v1/certificate-authority-router.ts | 110 +++++- .../certificate-authority-fns.ts | 56 ++- .../certificate-authority-service.ts | 366 ++++++++++++++++-- .../certificate-authority-types.ts | 22 ++ frontend/src/components/v2/index.tsx | 1 + frontend/src/hooks/api/ca/constants.tsx | 13 +- frontend/src/hooks/api/ca/enums.tsx | 4 + frontend/src/hooks/api/ca/index.tsx | 8 +- frontend/src/hooks/api/ca/mutations.tsx | 30 +- frontend/src/hooks/api/ca/queries.tsx | 21 +- frontend/src/hooks/api/ca/types.ts | 15 +- .../pages/project/[id]/ca/[caId]/index.tsx | 18 + frontend/src/views/Project/CaPage/CaPage.tsx | 147 +++++++ .../CaCertificatesSection.tsx | 31 ++ .../CaCertificatesTable.tsx | 131 +++++++ .../CaCertificatesSection/index.tsx | 1 + .../CaPage/components/CaDetailsSection.tsx | 141 +++++++ .../CaPage/components/CaRenewalModal.tsx | 215 ++++++++++ .../views/Project/CaPage/components/index.tsx | 3 + frontend/src/views/Project/CaPage/index.tsx | 1 + .../components/CaTab/components/CaTable.tsx | 66 +++- 28 files changed, 1490 insertions(+), 74 deletions(-) create mode 100644 backend/src/db/migrations/20240802181855_ca-cert-version.ts create mode 100644 frontend/src/pages/project/[id]/ca/[caId]/index.tsx create mode 100644 frontend/src/views/Project/CaPage/CaPage.tsx create mode 100644 frontend/src/views/Project/CaPage/components/CaCertificatesSection/CaCertificatesSection.tsx create mode 100644 frontend/src/views/Project/CaPage/components/CaCertificatesSection/CaCertificatesTable.tsx create mode 100644 frontend/src/views/Project/CaPage/components/CaCertificatesSection/index.tsx create mode 100644 frontend/src/views/Project/CaPage/components/CaDetailsSection.tsx create mode 100644 frontend/src/views/Project/CaPage/components/CaRenewalModal.tsx create mode 100644 frontend/src/views/Project/CaPage/components/index.tsx create mode 100644 frontend/src/views/Project/CaPage/index.tsx diff --git a/backend/src/db/migrations/20240802181855_ca-cert-version.ts b/backend/src/db/migrations/20240802181855_ca-cert-version.ts new file mode 100644 index 000000000..b1fffe4a3 --- /dev/null +++ b/backend/src/db/migrations/20240802181855_ca-cert-version.ts @@ -0,0 +1,114 @@ +import { Knex } from "knex"; + +import { TableName } from "../schemas"; + +export async function up(knex: Knex): Promise { + if (await knex.schema.hasTable(TableName.CertificateAuthority)) { + const hasActiveCaCertVersionColumn = await knex.schema.hasColumn( + TableName.CertificateAuthority, + "activeCaCertVersion" + ); + if (!hasActiveCaCertVersionColumn) { + await knex.schema.alterTable(TableName.CertificateAuthority, (t) => { + t.integer("activeCaCertVersion").nullable(); + }); + + await knex(TableName.CertificateAuthority).where("status", "active").update({ activeCaCertVersion: 1 }); + } + } + + if (await knex.schema.hasTable(TableName.CertificateAuthorityCert)) { + const hasVersionColumn = await knex.schema.hasColumn(TableName.CertificateAuthorityCert, "version"); + if (!hasVersionColumn) { + await knex.schema.alterTable(TableName.CertificateAuthorityCert, (t) => { + t.integer("version").nullable(); + // t.dropUnique(["caId"]); + }); + + await knex(TableName.CertificateAuthorityCert).update({ version: 1 }).whereNull("version"); + + await knex.schema.alterTable(TableName.CertificateAuthorityCert, (t) => { + t.integer("version").notNullable().alter(); + }); + } + + const hasCaSecretIdColumn = await knex.schema.hasColumn(TableName.CertificateAuthorityCert, "caSecretId"); + if (!hasCaSecretIdColumn) { + await knex.schema.alterTable(TableName.CertificateAuthorityCert, (t) => { + t.uuid("caSecretId").nullable(); + t.foreign("caSecretId").references("id").inTable(TableName.CertificateAuthoritySecret).onDelete("CASCADE"); + }); + + await knex.raw(` + UPDATE "${TableName.CertificateAuthorityCert}" cert + SET "caSecretId" = ( + SELECT sec.id + FROM "${TableName.CertificateAuthoritySecret}" sec + WHERE sec."caId" = cert."caId" + ) + `); + + // await knex.raw(` + // UPDATE ${TableName.CertificateAuthorityCert} cert + // SET caSecretId = ( + // SELECT sec.id + // FROM ${TableName.CertificateAuthoritySecret} sec + // WHERE sec."caId" = cert."caId" + // ) + // `); + + // await knex(TableName.CertificateAuthorityCert).update({ + // caSecretId: knex(TableName.CertificateAuthoritySecret) + // .select("id") + // .whereRaw("?? = ??", ["CertificateAuthoritySecret.caId", "CertificateAuthorityCert.caId"]) + // }); + + // await knex(TableName.CertificateAuthorityCert).update({ + // caSecretId: function () { + // this.select("id") + // .from(TableName.CertificateAuthoritySecret) + // .whereRaw("??.?? = ??.??", [ + // TableName.CertificateAuthoritySecret, + // "caId", + // TableName.CertificateAuthorityCert, + // "caId" + // ]); + // } + // }); + + await knex.schema.alterTable(TableName.CertificateAuthorityCert, (t) => { + t.uuid("caSecretId").notNullable().alter(); + }); + } + } + + // if (await knex.schema.hasTable(TableName.CertificateAuthoritySecret)) { + // await knex.schema.alterTable(TableName.CertificateAuthoritySecret, (t) => { + // t.dropUnique(["caId"]); + // }); + // } +} + +export async function down(knex: Knex): Promise { + if (await knex.schema.hasTable(TableName.CertificateAuthority)) { + if (await knex.schema.hasColumn(TableName.CertificateAuthority, "activeCaCertVersion")) { + await knex.schema.alterTable(TableName.CertificateAuthority, (t) => { + t.dropColumn("activeCaCertVersion"); + }); + } + } + + if (await knex.schema.hasTable(TableName.CertificateAuthorityCert)) { + if (await knex.schema.hasColumn(TableName.CertificateAuthorityCert, "version")) { + await knex.schema.alterTable(TableName.CertificateAuthorityCert, (t) => { + t.dropColumn("version"); + }); + } + + if (await knex.schema.hasColumn(TableName.CertificateAuthorityCert, "caSecretId")) { + await knex.schema.alterTable(TableName.CertificateAuthorityCert, (t) => { + t.dropColumn("caSecretId"); + }); + } + } +} diff --git a/backend/src/db/schemas/certificate-authorities.ts b/backend/src/db/schemas/certificate-authorities.ts index 16f303b5c..9255b037e 100644 --- a/backend/src/db/schemas/certificate-authorities.ts +++ b/backend/src/db/schemas/certificate-authorities.ts @@ -27,7 +27,8 @@ export const CertificateAuthoritiesSchema = z.object({ maxPathLength: z.number().nullable().optional(), keyAlgorithm: z.string(), notBefore: z.date().nullable().optional(), - notAfter: z.date().nullable().optional() + notAfter: z.date().nullable().optional(), + activeCaCertVersion: z.number().nullable().optional() }); export type TCertificateAuthorities = z.infer; diff --git a/backend/src/db/schemas/certificate-authority-certs.ts b/backend/src/db/schemas/certificate-authority-certs.ts index 96ad54f00..7074ce409 100644 --- a/backend/src/db/schemas/certificate-authority-certs.ts +++ b/backend/src/db/schemas/certificate-authority-certs.ts @@ -15,7 +15,9 @@ export const CertificateAuthorityCertsSchema = z.object({ updatedAt: z.date(), caId: z.string().uuid(), encryptedCertificate: zodBuffer, - encryptedCertificateChain: zodBuffer + encryptedCertificateChain: zodBuffer, + version: z.number(), + caSecretId: z.string().uuid() }); export type TCertificateAuthorityCerts = z.infer; diff --git a/backend/src/db/schemas/dynamic-secrets.ts b/backend/src/db/schemas/dynamic-secrets.ts index b27da396c..d90f1f7d2 100644 --- a/backend/src/db/schemas/dynamic-secrets.ts +++ b/backend/src/db/schemas/dynamic-secrets.ts @@ -5,6 +5,8 @@ import { z } from "zod"; +import { zodBuffer } from "@app/lib/zod"; + import { TImmutableDBKeys } from "./models"; export const DynamicSecretsSchema = z.object({ @@ -14,16 +16,12 @@ export const DynamicSecretsSchema = z.object({ type: z.string(), defaultTTL: z.string(), maxTTL: z.string().nullable().optional(), - inputIV: z.string(), - inputCiphertext: z.string(), - inputTag: z.string(), - algorithm: z.string().default("aes-256-gcm"), - keyEncoding: z.string().default("utf8"), folderId: z.string().uuid(), status: z.string().nullable().optional(), statusDetails: z.string().nullable().optional(), createdAt: z.date(), - updatedAt: z.date() + updatedAt: z.date(), + encryptedConfig: zodBuffer }); export type TDynamicSecrets = z.infer; diff --git a/backend/src/db/schemas/webhooks.ts b/backend/src/db/schemas/webhooks.ts index a7aac2933..3f670497f 100644 --- a/backend/src/db/schemas/webhooks.ts +++ b/backend/src/db/schemas/webhooks.ts @@ -5,27 +5,22 @@ import { z } from "zod"; +import { zodBuffer } from "@app/lib/zod"; + import { TImmutableDBKeys } from "./models"; export const WebhooksSchema = z.object({ id: z.string().uuid(), secretPath: z.string().default("/"), - url: z.string(), lastStatus: z.string().nullable().optional(), lastRunErrorMessage: z.string().nullable().optional(), isDisabled: z.boolean().default(false), - encryptedSecretKey: z.string().nullable().optional(), - iv: z.string().nullable().optional(), - tag: z.string().nullable().optional(), - algorithm: z.string().nullable().optional(), - keyEncoding: z.string().nullable().optional(), createdAt: z.date(), updatedAt: z.date(), envId: z.string().uuid(), - urlCipherText: z.string().nullable().optional(), - urlIV: z.string().nullable().optional(), - urlTag: z.string().nullable().optional(), - type: z.string().default("general").nullable().optional() + type: z.string().default("general").nullable().optional(), + encryptedSecretKeyWithKms: zodBuffer.nullable().optional(), + encryptedUrl: zodBuffer }); export type TWebhooks = z.infer; 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 76bf73d8c..61c2ecfa7 100644 --- a/backend/src/ee/services/audit-log/audit-log-types.ts +++ b/backend/src/ee/services/audit-log/audit-log-types.ts @@ -131,6 +131,7 @@ export enum EventType { UPDATE_CA = "update-certificate-authority", DELETE_CA = "delete-certificate-authority", GET_CA_CSR = "get-certificate-authority-csr", + GET_CA_CERTS = "get-certificate-authority-certs", GET_CA_CERT = "get-certificate-authority-cert", SIGN_INTERMEDIATE = "sign-intermediate", IMPORT_CA_CERT = "import-certificate-authority-cert", @@ -1101,6 +1102,14 @@ interface GetCaCsr { }; } +interface GetCaCerts { + type: EventType.GET_CA_CERTS; + metadata: { + caId: string; + dn: string; + }; +} + interface GetCaCert { type: EventType.GET_CA_CERT; metadata: { @@ -1328,6 +1337,7 @@ export type Event = | UpdateCa | DeleteCa | GetCaCsr + | GetCaCerts | GetCaCert | SignIntermediate | ImportCaCert diff --git a/backend/src/lib/api-docs/constants.ts b/backend/src/lib/api-docs/constants.ts index e151ffb85..575af97f9 100644 --- a/backend/src/lib/api-docs/constants.ts +++ b/backend/src/lib/api-docs/constants.ts @@ -1048,6 +1048,14 @@ export const CERTIFICATE_AUTHORITIES = { caId: "The ID of the CA to generate CSR from", csr: "The generated CSR from the CA" }, + RENEW_CA_CERT: { + caId: "The ID of the CA to renew the CA certificate for", + type: "The type of behavior to use for the renewal operation. Currently Infisical is only able to renew a CA certificate with the same key pair.", + notAfter: "The expiry date and time for the renewed CA certificatre in YYYY-MM-DDTHH:mm:ss.sssZ format", + certificate: "The renewed CA certificate body", + certificateChain: "The certificate chain of the CA", + serialNumber: "The serial number of the renewed CA certificate" + }, GET_CERT: { caId: "The ID of the CA to get the certificate body and certificate chain from", certificate: "The certificate body of the CA", diff --git a/backend/src/server/routes/v1/certificate-authority-router.ts b/backend/src/server/routes/v1/certificate-authority-router.ts index 533f249f2..ad49bacaf 100644 --- a/backend/src/server/routes/v1/certificate-authority-router.ts +++ b/backend/src/server/routes/v1/certificate-authority-router.ts @@ -8,7 +8,7 @@ import { readLimit, writeLimit } from "@app/server/config/rateLimiter"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; import { AuthMode } from "@app/services/auth/auth-type"; import { CertKeyAlgorithm } from "@app/services/certificate/certificate-types"; -import { CaStatus, CaType } from "@app/services/certificate-authority/certificate-authority-types"; +import { CaRenewalType, CaStatus, CaType } from "@app/services/certificate-authority/certificate-authority-types"; import { validateAltNamesField, validateCaDateField @@ -275,15 +275,119 @@ export const registerCaRouter = async (server: FastifyZodProvider) => { } }); + server.route({ + method: "POST", + url: "/:caId/renew", // TODO + config: { + rateLimit: writeLimit + }, + onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), + schema: { + description: "Renew CA certificate for CA", + params: z.object({ + caId: z.string().trim().describe(CERTIFICATE_AUTHORITIES.RENEW_CA_CERT.caId) + }), + body: z.object({ + type: z.nativeEnum(CaRenewalType).describe(CERTIFICATE_AUTHORITIES.RENEW_CA_CERT.type), + notAfter: validateCaDateField.describe(CERTIFICATE_AUTHORITIES.RENEW_CA_CERT.notAfter) + }), + response: { + 200: z.object({ + certificate: z.string().trim().describe(CERTIFICATE_AUTHORITIES.RENEW_CA_CERT.certificate), + certificateChain: z.string().trim().describe(CERTIFICATE_AUTHORITIES.RENEW_CA_CERT.certificateChain), + serialNumber: z.string().trim().describe(CERTIFICATE_AUTHORITIES.RENEW_CA_CERT.serialNumber) + }) + } + }, + handler: async (req) => { + const { certificate, certificateChain, serialNumber } = await server.services.certificateAuthority.renewCaCert({ + caId: req.params.caId, + actor: req.permission.type, + actorId: req.permission.id, + actorAuthMethod: req.permission.authMethod, + actorOrgId: req.permission.orgId, + ...req.body + }); + + // await server.services.auditLog.createAuditLog({ + // ...req.auditLogInfo, + // projectId: ca.projectId, + // event: { + // type: EventType.SIGN_INTERMEDIATE, + // metadata: { + // caId: ca.id, + // dn: ca.dn, + // serialNumber + // } + // } + // }); + + return { + certificate, + certificateChain, + serialNumber + }; + } + }); + server.route({ method: "GET", - url: "/:caId/certificate", + url: "/:caId/ca-certificates", config: { rateLimit: readLimit }, onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), schema: { - description: "Get cert and cert chain of a CA", + description: "Get list of past and current CA certificates for a CA", + params: z.object({ + caId: z.string().trim().describe(CERTIFICATE_AUTHORITIES.GET_CERT.caId) + }), + response: { + 200: z.array( + z.object({ + // TODO: consider not before and not after dates + certificate: z.string().describe(CERTIFICATE_AUTHORITIES.GET_CERT.certificate), + certificateChain: z.string().describe(CERTIFICATE_AUTHORITIES.GET_CERT.certificateChain), + serialNumber: z.string().describe(CERTIFICATE_AUTHORITIES.GET_CERT.serialNumber), + version: z.number() + }) + ) + } + }, + handler: async (req) => { + const { caCerts, ca } = await server.services.certificateAuthority.getCaCerts({ + caId: req.params.caId, + actor: req.permission.type, + actorId: req.permission.id, + actorAuthMethod: req.permission.authMethod, + actorOrgId: req.permission.orgId + }); + + await server.services.auditLog.createAuditLog({ + ...req.auditLogInfo, + projectId: ca.projectId, + event: { + type: EventType.GET_CA_CERTS, + metadata: { + caId: ca.id, + dn: ca.dn + } + } + }); + + return caCerts; + } + }); + + server.route({ + method: "GET", + url: "/:caId/certificate", // TODO: consider updating endpoint structure considering CA certificates + config: { + rateLimit: readLimit + }, + onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), + schema: { + description: "Get current CA cert and cert chain of a CA", params: z.object({ caId: z.string().trim().describe(CERTIFICATE_AUTHORITIES.GET_CERT.caId) }), diff --git a/backend/src/services/certificate-authority/certificate-authority-fns.ts b/backend/src/services/certificate-authority/certificate-authority-fns.ts index 32ac4bfb1..b58046555 100644 --- a/backend/src/services/certificate-authority/certificate-authority-fns.ts +++ b/backend/src/services/certificate-authority/certificate-authority-fns.ts @@ -5,7 +5,13 @@ import { BadRequestError } from "@app/lib/errors"; import { getProjectKmsCertificateKeyId } from "@app/services/project/project-fns"; import { CertKeyAlgorithm, CertStatus } from "../certificate/certificate-types"; -import { TDNParts, TGetCaCertChainDTO, TGetCaCredentialsDTO, TRebuildCaCrlDTO } from "./certificate-authority-types"; +import { + TDNParts, + TGetCaCertChainDTO, + TGetCaCertChainsDTO, + TGetCaCredentialsDTO, + TRebuildCaCrlDTO +} from "./certificate-authority-types"; export const createDistinguishedName = (parts: TDNParts) => { const dnParts = []; @@ -98,11 +104,59 @@ export const getCaCredentials = async ({ ]); return { + caSecret, caPrivateKey, caPublicKey }; }; +/** + * Return the list of decrypted pem-encoded certificates and certificate chains + * for CA with id [caId]. + */ +export const getCaCertChains = async ({ + caId, + certificateAuthorityDAL, + certificateAuthorityCertDAL, + projectDAL, + kmsService +}: TGetCaCertChainsDTO) => { + const ca = await certificateAuthorityDAL.findById(caId); + if (!ca) throw new BadRequestError({ message: "CA not found" }); + + const keyId = await getProjectKmsCertificateKeyId({ + projectId: ca.projectId, + projectDAL, + kmsService + }); + + const kmsDecryptor = await kmsService.decryptWithKmsKey({ + kmsId: keyId + }); + + const caCerts = await certificateAuthorityCertDAL.find({ caId: ca.id }, { sort: [["version", "asc"]] }); + + const decryptedChains = await Promise.all( + caCerts.map(async (caCert) => { + const decryptedCaCert = await kmsDecryptor({ + cipherTextBlob: caCert.encryptedCertificate + }); + const caCertObj = new x509.X509Certificate(decryptedCaCert); + const decryptedChain = await kmsDecryptor({ + cipherTextBlob: caCert.encryptedCertificateChain + }); + return { + certificate: caCertObj.toString("pem"), + certificateChain: decryptedChain.toString("utf-8"), + serialNumber: caCertObj.serialNumber, + version: caCert.version + }; + }) + ); + + return decryptedChains; +}; + /** * Return the decrypted pem-encoded certificate and certificate chain * for CA with id [caId]. diff --git a/backend/src/services/certificate-authority/certificate-authority-service.ts b/backend/src/services/certificate-authority/certificate-authority-service.ts index a1390b1f7..2bee10263 100644 --- a/backend/src/services/certificate-authority/certificate-authority-service.ts +++ b/backend/src/services/certificate-authority/certificate-authority-service.ts @@ -20,7 +20,8 @@ import { TCertificateAuthorityCertDALFactory } from "./certificate-authority-cer import { TCertificateAuthorityDALFactory } from "./certificate-authority-dal"; import { createDistinguishedName, - getCaCertChain, + getCaCertChain, // TODO: consider rename + getCaCertChains, getCaCredentials, keyAlgorithmToAlgCfg } from "./certificate-authority-fns"; @@ -32,10 +33,12 @@ import { TCreateCaDTO, TDeleteCaDTO, TGetCaCertDTO, + TGetCaCertsDTO, TGetCaCsrDTO, TGetCaDTO, TImportCertToCaDTO, TIssueCertFromCaDTO, + TRenewCaCertDTO, TSignIntermediateDTO, TUpdateCaDTO } from "./certificate-authority-types"; @@ -46,7 +49,7 @@ type TCertificateAuthorityServiceFactoryDep = { TCertificateAuthorityDALFactory, "transaction" | "create" | "findById" | "updateById" | "deleteById" | "findOne" >; - certificateAuthorityCertDAL: Pick; + certificateAuthorityCertDAL: Pick; certificateAuthoritySecretDAL: Pick; certificateAuthorityCrlDAL: Pick; certificateAuthorityQueue: TCertificateAuthorityQueueFactory; // TODO: Pick @@ -148,7 +151,8 @@ export const certificateAuthorityServiceFactory = ({ maxPathLength, notBefore: notBeforeDate, notAfter: notAfterDate, - serialNumber + serialNumber, + activeCaCertVersion: 1 }) }, tx @@ -163,6 +167,24 @@ export const certificateAuthorityServiceFactory = ({ kmsId: certificateManagerKmsId }); + // https://nodejs.org/api/crypto.html#static-method-keyobjectfromkey + const skObj = KeyObject.from(keys.privateKey); + + const { cipherTextBlob: encryptedPrivateKey } = await kmsEncryptor({ + plainText: skObj.export({ + type: "pkcs8", + format: "der" + }) + }); + + const caSecret = await certificateAuthoritySecretDAL.create( + { + caId: ca.id, + encryptedPrivateKey + }, + tx + ); + if (type === CaType.ROOT) { // note: create self-signed cert only applicable for root CA const cert = await x509.X509CertificateGenerator.createSelfSigned({ @@ -193,7 +215,9 @@ export const certificateAuthorityServiceFactory = ({ { caId: ca.id, encryptedCertificate, - encryptedCertificateChain + encryptedCertificateChain, + version: 1, + caSecretId: caSecret.id }, tx ); @@ -221,24 +245,6 @@ export const certificateAuthorityServiceFactory = ({ tx ); - // https://nodejs.org/api/crypto.html#static-method-keyobjectfromkey - const skObj = KeyObject.from(keys.privateKey); - - const { cipherTextBlob: encryptedPrivateKey } = await kmsEncryptor({ - plainText: skObj.export({ - type: "pkcs8", - format: "der" - }) - }); - - await certificateAuthoritySecretDAL.create( - { - caId: ca.id, - encryptedPrivateKey - }, - tx - ); - return ca; }); @@ -379,7 +385,316 @@ export const certificateAuthorityServiceFactory = ({ }; /** - * Return certificate and certificate chain for CA + * Renew certificate for CA with id [caId] + * Note: Currently implements CA renewal with same key-pair only + */ + const renewCaCert = async ({ caId, notAfter, actorId, actorAuthMethod, actor, actorOrgId }: TRenewCaCertDTO) => { + const ca = await certificateAuthorityDAL.findById(caId); + if (!ca) throw new BadRequestError({ message: "CA not found" }); + + const { permission } = await permissionService.getProjectPermission( + actor, + actorId, + ca.projectId, + actorAuthMethod, + actorOrgId + ); + + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionActions.Create, + ProjectPermissionSub.CertificateAuthorities + ); + + if (ca.status === CaStatus.DISABLED) throw new BadRequestError({ message: "CA is disabled" }); + + // get latest CA certificate + const [caCert] = await certificateAuthorityCertDAL.find({ caId: ca.id }, { sort: [["version", "desc"]] }); + if (!caCert) throw new BadRequestError({ message: "CA does not have a certificate installed" }); + + const serialNumber = crypto.randomBytes(32).toString("hex"); + + const certificateManagerKmsId = await getProjectKmsCertificateKeyId({ + projectId: ca.projectId, + projectDAL, + kmsService + }); + + const kmsEncryptor = await kmsService.encryptWithKmsKey({ + kmsId: certificateManagerKmsId + }); + + const { caPrivateKey, caPublicKey, caSecret } = await getCaCredentials({ + caId: ca.id, + certificateAuthorityDAL, + certificateAuthoritySecretDAL, + projectDAL, + kmsService + }); + + const alg = keyAlgorithmToAlgCfg(ca.keyAlgorithm as CertKeyAlgorithm); + + const kmsDecryptor = await kmsService.decryptWithKmsKey({ + kmsId: certificateManagerKmsId + }); + const decryptedCaCert = await kmsDecryptor({ + cipherTextBlob: caCert.encryptedCertificate + }); + + const caCertObj = new x509.X509Certificate(decryptedCaCert); + + let certificate = ""; + let certificateChain = ""; + + switch (ca.type) { + case CaType.ROOT: { + if (new Date(notAfter) <= new Date(caCertObj.notAfter)) { + throw new BadRequestError({ + message: + "New Root CA certificate must have notAfter date that is greater than the current certificate notAfter date" + }); + } + + const notBeforeDate = new Date(); + const cert = await x509.X509CertificateGenerator.createSelfSigned({ + name: ca.dn, + serialNumber, + notBefore: notBeforeDate, + notAfter: new Date(notAfter), + signingAlgorithm: alg, + keys: { + privateKey: caPrivateKey, + publicKey: caPublicKey + }, + extensions: [ + new x509.BasicConstraintsExtension( + true, + ca.maxPathLength === -1 || !ca.maxPathLength ? undefined : ca.maxPathLength, + true + ), + new x509.ExtendedKeyUsageExtension(["1.2.3.4.5.6.7", "2.3.4.5.6.7.8"], true), + // eslint-disable-next-line no-bitwise + new x509.KeyUsagesExtension(x509.KeyUsageFlags.keyCertSign | x509.KeyUsageFlags.cRLSign, true), + await x509.SubjectKeyIdentifierExtension.create(caPublicKey) + ] + }); + + const { cipherTextBlob: encryptedCertificate } = await kmsEncryptor({ + plainText: Buffer.from(new Uint8Array(cert.rawData)) + }); + + const { cipherTextBlob: encryptedCertificateChain } = await kmsEncryptor({ + plainText: Buffer.alloc(0) + }); + + await certificateAuthorityDAL.transaction(async (tx) => { + const newActiveCaCertVersion = caCert.version + 1; + await certificateAuthorityCertDAL.create( + { + caId: ca.id, + encryptedCertificate, + encryptedCertificateChain, + version: newActiveCaCertVersion, + caSecretId: caSecret.id + }, + tx + ); + + await certificateAuthorityDAL.updateById( + ca.id, + { + activeCaCertVersion: newActiveCaCertVersion, + notBefore: notBeforeDate, + notAfter: new Date(notAfter) + }, + tx + ); + }); + + certificate = cert.toString("pem"); + break; + } + case CaType.INTERMEDIATE: { + if (!ca.parentCaId) { + // TODO: look into optimal way to support renewal of intermediate CA with external parent CA + throw new BadRequestError({ + message: "Failed to renew intermediate CA certificate with external parent CA" + }); + } + + const parentCa = await certificateAuthorityDAL.findById(ca.parentCaId); + const { caPrivateKey: parentCaPrivateKey } = await getCaCredentials({ + caId: parentCa.id, + certificateAuthorityDAL, + certificateAuthoritySecretDAL, + projectDAL, + kmsService + }); + + // get latest parent CA certificate + const [parentCaCert] = await certificateAuthorityCertDAL.find( + { caId: parentCa.id }, + { sort: [["version", "desc"]] } + ); + + const decryptedParentCaCert = await kmsDecryptor({ + cipherTextBlob: parentCaCert.encryptedCertificate + }); + + const parentCaCertObj = new x509.X509Certificate(decryptedParentCaCert); + + if (new Date(notAfter) <= new Date(caCertObj.notAfter)) { + throw new BadRequestError({ + message: + "New Intermediate CA certificate must have notAfter date that is greater than the current certificate notAfter date" + }); + } + + if (new Date(notAfter) > new Date(parentCaCertObj.notAfter)) { + throw new BadRequestError({ + message: + "New Intermediate CA certificate must have notAfter date that is equal to or smaller than the notAfter date of the parent CA certificate current certificate notAfter date" + }); + } + + const csrObj = await x509.Pkcs10CertificateRequestGenerator.create({ + name: ca.dn, + keys: { + privateKey: caPrivateKey, + publicKey: caPublicKey + }, + signingAlgorithm: alg, + extensions: [ + // eslint-disable-next-line no-bitwise + new x509.KeyUsagesExtension( + x509.KeyUsageFlags.keyCertSign | + x509.KeyUsageFlags.cRLSign | + x509.KeyUsageFlags.digitalSignature | + x509.KeyUsageFlags.keyEncipherment + ) + ], + attributes: [new x509.ChallengePasswordAttribute("password")] + }); + + const notBeforeDate = new Date(); + const intermediateCert = await x509.X509CertificateGenerator.create({ + serialNumber, + subject: csrObj.subject, + issuer: caCertObj.subject, + notBefore: notBeforeDate, + notAfter: new Date(notAfter), + signingKey: parentCaPrivateKey, + publicKey: csrObj.publicKey, + signingAlgorithm: alg, + extensions: [ + new x509.KeyUsagesExtension( + x509.KeyUsageFlags.keyCertSign | + x509.KeyUsageFlags.cRLSign | + x509.KeyUsageFlags.digitalSignature | + x509.KeyUsageFlags.keyEncipherment, + true + ), + new x509.BasicConstraintsExtension( + true, + ca.maxPathLength === -1 || !ca.maxPathLength ? undefined : ca.maxPathLength, + true + ), + await x509.AuthorityKeyIdentifierExtension.create(caCertObj, false), + await x509.SubjectKeyIdentifierExtension.create(csrObj.publicKey) + ] + }); + + const { cipherTextBlob: encryptedCertificate } = await kmsEncryptor({ + plainText: Buffer.from(new Uint8Array(intermediateCert.rawData)) + }); + + const { cipherTextBlob: encryptedCertificateChain } = await kmsEncryptor({ + plainText: Buffer.alloc(0) + }); + + await certificateAuthorityDAL.transaction(async (tx) => { + const newActiveCaCertVersion = caCert.version + 1; + await certificateAuthorityCertDAL.create( + { + caId: ca.id, + encryptedCertificate, + encryptedCertificateChain, + version: newActiveCaCertVersion, + caSecretId: caSecret.id + }, + tx + ); + + await certificateAuthorityDAL.updateById( + ca.id, + { + activeCaCertVersion: newActiveCaCertVersion, + notBefore: notBeforeDate, + notAfter: new Date(notAfter) + }, + tx + ); + }); + + const { caCert: issuingCaCertificate, caCertChain } = await getCaCertChain({ + caId, + certificateAuthorityDAL, + certificateAuthorityCertDAL, + projectDAL, + kmsService + }); + + certificate = intermediateCert.toString("pem"); + certificateChain = `${issuingCaCertificate}\n${caCertChain}`.trim(); + break; + } + default: { + throw new BadRequestError({ + message: "Unrecognized CA type" + }); + } + } + + return { + certificate, + certificateChain, + serialNumber + }; + }; + + const getCaCerts = async ({ caId, actorId, actorAuthMethod, actor, actorOrgId }: TGetCaCertsDTO) => { + const ca = await certificateAuthorityDAL.findById(caId); + if (!ca) throw new BadRequestError({ message: "CA not found" }); + + const { permission } = await permissionService.getProjectPermission( + actor, + actorId, + ca.projectId, + actorAuthMethod, + actorOrgId + ); + + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionActions.Read, + ProjectPermissionSub.CertificateAuthorities + ); + + const caCertChains = await getCaCertChains({ + caId, + certificateAuthorityDAL, + certificateAuthorityCertDAL, + projectDAL, + kmsService + }); + + return { + ca, + caCerts: caCertChains + }; + }; + + /** + * Return current certificate and certificate chain for CA + * get latest?? ca cert */ const getCaCert = async ({ caId, actorId, actorAuthMethod, actor, actorOrgId }: TGetCaCertDTO) => { const ca = await certificateAuthorityDAL.findById(caId); @@ -628,7 +943,8 @@ export const certificateAuthorityServiceFactory = ({ { caId: ca.id, encryptedCertificate, - encryptedCertificateChain + encryptedCertificateChain, + version: 1 }, tx ); @@ -857,6 +1173,8 @@ export const certificateAuthorityServiceFactory = ({ updateCaById, deleteCaById, getCaCsr, + renewCaCert, + getCaCerts, getCaCert, signIntermediate, importCertToCa, diff --git a/backend/src/services/certificate-authority/certificate-authority-types.ts b/backend/src/services/certificate-authority/certificate-authority-types.ts index 7818c3a3e..b129c2a1b 100644 --- a/backend/src/services/certificate-authority/certificate-authority-types.ts +++ b/backend/src/services/certificate-authority/certificate-authority-types.ts @@ -20,6 +20,10 @@ export enum CaStatus { PENDING_CERTIFICATE = "pending-certificate" } +export enum CaRenewalType { + EXISTING = "existing" +} + export type TCreateCaDTO = { projectSlug: string; type: CaType; @@ -53,6 +57,16 @@ export type TGetCaCsrDTO = { caId: string; } & Omit; +export type TRenewCaCertDTO = { + caId: string; + notAfter: string; + type: CaRenewalType; +} & Omit; + +export type TGetCaCertsDTO = { + caId: string; +} & Omit; + export type TGetCaCertDTO = { caId: string; } & Omit; @@ -98,6 +112,14 @@ export type TGetCaCredentialsDTO = { kmsService: Pick; }; +export type TGetCaCertChainsDTO = { + caId: string; + certificateAuthorityDAL: Pick; + certificateAuthorityCertDAL: Pick; + projectDAL: Pick; + kmsService: Pick; +}; + export type TGetCaCertChainDTO = { caId: string; certificateAuthorityDAL: Pick; diff --git a/frontend/src/components/v2/index.tsx b/frontend/src/components/v2/index.tsx index 3a5cef86b..9c640e84e 100644 --- a/frontend/src/components/v2/index.tsx +++ b/frontend/src/components/v2/index.tsx @@ -1,5 +1,6 @@ export * from "./Accordion"; export * from "./Alert"; +export * from "./Badge"; export * from "./Button"; export * from "./Card"; export * from "./Checkbox"; diff --git a/frontend/src/hooks/api/ca/constants.tsx b/frontend/src/hooks/api/ca/constants.tsx index dbef15ffd..e77dfe2b8 100644 --- a/frontend/src/hooks/api/ca/constants.tsx +++ b/frontend/src/hooks/api/ca/constants.tsx @@ -1,4 +1,4 @@ -import { CaStatus,CaType } from "./enums"; +import { CaStatus, CaType } from "./enums"; export const caTypeToNameMap: { [K in CaType]: string } = { [CaType.ROOT]: "Root", @@ -10,3 +10,14 @@ export const caStatusToNameMap: { [K in CaStatus]: string } = { [CaStatus.DISABLED]: "Disabled", [CaStatus.PENDING_CERTIFICATE]: "Pending Certificate" }; + +export const getStatusBadgeVariant = (status: CaStatus) => { + switch (status) { + case CaStatus.ACTIVE: + return "success"; + case CaStatus.DISABLED: + return "danger"; + default: + return "primary"; + } +}; diff --git a/frontend/src/hooks/api/ca/enums.tsx b/frontend/src/hooks/api/ca/enums.tsx index bdd498a12..35d86c452 100644 --- a/frontend/src/hooks/api/ca/enums.tsx +++ b/frontend/src/hooks/api/ca/enums.tsx @@ -8,3 +8,7 @@ export enum CaStatus { DISABLED = "disabled", PENDING_CERTIFICATE = "pending-certificate" } + +export enum CaRenewalType { + EXISTING = "existing" +} diff --git a/frontend/src/hooks/api/ca/index.tsx b/frontend/src/hooks/api/ca/index.tsx index 60b53478d..45e675d4b 100644 --- a/frontend/src/hooks/api/ca/index.tsx +++ b/frontend/src/hooks/api/ca/index.tsx @@ -1,10 +1,10 @@ -export { CaStatus, CaType } from "./enums"; +export { CaRenewalType,CaStatus, CaType } from "./enums"; export { useCreateCa, useCreateCertificate, useDeleteCa, useImportCaCertificate, + useRenewCa, useSignIntermediate, - useUpdateCa -} from "./mutations"; -export { useGetCaById, useGetCaCert, useGetCaCrl,useGetCaCsr } from "./queries"; + useUpdateCa} from "./mutations"; +export { useGetCaById, useGetCaCert, useGetCaCerts, useGetCaCrl, useGetCaCsr } from "./queries"; diff --git a/frontend/src/hooks/api/ca/mutations.tsx b/frontend/src/hooks/api/ca/mutations.tsx index bb018f71a..54109adfc 100644 --- a/frontend/src/hooks/api/ca/mutations.tsx +++ b/frontend/src/hooks/api/ca/mutations.tsx @@ -3,6 +3,7 @@ import { useMutation, useQueryClient } from "@tanstack/react-query"; import { apiRequest } from "@app/config/request"; import { workspaceKeys } from "../workspace/queries"; +import { caKeys } from "./queries"; import { TCertificateAuthority, TCreateCaDTO, @@ -11,10 +12,11 @@ import { TDeleteCaDTO, TImportCaCertificateDTO, TImportCaCertificateResponse, + TRenewCaDTO, + TRenewCaResponse, TSignIntermediateDTO, TSignIntermediateResponse, - TUpdateCaDTO -} from "./types"; + TUpdateCaDTO} from "./types"; export const useCreateCa = () => { const queryClient = useQueryClient(); @@ -84,8 +86,10 @@ export const useImportCaCertificate = () => { ); return data; }, - onSuccess: (_, { projectSlug }) => { + onSuccess: (_, { caId, projectSlug }) => { queryClient.invalidateQueries(workspaceKeys.getWorkspaceCas({ projectSlug })); + queryClient.invalidateQueries(caKeys.getCaCerts(caId)); + queryClient.invalidateQueries(caKeys.getCaCert(caId)); } }); }; @@ -106,3 +110,23 @@ export const useCreateCertificate = () => { } }); }; + +export const useRenewCa = () => { + const queryClient = useQueryClient(); + return useMutation({ + mutationFn: async (body) => { + const { data } = await apiRequest.post( + `/api/v1/pki/ca/${body.caId}/renew`, + body + ); + return data; + }, + onSuccess: (_, { caId, projectSlug }) => { + queryClient.invalidateQueries(workspaceKeys.getWorkspaceCas({ projectSlug })); + queryClient.invalidateQueries(caKeys.getCaCert(caId)); + queryClient.invalidateQueries(caKeys.getCaCerts(caId)); + queryClient.invalidateQueries(caKeys.getCaCsr(caId)); + queryClient.invalidateQueries(caKeys.getCaCrl(caId)); + } + }); +}; diff --git a/frontend/src/hooks/api/ca/queries.tsx b/frontend/src/hooks/api/ca/queries.tsx index e78274391..278c335ca 100644 --- a/frontend/src/hooks/api/ca/queries.tsx +++ b/frontend/src/hooks/api/ca/queries.tsx @@ -6,6 +6,7 @@ import { TCertificateAuthority } from "./types"; export const caKeys = { getCaById: (caId: string) => [{ caId }, "ca"], + getCaCerts: (caId: string) => [{ caId }, "ca-cert"], getCaCert: (caId: string) => [{ caId }, "ca-cert"], getCaCsr: (caId: string) => [{ caId }, "ca-csr"], getCaCrl: (caId: string) => [{ caId }, "ca-crl"] @@ -24,6 +25,24 @@ export const useGetCaById = (caId: string) => { }); }; +export const useGetCaCerts = (caId: string) => { + return useQuery({ + queryKey: caKeys.getCaCerts(caId), + queryFn: async () => { + const { data } = await apiRequest.get< + { + certificate: string; + certificateChain: string; + serialNumber: string; + version: number; + }[] + >(`/api/v1/pki/ca/${caId}/ca-certificates`); // TODO: consider updating endpoint structure + return data; + }, + enabled: Boolean(caId) + }); +}; + export const useGetCaCert = (caId: string) => { return useQuery({ queryKey: caKeys.getCaCert(caId), @@ -32,7 +51,7 @@ export const useGetCaCert = (caId: string) => { certificate: string; certificateChain: string; serialNumber: string; - }>(`/api/v1/pki/ca/${caId}/certificate`); + }>(`/api/v1/pki/ca/${caId}/certificate`); // TODO: consider updating endpoint structure return data; }, enabled: Boolean(caId) diff --git a/frontend/src/hooks/api/ca/types.ts b/frontend/src/hooks/api/ca/types.ts index 7cb5dbf42..bba7aa699 100644 --- a/frontend/src/hooks/api/ca/types.ts +++ b/frontend/src/hooks/api/ca/types.ts @@ -1,5 +1,5 @@ import { CertKeyAlgorithm } from "../certificates/enums"; -import { CaStatus, CaType } from "./enums"; +import { CaRenewalType,CaStatus, CaType } from "./enums"; export type TCertificateAuthority = { id: string; @@ -94,3 +94,16 @@ export type TCreateCertificateResponse = { privateKey: string; serialNumber: string; }; + +export type TRenewCaDTO = { + projectSlug: string; + caId: string; + type: CaRenewalType; + notAfter: string; +}; + +export type TRenewCaResponse = { + certificate: string; + certificateChain: string; + serialNumber: string; +}; diff --git a/frontend/src/pages/project/[id]/ca/[caId]/index.tsx b/frontend/src/pages/project/[id]/ca/[caId]/index.tsx new file mode 100644 index 000000000..b5f11f55a --- /dev/null +++ b/frontend/src/pages/project/[id]/ca/[caId]/index.tsx @@ -0,0 +1,18 @@ +/* eslint-disable @typescript-eslint/no-unused-vars */ +import Head from "next/head"; + +import { CaPage } from "@app/views/Project/CaPage"; + +export default function Ca() { + return ( + <> + + Certificate Authority + + + + + ); +} + +Ca.requireAuth = true; diff --git a/frontend/src/views/Project/CaPage/CaPage.tsx b/frontend/src/views/Project/CaPage/CaPage.tsx new file mode 100644 index 000000000..b7b015f21 --- /dev/null +++ b/frontend/src/views/Project/CaPage/CaPage.tsx @@ -0,0 +1,147 @@ +/* eslint-disable @typescript-eslint/no-unused-vars */ +import { useRouter } from "next/router"; +import { faChevronLeft, faEllipsis } from "@fortawesome/free-solid-svg-icons"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { twMerge } from "tailwind-merge"; + +import { createNotification } from "@app/components/notifications"; +import { ProjectPermissionCan } from "@app/components/permissions"; +import { + Button, + DeleteActionModal, + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuTrigger, + Tooltip +} from "@app/components/v2"; +import { ProjectPermissionActions, ProjectPermissionSub, useWorkspace } from "@app/context"; +import { withProjectPermission } from "@app/hoc"; +import { useDeleteCa,useGetCaById } from "@app/hooks/api"; +import { usePopUp } from "@app/hooks/usePopUp"; +import { CaModal } from "@app/views/Project/CertificatesPage/components/CaTab/components/CaModal"; + +import { CaInstallCertModal } from "../CertificatesPage/components/CaTab/components/CaInstallCertModal"; +import { TabSections } from "../Types"; +import { CaCertificatesSection, CaDetailsSection, CaRenewalModal } from "./components"; + +export const CaPage = withProjectPermission( + () => { + const router = useRouter(); + const caId = router.query.caId as string; + const { data } = useGetCaById(caId); + + const { currentWorkspace } = useWorkspace(); + const projectId = currentWorkspace?.id || ""; + + const { mutateAsync: deleteCa } = useDeleteCa(); + + const { popUp, handlePopUpOpen, handlePopUpClose, handlePopUpToggle } = usePopUp([ + "ca", + "deleteCa", + "installCaCert", + "renewCa" + ] as const); + + const onRemoveCaSubmit = async (caIdToDelete: string) => { + try { + if (!currentWorkspace?.slug) return; + + await deleteCa({ caId: caIdToDelete, projectSlug: currentWorkspace.slug }); + + await createNotification({ + text: "Successfully deleted CA", + type: "success" + }); + + handlePopUpClose("deleteCa"); + router.push(`/project/${projectId}/certificates`); + } catch (err) { + console.error(err); + createNotification({ + text: "Failed to delete CA", + type: "error" + }); + } + }; + + return ( +
+ {data && ( +
+ +
+

{data.friendlyName}

+ + +
+ + + +
+
+ + + {(isAllowed) => ( + + handlePopUpOpen("deleteCa", { + caId: data.id, + dn: data.dn + }) + } + disabled={!isAllowed} + > + Delete CA + + )} + + +
+
+
+
+ +
+ +
+
+ )} + + + + handlePopUpToggle("deleteCa", isOpen)} + deleteKey="confirm" + onDeleteApproved={() => + onRemoveCaSubmit((popUp?.deleteCa?.data as { caId: string })?.caId) + } + /> +
+ ); + }, + { action: ProjectPermissionActions.Read, subject: ProjectPermissionSub.CertificateAuthorities } +); diff --git a/frontend/src/views/Project/CaPage/components/CaCertificatesSection/CaCertificatesSection.tsx b/frontend/src/views/Project/CaPage/components/CaCertificatesSection/CaCertificatesSection.tsx new file mode 100644 index 000000000..9b617f5cf --- /dev/null +++ b/frontend/src/views/Project/CaPage/components/CaCertificatesSection/CaCertificatesSection.tsx @@ -0,0 +1,31 @@ +// import { faPlus } from "@fortawesome/free-solid-svg-icons"; +// import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +// import { IconButton } from "@app/components/v2"; +import { CaCertificatesTable } from "./CaCertificatesTable"; + +type Props = { + caId: string; +}; + +export const CaCertificatesSection = ({ caId }: Props) => { + return ( +
+
+

CA Certificates

+ {/* { + // handlePopUpOpen("addIdentityToProject"); + }} + > + + */} +
+
+ +
+
+ ); +}; diff --git a/frontend/src/views/Project/CaPage/components/CaCertificatesSection/CaCertificatesTable.tsx b/frontend/src/views/Project/CaPage/components/CaCertificatesSection/CaCertificatesTable.tsx new file mode 100644 index 000000000..d1451fbf5 --- /dev/null +++ b/frontend/src/views/Project/CaPage/components/CaCertificatesSection/CaCertificatesTable.tsx @@ -0,0 +1,131 @@ +import { faCertificate, faEllipsis } from "@fortawesome/free-solid-svg-icons"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { twMerge } from "tailwind-merge"; + +import { ProjectPermissionCan } from "@app/components/permissions"; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuTrigger, + EmptyState, + Table, + TableContainer, + TableSkeleton, + TBody, + Td, + Th, + THead, + Tr} from "@app/components/v2"; +import { ProjectPermissionActions, ProjectPermissionSub } from "@app/context"; +import { useGetCaCerts } from "@app/hooks/api"; + +type Props = { + caId: string; +}; + +// TODO: not before + +// created at +// expires on + +export const CaCertificatesTable = ({ caId }: Props) => { + const { data: caCerts, isLoading } = useGetCaCerts(caId); + console.log("CaCertificatesTable data: ", caCerts); + return ( + + + + + + + + + + + {isLoading && } + {!isLoading && + caCerts?.map((caCert) => { + // console.log("caCert index: ", index); + // const isLastItem = index === caCerts.length - 1; + return ( + + + + + + + ); + })} + +
NameCreated AtValid Until +
+
+ Certificate {caCert.version} + {/* + Current + */} +
+
TestTest + + +
+ +
+
+ + + {(isAllowed) => ( + { + e.stopPropagation(); + // TODO + // router.push(`/org/${orgId}/identities/${id}`); + }} + disabled={!isAllowed} + > + Download CA Certificate + + )} + + + {(isAllowed) => ( + { + e.stopPropagation(); + // TODO + // handlePopUpOpen("deleteIdentity", { + // identityId: id, + // name + // }); + }} + disabled={!isAllowed} + > + Download CA Certificate Chain + + )} + + +
+
+ {!isLoading && !caCerts?.length && ( + + )} +
+ ); +}; diff --git a/frontend/src/views/Project/CaPage/components/CaCertificatesSection/index.tsx b/frontend/src/views/Project/CaPage/components/CaCertificatesSection/index.tsx new file mode 100644 index 000000000..14f5dbbb9 --- /dev/null +++ b/frontend/src/views/Project/CaPage/components/CaCertificatesSection/index.tsx @@ -0,0 +1 @@ +export { CaCertificatesSection } from "./CaCertificatesSection"; diff --git a/frontend/src/views/Project/CaPage/components/CaDetailsSection.tsx b/frontend/src/views/Project/CaPage/components/CaDetailsSection.tsx new file mode 100644 index 000000000..bc939b5d3 --- /dev/null +++ b/frontend/src/views/Project/CaPage/components/CaDetailsSection.tsx @@ -0,0 +1,141 @@ +import { faCheck, faCopy } from "@fortawesome/free-solid-svg-icons"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { format } from "date-fns"; + +import { ProjectPermissionCan } from "@app/components/permissions"; +import { Button, IconButton, Tooltip } from "@app/components/v2"; +import { ProjectPermissionActions, ProjectPermissionSub } from "@app/context"; +import { useTimedReset } from "@app/hooks"; +import { CaStatus,useGetCaById } from "@app/hooks/api"; +import { caStatusToNameMap, caTypeToNameMap } from "@app/hooks/api/ca/constants"; +import { certKeyAlgorithmToNameMap } from "@app/hooks/api/certificates/constants"; +import { UsePopUpState } from "@app/hooks/usePopUp"; + +type Props = { + caId: string; + handlePopUpOpen: ( + popUpName: keyof UsePopUpState<["ca", "renewCa", "installCaCert"]>, + data?: {} + ) => void; +}; + +export const CaDetailsSection = ({ caId, handlePopUpOpen }: Props) => { + const [copyTextId, isCopyingId, setCopyTextId] = useTimedReset({ + initialState: "Copy ID to clipboard" + }); + + const { data: ca } = useGetCaById(caId); + + return ca ? ( +
+
+

CA Details

+
+
+
+

CA ID

+
+

{ca.id}

+
+ + { + navigator.clipboard.writeText(ca.id); + setCopyTextId("Copied"); + }} + > + + + +
+
+
+
+

Friendly Name

+

{ca.friendlyName}

+
+
+

CA Type

+

{caTypeToNameMap[ca.type]}

+
+
+

Status

+

{caStatusToNameMap[ca.status]}

+
+
+

Key Algorithm

+

{certKeyAlgorithmToNameMap[ca.keyAlgorithm]}

+
+
+

Max Path Length

+

{ca.maxPathLength ?? "-"}

+
+
+

Not Before

+

+ {ca.notBefore ? format(new Date(ca.notBefore), "yyyy-MM-dd") : "-"} +

+
+
+

Not After

+

+ {ca.notAfter ? format(new Date(ca.notAfter), "yyyy-MM-dd") : "-"} +

+
+ {ca.status === CaStatus.ACTIVE && ( + + {(isAllowed) => { + return ( + + ); + }} + + )} + {ca.status === CaStatus.PENDING_CERTIFICATE && ( + + {(isAllowed) => { + return ( + + ); + }} + + )} +
+
+ ) : ( +
+ ); +}; diff --git a/frontend/src/views/Project/CaPage/components/CaRenewalModal.tsx b/frontend/src/views/Project/CaPage/components/CaRenewalModal.tsx new file mode 100644 index 000000000..a4742aa2c --- /dev/null +++ b/frontend/src/views/Project/CaPage/components/CaRenewalModal.tsx @@ -0,0 +1,215 @@ +import { useEffect } from "react"; +import { Controller, useForm } from "react-hook-form"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { z } from "zod"; + +import { createNotification } from "@app/components/notifications"; +import { + Button, + FormControl, + Input, + Modal, + ModalContent, + Select, + SelectItem +} from "@app/components/v2"; +import { useWorkspace } from "@app/context"; +import { + CaRenewalType, + // CaType, + CaStatus, + useGetCaById, + useRenewCa} from "@app/hooks/api/ca"; +import { UsePopUpState } from "@app/hooks/usePopUp"; + +const caRenewalTypes = [{ label: "Renew with same key pair", value: CaRenewalType.EXISTING }]; + +const isValidDate = (dateString: string) => { + const date = new Date(dateString); + return !Number.isNaN(date.getTime()); +}; + +// const getMiddleDate = (date1: Date, date2: Date): Date => { +// const timestamp1 = date1.getTime(); +// const timestamp2 = date2.getTime(); +// const middleTimestamp = (timestamp1 + timestamp2) / 2; +// return new Date(middleTimestamp); +// }; + +const schema = z + .object({ + type: z.enum([CaRenewalType.EXISTING]), + notAfter: z.string().trim().refine(isValidDate, { message: "Invalid date format" }) + }) + .required(); + +export type FormData = z.infer; + +type Props = { + popUp: UsePopUpState<["renewCa"]>; + handlePopUpToggle: (popUpName: keyof UsePopUpState<["renewCa"]>, state?: boolean) => void; +}; + +export const CaRenewalModal = ({ popUp, handlePopUpToggle }: Props) => { + const { currentWorkspace } = useWorkspace(); + const projectSlug = currentWorkspace?.slug || ""; + + const popUpData = popUp?.renewCa?.data as { + caId: string; + }; + + const { data: ca } = useGetCaById(popUpData?.caId || ""); + const { data: parentCa } = useGetCaById(ca?.parentCaId || ""); + const { mutateAsync: renewCa } = useRenewCa(); + + const { + control, + handleSubmit, + reset, + formState: { isSubmitting }, + setValue + } = useForm({ + resolver: zodResolver(schema), + defaultValues: { + type: CaRenewalType.EXISTING, + notAfter: "" // TODO: set sensible default + } + }); + + useEffect(() => { + if (ca && ca.status === CaStatus.ACTIVE) { + // if (ca.type === CaType.ROOT) { + // // extend Root CA validity by the same amount of time + // const notBeforeDate = new Date(ca.notBefore as string); + // const notAfterDate = new Date(ca.notAfter as string); + + // const newNotAfterDate = new Date( + // notAfterDate.getTime() + notAfterDate.getTime() - notBeforeDate.getTime() + // ); + + // setValue("notAfter", newNotAfterDate.toISOString().split("T")[0]); + // } else if (ca.type === CaType.INTERMEDIATE && parentCa) { + // } + // extend Root CA validity by the same amount of time + const notBeforeDate = new Date(ca.notBefore as string); + const notAfterDate = new Date(ca.notAfter as string); + + const newNotAfterDate = new Date( + notAfterDate.getTime() + notAfterDate.getTime() - notBeforeDate.getTime() + ); + + setValue("notAfter", newNotAfterDate.toISOString().split("T")[0]); + } + + // if (ca && parentCa) { + // // intermediate CA + // } else if (ca && ca.notAfter && !parentCa) { + // // root CA + // const timeDifference = new Date(ca.).getTime() - startDate.getTime(); + // } + }, [ca, parentCa]); + + const onFormSubmit = async ({ type, notAfter }: FormData) => { + try { + if (!projectSlug || !popUpData.caId) return; + + await renewCa({ + projectSlug, + caId: popUpData.caId, + notAfter, + type + }); + + handlePopUpToggle("renewCa", false); + + createNotification({ + text: "Successfully renewed CA", + type: "success" + }); + + reset(); + } catch (err) { + console.error(err); + const error = err as any; + const text = error?.response?.data?.message ?? "Failed to renew CA"; + + createNotification({ + text, + type: "error" + }); + } + }; + + return ( + { + handlePopUpToggle("renewCa", isOpen); + reset(); + }} + > + +
+ ( + + + + )} + /> + ( + + + + )} + /> +
+ + +
+ +
+
+ ); +}; diff --git a/frontend/src/views/Project/CaPage/components/index.tsx b/frontend/src/views/Project/CaPage/components/index.tsx new file mode 100644 index 000000000..78889e9b4 --- /dev/null +++ b/frontend/src/views/Project/CaPage/components/index.tsx @@ -0,0 +1,3 @@ +export { CaCertificatesSection } from "./CaCertificatesSection/CaCertificatesSection"; +export { CaDetailsSection } from "./CaDetailsSection"; +export { CaRenewalModal } from "./CaRenewalModal"; diff --git a/frontend/src/views/Project/CaPage/index.tsx b/frontend/src/views/Project/CaPage/index.tsx new file mode 100644 index 000000000..18703bd1b --- /dev/null +++ b/frontend/src/views/Project/CaPage/index.tsx @@ -0,0 +1 @@ +export { CaPage } from "./CaPage"; diff --git a/frontend/src/views/Project/CertificatesPage/components/CaTab/components/CaTable.tsx b/frontend/src/views/Project/CertificatesPage/components/CaTab/components/CaTable.tsx index a35a4e432..f719365eb 100644 --- a/frontend/src/views/Project/CertificatesPage/components/CaTab/components/CaTable.tsx +++ b/frontend/src/views/Project/CertificatesPage/components/CaTab/components/CaTable.tsx @@ -1,3 +1,4 @@ +import { useRouter } from "next/router"; import { faBan, faCertificate, @@ -12,6 +13,7 @@ import { twMerge } from "tailwind-merge"; import { ProjectPermissionCan } from "@app/components/permissions"; import { + Badge, DropdownMenu, DropdownMenuContent, DropdownMenuItem, @@ -31,9 +33,14 @@ import { ProjectPermissionActions, ProjectPermissionSub, useSubscription, - useWorkspace} from "@app/context"; + useWorkspace +} from "@app/context"; import { CaStatus, useListWorkspaceCas } from "@app/hooks/api"; -import { caStatusToNameMap, caTypeToNameMap } from "@app/hooks/api/ca/constants"; +import { + caStatusToNameMap, + caTypeToNameMap, + getStatusBadgeVariant +} from "@app/hooks/api/ca/constants"; import { UsePopUpState } from "@app/hooks/usePopUp"; type Props = { @@ -51,11 +58,13 @@ type Props = { }; export const CaTable = ({ handlePopUpOpen }: Props) => { + const router = useRouter(); const { subscription } = useSubscription(); const { currentWorkspace } = useWorkspace(); const { data, isLoading } = useListWorkspaceCas({ projectSlug: currentWorkspace?.slug ?? "" }); + return (
@@ -76,11 +85,26 @@ export const CaTable = ({ handlePopUpOpen }: Props) => { data.length > 0 && data.map((ca) => { return ( - + router.push(`/project/${currentWorkspace?.id}/ca/${ca.id}`)} + > {ca.friendlyName} - {caStatusToNameMap[ca.status]} + + + {caStatusToNameMap[ca.status]} + + {caTypeToNameMap[ca.type]} - {ca.notAfter ? format(new Date(ca.notAfter), "yyyy-MM-dd") : "-"} + +
+

{ca.notAfter ? format(new Date(ca.notAfter), "yyyy-MM-dd") : "-"}

+ {/* + Expires Soon + */} +
+ @@ -102,7 +126,8 @@ export const CaTable = ({ handlePopUpOpen }: Props) => { !isAllowed && "pointer-events-none cursor-not-allowed opacity-50" )} - onClick={async () => { + onClick={(e) => { + e.stopPropagation(); handlePopUpOpen("installCaCert", { caId: ca.id }); @@ -110,7 +135,7 @@ export const CaTable = ({ handlePopUpOpen }: Props) => { disabled={!isAllowed} icon={} > - Install Certificate + Install CA Certificate )} @@ -126,7 +151,8 @@ export const CaTable = ({ handlePopUpOpen }: Props) => { !isAllowed && "pointer-events-none cursor-not-allowed opacity-50" )} - onClick={async () => { + onClick={(e) => { + e.stopPropagation(); handlePopUpOpen("caCert", { caId: ca.id }); @@ -150,7 +176,8 @@ export const CaTable = ({ handlePopUpOpen }: Props) => { !isAllowed && "pointer-events-none cursor-not-allowed opacity-50" )} - onClick={async () => { + onClick={(e) => { + e.stopPropagation(); if (!subscription?.caCrl) { handlePopUpOpen("upgradePlan", { description: @@ -179,11 +206,12 @@ export const CaTable = ({ handlePopUpOpen }: Props) => { className={twMerge( !isAllowed && "pointer-events-none cursor-not-allowed opacity-50" )} - onClick={async () => + onClick={(e) => { + e.stopPropagation(); handlePopUpOpen("ca", { caId: ca.id - }) - } + }); + }} disabled={!isAllowed} icon={} > @@ -202,15 +230,16 @@ export const CaTable = ({ handlePopUpOpen }: Props) => { !isAllowed && "pointer-events-none cursor-not-allowed opacity-50" )} - onClick={async () => + onClick={(e) => { + e.stopPropagation(); handlePopUpOpen("caStatus", { caId: ca.id, status: ca.status === CaStatus.ACTIVE ? CaStatus.DISABLED : CaStatus.ACTIVE - }) - } + }); + }} disabled={!isAllowed} icon={} > @@ -228,12 +257,13 @@ export const CaTable = ({ handlePopUpOpen }: Props) => { className={twMerge( !isAllowed && "pointer-events-none cursor-not-allowed opacity-50" )} - onClick={async () => + onClick={(e) => { + e.stopPropagation(); handlePopUpOpen("deleteCa", { caId: ca.id, dn: ca.dn - }) - } + }); + }} disabled={!isAllowed} icon={} > From c70d0a577c3ae7ee8ae68ca6c5738e217176674a Mon Sep 17 00:00:00 2001 From: Tuan Dang Date: Mon, 5 Aug 2024 15:43:40 -0700 Subject: [PATCH 2/7] Finish preliminary CA renewal with same key pair --- .../20240802181855_ca-cert-version.ts | 40 +--- .../ee/services/audit-log/audit-log-types.ts | 10 + .../routes/v1/certificate-authority-router.ts | 42 ++-- .../certificate-authority-fns.ts | 2 + .../certificate-authority-service.ts | 46 +++-- frontend/package-lock.json | 194 ++++++++++++++++++ frontend/package.json | 1 + frontend/src/views/Project/CaPage/CaPage.tsx | 6 +- .../CaCertificatesTable.tsx | 54 ++--- .../CaPage/components/CaDetailsSection.tsx | 28 ++- .../CaPage/components/CaRenewalModal.tsx | 73 ++----- 11 files changed, 345 insertions(+), 151 deletions(-) diff --git a/backend/src/db/migrations/20240802181855_ca-cert-version.ts b/backend/src/db/migrations/20240802181855_ca-cert-version.ts index b1fffe4a3..7c7d956fc 100644 --- a/backend/src/db/migrations/20240802181855_ca-cert-version.ts +++ b/backend/src/db/migrations/20240802181855_ca-cert-version.ts @@ -22,7 +22,7 @@ export async function up(knex: Knex): Promise { if (!hasVersionColumn) { await knex.schema.alterTable(TableName.CertificateAuthorityCert, (t) => { t.integer("version").nullable(); - // t.dropUnique(["caId"]); + t.dropUnique(["caId"]); }); await knex(TableName.CertificateAuthorityCert).update({ version: 1 }).whereNull("version"); @@ -48,45 +48,17 @@ export async function up(knex: Knex): Promise { ) `); - // await knex.raw(` - // UPDATE ${TableName.CertificateAuthorityCert} cert - // SET caSecretId = ( - // SELECT sec.id - // FROM ${TableName.CertificateAuthoritySecret} sec - // WHERE sec."caId" = cert."caId" - // ) - // `); - - // await knex(TableName.CertificateAuthorityCert).update({ - // caSecretId: knex(TableName.CertificateAuthoritySecret) - // .select("id") - // .whereRaw("?? = ??", ["CertificateAuthoritySecret.caId", "CertificateAuthorityCert.caId"]) - // }); - - // await knex(TableName.CertificateAuthorityCert).update({ - // caSecretId: function () { - // this.select("id") - // .from(TableName.CertificateAuthoritySecret) - // .whereRaw("??.?? = ??.??", [ - // TableName.CertificateAuthoritySecret, - // "caId", - // TableName.CertificateAuthorityCert, - // "caId" - // ]); - // } - // }); - await knex.schema.alterTable(TableName.CertificateAuthorityCert, (t) => { t.uuid("caSecretId").notNullable().alter(); }); } } - // if (await knex.schema.hasTable(TableName.CertificateAuthoritySecret)) { - // await knex.schema.alterTable(TableName.CertificateAuthoritySecret, (t) => { - // t.dropUnique(["caId"]); - // }); - // } + if (await knex.schema.hasTable(TableName.CertificateAuthoritySecret)) { + await knex.schema.alterTable(TableName.CertificateAuthoritySecret, (t) => { + t.dropUnique(["caId"]); + }); + } } export async function down(knex: Knex): Promise { 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 61c2ecfa7..015410daf 100644 --- a/backend/src/ee/services/audit-log/audit-log-types.ts +++ b/backend/src/ee/services/audit-log/audit-log-types.ts @@ -130,6 +130,7 @@ export enum EventType { GET_CA = "get-certificate-authority", UPDATE_CA = "update-certificate-authority", DELETE_CA = "delete-certificate-authority", + RENEW_CA = "renew-certificate-authority", GET_CA_CSR = "get-certificate-authority-csr", GET_CA_CERTS = "get-certificate-authority-certs", GET_CA_CERT = "get-certificate-authority-cert", @@ -1094,6 +1095,14 @@ interface DeleteCa { }; } +interface RenewCa { + type: EventType.RENEW_CA; + metadata: { + caId: string; + dn: string; + }; +} + interface GetCaCsr { type: EventType.GET_CA_CSR; metadata: { @@ -1336,6 +1345,7 @@ export type Event = | GetCa | UpdateCa | DeleteCa + | RenewCa | GetCaCsr | GetCaCerts | GetCaCert diff --git a/backend/src/server/routes/v1/certificate-authority-router.ts b/backend/src/server/routes/v1/certificate-authority-router.ts index ad49bacaf..e39c5702e 100644 --- a/backend/src/server/routes/v1/certificate-authority-router.ts +++ b/backend/src/server/routes/v1/certificate-authority-router.ts @@ -277,7 +277,7 @@ export const registerCaRouter = async (server: FastifyZodProvider) => { server.route({ method: "POST", - url: "/:caId/renew", // TODO + url: "/:caId/renew", config: { rateLimit: writeLimit }, @@ -300,27 +300,27 @@ export const registerCaRouter = async (server: FastifyZodProvider) => { } }, handler: async (req) => { - const { certificate, certificateChain, serialNumber } = await server.services.certificateAuthority.renewCaCert({ - caId: req.params.caId, - actor: req.permission.type, - actorId: req.permission.id, - actorAuthMethod: req.permission.authMethod, - actorOrgId: req.permission.orgId, - ...req.body - }); + const { certificate, certificateChain, serialNumber, ca } = + await server.services.certificateAuthority.renewCaCert({ + caId: req.params.caId, + actor: req.permission.type, + actorId: req.permission.id, + actorAuthMethod: req.permission.authMethod, + actorOrgId: req.permission.orgId, + ...req.body + }); - // await server.services.auditLog.createAuditLog({ - // ...req.auditLogInfo, - // projectId: ca.projectId, - // event: { - // type: EventType.SIGN_INTERMEDIATE, - // metadata: { - // caId: ca.id, - // dn: ca.dn, - // serialNumber - // } - // } - // }); + await server.services.auditLog.createAuditLog({ + ...req.auditLogInfo, + projectId: ca.projectId, + event: { + type: EventType.RENEW_CA, + metadata: { + caId: ca.id, + dn: ca.dn + } + } + }); return { certificate, diff --git a/backend/src/services/certificate-authority/certificate-authority-fns.ts b/backend/src/services/certificate-authority/certificate-authority-fns.ts index b58046555..9174ecf02 100644 --- a/backend/src/services/certificate-authority/certificate-authority-fns.ts +++ b/backend/src/services/certificate-authority/certificate-authority-fns.ts @@ -61,6 +61,8 @@ export const keyAlgorithmToAlgCfg = (keyAlgorithm: CertKeyAlgorithm) => { * Return the public and private key of CA with id [caId] * Note: credentials are returned as crypto.webcrypto.CryptoKey * suitable for use with @peculiar/x509 module + * + * TODO: Update to get latest CA Secret once support for CA renewal with new key pair is added */ export const getCaCredentials = async ({ caId, diff --git a/backend/src/services/certificate-authority/certificate-authority-service.ts b/backend/src/services/certificate-authority/certificate-authority-service.ts index 2bee10263..21cd5a930 100644 --- a/backend/src/services/certificate-authority/certificate-authority-service.ts +++ b/backend/src/services/certificate-authority/certificate-authority-service.ts @@ -387,6 +387,8 @@ export const certificateAuthorityServiceFactory = ({ /** * Renew certificate for CA with id [caId] * Note: Currently implements CA renewal with same key-pair only + * + * TODO: check rebuilt chain? */ const renewCaCert = async ({ caId, notAfter, actorId, actorAuthMethod, actor, actorOrgId }: TRenewCaCertDTO) => { const ca = await certificateAuthorityDAL.findById(caId); @@ -607,8 +609,18 @@ export const certificateAuthorityServiceFactory = ({ plainText: Buffer.from(new Uint8Array(intermediateCert.rawData)) }); + const { caCert: parentCaCertificate, caCertChain: parentCaCertChain } = await getCaCertChain({ + caId: parentCa.id, + certificateAuthorityDAL, + certificateAuthorityCertDAL, + projectDAL, + kmsService + }); + + certificateChain = `${parentCaCertificate}\n${parentCaCertChain}`.trim(); + const { cipherTextBlob: encryptedCertificateChain } = await kmsEncryptor({ - plainText: Buffer.alloc(0) + plainText: Buffer.from(certificateChain) }); await certificateAuthorityDAL.transaction(async (tx) => { @@ -635,16 +647,7 @@ export const certificateAuthorityServiceFactory = ({ ); }); - const { caCert: issuingCaCertificate, caCertChain } = await getCaCertChain({ - caId, - certificateAuthorityDAL, - certificateAuthorityCertDAL, - projectDAL, - kmsService - }); - certificate = intermediateCert.toString("pem"); - certificateChain = `${issuingCaCertificate}\n${caCertChain}`.trim(); break; } default: { @@ -657,7 +660,8 @@ export const certificateAuthorityServiceFactory = ({ return { certificate, certificateChain, - serialNumber + serialNumber, + ca }; }; @@ -938,13 +942,31 @@ export const certificateAuthorityServiceFactory = ({ plainText: Buffer.from(certificateChain) }); + // TODO: validate that >latest< public-private key of CA is used to sign the certificate + const { caSecret, caPublicKey } = await getCaCredentials({ + caId: ca.id, + certificateAuthorityDAL, + certificateAuthoritySecretDAL, + projectDAL, + kmsService + }); + + const isCaAndCertPublicKeySame = Buffer.from(await crypto.subtle.exportKey("spki", caPublicKey)).equals( + Buffer.from(certObj.publicKey.rawData) + ); + + if (!isCaAndCertPublicKeySame) { + throw new BadRequestError({ message: "CA and certificate public key do not match" }); + } + await certificateAuthorityCertDAL.transaction(async (tx) => { await certificateAuthorityCertDAL.create( { caId: ca.id, encryptedCertificate, encryptedCertificateChain, - version: 1 + version: 1, + caSecretId: caSecret.id }, tx ); diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 6007a282d..74f00b74d 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -22,6 +22,7 @@ "@headlessui/react": "^1.7.7", "@hookform/resolvers": "^2.9.10", "@octokit/rest": "^19.0.7", + "@peculiar/x509": "^1.11.0", "@radix-ui/react-accordion": "^1.1.2", "@radix-ui/react-alert-dialog": "^1.0.5", "@radix-ui/react-checkbox": "^1.0.4", @@ -4520,6 +4521,149 @@ "@octokit/openapi-types": "^18.0.0" } }, + "node_modules/@peculiar/asn1-cms": { + "version": "2.3.13", + "resolved": "https://registry.npmjs.org/@peculiar/asn1-cms/-/asn1-cms-2.3.13.tgz", + "integrity": "sha512-joqu8A7KR2G85oLPq+vB+NFr2ro7Ls4ol13Zcse/giPSzUNN0n2k3v8kMpf6QdGUhI13e5SzQYN8AKP8sJ8v4w==", + "dependencies": { + "@peculiar/asn1-schema": "^2.3.13", + "@peculiar/asn1-x509": "^2.3.13", + "@peculiar/asn1-x509-attr": "^2.3.13", + "asn1js": "^3.0.5", + "tslib": "^2.6.2" + } + }, + "node_modules/@peculiar/asn1-csr": { + "version": "2.3.13", + "resolved": "https://registry.npmjs.org/@peculiar/asn1-csr/-/asn1-csr-2.3.13.tgz", + "integrity": "sha512-+JtFsOUWCw4zDpxp1LbeTYBnZLlGVOWmHHEhoFdjM5yn4wCn+JiYQ8mghOi36M2f6TPQ17PmhNL6/JfNh7/jCA==", + "dependencies": { + "@peculiar/asn1-schema": "^2.3.13", + "@peculiar/asn1-x509": "^2.3.13", + "asn1js": "^3.0.5", + "tslib": "^2.6.2" + } + }, + "node_modules/@peculiar/asn1-ecc": { + "version": "2.3.13", + "resolved": "https://registry.npmjs.org/@peculiar/asn1-ecc/-/asn1-ecc-2.3.13.tgz", + "integrity": "sha512-3dF2pQcrN/WJEMq+9qWLQ0gqtn1G81J4rYqFl6El6QV367b4IuhcRv+yMA84tNNyHOJn9anLXV5radnpPiG3iA==", + "dependencies": { + "@peculiar/asn1-schema": "^2.3.13", + "@peculiar/asn1-x509": "^2.3.13", + "asn1js": "^3.0.5", + "tslib": "^2.6.2" + } + }, + "node_modules/@peculiar/asn1-pfx": { + "version": "2.3.13", + "resolved": "https://registry.npmjs.org/@peculiar/asn1-pfx/-/asn1-pfx-2.3.13.tgz", + "integrity": "sha512-fypYxjn16BW+5XbFoY11Rm8LhZf6euqX/C7BTYpqVvLem1GvRl7A+Ro1bO/UPwJL0z+1mbvXEnkG0YOwbwz2LA==", + "dependencies": { + "@peculiar/asn1-cms": "^2.3.13", + "@peculiar/asn1-pkcs8": "^2.3.13", + "@peculiar/asn1-rsa": "^2.3.13", + "@peculiar/asn1-schema": "^2.3.13", + "asn1js": "^3.0.5", + "tslib": "^2.6.2" + } + }, + "node_modules/@peculiar/asn1-pkcs8": { + "version": "2.3.13", + "resolved": "https://registry.npmjs.org/@peculiar/asn1-pkcs8/-/asn1-pkcs8-2.3.13.tgz", + "integrity": "sha512-VP3PQzbeSSjPjKET5K37pxyf2qCdM0dz3DJ56ZCsol3FqAXGekb4sDcpoL9uTLGxAh975WcdvUms9UcdZTuGyQ==", + "dependencies": { + "@peculiar/asn1-schema": "^2.3.13", + "@peculiar/asn1-x509": "^2.3.13", + "asn1js": "^3.0.5", + "tslib": "^2.6.2" + } + }, + "node_modules/@peculiar/asn1-pkcs9": { + "version": "2.3.13", + "resolved": "https://registry.npmjs.org/@peculiar/asn1-pkcs9/-/asn1-pkcs9-2.3.13.tgz", + "integrity": "sha512-rIwQXmHpTo/dgPiWqUgby8Fnq6p1xTJbRMxCiMCk833kQCeZrC5lbSKg6NDnJTnX2kC6IbXBB9yCS2C73U2gJg==", + "dependencies": { + "@peculiar/asn1-cms": "^2.3.13", + "@peculiar/asn1-pfx": "^2.3.13", + "@peculiar/asn1-pkcs8": "^2.3.13", + "@peculiar/asn1-schema": "^2.3.13", + "@peculiar/asn1-x509": "^2.3.13", + "@peculiar/asn1-x509-attr": "^2.3.13", + "asn1js": "^3.0.5", + "tslib": "^2.6.2" + } + }, + "node_modules/@peculiar/asn1-rsa": { + "version": "2.3.13", + "resolved": "https://registry.npmjs.org/@peculiar/asn1-rsa/-/asn1-rsa-2.3.13.tgz", + "integrity": "sha512-wBNQqCyRtmqvXkGkL4DR3WxZhHy8fDiYtOjTeCd7SFE5F6GBeafw3EJ94PX/V0OJJrjQ40SkRY2IZu3ZSyBqcg==", + "dependencies": { + "@peculiar/asn1-schema": "^2.3.13", + "@peculiar/asn1-x509": "^2.3.13", + "asn1js": "^3.0.5", + "tslib": "^2.6.2" + } + }, + "node_modules/@peculiar/asn1-schema": { + "version": "2.3.13", + "resolved": "https://registry.npmjs.org/@peculiar/asn1-schema/-/asn1-schema-2.3.13.tgz", + "integrity": "sha512-3Xq3a01WkHRZL8X04Zsfg//mGaA21xlL4tlVn4v2xGT0JStiztATRkMwa5b+f/HXmY2smsiLXYK46Gwgzvfg3g==", + "dependencies": { + "asn1js": "^3.0.5", + "pvtsutils": "^1.3.5", + "tslib": "^2.6.2" + } + }, + "node_modules/@peculiar/asn1-x509": { + "version": "2.3.13", + "resolved": "https://registry.npmjs.org/@peculiar/asn1-x509/-/asn1-x509-2.3.13.tgz", + "integrity": "sha512-PfeLQl2skXmxX2/AFFCVaWU8U6FKW1Db43mgBhShCOFS1bVxqtvusq1hVjfuEcuSQGedrLdCSvTgabluwN/M9A==", + "dependencies": { + "@peculiar/asn1-schema": "^2.3.13", + "asn1js": "^3.0.5", + "ipaddr.js": "^2.1.0", + "pvtsutils": "^1.3.5", + "tslib": "^2.6.2" + } + }, + "node_modules/@peculiar/asn1-x509-attr": { + "version": "2.3.13", + "resolved": "https://registry.npmjs.org/@peculiar/asn1-x509-attr/-/asn1-x509-attr-2.3.13.tgz", + "integrity": "sha512-WpEos6CcnUzJ6o2Qb68Z7Dz5rSjRGv/DtXITCNBtjZIRWRV12yFVci76SVfOX8sisL61QWMhpLKQibrG8pi2Pw==", + "dependencies": { + "@peculiar/asn1-schema": "^2.3.13", + "@peculiar/asn1-x509": "^2.3.13", + "asn1js": "^3.0.5", + "tslib": "^2.6.2" + } + }, + "node_modules/@peculiar/asn1-x509/node_modules/ipaddr.js": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-2.2.0.tgz", + "integrity": "sha512-Ag3wB2o37wslZS19hZqorUnrnzSkpOVy+IiiDEiTqNubEYpYuHWIf6K4psgN2ZWKExS4xhVCrRVfb/wfW8fWJA==", + "engines": { + "node": ">= 10" + } + }, + "node_modules/@peculiar/x509": { + "version": "1.11.0", + "resolved": "https://registry.npmjs.org/@peculiar/x509/-/x509-1.11.0.tgz", + "integrity": "sha512-8rdxE//tsWLb2Yo2TYO2P8gieStbrHK/huFMV5PPfwX8I5HmtOus+Ox6nTKrPA9o+WOPaa5xKenee+QdmHBd5g==", + "dependencies": { + "@peculiar/asn1-cms": "^2.3.8", + "@peculiar/asn1-csr": "^2.3.8", + "@peculiar/asn1-ecc": "^2.3.8", + "@peculiar/asn1-pkcs9": "^2.3.8", + "@peculiar/asn1-rsa": "^2.3.8", + "@peculiar/asn1-schema": "^2.3.8", + "@peculiar/asn1-x509": "^2.3.8", + "pvtsutils": "^1.3.5", + "reflect-metadata": "^0.2.2", + "tslib": "^2.6.2", + "tsyringe": "^4.8.0" + } + }, "node_modules/@pkgjs/parseargs": { "version": "0.11.0", "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", @@ -9870,6 +10014,19 @@ "integrity": "sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA==", "dev": true }, + "node_modules/asn1js": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/asn1js/-/asn1js-3.0.5.tgz", + "integrity": "sha512-FVnvrKJwpt9LP2lAMl8qZswRNm3T4q9CON+bxldk2iwk3FFpuwhx2FfinyitizWHsVYyaY+y5JzDR0rCMV5yTQ==", + "dependencies": { + "pvtsutils": "^1.3.2", + "pvutils": "^1.1.3", + "tslib": "^2.4.0" + }, + "engines": { + "node": ">=12.0.0" + } + }, "node_modules/assert": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/assert/-/assert-2.1.0.tgz", @@ -20320,6 +20477,22 @@ "async-limiter": "~1.0.0" } }, + "node_modules/pvtsutils": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/pvtsutils/-/pvtsutils-1.3.5.tgz", + "integrity": "sha512-ARvb14YB9Nm2Xi6nBq1ZX6dAM0FsJnuk+31aUp4TrcZEdKUlSqOqsxJHUPJDNE3qiIp+iUPEIeR6Je/tgV7zsA==", + "dependencies": { + "tslib": "^2.6.1" + } + }, + "node_modules/pvutils": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/pvutils/-/pvutils-1.1.3.tgz", + "integrity": "sha512-pMpnA0qRdFp32b1sJl1wOJNxZLQ2cbQx+k6tjNtZ8CpvVhNqEPRgivZ2WOUev2YMajecdH7ctUPDvEe87nariQ==", + "engines": { + "node": ">=6.0.0" + } + }, "node_modules/qs": { "version": "6.11.2", "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.2.tgz", @@ -21186,6 +21359,11 @@ "redux": "^4" } }, + "node_modules/reflect-metadata": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.2.2.tgz", + "integrity": "sha512-urBwgfrvVP/eAyXx4hluJivBKzuEbSQs9rKWCrCkbSxNv8mxPcUZKeuoF3Uy4mJl3Lwprp6yy5/39VWigZ4K6Q==" + }, "node_modules/reflect.getprototypeof": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.4.tgz", @@ -23573,6 +23751,22 @@ "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", "dev": true }, + "node_modules/tsyringe": { + "version": "4.8.0", + "resolved": "https://registry.npmjs.org/tsyringe/-/tsyringe-4.8.0.tgz", + "integrity": "sha512-YB1FG+axdxADa3ncEtRnQCFq/M0lALGLxSZeVNbTU8NqhOVc51nnv2CISTcvc1kyv6EGPtXVr0v6lWeDxiijOA==", + "dependencies": { + "tslib": "^1.9.3" + }, + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/tsyringe/node_modules/tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==" + }, "node_modules/tty-browserify": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/tty-browserify/-/tty-browserify-0.0.1.tgz", diff --git a/frontend/package.json b/frontend/package.json index acbf220e1..994485252 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -30,6 +30,7 @@ "@headlessui/react": "^1.7.7", "@hookform/resolvers": "^2.9.10", "@octokit/rest": "^19.0.7", + "@peculiar/x509": "^1.11.0", "@radix-ui/react-accordion": "^1.1.2", "@radix-ui/react-alert-dialog": "^1.0.5", "@radix-ui/react-checkbox": "^1.0.4", diff --git a/frontend/src/views/Project/CaPage/CaPage.tsx b/frontend/src/views/Project/CaPage/CaPage.tsx index b7b015f21..7edc99af9 100644 --- a/frontend/src/views/Project/CaPage/CaPage.tsx +++ b/frontend/src/views/Project/CaPage/CaPage.tsx @@ -17,7 +17,7 @@ import { } from "@app/components/v2"; import { ProjectPermissionActions, ProjectPermissionSub, useWorkspace } from "@app/context"; import { withProjectPermission } from "@app/hoc"; -import { useDeleteCa,useGetCaById } from "@app/hooks/api"; +import { useDeleteCa, useGetCaById } from "@app/hooks/api"; import { usePopUp } from "@app/hooks/usePopUp"; import { CaModal } from "@app/views/Project/CertificatesPage/components/CaTab/components/CaModal"; @@ -73,9 +73,7 @@ export const CaPage = withProjectPermission( variant="link" type="submit" leftIcon={} - onClick={() => - router.push(`/project/${projectId}/members?selectedTab=${TabSections.Roles}`) - } + onClick={() => router.push(`/project/${projectId}/certificates`)} className="mb-4" > Certificate Authorities diff --git a/frontend/src/views/Project/CaPage/components/CaCertificatesSection/CaCertificatesTable.tsx b/frontend/src/views/Project/CaPage/components/CaCertificatesSection/CaCertificatesTable.tsx index d1451fbf5..9eb008706 100644 --- a/frontend/src/views/Project/CaPage/components/CaCertificatesSection/CaCertificatesTable.tsx +++ b/frontend/src/views/Project/CaPage/components/CaCertificatesSection/CaCertificatesTable.tsx @@ -1,9 +1,13 @@ import { faCertificate, faEllipsis } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import * as x509 from "@peculiar/x509"; +import { format } from "date-fns"; +import FileSaver from "file-saver"; import { twMerge } from "tailwind-merge"; import { ProjectPermissionCan } from "@app/components/permissions"; import { + Badge, DropdownMenu, DropdownMenuContent, DropdownMenuItem, @@ -16,7 +20,8 @@ import { Td, Th, THead, - Tr} from "@app/components/v2"; + Tr +} from "@app/components/v2"; import { ProjectPermissionActions, ProjectPermissionSub } from "@app/context"; import { useGetCaCerts } from "@app/hooks/api"; @@ -24,43 +29,45 @@ type Props = { caId: string; }; -// TODO: not before - -// created at -// expires on - export const CaCertificatesTable = ({ caId }: Props) => { const { data: caCerts, isLoading } = useGetCaCerts(caId); - console.log("CaCertificatesTable data: ", caCerts); + + const downloadTxtFile = (filename: string, content: string) => { + const blob = new Blob([content], { type: "text/plain;charset=utf-8" }); + FileSaver.saveAs(blob, filename); + }; + return ( - - - + + + {isLoading && } {!isLoading && - caCerts?.map((caCert) => { - // console.log("caCert index: ", index); - // const isLastItem = index === caCerts.length - 1; + caCerts?.map((caCert, index) => { + const isLastItem = index === caCerts.length - 1; + const caCertObj = new x509.X509Certificate(caCert.certificate); return ( - - + +
NameCreated AtValid UntilCA Certificate #Not BeforeNot After
- Certificate {caCert.version} - {/* - Current - */} + CA Certificate {caCert.version} + {isLastItem && ( + + Current + + )}
TestTest{format(new Date(caCertObj.notBefore), "yyyy-MM-dd")}{format(new Date(caCertObj.notAfter), "yyyy-MM-dd")} @@ -80,8 +87,7 @@ export const CaCertificatesTable = ({ caId }: Props) => { )} onClick={(e) => { e.stopPropagation(); - // TODO - // router.push(`/org/${orgId}/identities/${id}`); + downloadTxtFile("cert.pem", caCert.certificate); }} disabled={!isAllowed} > @@ -100,11 +106,7 @@ export const CaCertificatesTable = ({ caId }: Props) => { )} onClick={(e) => { e.stopPropagation(); - // TODO - // handlePopUpOpen("deleteIdentity", { - // identityId: id, - // name - // }); + downloadTxtFile("chain.pem", caCert.certificateChain); }} disabled={!isAllowed} > diff --git a/frontend/src/views/Project/CaPage/components/CaDetailsSection.tsx b/frontend/src/views/Project/CaPage/components/CaDetailsSection.tsx index bc939b5d3..eb8ccf86f 100644 --- a/frontend/src/views/Project/CaPage/components/CaDetailsSection.tsx +++ b/frontend/src/views/Project/CaPage/components/CaDetailsSection.tsx @@ -6,7 +6,7 @@ import { ProjectPermissionCan } from "@app/components/permissions"; import { Button, IconButton, Tooltip } from "@app/components/v2"; import { ProjectPermissionActions, ProjectPermissionSub } from "@app/context"; import { useTimedReset } from "@app/hooks"; -import { CaStatus,useGetCaById } from "@app/hooks/api"; +import { CaStatus, useGetCaById } from "@app/hooks/api"; import { caStatusToNameMap, caTypeToNameMap } from "@app/hooks/api/ca/constants"; import { certKeyAlgorithmToNameMap } from "@app/hooks/api/certificates/constants"; import { UsePopUpState } from "@app/hooks/usePopUp"; @@ -23,6 +23,9 @@ export const CaDetailsSection = ({ caId, handlePopUpOpen }: Props) => { const [copyTextId, isCopyingId, setCopyTextId] = useTimedReset({ initialState: "Copy ID to clipboard" }); + const [copyTextParentId, isCopyingParentId, setCopyTextParentId] = useTimedReset({ + initialState: "Copy ID to clipboard" + }); const { data: ca } = useGetCaById(caId); @@ -53,6 +56,29 @@ export const CaDetailsSection = ({ caId, handlePopUpOpen }: Props) => { + {ca.parentCaId && ( +
+

Parent CA ID

+
+

{ca.parentCaId}

+
+ + { + navigator.clipboard.writeText(ca.parentCaId as string); + setCopyTextParentId("Copied"); + }} + > + + + +
+
+
+ )}

Friendly Name

{ca.friendlyName}

diff --git a/frontend/src/views/Project/CaPage/components/CaRenewalModal.tsx b/frontend/src/views/Project/CaPage/components/CaRenewalModal.tsx index a4742aa2c..d66e0b71b 100644 --- a/frontend/src/views/Project/CaPage/components/CaRenewalModal.tsx +++ b/frontend/src/views/Project/CaPage/components/CaRenewalModal.tsx @@ -1,4 +1,4 @@ -import { useEffect } from "react"; +// import { useEffect } from "react"; import { Controller, useForm } from "react-hook-form"; import { zodResolver } from "@hookform/resolvers/zod"; import { z } from "zod"; @@ -16,10 +16,11 @@ import { import { useWorkspace } from "@app/context"; import { CaRenewalType, + useRenewCa + // useGetCaById, // CaType, - CaStatus, - useGetCaById, - useRenewCa} from "@app/hooks/api/ca"; + // CaStatus +} from "@app/hooks/api/ca"; import { UsePopUpState } from "@app/hooks/usePopUp"; const caRenewalTypes = [{ label: "Renew with same key pair", value: CaRenewalType.EXISTING }]; @@ -29,13 +30,6 @@ const isValidDate = (dateString: string) => { return !Number.isNaN(date.getTime()); }; -// const getMiddleDate = (date1: Date, date2: Date): Date => { -// const timestamp1 = date1.getTime(); -// const timestamp2 = date2.getTime(); -// const middleTimestamp = (timestamp1 + timestamp2) / 2; -// return new Date(middleTimestamp); -// }; - const schema = z .object({ type: z.enum([CaRenewalType.EXISTING]), @@ -58,56 +52,36 @@ export const CaRenewalModal = ({ popUp, handlePopUpToggle }: Props) => { caId: string; }; - const { data: ca } = useGetCaById(popUpData?.caId || ""); - const { data: parentCa } = useGetCaById(ca?.parentCaId || ""); + // const { data: ca } = useGetCaById(popUpData?.caId || ""); + // const { data: parentCa } = useGetCaById(ca?.parentCaId || ""); const { mutateAsync: renewCa } = useRenewCa(); const { control, handleSubmit, reset, - formState: { isSubmitting }, - setValue + formState: { isSubmitting } + // setValue } = useForm({ resolver: zodResolver(schema), defaultValues: { type: CaRenewalType.EXISTING, - notAfter: "" // TODO: set sensible default + notAfter: "" // TODO: consider setting a default value } }); - useEffect(() => { - if (ca && ca.status === CaStatus.ACTIVE) { - // if (ca.type === CaType.ROOT) { - // // extend Root CA validity by the same amount of time - // const notBeforeDate = new Date(ca.notBefore as string); - // const notAfterDate = new Date(ca.notAfter as string); + // useEffect(() => { + // if (ca && ca.status === CaStatus.ACTIVE) { + // const notBeforeDate = new Date(ca.notBefore as string); + // const notAfterDate = new Date(ca.notAfter as string); - // const newNotAfterDate = new Date( - // notAfterDate.getTime() + notAfterDate.getTime() - notBeforeDate.getTime() - // ); + // const newNotAfterDate = new Date( + // notAfterDate.getTime() + notAfterDate.getTime() - notBeforeDate.getTime() + // ); - // setValue("notAfter", newNotAfterDate.toISOString().split("T")[0]); - // } else if (ca.type === CaType.INTERMEDIATE && parentCa) { - // } - // extend Root CA validity by the same amount of time - const notBeforeDate = new Date(ca.notBefore as string); - const notAfterDate = new Date(ca.notAfter as string); - - const newNotAfterDate = new Date( - notAfterDate.getTime() + notAfterDate.getTime() - notBeforeDate.getTime() - ); - - setValue("notAfter", newNotAfterDate.toISOString().split("T")[0]); - } - - // if (ca && parentCa) { - // // intermediate CA - // } else if (ca && ca.notAfter && !parentCa) { - // // root CA - // const timeDifference = new Date(ca.).getTime() - startDate.getTime(); - // } - }, [ca, parentCa]); + // setValue("notAfter", newNotAfterDate.toISOString().split("T")[0]); + // } + // }, [ca, parentCa]); const onFormSubmit = async ({ type, notAfter }: FormData) => { try { @@ -130,13 +104,6 @@ export const CaRenewalModal = ({ popUp, handlePopUpToggle }: Props) => { reset(); } catch (err) { console.error(err); - const error = err as any; - const text = error?.response?.data?.message ?? "Failed to renew CA"; - - createNotification({ - text, - type: "error" - }); } }; From 8da8c6a66c8fdc412c49bd2a6cd202cdaed837fc Mon Sep 17 00:00:00 2001 From: Tuan Dang Date: Mon, 5 Aug 2024 20:06:17 -0700 Subject: [PATCH 3/7] Block expired CAs from issuing certs --- .../certificate-authority-service.ts | 24 +++++++++++++++---- 1 file changed, 19 insertions(+), 5 deletions(-) diff --git a/backend/src/services/certificate-authority/certificate-authority-service.ts b/backend/src/services/certificate-authority/certificate-authority-service.ts index 0e356a4ae..dc1d927da 100644 --- a/backend/src/services/certificate-authority/certificate-authority-service.ts +++ b/backend/src/services/certificate-authority/certificate-authority-service.ts @@ -348,7 +348,7 @@ export const certificateAuthorityServiceFactory = ({ if (ca.type === CaType.ROOT) throw new BadRequestError({ message: "Root CA cannot generate CSR" }); - const caCert = await certificateAuthorityCertDAL.findOne({ caId: ca.id }); + const [caCert] = await certificateAuthorityCertDAL.find({ caId: ca.id }, { sort: [["version", "desc"]] }); if (caCert) throw new BadRequestError({ message: "CA already has a certificate installed" }); const { caPrivateKey, caPublicKey } = await getCaCredentials({ @@ -767,6 +767,13 @@ export const certificateAuthorityServiceFactory = ({ if (ca.status === CaStatus.DISABLED) throw new BadRequestError({ message: "CA is disabled" }); + const [caCert] = await certificateAuthorityCertDAL.find({ caId: ca.id }, { sort: [["version", "desc"]] }); + if (!caCert) throw new BadRequestError({ message: "CA does not have a certificate installed" }); + + if (ca.notAfter && new Date() > new Date(ca.notAfter)) { + throw new BadRequestError({ message: "CA is expired" }); + } + const alg = keyAlgorithmToAlgCfg(ca.keyAlgorithm as CertKeyAlgorithm); const certificateManagerKmsId = await getProjectKmsCertificateKeyId({ @@ -778,7 +785,6 @@ export const certificateAuthorityServiceFactory = ({ kmsId: certificateManagerKmsId }); - const caCert = await certificateAuthorityCertDAL.findOne({ caId: ca.id }); const decryptedCaCert = await kmsDecryptor({ cipherTextBlob: caCert.encryptedCertificate }); @@ -896,7 +902,7 @@ export const certificateAuthorityServiceFactory = ({ ProjectPermissionSub.CertificateAuthorities ); - const caCert = await certificateAuthorityCertDAL.findOne({ caId: ca.id }); + const [caCert] = await certificateAuthorityCertDAL.find({ caId: ca.id }, { sort: [["version", "desc"]] }); if (caCert) throw new BadRequestError({ message: "CA has already imported a certificate" }); const certObj = new x509.X509Certificate(certificate); @@ -1022,9 +1028,13 @@ export const certificateAuthorityServiceFactory = ({ if (ca.status === CaStatus.DISABLED) throw new BadRequestError({ message: "CA is disabled" }); - const caCert = await certificateAuthorityCertDAL.findOne({ caId: ca.id }); + const [caCert] = await certificateAuthorityCertDAL.find({ caId: ca.id }, { sort: [["version", "desc"]] }); if (!caCert) throw new BadRequestError({ message: "CA does not have a certificate installed" }); + if (ca.notAfter && new Date() > new Date(ca.notAfter)) { + throw new BadRequestError({ message: "CA is expired" }); + } + const certificateManagerKmsId = await getProjectKmsCertificateKeyId({ projectId: ca.projectId, projectDAL, @@ -1225,9 +1235,13 @@ export const certificateAuthorityServiceFactory = ({ if (ca.status === CaStatus.DISABLED) throw new BadRequestError({ message: "CA is disabled" }); - const caCert = await certificateAuthorityCertDAL.findOne({ caId: ca.id }); + const [caCert] = await certificateAuthorityCertDAL.find({ caId: ca.id }, { sort: [["version", "desc"]] }); if (!caCert) throw new BadRequestError({ message: "CA does not have a certificate installed" }); + if (ca.notAfter && new Date() > new Date(ca.notAfter)) { + throw new BadRequestError({ message: "CA is expired" }); + } + const certificateManagerKmsId = await getProjectKmsCertificateKeyId({ projectId: ca.projectId, projectDAL, From 5af53d3398601e803b2503fc8e7a84a3853ed1c0 Mon Sep 17 00:00:00 2001 From: Tuan Dang Date: Mon, 5 Aug 2024 20:19:14 -0700 Subject: [PATCH 4/7] Update/remove comments --- .../certificate-authority/certificate-authority-service.ts | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/backend/src/services/certificate-authority/certificate-authority-service.ts b/backend/src/services/certificate-authority/certificate-authority-service.ts index dc1d927da..52cfa8039 100644 --- a/backend/src/services/certificate-authority/certificate-authority-service.ts +++ b/backend/src/services/certificate-authority/certificate-authority-service.ts @@ -389,8 +389,6 @@ export const certificateAuthorityServiceFactory = ({ /** * Renew certificate for CA with id [caId] * Note: Currently implements CA renewal with same key-pair only - * - * TODO: check rebuilt chain? */ const renewCaCert = async ({ caId, notAfter, actorId, actorAuthMethod, actor, actorOrgId }: TRenewCaCertDTO) => { const ca = await certificateAuthorityDAL.findById(caId); @@ -950,7 +948,8 @@ export const certificateAuthorityServiceFactory = ({ plainText: Buffer.from(certificateChain) }); - // TODO: validate that >latest< public-private key of CA is used to sign the certificate + // TODO: validate that latest key-pair of CA is used to sign the certificate + // once renewal with new key pair is supported const { caSecret, caPublicKey } = await getCaCredentials({ caId: ca.id, certificateAuthorityDAL, From bb6416acb7efe1f2c8dea9f1cd6fc85e2441ed70 Mon Sep 17 00:00:00 2001 From: Tuan Dang Date: Tue, 13 Aug 2024 11:22:45 -0700 Subject: [PATCH 5/7] Update CA certificate tracking impl to use foreign ref instead of number --- .../20240802181855_ca-cert-version.ts | 21 +++--- .../src/db/schemas/certificate-authorities.ts | 2 +- backend/src/db/schemas/dynamic-secrets.ts | 10 ++- backend/src/db/schemas/webhooks.ts | 15 ++-- .../routes/v1/certificate-authority-router.ts | 2 +- .../certificate-authority-fns.ts | 3 +- .../certificate-authority-service.ts | 73 ++++++++++--------- .../certificate-authority-types.ts | 2 +- frontend/src/hooks/api/ca/mutations.tsx | 4 +- frontend/src/hooks/api/ca/types.ts | 3 +- 10 files changed, 77 insertions(+), 58 deletions(-) diff --git a/backend/src/db/migrations/20240802181855_ca-cert-version.ts b/backend/src/db/migrations/20240802181855_ca-cert-version.ts index 7c7d956fc..805cf0a53 100644 --- a/backend/src/db/migrations/20240802181855_ca-cert-version.ts +++ b/backend/src/db/migrations/20240802181855_ca-cert-version.ts @@ -4,16 +4,19 @@ import { TableName } from "../schemas"; export async function up(knex: Knex): Promise { if (await knex.schema.hasTable(TableName.CertificateAuthority)) { - const hasActiveCaCertVersionColumn = await knex.schema.hasColumn( - TableName.CertificateAuthority, - "activeCaCertVersion" - ); - if (!hasActiveCaCertVersionColumn) { + const hasActiveCaCertIdColumn = await knex.schema.hasColumn(TableName.CertificateAuthority, "activeCaCertId"); + if (!hasActiveCaCertIdColumn) { await knex.schema.alterTable(TableName.CertificateAuthority, (t) => { - t.integer("activeCaCertVersion").nullable(); + t.uuid("activeCaCertId").nullable(); + t.foreign("activeCaCertId").references("id").inTable(TableName.CertificateAuthorityCert); }); - await knex(TableName.CertificateAuthority).where("status", "active").update({ activeCaCertVersion: 1 }); + await knex.raw(` + UPDATE "${TableName.CertificateAuthority}" ca + SET "activeCaCertId" = cac.id + FROM "${TableName.CertificateAuthorityCert}" cac + WHERE ca.id = cac."caId" + `); } } @@ -63,9 +66,9 @@ export async function up(knex: Knex): Promise { export async function down(knex: Knex): Promise { if (await knex.schema.hasTable(TableName.CertificateAuthority)) { - if (await knex.schema.hasColumn(TableName.CertificateAuthority, "activeCaCertVersion")) { + if (await knex.schema.hasColumn(TableName.CertificateAuthority, "activeCaCertId")) { await knex.schema.alterTable(TableName.CertificateAuthority, (t) => { - t.dropColumn("activeCaCertVersion"); + t.dropColumn("activeCaCertId"); }); } } diff --git a/backend/src/db/schemas/certificate-authorities.ts b/backend/src/db/schemas/certificate-authorities.ts index 9255b037e..e59a9225c 100644 --- a/backend/src/db/schemas/certificate-authorities.ts +++ b/backend/src/db/schemas/certificate-authorities.ts @@ -28,7 +28,7 @@ export const CertificateAuthoritiesSchema = z.object({ keyAlgorithm: z.string(), notBefore: z.date().nullable().optional(), notAfter: z.date().nullable().optional(), - activeCaCertVersion: z.number().nullable().optional() + activeCaCertId: z.string().uuid().nullable().optional() }); export type TCertificateAuthorities = z.infer; diff --git a/backend/src/db/schemas/dynamic-secrets.ts b/backend/src/db/schemas/dynamic-secrets.ts index d90f1f7d2..b27da396c 100644 --- a/backend/src/db/schemas/dynamic-secrets.ts +++ b/backend/src/db/schemas/dynamic-secrets.ts @@ -5,8 +5,6 @@ import { z } from "zod"; -import { zodBuffer } from "@app/lib/zod"; - import { TImmutableDBKeys } from "./models"; export const DynamicSecretsSchema = z.object({ @@ -16,12 +14,16 @@ export const DynamicSecretsSchema = z.object({ type: z.string(), defaultTTL: z.string(), maxTTL: z.string().nullable().optional(), + inputIV: z.string(), + inputCiphertext: z.string(), + inputTag: z.string(), + algorithm: z.string().default("aes-256-gcm"), + keyEncoding: z.string().default("utf8"), folderId: z.string().uuid(), status: z.string().nullable().optional(), statusDetails: z.string().nullable().optional(), createdAt: z.date(), - updatedAt: z.date(), - encryptedConfig: zodBuffer + updatedAt: z.date() }); export type TDynamicSecrets = z.infer; diff --git a/backend/src/db/schemas/webhooks.ts b/backend/src/db/schemas/webhooks.ts index 3f670497f..a7aac2933 100644 --- a/backend/src/db/schemas/webhooks.ts +++ b/backend/src/db/schemas/webhooks.ts @@ -5,22 +5,27 @@ import { z } from "zod"; -import { zodBuffer } from "@app/lib/zod"; - import { TImmutableDBKeys } from "./models"; export const WebhooksSchema = z.object({ id: z.string().uuid(), secretPath: z.string().default("/"), + url: z.string(), lastStatus: z.string().nullable().optional(), lastRunErrorMessage: z.string().nullable().optional(), isDisabled: z.boolean().default(false), + encryptedSecretKey: z.string().nullable().optional(), + iv: z.string().nullable().optional(), + tag: z.string().nullable().optional(), + algorithm: z.string().nullable().optional(), + keyEncoding: z.string().nullable().optional(), createdAt: z.date(), updatedAt: z.date(), envId: z.string().uuid(), - type: z.string().default("general").nullable().optional(), - encryptedSecretKeyWithKms: zodBuffer.nullable().optional(), - encryptedUrl: zodBuffer + urlCipherText: z.string().nullable().optional(), + urlIV: z.string().nullable().optional(), + urlTag: z.string().nullable().optional(), + type: z.string().default("general").nullable().optional() }); export type TWebhooks = z.infer; diff --git a/backend/src/server/routes/v1/certificate-authority-router.ts b/backend/src/server/routes/v1/certificate-authority-router.ts index bc5294f05..103d430c0 100644 --- a/backend/src/server/routes/v1/certificate-authority-router.ts +++ b/backend/src/server/routes/v1/certificate-authority-router.ts @@ -283,7 +283,7 @@ export const registerCaRouter = async (server: FastifyZodProvider) => { }, onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), schema: { - description: "Renew CA certificate for CA", + description: "Perform CA certificate renewal", params: z.object({ caId: z.string().trim().describe(CERTIFICATE_AUTHORITIES.RENEW_CA_CERT.caId) }), diff --git a/backend/src/services/certificate-authority/certificate-authority-fns.ts b/backend/src/services/certificate-authority/certificate-authority-fns.ts index 84d8d263d..bf9f639e8 100644 --- a/backend/src/services/certificate-authority/certificate-authority-fns.ts +++ b/backend/src/services/certificate-authority/certificate-authority-fns.ts @@ -206,8 +206,9 @@ export const getCaCertChain = async ({ }: TGetCaCertChainDTO) => { const ca = await certificateAuthorityDAL.findById(caId); if (!ca) throw new BadRequestError({ message: "CA not found" }); + if (!ca.activeCaCertId) throw new BadRequestError({ message: "CA does not have a certificate installed" }); - const caCert = await certificateAuthorityCertDAL.findOne({ caId: ca.id }); + const caCert = await certificateAuthorityCertDAL.findById(ca.activeCaCertId); const keyId = await getProjectKmsCertificateKeyId({ projectId: ca.projectId, diff --git a/backend/src/services/certificate-authority/certificate-authority-service.ts b/backend/src/services/certificate-authority/certificate-authority-service.ts index 52cfa8039..68d9bbf01 100644 --- a/backend/src/services/certificate-authority/certificate-authority-service.ts +++ b/backend/src/services/certificate-authority/certificate-authority-service.ts @@ -51,7 +51,10 @@ type TCertificateAuthorityServiceFactoryDep = { TCertificateAuthorityDALFactory, "transaction" | "create" | "findById" | "updateById" | "deleteById" | "findOne" >; - certificateAuthorityCertDAL: Pick; + certificateAuthorityCertDAL: Pick< + TCertificateAuthorityCertDALFactory, + "create" | "findOne" | "transaction" | "find" | "findById" + >; certificateAuthoritySecretDAL: Pick; certificateAuthorityCrlDAL: Pick; certificateAuthorityQueue: TCertificateAuthorityQueueFactory; // TODO: Pick @@ -153,8 +156,7 @@ export const certificateAuthorityServiceFactory = ({ maxPathLength, notBefore: notBeforeDate, notAfter: notAfterDate, - serialNumber, - activeCaCertVersion: 1 + serialNumber }) }, tx @@ -213,7 +215,7 @@ export const certificateAuthorityServiceFactory = ({ plainText: Buffer.alloc(0) }); - await certificateAuthorityCertDAL.create( + const caCert = await certificateAuthorityCertDAL.create( { caId: ca.id, encryptedCertificate, @@ -223,6 +225,14 @@ export const certificateAuthorityServiceFactory = ({ }, tx ); + + await certificateAuthorityDAL.updateById( + ca.id, + { + activeCaCertId: caCert.id + }, + tx + ); } // create empty CRL @@ -347,9 +357,7 @@ export const certificateAuthorityServiceFactory = ({ ); if (ca.type === CaType.ROOT) throw new BadRequestError({ message: "Root CA cannot generate CSR" }); - - const [caCert] = await certificateAuthorityCertDAL.find({ caId: ca.id }, { sort: [["version", "desc"]] }); - if (caCert) throw new BadRequestError({ message: "CA already has a certificate installed" }); + if (ca.activeCaCertId) throw new BadRequestError({ message: "CA already has a certificate installed" }); const { caPrivateKey, caPublicKey } = await getCaCredentials({ caId, @@ -394,6 +402,8 @@ export const certificateAuthorityServiceFactory = ({ const ca = await certificateAuthorityDAL.findById(caId); if (!ca) throw new BadRequestError({ message: "CA not found" }); + if (!ca.activeCaCertId) throw new BadRequestError({ message: "CA does not have a certificate installed" }); + const { permission } = await permissionService.getProjectPermission( actor, actorId, @@ -410,8 +420,7 @@ export const certificateAuthorityServiceFactory = ({ if (ca.status === CaStatus.DISABLED) throw new BadRequestError({ message: "CA is disabled" }); // get latest CA certificate - const [caCert] = await certificateAuthorityCertDAL.find({ caId: ca.id }, { sort: [["version", "desc"]] }); - if (!caCert) throw new BadRequestError({ message: "CA does not have a certificate installed" }); + const caCert = await certificateAuthorityCertDAL.findById(ca.activeCaCertId); const serialNumber = crypto.randomBytes(32).toString("hex"); @@ -489,13 +498,12 @@ export const certificateAuthorityServiceFactory = ({ }); await certificateAuthorityDAL.transaction(async (tx) => { - const newActiveCaCertVersion = caCert.version + 1; - await certificateAuthorityCertDAL.create( + const newCaCert = await certificateAuthorityCertDAL.create( { caId: ca.id, encryptedCertificate, encryptedCertificateChain, - version: newActiveCaCertVersion, + version: caCert.version + 1, caSecretId: caSecret.id }, tx @@ -504,7 +512,7 @@ export const certificateAuthorityServiceFactory = ({ await certificateAuthorityDAL.updateById( ca.id, { - activeCaCertVersion: newActiveCaCertVersion, + activeCaCertId: newCaCert.id, notBefore: notBeforeDate, notAfter: new Date(notAfter) }, @@ -533,10 +541,9 @@ export const certificateAuthorityServiceFactory = ({ }); // get latest parent CA certificate - const [parentCaCert] = await certificateAuthorityCertDAL.find( - { caId: parentCa.id }, - { sort: [["version", "desc"]] } - ); + if (!parentCa.activeCaCertId) + throw new BadRequestError({ message: "Parent CA does not have a certificate installed" }); + const parentCaCert = await certificateAuthorityCertDAL.findById(parentCa.activeCaCertId); const decryptedParentCaCert = await kmsDecryptor({ cipherTextBlob: parentCaCert.encryptedCertificate @@ -581,7 +588,7 @@ export const certificateAuthorityServiceFactory = ({ const intermediateCert = await x509.X509CertificateGenerator.create({ serialNumber, subject: csrObj.subject, - issuer: caCertObj.subject, + issuer: parentCaCertObj.subject, notBefore: notBeforeDate, notAfter: new Date(notAfter), signingKey: parentCaPrivateKey, @@ -600,7 +607,7 @@ export const certificateAuthorityServiceFactory = ({ ca.maxPathLength === -1 || !ca.maxPathLength ? undefined : ca.maxPathLength, true ), - await x509.AuthorityKeyIdentifierExtension.create(caCertObj, false), + await x509.AuthorityKeyIdentifierExtension.create(parentCaCertObj, false), await x509.SubjectKeyIdentifierExtension.create(csrObj.publicKey) ] }); @@ -624,13 +631,12 @@ export const certificateAuthorityServiceFactory = ({ }); await certificateAuthorityDAL.transaction(async (tx) => { - const newActiveCaCertVersion = caCert.version + 1; - await certificateAuthorityCertDAL.create( + const newCaCert = await certificateAuthorityCertDAL.create( { caId: ca.id, encryptedCertificate, encryptedCertificateChain, - version: newActiveCaCertVersion, + version: caCert.version + 1, caSecretId: caSecret.id }, tx @@ -639,7 +645,7 @@ export const certificateAuthorityServiceFactory = ({ await certificateAuthorityDAL.updateById( ca.id, { - activeCaCertVersion: newActiveCaCertVersion, + activeCaCertId: newCaCert.id, notBefore: notBeforeDate, notAfter: new Date(notAfter) }, @@ -764,9 +770,9 @@ export const certificateAuthorityServiceFactory = ({ ); if (ca.status === CaStatus.DISABLED) throw new BadRequestError({ message: "CA is disabled" }); + if (!ca.activeCaCertId) throw new BadRequestError({ message: "CA does not have a certificate installed" }); - const [caCert] = await certificateAuthorityCertDAL.find({ caId: ca.id }, { sort: [["version", "desc"]] }); - if (!caCert) throw new BadRequestError({ message: "CA does not have a certificate installed" }); + const caCert = await certificateAuthorityCertDAL.findById(ca.activeCaCertId); if (ca.notAfter && new Date() > new Date(ca.notAfter)) { throw new BadRequestError({ message: "CA is expired" }); @@ -900,8 +906,7 @@ export const certificateAuthorityServiceFactory = ({ ProjectPermissionSub.CertificateAuthorities ); - const [caCert] = await certificateAuthorityCertDAL.find({ caId: ca.id }, { sort: [["version", "desc"]] }); - if (caCert) throw new BadRequestError({ message: "CA has already imported a certificate" }); + if (ca.activeCaCertId) throw new BadRequestError({ message: "CA has already imported a certificate" }); const certObj = new x509.X509Certificate(certificate); const maxPathLength = certObj.getExtension(x509.BasicConstraintsExtension)?.pathLength; @@ -967,7 +972,7 @@ export const certificateAuthorityServiceFactory = ({ } await certificateAuthorityCertDAL.transaction(async (tx) => { - await certificateAuthorityCertDAL.create( + const newCaCert = await certificateAuthorityCertDAL.create( { caId: ca.id, encryptedCertificate, @@ -986,7 +991,8 @@ export const certificateAuthorityServiceFactory = ({ notBefore: new Date(certObj.notBefore), notAfter: new Date(certObj.notAfter), serialNumber: certObj.serialNumber, - parentCaId: parentCa?.id + parentCaId: parentCa?.id, + activeCaCertId: newCaCert.id }, tx ); @@ -1026,9 +1032,8 @@ export const certificateAuthorityServiceFactory = ({ ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Create, ProjectPermissionSub.Certificates); if (ca.status === CaStatus.DISABLED) throw new BadRequestError({ message: "CA is disabled" }); - - const [caCert] = await certificateAuthorityCertDAL.find({ caId: ca.id }, { sort: [["version", "desc"]] }); - if (!caCert) throw new BadRequestError({ message: "CA does not have a certificate installed" }); + if (!ca.activeCaCertId) throw new BadRequestError({ message: "CA does not have a certificate installed" }); + const caCert = await certificateAuthorityCertDAL.findById(ca.activeCaCertId); if (ca.notAfter && new Date() > new Date(ca.notAfter)) { throw new BadRequestError({ message: "CA is expired" }); @@ -1233,9 +1238,9 @@ export const certificateAuthorityServiceFactory = ({ ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Create, ProjectPermissionSub.Certificates); if (ca.status === CaStatus.DISABLED) throw new BadRequestError({ message: "CA is disabled" }); + if (!ca.activeCaCertId) throw new BadRequestError({ message: "CA does not have a certificate installed" }); - const [caCert] = await certificateAuthorityCertDAL.find({ caId: ca.id }, { sort: [["version", "desc"]] }); - if (!caCert) throw new BadRequestError({ message: "CA does not have a certificate installed" }); + const caCert = await certificateAuthorityCertDAL.findById(ca.activeCaCertId); if (ca.notAfter && new Date() > new Date(ca.notAfter)) { throw new BadRequestError({ message: "CA is expired" }); diff --git a/backend/src/services/certificate-authority/certificate-authority-types.ts b/backend/src/services/certificate-authority/certificate-authority-types.ts index f97844870..91e76e49c 100644 --- a/backend/src/services/certificate-authority/certificate-authority-types.ts +++ b/backend/src/services/certificate-authority/certificate-authority-types.ts @@ -134,7 +134,7 @@ export type TGetCaCertChainsDTO = { export type TGetCaCertChainDTO = { caId: string; certificateAuthorityDAL: Pick; - certificateAuthorityCertDAL: Pick; + certificateAuthorityCertDAL: Pick; projectDAL: Pick; kmsService: Pick; }; diff --git a/frontend/src/hooks/api/ca/mutations.tsx b/frontend/src/hooks/api/ca/mutations.tsx index 54109adfc..27d03248f 100644 --- a/frontend/src/hooks/api/ca/mutations.tsx +++ b/frontend/src/hooks/api/ca/mutations.tsx @@ -16,7 +16,8 @@ import { TRenewCaResponse, TSignIntermediateDTO, TSignIntermediateResponse, - TUpdateCaDTO} from "./types"; + TUpdateCaDTO +} from "./types"; export const useCreateCa = () => { const queryClient = useQueryClient(); @@ -123,6 +124,7 @@ export const useRenewCa = () => { }, onSuccess: (_, { caId, projectSlug }) => { queryClient.invalidateQueries(workspaceKeys.getWorkspaceCas({ projectSlug })); + queryClient.invalidateQueries(caKeys.getCaById(caId)); queryClient.invalidateQueries(caKeys.getCaCert(caId)); queryClient.invalidateQueries(caKeys.getCaCerts(caId)); queryClient.invalidateQueries(caKeys.getCaCsr(caId)); diff --git a/frontend/src/hooks/api/ca/types.ts b/frontend/src/hooks/api/ca/types.ts index bba7aa699..7513070c8 100644 --- a/frontend/src/hooks/api/ca/types.ts +++ b/frontend/src/hooks/api/ca/types.ts @@ -1,5 +1,5 @@ import { CertKeyAlgorithm } from "../certificates/enums"; -import { CaRenewalType,CaStatus, CaType } from "./enums"; +import { CaRenewalType, CaStatus, CaType } from "./enums"; export type TCertificateAuthority = { id: string; @@ -19,6 +19,7 @@ export type TCertificateAuthority = { notAfter?: string; notBefore?: string; keyAlgorithm: CertKeyAlgorithm; + activeCaCertId?: string; createdAt: string; updatedAt: string; }; From f993e4aa5c4f3389fadf3a8e8b05cb5906d65b7f Mon Sep 17 00:00:00 2001 From: Tuan Dang Date: Tue, 13 Aug 2024 11:31:57 -0700 Subject: [PATCH 6/7] Clear type check issue --- backend/src/services/certificate/certificate-service.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/src/services/certificate/certificate-service.ts b/backend/src/services/certificate/certificate-service.ts index f05ed8a87..01f736c29 100644 --- a/backend/src/services/certificate/certificate-service.ts +++ b/backend/src/services/certificate/certificate-service.ts @@ -21,7 +21,7 @@ type TCertificateServiceFactoryDep = { certificateDAL: Pick; certificateBodyDAL: Pick; certificateAuthorityDAL: Pick; - certificateAuthorityCertDAL: Pick; + certificateAuthorityCertDAL: Pick; certificateAuthorityCrlDAL: Pick; certificateAuthoritySecretDAL: Pick; projectDAL: Pick; From 856c2423be0e83a7dd33c1f98825ef086530df38 Mon Sep 17 00:00:00 2001 From: Tuan Dang Date: Wed, 14 Aug 2024 15:43:49 -0700 Subject: [PATCH 7/7] Update cert structure to ref correct ca cert in cert chain retrieval --- .../20240802181855_ca-cert-version.ts | 28 +++++++++++++++++++ backend/src/db/schemas/certificates.ts | 3 +- .../certificate-authority-fns.ts | 12 ++++---- .../certificate-authority-service.ts | 14 ++++++---- .../certificate-authority-types.ts | 2 +- .../certificate/certificate-service.ts | 2 +- 6 files changed, 45 insertions(+), 16 deletions(-) diff --git a/backend/src/db/migrations/20240802181855_ca-cert-version.ts b/backend/src/db/migrations/20240802181855_ca-cert-version.ts index 805cf0a53..24eca185d 100644 --- a/backend/src/db/migrations/20240802181855_ca-cert-version.ts +++ b/backend/src/db/migrations/20240802181855_ca-cert-version.ts @@ -62,6 +62,26 @@ export async function up(knex: Knex): Promise { t.dropUnique(["caId"]); }); } + + if (await knex.schema.hasTable(TableName.Certificate)) { + await knex.schema.alterTable(TableName.Certificate, (t) => { + t.uuid("caCertId").nullable(); + t.foreign("caCertId").references("id").inTable(TableName.CertificateAuthorityCert); + }); + + await knex.raw(` + UPDATE "${TableName.Certificate}" cert + SET "caCertId" = ( + SELECT caCert.id + FROM "${TableName.CertificateAuthorityCert}" caCert + WHERE caCert."caId" = cert."caId" + ) + `); + + await knex.schema.alterTable(TableName.Certificate, (t) => { + t.uuid("caCertId").notNullable().alter(); + }); + } } export async function down(knex: Knex): Promise { @@ -86,4 +106,12 @@ export async function down(knex: Knex): Promise { }); } } + + if (await knex.schema.hasTable(TableName.Certificate)) { + if (await knex.schema.hasColumn(TableName.Certificate, "caCertId")) { + await knex.schema.alterTable(TableName.Certificate, (t) => { + t.dropColumn("caCertId"); + }); + } + } } diff --git a/backend/src/db/schemas/certificates.ts b/backend/src/db/schemas/certificates.ts index 833396fb1..cb14d05f9 100644 --- a/backend/src/db/schemas/certificates.ts +++ b/backend/src/db/schemas/certificates.ts @@ -20,7 +20,8 @@ export const CertificatesSchema = z.object({ notAfter: z.date(), revokedAt: z.date().nullable().optional(), revocationReason: z.number().nullable().optional(), - altNames: z.string().default("").nullable().optional() + altNames: z.string().default("").nullable().optional(), + caCertId: z.string().uuid() }); export type TCertificates = z.infer; diff --git a/backend/src/services/certificate-authority/certificate-authority-fns.ts b/backend/src/services/certificate-authority/certificate-authority-fns.ts index bf9f639e8..4e3df1173 100644 --- a/backend/src/services/certificate-authority/certificate-authority-fns.ts +++ b/backend/src/services/certificate-authority/certificate-authority-fns.ts @@ -195,20 +195,18 @@ export const getCaCertChains = async ({ /** * Return the decrypted pem-encoded certificate and certificate chain - * for CA with id [caId]. + * corresponding to CA certificate with id [caCertId]. */ export const getCaCertChain = async ({ - caId, + caCertId, certificateAuthorityDAL, certificateAuthorityCertDAL, projectDAL, kmsService }: TGetCaCertChainDTO) => { - const ca = await certificateAuthorityDAL.findById(caId); - if (!ca) throw new BadRequestError({ message: "CA not found" }); - if (!ca.activeCaCertId) throw new BadRequestError({ message: "CA does not have a certificate installed" }); - - const caCert = await certificateAuthorityCertDAL.findById(ca.activeCaCertId); + const caCert = await certificateAuthorityCertDAL.findById(caCertId); + if (!caCert) throw new BadRequestError({ message: "CA certificate not found" }); + const ca = await certificateAuthorityDAL.findById(caCert.caId); const keyId = await getProjectKmsCertificateKeyId({ projectId: ca.projectId, diff --git a/backend/src/services/certificate-authority/certificate-authority-service.ts b/backend/src/services/certificate-authority/certificate-authority-service.ts index 68d9bbf01..b63ec9ba9 100644 --- a/backend/src/services/certificate-authority/certificate-authority-service.ts +++ b/backend/src/services/certificate-authority/certificate-authority-service.ts @@ -617,7 +617,7 @@ export const certificateAuthorityServiceFactory = ({ }); const { caCert: parentCaCertificate, caCertChain: parentCaCertChain } = await getCaCertChain({ - caId: parentCa.id, + caCertId: parentCa.activeCaCertId, certificateAuthorityDAL, certificateAuthorityCertDAL, projectDAL, @@ -704,11 +704,11 @@ export const certificateAuthorityServiceFactory = ({ /** * Return current certificate and certificate chain for CA - * get latest?? ca cert */ const getCaCert = async ({ caId, actorId, actorAuthMethod, actor, actorOrgId }: TGetCaCertDTO) => { const ca = await certificateAuthorityDAL.findById(caId); if (!ca) throw new BadRequestError({ message: "CA not found" }); + if (!ca.activeCaCertId) throw new BadRequestError({ message: "CA does not have a certificate installed" }); const { permission } = await permissionService.getProjectPermission( actor, @@ -724,7 +724,7 @@ export const certificateAuthorityServiceFactory = ({ ); const { caCert, caCertChain, serialNumber } = await getCaCertChain({ - caId, + caCertId: ca.activeCaCertId, certificateAuthorityDAL, certificateAuthorityCertDAL, projectDAL, @@ -860,7 +860,7 @@ export const certificateAuthorityServiceFactory = ({ }); const { caCert: issuingCaCertificate, caCertChain } = await getCaCertChain({ - caId, + caCertId: ca.activeCaCertId, certificateAuthorityDAL, certificateAuthorityCertDAL, projectDAL, @@ -1166,6 +1166,7 @@ export const certificateAuthorityServiceFactory = ({ const cert = await certificateDAL.create( { caId: ca.id, + caCertId: caCert.id, status: CertStatus.ACTIVE, friendlyName: friendlyName || commonName, commonName, @@ -1189,7 +1190,7 @@ export const certificateAuthorityServiceFactory = ({ }); const { caCert: issuingCaCertificate, caCertChain } = await getCaCertChain({ - caId: ca.id, + caCertId: caCert.id, certificateAuthorityDAL, certificateAuthorityCertDAL, projectDAL, @@ -1369,6 +1370,7 @@ export const certificateAuthorityServiceFactory = ({ const cert = await certificateDAL.create( { caId: ca.id, + caCertId: caCert.id, status: CertStatus.ACTIVE, friendlyName: friendlyName || csrObj.subject, commonName: cn, @@ -1392,7 +1394,7 @@ export const certificateAuthorityServiceFactory = ({ }); const { caCert: issuingCaCertificate, caCertChain } = await getCaCertChain({ - caId: ca.id, + caCertId: ca.activeCaCertId, certificateAuthorityDAL, certificateAuthorityCertDAL, projectDAL, diff --git a/backend/src/services/certificate-authority/certificate-authority-types.ts b/backend/src/services/certificate-authority/certificate-authority-types.ts index 91e76e49c..31a6e1629 100644 --- a/backend/src/services/certificate-authority/certificate-authority-types.ts +++ b/backend/src/services/certificate-authority/certificate-authority-types.ts @@ -132,7 +132,7 @@ export type TGetCaCertChainsDTO = { }; export type TGetCaCertChainDTO = { - caId: string; + caCertId: string; certificateAuthorityDAL: Pick; certificateAuthorityCertDAL: Pick; projectDAL: Pick; diff --git a/backend/src/services/certificate/certificate-service.ts b/backend/src/services/certificate/certificate-service.ts index 01f736c29..8dc2de901 100644 --- a/backend/src/services/certificate/certificate-service.ts +++ b/backend/src/services/certificate/certificate-service.ts @@ -180,7 +180,7 @@ export const certificateServiceFactory = ({ const certObj = new x509.X509Certificate(decryptedCert); const { caCert, caCertChain } = await getCaCertChain({ - caId: ca.id, + caCertId: cert.caCertId, certificateAuthorityDAL, certificateAuthorityCertDAL, projectDAL,