diff --git a/backend/src/lib/crypto/sign/signing.ts b/backend/src/lib/crypto/sign/signing.ts index 9be62c6e6..4b3cdea64 100644 --- a/backend/src/lib/crypto/sign/signing.ts +++ b/backend/src/lib/crypto/sign/signing.ts @@ -24,10 +24,17 @@ enum SupportedHashAlgorithm { SHA512 = "sha512" } +const COMMAND_TIMEOUT = 15_000; + const SHA256_DIGEST_LENGTH = 32; const SHA384_DIGEST_LENGTH = 48; const SHA512_DIGEST_LENGTH = 64; +const makeTempDir = async (name: string) => { + const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), `${name}-${crypto.randomBytes(16).toString("hex")}-`)); + return tempDir; +}; + /** * Service for cryptographic signing and verification operations using asymmetric keys * @@ -117,52 +124,106 @@ 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 - } - }); + const tempDir = await makeTempDir("kms-rsa-sign"); + const digestPath = path.join(tempDir, "digest.bin"); + const keyPath = path.join(tempDir, "private_key.pem"); + const sigPath = path.join(tempDir, "signature.bin"); - if (result.stderr) { - throw new Error(result.stderr); - } + try { + await fs.writeFile(digestPath, digest, { mode: 0o600 }); + await fs.writeFile(keyPath, privateKey, { mode: 0o600 }); - if (!result.stdout) { - throw new Error( - "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." + const { stderr } = await execFileAsync( + "openssl", + [ + "pkeyutl", + "-sign", + "-in", + digestPath, + "-inkey", + keyPath, + "-pkeyopt", + `digest:${hashAlgorithm}`, + "-out", + sigPath + ], + { + maxBuffer: 10 * 1024 * 1024, + timeout: COMMAND_TIMEOUT + } ); - } - return Buffer.from(result.stdout.trim(), "base64"); + 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 fs.rm(tempDir, { recursive: true, force: true }).catch(() => {}); + } }; 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 tempDir = await makeTempDir("ecc-sign"); + const digestPath = path.join(tempDir, "digest.bin"); + const keyPath = path.join(tempDir, "private_key.pem"); + const sigPath = path.join(tempDir, "signature.bin"); - 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 + try { + await fs.writeFile(digestPath, digest); + await fs.writeFile(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" + }); } - }); - if (result.stderr) { - throw new Error(result.stderr); + 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 fs.rm(tempDir, { recursive: true, force: true }).catch(() => {}); } - - 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 ( @@ -171,25 +232,16 @@ export const signingService = (algorithm: AsymmetricKeyAlgorithm): TAsymmetricSi publicKey: Buffer, hashAlgorithm: SupportedHashAlgorithm ) => { - const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), "ecc-signature-verification-")); + const tempDir = await makeTempDir("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", [ @@ -205,10 +257,9 @@ export const signingService = (algorithm: AsymmetricKeyAlgorithm): TAsymmetricSi "-pkeyopt", `digest:${hashAlgorithm}` ], - { timeout: 15_000 } + { timeout: COMMAND_TIMEOUT } ); - // If we get here, verification succeeded return true; } catch (error) { const err = error as { stderr: string }; @@ -231,7 +282,7 @@ export const signingService = (algorithm: AsymmetricKeyAlgorithm): TAsymmetricSi publicKey: Buffer, hashAlgorithm: SupportedHashAlgorithm ) => { - const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), "kms-signature-verification-")); + const tempDir = await makeTempDir("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"); @@ -240,14 +291,7 @@ export const signingService = (algorithm: AsymmetricKeyAlgorithm): TAsymmetricSi 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", [ @@ -263,7 +307,7 @@ export const signingService = (algorithm: AsymmetricKeyAlgorithm): TAsymmetricSi "-pkeyopt", `digest:${hashAlgorithm}` ], - { timeout: 15_000 } + { timeout: COMMAND_TIMEOUT } ); // it'll throw if the verification was not successful diff --git a/backend/src/server/routes/v1/cmek-router.ts b/backend/src/server/routes/v1/cmek-router.ts index 7c36c4dd5..64f47f980 100644 --- a/backend/src/server/routes/v1/cmek-router.ts +++ b/backend/src/server/routes/v1/cmek-router.ts @@ -412,7 +412,8 @@ export const registerCmekRouter = async (server: FastifyZodProvider) => { rateLimit: readLimit }, schema: { - description: "Get the public key for a KMS key that is used for signing and verifying data.", + 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) }), @@ -501,17 +502,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) => { - if (!isBase64(data)) { - ctx.addIssue({ - code: z.ZodIssueCode.custom, - message: "data must be base64 encoded" - }); - } - }) - .describe(KMS.SIGN.data) + data: base64Schema.describe(KMS.SIGN.data) }), response: { 200: z.object({ @@ -563,28 +554,8 @@ export const registerCmekRouter = async (server: FastifyZodProvider) => { }), body: z.object({ isDigest: z.boolean().optional().default(false).describe(KMS.VERIFY.isDigest), - data: z - .string() - .superRefine((data, ctx) => { - if (!isBase64(data)) { - ctx.addIssue({ - code: z.ZodIssueCode.custom, - message: "data must be base64 encoded" - }); - } - }) - .describe(KMS.VERIFY.data), - signature: z - .string() - .superRefine((data, ctx) => { - if (!isBase64(data)) { - ctx.addIssue({ - code: z.ZodIssueCode.custom, - message: "signature must be base64 encoded" - }); - } - }) - .describe(KMS.VERIFY.signature), + data: base64Schema.describe(KMS.VERIFY.data), + signature: base64Schema.describe(KMS.VERIFY.signature), signingAlgorithm: z.nativeEnum(SigningAlgorithm) }), response: {