После дискуссии с коллегами о TLS 1.3 в целом и прикладном использовании идущих в комплекте с ним шифров я решил кратко изложить основы, которые не худо было бы знать любому разработчику. Мне хотелось бы показать, на какую нишу направлен каждый из пяти шифров и как конкретно применяются содержащиеся в них хеш-функции.
В этой статье не будут освещаться детали согласования ключей в мере большей, нежели та, что необходима для описания установки безопасного соединения как такового. Я не буду рассуждать о протоколе Диффи-Хеллмана и эллиптических кривых, сертификатах, распределённых ключах и о тому подобном. Если возникнет необходимость, эти вопросы будут рассмотрены в будущем.
Выбор шифра
Первым делом давайте разберёмся, как две стороны, коммуницирующие посредством TLS 1.3, определяют, какой конкретно шифр при этом использовать. Этот процесс известен как процесс согласования шифров и является частью "рукопожатия" или, иначе говоря, первичного взаимодействия двух участников. Помимо этого происходит сразу несколько других вещей, но, как я и говорил выше, они не рассматриваются в данной статье.
Чтобы произвести рукопожатие, сторона, выступающая в качестве клиента, отправляет сообщение "ClientHello", которое помимо прочего содержит список поддерживаемых шифров в порядке от наиболее предпочтительного до наименее предпочтительного.
Как только сторона, выступающая в качестве сервера, получает эту информацию, она должна выбрать, какой именно шифр использовать (или закрыть соединение ввиду того, что не поддерживается ни один из предложенных). Это происходит с помощью сообщения, известного как "ServerHello". За ним следует обмен данными для старта зашифрованного соединения, называемый "ChangeCipherSpec". Весь процесс обычно происходит в чистом тексте и выглядит следующим образом:
Клиент: Привет! Я предпочитаю шифрA, ну или шифрБ или шифрЦ.
Сервер: Годится! Давай-ка возьмём шифрБ. Переключаюсь на него. (Следуют зашифрованные данные).
Клиент: Добро! Тож переключаюсь на него. (Следуют зашифрованные данные).
Собственно, если клиент по какой-то причине ограничен в выборе шифра, он может сообщить это сразу; и если сервер не решит забраковать выбор клиента, шифр будет использован (если поддерживается, конечно). Это полезно не столько с точки зрения безопасности, сколько для производительности, поскольку ни клиент, ни сервер изначально не должны анонсировать шифры, которые они считают небезопасными.
Доступные шифры
RFC 8446 описывает TLS 1.3 в совокупности с 5 доступными шифронаборами. Существует также RFC 8998, который определяет ещё два обязательных в Китае шифра (известных как ShangMi) и ещё два черновика, один из которых добавляет поддержку ГОСТовских шифров, а другой добавляет режимы "только аутентификация". Я опишу работу пяти "оригинальных" шифров, поскольку остальные либо экспериментальные, либо используются редко.
Итак, пять шифров, которые поддерживает TLS 1.3:
TLS_AES_128_GCM_SHA256
Блочный шифр AES в режиме GCM со 128-битным ключом и хеш-функцией SHA-256
TLS_AES_256_GCM_SHA384
Блочный шифр AES в режиме GCM с 256-битным ключом и хеш-функцией SHA-384
TLS_CHACHA20_POLY1305_SHA256
Потоковый шифр ChaCha20 с 256-битным ключом, аутентификатором Poly1305 и хеш-функцией SHA-256.
TLS_AES_128_CCM_SHA256
Блочный шифр AES в режиме CCM со 128-битным ключом и хеш-функцией SHA-256
TLS_AES_128_CCM_8_SHA256
Блочный шифр AES в режиме CCM со 128-битным ключом, ограниченными до 8 байт тэгами аутентификации и хеш-функцией SHA-256
Шифры и хеши
Вы могли заметить, что каждый раз мы упоминаем шифр в совокупности с хешем (а не, например, с механизмом согласования ключей). Это потому, что они связаны, но не так, как может показаться на первый взгляд.
В TLS 1.3 шифры служат как раз тому, что известно под именованием AEAD (аутентифицированное шифрование с присоединёнными данными). Это означает, что они делают сразу несколько вещей одновременно:
Шифруют данные с помощью ключа. Это сохраняет конфиденциальность данных, то есть гарантирует, что никто кроме владельца ключа не сможет их прочесть.
Аутентифицируют данные с помощью ключа. Это сохраняет целостность, то есть гарантирует, что в данных не было зафиксировано никаких изменений (добавление или удаление). Аутентификация также гарантирует, что только обладатель ключа шифрования может отправить данные, которые сможет принять вторая сторона.
Авторизуют (опять же, с помощью ключа) незашифрованные данные. Это даёт те же гарантии, что и в плане целостности и возможности создавать валидные сообщения для любых данных, какие потребуется валидировать. В частности, это полезно, когда нужно удостовериться, что незашифрованные данные, которые определяют свойства зашифрованных данных, не были изменены.
Следует, однако, помнить, что эти шифры не предоставляют репутационных гарантий (т�� есть гарантий того, что отправитель и обладатель подписи одно и то же лицо) потому как любой владеющий ключом дешифровки может создать новое сообщение, которое будет определено как валидное. В TLS, это, как правило, реализуется другим способом. Так или иначе, поскольку предоставляется конфиденциальность, гарантируется целостность и достоверность равно для зашифрованных и незашифрованных данных, эти шифры явлются идеальным "строительным материалом" для протоколов вроде TLS, которые в целом на всё вышеперечисленное и направлены.
Но если шифр может удовлетворить все возможные нужды, зачем нужен хеш? У него иное назначение. Во-первых, хеш используется, чтобы создать сводку сообщений, которыми уже был произведён обмен. Это, в частности, полезно, если атакующий изменяет любое сообщение – изменится и результат вычисления. Во-вторых, хеш нужен, чтобы извлечь все применённые протоколом ключи из данных обмена (включая и саму сводку). В-третьих, хеш используется, дабы посредством одного из извлечённых ключей удостоверить обе стороны, что не было произведено подделки при генерации тэга аутентификации из сводки обмена сообщениями и что ключи одинаковы на обоих концах.
Как видите, хеш играет важную роль ещё до того, как будет произведён обмен сообщениями. Коль скоро мы знаем, что общаемся с нужным человеком и что не был устроен подлог, мы отправляем данные, используя AEAD шифр. В результате для длительных соединений, когда отправляются значительные объёмы данных, этот последний шифр будет являться одним их главных факторов производительности, которые необходимо учитывать.
Далее я объясню разницу между алгоритмами хеширования и различными шифрами, а также укажу на факторы, определяющие выбор того или иного из них.
SHA-256
SHA-256 является достаточно старым (2001) алгоритмом хеширования, созданным NIST в качестве альтернативы SHA-1 и MD5 из-за опасений касательно их безопасности. В это же время был объявлен конкурс на создание нового алгоритма (SHA-3).
Данный алгоритм обеспечивает уровень безопасности, эквивалентный 128-битному шифру для большинства задач и, поскольку он оперирует 32 битами даных, достаточно быстр на большинстве архитерктур. Некоторые современные десктопные и серверные процессоры, а также многие криптографические ускорители поддерживают его на аппаратном уровне.
SHA-384
Как и SHA-256, SHA-384 был представлен в том же стандарте и использует похожий подход.
Однако в отличие от собрата SHA-384 обеспечивает уровень безопасности, эквивалентный 192-битному шифру. SHA-384 оперирует 64 битами данных, и это означает, что он может обработать вдвое больше данных за то же время. Расчёт производится дольше, но на 64-битных процессорах всё равно оказывается быстрее решений, основанных на SHA-256. К тому же в отличие от последнего он отбрасывает 128 бит данных от результата хеширования. Это делает атаки удлинением сообщения сложнее, поскольку чтобы обработать эти биты данных, атакующий должен фактически угадать их.
Десктопные и серверные процессоры не имеют специфической поддержки этого алгоритма, а его поддержка среди криптографических ускорителей сильно варьируется по причине необходимости хранить больше информации о состоянии операции и выполняемых действиях.
По итогу SHA-384 более безопасен, нежели SHA-256 (хотя и не в случае применения квантовых компьютеров). Также он быстрее, когда применяется в ПО, исполняемом на 64-битных процессорах, в случае если сообщение достаточно длинное. Но на системах с ускорителями или инструкциями поддержки только для SHA-256 или же без поддержки 64-битных инструкций он будет медленнее последнего.
128-битный AES в режиме GCM
В ходе проведённого NIST конкурса на замену ныне устаревшего DES (Data Encryption Standard) был выбран алгоритм Rijndael, впоследствии ставший известным как AES (Advanced Encryption Standard).
AES это блочный шифр; в качестве входных данных он получает ключ шифрования фиксированного размера (128, 192 или 256 бит) и блок данных фиксированного размера (128 бит), а на выходе возвращает зашифрованный блок данных размером 128 бит. Обратный процесс осуществляется аналогично: чтобы получить оригинальный блок данных, понадобится ключ и блок данных, зашифрованный этим ключом.
Однако нужно определиться, что делать с данными, которые имеют размер больше или меньше одного блока, и это как раз то, для чего нужен режим шифрования. В данном случае режим называется GCM, то есть Galois/Counter Mode. Работает он следующим образом:
Для генерации достаточно длинной строки двоичных данных перед осуществлением шифрования сообщения используется блочный шифр, шифрующий блоки, содержащие счётчик; и 128-битная строка, необходимая для аутентификации данных.
Производится XOR-выборка данных для шифровки вместе с ранее зашифрованными блоками-счётчиками. На этом шаге происходит шифрование сообщения.
Поверх дополнительных данных, зашифрованных данных и размера обоих вводов применяется "быстрый хеш" под названием GMAC, чтобы сгенерировать 128-битную строку для аутентификации данных. На этом шаге формируется уникальный идентификатор данных.
Идентификатор данных шифруется вместе с оставшимся зашифрованным блоком счётчика. Это предотвращает доступ к данным извне.
Дешифровка производится в обратном порядке с учётом сверки хешей (вычисленного и полученного).
Такой подход имеет множество преимуществ. Во-первых, блоки могут быть легко зашифрованы параллельно просто за счёт разного значения счётчиков. Во-вторых, используемый "хеш" может быть сильно упрощён, потому что он уже уникален за счёт использованного ключа. В-третьих, можно применить особый вид хеша (мультипликативный хеш) который тоже может быть запараллелен. Также для больших объёмов данных обычно требуется только один проход (шифрование вообще обычно ресурсоёмкая задача).
В-общем, этот режим шифрования в достаточной степени безопасен и достаточно быстр, особенно если присутствует его аппаратная поддержка. Это актуально для большинства современных десктопных процессоров и некоторых криптографических ускорителей, но не для большинства мобильных устройств или встроенных решений.
256-битный AES в режиме GCM
Этот шифр представляет собой в точности то же самое, что и описанный выше, вплоть до использования таких же 128-битных тэгов аутентификации, но с более длинными (256 бит) ключами, дабы удостовериться в соответствии уровня безопасности хеш-функции уровню безопасности ключей; при этом в отличие от других режимов используется SHA-384. Более "тяжеловесные" ключи делают этот шифр несколько более медленным, чем 128-битный. С другой стороны, ключи этого размера дают преимущество, будучи безопасными, даже если будет применён (а перед этим создан) достаточно мощный квантовый компьютер.
ChaCha20 с аутентификатором Poly1305
В отличие от шифров, описанных ранее, ChaCha20 представляет собой нечто более схожее с хеш-функцией. В частности, ChaCha20 это алгоритм, получающий на вход 512 бит и выводящий 512 бит способом, который делает крайне сложным определение того, каким был ввод, и который гарантирует, что каждый из выведенных битов испытал влияние каждого бита, поданного на вход. Методика: создание блока с 256-битным ключом, 128-битной константой и 128-битной смесью значения счётчика со значением, которое используется только единожды.
В остальном же этот шифр работает схожим с AES GCM образом. Вместо шифрования счётчиков, оные "хешируются" с помощью ChaCha20 функции. Также применяется несколько отличный от GCM хеш с ключом.
Используется ChaCha20, чтобы "хешировать" блоки, содержащие счётчик, чтобы, в свою очередь, сгенерировать достаточно длинную строку двоичных данных, чтобы (в свою очередь) зашифровать сообщение с использованием 128-битной строки для аутентификации данных.
Производится XOR-выборка данных, которые нужно зашифровать вместе с "хешированными" блоками-счётчиками. На этом шаге происходит шифрование сообщения.
Поверх дополнительных данных, зашифрованных данных и размера обоих вводов применяется "быстрая" "хеш-функция" с ключом (отличная от ChaCha20), чтобы сгенерировать 128-битную строку для аутентификации данных. На этом шаге генерируется уникальный идентификатор данных.
Идентификатор данных шифруется вместе с оставшимся зашифрованным блоком счётчика. Это предотвращает доступ к данным извне.
Но если AES одобрен NIST и в достаточной мере быстр и безопасен, зачем кому-то может понадобиться ещё один шифр? Причина проста: если AES GCM очень быстр при наличии аппаратной поддержки, он может быть не столь быстр, когда её нет. В противоположность ему, ChaCha20 и Poly-1305 созданы, чтобы быть как можно более быстрыми именно на программном уровне. Они медленнее, но всё же достаточно быстры в сравнении с аппаратно ускоренным AES GCM и ОЧЕНЬ быстры на чисто программном уровне. Иными словами, ChaCha20 в совокупности с Poly1305 лучший выбор при отсутствии поддержки на уровне "железа".
128-битный AES в режиме CCM
Как и GCM, ССM представляет собой ещё один режим функционирования блочного шифра AES; его название является сокращением от "счётчик режима CBC-MAC". В свою очередь, CBC-MAC означает Cipher Block Chaining Message Authentication Code. Этот режим сходен с GCM в том, что он использует блок-счётчик вместе с данными в качестве переменной для операции XOR, но применяемая хеш-функция с ключом – особая. В частности, CCM работает следующим образом:
С помощью блочного шифра создаётся "хеш" дополнительных данных, шифруемых данных (да, перед их шифрованием) и размеров обоих вводов. Это делается путём включения некоторых добавочных блоков с информацией о том, как "хеш" функция будет работать и "наполнения" для соответствия желаемому размеру блока. Для каждого блока производится XOR-выборка между ранее зашифрованным блоком и данными этого блока в виде обычного текста, и результат шифруется. Тэг аутентификации в этом случае представляет собой 128-битный результат последнего шифрования.
Для генерации достаточно длинной строки двоичных данных используется блочный шифр, шифрующий блоки, содержащие счётчик; и 128-битная строка, используемая для аутентификации данных.
Производится XOR-выборка данных, которые нужно зашифровать вместе с зашифрованными блоками-счётчиками. На этом шаге происходит шифрование сообщения.
Идентификатор данных шифруется вместе с оставшимся зашифрованным блоком-счётчиком. Это предотвращает доступ к данным извне.
Режим CCM имеет изъяны в своём дизайне, например, нужно знать размер шифруемых данных до начала самого процесса шифрования. CCM также требует произвести два цикла шифрования каждого блока данных и ещё одного на каждый блок для аутентификации, что может приводить к серьёзному падению производительности в сравнении с GCM. Наконец, эквивалент хеша, который использует CCM, в отличие от GCM или ChaCha20-Poly1305 не может быть распараллелен. В остальном, CCM столь же безопасен, как и 128-битный режим GCM.
Почему тогда этот режим вообще был добавлен? Ответ прост: некоторые встраиваемые устройства могут не иметь достаточно ресурсов для реализации AES и аппаратной поддержки хешей GCM. Для этих устройств проведение двух шифрований оказывается быстрее, нежели одно шифрование и одно хеширование с ключом. Также, в отличие от других режимов, предлагающих решения вроде OCB, использование этого не затрагивает никакие патенты.
Так или иначе, в openssl этот режим по умолчанию выключен.
128-битный AES в режиме CCM и 64-битными тэгами аутентификации
Этот режим схож с описанным выше, но использует только 64 бита "хеша", что является весьма нишевым сценарием, при котором допускается бо́льшая вероятность для атакующего модифицировать пакеты (1 из 264 вместо 1 из 2128) в обмен на отправку на 8 байт меньше данных в каждом пакете. Вообще говоря, это имеет смысл только в очень специфических ситуациях для встраиваемых устройств, где даже эти 8 байт решают. На данный момент этот шифр не рекомендуется к использованию и выключен в openssl по умолчанию. Лично я могу рекомендовать его только в том случае, если Вы знаете, что делаете.
Идеальный случай
В идеальном случае клиенты игнорировали бы шифры, которые они считают небезопасными, а после выстраивали бы их в порядке предпочтения уровня безопасности и скорости. Сервера бы использовали этот список для выбора лучшего шифра в соответствии с собственными ожиданиями и ограничениями.
Например, клиент без специфической аппаратной поддержки на первое место поставил бы ChaCha20-Poly1305 и сервер наверняка учёл бы это. С другой стороны, современный десктопный компьютер может избрать и 128-битный AES в режиме GCM, а сервер, не имеющий "железной" поддержки для него всё равно мог бы выбрать ChaCha20-Poly1305. Тот же первый клиент, соединяясь с сервером, имеющим на аппаратном уровне поддержку 128-битного AES в режиме GCM, был бы вынужден использовать именно его.
Иными словами, в случае файлообменной сети было бы так:
Клиент-параноик:
Только TLS_AES_256_GCM_SHA384 и, возможно,TLS_CHACHA20_POLY1305_SHA256 в порядке предпочтения скорости работы.
Клиент без аппаратной поддержки:TLS_CHACHA20_POLY1305_SHA256 TLS_AES_128_GCM_SHA256 TLS_AES_256_GCM_SHA384 и, возможно,TLS_AES_128_CCM_SHA256именно в таком порядке. В соответствии с требованиями безопасности 256-битный AES может идти в этом списке раньше.
Клиент с аппаратной поддержкой AES:TLS_AES_128_CCM_SHA256 TLS_CHACHA20_POLY1305_SHA256 TLS_AES_128_GCM_SHA256 и TLS_AES_256_GCM_SHA384именно в таком порядке. В соответствии с требованиями безопасности 256-битный AES может идти в этом списке раньше.
Клиент с аппаратной поддержкой AES GCM (например, AES-NI and PCLMULQDQ):TLS_AES_128_GCM_SHA256 TLS_AES_256_GCM_SHA384 TLS_CHACHA20_POLY1305_SHA256 и, возможно,TLS_AES_128_CCM_SHA256именно в таком порядке. В соответствии с требованиями безопасности 256-битный AES может идти в этом списке раньше.
Сервер-параноик:
Только TLS_AES_256_GCM_SHA384 и, возможно,TLS_CHACHA20_POLY1305_SHA256. ChaCha20 выбирается на основании предпочтений клиента или скорости работы сервера.
Сервер без аппаратной поддержки: TLS_CHACHA20_POLY1305_SHA256 TLS_AES_128_GCM_SHA256 TLS_AES_256_GCM_SHA384 и, возможно,TLS_AES_128_CCM_SHA256именно в таком порядке. В соответствии с требованиями безопасности 256-битный AES может идти в этом списке раньше.
Сервер с аппаратной поддержкой только AES:TLS_CHACHA20_POLY1305_SHA256 в случае предпочтения клиента, или же TLS_AES_128_CCM_SHA256если поддерживается клиентом. Ну или TLS_CHACHA20_POLY1305_SHA256 TLS_AES_128_GCM_SHA256 TLS_AES_256_GCM_SHA384именно в таком порядке. В соответствии с требованиями безопасности 256-битный AES может идти в этом списке раньше.
Сервер с аппаратной поддержкой AES GCM: основывается на предпочтениях клиента. 256-битный AES может иметь приоритет.
Обратите внимание, я намеренно избегаю упоминаний шифра TLS_AES_128_CCM_8_SHA256поскольку если бы его использование было необходимо, это было бы определено заранее.
Итоги представим в виде таблицы:
Клиент\Сервер | Без аппаратной поддержки | Только AES | AES GCM |
Без аппаратной поддержки | ChaCha20 | ChaCha20 | ChaCha20 |
Только AES | ChaCha20 | CCM | CCM |
AES GCM | ChaCha20 | CCM | GCM |
Как видно, выбор всегда останавливается на опции, обеспечивающей лучшую производительность для стороны, испытывавшей бы бо́льшую нагрузку в случае, если бы был выбран более производительный (для одной из сторон) вариант.
В сценарии "клиент-сервер" приоритет производительности сервера может быть критичен. И хотя выгоды GCM могут быть не столь уж существенны (поскольку скорость работы CCM варьируется от реализации к реализации) они могут давать разный результат в зависимости от самого сервиса. Тогда таблица выглядела бы так:
Клиент\Сервер | Без аппаратной поддержки | Только AES | AES GCM |
Без аппаратной поддержки | ChaCha20 | CCM | GCM |
Только AES | ChaCha20 | CCM | GCM |
AES GCM | ChaCha20 | CCM | GCM |
Возможны и другие подходы, но, убедившись, что клиент посредством порядка шифров выражает свои ожидания производительности от различных алгоритмов, мы можем удостовериться, что сервер всегда сделает максимально разумный выбор с учётом нужд и возможностей обеих сторон.
Выводы
Как было показано, хотя при использовании TLS 1.3 за сервером всегда будет последнее слово в выборе конкретного алгоритма, клиент имеет возможность выразить свои предпочтения. Он должен реализовать эту возможность, чтобы указать на ожидаемый уровень безопасности и производительности, дабы сервер мог произвести информированный выбор.
