Information Security
Cryptography
Open source
Programming
IT Standards
April 29

Об open-source реализациях хэш-функции ГОСТ Р 34.11-2012 и их влиянии на электронную подпись ГОСТ Р 34.10-2012

В свое время реализация отечественных криптографических алгоритмов в библиотеке 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:

test_digest.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 байт.
+12
5.4k 43
Comments 18