Repository: sendgrid/sendgrid-java
Pull request #622 (2020-05-06 02:05:31)

feat: verify signature from event webhook
<!--
We appreciate the effort for this pull request, but before that, please make sure you read the contribution guidelines, then fill out the blanks below.

Please format the PR title appropriately based on the type of change:
  <type>[!]: <description>
Where <type> is one of: docs, chore, feat, fix, test.
Add a '!' after the type for breaking changes (e.g. feat!: new breaking feature).

**All third-party contributors acknowledge that any contributions they provide will be made under the same open-source license that the open-source project is provided under.**

Please enter each issue number you are resolving in your PR after one of the following words [Fixes, Closes, Resolves]. This will auto-link these issues and close them when this PR is merged!
e.g.
Fixes #1
Closes #2
-->

# Fixes #

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.

I want to give a shoutout to @thavanle for helping me create this new feature. Thanks @thavanle for helping more developers use Twilio SendGrid securely!

### Checklist
- [x] I acknowledge that all my contributions will be made under the project's license.
- [x] I have made a material change to the repo (functionality, testing, spelling, grammar).
- [x] I have read the [Contribution Guidelines](CONTRIBUTING.md) and my PR follows them.
- [x] I have titled the PR appropriately.
- [x] I have updated my branch with the master branch.
- [x] I have added tests that prove my fix is effective or that my feature works.
- [x] I have added necessary documentation about the functionality in the appropriate .md file
- [x] I have added in-line documentation to the code I modified.

