На linux-сервере для работы с КриптоПро можно использовать Криптопро

На linux-сервере для работы с КриптоПро можно использовать Криптопро Электронная цифровая подпись

Добавление подписи ЭЦП в файл (в нем должны быть уже другие подписи)

cosign.sh
 
# Добавление подписи ЭЦП в файл (в нем должны быть уже другие подписи)
# Вход: файл на STDIN, `SHA1 HASH` и `PIN` как параметры
# Выход: результаты выполнения предназначены для парсинга
# Для удаленного вызова необходимо настроить на сервере авторизацию по ключу
# Для текущего пользователя (sign) необходимо добавить сертификаты,
# а также внести информацию по ним в БД (для работы всей схемы)
 
=
=
=
 
 -  
 
 
 .lib.sh
 
# Подписи добавляются без создания нового файла
  CRYPTCP BEGIN 
timeout 10s homesigninterfaceexpect-cosign.sh   
 
 
=
  CRYPTCP END 
 
     ; 
 
  certmgr_list
       ; 
    certmgr_review
  
 

 
 
  SUMMARY BEGIN 
 CRYPTCP CODE: 
 FILE: 
  SUMMARY END 
expect-cosign.sh
 

# Обход проблемы с интерактивным вводом пароля для подписания

 
 timeout 
 
 thumbprint lindex  
 password lindex  
 src_file lindex  
 
spawn cryptcp     
 
expect   send  
 
expect 
      
  "(o) OK, (c) Cancel" 
    send 
    exp_continue
  
  eof    

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

Несмотря на процесс переноса в 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). После чего проверяется сама подпись и извлекается первый подписант (по хорошему надо было бы извлечь всех, но подпись содержащая несколько подписантов это все таки экзотика).

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

Файл header. key

Из этого файла нам потребуется параметры электронной подписи CryptoProParamSet (подчеркнуто красным).

  • GostR3410_2001_CryptoPro_A_ParamSet — 1.2.643.2.2.35.1
  • GostR3410_2001_CryptoPro_B_ParamSet — 1.2.643.2.2.35.2
  • GostR3410_2001_CryptoPro_C_ParamSet — 1.2.643.2.2.35.3
  • GostR3410_2001_CryptoPro_XchA_ParamSet — 1.2.643.2.2.36.0
  • GostR3410_2001_CryptoPro_XchB_ParamSet — 1.2.643.2.2.36.1

А также первые 8 байт открытого ключа (подчеркнуто) для контроля правильности чтения закрытого.

header.key

Сертификаты

Список установленных сертификатов

certmgr -list, например:

1-------
Issuer            : [email protected], C=RU, L=Moscow, O=CRYPTO-PRO LLC, CN=CRYPTO-PRO Test Center 2
Subject           : CN=test2
Serial            : 0x120007E4E683979B734018897B00000007E4E6
SHA1 Hash         : 0x71b59d165ab5ea39e4cd73384f8e7d1e0c965e81
Not valid before  : 07/09/2015  10:41:18 UTC
Not valid after   : 07/12/2015  10:51:18 UTC
PrivateKey Link   : Yes. Container  : HDIMAGE\\test2.000\F9C9
2-------
Issuer            : [email protected], C=RU, L=Moscow, O=CRYPTO-PRO LLC, CN=CRYPTO-PRO Test Center 2
Subject           : CN=webservertest
Serial            : 0x120007E47F1FD9AE0EDE78616600000007E47F
SHA1 Hash         : 0x255c249150efe3e48f1abb3bc1928fc8f99980c4
Not valid before  : 07/09/2015  09:56:10 UTC
Not valid after   : 07/12/2015  10:06:10 UTC
PrivateKey Link   : Yes. Container  : HDIMAGE\\webserve.001\2608

Добавление реального сертификата

Добавить только сертификат (только проверка ЭЦП):

certmgr   cert.cer

Добавление реального сертификата с привязкой к закрытому ключу и возможностью подписывать документы

Закрытый ключ состоит из шести key-файлов:

