Превью
Криптографии в разработке системных и не только приложениях принято уделять
повышенное внимание. Особенно сейчас, когда набитость сундуков многих
благородных сэров, как и степень их свободы, напрямую зависит от степени защиты
их конфиденциальной информации. Чтобы сэкономить время и не ходить лишний раз к
проктологу, воспользуемся тем, что большинство стойких и эффективных алгоритмов
уже реализовано профессионалами этого дела. Плод их трудов открыто
предоставляется на коммерческой и бесплатной основе. Разработчики операционных
систем тоже не сидят сложа руки, предоставляя разработчикам специальные API. В
этой статье мы хотим рассказать об использовании таких API в модуле для
операционной системы Linux, который будет шифровать исходящий трафик и
расшифровывать входящий.
Гоу
А начнем, пожалуй, с подготовки системы. Для начала надо убедится, что
система готова к созданию разного рода приложений, а конкретно к разработке
модулей ядра. Помимо присутствия GCC надо еще убедиться в том, что ядро было
скомпилировано с поддержкой модульности и криптографических API. Про установку
GCC и включение модулей написано много мануалов, поэту не будем тратить ни
времени, ни чернил, а про то, как создавать эти самые модули, уже не раз
писалось на страницах ][, так что мы предполагаем, что читатель уже овладел
необходимыми навыками системного программирования (ведь он читает ][ ;-)).
Крипто API включаются во вкладке Cryptographic API, которую можно наблюдать если
находясь в каталоге с исходниками ядра из консоли набрать "make menuconfig". А
самый легкий способ проверки их доступности это попытаться скомпилировать
пример, который находится все в тех же исходниках ядра. Заходим в папку с
исходниками, дальше (как не трудно догадаться) в директорию crypto и компилируем
tcrypt.c. Если он компилируется без фатальных ошибок и даже работает, то
собираемся с духом и идем дальше.
Вот из ит
Теперь надо немного разобраться - что же все таки мы подключили и где это
найти. Исходники этого всего дела лежат в той же директории, что и упомянутый
выше tcrypt.c, а сам он является примером в котором показано как использовать все
реализованные в Cryptographic API алгоритмы. Пример этот хорошо комментирован, а
если что-то остается не совсем понятным, то всегда можно заглянуть в лежащие
рядом исходники самих Cryptographic API. Но, как говорится, сказать или даже
написать проще чем сделать. На практике довольно часто можно свалиться в штопор
в самых неожиданных местах, и потом довольно долго по частям собирать то, что
всего пять минут назад летало со скоростью звука. К сожалению ошибки всегда
банальны, но постоянны, и каждый раз вспоминать как приклеивается один и тоже
кусок становится утомительным. Сделать один раз инструкцию и со временем её
дополнять гораздо эффективнее и проще. Относительно наших API можно сказать тоже
самое. Нормального мана на русском языке по ним нам найти не удалось, а повторно
вкуривать одну и туже траву это не тру. Поэтому хотим оставить своего рода
рабочие записки и надеемся, что это еще кому-нибудь поможет.
Литл море
Предположим, мы хотим реализовать немного устаревший, но достаточно быстрый и
проверенный алгоритм DES. У этого алгоритма существует несколько режимов:
- ECB (Electronic Code Book) - режим где все блоки шифруются независимо
друг от друга и не сцепляются; - CBC (Cipher Block Chaining)- режим, в котором результат шифрования
предыдущего блока используется для шифрации текущего.
Под блоками тут подразумевается часть шифротекста (не будем же мы весь текст
шифровать за раз в самом деле). А аббревиатуры лучше всего запомнить, чтобы они
не сбивали с толку если понадобиться исследовать исходники самих API. Так с виду
один и тот же алгоритм может быть реализован в разных режимах. Если кому
интересно, то в качестве домашнего задания можно еще самостоятельно покурить CFB
и OFB.
Однако, мы отвлеклись от темы. Для своих целей выберем режим CBC как более
надежный и приступим, сделав наброски будущего кода.
Как это часто бывает, начнем с написания и описания ключевых структур и
переменных:
struct completion comp;
struct scatterlist sg[8];
struct crypto_ablkcipher *tfm;
struct ablkcipher_request *req;
Переменная comp служит для синхронизации выполнения между нашей функцией и
непосредственно шифрованием. Суть этого станет ясна чуть ниже. А пока рассмотрим
саму структуру. Она объявлена в include/linux/completion.h и содержит всего два
поля:
struct completion {
unsigned int done;
wait_queue_head_t wait;
};
Первое это флаг выполнения, а второе указатель на очередь ожидающих задач.
Следующая переменная sg описана в include/linux/scatterlist.h. Это структура
платформозависимая, и на i386 платформе она определена следующим образом:
struct scatterlist {
struct page *page;
unsigned int offset;
dma_addr_t dma_address;
unsigned int length;
};
Заполнение полей этой структуры вполне можно поручить функции sg_init_one().
В этой функции определяется страница памяти, с которой "начинается" buf, и
определяется смещение указателя buf относительно адреса начала страницы.
tfm и есть наша основная структура, в файле include/linux/crypto.h находится
ее описание:
struct crypto_ablkcipher {
struct crypto_tfm base;
};
Не очень информативно, поэтому посмотрим чуть выше, где находится:
struct crypto_tfm {
u32 crt_flags;
union {
struct ablkcipher_tfm ablkcipher;
struct aead_tfm aead;
struct blkcipher_tfm blkcipher;
struct cipher_tfm cipher;
struct hash_tfm hash;
struct compress_tfm compress;
} crt_u;
struct crypto_alg *__crt_alg;
void *__crt_ctx[] CRYPTO_MINALIGN_ATTR;
};
Уже что-то, не правда ли? Первым полем идут флаги, о некоторых из них
расскажем чуть ниже. Далее объединение более низкоуровневых структур для разных
типов криптографических задач, прямой доступ к которым использовался в
предыдущих версиях. В crypto_alg содержится указатель на структуру в которой
есть все необходимое для корректной работы непосредственно алгоритма шифрования.
А именно имя самого алгоритма, приоритет и т.д. (про это можно подробнее отсюда
http://diploma-thesis.siewior.net/html/diplomarbeitch4.html).
Следующая переменная, req, содержит указатель на структуру запросов struct
ablkcipher_request:
struct ablkcipher_request {
struct crypto_async_request base;
unsigned int nbytes;
void *info;
struct scatterlist *src;
struct scatterlist *dst;
void *__ctx[] CRYPTO_MINALIGN_ATTR;
};
С помощью этой переменной мы обращаемся к страницам памяти где находится
шифрованный и расшифрованный текст.
Когда мы разобрались с основными переменными можно приступить к написанию
самого кода. Начнем с инициализации переменной comp с помощью функции
init_completion():
init_completion(&comp);
Эта функция обнулят флаг done и добавляет comp в голову очереди
wait_queue_head_t, о которых рассказывалось выше. Подробнее о механизме работы
этой структуры и о том, для чего мы ее используем, будет рассказано еще чуть ниже
;).
Теперь выделим память для нашей основной переменной:
tfm = crypto_alloc_ablckhipher ("cbc(des)", 0, CRYPO_TFM_REQ_WEAK_KEY);
В качестве первого параметра мы передаем имя алгоритма, второй параметр это тип
шифрования (вспомните объединение crt_u в описании структуры), третьим параметром
мы передаем флаги. Переданный флаг означает, что алгоритм должен принимать даже
слабые ключи.
Далее выделяем память для ablkcipher_request:
req = ablkcipher_request_alloc (tfm, GFP_KERNEL);
С первым параметром думаю все понятно и так. Непосредственно память под
структуру выделяется функцией kmalloc, которая в качестве параметра принимает
флаг, говорящий о том как именно стоит выделять память. Флаг GFP_KERNEL (GFP -
Get Free Page) резервирует блок памяти, выделяя страницы памяти по мере
обращения к ним. Существует и другие флаги например:
- GFP_ATOMIC выделяет требуемую память немедленно (при необходимости вытесняя
другие страницы на диск); - GFP_BUFFER никогда не вытесняет другие страницы, и если запрошенная память
недоступна, с выделением наступает облом.
Фактически приходится выбирать между GFP_ATOMIC и GFP_KERNEL. Обычно используют
GFP_KERNEL, так как он ведет себя не столь агрессивно.
Потом установим ключ которым будем шифровать:
ret = crypto_ablkcopher_sekey (tfm, key, strlen(key));
Эта функция возвращает 0 в случае успеха и код ошибки больший нуля в случае
неудачи. К первому параметру думаю опять не возникает никаких вопросов, да и с
остальными ничего сложного: второй - указатель на строку с ключом, третий -
длина.
Теперь позаботимся о расположении текста который будем шифровать:
sg_init_one (&sg[0], text, strlen(text));
Стоит отметить, что текст должен быть кратен восьми, иначе при попытке
зашифровать\дешифровать получите ошибку -21 (что означает «некорректные входные
данные»). То, как с этим бороться, можно посмотреть в приложенном коде. Сама же
функция просто заполняет страницы памяти текстом, который собираемся зашифровать
или дешифровать :).
Подготовим запрос для шифрования/дешифрования, сообщив ему с какими страницами и
текстом какой длины предстоит работать:
u8 iv = {0xff, 0xdc, 0xba, 0x98, 0x76, 0x54, 0x32, 0x10};
ablkcipher_request_set_crypt (req, sg, sg, strlen(text), iv);
Последним параметром передается инициализирующий вектор, который также необходим
для шифрования и расшифровки и служит своего рода открытым ключом. Его можно
вычислять каким-либо образом от самого ключа, просить пользователя вводить его
или делать постоянным как в нашем случае для упрощения алгоритма.
Теперь приступаем непосредствнно к шифрованию:
ret = enc ? crypto_ablkcipher_encrypt (req) : crypto_ablkcipher_decrypt(req);
Сами функции и их параметр в пояснении думаю не нуждаются, если не понятно
перечитайте еще раз внимательно о req. Ну с ret все совсем понятно скажите вы. А
вот и нет :). Да, ret в случае удачного выполнения действительно будет равна 0.
Но вот остальные значения не всегда означают ошибку.
Теперь пришло время рассказать совсем подробно о роли переменной comp и этом
механизме.
switch (ret) {
case 0:
break;
case -EINPROGRESS: case -EBUSY:
printk ("\nwait\n");
ret = wait_for_completion_interruptible (&comp);
if (!ret) {
INIT_COMPLETION(comp);
break;
}
default:
printk ("failred err=%d", -ret);
goto out;
}
Вспомните описание структуры completion, непонятные слова, которые мы говорили о
флаге выполнения и очереди задач. Дело в том, что криптоалгоритмы не так быстры
как хотелось бы. Но как же так, скажите вы, как мы можем вернутся в вызвавшую
функцию до того, как завершилась вызываемая? Дело в том, что авторы этой
библиотеки очень хитрые люди и оптимизировали работу криптоалгоритмов в расчете
на распараллеливание вашего алгоритма и с учетом того, что к шифруемым/дешифруемым
данным будут обращаться другие алгоритмы. Поэтому и получилось так, что мы можем
вернутся в вызвавшую функцию до того, как шифрование/дешифрование завершится. Для
того, чтобы избежать обращения к еще не зашифрованным/не расшифрованным данным, мы
и используем структуру completion. Механизм работы этой структуры и функций, с
ней связанных, чрезвычайно идентичен механизму работы блокирующих семафоров.
Когда работа криптоалгоритма будет завершена он выставит поле done в 1 и наша
функция продолжит работу. Если вы знакомы с семафорами, то вопросов не возникнет,
если нет - настоятельно рекомендуем познакомится.
Ду ит нау
Сам модуль можно найти в приложении к данной статье. Код подробно
прокомментирован и не должен вызывать затруднений. По сути он добавляет свою
фукцию (nethook) по обработке полученных и отправленных пакетов на определенное
устройство, символьное имя которого задается параметром device_name. А за
шифрование/дешифрование отвечает функция my_des, в ней применено все то, о чем мы
писали выше. Инструкция по сборке и установке лежит рядом. Дерзайте 😉