Введение



Протокол Kerberos 5 сейчас активно используется для аутентификации. Особенностью данного протокола является то, что он осуществляет аутентификацию, базируясь на четырех китах:


  1. Симметричное шифрование;
  2. Хеширование;
  3. ЭЦП;
  4. Третья доверенная сторона.

Начиная с пятой версии появилась возможность использовать еще асимметричное шифрование (для электронной подписи). Более подробно на работе протокола Kerberos останавливаться не имеет смысла, ибо описание алгоритма можно посмотреть тут.


К сожалению, количество алгоритмов шифрования, хеширования и ЭЦП, которые использует данный протокол, не настолько велико, насколько хотелось бы, поэтому в данной статье я хочу показать, как добавить легко и просто собственные алгоритмы в реализацию данного протокола MIT'ом. Добавлять же мы будем наши отечественные алгоритмы: ГОСТ 28147-89 (aka Магма), ГОСТ Р 34.11-2012 (aka Стрибог) и ГОСТ Р 34.10-2012 (хотелось бы тоже иметь для него aka, но я не знаю:(). Готовое решение для данных алгоритмов можно его найти в моем репозитории. На стороне клиента мы будем использовать аппаратные реализации алгоритмов ГОСТ в Рутокене ЭЦП 2.0 и их программные реализации в engine GOST для openssl. Но самый безопасный вариант хранения ключей – когда они генерируются непосредственно на Рутокене и никогда не покидают его память во время криптографических операций. Для такого варианта работы потребуется rtengine.


Перед тем, как приступить к внедрению алгоритмов, опишем основные места, где будут производится изменения. Внутри директории src/lib/crypto/ находятся реализации всех алгоритмов, отвечающих за симметричную криптографию и хеширование. В ней имеются 2 реализации этих криптографических алгоритмов: встроенная(builtin) и openssl. Дабы сэкономить время и место, мы, естественно, будем добавлять реализации алгоритмов с помощью openssl, в котором они уже есть (ну или почти есть, но об этом чуть позже). Для добавления же асимметричных алгоритмов, нужно будет подправить плагин src/plugins/preauth/pkinit


Если у вас еще не настроен Kerberos, то инструкцию по его первичной настройки и эксплуатации можно взять тут. В дальнейшем автор полагает, что вы работаете с доменом AKTIV-TEST.RU


Добавление алгоритмов хеширования и симметричного шифрования


Как уже было ранее озвучено, мы не будем писать алгоритмы шифрования и хеширования с нуля, а воспользуемся готовой реализацией данных алгоритмов в openssl. Напрямую openssl не поддерживает в себе реализацию отечественных алгоритмов, но обладает мобильностью в данном вопросе и позволяет добавлять новые алгоритмы, используя механизм engine GOST для работы с ключами в файловой системе и, хранящие на токене — rtengine.


Небольшое введение в механизм engine openssl и подключение engine GOST


Engine в openssl – это небольшая динамическая библиотека, которую openssl подгружает в runtime по требованию. Каждая такая библиотека, должна содержать в себе определенные символы (функции) для загрузки необходимых алгоритмов. В данной работе мы воспользуемся engine gost, который содержит в себе все необходимые отечественные криптографические алгоритмы.


Установка дичайше проста, например, и происходит следующим образом:


  1. Выкачиваете из репозитория реализацию данного энджина.


  2. собираете библиотеку с ним (mkdir build && cd build && cmake… && make):


    mkdir build
    cd build
    cmake ..
    make

  3. В директории bin (КОТОРАЯ ПОЯВИТСЯ В КОРНЕВОМ КАТАЛОГЕ ПРОЕКТА!!!) будет лежать динамическая библиотека gost.so – это и есть наш энджин. Его нужно будет перенести в директорию, где хранятся энджины openssl. Узнать месторасположение данной директории можно с помощью:


    openssl version -a | grep ENGINESDIR

  4. Дело за последним – нужно сказать openssl, где находится данный энджин и как он называется. Сделать можно это с помощью изменения файла конфигурации openssl.cnf. Месторасположение которого можно узнать с помощью:


    openssl version -a | grep OPENSSLDIR

    В конец данного файла нужно будет внести следующее содержимое:


    # в начало файла написать
    openssl_conf = openssl_def
    
    ...
    
    # в конец файла
    # OpenSSL default section
    [openssl_def]
    engines = engine_section
    
    # Engine section
    [engine_section]
    gost = gost_section
    
    # Engine gost section
    [gost_section]
    engine_id = gost
    dynamic_path = /path/to/engines/dir/with/gost.so
    default_algorithms = ALL
    init = 0


После этого данный энджин должен появится в openssl. Проверить его работоспособность можно заставив, например, сгенерировать приватный ключ в файле по ГОСТ Р 34.10-2012:


openssl genpkey -engine gost -algorithm gost2012_512 -pkeyopt paramset:A -out client_key.pem

Флаг -engine как раз говорит о том, какой engine нужно подгрузить перед началом работы для того, чтобы стал виден алгоритм генерации ключей для ГОСТ Р 34.10-2012.


