如何在OpenSSL的EVP_PKEY结构中访问原始的ECDH公钥、私钥和参数?

25

我正在使用OpenSSL的c库生成椭圆曲线Diffie-Hellman(ECDH)密钥对,按照此处的第一个代码示例 here。它在实际公钥交换方面稍作掩盖:

peerkey = get_peerkey(pkey);

pkey变量和返回值都是EVP *类型。 pkey包含之前生成的公钥、私钥和参数,而返回值仅包含对等方的公钥。因此有三个问题:

  1. 如何从pkey中提取仅用于发送到对等方的公钥?
  2. 该代码如何从pKey中提取私钥和参数以便在密钥交换后存储它们?
  3. 如何通过对等方的原始公钥生成一个新的EVP_PKEY结构?

我看到了OpenSSL函数EVP_PKEY_print_public()EVP_PKEY_print_private()EVP_PKEY_print_params(),但这些函数用于生成人类可读输出。我没有找到任何将人类可读的公钥转换回EVP_PKEY结构的等效方法。

3个回答

51
为了回答自己的问题,私钥和公钥有不同的路径。
为序列化公钥: 1. 将EVP_PKEY传递给EVP_PKEY_get1_EC_KEY()以获取EC_KEY。 2. 将EC_KEY传递给EC_KEY_get0_public_key()以获取EC_POINT。 3. 将EC_POINT传递给EC_POINT_point2oct()以获取八进制数,即无符号字符 *。
为反序列化公钥: 1. 将八进制数传递给EC_POINT_oct2point()以获取EC_POINT。 2. 将EC_POINT传递给EC_KEY_set_public_key()以获取EC_KEY。 3. 将EC_KEY传递给EVP_PKEY_set1_EC_KEY以获取EVP_KEY。
为序列化私钥: 1. 将EVP_PKEY传递给EVP_PKEY_get1_EC_KEY()以获取EC_KEY。 2. 将EC_KEY传递给EC_KEY_get0_private_key()以获取BIGNUM。 3. 将BIGNUM传递给BN_bn2mpi()以获取mpi,这是一种写入无符号字符 * 格式的格式。
为反序列化私钥: 1. 将mpi传递给BN_mpi2bn()以获取BIGNUM。 2. 将BIGNUM传递给EC_KEY_set_private_key()以获取EC_KEY。 3. 将EC_KEY传递给EVP_PKEY_set1_EC_KEY以获取EVP_KEY。
还可以将BIGNUM转换为十六进制、十进制或"bin",但我认为mpi使用的字节数最少。

4
感谢提供后续答案! - samoz
1
反序列化公钥可能在4年5个月前运行良好,但今天听起来相当复杂!!!:p EVP_PKEY_get1_EC_KEY应该从八位字节中创建EC_POINT,但它从输入中获取EC_POINT*!我尝试使用EC_POINT_new进行创建,但后来无法使用create EVP_KEY创建秘密密钥! - madz

2
OpenSSL 3.x.x
将公钥序列化的方法如下:
// We assume the public and private keys have been already generated.
// EVP_PKEY* keyPair...

// Get the serialized public key length.
size_t serializedPublicKeyLen = 0;
if (EVP_PKEY_get_octet_string_param(keyPair, OSSL_PKEY_PARAM_PUB_KEY,
    NULL, 0, &serializedPublicKeyLen) != 1) {
  return;
}

// Allocate memory for the serialized public key.
unsigned char* serializedPublicKey = (unsigned char*)OPENSSL_malloc(serializedPublicKeyLen);
if (serializedPublicKey == NULL) {
  return;
}

// Get the serialized public key.
if (EVP_PKEY_get_octet_string_param(keyPair, OSSL_PKEY_PARAM_PUB_KEY,
   serializedPublicKey, serializedPublicKeyLen, &serializedPublicKeyLen) != 1) {
  return;
}

// Deallocate the memory when you finish using the serialized public key.
OPENSSL_free(serializedPublicKey);

将公钥反序列化:
// A parameter build for the public key.
OSSL_PARAM_BLD* paramBuild = OSSL_PARAM_BLD_new();
if (paramBuild == NULL) {
  return;
}

// This is just an example. Set the curve
// you used to generate the public and private keys.
const char curveName[] = "secp384r1";

// Set the curve name to the parameter build.
if (OSSL_PARAM_BLD_push_utf8_string(paramBuild,
    OSSL_PKEY_PARAM_GROUP_NAME, curveName, 0) != 1) {
  OSSL_PARAM_BLD_free(paramBuild);
  return;
}

// Set the serialized public key.
if (OSSL_PARAM_BLD_push_octet_string(paramBuild, OSSL_PKEY_PARAM_PUB_KEY,
    serializedPublicKey, serializedPublicKeyLen) != 1) {
  OSSL_PARAM_BLD_free(paramBuild);
  return;
}

