BLOOCK Docs
Search…
⌃K

Check the webhook signatures

Verify the events that Bloock sends to your webhook endpoints.
Bloock signs the webhook events it sends to your endpoints by including a signature in each event’s Bloock-Signature header. This allows you to verify that the events were sent by Bloock, not by a third party.
Before you can verify signatures, you need to retrieve your endpoint’s signing secret from your Dashboard’s Webhook's settings. Select an endpoint that you want to obtain the secret for, then click the Click to reveal button.
Bloock generates a unique secret key for each endpoint. If you use multiple endpoints, you must obtain a secret for each one you want to verify signatures on. After this setup, Bloock starts to sign each webhook it sends to the endpoint.

Preventing replay attacks

A replay attack is when an attacker intercepts a valid payload and its signature, then re-transmits them. To mitigate such attacks, Bloock includes a timestamp in the Bloock-Signature header. Because this timestamp is part of the signed payload, it is also verified by the signature, so an attacker can’t change the timestamp without invalidating the signature. If the signature is valid but the timestamp is too old, you can have your application reject the payload.
Bloock defines a default tolerance of ten minutes between the timestamp and the current time. You can enable and disable the tolerance control and change this tolerance by changing the number of minutes when verifying signatures.

Verifying signatures using our SDK library

Use our SDK to verify signatures. You perform the verification by providing the event payload, the Bloock-Signature header, and the endpoint’s secret. If verification fails, Bloock returns an error and a false boolean. Remember to import our SDK first.
Bloock requires the raw body of the request to perform signature verification. If you’re using a framework, make sure it doesn’t manipulate the raw body. Any manipulation to the raw body of the request causes the verification to fail.
Javascript
Java
Python
Golang
npm install @bloock/sdk --save
const { WebhookClient } = require("@bloock/sdk");
const express = require("express");
const app = express();
const port = 3000;
var bodyParser = require("body-parser");
const secretKey = "NHJTAE6ikKBccSaeCSBSWGdp7NmixXy7";
var options = {
inflate: true,
limit: "100kb",
type: "application/*",
};
app.use(bodyParser.raw(options));
app.post("/verify", async (req, res) => {
let enforceTolerance = false; // decide if you want to set tolerance when verifying
let body = req.body;
let header = req.get("Bloock-Signature");
let webhookClient = new WebhookClient();
const ok = await webhookClient.verifyWebhookSignature(body, header, secretKey, enforceTolerance);
if (!ok) {
console.error("Invalid Signature!");
} else {
console.log("Valid Signature!");
}
return res;
});
app.listen(port, () => {
console.log(`Example app listening on port ${port}`);
});
pip install bloock==SDK_VERSION
from flask import Flask
from flask import request
import bloock
from bloock.client.webhook import WebhookClient
app = Flask(__name__)
SECRET_KEY = "NHJTAE6ikKBccSaeCSBSWGdp7NmixXy7"
@app.route('/verify', methods=['POST'])
def index():
enforce_tolerance = False # decide if you want to set tolerance when verifying
body = request.data
bloock_signature = request.headers['Bloock-Signature']
webhook_client = WebhookClient()
ok = webhook_client.verify_webhook_signature(
body, bloock_signature, SECRET_KEY, enforce_tolerance)
if ok == False:
raise ValueError('Invalid Signature!')
else:
print('Valid Signature!')
return 'Finish'
app.run(host='0.0.0.0', port=81)
go get github.com/bloock/bloock-sdk-go/v2
package main
import (
"log"
"net/http"
"github.com/bloock/bloock-sdk-go/v2/client"
)
// SecretKey represents the client secret key associate to your webhook endpoint
const SecretKey = "NHJTAE6ikKBccSaeCSBSWGdp7NmixXy7"
func main() {
verifyHandler := func(w http.ResponseWriter, req *http.Request) {
enforceTolerance := false // decide if you want to set tolerance when verifying
body, err := io.ReadAll(req.Body)
if err != nil {
log.Fatalf("Cannot read body request: %v", err)
}
bloockSignature := req.Header.Get("Bloock-Signature")
webhookClient := client.NewWebhookClient()
ok, err := webhookClient.VerifyWebhookSignature(body, bloockSignature, SecretKey, false)
if err != nil {
log.Fatal(err)
}
if !ok {
log.Fatal("Invalid Signature!")
}
log.Println("Valid Signature!")
}
http.HandleFunc("/verify", verifyHandler)
log.Println("Listing for requests at http://localhost:8000/verify")
log.Fatal(http.ListenAndServe(":8000", nil))
}

