Verify Webhook Signatures
Use webhook signature verification for protection from a variety of attacks.
It’s important to ensure that a request truly came from the expected sender. To help secure incoming webhooks, Zai can sign all webhook events sent to your endpoints by including a signature in each Webhooks-signature
header.
This should be a part of the standard process for setting up webhooks and this guide provides the details you’ll need to get started. Also, refer to our Webhooks guide if you need more information on using webhooks.
Signature replay attacks
To prevent an attacker from intercepting and re-transmitting valid data transmissions, Zai includes a timestamp in the Webhooks-signature
header. The timestamp is also verified by the signature, so an attacker won’t be able to change the timestamp without invalidating the signature.
The timestamp and signature are generated each time Zai sends an event to your endpoint. If Zai retries an event, a new signature and timestamp is generated.
How to verify Zai is the sender
Before you can start verifying signatures, Zai will need to obtain your signature secret key information using the Create a Secret Key API. The value must be ASCII characters and 32 bytes in size, otherwise, we’ll let you know through validation checks if it isn’t.
Verification of signatures should be able to be performed in any language using the shared signature key. Example implementations are included at the bottom of this guide.
1. Extract the timestamp and signatures
Split the header, at the ,
character, to get both the t
(timestamp) and v
(signature) values. Then split each element, using the =
character as the separator, to get a prefix and value pair.
2. Create the signed_payload string
The signed_payload
string is created by linking:
-
The timestamp (as a string) from the previous step
-
The dot character
.
-
The request body (this is JSON in a string format)
3. Generate the expected signature
Use the secret_key
that you previously set using the webhook/secret_key
API endpoint. Each encryption uses hash-based message authentication code (HMAC) with SHA256 and the value will be base64 encoded format (raw URL encoding). Use the signed_payload
string from step 2 as the message.
4. Compare signatures
Compare the signature(s) in the header to the expected signature. If they match, compute the difference between the current timestamp and the received timestamp. Verify that the timestamp is within your allowed tolerance.
When comparing signatures, you should use a constant_time string comparison to protect against timing attacks. We recommend that you use HMAC's library functions to compare signatures.
Example implementations
package main
import (
"crypto/hmac"
"crypto/sha256"
"encoding/base64"
"fmt"
"io"
"strconv"
"time"
)
func createSignatureToken(payload string, secretKey string, timeToInclude int64) (string, error) {
hash := hmac.New(sha256.New, []byte(secretKey))
signedPayload := strconv.Itoa(int(timeToInclude)) + "." + string(payload)
_, err := io.WriteString(hash, signedPayload)
if err != nil {
return "", err
}
return base64.RawURLEncoding.EncodeToString(hash.Sum(nil)), nil
}
func main() {
s := "xPpcHHoAOM"
t := time.Now().Unix()
payload := "{\"event\": \"status_updated\"}"
result, _ := createSignatureToken(payload, s, t)
fmt.Println(result)
}
require "openssl";
require "uri";
require "base64";
require 'cgi'
def createSignatureToken(payload, secret, time)
t = [ time, payload].join(".")
digest = OpenSSL::Digest.new('sha256')
hash = OpenSSL::HMAC.digest(digest, secret, t)
result = Base64.urlsafe_encode64(hash, padding: false)
return result
end
time = "1257894000"
payload = "{\"event\": \"status_updated\"}"
secret = "xPpcHHoAOM"
result = createSignatureToken(payload, secret, time)
puts result
var crypto = require('crypto');
const fs = require('fs');
secret = "xPpcHHoAOM"
time = "1257894000"
payload = "{\"event\": \"status_updated\"}"
const {createHash} = require('crypto');
createSignatureToken(payload, secret, time)
function createSignatureToken(payload, secret, time) {
message = time + '.' + payload
result = crypto.createHmac('sha256', secret).update(message).digest("base64url").toString('utf8');
return result
}
console.log(result)
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.util.Base64;
public class ApiSecurityExample {
public static void main(String[] args) {
try {
String secret = "xPpcHHoAOM";
String time = "1257894000";
String payload = "{\"event\": \"status_updated\"}";
String message = time + "." + payload;
Mac sha256_HMAC = Mac.getInstance("HmacSHA256");
SecretKeySpec secret_key = new SecretKeySpec(secret.getBytes(), "HmacSHA256");
sha256_HMAC.init(secret_key);
byte[] hash = sha256_HMAC.doFinal(message.getBytes());
String encoded = Base64.getUrlEncoder().withoutPadding().encodeToString(hash);
System.out.println(encoded);
}
catch (Exception e){
System.out.println("Error");
}
}
}
import hmac
import hashlib
import base64
payload = "{\"event\": \"status_updated\"}"
time = "1257894000"
secretKey = "xPpcHHoAOM"
secret_key_bytes = bytes(secretKey, 'utf-8')
signed_payload = time + '.' + payload
signed_payload_bytes = bytes(signed_payload, 'utf-8')
signature = hmac.new(secret_key_bytes, signed_payload_bytes, digestmod=hashlib.sha256).digest()
result = str(base64.urlsafe_b64encode(signature), 'utf-8').rstrip('=')
print(result)
<!DOCTYPE html>
<html>
<body>
<?php
$payload = "{\"event\": \"status_updated\"}";
$time = "1257894000";
$secretKey = "xPpcHHoAOM";
$arr = array($time,'.',$payload);
$signed_payload = join('', $arr);
$hash = hash_hmac('sha256', $signed_payload, $secretKey, true);
$output = rtrim( strtr( base64_encode( $hash ), '+/', '-_'), '=');
echo $output;
?>
</body>
</html>
using System;
using System.Security.Cryptography;
public class Program
{
public static void Main()
{
string secret = "xPpcHHoAOM";
string payload = "{\"event\": \"status_updated\"}";
string time = "1257894000";
string signed_payload = time + "." + payload;
var encoding = new System.Text.UTF8Encoding();
byte[] keyByte = encoding.GetBytes(secret);
byte[] messageBytes = encoding.GetBytes(signed_payload);
using (var hmacsha256 = new HMACSHA256(keyByte))
{
byte[] hashmessage = hmacsha256.ComputeHash(messageBytes);
string signature = Convert.ToBase64String(hashmessage).Replace("/", "-").Replace("+", "_").Replace("=", "");
Console.WriteLine(signature);
}
}
}
Updated over 1 year ago