Выбор между HMAC и цифровой подписью — самое важное проектное решение в любой схеме аутентификации сообщений, и это вопрос доверия, а не скорости. Тег HMAC вычисляют и проверяют одним и тем же секретным ключом, поэтому любой, кто может проверить тег, может и подделать его. Подпись создают закрытым ключом, а проверяют открытым, поэтому проверяющие стороны не могут создавать новые сообщения с верной подписью.
Коротко о главном
| HMAC (RFC 2104, FIPS 198-1) | Цифровая подпись | |
|---|---|---|
| Ключи | Один общий секрет | Закрытый ключ подписывает, открытый проверяет |
| Кто может создать верный тег или подпись | Любой, у кого есть секрет, включая каждую проверяющую сторону | Только владелец закрытого ключа |
| Неотказуемость (доказуемая для третьих сторон) | Нет | Да, если привязка ключа к подписанту заслуживает доверия |
| Распространение ключей | Секрет нужно конфиденциально доставить каждой проверяющей стороне | Открытый ключ можно опубликовать (JWKS, сертификат, DNS) |
| Размер доказательства | 32 байта для HMAC-SHA256 | 64 байта (Ed25519, ECDSA P-256), 256–512 байт (RSA) |
| Скорость на одном ядре Apple M4 Max | Около 7 миллионов тегов в секунду для сообщений по 256 байт (OpenSSL 3.6.5) | Около 60 000 подписей Ed25519 или 87 000 подписей P-256 в секунду |
| Типичное применение | Вебхуки, сессионные cookie, AWS SigV4, TOTP, JWT с HS256, MAC записей TLS 1.2 | TLS-сертификаты и рукопожатия, ID-токены OIDC, подпись пакетов, Git, ключи доступа |
| Постквантовая стойкость | Сохраняется с SHA-256 (алгоритм Гровера лишь вдвое снижает стойкость к поиску прообраза) | Классические схемы взламываются алгоритмом Шора; нужна ML-DSA или аналогичная схема |
Пропускная способность HMAC измерена командой openssl speed -hmac sha256 (около 1,83 ГБ/с на входных данных по 256 байт), скорость подписи — командой openssl speed.
Всё решает модель доверия
HMAC — это хеш с ключом: H((K ⊕ opad) ‖ H((K ⊕ ipad) ‖ m)). Его цель с точки зрения безопасности в том, чтобы никто без ключа не мог создать верный тег. Этого достаточно, когда ключ есть ровно у двух сторон и каждая уверена, что другая не станет подделывать теги: тогда верный тег означает «это отправила другая сторона». Если сторон три или больше, схема перестаёт работать: любая проверяющая сторона может выдать себя за отправителя перед остальными, а в споре отправитель может заявить, что тег создал получатель.
Цифровая подпись разделяет эти возможности. Опубликованный ключ проверки подписи никому не помогает подписывать, поэтому один издатель может обслуживать тысячи проверяющих сторон, а подпись можно предъявить аудитору или суду как доказательство происхождения, если ключ был привязан к подписанту сертификатом, каталогом ключей или записью об аккаунте. Именно из-за этого разделения токены удостоверения, которые принимают многие сервисы (ID-токены OpenID Connect, токены доступа, которые проверяют серверы ресурсов), защищают подписями, и поэтому же утечка конфигурации проверяющей стороны безвредна при подписях, но катастрофична при HMAC.
На практике
Управление ключами. Секреты HMAC нужно генерировать случайно (не менее 256 бит для HMAC-SHA256; RFC 7518, § 3.2 требует этого для HS256), передавать конфиденциально, хранить у каждой проверяющей стороны и менять везде одновременно или с переходным периодом, в течение которого действительны два секрета. У ключей подписи закрытая половина хранится в одном месте, в идеале в HSM или KMS, а ротация идёт через идентификаторы ключей в опубликованном наборе ключей.
Производительность и размер. HMAC — это два вычисления хеша: миллионы в секунду на одно ядро и 32-байтный тег. Подпись обходится в десятки микросекунд и занимает 64 байта или больше. Для большей части веб-трафика ни то ни другое не важно; это важно при аутентификации каждого пакета или при очень интенсивном обмене внутренними сообщениями, где нормой считаются HMAC или AEAD.
Подводные камни. HMAC нужно сравнивать за постоянное время, его нельзя заменять на hash(key ‖ message) (такую конструкцию ломает атака удлинением сообщения на SHA-256), и он должен аутентифицировать ровно те байты, которые получены (необработанное тело запроса, а не заново сериализованный объект JSON). Подписи нужно проверять алгоритмом, который зафиксировала проверяющая сторона, и никогда — алгоритмом, который указан в самом сообщении.
Скрипт ниже, запущенный в Node.js 26.10, аутентифицирует одно и то же тело вебхука обоими способами.
import { createHmac, generateKeyPairSync, randomBytes, sign, timingSafeEqual, verify } from "node:crypto";
const body = Buffer.from('{"event":"invoice.paid","id":"evt_123"}');
// HMAC: sender and receiver share one secret (in production it comes from a secret store).
const secret = randomBytes(32);
const tag = createHmac("sha256", secret).update(body).digest();
const expected = createHmac("sha256", secret).update(body).digest();
console.log("HMAC-SHA256 tag:", tag.length, "bytes, valid:", timingSafeEqual(tag, expected));
// Signature: sender keeps the private key, receivers hold only the public key.
const { privateKey, publicKey } = generateKeyPairSync("ed25519");
const signature = sign(null, body, privateKey);
console.log("Ed25519 signature:", signature.length, "bytes, valid:", verify(null, body, publicKey, signature));
HMAC-SHA256 tag: 32 bytes, valid: true
Ed25519 signature: 64 bytes, valid: true
Секрет HMAC должен быть у каждой проверяющей стороны, а закрытый ключ Ed25519 никогда не покидает отправителя: распространяется только 32-байтный открытый ключ.
Вебхуки и API на практике
Большинство поставщиков вебхуков используют HMAC, потому что у каждого получателя свой секрет: GitHub отправляет X-Hub-Signature-256: sha256= и за ним HMAC-SHA256 тела в шестнадцатеричном виде, а Stripe подписывает метку времени вместе с телом и передаёт результат в заголовке Stripe-Signature, чтобы получатели могли отклонять повторно отправленные запросы. Раз секрет у каждого получателя уникален, проблема многих сторон не возникает. Вебхуки с подписями на основе HTTP Message Signatures (RFC 9421) или JWS появляются там, где один отправитель общается со многими получателями, у которых не должно быть общего секрета, или где получатели хотят проверять сообщения, не заводя секретов. AWS Signature Version 4 — это подпись запросов на основе HMAC, а OAuth DPoP и Web Bot Auth используют подписи, потому что проверяющая сторона не должна иметь возможности выдать себя за клиента.
Что когда выбирать
- Один и тот же сервис выпускает и проверяет (сессионные cookie, CSRF-токены, внутренние JWT): HMAC-SHA256.
- Один отправитель, один получатель, секрет передан по отдельному каналу (вебхуки для каждого клиента): HMAC с меткой времени в подписываемых данных.
- Один издатель, много проверяющих сторон (ID-токены, публичные API, мобильные приложения, которые проверяют данные сервера): подписи с ключами, опубликованными в JWKS.
- Доказательства для третьих сторон (договоры, журналы аудита, релизы ПО): подписи и доверенные метки времени.
- Клиенты, которые не должны иметь возможности выдавать себя друг за друга (DPoP, аутентификация ботов, аттестация устройств): подписи с отдельным ключом у каждого клиента.
- Очень большой поток сообщений по доверенному каналу: HMAC или AEAD.
Заметки о миграции
Переход с HMAC на подписи обычно происходит, когда появляется вторая проверяющая сторона или утекает секрет.
- Введите идентификатор алгоритма, если в вашем формате его нет (
algв JOSE, параметрalgв HTTP-подписи, префикс версии), чтобы проверяющие стороны могли узнать, какую схему использует сообщение, не доверяя ему слепо. - Опубликуйте открытый ключ с идентификатором ключа (JWKS, каталог ключей для HTTP-подписей) и дайте проверяющим сторонам время его получить.
- На переходный период подписывайте дважды: отправляйте и заголовок с HMAC, и подпись, а проверяющие стороны пусть отдают предпочтение подписи.
- Удалите общий секрет у проверяющих сторон, которым нужно только проверять: в этом и состоит выигрыш в безопасности, поэтому не оставляйте его «на всякий случай».
- Никогда не используйте один ключ в обеих схемах. Открытый ключ RSA, повторно использованный как секрет HMAC, даёт ту самую подмену алгоритма, из-за которой ломались библиотеки JWT; см. HS256 или RS256.