Русский
Русский
English
Статистика
Реклама

X509 v3

Конструктор Lego и объектно-ориентированное программирование в Tcl. Разбор сертификата x509.v3

21.12.2020 18:22:45 | Автор: admin
imageЧасто приходится слышать, что скриптовому языку Tcl не хватает поддержки объектно-ориентированного стиля программирования. Сам я до последнего времени мало прибегал к объектно-ориентированному программированию, тем более в среде Tcl. Но за Tcl стало обидно. Я решил разобраться. И оказалось, что практически с момента своего появления появилась возможность объектно-ориентированного программирования (ООП) в среде Tcl. Все неудобство заключалось в необходимости подключить пакет с поддержкой ООП. А таких пакетом было и есть несколько, как говорится на любой вкус. Это и Incr Tcl, Snit и XoTcl.
Программисты, привыкшие к языку C++, чувствуют себя как дома, программируя в среде Incr Tcl. Это было одним из первых широко используемых расширений для OOП на основе Tcl.
Пакет Snit в основном используется при построении Tk-виджетов, а XoTcl и его преемник nx предназначались для исследования динамического объектно-ориентированного программирования.
Обобщение опыта, полученного при использовании этих систем, позволило внедрить ООП в ядро Tcl начиная с версии 8.6. Так появился TclOO Tcl Object Oriented.
Сразу отметим, что Tcl не просто поддерживает объектно-ориентированное программирование, а в полном смысле динамическое объектно-ориентированное программирование.
Разрабатывая приложения на Tcl/Tk, например удостоверяющий центр CAFL63, я не прибегал к ООП. И, как сейчас понимаю, зря. Где, где, а в УЦ объектов хватает. Это и запросы на сертификаты, это и сами сертификаты, списки отозванных сертификатов и много чего другого:



Начать было решено с рассмотрения сертификата x509.v3 с учетом российской специфики как объекта при ООП. Тем более, что имеется опыт разбора квалифицированного сертификата на Python. Именно на примере разбора и работы с сертификатом мы и покажем объектно-ориентированный стиль программирования в TclOO.

