...

пятница, 9 августа 2013 г.

[Из песочницы] Смешанный (клиент/сервер) алгоритм формирования цифровой подписи xmlDsig на основе CryptoPro Browser Plugin

На хабре уже была обзорная статья о механизмах создания ЭЦП в браузере, где было рассказано о связке Крипто-Про CSP +их же плагин к браузерам. Как там было сказано, предварительные требования для работы — это наличие CryptoPro CSP на компьютере и установка сертификата, которым собираемся подписывать. Вариант вполне рабочий, к тому же в версии 1.05.1418 плагина добавлена работа с подписью XMLDsig. Если есть возможность гонять файлы на клиент и обратно, то для того, чтобы подписать документ на клиенте, достаточно почитать КриптоПрошную справку. Все делается на JavaScript вызовом пары методов.

Однако, что если файлы лежат на сервере и хочется минимизировать трафик и подписывать их, не гоняя на клиент целиком?

Интересно?

Итак, клиент/серверный алгоритм формирования цифровой подписи XMLDSig.

Информацию об спецификации по XMLDsig можно найти по адресу тут.

Я буду рассматривать формирование enveloping signature (обворачивающей подписи) для xml-документа.

Простой пример подписанного xml:

<MyTestXml>
<MySomeData>....</MySomeData>
<Signature xmlns="http://www.w3.org/2000/09/xmldsig#">
<SignedInfo>
<CanonicalizationMethod Algorithm="http://www.w3.org/TR/2001/REC-xml-c14n-20010315" />
<SignatureMethod Algorithm="urn:ietf:params:xml:ns:cpxmlsec:algorithms:gostr34102001-gostr3411" />
<Reference URI="">
<Transforms>
<Transform Algorithm="http://www.w3.org/TR/1999/REC-xpath-19991116">
<XPath xmlns:dsig="http://www.w3.org/2000/09/xmldsig#">not(ancestor-or-self::dsig:Signature)</XPath>
</Transform>
</Transforms>
<DigestMethod Algorithm="urn:ietf:params:xml:ns:cpxmlsec:algorithms:gostr3411" />
<DigestValue>...</DigestValue>
</Reference>
</SignedInfo>
<SignatureValue>...</SignatureValue>
<KeyInfo>
<X509Data>
<X509Certificate>...</X509Certificate>
</X509Data>
</KeyInfo>
</Signature>
</MyTestXml>


Чтобы лучше понять, что из себя представляет 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 сертификат из ключа, которым подписаны данные.


Итак, исходные данные:



  • на сервере имеем xml-документ, который надо подписать. Также я использовал на сервере CryptoPro .Net, но можно и без него.

  • на клиенте нам нужны ОС, поддерживающая работу с CryptoPro CSp 3.6 (в моем случае это была Windows 7), браузер, поддерживающий работу с КриптоПро ЭЦП Browser Plugin, и, собственно, ключ, которым собираемся подписывать (в моем случае ключ был на флешке).


Подготовка клиента:



  • устанавливаем CryptoPro CSP 3.6

  • устанавливаем КриптоПро ЭЦП Browser Plugin

  • устанавливаем сертификат с флешки в локальное хранилище (см. инструкцию

    пункт «2.5.2.2. Установка личного сертификата, хранящегося в контейнере закрытого ключа»)


Шаг №1. (сервер)

Подготавливаем шаблон подписи для документа, который собираемся подписать.

На этом этапе мы должны получить заготовку тэга Signature c посчитанными хэшами (DigestValue) от защищаемых данных. Алгоритм ручного высчитывания этих хэшей подробно описан здесь, но так как у нас в конторе куплен КриптоПро .Net и на его основе написана внутренняя библиотека по работе с подписями, то я просто подписывал с помощью этой библиотеки документ на сервере другим ключом, в результате получал нужный мне шаблон с посчитанными хэшами от данных, но с невалидными SignatureValue и X509Certificate.


Шаг №2. (сервер)

Каноникализируем SignedInfo, сформированный на шаге №1

Алгоритм следующий (взято отсюда с дополнениями. В спорных местах оставил оригинальный текст):



  • первый символ "<", последний ">".

  • все помежуточные пробелым внутри тэгов сохраняются (оригинал All leading space characters inbetween are retained.)

  • элементы вида

    <tag />


    заменяем на

    <tag></tag>


  • окончания строк заменяем на LF (0x0A).

  • выставляем namespace xmlns=«www.w3.org/2000/09/xmldsig#» тэгу SignedInfo (оригинал на английском The namespace xmlns=«www.w3.org/2000/09/xmldsig#» is propagated down from the parent Signature element.)

  • атрибуты тэгов должны быть расположены внутри тэгов в алфавитном порядке (эта проблема проявилась при формировании подписи, содержащей SignatureProperties)


