UNPKG

oidc-lib

Version:

A library for creating OIDC Service Providers

750 lines (632 loc) 22.5 kB
/*! * jws/sign.js - Sign to JWS * * Copyright (c) 2015 Cisco Systems, Inc. See LICENSE file. */ "use strict"; var pk; async function registerEndpoints(global_pk){ pk = global_pk; } //////////////////////////////////////////////// JWS ////////////////////////////////////////////////////////////////// /** * @class JWS.Signer * @classdesc Generator of signed content. * * @description * **NOTE:** this class cannot be instantiated directly. Instead call {@link * JWS.createSign}. */ var JWSSigner = function(cfg, signs) { var finalized = false, format = cfg.format || "general", jwsHeader = cfg.jwsHeader, content = new Buffer(0); Object.defineProperty(this, "compact", { get: function() { return "compact" === format; }, enumerable: true }); Object.defineProperty(this, "format", { get: function() { return format; }, enumerable: true }); Object.defineProperty(this, "jwsHeader", { get: function() { return jwsHeader; }, enumerable: true }); Object.defineProperty(this, "update", { value: function(data, encoding) { if (finalized) { throw new Error("already final"); } if (data != null) { content = pk.base64url.encode(data); } return this; } }); /** * @method JWS.Signer#final * @description * Finishes the signature operation. * * The returned Promise, when fulfilled, is the JSON Web Signature (JWS) * object, either in the Compact (if {@link JWS.Signer#format} is * `"compact"`), the flattened JSON (if {@link JWS.Signer#format} is * "flattened"), or the general JSON serialization. * * @param {Buffer|String} [data] The final content to apply. * @param {String} [encoding="binary"] The encoding of the final content * (if any). * @returns {Promise} The promise for the signatures * @throws {Error} If a signature has already been generated. */ Object.defineProperty(this, "final", { value: function(data, encoding) { if (finalized) { return Promise.reject(new Error("already final")); } var privateKey = signs.private; if (!privateKey.algorithm){ return Promise.reject(new Error('private key algorithm is not specified')); } var jwsHeader64 = pk.base64url.encode(JSON.stringify(jwsHeader)); var toBeSigned = Buffer.from(jwsHeader64 + '.' + content, 'utf8'); var algInfo; switch (privateKey.algorithm.name){ case 'RSASSA-PKCS1-v1_5': if (!privateKey.algorithm.hash || privateKey.algorithm.hash.name !== 'SHA-256'){ return Promise.reject(new Error('RSASSA-PKCS1-v1_5 requires algorithm.hash.name of SHA-256')); } algInfo = { name: privateKey.algorithm.name, hash: { name: privateKey.algorithm.hash.name } }; var promise = crypto.subtle.sign(algInfo, privateKey, toBeSigned); break; case 'ECDSA': // crypto.subtle uses 'namedCurve' where everyone else uses 'crv' // so generated the key with algorithm set to namedCurve // confusing because the hash is required for subtlecrypto sign // but generate key doesn't set the hash into the algorithm of the key switch (privateKey.algorithm.namedCurve){ case 'P-256': algInfo = { name: privateKey.algorithm.name, hash: { name: 'SHA-256' } }; break; case 'P-384': algInfo = { name: privateKey.algorithm.name, hash: { name: 'SHA-384' } }; break; case 'P-512': algInfo = { name: privateKey.algorithm.name, hash: { name: 'SHA-512' } }; break; default: return Promise.reject(new Error('ECDSA Requires crv of P-256, P-384 or P-512 and uses it to set algorithm.hash.name')); } var promise = crypto.subtle.sign(algInfo, privateKey, toBeSigned); break; case 'EdDSA': algInfo = privateKey.algorithm; var promise = ed25519_sign(algInfo, privateKey, toBeSigned); break; default: return Promise.reject(new Error('Unrecognized Algorithm name in sign final: ' + privateKey.algorithm.name)); } // var promise = crypto.subtle.sign(algInfo, privateKey, toBeSigned); // var promise = sign_func(algInfo, privateKey, toBeSigned); promise = promise.then(function (result){ var signature64 = pk.base64url.encode(result); var compactToken = jwsHeader64 + '.' + content + '.' + signature64; return Promise.resolve(compactToken); }) return promise; } }); }; function createSign(options, signs) { // fixup signatories var format = options.format; if (!format) { format = options.compact ? "compact" : "general"; } var jwsHeader = options.fields; // jose uses the name 'fields' if (!jwsHeader) { alert('jwsHeader not set in createSign'); // jwsHeader = {"alg":"RS256"}; } var cfg = { format: format, jwsHeader: jwsHeader }; return new JWSSigner(cfg, signs); } /** * @class JWS.Signer * @classdesc Generator of signed content. * * @description * **NOTE:** this class cannot be instantiated directly. Instead call {@link * JWS.createSign}. */ var JWSVerifier = function(cfg, signs) { var format = cfg.format || "general", content = new Buffer(0); Object.defineProperty(this, "compact", { get: function() { return "compact" === format; }, enumerable: true }); Object.defineProperty(this, "format", { get: function() { return format; }, enumerable: true }); Object.defineProperty(this, "verify", { value: function(jws) { if (format !== "compact"){ return Promise.reject(new Error("only compact format supported")); } var components = jws.split('.'); var jwsAlgorithmString = pk.base64url.decode(components[0]); var jwsAlgorithm = JSON.parse(jwsAlgorithmString); var toBeSigned = Buffer(components[0] + '.' + components[1], 'utf8'); var signature = pk.base64url.toBuffer(components[2]); // attempt to find a matching key or key_id var key_match; var keyset = signs.keyset; if (keyset === undefined){ reject('No keys present'); } for (var k=0; k < keyset.length; k++){ var keyObj = keyset[k]; if (keyObj.alg !== jwsAlgorithm.alg){ // the keyObj is not a candidate continue; } if (keyObj.kid === jwsAlgorithm.kid){ // an known match has been found key_match = keyObj; break; } if (key_match === undefined){ // the first key with the right alg will be taken as a match // but the search will continue looking for a kid to cause // another key to supplant it key_match = keyObj; } } if (key_match === undefined){ return Promise.reject('no key match was found'); } if (key_match.public === undefined){ return Promise.reject('key match found but contains no public CryptoKey'); } if (key_match.public.algorithm === undefined){ return Promise.reject('key match found but contains no public CryptoKey algorithm'); } switch (key_match.public.algorithm.name){ case 'RSASSA-PKCS1-v1_5': break; default: return Promise.reject('key match public key algorithm ' + key_match.public.algorithm.name + ' is not supported'); } if (key_match.public.algorithm.hash === undefined){ return Promise.reject('key match found but contains no public CryptoKey algorithm hash'); } switch (key_match.public.algorithm.hash.name){ case 'SHA-256': break; default: return Promise.reject('key match algorithm.hash.name ' + key_match.public.algorithm.hash.name + ' is not supported'); } var promise = crypto.subtle.verify({name: "RSASSA-PKCS1-v1_5"}, key_match.public, signature, toBeSigned); promise = promise.then(function (result){ return Promise.resolve(result); }, function(err){ return Promise.reject('Error running verification'); }) return promise; } }); }; function createVerify(options, signs) { // fixup signatories var format = options.format; if (!format) { format = options.compact ? "compact" : "general"; } var cfg = { format: format }; return new JWSVerifier(cfg, signs); } var JWS = { createSign: createSign, createVerify: createVerify }; //////////////////////////////////////////////// JWE ////////////////////////////////////////////////////////////////// //.............................................. Encrypt .............................................................. var JWEEncryptor = function(cfg, keyObject) { var finalized = false, format = cfg.format || "general", content = new Buffer(0); /** * @member {Boolean} JWS.Signer#compact * @description * Indicates whether the outuput of this signature generator is using * the Compact serialization (`true`) or the JSON serialization * (`false`). */ Object.defineProperty(this, "compact", { get: function() { return "compact" === format; }, enumerable: true }); Object.defineProperty(this, "format", { get: function() { return format; }, enumerable: true }); /** * @method JWS.Signer#update * @description * Updates the signing content for this signature content. The content * is appended to the end of any other content already applied. * * If {data} is a Buffer, {encoding} is ignored. Otherwise, {data} is * converted to a Buffer internally to {encoding}. * * @param {Buffer|String} data The data to sign. * @param {String} [encoding="binary"] The encoding of {data}. * @returns {JWS.Signer} This signature generator. * @throws {Error} If a signature has already been generated. */ Object.defineProperty(this, "update", { value: function(data, encoding) { if (finalized) { throw new Error("already final"); } if (data != null) { content = new Buffer(data, 'utf8'); } return this; } }); /** * @method JWS.Signer#final * @description * Finishes the signature operation. * * The returned Promise, when fulfilled, is the JSON Web Signature (JWS) * object, either in the Compact (if {@link JWS.Signer#format} is * `"compact"`), the flattened JSON (if {@link JWS.Signer#format} is * "flattened"), or the general JSON serialization. * * @param {Buffer|String} [data] The final content to apply. * @param {String} [encoding="binary"] The encoding of the final content * (if any). * @returns {Promise} The promise for the signatures * @throws {Error} If a signature has already been generated. */ Object.defineProperty(this, "final", { value: function(data, encoding) { if (finalized) { return Promise.reject(new Error("already final")); } switch (keyObject.alg){ case 'A128GCM': var symmetric = keyObject.symmetric; var kid = keyObject.kid; if (kid === undefined){ kid = 'prearranged'; } var jweAlgorithmString = JSON.stringify({"alg":"dir","kid":kid,"enc":"A128GCM"}); var jweAlgorithm64 = pk.base64url.encode(jweAlgorithmString); var vector = crypto.getRandomValues(new Uint8Array(12)); var vector64 = pk.base64url.encode(vector); var additionalDataString = jweAlgorithm64 + '.' + '.' + vector64; var additionalData = Buffer(additionalDataString, 'utf8'); var promise = crypto.subtle.encrypt({name: 'AES-GCM', iv: vector, additionalData: additionalData, tagLength: 128 }, symmetric, content); promise = promise.then(function(result){ var ciphertext = result.slice(0, result.byteLength - 16); var tag = result.slice(result.byteLength - 16, result.byteLength); var ciphertext64 = pk.base64url.encode(ciphertext); var tag64 = pk.base64url.encode(tag); return Promise.resolve(additionalDataString + '.' + ciphertext64 + '.' + tag64); }, function(e){ alert('Note to self - investigate non-standard edge behavior: ' + e.message); pk.util.log_debug(e.message); }); return promise; default: return Promise.reject(new Error("Encryption key must currently be A128GCM (AES-GCM)")); } } }); }; function createEncrypt(options, key) { // fixup signatories var format = options.format; if (!format) { format = options.compact ? "compact" : "general"; } var cfg = { format: format }; return new JWEEncryptor(cfg, key); } //.............................................. Decrypt .............................................................. var JWEDecryptor = function(keyset) { var finalized = false, content = new Buffer(0); /** * @method JWS.Signer#final * @description * Finishes the signature operation. * * The returned Promise, when fulfilled, is the JSON Web Signature (JWS) * object, either in the Compact (if {@link JWS.Signer#format} is * `"compact"`), the flattened JSON (if {@link JWS.Signer#format} is * "flattened"), or the general JSON serialization. * * @param {Buffer|String} [data] The final content to apply. * @param {String} [encoding="binary"] The encoding of the final content * (if any). * @returns {Promise} The promise for the signatures * @throws {Error} If a signature has already been generated. */ Object.defineProperty(this, "decrypt", { value: function(data) { var decryptResult = {}; var JWKBaseKeyObject = {}; var components = data.split('.'); var algorithmString = pk.base64url.decode(components[0]); var jweAlgorithm = JSON.parse(algorithmString); if (jweAlgorithm.alg !== 'dir'){ return Promise.reject('jwe alg must be dir'); } switch (jweAlgorithm.enc){ case 'A128GCM': var keyObject; var targetKey; for (var i=0; i < keyset.length; i++){ if (keyset[i].kid === jweAlgorithm.kid){ keyObject = keyset[i]; break; } } if (keyObject === undefined){ return Promise.reject('no A128GCM key matches the encryption algorithm header'); } var targetKey = keyObject.symmetric; if (targetKey.algorithm === undefined || targetKey.algorithm.name !== 'AES-GCM'){ return Promise.reject('the key for A128GCM encryption must be AES-GCM'); } JWKBaseKeyObject.kty = 'oct'; JWKBaseKeyObject.length = targetKey.algorithm.length; JWKBaseKeyObject.kid = keyObject.kid; if (targetKey.usages.indexOf('decrypt') < 0){ return Promise.reject('the key usage for A128GCM decryption must include "decrypt"'); } JWKBaseKeyObject.use = 'decrypt'; break; default: return Promise.reject('jwe enc must be A128GCM'); } decryptResult.header = jweAlgorithm; decryptResult.JWKBaseKeyObject = JWKBaseKeyObject; var vector = pk.base64url.toBuffer(components[2]); var ciphertext = pk.base64url.toBuffer(components[3]); var tag = pk.base64url.toBuffer(components[4]); var ciphertextPlusTag = new Uint8Array( ciphertext.byteLength + tag.byteLength ); ciphertextPlusTag.set(new Uint8Array(ciphertext), 0); ciphertextPlusTag.set(new Uint8Array(tag), ciphertext.byteLength); var additionalDataString = components[0] + '.' + '.' + components[2]; var additionalData = Buffer(additionalDataString, 'utf8'); var promise = crypto.subtle.decrypt({name: 'AES-GCM', iv: vector, additionalData: additionalData, tagLength: 128 }, targetKey, ciphertextPlusTag); promise = promise.then(function(result){ decryptResult.plaintext = result; // decryptResult.payload = pk.base64url.decode(decryptResult.plaintext); return Promise.resolve(decryptResult); }, function(e){ pk.util.log_debug(e.message); }); return promise; } }); }; /* encrypt_promise = crypto.subtle.encrypt({name: "AES-CBC", iv: vector}, key_object, convertStringToArrayBufferView(data)); encrypt_promise.then( function(result){ encrypted_data = new Uint8Array(result); }, function(e){ pk.util.log_debug(e.message); } ); */ function createDecrypt(keyOrKeystore){ var keyset; if (keyOrKeystore.keyset === undefined){ throw new Error("keyset not present in createDecrypt"); } else{ keyset = keyOrKeystore.keyset; } return new JWEDecryptor(keyset); } var JWE = { createEncrypt: createEncrypt, createDecrypt: createDecrypt }; //////////////////////////////////////////////// JWK ////////////////////////////////////////////////////////////////// //.............................................. Keystore .............................................................. // {input} is a String or JSON object representing the JWK-set function createKeyStore(input){ return new JWKKeystore(); } // {input} is a String or JSON object representing the JWK-set function asKeyStore(input){ return new Promise((resolve, reject) => { var inputType = typeof input; if (inputType === 'object' && input.constructor === Array){ resolve(new JWKKeystore(input)); return; } if (inputType === 'string'){ input = JSON.parse(input); } if (input === undefined || input.keys === undefined){ reject('Input is not a keystore json'); return; } var keyImportPromises = []; for (var i = 0; i < input.keys.length; i++){ var jwk = input.keys[i]; var importPromise = createKeyObject(jwk); keyImportPromises.push(importPromise); } var keyArray = []; Promise.all(keyImportPromises) .then(function(keyObjects){ resolve(new JWKKeystore(keyObjects)); }, function(err){ pk.util.log_detail('Error waiting for keyImportPromises', err); }); }); } function createKeyObject(jwk){ return new Promise((resolveImport, rejectImport) => { var keyObj = {}; for (var prop in jwk){ keyObj[prop] = jwk[prop]; } keyObj.jwk = JSON.stringify(jwk); var importAlg; switch (jwk.alg){ case 'RS256': keyObj.key_ops = ['verify']; importAlg = { name: "RSASSA-PKCS1-v1_5", length: 2048, hash: {name: "SHA-256"} }; break; case 'A128GCM': keyObj.key_ops = ['encrypt', 'decrypt']; importAlg = { name: "AES-GCM", length: 128 }; break; default: rejectImport('alg not supported for import'); return; } crypto.subtle.importKey("jwk", jwk, importAlg, true, keyObj.key_ops) .then(function(rawKey){ switch (jwk.alg){ case 'RS256': keyObj.private = rawKey; keyObj.public = rawKey; break; case 'A128GCM': keyObj.symmetric = rawKey; break; default: rejectImport('alg not supported for import'); return; } resolveImport(keyObj); }, function(err){ rejectImport('err'); }); }); } var JWKKeystore = function(input) { if (input === undefined){ input = []; } var _keyset = input; Object.defineProperty(this, "keyset", { get: function() { return _keyset; }, enumerable: true }); Object.defineProperty(this, "toJSON", { value: function(exportPrivate) { var jwkSet = { keys: [] }; for (var i=0; i<_keyset.length; i++){ jwkSet.keys.push(_keyset[i].jwk); } var ret = JSON.stringify(jwkSet); return ret; } }); Object.defineProperty(this, "add", { value: function(input) { var keystore = this; return new Promise((resolve, reject) => { createKeyObject(input) .then(function(keyObj){ _keyset.push(keyObj); resolve(keystore); }, function(err){ reject('Error adding key to keystore: ' + err); }); }); } }); } var JWK = { asKeyStore: asKeyStore, createKeyStore: createKeyStore }; //////////////////////////////////////////////// MODULE ////////////////////////////////////////////////////////////////// function ed25519_sign(algInfo, privateKey, toBeSigned){ return new Promise((resolve, reject) => { if (!algInfo){ reject('ed225519_sign_func missing algInfo'); } else if (algInfo.name !== 'EdDSA'){ reject('ed225519_sign_func invalid algInfo.name: ' + algInfo.name); } var ed25519 = forge.pki.ed25519; var signature = ed25519.sign({ message: toBeSigned, privateKey: privateKey.key }); resolve(signature); }); } module.exports = { JWS: JWS, JWE: JWE, JWK: JWK, provider: 'webcryptoapi', registerEndpoints: registerEndpoints };