diff --git a/backend/Dockerfile b/backend/Dockerfile index 0edfdfb84..b9edf8b98 100644 --- a/backend/Dockerfile +++ b/backend/Dockerfile @@ -8,7 +8,8 @@ RUN apt-get update && apt-get install -y \ python3 \ make \ g++ \ - openssh-client + openssh-client \ + openssl # Install dependencies for TDS driver (required for SAP ASE dynamic secrets) RUN apt-get install -y \ diff --git a/backend/Dockerfile.dev b/backend/Dockerfile.dev index adb5157f5..3435672e7 100644 --- a/backend/Dockerfile.dev +++ b/backend/Dockerfile.dev @@ -19,6 +19,7 @@ RUN apt-get update && apt-get install -y \ make \ g++ \ openssh-client \ + openssl \ curl \ pkg-config diff --git a/backend/package-lock.json b/backend/package-lock.json index 47e9014ce..3cd03cd6e 100644 --- a/backend/package-lock.json +++ b/backend/package-lock.json @@ -132,7 +132,7 @@ "@types/jsrp": "^0.2.6", "@types/libsodium-wrappers": "^0.7.13", "@types/lodash.isequal": "^4.5.8", - "@types/node": "^20.9.5", + "@types/node": "^20.17.30", "@types/nodemailer": "^6.4.14", "@types/passport-github": "^1.1.12", "@types/passport-google-oauth20": "^2.0.14", @@ -9753,11 +9753,12 @@ "license": "MIT" }, "node_modules/@types/node": { - "version": "20.9.5", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.9.5.tgz", - "integrity": "sha512-Uq2xbNq0chGg+/WQEU0LJTSs/1nKxz6u1iemLcGomkSnKokbW1fbLqc3HOqCf2JP7KjlL4QkS7oZZTrOQHQYgQ==", + "version": "20.17.30", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.17.30.tgz", + "integrity": "sha512-7zf4YyHA+jvBNfVrk2Gtvs6x7E8V+YDW05bNfG2XkWDJfYRXrTiP/DsB2zSYTaHX0bGIujTBQdMVAhb+j7mwpg==", + "license": "MIT", "dependencies": { - "undici-types": "~5.26.4" + "undici-types": "~6.19.2" } }, "node_modules/@types/node-fetch": { @@ -20081,11 +20082,6 @@ "undici-types": "~6.19.2" } }, - "node_modules/scim-patch/node_modules/undici-types": { - "version": "6.19.8", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz", - "integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==" - }, "node_modules/scim2-parse-filter": { "version": "0.2.10", "resolved": "https://registry.npmjs.org/scim2-parse-filter/-/scim2-parse-filter-0.2.10.tgz", @@ -22442,9 +22438,9 @@ } }, "node_modules/undici-types": { - "version": "5.26.5", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", - "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==" + "version": "6.19.8", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz", + "integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==" }, "node_modules/unicode-canonical-property-names-ecmascript": { "version": "2.0.0", diff --git a/backend/package.json b/backend/package.json index c575722fd..66eddcc10 100644 --- a/backend/package.json +++ b/backend/package.json @@ -89,7 +89,7 @@ "@types/jsrp": "^0.2.6", "@types/libsodium-wrappers": "^0.7.13", "@types/lodash.isequal": "^4.5.8", - "@types/node": "^20.9.5", + "@types/node": "^20.17.30", "@types/nodemailer": "^6.4.14", "@types/passport-github": "^1.1.12", "@types/passport-google-oauth20": "^2.0.14", diff --git a/backend/src/db/migrations/20250402000941_add-type-to-kms-keys.ts b/backend/src/db/migrations/20250402000941_add-type-to-kms-keys.ts new file mode 100644 index 000000000..fd99938db --- /dev/null +++ b/backend/src/db/migrations/20250402000941_add-type-to-kms-keys.ts @@ -0,0 +1,19 @@ +import { Knex } from "knex"; + +import { KmsKeyUsage } from "@app/services/kms/kms-types"; + +import { TableName } from "../schemas"; + +export async function up(knex: Knex): Promise { + const hasTypeColumn = await knex.schema.hasColumn(TableName.KmsKey, "type"); + + await knex.schema.alterTable(TableName.KmsKey, (t) => { + if (!hasTypeColumn) t.string("keyUsage").notNullable().defaultTo(KmsKeyUsage.ENCRYPT_DECRYPT); + }); +} + +export async function down(knex: Knex): Promise { + await knex.schema.alterTable(TableName.KmsKey, (t) => { + t.dropColumn("keyUsage"); + }); +} diff --git a/backend/src/db/schemas/kms-keys.ts b/backend/src/db/schemas/kms-keys.ts index b56fab7bf..ccb779d57 100644 --- a/backend/src/db/schemas/kms-keys.ts +++ b/backend/src/db/schemas/kms-keys.ts @@ -16,7 +16,8 @@ export const KmsKeysSchema = z.object({ name: z.string(), createdAt: z.date(), updatedAt: z.date(), - projectId: z.string().nullable().optional() + projectId: z.string().nullable().optional(), + keyUsage: z.string().default("encrypt-decrypt") }); export type TKmsKeys = z.infer; diff --git a/backend/src/ee/routes/v1/kmip-spec-router.ts b/backend/src/ee/routes/v1/kmip-spec-router.ts index c9899c98e..9a1f4902c 100644 --- a/backend/src/ee/routes/v1/kmip-spec-router.ts +++ b/backend/src/ee/routes/v1/kmip-spec-router.ts @@ -2,7 +2,7 @@ import z from "zod"; import { KmsKeysSchema } from "@app/db/schemas"; import { EventType } from "@app/ee/services/audit-log/audit-log-types"; -import { SymmetricEncryption } from "@app/lib/crypto/cipher"; +import { SymmetricKeyAlgorithm } from "@app/lib/crypto/cipher"; import { ForbiddenRequestError, NotFoundError } from "@app/lib/errors"; import { writeLimit } from "@app/server/config/rateLimiter"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; @@ -74,7 +74,7 @@ export const registerKmipSpecRouter = async (server: FastifyZodProvider) => { schema: { description: "KMIP endpoint for creating managed objects", body: z.object({ - algorithm: z.nativeEnum(SymmetricEncryption) + algorithm: z.nativeEnum(SymmetricKeyAlgorithm) }), response: { 200: KmsKeysSchema @@ -433,7 +433,7 @@ export const registerKmipSpecRouter = async (server: FastifyZodProvider) => { body: z.object({ key: z.string(), name: z.string(), - algorithm: z.nativeEnum(SymmetricEncryption) + algorithm: z.nativeEnum(SymmetricKeyAlgorithm) }), response: { 200: z.object({ 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 996a90555..91464bc0b 100644 --- a/backend/src/ee/services/audit-log/audit-log-types.ts +++ b/backend/src/ee/services/audit-log/audit-log-types.ts @@ -12,7 +12,8 @@ import { import { SshCaStatus, SshCertType } from "@app/ee/services/ssh/ssh-certificate-authority-types"; import { SshCertKeyAlgorithm } from "@app/ee/services/ssh-certificate/ssh-certificate-types"; import { SshCertTemplateStatus } from "@app/ee/services/ssh-certificate-template/ssh-certificate-template-types"; -import { SymmetricEncryption } from "@app/lib/crypto/cipher"; +import { SymmetricKeyAlgorithm } from "@app/lib/crypto/cipher"; +import { AsymmetricKeyAlgorithm, SigningAlgorithm } from "@app/lib/crypto/sign/types"; import { TProjectPermission } from "@app/lib/types"; import { AppConnection } from "@app/services/app-connection/app-connection-enums"; import { TCreateAppConnectionDTO, TUpdateAppConnectionDTO } from "@app/services/app-connection/app-connection-types"; @@ -255,6 +256,11 @@ export enum EventType { GET_CMEK = "get-cmek", CMEK_ENCRYPT = "cmek-encrypt", CMEK_DECRYPT = "cmek-decrypt", + CMEK_SIGN = "cmek-sign", + CMEK_VERIFY = "cmek-verify", + CMEK_LIST_SIGNING_ALGORITHMS = "cmek-list-signing-algorithms", + CMEK_GET_PUBLIC_KEY = "cmek-get-public-key", + UPDATE_EXTERNAL_GROUP_ORG_ROLE_MAPPINGS = "update-external-group-org-role-mapping", GET_EXTERNAL_GROUP_ORG_ROLE_MAPPINGS = "get-external-group-org-role-mapping", GET_PROJECT_TEMPLATES = "get-project-templates", @@ -1997,7 +2003,7 @@ interface CreateCmekEvent { keyId: string; name: string; description?: string; - encryptionAlgorithm: SymmetricEncryption; + encryptionAlgorithm: SymmetricKeyAlgorithm | AsymmetricKeyAlgorithm; }; } @@ -2045,6 +2051,39 @@ interface CmekDecryptEvent { }; } +interface CmekSignEvent { + type: EventType.CMEK_SIGN; + metadata: { + keyId: string; + signingAlgorithm: SigningAlgorithm; + signature: string; + }; +} + +interface CmekVerifyEvent { + type: EventType.CMEK_VERIFY; + metadata: { + keyId: string; + signingAlgorithm: SigningAlgorithm; + signature: string; + signatureValid: boolean; + }; +} + +interface CmekListSigningAlgorithmsEvent { + type: EventType.CMEK_LIST_SIGNING_ALGORITHMS; + metadata: { + keyId: string; + }; +} + +interface CmekGetPublicKeyEvent { + type: EventType.CMEK_GET_PUBLIC_KEY; + metadata: { + keyId: string; + }; +} + interface GetExternalGroupOrgRoleMappingsEvent { type: EventType.GET_EXTERNAL_GROUP_ORG_ROLE_MAPPINGS; metadata?: Record; // not needed, based off orgId @@ -2639,6 +2678,10 @@ export type Event = | GetCmeksEvent | CmekEncryptEvent | CmekDecryptEvent + | CmekSignEvent + | CmekVerifyEvent + | CmekListSigningAlgorithmsEvent + | CmekGetPublicKeyEvent | GetExternalGroupOrgRoleMappingsEvent | UpdateExternalGroupOrgRoleMappingsEvent | GetProjectTemplatesEvent diff --git a/backend/src/ee/services/external-kms/external-kms-service.ts b/backend/src/ee/services/external-kms/external-kms-service.ts index faaace343..49ac293ed 100644 --- a/backend/src/ee/services/external-kms/external-kms-service.ts +++ b/backend/src/ee/services/external-kms/external-kms-service.ts @@ -7,7 +7,7 @@ import { BadRequestError, InternalServerError, NotFoundError } from "@app/lib/er import { alphaNumericNanoId } from "@app/lib/nanoid"; import { TKmsKeyDALFactory } from "@app/services/kms/kms-key-dal"; import { TKmsServiceFactory } from "@app/services/kms/kms-service"; -import { KmsDataKey } from "@app/services/kms/kms-types"; +import { KmsDataKey, KmsKeyUsage } from "@app/services/kms/kms-types"; import { TLicenseServiceFactory } from "../license/license-service"; import { OrgPermissionActions, OrgPermissionSubjects } from "../permission/org-permission"; @@ -115,6 +115,7 @@ export const externalKmsServiceFactory = ({ { isReserved: false, description, + keyUsage: KmsKeyUsage.ENCRYPT_DECRYPT, name: kmsName, orgId: actorOrgId }, diff --git a/backend/src/ee/services/external-kms/providers/gcp-kms.ts b/backend/src/ee/services/external-kms/providers/gcp-kms.ts index b3b61694b..bee1eb24b 100644 --- a/backend/src/ee/services/external-kms/providers/gcp-kms.ts +++ b/backend/src/ee/services/external-kms/providers/gcp-kms.ts @@ -92,7 +92,7 @@ export const GcpKmsProviderFactory = async ({ inputs }: GcpKmsProviderArgs): Pro plaintext: data }); if (!encryptedText[0].ciphertext) throw new Error("encryption failed"); - return { encryptedBlob: Buffer.from(encryptedText[0].ciphertext) }; + return { encryptedBlob: Buffer.from(encryptedText[0].ciphertext as Uint8Array) }; }; const decrypt = async (encryptedBlob: Buffer) => { @@ -101,7 +101,7 @@ export const GcpKmsProviderFactory = async ({ inputs }: GcpKmsProviderArgs): Pro ciphertext: encryptedBlob }); if (!decryptedText[0].plaintext) throw new Error("decryption failed"); - return { data: Buffer.from(decryptedText[0].plaintext) }; + return { data: Buffer.from(decryptedText[0].plaintext as Uint8Array) }; }; return { diff --git a/backend/src/ee/services/hsm/hsm-service.ts b/backend/src/ee/services/hsm/hsm-service.ts index d35d17a24..0ed4c5faf 100644 --- a/backend/src/ee/services/hsm/hsm-service.ts +++ b/backend/src/ee/services/hsm/hsm-service.ts @@ -258,7 +258,7 @@ export const hsmServiceFactory = ({ hsmModule: { isInitialized, pkcs11 }, envCon const decrypt: { (encryptedBlob: Buffer, providedSession: pkcs11js.Handle): Promise; (encryptedBlob: Buffer): Promise; - } = async (encryptedBlob: Buffer, providedSession?: pkcs11js.Handle) => { + } = async (encryptedBlob: Buffer, providedSession?: pkcs11js.Handle): Promise => { if (!pkcs11 || !isInitialized) { throw new Error("PKCS#11 module is not initialized"); } @@ -309,10 +309,10 @@ export const hsmServiceFactory = ({ hsmModule: { isInitialized, pkcs11 }, envCon pkcs11.C_DecryptInit(sessionHandle, decryptMechanism, aesKey); - const tempBuffer = Buffer.alloc(encryptedData.length); + const tempBuffer: Buffer = Buffer.alloc(encryptedData.length); + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment const decryptedData = pkcs11.C_Decrypt(sessionHandle, encryptedData, tempBuffer); - // Create a new buffer from the decrypted data return Buffer.from(decryptedData); } catch (error) { logger.error(error, "HSM: Failed to perform decryption"); diff --git a/backend/src/ee/services/kmip/kmip-operation-service.ts b/backend/src/ee/services/kmip/kmip-operation-service.ts index 66c3a1d46..45f201498 100644 --- a/backend/src/ee/services/kmip/kmip-operation-service.ts +++ b/backend/src/ee/services/kmip/kmip-operation-service.ts @@ -3,6 +3,7 @@ import { ForbiddenError } from "@casl/ability"; 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 { KmsKeyUsage } from "@app/services/kms/kms-types"; import { TProjectDALFactory } from "@app/services/project/project-dal"; import { OrgPermissionKmipActions, OrgPermissionSubjects } from "../permission/org-permission"; @@ -403,6 +404,7 @@ export const kmipOperationServiceFactory = ({ algorithm, isReserved: false, projectId, + keyUsage: KmsKeyUsage.ENCRYPT_DECRYPT, orgId: project.orgId }); diff --git a/backend/src/ee/services/kmip/kmip-types.ts b/backend/src/ee/services/kmip/kmip-types.ts index a259a79b8..81d0d8766 100644 --- a/backend/src/ee/services/kmip/kmip-types.ts +++ b/backend/src/ee/services/kmip/kmip-types.ts @@ -1,4 +1,4 @@ -import { SymmetricEncryption } from "@app/lib/crypto/cipher"; +import { SymmetricKeyAlgorithm } from "@app/lib/crypto/cipher"; import { OrderByDirection, TOrgPermission, TProjectPermission } from "@app/lib/types"; import { CertKeyAlgorithm } from "@app/services/certificate/certificate-types"; @@ -49,7 +49,7 @@ type KmipOperationBaseDTO = { } & Omit; export type TKmipCreateDTO = { - algorithm: SymmetricEncryption; + algorithm: SymmetricKeyAlgorithm; } & KmipOperationBaseDTO; export type TKmipGetDTO = { @@ -77,7 +77,7 @@ export type TKmipLocateDTO = KmipOperationBaseDTO; export type TKmipRegisterDTO = { name: string; key: string; - algorithm: SymmetricEncryption; + algorithm: SymmetricKeyAlgorithm; } & KmipOperationBaseDTO; export type TSetupOrgKmipDTO = { diff --git a/backend/src/ee/services/permission/project-permission.ts b/backend/src/ee/services/permission/project-permission.ts index 153401900..c14bbb518 100644 --- a/backend/src/ee/services/permission/project-permission.ts +++ b/backend/src/ee/services/permission/project-permission.ts @@ -32,7 +32,9 @@ export enum ProjectPermissionCmekActions { Edit = "edit", Delete = "delete", Encrypt = "encrypt", - Decrypt = "decrypt" + Decrypt = "decrypt", + Sign = "sign", + Verify = "verify" } export enum ProjectPermissionDynamicSecretActions { @@ -732,7 +734,9 @@ const buildAdminPermissionRules = () => { ProjectPermissionCmekActions.Delete, ProjectPermissionCmekActions.Read, ProjectPermissionCmekActions.Encrypt, - ProjectPermissionCmekActions.Decrypt + ProjectPermissionCmekActions.Decrypt, + ProjectPermissionCmekActions.Sign, + ProjectPermissionCmekActions.Verify ], ProjectPermissionSub.Cmek ); @@ -935,7 +939,9 @@ const buildMemberPermissionRules = () => { ProjectPermissionCmekActions.Delete, ProjectPermissionCmekActions.Read, ProjectPermissionCmekActions.Encrypt, - ProjectPermissionCmekActions.Decrypt + ProjectPermissionCmekActions.Decrypt, + ProjectPermissionCmekActions.Sign, + ProjectPermissionCmekActions.Verify ], ProjectPermissionSub.Cmek ); diff --git a/backend/src/lib/api-docs/constants.ts b/backend/src/lib/api-docs/constants.ts index 8e390d9a3..7c0f47efe 100644 --- a/backend/src/lib/api-docs/constants.ts +++ b/backend/src/lib/api-docs/constants.ts @@ -1672,7 +1672,8 @@ export const KMS = { projectId: "The ID of the project to create the key in.", name: "The name of the key to be created. Must be slug-friendly.", description: "An optional description of the key.", - encryptionAlgorithm: "The algorithm to use when performing cryptographic operations with the key." + encryptionAlgorithm: "The algorithm to use when performing cryptographic operations with the key.", + type: "The type of key to be created, either encrypt-decrypt or sign-verify, based on your intended use for the key." }, UPDATE_KEY: { keyId: "The ID of the key to be updated.", @@ -1705,6 +1706,28 @@ export const KMS = { DECRYPT: { keyId: "The ID of the key to decrypt the data with.", ciphertext: "The ciphertext to be decrypted (base64 encoded)." + }, + + LIST_SIGNING_ALGORITHMS: { + keyId: "The ID of the key to list the signing algorithms for. The key must be for signing and verifying." + }, + + GET_PUBLIC_KEY: { + keyId: "The ID of the key to get the public key for. The key must be for signing and verifying." + }, + + SIGN: { + keyId: "The ID of the key to sign the data with.", + data: "The data in string format to be signed (base64 encoded).", + isDigest: + "Whether the data is already digested or not. Please be aware that if you are passing a digest the algorithm used to create the digest must match the signing algorithm used to sign the digest.", + signingAlgorithm: "The algorithm to use when performing cryptographic operations with the key." + }, + VERIFY: { + keyId: "The ID of the key to verify the data with.", + data: "The data in string format to be verified (base64 encoded). For data larger than 4096 bytes you must first create a digest of the data and then pass the digest in the data parameter.", + signature: "The signature to be verified (base64 encoded).", + isDigest: "Whether the data is already digested or not." } }; diff --git a/backend/src/lib/crypto/cipher/cipher.ts b/backend/src/lib/crypto/cipher/cipher.ts index 7bc16b470..718c8ad5e 100644 --- a/backend/src/lib/crypto/cipher/cipher.ts +++ b/backend/src/lib/crypto/cipher/cipher.ts @@ -1,6 +1,6 @@ import crypto from "crypto"; -import { SymmetricEncryption, TSymmetricEncryptionFns } from "./types"; +import { SymmetricKeyAlgorithm, TSymmetricEncryptionFns } from "./types"; const getIvLength = () => { return 12; @@ -10,7 +10,9 @@ const getTagLength = () => { return 16; }; -export const symmetricCipherService = (type: SymmetricEncryption): TSymmetricEncryptionFns => { +export const symmetricCipherService = ( + type: SymmetricKeyAlgorithm.AES_GCM_128 | SymmetricKeyAlgorithm.AES_GCM_256 +): TSymmetricEncryptionFns => { const IV_LENGTH = getIvLength(); const TAG_LENGTH = getTagLength(); diff --git a/backend/src/lib/crypto/cipher/index.ts b/backend/src/lib/crypto/cipher/index.ts index 41dbcf639..27373a009 100644 --- a/backend/src/lib/crypto/cipher/index.ts +++ b/backend/src/lib/crypto/cipher/index.ts @@ -1,2 +1,2 @@ export { symmetricCipherService } from "./cipher"; -export { SymmetricEncryption } from "./types"; +export { AllowedEncryptionKeyAlgorithms, SymmetricKeyAlgorithm } from "./types"; diff --git a/backend/src/lib/crypto/cipher/types.ts b/backend/src/lib/crypto/cipher/types.ts index f490d6a66..e2f63ce5e 100644 --- a/backend/src/lib/crypto/cipher/types.ts +++ b/backend/src/lib/crypto/cipher/types.ts @@ -1,7 +1,18 @@ -export enum SymmetricEncryption { +import { z } from "zod"; + +import { AsymmetricKeyAlgorithm } from "../sign/types"; + +// Supported symmetric encrypt/decrypt algorithms +export enum SymmetricKeyAlgorithm { AES_GCM_256 = "aes-256-gcm", AES_GCM_128 = "aes-128-gcm" } +export const SymmetricKeyAlgorithmEnum = z.enum(Object.values(SymmetricKeyAlgorithm) as [string, ...string[]]).options; + +export const AllowedEncryptionKeyAlgorithms = z.enum([ + ...Object.values(SymmetricKeyAlgorithm), + ...Object.values(AsymmetricKeyAlgorithm) +] as [string, ...string[]]).options; export type TSymmetricEncryptionFns = { encrypt: (text: Buffer, key: Buffer) => Buffer; diff --git a/backend/src/lib/crypto/sign/index.ts b/backend/src/lib/crypto/sign/index.ts new file mode 100644 index 000000000..5680cd27a --- /dev/null +++ b/backend/src/lib/crypto/sign/index.ts @@ -0,0 +1,2 @@ +export { signingService } from "./signing"; +export { AsymmetricKeyAlgorithm, SigningAlgorithm } from "./types"; diff --git a/backend/src/lib/crypto/sign/signing.ts b/backend/src/lib/crypto/sign/signing.ts new file mode 100644 index 000000000..7dd71b5f6 --- /dev/null +++ b/backend/src/lib/crypto/sign/signing.ts @@ -0,0 +1,539 @@ +import { execFile } from "child_process"; +import crypto from "crypto"; +import fs from "fs/promises"; +import path from "path"; +import { promisify } from "util"; + +import { BadRequestError } from "@app/lib/errors"; +import { cleanTemporaryDirectory, createTemporaryDirectory, writeToTemporaryFile } from "@app/lib/files"; +import { logger } from "@app/lib/logger"; + +import { AsymmetricKeyAlgorithm, SigningAlgorithm, TAsymmetricSignVerifyFns } from "./types"; + +const execFileAsync = promisify(execFile); + +interface SigningParams { + hashAlgorithm: SupportedHashAlgorithm; + padding?: number; + saltLength?: number; +} + +enum SupportedHashAlgorithm { + SHA256 = "sha256", + SHA384 = "sha384", + SHA512 = "sha512" +} + +const COMMAND_TIMEOUT = 15_000; + +const SHA256_DIGEST_LENGTH = 32; +const SHA384_DIGEST_LENGTH = 48; +const SHA512_DIGEST_LENGTH = 64; + +/** + * Service for cryptographic signing and verification operations using asymmetric keys + * + * @param algorithm The key algorithm itself. The signing algorithm is supplied in the individual sign/verify functions. + * @returns Object with sign and verify functions + */ +export const signingService = (algorithm: AsymmetricKeyAlgorithm): TAsymmetricSignVerifyFns => { + const $getSigningParams = (signingAlgorithm: SigningAlgorithm): SigningParams => { + switch (signingAlgorithm) { + // RSA PSS + case SigningAlgorithm.RSASSA_PSS_SHA_512: + return { + hashAlgorithm: SupportedHashAlgorithm.SHA512, + padding: crypto.constants.RSA_PKCS1_PSS_PADDING, + saltLength: SHA512_DIGEST_LENGTH + }; + case SigningAlgorithm.RSASSA_PSS_SHA_256: + return { + hashAlgorithm: SupportedHashAlgorithm.SHA256, + padding: crypto.constants.RSA_PKCS1_PSS_PADDING, + saltLength: SHA256_DIGEST_LENGTH + }; + case SigningAlgorithm.RSASSA_PSS_SHA_384: + return { + hashAlgorithm: SupportedHashAlgorithm.SHA384, + padding: crypto.constants.RSA_PKCS1_PSS_PADDING, + saltLength: SHA384_DIGEST_LENGTH + }; + + // RSA PKCS#1 v1.5 + case SigningAlgorithm.RSASSA_PKCS1_V1_5_SHA_512: + return { + hashAlgorithm: SupportedHashAlgorithm.SHA512, + padding: crypto.constants.RSA_PKCS1_PADDING + }; + case SigningAlgorithm.RSASSA_PKCS1_V1_5_SHA_384: + return { + hashAlgorithm: SupportedHashAlgorithm.SHA384, + padding: crypto.constants.RSA_PKCS1_PADDING + }; + case SigningAlgorithm.RSASSA_PKCS1_V1_5_SHA_256: + return { + hashAlgorithm: SupportedHashAlgorithm.SHA256, + padding: crypto.constants.RSA_PKCS1_PADDING + }; + + // ECDSA + case SigningAlgorithm.ECDSA_SHA_256: + return { hashAlgorithm: SupportedHashAlgorithm.SHA256 }; + case SigningAlgorithm.ECDSA_SHA_384: + return { hashAlgorithm: SupportedHashAlgorithm.SHA384 }; + case SigningAlgorithm.ECDSA_SHA_512: + return { hashAlgorithm: SupportedHashAlgorithm.SHA512 }; + + default: + throw new Error(`Unsupported signing algorithm: ${signingAlgorithm as string}`); + } + }; + + const $getEcCurveName = (keyAlgorithm: AsymmetricKeyAlgorithm): { full: string; short: string } => { + // We will support more in the future + switch (keyAlgorithm) { + case AsymmetricKeyAlgorithm.ECC_NIST_P256: + return { + full: "prime256v1", + short: "p256" + }; + default: + throw new Error(`Unsupported EC curve: ${keyAlgorithm}`); + } + }; + + const $validateAlgorithmWithKeyType = (signingAlgorithm: SigningAlgorithm) => { + const isRsaKey = algorithm.startsWith("RSA"); + const isEccKey = algorithm.startsWith("ECC"); + + const isRsaAlgorithm = signingAlgorithm.startsWith("RSASSA"); + const isEccAlgorithm = signingAlgorithm.startsWith("ECDSA"); + + if (isRsaKey && !isRsaAlgorithm) { + throw new BadRequestError({ message: `KMS RSA key cannot be used with ${signingAlgorithm}` }); + } + + if (isEccKey && !isEccAlgorithm) { + throw new BadRequestError({ message: `KMS ECC key cannot be used with ${signingAlgorithm}` }); + } + }; + + const $signRsaDigest = async (digest: Buffer, privateKey: Buffer, hashAlgorithm: SupportedHashAlgorithm) => { + const tempDir = await createTemporaryDirectory("kms-rsa-sign"); + const digestPath = path.join(tempDir, "digest.bin"); + const sigPath = path.join(tempDir, "signature.bin"); + const keyPath = path.join(tempDir, "key.pem"); + + try { + await writeToTemporaryFile(digestPath, digest); + await writeToTemporaryFile(keyPath, privateKey); + + const { stderr } = await execFileAsync( + "openssl", + [ + "pkeyutl", + "-sign", + "-in", + digestPath, + "-inkey", + keyPath, + "-pkeyopt", + `digest:${hashAlgorithm}`, + "-out", + sigPath + ], + { + maxBuffer: 10 * 1024 * 1024, + timeout: COMMAND_TIMEOUT + } + ); + + if (stderr) { + logger.error(stderr, "KMS: Failed to sign RSA digest"); + throw new BadRequestError({ + message: "Failed to sign RSA digest due to signing error" + }); + } + const signature = await fs.readFile(sigPath); + + if (!signature) { + throw new BadRequestError({ + message: + "No signature was created. Make sure you are using an appropriate signing algorithm that uses the same hashing algorithm as the one used to create the digest." + }); + } + + return signature; + } finally { + await cleanTemporaryDirectory(tempDir); + } + }; + + const $signEccDigest = async (digest: Buffer, privateKey: Buffer, hashAlgorithm: SupportedHashAlgorithm) => { + const tempDir = await createTemporaryDirectory("ecc-sign"); + const digestPath = path.join(tempDir, "digest.bin"); + const keyPath = path.join(tempDir, "key.pem"); + const sigPath = path.join(tempDir, "signature.bin"); + + try { + await writeToTemporaryFile(digestPath, digest); + await writeToTemporaryFile(keyPath, privateKey); + + const { stderr } = await execFileAsync( + "openssl", + [ + "pkeyutl", + "-sign", + "-in", + digestPath, + "-inkey", + keyPath, + "-pkeyopt", + `digest:${hashAlgorithm}`, + "-out", + sigPath + ], + { + maxBuffer: 10 * 1024 * 1024, + timeout: COMMAND_TIMEOUT + } + ); + + if (stderr) { + logger.error(stderr, "KMS: Failed to sign ECC digest"); + throw new BadRequestError({ + message: "Failed to sign ECC digest due to signing error" + }); + } + + const signature = await fs.readFile(sigPath); + + if (!signature) { + throw new BadRequestError({ + message: + "No signature was created. Make sure you are using an appropriate signing algorithm that uses the same hashing algorithm as the one used to create the digest." + }); + } + + return signature; + } finally { + await cleanTemporaryDirectory(tempDir); + } + }; + + const $verifyEccDigest = async ( + digest: Buffer, + signature: Buffer, + publicKey: Buffer, + hashAlgorithm: SupportedHashAlgorithm + ) => { + const tempDir = await createTemporaryDirectory("ecc-signature-verification"); + const publicKeyFile = path.join(tempDir, "public-key.pem"); + const sigFile = path.join(tempDir, "signature.sig"); + const digestFile = path.join(tempDir, "digest.bin"); + + try { + await writeToTemporaryFile(publicKeyFile, publicKey); + await writeToTemporaryFile(sigFile, signature); + await writeToTemporaryFile(digestFile, digest); + + await execFileAsync( + "openssl", + [ + "pkeyutl", + "-verify", + "-in", + digestFile, + "-inkey", + publicKeyFile, + "-pubin", // Important for EC public keys + "-sigfile", + sigFile, + "-pkeyopt", + `digest:${hashAlgorithm}` + ], + { timeout: COMMAND_TIMEOUT } + ); + + return true; + } catch (error) { + const err = error as { stderr: string }; + + if ( + !err?.stderr?.toLowerCase()?.includes("signature verification failure") && + !err?.stderr?.toLowerCase()?.includes("bad signature") + ) { + logger.error(error, "KMS: Failed to verify ECC signature"); + } + return false; + } finally { + await cleanTemporaryDirectory(tempDir); + } + }; + + const $verifyRsaDigest = async ( + digest: Buffer, + signature: Buffer, + publicKey: Buffer, + hashAlgorithm: SupportedHashAlgorithm + ) => { + const tempDir = await createTemporaryDirectory("kms-signature-verification"); + const publicKeyFile = path.join(tempDir, "public-key.pub"); + const signatureFile = path.join(tempDir, "signature.sig"); + const digestFile = path.join(tempDir, "digest.bin"); + + try { + await writeToTemporaryFile(publicKeyFile, publicKey); + await writeToTemporaryFile(signatureFile, signature); + await writeToTemporaryFile(digestFile, digest); + + await execFileAsync( + "openssl", + [ + "pkeyutl", + "-verify", + "-in", + digestFile, + "-inkey", + publicKeyFile, + "-pubin", + "-sigfile", + signatureFile, + "-pkeyopt", + `digest:${hashAlgorithm}` + ], + { timeout: COMMAND_TIMEOUT } + ); + + // it'll throw if the verification was not successful + return true; + } catch (error) { + const err = error as { stdout: string }; + + if (!err?.stdout?.toLowerCase()?.includes("signature verification failure")) { + logger.error(error, "KMS: Failed to verify signature"); + } + return false; + } finally { + await cleanTemporaryDirectory(tempDir); + } + }; + + const verifyDigestFunctionsMap: Record< + AsymmetricKeyAlgorithm, + (data: Buffer, signature: Buffer, publicKey: Buffer, hashAlgorithm: SupportedHashAlgorithm) => Promise + > = { + [AsymmetricKeyAlgorithm.ECC_NIST_P256]: $verifyEccDigest, + [AsymmetricKeyAlgorithm.RSA_4096]: $verifyRsaDigest + }; + + const signDigestFunctionsMap: Record< + AsymmetricKeyAlgorithm, + (data: Buffer, privateKey: Buffer, hashAlgorithm: SupportedHashAlgorithm) => Promise + > = { + [AsymmetricKeyAlgorithm.ECC_NIST_P256]: $signEccDigest, + [AsymmetricKeyAlgorithm.RSA_4096]: $signRsaDigest + }; + + const sign = async ( + data: Buffer, + privateKey: Buffer, + signingAlgorithm: SigningAlgorithm, + isDigest: boolean + ): Promise => { + $validateAlgorithmWithKeyType(signingAlgorithm); + + const { hashAlgorithm, padding, saltLength } = $getSigningParams(signingAlgorithm); + + if (isDigest) { + if (signingAlgorithm.startsWith("RSASSA_PSS")) { + throw new BadRequestError({ + message: "RSA PSS does not support digested input" + }); + } + + const signFunction = signDigestFunctionsMap[algorithm]; + + if (!signFunction) { + throw new BadRequestError({ + message: `Digested input is not supported for key algorithm ${algorithm}` + }); + } + + const signature = await signFunction(data, privateKey, hashAlgorithm); + return signature; + } + + const privateKeyObject = crypto.createPrivateKey({ + key: privateKey, + format: "pem", + type: "pkcs8" + }); + + // For RSA signatures + if (signingAlgorithm.startsWith("RSA")) { + const signer = crypto.createSign(hashAlgorithm); + signer.update(data); + + return signer.sign({ + key: privateKeyObject, + padding, + ...(signingAlgorithm.includes("PSS") ? { saltLength } : {}) + }); + } + if (signingAlgorithm.startsWith("ECDSA")) { + // For ECDSA signatures + const signer = crypto.createSign(hashAlgorithm); + signer.update(data); + return signer.sign({ + key: privateKeyObject, + dsaEncoding: "der" + }); + } + throw new BadRequestError({ + message: `Signing algorithm ${signingAlgorithm} not implemented` + }); + }; + + const verify = async ( + data: Buffer, + signature: Buffer, + publicKey: Buffer, + signingAlgorithm: SigningAlgorithm, + isDigest: boolean + ): Promise => { + try { + $validateAlgorithmWithKeyType(signingAlgorithm); + + const { hashAlgorithm, padding, saltLength } = $getSigningParams(signingAlgorithm); + + if (isDigest) { + if (signingAlgorithm.startsWith("RSASSA_PSS")) { + throw new BadRequestError({ + message: "RSA PSS does not support digested input" + }); + } + + const verifyFunction = verifyDigestFunctionsMap[algorithm]; + + if (!verifyFunction) { + throw new BadRequestError({ + message: `Digested input is not supported for key algorithm ${algorithm}` + }); + } + + const signatureValid = await verifyFunction(data, signature, publicKey, hashAlgorithm); + + return signatureValid; + } + + const publicKeyObject = crypto.createPublicKey({ + key: publicKey, + format: "der", + type: "spki" + }); + + // For RSA signatures + if (signingAlgorithm.startsWith("RSA")) { + const verifier = crypto.createVerify(hashAlgorithm); + verifier.update(data); + + return verifier.verify( + { + key: publicKeyObject, + padding, + ...(signingAlgorithm.includes("PSS") ? { saltLength } : {}) + }, + signature + ); + } + // For ECDSA signatures + if (signingAlgorithm.startsWith("ECDSA")) { + const verifier = crypto.createVerify(hashAlgorithm); + verifier.update(data); + return verifier.verify( + { + key: publicKeyObject, + dsaEncoding: "der" + }, + signature + ); + } + throw new BadRequestError({ + message: `Verification for algorithm ${signingAlgorithm} not implemented` + }); + } catch (error) { + if (error instanceof BadRequestError) { + throw error; + } + logger.error(error, "KMS: Failed to verify signature"); + return false; + } + }; + + const generateAsymmetricPrivateKey = async () => { + const { privateKey } = await new Promise<{ privateKey: string }>((resolve, reject) => { + if (algorithm.startsWith("RSA")) { + crypto.generateKeyPair( + "rsa", + { + modulusLength: Number(algorithm.split("_")[1]), + publicKeyEncoding: { type: "spki", format: "pem" }, + privateKeyEncoding: { type: "pkcs8", format: "pem" } + }, + (err, _, pk) => { + if (err) { + reject(err); + } else { + resolve({ privateKey: pk }); + } + } + ); + } else { + const { full: namedCurve } = $getEcCurveName(algorithm); + + crypto.generateKeyPair( + "ec", + { + namedCurve, + publicKeyEncoding: { type: "spki", format: "pem" }, + privateKeyEncoding: { type: "pkcs8", format: "pem" } + }, + (err, _, pk) => { + if (err) { + reject(err); + } else { + resolve({ + privateKey: pk + }); + } + } + ); + } + }); + + return Buffer.from(privateKey); + }; + + const getPublicKeyFromPrivateKey = (privateKey: Buffer) => { + const privateKeyObj = crypto.createPrivateKey({ + key: privateKey, + format: "pem", + type: "pkcs8" + }); + + const publicKey = crypto.createPublicKey(privateKeyObj).export({ + type: "spki", + format: "der" + }); + + return publicKey; + }; + + return { + sign, + verify, + generateAsymmetricPrivateKey, + getPublicKeyFromPrivateKey + }; +}; diff --git a/backend/src/lib/crypto/sign/types.ts b/backend/src/lib/crypto/sign/types.ts new file mode 100644 index 000000000..aa81b4057 --- /dev/null +++ b/backend/src/lib/crypto/sign/types.ts @@ -0,0 +1,45 @@ +import { z } from "zod"; + +export type TAsymmetricSignVerifyFns = { + sign: (data: Buffer, key: Buffer, signingAlgorithm: SigningAlgorithm, isDigest: boolean) => Promise; + verify: ( + data: Buffer, + signature: Buffer, + key: Buffer, + signingAlgorithm: SigningAlgorithm, + isDigest: boolean + ) => Promise; + generateAsymmetricPrivateKey: () => Promise; + getPublicKeyFromPrivateKey: (privateKey: Buffer) => Buffer; +}; + +// Supported asymmetric key types +export enum AsymmetricKeyAlgorithm { + RSA_4096 = "RSA_4096", + ECC_NIST_P256 = "ECC_NIST_P256" +} + +export const AsymmetricKeyAlgorithmEnum = z.enum( + Object.values(AsymmetricKeyAlgorithm) as [string, ...string[]] +).options; + +export enum SigningAlgorithm { + // RSA PSS algorithms + // These are NOT deterministic and include randomness. + // This means that the output signature is different each time for the same input. + RSASSA_PSS_SHA_512 = "RSASSA_PSS_SHA_512", + RSASSA_PSS_SHA_384 = "RSASSA_PSS_SHA_384", + RSASSA_PSS_SHA_256 = "RSASSA_PSS_SHA_256", + + // RSA PKCS#1 v1.5 algorithms + // These are deterministic and the output is the same each time for the same input. + RSASSA_PKCS1_V1_5_SHA_512 = "RSASSA_PKCS1_V1_5_SHA_512", + RSASSA_PKCS1_V1_5_SHA_384 = "RSASSA_PKCS1_V1_5_SHA_384", + RSASSA_PKCS1_V1_5_SHA_256 = "RSASSA_PKCS1_V1_5_SHA_256", + + // ECDSA algorithms + // None of these are deterministic and include randomness like RSA PSS. + ECDSA_SHA_512 = "ECDSA_SHA_512", + ECDSA_SHA_384 = "ECDSA_SHA_384", + ECDSA_SHA_256 = "ECDSA_SHA_256" +} diff --git a/backend/src/lib/files/files.ts b/backend/src/lib/files/files.ts new file mode 100644 index 000000000..063d71d09 --- /dev/null +++ b/backend/src/lib/files/files.ts @@ -0,0 +1,35 @@ +import crypto from "crypto"; +import fs from "fs/promises"; +import os from "os"; +import path from "path"; + +import { logger } from "@app/lib/logger"; + +const baseDir = path.join(os.tmpdir(), "infisical"); +const randomPath = () => `${crypto.randomBytes(32).toString("hex")}`; + +export const createTemporaryDirectory = async (name: string) => { + const tempDirPath = path.join(baseDir, `${name}-${randomPath()}`); + await fs.mkdir(tempDirPath, { recursive: true }); + + return tempDirPath; +}; + +export const removeTemporaryBaseDirectory = async () => { + await fs.rm(baseDir, { force: true, recursive: true }).catch((err) => { + logger.error(err, `Failed to remove temporary base directory [path=${baseDir}]`); + }); +}; + +export const cleanTemporaryDirectory = async (dirPath: string) => { + await fs.rm(dirPath, { recursive: true, force: true }).catch((err) => { + logger.error(err, `Failed to cleanup temporary directory [path=${dirPath}]`); + }); +}; + +export const writeToTemporaryFile = async (tempDirPath: string, data: string | Buffer) => { + await fs.writeFile(tempDirPath, data, { mode: 0o600 }).catch((err) => { + logger.error(err, `Failed to write to temporary file [path=${tempDirPath}]`); + throw err; + }); +}; diff --git a/backend/src/lib/files/index.ts b/backend/src/lib/files/index.ts new file mode 100644 index 000000000..b2cba4b62 --- /dev/null +++ b/backend/src/lib/files/index.ts @@ -0,0 +1 @@ +export * from "./files"; diff --git a/backend/src/main.ts b/backend/src/main.ts index d5c54991b..80d98abcf 100644 --- a/backend/src/main.ts +++ b/backend/src/main.ts @@ -9,6 +9,7 @@ import { runMigrations } from "./auto-start-migrations"; import { initAuditLogDbConnection, initDbConnection } from "./db"; import { keyStoreFactory } from "./keystore/keystore"; import { formatSmtpConfig, initEnvConfig } from "./lib/config/env"; +import { removeTemporaryBaseDirectory } from "./lib/files"; import { initLogger } from "./lib/logger"; import { queueServiceFactory } from "./queue"; import { main } from "./server/app"; @@ -21,6 +22,8 @@ const run = async () => { const logger = initLogger(); const envConfig = initEnvConfig(logger); + await removeTemporaryBaseDirectory(); + const db = initDbConnection({ dbConnectionUri: envConfig.DB_CONNECTION_URI, dbRootCert: envConfig.DB_ROOT_CERT, @@ -71,6 +74,7 @@ const run = async () => { process.on("SIGINT", async () => { await server.close(); await db.destroy(); + await removeTemporaryBaseDirectory(); hsmModule.finalize(); process.exit(0); }); @@ -79,6 +83,7 @@ const run = async () => { process.on("SIGTERM", async () => { await server.close(); await db.destroy(); + await removeTemporaryBaseDirectory(); hsmModule.finalize(); process.exit(0); }); diff --git a/backend/src/server/routes/v1/cmek-router.ts b/backend/src/server/routes/v1/cmek-router.ts index 7aecaee37..64f47f980 100644 --- a/backend/src/server/routes/v1/cmek-router.ts +++ b/backend/src/server/routes/v1/cmek-router.ts @@ -4,13 +4,15 @@ import { InternalKmsSchema, KmsKeysSchema } from "@app/db/schemas"; import { EventType } from "@app/ee/services/audit-log/audit-log-types"; import { KMS } from "@app/lib/api-docs"; import { getBase64SizeInBytes, isBase64 } from "@app/lib/base64"; -import { SymmetricEncryption } from "@app/lib/crypto/cipher"; +import { AllowedEncryptionKeyAlgorithms, SymmetricKeyAlgorithm } from "@app/lib/crypto/cipher"; +import { AsymmetricKeyAlgorithm, SigningAlgorithm } from "@app/lib/crypto/sign"; import { OrderByDirection } from "@app/lib/types"; import { readLimit, writeLimit } from "@app/server/config/rateLimiter"; import { slugSchema } from "@app/server/lib/schemas"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; import { AuthMode } from "@app/services/auth/auth-type"; -import { CmekOrderBy } from "@app/services/cmek/cmek-types"; +import { CmekOrderBy, TCmekKeyEncryptionAlgorithm } from "@app/services/cmek/cmek-types"; +import { KmsKeyUsage } from "@app/services/kms/kms-types"; const keyNameSchema = slugSchema({ min: 1, max: 32, field: "Name" }); const keyDescriptionSchema = z.string().trim().max(500).optional(); @@ -45,16 +47,46 @@ export const registerCmekRouter = async (server: FastifyZodProvider) => { }, schema: { description: "Create KMS key", - body: z.object({ - projectId: z.string().describe(KMS.CREATE_KEY.projectId), - name: keyNameSchema.describe(KMS.CREATE_KEY.name), - description: keyDescriptionSchema.describe(KMS.CREATE_KEY.description), - encryptionAlgorithm: z - .nativeEnum(SymmetricEncryption) - .optional() - .default(SymmetricEncryption.AES_GCM_256) - .describe(KMS.CREATE_KEY.encryptionAlgorithm) // eventually will support others - }), + body: z + .object({ + projectId: z.string().describe(KMS.CREATE_KEY.projectId), + name: keyNameSchema.describe(KMS.CREATE_KEY.name), + description: keyDescriptionSchema.describe(KMS.CREATE_KEY.description), + keyUsage: z + .nativeEnum(KmsKeyUsage) + .optional() + .default(KmsKeyUsage.ENCRYPT_DECRYPT) + .describe(KMS.CREATE_KEY.type), + encryptionAlgorithm: z + .enum(AllowedEncryptionKeyAlgorithms) + .optional() + .default(SymmetricKeyAlgorithm.AES_GCM_256) + .describe(KMS.CREATE_KEY.encryptionAlgorithm) + }) + .superRefine((data, ctx) => { + if ( + data.keyUsage === KmsKeyUsage.ENCRYPT_DECRYPT && + !Object.values(SymmetricKeyAlgorithm).includes(data.encryptionAlgorithm as SymmetricKeyAlgorithm) + ) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: `encryptionAlgorithm must be a valid symmetric encryption algorithm. Valid options are: ${Object.values( + SymmetricKeyAlgorithm + ).join(", ")}` + }); + } + if ( + data.keyUsage === KmsKeyUsage.SIGN_VERIFY && + !Object.values(AsymmetricKeyAlgorithm).includes(data.encryptionAlgorithm as AsymmetricKeyAlgorithm) + ) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: `encryptionAlgorithm must be a valid asymmetric sign-verify algorithm. Valid options are: ${Object.values( + AsymmetricKeyAlgorithm + ).join(", ")}` + }); + } + }), response: { 200: z.object({ key: CmekSchema @@ -64,12 +96,19 @@ export const registerCmekRouter = async (server: FastifyZodProvider) => { onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), handler: async (req) => { const { - body: { projectId, name, description, encryptionAlgorithm }, + body: { projectId, name, description, encryptionAlgorithm, keyUsage }, permission } = req; const cmek = await server.services.cmek.createCmek( - { orgId: permission.orgId, projectId, name, description, encryptionAlgorithm }, + { + orgId: permission.orgId, + projectId, + name, + description, + encryptionAlgorithm: encryptionAlgorithm as TCmekKeyEncryptionAlgorithm, + keyUsage + }, permission ); @@ -82,7 +121,7 @@ export const registerCmekRouter = async (server: FastifyZodProvider) => { keyId: cmek.id, name, description, - encryptionAlgorithm + encryptionAlgorithm: encryptionAlgorithm as TCmekKeyEncryptionAlgorithm } } }); @@ -126,7 +165,7 @@ export const registerCmekRouter = async (server: FastifyZodProvider) => { await server.services.auditLog.createAuditLog({ ...req.auditLogInfo, - orgId: permission.orgId, + projectId: cmek.projectId!, event: { type: EventType.UPDATE_CMEK, metadata: { @@ -169,7 +208,7 @@ export const registerCmekRouter = async (server: FastifyZodProvider) => { await server.services.auditLog.createAuditLog({ ...req.auditLogInfo, - orgId: permission.orgId, + projectId: cmek.projectId!, event: { type: EventType.DELETE_CMEK, metadata: { @@ -282,7 +321,7 @@ export const registerCmekRouter = async (server: FastifyZodProvider) => { rateLimit: readLimit }, schema: { - description: "Get KMS key by Name", + description: "Get KMS key by name", params: z.object({ keyName: slugSchema({ field: "Key name" }).describe(KMS.GET_KEY_BY_NAME.keyName) }), @@ -349,11 +388,11 @@ export const registerCmekRouter = async (server: FastifyZodProvider) => { permission } = req; - const ciphertext = await server.services.cmek.cmekEncrypt({ keyId, plaintext }, permission); + const { ciphertext, projectId } = await server.services.cmek.cmekEncrypt({ keyId, plaintext }, permission); await server.services.auditLog.createAuditLog({ ...req.auditLogInfo, - orgId: permission.orgId, + projectId, event: { type: EventType.CMEK_ENCRYPT, metadata: { @@ -366,6 +405,198 @@ export const registerCmekRouter = async (server: FastifyZodProvider) => { } }); + server.route({ + method: "GET", + url: "/keys/:keyId/public-key", + config: { + rateLimit: readLimit + }, + schema: { + description: + "Get the public key for a KMS key that is used for signing and verifying data. This endpoint is only available for asymmetric keys.", + params: z.object({ + keyId: z.string().uuid().describe(KMS.GET_PUBLIC_KEY.keyId) + }), + response: { + 200: z.object({ + publicKey: z.string() + }) + } + }, + onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), + handler: async (req) => { + const { + params: { keyId }, + permission + } = req; + + const { publicKey, projectId } = await server.services.cmek.getPublicKey({ keyId }, permission); + + await server.services.auditLog.createAuditLog({ + ...req.auditLogInfo, + projectId, + event: { + type: EventType.CMEK_GET_PUBLIC_KEY, + metadata: { + keyId + } + } + }); + + return { publicKey }; + } + }); + + server.route({ + method: "GET", + url: "/keys/:keyId/signing-algorithms", + config: { + rateLimit: readLimit + }, + schema: { + description: "List all available signing algorithms for a KMS key", + params: z.object({ + keyId: z.string().uuid().describe(KMS.LIST_SIGNING_ALGORITHMS.keyId) + }), + response: { + 200: z.object({ + signingAlgorithms: z.array(z.nativeEnum(SigningAlgorithm)) + }) + } + }, + onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), + handler: async (req) => { + const { keyId } = req.params; + + const { signingAlgorithms, projectId } = await server.services.cmek.listSigningAlgorithms( + { keyId }, + req.permission + ); + + await server.services.auditLog.createAuditLog({ + ...req.auditLogInfo, + projectId, + event: { + type: EventType.CMEK_LIST_SIGNING_ALGORITHMS, + metadata: { + keyId + } + } + }); + + return { signingAlgorithms }; + } + }); + + server.route({ + method: "POST", + url: "/keys/:keyId/sign", + config: { + rateLimit: writeLimit + }, + schema: { + description: "Sign data with a KMS key.", + params: z.object({ + keyId: z.string().uuid().describe(KMS.SIGN.keyId) + }), + body: z.object({ + signingAlgorithm: z.nativeEnum(SigningAlgorithm), + isDigest: z.boolean().optional().default(false).describe(KMS.SIGN.isDigest), + data: base64Schema.describe(KMS.SIGN.data) + }), + response: { + 200: z.object({ + signature: z.string(), + keyId: z.string().uuid(), + signingAlgorithm: z.nativeEnum(SigningAlgorithm) + }) + } + }, + onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), + handler: async (req) => { + const { + params: { keyId: inputKeyId }, + body: { data, signingAlgorithm, isDigest }, + permission + } = req; + + const { projectId, ...result } = await server.services.cmek.cmekSign( + { keyId: inputKeyId, data, signingAlgorithm, isDigest }, + permission + ); + + await server.services.auditLog.createAuditLog({ + ...req.auditLogInfo, + projectId, + event: { + type: EventType.CMEK_SIGN, + metadata: { + keyId: inputKeyId, + signingAlgorithm, + signature: result.signature + } + } + }); + return result; + } + }); + + server.route({ + method: "POST", + url: "/keys/:keyId/verify", + config: { + rateLimit: writeLimit + }, + schema: { + description: "Verify data signatures with a KMS key.", + params: z.object({ + keyId: z.string().uuid().describe(KMS.VERIFY.keyId) + }), + body: z.object({ + isDigest: z.boolean().optional().default(false).describe(KMS.VERIFY.isDigest), + data: base64Schema.describe(KMS.VERIFY.data), + signature: base64Schema.describe(KMS.VERIFY.signature), + signingAlgorithm: z.nativeEnum(SigningAlgorithm) + }), + response: { + 200: z.object({ + signatureValid: z.boolean(), + keyId: z.string().uuid(), + signingAlgorithm: z.nativeEnum(SigningAlgorithm) + }) + } + }, + onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), + handler: async (req) => { + const { + params: { keyId }, + body: { data, signature, signingAlgorithm, isDigest }, + permission + } = req; + + const { projectId, ...result } = await server.services.cmek.cmekVerify( + { keyId, data, signature, signingAlgorithm, isDigest }, + permission + ); + + await server.services.auditLog.createAuditLog({ + ...req.auditLogInfo, + projectId, + event: { + type: EventType.CMEK_VERIFY, + metadata: { + keyId, + signatureValid: result.signatureValid, + signingAlgorithm, + signature + } + } + }); + + return result; + } + }); + server.route({ method: "POST", url: "/keys/:keyId/decrypt", @@ -394,11 +625,11 @@ export const registerCmekRouter = async (server: FastifyZodProvider) => { permission } = req; - const plaintext = await server.services.cmek.cmekDecrypt({ keyId, ciphertext }, permission); + const { plaintext, projectId } = await server.services.cmek.cmekDecrypt({ keyId, ciphertext }, permission); await server.services.auditLog.createAuditLog({ ...req.auditLogInfo, - orgId: permission.orgId, + projectId, event: { type: EventType.CMEK_DECRYPT, metadata: { diff --git a/backend/src/services/cmek/cmek-service.ts b/backend/src/services/cmek/cmek-service.ts index 5e74a5bac..b968a8951 100644 --- a/backend/src/services/cmek/cmek-service.ts +++ b/backend/src/services/cmek/cmek-service.ts @@ -3,12 +3,18 @@ import { ForbiddenError } from "@casl/ability"; import { ActionProjectType, ProjectType } from "@app/db/schemas"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service"; import { ProjectPermissionCmekActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission"; +import { SigningAlgorithm } from "@app/lib/crypto/sign"; import { DatabaseErrorCode } from "@app/lib/error-codes"; import { BadRequestError, DatabaseError, NotFoundError } from "@app/lib/errors"; import { OrgServiceActor } from "@app/lib/types"; import { TCmekDecryptDTO, TCmekEncryptDTO, + TCmekGetPublicKeyDTO, + TCmekKeyEncryptionAlgorithm, + TCmekListSigningAlgorithmsDTO, + TCmekSignDTO, + TCmekVerifyDTO, TCreateCmekDTO, TListCmeksByProjectIdDTO, TUpdabteCmekByIdDTO @@ -16,6 +22,7 @@ import { import { TKmsKeyDALFactory } from "@app/services/kms/kms-key-dal"; import { TKmsServiceFactory } from "@app/services/kms/kms-service"; +import { KmsKeyUsage } from "../kms/kms-types"; import { TProjectDALFactory } from "../project/project-dal"; type TCmekServiceFactoryDep = { @@ -221,7 +228,151 @@ export const cmekServiceFactory = ({ kmsService, kmsDAL, permissionService, proj const { cipherTextBlob } = await encrypt({ plainText: Buffer.from(plaintext, "base64") }); - return cipherTextBlob.toString("base64"); + return { + ciphertext: cipherTextBlob.toString("base64"), + projectId: key.projectId + }; + }; + + const listSigningAlgorithms = async ({ keyId }: TCmekListSigningAlgorithmsDTO, actor: OrgServiceActor) => { + const key = await kmsDAL.findCmekById(keyId); + + if (!key) throw new NotFoundError({ message: `Key with ID "${keyId}" not found` }); + if (!key.projectId || key.isReserved) throw new BadRequestError({ message: "Key is not customer managed" }); + if (key.isDisabled) throw new BadRequestError({ message: "Key is disabled" }); + + const { permission } = await permissionService.getProjectPermission({ + actor: actor.type, + actorId: actor.id, + projectId: key.projectId, + actorAuthMethod: actor.authMethod, + actorOrgId: actor.orgId, + actionProjectType: ActionProjectType.KMS + }); + + ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionCmekActions.Read, ProjectPermissionSub.Cmek); + + if (key.keyUsage !== KmsKeyUsage.SIGN_VERIFY) { + throw new BadRequestError({ message: `Key with ID '${keyId}' is not intended for signing` }); + } + + const encryptionAlgorithm = key.encryptionAlgorithm as TCmekKeyEncryptionAlgorithm; + + const algos = [ + { + keyAlgorithm: "rsa", + signingAlgorithms: Object.values(SigningAlgorithm).filter((algorithm) => + algorithm.toLowerCase().startsWith("rsa") + ) + }, + { + keyAlgorithm: "ecc", + signingAlgorithms: Object.values(SigningAlgorithm).filter((algorithm) => + algorithm.toLowerCase().startsWith("ecdsa") + ) + } + ]; + + const selectedAlgorithm = algos.find((algo) => encryptionAlgorithm.toLowerCase().startsWith(algo.keyAlgorithm)); + + if (!selectedAlgorithm) { + throw new BadRequestError({ message: `Unsupported encryption algorithm: ${encryptionAlgorithm}` }); + } + + return { signingAlgorithms: selectedAlgorithm.signingAlgorithms, projectId: key.projectId }; + }; + + const getPublicKey = async ({ keyId }: TCmekGetPublicKeyDTO, actor: OrgServiceActor) => { + const key = await kmsDAL.findCmekById(keyId); + + if (!key) throw new NotFoundError({ message: `Key with ID "${keyId}" not found` }); + if (!key.projectId || key.isReserved) throw new BadRequestError({ message: "Key is not customer managed" }); + if (key.isDisabled) throw new BadRequestError({ message: "Key is disabled" }); + + const { permission } = await permissionService.getProjectPermission({ + actor: actor.type, + actorId: actor.id, + projectId: key.projectId, + actorAuthMethod: actor.authMethod, + actorOrgId: actor.orgId, + actionProjectType: ActionProjectType.KMS + }); + + ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionCmekActions.Read, ProjectPermissionSub.Cmek); + + const publicKey = await kmsService.getPublicKey({ kmsId: keyId }); + return { publicKey: publicKey.toString("base64"), projectId: key.projectId }; + }; + + const cmekSign = async ({ keyId, data, signingAlgorithm, isDigest }: TCmekSignDTO, actor: OrgServiceActor) => { + const key = await kmsDAL.findCmekById(keyId); + + if (!key) throw new NotFoundError({ message: `Key with ID "${keyId}" not found` }); + + if (!key.projectId || key.isReserved) throw new BadRequestError({ message: "Key is not customer managed" }); + + if (key.isDisabled) throw new BadRequestError({ message: "Key is disabled" }); + + const { permission } = await permissionService.getProjectPermission({ + actor: actor.type, + actorId: actor.id, + projectId: key.projectId, + actorAuthMethod: actor.authMethod, + actorOrgId: actor.orgId, + actionProjectType: ActionProjectType.KMS + }); + + ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionCmekActions.Sign, ProjectPermissionSub.Cmek); + + const sign = await kmsService.signWithKmsKey({ kmsId: keyId }); + + const { signature, algorithm } = await sign({ data: Buffer.from(data, "base64"), signingAlgorithm, isDigest }); + + return { + signature: signature.toString("base64"), + keyId: key.id, + projectId: key.projectId, + signingAlgorithm: algorithm + }; + }; + + const cmekVerify = async ( + { keyId, data, signature, signingAlgorithm, isDigest }: TCmekVerifyDTO, + actor: OrgServiceActor + ) => { + const key = await kmsDAL.findCmekById(keyId); + + if (!key) throw new NotFoundError({ message: `Key with ID "${keyId}" not found` }); + + if (!key.projectId || key.isReserved) throw new BadRequestError({ message: "Key is not customer managed" }); + + if (key.isDisabled) throw new BadRequestError({ message: "Key is disabled" }); + + const { permission } = await permissionService.getProjectPermission({ + actor: actor.type, + actorId: actor.id, + projectId: key.projectId, + actorAuthMethod: actor.authMethod, + actorOrgId: actor.orgId, + actionProjectType: ActionProjectType.KMS + }); + + ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionCmekActions.Verify, ProjectPermissionSub.Cmek); + + const verify = await kmsService.verifyWithKmsKey({ kmsId: keyId, signingAlgorithm }); + + const { signatureValid, algorithm } = await verify({ + isDigest, + data: Buffer.from(data, "base64"), + signature: Buffer.from(signature, "base64") + }); + + return { + signatureValid, + keyId: key.id, + projectId: key.projectId, + signingAlgorithm: algorithm + }; }; const cmekDecrypt = async ({ keyId, ciphertext }: TCmekDecryptDTO, actor: OrgServiceActor) => { @@ -248,7 +399,10 @@ export const cmekServiceFactory = ({ kmsService, kmsDAL, permissionService, proj const plaintextBlob = await decrypt({ cipherTextBlob: Buffer.from(ciphertext, "base64") }); - return plaintextBlob.toString("base64"); + return { + plaintext: plaintextBlob.toString("base64"), + projectId: key.projectId + }; }; return { @@ -259,6 +413,10 @@ export const cmekServiceFactory = ({ kmsService, kmsDAL, permissionService, proj cmekEncrypt, cmekDecrypt, findCmekById, - findCmekByName + findCmekByName, + cmekSign, + cmekVerify, + listSigningAlgorithms, + getPublicKey }; }; diff --git a/backend/src/services/cmek/cmek-types.ts b/backend/src/services/cmek/cmek-types.ts index b99ff1d6e..0421bce0e 100644 --- a/backend/src/services/cmek/cmek-types.ts +++ b/backend/src/services/cmek/cmek-types.ts @@ -1,12 +1,18 @@ -import { SymmetricEncryption } from "@app/lib/crypto/cipher"; +import { SymmetricKeyAlgorithm } from "@app/lib/crypto/cipher"; +import { AsymmetricKeyAlgorithm, SigningAlgorithm } from "@app/lib/crypto/sign"; import { OrderByDirection } from "@app/lib/types"; +import { KmsKeyUsage } from "../kms/kms-types"; + +export type TCmekKeyEncryptionAlgorithm = SymmetricKeyAlgorithm | AsymmetricKeyAlgorithm; + export type TCreateCmekDTO = { orgId: string; projectId: string; name: string; description?: string; - encryptionAlgorithm: SymmetricEncryption; + encryptionAlgorithm: TCmekKeyEncryptionAlgorithm; + keyUsage: KmsKeyUsage; }; export type TUpdabteCmekByIdDTO = { @@ -38,3 +44,26 @@ export type TCmekDecryptDTO = { export enum CmekOrderBy { Name = "name" } + +export type TCmekListSigningAlgorithmsDTO = { + keyId: string; +}; + +export type TCmekGetPublicKeyDTO = { + keyId: string; +}; + +export type TCmekSignDTO = { + keyId: string; + data: string; + signingAlgorithm: SigningAlgorithm; + isDigest: boolean; +}; + +export type TCmekVerifyDTO = { + keyId: string; + data: string; + signature: string; + signingAlgorithm: SigningAlgorithm; + isDigest: boolean; +}; diff --git a/backend/src/services/kms/kms-fns.ts b/backend/src/services/kms/kms-fns.ts index 06395272b..8c7aa13dd 100644 --- a/backend/src/services/kms/kms-fns.ts +++ b/backend/src/services/kms/kms-fns.ts @@ -1,13 +1,55 @@ -import { SymmetricEncryption } from "@app/lib/crypto/cipher"; +import { SymmetricKeyAlgorithm } from "@app/lib/crypto/cipher"; +import { AsymmetricKeyAlgorithm } from "@app/lib/crypto/sign"; +import { BadRequestError } from "@app/lib/errors"; + +import { KmsKeyUsage } from "./kms-types"; export const KMS_ROOT_CONFIG_UUID = "00000000-0000-0000-0000-000000000000"; -export const getByteLengthForAlgorithm = (encryptionAlgorithm: SymmetricEncryption) => { +export const getByteLengthForSymmetricEncryptionAlgorithm = (encryptionAlgorithm: SymmetricKeyAlgorithm) => { switch (encryptionAlgorithm) { - case SymmetricEncryption.AES_GCM_128: + case SymmetricKeyAlgorithm.AES_GCM_128: return 16; - case SymmetricEncryption.AES_GCM_256: + case SymmetricKeyAlgorithm.AES_GCM_256: default: return 32; } }; + +export const verifyKeyTypeAndAlgorithm = ( + keyUsage: KmsKeyUsage, + algorithm: SymmetricKeyAlgorithm | AsymmetricKeyAlgorithm, + extra?: { + forceType?: KmsKeyUsage; + } +) => { + if (extra?.forceType && keyUsage !== extra.forceType) { + throw new BadRequestError({ + message: `Unsupported key type, expected ${extra.forceType} but got ${keyUsage}` + }); + } + + if (keyUsage === KmsKeyUsage.ENCRYPT_DECRYPT) { + if (!Object.values(SymmetricKeyAlgorithm).includes(algorithm as SymmetricKeyAlgorithm)) { + throw new BadRequestError({ + message: `Unsupported encryption algorithm for encrypt/decrypt key: ${algorithm as string}` + }); + } + + return true; + } + + if (keyUsage === KmsKeyUsage.SIGN_VERIFY) { + if (!Object.values(AsymmetricKeyAlgorithm).includes(algorithm as AsymmetricKeyAlgorithm)) { + throw new BadRequestError({ + message: `Unsupported sign/verify algorithm for sign/verify key: ${algorithm as string}` + }); + } + + return true; + } + + throw new BadRequestError({ + message: `Unsupported key type: ${keyUsage as string}` + }); +}; diff --git a/backend/src/services/kms/kms-service.ts b/backend/src/services/kms/kms-service.ts index cfd64a89a..754c9be76 100644 --- a/backend/src/services/kms/kms-service.ts +++ b/backend/src/services/kms/kms-service.ts @@ -15,12 +15,17 @@ import { THsmServiceFactory } from "@app/ee/services/hsm/hsm-service"; import { KeyStorePrefixes, PgSqlLock, TKeyStoreFactory } from "@app/keystore/keystore"; import { TEnvConfig } from "@app/lib/config/env"; import { randomSecureBytes } from "@app/lib/crypto"; -import { symmetricCipherService, SymmetricEncryption } from "@app/lib/crypto/cipher"; +import { symmetricCipherService, SymmetricKeyAlgorithm } from "@app/lib/crypto/cipher"; import { generateHash } from "@app/lib/crypto/encryption"; +import { AsymmetricKeyAlgorithm, signingService } from "@app/lib/crypto/sign"; import { BadRequestError, ForbiddenRequestError, NotFoundError } from "@app/lib/errors"; import { logger } from "@app/lib/logger"; import { alphaNumericNanoId } from "@app/lib/nanoid"; -import { getByteLengthForAlgorithm, KMS_ROOT_CONFIG_UUID } from "@app/services/kms/kms-fns"; +import { + getByteLengthForSymmetricEncryptionAlgorithm, + KMS_ROOT_CONFIG_UUID, + verifyKeyTypeAndAlgorithm +} from "@app/services/kms/kms-fns"; import { TOrgDALFactory } from "../org/org-dal"; import { TProjectDALFactory } from "../project/project-dal"; @@ -29,6 +34,7 @@ import { TKmsKeyDALFactory } from "./kms-key-dal"; import { TKmsRootConfigDALFactory } from "./kms-root-config-dal"; import { KmsDataKey, + KmsKeyUsage, KmsType, RootKeyEncryptionStrategy, TDecryptWithKeyDTO, @@ -38,8 +44,11 @@ import { TEncryptWithKmsDTO, TGenerateKMSDTO, TGetKeyMaterialDTO, + TGetPublicKeyDTO, TImportKeyMaterialDTO, - TUpdateProjectSecretManagerKmsKeyDTO + TSignWithKmsDTO, + TUpdateProjectSecretManagerKmsKeyDTO, + TVerifyWithKmsDTO } from "./kms-types"; type TKmsServiceFactoryDep = { @@ -83,19 +92,42 @@ export const kmsServiceFactory = ({ tx, name, projectId, - encryptionAlgorithm = SymmetricEncryption.AES_GCM_256, + encryptionAlgorithm = SymmetricKeyAlgorithm.AES_GCM_256, + keyUsage = KmsKeyUsage.ENCRYPT_DECRYPT, description }: TGenerateKMSDTO) => { - const cipher = symmetricCipherService(SymmetricEncryption.AES_GCM_256); + // daniel: ensure that the key type (sign/encrypt) and the encryption algorithm are compatible. + verifyKeyTypeAndAlgorithm(keyUsage, encryptionAlgorithm); - const kmsKeyMaterial = randomSecureBytes(getByteLengthForAlgorithm(encryptionAlgorithm)); + let kmsKeyMaterial: Buffer | null = null; + if (keyUsage === KmsKeyUsage.ENCRYPT_DECRYPT) { + kmsKeyMaterial = randomSecureBytes( + getByteLengthForSymmetricEncryptionAlgorithm(encryptionAlgorithm as SymmetricKeyAlgorithm) + ); + } else if (keyUsage === KmsKeyUsage.SIGN_VERIFY) { + const { generateAsymmetricPrivateKey, getPublicKeyFromPrivateKey } = signingService( + encryptionAlgorithm as AsymmetricKeyAlgorithm + ); + kmsKeyMaterial = await generateAsymmetricPrivateKey(); + // daniel: safety check to ensure we're able to extract the public key from the private key before we proceed to key creation + getPublicKeyFromPrivateKey(kmsKeyMaterial); + } + + if (!kmsKeyMaterial) { + throw new BadRequestError({ + message: `Invalid KMS key type. No key material was created for key usage '${keyUsage}' using algorithm '${encryptionAlgorithm}'` + }); + } + + const cipher = symmetricCipherService(SymmetricKeyAlgorithm.AES_GCM_256); const encryptedKeyMaterial = cipher.encrypt(kmsKeyMaterial, ROOT_ENCRYPTION_KEY); const sanitizedName = name ? slugify(name) : slugify(alphaNumericNanoId(8).toLowerCase()); const dbQuery = async (db: Knex) => { const kmsDoc = await kmsDAL.create( { name: sanitizedName, + keyUsage, orgId, isReserved, projectId, @@ -115,6 +147,7 @@ export const kmsServiceFactory = ({ ); return kmsDoc; }; + if (tx) return dbQuery(tx); const doc = await kmsDAL.transaction(async (tx2) => dbQuery(tx2)); return doc; @@ -134,7 +167,7 @@ export const kmsServiceFactory = ({ */ const encryptWithInputKey = async ({ key }: Omit) => { // akhilmhdh: as more encryption are added do a check here on kmsDoc.encryptionAlgorithm - const cipher = symmetricCipherService(SymmetricEncryption.AES_GCM_256); + const cipher = symmetricCipherService(SymmetricKeyAlgorithm.AES_GCM_256); return ({ plainText }: Pick) => { const encryptedPlainTextBlob = cipher.encrypt(plainText, key); // Buffer#1 encrypted text + Buffer#2 version number @@ -149,7 +182,7 @@ export const kmsServiceFactory = ({ * This can be even later exposed directly as api for encryption as function */ const decryptWithInputKey = async ({ key }: Omit) => { - const cipher = symmetricCipherService(SymmetricEncryption.AES_GCM_256); + const cipher = symmetricCipherService(SymmetricKeyAlgorithm.AES_GCM_256); return ({ cipherTextBlob: versionedCipherTextBlob }: Pick) => { const cipherTextBlob = versionedCipherTextBlob.subarray(0, -KMS_VERSION_BLOB_LENGTH); @@ -227,7 +260,7 @@ export const kmsServiceFactory = ({ }; const encryptWithRootKey = () => { - const cipher = symmetricCipherService(SymmetricEncryption.AES_GCM_256); + const cipher = symmetricCipherService(SymmetricKeyAlgorithm.AES_GCM_256); return (plainTextBuffer: Buffer) => { const encryptedBuffer = cipher.encrypt(plainTextBuffer, ROOT_ENCRYPTION_KEY); @@ -236,7 +269,7 @@ export const kmsServiceFactory = ({ }; const decryptWithRootKey = () => { - const cipher = symmetricCipherService(SymmetricEncryption.AES_GCM_256); + const cipher = symmetricCipherService(SymmetricKeyAlgorithm.AES_GCM_256); return (cipherTextBuffer: Buffer) => { return cipher.decrypt(cipherTextBuffer, ROOT_ENCRYPTION_KEY); @@ -255,6 +288,11 @@ export const kmsServiceFactory = ({ throw new NotFoundError({ message: `KMS with ID '${kmsId}' not found` }); } + const encryptionAlgorithm = kmsDoc.internalKms?.encryptionAlgorithm as SymmetricKeyAlgorithm; + verifyKeyTypeAndAlgorithm(kmsDoc.keyUsage as KmsKeyUsage, encryptionAlgorithm, { + forceType: KmsKeyUsage.ENCRYPT_DECRYPT + }); + if (kmsDoc.externalKms) { let externalKms: TExternalKmsProviderFns; @@ -316,8 +354,8 @@ export const kmsServiceFactory = ({ } // internal KMS - const keyCipher = symmetricCipherService(SymmetricEncryption.AES_GCM_256); - const dataCipher = symmetricCipherService(kmsDoc.internalKms?.encryptionAlgorithm as SymmetricEncryption); + const keyCipher = symmetricCipherService(SymmetricKeyAlgorithm.AES_GCM_256); + const dataCipher = symmetricCipherService(encryptionAlgorithm); const kmsKey = keyCipher.decrypt(kmsDoc.internalKms?.encryptedKey as Buffer, ROOT_ENCRYPTION_KEY); return ({ cipherTextBlob: versionedCipherTextBlob }: Pick) => { @@ -345,19 +383,22 @@ export const kmsServiceFactory = ({ }); } - const keyCipher = symmetricCipherService(SymmetricEncryption.AES_GCM_256); + const keyCipher = symmetricCipherService(SymmetricKeyAlgorithm.AES_GCM_256); const kmsKey = keyCipher.decrypt(kmsDoc.internalKms?.encryptedKey as Buffer, ROOT_ENCRYPTION_KEY); return kmsKey; }; const importKeyMaterial = async ( - { key, algorithm, name, isReserved, projectId, orgId }: TImportKeyMaterialDTO, + { key, algorithm, name, isReserved, projectId, orgId, keyUsage }: TImportKeyMaterialDTO, tx?: Knex ) => { - const cipher = symmetricCipherService(SymmetricEncryption.AES_GCM_256); + // daniel: currently we only support imports for encrypt/decrypt keys + verifyKeyTypeAndAlgorithm(keyUsage, algorithm, { forceType: KmsKeyUsage.ENCRYPT_DECRYPT }); - const expectedByteLength = getByteLengthForAlgorithm(algorithm); + const cipher = symmetricCipherService(SymmetricKeyAlgorithm.AES_GCM_256); + + const expectedByteLength = getByteLengthForSymmetricEncryptionAlgorithm(algorithm as SymmetricKeyAlgorithm); if (key.byteLength !== expectedByteLength) { throw new BadRequestError({ message: `Invalid key length for ${algorithm}. Expected ${expectedByteLength} bytes but got ${key.byteLength} bytes` @@ -370,6 +411,7 @@ export const kmsServiceFactory = ({ const kmsDoc = await kmsDAL.create( { name: sanitizedName, + keyUsage: KmsKeyUsage.ENCRYPT_DECRYPT, orgId, isReserved, projectId @@ -393,12 +435,85 @@ export const kmsServiceFactory = ({ return doc; }; + const getPublicKey = async ({ kmsId }: TGetPublicKeyDTO) => { + const kmsDoc = await kmsDAL.findByIdWithAssociatedKms(kmsId); + if (!kmsDoc) { + throw new NotFoundError({ message: `KMS with ID '${kmsId}' not found` }); + } + + const encryptionAlgorithm = kmsDoc.internalKms?.encryptionAlgorithm as AsymmetricKeyAlgorithm; + + verifyKeyTypeAndAlgorithm(kmsDoc.keyUsage as KmsKeyUsage, encryptionAlgorithm, { + forceType: KmsKeyUsage.SIGN_VERIFY + }); + + const keyCipher = symmetricCipherService(SymmetricKeyAlgorithm.AES_GCM_256); + const kmsKey = keyCipher.decrypt(kmsDoc.internalKms?.encryptedKey as Buffer, ROOT_ENCRYPTION_KEY); + + return signingService(encryptionAlgorithm).getPublicKeyFromPrivateKey(kmsKey); + }; + + const signWithKmsKey = async ({ kmsId }: Pick) => { + const kmsDoc = await kmsDAL.findByIdWithAssociatedKms(kmsId); + if (!kmsDoc) { + throw new NotFoundError({ message: `KMS with ID '${kmsId}' not found` }); + } + + const encryptionAlgorithm = kmsDoc.internalKms?.encryptionAlgorithm as AsymmetricKeyAlgorithm; + verifyKeyTypeAndAlgorithm(kmsDoc.keyUsage as KmsKeyUsage, encryptionAlgorithm, { + forceType: KmsKeyUsage.SIGN_VERIFY + }); + + const keyCipher = symmetricCipherService(SymmetricKeyAlgorithm.AES_GCM_256); + const { sign } = signingService(encryptionAlgorithm); + return async ({ + data, + signingAlgorithm, + isDigest + }: Pick) => { + const kmsKey = keyCipher.decrypt(kmsDoc.internalKms?.encryptedKey as Buffer, ROOT_ENCRYPTION_KEY); + const signature = await sign(data, kmsKey, signingAlgorithm, isDigest); + + return Promise.resolve({ signature, algorithm: signingAlgorithm }); + }; + }; + + const verifyWithKmsKey = async ({ + kmsId, + signingAlgorithm + }: Pick) => { + const kmsDoc = await kmsDAL.findByIdWithAssociatedKms(kmsId); + if (!kmsDoc) { + throw new NotFoundError({ message: `KMS with ID '${kmsId}' not found` }); + } + + const encryptionAlgorithm = kmsDoc.internalKms?.encryptionAlgorithm as AsymmetricKeyAlgorithm; + verifyKeyTypeAndAlgorithm(kmsDoc.keyUsage as KmsKeyUsage, encryptionAlgorithm, { + forceType: KmsKeyUsage.SIGN_VERIFY + }); + + const keyCipher = symmetricCipherService(SymmetricKeyAlgorithm.AES_GCM_256); + const { verify, getPublicKeyFromPrivateKey } = signingService(encryptionAlgorithm); + return async ({ data, signature, isDigest }: Pick) => { + const kmsKey = keyCipher.decrypt(kmsDoc.internalKms?.encryptedKey as Buffer, ROOT_ENCRYPTION_KEY); + + const publicKey = getPublicKeyFromPrivateKey(kmsKey); + const signatureValid = await verify(data, signature, publicKey, signingAlgorithm, isDigest); + return Promise.resolve({ signatureValid, algorithm: signingAlgorithm }); + }; + }; + const encryptWithKmsKey = async ({ kmsId }: Omit, tx?: Knex) => { const kmsDoc = await kmsDAL.findByIdWithAssociatedKms(kmsId, tx); if (!kmsDoc) { throw new NotFoundError({ message: `KMS with ID '${kmsId}' not found` }); } + const encryptionAlgorithm = kmsDoc.internalKms?.encryptionAlgorithm as SymmetricKeyAlgorithm; + verifyKeyTypeAndAlgorithm(kmsDoc.keyUsage as KmsKeyUsage, encryptionAlgorithm, { + forceType: KmsKeyUsage.ENCRYPT_DECRYPT + }); + if (kmsDoc.externalKms) { let externalKms: TExternalKmsProviderFns; if (!kmsDoc.orgKms.id || !kmsDoc.orgKms.encryptedDataKey) { @@ -454,8 +569,8 @@ export const kmsServiceFactory = ({ } // internal KMS - const keyCipher = symmetricCipherService(SymmetricEncryption.AES_GCM_256); - const dataCipher = symmetricCipherService(kmsDoc.internalKms?.encryptionAlgorithm as SymmetricEncryption); + const keyCipher = symmetricCipherService(SymmetricKeyAlgorithm.AES_GCM_256); + const dataCipher = symmetricCipherService(encryptionAlgorithm); return ({ plainText }: Pick) => { const kmsKey = keyCipher.decrypt(kmsDoc.internalKms?.encryptedKey as Buffer, ROOT_ENCRYPTION_KEY); const encryptedPlainTextBlob = dataCipher.encrypt(plainText, kmsKey); @@ -729,7 +844,7 @@ export const kmsServiceFactory = ({ // case 2: root key is encrypted with software encryption if (kmsRootConfig.encryptionStrategy === RootKeyEncryptionStrategy.Software) { - const cipher = symmetricCipherService(SymmetricEncryption.AES_GCM_256); + const cipher = symmetricCipherService(SymmetricKeyAlgorithm.AES_GCM_256); const encryptionKeyBuffer = $getBasicEncryptionKey(); return cipher.decrypt(kmsRootConfig.encryptedRootKey, encryptionKeyBuffer); @@ -749,7 +864,7 @@ export const kmsServiceFactory = ({ } if (strategy === RootKeyEncryptionStrategy.Software) { - const cipher = symmetricCipherService(SymmetricEncryption.AES_GCM_256); + const cipher = symmetricCipherService(SymmetricKeyAlgorithm.AES_GCM_256); const encryptionKeyBuffer = $getBasicEncryptionKey(); return cipher.encrypt(plainKeyBuffer, encryptionKeyBuffer); @@ -765,7 +880,7 @@ export const kmsServiceFactory = ({ const createCipherPairWithDataKey = async (encryptionContext: TEncryptWithKmsDataKeyDTO, trx?: Knex) => { const dataKey = await $getDataKey(encryptionContext, trx); - const cipher = symmetricCipherService(SymmetricEncryption.AES_GCM_256); + const cipher = symmetricCipherService(SymmetricKeyAlgorithm.AES_GCM_256); return { encryptor: ({ plainText }: Pick) => { @@ -966,6 +1081,7 @@ export const kmsServiceFactory = ({ const decryptedRootKey = await $decryptRootKey(kmsRootConfig); logger.info("KMS: Loading ROOT Key into Memory."); + ROOT_ENCRYPTION_KEY = decryptedRootKey; }; @@ -1014,6 +1130,9 @@ export const kmsServiceFactory = ({ getKmsById, createCipherPairWithDataKey, getKeyMaterial, - importKeyMaterial + importKeyMaterial, + signWithKmsKey, + verifyWithKmsKey, + getPublicKey }; }; diff --git a/backend/src/services/kms/kms-types.ts b/backend/src/services/kms/kms-types.ts index 8be0b29fc..ca2401bb6 100644 --- a/backend/src/services/kms/kms-types.ts +++ b/backend/src/services/kms/kms-types.ts @@ -1,6 +1,7 @@ import { Knex } from "knex"; -import { SymmetricEncryption } from "@app/lib/crypto/cipher"; +import { SymmetricKeyAlgorithm } from "@app/lib/crypto/cipher"; +import { AsymmetricKeyAlgorithm, SigningAlgorithm } from "@app/lib/crypto/sign/types"; export enum KmsDataKey { Organization, @@ -13,6 +14,11 @@ export enum KmsType { Internal = "internal" } +export enum KmsKeyUsage { + ENCRYPT_DECRYPT = "encrypt-decrypt", + SIGN_VERIFY = "sign-verify" +} + export type TEncryptWithKmsDataKeyDTO = | { type: KmsDataKey.Organization; orgId: string } | { type: KmsDataKey.SecretManager; projectId: string }; @@ -25,7 +31,8 @@ export type TEncryptWithKmsDataKeyDTO = export type TGenerateKMSDTO = { orgId: string; projectId?: string; - encryptionAlgorithm?: SymmetricEncryption; + encryptionAlgorithm?: SymmetricKeyAlgorithm | AsymmetricKeyAlgorithm; + keyUsage?: KmsKeyUsage; isReserved?: boolean; name?: string; description?: string; @@ -37,6 +44,25 @@ export type TEncryptWithKmsDTO = { plainText: Buffer; }; +export type TGetPublicKeyDTO = { + kmsId: string; +}; + +export type TSignWithKmsDTO = { + kmsId: string; + data: Buffer; + signingAlgorithm: SigningAlgorithm; + isDigest: boolean; +}; + +export type TVerifyWithKmsDTO = { + kmsId: string; + data: Buffer; + signature: Buffer; + signingAlgorithm: SigningAlgorithm; + isDigest: boolean; +}; + export type TEncryptionWithKeyDTO = { key: Buffer; plainText: Buffer; @@ -67,9 +93,10 @@ export type TGetKeyMaterialDTO = { export type TImportKeyMaterialDTO = { key: Buffer; - algorithm: SymmetricEncryption; + algorithm: SymmetricKeyAlgorithm; name?: string; isReserved: boolean; projectId: string; orgId: string; + keyUsage: KmsKeyUsage; }; diff --git a/docs/api-reference/endpoints/kms/keys/decrypt.mdx b/docs/api-reference/endpoints/kms/encryption/decrypt.mdx similarity index 100% rename from docs/api-reference/endpoints/kms/keys/decrypt.mdx rename to docs/api-reference/endpoints/kms/encryption/decrypt.mdx diff --git a/docs/api-reference/endpoints/kms/keys/encrypt.mdx b/docs/api-reference/endpoints/kms/encryption/encrypt.mdx similarity index 100% rename from docs/api-reference/endpoints/kms/keys/encrypt.mdx rename to docs/api-reference/endpoints/kms/encryption/encrypt.mdx diff --git a/docs/api-reference/endpoints/kms/signing/public-key.mdx b/docs/api-reference/endpoints/kms/signing/public-key.mdx new file mode 100644 index 000000000..4c8e1fda5 --- /dev/null +++ b/docs/api-reference/endpoints/kms/signing/public-key.mdx @@ -0,0 +1,4 @@ +--- +title: "Retrieve Public Key" +openapi: "GET /api/v1/kms/keys/{keyId}/public-key" +--- diff --git a/docs/api-reference/endpoints/kms/signing/sign.mdx b/docs/api-reference/endpoints/kms/signing/sign.mdx new file mode 100644 index 000000000..ebeca5924 --- /dev/null +++ b/docs/api-reference/endpoints/kms/signing/sign.mdx @@ -0,0 +1,4 @@ +--- +title: "Sign Data" +openapi: "POST /api/v1/kms/keys/{keyId}/sign" +--- diff --git a/docs/api-reference/endpoints/kms/signing/signing-algorithms.mdx b/docs/api-reference/endpoints/kms/signing/signing-algorithms.mdx new file mode 100644 index 000000000..0a09ef9e0 --- /dev/null +++ b/docs/api-reference/endpoints/kms/signing/signing-algorithms.mdx @@ -0,0 +1,4 @@ +--- +title: "List Signing Algorithms" +openapi: "GET /api/v1/kms/keys/{keyId}/signing-algorithms" +--- diff --git a/docs/api-reference/endpoints/kms/signing/verify.mdx b/docs/api-reference/endpoints/kms/signing/verify.mdx new file mode 100644 index 000000000..a76270fc3 --- /dev/null +++ b/docs/api-reference/endpoints/kms/signing/verify.mdx @@ -0,0 +1,4 @@ +--- +title: "Verify Signature" +openapi: "POST /api/v1/kms/keys/{keyId}/verify" +--- diff --git a/docs/mint.json b/docs/mint.json index 0ebc22e92..f3c186535 100644 --- a/docs/mint.json +++ b/docs/mint.json @@ -1319,9 +1319,23 @@ "api-reference/endpoints/kms/keys/get-by-name", "api-reference/endpoints/kms/keys/create", "api-reference/endpoints/kms/keys/update", - "api-reference/endpoints/kms/keys/delete", - "api-reference/endpoints/kms/keys/encrypt", - "api-reference/endpoints/kms/keys/decrypt" + "api-reference/endpoints/kms/keys/delete" + ] + }, + { + "group": "Encryption", + "pages": [ + "api-reference/endpoints/kms/encryption/encrypt", + "api-reference/endpoints/kms/encryption/decrypt" + ] + }, + { + "group": "Signing", + "pages": [ + "api-reference/endpoints/kms/signing/sign", + "api-reference/endpoints/kms/signing/verify", + "api-reference/endpoints/kms/signing/public-key", + "api-reference/endpoints/kms/signing/signing-algorithms" ] } ] diff --git a/frontend/src/context/ProjectPermissionContext/types.ts b/frontend/src/context/ProjectPermissionContext/types.ts index 7c92988a1..27fd0273c 100644 --- a/frontend/src/context/ProjectPermissionContext/types.ts +++ b/frontend/src/context/ProjectPermissionContext/types.ts @@ -30,7 +30,9 @@ export enum ProjectPermissionCmekActions { Edit = "edit", Delete = "delete", Encrypt = "encrypt", - Decrypt = "decrypt" + Decrypt = "decrypt", + Sign = "sign", + Verify = "verify" } export enum ProjectPermissionKmipActions { diff --git a/frontend/src/helpers/kms.ts b/frontend/src/helpers/kms.ts new file mode 100644 index 000000000..64b85dc2c --- /dev/null +++ b/frontend/src/helpers/kms.ts @@ -0,0 +1,27 @@ +import { AsymmetricKeyAlgorithm, KmsKeyUsage, SymmetricKeyAlgorithm } from "@app/hooks/api/cmeks"; + +export const kmsKeyUsageOptions: Record< + KmsKeyUsage, + { + label: string; + tooltip: string; + } +> = { + [KmsKeyUsage.ENCRYPT_DECRYPT]: { + label: "Encrypt/Decrypt", + tooltip: "Use the key only to encrypt and decrypt data." + }, + [KmsKeyUsage.SIGN_VERIFY]: { + label: "Sign/Verify", + tooltip: + "Key pairs for digital signing. Uses the private key for signing and the public key for verification." + } +}; + +export const keyUsageDefaultOption: Record< + KmsKeyUsage, + SymmetricKeyAlgorithm | AsymmetricKeyAlgorithm +> = { + [KmsKeyUsage.ENCRYPT_DECRYPT]: SymmetricKeyAlgorithm.AES_GCM_256, + [KmsKeyUsage.SIGN_VERIFY]: AsymmetricKeyAlgorithm.RSA_4096 +}; diff --git a/frontend/src/hooks/api/auditLogs/constants.tsx b/frontend/src/hooks/api/auditLogs/constants.tsx index 464a46aa8..159e840ec 100644 --- a/frontend/src/hooks/api/auditLogs/constants.tsx +++ b/frontend/src/hooks/api/auditLogs/constants.tsx @@ -106,6 +106,10 @@ export const eventToNameMap: { [K in EventType]: string } = { [EventType.GET_CMEK]: "Get KMS key", [EventType.CMEK_ENCRYPT]: "Encrypt with KMS key", [EventType.CMEK_DECRYPT]: "Decrypt with KMS key", + [EventType.CMEK_SIGN]: "Sign with KMS key", + [EventType.CMEK_VERIFY]: "Verify with KMS key", + [EventType.CMEK_LIST_SIGNING_ALGORITHMS]: "List signing algorithms for KMS key", + [EventType.CMEK_GET_PUBLIC_KEY]: "Get public key for KMS key", [EventType.UPDATE_EXTERNAL_GROUP_ORG_ROLE_MAPPINGS]: "Update SSO group to organization role mapping", [EventType.GET_EXTERNAL_GROUP_ORG_ROLE_MAPPINGS]: "List SSO group to organization role mapping", diff --git a/frontend/src/hooks/api/auditLogs/enums.tsx b/frontend/src/hooks/api/auditLogs/enums.tsx index ab287226b..15adb0272 100644 --- a/frontend/src/hooks/api/auditLogs/enums.tsx +++ b/frontend/src/hooks/api/auditLogs/enums.tsx @@ -110,6 +110,10 @@ export enum EventType { GET_CMEK = "get-cmek", CMEK_ENCRYPT = "cmek-encrypt", CMEK_DECRYPT = "cmek-decrypt", + CMEK_SIGN = "cmek-sign", + CMEK_VERIFY = "cmek-verify", + CMEK_LIST_SIGNING_ALGORITHMS = "cmek-list-signing-algorithms", + CMEK_GET_PUBLIC_KEY = "cmek-get-public-key", UPDATE_EXTERNAL_GROUP_ORG_ROLE_MAPPINGS = "update-external-group-org-role-mapping", GET_EXTERNAL_GROUP_ORG_ROLE_MAPPINGS = "get-external-group-org-role-mapping", GET_PROJECT_TEMPLATES = "get-project-templates", diff --git a/frontend/src/hooks/api/cmeks/mutations.tsx b/frontend/src/hooks/api/cmeks/mutations.tsx index 41e70c193..b806e2b44 100644 --- a/frontend/src/hooks/api/cmeks/mutations.tsx +++ b/frontend/src/hooks/api/cmeks/mutations.tsx @@ -8,6 +8,10 @@ import { TCmekDecryptResponse, TCmekEncrypt, TCmekEncryptResponse, + TCmekSign, + TCmekSignResponse, + TCmekVerify, + TCmekVerifyResponse, TCreateCmek, TDeleteCmek, TUpdateCmek @@ -74,6 +78,44 @@ export const useCmekEncrypt = () => { }); }; +export const useCmekSign = () => { + return useMutation({ + mutationFn: async ({ + keyId, + data, + signingAlgorithm, + isBase64Encoded + }: TCmekSign & { isBase64Encoded: boolean }) => { + const res = await apiRequest.post(`/api/v1/kms/keys/${keyId}/sign`, { + data: isBase64Encoded ? data : encodeBase64(Buffer.from(data)), + signingAlgorithm + }); + + return res.data; + } + }); +}; + +export const useCmekVerify = () => { + return useMutation({ + mutationFn: async ({ + keyId, + data, + signature, + signingAlgorithm, + isBase64Encoded + }: TCmekVerify & { isBase64Encoded: boolean }) => { + const res = await apiRequest.post(`/api/v1/kms/keys/${keyId}/verify`, { + data: isBase64Encoded ? data : encodeBase64(Buffer.from(data)), + signature, + signingAlgorithm + }); + + return res.data; + } + }); +}; + export const useCmekDecrypt = () => { return useMutation({ mutationFn: async ({ keyId, ciphertext }: TCmekDecrypt) => { diff --git a/frontend/src/hooks/api/cmeks/types.ts b/frontend/src/hooks/api/cmeks/types.ts index c557c1fc2..2f6b8788b 100644 --- a/frontend/src/hooks/api/cmeks/types.ts +++ b/frontend/src/hooks/api/cmeks/types.ts @@ -1,10 +1,18 @@ +import { z } from "zod"; + import { OrderByDirection } from "@app/hooks/api/generic/types"; +export enum KmsKeyUsage { + ENCRYPT_DECRYPT = "encrypt-decrypt", + SIGN_VERIFY = "sign-verify" +} + export type TCmek = { id: string; + keyUsage: KmsKeyUsage; name: string; description?: string; - encryptionAlgorithm: EncryptionAlgorithm; + encryptionAlgorithm: AsymmetricKeyAlgorithm | SymmetricKeyAlgorithm; projectId: string; isDisabled: boolean; isReserved: boolean; @@ -17,7 +25,8 @@ export type TCmek = { type ProjectRef = { projectId: string }; type KeyRef = { keyId: string }; -export type TCreateCmek = Pick & ProjectRef; +export type TCreateCmek = Pick & + ProjectRef; export type TUpdateCmek = KeyRef & Partial> & ProjectRef; @@ -26,6 +35,13 @@ export type TDeleteCmek = KeyRef & ProjectRef; export type TCmekEncrypt = KeyRef & { plaintext: string; isBase64Encoded?: boolean }; export type TCmekDecrypt = KeyRef & { ciphertext: string }; +export type TCmekSign = KeyRef & { data: string; signingAlgorithm: SigningAlgorithm }; +export type TCmekVerify = KeyRef & { + data: string; + signature: string; + signingAlgorithm: SigningAlgorithm; +}; + export type TProjectCmeksList = { keys: TCmek[]; totalCount: number; @@ -44,6 +60,18 @@ export type TCmekEncryptResponse = { ciphertext: string; }; +export type TCmekSignResponse = { + signature: string; + keyId: string; + signingAlgorithm: SigningAlgorithm; +}; + +export type TCmekVerifyResponse = { + signatureValid: boolean; + keyId: string; + signingAlgorithm: SigningAlgorithm; +}; + export type TCmekDecryptResponse = { plaintext: string; }; @@ -52,7 +80,35 @@ export enum CmekOrderBy { Name = "name" } -export enum EncryptionAlgorithm { +export enum AsymmetricKeyAlgorithm { + RSA_4096 = "RSA_4096", + ECC_NIST_P256 = "ECC_NIST_P256" +} + +// Supported symmetric encrypt/decrypt algorithms +export enum SymmetricKeyAlgorithm { AES_GCM_256 = "aes-256-gcm", AES_GCM_128 = "aes-128-gcm" } + +export const AllowedEncryptionKeyAlgorithms = z.enum([ + ...Object.values(SymmetricKeyAlgorithm), + ...Object.values(AsymmetricKeyAlgorithm) +] as [string, ...string[]]).options; + +export enum SigningAlgorithm { + // RSA PSS algorithms + RSASSA_PSS_SHA_256 = "RSASSA_PSS_SHA_256", + RSASSA_PSS_SHA_384 = "RSASSA_PSS_SHA_384", + RSASSA_PSS_SHA_512 = "RSASSA_PSS_SHA_512", + + // RSA PKCS#1 v1.5 algorithms + RSASSA_PKCS1_V1_5_SHA_256 = "RSASSA_PKCS1_V1_5_SHA_256", + RSASSA_PKCS1_V1_5_SHA_384 = "RSASSA_PKCS1_V1_5_SHA_384", + RSASSA_PKCS1_V1_5_SHA_512 = "RSASSA_PKCS1_V1_5_SHA_512", + + // ECDSA algorithms + ECDSA_SHA_256 = "ECDSA_SHA_256", + ECDSA_SHA_384 = "ECDSA_SHA_384", + ECDSA_SHA_512 = "ECDSA_SHA_512" +} diff --git a/frontend/src/lib/fn/base64.ts b/frontend/src/lib/fn/base64.ts new file mode 100644 index 000000000..bcef1a60b --- /dev/null +++ b/frontend/src/lib/fn/base64.ts @@ -0,0 +1,14 @@ +const base64WithPadding = + /^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=|[A-Za-z0-9+/]{4})$/; + +export const isBase64 = (str: string): boolean => { + if (typeof str !== "string") { + throw new TypeError("Expected a string"); + } + + if (str === "") return true; + + const regex = base64WithPadding; + + return regex.test(str); +}; diff --git a/frontend/src/pages/kms/OverviewPage/components/CmekModal.tsx b/frontend/src/pages/kms/OverviewPage/components/CmekModal.tsx index 7598d8e80..c43b6c8d0 100644 --- a/frontend/src/pages/kms/OverviewPage/components/CmekModal.tsx +++ b/frontend/src/pages/kms/OverviewPage/components/CmekModal.tsx @@ -15,13 +15,23 @@ import { TextArea } from "@app/components/v2"; import { useWorkspace } from "@app/context"; -import { EncryptionAlgorithm, TCmek, useCreateCmek, useUpdateCmek } from "@app/hooks/api/cmeks"; +import { keyUsageDefaultOption, kmsKeyUsageOptions } from "@app/helpers/kms"; +import { + AllowedEncryptionKeyAlgorithms, + AsymmetricKeyAlgorithm, + KmsKeyUsage, + SymmetricKeyAlgorithm, + TCmek, + useCreateCmek, + useUpdateCmek +} from "@app/hooks/api/cmeks"; import { slugSchema } from "@app/lib/schemas"; const formSchema = z.object({ name: slugSchema({ min: 1, max: 32, field: "Name" }), description: z.string().max(500).optional(), - encryptionAlgorithm: z.nativeEnum(EncryptionAlgorithm) + encryptionAlgorithm: z.enum(AllowedEncryptionKeyAlgorithms), + keyUsage: z.nativeEnum(KmsKeyUsage) }); export type FormData = z.infer; @@ -47,24 +57,33 @@ const CmekForm = ({ onComplete, cmek }: FormProps) => { control, handleSubmit, register, + setValue, + watch, formState: { isSubmitting, errors } } = useForm({ resolver: zodResolver(formSchema), defaultValues: { name: cmek?.name, description: cmek?.description, - encryptionAlgorithm: EncryptionAlgorithm.AES_GCM_256 + encryptionAlgorithm: SymmetricKeyAlgorithm.AES_GCM_256, + keyUsage: KmsKeyUsage.ENCRYPT_DECRYPT } }); - const handleCreateCmek = async ({ encryptionAlgorithm, name, description }: FormData) => { + const handleCreateCmek = async ({ + encryptionAlgorithm, + name, + description, + keyUsage + }: FormData) => { const mutation = isUpdate ? updateCmek.mutateAsync({ keyId: cmek.id, projectId, name, description }) : createCmek.mutateAsync({ projectId, - encryptionAlgorithm, name, - description + description, + keyUsage, + encryptionAlgorithm: encryptionAlgorithm as AsymmetricKeyAlgorithm | SymmetricKeyAlgorithm }); try { @@ -83,6 +102,8 @@ const CmekForm = ({ onComplete, cmek }: FormProps) => { } }; + const selectedKeyUsage = watch("keyUsage"); + return (
{ > - {!isUpdate && ( - ( - - - - )} - /> - )} +
+ {!isUpdate && ( + <> + ( + + {Object.entries(KmsKeyUsage).map(([key, value]) => ( +
+

{kmsKeyUsageOptions[value].label}

+

{kmsKeyUsageOptions[value].tooltip}

+
+ ))} +
+ } + label="Key Usage" + errorText={error?.message} + isError={Boolean(error)} + > + + + )} + /> + ( + + + + )} + /> + + )} + ; + +type Props = { + isOpen: boolean; + onOpenChange: (isOpen: boolean) => void; + cmek: TCmek; +}; + +type FormProps = Pick; + +const SignForm = ({ cmek }: FormProps) => { + const cmekSign = useCmekSign(); + + const { + handleSubmit, + register, + control, + formState: { isSubmitting, errors } + } = useForm({ + resolver: zodResolver(formSchema), + defaultValues: { + signingAlgorithm: cmek?.encryptionAlgorithm?.startsWith("RSA") + ? SigningAlgorithm.RSASSA_PSS_SHA_512 + : SigningAlgorithm.ECDSA_SHA_256, + isBase64Encoded: false + } + }); + + const [copySignature, isCopyingSignature, setCopySignature] = useTimedReset({ + initialState: "Copy to Clipboard" + }); + + const handleSignData = async (formData: FormData) => { + try { + await cmekSign.mutateAsync({ ...formData, keyId: cmek.id }); + createNotification({ + text: "Successfully signed data", + type: "success" + }); + } catch (err) { + console.error(err); + createNotification({ + text: "Failed to sign data", + type: "error" + }); + } + }; + + const signature = cmekSign.data?.signature; + + const handleCopyToClipboard = () => { + navigator.clipboard.writeText(signature ?? ""); + + setCopySignature("Copied to Clipboard"); + }; + + const allowedSigningAlgorithms = Object.values(SigningAlgorithm).filter((a) => + cmek?.encryptionAlgorithm?.startsWith("RSA") + ? a.toLowerCase().startsWith("rsa") + : a.toLowerCase().startsWith("ecdsa") + ); + + return ( + + {signature ? ( + +