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?
- Data Integrity - Guarantees that payloads remain unchanged during transmission.
- Authentication - Verifies that webhooks originate from our system.
- Replay Protection - Each payload includes a unique identifier callback_transaction_id
How it works?
- Signature Generation
- An HMAC-SHA256 signature is generated using your secret key and the webhook payload.
- This signature is included in the X-Signature header of each webhook request.
- Signature Verification
- The client verifies the HMAC signature using their secret key and the received payload.
- Replay protection
- 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
- Retrieve the X-Signature header from the webhook request.
- Use your secret key and the payload to generate an HMAC-SHA256 signature.
- 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
- Secure Your Secret Key - Store it securely and rotate it periodically with our technical team.
- Validate Payloads - Ensure the payload matches the expected formats.
- Reject Stale Requests - Use callback_transaction_id to reject outdated payloads.
- 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.
Updated 2 months ago