Перейти к содержимому
ecdsa.com

HS256 или RS256: JWT с общим секретом или открытым ключом

HS256 — это HMAC: любая проверяющая сторона может и выпускать токены. RS256 — это подпись: у проверяющих сторон есть только открытый ключ. Большинство ошибок при проектировании систем на JWT начинается с того, что эту разницу не замечают.
Что сравниваетсяHS256RS256

Выбор между HS256 и RS256 — первое решение, которое приходится принимать, когда вы начинаете выпускать JWT. Два значения alg выглядят похоже, но HS256 аутентифицирует токены общим секретом (HMAC-SHA256), а RS256 подписывает их закрытым ключом RSA (RSASSA-PKCS1-v1_5 с SHA-256). От этого зависит, кто может создавать токены, как распространяются ключи и что происходит при утечке ключа.

Коротко о главном

HS256RS256
АлгоритмHMAC с SHA-256 (RFC 7518, § 3.2)RSASSA-PKCS1-v1_5 с SHA-256 (§ 3.3)
КлючОдин общий секрет, не менее 256 битПара ключей RSA, не менее 2048 бит
Кто может проверятьВладельцы секретаЛюбой, у кого есть открытый ключ
Кто может выпускать токеныЛюбой владелец секрета, включая проверяющие стороныТолько владелец закрытого ключа
Распространение ключейКонфиденциальная копия для каждой проверяющей стороныОткрытый JWKS (jwks_uri)
Часть с подписью32 байта, 43 символа base64url256 байт, 342 символа (RSA-2048)
Пример токена (замер ниже)187 символов508 символов
Скорость на одном ядре Apple M4 Maxмиллионы HMAC в секундуоколо 3 000 подписей и 114 000 проверок в секунду
Уровень требования в JWAОбязателенРекомендуется
Главное слабое местоСлабый или утёкший секрет: кто угодно может выпускать токеныПодмена алгоритма у нестрогих проверяющих сторон; обращение с ключами

Модель доверия

В HS256 ключ проверки и ключ подписи — одни и те же байты. Любой сервис, который проверяет токены, может и создавать действительные токены для любого пользователя, любой области доступа и любой аудитории, а секрет, утёкший хотя бы из одного такого сервиса (переменная окружения в логе, бандл фронтенда, мобильное приложение), позволяет кому угодно выпускать токены. Для монолита, который сам выпускает и проверяет свои токены сессий, это приемлемо; если же один издатель токенов обслуживает множество API или команд, это серьёзная слабость.

В RS256 издатель хранит закрытый ключ, в идеале в HSM или KMS, и публикует открытый ключ в JWKS по URL, указанному в своих метаданных. Проверяющие стороны загружают набор ключей и выбирают ключ по kid, а сами проверяющие стороны можно добавлять и убирать, ни с кем не делясь секретами. Скомпрометированная проверяющая сторона может читать токены, но не может их подделать. Поэтому поставщики OpenID Connect по умолчанию подписывают ID-токены алгоритмом RS256, а RFC о лучших практиках для JWT (RFC 8725) советует системам с несколькими участниками асимметричные алгоритмы.

При этом OpenID Connect допускает ID-токены HS256 для конфиденциальных клиентов, и ключом для них служит секрет клиента: это работает, потому что каждый клиент проверяет только токены, выпущенные для него самого.

Стойкость секрета и офлайн-перебор

Токен HS256 — идеальный оракул для офлайн-перебора: любой, кто увидел хотя бы один токен, может проверять варианты секрета с той скоростью, с какой его оборудование вычисляет HMAC-SHA256, ни разу не обращаясь к вашему серверу. Короткие, придуманные человеком или заданные по умолчанию секреты («secret», название проекта, пример из учебного руководства) взламываются за секунды распространёнными инструментами для подбора паролей. RFC 7518 требует ключ не короче выхода хеш-функции, то есть 256 бит для HS256, а RFC 8725, § 3.5 указывает, что у ключей должна быть достаточная энтропия. Генерируйте секрет с помощью CSPRNG (openssl rand -base64 32) и никогда не выводите его из пароля.

Для RS256 аналогичной офлайн-атаки на закрытый ключ нет, но RFC 7518 не допускает ключи RSA короче 2048 бит, а файл ключа должен быть защищён так же, как любой закрытый ключ.

Подмена алгоритма: HS256 вместо RS256

Самая известная уязвимость JWT построена на смешении этих двух алгоритмов. Сервис проверяет токены RS256, вызывая библиотеку с открытым ключом RSA в формате PEM. Если библиотека берёт алгоритм из заголовка токена и при этом поддерживает HS256, атакующий отправляет токен с "alg": "HS256", HMAC которого вычислен с байтами PEM открытого ключа в качестве секрета; библиотека использует те же байты, и подделка проходит проверку. Тим Маклин описал эту атаку в 2015 году вместе с атакой через "alg": "none", а RFC 8725, § 2.1 и § 3.1 закрепляют исправление: с каким алгоритмом можно использовать каждый ключ, решает проверяющая сторона, а не токен.

