[Информационная безопасность, Криптография, Python, Графические оболочки, C] Поддержка токенов PKCS#11 с ГОСТ-криптографией в Python. Часть I
Автор
Сообщение
news_bot ®
Стаж: 6 лет 9 месяцев
Сообщений: 27286
Поддержка криптографических токенов PKCS#11 с российской криптографией в скриптовых языках (Python, Tcl) давно находится в моём поле зрения. Это, прежде всего, пакет TclPKCS11 и реализованная на его базе кроссплатформенная утилита cryptoarmpkcs. Утилита cryptoarmpkcs написана на tcl/tk и функционирует на различных платформах, включая Android. Пакет TclPKCS11 версии 1.0.0 заточен на работу именно с токенами, поддерживающими ГОСТ Р 34.11-2012 и ГОСТ Р 34.10-2012. Он позволяет генерировать ключевые пары по ГОСТ Р 34.10-2012 с длиной закрытого ключа 256 и 512 бит, формировать и проверять электронную подпись. Все это можно наглядно видеть в утилите cryptoarmpkcs, в которой в качестве криптодвижка используется именно этот пакет:
Первым желанием было портировать этот модуль в среду Python. Но прежде чем это сделать, я посмотрел, что уже есть для работы с криптографическим токенами PKCS#11 в Python.
Вне конкуренции, на мой взгляд, здесь проект PyKCS11. Изучив его внимательно, я понял, что не составит труда добавить в него поддержку новой российской криптографии: ГОСТ Р 34.10-2012 (электронная подпись), ГОСТ Р 34.11-2012 (хэширование), ГОСТ Р 34.12-2015 и ГОСТ Р 34.13-2015 (алгоритмы шифрования Кузнечик и Магма). Я написал письмо авторам с предложением добавить российские алгоритмы, предлагая свою помощь. К сожалению, ответ меня несколько обескуражил:
И тогда я решил вернуться к этой теме немного позже, а сейчас всё внимание сосредоточил на портировании проект TckPKCS11-1.0.1 в Python. Почему всё же проект TclPKCS11? Да всё очень просто. Основная задача, которую необходимо решить на Python, связана с электронной подписью по ГОСТ Р 34.10-2012 и использование шифрования на данном этапе не предполагается. В этом контексте проект TclPKCS11 абсолютно подходит. В нём реализована поддержка следующих криптографических функций:
— генерация ключевых пар по ГОСТ Р 34.10-2012 (512 и 1024 бита для открытого ключа), и даже по ГОСТ Р 34.10-2001;
— подсчет хэша по ГОСТ Р 34.10-2012 (256 и 512 бит), а также по ГОСТ Р 34.11-94 и SHA1;
— подписание и проверка подписи.
Из общих функций реализованы:
— управление токенами (инициализация токена, установка и смена PIN-кодов);
— получения списка слотов и информации о них;
— импорт сертификатов и ключей (только для ГОСТ-криптографии):
— установка меток для объектов (сертификаты, ключи);
— и другие.
Самое главное то, что использование этих механизмов намного проще, чем использование стандартного интерфейса PKCS#11, а следовательно и проще использования пакета PyKCS11. Всё это будет видно на примерах.
I. Портирование кода модуля tclpkcs11 в модуль pyp11 для Python
Портирование заключается в адаптации кода модуля tclpkcs11 к требованиям со стороны Python. Все изменения в проекте будут касаться только модуля tclpkcs11.c. Поэтому, первое, что мы сделаем, скопируем модуль tclpkcs11.c в файл pythonpkcs11.c и в дальнейшем будем работать именно с ним. Модуль для Python назовем pyp11. Использовать для его создания будем C API Python. Почему-то этот способ многие (но не я) считают самым трудным, но зато он самый эффективный. Анализ C API для Tcl и C API для Python показал их значительное сходство, что и позволило очень быстро провести портирование. Отметим основные этапы портирования, которые вполне возможно кому-то помогут перенести те или иные модули (библиотеки) из Tcl в Python или наоборот.
Первое, в файле pythonpkcs11.c заменяем все объявления Tcl_Obj на PyObject, что вполне естественно: Tcl работает со своими объектами, а Python со своими.
Второе касается передачи параметров.
В общем виде объявление функции, реализующей ту или иную команду Tcl, в С-коде выглядит следующим образом (применительно к нашему коду):
name_proc_tcl (CliendData cd, Tcl_Interp *interp, int objc, Tcl_Obj[] *objv[] ){
. . .
};
В Python аналогичный заголовок функции будет выглядеть так:
name_proc_py (PyObject *self, PyObject *args){
. . .
};
В C-коде для tcl проверка количества входных параметров проводится с использованием переменной objc.
name_proc_tcl (CliendData cd, Tcl_Interp *interp, int objc, Tcl_Obj[] *objv[] ){
if (objc != 4) {
. . .
Tcl_SetObjResult(interp, Tcl_NewStringObj("wrong # args: should be "pki::pkcs11::login handle slot password"", -1));
return(TCL_ERROR);
}
. . .
};
В Python параметры передаются в виде кортежа. Поэтому число переданных параметров вычисляется функцией PyTuple_Size(args):
name_proc_py (PyObject *self, PyObject *args){
//Вводим переменную для числа параметров
int objc;
objc = PyTuple_Size(args);
. . .
if (objc != 3) {
PyErr_SetString(PyExc_TypeError, "pyp11_login args error (count args != 3)");
return NULL;
}
. . .
};
Отметим, что число параметров в коде для Tcl на единицу больше, т.к. в objv[0] хранится имя функции (аналогично функции main в C).
В приведенном коде наглядно видно как обрабатываются ошибки в Tcl и Python.
Вызов прерывания в случае ошибки для Tcl выполняется оператором
return (TCL_ERROR);
Текстовое сообщение об ошибке формируется оператором TclSetObjResult.
Для Python будут использоваться операторы return NULL и PyErr_SetString.
Теперь самое главное — разбор параметров.
В Tcl каждый параметр передается как отдельный Tcl-объект, а в Python — как кортеж параметров в виде Python-объектов. Поэтому, если мы хотим вносить минимальные изменения в код, целесообразно сначала распаковать кортеж по отдельным объектам, например (применительно к функции pyp11_login):
…
char *tcl_handle;
long slotid_long;
char *password;
//Массив PyObject-ов для входных параметров
PyObject *argspy[3];
//Растаскиваем входные параметры/объекты ("OOO" - три объекта) по своим ячейкам
PyArg_ParseTuple(args, "OOO", &argspy[0], &argspy[1], &argspy[2])
…
Полученные объекты распаковываем с их функциональным назначением:
…
//Получаем строку (s) с handle библиотеки PKCS11
PyArg_Parse(argspy[0], "s", &tcl_handle);
//Получаем номер слота (l), в котором находится токен
PyArg_Parse(argspy[1], "l", &slotid_long);
//Получаем строку (s) с PIN-кодом владельца
PyArg_Parse(argspy[2], "s", &password);
...
Сразу оговоримся, что в C API Python имеется функция, которая позволяет сразу разбирать кортеж параметров. В этом случае можно обойтись одним оператором:
PyArg_ParseTuple(args, «sls», &tcl_handle, &slotid_long, &password);
Как ни парадоксально, это практически все рекомендации.
Осталось последнее, — возвращаемые значения.
Результаты выполнения команд возвращаются либо в виде строки, либо в виде списка, либо в виде словаря.
Приведём некоторые соответствия. Так для создания списка в коде для Tcl используется функция Tcl_NewObj(), а в коде для Python используется функция PyListNew(0).
Для добавления элемента в список для Tcl используется функция TclListObjAppendElement, а для Python — функция PyList_Append. Все эти соответствия можно найти, сравнив код TclPKCS11 и код pyp11 (ССЫЛКА).
Также вместо используемых функций ckalloc и ckfree в tclpkcs11.c для Tcl, в модуле pythonpkcs11.c используются стандартные функции работы с памятью — malloc и free.
После проведенного анализа модификация кода вместе с тестированием заняла пару рабочих дней.
II. Сборка и установка модуля pyp11
Итак, скачиваем архив и распаковываем его. Заходим в папку PythonPKCS11 и выполняем команду установки:
#python3 setup.py install
Лично я тестировал на платформах Windows, Linux, OS X. Отметим, что пакет TclPKCS11 успешно работает и на платформе Android.
После установки модуля переходим в папку tests и начинаем тестирование.
Pаботоспособность модуля pyp11 можно проверить даже без токена. В составе модуля есть функция pyp11.dgst, которая не привязана к токенам и позволяет посчитать хэш по ГОСТ Р 34.10-2012:
bash-4.4$ python3
Python 3.7.9 (default, Feb 1 2021, 16:55:33)
[GCC 8.4.0] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> import pyp11
#Считаем хэш по ГОСТ Р 34.11-2012-256 (stribog256)
>>> hash256 = pyp11.dgst("stribog256", "Текст для хэширования")
#Считаем хэш по ГОСТ Р 34.11-2012-512 (stribog512)
>>> hash512 = pyp11.dgst("stribog512", "Текст для хэширования")
>>> print("STRIBOG256=" + hash256)
STRIBOG256=26b8865c37831aa254706e6c3514fb23f386358e9dd858703a24d4825d2c4794
>>> print("STRIBOG512=" + hash512)
STRIBOG512=e92ff2063c586ec6e9c9569dad7dd503de1c88faafc8b1bf43909bfa36db92ccbf3823f0b8f5d877f10933ed7e670081018dac0929d17729422f05ce1f4c4f25
>>> quit()
bash-4.4$
Значение хэш возвращается в шестнадцатеричном виде.
Для перевода хэш-а в бинарный вид можно воспользоваться следующей функцией:
>>> hash256_bin = bytes(bytearray.fromhex(hash256))
Напомним, как перевести бинарный код в шестнадцатеричный:
>>> hash256 = bytes(hash256_bin).hex()
>>> print("STRIBOG256_NEW=" + hash256)
STRIBOG256_NEW=26b8865c37831aa254706e6c3514fb23f386358e9dd858703a24d4825d2c4794
>>>
Есть еще одна функция, которая также может работать без токена. Это функция parsecert. На вход этой функции подается сертификат в DER-формате, упакованный в шестнадцатеричную кодировку:
bash-4.4$ python3
Python 3.7.9 (default, Feb 1 2021, 16:55:33)
[GCC 8.4.0] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> import pyp11
>>> #Читаем серификат в DER-кодировке из файла
>>> with open("cert_256.der", "rb") as f:
... cert_der = f.read()
...
>>> #Упаковываем сертификат der в hex
>>> cert_der_hex = bytes(cert_der).hex()
>>> #Распарсиваем сертификат
>>> pubk = pyp11.parsecert(cert_der_hex)
>>>
Результатом выполнения команды pyp11.parsecert является словарь (ассоциированный список):
>>>print (pubk.keys())
dict_keys(['pkcs11_id', 'pubkeyinfo', 'pubkey', 'subject', 'issuer', 'serial_number', 'tbsCertificate', 'signature_algo', 'signature'])
>>>
В этом словаре находятся as1-структуры элементов сертификата. Все элементы закодированы в шестнадцатеричный формат. Среди элементов находится элемент pubkeyinfo со значением asn1-структуры subjectpublickeyinfo, элемент pubkey со значением публичного ключа, серийный номер сертификата, tbs-сертификат, который будет использоваться для проверки подписи сертификата, алгоритм подписи сертификата и значение самой подписи, а также элементы с информацией о владельце и издателе сертификата, полученные из сертификата и закодированные в шестнадцатеричное представление:
>>> subject = pubk['subject']
>>> print ('SUBJECT=' + subject)
SUBJECT=30820205310b3009060355040613025255312a3028060355042a0c21d09fd0b0d0b2d0b5d0bb20d090d0bdd0b0d182d0bed0bbd18cd0b5d0b2d0b8d1873135303306035504030c2cd09ed09ed09e20d09ad09ed09cd09fd090d09dd098d0af20d0add09ad09e2dd0a1d0a2d0a0d09ed099203937311d301b06092a864886f70d010901160e696e666f4072746564632e6f72673118301606052a85036401120d313137373734363733343433393116301406052a85036403120b3133383632313537373734311a301806082a85030381030101120c3030393732393131303536393130302e060355040c0c27d093d0b5d0bdd0b5d180d0b0d0bbd18cd0bdd18bd0b920d0b4d0b8d180d0b5d0bad182d0bed180310a3008060355040b0c013031353033060355040a0c2cd09ed09ed09e20d09ad09ed09cd09fd090d09dd098d0af20d0add09ad09e2dd0a1d0a2d0a0d09ed099203937315f305d06035504090c5631313931333620d0b32e20d09cd0bed181d0bad0b2d0b020d0bfd1802dd0b420312dd0b920d0a1d0b5d182d183d0bdd18cd181d0bad0b8d0b920d0b42e203130d09020d181d182d1802e203120d0bfd0bed0bc2e20323115301306035504070c0cd09cd0bed181d0bad0b2d0b0311c301a06035504080c13373720d0b32e20d09cd0bed181d0bad0b2d0b0311b301906035504040c12d0a5d0b0d180d0b8d182d0bed0bdd0bed0b2
>>>
Элемент pkcs11_id берётся не из сертификата, а рассчитывается как значение хэш по SHA-1 от значения публичного ключа. При использовании функции pyp11.parsecert в данном контексте (без подключенного токена) pkcs11_id будет равен -1:
>>> pkcs11_id = pubk['pkcs11_id']
>>> print ('PKCS11_ID=' + pkcs11_id)
PKCS11_ID=-1
>>>
Кто-то может сказать, а что, разве в Python нет средств разбора сертификатов? А как же, например, asn1crypto? Ответ заключается в том, что в этих средствах не учтены особенности российской криптографии. И вот, чтобы получить максимальную самодостаточность пакета pyp11, в него помимо функций, связанных с генерацией ключевой пары, формирования и проверки подписи, включены дополнительные функции. Например, asn1-структура pubkeyinfo необходима при проверке электронной подписи. И именно поэтому и была включена функция parsecert для частичного разбора сертификата x509.v3 и получения, в частности, asn1-структуры subjectpublickeyinfo (pubkeyinfo).
В папке tests проекта в файлах test0_* находятся соответствующие тесты.
############УБРАТЬ про FSB795 ################################
Отметим также, что для разбора сертификатов с российской криптографией можно воспользоваться пакетом fsb795:
>>> import fsb795
>>> #Парсим наш сертификат с помощью fsb795
>>> mycert = fsb795.Certificate(cert_der)
>>> #читаем данные о владельце сертификата и типе владельце
>>> dn, type = mycert.subjectCert()
>>> #DN - это словарь/ассоциированный список
>>> for key in dn.keys():
... print (key + '=' + dn[key])
...
Country=RU
GN=Имя Отчество
CN=ООО КОМПАНИЯ
E=info@ooo.org
OGRN=xxxxxxxxxxxx
SNILS=xxxxxxxxxxx
INN=xxxxxxxxxxxx
title=Генеральный директор
OU=0
O=ООО КОМПАНИЯ
street=119136 г. Москва
L=Москва
ST=77 г. Москва
SN=Харитонов
>>>
Теперь можно переходить к работе с токенами.
III. Управление токенами PKCS#11
Для тестирования функций управления подойдет любой токен PKCS#11, даже токен без поддержки какой-либо криптографии, например RuTokenLite. Но поскольку мы ведём речь о российской криптографии, то целесообразно сразу иметь токен с поддержкой российской криптографии. Здесь мы имеем в виду ГОСТ Р 34.10-2012 и ГОСТ Р 34.11-2012. Это может быть как аппаратный токен, например RuTokenECP-2.0, так и программные или облачные токены.
Установить программный токен или получить доступ к облачному токену можно, воспользовавшись утилитой cryptoarmpkcs.
Скачать утилиту cryptoarmpkcs можно здесь.
SPL
После запуска утилиты необходимо зайти на вкладку «Создать токены»:
На вкладке можно найти инструкции для получения токенов.
Итак, у нас токен и библиотека для работы с ним. После загрузки модуля pyp11 требуется загрузить библиотеку для работы с нашим токеном. В примерах будут использоваться библиотека librtpkcs11ecp-2.0 для работы с аппаратным токеном, библиотека libls11sw2016 для работы с программным токеном и библиотека libls11cloud.so для работы с облачным токеном. Читатели могут использовать любые токены, даже те, на которых нет российской криптографии, на них тоже можно проверить функции управления.
Итак, загружаем библиотеку командой loadmodule:
bash-4.4$ python3
Python 3.7.9 (default, Feb 1 2021, 16:55:33)
[GCC 8.4.0] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> import sys
>>> import pyp11
>>> #Выбираем библиотеку pkcs11
>>> lib = "/usr/local/lib64/librtpkcs11ecp_2.0.so"
>>> #Обработка ошибки при загрузке библиотеки PKCS#11
>>> try:
... #Вызываем команду загрузки библиотеки и плохим числом параметров
... handlelib = pyp11.loadmodule(lib, 2)
... except:
... print ('Ошибка загрузки библиотеки: ')
... e = sys.exc_info()[1]
... e1 = e.args[0]
... print (e1)
...
Ошибка загрузки библиотеки:
pyp11_load_module args error (count args != 1)
>>> #Загружаем с правильным синтаксисом
>>> idlib = pyp11.loadmodule(lib)
>>> #Печатаем дескриптор библиотеки
>>> print (idlib)
pkcs0
>>>
Дескриптор загруженной библиотеки используется при её выгрузке:
>>> pyp11.unloadmodule(idlib)
Теперь, когда библиотека загружена, можно получить список поддерживаемых её слотов и узнать есть ли в каких слотах токены. Для получения списка слотов с полной информацией о них и содержащихся в них токенах используется команда:
>>> slots = pyp11.listslots(idlib)
>>>
Команда pyp11.listslots возвращает список, каждый элемент которого содержит информацию о слоте:
[<info slot1>, <info slot2>, ... , <info slotN>]
.
В свою очередь, каждый элемент этого списка также является списком, состоящим из четырех элементов:
[<номер слота>, <метка токена, находящегося в слоте>, <флаги слота и токена>, <информация о токене>]
Если слот не содержит токен, то элементы <метка токена ...> и <информация о слоте> содержат пустое значение.
Наличие токена в слоте определяется по наличию флага TOKEN_PRESENT в списке <флаги слота и токена>:
bash-4.4$ python3
Python 3.7.9 (default, Feb 1 2021, 16:55:33)
[GCC 8.4.0] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> import sys
>>> import pyp11
>>> #Выбираем библиотеку
>>> #lib = '/usr/local/lib64/libls11sw2016.so'
>>> lib = '/usr/local/lib64/librtpkcs11ecp_2.0.so'
>>> #Загружаем библиотеку
>>> libid = pyp11.loadmodule(lib)
>>> #Дескриптор библиотеки
>>> print (libid)
pkcs0
>>> #Загружаем список слотов
>>> slots = pyp11.listslots(libid)
>>> tokpr = 0
>>> #Ищем первый подключенный токен
>>> while (tokpr == 0):
... #Перебираем слоты
... for v in slots:
... #Список флагов текущего слота
... flags = v[2]
... #Проверяем наличие в стоке токена
... if (flags.count('TOKEN_PRESENT') !=0):
... tokpr = 1
... #Избавляемся от лишних пробелов у метки слота
... lab = v[1].strip()
... infotok = v[3]
... slotid = v[0]
... break
... if (tokpr == 0):
... input ('Нет ни одного подключенного токена.\nВставьте токен и нажмите ВВОД')
... slots = pyp11.listslots(libid)
... #Информация о подключенном токене
...
Нет ни одного подключенного токена.
Вставьте токен и нажмите ВВОД
''
>>> #Информация о подключенном токене
>>> print ('LAB="' + lab + '", SLOTID=' + str(slotid))
LAB="Rutoken lite <no label>", SLOTID=0
>>> print ('FLAGS:', flags)
FLAGS: ['TOKEN_PRESENT', 'RNG', 'LOGIN_REQUIRED', 'SO_PIN_TO_BE_CHANGED', 'REMOVABLE_DEVICE', 'HW_SLOT']
>>>
Если взглянуть на флаги (FLAGS:) подключенного токена, то в них отсутствует флаг 'TOKEN_INITIALIZED'. Отсутствие этого флага говорит о том, что токен не инициализирован и требуется его инициализация:
#Проверяем, что токен проинициализирован
>>> if (flags.count('TOKEN_INITIALIZED') == 0''):
... #Инициализируем токен
... dd = pyp11.inittoken (libid, 0, '87654321',"TESTPY2")
...
>>>
Как видим, для инициализации токена используется следующая команда:
pyp11.inittoken (<дискриптор библиоткети>, <номер слота>, <SO-PIN>, <метка токена>)
Естественно, токен можно переинициализировать независимо от наличия флага 'TOKEN_INITIALIZED', только надо иметь в виду, что переинициализация токена ведет к уничтожению на нем всех объектов (ключи, сертификаты и т.д).
После инициализации токена должен быть проинициализирован USER-PIN. Эту операцию, как правило, делает производитель или продавец токена:
pyp11.inituserpin (<дискриптор библиоткети>, <номер слота>, <SO-PIN>, <USER-PIN>)
При этом выставляется флаг 'USER_PIN_TO_BE_CHANGED', который напоминает владельцу токена, что надо бы сменить свой USER-PIN (параметр 'user'):
pyp11.setpin (<дискриптор библиоткети>, <номер слота>, <'user'|'so'>,<текущий PIN-код>, <новый PIN-код>)
Сегодня «модно» получать в УЦ токены с закрытыми ключами и предустановленными PIN-кодами и ключевой парой. И, как правило, получателей не предупреждают, что целесообразно PIN-коды поменять, и не говорят, как это сделать. Я бы рекомендовал использовать для этого уже упоминавшуюся утилиту cryptoarmpkcs:
В папке tests проекта pyp11 лежат три теста test1_0_inittoken.py, test1_1_inituserpin.py и test1_2_change_userpin, которые наглядно демонстрируют инициализацию токена. Выполнять их надо в порядке перечисления.
Было бы несправедливо не показать инициализацию токена с использованием уже упоминавшегося пакета PyKCS11:
$ python3
Python 3.7.9 (default, Feb 1 2021, 16:55:33)
[GCC 8.4.0] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> import PyKCS11
>>> #Библиотека PKCS#11
>>> lib = '/usr/local/lib64/librtpkcs11ecp_2.0.so'
>>> pkcs11.load(lib)
>>> #Получаем список слотов с токенами
>>> slots = pkcs11.getSlotList(tokenPresent=True)
>>> #Ищем первый подключенный токен
>>> while (len(slots) == 0):
... input ('Нет ни одного подключенного токена.\nВставьте токен и нажмите ВВОД')
... #Получаем список слотов с токенами
... slots = pkcs11.getSlotList(tokenPresent=True)
...
Нет ни одного подключенного токена.
Вставьте токен и нажмите ВВОД
''
>>>
>>> #Берём первый подключенный токен
>>> slot = slots[0]
>>> #Закрываем все сессии на токене
>>> #SO-PIN
>>> so_pin = '87654321'
>>> lab_tok = "myLabel"
>>> #Инициализация токена
>>> pkcs11.initToken(slot, so_pin, lab_tok)
>>> session = pkcs11.openSession(slot, PyKCS11.CKF_SERIAL_SESSION | PyKCS11.CKF_RW_SESSION)
>>> #Установка первичного USER-PIN
>>> init_pin = '1234'
>>> session.login(so_pin, user_type=PyKCS11.CKU_SO)
>>> session.initPin(init_pin)
>>> session.logout()
>>> #Новый USER-PIN
>>> user_pin = '01234567'
>>> session.login(init_pin)
>>> # change PIN
>>> session.setPin(init_pin, user_pin)
>>> session.logout()
>>> quit()
$
IV. Ключевая пара, электронная подпись и её проверка
Итак, наш токен готов в работе: мы его проинициализировали и установили метку, но самое главное, мы поменяли PIN-коды (USER, SO).
Первым делом необходимо убедиться, что наш токен поддерживает необходимые нам криптографические механизмы. Поскольку речь идет о ГОСТ Р 34.10-2012 и ГОСТ Р 34.11-2012, то токен должен поддерживать механизмы CKM_GOST* в соответствии рекомендациями ТК-26.
Для получения списка механизмов используется команда pyp11.lictmechs:
<список механизмов> = pyp11.listmech(<идентификатор библиотеки>, <номер слота>)
Как ни странно, но токены могут не иметь поддержки криптомеханизмов, например, RuToken Lite. Они нас интересовать не будут. Мы будем использовать только токены с поддержкой ГОСТ Р 34.10-2012 и ГОСТ Р 34.11-2012:
$ python3
Python 3.7.9 (default, Feb 1 2021, 16:55:33)
[GCC 8.4.0] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> import pyp11
>>> #Выбираем библиотеку
>>> #Программный токен
>>> lib = '/usr/local/lib64/libls11sw2016.so'
>>> #Для Windows
>>> #lib='C:\Temp\ls11sw2016.dll'
>>> #Облачный токен
>>> #lib = '/usr/local/lib64/libls11cloud.so'
>>> #Аппаратный токен
>>> #lib = '/usr/local/lib64/librtpkcs11ecp_2.0.so'
>>> #Закгружаем выбранную библиотеку
>>> aa = pyp11.loadmodule(lib)
>>> listmech = pyp11.listmechs(aa, 0)
>>> print ('\tКриптографические механизмы токена')
Криптографические механизмы токена
>>> for mech in listmech:
... print(mech)
...
Криптографические механизмы
SPL
CKM_GOSTR3410_KEY_PAIR_GEN (0x1200)
CKM_GOSTR3410_512_KEY_PAIR_GEN (0xD4321005)
CKM_GOSTR3410 (0x1201)
CKM_GOSTR3410_512 (0xD4321006)
CKM_GOSTR3410_WITH_GOSTR3411 (0x1202)
CKM_GOSTR3410_WITH_GOSTR3411_12_256 (0xD4321008)
CKM_GOSTR3410_WITH_GOSTR3411_12_512 (0xD4321009)
CKM_GOSTR3410_DERIVE (0x1204)
CKM_GOSTR3410_12_DERIVE (0xD4321007)
CKM_GOSR3410_2012_VKO_256 (0xD4321045)
CKM_GOSR3410_2012_VKO_512 (0xD4321046)
CKM_KDF_4357 (0xD4321025)
CKM_KDF_GOSTR3411_2012_256 (0xD4321026)
CKM_KDF_TREE_GOSTR3411_2012_256 (0xD4321044)
CKM_GOSTR3410_KEY_WRAP (0x1203)
CKM_GOSTR3410_PUBLIC_KEY_DERIVE (0xD432100A)
CKM_LISSI_GOSTR3410_PUBLIC_KEY_DERIVE (0xD4321037)
CKM_GOST_GENERIC_SECRET_KEY_GEN (0xD4321049)
CKM_GOST_CIPHER_KEY_GEN (0xD4321048)
CKM_GOST_CIPHER_ECB (0xD4321050)
CKM_GOST_CIPHER_CBC (0xD4321051)
CKM_GOST_CIPHER_CTR (0xD4321052)
CKM_GOST_CIPHER_OFB (0xD4321053)
CKM_GOST_CIPHER_CFB (0xD4321054)
CKM_GOST_CIPHER_OMAC (0xD4321055)
CKM_GOST_CIPHER_KEY_WRAP (0xD4321059)
CKM_GOST_CIPHER_ACPKM_CTR (0xD4321057)
CKM_GOST_CIPHER_ACPKM_OMAC (0xD4321058)
CKM_GOST28147_KEY_GEN (0x1220)
CKM_GOST28147 (0x1222)
CKM_GOST28147_KEY_WRAP (0x1224)
CKM_GOST28147_PKCS8_KEY_WRAP (0xD4321036)
CKM_GOST_CIPHER_PKCS8_KEY_WRAP (0xD432105A)
CKM_GOST28147_ECB (0x1221)
CKM_GOST28147_CNT (0xD4321825)
CKM_GOST28147_MAC (0x1223)
CKM_KUZNYECHIK_KEY_GEN (0xD4321019)
CKM_KUZNYECHIK_ECB (0xD432101A)
CKM_KUZNYECHIK_CBC (0xD432101E)
CKM_KUZNYECHIK_CTR (0xD432101B)
CKM_KUZNYECHIK_OFB (0xD432101D)
CKM_KUZNYECHIK_CFB (0xD432101C)
CKM_KUZNYECHIK_OMAC (0xD432101F)
CKM_KUZNYECHIK_KEY_WRAP (0xD4321028)
CKM_KUZNYECHIK_ACPKM_CTR (0xD4321042)
CKM_KUZNYECHIK_ACPKM_OMAC (0xD4321043)
CKM_MAGMA_KEY_GEN (0xD432102A)
CKM_MAGMA_ECB (0xD4321018)
CKM_MAGMA_CBC (0xD4321023)
CKM_MAGMA_CTR (0xD4321020)
CKM_MAGMA_OFB (0xD4321022)
CKM_MAGMA_CFB (0xD4321021)
CKM_MAGMA_OMAC (0xD4321024)
CKM_MAGMA_KEY_WRAP (0xD4321029)
CKM_MAGMA_ACPKM_CTR (0xD4321040)
CKM_MAGMA_ACPKM_OMAC (0xD4321041)
CKM_GOSTR3411 (0x1210)
CKM_GOSTR3411_12_256 (0xD4321012)
CKM_GOSTR3411_12_512 (0xD4321013)
CKM_GOSTR3411_HMAC (0x1211)
CKM_GOSTR3411_12_256_HMAC (0xD4321014)
CKM_GOSTR3411_12_512_HMAC (0xD4321015)
CKM_PKCS5_PBKD2 (0x3B0)
CKM_PBA_GOSTR3411_WITH_GOSTR3411_HMAC (0xD4321035)
CKM_TLS_GOST_KEY_AND_MAC_DERIVE (0xD4321033)
CKM_TLS_GOST_PRE_MASTER_KEY_GEN (0xD4321031)
CKM_TLS_GOST_MASTER_KEY_DERIVE (0xD4321032)
CKM_TLS_GOST_PRF (0xD4321030)
CKM_TLS_GOST_PRF_2012_256 (0xD4321016)
CKM_TLS_GOST_PRF_2012_512 (0xD4321017)
CKM_TLS12_MASTER_KEY_DERIVE (0x3E0)
CKM_TLS12_KEY_AND_MAC_DERIVE (0x3E1)
CKM_TLS_MAC (0x3E4)
CKM_TLS_KDF (0x3E5)
CKM_TLS_TREE_GOSTR3411_2012_256 (0xD4321047)
CKM_EXTRACT_KEY_FROM_KEY (0x365)
CKM_SHA_1 (0x220)
CKM_MD5 (0x210)
>>> quit()
$
Теперь, когда мы убедились, что токен поддерживает российскую криптографию, можно приступить к созданию ключевой пары на токене и использовать ее закрытый ключ для подписания различных документов.
Напомним, что закрытый и открытый ключи это не только их значения (для открытого ключа ГОСТ Р 34.10-2012-256 это 512 бит, а для открытого ключа ГОСТ Р 34.10-2012-512 это 1024 бита), но и параметры схемы цифровой подписи (п. 5.2 ГОСТ Р 34.10-2012). В дальнейшем параметры схемы цифровой подписи для простоты будем называть параметрами (криптопараметрами) ключевой пары.
Криптопараметры при генерации ключевой пары задаются OID-ами. В настоящее время TK-26 определил следующие oid-ы для криптопараметров алгоритма подписи ГОСТ Р 34.10-2012 с ключом 256:
- 1.2.643.7.1.2.1.1.1 (id-tc26-gost-3410-12-256-paramSetA);
- 1.2.643.7.1.2.1.1.2 (id-tc26-gost-3410-12-256-paramSetB;
- 1.2.643.7.1.2.1.1.3 (id-tc26-gost-3410-12-256-paramSetC);
- 1.2.643.7.1.2.1.1.4 (id-tc26-gost-3410-12-256-paramSetD).
При этом продолжают действовать так называемые OID-ы параметров от КриптоПро:
- 1.2.643.2.2.35.1 (id-GostR3410-2001-CryptoPro-A-ParamSet);
- 1.2.643.2.2.35.2 (d-GostR3410-2001-CryptoPro-B-ParamSet);
- 1.2.643.2.2.35.3 (id-GostR3410-2001-CryptoPro-C-ParamSet);
- 1.2.643.2.2.36.0 (id-GostR3410-2001-CryptoPro-XchA-Param)Set;
- 1.2.643.2.2.36.1 (id-GostR3410-2001-CryptoPro-XchB-Param)Set.
Кто-то может сказать, а что это за каша такая? Но если смотреть по сути, то окажется, что параметры КриптоПро с OID-ами 1.2.643.2.2.35.1, 1.2.643.2.2.35.2, 1.2.643.2.2.35.3 соответствуют параметрам ТК-26 с OID-ами 1.2.643.7.1.2.1.1.1, 1.2.643.7.1.2.1.1.2, 1.2.643.7.1.2.1.1.3 соответственно. Далее ещё интереснее. Параметр КриптоПро id-GostR3410-2001-CryptoPro-XchA-Param соответствует параметру id-GostR3410-2001-CryptoPro-A-ParamSet, а параметр id-GostR3410-2001-CryptoPro-XchB-Param — параметру id-GostR3410-2001-CryptoPro-C-ParamSet того же КриптоПро. Если не запутались, то идём дальше.
С криптопараметрам для алгоритма подписи ГОСТ Р 34.10-2012 с ключом 512 проще:
- 1.2.643.7.1.2.1.2.1 (id-tc26-gost-3410-2012-512-paramSetA);
- 1.2.643.7.1.2.1.2.2 (id-tc26-gost-3410-2012-512-paramSetB);
- 1.2.643.7.1.2.1.2.3 (id-tc26-gost-3410-2012-512-paramSetC);
Для генерации ключевой пары используется следующая команда:
<идентификатор словаря> = pyp11.keypair(<идентификатор библиотеки>, <номер слота с токеном>, <тип ключевой пары>, <OID криптопараметра>, <метка/CKA_LABEL>)
Единственное, с чем мы не сталкивались, это <тип ключевой пары>:
<тип ключевой пары> := 'g12_256' | 'g12_512'
Таким образом, если мы хотим получить пару по алгоритму подписи ГОСТ Р 34.10-2012 с ключом 512, то задаем тип 'g12_512', например:
genkey = pyp11.keypair(libid, slotid, 'g12_512', '1.2.643.7.1.2.1.2.2', 'KeyGost512')
Для алгоритма подписи ГОСТ Р 34.10-2012 с ключом 256 генерация может выглядеть так:
genkey256 = pyp11.keypair(libid, slotid, 'g12_256', '1.2.643.7.1.2.1.1.3', 'KeyGost256')
Перед генерацией ключевой пары необходимо обязательно залогиниться на токене:
pyp11.login(<идентификатор библиотеки>, <номер слота>, 'USER-PIN')
После выполнения требуемой операции целесообразно выполнить logout:
pyp11.logout(<идентификатор библиотеки>, <номер слота>)
При успешной генерации ключевой пары возвращается ассоциированный список (словарь), например:
>>> pyp11.login(libid, slotid, '01234567')
1
>>> genkey256 = pyp11.keypair(libid, slotid, 'g12_256', '1.2.643.7.1.2.1.1.3', 'KeyGost256')
>>> print (genkey256.keys())
dict_keys(['pkcs11_handle', 'pkcs11_slotid', 'hobj_pubkey', 'hobj_privkey', 'pkcs11_id', 'pkcs11_label', 'pubkey', 'pubkey_algo', 'pubkeyinfo', 'type'])
>>> pyp11.logout(libid, slotid)
1
>>>
Среди возвращаемых значений находятся указатели на открытый ('hobj_pubkey') и закрытый ключи ('hobj_privkey'). Последний мы будем использовать при подписании. Среди возвращаемых значений находится и CKA_ID открытого и закрытого ключей ('pkcs11_id'). Элемент pkcs11_id также может использоваться при подписании для поиска закрытого ключа. Напомним, CKA_ID это значение хэша SHA-1 от значения открытого ключа, которое находится в элементе 'pubkey'. При генерации ключевой пары CKA_ID автоматически выставляется для закрытого и открытого ключей. Именно по ним, как правило, ищут соответствие между ключами. Можно распечатать все возвращаемые значения:
>>> for key in genkey256.keys():
... print (key + '= ' + str(genkey256.get(key)))
...
pkcs11_handle= pkcs0
pkcs11_slotid= 0
hobj_pubkey= hobj0100000000000000
hobj_privkey= hobj0200000000000000
pkcs11_id= dd22fe35aeb7eb2ebcad7199b117eb3a7b5f5813
pkcs11_label= KeyGost256
pubkey= 4c2ed60bc5771b2a6616af58c8dd202b9463dde9bd1de028335e718634761e360a25b2f337c2e67c28402cd49fff4f708130a80dc479301b21ceb9324c47464b
pubkey_algo= 1 2 643 7 1 1 1 1
pubkeyinfo= 302106082a85030701010101301506092a850307010201010306082a8503070101020203430004404c2ed60bc5771b2a6616af58c8dd202b9463dde9bd1de028335e718634761e360a25b2f337c2e67c28402cd49fff4f708130a80dc479301b21ceb9324c47464b
type= pkcs11
>>>
Для формирования электронной подписи и её проверки сохраним следующие значения:
>>> hprivkey = genkey256.get("hobj_privkey")
>>> pkcs11_id = genkey256.get("pkcs11_id")
>>> pubkeyinfo = genkey256.get("pubkeyinfo")
>>>
Электронная подпись (ЭП) документа представляет собой подписанный хэш от этого документа.
Поэтому сначала считается соответствующий хэш:
<переменная для хранения хэш > = pyp11.digest(<идентификатор библиотеки>, <слот токена>, 'stribog256' | 'stribog512', <документ>)
Значение хэш всегда возвращается в шестнадцатеричном виде.
Итак, если мы хотим получить подпись по алгоритму ГОСТ Р 34.10-2012 с ключом 256 бит, то нам сначала надо посчитать хэш по алгоритму хэширования ГОСТ Р 34.11-2012 с длиной 256 бит, а затем подписать полученный хэш с использованием механизма 'CKM_GOSTR3410':
<переменная для ЭП> = pyp11.sign(<идентификатор библиотеки>, <слот токена>, 'CKM_GOSTR3410' | 'CKM_GOSTR3410_512', <хэш документа>, <hobj_privkey|pkcs11_id>)
Хэш документа должен быть в шестнадцатеричном виде. Электронная подпись также возвращается в шестнадцатеричном виде.
Ниже приведем пример кода формирования ЭП:
bash-4.4$ python3
Python 3.7.9 (default, Feb 1 2021, 16:55:33)
[GCC 8.4.0] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> import pyp11
>>> lib = '/usr/local/lib64/libls11sw2016.so'>>> libid = pyp11.loadmodule(lib)>>> slotid = 0
>>> #Незабывайте лигиниться на токене!!!
>>> pyp11.login(libid, slotid, '01234567')
1
>>> genkey256 = pyp11.keypair(libid, slotid, 'g12_256', '1.2.643.7.1.2.1.1.3', 'KeyGost256')
>>> hprivkey = genkey256.get("hobj_privkey")
>>> pkcs11_id = genkey256.get("pkcs11_id")
>>> pubkeyinfo = genkey256.get("pubkeyinfo")
>>> hashdoc_hex = pyp11.digest(libid, slotid, 'stribog256', 'Подписываемый документ')
>>> #Для ЭП используется hobj_privkey
>>> sign1 = pyp11.sign(libid, slotid, 'CKM_GOSTR3410', hashdoc_hex, hprivkey)
>>> #Для ЭП используется pkcs11_id (CKA_ID)
>>> sign2 = pyp11.sign(libid, slotid, 'CKM_GOSTR3410', hashdoc_hex, pkcs11_id)
>>> print ('SIGN1=' + sign1 )
SIGN1=5b3f881153f50d9a8da6bb37bb83f54fe997d074672c29c2c0aeb22739a14f1e776b8427e262b098c75abe3a4faffe383d3e2cc406afa09efb3e783919b4ca11
>>> print ('SIGN2=' + sign2)
SIGN2=441a29206a3622a9c76282b71b4fcdbf4c15034d0f0be7b1f711c6d5eef8162a2a2876a5d375cb56e23fc76173cacf88b620fd793cf756589a76cbee6b1fd27a
>>> pyp11.logout(libid, slotid)
1
>>>
Для проверки подписи используется asn1-структура открытого ключа subjectPublicKeyInfo, которую мы сохранили после генерации ключевой пары в переменной pubkeyinfo:
pubkeyinfo = genkey256.get("pubkeyinfo")
Для проверки подписи используется следующая команда:
pyp11.sign(<идентификатор библиотеки>, <слот токена>, <хэш документа в hex>, <подпись документа в hex>, <asn1-subjectPublicKeyInfo>)
Команда возвращает 1 (единицу), если подпись прошла проверку, и 0 (ноль), если проверка не прошла.
Продолжим наш пример проверкой двух полученных подписей:
>>> verify1 = pyp11.verify(libid, slotid, hashdoc_hex, sign1, pubkeyinfo)
>>> print (verify1)
1
>>> verify2 = pyp11.verify(libid, slotid, hashdoc_hex, sign2, pubkeyinfo)
>>> print (verify2)
1
>>>
Как видим обе полученные подписи корректны.
V. Проверка электронной подписи сертификата
Используя полученные знания, напишем пример проверки электронной подписи сертификата:
#!/usr/bin/python3
#-*- coding: utf-8 -*-
import pyp11
print('Проверка подписи сертификата')
#Библиотека для токена
lib = '/usr/local/lib64/libls11sw2016.so'
aa = pyp11.loadmodule(lib)
print (aa)
#Файл с корневым сертификатом в DER-кодировке
fileCA = "CA_12_512.der"
#Файл с сертификатом пользователя в DER-кодировке
fileUser = "habrCA_12_512.der"
#Читаем корневой сертификат в DER-кодировке из файла
with open(fileCA, "rb") as f:
certCA = f.read()
#Упаковываем der в hex
certCA_hex = bytes(certCA).hex()
#Читаем сертификат пользователя в DER-кодировке из файла
with open(fileUser, "rb") as f:
certHabr = f.read()
#Упаковываем der в hex
certHabr_hex = bytes(certHabr).hex()
print ('Разбираем корневой сертификат')
parseCA = pyp11.parsecert (aa, 0, certCA_hex)
print ('Разбираем сертификат пользователя')
parseHabre = pyp11.parsecert (certHabr_hex)
print (parseHabre.keys())
#Проверяем, что издатель сертификата совпадает с владельцем корневого сертификата
if (parseCA.get('subject') != parseHabre.get('issuer')):
print ('Сертификат выдан на другом УЦ')
quit()
print ('Сертификат выдан на данном УЦ')
#Переводим tbsCertificate пользователь в binary
tbs_hex = parseHabre.get('tbsCertificate')
tbsHabrDer = bytes(bytearray.fromhex(tbs_hex))
#tbsHabrDer = '1111'
#Получаем хэш для tbs-сертификата
hashTbs_hex = pyp11.digest(aa, 0, "stribog512", tbsHabrDer)
#hashTbs_hex = pyp11.digest(aa, 0, "stribog256", tbsHabrDer)
verify = pyp11.verify(aa, 0, hashTbs_hex, parseHabre.get('signature'), parseCA.get('pubkeyinfo'))
#verify = pyp11.verify(aa, 0, hashTbs_hex, parseHabre.get('signature'), parseHabre.get('pubkeyinfo'))
print (verify)
if (verify != 1):
print ('Подпись сертификата не прошла проверку')
quit()
print ('Подпись сертификата прошла проверку')
quit()
VI. Работа с объектами токена
Основными объектами, с которыми приходится иметь дело, работая с токенами PKCS#11, являются сертификаты и ключи. И те и другие имеют атрибуты. Нас в первую очередь интересуют атрибуты CKA_LABEL или метка объекта и СКА_ID или идентификатор объекта. Именно атрибут CKA_ID используется для доступа и к сертификатам и ключам.
Уже имея в своем распоряжении рассмотренные выше команды модуля pyp11, можно создать ключевую пару и сформировать подписанный запрос на сертификат. Отправить полученный запрос в удостоверяющий центр и получить там сертификат. Но получив сертификат, возникает вопрос как его поставить на токен и привязать к ключевой паре? Именно эту задачу решает команда pyp11.importcert:
<переменная для CKA_ID> = pyp11.importcert(<идентификатор библиотеки>, <слот токена>, <сертификат в DER-формате и hex-кодировке>, <метка CKA_LABEL>)
Как работает команда? Первым делом она вычисляет по открытому ключу сертификата идентификатор CKA_ID. Именно этот идентификатор будет возвращен в hex-кодировке после успешного размещения сертификата на токене. После установки сертификата на токен в DER-формате, устанавливаются его атрибуты CKA_ID и CKA_LABEL.
Если вам необходимо связать тройку <сертификат> x <открытый ключ> x <закрытый ключ> не только по CKA_ID, но и по метке CKA_LABEL, то необходимо установить метку у ключевой пары аналогичную метке сертификата. Для этого используется команда rename:
pyp11.rename(<идентификатор библиотеки>, <слот токена>, <тип объекта>, <ассоциированный список>)
В <типе объекта> указывается, к каким типам объектов будет применяться команда: 'cert' | 'key' | 'all' (сертификаты, ключевая пара, к тому и другому).
Команда rename позволяет менять не только CKA_LABEL, но и CKA_ID. Конкретные объекты могут задаваться идентификаторами объектов CKA_ID (pkcs11_id), например:
#Импортируем сертификат и получаем его CKA_ID
labcert = 'LabelNEW'
ckaid = pyp11.importcert(aa, 0, cert_der_hex, labcert)
#Устанавливаем метку сертификата и для ключей
#Готовим словарь
ldict = dict(pkcs11_id=ckaid, pkcs11_label=labcert)
#Меняем метки у ключей
pyp11.rename(aa, 0, 'key', ldict)
Аналогичным образом меняется атрибут CKA_ID. В этом случае в словарь вместо метки указывается новый CKA_ID:
ldict = dict(pkcs11_id=ckaid, pkcs11_id_new=11111)
Аналогичным образом можно удалить объекты:
pyp11.delete(<идентификатор библиотеки>, <слот токена>, <тип объекта>, <ассоциированный список>)
При уничтожении в словарь попадает только один элемент, который будет указывать на удаляемые объекты. Это либо CKA_ID (ключ pkcs11_id) либо непосредственно handle-объекта (как правило, его можно получить по команде pyp11.listobjects, ключ pkcs11_handle):
ldict = dict(pkcs11_id=ckaid)
#Или с handle-объекта:
#ldict = dict(hobj=pkcs11_handle)
#Уничтожить личный сертификат с ключами
pyp11.login(aa. 0, '01234567')
pyp11.delete(aa, 0, 'all', ldict)
pyp11.logout(aa, 0)
Упомянем еще об одной очень редко используемой команде. Это команда закрытия сессий на токене:
pyp11.closesession(<идентификатор библиотеки>)
Эту команду следует вызывать, когда возникнет ошибка «PKCS11_ERROR SESSION_HANDLE_INVALID», а затем повторить команду, на которой возникла ошибка. Эта ошибка может возникнуть при кратковременном извлечении токена из компьютера при работе вашей программы.
И завершим мы рассмотрение командой pyp11.listcertsder:
<список сертификатов> = pyp11.listcerts(<идентификатор библиотеки>, <слот токена>)
Вот пример кода:
#!/usr/bin/python3
#-*- coding: utf-8 -*-
import sys
import time
import pyp11
print('Список сертификатов токена')
aa = pyp11.loadmodule('/usr/local/lib64/libls11sw2016.so')
lcerts = pyp11.listcerts(aa, 0)
if (len(lcerts) == 0):
print ('На токене нет сертификатов')
quit()
#Перебираем сертификаты
for cert in lcerts:
#Информация о сертификате
for key in cert:
print (key + ': ' + cert[key])
#Сравним с pyp11.listobjects
lm = pyp11.listobjects(aa, 0, 'cert', 'value')
print('Работа с listobjects:')
for obj in lm:
for key in obj:
print (key + ': ' + obj[key])
quit()
Команды pyp11.listobjects для сертификатов и команда pyp11.listcerts фактически дублируют друг друга, но так сложилось исторически.
Заключение
Опыт использования аналогичного модуля tclpkcs11 показывает, что функциональности, заложенной в модуль pyp11 для Python, с лихвой хватит для его использования в ИОК для работы с электронной подписью на базе российской криптографии. Более того, во второй части статьи будет рассмотрен класс token, в рамках которого будут создаваться объекты для подключенных токенов. И это позволит ещё больше упростить работу с токенами. Кстати, аналогичный класс для tclpkcs11 уже имеется.
Но в заключении я хотел бы вернуться к началу статьи, а именно к проекту PyKCS11.
Когда я писал письмо авторам проекта PyKCS11, то я уже добавил в него поддержку российской криптографии и сообщал им об этом:
Сейчас заканчивается тестирования PyKCS11 для российской криптографии. Кстати, модуль pyp11 хорошо дополняет PyPCS11. Поэтому должна появиться и третья часть статьи, в которой будет рассказано, как добавить поддержку российской криптографии в проект PyKCS11.
===========
Источник:
habr.com
===========
Похожие новости:
- [Игры и игровые приставки, IT-компании] Втянутая в юридическую борьбу Apple и Epic компания Valve пытается выбраться из конфликта
- [Open source, Разработка под Android] Петербургский разработчик сделал неофициальный Android-клиент Clubhouse
- [IT-инфраструктура, Облачные сервисы, IT-компании] Зарубежные компании в 2020 году начали активнее закупать облачные мощности в РФ
- [Программирование, .NET, ASP, C#] Реализуем глобальную обработку исключений в ASP.NET Core приложении (перевод)
- [Программирование, Разработка под Android, GitHub, Социальные сети и сообщества] Российский разработчик написал клиент Clubhouse для Android после реверс-инжиниринга API
- [JavaScript, Программирование] Поддержка JavaScript-приложений в долгосрочной перспективе (перевод)
- [.NET] Как изменить формат данных JSON на Snake Case в ASP.NET Core Web API
- [Информационная безопасность] Как подружить «современный» TLS и «устаревшие» браузеры?
- [Информационная безопасность] Проблемы безопасности онлайн банков
- [Веб-дизайн, Разработка веб-сайтов, ReactJS, Интервью] Юзкейс нечаянно нагрянет, когда его совсем не ждешь
Теги для поиска: #_informatsionnaja_bezopasnost (Информационная безопасность), #_kriptografija (Криптография), #_python, #_graficheskie_obolochki (Графические оболочки), #_c, #_pkcs11, #_x509_v3, #_gost_r_34.102001 (гост р 34.10-2001), #_gost_r_34.112012 (гост р 34.11-2012), #_python, #_elektronnaja_podpis (электронная подпись), #_certificate, #_informatsionnaja_bezopasnost (
Информационная безопасность
), #_kriptografija (
Криптография
), #_python, #_graficheskie_obolochki (
Графические оболочки
), #_c
Вы не можете начинать темы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы
Текущее время: 22-Ноя 15:56
Часовой пояс: UTC + 5
Автор | Сообщение |
---|---|
news_bot ®
Стаж: 6 лет 9 месяцев |
|
Поддержка криптографических токенов PKCS#11 с российской криптографией в скриптовых языках (Python, Tcl) давно находится в моём поле зрения. Это, прежде всего, пакет TclPKCS11 и реализованная на его базе кроссплатформенная утилита cryptoarmpkcs. Утилита cryptoarmpkcs написана на tcl/tk и функционирует на различных платформах, включая Android. Пакет TclPKCS11 версии 1.0.0 заточен на работу именно с токенами, поддерживающими ГОСТ Р 34.11-2012 и ГОСТ Р 34.10-2012. Он позволяет генерировать ключевые пары по ГОСТ Р 34.10-2012 с длиной закрытого ключа 256 и 512 бит, формировать и проверять электронную подпись. Все это можно наглядно видеть в утилите cryptoarmpkcs, в которой в качестве криптодвижка используется именно этот пакет: Первым желанием было портировать этот модуль в среду Python. Но прежде чем это сделать, я посмотрел, что уже есть для работы с криптографическим токенами PKCS#11 в Python. Вне конкуренции, на мой взгляд, здесь проект PyKCS11. Изучив его внимательно, я понял, что не составит труда добавить в него поддержку новой российской криптографии: ГОСТ Р 34.10-2012 (электронная подпись), ГОСТ Р 34.11-2012 (хэширование), ГОСТ Р 34.12-2015 и ГОСТ Р 34.13-2015 (алгоритмы шифрования Кузнечик и Магма). Я написал письмо авторам с предложением добавить российские алгоритмы, предлагая свою помощь. К сожалению, ответ меня несколько обескуражил: И тогда я решил вернуться к этой теме немного позже, а сейчас всё внимание сосредоточил на портировании проект TckPKCS11-1.0.1 в Python. Почему всё же проект TclPKCS11? Да всё очень просто. Основная задача, которую необходимо решить на Python, связана с электронной подписью по ГОСТ Р 34.10-2012 и использование шифрования на данном этапе не предполагается. В этом контексте проект TclPKCS11 абсолютно подходит. В нём реализована поддержка следующих криптографических функций: — генерация ключевых пар по ГОСТ Р 34.10-2012 (512 и 1024 бита для открытого ключа), и даже по ГОСТ Р 34.10-2001; — подсчет хэша по ГОСТ Р 34.10-2012 (256 и 512 бит), а также по ГОСТ Р 34.11-94 и SHA1; — подписание и проверка подписи. Из общих функций реализованы: — управление токенами (инициализация токена, установка и смена PIN-кодов); — получения списка слотов и информации о них; — импорт сертификатов и ключей (только для ГОСТ-криптографии): — установка меток для объектов (сертификаты, ключи); — и другие. Самое главное то, что использование этих механизмов намного проще, чем использование стандартного интерфейса PKCS#11, а следовательно и проще использования пакета PyKCS11. Всё это будет видно на примерах. I. Портирование кода модуля tclpkcs11 в модуль pyp11 для Python Портирование заключается в адаптации кода модуля tclpkcs11 к требованиям со стороны Python. Все изменения в проекте будут касаться только модуля tclpkcs11.c. Поэтому, первое, что мы сделаем, скопируем модуль tclpkcs11.c в файл pythonpkcs11.c и в дальнейшем будем работать именно с ним. Модуль для Python назовем pyp11. Использовать для его создания будем C API Python. Почему-то этот способ многие (но не я) считают самым трудным, но зато он самый эффективный. Анализ C API для Tcl и C API для Python показал их значительное сходство, что и позволило очень быстро провести портирование. Отметим основные этапы портирования, которые вполне возможно кому-то помогут перенести те или иные модули (библиотеки) из Tcl в Python или наоборот. Первое, в файле pythonpkcs11.c заменяем все объявления Tcl_Obj на PyObject, что вполне естественно: Tcl работает со своими объектами, а Python со своими. Второе касается передачи параметров. В общем виде объявление функции, реализующей ту или иную команду Tcl, в С-коде выглядит следующим образом (применительно к нашему коду): name_proc_tcl (CliendData cd, Tcl_Interp *interp, int objc, Tcl_Obj[] *objv[] ){
. . . }; В Python аналогичный заголовок функции будет выглядеть так: name_proc_py (PyObject *self, PyObject *args){
. . . }; В C-коде для tcl проверка количества входных параметров проводится с использованием переменной objc. name_proc_tcl (CliendData cd, Tcl_Interp *interp, int objc, Tcl_Obj[] *objv[] ){
if (objc != 4) { . . . Tcl_SetObjResult(interp, Tcl_NewStringObj("wrong # args: should be "pki::pkcs11::login handle slot password"", -1)); return(TCL_ERROR); } . . . }; В Python параметры передаются в виде кортежа. Поэтому число переданных параметров вычисляется функцией PyTuple_Size(args): name_proc_py (PyObject *self, PyObject *args){
//Вводим переменную для числа параметров int objc; objc = PyTuple_Size(args); . . . if (objc != 3) { PyErr_SetString(PyExc_TypeError, "pyp11_login args error (count args != 3)"); return NULL; } . . . }; Отметим, что число параметров в коде для Tcl на единицу больше, т.к. в objv[0] хранится имя функции (аналогично функции main в C). В приведенном коде наглядно видно как обрабатываются ошибки в Tcl и Python. Вызов прерывания в случае ошибки для Tcl выполняется оператором return (TCL_ERROR); Текстовое сообщение об ошибке формируется оператором TclSetObjResult. Для Python будут использоваться операторы return NULL и PyErr_SetString. Теперь самое главное — разбор параметров. В Tcl каждый параметр передается как отдельный Tcl-объект, а в Python — как кортеж параметров в виде Python-объектов. Поэтому, если мы хотим вносить минимальные изменения в код, целесообразно сначала распаковать кортеж по отдельным объектам, например (применительно к функции pyp11_login): …
char *tcl_handle; long slotid_long; char *password; //Массив PyObject-ов для входных параметров PyObject *argspy[3]; //Растаскиваем входные параметры/объекты ("OOO" - три объекта) по своим ячейкам PyArg_ParseTuple(args, "OOO", &argspy[0], &argspy[1], &argspy[2]) … Полученные объекты распаковываем с их функциональным назначением: …
//Получаем строку (s) с handle библиотеки PKCS11 PyArg_Parse(argspy[0], "s", &tcl_handle); //Получаем номер слота (l), в котором находится токен PyArg_Parse(argspy[1], "l", &slotid_long); //Получаем строку (s) с PIN-кодом владельца PyArg_Parse(argspy[2], "s", &password); ... Сразу оговоримся, что в C API Python имеется функция, которая позволяет сразу разбирать кортеж параметров. В этом случае можно обойтись одним оператором: PyArg_ParseTuple(args, «sls», &tcl_handle, &slotid_long, &password);
Как ни парадоксально, это практически все рекомендации. Осталось последнее, — возвращаемые значения. Результаты выполнения команд возвращаются либо в виде строки, либо в виде списка, либо в виде словаря. Приведём некоторые соответствия. Так для создания списка в коде для Tcl используется функция Tcl_NewObj(), а в коде для Python используется функция PyListNew(0). Для добавления элемента в список для Tcl используется функция TclListObjAppendElement, а для Python — функция PyList_Append. Все эти соответствия можно найти, сравнив код TclPKCS11 и код pyp11 (ССЫЛКА). Также вместо используемых функций ckalloc и ckfree в tclpkcs11.c для Tcl, в модуле pythonpkcs11.c используются стандартные функции работы с памятью — malloc и free. После проведенного анализа модификация кода вместе с тестированием заняла пару рабочих дней. II. Сборка и установка модуля pyp11 Итак, скачиваем архив и распаковываем его. Заходим в папку PythonPKCS11 и выполняем команду установки: #python3 setup.py install
Лично я тестировал на платформах Windows, Linux, OS X. Отметим, что пакет TclPKCS11 успешно работает и на платформе Android. После установки модуля переходим в папку tests и начинаем тестирование. Pаботоспособность модуля pyp11 можно проверить даже без токена. В составе модуля есть функция pyp11.dgst, которая не привязана к токенам и позволяет посчитать хэш по ГОСТ Р 34.10-2012: bash-4.4$ python3
Python 3.7.9 (default, Feb 1 2021, 16:55:33) [GCC 8.4.0] on linux Type "help", "copyright", "credits" or "license" for more information. >>> import pyp11 #Считаем хэш по ГОСТ Р 34.11-2012-256 (stribog256) >>> hash256 = pyp11.dgst("stribog256", "Текст для хэширования") #Считаем хэш по ГОСТ Р 34.11-2012-512 (stribog512) >>> hash512 = pyp11.dgst("stribog512", "Текст для хэширования") >>> print("STRIBOG256=" + hash256) STRIBOG256=26b8865c37831aa254706e6c3514fb23f386358e9dd858703a24d4825d2c4794 >>> print("STRIBOG512=" + hash512) STRIBOG512=e92ff2063c586ec6e9c9569dad7dd503de1c88faafc8b1bf43909bfa36db92ccbf3823f0b8f5d877f10933ed7e670081018dac0929d17729422f05ce1f4c4f25 >>> quit() bash-4.4$ Значение хэш возвращается в шестнадцатеричном виде. Для перевода хэш-а в бинарный вид можно воспользоваться следующей функцией: >>> hash256_bin = bytes(bytearray.fromhex(hash256))
Напомним, как перевести бинарный код в шестнадцатеричный: >>> hash256 = bytes(hash256_bin).hex()
>>> print("STRIBOG256_NEW=" + hash256) STRIBOG256_NEW=26b8865c37831aa254706e6c3514fb23f386358e9dd858703a24d4825d2c4794 >>> Есть еще одна функция, которая также может работать без токена. Это функция parsecert. На вход этой функции подается сертификат в DER-формате, упакованный в шестнадцатеричную кодировку: bash-4.4$ python3
Python 3.7.9 (default, Feb 1 2021, 16:55:33) [GCC 8.4.0] on linux Type "help", "copyright", "credits" or "license" for more information. >>> import pyp11 >>> #Читаем серификат в DER-кодировке из файла >>> with open("cert_256.der", "rb") as f: ... cert_der = f.read() ... >>> #Упаковываем сертификат der в hex >>> cert_der_hex = bytes(cert_der).hex() >>> #Распарсиваем сертификат >>> pubk = pyp11.parsecert(cert_der_hex) >>> Результатом выполнения команды pyp11.parsecert является словарь (ассоциированный список): >>>print (pubk.keys())
dict_keys(['pkcs11_id', 'pubkeyinfo', 'pubkey', 'subject', 'issuer', 'serial_number', 'tbsCertificate', 'signature_algo', 'signature']) >>> В этом словаре находятся as1-структуры элементов сертификата. Все элементы закодированы в шестнадцатеричный формат. Среди элементов находится элемент pubkeyinfo со значением asn1-структуры subjectpublickeyinfo, элемент pubkey со значением публичного ключа, серийный номер сертификата, tbs-сертификат, который будет использоваться для проверки подписи сертификата, алгоритм подписи сертификата и значение самой подписи, а также элементы с информацией о владельце и издателе сертификата, полученные из сертификата и закодированные в шестнадцатеричное представление: >>> subject = pubk['subject']
>>> print ('SUBJECT=' + subject) SUBJECT=30820205310b3009060355040613025255312a3028060355042a0c21d09fd0b0d0b2d0b5d0bb20d090d0bdd0b0d182d0bed0bbd18cd0b5d0b2d0b8d1873135303306035504030c2cd09ed09ed09e20d09ad09ed09cd09fd090d09dd098d0af20d0add09ad09e2dd0a1d0a2d0a0d09ed099203937311d301b06092a864886f70d010901160e696e666f4072746564632e6f72673118301606052a85036401120d313137373734363733343433393116301406052a85036403120b3133383632313537373734311a301806082a85030381030101120c3030393732393131303536393130302e060355040c0c27d093d0b5d0bdd0b5d180d0b0d0bbd18cd0bdd18bd0b920d0b4d0b8d180d0b5d0bad182d0bed180310a3008060355040b0c013031353033060355040a0c2cd09ed09ed09e20d09ad09ed09cd09fd090d09dd098d0af20d0add09ad09e2dd0a1d0a2d0a0d09ed099203937315f305d06035504090c5631313931333620d0b32e20d09cd0bed181d0bad0b2d0b020d0bfd1802dd0b420312dd0b920d0a1d0b5d182d183d0bdd18cd181d0bad0b8d0b920d0b42e203130d09020d181d182d1802e203120d0bfd0bed0bc2e20323115301306035504070c0cd09cd0bed181d0bad0b2d0b0311c301a06035504080c13373720d0b32e20d09cd0bed181d0bad0b2d0b0311b301906035504040c12d0a5d0b0d180d0b8d182d0bed0bdd0bed0b2 >>> Элемент pkcs11_id берётся не из сертификата, а рассчитывается как значение хэш по SHA-1 от значения публичного ключа. При использовании функции pyp11.parsecert в данном контексте (без подключенного токена) pkcs11_id будет равен -1: >>> pkcs11_id = pubk['pkcs11_id']
>>> print ('PKCS11_ID=' + pkcs11_id) PKCS11_ID=-1 >>> Кто-то может сказать, а что, разве в Python нет средств разбора сертификатов? А как же, например, asn1crypto? Ответ заключается в том, что в этих средствах не учтены особенности российской криптографии. И вот, чтобы получить максимальную самодостаточность пакета pyp11, в него помимо функций, связанных с генерацией ключевой пары, формирования и проверки подписи, включены дополнительные функции. Например, asn1-структура pubkeyinfo необходима при проверке электронной подписи. И именно поэтому и была включена функция parsecert для частичного разбора сертификата x509.v3 и получения, в частности, asn1-структуры subjectpublickeyinfo (pubkeyinfo). В папке tests проекта в файлах test0_* находятся соответствующие тесты. ############УБРАТЬ про FSB795 ################################ Отметим также, что для разбора сертификатов с российской криптографией можно воспользоваться пакетом fsb795: >>> import fsb795
>>> #Парсим наш сертификат с помощью fsb795 >>> mycert = fsb795.Certificate(cert_der) >>> #читаем данные о владельце сертификата и типе владельце >>> dn, type = mycert.subjectCert() >>> #DN - это словарь/ассоциированный список >>> for key in dn.keys(): ... print (key + '=' + dn[key]) ... Country=RU GN=Имя Отчество CN=ООО КОМПАНИЯ E=info@ooo.org OGRN=xxxxxxxxxxxx SNILS=xxxxxxxxxxx INN=xxxxxxxxxxxx title=Генеральный директор OU=0 O=ООО КОМПАНИЯ street=119136 г. Москва L=Москва ST=77 г. Москва SN=Харитонов >>> Теперь можно переходить к работе с токенами. III. Управление токенами PKCS#11 Для тестирования функций управления подойдет любой токен PKCS#11, даже токен без поддержки какой-либо криптографии, например RuTokenLite. Но поскольку мы ведём речь о российской криптографии, то целесообразно сразу иметь токен с поддержкой российской криптографии. Здесь мы имеем в виду ГОСТ Р 34.10-2012 и ГОСТ Р 34.11-2012. Это может быть как аппаратный токен, например RuTokenECP-2.0, так и программные или облачные токены. Установить программный токен или получить доступ к облачному токену можно, воспользовавшись утилитой cryptoarmpkcs. Скачать утилиту cryptoarmpkcs можно здесь.SPL После запуска утилиты необходимо зайти на вкладку «Создать токены»:На вкладке можно найти инструкции для получения токенов. Итак, у нас токен и библиотека для работы с ним. После загрузки модуля pyp11 требуется загрузить библиотеку для работы с нашим токеном. В примерах будут использоваться библиотека librtpkcs11ecp-2.0 для работы с аппаратным токеном, библиотека libls11sw2016 для работы с программным токеном и библиотека libls11cloud.so для работы с облачным токеном. Читатели могут использовать любые токены, даже те, на которых нет российской криптографии, на них тоже можно проверить функции управления. Итак, загружаем библиотеку командой loadmodule: bash-4.4$ python3
Python 3.7.9 (default, Feb 1 2021, 16:55:33) [GCC 8.4.0] on linux Type "help", "copyright", "credits" or "license" for more information. >>> import sys >>> import pyp11 >>> #Выбираем библиотеку pkcs11 >>> lib = "/usr/local/lib64/librtpkcs11ecp_2.0.so" >>> #Обработка ошибки при загрузке библиотеки PKCS#11 >>> try: ... #Вызываем команду загрузки библиотеки и плохим числом параметров ... handlelib = pyp11.loadmodule(lib, 2) ... except: ... print ('Ошибка загрузки библиотеки: ') ... e = sys.exc_info()[1] ... e1 = e.args[0] ... print (e1) ... Ошибка загрузки библиотеки: pyp11_load_module args error (count args != 1) >>> #Загружаем с правильным синтаксисом >>> idlib = pyp11.loadmodule(lib) >>> #Печатаем дескриптор библиотеки >>> print (idlib) pkcs0 >>> Дескриптор загруженной библиотеки используется при её выгрузке: >>> pyp11.unloadmodule(idlib)
Теперь, когда библиотека загружена, можно получить список поддерживаемых её слотов и узнать есть ли в каких слотах токены. Для получения списка слотов с полной информацией о них и содержащихся в них токенах используется команда: >>> slots = pyp11.listslots(idlib)
>>> Команда pyp11.listslots возвращает список, каждый элемент которого содержит информацию о слоте: [<info slot1>, <info slot2>, ... , <info slotN>]
В свою очередь, каждый элемент этого списка также является списком, состоящим из четырех элементов: [<номер слота>, <метка токена, находящегося в слоте>, <флаги слота и токена>, <информация о токене>]
Если слот не содержит токен, то элементы <метка токена ...> и <информация о слоте> содержат пустое значение. Наличие токена в слоте определяется по наличию флага TOKEN_PRESENT в списке <флаги слота и токена>: bash-4.4$ python3
Python 3.7.9 (default, Feb 1 2021, 16:55:33) [GCC 8.4.0] on linux Type "help", "copyright", "credits" or "license" for more information. >>> import sys >>> import pyp11 >>> #Выбираем библиотеку >>> #lib = '/usr/local/lib64/libls11sw2016.so' >>> lib = '/usr/local/lib64/librtpkcs11ecp_2.0.so' >>> #Загружаем библиотеку >>> libid = pyp11.loadmodule(lib) >>> #Дескриптор библиотеки >>> print (libid) pkcs0 >>> #Загружаем список слотов >>> slots = pyp11.listslots(libid) >>> tokpr = 0 >>> #Ищем первый подключенный токен >>> while (tokpr == 0): ... #Перебираем слоты ... for v in slots: ... #Список флагов текущего слота ... flags = v[2] ... #Проверяем наличие в стоке токена ... if (flags.count('TOKEN_PRESENT') !=0): ... tokpr = 1 ... #Избавляемся от лишних пробелов у метки слота ... lab = v[1].strip() ... infotok = v[3] ... slotid = v[0] ... break ... if (tokpr == 0): ... input ('Нет ни одного подключенного токена.\nВставьте токен и нажмите ВВОД') ... slots = pyp11.listslots(libid) ... #Информация о подключенном токене ... Нет ни одного подключенного токена. Вставьте токен и нажмите ВВОД '' >>> #Информация о подключенном токене >>> print ('LAB="' + lab + '", SLOTID=' + str(slotid)) LAB="Rutoken lite <no label>", SLOTID=0 >>> print ('FLAGS:', flags) FLAGS: ['TOKEN_PRESENT', 'RNG', 'LOGIN_REQUIRED', 'SO_PIN_TO_BE_CHANGED', 'REMOVABLE_DEVICE', 'HW_SLOT'] >>> Если взглянуть на флаги (FLAGS:) подключенного токена, то в них отсутствует флаг 'TOKEN_INITIALIZED'. Отсутствие этого флага говорит о том, что токен не инициализирован и требуется его инициализация: #Проверяем, что токен проинициализирован
>>> if (flags.count('TOKEN_INITIALIZED') == 0''): ... #Инициализируем токен ... dd = pyp11.inittoken (libid, 0, '87654321',"TESTPY2") ... >>> Как видим, для инициализации токена используется следующая команда: pyp11.inittoken (<дискриптор библиоткети>, <номер слота>, <SO-PIN>, <метка токена>)
Естественно, токен можно переинициализировать независимо от наличия флага 'TOKEN_INITIALIZED', только надо иметь в виду, что переинициализация токена ведет к уничтожению на нем всех объектов (ключи, сертификаты и т.д). После инициализации токена должен быть проинициализирован USER-PIN. Эту операцию, как правило, делает производитель или продавец токена: pyp11.inituserpin (<дискриптор библиоткети>, <номер слота>, <SO-PIN>, <USER-PIN>)
При этом выставляется флаг 'USER_PIN_TO_BE_CHANGED', который напоминает владельцу токена, что надо бы сменить свой USER-PIN (параметр 'user'): pyp11.setpin (<дискриптор библиоткети>, <номер слота>, <'user'|'so'>,<текущий PIN-код>, <новый PIN-код>)
Сегодня «модно» получать в УЦ токены с закрытыми ключами и предустановленными PIN-кодами и ключевой парой. И, как правило, получателей не предупреждают, что целесообразно PIN-коды поменять, и не говорят, как это сделать. Я бы рекомендовал использовать для этого уже упоминавшуюся утилиту cryptoarmpkcs: В папке tests проекта pyp11 лежат три теста test1_0_inittoken.py, test1_1_inituserpin.py и test1_2_change_userpin, которые наглядно демонстрируют инициализацию токена. Выполнять их надо в порядке перечисления. Было бы несправедливо не показать инициализацию токена с использованием уже упоминавшегося пакета PyKCS11: $ python3
Python 3.7.9 (default, Feb 1 2021, 16:55:33) [GCC 8.4.0] on linux Type "help", "copyright", "credits" or "license" for more information. >>> import PyKCS11 >>> #Библиотека PKCS#11 >>> lib = '/usr/local/lib64/librtpkcs11ecp_2.0.so' >>> pkcs11.load(lib) >>> #Получаем список слотов с токенами >>> slots = pkcs11.getSlotList(tokenPresent=True) >>> #Ищем первый подключенный токен >>> while (len(slots) == 0): ... input ('Нет ни одного подключенного токена.\nВставьте токен и нажмите ВВОД') ... #Получаем список слотов с токенами ... slots = pkcs11.getSlotList(tokenPresent=True) ... Нет ни одного подключенного токена. Вставьте токен и нажмите ВВОД '' >>> >>> #Берём первый подключенный токен >>> slot = slots[0] >>> #Закрываем все сессии на токене >>> #SO-PIN >>> so_pin = '87654321' >>> lab_tok = "myLabel" >>> #Инициализация токена >>> pkcs11.initToken(slot, so_pin, lab_tok) >>> session = pkcs11.openSession(slot, PyKCS11.CKF_SERIAL_SESSION | PyKCS11.CKF_RW_SESSION) >>> #Установка первичного USER-PIN >>> init_pin = '1234' >>> session.login(so_pin, user_type=PyKCS11.CKU_SO) >>> session.initPin(init_pin) >>> session.logout() >>> #Новый USER-PIN >>> user_pin = '01234567' >>> session.login(init_pin) >>> # change PIN >>> session.setPin(init_pin, user_pin) >>> session.logout() >>> quit() $ IV. Ключевая пара, электронная подпись и её проверка Итак, наш токен готов в работе: мы его проинициализировали и установили метку, но самое главное, мы поменяли PIN-коды (USER, SO). Первым делом необходимо убедиться, что наш токен поддерживает необходимые нам криптографические механизмы. Поскольку речь идет о ГОСТ Р 34.10-2012 и ГОСТ Р 34.11-2012, то токен должен поддерживать механизмы CKM_GOST* в соответствии рекомендациями ТК-26. Для получения списка механизмов используется команда pyp11.lictmechs: <список механизмов> = pyp11.listmech(<идентификатор библиотеки>, <номер слота>)
Как ни странно, но токены могут не иметь поддержки криптомеханизмов, например, RuToken Lite. Они нас интересовать не будут. Мы будем использовать только токены с поддержкой ГОСТ Р 34.10-2012 и ГОСТ Р 34.11-2012: $ python3
Python 3.7.9 (default, Feb 1 2021, 16:55:33) [GCC 8.4.0] on linux Type "help", "copyright", "credits" or "license" for more information. >>> import pyp11 >>> #Выбираем библиотеку >>> #Программный токен >>> lib = '/usr/local/lib64/libls11sw2016.so' >>> #Для Windows >>> #lib='C:\Temp\ls11sw2016.dll' >>> #Облачный токен >>> #lib = '/usr/local/lib64/libls11cloud.so' >>> #Аппаратный токен >>> #lib = '/usr/local/lib64/librtpkcs11ecp_2.0.so' >>> #Закгружаем выбранную библиотеку >>> aa = pyp11.loadmodule(lib) >>> listmech = pyp11.listmechs(aa, 0) >>> print ('\tКриптографические механизмы токена') Криптографические механизмы токена >>> for mech in listmech: ... print(mech) ... Криптографические механизмыSPLCKM_GOSTR3410_KEY_PAIR_GEN (0x1200)
CKM_GOSTR3410_512_KEY_PAIR_GEN (0xD4321005) CKM_GOSTR3410 (0x1201) CKM_GOSTR3410_512 (0xD4321006) CKM_GOSTR3410_WITH_GOSTR3411 (0x1202) CKM_GOSTR3410_WITH_GOSTR3411_12_256 (0xD4321008) CKM_GOSTR3410_WITH_GOSTR3411_12_512 (0xD4321009) CKM_GOSTR3410_DERIVE (0x1204) CKM_GOSTR3410_12_DERIVE (0xD4321007) CKM_GOSR3410_2012_VKO_256 (0xD4321045) CKM_GOSR3410_2012_VKO_512 (0xD4321046) CKM_KDF_4357 (0xD4321025) CKM_KDF_GOSTR3411_2012_256 (0xD4321026) CKM_KDF_TREE_GOSTR3411_2012_256 (0xD4321044) CKM_GOSTR3410_KEY_WRAP (0x1203) CKM_GOSTR3410_PUBLIC_KEY_DERIVE (0xD432100A) CKM_LISSI_GOSTR3410_PUBLIC_KEY_DERIVE (0xD4321037) CKM_GOST_GENERIC_SECRET_KEY_GEN (0xD4321049) CKM_GOST_CIPHER_KEY_GEN (0xD4321048) CKM_GOST_CIPHER_ECB (0xD4321050) CKM_GOST_CIPHER_CBC (0xD4321051) CKM_GOST_CIPHER_CTR (0xD4321052) CKM_GOST_CIPHER_OFB (0xD4321053) CKM_GOST_CIPHER_CFB (0xD4321054) CKM_GOST_CIPHER_OMAC (0xD4321055) CKM_GOST_CIPHER_KEY_WRAP (0xD4321059) CKM_GOST_CIPHER_ACPKM_CTR (0xD4321057) CKM_GOST_CIPHER_ACPKM_OMAC (0xD4321058) CKM_GOST28147_KEY_GEN (0x1220) CKM_GOST28147 (0x1222) CKM_GOST28147_KEY_WRAP (0x1224) CKM_GOST28147_PKCS8_KEY_WRAP (0xD4321036) CKM_GOST_CIPHER_PKCS8_KEY_WRAP (0xD432105A) CKM_GOST28147_ECB (0x1221) CKM_GOST28147_CNT (0xD4321825) CKM_GOST28147_MAC (0x1223) CKM_KUZNYECHIK_KEY_GEN (0xD4321019) CKM_KUZNYECHIK_ECB (0xD432101A) CKM_KUZNYECHIK_CBC (0xD432101E) CKM_KUZNYECHIK_CTR (0xD432101B) CKM_KUZNYECHIK_OFB (0xD432101D) CKM_KUZNYECHIK_CFB (0xD432101C) CKM_KUZNYECHIK_OMAC (0xD432101F) CKM_KUZNYECHIK_KEY_WRAP (0xD4321028) CKM_KUZNYECHIK_ACPKM_CTR (0xD4321042) CKM_KUZNYECHIK_ACPKM_OMAC (0xD4321043) CKM_MAGMA_KEY_GEN (0xD432102A) CKM_MAGMA_ECB (0xD4321018) CKM_MAGMA_CBC (0xD4321023) CKM_MAGMA_CTR (0xD4321020) CKM_MAGMA_OFB (0xD4321022) CKM_MAGMA_CFB (0xD4321021) CKM_MAGMA_OMAC (0xD4321024) CKM_MAGMA_KEY_WRAP (0xD4321029) CKM_MAGMA_ACPKM_CTR (0xD4321040) CKM_MAGMA_ACPKM_OMAC (0xD4321041) CKM_GOSTR3411 (0x1210) CKM_GOSTR3411_12_256 (0xD4321012) CKM_GOSTR3411_12_512 (0xD4321013) CKM_GOSTR3411_HMAC (0x1211) CKM_GOSTR3411_12_256_HMAC (0xD4321014) CKM_GOSTR3411_12_512_HMAC (0xD4321015) CKM_PKCS5_PBKD2 (0x3B0) CKM_PBA_GOSTR3411_WITH_GOSTR3411_HMAC (0xD4321035) CKM_TLS_GOST_KEY_AND_MAC_DERIVE (0xD4321033) CKM_TLS_GOST_PRE_MASTER_KEY_GEN (0xD4321031) CKM_TLS_GOST_MASTER_KEY_DERIVE (0xD4321032) CKM_TLS_GOST_PRF (0xD4321030) CKM_TLS_GOST_PRF_2012_256 (0xD4321016) CKM_TLS_GOST_PRF_2012_512 (0xD4321017) CKM_TLS12_MASTER_KEY_DERIVE (0x3E0) CKM_TLS12_KEY_AND_MAC_DERIVE (0x3E1) CKM_TLS_MAC (0x3E4) CKM_TLS_KDF (0x3E5) CKM_TLS_TREE_GOSTR3411_2012_256 (0xD4321047) CKM_EXTRACT_KEY_FROM_KEY (0x365) CKM_SHA_1 (0x220) CKM_MD5 (0x210) >>> quit()
$ Теперь, когда мы убедились, что токен поддерживает российскую криптографию, можно приступить к созданию ключевой пары на токене и использовать ее закрытый ключ для подписания различных документов. Напомним, что закрытый и открытый ключи это не только их значения (для открытого ключа ГОСТ Р 34.10-2012-256 это 512 бит, а для открытого ключа ГОСТ Р 34.10-2012-512 это 1024 бита), но и параметры схемы цифровой подписи (п. 5.2 ГОСТ Р 34.10-2012). В дальнейшем параметры схемы цифровой подписи для простоты будем называть параметрами (криптопараметрами) ключевой пары. Криптопараметры при генерации ключевой пары задаются OID-ами. В настоящее время TK-26 определил следующие oid-ы для криптопараметров алгоритма подписи ГОСТ Р 34.10-2012 с ключом 256:
При этом продолжают действовать так называемые OID-ы параметров от КриптоПро:
Кто-то может сказать, а что это за каша такая? Но если смотреть по сути, то окажется, что параметры КриптоПро с OID-ами 1.2.643.2.2.35.1, 1.2.643.2.2.35.2, 1.2.643.2.2.35.3 соответствуют параметрам ТК-26 с OID-ами 1.2.643.7.1.2.1.1.1, 1.2.643.7.1.2.1.1.2, 1.2.643.7.1.2.1.1.3 соответственно. Далее ещё интереснее. Параметр КриптоПро id-GostR3410-2001-CryptoPro-XchA-Param соответствует параметру id-GostR3410-2001-CryptoPro-A-ParamSet, а параметр id-GostR3410-2001-CryptoPro-XchB-Param — параметру id-GostR3410-2001-CryptoPro-C-ParamSet того же КриптоПро. Если не запутались, то идём дальше. С криптопараметрам для алгоритма подписи ГОСТ Р 34.10-2012 с ключом 512 проще:
Для генерации ключевой пары используется следующая команда: <идентификатор словаря> = pyp11.keypair(<идентификатор библиотеки>, <номер слота с токеном>, <тип ключевой пары>, <OID криптопараметра>, <метка/CKA_LABEL>)
Единственное, с чем мы не сталкивались, это <тип ключевой пары>: <тип ключевой пары> := 'g12_256' | 'g12_512' Таким образом, если мы хотим получить пару по алгоритму подписи ГОСТ Р 34.10-2012 с ключом 512, то задаем тип 'g12_512', например: genkey = pyp11.keypair(libid, slotid, 'g12_512', '1.2.643.7.1.2.1.2.2', 'KeyGost512')
Для алгоритма подписи ГОСТ Р 34.10-2012 с ключом 256 генерация может выглядеть так: genkey256 = pyp11.keypair(libid, slotid, 'g12_256', '1.2.643.7.1.2.1.1.3', 'KeyGost256')
Перед генерацией ключевой пары необходимо обязательно залогиниться на токене: pyp11.login(<идентификатор библиотеки>, <номер слота>, 'USER-PIN')
После выполнения требуемой операции целесообразно выполнить logout: pyp11.logout(<идентификатор библиотеки>, <номер слота>)
При успешной генерации ключевой пары возвращается ассоциированный список (словарь), например: >>> pyp11.login(libid, slotid, '01234567') 1 >>> genkey256 = pyp11.keypair(libid, slotid, 'g12_256', '1.2.643.7.1.2.1.1.3', 'KeyGost256') >>> print (genkey256.keys()) dict_keys(['pkcs11_handle', 'pkcs11_slotid', 'hobj_pubkey', 'hobj_privkey', 'pkcs11_id', 'pkcs11_label', 'pubkey', 'pubkey_algo', 'pubkeyinfo', 'type']) >>> pyp11.logout(libid, slotid) 1 >>> Среди возвращаемых значений находятся указатели на открытый ('hobj_pubkey') и закрытый ключи ('hobj_privkey'). Последний мы будем использовать при подписании. Среди возвращаемых значений находится и CKA_ID открытого и закрытого ключей ('pkcs11_id'). Элемент pkcs11_id также может использоваться при подписании для поиска закрытого ключа. Напомним, CKA_ID это значение хэша SHA-1 от значения открытого ключа, которое находится в элементе 'pubkey'. При генерации ключевой пары CKA_ID автоматически выставляется для закрытого и открытого ключей. Именно по ним, как правило, ищут соответствие между ключами. Можно распечатать все возвращаемые значения: >>> for key in genkey256.keys():
... print (key + '= ' + str(genkey256.get(key))) ... pkcs11_handle= pkcs0 pkcs11_slotid= 0 hobj_pubkey= hobj0100000000000000 hobj_privkey= hobj0200000000000000 pkcs11_id= dd22fe35aeb7eb2ebcad7199b117eb3a7b5f5813 pkcs11_label= KeyGost256 pubkey= 4c2ed60bc5771b2a6616af58c8dd202b9463dde9bd1de028335e718634761e360a25b2f337c2e67c28402cd49fff4f708130a80dc479301b21ceb9324c47464b pubkey_algo= 1 2 643 7 1 1 1 1 pubkeyinfo= 302106082a85030701010101301506092a850307010201010306082a8503070101020203430004404c2ed60bc5771b2a6616af58c8dd202b9463dde9bd1de028335e718634761e360a25b2f337c2e67c28402cd49fff4f708130a80dc479301b21ceb9324c47464b type= pkcs11 >>> Для формирования электронной подписи и её проверки сохраним следующие значения: >>> hprivkey = genkey256.get("hobj_privkey")
>>> pkcs11_id = genkey256.get("pkcs11_id") >>> pubkeyinfo = genkey256.get("pubkeyinfo") >>> Электронная подпись (ЭП) документа представляет собой подписанный хэш от этого документа. Поэтому сначала считается соответствующий хэш: <переменная для хранения хэш > = pyp11.digest(<идентификатор библиотеки>, <слот токена>, 'stribog256' | 'stribog512', <документ>) Значение хэш всегда возвращается в шестнадцатеричном виде. Итак, если мы хотим получить подпись по алгоритму ГОСТ Р 34.10-2012 с ключом 256 бит, то нам сначала надо посчитать хэш по алгоритму хэширования ГОСТ Р 34.11-2012 с длиной 256 бит, а затем подписать полученный хэш с использованием механизма 'CKM_GOSTR3410': <переменная для ЭП> = pyp11.sign(<идентификатор библиотеки>, <слот токена>, 'CKM_GOSTR3410' | 'CKM_GOSTR3410_512', <хэш документа>, <hobj_privkey|pkcs11_id>)
Хэш документа должен быть в шестнадцатеричном виде. Электронная подпись также возвращается в шестнадцатеричном виде. Ниже приведем пример кода формирования ЭП: bash-4.4$ python3
Python 3.7.9 (default, Feb 1 2021, 16:55:33) [GCC 8.4.0] on linux Type "help", "copyright", "credits" or "license" for more information. >>> import pyp11 >>> lib = '/usr/local/lib64/libls11sw2016.so'>>> libid = pyp11.loadmodule(lib)>>> slotid = 0 >>> #Незабывайте лигиниться на токене!!! >>> pyp11.login(libid, slotid, '01234567') 1 >>> genkey256 = pyp11.keypair(libid, slotid, 'g12_256', '1.2.643.7.1.2.1.1.3', 'KeyGost256') >>> hprivkey = genkey256.get("hobj_privkey") >>> pkcs11_id = genkey256.get("pkcs11_id") >>> pubkeyinfo = genkey256.get("pubkeyinfo") >>> hashdoc_hex = pyp11.digest(libid, slotid, 'stribog256', 'Подписываемый документ') >>> #Для ЭП используется hobj_privkey >>> sign1 = pyp11.sign(libid, slotid, 'CKM_GOSTR3410', hashdoc_hex, hprivkey) >>> #Для ЭП используется pkcs11_id (CKA_ID) >>> sign2 = pyp11.sign(libid, slotid, 'CKM_GOSTR3410', hashdoc_hex, pkcs11_id) >>> print ('SIGN1=' + sign1 ) SIGN1=5b3f881153f50d9a8da6bb37bb83f54fe997d074672c29c2c0aeb22739a14f1e776b8427e262b098c75abe3a4faffe383d3e2cc406afa09efb3e783919b4ca11 >>> print ('SIGN2=' + sign2) SIGN2=441a29206a3622a9c76282b71b4fcdbf4c15034d0f0be7b1f711c6d5eef8162a2a2876a5d375cb56e23fc76173cacf88b620fd793cf756589a76cbee6b1fd27a >>> pyp11.logout(libid, slotid) 1 >>> Для проверки подписи используется asn1-структура открытого ключа subjectPublicKeyInfo, которую мы сохранили после генерации ключевой пары в переменной pubkeyinfo: pubkeyinfo = genkey256.get("pubkeyinfo")
Для проверки подписи используется следующая команда: pyp11.sign(<идентификатор библиотеки>, <слот токена>, <хэш документа в hex>, <подпись документа в hex>, <asn1-subjectPublicKeyInfo>)
Команда возвращает 1 (единицу), если подпись прошла проверку, и 0 (ноль), если проверка не прошла. Продолжим наш пример проверкой двух полученных подписей: >>> verify1 = pyp11.verify(libid, slotid, hashdoc_hex, sign1, pubkeyinfo)
>>> print (verify1) 1 >>> verify2 = pyp11.verify(libid, slotid, hashdoc_hex, sign2, pubkeyinfo) >>> print (verify2) 1 >>> Как видим обе полученные подписи корректны. V. Проверка электронной подписи сертификата Используя полученные знания, напишем пример проверки электронной подписи сертификата: #!/usr/bin/python3
#-*- coding: utf-8 -*- import pyp11 print('Проверка подписи сертификата') #Библиотека для токена lib = '/usr/local/lib64/libls11sw2016.so' aa = pyp11.loadmodule(lib) print (aa) #Файл с корневым сертификатом в DER-кодировке fileCA = "CA_12_512.der" #Файл с сертификатом пользователя в DER-кодировке fileUser = "habrCA_12_512.der" #Читаем корневой сертификат в DER-кодировке из файла with open(fileCA, "rb") as f: certCA = f.read() #Упаковываем der в hex certCA_hex = bytes(certCA).hex() #Читаем сертификат пользователя в DER-кодировке из файла with open(fileUser, "rb") as f: certHabr = f.read() #Упаковываем der в hex certHabr_hex = bytes(certHabr).hex() print ('Разбираем корневой сертификат') parseCA = pyp11.parsecert (aa, 0, certCA_hex) print ('Разбираем сертификат пользователя') parseHabre = pyp11.parsecert (certHabr_hex) print (parseHabre.keys()) #Проверяем, что издатель сертификата совпадает с владельцем корневого сертификата if (parseCA.get('subject') != parseHabre.get('issuer')): print ('Сертификат выдан на другом УЦ') quit() print ('Сертификат выдан на данном УЦ') #Переводим tbsCertificate пользователь в binary tbs_hex = parseHabre.get('tbsCertificate') tbsHabrDer = bytes(bytearray.fromhex(tbs_hex)) #tbsHabrDer = '1111' #Получаем хэш для tbs-сертификата hashTbs_hex = pyp11.digest(aa, 0, "stribog512", tbsHabrDer) #hashTbs_hex = pyp11.digest(aa, 0, "stribog256", tbsHabrDer) verify = pyp11.verify(aa, 0, hashTbs_hex, parseHabre.get('signature'), parseCA.get('pubkeyinfo')) #verify = pyp11.verify(aa, 0, hashTbs_hex, parseHabre.get('signature'), parseHabre.get('pubkeyinfo')) print (verify) if (verify != 1): print ('Подпись сертификата не прошла проверку') quit() print ('Подпись сертификата прошла проверку') quit() VI. Работа с объектами токена Основными объектами, с которыми приходится иметь дело, работая с токенами PKCS#11, являются сертификаты и ключи. И те и другие имеют атрибуты. Нас в первую очередь интересуют атрибуты CKA_LABEL или метка объекта и СКА_ID или идентификатор объекта. Именно атрибут CKA_ID используется для доступа и к сертификатам и ключам. Уже имея в своем распоряжении рассмотренные выше команды модуля pyp11, можно создать ключевую пару и сформировать подписанный запрос на сертификат. Отправить полученный запрос в удостоверяющий центр и получить там сертификат. Но получив сертификат, возникает вопрос как его поставить на токен и привязать к ключевой паре? Именно эту задачу решает команда pyp11.importcert: <переменная для CKA_ID> = pyp11.importcert(<идентификатор библиотеки>, <слот токена>, <сертификат в DER-формате и hex-кодировке>, <метка CKA_LABEL>)
Как работает команда? Первым делом она вычисляет по открытому ключу сертификата идентификатор CKA_ID. Именно этот идентификатор будет возвращен в hex-кодировке после успешного размещения сертификата на токене. После установки сертификата на токен в DER-формате, устанавливаются его атрибуты CKA_ID и CKA_LABEL. Если вам необходимо связать тройку <сертификат> x <открытый ключ> x <закрытый ключ> не только по CKA_ID, но и по метке CKA_LABEL, то необходимо установить метку у ключевой пары аналогичную метке сертификата. Для этого используется команда rename: pyp11.rename(<идентификатор библиотеки>, <слот токена>, <тип объекта>, <ассоциированный список>)
В <типе объекта> указывается, к каким типам объектов будет применяться команда: 'cert' | 'key' | 'all' (сертификаты, ключевая пара, к тому и другому). Команда rename позволяет менять не только CKA_LABEL, но и CKA_ID. Конкретные объекты могут задаваться идентификаторами объектов CKA_ID (pkcs11_id), например: #Импортируем сертификат и получаем его CKA_ID
labcert = 'LabelNEW' ckaid = pyp11.importcert(aa, 0, cert_der_hex, labcert) #Устанавливаем метку сертификата и для ключей #Готовим словарь ldict = dict(pkcs11_id=ckaid, pkcs11_label=labcert) #Меняем метки у ключей pyp11.rename(aa, 0, 'key', ldict) Аналогичным образом меняется атрибут CKA_ID. В этом случае в словарь вместо метки указывается новый CKA_ID: ldict = dict(pkcs11_id=ckaid, pkcs11_id_new=11111)
Аналогичным образом можно удалить объекты: pyp11.delete(<идентификатор библиотеки>, <слот токена>, <тип объекта>, <ассоциированный список>)
При уничтожении в словарь попадает только один элемент, который будет указывать на удаляемые объекты. Это либо CKA_ID (ключ pkcs11_id) либо непосредственно handle-объекта (как правило, его можно получить по команде pyp11.listobjects, ключ pkcs11_handle): ldict = dict(pkcs11_id=ckaid)
#Или с handle-объекта: #ldict = dict(hobj=pkcs11_handle) #Уничтожить личный сертификат с ключами pyp11.login(aa. 0, '01234567') pyp11.delete(aa, 0, 'all', ldict) pyp11.logout(aa, 0) Упомянем еще об одной очень редко используемой команде. Это команда закрытия сессий на токене: pyp11.closesession(<идентификатор библиотеки>)
Эту команду следует вызывать, когда возникнет ошибка «PKCS11_ERROR SESSION_HANDLE_INVALID», а затем повторить команду, на которой возникла ошибка. Эта ошибка может возникнуть при кратковременном извлечении токена из компьютера при работе вашей программы. И завершим мы рассмотрение командой pyp11.listcertsder: <список сертификатов> = pyp11.listcerts(<идентификатор библиотеки>, <слот токена>)
Вот пример кода: #!/usr/bin/python3
#-*- coding: utf-8 -*- import sys import time import pyp11 print('Список сертификатов токена') aa = pyp11.loadmodule('/usr/local/lib64/libls11sw2016.so') lcerts = pyp11.listcerts(aa, 0) if (len(lcerts) == 0): print ('На токене нет сертификатов') quit() #Перебираем сертификаты for cert in lcerts: #Информация о сертификате for key in cert: print (key + ': ' + cert[key]) #Сравним с pyp11.listobjects lm = pyp11.listobjects(aa, 0, 'cert', 'value') print('Работа с listobjects:') for obj in lm: for key in obj: print (key + ': ' + obj[key]) quit() Команды pyp11.listobjects для сертификатов и команда pyp11.listcerts фактически дублируют друг друга, но так сложилось исторически. Заключение Опыт использования аналогичного модуля tclpkcs11 показывает, что функциональности, заложенной в модуль pyp11 для Python, с лихвой хватит для его использования в ИОК для работы с электронной подписью на базе российской криптографии. Более того, во второй части статьи будет рассмотрен класс token, в рамках которого будут создаваться объекты для подключенных токенов. И это позволит ещё больше упростить работу с токенами. Кстати, аналогичный класс для tclpkcs11 уже имеется. Но в заключении я хотел бы вернуться к началу статьи, а именно к проекту PyKCS11. Когда я писал письмо авторам проекта PyKCS11, то я уже добавил в него поддержку российской криптографии и сообщал им об этом: Сейчас заканчивается тестирования PyKCS11 для российской криптографии. Кстати, модуль pyp11 хорошо дополняет PyPCS11. Поэтому должна появиться и третья часть статьи, в которой будет рассказано, как добавить поддержку российской криптографии в проект PyKCS11. =========== Источник: habr.com =========== Похожие новости:
Информационная безопасность ), #_kriptografija ( Криптография ), #_python, #_graficheskie_obolochki ( Графические оболочки ), #_c |
|
Вы не можете начинать темы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы
Текущее время: 22-Ноя 15:56
Часовой пояс: UTC + 5