From 939b77b0508b5a723c1d9d0517294e308bbdd6d2 Mon Sep 17 00:00:00 2001 From: Daniel Hougaard Date: Wed, 9 Apr 2025 01:55:26 +0400 Subject: [PATCH] fix: fixed local verification & added digest support --- backend/Dockerfile | 3 +- backend/Dockerfile.dev | 1 + backend/src/lib/api-docs/constants.ts | 8 +- backend/src/lib/crypto/sign/signing.ts | 508 ++++++++++++++------ backend/src/lib/crypto/sign/types.ts | 14 +- backend/src/server/routes/v1/cmek-router.ts | 12 +- backend/src/services/cmek/cmek-service.ts | 15 +- backend/src/services/cmek/cmek-types.ts | 2 + backend/src/services/kms/kms-service.ts | 27 +- backend/src/services/kms/kms-types.ts | 2 + frontend/src/hooks/api/cmeks/types.ts | 4 +- 11 files changed, 409 insertions(+), 187 deletions(-) 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/src/lib/api-docs/constants.ts b/backend/src/lib/api-docs/constants.ts index f60b4223d..d3026e223 100644 --- a/backend/src/lib/api-docs/constants.ts +++ b/backend/src/lib/api-docs/constants.ts @@ -1637,12 +1637,16 @@ export const KMS = { SIGN: { keyId: "The ID of the key to sign the data with.", - data: "The data in string format to be signed (base64 encoded)." + 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)." + signature: "The signature to be verified (base64 encoded).", + isDigest: "Whether the data is already digested or not." } }; diff --git a/backend/src/lib/crypto/sign/signing.ts b/backend/src/lib/crypto/sign/signing.ts index 2156d1159..2cbebb6ef 100644 --- a/backend/src/lib/crypto/sign/signing.ts +++ b/backend/src/lib/crypto/sign/signing.ts @@ -1,17 +1,29 @@ +import { execFile } from "child_process"; import crypto from "crypto"; +import fs from "fs/promises"; +import os from "os"; +import path from "path"; +import { promisify } from "util"; import { BadRequestError } from "@app/lib/errors"; import { logger } from "@app/lib/logger"; import { AsymmetricKeyAlgorithm, SigningAlgorithm, TAsymmetricSignVerifyFns } from "./types"; -// Map of signing algorithms to their parameters +const execFileAsync = promisify(execFile); + interface SigningParams { - hashAlgorithm: string; - padding?: number; // Will use crypto.constants values + hashAlgorithm: SupportedHashAlgorithm; + padding?: number; saltLength?: number; } +enum SupportedHashAlgorithm { + SHA256 = "sha256", + SHA384 = "sha384", + SHA512 = "sha512" +} + const SHA256_DIGEST_LENGTH = 32; const SHA384_DIGEST_LENGTH = 48; const SHA512_DIGEST_LENGTH = 64; @@ -19,76 +31,78 @@ const SHA512_DIGEST_LENGTH = 64; /** * Service for cryptographic signing and verification operations using asymmetric keys * - * @param algorithm The signing algorithm to use + * @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: "sha256", + hashAlgorithm: SupportedHashAlgorithm.SHA256, padding: crypto.constants.RSA_PKCS1_PSS_PADDING, saltLength: SHA256_DIGEST_LENGTH }; case SigningAlgorithm.RSASSA_PSS_SHA_384: return { - hashAlgorithm: "sha384", + hashAlgorithm: SupportedHashAlgorithm.SHA384, padding: crypto.constants.RSA_PKCS1_PSS_PADDING, saltLength: SHA384_DIGEST_LENGTH }; - case SigningAlgorithm.RSASSA_PSS_SHA_512: - return { - hashAlgorithm: "sha512", - padding: crypto.constants.RSA_PKCS1_PSS_PADDING, - saltLength: SHA512_DIGEST_LENGTH - }; // RSA PKCS#1 v1.5 - case SigningAlgorithm.RSASSA_PKCS1_V1_5_SHA_256: + case SigningAlgorithm.RSASSA_PKCS1_V1_5_SHA_512: return { - hashAlgorithm: "sha256", + hashAlgorithm: SupportedHashAlgorithm.SHA512, padding: crypto.constants.RSA_PKCS1_PADDING }; case SigningAlgorithm.RSASSA_PKCS1_V1_5_SHA_384: return { - hashAlgorithm: "sha384", + hashAlgorithm: SupportedHashAlgorithm.SHA384, padding: crypto.constants.RSA_PKCS1_PADDING }; - case SigningAlgorithm.RSASSA_PKCS1_V1_5_SHA_512: + case SigningAlgorithm.RSASSA_PKCS1_V1_5_SHA_256: return { - hashAlgorithm: "sha512", + hashAlgorithm: SupportedHashAlgorithm.SHA256, padding: crypto.constants.RSA_PKCS1_PADDING }; // ECDSA case SigningAlgorithm.ECDSA_SHA_256: - return { hashAlgorithm: "sha256" }; + return { hashAlgorithm: SupportedHashAlgorithm.SHA256 }; case SigningAlgorithm.ECDSA_SHA_384: - return { hashAlgorithm: "sha384" }; + return { hashAlgorithm: SupportedHashAlgorithm.SHA384 }; case SigningAlgorithm.ECDSA_SHA_512: - return { hashAlgorithm: "sha512" }; + return { hashAlgorithm: SupportedHashAlgorithm.SHA512 }; default: throw new Error(`Unsupported signing algorithm: ${signingAlgorithm as string}`); } }; - // For ECC key generation, nodejs has some strange and hardly documented curve naming conventions - const $getEcCurveName = (keyAlgorithm: AsymmetricKeyAlgorithm): string => { + const $getEcCurveName = (keyAlgorithm: AsymmetricKeyAlgorithm): { full: string; short: string } => { // We will support more in the future switch (keyAlgorithm) { case AsymmetricKeyAlgorithm.ECC_NIST_P256: - return "prime256v1"; + 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 isRsaKey = algorithm.startsWith("RSA"); + const isEccKey = algorithm.startsWith("ECC"); const isRsaAlgorithm = signingAlgorithm.startsWith("RSASSA"); const isEccAlgorithm = signingAlgorithm.startsWith("ECDSA"); @@ -102,13 +116,329 @@ export const signingService = (algorithm: AsymmetricKeyAlgorithm): TAsymmetricSi } }; + const $signRsaDigest = async (digest: Buffer, privateKey: Buffer, hashAlgorithm: SupportedHashAlgorithm) => { + const script = `openssl pkeyutl -sign -in <(base64 -d <<< "$DIGEST_B64") -inkey <(base64 -d <<< "$KEY_B64") -pkeyopt digest:"$HASH_ALG" | base64`; + const result = await execFileAsync("bash", ["-c", script], { + encoding: "utf8", + maxBuffer: 10 * 1024 * 1024, + env: { + DIGEST_B64: digest.toString("base64"), + KEY_B64: privateKey.toString("base64"), + HASH_ALG: hashAlgorithm + } + }); + + if (result.stderr) { + throw new Error(result.stderr); + } + + if (!result.stdout) { + throw new Error( + "No signature was created. Make sure you are using an appropiate signing algorithm that uses the same hashing algorithm as the one used to create the digest." + ); + } + + return Buffer.from(result.stdout.trim(), "base64"); + }; + + const $signEccDigest = async (digest: Buffer, privateKey: Buffer, hashAlgorithm: SupportedHashAlgorithm) => { + const script = `openssl pkeyutl -sign -in <(base64 -d <<< "$DIGEST_B64") -inkey <(base64 -d <<< "$KEY_B64") -pkeyopt digest:"$HASH_ALG" | base64`; + + const result = await execFileAsync("bash", ["-c", script], { + encoding: "utf8", + maxBuffer: 10 * 1024 * 1024, + env: { + DIGEST_B64: digest.toString("base64"), + KEY_B64: privateKey.toString("base64"), + HASH_ALG: hashAlgorithm + } + }); + + if (result.stderr) { + throw new Error(result.stderr); + } + + if (!result.stdout) { + throw new Error("No signature was created. Make sure you are using an appropriate ECC key and hash algorithm."); + } + + return Buffer.from(result.stdout.trim(), "base64"); + }; + + const $verifyEccDigest = async ( + digest: Buffer, + signature: Buffer, + publicKey: Buffer, + hashAlgorithm: SupportedHashAlgorithm + ) => { + const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), "ecc-signature-verification-")); + const pubKeyFile = path.join(tempDir, "public-key.pem"); + const sigFile = path.join(tempDir, "signature.sig"); + const digestFile = path.join(tempDir, "digest.bin"); + + try { + // Write the necessary files + await fs.writeFile(pubKeyFile, publicKey, { mode: 0o600 }); + await fs.writeFile(sigFile, signature, { mode: 0o600 }); + await fs.writeFile(digestFile, digest, { mode: 0o600 }); + } catch { + await fs.rm(tempDir, { recursive: true, force: true }).catch(() => {}); + throw new BadRequestError({ + message: "Failed to verify ECC signature due to internal error." + }); + } + + try { + // Execute OpenSSL verification command + await execFileAsync( + "openssl", + [ + "pkeyutl", + "-verify", + "-in", + digestFile, + "-inkey", + pubKeyFile, + "-pubin", // Important for EC public keys + "-sigfile", + sigFile, + "-pkeyopt", + `digest:${hashAlgorithm}` + ], + { timeout: 15_000 } + ); + + // If we get here, verification succeeded + 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 fs.rm(tempDir, { recursive: true, force: true }).catch(() => {}); + } + }; + + const $verifyRsaDigest = async ( + digest: Buffer, + signature: Buffer, + publicKey: Buffer, + hashAlgorithm: SupportedHashAlgorithm + ) => { + const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), "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 fs.writeFile(publicKeyFile, publicKey, { mode: 0o600 }); + await fs.writeFile(signatureFile, signature, { mode: 0o600 }); + await fs.writeFile(digestFile, digest, { mode: 0o600 }); + } catch { + await fs.rm(tempDir, { recursive: true, force: true }).catch(() => {}); + throw new BadRequestError({ + message: "Failed to verify RSA signature due to internal error." + }); + } + + try { + await execFileAsync( + "openssl", + [ + "pkeyutl", + "-verify", + "-in", + digestFile, + "-inkey", + publicKeyFile, + "-pubin", + "-sigfile", + signatureFile, + "-pkeyopt", + `digest:${hashAlgorithm}` + ], + { timeout: 15_000 } + ); + + // 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 fs.rm(tempDir, { recursive: true, force: true }).catch(() => {}); + } + }; + + 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")) { + if (algorithm.startsWith("RSA")) { crypto.generateKeyPair( "rsa", { - modulusLength: Number(algorithm.split("-")[1]), + modulusLength: Number(algorithm.split("_")[1]), publicKeyEncoding: { type: "spki", format: "pem" }, privateKeyEncoding: { type: "pkcs8", format: "pem" } }, @@ -121,7 +451,7 @@ export const signingService = (algorithm: AsymmetricKeyAlgorithm): TAsymmetricSi } ); } else { - const namedCurve = $getEcCurveName(algorithm); + const { full: namedCurve } = $getEcCurveName(algorithm); crypto.generateKeyPair( "ec", @@ -147,25 +477,6 @@ export const signingService = (algorithm: AsymmetricKeyAlgorithm): TAsymmetricSi }; const getPublicKeyFromPrivateKey = (privateKey: Buffer) => { - if (algorithm.startsWith("rsa")) { - // For RSA keys in PEM format - const privateKeyObj = crypto.createPrivateKey({ - key: privateKey, - format: "pem", - type: "pkcs8" - }); - - const publicKey = crypto.createPublicKey(privateKeyObj).export({ - type: "spki", - format: "pem" - }); - - if (Buffer.isBuffer(publicKey)) { - return publicKey; - } - return Buffer.from(publicKey); - } - const privateKeyObj = crypto.createPrivateKey({ key: privateKey, format: "pem", @@ -174,109 +485,10 @@ export const signingService = (algorithm: AsymmetricKeyAlgorithm): TAsymmetricSi const publicKey = crypto.createPublicKey(privateKeyObj).export({ type: "spki", - format: "pem" + format: "der" }); - if (Buffer.isBuffer(publicKey)) { - return publicKey; - } - return Buffer.from(publicKey); - }; - - const sign = (data: Buffer, privateKey: Buffer, signingAlgorithm: SigningAlgorithm): Buffer => { - $validateAlgorithmWithKeyType(signingAlgorithm); - - const { hashAlgorithm, padding, saltLength } = $getSigningParams(signingAlgorithm); - - const privateKeyObject = crypto.createPrivateKey({ - key: privateKey, - format: "pem", - type: "pkcs8" - }); - - // For RSA signatures - if (signingAlgorithm.startsWith("RSASSA")) { - const signer = crypto.createSign(hashAlgorithm); - signer.update(data); - - if (signingAlgorithm.includes("PSS")) { - // For PSS padding - return signer.sign({ - key: privateKeyObject, - padding, - saltLength - }); - } - // For PKCS1 v1.5 padding - return signer.sign({ - key: privateKeyObject, - padding - }); - } - if (signingAlgorithm.startsWith("ECDSA")) { - // For ECDSA signatures - const signer = crypto.createSign(hashAlgorithm); - signer.update(data); - return signer.sign({ - key: privateKeyObject, - dsaEncoding: "ieee-p1363" // Based on AWS KMS implementation, where ECDSA signatures follow the ANSI X9.62-2005 format, which is equivalent to the IEEE-P1363 format - }); - } - throw new BadRequestError({ - message: `Signing algorithm ${signingAlgorithm} not implemented` - }); - }; - - const verify = (data: Buffer, signature: Buffer, publicKey: Buffer, signingAlgorithm: SigningAlgorithm): boolean => { - try { - $validateAlgorithmWithKeyType(signingAlgorithm); - - const { hashAlgorithm, padding, saltLength } = $getSigningParams(signingAlgorithm); - - // For RSA signatures - if (signingAlgorithm.startsWith("RSASSA")) { - const verifier = crypto.createVerify(hashAlgorithm); - verifier.update(data); - - if (signingAlgorithm.includes("PSS")) { - // For PSS padding - return verifier.verify( - { - key: publicKey.toString(), - padding, - saltLength - }, - signature - ); - } - // For PKCS1 v1.5 padding - return verifier.verify( - { - key: publicKey.toString(), - padding - }, - signature - ); - } - // For ECDSA signatures - if (signingAlgorithm.startsWith("ECDSA")) { - const verifier = crypto.createVerify(hashAlgorithm); - verifier.update(data); - return verifier.verify( - { - key: publicKey.toString(), - dsaEncoding: "ieee-p1363" - }, - signature - ); - } - throw new BadRequestError({ - message: `Verification for algorithm ${signingAlgorithm} not implemented` - }); - } catch (error) { - logger.error(error, "KMS: Failed to verify signature"); - return false; - } + return publicKey; }; return { diff --git a/backend/src/lib/crypto/sign/types.ts b/backend/src/lib/crypto/sign/types.ts index 260953973..aa81b4057 100644 --- a/backend/src/lib/crypto/sign/types.ts +++ b/backend/src/lib/crypto/sign/types.ts @@ -1,16 +1,22 @@ import { z } from "zod"; export type TAsymmetricSignVerifyFns = { - sign: (data: Buffer, key: Buffer, signingAlgorithm: SigningAlgorithm) => Buffer; - verify: (data: Buffer, signature: Buffer, key: Buffer, signingAlgorithm: SigningAlgorithm) => boolean; + 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" + RSA_4096 = "RSA_4096", + ECC_NIST_P256 = "ECC_NIST_P256" } export const AsymmetricKeyAlgorithmEnum = z.enum( diff --git a/backend/src/server/routes/v1/cmek-router.ts b/backend/src/server/routes/v1/cmek-router.ts index 5de731390..7c36c4dd5 100644 --- a/backend/src/server/routes/v1/cmek-router.ts +++ b/backend/src/server/routes/v1/cmek-router.ts @@ -321,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) }), @@ -500,6 +500,7 @@ export const registerCmekRouter = async (server: FastifyZodProvider) => { }), body: z.object({ signingAlgorithm: z.nativeEnum(SigningAlgorithm), + isDigest: z.boolean().optional().default(false).describe(KMS.SIGN.isDigest), data: z .string() .superRefine((data, ctx) => { @@ -524,12 +525,12 @@ export const registerCmekRouter = async (server: FastifyZodProvider) => { handler: async (req) => { const { params: { keyId: inputKeyId }, - body: { data, signingAlgorithm }, + body: { data, signingAlgorithm, isDigest }, permission } = req; const { projectId, ...result } = await server.services.cmek.cmekSign( - { keyId: inputKeyId, data, signingAlgorithm }, + { keyId: inputKeyId, data, signingAlgorithm, isDigest }, permission ); @@ -561,6 +562,7 @@ export const registerCmekRouter = async (server: FastifyZodProvider) => { keyId: z.string().uuid().describe(KMS.VERIFY.keyId) }), body: z.object({ + isDigest: z.boolean().optional().default(false).describe(KMS.VERIFY.isDigest), data: z .string() .superRefine((data, ctx) => { @@ -597,12 +599,12 @@ export const registerCmekRouter = async (server: FastifyZodProvider) => { handler: async (req) => { const { params: { keyId }, - body: { data, signature, signingAlgorithm }, + body: { data, signature, signingAlgorithm, isDigest }, permission } = req; const { projectId, ...result } = await server.services.cmek.cmekVerify( - { keyId, data, signature, signingAlgorithm }, + { keyId, data, signature, signingAlgorithm, isDigest }, permission ); diff --git a/backend/src/services/cmek/cmek-service.ts b/backend/src/services/cmek/cmek-service.ts index 8373dc271..b968a8951 100644 --- a/backend/src/services/cmek/cmek-service.ts +++ b/backend/src/services/cmek/cmek-service.ts @@ -301,13 +301,10 @@ export const cmekServiceFactory = ({ kmsService, kmsDAL, permissionService, proj ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionCmekActions.Read, ProjectPermissionSub.Cmek); const publicKey = await kmsService.getPublicKey({ kmsId: keyId }); - - const base64EncodedPublicKey = publicKey.toString("base64"); - - return { publicKey: base64EncodedPublicKey, projectId: key.projectId }; + return { publicKey: publicKey.toString("base64"), projectId: key.projectId }; }; - const cmekSign = async ({ keyId, data, signingAlgorithm }: TCmekSignDTO, actor: OrgServiceActor) => { + 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` }); @@ -329,7 +326,7 @@ export const cmekServiceFactory = ({ kmsService, kmsDAL, permissionService, proj const sign = await kmsService.signWithKmsKey({ kmsId: keyId }); - const { signature, algorithm } = await sign({ data: Buffer.from(data, "base64"), signingAlgorithm }); + const { signature, algorithm } = await sign({ data: Buffer.from(data, "base64"), signingAlgorithm, isDigest }); return { signature: signature.toString("base64"), @@ -339,7 +336,10 @@ export const cmekServiceFactory = ({ kmsService, kmsDAL, permissionService, proj }; }; - const cmekVerify = async ({ keyId, data, signature, signingAlgorithm }: TCmekVerifyDTO, actor: OrgServiceActor) => { + 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` }); @@ -362,6 +362,7 @@ export const cmekServiceFactory = ({ kmsService, kmsDAL, permissionService, proj const verify = await kmsService.verifyWithKmsKey({ kmsId: keyId, signingAlgorithm }); const { signatureValid, algorithm } = await verify({ + isDigest, data: Buffer.from(data, "base64"), signature: Buffer.from(signature, "base64") }); diff --git a/backend/src/services/cmek/cmek-types.ts b/backend/src/services/cmek/cmek-types.ts index 6af8c44fc..0421bce0e 100644 --- a/backend/src/services/cmek/cmek-types.ts +++ b/backend/src/services/cmek/cmek-types.ts @@ -57,6 +57,7 @@ export type TCmekSignDTO = { keyId: string; data: string; signingAlgorithm: SigningAlgorithm; + isDigest: boolean; }; export type TCmekVerifyDTO = { @@ -64,4 +65,5 @@ export type TCmekVerifyDTO = { data: string; signature: string; signingAlgorithm: SigningAlgorithm; + isDigest: boolean; }; diff --git a/backend/src/services/kms/kms-service.ts b/backend/src/services/kms/kms-service.ts index 78356aefb..754c9be76 100644 --- a/backend/src/services/kms/kms-service.ts +++ b/backend/src/services/kms/kms-service.ts @@ -1,5 +1,3 @@ -import crypto from "node:crypto"; - import slugify from "@sindresorhus/slugify"; import { Knex } from "knex"; import { z } from "zod"; @@ -452,18 +450,7 @@ export const kmsServiceFactory = ({ const keyCipher = symmetricCipherService(SymmetricKeyAlgorithm.AES_GCM_256); const kmsKey = keyCipher.decrypt(kmsDoc.internalKms?.encryptedKey as Buffer, ROOT_ENCRYPTION_KEY); - const publicKeyBuffer = signingService(encryptionAlgorithm).getPublicKeyFromPrivateKey(kmsKey); - - return ( - crypto - .createPublicKey({ - key: publicKeyBuffer, - format: "pem", - type: "spki" - }) - // We return as a DER encoded X.509 certificate (https://datatracker.ietf.org/doc/html/rfc5280) - .export({ type: "spki", format: "der" }) - ); + return signingService(encryptionAlgorithm).getPublicKeyFromPrivateKey(kmsKey); }; const signWithKmsKey = async ({ kmsId }: Pick) => { @@ -479,9 +466,13 @@ export const kmsServiceFactory = ({ const keyCipher = symmetricCipherService(SymmetricKeyAlgorithm.AES_GCM_256); const { sign } = signingService(encryptionAlgorithm); - return ({ data, signingAlgorithm }: Pick) => { + return async ({ + data, + signingAlgorithm, + isDigest + }: Pick) => { const kmsKey = keyCipher.decrypt(kmsDoc.internalKms?.encryptedKey as Buffer, ROOT_ENCRYPTION_KEY); - const signature = sign(data, kmsKey, signingAlgorithm); + const signature = await sign(data, kmsKey, signingAlgorithm, isDigest); return Promise.resolve({ signature, algorithm: signingAlgorithm }); }; @@ -503,11 +494,11 @@ export const kmsServiceFactory = ({ const keyCipher = symmetricCipherService(SymmetricKeyAlgorithm.AES_GCM_256); const { verify, getPublicKeyFromPrivateKey } = signingService(encryptionAlgorithm); - return ({ data, signature }: Pick) => { + return async ({ data, signature, isDigest }: Pick) => { const kmsKey = keyCipher.decrypt(kmsDoc.internalKms?.encryptedKey as Buffer, ROOT_ENCRYPTION_KEY); const publicKey = getPublicKeyFromPrivateKey(kmsKey); - const signatureValid = verify(data, signature, publicKey, signingAlgorithm); + const signatureValid = await verify(data, signature, publicKey, signingAlgorithm, isDigest); return Promise.resolve({ signatureValid, algorithm: signingAlgorithm }); }; }; diff --git a/backend/src/services/kms/kms-types.ts b/backend/src/services/kms/kms-types.ts index 44c1b09c2..ca2401bb6 100644 --- a/backend/src/services/kms/kms-types.ts +++ b/backend/src/services/kms/kms-types.ts @@ -52,6 +52,7 @@ export type TSignWithKmsDTO = { kmsId: string; data: Buffer; signingAlgorithm: SigningAlgorithm; + isDigest: boolean; }; export type TVerifyWithKmsDTO = { @@ -59,6 +60,7 @@ export type TVerifyWithKmsDTO = { data: Buffer; signature: Buffer; signingAlgorithm: SigningAlgorithm; + isDigest: boolean; }; export type TEncryptionWithKeyDTO = { diff --git a/frontend/src/hooks/api/cmeks/types.ts b/frontend/src/hooks/api/cmeks/types.ts index 599249fac..2f6b8788b 100644 --- a/frontend/src/hooks/api/cmeks/types.ts +++ b/frontend/src/hooks/api/cmeks/types.ts @@ -81,8 +81,8 @@ export enum CmekOrderBy { } export enum AsymmetricKeyAlgorithm { - RSA_4096 = "rsa-4096", - ECC_NIST_P256 = "ecc-nist-p256" + RSA_4096 = "RSA_4096", + ECC_NIST_P256 = "ECC_NIST_P256" } // Supported symmetric encrypt/decrypt algorithms