3
votes

I'm trying to import ECDSA public key in C++ with OpenSSL (for verify signatures), but d2i_ECPKParameters return NULL.

The keys generated with Web Cryptographi API; public key exported in spki format (W3 TR doc talk about ASN.1 structure when exporting key, and DER encoding for spki).

I'm new to OpenSSL, what did I do wrong?

Import:

bool ecdsa_verify(
    const std::array<uint8_t, 20>& hash,
    const std::experimental::basic_string_view<uint8_t>& signature,
    const std::experimental::basic_string_view<uint8_t>& public_key) {
  EC_GROUP* ec_group = nullptr;

  const unsigned char* public_key_data = public_key.data();
  ec_group = d2i_ECPKParameters(nullptr, &public_key_data, public_key.length());
  if (ec_group == nullptr) {
    return false; // RETURN POINT
  }

  EC_KEY* ec_key = EC_KEY_new();
  if (ec_key == nullptr) {
    EC_GROUP_free(ec_group);
    return false;
  }

  if (!EC_KEY_set_group(ec_key, ec_group)) {
    EC_GROUP_free(ec_group);
    EC_KEY_free(ec_key);
    return false;
  }

  bool is_signature_valid =
      ECDSA_verify(0, hash.data(), hash.size(), signature.data(),
                   signature.length(), ec_key);

  EC_GROUP_free(ec_group);
  EC_KEY_free(ec_key);

  return is_signature_valid;
}

UPDATE: other import try (but still not work):

  const unsigned char* public_key_data = public_key.data();

  EC_KEY* ec_key =
      o2i_ECPublicKey(nullptr, &public_key_data, public_key.length());
  if (ec_key == nullptr) {
    return false; // RETURN POINT
  }

  bool is_signature_valid =
      ECDSA_verify(0, hash.data(), hash.size(), signature.data(),
                   signature.length(), ec_key);

  EC_KEY_free(ec_key);

Export:

function ecdsa_export_pub_key(key) {
  return window.crypto.subtle.exportKey(
    "spki",
    key);
}

UPDATE 2:

I generated the PEM keys (from the exported keys in JS), but primary, I don't use PEM keys. After exporting the public key in JavaScript, I creat a new Uint8Array from the ArrayBuffer, send it via WebSocket (binary frame) to the server, and try to parse it. The received uint8_t array always 158 byte length. I use P-521 -- secp521r1.

The private key exported in pkcs8!

-----BEGIN PRIVATE KEY-----
 MIHuAgEAMBAGByqGSM49AgEGBSuBBAAjBIHWMIHTAgEBBEIApK1m/qpIAZ1iENht
 XJxng4bdur6YV2SpMs+uFtSiJ/n96HbjVkqSENavv7vblIow+i5QUhaOkqSNWi0B
 7x695C6hgYkDgYYABAATsbs5B+ebSwoIXD6RD2NYONzSWOtt0SigPM27pdYEWpld
 /6j6S34gvRHQwDSMzs6//1zVE20Mn+izNM0KPWhRewD6SotR8/2QGWB5uo8GiXx1
 RLyBp+TOurQLEsYwiWSLkUIUMvPH/6WCxSNO4FzBf617PRqs7Zv3Vo98d9JH/3mI
 TA==
 -----END PRIVATE KEY-----

-----BEGIN PUBLIC KEY-----
 MIGbMBAGByqGSM49AgEGBSuBBAAjA4GGAAQAE7G7OQfnm0sKCFw+kQ9jWDjc0ljr
 bdEooDzNu6XWBFqZXf+o+kt+IL0R0MA0jM7Ov/9c1RNtDJ/oszTNCj1oUXsA+kqL
 UfP9kBlgebqPBol8dUS8gafkzrq0CxLGMIlki5FCFDLzx/+lgsUjTuBcwX+tez0a
 rO2b91aPfHfSR/95iEw=
 -----END PUBLIC KEY-----

Some detail:

% openssl asn1parse -inform PEM -in pub.pem 
    0:d=0  hl=3 l= 155 cons: SEQUENCE          
    3:d=1  hl=2 l=  16 cons: SEQUENCE          
    5:d=2  hl=2 l=   7 prim: OBJECT            :id-ecPublicKey
   14:d=2  hl=2 l=   5 prim: OBJECT            :secp521r1
   21:d=1  hl=3 l= 134 prim: BIT STRING        
