Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: verify signature from event webhook #425

Merged
merged 6 commits into from
Jun 10, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,5 @@ source 'http://rubygems.org'
gemspec

gem 'ruby_http_client'
gem 'starkbank-ecdsa'

16 changes: 16 additions & 0 deletions examples/helpers/eventwebhook/example.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
require 'sengrid-ruby'
include SendGrid

def is_valid_signature(request)
public_key = 'base64-encoded public key'

event_webhook = SendGrid::EventWebhook.new
ec_public_key = event_webhook.convert_public_key_to_ecdsa(public_key)

event_webhook.verify_signature(
ec_public_key,
request.body.read,
request.env[SendGrid::EventWebhookHeader::SIGNATURE],
request.env[SendGrid::EventWebhookHeader::TIMESTAMP]
)
end
1 change: 1 addition & 0 deletions lib/sendgrid-ruby.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
require_relative 'sendgrid/sendgrid'
require_relative 'sendgrid/twilio_email'
require_relative 'sendgrid/version'
require_relative 'sendgrid/helpers/eventwebhook/eventwebhook'
require_relative 'sendgrid/helpers/ip_management/ip_management'
require_relative 'sendgrid/helpers/mail/asm'
require_relative 'sendgrid/helpers/mail/attachment'
Expand Down
47 changes: 47 additions & 0 deletions lib/sendgrid/helpers/eventwebhook/eventwebhook.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
require 'starkbank-ecdsa'
module SendGrid
# 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
class EventWebhook
# * *Args* :
# - +public_key+ -> verification key under Mail Settings
#
def convert_public_key_to_ecdsa(public_key)
verify_engine
EllipticCurve::PublicKey.fromString(public_key)
end

# * *Args* :
# - +public_key+ -> elliptic curve public key
# - +payload+ -> event payload in the request body
# - +signature+ -> signature value obtained from the 'X-Twilio-Email-Event-Webhook-Signature' header
# - +timestamp+ -> timestamp value obtained from the 'X-Twilio-Email-Event-Webhook-Timestamp' header
def verify_signature(public_key, payload, signature, timestamp)
verify_engine
timestamped_playload = timestamp + payload
decoded_signature = EllipticCurve::Signature.fromBase64(signature)

EllipticCurve::Ecdsa.verify(timestamped_playload, decoded_signature, public_key)
end

def verify_engine
# JRuby does not fully support ECDSA: https://github.com/jruby/jruby-openssl/issues/193
if RUBY_PLATFORM == "java"
raise NotSupportedError, "Event Webhook verification is not supported by JRuby"
end
end

class Error < ::RuntimeError
end

class NotSupportedError < Error
end
end

# 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
class EventWebhookHeader
SIGNATURE = "HTTP_X_TWILIO_EMAIL_EVENT_WEBHOOK_SIGNATURE"
TIMESTAMP = "HTTP_X_TWILIO_EMAIL_EVENT_WEBHOOK_TIMESTAMP"
end
end
1 change: 1 addition & 0 deletions sendgrid-ruby.gemspec
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ Gem::Specification.new do |spec|
spec.test_files = spec.files.grep(/^(test|spec|features)/)
spec.require_paths = ['lib']
spec.add_dependency 'ruby_http_client', '~> 3.4'
spec.add_dependency 'starkbank-ecdsa', '~> 0.0.2'
spec.add_development_dependency 'sinatra', '>= 1.4.7', '< 3'
spec.add_development_dependency 'rake', '~> 13.0'
spec.add_development_dependency 'rspec'
Expand Down
91 changes: 91 additions & 0 deletions spec/sendgrid/helpers/eventwebhook/eventwebhook_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
require "json"
require 'spec_helper'

describe SendGrid::EventWebhook do
PUBLIC_KEY = 'MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEEDr2LjtURuePQzplybdC+u4CwrqDqBaWjcMMsTbhdbcwHBcepxo7yAQGhHPTnlvFYPAZFceEu/1FwCM/QmGUhA=='
SIGNATURE = 'MEUCIQCtIHJeH93Y+qpYeWrySphQgpNGNr/U+UyUlBkU6n7RAwIgJTz2C+8a8xonZGi6BpSzoQsbVRamr2nlxFDWYNH2j/0='
TIMESTAMP = '1588788367'
PAYLOAD = {
'category'=>'example_payload',
'event'=>'test_event',
'message_id'=>'message_id',
}.to_json

describe '.verify_signature' do
it 'verifies a valid signature' do
unless skip_jruby
expect(verify(PUBLIC_KEY, PAYLOAD, SIGNATURE, TIMESTAMP)).to be
end
end

it 'rejects a bad key' do
unless skip_jruby
expect(verify(
'MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEqTxd43gyp8IOEto2LdIfjRQrIbsd4SXZkLW6jDutdhXSJCWHw8REntlo7aNDthvj+y7GjUuFDb/R1NGe1OPzpA==',
PAYLOAD,
SIGNATURE,
TIMESTAMP
)).not_to be
end
end

it 'rejects a bad payload' do
unless skip_jruby
expect(verify(
PUBLIC_KEY,
'payload',
SIGNATURE,
TIMESTAMP
)).not_to be
end
end

it 'rejects a bad signature' do
unless skip_jruby
expect(verify(
PUBLIC_KEY,
PAYLOAD,
'MEUCIQCtIHJeH93Y+qpYeWrySphQgpNGNr/U+UyUlBkU6n7RAwIgJTz2C+8a8xonZGi6BpSzoQsbVRamr2nlxFDWYNH3j/0=',
TIMESTAMP
)).not_to be
end
end

it 'rejects a bad timestamp' do
unless skip_jruby
expect(verify(
PUBLIC_KEY,
PAYLOAD,
SIGNATURE,
'timestamp'
)).not_to be
end
end

it 'throws an error when using jruby' do
if skip_jruby
expect{ verify(PUBLIC_KEY, PAYLOAD, SIGNATURE, TIMESTAMP) }.to raise_error(SendGrid::EventWebhook::NotSupportedError)
end
end
end
end

describe SendGrid::EventWebhookHeader do
it 'sets the signature header constant' do
expect(SendGrid::EventWebhookHeader::SIGNATURE).to eq("HTTP_X_TWILIO_EMAIL_EVENT_WEBHOOK_SIGNATURE")
end

it 'sets the timestamp header constant' do
expect(SendGrid::EventWebhookHeader::TIMESTAMP).to eq("HTTP_X_TWILIO_EMAIL_EVENT_WEBHOOK_TIMESTAMP")
end
end

def verify(public_key, payload, signature, timestamp)
ew = SendGrid::EventWebhook.new
ec_public_key = ew.convert_public_key_to_ecdsa(public_key)
ew.verify_signature(ec_public_key, payload, signature, timestamp)
end

def skip_jruby
RUBY_PLATFORM == 'java'
end