signed-integrity: we are now able to verify the JWT signature (ref. #28)

merge-requests/9/merge
Michał 'rysiek' Woźniak 2022-01-12 23:47:00 +00:00
rodzic 68ad9b1fa6
commit 8f2d095ff0
2 zmienionych plików z 71 dodań i 14 usunięć

Wyświetl plik

@ -30,6 +30,7 @@ describe("plugin: signed-integrity", () => {
global.Blob = require('buffer').Blob; global.Blob = require('buffer').Blob;
jest.resetModules(); jest.resetModules();
self = global self = global
global.subtle = subtle
global.btoa = (bin) => { global.btoa = (bin) => {
return Buffer.from(bin, 'binary').toString('base64') return Buffer.from(bin, 'binary').toString('base64')
} }
@ -46,13 +47,13 @@ describe("plugin: signed-integrity", () => {
// debug // debug
console.log(await getArmouredKey((await generateECDSAKeypair()).publicKey)) console.log(await getArmouredKey((await generateECDSAKeypair()).publicKey))
console.log(await getArmouredKey((await generateECDSAKeypair()).privateKey))
// ES384: ECDSA using P-384 and SHA-384 // ES384: ECDSA using P-384 and SHA-384
header = btoa('{"alg": "ES384"}').replace(/\//g, '_').replace(/\+/g, '-').replace(/=/g, '') var header = btoa('{"alg": "ES384"}').replace(/\//g, '_').replace(/\+/g, '-').replace(/=/g, '')
payload = btoa('{"integrity": "sha256-eiMrFuthzteJuj8fPwUMyNQMb2SMW7VITmmt2oAxGj0="}').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", name: "ECDSA",
hash: {name: "SHA-384"} hash: {name: "SHA-384"}
@ -60,6 +61,7 @@ describe("plugin: signed-integrity", () => {
(await generateECDSAKeypair()).privateKey, (await generateECDSAKeypair()).privateKey,
(header + '.' + payload) (header + '.' + payload)
) )
// prepare it for inclusion in the JWT
signature = btoa(signature).replace(/\//g, '_').replace(/\+/g, '-').replace(/=/g, '') signature = btoa(signature).replace(/\//g, '_').replace(/\+/g, '-').replace(/=/g, '')
global.resolvingFetch = jest.fn((url, init)=>{ global.resolvingFetch = jest.fn((url, init)=>{
@ -99,7 +101,8 @@ describe("plugin: signed-integrity", () => {
fetch: resolvingFetch fetch: resolvingFetch
} }
], ],
requireIntegrity: false requireIntegrity: false,
publicKey: await subtle.exportKey('jwk', (await generateECDSAKeypair()).publicKey)
} }
requestInit = { requestInit = {
integrity: "sha256-eiMrFuthzteJuj8fPwUMyNQMb2SMW7VITmmt2oAxGj0=" integrity: "sha256-eiMrFuthzteJuj8fPwUMyNQMb2SMW7VITmmt2oAxGj0="

Wyświetl plik

@ -15,7 +15,7 @@
// sane defaults // sane defaults
let defaultConfig = { let defaultConfig = {
// public key used for signature verification on integrity files // public key used for signature verification on integrity files
pubkey: null, publicKey: null,
// suffix of integrity data files // suffix of integrity data files
integrityFileSuffix: '.integrity', integrityFileSuffix: '.integrity',
// is integrity data required for any fetched content? // 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.`) 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 * utility function
* base64url decode * base64url decode
@ -62,12 +86,10 @@
pad = 0 pad = 0
} }
// we're done, atob that thing // we're done, atob that thing
return atob( return data
data
.replace(/_/g, '/') .replace(/_/g, '/')
.replace(/-/g, '+') .replace(/-/g, '+')
+ '='.repeat(pad) + '='.repeat(pad)
)
} }
@ -98,11 +120,43 @@
console.log('jwt: ' + jwt) console.log('jwt: ' + jwt)
jwt = jwt.split('.') jwt = jwt.split('.')
// 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 // unpack it
var header = b64urlDecode(jwt[0]) var header = atob(b64urlDecode(jwt[0]))
var payload = b64urlDecode(jwt[1]) var payload = atob(b64urlDecode(jwt[1]))
var signature = jwt[2] LR.log(pluginName, `got a valid, signed JWT with integrity data:\n- header : ${header}\n- payload: ${payload}`)
LR.log(pluginName, `got a JWT:\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 { } else {
LR.log(pluginName, `fetching integrity data failed: ${integrityResponse.status} ${integrityResponse.statusText}`) LR.log(pluginName, `fetching integrity data failed: ${integrityResponse.status} ${integrityResponse.statusText}`)
} }