Подключение КриптоПро в Mono

Автор: admin от 14-09-2018, 13:20, посмотрело: 45

В связи с переходом на Linux возникла необходимость переноса одной из наших серверных систем написанной на C# в Mono. Система работает с усиленными ЭЦП, поэтому одной из поставленных перед нами задач была проверка работоспособности ГОСТовых сертификатов от КриптоПро в mono. Сам КриптоПро уже довольно давно реализовал CSP под Linux, но первая же попытка использования показала, что нативные классы криптографии Mono (аналогичные тем, что есть в базовом .Net — X509Store, X509Certificate2 и проч.) не только не работают с ГОСТовыми ключами, они даже не видят их в своих хранилищах. В силу этого работу с криптографией пришлось подключать напрямую через библиотеки КриптоПро.



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



А затем формируем статический класс «UCryptoAPI» который в зависимости от системы будет вызывать метод одного из двух классов:



/**<summary>Закрыть хранилище</summary>
* <param name="_iFlags">Флаги (нужно ставить 0)</param>
* <param name="_hCertStore">Ссылка на хранилище сертификатов</param>
* <returns>Флаг успешности закрытия хранилища</returns>
* **/
internal static bool CertCloseStore(IntPtr _hCertStore, uint _iFlags) {
    if (fIsLinux)
        return LCryptoAPI.CertCloseStore(_hCertStore, _iFlags);
    else
        return WCryptoAPI.CertCloseStore(_hCertStore, _iFlags);
}
/**<summary>Находимся в линуксе</summary>**/
public static bool fIsLinux {
    get {
        int iPlatform = (int) Environment.OSVersion.Platform;
        return (iPlatform == 4) || (iPlatform == 6) || (iPlatform == 128);
    }
}

Таким образом используя методы класса UCryptoAPI можно реализовывать почти единый код под обе системы.


Поиск сертификата


Работа с криптографией обычно начинается с поиска сертификата, для этого в crypt32.dll имеется два метода CertOpenStore (открывает указанное хранилище сертификатов) и простой CertOpenSystemStore (открывает личные сертификаты пользователя). В силу того, что работа с сертификатами не ограничивается только личными сертификатами пользователя подключаем первый:



Поиск происходит в несколько этапов:

  1. открытие хранилища;

  2. формирование структуры данных по которым ищем;

  3. поиск сертификата;

  4. если требуется, то проверка сертификата (описана в отдельном разделе);

  5. закрытие хранилища и освобождение структуры из пункта 2 (т. к. повсюду здесь идет работа с неуправляемой памятью .Net за нас ничего по очистке делать не будет);


В ходе поиска сертификатов есть несколько тонких моментов.


КриптоПро в Linux работает с ANSI строками, а в Windows с UTF8, поэтому:



  1. при подключении метода открытия хранилища в Linux необходимо параметру кода хранилища явно указать тип маршалинга [In, MarshalAs (UnmanagedType.LPStr)];

  2. передавая строку для поиска (например, по имени Subject) ее необходимо преобразовывать в набор байт различными кодировками;

  3. для всех констант криптования, у которых есть вариация по типу строки (например, CERT_FIND_SUBJECT_STR_A и CERT_FIND_SUBJECT_STR_W) в Windows необходимо выбирать *_W, а в Linux *_A;


Метод MapX509StoreFlags можно взять напрямую из исходников Microsoft без изменений, он просто формирует итоговую маску исходя из .Net флагов.


Значение по которому происходит поиск зависит от типа поиска (сверяйтесь с MSDN для CertFindCertificateInStore), в примере приведены два самых часто используемых варианта — для строкового формата (имена Subject, Issuer и проч) и бинарного (отпечаток, серийный номер).


Процесс создания сертификата из IntPtr в Windows и в Linux сильно отличается. Windows создаст сертификат простым способом:

 new X509Certificate2(hCert);


в Linux же приходиться создавать сертификат в два этапа:

X509Certificate2(new X509Certificate(hCert));


В дальнейшем нам для работы потребуется доступ к hCert, и его надо бы сохранить в объекте сертификата. В Windows его позже можно достать из свойства Handle, однако Linux преобразует структуру CERT_CONTEXT, лежащую по ссылке hCert, в ссылку на структуру x509_st (OpenSSL) и именно ее прописывает в Handle. Поэтому стоит создать наследника от X509Certificate2 (ISDP_X509Cert в примере), который сохранит у себя в отдельном поле hCert в обеих системах.


