Webhook Signature Verification

Signature Verification can be used to authenticate requests sent to your endpoint. At a high level, each request contains headers that you can use to validate that the request came from our system.

The following headers can be used to authenticate the request:

Our implementation uses digital signatures with the RSA with SHA algorithm; specifically SHA256 which for our purposes reduces the size of the signature without compromising its security.

The process for authenticating a request involves two parties - We (flexEngage) and You (Merchants with configured webhook endpoints) - and is explained in simple terms as follows:

  1. We send HTTP requests to your configured Webhook Endpoint. The following elements from the request can be used to authenticate we sent you the request.
    • Request Body - this is a body of the HTTP reqeust.
    • x-fr-wh-authorization (Request Header) - an HTTP Request Header that contains the digital signature.
    • x-fr-wh-pk (Request Header) - an HTTP Request Header that contains location (HTTPS URI) of the Public Key.
  2. You retrieve the Public Key contents from the Public Key location provided as an HTTP Request Header (x-fr-wh-pk). You should also validate the TLS certificate used in the https connection and verify that it was issued to one these domains (NOTE: Do not try to reuse this key with subsequent requests since it is not guaranteed that the same pair was used):
    • assets.webhooks.flexengage.com - This domain hosts Public Keys from our Production system.
    • assets.webhooks.flexengage-test.com - This domain hosts Public Keys from our Test system.
  3. You hash the Request Body using the SHA256 algorithim. Please consider the following in this step:
    • When decoding the Request Body to a string, do so using the UTF-8 charset.
    • Make sure no deserialization happens at this level since that might change the hash.
  4. You decode the Base64 signature in the header of the request (x-fr-wh-authorization).
  5. You verify the decoded signature matches the payload you previously hashed using the provided public key.

Sample Code

You can find sample code to guide you on how to verify the request signature below. Additionally, we have a public git repository with full examples on how to do the signature verification, including tests and dependencies.

Java sample code

import org.bouncycastle.util.io.pem.PemObject;
import org.bouncycastle.util.io.pem.PemReader;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.StringReader;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.security.InvalidKeyException;
import java.security.KeyFactory;
import java.security.NoSuchAlgorithmException;
import java.security.PublicKey;
import java.security.Signature;
import java.security.SignatureException;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.X509EncodedKeySpec;
import java.util.Base64;

public class SignatureVerifier {
    public static boolean verify(
            String payload,
            String signature,
            String pubKey,
            String algorithm)
            throws IOException,
            NoSuchAlgorithmException,
            InvalidKeySpecException,
            InvalidKeyException,
            SignatureException {

        PublicKey publicKey = getPublicKeyFromString(pubKey);

        Signature publicSignature = Signature.getInstance(algorithm);
        publicSignature.initVerify(publicKey);
        publicSignature.update(payload.getBytes(StandardCharsets.UTF_8));
        byte[] signatureBytes = Base64.getDecoder().decode(signature);

        return publicSignature.verify(signatureBytes);
    }

    private static PublicKey getPublicKeyFromString(String keyFileContents)
            throws IOException, NoSuchAlgorithmException, InvalidKeySpecException {
        StringReader reader = new StringReader(keyFileContents);
        PemReader pemReader = new PemReader(reader);
        PemObject pemObject = pemReader.readPemObject();
        byte[] keyContentAsBytes = pemObject.getContent();
        X509EncodedKeySpec spec = new X509EncodedKeySpec(keyContentAsBytes);
        KeyFactory kf = KeyFactory.getInstance("RSA");
        return kf.generatePublic(spec);
    }
}

Python sample code

This requires installation of the PyCryptodome package.
PyCryptodome installation instructions

import base64

from Crypto.Hash import SHA256
from Crypto.PublicKey import RSA
from Crypto.Signature import pkcs1_15


def verify(payload, signature, pub_key):
    msg = payload.encode()
    hashed_payload = SHA256.new(msg)
    public_key = RSA.import_key(pub_key)
    signer = pkcs1_15.new(public_key)
    try:
        signer.verify(hashed_payload, base64.b64decode(signature))
        return True
    except (ValueError, TypeError):
        return False