Подписание PDF на JS и вставка подписи на C#, используя Крипто ПРО

в 12:48, , рубрики: .net, ASP.NET, C#, CAdES-X Long Type 1, javascript, криптография

Итак. Пришла задача. Используя браузер предложить пользователю подписать PDF электронной подписью (далее ЭП). У пользователя должен быть токен, содержащий сертификат, открытый и закрытый ключ. Далее на сервере надо вставить подпись в PDF документ. После этого надо проверить подпись на валидность. В качестве back-end используем ASP.NET и соответственно C#.

Вся соль в том, что надо использовать подпись в формате CAdES-X Long Type 1, и российские ГОСТ Р 34.10-2001, ГОСТ Р 34.10-2012 и т.п. Кроме того подписей может быть более одной, то есть пользователи могут по очереди подписывать файл. При этом предыдущие подписи должны оставаться валидными.

В процессе решения задачу решили усложнить для нас, и уменьшить объем передаваемых данных на клиент. Передавать только hash документа, но не сам документ.

В исходниках буду опускать малозначимые для темы моменты, оставлю только то что касается криптографии. Код на JS приведу только для нормальных браузеров, JS-движки которых поддерживают Promise и function generator. Думаю кому нужно для IE напишут сами (мне пришлось «через не хочу»).

Что нужно:

  1. Пользователь должен получить пару ключей и сертификат.
  2. Пользователь должен установить plug-in от Крипто ПРО. Без этого средствами JS мы не сможем работать с криптопровайдером.

Замечания:

  1. Для тестов у меня был сертификат выданный тестовым ЦС Крипто ПРО и нормальный токен, полученный одним из наших сотрудников (на момент написания статьи ~1500р с годовой лицензией на Крипто ПРО и двумя сертификатами: но «новому» и «старому» ГОСТ)
  2. Говорят, plug-in умеет работать и с ViPNet, но я не проверял.

Теперь будем считать что у нас на сервере есть готовый для подписывания PDF.
Добавляем на страницу скрипт от Крипто ПРО:

<script src="/Scripts/cadesplugin_api.js" type="text/javascript"></script>

Дальше нам надо дождаться пока будет сформирован объект cadesplugin

window.cadespluginLoaded = false;
cadesplugin.then(function () {
    window.cadespluginLoaded = true;
});

Запрашиваем у сервера hash. Предварительно для этого нам ещё надо знать каким сертификатом, а значит и алгоритмом пользователь будет подписывать. Маленькая ремарка: все функции и «переменные» для работы с криптографией на стороне клиента я объединил в объект CryptographyObject.

Метод заполнения поля certificates объекта CryptographyObject:

    fillCertificates: function (failCallback) {

            cadesplugin.async_spawn(function*() {
                try {
                    let oStore = yield cadesplugin.CreateObjectAsync("CAPICOM.Store");

                    oStore.Open(cadesplugin.CAPICOM_CURRENT_USER_STORE,
                        cadesplugin.CAPICOM_MY_STORE,
                        cadesplugin.CAPICOM_STORE_OPEN_MAXIMUM_ALLOWED);

                    let certs = yield oStore.Certificates;
                    certs = yield certs.Find(cadesplugin.CAPICOM_CERTIFICATE_FIND_TIME_VALID);
                    let certsCount = yield certs.Count;
                    for (let i = 1; i <= certsCount; i++) {
                        let cert = yield certs.Item(i);
                        CryptographyObject.certificates.push(cert);
                    }
                    oStore.Close();
                } catch (exc) {
                     failCallback(exc);
                }
            });
    }

Комментарий: пробуем открыть хранилище сертификатов. В этот момент система пользователя выдаст предупреждение, что сайт пытается что-то сделать с сертификатами, криптографией и прочей магической непонятной ерундой. Пользователю тут надо будет нажать кнопку «Да»
Далее получаем сертификаты, валидные по времени (не просроченные) и складываем их в массив certificates. Это надо сделать из-за асинхронной природы cadesplugin (для IE всё иначе ;) ).

Метод получения hash:

getHash: function (certIndex, successCallback, failCallback, какие-то ещё параметры) {
        try {
            cadesplugin.async_spawn(function*() {
                let cert = CryptographyObject.certificates[certIndex];
                let certPublicKey = yield cert.PublicKey();
                let certAlgorithm = yield certPublicKey.Algorithm;
                let certAlgorithmFriendlyName = yield certAlgorithm.FriendlyName;
                let hashAlgorithm;
                //определяем алгоритм подписания по данным из сертификата и получаем алгоритм хеширования
                if (certAlgorithmFriendlyName.match(/2012 512/i))
                    hashAlgorithm = "2012512";
                else if (certAlgorithmFriendlyName.match(/2012 256/i))
                    hashAlgorithm = "2012256";
                else if (certAlgorithmFriendlyName.match(/2001/i))
                    hashAlgorithm = "3411";
                else {
                    failCallback();
                    return;
                }

                $.ajax({
                    url: "/Services/SignService.asmx/GetHash",
                    method: "POST",
                    contentType: "application/json; charset=utf-8 ",
                    dataType: "json",
                    data: JSON.stringify({
                        //какие-то данные для определения документа
                        //не забудем проверить на сервере имеет ли пользователь нужные права
                        hashAlgorithm: hashAlgorithm,
                    }),
                    complete: function (response) {
                        //получаем ответ от сервера, подписываем и отправляем подпись на сервер
                        if (response.status === 200) {
                            CryptographyObject.signHash(response.responseJSON,
                                function(data) {
                                    $.ajax({
                                        url: CryptographyObject.signServiceUrl,
                                        method: "POST",
                                        contentType: "application/json; charset=utf-8",
                                        dataType: "json",
                                        data: JSON.stringify({
                                            Signature: data.Signature,
                                            //какие-то данные для определения файла
                                            //не забудем про серверную валидацию и авторизацию
                                        }),
                                        complete: function(response) {
                                            if (response.status === 200)
                                                successCallback();
                                            else
                                                failCallback();
                                        }
                                    });
                                },
                                certIndex);
                        } else {
                            failCallback();
                        }
                    }
                });
            });
        } catch (exc) {
            failCallback(exc);
        }
    }

Комментарий: обратите внимание на cadesplugin.async_spawn, в нее передаётся функция-генератор, на которой последовательно вызывается next(), что приводит к переходу к yield.
Таким образом получается некий аналог async-await из C#. Всё выглядит синхронно, но работает асинхронно.

Теперь что происходит на сервере, когда у него запросили hash.

Во-первых необходимо установить nuget-пакет iTextSharp (на момент написания стать актуальная версия 5.5.13)

Во-вторых нужен CryptoPro.Sharpei, он идёт в нагрузку к Крипто ПРО .NET SDK

Теперь можно получать hash

                //определим hash-алгоритм
                HashAlgorithm hashAlgorithm;

                switch (hashAlgorithmName)
                {
                    case "3411":
                        hashAlgorithm = new Gost3411CryptoServiceProvider();
                        break;
                    case "2012256":
                        hashAlgorithm = new Gost3411_2012_256CryptoServiceProvider();
                        break;
                    case "2012512":
                        hashAlgorithm = new Gost3411_2012_512CryptoServiceProvider();
                        break;
                    default:
                        GetLogger().AddError("Неизвестный алгоритм хеширования", $"hashAlgorithmName: {hashAlgorithmName}");
                        return HttpStatusCode.BadRequest;
                }
                //получим hash в строковом представлении, понятном cadesplugin
                string hash;
                using (hashAlgorithm)
                //downloadResponse.RawBytes - просто массив байт исходного PDF файла
                using (PdfReader reader = new PdfReader(downloadResponse.RawBytes))
                {
                    //ищем уже существующие подписи
                    int existingSignaturesNumber = reader.AcroFields.GetSignatureNames().Count;
                    using (MemoryStream stream = new MemoryStream())
                    {
                        //добавляем пустой контейнер для новой подписи
                        using (PdfStamper st = PdfStamper.CreateSignature(reader, stream, '', null, true))
                        {
                            PdfSignatureAppearance appearance = st.SignatureAppearance;
                            //координаты надо менять в зависимости от существующего количества подписей, чтоб они не наложились друг на друга
                            appearance.SetVisibleSignature(new Rectangle(36, 100, 164, 150), reader.NumberOfPages,
                                //задаём имя поля, оно потом понадобиться для вставки подписи
                                $"{SignatureFieldNamePrefix}{existingSignaturesNumber + 1}");
                            //сообщаем, что подпись придёт извне
                            ExternalBlankSignatureContainer external =
                                new ExternalBlankSignatureContainer(PdfName.ADOBE_PPKLITE, PdfName.ADBE_PKCS7_DETACHED);
                            //третий параметр - сколько места в байтах мы выделяем под подпись
                            //я выделяю много, т.к. CAdES-X Long Type 1 содержит все сертификаты по цепочке до самого корневого центра
                            MakeSignature.SignExternalContainer(appearance, external, 65536);
                            //получаем поток, который содержит последовательность, которую мы хотим подписывать
                            using (Stream contentStream = appearance.GetRangeStream())
                            {
                                //вычисляем hash и переводим его в строку, понятную cadesplugin
                                hash = string.Join(string.Empty,
                                    hashAlgorithm.ComputeHash(contentStream).Select(x => x.ToString("X2")));
                            }
                        }
                        //сохраняем stream куда хотим, он нам пригодиться, что бы вставить туда подпись
                    }
                }

