Юрий Зырянов
Введение
Эта статья для тех, кто по тем или иным причинам решил написать собственный крипто-провайдер для OC семейства Windows. Если Вы хотите реализовать в вашем провайдере нестандартные алгоритмы, то вам предстоит столкнуться с определенными трудностями. Трудности могут возникнуть, например, при попытках использования вашего крипто-провайдера для проверки сертификатов в MS Internet Explorer.
Под нестандартными алгоритмами здесь понимаются не всемирные DES, RSA, DSA и т.д, а, например, алгоритмы семейства ГОСТ. Дело в том, что для RSA и подобных алгоритмов все необходимые идентификаторы уже зашиты в систему, а для ГОСТ-ов (или многих других алгоритмов) надо отдельно позаботиться о том, чтобы система их “увидела”.
Для примеров кода используется Си. Все примеры кода служат только для иллюстрации принципов изложенных в статье и не являются полноценными рабочими программами.
Также подразумевается, что у читателя есть базовые знания в области прикладной криптографии и термины “открытый ключ”, “ASN.1” и подобные для него не являются загадкой.
Интеграция провайдера в Windows
Помимо наличия библиотеки самого провайдера дополнительно требуется:
зарегистрировать провайдер и крипто-алгоритмы в системе, прописав определенные параметры в реестре;
создать библиотеку с функциями конвертирования ключей из форматов криптопровайдера во внешние форматы и зарегистрировать эти функции в реестре;
заменить функцию I_CryptGetDefaultCryptProvider в библиотеке crypt32.dll
Только после выполнения этих действий провайдер нормально интегрируется в систему и вы сможете, например, генерировать сертификаты при помощи вашего провайдера на основе стандартного компонента ОС Windows Server - Сertification services или на тестовом УЦ КриптоПро http://www.cryptopro.ru/certsrv
Корректно будет отображаться статус проверки подписи у сертификатов, подписанных вашим “нестандартным” алгоритмом и т.п.
Далее подробно рассмотрим каждый из упомянутых выше шагов, предполагая при этом что библиотека с Вашим CSP уже имеется и корректно работают все функции провайдера.
Регистрация крипто-провайдера и алгоритмов в системе
Когда у вас уже есть готовая библиотека с реализацией функций CSP, необходимо зарегистрировать ее в системе, для того чтобы новый крипто-провайдер стал доступен различным приложениям.
Процесс регистрации самого CSP подробно описан в MDSN, и повторять эту информацию здесь смысла нет. Все это подробно описано в [1]. Также здесь мы не будем останавливаться на проблеме подписи нового CSP в Microsoft и путях ее обхода. Эта проблема уже многократно обсуждалась на различных форумах, например смотрите [3]. Гораздо интереснее рассмотреть регистрацию криптографических алгоритмов. Каждый алгоритм имеет свой уникальный ASN.1 идентификтор Оbject Identifier - OID. Например, алгоритм подписи ГОСТ-34.10-2001 имеет такой OID (представленный в виде строки) – “1.2.643.2.2.3”. Идентификатор каждого поддерживаемого вашим CSP алгоритма следует занести в реестр. Помимо OID у каждого крипто-алгоритма в Windows существует еще идентификатор в виде четырех-байтового числа – AlgID, по которому алгоритмы идентифицируются в провайдере. Этот идентификатор заноситься в CSP и его можно узнать перечислив алгоритмы посредством вызова CPGetProvParam. В КриптоПро, например, для алгоритма хеширования ГОСТ-34.11-94 AlgID используется значение 0x801e
Пусть нам необходимо зарегистрировать алгоритм подписи ГОСТ-34.10-2001. Тогда в реестре необходимо прописать следующие идентификаторы:
“1.2.643.2.2. 9!1” – Хэш ГОСТ-34.11-94
“1.2.643.2.2.19!3” – Ключ подписи ГОСТ-34.10-2001
“1.2.643.2.2.3!4” – Подпись ГОСТ-34.10-2001 – Алгоритм Хэша + Алгоритм Ключа
Рисунок 1 - Идентификаторы алгоритмов
Далее приведен пример кода регистрации OID алгоритма ГОСТ-34.11-94
// Регистрация GOST-3411-94 HASH OID
//
CRYPT_OID_INFO oidInfo;
int rc = 0;
memset(&oidInfo, 0, sizeof(CRYPT_OID_INFO));
oidInfo.cbSize = sizeof(CRYPT_OID_INFO);
oidInfo.pszOID="1.2.643.2.2.9";
oidInfo.pwszName= L"GOST-3411-94 HASH";
oidInfo.dwGroupId = CRYPT_HASH_ALG_OID_GROUP_ID;
oidInfo.Algid = 0x801e;
oidInfo.ExtraInfo.cbData=0;
oidInfo.ExtraInfo.pbData=0;
rc = CryptRegisterOIDInfo(
&oidInfo,
0
);
if(rc)
printf("nHash algorithm registered”);
else
printf("nError registering hash algorithm”);
Аналогично регистрируются и остальные алгоритмы. Подробную информацию о структуре CRYPT_OID_INFO можно найти в MSDN: http://msdn.microsoft.com/library/default.asp?url=/library/en-us/seccrypto/security/crypt_oid_info.asp
Для того чтобы провайдер вызывался для проверки сертификата, подписанного нашим “нестандартным” алгоритмом необходимо еще одно дополнительное действие.
Дело в том, что Windows определяет какой провайдер использовать для проверки по полю ExtraInfo (см. ссылку в предыдущем абзаце для описания этого поля) в ключе реестра соответвующему алгоритму подписи – такой ключ мы создаем, вызывая функцию CryptRegisterOIDInfo. Поэтому надо указать системе наш провайдер в качестве провайдера по умолчанию для типа, который занесен в ExtraInfo алгоритма подписи.
Следующий код устанавливает провайдер по умолчанию для определенного типа.
#define YOUR_PROV_NAME "MY_PROV"
#define YOUR_PROV_TYPE 75
rc = CryptSetProvider(
YOUR_PROV_NAME,
YOUR_PROV_TYPE
);
Сценарии применения нашего CSP
Рассмотрим здесь два сценария применения CSP.
Первый сценарий – проверка подписи сертификата. Для проверки подписи система загружает открытый ключ из сертификата, которым подписан проверяемый. Затем по OID алгоритма подписи проверяемого сертификата, так как описано в предыдущем разделе статьи, определяется требуемый провайдер. Чтобы проверить подпись, нужно импортировать открытый ключ в CSP и можно было бы подумать что Windows сразу вызывает функцию нашего провайдера CPImportKey. Но не тут-то было!
Второй сценарий – генерация ключевой пары и отправка запроса на сертификат на Удостоверяющий Центр. Windows загружает наш CSP, генерирует ключевую пару и экспортирует к себе наверх открытый ключ вызывая функцию CPExportKey. Все хорошо. Вроде бы надо взять и поместить полученный буфер с ключом в PKCS#10 запрос, который затем будет отправлен на УЦ. И тут все опять не совсем так.
Рисунок 2 - Запрос на сертификат в УЦ
Рисунок 3 - Процесс создания сертификата
Оказывается, существуют промежуточные функции для экспорта/импорта открытых ключей, и без их реализации ничего хорошего с нашим CSP, в упомянутых выше двух сценариях, не получиться. Беда еще и в том, что функции эти недокументированные и найти информацию по ним крайне сложно. Их описанию посвящен следующий раздел.
Функции конвертирования ключей
Архитектура круговорота открытых ключей для “нестандартных” алгоритмов в Windows представлена на картинке:
Рисунок 4 - Импорт и экспорт открытых ключей в/из CSP
Функция A_ConvertPublicKeyInfo – на входе принимает ключ в формате ASN.1 DER и должна преобразовать его в формат, который “понимает” функция CSP CPImportKey
Функция A_EncodePublicKeyInfoAndParameters на входе принимает ключ в том формате, в котором ключ экспортирует CPExportKey. На выходе A_EncodePublicKeyInfoAndParameters должна сформировать ASN.1 DER структуру с ключом – ту же самую которая в случае импорта передается в A_ConvertPublicKeyInfo – на вход. Надеюсь, вы не запутались во всех этих входах Jи выходах
Вот сигнатуры и краткие описания параметров этих функций:
BOOL WINAPI A_ConvertPublicKeyInfo(
DWORD dwCertEncodingType, // IN -
VOID *EncodedKeyInfo, // IN – буферсключом - указатель
// наструктуру CERT_PUBLIC_KEY_INFO
DWORD dwAlg, // IN – AlgId ключа
DWORD dwFlags, // IN – обычно 0
BYTE **ppStructInfo, // OUT – двойной указатель на структуру
// в заголовке структуры идет сначала PUBLICKEYSTRUC, затем DSSPUBKEY,
// а затем сам ключ с параметрами
DWORD *StructLen // OUT – длиннаструктуры
);
BOOL WINAPI A_EncodePublicKeyAndParameters(
DWORD dwCertEncodingType, // IN
LPCSTR lpszStructType, // IN – OID алгоритма
const void* pvStructInfo, // IN – такаяжеструктуракак
// навыходе ConvertPublicKeyInfo
DWORD nStructLen, // IN – длиннавходнойструктуры
DWORD dwFlags, // IN – обычно 0
DWORD Unk, // ? - неизвестно
BYTE **pbPubKey, // OUT – открытый ключ в ASN.1 DER
DWORD* pcPubKeyLen, // OUT – длинна открытого ключа
BYTE **pbParams, // OUT – параметры открытого ключа
DWORD* pcParamsLen // OUT – длинна параметров
);
Форматы ключей зависят от крипто-провайдера и используемых алгоритмов.
Функция I_CryptGetDefaultCryptProvider из crypt32.dll
По непонятной для меня причине Windows часто не пытается искать нужный крипто- провайдер по идентификатору алгоритма с которым требуется работать. В таких случаях она просто вызывает недокументированную функцию I_CryptGetDefaultCryptProvider, и если тот провайдер, который вернула эта функция, не умеет работать с данным алгоритмом, то процесс завершается с ошибкой. Так происходит, например, при разборе в Internet Explorer PKCS#7 ответа в сценарии с запросом сертификата на тестовом УЦ.
HCRYPTPROV WINAPI I_CryptGetDefaultCryptProv(ALG_ID algid);
Необходимо заменить эту функцию таким образом, чтобы при нулевом параметре algid на входе она возвращала наш провайдер, который уже в отличии от штатного провайдера легко справиться с “нестандартными алгоритмами”.
Обсуждение способов замены функций в системной dll выходит далеко за рамки данной статьи. Могу лишь, как один из способов решения, предложить библиотеку Microsoft Detours: http://research.microsoft.com/sn/detours
Привязка закрытого ключа к сертификату
В отличие от открытого ключа закрытые ключи никогда не покидают крипто-провайдер и поэтому когда вы видите что для данного сертификата есть закрытый ключ (как на рисунке) это значит что в контексте этого сертификата существует явная ссылка на закрытый ключ.
Рисунок 5 - Закрытый ключ сертификата
Контекст сертификата это набор дополнительных атрибутов сертификата, которые находятся не в теле сертификата, а хранятся отдельно от него. Одним из таких атрибутов и является ссылка на закрытый ключ, которая состоит из имени провайдера и имени ключевого контейнера.