Секреты разработки CSP

<!-- Название статьи: Секреты разработки CSP Описание статьи: Статья расказывает о недокументиравнных особенностях разработки провайдера для MS Windows Ключевые слова: Windows CSP Криптография Провайдер WinAPI Автор: Юрий Зырянов, OOO "ЛИССИ" -->

Введение

Эта статья для тех, кто по тем или иным причинам решил написать собственный крипто-провайдер для 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 – на вход. Надеюсь, вы не запутались во всех этих входах и выходах 

Вот сигнатуры и краткие описания параметров этих функций:

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 - Закрытый ключ сертификата

Контекст сертификата это набор дополнительных атрибутов сертификата, которые находятся не в теле сертификата, а хранятся отдельно от него. Одним из таких атрибутов и является ссылка на закрытый ключ, которая состоит из имени провайдера и имени ключевого контейнера.

Пример кода для привязки закрытого ключа к сертификату:

	PCCERT_CONTEXT pCert;
	CRYPT_KEY_PROV_INFO prov_info;
	…
	prov_info.cProvParam = 0;
	prov_info.rgProvParam = 0;
	prov_info.dwFlags = 0;
	prov_info.dwKeySpec = AT_SIGNATURE;
	prov_info.dwProvType = 0;
	prov_info.pwszContainerName = L"key-kont-name";
	prov_info.pwszProvName = L"A-CSP";

	CertSetCertificateContextProperty(
		pCert,
		CERT_KEY_PROV_INFO_PROP_ID,
		0,
		&prov_info
	);

Успехов в разработке крипто-провайдера!

Дополнительная информация

  1. Разработка CSP для Windows .../library/en-us/seccrypto/security/cryptography_portal.asp
  2. Additional Cryptographic Algorithms for Use with GOST 28147-89, GOST R 34.10-94, GOST R 34.10-2001, and GOST R 34.11-94 Algorithms http://www.ietf.org/rfc/rfc4357.txt
  3. Много интересной информации об архитектуре CSP можно найти тут: http://www.derkeiler.com/Mailing-Lists/securityfocus/focus-ms


Опубликовал admin
18 Сен, Понедельник 2006г.



Программирование для чайников.