Skip to content

Commit 131706b

Browse files
author
childish-sambino
authored
feat: verify signature from event webhook (#1010)
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 197e97d commit 131706b

File tree

3 files changed

+156
-0
lines changed

3 files changed

+156
-0
lines changed
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
using Microsoft.AspNetCore.Http;
2+
using SendGrid.Helpers.EventWebhook;
3+
using System.IO;
4+
5+
public bool IsValidSignature(HttpRequest request)
6+
{
7+
var publicKey = "base64-encoded public key";
8+
string requestBody;
9+
10+
using (var reader = new StreamReader(request.Body))
11+
{
12+
requestBody = reader.ReadToEnd();
13+
}
14+
15+
var validator = new RequestValidator();
16+
var ecPublicKey = validator.ConvertPublicKeyToECDSA(publicKey);
17+
18+
return validator.VerifySignature(
19+
ecPublicKey,
20+
requestBody,
21+
request.Headers[RequestValidator.SIGNATURE_HEADER],
22+
request.Headers[RequestValidator.TIMESTAMP_HEADER]
23+
);
24+
}
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
using EllipticCurve;
2+
3+
namespace SendGrid.Helpers.EventWebhook
4+
{
5+
/// <summary>
6+
/// This class allows you to use the Event Webhook feature. Read the docs for
7+
/// more details: https://sendgrid.com/docs/for-developers/tracking-events/event
8+
/// </summary>
9+
public class RequestValidator
10+
{
11+
/// <summary>
12+
/// Signature verification HTTP header name for the signature being sent.
13+
/// </summary>
14+
public const string SIGNATURE_HEADER = "X-Twilio-Email-Event-Webhook-Signature";
15+
16+
/// <summary>
17+
/// Timestamp HTTP header name for timestamp.
18+
/// </summary>
19+
public const string TIMESTAMP_HEADER = "X-Twilio-Email-Event-Webhook-Timestamp";
20+
21+
/// <summary>
22+
/// Convert the public key string to a <see cref="PublicKey"/>.
23+
/// </summary>
24+
/// <param name="publicKey">verification key under Mail Settings</param>
25+
/// <returns>public key using the ECDSA algorithm</returns>
26+
public PublicKey ConvertPublicKeyToECDSA(string publicKey)
27+
{
28+
return PublicKey.fromPem(publicKey);
29+
}
30+
31+
/// <summary>
32+
/// Verify signed event webhook requests.
33+
/// </summary>
34+
/// <param name="publicKey">elliptic curve public key</param>
35+
/// <param name="payload">event payload in the request body</param>
36+
/// <param name="signature">value obtained from the 'X-Twilio-Email-Event-Webhook-Signature' header</param>
37+
/// <param name="timestamp">value obtained from the 'X-Twilio-Email-Event-Webhook-Timestamp' header</param>
38+
/// <returns>true or false if signature is valid</returns>
39+
public bool VerifySignature(PublicKey publicKey, string payload, string signature, string timestamp)
40+
{
41+
var timestampedPayload = timestamp + payload;
42+
var decodedSignature = Signature.fromBase64(signature);
43+
44+
return Ecdsa.verify(timestampedPayload, decodedSignature, publicKey);
45+
}
46+
}
47+
}
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
using Xunit;
2+
using SendGrid.Helpers.EventWebhook;
3+
4+
namespace SendGrid.Tests.Helpers.EventWebhook
5+
{
6+
public class RequestValidatorTests
7+
{
8+
private const string PUBLIC_KEY = "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEEDr2LjtURuePQzplybdC+u4CwrqDqBaWjcMMsTbhdbcwHBcepxo7yAQGhHPTnlvFYPAZFceEu/1FwCM/QmGUhA==";
9+
private const string PAYLOAD = "{\"category\":\"example_payload\",\"event\":\"test_event\",\"message_id\":\"message_id\"}";
10+
private const string SIGNATURE = "MEUCIQCtIHJeH93Y+qpYeWrySphQgpNGNr/U+UyUlBkU6n7RAwIgJTz2C+8a8xonZGi6BpSzoQsbVRamr2nlxFDWYNH2j/0=";
11+
private const string TIMESTAMP = "1588788367";
12+
13+
[Fact]
14+
public void TestVerifySignature()
15+
{
16+
var isValidSignature = Verify(
17+
PUBLIC_KEY,
18+
PAYLOAD,
19+
SIGNATURE,
20+
TIMESTAMP
21+
);
22+
23+
Assert.True(isValidSignature);
24+
}
25+
26+
[Fact]
27+
public void TestBadKey()
28+
{
29+
var isValidSignature = Verify(
30+
"MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEqTxd43gyp8IOEto2LdIfjRQrIbsd4SXZkLW6jDutdhXSJCWHw8REntlo7aNDthvj+y7GjUuFDb/R1NGe1OPzpA==",
31+
PAYLOAD,
32+
SIGNATURE,
33+
TIMESTAMP
34+
);
35+
36+
Assert.False(isValidSignature);
37+
}
38+
39+
[Fact]
40+
public void TestBadPayload()
41+
{
42+
var isValidSignature = Verify(
43+
PUBLIC_KEY,
44+
"payload",
45+
SIGNATURE,
46+
TIMESTAMP
47+
);
48+
49+
Assert.False(isValidSignature);
50+
}
51+
52+
[Fact]
53+
public void TestBadSignature()
54+
{
55+
var isValidSignature = Verify(
56+
PUBLIC_KEY,
57+
PAYLOAD,
58+
"MEQCIB3bJQOarffIdM7+MEee+kYAdoViz6RUoScOASwMcXQxAiAcrus/j853JUlVm5qIRfbKBJwJq89znqOTedy3RetXLQ==",
59+
TIMESTAMP
60+
);
61+
62+
Assert.False(isValidSignature);
63+
}
64+
65+
[Fact]
66+
public void TestBadTimestamp()
67+
{
68+
var isValidSignature = Verify(
69+
PUBLIC_KEY,
70+
PAYLOAD,
71+
SIGNATURE,
72+
"timestamp"
73+
);
74+
75+
Assert.False(isValidSignature);
76+
}
77+
78+
private bool Verify(string publicKey, string payload, string signature, string timestamp)
79+
{
80+
var validator = new RequestValidator();
81+
var ecPublicKey = validator.ConvertPublicKeyToECDSA(publicKey);
82+
return validator.VerifySignature(ecPublicKey, payload, signature, timestamp);
83+
}
84+
}
85+
}

0 commit comments

Comments
 (0)