From 292c9051bdae76a68052e036a6f52d172cb9a641 Mon Sep 17 00:00:00 2001 From: Sheen Capadngan Date: Tue, 11 Feb 2025 23:32:11 +0800 Subject: [PATCH] feat: kmip create and get --- backend/src/@types/fastify.d.ts | 8 ++ backend/src/ee/routes/v1/index.ts | 2 + .../src/ee/routes/v1/kmip-operation-router.ts | 117 ++++++++++++++++++ .../services/kmip/kmip-operation-service.ts | 110 ++++++++++++++++ backend/src/ee/services/kmip/kmip-service.ts | 28 ++++- backend/src/ee/services/kmip/kmip-types.ts | 13 ++ backend/src/server/routes/index.ts | 17 ++- backend/src/services/kms/kms-service.ts | 28 ++++- backend/src/services/kms/kms-types.ts | 3 + 9 files changed, 320 insertions(+), 6 deletions(-) create mode 100644 backend/src/ee/routes/v1/kmip-operation-router.ts create mode 100644 backend/src/ee/services/kmip/kmip-operation-service.ts diff --git a/backend/src/@types/fastify.d.ts b/backend/src/@types/fastify.d.ts index d56c13431..daa2f5ca2 100644 --- a/backend/src/@types/fastify.d.ts +++ b/backend/src/@types/fastify.d.ts @@ -16,6 +16,8 @@ import { TExternalKmsServiceFactory } from "@app/ee/services/external-kms/extern import { TGroupServiceFactory } from "@app/ee/services/group/group-service"; import { TIdentityProjectAdditionalPrivilegeServiceFactory } from "@app/ee/services/identity-project-additional-privilege/identity-project-additional-privilege-service"; import { TIdentityProjectAdditionalPrivilegeV2ServiceFactory } from "@app/ee/services/identity-project-additional-privilege-v2/identity-project-additional-privilege-v2-service"; +import { TKmipClientDALFactory } from "@app/ee/services/kmip/kmip-client-dal"; +import { TKmipOperationServiceFactory } from "@app/ee/services/kmip/kmip-operation-service"; import { TKmipServiceFactory } from "@app/ee/services/kmip/kmip-service"; import { TLdapConfigServiceFactory } from "@app/ee/services/ldap-config/ldap-config-service"; import { TLicenseServiceFactory } from "@app/ee/services/license/license-service"; @@ -121,6 +123,10 @@ declare module "fastify" { isUserCompleted: string; providerAuthToken: string; }; + kmipUser: { + projectId: string; + clientId: string; + }; auditLogInfo: Pick; ssoConfig: Awaited>; ldapConfig: Awaited>; @@ -214,11 +220,13 @@ declare module "fastify" { appConnection: TAppConnectionServiceFactory; secretSync: TSecretSyncServiceFactory; kmip: TKmipServiceFactory; + kmipOperation: TKmipOperationServiceFactory; }; // this is exclusive use for middlewares in which we need to inject data // everywhere else access using service layer store: { user: Pick; + kmipClient: Pick; }; } } diff --git a/backend/src/ee/routes/v1/index.ts b/backend/src/ee/routes/v1/index.ts index cb2e7e0da..2a83ba097 100644 --- a/backend/src/ee/routes/v1/index.ts +++ b/backend/src/ee/routes/v1/index.ts @@ -9,6 +9,7 @@ import { registerDynamicSecretRouter } from "./dynamic-secret-router"; import { registerExternalKmsRouter } from "./external-kms-router"; import { registerGroupRouter } from "./group-router"; import { registerIdentityProjectAdditionalPrivilegeRouter } from "./identity-project-additional-privilege-router"; +import { registerKmipOperationRouter } from "./kmip-operation-router"; import { registerKmipRouter } from "./kmip-router"; import { registerLdapRouter } from "./ldap-router"; import { registerLicenseRouter } from "./license-router"; @@ -112,4 +113,5 @@ export const registerV1EERoutes = async (server: FastifyZodProvider) => { await server.register(registerProjectTemplateRouter, { prefix: "/project-templates" }); await server.register(registerKmipRouter, { prefix: "/kmip" }); + await server.register(registerKmipOperationRouter, { prefix: "/kmip-operations" }); }; diff --git a/backend/src/ee/routes/v1/kmip-operation-router.ts b/backend/src/ee/routes/v1/kmip-operation-router.ts new file mode 100644 index 000000000..b90e097a7 --- /dev/null +++ b/backend/src/ee/routes/v1/kmip-operation-router.ts @@ -0,0 +1,117 @@ +import crypto from "crypto"; +import jwt, { JwtPayload } from "jsonwebtoken"; +import z from "zod"; + +import { KmsKeysSchema } from "@app/db/schemas"; +import { SymmetricEncryption } from "@app/lib/crypto/cipher"; +import { ForbiddenRequestError, NotFoundError } from "@app/lib/errors"; +import { writeLimit } from "@app/server/config/rateLimiter"; +import { CertKeyAlgorithm } from "@app/services/certificate/certificate-types"; + +export const registerKmipOperationRouter = async (server: FastifyZodProvider) => { + server.decorateRequest("kmipUser", null); + + server.addHook("preHandler", async (req) => { + const token = req.headers["x-kmip-jwt"] as string; + const serverCertSerialNumber = req.headers["x-server-certificate-serial-number"] as string; + + if (!jwt) { + throw new ForbiddenRequestError({ + message: "Missing KMIP JWT" + }); + } + + if (!serverCertSerialNumber) { + throw new ForbiddenRequestError({ + message: "Missing server certificate serial number from request" + }); + } + + const serverCert = await server.services.kmip.getServerCertificateBySerialNumber(serverCertSerialNumber); + + // TODO: assert that server certificate used is not revoked + // TODO: assert that client certificate used is not revoked + + const publicKey = crypto.createPublicKey({ + key: serverCert.publicKey, + format: "pem", + type: [CertKeyAlgorithm.ECDSA_P256, CertKeyAlgorithm.ECDSA_P384].includes(serverCert.keyAlgorithm) + ? "spki" + : "pkcs1" + }); + + const decodedToken = jwt.verify(token, publicKey) as JwtPayload & { projectId: string; clientId: string }; + + const kmipClient = await server.store.kmipClient.findOne({ + id: decodedToken.clientId, + projectId: decodedToken.projectId + }); + + if (!kmipClient) { + throw new NotFoundError({ + message: "KMIP client cannot be found." + }); + } + + req.kmipUser = { + projectId: decodedToken.projectId, + clientId: decodedToken.clientId + }; + }); + + server.route({ + method: "POST", + url: "/create", + config: { + rateLimit: writeLimit + }, + schema: { + description: "KMIP endpoint for creating managed objects", + body: z.object({ + encryptionAlgorithm: z.nativeEnum(SymmetricEncryption) + }), + response: { + 200: KmsKeysSchema + } + }, + handler: async (req) => { + const object = await server.services.kmipOperation.create({ + projectId: req.kmipUser.projectId, + clientId: req.kmipUser.clientId, + encryptionAlgorithm: req.body.encryptionAlgorithm + }); + + return object; + } + }); + + server.route({ + method: "POST", + url: "/get", + config: { + rateLimit: writeLimit + }, + schema: { + description: "KMIP endpoint for getting managed objects", + body: z.object({ + id: z.string() + }), + response: { + 200: z.object({ + id: z.string(), + value: z.string(), + algorithm: z.string() + }) + } + }, + handler: async (req) => { + const object = await server.services.kmipOperation.get({ + projectId: req.kmipUser.projectId, + clientId: req.kmipUser.clientId, + id: req.body.id + }); + + return object; + } + }); +}; diff --git a/backend/src/ee/services/kmip/kmip-operation-service.ts b/backend/src/ee/services/kmip/kmip-operation-service.ts new file mode 100644 index 000000000..bafecefa3 --- /dev/null +++ b/backend/src/ee/services/kmip/kmip-operation-service.ts @@ -0,0 +1,110 @@ +import { ProjectType } from "@app/db/schemas"; +import { BadRequestError, ForbiddenRequestError, NotFoundError } from "@app/lib/errors"; +import { TKmsKeyDALFactory } from "@app/services/kms/kms-key-dal"; +import { TKmsServiceFactory } from "@app/services/kms/kms-service"; +import { TProjectDALFactory } from "@app/services/project/project-dal"; + +import { TKmipClientDALFactory } from "./kmip-client-dal"; +import { KmipPermission } from "./kmip-enum"; +import { TKmipCreateDTO, TKmipGetDTO } from "./kmip-types"; + +type TKmipOperationServiceFactoryDep = { + kmsService: TKmsServiceFactory; + kmsDAL: TKmsKeyDALFactory; + kmipClientDAL: TKmipClientDALFactory; + projectDAL: Pick; +}; + +export type TKmipOperationServiceFactory = ReturnType; + +export const kmipOperationServiceFactory = ({ + kmsService, + kmsDAL, + projectDAL, + kmipClientDAL +}: TKmipOperationServiceFactoryDep) => { + const create = async ({ projectId: preSplitProjectId, clientId, encryptionAlgorithm }: TKmipCreateDTO) => { + let projectId = preSplitProjectId; + const cmekProjectFromSplit = await projectDAL.getProjectFromSplitId(projectId, ProjectType.KMS); + if (cmekProjectFromSplit) { + projectId = cmekProjectFromSplit.id; + } + + const project = await projectDAL.findById(projectId); + const kmipClient = await kmipClientDAL.findOne({ + id: clientId, + projectId + }); + + if (!kmipClient.permissions?.includes(KmipPermission.Create)) { + throw new ForbiddenRequestError({ + message: "Client does not have sufficient permission to perform KMIP create" + }); + } + + const kmsKey = await kmsService.generateKmsKey({ + encryptionAlgorithm, + orgId: project.orgId, + projectId, + isReserved: false + }); + + return kmsKey; + }; + + const get = async ({ projectId: preSplitProjectId, id, clientId }: TKmipGetDTO) => { + let projectId = preSplitProjectId; + const cmekProjectFromSplit = await projectDAL.getProjectFromSplitId(projectId, ProjectType.KMS); + + if (cmekProjectFromSplit) { + projectId = cmekProjectFromSplit.id; + } + + const kmipClient = await kmipClientDAL.findOne({ + id: clientId, + projectId + }); + + if (!kmipClient.permissions?.includes(KmipPermission.Get)) { + throw new ForbiddenRequestError({ + message: "Client does not have sufficient permission to perform KMIP get" + }); + } + + const key = await kmsDAL.findOne({ + id, + projectId + }); + + if (!key) { + throw new NotFoundError({ message: `Key with ID ${id} not found` }); + } + + if (key.isReserved) { + throw new BadRequestError({ message: "Cannot get reserved keys" }); + } + + const completeKeyDetails = await kmsDAL.findByIdWithAssociatedKms(id); + + if (!completeKeyDetails.internalKms) { + throw new BadRequestError({ + message: "Cannot get external key" + }); + } + + const kmsKey = await kmsService.getKeyMaterial({ + kmsId: key.id + }); + + return { + id: key.id, + value: kmsKey.toString("base64"), + algorithm: completeKeyDetails.internalKms.encryptionAlgorithm + }; + }; + + return { + create, + get + }; +}; diff --git a/backend/src/ee/services/kmip/kmip-service.ts b/backend/src/ee/services/kmip/kmip-service.ts index c430d4d06..a108dbb41 100644 --- a/backend/src/ee/services/kmip/kmip-service.ts +++ b/backend/src/ee/services/kmip/kmip-service.ts @@ -18,6 +18,7 @@ import { TKmipClientCertificateDALFactory } from "./kmip-client-certificate-dal" import { TKmipClientDALFactory } from "./kmip-client-dal"; import { INSTANCE_KMIP_CONFIG_ID } from "./kmip-constants"; import { TKmipInstanceConfigDALFactory } from "./kmip-instance-config-dal"; +import { TKmipInstanceServerCertificateDALFactory } from "./kmip-instance-server-certificate-dal"; import { TCreateKmipClientCertificateDTO, TCreateKmipClientDTO, @@ -30,6 +31,7 @@ import { type TKmipServiceFactoryDep = { kmipClientDAL: TKmipClientDALFactory; kmipClientCertificateDAL: TKmipClientCertificateDALFactory; + kmipInstanceServerCertificateDAL: TKmipInstanceServerCertificateDALFactory; permissionService: Pick; kmsService: Pick; kmipInstanceConfigDAL: TKmipInstanceConfigDALFactory; @@ -42,7 +44,8 @@ export const kmipServiceFactory = ({ permissionService, kmipClientCertificateDAL, kmipInstanceConfigDAL, - kmsService + kmsService, + kmipInstanceServerCertificateDAL }: TKmipServiceFactoryDep) => { const createKmipClient = async ({ actor, @@ -325,12 +328,33 @@ export const kmipServiceFactory = ({ }; }; + const getServerCertificateBySerialNumber = async (serialNumber: string) => { + const serverCert = await kmipInstanceServerCertificateDAL.findOne({ + serialNumber + }); + + if (!serverCert) { + throw new NotFoundError({ + message: "Server certificate not found" + }); + } + + const decryptWithRootKey = kmsService.decryptWithRootKey(); + const parsedCertificate = new x509.X509Certificate(decryptWithRootKey(serverCert.encryptedCertificate)); + + return { + publicKey: parsedCertificate.publicKey.toString("pem"), + keyAlgorithm: serverCert.keyAlgorithm as CertKeyAlgorithm + }; + }; + return { createKmipClient, updateKmipClient, deleteKmipClient, getKmipClient, listKmipClientsByProjectId, - createKmipClientCertificate + createKmipClientCertificate, + getServerCertificateBySerialNumber }; }; diff --git a/backend/src/ee/services/kmip/kmip-types.ts b/backend/src/ee/services/kmip/kmip-types.ts index 3447454e6..39dc4438c 100644 --- a/backend/src/ee/services/kmip/kmip-types.ts +++ b/backend/src/ee/services/kmip/kmip-types.ts @@ -1,3 +1,4 @@ +import { SymmetricEncryption } from "@app/lib/crypto/cipher"; import { OrderByDirection, TProjectPermission } from "@app/lib/types"; import { CertKeyAlgorithm } from "@app/services/certificate/certificate-types"; @@ -41,3 +42,15 @@ export type TListKmipClientsByProjectIdDTO = { orderDirection?: OrderByDirection; search?: string; } & TProjectPermission; + +export type TKmipCreateDTO = { + clientId: string; + projectId: string; + encryptionAlgorithm: SymmetricEncryption; +}; + +export type TKmipGetDTO = { + clientId: string; + projectId: string; + id: string; +}; diff --git a/backend/src/server/routes/index.ts b/backend/src/server/routes/index.ts index 1de9b7780..d9e3ff977 100644 --- a/backend/src/server/routes/index.ts +++ b/backend/src/server/routes/index.ts @@ -39,6 +39,7 @@ import { kmipClientCertificateDALFactory } from "@app/ee/services/kmip/kmip-clie import { kmipClientDALFactory } from "@app/ee/services/kmip/kmip-client-dal"; import { kmipInstanceConfigDALFactory } from "@app/ee/services/kmip/kmip-instance-config-dal"; import { kmipInstanceServerCertificateDALFactory } from "@app/ee/services/kmip/kmip-instance-server-certificate-dal"; +import { kmipOperationServiceFactory } from "@app/ee/services/kmip/kmip-operation-service"; import { kmipServiceFactory } from "@app/ee/services/kmip/kmip-service"; import { ldapConfigDALFactory } from "@app/ee/services/ldap-config/ldap-config-dal"; import { ldapConfigServiceFactory } from "@app/ee/services/ldap-config/ldap-config-service"; @@ -1434,7 +1435,15 @@ export const registerRoutes = async ( permissionService, kmipClientCertificateDAL, kmipInstanceConfigDAL, - kmsService + kmsService, + kmipInstanceServerCertificateDAL + }); + + const kmipOperationService = kmipOperationServiceFactory({ + kmsService, + kmsDAL, + projectDAL, + kmipClientDAL }); await superAdminService.initServerCfg(); @@ -1536,7 +1545,8 @@ export const registerRoutes = async ( totp: totpService, appConnection: appConnectionService, secretSync: secretSyncService, - kmip: kmipService + kmip: kmipService, + kmipOperation: kmipOperationService }); const cronJobs: CronJob[] = []; @@ -1548,7 +1558,8 @@ export const registerRoutes = async ( } server.decorate("store", { - user: userDAL + user: userDAL, + kmipClient: kmipClientDAL }); await server.register(injectIdentity, { userDAL, serviceTokenDAL }); diff --git a/backend/src/services/kms/kms-service.ts b/backend/src/services/kms/kms-service.ts index c41783860..3ca3f849c 100644 --- a/backend/src/services/kms/kms-service.ts +++ b/backend/src/services/kms/kms-service.ts @@ -37,6 +37,7 @@ import { TEncryptWithKmsDataKeyDTO, TEncryptWithKmsDTO, TGenerateKMSDTO, + TGetKeyMaterialDTO, TUpdateProjectSecretManagerKmsKeyDTO } from "./kms-types"; @@ -326,6 +327,30 @@ export const kmsServiceFactory = ({ }; }; + const getKeyMaterial = async ({ kmsId }: TGetKeyMaterialDTO) => { + const kmsDoc = await kmsDAL.findByIdWithAssociatedKms(kmsId); + if (!kmsDoc) { + throw new NotFoundError({ message: `KMS with ID '${kmsId}' not found` }); + } + + if (kmsDoc.isReserved) { + throw new BadRequestError({ + message: "Cannot get key material for reserved key" + }); + } + + if (kmsDoc.externalKms) { + throw new BadRequestError({ + message: "Cannot get key material for external key" + }); + } + + const keyCipher = symmetricCipherService(SymmetricEncryption.AES_GCM_256); + const kmsKey = keyCipher.decrypt(kmsDoc.internalKms?.encryptedKey as Buffer, ROOT_ENCRYPTION_KEY); + + return kmsKey; + }; + const encryptWithKmsKey = async ({ kmsId }: Omit, tx?: Knex) => { const kmsDoc = await kmsDAL.findByIdWithAssociatedKms(kmsId, tx); if (!kmsDoc) { @@ -967,6 +992,7 @@ export const kmsServiceFactory = ({ getProjectKeyBackup, loadProjectKeyBackup, getKmsById, - createCipherPairWithDataKey + createCipherPairWithDataKey, + getKeyMaterial }; }; diff --git a/backend/src/services/kms/kms-types.ts b/backend/src/services/kms/kms-types.ts index f655d4b5d..d4c192132 100644 --- a/backend/src/services/kms/kms-types.ts +++ b/backend/src/services/kms/kms-types.ts @@ -61,3 +61,6 @@ export enum RootKeyEncryptionStrategy { Software = "SOFTWARE", HSM = "HSM" } +export type TGetKeyMaterialDTO = { + kmsId: string; +};