Skip to content

Instantly share code, notes, and snippets.

@digitalresistor
Last active October 23, 2018 01:38
Show Gist options
  • Save digitalresistor/5300110 to your computer and use it in GitHub Desktop.
Save digitalresistor/5300110 to your computer and use it in GitHub Desktop.
Stupid OpenSSL idiosyncrasies/bad documentation/missing documentation I run across, or simply completely undocumented functions.
/*
* Brokeness exhibited by libcrypto when using the DER functions to
* encode/decode a Diffie Hellman private key using the PKCS8 functions.
*
* ---
*
* libcrypto's d2i_PKCS8PrivateKey_bio function is unable to read data that is
* generated using it's own i2d_PKCS8PrivateKey_bio function, this also limits
* functionality tremendously in that i2d_PrivateKey can not take any callbacks
* or information for a password, and thus can't encrypt the end result, which
* the PKCS8 functions are able to do.
*
* The biggest issue is that when trying to write a library that wraps this
* functionality, we don't know ahead of time whether we are going to be
* working with a DH key or not ... so if we try to d2i an memory BIO we are
* passed, if we use the PKCS8 functions we may fail. At that point part of the
* data has already been read from the BIO, and for read/write BIO's we can't
* reset it back to where it was, so we can't even attempt to fall back on the
* d2i_PrivateKey function.
*
* The biggest problem is that the PEM functions work without issues. So
* PEM_write_bio_PKCS8PrivateKey can correctly be decoded by
* PEM_read_bio_PrivateKey (there is no PKCS8 equivelant, since it handles it
* behind the scenes).
*
* ---
*
* Compile using:
*
* clang `pkg-config libcrypto --libs --cflags` -Wall -Wextra d2i_pkcs8privatekey.c
*
* or for broken version:
*
* clang `pkg-config libcrypto --libs --cflags` -Wall -Wextra d2i_pkcs8privatekey.c -DBROKEN
*
* Run:
*
* ./a.out
* echo $?
*
* When compiled with BROKEN, return code will be 8.
* When compiled without BROKEN, return code will be 0.
*/
#include <stdio.h>
#include <openssl/evp.h>
#include <openssl/err.h>
#include <openssl/dh.h>
#include <openssl/pem.h>
int main() {
OpenSSL_add_all_algorithms();
ERR_load_crypto_strings();
EVP_PKEY_CTX *ctx;
EVP_PKEY *pkey = 0;
/* Generate new DH parameters */
ctx = EVP_PKEY_CTX_new_id(EVP_PKEY_DH, 0);
if (!ctx) {
ERR_print_errors_fp(stderr);
return 1;
}
if (EVP_PKEY_paramgen_init(ctx) <= 0) {
ERR_print_errors_fp(stderr);
return 2;
}
// DONT USE 256 IN PRODUCTION
if (EVP_PKEY_CTX_set_dh_paramgen_prime_len(ctx, 256) <= 0) {
ERR_print_errors_fp(stderr);
return 3;
}
if (EVP_PKEY_paramgen(ctx, &pkey) <= 0) {
ERR_print_errors_fp(stderr);
return 4;
}
EVP_PKEY_CTX_free(ctx);
/* Print out the parameters in PEM format */
BIO *out = BIO_new_fp (stdout, BIO_NOCLOSE);
PEM_write_bio_Parameters(out, pkey);
EVP_PKEY *dh_pkey = 0;
ctx = 0;
/* Generate new DH private/public key */
ctx = EVP_PKEY_CTX_new(pkey, 0);
if (ctx == 0) {
ERR_print_errors_fp(stderr);
return 5;
}
if (EVP_PKEY_keygen_init(ctx) <= 0) {
ERR_print_errors_fp(stderr);
return 6;
}
if (EVP_PKEY_keygen(ctx, &dh_pkey) <=0) {
ERR_print_errors_fp(stderr);
}
/* Write the new DH private key in PEM format to stdout */
if (PEM_write_bio_PKCS8PrivateKey(out, dh_pkey, 0, 0, 0, 0, 0) <= 0) {
ERR_print_errors_fp(stderr);
}
/* Write the new DH public key in PEM format to stdout */
if (PEM_write_bio_PUBKEY(out, dh_pkey) <= 0) {
ERR_print_errors_fp(stderr);
}
BIO *mem = BIO_new(BIO_s_mem());
EVP_PKEY *mem_pkey = 0;
#ifdef BROKEN
/* Convert DH private key to DER format using PKCS8 */
if (i2d_PKCS8PrivateKey_bio(mem, dh_pkey, 0, 0, 0, 0, 0) <= 0) {
ERR_print_errors_fp(stderr);
return 7;
}
/*
* Attempt to load the DH private key from DER format using PKCS8
*
* This is where it fails.
*/
if (d2i_PKCS8PrivateKey_bio(mem, &mem_pkey, 0, 0) <= 0) {
ERR_print_errors_fp(stderr);
return 8;
}
#else
/* Convert DH private key to DER format, no encryption/password possible */
if (i2d_PrivateKey_bio(mem, dh_pkey) <= 0) {
ERR_print_errors_fp(stderr);
return 9;
}
/* Load DH private key from DER format, no encryption/password possible */
if (d2i_PrivateKey_bio(mem, &mem_pkey) <= 0) {
ERR_print_errors_fp(stderr);
return 10;
}
#endif
/* Clean up :-) */
EVP_PKEY_free(mem_pkey);
BIO_free_all(mem);
EVP_PKEY_free(pkey);
EVP_PKEY_CTX_free(ctx);
EVP_PKEY_free(dh_pkey);
BIO_free_all(out);
return 0;
}