header.key
masks2.key
masks.key
name.key
primary2.key
primary.key

Способ с дискетой или флешкой

Скопировать в корень дискеты или флэшки сертификат и приватный ключ (из каталога 999996.000, 999996 – название (alias) контейнера):

  pathtokey mediaflashdrive
 pathtocertclient.cer mediaflashdrive

Выполнить команду по копированию ключа с флэшки на диск, ключ попадет в пользовательское хранилище My.

Необходимо выполнять под пользователем, который будет использовать данный контейнер для подписи.

[email protected] – то, что прописано в поле E сертификата (можно посмотреть командой keytool --printcert -file /path/to/cert/client.cer):

csptest     

С жесткого диска

«Ручной способ».

Скопировать приватный ключ в хранилище (контейнер), где <username> – имя пользователя linux:

  pathtokey varoptcprocspkeysusername

Поставить «минимальные» права:

  varoptcprocspkeysusername

Узнать реальное название контейнера:

csptest  -enum_cont  

Ассоциировать сертификат с контейнером, сертификат попадет в пользовательское хранилище My:

certmgr   pathtofileclient.cer  

Если следующая ошибка, нужно узнать реальное название контейнера (см. выше):

Failed to open container \\.\HDIMAGE\<container>
[ErrorCode: 0x00000002]

Установить сертификат УЦ из-под пользователя root командой:

certmgr   uroot  pathtofileCA.cer

Проверка успешности установки закрытого ключа

certmgr 

PrivateKey Link

Если выводится PrivateKey Link: Yes. Container: HDIMAGE\\999996.000\D7BB, то есть и сертификат, и приватный ключ, а если выводится PrivateKey Link: No – связи нет, и использовать такой контейнер для подписи не удастся.

Источник

Добавление тестового сертификата

Добавление работает только на той же машине, и в тот же контейнер, где был сформированы следующий запрос на добавление:

cryptcp      test.csr

Ввести пароль на контейнер test123.

cryptcp      myname.csr

Пароль mysecurepass

Откройте в браузере ссылку тестовый удостоверяющий центр КриптоПро

cryptcp    certnew.cer

Ввести пароль на контейнер. По-умолчанию: 12345678

Удаление сертификата

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

certmgr   file.sig

Ответ:

1-------
Issuer            : [email protected], C=RU, L=Москва, O=ООО КРИПТО-ПРО, CN=УЦ KPИПTO-ПPO
Subject           : [email protected], C=RU, L=г. Москва, O="ООО ""Верес""", OU=Руководство, CN=Иванов Иван Иванович, T=Генеральный директор
Serial            : 0x75F5C86A000D00016A5F
SHA1 Hash         : 0x255c249150efe3e48f1abb3bc1928fc8f99980c4
Not valid before  : 08/12/2014  09:04:00 UTC
Not valid after   : 08/12/2019  09:14:00 UTC
PrivateKey Link   : No

Подписание пустого файла (размер 0) проходит успешно, но при просмотре сертификатов этого файла выдается ошибка:

Can't open certificate store: '/tmp/tmp.G8cd13vzfZ.sig'.
Error: No certificate found.
/dailybuilds/CSPbuild/CSP/samples/CPCrypt/Certs.cpp:312: 0x2000012D
[ErrorCode: 0x2000012d]

Будьте внимательны!

Просмотр всех атрибутов сертификата

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

Получаем SHA 1 хеши:

certmgr -list -f file.sig | grep 'SHA1 Hash'

В цикле извлекаем сертификаты:

cryptcp -nochain -copycert -thumbprint 255c249150efe3e48f1abb3bc1928fc8f99980c4 -f file.sig -df certificate.der -der
openssl x509 -in certificate.der -inform der -text -noout

Настройка openssl для поддержки ГОСТ:

В файл /etc/ssl/openssl.cnf

 openssl_def # Это в начало файла
#Все что ниже в конец

 
 

 
 

 
 /usr/lib/ssl/engines/libgost.so # заменить реальным файлом
 
 

Проверка:

