Как (не) подписать объект JSON

Android

Member
Регистрация
05.07.2019
Сообщения
86
Оценка реакций
26
E2C1D4E5-D651-4470-A8D2-5CD940A7EA39.png
В прошлом году мы сделали пост в блоге по межсервисной аутентификации. Этот пост в основном посвящен аутентификации пользователей в API. Это связанная, но слегка другая проблема: вы, вероятно, предъявляете больше требований к своим внутренним пользователям, чем к своим клиентам. Идея та же: вы пытаетесь провести различие между законным пользователем и злоумышленником, обычно заставляя законного пользователя доказать, что он знает учетные данные, которые злоумышленник не имеет.

Вы действительно не хотите подпись???
Когда криптографические инженеры говорят «подпись», они имеют в виду нечто асимметричное, например, RSA или ECDSA. Разработчики слишком часто используют асимметричные инструменты. Есть много способов испортить их. Для сравнения, симметричные «подписи» (MAC) просты в использовании и их сложно испортить. HMAC пуленепробиваемый и вездесущий.

Если у вас нет веской причины, по которой вам нужна (асимметричная) подпись, вам нужен MAC. Если вы действительно хотите подпись, ознакомьтесь с нашим постом «Криптографические правильные ответы», чтобы сделать его максимально безопасным. Для остальной части этого сообщения в блоге «подписание» означает симметрично, а на практике это означает HMAC.

Как подписать объект JSON
Сериализуйте как хотите.

HMAC. С SHA256? Конечно, что угодно. Мы сделали запись об этом тоже.

Объедините тег с сообщением, возможно, с запятой между ними для облегчения анализа или чего-то еще.

Подождите, разве это не HS256 JWT?
Заткнись. В любом случае, нет, потому что вам нужно проанализировать заголовок, чтобы прочитать JWT, поэтому вы унаследовали все проблемы, которые вытекают из этого.

Как не подписать объект JSON, если вы можете помочь
Кто-то спросил, как подписать объект JSON «внутри полосы»: где тег является частью объекта, который вы сами подписываете. Это нишевый вариант использования, но это случается. У вас есть объект JSON, который хотят прочитать несколько промежуточных систем, и важно, чтобы ни одна из них не вмешивалась в его содержимое. Вы не можете просто отправить тег || json: это может быть криптографически правильный ответ, но теперь это уже не объект JSON, поэтому сторонние сервисы и промежуточные блоки будут прерваны. Вы также не можете заставить их надежно передавать тег как метаданные (через заголовок HTTP или что-то в этом роде). Вам нужно как-то поместить ключ в объект JSON, чтобы «прозрачно» подписать его. Любой, кто заботится о проверке подписи, может, и любой, кто заботится о том, чтобы у объекта JSON была определенная структура, не ломался (потому что большой двоичный объект - все еще JSON, и у него все еще есть данные, которые он должен иметь во всех знакомых местах).

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

Как уже говорилось, внутриполосная подпись JSON означает изменение объекта JSON (например, удаление тега HMAC) и проверку того, что это то же самое, что было подписано. Вы делаете это, снова вычисляя HMAC и проверяя результат. К сожалению, существует бесконечно много одинаковых объектов JSON с различными представлениями на уровне байтов (для некоторого полезного определения равенства, например, встроенного в Python ==).

Некоторые из этих различий тривиальны, а другие - чертовски сложны. Вы можете добавить столько пробелов, сколько хотите между некоторыми частями грамматики, например, после двоеточия и перед значением в объекте. Вы можете изменить порядок ключей в объекте. Вы можете экранировать символ, используя escape-последовательность Unicode (\ u2603) вместо использования представления UTF-8. «UTF-8» может быть форматом сериализации для Unicode, но это не метод канонизации. Если персонаж имеет несколько диакритических знаков, они могут встречаться в разных порядках. Некоторые символы могут быть написаны как базовый символ плюс диакритический знак, но есть также эквивалентный одиночный символ. Вы не всегда можете знать, что такое «правильный» символ вне контекста: это символ единицы сопротивления (ЗНАК U + 2126) или греческая заглавная буква Омега (U + 03A9)? Даже не начинайте меня с того, как вы можете написать одно и то же число с плавающей запятой!

Три подхода:

Канонизируйте JSON.

Добавьте тег и точную строку, которую вы подписали, к объекту, проверьте подпись, а затем убедитесь, что объект JSON совпадает с тем, который вы получили.

Создайте альтернативный формат с более простой канонизацией, чем JSON.

Канонизация

Канонизация означает взятие объекта и создание уникального представления для него. Два объекта, которые означают одно и то же («равны»), но выражены по-разному, канонизируют одно и то же представление.