Verifying signatures manually

The Bloock-Signature header included in each signed event contains a timestamp and one signature. The timestamp is prefixed by t=, signature is prefixed by v1=.
Bloock-Signature:
t=1492774577,
v1=5257a869e7ecebeda32affa62cdca3fa51cad7e77a0e56ff536d0ce8e108d8bd
Bloock generates signatures using a hash-based message authentication code (HMAC) with SHA-256.
This code shows how to verify your webhook events signatures.
Golang
package main
import (
"bytes"
"crypto/hmac"
"crypto/sha256"
"encoding/hex"
"encoding/json"
"errors"
"fmt"
"io"
"log"
"net/http"
"strconv"
"strings"
"time"
)
const (
// SecretKey represents the client secret key associate to your webhook endpoint
SecretKey = "secret"
// DefaultTolerance indicates that signatures older than this will be rejected.
DefaultTolerance = 600 * time.Second
// signingVersion represents the version of the signature we currently use.
signingVersion = "v1"
)
var (
ErrInvalidHeader = errors.New("webhook has invalid Bloock-Signature header")
ErrNotSigned = errors.New("webhook has no Bloock-Signature header")
ErrNoValidSignature = errors.New("webhook had no valid signature")
ErrTooOld = errors.New("timestamp wasn't within tolerance")
)
type signedHeader struct {
timestamp time.Time
signature []byte
}
func main() {
verifyHandler := func(w http.ResponseWriter, req *http.Request) {
enforceTolerance := false // decide if you want to set tolerance when verifying
body, err := io.ReadAll(req.Body)
if err != nil {
log.Fatalf("Cannot read body request: %v", err)
}
bloockSignature := req.Header.Get("Bloock-Signature")
err = verifySignature(body, bloockSignature, SecretKey, enforceTolerance)
if err != nil {
log.Fatal(err)
}
log.Println("Valid Signature!")
}
http.HandleFunc("/verify", verifyHandler)
log.Println("Listing for requests at http://localhost:8000/verify")
log.Fatal(http.ListenAndServe(":8000", nil))
}
func verifySignature(payload []byte, sigHeader string, secretKey string, enforceTolerance bool) error {
header, err := parseSignatureHeader(sigHeader)
if err != nil {
return err
}
expectedSignature, err := computeSignature(header.timestamp, payload, secretKey)
if err != nil {
return err
}
expiredTimestamp := time.Since(header.timestamp) > DefaultTolerance
if enforceTolerance && expiredTimestamp {
return ErrTooOld
}
if hmac.Equal(expectedSignature, header.signature) {
return nil
}
return ErrNoValidSignature
}

Step 1: Extract the timestamp and signatures from the header

Split the header, using the , character as the separator, to get a list of elements. Then split each element, using the = character as the separator, to get a prefix and value pair.
The value for the prefix t corresponds to the timestamp, and v1 corresponds to the signature.
Golang
func parseSignatureHeader(header string) (*signedHeader, error) {
sh := &signedHeader{}
if header == "" {
return sh, ErrNotSigned
}
// Signed header looks like "t=1495999758,v1=ABC"
pairs := strings.Split(header, ",")
for _, pair := range pairs {
parts := strings.Split(pair, "=")
if len(parts) != 2 {
return sh, ErrInvalidHeader
}
switch parts[0] {
case "t":
timestamp, err := strconv.ParseInt(parts[1], 10, 64)
if err != nil {
return sh, ErrInvalidHeader
}
sh.timestamp = time.Unix(timestamp, 0)
case signingVersion:
sig, err := hex.DecodeString(parts[1])
if err != nil {
continue
}
sh.signature = sig
default:
continue
}
}
if len(sh.signature) == 0 {
return sh, ErrNoValidSignature
}
return sh, nil
}

Step 2: Determine the expected signature

