From 585d0e1e97b6747c10cf5b7689ccc5618a89b299 Mon Sep 17 00:00:00 2001 From: Brian J Brennan Date: Wed, 8 Apr 2015 11:21:21 -0400 Subject: [PATCH] v3.0.0 --- CHANGELOG.md | 10 +++++++++ lib/verify-stream.js | 31 ++++++++------------------ package.json | 2 +- readme.md | 8 +++++-- test/jws.test.js | 53 +++++++++++++++++++++++++++----------------- 5 files changed, 59 insertions(+), 45 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 773eeb5..af8fc28 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,16 @@ # Change Log All notable changes to this project will be documented in this file. +## [3.0.0] +### Changed +- **BREAKING**: `jwt.verify` now requires an `algorithm` parameter, and + `jws.createVerify` requires an `algorithm` option. The `"alg"` field + signature headers is ignored. This mitigates a critical security flaw + in the library which would allow an attacker to generate signatures with + arbitrary contents that would be accepted by `jwt.verify`. See + https://auth0.com/blog/2015/03/31/critical-vulnerabilities-in-json-web-token-libraries/ + for details. + ## [2.0.0] - 2015-01-30 ### Changed - **BREAKING**: Default payload encoding changed from `binary` to diff --git a/lib/verify-stream.js b/lib/verify-stream.js index 76f1a06..9867405 100644 --- a/lib/verify-stream.js +++ b/lib/verify-stream.js @@ -27,25 +27,6 @@ function securedInputFromJWS(jwsSig) { return jwsSig.split('.', 2).join('.'); } -function algoFromJWS(jwsSig) { - var err; - const header = headerFromJWS(jwsSig); - if (typeof header != 'object') { - err = new Error("Invalid token: no header in signature '" + jwsSig + "'"); - err.code = "MISSING_HEADER"; - err.signature = jwsSig; - throw err; - } - if (!header.alg) { - err = new Error("Missing `alg` field in header for signature '"+ jwsSig +"'"); - err.code = "MISSING_ALGORITHM"; - err.header = header; - err.signature = jwsSig; - throw err; - } - return header.alg; -} - function signatureFromJWS(jwsSig) { return jwsSig.split('.')[2]; } @@ -60,11 +41,16 @@ function isValidJws(string) { return JWS_REGEX.test(string) && !!headerFromJWS(string); } -function jwsVerify(jwsSig, secretOrKey) { +function jwsVerify(jwsSig, algorithm, secretOrKey) { + if (!algorithm) { + var err = new Error("Missing algorithm parameter for jws.verify"); + err.code = "MISSING_ALGORITHM"; + throw err; + } jwsSig = toString(jwsSig); const signature = signatureFromJWS(jwsSig); const securedInput = securedInputFromJWS(jwsSig); - const algo = jwa(algoFromJWS(jwsSig)); + const algo = jwa(algorithm); return algo.verify(securedInput, signature, secretOrKey); } @@ -96,6 +82,7 @@ function VerifyStream(opts) { const secretOrKey = opts.secret||opts.publicKey||opts.key; const secretStream = new DataStream(secretOrKey); this.readable = true; + this.algorithm = opts.algorithm; this.encoding = opts.encoding; this.secret = this.publicKey = this.key = secretStream; this.signature = new DataStream(opts.signature); @@ -111,7 +98,7 @@ function VerifyStream(opts) { } util.inherits(VerifyStream, Stream); VerifyStream.prototype.verify = function verify() { - const valid = jwsVerify(this.signature.buffer, this.key.buffer); + const valid = jwsVerify(this.signature.buffer, this.algorithm, this.key.buffer); const obj = jwsDecode(this.signature.buffer, this.encoding); this.emit('done', valid, obj); this.emit('data', valid); diff --git a/package.json b/package.json index 8259960..c36713b 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "jws", - "version": "2.0.0", + "version": "3.0.0", "description": "Implementation of JSON Web Signatures", "main": "index.js", "directories": { diff --git a/readme.md b/readme.md index fd26215..c3a5753 100644 --- a/readme.md +++ b/readme.md @@ -62,15 +62,18 @@ const signature = jws.sign({ }); ``` -## jws.verify(signature, secretOrKey) +## jws.verify(signature, algorithm, secretOrKey) (Synchronous) Returns`true` or `false` for whether a signature matches a secret or key. -`signature` is a JWS Signature. `secretOrKey` is a string or +`signature` is a JWS Signature. `header.alg` must be a value found in `jws.ALGORITHMS`. +See above for a table of supported algorithms. `secretOrKey` is a string or buffer containing either the secret for HMAC algorithms, or the PEM encoded public key for RSA and ECDSA. +Note that the `"alg"` value from the signature header is ignored. + ## jws.decode(signature) @@ -127,6 +130,7 @@ Returns a new VerifyStream object. Options: * `signature` +* `algorithm` * `key` || `publicKey` || `secret` * `encoding` (Optional, defaults to 'utf8') diff --git a/test/jws.test.js b/test/jws.test.js index dac803e..e59c6c2 100644 --- a/test/jws.test.js +++ b/test/jws.test.js @@ -46,7 +46,8 @@ const payload = { BITS.forEach(function (bits) { test('HMAC using SHA-'+bits+' hash algorithm', function (t) { - const header = { alg: 'HS'+bits, typ: 'JWT' }; + const alg = 'HS'+bits; + const header = { alg: alg, typ: 'JWT' }; const secret = 'sup'; const jwsObj = jws.sign({ header: header, @@ -55,8 +56,9 @@ BITS.forEach(function (bits) { encoding: 'utf8', }); const parts = jws.decode(jwsObj); - t.ok(jws.verify(jwsObj, secret), 'should verify'); - t.notOk(jws.verify(jwsObj, 'something else'), 'should not verify'); + t.ok(jws.verify(jwsObj, alg, secret), 'should verify'); + t.notOk(jws.verify(jwsObj, alg, 'something else'), 'should not verify with non-matching secret'); + t.notOk(jws.verify(jwsObj, 'RS'+bits, secret), 'should not verify with non-matching algorithm'); t.same(parts.payload, payload, 'should match payload'); t.same(parts.header, header, 'should match header'); t.end(); @@ -65,7 +67,8 @@ BITS.forEach(function (bits) { BITS.forEach(function (bits) { test('RSASSA using SHA-'+bits+' hash algorithm', function (t) { - const header = { alg: 'RS'+bits }; + const alg = 'RS'+bits; + const header = { alg: alg }; const privateKey = rsaPrivateKey; const publicKey = rsaPublicKey; const wrongPublicKey = rsaWrongPublicKey; @@ -75,8 +78,9 @@ BITS.forEach(function (bits) { privateKey: privateKey }); const parts = jws.decode(jwsObj, { json: true }); - t.ok(jws.verify(jwsObj, publicKey), 'should verify'); - t.notOk(jws.verify(jwsObj, wrongPublicKey), 'should not verify'); + t.ok(jws.verify(jwsObj, alg, publicKey), 'should verify'); + t.notOk(jws.verify(jwsObj, alg, wrongPublicKey), 'should not verify with non-matching public key'); + t.notOk(jws.verify(jwsObj, 'HS'+bits, publicKey), 'should not verify with non-matching algorithm'); t.same(parts.payload, payload, 'should match payload'); t.same(parts.header, header, 'should match header'); t.end(); @@ -86,7 +90,8 @@ BITS.forEach(function (bits) { BITS.forEach(function (bits) { const curve = CURVES[bits]; test('ECDSA using P-'+curve+' curve and SHA-'+bits+' hash algorithm', function (t) { - const header = { alg: 'ES'+bits }; + const alg = 'ES'+bits; + const header = { alg: alg }; const privateKey = ecdsaPrivateKey['256']; const publicKey = ecdsaPublicKey['256']; const wrongPublicKey = ecdsaWrongPublicKey['256']; @@ -96,8 +101,9 @@ BITS.forEach(function (bits) { privateKey: privateKey }); const parts = jws.decode(jwsObj); - t.ok(jws.verify(jwsObj, publicKey), 'should verify'); - t.notOk(jws.verify(jwsObj, wrongPublicKey), 'should not verify'); + t.ok(jws.verify(jwsObj, alg, publicKey), 'should verify'); + t.notOk(jws.verify(jwsObj, alg, wrongPublicKey), 'should not verify with non-matching public key'); + t.notOk(jws.verify(jwsObj, 'HS'+bits, publicKey), 'should not verify with non-matching algorithm'); t.same(parts.payload, payloadString, 'should match payload'); t.same(parts.header, header, 'should match header'); t.end(); @@ -105,15 +111,17 @@ BITS.forEach(function (bits) { }); test('No digital signature or MAC value included', function (t) { - const header = { alg: 'none' }; + const alg = 'none'; + const header = { alg: alg }; const payload = 'oh hey José!'; const jwsObj = jws.sign({ header: header, payload: payload, }); const parts = jws.decode(jwsObj); - t.ok(jws.verify(jwsObj), 'should verify'); - t.ok(jws.verify(jwsObj, 'anything'), 'should still verify'); + t.ok(jws.verify(jwsObj, alg), 'should verify'); + t.ok(jws.verify(jwsObj, alg, 'anything'), 'should still verify'); + t.notOk(jws.verify(jwsObj, 'HS256', 'anything'), 'should not verify with non-matching algorithm'); t.same(parts.payload, payload, 'should match payload'); t.same(parts.header, header, 'should match header'); t.end(); @@ -128,7 +136,7 @@ test('Streaming sign: HMAC', function (t) { }); dataStream.pipe(sig.payload); sig.on('done', function (signature) { - t.ok(jws.verify(signature, secret), 'should verify'); + t.ok(jws.verify(signature, 'HS256', secret), 'should verify'); t.end(); }); }); @@ -148,8 +156,8 @@ test('Streaming sign: RSA', function (t) { }); sig.on('done', function (signature) { - t.ok(jws.verify(signature, publicKey), 'should verify'); - t.notOk(jws.verify(signature, wrongPublicKey), 'should not verify'); + t.ok(jws.verify(signature, 'RS256', publicKey), 'should verify'); + t.notOk(jws.verify(signature, 'RS256', wrongPublicKey), 'should not verify'); t.same(jws.decode(signature).payload, readfile('data.txt'), 'got all the data'); t.end(); }); @@ -166,8 +174,8 @@ test('Streaming sign: RSA, predefined streams', function (t) { privateKey: privateKeyStream }); sig.on('done', function (signature) { - t.ok(jws.verify(signature, publicKey), 'should verify'); - t.notOk(jws.verify(signature, wrongPublicKey), 'should not verify'); + t.ok(jws.verify(signature, 'RS256', publicKey), 'should verify'); + t.notOk(jws.verify(signature, 'RS256', wrongPublicKey), 'should not verify'); t.same(jws.decode(signature).payload, readfile('data.txt'), 'got all the data'); t.end(); }); @@ -182,7 +190,7 @@ test('Streaming verify: ECDSA', function (t) { payload: dataStream, privateKey: privateKeyStream }); - const verifier = jws.createVerify(); + const verifier = jws.createVerify({algorithm: 'ES512'}); sigStream.pipe(verifier.signature); publicKeyStream.pipe(verifier.key); verifier.on('done', function (valid) { @@ -201,6 +209,7 @@ test('Streaming verify: ECDSA, with invalid key', function (t) { privateKey: privateKeyStream }); const verifier = jws.createVerify({ + algorithm: 'ES512', signature: sigStream, publicKey: publicKeyStream, }); @@ -225,14 +234,18 @@ test('jws.decode: with a bogus header ', function (t) { t.end(); }); -test('jws.decode: missing algo in header', function (t) { +test('jws.verify: missing or invalid algorithm', function (t) { const header = Buffer('{"something":"not an algo"}').toString('base64'); const payload = Buffer('sup').toString('base64'); const sig = header + '.' + payload + '.'; - try { jws.verify(sig, 'whatever') } + try { jws.verify(sig) } catch (e) { t.same(e.code, 'MISSING_ALGORITHM'); } + try { jws.verify(sig, 'whatever') } + catch (e) { + t.ok(e.message.match('"whatever" is not a valid algorithm.')); + } t.end(); });