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

Итак. Пришла задача. Используя браузер предложить пользователю подписать 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, '\0', 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 на этом всё. Проверка будет описана в продолжении статьи. Надеюсь, она будет…


ссылка на оригинал статьи https://habr.com/post/426087/

Добавить комментарий

Ваш e-mail не будет опубликован. Обязательные поля помечены *