copensslelliptic-curvediffie-hellman

elliptic curve routines:o2i_ECPublicKey:passed a null parameter:ec_asn1.c:1271:


I'm creating ECDH using openssl EVP method that is explained by OpenSSL Elliptic Curve Diffie-Hellman

Everything works expect where I get the other peer's public key and generate EVP_PKEY* based on it.

In the given link, it ignores implementation details of decoding other peer's public key and has used get_peerkey pseudo function:

/* Get the peer's public key, and provide the peer with our public key -
 * how this is done will be specific to your circumstances */
peerkey = get_peerkey(pkey);

In my implementation the other peer's public key is received and stored in publickey2 and its size is stored in pub_len2

size_t pub_len2 = 0;
const unsigned char *publickey2 = get_public_key(&pub_len2);

then I try to create EVP_PKEY* using following code:

EVP_PKEY *pkey3=NULL;

pkey3 = d2i_PublicKey(EVP_PKEY_EC, &pkey3, (const unsigned char **)&publickey2, pub_len2);

if(pkey3 == NULL) {

    ERR_print_errors_fp(stderr);
}

but pkey3 is always null and gives me following error!

139898837907104:error:10098043:elliptic curve routines:o2i_ECPublicKey:passed a null parameter:ec_asn1.c:1389:
139898837907104:error:0D09B00D:asn1 encoding routines:d2i_PublicKey:ASN1 lib:d2i_pu.c:123:

passed a null parameter error.

Do you guys have any idea?

Edit:

I've managed to solve this by using pkey3 = d2i_PUBKEY(NULL, (const unsigned char **)&publickey2, pub_len2);

But I faced another problems, The main problem is that calculated derived secret from these public keys is different in both peers!

Here is my secret derivation procedure which is the same in both peers and exactly copy and pasted (with some modification) from provided link, I made some comments for the modifications reason:

unsigned char *derive_secret(EVP_PKEY *pkey, 
const unsigned char *peer_key, 
size_t peerkey_len, size_t *secret_len)
{
    EVP_PKEY_CTX *ctx;
    unsigned char *secret;
    //MY modification to get peer key of peer_key buffer.
    //peer_key is created by i2d_PUBKEY at peer side and 
    //received by network and is passed here
    EVP_PKEY *peerkey = d2i_PUBKEY(NULL, &peer_key, peerkey_len);
    
    //I also set the group of newly created EC
    EC_KEY_set_group(EVP_PKEY_get1_EC_KEY(peer_key),
        EC_GROUP_new_by_curve_name(NID_X9_62_prime256v1));
    
    //I also set the CONVERSION format to make sure!
    EC_KEY_set_conv_form(EVP_PKEY_get1_EC_KEY(peer_key), POINT_CONVERSION_COMPRESSED);

    /* Create the context for the shared secret derivation */
    if(NULL == (ctx = EVP_PKEY_CTX_new(pkey, NULL))) handleErrors();

    /* Initialise */
    if(1 != EVP_PKEY_derive_init(ctx)) handleErrors();

    /* Provide the peer public key */
    if(1 != EVP_PKEY_derive_set_peer(ctx, peerkey)) handleErrors();

    /* Determine buffer length for shared secret */
    if(1 != EVP_PKEY_derive(ctx, NULL, secret_len)) handleErrors();

    /* Create the buffer */
    if(NULL == (secret = OPENSSL_malloc(*secret_len))) handleErrors();

    /* Derive the shared secret */
    if(1 != (EVP_PKEY_derive(ctx, secret, secret_len))) handleErrors();

    EVP_PKEY_CTX_free(ctx);
    EVP_PKEY_free(peerkey);
    EVP_PKEY_free(pkey);

    /* Never use a derived secret directly. Typically it is passed
     * through some hash function to produce a key */
    return secret;
}

I also printed out sent and received public keys on both side and made sure public keys are being received in one piece and exactly the same as they created and sent!

There is also some minor problem an that is when I figured that a long bytes of public keys are the same for both peers! Is that normal for a public key of EC Diffie-Hellman?

Here are the public keys shown in HEX format