You are now getting to a point where you know you want to get a copy of an EVP_PKEY for one reason or another, not that it matters much why, you just need it.

So you start looking for a way to duplicate it, there has to be a function for it, right? You come across EVP_PKEY_CTX_dup, so you make the assumption that EVP_PKEY_dup should probably exist too ... well you'd be wrong. You come across this message on the OpenSSL mailling list: http://www.mail-archive.com/[email protected]/msg17608.html and the next follow-up says to just up the reference count, or RSA_dup() and copy it into the new EVP_PKEY ... except RSA_dup() doesn't exist either.

No real solutions come out of that email thread. No deep copies seem to be possible, well until you simply consider converting it from an EVP_PKEY format to PEM/DER and then back to an EVP_PKEY.

So, in that case all that is left is to encode it to PEM/DER and then decode it from PEM/DER.

// Create new memory BIO
BIO* tbio = BIO_new(BIO_s_mem());

EVP_PKEY *target = 0;
EVP_PKEY *from = <whatever>;

// Do note that in OpenSSL functions return 1 on success, anything less than that on failure, unless the documentation says otherwise!
if (PEM_write_bio_PKCS8PrivateKey(tbio, from, 0, 0, 0, 0, 0) <= 0) {
    /* error has occured */
}

// For example, this function returns the new EVP_PKEY* it creates, unless it fails, in which case it returns NULL
if (PEM_read_bio_PrivateKey(tbio, &target, 0, 0) == 0) {
    /* error has occured */
}

Okay, now you should have a deep copy, of an EVP_PKEY, so long as it happens to be a private key, otherwise you will need to use PEM_write_bio_PUBKEY and PEM_read_bio_PUBKEY (Using a more standardised naming scheme seems to have been missed by the OpenSSL team. PEM_write_bio_PublicKey would match PEM_write_bio_PrivateKey much better.) So to duplicate a private key you use PKCS8PrivateKey, for a public key you use PUBKEY and for parameters you use Parameters.

I used the PEM functions here since they seemed to be the simplest and fastests way to accomplish what I needed, however you could use the DER functions instead:

i2d_PrivateKey, i2d_PUBKEY (Don't confuse this with i2d_PublicKey - although now I understand where the naming for the PEM function comes from ;-)), i2d_DHparams and i2d_ECParameters. Where the i2d stands for "internal to DER". The reverse functions are named d2i for "DER to internal".

Do note that there is no i2d_Parameters, so if you are making a deep copy of an EVP_PKEY containing paramaters and want to use the DER functions you are stuck getting a reference to the underlying key type DH or EC_KEY and using the specific DER functions. Specifically look for the documentation for EVP_PKEY_get1_<TYPE>. Remember that the returned object has its reference count increased, so make sure to use the appropriate <TYPE>_free() function to make sure you don't leak memory.

So you are attempting to convert the internal representation of a DH* object to DER using the i2d_DHparams_bio function, and run into the following error when compiling using a C++ compiler:

src/dh.cc:28:16: error: no matching function for call to 'ASN1_i2d_bio'
        return i2d_DHparams_bio(tbio, dhtemp);
               ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
/usr/local/Cellar/openssl/1.0.1e/include/openssl/dh.h:180:32: note: expanded from macro 'i2d_DHparams_bio'
#define i2d_DHparams_bio(bp,x) ASN1_i2d_bio_of_const(DH,i2d_DHparams,bp,x)
                               ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
/usr/local/Cellar/openssl/1.0.1e/include/openssl/asn1.h:1020:6: note: expanded from macro 'ASN1_i2d_bio_of_const'
    (ASN1_i2d_bio(CHECKED_I2D_OF(const type, i2d), \
     ^~~~~~~~~~~~
/usr/local/Cellar/openssl/1.0.1e/include/openssl/asn1.h:1012:5: note: candidate function not viable: cannot convert argument of incomplete type 'void *' to 'unsigned char *'
int ASN1_i2d_bio(i2d_of_void *i2d,BIO *out, unsigned char *x);
    ^
1 error generated.
scons: *** [src/dh.o] Error 1

Attempting to use i2d_DHParams_bio inside of a C++ project won't work, because when attempting to call ASN1_i2d_bio an implicit conversion is done from a void * to unsigned char* which in C++ mode is invalid and would need to be casted (preferably by adding (unsigned char*) at the conversion point, something that should most likely be added anyway ...).

I've fixed it with the following shim:

wrap_i2d.h

#ifndef WRAP_I2D_H_3CE37DFF55C2E2
#define WRAP_I2D_H_3CE37DFF55C2E2

#include <openssl/dh.h>

#ifdef __cplusplus
extern "C" {
#endif
    DH* wrap_d2i_DHparams_bio(BIO* tbio, DH** dhtemp);
    int wrap_i2d_DHparams_bio(BIO* tbio, DH* dhtemp);
#ifdef __cplusplus
}
#endif

#endif /* WRAP_I2D_H_3CE37DFF55C2E2 */

wrap_i2d.c

#include <openssl/dh.h>
/* Not including openssl/asn1.h will cause errors about implicit functions,
* this is due to the fact that eventhough dh.h references ASN1 functions it
* doesn't include the header ... 
*/
#include <openssl/asn1.h>

#include "wrap_i2d.h"

DH* wrap_d2i_DHparams_bio(BIO* tbio, DH** dhtemp) {
    return d2i_DHparams_bio(tbio, dhtemp);
}

int wrap_i2d_DHparams_bio(BIO* tbio, DH* dhtemp) {
    return i2d_DHparams_bio(tbio, dhtemp);
}

Now compile wrap_i2d.c with a C compiler, include wrap_i2d.h in your C++ project, and link the two together. Voilá now it works without issues.

OpenSSL provides various functions for reading/writing PEM files, specifically the ones we will be looking at today deal with taking an EVP_PKEY* and turning it into PEM, and reading it from PEM back into an EVP_PKEY*. So lets go take a look at the documentation, it is available at http://www.openssl.org/docs/crypto/pem.html

Let's look at the function prototype for turning it into a PKCS8 PEM file, note we are going to use PEM_write_bio_PKCS8PrivateKey because we don't want to use the old PrivateKey functions since they use an old PEM format that did the encryption not at the PKCS8 level, but at the PEM data level. Better interopability with PKCS8 encryption ...

int PEM_write_bio_PKCS8PrivateKey(BIO *bp, EVP_PKEY *x, const EVP_CIPHER *enc,
                                           char *kstr, int klen,
                                           pem_password_cb *cb, void *u);

Let's quickly go over the parameters, since from a first glance you just can't be sure what they are:

  • bp - A BIO, OpenSSL's abstraction for data IO.
  • x - A pointer to an EVP_PKEY
  • enc - an EVP_CIPHER, so that we can specify what encryption to use on this, so far so good.
  • kstr - A what now?

    For the PEM write routines if the kstr parameter is not NULL then klen bytes at kstr are used as the passphrase and cb is ignored. Alright, so we can give it a char* to a string that contains the passphrase

  • klen - The length of the passphrase
  • cb - A call back
  • u - A void pointer that is passed to the call back, UNLESS the cb parameter is set to NULL in which case it will contain a null terminated string to use as the passphrase.

    If the cb parameters is set to NULL and the u parameter is not NULL then the u parameter is interpreted as a null terminated string to use as the passphrase. If both cb and u are NULL then the default callback routine is used which will typically prompt for the passphrase on the current terminal with echoing turned off. What this fails to mention though, is that last segment isn't entirely true either, especially if you read the previous section stating that if the kstr parameter is not NULL then klen bytes are used as the passphrase and the cb is ignored, and you have to hope that it will ignore u as well eventhough that is not explicitly stated...

So far so good, in my case since the passphrase could potentially contain the NULL byte I will use the kstr and klen paramaters, so we get something along these lines:

const std::string password = "password that may contain null bytes"; 
PEM_write_bio_PKCS8PrivateKey(tbio, priv_key, EVP_aes_128_cbc(), const_cast<char *>(password.c_str()), password.length(), 0, NULL)

This will write a PEM formatted file to our BIO, which in my case is hooked up to stdout. In return you get something like this (the password above wasn't used to create this below, so don't try and decrypt it =)):

-----BEGIN ENCRYPTED PRIVATE KEY-----
MIGtMEkGCSqGSIb3DQEFDTA8MBsGCSqGSIb3DQEFDDAOBAj6AblCDSKMxgICCAAw
HQYJYIZIAWUDBAEqBBA0ZH2i/62kun3pYYdBH6uABGCnGxAcaRu2H1QgnlCktFMu
AbhenGnoRbqPrs2l1jLxVBo7NE37exg27O4Rhw2HczTxqJitn2eoeGJkooXr8zC0
4pMPYpfLG8TWR6wI3jYZfOxpHX0qFyUIP88D3fjJAwo=
-----END ENCRYPTED PRIVATE KEY-----

So far so good ... now lets go take a look at getting that back into an EVP_PKEY format, since we want this to be a two way street ...

First,there is no PEM_read_bio_PKCS8PrivateKey, so looking at the rest of the documentation we find that we need to use PEM_read_bio_PrivateKey and that it will automatically know how to load PKCS8 unencrypted and encrypted files.

EVP_PKEY *PEM_read_bio_PrivateKey(BIO *bp, EVP_PKEY **x,
                                       pem_password_cb *cb, void *u)

So lets go over the parameters here:

  • bp - Well, we know what that is
  • **x - A pointer to a pointer of EVP_PKEY, makes sense, lets the routine allocate and set the object if it doesn't exist yet
  • cb - Callback function
  • u - A void pointer to user defined data

Hold up ... there is no way to provide a passphrase, unless we do it in u, however that has to be a null terminated string, and my passphrase could potentially contain the null character, so I can't use that function.

Guess call back time it is, let's go take a look at what the call back looks like:

int pass_cb(char *buf, int size, int rwflag, void *u);
{
    int len;
    char *tmp;
    /* We'd probably do something else if 'rwflag' is 1 */
    printf("Enter pass phrase for \"%s\"\n", u);
    
    /* get pass phrase, length 'len' into 'tmp' */
    tmp = "hello";
    len = strlen(tmp);
    
    if (len <= 0) return 0;
    
    /* if too long, truncate */
    if (len > size) len = size;
    memcpy(buf, tmp, len);
    return len;
}

Alright, so the call back gets a char * named buf, an int named size, an int named rwflag, and a void* named u.

Looking at the example code I noticed something peculiar, for one, we get the pass phrase from the user, we then have to store that into buf... however buf can only be as long as the size that we were told it was (makes sense, buffer overflow and all) BUT that means that the users passphrase will be truncated if it doesn't happen to fit into the buffer that OpenSSL has allocated for us.

This buffer is set to PEM_BUFSIZE internally in OpenSSL, the default is a very generous 1024 . While I don't think anyone is going to be typing a passphrase longer than 1024 ... a passphrase could be longer than that.

That means using PEM_write_bio_PKCS8PrivateKey we could encrypt and create a PEM file that CAN NOT be decrypted and read back in using OpenSSL because PEM_read_bio_PrivateKey returns a buffer that is too small to hold the passphrase, UNLESS off course OpenSSL truncates the input provided in kstr.

It would also be possible to create a PEM certificate that is valid PKCS8 encoded with alternative libraries such as Botan and not be able to read it using OpenSSL. It makes no sense why you can encrypt to PKCS8 with a random string (possible containg null bytes) but can't decrypt that same file.


Do note that I haven't had the chance to sit down and test the truncation theory yet, for now I just needed to get my thoughts down on "paper". At the moment I am quite frustrated with OpenSSL's various API's.

To convert an EVP_PKEY* to PEM using PKCS you use the PEM_write_bio_PKCS8PrivateKey function, this "works" in that you can then load it back in using PEM_read_bio_PrivateKey without issues...

Now you go to implement the DER based equivelants (less storage space required, easier to send across the wire, etc, etc...) so you grab i2d_PKCS8PrivateKey_bio. This works, it writes binary data to the BIO. However now we need to load it back in, so we go for our trusty d2i_PKCS8PrivateKey_bio, except that as soon as we try to load a key we have just written to the BIO using that function we get the following error:

140735117963740:error:0D0680A8:asn1 encoding routines:ASN1_CHECK_TLEN:wrong tag:tasn_dec.c:1319:
140735117963740:error:0D07803A:asn1 encoding routines:ASN1_ITEM_EX_D2I:nested asn1 error:tasn_dec.c:381:Type=X509_ALGOR
140735117963740:error:0D08303A:asn1 encoding routines:ASN1_TEMPLATE_NOEXP_D2I:nested asn1 error:tasn_dec.c:751:Field=algor, Type=X509_SIG

So incredibly helpful that is ... why is it the wrong tag? More specifically, why can't we load something we just converted to DER back into OpenSSL's internal EVP_PKEY format?

Instead I switched to d2i_PrivateKey_bio and i2d_PrivateKey_bio, and those work without issues, but that means I lose out on the ability to encrypt the result into a valid PKCS8 DER format... Unlike PEM_write_bio_PrivateKey which still supports encryption although it is an older encryption format, see notes on pem(3), these functions don't support any encryption at all. This basically means that if you are using DER you are losing out on protection that is available for you so long as you use PEM format.


Fun side fact, the man page for d2i_PKCS8PrivateKey_bio has this to say in its description:

Other than the use of DER as opposed to PEM these functions are identical to the corresponding PEM function as described in the pem(3) manual page.

Oh, how I wish that was true ... the DER functions can't read in stuff generated with it's own counterpart!

This is a blog post that requires updating, but I might as well get this down here too. Using SSL_read() on a BIO_s_mem won't full the buffer size you have given it, even if technically there is enough data.

See http://funcptr.net/2012/04/08/openssl-as-a-filter-(or-non-blocking-openssl)/ for my blog post regarding OpenSSL as a filter, it reads data into a memory BIO and then reads from that using the standard SSL_read(), that way you can do SSL in process, between threads, over IPC, over ZeroMQ or a variety of other implementations. In my case I wanted to move SSL to a possible component in my stack that can be enabled/disabled at will as a sort of filter whereby OpenSSL doesn't take control over my file descriptor.

The problem is that when you receive an SSL encrypted message, it may actually come in one or more parts inside of the protocol. SSL_read() works on TLS/SSL records. However certain implementations (I'm looking at you Java 7) will send a single TLS message in a single TCP/IP packet that contains 2 or more records... and there is no way to find out how many records are available for processing. So reading the documentation for SSL_read() you come across SSL_pending() and you think to yourself "Bingo", we will call SSL_read(), and then call SSL_pending() to find out if there is more data to process.

SSL_pending(3) can be used to find out whether there are buffered bytes available for immediate retrieval. In this case SSL_read() can be called without blocking or actually receiving new data from the underlying socket.

However, go to the page for SSL_pending() and it tells a different story, under BUGS.

SSL_pending() takes into account only bytes from the TLS/SSL record that is currently being processed (if any).

Well there goes that possibility ... when you SSL_read() and reach the end of a record, you are done processing that record, so SSL_pending() will simply tell you that there is nothing left to read.

So now the only option left is the option I think sucks the most, you are required to loop on SSL_read().

So lets change:

// Read data for the application from the encrypted connection and place it in the string for the app to read
while (1) {
    char *readto = new char[1024];
    int read = SSL_read(ssl, readto, 1024);

    if (!continue_ssl_()) {
        delete readto;
        throw std::runtime_error("An SSL error occured.");
    }

    if (read > 0) {
        size_t cur_size = aread->length();
        aread->resize(cur_size + read);
        std::copy(readto, readto + read, aread->begin() + cur_size);
    }

    delete readto;

    if (static_cast<size_t>(read) != 1024 || read == 0) break;
}

This so that it does the right thing:

// Read data for the application from the encrypted connection and place it in the string for the app to read
while (1) {
    char *readto = new char[1024];
    int read = SSL_read(ssl, readto, 1024);

    if (!continue_ssl_()) {
        delete readto;
        throw std::runtime_error("An SSL error occured.");
    }

    if (read > 0) {
        size_t cur_size = aread->length();
        aread->resize(cur_size + read);
        std::copy(readto, readto + read, aread->begin() + cur_size);
        delete readto;
        continue;
    } else {
        delete readto;
        break;
    }
}

So now we will continue calling SSL_read() until it returns 0, at which point we break out of the loop because we know that it is done. SSL_pending() or another function should be able to return that there are still records pending for processing, so that we don't waste cycles calling into SSL_read() again when it is completely unnecessary, and in this case take the time to set up a new buffer to store the results into.

So beware that this may be an issue you could run into, and if it seems like SSL_read() is coming up short, it most likely is.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment