09.07.2007 8:16:10Применение CSP КриптоПро Ответов: 15
Сергей
Здравствуйте! Большая просьба – ответьте, пожалуйста, на ряд вопросов, связанных с применением CSP КриптоПро.

Исходные данные:
- WindowsXP со всеми последними обновлениями;
- КриптоПро CSP KC1 3.0.3300.1;
- Считыватель AKS ifdh, версии 3.0.3293.1;
- Считыватель Athena ASEDrive IIIe USB, версии 3.0.3293.1;
- eToken_PRO32, версии 3.0.15.0;
- eToken PRO USB-ключ (CardOS/M4.01);
- Считыватель Athena ASEDrive IIIe USB V2 + eToken PRO Смарт-карта (CardOS/M4.01).

Разрабатывается программа, которая должна обеспечивать:
- во время работы оператор вводит серию команд. Текстовое представление каждой команды подписывается и сохраняется в БД. За смену таких серий может быть несколько. Пароль доступа к носителю личного ключа должен вводиться перед изданием первой серии, а далее – при необходимости, если зафиксировано извлечение носителя. Окно ввода пароля от криптосистемы использоваться не должно;
- выбор издателя и криптопровайдера остается за заказчиком. Предполагается, что это будут КриптоПро и ГОСТ, но возможны варианты (..? в отдаленном будущем);
- в качестве носителей личных ключей предполагается использование Реестра (на время опытной эксплуатации) и eToken PRO (как USB-ключ, так и Смарт-карта);
- в некоторых подразделениях заказчика возможно отсутствие квалифицированных администраторов.

Для отработки на тестовом центре сертификации КриптоПро был получен ряд сертификатов для разных носителей:
- Crypto-Pro GOST R 34.10-2001 Cryptographic Service Provider – Реестр, USB-ключ, Смарт-карта;
- Crypto-Pro GOST R 34.10-2001 KC1 CSP – Реестр, USB-ключ, Смарт-карта;
- eToken Base Cryptographic Provider - USB-ключ, Смарт-карта;
- Microsoft Base Cryptographic Provider v1.0 – Реестр.

Уже для перечисленных криптопровайдеров унифицировать код в полном объеме невозможно. Но хотелось бы обойтись минимумом…

В штатном коде используется флаг CRYPT_SILENT.

1.
При получении дескриптора криптопровайдера, если носитель – Реестр, все проходит штатно.

Если носитель - USB-ключ, то необходимо сначала получить список носителей (обращение к функции CryptoAPI CryptGetProvParam с параметром PP_ENUMREADERS от CSP КриптоПро), а затем, сформировав FQCN-имя, пытаться получить дескриптор на каждом из них (из форума КриптоПро). Если же флаг CRYPT_SILENT не указывать, то все работает штатно и без указания FQCN-имени, хотя и мелькает окно выбора носителя.

Вопрос:
Можно ли ожидать, что в будущих версиях это будет реализовано прозрачно – при наличии флага CRYPT_SILENT поиск носителя будет выполняться внутри функции? Тем более, что такое решение реализует криптопровайдер eToken. Что, в свою очередь, может обеспечить унификацию кода.

Если носитель – Смарт-карта, то приведенный выше подход не срабатывает! Обращение с уникальным FQCN-именем (\\.\Athena ASEDrive IIIe USB 0\SCARD\ETOKEN_PRO32_2533c7052920\CC00\D0AC) возвращает код ошибки NTE_BAD_KEYSET_PARAM (0x8009001f). Если флаг CRYPT_SILENT не указывать, то все работает (с мельканием окна). По-видимому, это ошибка.

Вопрос:
Когда можно ждать исправление этой ошибки и как ее обойти сейчас?

2.
При выполнении следующих действий:
- получаем контекст сертификата;
- получаем дескриптор криптопровайдера для контейнера ключа;
- устанавливаем пароль доступа к контейнеру ключа (носителю);
- выполняем подпись 1;
- извлекаем носитель;
- выполняем подпись 2.
Последний шаг выполняется успешно.
В аналогичной ситуации криптопровайдер eToken фиксирует ошибку – NTE_BAD_KEYSET (0x80090016).

Вопрос:
Данная ситуация, видимо, связана с кэшированием и с CSP КриптоПро. Каких-либо настроек, отвечающих за такой режим, я не нашел. Что здесь можно сделать?

3.
В случае задания неправильного пароля CSP КриптоПро ошибку не фиксирует, даже если носитель извлечен. Ошибка фиксируется только при выполнении подписи. В CryptoAPI никаких средств проверки наличия носителя и правильности задания пароля (готовности к выполнению подписи) как будто нет. Есть непосредственно в реализации CSP КриптоПро, но тогда опять возникает вопрос с унификацией кода. Выполнять же фиктивную операцию подписи не хотелось бы.
Криптопровайдер eToken позволяет разрешить эту проблему хотя бы на уровне задания пароля – коды ошибок при отсутствии носителя и неверном пароле различаются – соответственно NTE_BAD_UID и SCARD_W_WRONG_CHV.

Вопрос:
Что здесь можно сделать? Можно ли в будущем ждать решения этой проблемы?

4.
При получении сертификата от текстового центра КриптоПро для подписи кода формируется личный ключ, который определяется как AT_KEYEXCHANGE. Если использовать стандартную утилиту makecert.exe (makecert -n "CN=Griboedov" -sk containerG02 -sr currentuser -ss My -cy end -eku 1.3.6.1.5.5.7.3.3), то созданный личный ключ определяется как AT_SIGNATURE.

Вопрос:
Что здесь правильно и почему?
 
Ответы:
09.07.2007 12:09:00Василий
0. Можно узнать - почему не используются функции, работающие с сертификатами (т.е. почему нужно прикладному ПО самостоятельно перечислять контейнеры, открывать их и задавать на них пароли)? И особенно интересно - почему самому CSP нельзя рисовать окошки с запросом ПИНа?

1. В CSP 3.6 ненужные окошки не возникают. Т.е. если контейнер находится на подключенном носителе, то окошка "вставить носитель" не будет.
Но, и в CSP 3.0 в случае использования fqcn-имён при вызове CryptAcquireContext окошек быть не должно. Это проверено.
Можно уточнить - при вызове какой именно функции возникает мелькание окна и каковы точные значения всех параметров этой функции (а также - какие на этой машине настроены считыватели и носители в панели КриптоПро CSP, и открывался ли ранее данный контейнер)?

2. Не имеет никакого отношения к кешированию.
Отличие в том, что для "eToken base CSP" при вынимании етокена вообще становится недоступным сам CSP, т.к. реализация криптоалгоритмов аппаратная.
"КриптоПро CSP" считывает ключ с токена и хранит его в памяти до тех пор, пока контекст этого контейнера не будет закрыт (CryptReleaseContext). Поскольку Вы не закрываете контекст - разумеется, ключ будет доступен и без носителя.

3. Функционирует как спроектировано - функция CryptSetProvParam с параметром задания пароля/ПИН-кода не проверяет правильность заданного пароля. Собственно говоря, даже если бы и проверяла - то это бы не решило "проблему готовности к выполнению подписи", т.к. носитель можно извлечь после задания пароля, но до первого обращения к секретному ключу при подписи. Если бы не было флажка SILENT - сам CSP сказал бы о факте ввода неправильного пароля или об отсутствии носителя при попытке подписи.

4. Зависит от приложения. Большинство клиентских приложений используют ключ AT_KEYEXCHANGE как для обмена ключами при шифровании, так и для подписи. В MSDN это описано.
В контейнере может быть:
а) один ключ - AT_KEYEXCHANGE
б) один ключ - AT_SIGNATURE
в) оба ключа
при этом для каждого ключа может быть свой сертификат.
11.07.2007 9:00:52Сергей
0. У нас есть две причины для отказа от окон криптосистемы:
- из API мы используем только само окно. Всю остальную графику написали сами в расчете на свои задачи. Этим мы подняли производительность, но полностью изменили всю логику работы с окнами. Поэтому, когда появляются чужие окна, все, конечно, работает, но … «осадок остается»;
- вторая причина более серьезная. Текстовое представление акцепта команды (принять/отклонить) также подписывается и сохраняется. Акцепт «очень хороших» и «очень плохих» команд выполняется на сервисе в автоматическом режиме. Соответственно, каких-либо окон там быть не должно.

1. Если указать флаг CRYPT_SILENT, то никаких окон и не появляется. Но в этом случае никак не получается выполнить CryptAcquireContext для контейнера ключа на смарт-карте. Для USB-ключа такой проблемы нет. Ошибка? Или для смарт-карты уникальное FQCN-имя должно формироваться как-то иначе? (Лог приведен перед примером кода.)
Без установки флага CRYPT_SILENT все работает, но при обращении к CryptAcquireContext появляется окно с текстом «Вставьте ключевой носитель …». Через пару секунд оно исчезает. Данные подписываются. То есть, на смарт-карте все есть и все правильно. При этом перебор носителей не требуется. Все выполняется внутри функции. Но тогда так должно быть и при указании флага CRYPT_SILENT.
Фрагмент отладочного примера - в конце сообщения (многое опущено, реально все организовано в библиотеку). Если требуется, могу выслать весь отладочный пример (390 строк).
Все считыватели и носители перечислены в начале первого обращения.

2. Вопрос снимается. Будем закрывать контекст.

3. Вообще, привязывание разработчика к интерфейсу – это не очень хорошо. Приложение может работать и на каком-нибудь промышленном компьютере, и интерфейс у него будет совсем не оконный (питающее напряжение промодулированное азбукой «Морзе» :)). Это, может быть, будет и не всегда по ГОСТ, но средства криптографии можно использовать более широко, чем это принято сейчас (например, внутри предприятия).
Понятно, что и у Microsoft еще не все устоялось (может быть, в Windows Vista это более стандартизовано - я не смотрел). Но мы и предлагаем сделать шаг навстречу разработчику.
Если функция установки пароля будет возвращать результат операции, то это позволяет унифицировать код. Все остальные криптопровайдеры так делают. Установка пароля может выполняться непосредственно перед операцией подписи. Правда, в этом случае придется в том или ином виде хранить пароль внутри приложения, но на это можно пойти.

4. Вопрос снимается. Запрос сертификата на тестовом центре с указанием «Использование ключей»/«Подпись» действительно задает значение AT_SIGNATURE (указание «Использование ключей»/«Оба» - задает только AT_KEYEXCHANGE).


// Лог ////////////////////////////////////

//Gost, USB-key, CRYPT_SILENT установлен

Regular: ProvType = 75; ProvName = Crypto-Pro GOST R 34.10-2001 Cryptographic Service Provider; KeyContName = SCARD\ETOKEN_PRO32_4cce1f14\CC03\8F18
Error from regular ::CryptAcquireContext = 8009001f
Attempt: ProvType = 75; ProvName = Crypto-Pro GOST R 34.10-2001 Cryptographic Service Provider; KeyContName = \\.\REGISTRY\SCARD\ETOKEN_PRO32_4cce1f14\CC03\8F18: ErrCode = 80090019
Attempt: ProvType = 75; ProvName = Crypto-Pro GOST R 34.10-2001 Cryptographic Service Provider; KeyContName = \\.\Athena ASEDrive IIIe USB 0\SCARD\ETOKEN_PRO32_4cce1f14\CC03\8F18: ErrCode = 8009001f
Attempt: ProvType = 75; ProvName = Crypto-Pro GOST R 34.10-2001 Cryptographic Service Provider; KeyContName = \\.\AKS ifdh 1\SCARD\ETOKEN_PRO32_4cce1f14\CC03\8F18: ErrCode = 80100069
Attempt: ProvType = 75; ProvName = Crypto-Pro GOST R 34.10-2001 Cryptographic Service Provider; KeyContName = \\.\AKS ifdh 0\SCARD\ETOKEN_PRO32_4cce1f14\CC03\8F18: OK
Yes among readers!!!
signatureA - OK
signatureB - OK


