oidc-lib
Version:
A library for creating OIDC Service Providers
750 lines (632 loc) • 22.5 kB
JavaScript
/*!
* 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
};