Программирование на Visual С++ - No.81 (Шифрование с CryptoAPI и .NET)
Информационный Канал Subscribe.Ru |
|
РАССЫЛКА САЙТА
RSDN.RU |
Здравствуйте!
CТАТЬЯ Хеширование, шифрование и цифровая подпись с использованием CryptoAPI и .NET Автор: Алексей Остапенко
|
ПРИМЕЧАНИЕ Заметим, что Enhanced-провайдер присутствует только на тех машинах, где установлена поддержка 128-битного шифрования (она автоматически устанавливается вместе с Internet Explorer 6.0). |
Каждый криптопровайдер относится к определенному типу. Это позволяет, перебрав все установленные на машине провайдеры, выбрать те, которые поддерживают нужные алгоритмы. Два упомянутых провайдера имеют тип PROV_RSA_FULL.
Криптопровайдер поддерживает защищенные области, называемые контейнерами ключей. Контейнеры позволяют приложениям сохранять и использовать в дальнейшем сгенерированные один раз ключи, обеспечивая защиту самого ключа от злоумышленника.
ПРИМЕЧАНИЕ Стандартные криптопровайдеры хранят ключи на диске, в зашифрованном виде. Однако существует потенциальная возможность, что злоумышленник, укравший компьютер или жесткий диск, сможет расшифровать сохраненные ключи.
Контейнеры бывают двух типов – пользовательские (этот тип используется по умолчанию) и машинные (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, единственным значащим параметром которой является полученный ранее хэндл криптографического контекста.
Генерация ключей и обмен ключами
Для генерации ключей в 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 (бывает еще OPAQUEKEYBLOB и SYMMETRICWRAPKEYBLOB, но мы их рассматривать не будем), а экспортируется он, как правило, на открытом ключе получателя.
Описание флагов можно найти в MSDN. Я выделю среди них флаг CRYPT_OAEP, который заставляет криптопровайдера использовать формат PKCS #1 версии 2 при сохранении сессионного ключа с шифрованием RSA. Ключ, сохраненный в этом формате, затем может быть расшифрован другими системами шифрования (например, я так передавал ключ в библиотеку Crypto++). Если же этот флаг не указан, то ключ сохраняется в каком-то ведомом только 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))
{ //. } }
Рабочие примеры генерации и импорта/экспорта ключей приведены в демонстрационных проектах rsakg и encfile.
Симметричные шифры DES и 3DES
Это одни из немногих симметричных шифров, предоставляемых стандартными криптопровайдерами CryptoAPI. Поскольку DES и 3DES – это практически один и тот же алгоритм (3DES – это DES, применяемый 3 раза подряд), то мы ограничимся примером использования алгоритма 3DES.
ПРИМЕЧАНИЕ Однако заметим, что для использования алгоритма 3DES требуется Enhanced провайдер, а для DES вполне достаточно Base. Впрочем, DES уже не является стойким по современным меркам алгоритмом, поэтому использовать его стоит лишь там, где надежность шифрования не очень критична.
Алгоритм 3DES использует разные ключи DES для каждой из своих итераций. Поэтому размер его ключа равен тройному размеру ключа DES, т.е. 192 (64*3) бита. Реально размер ключа 3DES – 168 (56*3) бит, т.к. в DES один байт ключа является контрольным для основных семи. Шифрование и дешифрование выполняются с помощью функций:
BOOL WINAPI CryptEncrypt(HCRYPTKEY hKey,HCRYPTHASH hHash,BOOL Final,DWORD dwFlags,
BYTE* pbData,DWORD* pdwDataLen,DWORD dwBufLen); BOOL WINAPI CryptDecrypt(HCRYPTKEY hKey,HCRYPTHASH hHash,BOOL Final,DWORD dwFlags,
BYTE* pbData,DWORD* pdwDataLen);
Параметр hHash позволяет параллельно с шифрованием/дешифрованием проводить хеширование данных для последующей электронной подписи или ее проверки. Флаг Final определяет, является ли шифруемый блок данных последним. Он необходим, поскольку данные можно шифровать по кускам, но для последнего блока всегда выполняется определенная деинициализация алгоритма (освобождаются внутренние структуры), и многие алгоритмы производят добавление (и проверку корректности при дешифровании) заполнителя (padding) после основных данных. Параметры pbData и pdwDataLen задают адрес буфера и размер шифруемых данных. Для не последнего блока данных (Final=FALSE) размер данных должен быть всегда кратен размеру шифруемого алгоритмом блока (для 3DES и DES этот размер равен 64 битам). Для последнего блока допускается нарушение этого условия.
ПРИМЕЧАНИЕ Заметим, что зашифрованные данные помещаются в тот же самый буфер поверх исходных.
Последний параметр функции CryptEncrypt – dwBufLen может показаться странным. Зачем нам размер буфера, если мы и так знаем размер входных данных? Однако на самом деле он необходим. Как я уже упомянул, многие алгоритмы добавляют заполнитель в последний блок после основных данных. В этом случае размер зашифрованных данных может оказаться больше, чем размер исходных данных. И результат может попросту не вместиться в буфер! Поэтому стоит заранее указать размер буфера, превышающий максимальный размер помещаемых в него открытых данных. Для DES и 3DES максимально возможный довесок составляет 64 бита, т.е. 8 байт (это я установил опытным путем).
В качестве примера шифрования приведу выдержку из демонстрационного проекта encfile:
BYTE buf[BUFFER_SIZE+8]; //8 - запас на padding while(fSize) { if(!::ReadFile(hInFile,buf,BUFFER_SIZE,&dwLen,NULL)) //читаем блок данных break; dwSzLow=dwLen;
//шифруем и хешируем его < /font> if(!::CryptEncrypt(hKey,hHash,fSize<=BUFFER_SIZE,0,buf,&dwSzLow,sizeof(buf)))
break; if(!::WriteFile(hOutFile,buf,dwSzLow,&dwSzLow,NULL)) break; fSize-=dwLen; }
Асимметричный шифр RSA
Начиная с Windows 2000 расширенный (Enhanced) криптопровайдер поддерживает прямое шифрование данных по алгоритму RSA. Максимальный размер данных, которые можно зашифровать за один вызов CryptEncrypt, равен размеру ключа минус 11 байт. Дело в том, что при шифровании добавляется обязательный заполнитель (padding), который впоследствии проверяется при дешифрации. Соответственно, использование шифра RSA может быть целесообразно только при небольших объемах шифруемых данных (например, при обмене ключами) из-за существенного увеличения объема шифрованного текста и относительно медленной работе алгоритма RSA по сравнению с блочными шифрами.
Хеши 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);
Алгоритмы цифровой подписи RSA и DSS
Позволяют установить корректность данных (точнее, их хеша) и принадлежность подписи владельцу закрытого ключа. Функции CryptoAPI позволяют подписывать только хеши, но не сами данные. Хеш предварительно должен быть вычислен (см. предыдущую главу), а затем подписан с помощью функции CryptSignHash на закрытом ключе для подписи (AT_KEYEXCHANGE) находящейся в контейнере криптопровайдера! Проверка подписи осуществляется на открытом ключе, который нужно предварительно импортировать:
//Вычисление подписи ::CryptSignHash(hHash,AT_SIGNATURE,NULL,0,buf,&dwLen); // Проверка подписи if(::CryptVerifySignature(hHash,buf,1024/8,hPubKey,NULL,0)) { // Верная подпись } else { // Неверная подпись или хеш } Размер подписи равняется размеру ключа RSA или DSS, соответственно.
Практический пример применения подписи можно увидеть в демонстрационном проекте encfile. Вычисление/проверка подписи RSA и DSS выполняется одинаково, однако для работы с подписью DSS нужно использовать Microsoft DSS Cryptographic Provider (MS_DEF_DSS_PROV,тип PROV_DSS). Кроме того, подпись DSS работает только с алгоритмом хеширования SHA.
Криптографические классы в .NET
Большая часть криптографических классов (не абстрактных) .NET базируется на криптопровайдерах CryptoAPI. Однако имеются и исключения (например, SHA256Managed). В принципе, иерархия классов .NET позволяет абстрагироваться от конкретной реализации алгоритма, что может обеспечить простой переход на не привязанные к CryptoAPI классы в будущем. К сожалению, документация .NET по криптографическим классам, мягко говоря, оставляет желать лучшего. Поэтому часть из написанного далее следует воспринимать как собственные мысли и догадки автора (иногда полученные в ходе активного наступания на грабли).
Симметричные шифры DES, 3DES, Rijndael
Симметричные блочные шифры представлены в .NET классами DESCryptoServiceProvider, TripleDESCryptoServiceProvider, RijndaelManaged. Все эти классы являются потомками абстрактного класса SymmetricAlgorithm, описывающего все семейство блочных алгоритмов с симметричными ключами. Класс описывает свойства, позволяющие манипулировать основными параметрами алгоритма: размером блока, режимом работы, инициализационным вектором, ключом и другими. И методы CreateEncryptor и CreateDecryptor, возвращающие контексты (интерфейс ICryptoTransform) для криптографических трансформаций данных. Также имеются методы GenerateKey и GenerateIV для генерации ключей и инициализационных векторов. Конкретные реализации наследуются от этого класса (возможно, через другие абстрактные классы – например, DESCryptoServiceProvider наследуется от класса DES, унаследованного от SymmetricAlgorithm). Использование симметричных шифров демонстрируется в примере symmetric. Собственно шифрование выполняется следующим кодом:
private SymmetricAlgorithm alg; alg=(SymmetricAlgorithm)RijndaelManaged.Create(); //пример создания класса RijndaelManaged
//класс, позволяющий генерировать ключи на базе паролей PasswordDeriveBytes pdb=new PasswordDeriveBytes(Password.Text,null); pdb.HashName="SHA512"; //будем использовать SHA512 int keylen=(int)KeySize.SelectedItem; //получаем размер ключа из ComboBox’а alg.KeySize=keylen; //устанавливаем размер ключа alg.Key=pdb.GetBytes(keylen>>3); //получаем ключ из пароля alg.Mode=CipherMode.CBC; //используем режим CBC alg.IV=new Byte[alg.BlockSize>>3]; //и пустой инициализационный вектор ICryptoTransform tr=alg.CreateEncryptor(); //создаем encryptor FileStream instream=new FileStream(inFile.Text,FileMode.Open,
FileAccess.Read,FileShare.Read); FileStream outstream=new FileStream(outFile.Text,FileMode.Create,
FileAccess.Write,FileShare.None); int buflen=((2<<16)/alg.BlockSize)*alg.BlockSize; byte []inbuf=new byte[buflen]; byte []outbuf=new byte[buflen]; int len; while((len=instream.Read(inbuf,0,buflen))==buflen) { int enclen=tr.TransformBlock(inbuf,0,buflen,outbuf,0); //собственно шифруем outstream.Write(outbuf,0,enclen); } instream.Close(); outbuf=tr.TransformFinalBlock(inbuf,0,len); //шифруем финальный блок outstream.Write(outbuf,0,outbuf.Length); outstream.Close(); alg.Clear(); //осуществляем зачистку
Как можно видеть, ничего сложного в процессе шифрования/дешифрования нет. Использование базового класса SymmetricAlgorithm позволяет свести всю привязку к конкретному алгоритму к одной строчке – созданию экземпляра класса нужного алгоритма.
ПРЕДУПРЕЖДЕНИЕ Стоит, однако, обратить внимание на необходимость явного задания инициализационного вектора (IV), поскольку, в отличие от CryptoAPI, он не инициализируется нулем по умолчанию, а выбирается случайно.
На вторые возможные грабли я наступил в коде дешифрования. Размер расшифрованных данных для не финального блока может быть меньше размера шифротекста. Ни с чем подобным в CryptoAPI я не сталкивался.
Наряду с самостоятельным чтением файла и шифрованием его по блокам, возможен еще вариант с использованием стрима CryptoStream. При этом вы создаете CryptoStream, передавая ему на входе стрим для записи и интерфейс трансформации, а затем просто пишете в этот стрим свои данные. Этот вариант описан в документации по .NET Framework, поэтому я не буду подробно на нем останавливаться.
Асимметричные шифры и цифровая подпись: RSA и DSA
Аналогично симметричным шифрам, все классы асимметричных шифров в .NET унаследованы от абстрактного базового класса AsymmetricAlgorithm. Конкретная реализация представлена классами RSACryptoServiceProvider и DSACryptoServiceProvider. Увы, здесь базовый класс уже не обеспечивает стандартных механизмов шифрования/дешифрования и ЭЦП, т.ч. приходится иметь дело с конкретной реализацией. И тут дело опять не обошлось без сюрпризов. Первым сюрпризом является то, что нет стандартного способа сохранить приватный ключ RSA в файл. Мне пришлось самому организовывать шифрование ключа по паролю. Вторым, гораздо более неприятным, сюрпризом оказывается факт, что
ПРЕДУПРЕЖДЕНИЕ Заявленные в классах RSACryptoServiceProvider и DSACryptoServiceProvider методы SignHash не работают. Вообще! При попытке передать в них хеш, сформированный с помощью MD5CryptoServiceProvider или SHA1CryptoServiceProvider, всегда вываливается исключение.
Поэтому приходится использовать классы RSAPKCS1SignatureFormatter и DSASignatureFormatter.
Третий (но не последний) веселый сюрприз – в описании метода Encrypt класса RSACryptoServiceProvider можно найти упоминания о максимальном размере куска данных, который можно зашифровать. Там утверждается, что для Win2k с установленным 128-битным шифрованием этот размер равен <размер ключа>/8-11 байтов.
ПРЕДУПРЕЖДЕНИЕ Не верьте этому! При попытке подсунуть блок данных длиной в 117 байт при длине ключа 1024 бита вы получите исключение. У меня получилось, что максимальный размер блока данных при таком размере ключа равен 87 байтам, т.е. на 30 байт меньше заявленного.
Последние два сюрприза чуть не повергли меня в состояние глубокой депрессии, т.к. я уже почти решил, что класс RSACryptoServiceProvider неработоспособен в принципе. Вот эти два перла из коллекции ошибок класса RSACryptoServiceProvider.
ПРЕДУПРЕЖДЕНИЕ Метод ImportParameters, принимающий на входе структуру RSAParameters, содержащую публичный или приватный ключ, модифицирует эту структуру, хотя по нормальной человеческой логике и по документации он не имеет права этого делать.
Метод Decrypt, обратный методу Encrypt, не может расшифровать зашифрованный блок данных, если его первоначальный размер был больше 86 байтов (для 1024-битных ключей). Т.е. вы можете зашифровать блок из 87 байтов, но потом вы не сможете его расшифровать.
В общем, по моему нескромному мнению, классу RSACryptoServiceProvider нужно присудить заслуженную премию, как самому кривому из всех классов .NET. Тем не менее, хорошо то, что хорошо кончается. Я все-таки сумел найти и обойти все эти ошибки, и на свет все-таки родился пример (см. Asymmetric.zip), демонстрирующий прямое шифрование RSA, цифровую подпись RSA и DSA и хеширование в .NET.
Чтобы закончить рассмотрение асимметричных шифров в .NET, рассмотрим куски кода, отвечающие за генерацию ключей, шифрование, дешифрацию и цифровую подпись:
//генерация ключей: новый ключ генерируется автоматически при создании экземпляра класса RSACryptoServiceProvider rsa=new RSACryptoServiceProvider(1024); //1024-битный ключ //Экспорт ключа RSAParameters rp=rsa.ExportParameters(true); //экспорт закрытого ключа.
//Для открытого нужно передавать false
//Импорт ключа rsa.ImportParameters(rp); //будьте внимательны. Этот вызов портит значение rp //шифрование блока byte []enc=rsa.Encrypt(buf,true); //шифрование RSA/OAEP //дешифрование блока rsa.Decrypt(enc,true); //дешифрация RSA/OAEP //создание цифровой подписи RSA AsymmetricSignatureFormatter sf; sf=(AsymmetricSignatureFormatter)new RSAPKCS1SignatureFormatter(rsa); //создаем форматер sf.SetHashAlgorithm("MD5"); //выбираем алгоритм хеширования sig=sf.CreateSignature(Hash); //создаем подпись (хеш должен быть уже посчитан ранее) //проверка цифровой подписи RSA AsymmetricSignatureDeformatter df;
//создаем деформатер df=(AsymmetricSignatureDeformatter)new RSAPKCS1SignatureDeformatter(rsa);
df.SetHashAlgorithm("MD5"); if(df.VerifySignature(Hash,sig)) //проверяем подпись { //подпись верна } else { //подпись неверна }
В качестве еще одного замечания стоит упомянуть, что хранить закрытые ключи RSA вне контейнеров CryptoAPI, пусть даже и временно, плохо. Их может перехватить злоумышленник. Также не стоит брать пример с моего кода сериализации закрытых ключей в файл – он был написан только для демонстрации, и сохраняет в файл много ненужной информации о типах данных, которая, теоретически, может упростить задачу дешифрование файла с ключом для злоумышленника.
Обмен симметричными ключами
За обмен сессионными ключами в .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");
Как можно видеть, здесь нет ничего сложного. Самым проблемным, пожалуй, является передача публичного ключа от получателя отправителю и зашифрованного сессионного ключа в обратную сторону.
Хеши
Все классы хешей MD5CryptoServiceProvider, SHA1CryptoServiceProvider, SHA1Managed, и т. д. в .NET унаследованы от базового класса HashAlgorithm, предоставляющего два различных пути для вычисления хеша – метод ComputeHash, вычисляющий хеш блока данных или потока за один раз, и методы TransformBlock и TransformFinalBlock, позволяющие вычислить хеш, разбивая данные на части. Вычисление хеша при помощи ComputeHash выглядит следующим образом:
HashAlgorithm ha=(HashAlgorithm)new MD5CryptoServiceProvider(); byte []hash=md5.ComputeHash(data,0,data.Length); //считаем хеш
Вариант с TransformBlock хорошо подходит для хеширования с параллельным шифрованием. Примет такого хеширования можно найти в примере Asymmetric. Вычисление хеша этим способом производится в цикле, причем на последней итерации нужно вызвать TransformFinalBlock:
ha=(HashAlgorithm)new SHA1CryptoServiceProvider();
//читаем файл поблочно
while((len=ins.Read(inbuf,0,MaxRSABlockSize))==MaxRSABlockSize)
{ ha.TransformBlock(inbuf,0,MaxRSABlockSize,outbuf,0); //считаем хеш от блока данных } byte []hash=ha.TransformFinalBlock(inbuf,0,len); //досчитываем хеш от последнего куска
Можно считать хеш целиком внутри цикла, а TransformFinalBlock вызвать для массива нулевой длины.
Цифровая подпись
Ввиду неработоспособности метода SignHash классов RSACryptoServiceProvider и DSACryptoServiceProvider, пары классов RSAPKCS1SignatureFormatter/Deformatter и DSASignatureFormatter/Deformatter, похоже, являются единственным вариантом создания цифровой подписи. Обе пары классов унаследованы от классов AsymmetricSignatureFormatter/Deformatter, предоставляющих стандартный интерфейс создания и верификации цифровой подписи – методы CreateSignature и VerifySignature. Перед вычислением или проверкой цифровой подписи нужно обязательно установить алгоритм хеширования, который будет использоваться в процессе работы, с помощью вызова SetHashAlgorithm. RSAPKCS1SignatureFormatter понимает два алгоритма хеширования – MD5 и SHA1, а DSASignatureFormatter - только SHA1. Пример создания и проверки подписи уже был приведен в разделе, посвященном асимметричным шифрам в .NET, и повторять его здесь я не буду.
Заключение
И CryptoAPI, и .NET предоставляют богатый набор средств шифрования, позволяющих организовать собственную систему защиты данных без использования сторонних средств. В .NET, правда, эти средства пока плохо отлажены и документированы, но использовать их все равно можно. Основными препятствиями на пути использования средств CryptoAPI и .NET могут стать отсутствие на платформах ниже Windows 2000 поддержки “сильной” криптографии и требования, налагаемые Российским законодательством на средства шифрования, используемые в коммерческой деятельности. По Российскому законодательству эти средства обязательно должны быть сертифицированы ФАПСИ. Но ни один из алгоритмов, реализованных в CryptoAPI и .NET, не является сертифицированным. Правда, это не мешает разработчикам shareware-программ использовать алгоритмы типа RSA для защиты своих продуктов.
Ссылки
- Bruce Schneier, Applied Cryptography 2 edition.
- MSDN, раздел Cryptography
До следующей недели!
Ведущий рассылки: Алекс Jenter jenter@rsdn.ru
Публикуемые в рассылке материалы принадлежат сайту RSDN.
http://subscribe.ru/
E-mail: ask@subscribe.ru |
Отписаться
Убрать рекламу |
В избранное | ||