Webhook Signature Verification
Since outbound webhook connections are not necessarily sent from a dedicated IP address, the webhook signature is the only way to prove that this payload is genuine and is not spoofed by an attacker. All customers using webhooks should implement the signature verification as described below in order to protect themselves against these kinds of spoofing attacks.
Extract the signature and event body
Each webhook event will have an x-sha2-signature header. This contains the webhook event’s signature in hexadecimal format.
Make sure you use the raw event request body. If you parse it from JSON first, the fields may be reordered.
Compute the expected signature
You can get the expected signature of the webhook by computing the HMAC of the event request body using the SHA256 algorithm, using the webhook’s secret token as the key.
Compare the signatures
A webhook event is valid if the signature from the header is equal to the expected signature you compute for it. Make sure signatures are both in hexadecimal, before comparing.
You should use a constant time equality function from a cryptographic library to prevent timing attacks. A timing attack is where a malicious user measures the small time differences taken to compare the signatures over many requests, to eventually work out the expected signature for a webhook event. A constant time equality function prevents this by always taking the same amount of time when comparing strings of a particular length.
Signature verification reference implementation examples
- Java
- CSharp
- Python
- NodeJs
package com.entrustdatacard.intellitrust.webhooks;
import org.apache.commons.codec.binary.Hex;
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
public class WebhookSignatureVerifier {
public void verifyPayload(String rawEventBody, String hexSignature, String webhookToken) {
Mac sha256Hmac;
String eventSignature = "";
try {
sha256Hmac = Mac.getInstance("HmacSHA256");
SecretKeySpec secretKey = new SecretKeySpec(webhookToken.getBytes(), "HmacSHA256");
sha256Hmac.init(secretKey);
eventSignature = new String(Hex.encodeHex(sha256Hmac.doFinal(rawEventBody.getBytes(StandardCharsets.UTF_8))));
} catch (Exception e) {
//Exception-Error while generating signature
}
if (!MessageDigest.isEqual(eventSignature.getBytes(), hexSignature.getBytes())) {
//Exception-Invalid webhook signature found
}
}
}
using System;
using System.Security.Cryptography;
using System.Text;
namespace com.entrustdatacard.intellitrust.webhooks
{
public class WebhookSignatureVerifier {
public void VerifyPayload(string rawEventBody, string hexSignature, string webhookToken) {
string eventSignature = "";
try {
HMACSHA256 hmac = new HMACSHA256(Encoding.UTF8.GetBytes(webhookToken));
byte[] hash = hmac.ComputeHash(Encoding.UTF8.GetBytes(rawEventBody));
eventSignature = BitConverter.ToString(hash).Replace("-", "").ToLower();
}
catch(Exception) {
//Exception-Error while generating signature
}
if (!CryptographicOperations.FixedTimeEquals(Encoding.UTF8.GetBytes(eventSignature), Encoding.UTF8.GetBytes(hexSignature))) {
//Exception-Invalid webhook signature found
}
}
}
}
import hmac
import hashlib
class WebhookSignatureVerifier:
def verify_payload(raw_event, signature, webhook_token):
# Compute the the actual HMAC signature from the raw request body.
event_signature = hmac.new(key=webhook_token.encode("utf-8"), msg=raw_event.encode("utf-8"), digestmod=hashlib.sha256).hexdigest()
# Compare the signatures (prevent against timing attacks).
if not hmac.compare_digest(signature, event_signature):
# Exception-Invalid webhook signature found
const crypto = require('crypto');
const verifyPayload = (rawEventBody, hexSignature, webhookToken) => {
// Compute the HMAC signature from the raw request body.
const eventSignature = crypto.createHmac('sha256', webhookToken)
.update(JSON.stringify(rawEventBody))
.digest('hex');
// Compare the signatures (prevent against timing attacks).
return crypto.timingSafeEqual(Buffer.from(eventSignature), Buffer.from(hexSignature));
}
Event payload schema
{
"resource": {
"id": "<UUID of the changed resource>",
"href": "<URI to read the updated resource>"
},
"resourceType": "credential",
"event": "credential.create | credential.update | credential.delete"
}
Fetching updated resource details
Post successful signature verification, the resource details can be fetched using the API endpoint referenced by the "href" attribute of event payload in case of create and update events.
- Java
- CSharp
- Python
- NodeJs
package com.entrustdatacard.intellitrust.webhooks;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
public class WebhookEventProcessor {
public void processWebhookEvent(String rawEventBody, String hexSignature, String webhookToken) {
try {
verifyPayload(rawEventBody, hexSignature, webhookToken);
} catch (Exception e) {
// Throw Exception, payload not verified.
}
JSONObject payload = new JSONObject(rawEventBody.toString());
JSONObject resource = payload.getJSONObject("resource");
String apiEndpoint = resource.get("href").toString();
try {
HttpClient client = HttpClient.newHttpClient();
HttpRequest httpGet = HttpRequest.newBuilder()
.GET().uri(URI.create(apiEndpoint))
.header("Accept", "application/json")
.header("Authorization", "<API_AUTH_TOKEN>").build(); // Replace with your API token.
HttpResponse<String> response = client.send(httpGet, HttpResponse.BodyHandlers.ofString());
// Process the response as needed.
} catch (Exception e) {
// Handle API response error.
}
}
}
using System;
using System.Net.Http;
using System.Threading.Tasks;
namespace com.entrustdatacard.intellitrust.webhooks
public class WebhookEventProcessor
{
public async Task ProcessWebhookEvent(string rawEventBody, string hexSignature, string webhookToken)
{
try
{
VerifyPayload(rawEventBody, hexSignature, webhookToken);
}
catch (Exception e)
{
// Throw Exception, payload not verified.
}
JObject payload = JObject.Parse(rawEventBody);
JObject resource = (JObject)payload["resource"];
string apiEndpoint = resource["href"].ToString();
try
{
using (HttpClient client = new HttpClient())
{
client.DefaultRequestHeaders.Add("Accept", "application/json");
client.DefaultRequestHeaders.Add("Authorization", "<API_AUTH_TOKEN>"); // Replace with your API token.
HttpResponseMessage response = await client.GetAsync(apiEndpoint);
string responseBody = await response.Content.ReadAsStringAsync();
// Process the response body as needed
}
}
catch (Exception e)
{
// Handle API response error.
}
}
}
import json
import requests
class WebhookEventProcessor:
def process_webhook_event(raw_event, signature, webhook_token):
try:
verify_payload(raw_event, signature, webhook_token)
except Exception as e:
# Throw Exception, payload not verified.
pass
payload = json.loads(raw_event)
resource = payload.get("resource")
api_endpoint = resource.get("href")
try:
headers = {
"Accept": "application/json",
"Authorization": "<API_AUTH_TOKEN>" # Replace with your API token
}
response = requests.get(api_endpoint, headers=headers)
# Process the response as needed.
# response.json() for JSON response or response.text for raw response.
except Exception as e:
# Handle API response error.
pass
const axios = require('axios');
const processWebhookEvent = (rawEventBody, hexSignature, webhookToken) => {
// Verify the event signature.
const isValid = verifyPayload(rawEventBody, hexSignature, webhookToken);
if (isValid) {
// Payload verified, parse the event body and process the event.
const apiEndpoint = rawEventBody.resource.href;
const reqConfig = {
method: 'get', // Request method varies by event type.
url: apiEndpoint,
headers: {
'Accept': 'application/json',
'Authorization': "<API_AUTH_TOKEN>" // Replace with your API token.
}
}
axios.request(reqConfig)
.then(response => {
// Process the response as needed.
console.log(JSON.stringify(response.data));
}).catch(error => {
console.log(error);
});
}
}
Notification delivery acknowledgement
Upon receiving a webhook notification, acknowledge the successful delivery by responding with an HTTP 200 OK status code within 15 seconds. Otherwise, the notification delivery will be reattempted for a maximum of 5 times. If a retry fails on the 5th attempt, the webhook will be disabled. Retry schedule:
- 30 seconds after the first attempt
- 2 minutes after the first attempt
- 15 minutes after the first attempt
- 2 hours after the first attempt
- 10 hours after the first attempt