Webhook Signature Validation
Once you generate the Webhook Secret Key from the Settings page of the AML Watcher App, every HTTP webhook response will include an X-Signature key in the headers.
Steps to Validate the Webhook Signature
-
Extract the Required Information
- Get the raw response payload from the webhook response.
- Retrieve the signature value from the
X-Signature
header.
-
Re-serialize the Payload
- Parse the raw JSON payload into an object.
- Re-serialize the object into a JSON string with the following considerations:
- Sort Keys: Ensure the keys in the JSON object are sorted alphabetically.
- Compact Format: Remove unnecessary spaces between commas and colons.
-
Generate a Computed Signature
- Use your Webhook Secret Key along with the raw response payload.
- Compute the HMAC-SHA256 hash of the re-serialized payload using your secret key.
-
Compare the Signatures
- Compare your
Computed Signature
with the signature value provided in theX-Signature
header. - Use a safe comparison method to prevent security risks.
- Compare your
-
Verify the Result
- If the two signatures match, the webhook response is authentic and can be trusted.
- If the signatures do not match, reject the response and take appropriate security measures.
Important Notes
- Always use a secure hash function (HMAC-SHA256).
- Ensure the raw response payload is unaltered before computing the signature.
- Javascript
- PHP
- Python
- Ruby
- Go
const crypto = require("crypto");
// Signature verification function
function verifySignature(req) {
const webhookSecretKey = "YOUR_SECRET_KEY";
const receivedSignature = req.headers["x-signature"];
if (!receivedSignature) {
return false;
}
// Sort the object
const sortedObj = sortObjectKeys(req.body);
// Re-serialize the request body in a consistent format
const reSerializedData = JSON.stringify(sortedObj);
// Compute the HMAC-SHA256 signature using the secret key and re-serialized payload
const computedSignature = crypto
.createHmac("sha256", webhookSecretKey)
.update(reSerializedData)
.digest("hex");
// Securely compare the signatures using timing-safe comparison
return crypto.timingSafeEqual(
Buffer.from(receivedSignature, "utf8"),
Buffer.from(computedSignature, "utf8")
);
}
// Function to recursively sort object keys
function sortObjectKeys(obj) {
if (Array.isArray(obj)) {
return obj.map((item) =>
typeof item === "object" ? sortObjectKeys(item) : item
);
} else if (obj !== null && typeof obj === "object") {
return Object.keys(obj)
.sort()
.reduce((acc, key) => {
acc[key] = sortObjectKeys(obj[key]); // Recursively sort nested objects
return acc;
}, {});
}
return obj;
}
<?php
// Function to verify the signature
function verifySignature($requestBody) {
$webhookSecretKey = "YOUR_SECRET_KEY";
$receivedSignature = $_SERVER['HTTP_X_SIGNATURE'] ?? null;
if (!$receivedSignature) {
return false;
}
// Sort the object
$sortedData = recursiveSort($requestBody);
// Re-serialize the request body into a JSON string
$reSerializedData = json_encode($sortedData, JSON_UNESCAPED_SLASHES);
// Compute the HMAC-SHA256 signature
$computedSignature = hash_hmac('sha256', $reSerializedData, $webhookSecretKey);
// Compare the received signature with the computed one using a constant-time comparison
return hash_equals($receivedSignature, $computedSignature);
}
// Function to recursively sort the array
function recursiveSort($array) {
if (is_array($array)) {
ksort($array); // Sort keys in ascending order
// Recursively sort nested arrays
foreach ($array as &$value) {
$value = recursiveSort($value);
}
}
return $array;
}
import json
import hmac
import hashlib
def verify_signature(request):
webhook_secret_key = "YOUR_SECRET_KEY"
# Extract the `X-Signature` header (the HMAC-SHA256 signature sent by the sender)
received_signature = request.headers.get("X-Signature")
if not received_signature:
return False
# Get the raw request body and parse it into a dictionary
raw_request_data = request.get_data(as_text=True)
try:
parsed_request_data = json.loads(raw_request_data)
except json.JSONDecodeError:
return False
# Re-serialize the parsed data with consistent formatting
re_serialized_data = json.dumps(
parsed_request_data,
separators=(",", ":"), # Compact format: no spaces
sort_keys=True # Ensure the keys are sorted consistently
)
# Compute the HMAC-SHA256 signature using the secret key and the re-serialized payload
computed_signature = hmac.new(
webhook_secret_key.encode('utf-8'),
re_serialized_data.encode('utf-8'),
hashlib.sha256
).hexdigest()
# Use `hmac.compare_digest` for a secure comparison to mitigate timing attacks
return hmac.compare_digest(received_signature, computed_signature)
require 'json'
require 'openssl'
require 'rack'
# Function to verify the signature
def verify_signature()
webhook_secret_key = 'YOUR_SECRET_KEY'
received_signature = request.env['HTTP_X_SIGNATURE']
request.body.rewind # Ensure we can read the request body
request_body = request.body.read
# Check if the signature header exists
unless received_signature
return false
end
# Parse the JSON request body
parsed_body = JSON.parse(request_body)
# Sort the object recursively
sorted_data = sort_object_keys(parsed_body)
# Re-serialize the request body into a JSON string
re_serialized_data = sorted_data.to_json
# Compute the HMAC-SHA256 signature
computed_signature = OpenSSL::HMAC.hexdigest('sha256', webhook_secret_key, re_serialized_data)
# Securely compare the received signature with the computed signature
Rack::Utils.secure_compare(received_signature, computed_signature)
end
# Function to recursively sort object keys
def sort_object_keys(obj)
case obj
when Array
obj.map { |item| sort_object_keys(item) }
when Hash
obj.keys.sort.each_with_object({}) do |key, sorted_hash|
sorted_hash[key] = sort_object_keys(obj[key])
end
else
obj
end
end
import (
"crypto/hmac"
"crypto/sha256"
"encoding/json"
"fmt"
"io/ioutil"
"net/http"
"sort"
)
// Function to handle the webhook request and verify the signature
func verifySignatureAndRespond(w http.ResponseWriter, r *http.Request) {
webhookSecretKey := "YOUR_SECRET_KEY" // Update your secret key
// Read the request body
requestBody, err := ioutil.ReadAll(r.Body)
if err != nil {
http.Error(w, "Error reading body", http.StatusInternalServerError)
return
}
// Get the signature from the header
receivedSignature := r.Header.Get("X-Signature")
// Sort the object recursively
var parsedBody map[string]interface{}
if err := json.Unmarshal(requestBody, &parsedBody); err != nil {
http.Error(w, "Error parsing body", http.StatusBadRequest)
return
}
sortedData := sortObjectKeys(parsedBody)
// Re-serialize the request body into a JSON string
reSerializedData, err := json.Marshal(sortedData)
if err != nil {
http.Error(w, "Error serializing data", http.StatusInternalServerError)
return
}
// Compute the HMAC-SHA256 signature
h := hmac.New(sha256.New, []byte(webhookSecretKey))
h.Write(reSerializedData)
computedSignature := fmt.Sprintf("%x", h.Sum(nil))
// Compare the received and computed signature
if hmac.Equal([]byte(receivedSignature), []byte(computedSignature)) {
w.WriteHeader(http.StatusOK)
w.Write([]byte(`{"status": "success", "message": "Signature verified Successfully!"}`))
} else {
w.WriteHeader(http.StatusBadRequest)
w.Write([]byte(`{"status": "failure", "message": "Invalid signature!"}`))
}
}
// Function to sort object keys (implemented with standard sorting)
func sortObjectKeys(obj map[string]interface{}) map[string]interface{} {
// Create a slice of keys and sort it
keys := make([]string, 0, len(obj))
for key := range obj {
keys = append(keys, key)
}
sort.Strings(keys)
// Create a new map with sorted keys
sortedObj := make(map[string]interface{})
for _, key := range keys {
sortedObj[key] = obj[key]
}
return sortedObj
}