Перейти к содержимому
Последнее обновление

Аутентификация

Это руководство описывает аутентификацию в Vilna API, верификацию подписей вебхуков и защиту учётных данных.

Аутентификация по API-ключу

Каждый запрос к Vilna API должен содержать API-ключ в заголовке X-Api-Key.

Получение API-ключа

  1. Зарегистрируйтесь и создайте учётную запись.
  2. Создайте рабочее пространство и проект.
  3. Сгенерируйте API-ключ в настройках проекта с нужными разрешениями.

API-ключи привязаны к проекту. RPC-ключи и управляющие ключи привязаны к рабочему пространству - создавайте их в настройках рабочего пространства. См. Личный кабинет.

Использование ключа в запросах

curl "https://api.vilna.io/v1/blockchains" \
  -H "X-Api-Key: your-api-key"

Если ключ отсутствует или недействителен, API возвращает ошибку в формате RFC 7807:

{
  "type": "https://docs.vilna.io/errors/unauthorized",
  "title": "Unauthorized",
  "status": 401,
  "detail": "Invalid or missing API key."
}

Типы API-ключей

Vilna использует три типа API-ключей, каждый из которых предназначен для определённого сервиса и области действия.

ТипПрефиксОбласть действияНазначение
API-ключvilna_api_ПроектДоступ к Platform API (адреса, транзакции, балансы)
RPC-ключvilna_rpc_Рабочее пространствоДоступ к блокчейн-нодам через Vilna RPC
Управляющий ключvilna_mgt_Рабочее пространствоManagement API (пространства, проекты, участники, ключи)

API-ключи привязаны к проекту. Каждый ключ связан с конкретным проектом в рабочем пространстве и может обращаться только к ресурсам этого проекта. Если у вас несколько проектов, для каждого нужен отдельный API-ключ.

RPC-ключи и управляющие ключи действуют на уровне рабочего пространства. Они работают со всеми проектами в пространстве. RPC-ключ предоставляет доступ к эндпоинтам блокчейн-нод, а управляющий ключ позволяет программно управлять пространствами, проектами, участниками и другими ключами.

Каждый тип ключа имеет собственный набор разрешений, определяющих доступные операции. Например, API-ключ может иметь разрешение api:address:read, но не api:address:write. Полная матрица разрешений доступна в справочнике по авторизации.

Все три типа ключей передаются в одном заголовке X-Api-Key. Шлюз определяет тип ключа по префиксу и автоматически направляет запрос в нужный сервис.

Длина ключа составляет 40 символов: префикс типа, 24 случайных символа base62 и 6 контрольных символов CRC32. Например:

vilna_api_aBcDeFgHiJkLmNoPqRsTuVwX123456

Создавать и управлять ключами можно через Management API или панель управления Vilna.

Верификация подписей вебхуков

Когда Vilna доставляет вебхук-событие, запрос содержит заголовки, позволяющие проверить подлинность, защититься от атак воспроизведения и отбросить повторные доставки.

Заголовки вебхука

ЗаголовокОписание
X-Webhook-SignatureHMAC-SHA256 подпись в формате Stripe: t=<unix-секунды>,v1=<hex> - временная метка передаётся внутри этого заголовка как t=
X-Webhook-EventИмя события в dot-нотации: transaction.detected или transaction.confirmed. Тестовые доставки используют то же значение - различайте их по полю is_test_message в теле.
X-Webhook-Event-IdUUIDv5 - стабилен между повторными доставками, используется для дедупликации
X-Webhook-Delivery-IdUUID - меняется при каждой попытке, нужен только для корреляции с поддержкой

Секрет подписи вебхуков

При создании вебхук-канала через POST /channels ответ содержит поле webhook_secret - 42-символьную строку в открытом виде вида vilna_whsec_ + 30 символов base62. Это ваш секрет подписи - сохраните его сразу в менеджере секретов. Он возвращается только один раз при создании и один раз при ротации через POST /channels/{channel_id}/webhook-secrets/actions/rotate.

Секрет показывается один раз

Секрет вебхука уникален для каждого канала уведомлений и возвращается только при создании и при явной ротации. В последующих ответах он не показывается. Сохраните его в менеджере секретов сразу.

Два эндпоинта позволяют посмотреть и удалить секреты канала, не раскрывая плейнтекст:

  • GET /channels/{channel_id}/webhook-secrets возвращает маску текущего секрета и временные метки, а также метаданные предыдущего секрета, если идёт 24-часовое окно ротации.
  • DELETE /channels/{channel_id}/webhook-secrets/previous досрочно завершает окно совмещения. После этого исходящие вебхуки подписываются только текущим секретом.

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

Заголовок X-Webhook-Signature имеет формат:

t=1714262800,v1=5257a8696bc5...

В период ротации секрета в заголовке одновременно присутствуют две записи v1=:

t=1714262800,v1=5257a8696bc5...,v1=8a7d4f3e1c9b...

Подписываемый payload - это ${t}.${rawBody}: значение временной метки из заголовка, литеральная точка и сырые байты тела запроса.

import crypto from "node:crypto";

function verifyWebhookSignature(
  rawBody: Buffer,
  signatureHeader: string,
  secret: string
): boolean {
  // Парсинг заголовка: t=...,v1=...[,v1=...]
  const parts = signatureHeader.split(",");
  const tEntry = parts.find((p) => p.startsWith("t="));
  const v1Values = parts
    .filter((p) => p.startsWith("v1="))
    .map((p) => p.slice(3));

  if (!tEntry || v1Values.length === 0) return false;

  const t = tEntry.slice(2);

  // Отклоняем события вне 5-минутного окна (защита от воспроизведения)
  const now = Math.floor(Date.now() / 1000);
  if (Math.abs(now - parseInt(t, 10)) > 300) return false;

  // Вычисляем ожидаемый HMAC над сырыми байтами - никогда не пересериализуйте распарсенный JSON
  const payload = Buffer.concat([
    Buffer.from(t),
    Buffer.from("."),
    rawBody,
  ]);
  const expected = crypto
    .createHmac("sha256", secret)
    .update(payload)
    .digest("hex");

  const expectedBuf = Buffer.from(expected, "utf8");

  // Подпись валидна, если совпадает любое значение v1= (покрывает окно ротации)
  return v1Values.some((v1) => {
    if (v1.length !== expected.length) return false;
    return crypto.timingSafeEqual(Buffer.from(v1, "utf8"), expectedBuf);
  });
}

Дедупликация событий

event_id присутствует и в теле, и в заголовке X-Webhook-Event-Id. Копия в теле покрывается HMAC и защищена от подделки; копия в заголовке нужна для маршрутизации и фильтрации без парсинга тела. Любая из них подходит как ключ дедупликации.

// После проверки подписи парсим тело
const payload = JSON.parse(rawBody.toString("utf8"));
const eventId = payload.event_id; // совпадает с заголовком X-Webhook-Event-Id

if (await db.webhookEvents.exists(eventId)) {
  return res.status(200).send("duplicate, skipped");
}

await db.webhookEvents.insert(eventId);
// ... обработка события

Обязательные требования к клиенту

Функция верификации выше уже покрывает большинство пунктов; этот список нужен, чтобы вы могли свериться с ним при аудите интеграции.

  1. Используйте сравнение, защищённое от атак по времени (crypto.timingSafeEqual, hmac.compare_digest или эквивалент). Обычное === утекает количество совпавших символов.
  2. Подписывайте сырые байты запроса. В Express это express.raw({ type: "application/json" }); в других фреймворках читайте буфер тела до любого парсинга JSON. Пересериализованный распарсенный объект побайтово не совпадёт.
  3. Дедуплицируйте по event_id. При повторных доставках значение то же.
  4. Проверяйте окно временной метки. Отклоняйте события вне ±300 секунд. Не ставьте окно в ноль - расхождение часов между серверами стоит вам легитимных доставок.
  5. Игнорируйте неизвестные версии подписи. Обрабатывайте только v1=. Будущие версии (v2= и т.д.) будут приходить рядом с v1=, а не вместо него. Отклоняйте доставку только если ни одна известная версия не проходит.
  6. Ротируйте секрет при изменениях в команде. Когда человек с доступом к секрету уходит, ротируйте сразу - 24-часовое окно совмещения позволяет выкатить новый без простоя, а после обновления всех получателей окно можно закрыть досрочно. Эндпоинты см. выше в разделе Секрет подписи вебхуков.

