Это руководство описывает аутентификацию в Vilna API, верификацию подписей вебхуков и защиту учётных данных.
Каждый запрос к Vilna API должен содержать API-ключ в заголовке X-Api-Key.
- Зарегистрируйтесь и создайте учётную запись.
- Создайте рабочее пространство и проект.
- Сгенерируйте 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."
}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-Signature | HMAC-SHA256 подпись в формате Stripe: t=<unix-секунды>,v1=<hex> - временная метка передаётся внутри этого заголовка как t= |
X-Webhook-Event | Имя события в dot-нотации: transaction.detected или transaction.confirmed. Тестовые доставки используют то же значение - различайте их по полю is_test_message в теле. |
X-Webhook-Event-Id | UUIDv5 - стабилен между повторными доставками, используется для дедупликации |
X-Webhook-Delivery-Id | UUID - меняется при каждой попытке, нужен только для корреляции с поддержкой |
При создании вебхук-канала через 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);
// ... обработка событияФункция верификации выше уже покрывает большинство пунктов; этот список нужен, чтобы вы могли свериться с ним при аудите интеграции.
- Используйте сравнение, защищённое от атак по времени (
crypto.timingSafeEqual,hmac.compare_digestили эквивалент). Обычное===утекает количество совпавших символов. - Подписывайте сырые байты запроса. В Express это
express.raw({ type: "application/json" }); в других фреймворках читайте буфер тела до любого парсинга JSON. Пересериализованный распарсенный объект побайтово не совпадёт. - Дедуплицируйте по
event_id. При повторных доставках значение то же. - Проверяйте окно временной метки. Отклоняйте события вне ±300 секунд. Не ставьте окно в ноль - расхождение часов между серверами стоит вам легитимных доставок.
- Игнорируйте неизвестные версии подписи. Обрабатывайте только
v1=. Будущие версии (v2=и т.д.) будут приходить рядом сv1=, а не вместо него. Отклоняйте доставку только если ни одна известная версия не проходит. - Ротируйте секрет при изменениях в команде. Когда человек с доступом к секрету уходит, ротируйте сразу - 24-часовое окно совмещения позволяет выкатить новый без простоя, а после обновления всех получателей окно можно закрыть досрочно. Эндпоинты см. выше в разделе Секрет подписи вебхуков.
Частый вопрос: почему 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.