Расширение и использование Linux Crypto API

[0] Интро


Криптографический API в Linux введён с версии 2.5.45 ядра. С тех пор Crypto API оброс всеми популярными (и не только) международными стандартами:


  • симметричного шифрования: AES, Blowfish, …
  • хэширования: SHA1/256/512, MD5, …
  • имитозащиты: hMAC-SHA1/256/512, hMAC-MD5, …
  • AEAD: AES-GCM, ChaCha20-Poly1305, …
  • генерации псевдослучайных чисел: FIPS, …
  • асимметричного шифрования: RSA
  • согласования ключей: DH, ECDH
  • сжатия: Deflate, ZLIB


Эта криптография доступна и, в основном, используется различными подсистемами ядра (в kernel space): IPsec, dm-crypt и др. Воспользоваться функциями Crypto API из пространства пользователя (user space) также возможно через Netlink-интерфейс, в который, начиная с версии 2.6.38 ядра, введено семейство _AFALG, предоставляющее доступ к криптографии ядра из user space кода. Однако, существующего функционала иногда недостаточно, таким образом появляется необходимость расширения Crypto API новыми алгоритмами.


Так, примерно два года назад передо мной стояла задача встраивания национальной (в Республике Беларусь) криптографии в IPsec-реализацию ядра Linux (версии 3.13). На тот момент я никогда не занимался программированием в kernel space и это было моим первым опытом написания модулей ядра, с чем мне очень помогла книга Роберта Лава «Ядро Linux. Описание процесса разработки». Куда сложнее было разбираться с самим Crypto API — в упомянутой книге этот вопрос не освещается в принципе, а в официальной документации, хоть и дано достаточно подробное описание архитектуры, практически нет информации о внедрении новых алгоритмов. В своих поисках, в журнале «Хакер» я наткнулся на замечательную статью, популярно рассказывающую о том, как использовать Crypto API, но, опять же, там нет ни слова о его расширении. В конце концов, за необходимыми мне знаниями я обратился к первоисточнику (к коду ядра), и решил поставленную задачу.


С тех пор уже прошло некторое время и только сейчас, актуализировав свои знания, я взялся за написание этого текста. Он о Crypto API. На конкретных примерах мы посмотрим, каким образом в Crypto API можно встроить произвольные алгоритмы и как к ним обратиться как из kernel space, так и из user space. Я уверен, что этот материал будет крайне полезен разработчикам, столкнувшимся со схожими проблемами, но надеюсь, что и случайный читатель сможет найти здесь для себя что-то интересное. Добро пожаловать под кат!


P.S. Компилируемые исходники моих программ, фрагменты которых будут встречаться ниже, доступны на github. Там же — исходник этой статьи. Весь код был написан и протестирован под десктопной Ubuntu 16.04 LTS с ядром версии 4.13.0–32-generic. Некоторые интерфейсы, которые я использовал в модулях, были введены в 4 версии ядра, поэтому на ядрах 3.* они точно НЕ скомпилируются.



[1] Архитектура Crypto API


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


Итак, из введения мы уже знаем, что криптографическая подсистема ядра включает множество реализаций криптографических алгоритмов и предоставляет к ним интерфейсы, доступные пользователю (здесь и далее — под пользователем и разработчиком имеются ввиду, соответственно, пользователь API и разработчик API). В терминах Crypto API, алгоритмы называются «преобразованиями» (transformations), так, различные типы преобразований имеют собственный под-API, а дескрипторам (хэндлам) преобразований, как правило, присваивается имя »tfm», например:


struct crypto_cipher *tfm = crypto_alloc_cipher("aes", 0, 0);


Здесь tfm — это хэндл некоторого преобразования, в данном случае — шифрования блока данных по алгоритму AES. Жизненный цикл хэндла Crypto API сводится к трём этапам:


  • создание/инициализация хэндла
  • выполнение необходимых криптоопераций, досупных через хэндл
  • уничтожение хэндла


Независимо от используемого алгоритма, через хэндл типа crypto_cipher доступны реализации базовых функций шифрования одного блока данных, а соответсвующий ему API (Single Block Cipher API) предоставляет методы установки ключа и зашифрования/расшифрования блока. В качестве самостоятельного API он не представляет большого интереса для пользователя, однако, с точки зрения разработчика, он важен, поскльку с его помощью реализована ключевая концепция Crypto API — так называемые «шаблоны» (templates). Шаблоны используются в связке с базовыми функциями и реализуют:


  • режимы блочного шифрования (ECB, CBC, CFB, CTR, …)
  • режим ключезависимого хэширования (hMAC)
  • AEAD-режимы шифрования
  • функции, не связанные с непосредственной криптообработкой данных, например — распараллеливание (pcrypt)


При создании хэндла (см. пример выше), требуемый алгоритм задётся строкой — именем алгоритма, эта строка имеет следующую семантику:


template(single block cipher/message digest)


Кроме того, там, где это применимо, шаблоны могут быть «завёрнуты» в другие шаблоны:


template_a(template_b(single block cipher/message digest))


