From 20f738a3a1e46f836f71290393a85792eb91e4ae Mon Sep 17 00:00:00 2001 From: Brian J Brennan Date: Wed, 16 Jan 2013 15:19:01 -0500 Subject: [PATCH] Implement RS256 sign and verify. --- .gitignore | 1 + Makefile | 6 ++++ index.js | 86 +++++++++++++++++++++++++++++++++--------------- package.json | 2 +- test/jws.test.js | 34 +++++++++++++++++-- 5 files changed, 99 insertions(+), 30 deletions(-) diff --git a/.gitignore b/.gitignore index 0d112af..da721e0 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,4 @@ node_modules /test/keys /test/private.pem /test/public.pem +/test/wrong-public.pem diff --git a/Makefile b/Makefile index 178614d..f653124 100644 --- a/Makefile +++ b/Makefile @@ -4,6 +4,12 @@ test: test/keys test/keys: @openssl genrsa 2048 > test/private.pem @openssl rsa -in test/private.pem -pubout > test/public.pem + @openssl genrsa 2048 > test/wrong-private.pem + @openssl rsa -in test/wrong-private.pem -pubout > test/wrong-public.pem + @rm test/wrong-private.pem @touch test/keys +clean: + rm test/keys + .PHONY: test \ No newline at end of file diff --git a/index.js b/index.js index a0bd680..3ef295b 100644 --- a/index.js +++ b/index.js @@ -2,28 +2,59 @@ const util = require('util'); const base64url = require('base64url'); const crypto = require('crypto'); + exports.sign = function jwsSign() { - const opts = {}; - if (typeof arguments[0] === 'object') - return jwsSign(arguments[0]); if (arguments.length === 2) { - opts.header = { alg : 'HS256' }; - opts.payload = arguments[0]; - opts.secret = arguments[1]; - return jwsHS256Sign(opts); + var payload = arguments[0]; + const secretOrKey = arguments[1].toString(); + const header = { }; + if (typeof payload === 'object') + payload = JSON.stringify(payload); + if (isPrivateKey(secretOrKey)) + return jwsRS256Sign(header, payload, secretOrKey); + return jwsHS256Sign(header, payload, secretOrKey); } } -function jwsHS256Sign(opts) { - var payload = opts.payload; - if (typeof opts.payload === 'object') - payload = JSON.stringify(opts.payload); - const secret = opts.secret; - const header = JSON.stringify(opts.header); +function jwsSecuredInput(header, payload) { + const encodedHeader = base64url(header); + const encodedPayload = base64url(payload); + return util.format('%s.%s', encodedHeader, encodedPayload); +} + +function isPrivateKey(secretOrKey) { + const RSA_INDICATOR = '-----BEGIN RSA PRIVATE KEY-----'; + return secretOrKey.indexOf(RSA_INDICATOR) === 0; +} + +function jwsRS256Sign(header, payload, key) { + header.alg = 'RS256'; + header = JSON.stringify(header); + const signature = createRS256Signature(header, payload, key); + return jwsOutput(header, payload, signature); +} + +function createRS256Signature(header, payload, key) { + const signer = crypto.createSign('RSA-SHA256', key); + const securedInput = jwsSecuredInput(header, payload); + const signature = (signer.update(securedInput), signer.sign(key, 'base64')); + return base64url.fromBase64(signature); +} + +function jwsHS256Sign(header, payload, secret) { + header.alg = 'HS256'; + header = JSON.stringify(header); const signature = createHS256Signature(header, payload, secret); return jwsOutput(header, payload, signature); } +function createHS256Signature(header, payload, secret) { + const hmac = crypto.createHmac('SHA256', secret); + const securedInput = jwsSecuredInput(header, payload); + const signature = (hmac.update(securedInput), hmac.digest('base64')); + return base64url.fromBase64(signature); +} + function jwsOutput(header, payload, signature) { return util.format( '%s.%s.%s', @@ -32,18 +63,7 @@ function jwsOutput(header, payload, signature) { signature); } -function createHS256Signature(header, payload, secret) { - const hmac = crypto.createHmac('SHA256', secret); - const encodedHeader = base64url(header); - const encodedPayload = base64url(payload); - hmac.update(encodedHeader); - hmac.update('.'); - hmac.update(encodedPayload); - const signature = hmac.digest('base64'); - return base64url.fromBase64(signature); -} - -exports.verify = function jwsVerify(jwsObject, keyOrSecret) { +exports.verify = function jwsVerify(jwsObject, secretOrKey) { const parts = jwsObject.split('.'); const encodedHeader = parts[0]; const encodedPayload = parts[1]; @@ -51,12 +71,24 @@ exports.verify = function jwsVerify(jwsObject, keyOrSecret) { const rawHeader = base64url.decode(encodedHeader); const payload = base64url.decode(encodedPayload); const header = JSON.parse(rawHeader); - if (header.alg === 'HS256') - return jwsHS256Verify(rawHeader, payload, keyOrSecret, encodedSignature) + const verifiers = { + HS256: jwsHS256Verify, + RS256: jwsRS256Verify + }; + const verifierFn = verifiers[header.alg]; + return verifierFn(rawHeader, payload, secretOrKey, encodedSignature) } function jwsHS256Verify(header, payload, secret, expectedSignature) { const calculatedSignature = createHS256Signature(header, payload, secret); return expectedSignature === calculatedSignature; +} + +function jwsRS256Verify(header, payload, publicKey, signature) { + const verifier = crypto.createVerify('RSA-SHA256'); + const securedInput = jwsSecuredInput(header, payload); + signature = base64url.toBase64(signature); + verifier.update(securedInput); + return verifier.verify(publicKey, signature, 'base64'); } \ No newline at end of file diff --git a/package.json b/package.json index 335e033..8f1ad97 100644 --- a/package.json +++ b/package.json @@ -7,7 +7,7 @@ "test": "test" }, "scripts": { - "test": "./node_modules/.bin/tap test/*.test.js" + "test": "make test" }, "repository": { "type": "git", diff --git a/test/jws.test.js b/test/jws.test.js index b517a1d..cd22598 100644 --- a/test/jws.test.js +++ b/test/jws.test.js @@ -1,9 +1,14 @@ +const fs = require('fs'); const base64url = require('base64url'); const crypto = require('crypto'); const test = require('tap').test; const jws = require('..'); -test('HS256 algorithm, signing', function (t) { +const testPrivateKey = fs.readFileSync('./private.pem').toString(); +const testPublicKey = fs.readFileSync('./public.pem').toString(); +const testWrongPublicKey = fs.readFileSync('./wrong-public.pem').toString(); + +test('HS256 algorithm implicit, signing', function (t) { const testString = 'oh hey'; const secret = 'sup'; const expectedHeader = { alg: 'HS256' }; @@ -18,7 +23,7 @@ test('HS256 algorithm, signing', function (t) { t.end(); }); -test('HS256 algorithm, verifying', function (t) { +test('HS256 algorithm implicit, verifying', function (t) { const testString = 'oh hey'; const secret = 'sup'; const jwsObject = jws.sign(testString, secret); @@ -31,3 +36,28 @@ test('HS256 algorithm, verifying', function (t) { t.end(); }); +test('RS256 algorithm implicit, signing', function (t) { + const testString = 'oh hi friends!'; + const expectedHeader = { alg: 'RS256' }; + + const jwsObject = jws.sign(testString, testPrivateKey); + const parts = jwsObject.split('.'); + const header = JSON.parse(base64url.decode(parts[0])); + const payload = base64url.decode(parts[1]); + + t.same(payload, testString, 'payload should match test string'); + t.same(header, expectedHeader, 'header should match expectation'); + t.end(); +}); + +test('RS256 algorithm implicit, verifying', function (t) { + const testString = 'hallo'; + const jwsObject = jws.sign(testString, testPrivateKey); + + const verified = jws.verify(jwsObject, testPublicKey); + t.ok(verified, 'should be verified'); + + const notVerified = jws.verify(jwsObject, testWrongPublicKey); + t.notOk(notVerified, 'should not be verified'); + t.end(); +});