feat: kmip create and get

This commit is contained in:
Sheen Capadngan
2025-02-11 23:32:11 +08:00
parent 77fac45df1
commit 292c9051bd
9 changed files with 320 additions and 6 deletions

View File

@@ -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<TCreateAuditLogDTO, "userAgent" | "userAgentType" | "ipAddress" | "actor">;
ssoConfig: Awaited<ReturnType<TSamlConfigServiceFactory["getSaml"]>>;
ldapConfig: Awaited<ReturnType<TLdapConfigServiceFactory["getLdapCfg"]>>;
@@ -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<TUserDALFactory, "findById">;
kmipClient: Pick<TKmipClientDALFactory, "findOne">;
};
}
}

View File

@@ -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" });
};

View File

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

View File

@@ -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<TProjectDALFactory, "getProjectFromSplitId" | "findById">;
};
export type TKmipOperationServiceFactory = ReturnType<typeof kmipOperationServiceFactory>;
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
};
};

View File

@@ -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<TPermissionServiceFactory, "getProjectPermission">;
kmsService: Pick<TKmsServiceFactory, "decryptWithRootKey">;
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
};
};

View File

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

View File

@@ -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<FastifyZodProvider["store"]>("store", {
user: userDAL
user: userDAL,
kmipClient: kmipClientDAL
});
await server.register(injectIdentity, { userDAL, serviceTokenDAL });

View File

@@ -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<TEncryptWithKmsDTO, "plainText">, tx?: Knex) => {
const kmsDoc = await kmsDAL.findByIdWithAssociatedKms(kmsId, tx);
if (!kmsDoc) {
@@ -967,6 +992,7 @@ export const kmsServiceFactory = ({
getProjectKeyBackup,
loadProjectKeyBackup,
getKmsById,
createCipherPairWithDataKey
createCipherPairWithDataKey,
getKeyMaterial
};
};

View File

@@ -61,3 +61,6 @@ export enum RootKeyEncryptionStrategy {
Software = "SOFTWARE",
HSM = "HSM"
}
export type TGetKeyMaterialDTO = {
kmsId: string;
};