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

 

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 среди них, естественно, нет, поэтому я использовал именно этот алгоритм для своих примеров.

Основные классы криптографических примитивов в .NET Framework
Основные классы криптографических примитивов в .NET Framework
 

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). На данном этапе у нас уже достаточно сведений, чтобы перейти непосредственно к кодингу.

Режим работы CFB (гаммирование с обратной связью)
Режим работы 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
Схема алгоритма, описанного в ГОСТе 28147-89

 

Режим выработки имитовставки

Режим выработки имитовставки по ГОСТу 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 Browser
Информация о сборке в GAC Browser
 

Добавление сборки в 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. Таким образом, в частности, можно заменить все стандартные имена алгоритмов.

Внутреннее устройство класса CryptoConfig
Внутреннее устройство класса CryptoConfig

Привязка 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. Благодаря такому подходу появляется возможность сцеплять разные объекты и осуществлять разные криптографические операции без создания промежуточных буферов для хранения данных.

CLR по умолчанию создает экземпляр нашего класса
CLR по умолчанию создает экземпляр нашего класса

Так, например, сначала данные из потока с открытым текстом поступают в данные с CryptoStream симметричного алгоритма, а выход этого потока — на CryptoStream алгоритма хеширования. При таком подходе одновременно осуществляется шифрование и хеширование открытого текста. Пример использования поточно-ориентированного подхода смотри в исходниках на диске.

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

 

Mono Project

Все описанное выше справедливо и для Mono, однако в синтаксисе команд, используемых для добавления сборки в GAC, есть небольшие различия. Так, например, для добавления сборки в глобальный кеш нужно использовать следующую команду:

$ gacutil -i <Путь и имя сборки>

А для просмотра параметров сборки — вот такую:

$ gacutil -l 

Файл machine.config находится (ОС Ubuntu) в /etc/mono/<Версия Mono>/.

 

Заключение

Вот и все, теперь ты сам можешь накодить криптографический алгоритм в стиле .NET Framework, а также создать свою сборку-криптопровайдер и корректно установить ее. А если остались какие-то вопросы или есть что обсудить — пиши мне на почту.

 

  • Подпишись на наc в Telegram!

    Только важные новости и лучшие статьи

    Подписаться

  • Подписаться
    Уведомить о
    7 комментариев
    Старые
    Новые Популярные
    Межтекстовые Отзывы
    Посмотреть все комментарии