При этом использование шаблонов без базового алгоритма невозможно. Приведу несколько примеров:


  • aes — базовый алгоритм шифрования блока AES
  • cbc (aes) — алгоритм шифрования AES в режиме сцепления блоков шифра (CBC)
  • sha1 — алгоритм хэширования SHA1
  • hmac (sha1) — алгоритм ключезависимого хэширования (имитозащиты) с хэш-функцией SHA1
  • authenc (hmac (sha1), cbc (aes)) — AEAD с алгоритмом шифрования AES-CBC и алгоритмом имитозащиты hMAC (SHA1)


Применение симметричных блочных шифров в каком-либо режиме осуществляеся через отдельный API. Стоит отметить, что сейчас в ядре существуют три таких API, однако два из них объявлены устаревшими (deprecated), а актуальным является Symmetric Key Cipher API. Этот API предоставляет пользователю методы установки ключа/синхропосылки и зашифрования/расшифрования данных произвольной длины, он так же является неблокирующим (асинхронным) — методы шифрования максимально быстро возвращают управление вызывающему, в то время как запрос на выполнение криптооперации и функция обратного вызова, используемая для оповещения пользователя о завершении операции, передаются системному планировщику. Symmetric Key Cipher API будет подробно рассмотрен, когда мы перейдём к созданию модуля ядра, тестирующего встроенный нами алгоритм.


Помимо двух названных, в Crypto API существуют под-API для:


  • AEAD-шифрования (Authenticated Encryption With Associated Data (AEAD) Cipher API)
  • блокирующего хэширования/имитозащиты (Synchronous Message Digest API)
  • неблокирующего хэширования/имитозащиты (Asynchronous Message Digest API)
  • генерации псевдослучайных чисел (Random Number API)
  • асимметричного шифрования (Asymmetric Cipher API)
  • согласования ключей (Key-agreement Protcol Primitives (KPP) Cipher API)


Для разработчика наличие шаблонов в Crypto API означает, что, встраивая новый алгоритм симметричного шифрования, достаточно реализовать лишь базовую функцию шифрования блока, которая, затем, может быть инкапсулирована в соответствующие шаблоны для получения требуемых режимов. Но это не совсем так. Действительно, если взглянуть на исходный код включенных в ядро алгоритмов (crypto/aes_generic.c, crypto/blowfish_generic.c, …), то можно увидеть, что там реализованы только базовые функции. Однако, если мы таким же образом реализуем функцию шифрования блока классического ГОСТ 28147–89, «завернём» её в режим гаммирования (шаблон CTR), а после этого полученный алгоритм проверим на тестовых последовательностях, то мы получим неверный результат! Всё дело в том, что режим гаммирования, описанный в ГОСТ, отличается от алгоритма гаммирования, реализованного в шаблоне CTR. То же самое касается и других национальных алгоритмов, с которыми я имел дело. В таких случаях необходимо встраивать полноценные блочные шифры в нужных режимах, как это сделано, например, в оптимизированной реализации алгоритма AES (AES-NI — arch/x86/crypto/aesni-intel_glue.c). Позже мы рассмотрим оба варианта встраивания.


Пожалуй, это всё, что я хотел сказать по поводу архитектуры. Заинтересованным в более подробном описании следует обратиться к документации, а моего изложения должно быть достаточно, чтобы мы могли двигаться дальше.



[2] Подготовка почвы


Итак, мы готовы к тому, чтобы начать встраивать в Crpyto API какой-нибудь новый алгоритм, с той лишь оговоркой, что у нас пока нет алгоритма, который можно было бы встроить. Для этой статьи я не стал утруждать себя реализацией «настоящего» криптоалгоритма, поскольку едва ли смог бы сделать это качественно (оставьте это криптографам). С другой стороны, делать шифр-«пустышку» (null cipher) было бы не так интересно, тем более, что такой в ядре уже есть. Таким образом, мы пойдём на компромисс и напишем реализацию элементарного алгоритма шифрования:


303cc3265be54ed476594f65674d8ace.svg


940f8d92d1ac3857a38a2d08d6da098b.svg


Где:


  • inline_formula — блок шифртекста
  • inline_formula — блок открытого текста
  • inline_formula — ключ
  • inline_formula — операция «исключающее ИЛИ» (XOR)


Размеры блока и ключа для этого алгоритма примем равными 128 битам (16 байт).


Можем приступать к реализации. Определим криптоконтекст нашего алгоритма и функции создания/уничтожения контекста:


#define XOR_CIPHER_KEY_SIZE 16

typedef struct xor_cipher_ctx xor_cipher_ctx;

struct xor_cipher_ctx
{
    uint8_t key[XOR_CIPHER_KEY_SIZE];
};

xor_cipher_ctx* xor_cipher_allocate()
{
    xor_cipher_ctx *cipher = calloc(1, sizeof(xor_cipher_ctx));
    return cipher;
}

void xor_cipher_free(xor_cipher_ctx *ctx)
{
    memset(ctx->key, 0xFF, XOR_CIPHER_KEY_SIZE);
    free(ctx);
}


Добавляем методы установки ключа и зашифрования/расшифрования блока:


#define XOR_CIPHER_BLOCK_SIZE 16

