Криптографические токены PKCS#11: просмотр и экспорт сертификатов, проверка их валидности

    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), формирования и проверки подписи и т.п.
    Поделиться публикацией
    AdBlock похитил этот баннер, но баннеры не зубы — отрастут

    Подробнее
    Реклама

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

    Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

    Самое читаемое