О 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 1set length 0set data1 $datawhile {$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 2continue    }    if {$peek_tag1 == 0x80} {incr tvlif {$tvl > 0} {    incr length 2}::asn::asnGetByte data1 tag     } else {set l1 [string length $data1]::asn::asnGetLength data1 llset l2 [string length $data1]set l3 [expr $l1 - $l2]incr length $l3incr length $llincr 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* hexstrscan $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-2012-256"set ::pki::oids(1.2.643.7.1.1.1.2) "GOST R 34.10-2012-512"set ::pki::oids(1.2.643.7.1.1.3.2) "GOST R 34.10-2012-256 with GOSTR 34.11-2012-256"set ::pki::oids(1.2.643.7.1.1.3.3) "GOST R 34.10-2012-512 with GOSTR 34.11-2012-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"

Для полноты не мешает также добавить символьное представление параметров подписи:
#Параметры подписи
#Параметры подписиset ::pki::oids((1.2.643.2.2.35.1)"id-GostR3410-2001-CryptoPro-A-ParamSet"set ::pki::oids(1.2.643.2.2.35.2)"id-GostR3410-2001-CryptoPro-B-ParamSet"set ::pki::oids(1.2.643.2.2.35.3)"id-GostR3410-2001-CryptoPro-C-ParamSet"set ::pki::oids(1.2.643.2.2.36.0)"id-GostR3410-2001-CryptoPro-XchA-ParamSet"set ::pki::oids(1.2.643.2.2.36.1)"id-GostR3410-2001-CryptoPro-XchB-ParamSet"set ::pki::oids(1.2.643.7.1.2.1.1.1)"id-tc26-gost-3410-2012-256-paramSetA"set ::pki::oids(1.2.643.7.1.2.1.1.2)"id-tc26-gost-3410-2012-256-paramSetB"set ::pki::oids(1.2.643.7.1.2.1.1.3)"id-tc26-gost-3410-2012-256-paramSetC"set ::pki::oids(1.2.643.7.1.2.1.1.4)"id-tc26-gost-3410-2012-256-paramSetD"set ::pki::oids(1.2.643.7.1.2.1.2.1)"id-tc26-gost-3410-2012-512-paramSetA"set ::pki::oids(1.2.643.7.1.2.1.2.2)"id-tc26-gost-3410-2012-512-paramSetB"set ::pki::oids(1.2.643.7.1.2.1.2.3)"id-tc26-gost-3410-2012-512-paramSetC"


Создание класса

Объявление класса в TclOO мало чем отличается от объявления класса в других языках. Класс в TclOO также содержит область данных, конструктор, область объектно-ориентированных методов и деструктор. При этом область данных, конструктор и деструктор могут опускаться. Напомним, что конструктор вызывается при создание объекта (экземпляра объекта) заданного класса, а деструктор при его уничтожении. Конструктор (в отличии от деструктора), также как и методы, может иметь параметры. В нашем случае параметром для конструктора выступает сертификат в DER или PEM кодировке.
Области данных может предшествовать область наследуемых классов (superclass). Её будем рассматривать ниже. Но, для написания универсального класса certificate, эта область будет нами задействована.
В TclOO можно узнать какие классы в данный момент доступны в программе. Для этих целей служит команда следующего вида:
info class instances oo::class

В последующем, мы будем задействовать для наследования класс pubkey. Поэтому в нашем определении класса certificate присутствует проверка наличия класса pubkey и, если он присутствует, он объявляется как наследуемый (superclass pubkey).
Итак, ниже представлен класс для сертификата пока что с одним методом parse_cert, который возвращает список элементов сертификата:
Объявление класса certificate
oo::class create certificate {#Список доступных классов    foreach cl  "[info class instances oo::class]" {if {$cl == "::pubkey" } {#Если класс pubkey есть, то наследуем его. Это будет использовано в примере 3    superclass pubkey    break}    }#Переменные класса#Доступны только в пределах класса#Переменная для хранения разобранного сертификата.     variable ret#Переменная для хранения расширений сертификата    variable extcert#Конструктор    constructor {cert} {array set parsed_cert [::pki::_parse_pem $cert "-----BEGIN CERTIFICATE-----" "-----END CERTIFICATE-----"]set cert_seq $parsed_cert(data)array set ret [list]#Полный сертификат der в hexbinary scan $cert_seq H* ret(cert_full)  # Decode X.509 certificate, which is an ASN.1 sequence::asn::asnGetSequence cert_seq wholething::asn::asnGetSequence wholething cert#tbs - сертификатset ret(tbsCert) [::asn::asnSequence $cert]binary scan $ret(tbsCert) H* ret(tbsCert)::asn::asnPeekByte cert peek_tagif {$peek_tag != 0x02} {    # Version number is optional, if missing assumed to be value of 0    ::asn::asnGetContext cert - asn_version    ::asn::asnGetInteger asn_version ret(version)    incr ret(version)} else {    set ret(version) 1}::asn::asnGetBigInteger cert ret(serial_number)::asn::asnGetSequence cert data_signature_algo_seq::asn::asnGetObjectIdentifier data_signature_algo_seq ret(data_signature_algo)::asn::asnGetSequence cert issuer    set ret(issuer) $issuer::asn::asnGetSequence cert validity::asn::asnGetUTCTime validity ret(notBefore)::asn::asnGetUTCTime validity ret(notAfter)::asn::asnGetSequence cert subject    set ret(subject) $subject::asn::asnGetSequence cert pubkeyinfobinary scan $pubkeyinfo H* ret(pubkeyinfo_hex)::asn::asnGetSequence pubkeyinfo pubkey_algoid::asn::asnGetObjectIdentifier pubkey_algoid ret(pubkey_algo)::asn::asnGetBitString pubkeyinfo pubkeyset extensions_list [list]while {$cert != ""} {    ::asn::asnPeekByte cert peek_tag    switch -- [format {0x%02x} $peek_tag] {    "0x81" {    ::asn::asnGetContext cert - issuerUniqueID        }    "0x82" {    ::asn::asnGetContext cert - subjectUniqueID        }    "0xa1" {    ::asn::asnGetContext cert - issuerUniqID        }    "0xa2" {    ::asn::asnGetContext cert - subjectUniqID        }    "0xa3" {    ::asn::asnGetContext cert - extensions_ctx    ::asn::asnGetSequence extensions_ctx extensions#Убираем перевод oid в текстset ::pki::oids1 [array get ::pki::oids]array unset ::pki::oids     while {$extensions != ""} {            ::asn::asnGetSequence extensions extension            ::asn::asnGetObjectIdentifier extension ext_oid        ::asn::asnPeekByte extension peek_tag        if {$peek_tag == 0x1} {        ::asn::asnGetBoolean extension ext_critical            } else {        set ext_critical false            }        ::asn::asnGetOctetString extension ext_value_seq        set ext_oid [::pki::_oid_number_to_name $ext_oid]        set ext_value [list $ext_critical]        switch -- $ext_oid {                        id-ce-basicConstraints {                ::asn::asnGetSequence ext_value_seq ext_value_bin                if {$ext_value_bin != ""} {            ::asn::asnGetBoolean ext_value_bin allowCA                } else {            set allowCA "false"                }            if {$ext_value_bin != ""} {            ::asn::asnGetInteger ext_value_bin caDepth                } else {            set caDepth -1                }                lappend ext_value $allowCA $caDepth                        }                        default {                binary scan $ext_value_seq H* ext_value_seq_hex                lappend ext_value $ext_value_seq_hex                        }                    }        lappend extensions_list $ext_oid $ext_value    }#Возвращаем перевод oid-ов в текстarray set ::pki::oids $::pki::oids1        }    }}set ret(extensions) $extensions_listarray set extcert $extensions_list::asn::asnGetSequence wholething signature_algo_seq::asn::asnGetObjectIdentifier signature_algo_seq ret(signature_algo)::asn::asnGetBitString wholething ret(signature)set ret(serial_number) [::math::bignum::tostr $ret(serial_number)]set ret(signature) [binary format B* $ret(signature)]binary scan $ret(signature) H* ret(signature)#Инициируем класс pubkeyinfo при наследовании - superclassif {[llength [self next]]} {#Если есть наследуемый класс, то вызываем его конструкторnext $ret(pubkeyinfo_hex)}    }    method parse_cert {} {        return [array get ret]    }}


В области данных командой variable определяются данные/переменные объекта через, которые доступны во всех методах класса.
Метод method определяется точно так же, как процедура proc Tcl. Методы могут иметь произвольное количество параметров. Внутри метода можно определять свои данные командой
my variable <идентификатор переменной>
. Методы могут быть публичными (экспортируемыми) и приватными.
Экспортируемые методы методы видимы за пределами класса. По умолчанию экспортируются методы начинаются со строчной буквы. По умолчанию методы, чьи имена начинаются с прописной буквы считаются неэкспортируемыми (приватными) методами. Область видимости независимо от первого символа можно задать явно. Для указания того, что метод является публичным служит следующая команда:
export <идентификатор метода>
.
Для запрета экспорта метода используется следующая команда:
unexport <идентификатор метода>
.
Для вызова одного метода из другого метода внутри класса используется команда my:
my <идентификатор метода>
.
Для этой же цели можно использовать внутреннюю команда класса self, которая возвращает идентификатор текущего объекта:
[self] <идентификатор метода>

Ниже мы увидим всё это.
Для дальнейшей работы соберем весь рассмотренный код в файле classparsecert.tcl.
Содержимое файла classparsecert.tcl
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 1set length 0set data1 $datawhile {$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 2continue    }    if {$peek_tag1 == 0x80} {incr tvlif {$tvl > 0} {    incr length 2}::asn::asnGetByte data1 tag     } else {set l1 [string length $data1]::asn::asnGetLength data1 llset l2 [string length $data1]set l3 [expr $l1 - $l2]incr length $l3incr length $llincr 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* hexstrscan $hexstr %llx length            }        }    }    return}package require pkiset ::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-2012-256"set ::pki::oids(1.2.643.7.1.1.1.2) "GOST R 34.10-2012-512"set ::pki::oids(1.2.643.7.1.1.3.2) "GOST R 34.10-2012-256 with GOSTR 34.11-2012-256"set ::pki::oids(1.2.643.7.1.1.3.3) "GOST R 34.10-2012-512 with GOSTR 34.11-2012-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"#Параметры подписиset ::pki::oids((1.2.643.2.2.35.1)"id-GostR3410-2001-CryptoPro-A-ParamSet"set ::pki::oids(1.2.643.2.2.35.2)"id-GostR3410-2001-CryptoPro-B-ParamSet"set ::pki::oids(1.2.643.2.2.35.3)"id-GostR3410-2001-CryptoPro-C-ParamSet"set ::pki::oids(1.2.643.2.2.36.0)"id-GostR3410-2001-CryptoPro-XchA-ParamSet"set ::pki::oids(1.2.643.2.2.36.1)"id-GostR3410-2001-CryptoPro-XchB-ParamSet"set ::pki::oids(1.2.643.7.1.2.1.1.1)"id-tc26-gost-3410-2012-256-paramSetA"set ::pki::oids(1.2.643.7.1.2.1.1.2)"id-tc26-gost-3410-2012-256-paramSetB"set ::pki::oids(1.2.643.7.1.2.1.1.3)"id-tc26-gost-3410-2012-256-paramSetC"set ::pki::oids(1.2.643.7.1.2.1.1.4)"id-tc26-gost-3410-2012-256-paramSetD"set ::pki::oids(1.2.643.7.1.2.1.2.1)"id-tc26-gost-3410-2012-512-paramSetA"set ::pki::oids(1.2.643.7.1.2.1.2.2)"id-tc26-gost-3410-2012-512-paramSetB"set ::pki::oids(1.2.643.7.1.2.1.2.3)"id-tc26-gost-3410-2012-512-paramSetC"#Класс certificateoo::class create certificate {#Наследуем класс pubkey#    superclass pubkey#Переменные класса#Доступны только в пределах класса#Переменная для хранения разобранного сертификата.     variable ret#Переменная для хранения расширений сертификатаvariable extcert#Конструктор    constructor {cert} {array set parsed_cert [::pki::_parse_pem $cert "-----BEGIN CERTIFICATE-----" "-----END CERTIFICATE-----"]set cert_seq $parsed_cert(data)array set ret [list]#Полный сертификат der в hexbinary scan $cert_seq H* ret(cert_full)  # Decode X.509 certificate, which is an ASN.1 sequence::asn::asnGetSequence cert_seq wholething::asn::asnGetSequence wholething cert#tbs - сертификатset ret(tbsCert) [::asn::asnSequence $cert]binary scan $ret(tbsCert) H* ret(tbsCert)::asn::asnPeekByte cert peek_tagif {$peek_tag != 0x02} {    # Version number is optional, if missing assumed to be value of 0    ::asn::asnGetContext cert - asn_version    ::asn::asnGetInteger asn_version ret(version)    incr ret(version)} else {    set ret(version) 1}::asn::asnGetBigInteger cert ret(serial_number)::asn::asnGetSequence cert data_signature_algo_seq::asn::asnGetObjectIdentifier data_signature_algo_seq ret(data_signature_algo)::asn::asnGetSequence cert issuer    set ret(issuer) $issuer::asn::asnGetSequence cert validity::asn::asnGetUTCTime validity ret(notBefore)::asn::asnGetUTCTime validity ret(notAfter)::asn::asnGetSequence cert subject    set ret(subject) $subject::asn::asnGetSequence cert pubkeyinfobinary scan $pubkeyinfo H* ret(pubkeyinfo_hex)::asn::asnGetSequence pubkeyinfo pubkey_algoid::asn::asnGetObjectIdentifier pubkey_algoid ret(pubkey_algo)::asn::asnGetBitString pubkeyinfo pubkeyset extensions_list [list]while {$cert != ""} {    ::asn::asnPeekByte cert peek_tag    switch -- [format {0x%02x} $peek_tag] {    "0x81" {    ::asn::asnGetContext cert - issuerUniqueID        }    "0x82" {    ::asn::asnGetContext cert - subjectUniqueID        }    "0xa1" {    ::asn::asnGetContext cert - issuerUniqID        }    "0xa2" {    ::asn::asnGetContext cert - subjectUniqID        }    "0xa3" {    ::asn::asnGetContext cert - extensions_ctx    ::asn::asnGetSequence extensions_ctx extensions#Убираем перевод oid в текстset ::pki::oids1 [array get ::pki::oids]array unset ::pki::oids     while {$extensions != ""} {            ::asn::asnGetSequence extensions extension            ::asn::asnGetObjectIdentifier extension ext_oid        ::asn::asnPeekByte extension peek_tag        if {$peek_tag == 0x1} {        ::asn::asnGetBoolean extension ext_critical            } else {        set ext_critical false            }        ::asn::asnGetOctetString extension ext_value_seq        set ext_oid [::pki::_oid_number_to_name $ext_oid]        set ext_value [list $ext_critical]        switch -- $ext_oid {                        id-ce-basicConstraints {                ::asn::asnGetSequence ext_value_seq ext_value_bin                if {$ext_value_bin != ""} {            ::asn::asnGetBoolean ext_value_bin allowCA                } else {            set allowCA "false"                }            if {$ext_value_bin != ""} {            ::asn::asnGetInteger ext_value_bin caDepth                } else {            set caDepth -1                }                           lappend ext_value $allowCA $caDepth                        }                        default {                binary scan $ext_value_seq H* ext_value_seq_hex                lappend ext_value $ext_value_seq_hex                        }                    }        lappend extensions_list $ext_oid $ext_value    }#Возвращаем перевод oid-ов в текстarray set ::pki::oids $::pki::oids1        }    }}set ret(extensions) $extensions_listarray set extcert $extensions_list::asn::asnGetSequence wholething signature_algo_seq::asn::asnGetObjectIdentifier signature_algo_seq ret(signature_algo)::asn::asnGetBitString wholething ret(signature)set ret(serial_number) [::math::bignum::tostr $ret(serial_number)]set ret(signature) [binary format B* $ret(signature)]binary scan $ret(signature) H* ret(signature)#Инициируем класс pubkeyinfo при наследовании - superclass#next $ret(pubkeyinfo_hex)    }    method parse_cert {} {        return [array get ret]    }}


После того как был определен класс можно создавать конкретный объект (экземпляр объекта). Для этого может быть использована одна из следующих команд:
<имя класса> create <идентификатор экземпляра класса> [параметры для констуктура]

или
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 binaryset data [read $fd]close $fdif {[catch {certificate create cert1 $data} er1]} {puts "Файл не содержит СЕРТИФИКАТ"exit}array set cert_parse [cert1 parse_cert]#parray cert_parseputs "Распарсенный сертификат"foreach ind [array names cert_parse] {    puts "\tcert_parse($ind)"}

Выполним пример:
$tclsh ./example1.tclLoading 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)$ 

О конструкторе Lego