void xor_cipher_set_key(xor_cipher_ctx *ctx, uint8_t *key)
{
    memmove(ctx->m_key, key, XOR_CIPHER_KEY_SIZE);
}

void xor_cipher_crypt_block(xor_cipher_ctx *ctx, uint8_t *dst, uint8_t *src)
{
    for (int i = 0; i < XOR_CIPHER_BLOCK_SIZE; i++)
    {
        dst[i] = src[i] ^ ctx->key[i];
    }
}


Учитывая обратимость операции »XOR», метод xor_cipher_crypt_block используется для зашифрования и расшифрования одновременно.


Шифрование блока — это хорошо, но станет ещё лучше, если мы реализуем какой-нибудь из режимов блочного шифрования, например, режим сцепления блоков шифра (CBC):


21cb89819f62e4c9954bab449892d9ec.svg


afad3d27760082b4dd6f41f5251e14dc.svg


9bd53d3318426661b03dddc58e4e02f5.svg


Где:


  • inline_formula — синхропосылка
  • inline_formula, inline_formula — функция зашифрования и расшифрования блока на ключе inline_formula соответственно


Продолжим работу, реализуем методы зашифрования и расшифрования в режиме сцепления блоков шифра.


Зашифрование
void xor_cipher_encrypt_cbc(xor_cipher_ctx *ctx,
        uint8_t *_dst, uint32_t len, uint8_t *_src, uint8_t *_iv)
{
    uint32_t blocks = len / XOR_CIPHER_BLOCK_SIZE;
    uint32_t leftover = len - (blocks * XOR_CIPHER_BLOCK_SIZE);

    uint8_t *dst = _dst, *src = _src, *iv = _iv;

    for (uint32_t i = 0; i < blocks; i++)
    {
        memmove(dst, src, XOR_CIPHER_BLOCK_SIZE);

        for (int j = 0; j < XOR_CIPHER_BLOCK_SIZE; j++)
        {
            dst[j] ^= iv[j];
        }

        xor_cipher_crypt_block(ctx, dst, dst);

        iv = dst;
        dst += XOR_CIPHER_BLOCK_SIZE;
        src += XOR_CIPHER_BLOCK_SIZE;
    }

    if (leftover)
    {
        memmove(dst, src, leftover);

        for (uint32_t i = 0; i < leftover; i++)
        {
            dst[i] ^= iv[i];
            dst[i] ^= ctx->key[i];
        }
    }
}


Расшифрование
void xor_cipher_decrypt_cbc(xor_cipher_ctx *ctx,
        uint8_t *_dst, uint32_t len, uint8_t *_src, uint8_t *_iv)
{
    uint32_t blocks = len / XOR_CIPHER_BLOCK_SIZE;
    uint32_t leftover = len - (blocks * XOR_CIPHER_BLOCK_SIZE);
    uint8_t u[XOR_CIPHER_BLOCK_SIZE], iv[XOR_CIPHER_IV_SIZE];

    uint8_t *dst = _dst, *src = _src;

    memmove(iv, _iv, XOR_CIPHER_IV_SIZE);

    for (uint32_t i = 0; i < blocks; i++)
    {
        memmove(u, src, XOR_CIPHER_BLOCK_SIZE);
        xor_cipher_crypt_block(ctx, dst, src);

        for (int j = 0; j < XOR_CIPHER_BLOCK_SIZE; j++)
        {
            dst[j] ^= iv[j];
        }

        memmove(iv, u, XOR_CIPHER_IV_SIZE);
        dst += XOR_CIPHER_BLOCK_SIZE;
        src += XOR_CIPHER_BLOCK_SIZE;
    }

    if (leftover)
    {
        for (uint32_t i = 0; i < leftover; i++)
        {
            dst[i] = src[i] ^ ctx->key[i];
            dst[i] ^= iv[i];
        }
    }
}


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


Тестовые последовательности
Шифрование блока
Ключ 2f 1b 1a c6 d1 be cb a2 f8 45 66 0d d2 97 5c a3
Тест №1
Входные данные cc 6b 79 0c db 55 4f e5 a0 69 05 96 11 be 8c 15
Выходные данные e3 70 63 ca 0a eb 84 47 58 2c 63 9b c3 29 d0 b6
Тест №2
Входные данные 53 f5 f1 ef 67 a5 ba 6c 68 09 b5 7a 24 de 82 5f
Выходные данные 7c ee eb 29 b6 1b 71 ce 90 4c d3 77 f6 49 de fc

Зашифрование CBC
Ключ ec 8d 93 30 69 7e f8 63 0b f5 58 ec de 78 24 f2
Синхропосылка db 02 1f a8 5a 22 15 cf 49 f7 80 8b 7c 24 a1 f3
Открытый текст 6e 96 50 42 84 d2 7e e8 44 9b 75 1d e0 ac 0a 58 ee 40 24 cc 32 fc 6e c4 e2 fc d1 f5 76 6a 45 9a e4 88 ba d6 12 07 28 86
Шифртекст 59 19 dc da b7 8e 93 44 06 99 ad 7a 42 f0 8f 59 5b d4 6b 26 ec 0c 05 e3 ef 90 24 63 ea e2 ee 31 53 d1 42 c0 97 75 d5 06

