В связи с переходом на Linux возникла необходимость переноса одной из наших серверных систем написанной на C# в Mono. Система работает с усиленными ЭЦП, поэтому одной из поставленных перед нами задач была проверка работоспособности ГОСТовых сертификатов от КриптоПро в mono. Сам КриптоПро уже довольно давно реализовал CSP под Linux, но первая же попытка использования показала, что нативные классы криптографии Mono (аналогичные тем, что есть в базовом .Net — X509Store, X509Certificate2 и проч.) не только не работают с ГОСТовыми ключами, они даже не видят их в своих хранилищах. В силу этого работу с криптографией пришлось подключать напрямую через библиотеки КриптоПро.
Мне нужно вычислить хэш-сумму используя ГОСТ Р 34.11-2012, а потом хэш-сумму подписать с помощью ГОСТ Р 34.10-2012. Подскажите какой командой сделать это на Linux? Сейчас я делаю следующие команды:
/opt/cprocsp/bin/amd64/cryptcp -dir /tmp -hash /tmp/hash_kOdlsl
/opt/cprocsp/bin/amd64/cryptcp -sign -dn E=myemail@inbox.ru /tmp/hash_kOdlsl.hsh
Вроде получаю два хеша, но при прохождении проверки получаю ответ “Подпись неверная”.
Заменил на следующие команды, мне кажутся они вернее, но ответ тот же:
/opt/cprocsp/bin/amd64/csptest -keyset -hash GOST12_256 -silent -cont '\\.\HDIMAGE\te-f9172e97-7dec-4b7c-a6d1-e11fadd1e993' -keytype exchange -in /tmp/hash_kOdlsl -hashout /tmp/hash_kOdlsl.hsh
/opt/cprocsp/bin/amd64/csptest -keyset -sign GOST12_256 -silent -cont '\\.\HDIMAGE\te-f9172e97-7dec-4b7c-a6d1-e11fadd1e993' -keytype exchange -in /tmp/hash_kOdlsl -out /tmp/hash_kOdlsl.sig
задан 16 окт 2020 в 21:31
Иван АнтоновИван Антонов
5572 серебряных знака15 бронзовых знаков
4
Вы нашли решение? Я заметил, что команда:
/opt/cprocsp/bin/amd64/csptest -keyset -hash GOST12_256 -silent -cont '\\.\HDIMAGE\te-f9172e97-7dec-4b7c-a6d1-e11fadd1e993' -keytype exchange -in /tmp/hash_kOdlsl -hashout /tmp/hash_kOdlsl.hsh
использует 34.10-2012 вместо 34.11-2012
ответ дан 23 окт 2020 в 17:38
1
Речь пойдет о файлах primary.key, masks.key и header.key, которые лежат в директории ххххх.000 на флешке. Данные файлы входят в состав криптоконтейнера закрытого ключа электронной подписи криптопровайдера КриптоПро, формат которого нигде не опубликован. Целью данной статьи является чтение контейнера и преобразование закрытого ключа в формат, который может быть прочитан в библиотеке OpenSSL. Долгое время было распространено ошибочное суждение, что достаточно сделать нечто вида (primary_key XOR masks_key) и мы получим закрытый ключ в чистом (raw) виде, однако забегая вперед, можно утверждать, что в КриптоПро было применено более сложное преобразование, в худшем случае состоящее из более чем 2000 (двух тысяч) операций хеширования.
Стоит упомянуть о существовании утилиты P12FromGostCSP которая позволяет конвертировать ключ в формат P12, доступный для работы с OpenSSL, но утилита имеет следующие существенные недостатки:
- Читает контейнер не напрямую, а через криптопровайдер, поэтому там, где кроме OpenSSL ничего нет, не работает.
- Если в свойствах ключа не отмечено, что ключ «экспортируемый», то конвертировать его невозможно.
- В демо версии не формирует файл с ключом, эта возможность присутствует только в платной версии.
Выберите, пожалуйста, ник на Пикабу
Отменить привязку?
Свяжите ник на Пикабу с аккаунтом , чтобы заходить в один аккаунт вводя логин и пароль или используя
Может быть, кому ни будь пригодиться. Недавно столкнулся с проблемой, когда при входе через IE (Internet Explorer), на любые площадки, где используется ЭЦП, система сообщает о критической ошибке и уходит в ребут.
В системных логах следующая ошибка:
«Ошибка проверки контрольной суммы. Файл: C:\Windows\SysWOW64\wininet.dll. Причиной ошибки может быть обновление операционной системы или некорректная установка КриптоПро CSP. Обновите контрольные суммы с помощью кнопки “Пересчитать хэши” в панели КриптоПро CSP на вкладке “Дополнительно” или пересчитайте контрольную сумму библиотеки в реестре, запустив с правами администратора: start cpverify -addreg -file “C:\Windows\SysWOW64\wininet.dll”»
И в английской версии возникает еще такая (в русской её по какой-то причине нет, хотя может логи проглючили):
«A critical system process, C:\Windows\system32\lsass.exe, failed with status code 255. The machine must now be restarted.»
Решается просто, необходимо обновить Крипто ПРО до версии 4.0.9944
P.S. Ошибка была на windows 7, 8.1 и 10. До обновления Крипто ПРО был версии 4.0.9842. Системные обновления везде были установлены.
Лучшие посты за сегодня
- Сборка утилиты конвертирования ключа
- Компиляция OpenSSL библиотеки
- Компиляция privkey
- Формирование файла закрытого ключа private. key
- Пользуемся закрытым ключом private. key для подписывания файла file. txt
- Проверяем подпись
- Установка сертификата
- Файл header. key
- Читаем закрытый ключ и конвертируем
- Подключение в коде
- Поиск сертификата
- Формирование подписи
- Проверка подписи
- Ссылки
- Файл masks. key
- Извлечение информация из подписи
- Шифрование
- Дешифрование
- Проверка сертификата
- Заключение
Сборка утилиты конвертирования ключа
Далее сборка исходников описана для Linux версии.
Версию для Windows можно скачать отсюда там же есть сертификаты и закрытый ключ для тестирования, для сборки потребуется бесплатный компилятор Borland C++ 5.5
Компиляция OpenSSL библиотеки
После скачивания и распаковки исходных текстов openssl в целевой директории выполняем команды:
./config
make
Получаем готовую библиотеку libcrypto.a в текущей директории.
Также потребуются заголовочные файлы из директорий engines/ccgost и include.
Компиляция privkey
gcc -o privkey -Iengines/ccgost -Iinclude privkey.c libcrypto.a -pthread -ldl
Формирование файла закрытого ключа private. key
./privkey /mnt/usbflash/lp-9a0fe.000
Тестовый закрытый ключ в криптоконтейнере lp-9a0fe.000, сертификат открытого ключа signer.cer и другие файлы для тестирования можно взять отсюда
Получаем результат работы:
-----BEGIN PRIVATE KEY-----
MEYCAQAwHAYGKoUDAgITMBIGByqFAwICJAAGByqFAwICHgEEIwIhAKzsrv/l1Uwk
uzph/LQN9mux0Jz0yaW21kOYEFv0Xyut
-----END PRIVATE KEY-----
Cохраняем в private.key
Пользуемся закрытым ключом private. key для подписывания файла file. txt
openssl cms -sign -inkey private.key -in file.txt -CAfile CA.cer -signer signer.cer -engine gost -out test.sign -outform DER -noattr -binary
Проверяем подпись
openssl cms -verify -content file.txt -in test.sign -CAfile CA.cer -signer signer.cer -engine gost -inform DER -noattr -binary
Все работает просто замечательно!
Спасибо за внимание. Это была моя первая статья на хабре.
Установка сертификата
Перед тем как реализовывать код, необходимо установить сертификат и убедится что он нормально работает.
Установка сертификата
Компонент КриптоПро 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 {файл подписи}
Если с сертификатом все нормально, то можно переходить к подключению в коде.
Файл header. key
Из этого файла нам потребуется параметры электронной подписи CryptoProParamSet (подчеркнуто красным).
- GostR3410_2001_CryptoPro_A_ParamSet — 1.2.643.2.2.35.1
- GostR3410_2001_CryptoPro_B_ParamSet — 1.2.643.2.2.35.2
- GostR3410_2001_CryptoPro_C_ParamSet — 1.2.643.2.2.35.3
- GostR3410_2001_CryptoPro_XchA_ParamSet — 1.2.643.2.2.36.0
- GostR3410_2001_CryptoPro_XchB_ParamSet — 1.2.643.2.2.36.1
А также первые 8 байт открытого ключа (подчеркнуто) для контроля правильности чтения закрытого.
Читаем закрытый ключ и конвертируем
Файл privkey.c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <openssl/pem.h>
#include <openssl/cms.h>
#include <openssl/err.h>
#include "gost_lcl.h"
/* Convert little-endian byte array into bignum */
BIGNUM *reverse32bn(char *b, BN_CTX *ctx)
{
BIGNUM *res;
char buf[32];
BUF_reverse(buf, b, 32);
res = BN_bin2bn(buf, 32, BN_CTX_get(ctx));
OPENSSL_cleanse(buf, sizeof(buf));
return res;
}
void xor_material(char *buf36, char *buf5C, char *src)
{
int i;
for(i = 0; i < 32; i++)
{
buf36[i] = src[i] ^ 0x36;
buf5C[i] = src[i] ^ 0x5C;
}
}
int make_pwd_key(char *result_key, char *start12, int start12_len, char *passw)
{
int result;
int i;
char pincode4[1024];
int pin_len;
char current[32];
char material36[32];
char material5C[32];
char hash_result[32];
gost_hash_ctx ctx;
init_gost_hash_ctx(&ctx, &GostR3411_94_CryptoProParamSet);
memset(pincode4, 0, sizeof(pincode4));
pin_len = strlen(passw);
if (pin_len*4 > sizeof(pincode4)) { result = 1; goto err; }
for(i = 0; i < pin_len; i++)
pincode4[i*4] = passw[i];
start_hash(&ctx);
hash_block(&ctx, start12, start12_len);
if (pin_len)
hash_block(&ctx, pincode4, pin_len * 4);
finish_hash(&ctx, hash_result);
memcpy(current, (char*)"DENEFH028.760246785.IUEFHWUIO.EF", 32);
for(i = 0; i < (pin_len?2000:2); i++)
{
xor_material(material36, material5C, current);
start_hash(&ctx);
hash_block(&ctx, material36, 32);
hash_block(&ctx, hash_result, 32);
hash_block(&ctx, material5C, 32);
hash_block(&ctx, hash_result, 32);
finish_hash(&ctx, current);
}
xor_material(material36, material5C, current);
start_hash(&ctx);
hash_block(&ctx, material36, 32);
hash_block(&ctx, start12, start12_len);
hash_block(&ctx, material5C, 32);
if (pin_len)
hash_block(&ctx, pincode4, pin_len * 4);
finish_hash(&ctx, current);
start_hash(&ctx);
hash_block(&ctx, current, 32);
finish_hash(&ctx, result_key);
result = 0; //ok
err:
return result;
}
BIGNUM *decode_primary_key(char *pwd_key, char *primary_key, BN_CTX *bn_ctx)
{
BIGNUM *res;
char buf[32];
gost_ctx ctx;
gost_init(&ctx, gost_cipher_list->sblock);
gost_key(&ctx, pwd_key);
gost_dec(&ctx, primary_key, buf, 4);
res = reverse32bn(buf, bn_ctx);
OPENSSL_cleanse(buf, sizeof(buf));
return res;
}
BIGNUM *remove_mask_and_check_public(char *oid_param_set8, BIGNUM *key_with_mask, BIGNUM *mask, char *public8, BN_CTX *ctx)
{
int result;
EC_KEY *eckey = NULL;
const EC_POINT *pubkey;
const EC_GROUP *group;
BIGNUM *X, *Y, *order, *raw_secret, *mask_inv;
char outbuf[32], public_X[32];
ASN1_OBJECT *obj;
int nid;
order = BN_CTX_get(ctx);
mask_inv = BN_CTX_get(ctx);
raw_secret = BN_CTX_get(ctx);
X = BN_CTX_get(ctx);
Y = BN_CTX_get(ctx);
if (!order || !mask_inv || !raw_secret || !X || !Y) { result = 1; goto err; }
obj = ASN1_OBJECT_create(0, oid_param_set8+1, *oid_param_set8, NULL, NULL);
nid = OBJ_obj2nid(obj);
ASN1_OBJECT_free(obj);
if (!(eckey = EC_KEY_new())) { result = 1; goto err; }
if (!fill_GOST2001_params(eckey, nid)) { result = 1; goto err; }
if (!(group = EC_KEY_get0_group(eckey))) { result = 1; goto err; }
if (!EC_GROUP_get_order(group, order, ctx)) { result = 1; goto err; }
if (!BN_mod_inverse(mask_inv, mask, order, ctx)) { result = 1; goto err; }
if (!BN_mod_mul(raw_secret, key_with_mask, mask_inv, order, ctx)) { result = 1; goto err; }
if (!EC_KEY_set_private_key(eckey, raw_secret)) { result = 1; goto err; }
if (!gost2001_compute_public(eckey)) { result = 1; goto err; }
if (!(pubkey = EC_KEY_get0_public_key(eckey))) { result = 1; goto err; }
if (!EC_POINT_get_affine_coordinates_GFp(group, pubkey, X, Y, ctx)) { result = 1; goto err; }
store_bignum(X, outbuf, sizeof(outbuf));
BUF_reverse(public_X, outbuf, sizeof(outbuf));
if (memcmp(public_X, public8, 8) != 0) { result = 1; goto err; }
result = 0; //ok
err:
if (eckey) EC_KEY_free(eckey);
if (result == 0) return raw_secret;
return NULL;
}
int file_length(char *fname)
{
int len;
FILE *f = fopen(fname, "rb");
if (f == NULL) return -1;
fseek(f, 0, SEEK_END);
len = ftell(f);
fclose(f);
return len;
}
int read_file(char *fname, int start_pos, char *buf, int len)
{
int read_len;
FILE *f = fopen(fname, "rb");
if (f == NULL) return 1;
if (start_pos) fseek(f, start_pos, SEEK_SET);
read_len = fread(buf, 1, len, f);
fclose(f);
if (read_len != len) return 1;
return 0; //ok
}
int get_asn1_len(unsigned char *buf, int *size_hdr)
{
int n, i, res;
int pos = 0;
if ((buf[pos]&0x80) == 0) {
*size_hdr = 1;
return buf[pos];
}
n = buf[pos++]&0x7f;
res = 0;
for(i = 0; i < n; i++) {
res = res*256 + buf[pos++];
}
*size_hdr = n+1;
return res;
}
#define MAX_HEADER 20000
int read_container(char *fpath, int flag2, char *salt12, char *primary_key, char *masks_key, char *public8, char *oid_param_set8)
{
int result;
char primary_path[1024+30];
char masks_path[1024+30];
char header_path[1024+30];
char header_buf[MAX_HEADER];
int header_len;
int i, len, pos, size_hdr;
if (strlen(fpath)>1024) { result = 1; goto err; }
sprintf(header_path, "%s/header.key", fpath);
if (flag2 == 0)
{
sprintf(primary_path, "%s/primary.key", fpath);
sprintf(masks_path, "%s/masks.key", fpath);
}
else
{
sprintf(primary_path, "%s/primary2.key", fpath);
sprintf(masks_path, "%s/masks2.key", fpath);
}
if (read_file(primary_path, 4, primary_key, 32)) { result = 1; goto err; }
if (read_file(masks_path, 4, masks_key, 32)) { result = 1; goto err; }
if (read_file(masks_path, 0x26, salt12, 12)) { result = 1; goto err; }
header_len = file_length(header_path);
if (header_len < 0x42 || header_len > MAX_HEADER) { result = 1; goto err; }
if (read_file(header_path, 0, header_buf, header_len)) { result = 1; goto err; }
//------------- skip certificate ---------------------------
pos = 0;
for(i = 0; i < 2; i++)
{
get_asn1_len(header_buf+pos+1, &size_hdr);
pos += size_hdr+1;
if (pos > header_len-8) { result = 2; goto err; }
}
//------------------ get oid_param_set8 -----------------------
#define PARAM_SET_POS 34
if (memcmp(header_buf+pos+PARAM_SET_POS, "\x6\x7", 2) != 0) { result = 2; goto err; }
memcpy(oid_param_set8, header_buf+pos+PARAM_SET_POS+1, 8);
//------------------ get public8 -----------------------
result = 2; //not found
pos += 52;
for(i = 0; i < 3; i++)
{
len = get_asn1_len(header_buf+pos+1, &size_hdr);
if (len == 8 && memcmp(header_buf+pos, "\x8a\x8", 2) == 0)
{
memcpy(public8,header_buf+pos+2,8);
result = 0; //ok
break;
}
pos += len+size_hdr+1;
if (pos > header_len-8) { result = 2; goto err; }
}
err:
OPENSSL_cleanse(header_buf, sizeof(header_buf));
return result;
}
#define START_OID 0x12
#define START_KEY 0x28
unsigned char asn1_private_key[72] = {
0x30,0x46,2,1,0,0x30,0x1c,6,6,0x2a,0x85,3,2,2,0x13,0x30,0x12,6,7,0x11,
0x11,0x11,0x11,0x11,0x11,0x11,6,7,0x2a,0x85,3,2,2,0x1e,1,4,0x23,2,0x21,0
};
int main(int argc, char **argv)
{
int result;
char *container_path;
char *passw;
char salt12[12];
char primary_key[32];
char masks_key[32];
char public8[8];
char oid_param_set8[8];
BN_CTX *ctx;
BIGNUM *key_with_mask;
BIGNUM *mask;
BIGNUM *raw_key;
char pwd_key[32];
char outbuf[32];
ctx = BN_CTX_new();
if (argc == 2)
{
container_path = argv[1];
passw = "";
}
else
if (argc == 3)
{
container_path = argv[1];
passw = argv[2];
}
else
{
printf("get_private container_path [passw]\n");
result = 1;
goto err;
}
if (read_container(container_path, 0, salt12, primary_key, masks_key, public8, oid_param_set8) != 0 &&
read_container(container_path, 1, salt12, primary_key, masks_key, public8, oid_param_set8) != 0)
{
printf("can not read container from %s\n", container_path);
result = 2;
goto err;
}
make_pwd_key(pwd_key, salt12, 12, passw);
key_with_mask = decode_primary_key(pwd_key, primary_key, ctx);
OPENSSL_cleanse(pwd_key, sizeof(pwd_key));
mask = reverse32bn(masks_key, ctx);
raw_key = remove_mask_and_check_public(oid_param_set8, key_with_mask, mask, public8, ctx);
if (raw_key)
{
BIO *bio;
store_bignum(raw_key, outbuf, sizeof(outbuf));
memcpy(asn1_private_key+START_OID, oid_param_set8, 8);
memcpy(asn1_private_key+START_KEY, outbuf, 32);
//bio = BIO_new_file("private.key", "w");
bio = BIO_new_fp(stdout, BIO_NOCLOSE | BIO_FP_TEXT);
PEM_write_bio(bio, "PRIVATE KEY", "", asn1_private_key, sizeof(asn1_private_key));
BIO_free(bio);
OPENSSL_cleanse(outbuf, sizeof(outbuf));
OPENSSL_cleanse(asn1_private_key, sizeof(asn1_private_key));
result = 0; //ok
}
else
{
printf("Error check public key\n");
result = 3;
}
err:
BN_CTX_free(ctx);
OPENSSL_cleanse(salt12, sizeof(salt12));
OPENSSL_cleanse(primary_key, sizeof(primary_key));
OPENSSL_cleanse(masks_key, sizeof(masks_key));
return result;
}
Небольшой комментарий.
Основную работу выполняют следующие 3 функции:
1. Создаем ключ хранения исходя из 12-ти байтовой «соли» и пароля.
make_pwd_key(pwd_key, salt12, 12, passw);
2. Расшифровываем основной ключ на ключе хранения.
key_with_mask = decode_primary_key(pwd_key, primary_key, ctx);
3. Делим ключ с маской на маску.
raw_key = remove_mask_and_check_public(oid_param_set8, key_with_mask, mask, public8, ctx);
Но так как в библиотеке OpenSLL операция деления по модулю традиционно отсутствует, пользуемся операцией взятия обратного числа и умножением.
if (!BN_mod_inverse(mask_inv, mask, order, ctx)) { result = 1; goto err; }
if (!BN_mod_mul(raw_secret, key_with_mask, mask_inv, order, ctx)) { result = 1; goto err; }
Подключение в коде
Несмотря на процесс переноса в 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 уже дело техники:
Получение 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). После чего проверяется сама подпись и извлекается первый подписант (по хорошему надо было бы извлечь всех, но подпись содержащая несколько подписантов это все таки экзотика).
После извлечения сертификата подписанта преобразуем его в наш класс и проверяем (если это указано в параметрах метода). Для проверки используется дата подписания первого подписанта (см. раздел извлечение информации из подписи, и раздел проверка сертификата).
Ссылки
- документация КриптоПро CAPILite
- ресурс c объявлением стандартных экспортируемых функций в С#
- исходники .Net:
- класс CAPIBase
- класс X509Certificate2
- класс SignedCMS
- класс SignerInfo
- исходники mono:
- класс X509Certificate2
- класс X509CertificateImplBtls
Файл masks. key
Содержит 32 байта маски ключа в формате Asn1, зашифрованного на ключе хранения pwd_key. Далее 12 байт «затравочной» информации для генерации ключа хранения pwd_key, если криптоконтейнер защищен паролем, то пароль также участвует в генерации ключа хранения.
Далее контрольная сумма (имитозащита) 4 байта. Контрольной информацией для простоты мы пользоваться не будем, общий контроль будет осуществляться путем генерации открытого ключа и сравнения первых 8 байт полученного ключа с соответствующим полем из файла header.key:
Извлечение информация из подписи
Часто в системах работающих с криптографией требуется печатное представление подписи. В каждом случае оно разное, поэтому лучше сформировать класс информации о подписи, который будет содержать информацию в удобном для использования виде и уже с его помощью обеспечивать печатное представление. В .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 сертификата.