В свое время реализация отечественных криптографических алгоритмов в библиотеке libgcrypt очень меня вдохновила. Стало возможным задействовать эти алгоритмы и в Kleopatra и в Kmail и в GnuPg в целом, рассматривать библиотеку libgcrypt как алтернативу openssl с ГОСТ-ым engine. И все было замечательно до прошлой пятницы.
Меня попросили проверить электронную подпись ГОСТ Р 34.10-2012-256 для документа, созданного в Microsoft Office на MS Windows. И я ее решил проверить в Kleopatra (у меня стоит Linux). И что вы думаете, подпись оказалась неверной. Закрались сомнения. Решил проверить на openssl с ГОСТ-ым (ССЫЛКА ) engine. Подпись успешно была проверена. Срочно переподписал файл в Kleopatra и он не прошел проверку на MS Windows. Попробовали другие файлы подписать и проверить, все было нормально. Встал вопрос в чем беда? Поскольку при подписании участвует хэш документа, было решено проверить вычисление хэш разными программами. Прежде всего была задействован open-source реализации для stribog:
И тут случился шок! Хэш, подсчитанный «знаменитой реализацией Дегтярева» совпадал с хэшом, подсчитанным в openssl с ГОСТ-ым endine, но не совпадал со значением хэш, посчитанным с помощью libgcrypt и libressl.
Как был прав ru_crypt, когда в начале своей статьи написал:
Сразу предупреждаю, что корректность реализаций не проверял.
Кстати, в самом стандарте на ГОСТ Р 34.10-2012 тоже написано, что контрольные примеры носят справочный характер. Надо четко понимать, что контрольные примеры не гарантирует, что разные реализации дают один и тот же результат на все случаи жизни.
Для вычисления хэш-значений использовались утилиты следующего вида:
1) openssl
$ openssl dgst [–md_gost12_256|-md_gost12_512] <file>
2) libressl
$libressl dgst [–streebog256|streebog512] <file>
3) libgcrypt
$gchash [stribog256|stribog512] <file>
4) Знаменитая реализация Дегтярева
$gost3411-2012 [-2|-5] <file>
Вот тоже интересная вещь: в латинской транскрипции стрибог пишут то stribog, то streebog. Неплохо бы прийти к единообразию. А так кажется, что это разные функции. Лично мне предпочтительней первый вариант – stribog.
Нужен был третейский судья.
В качестве третейского судьи было решено использовать токен PKCS#11 РУТОКЕН ЭЦП-2.0, который поддерживает российские криптографические стандарты ГОСТ Р 34.10-2012, ГОСТ Р 34.11-2012, VKO ГОСТ Р 34.10-2012 (RFC 7836) с длиной ключа 256 и 512 бит, и сертифицирован ФСБ России как средство криптографической защиты информации (СКЗИ) и средство электронной подписи.
К тому же токен РУТОКЕН ЭЦП-2.0 широко распространен и многие на нем хранят сертификаты для доступа на Госуслуги и другие порталы.
Для вычисления значения хэша на токене воспользуемся скриптом test_digest.tcl на языке Tcl:
#! /usr/bin/env tclsh
package require pki
lappend auto_path .
package require pki::pkcs11
#Задайте путь к вашей библиотеке PKCS#11
set pkcs11_module "/usr/local/lib64/librtpkcs11ecp_2.0.so"
#set pkcs11_module "/usr/local/lib64/libls11sw2016.so"
puts "Connect the Token and press Enter"
gets stdin yes
set handle [pki::pkcs11::loadmodule $pkcs11_module]
set slots [pki::pkcs11::listslots $handle]
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 token_slotlabel $slotlabel
set token_slotid $slotid
#Найден слот с токеном
break
}
}
proc usage {use error} {
puts "Copyright(C) Orlov Vladimir (http://soft.lissi.ru) 2019"
if {$use == 1} {
puts $error
puts "Usage:ndigest <stribog256|stribog512> <file for digest>n"
}
}
set countcert [llength $argv]
if { $countcert != 2 } {
usage 1 "Bad usage!"
exit
}
set digest_algo [lindex $argv 0]
if {$digest_algo != "stribog256" && $digest_algo != "stribog512"} {
usage 1 "Bad usage!"
exit
}
set file [lindex $argv 1]
if {![file exists $file]} {
usage 1 "File $file not exist"
exit
}
puts "Loading file for digest: $file"
set fd [open $file]
chan configure $fd -translation binary
set cert_user [read $fd]
close $fd
if {$cert_user == "" } {
usage 1 "Bad file: $file"
exit
}
set aa [dict create pkcs11_handle $handle pkcs11_slotid $token_slotid]
set digest_hex [pki::pkcs11::digest $digest_algo $cert_user $aa]
puts "digest_hex=n$digest_hex"
exit
Когда проявляется это расхождение в реализации? Пока что удалось определить, что данное расхождение возникает при подсчете хэш doc-файлов, созданных в MS Office. Причем хэш от первых 143 байтов считается одинаково, а уже при подсчете хэш от 144 байт значения получаются разные.
Первые 143 байта в шестнадцатиричном виде выглядят так:
d0cf11e0a1b11ae1000000000000000000000000000000003e000300feff0900060000000000000000000000010000000100000000000000001000002400000001000000feffffff0000000000000000ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff
Сохраним их в файле Doc1_143_hex.txt.
Первые 144 байта в шестнадцатиричном виде выглядят так:
d0cf11e0a1b11ae1000000000000000000000000000000003e000300feff0900060000000000000000000000010000000100000000000000001000002400000001000000feffffff0000000000000000ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff
Сохраним их в файле Doc1_144_hex.txt.
Для перевода из шестнадцатеричного вида в бинарный удобно воспользоваться скриптом hex2bin.tcl:
#!/usr/bin/tclsh
proc usage {use error} {
if {$use == 1} {
puts $error
puts "Usage:nhex2bin <file with hex> <file for bin>n"
}
}
set countcert [llength $argv]
if { $countcert != 2 } {
usage 1 "Bad usage!"
exit
}
set file [lindex $argv 0]
if {![file exists $file]} {
usage 1 "File $file not exist"
exit
}
set fd [open $file]
chan configure $fd -translation binary
set cert_user [read $fd]
close $fd
if {$cert_user == "" } {
usage 1 "Bad file with hex: $file"
exit
}
set cert_user [binary format H* $cert_user]
set fd [open [lindex $argv 1] w]
chan configure $fd -translation binary
puts -nonewline $fd $cert_user
close $fd
Преобразуем шестнадцатеричный код в бинарный:
$./hex2bin Doc1_143_hex.txt Doc1_143.bin
$./hex2bin Doc1_144_hex.txt Doc1_144.bin
$
Теперь можно проверить как вычисляется хэш различными реализациями:
Сначало считаем хэш для файла Doc1_143,bin:
$ ./openssl dgst -md_gost12_256 Doc1_143.bin
md_gost12_256(Doc1_143.bin)= e63bd3edc44f9a03fece4198b690a8ae291b973ae61b2a0f512a9a7479431a63
$ ./libressl dgst -streebog256 Doc1_143.bin
streebog256(Doc1_143.bin)= e63bd3edc44f9a03fece4198b690a8ae291b973ae61b2a0f512a9a7479431a63
$ ./gchash stribog256 Doc1_143.bin
e63bd3edc44f9a03fece4198b690a8ae291b973ae61b2a0f512a9a7479431a63 Doc1_143.bin
$ ./gost3411-2012 -2 Doc1_143.bin
GOST R 34.11-2012 (Doc1_143.bin) = e63bd3edc44f9a03fece4198b690a8ae291b973ae61b2a0f512a9a7479431a63
$
Наступил самый важный момент, момент проверки на сертифицированном СКЗИ:
$ ./test_digest.tcl stribog256 Doc1_143.bin
Connect the Token and press Enter
Loading file for digest: Doc1_143.bin
digest_hex=
e63bd3edc44f9a03fece4198b690a8ae291b973ae61b2a0f512a9a7479431a63
$
Как видим, все завершилось для хорошо.
Посмотрим, что будет для файла Doc1_144.bin:
$ ./openssl dgst -md_gost12_256 Doc1_144.bin
md_gost12_256(Doc1_144.bin)= c766085540caaa8953bfcf7a1ba220619cee50d65dc242f82f23ba4b180b18e0
$ ./libressl dgst -streebog256 Doc1_144.bin
streebog256(Doc1_144.bin)= 3965c99777eb1b64c783496fe950aa6540bc7baa399a3889995145afbdd76250
$
Все, значения хэшей не совпадают. Для чистоты эксперимента проверим и оставшиеся реализации:
$ ./gchash_1.7.10 stribog256 Doc1_144.bin
3965c99777eb1b64c783496fe950aa6540bc7baa399a3889995145afbdd76250 Doc1_144.bin
$ ./gost3411-2012 -2 Doc1_144.bin
GOST R 34.11-2012 (Doc1_144.bin) = c766085540caaa8953bfcf7a1ba220619cee50d65dc242f82f23ba4b180b18e0
$ ./test_digest.tcl stribog256 Doc1_144.bin
Connect the Token and press Enter
Loading file for digest: Doc1_144.bin
digest_hex=
c766085540caaa8953bfcf7a1ba220619cee50d65dc242f82f23ba4b180b18e0
$
Хэш, подсчитанный «знаменитой реализацией Дегтярева» совпадает с хэшом, подсчитанным в openssl с ГОСТ-овым engine, но не совпадает со значением хэша, посчитанным с помощью libgcrypt и libressl.
Аналогичный результат мы получим, если будем считать хэш stribog512.
Вывод один. Если вы хотите, чтобы электронная подпись ГОСТ Р 34.10-2012, формируемая средствами libressl и libgcrypt (а может и другими), была совместима с реализацией в openssl и, самое главное, с реализацией в СКЗИ, сертифицированных в системе сертификации ФСБ России, используйте проверенные реализации для вычисления хэшей. Надеюсь, эта публикация позволит избежать многих недоразумений, а авторам реализации stribog в libressl, libgrypt и возможно других поможет устранить эти расхождения. Сегодня, надо признать, в вышеназванных продуктах фактически реализован не ГОСТ Р 34.10-2012 а что-то другое. Это другой алгоритм. Приведенный тестовый пример, наверное, неплохо было бы включить как тестовый пример для ГОСТ Р 34.10-2012. А я иду править libgcrypt для Kleopatra и KMail. Сказание о Клеопарте и российской криптографии оказалось неоконченным.
P.S. Статья уже была готова, когда мой коллега сказал, что расхождение реализаций проявляются тогда, когда встречается достаточно длинная последовательность 0xFF. Она, эта последовательность, кстати, присутствует в начале файлов doc от MS Office. Я проверил, так оно и есть. Файл содержал 189 байт.
Автор: saipr