Реализация алгоритма хеширования


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


  1. Т.к. нам понадобится уверенность в подключении в разных местах нашей программы, давайте создадим сначала небольшую библиотеку gost_helper, которая будет содержать в себе функцию инициализации энджина в openssl, а так же для удобства несколько функций, возвращающих контексты некоторых алгоритмов хеширования – нам это поможет в дальнейшем. Назовем эту библиотеку gost_helper и создадим для нее соответствующий заголовочник и исходный файл в директории src/lib/crypto/openssl/:


    // gost_helper.h
    #include <openssl/evp.h>
    
    // функция инициализация энджина GOST
    void krb5int_init_gost();
    
    // вспомогательные функции, возвращающие контексты алгоритмов хеширования
    const EVP_MD * EVP_gostR3411_2012_256();
    const EVP_MD * EVP_gostR3411_2012_512();
    
    // gost_helper.c
    #include "gost_helper.h"
    #include <openssl/engine.h>
    
    // указатель на подключенный энджин
    static ENGINE *eng = NULL;
    
    void
    krb5int_init_gost()
    {
       // для ускорения работы проверяем, не подключен ли энджин уже
       if (eng) return;
       OPENSSL_add_all_algorithms_conf();
       ERR_load_crypto_strings();
    
       if (!(eng = ENGINE_by_id("gost"))) {
           printf("Engine gost doesn’t exist");
           return;
       }
    
       ENGINE_init(eng);
       ENGINE_set_default(eng, ENGINE_METHOD_ALL);
    }
    
    const EVP_MD *
    EVP_gostR3411_2012_256()
    {
       krb5int_init_gost();
       return EVP_get_digestbynid(NID_id_GostR3411_2012_256);
    }
    
    const EVP_MD *
    EVP_gostR3411_2012_512()
    {
       krb5int_init_gost();
       return EVP_get_digestbynid(NID_id_GostR3411_2012_512);
    }

  2. После добавления вспомогательной библиотеки необходимо будет объявить о ее существовании в Makefile и выписать ее зависимости от файлов. Для этого добавим следующее:


    # добавим в шаблон src/lib/crypto/openssl/Makefile.in описание зависимостей объектных файлов от исходников:
    
    STLIBOBJS=hmac.o  ...
    stubs.o gost_helper.o
    
    OBJS=$(OUTPRE)hmac.$(OBJEXT) ...
    $(OUTPRE)stubs.$(OBJEXT) $(OUTPRE)gost_helper$(OBJEXT)
    
    SRCS=$(srcdir)/hmac.c    ...
    $(srcdir)/stubs.c   $(srcdir)/gost_helper.c
    
    # В файл зависимостей src/lib/crypto/openssl/deps добавим, от чего будут зависеть наши объектные файлы и библиотеки:
    
    gost_helper.so gost_helper.po $(OUTPRE)gost_helper.$(OBJEXT): 
     $(BUILDTOP)/include/autoconf.h  $(BUILDTOP)/include/krb5/krb5.h $(BUILDTOP)/include/osconf.h  $(BUILDTOP)/include/profile.h $(COM_ERR_DEPS) $(top_srcdir)/include/k5-buf.h  $(top_srcdir)/include/k5-err.h $(top_srcdir)/include/k5-gmt_mktime.h  $(top_srcdir)/include/k5-int-pkinit.h $(top_srcdir)/include/k5-int.h  $(top_srcdir)/include/k5-platform.h $(top_srcdir)/include/k5-plugin.h  $(top_srcdir)/include/k5-thread.h $(top_srcdir)/include/k5-trace.h  $(top_srcdir)/include/krb5.h $(top_srcdir)/include/krb5/authdata_plugin.h  $(top_srcdir)/include/krb5/plugin.h $(top_srcdir)/include/port-sockets.h  gost_helper.c gost_helper.h

  3. Стоит заметить, что в дальнейшем при подключении данной библиотеки, нам надо будет добавлять зависимости некоторых файлов от заголовочника данной библиотеки. Делается это очень просто – отыскивается цель, куда надо добавить зависимость в deps файле и записывается зависимость от rel/path/to/gost_helper.h. Например, в src/lib/crypto/openssl/hash_provider/deps нужно будет добавить следующее:


    hash_evp.so hash_evp.po $(OUTPRE)hash_evp.$(OBJEXT):  $(BUILDTOP)/include/autoconf.h $(BUILDTOP)/include/krb5/krb5.h ...
     $(srcdir)/../gost_helper.h hash_evp.c
    

    В целях экономии места в статье и чтобы размять ваши мозги, я больше не буду обращать на это внимание: будьте внимательны и осторожны!


  4. Добавим теперь реализации функций хеширования, все их реализации у нас лежат в src/lib/crypto/openssl/hash_provider/hash_evp.c. Туда нужно будет дописать следующее:


    // Не забываем про изменение deps файла!!! Так уж и быть на первый раз предупрежу невнимательных
    #include "crypto_int.h"
    #include "gost_helper.h"
    #include <openssl/evp.h>
    
    ...
    
    static krb5_error_code
    hash_sha384(const krb5_crypto_iov *data, size_t num_data, krb5_data *output)
    {
       return hash_evp(EVP_sha384(), data, num_data, output);
    }
    
    static krb5_error_code
    hash_stribog256(const krb5_crypto_iov *data, size_t num_data, krb5_data *output)
    {
       return hash_evp(EVP_gostR3411_2012_256(), data, num_data, output);
    }
    
    static krb5_error_code
    hash_stribog512(const krb5_crypto_iov *data, size_t num_data, krb5_data *output)
    {
       return hash_evp(EVP_gostR3411_2012_512(), data, num_data, output);
    }
    
    // структура функции хеширования
    /*
    первый атрибут -- название алгоритма
    второй -- размер хеша в байтах
    третий -- размер блока передаваемых данных в байтах
    четвертый -- указатель на функцию хеширования
    */
    const struct krb5_hash_provider krb5int_hash_sha384 = {
       "SHA-384", 48, 128, hash_sha384
    };
    
    const struct krb5_hash_provider krb5int_hash_stribog256 = {
       "GOSTR34.11-2012-256", 32, 64, hash_stribog256
    };
    
    const struct krb5_hash_provider krb5int_hash_stribog512 = {
       "GOSTR34.11-2012-512", 64, 64, hash_stribog512
    };

  5. Теперь надо объявить эти контексты во всей библиотеке. Для этого нужно создать их описание в файле src/lib/crypto/krb/crypto_int.h.


    // Параллельно заметим, что компилятор начнет ругаться, что имя функций не помещается в указанный массив, поэтому немного поправим структуру krb5_hash_provider и добавим кол-во символов в массиве имени алгоритма:
    
    ...
    
    struct krb5_hash_provider {
       char hash_name[32]; // было 8
       size_t hashsize, blocksize;
    
       krb5_error_code (*hash)(const krb5_crypto_iov *data, size_t num_data,
                               krb5_data *output);
    };
    
    ...
    
    extern const struct krb5_hash_provider krb5int_hash_sha384;
    extern const struct krb5_hash_provider krb5int_hash_stribog256;
    extern const struct krb5_hash_provider krb5int_hash_stribog512;
    
    ...

  6. Объявим идентификатор связки Стрибог и AES, внедря макросы, которые мы назовем CKSUMTYPE_STRIBOG_256_AES256, CKSUMTYPE_STRIBOG_512_AES256, ENCTYPE_AES256_CTS_STRIBOG_256, ENCTYPE_AES256_CTS_STRIBOG_512. Их надо объявить в шаблоне заголовочного файла src/include/krb5/krb5.hin. Выглядеть это будет примерно так:


    #define ENCTYPE_ARCFOUR_HMAC_EXP            0x0018 /**< RFC 4757 */
    #define ENCTYPE_CAMELLIA128_CTS_CMAC        0x0019 /**< RFC 6803 */
    #define ENCTYPE_CAMELLIA256_CTS_CMAC        0x001a /**< RFC 6803 */
    #define ENCTYPE_AES256_CTS_STRIBOG_256      0x001b /**< NO RFC */
    #define ENCTYPE_AES256_CTS_STRIBOG_512      0x001c /**< NO RFC */
    #define ENCTYPE_UNKNOWN                     0x01ff
    
    ...
    
    #define CKSUMTYPE_CMAC_CAMELLIA256 0x0012 /**< RFC 6803 */
    #define CKSUMTYPE_MD5_HMAC_ARCFOUR -137 /* Microsoft netlogon */
    #define CKSUMTYPE_HMAC_MD5_ARCFOUR -138 /**< RFC 4757 */
    #define CKSUMTYPE_STRIBOG_256_AES256 -139 /**< NO RFC */
    #define CKSUMTYPE_STRIBOG_512_AES256 -140 /**< NO RFC */

  7. Теперь необходимо добавить две связки функции хеширования и шифрования и функции шифрования и хеширования. Зачем две, если они эквиваленты, спросите вы? Ответ: не знаю, или это исторический костыль или способ оптимизации. Тем не менее, давайте добавим в необходимые файлы новые структуры:


    // в файле src/lib/crypto/krb/cksumtypes.c добавить в конец списка структуру алгоритмов хеширования и шифрования
    /*
    первый атрибут -- идентификатор
    второй -- название связки
    в третьем -- можно добавить алиасы связок
    в четвертом -- описание
    в пятом -- указатель на функцию шифрования
    в шестом -- указатель на функцию хеширования
    в седьмом -- указатель на функцию "работы с хешом"
    в восьмом -- размер блока
    в девятом -- размер хеша
    в десятом -- различные флаги
    */
    const struct krb5_cksumtypes krb5int_cksumtypes_list[] = {
    ...
    
       { CKSUMTYPE_STRIBOG_256_AES256,
         "stribog-256-aes256", { 0 }, "STRIBOG256 AES256 key",
         &krb5int_enc_aes256, &krb5int_hash_stribog256,
         krb5int_etm_checksum, NULL,
         64, 32, 0 },
    
       { CKSUMTYPE_STRIBOG_512_AES256,
         "stribog-512-aes256", { 0 }, "STRIBOG512 AES256 key",
         &krb5int_enc_aes256, &krb5int_hash_stribog512,
         krb5int_etm_checksum, NULL,
         64, 64, 0 },
    
    };
    
    // в файле src/lib/crypto/krb/etypes.c добавить в конец списка структуру алгоритмов шифрования и хеширования:
    
    /*
    первый атрибут -- идентификатор
    второй -- название
    третий -- алиас
    четвертый -- описание
    пятый -- указатель на функцию шифрования
    шестой -- указатель на функцию хеширования
    седьмой -- размер возвращаемых данных функцией получения случайных чисел из данных
    восьмой -- указатель на функцию, возвращающую характеристики алгоритмов
    девятый -- указатель на функцию шифрования, использующую алгоритмы хеширования и шифрования
       десятый -- указатель на функцию расшифрования, использующую алгоритмы хеширования и шифрования
       одиннадцатый -- указатель на функцию преобразования строки в ключ
       двенадцатый -- указатель на функцию выработки случайного ключа
       тринадцатый -- различные флаги
       четырнадцатый -- размер ключа
    */
    const struct krb5_keytypes krb5int_enctypes_list[] = { 
    ...
    
       { ENCTYPE_AES256_CTS_STRIBOG_256,
         "aes256-cts-stribog-256", { "aes256-stribog256" },
         "AES-256 CTS mode with 256-bit stribog",
         &krb5int_enc_aes256, &krb5int_hash_stribog256,
         16,
         krb5int_aes2_crypto_length, krb5int_etm_encrypt, krb5int_etm_decrypt,
         krb5int_aes2_string_to_key, k5_rand2key_direct,
         krb5int_aes2_prf,
         CKSUMTYPE_STRIBOG_256_AES256,
         0 /*flags*/, 256 },
    
       { ENCTYPE_AES256_CTS_STRIBOG_512,
         "aes256-cts-stribog-512", { "aes256-stribog512" },
         "AES-256 CTS mode with 512-bit stribog",
         &krb5int_enc_aes256, &krb5int_hash_stribog512,
         16,
         krb5int_aes2_crypto_length, krb5int_etm_encrypt, krb5int_etm_decrypt,
         krb5int_aes2_string_to_key, k5_rand2key_direct,
         krb5int_aes2_prf,
         CKSUMTYPE_STRIBOG_512_AES256,
         0 /*flags*/, 256 },
    
    };

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


    // В файле src/lib/crypto/openssl/hmac.c будет задействоваться функция map_digest -- которая возвращает контекст хеш-функции по переданной структуре. Сейчас эта функция ничего не знает о наших ГОСТах. Исправим это:
    
    #include "crypto_int.h"
    #include "gost_helper.h"
    #include <openssl/hmac.h>
    #include <openssl/evp.h>
    
    static const EVP_MD *
    map_digest(const struct krb5_hash_provider *hash)
    {
       if (!strncmp(hash->hash_name, "SHA1",4))
           return EVP_sha1();
    
    ...
    
       else if (!strncmp(hash->hash_name, "GOSTR34.11-2012-256", 19))
           return EVP_gostR3411_2012_256();
       else if (!strncmp(hash->hash_name, "GOSTR34.11-2012-512", 19))
           return EVP_gostR3411_2012_512();
       else
           return NULL;
    }
    
    // В файле src/lib/crypto/openssl/pbkdf2.c вызывается krb5int_pbkdf2_hmac, которая сейчас ничего не знает о новых хешах:
    
    krb5_error_code
    krb5int_pbkdf2_hmac(const struct krb5_hash_provider *hash,
                       const krb5_data *out, unsigned long count,
                       const krb5_data *pass, const krb5_data *salt)
    {
       const EVP_MD *md = NULL;
       /* Get the message digest handle corresponding to the hash. */
       if (hash == &krb5int_hash_sha1)
           md = EVP_sha1();
    
    ...
    
       else if (hash == &krb5int_hash_stribog256)
           md = EVP_gostR3411_2012_256();
       else if (hash == &krb5int_hash_stribog512)
           md = EVP_gostR3411_2012_512();
    
    ...
    
       return 0;
    }
    
    // в файле src/lib/krb5/krb/init_ctx.c нужно добавить в список связок алгоритмов, идентификатор новых алгоритмов:
    
    static krb5_enctype default_enctype_list[] = {
    
    ...
    
       ENCTYPE_AES256_CTS_STRIBOG_256, ENCTYPE_AES256_CTS_STRIBOG_512,
       0
    };


После всех этих изменений можно проверить работу алгоритма. Соберем все, что мы наделали.


autoconf
./configure --with-crypto-impl=openssl # использовать реализацию алгоритмов из openssl
make
sudo make install

Теперь приступим к проверке. Для этого давайте выставим принудительное использование внедренных нами алгоритмов в файлы конфигурации Kerberos. Выполните следующее:


  1. Остановите krb5kdc:


    service krb5-kdc stop

  2. Подправим файл конфигурации kdc.conf (у меня это /usr/local/var/krb5kdc/kdc.conf). Выставим принудительное хеширование с помощью нововведенного алгоритма:


    [realms]
    AKTIV-TEST.RU = {
    master_key_type = aes256-stribog512
    supported_enctypes = aes256-stribog512:normal
    default_tgs_enctypes = aes256-stribog512
    default_tkt_enctypes = aes256-stribog512
    permitted_enctypes = aes256-stribog512
    }
    # Так же можно использовать хеш длиной 256 бит

  3. Аналогичные изменения в файле конфигурации всего протокола krb5.conf (у меня он находится по адресу /etc/krb5.conf):


    [libdefaults]
    supported_enctypes = aes256-stribog512:normal
    default_tgs_enctypes = aes256-stribog512
    default_tkt_enctypes = aes256-stribog512
    permitted_enctypes = aes256-stribog512
    # Также можно использовать хеш длиной 256 бит

  4. Далее запустим krb5kdc т.к. изменился master_key, то возможно придется создать базу данных principals заново с помощью krb5_newrealm.


  5. После этого создаем всех необходимых принципалов и можно начать работу. Попробуйте аутентифицироваться с помощью kinit.


  6. Проверим, что хеширование происходит по указанному алгоритму можно с
    помощью klist -e.



Если сервис не запускается, то его можно запустить из-под рута с помощью src/kdc/krb5kdc. Если все запустилось, прошло гладко – поздравляю вас! Иначе – увы, панацею от всех проблем я не предлагаю, а лишь предлагаю "небольшую" инструкцию, содержащую основные шаги, которые вам придется совершить, дабы внедрить новый алгоритм в Kerberos. И если у вас ничего не вышло – берите в руки gdb и смотрите, где что выходит не так. Вам лишь могу дать несколько советов:


  1. собирать проект в режиме отладки можно, если передать в ./configure CFLAGS=" -g -O0";
  2. запустить krb5kdc не в фоновом режиме можно с помощью флага -n;
  3. надеюсь до этого не дойдет (хотя при реализации асимметричных алгоритмов у меня все-таки дошло) – возможно вам потребуются отладочные символы openssl – установите их или взяв из репозитория, или установив openssl из исходников с отладочными символами;
  4. то же самое касается gost engine.

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


