Вся соль в том, что надо использовать подпись в формате CAdES-X Long Type 1, и российские ГОСТ Р 34.10-2001, ГОСТ Р 34.10-2012 и т.п. Кроме того подписей может быть более одной, то есть пользователи могут по очереди подписывать файл. При этом предыдущие подписи должны оставаться валидными.
В процессе решения задачу решили усложнить для нас, и уменьшить объем передаваемых данных на клиент. Передавать только hash документа, но не сам документ.
В исходниках буду опускать малозначимые для темы моменты, оставлю только то что касается криптографии. Код на JS приведу только для нормальных браузеров, JS-движки которых поддерживают Promise и function generator. Думаю кому нужно для IE напишут сами (мне пришлось «через не хочу»).
Что нужно:
- Пользователь должен получить пару ключей и сертификат.
- Пользователь должен установить plug-in от Крипто ПРО. Без этого средствами JS мы не сможем работать с криптопровайдером.
Замечания:
- Для тестов у меня был сертификат выданный тестовым ЦС Крипто ПРО и нормальный токен, полученный одним из наших сотрудников (на момент написания статьи ~1500р с годовой лицензией на Крипто ПРО и двумя сертификатами: но «новому» и «старому» ГОСТ)
- Говорят, 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 на этом всё. Проверка будет описана в продолжении статьи. Надеюсь, она будет…
cpdn.cryptopro.ru/content/cades/plugin-activation.html
www.cryptopro.ru/forum2/default.aspx?g=posts&t=11119
www.cryptopro.ru/forum2/default.aspx?g=posts&t=3691&p=21
cpdn.cryptopro.ru/default.asp?url=content/cades/plugin-samples-raw-signature.html
cpdn.cryptopro.ru/default.asp?url=content/cades/plugin.html
itextsupport.com/apidocs/itext5/5.5.9/com/itextpdf/text/pdf/PdfStamper.html#createSignature-com.itextpdf.text.pdf.PdfReader-java.io.OutputStream-char-java.io.File-boolean-
ссылка на оригинал статьи https://habr.com/post/426087/