Организация защиты данных средствами WinAPI

       

Общие положения CryptoAPI




2.1 Интерфейс CryptoAPI

В сфере защиты компьютерной информации криптография применяется в основном для: шифрования и дешифрования данных; а также создания и проверки цифровых подписей. Шифрование данных позволяет ограничить доступ к конфиденциальной информации, сделать ее нечитаемой и непонятной для посторонних. Применение цифровых подписей оставляет данные открытыми, но дает возможность верифицировать отправителя и проверять целостность полученных данных. Для защиты информации специалистами Microsoft был разработан интерфейс CryptoAPI, который является основой средств защиты Microsoft Internet Security Framework. Он позволяет создавать приложения, использующие криптографические методы и обеспечивает базовые функции защиты для безопасных каналов и подписи кода. Реализация CryptoAPI в Internet Explorer позволяет нам легко интегрировать со своими приложениями усиленные средства шифрования. Интерфейс CryptoAPI обеспечивает API высокого уровня для аутентификации, подписи, шифрования/дешифрации, а также полную инфраструктуру защиты с общим ключом. Благодаря данной инфраструктуре, можно воспользоваться функциями управления сертификатами, такими как запрос на создание сертификата, его сохранение или верификация.

Программный интерфейс CryptoAPI фирмы Microsoft предоставляет возможности для добавления в приложение, основанное на базе Win32, функций аутентификации, шифрования, дешифрования и электронной подписи.

