From 091e8e958d5f12efab66aa347e6b3578d3701446 Mon Sep 17 00:00:00 2001 From: blacktemplar Date: Sat, 4 Apr 2020 15:31:45 +0200 Subject: [PATCH 1/4] add support for created and expires values --- lib/parser.js | 47 +++++++++++++++++++++++++++++++++++++++++++++-- lib/signer.js | 38 ++++++++++++++++++++++++++++++-------- 2 files changed, 75 insertions(+), 10 deletions(-) diff --git a/lib/parser.js b/lib/parser.js index c820dca..5d0d424 100644 --- a/lib/parser.js +++ b/lib/parser.js @@ -23,7 +23,8 @@ var ParamsState = { Name: 0, Quote: 1, Value: 2, - Comma: 3 + Comma: 3, + Number: 4, }; ///--- Specific Errors @@ -180,7 +181,13 @@ module.exports = { tmpValue = ''; substate = ParamsState.Value; } else { - throw new InvalidHeaderError('bad param format'); + //number + substate = ParamsState.Number; + var code = c.charCodeAt(0); + if (code < 0x30 || code > 0x39) { //character not in 0-9 + throw new InvalidHeaderError('bad param format'); + } + tmpValue = c; } break; @@ -193,6 +200,21 @@ module.exports = { } break; + case ParamsState.Number: + if (c === ',') { + parsed.params[tmpName] = parseInt(tmpValue); + tmpName = ''; + substate = ParamsState.Name; + } else { + var code = c.charCodeAt(0); + if (code < 0x30 || code > 0x39) { //character not in 0-9 + throw new InvalidHeaderError('bad param format'); + } + tmpValue += c; + } + break; + + case ParamsState.Comma: if (c === ',') { tmpName = ''; @@ -280,6 +302,10 @@ module.exports = { authzHeaderName + ' header'); } parsed.signingString += '(opaque): ' + opaque; + } else if (h === '(created)') { + parsed.signingString += '(created): ' + parsed.params.created; + } else if (h === '(expires)') { + parsed.signingString += '(expires): ' + parsed.params.expires; } else { var value = request.headers[h]; if (value === undefined) @@ -310,6 +336,23 @@ module.exports = { } } + if (parsed.params.created) { + var skew = parsed.params.created - Math.floor(Date.now() / 1000); + if (skew > options.clockSkew) { + throw new ExpiredRequestError('Created lies in the future (with ' + + 'skew ' + skew + 's greater than allowed ' + options.clockSkew + + 's'); + } + } + + if (parsed.params.expires) { + var expiredSince = Math.floor(Date.now() / 1000) - parsed.params.expires; + if (expiredSince > options.clockSkew) { + throw new ExpiredRequestError('Request expired with skew ' + expiredSince + + 's greater than allowed ' + options.clockSkew + 's'); + } + } + headers.forEach(function (hdr) { // Remember that we already checked any headers in the params // were in the request, so if this passes we're good. diff --git a/lib/signer.js b/lib/signer.js index 3262ac2..b0e7410 100644 --- a/lib/signer.js +++ b/lib/signer.js @@ -17,7 +17,7 @@ var validateAlgorithm = utils.validateAlgorithm; ///--- Globals -var AUTHZ_PARAMS = [ 'keyId', 'algorithm', 'opaque', 'headers', 'signature' ]; +var AUTHZ_PARAMS = [ 'keyId', 'algorithm', 'created', 'expires','opaque', 'headers', 'signature', ]; ///--- Specific Errors @@ -41,9 +41,13 @@ function FormatAuthz(prefix, params) { var value = params[param]; if (value === undefined) continue; - assert.string(value, 'params.' + param); + if (typeof value === 'number') { + authz += prefix + sprintf('%s=%s', param, value); + } else { + assert.string(value, 'params.' + param); - authz += prefix + sprintf('%s="%s"', param, value); + authz += prefix + sprintf('%s="%s"', param, value); + } prefix = ','; } @@ -294,6 +298,9 @@ module.exports = { * signing algorithm for the type of key given * - {String} httpVersion optional; defaults to '1.1'. * - {Boolean} strict optional; defaults to 'false'. + * - {int} expiresIn optional; defaults to 60. The + * seconds after which the signature should + * expire; * @return {Boolean} true if Authorization (and optionally Date) were added. * @throws {TypeError} on bad parameter types (input). * @throws {InvalidAlgorithmError} if algorithm was bad or incompatible with @@ -310,6 +317,7 @@ module.exports = { assert.optionalString(options.opaque, 'options.opaque'); assert.optionalArrayOfString(options.headers, 'options.headers'); assert.optionalString(options.httpVersion, 'options.httpVersion'); + assert.optionalNumber(options.expiresIn, 'options.expiresIn'); if (!request.getHeader('Date')) request.setHeader('Date', jsprim.rfc1123(new Date())); @@ -355,6 +363,11 @@ module.exports = { options.algorithm = alg[0] + '-' + alg[1]; } + var params = { + 'keyId': options.keyId, + 'algorithm': options.algorithm, + }; + var i; var stringToSign = ''; for (i = 0; i < headers.length; i++) { @@ -391,6 +404,18 @@ module.exports = { throw new MissingHeaderError('options.opaque was not in the request'); } stringToSign += '(opaque): ' + opaque; + } else if (h === '(created)') { + var created = Math.floor(Date.now() / 1000) + params.created = created; + stringToSign += '(created): ' + created; + } else if (h === '(expires)') { + var expiresIn = options.expiresIn; + if (expiresIn === undefined) { + expiresIn = 60; + } + const expires = Math.floor(Date.now() / 1000) + expiresIn; + params.expires = expires; + stringToSign += '(expires): ' + expires; } else { var value = request.getHeader(h); if (value === undefined || value === '') { @@ -431,11 +456,8 @@ module.exports = { var prefix = authzHeaderName.toLowerCase() === utils.HEADER.SIG ? '' : 'Signature '; - var params = { - 'keyId': options.keyId, - 'algorithm': options.algorithm, - 'signature': signature - }; + params.signature = signature; + if (options.opaque) params.opaque = options.opaque; if (options.headers) From 94b5ccae976fb8888aa80a5f6a7ba72131ed7a1b Mon Sep 17 00:00:00 2001 From: blacktemplar Date: Mon, 20 Apr 2020 10:45:16 +0200 Subject: [PATCH 2/4] use %d when printing number parameters --- lib/signer.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/signer.js b/lib/signer.js index b0e7410..073d56d 100644 --- a/lib/signer.js +++ b/lib/signer.js @@ -42,7 +42,7 @@ function FormatAuthz(prefix, params) { if (value === undefined) continue; if (typeof value === 'number') { - authz += prefix + sprintf('%s=%s', param, value); + authz += prefix + sprintf('%s=%d', param, value); } else { assert.string(value, 'params.' + param); From d0653f94d34db4113d56e56dd0b2ae26b22b716c Mon Sep 17 00:00:00 2001 From: blacktemplar Date: Mon, 20 Apr 2020 11:04:00 +0200 Subject: [PATCH 3/4] fix lint + style errors --- lib/parser.js | 19 ++++++++++--------- lib/signer.js | 13 +++++++------ 2 files changed, 17 insertions(+), 15 deletions(-) diff --git a/lib/parser.js b/lib/parser.js index 5d0d424..ab4e5d2 100644 --- a/lib/parser.js +++ b/lib/parser.js @@ -24,7 +24,7 @@ var ParamsState = { Quote: 1, Value: 2, Comma: 3, - Number: 4, + Number: 4 }; ///--- Specific Errors @@ -183,7 +183,7 @@ module.exports = { } else { //number substate = ParamsState.Number; - var code = c.charCodeAt(0); + code = c.charCodeAt(0); if (code < 0x30 || code > 0x39) { //character not in 0-9 throw new InvalidHeaderError('bad param format'); } @@ -202,11 +202,11 @@ module.exports = { case ParamsState.Number: if (c === ',') { - parsed.params[tmpName] = parseInt(tmpValue); + parsed.params[tmpName] = parseInt(tmpValue, 10); tmpName = ''; substate = ParamsState.Name; } else { - var code = c.charCodeAt(0); + code = c.charCodeAt(0); if (code < 0x30 || code > 0x39) { //character not in 0-9 throw new InvalidHeaderError('bad param format'); } @@ -319,6 +319,7 @@ module.exports = { // Check against the constraints var date; + var skew; if (request.headers.date || request.headers['x-date']) { if (request.headers['x-date']) { date = new Date(request.headers['x-date']); @@ -326,7 +327,7 @@ module.exports = { date = new Date(request.headers.date); } var now = new Date(); - var skew = Math.abs(now.getTime() - date.getTime()); + skew = Math.abs(now.getTime() - date.getTime()); if (skew > options.clockSkew * 1000) { throw new ExpiredRequestError('clock skew of ' + @@ -337,10 +338,10 @@ module.exports = { } if (parsed.params.created) { - var skew = parsed.params.created - Math.floor(Date.now() / 1000); + skew = parsed.params.created - Math.floor(Date.now() / 1000); if (skew > options.clockSkew) { throw new ExpiredRequestError('Created lies in the future (with ' + - 'skew ' + skew + 's greater than allowed ' + options.clockSkew + + 'skew ' + skew + 's greater than allowed ' + options.clockSkew + 's'); } } @@ -348,8 +349,8 @@ module.exports = { if (parsed.params.expires) { var expiredSince = Math.floor(Date.now() / 1000) - parsed.params.expires; if (expiredSince > options.clockSkew) { - throw new ExpiredRequestError('Request expired with skew ' + expiredSince + - 's greater than allowed ' + options.clockSkew + 's'); + throw new ExpiredRequestError('Request expired with skew ' + + expiredSince + 's greater than allowed ' + options.clockSkew + 's'); } } diff --git a/lib/signer.js b/lib/signer.js index 073d56d..87eed11 100644 --- a/lib/signer.js +++ b/lib/signer.js @@ -17,7 +17,8 @@ var validateAlgorithm = utils.validateAlgorithm; ///--- Globals -var AUTHZ_PARAMS = [ 'keyId', 'algorithm', 'created', 'expires','opaque', 'headers', 'signature', ]; +var AUTHZ_PARAMS = [ 'keyId', 'algorithm', 'created', 'expires', 'opaque', + 'headers', 'signature' ]; ///--- Specific Errors @@ -41,7 +42,7 @@ function FormatAuthz(prefix, params) { var value = params[param]; if (value === undefined) continue; - if (typeof value === 'number') { + if (typeof (value) === 'number') { authz += prefix + sprintf('%s=%d', param, value); } else { assert.string(value, 'params.' + param); @@ -298,7 +299,7 @@ module.exports = { * signing algorithm for the type of key given * - {String} httpVersion optional; defaults to '1.1'. * - {Boolean} strict optional; defaults to 'false'. - * - {int} expiresIn optional; defaults to 60. The + * - {int} expiresIn optional; defaults to 60. The * seconds after which the signature should * expire; * @return {Boolean} true if Authorization (and optionally Date) were added. @@ -365,7 +366,7 @@ module.exports = { var params = { 'keyId': options.keyId, - 'algorithm': options.algorithm, + 'algorithm': options.algorithm }; var i; @@ -405,7 +406,7 @@ module.exports = { } stringToSign += '(opaque): ' + opaque; } else if (h === '(created)') { - var created = Math.floor(Date.now() / 1000) + var created = Math.floor(Date.now() / 1000); params.created = created; stringToSign += '(created): ' + created; } else if (h === '(expires)') { @@ -457,7 +458,7 @@ module.exports = { '' : 'Signature '; params.signature = signature; - + if (options.opaque) params.opaque = options.opaque; if (options.headers) From 3e92629be9fbdfd9f51c3a314c4d38d1b050d650 Mon Sep 17 00:00:00 2001 From: blacktemplar Date: Mon, 20 Apr 2020 12:00:00 +0200 Subject: [PATCH 4/4] add tests for parsing created and expires --- test/parser.test.js | 229 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 229 insertions(+) diff --git a/test/parser.test.js b/test/parser.test.js index 9b6e35d..7c626f9 100644 --- a/test/parser.test.js +++ b/test/parser.test.js @@ -288,6 +288,233 @@ test('no date header', function(t) { }); }); +test('valid numeric parameter', function(t) { + server.tester = function(req, res) { + var options = { + headers: ['(created)', 'digest'] + }; + + try { + httpSignature.parseRequest(req, options); + } catch (e) { + t.fail(e.stack); + } + + res.writeHead(200); + res.end(); + }; + + options.headers.Authorization = + 'Signature keyId="f,oo",algorithm="RSA-sha256",' + + 'created=123456,' + + 'headers="(created) dIgEsT",signature="digitalSignature"'; + options.headers['digest'] = uuid(); + http.get(options, function(res) { + t.equal(res.statusCode, 200); + t.end(); + }); +}); + +test('invalid numeric parameter', function(t) { + server.tester = function(req, res) { + var options = { + headers: ['(created)', 'digest'] + }; + + try { + httpSignature.parseRequest(req, options); + } catch (e) { + t.equal(e.name, 'InvalidHeaderError'); + t.equal(e.message, 'bad param format'); + res.writeHead(200); + res.end(); + return; + } + + t.fail("should throw error"); + res.writeHead(200); + res.end(); + }; + + options.headers.Authorization = + 'Signature keyId="f,oo",algorithm="RSA-sha256",' + + 'created=123@456,' + + 'headers="(created) dIgEsT",signature="digitalSignature"'; + options.headers['digest'] = uuid(); + http.get(options, function(res) { + t.equal(res.statusCode, 200); + t.end(); + }); +}); + +test('invalid numeric parameter - decimal', function(t) { + server.tester = function(req, res) { + var options = { + headers: ['(created)', 'digest'] + }; + + try { + httpSignature.parseRequest(req, options); + } catch (e) { + t.equal(e.name, 'InvalidHeaderError'); + t.equal(e.message, 'bad param format'); + res.writeHead(200); + res.end(); + return; + } + + t.fail("should throw error"); + res.writeHead(200); + res.end(); + }; + + options.headers.Authorization = + 'Signature keyId="f,oo",algorithm="RSA-sha256",' + + 'created=123.456,' + + 'headers="(created) dIgEsT",signature="digitalSignature"'; + options.headers['digest'] = uuid(); + http.get(options, function(res) { + t.equal(res.statusCode, 200); + t.end(); + }); +}); + +test('invalid numeric parameter - signed integer', function(t) { + server.tester = function(req, res) { + var options = { + headers: ['(created)', 'digest'] + }; + + try { + httpSignature.parseRequest(req, options); + } catch (e) { + t.equal(e.name, 'InvalidHeaderError'); + t.equal(e.message, 'bad param format'); + res.writeHead(200); + res.end(); + return; + } + + t.fail("should throw error"); + res.writeHead(200); + res.end(); + }; + + options.headers.Authorization = + 'Signature keyId="f,oo",algorithm="RSA-sha256",' + + 'created=-123456,' + + 'headers="(created) dIgEsT",signature="digitalSignature"'; + options.headers['digest'] = uuid(); + http.get(options, function(res) { + t.equal(res.statusCode, 200); + t.end(); + }); +}); + +test('created in future', function(t) { + var skew = 1000; + server.tester = function(req, res) { + var options = { + headers: ['(created)', 'digest'], + clockSkew: skew + }; + + try { + httpSignature.parseRequest(req, options); + } catch (e) { + t.equal(e.name, 'ExpiredRequestError'); + t.similar(e.message, new RegExp('Created lies in the future.*')); + res.writeHead(200); + res.end(); + return; + } + + t.fail("should throw error"); + res.writeHead(200); + res.end(); + }; + + var created = Math.floor(Date.now() / 1000) + skew + 10; + options.headers.Authorization = + 'Signature keyId="f,oo",algorithm="RSA-sha256",' + + 'created=' + created + ',' + + 'headers="(created) dIgEsT",signature="digitalSignature"'; + options.headers['digest'] = uuid(); + http.get(options, function(res) { + t.equal(res.statusCode, 200); + t.end(); + }); +}); + +test('expires expired', function(t) { + var skew = 1000; + server.tester = function(req, res) { + var options = { + headers: ['(expires)', 'digest'], + clockSkew: skew + }; + + try { + httpSignature.parseRequest(req, options); + } catch (e) { + t.equal(e.name, 'ExpiredRequestError'); + t.similar(e.message, new RegExp('Request expired.*')); + res.writeHead(200); + res.end(); + return; + } + + t.fail("should throw error"); + res.writeHead(200); + res.end(); + }; + + var expires = Math.floor(Date.now() / 1000) - skew - 1; + options.headers.Authorization = + 'Signature keyId="f,oo",algorithm="RSA-sha256",' + + 'expires=' + expires + ',' + + 'headers="(expires) dIgEsT",signature="digitalSignature"'; + options.headers['digest'] = uuid(); + http.get(options, function(res) { + t.equal(res.statusCode, 200); + t.end(); + }); +}); + +test('valid created and expires with skew', function(t) { + var skew = 1000; + server.tester = function(req, res) { + var options = { + headers: ['(created)', '(expires)', 'digest'], + clockSkew: skew + }; + + try { + httpSignature.parseRequest(req, options); + } catch (e) { + t.fail(e.stack); + } + + res.writeHead(200); + res.end(); + }; + + //created is in the future but within allowed skew + var created = Math.floor(Date.now() / 1000) + skew - 1; + //expires is in the past but within allowed skew + var expires = Math.floor(Date.now() / 1000) - skew + 10; + options.headers.Authorization = + 'Signature keyId="f,oo",algorithm="RSA-sha256",' + + 'created=' + created + ',' + 'expires=' + expires + ',' + + 'headers="(created) (expires) dIgEsT",signature="digitalSignature"'; + options.headers['digest'] = uuid(); + http.get(options, function(res) { + t.equal(res.statusCode, 200); + t.end(); + }); +}); + + test('valid default headers', function(t) { server.tester = function(req, res) { @@ -613,6 +840,8 @@ test('not whitelisted algorithm', function(t) { }); + + test('tearDown', function(t) { server.on('close', function() { t.end();