Современные библиотеки этому препятствуют: jsonwebtoken отказывается использовать открытый ключ как секрет HMAC (если в конфигурации их перепутали, появляется ошибка secretOrPublicKey must be an asymmetric key), PyJWT требует аргумент algorithms, а jose привязывает алгоритмы к типам ключей. И всё же всегда передавайте явный список разрешённых алгоритмов, в котором указан ровно тот алгоритм, который вы ожидаете.

Размер и скорость

Скрипт ниже, запущенный в Node.js 26.10, собирает одни и те же утверждения (claims) в токены HS256 и RS256, используя только node:crypto.

Node.js: токены HS256 и RS256 с одной и той же полезной нагрузкой
import { createHmac, generateKeyPairSync, randomBytes, sign } from "node:crypto";

const b64url = (value) => Buffer.from(value).toString("base64url");
const payload = b64url(JSON.stringify({ iss: "https://auth.example.com", sub: "user-42", aud: "api", exp: 1792500000 }));

// HS256: HMAC-SHA256 with a shared 256-bit secret.
const secret = randomBytes(32);
const hsInput = `${b64url(JSON.stringify({ alg: "HS256", typ: "JWT" }))}.${payload}`;
const hs256 = `${hsInput}.${b64url(createHmac("sha256", secret).update(hsInput).digest())}`;

// RS256: RSASSA-PKCS1-v1_5 with SHA-256 and a 2048-bit key.
const { privateKey } = generateKeyPairSync("rsa", { modulusLength: 2048 });
const rsInput = `${b64url(JSON.stringify({ alg: "RS256", typ: "JWT", kid: "2026-10" }))}.${payload}`;
const rs256 = `${rsInput}.${b64url(sign("sha256", Buffer.from(rsInput), privateKey))}`;

console.log("HS256 token:", hs256.length, "characters, signature part:", hs256.split(".")[2].length);
console.log("RS256 token:", rs256.length, "characters, signature part:", rs256.split(".")[2].length);
Результат
HS256 token: 187 characters, signature part: 43
RS256 token: 508 characters, signature part: 342

Кроме того, заголовок токена RS256 содержит kid, по которому проверяющие стороны выбирают ключ из JWKS. В продакшене используйте поддерживаемую библиотеку JWT, а не собранные вручную токены: здесь важен только размер части с подписью. Тот же компромисс для ECDSA разобран в статье ES256 или RS256.

Что когда выбирать

  • Одно приложение, которое само выпускает и проверяет свои токены сессий: HS256 со случайным секретом длиной 32 байта, который меняют с коротким периодом перекрытия.
  • Сервер авторизации, чьи токены проверяются в нескольких API: RS256, PS256 или ES256 с ключами, опубликованными в JWKS.
  • Токены, проверяемые третьими сторонами, мобильными приложениями или браузерами: только асимметричные алгоритмы; секрет, переданный клиенту, становится публичным.
  • ID-токены OpenID Connect для конфиденциальных клиентов: подойдёт любой вариант; асимметричный используется по умолчанию и проще в эксплуатации.
  • Вебхуки для отдельных клиентов: обычно используют схемы на основе HMAC, потому что у каждого клиента свой секрет.
  • Хранение ключей по нормативным требованиям (HSM, KMS): асимметричные ключи, поскольку сервисы могут проверять подписи без доступа к ключевому материалу для подписи.

Заметки о миграции

Переход с HS256 на RS256 или ES256 без одномоментного переключения всех сервисов:

  1. Создайте пару ключей в KMS или HSM и опубликуйте открытый ключ в JWKS с новыми kid и alg.
  2. Научите проверяющие стороны работать с обоими алгоритмами: выбирайте ключ по kid и привязывайте каждый ключ к одному алгоритму (общий секрет — к HS256, новый ключ — к RS256). Никогда не допускайте, чтобы один вызов проверки принимал оба алгоритма для одного и того же ключевого материала.
  3. Начните выпускать асимметричные токены с новым kid; уже выпущенные токены HS256 продолжают проходить проверку, пока не истечёт срок их действия.
  4. Удалите общий секрет со всех проверяющих сторон, как только пройдёт максимальный срок жизни токенов HS256. Именно этот шаг и даёт реальный выигрыш в безопасности.
  5. Всё равно смените старый секрет, если есть хоть какая-то вероятность его утечки: атакующий, у которого он есть, сможет выпускать токены HS256, пока проверяющие стороны не перестанут их принимать.

Источники

Все сравнения