Не стоит забывать, что это ссылка на область неуправляемой памяти и ее надо освобождать после окончания работы. Т.к. в .Net 4.5 X509Certificate2 не Disposable — очистку методом CertFreeCertificateContext, надо проводить в деструкторе.


Формирование подписи


При работе с ГОСТовыми сертификатами почти всегда используются отцепленные подписи с одним подписантом. Для того чтобы создать такую подпись требуется довольно простой блок кода:



В ходе работы метода формируется структура с параметрами и вызывается метод подписания. Структура параметров может позволять сохранить в подписи сертификаты для формирования полной цепочки (поля cMsgCert и rgpMsgCert, первый хранит количество сертификатов, второй список ссылок на структуры этих сертификатов).


Метод подписания может получать один или несколько документов для одновременного подписания одной подписью. Это, кстати, не противоречит 63 ФЗ и бывает очень удобно, т. к. пользователь вряд ли обрадуется необходимости несколько раз нажимать на кнопку «подписать».


Основной странностью данного метода является то, что он не работает в режиме двух вызовов, характерного для большинства библиотечных методов, работающих с большими блоками памяти (первый с null — выдает необходимую длину буфера, второй заполняет буфер). Поэтому необходимо создать большой буфер, а затем укоротить его по реальной длине.


Единственной серьезной проблемой является поиск OID алгоритма хэширования (Digest) используемый при подписании — в явном виде его нет в сертификате (там есть только алгоритм самой подписи). И если в Windows его можно указать пустой строкой — он подцепится автоматически, но Linux откажется подписывать если алгоритм не тот.


Но тут есть хитрость — в информации об алгоритме подписи (структура CRYPT_OID_INFO) в pszOID храниться OID подписи, а в Algid — храниться идентификатор алгоритма хэширования. А преобразовать Algid в OID уже дело техники:



Внимательно прочитав код можно удивится, что идентификатор алгоритма получается простым способом (CertOIDToAlgId) а Oid по нему — сложным (CryptFindOIDInfo). Логично было бы предположить использование либо оба сложных, либо оба простых способа, и в Linux оба варианта успешно работают. Однако в Windows сложный вариант получения идентификатора и простой получения OID работает нестабильно, поэтому стабильным решением будет вот такой странный гибрид.


Проверка подписи


Проверка подписи происходит в два этапа, в начале проверяется сама подпись, а затем проверяется сертификат, которым она была сформирована (цепочка, дата подписания и проч).
Так же как и при подписании необходимо указать набор подписываемых данных, параметры подписи и саму подпись:



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


После извлечения сертификата подписанта преобразуем его в наш класс и проверяем (если это указано в параметрах метода). Для проверки используется дата подписания первого подписанта (см. раздел извлечение информации из подписи, и раздел проверка сертификата).


Извлечение информация из подписи


Часто в системах работающих с криптографией требуется печатное представление подписи. В каждом случае оно разное, поэтому лучше сформировать класс информации о подписи, который будет содержать информацию в удобном для использования виде и уже с его помощью обеспечивать печатное представление. В .Net такой класс есть — SignedCms, однако его аналог в mono c подписями КритоПро, во первых отказывается работать, во вторых содержит модификатор sealed и в третьих почти все свойства у него закрыты на запись, поэтому придется формировать свой аналог.


Сама по себе подпись содержит два основных элемента — список сертификатов и список подписантов. Список сертификатов может быть пустой, а может содержать в себе все сертификаты для проверки, включая полные цепочки. Список же подписантов указывает на кол-во реальных подписей. Связь между ними осуществляется по серийному номеру и издателю (Issuer). Теоретически в одной подписи может быть два сертификата от разных издателей с одним серийным номером, но на практике этим можно пренебречь и искать только по серийному номеру.


Чтение подписи происходит следующим образом:



Разбор подписи происходит в несколько этапов, вначале формируется структура сообщения (CryptMsgOpenToDecode), затем в нее вносятся реальные данные подписи (CryptMsgUpdate). Остается проверить что это реально подпись и получить сначала список сертификатов, а потом список подписантов. Список сертификатов извлекается последовательно :



Сначала определятся количество сертификатов из параметра CMSG_CERT_COUNT_PARAM, а затем последовательно извлекается информация о каждом сертификате. Завершает процесс создания формирование контекста сертификата и на его основе самого сертификата.


Извлечение данных подписанта сложнее. В них содержится указание на сертификат и список параметров подписи (например, дата подписания). Процесс извлечения данных выглядит следующим образом:



В ходе него сначала определяется размер структуры подписанта, а затем извлекается и сама структура CMSG_SIGNER_INFO. В ней легко найти серийный номер сертификата и по нему найти нужный сертификат в ранее извлеченном списке. Обратите внимание, что серийный номер содержится в обратном порядке.


После извлечения сертификата необходимо определить параметры подписи, самая важная из которых — дата подписания (даже если это не верифицированная сервером штампа даты времени, для отображения она очень важна).



Атрибуты представляют из себя вложенный справочник вида Oid – список значений (по сути это разобранная структура ASN.1). Пройдя по первому уровню формируем вложенный список:



Ключевой особенностью данного процесса является правильный подбор наследника Pkcs9AttributeObject. Проблема в том, что стандартный способ создания в mono не работает и приходится формировать выбор класса прямо в коде. К тому же из основных типов Mono на данный момент позволяет формировать только дату.


Обернув представленные выше методы в два класса — информация о подписи и информация о подписанте — получаем аналог SignedCms, из которой при формировании печатного вида извлекаем данные.


Шифрование


Процесс шифрования во многом аналогичен процессу подписания, он довольно прост, и основная проблема так же состоит в определении алгоритма. В отличии от подписи шифрование чаще всего используются сцепленное в адрес одного или сразу нескольких адресатов (например, шифруют еще и в адрес себя, чтобы была возможность прочитать сообщение своим ключом).



Процесс шифрования происходит в три этапа — заполнение параметров, определение длины и наконец шифрование. Зашифрованные данные могут быть большие, вероятно, поэтому метод поддерживает режим двух вызовов.


В примере шифруется в адрес одного адресата, но путем добавления дополнительных сертификатов в массив и установке общего количества в параметры метода, можно увеличить число адресатов.


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



В примере извлекается контекст закрытого ключа и по нему происходит поиск по алгоритмам. Но в этом списке находятся все алгоритмы (обмена ключей, хэширования, подписи, шифрования и проч.), поэтому надо отфильтровать только алгоритмы шифрования. Пытаемся по каждому извлечь информацию ограничившись группой алгоритмов шифрования (UCConsts.CRYPT_ENCRYPT_ALG_OID_GROUP_ID). И если информация найдена — значит это наш алгоритм.


В случае если таких алгоритмов больше чем один можно так же фильтровать по размеру (опираясь на размер алгоритма хэширования).


Дешифрование


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



При установке параметров указывается хранилище, из которых система будет пытаться извлечь подходящий сертификат с ключом. В результате работы система выдаст дешифрованные данные и сертификат, который был использован (в Linux сертификат всегда возвращается пустой).


Проверка сертификата


Сертификат это не только открытый ключ, но еще и набор разной информации о его владельце, о том, кто его выдал и о наборе действий, которые с его помощью можно делать. Так же у сертификата есть период действия и возможность отзыва, в случае компрометации. Чаще всего под проверкой сертификата подразумевается следующее:



  1. целостность цепочки (сертификат издателя, сертификат издателя сертификата издателя, и т. п.);

  2. сертификат корневого издателя — должен быть в хранилище доверенных корневых центров;

  3. период действия всех сертификатов — момент использования сертификата должен быть в границах этого периода;

  4. каждый из сертификатов в цепочке, кроме корневого, должен отсутствовать в списке отозванных у своего издателя (CRL);


По хорошему надо еще проверять и права подписи, но в реальной жизни это делается редко.


Как уже понятно из введения, проверка сертификата на валидность, одна из самых сложных задач. Именно поэтому в библиотеке масса методов для реализации каждого из пунктов в отдельности. Поэтому, для упрощения обратимся к исходникам .Net для метода X509Certificate2.Verify() и возьмем их за основу.


Проверка состоит из двух этапов:

  1. сформировать цепочку сертификатов вплоть до корневого;

  2. проверить каждый из сертификатов в ней (на отзыв, время и проч.);