openssl ciphers        gost
GOST2001-GOST89-GOST89
GOST94-GOST89-GOST89

Экспорт сертификатов на другую машину

Закрытые ключи к сертификатам находятся тут: /var/opt/cprocsp/keys. Поэтому эти ключи переносятся просто: создаем архив и переносим на нужную машину в тот же каталог.

Экспорт самих сертификатов (если их 14):

 i    ;     certmgr   .cer; 

Переносим эти файлы на машину и смотрим, какие контейнеры есть:

csptest -keyset -enum_cont -verifycontext -fqcn

И как обычно, связываем сертификат и закрытый ключ:

certmgr -inst -file 1.cer -cont '\\.\HDIMAGE\container.name'

Если закрытый ключ и сертификат не подходят друг к другу, будет выведена ошибка:

Can not install certificate
Public keys in certificate and container are not identical

Если все успешно:

На linux-сервере для работы с КриптоПро можно использовать Криптопро

Если нет закрытого ключа, то просто ставим сертификат:

certmgr -inst -file 1.cer

Читаем закрытый ключ и конвертируем

Файл privkey.c

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#include <openssl/pem.h>
#include <openssl/cms.h>
#include <openssl/err.h>
#include "gost_lcl.h"

/* Convert little-endian byte array into bignum */
BIGNUM *reverse32bn(char *b, BN_CTX *ctx)
{
	BIGNUM *res;
	char buf[32];
	BUF_reverse(buf, b, 32);
	res = BN_bin2bn(buf, 32, BN_CTX_get(ctx));
	OPENSSL_cleanse(buf, sizeof(buf));
	return res;
}

void xor_material(char *buf36, char *buf5C, char *src)
{
	int i;
	for(i = 0; i < 32; i++)
	{
		buf36[i] = src[i] ^ 0x36;
		buf5C[i] = src[i] ^ 0x5C;
	}
}

int make_pwd_key(char *result_key, char *start12, int start12_len, char *passw)
{
	int result;
	int i;
	char pincode4[1024];
	int pin_len;
	char current[32];
	char material36[32];
	char material5C[32];
	char hash_result[32];
	gost_hash_ctx ctx;
	init_gost_hash_ctx(&ctx, &GostR3411_94_CryptoProParamSet);
	memset(pincode4, 0, sizeof(pincode4));
	pin_len = strlen(passw);
	if (pin_len*4 > sizeof(pincode4)) {	result = 1;	goto err; }
	for(i = 0; i < pin_len; i++)
		pincode4[i*4] = passw[i];

	start_hash(&ctx);
	hash_block(&ctx, start12, start12_len);
	if (pin_len) 
		hash_block(&ctx, pincode4, pin_len * 4);
	finish_hash(&ctx, hash_result);

	memcpy(current, (char*)"DENEFH028.760246785.IUEFHWUIO.EF", 32);

	for(i = 0; i < (pin_len?2000:2); i++)
	{
		xor_material(material36, material5C, current);
		start_hash(&ctx);
		hash_block(&ctx, material36, 32);
		hash_block(&ctx, hash_result, 32);
		hash_block(&ctx, material5C, 32);
		hash_block(&ctx, hash_result, 32);
		finish_hash(&ctx, current);
	}

	xor_material(material36, material5C, current);

	start_hash(&ctx);
	hash_block(&ctx, material36, 32);
	hash_block(&ctx, start12, start12_len);
	hash_block(&ctx, material5C, 32);
	if (pin_len) 
		hash_block(&ctx, pincode4, pin_len * 4);
	finish_hash(&ctx, current);

	start_hash(&ctx);
	hash_block(&ctx, current, 32);
	finish_hash(&ctx, result_key);

	result = 0; //ok
err:
	return result;
}

BIGNUM *decode_primary_key(char *pwd_key, char *primary_key, BN_CTX *bn_ctx)
{
	BIGNUM *res;
	char buf[32];
	gost_ctx ctx;
	gost_init(&ctx, gost_cipher_list->sblock);
	gost_key(&ctx, pwd_key);
	gost_dec(&ctx, primary_key, buf, 4);
	res = reverse32bn(buf, bn_ctx);
	OPENSSL_cleanse(buf, sizeof(buf));
	return res;
}

