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; };