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

от автора

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

Установка сертификата

Перед тем как реализовывать код, необходимо установить сертификат и убедится что он нормально работает.

Установка сертификата

Компонент КриптоПро CSP версии 3.9 был установлен в Centos 7 в папку /opt/cprocsp. Для того, чтобы не было конфликтов между утилитами mono и КриптоПро, имеющих одинаковые названия (например, certmgr), в переменные окружения не стали вносить путь до папки и все утилиты вызывались по полному пути.

Для начала определяем список считывателей:
/opt/cprocsp/bin/amd64/csptest -enum -info -type PP_ENUMREADERS | iconv -f cp1251

Если среди списка нет считывателя с папки на диске (HDIMAGE) ставим его:
/opt/cprocsp/sbin/amd64/cpconfig -hardware reader -add HDIMAGE store

После чего можно создавать контейнеры вида ‘\\.\HDIMAGE\{имя контейнера}’ путем либо создания нового контейнера с ключами:
/opt/cprocsp/bin/amd64/csptest -keyset -provtype 75 -newkeyset -cont '\\.\HDIMAGE\test'

либо формируя папку /var/opt/cprocsp/keys/root/{имя контейнера}.000, в которой располагается стандартный набор файлов контейнера КриптоПро (*.key, *.mask, и проч.).

После этого сертификат из контейнера можно установить в хранилище сертификатов:
/opt/cprocsp/bin/amd64/certmgr -inst mMy -cont '\\.\HDIMAGE\{имя контейнера}'

Установленный сертификат можно увидеть с помощью следующей команды:
/opt/cprocsp/bin/amd64/certmgr -list mMy

Работу сертификата можно проверить следующим образом:
/opt/cprocsp/bin/amd64/cryptcp – sign -norev -thumbprint {отпечаток} {файл} {файл подписи}
/opt/cprocsp/bin/amd64/cryptcp – verify -norev {файл подписи}

Если с сертификатом все нормально, то можно переходить к подключению в коде.

Подключение в коде

Несмотря на процесс переноса в Linux система должна была продолжать функционировать и в среде Windows, поэтому внешне работа с криптографией должна была осуществляться через общие методы вида «byte[] SignData(byte[] _arData, X509Certificate2 _pCert)», которые должны были одинаково работать как в Linux, так и в Windows.

Анализ методов библиотек криптографии оказался удачным, т. к. КриптоПро реализовало библиотеку «libcapi20.so» которая полностью мимикрирует под стандартные библиотеки Windows шифрования — «crypt32.dll» и «advapi32.dll». Возможно, конечно, не целиком, но все необходимые методы для работы с криптографии там в наличии, и почти все работают.

Поэтому формируем два статических класса «WCryptoAPI» и «LCryptoAPI» каждый из которых будет импортировать необходимый набор методов следующим образом:

[DllImport(LIBCAPI20, SetLastError = true)] internal static extern bool CertCloseStore(IntPtr _hCertStore, uint _iFlags); 

Синтаксис подключения каждого из методов можно либо сформировать самостоятельно, либо воспользоваться сайтом 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 (открывает личные сертификаты пользователя). В силу того, что работа с сертификатами не ограничивается только личными сертификатами пользователя подключаем первый:

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

