Подписание и проверка JWT

JWT — это три сегмента base64url: заголовок, полезная нагрузка, подпись, — и первые два читаются любым, у кого есть токен. Декодирование говорит вам, что он утверждает; только третий сегмент говорит, верить ли ему. Эта статья — об этом третьем сегменте: как создаётся подпись, кто может её создать, как проверяющие её проверяют и в какой именно точке «проверено» перестаёт означать «доверено». Каждый шаг можно выполнить самостоятельно в подписывающем и верификаторе JWT, который подписывает и проверяет локально в вашем браузере.

Декодирование — это не проверка

Самое распространённое заблуждение о JWT — оно же самое опасное: будто прочитать токен — то же самое, что проверить его. Это не так. Заголовок и полезная нагрузка — это base64url, кодирование, а не шифрование, поэтому любой, кто перехватил токен, нашёл его в журнале или скопировал из локального хранилища браузера, может декодировать его за секунды, вообще без ключа. Утверждения внутри (для кого он, когда истекает, какие роли он утверждает) — это заявления, а заявления дёшевы.

Подпись существует именно потому, что утверждения дёшевы. Это криптографическая привязка между заголовком, полезной нагрузкой и ключом, который есть только у эмитента: измените один символ полезной нагрузки, и подпись перестанет совпадать. Проверка — это повторение этой математики: пересчёт подписи по полученным заголовку и полезной нагрузке с ключом эмитента и сравнение. Если совпало, токен ровно такой, каким его подписал эмитент; если нет — что-то изменилось, и токен ничего не стоит, как бы правдоподобно ни выглядели его утверждения.

Симметричные и асимметричные: кто может выпустить токен

С HS256 секрет, который подписывает, — тот же секрет, который проверяет. Это делает HMAC быстрым, простым и вполне достаточным внутри одного сервиса: ваш API подписывает собственные токены и проверяет их сам, а секрет никогда не покидает здание. В тот момент, когда проверять нужно второй стороне, модель ломается: передав ей секрет, вы передаёте ей и возможность выпускать токены, потому что проверка и подписание — одна и та же операция. Утёкший секрет HMAC — это не проблема проверки; это проблема подделки.

Асимметричные алгоритмы разделяют эти две возможности. RS256, ES256 и EdDSA подписывают закрытым ключом и проверяют открытым, поэтому эмитент может публиковать свои открытые ключи — как JWKS, — и каждый проверяющий в мире может проверять токены, никогда не держа в руках возможность подделать один. Именно это разделение заставляет JWT работать между сервисами: ваш API доверяет токенам поставщика удостоверений, потому что доверяет опубликованным ключам этого поставщика, а не потому, что делит с ним секрет. Плата — сложность: форматы ключей, ротация, сопоставление kid, — и именно для того, чтобы сделать это конкретным, существует подписывающий и верификатор на этом сайте.

Атака с подменой алгоритма

Классической атаке на JWT не нужна сломанная криптография — ей нужен проверяющий, который доверяет токену называть собственный алгоритм. Злоумышленник берёт легитимный токен RS256, чей открытый ключ опубликован (как и положено открытым ключам), и повторно подписывает ту же полезную нагрузку с помощью HS256, используя сам открытый ключ как секрет HMAC. Проверяющий, который читает alg из заголовка и выбирает процедуру проверки соответственно, теперь «проверит» поддельный токен с помощью HMAC, используя открытый ключ как секрет, — а открытый ключ открыт.

Защита структурная, а не рекомендательная: проверяющий должен закреплять принимаемые им алгоритмы, а семейство ключа должно совпадать с семейством алгоритма — ключ RSA никогда не сможет проверить токен HMAC, а секрет никогда не сможет проверить токен RSA. Проверяющий, который переосмысливает ключ между семействами, сломан по замыслу, какой бы правильной ни была его криптография. Вот почему верификатор на этом сайте отказывается от ключей из другого семейства с явным предупреждением, вместо того чтобы пытаться выполнить проверку: отказ — это и есть функция.

Как на практике выглядят JWKS и ротация kid

Поставщик удостоверений не выдаёт один вечный ключ; он публикует набор — JWKS, обычно по адресу /.well-known/jwks.json, — и ротирует ключи, добавляя новый и, после льготного периода, удаляя старый. Заголовок каждого токена несёт kid, называющий ключ, который его подписал, поэтому проверяющий получает набор, выбирает совпадающий ключ и проверяет. Токены, подписанные старым ключом, продолжают работать, пока он не удалён, — именно это делает ротацию безопасной, а не днём флага.