Расшифрование CBC
Ключ ec 8d 93 30 69 7e f8 63 0b f5 58 ec de 78 24 f2
Синхропосылка db 02 1f a8 5a 22 15 cf 49 f7 80 8b 7c 24 a1 f3
Шифртекст db e9 1d c6 1f 13 1a 5a 34 2b 90 1e c3 b1 6f e9 52 1b 91 7f 8d 8f 6d b4 42 87 ad 85 5f 2d 89 7d
Открытый текст ec 66 91 5e 2c 4f f7 f6 76 29 48 79 61 ed ea e8 65 7f 1f 89 fb e2 8f 8d 7d 59 65 77 42 e4 c2 66


Исходники реализации алгоритма и программы тестирования доступны здесь, а мы практически готовы к тому, чтобы перейти в kernel space, но перед этим я хочу обратить ваше внимание на один важный момент. Дело в том, что многие известные алгоритмы шифрования не предполагают обработку последнего неполного блока входных данных. Так, например, реализации алгоритма ГОСТ 28147–89 ДОЛЖНЫ возвращать признак ошибки, если размер входных данных не кратен размеру блока, тогда как белорусский Belt предусматривает такую обработку. Мой алгоритм также её предусматривает (ключ и текущее значение синхропосылки усекаются до размеров неполного блока). Этот факт сыграет свою роль немного позже, пока стоит просто иметь это в виду.



[3] Погружение в ядро


Действие переносится в пространство ядра. Программирование в ядре несколько отличается от программирования в пространстве пользователя и, ввиду сложности отладки, является достаточно трудоёмким процессом. Однако, работая над этим материалом, я не ставил перед собой задачу познакомить читателя с основами программирования модулей ядра, ведь об этом и без меня предостаточно написано, в том числе и здесь, на Хабре (ещё; и ещё). Поэтому, если читатель совершенно не знаком с написанием модулей, то я бы рекомендовал сперва просмотреть те материалы, которые я привёл выше, либо поискать самостоятельно (уверяю, это не займёт много времени). А с теми, кто готов идти дальше, мы приступаем к глубокому изучению Crypto API.


Итак, у нас есть алгоритм, и мы хотим встроить его в ядро, но с чего же начать? Разумеется, с документации. К сожалению, раздел, посвящённый разработке/встраиванию алгоритмов, даёт лишь очень общие знания об этом процессе, но, по крайней мере, помогает сориентироваться в правильном направлении. Конкретно, мы узнаём о существовании функций, отвечающих за регистрацию и разрегистрацию алгоритмов в ядре. Давайте разбираться:


/* include/linux/crypto.h */

int crypto_register_alg(struct crypto_alg *alg);
int crypto_register_algs(struct crypto_alg *algs, int count);

int crypto_unregister_alg(struct crypto_alg *alg);
int crypto_unregister_algs(struct crypto_alg *algs, int count);


Эти функции возвращают отрицательное значение в случае ошибки, и 0 — в случае успешного завершения, а (раз)регистрируемый (ые) алгоритм (ы) описываются структурой crypto_alg, заглянем в её определение (include/linux/crypto.h):


/* include/linux/crypto.h */

struct crypto_alg {
    struct list_head cra_list;
    struct list_head cra_users;

    u32 cra_flags;
    unsigned int cra_blocksize;
    unsigned int cra_ctxsize;
    unsigned int cra_alignmask;

    int cra_priority;
    atomic_t cra_refcnt;

    char cra_name[CRYPTO_MAX_ALG_NAME];
    char cra_driver_name[CRYPTO_MAX_ALG_NAME];

    const struct crypto_type *cra_type;

    union {
        struct ablkcipher_alg ablkcipher;
        struct blkcipher_alg blkcipher;
        struct cipher_alg cipher;
        struct compress_alg compress;
    } cra_u;

    int (*cra_init)(struct crypto_tfm *tfm);
    void (*cra_exit)(struct crypto_tfm *tfm);
    void (*cra_destroy)(struct crypto_alg *alg);

    struct module *cra_module;
} CRYPTO_MINALIGN_ATTR;


