Содержание статьи
Сегодня сложно найти информационную систему, которая бы не использовала криптографию. Коммерческие системы в большинстве своем используют криптографические примитивы, которые внесены в стандарты в США, но порой возникают ситуации, когда такой вариант просто неприемлем. Здоровый партриотизм, отсутствие доверия и некоторые законы наталкивают нас на мысль о применении своих алгоритмов, определенных в наших государственных стандартах (ГОСТах).
INFO
- Strong name сборки — набор параметров, включающий в себя имя сборки, версию, информацию о культуре, а также открытый ключ и значение электронной подписи.
- Криптографический примитив — обобщенное название какого-либо криптографического алгоритма или протокола.
WWW
- gacbrowser.blogspot.com/ — сайт разработчика GAC Browser.
- bit.ly/uyxZs5 — статья о том, как реализовать алгоритм по ГОСТу 28147-89 на C#.
В этой статье я расскажу о том, как устроена криптографическая подсистема .NET Framework, представленная в пространстве имен System.Security.Cryptographi, а также о том, как реализовать свой алгоритм в стиле .NET Framework и заставить CLR использовать его в качестве криптографического алгоритма по умолчанию.
Это поможет тебе как в разработке собственных криптопровайдеров для .NET, так и во внедрении уже существующих библиотек с криптографическими алгоритмами.
System.Security.Cryptographi
Все, что касается криптографии, в .NET Framework находится в пространстве имен System.Security.Cryptographi, которое условно можно разделить на следующие составляющие:
- криптографические примитивы — набор классов, применяемых для реализации алгоритмов шифрования, хеш-функций и т. д.;
- вспомогательные классы — отвечают за генерацию случайных чисел, шифрование на основе потоковой модели и т. д.;
- сертификаты X.509 и цифровые подписи XML-документов (XMLSignature).
Что касается криптографических примитивов, то для всех их типов имеются абстрактные классы и интерфейсы, от которых наследуются конкретные программные реализации алгоритмов (см. рисунок 1). Так, любая реализация симметричного алгоритма шифрования должна наследовать абстрактный класс SymmetricAlgorithm, алгоритм с открытым ключом — AssymetricAlgorithm, функции хеширования — HashAlgorithm или KeyedHashAlgorithm в зависимости от того, ключевая это или бесключевая хеш-функция. В .NET в основном реализованы алгоритмы, описанные в стандартах, которые действуют или действовали ранее за рубежом. Нашего ГОСТа 28147-89 среди них, естественно, нет, поэтому я использовал именно этот алгоритм для своих примеров.
Хакер #156. Взлом XML Encryption
SymmetricAlgorithm
Как я уже упомянул, для реализации симметричных алгоритмов в .NET Framework используется абстрактный класс SymmetricAlgorithm. Согласно MSDN, в этом абстрактном классе обязательно необходимо переопределить следующие четыре метода:
public virtual ICryptoTransform CreateDecryptor();
public virtual ICryptoTransform CreateEncryptor();
public abstract void GenerateIV();
public abstract void GenerateKey();
С последними двумя методами все понятно, они необходимы для выработки ключа и вектора инициализации, и реализовать их не составляет труда. Необходимо разобраться, что представляет собой интерфейс ICryptoTransform в первых двух методах. MSDN выручает нас и на этот раз. Согласно документации, ICryptoTransform позволяет реализовать четыре свойства и три метода. Вот самые интересные из них:
int TransformBlock(byte[] inputBuffer,
int inputOffset, int inputCount,
byte[] outputBuffer, int outputOffset);
byte[] TransformFinalBlock(byte[] inputBuffer,
int inputOffset, int inputCount);
Первый отвечает за преобразование промежуточного блока данных, второй — за обработку последнего блока. В MSDN также говорится, что «классы блочных шифров, являющиеся производными класса SymmetricAlgorithm, используют режим сцепления, называемый сцеплением блоков шифротекста (CBC)».
Это порождает определенную проблему, так как отечественный стандарт ГОСТ 28147-89 определяет четыре режима работы алгоритма, среди которых CBC нет. Однако вскрытие показало, что .NET Framework’у все равно, как цепляются блоки, поэтому будем использовать режим гаммирования с обратной связью (CFB). На данном этапе у нас уже достаточно сведений, чтобы перейти непосредственно к кодингу.
Кодим ГОСТ 28147-89
Будем исходить из того, что реализация алгоритма ГОСТ28147-89 в режиме ECB (простой замены) уже имеется и сосредоточимся только на том, что касается .NET Framework’а и режима CFB. Создадим скелет класса GostCfb, который будем постепенно наполнять кодом.
namespace Gost
{
public class GostCfb : SymmetricAlgorithm
{
public GostCfb(){}
public override ICryptoTransform CreateDecryptor
(
byte[] rgbKey,
byte[] rgbIV
){}
public override ICryptoTransform CreateDecryptor()
{}
public override ICryptoTransform CreateEncryptor
(
byte[] rgbKey,
byte[] rgbIV
){}
public override ICryptoTransform CreateEncryptor()
{}
public override void GenerateIV(){}
public override void GenerateKey(){ }
}
Местами в коде я буду использовать два статических метода.
private static byte[] GetRandomBytes(int bytesCount)
private static void Gamm(byte[] input,
byte[] gamma, byte[] output)
Первый с помощью встроенного в систему генератора случайных чисел выдает массив со случайными данными. Второй просто осуществляет XOR двух массивов. На диске, прилагаемом к журналу, ты сможешь найти исходники этих статических методов. Функция GetRandomBytes легко позволяет реализовать методы GenerateIV и GenerateKey: она просто генерирует случайные данные и присваивает соответствующие массивы свойствам IVValue и KeyValue, которые служат для хранения вектора инициализации и ключа шифрования соответственно.
В конструкторе нашего класса определяем возможные размеры блока и ключа. Эти размеры, указываемые в битах, для описанного в ГОСТе алгоритма равны 64 и 256 бит.
public GostCfb()
{
LegalBlockSizesValue = new[]
{ new KeySizes(64, 64, 0) };
LegalKeySizesValue = new[]
{ new KeySizes(256, 256, 0) };
BlockSizeValue = 64;
KeySizeValue = 256;
}
Прежде чем перейти к реализации методов CreateEncryptor и CreateDecryptor, определим внутри нашего класса GostCfb еще пару классов, наследующих ICryptoTransform.
private sealed class GostCfbTransformEncr:ICryptoTransform
{}
private sealed class GostCfbTransformDecr:ICryptoTransform
{}
После этого реализовать методы типа CreateEncryptor не составит труда — нужно просто создать и вернуть объект соответствующего класса. На этом закончу рассказ об основном классе GostCfb. Во всем, что осталось «за кадром», легко разобраться самостоятельно, заглянув в исходники.
OID
В открытом мире информационных технологий и телекоммуникаций часто возникает потребность сослаться на какой-либо «объект», причем ссылка должна быть уникальной и универсальной. Обычно в данном случае под «объектом» понимают тип данных (например, формат файла), а не их единичный экземпляр (например, конкретный файл). Многие стандарты определяют некоторые объекты, которые нуждаются в точной идентификации. Такая точная идентификация достигается за счет присвоения каждому объекту своего идентификатора OID (object identificator), причем присвоение может осуществлять любая из заинтересованных сторон. Все OID собраны в специальную древовидную структуру, каждый элемент которой ассоциирован с именем (слово, начинающееся с маленькой буквы) и числом, используемым при передаче данных. Семантически OID представляет собой упорядоченный список компонентов объектного идентификатора (arcs), например:
"{joint-iso-itu-t(2) ds(5) attributeType(4)
distinguishedName(49)}"
или
"2.5.4.49"
Регистрационное дерево идентификаторов управляется децентрализовано (каждый узел не налагает никаких ограничений на дочерние узлы). Проект ASN.1 поддерживает репозиторий, который собирает информацию об объектах и их идентификаторах, но из-за децентрализации собрать все идентификаторы в одном месте не представляется возможным. Репозиторий располагается по адресу www.oid-info.com.
ASN.1 — абстрактно-синтаксическая нотация — язык для описания абстрактного синтаксиса данных, который состоит из набора формальных правил для определения структуры объектов, не зависящих от конкретной машины. Технологию ASN.1 широко используют как в ITU-T, так и в других организациях, занимающихся стандартизацией. Эту нотацию также поддерживают многие производители программного обеспечения.
Класс GostCfbTransformEncr
Я уже говорил, что на основе реализации стандарта, описанного в ГОСТе, в режиме ECB собираюсь реализовать режим CFB, в котором шифрование осуществляется по схеме, представленной на рисунке 2 (по сути, надо просто ее закодить). Заведем несколько приватных членов для хранения ключа шифрования и вектора инициализации (состояния) и один вспомогательный массив, по длине равный блоку данных.
// Ключ шифрования
private byte[] m_Key;
// Сначала вектор инициализации, ну а потом
// промежуточные значения
private byte[] m_State;
// Вспомогательный массив
private byte[] tmpState;
Сам метод шифрования промежуточного блока реализуется следующим образом:
public int TransformBlock(...)
{
...
byte[] plainBlock = new byte[8];
int result = 0;
while(inputCount > 0)
{
// Копируем блок открытого текста
Array.Copy(inputBuffer, inputOffset, plainBlock, 0,8);
Gost28147.Gost28147Ecb(m_State, tmpState, m_Key);
Gamm(plainBlock, tmpState, m_State);
Array.Copy(m_State, 0, outputBuffer, outputOffset, 8)
inputCount -= 8;
inputOffset += 8;
outputOffset += 8;
result += 8;
}
...
return result;
}
Из приведенного куска кода я выкинул несколько строк, которые не влияют на суть. Как видно, здесь выполняется шифрование текущего состояния, а затем над результатом и блоком открытого текста осуществляется XOR. Полученный шифрованный текст, являющийся новым состоянием схемы, записывается в выходном массиве и сохраняется в приватном члене класса.
Метод TransformFinalBlock аналогичен по функционалу методу TransformBlock, поэтому приводить здесь его код я не буду. Скажу лишь только, что для шифрования последнего неполного блока берется столько же байтов промежуточного состояния, сколько байтов содержится в последнем неполном блоке.
Класс GostCfbTransformDecr реализуется аналогично классу GostCfbTransformEncr, только в данном случае опираться надо на схему дешифрования CFB, понять принцип которой, я думаю, ты сможешь сам (ну а если не сможешь, ее легко восстановить по исходникам на диске).
KeyedHashAlgorithm
С абстрактным классом KeyedHashAlgorithm все гораздо проще. Он происходит от абстрактного класса HashAlgorithm, при наследовании от которого необходимо переопределить всего два метода:
protected abstract void HashCore(
byte[] array, int ibStart, int cbSize)
protected abstract byte[] HashFinal()
Будем использовать указанный класс в качестве базового при реализации режима выработки имитовставки, описанного в ГОСТе 28147-89. Я снова возьму за основу программную реализацию базового преобразования (на этот раз 16 раундов описанного в ГОСТе алгоритма, так как режим выработки имитовставки использует только 16 раундов). Кстати, класс, производный от HashAlgorithm, реализуется практически так же, как класс, который наследуется от KeyedHashAlgorithm: переопределить надо те же самые методы.
ГОСТ 28147-89
ГОСТ 28147-89 является стандартом СНГ и определяет блочный симметричный алгоритм шифрования, а также четыре режима его работы. Программная реализация алгоритма очень проста, так как он состоит из простых операций: XOR, сложения по модулю 2^32 и циклического сдвига на 11 бит влево. В самом начале блок открытого текста (8 байт) разбивается на две части по 4 байта каждая. К правой части прибавляется ключ (по mod 2^32), после чего результат преобразуется в соответствии с таблицей замены (SBox), а затем ксорится с левой часaтью. Далее части меняются местами. Такой порядок действий сохраняется на протяжении 31-го раунда, на последнем 32 раунде части не меняются местами, а компонуются в блок шифрованного текста размером 8 байт. Алгоритм, определенный в ГОСТе 28147-89, является очень быстрым и обеспечивает надежную защиту данных. В открытой криптографии еще не известен способ существенного снижения стойкости этого алгоритма. Общую схему его работы смотри на рисунке 7.
Режим выработки имитовставки
Режим выработки имитовставки по ГОСТу 28147-89 реализуется с помощью класса GostImito. Для этого присваиваем полю KeyValue значение ключа и устанавливаем значение поля HashValueSize равным 32, так как длина имитовставки составляет 32 бита.
В переопределении метода HashCore также нет ничего сложного — в итоге все сводится к использованию метода, работающего с целыми блоками данных (в исходниках метод называется InternalTransform). Каждый блок данных преобразуется в две переменные типа DWORD, к которым применяется 16-раундовая процедура шифрования, описанная в ГОСТе:
...
uint tempInH = Gost28147.Bytes2Dword(array,
(int)(ibStart + i * 8));
uint tempInL = Gost28147.Bytes2Dword(array,
(int)(ibStart + i * 8 + 4));
uint tempOutH = 0;
uint tempOutL = 0;
Gost28147.EncryptBlock16(ref tempInH, ref tempInL,
ref tempOutH, ref tempOutL,
Gost28147.P, KeyValue);
uImito ^= tempOutH;
...
В цикле сначала преобразуем блок данных (8 байт) в две переменные DWORD, затем применяем 16-раундовое шифрование по ГОСТу (EncryptBlock16), а после ксорим полученное значение с предыдущим. Преобразование блока данных в две переменные типа DWORD необходимо, во-первых, для увеличения скорости работы алгоритма (XOR двух значений типа DWORD выполняется гораздо быстрее, чем XOR двух четырехбайтовых массивов), а во-вторых, для удобства программирования.
Метод HashFinal реализуется аналогично, при этом неполный блок дополняется до полного нулями.
На следующем этапе встраивания своего алгоритма в .NET Framework необходимо добавить сборку в GAC.
Добавление сборки в GAC
Если ты не новичок в .NET, то наверняка слышал аббревиатуру GAC, которая расшифровывается как Global Assembly Cache (глобальное хранилище сборок). GAC служит для хранения сборок, разработанных для нескольких приложений. Сборки, помещаемые в глобальное хранилище, должны иметь strong name, что, в частности, обязывает нас подписать сборку.
Для начала необходимо сгенерировать пару ключей подписи, в чем тебе поможет утилита sn.exe, которая идет в комплекте с .NET Framework. Открываем консоль и пишем:
sn.exe -k keypair.snk
Эта утилита генерирует открытый и секретный ключи подписи и записывает их в файл keypair.snk. Теперь в свойствах нашего проекта на закладке Signing необходимо поставить галочку напротив Sign the assembly и указать путь к твоему файлу с парой ключей (см. рисунок 3). После делаем билд. Все, подписанная сборка готова.
Сборки добавляются в GAC с помощью специальной утилиты gacutil, которая также поставляется с .NET Framework. Если у тебя на компе несколько версий .NET, ты должен выбрать утилиту для своей версии фреймворка, иначе сборка в GAC не добавится. В консоли пишем:
gacutil /i <Путь и имя сборки>
Теперь нам надо узнать Public Key Token, версию и Culture сборки, что можно сделать при помощи либо GAC Explorer, встроенного в Windows, либо бесплатной утилиты GAC Browser (см. рисунок 4).
Изменение конфигурации
На втором этапе добавления нашего алгоритма в .NET производится правка его конфигурации. Основные параметры фреймворка собраны в файле machine.config, который имеет формат XML и отвечает за настройку фреймворка для всей системы в целом.
За криптографические настройки отвечает элемент cryptographySettings, который является дочерним элементом mscorlib. Процедура привязки имени класса криптографического алгоритма к имени алгоритма называется Name Mapping. Она выполняется следующим образом: сначала объявляем класс алгоритма при помощи элемента cryptoClass, а затем привязываем строковое имя алгоритма к объявленному классу при помощи элемента nameEntry. Таким образом, в частности, можно заменить все стандартные имена алгоритмов.
Привязка OID к ранее объявленному классу алгоритма осуществляется при помощи элемента oidMap и его дочернего элемента oidEntry.
<cryptographySettings>
<cryptoNameMapping>
<cryptoClasses>
<cryptoClass GOSTCFB="Gost.GostCfb, GostAlgs,
Version=1.0.0.0,Culture=ru,PublicKeyToken=9b088f4818daa492"/>
<cryptoClass GOSTIMITO="Gost.GostImito, GostAlgs,
Version=1.0.0.0,Culture=ru,PublicKeyToken=9b088f4818daa492"/>
</cryptoClasses>
<nameEntry name="GostImitoAlg" class="GOSTIMITO" />
<nameEntry name="GostCfbAlg" class="GOSTCFB" />
<nameEntry
name="System.Security.Cryptography.KeyedHashAlgorithm"
class="GOSTIMITO" />
<nameEntry
name="System.Security.Cryptography.SymmetricAlgorithm"
class="GOSTCFB" />
</cryptoNameMapping>
<oidMap>
<oidEntry OID="1.2.643.2.2.21" name="GostCfbAlg" />
<oidEntry OID="1.2.643.2.2.22" name="GostImitoAlg" />
</oidMap>
</cryptographySettings>
Здесь мы объявляем классы GOSTCFB и GOSTIMITO, а затем привязываем имя GostCfbAlg к имени класса GostCfb, а GostImitoAlg — к имени класса GostImito.
Строки
<nameEntry
name="System.Security.Cryptography.KeyedHashAlgorithm"
class="GOSTIMITO" />
<nameEntry
name="System.Security.Cryptography.SymmetricAlgorithm"
class="GOSTCFB" />
гарантируют, что теперь по умолчанию симметричным алгоритмом является GostCfb, а ключевой хеш-функцией — GostImito (см. рисунок 5). Теперь при вызове метода SymmetricAlgorithm.Create с параметром GostCfbAlg будет создаваться экземпляр нашего класса. То же самое верно и для вызова KeyedHashAlgorithm.Create, однако в этом случае вторым параметром нужно передавать ключ. Все перечисленные вызовы в итоге сводятся к вызову метода CryptoConfig.CreateFromName, в котором объекты создаются при помощи Activator.CreateInstance и все ошибки подавляются пустым catch. Так что при правке конфигурации необходимо быть предельно внимательным — при малейшей ошибке вместо ссылки на нужный экземпляр класса можно запросто получить null.
В секции oidMap мы осуществили привязку OID алгоритма к его имени, и теперь при помощи класса CryptoConfig легко можно получить OID по имени алгоритма. Кстати, в MSDN говорится, что значение атрибута name элемента oidEntry должно совпадать с именем класса (в нашем случае, например, GostCfb), однако это не так — следует указать то имя, которое мы задаем в элементе nameEntry, иначе OID не будут маппиться к именам.
Вообще, при возникновении каких-либо проблем, под отладчиком можно легко посмотреть данные, прочитанные из файла machine.config. Заданные пользователем OID хранятся в private-коллекции machineOidHT, а привязанные имена — в-private коллекции machineNameHT класса CryptoConfig (см. рисунок 6).
Использование
Для реализации шифрования и хеширования CLR использует поточно-ориентированный подход, ключевую роль в котором играет класс CryptoStream, являющийся производным от класса Stream. Благодаря такому подходу появляется возможность сцеплять разные объекты и осуществлять разные криптографические операции без создания промежуточных буферов для хранения данных.
Так, например, сначала данные из потока с открытым текстом поступают в данные с CryptoStream симметричного алгоритма, а выход этого потока — на CryptoStream алгоритма хеширования. При таком подходе одновременно осуществляется шифрование и хеширование открытого текста. Пример использования поточно-ориентированного подхода смотри в исходниках на диске.
При использовании CryptoStream для дешифрования следует учитывать то, что он обычно считывает из потока целое число блоков. В некоторых случаях, когда за шифрованным текстом следуют другие данные, а размер шифрованных данных не кратен размеру блока алгоритма, может потребоваться коррекция свойства Position потока-источника для CryptoStream.
Mono Project
Все описанное выше справедливо и для Mono, однако в синтаксисе команд, используемых для добавления сборки в GAC, есть небольшие различия. Так, например, для добавления сборки в глобальный кеш нужно использовать следующую команду:
$ gacutil -i <Путь и имя сборки>
А для просмотра параметров сборки — вот такую:
$ gacutil -l
Файл machine.config находится (ОС Ubuntu) в /etc/mono/<Версия Mono>/.
Заключение
Вот и все, теперь ты сам можешь накодить криптографический алгоритм в стиле .NET Framework, а также создать свою сборку-криптопровайдер и корректно установить ее. А если остались какие-то вопросы или есть что обсудить — пиши мне на почту.