// Convert the OSSL_PARAM_BLD to an OSSL_PARAM.
OSSL_PARAM* params = OSSL_PARAM_BLD_to_param(paramBuild);
if (params == NULL) {
  OSSL_PARAM_BLD_free(paramBuild);    
  return;
}

// Create a EVP_PKEY context.
EVP_PKEY_CTX* publicKeyCtx = EVP_PKEY_CTX_new_from_name(NULL, "EC", NULL);
if (publicKeyCtx == NULL) {
  OSSL_PARAM_BLD_free(paramBuild);
  OSSL_PARAM_free(params); 
  return;  
}

// Initialize the EVP_PKEY context.
if (EVP_PKEY_fromdata_init(publicKeyCtx) <= 0) {
  OSSL_PARAM_BLD_free(paramBuild);
  OSSL_PARAM_free(params);
  EVP_PKEY_CTX_free(publicKeyCtx);
  return;
}

// Create the peer public key object.
EVP_PKEY* publicKey = NULL;
if (EVP_PKEY_fromdata(publicKeyCtx, &publicKey,
    EVP_PKEY_PUBLIC_KEY, params) <= 0) {
  OSSL_PARAM_BLD_free(paramBuild);
  OSSL_PARAM_free(params);
  EVP_PKEY_CTX_free(publicKeyCtx);
  return;
}

// Free auxiliary things...
OSSL_PARAM_BLD_free(paramBuild);
OSSL_PARAM_free(params);
EVP_PKEY_CTX_free(publicKeyCtx);


// Now you can use publicKey for EVP_PKEY_derive_set_peer.
// Call EVP_PKEY_free when you finish using it.

一个完整的C++ OpenSSL 3示例:ec-diffie-hellman-openssl.hec-diffie-hellman-openssl.cpp。 一个完整的C++ LibreSSL 3/OpenSSL 1示例:ec-diffie-hellman-libressl.hec-diffie-hellman-libressl.cpp要序列化私钥,你需要获取BIGNUM:
BIGNUM* privateKey = NULL;
EVP_PKEY_get_bn_param(keyPair, OSSL_PKEY_PARAM_PRIV_KEY, &privateKey);

然后,您可以使用其中一个BIGNUM序列化函数:https://www.openssl.org/docs/man3.0/man3/BN_bn2bin.html 为了反序列化私钥,您可以使用上面链接中的一个BIGNUM反序列化函数,然后通过OSSL_PARAM_BLD_push_BN将其推送到使用OSSL_PKEY_PARAM_PRIV_KEY参数构建的参数中。

提醒那些试图反序列化私钥的人:您需要将EVP_PKEY_PUBLIC_KEY替换为EVP_PKEY_KEYPAIR,这将适用于公钥和私钥。此外,公钥不会自动从私钥派生,一些函数如EVP_PKEY_print_private_fp也无法直接使用。 - 123

0
上述实现似乎过于复杂。 openssl / evp.h 具有 i2d_PublicKey() d2i_PublicKey()函数,分别用于将公钥转换为二进制表示形式以及从二进制表示形式转换回公钥(私钥也有相应的函数-请参见:https://www.openssl.org/docs/manmaster/man3/d2i_PublicKey.html)。
一个小代码示例:
vector<unsigned char> ecdhPubkeyData(EVP_PKEY *key)
{
    int len = i2d_PublicKey(key, 0); // with 0 as second arg it gives length
    vector<unsigned char> ret(len);
    unsigned char *ptr = ret.data();
    len = i2d_PublicKey(key, &ptr);
    return ret;
}

// Make sure you free the returned pointer when you are done with it
EVP_PKEY *ecdhPubkeyFromData(vector <unsigned char> const &pubkeyData)
{       // You do need to put in in an existing EVP_PKEY that is assigned
        // an EC_KEY, because it needs to know what curve you use
    EC_KEY *ec_key = EC_KEY_new_by_curve_name(NID_X9_62_prime256v1);
    EVP_PKEY *ret = EVP_PKEY_new();
    EVP_PKEY_assign_EC_KEY(ret, ec_key);
    unsigned char const *ptr = pubkeyData.data();
    d2i_PublicKey(EVP_PKEY_EC, &ret, &ptr, pubkeyData.size());
    return ret;
}
// PS: In a real example you want to check if any of these functions
// return NULL or some error code

我正在使用C++向量来包含二进制数据,但当然你也可以使用C风格的数组 :-)

我绝对不是OpenSSL专家,所以如果我在这个实现中做错了什么,请告诉我 :-p


1
这个问题被标记为C,但这是一个C++的答案。 - Andrew Henle
i2d函数不返回原始密钥,而是根据OpenSSL文档以某种PKCS格式返回密钥。似乎可以更改格式,但不清楚如何操作。 - Johan Bjäreholt

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接