Дешифрование
Для того, чтобы дешифровать данные, на локальной машине в личных сертификатах пользователя или компьютера должен быть сертификат одного из адресатов. И к нему должен быть привязан закрытый ключ. Процесс проходит по уже привычному сценарию — список параметров, определение длины и сам процесс дешифрования:
/**<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 сертификат всегда возвращается пустой).
Извлечение информация из подписи
Часто в системах работающих с криптографией требуется печатное представление подписи. В каждом случае оно разное, поэтому лучше сформировать класс информации о подписи, который будет содержать информацию в удобном для использования виде и уже с его помощью обеспечивать печатное представление. В .
Сама по себе подпись содержит два основных элемента — список сертификатов и список подписантов. Список сертификатов может быть пустой, а может содержать в себе все сертификаты для проверки, включая полные цепочки. Список же подписантов указывает на кол-во реальных подписей.
Чтение подписи происходит следующим образом:
/**<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). Остается проверить что это реально подпись и получить сначала список сертификатов, а потом список подписантов. Список сертификатов извлекается последовательно :
Сначала определятся количество сертификатов из параметра 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. В ней легко найти серийный номер сертификата и по нему найти нужный сертификат в ранее извлеченном списке. Обратите внимание, что серийный номер содержится в обратном порядке.
После извлечения сертификата необходимо определить параметры подписи, самая важная из которых — дата подписания (даже если это не верифицированная сервером штампа даты времени, для отображения она очень важна).
Атрибуты представляют из себя вложенный справочник вида Oid – список значений (по сути это разобранная структура ASN.1). Пройдя по первому уровню формируем вложенный список:
Ключевой особенностью данного процесса является правильный подбор наследника Pkcs9AttributeObject. Проблема в том, что стандартный способ создания в mono не работает и приходится формировать выбор класса прямо в коде. К тому же из основных типов Mono на данный момент позволяет формировать только дату.
Обернув представленные выше методы в два класса — информация о подписи и информация о подписанте — получаем аналог SignedCms, из которой при формировании печатного вида извлекаем данные.
Подключение в коде
Несмотря на процесс переноса в 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 можно реализовывать почти единый код под обе системы.
Подписать документ с помощью криптопро 5.0 — уц айтиком
В КриптоПРО версии 5.0 добавлена возможность подписывать документы электронной подписью с помощью утилиты Инструменты КриптоПро. Данная утилита входит в состав пакета установки КриптоПро 5.0, устанавливать отдельно ее не нужно.
Скачать КриптоПро 5.0 можно по ссылке.
Для подписания документа:
1. Откройте меню «Пуск», в списке программ найдите папку КРИПТО-ПРО, откройте её. Запустите приложение Инструменты КриптоПро.
2. Перейдите во вкладку «Создание подписи», нажмите на кнопку «Выбрать файл для подписи».
3. В открывшемся окне выберите файл, который необходимо подписать. Нажмите кнопку «Открыть».
4. Под кнопками «Выбрать файл для подписи» и «Сохранить подпись как» отобразится путь исходного файла и путь подписанного файла, который в дальнейшем будет создан.
Обратите внимание! По умолчанию КриптоПро 5.0 подписывает файлы в формате .p7s. Если требуется подписать файл в другом формате (к примеру, .sig), необходимо в конце пути будущего подписанного файла заменить .p7s на .sig. Поддерживаемые форматы: .p7s .sig .sgn |
По умолчанию подписанный документ сохраняется в той же папке, где находится исходный.
5. Выберите, какой электронной подписью необходимо подписать документ.
6. Нажмите кнопку «Подписать«. После завершения процесса подписания Вы увидите сообщение «Создание подписи завершилось успехом«, а так же в указанной директории будет создан файл с указанным расширением (.p7s / .sig / .sgn) – это и есть подписанный файл.
Обратите внимание! По умолчанию КриптоПро 5.0 создает присоединённую подпись. Если Вам необходимо создать отсоединённую подпись, перед подписанием (шаг 6) нажмите кнопку «Показать расширенные» и поставьте галочку «Создать отсоединённую подпись». |
Подписанный документ вы можете проверить с помощью нашей инструкции
Для того, чтобы работала система автоматического одобрения заявок на основании заявления, подписанного электронной подписью, нужно, чтобы в поле заявления был загружен файл, подписанный прикрепленной электронной подписью.
Вам необходимо предоставить подписанное квалифицированной ЭП заявление и подписанную доверенность (в случаях выпуска на сотрудника).
Для прикрепления данных документов, необходимо перейти в заявку на выпуск подписи и выбрать «Анкета и документы».
Внимание! Документы не принимаются в случае:
— Если они находятся в архиве.
— Если они имеют расширение отличающееся от .sig, .p7s, .sgn.
— Документ должен быть подписан прикрепленной подписью, открепленные подписи не принимаются.
В открывшемся окне необходимо выбрать раздел «Документы».
Далее необходимо загрузить документы в правильные поля.
Подписанное заявление загружается в поле:
Необходимо нажать на данное поле, откроется окно выбора необходимого документа, выберите подписанное заявление и нажмите «ОК», либо перетяните документ прямо на данную иконку.
Подписанная доверенность загружается в поле:
Необходимо нажать на данное поле, откроется окно выбора необходимого документа, выберите подписанное заявление и нажмите «ОК», либо перетяните документ прямо на данную иконку.
Поиск сертификата
Работа с криптографией обычно начинается с поиска сертификата, для этого в 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.
Не стоит забывать, что это ссылка на область неуправляемой памяти и ее надо освобождать после окончания работы. Т.к. в .Net 4.5 X509Certificate2 не Disposable — очистку методом CertFreeCertificateContext, надо проводить в деструкторе.
Проверка подписи
Проверка подписи происходит в два этапа, в начале проверяется сама подпись, а затем проверяется сертификат, которым она была сформирована (цепочка, дата подписания и проч).Так же как и при подписании необходимо указать набор подписываемых данных, параметры подписи и саму подпись:
/**<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). После чего проверяется сама подпись и извлекается первый подписант (по хорошему надо было бы извлечь всех, но подпись содержащая несколько подписантов это все таки экзотика).
После извлечения сертификата подписанта преобразуем его в наш класс и проверяем (если это указано в параметрах метода). Для проверки используется дата подписания первого подписанта (см. раздел извлечение информации из подписи, и раздел проверка сертификата).
Проверка сертификата
Сертификат это не только открытый ключ, но еще и набор разной информации о его владельце, о том, кто его выдал и о наборе действий, которые с его помощью можно делать. Так же у сертификата есть период действия и возможность отзыва, в случае компрометации. Чаще всего под проверкой сертификата подразумевается следующее:
- целостность цепочки (сертификат издателя, сертификат издателя сертификата издателя, и т. п.);
- сертификат корневого издателя — должен быть в хранилище доверенных корневых центров;
- период действия всех сертификатов — момент использования сертификата должен быть в границах этого периода;
- каждый из сертификатов в цепочке, кроме корневого, должен отсутствовать в списке отозванных у своего издателя (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 по набору передаваемых флагов.
Формирование подписи
При работе с ГОСТовыми сертификатами почти всегда используются отцепленные подписи с одним подписантом. Для того чтобы создать такую подпись требуется довольно простой блок кода:
/**<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 оба варианта успешно работают.
Шифрование
Процесс шифрования во многом аналогичен процессу подписания, он довольно прост, и основная проблема так же состоит в определении алгоритма. В отличии от подписи шифрование чаще всего используются сцепленное в адрес одного или сразу нескольких адресатов (например, шифруют еще и в адрес себя, чтобы была возможность прочитать сообщение своим ключом).
/**<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). И если информация найдена — значит это наш алгоритм.
В случае если таких алгоритмов больше чем один можно так же фильтровать по размеру (опираясь на размер алгоритма хэширования).