У читателя, наверное, так и хочет сорваться с языка вопрос:- А причем здесь конструктор Lego? А вот при чем. Если, скажем в C++ класс объекта должен быть определен сразу, то в TclOO класс может собираться постепенно как модель в конструкторе. Более того одни части класса могут удаляться и заменяться другими и т.д. Более того, такой метод конструирования класса распространяется и на объекты, да на конкретные объекты.
Предположим, что необходимо вывести информацию и о владельце и об издателе сертификата. Для этого нам потребуется два публичных issur и subject и один приватный метод parse_dn для разбора отличительного имени (DN) издателя и владельца. Традиционно нам пришлось бы переписать класс certificate, добавив в него указанные методы. В TclOO можно поступить по другому. Можно просто в нужном месте программы выполнить оператор добавления в существующий класс новых членов.
Для добавления в класс новых членов в область данных используется команда (модуль конструктора) вида:
oo::define <идентификатор класса>  {#Область данных классаvariable <идентификатор переменной>  [<идентификатор переменной>][ variable <идентификатор переменной> ]}

Может быть несколькокоманд variable, каждая из которых определяет один или несколько элементов данных.
Аналогично добавляются методы:
oo::define <идентификатор класса>  {#методыmethod <идентификатор метода 1>  {<параметры>} {<тело метода>}[method <идентификатор метода N>  {<параметры>} {<тело метода>}]}

Любой метод можно удалить в любое время с помощьюкоманды deletemethod внутри сценария определения класса. Эта команды будет рассмотрена ниже при рассмотрении примера с отзывом сертификата.
Про видимость методов (публичные, приватные методы) мы уже говорили выше.
Отметим, что первоначально класс может создаваться абсолютно пустым:
oo::class create <Идентификатор класса>

с последующим наполнением его через команду:
oo::define <идентификатор класса>  {}

Итак, добавляем новые методы в класс Certificate:
oo::define certificate {    method issuer {} {return [ my parse_dn $ret(issuer)]    }    method subject {} {return [ my parse_dn $ret(subject)]    }    method parse_dn {asnblock} {set lret {}      while {[string length $asnblock]} {        asn::asnGetSet asnblock AttributeValueAssertion        asn::asnGetSequence AttributeValueAssertion valblock        asn::asnGetObjectIdentifier valblock oidset name [::pki::_oid_number_to_name $oid]::asn::asnGetString valblock  valuelappend lret [string toupper $name]lappend lret $value      }return $lret    }    unexport parse_dn}

Теперь дополним наш пример кодом для распечатки информации об издателе и владельце:
...puts "Сведения о владельце:"foreach {oid value} [cert1 subject] {    puts "\t$oid=$value"}puts "Сведения об издателе:"foreach {oid value} [cert1 issuer] {    puts "\t$oid=$value"}...

Таким образом мы получим второй пример.
Тестовый пример example2.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)"
}
#Добавляем новые методы
oo::define certificate {
method issuer {} {
return [ my parse_dn $ret(issuer)]
}
method subject {} {
return [ my parse_dn $ret(subject)]
}
method parse_dn {asnblock} {
set lret {}
while {[string length $asnblock]} {
asn::asnGetSet asnblock AttributeValueAssertion
asn::asnGetSequence AttributeValueAssertion valblock
asn::asnGetObjectIdentifier valblock oid
set name [::pki::_oid_number_to_name $oid]
::asn::asnGetString valblock value
lappend lret [string toupper $name]
lappend lret $value
}
return $lret
}
#Приватный метод
unexport parse_dn
}
#Применяем методы
puts Сведения о владельце:
foreach {oid value} [cert1 subject] {
puts "\t$oid=$value"
}
puts Сведения об издателе:
foreach {oid value} [cert1 issuer] {
puts "\t$oid=$value"
}

Попробуем выполнить этот пример:
$tclsh example2.tcl minenergo.cer  Loading file: minenergo.cerРаспарсенный сертификат        cert_parse(subject)        . . .         cert_parse(tbsCert)Сведения о владельце:        EMAIL=xxxxxxxxxxx        INN=xxxxxxxxxxx        OGRN=............. . .        ST=77 г. Москва        C=RU        CN=Мин РоссииСведения об издателе: . . .        C=RU        ST=77 Москва        L=Москва        CN=Тестовый удостоверяющий центр$

О наследовании


Определяющей характеристикой объектно-ориентированных систем является поддержка наследования.Наследование относится к способностипроизводногокласса (также называемогоподклассом) наследовать область данных и методы из наследуемого класса (из супер класса).
При разборе сертификата, естественно, требуется получить и полную информацию о его публичном ключе. Предположим у нас уже есть класс pubkey, который на основе asn-структуры pubkeyinfo выдает полную информацию о публичном ключе, включая RSA, EC, GOST:
oo::class create pubkey {#Внутренняя переменная класса для хранения asn-структуры pubkeyinfo    variable infopk    constructor {pubkinfo} {set infopk $pubkinfo    }    method infopubkey {} {array set retpk [list]set pubkeyinfo [binary format H* $infopk]::asn::asnGetSequence pubkeyinfo pubkey_algoid::asn::asnGetObjectIdentifier pubkey_algoid retpk(pubkey_algo)::asn::asnGetBitString pubkeyinfo pubkeyset pubkey [binary format B* $pubkey]binary scan $pubkey H* retpk(pubkey)set retpk(pkcs11id_hex) [::sha1::sha1  $pubkey]if {"1 2 643" == [string range $retpk(pubkey_algo) 0 6]} {#ГОСТ-ключ        set retpk(type) gost    ::asn::asnGetSequence pubkey_algoid pubalgost  #OID - параметра    ::asn::asnGetObjectIdentifier pubalgost retpk(paramkey)    set retpk(paramkey) [::pki::_oid_number_to_name $retpk(paramkey)]    if {$pubalgost != ""} {  #OID - Функция хэша::asn::asnGetObjectIdentifier pubalgost retpk(hashkey)    } else {set retpk(hashkey) ""    }} elseif {"1 2 840 10045 2 1" == $retpk(pubkey_algo) } {#EC-key        set retpk(type) ec    ::asn::asnGetObjectIdentifier pubkey_algoid retpk(pubkey_algo_par)} elseif {"1 2 840 113549 1 1 1" == $retpk(pubkey_algo) }  {#RSA- key        set retpk(type) rsa    binary scan $pubkey H* retpk(pubkey)    ::asn::asnGetSequence pubkey pubkey_parts    ::asn::asnGetBigInteger pubkey_parts retpk(n)    ::asn::asnGetBigInteger pubkey_parts retpk(e)    set retpk(n) [::math::bignum::tostr $retpk(n)]    set retpk(e) [::math::bignum::tostr $retpk(e)]    set retpk(l) [expr {int([::pki::_bits $retpk(n)] / 8.0000 + 0.5) * 8}]} else {        set retpk(type) unknown}return [array get retpk]    }}

Сохраним этот класс в файле classpubkeyinfo.tcl.
Для того, чтобы наследовать метод infopubkey для объектов класса certificate, в определение класса certificate добавляется определение суперкласса, методы которого будут наследоваться:
superclass pubkey

Также добавляем в конструктор класса certificate вызов конструктора класса pubkey с передачей ему в качестве параметра asn-структуры pubkeyinfo:
next $ret(pubkeyinfo_hex)

Команда next вызывает одноименный метод (в данном случае constructor) из суперкласса, т.е. из класса pubkey. Конструктор в классе pubkey просто сохранит в переменной класса infopk asn-структуру публичного ключа. Этот код с соответствующей проверкой наличия в теле программы класса pubkey и его конструктора был включен при определении класса certificate.
Полный техт example3.tcl здесь.
source ./classpubkeyinfo.tclsource ./classparsecert_and_pk.tclset 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 binaryset data [read $fd]close $fdif {[catch {certificate create cert1 $data} er1]} {puts "Файл не содержит сертификата"exit}array set cert_parse [cert1 parse_cert]puts "Распарсенный сертификат"foreach ind [array names cert_parse] {    puts "\tcert_parse($ind)"}#Добавляем новые методыoo::define certificate {    method issuer {} {return [ my parse_dn $ret(issuer)]    }    method subject {} {return [ my parse_dn $ret(subject)]    }    method parse_dn {asnblock} {set lret {}      while {[string length $asnblock]} {        asn::asnGetSet asnblock AttributeValueAssertion        asn::asnGetSequence AttributeValueAssertion valblock        asn::asnGetObjectIdentifier valblock oidset name [::pki::_oid_number_to_name $oid]::asn::asnGetString valblock  valuelappend lret [string toupper $name]lappend lret $value      }return $lret    }    unexport parse_dn}puts "Сведения о владельце:"foreach {oid value} [cert1 subject] {    puts "\t$oid=$value"}puts "Сведения об издателе:"foreach {oid value} [cert1 issuer] {    puts "\t$oid=$value"}puts "INFO PUB KEY"foreach {oid value} [cert1 infopubkey] {    puts "\t$oid=$value"}#Создаем объект pubkeyputs "КЛАСС INFO PUB KEY"if {[catch {pubkey create pk1 $cert_parse(pubkeyinfo_hex)} er1]} {puts "НЕ PUBKEYINFO"exit}foreach {oid value} [pk1 infopubkey] {    puts "\t$oid=$value"}puts "Публичные методы класса certificate"puts "\t[info class methods certificate]"puts "Все методы класса certificate, включая приватные"puts "\t[info class methods certificate -private]"

Выполним пример example3.tcl:
$ tclsh example3.tcl minenergo.cer Loading file: minenergo.cerРаспарсенный сертификат        cert_parse(subject)        . . .        cert_parse(tbsCert)Сведения о владельце:        . . .        ST=77 г. Москва        C=RU        CN=Мин РоссииСведения об издателе:        C=RU        ST=77 Москва        . . .        CN=Тестовый удостоверяющий центрINFO PUB KEY        pkcs11id_hex=842205ac57465fd853a158544f1ea1ba1de58569        pubkey=04401dc81447918c7694a74dbe6bb4e4c10a63ca21d6b95a41ae20837deda4700f2404a0c1141d9b535b95707bb751791eb684bd09ce8f0c98d912dea947e4b8bbdb        hashkey=1 2 643 7 1 1 2 2        paramkey=id-GostR3410-2001-CryptoPro-XchA-ParamSet        type=gost        pubkey_algo=1 2 643 7 1 1 1 1Публичные методы класса certificate        subject parse_cert issuerВсе методы класса certificate, включая приватные        parse_dn subject parse_cert issuer . . .$

Отметим также, что 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$ 

Подмешивание (mix in) методов в класс


Для расширения возможность класса, прежде всего с точки зрения его функциональности, помимо наследования можно использовать так называемый метод подмешивания (mix in).
Если мы хотим распечатать сертификат в текстовом виде, то нам потребуется разбор asn-структур расширений сертификата. Это и начначение ключа сертификата, это свойства квалифицированного сертификата и многое другое. Оформим разбор расширений сертификата в отдельный класс parseexts, в котором отсутствует констуктор и деструктор:
#Класс разбора расширений сертификатаoo::class create parseexts {#Переменные с распарсенным сертификатом и его расширениями#Область данных берется их класса, к которому будем плдмешивать    variable ret    variable extcert#Подмешиваемые методы    method issuerSignTool {} {set member {"Наименование СКЗИ УЦ" "Наименование УЦ" "Сертификат СКЗИ УЦ" "Сертификат УЦ"}#Проверка наличия расширенияif {![info exists extcert(1.2.643.100.112)]} {    return [list ]}set rr [list]set iss [binary format H* [lindex $extcert(1.2.643.100.112) 1]]::asn::asnGetSequence iss iss_polfor {set i 0} {[string length $iss_pol] > 0}  {incr i} {    ::asn::asnGetUTF8String iss_pol retist    lappend rr [lindex $member $i]    lappend rr $retist}return $rr  }    method subjectSignTool {} {#Проверка наличия расширенияif {![info exists extcert(1.2.643.100.111)]} {    return [list ]}set iss [binary format H* [lindex $extcert(1.2.643.100.111) 1]]lappend rr "User CKZI"::asn::asnGetUTF8String iss retsstlappend rr $retsstreturn $rr    }    method keyUsage {} {    #keyUsageset critcert "No"array set ist [list]#Проверка наличия расширенияif {![info exists extcert(2.5.29.15)]} {    return [array get ist]}    set ku_hex [lindex $extcert(2.5.29.15) 1]if {[lindex $extcert(2.5.29.15) 0] == 1} {    set critcert "Yes"}set ku_options {"Digital signature" "Non-Repudiation" "Key encipherment" "Data encipherment" "Key agreement" "Certificate signature" "CRL signature" "Encipher Only" "Decipher Only" "Revocation list signature"}set ku [binary format H* $ku_hex]::asn::asnGetBitString ku ku_binset retku {}for {set i 0} {$i < [string length $ku_bin]}  {incr i} {    if {[string range $ku_bin $i $i] > 0 } {    lappend retku [lindex $ku_options $i]    }}array set aku [list]set aku(keyUsage) $retkuset aku(critcert) $critcertreturn [array get aku]    }}

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

Для нашего примера это будет выглядеть следующим образом:
oo::define certificate {mixin parseexts}

Полный пример использования подмешивания example4.tcl находится здесь.

source ./classpubkeyinfo.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]
if {0} {
puts Распарсенный сертификат
foreach ind [array names cert_parse] {
puts "\tcert_parse($ind)"
}
}
#Добавляем новые методы
oo::define certificate {
method issuer {} {
return [ my parse_dn $ret(issuer)]
}
method subject {} {
return [ my parse_dn $ret(subject)]
}
method parse_dn {asnblock} {
set lret {}
while {[string length $asnblock]} {
asn::asnGetSet asnblock AttributeValueAssertion
asn::asnGetSequence AttributeValueAssertion valblock
asn::asnGetObjectIdentifier valblock oid
set name [::pki::_oid_number_to_name $oid]
::asn::asnGetString valblock value
lappend lret [string toupper $name]
lappend lret $value
}
return $lret
}
unexport parse_dn
}
puts Сведения о владельце:
foreach {oid value} [cert1 subject] {
puts "\t$oid=$value"
}
puts Сведения об издателе:
foreach {oid value} [cert1 issuer] {
puts "\t$oid=$value"
}
puts INFO PUB KEY
foreach {oid value} [cert1 infopubkey] {
puts "\t$oid=$value"
}
#Класс разбора расширений сертификата
oo::class create parseexts {
#Переменная с распарсенным сертификатом
variable ret
variable extcert
method issuerSignTool {} {
set member {Наименование СКЗИ УЦ Наименование УЦ Сертификат СКЗИ УЦ Сертификат УЦ}
#Проверка наличия расширения
if {![info exists extcert(1.2.643.100.112)]} {
return [list ]
}
set rr [list]
set iss [binary format H* [lindex $extcert(1.2.643.100.112) 1]]
::asn::asnGetSequence iss iss_pol
for {set i 0} {[string length $iss_pol] > 0} {incr i} {
::asn::asnGetUTF8String iss_pol retist
lappend rr [lindex $member $i]
lappend rr $retist
}
# unset extcert(1.2.643.100.112)
return $rr
}
method subjectSignTool {} {
#Проверка наличия расширения
if {![info exists extcert(1.2.643.100.111)]} {
return [list ]
}
set iss [binary format H* [lindex $extcert(1.2.643.100.111) 1]]
lappend rr User CKZI
::asn::asnGetUTF8String iss retsst
lappend rr $retsst
# unset extcert(1.2.643.100.111)
return $rr
}
method keyUsage {} {
#keyUsage
set critcert No
array set ist [list]
#Проверка наличия расширения
if {![info exists extcert(2.5.29.15)]} {
return [array get ist]
}
set ku_hex [lindex $extcert(2.5.29.15) 1]
if {[lindex $extcert(2.5.29.15) 0] == 1} {
set critcert Yes
}
set ku_options {Digital signature Non-Repudiation Key encipherment Data encipherment Key agreement Certificate signature CRL signature Encipher Only Decipher Only Revocation list signature}
set ku [binary format H* $ku_hex]
::asn::asnGetBitString ku ku_bin
set retku {}
for {set i 0} {$i < [string length $ku_bin]} {incr i} {
if {[string range $ku_bin $i $i] > 0 } {
lappend retku [lindex $ku_options $i]
}
}
array set aku [list]
set aku(keyUsage) $retku
set aku(critcert) $critcert
return [array get aku]
}
}
oo::define certificate {
mixin parseexts
}
puts keyUsage
foreach {oid value} [cert1 keyUsage] {
puts "\t$oid=$value"
}
puts issuerSignTool
foreach {oid value} [cert1 issuerSignTool] {
puts "\t$oid=$value"
}
puts subjectSignTool
foreach {oid value} [cert1 subjectSignTool] {
puts "\t$oid=$value"
}
puts Публичные методы класса certificate
puts "\t[info class methods certificate]"
puts Все методы класса certificate, включая приватные
puts "\t[info class methods certificate -private]"
puts Принадлежность объекта cert1 классу certificate
puts "\t[info object class cert1 certificate]"
puts Принадлежность объекта cert1 классу pubkey
puts "\t[info object class cert1 pubkey]"
puts Супер классы класса certificate
puts "\t[info class superclasses certificate]"
puts Супер классы класса pubkey
puts "\t[info class superclasses pubkey]"
puts Подклассы класса certificate
puts "\t[info class subclasses certificate]"
puts Подклассы класса pubkey
puts "\t[info class subclasses pubkey]"
puts Mixin-ы класса certificate
puts "\t[info class mixins certificate]"

