![Я построю свой собственный сервис для подписания документов - FOX © Я построю свой собственный сервис для подписания документов - FOX ©](https://habrastorage.org/getpro/habr/upload_files/ecf/3e9/1bb/ecf3e91bbd8e340628d78e0315436475.png)
Важное вступление
В гайде описывается формирование отсоединенной подписи в формате PKCS7 (рядом с файлом появится файл в формате .sig). Такую подпись может запросить нотариус, ЦБ и любой кому нужно долгосрочное хранения подписанного документа. Удобство такой подписи в том, что при улучшении ее до УКЭП CAdES-X Long Type 1 (CMS Advanced Electronic Signatures [1]) в нее добавляется штамп времени, который генерирует TSA (Time-Stamp Protocol [2]) и статус сертификата на момент подписания (OCSP [3]) - подлинность такой подписи можно подтвердить по прошествии длительного периода (Усовершенствованная квалифицированная подпись [4]).
Код основан на репозиториях corefx и DotnetCoreSampleProject - в последнем проще протестировать свои изменения перед переносом в основной проект и он будет отправной точкой по сборке corefx. Судя по записям с форума компании [5], решение для .NET Core в стадии бета-тестирования. Далее по тексту я также буду ссылаться на этот форум. Разработка велась в Visual Studio Community 2019.
Для получения штампа времени использован TSP-сервис http://qs.cryptopro.ru/tsp/tsp.srf
Что имеем на входе?
КриптоПро CSP версии 5.0 - для поддержки Российских криптографических алгоритмов (подписи, которые выпустили в аккредитованном УЦ в РФ)
КриптоПро TSP Client 2.0 - нужен для штампа времени
КриптоПро OCSP Client 2.0 - проверит не отозван ли сертификат на момент подписания
КриптоПро .NET Client - таков путь
Любой сервис по проверке ЭП - я использовал Контур.Крипто как основной сервис для проверки ЭП и КриптоАРМ как локальный. А еще можно проверить ЭП на сайте Госуслуг
КЭП по ГОСТ Р 34.11-2012/34.10-2012 256 bit, которую выпустил любой удостоверяющий центр
Лицензирование ПО и версии
КриптоПро CSP версии 5.0 - у меня установлена версия 5.0.11944 КС1, лицензия встроена в ЭП.
КриптоПро TSP Client 2.0 и КриптоПро OCSP Client 2.0 - лицензии покупается отдельно, а для гайда мне хватило демонстрационного срока.
КриптоПро .NET Client версии 1.0.7132.2 - в рамках этого гайда я использовал демонстрационную версию клиентской части и все действия выполнялись локально. Лицензию на сервер нужно покупать отдельно.
Контур.Крипто бесплатен, но требует регистрации. В нем также можно подписать документы КЭП, УКЭП и проверить созданную подпись загрузив ее файлы.
Так, а что надо на выходе?
А на выходе надо получить готовое решение, которое сделает отсоединенную ЭП в формате .sig со штампом времени на подпись и доказательством подлинности. Для этого зададим следующие критерии:
ЭП проходит проверку на портале Госуслуг, через сервис для подтверждения подлинности ЭП формата PKCS#7 в электронных документах;
КриптоАРМ после проверки подписи
Заполнит поле "Время создания ЭП" - в конце проверки появится окно, где можно выбрать ЭП и кратко посмотреть ее свойства
Стобец "Время создация ЭП" В информации о подписи и сертификате (двойной клик по записе в таблице) на вкладке "Штампы времени" в выпадающем списке есть оба значения и по ним заполнена информация:
Подпись:
Доказательства подлинности:
В протоколе проверки подписи есть блоки "Доказательства подлинности", "Штамп времени на подпись" и "Время подписания". Для сравнения: если документ подписан просто КЭП, то отчет по проверке будет достаточно коротким в сравнении с УКЭП.
Контур.Крипто при проверке подписи выдаст сообщение, что совершенствованная подпись подтверждена, сертификат на момент подписания действовал и указано время создания подпись:
Усовершенствованная подпись подтверждена
Соберем проект с поддержкой ГОСТ Р 34.11-2012 256 bit
Гайд разделен на несколько этапов. Основная инструкция по сборке опубликована вместе с репозиторием DotnetCoreSampleProject - периодически я буду на нее ссылаться.
Первым делом создадим новую папку
... и положим туда все необходимое.
Инструкция делится на 2 этапа - мне пришлось выполнить оба, чтобы решение заработало. В папку добавьте подпапки .\runtime и .\packages
I - Сборка проекта без сборки corefx для Windows
Установите КриптоПро 5.0 и убедитесь, что у вас есть действующая лицензия. - для меня подошла втроенная в ЭП;
Установите core 3.1 sdk и runtime и распространяемый пакет Visual C++ для Visual Studio 2015 обычно ставится вместе со студией; прим.: на II этапе мне пришлось через установщик студии поставить дополнительное ПО для разработки на C++ - сборщик требует предустановленный DIA SDK.
Задайте переменной среды DOTNET_MULTILEVEL_LOOKUP значение 0 - не могу сказать для чего это нужно, но в оригинальной инструкции это есть;
Скачайте 2 файла из релиза corefx (package_windows_debug.zip и runtime-debug-windows.zip) - они нужны для корректной сборки проекта. В гайде рассматривается версия v3.1.1-cprocsp-preview4.325 от 04.02.2021:
package_windows_debug.zip распакуйте в .\packages
runtime-debug-windows.zip распакуйте в .\runtime
Добавьте источник пакетов NuGet в файле %appdata%\NuGet\NuGet.Config - источник должен ссылаться на путь .\packages в созданной вами папке. Пример по добавлению источника есть в основной инструкеии. Для меня это не сработало, поэтому я добавил источник через VS Community;
Склонируйте NetStandard.Library в .\ и выполните PowerShell скрипт (взят из основной инструкции), чтобы заменить пакеты в $env:userprofile\.nuget\packages\
git clone https://github.com/CryptoProLLC/NetStandard.Library New-Item -ItemType Directory -Force -Path "$env:userprofile\.nuget\packages\netstandard.library" Copy-Item -Force -Recurse ".\NetStandard.Library\nugetReady\netstandard.library" -Destination "$env:userprofile\.nuget\packages\"
Склонируйте репизиторий DotnetCoreSampleProject в .\
Измените файл .\DotnetSampleProject\DotnetSampleProject.csproj - для сборок System.Security.Cryptography.Pkcs.dll и System.Security.Cryptography.Xml.dll укажите полные пути к .\runtime;
Перейдите в папку проекта и попробуйте собрать решение. Я собирал через Visual Studio после открытия проекта.
II - Сборка проекта со сборкой corefx для Windows
Выполните 1-3 и 6-й шаги из I этапа;
Склонируйте репозиторий corefx в .\
Выполните сборку запустив .\corefx\build.cmd - на этом этапе потребуется предустановленный DIA SDK
Выполните шаги 5, 7-9 из I этапа. Вместо условного пути .\packages укажите .\corefx\artifacts\packages\Debug\NonShipping, а вместо .\runtime укажите .\corefx\artifacts\bin\runtime\netcoreapp-Windows_NT-Debug-x64
На этом месте у вас должно получиться решение, которое поддерживает ГОСТ Р 34.11-2012 256 bit.
Немного покодим
Потребуется 2 COM библиотеки: "CAPICOM v2.1 Type Library" и "Crypto-Pro CAdES 1.0 Type Library". Они содержат необходимые объекты для создания УКЭП.
В этом примере будет подписываться BASE64 строка, содержащая в себе PDF-файл. Немного доработав код можно будет подписать hash-значение этого фала.
Основной код для подписания был взят со страниц Подпись PDF с помощью УЭЦП- Page 2 (cryptopro.ru) и Подпись НЕОПРЕДЕЛЕНА при создании УЭЦП для PDF на c# (cryptopro.ru), но он использовался для штампа подписи на PDF документ. Код из этого гайда переделан под сохранение файла подписи в отдельный файл.
Условно процесс можно поделить на 4 этапа:
Поиск сертификата в хранилище - я использовал поиск по отпечатку в хранилище пользователя;
Чтение байтов подписанного файла;
Создание УКЭП;
Сохранение файла подписи рядом с файлом.
using CAdESCOM;
using CAPICOM;
using System;
using System.Globalization;
using System.IO;
using System.Security.Cryptography;
using System.Security.Cryptography.X509Certificates;
using System.Security.Cryptography.Xml;
using System.Text;
using System.Threading.Tasks;
using System.Xml;
public static void Main()
{
//Сертификат для подписи
X509Certificate2 gostCert = GetX509Certificate2("отпечаток");
//Файл, который предстоит подписать
byte[] fileBytes = File.ReadAllBytes("C:\\Тестовое заявление.pdf");
//Файл открепленной подписи
byte[] signatureBytes = SignWithAdvancedEDS(fileBytes, gostCert);
//Сохранение файла подписи
File.WriteAllBytes("C:\\Users\\mikel\\Desktop\\Тестовое заявление.pdf.sig", signatureBytes);
}
//Поиск сертификата в хранилище
public static X509Certificate2 GetX509Certificate2(string thumbprint)
{
X509Store store = CreateStoreObject("My", StoreLocation.CurrentUser);
store.Open(OpenFlags.ReadOnly);
X509Certificate2Collection certCollection =
store.Certificates.Find(X509FindType.FindByThumbprint, thumbprint, false);
X509Certificate2Enumerator enumerator = certCollection.GetEnumerator();
X509Certificate2 gostCert = null;
while (enumerator.MoveNext())
gostCert = enumerator.Current;
if (gostCert == null)
throw new Exception("Certificiate was not found!");
return gostCert;
}
//Создание УКЭП
public static byte[] SignWithAdvancedEDS(byte[] fileBytes, X509Certificate2 certificate)
{
string signature = "";
try
{
string tspServerAddress = @"http://qs.cryptopro.ru/tsp/tsp.srf";
CPSigner cps = new CPSigner();
cps.Certificate = GetCAPICOMCertificate(certificate.Thumbprint);
cps.Options = CAPICOM_CERTIFICATE_INCLUDE_OPTION.CAPICOM_CERTIFICATE_INCLUDE_WHOLE_CHAIN;
cps.TSAAddress = tspServerAddress;
CadesSignedData csd = new CadesSignedData();
csd.ContentEncoding = CADESCOM_CONTENT_ENCODING_TYPE.CADESCOM_BASE64_TO_BINARY;
csd.Content = Convert.ToBase64String(fileBytes);
//Создание и проверка подписи CAdES BES
signature = csd.SignCades(cps, CADESCOM_CADES_TYPE.CADESCOM_CADES_BES, true, CAdESCOM.CAPICOM_ENCODING_TYPE.CAPICOM_ENCODE_BASE64);
csd.VerifyCades(signature, CADESCOM_CADES_TYPE.CADESCOM_CADES_BES, true);
//Дополнение и проверка подписи CAdES BES до подписи CAdES X Long Type 1
//(вторая подпись остается без изменения, так как она уже CAdES X Long Type 1)
signature = csd.EnhanceCades(CADESCOM_CADES_TYPE.CADESCOM_CADES_X_LONG_TYPE_1, tspServerAddress, CAdESCOM.CAPICOM_ENCODING_TYPE.CAPICOM_ENCODE_BASE64);
csd.VerifyCades(signature, CADESCOM_CADES_TYPE.CADESCOM_CADES_X_LONG_TYPE_1, true);
}
catch (Exception ex)
{
throw ex;
}
return Convert.FromBase64String(signature);
}
Пробный запуск
Для подписания возьмем PDF-документ, который содержит надпись "Тестовое заявление.":
![Больше для теста нам ничего не надо Больше для теста нам ничего не надо](https://habrastorage.org/getpro/habr/upload_files/2a0/107/071/2a0107071a61f7b03151751129d36fd6.png)
Далее запустим программу и дождемся подписания файла:
![](https://habrastorage.org/getpro/habr/upload_files/368/697/527/3686975279486f377fe6b97249183309.png)
Готово. Теперь можно приступать к проверкам.
Проверка в КриптоАРМ
Время создания ЭП заполнено:
![](https://habrastorage.org/getpro/habr/upload_files/489/c47/596/489c47596b33c46b7c0b9c163101d1af.png)
Штамп времени на подпись есть:
![](https://habrastorage.org/getpro/habr/upload_files/a4c/b7d/754/a4cb7d754366042fbfe39f1336018c53.png)
Доказательства подлинности также заполнены:
![](https://habrastorage.org/getpro/habr/upload_files/b3a/612/757/b3a612757b8eb4ea6b5575f0b9eb6161.png)
В протоколе проверки есть блоки "Доказательства подлинности", "Штамп времени на подпись" и "Время подписания":
![](https://habrastorage.org/getpro/habr/upload_files/da4/720/da2/da4720da2bedd1d932a8b319fe5c9272.png)
![](https://habrastorage.org/getpro/habr/upload_files/c72/b3d/d84/c72b3dd84368907e1d0de807a82b77f5.png)
![](https://habrastorage.org/getpro/habr/upload_files/6c9/172/dab/6c9172dab2628b6394c15d27019c852b.png)
Важно отметить, что серийный номер параметров сертификата принадлежит TSP-сервису http://qs.cryptopro.ru/tsp/tsp.srf
Проверка на Госуслугах
![](https://habrastorage.org/getpro/habr/upload_files/d06/a02/6e9/d06a026e944448a78678fbf03e3c73be.png)
Проверка в Контур.Крипто
![](https://habrastorage.org/getpro/habr/upload_files/965/4af/791/9654af791664a5b3d9a84526a6f26225.png)
Done.
Гайд написан с исследовательской целью - проверить возможность подписания документов УКЭП с помощью самописного сервиса на .NET Core 3.1 с формированием штампов подлинности и времени подписания документов.
Безусловно это решение не стоит брать в работу "как есть" и нужны некоторые доработки, но в целом оно работает и подписывает документы подписью УКЭП.
Это вообще законно?
С удовольствием узнаю ваше мнение в комментариях.
Ссылки на публичные источники
[1] CMS Advanced Electronic Signatures (CAdES) - https://tools.ietf.org/html/rfc5126#ref-ISO7498-2
[2] Internet X.509 Public Key Infrastructure Time-Stamp Protocol (TSP) - https://www.ietf.org/rfc/rfc3161.txt
[3] X.509 Internet Public Key Infrastructure Online Certificate Status Protocol - OCSP - https://tools.ietf.org/html/rfc2560
[4] Усовершенствованная квалифицированная подпись — Удостоверяющий центр СКБ Контур (kontur.ru)
[5] Поддержка .NET Core (cryptopro.ru)
[6] http://qs.cryptopro.ru/tsp/tsp.srf - TSP-сервис КриптоПро
UPD1: Поменял в коде переменную, куда записываются байты файла подписи.
Также я забыл написать немного про подпись штампа времени - он подписывается сертификатом владельца TSP-сервиса. По гайду это ООО "КРИПТО-ПРО":
![](https://habrastorage.org/getpro/habr/upload_files/d0c/792/f68/d0c792f680cb199aef55be52fca2894e.png)
UPD2: Про библиотеки CAdESCOM и CAPICOM
В ответ @kotov_a и @mayorovp- Все верно: для .NET Core 3.1 сборки System.Security.Cryptography.Pkcs.dll и System.Security.Cryptography.Xml.dll подменяются, так как поддержка этих алгоритмов в бета тестировании [5], а для .NET Framework 4.8 используется CryptoPro.Sharpei - он был перенесен в состав КриптоПро .NET. Последний, судя по информации с портала документации, работает только с .NET Framework.
Если создать пустой проект на .NET Core 3.1, подключив непропатченные библиотеки, то при обращении к закрытому ключу выпадет исключение "System.NotSupportedException" c сообщением "The certificate key algorithm is not supported.":
![.NET Core 3.1 - Исключение без пропатченных библиотек .NET Core 3.1 - Исключение без пропатченных библиотек](https://habrastorage.org/getpro/habr/upload_files/c44/297/140/c44297140cc799faf79f3bbd4ffe687c.png)
<TargetFramework>netcoreapp3.1</TargetFramework>
...
<ItemGroup>
<COMReference Include="CAdESCOM">
<WrapperTool>tlbimp</WrapperTool>
<VersionMinor>0</VersionMinor>
<VersionMajor>1</VersionMajor>
<Guid>e00b169c-ae7f-45d5-9c56-672e2b8942e0</Guid>
<Lcid>0</Lcid>
<Isolated>false</Isolated>
<EmbedInteropTypes>true</EmbedInteropTypes>
</COMReference>
<COMReference Include="CAPICOM">
<WrapperTool>tlbimp</WrapperTool>
<VersionMinor>1</VersionMinor>
<VersionMajor>2</VersionMajor>
<Guid>bd26b198-ee42-4725-9b23-afa912434229</Guid>
<Lcid>0</Lcid>
<Isolated>false</Isolated>
<EmbedInteropTypes>true</EmbedInteropTypes>
</COMReference>
</ItemGroup>
...
Но при использовании пропатченных библиотек это исключение не выпадает и с приватным ключем можно взаимодействовать:
![](https://habrastorage.org/getpro/habr/upload_files/164/76a/836/16476a836a6e25b479466b5e2155c981.png)
<TargetFramework>netcoreapp3.1</TargetFramework>
...
<ItemGroup>
<Reference Include="System.Security.Cryptography.Pkcs">
<HintPath>.\corefx\artifacts\bin\runtime\netcoreapp-Windows_NT-Debug-x64\System.Security.Cryptography.Pkcs.dll</HintPath>
</Reference>
<Reference Include="System.Security.Cryptography.Xml">
<HintPath>.\corefx\artifacts\bin\runtime\netcoreapp-Windows_NT-Debug-x64\System.Security.Cryptography.Xml.dll</HintPath>
</Reference>
</ItemGroup>
...
Также код из гайда работает с .NET Framework 4.8 без использования пропатченных библиотек, но вместо обращения к пространству имен "System.Security.Cryptography", которое подменяется пропатченными библиотеками для .NET Core, CSP Gost3410_2012_256CryptoServiceProvider будет использован из пространства имен "CryptoPro.Sharpei":
![](https://habrastorage.org/getpro/habr/upload_files/3a6/70a/032/3a670a032a40d89a0ab5b68e3d95b947.png)
<TargetFrameworkVersion>v4.8</TargetFrameworkVersion>
...
<ItemGroup>
<COMReference Include="CAdESCOM">
<Guid>{E00B169C-AE7F-45D5-9C56-672E2B8942E0}</Guid>
<VersionMajor>1</VersionMajor>
<VersionMinor>0</VersionMinor>
<Lcid>0</Lcid>
<WrapperTool>tlbimp</WrapperTool>
<Isolated>False</Isolated>
<EmbedInteropTypes>True</EmbedInteropTypes>
</COMReference>
<COMReference Include="CAPICOM">
<Guid>{BD26B198-EE42-4725-9B23-AFA912434229}</Guid>
<VersionMajor>2</VersionMajor>
<VersionMinor>1</VersionMinor>
<Lcid>0</Lcid>
<WrapperTool>tlbimp</WrapperTool>
<Isolated>False</Isolated>
<EmbedInteropTypes>True</EmbedInteropTypes>
</COMReference>
<COMReference Include="CERTENROLLLib">
<Guid>{728AB348-217D-11DA-B2A4-000E7BBB2B09}</Guid>
<VersionMajor>1</VersionMajor>
<VersionMinor>0</VersionMinor>
<Lcid>0</Lcid>
<WrapperTool>tlbimp</WrapperTool>
<Isolated>False</Isolated>
<EmbedInteropTypes>True</EmbedInteropTypes>
</COMReference>
</ItemGroup>
...
kotov_a
можете объяснить как это работает? Правильно понимаю что КриптоПро подменяют сборки .NET (System.Security.Cryptography.Pkcs.dll и System.Security.Cryptography.Xml.dll) своими и они бинарно зависят от текущей версии .NET Core, поэтому их нужно компилировать вместе со стандартной библиотекой?
mayorovp
Там в коде есть вот такой архитектурный идиотизм — SignedXml.cs#L443-L452. КриптоПро вынуждены патчить этот код, чтобы добавить туда российские алгоритмы.