- I. портирование кода модуля tclpkcs11 в модуль pyp11 для python
- Ii. сборка и установка модуля pyp11
- Iii. управление токенами pkcs#11
- V. проверка электронной подписи сертификата
- Vi. работа с объектами токена
- Добавление/переопределение методов у объектов
- Информационная поддержка
- О der и ber кодировках
- О конструкторе lego
- О наследовании
- Подмешивание (mix in) методов в класс
- Создание класса
- Заключение
I. портирование кода модуля tclpkcs11 в модуль pyp11 для python
Портирование заключается в адаптации кода модуля tclpkcs11 к требованиям со стороны Python. Все изменения в проекте будут касаться только модуля tclpkcs11.c. Поэтому, первое, что мы сделаем, скопируем модуль tclpkcs11.c в файл pythonpkcs11.c и в дальнейшем будем работать именно с ним. Модуль для Python назовем
pyp11
Использовать для его создания будем C API Python. Почему-то этот способ многие (но не я) считают самым трудным, но зато он самый эффективный. Анализ C API для Tcl и C API для 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 успешно работает и на платформе
После установки модуля переходим в папку tests и начинаем тестирование.
Pаботоспособность модуля pyp11 можно проверить даже без токена. В составе модуля есть функция pyp11.dgst, которая не привязана к токенам и позволяет посчитать хэш по ГОСТ Р 34.10-2021:
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-2021-256 (stribog256)
>>> hash256 = pyp11.dgst("stribog256", "Текст для хэширования")
#Считаем хэш по ГОСТ Р 34.11-2021-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, в него помимо функций, связанных с генерацией ключевой пары, формирования и проверки подписи, включены дополнительные функции.
В папке 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. Но поскольку мы ведём речь о российской криптографии, то целесообразно сразу иметь токен с поддержкой российской криптографии.
Установить программный токен или получить доступ к облачному токену можно, воспользовавшись утилитой cryptoarmpkcs.
После запуска утилиты необходимо зайти на вкладку «Создать токены»:
На вкладке можно найти инструкции для получения токенов.
Итак, у нас токен и библиотека для работы с ним. После загрузки модуля pyp11 требуется загрузить библиотеку для работы с нашим токеном. В примерах будут использоваться библиотека librtpkcs11ecp-2.0 для работы с аппаратным токеном, библиотека libls11sw2021 для работы с программным токеном и библиотека 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/libls11sw2021.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’, только надо иметь в виду, что переинициализация токена ведет к уничтожению на нем всех объектов (ключи, сертификаты и т.д).
V. проверка электронной подписи сертификата
Используя полученные знания, напишем пример проверки электронной подписи сертификата:
Vi. работа с объектами токена
Основными объектами, с которыми приходится иметь дело, работая с токенами PKCS#11, являются сертификаты и ключи. И те и другие имеют атрибуты. Нас в первую очередь интересуют атрибуты CKA_LABEL или метка объекта и СКА_ID или идентификатор объекта. Именно атрибут CKA_ID используется для доступа и к сертификатам и ключам.
Уже имея в своем распоряжении рассмотренные выше команды модуля pyp11, можно создать ключевую пару и сформировать подписанный запрос на сертификат. Отправить полученный запрос в удостоверяющий центр и получить там сертификат.
Как работает команда? Первым делом она вычисляет по открытому ключу сертификата идентификатор 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.listcerts:
Вот пример кода:
#!/usr/bin/python3
#-*- coding: utf-8 -*-
import sys
import time
import pyp11
print('Список сертификатов токена')
aa = pyp11.loadmodule('/usr/local/lib64/libls11sw2021.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 фактически дублируют друг друга, но так сложилось исторически.
Добавление/переопределение методов у объектов
В принципе этого материала достаточно, чтобы начать использовать ООП в Tcl. Но мы упомянули и то, что в TcllOO можно динамически конструировать не только сам класс, то и экземпляры класса, т.е. объекты. На одной из таких возможностей хотелось бы остановится.
Для этого добавим в класс certificate еще один метод для подписания этим сертификатом некоторого документа:
#Метод для Подписания документа
oo::define certificate {
method signDoc {doc} {
set sign "Здесь должна находиться подпись документа $doc"
#Счетчик подписанных документов
my variable signedDoc
#Количество подписанных документов
incr signedDoc
return [list $signedDoc $sign]
}
}
При вызове этого метода должно происходить подписание документа и увеличение счетчика подписанных документов на единицу. В качестве результата работы этого метода возвращается общее число подписанных на данный момент документов и сама подпись:
. . .
set doc "Подпись1"
puts "Подписание документа $doc"
foreach {count sign} [cert1 signDoc $doc] {
puts "tПодписано документов на данный момент=$count"
puts "tПодпись документа="$sign""
}
. . .
Результат будет выглядеть так:
. . .
Подписание документа Подпись1
Подписано документов на данный момент=1
Подпись документа="Здесь должна находиться подпись документа Подпись1"
. . .Сам алгорит подписи здесь не рассматривается, но его можно найти в утилите cryptoarmpkcs:
А теперь представим, что владелец сертификата убывает в отпуск. Он знает, что в отпуске он будет отдыхать и не в коем случае не будет работать с документами и тем более что-либо подписывать. Он хочет отозвать сертификат, а когда вернется восстановить его действие. Для этих целей служит следующая функция:
#Процедура отзыва сертификата
proc revoke {cert_obj} {
oo::objdefine $cert_obj {
#Переопределяем метод подписи для конкретного объекта
method signDoc {args} {
#Переменная accessCert хранит число несанкционированных попыток подписания
my variable accessCert
set sign "Сертификат временно отозван. Не пытайтесь им подписывать!"
#Число попыток несанкционированного использования возрастает на 1
incr accessCert
return [list $accessCert $sign]
}
method unrevoke {} {
my variable accessCert
#Вызов метод unrevoke удалит метод подписи для конкретного объекта,
#восстанавливая тем самым действие метода signDoc из класса и
#удалит сам метод unrevoke
oo::objdefine [self] { deletemethod signDoc unrevoke }
if {![info exist accessCert]} {
return 0
}
return $accessCert
}
}
}Вызов этой функции определяет новый функционал методв signDoc для конкретного объекта. Для остальных объектов, как существующих и так и новых, сохраняется действие метода, определенного для класса. Также определяется новый метод unrevoke, вызов которого сотрудником по возвращению из отпуска приведет к восстановлению метода signDoc из класса certificate, путем удаления метода signDoc для объекта, а также удалит и сам метод unrevoke.
Ниже приведен фрагмент выполнения примера example5.tcl:
. . .
Подписание документа Подпись1
Подписано документов на данный момент=1
Подпись документа="Здесь должна находиться подпись документа Подпись1"
Подписание документа Подпись2
Подписано документов на данный момент=2
Подпись документа="Здесь должна находиться подпись документа Подпись2"
Отзыв сертификата
Попытка подписать документ Подпись3
Попыток несанкционированного доступа=1
Подпись документа="Сертификат временно отозван. Не пытайтесь им подписывать!"
Попытка подписать документ подпись4
Попыток несанкционированного доступа=2
Подпись документа="Сертификат временно отозван. Не пытайтесь им подписывать!"
Действие сертификата восстанвлено
За время его отзыва было 2 попытки несанкционированного досьупа
Попытка подписать документ Подпись после восстановления
Подписано документов на данный момент=3
Подпись документа="Здесь должна находиться подпись документа Подпись после восстановления"
. . .Упомянем еще один оператор. Это оператор клонирования объекта:
oo::copy <идентификатор исходного объекта> <идентификатор клона>Говорить и писать об ООП на TclOO можно долго и долго.
Еще интересней его исследовать.
Информационная поддержка
В результатах выполнения примера мы видим перечень методов доступных в классе certificate.
Для получения списка методов используется следующая команда:
info class methods <идентификатор класса> [-private]
Если флаг “-private” не задан, то выдается список публичных методов. В противном случае, выдается весь перечень методов, включая приватные.
Проверить принадлежность объекта тому или иному классу можно командой:
info object clacc <идентификатор объекта>В нашем примере объект cert1 принадлежит двум классам: certuficate и pubkey.
Если требуется узнать какие классы наследует тот или иной класс, достаточно выполнить коиманду:
info class superclasses <идентификатор класса>А если требуется получить информацию о том, какими классами наследуется тот или иной класс, то достаточно выполнить следующую команду:
info class subclasses <идентификатор класса>В нашем примере мы имеем:
$
. . .
Публичные методы класса certificate
subject parse_cert issuer
Все методы класса certificate, включая приватные
parse_dn subject parse_cert issuer
Принадлежность объекта cert1 классу certificate
1
Принадлежность объекта cert1 классу pubkey
1
Супер классы класса certificate
::pubkey
Супер классы класса pubkey
::oo::object
Подклассы класса certificate
Подклассы класса pubkey
::certificate
$ О der и ber кодировках
Для доступа к сертификату будет создан класс certificate, в конструкторе которого при создании объекта конкретного сертификата будет проводится разбор его на составные части. Для этого нам потребуется в первую очередь пакет asn (package require asn), который поможет с разбором asn-структуры сертификата.
К сожалению, этот пакет (кстати, в других скриптовых языках встречается аналогичная проблема) заточен на разбор asn-структур в DER-кодировке. Но сегодня еще встречаются сертификаты (и электронные подписи и много чего другого) в BER-кодировке. Но оказалось решить эту проблему можно достаточно просто, заменив процедуру ::asn::asnLength из пакета ASN на новую, которая будет подсчитывать длины тега как в DER, так и BER-кодировках:
package require asn
#Переименовываем оригинальную процедуру подсчета длины
rename ::asn::asnGetLength ::asn::asnGetLength.orig
#Новая процедура подсчета длины
proc ::asn::asnGetLength {data_var length_var} {
upvar 1 $data_var data $length_var length
asnGetByte data length
if {$length == 0x080} {
#Поддержка BER-кодировки
set lendata [string length $data]
set tvl 1
set length 0
set data1 $data
while {$tvl != 0} {
::asn::asnGetByte data1 peek_tag
::asn::asnPeekByte data1 peek_tag1
if {$peek_tag == 0x00 && $peek_tag1 == 0x00} {
incr tvl -1
::asn::asnGetByte data1 tag
incr length 2
continue
}
if {$peek_tag1 == 0x80} {
incr tvl
if {$tvl > 0} {
incr length 2
}
::asn::asnGetByte data1 tag
} else {
set l1 [string length $data1]
::asn::asnGetLength data1 ll
set l2 [string length $data1]
set l3 [expr $l1 - $l2]
incr length $l3
incr length $ll
incr length
::asn::asnGetBytes data1 $ll strt
}
}
return
}
if {$length > 0x080} {
set len_length [expr {$length & 0x7f}]
if {[string length $data] < $len_length} {
return -code error
"length information invalid, not enough octets left"
}
asnGetBytes data $len_length lengthBytes
switch $len_length {
1 { binary scan $lengthBytes cu length }
2 { binary scan $lengthBytes Su length }
3 { binary scan x00$lengthBytes Iu length }
4 { binary scan $lengthBytes Iu length }
default {
binary scan $lengthBytes H* hexstr
scan $hexstr %llx length
}
}
}
return
}
Что нам еще потребуется? Любая ASN-структура, особенно такая как сертификат X509.v3 содержит большое количество OID-ов, для которых могут существовать достаточно общепризнанные символьные обозначения. Значительная часть OID-ов, которые используются в сертификатах, присутствует в пакете pki.
Мы его тоже будем использовать (package require pki). Естественно, что в этом пакете ничего не известно об OID-ах, которые используются в квалифицированных сертификатах и об OID-ах для российской криптографии. Их тоже целесообразно добавить в массив ::pki::oids:
set ::pki::oids(1.2.643.100.1) "OGRN"
set ::pki::oids(1.2.643.100.5) "OGRNIP"
set ::pki::oids(1.2.643.3.131.1.1) "INN"
set ::pki::oids(1.2.643.100.3) "SNILS"
#Для КПП ЕГАИС
set ::pki::oids(1.2.840.113549.1.9.2) "UN"
#set ::pki::oids(1.2.840.113549.1.9.2) "unstructuredName"
#Алгоритмы подписи
set ::pki::oids(1.2.643.2.2.3) "GOST R 34.10-2001 with GOST R 34.11-94"
set ::pki::oids(1.2.643.2.2.19) "GOST R 34.10-2001"
set ::pki::oids(1.2.643.7.1.1.1.1) "GOST R 34.10-2021-256"
set ::pki::oids(1.2.643.7.1.1.1.2) "GOST R 34.10-2021-512"
set ::pki::oids(1.2.643.7.1.1.3.2) "GOST R 34.10-2021-256 with GOSTR 34.11-2021-256"
set ::pki::oids(1.2.643.7.1.1.3.3) "GOST R 34.10-2021-512 with GOSTR 34.11-2021-512"
set ::pki::oids(1.2.643.100.113.1) "KC1 Class Sign Tool"
set ::pki::oids(1.2.643.100.113.2) "KC2 Class Sign Tool"
set ::pki::oids(2.5.4.42) "givenName"
Для полноты не мешает также добавить символьное представление параметров подписи:
О конструкторе lego
У читателя, наверное, так и хочет сорваться с языка вопрос:- А причем здесь конструктор Lego? А вот при чем. Если, скажем в C класс объекта должен быть определен сразу, то в
TclOO
класс может собираться постепенно как модель в конструкторе. Более того одни части класса могут удаляться и заменяться другими и т.д. Более того, такой метод конструирования класса распространяется и на объекты, да на конкретные объекты.
Предположим, что необходимо вывести информацию и о владельце и об издателе сертификата. Для этого нам потребуется два публичных issur и subject и один приватный метод parse_dn для разбора отличительного имени (DN) издателя и владельца. Традиционно нам пришлось бы переписать класс certificate, добавив в него указанные методы.
Для добавления в класс новых членов в область данных используется команда (модуль конструктора) вида:
oo::define <идентификатор класса> {
#Область данных класса
variable <идентификатор переменной> … [<идентификатор переменной>]
[ variable <идентификатор переменной> ]
}Может быть несколько команд variable, каждая из которых определяет один или несколько элементов данных.
Аналогично добавляются методы:
oo::define <идентификатор класса> {
#методы
method <идентификатор метода 1> {<параметры>} {
<тело метода>
}
[
…
method <идентификатор метода N> {<параметры>} {
<тело метода>
}
]
}Любой метод можно удалить в любое время с помощью команды deletemethod внутри сценария определения класса. Эта команды будет рассмотрена ниже при рассмотрении примера с отзывом сертификата.
Про видимость методов (публичные, приватные методы) мы уже говорили выше.
Отметим, что первоначально класс может создаваться абсолютно пустым:
oo::class create <Идентификатор класса>с последующим наполнением его через команду:
oo::define <идентификатор класса> {
…
}
Итак, добавляем новые методы в класс Certificate:
О наследовании
Определяющей характеристикой объектно-ориентированных систем является поддержка наследования. Наследование относится к способности производного класса (также называемого подклассом ) наследовать область данных и методы из наследуемого класса (из супер класса).
Подмешивание (mix in) методов в класс
Для расширения возможность класса, прежде всего с точки зрения его функциональности, помимо наследования можно использовать так называемый метод подмешивания (mix in).
Если мы хотим распечатать сертификат в текстовом виде, то нам потребуется разбор asn-структур расширений сертификата. Это и начначение ключа сертификата, это свойства квалифицированного сертификата и многое другое. Оформим разбор расширений сертификата в отдельный класс parseexts, в котором отсутствует констуктор и деструктор:
Создание класса
Объявление класса в TclOO мало чем отличается от объявления класса в других языках. Класс в TclOO также содержит область данных, конструктор, область объектно-ориентированных методов и деструктор. При этом область данных, конструктор и деструктор могут опускаться.
Напомним, что конструктор вызывается при создание объекта (экземпляра объекта) заданного класса, а деструктор при его уничтожении. Конструктор (в отличии от деструктора), также как и методы, может иметь параметры. В нашем случае параметром для конструктора выступает сертификат в DER или PEM кодировке.
Области данных может предшествовать область наследуемых классов (superclass). Её будем рассматривать ниже. Но, для написания универсального класса certificate, эта область будет нами задействована.
В TclOO можно узнать какие классы в данный момент доступны в программе. Для этих целей служит команда следующего вида:
info class instances oo::classВ последующем, мы будем задействовать для наследования класс pubkey. Поэтому в нашем определении класса certificate присутствует проверка наличия класса pubkey и, если он присутствует, он объявляется как наследуемый (superclass pubkey).
Итак, ниже представлен класс для сертификата пока что с одним методом parse_cert, который возвращает список элементов сертификата:
В области данных командой variable определяются данные/переменные объекта через, которые доступны во всех методах класса.
Метод method определяется точно так же, как процедура proc Tcl. Методы могут иметь произвольное количество параметров. Внутри метода можно определять свои данные командой
my variable <идентификатор переменной>. Методы могут быть публичными (экспортируемыми) и приватными.
Экспортируемые методы методы видимы за пределами класса. По умолчанию экспортируются методы начинаются со строчной буквы. По умолчанию методы, чьи имена начинаются с прописной буквы считаются неэкспортируемыми (приватными) методами. Область видимости независимо от первого символа можно задать явно. Для указания того, что метод является публичным служит следующая команда:
export <идентификатор метода>Для запрета экспорта метода используется следующая команда:
unexport <идентификатор метода>
Для вызова одного метода из другого метода внутри класса используется команда my:
my <идентификатор метода>Для этой же цели можно использовать внутреннюю команда класса self, которая возвращает идентификатор текущего объекта:
[self] <идентификатор метода>Ниже мы увидим всё это.
Для дальнейшей работы соберем весь рассмотренный код в файле classparsecert.tcl.
После того как был определен класс можно создавать конкретный объект (экземпляр объекта). Для этого может быть использована одна из следующих команд:
или
set <переменная для идентификатора экземпляра класса > <имя класса> new [параметры для констуктура]
В первом случае программист сам назначает идентификатор для создаваемого экземпляра объекта. Этот идентификатор фактически будет командой, через которую осуществляется доступ к объекту и его методам:
Во втором случае идентификатор создаваемого объекта назначается интерпретатором и возвращается как результат выполнения команды new для указанного класса. В этом случае идентификатор объекта будет браться из этой переменной.
Интересно сравнить с созданием объекта в Python. И что мы видим? Несущественную синтаксическую разницу.
Напишем небольшой пример example1.tcl использования этого класса:
#Загружаем описание класса
source ./classparsecert.tcl
#Загружаем сертификат
set file [lindex $argv 0]
if {$argc != 1 || ![file exists $file]} {
puts "Usage: tclsh example1 <файл с сертификатом>"
exit
}
puts "Loading file: $file"
set fd [open $file]
chan configure $fd -translation binary
set data [read $fd]
close $fd
if {[catch {certificate create cert1 $data} er1]} {
puts "Файл не содержит СЕРТИФИКАТ"
exit
}
array set cert_parse [cert1 parse_cert]
#parray cert_parse
puts "Распарсенный сертификат"
foreach ind [array names cert_parse] {
puts "tcert_parse($ind)"
}
Выполним пример:
$tclsh ./example1.tcl
Loading file: minenergo.cer
Распарсенный сертификат
cert_parse(subject)
cert_parse(pubkeyinfo_hex)
cert_parse(extensions)
cert_parse(issuer)
cert_parse(data_signature_algo)
cert_parse(cert_full)
cert_parse(serial_number)
cert_parse(signature)
cert_parse(pubkey_algo)
cert_parse(notAfter)
cert_parse(signature_algo)
cert_parse(notBefore)
cert_parse(version)
cert_parse(tbsCert)
$
Заключение
Опыт использования аналогичного модуля tclpkcs11 показывает, что функциональности, заложенной в модуль pyp11 для Python, с лихвой хватит для его использования в
ИОК
для работы с электронной подписью на базе российской криптографии. Более того, во
будет рассмотрен класс token, в рамках которого будут создаваться объекты для подключенных токенов. И это позволит ещё больше упростить работу с токенами. Кстати, аналогичный класс для tclpkcs11 уже имеется.
Но в заключении я хотел бы вернуться к началу статьи, а именно к проекту PyKCS11.
Когда я писал письмо авторам проекта PyKCS11, то я уже добавил в него поддержку российской криптографии и сообщал им об этом:
Сейчас заканчивается тестирования PyKCS11 для российской криптографии. Кстати, модуль pyp11 хорошо дополняет PyKCS11. Поэтому должна появиться и третья часть статьи, в которой будет рассказано, как добавить поддержку российской криптографии в проект PyKCS11.
