The Quantum Physicist
The Quantum Physicist

Reputation: 26356

OpenSSL: Read an EC-key, then write it again, and it's different

I wrote a wrapper for OpenSSL that supports ECC. I'm trying to read a private key that I generated with

openssl ecparam -name secp384r1 -genkey -noout -out privkey.pem

And compare it with what OpenSSL would produce after reading the key into a EVP_PKEY and EC_KEY and printing it again into a string. The results after reading are not the same.

In short:

  1. Read a key
  2. Save it to EVP_PKEY
  3. Write it again

And the results don't match. My program is quite big, so I produced an MCVE that demonstrates the problem.

My suspicion is that the problem happens because I'm reading to an EC_KEY, then writing from EVP_PKEY, which is generic. I'm guessing here because the input says it's EC, but the output doesn't say that. I'm not sure how to resolve this, because I don't see a way to write directly from an EC_KEY to a file (bio object). Is my assessment correct?

Please advise.


EDIT: I was asked to put the whole code here in the comments, so there you go:

#include <iostream>

#include <openssl/bio.h>
#include <openssl/err.h>
#include <openssl/ec.h>
#include <openssl/pem.h>

EC_KEY* ecKey = nullptr;
EVP_PKEY* pkey = nullptr;

void setPrivateKeyFromPEM(const std::string& pemkey)
{
    pkey = EVP_PKEY_new();

    BIO* bio = BIO_new(BIO_s_mem());

    int bio_write_ret = BIO_write(
        bio, static_cast<const char*>(pemkey.c_str()), pemkey.size());
    if (bio_write_ret <= 0) {
        throw std::runtime_error("error1");
    }

    if (!PEM_read_bio_PrivateKey(bio, &pkey, NULL, NULL)) {
        throw std::runtime_error("error1.5");
    }

    EC_KEY* eckey_local = EVP_PKEY_get1_EC_KEY(pkey);

    if (!eckey_local) {
        throw std::runtime_error("error2");
    } else {
        ecKey = eckey_local;
        EC_KEY_set_asn1_flag(ecKey, OPENSSL_EC_NAMED_CURVE);
    }
}

std::string getPrivateKeyAsPEM()
{
    if (!pkey) {
        throw std::runtime_error("error3");
    }

    BIO* outbio = BIO_new(BIO_s_mem());

    if (!PEM_write_bio_PrivateKey(outbio, pkey, NULL, NULL, 0, 0,
                                  NULL)) {
        throw std::runtime_error("error4");
    }

    std::string keyStr;
    int         priKeyLen = BIO_pending(outbio);
    keyStr.resize(priKeyLen);
    BIO_read(outbio, (void*)&(keyStr.front()), priKeyLen);
    return keyStr;
}

int main()
{
    std::string expectedPrivKey =
        "-----BEGIN EC PRIVATE KEY-----\n"
        "MIGkAgEBBDBNK0jwKqqf8zkM+Z2l++9r8bzdTS/XCoB4N1J07dPxpByyJyGbhvIy\n"
        "1kLvY2gIvlmgBwYFK4EEACKhZANiAAQvPxAK2RhvH/k5inDa9oMxUZPvvb9fq8G3\n"
        "9dKW1tS+ywhejnKeu/48HXAXgx2g6qMJjEPpcTy/DaYm12r3GTaRzOBQmxSItStk\n"
        "lpQg5vf23Fc9fFrQ9AnQKrb1dgTkoxQ=\n"
        "-----END EC PRIVATE KEY-----\n";

    setPrivateKeyFromPEM(expectedPrivKey);
    // compare priv key
    {
        std::string privKeyRead = getPrivateKeyAsPEM();
        std::cout << privKeyRead << std::endl;
        std::cout<<expectedPrivKey<<std::endl;
    }

    return 0;
}

Upvotes: 5

Views: 7572

Answers (2)

rustyx
rustyx

Reputation: 85521

PEM_write_bio_ECPrivateKey is available in OpenSSL 1.0.2 too, only the documentation is missing.

The stored key is the same, the difference is only in encoding.

The tag -----BEGIN PRIVATE KEY----- signifies a PEM-encoded ASN.1 format.

The tag -----BEGIN EC PRIVATE KEY----- signifies a PEM-encoded ANSI X9.62 key.

Compare: key 1 vs. key 2. Notice key2 doesn't contain the key type OID, the key itself is identical.

To write the EC key format, just use this:

    if (!PEM_write_bio_ECPrivateKey(outbio, ecKey, NULL, NULL, 0, 0, NULL)) {

coliru-demo

Upvotes: 5

dbush
dbush

Reputation: 225007

Although the outputs don't match exactly, they actually represent the same key.

Your code outputs the following:

-----BEGIN PRIVATE KEY-----
MIG2AgEAMBAGByqGSM49AgEGBSuBBAAiBIGeMIGbAgEBBDBNK0jwKqqf8zkM+Z2l
++9r8bzdTS/XCoB4N1J07dPxpByyJyGbhvIy1kLvY2gIvlmhZANiAAQvPxAK2Rhv
H/k5inDa9oMxUZPvvb9fq8G39dKW1tS+ywhejnKeu/48HXAXgx2g6qMJjEPpcTy/
DaYm12r3GTaRzOBQmxSItStklpQg5vf23Fc9fFrQ9AnQKrb1dgTkoxQ=
-----END PRIVATE KEY-----

-----BEGIN EC PRIVATE KEY-----
MIGkAgEBBDBNK0jwKqqf8zkM+Z2l++9r8bzdTS/XCoB4N1J07dPxpByyJyGbhvIy
1kLvY2gIvlmgBwYFK4EEACKhZANiAAQvPxAK2RhvH/k5inDa9oMxUZPvvb9fq8G3
9dKW1tS+ywhejnKeu/48HXAXgx2g6qMJjEPpcTy/DaYm12r3GTaRzOBQmxSItStk
lpQg5vf23Fc9fFrQ9AnQKrb1dgTkoxQ=
-----END EC PRIVATE KEY-----

If you look closely at the first key printed, which is the one imported/exported by your code, it is labeled "BEGIN PRIVATE KEY" instead of "BEGIN EC PRIVATE KEY". It also starts with MIG2AgEAMBAGByqGSM49AgEGBSuBBAAiBIGe but after that it is identical to the original key. In fact, if you put both PEMs in files and run openssl ec -in {filename} -text, both will output the same thing:

read EC key
Private-Key: (384 bit)
priv:
    4d:2b:48:f0:2a:aa:9f:f3:39:0c:f9:9d:a5:fb:ef:
    6b:f1:bc:dd:4d:2f:d7:0a:80:78:37:52:74:ed:d3:
    f1:a4:1c:b2:27:21:9b:86:f2:32:d6:42:ef:63:68:
    08:be:59
pub: 
    04:2f:3f:10:0a:d9:18:6f:1f:f9:39:8a:70:da:f6:
    83:31:51:93:ef:bd:bf:5f:ab:c1:b7:f5:d2:96:d6:
    d4:be:cb:08:5e:8e:72:9e:bb:fe:3c:1d:70:17:83:
    1d:a0:ea:a3:09:8c:43:e9:71:3c:bf:0d:a6:26:d7:
    6a:f7:19:36:91:cc:e0:50:9b:14:88:b5:2b:64:96:
    94:20:e6:f7:f6:dc:57:3d:7c:5a:d0:f4:09:d0:2a:
    b6:f5:76:04:e4:a3:14
ASN1 OID: secp384r1
NIST CURVE: P-384
writing EC key
-----BEGIN EC PRIVATE KEY-----
MIGkAgEBBDBNK0jwKqqf8zkM+Z2l++9r8bzdTS/XCoB4N1J07dPxpByyJyGbhvIy
1kLvY2gIvlmgBwYFK4EEACKhZANiAAQvPxAK2RhvH/k5inDa9oMxUZPvvb9fq8G3
9dKW1tS+ywhejnKeu/48HXAXgx2g6qMJjEPpcTy/DaYm12r3GTaRzOBQmxSItStk
lpQg5vf23Fc9fFrQ9AnQKrb1dgTkoxQ=
-----END EC PRIVATE KEY-----

The extra part at the beginning is metadata from exporting a EVP_PKEY key stating that the key is an EC key.

Had you instead exported the EC_KEY directly using PEM_write_bio_ECPrivateKey(outbio, ecKey, NULL, NULL, 0, 0, NULL) the output would be exactly the same.

Upvotes: 3

Related Questions