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

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

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

У вас токен типа «смарт—карта». Закрытый ключ невозможно экспортировать физически. Увы. Ключ помечен как неэкспортируемый, утилита откажется его экспортировать. Есть ли возможность это обойти — не знаю. Полученный файлик с раширением .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, так что ждём и надеемся, что примут.

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

© Habrahabr.ru