В предыдущей статье был описан процесс интеграции ГОСТовых сертификатов КриптоПро с mono. В этой же подробно остановимся на подключении RSA сертификатов.


Мы продолжали переносить одну из наших серверных систем написанных на C# в Linux, и очередь дошла до части связанной с RSA. Если в прошлый раз сложности в подключении легко объяснялись наличием взаимодействия двух, исходно не связанных друг с другом систем, то при подключении «обычных» RSA сертификатов от mono явно никто не ожидал подвоха.



Установка сертификата и ключа проблем не вызвала, и система даже увидела его в штатном хранилище. Однако ни подписать, ни зашифровать, ни вытащить данные из ранее сформированной подписи уже не получалось — mono стабильно падало с ошибкой. Пришлось как и в случае с КриптоПро подключаться напрямую к библиотеке шифрования. Для RSA сертификатов в Linux основной кандидат для такого подключения — OpenSSL.


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


К счастью Centos 7 обладает встроенной версией OpenSSL — 1.0.2k. Чтобы не вносить дополнительные сложности в работу системы, решили подключаться именно к этой версии. OpenSSL позволяет формировать специальные файловые хранилища сертификатов, однако:


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

В силу этих причин будем использовать для подключения OpenSSL штатные хранилища сертификатов .Net и mono. Для этого в Linux сертификат и закрытый ключ необходимо предварительно поместить в хранилище mono.

Установка сертификата
Воспользуемся для этого штатной утилитой certmgr. В начале инсталлируем закрытый ключ из pfx:

certmgr -importKey -c -p {password} My {pfx file}

Затем ставим сертификат из этого pfx, закрытый ключ автоматически подключится к нему:

certmgr -add -c My {cer file}

Если требуется установить ключ в хранилище для машины, то необходимо добавить опцию -m.

После чего сертификат можно увидеть в хранилище:

certmgr -list -c -v My

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

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


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


В идеале должны быть методы, которые работали бы как и в Windows — независимо от типа сертификата (в Linux через OpenSSL или КриптоПро в зависимости от сертификата, а в Windows – через crypt32).


Анализ библиотек OpenSSL, показал, что в Windows основной библиотекой является «libeay32.dll», а в Linux «libcrypto.so.10». Так же как и в прошлый раз формируем два класса WOpenSSLAPI и LOpenSSLAPI, содержащие список подключаемых методов библиотеки:

[DllImport(CTRYPTLIB, CharSet = CharSet.Auto, SetLastError = true, CallingConvention = CallingConvention.Cdecl)]
internal static extern void OPENSSL_init();

Обратите внимание на конвенцию вызова, в отличии от КриптоПро — здесь ее необходимо явно указать. Синтаксис подключения каждого из методов в этот раз придется формировать самостоятельно на базе *.h файлов исходников OpenSSL.


Основные правила формирования синтаксиса вызова в С# на основе данных из .h файлов следующие:


  1. любые ссылки на структуры, строки и прочее — IntPtr, в том числе ссылки внутри самих структур;
  2. ссылки на ссылки — ref IntPtr, если такой вариант не срабатывает, то просто IntPtr. В таком случае саму ссылку придется класть и извлекать вручную;
  3. массивы — byte[];
  4. long в С (OpenSSL) это int в С# (маленькая, на первый взгляд, ошибка может обернуться часами поиска источника непредсказуемых ошибок);

В объявлении можно, по привычке, указывать SetLastError = true, но библиотека это проигнорирует — ошибки не будут доступны через Marshal.GetLastWin32Error(). Для доступа к ошибкам у OpenSSL свои методы.


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


private static object fpOSSection = new object();
/**<summary>Иницииализация библиотеки OpenSSL</summary>**/
public static void OPENSSL_init() {
    lock (pOSSection) {
        if (fIsLinux)
            LOpenSSLAPI.OPENSSL_init();
        else
            WOpenSSLAPI.OPENSSL_init();
    }
}
/**<summary>Критическая секция доступа в OpenSSL</summary>**/
public static object pOSSection
{
    get { return fpOSSection; }
}
/**<summary>Находимся в линуксе</summary>**/
public static bool fIsLinux {
    get {
        int iPlatform = (int) Environment.OSVersion.Platform;
        return (iPlatform == 4) || (iPlatform == 6) || (iPlatform == 128);
    }
}

Стразу отметим наличие критической секции. OpenSSL теоретически обеспечивает работу в много-поточном окружении. Но, во первых, сразу в описании сказано, что это не гарантируется:


But you still can’t concurrently use most objects in multiple threads.

А во вторых, способ подключения не самый тривиальный. Обычная двух ядерная VM (сервер с процессором Intel Xeon E5649 в режиме Hyper-Threading) при использовании такой критической секции дает около 100 полных циклов (см. алгоритм тестирования из прошлой статьи) или 600 подписей в секунду, что в принципе достаточно для большинства задач (при больших нагрузках все равно будет использоваться скорее микросервисная или узловая архитектура системы).


Инициализация и выгрузка OpenSSL


В отличии от КриптоПро OpenSSL требует определенных действий перед тем как начать ее использовать и после окончания работы с библиотекой:

/**<summary>Инициализация OpenSSL</summary>**/
public static void InitOpenSSL() {
    UOpenSSLAPI.OPENSSL_init();
    UOpenSSLAPI.ERR_load_crypto_strings();
    UOpenSSLAPI.ERR_load_RSA_strings();
    UOpenSSLAPI.OPENSSL_add_all_algorithms_conf();
    UOpenSSLAPI.OpenSSL_add_all_ciphers();
    UOpenSSLAPI.OpenSSL_add_all_digests();
}
/**<summary>Очистка OpenSSL</summary>**/
public static void CleanupOpenSSL() {
    UOpenSSLAPI.EVP_cleanup();
    UOpenSSLAPI.CRYPTO_cleanup_all_ex_data();
    UOpenSSLAPI.ERR_free_strings();
}


Информация об ошибках


OpenSSL хранит информацию об ошибках во внутренних структурах для доступа к которым, в библиотеке есть специальные методы. К сожалению, часть простых методов, таких как ERR_error_string — работает нестабильно, поэтому приходится использовать более сложные методы:


Получение информации об ошибках
/**<summary>Сформировать строку с ошибкой OpenSSL</summary>
* <param name="_iErr">Код ошибки</param>
* <param name="_iPart">Раздел</param>
* <returns>Строка ошибки</returns>
* **/
public static string GetErrStrPart(ulong _iErr, int _iPart) {
    // 0) Определяем тип строки
    IntPtr hErrStr = IntPtr.Zero;
    switch (_iPart) {
        case 0: hErrStr = UOpenSSLAPI.ERR_lib_error_string(_iErr);
                break;
        case 1: hErrStr = UOpenSSLAPI.ERR_func_error_string(_iErr);
                break;
        case 2: hErrStr = UOpenSSLAPI.ERR_reason_error_string(_iErr);
                break;
    }
    // 1) Формируем сроку
    return PtrToFirstStr(hErrStr);
}
/**<summary>Сформировать строку с ошибкой OpenSSL</summary>
* <param name="_iErr">Код ошибки</param>
* <returns>Строка ошибки</returns>
* **/
public static string GetErrStr(ulong _iErr ) {
    return UCConsts.S_GEN_LIB_ERR_MAKRO.Frm(_iErr, GetErrStrPart(_iErr, 0),
                                            GetErrStrPart(_iErr, 1), 
                                            GetErrStrPart(_iErr, 2));
}
/**<summary>Сформировать строку с ошибкой OpenSSL</summary>
* <returns>Строка ошибки</returns>
* **/        
public static string GetErrStrOS() {
    return GetErrStr(UOpenSSLAPI.ERR_get_error());
}

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


Получение строки по IntPtr
/**<summary>Извлекает из указателя первый PChar из области памяти  длинной _iLen</summary>
* <param name="_hPtr">Указетель на область неуправляемой памяти</param>
* <param name="_iLen">Длина области памяти</param>
* <returns>Итоговая строка</returns>
* **/
public static string PtrToFirstStr(IntPtr _hPtr, int _iLen = 256) {
    if(_hPtr == IntPtr.Zero) return "";
    try {
        byte[] arStr = new byte[_iLen];
        Marshal.Copy(_hPtr, arStr, 0, arStr.Length);
        string[] arRes = Encoding.ASCII.GetString(arStr).Split(new char[] { (char)0 }, 
                                                               StringSplitOptions.RemoveEmptyEntries);
        if (arRes.Length > 0) return arRes[0];
          return "";
    }catch {
          return "";
    }
}

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


Получение ошибки верификации сертификата
/**<summary>Извлечение ошибки верификации сертификата</summary>
* <param name="_hStoreCtx">Контекст хранилища</param>
* <returns>Строка ошибки</returns>
* **/
public static string GetCertVerifyErr(IntPtr _hStoreCtx) {
    int iErr = UOpenSSLAPI.X509_STORE_CTX_get_error(_hStoreCtx);
    return PtrToFirstStr(UOpenSSLAPI.X509_verify_cert_error_string(iErr));
}

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


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


Поиск сертификата
/**<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>
* **/
internal static int FindCertificateOS(string _pFindValue, out X509Certificate2 _pCert, ref string _sError, 
                                      StoreLocation _pLocation = StoreLocation.CurrentUser, 
                                      StoreName _pName = StoreName.My, 
                                      X509FindType _pFindType = X509FindType.FindByThumbprint,                                             
                                      bool _fVerify = false) {
    lock (UOpenSSLAPI.pOSSection) {
        // 0) Сздаем нативное хранилище
        _pCert = null;
        X509Store pStore = new X509Store(_pName, _pLocation);
        X509Certificate2Collection pCerts = null;
        try {
            // 1) Открытие хранилища
            pStore.Open(OpenFlags.ReadOnly);
            // 2) Поиск в хранилище (не проверяя, т.к. Verify в Linux всегда false)
            pCerts = pStore.Certificates.Find(_pFindType, _pFindValue, false);
            if (pCerts.Count == 0) return UConsts.E_NO_CERTIFICATE;
            // 3) Нет проверки возвращаем первый
            if (!_fVerify) {
                _pCert = ISDP_X509Cert.Create(pCerts[0], TCryptoPath.cpOpenSSL);
                return UConsts.S_OK;
            }
            // 4) Проходим по сертфикатам и выбираем валидный
            foreach (X509Certificate2 pCert in pCerts) {
                ISDP_X509Cert pISDPCert = ISDP_X509Cert.Create(pCert, TCryptoPath.cpOpenSSL);
                if (pISDPCert.ISDPVerify()) {
                    _pCert = pISDPCert;
                     return UConsts.S_OK;
                }
            }
            return UConsts.E_NO_CERTIFICATE;
        } finally {
            if(pCerts != null) pCerts.Clear();
            pStore.Close();
        }
    }
}

Обратите внимание на критическую секцию. Mono с сертификатами работает так же через OpenSSL, но не через UOpenSSLAPI. Если ее здесь не сделать можно получить и утечки памяти и плавающие непонятные ошибки под нагрузкой.


Основной особенностью является создание сертификата. В отличии от варианта для КриптоПро в данном случае из хранилища получаем сам сертификат (X509Certificate2), и ссылка в Handle в нем уже направлена на OpenSSL структуру X509_st. Казалось бы это то, что надо, но вот указателя на EVP_PKEY (ссылка на структуру закрытого ключа в OpenSSL) в сертификате нет.

Сам закрытый ключ, как оказалось, хранится в открытом виде во внутреннем поле сертификата — impl/fallback/_cert/_rsa/rsa. Это класс RSAManaged, и беглый взгляд на его код (например, на метод DecryptValue) показывает на сколько в mono все плохо с криптографией. Вместо того, чтобы честно использовать методы криптографии OpenSSL, они, судя по всему, реализовали несколько алгоритмов вручную. Это предположение подкрепляется пустым результатом поиска по их проекту по таким OpenSSL методам, как например, CMS_final, CMS_sign или CMS_ContentInfo_new. А без них сложно представить себе формирование стандартной структуры CMS подписи. При этом работа с сертификатами частично ведется через OpenSSL.


Это говорит о том, что закрытый ключ придется выгружать из mono и загружать в EVP_PKEY через pem. В силу этого нам снова потребуется класс наследник от X509Certificate, который будет хранить все дополнительные ссылки.


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


Получение PEM сертификата
/**<summary>Получить файл сертификата</summary>
* <param name="_pCert">Сертификат</param>
* <param name="_arData">Выходные бинарные данные</param>
* <param name="_fBase64">Формат Base64</param>
* <param name="_sError">Возвращаемая строка с ошибкой</param>
* <returns>Стандартый код ошибки, если UConsts.S_OK то все ок</returns>
* **/
public static int ToCerFile(this X509Certificate2 _pCert, out byte[] _arData, 
                            ref string _sError, bool _fBase64 = true) {
    _arData = new byte[0];
    try {
        byte[] arData = _pCert.Export(X509ContentType.Cert);
        // 0) DER
        if (!_fBase64) {
            _arData = arData;
            return UConsts.S_OK;
        }
        // 1) Base64
        using (TextWriter pWriter = new StringWriter()) {                    
            pWriter.WriteLine(UCConsts.S_PEM_BEGIN_CERT);
            pWriter.WriteLine(Convert.ToBase64String(arData, Base64FormattingOptions.InsertLineBreaks));
            pWriter.WriteLine(UCConsts.S_PEM_END_CERT);
            // 1.2) Возвращаем итог
            _arData = Encoding.UTF8.GetBytes(pWriter.ToString());
        }                
        return UConsts.S_OK;
    } catch (Exception E) {
         _sError = UCConsts.S_TO_PEM_ERR.Frm(E.Message);
         return UConsts.E_GEN_EXCEPTION;
    }            
}

Сертификат получается без закрытого ключа и мы подключаем его самостоятельно, формируя отдельное поле для ссылки на ENV_PKEY:


