kopia lustrzana https://gitlab.com/rysiekpl/libresilient
367 wiersze
16 KiB
JavaScript
367 wiersze
16 KiB
JavaScript
const { subtle } = require('crypto').webcrypto;
|
|
|
|
describe("plugin: signed-integrity", () => {
|
|
|
|
var keypair = null
|
|
|
|
async function generateECDSAKeypair() {
|
|
if (keypair == null) {
|
|
keypair = await subtle.generateKey({
|
|
name: "ECDSA",
|
|
namedCurve: "P-384"
|
|
},
|
|
true,
|
|
["sign", "verify"]
|
|
);
|
|
}
|
|
|
|
return keypair;
|
|
}
|
|
|
|
async function getArmouredKey(key) {
|
|
return JSON.stringify(await subtle.exportKey('jwk', key))
|
|
}
|
|
|
|
beforeEach(async () => {
|
|
global.nodeFetch = require('node-fetch')
|
|
global.Request = global.nodeFetch.Request
|
|
global.Response = global.nodeFetch.Response
|
|
global.crypto = require('crypto').webcrypto
|
|
global.Blob = require('buffer').Blob;
|
|
jest.resetModules();
|
|
self = global
|
|
global.subtle = subtle
|
|
global.btoa = (bin) => {
|
|
return Buffer.from(bin, 'binary').toString('base64')
|
|
}
|
|
global.atob = (ascii) => {
|
|
return Buffer.from(ascii, 'base64').toString('binary')
|
|
}
|
|
|
|
global.LibResilientPluginConstructors = new Map()
|
|
LR = {
|
|
log: (component, ...items)=>{
|
|
console.debug(component + ' :: ', ...items)
|
|
}
|
|
}
|
|
|
|
// debug
|
|
console.log(await getArmouredKey((await generateECDSAKeypair()).publicKey))
|
|
|
|
// ES384: ECDSA using P-384 and SHA-384
|
|
var header = btoa('{"alg": "ES384"}').replace(/\//g, '_').replace(/\+/g, '-').replace(/=/g, '')
|
|
var payload = btoa('{"integrity": "sha256-eiMrFuthzteJuj8fPwUMyNQMb2SMW7VITmmt2oAxGj0="}').replace(/\//g, '_').replace(/\+/g, '-').replace(/=/g, '')
|
|
|
|
// get a signature
|
|
var signature = await subtle.sign(
|
|
{
|
|
name: "ECDSA",
|
|
hash: {name: "SHA-384"}
|
|
},
|
|
(await generateECDSAKeypair()).privateKey,
|
|
(header + '.' + payload)
|
|
)
|
|
// prepare it for inclusion in the JWT
|
|
signature = btoa(signature).replace(/\//g, '_').replace(/\+/g, '-').replace(/=/g, '')
|
|
|
|
// need to test with bad algo!
|
|
var noneHeader = btoa('{"alg": "none"}').replace(/\//g, '_').replace(/\+/g, '-').replace(/=/g, '')
|
|
|
|
// get an invalid signature
|
|
// an ECDSA signature for {alg: none} header makes zero sense
|
|
var noneSignature = await subtle.sign(
|
|
{
|
|
name: "ECDSA",
|
|
hash: {name: "SHA-384"}
|
|
},
|
|
(await generateECDSAKeypair()).privateKey,
|
|
(noneHeader + '.' + payload)
|
|
)
|
|
// prepare it for inclusion in the JWT
|
|
noneSignature = btoa(noneSignature).replace(/\//g, '_').replace(/\+/g, '-').replace(/=/g, '')
|
|
|
|
// prepare stuff for invalid JWT JSON test
|
|
var invalidPayload = btoa('not a valid JSON string').replace(/\//g, '_').replace(/\+/g, '-').replace(/=/g, '')
|
|
// get an valid signature for invalid payload
|
|
var invalidPayloadSignature = await subtle.sign(
|
|
{
|
|
name: "ECDSA",
|
|
hash: {name: "SHA-384"}
|
|
},
|
|
(await generateECDSAKeypair()).privateKey,
|
|
(header + '.' + invalidPayload)
|
|
)
|
|
// prepare it for inclusion in the JWT
|
|
invalidPayloadSignature = btoa(invalidPayloadSignature).replace(/\//g, '_').replace(/\+/g, '-').replace(/=/g, '')
|
|
|
|
// prepare stuff for JWT payload without integrity test
|
|
var noIntegrityPayload = btoa('{"no": "integrity"}').replace(/\//g, '_').replace(/\+/g, '-').replace(/=/g, '')
|
|
// get an valid signature for invalid payload
|
|
var noIntegrityPayloadSignature = await subtle.sign(
|
|
{
|
|
name: "ECDSA",
|
|
hash: {name: "SHA-384"}
|
|
},
|
|
(await generateECDSAKeypair()).privateKey,
|
|
(header + '.' + noIntegrityPayload)
|
|
)
|
|
// prepare it for inclusion in the JWT
|
|
noIntegrityPayloadSignature = btoa(noIntegrityPayloadSignature).replace(/\//g, '_').replace(/\+/g, '-').replace(/=/g, '')
|
|
|
|
global.resolvingFetch = jest.fn((url, init)=>{
|
|
var content = '{"test": "success"}'
|
|
var status = 200
|
|
var statusText = "OK"
|
|
|
|
if (url == 'https://resilient.is/test.json.integrity') {
|
|
content = header + '.' + payload + '.' + signature
|
|
// testing 404 not found on the integrity URL
|
|
} else if (url == 'https://resilient.is/not-found.json.integrity') {
|
|
content = '{"test": "fail"}'
|
|
status = 404
|
|
statusText = "Not Found"
|
|
// testing invalid base64-encoded data
|
|
} else if (url == 'https://resilient.is/invalid-base64.json.integrity') {
|
|
// for this test to work correctly the length must be (n*4)+1
|
|
content = header + '.' + payload + '.' + 'badbase64'
|
|
// testing "alg: none" on the integrity JWT
|
|
} else if (url == 'https://resilient.is/alg-none.json.integrity') {
|
|
content = noneHeader + '.' + payload + '.'
|
|
// testing bad signature on the integrity JWT
|
|
} else if (url == 'https://resilient.is/bad-signature.json.integrity') {
|
|
content = header + '.' + payload + '.' + noneSignature
|
|
// testing invalid payload
|
|
} else if (url == 'https://resilient.is/invalid-payload.json.integrity') {
|
|
content = header + '.' + invalidPayload + '.' + invalidPayloadSignature
|
|
// testing payload without integrity data
|
|
} else if (url == 'https://resilient.is/no-integrity.json.integrity') {
|
|
content = header + '.' + noIntegrityPayload + '.' + noIntegrityPayloadSignature
|
|
}
|
|
|
|
return Promise.resolve(
|
|
new Response(
|
|
[content],
|
|
{
|
|
type: "application/json",
|
|
status: status,
|
|
statusText: statusText,
|
|
headers: {
|
|
'ETag': 'TestingETagHeader'
|
|
},
|
|
url: url
|
|
}
|
|
)
|
|
)
|
|
})
|
|
|
|
init = {
|
|
name: 'signed-integrity',
|
|
uses: [
|
|
{
|
|
name: 'resolve-all',
|
|
description: 'Resolves all',
|
|
version: '0.0.1',
|
|
fetch: resolvingFetch
|
|
}
|
|
],
|
|
requireIntegrity: false,
|
|
publicKey: await subtle.exportKey('jwk', (await generateECDSAKeypair()).publicKey)
|
|
}
|
|
requestInit = {
|
|
integrity: "sha256-eiMrFuthzteJuj8fPwUMyNQMb2SMW7VITmmt2oAxGj0="
|
|
}
|
|
self.log = function(component, ...items) {
|
|
console.debug(component + ' :: ', ...items)
|
|
}
|
|
})
|
|
|
|
test("it should register in LibResilientPluginConstructors", () => {
|
|
require("../../plugins/signed-integrity.js");
|
|
expect(LibResilientPluginConstructors.get('signed-integrity')(LR, init).name).toEqual('signed-integrity');
|
|
});
|
|
|
|
test("it should throw an error when there aren't any wrapped plugins configured", async () => {
|
|
require("../../plugins/signed-integrity.js");
|
|
init = {
|
|
name: 'signed-integrity',
|
|
uses: []
|
|
}
|
|
|
|
expect.assertions(2);
|
|
try {
|
|
await LibResilientPluginConstructors.get('signed-integrity')(LR, init).fetch('https://resilient.is/test.json')
|
|
} catch (e) {
|
|
expect(e).toBeInstanceOf(Error)
|
|
expect(e.toString()).toMatch('Expected exactly one plugin to wrap')
|
|
}
|
|
});
|
|
|
|
test("it should throw an error if the configured public key is impossible to load", async () => {
|
|
require("../../plugins/signed-integrity.js");
|
|
|
|
init.publicKey = 'NOTAKEY'
|
|
|
|
expect.assertions(2);
|
|
try {
|
|
await LibResilientPluginConstructors.get('signed-integrity')(LR, init).fetch('https://resilient.is/test.json')
|
|
} catch (e) {
|
|
expect(e).toBeInstanceOf(Error)
|
|
expect(e.toString()).toMatch('Unable to load the public key')
|
|
}
|
|
});
|
|
|
|
test("it should throw an error when there are more than one wrapped plugins configured", async () => {
|
|
require("../../plugins/signed-integrity.js");
|
|
init = {
|
|
name: 'signed-integrity',
|
|
uses: [{
|
|
name: 'plugin-1'
|
|
},{
|
|
name: 'plugin-2'
|
|
}]
|
|
}
|
|
|
|
expect.assertions(2);
|
|
try {
|
|
await LibResilientPluginConstructors.get('signed-integrity')(LR, init).fetch('https://resilient.is/test.json')
|
|
} catch (e) {
|
|
expect(e).toBeInstanceOf(Error)
|
|
expect(e.toString()).toMatch('Expected exactly one plugin to wrap')
|
|
}
|
|
});
|
|
|
|
test("it should fetch content when integrity data provided without trying to fetch the integrity data URL", async () => {
|
|
require("../../plugins/signed-integrity.js");
|
|
|
|
const response = await LibResilientPluginConstructors.get('signed-integrity')(LR, init).fetch('https://resilient.is/test.json', {
|
|
integrity: "sha384-x4iqiH3PIPD51TibGEhTju/WhidcIEcnrpdklYEtIS87f96c4nLyj6CuwUp8kyOo"
|
|
});
|
|
|
|
expect(resolvingFetch).toHaveBeenCalledTimes(1);
|
|
expect(await response.json()).toEqual({test: "success"})
|
|
expect(response.url).toEqual('https://resilient.is/test.json')
|
|
});
|
|
|
|
test("it should fetch content when integrity data not provided, by also fetching the integrity data URL", async () => {
|
|
require("../../plugins/signed-integrity.js");
|
|
|
|
const response = await LibResilientPluginConstructors.get('signed-integrity')(LR, init).fetch('https://resilient.is/test.json', {});
|
|
|
|
expect(resolvingFetch).toHaveBeenCalledTimes(2);
|
|
expect(resolvingFetch).toHaveBeenNthCalledWith(1, 'https://resilient.is/test.json.integrity')
|
|
expect(await response.json()).toEqual({test: "success"})
|
|
expect(response.url).toEqual('https://resilient.is/test.json')
|
|
});
|
|
|
|
test("it should fetch content when integrity data not provided, and integrity data URL 404s", async () => {
|
|
require("../../plugins/signed-integrity.js");
|
|
|
|
const response = await LibResilientPluginConstructors.get('signed-integrity')(LR, init).fetch('https://resilient.is/not-found.json', {});
|
|
|
|
expect(resolvingFetch).toHaveBeenCalledTimes(2);
|
|
expect(resolvingFetch).toHaveBeenNthCalledWith(1, 'https://resilient.is/not-found.json.integrity')
|
|
expect(await response.json()).toEqual({test: "success"})
|
|
expect(response.url).toEqual('https://resilient.is/not-found.json')
|
|
});
|
|
|
|
test("it should refuse to fetch content when integrity data not provided and integrity data URL 404s, but requireIntegrity is set to true", async () => {
|
|
require("../../plugins/signed-integrity.js");
|
|
|
|
var newInit = init
|
|
newInit.requireIntegrity = true
|
|
|
|
expect.assertions(4);
|
|
try {
|
|
const response = await LibResilientPluginConstructors.get('signed-integrity')(LR, newInit).fetch('https://resilient.is/not-found.json', {});
|
|
} catch (e) {
|
|
expect(resolvingFetch).toHaveBeenCalledTimes(1);
|
|
expect(resolvingFetch).toHaveBeenCalledWith('https://resilient.is/not-found.json.integrity')
|
|
expect(e).toBeInstanceOf(Error)
|
|
expect(e.toString()).toMatch('No integrity data available, though required.')
|
|
}
|
|
});
|
|
|
|
test("it should refuse to fetch content when integrity data not provided and integrity data URL is fetched, but JWT is invalid", async () => {
|
|
require("../../plugins/signed-integrity.js");
|
|
|
|
expect.assertions(4);
|
|
try {
|
|
const response = await LibResilientPluginConstructors.get('signed-integrity')(LR, init).fetch('https://resilient.is/invalid-base64.json', {});
|
|
} catch (e) {
|
|
expect(resolvingFetch).toHaveBeenCalledTimes(1);
|
|
expect(resolvingFetch).toHaveBeenCalledWith('https://resilient.is/invalid-base64.json.integrity')
|
|
expect(e).toBeInstanceOf(Error)
|
|
expect(e.toString()).toMatch('Invalid base64-encoded string')
|
|
}
|
|
});
|
|
|
|
test("it should refuse to fetch content when integrity data not provided and integrity data URL is fetched, but JWT uses alg: none", async () => {
|
|
require("../../plugins/signed-integrity.js");
|
|
|
|
expect.assertions(4);
|
|
try {
|
|
const response = await LibResilientPluginConstructors.get('signed-integrity')(LR, init).fetch('https://resilient.is/alg-none.json', {});
|
|
} catch (e) {
|
|
expect(resolvingFetch).toHaveBeenCalledTimes(1);
|
|
expect(resolvingFetch).toHaveBeenCalledWith('https://resilient.is/alg-none.json.integrity')
|
|
expect(e).toBeInstanceOf(Error)
|
|
expect(e.toString()).toMatch('JWT seems invalid (one or more sections are empty)')
|
|
}
|
|
});
|
|
|
|
test("it should refuse to fetch content when integrity data not provided and integrity data URL is fetched, but JWT signature check fails", async () => {
|
|
require("../../plugins/signed-integrity.js");
|
|
|
|
expect.assertions(4);
|
|
try {
|
|
const response = await LibResilientPluginConstructors.get('signed-integrity')(LR, init).fetch('https://resilient.is/bad-signature.json', {});
|
|
} catch (e) {
|
|
expect(resolvingFetch).toHaveBeenCalledTimes(1);
|
|
expect(resolvingFetch).toHaveBeenCalledWith('https://resilient.is/bad-signature.json.integrity')
|
|
expect(e).toBeInstanceOf(Error)
|
|
expect(e.toString()).toMatch('JWT signature validation failed')
|
|
}
|
|
});
|
|
|
|
test("it should refuse to fetch content when integrity data not provided and integrity data URL is fetched, but JWT payload is unparseable", async () => {
|
|
require("../../plugins/signed-integrity.js");
|
|
|
|
expect.assertions(4);
|
|
try {
|
|
const response = await LibResilientPluginConstructors.get('signed-integrity')(LR, init).fetch('https://resilient.is/invalid-payload.json', {});
|
|
} catch (e) {
|
|
expect(resolvingFetch).toHaveBeenCalledTimes(1);
|
|
expect(resolvingFetch).toHaveBeenCalledWith('https://resilient.is/invalid-payload.json.integrity')
|
|
expect(e).toBeInstanceOf(Error)
|
|
expect(e.toString()).toMatch('JWT payload parsing failed')
|
|
}
|
|
});
|
|
|
|
test("it should refuse to fetch content when integrity data not provided and integrity data URL is fetched, but JWT payload does not contain integrity data", async () => {
|
|
require("../../plugins/signed-integrity.js");
|
|
|
|
expect.assertions(4);
|
|
try {
|
|
const response = await LibResilientPluginConstructors.get('signed-integrity')(LR, init).fetch('https://resilient.is/no-integrity.json', {});
|
|
} catch (e) {
|
|
expect(resolvingFetch).toHaveBeenCalledTimes(1);
|
|
expect(resolvingFetch).toHaveBeenCalledWith('https://resilient.is/no-integrity.json.integrity')
|
|
expect(e).toBeInstanceOf(Error)
|
|
expect(e.toString()).toMatch('JWT payload did not contain integrity data')
|
|
}
|
|
});
|
|
|
|
test("it should fetch and verify content, when integrity data not provided, by fetching the integrity data URL and using integrity data from it", async () => {
|
|
require("../../plugins/signed-integrity.js");
|
|
|
|
const response = await LibResilientPluginConstructors.get('signed-integrity')(LR, init).fetch('https://resilient.is/test.json', {});
|
|
|
|
expect(resolvingFetch).toHaveBeenCalledTimes(2);
|
|
expect(resolvingFetch).toHaveBeenNthCalledWith(1, 'https://resilient.is/test.json.integrity')
|
|
expect(resolvingFetch).toHaveBeenNthCalledWith(2, 'https://resilient.is/test.json', {integrity: "sha256-eiMrFuthzteJuj8fPwUMyNQMb2SMW7VITmmt2oAxGj0="})
|
|
expect(await response.json()).toEqual({test: "success"})
|
|
expect(response.url).toEqual('https://resilient.is/test.json')
|
|
});
|
|
|
|
});
|