Skip to content

Commit

Permalink
Update: implement JWK thumbprint support [RFC 7638]
Browse files Browse the repository at this point in the history
  • Loading branch information
linuxwolf committed Dec 15, 2015
1 parent 7bee091 commit e57384c
Show file tree
Hide file tree
Showing 11 changed files with 334 additions and 21 deletions.
20 changes: 20 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ A JavaScript implementation of the JSON Object Signing and Encryption (JOSE) for
- [Searching for Keys](#searching-for-keys)
- [Managing Keys](#managing-keys)
- [Importing and Exporting a Single Key](#importing-and-exporting-a-single-key)
- [Obtaining a Key's Thumbprint](#obtaining-a-keys-thumbprint)
- [Signatures](#signatures)
- [Signing Content](#signing-content)
- [Verifying a JWS](#verifying-a-jws)
Expand Down Expand Up @@ -269,6 +270,25 @@ To export the public **and** private portions of a Key:
var output = key.toJSON(true);
```

### Obtaining a Key's Thumbprint ###

To get or calculate a [RFC 7638](https://tools.ietf.org/html/rfc7638) thumbprint for a key:

```
// where hash is a supported algorithm, currently one of:
// * SHA-1
// * SHA-256
// * SHA-384
// * SHA-512
key.thumbprint(hash).
then(function(print) {
// {print} is a Buffer containing the thumbprint binary value
});
```

When importing or generating a key that does not have a "kid" defined, a
"SHA-256" thumbprint is calculated and used as the "kid".

## Signatures ##

### Signing Content ###
Expand Down
54 changes: 49 additions & 5 deletions lib/jwk/basekey.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@
*/
"use strict";

var clone = require("lodash.clone"),
var assign = require("lodash.assign"),
clone = require("lodash.clone"),
flatten = require("lodash.flatten"),
intersection = require("lodash.intersection"),
merge = require("../util/merge"),
Expand All @@ -16,7 +17,8 @@ var clone = require("lodash.clone"),

var ALGORITHMS = require("../algorithms"),
CONSTANTS = require("./constants.js"),
HELPERS = require("./helpers.js");
HELPERS = require("./helpers.js"),
UTIL = require("../util");

/**
* @class JWK.Key
Expand Down Expand Up @@ -50,12 +52,31 @@ var JWKBaseKeyObject = function(kty, ks, props, cfg) {

var excluded = [];
var keys = {},
json = {};
json = {},
prints,
kid;

// force certain values
props = clone(props);
// strip thumbprints if present
prints = assign({}, props[HELPERS.INTERNALS.THUMBPRINT_KEY] || {});
delete props[HELPERS.INTERNALS.THUMBPRINT_KEY];
Object.keys(prints).forEach(function(a) {
var h = prints[a];
if (!kid) {
kid = h;
if (Buffer.isBuffer(kid)) {
kid = UTIL.base64url.encode(kid);
}
}
if (!Buffer.isBuffer(h)) {
h = UTIL.base64url.decode(h);
prints[a] = h;
}
});

// force certain values
props.kty = kty;
props.kid = props.kid || uuid();
props.kid = props.kid || kid || uuid();

// setup base info
var included = Object.keys(HELPERS.COMMON_PROPS).map(function(p) {
Expand Down Expand Up @@ -153,6 +174,29 @@ var JWKBaseKeyObject = function(kty, ks, props, cfg) {
});

// ### Public Methods ###
/**
* Generates the thumbprint of this Key.
*
* @param {String} [] The hash algorithm to use
* @returns {Promise} The promise for the thumbprint generation.
*/
Object.defineProperty(this, "thumbprint", {
value: function(hash) {
hash = (hash || HELPERS.INTERNALS.THUMBPRINT_HASH).toUpperCase();
if (prints[hash]) {
// return cached value
return Promise.resolve(prints[hash]);
}
var p = HELPERS.thumbprint(cfg, json, hash);
p = p.then(function(result) {
if (result) {
prints[hash] = result;
}
return result;
});
return p;
}
});
/**
* @method JWK.Key#algorithms
* @description
Expand Down
28 changes: 26 additions & 2 deletions lib/jwk/eckey.js
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,18 @@ var JWKEcCfg = {

return pk;
},
thumbprint: function(json) {
if (json.public) {
json = json.public;
}
var fields = {
crv: json.crv,
kty: "EC",
x: json.x,
y: json.y
};
return fields;
},
algorithms: function(keys, mode) {
var len = (keys.public && keys.public.length) ||
(keys.private && keys.private.length) ||
Expand Down Expand Up @@ -294,8 +306,20 @@ var validators = {
var JWKEcFactory = {
kty: "EC",
validators: validators,
prepare: function() {
return Promise.resolve(JWKEcCfg);
prepare: function(props) {
// TODO: validate key properties
var cfg = JWKEcCfg;
var p = Promise.resolve(props);
p = p.then(function(json) {
return JWK.helpers.thumbprint(cfg, json);
});
p = p.then(function(hash) {
var prints = {};
prints[JWK.helpers.INTERNALS.THUMBPRINT_HASH] = hash;
props[JWK.helpers.INTERNALS.THUMBPRINT_KEY] = prints;
return cfg;
});
return p;
},
generate: function(size) {
var keypair = depsecc.generateKeyPair(size);
Expand Down
33 changes: 32 additions & 1 deletion lib/jwk/helpers.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ var clone = require("lodash.clone"),
util = require("../util"),
forge = require("../deps/forge");

var ALGORITHMS = require("../algorithms");

// ### ASN.1 Validators
// Adapted from digitalbazaar/node-forge/js/asn1.js
// PrivateKeyInfo
Expand Down Expand Up @@ -289,13 +291,41 @@ var X509CertificateValidator = {
]
};

var INTERNALS = {
THUMBPRINT_KEY: "internal\u0000thumbprint",
THUMBPRINT_HASH: "SHA-256"
};

module.exports = {
validators: {
privateKey: privateKeyValidator,
publicKey: publicKeyValidator,
certificate: X509CertificateValidator
},

thumbprint: function(cfg, json, hash) {
if ("function" !== typeof cfg.thumbprint) {
return Promise.reject(new Error("thumbprint not supported"));
}

hash = (hash || INTERNALS.THUMBPRINT_HASH).toUpperCase();
var fields = cfg.thumbprint(json);
var input = Object.keys(fields).
sort().
map(function(k) {
var v = fields[k];
if (Buffer.isBuffer(v)) {
v = util.base64url.encode(v);
}
return JSON.stringify(k) + ":" + JSON.stringify(v);
});
input = "{" + input.join(",") + "}";
try {
return ALGORITHMS.digest(hash, new Buffer(input, "utf8"));
} catch (err) {
return Promise.reject(err);
}
},
unpackProps: function(props, allowed) {
var output;

Expand Down Expand Up @@ -353,5 +383,6 @@ module.exports = {
{name: "x5t", type: "binary"},
{name: "x5u", type: "string"},
{name: "key_ops", type: "array"}
]
],
INTERNALS: INTERNALS
};
26 changes: 24 additions & 2 deletions lib/jwk/octkey.js
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,17 @@ var JWKOctetCfg = {
return pk;
},

thumbprint: function(json) {
if (json.private) {
json = json.private;
}
var fields;
fields = {
k: json.k || "",
kty: "oct"
};
return fields;
},
algorithms: function(keys, mode) {
var len = keys.private && (keys.private.k.length * 8);
var mins = [256, 384, 512];
Expand Down Expand Up @@ -175,9 +186,20 @@ var JWKOctetCfg = {
// Factory
var JWKOctetFactory = {
kty: "oct",
prepare: function() {
prepare: function(props) {
// TODO: validate key properties
return Promise.resolve(JWKOctetCfg);
var cfg = JWKOctetCfg;
var p = Promise.resolve(props);
p = p.then(function(json) {
return JWK.helpers.thumbprint(cfg, json);
});
p = p.then(function(hash) {
var prints = {};
prints[JWK.helpers.INTERNALS.THUMBPRINT_HASH] = hash;
props[JWK.helpers.INTERNALS.THUMBPRINT_KEY] = prints;
return cfg;
});
return p;
},
generate: function(size) {
// TODO: validate key sizes
Expand Down
30 changes: 26 additions & 4 deletions lib/jwk/rsakey.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@
*/
"use strict";

var forge = require("../deps/forge.js");
var rsau = require("../algorithms/rsa-util");
var forge = require("../deps/forge.js"),
rsau = require("../algorithms/rsa-util");

var JWK = {
BaseKey: require("./basekey.js"),
Expand Down Expand Up @@ -66,6 +66,17 @@ var JWKRsaCfg = {

return pk;
},
thumbprint: function(json) {
if (json.public) {
json = json.public;
}
var fields = {
e: json.e,
kty: "RSA",
n: json.n
};
return fields;
},
algorithms: function(keys, mode) {
switch (mode) {
case "encrypt":
Expand Down Expand Up @@ -229,9 +240,20 @@ var validators = {
var JWKRsaFactory = {
kty: "RSA",
validators: validators,
prepare: function() {
prepare: function(props) {
// TODO: validate key properties
return Promise.resolve(JWKRsaCfg);
var cfg = JWKRsaCfg;
var p = Promise.resolve(props);
p = p.then(function(json) {
return JWK.helpers.thumbprint(cfg, json);
});
p = p.then(function(hash) {
var prints = {};
prints[JWK.helpers.INTERNALS.THUMBPRINT_HASH] = hash;
props[JWK.helpers.INTERNALS.THUMBPRINT_KEY] = prints;
return cfg;
});
return p;
},
generate: function(size) {
// TODO: validate key sizes
Expand Down
70 changes: 70 additions & 0 deletions test/jwk/basekey-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,12 @@ describe("jwk/basekey", function() {

return pk;
},
thumbprint: function(json) {
var fields = {};
fields.pub = json.public.pub;
fields.kty = "DUMMY";
return fields;
},
algorithms: function(keys, mode) {
var supported;
switch (mode) {
Expand Down Expand Up @@ -625,6 +631,70 @@ describe("jwk/basekey", function() {
});
});

describe("thumbprints", function() {
it("returns a promise for a 'default' thumbprint", function() {
var inst;

inst = createInstance({
pub: "Lc3EY3_96tfej0F7Afa0TQ",
prv: "SBh6LBt1DBTeyHTvwDgSjg"
});
var p = inst.thumbprint();
p = p.then(function(print) {
assert.equal(print.toString("hex"),
"37e91104e7e5b0b923926844c710a100aa48fa14554e85fc901a5ebc99cd13e6");
});
return p;
});
it("returns a promise for a specified thumbprint", function() {
var inst;

inst = createInstance({
pub: "Lc3EY3_96tfej0F7Afa0TQ",
prv: "SBh6LBt1DBTeyHTvwDgSjg"
});
var p = inst.thumbprint("SHA-1");
p = p.then(function(print) {
assert.equal(print.toString("hex"),
"d14514ab53c383798343e3577ad12947e84fad40");
});
return p;
});
it("fails on invalid hash algoritm", function() {
var inst;

inst = createInstance({
pub: "Lc3EY3_96tfej0F7Afa0TQ",
prv: "SBh6LBt1DBTeyHTvwDgSjg"
});
var p = inst.thumbprint("SHA3");
p = p.then(function(print) {
assert.ok(false, "unexpected success");
}, function(err) {
assert.ok(err);
});
return p;
});
it("returns the same thumbprint as before", function() {
var inst;

inst = createInstance({
pub: "Lc3EY3_96tfej0F7Afa0TQ",
prv: "SBh6LBt1DBTeyHTvwDgSjg"
});
var p = inst.thumbprint();
p = p.then(function(print) {
assert.equal(print.toString("hex"),
"37e91104e7e5b0b923926844c710a100aa48fa14554e85fc901a5ebc99cd13e6");
return inst.thumbprint();
});
p = p.then(function(print) {
assert.equal(print.toString("hex"),
"37e91104e7e5b0b923926844c710a100aa48fa14554e85fc901a5ebc99cd13e6");
});
return p;
});
});
describe("algorithms and supports", function() {
it("returns all supported algorithms", function() {
var inst;
Expand Down
Loading

0 comments on commit e57384c

Please sign in to comment.