Реализация алгоритма шифрования


В этом разделе я покажу, как можно добавить свой алгоритм шифрования данных в Kerberos, и мы попробуем создать связку Магмы и Стрибога, добавленного в прошлом разделе. Тут я уже предполагаю, что небольшая библиотека gost_helper уже была добавлена в прошлом разделе. Ну что же, разминаем пальцы и приступаем:


  1. Сначала опишем алгоритм в нашей библиотеке libk5crypto описав их в заголовочном файле src/lib/crypto/krb/crypto_int.h.


    ...
    
    extern const struct krb5_enc_provider krb5int_enc_camellia256;
    extern const struct krb5_enc_provider krb5int_enc_gost89;
    
    ...

  2. В директории src/lib/crypto/openssl/enc_provider добавим исходник gost.c, содержащий в себе реализацию всех необходимых алгоритмов (за основу я взял исходник алгоритма des). Важно заметить, что мы реализуем только режим шифрования cbc, так что для самопроверки вы можете взять любой другой режим шифрования и добавить его:


    #include "crypto_int.h"
    #include "gost_helper.h"
    #include <openssl/evp.h>
    
    #define BLOCK_SIZE 8
    
    static krb5_error_code
    krb5int_gost_encrypt(krb5_key key, const krb5_data *ivec, krb5_crypto_iov *data,
                   size_t num_data)
    {
       int ret, olen = BLOCK_SIZE;
       unsigned char iblock[BLOCK_SIZE], oblock[BLOCK_SIZE];
       struct iov_cursor cursor;
       EVP_CIPHER_CTX *ctx;
    
       // я бы перенес вызов этой функции в функцию инициализации состояния, но функция krb5int_gost_encrypt, иногда вызывается без нее:
       krb5int_init_gost();
       ctx = EVP_CIPHER_CTX_new();
       if (ctx == NULL)
           return ENOMEM;
    
       ret = EVP_EncryptInit_ex(ctx, EVP_get_cipherbynid(NID_gost89_cbc), NULL,
                                key->keyblock.contents,
                                (ivec) ? (unsigned char*)ivec->data : NULL);
       if (!ret) {
           EVP_CIPHER_CTX_free(ctx);
           return KRB5_CRYPTO_INTERNAL;
       }
    
       EVP_CIPHER_CTX_set_padding(ctx,0);
    
       k5_iov_cursor_init(&cursor, data, num_data, BLOCK_SIZE, FALSE);
       while (k5_iov_cursor_get(&cursor, iblock)) {
           ret = EVP_EncryptUpdate(ctx, oblock, &olen, iblock, BLOCK_SIZE);
           if (!ret)
               break;
           k5_iov_cursor_put(&cursor, oblock);
       }
    
       if (ivec != NULL)
           memcpy(ivec->data, oblock, BLOCK_SIZE);
    
       EVP_CIPHER_CTX_free(ctx);
    
       zap(iblock, sizeof(iblock));
       zap(oblock, sizeof(oblock));
    
       if (ret != 1)
           return KRB5_CRYPTO_INTERNAL;
       return 0;
    }
    
    static krb5_error_code
    krb5int_gost_decrypt(krb5_key key, const krb5_data *ivec, krb5_crypto_iov *data,
                   size_t num_data)
    {
       int ret, olen = BLOCK_SIZE;
       unsigned char iblock[BLOCK_SIZE], oblock[BLOCK_SIZE];
       struct iov_cursor cursor;
       EVP_CIPHER_CTX *ctx;
    
       krb5int_init_gost();
       ctx = EVP_CIPHER_CTX_new();
       if (ctx == NULL)
           return ENOMEM;
    
       ret = EVP_DecryptInit_ex(ctx, EVP_get_cipherbynid(NID_gost89_cbc), NULL,
                                key->keyblock.contents,
                                (ivec) ? (unsigned char*)ivec->data : NULL);
       if (!ret) {
           EVP_CIPHER_CTX_free(ctx);
           return KRB5_CRYPTO_INTERNAL;
       }
    
       EVP_CIPHER_CTX_set_padding(ctx,0);
    
       k5_iov_cursor_init(&cursor, data, num_data, BLOCK_SIZE, FALSE);
       while (k5_iov_cursor_get(&cursor, iblock)) {
           ret = EVP_DecryptUpdate(ctx, oblock, &olen,
                                   (unsigned char *)iblock, BLOCK_SIZE);
           if (!ret)
               break;
           k5_iov_cursor_put(&cursor, oblock);
       }
    
       if (ivec != NULL)
           memcpy(ivec->data, iblock, BLOCK_SIZE);
    
       EVP_CIPHER_CTX_free(ctx);
    
       zap(iblock, sizeof(iblock));
       zap(oblock, sizeof(oblock));
    
       if (ret != 1)
           return KRB5_CRYPTO_INTERNAL;
       return 0;
    }
    
    static krb5_error_code
    krb5int_gost_init_state (const krb5_keyblock *key, krb5_keyusage usage,
                           krb5_data *state)
    {
       state->length = 8;
       state->data = (void *) malloc(8);
       if (state->data == NULL)
           return ENOMEM;
       memset(state->data, 0, state->length);
       return 0;
    }
    
    static void
    krb5int_gost_free_state(krb5_data *state)
    {
       free(state->data);
       *state = empty_data();
    }
    
    /*
    первый атрибут -- как неожиданно, размер блока
    второй -- кол-во байтов для хранения первичного ключа
    третий -- размер байтов для хранения развернутого ключа
    четвертый -- указатель на функцию шифрования
    пятый -- указатель на функцию расшифровки
    шестой -- указатель на функцию cbc-mac checksum, не вдавался в подробности что это, т.к. наш алгоритм этого не имеет
    седьмой -- указатель на функцию инициализации шифрования
    восьмой -- указатель на функцию освобождения памяти под первичное состояние 
    */
    const struct krb5_enc_provider krb5int_enc_gost89 = {
       BLOCK_SIZE,
       32, 32,
       krb5int_gost_encrypt,
       krb5int_gost_decrypt,
       NULL,
       krb5int_gost_init_state,
       krb5int_gost_free_state
    };
    

  3. В шаблоне src/lib/crypto/openssl/enc_provider/Makefile.in укажем, что появился новый исходник:


    STLIBOBJS= des.o   ...
    gost.o
    
    OBJS= $(OUTPRE)des.$(OBJEXT)  ...
    $(OUTPRE)gost$(OBJEXT)
    
    SRCS= $(srcdir)/des.c     ...
    $(srcdir)/gost.c

  4. Не забываем про указание зависимостей в src/lib/crypto/openssl/enc_provider/deps:


    gost.so gost.po $(OUTPRE)gost.$(OBJEXT): $(BUILDTOP)/include/autoconf.h  $(BUILDTOP)/include/krb5/krb5.h $(BUILDTOP)/include/osconf.h  $(BUILDTOP)/include/profile.h $(COM_ERR_DEPS) $(srcdir)/../../krb/crypto_int.h  $(srcdir)/../crypto_mod.h $(top_srcdir)/include/k5-buf.h  $(top_srcdir)/include/k5-err.h $(top_srcdir)/include/k5-gmt_mktime.h  $(top_srcdir)/include/k5-int-pkinit.h $(top_srcdir)/include/k5-int.h  $(top_srcdir)/include/k5-platform.h $(top_srcdir)/include/k5-plugin.h  $(top_srcdir)/include/k5-thread.h $(top_srcdir)/include/k5-trace.h  $(top_srcdir)/include/krb5.h $(top_srcdir)/include/krb5/authdata_plugin.h  $(top_srcdir)/include/krb5/plugin.h $(top_srcdir)/include/port-sockets.h  $(top_srcdir)/include/socket-utils.h $(srcdir)/../gost_helper.h gost.c

  5. Добавим идентификаторы связок к src/include/krb5/krb5.hin:


    ...
    
    #define ENCTYPE_AES256_CTS_STRIBOG_256      0x001b /**< NO RFC */
    #define ENCTYPE_AES256_CTS_STRIBOG_512      0x001c /**< NO RFC */
    #define ENCTYPE_GOST89_CBC_STRIBOG_256      0x001d /**< SOME GOST */
    #define ENCTYPE_GOST89_CBC_STRIBOG_512      0x001e /**< SOME GOST */
    #define ENCTYPE_UNKNOWN                     0x01ff
    
    ...
    
    #define CKSUMTYPE_STRIBOG_256_AES256 -139 /**< NO RFC */
    #define CKSUMTYPE_STRIBOG_512_AES256 -140 /**< NO RFC */
    #define CKSUMTYPE_STRIBOG_256_GOST89 -141 /**< SOME GOST */
    #define CKSUMTYPE_STRIBOG_512_GOST89 -142 /**< SOME GOST */

  6. Опишем структуру связок функции хеширования и шифрования, как в прошлом разделе:


    // src/lib/crypto/krb/cksumtypes.c 
    
    const struct krb5_cksumtypes krb5int_cksumtypes_list[] = {
    
    ...
    
       { CKSUMTYPE_STRIBOG_256_GOST89,
         "stribog-256-gost89", { 0 }, "STRIBOG256 GOST89 key",
         &krb5int_enc_gost89, &krb5int_hash_stribog256,
         krb5int_dk_checksum, NULL,
         64, 32, 0 },
    
       { CKSUMTYPE_STRIBOG_512_GOST89,
         "stribog-512-gost89", { 0 }, "STRIBOG512 GOST89 key",
         &krb5int_enc_gost89, &krb5int_hash_stribog512,
         krb5int_dk_checksum, NULL,
         64, 64, 0 },
    
    };
    
    // src/lib/crypto/krb/etypes.c
    
    // тут я использовал только функции по умолчанию, но вы можете попробовать поэкспериментировать с различными другими функциями, например, с такими же как в aes
    const struct krb5_keytypes krb5int_enctypes_list[] = {
    
    ...
    
        { ENCTYPE_GOST89_CBC_STRIBOG_256,
          "gost89-cbc-stribog-256", { "gost89-stribog256" },
          "GOST 28147-89 CBC mode with 256-bit stribog",
          &krb5int_enc_gost89, &krb5int_hash_stribog256,
          16,
          krb5int_dk_crypto_length, krb5int_dk_encrypt, krb5int_dk_decrypt,
          krb5int_dk_string_to_key, k5_rand2key_direct,
          krb5int_dk_prf,
          CKSUMTYPE_STRIBOG_256_GOST89,
          0 /*flags*/, 256 },
    
       { ENCTYPE_GOST89_CBC_STRIBOG_512,
         "gost89-cbc-stribog-512", { "gost89-stribog512" },
         "GOST 28147-89 CBC mode with 512-bit stribog",
         &krb5int_enc_gost89, &krb5int_hash_stribog512,
         16,
         krb5int_dk_crypto_length, krb5int_dk_encrypt, krb5int_dk_decrypt,
         krb5int_dk_string_to_key, k5_rand2key_direct,
         krb5int_dk_prf,
         CKSUMTYPE_STRIBOG_512_GOST89,
         0 /*flags*/, 256 },
    
    };

  7. Добавим в список режимов шифрования по умолчанию в файле src/lib/krb5/krb/init_ctx.c:


    static krb5_enctype default_enctype_list[] = {
    
    ...
    
       ENCTYPE_AES256_CTS_STRIBOG_256, ENCTYPE_AES256_CTS_STRIBOG_512,
       ENCTYPE_GOST89_CBC_STRIBOG_256, ENCTYPE_GOST89_CBC_STRIBOG_512,
       0
    };


