kopia lustrzana https://gitlab.com/rysiekpl/libresilient
signed-integrity: we are now able to verify the JWT signature (ref. #28)
rodzic
68ad9b1fa6
commit
8f2d095ff0
|
@ -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="
|
||||||
|
|
|
@ -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}`)
|
||||||
}
|
}
|
||||||
|
|
Ładowanie…
Reference in New Issue