image В комментариях к статье «Англоязычная кроссплатформенная утилита для просмотра российских квалифицированных сертификатов 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 отсутствуют соответствующие функции. Поэтому пришлось написать процедуру для проверки валидности сертификата (его неотозванности) по списку отозванных сертификатов

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), формирования и проверки подписи и т.п.

Комментарии (0)