📜 ⬆️ ⬇️

Ruby and GOST cryptographic algorithms

Ruby Logo and Stern GOST Padlock In life, not every developer comes the moment when they have to interact with government systems. And few of them have to interact with the Russian state systems. And so it happened the stars, that I was one of these "lucky ones".

The peculiarity of the Russian sovereign IT is that everywhere where it is necessary to ensure security (encryption) and integrity (signature) of information, it is necessary to use only domestic cryptoalgorithms (which are standardized and described in a good dozen of 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 a not very popular language (these are javisty favored by attention from all sides).

And so, when we faced the task of a very dense exchange of messages with a GOST electronic signature from one of these systems, the proposed solution in the form of a network SOAP service that signed requests (and answers) I didn’t like the word “absolutely” (wrapping SOAP SOAP is some kind of nightmare squared). The long May weekend came, and when it was over - I had a better decision ...

And this is Ruby with native support for GOST cryptoalgorithms. No new external dependencies. Want to try? Go!
')

Installation


OpenSSL setup

In order for everything related to GOST algorithms to work, a configured OpenSSL, version 1.0.0 or later, is required. In Linux, there is an "out of the box", in OS X, you need to install from HomeBrew (because Apple is a sloop box):

brew install openssl brew link --force openssl 

How to configure OpenSSL for GOST on the Internet have been written many, many times, I recommend using the original manual: README.gost

Read immediately, do not follow third-party links!
In Ubuntu Linux, the configuration file is on the path /etc/ssl/openssl.cnf , in OS X - on the path /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 the dynamic_path parameter, but is not needed in recent versions of Linux and Mac OS X. If necessary, its value can be found with the command locate libgost.so .

After these actions, if the openssl ciphers | tr ":" "\n" | grep GOST openssl ciphers | tr ":" "\n" | grep GOST openssl ciphers | tr ":" "\n" | grep GOST will return the following lines, everything is configured correctly:

 GOST2001-GOST89-GOST89 GOST94-GOST89-GOST89 


Ruby

In order for Ruby to also begin to understand everything GOST, I’ll need to put a couple of patches from my # 9022 and # 9030 bug reports. These patches successfully superimposed on Ruby versions 2.0.0 and 2.1.x, with other versions did not check.

What are these patches?
Well, look at them yourself, that you have never seen C code, have you?

The first one inserts a call to the OPENSSL_config magic OpenSSL function OPENSSL_config where Ruby initializes OpenSSL for itself. This causes OpenSSL to become interested in the config, which we have just ruled with and apply it. Thanks to the xtron habraiser , which in its article did the same, but for PHP (and struggled with the same problem as we, by the way). With this patch, Ruby can already access HTTPS hosts with GOST encryption, for example (but without certificate authorization).

The second patch, by means of forgery of conditions and unauthorized removal of checks, makes Ruby naively believe that the GOST keys are the keys on the elliptic curves (Elliptic Curve, EC), which, however, although it seems, is true, but the solution’s catholicity is not justifies. With this patch, Ruby will begin to “recognize” GOST private and public keys, make an electronic signature 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 will have to execute two commands (this method is not specifically tested):

 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 installed named ruby-2.1.2-gost . This name can be written to a .ruby-version file, and this instruction in the README , and then it will always be clear that the project needs an unusual Ruby ...

Installation on servers using Puppet
When the time comes to put Ruby on the server, the Rbenv module for Puppet can help you, for example, but not simple, but also patched. You will need a patch from the gitamub-gsamokovarov user who is here: github.com/alup/puppet-rbenv/pull/95 . In order not to suffer 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, from the pkg directory, you can get a freshly baked archive with the module, upload it to the server and install it with the command puppet module install /path/to/alup-rbenv-1.2.1.tar.gz (the --force key may still be required) and restart Puppet -master (puppet likes to cache ruby-code of used modules).

Convert key pairs to a format that OpenSSL understands

Your keys and self-signed certificates can be generated by the official manual .

However, of course, of interest are the original key pairs on the token (most likely), either in the form of a daddy with six files or a floppy disk image.

Unfortunately, so far the only working option to export the keys in the desired format is the Lissy-Software utility P12fromCSP . Unfortunately, only under Windows and paid. You 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 purchased by bank transfer (via online banking), and this is unbearably long - 3-4 days.

You need a machine with Windows and Crypto Pro. Using Crypto Pro, install the certificate from the key carrier into the system. If the keys are in the form of a file folder, create a virtual diskette and copy them there, this Krypto Pro diskette will recognize it as a key carrier. After installing the certificate, make sure that it is in the system (there is a “Certificates” shortcut 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 stage the utility will ask you to eat).

If you did everything right, but the certificate was not displayed 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 unexported, the utility will refuse to export it. Is it possible to get around this - I do not know.

The resulting file with the extension .p12 or .pfx drag on the machine with OpenSSL and pull out of it the certificate and private key 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 and work!

What to do with it?


If you are interested, it means that you ALREADY have to do something. Look, here are just a small fraction of what is now possible and accessible to us:

General

In order for everything related to GOST algorithms to work in Ruby, you must first get OpenSSL goss engine, 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 start working. We still need the @gost_engine 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) 

Simple signature verification:

 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 (hello to the registry of prohibited 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) 

Validation of detached signatures with certificate 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,      

Digital Signature XML (incl. SOAP) messages

Gem signer , which, after several pull requests, perfectly signs “according to GOST”, will perfectly cope with this. Thanks to Edgars Beigarts for creating the gem, as well as patience and help in the process of receiving pull requests.

Here, for example, is how to sign XML with the help of 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 these messages, the Akami::WSSE::VerifySignature from the Akami::WSSE::VerifySignature master can be useful. It will verify the signature is correct, but the verification of 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 encryption according to GOST and certificate authentication

There are no differences at all. The only thing you need is to add root certificates from the servers that you go to to the system (here, however, Mac OS X has problems).

You take your favorite library ( Net::HTTP is it, HTTPI is), give it an https address, your key and certificate, and let's go!

As a test, you can try to access the ssl-gost.envek.name website . Attention, regular browsers (and unpatched Ruby) will not go to it and the page will not show you, because GOST cryptoalgorithms are not understood, and only Firefox will show a clear error message.

And many many others

In general, the use of GOST algorithms does not differ from the use, for example, RSA. Therefore, all materials on the Internet, such as 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 supports only old algorithms: ** GOST 28147-89 ** (symmetric encryption), ** GOST R 34.11-94 ** (hashing algorithm) and ** GOST P 34.10-2001 ** (asymmetric encryption and digital signature). Patches to support the new algorithms have already been sent to OpenSSL by someone, apparently, very cool, by the name of Dmitry Olshansky and they can be viewed on the githaba: openssl / openssl # 68 and openssl / openssl # 75 , so we are waiting and hoping that they will accept .

Finally


This is how it is pleasant and “native” to work with GOST EDS and other things. It's really simple and cool (and fast - after all, 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!

Source: https://habr.com/ru/post/231261/


All Articles