На клиенте, получив hash от сервера подписываем его

    //certIndex - индекс в массиве сертификатов. На основании именно этого сертификата мы получали алгоритм и формировали hash на сервере
    signHash: function (data, callback, certIndex, failCallback) {
        try {
            cadesplugin.async_spawn(function*() {
                certIndex = certIndex | 0;

                let oSigner = yield cadesplugin.CreateObjectAsync("CAdESCOM.CPSigner");

                let cert = CryptographyObject.certificates[certIndex];

                oSigner.propset_Certificate(cert);
                oSigner.propset_Options(cadesplugin.CAPICOM_CERTIFICATE_INCLUDE_WHOLE_CHAIN);
                //тут надо указать нормальный адрес TSP сервера. Это тестовый от Крипто ПРО
                oSigner.propset_TSAAddress("https://www.cryptopro.ru/tsp/");

                let hashObject = yield cadesplugin.CreateObjectAsync("CAdESCOM.HashedData");

                let certPublicKey = yield cert.PublicKey();
                let certAlgorithm = yield certPublicKey.Algorithm;
                let certAlgorithmFriendlyName = yield certAlgorithm.FriendlyName;

                if (certAlgorithmFriendlyName.match(/2012 512/i)) {
                    yield hashObject.propset_Algorithm(cadesplugin.CADESCOM_HASH_ALGORITHM_CP_GOST_3411_2012_512);
                } else if (certAlgorithmFriendlyName.match(/2012 256/i)) {
                    yield hashObject.propset_Algorithm(cadesplugin.CADESCOM_HASH_ALGORITHM_CP_GOST_3411_2012_256);
                } else if (certAlgorithmFriendlyName.match(/2001/i)) {
                    yield hashObject.propset_Algorithm(cadesplugin.CADESCOM_HASH_ALGORITHM_CP_GOST_3411);
                } else {
                    alert("Невозможно подписать документ этим сертификатом");
                    return;
                }
                //в объект описания hash вставляем уже готовый hash с сервера
                yield hashObject.SetHashValue(data.Hash);

                let oSignedData = yield cadesplugin.CreateObjectAsync("CAdESCOM.CadesSignedData");
                oSignedData.propset_ContentEncoding(cadesplugin.CADESCOM_BASE64_TO_BINARY);
                //результат подписания в base64
                let signatureHex =
                    yield oSignedData.SignHash(hashObject, oSigner, cadesplugin.CADESCOM_CADES_X_LONG_TYPE_1);

                data.Signature = signatureHex;
                callback(data);
            });
        } catch (exc) {
            failCallback(exc);
        }
    }

Комментарий: полученную подпись отправляем на сервер (см. выше)

Ну и наконец вставляем подпись в документ на стороне сервера


//всякие нужные проверки
                //downloadResponse.RawBytes - ранее созданный PDF с пустым контейнером для подписи
                using (PdfReader reader = new PdfReader(downloadResponse.RawBytes))
                {
                    using (MemoryStream stream = new MemoryStream())
                    {
                        //requestData.Signature - собственно подпись от клиента
                        IExternalSignatureContainer external = new SimpleExternalSignatureContainer(Convert.FromBase64String(requestData.Signature));
                    //lastSignatureName - имя контейнера, которое мы определили при формировании hash
                        MakeSignature.SignDeferred(reader, lastSignatureName, stream, external);
                        
                    //сохраняем подписанный файл
                    }
                }

Комментарий: SimpleExternalSignatureContainer — это простейший класс, реализующий интерфейс IExternalSignatureContainer

        /// <summary>
        /// Простая реализация контейнера внешней подписи
        /// </summary>
        private class SimpleExternalSignatureContainer : IExternalSignatureContainer
        {
            private readonly byte[] _signedBytes;

            public SimpleExternalSignatureContainer(byte[] signedBytes)
            {
                _signedBytes = signedBytes;
            }

            public byte[] Sign(Stream data)
            {
                return _signedBytes;
            }

            public void ModifySigningDictionary(PdfDictionary signDic)
            {

            }
        }

Собственно с подписанием PDF на этом всё. Проверка будет описана в продолжении статьи. Надеюсь, она будет…

Автор: Sonkkorh

Источник


  1. Сергей:

    Есть ли у кого то исходники, чтобы хотя бы как то понять взаимодействие частей программы?

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js