Перейти до вмісту

Перевірка підписів

Кожна доставка підписана секретом ендпоінта. Перевіряйте підпис, перш ніж діяти за запитом, щоб ніхто, хто дізнався URL вашого ендпоінта, не міг надсилати вам події.

Доставка може нести два незалежні підписи того самого тіла. Які з них надсилати, вирішує signatureScheme ендпоінта:

signatureScheme Які заголовки надсилаються
BOTH (типово) X-Signature і заголовки Standard Webhooks.
LEGACY Лише X-Signature.
STANDARD Лише webhook-id, webhook-timestamp, webhook-signature.

Отримувач ігнорує заголовки, яких не читає, тож перевіряйте будь-який із двох. Звужуйте ендпоінт до однієї схеми лише тоді, коли іншу вже ніхто не читає.

Заголовки кожної доставки

Section titled “Заголовки кожної доставки”
Заголовок Значення
X-Signature t=<unix-ms>,v1=<hex>. Під час ротації секрету може нести другий v1.
X-Timestamp Те саме значення, що й t: коли підписано цю спробу, у мілісекундах.
X-Event-Id Подія. Той самий ідентифікатор отримує кожен підписаний ендпоінт.
X-Delivery-Id Ця доставка: одна подія на один ендпоінт. Не змінюється між спробами.
X-Sequence-Number Позиція цієї доставки, коли порядок увімкнено, і 0, коли вимкнено.
Idempotency-Key Не змінюється між спробами, тож можна відкинути вже оброблене.
webhook-id Ідентифікатор доставки. Входить до того, що покриває підпис Standard Webhooks.
webhook-timestamp Коли підписано цю спробу, у секундах.
webhook-signature v1,<base64>, через пробіл, якщо значень кілька.

v1 — це HMAC-SHA256 від точного рядка <t>.<сире тіло> із ключем-секретом ендпоінта, у шістнадцятковому вигляді малими літерами. t — у мілісекундах. SDK Railhook відхиляють мітку часу, що відрізняється від поточного часу більш ніж на 300 секунд.

import express from 'express';
import { constructEvent } from '@railhook/node';
const app = express();
// express.raw, not express.json: the signature covers the bytes that arrived.
app.post('/webhooks', express.raw({ type: 'application/json' }), (req, res) => {
try {
const event = constructEvent(req.body.toString('utf8'), req.headers, process.env.WEBHOOK_SECRET);
// event.data is the parsed body; event.deliveryId is safe to deduplicate on.
res.sendStatus(200);
} catch {
res.sendStatus(400);
}
});

Та сама перевірка звичайним кодом. Читайте кожен v1 у заголовку, а не лише перший.

import crypto from 'node:crypto';
export function verify(rawBody, header, secret) {
let t;
const candidates = [];
for (const part of header.split(',')) {
const [key, value] = part.split('=', 2);
if (key === 't') t = value;
if (key === 'v1') candidates.push(value);
}
if (!t || candidates.length === 0) return false;
if (Math.abs(Date.now() - Number(t)) > 5 * 60 * 1000) return false;
const expected = Buffer.from(
crypto.createHmac('sha256', secret).update(`${t}.${rawBody}`).digest('hex'),
);
let ok = false;
for (const candidate of candidates) {
const given = Buffer.from(candidate);
if (given.length === expected.length && crypto.timingSafeEqual(given, expected)) ok = true;
}
return ok;
}

Перевірка заголовків Standard Webhooks

Section titled “Перевірка заголовків Standard Webhooks”

Заголовки Standard Webhooks дотримуються опублікованої домовленості, тож їх може перевірити готова бібліотека. Від X-Signature вони відрізняються трьома речами:

  • Підписаний рядок — <webhook-id>.<webhook-timestamp>.<сире тіло>: ідентифікатор входить до нього.
  • webhook-timestamp — у секундах, а не в мілісекундах.
  • Хеш у base64, а не в hex, а ключ — це standardWebhooksSecret ендпоінта у формі whsec_<base64>.

API повертає standardWebhooksSecret поруч із secret під час створення та ротації. SDK приймають і сирий секрет та використовують його як є, тож передана не та форма зазнає невдачі як неправильний підпис, а не як помилка, що називає форму.

import express from 'express';
import { verifyStandardWebhook } from '@railhook/node';
app.post('/webhooks', express.raw({ type: 'application/json' }), (req, res) => {
try {
verifyStandardWebhook(req.body.toString('utf8'), req.headers, process.env.RAILHOOK_WHSEC);
} catch {
return res.sendStatus(400);
}
const data = JSON.parse(req.body.toString('utf8'));
return res.sendStatus(204);
});

Ротація секрету без збоїв у отримувача

Section titled “Ротація секрету без збоїв у отримувача”
  1. Викличте POST /api/v1/projects/{projectId}/endpoints/{id}/rotate-secret. Відповідь містить новий secret і standardWebhooksSecret.

  2. Протягом пільгового періоду ендпоінта, типово 24 години, кожну доставку підписують і новим, і старим секретом. X-Signature несе по одному v1 на кожен, а webhook-signature — по одному значенню через пробіл на кожен:

    X-Signature: t=1735689600000,v1=8f2a41c7…new,v1=3b90de55…retired
  3. Розгорніть новий секрет в отримувача будь-коли в межах цього вікна. Після його завершення підписує лише новий секрет.