diff --git a/examples/eventwebhook/RequestValidator.cs b/examples/eventwebhook/RequestValidator.cs new file mode 100644 index 000000000..e849b8544 --- /dev/null +++ b/examples/eventwebhook/RequestValidator.cs @@ -0,0 +1,24 @@ +using Microsoft.AspNetCore.Http; +using SendGrid.Helpers.EventWebhook; +using System.IO; + +public bool IsValidSignature(HttpRequest request) +{ + var publicKey = "base64-encoded public key"; + string requestBody; + + using (var reader = new StreamReader(request.Body)) + { + requestBody = reader.ReadToEnd(); + } + + var validator = new RequestValidator(); + var ecPublicKey = validator.ConvertPublicKeyToECDSA(publicKey); + + return validator.VerifySignature( + ecPublicKey, + requestBody, + request.Headers[RequestValidator.SIGNATURE_HEADER], + request.Headers[RequestValidator.TIMESTAMP_HEADER] + ); +} diff --git a/src/SendGrid/Helpers/EventWebhook/RequestValidator.cs b/src/SendGrid/Helpers/EventWebhook/RequestValidator.cs new file mode 100644 index 000000000..5b708eb5b --- /dev/null +++ b/src/SendGrid/Helpers/EventWebhook/RequestValidator.cs @@ -0,0 +1,47 @@ +using EllipticCurve; + +namespace SendGrid.Helpers.EventWebhook +{ + /// + /// 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 + /// + public class RequestValidator + { + /// + /// Signature verification HTTP header name for the signature being sent. + /// + public const string SIGNATURE_HEADER = "X-Twilio-Email-Event-Webhook-Signature"; + + /// + /// Timestamp HTTP header name for timestamp. + /// + public const string TIMESTAMP_HEADER = "X-Twilio-Email-Event-Webhook-Timestamp"; + + /// + /// Convert the public key string to a . + /// + /// verification key under Mail Settings + /// public key using the ECDSA algorithm + public PublicKey ConvertPublicKeyToECDSA(string publicKey) + { + return PublicKey.fromPem(publicKey); + } + + /// + /// Verify signed event webhook requests. + /// + /// elliptic curve public key + /// event payload in the request body + /// value obtained from the 'X-Twilio-Email-Event-Webhook-Signature' header + /// value obtained from the 'X-Twilio-Email-Event-Webhook-Timestamp' header + /// true or false if signature is valid + public bool VerifySignature(PublicKey publicKey, string payload, string signature, string timestamp) + { + var timestampedPayload = timestamp + payload; + var decodedSignature = Signature.fromBase64(signature); + + return Ecdsa.verify(timestampedPayload, decodedSignature, publicKey); + } + } +} diff --git a/tests/SendGrid.Tests/Helpers/EventWebhook/RequestValidatorTests.cs b/tests/SendGrid.Tests/Helpers/EventWebhook/RequestValidatorTests.cs new file mode 100644 index 000000000..d05a94fe8 --- /dev/null +++ b/tests/SendGrid.Tests/Helpers/EventWebhook/RequestValidatorTests.cs @@ -0,0 +1,85 @@ +using Xunit; +using SendGrid.Helpers.EventWebhook; + +namespace SendGrid.Tests.Helpers.EventWebhook +{ + public class RequestValidatorTests + { + private const string PUBLIC_KEY = "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEEDr2LjtURuePQzplybdC+u4CwrqDqBaWjcMMsTbhdbcwHBcepxo7yAQGhHPTnlvFYPAZFceEu/1FwCM/QmGUhA=="; + private const string PAYLOAD = "{\"category\":\"example_payload\",\"event\":\"test_event\",\"message_id\":\"message_id\"}"; + private const string SIGNATURE = "MEUCIQCtIHJeH93Y+qpYeWrySphQgpNGNr/U+UyUlBkU6n7RAwIgJTz2C+8a8xonZGi6BpSzoQsbVRamr2nlxFDWYNH2j/0="; + private const string TIMESTAMP = "1588788367"; + + [Fact] + public void TestVerifySignature() + { + var isValidSignature = Verify( + PUBLIC_KEY, + PAYLOAD, + SIGNATURE, + TIMESTAMP + ); + + Assert.True(isValidSignature); + } + + [Fact] + public void TestBadKey() + { + var isValidSignature = Verify( + "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEqTxd43gyp8IOEto2LdIfjRQrIbsd4SXZkLW6jDutdhXSJCWHw8REntlo7aNDthvj+y7GjUuFDb/R1NGe1OPzpA==", + PAYLOAD, + SIGNATURE, + TIMESTAMP + ); + + Assert.False(isValidSignature); + } + + [Fact] + public void TestBadPayload() + { + var isValidSignature = Verify( + PUBLIC_KEY, + "payload", + SIGNATURE, + TIMESTAMP + ); + + Assert.False(isValidSignature); + } + + [Fact] + public void TestBadSignature() + { + var isValidSignature = Verify( + PUBLIC_KEY, + PAYLOAD, + "MEQCIB3bJQOarffIdM7+MEee+kYAdoViz6RUoScOASwMcXQxAiAcrus/j853JUlVm5qIRfbKBJwJq89znqOTedy3RetXLQ==", + TIMESTAMP + ); + + Assert.False(isValidSignature); + } + + [Fact] + public void TestBadTimestamp() + { + var isValidSignature = Verify( + PUBLIC_KEY, + PAYLOAD, + SIGNATURE, + "timestamp" + ); + + Assert.False(isValidSignature); + } + + private bool Verify(string publicKey, string payload, string signature, string timestamp) + { + var validator = new RequestValidator(); + var ecPublicKey = validator.ConvertPublicKeyToECDSA(publicKey); + return validator.VerifySignature(ecPublicKey, payload, signature, timestamp); + } + } +}