Формат подписи для загрузки подписанных xml файлов должен быть base64
На хабре уже была обзорная статья о механизмах создания ЭЦП в браузере, где было рассказано о связке Крипто-Про CSP +их же плагин к браузерам. Как там было сказано, предварительные требования для работы — это наличие CryptoPro CSP на компьютере и установка сертификата, которым собираемся подписывать. Вариант вполне рабочий, к тому же в версии 1.05.1418 плагина добавлена работа с подписью XMLDsig. Если есть возможность гонять файлы на клиент и обратно, то для того, чтобы подписать документ на клиенте, достаточно почитать КриптоПрошную справку. Все делается на JavaScript вызовом пары методов.
Однако, что если файлы лежат на сервере и хочется минимизировать трафик и подписывать их, не гоняя на клиент целиком?
Интересно?
Итак, клиент/серверный алгоритм формирования цифровой подписи XMLDSig.
Информацию об спецификации по XMLDsig можно найти по адресу тут.
Я буду рассматривать формирование enveloping signature (обворачивающей подписи) для xml-документа.
Простой пример подписанного xml:
Чтобы лучше понять, что из себя представляет enveloping signature, предлагаю краткий перевод описания тэгов из спецификации:
- Signature -содержит данные подписи, включая саму подпись и сертификат.
- SignedInfo -содержит информация о подписываемых данных и алгоритмах, которые будут применяться при формировании подписи.
- CanonicalizationMethod -указывает канокализирующий алгоритм, который будет применен к SignedInfo перед вычислением подписи.
- SignatureMethod -указывает алгоритм, используемый для генерации и валидации подписи. На вход алгоритму приходит канокализированный тэг SignedInfo.
- Reference -может встречаться 1 или больше раз. Содержит информацию о подписываемых данных, включая местоположение данных в документе, алгоритм вычисления хэша от данных, преобразования, и сам хэш.
- Transforms и Transform -перобразования над данными. На вход первого transform приходит результат разыменовании(dereferencing ) атрибута URI тэга Reference. На вход каждому последующему transform приходит результат предыдущего, результат последнего transform приходит на вход алгоритма, указанного в DigestMethod. Обычно в transform указывают XPath, определяющий защищаемые части документа.
- DigestMethod -алгоритм вычисления хэша от результатов Transforms.
- DigestValue -значение хэша от результатов Transforms.Часто это хэш от данных, на которые указывает Reference URI.
- SignatureValue -собственно сама подпись, ради формирования которой все и затевается.
- KeyInfo — информация о ключе, на тут интересует тэг X509Certificate, который содержит base64encoded сертификат из ключа, которым подписаны данные.
Итак, исходные данные:
- устанавливаем CryptoPro CSP 3.6
- устанавливаем КриптоПро ЭЦП Browser Plugin
- устанавливаем сертификат с флешки в локальное хранилище (см. инструкцию
пункт «2.5.2.2. Установка личного сертификата, хранящегося в контейнере закрытого ключа»)
Посмотреть хэш можно с помощью hashObject.Value
3.2)Считаем хэш на сервере и отправляем на клиент. Этот вариант у меня так и не заработал, но честно сказать я особо и не пытался.
Возможно хэш надо преобразовывать в base64.
Отправляем на клиент, там используем
Именно на методе hashObject.SetHashValue у меня падала ошибка. Разбираться я не стал, но криптопрошном форуме говорят, что можно как-то заставить ее работать.
Если соберетесь реализовывать серверный алгоритм генерации хэша, то вот пара полезных советов:
1) Посчитайте хэш на клиенте и на сервере от пустой строки. он должен совпадать, это значит ваши алгоритмы одинаковые.
Для GOST3411 это следующие значения:
base64: mB5fPKMMhBSHgw+E+0M+E6wRAVabnBNYSsSDI0zWVsA=
hex: 98 1e 5f 3c a3 0c 84 14 87 83 0f 84 fb 43 3e 13 ac 11 01 56 9b 9c 13 58 4a c4 83 23 4c d6 56 c0
2) Добейтесь, чтобы у вас совпадали хэши для произвольных данных, генерируемые на клиенте и на сервере.
После этого можно пересылать клиенту только хэш от SignedInfo вместо всего SignedInfo.
Шаг №4.(клиент)
Генерируем SignatureValue и отсылаем на сервер SignatureValue и информацию о сертификате
Возвращем на сервер binReversedSignatureString и certValue.
Код функций из utils не выкладываю. Мне его подсказали на форуме криптоПро и его можно посмотреть в этой теме
Шаг №5. (сервер)
Заменяем в сгенерированном на шаге №1 тэге Signature значения тэгов SignatureValue и X509Certificate значениями, полученными с клиента
Шаг №6. (сервер)
Верифицируем карточку.
Если верификация прошла успешно, то все хорошо. В результате мы получаем на сервере документ, подписанный клиентским ключом, не гоняя туда-обратно сам файл.
Примечание: если работа ведется с документом, уже содержащим подписи, то их надо отсоединить от документа до шага №1 и присоединить к документу обратно после шага №6
В заключение хочется сказать большое спасибо за помощь в нахождении алгоритма участникам форума КриптоПро dmishin и Fomich.
Без их советов я бы плюхался с этим в разы дольше.
Отмечу несколько особенностей формата XMLDSig:
2. Различные части XML-документа могут быть подписаны несколькими исполнителями.
3. XML-подпись может находиться на разных уровнях по отношению к подписываемому объекту:
- в структуре подписи может находиться URI (унифицированный идентификатор ресурса);
- XML-подпись может находиться на одном уровне с подписываемым узлом;
- XML-подпись может находиться внутри подписываемого узла;
- подписываемый узел может находиться внутри структуры XML-подписи.
4. Для проверки действительности ЭП необходим доступ к объекту подписания.
Структура SOAP-коверта
Криптографические алгоритмы и каноникализация.
Для решения задачи были использованы ГОСТ Р 34.11-94 — российский криптографический стандарт вычисления хеш-функции и ГОСТ Р 34.10-2001 - стандарт электронной подписи.
В силу гибкости правил составления XML, одна и та же структура документа и одна и та же часть информации могут быть представлены различными XML-документами. Рассмотрим два документа:
С логической точки зрения они равнозначны, то есть имеют одинаковую XML-схему. Но XML-файлы этих листингов не содержат одну и ту же последовательность символов, что повлечёт разные результаты, например, при получении значения хэша.
Библиотека SIRCrypt
Для реализации подписания XML в DIRECTUM была написана COM-библиотека, внутри которорй описаны 3 класса: Hasher, Signer и XMLCanonicalizer для получения хэша, значения ЭП и каноникализации XML-документов соответственно.
> regasm.exe "путь к dll" /codebase /tlb
Объект Hasher для вычисления хэша по ГОСТ
Содержит поля Content (тип 'строка') и HashValueAsBase64 (тип 'строка'), а также метод для вычисления значения хэш-функции Hash(). Для вычисления необходимо означить Content, вызвать метод Hash(), в результате которого в поле HashValueAsBase64 запишется значение хэш-функции в Base64.
Объект Signer для получения значения ЭП по ГОСТ
Содержит поля Content (тип 'строка'), ContainerName (тип 'строка'), CertificateAsPEM (тип 'строка'), BESignatureValueAsBase64 (тип 'строка'), метод Sign(). После инициализации объекта необходимо означить Content (данные для подписания), ContainerName (имя контейнера закрытого ключа сертификата), вызвать метод Sign(). После чего в поле CertificateAsPEM попадёт соответствующий закрытому ключу сертификат в Base64, а в поле BESignatureValueAsBase64 значение подписи в виде Base64-строки.
Объект XMLCanonicalizer для каноникализации XML
Содержит поля XMLContent (тип 'строка'), CanonicalXML (тип 'строка'), метод C14NExc(). Для получения канонической формы XML нужно означить XMLContent, вызвать C14NExc(), получить результат из поля CanonicalXML.
Структура XML-подписи
Создание подписи выглядит следующим образом: сначала формируется основа soap-пакета, узлы Header и Body. Body заполняется данными и добавляется атрибут wsu:ID="Body" - идентификатор подписываемых данных.
Далее нужно подготовить структуру Security и включить её в Header.
Заполнение структуры Security происходит в следующем порядке:
- Берётся значение хэш-функции от узла Body в каноническом виде и помещается в узел DigestValue.
- Узел SignedInfo приводится к каноническому виду, подписывается ЭП. Результат в формате Base64-строки попадает в узел SignatureValue.
- Открытый ключ сертификата, которым было выполнено подписание помещается в узел BinarySecurityToken в формате строки Base64.
Для того, чтобы проверить сформированную таким образом ЭП необходимо проделать все действия в обратном порядке, а именно:
- получить каноническую форму элемента SignedInfo.
- С использованием резльтата предыдущего шага проверить, действительно ли значение ЭП из узла SignatureValue с помощью открытого ключа сертификата. На данном этапе проверяется только корректность ЭП, что не гарантирует неизменность данных.
- Если проверка действительности ЭП пройдена успешно, сравнивается хэш из узла DigestValue и хэш от узла с данными. Если они неравны, то подписанные данные изменены и вся ЭП недействительна.
Пример использования
Пакет разработки и библиотека
Примеры подписания XML на ISBL (сценарий): dev.zip (5,95 Кб)
Для постоянного использования код, выполняющий типовое подписание готового SOAP-конверта, вынесен в функцию SignSOAP().
Для подписания используется сертификат из личного хранилища сертификатов текущего пользователя.
Cертификат для тесподписания можно получить на тестовом центре сертификации КриптоПРО.
На входе мы имеем два или более файлов (тело электронного документа и файлы отсоединённых ЭП).
Необходимо выполнить загрузку документа с ЭП в систему DIRECTUM и обеспечить удобный доступ пользователя к информации об ЭП.
И обратная задача: в системе DIRECTUM есть подписанный электронный документ, необходимо выгрузить тело документа и его ЭП в отдельные файлы.
В системе DIRECTUM «из коробки» есть возможность импортировать документы с ЭП в формате ESD (см. EDocuments.CreateNewFromFile). Посмотрим, получится ли сгенерировать ESD из имеющихся данных (файлы тела документа и ЭП). Для этого создадим в системе тестовый документ, подпишем его и экспортируем в ESD.
ESD представляет собой XML следующего содержания:
Как видим, ESD, кроме самого тела документа и ЭП, содержит ещё много различных реквизитов (ИД, Дата изменения, криптопровайдер, имя пользователя-подписанта и т.п.).
В нашем случае, на этапе импорта мы имеем только файлы тела документа и ЭП. Некоторую информацию о сертификате и его владельце можно достать из ЭП. Но сначала проверим, можно ли без этого обойтись и импортировать в систему ESD, указав только тело документа и ЭП (в CDATA).
Сначала получаем ошибку «Недопустимое имя типа подписи ""». Хорошо, означим атрибут в ESD «SignatureType="Approving"».
После этого импорт проходит, но, при просмотре ЭП документа, получаем ошибку «Подпись недостоверна».
Методом проб и ошибок можно установить минимальный набор реквизитов ESD, которые необходимо обязательно заполнить:
- Extension – расширение (DOC, DOCX…).
- CryptoProvider – плагин для подписания (CryptoPro, Capicom. ).
- Version – версия плагина для подписания.
- SignatureType – тип ЭП (Approving/Authenticating).
Бинго! Расширение документа мы знаем, тип ЭП можно выбрать «по-умолчанию», например, утверждающая (Approving), а вот с плагином оказалось не всё так просто. В системе DIRECTUM есть три плагина для взаимодействия с криптопровайдерами: Bicrypt signing, Capicom Encryption и CryptoPro Encryption. Bicrypt и CryptoPro "заточены" для одноименных CSP. Универсальным же из всех перечисленных является Capicom. На входе у нас только файлы документа и ЭП. Как и с помощью чего было выполнено подписание, мы не знаем. Из ЭП сведений об используемом криптопровайдере достать, к сожалению, не получилось.
Таким образом, в атрибуте ESD «CryptoProvider» необходимо указывать «Capicom Encryption», а «Version=”2.0”». (Подробностей описания механизмов работы с ЭП здесь касаться не буду, т.к. это тема уже отдельной статьи и даже не одной.)
Примечание: «Version=”2.0”» актуально для версии IS-Builder 7.16.0.1706 и выше. В ранних версиях IS-Builder были проблемы с плагином Capicom Encryption в части работы с ЭП, которые были сгенерированы по алгоритмам ГОСТ. Таким образом, для IS-Builder старше чем 7.16.0.1706 версию плагина указывать не нужно, но при просмотре ЭП по ГОСТ будет ошибка «Подпись недостоверна». Ещё один повод обновиться на новую версию!
У Capicom есть ещё одна особенность. Как правило, отсоединённая ЭП кодируется в BASE64 и передаётся в файле с расширением «.p7s». В ESD для плагина Capicom ЭП должна быть закодирована в BASE64 два раза (а, например, для CryptoPro хватит и одного раза). Поэтому, при формировании ESD, необходимо проверить, закодирована ли ЭП в BASE64, если нет, то закодировать 2 раза, если да, то закодировать в BASE64 ещё один раз.
После того как мы сгенерировали XML в формате ESD, можно смело импортировать структурированный документ в DIRECTUM.
При просмотре подписей в системе, пользователи смогут увидеть всю необходимую информацию: ФИО владельца сертификата и дату подписания, эти данные подтянутся автоматом, так что отдельно указывать их в ESD смысла нет.
Код можно посмотреть в функции «CreateEDocWithSigFromFile». Пакет разработки прилагается.
С экспортом ЭП в файлы всё обстоит гораздо проще. Каких-то подводных камней и хитростей здесь нет.
Выгрузить ЭП можно двумя способами.
Первый способ: через ESD (по аналогии с импортом, но в обратном порядке).
- Выгрузить подписанный документ в ESD.
- Пробежаться по XML и выгрузить ЭП в отдельные файлы.
- Кроме самих ЭП в ESD можно найти много полезной информации, которую тоже можно использовать при выгрузке.
- Такой способ обработки достаточно медленный, т.к. нужно разбирать XML.
Второй способ: получить ЭП напрямую из базы с помощью SQL-запроса. Как можно подсмотреть в справке, нас интересует таблица «SBEDocSignature», где в поле «Sign» типа «image» хранится не что иное, как Электронная подпись документа.
С помощью нехитрого SQL-запроса можно быстро достать электронные подписи документа
- Высокая скорость
- В БД информации не меньше, чем в ESD.
Пример реализации функции экспорта ЭП «ExportSignaturesFromEDocVer». Пакет разработки прилагается.
Данный функционал может применяться в интеграционных решениях, где необходима передача электронных документов с отсоединённой электронной подписью, а интегрируемая система не умеет генерировать ESD.
При использовании реализованного в статье решения, файл отсоединенной электронной подписи не проходил проверку на валидность на сайте «Госуслуги» и в установленной у заказчика программе «КриптоПро».
Постановка задачи
Разработать функцию, осуществляющую экспорт документов с открепленной электронной подписью. В результате ее выполнения из Directum должны выгружаться два файла: файл документа и файл отсоединенной электронной подписи формата .sig. Оба файла должны пройти проверку на валидность в специализированном ПО (например, КриптоАРМ).
Также необходимо разработать функцию, осуществляющую импорт документов с открепленной электронной подписью. В результате ее выполнения в Directum должен создаваться подписанный электронный документ, содержимое которого получено из файла документа, а подпись – из файла отсоединенной подписи формата ‘.sig’.
Функция экспорта
При использовании реализованной в статье функции ExportSignaturesFromEDocVer файл отсоединенной электронной подписи не проходил проверку на сайте «Госуслуги» и в «КриптоПро». Выяснилось, что в нашем случае функция неверно получала тело подписи из базы данных, вследствие чего было решено реализовать второй вариант экспорта отсоединенной подписи, описанный в статье: через чтение ESD-файла.
Кроме того, из функции была удалена возможность выгрузки нескольких подписей, так как по требованиям заказчика необходимо было на выходе иметь один файл подписи, а не несколько.
Вопрос выгрузки нескольких отсоединенных подписей в один файл остался открытым.
В результате экспорт был реализован следующим образом:
Функция импорта
Предложенное в упомянутой выше статье решение производило импорт файла формата ‘.p7s’, что не подходило по требованиям заказчика, но при дальнейшем изучении форматов ‘.p7s’ и ‘.sig’ было выяснено, что подпись можно выгружать в файл формата ‘.sig’.
При использовании функции CreateEDocWithSigFromFile в чистом виде для импорта документа в формате ‘.doc’ и файла отсоединенной подписи формата ‘.p7s’ возникли другие трудности. В результате выполнения функции создавался документ с корректным содержимым, но недействительной подписью:
Причиной такого поведения стали комментарии « -----BEGIN CMS----- » и « -----END CMS----- », добавляемые некоторыми программами при экспорте отсоединенной электронной подписи в текст подписи. Программный поиск и удаление этих комментариев решило проблему с некорректным чтением электронной подписи системой.
Функция из статьи может импортировать файл с несколькими файлами подписей, но в нашем случае заказчику этот функционал не был необходим, поэтому он был исключен из итоговой функции.
После всех изменений функция CreateEDocWithSigFromFile приняла следующий вид:
Как с помощью КриптоПро выгрузить подпись для методов PostMessagePatch и GenerateUniversalTransferDocumentXmlForBuyer . Подпись в формате X509 Base64 не подходит для метода PostMessagePatch, ошибка "Invalid signature". Файл подписи в формате PKCS7 (.p7b) кодированный в Base64 дает ошибку "The server encountered an internal error or misconfiguration and was unable to complete your request"
The text was updated successfully, but these errors were encountered:
atytsky commented Jan 24, 2020 •
@elvirazakharova Что вы и как пытаетесь подписать? У вас есть бинарное представление подписи?
@ethaniel Операция подписания происходит на клиентской стороне, поэтому нам в PostMessagePatch не нужно передавать сертификат, нужно передавать подпись.
ethaniel commented Jan 24, 2020
@atytsky у нас есть "-----BEGIN CERTIFICATE-----" и "-----BEGIN ENCRYPTED PRIVATE KEY-----". Я так понимаю, что сам PRIVATE KEY мы передавать не должны.
Как должна выглядеть подпись?
atytsky commented Jan 24, 2020 •
@ethaniel конечно нет, Private Key — это ваш секрет, он как раз нужен для подписания/шифрования различного контента. Про подписи можно почитать например тут, или вот еще хорошая спека
Вообще, уже есть сертифицированные криптопровайдеры, которые умеют делать хорошие подписи, самый популярный — Крипто ПРо
ethaniel commented Jan 24, 2020 •
@ethaniel конечно нет, Private Key — это ваш секрет, он как раз нужен для подписания/шифрования различного контента. Про подписи можно почитать например тут, или вот еще хорошая спека
Да вот только непонятно, что именно мы подписываем. Нужно заново скачать обратно XML по 820 приказу через GetMessage и заново его подписать как в SignedContent?
Мы делаем все на PHP и JSON к примеру, поэтому нехватка документации и примеров смущает.
atytsky commented Jan 24, 2020
Весь workflow описан здесь. Вам надо подписать XML-файл 1-го, либо 2-го титула (в зависимости от того, получаете или отправляете документ).
Если вы отправляете — подписывается 1-й титул УПД по №820, если получаете 2-й титул.
elvirazakharova commented Jan 24, 2020
Частично разобралась. Надеюсь это поможет многим людям за неимением какого-либо мануала в доступе. Для подписания титула (PostMessagePatch) покупателем понадобятся:
- Непосредственно сам титул титул (использовать GenerateUniversalTransferDocumentXmlForBuyer)
- Файл открепленной подписи к этому файлу.
То что подпись идет к файлу нигде не написано, ровно как и то, что она открепленная. Но это не точно.
Создать такую подпись возможно например, программой КриптоАРМ или ViPNet CryptoFile. Если ваша среда разработки не особо позволяет пользоваться какими-либо библиотеками, то в коде этот файл открепленной подписи можно сгенерировать с помощью объекта (он же COM-компонент, он же ActiveXObject, он же AutoObject) с названием "CAdESCOM". Предварительно нужно будет: - Установить криптопровайдер(=ПО, что понимает, что такое подпись и как на нее посмотреть) И именно тот, на котором вам подпись дали, скорее всего это КриптоПРо.
- Установить плагин КриптоПро ЭЦП Browser plug-in
- Установить сертификат на компьютер в хранилище “Личные”.
У объекта "CAdESCOM" есть весьма гуглимый набор методов, который позволит получить подпись к файлу (который предварительно нужно кодировать в Base64 и подавать на вход строкой). А подойдет она или нет я узнаю только в понедельник. Открепленная подпись, созданная КриптоАРМом подошла.
На выходе подпись уже в Base64, не нужно ее еще раз кодировать, это текстовый файл, можно блокнотом открыть, посмотреть что к чему там.
atytsky commented Jan 27, 2020
@elvirazakharova в целом описано все верно, но есть следующие моменты:
- Мануала по подписнию нет, так как наше API ничего не подписывает, и все криптографические операции происходит на стороне клиента/интеграции/etc. Мы не можем написать инструкцию, так как она отличается в зависимости от того, какой стек/ОС использует интегратор
- То, что подписывается сам файл, написано в приказах ФНС. Подпись мы умеем принимать как прикрепленную, так и открепленную
- GenerateUniversalTransferDocumentXmlForBuyer - используется для приказа №155, который уже не действует. Нужно использовать новый формат приказа №820
ethaniel commented Jan 28, 2020 •
Если кому-то нужно пример на php (linux), то вот наша функция для подписания документов:
Читайте также: