Appearance
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
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');
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
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")
1
2
3
4
5
6
7
8
9
10
11
12
13
2
3
4
5
6
7
8
9
10
11
12
13
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
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
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
)
);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
2
3
4
5
6
7
8
9
10
11
12
13
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);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
2
3
4
5
6
7
8
9
10
11
12
13
What should my application return?
Signatures are identical
You should return a 200
HTTP Status Code.
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.