Формирование ENV_PKEY на основе PEM закрытого ключа
/**<summary>Формируем OpenSSL контекст закрытого ключа (EVP_PKEY) по данным закрытого ключа</summary>
* <remarks>Преобразование через выгрузку закрытого ключа в PEM</remarks>
* <param name="_arData">Данные закрытого ключа сертификата</param>
* <returns>Контекст закрытого ключа (EVP_PKEY)</returns>
* **/
internal static IntPtr GetENV_PKEYOS(byte[] _arData) {
    IntPtr hBIOPem = IntPtr.Zero;
    try {
        // 0)  Выгружаем в BIO
        hBIOPem = UOpenSSLAPI.BIO_new_mem_buf( _arData, _arData.Length);
        if (hBIOPem == IntPtr.Zero) return IntPtr.Zero;
        IntPtr hKey = IntPtr.Zero;
        // 1)  Формируем структуру закрытого ключа
        UOpenSSLAPI.PEM_read_bio_PrivateKey(hBIOPem, ref hKey, IntPtr.Zero, 0);
        return hKey;
    } finally {
        if(hBIOPem != IntPtr.Zero) UOpenSSLAPI.BIO_free(hBIOPem);
    }
}

Выгрузка закрытого ключа в PEM, задача значительно сложнее, чем PEM сертификата, но она уже описана здесь. Отметим, что выгрузка закрытого ключа дело «крайне небезопасное», и этого следует всячески избегать. И т. к. для работы с OpenSSL такая выгрузка получается обязательной, то в Windows использованию этой библиотеки лучше предпочти методы crypt32.dll или же штатные классы .Net. В Linux же пока придется работать так.


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


Подписание


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


Подпись данных
/**<summary> Подписывает информацию</summary>
* <param name="_arData">Данные для подписания</param>
* <param name="_pCert">Сертификат</param>
* <param name="_sError">Возвращаемая строка с ошибкой</param>
* <param name="_arRes">Подпись сертфиикат</param>
* <returns>Стандартый код ошибки, если UConsts.S_OK то все ок</returns>
* **/
internal static int SignDataOS(byte[] _arData, X509Certificate2 _pCert, out byte[] _arRes, ref string _sError) {

    _arRes         = new byte[0];
    uint iFlags    = UCConsts.CMS_DETACHED;
    IntPtr hData   = IntPtr.Zero;
    IntPtr hBIORes = IntPtr.Zero;
    IntPtr hCMS    = IntPtr.Zero;
    try {
        // 0) Формируем сертфиикат
        ISDP_X509Cert pCert = ISDP_X509Cert.Convert(_pCert, TCryptoPath.cpOpenSSL);
        // 1) Формируем BIO с данными
        int iRes = GetBIOByBytesOS(_arData, out hData, ref _sError);
        if (iRes != UConsts.S_OK) return iRes;
        // 2) Создаем итоговый BIO
        hBIORes = UOpenSSLAPI.BIO_new(UOpenSSLAPI.BIO_s_mem());
        // 3) Формируем объект подписи
        hCMS = UOpenSSLAPI.CMS_ContentInfo_new();
        if (hCMS == IntPtr.Zero) return RetErrOS(ref _sError, UCConsts.S_OS_CMS_CR_ERR);
        if (!UOpenSSLAPI.CMS_SignedData_init(hCMS)) 
            return RetErrOS(ref _sError, UCConsts.S_OS_CMS_INIT_ERR);
        // 4) Добавляем подписанта
        if(UOpenSSLAPI.CMS_add1_signer(hCMS, pCert.hRealHandle, pCert.hOSKey,
                                       pCert.hOSDigestAlg, iFlags) == IntPtr.Zero)
            return RetErrOS(ref _sError, UCConsts.S_OS_CMS_SET_SIGNER_ERR);
        // 5) Установка флага - отцепленная подпись
        if (!UOpenSSLAPI.CMS_set_detached(hCMS, 1))
            return RetErrOS(ref _sError, UCConsts.S_OS_CMS_SET_DET_ERR);
        // 6) Завершение формирования подписи
        if (!UOpenSSLAPI.CMS_final(hCMS, hData, IntPtr.Zero, iFlags))
            return RetErrOS(ref _sError, UCConsts.S_OS_CMS_FINAL_ERR);
        // 7) Переписываем в заготовленный BIO
        if (!UOpenSSLAPI.i2d_CMS_bio_stream(hBIORes, hCMS, IntPtr.Zero, iFlags)) 
             return RetErrOS(ref _sError, UCConsts.S_OS_CMS_EXP_TO_BIO_ERR);
        // 8) Считываем полученные данные из BIO
        return ReadFromBIO_OS(hBIORes, out _arRes, ref _sError);
    } catch (Exception E) {
        _sError = UCConsts.S_SIGN_OS_GEN_ERR.Frm(E.Message);
        return UConsts.E_GEN_EXCEPTION;
    } finally {
        if(hBIORes != IntPtr.Zero) UOpenSSLAPI.BIO_free(hBIORes);
        if(hData != IntPtr.Zero) UOpenSSLAPI.BIO_free(hData);
        if(hCMS != IntPtr.Zero) UOpenSSLAPI.CMS_ContentInfo_free(hCMS);
    }
}

Ход алгоритма следующий. Сначала преобразуем входящий сертификат (если он является X509Certificate2) в наш тип. Т.к. мы работаем со ссылками на неуправляемую область памяти, то за ними надо внимательно следить. .Net через какое-то время после выхода ссылки на сертификат из области видимости сам запустит деструктор. А в нем мы уже точно прописали методы, необходимые для очистки всей связанной с ним неуправляемой памяти. Такой подход позволит нам не тратить время на отслеживания этих ссылок непосредственно внутри метода.


Разобравшись с сертификатом формируем BIO с данными и структуру подписи. Затем добавляем данные подписанта, ставим флаг отцепленности подписи и запускаем финальное формирование подписи. Результат переносим в BIO. Остается только извлечь из BIO массив байт. Преобразование BIO в набор байт и обратно довольно часто используются, поэтому лучше вынести их в отдельный методы:


BIO в byte[] и обратно
/**<summary>Прочитать из BIO массив байт из OpenSSL</summary>
* <param name="_hBIO">Контекст BIO</param>
* <param name="_sError">Возвращаемая стркоа с ошибкой</param>
* <param name="_arRes">Результат</param>
* <param name="_iLen">Длина данных, если 0 - то все что есть</param>
* <returns>Стандартный код с ошибкой, если UConsts.S_OK то все ок</returns>
* **/
internal static int ReadFromBIO_OS(IntPtr _hBIO, out byte[] _arRes, ref string _sError, uint _iLen = 0) {
    _arRes = new byte[0];
    IntPtr hRes = IntPtr.Zero;
    uint iLen = _iLen;
    if(iLen == 0) iLen = int.MaxValue;
    try {
        // 0) Определяем длину
        iLen = UOpenSSLAPI.BIO_read(_hBIO, IntPtr.Zero, int.MaxValue);
        // 1) Формируем буфер и читаем  него 
        hRes  = Marshal.AllocHGlobal((int)iLen);
        if (UOpenSSLAPI.BIO_read(_hBIO, hRes, iLen) != iLen) {
            _sError = UCConsts.S_OS_BIO_READ_LEN_ERR;
            return UConsts.E_CRYPTO_ERR;
        } 
        // 2) Итоговый массив
        _arRes = new byte[iLen];
        Marshal.Copy(hRes, _arRes, 0, _arRes.Length);
        return UConsts.S_OK;;
    } catch (Exception E) {
        _sError = UCConsts.S_OS_BIO_READ_GEN_ERR.Frm(E.Message);
        return UConsts.E_GEN_EXCEPTION;
    } finally { 
        if(hRes != IntPtr.Zero) Marshal.FreeHGlobal(hRes);
    }
}
/**<summary>Получить BIO по набору байт</summary>
* <param name="_arData">Данные</param>
* <param name="_hBIO">Возвращаемый указатель на BIO</param>
* <param name="_sError">Возвращаемая строка с ошибкой</param>
* <returns>Стандартный код ошибки, если UConsts.S_OK то все ок</returns>
* **/
internal static int GetBIOByBytesOS(byte[] _arData, out IntPtr _hBIO, ref string _sError) {
    _hBIO = UOpenSSLAPI.BIO_new_mem_buf( _arData, _arData.Length);
    if (_hBIO == IntPtr.Zero) 
        return  RetErrOS(ref _sError, UCConsts.S_OS_CM_BIO_CR_ERR);
    return UConsts.S_OK;
}

Так же как и в случае с КриптоПро необходимо извлечь из сертификата информацию об алгоритме хэширования подписи. Но в случае с OpenSSL он храниться прямо в сертификате:


Извлечение алгоритма хэширования
/**<summary>Извлечение алгоритма хэширования из контекста сертфииката OpenSSL</summary>
* <param name="_hCert">Контекст сертификта (X509)</param>
* <returns>Контекст аглоритма</returns>
* **/
public static IntPtr GetDigestAlgOS(IntPtr _hCert) {
    x509_st pCert = (x509_st)Marshal.PtrToStructure(_hCert, typeof(x509_st));
    X509_algor_st pAlgInfo = (X509_algor_st)Marshal.PtrToStructure(pCert.sig_alg, typeof(X509_algor_st));
    IntPtr hAlgSn = UOpenSSLAPI.OBJ_nid2sn(UOpenSSLAPI.OBJ_obj2nid(pAlgInfo.algorithm));
    return UOpenSSLAPI.EVP_get_digestbyname(hAlgSn);
}

Способ получился довольно хитрый, но он работает. В документации 1.0.2 можно найти метод EVP_get_digestbynid, однако библиотеки используемой нами версии его не экспортируют. Поэтому сначала формируем nid, а на его основе короткое название. И уже по короткому названию можно извлечь алгоритм штатным способом поиска по имени.


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


Полученная подпись нуждается в проверке. OpenSSL проверяет подпись следующим образом:


Проверка подписи
/**<summary>Проверяет подпись</summary>
* <param name="_arData">данные, которые было подписаны</param>
* <param name="_arSign">подпись</param>
* <param name="_pCert">сертификат</param>
* <param name="_sError">возвращаемая строка с ошибкой</param>
* <param name="_pLocation">Местопложение</param>
* <param name="_fVerifyOnlySign">Проверять только подпись</param>
* <returns>Стандартый код ошибки, если UConsts.S_OK то все ок</returns>
* <remarks>Проверяется только первый подписант</remarks>
* **/
internal static int CheckSignOS(byte[] _arData, byte[] _arSign, out X509Certificate2 _pCert, ref string _sError,
                                bool _fVerifyOnlySign = true, 
                                StoreLocation _pLocation = StoreLocation.CurrentUser){
    _pCert = null;
    IntPtr hBIOData = IntPtr.Zero;
    IntPtr hCMS     = IntPtr.Zero;
    IntPtr hTrStore = IntPtr.Zero;
    try {
        // 0) Формирование BIO с данными для подписи
        int iRes = GetBIOByBytesOS(_arData, out hBIOData, ref _sError);
        if (iRes != UConsts.S_OK) return iRes;
        // 1) Чтение структуры CMS
        iRes = GetCMSFromBytesOS(_arSign, out hCMS, ref _sError);
        if (iRes != UConsts.S_OK) return iRes;
        uint iFlag =  UCConsts.CMS_DETACHED;                
        // 2) Формирование доверенного хранилища
        if (!_fVerifyOnlySign) {
            iRes = GetTrustStoreOS(_pLocation, out hTrStore, ref _sError);
            if (iRes != UConsts.S_OK) return iRes;
        } else 
            iFlag |= UCConsts.CMS_NO_SIGNER_CERT_VERIFY;
        // 3) Проверка подписи
        if (!UOpenSSLAPI.CMS_verify(hCMS, IntPtr.Zero, hTrStore, hBIOData, IntPtr.Zero, iFlag)) 
            return RetErrOS(ref _sError, UCConsts.S_OS_CM_CHECK_ERR);
        return UConsts.S_OK;                 
    } finally {
        if(hBIOData != IntPtr.Zero) UOpenSSLAPI.BIO_free(hBIOData);
        if(hCMS != IntPtr.Zero) UOpenSSLAPI.CMS_ContentInfo_free(hCMS);
        if(hTrStore != IntPtr.Zero) UOpenSSLAPI.X509_STORE_free(hTrStore);
    }
}

Вначале происходит преобразование данных подписи из массива байт в структуру CMS:


Формирование структуры CMS
/**<summary>Получить CMS из набора байт</summary>
* <param name="_arData">Данные CMS</param>
* <param name="_hCMS">Возвращаемый указатель  на структуру CMS</param>
* <param name="_sError">Возвращаемая строка с ошибкой</param>
* <returns>Стандартный код ошибки, если UConsts.S_OK то все ок</returns>
* **/
internal static int GetCMSFromBytesOS(byte[] _arData, out IntPtr _hCMS, ref string _sError) {
    _hCMS          = IntPtr.Zero; 
    IntPtr hBIOCMS = IntPtr.Zero;
    IntPtr hCMS    = IntPtr.Zero;
    try {
        // 0) Инициалиацзия структуры CMS
        hCMS = UOpenSSLAPI.CMS_ContentInfo_new();
        if (hCMS == IntPtr.Zero) return RetErrOS(ref _sError);
        if (!UOpenSSLAPI.CMS_SignedData_init(hCMS))
            return RetErrOS(ref _sError);
        // 1) Чтение данных в BIO
        hBIOCMS = UOpenSSLAPI.BIO_new_mem_buf(_arData, _arData.Length);
        if (hBIOCMS == IntPtr.Zero) return RetErrOS(ref _sError);
        // 2) Преобразование в CMS
        if (UOpenSSLAPI.d2i_CMS_bio(hBIOCMS, ref hCMS) == IntPtr.Zero)
            return RetErrOS(ref _sError);
        // 3) Все ок - перекрываем, чтобы не занулило
        _hCMS = hCMS;
        hCMS = IntPtr.Zero;
        return UConsts.S_OK;
    } finally {
        if(hBIOCMS != IntPtr.Zero) UOpenSSLAPI.BIO_free(hBIOCMS);
        if(hCMS != IntPtr.Zero) UOpenSSLAPI.CMS_ContentInfo_free(hCMS);
    }
}

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


