diff --git a/backend/src/@types/knex.d.ts b/backend/src/@types/knex.d.ts index 8b3c8b0ed..f85e9393d 100644 --- a/backend/src/@types/knex.d.ts +++ b/backend/src/@types/knex.d.ts @@ -146,6 +146,12 @@ import { TKmipClients, TKmipClientsInsert, TKmipClientsUpdate, + TKmipInstanceConfigs, + TKmipInstanceConfigsInsert, + TKmipInstanceConfigsUpdate, + TKmipInstanceServerCertificates, + TKmipInstanceServerCertificatesInsert, + TKmipInstanceServerCertificatesUpdate, TKmsKeys, TKmsKeysInsert, TKmsKeysUpdate, @@ -906,5 +912,15 @@ declare module "knex/types/tables" { >; [TableName.SecretSync]: KnexOriginal.CompositeTableType; [TableName.KmipClient]: KnexOriginal.CompositeTableType; + [TableName.KmipInstanceConfig]: KnexOriginal.CompositeTableType< + TKmipInstanceConfigs, + TKmipInstanceConfigsInsert, + TKmipInstanceConfigsUpdate + >; + [TableName.KmipInstanceServerCertificates]: KnexOriginal.CompositeTableType< + TKmipInstanceServerCertificates, + TKmipInstanceServerCertificatesInsert, + TKmipInstanceServerCertificatesUpdate + >; } } diff --git a/backend/src/db/migrations/20250203141127_add-kmip.ts b/backend/src/db/migrations/20250203141127_add-kmip.ts index c80059b9d..24b806a9d 100644 --- a/backend/src/db/migrations/20250203141127_add-kmip.ts +++ b/backend/src/db/migrations/20250203141127_add-kmip.ts @@ -1,6 +1,7 @@ import { Knex } from "knex"; import { TableName } from "../schemas"; +import { createOnUpdateTrigger, dropOnUpdateTrigger } from "../utils"; export async function up(knex: Knex): Promise { const hasKmipClientTable = await knex.schema.hasTable(TableName.KmipClient); @@ -14,6 +15,55 @@ export async function up(knex: Knex): Promise { t.foreign("projectId").references("id").inTable(TableName.Project).onDelete("CASCADE"); }); } + + const hasKmipInstanceConfigTable = await knex.schema.hasTable(TableName.KmipInstanceConfig); + if (!hasKmipInstanceConfigTable) { + await knex.schema.createTable(TableName.KmipInstanceConfig, (t) => { + t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid()); + + t.string("caKeyAlgorithm").notNullable(); + + t.datetime("rootCaIssuedAt").notNullable(); + t.datetime("rootCaExpiration").notNullable(); + t.string("rootCaSerialNumber").notNullable(); + t.binary("encryptedRootCaCertificate").notNullable(); + t.binary("encryptedRootCaPrivateKey").notNullable(); + + t.datetime("serverIntermediateCaIssuedAt").notNullable(); + t.datetime("serverIntermediateCaExpiration").notNullable(); + t.string("serverIntermediateCaSerialNumber"); + t.binary("encryptedServerIntermediateCaCertificate").notNullable(); + t.binary("encryptedServerIntermediateCaChain").notNullable(); + t.binary("encryptedServerIntermediateCaPrivateKey").notNullable(); + + t.datetime("clientIntermediateCaIssuedAt").notNullable(); + t.datetime("clientIntermediateCaExpiration").notNullable(); + t.string("clientIntermediateCaSerialNumber").notNullable(); + t.binary("encryptedClientIntermediateCaCertificate").notNullable(); + t.binary("encryptedClientIntermediateCaChain").notNullable(); + t.binary("encryptedClientIntermediateCaPrivateKey").notNullable(); + + t.timestamps(true, true, true); + }); + + await createOnUpdateTrigger(knex, TableName.KmipInstanceConfig); + } + + const hasKmipInstanceServerCertTable = await knex.schema.hasTable(TableName.KmipInstanceServerCertificates); + if (!hasKmipInstanceServerCertTable) { + await knex.schema.createTable(TableName.KmipInstanceServerCertificates, (t) => { + t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid()); + t.string("commonName").notNullable(); + t.string("altNames").notNullable(); + t.string("serialNumber").notNullable(); + t.string("keyAlgorithm").notNullable(); + t.datetime("issuedAt").notNullable(); + t.datetime("expiration").notNullable(); + t.binary("encryptedCertificate").notNullable(); + t.binary("encryptedChain").notNullable(); + t.binary("encryptedPrivateKey").notNullable(); + }); + } } export async function down(knex: Knex): Promise { @@ -21,4 +71,15 @@ export async function down(knex: Knex): Promise { if (hasKmipClientTable) { await knex.schema.dropTable(TableName.KmipClient); } + + const hasKmipInstanceConfigTable = await knex.schema.hasTable(TableName.KmipInstanceConfig); + if (hasKmipInstanceConfigTable) { + await knex.schema.dropTable(TableName.KmipInstanceConfig); + await dropOnUpdateTrigger(knex, TableName.KmipInstanceConfig); + } + + const hasKmipInstanceServerCertTable = await knex.schema.hasTable(TableName.KmipInstanceServerCertificates); + if (hasKmipInstanceServerCertTable) { + await knex.schema.dropTable(TableName.KmipInstanceServerCertificates); + } } diff --git a/backend/src/db/schemas/index.ts b/backend/src/db/schemas/index.ts index f3e687676..e5dc3e53c 100644 --- a/backend/src/db/schemas/index.ts +++ b/backend/src/db/schemas/index.ts @@ -46,6 +46,8 @@ export * from "./integration-auths"; export * from "./integrations"; export * from "./internal-kms"; export * from "./kmip-clients"; +export * from "./kmip-instance-configs"; +export * from "./kmip-instance-server-certificates"; export * from "./kms-key-versions"; export * from "./kms-keys"; export * from "./kms-root-config"; diff --git a/backend/src/db/schemas/kmip-instance-configs.ts b/backend/src/db/schemas/kmip-instance-configs.ts new file mode 100644 index 000000000..62038fd8a --- /dev/null +++ b/backend/src/db/schemas/kmip-instance-configs.ts @@ -0,0 +1,38 @@ +// Code generated by automation script, DO NOT EDIT. +// Automated by pulling database and generating zod schema +// To update. Just run npm run generate:schema +// Written by akhilmhdh. + +import { z } from "zod"; + +import { zodBuffer } from "@app/lib/zod"; + +import { TImmutableDBKeys } from "./models"; + +export const KmipInstanceConfigsSchema = z.object({ + id: z.string().uuid(), + caKeyAlgorithm: z.string(), + rootCaIssuedAt: z.date(), + rootCaExpiration: z.date(), + rootCaSerialNumber: z.string(), + encryptedRootCaCertificate: zodBuffer, + encryptedRootCaPrivateKey: zodBuffer, + serverIntermediateCaIssuedAt: z.date(), + serverIntermediateCaExpiration: z.date(), + serverIntermediateCaSerialNumber: z.string().nullable().optional(), + encryptedServerIntermediateCaCertificate: zodBuffer, + encryptedServerIntermediateCaChain: zodBuffer, + encryptedServerIntermediateCaPrivateKey: zodBuffer, + clientIntermediateCaIssuedAt: z.date(), + clientIntermediateCaExpiration: z.date(), + clientIntermediateCaSerialNumber: z.string(), + encryptedClientIntermediateCaCertificate: zodBuffer, + encryptedClientIntermediateCaChain: zodBuffer, + encryptedClientIntermediateCaPrivateKey: zodBuffer, + createdAt: z.date(), + updatedAt: z.date() +}); + +export type TKmipInstanceConfigs = z.infer; +export type TKmipInstanceConfigsInsert = Omit, TImmutableDBKeys>; +export type TKmipInstanceConfigsUpdate = Partial, TImmutableDBKeys>>; diff --git a/backend/src/db/schemas/kmip-instance-server-certificates.ts b/backend/src/db/schemas/kmip-instance-server-certificates.ts new file mode 100644 index 000000000..916e0b131 --- /dev/null +++ b/backend/src/db/schemas/kmip-instance-server-certificates.ts @@ -0,0 +1,32 @@ +// Code generated by automation script, DO NOT EDIT. +// Automated by pulling database and generating zod schema +// To update. Just run npm run generate:schema +// Written by akhilmhdh. + +import { z } from "zod"; + +import { zodBuffer } from "@app/lib/zod"; + +import { TImmutableDBKeys } from "./models"; + +export const KmipInstanceServerCertificatesSchema = z.object({ + id: z.string().uuid(), + commonName: z.string(), + altNames: z.string(), + serialNumber: z.string(), + keyAlgorithm: z.string(), + issuedAt: z.date(), + expiration: z.date(), + encryptedCertificate: zodBuffer, + encryptedChain: zodBuffer, + encryptedPrivateKey: zodBuffer +}); + +export type TKmipInstanceServerCertificates = z.infer; +export type TKmipInstanceServerCertificatesInsert = Omit< + z.input, + TImmutableDBKeys +>; +export type TKmipInstanceServerCertificatesUpdate = Partial< + Omit, TImmutableDBKeys> +>; diff --git a/backend/src/db/schemas/models.ts b/backend/src/db/schemas/models.ts index fdaad428b..fd132b461 100644 --- a/backend/src/db/schemas/models.ts +++ b/backend/src/db/schemas/models.ts @@ -133,7 +133,9 @@ export enum TableName { ProjectSlackConfigs = "project_slack_configs", AppConnection = "app_connections", SecretSync = "secret_syncs", - KmipClient = "kmip_clients" + KmipClient = "kmip_clients", + KmipInstanceConfig = "kmip_instance_configs", + KmipInstanceServerCertificates = "kmip_instance_server_certificates" } export type TImmutableDBKeys = "id" | "createdAt" | "updatedAt"; diff --git a/backend/src/ee/services/kmip/kmip-instance-config-dal.ts b/backend/src/ee/services/kmip/kmip-instance-config-dal.ts new file mode 100644 index 000000000..61b994d1a --- /dev/null +++ b/backend/src/ee/services/kmip/kmip-instance-config-dal.ts @@ -0,0 +1,13 @@ +import { TDbClient } from "@app/db"; +import { TableName } from "@app/db/schemas"; +import { ormify } from "@app/lib/knex"; + +export type TKmipInstanceConfigDALFactory = ReturnType; + +export const kmipInstanceConfigDALFactory = (db: TDbClient) => { + const kmipInstanceConfigOrm = ormify(db, TableName.KmipInstanceConfig); + + return { + ...kmipInstanceConfigOrm + }; +}; diff --git a/backend/src/ee/services/kmip/kmip-instance-server-certificate-dal.ts b/backend/src/ee/services/kmip/kmip-instance-server-certificate-dal.ts new file mode 100644 index 000000000..ebaeb1142 --- /dev/null +++ b/backend/src/ee/services/kmip/kmip-instance-server-certificate-dal.ts @@ -0,0 +1,13 @@ +import { TDbClient } from "@app/db"; +import { TableName } from "@app/db/schemas"; +import { ormify } from "@app/lib/knex"; + +export type TKmipInstanceServerCertificateDALFactory = ReturnType; + +export const kmipInstanceServerCertificateDALFactory = (db: TDbClient) => { + const kmipInstanceServerCertificateOrm = ormify(db, TableName.KmipInstanceServerCertificates); + + return { + ...kmipInstanceServerCertificateOrm + }; +}; diff --git a/backend/src/server/routes/index.ts b/backend/src/server/routes/index.ts index eabbb8e0a..109c9632c 100644 --- a/backend/src/server/routes/index.ts +++ b/backend/src/server/routes/index.ts @@ -36,6 +36,8 @@ import { identityProjectAdditionalPrivilegeDALFactory } from "@app/ee/services/i import { identityProjectAdditionalPrivilegeServiceFactory } from "@app/ee/services/identity-project-additional-privilege/identity-project-additional-privilege-service"; import { identityProjectAdditionalPrivilegeV2ServiceFactory } from "@app/ee/services/identity-project-additional-privilege-v2/identity-project-additional-privilege-v2-service"; 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 { 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"; @@ -383,6 +385,8 @@ export const registerRoutes = async ( const projectTemplateDAL = projectTemplateDALFactory(db); const resourceMetadataDAL = resourceMetadataDALFactory(db); const kmipClientDAL = kmipClientDALFactory(db); + const kmipInstanceConfigDAL = kmipInstanceConfigDALFactory(db); + const kmipInstanceServerCertificateDAL = kmipInstanceServerCertificateDALFactory(db); const permissionService = permissionServiceFactory({ permissionDAL, @@ -623,7 +627,9 @@ export const registerRoutes = async ( orgService, keyStore, licenseService, - kmsService + kmsService, + kmipInstanceConfigDAL, + kmipInstanceServerCertificateDAL }); const orgAdminService = orgAdminServiceFactory({ diff --git a/backend/src/server/routes/v1/admin-router.ts b/backend/src/server/routes/v1/admin-router.ts index 6ecebb274..9783af31b 100644 --- a/backend/src/server/routes/v1/admin-router.ts +++ b/backend/src/server/routes/v1/admin-router.ts @@ -1,3 +1,4 @@ +import ms from "ms"; import { z } from "zod"; import { OrganizationsSchema, SuperAdminSchema, UsersSchema } from "@app/db/schemas"; @@ -7,6 +8,8 @@ import { readLimit, writeLimit } from "@app/server/config/rateLimiter"; import { verifySuperAdmin } from "@app/server/plugins/auth/superAdmin"; 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 { validateAltNamesField } from "@app/services/certificate-authority/certificate-authority-validators"; import { RootKeyEncryptionStrategy } from "@app/services/kms/kms-types"; import { getServerCfg } from "@app/services/super-admin/super-admin-service"; import { LoginMethod } from "@app/services/super-admin/super-admin-types"; @@ -316,4 +319,88 @@ export const registerAdminRouter = async (server: FastifyZodProvider) => { }; } }); + + server.route({ + method: "POST", + url: "/kmip", + config: { + rateLimit: writeLimit + }, + schema: { + body: z.object({ + caKeyAlgorithm: z.nativeEnum(CertKeyAlgorithm) + }), + response: { + 200: z.object({ + serverCertificateChain: z.string() + }) + } + }, + onRequest: (req, res, done) => { + verifyAuth([AuthMode.JWT])(req, res, () => { + verifySuperAdmin(req, res, done); + }); + }, + handler: async (req) => { + return server.services.superAdmin.setupInstanceKmip({ + ...req.body + }); + } + }); + + server.route({ + method: "GET", + url: "/kmip", + config: { + rateLimit: readLimit + }, + schema: { + response: { + 200: z.object({ + serverCertificateChain: z.string() + }) + } + }, + onRequest: (req, res, done) => { + verifyAuth([AuthMode.JWT])(req, res, () => { + verifySuperAdmin(req, res, done); + }); + }, + handler: async () => { + return server.services.superAdmin.getInstanceKmip(); + } + }); + + server.route({ + method: "POST", + url: "/kmip/server-certificates", + config: { + rateLimit: writeLimit + }, + schema: { + body: z.object({ + commonName: z.string().trim().min(1), + altNames: validateAltNamesField, + keyAlgorithm: z.nativeEnum(CertKeyAlgorithm), + ttl: z.string().refine((val) => ms(val) > 0, "TTL must be a positive number") + }), + response: { + 200: z.object({ + certificateChain: z.string(), + certificate: z.string(), + privateKey: z.string() + }) + } + }, + onRequest: (req, res, done) => { + verifyAuth([AuthMode.JWT])(req, res, () => { + verifySuperAdmin(req, res, done); + }); + }, + handler: async (req) => { + return server.services.superAdmin.generateInstanceKmipServerCertificate({ + ...req.body + }); + } + }); }; diff --git a/backend/src/services/super-admin/super-admin-service.ts b/backend/src/services/super-admin/super-admin-service.ts index b0fdd9c5c..33220119d 100644 --- a/backend/src/services/super-admin/super-admin-service.ts +++ b/backend/src/services/super-admin/super-admin-service.ts @@ -1,6 +1,12 @@ +import * as x509 from "@peculiar/x509"; import bcrypt from "bcrypt"; +import crypto, { KeyObject } from "crypto"; +import ms from "ms"; +import z from "zod"; import { TSuperAdmin, TSuperAdminUpdate } from "@app/db/schemas"; +import { TKmipInstanceConfigDALFactory } from "@app/ee/services/kmip/kmip-instance-config-dal"; +import { TKmipInstanceServerCertificateDALFactory } from "@app/ee/services/kmip/kmip-instance-server-certificate-dal"; import { TLicenseServiceFactory } from "@app/ee/services/license/license-service"; import { TKeyStoreFactory } from "@app/keystore/keystore"; import { getConfig } from "@app/lib/config/env"; @@ -10,6 +16,9 @@ import { BadRequestError, NotFoundError } from "@app/lib/errors"; import { TAuthLoginFactory } from "../auth/auth-login-service"; import { AuthMethod } from "../auth/auth-type"; +import { CertExtendedKeyUsage, CertKeyAlgorithm, CertKeyUsage } from "../certificate/certificate-types"; +import { createSerialNumber, keyAlgorithmToAlgCfg } from "../certificate-authority/certificate-authority-fns"; +import { hostnameRegex } from "../certificate-authority/certificate-authority-validators"; import { KMS_ROOT_CONFIG_UUID } from "../kms/kms-fns"; import { TKmsRootConfigDALFactory } from "../kms/kms-root-config-dal"; import { TKmsServiceFactory } from "../kms/kms-service"; @@ -19,7 +28,13 @@ import { TUserDALFactory } from "../user/user-dal"; import { TUserAliasDALFactory } from "../user-alias/user-alias-dal"; import { UserAliasType } from "../user-alias/user-alias-types"; import { TSuperAdminDALFactory } from "./super-admin-dal"; -import { LoginMethod, TAdminGetUsersDTO, TAdminSignUpDTO } from "./super-admin-types"; +import { + LoginMethod, + TAdminGetUsersDTO, + TAdminSignUpDTO, + TGenerateInstanceKmipServerCertificateDTO, + TSetupInstanceKmipDTO +} from "./super-admin-types"; type TSuperAdminServiceFactoryDep = { serverCfgDAL: TSuperAdminDALFactory; @@ -31,6 +46,8 @@ type TSuperAdminServiceFactoryDep = { orgService: Pick; keyStore: Pick; licenseService: Pick; + kmipInstanceConfigDAL: TKmipInstanceConfigDALFactory; + kmipInstanceServerCertificateDAL: TKmipInstanceServerCertificateDALFactory; }; export type TSuperAdminServiceFactory = ReturnType; @@ -57,7 +74,9 @@ export const superAdminServiceFactory = ({ keyStore, kmsRootConfigDAL, kmsService, - licenseService + licenseService, + kmipInstanceConfigDAL, + kmipInstanceServerCertificateDAL }: TSuperAdminServiceFactoryDep) => { const initServerCfg = async () => { // TODO(akhilmhdh): bad pattern time less change this later to me itself @@ -369,6 +388,302 @@ export const superAdminServiceFactory = ({ await kmsService.updateEncryptionStrategy(strategy); }; + const setupInstanceKmip = async ({ caKeyAlgorithm }: TSetupInstanceKmipDTO) => { + const kmipInstanceConfig = await kmipInstanceConfigDAL.findById(ADMIN_CONFIG_DB_UUID); + if (kmipInstanceConfig) { + throw new BadRequestError({ + message: "KMIP has already been configured for the instance" + }); + } + + const alg = keyAlgorithmToAlgCfg(caKeyAlgorithm); + + // generate root CA + const rootCaSerialNumber = createSerialNumber(); + const rootCaKeys = await crypto.subtle.generateKey(alg, true, ["sign", "verify"]); + const rootCaSkObj = KeyObject.from(rootCaKeys.privateKey); + const rootCaIssuedAt = new Date(); + const rootCaExpiration = new Date(new Date().setFullYear(new Date().getFullYear() + 20)); + + const rootCaCert = await x509.X509CertificateGenerator.createSelfSigned({ + name: "CN=KMIP Root CA", + serialNumber: rootCaSerialNumber, + notBefore: rootCaIssuedAt, + notAfter: rootCaExpiration, + signingAlgorithm: alg, + keys: rootCaKeys, + extensions: [ + // eslint-disable-next-line no-bitwise + new x509.KeyUsagesExtension(x509.KeyUsageFlags.keyCertSign | x509.KeyUsageFlags.cRLSign, true), + await x509.SubjectKeyIdentifierExtension.create(rootCaKeys.publicKey) + ] + }); + + // generate intermediate server CA + const serverIntermediateCaSerialNumber = createSerialNumber(); + const serverIntermediateCaIssuedAt = new Date(); + const serverIntermediateCaExpiration = new Date(new Date().setFullYear(new Date().getFullYear() + 10)); + const serverIntermediateCaKeys = await crypto.subtle.generateKey(alg, true, ["sign", "verify"]); + const serverIntermediateCaSkObj = KeyObject.from(serverIntermediateCaKeys.privateKey); + + const serverIntermediateCaCert = await x509.X509CertificateGenerator.create({ + serialNumber: serverIntermediateCaSerialNumber, + subject: "CN=KMIP Server Intermediate CA", + issuer: rootCaCert.subject, + notBefore: serverIntermediateCaIssuedAt, + notAfter: serverIntermediateCaExpiration, + signingKey: rootCaKeys.privateKey, + publicKey: serverIntermediateCaKeys.publicKey, + signingAlgorithm: alg, + extensions: [ + new x509.KeyUsagesExtension( + // eslint-disable-next-line no-bitwise + x509.KeyUsageFlags.keyCertSign | + x509.KeyUsageFlags.cRLSign | + x509.KeyUsageFlags.digitalSignature | + x509.KeyUsageFlags.keyEncipherment, + true + ), + new x509.BasicConstraintsExtension(true, 0, true), + await x509.AuthorityKeyIdentifierExtension.create(rootCaCert, false), + await x509.SubjectKeyIdentifierExtension.create(serverIntermediateCaKeys.publicKey) + ] + }); + + // generate intermediate client CA + const clientIntermediateCaSerialNumber = createSerialNumber(); + const clientIntermediateCaIssuedAt = new Date(); + const clientIntermediateCaExpiration = new Date(new Date().setFullYear(new Date().getFullYear() + 10)); + const clientIntermediateCaKeys = await crypto.subtle.generateKey(alg, true, ["sign", "verify"]); + const clientIntermediateCaSkObj = KeyObject.from(clientIntermediateCaKeys.privateKey); + + const clientIntermediateCaCert = await x509.X509CertificateGenerator.create({ + serialNumber: clientIntermediateCaSerialNumber, + subject: "CN=KMIP Client Intermediate CA", + issuer: rootCaCert.subject, + notBefore: clientIntermediateCaIssuedAt, + notAfter: clientIntermediateCaExpiration, + signingKey: rootCaKeys.privateKey, + publicKey: clientIntermediateCaKeys.publicKey, + signingAlgorithm: alg, + extensions: [ + new x509.KeyUsagesExtension( + // eslint-disable-next-line no-bitwise + x509.KeyUsageFlags.keyCertSign | + x509.KeyUsageFlags.cRLSign | + x509.KeyUsageFlags.digitalSignature | + x509.KeyUsageFlags.keyEncipherment, + true + ), + new x509.BasicConstraintsExtension(true, 0, true), + await x509.AuthorityKeyIdentifierExtension.create(rootCaCert, false), + await x509.SubjectKeyIdentifierExtension.create(clientIntermediateCaKeys.publicKey) + ] + }); + + const encryptWithRoot = kmsService.encryptWithRootKey(); + + await kmipInstanceConfigDAL.create({ + // @ts-expect-error id is kept as fixed for idempotence and to avoid race condition + id: ADMIN_CONFIG_DB_UUID, + caKeyAlgorithm, + rootCaIssuedAt, + rootCaExpiration, + rootCaSerialNumber, + encryptedRootCaCertificate: encryptWithRoot(Buffer.from(rootCaCert.rawData)), + encryptedRootCaPrivateKey: encryptWithRoot( + rootCaSkObj.export({ + type: "pkcs8", + format: "der" + }) + ), + serverIntermediateCaIssuedAt, + serverIntermediateCaExpiration, + serverIntermediateCaSerialNumber, + encryptedServerIntermediateCaCertificate: encryptWithRoot( + Buffer.from(new Uint8Array(serverIntermediateCaCert.rawData)) + ), + encryptedServerIntermediateCaChain: encryptWithRoot(Buffer.from(rootCaCert.toString("pem"))), + encryptedServerIntermediateCaPrivateKey: encryptWithRoot( + serverIntermediateCaSkObj.export({ + type: "pkcs8", + format: "der" + }) + ), + clientIntermediateCaIssuedAt, + clientIntermediateCaExpiration, + clientIntermediateCaSerialNumber, + encryptedClientIntermediateCaCertificate: encryptWithRoot( + Buffer.from(new Uint8Array(clientIntermediateCaCert.rawData)) + ), + encryptedClientIntermediateCaChain: encryptWithRoot(Buffer.from(rootCaCert.toString("pem"))), + encryptedClientIntermediateCaPrivateKey: encryptWithRoot( + clientIntermediateCaSkObj.export({ + type: "pkcs8", + format: "der" + }) + ) + }); + + return { + // the order of the cert is intentional - for client chains, ordering should be from intermediate to root + serverCertificateChain: `${serverIntermediateCaCert.toString("pem")}\n${rootCaCert.toString("pem")}`.trim() + }; + }; + + const getInstanceKmip = async () => { + const kmipInstanceConfig = await kmipInstanceConfigDAL.findById(ADMIN_CONFIG_DB_UUID); + if (!kmipInstanceConfig) { + throw new BadRequestError({ + message: "KMIP has not been configured for the instance" + }); + } + + const decryptWithRoot = kmsService.decryptWithRootKey(); + const rootCaCert = new x509.X509Certificate(decryptWithRoot(kmipInstanceConfig.encryptedRootCaCertificate)); + const serverIntermediateCaCert = new x509.X509Certificate( + decryptWithRoot(kmipInstanceConfig.encryptedServerIntermediateCaCertificate) + ); + + return { + // the order of the cert is intentional - for client chains, ordering should be from intermediate to root + serverCertificateChain: `${serverIntermediateCaCert.toString("pem")}\n${rootCaCert.toString("pem")}`.trim() + }; + }; + + const generateInstanceKmipServerCertificate = async ({ + ttl, + commonName, + altNames, + keyAlgorithm + }: TGenerateInstanceKmipServerCertificateDTO) => { + const kmipInstanceConfig = await kmipInstanceConfigDAL.findById(ADMIN_CONFIG_DB_UUID); + if (!kmipInstanceConfig) { + throw new BadRequestError({ + message: "KMIP has not been configured for the instance" + }); + } + + const decryptWithRoot = kmsService.decryptWithRootKey(); + const caCertObj = new x509.X509Certificate( + decryptWithRoot(kmipInstanceConfig.encryptedServerIntermediateCaCertificate) + ); + + const notBeforeDate = new Date(); + const notAfterDate = new Date(new Date().getTime() + ms(ttl)); + + const caCertNotBeforeDate = new Date(caCertObj.notBefore); + const caCertNotAfterDate = new Date(caCertObj.notAfter); + + // check not before constraint + if (notBeforeDate < caCertNotBeforeDate) { + throw new BadRequestError({ message: "notBefore date is before CA certificate's notBefore date" }); + } + + if (notBeforeDate > notAfterDate) throw new BadRequestError({ message: "notBefore date is after notAfter date" }); + + // check not after constraint + if (notAfterDate > caCertNotAfterDate) { + throw new BadRequestError({ message: "notAfter date is after CA certificate's notAfter date" }); + } + + const alg = keyAlgorithmToAlgCfg(keyAlgorithm); + const leafKeys = await crypto.subtle.generateKey(alg, true, ["sign", "verify"]); + + const extensions: x509.Extension[] = [ + new x509.BasicConstraintsExtension(false), + await x509.AuthorityKeyIdentifierExtension.create(caCertObj, false), + await x509.SubjectKeyIdentifierExtension.create(leafKeys.publicKey), + new x509.CertificatePolicyExtension(["2.5.29.32.0"]), // anyPolicy + new x509.KeyUsagesExtension( + // eslint-disable-next-line no-bitwise + x509.KeyUsageFlags[CertKeyUsage.DIGITAL_SIGNATURE] | x509.KeyUsageFlags[CertKeyUsage.KEY_ENCIPHERMENT], + true + ), + new x509.ExtendedKeyUsageExtension([x509.ExtendedKeyUsage[CertExtendedKeyUsage.SERVER_AUTH]], true) + ]; + + const altNamesArray: { + type: "email" | "dns"; + value: string; + }[] = altNames + .split(",") + .map((name) => name.trim()) + .map((altName) => { + // check if the altName is a valid email + if (z.string().email().safeParse(altName).success) { + return { + type: "email", + value: altName + }; + } + + // check if the altName is a valid hostname + if (hostnameRegex.test(altName)) { + return { + type: "dns", + value: altName + }; + } + + // If altName is neither a valid email nor a valid hostname, throw an error or handle it accordingly + throw new Error(`Invalid altName: ${altName}`); + }); + + const altNamesExtension = new x509.SubjectAlternativeNameExtension(altNamesArray, false); + extensions.push(altNamesExtension); + + const caAlg = keyAlgorithmToAlgCfg(kmipInstanceConfig.caKeyAlgorithm as CertKeyAlgorithm); + + const decryptedCaCertChain = decryptWithRoot(kmipInstanceConfig.encryptedServerIntermediateCaChain).toString( + "utf-8" + ); + + const caSkObj = crypto.createPrivateKey({ + key: decryptWithRoot(kmipInstanceConfig.encryptedServerIntermediateCaPrivateKey), + format: "der", + type: "pkcs8" + }); + + const caPrivateKey = await crypto.subtle.importKey( + "pkcs8", + caSkObj.export({ format: "der", type: "pkcs8" }), + caAlg, + true, + ["sign"] + ); + + const serialNumber = createSerialNumber(); + const leafCert = await x509.X509CertificateGenerator.create({ + serialNumber, + subject: `CN=${commonName}`, + issuer: caCertObj.subject, + notBefore: notBeforeDate, + notAfter: notAfterDate, + signingKey: caPrivateKey, + publicKey: leafKeys.publicKey, + signingAlgorithm: alg, + extensions + }); + + const encryptWithRoot = kmsService.encryptWithRootKey(); + const skLeafObj = KeyObject.from(leafKeys.privateKey); + + const serverCert = await kmipInstanceServerCertificateDAL.create({ + keyAlgorithm, + issuedAt: notBeforeDate, + expiration: notAfterDate, + serialNumber, + commonName, + altNames, + encryptedCertificate: encryptWithRoot(Buffer.from(new Uint8Array(leafCert.rawData))), + encryptedPrivateKey: encryptWithRoot(skLeafObj.export({ format: "der", type: "pkcs8" })), + encryptedChain: encryptWithRoot(Buffer.from(`${decryptedCaCertChain}\n${caCertObj.toString("pem")}`.trim())) + }); + + return serverCert; + }; + return { initServerCfg, updateServerCfg, @@ -377,6 +692,9 @@ export const superAdminServiceFactory = ({ deleteUser, getAdminSlackConfig, updateRootEncryptionStrategy, - getConfiguredEncryptionStrategies + getConfiguredEncryptionStrategies, + setupInstanceKmip, + getInstanceKmip, + generateInstanceKmipServerCertificate }; }; diff --git a/backend/src/services/super-admin/super-admin-types.ts b/backend/src/services/super-admin/super-admin-types.ts index 2d10941b4..e246dfed8 100644 --- a/backend/src/services/super-admin/super-admin-types.ts +++ b/backend/src/services/super-admin/super-admin-types.ts @@ -1,3 +1,5 @@ +import { CertKeyAlgorithm } from "../certificate/certificate-types"; + export type TAdminSignUpDTO = { email: string; password: string; @@ -31,3 +33,14 @@ export enum LoginMethod { LDAP = "ldap", OIDC = "oidc" } + +export type TSetupInstanceKmipDTO = { + caKeyAlgorithm: CertKeyAlgorithm; +}; + +export type TGenerateInstanceKmipServerCertificateDTO = { + commonName: string; + altNames: string; + keyAlgorithm: CertKeyAlgorithm; + ttl: string; +}; diff --git a/frontend/src/helpers/download.ts b/frontend/src/helpers/download.ts new file mode 100644 index 000000000..50adf4fe2 --- /dev/null +++ b/frontend/src/helpers/download.ts @@ -0,0 +1,6 @@ +import FileSaver from "file-saver"; + +export const downloadTxtFile = (filename: string, content: string) => { + const blob = new Blob([content], { type: "text/plain;charset=utf-8" }); + FileSaver.saveAs(blob, filename); +}; diff --git a/frontend/src/hooks/api/admin/index.ts b/frontend/src/hooks/api/admin/index.ts index 5405878c7..494426338 100644 --- a/frontend/src/hooks/api/admin/index.ts +++ b/frontend/src/hooks/api/admin/index.ts @@ -1,6 +1,7 @@ export { useAdminDeleteUser, useCreateAdminUser, + useSetupInstanceKmip, useUpdateAdminSlackConfig, useUpdateServerConfig, useUpdateServerEncryptionStrategy @@ -8,6 +9,7 @@ export { export { useAdminGetUsers, useGetAdminSlackConfig, + useGetInstanceKmipConfig, useGetServerConfig, useGetServerRootKmsEncryptionDetails } from "./queries"; diff --git a/frontend/src/hooks/api/admin/mutation.ts b/frontend/src/hooks/api/admin/mutation.ts index c68760fa4..57cc586e7 100644 --- a/frontend/src/hooks/api/admin/mutation.ts +++ b/frontend/src/hooks/api/admin/mutation.ts @@ -10,6 +10,7 @@ import { RootKeyEncryptionStrategy, TCreateAdminUserDTO, TServerConfig, + TSetupInstanceKmipDTO, TUpdateAdminSlackConfigDTO } from "./types"; @@ -98,3 +99,15 @@ export const useUpdateServerEncryptionStrategy = () => { } }); }; + +export const useSetupInstanceKmip = () => { + const queryClient = useQueryClient(); + return useMutation({ + mutationFn: async (payload: TSetupInstanceKmipDTO) => { + await apiRequest.post("/api/v1/admin/kmip", payload); + }, + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: adminQueryKeys.getInstanceKmip() }); + } + }); +}; diff --git a/frontend/src/hooks/api/admin/queries.ts b/frontend/src/hooks/api/admin/queries.ts index 12ae1cf64..4c0676d50 100644 --- a/frontend/src/hooks/api/admin/queries.ts +++ b/frontend/src/hooks/api/admin/queries.ts @@ -6,6 +6,7 @@ import { User } from "../types"; import { AdminGetUsersFilters, AdminSlackConfig, + InstanceKmipConfig, TGetServerRootKmsEncryptionDetails, TServerConfig } from "./types"; @@ -18,7 +19,8 @@ export const adminQueryKeys = { serverConfig: () => ["server-config"] as const, getUsers: (filters: AdminGetUsersFilters) => [adminStandaloneKeys.getUsers, { filters }] as const, getAdminSlackConfig: () => ["admin-slack-config"] as const, - getServerEncryptionStrategies: () => ["server-encryption-strategies"] as const + getServerEncryptionStrategies: () => ["server-encryption-strategies"] as const, + getInstanceKmip: () => ["instance-kmip"] as const }; export const fetchServerConfig = async () => { @@ -93,3 +95,14 @@ export const useGetServerRootKmsEncryptionDetails = () => { } }); }; + +export const useGetInstanceKmipConfig = () => { + return useQuery({ + queryKey: adminQueryKeys.getInstanceKmip(), + queryFn: async () => { + const { data } = await apiRequest.get("/api/v1/admin/kmip"); + + return data; + } + }); +}; diff --git a/frontend/src/hooks/api/admin/types.ts b/frontend/src/hooks/api/admin/types.ts index 60fa3ab98..979f5f445 100644 --- a/frontend/src/hooks/api/admin/types.ts +++ b/frontend/src/hooks/api/admin/types.ts @@ -1,3 +1,5 @@ +import { CertKeyAlgorithm } from "../certificates/enums"; + export enum LoginMethod { EMAIL = "email", GOOGLE = "google", @@ -62,7 +64,15 @@ export type TGetServerRootKmsEncryptionDetails = { }[]; }; +export type InstanceKmipConfig = { + serverCertificateChain: string; +}; + export enum RootKeyEncryptionStrategy { Software = "SOFTWARE", HSM = "HSM" } + +export type TSetupInstanceKmipDTO = { + caKeyAlgorithm: CertKeyAlgorithm; +}; diff --git a/frontend/src/pages/admin/OverviewPage/OverviewPage.tsx b/frontend/src/pages/admin/OverviewPage/OverviewPage.tsx index 67c1bef18..79f5fdca0 100644 --- a/frontend/src/pages/admin/OverviewPage/OverviewPage.tsx +++ b/frontend/src/pages/admin/OverviewPage/OverviewPage.tsx @@ -31,6 +31,7 @@ import { import { AuthPanel } from "./components/AuthPanel"; import { EncryptionPanel } from "./components/EncryptionPanel"; import { IntegrationPanel } from "./components/IntegrationPanel"; +import { KmipPanel } from "./components/KmipPanel"; import { RateLimitPanel } from "./components/RateLimitPanel"; import { UserPanel } from "./components/UserPanel"; @@ -40,7 +41,8 @@ enum TabSections { Auth = "auth", RateLimit = "rate-limit", Integrations = "integrations", - Users = "users" + Users = "users", + Kmip = "kmip" } enum SignUpModes { @@ -149,6 +151,7 @@ export const OverviewPage = () => { Rate Limit Integrations Users + KMIP @@ -347,6 +350,9 @@ export const OverviewPage = () => { + + + )} diff --git a/frontend/src/pages/admin/OverviewPage/components/KmipPanel.tsx b/frontend/src/pages/admin/OverviewPage/components/KmipPanel.tsx new file mode 100644 index 000000000..1c597168f --- /dev/null +++ b/frontend/src/pages/admin/OverviewPage/components/KmipPanel.tsx @@ -0,0 +1,214 @@ +import { Controller, useForm } from "react-hook-form"; +import { faCheck, faCopy, faDownload } from "@fortawesome/free-solid-svg-icons"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { z } from "zod"; + +import { createNotification } from "@app/components/notifications"; +import { + Button, + FormControl, + IconButton, + Modal, + ModalContent, + Select, + SelectItem, + Spinner, + TextArea, + Tooltip +} from "@app/components/v2"; +import { downloadTxtFile } from "@app/helpers/download"; +import { usePopUp, useTimedReset } from "@app/hooks"; +import { useGetInstanceKmipConfig, useSetupInstanceKmip } from "@app/hooks/api"; +import { InstanceKmipConfig } from "@app/hooks/api/admin/types"; +import { certKeyAlgorithms } from "@app/hooks/api/certificates/constants"; +import { CertKeyAlgorithm } from "@app/hooks/api/certificates/enums"; + +const kmipInstanceConfigFormSchema = z.object({ + caKeyAlgorithm: z.nativeEnum(CertKeyAlgorithm) +}); + +type TKmipInstanceConfigForm = z.infer; + +const KmipInstanceConfigSection = ({ + kmipConfig, + isKmipConfigLoading +}: { + kmipConfig?: InstanceKmipConfig; + isKmipConfigLoading: boolean; +}) => { + const { popUp, handlePopUpToggle, handlePopUpClose, handlePopUpOpen } = usePopUp([ + "configureKmip" + ] as const); + const { + handleSubmit, + control, + formState: { isSubmitting } + } = useForm({ + resolver: zodResolver(kmipInstanceConfigFormSchema) + }); + const { mutateAsync: setupInstanceKmip } = useSetupInstanceKmip(); + + const onFormSubmit = async (formData: TKmipInstanceConfigForm) => { + await setupInstanceKmip(formData); + + createNotification({ + type: "success", + text: "Successfully configured KMIP" + }); + + handlePopUpClose("configureKmip"); + }; + const [copyTextCertificate, isCopyingCertificate, setCopyTextCertificate] = useTimedReset( + { + initialState: "Copy to clipboard" + } + ); + + return ( + <> +
+
KMIP configuration
+ {isKmipConfigLoading && ( +
+ +
+ )} + {!isKmipConfigLoading && kmipConfig && ( +
+
KMIP CA Certificate for Clients
+
+ This certificate chain should be used by KMIP clients to verify the identity of the + KMIP servers and establish a secure TLS connection for encrypted communication. +
+
+
+ + { + navigator.clipboard.writeText(kmipConfig.serverCertificateChain); + setCopyTextCertificate("Copied"); + }} + > + + + + + { + downloadTxtFile("ca-chain.pem", kmipConfig.serverCertificateChain); + }} + > + + + +
+
+ +