Ruby и криптоалгоритмы ГОСТ

в 5:30, , рубрики: openssl, ruby, ruby on rails, гост р 34.10-2001, гост р 34.11-94, информационная безопасность, криптоалгоритмы гост, криптография, электронная цифровая подпись, электронное правительство, эцп

Логотип Ruby и суровый ГОСТовый навесной замокВ жизни далеко не каждого разработчика наступает момент, когда приходится взаимодействовать с государственными системами. И немногим из них приходится взаимодействовать именно с российскими государственными системами. И так уж сложились звёзды, что я оказался одним из этих «счастливчиков».

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

И вот, когда встала перед нами задача весьма плотного обмена сообщениями с ГОСТовой электронной подписью с одной из таких систем, то предложенный вариант решения в виде сетевого SOAP-сервиса, подписывающего запросы (и ответы) мне не понравился от слова «совсем» (оборачивать SOAP в SOAP — это какой-то кошмар в квадрате). Наступили длинные майские выходные, а когда они закончились — у меня было решение получше…

И это — Ruby с нативной поддержкой криптоалгоритмов ГОСТ. Без новых внешних зависимостей. Хотите попробовать? Поехали!

Установка

Настройка OpenSSL

Для того, чтобы всё, связанное с ГОСТовыми алгоритмами работало, необходим настроенный OpenSSL, версии 1.0.0 или новее. В Linux есть «из коробки», в OS X надо ставить из HomeBrew (потому что Apple — слоупоки):

brew install openssl
brew link --force openssl

Как настраивать OpenSSL для ГОСТ в интернете писали много-много раз, я же рекомендую воспользоваться оригинальным мануалом: README.gost

Читать сразу, не ходить по сторонним ссылкам!

В Ubuntu Linux конфигурационный файл находится по пути /etc/ssl/openssl.cnf, в OS X — по пути /usr/local/etc/openssl/openssl.cnf.

В него необходимо добавить следующую строку в самое начало файла:

openssl_conf = openssl_def

И следующие секции в самый конец файла:

[openssl_def]
engines = engine_section

[engine_section]
gost = gost_section

[gost_section]
default_algorithms = ALL
engine_id = gost
CRYPT_PARAMS = id-Gost28147-89-CryptoPro-A-ParamSet

В последней секции может потребоваться параметр dynamic_path но в свежих версиях Linux и Mac OS X он не нужен. При необходимости, его значение можно узнать командой locate libgost.so.

После данных действий, если команда openssl ciphers | tr ":" "n" | grep GOST вернёт следующие строки, всё настроено верно:

GOST2001-GOST89-GOST89
GOST94-GOST89-GOST89

Ruby

Для того, чтобы Ruby тоже начал понимать всё ГОСТовое, потребуется наложить парочку патчей из моих багрепортов #9022 и #9030. Данные патчи успешно накладываются на Ruby версий 2.0.0 и 2.1.x, с другими версиями не проверял.

Что это за патчи?
Ну посмотрите на них сами, что вы кода на C никогда не видели, что ли?

Первый вставляет вызов волшебной OpenSSL-евской функции OPENSSL_config куда-то туда, где Ruby инициализирует OpenSSL для себя. Это заставляет OpenSSL заинтересоваться конфигом, который мы с вами правили только что и применить его. Спасибо читательу xtron, который в своей статье делал то же, но для PHP (и боролся с той же проблемой, что и мы, кстати). С этим патчем Ruby уже сможет ходить на HTTPS-хосты с ГОСТовым шифрованием, например (но без авторизации по сертификатам).

Второй же патч, путём подлога условий и несанкционированного удаления проверок, заставляет Ruby наивно верить, что ГОСТовые ключи являются ключами на элдиптических кривых (Elliptic Curve, EC), что, впрочем, хоть, по всей видимости, и является правдой, но костыльности решения не оправдывает. С этим патчем Ruby начнёт «узнавать» ГОСТовые закрытые и открытые ключи, делать электронную подпись и шифрование. И вообще всё станет хорошо.

С использованием RVM установка производится командой:

rvm install ruby-2.1.2-gost --patch https://bugs.ruby-lang.org/attachments/download/4420/respect_system_openssl_settings.patch --patch https://bugs.ruby-lang.org/attachments/download/4415/gost_keys_support_draft.patch

В случае Rbenv (ruby-build), всё несколько сложнее, придётся выполнить две команды (этот способ особо не тестировал):

cp ~/.rbenv/plugins/ruby-build/share/ruby-build/{2.1.2,ruby-2.1.2-gost} # Копируем определение, чтобы у нашей Ruby было своё имя. Если вы хотите, чтобы имя было тем же — эта команда не нужна
curl -sSL https://gist.githubusercontent.com/Envek/82be109c58a0a565d382/raw/44e2330f233d7e5be707482ca94754a3a71cbe68/ruby_enable_gost.patch | rbenv install ruby-2.1.2-gost --patch

Готово!

В результате у вас будет установлен отдельный Ruby с именем ruby-2.1.2-gost. Это имя можно записать в файл .ruby-version, а эту инструкцию — в README, и тогда всегда будет понятно, что проекту нужен не совсем обычный Ruby…

Установка на серверы с помощью Puppet

Когда придёт время ставить Ruby на сервер, вам может помочь, например, модуль Rbenv для Puppet'а, но не простой, а тоже патченный. Вам понадобится патч от GitHub-пользователя gsamokovarov который находится здесь: github.com/alup/puppet-rbenv/pull/95. Чтобы не сильно мучаться — вот вам инструкция по установке модуля на сервер:

git clone git@github.com:Envek/puppet-rbenv.git # Мой форк с применённым патчем и обновлённым манифестом
gem install puppet
cd puppet-rbenv
puppet module build .

Теперь из каталога pkg вы можете достать свежеиспечённый архив с модулем, закачать его на сервер и установить командой puppet module install /path/to/alup-rbenv-1.2.1.tar.gz (ещё может потребоваться ключик --force) и перезапустить Puppet-мастер (puppet любит кэшировать ruby-код используемых модулей).

Конвертация ключевых пар в формат, понятный OpenSSL

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

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

К сожалению, пока что единственным рабочим вариантом экспортировать ключи в нужный формат является утилита P12fromCSP от Лисси-софт. К сожалению, только под Windows и платная. Придётся покупать, но перед этим демо-версией программы можно проверить, поможет ли она вам в принципе. Будьте предупреждены, что программа покупается банковским переводом (можно через онлайн-банкинг), а это невыносимо долго — дня 3-4.

Вам понадобится машина с Windows и Крипто Про. Силами Крипто Про устанавливаете сертификат из ключевого носителя в систему. Если ключи у вас в виде папки с файлами, создайте виртуальную дискету и скопируйте их туда, эту дискету Крипто Про распознает, как ключевой носитель. После установки сертификата убедитесь, что он есть в системе (ярлык «Сертификаты» есть в «Пуске» в папке с Крипто Про). И запустите утилиту, она должна показать список, а в нём ваш сертификат, выбираете его и сохраняете в файл (на этом-то этапе утилита и попросит кушать).

Если вы всё сделали верно, но сертификат не отобразился в утилите, то тут возможны две причины:

  1. У вас токен типа «смарт—карта». Закрытый ключ невозможно экспортировать физически. Увы.
  2. Ключ помечен как неэкспортируемый, утилита откажется его экспортировать. Есть ли возможность это обойти — не знаю.

Полученный файлик с раширением .p12 или .pfx тащите на машину с OpenSSL и вытаскиваете из него сертификат и закрытый ключ следующими командами:

Сертификат: openssl pkcs12 -engine gost -in gost.pfx -clcerts -nokeys -out gost.crt

Закрытый ключ: openssl pkcs12 -engine gost -in gost.pfx -nocerts -nodes -out gost.pem

Вот теперь можно и работу работать!

Что же с этим делать?

Если вы заинтересовались, то значит, вам УЖЕ что-то надо делать. Смотрите, вот лишь малая толика того, что теперь возможно и доступно нам:

Общее

Для того, чтобы всё, связанное с ГОСТовыми алгоритмами работало в Ruby, необходимо сначала «завести» OpenSSL-вский движок gost, вот так:

require 'openssl'
OpenSSL::Engine.load
@gost_engine = OpenSSL::Engine.by_id('gost')
@gost_engine.set_default(0xFFFF) # Решительно не знаю, что бы это значило, но без него не работает

После выполнения этого магического куска кода, все дальнейшие примеры начнут работать. Переменная @gost_engine нам ещё понадобится.

Цифровая подпись куска данных и её проверка

Простая подпись:

pkey = OpenSSL::PKey.read(File.read('gost.pem'))
data = 'Same message'
digester  = @gost_engine.digest('md_gost94')
signature = privkey.sign(digester, data)

Проверка простой подписи:

cert     = OpenSSL::X509::Certificate.new(File.read('gost.crt'))
digester = @gost_engine.digest('md_gost94')
data     = 'Same message'
cert.public_key.verify(dgst94, signature, data)                   # Should be true
cert.public_key.verify(dgst94, signature, data.sub('S', 'Not s')) # Should be false

Создание detached-подписи (привет реестру запрещённых сайтов):

cert   = OpenSSL::X509::Certificate.new(File.read('gost.crt'))
pkey   = OpenSSL::PKey.read(File.read('gost.pem'))
data   = 'Some message'
signed = OpenSSL::PKCS7::sign(crt, key, data, [], OpenSSL::PKCS7::DETACHED)

Проверка detached-подписи с проверкой доверености сертификатов:

cert_store = OpenSSL::X509::Store.new
cert_store.set_default_paths # Этой командой можно подгрузить системные корневые сертификаты
# Если же вам этого не хочется, или вам просто не удаётся без плясок с бубном добавить корневой сертификат в систему (например, вы несчастный пользователь OS X)
cert_store.add_file 'uec.cer' # Позволяет добавить свой корневой сертификат, здесь — корневой сертификат УЭК
data = File.read('исходный-файл')                             # Подписанные данные
signature = OpenSSL::PKCS7.new(File.read('файл-подписи.sig')) # Сама подпись
signature.verify(signature.certificates, cert_store, data, OpenSSL::PKCS7::DETACHED) # Можно за-OR-ить ещё OpenSSL::PKCS7::NOVERIFY, если вам плевать на сертификаты

Цифровая подпись XML (в т.ч. SOAP) сообщений

С этим прекрасно справится gem signer, который, после нескольких pull request'ов, прекрасно подписывает «по ГОСТ». Спасибо Эдгарсу Бейгартсу за создание гема, а также терпение и помощь в процессе приёма pull request'ов.

Вот, например, как подписать с помощью signer'а XML для СМЭВ:

def sign_for_smev(xml)
  signer = Signer.new(xml)
  signer.cert = OpenSSL::X509::Certificate.new(File.read(Settings.smev.cert_path))
  signer.private_key = OpenSSL::PKey.read(File.read(Settings.smev.pkey_path))
  signer.digest_algorithm = :gostr3411

  namespaces = {
    'soap' => 'http://schemas.xmlsoap.org/soap/envelope/',
  }

  # Digest soap:Body tag
  signer.document.xpath('/soap:Envelope/soap:Body', namespaces).each do |node|
    signer.digest!(node)
  end

  # Sign document itself
  signer.sign!(security_token: true)

  signer.to_xml
end

А вот другой пример, для другой системы, у которой требования построже

