Не в службу, а в дружбу или как сделать то, чего сделать нельзя

Продолжаем цикл статей о хакерских разработках. В прошлой статье Крипто Про создание электронной подписи или как я перешел на темную сторону речь шла о создании Windows-службы для работы с Крипто Про и создании электронной подписи. В этой статье я расскажу о возникших проблемах и о том, как они были решены.

Первые неожиданные проблемы

Итак, служба была написана и даже работала, но тут случилось неожиданное – ее начали тестировать!  И как часто бывает в процессе тестирования, выясняются особенности, о которых разработчик возможно даже не задумывался. Дело в том, что вся разработка и первоначальные проверки выполнялись в системе Windows 8.1, и на ней все прекрасно работало: подписание сертификатом, подписание токеном. И я даже и не предполагал, что могут возникнуть какие-то проблемы в Windows 7. Однако они возникли. Нет, в целом служба работала и даже подписывала ЭП если выбран для подписания сертификат, однако что-то непонятно происходило при попытке подписания токеном. Служба вела себя так, словно токен просто не был вставлен в компьютер. Начались долгие и мучительные поиски причин и попытки их устранения…

Первоначальное предположение было, что у службы почему-то просто нет доступа до внешних носителей, поэтому я сделал попытку просто прочитать текстовый файл с флешки. Естественно все читалось.

Дальнейшие попытки разобраться привели к написанию простого консольного приложения подписывающего ЭП текстовый файлик, с использованием токена. Все прекрасно подписывалось. В итоге я преобразовал службу в консольное приложение, в котором код отвечающий за подписания был абсолютно идентичным тому, что был в службе, отличался только запуск приложения. И снова все подписывалось. Идеи иссякли, почему один и тот же код, запущенный из консоли работает, а запущенный из службы – нет.

Написал письмо в техподдержку КриптоПро, с описанием проблемы, тех поддержка ответила: “Попробуйте, пожалуйста, перевести КриптоПро CSP в режим службы (Панель управления – КриптоПро CSP – Безопасность – Использовать службу хранения ключей – Перезагрузить компьютер).”. Попробовал, и о чудо, оно заработало! Правда я не понял почему, то есть получилось, что если КрипроПро запущено в режиме службы, то и другая служба с ним нормально работает, а если в режиме приложения то почему-то нет. Такое решение в принципе нас устраивало, однако не очень хотелось включать еще дополнительные настройки при установки КриптоПро и хотелось разобраться, почему все-таки так работает, а с первоначальными настройками нет.

Имперсонация (олицетворение)

Дальнейшая переписка с сотрудниками КриптоПро привела к их следующему ответу: “Если служба запущена не под SYSTEM, то доступа к смарт-картам, согласно документации MS, она не получит ни при каких условиях. Это, правда, немного противоречит тому, что вы наблюдаете в Windows 8.1. Тут два варианта: либо они сняли ограничение, либо вы наблюдаете частичную работоспособность, то есть undefined behavior.”. Да, действительно странно… Напомню, мы запускали службу под определенным пользователем, а не под системной учетной записью (SYSTEM) потому, что нам был необходим доступ к сертификатам пользователя, грубо говоря, мы вызывали функцию Crypto API, говоря: “дай мне мои сертификаты”, код:

var store = new X509Store(StoreName.My, StoreLocation.CurrentUser)

естественно у системной учетной записи не было никаких сертификатов, мы получали пустой список и ничего не могли подписать. Поэтому дальнейшая наша переписка с КриптоПро свелась, к вопросу, как же все-таки получить сертификаты нужного нам пользователя, когда процесс запущен от SYSTEM?

Конечно, пришлось объяснять, почему нам необходима служба, почему мы не можем вынести код подписания в приложение. Но в итоге, сотрудниками КриптоПро было предложено использовать “имперсонацию” (или “олицетворение”), вот большая статья по этому поводу https://msdn.microsoft.com/ru-ru/library/ms730088%28v=vs.110%29.aspx?f=255&MSPPError=-2147217396, если коротко, то “импероснация” это такой механизм, встроенный в .NET, позволяющий выполнять код от имени пользователя, который авторизован в системе, при этом процесс может быть запущен от другого пользователя в нашем случае от SYSTEM. Этот подход повсеместно встречается в Windows, например, в сервисе IIS.

