Skip to content

Commit

Permalink
feat: verify signature from event webhook (#969)
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
childish-sambino authored May 29, 2020
1 parent 1712b5a commit eb22165
Show file tree
Hide file tree
Showing 7 changed files with 179 additions and 2 deletions.
1 change: 0 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,5 @@ test.php
.vscode
prism*
temp.php
example*.php
TODO.txt
sendgrid-php.zip
4 changes: 3 additions & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
"require": {
"php": ">=5.6",
"sendgrid/php-http-client": "~3.10",
"starkbank/ecdsa": "0.*",
"ext-curl": "*",
"ext-json": "*",
"ext-mbstring": "*",
Expand All @@ -31,8 +32,9 @@
"type": "library",
"autoload": {
"psr-4": {
"SendGrid\\Mail\\": "lib/mail/",
"SendGrid\\Contacts\\": "lib/contacts/",
"SendGrid\\EventWebhook\\": "lib/eventwebhook/",
"SendGrid\\Mail\\": "lib/mail/",
"SendGrid\\Stats\\": "lib/stats/"
},
"classmap": [
Expand Down
20 changes: 20 additions & 0 deletions examples/helpers/eventwebhook/example.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
<?php

use SendGrid\EventWebhook\EventWebhook;
use SendGrid\EventWebhook\EventWebhookHeader;


function isValidSignature($request)
{
$publicKey = 'base64-encoded public key';

$eventWebhook = new EventWebhook();
$ecPublicKey = $eventWebhook->convertPublicKeyToECDSA($publicKey);

return $eventWebhook->verifySignature(
$ecPublicKey,
$request->getContent(),
$request->header(EventWebhookHeader::SIGNATURE),
$request->header(EventWebhookHeader::TIMESTAMP)
);
}
46 changes: 46 additions & 0 deletions lib/eventwebhook/EventWebhook.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
<?php

namespace SendGrid\EventWebhook;

use EllipticCurve\Ecdsa;
use EllipticCurve\PublicKey;
use EllipticCurve\Signature;

/**
* This class allows you to use the Event Webhook feature. Read the docs for
* more details: https://sendgrid.com/docs/for-developers/tracking-events/event
*
* @package SendGrid\EventWebhook
*/
class EventWebhook
{
/**
* Convert the public key string to a ECPublicKey.
*
* @param string $publicKey verification key under Mail Settings
* @return PublicKey public key using the ECDSA algorithm
*/
public function convertPublicKeyToECDSA($publicKey)
{
return PublicKey::fromString($publicKey);
}

/**
* Verify signed event webhook requests.
*
* @param PublicKey $publicKey elliptic curve public key
* @param 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 bool true or false if signature is valid
*/
public function verifySignature($publicKey, $payload, $signature, $timestamp)
{
$timestampedPayload = $timestamp . $payload;
$decodedSignature = Signature::fromBase64($signature);

return Ecdsa::verify($timestampedPayload, $decodedSignature, $publicKey);
}
}
15 changes: 15 additions & 0 deletions lib/eventwebhook/EventWebhookHeader.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<?php

namespace SendGrid\EventWebhook;

/**
* This class lists headers that get posted to the webhook. Read the docs for
* more details: https://sendgrid.com/docs/for-developers/tracking-events/event
*
* @package SendGrid\EventWebhook
*/
abstract class EventWebhookHeader
{
const SIGNATURE = "X-Twilio-Email-Event-Webhook-Signature";
const TIMESTAMP = "X-Twilio-Email-Event-Webhook-Timestamp";
}
1 change: 1 addition & 0 deletions lib/loader.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
require_once __DIR__ . '/TwilioEmail.php';
require_once __DIR__ . '/contacts/Recipient.php';
require_once __DIR__ . '/contacts/RecipientForm.php';
require_once __DIR__ . '/eventwebhook/EventWebhook.php';
require_once __DIR__ . '/mail/EmailAddress.php';
require_once __DIR__ . '/mail/Asm.php';
require_once __DIR__ . '/mail/Attachment.php';
Expand Down
94 changes: 94 additions & 0 deletions test/unit/EventWebhookTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
<?php

namespace SendGrid\Tests\Unit;

use PHPUnit\Framework\TestCase;
use SendGrid\EventWebhook\EventWebhook;

/**
* This class tests the EventWebhook functionality.
*
* @package SendGrid\Tests\Unit
*/
class EventWebhookTest extends TestCase
{
const PUBLIC_KEY = 'MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEEDr2LjtURuePQzplybd
C+u4CwrqDqBaWjcMMsTbhdbcwHBcepxo7yAQGhHPTnlvFYPAZFceEu/1FwCM/QmGUhA==';
const PAYLOAD = '{"category":"example_payload","event":"test_event","message_id":"message_id"}';
const SIGNATURE = 'MEUCIQCtIHJeH93Y+qpYeWrySphQgpNGNr/U+UyUlBkU6n7RAwIgJTz2
C+8a8xonZGi6BpSzoQsbVRamr2nlxFDWYNH2j/0=';
const TIMESTAMP = '1588788367';

public function testVerifySignature()
{
$isValidSignature = $this->verify(
self::PUBLIC_KEY,
self::PAYLOAD,
self::SIGNATURE,
self::TIMESTAMP
);

$this->assertTrue($isValidSignature);
}

public function testBadKey()
{
$isValidSignature = $this->verify(
'MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEqTxd43gyp8IOEto2LdIfjRQrIbsd4S
XZkLW6jDutdhXSJCWHw8REntlo7aNDthvj+y7GjUuFDb/R1NGe1OPzpA==',
self::PAYLOAD,
self::SIGNATURE,
self::TIMESTAMP
);

$this->assertFalse($isValidSignature);
}

public function testBadPayload()
{
$isValidSignature = $this->verify(
self::PUBLIC_KEY,
'payload',
self::SIGNATURE,
self::TIMESTAMP
);

$this->assertFalse($isValidSignature);
}

public function testBadSignature()
{
$isValidSignature = $this->verify(
self::PUBLIC_KEY,
self::PAYLOAD,
'signature',
self::TIMESTAMP
);

$this->assertFalse($isValidSignature);
}

public function testBadTimestamp()
{
$isValidSignature = $this->verify(
self::PUBLIC_KEY,
self::PAYLOAD,
self::SIGNATURE,
'timestamp'
);

$this->assertFalse($isValidSignature);
}

private function verify($publicKey, $payload, $signature, $timestamp)
{
$eventWebhook = new EventWebhook();
$ecPublicKey = $eventWebhook->convertPublicKeyToECDSA($publicKey);
return $eventWebhook->verifySignature(
$ecPublicKey,
$payload,
$signature,
$timestamp
);
}
}

0 comments on commit eb22165

Please sign in to comment.