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");
}