272 lines
8.0 KiB
JavaScript
272 lines
8.0 KiB
JavaScript
/*!
|
|
* jws/verify.js - Verifies from a JWS
|
|
*
|
|
* Copyright (c) 2015 Cisco Systems, Inc. See LICENSE file.
|
|
*/
|
|
"use strict";
|
|
|
|
var clone = require("lodash/clone"),
|
|
merge = require("../util/merge"),
|
|
base64url = require("../util/base64url"),
|
|
AlgConfig = require("../util/algconfig"),
|
|
JWK = require("../jwk");
|
|
|
|
var DEFAULT_OPTIONS = {
|
|
algorithms: "*",
|
|
allowEmbeddedKey: false
|
|
};
|
|
|
|
/**
|
|
* @class JWS.Verifier
|
|
* @classdesc Parser of signed content.
|
|
*
|
|
* @description
|
|
* **NOTE:** this class cannot be instantiated directly. Instead call {@link
|
|
* JWS.createVerify}.
|
|
*/
|
|
var JWSVerifier = function(ks, globalOpts) {
|
|
var assumedKey,
|
|
keystore;
|
|
|
|
if (JWK.isKey(ks)) {
|
|
assumedKey = ks;
|
|
keystore = assumedKey.keystore;
|
|
} else if (JWK.isKeyStore(ks)) {
|
|
keystore = ks;
|
|
} else {
|
|
keystore = JWK.createKeyStore();
|
|
}
|
|
|
|
globalOpts = merge(DEFAULT_OPTIONS, globalOpts);
|
|
|
|
Object.defineProperty(this, "defaultKey", {
|
|
value: assumedKey || undefined,
|
|
enumerable: true
|
|
});
|
|
Object.defineProperty(this, "keystore", {
|
|
value: keystore,
|
|
enumerable: true
|
|
});
|
|
|
|
Object.defineProperty(this, "verify", {
|
|
value: function(input, opts) {
|
|
opts = merge({}, globalOpts, opts || {});
|
|
var extraHandlers = opts.handlers || {};
|
|
var handlerKeys = Object.keys(extraHandlers);
|
|
var algSpec = new AlgConfig(opts.algorithms);
|
|
|
|
if ("string" === typeof input) {
|
|
input = input.split(".");
|
|
input = {
|
|
payload: input[1],
|
|
signatures: [
|
|
{
|
|
protected: input[0],
|
|
signature: input[2]
|
|
}
|
|
]
|
|
};
|
|
} else if (!input || "object" !== typeof input) {
|
|
throw new Error("invalid input");
|
|
}
|
|
|
|
// fixup "flattened JSON" to look like "general JSON"
|
|
if (input.signature) {
|
|
input.signatures = [
|
|
{
|
|
protected: input.protected || undefined,
|
|
header: input.header || undefined,
|
|
signature: input.signature
|
|
}
|
|
];
|
|
}
|
|
|
|
// ensure signatories exists
|
|
var sigList = input.signatures || [{}];
|
|
|
|
// combine fields and decode signature per signatory
|
|
sigList = sigList.map(function(s) {
|
|
var header = clone(s.header || {});
|
|
var protect = s.protected ?
|
|
JSON.parse(base64url.decode(s.protected, "utf8")) :
|
|
{};
|
|
header = merge(header, protect);
|
|
var signature = base64url.decode(s.signature);
|
|
|
|
// process allowed algorithims
|
|
if (!algSpec.match(header.alg)) {
|
|
return Promise.reject(new Error("Algorithm not allowed: " + header.alg));
|
|
}
|
|
|
|
// process "crit" first
|
|
var crit = protect.crit;
|
|
if (crit) {
|
|
if (!Array.isArray(crit)) {
|
|
return Promise.reject(new Error("Invalid 'crit' header"));
|
|
}
|
|
for (var idx = 0; crit.length > idx; idx++) {
|
|
if (-1 === handlerKeys.indexOf(crit[idx])) {
|
|
return Promise.reject(new Error(
|
|
"Critical extension is not supported: " + crit[idx]
|
|
));
|
|
}
|
|
}
|
|
}
|
|
protect = Object.keys(protect);
|
|
|
|
return Promise.resolve({
|
|
protected: protect,
|
|
aad: s.protected || "",
|
|
header: header,
|
|
signature: signature
|
|
});
|
|
});
|
|
|
|
var promise = Promise.all(sigList);
|
|
promise = promise.then(function(sigList) {
|
|
return new Promise(function(resolve, reject) {
|
|
var processSig = function() {
|
|
var sig = sigList.shift();
|
|
if (!sig) {
|
|
reject(new Error("no key found"));
|
|
return;
|
|
}
|
|
|
|
sig = merge({}, sig, {
|
|
payload: input.payload
|
|
});
|
|
var p = Promise.resolve(sig);
|
|
// find the key
|
|
p = p.then(function(sig) {
|
|
var algKey;
|
|
// TODO: resolve jku, x5c, x5u
|
|
if (opts.allowEmbeddedKey && sig.header.jwk) {
|
|
algKey = JWK.asKey(sig.header.jwk);
|
|
} else if (opts.allowEmbeddedKey && sig.header.x5c) {
|
|
algKey = sig.header.x5c[0];
|
|
algKey = Buffer.from(algKey, "base64");
|
|
// TODO: callback to validate chain
|
|
algKey = JWK.asKey(algKey, "pkix");
|
|
} else {
|
|
algKey = Promise.resolve(assumedKey || keystore.get({
|
|
use: "sig",
|
|
alg: sig.header.alg,
|
|
kid: sig.header.kid
|
|
}));
|
|
}
|
|
return algKey.then(function(k) {
|
|
if (!k) {
|
|
return Promise.reject(new Error("key does not match"));
|
|
}
|
|
sig.key = k;
|
|
return sig;
|
|
});
|
|
});
|
|
|
|
// process any prepare-verify handlers
|
|
p = p.then(function(sig) {
|
|
var processing = [];
|
|
handlerKeys.forEach(function(h) {
|
|
h = extraHandlers[h];
|
|
var p;
|
|
if ("function" === typeof h) {
|
|
p = h(sig);
|
|
} else if ("object" === typeof h && "function" === typeof h.prepare) {
|
|
p = h.prepare(sig);
|
|
}
|
|
if (p) {
|
|
processing.push(Promise.resolve(p));
|
|
}
|
|
});
|
|
return Promise.all(processing).then(function() {
|
|
// don't actually care about individual handler results
|
|
// assume {sig} is updated
|
|
return sig;
|
|
});
|
|
});
|
|
|
|
// prepare verify inputs
|
|
p = p.then(function(sig) {
|
|
var aad = sig.aad || "",
|
|
payload = sig.payload || "";
|
|
var content = Buffer.alloc(1 + aad.length + payload.length),
|
|
pos = 0;
|
|
content.write(aad, pos, "ascii");
|
|
pos += aad.length;
|
|
content.write(".", pos, "ascii");
|
|
pos++;
|
|
|
|
if (Buffer.isBuffer(payload)) {
|
|
payload.copy(content, pos);
|
|
} else {
|
|
content.write(payload, pos, "binary");
|
|
}
|
|
sig.content = content;
|
|
return sig;
|
|
});
|
|
|
|
p = p.then(function(sig) {
|
|
return sig.key.verify(sig.header.alg,
|
|
sig.content,
|
|
sig.signature);
|
|
});
|
|
|
|
p = p.then(function(result) {
|
|
var payload = sig.payload;
|
|
payload = base64url.decode(payload);
|
|
return {
|
|
protected: sig.protected,
|
|
header: sig.header,
|
|
payload: payload,
|
|
signature: result.mac,
|
|
key: sig.key
|
|
};
|
|
});
|
|
|
|
// process any post-verify handlers
|
|
p = p.then(function(jws) {
|
|
var processing = [];
|
|
handlerKeys.forEach(function(h) {
|
|
h = extraHandlers[h];
|
|
var p;
|
|
if ("object" === typeof h && "function" === typeof h.complete) {
|
|
p = h.complete(jws);
|
|
}
|
|
if (p) {
|
|
processing.push(Promise.resolve(p));
|
|
}
|
|
});
|
|
return Promise.all(processing).then(function() {
|
|
// don't actually care about individual handler results
|
|
// assume {jws} is updated
|
|
return jws;
|
|
});
|
|
});
|
|
p.then(resolve, processSig);
|
|
};
|
|
processSig();
|
|
});
|
|
});
|
|
return promise;
|
|
}
|
|
});
|
|
};
|
|
|
|
/**
|
|
* @description
|
|
* Creates a new JWS.Verifier with the given Key or KeyStore.
|
|
*
|
|
* @param {JWK.Key|JWK.KeyStore} ks The Key or KeyStore to use for verification.
|
|
* @returns {JWS.Verifier} The new Verifier.
|
|
*/
|
|
function createVerify(ks, opts) {
|
|
var vfy = new JWSVerifier(ks, opts);
|
|
|
|
return vfy;
|
|
}
|
|
|
|
module.exports = {
|
|
verifier: JWSVerifier,
|
|
createVerify: createVerify
|
|
};
|