В комментариях к статье «Англоязычная кроссплатформенная утилита для просмотра российских квалифицированных сертификатов 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 мы будем задействовать следующие функции:
::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
Сразу оговоримся, что функции login и logout здесь рассматриваться не будут. Это связано с тем, что в рамках этой статьи мы будем иметь дело только с сертификатами, а они являются публичными объектамси токена. Для доступа к публичным объектам нет необходимости авторизовываться через PIN-код на токене.
Первая функция ::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 отсутствуют соответствующие функции. Поэтому пришлось написать процедуру для проверки валидности сертификата (его неотозванности) по списку отозванных сертификатов
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), формирования и проверки подписи и т.п.
Автор: saipr