Skip to content

feat: Encryption tests for integration-node #153

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Jul 21, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions modules/integration-node/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
61 changes: 50 additions & 11 deletions modules/integration-node/src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand All @@ -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)
Original file line number Diff line number Diff line change
Expand Up @@ -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 })
Expand All @@ -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) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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]))
Expand All @@ -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'))

Expand Down
101 changes: 101 additions & 0 deletions modules/integration-node/src/get_encrypt_test_iterator.ts
Original file line number Diff line number Diff line change
@@ -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<EncryptTestVectorInfo> {
for (const [name, testInfo] of Object.entries(tests)) {
const {
plaintext,
'master-keys': masterKeys,
algorithm,
'frame-size': frameLength,
'encryption-context': encryptionContext
} = testInfo
const keysInfo = <KeyInfoTuple[]>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 = <AlgorithmSuiteIdentifier>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)
}
3 changes: 2 additions & 1 deletion modules/integration-node/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'
89 changes: 77 additions & 12 deletions modules/integration-node/src/integration_tests.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<TestVectorResults> {
export async function testDecryptVector ({ name, keysInfo, plainTextStream, cipherStream }: TestVectorInfo): Promise<TestVectorResults> {
try {
const cmm = decryptMaterialsManagerNode(keysInfo)
const knowGood: Buffer[] = []
Expand All @@ -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<TestVectorResults> {
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)) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we want to enumerate the vectors that are not supported? Right now, if something returns not supported, but should be, it would slip by. That being said, this can go into a new issue, and doesn't need to be fixed here.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

or is that what "tolerateFailures" does?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

tolerateFailures is just a count of failures to tolerate. CI should be 0.

Your comment is true. The hope is to come up with a results manifest. That could be compared and determine success.

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 {
Expand Down
Loading