Подпись и верификация запросов
Подпись запроса доказывает подлинность отправителя и целостность содержимого. В отличие от TLS (который защищает канал), прикладная подпись делает каждое сообщение проверяемым и устойчивым к прокси, кэшу и отложенной доставке.
Цели:1. Аутентичность (кто отправил) и целостность (не изменялось).
2. Неповторяемость (защита от реплеев).
3. Отвязка от транспорта (работает поверх HTTP, очередей, вебхуков).
4. Аудитируемость (воспроизводимая проверка спустя месяцы).
1) Модель угроз (минимум)
Подмена тела/заголовков в пути следования.
Реплей (повтор легитимного запроса).
Downgrade/strip заголовков подписи.
Кража секретов интеграции.
Несинхронные часы (clock skew) и длинные очереди.
2) Выбор примитива
HMAC (симметрия): простой и быстрый, ключ хранится у обеих сторон. Подходит для B2B-вебхуков и внутренних API.
RSA/ECDSA (асимметрия): ключ приватный у отправителя, публичный у получателя. Подходит для открытых интеграций и когда важно не делиться секретом.
mTLS: взаимная аутентификация на транспортном уровне; часто комбинируется с HMAC/подписью тела.
JWT/JWS: удобно для bearer-токенов и самодостаточных клеймов; для подписи тела лучше использовать каноникализацию + JWS Detached/HTTP Message Signatures.
HTTP Message Signatures (подпись выбранных частей запроса): современный подход для REST.
Рекомендация: для вебхуков — HMAC + timestamp + nonce + каноникализация тела; для публичного API — HTTP Message Signatures или JWS; при высоких рисках — добавьте mTLS.
3) Каноникализация (что именно подписываем)
Подписывать нужно детерминированную строку, одинаково восстанавливаемую обеими сторонами.
Референсный состав:
method \n path_with_query_normalized \n content-type \n digest: SHA-256=BASE64(SHA256(body)) \n x-ts: <unix iso> \n x-nonce: <uuid> \n host \n x-tenant: <tenant_id> \n
Итоговая строка:
canonical = join("\n", fields)
signature = HMAC(secret, canonical) # или ECDSA_sign(private_key, canonical)
Правила:
- Нормализуйте путь и порядок query-параметров.
- Пробелы/юникод/регистр — фиксируйте (например, lower-case заголовков, trim).
- Большие тела — хешируйте (Digest), а не включайте «как есть».
4) Формат заголовков
Пример для HMAC:
X-Signature-Alg: hmac-sha256
X-Signature: v1=hex(hmac),ts=1730379005,nonce=550e8400-e29b-41d4-a716-446655440000,kid=prov_42
Digest: SHA-256=BASE64(SHA256(body))
X-Tenant: brand_eu
Пример для асимметрии (ECDSA P-256):
Signature: keyId="prov_42", alg="ecdsa-p256-sha256",
ts="2025-10-31T12:30:05Z", nonce="550e...", headers="(request-target) host digest x-tenant",
sig="BASE64(raw_signature)"
Где `kid`/`keyId` позволяет выбрать ключ из реестра (см. ротацию).
5) Верификация на приемной стороне
Псевдокод:python def verify(request):
1) Basic assert abs (now () - request. ts) <= ALLOWED_SKEW # напр., 300 с assert not replayed(request. nonce, window = TTL) # store nonce/ts in KV
2) Restore canonical canonical = build_canonical (
method=request. method,
path=normalize_path(request. path, request. query),
content_type=request. headers["content-type"],
digest=hash_body(request. body),
ts=request. ts,
nonce=request. nonce,
host=request. headers["host"],
tenant=request. headers. get("x-tenant")
)
3) Get the key key = key_registry. get(request. kid) # secret (HMAC) или public key (ECDSA)
4) Verify if request signature. alg. startswith("hmac"):
ok = hmac_compare(key. secret, canonical, request. signature)
else:
ok = asym_verify(key. public, canonical, request. signature)
5) Solution if not ok: return 401, "SIGNATURE_INVALID"
return 200, "OK"
Constant-time сравнение HMAC, хранение `nonce`/`(ts, event_id)` в быстрых KV (TTL ≥ окно доставки).
6) Анти-реплей и окна
Timestamp + Nonce: отклоняйте запросы старше `±Δ` (напр., 5 мин) и повторы nonce в этом окне.
Для вебхуков: используйте стабильный `event_id` и inbox-таблицу — это надежнее, чем только nonce.
Повторная доставка (ретраи) должна использовать те же ts/nonce/event_id, а не генерировать новые.
7) Мульти-тенант и регионы
Храните ключи per tenant/region: `kid = <tenant>:<region>:<key_id>`.
Разделяйте пулы секретов и лимиты; соблюдайте data residency.
В заголовках/каноникализации указывайте `X-Tenant` и регион — это часть проверяемого контекста.
8) Управление ключами и ротация
Реестр ключей (KMS/Vault): `kid`, тип, алгоритм, статус (`active`, `deprecating`, `retired`), `valid_from/valid_to`.
Dual-secret: держите одновременно текущий и следующий ключ (приемник принимает оба).
Ротация по расписанию и по событию (компрометация).
Key pinning (по возможности) и ограничение доступа к материалам ключей.
Логи доступа к ключам и действиям с ними.
9) Сочетание с mTLS и OAuth
mTLS проверяет канал и «кто вы» на уровне сертификата.
Подпись защищает сообщение (полезна через прокси/кеши/очереди).
OAuth/JWT дополняет аутентификацию/авторизацию, но сам по себе не гарантирует целостность тела (если его не подписывать в каноникализации).
Лучшие практики: mTLS + подпись тела (Digest) + HMAC/ECDSA + короткий `ts`-интервал.
10) Ошибки и коды ответов
`401 SIGNATURE_INVALID` — неверная подпись/алгоритм.
`401 KEY_REVOKED` — `kid` недействителен/просрочен.
`400 TIMESTAMP_OUT_OF_RANGE` — часы/окно.
`409 NONCE_REPLAYED` — обнаружен повтор.
`400 DIGEST_MISMATCH` — тело изменено.
`415 UNSUPPORTED_ALGORITHM` — неразрешенный `alg`.
`429 TOO_MANY_ATTEMPTS` — троттлинг по ключу/тенанту.
Пинайте точную причину в машинно-читаемом `error_code`; не возвращайте секреты/каноникализацию «как есть».
11) Наблюдаемость и аудит
Метрики:- `verify_p95_ms`, `verify_error_rate`, `digest_mismatch_rate`, `replay_blocked_rate`, `alg_usage{hmac,ecdsa}`, `clock_skew_ms`.
- Логи (структурные): `kid`, `alg`, `tenant`, `region`, `ts`, `nonce`, `digest_hash`, `decision`, `reason`.
- Трейсинг: атрибуты `signature.kid`, `signature.alg`, `signature.ts_skew`.
- Аудит: неизменяемый журнал ротаций, использования ключей и флагов допуска.
12) Производительность
Хэшируйте тело стримингом (не держите в памяти).
Кэшируйте публичные ключи по `kid` с коротким TTL и инвалидацией по событию.
На edge/gateway выносите предварительные проверки (ts/nonce/формат).
HMAC быстрее ECDSA; ECDSA удобнее для внешних интеграций и «неразделяемых» ключей.
13) Тестирование
Наборы фикстур: одинаковые запросы → одинаковая каноникализация/подпись; «грязные» пробелы/порядок query/заголовков → устойчивы.
Negative: неверный `kid/alg`, модифицированное тело/host, повтор nonce, устаревший ts, clock skew.
Property-based: любые эквивалентные запросы дают одну canonical-строку.
Interop: кросс-языковые проверки (Go/Java/Node/Python).
Chaos: задержки, ретраи, смена ключа «на лету».
14) Плейбуки (runbooks)
1. Всплеск `SIGNATURE_INVALID`
Проверить ротацию ключей, рассинхрон clock, изменения каноникализации у отправителя.
Временно включить `dual-accept` для старого `kid`, уведомить партнера.
2. Рост `REPLAYED`
Увеличить TTL хранения nonce, сверить ретрайнеры у отправителя, проверить clock skew.
Перекрыть злоупотребляющий IP/ASN на edge.
3. `DIGEST_MISMATCH` массово
Проверить прокси/компрессию/перезапись заголовков; зафиксировать версию каноникализации.
Отключить посредников, нарушающих тело/заголовки.
4. Компрометация ключа
Немедленно revoke `kid`, перевести на `next_kid`, регенерировать все секреты/токены, аудит доступа.
15) Типичные ошибки
Подписывать «часть тела» или JSON без фиксации порядка → уязвимость к перестановке полей.
Отсутствие `Digest` → прокси может изменить тело незаметно.
Длинное окно `ts` без nonce → открыт реплей.
Хранить секреты в переменных окружения без KMS/Vault.
Сравнивать подпись не constant-time.
Игнорировать `host`/`path` в каноникализации → атаки на переадресацию.
Смешивать `kid` разных тенантов и регионов.
16) Чек-лист перед продом
- Определен формат каноникализации (method, path+query, content-type, Digest, ts, nonce, host, tenant).
- Реализованы HMAC/ECDSA с `kid`, реестр ключей и dual-secret.
- Включены анти-реплей (nonce+ts) и хранение inbox/event_id для вебхуков.
- Настроены коды ошибок/политика ретраев и троттлинг per tenant/key.
- Наблюдаемость: метрики verify, логи, трассировка, алерты на всплески.
- Ротация ключей автоматизирована; аудит и права доступа ограничены.
- Тест-наборы на каноникализацию и межъязыковую совместимость.
- Документация для интеграторов с примером на 3–4 языках и фикстурами.
- mTLS включен для чувствительных интеграций; JWT используется только как дополнение, не замена подписи тела.
Заключение
Подпись и верификация запросов — это не «один заголовок», а дисциплина: четкая каноникализация, короткие окна времени, анти-реплей, ротация ключей и наблюдаемость. Постройте единый стандарт для всех интеграций (API и вебхуки), используйте `kid`/KMS, принимайте два ключа при ротации, и ваши контуры станут устойчивыми к подменам, предсказуемыми и удобными для аудита.