Результат выполнения примера:
$tclsh example4.tcl cert.cer. . .Сведения об издателе:. . .        C=RU        ST=77 Москва        L=Москва. . .        CN=Тестовый удостоверяющий центрINFO PUB KEY        pkcs11id_hex=842205ac57465fd853a158544f1ea1ba1de58569        pubkey=04401dc81447918c7694a74dbe6bb4e4c10a63ca21d6b95a41ae20837deda4700f2404a0c1141d9b535b95707bb751791eb684bd09ce8f0c98d912dea947e4b8bbdb        hashkey=1 2 643 7 1 1 2 2        paramkey=id-GostR3410-2001-CryptoPro-XchA-ParamSet        type=gost        pubkey_algo=1 2 643 7 1 1 1 1keyUsage        critcert=Yes        keyUsage={Digital signature} Non-Repudiation {Key encipherment} {Data encipherment}issuerSignTool        Наименование СКЗИ УЦ="CSP"         Наименование УЦ="Удостоверяющий центр" версии         Сертификат СКЗИ УЦ=Сертификат соответствия         Сертификат УЦ=Сертификат соответствия  subjectSignTool        User CKZI=CSP Публичные методы класса 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        ::certificateMixin-ы класса certificate        ::parseexts

Добавление/переопределение методов у объектов


В принципе этого материала достаточно, чтобы начать использовать ООП в Tcl. Но мы упомянули и то, что в TcllOO можно динамически конструировать не только сам класс, то и экземпляры класса, т.е. объекты. На одной из таких возможностей хотелось бы остановится.
Для этого добавим в класс certificate еще один метод для подписания этим сертификатом некоторого документа:
#Метод для Подписания документаoo::define certificate {method signDoc {doc} {set sign "Здесь должна находиться подпись документа  $doc"#Счетчик подписанных документовmy variable signedDoc#Количество подписанных документовincr signedDocreturn [list $signedDoc $sign]}}

При вызове этого метода должно происходить подписание документа и увеличение счетчика подписанных документов на единицу. В качестве результата работы этого метода возвращается общее число подписанных на данный момент документов и сама подпись:
. . . set doc "Подпись1"puts "Подписание документа $doc"foreach {count sign} [cert1 signDoc $doc] {    puts "\tПодписано документов на данный момент=$count"    puts "\tПодпись документа=\"$sign\""}. . .

Результат будет выглядеть так:
. . .Подписание документа Подпись1        Подписано документов на данный момент=1        Подпись документа="Здесь должна находиться подпись документа  Подпись1". . .

Сам алгорит подписи здесь не рассматривается, но его можно найти в утилите cryptoarmpkcs:

image