Каноникализация - это квагнет, который является искусством в исследованиях уязвимостей, означая болото и магнит уязвимости. Вы можете сказать, что это плохо, просто по тому, как сложно набрать слово «канонизация».

Моя любимая ошибка канонизации в недавней памяти, вероятно, ошибка SAML Келби Людвига. Держитесь за задницы, потому что эта ошибка мастерски ударила практически каждую реализацию SAML под солнцем. Он использовал NameIds (SAML-говорят для «сущности, о которой это утверждение»), которые выглядят так:

<NameID> [email protected] <! ---->. Evil.com </ NameID>
Общая стратегия канонизации («exc-c14n») удаляет комментарии, поэтому эта сторона видит «[email protected]». Общая стратегия синтаксического анализа («yolo») не совпадает и видит текстовый узел, комментарий и другой текстовый узел. Поскольку все ожидают, что у NameId будет один текстовый узел, вы берете первый. Но это говорит [email protected], что не подписано IdP или проверено вашей библиотекой XML-DSIG.
Не беспокойтесь: мы сказали, что делаем JSON, а JSON - это не XML. Это проще! Правильно? Здесь есть как минимум две спецификации: Canonical JSON (из OLPC) и черновик IETF (

Авторизируйтесь или Зарегистрируйтесь что бы просматривать ссылки.

). Они работают? Наверное? Но их не весело реализовывать.

Укажите, что именно вы подписываете

Если вы интерпретируете проблему как «для проверки подписи, мне нужно точное байтовое представление того, что подписывать» и канонизация является просто механизмом по умолчанию для получения точного байтового представления, вы также можете просто прикрепить конкретную сериализацию байтов к объекту с помощью тег для этого.

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

Regex приманка и трюк с переключателем

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

Умный трюк здесь состоит в том, чтобы добавить поле соответствующего размера для вашего тега с хорошо известным поддельным значением, затем HMAC, а затем поменять его местами. Например, если вы знаете, что тег - это HMAC-SHA256, ваш размер тега - 256 бит, или 32 байта, или 64 шестнадцатеричных символа. Вы добавляете уникальный ключ (что-то вроде __hmac_tag) со значением 64 общеизвестных байтов, например, 64 ASCII ноль байтов. Сериализуйте объект и вычислите его HMAC. Если вы документируете некоторое подмножество сериализации JSON (например, где могут возникать CRLF или могут возникать дополнительные пробелы), вы знаете, что строка «__hmac_tag»: «000 ...» будет появляться в сериализованном потоке байтов. Теперь вы можете использовать замену строк, чтобы показать реальное значение HMAC. После получения декодер находит тег, считывает значение HMAC, заменяет его нулями, вычисляет ожидаемый тег и сравнивает с ранее прочитанным значением.

Поскольку нет обхода JSON, парсер не может испортить конкретную сериализацию объекта JSON. Ключ должен быть уникальным, потому что, конечно, замена строки или регулярное выражение не знают, как анализировать JSON.

Это кажется странно грубым? Но в то же время, вероятно, менее раздражает, чем канонизация.

Альтернативный формат

Если вы интерпретируете проблему как «канонизация трудна, потому что JSON сложнее, чем то, что я действительно хочу подписать», вы можете подумать, что ответ заключается в переформатировании данных, которые вы хотите подписать, в формат, где канонизация проста или даже автоматическая. Сигнатуры AWS делают это: есть формат сериализации, который гораздо менее гибок, чем JSON, где вы вводите некоторые ключевые параметры, а затем вы используете HMAC. (Здесь есть интересная часть, в которой также содержится хэш точного сообщения, которое вы подписываете - но мы вернемся к этому позже.)

Это особенно привлекательно, если есть фиксированный набор простых значений, которые вы должны подписать, или, в более общем случае, если вещь, которую вы подписываете, имеет предсказуемый формат.

Подписание запроса на практике
Давайте применим эту модель к конкретному исследованию подписания запросов, которое работало на протяжении многих лет в некоторых популярных сервисах. Это не примеры того, как сделать это хорошо, а скорее предостерегающие сказки.

Во-первых, AWS. AWS требует от вас подписывать запросы API. Текущая спецификация - «v4», которая говорит вам, что, вероятно, существует по крайней мере одна интересная версия, предшествующая ей.

AWS Signing v1

Допустим, операция AWS CreateWidget принимает атрибут Name, который может быть любой строкой ASCII. Он также принимает атрибут Unsafe, который по умолчанию имеет значение false, а желания атакующего были истинными. V1 объединяет пары ключ-значение, которые вы подписываете, поэтому что-то вроде Operation = CreateWidget & Name = iddqd стало OperationCreateWidgetNameiddqd. Затем вы подписали полученную строку, используя HMAC.

