HS256 и RS256: какую подпись JWT выбрать

9 ноября 2026 г.

Настройка аутентификации доходит до строки, где библиотека просит выбрать алгоритм подписи токена, и в выпадающем списке встречаются HS256, RS256, ES256. Выбор выглядит мелочью, а определяет крупные вещи: где живут ключи, кто сможет проверять ваши токены и как пройдёт переезд от монолита к микросервисам. Разберём два главных алгоритма на практике: что стоит за каждым названием, какой выбирать в типовых ситуациях, где проходит граница между «достаточно» и «уже опасно», и как устроена атака на подмену алгоритма, после которой библиотеки стали требовать фиксировать alg явно.

Два семейства: общий секрет и пара ключей

Названия расшифровываются буквально. HS256 это HMAC-SHA-256: подпись считается хеш-функцией, замешанной на секретном ключе, и один и тот же секрет и ставит подпись, и проверяет её. RS256 это RSA-SHA-256: подпись ставится приватным ключом, а проверяется парным ему публичным. Вся практическая разница между алгоритмами вытекает из одного отличия: симметрия против асимметрии.

  • HS256: выпустить и проверить токен может только владелец секрета; если секрет утёк, подделывать подписи может каждый, у кого он оказался, и отличить подделку от настоящей подписи невозможно;
  • RS256: подписывает только владелец приватного ключа, обычно сервер авторизации; проверять может любой, у кого есть публичный ключ, а публичный скрывать не нужно;
  • инфраструктурное следствие: монолит с одной кодовой базой довольствуется HS256, а архитектура, где токены проверяют много независимых сервисов, просит RS256;
  • скоростное следствие: HMAC это два прохода хеш-функции, RSA это модульная арифметика с большими числами, и HS256 дешевле в вычислении на порядки.

HS256: один секрет на всех

Проверка в HS256 сводится к повторному вычислению. Сервер берёт первые две части токена, считает HMAC-SHA-256 со своим экземпляром секрета и сравнивает результат с третьей частью. Совпало: токен выпущен тем, кто знает секрет, и содержимое не менялось по дороге. Как устроена математика HMAC, почему подписи нельзя сравнивать обычным равенством и какой длины должен быть секрет, разобрано в статье про HMAC-подпись API-запросов.

Практическое следствие симметрии: секрет обязан жить везде, где проверяются токены. Каждый проверяющий сервис становится полноправным владельцем секрета с теми же правами, что у эмитента, включая право выпускать токены. Три проверяющих сервиса означают три хранилища одного секрета и три поверхности утечки. Смена скомпрометированного секрета ложится на всех одновременно: эмитент выпускает токены новым секретом, а проверяющий со старым секретом в конфигурации отвергает вполне валидные токены, пока его не обновят.

HMAC-SHA-256: базовая операция HS256
const crypto = require('crypto');
crypto.createHmac('sha256', 'secret').update('hello').digest('hex');

// результат:
88aab3ede8d3adf94d26ab90d3bafd4a2083070c3bcce9c014ee04a443847c0b

Вектор выше воспроизводится в генераторе HMAC: сообщение hello, ключ secret, алгоритм SHA-256. Ровно эту операцию со своим секретом сервер выполняет при проверке каждого токена HS256, только вместо слова hello у него строка из первых двух частей токена, соединённых точкой.

RS256: приватный подписывает, публичный проверяет

Сервер авторизации держит приватный ключ и подписывает им токены. Все проверяющие используют публичный ключ: его можно положить в открытый репозиторий, отдать партнёру, опубликовать по адресу вида /.well-known/jwks.json, это типовой паттерн провайдеров аутентификации. Знание публичного ключа не даёт возможности подписать ни одного токена: подпись требует приватного, и в этом весь смысл схемы.

  • микросервис проверяет токены без общего секрета: достаточно публичного ключа, и компрометация микросервиса не даёт права выпускать токены;
  • внешний потребитель: партнёрский шлюз или сторонний разработчик может проверять ваши токены, не получая от вас никаких секретов;
  • ротация: приватный ключ меняется на эмитенте, а проверяющие подхватывают новый публичный ключ из JWKS автоматически; поле kid в заголовке токена подсказывает, каким из ключей он подписан;
  • локализация инцидента: при утечке приватного ключа проблема ограничена сервером авторизации, проверяющим достаточно обновить ключ.

Сравнение по пунктам