Peer1:
3082010A3081E306072A8648CE3D02013081D7020101302C06072A8648CE3D0101022100FFFFFFFF00000001000000000000000000000000FFFFFFFFFFFFFFFFFFFFFFFF305B0420FFFFFFFF00000001000000000000000000000000FFFFFFFFFFFFFFFFFFFFFFFC04205AC635D8AA3A93E7B3EBBD55769886BC651D06B0CC53B0F63BCE3C3E27D2604B031500C49D360886E704936A6678E1139D26B7819F7E900421036B17D1F2E12C4247F8BCE6E563A440F277037D812DEB33A0F4A13945D898C296022100FFFFFFFF00000000FFFFFFFFFFFFFFFFBCE6FAADA7179E84F3B9CAC2FC632551020101032200026B0E07FE6177D23B0E6B776CF4CB0569735159D3261767FA5FC0A4636EF310C4

Peer2:

3082010A3081E306072A8648CE3D02013081D7020101302C06072A8648CE3D0101022100FFFFFFFF00000001000000000000000000000000FFFFFFFFFFFFFFFFFFFFFFFF305B0420FFFFFFFF00000001000000000000000000000000FFFFFFFFFFFFFFFFFFFFFFFC04205AC635D8AA3A93E7B3EBBD55769886BC651D06B0CC53B0F63BCE3C3E27D2604B031500C49D360886E704936A6678E1139D26B7819F7E900421036B17D1F2E12C4247F8BCE6E563A440F277037D812DEB33A0F4A13945D898C296022100FFFFFFFF00000000FFFFFFFFFFFFFFFFBCE6FAADA7179E84F3B9CAC2FC6325510201010322000208AE9F32ECE16072428A5FC875A19B3913C4516419917E723AA4C2DC20105C0A

