Почему подпись `SHA-256(секрет + данные)` можно подделать без знания секретного ключа
Самодельная схема авторизации часто выглядит убедительно: сервер передаёт клиенту данные и вычисленную рядом подпись. Например:
```text
user=guest&role=reader
sig=...
```
Предполагается, что пользователь может увидеть и изменить параметры, но не знает секретный ключ сервера. Значит, после замены `role=reader` на `role=admin` он якобы не сможет получить правильную подпись.
На практике такая защита может оказаться полностью ненадёжной. Проблема не в том, что SHA-256 удалось взломать, и не в подборе ключа. Уязвимость появляется из-за неправильного способа применения хеш-функции:
```text
SHA-256(secret + message)
```
Для некоторых хеш-функций, включая SHA-256, SHA-1 и MD5, такая конструкция допускает атаку расширения сообщения - length extension attack. Она позволяет добавить данные в конец исходного сообщения и вычислить новую корректную подпись, не зная секрета.
Как устроена уязвимая схема
Пусть сервер использует ключ:
```text
super-secret-key-42
```
А подписываемое сообщение выглядит так:
```text
user=guest&role=reader
```
Сервер рассчитывает подпись:
```text
sig = SHA-256(secret + message)
```
Клиент получает и исходные данные, и значение `sig`. Секрет ему неизвестен. На первый взгляд изменить роль невозможно: новая строка потребует новой подписи.
Однако SHA-256 работает не как "чёрный ящик", полностью скрывающий промежуточные данные. Алгоритм обрабатывает сообщение блоками по 64 байта. После каждого блока он обновляет внутреннее состояние из восьми 32-битных слов. Состояние после последнего блока и становится итоговым хешем.
Иными словами, 256-битный результат SHA-256 - это не отдельная криптографическая печать, а финальное состояние вычисления, представленное в виде 64 шестнадцатеричных символов.
Что именно узнаёт атакующий
Если у злоумышленника есть:
```text
SHA-256(secret + message)
```
то он фактически располагает внутренним состоянием SHA-256 после обработки исходной последовательности. Восстановить эти значения не требуется: они уже записаны в подписи.
Дайджест длиной 64 hex-символа разбивается на восемь частей по 8 символов. Каждая часть соответствует одному 32-битному регистру состояния:
```text
H0 H1 H2 H3 H4 H5 H6 H7
```
Получив эти значения, специальная реализация SHA-256 может продолжить вычисление так, будто первые блоки уже были обработаны. Злоумышленник указывает длину ранее обработанных данных, а затем добавляет собственный суффикс.
Например, к сообщению можно попытаться присоединить:
```text
&role=admin
```
Новая строка будет иметь вид:
```text
user=guest&role=reader
[служебное дополнение SHA-256]
&role=admin
```
При этом секрет в начало строки добавляет уже сервер. Первые байты его версии и версии атакующего совпадут, поэтому сервер получит тот же промежуточный результат, после чего обработает добавленный суффикс.
Как работает дополнение SHA-256
Перед обработкой SHA-256 дополняет сообщение до размера, кратного 64 байтам. Формат дополнения фиксирован:
1. добавляется байт `0x80`;
2. затем идут нулевые байты;
3. в конце записывается исходная длина сообщения в битах - восьмью байтами.
В рассматриваемом примере:
- секрет занимает 19 байт;
- исходные параметры - 22 байта;
- общая длина составляет 41 байт.
После добавления служебных данных последовательность достигает 64 байт, то есть ровно одного блока SHA-256.
Итоговая подпись является состоянием алгоритма после обработки этого блока. Для продолжения вычисления не нужно знать содержимое блока: достаточно восьми регистров, зашифрованных в самой подписи.
Атакующий восстанавливает формат дополнения, предполагая длину секрета. Важно, что точную длину ключа иногда даже не требуется знать заранее: можно перебрать разумный диапазон вариантов и отправить серверу несколько запросов. Правильный вариант будет принят, остальные - отклонены.
Почему сервер принимает подделку
Сервер при проверке делает примерно следующее:
```text
SHA-256(secret + полученное_сообщение)
```
Если в качестве сообщения передать исходные данные, дополнение и добавленный суффикс, сервер фактически обработает:
```text
secret + message + padding + suffix
```
Первые 64 байта будут полностью совпадать с тем блоком, который уже был учтён при создании исходной подписи. Далее сервер продолжит расчёт с того же состояния и получит именно тот хеш, который заранее вычислил атакующий.
SHA-256 при этом остаётся математически стойкой хеш-функцией. Не происходит ни коллизии, ни обращения хеша, ни подбора секретного ключа. Ошибка заключается в том, что хеш-функцию применили как MAC, хотя для этого она не предназначена.
Почему простая замена параметра может не сработать напрямую
Есть важная практическая деталь. Добавление `&role=admin` не всегда автоматически означает, что приложение выберет значение `admin`. Всё зависит от обработчика параметров.
Некоторые серверы используют первое значение ключа:
```text
role=reader&role=admin
```
Другие выбирают последнее:
```text
role=reader&role=admin
```
Третьи превращают повторяющиеся параметры в массив или отклоняют запрос целиком. Поэтому успешность атаки определяется не только криптографией, но и тем, как конкретный фреймворк разбирает параметры.
Кроме того, дополнение SHA-256 содержит непечатаемые байты. Их нельзя бездумно вставлять в URL или cookie: данные придётся корректно кодировать, например в percent-encoding или другом безопасном формате передачи.
Почему нельзя просто скрыть подпись
Иногда пытаются решить проблему, спрятав подпись в cookie, добавив необычное кодирование или усложнив формат параметров. Это не помогает, если атакующий всё равно может получить исходные данные и их хеш.
Секретность формата не заменяет криптографическую защиту. Подпись должна оставаться безопасной даже в ситуации, когда злоумышленник знает:
- алгоритм;
- структуру сообщения;
- исходные данные;
- множество корректных подписей;
- длину или примерную длину ключа.
Именно это является базовым принципом современной криптографии: алгоритм не должен зависеть от сокрытия своей конструкции.
Как исправить схему
Правильный вариант - использовать HMAC:
```text
HMAC-SHA-256(secret, message)
```
HMAC специально создан для аутентификации сообщений с использованием секретного ключа. В упрощённом виде он устроен как двухэтапная конструкция:
```text
HMAC(K, M) =
SHA-256((K ⊕ opad) || SHA-256((K ⊕ ipad) || M))
```
Внутренний и внешний хеши не позволяют трактовать итоговый результат как состояние простого вычисления `SHA-256(secret + message)`. Поэтому классическая атака расширения сообщения к HMAC не применяется.
На практике следует использовать готовую библиотечную реализацию HMAC, а не писать формулу вручную. Дополнительно нужно сравнивать подписи функцией constant-time comparison, чтобы не создавать побочный канал по времени выполнения.
На что обратить внимание при аудите
При проверке собственного кода стоит найти все конструкции, похожие на:
```text
hash(secret + data)
hash(data + secret)
```
Первая форма уязвима к length extension для Merkle-Damgård-хешей. Вторая обычно не имеет именно этой проблемы, но тоже не считается универсальной заменой HMAC: ошибки в формате, кодировке, обработке длины и сравнении могут привести к другим уязвимостям.
Нужно также проверить:
- используется ли стандартный HMAC;
- одинаково ли кодируются данные при создании и проверке подписи;
- защищена ли подпись от повторного использования;
- присутствуют ли срок действия и идентификатор сессии;
- проверяется ли целостность всей структуры, а не отдельных полей;
- применяется ли безопасное сравнение подписей;
- нельзя ли изменить порядок, тип или количество параметров.
Если подпись используется для cookies, желательно подписывать не "сырой текст", а чётко сериализованную структуру с однозначным форматом. Для критичных данных полезно включать в неё срок действия, назначение токена и контекст приложения.
Итог
Конструкция `SHA-256(secret + message)` выглядит как цифровая подпись, но криптографической подписью не является. Финальный дайджест SHA-256 содержит внутреннее состояние алгоритма, а структура хеша позволяет продолжить вычисление с добавленными данными.
Поэтому злоумышленник, имеющий исходное сообщение и подпись, в некоторых условиях способен сформировать корректный хеш для расширенной строки без знания секрета. Надёжное решение - использовать HMAC-SHA-256 либо другой стандартный механизм аутентификации сообщений, предоставляемый проверенной криптографической библиотекой.
