Пришла пора, несмотря на все пожары, исполнить свой гражданский долг – заплатить налоги. Платить налоги мы будем через портал Госуслуги. В личный кабинет портала Госуслуг будем входить с помощью электронной подписи (терминология портала Госуслуг ), т.е. имея на руках сертификат, полученный в аккредитованном удостоверяющем центре (УЦ), и закрытый ключ. И то и другое я храню на токене PKCS#11 с поддержкой российской криптографии:
И вот, выполнив свой гражданский долг, я решил еще раз проверить работу электронной подписи в офисном пакете libreoffice.
Почему я решил это сделать? Для доступа к порталу Госуслуг я использую ОС Linux и браузер Redfox, который представляет собой доработанный с учетом поддержки российской криптографии браузер Mozilla Firefox. Как известно, офисный пакет libreoffice в качестве хранилища сертификатов также использует хранилище NSS.
Браузер Redfox-52 был установлен в папку /usr/local/lib64/firefox-52.
Для подключения библиотек пакета NSS (Network Security Services) с поддержкой ГОСТ-алгоритмов устанавливаем значение переменной LD_LIBRARY_PATH следующим образом:
$export LD_LIBRARY_PATH=/usr/local/lib64/firefox-52:$LD_LIBRARY_PATH $
В качестве хранилища сертификатов в libreoffice как правило используется хранилище сертификатов из браузера Firefox, почтового клиента Thunderbird или интегрированного пакета Seamonkey. Ничто не мешает использовать хранилище сертификатов браузеров GoogleChrome/Cromium или создать свое независмое хранилище (Сервис->Параметры->Безопасность->Сертификат):

После того как выбрано хранилище, подключены библиотеки, запускаем libreoffice, создаем файл формата odt и пытаемся его подписать (Файл->Цифровые подписи->Цифровые подписи).
Сертификаты в хранилище Firefox/NSS успешно отображаются и проверяются:

Однако, подпись после выбора сертификата и нажатия кнопки «ОК» не формируется:

Похоже libreoffice не хочет понимать российские криптоалгоритмы, несмотря на то, что используется NSS из браузера Redfox, который понимает ГОСТ-овые алгоритмы, что подтверждается успешной проверкой сертификатов.
Делаем вторую попытку: на этот раз попытаемся подписать PDF-файл. Для этого подготовленный документ экспортируем в PDF-формат. Для подписания PDF-файла его, естественно, необходимо загрузить (Файл->Цифровые подписи->Подписать PDF). После его загрузки пытаемся его подписать (Файл->Цифровые подписи->Цифровые подписи) (см.выше, выбираем сертификат, прописываем, например, цель подписания документа):

И подпись формируется!!! Мы видим, что подпись, сформирования на базе сертификата «Test 12 512» с ключом ГОСТ Р 34.10-2012 512 бит. Подпись верна.
Выходим из libreoffice. Снова запускаем libreoffice, загружаем подписанный pdf-файл, проверяем подписи. Все ОК. Просматривает сертификаты подписантов. Все ОК. Чудеса! Подпись PDF-файлов работает. Ставим вторую, третью подпись… Все работает. Но что-то не дает покоя. Делаем дополнительную проверку.
Открываем подписанный PDF-файл (я использовал встроенный редактор от mc – Midnight Commander — консольный файловый менеджер для Linux). Находим электронную подпись (/Type/Sig/ ):

