Я построю свой собственный сервис для подписания документов - FOX ©
Я построю свой собственный сервис для подписания документов - FOX ©

Важное вступление

В гайде описывается формирование отсоединенной подписи в формате 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

Что имеем на входе?

  1. КриптоПро CSP версии 5.0 - для поддержки Российских криптографических алгоритмов (подписи, которые выпустили в аккредитованном УЦ в РФ)

  2. КриптоПро TSP Client 2.0 - нужен для штампа времени

  3. КриптоПро OCSP Client 2.0 - проверит не отозван ли сертификат на момент подписания

  4. КриптоПро .NET Client - таков путь

  5. Любой сервис по проверке ЭП - я использовал Контур.Крипто как основной сервис для проверки ЭП и КриптоАРМ как локальный. А еще можно проверить ЭП на сайте Госуслуг

  6. КЭП по ГОСТ Р 34.11-2012/34.10-2012 256 bit, которую выпустил любой удостоверяющий центр

Лицензирование ПО и версии
  1. КриптоПро CSP версии 5.0 - у меня установлена версия 5.0.11944 КС1, лицензия встроена в ЭП.

  2. КриптоПро TSP Client 2.0 и КриптоПро OCSP Client 2.0 - лицензии покупается отдельно, а для гайда мне хватило демонстрационного срока.

  3. КриптоПро .NET Client версии 1.0.7132.2 - в рамках этого гайда я использовал демонстрационную версию клиентской части и все действия выполнялись локально. Лицензию на сервер нужно покупать отдельно.

  4. Контур.Крипто бесплатен, но требует регистрации. В нем также можно подписать документы КЭП, УКЭП и проверить созданную подпись загрузив ее файлы.

Так, а что надо на выходе?

А на выходе надо получить готовое решение, которое сделает отсоединенную ЭП в формате .sig со штампом времени на подпись и доказательством подлинности. Для этого зададим следующие критерии:

  1. ЭП проходит проверку на портале Госуслуг, через сервис для подтверждения подлинности ЭП формата PKCS#7 в электронных документах;

  2. КриптоАРМ после проверки подписи

    1. Заполнит поле "Время создания ЭП" - в конце проверки появится окно, где можно выбрать ЭП и кратко посмотреть ее свойства

      Стобец "Время создация ЭП"
      Стобец "Время создация ЭП"
    2. В информации о подписи и сертификате (двойной клик по записе в таблице) на вкладке "Штампы времени" в выпадающем списке есть оба значения и по ним заполнена информация:

      1. Подпись:

      2. Доказательства подлинности:

    3. В протоколе проверки подписи есть блоки "Доказательства подлинности", "Штамп времени на подпись" и "Время подписания". Для сравнения: если документ подписан просто КЭП, то отчет по проверке будет достаточно коротким в сравнении с УКЭП.

  3. Контур.Крипто при проверке подписи выдаст сообщение, что совершенствованная подпись подтверждена, сертификат на момент подписания действовал и указано время создания подпись:

    Усовершенствованная подпись подтверждена
    Усовершенствованная подпись подтверждена

Соберем проект с поддержкой ГОСТ Р 34.11-2012 256 bit

Гайд разделен на несколько этапов. Основная инструкция по сборке опубликована вместе с репозиторием DotnetCoreSampleProject - периодически я буду на нее ссылаться.

Первым делом создадим новую папку

... и положим туда все необходимое.

Инструкция делится на 2 этапа - мне пришлось выполнить оба, чтобы решение заработало. В папку добавьте подпапки .\runtime и .\packages

I - Сборка проекта без сборки corefx для Windows

  1. Установите КриптоПро 5.0 и убедитесь, что у вас есть действующая лицензия. - для меня подошла втроенная в ЭП;

  2. Установите core 3.1 sdk и runtime и распространяемый пакет Visual C++ для Visual Studio 2015 обычно ставится вместе со студией; прим.: на II этапе мне пришлось через установщик студии поставить дополнительное ПО для разработки на C++ - сборщик требует предустановленный DIA SDK.

  3. Задайте переменной среды DOTNET_MULTILEVEL_LOOKUP значение 0 - не могу сказать для чего это нужно, но в оригинальной инструкции это есть;

  4. Скачайте 2 файла из релиза corefx (package_windows_debug.zip и runtime-debug-windows.zip) - они нужны для корректной сборки проекта. В гайде рассматривается версия v3.1.1-cprocsp-preview4.325 от 04.02.2021:

    1. package_windows_debug.zip распакуйте в .\packages

    2. runtime-debug-windows.zip распакуйте в .\runtime

  5. Добавьте источник пакетов NuGet в файле %appdata%\NuGet\NuGet.Config - источник должен ссылаться на путь .\packages в созданной вами папке. Пример по добавлению источника есть в основной инструкеии. Для меня это не сработало, поэтому я добавил источник через VS Community;

  6. Склонируйте 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\"
  7. Склонируйте репизиторий DotnetCoreSampleProject в .\

  8. Измените файл .\DotnetSampleProject\DotnetSampleProject.csproj - для сборок System.Security.Cryptography.Pkcs.dll и System.Security.Cryptography.Xml.dll укажите полные пути к .\runtime;

  9. Перейдите в папку проекта и попробуйте собрать решение. Я собирал через Visual Studio после открытия проекта.

II - Сборка проекта со сборкой corefx для Windows

  1. Выполните 1-3 и 6-й шаги из I этапа;

  2. Склонируйте репозиторий corefx в .\

  3. Выполните сборку запустив .\corefx\build.cmd - на этом этапе потребуется предустановленный DIA SDK

  4. Выполните шаги 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 этапа:

  1. Поиск сертификата в хранилище - я использовал поиск по отпечатку в хранилище пользователя;

  2. Чтение байтов подписанного файла;

  3. Создание УКЭП;

  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-документ, который содержит надпись "Тестовое заявление.":

Больше для теста нам ничего не надо
Больше для теста нам ничего не надо

Далее запустим программу и дождемся подписания файла:

Готово. Теперь можно приступать к проверкам.

Проверка в КриптоАРМ

Время создания ЭП заполнено:

Штамп времени на подпись есть:

Доказательства подлинности также заполнены:

В протоколе проверки есть блоки "Доказательства подлинности", "Штамп времени на подпись" и "Время подписания":

Важно отметить, что серийный номер параметров сертификата принадлежит TSP-сервису http://qs.cryptopro.ru/tsp/tsp.srf

Проверка на Госуслугах

Проверка в Контур.Крипто

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-сервиса. По гайду это ООО "КРИПТО-ПРО":

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 - Исключение без пропатченных библиотек
<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>
...

Но при использовании пропатченных библиотек это исключение не выпадает и с приватным ключем можно взаимодействовать:

<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":

<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>
...