Compute an HMAC with the SHA256 hash function. Use the endpoint’s signing secret as the key, and use the concatenation of the timestamp with the payload.
Golang
func computeSignature(t time.Time, payload []byte, secretKey string) ([]byte, error) {
buffer := new(bytes.Buffer)
if err := json.Compact(buffer, payload); err != nil {
return nil, err
}
mac := hmac.New(sha256.New, []byte(secretKey))
mac.Write([]byte(fmt.Sprintf("%d", t.Unix())))
mac.Write([]byte("."))
mac.Write(buffer.Bytes())
return mac.Sum(nil), nil
}

Step 3: Compare the signatures

Compare the signature in the header to the expected signature. For an equality match, compute the difference between the current timestamp and the received timestamp, then decide if the difference is within your tolerance.
Golang
expiredTimestamp := time.Since(header.timestamp) > DefaultTolerance
if enforceTolerance && expiredTimestamp {
return ErrTooOld
}
if hmac.Equal(expectedSignature, header.signature) {
return nil
}
All the complete code snippet.
Golang
package main
import (
"bytes"
"crypto/hmac"
"crypto/sha256"
"encoding/hex"
"encoding/json"
"errors"
"fmt"
"io"
"log"
"net/http"
"strconv"
"strings"
"time"
)
const (
// SecretKey represents the client secret key associate to your webhook endpoint
SecretKey = "secret"
// DefaultTolerance indicates that signatures older than this will be rejected.
DefaultTolerance = 600 * time.Second
// signingVersion represents the version of the signature we currently use.
signingVersion = "v1"
)
var (
ErrInvalidHeader = errors.New("webhook has invalid Bloock-Signature header")
ErrNotSigned = errors.New("webhook has no Bloock-Signature header")
ErrNoValidSignature = errors.New("webhook had no valid signature")
ErrTooOld = errors.New("timestamp wasn't within tolerance")
)
type signedHeader struct {
timestamp time.Time
signature []byte
}
func main() {
verifyHandler := func(w http.ResponseWriter, req *http.Request) {
enforceTolerance := false // decide if you want to set tolerance when verifying
body, err := io.ReadAll(req.Body)
if err != nil {
log.Fatalf("Cannot read body request: %v", err)
}
bloockSignature := req.Header.Get("Bloock-Signature")
err = verifySignature(body, bloockSignature, SecretKey, enforceTolerance)
if err != nil {
log.Fatal(err)
}
log.Println("Valid Signature!")
}
http.HandleFunc("/verify", verifyHandler)
log.Println("Listing for requests at http://localhost:8000/verify")
log.Fatal(http.ListenAndServe(":8000", nil))
}
func verifySignature(payload []byte, sigHeader string, secretKey string, enforceTolerance bool) error {
header, err := parseSignatureHeader(sigHeader)
if err != nil {
return err
}
expectedSignature, err := computeSignature(header.timestamp, payload, secretKey)
if err != nil {
return err
}
expiredTimestamp := time.Since(header.timestamp) > DefaultTolerance
if enforceTolerance && expiredTimestamp {
return ErrTooOld
}
if hmac.Equal(expectedSignature, header.signature) {
return nil
}
return ErrNoValidSignature
}
func parseSignatureHeader(header string) (*signedHeader, error) {
sh := &signedHeader{}
if header == "" {
return sh, ErrNotSigned
}
// Signed header looks like "t=1495999758,v1=ABC"
pairs := strings.Split(header, ",")
for _, pair := range pairs {
parts := strings.Split(pair, "=")
if len(parts) != 2 {
return sh, ErrInvalidHeader
}
switch parts[0] {
case "t":
timestamp, err := strconv.ParseInt(parts[1], 10, 64)
if err != nil {
return sh, ErrInvalidHeader
}
sh.timestamp = time.Unix(timestamp, 0)
case signingVersion:
sig, err := hex.DecodeString(parts[1])
if err != nil {
continue
}
sh.signature = sig
default:
continue
}
}
if len(sh.signature) == 0 {
return sh, ErrNoValidSignature
}
return sh, nil
}
func computeSignature(t time.Time, payload []byte, secretKey string) ([]byte, error) {
buffer := new(bytes.Buffer)
if err := json.Compact(buffer, payload); err != nil {
return nil, err
}
mac := hmac.New(sha256.New, []byte(secretKey))
mac.Write([]byte(fmt.Sprintf("%d", t.Unix())))
mac.Write([]byte("."))
mac.Write(buffer.Bytes())
return mac.Sum(nil), nil
}