From 3bb9c9a596d6618d0cb86e28523c73a18f0c09ed Mon Sep 17 00:00:00 2001 From: seebees Date: Thu, 18 Jul 2019 15:28:15 -0700 Subject: [PATCH] feat: Encryption tests for integration-node https://github.com/awslabs/aws-crypto-tools-test-vector-framework defines an encryption manifest. This manifest should be used to maintain cross compatibility of language implementations. Then integrate this into the CI integration tests. --- modules/integration-node/package.json | 2 + modules/integration-node/src/cli.ts | 61 +++++++++-- .../src/decrypt_materials_manager_node.ts | 9 +- ...erator.ts => get_decrypt_test_iterator.ts} | 6 +- .../src/get_encrypt_test_iterator.ts | 101 ++++++++++++++++++ modules/integration-node/src/index.ts | 3 +- .../integration-node/src/integration_tests.ts | 89 ++++++++++++--- modules/integration-node/src/types.ts | 35 +++++- package.json | 4 +- 9 files changed, 276 insertions(+), 34 deletions(-) rename modules/integration-node/src/{get_test_iterator.ts => get_decrypt_test_iterator.ts} (91%) create mode 100644 modules/integration-node/src/get_encrypt_test_iterator.ts diff --git a/modules/integration-node/package.json b/modules/integration-node/package.json index 273629128..eda0375a6 100644 --- a/modules/integration-node/package.json +++ b/modules/integration-node/package.json @@ -15,8 +15,10 @@ "license": "Apache-2.0", "dependencies": { "@aws-crypto/client-node": "^0.1.0-preview.2", + "@types/got": "^9.6.2", "@types/unzipper": "^0.9.1", "@types/yargs": "^13.0.0", + "got": "^9.6.0", "tslib": "^1.9.3", "unzipper": "^0.9.11", "yargs": "^13.2.2" diff --git a/modules/integration-node/src/cli.ts b/modules/integration-node/src/cli.ts index b39bf6d8b..2c89de560 100644 --- a/modules/integration-node/src/cli.ts +++ b/modules/integration-node/src/cli.ts @@ -15,15 +15,37 @@ */ import yargs from 'yargs' -import { integrationTestVectors } from './integration_tests' +import { integrationDecryptTestVectors, integrationEncryptTestVectors } from './integration_tests' -const argv = yargs - .option('vectorFile', { - alias: 'v', - describe: 'a vector zip file from aws-encryption-sdk-test-vectors', - demandOption: true, - type: 'string' - }) +const cli = yargs + .command('decrypt', 'verify decrypt vectors', y => y + .option('vectorFile', { + alias: 'v', + describe: 'a vector zip file from aws-encryption-sdk-test-vectors', + demandOption: true, + type: 'string' + }) + ) + .command('encrypt', 'verify encrypt manifest', y => y + .option('manifestFile', { + alias: 'm', + describe: 'a path/url to aws-crypto-tools-test-vector-framework canonical manifest', + demandOption: true, + type: 'string' + }) + .option('keyFile', { + alias: 'k', + describe: 'a path/url to aws-crypto-tools-test-vector-framework canonical key list', + demandOption: true, + type: 'string' + }) + .option('decryptOracle', { + alias: 'o', + describe: 'a url to the decrypt oracle', + demandOption: true, + type: 'string' + }) + ) .option('tolerateFailures', { alias: 'f', describe: 'an optional number of failures to tolerate before exiting', @@ -35,7 +57,24 @@ const argv = yargs describe: 'an optional test name to execute', type: 'string' }) - .argv + .demandCommand() + +;(async (argv) => { + const { _: [ command ], tolerateFailures, testName } = argv + /* I set the result to 1 so that if I fall through the exit condition is a failure */ + let result = 1 + if (command === 'decrypt') { + const { vectorFile } = argv + // @ts-ignore + result = await integrationDecryptTestVectors(vectorFile, tolerateFailures, testName) + } else if (command === 'encrypt') { + const { manifestFile, keyFile, decryptOracle } = argv + // @ts-ignore + result = await integrationEncryptTestVectors(manifestFile, keyFile, decryptOracle, tolerateFailures, testName) + } else { + console.log(`Unknown command ${command}`) + cli.showHelp() + } -const { vectorFile, tolerateFailures, testName } = argv -integrationTestVectors(vectorFile, tolerateFailures, testName) + if (result) process.exit(result) +})(cli.argv) diff --git a/modules/integration-node/src/decrypt_materials_manager_node.ts b/modules/integration-node/src/decrypt_materials_manager_node.ts index 869b657e0..42eadd969 100644 --- a/modules/integration-node/src/decrypt_materials_manager_node.ts +++ b/modules/integration-node/src/decrypt_materials_manager_node.ts @@ -39,6 +39,11 @@ const Bits2RawAesWrappingSuiteIdentifier: {[key: number]: WrappingSuiteIdentifie 256: RawAesWrappingSuiteIdentifier.AES256_GCM_IV12_TAG16_NO_PADDING } +export function encryptMaterialsManagerNode (keyInfos: KeyInfoTuple[]) { + const [generator, ...children] = keyInfos.map(keyringNode) + return new MultiKeyringNode({ generator, children }) +} + export function decryptMaterialsManagerNode (keyInfos: KeyInfoTuple[]) { const children = keyInfos.map(keyringNode) return new MultiKeyringNode({ children }) @@ -58,8 +63,8 @@ function keyringNode ([ info, key ]: KeyInfoTuple) { } function kmsKeyring (_keyInfo: KmsKeyInfo, key: KMSKey) { - const keyIds = [key['key-id']] - return new KmsKeyringNode({ keyIds }) + const generatorKeyId = key['key-id'] + return new KmsKeyringNode({ generatorKeyId }) } function aesKeyring (keyInfo:AesKeyInfo, key: AESKey) { diff --git a/modules/integration-node/src/get_test_iterator.ts b/modules/integration-node/src/get_decrypt_test_iterator.ts similarity index 91% rename from modules/integration-node/src/get_test_iterator.ts rename to modules/integration-node/src/get_decrypt_test_iterator.ts index 5c9630b5a..33a2a699f 100644 --- a/modules/integration-node/src/get_test_iterator.ts +++ b/modules/integration-node/src/get_decrypt_test_iterator.ts @@ -15,13 +15,13 @@ import { Open } from 'unzipper' import { - ManifestList, // eslint-disable-line no-unused-vars + DecryptManifestList, // eslint-disable-line no-unused-vars KeyList, // eslint-disable-line no-unused-vars KeyInfoTuple // eslint-disable-line no-unused-vars } from './types' import { Readable } from 'stream' // eslint-disable-line no-unused-vars -export async function getTestVectorIterator (vectorFile: string) { +export async function getDecryptTestVectorIterator (vectorFile: string) { const centralDirectory = await Open.file(vectorFile) // @ts-ignore const filesMap = new Map(centralDirectory.files.map(file => [file.path, file])) @@ -40,7 +40,7 @@ export async function getTestVectorIterator (vectorFile: string) { })() const manifestBuffer = await readUriOnce('manifest.json') - const { keys: keysFile, tests }: ManifestList = JSON.parse(manifestBuffer.toString('utf8')) + const { keys: keysFile, tests }: DecryptManifestList = JSON.parse(manifestBuffer.toString('utf8')) const keysBuffer = await readUriOnce(keysFile) const { keys }: KeyList = JSON.parse(keysBuffer.toString('utf8')) diff --git a/modules/integration-node/src/get_encrypt_test_iterator.ts b/modules/integration-node/src/get_encrypt_test_iterator.ts new file mode 100644 index 000000000..ca9ee7217 --- /dev/null +++ b/modules/integration-node/src/get_encrypt_test_iterator.ts @@ -0,0 +1,101 @@ +/* + * Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use + * this file except in compliance with the License. A copy of the License is + * located at + * + * http://aws.amazon.com/apache2.0/ + * + * or in the "license" file accompanying this file. This file is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + * implied. See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { + EncryptManifestList, // eslint-disable-line no-unused-vars + KeyList, // eslint-disable-line no-unused-vars + KeyInfoTuple // eslint-disable-line no-unused-vars +} from './types' +import { randomBytes } from 'crypto' +import { + AlgorithmSuiteIdentifier, // eslint-disable-line no-unused-vars + EncryptionContext // eslint-disable-line no-unused-vars +} from '@aws-crypto/client-node' +import { URL } from 'url' +import { readFileSync } from 'fs' +import got from 'got' + +export async function getEncryptTestVectorIterator (manifestFile: string, keyFile: string) { + const { tests, plaintexts }: EncryptManifestList = await getParsedJSON(manifestFile) + const { keys }: KeyList = await getParsedJSON(keyFile) + + const plaintextBytes: {[name: string]: Buffer} = {} + + Object + .keys(plaintexts) + .forEach(name => { + plaintextBytes[name] = randomBytes(plaintexts[name]) + }) + + return (function * nextTest (): IterableIterator { + for (const [name, testInfo] of Object.entries(tests)) { + const { + plaintext, + 'master-keys': masterKeys, + algorithm, + 'frame-size': frameLength, + 'encryption-context': encryptionContext + } = testInfo + const keysInfo = masterKeys.map(keyInfo => { + const key = keys[keyInfo.key] + if (!key) throw new Error(`no key for ${name}`) + return [keyInfo, key] + }) + + /* I'm expecting that the encrypt function will throw if this is not a supported AlgorithmSuiteIdentifier */ + const suiteId = parseInt(algorithm, 16) + + yield { + name, + keysInfo, + plainTextData: plaintextBytes[plaintext], + encryptOp: { suiteId, frameLength, encryptionContext } + } + } + })() +} + +export interface EncryptTestVectorInfo { + name: string, + keysInfo: KeyInfoTuple[], + plainTextData: Buffer, + encryptOp: { + suiteId: AlgorithmSuiteIdentifier, + frameLength: number, + encryptionContext: EncryptionContext + } +} + +async function getParsedJSON (thing: string) { + try { + const url = new URL(thing) + if (url.protocol === 'file:') { + return jsonAtPath(thing) + } else { + return jsonAtUrl(url) + } + } catch (ex) { + return jsonAtPath(thing) + } +} +async function jsonAtUrl (url: URL) { + const { body } = await got(url) + return JSON.parse(body) +} + +function jsonAtPath (path: string) { + const json = readFileSync(path, { encoding: 'utf-8' }) + return JSON.parse(json) +} diff --git a/modules/integration-node/src/index.ts b/modules/integration-node/src/index.ts index d0dc2063b..1a7c82040 100644 --- a/modules/integration-node/src/index.ts +++ b/modules/integration-node/src/index.ts @@ -14,7 +14,8 @@ * limitations under the License. */ -export * from './get_test_iterator' +export * from './get_decrypt_test_iterator' +export * from './get_encrypt_test_iterator' export * from './integration_tests' export * from './decrypt_materials_manager_node' export * from './types' diff --git a/modules/integration-node/src/integration_tests.ts b/modules/integration-node/src/integration_tests.ts index ef6b8a772..2b684123e 100644 --- a/modules/integration-node/src/integration_tests.ts +++ b/modules/integration-node/src/integration_tests.ts @@ -15,13 +15,28 @@ import { TestVectorInfo, // eslint-disable-line no-unused-vars - getTestVectorIterator -} from './get_test_iterator' -import { decryptMaterialsManagerNode } from './decrypt_materials_manager_node' -import { decrypt } from '@aws-crypto/client-node' + getDecryptTestVectorIterator +} from './get_decrypt_test_iterator' +import { + EncryptTestVectorInfo, // eslint-disable-line no-unused-vars + getEncryptTestVectorIterator +} from './get_encrypt_test_iterator' +import { decryptMaterialsManagerNode, encryptMaterialsManagerNode } from './decrypt_materials_manager_node' +import { decrypt, encrypt, needs } from '@aws-crypto/client-node' +import { URL } from 'url' +import got from 'got' + +const notSupportedDecryptMessages = [ + 'Not supported at this time.' +] + +const notSupportedEncryptMessages = [ + 'frameLength out of bounds: 0 > frameLength >= 4294967295', + 'Not supported at this time.' +] // This is only viable for small streams, if we start get get larger streams, an stream equality should get written -export async function testVector ({ name, keysInfo, plainTextStream, cipherStream }: TestVectorInfo): Promise { +export async function testDecryptVector ({ name, keysInfo, plainTextStream, cipherStream }: TestVectorInfo): Promise { try { const cmm = decryptMaterialsManagerNode(keysInfo) const knowGood: Buffer[] = [] @@ -34,30 +49,80 @@ export async function testVector ({ name, keysInfo, plainTextStream, cipherStrea } } -export async function integrationTestVectors (vectorFile: string, tolerateFailures: number = 0, testName?: string) { - const tests = await getTestVectorIterator(vectorFile) +// This is only viable for small streams, if we start get get larger streams, an stream equality should get written +export async function testEncryptVector ({ name, keysInfo, encryptOp, plainTextData }: EncryptTestVectorInfo, decryptOracle: URL): Promise { + try { + const cmm = encryptMaterialsManagerNode(keysInfo) + const { ciphertext } = await encrypt(cmm, plainTextData, encryptOp) + + const decryptResponse = await got.post(decryptOracle, { + headers: { + 'Content-Type': 'application/octet-stream', + 'Accept': 'application/octet-stream' + }, + body: ciphertext, + encoding: null + }) + needs(decryptResponse.statusCode === 200, 'decrypt failure') + const { body } = decryptResponse + const result = plainTextData.equals(body) + return { result, name } + } catch (err) { + return { result: false, name, err } + } +} + +export async function integrationDecryptTestVectors (vectorFile: string, tolerateFailures: number = 0, testName?: string) { + const tests = await getDecryptTestVectorIterator(vectorFile) + let failureCount = 0 + for (const test of tests) { + if (testName) { + if (test.name !== testName) continue + } + const { result, name, err } = await testDecryptVector(test) + if (result) { + console.log({ name, result }) + } else { + if (err && notSupportedDecryptMessages.includes(err.message)) { + console.log({ name, result: `Not supported: ${err.message}` }) + continue + } + console.log({ name, result, err }) + } + if (!result) { + failureCount += 1 + if (!tolerateFailures) return failureCount + tolerateFailures-- + } + } + return failureCount +} + +export async function integrationEncryptTestVectors (manifestFile: string, keyFile: string, decryptOracle: string, tolerateFailures: number = 0, testName?: string) { + const decryptOracleUrl = new URL(decryptOracle) + const tests = await getEncryptTestVectorIterator(manifestFile, keyFile) let failureCount = 0 for (const test of tests) { if (testName) { if (test.name !== testName) continue } - const { result, name, err } = await testVector(test) + const { result, name, err } = await testEncryptVector(test, decryptOracleUrl) if (result) { console.log({ name, result }) } else { - if (err && err.message === 'Not supported at this time.') { - console.log({ name, result: 'Not supported at this time.' }) + if (err && notSupportedEncryptMessages.includes(err.message)) { + console.log({ name, result: `Not supported: ${err.message}` }) continue } console.log({ name, result, err }) } if (!result) { failureCount += 1 - if (!tolerateFailures) return process.exit(failureCount) + if (!tolerateFailures) return failureCount tolerateFailures-- } } - return process.exit(failureCount) + return failureCount } interface TestVectorResults { diff --git a/modules/integration-node/src/types.ts b/modules/integration-node/src/types.ts index 88a1e6a6e..d2e3f7346 100644 --- a/modules/integration-node/src/types.ts +++ b/modules/integration-node/src/types.ts @@ -13,11 +13,21 @@ * limitations under the License. */ -export interface ManifestList { - manifest: Manifest +import { EncryptionContext } from '@aws-crypto/client-node' // eslint-disable-line no-unused-vars + +export interface DecryptManifestList { + manifest: DecryptManifest + client: Client + keys: string + tests: {[testName: string]: DecryptTest} +} + +export interface EncryptManifestList { + manifest: EncryptManifest client: Client keys: string - tests: {[key: string]: Test} + plaintexts: {[name: string]: number} + tests: {[testName: string]: EncryptTest} } export interface KeyList { @@ -29,6 +39,15 @@ interface Manifest { type: string version: number } + +interface DecryptManifest extends Manifest { + type: 'awses-decrypt' +} + +interface EncryptManifest extends Manifest { + type: 'awses-encrypt' +} + interface Client { name: string version: string @@ -62,7 +81,15 @@ export interface KmsKeyInfo extends KeyInfo { key: string } -interface Test { +interface EncryptTest { + plaintext: string + algorithm: string + 'frame-size': number + 'encryption-context': EncryptionContext + 'master-keys': (RsaKeyInfo|AesKeyInfo|KmsKeyInfo)[] +} + +interface DecryptTest { plaintext: string ciphertext: string 'master-keys': (RsaKeyInfo|AesKeyInfo|KmsKeyInfo)[] diff --git a/package.json b/package.json index c6369b87e..cd989a7d1 100644 --- a/package.json +++ b/package.json @@ -19,7 +19,9 @@ "coverage": "run-s coverage-*", "test": "npm run lint && npm run build && npm run coverage", "integration-browser": "npm run build; lerna run build_fixtures --stream --no-prefix -- -- -v ../../aws-encryption-sdk-test-vectors/vectors/awses-decrypt/python-1.3.8.zip -k", - "integration-node": "npm run build; lerna run integration_node --stream --no-prefix -- -- -v ../../aws-encryption-sdk-test-vectors/vectors/awses-decrypt/python-1.3.8.zip", + "integration-node-decrypt": "npm run build; lerna run integration_node --stream --no-prefix -- -- decrypt -v ../../aws-encryption-sdk-test-vectors/vectors/awses-decrypt/python-1.3.8.zip", + "integration-node-encrypt": "npm run build; lerna run integration_node --stream --no-prefix -- -- encrypt -m 'https://raw.githubusercontent.com/awslabs/aws-crypto-tools-test-vector-framework/master/features/CANONICAL-GENERATED-MANIFESTS/0003-awses-message-encryption.v1.json' -k 'https://raw.githubusercontent.com/awslabs/aws-crypto-tools-test-vector-framework/master/features/CANONICAL-GENERATED-MANIFESTS/0002-keys.v1.json' -o 'https://xi1mwx3ttb.execute-api.us-west-2.amazonaws.com/api/v0/decrypt'", + "integration-node": "run-s integration-node-*", "integration": "run-s integration-*" }, "repository": {