If you have questions, please file a [support ticket](https://twilio.com/help/contact), or create a GitHub Issue in this repository.


thavanle: Can we rename the directory "eventsettings" to "eventwebhook" instead please? I know it stutters a little bit for helpers/eventwebhook/EventWebhook.java, but I think it is ok in this context.

Patch:
diff --git a/examples/helpers/eventwebhook/Example.java b/examples/helpers/eventwebhook/Example.java
new file mode 100644
index 0000000..786d590
--- /dev/null
+++ b/examples/helpers/eventwebhook/Example.java
@@ -0,0 +1,27 @@
+import com.sendgrid.helpers.eventwebhook.EventWebhook;
+import java.security.PublicKey;
+import java.security.Security;
+import java.security.interfaces.ECPublicKey;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+import org.bouncycastle.jce.provider.BouncyCastleProvider;
+
+public class Example {
+
+    public static void main(String[] args) {
+        Security.addProvider(new BouncyCastleProvider());
+
+        try {
+            String publicKey = "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEEDr2LjtURuePQzplybdC+u4CwrqDqBaWjcMMsTbhdbcwHBcepxo7yAQGhHPTnlvFYPAZFceEu/1FwCM/QmGUhA==";
+            String payload = "{\"category\":\"example_payload\",\"event\":\"test_event\",\"message_id\":\"message_id\"}";
+            String signature = "MEUCIQCtIHJeH93Y+qpYeWrySphQgpNGNr/U+UyUlBkU6n7RAwIgJTz2C+8a8xonZGi6BpSzoQsbVRamr2nlxFDWYNH2j/0=";
+            String timestamp = "1588788367";
+            EventWebhook ew = new EventWebhook();
+            ECPublicKey ellipticCurvePublicKey = ew.ConvertPublicKeyToECDSA(publicKey);
+            boolean valid = ew.VerifySignature(ellipticCurvePublicKey, payload, signature, timestamp);
+            System.out.println("Valid Signature: " + valid);
+        } catch (Exception exception) {
+            Logger.getLogger(Example.class.getName()).log(Level.SEVERE, "something went wrong", exception);
+        }
+    }
+}
diff --git a/pom.xml b/pom.xml
index 3463b41..c7bb48e 100644
--- a/pom.xml
+++ b/pom.xml
@@ -303,5 +303,10 @@
       <version>2.1.0</version>
       <scope>test</scope>
     </dependency>
+   <dependency>
+     <groupId>org.bouncycastle</groupId>
+     <artifactId>bcprov-jdk15on</artifactId>
+     <version>1.65</version>
+   </dependency>
   </dependencies>
 </project>
\ No newline at end of file
diff --git a/src/main/java/com/sendgrid/helpers/eventwebhook/EventWebhook.java b/src/main/java/com/sendgrid/helpers/eventwebhook/EventWebhook.java
new file mode 100644
index 0000000..6e52bd3
--- /dev/null
+++ b/src/main/java/com/sendgrid/helpers/eventwebhook/EventWebhook.java
@@ -0,0 +1,68 @@
+package com.sendgrid.helpers.eventwebhook;
+
+import java.security.InvalidKeyException;
+import java.security.KeyFactory;
+import java.security.NoSuchAlgorithmException;
+import java.security.NoSuchProviderException;
+import java.security.Signature;
+import java.security.SignatureException;
+import java.security.interfaces.ECPublicKey;
+import java.security.spec.InvalidKeySpecException;
+import java.security.spec.X509EncodedKeySpec;
+import java.util.Base64;
+
+/**
+ * This class allows you to easily use the Event Webhook feature. Read the docs
+ * for more details:
+ * https://sendgrid.com/docs/for-developers/tracking-events/event.
+ */
+public class EventWebhook {
+    /**
+     * Convert the public key string to a ECPublicKey.
+     * 
+     * @param publicKey: verification key under Mail Settings
+     * @return a public key using the ECDSA algorithm
+     * @throws NoSuchAlgorithmException
+     * @throws NoSuchProviderException
+     * @throws InvalidKeySpecException
+     */
+    public java.security.interfaces.ECPublicKey ConvertPublicKeyToECDSA(String publicKey)
+            throws NoSuchAlgorithmException, NoSuchProviderException, InvalidKeySpecException {
+        byte[] publicKeyInBytes = Base64.getDecoder().decode(publicKey);
+        KeyFactory factory = KeyFactory.getInstance("ECDSA", "BC");
+        return (ECPublicKey) factory.generatePublic(new X509EncodedKeySpec(publicKeyInBytes));
+    }
+
+    /**
+     * Verify signed event webhook requests.
+     * 
+     * @param publicKey: elliptic curve public key
+     * @param payload:   event payload in the request body
+     * @param signature: value obtained from the
+     *                   'X-Twilio-Email-Event-Webhook-Signature' header
+     * @param timestamp: value obtained from the
+     *                   'X-Twilio-Email-Event-Webhook-Timestamp' header
+     * @return true or false if signature is valid
+     * @throws NoSuchAlgorithmException
+     * @throws NoSuchProviderException
+     * @throws InvalidKeyException
+     * @throws SignatureException
+     */
+    public boolean VerifySignature(ECPublicKey publicKey, String payload, String signature, String timestamp)
+            throws NoSuchAlgorithmException, NoSuchProviderException, InvalidKeyException, SignatureException {
+
+        // prepend the payload with the timestamp
+        String payloadWithTimestamp = timestamp + payload;
+
+        // create the signature object
+        Signature signatureObject = Signature.getInstance("SHA256withECDSA", "BC");
+        signatureObject.initVerify(publicKey);
+        signatureObject.update(payloadWithTimestamp.getBytes());
+
+        // decode the signature
+        byte[] signatureInBytes = Base64.getDecoder().decode(signature);
+
+        // verify the signature
+        return signatureObject.verify(signatureInBytes);
+    }
+}
diff --git a/src/main/java/com/sendgrid/helpers/eventwebhook/EventWebhookHeader.java b/src/main/java/com/sendgrid/helpers/eventwebhook/EventWebhookHeader.java
new file mode 100644
index 0000000..7bdc422
--- /dev/null
+++ b/src/main/java/com/sendgrid/helpers/eventwebhook/EventWebhookHeader.java
@@ -0,0 +1,20 @@
+package com.sendgrid.helpers.eventwebhook;
+
+/**
+ * This enum lists headers that get posted to the webhook. Read the docs for
+ * more details: https://sendgrid.com/docs/for-developers/tracking-events/event.
+ */
+public enum EventWebhookHeader {
+    SIGNATURE("X-Twilio-Email-Event-Webhook-Signature"), TIMESTAMP("X-Twilio-Email-Event-Webhook-Timestamp");
+
+    public final String name;
+
+    EventWebhookHeader(String name) {
+        this.name = name;
+    }
+
+    @Override
+    public String toString() {
+        return name;
+    }
+}


Test patch:
diff --git a/src/test/java/com/sendgrid/helpers/eventwebhook/EventWebhookTest.java b/src/test/java/com/sendgrid/helpers/eventwebhook/EventWebhookTest.java
new file mode 100644
index 0000000..434201b
--- /dev/null
+++ b/src/test/java/com/sendgrid/helpers/eventwebhook/EventWebhookTest.java
@@ -0,0 +1,27 @@
+package com.sendgrid.helpers.eventwebhook;
+
+import java.security.Security;
+import java.security.interfaces.ECPublicKey;
+import org.bouncycastle.jce.provider.BouncyCastleProvider;
+import org.junit.Assert;
+import org.junit.Test;
+
+public class EventWebhookTest {
+    @Test
+    public void testVerifySignature() throws Exception {
+
+        Security.addProvider(new BouncyCastleProvider());
+
+        String testPublicKey = "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEEDr2LjtURuePQzplybdC+u4CwrqDqBaWjcMMsTbhdbcwHBcepxo7yAQGhHPTnlvFYPAZFceEu/1FwCM/QmGUhA==";
+        String testPayload = "{\"category\":\"example_payload\",\"event\":\"test_event\",\"message_id\":\"message_id\"}";
+        String testSignature = "MEUCIQCtIHJeH93Y+qpYeWrySphQgpNGNr/U+UyUlBkU6n7RAwIgJTz2C+8a8xonZGi6BpSzoQsbVRamr2nlxFDWYNH2j/0=";
+        String testTimestamp = "1588788367";
+
+        EventWebhook ew = new EventWebhook();
+        ECPublicKey ellipticCurvePublicKey = ew.ConvertPublicKeyToECDSA(testPublicKey);
+        boolean isValidSignature = ew.VerifySignature(ellipticCurvePublicKey, testPayload, testSignature,
+                testTimestamp);
+
+        Assert.assertTrue(isValidSignature);
+    }
+}