Ruby and GOST cryptographic algorithms

  • Tutorial
Ruby logo and stern GOST padlockIn the life of not every developer, there comes a time when you have to interact with government systems. And few of them have to interact specifically with Russian state systems. And so the stars formed that I was one of these "lucky ones."

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 /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_pathbut 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 GOSTreturns 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 OPENSSL_configsomewhere 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:

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

Now pkgyou 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:

  1. You have a smart card token. The private key cannot be exported physically. Alas.
  2. 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_engineWe 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::VerifySignaturefrom 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::HTTPwhether it is, HTTPIwhether), 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!

Also popular now: