Preskočiť na obsah

Overenie podpisu webhooku

Na adresu vášho koncového bodu môže POST poslať ktokoľvek, kto ju pozná, preto pred dôverou skontrolujte podpis každého doručenia. Dakoty podpisuje podľa štandardu Standard Webhooks a pre spätnú kompatibilitu posiela aj starší hlavičkový podpis X-Dakoty-Signature. Tajný kľúč (začína whsec_) nájdete v detaile webhooku v aplikácii a REST hook ho dostane raz v odpovedi na vytvorenie. Anglická verzia: Verifying signatures.

Každé doručenie nesie webhook-id, webhook-timestamp a webhook-signature. Podpis je v1, a za ním base64 z HMAC-SHA256 nad reťazcom webhook-id.webhook-timestamp.surové-telo. Kľúčom HMAC je base64 dekódovaná časť tajného kľúča za whsec_. Pri výmene kľúča s prekrytím obsahuje hlavička dva podpisy oddelené medzerou, jeden pre každý kľúč, a stačí, aby sa zhodoval jeden.

Pravidlá, ktoré musí každý overovač dodržať

Section titled “Pravidlá, ktoré musí každý overovač dodržať”

Podpis počítajte zo surového tela presne tak, ako prišlo, pred parsovaním JSON, lebo opätovný zápis JSON zmení bajty. Odmietnite doručenia, ktorých webhook-timestamp je od vašich hodín vzdialený viac ako päť minút, čím zastavíte opakované posielanie zachytených správ. Podpisy porovnávajte funkciou s konštantným časom, nikdy cez ==. Udalosť s id, ktoré ste už spracovali, ignorujte. Knižnice z nasledujúcich častí robia prvé tri body za vás.

Nainštalujte standardwebhooks a express (npm install standardwebhooks express). Cesta musí dostať surové telo, preto pre ňu použite express.raw, nie express.json. Funkcii verify dajte Buffer a objekt hlavičiek, pri akomkoľvek probléme vyhodí chybu a pri úspechu vráti rozparsovanú obálku.

import express from "express";
import { Webhook } from "standardwebhooks";
const wh = new Webhook(process.env.DAKOTY_WEBHOOK_SECRET); // whsec_...
const app = express();
app.post("/dakoty/webhook", express.raw({ type: "application/json" }), (req, res) => {
let event;
try {
event = wh.verify(req.body, req.headers);
} catch {
return res.status(400).send("invalid signature");
}
res.sendStatus(200); // acknowledge first, process afterwards
console.log("verified", event.id, event.type);
});
app.listen(Number(process.env.PORT) || 3000);

Nainštalujte ju cez pip install standardwebhooks flask. Telo čítajte cez request.get_data(), ktoré vracia surové bajty, a hlavičky odovzdajte ako slovník. Knižnica názvy hlavičiek sama zmení na malé písmená.

import os
from flask import Flask, request
from standardwebhooks.webhooks import Webhook, WebhookVerificationError
wh = Webhook(os.environ["DAKOTY_WEBHOOK_SECRET"]) # whsec_...
app = Flask(__name__)
@app.post("/dakoty/webhook")
def dakoty_webhook():
try:
event = wh.verify(request.get_data(), dict(request.headers))
except (WebhookVerificationError, ValueError):
return "invalid signature", 400
print("verified", event["id"], event["type"])
return "", 200
if __name__ == "__main__":
app.run(port=int(os.environ.get("PORT", "3000")))

Nainštalujte ju cez composer require standard-webhooks/standard-webhooks. Surové telo čítajte z php://input a knižnici dajte názvy hlavičiek malými písmenami, lebo getallheaders() ich vracia v pôvodnej veľkosti.

<?php
require __DIR__ . '/vendor/autoload.php';
use StandardWebhooks\Webhook;
use StandardWebhooks\Exception\WebhookVerificationException;
$payload = file_get_contents('php://input'); // raw body, before any JSON parsing
$headers = [];
foreach (getallheaders() as $name => $value) {
$headers[strtolower($name)] = $value;
}
try {
$wh = new Webhook(getenv('DAKOTY_WEBHOOK_SECRET')); // whsec_...
$event = $wh->verify($payload, $headers);
} catch (WebhookVerificationException $e) {
http_response_code(400);
exit('invalid signature');
}
http_response_code(200); // acknowledge first, process afterwards
error_log('verified ' . $event['id'] . ' ' . $event['type']);

Ak nemôžete pridať závislosť, táto funkcia urobí celú kontrolu s vstavaným modulom crypto. Odovzdajte surový Buffer, nie rozparsovaný objekt, a objekt hlavičiek s malými písmenami, aký dáva req.headers v Node.

