В комментариях к статье «Англоязычная кроссплатформенная утилита для просмотра российских квалифицированных сертификатов x509» было пожелание от пользователя Pas иметь не только «парсинг сертификатов», но и получать «цепочки корневых сертификатов и проводить PKI-валидацию, хотя бы для сертификатов на токенах с неизвлекаемым ключом». О получении цепочки сертификатов рассказывалось в одной из предыдущих статей. Правда там речь шла о сертификатах, хранящихся в файлах, но мы обещали добавить механизмы для работы с сертификатами, хранящимися на токенах PKCS#11. И вот что в итоге получилось.
Утилита разбора и просмотра написана на Tcl/Tk и, чтобы в нее добавить просмотр сертификатов на токенах/смарткартах PKCS#11, а также проверку валидности сертификатов потребовалось решить несколько задач:
- определиться с механизмом получения сертификатов с токена/смарт карты;
- проверить сертификат по списку отозванных сертификатов CRL;
- проверить сертификат на валидность по механизму OCSP.
Доступ к токену PKCS#11
Для доступа к токену и сертификатам, хранящимя на нем, воспользуемся пакетом TclPKCS11. Пакет распространяется как в бинарниках, так и в исходниках. Исходные коды пригодятся позднее, когда мы будем добавлять в пакет поддержку токенов с российской криптографией. Загрузить пакет TclPKCS11 можно двумя способами, либо командой tcl вида:
load <библиотека tclpkcs11> Tclpkcs11
Либо загрузить просто как пакет pki::pkcs11, предварительно положив библиотеку tclpkcs11 и файл pkgIndex.tcl в удобный вам каталог (в нашем случае это подкаталог pkcs11 текущего каталога) и добавив его в путь auto_path:
#lappend auto_path [file dirname [info scrypt]] lappend auto_path pkcs11 package require pki package require pki::pkcs11
Поскольку нас интересуют токены прежде всего с поддержкой российской криптографии, то из пакета TclPKCS11 мы будем задействовать следующие функции:
Сразу оговоримся, что функции login и logout здесь рассматриваться не будут. Это связано с тем, что в рамках этой статьи мы будем иметь дело только с сертификатами, а они являются публичными объектами токена. Для доступа к публичным объектам нет необходимости авторизовываться через PIN-код на токене.::pki::pkcs11::loadmodule <filename> -> handle ::pki::pkcs11::unloadmodule <handle> -> true/false ::pki::pkcs11::listslots <handle> -> list: slotId label flags ::pki::pkcs11::listcerts <handle> <slotId> -> list: keylist ::pki::pkcs11::login <handle> <slotId> <password> -> true/false ::pki::pkcs11::logout <handle> <slotId> -> true/false
Первая функция ::pki::pkcs11::loadmodule предназначена для загрузки библиотеки PKCS#11, которая поддерживает токен/смарткарту, на котором находятся сертификаты. Библиотека может быть получена либо при приобретении токена, либо загружена из Интернета или она была предустановлена на компьютере. В любом случае надо знать какая библиотека поддерживает ваш токен. Функция loadmodule возвращает указатель (handle) на загруженную библиотеку:
set filelib "/usr/local/lib64/librtpkcs11ecp_2.0.so" set handle [::pki::pkcs11::loadmodule $filelib]
Соответственно есть функция выгрузки загруженной библиотеки:
::pki::pkcs11::unloadmodule $handle
После того как была загружена библиотека и у нас есть ее handle можно получить список слотов, поддерживаемых этой библиотекой:
::pki::pkcs11::listslots $handle {0 {ruToken ECP } {TOKEN_PRESENT RNG LOGIN_REQUIRED USER_PIN_INITIALIZED TOKEN_INITIALIZED REMOVABLE_DEVICE HW_SLOT}} {1 { } {REMOVABLE_DEVICE HW_SLOT}} . . . {14 { } {REMOVABLE_D EVICE HW_SLOT}}
В данном примере список содержит 15 (пятнадцать от 0 до 14) элементов. Именно столько слотов может поддерживать библиотека токенов семейства RuToken. В свою очередь каждый элемент списка сам является списком из трех элементов:
{{номер слота} {метка токена} {флаги слота и токена}}
Первый элемент списка – это номер слота. Второй элемент списка это метка, находящегося в слоте токена (32 байта). Если слот пуст, то второй элемент содержит 32 пробела. И последний, третий элемент списка содержит флаги. Мы не будем рассматривать все множество флагов. Нас интересует в этих флагах только наличие флага TOKEN_PRESENT. Именно этот флаг говорит о том, что в слоте находится токен, а на токене могут находиться интересующие нас сертификаты. Флаги очень полезная вещь, они описывают состояние токена, состояние PIN –кодов и т.д. На основание значения флагов проводится управление токенами PKCS#11:

Теперь ничто не мешает написать процедуру slots_with_token, которая будет возвращать список слотов с метками находящихся в них токенов:
#!/usr/bin/tclsh lappend auto_path pkcs11 package require pki package require pki::pkcs11 #Список токенов со слотами proc ::slots_with_token {handle} { set slots [pki::pkcs11::listslots $handle] # puts "Slots: $slots" array set listtok [] 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} { set listtok($slotid) $slotlabel } } #Список найденных токенов в слотах parray listtok return [array get listtok] } set filelib "/usr/local/lib64/librtpkcs11ecp_2.0.so" if {[catch {set handle [::pki::pkcs11::loadmodule $filelib]} res]} { puts "Cannot load library $filelib : $res" exit } #Получаем список слотов set listslots {} set listslots [::slots_with_token $handle] #Если все слоты пустые ждем когда вставят токен while {[llength $listslots] == 0} { puts "Вставьте токен" after 3000 set listslots [::slots_with_token $handle] } #Печатаем номер заполненного слота и метку вставленного токена foreach {slotid labeltok} $listslots { puts "Number slot: $slotid" puts "Label token: $labeltok" }
Если выполнить этот скрипт, предварительно сохранив его в файле slots_with_token.tcl, то в результате получим:
$ ./slots_with_token.tcl listtok(0) = ruToken ECP listtok(1) = RuTokenECP20 Number slot: 0 Label token: RuTokenECP20 Number slot: 1 Label token: ruToken ECP $
Из 15 доступных слотов для данной библиотеки задействовано только два, нулевой и первый.
Теперь ничего не мешает получить список сертификатов, находящихся на том или ином токене:
set listcerts [::pki::pkcs11::listcerts $handle $slotid]
Каждый элемент списка содержит сведения об одном сертификате. Для получения сведений из сертификата используется функция ::pki::pkcs11::listcerts использует в свою очередь функцию ::pki::x509::parse_cert из пакета pki. Но функция ::pki::pkcs11::listcerts дополняет этот список данные, присущими протоколу PKCS#11, а именно:
- элемент pkcs11_ label (в терминологии PKCS#11 атрибут CKA_LABEL);
- элемент pkcs11_id (в терминологии PKCS#11 атрибут CKA_ID);
- элемент pkcs11_handle, содержащий указание на загруженную библиотеку PKCS#11;
- элемент pkcs11_slotid, содержащий номер слота с токеном, на котором находится данный сертификат;
- элемент type, который содержит значение pkcs11 для сертификата, находящегося на токене.
Напомним, что остальные элементы в основном определяются функцией pki::parse_cert.
Ниже представлена процедура, получения списка меток (listCert) сертификатов (CKA_LABEL, pkcs11_label) и массива распарсенных сентификатоы (::certs_p11). Ключом для доступа к элементу массива сертификатов служит метка сертификата (CKA_LABEL, pkcs11_label):
#Список сертификатов proc listcerttok {handle token_slotlabel token_slotid} { #Список меток сертификатов на токене set listCer {} #Массив распарсенных сертификатов array set ::arrayCer [] set ::certs_p11 [pki::pkcs11::listcerts $handle $token_slotid] if {[llength $::certs_p11] == 0} { puts {Certificates are not on the token:$tokenslotlabel} return $listCer } foreach certinfo_list $::certs_p11 { unset -nocomplain certinfo array set certinfo $certinfo_list set certinfo(pubkeyinfo) [::pki::x509::parse_cert_pubkeyinfo $certinfo(cert)] set ::arrayCer($certinfo(pkcs11_label)) $certinfo(cert) lappend listCer $certinfo(pkcs11_label) } return $listCer }
А теперь, когда мы имеем распарсенные сертификаты, мы спокойно отображаем в combobox список их меток:

Как распарсить ГОСТ-овые публичные ключи мы рассматривали в предыдущей статье.
Два слова об экспорте сертификата. Сертификаты экспортируются как в PEM-кодировке, так и DER-кодировке (кнопки DER, PEM-формат). Для преобразования в PEM-формат в пакете pki имеется удобная функция pki::_encode_pem:
set bufpem [::pki::_encode_pem <der-buffer> <Headline> <Lastline>]
например:
set certpem [::pki::encode_pen $cert_der "-----BEGIN CERTIFICATE-----" "-----END CERTIFICATE-----"]
Выбрав метку септификата в combobox, мы получаем доступ к телу сертификата:
#Читаем метку выбранного сертификата set nick [.saveCert.labExp.listCert get] #Ищем в списке сертификатов сертификат с выбранной меткой foreach certinfo_list $::certs_p11 { unset -nocomplain cert_parse array set cert_parse $certinfo_list if {$cert_parse(pkcs11_label) == $nick} { #Читаем публичный ключ set cert_parse(pubkeyinfo) [::pki::x509::parse_cert_pubkeyinfo $cert_parse(cert)] break } } #Тип хранения сертификата file|pkcs11 set ::tekcert "pkcs11"
Дальнейший механизм разбора сертификата и его отображения был ранее рассмотрен здесь.
Проверка срока действия сертификата
При разборе сертифмката в переменных ::notbefore и ::notafter хранится дата, с которой сертификат может использоваться в криптографических операциях (подписать, зашифровать и т.д.), и дата окончания срока действия сертификата. Процедура проверки срока действия сертификата имеет вид:
proc cert_valid_date {} { # Проверяем валидность сертификата по срокам действия #Дата начала действия сертификата set startdate $::notbefore #Дата окончания действия сертификата set enddate $::notafter # Получаем текущее время в секундах set now [clock seconds] set isvalid 1 set reason "Certificate is valid" if {$startdate > $now} { set isvalid 0 #Срок действия сертификата еще не наступил set reason "Certificate is not yet valid" } elseif {$now > $enddate} { set isvalid 0 #Срок действия сертификата истек set reason "Certificate has expired" } return [list $isvalid $reason] }
Возвращаемый список содержит два элемента. Первый элемент может содержать либо 0 (ноль) либо 1 (один). Значение «1» указывает на то, что сертификат действует, а 0 – на то, что сертификат не действует. Причина по которой не действует сертификат раскрывается во втором элементе. Этот элемент может содержать одно из трех значений:
- certificate valid (первый элемент списка равен 1):
- certificate is not yet valid (время действия сертификата еще не наступило)
- certificate has expired (срок действия сертификата истек).
Валидность сертификата определяется не только периодом его действия. Действие сертификата может быть приостановлено или прекращено удостоверяющим центром, как по его инициативе, так и по заявлению владельца сертификата, например при утрате носителя с закрытым ключом. В этом случае сертификат включается удостоверяющим центром в список отозванных сертификатов СОС/CRL, которые распространяются УЦ. Как правило, точка распространения CRL включается в сертификат. Именно по списку отозванных сертификатов и проверяется валидность сертификата.
Проверка валидности сертификата по СОС/CRL
Первым шагом необходимо получить СОС, затем его распарсить и проверить по нему сертификат.
Список точек выдачи СОС/CRL находится в расширении сертификата с oid-ом 2.5.29.31 (id-ce-cRLDistributionPoints):
array set extcert $cert_parse(extensions) set ::crlfile "" if {[info exists extcert(2.5.29.31)]} { set ::crlfile [crlpoints [lindex $extcert(2.5.29.31) 1]] } else { puts "cannot load CRL" }
Собственно загрузка файла с СОС/CRL ведется следующим образом:
set filecrl "" set pointcrl "" foreach pointcrl $::crlfile { set filecrl [readca $pointcrl $dir] if {$filecrl != ""} { set f [file join $dir [file tail $pointcrl]] set fd [open $f w] chan configure $fd -translation binary puts -nonewline $fd $filecrl close $fd set filecrl $f break } #Прочитать CRL не удалось. Берем следующую точку с CRL } if {$filecrl == ""} { puts "Cannot load CRL" }
Собственно для загрузки СОС/CRL используется процедура readca:
proc readca {url dir} { set cer "" #Проверяем тип протокола if { "https://" == [string range $url 0 7]} { #должен быть загружен пакет tls http::register https 443 ::tls::socket } #Читаем сертификат в бинарном виде if {[catch {set token [http::geturl $url -binary 1] #получаем статус выполнения функции set ere [http::status $token] if {$ere == "ok"} { #Получаем код возврата с которым был прочитан сертификат set code [http::ncode $token] if {$code == 200} { #Сертификат успешно прочитан и будет созвращен set cer [http::data $token] } elseif {$code == 301 || $code == 302} { #Сертификат перемещен в другое место, получаем его set newURL [dict get [http::meta $token] Location] #Читаем сертификат с другого сервера set cer [readca $newURL $dir] } else { #Сертификат не удалось прочитать set cer "" } } } error]} { #Сертификат не удалось прочитать, нет узла в сети set cer "" } return $cer }
В переменной dir хранится путь к каталогу, в котором будет сохранен СОС/CRL, а в переменной url – ранее полученный список точек распространения CRL.
При получении СОС/CRL неожиданно пришлось столкнуться с тем, что для некоторых сертификатов этот список приходиться получать по протоколу https (tls) в анонимном режиме. Честно говоря, это удивительно: список CRL это публичный документ и его целостность защищена электронной подписью и иметь доступ к нему по анонимному https на мой взгляд перебор. Но делать нечего, приходится подключать пакет tls – package require tls.
Если СОС/CRL загрузить не удалось, то валидность сертификата проверена быть не может, если только в сертификате не указана точка доступа с сервису OCSP. Но об этом речь пойдет в одной из следующих статей.
Итак, сертификат для проверки есть, список СОС/CRL есть, осталось проверить по нему сертификт. К сожалению, в пакете pki отсутствуют соответствующие функции. Поэтому пришлось написать процедуру для проверки валидности сертификата (его неотозванности) по списку отозванных сертификатов
validaty_cert_from_crl :
proc validaty_cert_from_crl {crl sernum issuer} { array set ret [list] if { [string range $crl 0 9 ] == "-----BEGIN" } { array set parsed_crl [::pki::_parse_pem $crl "-----BEGIN X509 CRL-----" "-----END X509 CRL-----"] set crl $parsed_crl(data) } ::asn::asnGetSequence crl crl_seq ::asn::asnGetSequence crl_seq crl_base ::asn::asnPeekByte crl_base peek_tag if {$peek_tag == 0x02} { # Номер версии СОС.CRL ::asn::asnGetInteger crl_base ret(version) incr ret(version) } else { set ret(version) 1 } ::asn::asnGetSequence crl_base crl_full ::asn::asnGetObjectIdentifier crl_full ret(signtype) ::::asn::asnGetSequence crl_base crl_issue set ret(issue) [::pki::x509::_dn_to_string $crl_issue] #Проверка издателя проверяемого сертификата и СОС/CRL if {$ret(issue) != $issuer } { #СОС/CRL издан чужим УЦ set ret(error) "Bad Issuer" return [array get ret] } binary scan $crl_issue H* ret(issue_hex) #Дата издания ::asn::asnGetUTCTime crl_base ret(publishDate) #Следующая дата издания ::asn::asnGetUTCTime crl_base ret(nextDate) #Список сертификатов отозванных ::asn::asnPeekByte crl_base peek_tag if {$peek_tag != 0x30} { #Список сертификатов отозванных пустой return [array get ret] } ::asn::asnGetSequence crl_base lcert # binary scan $lcert H* ret(lcert) while {$lcert != ""} { ::asn::asnGetSequence lcert lcerti #Разбираем очередной отозванный сертификат ::asn::asnGetBigInteger lcerti ret(sernumrev) set ret(sernumrev) [::math::bignum::tostr $ret(sernumrev)] #Проверяем отозванность сертификата по номеру из CRL if {$ret(sernumrev) != $sernum} { continue } #Сертификат отозван. Определяем дату отзыва ::asn::asnGetUTCTime lcerti ret(revokeDate) if {$lcerti != ""} { #Разбираем причину отзыва ::asn::asnGetSequence lcerti lcertir ::asn::asnGetSequence lcertir reasone ::asn::asnGetObjectIdentifier reasone ret(reasone) ::asn::asnGetOctetString reasone reasone2 ::asn::asnGetEnumeration reasone2 ret(reasoneData) } break; } return [array get ret] }
Параметрами этой функции являются список отозванных сертификатов (crl), серийный номер проверяемого сертификата (sernum) и его издатель (issuer).
Список отозванных сертификатов (crl) загружается следующим образом:
set f [open $filecrl r] chan configure $f -translation binary set crl [read $f] close $f
Серийный номер проверяемого сертификата (sernum) и его издатель (issuer) берутся из распарсенного сертификата и сохраненные в переменных ::sncert и ::issuercert.
Все процедуры можно найти в исходном коде. Исходный код утилиты и ее дистрибутивы для платформ Linux, OS X (macOS) и MS Windows можно найти здесь
В утилите также сохранена возможность просмотра и проверки сертификатов, хранящихся в файле:

Кстати, просматриваемые сертификаты из файлов, также можно экспортировать, как и хранящиеся на токене. Это позволяет легко конвертировать файлы с сертификатами из DER-формата в PEM и наоборот.
Теперь у нас есть единый просмоторщик для сертификатов хранящихся как в файлах, так и на токенах/смаркартах PKCS#11.
Да, упустил главное, для проверки валидности сертификата надо нажать кнопку «Дополнительно» («Additionaly») и выбрать пункт меню «Валидность по СОС/CRL» («Validaty by CRL») или нажать правую кнопку мыши и при нахождении курсора на основном информационном поле и также выбрать пункт меню «Валидность по СОС/CRL» («Validaty by CRL»):

На данном скриншоте показан просморт и проверка валидности сертификатов, находящихся в облачном токене.
В заключении отметим следующее. В своих комментариях к статье пользователь Pas очень правильно заметил про токены PKCS#11, что они «сами все умеют считать». Да, токены фактически являются криптографическими компьютерами. И в следующих статьях мы поговорим не только о том как проверяются сертификаты по OCSP-протоколу, но и о том как задействовать криптографические механизмы (речь идет, конечно, о ГОСТ-криптографии) токенов/смартарт для вычисления хэша (ГОСТ Р 34-10-94/2012), формирования и проверки подписи и т.п.
