Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
29 changes: 27 additions & 2 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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() {
Expand Down Expand Up @@ -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();
Expand Down
120 changes: 119 additions & 1 deletion test/jwa.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down Expand Up @@ -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';
Expand Down Expand Up @@ -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';
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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();
});
}