Локализовано с помощью ИИ

Подпись запроса

Каждый webhook-запрос от Emailit содержит заголовок X-Emailit-Signature с HMAC-SHA256 подписью и заголовок X-Emailit-Timestamp с Unix-временной меткой момента подписания запроса. Вам следует проверять эту подпись, чтобы убедиться в подлинности запроса и отсутствии несанкционированных изменений.

Принцип работы

  1. При создании webhook'а Emailit генерирует секретный ключ подписи для данной конечной точки
  2. Для каждой доставки Emailit объединяет временную метку и исходное тело запроса в формате {timestamp}.{rawBody}
  3. Вычисляется HMAC-SHA256 хеш этой строки с использованием вашего секретного ключа подписи
  4. Подпись отправляется в заголовке X-Emailit-Signature, а временная метка — в X-Emailit-Timestamp
  5. Ваш сервер пересчитывает подпись и сравнивает её со значением в заголовке

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

Для проверки подписи webhook'а:

  1. Извлеките заголовки X-Emailit-Signature и X-Emailit-Timestamp из запроса
  2. Объедините временную метку и исходное тело запроса в формате {timestamp}.{rawBody}
  3. Вычислите HMAC-SHA256 хеш этой строки, используя ваш секретный ключ подписи webhook'а
  4. Сравните вычисленный хеш с подписью, используя безопасное по времени сравнение
  5. При необходимости отклоняйте запросы с временными метками старше нескольких минут для защиты от атак повторного воспроизведения

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

import crypto from 'crypto';

function verifyWebhookSignature(rawBody, signature, timestamp, secret) {
  const signedPayload = `${timestamp}.${rawBody}`;
  const computed = crypto
    .createHmac('sha256', secret)
    .update(signedPayload, 'utf8')
    .digest('hex');

  return crypto.timingSafeEqual(
    Buffer.from(computed, 'hex'),
    Buffer.from(signature, 'hex')
  );
}

// В обработчике webhook'а:
const rawBody = req.body; // исходное тело запроса как строка
const signature = req.headers['x-emailit-signature'];
const timestamp = req.headers['x-emailit-timestamp'];
const secret = process.env.WEBHOOK_SIGNING_SECRET;

// Защита от атак повторного воспроизведения (допуск 5 минут)
const age = Math.floor(Date.now() / 1000) - parseInt(timestamp, 10);
if (age > 300) {
  return res.status(401).send('Запрос слишком старый');
}

if (!verifyWebhookSignature(rawBody, signature, timestamp, secret)) {
  return res.status(401).send('Неверная подпись');
}

Важные замечания

  • Подпись вычисляется для строки {timestamp}.{rawBody} — всегда используйте исходную строку тела запроса, а не повторно сериализованную версию (например, JSON.stringify(req.body)). Различия в пробелах или порядке ключей нарушат подпись.
  • Всегда используйте функцию безопасного по времени сравнения (например, crypto.timingSafeEqual, hmac.compare_digest, hash_equals) для предотвращения атак по времени.
  • Рассмотрите возможность отклонения запросов, где X-Emailit-Timestamp старше нескольких минут, для защиты от атак повторного воспроизведения.
  • Храните секретный ключ подписи в безопасности — используйте переменные окружения, а не исходный код.
  • Секретный ключ подписи webhook'а можно найти в панели управления Emailit в настройках webhook'а или через API Webhooks.