2022-01-12 03:54:36 +00:00
const { subtle } = require ( 'crypto' ) . webcrypto ;
2022-01-10 22:21:16 +00:00
describe ( "plugin: signed-integrity" , ( ) => {
2022-01-12 03:54:36 +00:00
var keypair = null
async function generateECDSAKeypair ( ) {
if ( keypair == null ) {
keypair = await subtle . generateKey ( {
name : "ECDSA" ,
namedCurve : "P-384"
} ,
true ,
[ "sign" , "verify" ]
) ;
}
return keypair ;
}
async function getArmouredKey ( key ) {
return JSON . stringify ( await subtle . exportKey ( 'jwk' , key ) )
}
beforeEach ( async ( ) => {
2022-01-10 22:21:16 +00:00
global . nodeFetch = require ( 'node-fetch' )
global . Request = global . nodeFetch . Request
global . Response = global . nodeFetch . Response
global . crypto = require ( 'crypto' ) . webcrypto
global . Blob = require ( 'buffer' ) . Blob ;
jest . resetModules ( ) ;
self = global
2022-01-12 23:47:00 +00:00
global . subtle = subtle
2022-01-10 22:21:16 +00:00
global . btoa = ( bin ) => {
return Buffer . from ( bin , 'binary' ) . toString ( 'base64' )
}
2022-01-12 00:14:31 +00:00
global . atob = ( ascii ) => {
return Buffer . from ( ascii , 'base64' ) . toString ( 'binary' )
}
2022-01-10 22:21:16 +00:00
global . LibResilientPluginConstructors = new Map ( )
LR = {
log : ( component , ... items ) => {
console . debug ( component + ' :: ' , ... items )
}
}
2022-01-12 03:54:36 +00:00
// debug
2022-05-17 23:36:08 +00:00
console . log ( "pubkey: " , await getArmouredKey ( ( await generateECDSAKeypair ( ) ) . publicKey ) )
2022-01-12 03:54:36 +00:00
// ES384: ECDSA using P-384 and SHA-384
2022-01-12 23:47:00 +00:00
var header = btoa ( '{"alg": "ES384"}' ) . replace ( /\//g , '_' ) . replace ( /\+/g , '-' ) . replace ( /=/g , '' )
var payload = btoa ( '{"integrity": "sha256-eiMrFuthzteJuj8fPwUMyNQMb2SMW7VITmmt2oAxGj0="}' ) . replace ( /\//g , '_' ) . replace ( /\+/g , '-' ) . replace ( /=/g , '' )
2022-01-12 03:54:36 +00:00
2022-01-12 23:47:00 +00:00
// get a signature
var signature = await subtle . sign (
2022-01-12 03:54:36 +00:00
{
name : "ECDSA" ,
hash : { name : "SHA-384" }
} ,
( await generateECDSAKeypair ( ) ) . privateKey ,
( header + '.' + payload )
)
2022-01-12 23:47:00 +00:00
// prepare it for inclusion in the JWT
2022-01-12 03:54:36 +00:00
signature = btoa ( signature ) . replace ( /\//g , '_' ) . replace ( /\+/g , '-' ) . replace ( /=/g , '' )
2022-01-12 00:14:31 +00:00
2022-01-13 01:43:38 +00:00
// need to test with bad algo!
var noneHeader = btoa ( '{"alg": "none"}' ) . replace ( /\//g , '_' ) . replace ( /\+/g , '-' ) . replace ( /=/g , '' )
// get an invalid signature
// an ECDSA signature for {alg: none} header makes zero sense
var noneSignature = await subtle . sign (
{
name : "ECDSA" ,
hash : { name : "SHA-384" }
} ,
( await generateECDSAKeypair ( ) ) . privateKey ,
( noneHeader + '.' + payload )
)
// prepare it for inclusion in the JWT
noneSignature = btoa ( noneSignature ) . replace ( /\//g , '_' ) . replace ( /\+/g , '-' ) . replace ( /=/g , '' )
2022-01-13 02:41:22 +00:00
// prepare stuff for invalid JWT JSON test
var invalidPayload = btoa ( 'not a valid JSON string' ) . replace ( /\//g , '_' ) . replace ( /\+/g , '-' ) . replace ( /=/g , '' )
// get an valid signature for invalid payload
var invalidPayloadSignature = await subtle . sign (
{
name : "ECDSA" ,
hash : { name : "SHA-384" }
} ,
( await generateECDSAKeypair ( ) ) . privateKey ,
( header + '.' + invalidPayload )
)
// prepare it for inclusion in the JWT
invalidPayloadSignature = btoa ( invalidPayloadSignature ) . replace ( /\//g , '_' ) . replace ( /\+/g , '-' ) . replace ( /=/g , '' )
2022-01-13 02:48:43 +00:00
// prepare stuff for JWT payload without integrity test
var noIntegrityPayload = btoa ( '{"no": "integrity"}' ) . replace ( /\//g , '_' ) . replace ( /\+/g , '-' ) . replace ( /=/g , '' )
// get an valid signature for invalid payload
var noIntegrityPayloadSignature = await subtle . sign (
{
name : "ECDSA" ,
hash : { name : "SHA-384" }
} ,
( await generateECDSAKeypair ( ) ) . privateKey ,
( header + '.' + noIntegrityPayload )
)
// prepare it for inclusion in the JWT
noIntegrityPayloadSignature = btoa ( noIntegrityPayloadSignature ) . replace ( /\//g , '_' ) . replace ( /\+/g , '-' ) . replace ( /=/g , '' )
2022-01-10 22:21:16 +00:00
global . resolvingFetch = jest . fn ( ( url , init ) => {
var content = '{"test": "success"}'
var status = 200
var statusText = "OK"
2022-01-13 01:43:38 +00:00
2022-01-10 22:21:16 +00:00
if ( url == 'https://resilient.is/test.json.integrity' ) {
2022-01-12 00:14:31 +00:00
content = header + '.' + payload + '.' + signature
2022-01-13 01:43:38 +00:00
// testing 404 not found on the integrity URL
} else if ( url == 'https://resilient.is/not-found.json.integrity' ) {
2022-01-10 22:21:16 +00:00
content = '{"test": "fail"}'
status = 404
statusText = "Not Found"
2022-01-13 02:31:31 +00:00
// testing invalid base64-encoded data
} else if ( url == 'https://resilient.is/invalid-base64.json.integrity' ) {
2022-01-13 10:56:45 +00:00
// for this test to work correctly the length must be (n*4)+1
content = header + '.' + payload + '.' + 'badbase64'
2022-01-13 01:53:35 +00:00
// testing "alg: none" on the integrity JWT
} else if ( url == 'https://resilient.is/alg-none.json.integrity' ) {
content = noneHeader + '.' + payload + '.'
2022-01-13 01:43:38 +00:00
// testing bad signature on the integrity JWT
} else if ( url == 'https://resilient.is/bad-signature.json.integrity' ) {
content = header + '.' + payload + '.' + noneSignature
2022-01-13 02:41:22 +00:00
// testing invalid payload
} else if ( url == 'https://resilient.is/invalid-payload.json.integrity' ) {
content = header + '.' + invalidPayload + '.' + invalidPayloadSignature
2022-01-13 02:48:43 +00:00
// testing payload without integrity data
} else if ( url == 'https://resilient.is/no-integrity.json.integrity' ) {
content = header + '.' + noIntegrityPayload + '.' + noIntegrityPayloadSignature
2022-01-10 22:21:16 +00:00
}
2022-01-13 01:43:38 +00:00
2022-01-10 22:21:16 +00:00
return Promise . resolve (
new Response (
[ content ] ,
{
type : "application/json" ,
status : status ,
statusText : statusText ,
headers : {
'ETag' : 'TestingETagHeader'
} ,
url : url
}
)
)
} )
init = {
name : 'signed-integrity' ,
uses : [
{
name : 'resolve-all' ,
description : 'Resolves all' ,
version : '0.0.1' ,
fetch : resolvingFetch
}
] ,
2022-01-12 23:47:00 +00:00
requireIntegrity : false ,
publicKey : await subtle . exportKey ( 'jwk' , ( await generateECDSAKeypair ( ) ) . publicKey )
2022-01-10 22:21:16 +00:00
}
requestInit = {
integrity : "sha256-eiMrFuthzteJuj8fPwUMyNQMb2SMW7VITmmt2oAxGj0="
}
self . log = function ( component , ... items ) {
console . debug ( component + ' :: ' , ... items )
}
} )
test ( "it should register in LibResilientPluginConstructors" , ( ) => {
2022-05-08 21:01:38 +00:00
require ( "../../../plugins/signed-integrity/index.js" ) ;
2022-01-10 22:21:16 +00:00
expect ( LibResilientPluginConstructors . get ( 'signed-integrity' ) ( LR , init ) . name ) . toEqual ( 'signed-integrity' ) ;
} ) ;
test ( "it should throw an error when there aren't any wrapped plugins configured" , async ( ) => {
2022-05-08 21:01:38 +00:00
require ( "../../../plugins/signed-integrity/index.js" ) ;
2022-01-10 22:21:16 +00:00
init = {
name : 'signed-integrity' ,
uses : [ ]
}
expect . assertions ( 2 ) ;
try {
await LibResilientPluginConstructors . get ( 'signed-integrity' ) ( LR , init ) . fetch ( 'https://resilient.is/test.json' )
} catch ( e ) {
expect ( e ) . toBeInstanceOf ( Error )
expect ( e . toString ( ) ) . toMatch ( 'Expected exactly one plugin to wrap' )
}
} ) ;
2022-01-13 02:31:31 +00:00
test ( "it should throw an error if the configured public key is impossible to load" , async ( ) => {
2022-05-08 21:01:38 +00:00
require ( "../../../plugins/signed-integrity/index.js" ) ;
2022-01-13 02:31:31 +00:00
init . publicKey = 'NOTAKEY'
expect . assertions ( 2 ) ;
try {
await LibResilientPluginConstructors . get ( 'signed-integrity' ) ( LR , init ) . fetch ( 'https://resilient.is/test.json' )
} catch ( e ) {
expect ( e ) . toBeInstanceOf ( Error )
expect ( e . toString ( ) ) . toMatch ( 'Unable to load the public key' )
}
} ) ;
2022-01-10 22:21:16 +00:00
test ( "it should throw an error when there are more than one wrapped plugins configured" , async ( ) => {
2022-05-08 21:01:38 +00:00
require ( "../../../plugins/signed-integrity/index.js" ) ;
2022-01-10 22:21:16 +00:00
init = {
name : 'signed-integrity' ,
uses : [ {
name : 'plugin-1'
} , {
name : 'plugin-2'
} ]
}
expect . assertions ( 2 ) ;
try {
await LibResilientPluginConstructors . get ( 'signed-integrity' ) ( LR , init ) . fetch ( 'https://resilient.is/test.json' )
} catch ( e ) {
expect ( e ) . toBeInstanceOf ( Error )
expect ( e . toString ( ) ) . toMatch ( 'Expected exactly one plugin to wrap' )
}
} ) ;
test ( "it should fetch content when integrity data provided without trying to fetch the integrity data URL" , async ( ) => {
2022-05-08 21:01:38 +00:00
require ( "../../../plugins/signed-integrity/index.js" ) ;
2022-01-10 22:21:16 +00:00
const response = await LibResilientPluginConstructors . get ( 'signed-integrity' ) ( LR , init ) . fetch ( 'https://resilient.is/test.json' , {
integrity : "sha384-x4iqiH3PIPD51TibGEhTju/WhidcIEcnrpdklYEtIS87f96c4nLyj6CuwUp8kyOo"
} ) ;
expect ( resolvingFetch ) . toHaveBeenCalledTimes ( 1 ) ;
expect ( await response . json ( ) ) . toEqual ( { test : "success" } )
expect ( response . url ) . toEqual ( 'https://resilient.is/test.json' )
} ) ;
test ( "it should fetch content when integrity data not provided, by also fetching the integrity data URL" , async ( ) => {
2022-05-08 21:01:38 +00:00
require ( "../../../plugins/signed-integrity/index.js" ) ;
2022-01-10 22:21:16 +00:00
const response = await LibResilientPluginConstructors . get ( 'signed-integrity' ) ( LR , init ) . fetch ( 'https://resilient.is/test.json' , { } ) ;
expect ( resolvingFetch ) . toHaveBeenCalledTimes ( 2 ) ;
expect ( resolvingFetch ) . toHaveBeenNthCalledWith ( 1 , 'https://resilient.is/test.json.integrity' )
expect ( await response . json ( ) ) . toEqual ( { test : "success" } )
expect ( response . url ) . toEqual ( 'https://resilient.is/test.json' )
} ) ;
test ( "it should fetch content when integrity data not provided, and integrity data URL 404s" , async ( ) => {
2022-05-08 21:01:38 +00:00
require ( "../../../plugins/signed-integrity/index.js" ) ;
2022-01-10 22:21:16 +00:00
2022-01-13 01:43:38 +00:00
const response = await LibResilientPluginConstructors . get ( 'signed-integrity' ) ( LR , init ) . fetch ( 'https://resilient.is/not-found.json' , { } ) ;
2022-01-10 22:21:16 +00:00
expect ( resolvingFetch ) . toHaveBeenCalledTimes ( 2 ) ;
2022-01-13 01:43:38 +00:00
expect ( resolvingFetch ) . toHaveBeenNthCalledWith ( 1 , 'https://resilient.is/not-found.json.integrity' )
2022-01-10 22:21:16 +00:00
expect ( await response . json ( ) ) . toEqual ( { test : "success" } )
2022-01-13 01:43:38 +00:00
expect ( response . url ) . toEqual ( 'https://resilient.is/not-found.json' )
2022-01-10 22:21:16 +00:00
} ) ;
test ( "it should refuse to fetch content when integrity data not provided and integrity data URL 404s, but requireIntegrity is set to true" , async ( ) => {
2022-05-08 21:01:38 +00:00
require ( "../../../plugins/signed-integrity/index.js" ) ;
2022-01-10 22:21:16 +00:00
var newInit = init
newInit . requireIntegrity = true
expect . assertions ( 4 ) ;
try {
2022-01-13 01:43:38 +00:00
const response = await LibResilientPluginConstructors . get ( 'signed-integrity' ) ( LR , newInit ) . fetch ( 'https://resilient.is/not-found.json' , { } ) ;
2022-01-10 22:21:16 +00:00
} catch ( e ) {
expect ( resolvingFetch ) . toHaveBeenCalledTimes ( 1 ) ;
2022-01-13 01:43:38 +00:00
expect ( resolvingFetch ) . toHaveBeenCalledWith ( 'https://resilient.is/not-found.json.integrity' )
2022-01-10 22:21:16 +00:00
expect ( e ) . toBeInstanceOf ( Error )
expect ( e . toString ( ) ) . toMatch ( 'No integrity data available, though required.' )
}
} ) ;
2022-01-13 02:31:31 +00:00
test ( "it should refuse to fetch content when integrity data not provided and integrity data URL is fetched, but JWT is invalid" , async ( ) => {
2022-05-08 21:01:38 +00:00
require ( "../../../plugins/signed-integrity/index.js" ) ;
2022-01-13 02:31:31 +00:00
expect . assertions ( 4 ) ;
try {
const response = await LibResilientPluginConstructors . get ( 'signed-integrity' ) ( LR , init ) . fetch ( 'https://resilient.is/invalid-base64.json' , { } ) ;
} catch ( e ) {
expect ( resolvingFetch ) . toHaveBeenCalledTimes ( 1 ) ;
expect ( resolvingFetch ) . toHaveBeenCalledWith ( 'https://resilient.is/invalid-base64.json.integrity' )
expect ( e ) . toBeInstanceOf ( Error )
expect ( e . toString ( ) ) . toMatch ( 'Invalid base64-encoded string' )
}
} ) ;
2022-01-13 01:53:35 +00:00
test ( "it should refuse to fetch content when integrity data not provided and integrity data URL is fetched, but JWT uses alg: none" , async ( ) => {
2022-05-08 21:01:38 +00:00
require ( "../../../plugins/signed-integrity/index.js" ) ;
2022-01-13 01:53:35 +00:00
expect . assertions ( 4 ) ;
try {
const response = await LibResilientPluginConstructors . get ( 'signed-integrity' ) ( LR , init ) . fetch ( 'https://resilient.is/alg-none.json' , { } ) ;
} catch ( e ) {
expect ( resolvingFetch ) . toHaveBeenCalledTimes ( 1 ) ;
expect ( resolvingFetch ) . toHaveBeenCalledWith ( 'https://resilient.is/alg-none.json.integrity' )
expect ( e ) . toBeInstanceOf ( Error )
expect ( e . toString ( ) ) . toMatch ( 'JWT seems invalid (one or more sections are empty)' )
}
} ) ;
2022-01-13 01:43:38 +00:00
test ( "it should refuse to fetch content when integrity data not provided and integrity data URL is fetched, but JWT signature check fails" , async ( ) => {
2022-05-08 21:01:38 +00:00
require ( "../../../plugins/signed-integrity/index.js" ) ;
2022-01-13 01:43:38 +00:00
expect . assertions ( 4 ) ;
try {
const response = await LibResilientPluginConstructors . get ( 'signed-integrity' ) ( LR , init ) . fetch ( 'https://resilient.is/bad-signature.json' , { } ) ;
} catch ( e ) {
expect ( resolvingFetch ) . toHaveBeenCalledTimes ( 1 ) ;
expect ( resolvingFetch ) . toHaveBeenCalledWith ( 'https://resilient.is/bad-signature.json.integrity' )
expect ( e ) . toBeInstanceOf ( Error )
expect ( e . toString ( ) ) . toMatch ( 'JWT signature validation failed' )
}
} ) ;
2022-01-13 02:41:22 +00:00
test ( "it should refuse to fetch content when integrity data not provided and integrity data URL is fetched, but JWT payload is unparseable" , async ( ) => {
2022-05-08 21:01:38 +00:00
require ( "../../../plugins/signed-integrity/index.js" ) ;
2022-01-13 02:41:22 +00:00
expect . assertions ( 4 ) ;
try {
const response = await LibResilientPluginConstructors . get ( 'signed-integrity' ) ( LR , init ) . fetch ( 'https://resilient.is/invalid-payload.json' , { } ) ;
} catch ( e ) {
expect ( resolvingFetch ) . toHaveBeenCalledTimes ( 1 ) ;
expect ( resolvingFetch ) . toHaveBeenCalledWith ( 'https://resilient.is/invalid-payload.json.integrity' )
expect ( e ) . toBeInstanceOf ( Error )
expect ( e . toString ( ) ) . toMatch ( 'JWT payload parsing failed' )
}
} ) ;
2022-01-13 02:48:43 +00:00
test ( "it should refuse to fetch content when integrity data not provided and integrity data URL is fetched, but JWT payload does not contain integrity data" , async ( ) => {
2022-05-08 21:01:38 +00:00
require ( "../../../plugins/signed-integrity/index.js" ) ;
2022-01-13 02:48:43 +00:00
expect . assertions ( 4 ) ;
try {
const response = await LibResilientPluginConstructors . get ( 'signed-integrity' ) ( LR , init ) . fetch ( 'https://resilient.is/no-integrity.json' , { } ) ;
} catch ( e ) {
expect ( resolvingFetch ) . toHaveBeenCalledTimes ( 1 ) ;
expect ( resolvingFetch ) . toHaveBeenCalledWith ( 'https://resilient.is/no-integrity.json.integrity' )
expect ( e ) . toBeInstanceOf ( Error )
expect ( e . toString ( ) ) . toMatch ( 'JWT payload did not contain integrity data' )
}
} ) ;
2022-01-13 02:07:11 +00:00
test ( "it should fetch and verify content, when integrity data not provided, by fetching the integrity data URL and using integrity data from it" , async ( ) => {
2022-05-08 21:01:38 +00:00
require ( "../../../plugins/signed-integrity/index.js" ) ;
2022-01-13 02:07:11 +00:00
const response = await LibResilientPluginConstructors . get ( 'signed-integrity' ) ( LR , init ) . fetch ( 'https://resilient.is/test.json' , { } ) ;
expect ( resolvingFetch ) . toHaveBeenCalledTimes ( 2 ) ;
expect ( resolvingFetch ) . toHaveBeenNthCalledWith ( 1 , 'https://resilient.is/test.json.integrity' )
expect ( resolvingFetch ) . toHaveBeenNthCalledWith ( 2 , 'https://resilient.is/test.json' , { integrity : "sha256-eiMrFuthzteJuj8fPwUMyNQMb2SMW7VITmmt2oAxGj0=" } )
expect ( await response . json ( ) ) . toEqual ( { test : "success" } )
expect ( response . url ) . toEqual ( 'https://resilient.is/test.json' )
} ) ;
2022-01-10 22:21:16 +00:00
} ) ;