Validating Webhooks
To verify the authenticity and integrity of the webhook payload you should validate the signature provided in the Venndr-Signature
HTTP header.
We strongly recommend validating the timestamp against the current UTC time to ensure that the request is not a replay of an older request.
The signature is calculated using the following details (in pseudocode):
sig = headers.Venndr-Id +
headers.Venndr-Key-Version +
headers.Venndr-Version +
headers.Venndr-Timestamp +
headers.Venndr-Platform-Id +
headers.Venndr-Store-Id +
headers.Venndr-Topic +
request_body
The resulting string is HMACed (with SHA256) using a private key and finally base64 encoded for transport.
You must fetch the public key that matches the version in the Venndr-Key-Version
header to verify the signature.
The public key is available at https://api.venndr.cloud/.well-known/public-keys/<version>
.
Test Data
The following key and dummy request can be used for verifying your implementation.
Public Key
-----BEGIN RSA PUBLIC KEY-----
MIIBCgKCAQEAnzKquBKihkXANnvanftNv/MG3Zd4tMMj+AByMiLFrBGpiOnDfPuh
nuKszZhUGN5eC1PEFrzf5QnTK58dY2+/r2PXuZcXz3w+hwk+aC09ryboCD1Cc1ae
0Sins7p22uQyWSt0cfhun5TdeXhPhFFSQgI7DtA8sUfHE+fsYB4feOsimouNweKE
/gKb0S7yq1Bno3e1/iBsFrj26ekYOVQQ1tn5dOzmoI5zM5wKAburKZEGL4xOU/mq
kPL0nUpaxoGT8Vx3zx22yr9Y2O7CIfYGESLHSRcNYh4z2JZrPq8QgptuUAB/wCF/
vEwI/GwPk8XWswxPwbI/VXrBqtSq4/06jwIDAQAB
-----END RSA PUBLIC KEY-----
Request
Venndr-Id: b2bd8273-8991-4d6a-b625-88af91d2d04d
Venndr-Platform-Handle: platform
Venndr-Platform-Id: 5020e8a0-3266-4b7c-8a90-f0ce2f5c1087
Venndr-Store-Handle: store
Venndr-Store-Id: a5902e6b-5513-4603-b5ba-d0504d890db7
Venndr-Topic: testing
Venndr-Version: 1
Venndr-Key-Version: testing
Venndr-Signature: l2YqsY6wo689LgUG7uwSI3Jzseqkso7LyWlDZEdGg6Dc+2p0d32/OXT1VHhsmWuIhIIh7+OvU/0zT5VD2RDsO4PNLLFmhul+OiVa1v0/jWbWEeJDqm0vOdfVGyqLETLecSBtDhO7gziwUSrJsLnptoyvxgvhmCzuV9fBp0ObP0ekA6CN1Uxn33kIhPM7iETFRbEpi8uA1drsF0vcMDjz4b6tSROzhtcp2aY/AfpqbbiVJJZ4sjwPamIJNa2RMzOsdBzt8RYwmrabd6NNfvtf3GGdL/gc0vkKAZSuRwjQBDPKq/arzb0s0q4/kNIVg/tUcg++dsOqd3XtxYHbNLPI8g==
Venndr-Timestamp: 1689079288
{"action":"testing","payload":{"created_at":"2023-07-11T12:41:18.671870Z","message":"Testing 1..2..3! Beep boop, bleep bloop!"},"platform_id":"5020e8a0-3266-4b7c-8a90-f0ce2f5c1087","request_id":"b2bd8273-8991-4d6a-b625-88af91d2d04d","store_id":"a5902e6b-5513-4603-b5ba-d0504d890db7","topic":"testing","version":"1"}
Note! The payload (request body) should consist of the single line only with no surrounding whitespace.
Examples
Ruby
require 'openssl'
require 'base64'
# 1. download the public key (implementation excluded)
public_key = download_pkey(headers["Venndr-Key-Version"])
rsa = OpenSSL::PKey::RSA.new(public_key)
# 2. compose payload for signing
body = request.body.read
data = headers["Venndr-Id"] +
headers["Venndr-Key-Version"] +
headers["Venndr-Version"] +
headers["Venndr-Timestamp"] +
headers["Venndr-Platform-Id"] +
headers["Venndr-Store-Id"] +
headers["Venndr-Topic"] +
body
# 3. verify the signature
digest = Base64.decode64(headers["Venndr-Signature"])
is_sig_ok = rsa.verify("SHA256", digest, data)
PHP
// 1. download the public key (implementation excluded)
$pubkey = download_pkey($_SERVER["HTTP_VENNDR_KEY_VERSION"]);
// Example uses https://packagist.org/packages/phpseclib/phpseclib#2.0.36
$rsa = new \phpseclib\Crypt\RSA();
$rsa->loadKey($pubkey);
$rsa->setHash("sha256");
$rsa->setSignatureMode(\phpseclib\Crypt\RSA::SIGNATURE_PKCS1);
$body = file_get_contents('php://input');
// 2. compose payload for signing
$message = $_POST["HTTP_VENNDR_ID"] .
$_POST["HTTP_VENNDR_KEY_VERSION"] .
$_POST["HTTP_VENNDR_VERSION"] .
$_POST["HTTP_VENNDR_TIMESTAMP"] .
$_POST["HTTP_VENNDR_PLATFORM_ID"] .
$_POST["HTTP_VENNDR_STORE_ID"] .
$_POST["HTTP_VENNDR_TOPIC"] .
$body;
// 3. verify the signature
$digest = base64_decode($_POST["HTTP_VENNDR_SIGNATURE"]);
$is_sig_ok = $rsa->verify($message, $digest);
Go
package verifier
import (
"crypto"
"crypto/rsa"
"crypto/sha256"
"encoding/base64"
"net/http"
"strings"
)
type Verifier struct {
key *rsa.PublicKey
}
func NewVerifier(pubkey *rsa.PublicKey) *Verifier {
return &Verifier{pubkey}
}
func (v *Verifier) VerifyWebhook(r *http.Request, body string) error {
// Decode the delivered MAC
msgMAC, err := getMessageMAC(r)
if err != nil {
return err
}
// Compose the payload for signing
msg := getMessage(r, body)
// Generate the expected MAC
dgst := sha256.New()
if _, err := dgst.Write([]byte(msg)); err != nil {
return err
}
expMAC := dgst.Sum(nil)
// Verify using the public key
return rsa.VerifyPKCS1v15(v.key, crypto.SHA256, expMAC, msgMAC)
}
func getMessageMAC(r *http.Request) ([]byte, error) {
sig := r.Header.Get("Venndr-Signature")
messageMAC, err := base64.StdEncoding.DecodeString(sig)
if err != nil {
return nil, err
}
return messageMAC, nil
}
func getMessage(r *http.Request, body string) string {
m := strings.Join([]string{
r.Header.Get("Venndr-Id"),
r.Header.Get("Venndr-Key-Version"),
r.Header.Get("Venndr-Version"),
r.Header.Get("Venndr-Timestamp"),
r.Header.Get("Venndr-Platform-Id"),
r.Header.Get("Venndr-Store-Id"),
r.Header.Get("Venndr-Topic"),
body,
}, "")
return m
}
NodeJS
const crypto = require("crypto");
const msgHeaders = [
"Venndr-Id",
"Venndr-Key-Version",
"Venndr-Version",
"Venndr-Timestamp",
"Venndr-Platform-Id",
"Venndr-Store-Id",
"Venndr-Topic",
];
// 1. download the public key (implementation excluded)
const key = await fetchKey(request.header("Venndr-Key-Version"));
// 2. compose payload for signing
const message = Buffer.from(
msgHeaders.reduce((acc, header) => acc + request.header(header), "") + request.body,
);
// 3. verify the signature
const signature = Buffer.from(request.header("Venndr-Signature") ?? "", "base64");
if (!crypto.verify("sha256", message, key, signature)) {
throw new Error("validation failed");
}