Методы криптографической защиты информации Windows
Криптографические возможности Windows
Сразу договоримся, что никакая система защиты информации не может быть абсолютно надежной. Речь может идти лишь о некоторой степени надежности и рисках, связанных со взломом защиты. Поэтому с практической точки зрения есть смысл оценить важность данных и экономно подстелить соломку на случай неудачи. В наших приложениях, например, мы выдаем кредит доверия операционной системе Windows, несмотря на закрытость ее кода.
Итак, ОС мы доверяем. Чтобы криптозащиту нельзя было «обойти» с другой стороны — к примеру, перехватить из незащищенной области памяти секретные пароли — криптографические функции должны быть частью операционной системы. В семействе Windows, начиная с Windows 95, обеспечивается реализация шифрования, генерации ключей, создания и проверки цифровых подписей и других криптографических задач. Эти функции необходимы для работы операционной системы, однако ими может воспользоваться и любая прикладная программа — для этого программисту достаточно обратиться к нужной подпрограмме так, как предписывает криптографический интерфейс прикладных программ (CryptoAPI).
Разумеется, по мере совершенствования Windows расширялся и состав ее криптографической подсистемы. Помимо базовых операций, в настоящее время в CryptoAPI 2.0 поддерживается работа с сертификатами, шифрованными сообщениями в формате PKCS #7 и пр.
Описание функций CryptoAPI, помимо специальных книг, можно найти в MSDN Library, или в CD-версии, в файле crypto.chm.
Взаимодействие с CryptoAPI
Функции CryptoAPI можно вызвать из программы, написанной на любимом многими (в том числе и авторами) языке С++. Тем не менее, Pascal де-факто признан стандартом в области обучения программированию. (Не будем спорить о том, хорошо это или плохо, чтобы не ввязываться в драку, пусть даже и виртуальную.) Кроме того, в ряде отечественных компаний Delphi является базовым средством разработки. Поэтому все примеры были реализованы в среде Delphi. Хотя в качестве инструмента можно было бы выбрать и MS Visual C++.
Код функций криптографической подсистемы содержится в нескольких динамически загружаемых библиотеках Windows (advapi32.dll, crypt32.dll). Для обращения к такой функции из прикладной программы на Object Pascal следует объявить ее как внешнюю. Заголовок функции в интерфейсной части модуля будет выглядеть, например, так:
function CryptAcquireContext( phPROV: PHCRYPTPROV; pszContainer: LPCTSTR; pszProvider: LPCTSTR; dwProvType: DWORD; dwFlags: DWORD): BOOL; stdcall;
а в исполняемой части вместо тела функции нужно вписать директиву extern с указанием библиотеки, в которой содержится функция, и, возможно, ее имени в этой библиотеке (если оно отличается от имени функции в создаваемом модуле), например:
function CryptAcquireContext; external ‘advapi32.dll’
name 'CryptAcquireContextA';
Таким образом, имея описание функций CryptoAPI, можно собрать заголовки функций в отдельном модуле, который будет обеспечивать взаимодействие прикладной программы с криптографической подсистемой. Разумеется, такая работа была проделана программистами Microsoft, и соответствующий заголовочный файл (wincrypt.h) был включен в поставку MS Visual C++. К счастью, появилась и Delphi-версия (wcrypt2.pas). Ее можно найти здесь. Подключив модуль к проекту, вы сможете использовать не только функции CryptoAPI, но и мнемонические константы режимов, идентификаторы алгоритмов и прочих параметров, необходимых на практике.
И последнее замечание перед тем, как опробовать CryptoAPI в деле. Ряд функций был реализован только в Windows 2000. Но и на старушку Windows 98 можно найти управу: при установке Internet Explorer 5 интересующие нас библиотеки обновляются, позволяя использовать новейшие криптографические возможности. Нужно лишь задать для Delphi-проекта параметр условной компиляции NT5, после чего вызовы функций, появившихся лишь в Windows 2000, будут нормально работать.
Знакомство с криптопровайдерами
Функции CryptoAPI обеспечивают прикладным программам доступ к криптографическим возможностям Windows. Однако они являются лишь «передаточным звеном» в сложной цепи обработки информации. Основную работу выполняют скрытые от глаз программиста функции, входящие в специализированные программные (или программно-аппаратные) модули — провайдеры (поставщики) криптографических услуг (CSP — Cryptographic Service Providers), или криптопровайдеры
Программная часть криптопровайдера представляет собой dll-файл, подписанный Microsoft; периодически Windows проверяет цифровую подпись, что исключает возможность подмены криптопровайдера.
Криптопровайдеры отличаются друг от друга:
составом функций (например, некоторые криптопровайдеры не выполняют шифрование данных, ограничиваясь созданием и проверкой цифровых подписей);
требованиями к оборудованию (специализированные криптопровайдеры могут требовать устройства для работы со смарт-картами для выполнения аутентификации пользователя);
алгоритмами, осуществляющими базовые действия (создание ключей, хеширование и пр.).
По составу функций и обеспечивающих их алгоритмов криптопровайдеры подразделяются на типы. Например, любой CSP типа PROV_RSA_FULL поддерживает как шифрование, так и цифровые подписи, использует для обмена ключами и создания подписей алгоритм RSA, для шифрования — алгоритмы RC2 и RC4, а для хеширования — MD5 и SHA.
В зависимости от версии операционной системы состав установленных криптопровайдеров может существенно изменяться. Однако на любом компьютере с Windows можно найти Microsoft Base Cryptographic Provider, относящийся к уже известному нам типу PROV_RSA_FULL. Именно с этим провайдером по умолчанию будут взаимодействовать все программы.
Использование криптографических возможностей Windows напоминает работу программы с графическим устройством. Криптопровайдер подобен графическому драйверу: он может обеспечивать взаимодействие программного обеспечения с оборудованием (устройство чтения смарт-карт, аппаратные датчики случайных чисел и пр.). Для вывода информации на графическое устройство приложение не должно непосредственно обращаться к драйверу — вместо этого нужно получить у системы контекст устройства, посредством которого и осуществляются все операции. Это позволяет прикладному программисту использовать графическое устройство, ничего не зная о его аппаратной реализации. Точно так же для использования криптографических функций приложение обращается к криптопровайдеру не напрямую, а через CryptoAPI. При этом вначале необходимо запросить у системы контекст криптопровайдера.
Первым делом, хотя бы из любопытства, выясним, какие же криптопровайдеры установлены в системе. Для этого нам понадобятся четыре функции CryptoAPI (выходные параметры выделены жирным шрифтом, а входные — курсивом):
CryptEnumProviders (i, резерв, флаги, тип, имя, длина_имени) — возвращает имя и тип i-го по порядку криптопровайдера в системе (нумерация начинается с нуля);
CryptAcquireContext (провайдер, контейнер, имя, тип, флаги) — выполняет подключение к криптопровайдеру с заданным типом и именем и возвращает его дескриптор (контекст). При подключении мы будем передавать функции флаг CRYPT_VERIFYCONTEXT, служащий для получения контекста без подключения к контейнеру ключей;
CryptGetProvParam (провайдер, параметр, данные, размер_данных, флаги) — возвращает значение указанного параметра провайдера, например, версии (второй параметр при вызове функции — PP_VERSION), типа реализации (программный, аппаратный, смешанный — PP_IMPTYPE), поддерживаемых алгоритмов (PP_ENUMALGS). Список поддерживаемых алгоритмов при помощи этой функции может быть получен следующим образом: при одном вызове функции возвращается информация об одном алгоритме; при первом вызове функции следует передать значение флага CRYPT_FIRST, а при последующих флаг должен быть равен 0;
CryptReleaseContext (провайдер, флаги) — освобождает дескриптор криптопровайдера.
Каждая из этих функций, как и большинство других функций CryptoAPI, возвращает логическое значение, равное true, в случае успешного завершения, и false — если возникли ошибки. Код ошибки может быть получен при помощи функции GetLastError. Возможные значения кодов ошибки приведены в упоминавшейся выше документации. Например, при вызове функции CryptGetProvParam для получения версии провайдера следует учесть возможность возникновения ошибок следующим образом:
if not CryptGetProvParam(hProv, PP_VERSION, (@vers), @DataLen, 0) then begin case int64(GetLastError) of ERROR_INVALID_HANDLE: err := 'ERROR_INVALID_HANDLE'; ERROR_INVALID_PARAMETER: err := 'ERROR_INVALID_PARAMETER'; ERROR_MORE_DATA: err := 'ERROR_MORE_DATA'; ERROR_NO_MORE_ITEMS: err := 'ERROR_NO_MORE_ITEMS'; NTE_BAD_FLAGS: err := 'NTE_BAD_FLAGS'; NTE_BAD_TYPE: err := 'NTE_BAD_TYPE'; NTE_BAD_UID: err := 'NTE_BAD_UID'; else err := 'Unknown error'; end; MessageDlg('Error of CryptGetProvParam: ' + err, mtError, [mbOK], 0); exit end;
Текст процедуры, выводящей в Memo-поле FileMemo формы информацию об установленных в системе криптопровайдерах, приведен ниже. Предполагается, что процедура вызывается при выборе соответствующего элемента в главном меню формы. Для краткости в тексте программы опущены фрагменты, выполняющие обработку ошибок.
type algInfo = record algID: ALG_ID; dwBits: DWORD; dwNameLen: DWORD; szName: array[0..100] of char; end; {вспомогательная функция, преобразующая тип провайдера в строку} function ProvTypeToStr(provType: DWORD): string; begin case provType of PROV_RSA_FULL: ProvTypeToStr := 'RSA full provider'; PROV_RSA_SIG: ProvTypeToStr := 'RSA signature provider'; PROV_DSS: ProvTypeToStr := 'DSS provider'; PROV_DSS_DH: ProvTypeToStr := 'DSS and Diffie-Hellman provider'; PROV_FORTEZZA: ProvTypeToStr := 'Fortezza provider'; PROV_MS_EXCHANGE: ProvTypeToStr := 'MS Exchange provider'; PROV_RSA_SCHANNEL: ProvTypeToStr := 'RSA secure channel provider'; PROV_SSL: ProvTypeToStr := 'SSL provider'; else ProvTypeToStr := 'Unknown provider'; end; end; {вспомогательная функция, преобразующая тип реализации в строку} function ImpTypeToStr(it: DWORD): string; begin case it of CRYPT_IMPL_HARDWARE: ImpTypeToStr := 'аппаратный'; CRYPT_IMPL_SOFTWARE: ImpTypeToStr := 'программный'; CRYPT_IMPL_MIXED: ImpTypeToStr := 'смешанный'; CRYPT_IMPL_UNKNOWN: ImpTypeToStr := 'неизвестен'; else ImpTypeToStr := 'неверное значение'; end; end; {процедура вывода информации о криптопровайдерах} procedure TMainForm.InfoItemClick(Sender: TObject); var i: DWORD; dwProvType, cbName, DataLen: DWORD; provName: array[0..200] of char; vers: array[0..3] of byte; impType: DWORD; ai: algInfo; err: string; begin i := 0; FileMemo.Clear; while (CryptEnumProviders(i, nil, 0, {проверяем наличие еще одного} @dwProvType, nil, @cbName)) do begin if CryptEnumProviders(i, nil, 0, {получаем имя CSP} @dwProvType, @provName, @cbName) then begin FileMemo.Lines.Add('Криптопровайдер: ' + provName); FileMemo.Lines.Add('Тип: ' + IntToStr(dwProvType) + ' - ' + ProvTypeToStr(dwProvType)); if not CryptAcquireContext(@hProv, nil, provName, dwProvType, CRYPT_VERIFYCONTEXT) then begin {обработка ошибок} end; DataLen := 4; if not CryptGetProvParam(hProv, PP_VERSION, (@vers), @DataLen, 0) then begin {обработка ошибок} end; FileMemo.Lines.Add('Версия: ' + chr(vers[1] + ) + '.' + chr(vers[0] + )); if not CryptGetProvParam(hProv, PP_IMPTYPE, @impType, @DataLen, 0) then begin {обработка ошибок} end; FileMemo.Lines.Add('Тип реализации: ' + ImpTypeToStr(impType)); FileMemo.Lines.Add('Поддерживает алгоритмы:'); DataLen := sizeof(ai); if not CryptGetProvParam(hProv, PP_ENUMALGS, @ai, @DataLen, CRYPT_FIRST) then begin {обработка ошибок} end; with ai do FileMemo.Lines.Add(szName + #9 + 'длина ключа - ' + IntToStr(dwBits) + ' бит' + #9 + 'ID: ' + IntToStr(AlgID)); DataLen := sizeof(ai); while CryptGetProvParam(hProv, PP_ENUMALGS, @ai, @DataLen, 0) do begin with ai do FileMemo.Lines.Add(szName + #9 + 'длина ключа - ' + IntToStr(dwBits) + ' бит' + #9 + 'ID: ' + IntToStr(AlgID)); DataLen := sizeof(ai); end; FileMemo.Lines.Add(''); CryptReleaseContext(hProv, 0); end; inc(i); end; end;
На рис. 2 показан пример отчета, выдаваемого приведенным выше кодом, выполненным в среде Windows 98.
Шифрование с использованием паролей
После того как мы узнали кое-что о структуре CryptoAPI, можно воспользоваться ею в практических целях. Пожалуй, самым ожидаемым действием криптографической подсистемы является шифрование файлов — так, чтобы лишь пользователь, знающий определенный пароль, мог получить к ним доступ.
Для шифрования данных в CryptoAPI применяются симметричные алгоритмы. Симметричность означает, что для шифрования и расшифровки данных используется один и тот же ключ, известный как шифрующей, так и расшифровывающей стороне. При этом плохо выбранный ключ шифрования может дать противнику возможность взломать шифр. Поэтому одной из функций криптографической подсистемы должна быть генерация «хороших» ключей либо случайным образом, либо на основании некоторой информации, предоставляемой пользователем, например пароля.
В случае создания ключа на основании пароля должно выполняться следующее обязательное условие: при многократном повторении процедуры генерации ключа на одном и том же пароле должны получаться идентичные ключи. Ключ шифрования имеет, как правило, строго определенную длину, определяемую используемым алгоритмом, а длина пароля может быть произвольной. Даже интуитивно понятно, что для однозначной генерации ключей нужно привести разнообразные пароли к некоторой единой форме. Это достигается с помощью хеширования.
Хешированием (от англ. hash — разрезать, крошить, перемешивать) называется преобразование строки произвольной длины в битовую последовательность фиксированной длины (хеш-значение, или просто хеш) с обеспечением следующих условий:
по хеш-значению невозможно восстановить исходное сообщение;
практически невозможно найти еще один текст, дающий такой же хеш, как и наперед заданное сообщение;
практически невозможно найти два различных текста, дающих одинаковые хеш-значения (такие ситуации называют коллизиями).
При соблюдении приведенных условий хеш-значение служит компактным цифровым отпечатком (дайджестом) сообщения. Существует множество алгоритмов хеширования. CryptoAPI поддерживает, например, алгоритмы MD5 (MD — Message Digest) и SHA (Secure Hash Algorithm).
Итак, чтобы создать ключ шифрования на основании пароля, нам нужно вначале получить хеш этого пароля. Для этого следует создать с помощью CryptoAPI хеш-объект, воспользовавшись функцией CryptCreateHash (провайдер, ID_алгоритма, ключ, флаги, хеш), которой нужно передать дескриптор криптопровайдера (полученный с помощью CryptAcquireContext) и идентификатор алгоритма хеширования (остальные параметры могут быть нулями). В результате мы получим дескриптор хеш-объекта. Этот объект можно представить себе как черный ящик, который принимает любые данные и «перемалывает» их, сохраняя внутри себя лишь хеш-значение. Подать данные на вход хеш-объекта позволяет функция CryptHashData (дескриптор, данные, размер_данных, флаги).
Непосредственно создание ключа выполняет функция CryptDeriveKey (провайдер, ID_алгоритма, хеш-объект, флаги, ключ), которая принимает хеш-объект в качестве исходных данных и строит подходящий ключ для алгоритма шифрования, заданного своим ID. Результатом будет дескриптор ключа, который можно использовать для шифрования (рис. 3).
Следует обратить внимание, что при работе с CryptoAPI мы все время имеем дело не с самими объектами или их адресами, а с дескрипторами — целыми числами, характеризующими положение объекта во внутренних таблицах криптопровайдера. Сами таблицы располагаются в защищенной области памяти, так что программы-«шпионы» не могут получить к ним доступ.
Алгоритмы шифрования, поддерживаемые CryptoAPI, можно разделить на блочные и поточные: первые обрабатывают данные относительно большими по размеру блоками (например, 64, 128 битов или более), а вторые — побитно (теоретически, на практике же — побайтно). Если размер данных, подлежащих шифрованию, не кратен размеру блока, то последний, неполный блок данных, будет дополнен необходимым количеством случайных битов, в результате чего размер зашифрованной информации может несколько увеличиться. Разумеется, при использовании поточных шифров размер данных при шифровании остается неизменным.
Шифрование выполняется функцией CryptEncrypt (ключ, хеш, финал, флаги, данные, рамер_данных, размер_буфера):
через параметр ключ передается дескриптор ключа шифрования;
параметр хеш используется, если одновременно с шифрованием нужно вычислить хеш-значение шифруемого текста;
параметр финал равен true, если шифруемый блок текста — последний или единственный (шифрование можно осуществлять частями, вызывая функцию CryptEncrypt несколько раз);
значение флага должно быть нулевым;
параметр данные представляет собой адрес буфера, в котором при вызове функции находится исходный текст, а по завершению работы функции — зашифрованный;
следующий параметр, соответственно, описывает размер входных/выходных данных,
последний параметр задает размер буфера — если в результате шифрования зашифрованный текст не уместится в буфере, возникнет ошибка.
Для расшифровки данных используется функция CryptDecrypt (ключ, хеш, финал, флаги, данные, рамер_данных), отличающаяся от шифрующей функции только тем, что размер буфера указывать не следует: поскольку размер данных при расшифровке может только уменьшиться, отведенного под них буфера наверняка будет достаточно.
Приведем лишь фрагменты программы, реализующей шифрование файла с использованием заданного пароля, опустив громоздкие проверки успешности выполнения криптографических операций (что в реальной программе делать крайне нежелательно).
{«описание» используемых переменных} hProv: HCRYPTPROV; hash: HCRYPTHASH; password: string; key: HCRYPTKEY; plaintext, ciphertext: string; inFile, outFile: file; data: PByte; l: DWORD; {получаем контекст криптопровайдера} CryptAcquireContext(@hProv, nil, nil, PROV_RSA_FULL, CRYPT_VERIFYCONTEXT); {создаем хеш-объект} CryptCreateHash(hProv, CALG_SHA, 0, 0, @hash); {хешируем пароль} CryptHashData(hash, @password[1], length(password), 0); {создаем ключ на основании пароля для потокового шифра RC4} CryptDeriveKey(hProv, CALG_RC4, hash, 0, @key); {уничтожаем хеш-объект} CryptDestroyHash(hash); {открываем файлы} AssignFile(inFile, plaintext); AssignFile(outFile, ciphertext); reset(inFile, 1); rewrite(outFile, 1); {выделяем место для буфера} GetMem(data, 512); {шифруем данные} while not eof(inFile) do begin BlockRead(inFile, data^, 512, l); CryptEncrypt(key, 0, eof(inFile), 0, data, @l, l); BlockWrite(outFile, data^, l); end; {освобождаем место и закрываем файлы} FreeMem(data, 512); CloseFile(inFile); CloseFile(outFile); {освобождаем контекст криптопровайдера} CryptReleaseContext(hProv, 0);
Конечно, шифрование вами всех файлов одним и тем же паролем облегчает «противнику» задачу их расшифровки, запоминание огромного числа паролей сильно усложняет жизнь, а их записывание в незашифрованном виде создает опасность раскрытия всей системы. CryptoAPI может предложить на этот случай ряд решений. О них поговорим в следующий раз.
Проблема распределения ключей
В прошлый раз при помощи CryptoAPI мы решали такую "классическую" задачу как шифрование на основе пароля. Напомним, что пароль использовался для создания ключа шифрования какого-либо симметричного алгоритма. В таком случае расшифровать файл может лишь тот, кто знает пароль. А значит, для обеспечения конфиденциальности нужно держать пароль в строжайшем секрете - желательно, чтобы его знали лишь отправитель и получатель информации. (А еще лучше, если отправитель и получатель - одно и то же лицо.)
Предположим, что отправитель и получатель при личной встрече договорились использовать для конфиденциальной переписки определенный пароль. Но если они будут шифровать все свои сообщения одним и тем же ключом, то возможный противник, перехватив корреспонденцию, будеть иметь хорошие шансы взломать шифр: при современных методах криптоанализа наличие нескольких шифртекстов, полученных путем использования одного и того же ключа, почти гарантирует успешный результат. Поэтому при использовании симметричных алгоритмов шифрования настоятельно рекомендуется не применять один и тот же ключ дважды!
Однако помнить отдельный пароль для каждого зашифрованного сообщения - задача достаточно трудоемкая. А для корреспондентов, не имеющих возможности встретиться лично для согласования ключей шифрования, конфиденциальный обмен сообщениями вообще становится недоступным. Такая практическая трудность называется проблемой распределения ключей.
Спасительный способ, позволяющий шифровать сообщения, обмениваясь ключами по открытым каналам связи, был придуман в середине 70-х годов прошлого столетия, а в начале восьмидесятых появился первый реализующий его алгоритм - RSA. Теперь пользователь может сгенерировать два связанных между собой ключа - ключевую пару. Один из этих ключей по несекретным каналам рассылается всем, с кем пользователь хотел бы обмениваться конфиденциальными сообщениями. Этот ключ называют открытым (англ. public key). Зная открытый ключ пользователя, можно зашифровать адресованное ему сообщение, но вот расшифровать его позволяет лишь вторая часть ключевой пары - закрытый ключ (private key). При этом открытый ключ не дает "практической" возможности вычислить закрытый: такая задача, хоть и разрешима в принципе, но при достаточно большом размере ключа требует многих лет машинного времени. Для сохранения конфиденциальности получателю необходимо лишь хранить в строгом секрете свой закрытый ключ, а отправителю - убедиться, что имеющийся у него открытый ключ действительно принадлежит адресату.
Так как для шифрования и расшифровки используются различные ключи, алгоритмы такого рода назвали асимметричными. Наиболее существенным их недостатком является низкая производительность - они примерно в 100 раз медленнее симметричных алгоритмов. Поэтому были созданы криптографические схемы, использующие преимущества как симметричных, так и асимметричных алгоритмов:
для шифрования файла или сообщения используется быстрый симметричный алгоритм, причем ключ шифрования генерируется случайным образом с обеспечением "хороших" статистических свойств;
небольшой по размерам симметричный ключ шифрования шифруется при помощи асимметричного алгоритма с использованием открытого ключа адресата и в зашифрованном виде пересылается вместе с сообщением;
получив сообщение, адресат своим закрытым ключом расшифровывает симметричный ключ, а с его помощью - и само сообщение.
Описанная схема реализована и в CryptoAPI.
Целостность и аутентичность информации
Как удостовериться в том, что пришедшее сообщение действительно отправлено тем, чье имя стоит в графе "отправитель"? Асимметричные схемы шифрования дают нам элегантный способ аутентификации. Если отправитель зашифрует сообщение своим закрытым ключом, то успешное расшифровывание убедит получателя в том, что послать корреспонденцию мог только хозяин ключевой пары, и никто иной. При этом расшифровку может выполнить любой, кто имеет открытый ключ отправителя. Ведь наша цель - не конфиденциальность, а аутентификация.
Чтобы избежать шифрования всего сообщения при помощи асимметричных алгоритмов, используют хеширование: вычисляется хеш-значение исходного сообщения, и только эта короткая последовательность байтов шифруется закрытым ключом отправителя. Результат представляет собой электронную цифровую подпись. Добавление такой подписи к сообщению позволяет установить:
аутентичность сообщения - создать подпись на основе закрытого ключа мог только его хозяин;
целостность данных - легко вычислить хеш-значение полученного сообщения и сравнить его с тем, которое хранится в подписи: если значения совпадают, значит, сообщение не было изменено злоумышленником после того, как отправитель его подписал.
Таким образом, асимметричные алгоритмы позволяют решить две непростые задачи: обмена ключами шифрования по открытым каналам связи и подписи сообщения. Чтобы воспользоваться этими возможностями, нужно сгенерировать и сохранить две ключевые пары - для обмена ключами и для подписей. В этом нам поможет CryptoAPI.
Контейнеры ключей
Каждый криптопровайдер располагает базой данных, в которой хранятся долговременные ключи пользователей. База данных содержит один или более контейнеров ключе. Пользователь может создать несколько контейнеров с различными именами (именем контейнера по умолчанию является имя пользователя в системе).
Подключение к контейнеру производится одновременно с получением контекста криптопровайдера при вызове функции CryptAcquireContext - имя контейнера ключей передается функции вторым ее аргументом. Если второй аргумент содержит пустой указатель (nil), то используется имя по умолчанию, т. е. имя пользователя. В том случае, если доступ к контейнеру не нужен, можно передать в последнем аргументе функции флаг CRYPT_VERIFYCONTEXT; при необходимости создать новый контейнер используется флаг CRYPT_NEWKEYSET; а для удаления существующего контейнера вместе с хранящимися в нем ключами - CRYPT_DELETEKEYSET.
Каждый контейнер может содержать, как минимум, две ключевые пары - ключ обмена ключами и ключ подписи. Ключи, используемые для шифрования симметричными алгоритмами, не сохраняются. Как мы уже говорили, такие ключи не рекомендуется применять более одного раза, поэтому их называют сеансовыми (англ. session key).
Создание ключевых пар
После создания контейнера ключей необходимо сгенерировать ключевые пары обмена ключами и подписи. Эту работу в CryptoAPI выполняет функция CryptGenKey (провайдер, алгоритм, флаги, ключ):
провайдер - дескриптор криптопровайдера, полученный в результате обращения к функции CryptAcquireContext;
алгоритм - указывает, какому алгоритму шифрования будет соответствовать создаваемый ключ. Информация об алгоритме, таким образом, является частью описания ключа. Каждый криптопровайдер использует для обмена ключами и подписи строго определенные алгоритмы. Так, провайдеры типа PROV_RSA_FULL, к которым относится и Microsoft Base Cryptographic Provider, реализуют алгоритм RSA. Но при генерации ключей знать это не обязательно: достаточно указать, какой ключ мы собираемся создать - обмена ключами или подписи. Для этого используются мнемонические константы AT_KEYEXCHANGE и AT_SIGNATURE;
флаги - при создании асимметричных ключей управляет их размером. Используемый нами криптопровайдер позволяет генерировать ключ обмена ключами длиной от 384 до 512 бит**, а ключ подписи - от 512 до 16384 бит. Чем больше длина ключа, тем выше его надежность, поэтому трудно найти причины для использования ключа обмена ключами длиной менее 512 бит, а длину ключа подписи не рекомендуется делать меньше 1024 бит**. По умолчанию криптопровайдер создает оба ключа длиной 512 бит. Необходимую длину ключа можно передать в старшем слове параметра флаги;
ключ - в случае успешного завершения функции в этот параметр заносится дескриптор созданного ключа.
Рассмотрим пример создания ключевых пар при помощи формы. В поле "Контейнер" можно указать имя контейнера ключей; если оставить это поле пустым, будет использован контейнер по умолчанию. Назначение остальных элементов управления должно быть интуитивно понятным. После генерации ключа в memo-поле выводится отчет о его параметрах. Для этого используется функция CryptGetKeyParam (ключ, параметр, буфер, размер, флаги). Чтобы получить информацию о требуемом параметре, нужно через второй аргумент функции передать соответствующую константу: KP_ALGID - идентификатор алгоритма, KP_KEYLEN - размер ключа, и т. д. Ниже приведен текст процедуры генерации ключей без операторов обработки ошибок:
procedure TGenerateForm.OKBtnClick(Sender: TObject); var cont: PChar; err: string; hProv: HCRYPTPROV; KeyExchKey, SignKey: HCRYPTKEY; flag, keyLen: DWORD; begin {если ни один ключ не выбран - выход} if not (KEKCheckBox.Checked or SKCheckBox.Checked) then exit; {"считываем" имя контейнера} if length(ContainerEdit.Text) = 0 then cont := nil else begin err := ContainerEdit.Text; cont := StrAlloc(length(err) + 1); StrPCopy(cont, err); end; CryptAcquireContext(@hProv, cont, nil, PROV_RSA_FULL, 0); {генерация ключа обмена ключами (Key Exchange Key)} if KEKCheckBox.Checked then begin {"считываем" длину ключа и помещаем ее в старшее слово параметра ФЛАГИ} keyLen := strtoint(KeyExchLenEdit.text); flag := keyLen shl 16; if not CryptGenKey(hProv, AT_KEYEXCHANGE, flag, @KeyExchKey) then begin јобработка ошибокј end else begin ReportMemo.Lines.Add(''); ReportMemo.Lines.Add('Создан ключ обмена ключами:'); flag := 4; if not CryptGetKeyParam(KeyExchKey, KP_KEYLEN, @keyLen, @flag, 0) then begin јобработка ошибокј end else ReportMemo.Lines.Add(' длина ключа - ' + inttostr(keyLen)); flag := 4; if not CryptGetKeyParam(KeyExchKey, KP_ALGID, @keyLen, @flag, 0) then begin јобработка ошибокј end else ReportMemo.Lines.Add(' алгоритм - ' + algIDtostr(keyLen)); {функция algIDtostr здесь не приводится. Она состоит из единственного оператора case, отображающего целый идентификатор алгоритма в строку} end; end; {генерация ключа подписи (Signature Key)} if SKCheckBox.Checked then begin јвыполняется аналогично генерации ключа обмена ключамиј end; CryptReleaseContext(hProv, 0); end;
Обмен ключами
Теперь мы располагаем набором ключей, однако все они останутся мертвым грузом, до тех пор пока мы не получим возможности обмена с другими пользователями открытыми ключами. Для этого необходимо извлечь их из базы данных ключей и записать в файл, который можно будет передать своим корреспондентам. При экспорте данные ключа сохраняются в одном из трех возможных форматов:
PUBLICKEYBLOB - используется для сохранения открытых ключей. Поскольку открытые ключи не являются секретными, они сохраняются в незашифрованном виде;
PRIVATEKEYBLOB - используется для сохранения ключевой пары целиком (открытого и закрытого ключей). Эти данные являются в высшей степени секретными, поэтому сохраняются в зашифрованном виде, причем для шифрования используется сеансовый ключ (и, соответственно, симметричный алгоритм);
SIMPLEBLOB - используется для сохранения сеансовых ключей. Для обеспечения секретности данные ключа шифруются с использованием открытого ключа получателя сообщения.
Экспорт ключей в CryptoAPI выполняется функцией CryptExportKey (экспортируемый ключ, ключ адресата, формат, флаги, буфер, размер буфера):
экспортируемый ключ - дескриптор нужного ключа;
ключ адресата - в случае сохранения открытого ключа должен быть равен нулю (данные не шифруются);
формат - указывается один из возможных форматов экспорта (PUBLICKEYBLOB, PRIVATEKEYBLOB, SIMPLEBLOB);
флаги - зарезервирован на будущее (должен быть равен нулю);
буфер - содержит адрес буфера, в который будет записан ключевой BLOB (Binary Large OBject - большой двоичный объект);
размер буфера - при вызове функции в этой переменной должен находиться доступный размер буфера, а по окончании работы в нее записывается количество экспортируемых данных. Если размер буфера заранее не известен, то функцию нужно вызвать с параметром буфер, равным пустому указателю, тогда размер буфера будет вычислен и занесен в переменную размер буфера.
Экспорт ключевой пары целиком, включая и закрытый ключ, может понадобиться для того, чтобы иметь возможность подписывать документы на различных компьютерах (например, дома и на работе), или для сохранения страховочной копии. В этом случае нужно создать ключ шифрования на основании пароля (см. "К+П", № 10/2002) и передать дескриптор этого ключа в качестве второго параметра функции CryptExportKey.
Запросить у криптопровайдера дескриптор самого' экспортируемого ключа позволяет функция CryptGetUserKey (провайдер, описание ключа, дескриптор ключа). Описание ключа - это либо AT_KEYEXCHANGE, либо AT_SIGNATURE.
Экспорт асимметричных ключей во всем возможном многообразии можно осуществить при помощи формы.
Ниже приведены наиболее важные фрагменты программы:
procedure TExportForm.OKBtnClick(Sender: TObject); var cont: PChar; err: string; hProv: HCRYPTPROV; key, expKey: HCRYPTKEY; pbuf: PBYTE; buflen: DWORD; f: file; hash: HCRYPTHASH; begin {если ни один ключ не выбран - выход} if not (KEKCheckBox.Checked or SKCheckBox.Checked) then exit; {если нужен пароль, т.е. экспортируется ключевая пара целиком} if PasswEdit.Enabled and (PasswEdit.Text < > Passw2Edit.Text) then begin MessageDlg('Ошибка при вводе пароля! Повторите ввод.', mtError, [mbOK], 0); exit; end;
…
"считываем" имя контейнера и подключаемся к криптопровайдеру
…
если нужен ключ шифрования - создаем его на основании пароля
…
{ключ обмена ключами} if KEKCheckBox.Checked then repeat {получаем дескриптор ключа} CryptGetUserKey(hProv, AT_KEYEXCHANGE, @key); {пытаемся определить размер буфера для экспорта ключа} if (WhatRadioGroup.ItemIndex = 0) then CryptExportKey(key, 0, PUBLICKEYBLOB, 0, nil, @bufLen) else CryptExportKey(key, expKey, PRIVATEKEYBLOB, 0, nil, @bufLen); GetMem(pbuf, bufLen); {экспортируем данные} if (WhatRadioGroup.ItemIndex = 0) then CryptExportKey(key, 0, PUBLICKEYBLOB, 0, pbuf, @bufLen) else CryptExportKey(key, expKey, PRIVATEKEYBLOB, 0, pbuf, @bufLen); {освобождаем дескриптор ключа обмена ключами (сам ключ при этом не уничтожается)} CryptDestroyKey(key); SaveDialog1.Title := 'Укажите файл для сохранения ключа обмена ключами'; if SaveDialog1.Execute then begin AssignFile(f, SaveDialog1.FileName); rewrite(f, 1); BlockWrite(f, pbuf^, bufLen); CloseFile(f); MessageDlg('Ключ обмена ключами успешно сохранен', mtInformation, [mbOK], 0); end; until true; {KeyExchange} {ключ подписи} if SKCheckBox.Checked then repeat јаналогично ключу обмена ключамиј until true; {Signature} end;
…
если создавался ключ на основании пароля - уничтожаем его,
после чего освобождаем контекст криптопровайдера
…
Экспортированные таким образом открытые части ключей понадобятся нам для проверки подписи и расшифровки сеансового ключа.
Импорт ключевых пар во вновь созданный контейнер - это самостоятельная процедура. Необходимо запросить у пользователя название контейнера и пароль, подключиться к провайдеру, создать на основании пароля ключ, считать из файла импортируемые данные в буфер, после чего воспользоваться функцией CryptImportKey (провайдер, буфер, длина буфера, ключ для расшифровки, флаги, импортируемый ключ). Если нужно обеспечить возможность экспорта импортируемой ключевой пары впоследствии, то в параметре флаги необходимо передать значение CRYPT_EXPORTABLE; в противном случае вызов для данной ключевой пары функции CryptExportKey приведет к ошибке.
Мы уже обсуждали, что при работе с асимметричными алгоритмами важно убедиться, что открытый ключ действительно принадлежит тому, кого вы считаете его хозяином, и не был подменен злоумышленником. Простейшим способом обеспечить аутентичность ключа является побайтная сверка с оригиналом, хранящимся у его хозяина. Для этого можно просто позволить пользователю просмотреть экспортированные данные в шестнадцатеричном виде - например, открыть файл, в который был записан открытый ключ, и вывести его содержимое в окно просмотра.
Электронная цифровая подпись
Для создания электронной цифровой подписи необходимо вычислить хеш заданного файла и зашифровать этот "цифровой отпечаток сообщения" своим закрытым ключом - "подписать". Чтобы подпись впоследствии можно было проверить, необходимо указать, какой алгоритм хеширования использовался при ее создании. Поэтому подписанное сообщение должно иметь структуру.
Подписать вычисленный хеш в CryptoAPI позволяет функция CryptSignHash (хеш, описание ключа, комментарий, флаги, подпись, длина подписи). Вторым параметром может быть либо AT_KEYEXCHANGE, либо AT_SIGNATURE (в нашем случае логичнее использовать ключ подписи). Третий параметр в целях безопасности настоятельно рекомендуется оставлять пустым (nil). Флаги в настоящее время также не используются - на месте этого аргумента должен быть нуль. Готовую электронную подпись функция запишет в буфер, адрес которого содержится в предпоследнем параметре, последний же параметр будет содержать длину подписи в байтах.
procedure TSigningForm.SignBtnClick(Sender: TObject); var cont: PChar; err: string; hProv: HCRYPTPROV; key: HCRYPTKEY; alg: ALG_ID; hash: HCRYPTHASH; infile, outfile: file; size: DWORD; buf: array[0..511] of byte; signature: PBYTE; begin {проверка существования выбранного файла} if not FileExists(DataNameEdit.Text) then begin MessageDlg('Неверное имя файла!', mtError, [mbOK], 0); exit; end; AssignFile(infile, DataNameEdit.Text); … "считываем" имя контейнера и подключаемся к нему … case HashRadioGroup.ItemIndex of 0: alg := CALG_MD5; 1: alg := CALG_SHA; end; CryptCreateHash(hProv, alg, 0, 0, @hash); SaveDialog1.Title := 'Задайте имя файла для хранения подписанных данных'; if SaveDialog1.Execute then begin AssignFile(outfile, SaveDialog1.FileName); rewrite(outfile, 1); {записываем в файл идентификатор алгоритма хеширования} BlockWrite(outfile, alg, 4); reset(infile, 1); size := FileSize(infile); {записываем размер подписываемых данных} BlockWrite(outfile, size, 4); {пишем сами данные и вычисляем хеш:} while not eof(infile) do begin BlockRead(infile, buf, 512, size); BlockWrite(outFile, buf, size); CryptHashData(hash, @buf, size, 0); end; CloseFile(infile); {выясняем размер подписи} CryptSignHash(hash, AT_SIGNATURE, nil, 0, nil, @size); {создаем подпись} GetMem(signature, size); CryptSignHash(hash, AT_SIGNATURE, nil, 0, signature, @size); BlockWrite(outfile, size, 4); BlockWrite(outfile, signature^, size); CloseFile(outfile); end; … уничтожаем хеш - объект и освобождаем контекст … end;
Чтобы проверить правильность подписи, получатель подписанного сообщения должен иметь файл с открытым ключом подписи отправителя. В процессе проверки подписи этот ключ импортируется внутрь криптопровайдера. Проверка выполняется функцией CryptVerifySignature (хеш, подпись, длина подписи, открытый ключ, комментарий, флаги). О последних двух аргументах можно сказать то же, что и о параметрах комментарий и флаги функции CryptSignHash, назначение же остальных должно быть понятно. Если подпись верна, функция возвращает true. Значение false в качестве результата может свидетельствовать либо о возникновении ошибки в процессе проверки, либо о том, что подпись оказалась неверной. В последнем случае функция GetLastError вернет ошибку NTE_BAD_SIGNATURE. Для примера приведем наиболее значимые фрагменты программы проверки подписи:
procedure TMainForm.VerifyItemClick(Sender: TObject); var err: string; hProv: HCRYPTPROV; key: HCRYPTKEY; alg: ALG_ID; hash: HCRYPTHASH; infile: file; size, test, textsize: DWORD; buf: PBYTE; signature, signkey: PBYTE; begin … получаем контекст криптопровайдера … OpenDialog1.Title := 'Укажите файл с подписанными данными'; if OpenDialog1.Execute then begin AssignFile(infile, OpenDialog1.FileName); reset(infile, 1); {считываем идентификатор алгоритма хеширования} BlockRead(infile, alg, 4); {считываем размер подписанных данных и сами данные} BlockRead(infile, textsize, 4); GetMem(buf, textsize); BlockRead(infile, buf^, textsize, test); if test < textsize then begin MessageDlg('Неверный формат файла! Процесс прерван.', mtError, [mbOK], 0); exit; end; {считываем размер подписи и саму подпись} BlockRead(infile, test, 4); GetMem(signature, test); BlockRead(infile, signature^, test); CloseFile(infile); end else exit; … создаем хеш - объект и хешируем данные … OpenDialog1.Title := 'Укажите файл с открытым ключом подписи'; if OpenDialog1.Execute then begin AssignFile(infile, OpenDialog1.FileName); reset(infile, 1); size := FileSize(infile); GetMem(signkey, size); BlockRead(infile, signkey^, size); CloseFile(infile); end else exit; {импортируем открытый ключ подписи отправителя} CryptImportKey(hProv, signkey, size, 0, 0, @key); FreeMem(signkey, size); {проверяем подпись} if CryptVerifySignature(hash, signature, test, key, nil, 0) then begin MessageDlg('Подпись верна.', mtInformation, [mbOK], 0); {сохраняем подписанные данные} SaveDialog1.Title := 'Укажите имя файла для сохранения данных'; if SaveDialog1.Execute then begin AssignFile(infile, SaveDialog1.FileName); rewrite(infile, 1); BlockWrite(infile, buf^, textsize); CloseFile(infile); end; end else begin case int64(GetLastError) of NTE_BAD_SIGNATURE: err := 'Подпись неверна!'; {обработка других ошибок} else err := 'Ошибка при проверке подписи: Unknown error'; end; MessageDlg(err, mtError, [mbOK], 0); end; … уничтожаем хеш - объект и импортированный ключ и освобождаем контекст криптопровайдера … end;