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

imageВ комментариях к статье «Англоязычная кроссплатформенная утилита для просмотра российских квалифицированных сертификатов x509» было пожелание от пользователя Pas иметь не только «парсинг сертификатов», но и получать «цепочки корневых сертификатов и проводить PKI-валидацию, хотя бы для сертификатов на токенах с неизвлекаемым ключём». О получении цепочки сертификатов рассказывалось в одной из предыдущих статей. Правда там речь шла о сертификатах, хранящихся в файлах, но мы обещали добавить механизмы для работы с сертификатами, хранящимися на токенах PKCS#11. И вот что в итоге получилось :
cgvdfwfqdxc1tbvkkpcec_vzye8.png

Утилита разбора и просмотра написана на 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                        -> handle
::pki::pkcs11::unloadmodule                        -> true/false
::pki::pkcs11::listslots                         -> list: slotId label flags
::pki::pkcs11::listcerts                   -> list: keylist
::pki::pkcs11::login             -> true/false
::pki::pkcs11::logout                      -> 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:

yq7xn1qbusjtjirrudo1ebhjzhc.png

Теперь ничто не мешает написать процедуру 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 список их меток:

wd7_siithrzlh1gfwa5tm78rpbg.png

Как распарсить ГОСТ-овые публичные ключи мы рассматривали в предыдущей статье.
Два слова об экспорте сертификата. Сертификаты экспортируются как в PEM-кодировке, так и DER-кодировке (кнопки DER, PEM-формат). Для преобразования в PEM-формат в пакете pki имеется удобная функция pki::_encode_pem:

set bufpem [::pki::_encode_pem    ]

,
например:

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 можно найти здесь

здесь

.
В утилите также сохранена возможность просмотра и проверки сертификатов, хранящихся в файле:

jatvnlolqcsggytw3nn8x51zpls.png

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

igphrmcbunw9b8jtkdiepfjaqpm.png

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

© Habrahabr.ru