3

我正在尝试使用 OpenSSL(用于验证签名)在 C++ 中导入ECDSA公钥,但d2i_ECPKParameters返回 NULL。

使用 Web Cryptographi API 生成的密钥;以spki格式导出的公钥(W3 TR 文档在导出密钥时讨论 ASN.1 结构,以及 spki 的 DER 编码)。

我是 OpenSSL 的新手,我做错了什么?

进口:

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

更新: 其他导入尝试(但仍然无效):

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

出口:

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

更新 2:

我生成了 PEM 密钥(从 JS 中的导出密钥),但主要的是,我不使用 PEM 密钥。在 JavaScript 中导出公钥后,我从 ArrayBuffer 中创建一个新的 Uint8Array,通过 WebSocket(二进制帧)将其发送到服务器,并尝试解析它。接收到的 uint8_t 数组总是 158 字节长度。我使用 P-521——secp521r1。

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

一些细节:

% 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

调用 o2i_ECPublicKey 时的错误代码:

(使用相同的数据调用,但每次错误都不同——无论如何都会重复。)

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

更新 3:

从 C++ 中,我将接收到的数据(密钥)写到一个文件中:

% % 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

十六进制编码的导出 SPKI(WebCrypto 导出的结果):

私人的:

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

上市:

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

更新 4:

私钥 jwk 格式:

{
  "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"
}
4

2 回答 2

3

...导出的公钥是 spki 格式 ...

使用 ASN.1 编码的密钥大致结构如下。请参阅RFC 5480,第 2 节。主题公钥信息字段RFC 3279,第 2.3.5 节 ECDSA 和 ECDH 密钥,了解详细信息。

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

还有一个“原始密钥”,它是没有外部序列和算法标识符的密钥材料。OpenSSL在其手册页中将“原始密钥”称为传统密钥。


d2i_ECPKParameters 返回 NULL...

好的,你有一个 SPKI,而不仅仅是域参数。域参数是曲线系数(ab)、模量(p)、基点(G)等,它们描述了曲线。他们没有钥匙。

因此,您应该使用类似d2i_PublicKey的方法将密钥解析为 a EVP_KEY,然后在加载密钥后获取域参数。


我见过的互操作的最大问题是:

NAMED_CURVE or DOMAIN_PARAMETERS

如果它是一条命名曲线,那么它将类似于secp256prime256v1。如果它的域参数,那么命名曲线是“展开”或“完全展开”,它将是曲线系数(ab)、模量(p)、基点(G)等。虽然他们指定完全相同的事情,它们在实践中造成了很多麻烦。

命名曲线和域参数之间的互操作会造成很多麻烦,以至于 OpenSSL 上有一个 wiki 页面:椭圆曲线密码学 | 命名曲线。事实上,OpenSSL Web 服务器无法正常运行,因为这太荒谬了!

所以我在这里唯一能说的是:认识到有什么,不要指望命名曲线和域参数在软件中是相等的,即使它们完全相同(除了演示细节)。


如果您提供一些测试密钥,我们可能会提供更多详细信息。我的猜测是你有一个 PEM 编码的密钥,所以你应该使用其他函数,比如PEM_read_PUBKEY。但这只是一个猜测。

于 2015-11-12T03:53:36.190 回答
1

OpenSSL 中似乎没有实现这些功能d2i_ECPublicKey()和/或等价物。i2d_ECPublicKey()根据用户 jww 指向的 ASN.1 定义,您可以选择自己定义它们,如下所示:

#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)

注意:这不是一个完整的定义。它假定密钥包含命名曲线,而不是曲线参数(也请参见 jww 的答案)。它应该适用于您的示例,但如果您希望它适用于所有可能的 EC 密钥,您应该包含一个CHOICE字段——很好的练习 :-)

从这里开始,您可以使用函数d2i_EC_PUBLICKEY()i2d_EC_PUBLICKEY()进行来回转换。这是一个没有任何错误检查的示例:

#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) */
}

使用您提供的测试仪密钥,输出为

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

testKey 数组根据您的密钥定义如下:

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:分析 ASN.1 数据的好工具是这个免费的ASN.1 编辑器。使用它时,您的测试密钥如下所示: ASN.1 编辑器

于 2015-11-12T22:10:09.237 回答