Solution

  • some bytes (prefix) the same: all public keys in X.509-format (more exactly, SubjectPublicKeyInfo) have at least an 'object identifier' (OID), which is the same for all keys of a given algorithm (like EC), and EC keys also have 'parameters' specifying a group/curve, which is the same for all keys on the same curve -- and keys for an ECDH agreement must be on the same curve. This identical data, combined with the fact that the actual pubkeys have different values but same size, results in the ASN.1 DER encodings starting with the same bytes.

    The pubkey encodings you posted use the mostly-obsolete explicit curve specification, see rfc3279 sec 2.3.5 (equivalent to X9.62 or SEC1) which is much longer than the now-preferred and often required 'named' specification. At a guess, you used OpenSSL library below 1.1.0 to generate these keys and didn't set asn1_flag in the EC_GROUP object (or group subobject of EC_KEY) before serializing (aka encoding) with i2d (or PEM_write). The wiki page you referenced sort of covers this in section 3 "ECDH and Named Curves" although it only mentions private keys when this also applies to public keys and certificates -- but public key and (then) certificate are derived from private key, so setting asn1_flag on private key is sufficient. And it doesn't say that 'named' is now the default in 1.1.0 and no longer needs to be explicitly set.

    your newly-posted code: point_format is meaningful only when serializing (i2d or PEM_write) so setting it on a deserialized key that will only be used and free'd (not reserialized) is useless. OTOH setting the EC group to its existing value (which was set from deserialization) is useless, but setting it to any other value will cause chaos. EC public keys are points on a specific curve and different curves have entirely different points -- a value that is a point on one curve is not a point on another curve. Also, using a get1 function and not free'ing the result leaks memory.

    derivation result different: that's quite wrong for (EC)DH, and I can't reproduce it. Below is your deriviation code with the few fixes indicated above and some tiny changes to match my coding style, plus the generation code from the wiki and a trivial main to drive them, and when I run this I get pubkeys with a common prefix (shorter due to using named form) but as expected the same derivation result:

    $ cat SO48130343.c 
    /* SO48130343 */
    #include <stdio.h>
    #include <openssl/opensslv.h>
    #include <openssl/evp.h>
    #include <openssl/x509.h>
    #include <openssl/ec.h>
    #include <openssl/err.h>
    
    void hex (unsigned char *p, size_t n){ while(n--) printf("%02x", *p++); }
    
    void err (const char * msg){ fprintf(stderr, "%s:\n", msg); ERR_print_errors_fp(stderr); exit(1); }
    
    EVP_PKEY * gen (void) {
      EVP_PKEY_CTX *pctx, *kctx;
      EVP_PKEY *params = NULL, *pkey = NULL;
      if( NULL == (pctx = EVP_PKEY_CTX_new_id(EVP_PKEY_EC, NULL)) ) err("CTX1_new");
      if( 1 != EVP_PKEY_paramgen_init(pctx) ) err("pg_init");
      if( 1 != EVP_PKEY_CTX_set_ec_paramgen_curve_nid(pctx, NID_X9_62_prime256v1) ) err("pg_curve");
      if( 1 != EVP_PKEY_paramgen(pctx, &params) ) err("pg");
      if( NULL == (kctx = EVP_PKEY_CTX_new(params, NULL)) ) err("CTX2_new");
      if( 1 != EVP_PKEY_keygen_init(kctx) ) err("kg_init");
      if( 1 != EVP_PKEY_keygen(kctx, &pkey) ) err("kg");
    #if OPENSSL_VERSION_NUMBER < 0x1010000fL
      EC_KEY_set_asn1_flag (pkey->pkey.ec, OPENSSL_EC_NAMED_CURVE);
      /* point format needed before 'sending' and this is convenient */
      EC_KEY_set_conv_form (pkey->pkey.ec, POINT_CONVERSION_COMPRESSED);
    #else
      /* asn1_flag now default but point format still needed */
      EC_KEY_set_conv_form (EVP_PKEY_get0_EC_KEY (pkey), POINT_CONVERSION_COMPRESSED);
    #endif
      EVP_PKEY_CTX_free(pctx);
      EVP_PKEY_CTX_free(kctx);
      EVP_PKEY_free(params);
      return pkey;
    }
    unsigned char * derive (EVP_PKEY * self,
        const unsigned char * peer_ptr, size_t peer_len, size_t *len_ptr){
      EVP_PKEY * peer = d2i_PUBKEY (NULL, &peer_ptr, peer_len);
      /* DON'T change EC_GROUP; point_format not needed on 'receive' */
    
      EVP_PKEY_CTX *ctx; unsigned char * buf_ptr;
      if( !(ctx = EVP_PKEY_CTX_new (self, NULL)) ) err("CTX_new");
      if( 1 != EVP_PKEY_derive_init(ctx) ) err("derive_init");
      if( 1 != EVP_PKEY_derive_set_peer(ctx, peer) ) err("derive_peer");
      if( 1 != EVP_PKEY_derive (ctx, NULL, len_ptr) ) err("derive1");
      if( !(buf_ptr = OPENSSL_malloc (*len_ptr)) ) err("malloc");
      if( 1 != EVP_PKEY_derive (ctx, buf_ptr, len_ptr) ) err("derive2");
      EVP_PKEY_CTX_free(ctx);
      EVP_PKEY_free(peer);
      return buf_ptr;
    }
    
    int main (void){
      EVP_PKEY * pkey1 = gen(), * pkey2 = gen();
      unsigned char pub1 [100], pub2 [100], *ptr1 = &pub1[0], *ptr2 = &pub2[0];
      size_t publen1 = i2d_PUBKEY (pkey1, &ptr1), publen2 = i2d_PUBKEY (pkey2, &ptr2);
      printf ("pub1="); hex(pub1, publen1); putchar('\n');
      printf ("pub2="); hex(pub2, publen2); putchar('\n');
    
      size_t len1, len2;
      unsigned char * out1 = derive (pkey1, pub2, publen2, &len1);
      unsigned char * out2 = derive (pkey2, pub1, publen1, &len2);
      printf ("prv1/pub2="); hex(out1, len1); putchar('\n');
      printf ("prv2/pub1="); hex(out2, len2); putchar('\n');
      /* don't bother freeing for Q&D test code */
      return 0;
    }
    $ gcc [details for my system redacted]
    $ ./SO48130343.exe 
    pub1=3039301306072a8648ce3d020106082a8648ce3d03010703220003302c6f990445ddd27b2c0ecd3a0cd33109eec44dea0edd538c6bfc98796885e3
    pub2=3039301306072a8648ce3d020106082a8648ce3d0301070322000311940ba32c0b4d71f8785a884f7ea74cebed17e841e93a0fb1ccbeac32b2eb3b
    prv1/pub2=84b7a84249f1e88741a751a05d34a43e4cb131e012181967e4f465c1f4bf3b35
    prv2/pub1=84b7a84249f1e88741a751a05d34a43e4cb131e012181967e4f465c1f4bf3b35