Согласно статье, для имеперсонации достаточно повесить на нужный метод атрибут:

[OperationBehavior(Impersonation = ImpersonationOption.Required)]

Что я и сделал и благополучно получил ошибку при старте службы:

A Windows identity that represents the caller is not provided by binding (‘BasicHttpBinding’)

Интерфейса взаимодействия BasicHttpBinding (который использовался по умолчанию) явно не хватало для имперсонации, поэтому пришлось переделать его на WSHttpBinding.

После этого служба запустилась, код: WindowsIdentity.GetCurrent().Name возвращал имя текущего авторизованного пользователя. То есть имперсонация заработала, но вот метод, отвечающий за непосредственно подписание теперь возвращал ошибку:

unable to load DLL 0x80070542

По каким-то причинам теперь не загружалась моя библиотека, написанная на C++, при этом, если запускать метод без имперсонаций, библиотека подгружалась, но при подписании валилось, т.к. внутри C++ кода еще раз открывается хранилище сертификатов пользователя. Временно решил эту проблему добавив пустой метод в С++ код, который вызвал в коде перед стартом службы, в итоге нужна dll стала загружаться.

Однако теперь при открытии хранишища сертификатов из C++ кода:

CertOpenStore(CERT_STORE_PROV_SYSTEM, 0, 0, CERT_SYSTEM_STORE_CURRENT_USER, _TEXT("MY"));

Я получал ошибку 1346. Точно такую же ошибку получал и при вызове метода получения сертификатов из C# кода:

var store = new X509Store(StoreName.My, StoreLocation.CurrentUser);
store.Open(OpenFlags.ReadOnly);

Что-то явно было не так с этой имперсонацией. Перечитав еще раз, внимательно статью, приведенную выше, я понял что, в интерфейсе взаимодействия со службой не хватает

блока <behaviors>, добавил его и все заработало! И получение сертификатов для пользователя, и самое главное подписание с использованием токена. При этом WSHttpBinding оказался не обязательным, и я вернул на BasicHttpBinding, итоговый конфиг для WPF:

<system.serviceModel>
    <bindings>
      <basicHttpBinding>
        <binding name="BasicHttpBinding_IDigitalSignatureService">
          <security mode="TransportCredentialOnly">
            <transport clientCredentialType="Windows" />
          </security>
        </binding>
      </basicHttpBinding>
    </bindings>
    <behaviors>
      <endpointBehaviors>
        <behavior name="ImpersonationBehavior">
          <clientCredentials>
            <windows allowedImpersonationLevel="Impersonation" allowNtlm="true"/>
          </clientCredentials>
        </behavior>
      </endpointBehaviors>
    </behaviors>
    <client>
      <endpoint address="http://localhost:8523/DigitalSignatureService"
        behaviorConfiguration="ImpersonationBehavior" binding="basicHttpBinding" 
        bindingConfiguration="BasicHttpBinding_IDigitalSignatureService"
        contract="DigitalSignatureService.IDigitalSignatureService"
        name="BasicHttpBinding_IDigitalSignatureService">
        <identity>
          <dns value="localhost" />
        </identity>
      </endpoint>
    </client>
  </system.serviceModel>

Теперь служба работала нормально и на Windows 7 и на Windows 8.1 с WPF приложением.

Теперь это решение нужно было перенести на Windows Store приложение.

Перенос решения на Windows Store

Радость моя оказалась недолгой. Настройки интерфейса для взаимодействия со службой в WinRT задаются в коде (а не в xml-файле как в WPF), который автоматически генерируется при добавлении Servece Referense, поэтому нужно добавлять свой partioal класс, в котором можно расширить базовые настройки. В нем я написал следующее:

static partial void ConfigureEndpoint(System.ServiceModel.Description.ServiceEndpoint serviceEndpoint,
            System.ServiceModel.Description.ClientCredentials clientCredentials)
{
    clientCredentials.Windows.ClientCredential = new NetworkCredential("VShakhlin", "******", "CROC");
    clientCredentials.Windows.AllowedImpersonationLevel = TokenImpersonationLevel.Impersonation;
}

Имперсонация включилась, запрос сертификатов для пользователя через службу работал нормально, возвращал список. Но при подписании опять начались проблемы. Если в настройках закомментирована первая строчка, в которой передавался конкретный пользователь, с логином и паролем, то на вызове функции из C++ кода:

CryptAcquireCertificatePrivateKey(context, CRYPT_ACQUIRE_SILENT_FLAG, 0, &hProv, &dwKeySpec, &mustFree)

Получал ошибку с кодом 2148073498. При этом значение поля ServiceSecurityContext.Current.WindowsIdentity.IsAuthenticated – false

Если же были указаны обе строчки (как в коде, приведенном выше), то получаю ошибку с кодом 5 на функции открытия хранилища:

CertOpenStore(CERT_STORE_PROV_SYSTEM, 0, 0, CERT_SYSTEM_STORE_CURRENT_USER, _TEXT("MY"));

В этом случае значение поля ServiceSecurityContext.Current.WindowsIdentity.IsAuthenticated – true (остальные значения WindowsIdentity совпадали)

Написал очередное письмо в КриптоПро, тут уже сотрудники развели руками, мол: “не знаем как там в Windows Store все устроено, пришлите исходники приложения и службы, тогда возможно, что-то и подскажем”.

Естественно, исходники по понятным причинам выслать было нельзя, и создавать тестовое приложение, делающее то же самое, тоже не очень хотелось. Решил сам пока попробовать разобраться, в чем может быть дело.

Загадочная особенность

Долго ли, коротко ли, пробовал разные варианты настроек, читал stackoerflow, посыпал голову пеплом и, в конце концов, случайно заметил довольно странную особенность. При попытке прочитать определенный файл из файловой системы, я получил ошибку “Нет доступа”, это было очень подозрительно, потому что я попробовал прочитать тот же самый файл, но вызов сделать не из Windows Store приложения, а из WPF, все прекрасно прочиталось. Как такое возможно? Один и тот же файл, один и тот же код в службе читающий этот файл, один и тот же пользователь “передается” в службу (то есть права на файл у пользователя точно есть), единственное различие, это то из какого приложения делается запрос в службу. Все это было очень похоже на те ограничения, которые существуют в Windows Store приложениях, которые запускаются в песочнице, там так же нет доступа к файлам, за исключением тех, которые лежат в специальной папке для этого приложения, то есть нельзя вот так просто взять и прочитать какой-нибудь файл на диске С. Но ведь файл читается не в коде Windows Store приложения, а в коде Windows-службы, у которой никаких подобных ограничений нет. Почему же он не читался? Единственное объяснение, которое приходило в голову, это то, что система накладывает ограничения не только на код приложения, но и на пользователя запустившего это приложение, в итоге в службу приходит пользователь со всеми этими ограничениями и поэтому не может ни прочитать файл, ни выполнить Crypto API (доступ к нему так же отсутствует в Windows Store). При этом, сколько я не искал в интернете, нигде не мог найти, что подобные ограничения существуют. Но факт оставался фактом. То для чего изначально задумывалась наша служба – это обойти ограничения платформы Windows Store. Но вся эта идея становилась бессмысленной, Windows оказалась не настолько глупа, и, по всей видимости, накладывала ограничения еще и на пользователя, без которого мы ничего не могли подписать. На глазах рушились все наши планы и мечты…

Гениальное решение

Продолжая поиск решений  в интернете, уже без особой надежды, наткнулся на статью Speedy.Net: Impersonating CurrentUser from SYSTEM.​ Уже не помню, как именно я ее нашел, но это было просто гениально. Там было описано решение, которое позволяло сделать “имперсонацию”, но не стандартными средствами .NET, передавая пользователя из одного потока в другой, а получить текущий процесс “explorer”, из него получить токен текущего пользователя и выполнить “имепрсонацию” используя этот токен. В итоге пользуясь таким способом, можно было не передавать пользователя в службу, а получать его непосредственно внутри службы и выполнять нужный код от имени этого пользователя. Что естественно снимало все ограничения Windows Store и служба заработала и с WPF, и c Windows Store приложениями.

Решение очень крутое, в комментариях к статье кто-то предложил автору донейт, за это решение, на тот момент я был так рад найденному решению, что тоже бы задонатил, но автор скромно ответил в комментариях, что ему ничего не нужно.

Заключение

