From 8f2d095ff030b161f7e951a26613274928f7fb1e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20=27rysiek=27=20Wo=C5=BAniak?= Date: Wed, 12 Jan 2022 23:47:00 +0000 Subject: [PATCH] signed-integrity: we are now able to verify the JWT signature (ref. #28) --- __tests__/plugins/signed-integrity.test.js | 13 ++-- plugins/signed-integrity.js | 72 +++++++++++++++++++--- 2 files changed, 71 insertions(+), 14 deletions(-) diff --git a/__tests__/plugins/signed-integrity.test.js b/__tests__/plugins/signed-integrity.test.js index 44c8cdb..cc8b882 100644 --- a/__tests__/plugins/signed-integrity.test.js +++ b/__tests__/plugins/signed-integrity.test.js @@ -30,6 +30,7 @@ describe("plugin: signed-integrity", () => { global.Blob = require('buffer').Blob; jest.resetModules(); self = global + global.subtle = subtle global.btoa = (bin) => { return Buffer.from(bin, 'binary').toString('base64') } @@ -46,13 +47,13 @@ describe("plugin: signed-integrity", () => { // debug console.log(await getArmouredKey((await generateECDSAKeypair()).publicKey)) - console.log(await getArmouredKey((await generateECDSAKeypair()).privateKey)) // ES384: ECDSA using P-384 and SHA-384 - header = btoa('{"alg": "ES384"}').replace(/\//g, '_').replace(/\+/g, '-').replace(/=/g, '') - payload = btoa('{"integrity": "sha256-eiMrFuthzteJuj8fPwUMyNQMb2SMW7VITmmt2oAxGj0="}').replace(/\//g, '_').replace(/\+/g, '-').replace(/=/g, '') + var header = btoa('{"alg": "ES384"}').replace(/\//g, '_').replace(/\+/g, '-').replace(/=/g, '') + var payload = btoa('{"integrity": "sha256-eiMrFuthzteJuj8fPwUMyNQMb2SMW7VITmmt2oAxGj0="}').replace(/\//g, '_').replace(/\+/g, '-').replace(/=/g, '') - signature = await subtle.sign( + // get a signature + var signature = await subtle.sign( { name: "ECDSA", hash: {name: "SHA-384"} @@ -60,6 +61,7 @@ describe("plugin: signed-integrity", () => { (await generateECDSAKeypair()).privateKey, (header + '.' + payload) ) + // prepare it for inclusion in the JWT signature = btoa(signature).replace(/\//g, '_').replace(/\+/g, '-').replace(/=/g, '') global.resolvingFetch = jest.fn((url, init)=>{ @@ -99,7 +101,8 @@ describe("plugin: signed-integrity", () => { fetch: resolvingFetch } ], - requireIntegrity: false + requireIntegrity: false, + publicKey: await subtle.exportKey('jwk', (await generateECDSAKeypair()).publicKey) } requestInit = { integrity: "sha256-eiMrFuthzteJuj8fPwUMyNQMb2SMW7VITmmt2oAxGj0=" diff --git a/plugins/signed-integrity.js b/plugins/signed-integrity.js index cc9cb4e..cb40429 100644 --- a/plugins/signed-integrity.js +++ b/plugins/signed-integrity.js @@ -15,7 +15,7 @@ // sane defaults let defaultConfig = { // public key used for signature verification on integrity files - pubkey: null, + publicKey: null, // suffix of integrity data files integrityFileSuffix: '.integrity', // is integrity data required for any fetched content? @@ -46,6 +46,30 @@ 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 @@ -62,12 +86,10 @@ pad = 0 } // we're done, atob that thing - return atob( - data + return data .replace(/_/g, '/') .replace(/-/g, '+') + '='.repeat(pad) - ) } @@ -98,11 +120,43 @@ 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}`) + // get the key + let k = await getJWTPublicKey() + + console.log(`JWT b64urlDecoded:\n- ${b64urlDecode(jwt[0])}\n- ${b64urlDecode(jwt[1])}\n- ${b64urlDecode(jwt[2])}`) + + // WARNING: this is in neither efficient or clear... but works, and this is a PoC + var signature = Uint8Array.from( + Array.from( + atob( + b64urlDecode(jwt[2]) + ) + ) + .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])) + LR.log(pluginName, `got a valid, signed JWT with integrity data:\n- header : ${header}\n- payload: ${payload}`) + + } 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}`) }