/* ========================================================================= *\ |* === Signed Integrity: content integrity using signed integrity data === *| \* ========================================================================= */ /** * signed-integrity plugin's deploy/utility functions * * this code expects a Deno runtime: * https://deno.land/ */ /** * helper function, converting binary to base64 * this need not be extremely fast, since it will only be used on digests * * binary_data - data to convert to base64 */ let binToBase64 = (binary_data) => { return btoa( (new Uint8Array(binary_data)) .reduce((bin, byte)=>{ return bin += String.fromCharCode(byte) }, '') ) } /** * generate an ECDSA P-384 keypair and export it as a JWK * https://developer.mozilla.org/en-US/docs/Web/API/SubtleCrypto/importKey#json_web_key */ let genKeypair = async () => { let keypair = await crypto.subtle.generateKey({ name: "ECDSA", namedCurve: "P-384" }, true, ["sign", "verify"] ); let exported_keypair = { publicKey: await crypto.subtle.exportKey("jwk", keypair.publicKey), privateKey: await crypto.subtle.exportKey("jwk", keypair.privateKey) } return JSON.stringify(exported_keypair) } /** * get a public key from a provided private key file * * keyfile - a path to a file containing the private key */ let getPubkey = async (keyfile) => { // we only want to process one file if (Array.isArray(keyfile)) { keyfile = keyfile[0] } // var keydata = JSON.parse(await Deno.readTextFile(keyfile)); // the key can be either in a CryptoKeyPair structure, or directly in CryptoKey structure // standardize! if ("privateKey" in keydata) { keydata = keydata.privateKey } // make the key public by deleting private parts and modifying key_ops // ref. https://stackoverflow.com/a/57571350 delete keydata.d; keydata.key_ops = ['verify'] // import the key, thus making sure data is valid and makes sense let key = await crypto.subtle.importKey( "jwk", keydata, { name: 'ECDSA', namedCurve: 'P-384' }, true, ['verify'] ) // export it again return JSON.stringify(await crypto.subtle.exportKey("jwk", key)) } /** * get integrity digests for a given path * * path - path to a file whose digest is to be generated * algos - array of SubtleCrypto.digest-compatible algorithm names */ let getFileIntegrity = async (path, algos) => { var result = [] // open the file and get some info on it const file = await Deno.open( path, { read: true } ); const fileInfo = await file.stat(); // are we working with a file? if (fileInfo.isFile) { //console.log(`+-- reading: ${path}`) // initialize var content = new Uint8Array() var buf = new Uint8Array(1000); // read the first batch var numread = file.readSync(buf); // read the rest, if there is anything to read while (numread !== null) { //console.log(` +-- read: ${numread}`) //console.log(` +-- length: ${content.length}`) // there has to be a better way... var new_content = new Uint8Array(content.length + numread); //console.log(` +-- new length: ${new_content.length}`) new_content.set(content) if (buf.length === numread) { new_content.set(buf, content.length) } else { new_content.set(buf.slice(0, numread), content.length) } content = new_content // read some more numread = file.readSync(buf); } //console.log(' +-- done.') //console.log('+-- calculating digests') for (const algo of algos) { //console.log(` +-- ${algo}`) var digest = algo.toLowerCase().replace('-', '') + '-' + binToBase64(await crypto.subtle.digest(algo, content)) //console.log(digest) result.push(digest) } //console.log(`+-- file done: ${path}`) // we are not working with a file } else { result = false; } // putting this in a try-catch block as the file // is apparently being auto-closed? // https://issueantenna.com/repo/denoland/deno/issues/15442 try { await file.close(); } catch (BadResource) {} // return the result return result } /** * generate integrity files for provided paths * * paths - paths to files for which integrity files are to be generated * keyfile - path of the file containing the private key to use * extension - file extension to use when saving integrity files (default: ".integrity") */ let genSignedIntegrity = async (paths, keyfile, extension='.integrity') => { // load the key var keydata = JSON.parse(await Deno.readTextFile(keyfile)); // the key can be either in a CryptoKeyPair structure, or directly in CryptoKey structure // standardize! if ("privateKey" in keydata) { keydata = keydata.privateKey } // import the key, thus making sure data is valid and makes sense let privkey = await crypto.subtle.importKey( "jwk", keydata, { name: 'ECDSA', namedCurve: 'P-384' }, true, ['sign'] ) // TODO: implement shit } // this never changes const pluginName = "signed-integrity" const pluginDescription = "Fetching signed integrity data and using it to verify content.\nCLI used to generate subresource integrity tokens and save them in integrity files." const pluginVersion = 'COMMIT_UNKNOWN' const pluginActions = { "gen-keypair": { run: genKeypair, description: "generate a keypair and export it as a JSON Web Key" }, "get-pubkey": { run: getPubkey, description: "print out a public key derived from the provided private key", arguments: { '_': { name: "keyfile", description: "file containing the private key in JSON Web Key format" } } }, "gen-integrity": { run: genSignedIntegrity, description: "generate integrity files for given paths", arguments: { '_': { name: "file", description: "paths to generate signed integrity files for" }, keyfile: { description: "path to the file containing a private key in JSON Web Key format", string: true }, extension: { description: "file extension to use when saving integrity files", default: '.integrity', string: true } } } } export { pluginName as name, pluginDescription as description, pluginVersion as version, pluginActions as actions }