BIGNUM *remove_mask_and_check_public(char *oid_param_set8, BIGNUM *key_with_mask, BIGNUM *mask, char *public8, BN_CTX *ctx)
{
	int result;
	EC_KEY *eckey = NULL;
	const EC_POINT *pubkey;
	const EC_GROUP *group;
	BIGNUM *X, *Y, *order, *raw_secret, *mask_inv;
	char outbuf[32], public_X[32];
	ASN1_OBJECT *obj;
	int nid;

	order = BN_CTX_get(ctx);
	mask_inv = BN_CTX_get(ctx);
	raw_secret = BN_CTX_get(ctx);
	X = BN_CTX_get(ctx);
	Y = BN_CTX_get(ctx);
	if (!order || !mask_inv || !raw_secret || !X || !Y) { result = 1; goto err; }

	obj = ASN1_OBJECT_create(0, oid_param_set8+1, *oid_param_set8, NULL, NULL);
	nid = OBJ_obj2nid(obj);
	ASN1_OBJECT_free(obj);

	if (!(eckey = EC_KEY_new())) { result = 1; goto err; }
	if (!fill_GOST2001_params(eckey, nid)) { result = 1; goto err; }
	if (!(group = EC_KEY_get0_group(eckey))) { result = 1; goto err; }
	if (!EC_GROUP_get_order(group, order, ctx)) { result = 1; goto err; }

	if (!BN_mod_inverse(mask_inv, mask, order, ctx)) { result = 1; goto err; }
	if (!BN_mod_mul(raw_secret, key_with_mask, mask_inv, order, ctx)) { result = 1; goto err; }

	if (!EC_KEY_set_private_key(eckey, raw_secret)) { result = 1; goto err; }
	if (!gost2001_compute_public(eckey)) { result = 1; goto err; }
	if (!(pubkey = EC_KEY_get0_public_key(eckey))) { result = 1; goto err; }
	if (!EC_POINT_get_affine_coordinates_GFp(group, pubkey, X, Y, ctx)) { result = 1; goto err; }

	store_bignum(X, outbuf, sizeof(outbuf));
	BUF_reverse(public_X, outbuf, sizeof(outbuf));
	if (memcmp(public_X, public8, 8) != 0) { result = 1; goto err; }

	result = 0; //ok
err:
	if (eckey) EC_KEY_free(eckey);
	if (result == 0) return raw_secret;
	return NULL;
}

int file_length(char *fname)
{
	int len;
	FILE *f = fopen(fname, "rb");
	if (f == NULL) return -1;
	fseek(f, 0, SEEK_END);
	len = ftell(f);
	fclose(f);
	return len;
}

int read_file(char *fname, int start_pos, char *buf, int len)
{
	int read_len;
	FILE *f = fopen(fname, "rb");
	if (f == NULL) return 1;
	if (start_pos) fseek(f, start_pos, SEEK_SET);
	read_len = fread(buf, 1, len, f);
	fclose(f);
	if (read_len != len) return 1;
	return 0; //ok
}

int get_asn1_len(unsigned char *buf, int *size_hdr)
{
	int n, i, res;
	int pos = 0;
	if ((buf[pos]&0x80) == 0) {
		*size_hdr = 1;
		return buf[pos];
	}
	n = buf[pos++]&0x7f;
	res = 0;
	for(i = 0; i < n; i++) {
		res = res*256 + buf[pos++];
	}
	*size_hdr = n+1;
	return res;
}

