kopia lustrzana https://gitlab.com/rysiekpl/libresilient
203 wiersze
8.8 KiB
JavaScript
203 wiersze
8.8 KiB
JavaScript
/* ========================================================================= *\
|
|
|* === 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
|
|
publicKey: 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 the key from the config
|
|
let jwtPublicKey = null
|
|
let getJWTPublicKey = async () => {
|
|
if (jwtPublicKey == null) {
|
|
try {
|
|
jwtPublicKey = await subtle
|
|
.importKey(
|
|
"jwk",
|
|
config.publicKey,
|
|
{
|
|
name: "ECDSA",
|
|
namedCurve: "P-384"
|
|
},
|
|
true,
|
|
["verify"]
|
|
)
|
|
LR.log(pluginName, 'JWT signing key successfully loaded.')
|
|
} catch(e) {
|
|
throw new Error(`Unable to load the public key: ${e}`)
|
|
}
|
|
}
|
|
return jwtPublicKey;
|
|
}
|
|
|
|
/**
|
|
* 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 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()
|
|
jwt = jwt.split('.')
|
|
|
|
// get the key
|
|
let k = await getJWTPublicKey()
|
|
|
|
// reality check: all parts of the JWT should be non-empty
|
|
if ( (jwt[0].length == 0) || (jwt[1].length == 0) || (jwt[2].length == 0) ) {
|
|
throw new Error('JWT seems invalid (one or more sections are empty).')
|
|
}
|
|
|
|
// WARNING: this is in neither efficient or clear... but works, and this is a PoC
|
|
var signature = atob(b64urlDecode(jwt[2]))
|
|
signature = Uint8Array
|
|
.from(
|
|
Array
|
|
.from(signature)
|
|
.map(e=>e.charCodeAt(0))
|
|
).buffer
|
|
|
|
// verify the JWT
|
|
if (await subtle
|
|
.verify(
|
|
{
|
|
name: "ECDSA",
|
|
hash: {name: "SHA-384"}
|
|
},
|
|
k,
|
|
signature,
|
|
(jwt[0] + '.' + jwt[1])
|
|
)) {
|
|
// unpack it
|
|
var header = atob(b64urlDecode(jwt[0]))
|
|
var payload = atob(b64urlDecode(jwt[1]))
|
|
try {
|
|
payload = JSON.parse(payload)
|
|
} catch (e) {
|
|
throw new Error(`JWT payload parsing failed: ${e}`)
|
|
}
|
|
if ('integrity' in payload) {
|
|
LR.log(pluginName, `got a correct, validated JWT; integrity: ${payload.integrity}`)
|
|
init.integrity = payload.integrity
|
|
} else {
|
|
throw new Error(`JWT payload did not contain integrity data.`)
|
|
}
|
|
|
|
} else {
|
|
// we want to error out here, because we did get the integrity file,
|
|
// which means we should expect valid and signed integrity data!
|
|
throw new Error(`JWT signature validation failed! Somebody might be doing something nasty!`)
|
|
}
|
|
|
|
} 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)
|