В связи с переходом на Linux возникла необходимость переноса одной из наших серверных систем написанной на C# в Mono. Система работает с усиленными ЭЦП, поэтому одной из поставленных перед нами задач была проверка работоспособности ГОСТовых сертификатов от КриптоПро в mono. Сам КриптоПро уже довольно давно реализовал CSP под Linux, но первая же попытка использования показала, что нативные классы криптографии Mono (аналогичные тем, что есть в базовом .Net — X509Store, X509Certificate2 и проч.) не только не работают с ГОСТовыми ключами, они даже не видят их в своих хранилищах. В силу этого работу с криптографией пришлось подключать напрямую через библиотеки КриптоПро.
Установка сертификата
Перед тем как реализовывать код, необходимо установить сертификат и убедится что он нормально работает.
Компонент КриптоПро CSP версии 3.9 был установлен в Centos 7 в папку /opt/cprocsp. Для того, чтобы не было конфликтов между утилитами mono и КриптоПро, имеющих одинаковые названия (например, certmgr), в переменные окружения не стали вносить путь до папки и все утилиты вызывались по полному пути.
Для начала определяем список считывателей:
/opt/cprocsp/bin/amd64/csptest -enum -info -type PP_ENUMREADERS | iconv -f cp1251
Если среди списка нет считывателя с папки на диске (HDIMAGE) ставим его:
/opt/cprocsp/sbin/amd64/cpconfig -hardware reader -add HDIMAGE store
После чего можно создавать контейнеры вида '\\.\HDIMAGE\{имя контейнера}' путем либо создания нового контейнера с ключами:
/opt/cprocsp/bin/amd64/csptest -keyset -provtype 75 -newkeyset -cont '\\.\HDIMAGE\test'
либо формируя папку /var/opt/cprocsp/keys/root/{имя контейнера}.000, в которой располагается стандартный набор файлов контейнера КриптоПро (*.key, *.mask, и проч.).
После этого сертификат из контейнера можно установить в хранилище сертификатов:
/opt/cprocsp/bin/amd64/certmgr -inst mMy -cont '\\.\HDIMAGE\{имя контейнера}'
Установленный сертификат можно увидеть с помощью следующей команды:
/opt/cprocsp/bin/amd64/certmgr -list mMy
Работу сертификата можно проверить следующим образом:
/opt/cprocsp/bin/amd64/cryptcp – sign -norev -thumbprint {отпечаток} {файл} {файл подписи}
/opt/cprocsp/bin/amd64/cryptcp – verify -norev {файл подписи}
Если с сертификатом все нормально, то можно переходить к подключению в коде.
Подключение в коде
Несмотря на процесс переноса в Linux система должна была продолжать функционировать и в среде Windows, поэтому внешне работа с криптографией должна была осуществляться через общие методы вида «byte[] SignData(byte[] _arData, X509Certificate2 _pCert)», которые должны были одинаково работать как в Linux, так и в Windows.
Анализ методов библиотек криптографии оказался удачным, т. к. КриптоПро реализовало библиотеку «libcapi20.so» которая полностью мимикрирует под стандартные библиотеки Windows шифрования — «crypt32.dll» и «advapi32.dll». Возможно, конечно, не целиком, но все необходимые методы для работы с криптографии там в наличии, и почти все работают.
Поэтому формируем два статических класса «WCryptoAPI» и «LCryptoAPI» каждый из которых будет импортировать необходимый набор методов следующим образом:
[DllImport(LIBCAPI20, SetLastError = true)]
internal static extern bool CertCloseStore(IntPtr _hCertStore, uint _iFlags);
Синтаксис подключения каждого из методов можно либо сформировать самостоятельно, либо воспользоваться сайтом pinvoke, либо скопировать из исходников .Net (класс CAPISafe). Из этого же модуля можно почерпнуть константы и структуры связанные с криптографией, наличие которых всегда облегчают жизнь при работе с внешними библиотеками.
А затем формируем статический класс «UCryptoAPI» который в зависимости от системы будет вызывать метод одного из двух классов:
/**<summary>Закрыть хранилище</summary>
* <param name="_iFlags">Флаги (нужно ставить 0)</param>
* <param name="_hCertStore">Ссылка на хранилище сертификатов</param>
* <returns>Флаг успешности закрытия хранилища</returns>
* **/
internal static bool CertCloseStore(IntPtr _hCertStore, uint _iFlags) {
if (fIsLinux)
return LCryptoAPI.CertCloseStore(_hCertStore, _iFlags);
else
return WCryptoAPI.CertCloseStore(_hCertStore, _iFlags);
}
/**<summary>Находимся в линуксе</summary>**/
public static bool fIsLinux {
get {
int iPlatform = (int) Environment.OSVersion.Platform;
return (iPlatform == 4) || (iPlatform == 6) || (iPlatform == 128);
}
}
Таким образом используя методы класса UCryptoAPI можно реализовывать почти единый код под обе системы.
Поиск сертификата
Работа с криптографией обычно начинается с поиска сертификата, для этого в crypt32.dll имеется два метода CertOpenStore (открывает указанное хранилище сертификатов) и простой CertOpenSystemStore (открывает личные сертификаты пользователя). В силу того, что работа с сертификатами не ограничивается только личными сертификатами пользователя подключаем первый:
/**<summary>Поиск сертификата (первого удовлетворяющего критериям поиска)</summary>
* <param name="_pFindType">Тип поиска</param>
* <param name="_pFindValue">Значение поиска</param>
* <param name="_pLocation">Место </param>
* <param name="_pName">Имя хранилища</param>
* <param name="_pCert">Возвращаемый сертификат</param>
* <param name="_sError">Возвращаемая строка с ошибкой</param>
* <param name="_fVerify">Проверить сертфиикат</param>
* <returns>Стандартый код ошибки, если UConsts.S_OK то все ок</returns>
* **/
public static int FindCertificateCP(string _pFindValue, out X509Certificate2 _pCert, ref string _sError,
StoreLocation _pLocation = StoreLocation.CurrentUser,
StoreName _pName = StoreName.My,
X509FindType _pFindType = X509FindType.FindByThumbprint,
bool _fVerify = false) {
_pCert = null;
IntPtr hCert = IntPtr.Zero;
GCHandle hInternal = new GCHandle();
GCHandle hFull = new GCHandle();
IntPtr hSysStore = IntPtr.Zero;
try {
// 0) Открываем хранилище
hSysStore = UCryptoAPI.CertOpenStore(UCConsts.AR_CERT_STORE_PROV_SYSTEM[fIsLinux.ToByte()],
UCConsts.PKCS_7_OR_X509_ASN_ENCODING,
IntPtr.Zero,
UCUtils.MapX509StoreFlags(_pLocation, OpenFlags.ReadOnly),
UCConsts.AR_CRYPTO_STORE_NAME[(int)_pName]);
if (hSysStore == IntPtr.Zero) {
_sError = UCConsts.S_ERR_STORE_OPEN.Frm(Marshal.GetLastWin32Error());
return UConsts.E_CRYPTO_ERR;
}
// 1) Формируем данные в пакете
if ((_pFindType == X509FindType.FindByThumbprint) || (_pFindType == X509FindType.FindBySerialNumber))
{
byte[] arData = _pFindValue.FromHex();
CRYPTOAPI_BLOB cryptBlob;
cryptBlob.cbData = arData.Length;
hInternal = GCHandle.Alloc(arData, GCHandleType.Pinned);
cryptBlob.pbData = hInternal.AddrOfPinnedObject();
hFull = GCHandle.Alloc(cryptBlob, GCHandleType.Pinned);
} else {
byte[] arData;
if(fIsLinux)
arData = Encoding.UTF8.GetBytes(_pFindValue);
else
arData = Encoding.Unicode.GetBytes(_pFindValue);
hFull = GCHandle.Alloc(arData, GCHandleType.Pinned);
}
// 2) Получаем
IntPtr hPrev = IntPtr.Zero;
do {
hCert = UCryptoAPI.CertFindCertificateInStore(hSysStore,
UCConsts.PKCS_7_OR_X509_ASN_ENCODING, 0,
UCConsts.AR_CRYPT_FIND_TYPE[(int)_pFindType, fIsLinux.ToByte()],
hFull.AddrOfPinnedObject(), hPrev);
// 2.1) Освобождаем предыдущий
if(hPrev != IntPtr.Zero) UCryptoAPI.CertFreeCertificateContext(hPrev);
// 2.2) Кончились в списке
if(hCert == IntPtr.Zero) return UConsts.E_NO_CERTIFICATE;
// 2.3) Нашли и валиден
X509Certificate2 pCert = new ISDP_X509Cert(hCert);
if (!_fVerify || pCert.ISDPVerify()) {
hCert = IntPtr.Zero;
_pCert = pCert;
return UConsts.S_OK;
}
hPrev = hCert;
// Чтобы не очистило
hCert = IntPtr.Zero;
} while(hCert != IntPtr.Zero);
return UConsts.E_NO_CERTIFICATE;
} catch (Exception E) {
_sError = UCConsts.S_FIND_CERT_GEN_ERR.Frm(E.Message);
return UConsts.E_GEN_EXCEPTION;
} finally {
// Очищаем ссылки и закрываем хранилище
if(hInternal.IsAllocated) hInternal.Free();
if(hFull.IsAllocated) hFull.Free();
if (hCert != IntPtr.Zero) UCryptoAPI.CertFreeCertificateContext(hCert);
UCryptoAPI.CertCloseStore(hSysStore, 0);
}
}
Поиск происходит в несколько этапов:
- открытие хранилища;
- формирование структуры данных по которым ищем;
- поиск сертификата;
- если требуется, то проверка сертификата (описана в отдельном разделе);
- закрытие хранилища и освобождение структуры из пункта 2 (т. к. повсюду здесь идет работа с неуправляемой памятью .Net за нас ничего по очистке делать не будет);
В ходе поиска сертификатов есть несколько тонких моментов.
КриптоПро в Linux работает с ANSI строками, а в Windows с UTF8, поэтому:
- при подключении метода открытия хранилища в Linux необходимо параметру кода хранилища явно указать тип маршалинга [In, MarshalAs (UnmanagedType.LPStr)];
- передавая строку для поиска (например, по имени Subject) ее необходимо преобразовывать в набор байт различными кодировками;
- для всех констант криптования, у которых есть вариация по типу строки (например, 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 уже дело техники:
/**<summary>Получение OID алгоритма хэширования сертификату</summary>
* <param name="_hCertHandle">Хэндл сертификата</param>
* <param name="_sOID">Возвращаемый параметр OID</param>
* <param name="_sError">Возвращаемая строка с ошибкой</param>
* <returns>Стандартный код ошибки, если UConsts.S_OK то все ок</returns>
* **/
internal static int GetHashAlgoritmOID(IntPtr _hCertHandle, out string _sOID, ref string _sError) {
_sOID = "";
IntPtr hHashAlgInfo = IntPtr.Zero;
IntPtr hData = IntPtr.Zero;
try {
CERT_CONTEXT pContext = (CERT_CONTEXT)Marshal.PtrToStructure(_hCertHandle, typeof(CERT_CONTEXT));
CERT_INFO pCertInfo = (CERT_INFO)Marshal.PtrToStructure(pContext.pCertInfo, typeof(CERT_INFO));
// Извлекаем AlgID
// через UCryptoAPI.CertAlgIdToOID в Windows первый раз работает, второй падает
byte[] arData = BitConverter.GetBytes(UCryptoAPI.CertOIDToAlgId(pCertInfo.SignatureAlgorithm.pszObjId));
hData = Marshal.AllocHGlobal(arData.Length);
Marshal.Copy(arData, 0, hData, arData.Length);
// Поиск OID
hHashAlgInfo = UCryptoAPI.CryptFindOIDInfo(UCConsts.CRYPT_OID_INFO_ALGID_KEY,
hData,
UCConsts.CRYPT_HASH_ALG_OID_GROUP_ID);
if (hHashAlgInfo == IntPtr.Zero) {
_sError = UCConsts.S_NO_HASH_ALG_ERR.Frm( Marshal.GetLastWin32Error());
return UConsts.E_GEN_EXCEPTION;
}
CRYPT_OID_INFO pHashAlgInfo = (CRYPT_OID_INFO)Marshal.PtrToStructure(hHashAlgInfo, typeof(CRYPT_OID_INFO));
_sOID = pHashAlgInfo.pszOID;
return UConsts.S_OK;
} catch (Exception E) {
_sError = UCConsts.S_DETERM_HASH_ALG_ERR.Frm(E.Message);
return UConsts.E_GEN_EXCEPTION;
} finally {
Marshal.FreeHGlobal(hData);
}
}
Внимательно прочитав код можно удивится, что идентификатор алгоритма получается простым способом (CertOIDToAlgId) а Oid по нему — сложным (CryptFindOIDInfo). Логично было бы предположить использование либо оба сложных, либо оба простых способа, и в Linux оба варианта успешно работают. Однако в Windows сложный вариант получения идентификатора и простой получения OID работает нестабильно, поэтому стабильным решением будет вот такой странный гибрид.
Проверка подписи
Проверка подписи происходит в два этапа, в начале проверяется сама подпись, а затем проверяется сертификат, которым она была сформирована (цепочка, дата подписания и проч).
Так же как и при подписании необходимо указать набор подписываемых данных, параметры подписи и саму подпись:
/**<summary>Формирует стандартную сктруктуру для проверки подписи </summary>
* <returns>Структуру</returns>
* **/
internal static CRYPT_VERIFY_MESSAGE_PARA GetStdSignVerifyPar() {
CRYPT_VERIFY_MESSAGE_PARA pVerifyParams = new CRYPT_VERIFY_MESSAGE_PARA();
pVerifyParams.cbSize = (int)Marshal.SizeOf(pVerifyParams);
pVerifyParams.dwMsgEncodingType = UCConsts.PKCS_7_OR_X509_ASN_ENCODING;
pVerifyParams.hCryptProv = 0;
pVerifyParams.pfnGetSignerCertificate = IntPtr.Zero;
pVerifyParams.pvGetArg = IntPtr.Zero;
return pVerifyParams;
}
/**<summary>Проверяет подпись</summary>
* <param name="_arData">данные, которые было подписаны</param>
* <param name="_pSign">подпись</param>
* <param name="_pCert">сертификат</param>
* <param name="_sError">возвращаемая строка с ошибкой</param>
* <param name="_fVerifyOnlySign">Проверять только подпись</param>
* <param name="_pRevMode">Режим проверки сертификата</param>
* <param name="_pRevFlag">Флаг проверки сертфииката</param>
* <returns>Стандартый код ошибки, если UConsts.S_OK то все ок</returns>
* <remarks>Проверяется только первый подписант</remarks>
* **/
public static int CheckSignCP(byte[] _arData, byte[] _pSign, out X509Certificate2 _pCert, ref string _sError,
bool _fVerifyOnlySign = true,
X509RevocationMode _pRevMode = X509RevocationMode.Online,
X509RevocationFlag _pRevFlag = X509RevocationFlag.ExcludeRoot){
_pCert = null;
IntPtr pHData = Marshal.AllocHGlobal(_arData.Length);
GCHandle pCertContext = GCHandle.Alloc(IntPtr.Zero, GCHandleType.Pinned);
try {
Marshal.Copy(_arData, 0, pHData, _arData.Length);
CRYPT_VERIFY_MESSAGE_PARA pVerParam = UCUtils.GetStdSignVerifyPar();
// 0) Проверка подписи
bool fRes = UCryptoAPI.CryptVerifyDetachedMessageSignature(
ref pVerParam, // Параметры подтверждения
0, // Индекс подписанта
_pSign, // Подпись
_pSign.Length, // Длина подписи
1, // кол-во файлов на подпись
new IntPtr[1] { pHData }, // подписанные файлы
new int[1] { _arData.Length }, // Длины подписанных файлов
pCertContext.AddrOfPinnedObject());// Ссылка на сертификат
if (!fRes) {
_sError = UCConsts.S_SIGN_CHECK_ERR.Frm(Marshal.GetLastWin32Error().ToString("X"));
return UConsts.E_CRYPTO_ERR;
}
// 1) Извлечение сертфииката
_pCert = new ISDP_X509Cert((IntPtr)pCertContext.Target);
if (_pCert == null) {
_sError = UCConsts.S_SIGN_CHECK_CERT_ERR;
return UConsts.E_CRYPTO_ERR;
}
// 2) Проверка сертификата
if (!_fVerifyOnlySign) {
List<DateTime> pDates;
// 2.1) Получаем список дат
int iRes = GetSignDateTimeCP(_pSign, out pDates, ref _sError);
// 2.2) Верифицируем первый сертификат
iRes = _pCert.ISDPVerify(ref _sError, pDates[0], _pRevMode, _pRevFlag);
if (iRes != UConsts.S_OK) return iRes;
}
return UConsts.S_OK;
} catch (Exception E) {
_sError = UCConsts.S_SIGN_CHECK_ERR.Frm(E.Message);
return UConsts.E_GEN_EXCEPTION;;
} finally {
Marshal.FreeHGlobal(pHData);
if ((_pCert == null) && pCertContext.IsAllocated && ((IntPtr)pCertContext.Target != IntPtr.Zero))
UCryptoAPI.CertFreeCertificateContext((IntPtr)pCertContext.Target);
pCertContext.Free();
}
}
Для удобства процесс формирования структуры с параметрами вынесен в отдельный метод (GetStdSignVerifyPar). После чего проверяется сама подпись и извлекается первый подписант (по хорошему надо было бы извлечь всех, но подпись содержащая несколько подписантов это все таки экзотика).
После извлечения сертификата подписанта преобразуем его в наш класс и проверяем (если это указано в параметрах метода). Для проверки используется дата подписания первого подписанта (см. раздел извлечение информации из подписи, и раздел проверка сертификата).
Извлечение информация из подписи
Часто в системах работающих с криптографией требуется печатное представление подписи. В каждом случае оно разное, поэтому лучше сформировать класс информации о подписи, который будет содержать информацию в удобном для использования виде и уже с его помощью обеспечивать печатное представление. В .Net такой класс есть — SignedCms, однако его аналог в mono c подписями КритоПро, во первых отказывается работать, во вторых содержит модификатор sealed и в третьих почти все свойства у него закрыты на запись, поэтому придется формировать свой аналог.
Сама по себе подпись содержит два основных элемента — список сертификатов и список подписантов. Список сертификатов может быть пустой, а может содержать в себе все сертификаты для проверки, включая полные цепочки. Список же подписантов указывает на кол-во реальных подписей. Связь между ними осуществляется по серийному номеру и издателю (Issuer). Теоретически в одной подписи может быть два сертификата от разных издателей с одним серийным номером, но на практике этим можно пренебречь и искать только по серийному номеру.
Чтение подписи происходит следующим образом:
/**<summary>Расшифровать</summary>
* <param name="_arSign">Подпись</param>
* <param name="_sError">Возвращаемая строка с ошибкой</param>
* <returns>Стандартный код ошибки, если UConsts.S_OK то все ок</returns>
* **/
public int Decode(byte[] _arSign, ref string _sError) {
IntPtr hMsg = IntPtr.Zero;
// 0) Формируем информацию
try {
hMsg = UCryptoAPI.CryptMsgOpenToDecode(UCConsts.PKCS_7_OR_X509_ASN_ENCODING, UCConsts.CMSG_DETACHED_FLAG,
0, IntPtr.Zero, IntPtr.Zero, IntPtr.Zero);
if (hMsg == IntPtr.Zero) {
_sError = UCConsts.S_CRYP_MSG_FORM_ERR.Frm(Marshal.GetLastWin32Error());
return UConsts.E_CRYPTO_ERR;
}
// 1) Вносим сообщение
if (!UCryptoAPI.CryptMsgUpdate(hMsg, _arSign, (uint)_arSign.Length, true)) {
_sError = UCConsts.S_CRYP_MSG_SIGN_COPY_ERR.Frm(Marshal.GetLastWin32Error());
return UConsts.E_CRYPTO_ERR;
}
// 2) Проверяем тип (PKCS7 SignedData)
uint iMessType = UCUtils.GetCryptMsgParam<uint>(hMsg, UCConsts.CMSG_TYPE_PARAM);
if (UCConsts.CMSG_SIGNED != iMessType) {
_sError = UCConsts.S_CRYP_MSG_SIGN_TYPE_ERR.Frm(iMessType, UCConsts.CMSG_SIGNED);
return UConsts.E_CRYPTO_ERR;
}
// 3) Формируем список сертфикатов
fpCertificates = UCUtils.GetSignCertificates(hMsg);
// 4) Список подписантов
uint iSignerCount = UCUtils.GetCryptMsgParam<uint>(hMsg, UCConsts.CMSG_SIGNER_COUNT_PARAM);
for (int i = 0; i < iSignerCount; i++) {
ISDPSignerInfo pInfo = new ISDPSignerInfo();
fpSignerInfos.Add(pInfo);
int iRes = pInfo.Decode(hMsg, i, this, ref _sError);
if (iRes != UConsts.S_OK) return iRes;
}
return UConsts.S_OK;
} catch (Exception E) {
_sError = UCConsts.S_SIGN_INFO_GEN_ERR.Frm(E.Message);
return UConsts.E_GEN_EXCEPTION;
} finally {
if(hMsg != IntPtr.Zero) UCryptoAPI.CryptMsgClose(hMsg);
}
}
Разбор подписи происходит в несколько этапов, вначале формируется структура сообщения (CryptMsgOpenToDecode), затем в нее вносятся реальные данные подписи (CryptMsgUpdate). Остается проверить что это реально подпись и получить сначала список сертификатов, а потом список подписантов. Список сертификатов извлекается последовательно :
/**<summary>Получить коллекцию сертификатов по подписи </summary>
* <param name="_hMsg">Handle подписи</param>
* <returns>Коллекция сертификатов</returns>
* **/
internal static X509Certificate2Collection GetSignCertificates(IntPtr _hMsg) {
X509Certificate2Collection certificates = new X509Certificate2Collection();
uint iCnt = GetCryptMsgParam<uint>(_hMsg, UCConsts.CMSG_CERT_COUNT_PARAM);
for (int i = 0; i < iCnt; i++) {
IntPtr hInfo = IntPtr.Zero;
IntPtr hCert = IntPtr.Zero;
try {
uint iLen = 0;
if (!GetCryptMsgParam(_hMsg, UCConsts.CMSG_CERT_PARAM, out hInfo, out iLen)) continue;
hCert = UCryptoAPI.CertCreateCertificateContext(UCConsts.PKCS_7_OR_X509_ASN_ENCODING, hInfo, iLen);
if (hCert != IntPtr.Zero) {
certificates.Add(new ISDP_X509Cert(hCert));
hCert = IntPtr.Zero;
}
} finally {
if (hInfo != IntPtr.Zero) Marshal.FreeHGlobal(hInfo);
if (hInfo != IntPtr.Zero) Marshal.FreeHGlobal(hCert);
}
}
return certificates;
}
Сначала определятся количество сертификатов из параметра CMSG_CERT_COUNT_PARAM, а затем последовательно извлекается информация о каждом сертификате. Завершает процесс создания формирование контекста сертификата и на его основе самого сертификата.
Извлечение данных подписанта сложнее. В них содержится указание на сертификат и список параметров подписи (например, дата подписания). Процесс извлечения данных выглядит следующим образом:
/**<summary>Распарсить информацию из подписи</summary>
* <param name="_hMsg">Handler подписи</param>
* <param name="_iIndex">Индекс подписанта</param>
* <param name="_pSignedCms">Структура подписи</param>
* <param name="_sError">Возвращаемая строка с ошибкой</param>
* <returns>Стандартный код ошибки, если UConsts.S_OK то все ок</returns>
* **/
public int Decode(IntPtr _hMsg, int _iIndex, ISDPSignedCms _pSignedCms, ref string _sError) {
// 1) Определяем длину
uint iLen = 0;
// 2) Считываем
IntPtr hInfo = IntPtr.Zero;
try {
if (!UCryptoAPI.CryptMsgGetParam(_hMsg, UCConsts.CMSG_SIGNER_INFO_PARAM, (uint)_iIndex, IntPtr.Zero, ref iLen)) {
_sError = UCConsts.S_ERR_SIGNER_INFO_LEN.Frm(_iIndex, Marshal.GetLastWin32Error());
return UConsts.E_CRYPTO_ERR;
}
hInfo = Marshal.AllocHGlobal((int)iLen);
if (!UCryptoAPI.CryptMsgGetParam(_hMsg, UCConsts.CMSG_SIGNER_INFO_PARAM, (uint)_iIndex, hInfo, ref iLen)) {
_sError = UCConsts.S_ERR_SIGNER_INFO.Frm(_iIndex, Marshal.GetLastWin32Error());
return UConsts.E_CRYPTO_ERR;
}
CMSG_SIGNER_INFO pSignerInfo = (CMSG_SIGNER_INFO) Marshal.PtrToStructure(hInfo, typeof(CMSG_SIGNER_INFO));
// 2.1) Ищем сертификат
byte[] arSerial = new byte[pSignerInfo.SerialNumber.cbData];
Marshal.Copy(pSignerInfo.SerialNumber.pbData, arSerial, 0, arSerial.Length);
X509Certificate2Collection pLocCerts = _pSignedCms.pCertificates.Find(X509FindType.FindBySerialNumber,
arSerial.Reverse().ToArray().ToHex(), false);
if (pLocCerts.Count != 1) {
_sError = UCConsts.S_ERR_SIGNER_INFO_CERT.Frm(_iIndex);
return UConsts.E_NO_CERTIFICATE;
}
fpCertificate = pLocCerts[0];
fpSignedAttributes = UCUtils.ReadCryptoAttrsCollection(pSignerInfo.AuthAttrs);
return UConsts.S_OK;
} catch (Exception E) {
_sError = UCConsts.S_ERR_SIGNER_INFO_READ.Frm(_iIndex, E.Message);
return UConsts.E_GEN_EXCEPTION;
} finally {
if(hInfo != IntPtr.Zero) Marshal.FreeHGlobal(hInfo);
}
}
В ходе него сначала определяется размер структуры подписанта, а затем извлекается и сама структура CMSG_SIGNER_INFO. В ней легко найти серийный номер сертификата и по нему найти нужный сертификат в ранее извлеченном списке. Обратите внимание, что серийный номер содержится в обратном порядке.
После извлечения сертификата необходимо определить параметры подписи, самая важная из которых — дата подписания (даже если это не верифицированная сервером штампа даты времени, для отображения она очень важна).
/**<summary>Получить список атрибутов подписи</summary>
* <param name="_pAttrs">Структура атрибутов</param>
* <returns>Коллекция аттрибутов</returns>
* **/
internal static CryptographicAttributeObjectCollection ReadCryptoAttrsCollection(CRYPT_ATTRIBUTES _pAttrs) {
CryptographicAttributeObjectCollection pRes = new CryptographicAttributeObjectCollection();
for (int i = 0; i < _pAttrs.cAttr; i++) {
IntPtr hAttr = new IntPtr((long)_pAttrs.rgAttr + (i * Marshal.SizeOf(typeof(CRYPT_ATTRIBUTE))));
CRYPT_ATTRIBUTE pAttr = (CRYPT_ATTRIBUTE) Marshal.PtrToStructure(hAttr, typeof(CRYPT_ATTRIBUTE));
CryptographicAttributeObject pAttrInfo = new CryptographicAttributeObject(new Oid(pAttr.pszObjId),
GetAsnEncodedDataCollection(pAttr));
pRes.Add(pAttrInfo);
}
return pRes;
}
Атрибуты представляют из себя вложенный справочник вида Oid – список значений (по сути это разобранная структура ASN.1). Пройдя по первому уровню формируем вложенный список:
/**<summary>Сформировать объект коллекции нужного класса по имени</summary>
* <param name="_sName">Имя</param>
* <returns>Созданный объект</returns>
* **/
internal static Pkcs9AttributeObject Pkcs9AttributeFromOID(string _sName) {
switch (_sName) {
case UCConsts.S_SIGN_DATE_OID : return new Pkcs9SigningTime();
// case UConsts.S_CONTENT_TYPE_OID : return new Pkcs9ContentType(); ->> в Mono падает
// case UConsts.S_MESS_DIGEST_OID : return new Pkcs9MessageDigest();
default: return new Pkcs9AttributeObject();
}
}
/**<summary>Формирует коллекуцию ASN</summary>
* <param name="_pAttr">Структура</param>
* <returns>Коллекция</returns>
* **/
internal static AsnEncodedDataCollection GetAsnEncodedDataCollection (CRYPT_ATTRIBUTE _pAttr) {
AsnEncodedDataCollection pRes = new AsnEncodedDataCollection();
Oid pOid = new Oid(_pAttr.pszObjId);
string sOid = pOid.Value;
for (uint i = 0; i < _pAttr.cValue; i++) {
checked {
IntPtr pAttributeBlob = new IntPtr((long)_pAttr.rgValue + (i * Marshal.SizeOf(typeof(CRYPTOAPI_BLOB))));
Pkcs9AttributeObject attribute = new Pkcs9AttributeObject(pOid, BlobToByteArray(pAttributeBlob));
Pkcs9AttributeObject customAttribute = Pkcs9AttributeFromOID(sOid);
if (customAttribute != null) {
customAttribute.CopyFrom(attribute);
attribute = customAttribute;
}
pRes.Add(attribute);
}
}
return pRes;
}
Ключевой особенностью данного процесса является правильный подбор наследника Pkcs9AttributeObject. Проблема в том, что стандартный способ создания в mono не работает и приходится формировать выбор класса прямо в коде. К тому же из основных типов Mono на данный момент позволяет формировать только дату.
Обернув представленные выше методы в два класса — информация о подписи и информация о подписанте — получаем аналог SignedCms, из которой при формировании печатного вида извлекаем данные.
Шифрование
Процесс шифрования во многом аналогичен процессу подписания, он довольно прост, и основная проблема так же состоит в определении алгоритма. В отличии от подписи шифрование чаще всего используются сцепленное в адрес одного или сразу нескольких адресатов (например, шифруют еще и в адрес себя, чтобы была возможность прочитать сообщение своим ключом).
/**<summary>Зашифрованные данные</summary>
* <param name="_arInput">Данные для расшифровки</param>
* <param name="_pCert">Сертификат</param>
* <param name="_arRes">Результат</param>
* <param name="_sError">Возвращаемая строка с ошибкой</param>
* <returns>Стандартный код с ошибкой, если UConsts.S_OK то все ок</returns>
* **/
public static int EncryptDataCP(byte[] _arInput, X509Certificate2 _pCert, out byte[] _arRes, ref string _sError) {
_arRes = new byte[0];
try {
// 0) Инициализация параметров
CRYPT_ENCRYPT_MESSAGE_PARA pParams = new CRYPT_ENCRYPT_MESSAGE_PARA();
pParams.dwMsgEncodingType = UCConsts.PKCS_7_OR_X509_ASN_ENCODING;
pParams.ContentEncryptionAlgorithm.pszObjId = _pCert.getEncodeAlgirtmOid();
pParams.cbSize = Marshal.SizeOf(pParams);
// 1) Извлечение длины
int iLen = 0;
if (!UCryptoAPI.CryptEncryptMessage(ref pParams, 1, new IntPtr[] { _pCert.getRealHandle() },
_arInput, _arInput.Length, null, ref iLen)) {
_sError = UCConsts.S_CRYPT_ENCODE_LEN_ERR.Frm(Marshal.GetLastWin32Error());
return UConsts.E_CRYPTO_ERR;
}
// 2) Второй запрос реальное шифрование
_arRes = new byte[iLen];
if (!UCryptoAPI.CryptEncryptMessage(ref pParams, 1, new IntPtr[] {_pCert.getRealHandle() },
_arInput, _arInput.Length, _arRes, ref iLen)) {
_sError = UCConsts.S_CRYPT_ENCODE_ERR.Frm(Marshal.GetLastWin32Error());
return UConsts.E_CRYPTO_ERR;
}
return UConsts.S_OK;
} catch (Exception E) {
_sError = UCConsts.S_CRYPT_ENCODE_ERR.Frm(E.Message);
return UConsts.E_GEN_EXCEPTION;
}
}
Процесс шифрования происходит в три этапа — заполнение параметров, определение длины и наконец шифрование. Зашифрованные данные могут быть большие, вероятно, поэтому метод поддерживает режим двух вызовов.
В примере шифруется в адрес одного адресата, но путем добавления дополнительных сертификатов в массив и установке общего количества в параметры метода, можно увеличить число адресатов.
А вот с алгоритмом опять проблемы. В сертификате нет ни его, ни даже косвенных значений по которым его можно было бы определить (как удалось с алгоритмом подписи). Поэтому придется извлекать список поддерживаемых алгоритмов из провайдера:
/**<summary>Получение OID алгоритма шифрования сертификату</summary>
* <param name="_hCertHandle">Хэндл сертификата</param>
* <param name="_sOID">Возвращаемый параметр OID</param>
* <param name="_sError">Возвращаемая строка с ошибкой</param>
* <returns>Стандартный код ошибки, если UConsts.S_OK то все ок</returns>
* **/
internal static int GetEncodeAlgoritmOID(IntPtr _hCertHandle, out string _sOID, ref string _sError) {
bool fNeedRelease = false;
_sOID = "";
uint iKeySpec = 0;
IntPtr hCrypto = IntPtr.Zero;
try {
// 0) Получаем контекст провайдера
if (!UCryptoAPI.CryptAcquireCertificatePrivateKey(_hCertHandle, 0, IntPtr.Zero,
ref hCrypto, ref iKeySpec, ref fNeedRelease)) {
_sError = UCConsts.S_CRYPTO_PROV_INIT_ERR.Frm(Marshal.GetLastWin32Error());
return UConsts.E_CRYPTO_ERR;
}
uint iLen = 1000;
byte[] arData = new byte[1000];
uint iFlag = 1; // Инициализация
// 1) Проходим в цикле по алгоритмам
while (UCryptoAPI.CryptGetProvParam(hCrypto, UCConsts.PP_ENUMALGS, arData, ref iLen, iFlag)){
iFlag = 2; // Следующий
PROV_ENUMALGS pInfo = ConvertBytesToStruct<PROV_ENUMALGS>(arData);
// 2) Пытаемся получить OID в рамках алгоримтов шифрования
byte[] arDataAlg = BitConverter.GetBytes(pInfo.aiAlgid);
IntPtr hDataAlg = Marshal.AllocHGlobal(arDataAlg.Length);
try {
Marshal.Copy(arDataAlg, 0, hDataAlg, arDataAlg.Length);
IntPtr hHashAlgInfo2 = UCryptoAPI.CryptFindOIDInfo(UCConsts.CRYPT_OID_INFO_ALGID_KEY,
hDataAlg,
UCConsts.CRYPT_ENCRYPT_ALG_OID_GROUP_ID);
// 2.1) Нашли - возвращаем
if (hHashAlgInfo2 != IntPtr.Zero) {
CRYPT_OID_INFO pHashAlgInfo2 = (CRYPT_OID_INFO)Marshal.PtrToStructure(hHashAlgInfo2,
typeof(CRYPT_OID_INFO));
_sOID = pHashAlgInfo2.pszOID ;
return UConsts.S_OK;
}
} finally {
Marshal.FreeHGlobal(hDataAlg);
}
}
// 3) Не нашли - ошибка
_sError = UCConsts.S_NO_ENCODE_ALG_ERR;
return UConsts.E_CRYPTO_ERR;
} catch (Exception E) {
_sError = UCConsts.S_DETERM_ENCODE_ALG_ERR.Frm(E.Message);
return UConsts.E_GEN_EXCEPTION;
}finally {
if((hCrypto != IntPtr.Zero) && fNeedRelease) UCryptoAPI.CryptReleaseContext(hCrypto, 0);
}
}
В примере извлекается контекст закрытого ключа и по нему происходит поиск по алгоритмам. Но в этом списке находятся все алгоритмы (обмена ключей, хэширования, подписи, шифрования и проч.), поэтому надо отфильтровать только алгоритмы шифрования. Пытаемся по каждому извлечь информацию ограничившись группой алгоритмов шифрования (UCConsts.CRYPT_ENCRYPT_ALG_OID_GROUP_ID). И если информация найдена — значит это наш алгоритм.
В случае если таких алгоритмов больше чем один можно так же фильтровать по размеру (опираясь на размер алгоритма хэширования).
Дешифрование
Для того, чтобы дешифровать данные, на локальной машине в личных сертификатах пользователя или компьютера должен быть сертификат одного из адресатов. И к нему должен быть привязан закрытый ключ. Процесс проходит по уже привычному сценарию — список параметров, определение длины и сам процесс дешифрования:
/**<summary>Дешифровывает данные</summary>
* <param name="_arInput">Данные для расшифровки</param>
* <param name="_arRes">Результат</param>
* <param name="_sError">Возвращаемая строка с ошибкой</param>
* <param name="_pCert">Сертификат</param>
* <returns>Стандартный код ошибки, если UCOnsts.S_OK то все ок</returns>
* **/
public static int DecryptDataCP(byte[] _arInput, out X509Certificate2 _pCert, out byte[] _arRes, ref string _sError) {
_arRes = new byte[0];
_pCert = null;
IntPtr hSysStore = UCryptoAPI.CertOpenSystemStore(IntPtr.Zero, UCConsts.AR_CRYPTO_STORE_NAME[(int)StoreName.My]);
GCHandle GC = GCHandle.Alloc(hSysStore, GCHandleType.Pinned);
IntPtr hOutCertL = IntPtr.Zero;
IntPtr hOutCert = IntPtr.Zero;
try {
// 0) Подготовка параметров
CRYPT_DECRYPT_MESSAGE_PARA pParams = new CRYPT_DECRYPT_MESSAGE_PARA();
pParams.dwMsgAndCertEncodingType = UCConsts.PKCS_7_OR_X509_ASN_ENCODING;
pParams.cCertStore = 1;
pParams.rghCertStore = GC.AddrOfPinnedObject();
pParams.cbSize = Marshal.SizeOf(pParams);
int iLen = 0;
// 1) Первый вызов определяем длину
if (!UCryptoAPI.CryptDecryptMessage(ref pParams, _arInput, _arInput.Length,
null, ref iLen, ref hOutCertL)) {
_sError = UCConsts.S_DECRYPT_LEN_ERR.Frm(Marshal.GetLastWin32Error());
return UConsts.E_CRYPTO_ERR;
}
// 2) Второй вызов дешифруем
_arRes = new byte[iLen];
if (!UCryptoAPI.CryptDecryptMessage(ref pParams, _arInput, _arInput.Length,
_arRes, ref iLen, ref hOutCert)) {
_sError = UCConsts.S_DECRYPT_ERR.Frm(Marshal.GetLastWin32Error());
return UConsts.E_CRYPTO_ERR;
}
// 3) Если есть вытаскиваем сертификат
if (hOutCert != IntPtr.Zero) _pCert = new ISDP_X509Cert(hOutCert);
if(_pCert != null) hOutCert = IntPtr.Zero;
// Все ок возвращаем
return UConsts.S_OK;
} catch (Exception E) {
_sError = UCConsts.S_DECRYPT_ERR.Frm(E.Message);
return UConsts.E_GEN_EXCEPTION;
} finally {
if (hOutCertL != IntPtr.Zero) UCryptoAPI.CertFreeCertificateContext(hOutCertL);
if (hOutCert != IntPtr.Zero) UCryptoAPI.CertFreeCertificateContext(hOutCert);
GC.Free();
UCryptoAPI.CertCloseStore(hSysStore, 0);
}
}
При установке параметров указывается хранилище, из которых система будет пытаться извлечь подходящий сертификат с ключом. В результате работы система выдаст дешифрованные данные и сертификат, который был использован (в Linux сертификат всегда возвращается пустой).
Проверка сертификата
Сертификат это не только открытый ключ, но еще и набор разной информации о его владельце, о том, кто его выдал и о наборе действий, которые с его помощью можно делать. Так же у сертификата есть период действия и возможность отзыва, в случае компрометации. Чаще всего под проверкой сертификата подразумевается следующее:
- целостность цепочки (сертификат издателя, сертификат издателя сертификата издателя, и т. п.);
- сертификат корневого издателя — должен быть в хранилище доверенных корневых центров;
- период действия всех сертификатов — момент использования сертификата должен быть в границах этого периода;
- каждый из сертификатов в цепочке, кроме корневого, должен отсутствовать в списке отозванных у своего издателя (CRL);
По хорошему надо еще проверять и права подписи, но в реальной жизни это делается редко.
Как уже понятно из введения, проверка сертификата на валидность, одна из самых сложных задач. Именно поэтому в библиотеке масса методов для реализации каждого из пунктов в отдельности. Поэтому, для упрощения обратимся к исходникам .Net для метода X509Certificate2.Verify() и возьмем их за основу.
Проверка состоит из двух этапов:
- сформировать цепочку сертификатов вплоть до корневого;
- проверить каждый из сертификатов в ней (на отзыв, время и проч.);
Такая проверка должна осуществляться перед подписанием и шифрованием на текущую дату, и в момент проверки подписи на дату подписания. Сам метод проверки небольшой:
/**<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 по набору передаваемых флагов.
Заключение
Набор реализованных методов работы с криптографией был подвергнут нагрузочному тестированию по схеме цикла полной работы:
- ожидание 10 мс;
- извлечение сертификата;
- подписание byte[] {1, 2, 3, 4, 5};
- проверка полученной подписи;
- извлечение параметров подписи;
- шифрование byte[] {1, 2, 3, 4, 5};
- дешифрование полученных данных;
Данный цикл был запущен в Windows и в Linux в 1-ом, 10-и и 50-и потоках, чтобы проверить работу в Linux сразу в нескольких потоках. Приложение в Linux работало стабильно в течении какого-то времени во много-поточном режиме (и чем больше потоков, тем меньше по времени), а затем «вставало» наглухо. Что свидетельствует о наличии взаимной блокировки (deadlock-е) в библиотеке (при нарушении работы с потоками связанных с разделяемым доступом обычно происходит падение с «Access Violation»).
По этой причине для обеспечения стабильности работы все методы класса UCryptoAPI стоит обрамить критической секцией. Для этого добавляем поле fpCPSection типа object после чего в каждый вызов добавляем следующую конструкцию:
private static object fpCPSection = new object();
/**<summary>Закрывает сообщение</summary>
* <param name="_hCryptMsg">Указатель на сообщение</param>
* **/
internal static bool CryptMsgClose(IntPtr _hCryptMsg) {
lock (pCPSection) {
if (fIsLinux)
return LCryptoAPI.CryptMsgClose(_hCryptMsg);
else
return WCryptoAPI.CryptMsgClose(_hCryptMsg);
}
}
/**<summary>Критическая секция для работы с КриптоПро</summary>**/
public static object pCPSection {
get { return fpCPSection;}
}
Это замедляет работу, поэтому желающие могут обрамлять критической секцией только вызов Linux- варианта.
Нагрузочное тестирование так же показало утечки памяти в mono при обращении к полям Issuer и Subject сертификата. Утечка, вероятно, происходит при попытке mono сформировать классы X500DistinguishedName для подписанта и издателя. К счастью, mono посчитали этот процесс достаточно ресурсоемким (или же они знают об утечке), поэтому предусмотрели кэширование результата данного формирования во внутренние поля сертификата (impl.issuerName и impl.subjectName). Поэтому данная утечка лечится прямой записью через отражение (Reflection) в эти поля экземпляров класса X500DistinguishedName, сформированных на базе значений из структуры CERT_CONTEXT сертификата.
Ссылки
- документация КриптоПро CAPILite
- ресурс c объявлением стандартных экспортируемых функций в С#
- исходники .Net:
- класс CAPIBase
- класс X509Certificate2
- класс SignedCMS
- класс SignerInfo
- исходники mono:
- класс X509Certificate2
- класс X509CertificateImplBtls