Skip to content

Commit d7b5e73

Browse files
authored
feat: Encryption tests for integration-node (#153)
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.
1 parent dc1f92e commit d7b5e73

File tree

9 files changed

+276
-34
lines changed

9 files changed

+276
-34
lines changed

modules/integration-node/package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,10 @@
1515
"license": "Apache-2.0",
1616
"dependencies": {
1717
"@aws-crypto/client-node": "^0.1.0-preview.2",
18+
"@types/got": "^9.6.2",
1819
"@types/unzipper": "^0.9.1",
1920
"@types/yargs": "^13.0.0",
21+
"got": "^9.6.0",
2022
"tslib": "^1.9.3",
2123
"unzipper": "^0.9.11",
2224
"yargs": "^13.2.2"

modules/integration-node/src/cli.ts

Lines changed: 50 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -15,15 +15,37 @@
1515
*/
1616

1717
import yargs from 'yargs'
18-
import { integrationTestVectors } from './integration_tests'
18+
import { integrationDecryptTestVectors, integrationEncryptTestVectors } from './integration_tests'
1919

20-
const argv = yargs
21-
.option('vectorFile', {
22-
alias: 'v',
23-
describe: 'a vector zip file from aws-encryption-sdk-test-vectors',
24-
demandOption: true,
25-
type: 'string'
26-
})
20+
const cli = yargs
21+
.command('decrypt', 'verify decrypt vectors', y => y
22+
.option('vectorFile', {
23+
alias: 'v',
24+
describe: 'a vector zip file from aws-encryption-sdk-test-vectors',
25+
demandOption: true,
26+
type: 'string'
27+
})
28+
)
29+
.command('encrypt', 'verify encrypt manifest', y => y
30+
.option('manifestFile', {
31+
alias: 'm',
32+
describe: 'a path/url to aws-crypto-tools-test-vector-framework canonical manifest',
33+
demandOption: true,
34+
type: 'string'
35+
})
36+
.option('keyFile', {
37+
alias: 'k',
38+
describe: 'a path/url to aws-crypto-tools-test-vector-framework canonical key list',
39+
demandOption: true,
40+
type: 'string'
41+
})
42+
.option('decryptOracle', {
43+
alias: 'o',
44+
describe: 'a url to the decrypt oracle',
45+
demandOption: true,
46+
type: 'string'
47+
})
48+
)
2749
.option('tolerateFailures', {
2850
alias: 'f',
2951
describe: 'an optional number of failures to tolerate before exiting',
@@ -35,7 +57,24 @@ const argv = yargs
3557
describe: 'an optional test name to execute',
3658
type: 'string'
3759
})
38-
.argv
60+
.demandCommand()
61+
62+
;(async (argv) => {
63+
const { _: [ command ], tolerateFailures, testName } = argv
64+
/* I set the result to 1 so that if I fall through the exit condition is a failure */
65+
let result = 1
66+
if (command === 'decrypt') {
67+
const { vectorFile } = argv
68+
// @ts-ignore
69+
result = await integrationDecryptTestVectors(vectorFile, tolerateFailures, testName)
70+
} else if (command === 'encrypt') {
71+
const { manifestFile, keyFile, decryptOracle } = argv
72+
// @ts-ignore
73+
result = await integrationEncryptTestVectors(manifestFile, keyFile, decryptOracle, tolerateFailures, testName)
74+
} else {
75+
console.log(`Unknown command ${command}`)
76+
cli.showHelp()
77+
}
3978

40-
const { vectorFile, tolerateFailures, testName } = argv
41-
integrationTestVectors(vectorFile, tolerateFailures, testName)
79+
if (result) process.exit(result)
80+
})(cli.argv)

modules/integration-node/src/decrypt_materials_manager_node.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,11 @@ const Bits2RawAesWrappingSuiteIdentifier: {[key: number]: WrappingSuiteIdentifie
3939
256: RawAesWrappingSuiteIdentifier.AES256_GCM_IV12_TAG16_NO_PADDING
4040
}
4141

42+
export function encryptMaterialsManagerNode (keyInfos: KeyInfoTuple[]) {
43+
const [generator, ...children] = keyInfos.map(keyringNode)
44+
return new MultiKeyringNode({ generator, children })
45+
}
46+
4247
export function decryptMaterialsManagerNode (keyInfos: KeyInfoTuple[]) {
4348
const children = keyInfos.map(keyringNode)
4449
return new MultiKeyringNode({ children })
@@ -58,8 +63,8 @@ function keyringNode ([ info, key ]: KeyInfoTuple) {
5863
}
5964

6065
function kmsKeyring (_keyInfo: KmsKeyInfo, key: KMSKey) {
61-
const keyIds = [key['key-id']]
62-
return new KmsKeyringNode({ keyIds })
66+
const generatorKeyId = key['key-id']
67+
return new KmsKeyringNode({ generatorKeyId })
6368
}
6469

6570
function aesKeyring (keyInfo:AesKeyInfo, key: AESKey) {

modules/integration-node/src/get_test_iterator.ts renamed to modules/integration-node/src/get_decrypt_test_iterator.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,13 +15,13 @@
1515

1616
import { Open } from 'unzipper'
1717
import {
18-
ManifestList, // eslint-disable-line no-unused-vars
18+
DecryptManifestList, // eslint-disable-line no-unused-vars
1919
KeyList, // eslint-disable-line no-unused-vars
2020
KeyInfoTuple // eslint-disable-line no-unused-vars
2121
} from './types'
2222
import { Readable } from 'stream' // eslint-disable-line no-unused-vars
2323

24-
export async function getTestVectorIterator (vectorFile: string) {
24+
export async function getDecryptTestVectorIterator (vectorFile: string) {
2525
const centralDirectory = await Open.file(vectorFile)
2626
// @ts-ignore
2727
const filesMap = new Map(centralDirectory.files.map(file => [file.path, file]))
@@ -40,7 +40,7 @@ export async function getTestVectorIterator (vectorFile: string) {
4040
})()
4141

4242
const manifestBuffer = await readUriOnce('manifest.json')
43-
const { keys: keysFile, tests }: ManifestList = JSON.parse(manifestBuffer.toString('utf8'))
43+
const { keys: keysFile, tests }: DecryptManifestList = JSON.parse(manifestBuffer.toString('utf8'))
4444
const keysBuffer = await readUriOnce(keysFile)
4545
const { keys }: KeyList = JSON.parse(keysBuffer.toString('utf8'))
4646

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
/*
2+
* Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License"). You may not use
5+
* this file except in compliance with the License. A copy of the License is
6+
* located at
7+
*
8+
* http://aws.amazon.com/apache2.0/
9+
*
10+
* or in the "license" file accompanying this file. This file is distributed on an
11+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
12+
* implied. See the License for the specific language governing permissions and
13+
* limitations under the License.
14+
*/
15+
16+
import {
17+
EncryptManifestList, // eslint-disable-line no-unused-vars
18+
KeyList, // eslint-disable-line no-unused-vars
19+
KeyInfoTuple // eslint-disable-line no-unused-vars
20+
} from './types'
21+
import { randomBytes } from 'crypto'
22+
import {
23+
AlgorithmSuiteIdentifier, // eslint-disable-line no-unused-vars
24+
EncryptionContext // eslint-disable-line no-unused-vars
25+
} from '@aws-crypto/client-node'
26+
import { URL } from 'url'
27+
import { readFileSync } from 'fs'
28+
import got from 'got'
29+
30+
export async function getEncryptTestVectorIterator (manifestFile: string, keyFile: string) {
31+
const { tests, plaintexts }: EncryptManifestList = await getParsedJSON(manifestFile)
32+
const { keys }: KeyList = await getParsedJSON(keyFile)
33+
34+
const plaintextBytes: {[name: string]: Buffer} = {}
35+
36+
Object
37+
.keys(plaintexts)
38+
.forEach(name => {
39+
plaintextBytes[name] = randomBytes(plaintexts[name])
40+
})
41+
42+
return (function * nextTest (): IterableIterator<EncryptTestVectorInfo> {
43+
for (const [name, testInfo] of Object.entries(tests)) {
44+
const {
45+
plaintext,
46+
'master-keys': masterKeys,
47+
algorithm,
48+
'frame-size': frameLength,
49+
'encryption-context': encryptionContext
50+
} = testInfo
51+
const keysInfo = <KeyInfoTuple[]>masterKeys.map(keyInfo => {
52+
const key = keys[keyInfo.key]
53+
if (!key) throw new Error(`no key for ${name}`)
54+
return [keyInfo, key]
55+
})
56+
57+
/* I'm expecting that the encrypt function will throw if this is not a supported AlgorithmSuiteIdentifier */
58+
const suiteId = <AlgorithmSuiteIdentifier>parseInt(algorithm, 16)
59+
60+
yield {
61+
name,
62+
keysInfo,
63+
plainTextData: plaintextBytes[plaintext],
64+
encryptOp: { suiteId, frameLength, encryptionContext }
65+
}
66+
}
67+
})()
68+
}
69+
70+
export interface EncryptTestVectorInfo {
71+
name: string,
72+
keysInfo: KeyInfoTuple[],
73+
plainTextData: Buffer,
74+
encryptOp: {
75+
suiteId: AlgorithmSuiteIdentifier,
76+
frameLength: number,
77+
encryptionContext: EncryptionContext
78+
}
79+
}
80+
81+
async function getParsedJSON (thing: string) {
82+
try {
83+
const url = new URL(thing)
84+
if (url.protocol === 'file:') {
85+
return jsonAtPath(thing)
86+
} else {
87+
return jsonAtUrl(url)
88+
}
89+
} catch (ex) {
90+
return jsonAtPath(thing)
91+
}
92+
}
93+
async function jsonAtUrl (url: URL) {
94+
const { body } = await got(url)
95+
return JSON.parse(body)
96+
}
97+
98+
function jsonAtPath (path: string) {
99+
const json = readFileSync(path, { encoding: 'utf-8' })
100+
return JSON.parse(json)
101+
}

modules/integration-node/src/index.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,8 @@
1414
* limitations under the License.
1515
*/
1616

17-
export * from './get_test_iterator'
17+
export * from './get_decrypt_test_iterator'
18+
export * from './get_encrypt_test_iterator'
1819
export * from './integration_tests'
1920
export * from './decrypt_materials_manager_node'
2021
export * from './types'

modules/integration-node/src/integration_tests.ts

Lines changed: 77 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -15,13 +15,28 @@
1515

1616
import {
1717
TestVectorInfo, // eslint-disable-line no-unused-vars
18-
getTestVectorIterator
19-
} from './get_test_iterator'
20-
import { decryptMaterialsManagerNode } from './decrypt_materials_manager_node'
21-
import { decrypt } from '@aws-crypto/client-node'
18+
getDecryptTestVectorIterator
19+
} from './get_decrypt_test_iterator'
20+
import {
21+
EncryptTestVectorInfo, // eslint-disable-line no-unused-vars
22+
getEncryptTestVectorIterator
23+
} from './get_encrypt_test_iterator'
24+
import { decryptMaterialsManagerNode, encryptMaterialsManagerNode } from './decrypt_materials_manager_node'
25+
import { decrypt, encrypt, needs } from '@aws-crypto/client-node'
26+
import { URL } from 'url'
27+
import got from 'got'
28+
29+
const notSupportedDecryptMessages = [
30+
'Not supported at this time.'
31+
]
32+
33+
const notSupportedEncryptMessages = [
34+
'frameLength out of bounds: 0 > frameLength >= 4294967295',
35+
'Not supported at this time.'
36+
]
2237

2338
// This is only viable for small streams, if we start get get larger streams, an stream equality should get written
24-
export async function testVector ({ name, keysInfo, plainTextStream, cipherStream }: TestVectorInfo): Promise<TestVectorResults> {
39+
export async function testDecryptVector ({ name, keysInfo, plainTextStream, cipherStream }: TestVectorInfo): Promise<TestVectorResults> {
2540
try {
2641
const cmm = decryptMaterialsManagerNode(keysInfo)
2742
const knowGood: Buffer[] = []
@@ -34,30 +49,80 @@ export async function testVector ({ name, keysInfo, plainTextStream, cipherStrea
3449
}
3550
}
3651

37-
export async function integrationTestVectors (vectorFile: string, tolerateFailures: number = 0, testName?: string) {
38-
const tests = await getTestVectorIterator(vectorFile)
52+
// This is only viable for small streams, if we start get get larger streams, an stream equality should get written
53+
export async function testEncryptVector ({ name, keysInfo, encryptOp, plainTextData }: EncryptTestVectorInfo, decryptOracle: URL): Promise<TestVectorResults> {
54+
try {
55+
const cmm = encryptMaterialsManagerNode(keysInfo)
56+
const { ciphertext } = await encrypt(cmm, plainTextData, encryptOp)
57+
58+
const decryptResponse = await got.post(decryptOracle, {
59+
headers: {
60+
'Content-Type': 'application/octet-stream',
61+
'Accept': 'application/octet-stream'
62+
},
63+
body: ciphertext,
64+
encoding: null
65+
})
66+
needs(decryptResponse.statusCode === 200, 'decrypt failure')
67+
const { body } = decryptResponse
68+
const result = plainTextData.equals(body)
69+
return { result, name }
70+
} catch (err) {
71+
return { result: false, name, err }
72+
}
73+
}
74+
75+
export async function integrationDecryptTestVectors (vectorFile: string, tolerateFailures: number = 0, testName?: string) {
76+
const tests = await getDecryptTestVectorIterator(vectorFile)
77+
let failureCount = 0
78+
for (const test of tests) {
79+
if (testName) {
80+
if (test.name !== testName) continue
81+
}
82+
const { result, name, err } = await testDecryptVector(test)
83+
if (result) {
84+
console.log({ name, result })
85+
} else {
86+
if (err && notSupportedDecryptMessages.includes(err.message)) {
87+
console.log({ name, result: `Not supported: ${err.message}` })
88+
continue
89+
}
90+
console.log({ name, result, err })
91+
}
92+
if (!result) {
93+
failureCount += 1
94+
if (!tolerateFailures) return failureCount
95+
tolerateFailures--
96+
}
97+
}
98+
return failureCount
99+
}
100+
101+
export async function integrationEncryptTestVectors (manifestFile: string, keyFile: string, decryptOracle: string, tolerateFailures: number = 0, testName?: string) {
102+
const decryptOracleUrl = new URL(decryptOracle)
103+
const tests = await getEncryptTestVectorIterator(manifestFile, keyFile)
39104
let failureCount = 0
40105
for (const test of tests) {
41106
if (testName) {
42107
if (test.name !== testName) continue
43108
}
44-
const { result, name, err } = await testVector(test)
109+
const { result, name, err } = await testEncryptVector(test, decryptOracleUrl)
45110
if (result) {
46111
console.log({ name, result })
47112
} else {
48-
if (err && err.message === 'Not supported at this time.') {
49-
console.log({ name, result: 'Not supported at this time.' })
113+
if (err && notSupportedEncryptMessages.includes(err.message)) {
114+
console.log({ name, result: `Not supported: ${err.message}` })
50115
continue
51116
}
52117
console.log({ name, result, err })
53118
}
54119
if (!result) {
55120
failureCount += 1
56-
if (!tolerateFailures) return process.exit(failureCount)
121+
if (!tolerateFailures) return failureCount
57122
tolerateFailures--
58123
}
59124
}
60-
return process.exit(failureCount)
125+
return failureCount
61126
}
62127

63128
interface TestVectorResults {

0 commit comments

Comments
 (0)