Webhook Verification with HMAC Signatures

This page outlines Decentro's webhook callback verification capability with HMAC signatures

Ensuring the security and authenticity of webhook callbacks is crucial to safeguarding data integrity and preventing malicious interference. To achieve this, Decentro utilizes HMAC (Hash-based Message Authentication Code) signature generation, providing a robust mechanism for verification.

Why HMAC Signatures?

  1. Data Integrity - Guarantees that payloads remain unchanged during transmission.
  2. Authentication - Verifies that webhooks originate from our system.
  3. Replay Protection - Each payload includes a unique identifier callback_transaction_id

How it works?

  1. Signature Generation
    1. An HMAC-SHA256 signature is generated using your secret key and the webhook payload.
    2. This signature is included in the X-Signature header of each webhook request.
  2. Signature Verification
    1. The client verifies the HMAC signature using their secret key and the received payload.
  3. Replay protection
    1. Unique fields like callback_transaction_id prevent replay attacks

Sample Payload

{
  "decentro_transaction_id": "8258CC87775046D5872F83A80C033D4E",
  "payment_unique_reference_number": "MTMO_8382F172677D4E9AA5F2BE2195D99F64",
  "virtual_account_urn": "VA_AD2DB4E3977D46799C51C4958E6B0C9B",
  "currency_account_urn": "CAUSD_7E73F1E8E6ED4AC19655B2E6D21FD236",
  "created_timestamp": "2024-03-29T06:04:45.110062+00:00",
  "callback_transaction_id": "CALLB_ECC5032819694FAB843E5D45919DC678",
  "attempt": 1,
  "original_callback_transaction_id": "CALLB_ECC5032819694FAB843E5D45919DC678",
  "purpose": "PAYMENT: DECENTRO PTE. LTD. - DBS Bank Ltd",
  "beneficiary_code": "BEN_CODE_676DA523020E49AA83DEA7F6014030CA",
  "payment_currency": "USD",
  "source_currency": "USD",
  "payment_method": "SWIFT",
  "source_amount": "99.50",
  "payment_amount": "133.65",
  "transaction_status": "PENDING",
  "ledger_entry_type": "MAIN"
}

Verifying Signature

  1. Retrieve the X-Signature header from the webhook request.
  2. Use your secret key and the payload to generate an HMAC-SHA256 signature.
  3. Compare your generated signature to the one in the X-Signature header.

Code Examples

Python

import hmac
import hashlib
import base64

def verify_signature(secret_key, payload, received_signature):
    """
    Verify the HMAC-SHA256 signature of a webhook payload.

    Parameters:
        secret_key (str): Your secret key.
        payload (str): The raw webhook payload as a string.
        received_signature (str): The signature received in the `X-Signature` header.

    Returns:
        bool: True if the signature is valid, False otherwise.
    """
    hmac_obj = hmac.new(secret_key.encode(), payload.encode(), hashlib.sha256)
    calculated_signature = base64.b64encode(hmac_obj.digest()).decode()
    return hmac.compare_digest(calculated_signature, received_signature)

# Example Usage
secret_key = "your_secret_key"
payload = "{\"decentro_transaction_id\": \"8258CC87775046D5872F83A80C033D4E\", ...}"  # Truncated payload
received_signature = "Base64EncodedSignatureHere"

if verify_signature(secret_key, payload, received_signature):
    print("Signature is valid. Proceed with processing.")
else:
    print("Invalid signature. Discard the webhook.")

Javascript

const crypto = require('crypto');

function verifySignature(secretKey, payload, receivedSignature) {
    const hmac = crypto.createHmac('sha256', secretKey);
    hmac.update(payload);
    const calculatedSignature = hmac.digest('base64');
    return calculatedSignature === receivedSignature;
}

// Example Usage
const secretKey = "your_secret_key";
const payload = '{"decentro_transaction_id": "8258CC87775046D5872F83A80C033D4E", ...}';
const receivedSignature = "Base64EncodedSignatureHere";

if (verifySignature(secretKey, payload, receivedSignature)) {
    console.log("Signature is valid. Proceed with processing.");
} else {
    console.log("Invalid signature. Discard the webhook.");
}

PHP

<?php
function verify_signature($secret_key, $payload, $received_signature) {
    $calculated_signature = base64_encode(hash_hmac('sha256', $payload, $secret_key, true));
    return hash_equals($calculated_signature, $received_signature);
}

$secret_key = "your_secret_key";
$payload = '{"decentro_transaction_id": "8258CC87775046D5872F83A80C033D4E", ...}';
$received_signature = "Base64EncodedSignatureHere";

if (verify_signature($secret_key, $payload, $received_signature)) {
    echo "Signature is valid. Proceed with processing.";
} else {
    echo "Invalid signature. Discard the webhook.";
}
?>

Java

import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.util.Base64;

public class WebhookVerifier {
    public static boolean verifySignature(String secretKey, String payload, String receivedSignature) throws Exception {
        Mac hmac = Mac.getInstance("HmacSHA256");
        SecretKeySpec secretKeySpec = new SecretKeySpec(secretKey.getBytes(), "HmacSHA256");
        hmac.init(secretKeySpec);
        byte[] calculatedSignature = hmac.doFinal(payload.getBytes());
        String encodedSignature = Base64.getEncoder().encodeToString(calculatedSignature);
        return encodedSignature.equals(receivedSignature);
    }
}

Ruby

require 'openssl'
require 'base64'

def verify_signature(secret_key, payload, received_signature)
  calculated_signature = Base64.encode64(OpenSSL::HMAC.digest('sha256', secret_key, payload)).strip
  calculated_signature == received_signature
end

secret_key = "your_secret_key"
payload = '{"decentro_transaction_id": "8258CC87775046D5872F83A80C033D4E", ...}'
received_signature = "Base64EncodedSignatureHere"

if verify_signature(secret_key, payload, received_signature)
  puts "Signature is valid. Proceed with processing."
else
  puts "Invalid signature. Discard the webhook."
end

Go

package main

import (
	"crypto/hmac"
	"crypto/sha256"
	"encoding/base64"
	"fmt"
)

func verifySignature(secretKey, payload, receivedSignature string) bool {
	hmacObj := hmac.New(sha256.New, []byte(secretKey))
	hmacObj.Write([]byte(payload))
	calculatedSignature := base64.StdEncoding.EncodeToString(hmacObj.Sum(nil))

	return hmac.Equal([]byte(calculatedSignature), []byte(receivedSignature))
}

func main() {
	secretKey := "your_secret_key"
	payload := "{\"decentro_transaction_id\": \"8258CC87775046D5872F83A80C033D4E\", ...}"
	receivedSignature := "Base64EncodedSignatureHere"

	if verifySignature(secretKey, payload, receivedSignature) {
		fmt.Println("Signature is valid. Proceed with processing.")
	} else {
		fmt.Println("Invalid signature. Discard the webhook.")
	}
}

Rust

use hmac::{Hmac, Mac, NewMac};
use sha2::Sha256;
use base64;

type HmacSha256 = Hmac<Sha256>;

fn verify_signature(secret_key: &str, payload: &str, received_signature: &str) -> bool {
    let mut mac = HmacSha256::new_from_slice(secret_key.as_bytes()).expect("HMAC can take key of any size");
    mac.update(payload.as_bytes());
    let calculated_signature = base64::encode(mac.finalize().into_bytes());

    calculated_signature == received_signature
}

fn main() {
    let secret_key = "your_secret_key";
    let payload = "{\"decentro_transaction_id\": \"8258CC87775046D5872F83A80C033D4E\", ...}";
    let received_signature = "Base64EncodedSignatureHere";

    if verify_signature(secret_key, payload, received_signature) {
        println!("Signature is valid. Proceed with processing.");
    } else {
        println!("Invalid signature. Discard the webhook.");
    }
}

Best Practices

  1. Secure Your Secret Key - Store it securely and rotate it periodically with our technical team.
  2. Validate Payloads - Ensure the payload matches the expected formats.
  3. Reject Stale Requests - Use callback_transaction_id to reject outdated payloads.
  4. Use HTTPS - All webhook requests should be sent over HTTPS for added security.

By following this guide, you can ensure that only secure and verified webhook events are processed, minimizing risks and enhancing trust.