К счастью, эта структура очень хорошо задокументирована, и нам не придётся гадать о значении того или иного поля:


  • cra_flags: набор флагов, описывающих алгоритм. Флаги определены константами, начинающимися с »CRYPTO_ALG_», в include/linux/crypto.h, и используются для «тонкой настройки» описания алгоритма
  • cra_blocksize: байтовый размер блока алгоритма. Все типы преобразований, кроме хэширования, возвращают ошибку при попытке обработать данные, размер которых меньше этого значения
  • cra_ctxsize: байтовый размер криптоконтекста. Ядро использует это значение при выделении памяти под контекст
  • cra_alignmask: маска выравнивания для входных и выходных данных. Буферы для входных и выходных данных алгоритма должны быть выровнены по этой маске. Это обязательно для алгоритмов, выполняющихся на «железе», неспособном обращаться к данным по произвольным адресам
  • cra_priority: приоритет данной реализации алгоритма. Если в ядре зарегистрировано больше одного преобразования с одинаковым cra_name, то, при обращении по этому имени, будет возвращён алгоритм с наибольшим приоритетом
  • cra_name: название алгоритма. Ядро использует это поле для поиска реализаций
  • cra_driver_name: уникальное имя реализации алгоритма. Если в ядре зарегистрировано больше одного преобразования с одинаковым cra_name, и необходимо запросить алгоритм с меньшим приоритетом, то нужно обращаться по этому имени
  • cra_type: тип криптопреобразования. Указатель на экземпляр структуры типа crypto_type, реализующий функции обратного вызова, общие для всех типов преобразований. Доступные варианты: &crypto_blkcipher_type, &crypto_ablkcipher_type, &crypto_ahash_type, &crypto_rng_type. Это поле следует оставлять пустым для следующих типов преобразований: шифрования блока (cipher), сжатия (compress), блокирующего хэширования (shash)
  • cra_u: собственно, реализация алгоритма. Одна из структур союза, должна быть заполнена специфическими функциями, определяемыми типом преобразования. Это поле следует оставлять пустым для блокирующего и неблокирующего хэширования (ahash)
  • cra_init: функция инициализации экземпляра преобразования. Эта функция вызывается единожды, во время создания экземпляра (сразу после выделения памяти под криптоконтекст). В этой функции следует выполнять различные подготовительные действия, если такие необходимы (например, выделение дополнительной памяти, проверка аппаратных возможностей). В ином случае поле можно оставить пустым
  • cra_exit: деинициализация экземпляра преобразования. В этой функции следует выполнять действия, обратные тем, что были выполнены в cra_init, если таковые имели место. В ином случае поле можно оставить пустым
  • cra_module: владелец этой реализации преобразования. Следует установить значение THIS_MODULE


Недокумментированные оставшиеся поля предназначены для внутреннего использования и не должны заполняться. Вроде бы ничего сложного. Для начала, мы хотим встроить реализацию алгоритма шифрования блока, поэтому взглянем ещё на структуру cipher_alg из союза cra_u:


/* include/linux/crypto.h */

struct cipher_alg {
    unsigned int cia_min_keysize;
    unsigned int cia_max_keysize;
    int (*cia_setkey)(struct crypto_tfm *tfm, const u8 *key,
                      unsigned int keylen);
    void (*cia_encrypt)(struct crypto_tfm *tfm, u8 *dst, const u8 *src);
    void (*cia_decrypt)(struct crypto_tfm *tfm, u8 *dst, const u8 *src);
};


Тут всё ещё проще и, как мне кажется, не нуждается в разъяснении. Теперь мы готовы к тому, чтобы посмотреть, как всё это работает на практике. Кстати, обратите внимание на то, что сигнатуры функций в структуре cipher_alg аналогичны сигнатурам функций из API нашего алгоритма из 2 части


Пишем модуль ядра. Определяем криптоконтекст и функцию установки ключа в соответствии с сигнатурой cipher_alg.cia_setkey:


#define XOR_CIPHER_KEY_SIZE   16

struct xor_cipher_ctx
{
    u8 key[XOR_CIPHER_KEY_SIZE];
};

static int xor_cipher_setkey(struct crypto_tfm *tfm, const u8 *key,
                                unsigned int len)
{
    struct xor_cipher_ctx *ctx = crypto_tfm_ctx(tfm);
    u32 *flags = &tfm->crt_flags;

    if (len != XOR_CIPHER_KEY_SIZE)
    {
        *flags |= CRYPTO_TFM_RES_BAD_KEY_LEN;
        return -EINVAL;
    }

    memmove(ctx->key, key, XOR_CIPHER_KEY_SIZE);
    return 0;
}


В API нашего алгоритма криптоконтекст передавался функциям напрямую, здесь же функции принимают хэндл алгоритма tfm, из которого, затем, контекст извлекается с помощью функции crypto_tfm_ctx. Так же, здесь мы проверяем длину переданного ключа. Если длина некорректна, то мы выставляем соответствующий флаг (CRYPTO_TFM_RES_BAD_KEY_LEN) и возвращаем код EINVAL (22: Invalid argument).


Теперь, определяем функцию шифрования блока в соответствии с cipher_alg.cia_encrypt:


static void xor_cipher_crypt(struct crypto_tfm *tfm, u8 *out, const u8 *in)
{
    struct xor_cipher_ctx *ctx = crypto_tfm_ctx(tfm);
    int i;

    for (i = 0; i < XOR_CIPHER_BLOCK_SIZE; i++)
    {
        out[i] = in[i] ^ ctx->key[i];
    }
}


Здесь — ничего нового. Так же, как и в оригинальном API, мы не будем определять дополнительную функцию для расшифровки, а указатель cipher_alg.cia_decrypt просто проинициализируем функцией xor_cipher_crypt.


Наконец, определяем экземпляр структуры crypto_alg, заполняем её и вызываем функции регистрации и разрегистрации алгоритма, соответсвенно, после загрузки и перед выгрузкой модуля:


static struct crypto_alg xor_cipher = {
    .cra_name = "xor-cipher",
    .cra_driver_name = "xor-cipher-generic",
    .cra_priority = 100,
    .cra_flags = CRYPTO_ALG_TYPE_CIPHER,
    .cra_blocksize = XOR_CIPHER_BLOCK_SIZE,
    .cra_ctxsize = sizeof(struct xor_cipher_ctx),
    .cra_module = THIS_MODULE,
    .cra_u = {
        .cipher = {
            .cia_min_keysize = XOR_CIPHER_KEY_SIZE,
            .cia_max_keysize = XOR_CIPHER_KEY_SIZE,
            .cia_setkey = xor_cipher_setkey,
            .cia_encrypt = xor_cipher_crypt,
            .cia_decrypt = xor_cipher_crypt
        }
    }
};

static int __init xor_cipher_init(void)
{
    return crypto_register_alg(&xor_cipher);
}

static void __exit xor_cipher_exit(void)
{
    crypto_unregister_alg(&xor_cipher);
}


Учитывая всё вышесказанное, здесь не должно возникать никаких вопросов. Если, скомпилировав и загрузив такой модуль, выполнить в терминале команду »cat /proc/crypto», то в выводимом списке зарегистрированных алгоритмов можно отыскать и свой:


name         : xor-cipher
driver       : xor-cipher-generic
module       : xor_cipher
priority     : 100
refcnt       : 1
selftest     : passed
internal     : no
type         : cipher
blocksize    : 16
min keysize  : 16
max keysize  : 16


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



[3.1] «Немного более глубокое» погружение в ядро


Забегу немного вперёд. Внимательный читатель помнит о существовании шаблонов. Так вот, реализовав в прошлой части шифрование блока, мы запросто можем обернуть эту реализацию в шаблон CBC и получть реализацию нашего алгоритма в режиме сцепления блоков шифра, но, протестировав её на моих последовательностях, вызов шифрования в режиме CBC вернёт нам код ошибки 22 (EINVAL). И тут самое время вспомнить, что я говорил по поводу обработки неполного блока входных данных и поля crypto_alg.cra_blocksize. Дело в том, что реализация режима CBC ядра понятия не имеет о том, как обрабатывать неполный блок. Более того, оборачивая наш алгоритм в режим CBC, ядро регистрирует новый алгоритм, размер блока которого равен размеру блока базового алгоритма. После вызова «апишной» функции зашифрования алгоритмом cbc (xor-cipher), размер входных данных проверяется на кратность размеру блока и, если они не кратны, функция возвращает EINVAL. Размер тестового вектора для шифрования в режиме CBC (40 байт) умышленно выбран не кратным размеру блока. По моему мнению, если стандартом шифрования предусматривается обработка неполного блока, а реализация этого не делает, то такая реализация едва ли пройдёт проверку на соответствие стандарту, даже если реализация даёт корректный результат, когда условие кратности выполняется (в данном случае, это так). Поэтому, сейчас мы сделаем полную реализацию режима сцепления блоков шифра для нашего алгоритма. Когда я занимался этим 2 года назад, я встраивал алгоритмы с расчётом на их использование через устаревший ныне API блокирующего симметричного шифрования, хотя уже тогда предпочтительным считался неблокирующий API, который, на сегодняшний день, так же устарел. Эту реализацию мы будем делать под актуальный Symmetric Key Cipher API.


Изучив документацию и реализации других алгоритмов в ядре своей ОС, я обнаружил, что объявление и регистрация таких алгоритмов существенно отличается от того, как это делалось раньше, и осуществляется при помощи части Crypto API, не отражённой в документации. Но, имея под рукой образец (arch/x86/crypto/aesni-intel_glue.c), разобраться, что к чему, оказалось не так уж и сложно. Итак, у нас здесь несколько иные функции для регистрации/разрегистрации алгоритмов:


/* include/crypto/internal/skcipher.h */

int crypto_register_skcipher(struct skcipher_alg *alg);
int crypto_register_skciphers(struct skcipher_alg *algs, int count);

void crypto_unregister_skcipher(struct skcipher_alg *alg);
void crypto_unregister_skciphers(struct skcipher_alg *algs, int count);


Заглянем в определение структуры skcipher_alg:


/* include/crypto/skcipher.h */

struct skcipher_alg {
    int (*setkey)(struct crypto_skcipher *tfm, const u8 *key,
                  unsigned int keylen);
    int (*encrypt)(struct skcipher_request *req);
    int (*decrypt)(struct skcipher_request *req);
    int (*init)(struct crypto_skcipher *tfm);
    void (*exit)(struct crypto_skcipher *tfm);

    unsigned int min_keysize;
    unsigned int max_keysize;
    unsigned int ivsize;
    unsigned int chunksize;
    unsigned int walksize;

    struct crypto_alg base;
};