Сведём различия к списку: значение и что оно означает на практике.

  • размер подписи: HS256 укладывается в 43 символа base64url (32 байта), RS256 с ключом 2048 бита занимает 342 символа base64url (256 байт); чем короче токен, тем меньше накладных расходов на каждый запрос и каждый лог;
  • скорость: генерация RSA-подписи дороже HMAC-SHA-256 на порядки, проверка RSA дешевле генерации, но всё равно дороже HMAC; разница становится заметной на нагрузках с большим числом проверок в секунду;
  • кто может проверить: HS256 только держатели секрета, RS256 любой владелец публичного ключа;
  • выдача ключей: секрет HS256 доставляется каждому проверяющему по защищённому каналу, публичный ключ RS256 публикуется открыто;
  • компрометация: утечка секрета HS256 равнозначна утечке права подписи, утечка публичного ключа RS256 не даёт атакующему ничего;
  • смена ключей: HS256 требует обновить секрет на каждом проверяющем, RS256 обновляет один приватный ключ на эмитенте.

Когда HS256 достаточно, а когда нужен RS256

  • монолит или приложение, где выпуск и проверка токена живут в одной кодовой базе под одной командой;
  • два внутренних сервиса под вашим контролем, чья конфигурация раскатывается из одного репозитория;
  • фоновые задачи и очереди: подпись ставит ваш же воркер и проверяет ваш же планировщик;
  • прототип и ранний продукт: пока проверяющий один, асимметрия решает несуществующую проблему.

Критерий простой: если токен выпускает и проверяет одна и та же сторона, HS256 выигрывает простотой. Нет ключевых пар, нет JWKS, нет процедуры хранения приватного ключа: один секрет длиной не менее 32 случайных байтов в хранилище конфигурации, и вопрос закрыт.

Зеркальная картина складывается, как только проверяющих становится несколько и они перестают зависеть друг от друга:

  • токены проверяют несколько сервисов, особенно на разных языках и в разных репозиториях: раздать общий секрет пяти командам значит размножить поверхности утечки, а публичный ключ публикуется один раз;
  • клиентские приложения или внешние разработчики должны иметь возможность проверить токен сами: публичный ключ для этого и существует;
  • аутентификация вынесена в отдельный провайдер: готовые решения вроде Keycloak, Auth0 или AWS Cognito по умолчанию выпускают токены RS256, и потребители берут ключи из JWKS по адресу из конфигурации;
  • плановая ротация ключей без простоя: kid и JWKS закрывают период, когда старые и новые подписи сосуществуют;
  • требование к локализации взлома: веб-сервер, на котором остался только публичный ключ, не может выпустить ни одного токена, сколько бы раз его ни компрометировали.

Проверяем токен на практике

Начнём с чтения. Как устроен токен внутри, что несут три части и почему в payload нельзя класть секреты, разобрано в статье про анатомию JWT. Декодер JWT раскрывает заголовок и payload мгновенно, но подпись он показывает, а не проверяет: проверка требует ключа и выполняется на той стороне, которой токен адресован. Вот токен, подписанный HS256 с ключом secret:

Токен HS256 и его проверка
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJ1c2VyXzQyIiwicm9sZSI6InZpZXdlciIsImV4cCI6MTgwMDAwMDAwMH0.Ci3X_62brDlB-LGPnZwEDiQx0gidMXeY3uMgefb3P8A

// заголовок: {"alg":"HS256","typ":"JWT"}
// payload: {"sub":"user_42","role":"viewer","exp":1800000000}
// exp = 1800000000 это 15 января 2027, 08:00 UTC

const crypto = require('crypto');
const [h, p] = token.split('.');
crypto.createHmac('sha256', 'secret').update(h + '.' + p).digest('base64url');
// 'Ci3X_62brDlB-LGPnZwEDiQx0gidMXeY3uMgefb3P8A' — совпало с третьей частью

Подпись считается от строки из первых двух частей с точкой между ними, и вектор воспроизводим: тот же ключ secret и то же сообщение в генераторе HMAC дадут тот же результат в hex-виде 0a2dd7ffad9bac3941f8b18f9d9c040e2431d2089d317798dee32079f6f73fc0. Подменим одно слово в payload, роль viewer на editor: кодировка payload изменится, и старая подпись перестанет сходиться. Атакующий не досчитает новую подпись, не зная секрета, и на этом держится вся защита.

Подписать собственные токены позволяет генератор JWT: он работает по HS256, и правило то же, что при ручном подсчёте: одинаковые строки заголовка и payload с одинаковым секретом всегда дают одинаковую подпись. В коде проверка выглядит одинаково для обоих алгоритмов, отличается только аргумент-ключ:

Проверка в библиотеке: HS256 и RS256
const jwt = require('jsonwebtoken');

// HS256: вторым аргументом строка-секрет
jwt.verify(token, 'secret', { algorithms: ['HS256'] });

// RS256: вторым аргументом публичный ключ в формате PEM
jwt.verify(token, publicKeyPem, { algorithms: ['RS256'] });

// фиксация алгоритма в параметре algorithms не перестраховка:
// это защита от атаки, о которой ниже

Атака alg confusion: почему alg нельзя верить

