From a6985e85219945d3781528fe4de0fcbefbbfb8e2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20=27rysiek=27=20Wo=C5=BAniak?= Date: Thu, 15 Dec 2022 16:56:50 +0000 Subject: [PATCH] cli: more tests for signed-integrity cli done (ref. #66) --- .../plugins/signed-integrity.test.js | 178 ++++++++++++++++-- plugins/basic-integrity/cli.js | 2 - plugins/signed-integrity/cli.js | 35 ++-- 3 files changed, 175 insertions(+), 40 deletions(-) diff --git a/__denotests__/plugins/signed-integrity.test.js b/__denotests__/plugins/signed-integrity.test.js index fd74707..b4a9990 100644 --- a/__denotests__/plugins/signed-integrity.test.js +++ b/__denotests__/plugins/signed-integrity.test.js @@ -2,9 +2,85 @@ import { assert, assertThrows, assertRejects, - assertEquals + assertEquals, + assertStringIncludes } from "https://deno.land/std@0.167.0/testing/asserts.ts"; +// this needs to be the same as the pubkey in: +// ./__denotests__/mocks/keyfile.json +var pubkey = { + "kty": "EC", + "crv": "P-384", + "alg": "ES384", + "x": "rrFawYTuFo8ZjoDxaztUU-c_RAwjw1Y9Tp3j4nH4WsY2Zlizf40Mvz_0BUkVVZCw", + "y": "HaFct6PVK2CQ7ZT2SHClnN-knmGfjY_DFwc6qrAu1s0DFZ8fEUuNdmkTlj9T4NQw", + "key_ops": [ + "verify" + ], + "ext": true +} + +/** + * helper function — decode a b64url-encoded string + */ +let b64urlDecode = (data) => { + data = data.replace(/_/g, '/').replace(/-/g, '+') + data += '='.repeat((4 - (data.length % 4)) % 4) + return atob(data) +} + +/** + * helper function — verify a signed JWT using a provided key + */ +let verifySignedJWT = async (jwt, key) => { + + // working in sections + jwt = jwt.split('.') + + // sections cannot be empty + if ( (jwt[0].length == 0) || (jwt[1].length == 0) || (jwt[2].length == 0) ) { + throw new Error('JWT is invalid, one or more sections are empty.') + } + + // load the key + key = await crypto.subtle.importKey( + "jwk", + key, + { + name: 'ECDSA', + namedCurve: 'P-384' + }, + true, + ['verify'] + ) + + // checking the header + var header = JSON.parse(b64urlDecode(jwt[0])) + if (!("alg" in header) || (header.alg != "ES384")) { + throw new Error('Expected header to contain alg field set to "ES384"') + } + + // get the signature in a usable format + var signature = Uint8Array + .from( + Array + .from(b64urlDecode(jwt[2])) + .map(e=>e.charCodeAt(0)) + ).buffer + + // return the result of signature verification + return await crypto.subtle.verify( + { + name: "ECDSA", + hash: {name: "SHA-384"} + }, + key, + signature, + new TextEncoder("utf-8").encode(jwt[0] + '.' + jwt[1]) + ) +} + + Deno.test("plugin loads", async () => { const bi = await import('../../plugins/signed-integrity/cli.js') assert("name" in bi) @@ -94,42 +170,104 @@ Deno.test("gen-integrity verifies arguments are sane", async () => { }, Error, "Failed to load private key from 'irrelevant': No such file or directory") }); -Deno.test("gen-integrity handles paths in a sane way", async () => { +Deno.test("gen-integrity handles paths correctly", async () => { const bi = await import('../../plugins/signed-integrity/cli.js') const gi = bi.actions["gen-integrity"] assertRejects(async ()=>{ await gi.run(['./'], 'non-existent') }, Error, "Failed to load private key from 'non-existent'") + assertRejects(async ()=>{ + await gi.run(['./'], './') + }, Error, "ailed to load private key from './': Is a directory") assertEquals( await gi.run(['./'], './__denotests__/mocks/keyfile.json'), '{}' ) - /*assertEquals( - )await gi.run(['./__denotests__/mocks/hello.txt'], './__denotests__/mocks/keyfile.json'), - '{}' - )*/ - //assertEquals(await gi.run(['./__denotests__/mocks/hello.txt']), '{"./__denotests__/mocks/hello.txt":["sha256-uU0nuZNNPgilLlLX2n2r+sSE7+N6U4DukIj3rOLvzek="]}') - //assertEquals(await gi.run(['./', './__denotests__/mocks/hello.txt']), '{"./__denotests__/mocks/hello.txt":["sha256-uU0nuZNNPgilLlLX2n2r+sSE7+N6U4DukIj3rOLvzek="]}') + assertStringIncludes( + await gi.run(['./__denotests__/mocks/hello.txt'], './__denotests__/mocks/keyfile.json'), + '"./__denotests__/mocks/hello.txt":"eyJhbGciOiAiRVMzODQifQ.eyJpbnRlZ3JpdHkiOiAic2hhMjU2LXVVMG51Wk5OUGdpbExsTFgybjJyK3NTRTcrTjZVNER1a0lqM3JPTHZ6ZWs9In0.' + ) }); -/*Deno.test("gen-integrity handles algos argument in a sane way", async () => { +Deno.test("gen-integrity handles algos argument correctly", async () => { const bi = await import('../../plugins/signed-integrity/cli.js') const gi = bi.actions["gen-integrity"] assertRejects(async ()=>{ - await gi.run(['./__denotests__/mocks/hello.txt'], ['BAD-ALG']) + await gi.run(['./__denotests__/mocks/hello.txt'], './__denotests__/mocks/keyfile.json', ['BAD-ALG']) }, Error, 'Unrecognized algorithm name') - assertEquals(await gi.run(['./__denotests__/mocks/hello.txt'], ['SHA-256']), '{"./__denotests__/mocks/hello.txt":["sha256-uU0nuZNNPgilLlLX2n2r+sSE7+N6U4DukIj3rOLvzek="]}') - assertEquals(await gi.run(['./__denotests__/mocks/hello.txt'], ['SHA-384']), '{"./__denotests__/mocks/hello.txt":["sha384-/b2OdaZ/KfcBpOBAOF4uI5hjA+oQI5IRr5B/y7g1eLPkF8txzmRu/QgZ3YwIjeG9"]}') - assertEquals(await gi.run(['./__denotests__/mocks/hello.txt'], ['SHA-512']), '{"./__denotests__/mocks/hello.txt":["sha512-MJ7MSJwS1utMxA9QyQLytNDtd+5RGnx6m808qG1M2G+YndNbxf9JlnDaNCVbRbDP2DDoH2Bdz33FVC6TrpzXbw=="]}') - assertEquals(await gi.run(['./__denotests__/mocks/hello.txt'], ['SHA-256', 'SHA-384', 'SHA-512']), '{"./__denotests__/mocks/hello.txt":["sha256-uU0nuZNNPgilLlLX2n2r+sSE7+N6U4DukIj3rOLvzek=","sha384-/b2OdaZ/KfcBpOBAOF4uI5hjA+oQI5IRr5B/y7g1eLPkF8txzmRu/QgZ3YwIjeG9","sha512-MJ7MSJwS1utMxA9QyQLytNDtd+5RGnx6m808qG1M2G+YndNbxf9JlnDaNCVbRbDP2DDoH2Bdz33FVC6TrpzXbw=="]}') + + // helper function + let getGeneratedTestIntegrity = async (algos) => { + let integrity = JSON.parse(await gi.run( + ['./__denotests__/mocks/hello.txt'], + './__denotests__/mocks/keyfile.json', + algos) + ) + integrity = b64urlDecode(integrity["./__denotests__/mocks/hello.txt"].split('.')[1]) + return integrity + } + + assertEquals(await getGeneratedTestIntegrity(['SHA-256']), '{"integrity": "sha256-uU0nuZNNPgilLlLX2n2r+sSE7+N6U4DukIj3rOLvzek="}') + assertEquals(await getGeneratedTestIntegrity(['SHA-384']), '{"integrity": "sha384-/b2OdaZ/KfcBpOBAOF4uI5hjA+oQI5IRr5B/y7g1eLPkF8txzmRu/QgZ3YwIjeG9"}') + assertEquals(await getGeneratedTestIntegrity(['SHA-512']), '{"integrity": "sha512-MJ7MSJwS1utMxA9QyQLytNDtd+5RGnx6m808qG1M2G+YndNbxf9JlnDaNCVbRbDP2DDoH2Bdz33FVC6TrpzXbw=="}') + assertEquals(await getGeneratedTestIntegrity(['SHA-256', 'SHA-384', 'SHA-512']), '{"integrity": "sha256-uU0nuZNNPgilLlLX2n2r+sSE7+N6U4DukIj3rOLvzek= sha384-/b2OdaZ/KfcBpOBAOF4uI5hjA+oQI5IRr5B/y7g1eLPkF8txzmRu/QgZ3YwIjeG9 sha512-MJ7MSJwS1utMxA9QyQLytNDtd+5RGnx6m808qG1M2G+YndNbxf9JlnDaNCVbRbDP2DDoH2Bdz33FVC6TrpzXbw=="}') }); -Deno.test("gen-integrity handles output argument in a sane way", async () => { +Deno.test("gen-integrity text output is correct", async () => { const bi = await import('../../plugins/signed-integrity/cli.js') const gi = bi.actions["gen-integrity"] - assertEquals(await gi.run(['./__denotests__/mocks/hello.txt'], ['SHA-256'], 'text'), './__denotests__/mocks/hello.txt: sha256-uU0nuZNNPgilLlLX2n2r+sSE7+N6U4DukIj3rOLvzek=\n') - assertEquals(await gi.run(['./__denotests__/mocks/hello.txt'], ['SHA-384'], 'text'), './__denotests__/mocks/hello.txt: sha384-/b2OdaZ/KfcBpOBAOF4uI5hjA+oQI5IRr5B/y7g1eLPkF8txzmRu/QgZ3YwIjeG9\n') - assertEquals(await gi.run(['./__denotests__/mocks/hello.txt'], ['SHA-512'], 'text'), './__denotests__/mocks/hello.txt: sha512-MJ7MSJwS1utMxA9QyQLytNDtd+5RGnx6m808qG1M2G+YndNbxf9JlnDaNCVbRbDP2DDoH2Bdz33FVC6TrpzXbw==\n') - assertEquals(await gi.run(['./__denotests__/mocks/hello.txt'], ['SHA-256', 'SHA-384', 'SHA-512'], 'text'), './__denotests__/mocks/hello.txt: sha256-uU0nuZNNPgilLlLX2n2r+sSE7+N6U4DukIj3rOLvzek= sha384-/b2OdaZ/KfcBpOBAOF4uI5hjA+oQI5IRr5B/y7g1eLPkF8txzmRu/QgZ3YwIjeG9 sha512-MJ7MSJwS1utMxA9QyQLytNDtd+5RGnx6m808qG1M2G+YndNbxf9JlnDaNCVbRbDP2DDoH2Bdz33FVC6TrpzXbw==\n') + + let getGeneratedTestIntegrity = async (algos) => { + let result = await gi.run( + ['./__denotests__/mocks/hello.txt'], + './__denotests__/mocks/keyfile.json', + algos, + 'text' + ) + result = result.split(' ') + return [result[0], b64urlDecode(result[1].split('.')[1])] + } + + assertEquals( + await getGeneratedTestIntegrity(['SHA-256']), + [ + "./__denotests__/mocks/hello.txt:", + '{"integrity": "sha256-uU0nuZNNPgilLlLX2n2r+sSE7+N6U4DukIj3rOLvzek="}' + ] + ) + assertEquals( + await getGeneratedTestIntegrity(['SHA-384']), + [ + "./__denotests__/mocks/hello.txt:", + '{"integrity": "sha384-/b2OdaZ/KfcBpOBAOF4uI5hjA+oQI5IRr5B/y7g1eLPkF8txzmRu/QgZ3YwIjeG9"}' + ] + ) + assertEquals( + await getGeneratedTestIntegrity(['SHA-512']), + [ + "./__denotests__/mocks/hello.txt:", + '{"integrity": "sha512-MJ7MSJwS1utMxA9QyQLytNDtd+5RGnx6m808qG1M2G+YndNbxf9JlnDaNCVbRbDP2DDoH2Bdz33FVC6TrpzXbw=="}' + ] + ) + + + assertEquals( + await getGeneratedTestIntegrity(['SHA-256', 'SHA-384', 'SHA-512']), + [ + "./__denotests__/mocks/hello.txt:", + '{"integrity": "sha256-uU0nuZNNPgilLlLX2n2r+sSE7+N6U4DukIj3rOLvzek= sha384-/b2OdaZ/KfcBpOBAOF4uI5hjA+oQI5IRr5B/y7g1eLPkF8txzmRu/QgZ3YwIjeG9 sha512-MJ7MSJwS1utMxA9QyQLytNDtd+5RGnx6m808qG1M2G+YndNbxf9JlnDaNCVbRbDP2DDoH2Bdz33FVC6TrpzXbw=="}' + ] + ) }); -*/ + +// TODO: "files" output mode, which will require mocking file writing routines + +Deno.test("gen-integrity signs the data correctly", async () => { + const bi = await import('../../plugins/signed-integrity/cli.js') + const gi = bi.actions["gen-integrity"] + let jwt = JSON.parse(await gi.run(['./__denotests__/mocks/hello.txt'], './__denotests__/mocks/keyfile.json')) + assert( + await verifySignedJWT( + jwt['./__denotests__/mocks/hello.txt'], + pubkey)) +}) diff --git a/plugins/basic-integrity/cli.js b/plugins/basic-integrity/cli.js index 83ad926..93e3b0c 100644 --- a/plugins/basic-integrity/cli.js +++ b/plugins/basic-integrity/cli.js @@ -47,8 +47,6 @@ let getFileIntegrity = async (path, algos) => { // are we working with a file? if (fileInfo.isFile) { - //console.log(`+-- reading: ${path}`) - // initialize content = new Uint8Array() var buf = new Uint8Array(1000); diff --git a/plugins/signed-integrity/cli.js b/plugins/signed-integrity/cli.js index e6c2328..d0063a3 100644 --- a/plugins/signed-integrity/cli.js +++ b/plugins/signed-integrity/cli.js @@ -106,6 +106,7 @@ let getPubkey = async (keyfile) => { let getFileIntegrity = async (path, algos) => { var result = [] + var content = false // open the file and get some info on it const file = await Deno.open( @@ -117,10 +118,8 @@ let getFileIntegrity = async (path, algos) => { // are we working with a file? if (fileInfo.isFile) { - //console.log(`+-- reading: ${path}`) - // initialize - var content = new Uint8Array() + content = new Uint8Array() var buf = new Uint8Array(1000); // read the first batch @@ -145,22 +144,8 @@ let getFileIntegrity = async (path, algos) => { // read some more numread = file.readSync(buf); } - //console.log(' +-- done.') - - //console.log('+-- calculating digests') - for (const algo of algos) { - //console.log(` +-- ${algo}`) - var digest = algo.toLowerCase().replace('-', '') + '-' + binToBase64(await crypto.subtle.digest(algo, content)) - //console.log(digest) - result.push(digest) - } - //console.log(`+-- file done: ${path}`) - - // we are not working with a file - } else { - result = false; } - + // putting this in a try-catch block as the file // is apparently being auto-closed? // https://issueantenna.com/repo/denoland/deno/issues/15442 @@ -168,6 +153,20 @@ let getFileIntegrity = async (path, algos) => { await file.close(); } catch (BadResource) {} + // did we get any content? + if (typeof content != "boolean") { + for (const algo of algos) { + //console.log(` +-- ${algo}`) + var digest = algo.toLowerCase().replace('-', '') + '-' + binToBase64(await crypto.subtle.digest(algo, content)) + //console.log(digest) + result.push(digest) + } + // no content means not a file + } else { + // so no result + result = false + } + // return the result return result }