import { createHmac, timingSafeEqual } from "node:crypto";
export function verifyDakoty(secret, rawBody, headers, toleranceSeconds = 300) {
const id = headers["webhook-id"];
const timestamp = Number(headers["webhook-timestamp"]);
const signatures = headers["webhook-signature"];
if (!id || !signatures || !Number.isFinite(timestamp)) return false;
if (Math.abs(Date.now() / 1000 - timestamp) > toleranceSeconds) return false;
const key = Buffer.from(secret.replace(/^whsec_/, ""), "base64");
const expected = createHmac("sha256", key)
.update(`${id}.${timestamp}.`)
.update(rawBody)
.digest();
return signatures.split(" ").some((part) => {
const [version, signature] = part.split(",");
if (version !== "v1" || !signature) return false;
const received = Buffer.from(signature, "base64");
return received.length === expected.length && timingSafeEqual(received, expected);
});
}

Každé doručenie nesie aj X-Dakoty-Signature: t=<sekundy Unixu>,v1=<hex>, aby fungovali integrácie napísané podľa starších pokynov. Hodnota hex je HMAC-SHA256 nad <t>.<surové telo>. Kľúčom je samotný tajný reťazec v UTF-8 vrátane predpony whsec_ a nedekóduje sa z base64. Webhook so štítkom starší formát (staršie webhooky objednávok) posiela iba túto hlavičku. Nový kód má uprednostniť webhook-signature. Starší webhook prepnite v aplikácii na nový formát a súčasne upravte príjemcu, lebo sa mení telo správy.

Kontrola X-Dakoty-Signature s rovnakými pravidlami: surové telo, tolerancia päť minút a porovnanie v konštantnom čase.

import { createHmac, timingSafeEqual } from "node:crypto";
export function verifyLegacy(secret, rawBody, header, toleranceSeconds = 300) {
const parts = Object.fromEntries(header.split(",").map((p) => p.split("=")));
const timestamp = Number(parts.t);
if (!parts.v1 || !Number.isFinite(timestamp)) return false;
if (Math.abs(Date.now() / 1000 - timestamp) > toleranceSeconds) return false;
const expected = createHmac("sha256", secret) // the full secret string, whsec_ prefix included
.update(`${timestamp}.`)
.update(rawBody)
.digest("hex");
const a = Buffer.from(parts.v1);
const b = Buffer.from(expected);
return a.length === b.length && timingSafeEqual(a, b);
}

Rovnaká kontrola v Pythone. Ako raw_body odovzdajte request.get_data() a ako header hodnotu hlavičky.

import hashlib
import hmac
import time
def verify_legacy(secret: str, raw_body: bytes, header: str, tolerance: int = 300) -> bool:
try:
parts = dict(p.split("=", 1) for p in header.split(","))
timestamp = int(parts["t"])
received = parts["v1"]
except (KeyError, ValueError):
return False
if abs(time.time() - timestamp) > tolerance:
return False
signed = f"{timestamp}.".encode() + raw_body
expected = hmac.new(secret.encode(), signed, hashlib.sha256).hexdigest()
return hmac.compare_digest(received, expected)

Rovnaká kontrola v PHP. Ako $rawBody odovzdajte obsah php://input a ako $header hodnotu $_SERVER['HTTP_X_DAKOTY_SIGNATURE'].

<?php
function verify_legacy(string $secret, string $rawBody, string $header, int $tolerance = 300): bool
{
$parts = [];
foreach (explode(',', $header) as $part) {
[$name, $value] = array_pad(explode('=', $part, 2), 2, '');
$parts[$name] = $value;
}
if (!isset($parts['t'], $parts['v1']) || !ctype_digit($parts['t'])) {
return false;
}
if (abs(time() - (int) $parts['t']) > $tolerance) {
return false;
}
$expected = hash_hmac('sha256', $parts['t'] . '.' . $rawBody, $secret);
return hash_equals($expected, $parts['v1']);
}

Projekt Standard Webhooks zverejňuje tento testovací vektor, voči ktorému je podpisovač Dakoty overený. Pre tajný kľúč whsec_MfKQ9r8GKYqrTwjUPD8ILPZIo2LaLaSw, webhook-id msg_p5jXN8AQM9LWM0D4loKWxJek, webhook-timestamp 1614265330 a telo {"test": 2432232314} je správny webhook-signature rovný v1,g0hM9SsE+OTPJTGt/tmIKtSyZlE3uFJELVlNIOLJ1OE=. Časová značka je stará, preto kontrolu spustite s vypnutou toleranciou alebo s pevnými hodinami. Živý test urobíte tlačidlom testu pri webhooku v aplikácii alebo vzorom z Webhookov.