В отличие от документации, в заголовочном файле include/crypto/skcipher.h эта структура задокументирована. Так, эта структура содержит экземпляр уже знакомой нам struct crypto_alg, описывающей алгоритм, здесь же, функции init и exit, по своему назначению аналогичны таковым из crypto_alg. Новыми, на данный момент, являются поля chunksize и walksize:


  • chunksize: по поводу значения этого поля в комментарии к коду сказано, что для всех алгоритмов, за исключением поточных (stream cipher), оно должно быть равно размеру блока, а, в случае поточных шифров, оно должно быть равно размеру блока базового (underlying) алгоритма. Это, в большей степени, касается блочных алгоритмов в режимах гаммирования. Для таких реализаций фактический размер блока (skcipher_alg.base.cra_blocksize) равен единице, а значение поля chunksize должно быть равно размеру блока алгоритма, используемого для выработки гаммы
  • walksize: равен значению chunksize, за исключением случаев, когда алгоритм может параллельно обрабатывать несколько блоков, тогда walksize может быть больше, чем chunksize, но обязательно должен быть кратен ему


Ещё одну неопознанную структуру мы видим в аргументах функций encrypt и decrypt. Структура skcipher_request содержит данные, необходимые для выполнения операции симметричного шифрования: указатели на входные/выходные данные и синхропосылку, хэндл криптопреобразования и так далее. Напрямую обращаться к полям этой структуры нет необходимости, поскольку для работы с ней существует отдельный под-API, но, всё же, в ней есть одна особенность, о которой обязательно нужно упомянуть.


На самом деле, эта особенность касается Crypto API в целом. Дело в том, что все под-API шифрования данных произвольной длины работают со входными/выходными данными не через привычные указатели на байтовые массивы (как было в API шифрования блока), а через структуры типа scatterlist. Вот, например, функция зашифрования из Synchronous Block Cipher API:


/* include/linux/crypto.h */

int crypto_blkcipher_encrypt(struct blkcipher_desc *desc,
        struct scatterlist *dst, struct scatterlist *src, unsigned int nbytes);


Структура skcipher_request так же содержит поля struct scatterlist *src и struct scatterlist *dst для входных и выходных данных соответственно. Посмотрим на основные элементы структуры scatterlist:


/* include/linux/scatterlist.h */

struct scatterlist {
    /* ... */
    unsigned long   page_link;
    unsigned int    offset;
    unsigned int    length;
    /* ... */
};


Экземпляр этой структуры можно проинициализировать указателем на некоторые данные. Например, при помощи вызова функции sg_init_one:


/* include/linux/scatterlist.h */

void sg_init_one(struct scatterlist *sg, const void *buf, unsigned int buflen);


Цитируя отсюда:


В этой функции определяется страница памяти, с которой «начинается» buf (page_link), и определяется смещение указателя buf относительно адреса начала страницы (offset).

Таким образом, криптографическая подсистема работает напрямую со страницами памяти. Но это ещё не всё, посмотрите на следующий пример:


struct crypto_blkcipher *tfm;
struct blkcipher_dest desc;
struct scatterlist sg[2];
u8 *first_segment, *second_segment;

/* crypto and data allocation... */

sg_init_table(sg, 2);
sg_set_buf(&sg[0], first_segment, len);
sg_set_buf(&sg[1], second_segment, len);

crypto_blkcipher_encrypt(&desc, &sg, &sg, 2*len);


Здесь, first_segment и second_segment указывают на данные, несмежные в памяти. Таким образом, Crypto API (при помощи специальных вспомогательных структур) в один вызов способно обработать «цепочку» scatterlist-ов, содержащую «рассеянные» (scattered) данные. Такой подход был изначально заложен в Crypto API и обусловлен нуждами подсистемы IPsec:


One of the initial goals of this design was to readily support IPsec, so that processing can be applied to paged skb’s without the need for linearization.

На самом деле, scatterlist и связанный с ним API ядра являются фундаментальной вещью при организиации ввода/вывода с прямым доступом к памяти (Direct Memory Access, DMA I/O). Они позволяют эффективно осуществлять операции такого ввода/вывода над непрерывными в виртуальной, но «рассеянными» по физической памяти буферами данных, но, впрочем, это уже совсем другая история.


Теперь можем переходить к реализации. Как и в прошлый раз, начинаем с определения функции установки ключа:


static int xor_skcipher_setkey(struct crypto_skcipher *tfm, const u8 *key,
                                    unsigned int len)
{
    return xor_cipher_setkey(crypto_skcipher_tfm(tfm), key, len);
}


Здесь всё просто. Для режима CBC мы будем работать с тем же типом криптоконтекста (struct xor_cipher_ctx), что и в случае шифрования блока, поэтому мы просто переиспользовали функцию установки ключа, реализованную ранее.


Пишем функции зашифрования и расшифрования.