//Gost, SmartCard, CRYPT_SILENT установлен

Regular: ProvType = 75; ProvName = Crypto-Pro GOST R 34.10-2001 Cryptographic Service Provider; KeyContName = SCARD\ETOKEN_PRO32_2533c7052920\CC00\D0AC
Error from regular ::CryptAcquireContext = 8009001f
Attempt: ProvType = 75; ProvName = Crypto-Pro GOST R 34.10-2001 Cryptographic Service Provider; KeyContName = \\.\REGISTRY\SCARD\ETOKEN_PRO32_2533c7052920\CC00\D0AC: ErrCode = 80090019
Attempt: ProvType = 75; ProvName = Crypto-Pro GOST R 34.10-2001 Cryptographic Service Provider; KeyContName = \\.\Athena ASEDrive IIIe USB 0\SCARD\ETOKEN_PRO32_2533c7052920\CC00\D0AC: ErrCode = 8009001f
Attempt: ProvType = 75; ProvName = Crypto-Pro GOST R 34.10-2001 Cryptographic Service Provider; KeyContName = \\.\AKS ifdh 1\SCARD\ETOKEN_PRO32_2533c7052920\CC00\D0AC: ErrCode = 80100069
Attempt: ProvType = 75; ProvName = Crypto-Pro GOST R 34.10-2001 Cryptographic Service Provider; KeyContName = \\.\AKS ifdh 0\SCARD\ETOKEN_PRO32_2533c7052920\CC00\D0AC: ErrCode = 80100069
No among readers???


//Gost, SmartCard, CRYPT_SILENT не установлен

Regular: ProvType = 75; ProvName = Crypto-Pro GOST R 34.10-2001 Cryptographic Service Provider; KeyContName = SCARD\ETOKEN_PRO32_2533c7052920\CC00\D0AC
signatureA - OK
signatureB - OK


// Код ////////////////////////////////////

//Gost on SmartCard
#define OPERATOR L"Грибоедов Александр Сергеевич"
#define ALGORITHM_HASH "1.2.643.2.2.9"
#define PASSWORD "pipopolam"

////Gost on USB-key
//#define OPERATOR L"Тургеньев Иван Сергеевич"
//#define ALGORITHM_HASH "1.2.643.2.2.9"
//#define PASSWORD "pipopolam"

#define _PASSWORD_SET

HCERTSTORE hcs = 0;
const CERT_CONTEXT* pcc = 0;
CRYPT_KEY_PROV_INFO* kpi = 0;
DWORD provType;
std::string provName;
std::string containerName;
HCRYPTPROV hcp = 0;
DWORD flagAcquireContext = 0;
std::map<DWORD, std::string> mapReaders;

// . . .