#define MAX_HEADER 20000
int read_container(char *fpath, int flag2, char *salt12, char *primary_key, char *masks_key, char *public8, char *oid_param_set8)
{
	int result;
	char primary_path[1024+30];
	char masks_path[1024+30];
	char header_path[1024+30];
	char header_buf[MAX_HEADER];
	int header_len;
	int i, len, pos, size_hdr;

	if (strlen(fpath)>1024) { result = 1; goto err; }

	sprintf(header_path, "%s/header.key", fpath);
	if (flag2 == 0)
	{
		sprintf(primary_path, "%s/primary.key", fpath);
		sprintf(masks_path, "%s/masks.key", fpath);
	}
	else
	{
		sprintf(primary_path, "%s/primary2.key", fpath);
		sprintf(masks_path, "%s/masks2.key", fpath);
	}

	if (read_file(primary_path, 4, primary_key, 32)) { result = 1; goto err; }
	if (read_file(masks_path, 4, masks_key, 32)) { result = 1; goto err; }
	if (read_file(masks_path, 0x26, salt12, 12)) { result = 1; goto err; }

	header_len = file_length(header_path);
	if (header_len < 0x42 || header_len > MAX_HEADER) { result = 1; goto err; }
	if (read_file(header_path, 0, header_buf, header_len)) { result = 1; goto err; }

//------------- skip certificate ---------------------------
	pos = 0;
	for(i = 0; i < 2; i++)
	{
		get_asn1_len(header_buf+pos+1, &size_hdr);
		pos += size_hdr+1;
		if (pos > header_len-8) { result = 2; goto err; }
	}

//------------------ get oid_param_set8 -----------------------
#define PARAM_SET_POS 34
	if (memcmp(header_buf+pos+PARAM_SET_POS, "\x6\x7", 2) != 0) { result = 2; goto err; }
	memcpy(oid_param_set8, header_buf+pos+PARAM_SET_POS+1, 8);

//------------------ get public8 -----------------------
	result = 2; //not found
	pos += 52;
	for(i = 0; i < 3; i++)
	{
		len = get_asn1_len(header_buf+pos+1, &size_hdr);
		if (len == 8 && memcmp(header_buf+pos, "\x8a\x8", 2) == 0)
		{
			memcpy(public8,header_buf+pos+2,8);
			result = 0; //ok
			break;
		}
		pos += len+size_hdr+1;
		if (pos > header_len-8) { result = 2; goto err; }
	}
err:
	OPENSSL_cleanse(header_buf, sizeof(header_buf));
	return result;
}

#define START_OID 0x12
#define START_KEY 0x28
unsigned char asn1_private_key[72] = {
	0x30,0x46,2,1,0,0x30,0x1c,6,6,0x2a,0x85,3,2,2,0x13,0x30,0x12,6,7,0x11,
	0x11,0x11,0x11,0x11,0x11,0x11,6,7,0x2a,0x85,3,2,2,0x1e,1,4,0x23,2,0x21,0
};

int main(int argc, char **argv)
{
	int result;
	char *container_path;
	char *passw;
	char salt12[12];
	char primary_key[32];
	char masks_key[32];
	char public8[8];
	char oid_param_set8[8];
	BN_CTX *ctx;
	BIGNUM *key_with_mask;
	BIGNUM *mask;
	BIGNUM *raw_key;
	char pwd_key[32];
	char outbuf[32];

	ctx = BN_CTX_new();

	if (argc == 2)
	{
		container_path = argv[1];
		passw = "";
	}
	else
	if (argc == 3)
	{
		container_path = argv[1];
		passw = argv[2];
	}
	else
	{
		printf("get_private container_path [passw]\n");
		result = 1;
		goto err;
	}

	if (read_container(container_path, 0, salt12, primary_key, masks_key, public8, oid_param_set8) != 0 &&
		read_container(container_path, 1, salt12, primary_key, masks_key, public8, oid_param_set8) != 0)
	{
		printf("can not read container from %s\n", container_path);
		result = 2;
		goto err;
	}

	make_pwd_key(pwd_key, salt12, 12, passw);
	key_with_mask = decode_primary_key(pwd_key, primary_key, ctx);
	OPENSSL_cleanse(pwd_key, sizeof(pwd_key));
	mask = reverse32bn(masks_key, ctx);
	raw_key = remove_mask_and_check_public(oid_param_set8, key_with_mask, mask, public8, ctx);

	if (raw_key)
	{
		BIO *bio;
		store_bignum(raw_key, outbuf, sizeof(outbuf));
		memcpy(asn1_private_key+START_OID, oid_param_set8, 8);
		memcpy(asn1_private_key+START_KEY, outbuf, 32);
		//bio = BIO_new_file("private.key", "w");
		bio = BIO_new_fp(stdout, BIO_NOCLOSE | BIO_FP_TEXT);
		PEM_write_bio(bio, "PRIVATE KEY", "", asn1_private_key, sizeof(asn1_private_key));
		BIO_free(bio);
		OPENSSL_cleanse(outbuf, sizeof(outbuf));
		OPENSSL_cleanse(asn1_private_key, sizeof(asn1_private_key));
		result = 0; //ok
	}
	else
	{
		printf("Error check public key\n");
		result = 3;
	}

err:
	BN_CTX_free(ctx);
	OPENSSL_cleanse(salt12, sizeof(salt12));
	OPENSSL_cleanse(primary_key, sizeof(primary_key));
	OPENSSL_cleanse(masks_key, sizeof(masks_key));
	return result;
}