Проблема в том, что если я могу заставить вас подписывать сообщения для создания виджетов с произвольными именами, я могу заставить вас подписывать операции для произвольных запросов CreateWidget: я просто помещаю все дополнительные ключи и значения, которые я хочу, в значение, которое вы подписываете для меня. Например, подпись запроса для создания виджета с именем iddqdUnsafetrue точно такая же, как и подпись запроса для создания виджета с именем iddqd, для которого значение Unsafe равно true: OperationCreateWidgetNameiddqdUnsafetru.

AWS Подписание V2

С точки зрения безопасности: хорошо.

С точки зрения реализации: он ограничен запросами в стиле запросов (параметры запроса для GET, x-www-form-urlencoded для тел POST) и не поддерживает другие методы, не говоря уже о запросах не HTTP. Сортировка параметров запроса - это бремя для достаточно больших запросов. Ничего для чанкованных запросов тоже нет.

(Некоторый контекст: несмотря на то, что большинство AWS SDK предоставляют вам унифицированный интерфейс, в AWS используется несколько различных стилей протокола. Например, EC2 и S3 - это их собственная вещь, некоторые протоколы используют Query Requests (в основном параметры запроса в запросах GET и POST formencoded тела), другие используют REST + JSON, некоторые используют REST + XML ... Есть даже немного SOAP! Но я думаю, что это уже выход.)

AWS Подписание V3

AWS не очень нравится V3. «Что нового в документе v4» почти отрицает его существование, и, похоже, нет живых сервисов для его реализации. У него были некоторые раздражающие проблемы, такие как различие между подписанными и неподписанными заголовками (оставляя сервис, чтобы выяснить это) и переход к эффективному токену-носителю при использовании по TLS (что здорово, если он фактически используется по TLS).

Учитывая то, как AWS отмахнулся от него, трудно с уверенностью что-либо сказать. Я нашел реализации, но это не достаточно хорошо: реализация может использовать только часть спецификации, в то время как вредность может скрываться в остальных.

AWS Подписание V4

С точки зрения безопасности: хорошо.

Решены некоторые проблемы, отмеченные в V2; например: просто подписывает необработанные байты тела и не заботится о порядке расположения параметров. Это очень близко к первоначальной рекомендации: вообще не выполняйте встроенную подпись, просто подпишите точное сообщение, которое вы отправляете, и поместите MAC-тег снаружи. Традиционное возражение состоит в том, что несколько эквивалентных запросов будут иметь различное представление, например те же аргументы, но в другом порядке. Просто получается, что в большинстве случаев это не имеет значения, и API-аутентификация является одним из таких случаев.

Также обратите внимание, что все эти схемы на самом деле находятся за пределами подписи, но они по-прежнему интересны, потому что у них было много проблем, которые вы видите на встроенной схеме подписи (это были в основном просто вынужденные ошибки).

AWS Подписание V0

Для полноты. Найти его еще сложнее, чем V3: для этого вам нужно написать несколько SDK. Я слышал, что это мог быть HMAC (k, service || operation || timestamp), поэтому он не подписывал большую часть запроса.

Подписание Flickr API

Одна из общих уязвимостей AWS заключается в том, что никто из них не атаковал примитив. Все они использовали HMAC, а HMAC всегда был безопасным. Flickr имел точно такую же ошибку, что и AWS V1, но также использовал плохой MAC. Отправленный вами тег - MD5 (secret + your_concatenated_key_value_pairs). Мы оставим детали атак на расширение на другое время, но изюминка в том, что если вы знаете значение H (секрет + сообщение) и не знаете s, вы можете вычислить H (секрет + сообщение + клей +) message2), где glue - это некоторая двоичная чепуха, а message2 - произвольная строка, контролируемая злоумышленником.

Типичный протокол, в котором это используется, выглядит как параметры запроса. Самая простая реализация просто зациклит каждую пару ключ-значение и присвоит значение в ассоциативный массив. Так что если у вас есть user = lvh & role = user, я мог бы расширить его до действительной подписи для user = lvh & role = userSOMEBINARYGARBAGE & role = admin.

Заключение:
Просто продолжайте и всегда применяйте TLS для своих API.

Может быть, вам не нужно подписывать заявку? Хороший заголовок токена на предъявителя, или HMAC (k, временная метка), или mTLS, если вы действительно заботитесь.

Канонизация чрезвычайно сложна.

Добавьте подпись снаружи тела запроса, убедитесь, что тело запроса завершено, и не беспокойтесь о «подписи того, что сказано, а не того, что подразумевается» - можно подписать точную последовательность байтов.

Следствием этого является то, что подписать запрос для REST API гораздо сложнее (где важны такие вещи, как заголовки, пути и методы), чем подписать RPC-подобный API.