Signature Verification
Cum să verificați semnăturile platformei webhook cu HMAC-SHA256 peste corpul brut și timestamp, cu exemple de lucru în Go, Python și JavaScript.
Fiecare platformă webhook include o semnătură HMAC. Verificați-o înainte de a analiza corpul sau de a efectua efecte secundare. Verificarea semnăturilor dovedește că sarcina de utilizare și timestamp au fost produse cu secretul de semnătură al abonamentului; un timestamp verifică prospețimea limitează atacurile de replay.
How it works
Section titled “How it works”the platform signs each webhook payload using HMAC-SHA256 cu secretul de semnătură din abonamentul dvs. webhook. Semnătura este trimisă în X-Probo-Webhook-Signature header.
Mesajul semnat este concatenarea timestamp-ului și a corpului de cerere brut, separat de un colon:
{timestamp}:{body}
Where:
timestampEste valoarea de laX-Probo-Webhook-Timestampheader (Unix seconds)bodyeste corpul de solicitare JSON
Use the full signing secret string (including the whsec_ Prefix) ca cheie HMAC. Nu ștergeți prefixul sau hexedecodarea secretului.
Verification steps
Section titled “Verification steps”-
Extract the headers
Read
X-Probo-Webhook-TimestampandX-Probo-Webhook-Signaturedin cererea de. -
Build the signed message
Concatenate the timestamp, a colon (
:), și corpul de cerere brută. -
Compute the expected signature
Calculate
HMAC-SHA256folosind secretul complet de semnătură (inclusivwhsec_Prefix) ca cheie şi mesajul semnat ca intrare. -
Compare signatures
Utilizaţi o comparaţie constantă a timpului pentru a verifica dacă semnătura calculată corespunde
X-Probo-Webhook-Signatureheader. -
Check timestamp freshness
După ce semnătura se potrivește, respingeți solicitarea dacă timestamp-ul său este mai mare de 5 minute în trecut sau în viitor.
Examples
Section titled “Examples”package main
import (
"crypto/hmac"
"crypto/sha256"
"encoding/hex"
"fmt"
"io"
"net/http"
"strconv"
"time"
)
func verifyWebhook(r *http.Request, signingSecret string) ([]byte, error) {
body, err := io.ReadAll(r.Body)
if err != nil {
return nil, err
}
timestamp := r.Header.Get("X-Probo-Webhook-Timestamp")
signature := r.Header.Get("X-Probo-Webhook-Signature")
if timestamp == "" || signature == "" {
return nil, fmt.Errorf("missing signature headers")
}
mac := hmac.New(sha256.New, []byte(signingSecret))
mac.Write([]byte(timestamp))
mac.Write([]byte(":"))
mac.Write(body)
received, err := hex.DecodeString(signature)
if err != nil || !hmac.Equal(mac.Sum(nil), received) {
return nil, fmt.Errorf("invalid signature")
}
signedAt, err := strconv.ParseInt(timestamp, 10, 64)
if err != nil {
return nil, fmt.Errorf("invalid timestamp")
}
delta := time.Now().Unix() - signedAt
if delta > 300 || delta < -300 {
return nil, fmt.Errorf("stale timestamp")
}
return body, nil
}
import hashlib
import hmac
import re
import time
def verify_webhook(
body: bytes,
timestamp: str | None,
signature: str | None,
signing_secret: str,
) -> bool:
if (
timestamp is None
or signature is None
or re.fullmatch(r"[0-9]+", timestamp) is None
):
return False
expected = hmac.new(
signing_secret.encode(),
timestamp.encode("ascii") + b":" + body,
hashlib.sha256,
).digest()
if re.fullmatch(r"[0-9a-fA-F]{64}", signature) is None:
return False
if not hmac.compare_digest(expected, bytes.fromhex(signature)):
return False
signed_at = int(timestamp)
return abs(time.time() - signed_at) <= 300
import { createHmac, timingSafeEqual } from "node:crypto";
function verifyWebhook(rawBody, timestamp, signature, signingSecret) {
if (
!Buffer.isBuffer(rawBody) ||
typeof timestamp !== "string" ||
typeof signature !== "string" ||
!/^[0-9]+$/.test(timestamp) ||
!/^[0-9a-fA-F]{64}$/.test(signature)
) {
return false;
}
const expected = createHmac("sha256", signingSecret)
.update(timestamp)
.update(":")
.update(rawBody)
.digest();
const received = Buffer.from(signature, "hex");
if (
expected.length !== received.length ||
!timingSafeEqual(expected, received)
) {
return false;
}
const signedAt = Number(timestamp);
return (
Number.isFinite(signedAt) && Math.abs(Date.now() / 1000 - signedAt) <= 300
);
}
Security recommendations
Section titled “Security recommendations”- Verificați semnătura înainte de a analiza JSON, de a autoriza organizația sau de a coada lucrările.
- Refuzați timestamp-urile lipsă, deformate, învechite și datate în viitor. Exemplele utilizează o toleranță de 5 minute.
- Verificați mai întâi lungimea lor, unde comparația API necesită intrări de lungime egală.
- Păstrați un secret separat pentru fiecare abonament și stocați-l într-un manager secret.
- Return a generic
400or403response. Do not reveal which verification check failed. - Record
eventIdValidarea timestamp limitează timpul de redare; idempotency previne efectele secundare duplicate.
Troubleshooting
Section titled “Troubleshooting”| Symptom | Likely cause |
|---|---|
| Every signature fails | Cadrul parsează sau modifică corpul înainte de verificare |
| Only non-ASCII payloads fail | Receptorul a decodificat și re-codificat corpul în loc să hasheze byte brute |
timingSafeEqual throws | Semnătura primită nu a fost validată mai întâi ca 32-byte hexadecimal |
| Livrările valabile sunt aproape ca stale | Ceasul receptorului nu este sincronizat sau timestamp-ul a fost tratat ca milisecunde |
| Verificarea funcționează cu o singură abonare | Punctul final este selectarea secretului de abonament greșit |