Протокол Kerberos 5 сейчас активно используется для аутентификации. Особенностью данного протокола является то, что он осуществляет аутентификацию, базируясь на четырех китах:
Симметричное шифрование;
Хеширование;
ЭЦП;
Третья доверенная сторона.
Начиная с пятой версии появилась возможность использовать еще асимметричное шифрование (для электронной подписи). Более подробно на работе протокола 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, который содержит в себе все необходимые отечественные криптографические алгоритмы.
Установка дичайше проста, например, и происходит следующим образом:
Выкачиваете из репозитория реализацию данного энджина.
собираете библиотеку с ним (mkdir build && cd build && cmake… && make):
mkdir build
cd build
cmake ..
make
В директории bin (КОТОРАЯ ПОЯВИТСЯ В КОРНЕВОМ КАТАЛОГЕ ПРОЕКТА!!!) будет лежать динамическая библиотека gost.so – это и есть наш энджин. Его нужно будет перенести в директорию, где хранятся энджины openssl. Узнать месторасположение данной директории можно с помощью:
openssl version -a | grep ENGINESDIR
Дело за последним – нужно сказать 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:
Флаг -engine как раз говорит о том, какой engine нужно подгрузить перед началом работы для того, чтобы стал виден алгоритм генерации ключей для ГОСТ Р 34.10-2012.
Реализация алгоритма хеширования
Начнем с самого простого – с реализации алгоритма Стрибог. Kerberos обладает жесткой связью алгоритма хеширования и шифрования, то есть вы не можете просто так взять и выбрать для шифрования один алгоритм, а для хеширования другой – вам нужно будет встроить связку алгоритма хеширования и шифрования. Причина этого мне не известна, но раз там бытуют такие правила – давайте попробуем создать связку алгоритма Стрибог и AES.
Т.к. нам понадобится уверенность в подключении в разных местах нашей программы, давайте создадим сначала небольшую библиотеку gost_helper, которая будет содержать в себе функцию инициализации энджина в openssl, а так же для удобства несколько функций, возвращающих контексты некоторых алгоритмов хеширования – нам это поможет в дальнейшем. Назовем эту библиотеку gost_helper и создадим для нее соответствующий заголовочник и исходный файл в директории src/lib/crypto/openssl/:
После добавления вспомогательной библиотеки необходимо будет объявить о ее существовании в 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
Стоит заметить, что в дальнейшем при подключении данной библиотеки, нам надо будет добавлять зависимости некоторых файлов от заголовочника данной библиотеки. Делается это очень просто – отыскивается цель, куда надо добавить зависимость в deps файле и записывается зависимость от rel/path/to/gost_helper.h. Например, в src/lib/crypto/openssl/hash_provider/deps нужно будет добавить следующее:
В целях экономии места в статье и чтобы размять ваши мозги, я больше не буду обращать на это внимание: будьте внимательны и осторожны!
Добавим теперь реализации функций хеширования, все их реализации у нас лежат в 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
};
Теперь надо объявить эти контексты во всей библиотеке. Для этого нужно создать их описание в файле 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;
...
Объявим идентификатор связки Стрибог и AES, внедря макросы, которые мы назовем CKSUMTYPE_STRIBOG_256_AES256, CKSUMTYPE_STRIBOG_512_AES256, ENCTYPE_AES256_CTS_STRIBOG_256, ENCTYPE_AES256_CTS_STRIBOG_512. Их надо объявить в шаблоне заголовочного файла src/include/krb5/krb5.hin. Выглядеть это будет примерно так:
Теперь необходимо добавить две связки функции хеширования и шифрования и функции шифрования и хеширования. Зачем две, если они эквиваленты, спросите вы? Ответ: не знаю, или это исторический костыль или способ оптимизации. Тем не менее, давайте добавим в необходимые файлы новые структуры:
// в файле 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 },
};
Казалось бы уже все, а вот и нет! Далее наступают проблемы, которые будут видны только в процессе отладки, некоторых из них можно было бы избежать, указав, например, другие указатели на функции в вышеуказанных структурах, но мы пойдем более сложным путем, чтобы показать, что еще возможно придется исправить по дороге. Обо всех этих проблемах я узнал только пользуясь отладчиком:
// В файле 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. Выполните следующее:
Остановите krb5kdc:
service krb5-kdc stop
Подправим файл конфигурации 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 бит
Аналогичные изменения в файле конфигурации всего протокола 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 бит
Далее запустим krb5kdc т.к. изменился master_key, то возможно придется создать базу данных principals заново с помощью krb5_newrealm.
После этого создаем всех необходимых принципалов и можно начать работу. Попробуйте аутентифицироваться с помощью kinit.
Проверим, что хеширование происходит по указанному алгоритму можно с
помощью klist -e.
Если сервис не запускается, то его можно запустить из-под рута с помощью src/kdc/krb5kdc. Если все запустилось, прошло гладко – поздравляю вас! Иначе – увы, панацею от всех проблем я не предлагаю, а лишь предлагаю "небольшую" инструкцию, содержащую основные шаги, которые вам придется совершить, дабы внедрить новый алгоритм в Kerberos. И если у вас ничего не вышло – берите в руки gdb и смотрите, где что выходит не так. Вам лишь могу дать несколько советов:
собирать проект в режиме отладки можно, если передать в ./configure CFLAGS=" -g -O0";
запустить krb5kdc не в фоновом режиме можно с помощью флага -n;
надеюсь до этого не дойдет (хотя при реализации асимметричных алгоритмов у меня все-таки дошло) – возможно вам потребуются отладочные символы openssl – установите их или взяв из репозитория, или установив openssl из исходников с отладочными символами;
то же самое касается gost engine.
По идее этого набора советов вам должно хватить для того чтобы избавить себя от лишней траты времени в поисках "неизведанного".
Реализация алгоритма шифрования
В этом разделе я покажу, как можно добавить свой алгоритм шифрования данных в Kerberos, и мы попробуем создать связку Магмы и Стрибога, добавленного в прошлом разделе. Тут я уже предполагаю, что небольшая библиотека gost_helper уже была добавлена в прошлом разделе. Ну что же, разминаем пальцы и приступаем:
Сначала опишем алгоритм в нашей библиотеке libk5crypto описав их в заголовочном файле src/lib/crypto/krb/crypto_int.h.
В директории 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
};
В шаблоне src/lib/crypto/openssl/enc_provider/Makefile.in укажем, что появился новый исходник:
После этих манипуляций можно попробовать протестировать нововведенный алгоритм. Тестирование происходит аналогичным образом, что и в прошлом разделе. Имя связки можно взять в виде алиаса (например, gost89-stribog512) или с помощью имени самого алгоритма (например, gost89-cbc-stribog-512). Надеюсь, что все заработает, иначе не забывайте о том, что я сказал ранее.
Добавление алгоритма цифровой подписи
Ура! Мы приступаем к финальному разделу данной статьи и попробуем добавить собственный алгоритм электронной подписи. Не стоит пугаться, его добавить проще чем что-либо еще, так что давайте поскорее приступим… Хотя нет, подождите, для начала небольшая ремарка.
Асимметричное шифрование, достаточно, тяжеловесная штука. Одна из его главных особенностей – возможность аутентификации: все это построено на удостоверяющих центрах, сертификатах и всякой-всякой сложной лабуде. Как говорится...
Ребята, не стоит вскрывать эту тему. Вы молодые, шутливые, вам все легко. Это не то. Это не Чикатило и даже не архивы спецслужб. Сюда лучше не лезть. Серьезно, любой из вас будет жалеть. Лучше закройте тему и забудьте, что тут писалось. Я вполне понимаю, что данным сообщением вызову дополнительный интерес, но хочу сразу предостеречь пытливых — стоп. Остальные просто не найдут.
Как-то так. Ну что же, если у вас хватило смелости приступить к этому, то давайте начнем. В данной статье я покажу вам два способа получения и хранения приватных ключей и сертификатов: в файловой системе и на токене. В сущности, для обоих применима Аксиома Эскобара, но для второго способа потребуется дополнительный энджин, так что если вы решитесь пойти путем шиноби, то милости просим в данный раздел, остальные могут его пропустить.
Настройка openssl для работы с токенами (на примере Рутокен)
Для работы с Рутокенами, аппаратно поддерживающими ГОСТ-криптографию, для openssl существует rtengine. Его установка достаточно проста и не сильно отличается от GOST, нужно только знать, что и где брать.
Скачиваете отсюда SDK rutoken разработчика и достаете из папки sdk/openssl/rtengine/bin/ собранную под вашу платформу библиотеку с энжином и помещаете в папку с engine.
Скачать и установить модуль librtpkcs11ecp.so. отсюда
Собирать утилиту из ветки master OpenSC взяв коммит 8cf1e6f
После того как мы все это сделали, осталось настроить конфигурационный файл openssl.cnf:
Создание приватного ключа и сертификата для принципала, KDC и УЦ
За основу данной инструкции была взята инструкция по настройке kerberos. Я лишь адаптировал ее для работы с русскими алгоритмами, так что частично заглядывайте туда.
Создадим приватный ключ и сертификат удостоверяющего центра, а также приватный ключ, запрос на сертификат и сам сертификат, подписанный УЦ для 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.
Изменим конфигурационный файл kdc, чтобы он знал, откуда брать ключи и сертификаты:
3. Зададим принудительную предварительную аутентификацию для принципала:
```bash
sudo kadmin.local
kadmin.local$: modprinc +requires_preauth user
Создадим приватный ключ и заявку на сертификат для нашего принципала. Тут будет развилка, и надо выполнить разные действия в зависимости от того, где мы будем хранить приватный ключ: на токене или в ФС:
Для создания ключа и заявки в ФС все делаем почти аналогично, как для KDC:
Для создания ключа на токене и подписания сертификата для него нужно выполнить следующее:
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
Теперь подпишем нашу заявку:
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
Теперь дело за малым: в первом случае загружаем сертификат в специальную директорию, во втором – на токен:
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, что и ключ)
Настраиваем файл конфигурации клиента (у меня это /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
Начнем с добавления идентификаторов новых механизмов и ключей в заголовочник pkcs11.h. Идентификатор механизма – это название алгоритма, который подается токену для того, чтобы он его совершил. Все эти идентификаторы стандартизированы и их можно найти в интернете (хоть и с большим трудом). Наши я нашел здесь в sdk/pkcs11/include/rtpkcs11t.h. Добавим в заголовочник следующие идентификаторы ключей и механизмов:
Всеми ими мы не воспользуемся, но на будущее можно добавить.
В файле pkinit_crypto_openssl.c, в первую очередь, нужно добавить подгрузку энджинов перед началом работы. Вызов этой функции также нужно вставить перед get_key, т.к. эта функция почему-то вызывается перед подгрузкой энджинов:
После инициализации энджинов можно приступить к замене механизмов и электронной подписи. В данный момент жестко зашит только один алгоритм – 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 sessionn");
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: %sn Used RSAn", 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;
}
}
Теперь лишь осталось в функциях создания цифровой подписи 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, значит, все отработало правильно.
Все замечания и вопросы вы можете писать в комментариях, а я постараюсь на них оперативно ответить.