started implementing signed-integrity PoC (ref. #28)

merge-requests/9/merge
Michał 'rysiek' Woźniak 2022-01-10 22:21:16 +00:00
rodzic b1b6877e54
commit e1745144c1
2 zmienionych plików z 261 dodań i 0 usunięć

Wyświetl plik

@ -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.')
}
});
});

Wyświetl plik

@ -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)