Код на C#, который заработал в моем случае:



XmlNode xmlNode = xmlElement.GetElementsByTagName("SignedInfo")[0];
XmlDocument xmlDocumentSignInfo = new XmlDocument();
xmlDocumentSignInfo.PreserveWhitespace = true;
xmlDocumentSignInfo.LoadXml(xmlNode.OuterXml);
result = Canonicalize(xmlDocumentSignInfo);


где:



public string Canonicalize(XmlDocument document)
{
XmlDsigExcC14NTransform xmlTransform = new XmlDsigExcC14NTransform();
xmlTransform.LoadInput(document);
string result = new StreamReader((MemoryStream)xmlTransform.GetOutput()).ReadToEnd();
//C# метод канокализации не добавляет в XPath неймсппейс
result = s.Replace("<XPath>", "<XPath xmlns:dsig=\"http://www.w3.org/2000/09/xmldsig#\">");


return result ;
}


Шаг №3.

Берем хэш от канокализированного SignedInfo.

Тут возможны 2 варианта-серверный и клиентский.

3.1) Взятие хэша на клиенте. Именно его я использую, так что опишу его первым:

На сервере кодируем канокализированный SignedInfo в base64

C#:



string b64CanonicalizeSignedInfo= Convert.ToBase64String(Encoding.UTF8.GetBytes(s));




и отправляем эти данные на клиент.

На клиенте берем хэш с помощь криптопрошного плагина

JavaScript:

var CADESCOM_HASH_ALGORITHM_CP_GOST_3411 = 100;
var CADESCOM_BASE64_TO_BINARY = 1;

var hashObject = CreateObject("CAdESCOM.HashedData");
hashObject.Algorithm = CADESCOM_HASH_ALGORITHM_CP_GOST_3411;
hashObject.DataEncoding = CADESCOM_BASE64_TO_BINARY;
hashObject.Hash(hexCanonicalSignedInfo);


Посмотреть хэш можно с помощью hashObject.Value

3.2)Считаем хэш на сервере и отправляем на клиент. Этот вариант у меня так и не заработал, но честно сказать я особо и не пытался.


Берем хэш(сервер C#):



HashAlgorithm myhash = HashAlgorithm.Create("GOST3411");
byte[] hashResult = myhash.ComputeHash(сanonicalSignedInfoByteArr);




Возможно хэш надо преобразовывать в base64.

Отправляем на клиент, там используем



var hashObject = CreateObject("CAdESCOM.HashedData");
hashObject.SetHashValue(hashFromServer);




Именно на методе 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 и информацию о сертификате




var certNumber=2; //номер нужного вам сертификата из хранилища
var CAPICOM_CURRENT_USER_STORE = 2;
var CAPICOM_MY_STORE = "my";
var CAPICOM_STORE_OPEN_MAXIMUM_ALLOWED = 2;
var oStore = CreateObject("CAPICOM.Store");
oStore.Open(CAPICOM_CURRENT_USER_STORE, CAPICOM_MY_STORE, CAPICOM_STORE_OPEN_MAXIMUM_ALLOWED);
var certificate=oStore.Certificates.Item(certNumber)


var rawSignature = CreateObject("CAdESCOM.RawSignature");
var signatureHex = rawSignature.SignHash(hashObject, certificate);
//в base64 и переворачиваем
var binReversedSignatureString = utils.reverse(utils.hexToString(signatureHex));

var certValue = certificate.Export(certNumber);


Возвращем на сервер binReversedSignatureString и certValue.


Код функций из utils не выкладываю. Мне его подсказали на форуме криптоПро и его можно посмотреть в этой теме


Шаг №5. (сервер)

Заменяем в сгенерированном на шаге №1 тэге Signature значения тэгов SignatureValue и X509Certificate значениями, полученными с клиента


Шаг №6. (сервер)

Верифицируем карточку.

Если верификация прошла успешно, то все хорошо. В результате мы получаем на сервере документ, подписанный клиентским ключом, не гоняя туда-обратно сам файл.


Примечание: если работа ведется с документом, уже содержащим подписи, то их надо отсоединить от документа до шага №1 и присоединить к документу обратно после шага №6


В заключение хочется сказать большое спасибо за помощь в нахождении алгоритма участникам форума КриптоПро dmishin и Fomich.

Без их советов я бы плюхался с этим в разы дольше.


This entry passed through the Full-Text RSS service — if this is your content and you're reading it on someone else's site, please read the FAQ at fivefilters.org/content-only/faq.php#publishers. Five Filters recommends: 'You Say What You Like, Because They Like What You Say' - http://www.medialens.org/index.php/alerts/alert-archive/alerts-2013/731-you-say-what-you-like-because-they-like-what-you-say.html


Комментариев нет:

Отправить комментарий