libresilient/plugins/signed-integrity.js

138 wiersze
5.7 KiB
JavaScript
Czysty Zwykły widok Historia

/* ========================================================================= *\
|* === 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.`)
}
/**
* utility function
* base64url decode
*/
let b64urlDecode = (data) => {
// figure out the padding to add
var pad = 4 - (data.length % 4);
// that's no bueno
if (pad == 3) {
throw new Error(`Invalid base64-encoded string!`)
}
// no padding, then
if (pad == 4) {
pad = 0
}
// we're done, atob that thing
return atob(
data
.replace(/_/g, '/')
.replace(/-/g, '+')
+ '='.repeat(pad)
)
}
/**
* 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) {
// this is where magic happens
LR.log(pluginName, `fetched integrity data file`)
// get the JWT
var jwt = await integrityResponse.text()
console.log('jwt: ' + jwt)
jwt = jwt.split('.')
// unpack it
var header = b64urlDecode(jwt[0])
var payload = b64urlDecode(jwt[1])
var signature = jwt[2]
LR.log(pluginName, `got a JWT:\n- header : ${header}\n- payload: ${payload}`)
} 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)