% openssl asn1parse -inform PEM -in priv.pem
    0:d=0  hl=3 l= 238 cons: SEQUENCE          
    3:d=1  hl=2 l=   1 prim: INTEGER           :00
    6:d=1  hl=2 l=  16 cons: SEQUENCE          
    8:d=2  hl=2 l=   7 prim: OBJECT            :id-ecPublicKey
   17:d=2  hl=2 l=   5 prim: OBJECT            :secp521r1
   24:d=1  hl=3 l= 214 prim: OCTET STRING      [HEX DUMP]:3081D3020101044200A4AD66FEAA48019D6210D86D5C9C678386DDBABE985764A932CFAE16D4A227F9FDE876E3564A9210D6AFBFBBDB948A30FA2E5052168E92A48D5A2D01EF1EBDE42EA1818903818600040013B1BB3907E79B4B0A085C3E910F635838DCD258EB6DD128A03CCDBBA5D6045A995DFFA8FA4B7E20BD11D0C0348CCECEBFFF5CD5136D0C9FE8B334CD0A3D68517B00FA4A8B51F3FD90196079BA8F06897C7544BC81A7E4CEBAB40B12C63089648B91421432F3C7FFA582C5234EE05CC17FAD7B3D1AACED9BF7568F7C77D247FF79884C

Error codes when call o2i_ECPublicKey:

(Call with the same data, but error is different each time -- which is repeated anyway.)

error:10067066:elliptic curve routines:ec_GFp_simple_oct2point:invalid
error:10098010:elliptic curve routines:o2i_ECPublicKey:EC lib
error:0D07207B:asn1 encoding routines:ASN1_get_object:header too long
error:10067066:elliptic curve routines:ec_GFp_simple_oct2point:invalid
error:10098010:elliptic curve routines:o2i_ECPublicKey:EC lib

Update 3:

From C++, i wrote out the received data (key) into a file:

% % openssl asn1parse -inform DER -in data.bin 
    0:d=0  hl=3 l= 155 cons: SEQUENCE          
    3:d=1  hl=2 l=  16 cons: SEQUENCE          
    5:d=2  hl=2 l=   7 prim: OBJECT            :id-ecPublicKey
   14:d=2  hl=2 l=   5 prim: OBJECT            :secp521r1
   21:d=1  hl=3 l= 134 prim: BIT STRING
% 
% hexdump data.bin                          
0000000 8130 309b 0610 2a07 4886 3dce 0102 0506
0000010 812b 0004 0323 8681 0400 1300 bbb1 0739
0000020 9be7 0a4b 5c08 913e 630f 3858 d2dc eb58
0000030 d16d a028 cd3c a5bb 04d6 995a ff5d faa8
0000040 7e4b bd20 d011 34c0 ce8c bfce 5cff 13d5
0000050 0c6d e89f 34b3 0acd 683d 7b51 fa00 8b4a
0000060 f351 90fd 6019 ba79 068f 7c89 4475 81bc
0000070 e4a7 bace 0bb4 c612 8930 8b64 4291 3214
0000080 c7f3 a5ff c582 4e23 5ce0 7fc1 7bad 1a3d
0000090 edac f79b 8f56 777c 47d2 79ff 4c88     
000009

Hex encoded exported SPKI (the result of the WebCrypto export):

Private:

MIHuAgEAMBAGByqGSM49AgEGBSuBBAAjBIHWMIHTAgEBBEIApK1m/qpIAZ1iENhtXJxng4bdur6YV2SpMs+uFtSiJ/n96HbjVkqSENavv7vblIow+i5QUhaOkqSNWi0B7x695C6hgYkDgYYABAATsbs5B+ebSwoIXD6RD2NYONzSWOtt0SigPM27pdYEWpld/6j6S34gvRHQwDSMzs6//1zVE20Mn+izNM0KPWhRewD6SotR8/2QGWB5uo8GiXx1RLyBp+TOurQLEsYwiWSLkUIUMvPH/6WCxSNO4FzBf617PRqs7Zv3Vo98d9JH/3mITA==

Public:

MIGbMBAGByqGSM49AgEGBSuBBAAjA4GGAAQAE7G7OQfnm0sKCFw+kQ9jWDjc0ljrbdEooDzNu6XWBFqZXf+o+kt+IL0R0MA0jM7Ov/9c1RNtDJ/oszTNCj1oUXsA+kqLUfP9kBlgebqPBol8dUS8gafkzrq0CxLGMIlki5FCFDLzx/+lgsUjTuBcwX+tez0arO2b91aPfHfSR/95iEw=

Update 4:

Private key jwk format:

{
  "crv":"P-521",
  "d":"AKStZv6qSAGdYhDYbVycZ4OG3bq-mFdkqTLPrhbUoif5_eh241ZKkhDWr7-725SKMPouUFIWjpKkjVotAe8eveQu",
  "ext":true,
  "key_ops":["sign"],
  "kty":"EC",
  "x":"ABOxuzkH55tLCghcPpEPY1g43NJY623RKKA8zbul1gRamV3_qPpLfiC9EdDANIzOzr__XNUTbQyf6LM0zQo9aFF7",
  "y":"APpKi1Hz_ZAZYHm6jwaJfHVEvIGn5M66tAsSxjCJZIuRQhQy88f_pYLFI07gXMF_rXs9Gqztm_dWj3x30kf_eYhM"
}
2
Please create a test key, and then post it in some encoding (hex, base64, pem) so we can hack on it.jww
You added pem encoded keys, but are they the keys you are having problems with (it sounds like the answer is NO)? If not, then what presentation format are they in? If its ASN/1's DER, then just hex encode them and post them. We can always strip what we don't need (like a hex encoding). But we can't create the keys we don't have.jww
@jww I add them too.bsz
I tried to decode the private key you provided. You can use, for example (on OS X): pbpaste | openssl ec ... -text -noout and/or pbpaste | openssl pkcs8 ... -text -noout. None of them can decode it. I'm guessing the key is malformed. You can take the error code you get, and use openssl errstr to decode it. For example, I keep getting error:0906D066:PEM routines:PEM_read_bio:bad end line for openssl errstr 0x0906D066.jww
But WebCrypto import these datas succesfully.bsz

2 Answers

1
votes

The functions d2i_ECPublicKey() and i2d_ECPublicKey() or equivalents do not seem to be implemented in OpenSSL. Based on the ASN.1 definitions pointed to by user jww, you could choose to define them yourself, like this:

#include <openssl/asn1t.h>

/* C-struct definitions */

typedef struct ec_identifiers_st {
    ASN1_OBJECT  *algorithm;
    ASN1_OBJECT  *namedCurve;
} EC_IDENTIFIERS;

typedef struct ec_publickey_st {
    EC_IDENTIFIERS  *identifiers;
    ASN1_BIT_STRING *publicKey;
} EC_PUBLICKEY;


/* ASN.1 definitions */

ASN1_SEQUENCE(EC_IDENTIFIERS) = {
    ASN1_SIMPLE(EC_IDENTIFIERS, algorithm, ASN1_OBJECT),
    ASN1_SIMPLE(EC_IDENTIFIERS, namedCurve, ASN1_OBJECT)
} ASN1_SEQUENCE_END(EC_IDENTIFIERS)

DECLARE_ASN1_ALLOC_FUNCTIONS(EC_IDENTIFIERS)
IMPLEMENT_ASN1_ALLOC_FUNCTIONS(EC_IDENTIFIERS)

ASN1_SEQUENCE(EC_PUBLICKEY) = {
    ASN1_SIMPLE(EC_PUBLICKEY, identifiers, EC_IDENTIFIERS),
    ASN1_SIMPLE(EC_PUBLICKEY, publicKey, ASN1_BIT_STRING)
} ASN1_SEQUENCE_END(EC_PUBLICKEY)

DECLARE_ASN1_FUNCTIONS_const(EC_PUBLICKEY)
DECLARE_ASN1_ENCODE_FUNCTIONS_const(EC_PUBLICKEY, EC_PUBLICKEY)
IMPLEMENT_ASN1_FUNCTIONS_const(EC_PUBLICKEY)

