Notification Validation

Webhook notifications are sent with a digital signature that you can use to validate the integrity of the notification and prevent replay attacks. You must first generate a digital signature key. See the Requesting a Digital Signature Key topic for the applicable Webhooks subscription workflow.

Digital Signature Format

The
v-c-signature
header uses this format:
v-c-signature: t=1617830804768;keyId=bf44c857-b182-bb05-e053-34b8d30a7a72;sig=CzHY47nzJgCSD/BREtSIb+9l/vfkaaL4qf9n8MNJ4CY=";
The
v-c-signature
header contains three parts, each separated by a semicolon:
  • t
    is the timestamp at the moment the signature key was created.
  • keyId
    is the value of the
    keyInformation.keyId
    field from the API response to the digital signature key request.
  • sig
    contains the signature, encrypted using HMAC-SHA256. For instructions for generating the signature, see Validating a Notification.
Notification Validation

Validating a Notification

To validate a notification, you must use the digital signature key to generate your own signature and match it with the signature in the notification. The digital signature of the notification is contained in the
sig
parameter of the
v-c-signature
header of the notification.
When you sent the API request that created the digital signature key, you received a response that contains a
keyInformation
array. The
keyInformation
array contains a
keyinformation.key
field which contains the digital signature key and a
keyinformation.keyId
field that identifies the digital signature key. The
keyinformation.key
field is required to generate your own signature, which you can use to validate the notification's signature.
Follow these steps to validate the integrity of a notification.
  1. Split the signature by semicolon and extract
    t
    ,
    keyId
    , and
    sig
    .
  2. Use
    keyId
    to fetch the digital signature key.
  3. Generate the payload by concatenating the timestamp with a period character (.) and the payload from the body of the notification.
  4. Use the SHA256 algorithm to encrypt the generated payload from Step 3 using the key from Step 2.
  5. Verify that the encrypted value matches the value in
    sig
    .
Notification Validation

Example: Validating a Notification

import javax.crypto.Mac; import javax.crypto.spec.SecretKeySpec; import java.nio.charset.StandardCharsets; import java.util.Arrays; import java.util.Base64; public class Validator { public static void main(String[] args) { // Sample signature header String signatureHeader = "v-c-signature: t=1617830804768;keyId=bf44c857-b182-bb05-e053-34b8d30a7a72; sig = CzHY47nzJgCSD / BREtSIb + 9 l / vfkaaL4qf9n8MNJ4CY = "; String payload = "this is a decrypted payload"; // Convert the received signatureHeader into timestamp, keyId, and signature. DigitalSignature companySignature = new DigitalSignature(signatureHeader); // Check if the timestamp is within tolerance. if (companySignature.isValidTimestamp()) { // Client regenerates their signature using the timestamp from header and received payload. byte[] signature = regenerateSignature(companySignature.getTimestamp(), payload); // Check if the generated signature is same as signature received in header. if (isValidSignature(signature, companySignature.getSignature())) { System.out.println("Success - Signature is valid"); } else { System.out.println("Error - Signatures do not match"); } } else { System.out.println("Error - timestamp is outside of tolerance level"); } } /** * Compute HMAC with the SHA256 hash function. * key is your private key. * message is timestamp.payload. * @return */ public static byte[] regenerateSignature(long timestamp, String message) { String timestampedMessage = timestamp + "." + message; String key = getSecurityKey(); // Generate the hash using key and message. return calcHmacSHA256(Base64.getDecoder().decode(key), timestampedMessage.getBytes(StandardCharsets.UTF_8)); } /** * A mechanism to fetch the security key using keyId from a source. We're using Base64encoded version of (test_key). * @return */ private static String getSecurityKey() { return "dGVzdF9rZXk="; //test_key } /** * Generate SHA256 using secretKey and a message. * Sample Hmacgenerator to test: https://8gwifi.org/hmacgen.jsp * @param secretKey * @param message * @return */ private static byte[] calcHmacSHA256(byte[] secretKey, byte[] message) { byte[] hmacSha256 = null; try { Mac mac = Mac.getInstance("HmacSHA256"); SecretKeySpec secretKeySpec = new SecretKeySpec(secretKey, "HmacSHA256"); mac.init(secretKeySpec); hmacSha256 = mac.doFinal(message); } catch (Exception e) { throw new RuntimeException("Failed to calculate hmac-sha256", e); } return hmacSha256; } /** * Compare the Base64 decoding of the signature with the signature received in the header. * Sample encoder/decoder to test: * https://www.base64encode.org/ * https://www.base64decode.org/ * @param bankSignature * @param companySignature * @return */ private static boolean isValidSignature(byte[] bankSignature, String companySignature) { return Arrays.equals(bankSignature, Base64.getDecoder().decode(companySignature)); } } import java.time.Clock; public class DigitalSignature { private long timestamp; private String keyId; private String signature; public DigitalSignature(String digitalSignature) { try { // Split the header by space. The first part is the key "v-c-signature". The second part is the actual signature. String signature = digitalSignature.split(" ")[1]; // Separate the actual signature by semicolon. This creates 3 parts (timestamp, keyId, sig). String[] signatureParts = signature.split(";"); // The timestamp section is the first block. Split the timestamp section by = sign and actual timestamp is in the second block. this.timestamp = Long.parseLong(signatureParts[0].split("=")[1]); // The keyId section is the second block. Split the keyId section by = sign and actual keyId is in the second block this.keyId = signatureParts[1].split("=")[1]; // The digital signature is the third block. Split digital signature by = sign and actual signature is in the second block. This is Base64 encoded. this.signature = signatureParts[2].split("=")[1]; } catch (Exception e) { System.out.println("Invalid digital signature format"); } } public long getTimestamp() { return timestamp; } public void setTimestamp(long timestamp) { this.timestamp = timestamp; } public String getKeyId() { return keyId; } public void setKeyId(String keyId) { this.keyId = keyId; } public String getSignature() { return signature; } public void setSignature(String signature) { this.signature = signature; } /** * Using a tolerance of 60 mins * Compute the current time in UTC and make sure the timestamp was generated within the tolerance period. * UTC millis generator to test: https://currentmillis.com/ * * @return */ public boolean isValidTimestamp() { long tolerance = 60 * 60 * 1000 L; //60 mins // return Clock.systemUTC().millis() - timestamp < tolerance; // Enable this if you want timestamp validation. return true; } }
Notification Validation