kopia lustrzana https://gitlab.com/rysiekpl/libresilient
started implementing signed-integrity PoC (ref. #28)
rodzic
b1b6877e54
commit
e1745144c1
|
@ -0,0 +1,161 @@
|
|||
describe("plugin: signed-integrity", () => {
|
||||
|
||||
beforeEach(() => {
|
||||
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.btoa = (bin) => {
|
||||
return Buffer.from(bin, 'binary').toString('base64')
|
||||
}
|
||||
|
||||
global.LibResilientPluginConstructors = new Map()
|
||||
LR = {
|
||||
log: (component, ...items)=>{
|
||||
console.debug(component + ' :: ', ...items)
|
||||
}
|
||||
}
|
||||
|
||||
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 = '{"integrity": "sha256-eiMrFuthzteJuj8fPwUMyNQMb2SMW7VITmmt2oAxGj0="}'
|
||||
} else if (url == 'https://resilient.is/fail.json.integrity') {
|
||||
content = '{"test": "fail"}'
|
||||
status = 404
|
||||
statusText = "Not Found"
|
||||
}
|
||||
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
|
||||
}
|
||||
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 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/fail.json', {});
|
||||
|
||||
expect(resolvingFetch).toHaveBeenCalledTimes(2);
|
||||
expect(resolvingFetch).toHaveBeenNthCalledWith(1, 'https://resilient.is/fail.json.integrity')
|
||||
expect(await response.json()).toEqual({test: "success"})
|
||||
expect(response.url).toEqual('https://resilient.is/fail.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/fail.json', {});
|
||||
} catch (e) {
|
||||
expect(resolvingFetch).toHaveBeenCalledTimes(1);
|
||||
expect(resolvingFetch).toHaveBeenCalledWith('https://resilient.is/fail.json.integrity')
|
||||
expect(e).toBeInstanceOf(Error)
|
||||
expect(e.toString()).toMatch('No integrity data available, though required.')
|
||||
}
|
||||
});
|
||||
|
||||
});
|
|
@ -0,0 +1,100 @@
|
|||
/* ========================================================================= *\
|
||||
|* === Signed Integrity: content integrity using signed integrity data === *|
|
||||
\* ========================================================================= */
|
||||
|
||||
// no polluting of the global namespace please
|
||||
(function(LRPC){
|
||||
// this never changes
|
||||
const pluginName = "signed-integrity"
|
||||
LRPC.set(pluginName, (LR, init={})=>{
|
||||
|
||||
/*
|
||||
* plugin config settings
|
||||
*/
|
||||
|
||||
// sane defaults
|
||||
let defaultConfig = {
|
||||
// public key used for signature verification on integrity files
|
||||
pubkey: null,
|
||||
// suffix of integrity data files
|
||||
integrityFileSuffix: '.integrity',
|
||||
// is integrity data required for any fetched content?
|
||||
//
|
||||
// NOTICE: this requires *any* integrity data to be available; if integrity data
|
||||
// NOTICE: is already set in the original Request, that's considered enough
|
||||
//
|
||||
// TODO: do we need to have forceSignedIntegrity too, to *force* usage of signed integrity data?
|
||||
requireIntegrity: false,
|
||||
// plugin used for actually fetching the content
|
||||
uses: [{
|
||||
// by default using standard fetch(),
|
||||
// leaning on browser implementations of subresource integrity checks
|
||||
//
|
||||
// if using a different transport plugin, remember to make sure that it verifies
|
||||
// subresource integrity when provided, or wrap it in an integrity-checking
|
||||
// wrapper plugin (like integrity-check) to make sure integrity is in fact
|
||||
// verified when present
|
||||
name: "fetch"
|
||||
}]
|
||||
}
|
||||
|
||||
// merge the defaults with settings from LibResilientConfig
|
||||
let config = {...defaultConfig, ...init}
|
||||
|
||||
// reality check: if no wrapped plugin configured, or more than one, complain
|
||||
if (config.uses.length != 1) {
|
||||
throw new Error(`Expected exactly one plugin to wrap, but ${config.uses.length} configured.`)
|
||||
}
|
||||
|
||||
/**
|
||||
* getting content using the configured plugin,
|
||||
* but also making sure integrity data file is fetched, signature checked,
|
||||
* and integrity data set in the init for the wrapped plugin
|
||||
*/
|
||||
let fetchContent = async (url, init={}) => {
|
||||
|
||||
// do we have integrity data in init?
|
||||
if (!('integrity' in init)) {
|
||||
// integrity data file URL
|
||||
var integrityUrl = url + config.integrityFileSuffix
|
||||
|
||||
// let's try to get integrity data
|
||||
LR.log(pluginName, `fetching integrity file:\n- ${integrityUrl}\nusing plugin:\n- ${config.uses[0].name}`)
|
||||
var integrityResponse = await config.uses[0].fetch(integrityUrl)
|
||||
|
||||
// did we get anything sane?
|
||||
if (integrityResponse.status == 200) {
|
||||
LR.log(pluginName, `fetched integrity data file`)
|
||||
// this is where magic happens
|
||||
} else {
|
||||
LR.log(pluginName, `fetching integrity data failed: ${integrityResponse.status} ${integrityResponse.statusText}`)
|
||||
}
|
||||
}
|
||||
|
||||
// at this point we should have integrity in init one way or another
|
||||
if (config.requireIntegrity && !('integrity' in init)) {
|
||||
throw new Error(`No integrity data available, though required.`)
|
||||
}
|
||||
|
||||
LR.log(pluginName, `fetching content using: [${config.uses[0].name}]`)
|
||||
// fetch using the configured wrapped plugin
|
||||
//
|
||||
// NOTICE: we have no way of knowing if the wrapped plugin performs any actual integrity check
|
||||
// NOTICE: if the wrapped plugin doesn't actually check integrity,
|
||||
// NOTICE: setting integrity here is not going to do anything
|
||||
return config.uses[0].fetch(url, init)
|
||||
}
|
||||
|
||||
// and add ourselves to it
|
||||
// with some additional metadata
|
||||
return {
|
||||
name: pluginName,
|
||||
description: `Fetching signed integrity data, using: [${config.uses.map(p=>p.name).join(', ')}]`,
|
||||
version: 'COMMIT_UNKNOWN',
|
||||
fetch: fetchContent,
|
||||
uses: config.uses
|
||||
}
|
||||
|
||||
})
|
||||
// done with not polluting the global namespace
|
||||
})(LibResilientPluginConstructors)
|
Ładowanie…
Reference in New Issue