Смешанный (клиент/сервер) алгоритм формирования цифровой подписи 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, предлагаю краткий перевод описания тэгов из спецификации:



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



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



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

Шаг №2. (сервер)
Каноникализируем SignedInfo, сформированный на шаге №1
Алгоритм следующий (взято отсюда с дополнениями. В спорных местах оставил оригинальный текст):


Код на 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.
Без их советов я бы плюхался с этим в разы дольше.
@
09.08.2013 19:43 UTC
Первоисточник