diff --git a/README.md b/README.md index 6ab1b8e..9750d74 100644 --- a/README.md +++ b/README.md @@ -15,11 +15,16 @@ HS512 | HMAC using SHA-512 hash algorithm RS256 | RSASSA using SHA-256 hash algorithm RS384 | RSASSA using SHA-384 hash algorithm RS512 | RSASSA using SHA-512 hash algorithm +PS256 | RSASSA-PSS using SHA-256 hash algorithm +PS384 | RSASSA-PSS using SHA-384 hash algorithm +PS512 | RSASSA-PSS using SHA-512 hash algorithm ES256 | ECDSA using P-256 curve and SHA-256 hash algorithm ES384 | ECDSA using P-384 curve and SHA-384 hash algorithm ES512 | ECDSA using P-521 curve and SHA-512 hash algorithm none | No digital signature or MAC value included +Please note that PS* only works on Node 6.12+ (excluding 7.x). + # Requirements In order to run the tests, a recent version of OpenSSL is @@ -64,7 +69,7 @@ called on it to attempt to coerce it. For the HMAC algorithm, `secretOrPrivateKey` should be a string or a buffer. For ECDSA and RSA, the value should be a string representing a -PEM encoded **private** key. +PEM encoded **private** key. Output [base64url](http://en.wikipedia.org/wiki/Base64#URL_applications) formatted. This is for convenience as JWS expects the signature in this diff --git a/index.js b/index.js index ed48509..95126b8 100644 --- a/index.js +++ b/index.js @@ -4,7 +4,7 @@ var crypto = require('crypto'); var formatEcdsa = require('ecdsa-sig-formatter'); var util = require('util'); -var MSG_INVALID_ALGORITHM = '"%s" is not a valid algorithm.\n Supported algorithms are:\n "HS256", "HS384", "HS512", "RS256", "RS384", "RS512" and "none".' +var MSG_INVALID_ALGORITHM = '"%s" is not a valid algorithm.\n Supported algorithms are:\n "HS256", "HS384", "HS512", "RS256", "RS384", "RS512", "PS256", "PS384", "PS512", "ES256", "ES384", "ES512" and "none".' var MSG_INVALID_SECRET = 'secret must be a string or buffer'; var MSG_INVALID_VERIFIER_KEY = 'key must be a string or a buffer'; var MSG_INVALID_SIGNER_KEY = 'key must be a string, a buffer or an object'; @@ -88,6 +88,29 @@ function createKeyVerifier(bits) { } } +function createPSSKeySigner(bits) { + return function sign(thing, privateKey) { + if (!bufferOrString(privateKey) && !(typeof privateKey === 'object')) + throw typeError(MSG_INVALID_SIGNER_KEY); + thing = normalizeInput(thing); + var signer = crypto.createSign('RSA-SHA' + bits); + var sig = (signer.update(thing), signer.sign({key: privateKey, padding: crypto.constants.RSA_PKCS1_PSS_PADDING}, 'base64')); + return fromBase64(sig); + } +} + +function createPSSKeyVerifier(bits) { + return function verify(thing, signature, publicKey) { + if (!bufferOrString(publicKey)) + throw typeError(MSG_INVALID_VERIFIER_KEY); + thing = normalizeInput(thing); + signature = toBase64(signature); + var verifier = crypto.createVerify('RSA-SHA' + bits); + verifier.update(thing); + return verifier.verify({key: publicKey, padding: crypto.constants.RSA_PKCS1_PSS_PADDING}, signature, 'base64'); + } +} + function createECDSASigner(bits) { var inner = createKeySigner(bits); return function sign() { @@ -122,16 +145,18 @@ module.exports = function jwa(algorithm) { var signerFactories = { hs: createHmacSigner, rs: createKeySigner, + ps: createPSSKeySigner, es: createECDSASigner, none: createNoneSigner, } var verifierFactories = { hs: createHmacVerifier, rs: createKeyVerifier, + ps: createPSSKeyVerifier, es: createECDSAVerifer, none: createNoneVerifier, } - var match = algorithm.match(/^(RS|ES|HS)(256|384|512)$|^(none)$/i); + var match = algorithm.match(/^(RS|PS|ES|HS)(256|384|512)$|^(none)$/i); if (!match) throw typeError(MSG_INVALID_ALGORITHM, algorithm); var algo = (match[1] || match[3]).toLowerCase(); diff --git a/test/jwa.test.js b/test/jwa.test.js index fae1658..2703f47 100644 --- a/test/jwa.test.js +++ b/test/jwa.test.js @@ -72,6 +72,19 @@ if (semver.gte(nodeVersion, '0.11.8')) { }); } +if (semver.satisfies(nodeVersion, '^6.12.0 || >=8.0.0')) { + test('RSA-PSS signing, verifying', function (t) { + const input = 'h. jon benjamin'; + BIT_DEPTHS.forEach(function (bits) { + const algo = jwa('ps'+bits); + const sig = algo.sign(input, rsaPrivateKey); + t.ok(algo.verify(input, sig, rsaPublicKey), 'should verify'); + t.notOk(algo.verify(input, sig, rsaWrongPublicKey), 'shoud not verify'); + }); + t.end(); + }); +} + BIT_DEPTHS.forEach(function (bits) { test('RS'+bits+': openssl sign -> js verify', function (t) { @@ -100,6 +113,35 @@ BIT_DEPTHS.forEach(function (bits) { }); }); +if (semver.satisfies(nodeVersion, '^6.12.0 || >=8.0.0')) { + BIT_DEPTHS.forEach(function (bits) { + test('PS'+bits+': openssl sign -> js verify', function (t) { + const input = 'iodine'; + const algo = jwa('ps'+bits); + const dgst = spawn('openssl', ['dgst', '-sha'+bits, '-sigopt', 'rsa_padding_mode:pss', '-sign', __dirname + '/rsa-private.pem']); + var buffer = Buffer.alloc(0); + + dgst.stdout.on('data', function (buf) { + buffer = Buffer.concat([buffer, buf]); + }); + + dgst.stdin.write(input, function() { + dgst.stdin.end(); + }); + + dgst.on('exit', function (code) { + if (code !== 0) + return t.fail('could not test interop: openssl failure'); + const sig = base64url(buffer); + + t.ok(algo.verify(input, sig, rsaPublicKey), 'should verify'); + t.notOk(algo.verify(input, sig, rsaWrongPublicKey), 'should not verify'); + t.end(); + }); + }); + }); +} + BIT_DEPTHS.forEach(function (bits) { test('ES'+bits+': signing, verifying', function (t) { const input = 'kristen schaal'; @@ -205,6 +247,45 @@ BIT_DEPTHS.forEach(function (bits) { }); }); +if (semver.satisfies(nodeVersion, '^6.12.0 || >=8.0.0')) { + BIT_DEPTHS.forEach(function (bits) { + const input = 'burgers'; + const inputFile = path.join(__dirname, 'interop.input.txt'); + const signatureFile = path.join(__dirname, 'interop.sig.txt'); + + function opensslVerify(keyfile) { + return spawn('openssl', [ + 'dgst', + '-sha'+bits, + '-sigopt', 'rsa_padding_mode:pss', + '-verify', keyfile, + '-signature', signatureFile, + inputFile + ]); + } + + test('PS'+bits+': js sign -> openssl verify', function (t) { + const publicKeyFile = path.join(__dirname, 'rsa-public.pem'); + const wrongPublicKeyFile = path.join(__dirname, 'rsa-wrong-public.pem'); + const privateKey = rsaPrivateKey; + const signature = + base64url.toBuffer( + jwa('ps'+bits).sign(input, privateKey) + ); + fs.writeFileSync(signatureFile, signature); + fs.writeFileSync(inputFile, input); + + t.plan(2); + opensslVerify(publicKeyFile).on('exit', function (code) { + t.same(code, 0, 'should be a successful exit'); + }); + opensslVerify(wrongPublicKeyFile).on('exit', function (code) { + t.same(code, 1, 'should be invalid'); + }); + }); + }); +} + test('jwa: none', function (t) { const input = 'whatever'; @@ -239,7 +320,7 @@ test('jwa: some garbage algorithm', function (t) { }); }); -['rs', 'es', 'hs'].forEach(function (partialAlg) { +['rs', 'ps', 'es', 'hs'].forEach(function (partialAlg) { test('jwa: partial strings of other algorithms', function (t) { try { jwa(partialAlg); @@ -308,3 +389,40 @@ test('jwa: rs512, missing verifying key', function (t) { } t.end(); }); + +if (semver.satisfies(nodeVersion, '^6.12.0 || >=8.0.0')) { + test('jwa: ps512, weird input type', function (t) { + const algo = jwa('ps512'); + const input = {a: ['whatever', 'this', 'is']}; + const sig = algo.sign(input, rsaPrivateKey); + t.ok(algo.verify(input, sig, rsaPublicKey), 'should verify'); + t.notOk(algo.verify(input, sig, rsaWrongPublicKey), 'should not verify'); + t.end(); + }); + + test('jwa: ps512, missing signing key', function (t) { + const algo = jwa('ps512'); + try { + algo.sign('some stuff'); + t.fail('should throw'); + } catch(ex) { + t.same(ex.name, 'TypeError'); + t.ok(ex.message.match(/key/), 'should say something about keys'); + } + t.end(); + }); + + test('jwa: ps512, missing verifying key', function (t) { + const algo = jwa('ps512'); + const input = {a: ['whatever', 'this', 'is']}; + const sig = algo.sign(input, rsaPrivateKey); + try { + algo.verify(input, sig); + t.fail('should throw'); + } catch(ex) { + t.same(ex.name, 'TypeError'); + t.ok(ex.message.match(/key/), 'should say something about keys'); + } + t.end(); + }); +}