В жизни далеко не каждого разработчика наступает момент, когда приходится взаимодействовать с государственными системами. И немногим из них приходится взаимодействовать именно с российскими государственными системами. И так уж сложились звёзды, что я оказался одним из этих «счастливчиков».
Особенность российского государева ИТ в том, что везде, где нужно обеспечить безопасность (шифрование) и целостность (подпись) информации, необходимо использовать только отечественные криптоалгоритмы (которые стандартизованы и описаны в добром десятке ГОСТов и 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
, в OS X — по пути ssl/openssl.cnf
.
local 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, с другими версиями не проверял.
Первый вставляет вызов волшебной 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…
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) # Решительно не знаю, что бы это значило, но без него не работает
После выполнения этого магического куска кода, все дальнейшие примеры начнут работать. Переменная нам ещё понадобится.
Цифровая подпись куска данных и её проверка
Простая подпись:
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
Привет! Подскажи как на данный момент обстоят дела с поддержкой ГОСТ-овских алгоритмов шифрования в Ruby и OpenSSL.