Уязвимости семейства alg confusion эксплуатируют одну деталь: проверяющий код читает alg из заголовка токена и подстраивается под пришедшее значение. Разборы 2015 года показали две классические версии атаки.

  1. alg: none. Атакующий снимает третью часть токена, правит payload и ставит в заголовке alg со значением none: подпись не нужна. Библиотеки, доверявшие заголовку, принимали такой токен как подписанный;
  2. RS256 под видом HS256. Атакующий берёт публичный ключ сервера, который публичен по определению, считает от него HMAC, как будто это секрет, и ставит alg: HS256. Проверяющий, послушно переключившийся на HMAC, сравнивает подпись против публичного ключа в роли секрета и принимает подделку: всё, что нужно атакующему для её изготовления, у него уже было.

Защита занимает одну строку: проверяющий не читает alg из токена, а фиксирует ожидаемое значение в конфигурации, algorithms: ['RS256'], и всё остальное отбрасывает ещё до проверки подписи. Современные библиотеки либо требуют явного списка алгоритмов, либо жёстко связывают каждый ключ с одним алгоритмом. Сами HS256 и RS256 тут ни при чём: оба алгоритма корректны, а дыра появляется тогда, когда выбор криптографии делегируется данным от атакующего.

Типичные ошибки

  • секрет HS256 в клиентском коде: фронтенд-бандл или мобильное приложение содержат секрет в собранном файле, и «проверка на клиенте» превращается в раздачу ключа подписи всем пользователям;
  • приватный ключ RS256 в браузере или приложении: приватный по определению живёт только на сервере авторизации, всё остальное это инфраструктура для подделки токенов;
  • секрет HS256 короче 32 байтов или собранный из словарных слов: короткая фраза перебирается словарём, потому что проверка одной догадки стоит микросекунды;
  • доверие к alg из заголовка токена без фиксации списка алгоритмов: открытая дверь для alg confusion из предыдущего раздела;
  • один секрет на все среды: тестовый стенд с продакшн-секретом превращает утечку на стенде в компрометацию боевой системы;
  • сравнение подписи самописным равенством строк вместо функции библиотеки: разница во времени сравнения технически измерима, подробности в статье про HMAC.

Частые вопросы

Можно ли переехать с HS256 на RS256 без простоя?

Можно. Эмитент начинает выпускать RS256-токены, а проверяющие на переходный период фиксируют список algorithms: ['HS256', 'RS256'] и держат оба ключа. Старые HS256-токены доживают до своего exp, обычно до часа, после чего список сужается до одного алгоритма. Важно, что на переходном периоде оба значения зафиксированы явно, а не взяты из заголовка токена.

Что за алгоритмы HS384, HS512, RS384, RS512 и ES256?

Те же семейства с другими хеш-функциями: HS512 это HMAC-SHA-512 с подписью 64 байта, RS512 это RSA с SHA-512; голый хеш SHA-512 без ключа считает генератор SHA-512, но голый хеш подписью не является: ключ в HMAC обязателен. ES256 принадлежит третьему семейству, эллиптическим кривым: при сопоставимой стойкости подпись заметно короче RSA, около 64 байтов. Логика выбора та же, что между HS256 и RS256, различаются только размер и скорость.

Насколько RS256 медленнее HS256?

Генерация RSA-подписи дороже HMAC-SHA-256 на порядки, проверка RSA дешевле генерации, но всё равно заметно дороже HMAC. Точные числа зависят от длины ключа, библиотеки и оборудования, поэтому универсальной цифры не называют. Для типового веб-приложения проверка одного токена на запрос разницы не создаёт; она проявляется на сервисах, проверяющих десятки тысяч токенов в секунду, и там ставят RS256 с кешем публичных ключей и замеряют собственным бенчмарком.

Где хранить приватный ключ RS256?

В хранилище секретов: сервисе управления ключами, переменных окружения с ограниченным доступом или выделенном хранилище конфигурации. Не в репозитории, не в Docker-образе, не в клиентском коде. Доступ к приватному ключу равнозначен праву выпускать токены от имени любого пользователя, поэтому круг людей с доступом держат минимальным, а операции с ключом логируют.

Хватит ли 32 символов для секрета HS256?

Минимум это 32 случайных байта, а не 32 символа осмысленного текста. Строка из словарных слов слабее 16 действительно случайных байтов, потому что перебор идёт по словарю, а не по всем байтовым комбинациям подряд. Правило: возьмите 32-64 байта из криптографического генератора и положите в хранилище секретов; длина без энтропии ничего не гарантирует.

Выбор сводится к одному вопросу: кто проверяет ваши токены. Одна сторона, один сервис: HS256, 32 случайных байта в хранилище секретов, и тема закрыта. Проверяющих несколько и они независимы: RS256, приватный ключ только у эмитента, публичный раздаётся через JWKS. Между схемами можно переехать без простоя, поэтому выбор это точка старта, а не приговор.

Читайте также