diff --git a/examples/helpers/eventwebhook/Example.java b/examples/helpers/eventwebhook/Example.java index 786d5902..6077736a 100644 --- a/examples/helpers/eventwebhook/Example.java +++ b/examples/helpers/eventwebhook/Example.java @@ -1,27 +1,49 @@ import com.sendgrid.helpers.eventwebhook.EventWebhook; -import java.security.PublicKey; +import com.sendgrid.helpers.eventwebhook.EventWebhookHeader; +import com.twilio.security.RequestValidator; +import com.twilio.twiml.MessagingResponse; +import com.twilio.twiml.messaging.Body; +import com.twilio.twiml.messaging.Message; +import org.bouncycastle.jce.provider.BouncyCastleProvider; +import spark.Route; + 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; +import java.util.HashMap; +import java.util.Map; -public class Example { +import static spark.Spark.post; +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); - } + final Route webhookHandler = (req, res) -> { + try { + final String publicKey = "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE83T4O/n84iotIvIW4mdBgQ/7dAfSmpqIM8kF9mN1flpVKS3GRqe62gw+2fNNRaINXvVpiglSI8eNEc6wEA3F+g=="; + + final String signature = req.headers(EventWebhookHeader.SIGNATURE.toString()); + final String timestamp = req.headers(EventWebhookHeader.TIMESTAMP.toString()); + final byte[] requestBody = req.bodyAsBytes(); + + final EventWebhook ew = new EventWebhook(); + final ECPublicKey ellipticCurvePublicKey = ew.ConvertPublicKeyToECDSA(publicKey); + final boolean valid = ew.VerifySignature(ellipticCurvePublicKey, requestBody, signature, timestamp); + System.out.println("Valid Signature: " + valid); + + if (valid) { + res.status(204); + } else { + res.status(403); + } + + return null; + } catch (final Exception exception) { + res.status(500); + return exception.toString(); + } + }; + + post("/sendgrid/webhook", webhookHandler); } } diff --git a/src/main/java/com/sendgrid/helpers/eventwebhook/EventWebhook.java b/src/main/java/com/sendgrid/helpers/eventwebhook/EventWebhook.java index 6e52bd38..0898133b 100644 --- a/src/main/java/com/sendgrid/helpers/eventwebhook/EventWebhook.java +++ b/src/main/java/com/sendgrid/helpers/eventwebhook/EventWebhook.java @@ -1,11 +1,8 @@ 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.io.ByteArrayOutputStream; +import java.io.IOException; +import java.security.*; import java.security.interfaces.ECPublicKey; import java.security.spec.InvalidKeySpecException; import java.security.spec.X509EncodedKeySpec; @@ -19,7 +16,7 @@ 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 @@ -27,17 +24,18 @@ public class EventWebhook { * @throws InvalidKeySpecException */ public java.security.interfaces.ECPublicKey ConvertPublicKeyToECDSA(String publicKey) - throws NoSuchAlgorithmException, NoSuchProviderException, InvalidKeySpecException { + 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 payload: event payload string in the request body * @param signature: value obtained from the * 'X-Twilio-Email-Event-Webhook-Signature' header * @param timestamp: value obtained from the @@ -47,20 +45,44 @@ public java.security.interfaces.ECPublicKey ConvertPublicKeyToECDSA(String publi * @throws NoSuchProviderException * @throws InvalidKeyException * @throws SignatureException + * @throws IOException */ public boolean VerifySignature(ECPublicKey publicKey, String payload, String signature, String timestamp) - throws NoSuchAlgorithmException, NoSuchProviderException, InvalidKeyException, SignatureException { + throws NoSuchAlgorithmException, NoSuchProviderException, InvalidKeyException, SignatureException, IOException { + return VerifySignature(publicKey, payload.getBytes(), signature, timestamp); + } + + /** + * Verify signed event webhook requests. + * + * @param publicKey: elliptic curve public key + * @param payload: event payload bytes 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 + * @throws IOException + */ + public boolean VerifySignature(ECPublicKey publicKey, byte[] payload, String signature, String timestamp) + throws NoSuchAlgorithmException, NoSuchProviderException, InvalidKeyException, SignatureException, IOException { // prepend the payload with the timestamp - String payloadWithTimestamp = timestamp + payload; + final ByteArrayOutputStream payloadWithTimestamp = new ByteArrayOutputStream(); + payloadWithTimestamp.write(timestamp.getBytes()); + payloadWithTimestamp.write(payload); // create the signature object - Signature signatureObject = Signature.getInstance("SHA256withECDSA", "BC"); + final Signature signatureObject = Signature.getInstance("SHA256withECDSA", "BC"); signatureObject.initVerify(publicKey); - signatureObject.update(payloadWithTimestamp.getBytes()); + signatureObject.update(payloadWithTimestamp.toByteArray()); // decode the signature - byte[] signatureInBytes = Base64.getDecoder().decode(signature); + final byte[] signatureInBytes = Base64.getDecoder().decode(signature); // verify the signature return signatureObject.verify(signatureInBytes); diff --git a/src/test/java/com/sendgrid/helpers/eventwebhook/EventWebhookTest.java b/src/test/java/com/sendgrid/helpers/eventwebhook/EventWebhookTest.java index 434201bb..d73cedbc 100644 --- a/src/test/java/com/sendgrid/helpers/eventwebhook/EventWebhookTest.java +++ b/src/test/java/com/sendgrid/helpers/eventwebhook/EventWebhookTest.java @@ -1,27 +1,124 @@ package com.sendgrid.helpers.eventwebhook; -import java.security.Security; -import java.security.interfaces.ECPublicKey; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; import org.bouncycastle.jce.provider.BouncyCastleProvider; import org.junit.Assert; +import org.junit.BeforeClass; import org.junit.Test; +import java.security.Security; +import java.security.interfaces.ECPublicKey; +import java.util.Collections; +import java.util.List; + public class EventWebhookTest { + private static class Event { + public Event(final String email, + final String event, + final String reason, + final String sgEventId, + final String sgMessageId, + final String smtpId, + final long timestamp) { + this.email = email; + this.event = event; + this.reason = reason; + this.sgEventId = sgEventId; + this.sgMessageId = sgMessageId; + this.smtpId = smtpId; + this.timestamp = timestamp; + } + + public String email; + public String event; + public String reason; + @JsonProperty("sg_event_id") + public String sgEventId; + @JsonProperty("sg_message_id") + public String sgMessageId; + @JsonProperty("smtp-id") + public String smtpId; + @JsonProperty("timestamp") + public long timestamp; + } + + private static final String PUBLIC_KEY = "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE83T4O/n84iotIvIW4mdBgQ/7dAfSmpqIM8kF9mN1flpVKS3GRqe62gw+2fNNRaINXvVpiglSI8eNEc6wEA3F+g=="; + private static final String SIGNATURE = "MEUCIGHQVtGj+Y3LkG9fLcxf3qfI10QysgDWmMOVmxG0u6ZUAiEAyBiXDWzM+uOe5W0JuG+luQAbPIqHh89M15TluLtEZtM="; + private static final String TIMESTAMP = "1600112502"; + private static final List EVENTS = Collections.singletonList(new Event( + "hello@world.com", + "dropped", + "Bounced Address", + "ZHJvcC0xMDk5NDkxOS1MUnpYbF9OSFN0T0doUTRrb2ZTbV9BLTA", + "LRzXl_NHStOGhQ4kofSm_A.filterdrecv-p3mdw1-756b745b58-kmzbl-18-5F5FC76C-9.0", + "", + 1600112492)); + + private static String PAYLOAD; + + @BeforeClass + public static void setUp() throws JsonProcessingException { + Security.addProvider(new BouncyCastleProvider()); + + // Be sure to include the trailing carriage return and newline! + PAYLOAD = new ObjectMapper().writeValueAsString(EVENTS) + "\r\n"; + } + @Test public void testVerifySignature() throws Exception { + Assert.assertTrue(verify( + PUBLIC_KEY, + PAYLOAD, + SIGNATURE, + TIMESTAMP + )); + } - Security.addProvider(new BouncyCastleProvider()); + @Test + public void testBadKey() throws Exception { + Assert.assertFalse(verify( + "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEqTxd43gyp8IOEto2LdIfjRQrIbsd4SXZkLW6jDutdhXSJCWHw8REntlo7aNDthvj+y7GjUuFDb/R1NGe1OPzpA==", + PAYLOAD, + SIGNATURE, + TIMESTAMP + )); + } + + @Test + public void testBadPayload() throws Exception { + Assert.assertFalse(verify( + PUBLIC_KEY, + "payload", + SIGNATURE, + TIMESTAMP + )); + } - 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"; + @Test + public void testBadSignature() throws Exception { + Assert.assertFalse(verify( + PUBLIC_KEY, + PAYLOAD, + "MEUCIQCtIHJeH93Y+qpYeWrySphQgpNGNr/U+UyUlBkU6n7RAwIgJTz2C+8a8xonZGi6BpSzoQsbVRamr2nlxFDWYNH3j/0=", + TIMESTAMP + )); + } - EventWebhook ew = new EventWebhook(); - ECPublicKey ellipticCurvePublicKey = ew.ConvertPublicKeyToECDSA(testPublicKey); - boolean isValidSignature = ew.VerifySignature(ellipticCurvePublicKey, testPayload, testSignature, - testTimestamp); + @Test + public void testBadTimestamp() throws Exception { + Assert.assertFalse(verify( + PUBLIC_KEY, + PAYLOAD, + SIGNATURE, + "timestamp" + )); + } - Assert.assertTrue(isValidSignature); + private boolean verify(final String publicKey, final String payload, final String signature, final String timestamp) throws Exception { + final EventWebhook ew = new EventWebhook(); + final ECPublicKey ellipticCurvePublicKey = ew.ConvertPublicKeyToECDSA(publicKey); + return ew.VerifySignature(ellipticCurvePublicKey, payload, signature, timestamp); } }