Недавно мы применили плату Ethond в качестве мини-роутера и запустили на нём OpenVPN. Но обнаружилось, что процессор часто нагружается на 100%, а скорость не поднимается выше 15-16 Мбит/с. На канале связи 100 мегабит это очень мало, поэтому мы решили ускорить процесс аппаратно. Ребята из группы FPGA-разработчиков сделали прошивку на базе открытого IP-core для Altera CycloneV с реализацией шифра AES-128, которая умеет шифровать 8 Гбит/сек и дешифровать 700 Мбит/сек. Для сравнения, программа openssl на CPU (ARM Cortex A9) того же CycloneV может обрабатывать лишь около 160 Мбит/сек. Эта статья посвящена нашему исследованию по применению аппаратного шифрования AES. Мы сжато представим описание криптографической инфраструктуры в Linux и опишем драйвер (исходный код открыт и доступен на github), который осуществляет обмен между FPGA и ядром. Реализация шифрования на FPGA не является темой статьи — мы описываем лишь интерфейс, с которым происходит взаимодействие c акселератором со стороны процессора. Сейчас мы понимаем, что лучше было бы сначала определить, в чём заключается главный фактор понижения пропускной способности канала: вдруг на самом деле не сам процесс шифрования, а проход через программный стек занимает основную часть времени? Мы этого не сделали, и прирост производительности в результате оказался вовсем не таким, как ожидалось. Однако польза всё равно есть: было интересно изучить, как в Linux зарегистрировать механизм аппаратного ускорения, как к нему можно обратиться из пользовательских программ и, наконец, как заставить популярные вещи вроде openssl и openvpn выбирать ускоренный алгоритм, а не стандартный, реализованный программно. В будущем мы собираемся ускорить именно OpenVPN на Ethond — тогда ждите от нас очередную статью! Чем нас так заинтересовала криптография? На первый взгляд, она не идёт бок о бок с оборудованием, которое мы делаем. Однако, применение для неё мы тоже можем найти. Огромная часть трафика зашифрована, и каждый пользователь Интернета регулярно сталкивается, даже сам того не зная, с криптографией. Например, растёт популярность VPN: согласно исследованию, на начало 2017 года трое людей из десяти используют эту технологию. Нам пришла мысль сделать свой маленький маршрутизатор, который бы мог на больших скоростях осуществлять шифрование и дешифрование. Идея в том, чтобы по VPN подключался не пользователь со своей машины, а сам роутер. Ну, и просто интересно было попробовать себя в новом. За счёт чего можно добиться ускорения криптографических алгоритмов? Если их выражать через арифметические и логические операции, получается медленно. В реализации на специализированных цифровых схемах можно добиться намного большей производительности. Хороший набор ссылок по теме можно найти в статье в Википедии про AES instruction set. Возможно реализовать алгоритм аппаратно на CPU и вызывать его выполнение через специальные инструкции. Известный пример — AES-NI от Intel. Однако процессоры встраиваемых систем часто таким функционалом не обладают или имеют ограничение на экспорт/импорт. В таком случае можно установить дополнительные периферийные устройства, которые сами будут обрабатывать данные. И, конечно, функциональность такой периферии можно реализовать и на FPGA. Решив перевести возможное в реальное, мы начали исследование аппаратного ускорения шифрования на FPGA. В ходе исследования способов связки аппаратного шифрования на FPGA и пользовательских программ мы на некоторое время застряли в изучении матчасти и думаем, что имеет смысл изложить здесь некоторые основные моменты. В ядре Linux реализовано множество криптографических алгоритмов: и симметричные шифры, и хеши, и режимы работы блочных шифров. Всё это может использоваться самим ядром: например, для шифрования дисков (dm-crypt) или работы VPN (IPsec). Обеспечением унифицированного доступа к криптографическим функциям занимается Kernel CryptoAPI, котороый позволяет драйверам регистрировать аппаратные реализации соответствующих алгоритмов. Пользовательские программы также могут обращаться к CryptoAPI. Одним из интерфейсов, предоставляющих такую возможность, является сокет address family AF_ALG и библиотека-обёртка над ним libkcapi. Основной конкурент AF_ALG — модуль ядра cryptodev (доступ к CryptoAPI через символьное устройство /dev/crypto). По словам авторов cryptodev, их решение намного производительнее, чем AF_ALG (см. сравнение).  На схеме показано CryptoAPI, где зарегистрированы две реализации AES-128: программная и аппаратная, обращение к которой происходит через соответствующий драйвер. Осуществить запрос к CryptoAPI могут, к примеру, IPsec, а также модули af_alg.ko и cryptodev.ko. Оба дают пользовательским программам возможность обращаться к криптографической подсистеме ядра, первый — через семейство адресов, второй — через символьное устройство. Как правило, это выполняется прозрачно: CryptoAPI само, получая запрос, выбирает, какой реализацией пользоваться, однако при желании можно узнать некоторые детали его работы через файл /proc/crypto. В нём заданы, в частности, такие поля: CryptoAPI, несмотря на то, что уже является высокоуровневой абстракцией, имеет ещё одну обёртку: в userspace мало кто обращается к нему напрямую и большинство программ предпочитает использование библиотек, к примеру, libcrypto и libssl из проекта openssl. Ими пользуются, например, openssh, opvenvpn и, вполне ожидаемо, openssl. Эти библиотеки поддерживают engines, — то, что обычно называется плагинами, механизмы добавления новых реализаций алгоритмов криптографии. Разработчиками openssl уже написаны engines для шифрования в ядре через AF_ALG и /dev/crypto. Поэтому множество программ автоматически получают доступ к аппаратной реализации криптоалгоритма, если она зарегистрирована в CryptoAPI.  На схеме изображены несколько программ, которые пользуются криптографическими функциями, предоставленными в библиотеках libssl и libcrypto, способных обратиться к CryptoAPI через /dev/crypto или AF_ALG. К примеру, в libcrypto для того и другого заданы cryptodev engine и afalg engine соответственно. Разобравшись с теорией, переходим к суровой реальности: описанию нашего практического опыта. При разработке драйвера ускорителя шифрования главный исследовательский вопрос заключался в том, какую пропускную способность между FPGA и пользовательскими программами мы сможем обеспечить на своих платах с CycloneV на борту (например, Ethond или BlueSom). В наших условиях это оказалось важно: когда шифрование происходит настолько быстро, основное время тратится на пересылку данных и синхронизацию происходящего в разных частях системы. Наш драйвер практически полностью определяется тем, что нам предоставляет железо. Поэтому начнём с описания аппаратной части.  На схеме упрощённо показана модель взаимодействия процессора и FPGA внутри SoC CycloneV. Для более точного и подробного описания можно обратиться к оригинальному изображению в главе "Introduction to the Hard Processor" из Cyclone V Device Handbook, Volume 3: Hard Processor Technical Reference Manual, увлекательной, но толстой книжки. Линукс с нашим драйвером запущен на MPU (Microprocessor Unit) внутри HPS (Hard Processor System). Процессор может обращаться к регистрам FPGA через L3 SWITCH по интерфейсу HPS-to-FPGA. Обращения к регистрам на схеме изображены голубыми стрелками. Устройства, реализованные в FPGA, могут обращаться к SDRAM-памяти через интерфейс FPGA-to-HPS по DMA (зелёные стрелки), а также слать процессору прерывания (красные стрелки). В FPGA реализованы две независимых сущности: ускоритель шифрования и ускоритель дешифрования. Каждый из них состоит из двух связанных блоков, один из которых реализует алгоритм (шифрование/дешифрование), а другой обменивается данными с памятью по DMA.  Обе пары "блоков", реализованных в FPGA, имеют свои регистры. Encrypt Core и Decrypt Core имеют идентичные наборы регистров, которые позволяют задать ключ (Key), вектор инициализации (IV, initialization vector), используемые при следующей операции шифрования/дешифрования. Decrypt DMA и Encrypt DMA также имеют зеркальную структуру. Через их регистры можно задать адреса и длины отрезков памяти — эти наборы параметров мы названы дескрипторами — в одной части которых находятся исходные данные, а в другую требуется разместить результат применения AES. Также доступна возможность включения и выключения прерываний для оповещения об окончании обработки каждого дескриптора. Расскажем чуть подробнее о некоторых концепциях криптографической подсистемы Linux. Одной из её основных сущностей являются "транформации" (transformation) — так называются любые преобразования данных: подсчет хеш-суммы, сжатие, шифрование. Драйвер может самостоятельно "зарегистрировать" трансформацию — предоставить возможность ей пользоваться. Класс типов трансформаций весьма обширен, однако шифрованием занимается лишь три их типа: В CryptoAPI также можно задавать "шаблоны" (templates) — реализации сложных сущностей, например, определенного режима работы блочного шифра или HMAC, на основе простых трансформаций вроде шифрования одного блока данных или подсчета хеш-суммы. В частности, существование шаблонов позволяет использовать CRYPTO_ALG_TYPE_CIPHER для реализации блочного шифра, хотя сама трансформация оперирует лишь 16-байтовыми блоками. Однако это сильно нагружает CPU: ядро само осуществляет управление режимом работы шифра. Трансформации CRYPTO_ALG_TYPE_BLKCIPHER и CRYPTO_ALG_TYPE_ABLKCIPHER же берут это на себя, и ядру не приходится применять шаблоны, обеспечивающие режим. Так как наша прошивка в FPGA сейчас реализует AES-128 в режиме CBC, CRYPTO_ALG_TYPE_CIPHER для нас интереса не представляет: у нас уже реализован требуемый режим работы и со стороны процессора не требуется дополнительных затрат. Наш драйвер предоставляет трансформацию типа CRYPTO_ALG_TYPE_BLKCIPHER, нам показалось, что её реализовать будет проще. Трансформация отдаёт CryptoAPI функции для задания IV (initialization vector), ключа, для шифрования и для дешифрования. Всё это указывается драйвером трансформации при её регистрации в полях структуры blkcipher_alg. Так выглядит эта структура: Рассмотрим самые интересные поля: setkey, callback для установки ключа, и encrypt / decrypt — для шифрования/дешифрования. Структура blkcipher_desc содержит в себе IV для операции шифрования. Параметрами src и dst callback'ов encrypt и decrypt задаются области памяти, из которых нужно взять исходные данные и в которых надо разместить результат. Теперь мы чуть лучше представляем, как в Линуксе реализован криптографический стек, и можем более ответственно рассмотреть различные варианты его построения с разными сопутствующими преимуществами и недостатками.  Очевидно, что если драйвер ускорителя шифрования должен использоваться и самим ядром — например, в IPsec — требуется зарегистрировать свою реализацию в CryptoAPI. В таком случае доступ к драйверу имеют как пользовательские програмы, так и Linux. Это отмечено на схеме как "Driver option 1". Однако существует и альтернативная возможность, обозначенная как "Driver option 2": реализация своего интерфейса для пользовательской среды в обход CryptoAPI и модулей cryptodev и af_alg. Это может показаться неправильным решением: редко уход от стандартизированных механизмов приводит к приятным результатам. Однако мы ещё недостаточно изучили вопрос, чтобы быть уверенными, что наш случай хорошо подходит под CryptoAPI и тот не накладывает существенные ограничения на производительность. В текущей реализации мы выбрали первый вариант, но готовы попробовать и второй. Когда ядро хочет, чтобы мы зашифровали или расшифровали что-то, оно вызывает наши функции fpga_encrypt и fpga_decrypt. Они имеют идентичные сигнатуры и делают одно и то же, только одна обращается к регистрам шифрующего устройства в FPGA, а вторая — к регистрам дешифрующего. Эти функции принимают два указателя на массивы из struct scatterlist. Каждый из них хранит последовательность областей памяти: src — откуда взять исходные данные, dst — куда положить результат. Гарантируется, что каждый из таких кусков памяти не пересекает границ одной страницы. Задача драйвера выглядит тривиальной: Но дьявол в деталях: версия DMA-контроллера в FPGA, которая у нас реализована, принимает только входные данные, имеющие размер, кратный 16 байтам. Скоро это ограничение будет снято, но пока FPGA-разработчики не успевают изменить прошивку, мы работаем с тем, что есть: копируем имеющие не кратную 16 байтам длину куски памяти в последовательный буфер.  Теперь, когда драйвер написан, требуется ответить на животрепещущий вопрос: а насколько он производительный? Конечно, можно было бы для замеров использовать openssl, однако мы решили написать свою программу openssl_benchmark.c: openssl не предоставляет нам достаточной гибкости. В частности, нельзя установить точный размер одиночного буфера, который будет отослан в libcrypto, поскольку openssl может решить обрабатывать входные данные по частям. Также показатели производительности становится сложнее различить, так как тратится некоторое время на ввод/вывод, выделение памяти, инициализацию openssl и тому подобное. Наша программа работает просто: она выделяет и зануляет буферы заданного размера в качестве входных данных, а затем отсылает их в libcrypto на обработку указанное число раз. Замеряет она только время, потраченное на вызовы libcrypto. В силу того, что обработка происходит многократно, можно весьма точно установить производительность именно выполнения AES без учёта потерь, вызванных вспомогательными задачами вроде получения входных данных. С помощью этой программы мы шифровали и дешифровали буферы разной длины (по 1000 операций обоих типов для каждой длины буфера). Потом посчитали пропускную способность в Мбит/сек. Всё это мы проделали дважды: в первом случае libcrypto шифровала наши данные своей софтовой реализацией ("Software" на графиках), во втором — отдавала ядру через cryptodev engine ("Hardware" на графиках). 
 Особый интерес представляет момент, в который аппаратная реализация настигает программную по пропускной способности. Проведём измерения на меньшем промежутке и с меньшими интервалами: 
 Мы видим, что производительность программной реализации практически не зависит от размера буфера. Это можно объяснить тем, что обмен данными между нашей программой и libcrypto происходит очень быстро и с ростом размера буфера быстро становится незаметной трата времени на вызов функций. С шифрованием на FPGA ситуация сложнее. Обращение к librypto передается cryptodev engine, тот открывает /dev/crypto, несколькими системными вызовами настраивает сессию для шифрования и только потом передает указатели на буферы для шифрования в модуль cryptodev. Тот, в свою очередь, формирует в виде struct scatterlist * два списка физических страниц, на которые спроецирован пользовательский буфер в его виртуальном адресном пространстве, и передаёт в CryptoAPI. Наш драйвер имеет наивысший приоритет из всех зарегистрированных реализаций AES, поэтому списки получает он. Когда драйвер убеждается в том, что длины всех кусков памяти кратны длине блока AES, он получает соответствующие шинные адреса и записывает их в регистры FPGA. Мы видим, что между FPGA и пользовательскими программами запросы должны пройти сквозь несколько слоёв и потери времени на них очень велики. Лишь на данных весьма большого объёма на пропускную способность перестаёт сильно влиять время на осуществление запроса. На графике этот момент можно определить по тому, что линия становится почти горизонтальной: основная часть времени тратится именно на обработку данных. Читатель, который посмотрит не только на форму линий, но и на конкретные числовые значения, конечно, удивится: почему дешифрование стабилизируется на 250 Мбит/с, а шифрование — на 400 Мбит/с? На самом деле драйвер тут совершенно ни при чём, так происходит потому, что в FPGA, несмотря на зеркальный интерфейс, процедуры шифрования и дешифрования реализованы по-разному. Выясним теперь, насколько нагружается CPU при использовании аппаратного шифрования. С нагрузкой на CPU при работе программной реализации шифрования всё и так очевидно: CPU должен использоваться на сто процентов, так как именно он является узким местом. Беглая проверка показывает, что так и есть. Мы запустили аппаратное шифрование и дешифрование на буферах разной длины в цикле и тем временем смотрели в соседнем терминале на наргузку на CPU в top. Понятно, что измерения "на глаз" при помощи top будут не самыми точными, но мы решили, что некоторое представление они всё-таки дают. Вот полученные результаты: Дешифрование даёт большую нагрузку на процессор из-за того, что дешифрующая часть в FPGA обрабатывает наши данные с меньшей задержкой и драйверу приходится чаще просыпаться, чтобы загрузить в FPGA новую порцию данных. Видно, что с ростом объёма данных, отдаваемых драйверу за одно обращение из пользовательского пространства, снижается нагрузка на CPU. Это можно объяснить тем, что на больших объёмах обращения к драйверу происходят реже, избавляя его от лишних накладных расходов. Между молотом FPGA и наковальней программ в пользовательском пространстве живётся на удивление непросто. Основная проблема в положении драйвера заключается в очень высоких требованиях к скорости обмена: лишь на больших объёмах данных становится заметным преимущество аппаратной реализации. Слишком много времени тратится на прохождение запроса через весь стек. Однако начиная с некоторого объёма аппаратная реализация существенно снимает нагрузку с процессора и становится быстрее программной в полтора-два раза. Информационная Безопасность