bool open()
{
DWORD errCode = 0;

hcs = ::CertOpenStore(CERT_STORE_PROV_SYSTEM, 0, 0, CERT_SYSTEM_STORE_CURRENT_USER, L"MY");
if (!hcs)
{
errCode = ::GetLastError();
printf("Error from ::CertOpenStore = %08x\n", errCode);
return false;
}

pcc = ::CertFindCertificateInStore(hcs, X509_ASN_ENCODING | PKCS_7_ASN_ENCODING, 0, CERT_FIND_SUBJECT_STR, OPERATOR, 0);
if (!pcc)
{
errCode = ::GetLastError();
printf("Error from ::CertFindCertificateInStore = %08x\n", errCode);
return false;
}

{
DWORD dataSize = 0;

if (!::CertGetCertificateContextProperty(pcc, CERT_KEY_PROV_INFO_PROP_ID, 0, &dataSize))
{
errCode = ::GetLastError();
printf("Error from ::CertGetCertificateContextProperty 1st = %08x\n", errCode);
return false;
}

BYTE* data = static_cast<BYTE*> (::malloc(dataSize));
if (!data)
{
errCode = ::GetLastError();
printf("Error from ::CertGetCertificateContextProperty ::malloc = %08x\n", errCode);
return false;
}

kpi = reinterpret_cast<CRYPT_KEY_PROV_INFO*>(data);

if (!::CertGetCertificateContextProperty(pcc, CERT_KEY_PROV_INFO_PROP_ID, data, &dataSize))
{
errCode = ::GetLastError();
printf("Error from ::CertGetCertificateContextProperty 2nd = %08x\n", errCode);
return false;
}

printf("Regular: ProvType = %02u; ProvName = %ws; KeyContName = %ws\n", kpi->dwProvType, kpi->pwszProvName, kpi->pwszContainerName);

provType = kpi->dwProvType;
provName = recodeWtoC(kpi->pwszProvName);
containerName = recodeWtoC(kpi->pwszContainerName);
}

#ifdef _PASSWORD_SET
{
flagAcquireContext = CRYPT_SILENT; //Устанавливаем или не устанавливаем

if (!::CryptAcquireContext(&hcp, containerName.c_str(), provName.c_str(), provType, flagAcquireContext))
{
errCode = ::GetLastError();
printf("Error from regular ::CryptAcquireContext = %08x\n", errCode);

HCRYPTPROV hcpView = 0;
if (!::CryptAcquireContext(&hcpView, 0, provName.c_str(), provType, CRYPT_VERIFYCONTEXT))
{
errCode = ::GetLastError();
printf("Error from view ::CryptAcquireContext = %08x\n", errCode);
return false;
}
loadReaders(hcpView);
::CryptReleaseContext(hcpView, 0);

for (DWORD i = 0; i < mapReaders.size(); ++i)
{
std::string containerFqcn = "\\\\.\\";
containerFqcn += mapReaders[i];
containerFqcn += "\\";
containerFqcn += containerName;

errCode = 0;
if (!::CryptAcquireContext(&hcp, containerFqcn.c_str(), provName.c_str(), provType, flagAcquireContext))
{
errCode = ::GetLastError();
printf("Attempt: ProvType = %02u; ProvName = %s; KeyContName = %s: ErrCode = %08x\n",
provType, provName.c_str(), containerFqcn.c_str(), errCode);
}
else
{
printf("Attempt: ProvType = %02u; ProvName = %s; KeyContName = %s: OK\n",
provType, provName.c_str(), containerFqcn.c_str());
break;
}
}

if (!hcp)
{
printf("No among readers???\n");
return false;
}
else
{
printf("Yes among readers!!!\n");
}
}
}

{
if(!::CryptSetProvParam(hcp, PP_KEYEXCHANGE_PIN, reinterpret_cast<const BYTE*>(PASSWORD), 0))
{
errCode = ::GetLastError();
printf("Error from ::CryptSetProvParam = %08x\n", errCode);
}
}
#endif //_PASSWORD_SET

return true;
}

