diff --git a/index.js b/index.js index 99d894f..0e57b0d 100644 --- a/index.js +++ b/index.js @@ -8,6 +8,7 @@ import validator from 'validator' const TIMESTAMP_TOLERANCE = 150 const SIGNATURE_FORMAT = 'base64' +const CHARACTER_ENCODING = 'utf8' function getCert (cert_url, callback) { @@ -17,6 +18,7 @@ function getCert (cert_url, callback) { return process.nextTick(callback, result) fetchCert(options, function (er, pem_cert) { + if (er) return callback(er) @@ -31,8 +33,8 @@ function getCert (cert_url, callback) { // returns true if the signature for the request body is valid, false otherwise function isValidSignature (pem_cert, signature, requestBody) { - const verifier = crypto.createVerify('RSA-SHA1') - verifier.update(requestBody, 'utf8') + const verifier = crypto.createVerify('RSA-SHA256') + verifier.update(requestBody, CHARACTER_ENCODING) return verifier.verify(pem_cert, signature, SIGNATURE_FORMAT) } diff --git a/package.json b/package.json index 0f77d61..ad40dc5 100644 --- a/package.json +++ b/package.json @@ -25,6 +25,7 @@ "validator": "^13.7.0" }, "devDependencies": { + "esmock": "^2.6.0", "nock": "^13.0.0", "sinon": "^14.0.0", "tap": "^16.0.0", diff --git a/test/index.js b/test/index.js index a97c079..338338e 100644 --- a/test/index.js +++ b/test/index.js @@ -1,11 +1,22 @@ import { test } from 'tap' +import crypto from 'crypto' +import esmock from 'esmock' +import fs from 'fs' import url from 'url' import verifier from '../index.js' import sinon from 'sinon' +import { dirname } from 'path' +import { fileURLToPath } from 'url' +const __dirname = dirname(fileURLToPath(import.meta.url)) + const cert_url = 'https://s3.amazonaws.com/echo.api/echo-api-cert-12.pem' // latest valid cert +const rsaSha256Key = fs.readFileSync(`${__dirname}/mocks/rsa_sha256`).toString() +const validPem = fs.readFileSync(`${__dirname}/mocks/rsa_sha256_pub`).toString() + + test('handle missing cert_url parameter', function (t) { const signature = 'JbWZ4iO5ogpq1NhsOqyqq/QRrvc1/XyDwjcBO9wWSk//c11+gImmtWzMG9tDEW40t0Xwt1cnGU93DwUZQzMyzJ5CMi+09qVQUSIHiSmPekKaQRxS0Ibu7l7cXXuCcOBupbkheD/Dsd897Bm5SQwd1cFKRv+PJlpmGKimgh2QmbivogsEkFl8b9SW48kjKWazwj/XP2SrHY0bTvwMTVu7zvTcp0ZenEGlY2DNr5zSd1n6lmS6rgAt1IPwhBzqI0PVMngaM0DQhB0wUPj3QoIUh0IyMVAQzRFbQpS4UGrA4M9a5a+AGy0jCQKiRCI+Yi9iZYEVYvfafF/lyOUHHYcpOg==' const now = new Date() @@ -112,12 +123,27 @@ test('handle invalid base64-encoded signature parameter', function (t) { }) -test('handle valid signature', function (t) { - const ts = '2017-02-10T07:27:59Z' +test('handle valid signature', async function (t) { + + const verifier = await esmock('../index.js', { + '../fetch-cert.js': { + default: function fetchCert (options, callback) { + callback(undefined, validPem) + } + }, + '../validate-cert.js': { + default: function validateCert (pem_cert) { + // we're using our mocked sha256 pub/private keypair, so skip all the validation unrelated to + // signature checking. + } + }, + }) + + + const ts = '2019-09-01T07: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": { @@ -143,20 +169,37 @@ test('handle valid signature', function (t) { } } - verifier(cert_url, signature, JSON.stringify(body), function (er) { + const requestEnvelope = JSON.stringify(body) + const signer = crypto.createSign('RSA-SHA256') + signer.update(requestEnvelope) + const signature = signer.sign(rsaSha256Key, 'base64'); + + verifier(cert_url, signature, requestEnvelope, function (er) { t.equal(er, undefined) clock.restore() - t.end() }) }) -test('handle valid signature with double byte utf8 encodings', function (t) { +test('handle valid signature with double byte utf8 encodings', async function (t) { + const verifier = await esmock('../index.js', { + '../fetch-cert.js': { + default: function fetchCert (options, callback) { + callback(undefined, validPem) + } + }, + '../validate-cert.js': { + default: function validateCert (pem_cert) { + // we're using our mocked sha256 pub/private keypair, so skip all the validation unrelated to + // signature checking. + } + }, + }) + 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": { @@ -189,6 +232,11 @@ test('handle valid signature with double byte utf8 encodings', function (t) { } } + const requestEnvelope = JSON.stringify(body) + const signer = crypto.createSign('RSA-SHA256') + signer.update(requestEnvelope) + const signature = signer.sign(rsaSha256Key, 'base64'); + verifier(cert_url, signature, JSON.stringify(body), function (er) { t.equal(er, undefined) clock.restore() @@ -199,10 +247,7 @@ test('handle valid signature with double byte utf8 encodings', function (t) { test('invocation', function (t) { 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 signature = '' const body = { "version": "1.0", "session": { diff --git a/test/cert-expired.pem b/test/mocks/cert-expired.pem similarity index 100% rename from test/cert-expired.pem rename to test/mocks/cert-expired.pem diff --git a/test/echo-api-cert-12.cer b/test/mocks/echo-api-cert-12.cer similarity index 100% rename from test/echo-api-cert-12.cer rename to test/mocks/echo-api-cert-12.cer diff --git a/test/mocks/rsa_sha256 b/test/mocks/rsa_sha256 new file mode 100644 index 0000000..d054677 --- /dev/null +++ b/test/mocks/rsa_sha256 @@ -0,0 +1,51 @@ +-----BEGIN RSA PRIVATE KEY----- +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 +-----END RSA PRIVATE KEY----- diff --git a/test/mocks/rsa_sha256_pub b/test/mocks/rsa_sha256_pub new file mode 100644 index 0000000..2f84598 --- /dev/null +++ b/test/mocks/rsa_sha256_pub @@ -0,0 +1,14 @@ +-----BEGIN PUBLIC KEY----- +MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEA5z4oJtvKEKb/JrbKYfQw +kRsa/T46XGCxJEbUDQ0OyS++KXOTBn/wILCKgShRE5jxvWaG4xOPEjQjuVWuuUee +tuG5zsmybqyMaQKiRbp9WNwL3dSheQWee4Kn3F86AeM8HBu1XQIR9YAqY4utLVG4 +J8cUUaP2ScgtJ7rSHXvak61ZTUThWi4uDhINK96Tz2UvN7qB0j5gqLE828Ii+Qdk +F1TmF3XwVRwIZ6o0xhAiLmoCqta9dQO7m6etPNbQrj5Kbb4zrgaVXBABenwHh3Ix +VyIILs3V9opVeuInDAmq0mMBVPEK5LARrbC3/eZKJOIuqrwMmv8tTL+vcaWgrQ4b +jA0YL5gYx3w3uDVwHm0kj8IwRPYWRxxSjXvOqVPtIYN0zetjjAfJkzaRQlZO93k1 +rxS39PkwmeNpfk97PgM8ca9+ZHyoHQujcUTd2pCa0jAC/AJSzlr6zF/nBfTU3dIu +tbxc0PhUCegh3KVjA38lRXMhSkAFaN5TAXZM1NClAUFaYaoBZdFu/MwRefNqpg+N +zy47jJmD/4S56vdp/mZxWxb5rkUG1RtbHhTI9AEZsF9W3LfeO1D+hR134uvnapwp +fGpTFl6lqQDcvj9nqNCpuLfM77roBNigwLnhlC30U4bHtmPlZrp4Ys5NojSkJUO1 +jhdLMKdwJZVUKYl7rhmhCcMCAwEAAQ== +-----END PUBLIC KEY----- diff --git a/test/validate-cert.js b/test/validate-cert.js index 1f7e9dd..ff6445d 100644 --- a/test/validate-cert.js +++ b/test/validate-cert.js @@ -118,13 +118,13 @@ test('fails on non amazon subject alt name', function (t) { }) test('fails on expired certificate (Not After)', function (t) { - const pem = fs.readFileSync(__dirname + '/cert-expired.pem') + const pem = fs.readFileSync(__dirname + '/mocks/cert-expired.pem') t.ok(validate(pem) === 'invalid certificate validity (past expired date)') t.end() }) test('approves valid certifcate', function (t) { - const pem = fs.readFileSync(__dirname + '/echo-api-cert-12.cer') + const pem = fs.readFileSync(__dirname + '/mocks/echo-api-cert-12.cer') t.ok(validate(pem) === undefined, 'Certificate should be valid') t.end() })