Небольшой комментарий.

Читайте также:  Основные виды ошибок Рутокен

Основную работу выполняют следующие 3 функции:

1. Создаем ключ хранения исходя из 12-ти байтовой «соли» и пароля.

make_pwd_key(pwd_key, salt12, 12, passw);

2. Расшифровываем основной ключ на ключе хранения.

key_with_mask = decode_primary_key(pwd_key, primary_key, ctx);

3. Делим ключ с маской на маску.

raw_key = remove_mask_and_check_public(oid_param_set8, key_with_mask, mask, public8, ctx);

Но так как в библиотеке OpenSLL операция деления по модулю традиционно отсутствует, пользуемся операцией взятия обратного числа и умножением.

if (!BN_mod_inverse(mask_inv, mask, order, ctx)) { result = 1; goto err; }
if (!BN_mod_mul(raw_secret, key_with_mask, mask_inv, order, ctx)) { result = 1; goto err; }

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

Для верифицирования сертификатов нужен сертификат удостоверяющего центра и актуальный список отзыва сертификатов,
либо настроенный для этого revocation provider.

Корневой сертификат УЦ, список отзыва сертификата является одним из реквизитов самого сертификата.

Контрагенты когда открывают подписи в КриптоАРМ используют revocation provider, он делает проверки отзыва сертификата онлайн.
Как реализована проверка в Шарепоинте не знаю. Знаю только что используется библиотека Крипто.Net

cryptcp  

Проверка конкретной подписи из локального хранилища по его хешу:

cryptcp   255c249150efe3e48f1abb3bc1928fc8f99980c4  test.txt.sig

Проверить, взяв сертификат из file1.sig, подпись файла file2.sig. Практически, надо использовать один и тот же файл:

cryptcp    file1.sig file2.sig

Ответ:

Certificates found: 2
Certificate chains are checked.
Folder './':
file.xls.sig... Signature verifying...
Signer: Старший инженер, Иванов Иван Иванович, Отдел закупок, ООО «Верес», Москва, RU, [email protected]
 Signature's verified.
Signer: Генеральный директор, Сидоров Иван Петрович, Руководство, ООО «Кемоптика», Москва, RU, [email protected]
 Signature's verified.
[ReturnCode: 0]

Результат:

[ReturnCode: x] Текст Описание Возвращаемый код завершения в баше $?
0 Успешно 0
0x80091004 Invalid cryptographic message type Неправильный формат файла 4
0x80091010 The streamed cryptographic message is not ready to return data Пустой файл 16

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

Часто в системах работающих с криптографией требуется печатное представление подписи. В каждом случае оно разное, поэтому лучше сформировать класс информации о подписи, который будет содержать информацию в удобном для использования виде и уже с его помощью обеспечивать печатное представление. В .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

Оцените статью
ЭЦП Эксперт
Добавить комментарий