Jump to content

Webhook Signatures

Verify the webhook signature

Verify the events that Treddy sends to your webhook endpoints.

Treddy will sign the webhook events it sends to your endpoints by including a signature in each event’s Treddy-Signature header. This allows you to verify that the events were sent by Treddy and not by a third party.

Treddy generates a unique secret key for each endpoint so you must obtain a secret for each one you want to verify signatures on. In order to verify a signature, you first need to retrieve your endpoint’s secret from the Developer portal’s Webhook settings.

Verifying signatures

The Treddy-Signature header included in each signed event contains a timestamp and a signature. The timestamp is prefixed by t=, and each signature is prefixed by s=.

Treddy-Signature:
t=1671780963342,
s=43aaedf4b6ff94856d76093067512f3ad48186346a60e7b0dea33badeba35430

Treddy generates a signature using a hash-based message authentication code (HMAC) with SHA-256.

Step 1: Extract the timestamp and signatures from the header

Split the header, using the , character as the separator, to get a list of elements. Then split each element, using the = character as the separator, to get a prefix and value pair.

The value for the prefix t corresponds to the timestamp, and s corresponds to the signature.

Step 2: Prepare the signed_payload string

The signed_payload string is created by concatenating:

The timestamp (as a string) The character . The actual JSON payload (that is, the request body) Step 3: Determine the expected signature Compute an HMAC with the SHA256 hash function. Use the endpoint’s signing secret as the key, and use the signed_payload string as the message.

Step 4: Compare the signatures

Compare the signature (or signatures) in the header to the expected signature. For an equality match, compute the difference between the current timestamp and the received timestamp, then decide if the difference is within your tolerance.

To protect against timing attacks, use a constant-time string comparison to compare the expected signature to each of the received signatures.

@Path("/webhook")
public class WebhookResource {

    @POST
    @Produces(MediaType.APPLICATION_JSON)
    public Response handleEvent(String payload, @HeaderParam("Treddy-Signature") String signature) {

        if (signature == null || !Signature.verifyHeader(payload, signature)) {
            return Response.status(400).build();
        }

        return Response.ok().build();
    }
}

class Signature {
    static String secret = "";

    public static boolean verifyHeader(String payload, String header) {
        long timestamp = getTimestamp(header);
        String signature = getSignature(header);

        long tolerance = 1000;

        if (timestamp <= 0) {
            return false;
        }

        if (signature == null) {
            return false;
        }

        String signedPayload = String.format("%d.%s", timestamp, payload);
        String expectedSignature;
        try {
            expectedSignature = Util.computeHmacSha256(signedPayload, secret);
        } catch (NoSuchAlgorithmException | InvalidKeyException e) {
            return false;
        }

        // Compare signatures
        boolean isSignaturesEqual = Util.secureCompare(expectedSignature, signature);

        if (!isSignaturesEqual) {
            return false;
        }

        // Check tolerance
        if ((tolerance > 0) && (timestamp < (System.currentTimeMillis() - tolerance))) {
            return false;
        }

        return true;
    }

    private static long getTimestamp(String signature) {
        String[] items = signature.split(",", -1);

        for (String item : items) {
            String[] itemParts = item.split("=", 2);
            if (itemParts[0].equals("t")) {
                return Long.parseLong(itemParts[1]);
            }
        }

        return -1;
    }

    private static String getSignature(String signature) {
        String[] items = signature.split(",", -1);

        for (String item : items) {
            String[] itemParts = item.split("=", 2);
            if (itemParts[0].equals("s")) {
                return itemParts[1];
            }
        }

        return null;
    }
}

class Util {

    public static String computeHmacSha256(String data, String key) throws NoSuchAlgorithmException, InvalidKeyException {
        Mac hasher = Mac.getInstance("HmacSHA256");
        hasher.init(new SecretKeySpec(key.getBytes(StandardCharsets.UTF_8), "HmacSHA256"));
        byte[] hash = hasher.doFinal(data.getBytes(StandardCharsets.UTF_8));
        String result = "";
        for (byte b : hash) {
            result += Integer.toString((b & 0xff) + 0x100, 16).substring(1);
        }
        return result;
    }

    public static boolean secureCompare(String a, String b) {
        byte[] digesta = a.getBytes(StandardCharsets.UTF_8);
        byte[] digestb = b.getBytes(StandardCharsets.UTF_8);

        return MessageDigest.isEqual(digesta, digestb);
    }
}