Как видим, подпись хранится в символьном шестнадцатеричном виде. Копируем ее и сохраняем в файле. Для конвертации файла в бинарный вид (DER-кодировка) воспользуемся утилитой xxd:
$xxd –p –r <файл с подписью из PDF> > <файл>.der $
Полученный файл содержит отсоединенную подпись формата PKCS#7 в DER-кодировке. Теперь это подпись можно просмотреть любым asn1-prase-ом, например, утилитой openssl. Но поскольку мы говорим о пакете NSS, то воспользуемся утилитой derdump или утилитой pp:
$pp –t p7 –u –i pkcs7_detach.p7 PKCS #7 Content Info: PKCS #7 Signed Data: Version: 1 (0x1) Digest Algorithm List: Digest Algorithm (1): SHA-256 Content Information: PKCS #7 Data: <no content> Certificate List: Certificate (1): Data: Version: 3 (0x2) Serial Number: 4107 (0x100b) Signature Algorithm: GOST R 34.10-2012 signature with GOST R 34.11-2012-512 Issuer: "E=ca_12_512@lissi.ru,OGRN=1234567890123,INN=1234 56789012,CN=УЦ 12_512,O=УЦ 12_512,L=GnuPG ГОСТ -2012-512,ST=Московская область,C=RU" Validity: Not Before: Sat Sep 08 07:17:56 2018 Not After : Tue Sep 12 07:17:56 2023 Subject: "C=RU,ST=Московская область,CN=Ф И О,SN=Фам,givenName=И О,E=xx@xx.ru,L=Город ,STREET=Улица,INN=123456789012,SNILS=12345678901" Subject Public Key Info: Public Key Algorithm: GOST R 34.10-2012 512 Public Key: . . . Digest Encryption Algorithm: GOST R 34.10-2012 Key 512 Encrypted Digest: 34:9d:6f:37:e6:60:00:ed:fe:ef:f7:96:db:52:66:e1: 47:4c:5d:da:7f:9f:f3:20:50:ac:73:6c:97:db:f9:8d: 43:9b:8f:40:61:99:d3:4b:17:08:b8:34:e3:1e:92:76: b1:0c:dd:37:01:1e:2a:30:45:68:06:af:3d:33:5e:2f: 71:c8:17:b3:a9:8a:6b:2f:78:9e:e4:b2:00:59:6f:5a: a0:c5:9e:be:1e:4b:ca:d5:64:25:50:1a:6f:f9:55:b8: 3a:cf:37:a0:04:eb:89:b4:6c:39:77:27:92:de:61:c7: b1:d3:a5:2f:ef:66:9b:f5:71:42:77:0a:d2:10:7f:50 $
И тут стало понятно, что не все так хорошо. Да, алгоритм подписи Digest Encryption Algorithm: GOST R 34.10-2012 Key 512 в соответствии с выбранным для подписи сертификатом, но подпись формируется от хэша, посчитанному по алгоритму SHA-256 (Digest Algorithm (1): SHA-256). А это неправильно с точки: хэш для GOST R 34.10-2012 Key 512 должен считаться по алгоритму ГОСТ Р 34.11-2012-512.
Приступаем а анализу исходного кода libreoffice: не так страшен черт как его малюют. В данной статье мы рассматриваем использование пакета NSS для формирования электронной подписи. Если кто предпочитает на платформе MS Windows, использовать CryptoAPI (и, соответственно, ГОСТ-CSP), может по аналогии с данным материалом сделать соответствующую доработку.
Анилиз показал, что правки придется внести всего в два файла:
— ~/libreoffice-5.3.7.2/vcl/source/gdi/pdfwriter_impl.cxx
— ~/libreoffice-5.3.7.2/xmlsecurity/source/pdfio/pdfdocument.cxx
Эти изменения связаны с правильным выбором хэш-функции для ГОСТ-овых сертификатов. Выбор хэш-функции будем определять в зависимости от типа ключа сертификата. Выбор хэш-алгоритма, например, в PDFWriter::Sign (файл pdfwriter_impl.cxx) будет выглядеть так:
bool PDFWriter::Sign(PDFSignContext& rContext) { #ifndef _WIN32 /*Добавленные переменные*/ SECKEYPublicKey *pubk = NULL; SECOidTag hashAlgTag; HASH_HashType hashType; int hashLen; CERTCertificate *cert = CERT_DecodeCertFromPackage(reinterpret_cast<char *>(rContext.m_pDerEncoded), rContext.m_nDerEncoded); if (!cert) { SAL_WARN("vcl.pdfwriter", "CERT_DecodeCertFromPackage failed"); return false; } /*Получаем из сертификата открытый ключ*/ pubk = CERT_ExtractPublicKey(cert); if (pubk == NULL) return NULL; /*Проверяем тип открытого ключа*/ switch(pubk->keyType){ case gost3410Key: hashAlgTag = SEC_OID_GOSTHASH; hashType = HASH_AlgGOSTHASH; hashLen = SHA256_LENGTH; break; case gost3410Key_256: hashAlgTag = SEC_OID_GOST3411_2012_256; hashType = HASH_AlgGOSTHASH_12_256; hashLen = SHA256_LENGTH; break; case gost3410Key_512: hashAlgTag = SEC_OID_GOST3411_2012_512; hashLen = SHA256_LENGTH * 2; hashType = HASH_AlgGOSTHASH_12_512; break; default: hashAlgTag = SEC_OID_SHA256; hashType = HASH_AlgSHA256; hashLen = SHA256_LENGTH; break; } /*Вычисление хэш*/ HashContextScope hc(HASH_Create(hashType)); . . . }
Остальные изменения по логике аналогичны этим. Патч для файла ~/libreoffice-5.3.7.2/vcl/source/gdi/pdfwriter_impl.cxx находится
здесь:
--- pdfwriter_impl_ORIG.cxx 2017-10-25 17:25:39.000000000 +0300 +++ pdfwriter_impl.cxx 2018-10-31 19:48:32.078482227 +0300 @@ -6698,6 +6698,9 @@ CERTCertificate *cert, SECItem *digest) { + SECKEYPublicKey *pubk = NULL; + SECOidTag hashAlgTag; + NSSCMSMessage *result = NSS_CMSMessage_Create(nullptr); if (!result) { @@ -6732,8 +6735,31 @@ NSS_CMSMessage_Destroy(result); return nullptr; } - + pubk = CERT_ExtractPublicKey(cert); + if (pubk == NULL) + return NULL; + switch(pubk->keyType){ + case gost3410Key: + hashAlgTag = SEC_OID_GOSTHASH; +fprintf(stderr, "CreateCMSMessage: gost3410Key Use HASH_AlgGOSTHASH_=%d\n", hashAlgTag); + break; + case gost3410Key_256: + hashAlgTag = SEC_OID_GOST3411_2012_256; +fprintf(stderr, "CreateCMSMessage: gost3410Key_256 Use HASH_AlgGOSTHASH_=%d\n", hashAlgTag); + break; + case gost3410Key_512: + hashAlgTag = SEC_OID_GOST3411_2012_512; +fprintf(stderr, "CreateCMSMessage: gost3410Key_512 Use HASH_AlgGOSTHASH_=%d\n", hashAlgTag); + break; + default: + hashAlgTag = SEC_OID_SHA256; + break; + } +/* *cms_signer = NSS_CMSSignerInfo_Create(result, cert, SEC_OID_SHA256); +*/ + *cms_signer = NSS_CMSSignerInfo_Create(result, cert, hashAlgTag); + if (!*cms_signer) { SAL_WARN("vcl.pdfwriter", "NSS_CMSSignerInfo_Create failed"); @@ -6773,8 +6799,8 @@ NSS_CMSMessage_Destroy(result); return nullptr; } + if (NSS_CMSSignedData_SetDigestValue(*cms_sd, hashAlgTag, digest) != SECSuccess) - if (NSS_CMSSignedData_SetDigestValue(*cms_sd, SEC_OID_SHA256, digest) != SECSuccess) { SAL_WARN("vcl.pdfwriter", "NSS_CMSSignedData_SetDigestValue failed"); NSS_CMSSignedData_Destroy(*cms_sd); @@ -6982,6 +7008,10 @@ bool PDFWriter::Sign(PDFSignContext& rContext) { #ifndef _WIN32 + SECKEYPublicKey *pubk = NULL; + SECOidTag hashAlgTag; + HASH_HashType hashType; + int hashLen; CERTCertificate *cert = CERT_DecodeCertFromPackage(reinterpret_cast<char *>(rContext.m_pDerEncoded), rContext.m_nDerEncoded); @@ -6990,8 +7020,33 @@ SAL_WARN("vcl.pdfwriter", "CERT_DecodeCertFromPackage failed"); return false; } + pubk = CERT_ExtractPublicKey(cert); + if (pubk == NULL) + return NULL; + switch(pubk->keyType){ + case gost3410Key: + hashAlgTag = SEC_OID_GOSTHASH; + hashType = HASH_AlgGOSTHASH; + hashLen = SHA256_LENGTH; + break; + case gost3410Key_256: + hashAlgTag = SEC_OID_GOST3411_2012_256; + hashType = HASH_AlgGOSTHASH_12_256; + hashLen = SHA256_LENGTH; + break; + case gost3410Key_512: + hashAlgTag = SEC_OID_GOST3411_2012_512; + hashLen = SHA256_LENGTH * 2; + hashType = HASH_AlgGOSTHASH_12_512; + break; + default: + hashAlgTag = SEC_OID_SHA256; + hashType = HASH_AlgSHA256; + hashLen = SHA256_LENGTH; + break; + } + HashContextScope hc(HASH_Create(hashType)); - HashContextScope hc(HASH_Create(HASH_AlgSHA256)); if (!hc.get()) { SAL_WARN("vcl.pdfwriter", "HASH_Create failed"); @@ -7005,15 +7060,18 @@ HASH_Update(hc.get(), static_cast<const unsigned char*>(rContext.m_pByteRange2), rContext.m_nByteRange2); SECItem digest; - unsigned char hash[SHA256_LENGTH]; + unsigned char hash[SHA256_LENGTH * 2]; + digest.data = hash; - HASH_End(hc.get(), digest.data, &digest.len, SHA256_LENGTH); + HASH_End(hc.get(), digest.data, &digest.len, hashLen); + hc.clear(); #ifdef DBG_UTIL { FILE *out = fopen("PDFWRITER.hash.data", "wb"); - fwrite(hash, SHA256_LENGTH, 1, out); + fwrite(hash, hashLen, 1, out); + fclose(out); } #endif @@ -7078,8 +7136,8 @@ fclose(out); } #endif + HashContextScope ts_hc(HASH_Create(hashType)); - HashContextScope ts_hc(HASH_Create(HASH_AlgSHA256)); if (!ts_hc.get()) { SAL_WARN("vcl.pdfwriter", "HASH_Create failed"); @@ -7090,16 +7148,19 @@ HASH_Begin(ts_hc.get()); HASH_Update(ts_hc.get(), ts_cms_signer->encDigest.data, ts_cms_signer->encDigest.len); SECItem ts_digest; - unsigned char ts_hash[SHA256_LENGTH]; + unsigned char ts_hash[SHA256_LENGTH * 2]; + ts_digest.type = siBuffer; ts_digest.data = ts_hash; - HASH_End(ts_hc.get(), ts_digest.data, &ts_digest.len, SHA256_LENGTH); + HASH_End(ts_hc.get(), ts_digest.data, &ts_digest.len, hashLen); + ts_hc.clear(); #ifdef DBG_UTIL { FILE *out = fopen("PDFWRITER.ts_hash.data", "wb"); - fwrite(ts_hash, SHA256_LENGTH, 1, out); + fwrite(ts_hash, hashLen, 1, out); + fclose(out); } #endif @@ -7111,7 +7172,8 @@ src.messageImprint.hashAlgorithm.algorithm.data = nullptr; src.messageImprint.hashAlgorithm.parameters.data = nullptr; - SECOID_SetAlgorithmID(nullptr, &src.messageImprint.hashAlgorithm, SEC_OID_SHA256, nullptr); + SECOID_SetAlgorithmID(nullptr, &src.messageImprint.hashAlgorithm, hashAlgTag, nullptr); + src.messageImprint.hashedMessage = ts_digest; src.reqPolicy.type = siBuffer; @@ -7340,11 +7402,13 @@ // Write ESSCertIDv2.hashAlgorithm. aCertID.hashAlgorithm.algorithm.data = nullptr; aCertID.hashAlgorithm.parameters.data = nullptr; - SECOID_SetAlgorithmID(nullptr, &aCertID.hashAlgorithm, SEC_OID_SHA256, nullptr); + SECOID_SetAlgorithmID(nullptr, &aCertID.hashAlgorithm, hashAlgTag, nullptr); + // Write ESSCertIDv2.certHash. SECItem aCertHashItem; - unsigned char aCertHash[SHA256_LENGTH]; - HashContextScope aCertHashContext(HASH_Create(HASH_AlgSHA256)); + unsigned char aCertHash[SHA256_LENGTH*2]; + HashContextScope aCertHashContext(HASH_Create(hashType)); + if (!aCertHashContext.get()) { SAL_WARN("vcl.pdfwriter", "HASH_Create() failed"); @@ -7354,7 +7418,8 @@ HASH_Update(aCertHashContext.get(), reinterpret_cast<const unsigned char *>(rContext.m_pDerEncoded), rContext.m_nDerEncoded); aCertHashItem.type = siBuffer; aCertHashItem.data = aCertHash; - HASH_End(aCertHashContext.get(), aCertHashItem.data, &aCertHashItem.len, SHA256_LENGTH); + HASH_End(aCertHashContext.get(), aCertHashItem.data, &aCertHashItem.len, hashLen); + aCertID.certHash = aCertHashItem; // Write ESSCertIDv2.issuerSerial. IssuerSerial aSerial;
Патч для файла ~/libreoffice-5.3.7.2/xmlsecurity/source/pdfio/pdfdocument.cxx находится
здесь:
--- pdfdocument_ORIG.cxx 2017-10-25 17:25:39.000000000 +0300 +++ pdfdocument.cxx 2018-10-31 19:49:34.174485641 +0300 @@ -2400,6 +2400,19 @@ case SEC_OID_PKCS1_SHA512_WITH_RSA_ENCRYPTION: eOidTag = SEC_OID_SHA512; break; + case SEC_OID_GOST3410_SIGN_256: + case SEC_OID_GOST3411_2012_256: + eOidTag = SEC_OID_GOST3411_2012_256; + break; + case SEC_OID_GOST3410_SIGN_512: + case SEC_OID_GOST3411_2012_512: + eOidTag = SEC_OID_GOST3411_2012_512; + break; + case SEC_OID_GOST3410_SIGNATURE: + case SEC_OID_GOSTHASH: + eOidTag = SEC_OID_GOSTHASH; + break; + default: break; } @@ -2453,6 +2466,16 @@ case SEC_OID_SHA512: nMaxResultLen = msfilter::SHA512_HASH_LENGTH; break; + case SEC_OID_GOST3411_2012_256: + nMaxResultLen = msfilter::SHA256_HASH_LENGTH; + break; + case SEC_OID_GOST3411_2012_512: + nMaxResultLen = msfilter::SHA512_HASH_LENGTH; + break; + case SEC_OID_GOSTHASH: + nMaxResultLen = msfilter::SHA256_HASH_LENGTH; + break; + default: SAL_WARN("xmlsecurity.pdfio", "PDFDocument::ValidateSignature: unrecognized algorithm"); return false;
После внесения изменений проводим сборку пакета libreoffice. Внесенные изменения коснулись трех библиотек (/usr/lib64/libreoffice/program):
- libvcllo.so;
- libxmlsecurity.so;
- libxsec-xmlsec.so
Именно эти три библиотеки были заменены в установленном дистрибутиве libreoffice (/usr/lib64/libreoffice/program).
После этого подписание и проверка ГОСТ-подписи в PDF-файлах прошла без сучка и задоринки. И тут на одном из сайтов попадается на глаза такая выдержка:
У Федеральной налоговой службы есть отличный сервис для получения выписки из ЕГРЮЛ для любого юридического лица, причем абсолютно бесплатно. Выписку можно получить в виде документа формата PDF, подписанном квалифицированной электронной подписью. И такую выписку можно отправить в коммерческий банк, госучреждение, и с вас не попросят ее в бумажном виде. В общем, очень удобно.Заказываем, получаем и проверяем:

Стоит напомнить, что не следует забывать устанавливать в хранилище цепочку доверенных сертификатов для сертификата подписанта. Но это естественно.
Все, теперь есть возможность использования электронной подписи (одной или несколько) в PDF-файлах. Это очень удобно как при согласовании документов, так и хранении документов.
А если кто привык работать с классической электронной подписью в формате PKCS#7 как присоединенной, так и отсоединенной, то для них подготовлена обновленная версия (для платформы Linux и Windows) графического пакета GUINSSPY:

Разработка велась на Python3 и если на платформе Linux проблем не было, то на MS Windows пришлось попотеть с кодировками. Фактически это была отдельная разработка и это требует отдельной статьи. Все эти нюансы можно увидеть в исходном коде.
С помощью этой утилиты можно создать хранилище сертификатов для libreoffice, управлять сертификатами, подписывать файлы и т.д.:

Утилита также позволяет создать запрос на сертификат с генерацией ключевой пары, который затем можно передать в удостоверяющий центр, а полученный сертификат установить в хранилище:

И вот если производители отечественных форков Linux доработали различные пакеты (NSS, Firefox, Thunderbiird, GnuPG/SMIME, SSH, KMail, Kleopatra, LibreOffice, OpenSSL, и т.д и т.п.) для работы с российской криптографией, то тогда можно было бы говорить об импортозамещении в области криптографии.