Операционные детали — там, где всё ломается. Проверяющие должны кэшировать JWKS (получение его при каждом запросе — это собственноручно устроенный сбой), обрабатывать случай, когда kid неизвестен, — обычно это ротация, которую кэш ещё не видел, поэтому перед отказом нужно получить набор заново, — и никогда не принимать набор ключей из недоверенного URL: набор заслуживает доверия ровно настолько, насколько канал, по которому он пришёл, поэтому конечные точки JWKS живут за HTTPS, а построитель JWKS здесь выводит каждый kid как отпечаток ключа по RFC 7638, поэтому идентификатор нельзя подделать независимо от ключа.

exp, nbf, iat — и проблема часов

Подпись доказывает, что токен был подписан; она ничего не говорит о том, следует ли его ещё чтить. Это работа временных утверждений: exp (когда он перестаёт быть действительным), nbf (когда он начинает действовать), iat (когда он был выдан). Проверяющий, который проверяет подпись и пропускает часы, принимает токен навсегда — включая тот, что украли год назад. Короткие сроки жизни — практический ответ: токены доступа, измеряемые минутами, токены обновления, несущие долгосрочное доверие, а отзыв сводится к простому неповторному выпуску.

Часы расходятся, поэтому проверяющие допускают небольшой запас — обычно минуту-другую допуска на exp и nbf, — но допуск — это терпимость к дрейфу, а не разрешение игнорировать истечение. Устройство, чьи часы ушли на минуты, будет производить токены, которые его же сервер отклоняет, а токен с iat в будущем — сигнал подделки, а не ошибка округления. Та же дисциплина часов относится и к инструментам: верификатор проверяет истечение по часам, которые вы можете видеть, и говорит прямо, когда токен истёк, а не просто «недействителен».

Что «проверено» доказывает, а что — нет

Проверенный токен доказывает три вещи и ничего больше: заголовок и полезная нагрузка ровно такие, какими были подписаны, подписывающий держал ключ, и токен находится внутри окна действительности. Он не доказывает, что эмитент — тот, за кого вы его принимаете: это зависит от того, откуда взялся ключ, а ключ, полученный по URL, контролируемому злоумышленником, прекрасно проверяет токены злоумышленника. Он не доказывает, что утверждения верны: подпись удостоверяет заявление, а не факты за ним. И он не доказывает, что токен не украли: действительный токен, предъявленный кем угодно, принимается, поэтому токены короткоживущие и с ограниченной областью.

Значит, решение о доверии живёт вне токена. Ваш API доверяет токену, потому что получил ключ по известному URL поставщика удостоверений через HTTPS, потому что iss совпадает с настроенным поставщиком и потому что aud совпадает с сервисом, которым он является. Проверка — последняя проверка, а не первая: происхождение ключа, эмитент, аудитория, затем подпись, затем время. Инструмент, который сообщает «ДЕЙСТВИТЕЛЕН» без этого контекста, говорит правду о математике и ничего — о доверии, поэтому верификатор этого сайта так и говорит на странице, рядом с вердиктом.

Часто задаваемые вопросы

Можно ли проверить токен без ключа?

Нет — для проверки нужен открытый ключ эмитента (или общий секрет — для HMAC). Для декодирования ключ не нужен, и именно эту путаницу следует избегать: декодированный токен читаем, а не проверен. Если эмитент публикует JWKS, получите его по известному URL и сопоставьте kid токена; верификатор на этом сайте делает это за вас.

Почему мой токен RS256 не проходит с моим секретом?

Потому что секрет и ключ RSA — разные семейства, а здравый проверяющий отказывается их смешивать. Токены RS256 проверяются открытым ключом RSA эмитента — PEM, JWK или JWKS, — никогда текстовым секретом. Использование секрета и было бы атакой с подменой алгоритма: открытый ключ открыт, поэтому любой мог бы подделывать токены, если бы проверка HMAC принимала его как секрет.

Как долго должен жить JWT?

Минуты, а не дни, для токенов доступа: 5–15 минут — обычный диапазон, а токен обновления несёт более длинную сессию. Короткий срок жизни ограничивает ущерб от украденного токена окном до его истечения и делает отзыв вопросом неповторного выпуска, а не ведения чёрного списка. Токены, живущие неделями, — это сессионные cookie с худшим инструментарием.

Источники