Задачами CryptoAPI являются:

  • Аутентификация сетевых пользователей
  • Шифрование и дешифрование сетевых сообщений
  • Шифрование и дешифрование данных
  • Создание цифровой подписи и ее подтверждение.

  • CryptoAPI состоит из пяти функциональных различных областей, как показано на иллюстрации, взаимодействующих с приложением. Напрямую к провайдеру (CSP) приложение обратиться не может (см. Рис.1).


    Рис.1 Интерфейс CryptoAPI

    Открытость интерфейса: открытая архитектура CryptoAPI позволяет выбирать Cryptographic Service Provider (CSP) по своему усмотрению. CryptoAPI доступен в операционных системах Windows, Macintosh и UNIX. Кроме того, в CryptoAPI поддерживаются стандартные форматы сертификатов X.509 версии 3, ASN.1 и DER.

    2.2. Симметричные и асимметричные шифры

    Основное назначение любого шифра – обеспечение возможности передачи сообщения по незащищенным каналам (не обязательно сетевым) с защитой этого сообщения от прочтения посторонними лицами.

    Сначала дадим краткое определение криптографических функций шифрования и дешифрования.

    Пусть есть две функции E(M1,K1) и D(M2,K2), зависящие от сообщений M1 и M2 и ключей K1 и K2, такие, что D(E(M,K1),K2)=M для некоторой пары ключей K1 и K2 и любого M, тогда E(.) и D(.) – функции шифрования и дешифрования, соответственно. Результат C=E(M,K1) называется криптограммой (или шифрограммой).

    К функциям E(.) и D(.) предъявляются следующие требования:


  • Они должны быть легко вычислимы для любых M, если известны K1 и K2.



  • Вычисление D(M,?) – тяжелая задача при неизвестном ключе K2 (т.е., не зная ключа дешифрования, невозможно вычислить исходное сообщение по криптограмме).



  • Вычисление ключей K1 и K2 – тяжелая задача при наличии некоторого набора пар {M,E(M,K1)} (имея набор криптограмм и исходных сообщений, невозможно вычислить ключи).



  • Вычисление K2 (в случае K2 отличного от K1) при известном K1 – также должно быть трудной задачей (это требование относится к асимметричным шифрам. Для цифровой подписи K2 и K1 меняются ролями).

    Надежность шифра определяется именно по его соответствию вышеперечисленным требованиям, при этом считается, что алгоритмы вычисления E(.) и D(.) известны всем (есть еще вариант так называемой Security by obscurity, т.е. защиты из-за неизвестности алгоритма, но он при оценке стойкости шифров не рассматривается).

    Если K1=K2, то шифр называется симметричным, в противном случае – асимметричным. Примеры известных симметричных шифров – DES, IDEA, Blowfish, ГОСТ, асимметричных – RSA, ECC.

    Шифры также бывают блочными и потоковыми. Блочный шифр работает с сообщениями фиксированного размера (например, 64 бита), а поточный – шифрует весь поток данных посимвольно (например, побайтно). Известные блочные шифры – DES, IDEA, Blowfish, потоковые – RC4. Блочность шифра не означает невозможность шифрования им сообщений, превышающих по длине размер блока.

    2.3. Блочные шифры

    Блочные шифры считаются более надежными, нежели поточные, поскольку каждый блок текста подвергается сложным преобразованиям. Тем не менее, одних только этих преобразований оказывается недостаточно для обеспечения должного уровня безопасности - важно, каким образом они применяются к исходному тексту в процессе шифрования.

    Наиболее простой и интуитивно понятный способ состоит в том, чтобы разбить исходный текст на блоки соответствующего размера, а затем отдельно каждый блок подвергнуть шифрующему преобразованию. Такой режим использования блочных шифров называют электронной кодовой книгой (ECB - electronic codebook). Его главный недостаток состоит в том, что одинаковые блоки исходного текста при шифровании дадут одинаковые же блоки шифр-текста - а это может существенно облегчить противнику задачу взлома. Поэтому режим ECB не рекомендуется использовать при шифровании текстов, по длине превышающих один блок - в таких случаях лучше воспользоваться одним из режимов, связывающих различные блоки между собой. По умолчанию в CryptoAPI блочные шифры используются в режиме сцепления блоков шифр-текста (CBC - cipher block chaining). В этом режиме при шифровании очередной блок исходного текста вначале комбинируется с предыдущим блоком шифр-текста (при помощи побитового исключающего ИЛИ), а затем полученная последовательность битов поступает на вход блочного шифра (рис. 2). Образующийся на выходе блок шифр-текста используется для шифрования следующего блока. Самый первый блок исходного текста также должен быть скомбинирован с некоторой последовательностью битов, но "предыдущего блока шифр-текста" еще нет; поэтому режимы шифрования с обратной связью требуют использования еще одного параметра - он называется инициализирующим вектором (IV - initialization vector).


    Рис.2 Схема блочного шифра

    Инициализирующий вектор должен генерироваться отдельно с помощью уже известной нам функции CryptGenRandom и, как и солт-значение, передаваться вместе с ключом в открытом виде. Размер IV равен длине блока шифра. Например, для алгоритма RC2, поддерживаемого базовым криптопровайдером Microsoft, размер блока составляет 64 бита (8 байтов).

    Потоковые шифры тут рассматриваться не будут т.к. это выходит за рамки данной темы. Однако стоит немного остановиться на асимметричных шифрах, а точнее на популярной их разновидности – шифрах с открытым ключом. Шифр с открытым ключом имеет два ключа – открытый (public), который можно свободно распространять, и закрытый (private), который держится в секрете. Зашифровать сообщение может кто угодно, но расшифровать его сможет только владелец закрытого ключа. Системы шифрования с открытым ключом, как правило, основываются на сложности разрешения какой-либо математической задачи. Например, популярный алгоритм RSA основан на сложности разложения на множители произведения двух больших простых чисел. Ниже этот алгоритм будет рассмотрен подробнее, т.к. понимание основ его работы пригодится в дальнейшем.

    2.4. Алгоритм RSA

    Для генерации ключевой пары выбираются два больших (по современным требованиям надежности не менее 512 бит) простых числа (некоторые популярные алгоритмы их генерации рассмотрены в [1]) p и q. Затем вычисляется их произведение n=p*q и выбирается случайное число e, так что e взаимно просто с (p-1)* (q-1). Также вычисляется число d, такое, что d*e=1 mod (p-1)*(q-1)

    Это означает, что d*e=1+k*(p-1)*(q-1),
    где k – некое целое число.

    Открытым ключом является пара e и n, закрытым – d и n. Алгоритм позволяет шифровать сообщения m меньшие, либо равные n (как целые неотрицательные числа). Большие сообщения следует разбивать на части.

    Шифрование осуществляется следующим способом:
    c=m^e mod n (возведение m в степень e по модулю n),

    а дешифрование производится так:
    m=c^d mod n.

    Гарантию того, что получится верный результат, дает малая теорема Ферма. Для любого a меньше, либо равного n:

    a^phi(n)=1 mod n, где phi(n) – число целых положительных чисел меньших n и взаимно простых с ним. Для n=p*q, phi(n)=(p-1)*(q-1) и
    (m^e)^d mod n = m^(1+k*phi(n)) mod n=m.

    Начиная с Windows 2000 расширенный (Enhanced) криптопровайдер поддерживает прямое шифрование данных по алгоритму RSA. Максимальный размер данных, которые можно зашифровать за один вызов CryptEncrypt, равен размеру ключа минус 11 байт. Дело в том, что при шифровании добавляется обязательный заполнитель (padding), который впоследствии проверяется при дешифрации. Соответственно, использование шифра RSA может быть целесообразно только при небольших объемах шифруемых данных (например, при обмене ключами) из-за существенного увеличения объема шифрованного текста и относительно медленной работе алгоритма RSA по сравнению с блочными шифрами.

    2.5. Хеши

    Основное предназначение криптографических хешей – контроль подлинности данных путем вычисления от них некоторой функции H(.), дающей результат фиксированной (и обычно небольшой длины). Функция H(.) должна удовлетворять следующим требованиям:

    Для любых сообщений m, h=H(m) легко вычисляема.
    Задача нахождения такого u (отличного от m), чтобы H(u)=h, должна являться трудной при неизвестном m.
    Задача нахождения такого u, что H(u)=H(m) является трудной при известном m.

    Большинство популярных хеш-функций генерируют хеш длиной 128 бит и более. Примерами наиболее распространенных хеш-функций являются MD5 и SHA. Значения хеш-функций часто используются в системах электронной цифровой подписи для генерации дайджеста сообщения, который затем и подписывается тем или иным алгоритмом. Также хеш-функции применяются в системах аутентификации для проверки паролей – открытый пароль пользователя не должен храниться в системе, вместо него хранится его хеш, который затем и сравнивается с хешем от пароля, вводимого пользователем при входе в систему.

    3. Работа с CryptoAPI

    3.1. Криптопровайдеры, инициализация и деинициализация

    Любой сеанс работы с CryptoAPI начинается с инициализации (получения контекста). Инициализация выполняется при помощи функции CryptAcquireContext. В качестве параметров эта функция принимает имя контейнера ключей, имя криптопровайдера, тип провайдера и флаги, определяющие тип и действия с контейнером ключей и режим работы криптопровайдера:

    BOOL WINAPI CryptAcquireContext(HCRYPTPROV* phProv,LPCTSTR pszContainer, LPCTSTR pszProvider,DWORD dwProvType,DWORD dwFlags);

    Криптопровайдер – это независимый модуль, содержащий библиотеку криптографических функций со стандартизованным интерфейсом. Криптопровайдер отвечает за реализацию функций интерфейса, а также играет роль хранилища для ключей всех типов. Подобная архитектура позволяет переходить от одного провайдера к другому с минимальными изменениями исходного кода, так как интерфейс (т. е. сами функции) не меняется.


    Рис.3 Алгоритм работы криптопровайдеров

    Прежде чем использовать какие-либо функции Crypto API, необходимо запустить криптопровайдер. Делается это с помощью функции CryptAcquireContext:

    BOOL CRYPTFUNC CryptAcquireContext(
    HCRYPTPROV* hCryptProvider,//дескриптор провайдера, out-параметр
    LPCTSTR pszContainer, // имя контейнера ключей
    LPCTSTR pszProvider, // имя провайдера
    DWORD dwProvType, // тип провайдера
    DWORD dwFlags // флаги
    )

    Кроме инициализации криптопровайдера данную функцию можно использовать для создания и удаления контейнеров ключей. Для этого параметру dwFlags присваивается значение, соответственно, CRYPT_NEW-KEYSET и CRYPT_DELETEKEYSET.

    Функция CryptAcquireContext работает в два этапа: сначала она ищет криптопровайдер по имени и типу, указанному в аргументах, а затем контейнер ключей с заданным именем.

    По окончании работы с криптопровайдером необходимо вызвать функцию CryptReleaseContext

    Криптопровайдеры отличаются друг от друга:





  • составом функций (например, некоторые криптопровайдеры не выполняют шифрование данных, ограничиваясь созданием и проверкой цифровых подписей);
  • требованиями к оборудованию (специализированные криптопровайдеры могут требовать устройства для работы со смарт-картами для выполнения аутентификации пользователя);
  • алгоритмами, осуществляющими базовые действия (создание ключей, хеширование и пр.).

    По составу функций и обеспечивающих их алгоритмов криптопровайдеры подразделяются на типы. Например, любой CSP типа PROV_RSA_FULL поддерживает как шифрование, так и цифровые подписи, использует для обмена ключами и создания подписей алгоритм RSA, для шифрования — алгоритмы RC2 и RC4, а для хеширования — MD5 и SHA.

    В зависимости от версии операционной системы состав установленных криптопровайдеров может существенно изменяться. Однако на любом компьютере с Windows можно найти Microsoft Base Cryptographic Provider, относящийся к уже известному нам типу PROV_RSA_FULL. Именно с этим провайдером по умолчанию будут взаимодействовать все программы.

    Приложение обращается к криптопровайдеру не напрямую, а через CryptoAPI. При этом вначале необходимо запросить у системы контекст криптопровайдера.

    Первым делом, хотя бы из любопытства, выясним, какие же криптопровайдеры установлены в системе.
    Для этого нам понадобятся четыре функции CryptoAPI:

    CryptEnumProviders (i, резерв, флаги, тип, имя, длина_имени) — возвращает имя и тип i-го по порядку криптопровайдера в системе (нумерация начинается с нуля);

    CryptAcquireContext (провайдер, контейнер, имя, тип, флаги) — выполняет подключение к криптопровайдеру с заданным типом и именем и возвращает его дескриптор (контекст). При подключении мы будем передавать функции флаг CRYPT_VERIFYCONTEXT, служащий для получения контекста без подключения к контейнеру ключей;

    CryptGetProvParam (провайдер, параметр, данные, размер_данных, флаги) — возвращает значение указанного параметра провайдера, например, версии (второй параметр при вызове функции — PP_VERSION), типа реализации (программный, аппаратный, смешанный — PP_IMPTYPE), поддерживаемых алгоритмов (PP_ENUMALGS). Список поддерживаемых алгоритмов при помощи этой функции может быть получен следующим образом: при одном вызове функции возвращается информация об одном алгоритме; при первом вызове функции следует передать значение флага CRYPT_FIRST, а при последующих флаг должен быть равен 0;

    CryptReleaseContext (провайдер, флаги) — освобождает дескриптор криптопровайдера.

    Каждая из этих функций, как и большинство других функций CryptoAPI, возвращает логическое значение, равное true, в случае успешного завершения, и false — если возникли ошибки. Код ошибки может быть получен при помощи функции GetLastError.

    Существует около семи стандартных провайдеров, предустановленных в системе. Для примеров понадобятся два из них – Microsoft Base Cryptographic Provider (MS_DEF_PROV) и Microsoft Enhanced Cryptographic Provider (MS_ENHANCED_PROV).

    Стоит отметить, что Enhanced-провайдер присутствует только на тех машинах, где установлена поддержка 128-битного шифрования (она автоматически устанавливается вместе с Internet Explorer 6.0).

    Программная часть криптопровайдера представляет собой dll-файл, подписанный Microsoft; периодически Windows проверяет цифровую подпись, что исключает возможность подмены криптопровайдера.

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

    Контейнеры бывают двух типов – пользовательские (этот тип используется по умолчанию) и машинные (CRYPT_MACHINE_KEYSET). Тип контейнера задается флагом при получении контекста.

    Для первоначального создания контейнера нужно вызвать CryptAcquireContext с флагом CRYPT_NEWKEYSET.
    Для удаления контейнера требуется указать флаг CRYPT_DELETEKEYSET.

    Если приложению не требуется доступ к контейнеру ключей (например, приложение вычисляет хеш MD5), то стоит вызывать CryptAcquireContext с флагом CRYPT_VERIFYCONTEXT, передавая NULL вместо имени контейнера.

    Следующий пример демонстрирует инициализацию CryptoAPI для последующего вычисления хеша MD5:

    HCRYPTPROV hProv;
    if(!CryptAcquireContext(&hProv, NULL, MS_DEF_PROV, PROV_RSA_FULL, CRYPT_VERIFYCONTEXT))
    {
    MessageBoxA("Failed to acquire cryptographic context", "Error", MB_OK|MB_ICONERROR);
    return;
    }

    //здесь что-то делаем
    CryptReleaseContext(hProv,0);

    Деинициализация CryptoAPI выполняется с помощью функции CryptReleaseContext, единственным значащим параметром которой является полученный ранее хэндл криптографического контекста.

    3.2. Работа с ключами

    3.2.1. Криптографические ключи и выполняемые функции

    В CryptoAPI существуют ключи двух типов:





  • сессионные ключи (session keys)
  • пары открытый/закрытый ключ (public/private key pairs).

    Сессионные ключи.

    Это симметричные ключи, так как один и тот же ключ применяется и для шифрования, и для расшифровки. Сессионные ключи меняются. Алгоритмы, использующие сессионные ключи (так называемые симметричные алгоритмы), – RC2, RC4, DES. Microsoft RSA Base Provider работает с 40-разрядными сессионными ключами.

    Пары открытый/закрытый ключ используются в так называемых асимметричных алгоритмах шифрования. Если шифрование выполнялось одним ключом из пары, то Дешифрование производится другим. Открытые (public) ключи могут передаваться другим лицам для проверки цифровых подписей и шифрования пересылаемых данных. Длина открытого ключа в Microsoft RSA Base Provider составляет 512 разрядов. Закрытые (private) ключи не могут быть экспортированы; они используются для создания цифровых подписей и дешифровки данных. Закрытый ключ должен быть известен только его владельцу. При шифровании с открытым ключом жизненно важна абсолютно достоверная ассоциация открытого ключа и передавшей его стороны, поскольку в обратном случае возможна подмена открытого ключа и осуществление несанкционированного доступа к передаваемым зашифрованным данным. Необходим механизм, гарантирующий достоверность корреспондента, например, применение сертификата, созданного авторизованным генератором сертификатов.

    Обычно сертификаты содержат дополнительную информацию, позволяющую идентифицировать владельца личного ключа, соответствующего данному открытому ключу. Сертификат должен быть подписан авторизованным генератором сертификатов.

    Выполняемые функции:

    Функции кодирования сертификатов:

    Эти функции управляют сертификатами и сопутствующими данными через сеть OSI (соединение открытых систем, семиуровневая модель) как описано в CCIT X.200. Методы OSI, описывающие абстрактные объекты, используют абстрактную синтаксическую нотацию один (ASN.1), как описано в CCITT X.209.

    Функции базы сертификатов:

    Используются для хранения сертификатов и управления ими. Пользователь со временем может собрать весьма много сертификатов. Обычно это сертификаты, описывающие самого пользователя и сертификаты сущностей с которыми он взаимодействует. Обычно для каждой сущности есть несколько сертификатов - связок, используемых для проверки отслеживания существующих сертификатов у авторитета сертификатов (обычно это сайт с сертификатами, отвечающий своим авторитетом за их верность).

    Базовые криптографические функции:

    Используются для наиболее полного использования криптографических возможностей в приложении. Это функции взаимодействующие с провайдером. Все криптографические операции выполняются независимыми модулями называемыми Cryptographic Service Providers (CSP). Каждый CSP предлагает различные реализации криптографической поддержки используемой через CryptoAPI. Некоторые провайдеры несут более сильные алгоритмы, некоторые содержат физические компоненты (смарткарты, криптоускорители). В дополнение, некоторые CSP могут напрямую работать с пользователем при использовании личного ключа или подписи.

    Функции сообщений низкого уровня:

    Используются для быстрого создания сообщений отвечающих требованиям PKCS#7(RFC2315 , RFC2316). Эти функции предназначены для шифрования данных при передаче и дешифровке их при приеме, а также для дешифровки и проверке подписей получаемых сообщений.

    Упрощенные функции сообщений:

    Они находятся на верхнем уровне функций сообщений и в принципе представляют низкоуровневые функции сообщений и сертификатов в одном. Они уменьшают число вызовов функций для приложения.

    Хранение ключей

    Криптопровайдер отвечает за хранение и разрушение ключей. Программист не имеет доступа непосредственно к двоичным данным ключа, за исключением операций экспорта открытых ключей. Вся работа с ключами производится через дескрипторы (handle). В CryptoAPI ключи для шифрования / дешифрования и создания / проверки подписей разделены. Называются они соответственно «пара для обмена ключами» и «пара для подписи». База данных ключей состоит из контейнеров, в каждом из которых хранятся ключи, принадлежащие определенному пользователю. Контейнер ключей имеет уникальное имя и содержит пару для обмена и пару для подписи. Все ключи хранятся в защищенном виде. По умолчанию для каждого пользователя создается контейнер с именем этого пользователя. Можно создавать дополнительные контейнеры и назначать им произвольные имена, которые обязательно должны быть уникальными (см. Рис. 4).


    Рис. 4 База данных криптографических ключей.

    3.2.2. Генерация ключей и обмен ключами

    Для генерации ключей в CryptoAPI предусмотрены две функции – CryptGenKey и CryptDeriveKey. Первая из них генерирует ключи случайным образом, а вторая – на основе пользовательских данных. При этом гарантируется, что для одних и тех же входных данных CryptDeriveKey всегда выдает один и тот же результат. Это способ генерации ключей может быть полезен для создания симметричного ключа шифрования на базе пароля. Более подробно следует остановиться на функции CryptGenKey, которая используется чаще всего. Эта функция имеет прототип:

    BOOL WINAPI CryptGenKey(HCRYPTPROV hProv, ALG_ID Algid, DWORD dwFlags, HCRYPTKEY* phKey);

    Первый и четвертый параметры говорят сами за себя. Вторым параметром передается идентификатор алгоритма шифрования, для которого генерируется ключ (например, CALG_3DES). При генерации ключевых пар RSA для шифрования и подписи используются специальные значения AT_KEYEXCHANGE и AT_SIGNATURE. Третий параметр задает различные опции ключа, которые зависят от алгоритма и провайдера. Например, старшие 16 битов этого параметра могут задавать размер ключа для алгоритма RSA. Подробное описание всех флагов можно найти в MSDN. Здесь я упомяну только один флаг - CRYPT_EXPORTABLE, который позволяет экспортировать закрытые ключи RSA из контейнера (по умолчанию это запрещено). Для других ключей этот параметр смысла не имеет – открытые ключи являются свободно экспортируемыми, а сессионные ключи вообще не хранятся внутри контейнера, т.ч. их обязательно нужно экспортировать.

    Параметры, которые нельзя задать при генерации ключа (например, инициализационные векторы), можно установить уже после его создания с помощью функции CryptSetKeyParam.

    Обмен ключами в CryptoAPI реализуется с помощью функций CryptExportKey и CryptImportKey, имеющих следующие прототипы:

    BOOL WINAPI CryptExportKey(HCRYPTKEY hKey,HCRYPTKEY hExpKey, DWORD dwBlobType, DWORD dwFlags,BYTE* pbData,DWORD* pdwDataLen);

    BOOL WINAPI CryptImportKey(HCRYPTPROV hProv, BYTE* pbData, DWORD dwDataLen, HCRYPTKEY hImpKey, DWORD dwFlags, HCRYPTKEY* phKey);

    В качестве ключей экспорта/импорта могут использоваться либо ключевая пара RSA (с типом AT_KEYEXCHANGE), либо симметричный сеансовый ключ. Параметр dwBlobType зависит от того, какой ключ экспортируется (импортируется), и задает тип структуры, в которую помещается экспортируемый ключ. Для открытого ключа это PUBLICKEYBLOB, и ключ экспорта/импорта при этом лишен смысла и должен быть нулем. Для закрытого ключа это PRIVATEKEYBLOB, и в качестве ключа экспорта может использоваться сеансовый ключ. Для сеансового ключа это обычно SIMPLEBLOB, а экспортируется он, как правило, на открытом ключе получателя.

    Описание флагов можно найти в MSDN. Можно выделить среди них флаг CRYPT_OAEP, который заставляет криптопровайдера использовать формат PKCS #1 версии 2 при сохранении сессионного ключа с шифрованием RSA. Ключ, сохраненный в этом формате, затем может быть расшифрован другими системами шифрования. Если же этот флаг не указан, то ключ сохраняется в каком-то известном только CryptoAPI формате, и использовать его где-либо еще вряд ли удастся.

    И, наконец, параметры pbData и pdwDataLen задают адрес и размер буфера под структуру экспортируемого ключа. Если размер структуры не известен при написании программы, то можно определить требуемый размер буфера, установив в pbData ноль. В этом случае нужный размер возвращается в pdwDataLen.

    После окончания работы с ключом, его нужно уничтожить вызовом CryptDestroyKey. При этом закрытый ключ сохраняется в контейнере (если, конечно, не использовался режим CRYPT_VERIFYCONTEXT), а сессионные ключи уничтожаются совсем.

    В качестве примера ниже будет рассмотрено создание и экспорт пары ключей для шифрования RSA и симметричного ключа 3DES:

    //создание и экспорт пары ключей RSA
    if(CryptGenKey(hProv,AT_KEYEXCHANGE,1024<<16,&hKey)) //генерируем 1024-битный ключ
    {
    RSAPubKey1024 key; //эта структура описана в файле tools.h в примере rsakg
    DWORD dwLen=sizeof(RSAPubKey1024);

    //экспортируем ключ
    if(CryptExportKey(hKey,NULL,PUBLICKEYBLOB,0,(BYTE *)&key,&dwLen))
    {

    //. тут что-то делаем
    }
    CryptDestroyKey(hKey); //уничтожаем ключ
    }

    //генерация и экспорт ключа 3DES
    if(::CryptGenKey(hProv,CALG_3DES,CRYPT_EXPORTABLE,&hKey)) //генерируем ключ для 3DES
    {
    RSA1024KeyExchBLOB kb; //описание см. в tools.h
    dwLen=sizeof(RSA1024KeyExchBLOB);

    //экспортируем ключ 3DES на публичном ключе RSA
    if(::CryptExportKey(hKey,hPubKey,SIMPLEBLOB,0,(BYTE*)&kb,&dwLen))
    {

    //.
    }
    }

    Обмен симметричными ключами

    За обмен сессионными ключами в .NET отвечают классы RSAOAEPKeyExchangeFormatter/Deformatter и RSAPKCS1KeyExchangeFormatter/Deformatter. Они унаследованы от базовых классов AsymmetricKeyExchangeFormatter/Deformatter, предоставляющих методы CreateKeyExchange и DecryptKeyExchange для шифрования и дешифрации сессионных ключей, соответственно. Рассмотрим передачу сессионного ключа на небольшом примере:

    RSACryptoServiceProvider rsa1=new RSACryptoServiceProvider(1024); //получатель ключа
    RSAParameters rp=rsa1.ExportParameters(false);
    Console.WriteLine("Passing public key to sender...");

    //передаем открытый ключ отправителю
    RSACryptoServiceProvider rsa2=new RSACryptoServiceProvider(1024); //отправитель ключа
    Console.WriteLine("Importing receiver's public key...");

    //импортируем открытый ключ получателя
    rsa2.ImportParameters(rp);
    AsymmetricKeyExchangeFormatter kf = (AsymmetricKeyExchangeFormatter)new RSAOAEPKeyExchangeFormatter(rsa2);
    byte []key=new Byte[16]; //128-битный ключ
    byte []enckey=kf.CreateKeyExchange(key);
    Console.WriteLine("Sending encrypted session key to receiver...");

    //передаем зашифрованный сессионный ключ получателю
    AsymmetricKeyExchangeDeformatter kd = (AsymmetricKeyExchangeDeformatter)new RSAOAEPKeyExchangeDeformatter(rsa1);

    //Расшифровываем ключ
    byte []deckey=kd.DecryptKeyExchange(enckey);
    for(uint i=0;i<16;i++)
    if(deckey[i]!=key[i])
    {
    Console.WriteLine("Key exchange failed");
    return;
    }

    Console.WriteLine("Key exchange succeeded");

    Самым сложным, пожалуй, является передача публичного ключа от получателя отправителю и зашифрованного сессионного ключа в обратную сторону.

    3.2.3. Получение дескриптора ключа.

    Дескриптор открытого ключа можно получить, вызвав функцию CryptGetUserKey.

    3.2.4. Экспорт ключей.

    Операция экспорта выполняется при сохранении сессионных ключей и при передаче ключей третьим лицам. Двоичные данные ключа могут быть получены при помощи функции CryptExportKey:

    BOOL CRYPTFUNC CryptExportKey ( HCRYPTKEY hKeyToExport
    HCRYPTKEY hCryptKey
    DWORD dwBlobType
    DWORD dwFlags
    BYTE* pbData, // указатель на буфер
    DWORD* pdwDataLen ) // длина буфера

    Первый аргумент – дескриптор экспортируемого ключа.

    Второй аргумент – дескриптор ключа, которым шифруется экспортируемый ключ. Открытые ключи экспортируются в незашифрованном виде. В этом случае hCryptKey = 0. При экспорте сессионных и закрытых ключей необходимо их предварительно зашифровать. Аргумент hCryptKey должен содержать дескриптор открытого ключа получателя. При вызове с аргументом pbData = NULL функция вернет необходимую длину буфера по адресу, на который указывает аргумент pdwDataLen (см. Листинг 1).

    Листинг 1. Экспорт сессионного ключа.

    // Инициализировать криптопровайдер
    // Сгенерировать сессионный ключ
    // Получить дескриптор открытого ключа получателя
    // Определить размер памяти под ключ
    CryptExportKey( hSessionKey,
    hReceiverPublicKey, // открытый ключ получателя
    SIMPLEBLOB, 0, 0, &dwSessionKeyLen));
    BYTE * pbSessionKey = new BYTE[dwSessionKeyLen];

    // Зашифровать сессионный ключ
    CryptExportKey( hSessionKey, HReceiverPublicKey,
    SIMPLEBLOB, 0, pbSessionKey,
    &dwSessionKeyLen));

    После успешного завершения переменная dwSessionKeyLen будет содержать действительную длину BLOB-структуры ключа. Это значение необходимо сохранить для обратной операции – импорта ключа в криптопровайдер.

    3.2.5. Импорт ключей.

    Ключи импортируются функцией CryptImportKey:

    CryptImportKey ( HCRYPTPROV hProv
    BYTE* pbData
    DWORD dwDataLen
    HCRYPTKEY hCryptKey
    DWORD dwFlags
    HCRYPTKEY* phImportedKey )

    hCryptKey = 0 в том случае, если импортируемый ключ был, зашифрован асимметричным ключом или не был зашифрован вообще. Если импортируемый ключ шифровали сессионным ключом, то hCryptKey должен содержать дескриптор этого ключа. По окончании работы с ключом необходимо для его дескриптора вызвать функцию CryptDestroyKey(HCRYPTKEY hKey).

    3.3. Цифровая подпись

    Цифровая подпись – это двоичные данные небольшого объема, обычно не более 256 байт. Цифровая подпись есть не что иное, как результат работы хеш-алгоритма над исходными данными, зашифрованный закрытым ключом отправителя. Полученные в результате шифрования хеша двоичные данные и есть цифровая подпись (см. Рис. 5).


    Рис. 5 Создание цифровой подписи

    Получатель, чтобы удостовериться, что данные присланы именно от нас и не были искажены, производит следующие операции:





  • получает хеш от данных, (данные остались открытыми);
  • расшифровывает нашу цифровую подпись при помощи нашего открытого ключа;
  • сравнивает свой хеш и результат дешифровки.

    Если они эквивалентны, значит, данные были подписаны именно нами (см. Рис. 6).


    Рис. 6 Проверка цифровой подписи

    Подобный алгоритм позволяет любому лицу проверить подлинность подписи отправителя. Напомню, что открытый ключ отправителя, с помощью которого собственно и производится проверка, распространяется (как правило) свободно. Для работы с цифровыми подписями используются функции CryptCreateHash, CryptHashData, CryptSignHash, CryptVerifySignature, CryptDestroyHash.

    Создание цифровой подписи.

    Процесс создания подписи состоит из следующих этапов:



  • создание хеш-объекта функцией CryptCreateHash;
  • наполнение хеш-объекта данными (CryptHashData);
  • подписание хеша (CryptSignHash);
  • разрушение хеш-объекта (Crypt-DestroyHash).

    Аналогично функциям CryptEncrypt, проверка длины буфера для подписи производится вызовом функции CryptSignHash с нулем вместо указателя на данные. Как и у функции CryptEncrypt, указатели на данные и их длину являются параметрами типа [in, out] (см. Листинг 2).

    Листинг 2. Создание цифровой подписи.

    // Необходимые действия по инициализации криптопровайдера,
    // получению исходных данных

    // BYTE * pbData – указатель на заданные исходные данные
    // DWORD dwDataLen – длина исходных данных
    // ALG_ID alg_id – алгоритм получения хеш-данных

    HCRYPTHASH hHash = 0;
    CryptCreateHash(hProv, alg_id, 0 , 0, &hHash);
    CryptHashData ( hHash, pbData, dwDataLen, 0);

    // Подписать хэш
    CryptSignHash ( hHash, AT_SIGNATURE, 0, 0, pbData, &dwDataLen);
    CryptDestroyHash(hHash);

    Проверка цифровой подписи.

    Проверка подписи (см. Листинг 3) выполняется так:



  • создается хеш-объект функцией CryptCreateHash;
  • хеш-объект наполняется данными (CryptHashData);
  • подпись расшифровывается и результат сравнивается со «своим» хешем (CryptVerifySignature);
  • хеш-объект разрушается (CryptDes-troyHash).

    Листинг 3. Проверка цифровой подписи.

    // Необходимые действия по инициализации криптопровайдера,
    // получению подписи и подготовке открытого ключа отправителя

    // BYTE * pbData – указатель на буфер с данными
    // DWORD dwDataLen – длина исходных данных
    // BYTE * pbSignature – указатель на буфер с подписью
    // DWORD dwSigLen – длина подписи
    // ALG_ID algid – алгоритм получения хеш-данных
    // HCRYPTKEY hSigSenderKey – дескриптор открытого ключа отправителя из ключевой пары для подписи

    HCRYPTHASH hHash = 0;
    CryptCreateHash(hProv, alg_id, 0 , 0, &hHash));
    CryptHashData(hHash, pbData, dwDataLen,0);
    if ( ! CryptVerifySignature(hHash, pbSignature, dwSigLen, hSigSenderKey, 0, 0) )
    { if (GetLastError() == NTE_BAD_IGNATURE)
    { // неверная подпись }

    else

    { // другая ошибка }
    }

    CryptDestroyHash(hHash);

    3.4. Шифрование и дешифрование данных

    В CryptoAPI для шифрования и дешифрования используются и симметричный, и асимметричный алгоритмы. Симметричный алгоритм менее надежен, но работает намного быстрее, чем асимметричный. Поэтому в CryptoAPI применяется комбинация алгоритмов. Данные шифруются с помощью симметричного алгоритма с сессионным ключом, а сам сессионный ключ шифруется по асимметричному алгоритму открытым ключом получателя. Дешифрование происходит в обратном порядке: сначала закрытым ключом получателя дешифруется сессионный ключ, затем этим сессионным ключом дешифруются сами данные (см. Рис. 7 и 8).


    Рис. 7 Шифрование


    Рис. 8 Дешифрование

    Таким образом, расшифровать данные можно только, имея закрытый ключ из той же ключевой пары, что и открытый ключ, которым данные были зашифрованы. Для шифрования и дешифровки применяется функция (см. Листинг 4).

    BOOL CRYPTFUNC CryptEncrypt (
    HCRYPTKEY hKey, // дескриптор ключа для шифрования
    HCRYPTHASH hHash
    BOOL bFinal
    BYTE* pbData, // параметр [in, out
    DWORD* pdwDataLen, // параметр [in, out
    DWORD dwBufferLen )

    Последний и предпоследний параметры являются одновременно входными и выходными. Это означает, что зашифрованные данные записываются поверх исходных в буфер pbData, а длина данных, соответственно, в dwBufferLen.

    Как и в случае с функцией CryptExportKey, CryptEncrypt позволяет предварительно определить необходимый размер буфера под зашифрованные данные. Для этого нужно вызвать функцию с аргументами:

    pbData = NULL;
    pdwDataLen = длина исходных данных.
    Длину буфера функция вернет по адресу: pdwDataLen.
    Дешифрование данных производится аналогично функцией CryptDecrypt.

    Листинг 4. Шифрование большого блока данных блочным алгоритмом.

    Для ясности обработка ошибок в код не включена.
    #define ENCRYPT_BLOCK_SIZE 8 // где-то в header-файле

    В .cpp файле
    // Необходимые действия по инициализации криптопровайдера,
    // генерации сессионного ключа и получению исходных данных

    /*

    hSessionKey – сгенерированный сессионный ключ
    BYTE * pbData – указатель на заданные исходные данных
    DWORD dwDataLen – длина исходных данных
    */

    DWORD dwBlockLen = 1000 - 1000 % ENCRYPT_BLOCK_SIZE;
    DWORD dwBufferLen=0; // Размер буфера для блока данных,

    //который будет шифроваться за один шаг
    BYTE * pbBuffer = 0; // временный рабочий буфер
    BYTE * pbOutData = 0; // буфер для записи выходных данных

    if(ENCRYPT_BLOCK_SIZE > 1)
    dwBufferLen = dwBlockLen + ENCRYPT_BLOCK_SIZE;

    else

    dwBufferLen = dwBlockLen;
    pbBuffer = new BYTE[dwBufferLen];

    // Шифруем данные
    // определить длину буфера для зашифрованных данных

    DWORD dwOutLen = dwDataLen;
    CryptEncrypt(hSessionKey, 0, TRUE, 0, 0, &dwOutLen, dwBufferLen));

    // Выделить буфер подходящего размера
    pbOutData = new BYTE[dwOutLen];
    int nOutPos = 0, nInPos = 0, i = 1; // i - номер текущего блока
    bool bMore = true;
    DWORD dwCount = 0; // количество байтов очередной порции данных

    // счетчик (на сколько блоков разбиваются исходные данные)
    int nBlocks = (dwDataLen%dwBlockLen == 0) ?
    dwDataLen/dwBlockLen:(dwDataLen/dwBlockLen+1);
    BOOL bEnd = (nBlocks == 1); // один блок? шифруем за один раз?

    // Сколько байт считывать первый раз?
    if (bEnd)

    dwCount = dwDataLen;
    else
    dwCount = dwBlockLen;
    do
    {

    // скопировать в буфер очередной блок данных
    memcpy(pbBuffer, pbData+nInPos, dwCount);
    nInPos += dwCount;
    CryptEncrypt(hSessionKey, 0, bEnd, 0, pbBuffer, &dwCount, dwBufferLen);
    memcpy(pbOutData+nOutPos, pbBuffer, dwCount);
    nOutPos += dwCount;
    if (++i > nBlocks)

    bMore = false; // Это был последний блок входных данных
    else // сколько байт считывать в следующий раз?
    dwCount = ((dwDataLen - nInPos) > dwBlockLen) ?
    dwBlockLen : (dwDataLen - nInPos);
    if (i == nBlocks)
    bEnd = TRUE;
    }
    while(bMore);

    3.5. Хеши MD5 и SHA

    Хеш создается вызовом функции CryptCreateHash, принимающей на входе контекст криптопровайдера, идентификатор алгоритма (CALG_MD5 или CALG_SHA) и хендл ключа (для хешей с ключем типа MAC и HMAC). После этого хеш можно вычислять как явно, используя функцию CryptHashData, так и неявно, передавая хендл хеша в функцию CryptEncrypt. Использование CryptEncrypt обсуждалось в разделе про DES, поэтому остановимся на функции CryptHashData. Ее вызов может выглядеть следующим образом:

    CryptHashData(hHash,(BYTE *)&data,dwLen,0);

    Вычисление хеша (также как и шифрование) можно производить по частям (например, читая данные в буфер и хешируя содержимое буфера в цикле). После окончания хеширования можно либо подписать хеш (см. ниже), либо получить его значение вызовом:

    CryptGetHashParam(hHash,HP_HASHVAL,(BYTE *)&buf,&dwLen,0);

    Размер хеша MD5 равен 128 бит или 16 байтов. Для SHA это 160 бит или 20 байтов. После получения значения хеш использовать уже нельзя. Его нужно разрушить вызовом CryptDestroyHash. Проверка хеша производится точно также, как и его создание – нужно вычислить хеш и сверить полученное значение с сохраненным:

    HCRYPTHASH hHash;

    ::CryptCreateHash(hProv,CALG_MD5,0,0,&hHash);

    ::CryptHashData(hHash,(BYTE *)&data,dwLen,0);

    BYTE newHash[16];
    dwLen=16;

    ::CryptGetHashParam(hHash,HP_HASHVAL,newHash,&dwLen,0);

    if(!memcmp(newHash,oldHash,16))
    {

    //хеш верен
    }
    else
    {

    //хеш не верен
    }
    ::CryptDestroyHash(hHash);




  • Содержание раздела