Зашифрование
static int cbc_encrypt(struct skcipher_request *req)
{
    struct crypto_tfm *tfm = crypto_skcipher_tfm(crypto_skcipher_reqtfm(req));
    struct xor_cipher_ctx *ctx = crypto_tfm_ctx(tfm);
    struct skcipher_walk walk;
    u32 nbytes;
    int i, blocks;
    u8 *src, *dst, *iv;

    skcipher_walk_virt(&walk, req, true);
    iv = walk.iv;

    while ((nbytes = walk.nbytes) >= XOR_CIPHER_BLOCK_SIZE)
    {
        src = (u8*)walk.src.virt.addr;
        dst = (u8*)walk.dst.virt.addr;
        blocks = nbytes / XOR_CIPHER_BLOCK_SIZE;

        while (blocks)
        {
            for (i = 0; i < XOR_CIPHER_BLOCK_SIZE; i++)
            {
                dst[i] = src[i] ^ iv[i];
            }

            xor_cipher_crypt(tfm, dst, dst);
            iv = dst;

            src += XOR_CIPHER_BLOCK_SIZE;
            dst += XOR_CIPHER_BLOCK_SIZE;
            blocks--;
        }

        nbytes &= XOR_CIPHER_BLOCK_SIZE - 1;
        skcipher_walk_done(&walk, nbytes);
    }

    if ((nbytes = walk.nbytes))
    {
        src = (u8*)walk.src.virt.addr;
        dst = (u8*)walk.dst.virt.addr;

        for (i = 0; i < nbytes; i++)
        {
            dst[i] = src[i] ^ iv[i];
            dst[i] ^= ctx->key[i];
        }

        skcipher_walk_done(&walk, 0);
    }

    return 0;
}


Расшифрование
#define XOR_CIPHER_IV_SIZE 16

static int cbc_decrypt(struct skcipher_request *req)
{
    struct crypto_tfm *tfm = crypto_skcipher_tfm(crypto_skcipher_reqtfm(req));
    struct xor_cipher_ctx *ctx = crypto_tfm_ctx(tfm);
    struct skcipher_walk walk;
    u8 u[XOR_CIPHER_BLOCK_SIZE], iv[XOR_CIPHER_BLOCK_SIZE];
    u32 nbytes;
    int i, blocks;
    u8 *src, *dst;

    skcipher_walk_virt(&walk, req, true);
    memmove(iv, walk.iv, XOR_CIPHER_IV_SIZE);

    while ((nbytes = walk.nbytes) >= XOR_CIPHER_BLOCK_SIZE)
    {
        src = (u8*)walk.src.virt.addr;
        dst = (u8*)walk.dst.virt.addr;
        blocks = nbytes / XOR_CIPHER_BLOCK_SIZE;

        while (blocks)
        {
            memmove(u, src, XOR_CIPHER_BLOCK_SIZE);
            xor_cipher_crypt(tfm, dst, src);

            for (i = 0; i < XOR_CIPHER_BLOCK_SIZE; i++)
            {
                dst[i] ^= iv[i];
            }

            memmove(iv, u, XOR_CIPHER_IV_SIZE);

            dst += XOR_CIPHER_BLOCK_SIZE;
            src += XOR_CIPHER_BLOCK_SIZE;
            blocks--;
        }

        nbytes &= XOR_CIPHER_BLOCK_SIZE - 1;
        skcipher_walk_done(&walk, nbytes);
    }

    if ((nbytes = walk.nbytes))
    {
        src = (u8*)walk.src.virt.addr;
        dst = (u8*)walk.dst.virt.addr;

        for (i = 0; i < nbytes; i++)
        {
            dst[i] = src[i] ^ ctx->key[i];
            dst[i] ^= iv[i];
        }

        skcipher_walk_done(&walk, 0);
    }

    return 0;
}


Здесь, мы, так сказать, «прогуливаемся» по входным данным при помощи вспомогательной структуры skcipher_walk и связанных с ней функций. Сперва, экземпляр skcipher_walk инициализируется вызовом skcipher_walk_virt, а после этого функция skcipher_walk_done пересчитвает для нас указатели на очередную непрерывную «порцию» входных/выходных данных (walk.src.virt.addr и walk.dst.virt.addr) длины walk.nbytes, содержащуюся в цепочке scatterlist-ов.


Завершаем заполнением экземпляра структуры skcipher_alg, и регистрацией/разрегистрацией алгоритма:


static struct skcipher_alg cbc_xor_cipher = {
    .base = {
        .cra_name = "cbc(xor-cipher)",
        .cra_driver_name = "cbc-xor-cipher",
        .cra_priority = 400,
        .cra_flags = CRYPTO_ALG_ASYNC,
        .cra_blocksize = 1,
        .cra_ctxsize = sizeof(struct xor_cipher_ctx),
        .cra_module = THIS_MODULE,
    },
    .min_keysize = XOR_CIPHER_KEY_SIZE,
    .max_keysize = XOR_CIPHER_KEY_SIZE,
    .ivsize = XOR_CIPHER_IV_SIZE,
    .setkey = xor_skcipher_setkey,
    .encrypt = cbc_encrypt,
    .decrypt = cbc_decrypt,
    .chunksize = XOR_CIPHER_BLOCK_SIZE,
};

static int __init xor_cipher_init(void)
{
    crypto_register_alg(&xor_cipher);
    return crypto_register_skcipher(&cbc_xor_cipher);
}

static void __exit xor_cipher_exit(void)
{
    crypto_unregister_alg(&xor_cipher);
    crypto_unregister_skcipher(&cbc_xor_cipher);
}


Обратите внимание на значение поля cra_blocksize. Это небольшая хитрость, необходимая для того, чтобы вызов skcipher_walk_done «отдавал» нам последний неполный блок, если такой есть.


Скомпилировав и загрузив модуль, нах

© Habrahabr.ru