В итоге мы получили службу, которая теперь нормально работает и в Windows 7, и в Windows 8.1, и с токенами, и с сертификатами, и с WPF, и с Windows Store. Удалось обойти все возможные ограничения, накладываемые Windows Store. И то, что изначально было сделать нельзя, теперь стало возможным! Сейчас любое решение начинается с поиска в интернете, и наше, так же когда-то началось с поиска “как сделать подписание ЭП в Windows Store”, на что был получен четкий ответ, “В Windows Store это сделать нельзя, отсутствует доступ к Crypto API”. Можно было бы просто продублировать этот ответ и сказать: “Мы это сделать не сможем”, но мы сказали: “Попробуем” и в итоге все получилось! Возможно, решение получилось довольно сложным, конечно, оно не лишено своих недостатков, но оно работает и решает конкретные задачи на нее возложенные. А еще делая подобные вещи, появляется ощущение, что ты теперь можешь сделать вообще все, что угодно. Хорошо это или плохо? Пока не разобрался

Крипто Про создание электронной подписи или как я перешел на темную сторону Cи++лы

Введение

Для мобильного клиента одной крупной Российской компании стояла задача разработки ЭЦП. Все оказалось не таким простым как я думал изначально.

Начнем с того, что мобильный клиент на данный момент есть двух версий, версия для установки на Desctop Windows, на базе платформы WPF и версия для планшетов Windows Store Application (WinRT). С версией WPF никаких особо проблем возникнуть не должно, т.к. там есть полноценный доступ ко всей системе включая Crypto API Windows, то есть достаточно установить КриптоПро CSP, которая расширяет стандартное Crypto API, добавляя в него российские криптографические алгоритмы (такие как https://ru.wikipedia.org/wiki/%D0%93%D0%9E%D0%A1%D0%A2_%D0%A0_34.10-2012https://ru.wikipedia.org/wiki/%D0%93%D0%9E%D0%A1%D0%A2_%D0%A0_34.11-2012  и т.д.) и написать реализацию на C# с вызовом этого Crypto API. Однако у платформы WinRT нет доступа к Crypto API и по заявлению КриптоПро поддержки их продукта для данной платформы нет и не планируется. Поэтому было принято решение создать специальную Windows-Службу через которую будет подписываться нужные документы, и с которой сможет взаимодействовать наше WinRT приложение.

Создание службы

При создании службы никаких особо проблем не было. Но, как известно, службы, к сожалению, не умеют отображать диалоговых окон, которые вызываются КриптоПро при подписании, например для ввода пароля от ключа. Ну не может так не может, будем отображать собственные диалоговые окна, а результаты передавать в службу. Но не все так просто, во первых нужно как-то отключить отображение диалоговых окон КриптоПРО, а во вторых, как-то понимать в какой момент, какое диалоговое окно нужно отобразить в клиенте. Путем проб и ошибок, было найдено решение, которое правда потребовало установку и добавления в проект КриптоПро .NET, т.к. стандартными методами Crypto API было уже не обойтись. Получился вот такой код:

//выбор сертификата из коллекции личных сертификатов текущего пользователя по ключу Thumbprint (SHA1 Hash сертификата)
var certificates = GetX509Certificate2Collection();
var certificate = certificates.Cast<X509Certificate2>().First(item => certificateId == item.Thumbprint);


//формирование ЭЦП
var contentInfo = new ContentInfo(dataBytes);
// Свойство detached установлено в true, таким образом, мы получим отсоединенную от исходных данных подпись.
var signedCms = new SignedCms(contentInfo, true);


/*
 * Идиотизм КриптоПро: мы не можем создать экземпляр Gost3410CryptoServiceProvider и в нем настроить параметры.
 * Поэтому приходится по приватному ключу сертификата получать абстрактного криптопровайдера, из него достать контейнер сертификата,
 * по контейнеру создать параметры криптопровайдера, задать нужные нам опции 
 * и на основе этих параметров получить экземпляр Gost3410CryptoServiceProvider!
*/
var keyContainerInfo = ((ICspAsymmetricAlgorithm) certificate.PrivateKey).CspKeyContainerInfo;
var cspParameters = new CspParameters(keyContainerInfo.ProviderType, keyContainerInfo.ProviderName,
  keyContainerInfo.KeyContainerName)
{
  Flags = CspProviderFlags.NoPrompt
};


/*
 * Возможно может понадобиться параметр CspParameters.KeyPassword
 * а также развлекаться с CspParameters.Flags,
 * если придется работать с аппаратными ключевыми носителями.
*/
var provider = new Gost3410CryptoServiceProvider(cspParameters);


//проверим, все ли достаточно для ЭЦП
try
{
  provider.SetContainerPassword(GetSecureStringPassword(keyContainerPassword));
  EnsureCertificateKeyContainerAccess(provider);
}
catch (CryptographicException e)
{
  if (e.HResult == HRESULT_NEED_KEY_CONTAINER_PASSWORD)
  {
  return string.IsNullOrEmpty(keyContainerPassword)
  ? new SignResult(SignResultCodeEnum.NeedKeyContainerPassword, null, e.Message)
  : new SignResult(SignResultCodeEnum.KeyContainerPasswordIncorrect, null, e.Message);
  }
  if (e.HResult == HRESULT_CRYPTOPROVIDER_LISENCE_ERROR)
  {
  return new SignResult(SignResultCodeEnum.CspLisenceError, null, e.Message);
  }
}
catch (Exception e)
{
  return new SignResult(SignResultCodeEnum.Error, null, e.Message);
}


/*
 * Вычисление ЭЦП
 * Результатом является ЭЦП в чистом виде (по умолчанию ЭЦП аттачится к исходным данным)
 * Криптопровайдер определяется заново из параметров выбранного сертификата.
*/
var cmsSigner = new CmsSigner(certificate);
signedCms.ComputeSignature(cmsSigner);
var signBytes = signedCms.Encode();
result.Add(Convert.ToBase64String(signBytes));

Тут в коде вначале мы выбираем сертификат, которым будем подписывать, далее в строках 19-31, мы по приватному ключу из сертификата создаем криптопровайдра через которого будем подписывать, так же тут стоит флаг CspProviderFlags.NoPrompt, который говорит, не отображать никаких окон при подписании. Далее в строке 37 провайдеру устанавливается пароль, ключ от контейнера. Если пароль установлен неправильно или вообще не установлен (при первой попытке подписания), мы получим исключение со специальным кодом ошибки, которое отловим в строках 42-51 и вернем клиенту специальный ответ, о том что нам нужен пароль. Клиент получив его отобразит собственное диалоговое окно для ввода пароля, и запустить процедуру подписания еще раз уже с введенным паролем. Если пароль верен, то в строках 64-67 мы формируем подпись.

Вроде все, такой код работает и даже решает проблему с отображением диалогового окна ввода пароля повторно если требуется подписать не один файл вложения а сразу несколько, что наблюдалось при работе с чистым Crypto API.

Первые проблемы

Служба работала, работала хорошо, но близился показ системы и службу начали проверять и тут выяснился очень неприятный момент, оказалось, что служба падала при работе с токенами (физическими носителями ключа, например записанными на флешку). У меня не было токена, поэтому проверить их работоспособность я не мог, полагая, что их работа ничем не отличается от подписания обычным сертификатом. И это действительно так, код, который подписывает не отличается для обычных сертификатов и для токенов, там нет каких-то специальных условий и проверок для этого. Ошибка происходила раньше, при попытке получить приватный ключ из токена. При получении просто возникало исключение с сообщением: “Keyset as registered is invalid” и никакими другими способами получить ключ не удавалось, создать криптопровайдер из публичного ключа так же не получалось. Обратившись за помощью к сотрудникам КриптоПро, те выяснили, что мы хотим сделать и посоветовали воспользоваться функцией CryptAcquireCertificatePrivateKey​ с переданным флагом SILENT. Выяснилось так же, что эта низкоуровневая C++ функция, аналогов которой нет на C#.

Погружение в C++

Поскольку нет аналогов функций на C#, было всего два варианта, либо вызов нужных функций через P/Invoke, либо переписывание всего подписания на C++. Поскольку помимо этой функции, требовалось вызывать еще 5-10 других C++ функций с передачей различных параметров и в качестве параметров там выступали разные C++ типы, например vector, было решено пойти по второму пути и разобраться с подписанием на C++.

И начал я с примеров от КриптоПро, которые лежал в их SDK. Взял за основу пример LowlevelSignCadesBes, вот замечательный код этого примера (с использованием CryptAcquireCertificatePrivateKey), который должен был сформировать мне подпись:

int main(int argc, char *argv[])
{
    // Открываем хранилище сертификатов пользователя
    HCERTSTORE hStoreHandle = CertOpenSystemStore(0, _TEXT("MY"));
    if (!hStoreHandle)
    {
  cout << "Store handle was not got" << endl;
  return -1;
    }


    // Получаем сертификат для подписания
    PCCERT_CONTEXT context = GetRecipientCert(hStoreHandle);


    // Если сертификат не найден, завершаем работу
    if (!context)
    {
  cout << "There is no certificate with a CERT_KEY_CONTEXT_PROP_ID " << endl
  << "property and an AT_KEYEXCHANGE private key available." << endl
  << "While the message could be sign, in this case, it could" << endl
  << "not be verify in this program." << endl
  << "For more information, read the documentation http://cpdn.cryptopro.ru/" << endl;
  return -1;
    }


    HCRYPTPROV hProv;
    int mustFree;
    DWORD dwKeySpec = 0;


    // Получаем ссылку на закрытый ключ сертификата и дестриптор криптопровайдера
    if (!CryptAcquireCertificatePrivateKey(context, 0, 0, &hProv, &dwKeySpec, &mustFree))
    {
  CertFreeCertificateContext(context);
  return 0;
    }


    // Задаем параметры
    CMSG_SIGNER_ENCODE_INFO signer = { sizeof(CMSG_SIGNER_ENCODE_INFO) };
    signer.pCertInfo = context->pCertInfo; // Сертификат подписчика
    signer.hCryptProv = hProv; // Дескриптор криптопровайдера
    signer.dwKeySpec = dwKeySpec;
    signer.HashAlgorithm.pszObjId = (LPSTR) szOID_CP_GOST_R3411; // Хэш считается по ГОСТ Р 34.11-94


    CMSG_SIGNED_ENCODE_INFO info = { sizeof(CMSG_SIGNED_ENCODE_INFO) };
    info.cSigners = 1; // Количество подписчиков
    info.rgSigners = &signer; // Массив подписчиков 


    CADES_ENCODE_INFO cadesInfo = { sizeof(cadesInfo) };
    cadesInfo.pSignedEncodeInfo = &info;


    // Открываем дескриптор сообщения для создания усовершенствованной подписи
    HCRYPTMSG hMsg = CadesMsgOpenToEncode( X509_ASN_ENCODING | PKCS_7_ASN_ENCODING, 0, &cadesInfo, 0, 0);


    if (!hMsg)
    {
  if (mustFree)
  CryptReleaseContext(hProv, 0);
  CertFreeCertificateContext(context);
  cout << "CadesMsgOpenToEncode() failed" << endl;
  return -1;
    }


    // Формируем данные для подписания
    vector<unsigned char> data(10, 25);


    // Формируем подпись в сообщении
    if (!CryptMsgUpdate(hMsg, &data[0], (unsigned long)data.size(), 1))
    {
  if (mustFree)
  CryptReleaseContext(hProv, 0);
  CertFreeCertificateContext(context);
  CryptMsgClose(hMsg);
  cout << "CryptMsgUpdate() failed" << endl;
  return -1;
    }


    DWORD size = 0;
    // Получаем размер подписи
    if (!CryptMsgGetParam(hMsg, CMSG_CONTENT_PARAM, 0, 0, &size))
    {
  if (mustFree)
  CryptReleaseContext(hProv, 0);
  CertFreeCertificateContext(context);
  CryptMsgClose(hMsg);
  cout << "CryptMsgGetParam() failed" << endl;
  return -1;
    }


    vector<unsigned char> message(size);
    // Получаем подпись
    if (!CryptMsgGetParam(hMsg, CMSG_CONTENT_PARAM, 0, &message[0], &size))
    {
  if (mustFree)
  CryptReleaseContext(hProv, 0);
  CertFreeCertificateContext(context);
  CryptMsgClose(hMsg);
  cout << "CryptMsgGetParam() failed" << endl;
  return -1;
    }


    // Закрываем хранилище
    if (!CertCloseStore(hStoreHandle, 0))
    {
        cout << "Certificate store handle was not closed." << endl;
        return -1;
    }


    // Закрываем дескриптор сообщения
    if (!CryptMsgClose(hMsg))
    {
        if (mustFree)
            CryptReleaseContext(hProv, 0);
        CertFreeCertificateContext(context);
        cout << "CryptMsgGetParam() failed" << endl;
        return -1;
    }


    // Освобождаем ресурсы
    if (mustFree)
  CryptReleaseContext(hProv, 0);
    CertFreeCertificateContext(context);


    // Сохраняем результат в файл sign.dat
    if (SaveVectorToFile<unsigned char>("sign.dat", message))
    {
  cout << "Signature was not saved" << endl;
  return -1;
    }


    cout << "Signature was saved successfully" << endl;


    return 0;
}

Первая проблема в том, что этот код собирался, но в процессе работы я получал ошибку: “Точка входа в процедуру _CadesSignMessage@24 не найдена в библиотеке DLL”, я думал что причина в том, что не хватает каких-то dll библиотек от КриптоПро, копировал их в папку с программой, но ничего не помогало. Совместно с сотрудниками КриптоПро мы выяснили, что dll загружались, но не те которые нужно. Загружались dll первой версии SDK, хотя у меня стояла вторая. Решилось все исправлением файла cades.h (не знаю почему у меня был не правильный заголовочный файл, ну да ладно с КриптПРО бывают проблемы при установках). Было задано: #define CADES_ASSEMBLY_VERSION “1.0.0.0”, исправил на: #define CADES_ASSEMBLY_VERSION “2.0.0.0” и о чудо, оно заработало! Но конечно же не все так просто. Пример из SDK не может вот так просто взять и заработать, тем более если это КриптоПро SDK 🙂

В итоге подпись формировалась, но какая-то странная, она была слишком маленькая и естественно не проходила валидацию ни на сайте госуслуг, ни через программу КриптоАРМ. Пришлось снова писать сотрудникам КриптоПро, задавая неудобные вопросы: “а почему это пример из вашего SDK не работает?”. Оказалось, что не хватает 10-20 строк кода, чтобы все заработало и сформировало корректную подпись. В итоге строки 42-55, нужно было заменить на вот такой код:

memset(&SignerEncodeInfo, 0, sizeof(CMSG_SIGNER_ENCODE_INFO));
SignerEncodeInfo.cbSize = sizeof(CMSG_SIGNER_ENCODE_INFO);
SignerEncodeInfo.pCertInfo = context->pCertInfo; // Сертификат подписчика
SignerEncodeInfo.hCryptProv = hProv; // Дескриптор криптопровайдера
SignerEncodeInfo.dwKeySpec = dwKeySpec;
SignerEncodeInfo.HashAlgorithm.pszObjId = (LPSTR)szOID_CP_GOST_R3411; // Хэш считается по ГОСТ Р 34.11-94
//SignerEncodeInfo.pvHashAuxInfo = NULL;


SignerEncodeInfoArray[0] = SignerEncodeInfo;


SignerCertBlob.cbData = context->cbCertEncoded;
SignerCertBlob.pbData = context->pbCertEncoded;


SignerCertBlobArray[0] = SignerCertBlob;
memset(&SignedMsgEncodeInfo, 0, sizeof(CMSG_SIGNED_ENCODE_INFO));
SignedMsgEncodeInfo.cbSize = sizeof(CMSG_SIGNED_ENCODE_INFO);
SignedMsgEncodeInfo.cSigners = 1;
SignedMsgEncodeInfo.rgSigners = SignerEncodeInfoArray;
SignedMsgEncodeInfo.cCertEncoded = 1;
SignedMsgEncodeInfo.rgCertEncoded = SignerCertBlobArray;


CADES_ENCODE_INFO cadesInfo = { sizeof(cadesInfo) };
cadesInfo.pSignedEncodeInfo = &SignedMsgEncodeInfo;

По моему, все очевидно, не правда ли? Не понимаю как я сразу до этого не додумался? 🙂

Теперь достаточно добавить в CryptAcquireCertificatePrivateKey флаг CRYPT_ACQUIRE_SILENT_FLAG вторым параметром, чтобы убедится, что все работает без диалоговых окон. Так же был добавлен код установки пароля:

if (!CryptSetProvParam(
  hProv,
  PP_KEYEXCHANGE_PIN,
  (BYTE*)password,
  0))
{
  if (mustFree)
  CryptReleaseContext(hProv, 0);
  CertFreeCertificateContext(context);
  cout << "CryptSetProvParam not succeeded." << endl;
  return -1;
}

Отлично, все работает как надо!

Осваиваем P/Invoke

Казалось бы практически всё, работающий код на C++ у нас есть, теперь осталось как-то его вызывать через нашу службу. Но и тут без сложностей и проблем не обошлось. Переписывать службу все на C++ я не решился, поэтому решил создать свою dll с единcтвенным методом Sign и вызывать этот метод из C# кода через P/Invoke.

Изначально я хотел передавать в эту функцию строку для подписания, пароль и идентификатор сертификата, а возвращать результат подписи, в виде строки. Оказалось со строками в C++ не так все просто, есть целый рад типов TCHAR, WCHAR, LPSTR, LPWSTR,LPCTSTR, есть так же просто массив символов char, а так же wchar_t. Оказывается такое разнообразие связано с необходимостью поддержки Unicode и ANSII. Помимо этого, при вызове кода через P/Invoke необходимо указать специальным параметром, как будет осуществляться конвертирование (Marshal) из C++ строки в C# строку, делается это примерно так:

[DllImport("<path to DLL>", CharSet = CharSet.Ansi, CallingConvention = CallingConvention.StdCall)]
[return: MarshalAs(UnmanagedType.LPStr)]
public static extern string StringReturnAPI01();

Я долго экспериментировал, подставляя в качестве возвращаемого значения разные типы строк и разные типы преобразований, но на выходе получал либо знаки вопросов (????), либо набор каких-то непонятных символов. Оказалось, что проблема не так тривиальна, и связано это с необходимостью выделения памяти под возвращаемую строку и при этом освобождения памяти в неуправляемом коде (одна из статей коротко касающаяся этой проблемы https://manski.net/2012/06/pinvoke-tutorial-passing-strings-part-2/). Как это сделать я так и не разобрался и пошел более простым путем. Я создаю StringBuilder в C# с заданным размером и передаю его в качестве аргумента в вызываемую функцию. Используется именно StringBuilder, а не string потому, что string в C#, так же как и в Java не изменяемый тип, и при любых манипуляциях создается новый объект string. Итак мы выделяем память под нашу строку в C# коде и передаем ее в функцию Sign, так производится подпись и результат записывается в нашу строку. После завершения работы C++ функции в нашем StringBuilder содержится нужная строка.

Так же пришлось разобраться, что такое тип usigned char и чем он отличается от просто char. А так же какие типы из C++ каким соответствуют в C#, например возвращаемый мною тип DWORD из C++, соответствует uint в C#

В итоге получилось следующее:

[DllImport("Arm.DigitalSignatureClassLibrary.dll", CallingConvention = CallingConvention.Cdecl, CharSet = CharSet.Ansi, SetLastError = true)]
        public extern static uint Sign(StringBuilder resultSignature, byte[] contentForSign, int contentLength, string password, byte[] certId);

var resultSignature = new StringBuilder(5000);

// Вызываем функцию подписания
var resultCode = Sign(resultSignature, contetForSign, contetForSign.Length, keyContainerPassword, certificateIdBytes);

Сигнатура функции на C++:

DWORD SignA(char *pBuff, unsigned char *contentForSign, int contentLen, char *password, unsigned char *certId);

Данный код хорошо работал, все подписывалось, подпись была валидной. Но когда я попробовал выбрать для подписания другой сертификат, все начало падать, причем падало неконтролируемо и все время в разных местах C++ кода. Изначально я думал, что где-то не очищается память. В итоге мне все же удалось поймать хоть какую-то вразумительную ошибку, которая гласила: “Access violation reading location”. Оказалось, что результат подписания был больше чем 5000 символов. Связано это с тем, что в результат помимо самой ЭП попадает еще и информация о сертификате, которым подписывается, поэтому с одним небольшим сертификатом все работало, а с другим начало падать. Судя по всему, когда результат превышал 5000, он пытался записать не влезающие символы в области памяти, в которые не должен был писать. В итоге все решилось просто увеличением длины StringBuilder до 10 000 символов, хотя подпись больше чем 5126 символов мне не удалось получить, на всякий случай выделяем с запасом.

Итог

На текущий момент все работает, без каких либо проблем. Вывод на C++ писать не просто и нужно учитывать много нюансов, но он позволяет сделать то, что на других языках сделать сложно или вообще невозможно.