Note: this is not a complete definition. It assumes that the key contains the named curve, not the curve parameters (again see jww's answer as well). It should work with your example but if you want it to work with all possible EC keys, you should include a CHOICE field -- nice exercise :-)

From here on, you can use the functions d2i_EC_PUBLICKEY() and i2d_EC_PUBLICKEY() to do the back and forth transformations. Here is an example without any error checking:

#include <openssl/objects.h>
#include <openssl/evp.h>
#include <openssl/ec.h>

void ASN1ECPublicKeyTester(void)
{
    EC_PUBLICKEY *parsedKey = NULL;
    EC_KEY *ecKey = NULL;
    const unsigned char *helper = NULL;
    char buffer[100] = { 0 };
    int nid = -1;
    EVP_PKEY *evpKey;

#   define COUNT(_Array) (sizeof(_Array) / sizeof(_Array[0]))
    helper = testKey;
    parsedKey = d2i_EC_PUBLICKEY(NULL, &helper, COUNT(testKey));
    OBJ_obj2txt(buffer, COUNT(buffer), parsedKey->identifiers->algorithm, 0);
    printf("Algorithm: \"%s\"\n", buffer);
    OBJ_obj2txt(buffer, COUNT(buffer), parsedKey->identifiers->namedCurve, 0);
    printf("Curve:     \"%s\"\n", buffer);

    /* o2i_ECPublicKey needs to be fed an EC_KEY that has the GROUP set */
    nid = OBJ_obj2nid(parsedKey->identifiers->namedCurve);
    ecKey = EC_KEY_new_by_curve_name(nid);
    helper = parsedKey->publicKey->data;
    o2i_ECPublicKey(&ecKey, &helper, parsedKey->publicKey->length);

    /* Create EVP key for use with EVP API */
    evpKey = EVP_PKEY_new();
    if (1 == EVP_PKEY_set1_EC_KEY(evpKey, ecKey)) {
        printf("It looks like everything worked\n");
        /* EVP_PKEY now owns the key */
        EC_KEY_free(ecKey);
    };
    /* Use your evpKey from here (and free afterwards) */
}

With the tester key that you provided, the output is

Algorithm: "id-ecPublicKey"
Curve:     "secp521r1"
It looks like everything worked

The testKey array was defined as follows, based on your key:

const unsigned char testKey[] = {
    0x30, 0x81, 0x9B, 0x30, 0x10, 0x06, 0x07, 0x2A,
    0x86, 0x48, 0xCE, 0x3D, 0x02, 0x01, 0x06, 0x05,
    0x2B, 0x81, 0x04, 0x00, 0x23, 0x03, 0x81, 0x86,
    0x00, 0x04, 0x00, 0x13, 0xB1, 0xBB, 0x39, 0x07,
    0xE7, 0x9B, 0x4B, 0x0A, 0x08, 0x5C, 0x3E, 0x91,
    0x0F, 0x63, 0x58, 0x38, 0xDC, 0xD2, 0x58, 0xEB,
    0x6D, 0xD1, 0x28, 0xA0, 0x3C, 0xCD, 0xBB, 0xA5,
    0xD6, 0x04, 0x5A, 0x99, 0x5D, 0xFF, 0xA8, 0xFA,
    0x4B, 0x7E, 0x20, 0xBD, 0x11, 0xD0, 0xC0, 0x34,
    0x8C, 0xCE, 0xCE, 0xBF, 0xFF, 0x5C, 0xD5, 0x13,
    0x6D, 0x0C, 0x9F, 0xE8, 0xB3, 0x34, 0xCD, 0x0A,
    0x3D, 0x68, 0x51, 0x7B, 0x00, 0xFA, 0x4A, 0x8B,
    0x51, 0xF3, 0xFD, 0x90, 0x19, 0x60, 0x79, 0xBA, 
    0x8F, 0x06, 0x89, 0x7C, 0x75, 0x44, 0xBC, 0x81,
    0xA7, 0xE4, 0xCE, 0xBA, 0xB4, 0x0B, 0x12, 0xC6,
    0x30, 0x89, 0x64, 0x8B, 0x91, 0x42, 0x14, 0x32,
    0xF3, 0xC7, 0xFF, 0xA5, 0x82, 0xC5, 0x23, 0x4E,
    0xE0, 0x5C, 0xC1, 0x7F, 0xAD, 0x7B, 0x3D, 0x1A,
    0xAC, 0xED, 0x9B, 0xF7, 0x56, 0x8F, 0x7C, 0x77,
    0xD2, 0x47, 0xFF, 0x79, 0x88, 0x4C
};

PS: A nice tool to analyse your ASN.1 data is this free ASN.1 Editor. When using that, your test key looks like this: ASN.1 Editor

3
votes

... the public key exported is in spki format ...

The encoded key is roughly structured as follows using ASN.1. See RFC 5480, Section 2. Subject Public Key Information Fields and RFC 3279, Section 2.3.5 ECDSA and ECDH Keys for the grueling details.

SEQUENCE {
   ALGORITHM ID
   KEY {
      NAMED_CURVE or DOMAIN_PARAMETERS
      PUBLIC_KEY or PRIVATE_KEY  
   }
}

There is also a "raw key", which is the key material without the outer sequence and algorithm identifier. OpenSSL calls the "raw key" a Traditional Key in its man pages.


d2i_ECPKParameters return NULL...

OK, you have a SPKI, and not just domain parameters. Domain parameters are the curve coefficients (a and b), the modulus (p), the base point (G), etc, and they describe the curve. They don't have the key.

So you should use something like d2i_PublicKey to parse the key into a EVP_KEY, and then fetch the domain parameters after loading the key.


The biggest problem with interop I have seen is this:

NAMED_CURVE or DOMAIN_PARAMETERS

If its a named curve, then it will be something like secp256 or prime256v1. If its domain parameters, then the named curve is "unwound" or "fully expanded", and it will be the curve coefficients (a and b), the modulus (p), the base point (G), etc. Though they specify the exact same thing, they cause a lot of trouble in practice.

Interop'ing between named curves and domain parameters cause so much trouble that OpenSSL has a wiki page on it: Elliptic Curve Cryptography | Named Curve. In fact, OpenSSL web servers cannot run properly with itself because of the ridiculousness!

So the only thing I can say here is: be cognizant of what have, and don't expect named curves and domain parameters to be equal in software even though they are exactly the same (except for a presentation detail).


If you provide some test keys we can probably provide more details. My guess is you have a PEM encoded key, so you should be using other functions like PEM_read_PUBKEY. But its only a guess.