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