feat(kms): sign & verify data

This commit is contained in:
Daniel Hougaard
2025-04-03 07:17:29 +04:00
parent ebcf4761b6
commit c6ca668db9
36 changed files with 1873 additions and 162 deletions
+22 -2
View File
@@ -633,7 +633,8 @@ export const FOLDERS = {
path: "The path to list folders from.",
directory: "The directory to list folders from. (Deprecated in favor of path)",
recursive: "Whether or not to fetch all folders from the specified base path, and all of its subdirectories.",
lastSecretModified: "The timestamp used to filter folders with secrets modified after the specified date. The format for this timestamp is ISO 8601 (e.g. 2025-04-01T09:41:45-04:00)"
lastSecretModified:
"The timestamp used to filter folders with secrets modified after the specified date. The format for this timestamp is ISO 8601 (e.g. 2025-04-01T09:41:45-04:00)"
},
GET_BY_ID: {
folderId: "The ID of the folder to get details."
@@ -1590,7 +1591,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.",
@@ -1623,6 +1625,24 @@ 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)."
},
VERIFY: {
keyId: "The ID of the key to verify the data with.",
data: "The data in string format to be verified (base64 encoded).",
signature: "The signature to be verified (base64 encoded)."
}
};
+4 -2
View File
@@ -1,6 +1,6 @@
import crypto from "crypto";
import { SymmetricEncryption, TSymmetricEncryptionFns } from "./types";
import { SymmetricKeyEncryptDecrypt, 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: SymmetricKeyEncryptDecrypt.AES_GCM_128 | SymmetricKeyEncryptDecrypt.AES_GCM_256
): TSymmetricEncryptionFns => {
const IV_LENGTH = getIvLength();
const TAG_LENGTH = getTagLength();
+1 -1
View File
@@ -1,2 +1,2 @@
export { symmetricCipherService } from "./cipher";
export { SymmetricEncryption } from "./types";
export { AllowedEncryptionKeyAlgorithms, SymmetricKeyEncryptDecrypt } from "./types";
+14 -1
View File
@@ -1,7 +1,20 @@
export enum SymmetricEncryption {
import { z } from "zod";
import { AsymmetricKeySignVerify } from "../sign/types";
// Supported symmetric encrypt/decrypt algorithms
export enum SymmetricKeyEncryptDecrypt {
AES_GCM_256 = "aes-256-gcm",
AES_GCM_128 = "aes-128-gcm"
}
export const SymmetricKeyEncryptDecryptEnum = z.enum(
Object.values(SymmetricKeyEncryptDecrypt) as [string, ...string[]]
).options;
export const AllowedEncryptionKeyAlgorithms = z.enum([
...Object.values(SymmetricKeyEncryptDecrypt),
...Object.values(AsymmetricKeySignVerify)
] as [string, ...string[]]).options;
export type TSymmetricEncryptionFns = {
encrypt: (text: Buffer, key: Buffer) => Buffer;
+2
View File
@@ -0,0 +1,2 @@
export { signingService } from "./signing";
export { AsymmetricKeySignVerify, SigningAlgorithm } from "./types";
+287
View File
@@ -0,0 +1,287 @@
import crypto from "crypto";
import { BadRequestError } from "@app/lib/errors";
import { logger } from "@app/lib/logger";
import { AsymmetricKeySignVerify, SigningAlgorithm, TAsymmetricSignVerifyFns } from "./types";
// Map of signing algorithms to their parameters
interface SigningParams {
hashAlgorithm: string;
padding?: number; // Will use crypto.constants values
saltLength?: number;
}
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 signing algorithm to use
* @returns Object with sign and verify functions
*/
export const signingService = (algorithm: AsymmetricKeySignVerify): TAsymmetricSignVerifyFns => {
const $getSigningParams = (signingAlgorithm: SigningAlgorithm): SigningParams => {
switch (signingAlgorithm) {
// RSA PSS
case SigningAlgorithm.RSASSA_PSS_SHA_256:
return {
hashAlgorithm: "sha256",
padding: crypto.constants.RSA_PKCS1_PSS_PADDING,
saltLength: SHA256_DIGEST_LENGTH
};
case SigningAlgorithm.RSASSA_PSS_SHA_384:
return {
hashAlgorithm: "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:
return {
hashAlgorithm: "sha256",
padding: crypto.constants.RSA_PKCS1_PADDING
};
case SigningAlgorithm.RSASSA_PKCS1_V1_5_SHA_384:
return {
hashAlgorithm: "sha384",
padding: crypto.constants.RSA_PKCS1_PADDING
};
case SigningAlgorithm.RSASSA_PKCS1_V1_5_SHA_512:
return {
hashAlgorithm: "sha512",
padding: crypto.constants.RSA_PKCS1_PADDING
};
// ECDSA
case SigningAlgorithm.ECDSA_SHA_256:
return { hashAlgorithm: "sha256" };
case SigningAlgorithm.ECDSA_SHA_384:
return { hashAlgorithm: "sha384" };
case SigningAlgorithm.ECDSA_SHA_512:
return { hashAlgorithm: "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: AsymmetricKeySignVerify): string => {
// We will support more in the future
switch (keyAlgorithm) {
case AsymmetricKeySignVerify.ECC_NIST_P256:
return "prime256v1";
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 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 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) => {
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",
type: "pkcs8"
});
// Return public key in PEM format for both RSA and EC
const publicKey = crypto.createPublicKey(privateKeyObj).export({
type: "spki",
format: "pem"
});
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
});
}
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()
},
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 {
sign,
verify,
generateAsymmetricPrivateKey,
getPublicKeyFromPrivateKey
};
};
+39
View File
@@ -0,0 +1,39 @@
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;
generateAsymmetricPrivateKey: () => Promise<Buffer>;
getPublicKeyFromPrivateKey: (privateKey: Buffer) => Buffer;
};
// Supported asymmetric key types
export enum AsymmetricKeySignVerify {
RSA_4096 = "rsa-4096",
ECC_NIST_P256 = "ecc-nist-p256"
}
export const AsymmetricKeySignVerifyEnum = z.enum(
Object.values(AsymmetricKeySignVerify) 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_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
// These are deterministic and the output is the same each time for the same input.
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
// None of these are deterministic and include randomness like RSA PSS.
ECDSA_SHA_256 = "ECDSA_SHA_256",
ECDSA_SHA_384 = "ECDSA_SHA_384",
ECDSA_SHA_512 = "ECDSA_SHA_512"
}