А теперь представим, что владелец сертификата убывает в отпуск. Он знает, что в отпуске он будет отдыхать и не в коем случае не будет работать с документами и тем более что-либо подписывать. Он хочет отозвать сертификат, а когда вернется восстановить его действие. Для этих целей служит следующая функция:
#Процедура отзыва сертификата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 находится здесь
source ./classpubkeyinfo.tclsource ./classparsecert.tcl#Примерset file [lindex $argv 0]if {$argc != 1 || ![file exists $file]} {    puts "File $file not exist"    puts "Usage: tclsh example1 <файл с сертификатом>"    exit}puts "Loading file: $file"set fd [open $file]chan configure $fd -translation binaryset data [read $fd]close $fdif {$data == "" } {    puts "Bad file with certificate=$file"    usage 1    exit}if {[catch {certificate create cert1 $data} er1]} {puts "НЕ СЕРТИФИКАТ"exit}array set cert_parse [cert1 parse_cert]#parray cert_parseif {0} {puts "Распарсенный сертификат"foreach ind [array names cert_parse] {    puts "\tcert_parse($ind)"}}#Добавляем новые методыoo::define certificate {    method issuer {} {return [ my parse_dn $ret(issuer)]    }    method subject {} {return [ my parse_dn $ret(subject)]    }    method parse_dn {asnblock} {set lret {}      while {[string length $asnblock]} {        asn::asnGetSet asnblock AttributeValueAssertion        asn::asnGetSequence AttributeValueAssertion valblock        asn::asnGetObjectIdentifier valblock oidset name [::pki::_oid_number_to_name $oid]::asn::asnGetString valblock  valuelappend lret [string toupper $name]lappend lret $value      }return $lret    }    unexport parse_dn}puts "Сведения о владельце:"foreach {oid value} [cert1 subject] {    puts "\t$oid=$value"}puts "Сведения об издателе:"foreach {oid value} [cert1 issuer] {    puts "\t$oid=$value"}puts "INFO PUB KEY"foreach {oid value} [cert1 infopubkey] {    puts "\t$oid=$value"}#Метод для Подписания документаoo::define certificate {method signDoc {doc} {set sign "Здесь должна находиться подпись документа  $doc"#Счетчик подписанных документовmy variable signedDoc#Количество подписанных документовincr signedDocreturn [list $signedDoc $sign]}}set doc "Подпись1"puts "Подписание документа $doc"foreach {count sign} [cert1 signDoc $doc] {    puts "\tПодписано документов на данный момент=$count"    puts "\tПодпись документа=\"$sign\""}set doc "Подпись2"puts "Подписание документа $doc"foreach {count sign} [cert1 signDoc $doc] {    puts "\tПодписано документов на данный момент=$count"    puts "\tПодпись документа=\"$sign\""}#Процедура отзыва сертификата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        }    }}#Клонируем объектoo::copy cert1 cert11#Отзыв сертификатаputs "Отзыв сертификата"revoke cert1foreach doc "Подпись3 подпись4" {    puts "Попытка подписать документ $doc"    foreach {count sign} [cert1 signDoc $doc] {puts "\tПопыток несанкционированного доступа=$count"puts "\tПодпись документа=\"$sign\""    }}#Для клонированного объекта отзыв не действуетforeach doc "Подпись3к подпись4к" {    puts "Попытка подписать документ $doc клонированным объектом"    foreach {count sign} [cert11 signDoc $doc] {    puts "\tПодписано документов на данный момент=$count"    puts "\tПодпись документа=\"$sign\""    }}#Восстанавливаем действие сертификатаforeach {count info} [cert1 unrevoke] {    puts "Действие сертификата восстанвлено"    puts "\tЗа время его отзыва было $count попытки несанкционированного досьупа"}foreach doc "\"Подпись после восстановления\"" {    puts "Попытка подписать документ $doc"    foreach {count sign} [cert1 signDoc $doc] {puts "\tПодписано документов на данный момент=$count"puts "\tПодпись документа=\"$sign\""    }}

Ниже приведен фрагмент выполнения примера example5.tcl:
. . . Подписание документа Подпись1        Подписано документов на данный момент=1        Подпись документа="Здесь должна находиться подпись документа  Подпись1"Подписание документа Подпись2        Подписано документов на данный момент=2        Подпись документа="Здесь должна находиться подпись документа  Подпись2"Отзыв сертификатаПопытка подписать документ Подпись3        Попыток несанкционированного доступа=1        Подпись документа="Сертификат временно отозван. Не пытайтесь им подписывать!"Попытка подписать документ подпись4        Попыток несанкционированного доступа=2        Подпись документа="Сертификат временно отозван. Не пытайтесь им подписывать!"Действие сертификата восстанвлено        За время его отзыва было 2 попытки несанкционированного досьупаПопытка подписать документ Подпись после восстановления        Подписано документов на данный момент=3        Подпись документа="Здесь должна находиться подпись документа  Подпись после восстановления". . .

Упомянем еще один оператор. Это оператор клонирования объекта:
oo::copy <идентификатор исходного объекта> <идентификатор клона>
Говорить и писать об ООП на TclOO можно долго и долго.
Еще интересней его исследовать.
Подробнее..

Поддержка токенов PKCS11 с ГОСТ-криптографией в Python. Часть I

20.02.2021 18:04:03 | Автор: admin
imageПоддержка криптографических токенов 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, в которой в качестве криптодвижка используется именно этот пакет:

image

Первым желанием было портировать этот модуль в среду 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 библиотеки PKCS11PyArg_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$ python3Python 3.7.9 (default, Feb  1 2021, 16:55:33) [GCC 8.4.0] on linuxType "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$ python3Python 3.7.9 (default, Feb  1 2021, 16:55:33) [GCC 8.4.0] on linuxType "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=RUGN=Имя ОтчествоCN=ООО КОМПАНИЯ E=info@ooo.orgOGRN=xxxxxxxxxxxxSNILS=xxxxxxxxxxxINN=xxxxxxxxxxxxtitle=Генеральный директорOU=0O=ООО КОМПАНИЯ street=119136 г. Москва L=МоскваST=77 г. МоскваSN=Харитонов>>> 

Теперь можно переходить к работе с токенами.

III. Управление токенами PKCS#11


Для тестирования функций управления подойдет любой токен PKCS#11, даже токен без поддержки какой-либо криптографии, например RuTokenLite. Но поскольку мы ведём речь о российской криптографии, то целесообразно сразу иметь токен с поддержкой российской криптографии. Здесь мы имеем в виду ГОСТ Р 34.10-2012 и ГОСТ Р 34.11-2012. Это может быть как аппаратный токен, например RuTokenECP-2.0, так и программные или облачные токены.
Установить программный токен или получить доступ к облачному токену можно, воспользовавшись утилитой cryptoarmpkcs.
Скачать утилиту cryptoarmpkcs можно здесь.
После запуска утилиты необходимо зайти на вкладку Создать токены:



На вкладке можно найти инструкции для получения токенов.
Итак, у нас токен и библиотека для работы с ним. После загрузки модуля 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 linuxType "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$ python3Python 3.7.9 (default, Feb  1 2021, 16:55:33) [GCC 8.4.0] on linuxType "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:

image

В папке tests проекта pyp11 лежат три теста test1_0_inittoken.py, test1_1_inituserpin.py и test1_2_change_userpin, которые наглядно демонстрируют инициализацию токена. Выполнять их надо в порядке перечисления.
Было бы несправедливо не показать инициализацию токена с использованием уже упоминавшегося пакета PyKCS11:
$ python3Python 3.7.9 (default, Feb  1 2021, 16:55:33) [GCC 8.4.0] on linuxType "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:
$ python3Python 3.7.9 (default, Feb  1 2021, 16:55:33) [GCC 8.4.0] on linuxType "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)... 
Криптографические механизмы
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= pkcs0pkcs11_slotid= 0hobj_pubkey= hobj0100000000000000hobj_privkey= hobj0200000000000000pkcs11_id= dd22fe35aeb7eb2ebcad7199b117eb3a7b5f5813pkcs11_label= KeyGost256pubkey= 4c2ed60bc5771b2a6616af58c8dd202b9463dde9bd1de028335e718634761e360a25b2f337c2e67c28402cd49fff4f708130a80dc479301b21ceb9324c47464bpubkey_algo= 1 2 643 7 1 1 1 1pubkeyinfo= 302106082a85030701010101301506092a850307010201010306082a8503070101020203430004404c2ed60bc5771b2a6616af58c8dd202b9463dde9bd1de028335e718634761e360a25b2f337c2e67c28402cd49fff4f708130a80dc479301b21ceb9324c47464btype= 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$ python3Python 3.7.9 (default, Feb  1 2021, 16:55:33) [GCC 8.4.0] on linuxType "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 pyp11print('Проверка подписи сертификата')#Библиотека для токена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 в hexcertCA_hex = bytes(certCA).hex()#Читаем сертификат пользователя в DER-кодировке из файлаwith open(fileUser, "rb") as f:    certHabr = f.read()#Упаковываем der в hexcertHabr_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 пользователь в binarytbs_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_IDlabcert = '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 sysimport timeimport pyp11print('Список сертификатов токена')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.listobjectslm = 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.
Подробнее..

Поддержка токенов PKCS11 с ГОСТ-криптографией в Python. Часть II Объекты класса Token

18.03.2021 18:13:59 | Автор: admin
imageВ предыдущей статье был представлен модуль pyp11, написанный на языке Си и обеспечивающий поддержку токенов PKCS#11 с российской криптографией. В этой статье будет рассмотрен класс Token, который позволит упростить использование функционала модуля pyp11 в скриптах, написанных на Python-е. Отметим, что в качестве прототипа этого класса был взят класс token, написанный на TclOO и который используется в утилите cryptoarmpkcs:


Прототип класса Token
oo::class create token {  variable libp11  variable handle  variable infotok  variable pintok  variable nodet#Конструктор  constructor {handlelp11 labtoken slottoken} {    global pass    global yespas    set handle $handlelp11    set slots [pki::pkcs11::listslots $handle]    array set infotok []    foreach slotinfo $slots {      set slotflags [lindex $slotinfo 2]      if {[lsearch -exact $slotflags TOKEN_PRESENT] != -1} {        if {[string first $labtoken [lindex $slotinfo 1]] != -1} {          set infotok(slotlabel) [lindex $slotinfo 1]          set infotok(slotid) [lindex $slotinfo 0]          set infotok(slotflags) [lindex $slotinfo 2]          set infotok(token) [lindex $slotinfo 3]          #Берется наш токен          break        }      }    }    #Список найденных токенов в слотах    if {[llength [array names infotok]] == 0 } {      error "Constructor: Token not present for library   : $handle"    }    #Объект какого токена    set nodet [dict create pkcs11_handle $handle]    dict set nodet pkcs11_slotid $infotok(slotid)    set tit "Введите PIN-код для токене $infotok(slotlabel)"    set xa [my read_password $tit]    if {$xa == "no"} {      error "Вы передумали вводить PIN для токена $infotok(slotlabel)"    }    set pintok $pass    set pass ""    set rr [my login ]    if { $rr == 0 } {      unset pintok      error "Проверьте PIN-код токена $infotok(slotlabel)."    } elseif {$rr == -1} {      unset pintok      error "Отсутствует токен."    }    my logout  }#Методы класса  method infoslot {} {    return [array get infotok]  }  method listcerts {} {    array set lcerts []    set certsder [pki::pkcs11::listcertsder $handle $infotok(slotid)]    #Перебираем сертификаты    foreach lc $certsder {      array set derc $lc      set lcerts($derc(pkcs11_label)) [list $derc(cert_der) $derc(pkcs11_id)]      #parray derc    }    return [array get lcerts]  }  method read_password {tit} {    global yespas    global pass    set tit_orig "$::labpas"    if {$tit != ""} {      set ::labpas "$tit"    }    tk busy hold ".st.fr1"    tk busy hold ".st.fr3"    #place .topPinPw -in .st.fr1.fr2_certs.labCert  -relx 1.0 -rely 3.0 -relwidth 3.5    place .topPinPw -in .st.labMain  -relx 0.35 -rely 5.0 -relwidth 0.30    set yespas ""    focus .topPinPw.labFrPw.entryPw    vwait yespas    catch {tk busy forget ".st.fr1"}    catch {tk busy forget ".st.fr3"}    if {$tit != ""} {      set ::labpas "$tit_orig"    }    place forget .topPinPw    return $yespas  }  unexport read_password  method rename {type ckaid newlab} {    if {$type != "cert" && $type != "key" && $type != "all"} {      error "Bad type for rename "    }    set uu $nodet    lappend uu "pkcs11_id"    lappend uu $ckaid    lappend uu "pkcs11_label"    lappend uu $newlab    if { [my login ] == 0 } {      unset uu      return 0    }    pki::pkcs11::rename $type $uu    my logout    return 1  }  method changeid {type ckaid newid} {    if {$type != "cert" && $type != "key" && $type != "all"} {      error "Bad type for changeid "    }    set uu $nodet    lappend uu "pkcs11_id"    lappend uu $ckaid    lappend uu "pkcs11_id_new"    lappend uu $newid    if { [my login ] == 0 } {      unset uu      return 0    }    pki::pkcs11::rename $type $uu    my logout    return 1  }  method delete {type ckaid} {    if {$type != "cert" && $type != "key" && $type != "all" && $type != "obj"} {      error "Bad type for delete"    }    set uu $nodet    lappend uu "pkcs11_id"    lappend uu $ckaid    my login    ::pki::pkcs11::delete $type $uu    my logout    return 1  }  method deleteobj {hobj} {    set uu $nodet    lappend uu "hobj"    lappend uu $hobj#tk_messageBox -title "class deleteobj" -icon info -message "hobj: $hobj\n" -detail "$uu"    return [::pki::pkcs11::delete obj $uu ]  }  method listmechs {} {    set llmech [pki::pkcs11::listmechs $handle $infotok(slotid)]    return $llmech  }  method pubkeyinfo {cert_der_hex} {    array set linfopk [pki::pkcs11::pubkeyinfo $cert_der_hex $nodet]    return [array get linfopk]  }  method listobjects {type} {    if {$type != "cert" && $type != "pubkey" && $type != "privkey" && $type != "all" && $type != "data"} {      error "Bad type for listobjects "    }    set allobjs [::pki::pkcs11::listobjects $handle $infotok(slotid) $type]    return $allobjs  }  method importcert {cert_der_hex cka_label} {    set uu $nodet    dict set uu pkcs11_label $cka_label    if {[catch {set pkcs11id [pki::pkcs11::importcert $cert_der_hex $uu]} res] } {      error "Cannot import this certificate:$res"      #          return 0    }    return $pkcs11id  }  method login {} {    set wh 1    set rl -1    while {$wh == 1} {      if {[catch {set rl [pki::pkcs11::login $handle $infotok(slotid) $pintok]} res]} {        if {[string first "SESSION_HANDLE_INVALID" $res] != -1} {          pki::pkcs11::closesession $handle          continue        } elseif {[string first "TOKEN_NOT_PRESENT" $res] != -1} {          set wh 0          continue        }      }      break    }    if {$wh == 0} {      return -1    }    return $rl  }  method logout {} {    return [pki::pkcs11::logout $handle $infotok(slotid)]  }  method keypair {typegost parkey} {    my login    set skey [pki::pkcs11::keypair $typegost $parkey $nodet]    my logout    return $skey  }  method digest {typehash source} {    return [pki::pkcs11::digest $typehash $source $nodet]  }  method signkey {ckm digest hobj_priv} {    set uu $nodet    dict set uu hobj_privkey $hobj_priv    my login    set ss [pki::pkcs11::sign $ckm $digest $uu]    my logout    return $ss  }  method signcert {ckm digest pkcs11_id} {    set uu $nodet    dict set uu pkcs11_id $pkcs11_id    my login    set ss  [pki::pkcs11::sign $ckm $digest $uu]    my logout    return $ss  }  method verify {digest signature asn1pubkey} {    set uu $nodet    dict set uu pubkeyinfo $asn1pubkey    return [pki::pkcs11::verify $digest $signature $uu]  }  method tokenpresent {} {    set slots [pki::pkcs11::listslots $handle]    foreach slotinfo $slots {      set slotid [lindex $slotinfo 0]      set slotlabel [lindex $slotinfo 1]      set slotflags [lindex $slotinfo 2]      if {[lsearch -exact $slotflags TOKEN_PRESENT] != -1} {        if {infotok(slotlabel) == $slotlabel && $slotid == $infotok(slotid)} {          return 1        }      }    }    return 0  }  method setpin {type tpin newpin} {    if {$type != "user" && $type != "so"} {      return 0    }    if {$type == "user"} {      if {$tpin != $pintok} {        return 0      }    }    set ret [::pki::pkcs11::setpin  $handle $infotok(slotid) $type $tpin $newpin]    if {$type == "user"} {      if {$ret} {        set pitok $newpin      }    }    return $ret  }  method inituserpin {sopin upin} {    set ret [::pki::pkcs11::inituserpin $handle $infotok(slotid) $sopin $upin]    return $ret  }  method importkey {uukey} {    set uu $nodet    append uu " $uukey"    my login    if {[catch {set impkey [pki::pkcs11::importkey $uu ]} res] } {        set impkey 0    }    my logout    return $impkey  }#Деструктор  destructor {    variable handle    if {[info exists pintok]} {      my login    }    #    ::pki::pkcs11::unloadmodule  $handle  }}

Класс Token, как и любой класс в Python, включает конструктор, методы и деструктор. Конструктор и деструктор это те же методы, только с предопределёнными именами. Конструктор имеет имя __init__, а деструктор имя __del__. Объявление конструктора и деструктора можно опускать. И в классе Token мы опустим объявление деструктора, а вот конструктор будет необходим. Конструктор будет создавать экземпляр класса Token для конкретного токена с конкретными атрибутами.

I. Конструктор класса Token


Итак, конструктор класса Token выглядит следующим образом:
import sysimport pyp11class Token:  def __init__ (self, handlelp11, slottoken, serialnum):    flags = ''    self.pyver = sys.version[0]    if (self.pyver == '2'):        print ('Только для python3')        quit()#Сохраняем handle библиотеки PKCS#11    self.handle = handlelp11#Сохраняем номер слота с токеном    self.slotid = slottoken#Сохраняем серийный номер токена    self.sn = serialnum#Проверяем наличие в указанном слоте токена с заданным серийным номером    ret, stat = self.tokinfo()#Проверяем код возврата    if (stat != ''):#Возвращаем информацию об ошибке        self.returncode = stat        return#Экземпляр класса (объект) успешно создан


Параметрами конструктора (метода __init__) являются (помимо обязательного self) handle библиотеки токена (handlelp11), номер слота (slottoken), в котором должен находиться токен, и серийный номер токена (serialnum).
Для получения handle библиотеки pkcs#11, номеров слотов и информации о находящихся в них токенах можно использовать следующий скрипт:
#!/usr/bin/python3import sysimport pyp11from Token import Tokendef listslots (handle):    slots = pyp11.listslots(aa)    i = 0    lslots = []    for v in slots:        for f in v[2]:        if (f == 'TOKEN_PRESENT'):                i = 1                lslots.append(v)                break    i += 1    return (lslots)#Библиотеки для Linux#Программный токенlib = '/usr/local/lib64/libls11sw2016.so'#Облачный токен#lib = '/usr/local/lib64/libls11cloud.so'#Аппаратный токен#lib = '/usr/local/lib64/librtpkcs11ecp_2.0.so'#Библиотеки для Windows#lib='C:\Temp\ls11sw2016.dll'try:#Вызываем команду загрузки библиотеки и получаем её handle (дескриптор библиотеки)    aa = pyp11.loadmodule(lib)    print('Handle библиотеки ' + lib + ': ' + aa)except:    print('Except load lib: ')    e = sys.exc_info()[1]    e1 = e.args[0]#Печать ошибки    print (e1)    quit()#Список слотовslots = listslots(aa)i = 0for v in slots:    for f in v[2]:        if (f == 'TOKEN_PRESENT'):        if (i == 0):            print ('\nИнформация о токенах в слотах\n')        it = v[3]        print ('slotid=' + str(v[0]))        print ('\tFlags=' + str(v[2]))        print ('\tLabel="' + it[0].strip() + '"')        print ('\tManufacturer="' + it[1].strip() + '"')        print ('\tModel="' + it[2].strip() + '"')        print ('\tSerialNumber="' + it[3].strip() + '"')        i = 1        break    i += 1pyp11.unloadmodule(aa)if (i == 0):    print ('Нет ни одного подключенного токена. Вставьте токен и повторите операцию')quit()

Если с библиотекой и слотом всё ясно, то с серийным номером токена может возникнуть вопрос а зачем этот параметр нужен и почему именно он, а, например, не метка токена. Сразу оговоримся, что это принципиально для извлекаемых токенов, когда злоумышленником (или случайно) один токен в слоте будет заменён другим токеном. Более того, различные экземпляры токена могут иметь одинаковые метки. И наконец, токен может быть еще не проинициализирован или владелец будет его переинициализировать, в частности, сменит метку токена. Теоретически даже серийный номер не гарантирует его идентичность, оптимально учитывать всю информацию о токене (серийный номер, модель, производитель). В задачи конструктора и входит сохранение в переменных создаваемого экземпляра класса аргументов объекта токен:
...#Сохраняем handle библиотеки PKCS#11    self.handle = handlelp11#Сохраняем номер слота с токеном    self.slotid = slottoken#Сохраняем серийный номер токена    self.sn = serialnum...

Проверкой наличия указанного токена в указанном слоте занимается метод tokinfo(), определенный в данном классе.
Метод tokinfo возвращает два значения (см. выше в конструкторе):
#Проверяем наличие в указанном слоте токена с заданным серийным номером    ret, stat = self.tokinfo()

В первой переменной (ret) содержится результат выполнения метода, а во второй (stat) информация о том, как завершилось выполнение метода. Если вторая переменная пуста, то метод tokinfo успешно выполнился. Если вторая переменная не пуста, то выполнение метода завершилось с ошибкой. Информация об ошибке будет находиться в этой переменной. При обнаружении ошибки выполнения метода self.tokinfo конструктор записывает её в переменную returncode:
#Проверяем наличие в указанном слоте токена с заданным серийным номером    ret, stat = self.tokinfo()#Проверяем код возврата    if (stat != ''):#Возвращаем информацию об ошибке в переменной returncode        self.returncode = stat        return

После создания объекта (экземпляра класса) необходимо проверить значение переменной returncode, чтобы быть уверенным в том, что объект для указанного токена создан:
#!/usr/bin/python3import sysimport pyp11from Token import Token#Выбираем библиотеку#Аппаратный токенlib = '/usr/local/lib64/librtpkcs11ecp_2.0.so'try:    aa = pyp11.loadmodule(lib)except:    e = sys.exc_info()[1]    e1 = e.args[0]    print (e1)    quit()#Серийный номер токенаsn = '9999999999999999'slot = 110#Создаем объект токенаt1 = Token(aa, slot, sn)#Проверка переменной returncodeif (t1.returncode != ''):#Объект создан с ошибкой    print (t1.returncode)#Уничтожение объекта    del t1#Завершение скрипта    quit()#объект успешно создан. . .

Если обнаружена ошибка при создании объекта, то целесообразно этот объект уничтожить:
del <идентификатор объекта>

II. Архитектура методов в классе Token


Главным принципом при написании методов было то, чтобы обработка исключений была внутри методов, а информация об исключениях (ошибках) возвращалась в текстовом виде. Исходя из этого все методы возвращают два значения: собственно результат выполнения и информацию об ошибке. Если ошибок нет, то второе значение пустое. Мы это уже видели на примере использования метода tokinfo в конструкторе. А вот и сам код метода tokinfo:
  def tokinfo(self):    status = ''#Получаем список слотов    try:        slots = pyp11.listslots(self.handle)    except:#Проблемы с библиотекой токена        e = sys.exc_info()[1]        e1 = e.args[0]        dd = ''        status = e1        return (dd, status)    status = ''#Ищем заданный слот с указанным  токеном#Перебираем слоты    for v in slots:#Ищем заданный слот            if (v[0] != self.slotid):                status = "Ошибочный слот"                continue            self.returncode = ''#Список флагов текущего слота            self.flags = v[2]#Проверяем наличие в стоке токена            if (self.flags.count('TOKEN_PRESENT') !=0):#Проверяем серийный номер токена                tokinf = v[3]                sn = tokinf[3].strip()                if (self.sn != sn):                    status = 'Серийный номер токена=\"' + sn + '\" не совпадает с заданным \"' + self.sn + '\"'                    dd = ''                    return (dd, status)                status = ''                break            else:                dd = ''                status = "В слоте нет токена"                return (dd, status)    tt = tokinf    dd = dict(Label=tt[0].strip())    dd.update(Manufacturer=tt[1].strip())    dd.update(Model=tt[2].strip())    dd.update(SerialNumber=tt[3].strip())    self.infotok = dd#Возвращаемые значения    return (dd, status)

Полное описание класса Token находится здесь.
import sysimport pyp11class Token:  def __init__ (self, handlelp11, slottoken, serialnum):    flags = ''    self.pyver = sys.version[0]    if (self.pyver == '2'):        print ('Только для python3')        quit()#Сохраняем handle библиотеки PKCS#11    self.handle = handlelp11#Сохраняем номер слота с токеном    self.slotid = slottoken#Сохраняем серийный номер токена    self.sn = serialnum#Проверяем наличие в указанном слоте с токена с заданным серийным номером    ret, stat = self.tokinfo()#Проверяем код возврата    if (stat != ''):#Возвращаем информацию об ошибке        self.returncode = stat        return#Экземпляр класса (объект) успешно создан  def tokinfo(self):    status = ''#Получаем список слотов    try:        slots = pyp11.listslots(self.handle)    except:#Проблемы с библиотекой токена        e = sys.exc_info()[1]        e1 = e.args[0]        dd = ''        status = e1        return (dd, status)    status = ''#Ищем заданный слот с указанным  токеном#Перебираем слоты    for v in slots:#Ищем заданный слот            if (v[0] != self.slotid):                status = "Ошибочный слот"                continue            self.returncode = ''#Список флагов текущего слота            self.flags = v[2]#Проверяем наличие в стоке токена            if (self.flags.count('TOKEN_PRESENT') !=0):#Проверяем серийный номер токена                tokinf = v[3]                sn = tokinf[3].strip()                if (self.sn != sn):                    status = 'Серийный номер токена=\"' + sn + '\" не совпадает с заданным \"' + self.sn + '\"'                    dd = ''                    return (dd, status)                status = ''                break            else:                dd = ''                status = "В слоте нет токена"                return (dd, status)    tt = tokinf    dd = dict(Label=tt[0].strip())    dd.update(Manufacturer=tt[1].strip())    dd.update(Model=tt[2].strip())    dd.update(SerialNumber=tt[3].strip())    self.infotok = dd    return (dd, status)  def listcerts(self):    try:        status = ''        lcerts = pyp11.listcerts(self.handle, self.slotid)    except:#Проблемы с библиотекой токена        e = sys.exc_info()[1]        e1 = e.args[0]        lcerts = ''        status = e1    return (lcerts, status)  def listobjects(self, type1, value = '' ):    try:        status = ''        if (value == ''):        lobjs = pyp11.listobjects(self.handle, self.slotid, type1)        else:        lobjs = pyp11.listobjects(self.handle, self.slotid, type1, value)    except:#Проблемы с библиотекой токена        e = sys.exc_info()[1]        e1 = e.args[0]        lobjs = ''        status = e1    return (lobjs, status)  def rename(self, type, pkcs11id, label):    try:        status = ''        dd = dict(pkcs11_id=pkcs11id, pkcs11_label=label)        ret = pyp11.rename(self.handle, self.slotid, type, dd)    except:#Проблемы с библиотекой токена        e = sys.exc_info()[1]        e1 = e.args[0]        ret = ''        status = e1    return (ret, status)  def changeckaid(self, type, pkcs11id, pkcs11idnew):    try:        status = ''        dd = dict(pkcs11_id=pkcs11id, pkcs11_id_new=pkcs11idnew)        ret = pyp11.rename(self.handle, self.slotid, type, dd)    except:#Проблемы с библиотекой токена        e = sys.exc_info()[1]        e1 = e.args[0]        ret = ''        status = e1    return (ret, status)  def login(self, userpin):    try:        status = ''        bb = pyp11.login (self.handle, self.slotid, userpin)    except:        e = sys.exc_info()[1]        e1 = e.args[0]        bb = 0        status = e1    return (bb, status)  def logout(self):    try:        status = ''        bb = pyp11.logout (self.handle, self.slotid)    except:        e = sys.exc_info()[1]        e1 = e.args[0]        bb = 0        status = e1    return (bb, status)  def keypair(self, typek, paramk, labkey):#Параметры для ключей    gost2012_512 = ['1.2.643.7.1.2.1.2.1', '1.2.643.7.1.2.1.2.2', '1.2.643.7.1.2.1.2.3']    gost2012_256 = ['1.2.643.2.2.35.1', '1.2.643.2.2.35.2',  '1.2.643.2.2.35.3',  '1.2.643.2.2.36.0', '1.2.643.2.2.36.1', '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', '1.2.643.7.1.2.1.1.4']    gost2001 = ['1.2.643.2.2.35.1', '1.2.643.2.2.35.2',  '1.2.643.2.2.35.3',  '1.2.643.2.2.36.0', '1.2.643.2.2.36.1']#Тип ключа    typekey = ['g12_256', 'g12_512', 'gost2001']    genkey = ''    if (typek == typekey[0]):    gost = gost2012_256    elif (typek == typekey[1]):    gost = gost2012_512    elif (typek == typekey[2]):    gost = gost2001    else:    status = 'Неподдерживаемый тип ключа'    return (genkey, status)    if (gost.count(paramk) == 0) :    status = 'Неподдерживаемые параметры ключа'    return (genkey, status)    try:#Ошибок нет, есть ключевая пара    status = ''    genkey = pyp11.keypair(self.handle, self.slotid, typek, paramk, labkey)    except:#Не удалось создать ключевую пару    e = sys.exc_info()[1]    e1 = e.args[0]    print (e1)#Возвращаеи текст ошибки в словаре    status = e1    return (genkey, status)     def digest(self, typehash, source):#Считаем хэш    try:        status = ''        digest_hex = pyp11.digest (self.handle, self.slotid, typehash, source)    except:    e = sys.exc_info()[1]    e1 = e.args[0]#Возвращаеи текст ошибки в словаре    status = e1    digest_hex = ''    return (digest_hex, status)#Формирование подписи  def sign(self, ckmpair, digest_hex, idorhandle):#Для подписи можно использовать CKA_ID или handle закрытого ключа    try:        status = ''        sign_hex = pyp11.sign(self.handle, self.slotid, ckmpair, digest_hex, idorhandle)    except:    e = sys.exc_info()[1]    e1 = e.args[0]#Возвращаеи текст ошибки в словаре    status = e1    sign_hex = ''    return (sign_hex, status)#Проверка подписи  def verify(self, digest_hex, sign_hex, pubkeyinfo):#Для подписи можно использовать CKA_ID или handle закрытого ключа    try:        status = ''        verify = pyp11.verify(self.handle, self.slotid, digest_hex, sign_hex, pubkeyinfo)    except:    e = sys.exc_info()[1]    e1 = e.args[0]#Возвращаеи текст ошибки в status    verify = 0    status = e1    return (verify, status)#Инициализировать токен  def inittoken(self, sopin, labtoken):    try:        status = ''        dd = pyp11.inittoken (self.handle, self.slotid, sopin, labtoken)    except:    e = sys.exc_info()[1]    e1 = e.args[0]#Возвращаеи текст ошибки в status    dd = 0    status = e1    return (dd, status)#Инициализировать пользовательский PIN-код  def inituserpin(self, sopin, userpin):    try:        status = ''        dd = pyp11.inituserpin (self.handle, self.slotid, sopin, userpin)    except:    e = sys.exc_info()[1]    e1 = e.args[0]#Возвращаеи текст ошибки в status    dd = 0    status = e1    return (dd, status)#Сменить пользовательский PIN-код  def changeuserpin(self, oldpin, newpin):    try:        status = ''        dd = pyp11.setpin (self.handle, self.slotid, 'user', oldpin, newpin)    except:    e = sys.exc_info()[1]    e1 = e.args[0]#Возвращаеи текст ошибки в status    dd = 0    status = e1    self.closesession ()    return (dd, status)  def closesession(self):    try:        status = ''        dd = pyp11.closesession (self.handle)    except:    e = sys.exc_info()[1]    e1 = e.args[0]#Возвращаеи текст ошибки в status    dd = 0    status = e1    return (dd, status)  def parsecert(self, cert_der_hex):    try:        status = ''        dd = pyp11.parsecert (self.handle, self.slotid, cert_der_hex)    except:#Не удалось разобрать сертификат    e = sys.exc_info()[1]    e1 = e.args[0]#Возвращаеи текст ошибки в status    dd = ''    status = e1    return (dd, status)  def importcert(self, cert_der_hex, labcert):    try:        status = ''        dd = pyp11.importcert (self.handle, self.slotid, cert_der_hex, labcert)    except:    e = sys.exc_info()[1]    e1 = e.args[0]#Возвращаеи текст ошибки в status    dd = ''    status = e1    return (dd, status)  def delobject(self, hobject):    try:        status = ''        hobjc = dict(hobj=hobject)        dd = pyp11.delete(self.handle, self.slotid, 'obj', hobjc)    except:    e = sys.exc_info()[1]    e1 = e.args[0]#Возвращаеи текст ошибки в status    dd = ''    status = e1    return (dd, status)  def delete(self, type, pkcs11id):    if (type == 'obj'):        dd = ''        status = 'delete for type obj use nethod delobject'        return (dd, status)    try:        status = ''        idobj = dict(pkcs11_id=pkcs11id)        dd = pyp11.delete(self.handle, self.slotid, type, idobj)    except:    e = sys.exc_info()[1]    e1 = e.args[0]#Возвращаеи текст ошибки в status    dd = ''    status = e1    return (dd, status)  def listmechs(self):    try:        status = ''        dd = pyp11.listmechs (self.handle, self.slotid)    except:#Не удалось получить список механизмов токена    e = sys.exc_info()[1]    e1 = e.args[0]#Возвращаеи текст ошибки в status    dd = ''    status = e1    return (dd, status)


Рассмотрим импользование функционала модуля pyp11 и аналогичных операторов с использованием класса Token.
В последнем случае необходимо будет создать и объект токена:
<дескриптор объекта> = Token(<дескриптор библиоткети>, <номер слота>, <серийный номер>)if (<дескриптор объекта>.returncode != ''):   print('Ошибка при создании объекта:')#Печать ошибки   print(<дескриптор объекта>.returncode)#Уничтожение объекта   del <дескриптор объекта>   quit()

Начнем с инициализации токена:
try:    ret = pyp11.inittoken (<дескриптор библиоткети>, <номер слота>, <SO-PIN>, <метка токена>)except:#Не удалось проинициализировать токен    e = sys.exc_info()[1]    e1 = e.args[0]    print (e1)    quit()

Аналогичный код при использовании класса Token выглядит так (идентификатор объекта t1):
ret, stat = t1.inittoken(<SO-PIN>, <метка токена>)#Проверка корретности инициализацииif (stat != ''):   print('Ошибка при инициализации токена:')#Печать ошибки   print(stat)   quit()  

Далее мы просто дадим соответствие основных операторов модуля pyp11 и методов класса Token без обработки исключений и ошибок:
<handle> := <дескриптор библиотеки pkcs11><slot> := <дескриптор слота с токеном><error> := <переменная с текстом ошибки><ret> := <результат выполнения оператора><cert_der_hex> := <сертификат в DER-формате в HEX-кодировке>=================================================#Инициализация пользовательского PIN-кода<ret> = pyp11.inituserpin (<handle>, <slot>, <SO-PIN>, <USER-PIN>)<ret>, <error> = <идентификатор объекта>.inituserpin (<SO-PIN>, <USER-PIN>)#Смена USER-PIN кода<ret> = pyp11.setpin (<handle>, <slot>, 'user', <USER-PIN старый>, <USER-PIN новый>)<ret>, <error> = t1.changeuserpin (<USER-PIN старый>, <USER-PIN новый>)#Смена SO-PIN кода<ret> = pyp11.setpin (<handle>, <slot>, 'so', <SO-PIN старый>, <SO-PIN новый>)<ret>, <error> = t1.changesopin (<SO-PIN старый>, <SO-PIN новый>)#Login<ret> = pyp11.login (<handle>, <slot>, <USER-PIN>)<ret>, <error> = t1.login (<USER-PIN>)#Logout<ret> = pyp11.logout (<handle>, <slot>)<ret>, <error> = t1.logout ()#Закрытие сессии<ret> = pyp11.closesession (<handle>)<ret>, <error> = t1.closesession ()#Список сертификатов на токене<ret> = pyp11.listcerts (<handle>, <slot>)<ret>, <error> = t1.listcerts ()#Список объектов на токене<ret> = pyp11.listobjects (<handle>, <slot>, <'cert' | 'pubkey' | 'privkey' | 'data' | 'all'> [, 'value'])<ret>, <error> = t1.listobjects (<'cert' | 'pubkey' | 'privkey' | 'data' | 'all'> [, 'value'])#Разбор сертификата<ret> = pyp11.parsecert (<handle>, <slot>, <cert_der_hex>)<ret>, <error> = t1.parsecert(<cert_der_hex>)#Импорт сертификата<ret> = pyp11.importcert (<handle>, <slot>, <cert_der_hex>, <Метка сертификата>)<ret>, <error> = t1.importcert(<cert_der_hex>, <Метка сертификата>)#Вычисление хэша<ret> = pyp11.digest (<handle>, <slot>, <тип алгоритма>, <контент>)<ret>, <error> = t1.digest(<тип алгоритма>, <контент>)#Вычисление электронной подписи<ret> = pyp11.digest (<handle>, <slot>, <механизм подписи>, <хэш от контента>, <CKA_ID | handle закрытого ключа>)<ret>, <error> = t1.digest(<механизм подписи>, <хэш от контента>, <CKA_ID | handle закрытого ключа>)#Проверка электронной подписи<ret> = pyp11.verify (<handle>, <slot>, <хэш от контента>, <подпись>, <asn1-структура subjectpublickeyinfo в hex>)<ret>, <error> = t1.verify(<хэш от контента>, <подпись>, <asn1-структура subjectpublickeyinfo в hex>)#Генерация ключевой пары<ret> = pyp11.keypair (<handle>, <slot>, <тип ключа>, <OID криптопараметра>, <CKA_LABEL>)<ret>, <error> = t1.keypair(<тип ключа>, <OID криптопараметра>, <CKA_LABEL>)

III. Сборка и установка модуля pyp11 с классом Token


Сборка и установка модуля pyp11 с классом Token ничем не отличается от описанной в первой части.
Итак, скачиваем архив и распаковываем его. Заходим в папку PythonPKCS11 и выполняем команду установки:
python3 setup.py install

После установки модуля переходим в папку tests и запускаем тесты для модуля pyp11.
Для тестирования класса Token переходим в папку test/classtoken.
Для подключения модуля pyp11 и класса Token в скрипты достаточно добавить следующие операторы:
import pyp11from Token import Token


IV. Заключение


В ближайшее время должна появиться и третья часть статьи, в которой будет рассказано, как добавить поддержку российской криптографии в проект PyKCS11.

P.S. Хочу сказать спасибо svyatikov за то, что помог протестировать проект на платформе Windows.
Подробнее..

Категории

Последние комментарии

  • Имя: Макс
    24.08.2022 | 11:28
    Я разраб в IT компании, работаю на арбитражную команду. Мы работаем с приламы и сайтами, при работе замечаются постоянные баны и лаги. Пацаны посоветовали сервис по анализу исходного кода,https://app Подробнее..
  • Имя: 9055410337
    20.08.2022 | 17:41
    поможем пишите в телеграм Подробнее..
  • Имя: sabbat
    17.08.2022 | 20:42
    Охренеть.. это просто шикарная статья, феноменально круто. Большое спасибо за разбор! Надеюсь как-нибудь с тобой связаться для обсуждений чего-либо) Подробнее..
  • Имя: Мария
    09.08.2022 | 14:44
    Добрый день. Если обладаете такой информацией, то подскажите, пожалуйста, где можно найти много-много материала по Yggdrasil и его уязвимостях для написания диплома? Благодарю. Подробнее..
© 2006-2024, personeltest.ru