From b94e7141532117d0f71df480f4dd9319d4c1ce0f Mon Sep 17 00:00:00 2001 From: Elise Shanholtz Date: Wed, 3 Jun 2020 11:24:47 -0700 Subject: [PATCH 1/3] feat: verify signature from event webhook When enabling the "Signed Event Webhook Requests" feature in Mail Settings, Twilio SendGrid will generate a private and public key pair using the Elliptic Curve Digital Signature Algorithm (ECDSA). Once that is successfully enabled, all new event posts will have two new headers: X-Twilio-Email-Event-Webhook-Signature and X-Twilio-Email-Event-Webhook-Timestamp, which can be used to validate your events. This SDK update will make it easier to verify signatures from signed event webhook requests by using the verifySignature method. Pass in the public key, event payload, signature, and timestamp to validate. Note: You will need to convert your public key string to an elliptic public key object in order to use the verifySignature method. --- package.json | 1 + packages/eventwebhook/index.d.ts | 3 + packages/eventwebhook/index.js | 5 ++ packages/eventwebhook/package.json | 28 ++++++++ packages/eventwebhook/src/eventwebhook.d.ts | 22 +++++++ packages/eventwebhook/src/eventwebhook.js | 35 ++++++++++ .../eventwebhook/src/eventwebhook.spec.js | 65 +++++++++++++++++++ test/typescript/eventwebhook.ts | 13 ++++ 8 files changed, 172 insertions(+) create mode 100644 packages/eventwebhook/index.d.ts create mode 100644 packages/eventwebhook/index.js create mode 100644 packages/eventwebhook/package.json create mode 100644 packages/eventwebhook/src/eventwebhook.d.ts create mode 100644 packages/eventwebhook/src/eventwebhook.js create mode 100644 packages/eventwebhook/src/eventwebhook.spec.js create mode 100644 test/typescript/eventwebhook.ts diff --git a/package.json b/package.json index 8619f964e..2701f7f9c 100644 --- a/package.json +++ b/package.json @@ -31,6 +31,7 @@ "test:helpers": "babel-node ./node_modules/istanbul/lib/cli cover ./node_modules/mocha/bin/_mocha \"packages/helpers/**/*.spec.js\"", "test:client": "babel-node ./node_modules/istanbul/lib/cli cover ./node_modules/mocha/bin/_mocha \"packages/client/**/*.spec.js\"", "test:mail": "babel-node ./node_modules/istanbul/lib/cli cover ./node_modules/mocha/bin/_mocha \"packages/mail/**/*.spec.js\"", + "test:eventwebhook": "babel-node ./node_modules/istanbul/lib/cli cover ./node_modules/mocha/bin/_mocha \"packages/eventwebhook/**/*.spec.js\"", "test:inbound": "babel-node ./node_modules/istanbul/lib/cli cover ./node_modules/mocha/bin/_mocha \"packages/inbound-mail-parser/**/*.spec.js\"", "test:contact": "babel-node ./node_modules/istanbul/lib/cli cover ./node_modules/mocha/bin/_mocha \"packages/contact-importer/**/*.spec.js\"", "test:files": "babel-node ./node_modules/istanbul/lib/cli cover ./node_modules/mocha/bin/_mocha \"test/files.spec.js\"", diff --git a/packages/eventwebhook/index.d.ts b/packages/eventwebhook/index.d.ts new file mode 100644 index 000000000..d2f6d68bf --- /dev/null +++ b/packages/eventwebhook/index.d.ts @@ -0,0 +1,3 @@ +import EventWebhook = require('./src/eventwebhook'); + +export = EventWebhook; diff --git a/packages/eventwebhook/index.js b/packages/eventwebhook/index.js new file mode 100644 index 000000000..44a6aaeaf --- /dev/null +++ b/packages/eventwebhook/index.js @@ -0,0 +1,5 @@ +'use strict'; + +const EventWebhook = require('./src/eventwebhook'); + +module.exports = EventWebhook; diff --git a/packages/eventwebhook/package.json b/packages/eventwebhook/package.json new file mode 100644 index 000000000..80f92e01b --- /dev/null +++ b/packages/eventwebhook/package.json @@ -0,0 +1,28 @@ +{ + "name": "@sendgrid/eventwebhook", + "description": "Twilio SendGrid NodeJS Event Webhook", + "version": "1.0.0", + "author": "Twilio SendGrid (sendgrid.com)", + "contributors": [ + "Elise Shanholtz " + ], + "license": "MIT", + "homepage": "https://sendgrid.com", + "repository": { + "type": "git", + "url": "git://github.com/sendgrid/sendgrid-nodejs.git" + }, + "publishConfig": { + "access": "public" + }, + "main": "src/eventwebhook.js", + "engines": { + "node": ">=6.0.0" + }, + "dependencies": { + "@starkbank/ecdsa": "^0.0.3" + }, + "tags": [ + "sendgrid" + ] +} diff --git a/packages/eventwebhook/src/eventwebhook.d.ts b/packages/eventwebhook/src/eventwebhook.d.ts new file mode 100644 index 000000000..0e5aeb814 --- /dev/null +++ b/packages/eventwebhook/src/eventwebhook.d.ts @@ -0,0 +1,22 @@ +import {PublicKey} from "@starkbank/ecdsa"; + +declare class EventWebhook { + /** + * + * @param {string} publicKey verification key under Mail Settings + * @return {PublicKey} A public key using the ECDSA algorithm + */ + convertPublicKeyToECDSA(publicKey: string): PublicKey; + + /** + * + * @param {PublicKey} publicKey elliptic curve public key + * @param {object|string} payload event payload in the request body + * @param {string} signature value obtained from the 'X-Twilio-Email-Event-Webhook-Signature' header + * @param {string} timestamp value obtained from the 'X-Twilio-Email-Event-Webhook-Timestamp' header + * @return {Boolean} true or false if signature is valid + */ + verifySignature(publicKey: PublicKey, payload: object|string, signature: string, timestamp: string): boolean; +} + +export = EventWebhook; diff --git a/packages/eventwebhook/src/eventwebhook.js b/packages/eventwebhook/src/eventwebhook.js new file mode 100644 index 000000000..de2ddbaf3 --- /dev/null +++ b/packages/eventwebhook/src/eventwebhook.js @@ -0,0 +1,35 @@ +'use strict'; + +const ecdsa = require('@starkbank/ecdsa'); +const Ecdsa = ecdsa.Ecdsa; +const Signature = ecdsa.Signature; +const PublicKey = ecdsa.PublicKey; + +class EventWebhook { + /** + * + * @param {string} publicKey verification key under Mail Settings + * @return {PublicKey} A public key using the ECDSA algorithm + */ + convertPublicKeyToECDSA(publicKey) { + return PublicKey.fromPem(publicKey); + } + + /** + * + * @param {PublicKey} publicKey elliptic curve public key + * @param {Object|string} payload event payload in the request body + * @param {string} signature value obtained from the 'X-Twilio-Email-Event-Webhook-Signature' header + * @param {string} timestamp value obtained from the 'X-Twilio-Email-Event-Webhook-Timestamp' header + * @return {Boolean} true or false if signature is valid + */ + verifySignature(publicKey, payload, signature, timestamp) { + let timestampPayload = typeof payload === 'object' ? JSON.stringify(payload) : payload; + timestampPayload = timestamp + timestampPayload; + const decodedSignature = Signature.fromBase64(signature); + + return Ecdsa.verify(timestampPayload, decodedSignature, publicKey); + } +} + +module.exports = EventWebhook; diff --git a/packages/eventwebhook/src/eventwebhook.spec.js b/packages/eventwebhook/src/eventwebhook.spec.js new file mode 100644 index 000000000..8588b6a13 --- /dev/null +++ b/packages/eventwebhook/src/eventwebhook.spec.js @@ -0,0 +1,65 @@ +const EventWebhook = require('./eventwebhook'); + +describe('EventWebhook', () => { + const PUBLIC_KEY = 'MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEEDr2LjtURuePQzplybdC+u4CwrqDqBaWjcMMsTbhdbcwHBcepxo7yAQGhHPTnlvFYPAZFceEu/1FwCM/QmGUhA=='; + const SIGNATURE = 'MEUCIQCtIHJeH93Y+qpYeWrySphQgpNGNr/U+UyUlBkU6n7RAwIgJTz2C+8a8xonZGi6BpSzoQsbVRamr2nlxFDWYNH2j/0='; + const TIMESTAMP = '1588788367'; + const PAYLOAD = { + event: 'test_event', + category: 'example_payload', + message_id: 'message_id', + }; + + describe('#verifySignature()', () => { + it('should verify a valid signature', () => { + expect(verify( + PUBLIC_KEY, + PAYLOAD, + SIGNATURE, + TIMESTAMP + )).to.be.true; + }); + + it('should reject for invalid key', () => { + expect(verify( + 'MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEqTxd43gyp8IOEto2LdIfjRQrIbsd4SXZkLW6jDutdhXSJCWHw8REntlo7aNDthvj+y7GjUuFDb/R1NGe1OPzpA==', + PAYLOAD, + SIGNATURE, + TIMESTAMP + )).to.be.false; + }); + + it('should reject for bad payload', () => { + expect(verify( + PUBLIC_KEY, + 'payload', + SIGNATURE, + TIMESTAMP + )).to.be.false; + }); + + it('should reject for bad signature', () => { + expect(verify( + PUBLIC_KEY, + PAYLOAD, + 'MEUCIQCtIHJeH93Y+qpYeWrySphQgpNGNr/U+UyUlBkU6n7RAwIgJTz2C+8a8xonZGi6BpSzoQsbVRamr2nlxFDWYNH3j/0=', + TIMESTAMP + )).to.be.false; + }); + + it('should reject for bad timestamp', () => { + expect(verify( + PUBLIC_KEY, + PAYLOAD, + SIGNATURE, + 'timestamp' + )).to.be.false; + }); + }); +}); + +function verify(publicKey, payload, signature, timestamp) { + const ew = new EventWebhook(); + const key = ew.convertPublicKeyToECDSA(publicKey); + return ew.verifySignature(key, payload, signature, timestamp); +} diff --git a/test/typescript/eventwebhook.ts b/test/typescript/eventwebhook.ts new file mode 100644 index 000000000..158853796 --- /dev/null +++ b/test/typescript/eventwebhook.ts @@ -0,0 +1,13 @@ +import EventWebhook = require('@sendgrid/eventwebhook'); + +var ew = new EventWebhook(); +const PUBLIC_KEY = "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEEDr2LjtURuePQzplybdC+u4CwrqDqBaWjcMMsTbhdbcwHBcepxo7yAQGhHPTnlvFYPAZFceEu/1FwCM/QmGUhA=="; +const SIGNATURE = "MEUCIQCtIHJeH93Y+qpYeWrySphQgpNGNr/U+UyUlBkU6n7RAwIgJTz2C+8a8xonZGi6BpSzoQsbVRamr2nlxFDWYNH2j/0="; +const TIMESTAMP = "1588788367"; +const PAYLOAD = { + event: 'test_event', + category: 'example_payload', + message_id: 'message_id', +}; +var key = ew.convertPublicKeyToECDSA(PUBLIC_KEY); +console.log(ew.verifySignature(key, PAYLOAD, SIGNATURE, TIMESTAMP)); From 8f52ef8311ca620f56f2aadbb5eda0531c68b5f9 Mon Sep 17 00:00:00 2001 From: Elise Shanholtz Date: Wed, 3 Jun 2020 13:34:25 -0700 Subject: [PATCH 2/3] chore: drop support for node version 7 --- .travis.yml | 1 - packages/client/package.json | 2 +- packages/contact-importer/package.json | 2 +- packages/eventwebhook/package.json | 2 +- packages/inbound-mail-parser/package.json | 2 +- packages/mail/package.json | 2 +- packages/subscription-widget/package.json | 2 +- 7 files changed, 6 insertions(+), 7 deletions(-) diff --git a/.travis.yml b/.travis.yml index 1daa07b9b..bbc0df0c8 100644 --- a/.travis.yml +++ b/.travis.yml @@ -3,7 +3,6 @@ node_js: - 10 env: - version=6 - - version=7 - version=8 - version=10 - version=lts diff --git a/packages/client/package.json b/packages/client/package.json index 2967df1d4..3a643ac95 100644 --- a/packages/client/package.json +++ b/packages/client/package.json @@ -24,7 +24,7 @@ }, "main": "index.js", "engines": { - "node": ">=6.0.0" + "node": "6.* || 8.* || >=10.*" }, "dependencies": { "@sendgrid/helpers": "^7.0.1", diff --git a/packages/contact-importer/package.json b/packages/contact-importer/package.json index 87aff9109..8e13e61f4 100644 --- a/packages/contact-importer/package.json +++ b/packages/contact-importer/package.json @@ -20,7 +20,7 @@ }, "main": "src/importer.js", "engines": { - "node": ">=6.0.0" + "node": "6.* || 8.* || >=10.*" }, "publishConfig": { "access": "public" diff --git a/packages/eventwebhook/package.json b/packages/eventwebhook/package.json index 80f92e01b..4ea546f02 100644 --- a/packages/eventwebhook/package.json +++ b/packages/eventwebhook/package.json @@ -17,7 +17,7 @@ }, "main": "src/eventwebhook.js", "engines": { - "node": ">=6.0.0" + "node": "6.* || 8.* || >=10.*" }, "dependencies": { "@starkbank/ecdsa": "^0.0.3" diff --git a/packages/inbound-mail-parser/package.json b/packages/inbound-mail-parser/package.json index 724edfe70..156f354bc 100644 --- a/packages/inbound-mail-parser/package.json +++ b/packages/inbound-mail-parser/package.json @@ -23,7 +23,7 @@ }, "main": "src/parser.js", "engines": { - "node": ">=6.0.0" + "node": "6.* || 8.* || >=10.*" }, "dependencies": { "@sendgrid/helpers": "^7.0.1", diff --git a/packages/mail/package.json b/packages/mail/package.json index dbbc30a87..9089d0e1f 100644 --- a/packages/mail/package.json +++ b/packages/mail/package.json @@ -21,7 +21,7 @@ }, "main": "index.js", "engines": { - "node": ">=6.0.0" + "node": "6.* || 8.* || >=10.*" }, "publishConfig": { "access": "public" diff --git a/packages/subscription-widget/package.json b/packages/subscription-widget/package.json index ff19149cc..ba14ab9e4 100644 --- a/packages/subscription-widget/package.json +++ b/packages/subscription-widget/package.json @@ -21,6 +21,6 @@ "access": "public" }, "engines": { - "node": ">=6.0.0" + "node": "6.* || 8.* || >=10.*" } } From 7550cb3d591f0f07c005fbac885dc2d197e31f14 Mon Sep 17 00:00:00 2001 From: Elise Shanholtz Date: Wed, 3 Jun 2020 13:43:05 -0700 Subject: [PATCH 3/3] chore: update readme --- CONTRIBUTING.md | 2 +- docs/examples/webhooks-docker/CONTRIBUTING.md | 2 +- packages/client/README.md | 2 +- packages/inbound-mail-parser/README.md | 2 +- packages/mail/README.md | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 6a2054a53..cefb15adf 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -62,7 +62,7 @@ We welcome direct contributions to the sendgrid-nodejs code base. Thank you! ##### Prerequisites ##### -- Node.js version 6, 7 or 8 +- Node.js version 6, 8 or >=10 - Please see [package.json](https://github.com/sendgrid/sendgrid-nodejs/tree/master/package.json) ##### Initial setup: ##### diff --git a/docs/examples/webhooks-docker/CONTRIBUTING.md b/docs/examples/webhooks-docker/CONTRIBUTING.md index b4d4316e9..b05f2b2c2 100644 --- a/docs/examples/webhooks-docker/CONTRIBUTING.md +++ b/docs/examples/webhooks-docker/CONTRIBUTING.md @@ -63,7 +63,7 @@ We welcome direct contributions to the sendgrid-nodejs code base. Thank you! ##### Prerequisites ##### -- Node.js version 6, 7 or 8 +- Node.js versions 6, 8, or >=10 - Please see [package.json](https://github.com/sendgrid/sendgrid-nodejs/tree/master/package.json) ##### Initial setup: ##### diff --git a/packages/client/README.md b/packages/client/README.md index 8504c24b7..0afbc74b9 100644 --- a/packages/client/README.md +++ b/packages/client/README.md @@ -13,7 +13,7 @@ To be notified when this package is updated, please subscribe to email [notifica ## Prerequisites -- Node.js version 6, 7 or 8 +- Node.js version 6, 8 or >=10 - A Twilio SendGrid account, [sign up for free](https://sendgrid.com/free?source=sendgrid-nodejs) to send up to 40,000 emails for the first 30 days or check out [our pricing](https://sendgrid.com/pricing?source=sendgrid-nodejs). ## Obtain an API Key diff --git a/packages/inbound-mail-parser/README.md b/packages/inbound-mail-parser/README.md index 475ddc72e..84b9c8211 100644 --- a/packages/inbound-mail-parser/README.md +++ b/packages/inbound-mail-parser/README.md @@ -11,7 +11,7 @@ To be notified when this package is updated, please subscribe to email [notifica ## Prerequisites -- Node.js version 6, 7 or 8 +- Node.js version 6, 8 or >=10 - A Twilio SendGrid account, [sign up for free](https://sendgrid.com/free?source=sendgrid-nodejs) to send up to 40,000 emails for the first 30 days or check out [our pricing](https://sendgrid.com/pricing?source=sendgrid-nodejs). ## Obtain an API Key diff --git a/packages/mail/README.md b/packages/mail/README.md index 9a0b9962c..c27c39321 100644 --- a/packages/mail/README.md +++ b/packages/mail/README.md @@ -13,7 +13,7 @@ To be notified when this package is updated, please subscribe to email [notifica ## Prerequisites -- Node.js version 6, 7 or 8 +- Node.js version 6, 8 or >=10 - A Twilio SendGrid account, [sign up for free](https://sendgrid.com/free?source=sendgrid-nodejs) to send up to 40,000 emails for the first 30 days or check out [our pricing](https://sendgrid.com/pricing?source=sendgrid-nodejs). ## Obtain an API Key