Replacing OpenSSL with BoringSSL in a Complex Multi-Platform Layout

Intro

In Themis, we use industry-recognized implementations of cryptographic algorithms that come from OpenSSL/LibreSSL packages. However, we often experiment with other sources of crypto primitives: either for performance, the elegance of build, or for their ability to address native implementations.

To achieve compatibility with Android 6, we needed to integrate BoringSSL anyway. But, apart from addressing the nativeness, we were highly interested in what is hiding behind Google's revamp of OpenSSL. This post summarises our experience of integrating BoringSSL instead of OpenSSL into Themis, and is the first article of 3 that will cover our experiments with using different cryptographic primitives inside Soter (algorithm integration layer in Themis), enhancing its integration flexibility and building custom versions of Themis.

First impressions

Since Google has introduced its own fork of OpenSSL — BoringSSL — we at Cossack Labs were keen on testing the architectural agility of Soter with it. Namely, if it is possible to seamlessly replace one libcrypto for another. Modular design in Themis should have allowed us to do that easily, without re-writing a lot of code.

Even though it is created by Google, BoringSSL (currently) displays no intent of dominating all the possible platforms. For instance, it won't just work out of the box on a barebones Linux distro, because it needs Android NDK (which, in turn, needs Android SDK) to compile. Since BoringSSL is a default dependency for building SSL-dependent apps on Android now, it was not only curiosity that drove us, but a desire to support the native means where we can, too.

Our goal was to:

  • Study BoringSSL,
  • Understand what needs to be changed in order to implement all Themis functions on top of BoringSSL,
  • Actually do that.

Both OpenSSL and BoringSSL have advanced high-level interfaces. However, while pairing OpenSSL/BoringSSL with Themis, we only use the libraries for their crypto primitives. It's important to note that, given a number of build targets, we want calling methods as universal and as explicit as possible: given the vast availability of Themis, you never know on which platform custom hacks fail (hi, OpenSSL and iOS!)

BoringSSL forked from OpenSSL when Google decided that OpenSSL accepted requests and fixes from Google too slowly. Forking OpenSSL into a new independent project allowed Google to do whatever they deemed necessary. However, Google still tries to keep the interface of BoringSSL as close as that of OpenSSL. Our wild guess is that maybe Google is still hoping that OpenSSL will accept and implement all their corrections, and BoringSSL and OpenSSL will mostly merge back in together.

Integration surface: Soter

Soter is a way to elegantly bridge cryptographic implementations to Themis. We're proponents of traditional UNIX philosophy — each component has to do one thing and do it well, and multi-functional objects should be decomposed into modules with well-defined interfaces.

That's why, foreseeing the need to build different cryptographic implementations into Themis, we've separated low-level crypto into a separate module called Soter.


Scheme of Soter with underlying primitives, as it is embedded inside Themis

The major way in which BoringSSL is different from OpenSSL concerns the optimisation and rudiment deletions, as well as a number of other distinctive features. Some interesting changes to the interface have made it (subjectively) more comfortable in terms of use. One of the large improvements is the deprecation of EVP_PKEY_CTX_ctrl family of functions that one needs to tiptoe around when using with OpenSSL due to a high probability of introducing errors in the process.

To switch from OpenSSL's libcrypto to BoringSSL's, we had to make Themis' code talk to BoringSSL's implementation of libcrypto, adjusting Soter interfaces that are looking into crypto primitives.

Here is how Themis utilizes symmetric algorithms from OpenSSL (https://github.com/cossacklabs/themis/blob/master/src/soter/openssl/soter_sym.c):


...
soter_sym_ctx_t* soter_sym_encrypt_create(const uint32_t alg, const void* key, const size_t key_length, const void* salt, const size_t salt_length, const void* iv, const size_t iv_length);
soter_status_t soter_sym_encrypt_update(soter_sym_ctx_t *ctx, const void* plain_data,  const size_t data_length, void* cipher_data, size_t* cipher_data_length);
soter_status_t soter_sym_encrypt_final(soter_sym_ctx_t *ctx, void* cipher_data, size_t* cipher_data_length);
...


...
  if(encrypt){
    SOTER_IF_FAIL_(EVP_EncryptInit_ex(&(ctx->evp_sym_ctx), algid_to_evp_aead(alg), NULL, key_, iv), soter_sym_encrypt_destroy(ctx));
  } else {
    SOTER_IF_FAIL_(EVP_DecryptInit_ex(&(ctx->evp_sym_ctx), algid_to_evp_aead(alg), NULL, key_, iv), soter_sym_encrypt_destroy(ctx));
  }
...
  if(encrypt){
    SOTER_CHECK(EVP_EncryptUpdate(&(ctx->evp_sym_ctx), out_data, (int*)out_data_length, (void*)in_data, (int)in_data_length)==1);
  } else {
    SOTER_CHECK(EVP_DecryptUpdate(&(ctx->evp_sym_ctx), out_data, (int*)out_data_length, (void*)in_data, (int)in_data_length)==1);
  }   
...
  if(encrypt){
    SOTER_CHECK(EVP_EncryptFinal(&(ctx->evp_sym_ctx), out_data, (int*)out_data_length)!=0);
  } else {
    SOTER_CHECK(EVP_DecryptFinal(&(ctx->evp_sym_ctx), out_data, (int*)out_data_length)!=0);
  }
  ...

The asymmetric part of using algorhitms from OpenSSL in Soter (https://github.com/cossacklabs/themis/blob/master/src/soter/openssl/soter_asym_cipher.c) is implemented like this:

...
soter_asym_ka_t* soter_asym_ka_create(soter_asym_ka_alg_t alg);
soter_status_t soter_asym_ka_derive(soter_asym_ka_t* asym_ka_ctx, const void* peer_key, size_t peer_key_length, void *shared_secret, size_t* shared_secret_length);
soter_status_t soter_asym_ka_destroy(soter_asym_ka_t* asym_ka_ctx);
...

    ...
    soter_status_t soter_asym_ka_derive(soter_asym_ka_t* asym_ka_ctx, const void* peer_key, size_t peer_key_length, void *shared_secret, size_t* shared_secret_length)
    {
            EVP_PKEY *peer_pkey = EVP_PKEY_new();
            soter_status_t res;
            size_t out_length;
    
            if (NULL == peer_pkey)
            {
                    return SOTER_NO_MEMORY;
            }
    
            if ((!asym_ka_ctx) || (!shared_secret_length))
            {
                    EVP_PKEY_free(peer_pkey);
                    return SOTER_INVALID_PARAMETER;
            }
    
            res = soter_ec_pub_key_to_engine_specific((const soter_container_hdr_t *)peer_key, peer_key_length, ((soter_engine_specific_ec_key_t **)&peer_pkey));
            if (SOTER_SUCCESS != res)
            {
                    EVP_PKEY_free(peer_pkey);
                    return res;
            }
    
            if (1 != EVP_PKEY_derive_init(asym_ka_ctx->pkey_ctx))
            {
                    EVP_PKEY_free(peer_pkey);
                    return SOTER_FAIL;
            }
    
            if (1 != EVP_PKEY_derive_set_peer(asym_ka_ctx->pkey_ctx, peer_pkey))
            {
                    EVP_PKEY_free(peer_pkey);
                    return SOTER_FAIL;
            }
    
            if (1 != EVP_PKEY_derive(asym_ka_ctx->pkey_ctx, NULL, &out_length))
            {
                    EVP_PKEY_free(peer_pkey);
                    return SOTER_FAIL;
            }
    
            if (out_length > *shared_secret_length)
            {
                    EVP_PKEY_free(peer_pkey);
                    *shared_secret_length = out_length;
                    return SOTER_BUFFER_TOO_SMALL;
            }
    
            if (1 != EVP_PKEY_derive(asym_ka_ctx->pkey_ctx, (unsigned char *)shared_secret, shared_secret_length))
            {
                    EVP_PKEY_free(peer_pkey);
                    return SOTER_FAIL;
            }
    
            EVP_PKEY_free(peer_pkey);
            return SOTER_SUCCESS;
    }
    ...

After careful consideration, it turned out there is no significant difference between OpenSSL/BoringSSL interfaces for AES. Both libraries are used in the same way (code example from themis/src/soter/boringssl/soter_sym.c):

...  
  if(encrypt){
    SOTER_IF_FAIL_(EVP_EncryptInit_ex(&(ctx->evp_sym_ctx), algid_to_evp_aead(alg), NULL, key_, iv), soter_sym_encrypt_destroy(ctx));
  } else {
    SOTER_IF_FAIL_(EVP_DecryptInit_ex(&(ctx->evp_sym_ctx), algid_to_evp_aead(alg), NULL, key_, iv), soter_sym_encrypt_destroy(ctx));
  }
...
  if(encrypt){
    SOTER_CHECK(EVP_EncryptUpdate(&(ctx->evp_sym_ctx), out_data, (int*)out_data_length, (void*)in_data, (int)in_data_length)==1);
  } else {
    SOTER_CHECK(EVP_DecryptUpdate(&(ctx->evp_sym_ctx), out_data, (int*)out_data_length, (void*)in_data, (int)in_data_length)==1);
  }   
...
  if(encrypt){
    SOTER_CHECK(EVP_EncryptFinal(&(ctx->evp_sym_ctx), out_data, (int*)out_data_length)!=0);
  } else {
    SOTER_CHECK(EVP_DecryptFinal(&(ctx->evp_sym_ctx), out_data, (int*)out_data_length)!=0);
  }
...  

However, the asymmetric part used by Themis is quite different, because — as we've mentioned before — the EVP_PKEY_CTX_ctrl family of functions is deprecated in BoringSSL.

For example, setting the padding type in asymmetric encryption with OpenSSL will look like this:

...        
        if (1 > EVP_PKEY_CTX_ctrl(asym_cipher->pkey_ctx, -1, -1, EVP_PKEY_CTRL_RSA_PADDING, RSA_PKCS1_OAEP_PADDING, NULL))
        {
                return SOTER_FAIL;
        }
... 

And in BoringSSL things work in a slightly different manner:


...        
        if (1 > EVP_PKEY_CTX_set_rsa_padding(asym_cipher->pkey_ctx, RSA_PKCS1_OAEP_PADDING))
        {
                return SOTER_FAIL;
        }
...   

While in BoringSSL things may look a bit different on the surface and in implementations of internal functions, under the hood, everything works similarly to OpenSSL. Generally speaking, these slight improvements are welcome and make the external developers' work easier.

On the other hand, it is not hard to see why the changes in BoringSSL compared to OpenSSL have been cosmetic and not radical. So many products are utilising OpenSSL that a drastic change in the interfaces of BoringSSL would only lead to everyone leaving it for the vanilla version of OpenSSL. BoringSSL would turn people away if it was changing too fast.

Google's actions are more subtle. Since the changes are slight and the benefits of using BoringSSL — as compared to bloated OpenSSL — when performance and lightness is valued over tradition, more and more developers are very likely to gradually switch to BoringSSL.

Let's go deeper into the details.

Details and Challenges

In the part of Soter that used OpenSSL, we needed to use EVP_PKEY_CTX_ctrl family of functions for setting some specific parameters for the used crypto algorithms, i.e. for setting the padding type in asymmetric cryptoprimitives, etc.. The main problem turned out to be that with OpenSSL the EVP_PKEY_CTX_ctrl function must accept parameter, which is a mix of an identifier and a set/get flag. A sample of such Themis code can be found in themis/src/soter/openssl/soter_asym_cipher.c:


...        
        if (1 > EVP_PKEY_CTX_ctrl(asym_cipher->pkey_ctx, -1, -1, EVP_PKEY_CTRL_RSA_PADDING, RSA_PKCS1_OAEP_PADDING, NULL))
        {
                return SOTER_FAIL;
        }
...

This approach in OpenSSL allows supporting huge sets of algorithms without changing the interface. But still, there is a problem with such approach — the correctness of a parameter with an identifier may only be checked in runtime.

In some algorithms, OpenSSL supports the so-called paramgen set of functions which can be used for setting algorithm-dependent parameters (the following code sample is taken from Themis themis/src/soter/openssl/soter_asym_ka.c):


...        
        if (1 != EVP_PKEY_paramgen_init(asym_ka_ctx->pkey_ctx))
        {
                EVP_PKEY_free(pkey);
                return SOTER_FAIL;
        }

        if (1 != EVP_PKEY_CTX_set_ec_paramgen_curve_nid(asym_ka_ctx->pkey_ctx, nid))
        {
                EVP_PKEY_free(pkey);
                return SOTER_FAIL;
        }

        if (1 != EVP_PKEY_paramgen(asym_ka_ctx->pkey_ctx, &pkey))
        {
                EVP_PKEY_free(pkey);
                return SOTER_FAIL;
        }
...        

Usage of paramgen allows us to solve a part of the problem — it separates the parameter identifier from the set/get flag. But the problem of verifying whether the parameter is correct or not remains, and you can only find out during the runtime. This happens because at the compilation stage the call for, for example EVP_PKEY_CTX_set_ec_paramgen_curve_nid(asym_ka_ctx->pkey_ctx, nid)) doesn't know the exact type that asym_ka_ctx->pkey_ctx has.

The BoringSSL way

BoringSSL uses an approach that is strikingly different from that of OpenSSL. For each customizable parameter of crypto algorithms/procedures, separate getter and setter were defined.

This is a rather obvious decision to be made when trying to simplify and “humanise” the use of OpenSSL. The main reason why OpenSSL doesn't implement this obvious move is that the main goal for OpenSSL is providing the support of everything but the kitchen sink (and, well, who knows, maybe a kitchen sink needs security, too), even though the overwhelmingly major part of the currently supported algorithms is very rarely used, and when it is indeed used, it is for outright ancient projects. In fact, with Soter we did a similar thing — we only kept the algorithms we needed and wrapped them in a way that was convenient for us, creating a lightweight goal-specific version.

An example of this can be found in our Themis code here themis/src/soter/boringssl/soter_asym_cipher.c:

if (1 > EVP_PKEY_CTX_set_rsa_padding(asym_cipher->pkey_ctx, RSA_PKCS1_OAEP_PADDING))

In this case, an attempt to call an incorrect function (setting an incorrect parameter) error was detected during compilation. Moreover, certain algorithm context must support parameter setting, but there are also algorithms that have no specific parameters at all.

See, all the crypto primitives are actually very similar in their interfaces. The usual scheme looks something like this: there is a key, there is the data that is being transformed, and there is a resulting blob of encrypted data. But every algorithm can also have (and has!) some parameters that are rather peculiar, and are characteristic only for that algorithm. Here, as a rule, 2 different approaches are used:

  • Setting the parameters during the compilation (as cryptoPP or Themis does),
  • Realising additional configuration functionality (as in OpenSSL/BoringSSL).

The 2nd way is more flexible, but we prefer using the 1st approach because there still is a problem with the 2nd one — when the number of primitives increases, it becomes much harder to maintain clean code and use this method. This happens because in OpenSSL such settings are carried out using just one function EVP_PKEY_CTX_ctrl(); into which a parameter identifier is passed. It needs to be set/get and a buffer to get it from /set it to. It is extremely inconvenient to code this way because an error (i.e. setting a parameter what is not supported by the algorithm used) can only be detected at the execution stage.

Obviously, creating a full set of setters and getters for OpenSSL is a hard task because OpenSSL supports many algorithms with many parameters (cryptographic agility always comes at a price). Saving EVP_PKEY_CTX_ctrl even for backward compatibility's sake completely negates the advantages of using setters and getters.

However, from the point of view of OpenSSL, such an approach is justified because the number of supported algorithms is really large — supporting separate functions for each would be much harder and this would also mess with the algorithm's universality.

But BoringSSL is more advanced. The support of many older and outdated algorithms was removed — only the algorithms that are really used remain. This allowed reworking the inner structure in such a way that makes it possible to find errors on the compilation stage. This means that the use of getters and setters in BoringSSL is justified — in fact, there are different interfaces for different types, and each type received its own getters and setters.

Performance Benchmarks

We carried out several performance tests to compare OpenSSL with BoringSSL when used by 3 of the Themis cryptosystems: Secure Cell, Secure Message, and Secure Session.

Since the interfaces of OpenSSL and BoringSSL are very close, their performance as Soter wrappers is very similar. Besides the interface-related changes, Google introduced some changes into the inner implementation of the AES algorithm (this could have affected other algorithms, too) with the goal of optimising it for small-length blocks of data (judging by the graphs) — up to 10Kb. Most likely the goal of this optimisation this is tied to the fact that the target audience of BoringSSL is constituted by the users of mobile platforms and internet services.

Secure Cell

Here you can see that symmetric encryption for small blocks (up to 10Kb) is the priority for BoringSSL.

Secure Message

Because the point of intersection has not moved, it's obvious that nothing was changed in the department of asymmetric encryption. The overall looks of this graph closely resemble the graph for Secure Cell since Secure Message is basically the Secure Cell with an additional asymmetric operation.

Secure Session

Also very similar to Secure Message, but here there is more than one asymmetric operation. Because OpenSSL and BoringSSL deal with asymmetric encryption in a similar manner, The mutual arrangement of graphs does not change when compared to Secure Cell.

The performance capabilities of BoringSSL vs OpenSSL turned out to be comparable in all the tests, driven by one main difference: symmetric cipher speed. However, BoringSSL is considerably faster when it comes to small (less than 10Kb) chunks of data.

Conclusion

What we've learnt after carrying out the tests?

Integrating new things into Soter turned out to be easy — the modular structure of Soter allowed seamlessly incorporating a new source of crypto primitives into Themis and this new custom build worked as intended.

The differences in approach between OpenSSL and BoringSSL are minimal, yet substantial in their effect on the ability to make mistakes. The introduction of individual getters and setters for each customizable parameter of crypto algorithms/procedures in BoringSSL has made it harder to shoot yourself in the foot, which is a welcome change.

Google did a good job of re-engineering the OpenSSL to improve not only the internals of the library but some elements of the interface, too. Still, the attempt at preserving full interchangeability between OpenSSL and BoringSSL is obvious. This means that the implemented interface changes were minimal. Deep changes in BoringSSL's interfaces happened only where the old approaches would be impossible because of the internal changes in calling methods.

Copyright © 2014-2017 Cossack Labs Limited
Cossack Labs is a privately-held British company with a team of data security experts based in Kyiv, Ukraine.