Skip to content

Commit 16d4d39

Browse files
authored
feat: verify signature from event webhook (#1136)
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.
1 parent 12f8f80 commit 16d4d39

File tree

19 files changed

+182
-11
lines changed

19 files changed

+182
-11
lines changed

Diff for: .travis.yml

-1
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@ node_js:
33
- 10
44
env:
55
- version=6
6-
- version=7
76
- version=8
87
- version=10
98
- version=lts

Diff for: CONTRIBUTING.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@ We welcome direct contributions to the sendgrid-nodejs code base. Thank you!
6262

6363
##### Prerequisites #####
6464

65-
- Node.js version 6, 7 or 8
65+
- Node.js version 6, 8 or >=10
6666
- Please see [package.json](https://github.com/sendgrid/sendgrid-nodejs/tree/master/package.json)
6767

6868
##### Initial setup: #####

Diff for: docs/examples/webhooks-docker/CONTRIBUTING.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@ We welcome direct contributions to the sendgrid-nodejs code base. Thank you!
6363

6464
##### Prerequisites #####
6565

66-
- Node.js version 6, 7 or 8
66+
- Node.js versions 6, 8, or >=10
6767
- Please see [package.json](https://github.com/sendgrid/sendgrid-nodejs/tree/master/package.json)
6868

6969
##### Initial setup: #####

Diff for: package.json

+1
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
"test:helpers": "babel-node ./node_modules/istanbul/lib/cli cover ./node_modules/mocha/bin/_mocha \"packages/helpers/**/*.spec.js\"",
3232
"test:client": "babel-node ./node_modules/istanbul/lib/cli cover ./node_modules/mocha/bin/_mocha \"packages/client/**/*.spec.js\"",
3333
"test:mail": "babel-node ./node_modules/istanbul/lib/cli cover ./node_modules/mocha/bin/_mocha \"packages/mail/**/*.spec.js\"",
34+
"test:eventwebhook": "babel-node ./node_modules/istanbul/lib/cli cover ./node_modules/mocha/bin/_mocha \"packages/eventwebhook/**/*.spec.js\"",
3435
"test:inbound": "babel-node ./node_modules/istanbul/lib/cli cover ./node_modules/mocha/bin/_mocha \"packages/inbound-mail-parser/**/*.spec.js\"",
3536
"test:contact": "babel-node ./node_modules/istanbul/lib/cli cover ./node_modules/mocha/bin/_mocha \"packages/contact-importer/**/*.spec.js\"",
3637
"test:files": "babel-node ./node_modules/istanbul/lib/cli cover ./node_modules/mocha/bin/_mocha \"test/files.spec.js\"",

Diff for: packages/client/README.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ To be notified when this package is updated, please subscribe to email [notifica
1313

1414
## Prerequisites
1515

16-
- Node.js version 6, 7 or 8
16+
- Node.js version 6, 8 or >=10
1717
- 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).
1818

1919
## Obtain an API Key

Diff for: packages/client/package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@
2424
},
2525
"main": "index.js",
2626
"engines": {
27-
"node": ">=6.0.0"
27+
"node": "6.* || 8.* || >=10.*"
2828
},
2929
"dependencies": {
3030
"@sendgrid/helpers": "^7.0.1",

Diff for: packages/contact-importer/package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@
2020
},
2121
"main": "src/importer.js",
2222
"engines": {
23-
"node": ">=6.0.0"
23+
"node": "6.* || 8.* || >=10.*"
2424
},
2525
"publishConfig": {
2626
"access": "public"

Diff for: packages/eventwebhook/index.d.ts

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
import EventWebhook = require('./src/eventwebhook');
2+
3+
export = EventWebhook;

Diff for: packages/eventwebhook/index.js

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
'use strict';
2+
3+
const EventWebhook = require('./src/eventwebhook');
4+
5+
module.exports = EventWebhook;

Diff for: packages/eventwebhook/package.json

+28
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
{
2+
"name": "@sendgrid/eventwebhook",
3+
"description": "Twilio SendGrid NodeJS Event Webhook",
4+
"version": "1.0.0",
5+
"author": "Twilio SendGrid <[email protected]> (sendgrid.com)",
6+
"contributors": [
7+
"Elise Shanholtz <[email protected]>"
8+
],
9+
"license": "MIT",
10+
"homepage": "https://sendgrid.com",
11+
"repository": {
12+
"type": "git",
13+
"url": "git://github.com/sendgrid/sendgrid-nodejs.git"
14+
},
15+
"publishConfig": {
16+
"access": "public"
17+
},
18+
"main": "src/eventwebhook.js",
19+
"engines": {
20+
"node": "6.* || 8.* || >=10.*"
21+
},
22+
"dependencies": {
23+
"@starkbank/ecdsa": "^0.0.3"
24+
},
25+
"tags": [
26+
"sendgrid"
27+
]
28+
}

Diff for: packages/eventwebhook/src/eventwebhook.d.ts

+22
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import {PublicKey} from "@starkbank/ecdsa";
2+
3+
declare class EventWebhook {
4+
/**
5+
*
6+
* @param {string} publicKey verification key under Mail Settings
7+
* @return {PublicKey} A public key using the ECDSA algorithm
8+
*/
9+
convertPublicKeyToECDSA(publicKey: string): PublicKey;
10+
11+
/**
12+
*
13+
* @param {PublicKey} publicKey elliptic curve public key
14+
* @param {object|string} payload event payload in the request body
15+
* @param {string} signature value obtained from the 'X-Twilio-Email-Event-Webhook-Signature' header
16+
* @param {string} timestamp value obtained from the 'X-Twilio-Email-Event-Webhook-Timestamp' header
17+
* @return {Boolean} true or false if signature is valid
18+
*/
19+
verifySignature(publicKey: PublicKey, payload: object|string, signature: string, timestamp: string): boolean;
20+
}
21+
22+
export = EventWebhook;

Diff for: packages/eventwebhook/src/eventwebhook.js

+35
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
'use strict';
2+
3+
const ecdsa = require('@starkbank/ecdsa');
4+
const Ecdsa = ecdsa.Ecdsa;
5+
const Signature = ecdsa.Signature;
6+
const PublicKey = ecdsa.PublicKey;
7+
8+
class EventWebhook {
9+
/**
10+
*
11+
* @param {string} publicKey verification key under Mail Settings
12+
* @return {PublicKey} A public key using the ECDSA algorithm
13+
*/
14+
convertPublicKeyToECDSA(publicKey) {
15+
return PublicKey.fromPem(publicKey);
16+
}
17+
18+
/**
19+
*
20+
* @param {PublicKey} publicKey elliptic curve public key
21+
* @param {Object|string} payload event payload in the request body
22+
* @param {string} signature value obtained from the 'X-Twilio-Email-Event-Webhook-Signature' header
23+
* @param {string} timestamp value obtained from the 'X-Twilio-Email-Event-Webhook-Timestamp' header
24+
* @return {Boolean} true or false if signature is valid
25+
*/
26+
verifySignature(publicKey, payload, signature, timestamp) {
27+
let timestampPayload = typeof payload === 'object' ? JSON.stringify(payload) : payload;
28+
timestampPayload = timestamp + timestampPayload;
29+
const decodedSignature = Signature.fromBase64(signature);
30+
31+
return Ecdsa.verify(timestampPayload, decodedSignature, publicKey);
32+
}
33+
}
34+
35+
module.exports = EventWebhook;

Diff for: packages/eventwebhook/src/eventwebhook.spec.js

+65
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
const EventWebhook = require('./eventwebhook');
2+
3+
describe('EventWebhook', () => {
4+
const PUBLIC_KEY = 'MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEEDr2LjtURuePQzplybdC+u4CwrqDqBaWjcMMsTbhdbcwHBcepxo7yAQGhHPTnlvFYPAZFceEu/1FwCM/QmGUhA==';
5+
const SIGNATURE = 'MEUCIQCtIHJeH93Y+qpYeWrySphQgpNGNr/U+UyUlBkU6n7RAwIgJTz2C+8a8xonZGi6BpSzoQsbVRamr2nlxFDWYNH2j/0=';
6+
const TIMESTAMP = '1588788367';
7+
const PAYLOAD = {
8+
event: 'test_event',
9+
category: 'example_payload',
10+
message_id: 'message_id',
11+
};
12+
13+
describe('#verifySignature()', () => {
14+
it('should verify a valid signature', () => {
15+
expect(verify(
16+
PUBLIC_KEY,
17+
PAYLOAD,
18+
SIGNATURE,
19+
TIMESTAMP
20+
)).to.be.true;
21+
});
22+
23+
it('should reject for invalid key', () => {
24+
expect(verify(
25+
'MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEqTxd43gyp8IOEto2LdIfjRQrIbsd4SXZkLW6jDutdhXSJCWHw8REntlo7aNDthvj+y7GjUuFDb/R1NGe1OPzpA==',
26+
PAYLOAD,
27+
SIGNATURE,
28+
TIMESTAMP
29+
)).to.be.false;
30+
});
31+
32+
it('should reject for bad payload', () => {
33+
expect(verify(
34+
PUBLIC_KEY,
35+
'payload',
36+
SIGNATURE,
37+
TIMESTAMP
38+
)).to.be.false;
39+
});
40+
41+
it('should reject for bad signature', () => {
42+
expect(verify(
43+
PUBLIC_KEY,
44+
PAYLOAD,
45+
'MEUCIQCtIHJeH93Y+qpYeWrySphQgpNGNr/U+UyUlBkU6n7RAwIgJTz2C+8a8xonZGi6BpSzoQsbVRamr2nlxFDWYNH3j/0=',
46+
TIMESTAMP
47+
)).to.be.false;
48+
});
49+
50+
it('should reject for bad timestamp', () => {
51+
expect(verify(
52+
PUBLIC_KEY,
53+
PAYLOAD,
54+
SIGNATURE,
55+
'timestamp'
56+
)).to.be.false;
57+
});
58+
});
59+
});
60+
61+
function verify(publicKey, payload, signature, timestamp) {
62+
const ew = new EventWebhook();
63+
const key = ew.convertPublicKeyToECDSA(publicKey);
64+
return ew.verifySignature(key, payload, signature, timestamp);
65+
}

Diff for: packages/inbound-mail-parser/README.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ To be notified when this package is updated, please subscribe to email [notifica
1111

1212
## Prerequisites
1313

14-
- Node.js version 6, 7 or 8
14+
- Node.js version 6, 8 or >=10
1515
- 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).
1616

1717
## Obtain an API Key

Diff for: packages/inbound-mail-parser/package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@
2323
},
2424
"main": "src/parser.js",
2525
"engines": {
26-
"node": ">=6.0.0"
26+
"node": "6.* || 8.* || >=10.*"
2727
},
2828
"dependencies": {
2929
"@sendgrid/helpers": "^7.0.1",

Diff for: packages/mail/README.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ To be notified when this package is updated, please subscribe to email [notifica
1313

1414
## Prerequisites
1515

16-
- Node.js version 6, 7 or 8
16+
- Node.js version 6, 8 or >=10
1717
- 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).
1818

1919
## Obtain an API Key

Diff for: packages/mail/package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@
2121
},
2222
"main": "index.js",
2323
"engines": {
24-
"node": ">=6.0.0"
24+
"node": "6.* || 8.* || >=10.*"
2525
},
2626
"publishConfig": {
2727
"access": "public"

Diff for: packages/subscription-widget/package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,6 @@
2121
"access": "public"
2222
},
2323
"engines": {
24-
"node": ">=6.0.0"
24+
"node": "6.* || 8.* || >=10.*"
2525
}
2626
}

Diff for: test/typescript/eventwebhook.ts

+13
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import EventWebhook = require('@sendgrid/eventwebhook');
2+
3+
var ew = new EventWebhook();
4+
const PUBLIC_KEY = "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEEDr2LjtURuePQzplybdC+u4CwrqDqBaWjcMMsTbhdbcwHBcepxo7yAQGhHPTnlvFYPAZFceEu/1FwCM/QmGUhA==";
5+
const SIGNATURE = "MEUCIQCtIHJeH93Y+qpYeWrySphQgpNGNr/U+UyUlBkU6n7RAwIgJTz2C+8a8xonZGi6BpSzoQsbVRamr2nlxFDWYNH2j/0=";
6+
const TIMESTAMP = "1588788367";
7+
const PAYLOAD = {
8+
event: 'test_event',
9+
category: 'example_payload',
10+
message_id: 'message_id',
11+
};
12+
var key = ew.convertPublicKeyToECDSA(PUBLIC_KEY);
13+
console.log(ew.verifySignature(key, PAYLOAD, SIGNATURE, TIMESTAMP));

0 commit comments

Comments
 (0)