Skip to content

Commit

Permalink
feat: verify signature from event webhook
Browse files Browse the repository at this point in the history
Brings in the changes raised in this PR - sendgrid#969
  • Loading branch information
RobertG4M committed Jan 19, 2021
1 parent 95bc3a3 commit 6e61ab6
Show file tree
Hide file tree
Showing 8 changed files with 222 additions and 6 deletions.
3 changes: 1 addition & 2 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -14,5 +14,4 @@ test/prism/*
.vscode
prism*
temp.php
example*.php
TODO.txt
TODO.txt
2 changes: 2 additions & 0 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
"require": {
"php": ">=5.4",
"gear4music/php-http-client": "dev-php-downgrade@dev",
"starkbank/ecdsa": "0.*",
"ext-curl": "*",
"ext-json": "*",
"ext-mbstring": "*"
Expand All @@ -35,6 +36,7 @@
"SendGrid\\": "lib/",
"SendGrid\\Mail\\": "lib/mail/",
"SendGrid\\Contacts\\": "lib/contacts/",
"SendGrid\\EventWebhook\\": "lib/eventwebhook/",
"SendGrid\\Stats\\": "lib/stats/"
},
"files": ["lib/SendGrid.php"]
Expand Down
43 changes: 41 additions & 2 deletions composer.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

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";
}
5 changes: 3 additions & 2 deletions lib/loader.php
Original file line number Diff line number Diff line change
@@ -1,19 +1,20 @@
<?php
/**
* Allows us to include one file instead of two when working without composer.
*
*
* PHP Version - 5.6, 7.0, 7.1, 7.2
*
* @package SendGrid\Tests
* @author Elmer Thomas <[email protected]>
* @copyright 2018 SendGrid
* @license https://opensource.org/licenses/MIT The MIT License
* @version GIT: <git_id>
* @link http://packagist.org/packages/sendgrid/sendgrid
* @link http://packagist.org/packages/sendgrid/sendgrid
*/
require_once __DIR__ . '/SendGrid.php';
require_once __DIR__ . '/contacts/Recipient.php';
require_once __DIR__ . '/contacts/RecipientForm.php';
require_once __DIR__ . '/eventwebhook/EventWebhook.php';
require_once __DIR__ . '/mail/Asm.php';
require_once __DIR__ . '/mail/Attachment.php';
require_once __DIR__ . '/mail/BatchId';
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 6e61ab6

Please sign in to comment.