Skip to content

Enforcing integrity

Every object_payload you receive is accompanied by an object_payload_signature.

This signature (or hash) allows you to make sure that:

  • The payload was emitted by Treezor
  • The payload has not been altered
Lock icon

Security – Check for integrity

You MUST check the integrity of the payload against the object_payload_signature before trusting it.

How to check the payload integrity

For each received webhook, follow these steps:

  • Flatten the received JSON payload
  • Convert UTF-8 characters to their unicode sequence equivalent (é to \u00e9, è to \u00e8, etc.)
  • Generate the cryptographic signature of the payload (HMAC using the secret)
  • Convert the binary signature to base64
  • Compare your signature to the one provided along with the webhook
  • Respond according to the comparison result

Generate your own signature of the payload

To generate the signature, use the webhook_secret as a salt:

js
// dependencies
const fs = require('fs');
const crypto = require('crypto');


// declare a function to encode UTF-8 characters to their unicode sequence equivalent (\uxxxx)
let encodeUTF8ToCodePoint = (s) => {
	return s.replace(
		/[^\x20-\x7F]/g,
        x => "\\u" + ("000"+x.codePointAt(0).toString(16)).slice(-4)
	)
}

// function to compute the signature
const computedSignature = crypto.createHmac('sha256', WEBHOOK_SECRET)
	.update(
		encodeUTF8ToCodePoint(
			JSON.stringify(body.object_payload).replace(/\//g, '\\/')
		)
	)
	.digest('base64');
python
# function to compute the signature
def compute_signature(secret, payload):
	payload = json.dumps(payload, separators=(",", ":"))
	payload = payload.replace("\\/", "/") # Json dump escape / twice
	payload = payload.replace("\\/", "/") # remove all \
	payload = payload.replace("/", "\\/") # add \

	hmac_sha256 = hmac.new(
	    secret.encode("utf-8"),
	    payload=payload.encode("utf-8"),
	    digestmod=hashlib.sha256
	)
	return base64.b64encode(hmac_sha256.digest()).decode("utf-8")
ruby
# dependencies
require 'json'
require 'openssl'

# Function to encode UTF-8 characters to their unicode sequence equivalent (\uxxxx)
def encode_utf8_to_code_point(s)
s.gsub('/', '\\/').gsub(/[^ -~]/) { |m| "\\u%04x" % m.ord }
end

# Function to compute the signature
def compute_signature(secret, object_payload)
digest = OpenSSL::Digest.new('sha256')
hmac = OpenSSL::HMAC.digest(digest, secret, encode_utf8_to_code_point(object_payload.to_json))
base64_signature = [hmac].pack('m0') # Base64 encoding
end
java
// dependencies
import java.util.regex.Pattern;
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.StandardCharsets;
import java.util.Base64;

// Function to encode UTF-8 characters to their unicode sequence equivalent (\uxxxx)
public static String encodeUTF8ToCodePoint(String s) {
	Pattern pattern = Pattern.compile("[^\\x20-\\x7E]");
	return pattern.matcher(s).replaceAll(match -> {
		String hex = Integer.toHexString(match.group().codePointAt(0));
		return "\\u" + ("0000" + hex).substring(hex.length());
	});
}

// Function to compute the signature
public static String computeSignature(String secret, String objectPayload) throws Exception {
	// Initialize HMAC with SHA256
	Mac sha256_Hmac = Mac.getInstance("HmacSHA256");
	SecretKeySpec secretKey = new SecretKeySpec(secret.getBytes(StandardCharsets.US_ASCII), "HmacSHA256");
	sha256_Hmac.init(secretKey);

	// Encode payload and replace characters
	String encodedPayload = encodeUTF8ToCodePoint(objectPayload).replace("/", "\\/");
	byte[] payloadBytes = encodedPayload.getBytes(StandardCharsets.UTF_8);

	// Compute HMAC data
	byte[] hmacData = sha256_Hmac.doFinal(payloadBytes);

	// Convert to Base64 string
	return Base64.getEncoder().encodeToString(hmacData);
}
php
// generate your own version of the signature
function compute_signature(string $secret, array $object_payload) :string {
	return base64_encode(
		hash_hmac(
			'sha256',
			// In PHP, there is no need to encode UTF-8 characters to their unicode sequence 
			// since PHP's json_encode() function already does that automatically
			json_encode($object_payload),
			$secret, // as provided by your Treezor Account Manager
			true
		)
	);
}
Info icon

Information – Convert your signature UTF-8 characters to unicode

Treezor generates the object_payload_signature after converting all UTF-8 characters into the corresponding unicode sequences. You must do the same, otherwise you will produce mismatching signatures. (e.g., é must become \u00e9, è becomes \u00e8).

Compare your signature with the webhook's signature

php
// compare the newly generated signature with the webhook's signature
if(strcomp(
	$payload_local_signature, 
	// decode the payload and extract the provided signature from it
	json_decode(file_get_contents("php://input"), true)['object_payload_signature']
) === 0) {
	// signature are identical, we can trust the payload
	// proceed...
}
else {
	// signatures differ, we cannot trust the payload
	Throw new Exception('Mismatching signatures', 500);
}

What should my application return?

Signatures are identical

You should return a 200 HTTP Status Code.

Thumbs icon

Best practice – Defer the verified webhook in a queueing system for async processing

Without asynchroneous processing, if your code fails to process the webhook, Treezor would not attempt to deliver the webhook again as you have already answered with a 200 HTTP Status Code. This could lead to data inconsistency on your side.

Signatures don't match

You should return an HTTP Status Code in the 500 range.

Either way, there is no need to populate the response any further.

When your server answers with a Status Code higher than 499 or when it takes more than 150ms to answer, Treezor sends you the webhook again every minute (maximum of 30 attempts). If the 30 attempts limit is reached, then:

  • No more attempts are made
  • An incident notification is sent to Treezor

Treezor will get in touch with you to diagnose the issue. Once the issue is resolved, webhook are sent normally again.

Increased security

Treezor offers several ways to increase the security of webhooks:

IP Restriction

You may request that they be sent to you from a fixed IP. This allows your code to check the source IP in addition to webhooks signatures. To request a fixed IP, please get in touch with your Treezor Account Manager.

By default webhooks are sent from dynamic IP and don't allow for such checks.

Amazon Web Services SQS

If your infrastructure is built on AWS, you should get in touch with your Treezor Account Manager so that we can set up Amazon SQS instead of relying on webhook signatures.