def sign_for_system_name(xml)
  signer = Signer.new(xml)
  signer.cert = OpenSSL::X509::Certificate.new(File.read(Settings.smev.cert_path))
  signer.private_key = OpenSSL::PKey.read(File.read(Settings.smev.pkey_path))
  signer.digest_algorithm = :gostr3411
  namespaces = {
    wsa:  'http://www.w3.org/2005/08/addressing',
    soap: 'http://www.w3.org/2003/05/soap-envelope',
  }
  # Digest WS-Addressing nodes
  signer.document.xpath('/soap:Envelope/soap:Header/wsa:*', namespaces).each do |node|
    signer.digest!(node)
  end
  # Digest soap:Body tag
  signer.document.xpath('/soap:Envelope/soap:Body', namespaces).each do |node|
    signer.digest!(node)
  end
  # Digest our own certificate
  signer.digest!(signer.binary_security_token_node)
  # Sign document itself
  signer.sign!
  signer.to_xml
end

Для проверки таких сообщений может пригодится класс Akami::WSSE::VerifySignature из master-ветки гема akami. Он проверит корректность подписи, а вот проверка сертификата и того, все ли необходимые тэги были подписаны, остаётся на вас:

def verify(signed_xml)
  verifier = Akami::WSSE::VerifySignature.new(signed_xml)
  verifier.verify!     # Здесь произойдёт БУМ, если подпись не сошлась
  verifier.certificate # Вот сертификат подписавшего, а верить ему или нет — это ваше дело.
  signed_xml
end

Хождение по HTTPS с шифрованием по ГОСТ и аутентификацией по сертификатам

Тут вообще никаких отличий нет. Единственное, что вам может поннадобиться — это добавить корневые сертификаты от серверов, на которые вы ходите, в систему (тут, правда, есть проблемы у Mac OS X).

Берёте любимую библиотеку (Net::HTTP ли это, HTTPI ли), указываете ей https адрес, ваш ключ и сертификат, и поехали!

В качестве теста можете попробовать зайти на сайт ssl-gost.envek.name/ Внимание, обычные браузеры (и непатченный Ruby) на него не зайдут и страничку вам не покажут, поскольку ГОСТовых криптоалгоритмов не разумеют, и только Firefox покажет внятное сообщение об ошибке.

И многое, многое другое

В целом, использование ГОСТовых алгоритмов не отличается от использования, например, RSA. Поэтому все материалы в интернете, такие как Ruby OpenSSL Cheat Sheet вам помогут. Я же, кажется, сказал всё, что знал.

Важно заметить, что OpenSSL (и, соответственно, Ruby) пока что поддерживает только старые алгоритмы: **ГОСТ 28147-89** (симметричное шифрование), **ГОСТ Р 34.11-94** (алгоритм хэширования) и **ГОСТ Р 34.10-2001** (асимметричное шифрование и цифровая подпись). Патчи для поддержки новых алгоритмов уже отправлены в OpenSSL кем-то, видимо, очень крутым, по имени Дмитрий Ольшанский и на них можно посмотреть на гитхабе: openssl/openssl#68 и openssl/openssl#75, так что ждём и надеемся, что примут.

В заключение

Вот так можно приятно и «нативно» работать с ГОСТовыми ЭЦП и прочим. Это действительно здорово, но как всегда есть «но», и главное из них — вопрос о возможности использования всего этого с правовой точки зрения, так как у нас получается несертифицированная СКЗИ, которую можно использовать не всегда. К сожалению, вам придётся решать этот вопрос для каждого конкретного случая отдельно, и часто ответом может оказаться «нельзя». Увы. Тут я ничего не могу подсказать, в виду моей слабой правовой грамотности, если кто-то может рассказать лучше (или уже рассказал) — с удовольствием выслушаю или прочитаю.

Если у вас есть дополнения, исправления и вопросы — жду с нетерпением!

Автор: Envek

Источник


  1. Sergey:

    Привет! Подскажи как на данный момент обстоят дела с поддержкой ГОСТ-овских алгоритмов шифрования в Ruby и OpenSSL.

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js