Skip to content

Commit

Permalink
fix: update the eventwebhook sample data, example, tests, and byte ha…
Browse files Browse the repository at this point in the history
…ndling (#649)
  • Loading branch information
childish-sambino authored Sep 17, 2020
1 parent ad48e14 commit 0993731
Show file tree
Hide file tree
Showing 3 changed files with 185 additions and 44 deletions.
56 changes: 39 additions & 17 deletions examples/helpers/eventwebhook/Example.java
Original file line number Diff line number Diff line change
@@ -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);
}
}
52 changes: 37 additions & 15 deletions src/main/java/com/sendgrid/helpers/eventwebhook/EventWebhook.java
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -19,25 +16,26 @@
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 {
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
Expand All @@ -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);
Expand Down
121 changes: 109 additions & 12 deletions src/test/java/com/sendgrid/helpers/eventwebhook/EventWebhookTest.java
Original file line number Diff line number Diff line change
@@ -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<Event> EVENTS = Collections.singletonList(new Event(
"hello@world.com",
"dropped",
"Bounced Address",
"ZHJvcC0xMDk5NDkxOS1MUnpYbF9OSFN0T0doUTRrb2ZTbV9BLTA",
"LRzXl_NHStOGhQ4kofSm_A.filterdrecv-p3mdw1-756b745b58-kmzbl-18-5F5FC76C-9.0",
"<LRzXl_NHStOGhQ4kofSm_A@ismtpd0039p1iad1.sendgrid.net>",
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);
}
}

0 comments on commit 0993731

Please sign in to comment.