После этих манипуляций можно попробовать протестировать нововведенный алгоритм. Тестирование происходит аналогичным образом, что и в прошлом разделе. Имя связки можно взять в виде алиаса (например, gost89-stribog512) или с помощью имени самого алгоритма (например, gost89-cbc-stribog-512). Надеюсь, что все заработает, иначе не забывайте о том, что я сказал ранее.


Добавление алгоритма цифровой подписи


Ура! Мы приступаем к финальному разделу данной статьи и попробуем добавить собственный алгоритм электронной подписи. Не стоит пугаться, его добавить проще чем что-либо еще, так что давайте поскорее приступим… Хотя нет, подождите, для начала небольшая ремарка.


Асимметричное шифрование, достаточно, тяжеловесная штука. Одна из его главных особенностей – возможность аутентификации: все это построено на удостоверяющих центрах, сертификатах и всякой-всякой сложной лабуде. Как говорится...


Ребята, не стоит вскрывать эту тему. Вы молодые, шутливые, вам все легко. Это не то. Это не Чикатило и даже не архивы спецслужб. Сюда лучше не лезть. Серьезно, любой из вас будет жалеть. Лучше закройте тему и забудьте, что тут писалось. Я вполне понимаю, что данным сообщением вызову дополнительный интерес, но хочу сразу предостеречь пытливых — стоп. Остальные просто не найдут.

Как-то так. Ну что же, если у вас хватило смелости приступить к этому, то давайте начнем. В данной статье я покажу вам два способа получения и хранения приватных ключей и сертификатов: в файловой системе и на токене. В сущности, для обоих применима Аксиома Эскобара, но для второго способа потребуется дополнительный энджин, так что если вы решитесь пойти путем шиноби, то милости просим в данный раздел, остальные могут его пропустить.


Настройка openssl для работы с токенами (на примере Рутокен)


Для работы с Рутокенами, аппаратно поддерживающими ГОСТ-криптографию, для openssl существует rtengine. Его установка достаточно проста и не сильно отличается от GOST, нужно только знать, что и где брать.


  1. Скачиваете отсюда SDK rutoken разработчика и достаете из папки sdk/openssl/rtengine/bin/ собранную под вашу платформу библиотеку с энжином и помещаете в папку с engine.


  2. Скачать и установить модуль librtpkcs11ecp.so. отсюда


  3. Собирать утилиту из ветки master OpenSC взяв коммит 8cf1e6f


  4. После того как мы все это сделали, осталось настроить конфигурационный файл openssl.cnf:


    # в начала файла написать
    openssl_conf = openssl_def
    
    ...
    
    # в конец файла
    # OpenSSL default section
    [openssl_def]
    engines = engine_section
    
    # Engine section
    [engine_section]
    gost = gost_section
    rtengine = rtengine_section
    
    # Engine gost section
    [gost_section]
    engine_id = gost
    dynamic_path = /usr/lib/x86_64-linux-gnu/engines-1.1/gost.so
    default_algorithms = ALL
    
    # Engine rtengine section
    [rtengine_section]
    engine_id = rtengine
    dynamic_path = /path/to/engine/librtengine.so
    MODULE_PATH = /path/to/module/librtpkcs11ecp.so
    RAND_TOKEN = pkcs11:manufacturer=Aktiv%20Co.;model=Rutoken%20ECP
    default_algorithms = CIPHERS, DIGEST, PKEY, RAND