11.07.2007 12:55:36Василий
Ясно.
По-видимому, вся проблема в том, что слишком длинное имя считывателя Athena.
В результате строка "\\.\Athena ASEDrive IIIe USB 0\SCARD\ETOKEN_PRO32_2533c7052920\CC00\D0AC" длиннее максимально допустимой (т.е. больше 64 байт).
Рекомендация такая:
1) удалить из списка считывателей Athena...
2) переименовать (сократить название) раздел реестра, где зарегистрирован считыватель:
HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Cryptography\Calais\Readers\Athena...
например, до:
Athena USB
3) добавить считыватель Athena USB и в дальнейшем использовать сокращённое название.
11.07.2007 12:59:36Василий
Кстати, в CSP 3.6 значение этой константы увеличено с 64 до 260.
11.07.2007 13:54:44Сергей
И все-таки: …, но тогда так должно быть и при указании флага CRYPT_SILENT, то есть, без перечисления носителей.
А по пункту 3 нас устроит такой ответ: «Передано на рассмотрение Генеральному конструктору». :)
11.07.2007 18:51:39Василий
По пункту 3. Рассмотрим вопрос о проверке пароля в функции CryptSetProvParam. Будет ли принято решение о переделке или нет - обещать не могу. Если будет - то в CSP 3.6 (и выше).
12.07.2007 8:14:18Сергей
Большое спасибо!
25.07.2007 1:50:36maxdm
по пункту 3 у нас уже полгода дискуссии. SetProvParam не может возвращать ошибку (точнее не предусмотренно MS). Вполне можно проверить успешность операции с закрытым ключом. Это лишь ограничения интерфейса, но мы вынуждены жить с ним
25.07.2007 1:56:38maxdm
"Генеральный конструктор" ответил :)
26.07.2007 8:49:31Сергей
Еще раз большое спасибо.

Мы уже тоже как-то ужились со всеми ограничениями и особенностями. Даже с необходимостью перечисления носителей при указании флага CRYPT_SILENT :) .
06.11.2007 15:07:30Стас
Сергей
Как вы решили проблему по второму пункту?
06.11.2007 16:03:04Сергей
Закрываем контекст контейнера ключа, предварительно сохранив пароль.
06.11.2007 16:24:55Стас
Сергей, если не затруднит, напишите пожалуйста здесь последовательность тех функций которые использовались Вами для добавления второй подписи.

P.S: здесь http://www.cryptopro.ru/cryptopro/forum/view.asp?q=6539 я выложил свой код который работает только с дискетой, но отказывается с eToken.
07.11.2007 9:20:15Сергей
Насколько я помню, действительно, при одновременном подключении двух еТокенов система видит только один (или обращается только к первому из списка). Я такую ситуацию не разруливал, т.к. для нас это не штатная работа. Если, все-таки, список содержит все носители (это можно посмотреть – у КриптПРО есть средства - #define PP_ENUMREADERS 114), то можно попытаться получить хэндл ключа на каждом носителе из списка. Ниже привожу функцию получения списка носителей.

void loadReaders(const HCRYPTPROV& hcpv)
{
BYTE* data;
DWORD dataSize = 0;
DWORD flag = CRYPT_FIRST;

DWORD paramPP_ENUMREADERS = 114; /*Это значение определено в WinCryptEx.h от Крипто ПРО*/
DWORD errCode = 0;

if (::CryptGetProvParam(hcpv, paramPP_ENUMREADERS, 0, &dataSize, flag) == 0)
{
errCode = ::GetLastError();
printf("Error from ::CryptGetProvParam 1st = %08x\n", errCode);
}

DWORD index = 0;
bool ok;
do
{
data = static_cast<BYTE*> (::malloc(dataSize));
if (data == 0)
{
errCode = ::GetLastError();
printf("Error from ::CryptGetProvParam ::malloc = %08x\n", errCode);
}

ok = (::CryptGetProvParam(hcpv, paramPP_ENUMREADERS, data, &dataSize, flag) != 0);
if (!ok)
{
errCode = ::GetLastError();
if (errCode != ERROR_NO_MORE_ITEMS)
{
errCode = ::GetLastError();
printf("Error from ::CryptGetProvParam 2nd = %08x\n", errCode);
}
}
else
{

DWORD i = 0;
while (data[i++] != '\0')
;
std::string name = reinterpret_cast<char*>(&data[i]);

mapReaders.insert(std::make_pair(index++, name));
}
::free(data);

flag = 0;
} while (ok);
}

Ситуация, когда для дискет все срабатывает, а для еТокенов - нет, вполне возможна, т.к. для каждого носителя устанавливается своя математика.
07.11.2007 12:12:39Стас
Сергей спасибо что ответил, но я немного другое спрашивал. Мне хочется узнать какие функции и в какой последовательности Вы использовали для добавления второй подписи?