diff --git a/.gitignore b/.gitignore index 86ec33a..b81bdb4 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,5 @@ node_modules # Don't checkin package-lock.json for libraries package-lock.json + +.nyc_output diff --git a/.npmrc b/.npmrc new file mode 100644 index 0000000..e29745a --- /dev/null +++ b/.npmrc @@ -0,0 +1,2 @@ +engine-strict=true + diff --git a/.travis.yml b/.travis.yml index 5ad3afa..4e036de 100644 --- a/.travis.yml +++ b/.travis.yml @@ -3,35 +3,31 @@ sudo: false language: node_js node_js: -- '4' -- '5' -- '6' -- '7' -- '8' -- '9' -- '10' +- '12' +- '14' +- '16' matrix: include: - - node_js: '4' + - node_js: '12' env: BROWSER_NAME=chrome BROWSER_VERSION=latest - - node_js: '4' + - node_js: '12' env: BROWSER_NAME=chrome BROWSER_VERSION=29 - - node_js: '4' + - node_js: '12' env: BROWSER_NAME=firefox BROWSER_VERSION=latest - - node_js: '4' + - node_js: '12' env: BROWSER_NAME=opera BROWSER_VERSION=latest - - node_js: '4' + - node_js: '12' env: BROWSER_NAME=safari BROWSER_VERSION=latest - - node_js: '4' + - node_js: '12' env: BROWSER_NAME=safari BROWSER_VERSION=7 - - node_js: '4' + - node_js: '12' env: BROWSER_NAME=safari BROWSER_VERSION=6 - - node_js: '4' + - node_js: '12' env: BROWSER_NAME=safari BROWSER_VERSION=5 - - node_js: '4' + - node_js: '12' env: BROWSER_NAME=ie BROWSER_VERSION=11 - - node_js: '4' + - node_js: '12' env: BROWSER_NAME=ie BROWSER_VERSION=10 - - node_js: '4' + - node_js: '12' env: BROWSER_NAME=ie BROWSER_VERSION=9 \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..3621f82 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,5 @@ +# 3.0.0 +* BREAKING: publish as a pure ES module +* BREAKING: only support node >= 12.17 +* BREAKING: switch to es6 everywhere +* update tap dependency diff --git a/fetch-cert.js b/fetch-cert.js index c2189fd..b0b7cb4 100644 --- a/fetch-cert.js +++ b/fetch-cert.js @@ -1,41 +1,39 @@ -'use strict' +import https from 'https' -var https = require('https') +const globalCache = { } // default in-memory cache for downloaded certificates -var globalCache = {} // default in-memory cache for downloaded certificates -module.exports = function fetchCert (options, callback) { - var url = options.url - var cache = options.cache || globalCache - var cachedResponse = cache[url.href] - var servedFromCache = false - if (cachedResponse) { - servedFromCache = true - process.nextTick(callback, undefined, cachedResponse, servedFromCache) - return - } +export default function fetchCert (options, callback) { + const url = options.url + const cache = options.cache || globalCache + const cachedResponse = cache[url.href] + let servedFromCache = false + if (cachedResponse) { + servedFromCache = true + process.nextTick(callback, undefined, cachedResponse, servedFromCache) + return + } - var body = '' + let body = '' - https.get(url.href, function (response) { - var statusCode + https.get(url.href, function (response) { + if (!response || 200 !== response.statusCode) { + const statusCode = response ? response.statusCode : 0 + return callback('Failed to download certificate at: ' + url.href + '. Response code: ' + statusCode) + } - if (!response || 200 !== response.statusCode) { - statusCode = response ? response.statusCode : 0 - return callback('Failed to download certificate at: ' + url.href + '. Response code: ' + statusCode) - } + response.setEncoding('utf8') + response.on('data', function (chunk) { + body += chunk + }) - response.setEncoding('utf8') - response.on('data', function (chunk) { - body += chunk + response.on('end', function () { + cache[url.href] = body + callback(undefined, body, servedFromCache) + }) }) - response.on('end', function () { - cache[url.href] = body - callback(undefined, body, servedFromCache) + .on('error', function (er) { + callback('Failed to download certificate at: ' + url.href +'. Error: ' + er) }) - }) - .on('error', function(er) { - callback('Failed to download certificate at: ' + url.href +'. Error: ' + er) - }) } diff --git a/index.js b/index.js index 8eac4d2..99d894f 100644 --- a/index.js +++ b/index.js @@ -1,41 +1,39 @@ -'use strict' +import crypto from 'crypto' +import fetchCert from './fetch-cert.js' +import url from 'url' +import validateCert from './validate-cert.js' +import validateCertUri from './validate-cert-uri.js' +import validator from 'validator' -var crypto = require('crypto') -var fetchCert = require('./fetch-cert') -var url = require('url') -var validateCert = require('./validate-cert') -var validateCertUri = require('./validate-cert-uri') -var validator = require('validator') +const TIMESTAMP_TOLERANCE = 150 +const SIGNATURE_FORMAT = 'base64' -// constants -var TIMESTAMP_TOLERANCE = 150 -var SIGNATURE_FORMAT = 'base64' function getCert (cert_url, callback) { - var options = { url: url.parse(cert_url) } - var result = validateCertUri(options.url) - if (result !== true) - return process.nextTick(callback, result) + const options = { url: url.parse(cert_url) } + const result = validateCertUri(options.url) + if (result !== true) + return process.nextTick(callback, result) - fetchCert(options, function (er, pem_cert) { - if (er) - return callback(er) + fetchCert(options, function (er, pem_cert) { + if (er) + return callback(er) - er = validateCert(pem_cert) - if (er) - return callback(er) + er = validateCert(pem_cert) + if (er) + return callback(er) - callback(er, pem_cert) - }) + callback(er, pem_cert) + }) } // returns true if the signature for the request body is valid, false otherwise function isValidSignature (pem_cert, signature, requestBody) { - var verifier = crypto.createVerify('RSA-SHA1') - verifier.update(requestBody, 'utf8') - return verifier.verify(pem_cert, signature, SIGNATURE_FORMAT) + const verifier = crypto.createVerify('RSA-SHA1') + verifier.update(requestBody, 'utf8') + return verifier.verify(pem_cert, signature, SIGNATURE_FORMAT) } @@ -43,66 +41,67 @@ function isValidSignature (pem_cert, signature, requestBody) { // TIMESTAMP_TOLERANCE seconds // returns undefined if valid, or an error string otherwise function validateTimestamp (requestBody) { - var d, e, error, now, oldestTime, request_json - try { - request_json = JSON.parse(requestBody) - } catch (error) { - e = error - return 'request body invalid json' - } - 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) - return 'Request is from more than ' + TIMESTAMP_TOLERANCE + ' seconds ago' -} + let e, error, request_json + try { + request_json = JSON.parse(requestBody) + } catch (error) { + e = error + return 'request body invalid json' + } -function verifier (cert_url, signature, requestBody, callback) { - var er + if (!(request_json.request && request_json.request.timestamp)) + return 'Timestamp field not present in request' - if(!cert_url) - return process.nextTick(callback, 'missing certificate url') + const d = new Date(request_json.request.timestamp) + const now = new Date() + const oldestTime = now.getTime() - (TIMESTAMP_TOLERANCE * 1000) - if (!signature) - return process.nextTick(callback, 'missing signature') + if (d.getTime() < oldestTime) + return 'Request is from more than ' + TIMESTAMP_TOLERANCE + ' seconds ago' +} - if (!requestBody) - return process.nextTick(callback, 'missing request (certificate) body') - if (!validator.isBase64(signature)) - return process.nextTick(callback, 'invalid signature (not base64 encoded)') +function verifier (cert_url, signature, requestBody, callback) { + if (!cert_url) + return process.nextTick(callback, 'missing certificate url') + + if (!signature) + return process.nextTick(callback, 'missing signature') + + if (!requestBody) + return process.nextTick(callback, 'missing request (certificate) body') - er = validateTimestamp(requestBody) + if (!validator.isBase64(signature)) + return process.nextTick(callback, 'invalid signature (not base64 encoded)') - if (er) - return process.nextTick(callback, er) + const er = validateTimestamp(requestBody) - getCert(cert_url, function (er, pem_cert) { if (er) - return callback(er) + return process.nextTick(callback, er) - if (!isValidSignature(pem_cert, signature, requestBody)) - return callback('invalid signature') + getCert(cert_url, function (er, pem_cert) { + if (er) + return callback(er) - callback() - }) + if (!isValidSignature(pem_cert, signature, requestBody)) + return callback('invalid signature') + + callback() + }) } // certificate validator for amazon echo -module.exports = function (cert_url, signature, requestBody, cb) { - if(cb) - return verifier(cert_url, signature, requestBody, cb) - - return new Promise(function( resolve, reject) { - verifier(cert_url, signature, requestBody, function(er) { - if(er) - return reject(er) - resolve() - }) +export default function alexaVerifier (cert_url, signature, requestBody, cb) { + if (cb) + return verifier(cert_url, signature, requestBody, cb) + + return new Promise(function (resolve, reject) { + verifier(cert_url, signature, requestBody, function (er) { + if (er) + return reject(er) + resolve() + }) }) } diff --git a/package.json b/package.json index f358687..3069591 100644 --- a/package.json +++ b/package.json @@ -3,6 +3,7 @@ "version": "2.0.2", "description": "Verify HTTP requests sent to an Alexa skill are sent from Amazon", "main": "index.js", + "type": "module", "repository": { "type": "git", "url": "https://github.com/mreinstein/alexa-verifier.git" @@ -26,10 +27,10 @@ "devDependencies": { "nock": "^9.0.2", "sinon": "^4.1.5", - "tap": "^11.0.0", + "tap": "^15.0.9", "unroll": "1.4.0" }, "engine": { - "node": ">=4" + "node": ">=12.17" } } diff --git a/test/echo-api-cert-8.pem b/test/echo-api-cert-9.pem similarity index 77% rename from test/echo-api-cert-8.pem rename to test/echo-api-cert-9.pem index fd290ad..f63b2b9 100644 --- a/test/echo-api-cert-8.pem +++ b/test/echo-api-cert-9.pem @@ -1,34 +1,34 @@ -----BEGIN CERTIFICATE----- -MIIFbjCCBFagAwIBAgIQBRsSsK9/f9dI7rlZGtPo3TANBgkqhkiG9w0BAQsFADBG +MIIFcDCCBFigAwIBAgIQCoVR+Rf6FN31ezpuGY39bTANBgkqhkiG9w0BAQsFADBG MQswCQYDVQQGEwJVUzEPMA0GA1UEChMGQW1hem9uMRUwEwYDVQQLEwxTZXJ2ZXIg -Q0EgMUIxDzANBgNVBAMTBkFtYXpvbjAeFw0yMDAzMTkwMDAwMDBaFw0yMTAzMTAx -MjAwMDBaMB4xHDAaBgNVBAMTE2VjaG8tYXBpLmFtYXpvbi5jb20wggEiMA0GCSqG -SIb3DQEBAQUAA4IBDwAwggEKAoIBAQCmd8SQClJ92SBjL2Q3G1MxQ4QEnTVE7qA3 -QhsAphIe1FHkSt4IVODhugprFNecdYppWSGy0Bzl7kn0KGer4NE6TJ3KV/Ux4jzw -M1aXhq3JmQcOA7DFO+EqdjhcwSoVmZJF32M7X0M2AdR3HZB9s/dk8MDpEPewtT4/ -2V0CsbEsA9sgKnzVZUdbL3SETLPJyh/+8N84fm32mjkrQdFPVqGWK9fp1tYwE+3M -5X6ZiiCXJsnyewnxOFm1upiNbX/6hJtMJch9kpIfQzz8eKHt9jtLpsM5m04pV/t8 -SmQlQ+rnnH/JrDN5HJ86D6nAW3o8UcILwGTDnsre18OJ3AbFxkBXAgMBAAGjggJ+ -MIICejAfBgNVHSMEGDAWgBRZpGYGUqB7lZI8o5QHJ5Z0W/k90DAdBgNVHQ4EFgQU -C+rUYvMdvL3ZR8OQM3yvONiDbZUwHgYDVR0RBBcwFYITZWNoby1hcGkuYW1hem9u +Q0EgMUIxDzANBgNVBAMTBkFtYXpvbjAeFw0yMDEyMTEwMDAwMDBaFw0yMTExMTYy +MzU5NTlaMB4xHDAaBgNVBAMTE2VjaG8tYXBpLmFtYXpvbi5jb20wggEiMA0GCSqG +SIb3DQEBAQUAA4IBDwAwggEKAoIBAQCSjIho0ATYb0MjLdh9Zm6H5+eiV+I2hydR +N2BM/H626EfAxe3Ngj9RDPJodjoayeXM4bYDAd4nSpATfn89KvMcmhwKy3Nln/Eh +9mwfJUq4PSitbHwnGUA2DYNPy1j0Lg9t8BxNqzL3oeMhccEGhsOuQBCBTPshrxPE +aLATgZphjFMh7ei5ARYa3DZcfXRpqQdO6yBT9b5QzIeJELVJslZYNfFan3P0Is7H +10pKIydgsH68bEJvl8h7w9DOPJGTuSLhr412SEP2+lD1w1zMWG9qXP4NvqWjNsqy +5VrbmtztNho5Z4h/VAA3H0ycxoiKDxsJUkLWdJThxPLtyJCeMGiTAgMBAAGjggKA +MIICfDAfBgNVHSMEGDAWgBRZpGYGUqB7lZI8o5QHJ5Z0W/k90DAdBgNVHQ4EFgQU +5lBFREdWwbMOpIWruZdyyizsE8UwHgYDVR0RBBcwFYITZWNoby1hcGkuYW1hem9u LmNvbTAOBgNVHQ8BAf8EBAMCBaAwHQYDVR0lBBYwFAYIKwYBBQUHAwEGCCsGAQUF BwMCMDsGA1UdHwQ0MDIwMKAuoCyGKmh0dHA6Ly9jcmwuc2NhMWIuYW1hem9udHJ1 c3QuY29tL3NjYTFiLmNybDAgBgNVHSAEGTAXMAsGCWCGSAGG/WwBAjAIBgZngQwB AgEwdQYIKwYBBQUHAQEEaTBnMC0GCCsGAQUFBzABhiFodHRwOi8vb2NzcC5zY2Ex Yi5hbWF6b250cnVzdC5jb20wNgYIKwYBBQUHMAKGKmh0dHA6Ly9jcnQuc2NhMWIu -YW1hem9udHJ1c3QuY29tL3NjYTFiLmNydDAMBgNVHRMBAf8EAjAAMIIBAwYKKwYB -BAHWeQIEAgSB9ASB8QDvAHUAu9nfvB+KcbWTlCOXqpJ7RzhXlQqrUugakJZkNo4e -0YUAAAFw8nrysgAABAMARjBEAiAVBY5M1ilaFGHsa5K84rYLBNQ6F8uk1k3JXVj4 -H0iIjgIgDFq0tTjV4Og5gs+NpUqZpLrr3glIFPLnqrx+/0S+vtYAdgBc3EOS/uar -RUSxXprUVuYQN/vV+kfcoXOUsl7m9scOygAAAXDyevLgAAAEAwBHMEUCIQDbA/GW -VyYTwK+8DuLEIu2hL2/ZJyrK4oxW+tLqhHlJdgIgLWsr6aTfpkV37kNwPGU/nTFC -XZJ4kTSx8LM+flSvivQwDQYJKoZIhvcNAQELBQADggEBAJRWyWcxgKYiy2Zej3Oa -2Xlju+I7YZAtFqD9at6SxUm/HNJ9h9Faclv927q0MDvHesYvIshJwMqTz2Vad9jC -burAboHup9HCP9vPP0sHB1CUveifu0FcxpmpEr6LWISf3Jl0bTTYGhfJFZ1H+L/R -N5XefXdqFJg5PY5A0Ex8ajBNW9/sSzAuiemm9Ijose6P8g4q9ydgSfOaiasyjdTh -ySVOhyKspsBm6FsYySlBukUHWdEUnbCuRBljy6ES5TmY0YGAXzV+sBiSC2iMgMWh -HeATCmBpbaKkFKrR+WHKcYGKHaVsiXA/acVlIhKTyA1d7HQP0y4cytcOQUvhr4Bw -x3o= +YW1hem9udHJ1c3QuY29tL3NjYTFiLmNydDAMBgNVHRMBAf8EAjAAMIIBBQYKKwYB +BAHWeQIEAgSB9gSB8wDxAHcA9lyUL9F3MCIUVBgIMJRWjuNNExkzv98MLyALzE7x +ZOMAAAF2U26sUAAABAMASDBGAiEA9qNCDx3nS1NBQ7Zyks0eIefuLEmcPILG28tB +yDUjpaICIQDjeJBpkm4sa3tv7O2fN1zWSNQLMhEet0arScAC66LxMQB2AFzcQ5L+ +5qtFRLFemtRW5hA3+9X6R9yhc5SyXub2xw7KAAABdlNurL0AAAQDAEcwRQIhANzn +6G/q7xfzZkM65PySGr1Or6pQEpSLQ/dZBEYJKhCcAiBGYYlomaCPEoOPK5I1fXeQ +AGFxBwFVUQ48LOmIjC9eyDANBgkqhkiG9w0BAQsFAAOCAQEACSwkS7NH5VyACsMO +EwlnzxnyI7OBSSl0mOH0Kg9heH7fmwlGjC4boyHFiTdSVebRXXJCgBzXZKcxw4Am +2g09qzDEtEgcomeqnJ5YnbZ1qKWaFBHcU1TeC9pJXp7FJ3ruxzBnv94P+qqAs99w +/8sNkJK9JzLzRvka29nN5bEHOQ/H5zRS9S8SvJ/P79/bXBVGoxLGB3IY7Ryxi5lg +PbMdtbsUuezPpO0XPEfDcllCCIMgtfOW4Ndeg3lG8UEYSQJcR8oGmvia5PuMtUK7 +1qJIDGYQwQlMgoZUXQznJImfE8ebFXEC5PVyHGcdTGXXKaiTdpdQy8hVE2evz+18 +av3OzQ== -----END CERTIFICATE----- -----BEGIN CERTIFICATE----- MIIESTCCAzGgAwIBAgITBn+UV4WH6Kx33rJTMlu8mYtWDTANBgkqhkiG9w0BAQsF @@ -107,4 +107,4 @@ l08YuW3e95ORCLp+QCztweq7dp4zBncdDQh/U90bZKuCJ/Fp1U1ervShw3WnWEQt 8jxwmKy6abaVd38PMV4s/KCHOkdp8Hlf9BRUpJVeEXgSYCfOn8J3/yNTd126/+pZ 59vPr5KW7ySaNRB6nJHGDn2Z9j8Z3/VyVOEVqQdZe4O/Ui5GjLIAZHYcSNPYeehu VsyuLAOQ1xk4meTKCRlb/weWsKh/NEnfVqn3sF/tM+2MR7cwA130A4w= ------END CERTIFICATE----- +-----END CERTIFICATE----- \ No newline at end of file diff --git a/test/fetch-cert.js b/test/fetch-cert.js index 87162fc..c00f1d9 100644 --- a/test/fetch-cert.js +++ b/test/fetch-cert.js @@ -1,54 +1,52 @@ -'use strict' +import fetchCert from '../fetch-cert.js' +import nock from 'nock' +import { test } from 'tap' +import url from 'url' -var fetchCert = require('../fetch-cert') -var nock = require('nock') -var test = require('tap').test -var url = require('url') - -var cert_url = url.parse('https://s3.amazonaws.com/echo.api/echo-api-cert.pem') +const cert_url = url.parse('https://s3.amazonaws.com/echo.api/echo-api-cert.pem') test('fetchCert should ignore response for error HTTP status', function (t) { - var options = { url: cert_url } + const options = { url: cert_url } - nock('https://s3.amazonaws.com').get(cert_url.path).reply(400, 'Bad Request') + nock('https://s3.amazonaws.com').get(cert_url.path).reply(400, 'Bad Request') - fetchCert(options, function (er, pem_cert) { - t.notEqual(er, undefined) - t.equal(pem_cert, undefined) - t.end() - }) + fetchCert(options, function (er, pem_cert) { + t.not(er, undefined) + t.equal(pem_cert, undefined) + t.end() + }) }) test('fetchCert should call back with response body for OK HTTP status', function (t) { - var options = { url: cert_url } + const options = { url: cert_url } - nock('https://s3.amazonaws.com').get(cert_url.path).reply(200, 'mock pem data') + nock('https://s3.amazonaws.com').get(cert_url.path).reply(200, 'mock pem data') - fetchCert(options, function (er, pem_cert) { - t.equal(er, undefined) - t.equal(pem_cert, 'mock pem data') - t.end() - }) + fetchCert(options, function (er, pem_cert) { + t.equal(er, undefined) + t.equal(pem_cert, 'mock pem data') + t.end() + }) }) test('fetchCert should hit cache for subsequent certificate reqs', function (t) { - var options = { url: cert_url, cache: { } } - var pem_data = 'mock pem data' - - nock('https://s3.amazonaws.com').get(cert_url.path).reply(200, 'mock pem data') + const options = { url: cert_url, cache: { } } + const pem_data = 'mock pem data' - fetchCert(options, function (er, pem_cert, servedFromCache) { - t.equal(er, undefined) - t.equal(pem_cert, 'mock pem data') - t.equal(servedFromCache, false) + nock('https://s3.amazonaws.com').get(cert_url.path).reply(200, 'mock pem data') fetchCert(options, function (er, pem_cert, servedFromCache) { - t.equal(pem_cert, 'mock pem data') - t.equal(servedFromCache, true) - t.end() + t.equal(er, undefined) + t.equal(pem_cert, 'mock pem data') + t.equal(servedFromCache, false) + + fetchCert(options, function (er, pem_cert, servedFromCache) { + t.equal(pem_cert, 'mock pem data') + t.equal(servedFromCache, true) + t.end() + }) }) - }) }) diff --git a/test/index.js b/test/index.js index 7d1821a..d95ebd6 100644 --- a/test/index.js +++ b/test/index.js @@ -1,242 +1,240 @@ -'use strict' +import { test } from 'tap' +import url from 'url' +import verifier from '../index.js' +import sinon from 'sinon' -var test = require('tap').test -var url = require('url') -var verifier = require('../') -var sinon = require('sinon') - -var cert_url = 'https://s3.amazonaws.com/echo.api/echo-api-cert-8.pem' // latest valid cert +const cert_url = 'https://s3.amazonaws.com/echo.api/echo-api-cert-9.pem' // latest valid cert 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() - body = { - request: { - timestamp: now.getTime() + const signature = 'JbWZ4iO5ogpq1NhsOqyqq/QRrvc1/XyDwjcBO9wWSk//c11+gImmtWzMG9tDEW40t0Xwt1cnGU93DwUZQzMyzJ5CMi+09qVQUSIHiSmPekKaQRxS0Ibu7l7cXXuCcOBupbkheD/Dsd897Bm5SQwd1cFKRv+PJlpmGKimgh2QmbivogsEkFl8b9SW48kjKWazwj/XP2SrHY0bTvwMTVu7zvTcp0ZenEGlY2DNr5zSd1n6lmS6rgAt1IPwhBzqI0PVMngaM0DQhB0wUPj3QoIUh0IyMVAQzRFbQpS4UGrA4M9a5a+AGy0jCQKiRCI+Yi9iZYEVYvfafF/lyOUHHYcpOg==' + const now = new Date() + const body = { + request: { + timestamp: now.getTime() + } } - } - verifier(undefined, signature, JSON.stringify(body), function (er) { - t.equal(er, 'missing certificate url') - t.end() - }) + verifier(undefined, signature, JSON.stringify(body), function (er) { + t.equal(er, 'missing certificate url') + t.end() + }) }) 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() - body = { - request: { - timestamp: now.getTime() + const signature = 'JbWZ4iO5ogpq1NhsOqyqq/QRrvc1/XyDwjcBO9wWSk//c11+gImmtWzMG9tDEW40t0Xwt1cnGU93DwUZQzMyzJ5CMi+09qVQUSIHiSmPekKaQRxS0Ibu7l7cXXuCcOBupbkheD/Dsd897Bm5SQwd1cFKRv+PJlpmGKimgh2QmbivogsEkFl8b9SW48kjKWazwj/XP2SrHY0bTvwMTVu7zvTcp0ZenEGlY2DNr5zSd1n6lmS6rgAt1IPwhBzqI0PVMngaM0DQhB0wUPj3QoIUh0IyMVAQzRFbQpS4UGrA4M9a5a+AGy0jCQKiRCI+Yi9iZYEVYvfafF/lyOUHHYcpOg==' + const now = new Date() + const body = { + request: { + timestamp: now.getTime() + } } - } - verifier('http://someinsecureurl', signature, JSON.stringify(body), function (er) { - t.equal(er.indexOf('Certificate URI MUST be https'), 0) - t.end() - }) + + 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) { - 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') - t.end() - }) + const 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') + t.end() + }) }) 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') - t.end() - }) + const 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') + t.end() + }) }) 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() - body = { - request: { - timestamp: now.getTime() - 200000 + const signature = 'JbWZ4iO5ogpq1NhsOqyqq/QRrvc1/XyDwjcBO9wWSk//c11+gImmtWzMG9tDEW40t0Xwt1cnGU93DwUZQzMyzJ5CMi+09qVQUSIHiSmPekKaQRxS0Ibu7l7cXXuCcOBupbkheD/Dsd897Bm5SQwd1cFKRv+PJlpmGKimgh2QmbivogsEkFl8b9SW48kjKWazwj/XP2SrHY0bTvwMTVu7zvTcp0ZenEGlY2DNr5zSd1n6lmS6rgAt1IPwhBzqI0PVMngaM0DQhB0wUPj3QoIUh0IyMVAQzRFbQpS4UGrA4M9a5a+AGy0jCQKiRCI+Yi9iZYEVYvfafF/lyOUHHYcpOg==' + const now = new Date() + const body = { + request: { + timestamp: now.getTime() - 200000 + } } - } - verifier(cert_url, signature, JSON.stringify(body), function(er) { - t.equal(er, 'Request is from more than 150 seconds ago') - t.end() - }) + verifier(cert_url, signature, JSON.stringify(body), function (er) { + t.equal(er, 'Request is from more than 150 seconds ago') + t.end() + }) }) test('handle missing signature parameter', function (t) { - var body, now - now = new Date() - body = { - request: { - timestamp: now.getTime() + const now = new Date() + const body = { + request: { + timestamp: now.getTime() + } } - } - verifier(cert_url, undefined, JSON.stringify(body), function (er) { - t.equal(er, 'missing signature') - t.end() - }) + verifier(cert_url, undefined, JSON.stringify(body), function (er) { + t.equal(er, 'missing signature') + t.end() + }) }) -test('handle invalid signature parameter', function(t) { - var body, now - now = new Date() - body = { - request: { - timestamp: now.getTime() +test('handle invalid signature parameter', function (t) { + const now = new Date() + const body = { + request: { + timestamp: now.getTime() + } } - } - verifier(cert_url, '....$#%@$se', JSON.stringify(body), function (er) { - t.equal(er, 'invalid signature (not base64 encoded)') - t.end() - }) + 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) { - var body, now - now = new Date() - body = { - request: { - timestamp: now.getTime() + const now = new Date() + const body = { + request: { + timestamp: now.getTime() + } } - } - verifier(cert_url, 'aGVsbG8NCg==', JSON.stringify(body), function (er) { - t.equal(er, 'invalid signature') - t.end() - }) + 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()) - 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 body = { - "version": "1.0", - "session": { - "new": true, - "sessionId": "SessionId.7745e45d-3042-45eb-8e86-cab2cf285daf", - "application": { - "applicationId": "amzn1.ask.skill.75c997b8-610f-4eb4-bf2e-95810e15fba2" - }, - "attributes": {}, - "user": { - "userId": "amzn1.ask.account.AF6Z7574YHBQCNNTJK45QROUSCUJEHIYAHZRP35FVU673VDGDKV4PH2M52PX4XWGCSYDM66B6SKEEFJN6RYWN7EME3FKASDIG7DPNGFFFNTN4ZT6B64IIZKSNTXQXEMVBXMA7J3FN3ERT2A4EDYFUYMGM4NSQU4RTAQOZWDD2J7JH6P2ROP2A6QEGLNLZDXNZU2DL7BKGCVLMNA" - } - }, - "request": { - "type": "IntentRequest", - "requestId": "EdwRequestId.fa7428b7-75d0-44c8-aebb-4c222ed48ebe", - "timestamp": ts, - "locale": "en-US", - "intent": { - "name": "HelloWorld" - }, - "inDialog": false + const ts = '2017-02-10T07:27:59Z' + const now = new Date(ts) + const clock = sinon.useFakeTimers(now.getTime()) + const cert_url = 'https://s3.amazonaws.com/echo.api/echo-api-cert-4.pem' + const signature = 'Qc8OuaGEHWeL/39XTEDYFbOCufYWpwi45rqmM2R4WaSEYcSXq+hUko/88wv48+6SPUiEddWSEEINJFAFV5auYZsnBzqCK+SO8mGNOGHmLYpcFuSEHI3eA3nDIEARrXTivqqbH/LCPJHc0tqNYr3yPZRIR2mYFndJOxgDNSOooZX+tp2GafHHsjjShCjmePaLxJiGG1DmrL6fyOJoLrzc0olUxLmnJviS6Q5wBir899TMEZ/zX+aiBTt/khVvwIh+hI/PZsRq/pQw4WAvQz1bcnGNamvMA/TKSJtR0elJP+TgCqbVoYisDgQXkhi8/wonkLhs68pN+TurbR7GyC1vxw==' + const body = { + "version": "1.0", + "session": { + "new": true, + "sessionId": "SessionId.7745e45d-3042-45eb-8e86-cab2cf285daf", + "application": { + "applicationId": "amzn1.ask.skill.75c997b8-610f-4eb4-bf2e-95810e15fba2" + }, + "attributes": {}, + "user": { + "userId": "amzn1.ask.account.AF6Z7574YHBQCNNTJK45QROUSCUJEHIYAHZRP35FVU673VDGDKV4PH2M52PX4XWGCSYDM66B6SKEEFJN6RYWN7EME3FKASDIG7DPNGFFFNTN4ZT6B64IIZKSNTXQXEMVBXMA7J3FN3ERT2A4EDYFUYMGM4NSQU4RTAQOZWDD2J7JH6P2ROP2A6QEGLNLZDXNZU2DL7BKGCVLMNA" + } + }, + "request": { + "type": "IntentRequest", + "requestId": "EdwRequestId.fa7428b7-75d0-44c8-aebb-4c222ed48ebe", + "timestamp": ts, + "locale": "en-US", + "intent": { + "name": "HelloWorld" + }, + "inDialog": false + } } - } - verifier(cert_url, signature, JSON.stringify(body), function (er) { - t.equal(er, undefined) - clock.restore() - t.end() - }) + 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()) - 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 body = { - "version":"1.0", - "session":{ - "new":true, - "sessionId":"SessionId.07e59233-1f59-43f9-bfc1-ac3ae3b843c6", - "application":{ - "applicationId":"amzn1.ask.skill.5535124f-0d41-472a-be31-589b1d3d04bf" - }, - "attributes":{ - - }, - "user":{ - "userId":"amzn1.ask.account.AGDZF2M6WHR5KHCXH5ODUYS6VUFUKNI2TABAZSUABKCMIEILVW5ZVME7OI2IOPPV4V7DAYVHMU2CMABL4HTCF7R33N2D6OH7QBEVTSGJUCYZPFX4EQO56TRHEHYUME3BSSDETEJUFFGB4JZBB6OCNQ2A7EKQHW6JQL5YK2HMIDH4ADCCQRJ24SFWBMENZUDPXWN2UNLP42EA4FQ" - } - }, - "request":{ - "type":"IntentRequest", - "requestId":"EdwRequestId.5581fcba-e41a-4059-a9d7-eb7b46f2a543", - "timestamp":"2017-04-05T12:02:36Z", - "locale":"en-US", - "intent":{ - "name":"Ask_term_info", - "slots":{ - "termslot":{ - "name":"termslot", - "value":"Pokémon" + const ts = '2017-04-05T12:02:36Z' + const now = new Date(ts) + const clock = sinon.useFakeTimers(now.getTime()) + const cert_url = 'https://s3.amazonaws.com/echo.api/echo-api-cert-4.pem' + const signature = 'WLShxe8KMwHUt8hVD5+iE4tDO+J8Li21yocDWnq8LVRpE2PMMWCxjQzOCzyoFm4i/yW07UKtKQxcnzB44ZEdP6e6HelwBwEdP4lb8jQcc5knk8SuUth4N7cu6Em8FPOdOJdd9idHbO/p8BTb14wgua5n+1SDKHm+wPikOVsfCMYsXcwRWx5FsgP1wVPrDsCHN/ISiCXz+UuMnd6H0uRNdLZ/x/ikPkknh+P1kuFa2a2LN4r57IwBDAxkdf9MzXEexSOO0nWLnyJY2VAFB+O7JKE39CwMJ1+YDOwTTTLjilkCnSlfnr6DP4HPGHnYhh2HQZle8UBrSDm4ntflErpISQ==' + const body = { + "version":"1.0", + "session": { + "new":true, + "sessionId":"SessionId.07e59233-1f59-43f9-bfc1-ac3ae3b843c6", + "application": { + "applicationId":"amzn1.ask.skill.5535124f-0d41-472a-be31-589b1d3d04bf" + }, + "attributes": { + + }, + "user": { + "userId":"amzn1.ask.account.AGDZF2M6WHR5KHCXH5ODUYS6VUFUKNI2TABAZSUABKCMIEILVW5ZVME7OI2IOPPV4V7DAYVHMU2CMABL4HTCF7R33N2D6OH7QBEVTSGJUCYZPFX4EQO56TRHEHYUME3BSSDETEJUFFGB4JZBB6OCNQ2A7EKQHW6JQL5YK2HMIDH4ADCCQRJ24SFWBMENZUDPXWN2UNLP42EA4FQ" } - } - } - } -} - verifier(cert_url, signature, JSON.stringify(body), function (er) { - t.equal(er, undefined) - clock.restore() - t.end() - }) + }, + "request": { + "type":"IntentRequest", + "requestId":"EdwRequestId.5581fcba-e41a-4059-a9d7-eb7b46f2a543", + "timestamp":"2017-04-05T12:02:36Z", + "locale":"en-US", + "intent":{ + "name":"Ask_term_info", + "slots":{ + "termslot":{ + "name":"termslot", + "value":"Pokémon" + } + } + } + } + } + + verifier(cert_url, signature, JSON.stringify(body), function (er) { + t.equal(er, undefined) + clock.restore() + t.end() + }) }) test('invocation', 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 = 'Qc8OuaGEHWeL/39XTEDYFbOCufYWpwi45rqmM2R4WaSEYcSXq+hUko/88wv48+6SPUiEddWSEEINJFAFV5auYZsnBzqCK+SO8mGNOGHmLYpcFuSEHI3eA3nDIEARrXTivqqbH/LCPJHc0tqNYr3yPZRIR2mYFndJOxgDNSOooZX+tp2GafHHsjjShCjmePaLxJiGG1DmrL6fyOJoLrzc0olUxLmnJviS6Q5wBir899TMEZ/zX+aiBTt/khVvwIh+hI/PZsRq/pQw4WAvQz1bcnGNamvMA/TKSJtR0elJP+TgCqbVoYisDgQXkhi8/wonkLhs68pN+TurbR7GyC1vxw==' - var body = { - "version": "1.0", - "session": { - "new": true, - "sessionId": "SessionId.7745e45d-3042-45eb-8e86-cab2cf285daf", - "application": { - "applicationId": "amzn1.ask.skill.75c997b8-610f-4eb4-bf2e-95810e15fba2" - }, - "attributes": {}, - "user": { - "userId": "amzn1.ask.account.AF6Z7574YHBQCNNTJK45QROUSCUJEHIYAHZRP35FVU673VDGDKV4PH2M52PX4XWGCSYDM66B6SKEEFJN6RYWN7EME3FKASDIG7DPNGFFFNTN4ZT6B64IIZKSNTXQXEMVBXMA7J3FN3ERT2A4EDYFUYMGM4NSQU4RTAQOZWDD2J7JH6P2ROP2A6QEGLNLZDXNZU2DL7BKGCVLMNA" - } - }, - "request": { - "type": "IntentRequest", - "requestId": "EdwRequestId.fa7428b7-75d0-44c8-aebb-4c222ed48ebe", - "timestamp": ts, - "locale": "en-US", - "intent": { - "name": "HelloWorld" - }, - "inDialog": false + const ts = '2017-04-05T12:02:36Z' + const now = new Date(ts) + const clock = sinon.useFakeTimers(now.getTime()) + const cert_url = 'https://s3.amazonaws.com/echo.api/echo-api-cert-4.pem' + const signature = 'Qc8OuaGEHWeL/39XTEDYFbOCufYWpwi45rqmM2R4WaSEYcSXq+hUko/88wv48+6SPUiEddWSEEINJFAFV5auYZsnBzqCK+SO8mGNOGHmLYpcFuSEHI3eA3nDIEARrXTivqqbH/LCPJHc0tqNYr3yPZRIR2mYFndJOxgDNSOooZX+tp2GafHHsjjShCjmePaLxJiGG1DmrL6fyOJoLrzc0olUxLmnJviS6Q5wBir899TMEZ/zX+aiBTt/khVvwIh+hI/PZsRq/pQw4WAvQz1bcnGNamvMA/TKSJtR0elJP+TgCqbVoYisDgQXkhi8/wonkLhs68pN+TurbR7GyC1vxw==' + const body = { + "version": "1.0", + "session": { + "new": true, + "sessionId": "SessionId.7745e45d-3042-45eb-8e86-cab2cf285daf", + "application": { + "applicationId": "amzn1.ask.skill.75c997b8-610f-4eb4-bf2e-95810e15fba2" + }, + "attributes": {}, + "user": { + "userId": "amzn1.ask.account.AF6Z7574YHBQCNNTJK45QROUSCUJEHIYAHZRP35FVU673VDGDKV4PH2M52PX4XWGCSYDM66B6SKEEFJN6RYWN7EME3FKASDIG7DPNGFFFNTN4ZT6B64IIZKSNTXQXEMVBXMA7J3FN3ERT2A4EDYFUYMGM4NSQU4RTAQOZWDD2J7JH6P2ROP2A6QEGLNLZDXNZU2DL7BKGCVLMNA" + } + }, + "request": { + "type": "IntentRequest", + "requestId": "EdwRequestId.fa7428b7-75d0-44c8-aebb-4c222ed48ebe", + "timestamp": ts, + "locale": "en-US", + "intent": { + "name": "HelloWorld" + }, + "inDialog": false + } } - } - var result = verifier(cert_url, signature, JSON.stringify(body)) - result.catch(function(er) { }) - t.assert(result instanceof Promise, 'omitting callback returns a promise') + const result = verifier(cert_url, signature, JSON.stringify(body)) + result.catch(function (er) { }) + t.ok(result instanceof Promise, 'omitting callback returns a promise') - var callbackResult = verifier(cert_url, signature, JSON.stringify(body), function(er) { }) + const callbackResult = verifier(cert_url, signature, JSON.stringify(body), function (er) { }) - t.equal(callbackResult, undefined, 'including callback does not return a promise') + t.equal(callbackResult, undefined, 'including callback does not return a promise') - t.end() + t.end() }) diff --git a/test/validate-cert-uri.js b/test/validate-cert-uri.js index 7142b31..b6439be 100644 --- a/test/validate-cert-uri.js +++ b/test/validate-cert-uri.js @@ -1,38 +1,36 @@ -'use strict' - -var test = require('tap').test -var unroll = require('unroll') -var url = require('url') -var validateCertUri = require('../validate-cert-uri') +import { test } from 'tap' +import unroll from 'unroll' +import url from 'url' +import validateCertUri from '../validate-cert-uri.js' unroll.use(test) unroll('verifier.validateCertUri should be #valid for #url', - function (t, testArgs) { - var cert_uri = url.parse(testArgs['url']) - var result = validateCertUri(cert_uri) - var valid = testArgs['valid'] - t.notEqual(valid, undefined) - if (valid === true) { - t.equal(result, true) - } else { - // I don't need the error message, do negated comparison with 'true' - t.notEqual(result, true) - } - t.end() - }, - [ - ['valid', 'url'], - [true, 'https://s3.amazonaws.com/echo.api/echo-api-cert.pem'], - [true, 'HTTPS://s3.amazonaws.com/echo.api/echo-api-cert.pem'], - [true, 'https://S3.AMAZONAWS.COM/echo.api/echo-api-cert.pem'], - [true, 'https://s3.amazonaws.com:443/echo.api/echo-api-cert.pem'], - [true, 'https://s3.amazonaws.com/echo.api/../echo.api/echo-api-cert.pem'], - [false, 'http://s3.amazonaws.com/echo.api/echo-api-cert.pem'], // (invalid protocol) - [false, 'https://notamazon.com/echo.api/echo-api-cert.pem'], // (invalid hostname) - [false, 'https://s3.amazonaws.com/EcHo.aPi/echo-api-cert.pem'], // (invalid path) - [false, 'https://s3.amazonaws.com/invalid.path/echo-api-cert.pem'], // (invalid path) - [false, 'https://s3.amazonaws.com:563/echo.api/echo-api-cert.pem'] // (invalid port) - ] + function (t, testArgs) { + const cert_uri = url.parse(testArgs['url']) + const result = validateCertUri(cert_uri) + const valid = testArgs['valid'] + t.not(valid, undefined) + if (valid === true) { + t.equal(result, true) + } else { + // I don't need the error message, do negated comparison with 'true' + t.not(result, true) + } + t.end() + }, + [ + [ 'valid', 'url' ], + [ true, 'https://s3.amazonaws.com/echo.api/echo-api-cert.pem' ], + [ true, 'HTTPS://s3.amazonaws.com/echo.api/echo-api-cert.pem' ], + [ true, 'https://S3.AMAZONAWS.COM/echo.api/echo-api-cert.pem' ], + [ true, 'https://s3.amazonaws.com:443/echo.api/echo-api-cert.pem' ], + [ true, 'https://s3.amazonaws.com/echo.api/../echo.api/echo-api-cert.pem' ], + [ false, 'http://s3.amazonaws.com/echo.api/echo-api-cert.pem' ], // (invalid protocol) + [ false, 'https://notamazon.com/echo.api/echo-api-cert.pem' ], // (invalid hostname) + [ false, 'https://s3.amazonaws.com/EcHo.aPi/echo-api-cert.pem' ], // (invalid path) + [ false, 'https://s3.amazonaws.com/invalid.path/echo-api-cert.pem' ], // (invalid path) + [ false, 'https://s3.amazonaws.com:563/echo.api/echo-api-cert.pem' ] // (invalid port) + ] ) diff --git a/test/validate-cert.js b/test/validate-cert.js index c9b98e2..b29dd23 100644 --- a/test/validate-cert.js +++ b/test/validate-cert.js @@ -1,109 +1,130 @@ -'use strict' +import fs from 'fs' +import pkg from 'node-forge' +import { test } from 'tap' +import url from 'url' +import validate from '../validate-cert.js' +import { dirname } from 'path' +import { fileURLToPath } from 'url' -var fs = require('fs') -var pki = require('node-forge').pki -var test = require('tap').test -var url = require('url') -var validate = require('../validate-cert') + +const __dirname = dirname(fileURLToPath(import.meta.url)) +const { pki } = pkg function createInvalidCert () { - var keys = pki.rsa.generateKeyPair(512) - var cert = pki.createCertificate() - cert.publicKey = keys.publicKey - // alternatively set public key from a csr - //cert.publicKey = csr.publicKey - cert.serialNumber = '01' - cert.validity.notBefore = new Date() - cert.validity.notAfter = new Date() - cert.validity.notAfter.setFullYear(cert.validity.notBefore.getFullYear() + 1) - var attrs = [{ - name: 'commonName', - value: 'example.org' - }, { - name: 'countryName', - value: 'US' - }, { - shortName: 'ST', - value: 'Virginia' - }, { - name: 'localityName', - value: 'Blacksburg' - }, { - name: 'organizationName', - value: 'Test' - }, { - shortName: 'OU', - value: 'Test' - }] - cert.setSubject(attrs) - // alternatively set subject from a csr - //cert.setSubject(csr.subject.attributes) - cert.setIssuer(attrs) - cert.setExtensions([{ - name: 'basicConstraints', - cA: true - }, { - name: 'keyUsage', - keyCertSign: true, - digitalSignature: true, - nonRepudiation: true, - keyEncipherment: true, - dataEncipherment: true - }, { - name: 'extKeyUsage', - serverAuth: true, - clientAuth: true, - codeSigning: true, - emailProtection: true, - timeStamping: true - }, { - name: 'nsCertType', - client: true, - server: true, - email: true, - objsign: true, - sslCA: true, - emailCA: true, - objCA: true - }, { - name: 'subjectAltName', - altNames: [{ - type: 6, // URI - value: 'http://example.org/webid#me' - }, { - type: 7, // IP - ip: '127.0.0.1' - }] - }, { - name: 'subjectKeyIdentifier' - }]) + const keys = pki.rsa.generateKeyPair(512) + const cert = pki.createCertificate() + cert.publicKey = keys.publicKey + // alternatively set public key from a csr + //cert.publicKey = csr.publicKey + cert.serialNumber = '01' + cert.validity.notBefore = new Date() + cert.validity.notAfter = new Date() + cert.validity.notAfter.setFullYear(cert.validity.notBefore.getFullYear() + 1) + const attrs = [ + { + name: 'commonName', + value: 'example.org' + }, + { + name: 'countryName', + value: 'US' + }, + { + shortName: 'ST', + value: 'Virginia' + }, + { + name: 'localityName', + value: 'Blacksburg' + }, + { + name: 'organizationName', + value: 'Test' + }, + { + shortName: 'OU', + value: 'Test' + } + ] + + cert.setSubject(attrs) + // alternatively set subject from a csr + //cert.setSubject(csr.subject.attributes) + cert.setIssuer(attrs) + cert.setExtensions([ + { + name: 'basicConstraints', + cA: true + }, + { + name: 'keyUsage', + keyCertSign: true, + digitalSignature: true, + nonRepudiation: true, + keyEncipherment: true, + dataEncipherment: true + }, + { + name: 'extKeyUsage', + serverAuth: true, + clientAuth: true, + codeSigning: true, + emailProtection: true, + timeStamping: true + }, + { + name: 'nsCertType', + client: true, + server: true, + email: true, + objsign: true, + sslCA: true, + emailCA: true, + objCA: true + }, + { + name: 'subjectAltName', + altNames: [{ + type: 6, // URI + value: 'http://example.org/webid#me' + }, + { + type: 7, // IP + ip: '127.0.0.1' + }] + }, + { + name: 'subjectKeyIdentifier' + } + ]) - // self-sign certificate - cert.sign(keys.privateKey) + // self-sign certificate + cert.sign(keys.privateKey) - return pki.certificateToPem(cert) + return pki.certificateToPem(cert) } + test('fails on invalid pem cert parameter', function (t) { - t.assert(validate(undefined) !== undefined, 'Error should have been thrown') - t.end() + t.ok(validate(undefined) !== undefined, 'Error should have been thrown') + t.end() }) 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() + const pem = createInvalidCert() + t.ok(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) { - var pem = fs.readFileSync(__dirname + '/cert-expired.pem') - t.assert(validate(pem) === 'invalid certificate validity (past expired date)') - t.end() + const pem = fs.readFileSync(__dirname + '/cert-expired.pem') + t.ok(validate(pem) === 'invalid certificate validity (past expired date)') + t.end() }) test('approves valid certifcate', function (t) { - var pem = fs.readFileSync(__dirname + '/echo-api-cert-8.pem') - t.assert(validate(pem) === undefined, 'Certificate should be valid') - t.end() + const pem = fs.readFileSync(__dirname + '/echo-api-cert-9.pem') + t.ok(validate(pem) === undefined, 'Certificate should be valid') + t.end() }) diff --git a/validate-cert-uri.js b/validate-cert-uri.js index 2442bad..5072b44 100644 --- a/validate-cert-uri.js +++ b/validate-cert-uri.js @@ -1,23 +1,21 @@ -'use strict' +const VALID_CERT_HOSTNAME = 's3.amazonaws.com' +const VALID_CERT_PATH_START = '/echo.api/' +const VALID_CERT_PORT = '443' -// constants -var VALID_CERT_HOSTNAME = 's3.amazonaws.com' -var VALID_CERT_PATH_START = '/echo.api/' -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:') - return 'Certificate URI MUST be https: ' + cert_uri +export default function validateCertUri (cert_uri) { + if (cert_uri.protocol !== 'https:') + return 'Certificate URI MUST be https: ' + cert_uri - 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.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) - return 'Certificate URI hostname must be ' + VALID_CERT_HOSTNAME + ': ' + cert_uri.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) - return 'Certificate URI path must start with ' + VALID_CERT_PATH_START + ': ' + cert_uri + 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 + return true } diff --git a/validate-cert.js b/validate-cert.js index 57e793c..9e10934 100644 --- a/validate-cert.js +++ b/validate-cert.js @@ -1,38 +1,38 @@ -'use strict' +import pkg from 'node-forge' -var pki = require('node-forge').pki +const { pki } = pkg +const VALID_CERT_SAN = 'echo-api.amazon.com' -// 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) { - try { - var cert = pki.certificateFromPem(pem_cert) - - // check that cert has a Subject Alternative Names (SANs) section - var altNameExt = cert.getExtension("subjectAltName") - 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) - 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) - return 'invalid certificate validity (past expired date)' - - var notBeforeTime = new Date(cert.validity.notBefore).getTime() - if (currTime <= notBeforeTime) - return 'invalid certificate validity (start date is in the future)' - - } catch (e) { - return e - } +export default function validate (pem_cert) { + try { + const cert = pki.certificateFromPem(pem_cert) + + // check that cert has a Subject Alternative Names (SANs) section + const altNameExt = cert.getExtension('subjectAltName') + if (!altNameExt) + return 'invalid certificate validity (subjectAltName extension not present)' + + // check that the domain echo-api.amazon.com is present in SANs section + const domainExists = altNameExt.altNames.some(function (name) { + return name.value === VALID_CERT_SAN + }) + + if (!domainExists) + return 'invalid certificate validity (correct domain not found in subject alternative names)' + + const currTime = new Date().getTime() + const notAfterTime = new Date(cert.validity.notAfter).getTime() + if (notAfterTime <= currTime) + return 'invalid certificate validity (past expired date)' + + const notBeforeTime = new Date(cert.validity.notBefore).getTime() + if (currTime <= notBeforeTime) + return 'invalid certificate validity (start date is in the future)' + + } catch (e) { + return e + } }