Работоспособность engine мы проверим на деле.


Создание приватного ключа и сертификата для принципала, KDC и УЦ


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


  1. Создадим приватный ключ и сертификат удостоверяющего центра, а также приватный ключ, запрос на сертификат и сам сертификат, подписанный УЦ для KDC:


    openssl genpkey -engine gost -algorithm gost2012_256 -pkeyopt paramset:B -out CA_key.pem # приватный ключ УЦ
    openssl req -engine gost -key CA_key.pem -new -x509 -out CA_cert.pem # самоподписанный сертификат УЦ
    
    openssl genpkey -engine gost -algorithm gost2012_256 -pkeyopt paramset:B -out KDC_key.pem # приватный ключ KDC
    openssl req -engine gost -new -out KDC.req -key ./KDC_key.pem # заявка на подписание сертификата для KDC
    
    # !!! ТУТ ВАЖНО СОЗДАТЬ РАСШИРЕНИЕ pkinit_extensions ИЗ ИНСТРУКЦИИ ВЫШЕ
    REALM=AKTIV-TEST.RU; export REALM # устанавливаем домен KDC
    CLIENT=127.0.0.1; export CLIENT # устанавливаем имя клиента, для которого подписываем сертификат (в нашем случае имя KDC). Я все воспроизвожу локально, поэтому ставлю localhost
    openssl x509 -engine gost -req -in ./KDC.req -CAkey ./CA_key.pem -CA ./CA_cert.pem -out ./KDC.pem -extfile ./pkinit_extensions -extensions kdc_cert -CAcreateserial # подписываем сертификат для KDC.
    
    sudo cp ./KDC.pem ./KDC_key.pem ./CA_cert.pem /var/lib/krb5kdc # отправляем все ключи и сертификаты у директорию kdc.

  2. Изменим конфигурационный файл kdc, чтобы он знал, откуда брать ключи и сертификаты:


    
    [kdcdefaults]
    
    ...
    
       pkinit_identity = FILE:/var/lib/krb5kdc/KDC.pem,/var/lib/krb5kdc/KDC_key.pem
       pkinit_anchors = FILE:/var/lib/krb5kdc/CA_cert.pem


[libdefaults]
spake_preauth_groups = edwards25519


