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.