Такая проверка должна осуществляться перед подписанием и шифрованием на текущую дату, и в момент проверки подписи на дату подписания. Сам метод проверки небольшой:



Сначала формируется цепочка методом BuildChain, а затем она проверяется. В ходе формирования цепочки формируется структура параметров, дата проверки и флаги проверки:



Это сильно упрощенный вариант формирования цепочки по сравнению с тем, как ее формирует Microsoft. Структуры hCertPolicy и hAppPolicy можно наполнить OID-ами, отображающими права на действия, которые необходимы в проверяемом сертификате. Но в примере, будем считать, что их мы не проверяем.


Так же можно в параметры построения цепочки добавить дополнительное хранилище сертификатов (например, извлеченное из подписи).


Метод MapRevocationFlags — можно взять напрямую из исходников .Net без изменений —он просто формирует uint по набору передаваемых флагов.


Заключение


Набор реализованных методов работы с криптографией был подвергнут нагрузочному тестированию по схеме цикла полной работы:



  1. ожидание 10 мс;

  2. извлечение сертификата;

  3. подписание byte[] {1, 2, 3, 4, 5};

  4. проверка полученной подписи;

  5. извлечение параметров подписи;

  6. шифрование byte[] {1, 2, 3, 4, 5};

  7. дешифрование полученных данных;


Данный цикл был запущен в Windows и в Linux в 1-ом, 10-и и 50-и потоках, чтобы проверить работу в Linux сразу в нескольких потоках. Приложение в Linux работало стабильно в течении какого-то времени во много-поточном режиме (и чем больше потоков, тем меньше по времени), а затем «вставало» наглухо. Что свидетельствует о наличии взаимной блокировки (deadlock-е) в библиотеке (при нарушении работы с потоками связанных с разделяемым доступом обычно происходит падение с «Access Violation»).


По этой причине для обеспечения стабильности работы все методы класса UCryptoAPI стоит обрамить критической секцией. Для этого добавляем поле fpCPSection типа object после чего в каждый вызов добавляем следующую конструкцию:


private static object fpCPSection = new object();
/**<summary>Закрывает сообщение</summary>
* <param name="_hCryptMsg">Указатель на сообщение</param>
* **/
internal static bool CryptMsgClose(IntPtr _hCryptMsg) {
    lock (pCPSection) {
        if (fIsLinux)
            return LCryptoAPI.CryptMsgClose(_hCryptMsg);
        else
            return WCryptoAPI.CryptMsgClose(_hCryptMsg);
    }
}
/**<summary>Критическая секция для работы с КриптоПро</summary>**/
public static object pCPSection {
    get { return fpCPSection;}
}


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



Нагрузочное тестирование так же показало утечки памяти в mono при обращении к полям Issuer и Subject сертификата. Утечка, вероятно, происходит при попытке mono сформировать классы X500DistinguishedName для подписанта и издателя. К счастью, mono посчитали этот процесс достаточно ресурсоемким (или же они знают об утечке), поэтому предусмотрели кэширование результата данного формирования во внутренние поля сертификата (impl.issuerName и impl.subjectName). Поэтому данная утечка лечится прямой записью через отражение (Reflection) в эти поля экземпляров класса X500DistinguishedName, сформированных на базе значений из структуры CERT_CONTEXT сертификата.



Ссылки




  • документация КриптоПро CAPILite

  • ресурс c объявлением стандартных экспортируемых функций в С#

  • исходники .Net:


  • класс CAPIBase

  • класс X509Certificate2

  • класс SignedCMS

  • класс SignerInfo




  • исходники mono:


  • класс X509Certificate2

  • класс X509CertificateImplBtls





  • Источник: Хабр / Интересные публикации

    Категория: Программирование, Криптография

    Уважаемый посетитель, Вы зашли на сайт как незарегистрированный пользователь.
    Мы рекомендуем Вам зарегистрироваться либо войти на сайт под своим именем.

    Добавление комментария

    Имя:*
    E-Mail:
    Комментарий:
    Полужирный Наклонный текст Подчеркнутый текст Зачеркнутый текст | Выравнивание по левому краю По центру Выравнивание по правому краю | Вставка смайликов Выбор цвета | Скрытый текст Вставка цитаты Преобразовать выбранный текст из транслитерации в кириллицу Вставка спойлера
    Введите два слова, показанных на изображении: *