kopia lustrzana https://gitlab.com/rysiekpl/libresilient
343 wiersze
12 KiB
JavaScript
343 wiersze
12 KiB
JavaScript
import {
|
|
assert,
|
|
assertRejects,
|
|
assertEquals,
|
|
assertStringIncludes,
|
|
assertObjectMatch
|
|
} 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('../cli.js')
|
|
assert("name" in bi)
|
|
assert(bi.name == "signed-integrity")
|
|
assert("description" in bi)
|
|
assert("actions" in bi)
|
|
});
|
|
|
|
Deno.test("gen-integrity action defined", async () => {
|
|
const bi = await import('../cli.js')
|
|
assert("gen-integrity" in bi.actions)
|
|
|
|
const gi = bi.actions["gen-integrity"]
|
|
assert("run" in gi)
|
|
assert("description" in gi)
|
|
assert("arguments" in gi)
|
|
|
|
const gia = gi.arguments
|
|
assert("_" in gia)
|
|
assert("keyfile" in gia)
|
|
assert("algorithm" in gia)
|
|
assert("output" in gia)
|
|
assert("extension" in gia)
|
|
|
|
assert("name" in gia._)
|
|
assert("description" in gia._)
|
|
|
|
assert("description" in gia.keyfile)
|
|
assert("string" in gia.keyfile)
|
|
assert(!("collect" in gia.keyfile))
|
|
assert(gia.keyfile.string)
|
|
|
|
assert("description" in gia.algorithm)
|
|
assert("collect" in gia.algorithm)
|
|
assert(gia.algorithm.collect)
|
|
assert("string" in gia.algorithm)
|
|
assert(gia.algorithm.string)
|
|
|
|
assert("description" in gia.output)
|
|
assert(!("collect" in gia.output))
|
|
assert("string" in gia.output)
|
|
assert(gia.output.string)
|
|
|
|
assert("description" in gia.extension)
|
|
assert(!("collect" in gia.extension))
|
|
assert("string" in gia.extension)
|
|
assert(gia.extension.string)
|
|
});
|
|
|
|
// this is a separate test in order to catch any changing defaults
|
|
Deno.test("gen-integrity action defaults", async () => {
|
|
const bi = await import('../cli.js')
|
|
const gia = bi.actions["gen-integrity"].arguments
|
|
assert("default" in gia.algorithm)
|
|
assert(gia.algorithm.default == "SHA-256")
|
|
assert("default" in gia.output)
|
|
assert(gia.output.default == "json")
|
|
assert("default" in gia.extension)
|
|
assert(gia.extension.default == ".integrity")
|
|
});
|
|
|
|
Deno.test("gen-integrity verifies arguments are sane", async () => {
|
|
const bi = await import('../cli.js')
|
|
const gi = bi.actions["gen-integrity"]
|
|
assertRejects(gi.run, Error, "Expected non-empty list of paths to process.")
|
|
assertRejects(async ()=>{
|
|
await gi.run(['no-such-file'])
|
|
}, Error, "No keyfile provided.")
|
|
assertRejects(async ()=>{
|
|
await gi.run(['no-such-file'], 'irrelevant')
|
|
}, Error, "No such file or directory")
|
|
assertRejects(async ()=>{
|
|
await gi.run(['irrelevant'], 'irrelevant', [])
|
|
}, Error, "Expected non-empty list of algorithms to use.")
|
|
assertRejects(async ()=>{
|
|
await gi.run(['irrelevant'], 'irrelevant', ['SHA-384'], false)
|
|
}, Error, "Expected 'json', 'text', or 'files' as output type.")
|
|
assertRejects(async ()=>{
|
|
await gi.run(['irrelevant'], 'irrelevant', ['SHA-384'], 'files', false)
|
|
}, Error, "No extension provided.")
|
|
// extension is ignored even if incorrect when output is not 'files'
|
|
assertRejects(async ()=>{
|
|
await gi.run(['irrelevant'], 'irrelevant', ['SHA-384'], 'text', false)
|
|
}, Error, "Failed to load private key from 'irrelevant': No such file or directory")
|
|
assertRejects(async ()=>{
|
|
await gi.run(['irrelevant'], 'irrelevant', ['SHA-384'], 'json', false)
|
|
}, Error, "Failed to load private key from 'irrelevant': No such file or directory")
|
|
});
|
|
|
|
Deno.test("gen-integrity handles paths correctly", async () => {
|
|
const bi = await import('../cli.js')
|
|
const gi = bi.actions["gen-integrity"]
|
|
const mh = import.meta.resolve('./mocks/hello.txt').replace(/^file:\/\//gi, "")
|
|
const mk = import.meta.resolve('./mocks/keyfile.json').replace(/^file:\/\//gi, "")
|
|
assertRejects(async ()=>{
|
|
await gi.run(['./'], 'non-existent')
|
|
}, Error, "Failed to load private key from 'non-existent'")
|
|
assertRejects(async ()=>{
|
|
await gi.run(['./'], './')
|
|
}, Error, "Failed to load private key from './': Is a directory")
|
|
assertEquals(
|
|
await gi.run(['./'], mk),
|
|
'{}'
|
|
)
|
|
assertStringIncludes(
|
|
await gi.run([mh], mk),
|
|
'"' + mh + '":"eyJhbGciOiAiRVMzODQifQ.eyJpbnRlZ3JpdHkiOiAic2hhMjU2LXVVMG51Wk5OUGdpbExsTFgybjJyK3NTRTcrTjZVNER1a0lqM3JPTHZ6ZWs9In0.'
|
|
)
|
|
});
|
|
|
|
Deno.test("gen-integrity handles algos argument correctly", async () => {
|
|
const bi = await import('../cli.js')
|
|
const gi = bi.actions["gen-integrity"]
|
|
const mh = import.meta.resolve('./mocks/hello.txt').replace(/^file:\/\//gi, "")
|
|
const mk = import.meta.resolve('./mocks/keyfile.json').replace(/^file:\/\//gi, "")
|
|
assertRejects(async ()=>{
|
|
await gi.run([mh], mk, ['BAD-ALG'])
|
|
}, Error, 'Unrecognized algorithm name')
|
|
|
|
// helper function
|
|
let getGeneratedTestIntegrity = async (algos) => {
|
|
let integrity = JSON.parse(await gi.run(
|
|
[mh],
|
|
mk,
|
|
algos)
|
|
)
|
|
integrity = b64urlDecode(integrity[mh].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 text output is correct", async () => {
|
|
const bi = await import('../cli.js')
|
|
const gi = bi.actions["gen-integrity"]
|
|
const mh = import.meta.resolve('./mocks/hello.txt').replace(/^file:\/\//gi, "")
|
|
const mk = import.meta.resolve('./mocks/keyfile.json').replace(/^file:\/\//gi, "")
|
|
|
|
let getGeneratedTestIntegrity = async (algos) => {
|
|
let result = await gi.run(
|
|
[mh],
|
|
mk,
|
|
algos,
|
|
'text'
|
|
)
|
|
result = result.split(' ')
|
|
return [result[0], b64urlDecode(result[1].split('.')[1])]
|
|
}
|
|
|
|
assertEquals(
|
|
await getGeneratedTestIntegrity(['SHA-256']),
|
|
[
|
|
mh + ":",
|
|
'{"integrity": "sha256-uU0nuZNNPgilLlLX2n2r+sSE7+N6U4DukIj3rOLvzek="}'
|
|
]
|
|
)
|
|
assertEquals(
|
|
await getGeneratedTestIntegrity(['SHA-384']),
|
|
[
|
|
mh + ":",
|
|
'{"integrity": "sha384-/b2OdaZ/KfcBpOBAOF4uI5hjA+oQI5IRr5B/y7g1eLPkF8txzmRu/QgZ3YwIjeG9"}'
|
|
]
|
|
)
|
|
assertEquals(
|
|
await getGeneratedTestIntegrity(['SHA-512']),
|
|
[
|
|
mh + ":",
|
|
'{"integrity": "sha512-MJ7MSJwS1utMxA9QyQLytNDtd+5RGnx6m808qG1M2G+YndNbxf9JlnDaNCVbRbDP2DDoH2Bdz33FVC6TrpzXbw=="}'
|
|
]
|
|
)
|
|
|
|
|
|
assertEquals(
|
|
await getGeneratedTestIntegrity(['SHA-256', 'SHA-384', 'SHA-512']),
|
|
[
|
|
mh + ":",
|
|
'{"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('../cli.js')
|
|
const gi = bi.actions["gen-integrity"]
|
|
const mh = import.meta.resolve('./mocks/hello.txt').replace(/^file:\/\//gi, "")
|
|
const mk = import.meta.resolve('./mocks/keyfile.json').replace(/^file:\/\//gi, "")
|
|
let jwt = JSON.parse(await gi.run([mh], mk))
|
|
assert(
|
|
await verifySignedJWT(
|
|
jwt[mh],
|
|
pubkey))
|
|
})
|
|
|
|
Deno.test("get-pubkey works correctly", async () => {
|
|
const bi = await import('../cli.js')
|
|
const gp = bi.actions["get-pubkey"]
|
|
const mk = import.meta.resolve('./mocks/keyfile.json').replace(/^file:\/\//gi, "")
|
|
assertRejects(gp.run, Error, "No keyfile provided.")
|
|
assertRejects(async ()=>{
|
|
await gp.run('no-such-file')
|
|
}, Error, "No such file or directory")
|
|
assertRejects(async ()=>{
|
|
await gp.run(['no-such-file'])
|
|
}, Error, "No such file or directory")
|
|
assertEquals(
|
|
await gp.run(mk),
|
|
'{"kty":"EC","crv":"P-384","alg":"ES384","x":"rrFawYTuFo8ZjoDxaztUU-c_RAwjw1Y9Tp3j4nH4WsY2Zlizf40Mvz_0BUkVVZCw","y":"HaFct6PVK2CQ7ZT2SHClnN-knmGfjY_DFwc6qrAu1s0DFZ8fEUuNdmkTlj9T4NQw","key_ops":["verify"],"ext":true}'
|
|
)
|
|
assertEquals(
|
|
await gp.run([mk, 'irrelevant']),
|
|
'{"kty":"EC","crv":"P-384","alg":"ES384","x":"rrFawYTuFo8ZjoDxaztUU-c_RAwjw1Y9Tp3j4nH4WsY2Zlizf40Mvz_0BUkVVZCw","y":"HaFct6PVK2CQ7ZT2SHClnN-knmGfjY_DFwc6qrAu1s0DFZ8fEUuNdmkTlj9T4NQw","key_ops":["verify"],"ext":true}'
|
|
)
|
|
});
|
|
|
|
Deno.test("gen-keypair works correctly", async () => {
|
|
const bi = await import('../cli.js')
|
|
const gk = bi.actions["gen-keypair"]
|
|
const keypair = JSON.parse(await gk.run())
|
|
assert('privateKey' in keypair)
|
|
assert('x' in keypair.privateKey)
|
|
assert('y' in keypair.privateKey)
|
|
assert('d' in keypair.privateKey)
|
|
assertObjectMatch(
|
|
keypair.privateKey,
|
|
{
|
|
kty: "EC",
|
|
crv: "P-384",
|
|
alg: "ES384",
|
|
key_ops: [
|
|
"sign"
|
|
],
|
|
ext: true
|
|
}
|
|
)
|
|
assert('publicKey' in keypair)
|
|
assert('x' in keypair.publicKey)
|
|
assert('y' in keypair.publicKey)
|
|
assert(!('d' in keypair.publicKey))
|
|
assertObjectMatch(
|
|
keypair.publicKey,
|
|
{
|
|
kty: "EC",
|
|
crv: "P-384",
|
|
alg: "ES384",
|
|
key_ops: [
|
|
"verify"
|
|
],
|
|
ext: true
|
|
}
|
|
)
|
|
assert((keypair.privateKey.x == keypair.publicKey.x))
|
|
assert((keypair.privateKey.y == keypair.publicKey.y))
|
|
});
|