diff --git a/fetch-cert.js b/fetch-cert.js index 956f079..c2189fd 100644 --- a/fetch-cert.js +++ b/fetch-cert.js @@ -5,7 +5,7 @@ var https = require('https') var globalCache = {} // default in-memory cache for downloaded certificates -module.exports = function fetchCert(options, callback) { +module.exports = function fetchCert (options, callback) { var url = options.url var cache = options.cache || globalCache var cachedResponse = cache[url.href] @@ -18,7 +18,7 @@ module.exports = function fetchCert(options, callback) { var body = '' - https.get(url.href, function(response) { + https.get(url.href, function (response) { var statusCode if (!response || 200 !== response.statusCode) { diff --git a/index.js b/index.js index 5e2c10b..7f8c6c1 100644 --- a/index.js +++ b/index.js @@ -12,22 +12,19 @@ var validator = require('validator') var TIMESTAMP_TOLERANCE = 150 var SIGNATURE_FORMAT = 'base64' -function getCert(cert_url, callback) { +function getCert (cert_url, callback) { var options = { url: url.parse(cert_url) } var result = validateCertUri(options.url) - if (result !== true) { + if (result !== true) return process.nextTick(callback, result) - } - fetchCert(options, function(er, pem_cert) { - if (er) { + fetchCert(options, function (er, pem_cert) { + if (er) return callback(er) - } er = validateCert(pem_cert) - if (er) { + if (er) return callback(er) - } callback(er, pem_cert) }) @@ -35,7 +32,7 @@ function getCert(cert_url, callback) { // returns true if the signature for the request body is valid, false otherwise -function isValidSignature(pem_cert, signature, requestBody) { +function isValidSignature (pem_cert, signature, requestBody) { var verifier = crypto.createVerify('RSA-SHA1') verifier.update(requestBody, 'utf8') return verifier.verify(pem_cert, signature, SIGNATURE_FORMAT) @@ -45,7 +42,7 @@ function isValidSignature(pem_cert, signature, requestBody) { // determine if a timestamp is valid for a given request with a tolerance of // TIMESTAMP_TOLERANCE seconds // returns undefined if valid, or an error string otherwise -function validateTimestamp(requestBody) { +function validateTimestamp (requestBody) { var d, e, error, now, oldestTime, request_json try { request_json = JSON.parse(requestBody) @@ -53,49 +50,45 @@ function validateTimestamp(requestBody) { e = error return 'request body invalid json' } - if (!(request_json.request && request_json.request.timestamp)) { + if (!(request_json.request && request_json.request.timestamp)) return 'Timestamp field not present in request' - } + d = new Date(request_json.request.timestamp) now = new Date() oldestTime = now.getTime() - (TIMESTAMP_TOLERANCE * 1000) - if (d.getTime() < oldestTime) { + if (d.getTime() < oldestTime) return 'Request is from more than ' + TIMESTAMP_TOLERANCE + ' seconds ago' - } } // certificate validator express middleware for amazon echo -module.exports = function verifier(cert_url, signature, requestBody, callback) { +module.exports = function verifier (cert_url, signature, requestBody, callback) { var er - if(!cert_url) { + if(!cert_url) return process.nextTick(callback, 'missing certificate url') - } - if (!signature) { + if (!signature) return process.nextTick(callback, 'missing signature') - } - if (!requestBody) { + + if (!requestBody) return process.nextTick(callback, 'missing request (certificate) body') - } - if (!validator.isBase64(signature)) { + if (!validator.isBase64(signature)) return process.nextTick(callback, 'invalid signature (not base64 encoded)') - } + er = validateTimestamp(requestBody) - if (er) { + if (er) return process.nextTick(callback, er) - } - getCert(cert_url, function(er, pem_cert) { - if (er) { + getCert(cert_url, function (er, pem_cert) { + if (er) return callback(er) - } - if (!isValidSignature(pem_cert, signature, requestBody)) { + + if (!isValidSignature(pem_cert, signature, requestBody)) return callback('invalid signature') - } + callback() }) } diff --git a/test/index.js b/test/index.js index b5cc498..aadcc5b 100644 --- a/test/index.js +++ b/test/index.js @@ -8,7 +8,7 @@ var sinon = require('sinon') var cert_url = 'https://s3.amazonaws.com/echo.api/echo-api-cert-5.pem' // latest valid cert -test('handle missing cert_url parameter', function(t) { +test('handle missing cert_url parameter', function (t) { var body, now, signature signature = 'JbWZ4iO5ogpq1NhsOqyqq/QRrvc1/XyDwjcBO9wWSk//c11+gImmtWzMG9tDEW40t0Xwt1cnGU93DwUZQzMyzJ5CMi+09qVQUSIHiSmPekKaQRxS0Ibu7l7cXXuCcOBupbkheD/Dsd897Bm5SQwd1cFKRv+PJlpmGKimgh2QmbivogsEkFl8b9SW48kjKWazwj/XP2SrHY0bTvwMTVu7zvTcp0ZenEGlY2DNr5zSd1n6lmS6rgAt1IPwhBzqI0PVMngaM0DQhB0wUPj3QoIUh0IyMVAQzRFbQpS4UGrA4M9a5a+AGy0jCQKiRCI+Yi9iZYEVYvfafF/lyOUHHYcpOg==' now = new Date() @@ -17,14 +17,14 @@ test('handle missing cert_url parameter', function(t) { timestamp: now.getTime() } } - verifier(undefined, signature, JSON.stringify(body), function(er) { + verifier(undefined, signature, JSON.stringify(body), function (er) { t.equal(er, 'missing certificate url') t.end() }) }) -test('handle invalid cert_url parameter', function(t) { +test('handle invalid cert_url parameter', function (t) { var body, now, signature signature = 'JbWZ4iO5ogpq1NhsOqyqq/QRrvc1/XyDwjcBO9wWSk//c11+gImmtWzMG9tDEW40t0Xwt1cnGU93DwUZQzMyzJ5CMi+09qVQUSIHiSmPekKaQRxS0Ibu7l7cXXuCcOBupbkheD/Dsd897Bm5SQwd1cFKRv+PJlpmGKimgh2QmbivogsEkFl8b9SW48kjKWazwj/XP2SrHY0bTvwMTVu7zvTcp0ZenEGlY2DNr5zSd1n6lmS6rgAt1IPwhBzqI0PVMngaM0DQhB0wUPj3QoIUh0IyMVAQzRFbQpS4UGrA4M9a5a+AGy0jCQKiRCI+Yi9iZYEVYvfafF/lyOUHHYcpOg==' now = new Date() @@ -33,14 +33,14 @@ test('handle invalid cert_url parameter', function(t) { timestamp: now.getTime() } } - verifier('http://someinsecureurl', signature, JSON.stringify(body), function(er) { + verifier('http://someinsecureurl', signature, JSON.stringify(body), function (er) { t.equal(er.indexOf('Certificate URI MUST be https'), 0) t.end() }) }) -test('handle invalid body json', function(t) { +test('handle invalid body json', function (t) { var signature = 'JbWZ4iO5ogpq1NhsOqyqq/QRrvc1/XyDwjcBO9wWSk//c11+gImmtWzMG9tDEW40t0Xwt1cnGU93DwUZQzMyzJ5CMi+09qVQUSIHiSmPekKaQRxS0Ibu7l7cXXuCcOBupbkheD/Dsd897Bm5SQwd1cFKRv+PJlpmGKimgh2QmbivogsEkFl8b9SW48kjKWazwj/XP2SrHY0bTvwMTVu7zvTcp0ZenEGlY2DNr5zSd1n6lmS6rgAt1IPwhBzqI0PVMngaM0DQhB0wUPj3QoIUh0IyMVAQzRFbQpS4UGrA4M9a5a+AGy0jCQKiRCI+Yi9iZYEVYvfafF/lyOUHHYcpOg==' verifier(cert_url, signature, '', function(er) { t.equal(er, 'missing request (certificate) body') @@ -49,7 +49,7 @@ test('handle invalid body json', function(t) { }) -test('handle missing timestamp field', function(t) { +test('handle missing timestamp field', function (t) { var signature = 'JbWZ4iO5ogpq1NhsOqyqq/QRrvc1/XyDwjcBO9wWSk//c11+gImmtWzMG9tDEW40t0Xwt1cnGU93DwUZQzMyzJ5CMi+09qVQUSIHiSmPekKaQRxS0Ibu7l7cXXuCcOBupbkheD/Dsd897Bm5SQwd1cFKRv+PJlpmGKimgh2QmbivogsEkFl8b9SW48kjKWazwj/XP2SrHY0bTvwMTVu7zvTcp0ZenEGlY2DNr5zSd1n6lmS6rgAt1IPwhBzqI0PVMngaM0DQhB0wUPj3QoIUh0IyMVAQzRFbQpS4UGrA4M9a5a+AGy0jCQKiRCI+Yi9iZYEVYvfafF/lyOUHHYcpOg==' verifier(cert_url, signature, '{}', function(er) { t.equal(er, 'Timestamp field not present in request') @@ -58,7 +58,7 @@ test('handle missing timestamp field', function(t) { }) -test('handle outdated timestamp field', function(t) { +test('handle outdated timestamp field', function (t) { var body, now, signature signature = 'JbWZ4iO5ogpq1NhsOqyqq/QRrvc1/XyDwjcBO9wWSk//c11+gImmtWzMG9tDEW40t0Xwt1cnGU93DwUZQzMyzJ5CMi+09qVQUSIHiSmPekKaQRxS0Ibu7l7cXXuCcOBupbkheD/Dsd897Bm5SQwd1cFKRv+PJlpmGKimgh2QmbivogsEkFl8b9SW48kjKWazwj/XP2SrHY0bTvwMTVu7zvTcp0ZenEGlY2DNr5zSd1n6lmS6rgAt1IPwhBzqI0PVMngaM0DQhB0wUPj3QoIUh0IyMVAQzRFbQpS4UGrA4M9a5a+AGy0jCQKiRCI+Yi9iZYEVYvfafF/lyOUHHYcpOg==' now = new Date() @@ -74,7 +74,7 @@ test('handle outdated timestamp field', function(t) { }) -test('handle missing signature parameter', function(t) { +test('handle missing signature parameter', function (t) { var body, now now = new Date() body = { @@ -82,7 +82,7 @@ test('handle missing signature parameter', function(t) { timestamp: now.getTime() } } - verifier(cert_url, undefined, JSON.stringify(body), function(er) { + verifier(cert_url, undefined, JSON.stringify(body), function (er) { t.equal(er, 'missing signature') t.end() }) @@ -97,13 +97,13 @@ test('handle invalid signature parameter', function(t) { timestamp: now.getTime() } } - verifier(cert_url, '....$#%@$se', JSON.stringify(body), function(er) { + verifier(cert_url, '....$#%@$se', JSON.stringify(body), function (er) { t.equal(er, 'invalid signature (not base64 encoded)') t.end() }) }) -test('handle invalid base64-encoded signature parameter', function(t) { +test('handle invalid base64-encoded signature parameter', function (t) { var body, now now = new Date() body = { @@ -111,18 +111,18 @@ test('handle invalid base64-encoded signature parameter', function(t) { timestamp: now.getTime() } } - verifier(cert_url, 'aGVsbG8NCg==', JSON.stringify(body), function(er) { + verifier(cert_url, 'aGVsbG8NCg==', JSON.stringify(body), function (er) { t.equal(er, 'invalid signature') t.end() }) }) -test('handle valid signature', function(t) { - var ts = '2017-02-10T07:27:59Z'; - var now = new Date(ts); - var clock = sinon.useFakeTimers(now.getTime()); +test('handle valid signature', function (t) { + var ts = '2017-02-10T07:27:59Z' + var now = new Date(ts) + var clock = sinon.useFakeTimers(now.getTime()) var cert_url = 'https://s3.amazonaws.com/echo.api/echo-api-cert-4.pem' - var signature = 'Qc8OuaGEHWeL/39XTEDYFbOCufYWpwi45rqmM2R4WaSEYcSXq+hUko/88wv48+6SPUiEddWSEEINJFAFV5auYZsnBzqCK+SO8mGNOGHmLYpcFuSEHI3eA3nDIEARrXTivqqbH/LCPJHc0tqNYr3yPZRIR2mYFndJOxgDNSOooZX+tp2GafHHsjjShCjmePaLxJiGG1DmrL6fyOJoLrzc0olUxLmnJviS6Q5wBir899TMEZ/zX+aiBTt/khVvwIh+hI/PZsRq/pQw4WAvQz1bcnGNamvMA/TKSJtR0elJP+TgCqbVoYisDgQXkhi8/wonkLhs68pN+TurbR7GyC1vxw=='; + var signature = 'Qc8OuaGEHWeL/39XTEDYFbOCufYWpwi45rqmM2R4WaSEYcSXq+hUko/88wv48+6SPUiEddWSEEINJFAFV5auYZsnBzqCK+SO8mGNOGHmLYpcFuSEHI3eA3nDIEARrXTivqqbH/LCPJHc0tqNYr3yPZRIR2mYFndJOxgDNSOooZX+tp2GafHHsjjShCjmePaLxJiGG1DmrL6fyOJoLrzc0olUxLmnJviS6Q5wBir899TMEZ/zX+aiBTt/khVvwIh+hI/PZsRq/pQw4WAvQz1bcnGNamvMA/TKSJtR0elJP+TgCqbVoYisDgQXkhi8/wonkLhs68pN+TurbR7GyC1vxw==' var body = { "version": "1.0", "session": { @@ -146,19 +146,20 @@ test('handle valid signature', function(t) { }, "inDialog": false } - }; - verifier(cert_url, signature, JSON.stringify(body), function(er) { - t.equal(er, undefined); - clock.restore(); + } + + verifier(cert_url, signature, JSON.stringify(body), function (er) { + t.equal(er, undefined) + clock.restore() t.end() - }); + }) }) -test('handle valid signature with double byte utf8 encodings', function(t) { - var ts = '2017-04-05T12:02:36Z'; - var now = new Date(ts); - var clock = sinon.useFakeTimers(now.getTime()); +test('handle valid signature with double byte utf8 encodings', function (t) { + var ts = '2017-04-05T12:02:36Z' + var now = new Date(ts) + var clock = sinon.useFakeTimers(now.getTime()) var cert_url = 'https://s3.amazonaws.com/echo.api/echo-api-cert-4.pem' - var signature = 'WLShxe8KMwHUt8hVD5+iE4tDO+J8Li21yocDWnq8LVRpE2PMMWCxjQzOCzyoFm4i/yW07UKtKQxcnzB44ZEdP6e6HelwBwEdP4lb8jQcc5knk8SuUth4N7cu6Em8FPOdOJdd9idHbO/p8BTb14wgua5n+1SDKHm+wPikOVsfCMYsXcwRWx5FsgP1wVPrDsCHN/ISiCXz+UuMnd6H0uRNdLZ/x/ikPkknh+P1kuFa2a2LN4r57IwBDAxkdf9MzXEexSOO0nWLnyJY2VAFB+O7JKE39CwMJ1+YDOwTTTLjilkCnSlfnr6DP4HPGHnYhh2HQZle8UBrSDm4ntflErpISQ=='; + var signature = 'WLShxe8KMwHUt8hVD5+iE4tDO+J8Li21yocDWnq8LVRpE2PMMWCxjQzOCzyoFm4i/yW07UKtKQxcnzB44ZEdP6e6HelwBwEdP4lb8jQcc5knk8SuUth4N7cu6Em8FPOdOJdd9idHbO/p8BTb14wgua5n+1SDKHm+wPikOVsfCMYsXcwRWx5FsgP1wVPrDsCHN/ISiCXz+UuMnd6H0uRNdLZ/x/ikPkknh+P1kuFa2a2LN4r57IwBDAxkdf9MzXEexSOO0nWLnyJY2VAFB+O7JKE39CwMJ1+YDOwTTTLjilkCnSlfnr6DP4HPGHnYhh2HQZle8UBrSDm4ntflErpISQ==' var body = { "version":"1.0", "session":{ @@ -189,10 +190,10 @@ test('handle valid signature with double byte utf8 encodings', function(t) { } } } -}; - verifier(cert_url, signature, JSON.stringify(body), function(er) { - t.equal(er, undefined); - clock.restore(); +} + verifier(cert_url, signature, JSON.stringify(body), function (er) { + t.equal(er, undefined) + clock.restore() t.end() - }); + }) }) diff --git a/test/validate-cert-uri.js b/test/validate-cert-uri.js index 13a38b6..7142b31 100644 --- a/test/validate-cert-uri.js +++ b/test/validate-cert-uri.js @@ -9,7 +9,7 @@ var validateCertUri = require('../validate-cert-uri') unroll.use(test) unroll('verifier.validateCertUri should be #valid for #url', - function(t, testArgs) { + function (t, testArgs) { var cert_uri = url.parse(testArgs['url']) var result = validateCertUri(cert_uri) var valid = testArgs['valid'] diff --git a/test/validate-cert.js b/test/validate-cert.js index 498c959..1f9fd85 100644 --- a/test/validate-cert.js +++ b/test/validate-cert.js @@ -7,7 +7,7 @@ var url = require('url') var validate = require('../validate-cert') -function createInvalidCert() { +function createInvalidCert () { var keys = pki.rsa.generateKeyPair(512) var cert = pki.createCertificate() cert.publicKey = keys.publicKey @@ -85,24 +85,24 @@ function createInvalidCert() { return pki.certificateToPem(cert) } -test('fails on invalid pem cert parameter', function(t) { +test('fails on invalid pem cert parameter', function (t) { t.assert(validate(undefined) !== undefined, 'Error should have been thrown') t.end() }) -test('fails on non amazon subject alt name', function(t) { +test('fails on non amazon subject alt name', function (t) { var pem = createInvalidCert() t.assert(validate(pem) === 'invalid certificate validity (correct domain not found in subject alternative names)', 'Certificate must be from amazon') t.end() }) -test('fails on expired certificate (Not After)', function(t) { +test('fails on expired certificate (Not After)', function (t) { var pem = fs.readFileSync(__dirname + '/cert-expired.pem') t.assert(validate(pem) === 'invalid certificate validity (past expired date)') t.end() }) -test('approves valid certifcate', function(t) { +test('approves valid certifcate', function (t) { var pem = fs.readFileSync(__dirname + '/cert-latest.pem') t.assert(validate(pem) === undefined, 'Certificate should be valid') t.end() diff --git a/validate-cert-uri.js b/validate-cert-uri.js index 24dcf7f..2442bad 100644 --- a/validate-cert-uri.js +++ b/validate-cert-uri.js @@ -7,17 +7,17 @@ var VALID_CERT_PORT = '443' // parse a certificate and check it's contents for validity module.exports = function validateCertUri(cert_uri) { - if (cert_uri.protocol !== 'https:') { + if (cert_uri.protocol !== 'https:') return 'Certificate URI MUST be https: ' + cert_uri - } - if (cert_uri.port && (cert_uri.port !== VALID_CERT_PORT)) { + + if (cert_uri.port && (cert_uri.port !== VALID_CERT_PORT)) return 'Certificate URI port MUST be ' + VALID_CERT_PORT + ', was: ' + cert_uri.port - } - if (cert_uri.hostname !== VALID_CERT_HOSTNAME) { + + if (cert_uri.hostname !== VALID_CERT_HOSTNAME) return 'Certificate URI hostname must be ' + VALID_CERT_HOSTNAME + ': ' + cert_uri.hostname - } - if (cert_uri.path.indexOf(VALID_CERT_PATH_START) !== 0) { + + if (cert_uri.path.indexOf(VALID_CERT_PATH_START) !== 0) return 'Certificate URI path must start with ' + VALID_CERT_PATH_START + ': ' + cert_uri - } + return true } diff --git a/validate-cert.js b/validate-cert.js index 83f7099..57e793c 100644 --- a/validate-cert.js +++ b/validate-cert.js @@ -2,38 +2,36 @@ var pki = require('node-forge').pki + // constants var VALID_CERT_SAN = 'echo-api.amazon.com' // @returns an error string if certificate isn't valid, undefined otherwise -module.exports = function validate(pem_cert) { +module.exports = function validate (pem_cert) { try { var cert = pki.certificateFromPem(pem_cert) // check that cert has a Subject Alternative Names (SANs) section var altNameExt = cert.getExtension("subjectAltName") - if (!altNameExt) { + if (!altNameExt) return 'invalid certificate validity (subjectAltName extension not present)' - } // check that the domain echo-api.amazon.com is present in SANs section var domainExists = altNameExt.altNames.some(function(name) { return name.value === VALID_CERT_SAN }) - if(!domainExists) { + if(!domainExists) return 'invalid certificate validity (correct domain not found in subject alternative names)' - } var currTime = new Date().getTime() var notAfterTime = new Date(cert.validity.notAfter).getTime() - if (notAfterTime <= currTime) { + if (notAfterTime <= currTime) return 'invalid certificate validity (past expired date)' - } var notBeforeTime = new Date(cert.validity.notBefore).getTime() - if (currTime <= notBeforeTime) { + if (currTime <= notBeforeTime) return 'invalid certificate validity (start date is in the future)' - } + } catch (e) { return e }