Почему HMAC, а не асимметричные подписи?

Частый вопрос: почему Vilna использует общий HMAC-секрет, а не подписывает вебхуки асимметричным приватным ключом (Ed25519, RSA), выдавая публичный ключ для проверки?

HMAC - индустриальный стандарт для аутентификации вебхуков: Stripe, GitHub, Twilio, Slack и спецификация Standard Webhooks по умолчанию используют HMAC. Причины практические:

  • Производительность. HMAC-SHA256 в 10-100 раз быстрее, чем Ed25519 или RSA. На масштабе доставки вебхуков это заметно влияет на задержку и стоимость инфраструктуры.
  • Простота эксплуатации. У общего секрета нет проблемы распространения ключей, нет эндпоинта для публикации публичного ключа и нет сложной хореографии ротации, как с сертификатами. Вы получаете одну строку при создании канала и сохраняете её.
  • Привычность для разработчиков. Код проверки тот же, что ваша команда уже пишет для других провайдеров вебхуков - никаких новых криптографических примитивов.

Компромисс в том, что HMAC не даёт невозможности отказа от авторства: тот, кто владеет общим секретом, может произвести валидную подпись, поэтому одна подпись не доказывает, что запрос отправлен именно Vilna и только Vilna. Это становится проблемой ровно в одном сценарии: злонамеренный инсайдер на вашей стороне (например, бывший сотрудник, сохранивший секрет) посылает поддельный запрос на ваш же вебхук-эндпоинт, а вы доверяете ему как источнику истины.

Правильная защита - никогда не считать вебхук источником истины. Вебхуки - это уведомления; ваш обработчик должен повторно проверить событие через Vilna API или напрямую в блокчейне, прежде чем выполнить необратимое действие (выпуск средств, отметку счёта оплаченным, выдачу доступа). С таким подходом поддельный вебхук всё равно не пройдёт повторную проверку, независимо от того, кто сгенерировал подпись.

Если вашему сценарию нужна криптографическая невозможность отказа от авторства (регулируемые финансовые расчёты, формальный арбитраж, подписанные квитанции о доставке), напишите нам - мы добавим Ed25519 как дополнительную схему подписи рядом с v1= (как v2= в том же заголовке X-Webhook-Signature), не ломая HMAC-путь.

Лучшие практики безопасности

Никогда не раскрывайте ключи во фронтенд-коде. API-ключи предоставляют полный доступ к ресурсам вашего проекта. Храните их только на серверной стороне.

Используйте переменные окружения. Храните учётные данные в переменных окружения или менеджере секретов, а не в исходном коде.

export VILNA_API_KEY="your-api-key"
const client = createVilnaClient({
  apiKey: process.env.VILNA_API_KEY!,
});

Регулярно ротируйте ключи. Если ключ скомпрометирован, немедленно отзовите его через личный кабинет или Management API и создайте новый.

Права фиксируются при создании. Каждый ключ выпускается с неизменяемым набором прав. Эндпоинта для изменения прав уже выпущенного ключа нет — если нужно расширить или сузить доступ, отзовите ключ и создайте новый с нужным набором прав.

Используйте отдельные ключи для каждого окружения. Используйте разные API-ключи для разработки, стейджинга и продакшна, чтобы утечка в одном окружении не затронула остальные.

Ограничивайте сетевой доступ. По возможности добавьте в белый список IP-адреса, с которых ваш сервер обращается к Vilna API.

Ответы об ошибках

Все ошибки аутентификации и авторизации соответствуют формату RFC 7807 application/problem+json:

СтатусЗначение
401Отсутствующий или недействительный API-ключ
403Действительный ключ, но недостаточно прав
{
  "type": "https://docs.vilna.io/errors/forbidden",
  "title": "Forbidden",
  "status": 403,
  "detail": "Your API key does not have access to this resource."
}

Полная информация об обработке ошибок доступна в Platform API.

Дополнительные материалы