Формирование хранилища сертификатов
/**<summary>Получить доверенное хранилище в памяти</summary>
* <param name="_hStore">Возвращаемая ссылка на хранилище</param>
* <param name="_pLocation">Местоположение хранилища</param>
* <param name="_sError">Возвращаемая строка с ошибкой</param>
* <returns>Стандартный код ошибки, если UConsts.S_OK то все ок</returns>
*/
internal static int GetTrustStoreOS(StoreLocation _pLocation, out IntPtr _hStore, ref string _sError) {
    _hStore = IntPtr.Zero;
    IntPtr hStore   = IntPtr.Zero;
    try {
        List<X509Certificate2> pCerts = GetCertList(_pLocation, StoreName.Root, TCryptoPath.cpOpenSSL);
        pCerts.AddRange(GetCertList(_pLocation, StoreName.AuthRoot, TCryptoPath.cpOpenSSL));
        // 1) Формируем хранилище
        hStore = UOpenSSLAPI.X509_STORE_new();
        foreach (X509Certificate2 pCert in pCerts) {
            // Даже если ошибка идем дальше (чаще всего ошибка дубля сертификатов)
            UOpenSSLAPI.X509_STORE_add_cert(hStore, pCert.getRealHandle());
        }
        // 2) Очистка ошибок
        UOpenSSLAPI.ERR_clear_error();
        _hStore = hStore;
        hStore = IntPtr.Zero;
        return UConsts.S_OK;
    } catch (Exception E) {
        _sError = UCConsts.S_FORM_TRUST_STORE_ERR.Frm(E.Message);
        return UConsts.E_GEN_EXCEPTION;
    } finally {
        if (hStore != IntPtr.Zero) UOpenSSLAPI.X509_STORE_free(hStore);
}

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


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


Информация о подписи


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


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


Разбор данных подписи
/**<summary>Разбор данных подписи</summary>
* <param name="_arSign">Подпись</param>
* <param name="_sError">Возвращаемая строка с ошибкой</param>
* <param name="_arContent">Данные для подписи</param>
* <returns>Стандартный код ошибки, если UConsts.S_OK то все ок</returns>
* **/
internal int DecodeOS(byte[] _arSign, byte[] _arContent, ref string _sError) {
    IntPtr hBIOData = IntPtr.Zero;
    IntPtr hCMS     = IntPtr.Zero;
    IntPtr hCerts   = IntPtr.Zero;
    try {
        // 0) Формируем данные и СMS
        int iRes = UCUtils.GetCMSFromBytesOS(_arSign, out hCMS, ref _sError);
        if(iRes != UConsts.S_OK) return iRes;
        iRes = UCUtils.GetBIOByBytesOS(_arContent, out hBIOData, ref _sError);
        if(iRes != UConsts.S_OK) return iRes;
        // 1) Устанавливаем флаги
        uint iFlags = UCConsts.CMS_NO_SIGNER_CERT_VERIFY;
        if(_arContent.Length == 0) iFlags |= UCConsts.CMS_NO_CONTENT_VERIFY;
        // 2) Считываем CMS
        if (!UOpenSSLAPI.CMS_verify(hCMS, IntPtr.Zero, IntPtr.Zero, hBIOData, IntPtr.Zero,  iFlags))
             return UCUtils.RetErrOS(ref _sError, UCConsts.S_OS_CMS_VERIFY_ERR);
        // 3) Извлекаем сертификаты
        hCerts = UOpenSSLAPI.CMS_get0_signers(hCMS);
        int iCnt = UOpenSSLAPI.sk_num(hCerts);
        for (int i = 0; i < iCnt; i++) {
            IntPtr hCert = UOpenSSLAPI.sk_value(hCerts, i);
            byte[] arData;
            iRes = UCUtils.GetCertBytesOS(hCert, out arData, ref _sError);
            if(iRes != UConsts.S_OK) return iRes;
            fpCertificates.Add(ISDP_X509Cert.Create(arData, TCryptoPath.cpOpenSSL));
        }
        // 4) Извлекаем подписантов
        IntPtr hSigners = UOpenSSLAPI.CMS_get0_SignerInfos(hCMS);                
        iCnt = UOpenSSLAPI.sk_num(hSigners);                
        for (int i = 0; i < iCnt; i++) {
            IntPtr hSignerInfo = UOpenSSLAPI.sk_value(hSigners, i);
            // 4.1) Информация о подписанте
            ISDPSignerInfo pInfo = new ISDPSignerInfo(this);
            iRes = pInfo.DecodeOS(hSignerInfo, ref _sError);
            if(iRes != UConsts.S_OK) return iRes;
            fpSignerInfos.Add(pInfo);
        }
        return UConsts.S_OK;
    } catch (Exception E) {
        _sError = UCConsts.S_OS_CMS_DECODE.Frm(E.Message);
        return UConsts.E_GEN_EXCEPTION;
    } finally {
        if(hCerts != IntPtr.Zero) UOpenSSLAPI.sk_free(hCerts);
        if(hBIOData != IntPtr.Zero) UOpenSSLAPI.BIO_free(hBIOData);
        if(hCMS != IntPtr.Zero) UOpenSSLAPI.CMS_ContentInfo_free(hCMS);
    }
}

Как и в прошлой главе, сначала формируем из массивов байт BIO с данными для подписи (если нужно) и структуру CMS, после чего запускаем проверку. Это может показаться странным, но информация о подписи формируется именно в методе проверки — поэтому перед извлечением данных нужно вызвать этот метод.


Список сертификатов и подписантов выдается в виде стеков (STACK_OF(X509)), однако если извлекать элементы методом sk_pop, то освобождение извлеченных структур будет задачей разработчика. Если желания делать этого нет, то стоит проходить в цикле и извлекать значение методом sk_value.


Стоит отметить, что при получении списка сертификатов необходимо использовать метод CMS_get0_signers вместо CMS_get1_certs. Первый выдает список сертификатов подписантов, второй все сертификаты. По логике правильнее использовать второй вариант, но он содержит вызов метода потоковой блокировки, что в результате под нагрузкой ведет к утечкам памяти:


CRYPTO_add(&cch->d.certificate->references, 1, CRYPTO_LOCK_X509);

В версии 1.1.0 вызов заменен на X509_up_ref, поэтому проблема может быть уже устранена.
Для получения информации о каждом подписанте сформируем отдельный метод:


Распарсить информацию о подписанте
/**<summary>Распарсить информацию о подписанте</summary>
* <param name="_hSignerInfo">Handler информации о подписанте (OpenSSL)</param>
* <param name="_sError">Возвращаемая строка с ошибкой</param>
* <returns>Стандартный код ошибки, если UConsts.S_OK то все ок</returns>
* **/
public int DecodeOS(IntPtr _hSignerInfo, ref string _sError) {
    try {
        // 0) Определение сертфиката подписанта
        int iRes = UCUtils.GetSignerInfoCertOS(_hSignerInfo, fpSignedCMS.pCertificates, 
                                               out fpCertificate, ref _sError);
        if(iRes != UConsts.S_OK) return iRes;
        // 1) Извлечение даты подписи
        uint iPos =  UOpenSSLAPI.CMS_signed_get_attr_by_NID(_hSignerInfo, UCConsts.NID_pkcs9_signingTime, 0);
        IntPtr hAttr = UOpenSSLAPI.CMS_signed_get_attr(_hSignerInfo, iPos);
        IntPtr hDateTime = UOpenSSLAPI.X509_ATTRIBUTE_get0_data(hAttr, 0, UCConsts.V_ASN1_UTCTIME, IntPtr.Zero);
        asn1_string_st pDate = (asn1_string_st)Marshal.PtrToStructure(hDateTime, typeof(asn1_string_st));
        // 2) Преобрзование в Pkcs9SigningTime
        byte[] arDateAttr = new byte[pDate.iLength];
        Marshal.Copy(pDate.hData, arDateAttr, 0, (int)pDate.iLength);
        arDateAttr = new byte[] { (byte)UCConsts.V_ASN1_UTCTIME, (byte)pDate.iLength}.Concat(arDateAttr).ToArray();
        fpSignedAttributes.Add(new Pkcs9SigningTime(arDateAttr));
        return UConsts.S_OK;
    } catch (Exception E) {
        _sError = UCConsts.S_CMS_SIGNER_DEC_OS_ER.Frm(E.Message);
        return UConsts.E_GEN_EXCEPTION;
    }
}

В нем, вначале извлекаем сертификат подписанта, а затем получаем дату подписания. Дата подписания это один из атрибутов подписи в формате ASN.1. Для его извлечения считаем структуру asn1_string_st и на основе данных в нем сформируем атрибут Pkcs9SigningTime.


Получение сертификата реализованно отдельно:


Получение сертификата подписанта
/**<summary>Получить сертификата подписанта</summary>
* <param name="_hSignerInfo">Информация об подписанте</param>
* <param name="_pCert">Возврат сертификата</param>
* <param name="_pCerts">Список сертификатов где искать</param>
* <param name="_sError">Возвращаемая строкка с ошибкой</param>
* <returns>Стандартый код ошибки, если UConsts.S_OK то все ок</returns>
* **/
internal static int GetSignerInfoCertOS(IntPtr _hSignerInfo, X509Certificate2Collection _pCerts,
                                        out X509Certificate2 _pCert, ref string _sError) {
    _pCert = null;
    try {
        // 0) Получаем информацию об адресате
        IntPtr hKey    = IntPtr.Zero;
        IntPtr hIssuer = IntPtr.Zero;
        IntPtr hSNO    = IntPtr.Zero;
        if (!UOpenSSLAPI.CMS_SignerInfo_get0_signer_id(_hSignerInfo, ref hKey, ref hIssuer, ref hSNO))
             return RetErrOS(ref _sError, UCConsts.S_GET_RECEIP_INFO_ERR);
        // 1) Извлекается серийный номер
        string sSerial;
        int iRes = GetBinaryHexFromASNOS(hSNO, out sSerial, ref _sError);
        if(iRes != UConsts.S_OK) return iRes;
        X509Certificate2Collection pResCerts = _pCerts.Find(X509FindType.FindBySerialNumber, sSerial, false);
        if(pResCerts.Count == 0) return RetErrOS(ref _sError, UCConsts.S_NO_CERTIFICATE, UConsts.E_NO_CERTIFICATE);
        _pCert = pResCerts[0];
        return UConsts.S_OK;
    } catch (Exception E) {
        _sError = UCConsts.S_GET_SIGN_INFO_GEN_ERR.Frm(E.Message);
        return UConsts.E_GEN_EXCEPTION;
    }
}

Вначале извлекается информация о серийном номере и издателе, а затем по нему из ранее полученного списка извлекается сертификат. Серийный номер представляет из себя структуру asn1_string_st, из которой извлекаются бинарные данные и преобразуются в hex формат:


Получить hex бинарных данных их ANS.1
 /**<summary>Получить Hex отображение бинарных данных из ASN.1</summary>
* <param name="_hASN">Ссылка на элемент ASN.1</param>
* <param name="_sError">Возвращаемая строка с ошибкой</param>
* <param name="_sHexData">Данные в формате Hex</param>
* <returns>Стандартый код ошибки, если UConsts.S_OK то все ок</returns>
* **/
internal static int GetBinaryHexFromASNOS(IntPtr _hASN, out string _sHexData, ref string _sError) {
    _sHexData = "";
    try {
        asn1_string_st pSerial = (asn1_string_st)Marshal.PtrToStructure(_hASN, typeof(asn1_string_st));
        byte[] arStr = new byte[pSerial.iLength];
        Marshal.Copy(pSerial.hData, arStr, 0, (int)pSerial.iLength);
        _sHexData = arStr.ToHex().ToUpper();
        return UConsts.S_OK;
    } catch (Exception E) {
        _sError = UCConsts.S_HEX_ASN_BINARY_ERR.Frm(E.Message);
        return UConsts.E_GEN_EXCEPTION;
    }
}

Как и в случае подписи КриптоПро, поиска по серийному номеру обычно достаточно, чтобы однозначно идентифицировать сертификат.


Шифрование


Шифрование в OpenSSL происходит довольно логичным способом:


Шифрование данных
/**<summary>Зашифрованные данные</summary>
* <param name="_arInput">Данные для расшифровки</param>
* <param name="_pReceipients">Список сертфиикатов адресатов</param>
* <param name="_arRes">Результат</param>
* <param name="_sError">Возвращаемая строка с ошибкой</param>
* <returns>Стандартный код с ошибкой, если UConsts.S_OK то все ок</returns>
* **/
internal static int EncryptDataOS(byte[] _arInput, List<X509Certificate2> _pReceipients, out byte[] _arRes, 
                                  ref string _sError) {
    _arRes           = new byte[0];
    uint iFlags      = UCConsts.CMS_BINARY;
    IntPtr hData     = IntPtr.Zero;
    IntPtr hReceipts = IntPtr.Zero;
    IntPtr hBIORes   = IntPtr.Zero;
    IntPtr hCMS      = IntPtr.Zero;
    try {
        // 0) Сформировать BIO с данными для кодирования
        int iRes = GetBIOByBytesOS(_arInput, out hData, ref _sError);
        if (iRes != UConsts.S_OK) return iRes;
        // 1) Формирование стека сертификатов адресатов
        iRes = GetCertsStackOS(_pReceipients, out hReceipts, ref _sError);
        if (iRes != UConsts.S_OK) return iRes;
        // 2) Формирование CMS
        hCMS = UOpenSSLAPI.CMS_encrypt(hReceipts, hData, UOpenSSLAPI.EVP_des_ede3_cbc(), iFlags);
        if (hCMS == IntPtr.Zero) return RetErrOS(ref _sError, UCConsts.S_ENC_CMS_ERR);
        // 3) Запись CMS в BIO
        hBIORes = UOpenSSLAPI.BIO_new(UOpenSSLAPI.BIO_s_mem());
        if (!UOpenSSLAPI.i2d_CMS_bio_stream(hBIORes, hCMS, IntPtr.Zero, iFlags))
             return RetErrOS(ref _sError, UCConsts.S_OS_CMS_EXP_TO_BIO_ERR);
        // 4) Преобразование из BIO в набор байт
        return ReadFromBIO_OS(hBIORes, out _arRes, ref _sError);
    } catch (Exception E) {
        _sError = UCConsts.S_ENC_OS_GEN_ERR.Frm(E.Message);
        return UConsts.E_GEN_EXCEPTION;
    } finally {
        if(hBIORes != IntPtr.Zero) UOpenSSLAPI.BIO_free(hBIORes);
        if(hData != IntPtr.Zero) UOpenSSLAPI.BIO_free(hData);
        if(hCMS != IntPtr.Zero) UOpenSSLAPI.CMS_ContentInfo_free(hCMS);
        if(hReceipts != IntPtr.Zero) UOpenSSLAPI.sk_free(hReceipts);
    }
}

В начале формируется BIO с данными и список сертификатов — адресатов. Именно в адрес этих сертификатов будет происходит шифрование. Затем происходит непосредственно шифрование, и уже после этого данные из BIO экспортируются в массив байт. OpenSSL обладает довольно большим набором алгоритмов шифрования, поэтому однозначно выбрать его, как удалось для КриптоПро, вряд ли удастся. В силу этих причин либо надо использовать часто используемый EVP_des_ede3_cbc, либо переложить этот выбор на вызывающую сторону.


Для формирования стека сертификатов адресатов стоит выделить отдельный метод, т. к. параметр со стеком сертификатов довольно часто встречается в других методах OpenSSL:


Формирование стека сертификатов
/**<summary>Получить стек сертификатов</summary>* <param name="_hStack">Возвращаемый стек</param>
* <param name="_pCerts">Список сертификатов</param>
* <param name="_sError">Возвращаемая строка с ошибки</param>
* <returns>Стандартый код ошибки, если UConsts.S_OK то все ок</returns>
* **/
public static int GetCertsStackOS(List<X509Certificate2> _pCerts, out IntPtr _hStack, ref string _sError) {
    _hStack       = IntPtr.Zero;
    IntPtr hStack = IntPtr.Zero;
    try {
        hStack = UOpenSSLAPI.sk_new_null();
        foreach (X509Certificate2 pCert in _pCerts) {
            // 0) Формируем класс, чтобы не было утечек
            ISDP_X509Cert pLocCert = ISDP_X509Cert.Convert(pCert, TCryptoPath.cpOpenSSL);
            // 1) Добавляем
            UOpenSSLAPI.sk_push(hStack, pLocCert.hRealHandle);
        }
        _hStack = hStack;
        hStack = IntPtr.Zero;
        return UConsts.S_OK;
    } catch (Exception E) { 
        _sError = UCConsts.S_GEN_CERT_STACK_ERR.Frm(E.Message);
        return UConsts.E_GEN_EXCEPTION;
    } finally {
        if(hStack != IntPtr.Zero) UOpenSSLAPI.sk_free(hStack);
    }
}

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


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


  1. формируется, структура данных шифрования на основе массива данных;
  2. извлекается список адресатов;
  3. для каждого адресата пытаемся найти его сертификат на машине и дешифровать;
  4. если удалось запускаем общее преобразование;
  5. после чего выгружаем из сформированного BIO данные в массив байт;

Дешифрование данных
/**<summary>Дешифровывает данные</summary>
* <param name="_arInput">Данные для расшифровки</param>
* <param name="_arRes">Результат</param>
* <param name="_pLocation">Местоположение хранилища, где искать</param>
* <param name="_sError">Возвращаемая строка с ошибкой</param>
* <param name="_pCert">Сертификат</param>
* <returns>Стандартный код ошибки, если UCOnsts.S_OK то все ок</returns>
* **/
internal static int DecryptDataOS(byte[] _arInput, out X509Certificate2 _pCert, out byte[] _arRes, ref string _sError, 
                                  StoreLocation _pLocation = StoreLocation.CurrentUser ) {             
    _arRes = new byte[0];
    _pCert = null;
    uint   iFlag    = UCConsts.CMS_BINARY;
    IntPtr hBIORes  = IntPtr.Zero;
    IntPtr hCMS     = IntPtr.Zero;
    X509Certificate2 pCert;
    try {
        // 0) Чтение структуры CMS
        int iRes = GetCMSFromBytesOS(_arInput, out hCMS, ref _sError);
        if (iRes != UConsts.S_OK) return iRes;
        // 1) Прохоим по списку подписантов
        IntPtr hReceipts =  UOpenSSLAPI.CMS_get0_RecipientInfos(hCMS);
        int iCnt = UOpenSSLAPI.sk_num(hReceipts);                
        for(int i = 0; i < iCnt; i++) {                 
            IntPtr hRecep    = UOpenSSLAPI.sk_value(hReceipts, i);                    
            iRes = GetRecepInfoCertOS(hRecep, _pLocation, out pCert, ref _sError);
            if (iRes != UConsts.S_OK && iRes != UConsts.E_NO_CERTIFICATE) return iRes;
            // 1.1) Нет сертфиката 
            if (iRes == UConsts.E_NO_CERTIFICATE)  continue;
            ISDP_X509Cert pLocCert = ISDP_X509Cert.Convert(pCert);
            // 1.2) Нет зарытого ключа
            if (pLocCert.hOSKey == IntPtr.Zero)  continue;
            // 1.3) Установка ключа
            if (!UOpenSSLAPI.CMS_RecipientInfo_set0_pkey(hRecep, pLocCert.hOSKey))
                 return RetErrOS(ref _sError, UCConsts.S_OS_CMS_SET_DEC_KEY_ERR);
            try {
                // 1.4) Декодирование
                if (!UOpenSSLAPI.CMS_RecipientInfo_decrypt(hCMS, hRecep))
                    return RetErrOS(ref _sError, UCConsts.S_OS_CMS_REC_DEC_ERR);
            } finally {
                // !! Иначе два освобождения и ошибка
                UOpenSSLAPI.CMS_RecipientInfo_set0_pkey(hRecep, IntPtr.Zero);
            }
            // 1.5) Общее декодирование
            hBIORes = UOpenSSLAPI.BIO_new(UOpenSSLAPI.BIO_s_mem());
            if (!UOpenSSLAPI.CMS_decrypt(hCMS, IntPtr.Zero, pLocCert.hRealHandle, IntPtr.Zero, hBIORes, iFlag))
                return RetErrOS(ref _sError, UCConsts.S_OS_CMS_FULL_DEC_ERR);
            _pCert = pLocCert;
            // 2) Считываем полученные данные из BIO                    
            return ReadFromBIO_OS(hBIORes, out _arRes, ref _sError);
        }
        _sError = UCConsts.S_DEC_NO_CERT_ERR;
        return UConsts.E_NO_CERTIFICATE;
    } catch (Exception E) {
        _sError = UCConsts.S_DEC_GEN_ERR.Frm(E.Message);
        return UConsts.E_GEN_EXCEPTION;
    } finally {
        if(hBIORes != IntPtr.Zero) UOpenSSLAPI.BIO_free(hBIORes);
        if(hCMS != IntPtr.Zero) UOpenSSLAPI.CMS_ContentInfo_free(hCMS);
    }
}

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


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


Получение сертификата адресата
/**<summary>Получение сертификата адресата</summary>
* <param name="_hRecep">Информация об адресате</param>
* <param name="_pCert">Возврат сертификата</param>
* <param name="_pLocation">Расположения хранилища для поиска</param>
* <param name="_sError">Возвращаемая строкка с ошибкой</param>
* <returns>Стандартый код ошибки, если UConsts.S_OK то все ок</returns>
* **/
internal static int GetRecepInfoCertOS(IntPtr _hRecep, StoreLocation _pLocation,
                                       out X509Certificate2 _pCert, ref string _sError) {
    _pCert = null;
    try {
         // 0) Получаем информацию об адресате
         IntPtr hKey    = IntPtr.Zero;
         IntPtr hIssuer = IntPtr.Zero;
         IntPtr hSNO    = IntPtr.Zero;
         if (!UOpenSSLAPI.CMS_RecipientInfo_ktri_get0_signer_id(_hRecep, ref hKey,
                                                                ref hIssuer, ref hSNO))
             return RetErrOS(ref _sError, UCConsts.S_GET_RECEIP_INFO_ERR);
         // 1) Извлекается серийный номер
         string sSerial;
         int iRes = GetBinaryHexFromASNOS(hSNO, out sSerial, ref _sError);
         if(iRes != UConsts.S_OK) return iRes;
         // 2) Ищем сертификат
         iRes = FindCertificateOS(sSerial, out _pCert, ref _sError, _pLocation, 
                                  StoreName.My, X509FindType.FindBySerialNumber);
         if(iRes != UConsts.S_OK) return iRes;
         return UConsts.S_OK;
    } catch (Exception E) {
         _sError = UCConsts.S_GET_RECEIP_INFO_GEN_ERR.Frm(E.Message);
         return UConsts.E_GEN_EXCEPTION;
    }
}

Метод CMS_RecipientInfo_ktri_get0_signer_id извлекает информацию о сертификате адресата, а из структуры на которую ссылается переменная hSNO извлекается сам серийный номер уже описанным ранее методом. Остается найти сертификат по серийному номеру.


Cтруктура данных адресата, вообще говоря, может быть разной. Но при шифровании документов чаще всего встречается именно тип ktri — с хранением информации об открытых ключах. Для работы с другими типами OpenSSL тоже реализовало несколько методов: CMS_RecipientInfo_kari_*, CMS_RecipientInfo_kekri_* и метод CMS_RecipientInfo_set0_password для pwri.


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


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

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


Проверка сертификата
/**<summary>Проверить сертификат в рамках OpenSSL</summary>
* <param name="_iRevFlag">Флаг отзыва</param>
* <param name="_iRevMode">Режим отзыва</param>
* <param name="_hCert">контекст сертфиката</param>
* <param name="_rOnDate">Дата верификацмм</param>
* <param name="_pLocation">Местоположение проверки</param>
* <param name="_sError">Возвращаемая строка с ошибкой</param>
* <returns>Стандартый код ошибки, если UConsts.S_OK то все ок</returns>
* **/
internal static int VerifyCertificateOS(IntPtr _hCert, X509RevocationMode _iRevMode, X509RevocationFlag _iRevFlag,
                                        StoreLocation _pLocation, DateTime _rOnDate, ref string _sError) {
    IntPtr hStore   = IntPtr.Zero;
    IntPtr hStoreCtx = IntPtr.Zero;
    try {                                            
        // 0) Формируем хранилище
        int iRes = GetTrustStoreOS(_pLocation, out hStore, ref _sError);
        if(iRes != UConsts.S_OK) return iRes;
        // 1) Формируем контекст проверки
        hStoreCtx = UOpenSSLAPI.X509_STORE_CTX_new();
        if (!UOpenSSLAPI.X509_STORE_CTX_init(hStoreCtx, hStore, _hCert, IntPtr.Zero)) {
            _sError = UCConsts.S_CRYPTO_CONTEXT_CER_ERR;
            return UConsts.E_CRYPTO_ERR;
        }
        // 2) Устанавливаем дату проверки и доп флаги
        SetStoreCtxCheckDate(hStoreCtx, _rOnDate);
        // 3) Проверка
        if (!UOpenSSLAPI.X509_verify_cert(hStoreCtx)) {
            _sError = UCConsts.S_CRYPTO_CHAIN_CHECK_ERR.Frm(GetCertVerifyErr(hStoreCtx));
            return UConsts.E_CRYPTO_ERR;
        }
        return UConsts.S_OK;
    } finally {
        if (hStore != IntPtr.Zero) UOpenSSLAPI.X509_STORE_free(hStore);
        if (hStoreCtx != IntPtr.Zero) UOpenSSLAPI.X509_STORE_CTX_free(hStoreCtx);
    }
}

В сформированный контекст проверки (X509_STORE_CTX) необходимо прописать дату и флаги. Делать это удобнее в отдельном методе:


Установка даты и флагов проверки
/**<summary>Установить дату проверки сертификата</summary>
* <param name="_hStoreCtx">Контекст хранилища</param>
* <param name="_rDate">Дата</param>
* **/
public static void SetStoreCtxCheckDate(IntPtr _hStoreCtx, DateTime _rDate) {
    uint iFlags = UCConsts.X509_V_FLAG_USE_CHECK_TIME | UCConsts.X509_V_FLAG_X509_STRICT | 
                  UCConsts.X509_V_FLAG_CRL_CHECK_ALL;
    // Установка флагов
    UOpenSSLAPI.X509_STORE_CTX_set_flags(_hStoreCtx, iFlags);
    // Установка времени
    UOpenSSLAPI.X509_STORE_CTX_set_time(_hStoreCtx, iFlags, (uint)_rDate.ToUnix());
    // Установка обязательств - все верифифированны
    UOpenSSLAPI.X509_STORE_CTX_set_trust(_hStoreCtx, UCConsts.X509_TRUST_TRUSTED);
}

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


Заключение


Данный вариант реализации был протестирован, по алгоритму описанному в предыдущей статье. После устранения ранее описанной утечки в X509Certificate2 (mono) он стабильно работает без утечек памяти. Скорость соизмерима с работой библиотеки КриптоПро.


В целом же, для Windows в силу особенностей хранения и работы с закрытыми ключами эта библиотека не очень применима. Поэтому стоит скорее воспользоваться штатными процедурами криптографии. В Linux же, если закрыть глаза на необходимость выгрузки закрытых ключей, для не ГОСТовой криптографии это вполне рабочий вариант.


В тоже время КриптоПро уже выпустила CSP 5.0, в котором обещана поддержка RSA сертификатов. Судя по информации на их сайте, пока она еще не сертифицирована, но когда будет, то работу системы одновременно использующую и ГОСТовые ключи и RSA, наверное, логичнее строить на ней.


Ссылки


  1. OpenSSL 1.0.2 ManPages;
  2. Многопоточность в OpenSSL 1 и 2;
  3. Исходники OpenSSL:
    1. cms_smime.c;
  4. Wiki OpenSSL;
  5. Исходники mono:
    1. класс RSAManaged;

Комментарии (6)


  1. saipr
    19.09.2018 18:58

    OpenSSL позволяет формировать специальные файловые хранилища сертификатов

    А что мешает использовать для хранения сертификатов OpenSSL, тем более с ГОСТ-ами, базы данных, например, на SQLite3?..


    1. kovrovdv Автор
      19.09.2018 19:33

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


      1. saipr
        19.09.2018 20:10

        Попробуйте, тем более код есть.


  1. saipr
    19.09.2018 19:00

    В тоже время КриптоПро уже выпустила CSP 5.0, в котором обещана поддержка RSA сертификатов.

    А что в винде не хватает провайдеров с поддержкой RSA и почему не EC?


    1. kovrovdv Автор
      19.09.2018 19:49

      Речь шла о Linux варианте, чтобы не приходилось две ветки делать. Да и код в таком случае для обеих ОС (Linux/Windows) унифицированный получается. А в Windows да, проблем с RSA к счастью нет.


      1. saipr
        19.09.2018 20:15

        А в Linux какие проблемы. Посмотрите на Pgp. Пора забыть про всякие CSP и нормально использовать PKCS#11. MS CSP это как кодовая страница CP1251 с неопределенной ячейкой 0x98. Хоть бы знак Рубля туда вставили чтоли.