From b274fe3fa8c8907cc7abfcffda58e099bf9a064f Mon Sep 17 00:00:00 2001 From: Redi Kurti Date: Sun, 22 May 2022 13:06:16 +0200 Subject: [PATCH] add sign and verify to transit api fixed formatting issues Update tests/util.spec.ts --- src/engines/transit.ts | 73 +++++++++++++++++++++ src/engines/transit_types-ti.ts | 108 ++++++++++++++++++++++++++++++++ src/engines/transit_types.ts | 91 +++++++++++++++++++++++++++ tests/engines/transit.spec.ts | 103 ++++++++++++++++++++++++++++++ tests/util.spec.ts | 2 +- 5 files changed, 376 insertions(+), 1 deletion(-) diff --git a/src/engines/transit.ts b/src/engines/transit.ts index b84db8e..01b35ba 100644 --- a/src/engines/transit.ts +++ b/src/engines/transit.ts @@ -13,11 +13,19 @@ import { ITransitEncryptOptionsSingle, ITransitEncryptResponseBatch, ITransitEncryptResponseSingle, + ITransitSignOptionsSingle, + ITransitSignOptionsBatch, ITransitExportOptions, ITransitExportResponse, ITransitListResponse, ITransitReadResponse, ITransitUpdateOptions, + ITransitSignResponseSingle, + ITransitSignResponseBatch, + ITransitVerifyOptionsSingle, + ITransitVerifyResponseSingle, + ITransitVerifyOptionsBatch, + ITransitVerifyResponseBatch, } from "./transit_types"; import { validateKeyName } from "../util"; @@ -210,6 +218,71 @@ export class TransitVaultClient extends AbstractVaultClient { }); } + /** + * Returns the cryptographic signature of the given data using the named key and the specified hash algorithm + * @see https://www.vaultproject.io/api-docs/secret/transit#sign-data + * @param key + * @param options + * + * @param options.input Specifies the base64 encoded input data. One of input or batch_input must be supplied. + * @param options.batch_input Specifies a list of items for processing. When this parameter is set, any supplied 'input' or 'context' parameters will be ignored. + * @param options.key_version Specifies the version of the key to use for encryption. If not set, uses the latest version. Must be greater than or equal to the key's min_encryption_version, if set. + * @param options.context Base64 encoded context for key derivation. Required if key derivation is enabled. + * @param options.hash_algorithm Specifies the hash algorithm to use for supporting key types. + * @param options.prehashed Set to true when the input is already hashed. When set, input is expected to be base64-encoded binary hashed data, not hex-formatted. + * @param options.signature_algorithm When using a RSA key, specifies the RSA signature algorithm to use for signing. + * @param options.marshaling_algorithm Specifies the way in which the signature should be marshaled. + */ + public async sign(key: string, options: ITransitSignOptionsSingle): Promise; + public async sign(key: string, options: ITransitSignOptionsBatch): Promise; + public async sign( + key: string, + options: ITransitSignOptionsSingle | ITransitSignOptionsBatch, + ): Promise { + validateKeyName(key); + return this.rawWrite(["sign", key], options).then((res) => { + if ("batch_input" in options) { + transitChecker.ITransitSignResponseBatch.check(res); + } else { + transitChecker.ITransitSignResponseSingle.check(res); + } + return res; + }); + } + + /** + * Returns whether the provided signature is valid for the given data. + * @see https://www.vaultproject.io/api-docs/secret/transit#verify-signed-data + * @param key + * @param options + * + * @param options.input Specifies the base64 encoded input data. One of input or batch_input must be supplied. + * @param options.batch_input Specifies a list of items for processing. When this parameter is set, any supplied 'input', 'hmac' or 'signature' parameters will be ignored. All items in the batch must consistently supply either 'hmac' or 'signature' parameters. + * @param options.signature Specifies the signature output from the /transit/sign function. Either this must be supplied or hmac must be supplied. + * @param options.hmac Specifies the signature output from the /transit/hmac function. Either this must be supplied or signature must be supplied. + * @param options.hash_algorithm Specifies the hash algorithm to use. + * @param options.context Base64 encoded context for key derivation. + * @param options.prehashed Set to true when the input is already hashed. + * @param options.signature_algorithm When using a RSA key, specifies the RSA signature algorithm to use for signature verification. + * @param options.marshaling_algorithm Specifies the way in which the signature was originally marshaled. + */ + public async verify(key: string, options: ITransitVerifyOptionsSingle): Promise; + public async verify(key: string, options: ITransitVerifyOptionsBatch): Promise; + public async verify( + key: string, + options: ITransitVerifyOptionsSingle | ITransitVerifyOptionsBatch, + ): Promise { + validateKeyName(key); + return this.rawWrite(["verify", key], options).then((res) => { + if ("batch_input" in options) { + transitChecker.ITransitVerifyResponseBatch.check(res); + } else { + transitChecker.ITransitVerifyResponseSingle.check(res); + } + return res; + }); + } + /** * Encrypts the specified plaintext with default options using the named key. * @param key diff --git a/src/engines/transit_types-ti.ts b/src/engines/transit_types-ti.ts index ca7c9cf..a370dbb 100644 --- a/src/engines/transit_types-ti.ts +++ b/src/engines/transit_types-ti.ts @@ -6,6 +6,12 @@ import * as t from "ts-interface-checker"; export const ITransitKeyType = t.union(t.lit("aes256-gcm96"), t.lit("chacha20-poly1305"), t.lit("d25519"), t.lit("ecdsa-p256"), t.lit("rsa-2048"), t.lit("rsa-4096")); +export const ITransitSignHashAlgorithm = t.union(t.lit("sha1"), t.lit("sha2-224"), t.lit("sha2-256"), t.lit("sha2-384"), t.lit("sha2-512"), t.lit("sha3-224"), t.lit("sha3-256"), t.lit("sha3-384"), t.lit("sha3-512")); + +export const ITransitSignSignatureAlgorithm = t.union(t.lit("pss"), t.lit("pkcs1v15")); + +export const ITransitSignMarshalingAlgorithm = t.union(t.lit("asn1"), t.lit("jws")); + export const ITransitBatchPlaintext = t.array(t.iface([], { "plaintext": "string", "context": t.opt("string"), @@ -21,6 +27,29 @@ export const ITransitBatchCiphertext = t.array(t.iface([], { "context": t.opt("string"), })); +export const ITransitSignBatchInput = t.array(t.iface([], { + "input": "string", + "context": t.opt("string"), +})); + +export const ITransitVerifyBatchInputSignature = t.array(t.iface([], { + "input": "string", + "signature": "string", + "context": t.opt("string"), +})); + +export const ITransitVerifyBatchInputHMAC = t.array(t.iface([], { + "input": "string", + "hmac": "string", + "context": t.opt("string"), +})); + +export const ITransitSignBatchOutput = t.array(t.iface([], { + "signature": t.opt("string"), + "publickey": t.opt("string"), + "error": t.opt("string"), +})); + export const ITransitCreateOptions = t.iface([], { "convergent_encryption": t.opt("boolean"), "derived": t.opt("boolean"), @@ -136,11 +165,82 @@ export const ITransitDecryptRawResponseBatch = t.iface([], { }), }); +export const ITransitSignOptionsSingle = t.iface([], { + "key_version": t.opt("number"), + "hash_algorithm": t.opt("ITransitSignHashAlgorithm"), + "input": "string", + "context": t.opt("string"), + "prehashed": t.opt("boolean"), + "signature_algorithm": t.opt("ITransitSignSignatureAlgorithm"), + "marshaling_algorithm": t.opt("ITransitSignMarshalingAlgorithm"), +}); + +export const ITransitSignOptionsBatch = t.iface([], { + "key_version": t.opt("number"), + "hash_algorithm": t.opt("ITransitSignHashAlgorithm"), + "batch_input": "ITransitSignBatchInput", + "prehashed": t.opt("boolean"), + "signature_algorithm": t.opt("ITransitSignSignatureAlgorithm"), + "marshaling_algorithm": t.opt("ITransitSignMarshalingAlgorithm"), +}); + +export const ITransitSignResponseSingle = t.iface([], { + "data": t.iface([], { + "signature": t.union("string", "undefined"), + }), +}); + +export const ITransitSignResponseBatch = t.iface([], { + "data": t.iface([], { + "batch_results": "ITransitSignBatchOutput", + }), +}); + +export const ITransitVerifyOptionsSingle = t.iface([], { + "input": "string", + "signature": t.opt("string"), + "hmac": t.opt("string"), + "hash_algorithm": t.opt("ITransitSignHashAlgorithm"), + "context": t.opt("string"), + "prehashed": t.opt("boolean"), + "signature_algorithm": t.opt("ITransitSignSignatureAlgorithm"), + "marshaling_algorithm": t.opt("ITransitSignMarshalingAlgorithm"), +}); + +export const ITransitVerifyOptionsBatch = t.iface([], { + "batch_input": t.union("ITransitVerifyBatchInputSignature", "ITransitVerifyBatchInputHMAC"), + "hash_algorithm": t.opt("ITransitSignHashAlgorithm"), + "prehashed": t.opt("boolean"), + "signature_algorithm": t.opt("ITransitSignSignatureAlgorithm"), + "marshaling_algorithm": t.opt("ITransitSignMarshalingAlgorithm"), +}); + +export const ITransitVerifyResponseSingle = t.iface([], { + "data": t.iface([], { + "valid": "boolean", + }), +}); + +export const ITransitVerifyResponseBatch = t.iface([], { + "data": t.iface([], { + "batch_results": t.array(t.iface([], { + "valid": "boolean", + })), + }), +}); + const exportedTypeSuite: t.ITypeSuite = { ITransitKeyType, + ITransitSignHashAlgorithm, + ITransitSignSignatureAlgorithm, + ITransitSignMarshalingAlgorithm, ITransitBatchPlaintext, ITransitRawBatchPlaintext, ITransitBatchCiphertext, + ITransitSignBatchInput, + ITransitVerifyBatchInputSignature, + ITransitVerifyBatchInputHMAC, + ITransitSignBatchOutput, ITransitCreateOptions, ITransitReadResponse, ITransitListResponse, @@ -156,5 +256,13 @@ const exportedTypeSuite: t.ITypeSuite = { ITransitDecryptResponseSingle, ITransitDecryptResponseBatch, ITransitDecryptRawResponseBatch, + ITransitSignOptionsSingle, + ITransitSignOptionsBatch, + ITransitSignResponseSingle, + ITransitSignResponseBatch, + ITransitVerifyOptionsSingle, + ITransitVerifyOptionsBatch, + ITransitVerifyResponseSingle, + ITransitVerifyResponseBatch, }; export default exportedTypeSuite; diff --git a/src/engines/transit_types.ts b/src/engines/transit_types.ts index 82b1d91..04e63e3 100644 --- a/src/engines/transit_types.ts +++ b/src/engines/transit_types.ts @@ -1,4 +1,16 @@ export type ITransitKeyType = "aes256-gcm96" | "chacha20-poly1305" | "d25519" | "ecdsa-p256" | "rsa-2048" | "rsa-4096"; +export type ITransitSignHashAlgorithm = + | "sha1" + | "sha2-224" + | "sha2-256" + | "sha2-384" + | "sha2-512" + | "sha3-224" + | "sha3-256" + | "sha3-384" + | "sha3-512"; +export type ITransitSignSignatureAlgorithm = "pss" | "pkcs1v15"; +export type ITransitSignMarshalingAlgorithm = "asn1" | "jws"; export type ITransitBatchPlaintext = Array<{ plaintext: string; @@ -12,6 +24,25 @@ export type ITransitBatchCiphertext = Array<{ ciphertext: string; context?: string; }>; +export type ITransitSignBatchInput = Array<{ + input: string; + context?: string; +}>; +export type ITransitVerifyBatchInputSignature = Array<{ + input: string; + signature: string; + context?: string; +}>; +export type ITransitVerifyBatchInputHMAC = Array<{ + input: string; + hmac: string; + context?: string; +}>; +export type ITransitSignBatchOutput = Array<{ + signature?: string; + publickey?: string; + error?: string; +}>; export interface ITransitCreateOptions { convergent_encryption?: boolean; @@ -127,3 +158,63 @@ export interface ITransitDecryptRawResponseBatch { batch_results: ITransitRawBatchPlaintext; }; } + +export interface ITransitSignOptionsSingle { + key_version?: number; + hash_algorithm?: ITransitSignHashAlgorithm; + input: string; + context?: string; + prehashed?: boolean; + signature_algorithm?: ITransitSignSignatureAlgorithm; + marshaling_algorithm?: ITransitSignMarshalingAlgorithm; +} +export interface ITransitSignOptionsBatch { + key_version?: number; + hash_algorithm?: ITransitSignHashAlgorithm; + batch_input: ITransitSignBatchInput; + prehashed?: boolean; + signature_algorithm?: ITransitSignSignatureAlgorithm; + marshaling_algorithm?: ITransitSignMarshalingAlgorithm; +} + +export interface ITransitSignResponseSingle { + data: { + signature: string | undefined; + }; +} +export interface ITransitSignResponseBatch { + data: { + batch_results: ITransitSignBatchOutput; + }; +} + +export interface ITransitVerifyOptionsSingle { + input: string; + signature?: string; + hmac?: string; + hash_algorithm?: ITransitSignHashAlgorithm; + context?: string; + prehashed?: boolean; + signature_algorithm?: ITransitSignSignatureAlgorithm; + marshaling_algorithm?: ITransitSignMarshalingAlgorithm; +} +export interface ITransitVerifyOptionsBatch { + batch_input: ITransitVerifyBatchInputSignature | ITransitVerifyBatchInputHMAC; + hash_algorithm?: ITransitSignHashAlgorithm; + prehashed?: boolean; + signature_algorithm?: ITransitSignSignatureAlgorithm; + marshaling_algorithm?: ITransitSignMarshalingAlgorithm; +} + +export interface ITransitVerifyResponseSingle { + data: { + valid: boolean; + }; +} +export interface ITransitVerifyResponseBatch { + data: { + batch_results: Array<{ + valid: boolean; + }>; + }; +} diff --git a/tests/engines/transit.spec.ts b/tests/engines/transit.spec.ts index 2b40f45..6bfe76b 100644 --- a/tests/engines/transit.spec.ts +++ b/tests/engines/transit.spec.ts @@ -155,5 +155,108 @@ describe("Transit Vault Client", () => { expect(err).toBeInstanceOf(VaultDecryptionKeyNotFoundError); } }); + + test("successfully create and read signing key", async () => { + await client.create("test_sign", { + type: "ecdsa-p256", + }); + const res = await client.read("test_sign"); + }); + + test("should respond with 400 if using a key that does not support signing", async () => { + await client.create("test_cannot_sign", { + type: "aes256-gcm96", + }); + + const text = Buffer.from("test123").toString("base64"); + + try { + await client.sign("test_cannot_sign", { input: text }); + } catch (err) { + expect(err.response.statusCode).toEqual(400); + } + }); + + test("successfully sign and verify", async () => { + const text = Buffer.from("test123").toString("base64"); + + const signature = await client + .sign("test_sign", { + input: text, + }) + .then((res) => res.data.signature); + const res = await client.verify("test_sign", { + input: text, + signature, + }); + }); + + test("successfully sign and verify (batch)", async () => { + const text = Buffer.from("test123").toString("base64"); + + const signatures = await client + .sign("test_sign", { + batch_input: [ + { + input: text, + }, + { + input: text, + }, + ], + }) + .then((res) => res.data.batch_results); + + expect(signatures[0].error).toBeUndefined(); + expect(signatures[1].error).toBeUndefined(); + + expect(signatures[0].signature).toBeDefined(); + expect(signatures[1].signature).toBeDefined(); + + const verifications = await client + .verify("test_sign", { + batch_input: [ + { + input: text, + signature: signatures[0].signature ? signatures[0].signature.toString() : "false_signature", + }, + { + input: text, + signature: "false_signature", + }, + ], + }) + .then((res) => res.data.batch_results); + + expect(verifications[0].valid).toBe(true); + expect(verifications[1].valid).toBe(false); + }); + + test("should respond with 404 if the keyID for signing does not exist", async () => { + const text = Buffer.from("404test").toString("base64"); + try { + await client.sign("unknownkey", { input: text }); + } catch (err) { + expect(err).toBeInstanceOf(VaultDecryptionKeyNotFoundError); + } + }); + + test("should respond with 404 if the keyID for signing does not exist (batch)", async () => { + const text = Buffer.from("404test").toString("base64"); + try { + await client.sign("unknownkey", { + batch_input: [ + { + input: text, + }, + { + input: text, + }, + ], + }); + } catch (err) { + expect(err).toBeInstanceOf(VaultDecryptionKeyNotFoundError); + } + }); }); }); diff --git a/tests/util.spec.ts b/tests/util.spec.ts index 95cf1c9..6a34c9e 100644 --- a/tests/util.spec.ts +++ b/tests/util.spec.ts @@ -18,6 +18,6 @@ describe("URL Resolve", () => { test("URL Resolve fails with invalid URL", () => { const resolveWrap = () => resolveURL("ht/example", "/test///", "////foo/baz/lul", "/bar///"); - expect(resolveWrap).toThrowError(new TypeError("Invalid URL: ht/example/test/foo/baz/lul/bar")); + expect(resolveWrap).toThrowError(new RegExp("Invalid URL.*")); }); });