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

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

Кожну доставку підписано секретом ендпоінта. Перевіряйте підпис, перш ніж обробляти запит, щоб ніхто, хто дізнався 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>.<необроблене тіло> байт у байт із секретом ендпоінта як ключем, записаним у hex малими літерами. 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. Розгорніть новий секрет в отримувача протягом цього періоду. Після нього підписує лише новий секрет.