Search code examples
c++opensslprivate-keyelliptic-curve

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;
}

Solution

  • 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