- PVSM.RU - https://www.pvsm.ru -

Инфраструктура открытых ключей. Цепочка корневых сертификатов X509 v.3

Инфраструктура открытых ключей. Цепочка корневых сертификатов X509 v.3 - 1

Неумолимо приближается час «Ч» [1]: «использование схемы подписи ГОСТ Р 34.10-2001 для формирования подписи после 31 декабря 2018 года не допускается!».

Однако потом что-то пошло не так, кто-то оказался не готов, и использование ГОСТ Р 34.10-2001 продлили на 2019 год. Но вдруг все бросились переводить УЦ на ГОСТ Р 34.10-2012, а простых граждан переводить на новые сертификаты. У людей на руках стало по нескольку сертификатов. При проверки сертификатов или электронной подписи стали возникать вопросы, а где взять корневые сертификаты, чтобы установить в хранилища доверенных корневых сертификатов.

Это касается как хранилища сертификатов в Windows, так и хранилища сертификатов в браузерах Firefox и Google Chrome, GnuPG [1], LibreOffice [2], почтовых клиентах [3] и даже OpenSSL [4]. Конечно, надо было озаботиться этим при получении сертификата в УЦ и записать цепочку сертификатов на флешку. А с другой стороны у нас же цифровое общество и в любой момент должна быть возможность получить эту цепочку из сети. Как это сделать на страницах Хабра показал [5] simpleadmin [6]. Однако для рядового гражданина это все же сложновато (особенно, если иметь ввиду, что абсолютное их большинство сидит на Windows): нужно иметь «какой-то» openssl, утилиту fetch, которой и у меня не оказалось на компьютере, и далеко не каждый знает, что вместо нее можно использовать wget. А сколько действий нужно выполнить. Выход конечно есть, написать скрипт, но не просто скрипт поверх openssl и иже с ним, а упакованный в самодостаточный выполняемый модуль для различных платформ.

На чем писать никаких сомнений не было – на Tcl и Python [7]. И начинаем с Tcl и вот почему [8]:

* охренительной вики [9], где есть даже игрушки [10] (там можно подсмотреть интересное :)
* шпаргалки [11]
* нормальные сборки tclkit [12] (1.5 — 2 Мб как плата за реальную кросс-платформенность)
* и моя любимая сборка eTcl от Evolane [13] (бережно сохранённая с умершего сайта :(
сохраняют высокий рейтинг Tcl/Tk в моём личном списке инструментария
и, да, wiki.tcl.tk/16867 [14] (мелкий web-сервер с cgi на Tcl, периодически используется с завидным постоянством под tclkit)
а ещё — это просто красиво [15] и красиво [16] :)

К этому бы я добавил наличии утилиты freewrap [17], которая нам и поможет собрать автономные (standalone) утилиты для Linux и MS Windows. В результате мы будем иметь утилиту chainfromcert:

bash-4.3$ ./chainfromcert_linux64  
Copyright(C)2019 
Usage: 
chainfromcert <file with certificate> <directory for chain certificate> 
Bad usage! 
bash-4.3$

В качестве параметров утилите задаются файл с пользовательским сертификатом (как в формате PEM, так и формате DER) и каталог, в котором будут сохранены сертификаты УЦ, входящие в цепочку:

bash-4.3$ ./chainfromcert_linux64  ./cert_test.der /tmp
Loading file: cert_test.der 
Directory for chain: . 
cert 1 from http://ca.ekey.ru/cdp/ekeyUC2012.cer 
cert 2 from http://reestr-pki.ru/cdp/guc_gost12.crt 
Goodby! 
Length chain=2 
Copyright(C) 2019
bash-4.3$

А теперь рассмотрим как работает утилита.
Информация о центре сертификации, выдавшем сертификат пользователю, хранится в расширении с oid-ом 1.3.6.1.5.5.7.1.1. В этом расширении может хранится как о местонахождении сертификата УЦ (oid 1.3.6.1.5.5.7.48.2), так и информация о службе OCSP УЦ (oid 1.3.6.1.5.5.7.48.1):

Инфраструктура открытых ключей. Цепочка корневых сертификатов X509 v.3 - 2

А информация, например, о периоде использования ключа электронной подписи хранится в расширении с oid-ом 2.5.29.16.

Для разбора сертификата и доступа к расширениям сертификата воспользуемся пакетом pki:

#!/usr/bin/tclsh -f
package require pki

Нам также потребуются пакет base64:

package require base64

Пакет pki, а также подгружаемые им пакет asn, и пакет base64 и помогут нам преобразовывать сертификаты из PEM-кодировки в DER-кодировку, разобрать ASN-структуры и собственно получить доступ к информации о местонахождении сертификатов УЦ.

Работа утилиты начинается с проверки параметров и загрузки файла с сертификатом:

proc usage {use } {
    puts "Copyright(C) LISSI-Soft Ltd (http://soft.lissi.ru) 2011-2019"
    if {$use == 1} {
	puts "Usage:nchainfromcert <file with certificate> <directory for chain certificate>n"
    }
}
if {[llength $argv] != 2 } {
    usage 1
    puts "Bad usage!"
    exit
}
set file [lindex $argv 0]
if {![file exists $file]} {
    puts "File $file not exist"
    usage 1
    exit
}
puts "Loading file: $file"
set dir [lindex $argv 1]
if {![file exists $dir]} {
    puts "Dir $dir not exist"
    usage 1
    exit
}
puts "Directory for chain: $dir"
set fd [open $file]
chan configure $fd -translation binary
set data [read $fd]
close $fd
if {$data == "" } {
    puts "Bad file with certificate=$file"
    usage 1
    exit
}

Здесь все понятно и отметим только одно – файл с сертификатом рассматривается как бинарный файл:

chan configure $fd -translation binary

Это связано с тем, что сертификат может хранится как в формате DER (двоичный код), так и в формате PEM (base64 — кодировка).

После того как файл загружен вызывается процедура chainfromcert:

set depth [chainfromcert $data $dir]

которая собственно и загружает корневые сертификаты:

proc chainfromcert {cert dir} {
    if {$cert == "" } {
	exit
    }
    set asndata [cert_to_der $cert]
    if {$asndata == "" } {
#Файл содержит все что угодно, только не сертификат
	return -1
    }
    array set cert_parse [::pki::x509::parse_cert $asndata]
    array set extcert $cert_parse(extensions)
    if {![info exists extcert(1.3.6.1.5.5.7.1.1)]} {
#В сертификате нет расширений
	return 0
    }
    set a [lindex $extcert(1.3.6.1.5.5.7.1.1) 0]
#    if {$a == "false"} {
#	puts $a
#    }
#Читаем ASN1-последовательность расширения в Hex-кодировке
    set b [lindex $extcert(1.3.6.1.5.5.7.1.1) 1]
#Переводим в двоичную кодировку
    set c [binary format H* $b]
#Sequence 1.3.6.1.5.5.7.1.1
    ::asn::asnGetSequence c c_par_first
#Цикл перебора значений в засширении 1.3.6.1.5.5.7.1.1
    while {[string length $c_par_first] > 0 } {
#Выбираем очередную последовательность (sequence)
	::asn::asnGetSequence c_par_first c_par
#Выбираем oid из последовательности
	::asn::asnGetObjectIdentifier c_par c_type
	set tas1 [::pki::_oid_number_to_name $c_type]
#Выбираем установленное значение
	::asn::asnGetContext c_par c_par_two
#Ищем oid с адресом корневого сертификата
	if {$tas1 == "1.3.6.1.5.5.7.48.2" } {
#Читаем очередной корневой сертификат
	    set certca [readca $c_par $dir]
	    if {$certca == ""} {
#Прочитать сертификат не удалось. Ищем следующую точку с сертификатом
		continue
	    } else {
		global count
#Сохраняем корневой сертификат в указанном каталоге
		set f [file join $dir [file tail $c_par]]
		set fd [open $f w]
		chan configure $fd -translation binary
		puts -nonewline $fd $certca 
		close $fd
		incr count
		puts "cert $count from $c_par"
#ПОДЫМАЕМСЯ по ЦЕПОЧКЕ СЕРТИФИКАТОВ ВВЕРХ
		chainfromcert $certca $dir
		continue
	    }
	} elseif {$tas1 == "1.3.6.1.5.5.7.48.1" } {
#	    puts "OCSP server (oid=$tas1)=$c_par"
	}
    }
# Цепочка закончилась
    return $count
}

К комментариям добавить нечего, но у нас осталась не рассмотренной процедура readca:

proc readca {url dir} {
    set cer ""
#Читаем сертификат в бинарном виде
    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
}

Это процедура построена на использовании пакета http:

package require http

Для чтения сертификата мы используем следующую функцию:

set token [http::geturl $url -binary 1]

Назначение остальных используемых функции понятно из комментариев. Дадим только расшифровку кодов возврата для функции http::ncodel:

200 Запрос успешно выполнен
206 Запрос успешно выполнен, но удалось скачать только часть файла
301 Файл перемещен в другое место
302 Файл временно перемещен в другое место
401 Требуется аутентификация на сервере
403 Доступ к этому ресурсу запрещен
404 Указанный ресурс не может быть найден
500 Внутренняя ошибка

Осталось не рассмотренной одна процедура, а именно cert_to_der:

proc cert_to_der {data} {
    set lines [split $data n]
    set hlines 0
    set total 0
    set first 0
#Ищем PEM-сертификат в файле
    foreach line $lines {
        incr total
        if {[regexp {^-----BEGIN CERTIFICATE-----$} $line]} {
            if {$first} {
                incr total -1
                break
            } else {
                set first 1
                incr hlines
            }
        }        
        if {[regexp {^(.*):(.*)$} $line ]} {
            incr hlines
        } 
    }
    if { $first == 0 && [string range $data 0 0 ] == "0" } {
#Очень похоже на DER-кодировку "0" == 0x30 
	return $data
    }
    if {$first == 0} {return ""}
    set block [join [lrange $lines $hlines [expr {$total-1}]]]
#from PEM to DER
    set asnblock [base64::decode $block]
    return $asnblock
}

Схема процедуры очень простая. Если это PEM-файл с сертификатом («-----BEGIN CERTIFICATE----- »), то выбирается тело этого файла и преобразуется в бинаоный код:

set asnblock [base64::decode $block]

Если это не PEM-файл, то проверяется это «похожесть» на asn-кодировку (нулевой бит должен быть равен 0x30).

Вот собственно и все, осталось добавить завершающие строки:

if {$depth == -1} {
    puts "Bad file with certificate=$file"
    usage 1
    exit
}
puts "Goodby!nLength chain=$depth"
usage 0
exit

Теперь все собираем в один файл с именем

chainfromcert.tcl

#!/usr/bin/tclsh
encoding system utf-8

package require pki
package require base64
#package require asn
package require http 
global count
set count 0

proc chainfromcert {cert dir} {
    if {$cert == "" } {
	exit
    }
    set asndata [cert_to_der $cert]
    if {$asndata == "" } {
#Файл содержит все что угодно, только не сертификат
	return -1
    }
    array set cert_parse [::pki::x509::parse_cert $asndata]
    array set extcert $cert_parse(extensions)
    if {![info exists extcert(1.3.6.1.5.5.7.1.1)]} {
#В сертификате нет расширений
	return 0
    }
    set a [lindex $extcert(1.3.6.1.5.5.7.1.1) 0]
#    if {$a == "false"} {
#	puts $a
#    }
#Читаем ASN1-последовательность расширения в Hex-кодировке
    set b [lindex $extcert(1.3.6.1.5.5.7.1.1) 1]
#Переводим в двоичную кодировку
    set c [binary format H* $b]
#Sequence 1.3.6.1.5.5.7.1.1
    ::asn::asnGetSequence c c_par_first
#Цикл перебора значений в засширении 1.3.6.1.5.5.7.1.1
    while {[string length $c_par_first] > 0 } {
#Выбираем очередную последовательность (sequence)
	::asn::asnGetSequence c_par_first c_par
#Выбираем oid из последовательности
	::asn::asnGetObjectIdentifier c_par c_type
	set tas1 [::pki::_oid_number_to_name $c_type]
#Выбираем установленное значение
	::asn::asnGetContext c_par c_par_two
#Ищем oid с адресом корневого сертификата
	if {$tas1 == "1.3.6.1.5.5.7.48.2" } {
#Читаем очередной корневой сертификат
	    set certca [readca $c_par $dir]
	    if {$certca == ""} {
#Прочитать сертификат не удалось. Ищем следующую точку с сертификатом
		continue
	    } else {
		global count
#Сохраняем корневой сертификат в указанном каталоге
		set f [file join $dir [file tail $c_par]]
		set fd [open $f w]
		chan configure $fd -translation binary
		puts -nonewline $fd $certca 
		close $fd
		incr count
		puts "cert $count from $c_par"
#ПОДЫМАЕМСЯ по ЦЕПОЧКЕ СЕРТИФИКАТОВ ВВЕРХ
		chainfromcert $certca $dir
		continue
	    }
	} elseif {$tas1 == "1.3.6.1.5.5.7.48.1" } {
#	    puts "OCSP server (oid=$tas1)=$c_par"
	}
    }
# Цепочка закончилась
    return $count
}

proc readca {url dir} {
    set cer ""
#Читаем сертификат в бинарном виде
    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
}

proc cert_to_der {data} {
    set lines [split $data n]
    set hlines 0
    set total 0
    set first 0
#Ищем PEM-сертификат в файле
    foreach line $lines {
        incr total
#        if {[regexp {^-----(.*?)-----$} $line]} {}
        if {[regexp {^-----BEGIN CERTIFICATE-----$} $line]} {
            if {$first} {
                incr total -1
                break
            } else {
                set first 1
                incr hlines
            }
        }        
        if {[regexp {^(.*):(.*)$} $line ]} {
            incr hlines
        } 
    }
    if { $first == 0 && [string range $data 0 0 ] == "0" } {
#Очень похоже на DER-кодировку "0" == 0x30 
	return $data
    }
    if {$first == 0} {return ""}
    set block [join [lrange $lines $hlines [expr {$total-1}]]]
#from PEM to DER
    set asnblock [base64::decode $block]
    return $asnblock
}

proc usage {use } {
    puts "Copyright(C) LISSI-Soft Ltd (http://soft.lissi.ru) 2011-2019"
    if {$use == 1} {
	puts "Usage:nchainfromcert <file with certificate> <directory for chain certificate>n"
    }
}
if {[llength $argv] != 2 } {
    usage 1
    puts "Bad usage!"
    exit
}
set file [lindex $argv 0]
if {![file exists $file]} {
    puts "File $file not exist"
    usage 1
    exit
}
puts "Loading file: $file"
set dir [lindex $argv 1]
if {![file exists $dir]} {
    puts "Dir $dir not exist"
    usage 1
    exit
}
puts "Directory for chain: $dir"
set fd [open $file]
chan configure $fd -translation binary
set data [read $fd]
close $fd
if {$data == "" } {
    puts "Bad file with certificate=$file"
    usage 1
    exit
}
set depth [chainfromcert $data $dir]
if {$depth == -1} {
    puts "Bad file with certificate=$file"
    usage 1
    exit
}
puts "Goodby!nLength chain=$depth"
usage 0
exit

Проверить работу этого файла можно с помощью интерпретарора tclsh:

$ tclsh ./chainfromcert.tcl cert_orlov.der /tmp 
Loading file: cert_test.der 
Directory for chain: /tmp 
cert 1 from http://ca.ekey.ru/cdp/ekeyUC2012.cer 
cert 2 from http://reestr-pki.ru/cdp/guc_gost12.crt 
Goodby! 
Length chain=2 
Copyright(C) 2019 
$

В результате работы мы получили цепочку из двух сертификатов в каталоге /tmp.

Но мы хотели получить выполняемые модули для платформ Linux и Windowsи и чтобы пользователи не задумывались о каких-то интерпретаторах.

Для этой цели мы воспользуемся утилитой freewrapTCLSH [18]. С помощью этой утилиты мы сделаем выполняемые модули нашей утилиты для платформ Linux и Windows как 32-х разрядных так и 64-х. Сборку утилит можно проводить для всех платформ на любой из платформ. Извините за тавтологию. Я буду собирать на linux_x86_64 (Mageia).

Для сборки потребуется:

1. Утилита freewrapTCLSH для платформы linux_x86_64;
2. Файл freewrapTCLSH с этой утилитой для каждой платформы:
— freewrapTCLSH_linux32
— freewrapTCLSH_linux64
— freewrapTCLSH_win32
— freewrapTCLSH_win64
3. Исходный файл нашей утилиты: chainfromcert.tcl

Итак, собираемый выполняемый файл chainfromcerty_linuxx86 для платформы Linux x86:

$freewrapTCLSH chainfromcert.tcl –w freewrapTCLSH_linux32 –o chainfromcerty_linuxx86
$

Сборка утилиты для платформы Windows 64-х битного выглядит так:

$freewrapTCLSH chainfromcert.tcl –w freewrapTCLSH_win64 –o chainfromcerty_win64.exe
$

И т.д. Утилиты готовы к использованию. Все необходимое для их работы они вобрали в себя.
Аналогичным образом пишется код и на Python-е.

В ближайшие дни я думаю дополнить пакет fsb795 [19] (а он написан на Python-е) функцией получения цепочки корневых сертификатов.

Автор: saipr

Источник [20]


Сайт-источник PVSM.RU: https://www.pvsm.ru

Путь до страницы источника: https://www.pvsm.ru/python/305496

Ссылки в тексте:

[1] час «Ч»: https://habr.com/ru/post/417735/

[2] LibreOffice: https://habr.com/ru/post/428429/

[3] почтовых клиентах: https://habr.com/ru/post/316736/

[4] OpenSSL: https://habr.com/ru/post/415423/

[5] показал: https://habr.com/ru/post/304458/

[6] simpleadmin: https://habr.com/ru/users/simpleadmin/

[7] Tcl и Python: https://habr.com/ru/post/335842/

[8] вот почему: https://www.linux.org.ru/forum/development/12833014/page1

[9] вики: http://wiki.tcl.tk/

[10] даже игрушки: http://wiki.tcl.tk/20801

[11] шпаргалки: http://pleac.sourceforge.net/pleac_tcl/index.html

[12] сборки tclkit: https://code.google.com/archive/p/tclkit/downloads

[13] eTcl от Evolane: http://wiki.tcl.tk/15260

[14] wiki.tcl.tk/16867: http://wiki.tcl.tk/16867

[15] красиво: https://habrahabr.ru/post/89919/

[16] красиво: https://habrahabr.ru/post/89822/

[17] freewrap: http://freewrap.sourceforge.net/

[18] freewrapTCLSH: https://sourceforge.net/projects/freewrap/files/freewrap/freeWrap%206.64/

[19] fsb795: https://habr.com/ru/post/421107/

[20] Источник: https://habr.com/ru/post/436370/?utm_campaign=436370