From c70d0a577c3ae7ee8ae68ca6c5738e217176674a Mon Sep 17 00:00:00 2001 From: Tuan Dang Date: Mon, 5 Aug 2024 15:43:40 -0700 Subject: [PATCH] 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" - }); } };