3. Зададим принудительную предварительную аутентификацию для принципала:

   ```bash
   sudo kadmin.local
   kadmin.local$: modprinc +requires_preauth user

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


    1. Для создания ключа и заявки в ФС все делаем почти аналогично, как для KDC:


      openssl genpkey -engine gost -algorithm gost2012_256 -pkeyopt paramset:B -out client_key.pem # приватный ключ клиента
      openssl req -engine gost -new -out client.req -key ./client_key.pem # заявка на подписание сертификата для клиента

    2. Для создания ключа на токене и подписания сертификата для него нужно выполнить следующее:


      pkcs11-tool --module /path/to/module/librtpkcs11ecp.so --keypairgen --key-type GOSTR3410-2012-256:B -l --id 45 # создаем приватный и публичный ключ и помещаем их на токен и присваеваем  id=45
      openssl req -engine rtengine -new -key="pkcs11:id=E" -keyform engine -out client.req # создаем заявку на сертификат для ключа хранящегося на токене. E -- ascii запись 45


  2. Теперь подпишем нашу заявку:


    REALM=AKTIV-TEST.RU; export REALM # имя домена
    CLIENT=user; export CLIENT # имя пользователя, для которого подписываем сертификат
    openssl x509 -engine gost -CAkey ./CA_key.pem -CA ./CA_cert.pem -req -in ./client.req -extensions client_cert -extfile ./pkinit_extensions -out client.pem
    openssl x509 -engine gost -in client.pem -out client.crt -outform DER # Конвертируем сертификат из формата PEM в формат CRT

  3. Теперь дело за малым: в первом случае загружаем сертификат в специальную директорию, во втором – на токен:


    sudo cp ./client_key.pem client.pem /etc/krb5 # первый случай
    
    pkcs11-tool --module /usr/lib/librtpkcs11ecp.so -l -y cert -w ./client.crt --id 45 # второй случай (сертификат должен иметь тот же id, что и ключ)

  4. Настраиваем файл конфигурации клиента (у меня это /etc/krb5.conf):


    [libdefaults]
    
    ...
       pkinit_anchors = FILE:/var/lib/krb5kdc/CA_cert.pem
       # для аутентификации через ФС
       pkinit_identities = FILE:/etc/krb5/client.pem,/etc/krb5/client_key.pem 
       # для аутентификации через токен
       #pkinit_identities = PKCS11:/usr/lib/librtpkcs11ecp.so
    


Надеюсь, что на данном этапе проблем не возникнет. Мы совсем близко! Добавим реализацию новых алгоритмов.


Добавление нового алгоритма цифровой подписи


Алгоритмы ЭЦП добавить куда проще чем те, что рассматривались ранее – придется заменить всего-то 2 файли! src/plugins/preauth/pkinit/pkcs11.h и src/plugins/preauth/pkinit/pkinit_crypto_openssl.c


  1. Начнем с добавления идентификаторов новых механизмов и ключей в заголовочник pkcs11.h. Идентификатор механизма – это название алгоритма, который подается токену для того, чтобы он его совершил. Все эти идентификаторы стандартизированы и их можно найти в интернете (хоть и с большим трудом). Наши я нашел здесь в sdk/pkcs11/include/rtpkcs11t.h. Добавим в заголовочник следующие идентификаторы ключей и механизмов:


    ...
    
    #define CKK_TWOFISH      (0x21)
    #define CKK_GOSTR3410        (0x30)
    #define CKK_GOSTR3411        (0x31)
    #define CKK_GOST28147        (0x32)
    #define CKK_VENDOR_DEFINED   (1UL << 31)
    
    // A mask for new GOST algorithms.
    // For details visit https://tc26.ru/standarts/perevody/guidelines-the-pkcs-11-extensions-for-implementing-the-gost-r-34-10-2012-and-gost-r-34-11-2012-russian-standards-.html
    #define NSSCK_VENDOR_PKCS11_RU_TEAM     (CKK_VENDOR_DEFINED | 0x54321000)
    #define CK_VENDOR_PKCS11_RU_TEAM_TK26   NSSCK_VENDOR_PKCS11_RU_TEAM
    
    #define CKK_GOSTR3410_512    (CK_VENDOR_PKCS11_RU_TEAM_TK26 | 0x003)
    
    ...
    
    #define CKM_AES_MAC_GENERAL      (0x1084)
    #define CKM_AES_CBC_PAD          (0x1085)
    #define CKM_GOSTR3410_KEY_PAIR_GEN   (0x1200UL)
    #define CKM_GOSTR3410            (0x1201UL)
    #define CKM_GOSTR3410_WITH_GOSTR3411 (0x1202UL)
    #define CKM_GOSTR3410_KEY_WRAP  (0x1203UL)
    #define CKM_GOSTR3410_DERIVE    (0x1204UL)
    #define CKM_GOSTR3410_512_KEY_PAIR_GEN   (CK_VENDOR_PKCS11_RU_TEAM_TK26 | 0x005)
    #define CKM_GOSTR3410_512    (CK_VENDOR_PKCS11_RU_TEAM_TK26 | 0x006)
    #define CKM_GOSTR3410_12_DERIVE  (CK_VENDOR_PKCS11_RU_TEAM_TK26 | 0x007)
    #define CKM_GOSTR3410_WITH_GOSTR3411_12_256  (CK_VENDOR_PKCS11_RU_TEAM_TK26 | 0x008)
    #define CKM_GOSTR3410_WITH_GOSTR3411_12_512  (CK_VENDOR_PKCS11_RU_TEAM_TK26 | 0x009)
    #define CKM_GOSTR3411            (0x1210UL)
    #define CKM_GOSTR3411_HMAC      (0x1211UL)
    #define CKM_GOSTR3411_12_256 (CK_VENDOR_PKCS11_RU_TEAM_TK26 | 0x012)
    #define CKM_GOSTR3411_12_512 (CK_VENDOR_PKCS11_RU_TEAM_TK26 | 0x013)
    #define CKM_GOSTR3411_12_256_HMAC    (CK_VENDOR_PKCS11_RU_TEAM_TK26 | 0x014)
    #define CKM_GOSTR3411_12_512_HMAC    (CK_VENDOR_PKCS11_RU_TEAM_TK26 | 0x015)
    #define CKM_GOST28147_KEY_GEN   (0x1220UL)
    #define CKM_GOST28147_ECB       (0x1221UL)
    #define CKM_GOST28147           (0x1222UL)
    #define CKM_GOST28147_MAC       (0x1223UL)
    #define CKM_GOST28147_KEY_WRAP  (0x1224UL)
    

    Всеми ими мы не воспользуемся, но на будущее можно добавить.


  2. В файле pkinit_crypto_openssl.c, в первую очередь, нужно добавить подгрузку энджинов перед началом работы. Вызов этой функции также нужно вставить перед get_key, т.к. эта функция почему-то вызывается перед подгрузкой энджинов:


    #include <dirent.h>
    #include <arpa/inet.h>
    #include <openssl/engine.h>
    
    static ENGINE *eng = NULL;
    
    krb5int_init_engines()
    {
       if (eng) return;
       OPENSSL_add_all_algorithms_conf();
       ERR_load_crypto_strings();
    
     if (!(eng = ENGINE_by_id("rtengine"))) {
           printf("Engine rtengine doesn’t exist");
           return;
       }
    
       ENGINE_init(eng);
       ENGINE_set_default(eng, ENGINE_METHOD_ALL);
    
       if (!(eng = ENGINE_by_id("gost"))) {
           printf("Engine gost doesn’t exist");
           return;
       }
    
       ENGINE_init(eng);
       ENGINE_set_default(eng, ENGINE_METHOD_ALL);
    }
    
    ...
    
    get_key(krb5_context context, pkinit_identity_crypto_context id_cryptoctx,
           char *filename, const char *fsname, EVP_PKEY **retkey,
           const char *password)
    {
    ...
    
       krb5_error_code retval;
    
       krb5int_init_engines();
    
    ...
    }
    
    ...
    
    int
    pkinit_openssl_init()
    {
       /* Initialize OpenSSL. */
       ERR_load_crypto_strings();
       OpenSSL_add_all_algorithms();
       krb5int_init_engines();
       return 0;
    }

  3. После инициализации энджинов можно приступить к замене механизмов и электронной подписи. В данный момент жестко зашит только один алгоритм – RSA с хешом, полученным из sha1. Мы же встроим выбор между возможными режимами в зависимости от типа ключа, хранящегося на токене или в ФС. Для этого введем несколько функций, которые будут на вход получать контексты шифрования, а на выходе выдавать необходимые идентификаторы алгоритмов и механизмов. Также будет необходимо немного подправить функцию получения хендла приватного ключа с токена, т.к. она возвращает только RSA ключи:


    // закомментируем фильтр на тип приватного ключа
    krb5_error_code
    pkinit_find_private_key(pkinit_identity_crypto_context id_cryptoctx,
                           CK_ATTRIBUTE_TYPE usage,
                           CK_OBJECT_HANDLE *objp)
    {
    
    ...
    
       true_false = TRUE;
       attrs[nattrs].type = usage;
       attrs[nattrs].pValue = &true_false;
       attrs[nattrs].ulValueLen = sizeof true_false;
       nattrs++;
    #endif
    
    //    keytype = CKK_RSA;
    //    attrs[nattrs].type = CKA_KEY_TYPE;
    //    attrs[nattrs].pValue = &keytype;
    //    attrs[nattrs].ulValueLen = sizeof keytype;
    //    nattrs++;
    
    ...
    
    }
    
    // функция получения типа идентификатора алгоритма цифровой подписи в зависимости от полученного типа ключа:
    static int
    ckk_key_to_nid(CK_KEY_TYPE type)
    {
       switch(type){
       case CKK_GOSTR3410:
           return NID_id_GostR3410_2012_256;
       case CKK_GOSTR3410_512:
           return NID_id_GostR3410_2012_512;
       default:
           return NID_rsa;
       }
    }
    
    // функция, возвращающая идентификатор алгоритма цифровой подписи, если он хранится на токене:
    static int
    pkinit_get_pkey_type(krb5_context context,
                           pkinit_identity_crypto_context id_cryptoctx)
    {
       CK_OBJECT_HANDLE obj;
       CK_ATTRIBUTE attrs[1];
       CK_KEY_TYPE key_type;
       int r;
    
       // открываем сессию:
       if (pkinit_open_session(context, id_cryptoctx)) {
           pkiDebug("can't open pkcs11 session\n");
           return NID_rsa;
       }
    
       // находим приватный ключ:
       if (pkinit_find_private_key(id_cryptoctx, CKA_SIGN, &obj)) {
           return NID_rsa;
       }
    
       // вытаскиваем тип ключа:
       attrs[0].type = CKA_KEY_TYPE;
       attrs[0].pValue = &key_type;
       attrs[0].ulValueLen = sizeof (key_type);
    
       if ((r = id_cryptoctx->p11->C_GetAttributeValue(id_cryptoctx->session,
                                                       obj, attrs, 1)) != CKR_OK) {
           pkiDebug("C_GetAttributeValue: %s\n Used RSA\n", pkinit_pkcs11_code_to_text(r));
           return NID_rsa;
       }
    
       // возвращаем идентификатор алгоритма:
       return ckk_key_to_nid(key_type);
    
    }
    
    // функция, возвращающая идентификатор алгоритма хеширования в зависимости от того, какой алгоритм цифровой подписи используется:
    static int
    pkey_to_digest_nid(const EVP_PKEY* const pkey)
    {
       switch (EVP_PKEY_id(pkey)) {
       case NID_id_GostR3410_2012_256:
           return NID_id_GostR3411_2012_256;
       case NID_id_GostR3410_2012_512:
           return NID_id_GostR3411_2012_512;
       case NID_id_GostR3410_2001:
           return NID_id_GostR3411_2012_256;
       default:
           return NID_sha1;
       }
    }
    
    // функция, возвращающая идентификатор алгоритма хеширования для данного контекста:
    static int
    get_digest_nid(krb5_context context, const pkinit_identity_crypto_context id_cryptctx)
    {
       int nid;
       // если ключ указан (он находится в ФС), значит достаем NID оттуда, иначе из токена
       if (id_cryptctx->my_key) {
           nid = EVP_PKEY_id(id_cryptctx->my_key);
       } else {
           nid = pkinit_get_pkey_type(context, id_cryptctx);
       }
    
       switch (nid) {
       case NID_id_GostR3410_2012_256:
           return NID_id_GostR3411_2012_256;
       case NID_id_GostR3410_2012_512:
           return NID_id_GostR3411_2012_512;
       case NID_id_GostR3410_2001:
           return NID_id_GostR3411_2012_256;
       default:
           return NID_sha1;
       }
    }
    
    // функция, возвращающая идентификатор алгоритма цифровой подписи:
    static int
    get_alg_nid(krb5_context context, const pkinit_identity_crypto_context id_cryptctx)
    {
       int nid;
       if (id_cryptctx->my_key) {
           nid = EVP_PKEY_id(id_cryptctx->my_key);
       } else {
           nid = pkinit_get_pkey_type(context, id_cryptctx);
       }
    
       switch (nid) {
       case NID_id_GostR3410_2012_256:
           return NID_id_tc26_signwithdigest_gost3410_2012_256;
       case NID_id_GostR3410_2012_512:
           return NID_id_tc26_signwithdigest_gost3410_2012_512;
       case NID_id_GostR3410_2001:
           return NID_id_tc26_signwithdigest_gost3410_2012_256;
       default:
           return NID_sha1WithRSAEncryption;
       }
    }
    
    // функция возвращающая идентификатор механизма:
    static CK_MECHANISM_TYPE
    get_mech_type(krb5_context context, const pkinit_identity_crypto_context id_cryptctx)
    {
       int nid;
       if (id_cryptctx->my_key) {
           nid = EVP_PKEY_id(id_cryptctx->my_key);
       } else {
           nid = pkinit_get_pkey_type(context, id_cryptctx);
       }
    
       switch (nid) {
       case NID_id_GostR3410_2012_256:
           return CKM_GOSTR3410_WITH_GOSTR3411_12_256;
       case NID_id_GostR3410_2012_512:
           return CKM_GOSTR3410_WITH_GOSTR3411_12_512;
       case NID_id_GostR3410_2001:
           return CKM_GOSTR3410_WITH_GOSTR3411_12_256;
       default:
           return CKM_RSA_PKCS;
       }
    }

  4. Теперь лишь осталось в функциях создания цифровой подписи cms_signeddata_create и create_signature заменить жесткую установку алгоритма на выбор алгоритма в зависимости от контекста:


    
    krb5_error_code
    cms_signeddata_create(krb5_context context,
                         pkinit_plg_crypto_context plg_cryptoctx,
                         pkinit_req_crypto_context req_cryptoctx,
                         pkinit_identity_crypto_context id_cryptoctx,
                         int cms_msg_type,
                         int include_certchain,
                         unsigned char *data,
                         unsigned int data_len,
                         unsigned char **signed_data,
                         unsigned int *signed_data_len)
    {
    
    ...
    
           /* Set digest algs */
           p7si->digest_alg->algorithm = OBJ_nid2obj(
                       get_digest_nid(context, id_cryptoctx));
    
    ...
    
           p7si->digest_enc_alg->algorithm = OBJ_nid2obj(get_alg_nid(context, id_cryptoctx));
    
    ...
    
               EVP_DigestInit_ex(ctx, EVP_get_digestbynid(get_digest_nid(context, id_cryptoctx)), NULL);
    
    ...
    
               alen = (unsigned int )ASN1_item_i2d((ASN1_VALUE *) sk, &abuf,
                                    ASN1_ITEM_rptr(PKCS7_ATTR_SIGN));
    
    ...
            // заменяем проверку механизма, на проверку идентификатора алгоритма хеширования (сделать это в двух местах):
               if (id_cryptoctx->pkcs11_method == 1 &&
               get_digest_nid(context, id_cryptoctx) == NID_sha1) {
    
    }
    
    static krb5_error_code
    create_signature(unsigned char **sig, unsigned int *sig_len,
                    unsigned char *data, unsigned int data_len, EVP_PKEY *pkey)
    {
    
    ...
    
       EVP_SignInit(ctx, EVP_get_digestbynid(pkey_to_digest_nid(pkey)));
    
    ...
    
    }
    
    // узнаем механизм теперь на этапе создания подписи:
    static krb5_error_code
    pkinit_sign_data_pkcs11(krb5_context context,
                           pkinit_identity_crypto_context id_cryptoctx,
                           unsigned char *data,
                           unsigned int data_len,
                           unsigned char **sig,
                           unsigned int *sig_len)
    {
    
    ...
    
       mech.mechanism = get_mech_type(context, id_cryptoctx);
       mech.pParameter = NULL;
       mech.ulParameterLen = 0;
    
    ...
    
    }


После всех этих манипуляций вы можете попробовать зайти, используя токен или сертификат в файловой системе (во втором случае, вероятно, потребуются права рута):


sudo kinit user

Если после запроса пароля токена не потребовалось вводить пароль user, значит, все отработало правильно.


Все замечания и вопросы вы можете писать в комментариях, а я постараюсь на них оперативно ответить.

Комментарии (8)


  1. Barsik68
    20.09.2019 21:26

    Спасибо за статью!
    Одна просьба заменить устаревший термин ЭЦП, который официально не используется с 2014 года.

    Термин «электронная цифровая подпись» (сокращенно – ЭЦП), ставший привычным для большинства граждан, считается устаревшим. Он был введен в 2002 г. с принятием Федерального закона №1-ФЗ «Об электронной цифровой подписи» и утратил силу с 01.01.2014 г. после вступления в силу Федерального закона от 6 апреля 2011 г.63-ФЗ «Об электронной подписи».


    1. lo1ol Автор
      21.09.2019 09:31

      Учту замечание, но в статье используется неформальный стиль изложения, так что искажения смысла от этого не должно возникать


  1. pilniy
    21.09.2019 04:51

    В академических целях — интересно.
    А практическая значимость?


  1. pilniy
    21.09.2019 04:51

    В академических целях — интересно.
    А практическая значимость?


    1. lo1ol Автор
      21.09.2019 09:32

      Насколько мне известно, в госучреждениях часто стремиться использовать только отечественные криптографические алгоритмы. Эта статья поможет сисадминам перевести и Kerberos на ГОСТы.


    1. vov_i
      21.09.2019 09:40

      Возможно Вы удивитесь, но «основано на реальных событиях». Правда дело происходило под виндой. Некий регулятор потребовал у некого заказчика, чтобы доменная аутентификация была реализована на ГОСТ-алгоритмах. Под виндой вопрос решается при помощи ГОСТового CSP, а вот под Линуксами требуются такие танцы.
      Мне самому такие требования от регулятора показались странными, но проще было сделать, чем объяснить, почему не.


      1. pilniy
        21.09.2019 12:32

        В двойне странно. Тот же «регулятор» (как вы все таки красиво называете паразита :) должен был потребовать «заключение о корректности встраивания».


        1. gotch
          22.09.2019 22:08

          И поэтому бесплатной реализацией обойтись не получается, надо купить коммерческую реализацию, с сертификатом соответствия от Аль Эфесби.