/**<summary>Поиск сертификата (первого удовлетворяющего критериям поиска)</summary> * <param name="_pFindType">Тип поиска</param> * <param name="_pFindValue">Значение поиска</param> * <param name="_pLocation">Место </param> * <param name="_pName">Имя хранилища</param> * <param name="_pCert">Возвращаемый сертификат</param> * <param name="_sError">Возвращаемая строка с ошибкой</param> * <param name="_fVerify">Проверить сертфиикат</param> * <returns>Стандартый код ошибки, если UConsts.S_OK то все ок</returns> * **/ public static int FindCertificateCP(string _pFindValue, out X509Certificate2 _pCert, ref string _sError,                                      StoreLocation _pLocation = StoreLocation.CurrentUser,                               StoreName _pName = StoreName.My,                                      X509FindType _pFindType = X509FindType.FindByThumbprint,                                                                                  bool _fVerify = false) {     _pCert = null;     IntPtr   hCert = IntPtr.Zero;                             GCHandle hInternal    = new GCHandle();     GCHandle hFull        = new GCHandle();     IntPtr   hSysStore    = IntPtr.Zero;     try {           // 0) Открываем хранилище           hSysStore = UCryptoAPI.CertOpenStore(UCConsts.AR_CERT_STORE_PROV_SYSTEM[fIsLinux.ToByte()],                                                UCConsts.PKCS_7_OR_X509_ASN_ENCODING,                                                IntPtr.Zero,                                                UCUtils.MapX509StoreFlags(_pLocation, OpenFlags.ReadOnly),                                                UCConsts.AR_CRYPTO_STORE_NAME[(int)_pName]);           if (hSysStore == IntPtr.Zero) {               _sError = UCConsts.S_ERR_STORE_OPEN.Frm(Marshal.GetLastWin32Error());               return UConsts.E_CRYPTO_ERR;           }            // 1) Формируем данные в пакете           if ((_pFindType == X509FindType.FindByThumbprint) || (_pFindType == X509FindType.FindBySerialNumber))           {               byte[] arData = _pFindValue.FromHex();               CRYPTOAPI_BLOB cryptBlob;               cryptBlob.cbData = arData.Length;               hInternal = GCHandle.Alloc(arData, GCHandleType.Pinned);               cryptBlob.pbData = hInternal.AddrOfPinnedObject();               hFull = GCHandle.Alloc(cryptBlob, GCHandleType.Pinned);                               } else {                byte[] arData;                if(fIsLinux)                    arData = Encoding.UTF8.GetBytes(_pFindValue);                else                    arData = Encoding.Unicode.GetBytes(_pFindValue);                hFull = GCHandle.Alloc(arData, GCHandleType.Pinned);           }           // 2) Получаем            IntPtr hPrev = IntPtr.Zero;           do {                hCert = UCryptoAPI.CertFindCertificateInStore(hSysStore,                                                               UCConsts.PKCS_7_OR_X509_ASN_ENCODING, 0,                                                              UCConsts.AR_CRYPT_FIND_TYPE[(int)_pFindType, fIsLinux.ToByte()],                                                              hFull.AddrOfPinnedObject(), hPrev);                // 2.1) Освобождаем предыдущий                if(hPrev != IntPtr.Zero) UCryptoAPI.CertFreeCertificateContext(hPrev);                // 2.2) Кончились в списке                if(hCert == IntPtr.Zero) return UConsts.E_NO_CERTIFICATE;                                    // 2.3) Нашли и валиден                X509Certificate2 pCert = new ISDP_X509Cert(hCert);                if (!_fVerify || pCert.ISDPVerify()) {                    hCert =  IntPtr.Zero;                    _pCert = pCert;                    return UConsts.S_OK;                }                 hPrev = hCert;                // Чтобы не очистило                hCert = IntPtr.Zero;           } while(hCert != IntPtr.Zero);           return UConsts.E_NO_CERTIFICATE;     } catch (Exception E) {            _sError = UCConsts.S_FIND_CERT_GEN_ERR.Frm(E.Message);           return UConsts.E_GEN_EXCEPTION;     } finally {           // Очищаем ссылки и закрываем хранилище           if(hInternal.IsAllocated) hInternal.Free();           if(hFull.IsAllocated) hFull.Free();           if (hCert != IntPtr.Zero) UCryptoAPI.CertFreeCertificateContext(hCert);           UCryptoAPI.CertCloseStore(hSysStore, 0);                     } } 

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

  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, надо проводить в деструкторе.

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

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

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

/**<summary> Подписывает информацию</summary> * <param name="_arData">Данные для подписания</param> * <param name="_pCert">Сертификат</param> * <param name="_sError">Возвращаемая строка с ошибкой</param> * <param name="_arRes">Подпись сертфииката</param> * <returns>Стандартый код ошибки, если UConsts.S_OK то все ок</returns> * **/ public static int SignDataCP(byte[] _arData, X509Certificate2 _pCert, out byte[] _arRes, ref string _sError) {     _arRes = new byte[0];     // 0) Формируем параметры     CRYPT_SIGN_MESSAGE_PARA pParams = new CRYPT_SIGN_MESSAGE_PARA();     pParams.cbSize = Marshal.SizeOf(typeof(CRYPT_SIGN_MESSAGE_PARA));     pParams.dwMsgEncodingType      = (int)(UCConsts.PKCS_7_OR_X509_ASN_ENCODING);     pParams.pSigningCert           = _pCert.getRealHandle();     pParams.cMsgCert               = 1;                 pParams.HashAlgorithm.pszObjId = _pCert.getHashAlgirtmOid();     IntPtr pGlobData = Marshal.AllocHGlobal(_arData.Length);     GCHandle pGC = GCHandle.Alloc(_pCert.getRealHandle(), GCHandleType.Pinned);     try {         pParams.rgpMsgCert = pGC.AddrOfPinnedObject();         Marshal.Copy(_arData, 0, pGlobData, _arData.Length);         uint iLen = 50000;         byte[] arRes = new byte[iLen];         // 1) Формирование подписи         if (!UCryptoAPI.CryptSignMessage(ref pParams, true, 1, new IntPtr[1] { pGlobData },                                          new uint[1] { (uint)_arData.Length }, arRes, ref iLen)) {             _sError = UCConsts.S_MAKE_SIGN_ERR.Frm(Marshal.GetLastWin32Error());             return UConsts.E_CRYPTO_ERR;         }         Array.Resize(ref arRes, (int)iLen);         _arRes = arRes;         return UConsts.S_OK;;     } catch (Exception E) {          _sError = UCConsts.S_MAKE_SIGN_ERR.Frm(E.Message);         return UConsts.E_GEN_EXCEPTION;     } finally {         pGC.Free();         Marshal.FreeHGlobal(pGlobData);     } } 

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

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

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

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

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

Получение OID алгоритма хэширования

/**<summary>Получение OID алгоритма хэширования сертификату</summary> * <param name="_hCertHandle">Хэндл сертификата</param> * <param name="_sOID">Возвращаемый параметр OID</param> * <param name="_sError">Возвращаемая строка с ошибкой</param> * <returns>Стандартный код ошибки, если UConsts.S_OK то все ок</returns> * **/ internal static int GetHashAlgoritmOID(IntPtr _hCertHandle, out string _sOID, ref string _sError) {     _sOID = "";     IntPtr hHashAlgInfo = IntPtr.Zero;     IntPtr hData        = IntPtr.Zero;     try {         CERT_CONTEXT pContext = (CERT_CONTEXT)Marshal.PtrToStructure(_hCertHandle, typeof(CERT_CONTEXT));         CERT_INFO pCertInfo = (CERT_INFO)Marshal.PtrToStructure(pContext.pCertInfo, typeof(CERT_INFO));         // Извлекаем AlgID         // через UCryptoAPI.CertAlgIdToOID  в Windows первый раз работает, второй падает         byte[] arData = BitConverter.GetBytes(UCryptoAPI.CertOIDToAlgId(pCertInfo.SignatureAlgorithm.pszObjId));         hData = Marshal.AllocHGlobal(arData.Length);                         Marshal.Copy(arData, 0, hData, arData.Length);         // Поиск OID         hHashAlgInfo = UCryptoAPI.CryptFindOIDInfo(UCConsts.CRYPT_OID_INFO_ALGID_KEY,                                                    hData,                                                    UCConsts.CRYPT_HASH_ALG_OID_GROUP_ID);         if (hHashAlgInfo == IntPtr.Zero) {             _sError = UCConsts.S_NO_HASH_ALG_ERR.Frm( Marshal.GetLastWin32Error());             return UConsts.E_GEN_EXCEPTION;         }         CRYPT_OID_INFO pHashAlgInfo = (CRYPT_OID_INFO)Marshal.PtrToStructure(hHashAlgInfo, typeof(CRYPT_OID_INFO));         _sOID = pHashAlgInfo.pszOID;         return UConsts.S_OK;     } catch (Exception E) {         _sError = UCConsts.S_DETERM_HASH_ALG_ERR.Frm(E.Message);         return UConsts.E_GEN_EXCEPTION;     } finally {          Marshal.FreeHGlobal(hData);     } } 

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

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

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

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

/**<summary>Формирует стандартную сктруктуру для проверки подписи </summary> * <returns>Структуру</returns> * **/ internal static CRYPT_VERIFY_MESSAGE_PARA GetStdSignVerifyPar() {     CRYPT_VERIFY_MESSAGE_PARA  pVerifyParams  =  new CRYPT_VERIFY_MESSAGE_PARA();     pVerifyParams.cbSize = (int)Marshal.SizeOf(pVerifyParams);     pVerifyParams.dwMsgEncodingType = UCConsts.PKCS_7_OR_X509_ASN_ENCODING;     pVerifyParams.hCryptProv = 0;     pVerifyParams.pfnGetSignerCertificate   = IntPtr.Zero;     pVerifyParams.pvGetArg  = IntPtr.Zero;     return pVerifyParams; } /**<summary>Проверяет подпись</summary> * <param name="_arData">данные, которые было подписаны</param> * <param name="_pSign">подпись</param> * <param name="_pCert">сертификат</param> * <param name="_sError">возвращаемая строка с ошибкой</param> * <param name="_fVerifyOnlySign">Проверять только подпись</param> * <param name="_pRevMode">Режим проверки сертификата</param> * <param name="_pRevFlag">Флаг проверки сертфииката</param> * <returns>Стандартый код ошибки, если UConsts.S_OK то все ок</returns> * <remarks>Проверяется только первый подписант</remarks> * **/ public static int CheckSignCP(byte[] _arData, byte[] _pSign, out X509Certificate2 _pCert, ref string _sError,                               bool _fVerifyOnlySign = true,                                X509RevocationMode _pRevMode = X509RevocationMode.Online,                               X509RevocationFlag _pRevFlag = X509RevocationFlag.ExcludeRoot){     _pCert = null;     IntPtr pHData = Marshal.AllocHGlobal(_arData.Length);     GCHandle pCertContext = GCHandle.Alloc(IntPtr.Zero, GCHandleType.Pinned);     try {         Marshal.Copy(_arData, 0, pHData, _arData.Length);         CRYPT_VERIFY_MESSAGE_PARA pVerParam = UCUtils.GetStdSignVerifyPar();         // 0) Проверка подписи         bool fRes = UCryptoAPI.CryptVerifyDetachedMessageSignature(                                            ref pVerParam,                     // Параметры подтверждения                                            0,                                 // Индекс подписанта                                            _pSign,                            // Подпись                                            _pSign.Length,                     // Длина подписи                                            1,                                 // кол-во файлов на подпись                                            new IntPtr[1] { pHData },          // подписанные файлы                                            new int[1] { _arData.Length },     // Длины подписанных файлов                                            pCertContext.AddrOfPinnedObject());// Ссылка на сертификат         if (!fRes) {             _sError = UCConsts.S_SIGN_CHECK_ERR.Frm(Marshal.GetLastWin32Error().ToString("X"));             return UConsts.E_CRYPTO_ERR;         }         // 1) Извлечение сертфииката         _pCert = new ISDP_X509Cert((IntPtr)pCertContext.Target);         if (_pCert == null) {             _sError = UCConsts.S_SIGN_CHECK_CERT_ERR;             return UConsts.E_CRYPTO_ERR;         }         // 2) Проверка сертификата         if (!_fVerifyOnlySign) {             List<DateTime> pDates;             // 2.1) Получаем список дат             int iRes = GetSignDateTimeCP(_pSign, out pDates, ref  _sError);             // 2.2) Верифицируем первый сертификат             iRes = _pCert.ISDPVerify(ref _sError, pDates[0], _pRevMode, _pRevFlag);             if (iRes != UConsts.S_OK) return iRes;         }         return UConsts.S_OK;     } catch (Exception E) {         _sError = UCConsts.S_SIGN_CHECK_ERR.Frm(E.Message);         return UConsts.E_GEN_EXCEPTION;;     } finally {         Marshal.FreeHGlobal(pHData);         if ((_pCert == null) && pCertContext.IsAllocated && ((IntPtr)pCertContext.Target != IntPtr.Zero))             UCryptoAPI.CertFreeCertificateContext((IntPtr)pCertContext.Target);         pCertContext.Free();                     } } 

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

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

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

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

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

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

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

/**<summary>Расшифровать</summary> * <param name="_arSign">Подпись</param> * <param name="_sError">Возвращаемая строка с ошибкой</param> * <returns>Стандартный код ошибки, если UConsts.S_OK то все ок</returns> * **/ public int Decode(byte[] _arSign, ref string _sError) {     IntPtr hMsg = IntPtr.Zero;     // 0) Формируем информацию      try {         hMsg = UCryptoAPI.CryptMsgOpenToDecode(UCConsts.PKCS_7_OR_X509_ASN_ENCODING, UCConsts.CMSG_DETACHED_FLAG,                                                0, IntPtr.Zero, IntPtr.Zero, IntPtr.Zero);         if (hMsg == IntPtr.Zero) {             _sError = UCConsts.S_CRYP_MSG_FORM_ERR.Frm(Marshal.GetLastWin32Error());             return UConsts.E_CRYPTO_ERR;         }         // 1) Вносим сообщение         if (!UCryptoAPI.CryptMsgUpdate(hMsg, _arSign, (uint)_arSign.Length, true)) {             _sError = UCConsts.S_CRYP_MSG_SIGN_COPY_ERR.Frm(Marshal.GetLastWin32Error());             return UConsts.E_CRYPTO_ERR;         }         // 2) Проверяем тип (PKCS7 SignedData)         uint iMessType = UCUtils.GetCryptMsgParam<uint>(hMsg, UCConsts.CMSG_TYPE_PARAM);         if (UCConsts.CMSG_SIGNED != iMessType) {             _sError = UCConsts.S_CRYP_MSG_SIGN_TYPE_ERR.Frm(iMessType, UCConsts.CMSG_SIGNED);             return UConsts.E_CRYPTO_ERR;         }         // 3) Формируем список сертфикатов         fpCertificates = UCUtils.GetSignCertificates(hMsg);         // 4) Список подписантов                     uint iSignerCount =  UCUtils.GetCryptMsgParam<uint>(hMsg, UCConsts.CMSG_SIGNER_COUNT_PARAM);         for (int i = 0; i < iSignerCount; i++) {              ISDPSignerInfo pInfo = new ISDPSignerInfo();              fpSignerInfos.Add(pInfo);              int iRes = pInfo.Decode(hMsg, i, this, ref _sError);              if (iRes != UConsts.S_OK) return iRes;         }         return UConsts.S_OK;     } catch (Exception E) {         _sError = UCConsts.S_SIGN_INFO_GEN_ERR.Frm(E.Message);         return UConsts.E_GEN_EXCEPTION;     } finally {         if(hMsg != IntPtr.Zero) UCryptoAPI.CryptMsgClose(hMsg);     } } 

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

Получение списка сертификатов

/**<summary>Получить коллекцию сертификатов по подписи </summary> * <param name="_hMsg">Handle подписи</param> * <returns>Коллекция сертификатов</returns> * **/ internal static X509Certificate2Collection GetSignCertificates(IntPtr _hMsg) {     X509Certificate2Collection certificates = new X509Certificate2Collection();     uint iCnt = GetCryptMsgParam<uint>(_hMsg, UCConsts.CMSG_CERT_COUNT_PARAM);     for (int i = 0; i < iCnt; i++) {         IntPtr hInfo = IntPtr.Zero;         IntPtr hCert = IntPtr.Zero;         try {             uint iLen = 0;             if (!GetCryptMsgParam(_hMsg, UCConsts.CMSG_CERT_PARAM, out hInfo, out iLen)) continue;             hCert = UCryptoAPI.CertCreateCertificateContext(UCConsts.PKCS_7_OR_X509_ASN_ENCODING, hInfo, iLen);             if (hCert != IntPtr.Zero) {                 certificates.Add(new ISDP_X509Cert(hCert));                 hCert = IntPtr.Zero;             }         } finally {             if (hInfo != IntPtr.Zero) Marshal.FreeHGlobal(hInfo);             if (hInfo != IntPtr.Zero) Marshal.FreeHGlobal(hCert);         }     }     return certificates; } 

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

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

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

/**<summary>Распарсить информацию из подписи</summary> * <param name="_hMsg">Handler подписи</param> * <param name="_iIndex">Индекс подписанта</param> * <param name="_pSignedCms">Структура подписи</param> * <param name="_sError">Возвращаемая строка с ошибкой</param> * <returns>Стандартный код ошибки, если UConsts.S_OK то все ок</returns> * **/ public int Decode(IntPtr _hMsg, int _iIndex, ISDPSignedCms _pSignedCms, ref string _sError) {     // 1) Определяем длину     uint iLen = 0;     // 2) Считываем     IntPtr hInfo =  IntPtr.Zero;     try {         if (!UCryptoAPI.CryptMsgGetParam(_hMsg, UCConsts.CMSG_SIGNER_INFO_PARAM, (uint)_iIndex, IntPtr.Zero, ref iLen)) {             _sError = UCConsts.S_ERR_SIGNER_INFO_LEN.Frm(_iIndex, Marshal.GetLastWin32Error());             return UConsts.E_CRYPTO_ERR;         }         hInfo = Marshal.AllocHGlobal((int)iLen);         if (!UCryptoAPI.CryptMsgGetParam(_hMsg, UCConsts.CMSG_SIGNER_INFO_PARAM, (uint)_iIndex, hInfo, ref iLen)) {             _sError = UCConsts.S_ERR_SIGNER_INFO.Frm(_iIndex, Marshal.GetLastWin32Error());             return UConsts.E_CRYPTO_ERR;         }         CMSG_SIGNER_INFO pSignerInfo = (CMSG_SIGNER_INFO) Marshal.PtrToStructure(hInfo, typeof(CMSG_SIGNER_INFO));         // 2.1) Ищем сертификат         byte[] arSerial = new byte[pSignerInfo.SerialNumber.cbData];         Marshal.Copy(pSignerInfo.SerialNumber.pbData, arSerial, 0, arSerial.Length);         X509Certificate2Collection pLocCerts = _pSignedCms.pCertificates.Find(X509FindType.FindBySerialNumber,                                                                               arSerial.Reverse().ToArray().ToHex(), false);         if (pLocCerts.Count != 1) {             _sError = UCConsts.S_ERR_SIGNER_INFO_CERT.Frm(_iIndex);             return UConsts.E_NO_CERTIFICATE;         }         fpCertificate = pLocCerts[0];         fpSignedAttributes = UCUtils.ReadCryptoAttrsCollection(pSignerInfo.AuthAttrs);         return UConsts.S_OK;     } catch (Exception E) {         _sError = UCConsts.S_ERR_SIGNER_INFO_READ.Frm(_iIndex, E.Message);          return UConsts.E_GEN_EXCEPTION;                 } finally {          if(hInfo != IntPtr.Zero) Marshal.FreeHGlobal(hInfo);     } } 

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

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

Список атрибутов подписи

/**<summary>Получить список атрибутов подписи</summary> * <param name="_pAttrs">Структура атрибутов</param> * <returns>Коллекция аттрибутов</returns> * **/ internal static CryptographicAttributeObjectCollection ReadCryptoAttrsCollection(CRYPT_ATTRIBUTES _pAttrs) {     CryptographicAttributeObjectCollection pRes = new CryptographicAttributeObjectCollection();                 for (int i = 0; i < _pAttrs.cAttr; i++) {         IntPtr hAttr = new IntPtr((long)_pAttrs.rgAttr + (i * Marshal.SizeOf(typeof(CRYPT_ATTRIBUTE))));         CRYPT_ATTRIBUTE pAttr = (CRYPT_ATTRIBUTE) Marshal.PtrToStructure(hAttr, typeof(CRYPT_ATTRIBUTE));         CryptographicAttributeObject pAttrInfo =  new CryptographicAttributeObject(new Oid(pAttr.pszObjId),                                                                                     GetAsnEncodedDataCollection(pAttr));         pRes.Add(pAttrInfo);     }     return pRes; } 

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

Разобрать атрибут подписи

/**<summary>Сформировать объект коллекции нужного класса по имени</summary> * <param name="_sName">Имя</param> * <returns>Созданный объект</returns> * **/ internal static Pkcs9AttributeObject Pkcs9AttributeFromOID(string _sName) {     switch (_sName) {         case UCConsts.S_SIGN_DATE_OID    : return new Pkcs9SigningTime();            //        case UConsts.S_CONTENT_TYPE_OID : return new Pkcs9ContentType();      ->> в Mono падает                           //        case UConsts.S_MESS_DIGEST_OID  : return new Pkcs9MessageDigest();         default:  return new Pkcs9AttributeObject();     }         }  /**<summary>Формирует коллекуцию ASN</summary> * <param name="_pAttr">Структура</param> * <returns>Коллекция</returns> * **/ internal static AsnEncodedDataCollection GetAsnEncodedDataCollection (CRYPT_ATTRIBUTE _pAttr) {     AsnEncodedDataCollection pRes = new AsnEncodedDataCollection();     Oid pOid = new Oid(_pAttr.pszObjId);     string sOid = pOid.Value;     for (uint i = 0; i < _pAttr.cValue; i++) {         checked {             IntPtr pAttributeBlob = new IntPtr((long)_pAttr.rgValue + (i * Marshal.SizeOf(typeof(CRYPTOAPI_BLOB))));             Pkcs9AttributeObject attribute = new Pkcs9AttributeObject(pOid, BlobToByteArray(pAttributeBlob));             Pkcs9AttributeObject customAttribute = Pkcs9AttributeFromOID(sOid);             if (customAttribute != null) {                 customAttribute.CopyFrom(attribute);                 attribute = customAttribute;             }             pRes.Add(attribute);         }     }     return pRes; } 

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

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

Шифрование

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

Зашифровать данные

/**<summary>Зашифрованные данные</summary> * <param name="_arInput">Данные для расшифровки</param> * <param name="_pCert">Сертификат</param> * <param name="_arRes">Результат</param> * <param name="_sError">Возвращаемая строка с ошибкой</param> * <returns>Стандартный код с ошибкой, если UConsts.S_OK то все ок</returns> * **/ public static int EncryptDataCP(byte[] _arInput, X509Certificate2 _pCert, out byte[] _arRes, ref string _sError) {     _arRes = new byte[0];     try {         // 0) Инициализация параметров         CRYPT_ENCRYPT_MESSAGE_PARA  pParams         = new CRYPT_ENCRYPT_MESSAGE_PARA();         pParams.dwMsgEncodingType                   = UCConsts.PKCS_7_OR_X509_ASN_ENCODING;         pParams.ContentEncryptionAlgorithm.pszObjId = _pCert.getEncodeAlgirtmOid();         pParams.cbSize = Marshal.SizeOf(pParams);         // 1) Извлечение длины         int iLen = 0;         if (!UCryptoAPI.CryptEncryptMessage(ref pParams, 1, new IntPtr[] { _pCert.getRealHandle() },                                             _arInput, _arInput.Length, null, ref iLen)) {             _sError = UCConsts.S_CRYPT_ENCODE_LEN_ERR.Frm(Marshal.GetLastWin32Error());             return UConsts.E_CRYPTO_ERR;         }         // 2) Второй запрос реальное шифрование         _arRes = new byte[iLen];         if (!UCryptoAPI.CryptEncryptMessage(ref pParams, 1, new IntPtr[] {_pCert.getRealHandle() },                                            _arInput, _arInput.Length, _arRes, ref iLen)) {               _sError = UCConsts.S_CRYPT_ENCODE_ERR.Frm(Marshal.GetLastWin32Error());               return UConsts.E_CRYPTO_ERR;         }         return UConsts.S_OK;     } catch (Exception E) {         _sError = UCConsts.S_CRYPT_ENCODE_ERR.Frm(E.Message);         return UConsts.E_GEN_EXCEPTION;     } } 

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

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

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

Получение алгоритма шифрования

/**<summary>Получение OID алгоритма шифрования сертификату</summary> * <param name="_hCertHandle">Хэндл сертификата</param> * <param name="_sOID">Возвращаемый параметр OID</param> * <param name="_sError">Возвращаемая строка с ошибкой</param> * <returns>Стандартный код ошибки, если UConsts.S_OK то все ок</returns> * **/ internal static int GetEncodeAlgoritmOID(IntPtr _hCertHandle, out string _sOID, ref string _sError) {     bool fNeedRelease = false;     _sOID = "";     uint iKeySpec = 0;     IntPtr  hCrypto = IntPtr.Zero;     try {         // 0) Получаем контекст провайдера         if (!UCryptoAPI.CryptAcquireCertificatePrivateKey(_hCertHandle, 0, IntPtr.Zero,                                                           ref hCrypto, ref iKeySpec, ref fNeedRelease)) {             _sError = UCConsts.S_CRYPTO_PROV_INIT_ERR.Frm(Marshal.GetLastWin32Error());             return UConsts.E_CRYPTO_ERR;         }          uint iLen = 1000;         byte[] arData = new byte[1000];         uint iFlag = 1; // Инициализация         // 1) Проходим в цикле по алгоритмам         while (UCryptoAPI.CryptGetProvParam(hCrypto, UCConsts.PP_ENUMALGS, arData, ref iLen, iFlag)){             iFlag = 2; // Следующий             PROV_ENUMALGS pInfo = ConvertBytesToStruct<PROV_ENUMALGS>(arData);             // 2) Пытаемся получить OID  в рамках алгоримтов шифрования             byte[]  arDataAlg = BitConverter.GetBytes(pInfo.aiAlgid);             IntPtr hDataAlg = Marshal.AllocHGlobal(arDataAlg.Length);             try {                 Marshal.Copy(arDataAlg, 0, hDataAlg, arDataAlg.Length);                 IntPtr hHashAlgInfo2 = UCryptoAPI.CryptFindOIDInfo(UCConsts.CRYPT_OID_INFO_ALGID_KEY,                                                                    hDataAlg,                                                                    UCConsts.CRYPT_ENCRYPT_ALG_OID_GROUP_ID);                 // 2.1) Нашли - возвращаем                 if (hHashAlgInfo2 != IntPtr.Zero) {                     CRYPT_OID_INFO pHashAlgInfo2 = (CRYPT_OID_INFO)Marshal.PtrToStructure(hHashAlgInfo2,                                                                                           typeof(CRYPT_OID_INFO));                     _sOID = pHashAlgInfo2.pszOID ;                     return UConsts.S_OK;                 }             } finally {                  Marshal.FreeHGlobal(hDataAlg);             }         }         // 3) Не нашли - ошибка         _sError = UCConsts.S_NO_ENCODE_ALG_ERR;         return UConsts.E_CRYPTO_ERR;     } catch (Exception E) {          _sError = UCConsts.S_DETERM_ENCODE_ALG_ERR.Frm(E.Message);         return UConsts.E_GEN_EXCEPTION;     }finally {         if((hCrypto != IntPtr.Zero) && fNeedRelease) UCryptoAPI.CryptReleaseContext(hCrypto, 0);     } } 

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

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

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

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

Дешифрование данных

/**<summary>Дешифровывает данные</summary> * <param name="_arInput">Данные для расшифровки</param> * <param name="_arRes">Результат</param> * <param name="_sError">Возвращаемая строка с ошибкой</param> * <param name="_pCert">Сертификат</param> * <returns>Стандартный код ошибки, если UCOnsts.S_OK то все ок</returns> * **/ public static int DecryptDataCP(byte[] _arInput, out X509Certificate2 _pCert, out byte[] _arRes, ref string _sError) {                  _arRes = new byte[0];     _pCert = null;     IntPtr hSysStore =  UCryptoAPI.CertOpenSystemStore(IntPtr.Zero, UCConsts.AR_CRYPTO_STORE_NAME[(int)StoreName.My]);     GCHandle GC = GCHandle.Alloc(hSysStore, GCHandleType.Pinned);     IntPtr hOutCertL = IntPtr.Zero;     IntPtr hOutCert  = IntPtr.Zero;     try {         // 0) Подготовка параметров         CRYPT_DECRYPT_MESSAGE_PARA pParams = new CRYPT_DECRYPT_MESSAGE_PARA();         pParams.dwMsgAndCertEncodingType = UCConsts.PKCS_7_OR_X509_ASN_ENCODING;         pParams.cCertStore = 1;         pParams.rghCertStore = GC.AddrOfPinnedObject();         pParams.cbSize = Marshal.SizeOf(pParams);         int iLen = 0;         // 1) Первый вызов определяем длину          if (!UCryptoAPI.CryptDecryptMessage(ref pParams, _arInput, _arInput.Length,                                             null, ref iLen, ref hOutCertL)) {              _sError = UCConsts.S_DECRYPT_LEN_ERR.Frm(Marshal.GetLastWin32Error());             return UConsts.E_CRYPTO_ERR;         }         // 2) Второй  вызов дешифруем         _arRes = new byte[iLen];         if (!UCryptoAPI.CryptDecryptMessage(ref pParams, _arInput, _arInput.Length,                                            _arRes, ref iLen, ref hOutCert)) {             _sError = UCConsts.S_DECRYPT_ERR.Frm(Marshal.GetLastWin32Error());             return UConsts.E_CRYPTO_ERR;         }         // 3) Если есть вытаскиваем сертификат         if (hOutCert != IntPtr.Zero) _pCert = new ISDP_X509Cert(hOutCert);         if(_pCert != null) hOutCert = IntPtr.Zero;         // Все ок возвращаем         return UConsts.S_OK;     } catch (Exception E) {          _sError = UCConsts.S_DECRYPT_ERR.Frm(E.Message);         return UConsts.E_GEN_EXCEPTION;     } finally {         if (hOutCertL != IntPtr.Zero) UCryptoAPI.CertFreeCertificateContext(hOutCertL);         if (hOutCert != IntPtr.Zero) UCryptoAPI.CertFreeCertificateContext(hOutCert);         GC.Free();         UCryptoAPI.CertCloseStore(hSysStore, 0);     } } 

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

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

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

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

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

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

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

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

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

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

/**<summary>Проверить сертификат</summary> * <param name="_iRevFlag">Флаг отзыва</param> * <param name="_iRevMode">Режим отзыва</param> * <param name="_hPolicy">Ссылка на правила проверки</param> * <param name="_hCert">контекст сертфиката</param> * <param name="_iCTLTimeout">таймаут запроса списка отзыва</param> * <param name="_rOnDate">Дата верификацмм</param> * <param name="_sError">Возвращаемая строка с ошибкой</param> * <returns>Стандартый код ошибки, если UConsts.S_OK то все ок</returns> * **/ internal static int VerifyCertificate (IntPtr _hCert, X509RevocationMode _iRevMode, X509RevocationFlag _iRevFlag,                                         DateTime _rOnDate, TimeSpan _iCTLTimeout, IntPtr _hPolicy, ref string _sError) {      if (_hCert == IntPtr.Zero) {         _sError = UCConsts.S_CRYPTO_CERT_CHECK_ERR;         return UConsts.E_NO_CERTIFICATE;     }     CERT_CHAIN_POLICY_PARA   pPolicyParam  = new CERT_CHAIN_POLICY_PARA(Marshal.SizeOf(typeof(CERT_CHAIN_POLICY_PARA)));     CERT_CHAIN_POLICY_STATUS pPolicyStatus = new CERT_CHAIN_POLICY_STATUS(Marshal.SizeOf(typeof(CERT_CHAIN_POLICY_STATUS)));      // 1) Формируем цепочку     IntPtr hChain = IntPtr.Zero;     try {         int iRes = BuildChain(new IntPtr(UCConsts.HCCE_CURRENT_USER), _hCert, __iRevMode, _iRevFlag,                              _rOnDate, _iCTLTimeout, ref hChain, ref _sError);         if (iRes != UConsts.S_OK) return iRes;         // 2) Проверяем цепочку         if (UCryptoAPI.CertVerifyCertificateChainPolicy(_hPolicy, hChain, ref pPolicyParam, ref pPolicyStatus)) {             if (pPolicyStatus.dwError != 0) {                 _sError = UCConsts.S_CRYPTO_CHAIN_CHECK_ERR.Frm(pPolicyStatus.dwError);                 return UConsts.E_CRYPTO_ERR;             }          } else{             _sError = UCConsts.S_CRYPTO_CHAIN_CHECK_ERR.Frm(Marshal.GetLastWin32Error());             return UConsts.E_CRYPTO_ERR;         }         return UConsts.S_OK;     } catch (Exception E) {         _sError = UCConsts.S_CRYPTO_CERT_VERIFY_GEN_ERR.Frm(E.Message);         return UConsts.E_GEN_EXCEPTION;     } finally {         if(hChain != IntPtr.Zero) UCryptoAPI.CertFreeCertificateChain(hChain);     } } 

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

Формирование цепочки сертификата

/**<summary>Формирует цепочку сертфикиата для проверки</summary> * <param name="_hChain">КОнтекст цепочки сертфиикатов</param> * <param name="_iRevFlag">Флаг отзыва</param> * <param name="_iRevMode">Режим отзыва</param> * <param name="_hChainEngine">Тип хранилища</param> * <param name="_hCert">контекст сертфиката</param> * <param name="_rCTLTimeOut">таймаут запроса списка отзыва</param> * <param name="_rOnDate">Дата верификацмм</param> * <param name="_sError">Возвращаемая строка с ошибкой</param> * <returns>Стандартый код ошибки, если UConsts.S_OK то все ок</returns> * **/ internal static int BuildChain (IntPtr _hChainEngine, IntPtr _hCert, X509RevocationMode _iRevMode,                                  X509RevocationFlag _iRevFlag, DateTime _rOnDate, TimeSpan _rCTLTimeOut,                                  ref IntPtr _hChain, ref string _sError) {     // 0) Проверка наличия сертификата     if (_hCert == IntPtr.Zero) {         _sError = UCConsts.S_CRYPTO_CERT_CHAIN_ERR;         return UConsts.E_NO_CERTIFICATE;     }     // 1) Параметры     CERT_CHAIN_PARA pChainParams = new CERT_CHAIN_PARA();     pChainParams.cbSize = (uint) Marshal.SizeOf(pChainParams);      IntPtr hAppPolicy = IntPtr.Zero;     IntPtr hCertPolicy = IntPtr.Zero;     try {         // 2) Формируем правила приложения         pChainParams.dwUrlRetrievalTimeout = (uint)Math.Floor(_rCTLTimeOut.TotalMilliseconds);         // 3) Время проверки         FILETIME pVerifyTime = new FILETIME(_rOnDate.ToFileTime());         // 4) Формируем флаг         uint _iFlags = MapRevocationFlags(_iRevMode, _iRevFlag);         // 5) Формирование цепочки         if (!UCryptoAPI.CertGetCertificateChain(_hChainEngine, _hCert, ref pVerifyTime,                                                 IntPtr.Zero, ref pChainParams, _iFlags,                                                 IntPtr.Zero, ref _hChain)) {             _sError = UCConsts.S_CRYPTO_CHAIN_BUILD_ERR.Frm(Marshal.GetLastWin32Error());             return UConsts.E_CRYPTO_ERR;         }     } catch(Exception E) {          _sError = UCConsts.S_CRYPTO_CHAIN_GEN_ERR.Frm(E.Message);         return UConsts.E_GEN_EXCEPTION;     } finally {         Marshal.FreeHGlobal(hAppPolicy);         Marshal.FreeHGlobal(hCertPolicy);     }     return UConsts.S_OK; } 

Это сильно упрощенный вариант формирования цепочки по сравнению с тем, как ее формирует 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 сертификата.

Ссылки

  1. документация КриптоПро CAPILite
  2. ресурс c объявлением стандартных экспортируемых функций в С#
  3. исходники .Net:
    1. класс CAPIBase
    2. класс X509Certificate2
    3. класс SignedCMS
    4. класс SignerInfo

  4. исходники mono:
    1. класс X509Certificate2
    2. класс X509CertificateImplBtls


ссылка на оригинал статьи https://habr.com/post/423163/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *