
Ruby and GOST cryptographic algorithms
- Tutorial

The peculiarity of the Russian sovereign IT is that wherever it is necessary to ensure the security (encryption) and integrity (signature) of information, it is necessary to use only domestic cryptographic algorithms (which are standardized and described in a dozen state standards and RFCs). This is very logical from the point of view of national security, but it is very painful from the point of view of development in the not very popular language (this is the Javists out of the blue with kind attention).
And now, when we faced the task of a very dense messaging with a GOST electronic signature from one of these systems, I did not like the proposed solution in the form of a network SOAP service signing requests (and answers) from the word “completely” (wrap SOAP in SOAP, it's some kind of nightmare squared). Long May weekends came, and when they ended, I had a better solution ...
And this is Ruby with native support for GOST cryptographic algorithms. No new external dependencies. Want to try? Go!
Installation
Configure OpenSSL
In order for everything related to GOST algorithms to work, you need customized OpenSSL, version 1.0.0 or later. In Linux, there is “out of the box”, in OS X it is necessary to install from HomeBrew (because Apple is slowpoke):
brew install openssl
brew link --force openssl
How to configure OpenSSL for GOST on the Internet they wrote many, many times, I recommend using the original manual: README.gost
Read immediately, do not follow third-party links!
On Ubuntu Linux, the configuration file is on the way
It is necessary to add the following line to the very beginning of the file:
And the following sections at the very end of the file:
The last section may require a parameter,
After these actions, if the command
/etc/ssl/openssl.cnf
, on OS X, on the way /usr/local/etc/openssl/openssl.cnf
. It is necessary to add the following line to the very beginning of the file:
openssl_conf = openssl_def
And the following sections at the very end of the file:
[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
The last section may require a parameter,
dynamic_path
but in newer versions of Linux and Mac OS X it is not needed. If necessary, its value can be learned by the team locate libgost.so
. After these actions, if the command
openssl ciphers | tr ":" "\n" | grep GOST
returns the following lines, everything is configured correctly:GOST2001-GOST89-GOST89
GOST94-GOST89-GOST89
Ruby
In order for Ruby to begin to understand everything GOST, you will need to apply a couple of patches from my bug reports # 9022 and # 9030 . These patches are successfully applied to Ruby versions 2.0.0 and 2.1.x, but have not been tested with other versions.
What are these patches?
Well, look at them yourself, that you have never seen C code, or what?
The first one inserts a call to the magic OpenSSL function
The second patch, by forging conditions and unauthorized removal of checks, makes Ruby naively believe that GOST keys are keys on elliptic curves (Elliptic Curve, EC), which, however, seems to be true, but there is no solution justifies. With this patch, Ruby will begin to “recognize” GOST private and public keys, make electronic signatures and encryption. In general, everything will be fine.
The first one inserts a call to the magic OpenSSL function
OPENSSL_config
somewhere to where Ruby initializes OpenSSL for itself. This makes OpenSSL interested in the config that we just corrected and apply it. Thanks to xtron , who in his article did the same, but for PHP (and struggled with the same problem as us, by the way). With this patch, Ruby will be able to go to HTTPS hosts with GOST encryption, for example (but without certificate authorization).The second patch, by forging conditions and unauthorized removal of checks, makes Ruby naively believe that GOST keys are keys on elliptic curves (Elliptic Curve, EC), which, however, seems to be true, but there is no solution justifies. With this patch, Ruby will begin to “recognize” GOST private and public keys, make electronic signatures and encryption. In general, everything will be fine.
Using RVM, installation is done with the command:
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
In the case of Rbenv (ruby-build), everything is somewhat more complicated, you have to execute two commands (this method did not particularly test):
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
Done!
As a result, you will have a separate Ruby named
ruby-2.1.2-gost
. This name can be written to a file .ruby-version
, and this instruction can be written to README
, and then it will always be clear that the project needs an unusual Ruby ...Installing on servers using Puppet
When it comes time to put Ruby on the server, for example, the Rbenv module for Puppet can help you, but not simple, but also patched. You will need the patch from the gitHub user gsamokovarov located here: github.com/alup/puppet-rbenv/pull/95 . In order not to suffer too much - here is the instruction for installing the module on the server:
Now
git clone git@github.com:Envek/puppet-rbenv.git # Мой форк с применённым патчем и обновлённым манифестом
gem install puppet
cd puppet-rbenv
puppet module build .
Now
pkg
you can get the freshly baked archive with the module from the catalog , upload it to the server and install it with a command puppet module install /path/to/alup-rbenv-1.2.1.tar.gz
(you may also need a key --force
) and restart the Puppet-master (puppet likes to cache the ruby-code of the modules used).Convert key pairs to a format that OpenSSL understands
Your keys and self-signed certificates can be generated using the official manual .
However, of course, the original key pairs on the token (most likely) are of interest, either in the form of a daddy with six files or an image of a floppy disk.
Unfortunately, so far the only working option to export keys to the desired format is the P12fromCSP utility from Lissy-soft. Unfortunately, only under Windows and paid. You will have to buy, but before this demo version of the program you can check whether it will help you in principle. Be warned that the program is bought by bank transfer (you can through online banking), and this is unbearably long - 3-4 days.
You will need a machine with Windows and Crypto Pro. Using Crypto Pro, install the certificate from the key medium into the system. If the keys are in the form of a folder with files, create a virtual diskette and copy them there, Crypto Pro recognizes this diskette as a key medium. After installing the certificate, make sure that it is in the system (the "Certificates" label is in the "Start" folder in the Crypto Pro folder). And run the utility, it should show the list, and in it your certificate, select it and save it to a file (at this point the utility will ask you to eat).
If you did everything correctly, but the certificate did not appear in the utility, then there are two possible reasons:
- You have a smart card token. The private key cannot be exported physically. Alas.
- The key is marked as non-exportable, the utility will refuse to export it. Is there any way around this? I don’t know.
Drag the resulting file with the extension .p12 or .pfx to the OpenSSL machine and pull out the certificate and private key from it with the following commands:
Certificate:
openssl pkcs12 -engine gost -in gost.pfx -clcerts -nokeys -out gost.crt
Private key:
openssl pkcs12 -engine gost -in gost.pfx -nocerts -nodes -out gost.pem
Now you can work!
What to do about it?
If you are interested, it means that you ALREADY need to do something. Look, here is just a small fraction of what is now possible and available to us:
General
In order for everything related to GOST algorithms to work in Ruby, you must first "get" the OpenSSL-gost engine gost, like this:
require 'openssl'
OpenSSL::Engine.load
@gost_engine = OpenSSL::Engine.by_id('gost')
@gost_engine.set_default(0xFFFF) # Решительно не знаю, что бы это значило, но без него не работает
After executing this magic piece of code, all further examples will begin to work.
@gost_engine
We still need a variable .Digital signature of a piece of data and its verification
Simple signature:
pkey = OpenSSL::PKey.read(File.read('gost.pem'))
data = 'Same message'
digester = @gost_engine.digest('md_gost94')
signature = privkey.sign(digester, data)
Verify a simple signature:
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
Creating a detached signature (hi to the registry of banned sites):
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)
Verification of detached signatures with certificate proxy verification:
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, если вам плевать на сертификаты
Digitally Sign XML (including SOAP) Messages
The gem signer will do just fine with this , which, after several pull requests, sign perfectly "according to GOST". Thanks to Edgars Beigarts for creating the gem, as well as patience and assistance in receiving pull requests.
Here, for example, is how to sign using XML signer for SMEV:
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
And here is another example, for another system whose requirements are stricter
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
To check for such messages, a class
Akami::WSSE::VerifySignature
from the akami gem master branch may come in handy . He will verify the signature is correct, but checking the certificate and whether all the necessary tags have been signed remains with you:def verify(signed_xml)
verifier = Akami::WSSE::VerifySignature.new(signed_xml)
verifier.verify! # Здесь произойдёт БУМ, если подпись не сошлась
verifier.certificate # Вот сертификат подписавшего, а верить ему или нет — это ваше дело.
signed_xml
end
HTTPS walking with GOST encryption and certificate authentication
There are no differences at all. The only thing you might like is to add root certificates from the servers you go to to the system (here, however, there are problems with Mac OS X).
Take your favorite library (
Net::HTTP
whether it is, HTTPI
whether), give it an https address, your key and certificate, and let's go! As a test, you can try visiting the ssl-gost.envek.name website . Attention, regular browsers (and unpatched Ruby) will not access it and they will not show you a page, because they do not understand the GOST cryptographic algorithms, and only Firefox will display a clear error message.
And many many others
In general, the use of GOST algorithms is no different from the use of, for example, RSA. Therefore, all materials on the Internet, such as the Ruby OpenSSL Cheat Sheet, will help you. I, it seems, said everything I knew.
It is important to note that OpenSSL (and, accordingly, Ruby) so far only supports old algorithms: ** GOST 28147-89 ** (symmetric encryption), ** GOST R 34.11-94 ** (hashing algorithm) and ** GOST R 10/34/2001 ** (asymmetric encryption and digital signature). Patches for supporting new algorithms have already been sent to OpenSSL by someone, apparently very cool, by the name of Dmitry Olshansky and can be viewed on the github: openssl / openssl # 68 and openssl / openssl # 75 , so we are waiting and hope that they will .
Finally
This is how you can pleasantly and “natively” work with GOST EDS and so on. It's really simple and cool (and fast - because the methods for working with OpenSSL in Ruby are just “wrappers” around the C library).
If you have additions, corrections and questions - I look forward to it!