如何使用OpenSSL库将ECDSA二进制签名进行转换和验证?

huangapple go评论60阅读模式
英文:

How to convert and verify ECDSA binary signature using OpenSSL library?

问题

I am trying to validate a signature returned by DS28C36 using OpenSSL in Linux. For testing purpose, I copied pasted public key and signature into global arrays. For signature conversion I followed this old post: https://stackoverflow.com/questions/31390784/creating-a-der-formatted-ecdsa-signature-from-raw-r-and-s
However, when I converted it using i2d_ECDSA_SIG, it only return 2 bytes for some reasons and of course the following ECDSA_verify failed. In addition, when I tried to dump it using BN_bn2hex, I got a segmentation fault. I am not sure what I did wrong here.

Thanks for your help

This is my test code:

#include <errno.h>
#include <unistd.h>
#include <malloc.h>
#include <string.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <netinet/in.h>
#include <resolv.h>

#include <openssl/obj_mac.h>
#include <openssl/ec.h>
#include <openssl/ecdsa.h>
#include <openssl/crypto.h>
#include <openssl/err.h>
#include <openssl/rand.h>
#include <openssl/ssl.h>

unsigned char rom_id[] = {0x4c, 0x51, 0xb7, 0x12, 0x00, 0x00, 0x00, 0xfa};

unsigned char man_id[] = {0x00, 0x80};

unsigned char pub_key_raw[] = { 0x04, 
0x34, 0xe3, 0x12, 0x1d, 0x73, 0xc4, 0x25, 0x68, 0xef, 0xf8, 0xcc, 0x89, 0xc7, 0x77, 0x9c, 0x4e,
0x16, 0xe7, 0x79, 0xd5, 0x76, 0x68, 0x0e, 0xff, 0xad, 0x1f, 0x53, 0xd6, 0x33, 0x70, 0xb8, 0xa3,
0xa5, 0xb9, 0x92, 0xc9, 0x3c, 0x75, 0xf7, 0x07, 0xbf, 0xed, 0xe1, 0x25, 0xbf, 0x1f, 0xcb, 0xeb,
0x30, 0x57, 0x4f, 0x05, 0x96, 0x28, 0x9f, 0x66, 0x33, 0x7a, 0x46, 0xb6, 0x76, 0x35, 0x8c, 0xdd};

unsigned char challenge[] = {0xcc, 0xcc, 0xcc, 0xcc, 0xcc, 0xcc, 0xcc, 0xcc};

unsigned char usr_page3[] = {
                   0xB6, 0xB5, 0x88, 0x2C, 0xE1, 0xC3, 0x4F, 0x2B, 0x56, 0x36, 0xD7, 0xE3, 0x22, 0x5E, 0x3B, 0x34,
                   0x5D, 0x33, 0xD6, 0x7F, 0xEC, 0x1C, 0x2D, 0x45, 0x86, 0xFD, 0x14, 0xE6, 0x05, 0x39, 0x07, 0xF9};

unsigned char sig_s[] = {
               0x18, 0x4e, 0xbf, 0xa0, 0x9b, 0xe6, 0xeb, 0x66, 0x42, 0x1f, 0x01, 0x9e, 0xb8, 0xf5, 0xf5, 0x18,
               0x41, 0x34, 0x37, 0xd8, 0x20, 0x6e, 0x1d, 0x69, 0x94, 0x05, 0x15, 0x98, 0xa8, 0x64, 0x90, 0x1e};

unsigned char sig_r[] = {
               0x8c, 0xf0, 0x3e, 0xb2, 0x51, 0x87, 0xb2, 0x5c, 0x78, 0x98, 0x71, 0xd9, 0x57, 0x5b, 0xdf, 0x13,
               0x6e, 0x1c, 0xcd, 0x18, 0xba, 0xee, 0xec, 0x5e, 0xac, 0xbe, 0x95, 0xf1, 0x2f, 0x88, 0xaa, 0xa2};

void hexdump(void* buf, size_t length) {
    unsigned char* p = (unsigned char*)buf;
    unsigned char c;
    int i = 0;
    while (i < length) {
        printf("%04x: ", i);
        for (int j = 0; j < 16; j++) {
            if (i + j < length) {
                c = *(p + i + j);
                printf("%02x ", c);
            } else {
                printf("   ");
            }
        }
        printf(" ");
        for (int j = 0; j < 16; j++) {
            if (i + j < length) {
                c = *(p + i + j);
                if (c >= 32 && c <= 126) {
                    printf("%c", c);
                } else {
                    printf(".");
                }
            } else {
                printf(" ");
            }
        }
        printf("\n");
        i += 16;
    }
}

int main(int argc, char **argv)
{
   unsigned char *sig_ptr;
   char *s_r, *s_s;
   int param, msg_len, sig_len;
   unsigned char message[256];
   unsigned char hash[SHA256_DIGEST_LENGTH], signature_der[256];

   EC_GROUP *g;
   EC_POINT *p;
   EC_KEY  *k;
   SHA256_CTX sha256;
   ECDSA_SIG* ec_sig = ECDSA_SIG_new();

   OPENSSL_init_ssl(OPENSSL_INIT_LOAD_SSL_STRINGS, NULL);
   OPENSSL_init_crypto(OPENSSL_INIT_LOAD_CRYPTO_STRINGS, NULL);

   //

<details>
<summary>英文:</summary>

I am trying to validate a signature returned by DS28C36 using OpenSSL in Linux. For testing purpose, I copied pasted public key and signature into global arrays. For signature conversion I followed this old post: https://stackoverflow.com/questions/31390784/creating-a-der-formatted-ecdsa-signature-from-raw-r-and-s
However, when I converted it using i2d_ECDSA_SIG, it only return 2 bytes for some reasons and of course the following ECDSA_verify failed. In addition, when I tried to dump it using BN_bn2hex, I got a segmentation fault. I am not sure what I did wrong here.

Thanks for your help

This is my test code

#include <errno.h>
#include <unistd.h>
#include <malloc.h>
#include <string.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <netinet/in.h>
#include <resolv.h>

#include <openssl/obj_mac.h>
#include <openssl/ec.h>
#include <openssl/ecdsa.h>
#include <openssl/crypto.h>
#include <openssl/err.h>
#include <openssl/rand.h>
#include <openssl/ssl.h>

unsigned char rom_id[] = {0x4c, 0x51, 0xb7, 0x12, 0x00, 0x00, 0x00, 0xfa};

unsigned char man_id[] = {0x00, 0x80};

unsigned char pub_key_raw[] = { 0x04,
0x34, 0xe3, 0x12, 0x1d, 0x73, 0xc4, 0x25, 0x68, 0xef, 0xf8, 0xcc, 0x89, 0xc7, 0x77, 0x9c, 0x4e,
0x16, 0xe7, 0x79, 0xd5, 0x76, 0x68, 0x0e, 0xff, 0xad, 0x1f, 0x53, 0xd6, 0x33, 0x70, 0xb8, 0xa3,
0xa5, 0xb9, 0x92, 0xc9, 0x3c, 0x75, 0xf7, 0x07, 0xbf, 0xed, 0xe1, 0x25, 0xbf, 0x1f, 0xcb, 0xeb,
0x30, 0x57, 0x4f, 0x05, 0x96, 0x28, 0x9f, 0x66, 0x33, 0x7a, 0x46, 0xb6, 0x76, 0x35, 0x8c, 0xdd};

unsigned char challenge[] = {0xcc, 0xcc, 0xcc, 0xcc, 0xcc, 0xcc, 0xcc, 0xcc};

unsigned char usr_page3[] = {
0xB6, 0xB5, 0x88, 0x2C, 0xE1, 0xC3, 0x4F, 0x2B, 0x56, 0x36, 0xD7, 0xE3, 0x22, 0x5E, 0x3B, 0x34,
0x5D, 0x33, 0xD6, 0x7F, 0xEC, 0x1C, 0x2D, 0x45, 0x86, 0xFD, 0x14, 0xE6, 0x05, 0x39, 0x07, 0xF9};

unsigned char sig_s[] = {
0x18, 0x4e, 0xbf, 0xa0, 0x9b, 0xe6, 0xeb, 0x66, 0x42, 0x1f, 0x01, 0x9e, 0xb8, 0xf5, 0xf5, 0x18,
0x41, 0x34, 0x37, 0xd8, 0x20, 0x6e, 0x1d, 0x69, 0x94, 0x05, 0x15, 0x98, 0xa8, 0x64, 0x90, 0x1e};

unsigned char sig_r[] = {
0x8c, 0xf0, 0x3e, 0xb2, 0x51, 0x87, 0xb2, 0x5c, 0x78, 0x98, 0x71, 0xd9, 0x57, 0x5b, 0xdf, 0x13,
0x6e, 0x1c, 0xcd, 0x18, 0xba, 0xee, 0xec, 0x5e, 0xac, 0xbe, 0x95, 0xf1, 0x2f, 0x88, 0xaa, 0xa2};

void hexdump(void* buf, size_t length) {
unsigned char* p = (unsigned char*)buf;
unsigned char c;
int i = 0;
while (i < length) {
printf("%04x: ", i);
for (int j = 0; j < 16; j++) {
if (i + j < length) {
c = *(p + i + j);
printf("%02x ", c);
} else {
printf(" ");
}
}
printf(" ");
for (int j = 0; j < 16; j++) {
if (i + j < length) {
c = *(p + i + j);
if (c >= 32 && c <= 126) {
printf("%c", c);
} else {
printf(".");
}
} else {
printf(" ");
}
}
printf("\n");
i += 16;
}
}

int main(int argc, char **argv)
{
unsigned char *sig_ptr;
char *s_r, *s_s;
int param, msg_len, sig_len;
unsigned char message[256];
unsigned char hash[SHA256_DIGEST_LENGTH], signature_der[256];

EC_GROUP *g;
EC_POINT *p;
EC_KEY k;
SHA256_CTX sha256;
ECDSA_SIG
ec_sig = ECDSA_SIG_new();

OPENSSL_init_ssl(OPENSSL_INIT_LOAD_SSL_STRINGS, NULL);
OPENSSL_init_crypto(OPENSSL_INIT_LOAD_CRYPTO_STRINGS, NULL);

// seed the random number generator
srand((unsigned)time(NULL));

//Some initialization works
k = EC_KEY_new_by_curve_name(NID_X9_62_prime256v1);
EC_KEY_set_conv_form(k, POINT_CONVERSION_UNCOMPRESSED);
g = EC_KEY_get0_group(k);
p = EC_POINT_new(g);

//Load EC points
if (1 != EC_POINT_oct2point(g, p, pub_key_raw, 65, NULL)){
printf("EC_POINT_oct2point failed:\n");
printf("%s\n", ERR_error_string(ERR_get_error(), NULL));
}

//Set EC public key
if (1 != EC_KEY_set_public_key(k, p)){
printf("EC_KEY_set_public_key failed:\n");
printf("%s\n", ERR_error_string(ERR_get_error(), NULL));
}
//Check EC public key
if (1 != EC_KEY_check_key(k)) {
printf("EC_KEY_check_key failed:\n");
printf("%s\n", ERR_error_string(ERR_get_error(), NULL));
}
else {
printf("Public key verified OK\n");
}

printf(" Page Data: ");
for (int i = 0; i < 32; i++){
printf("%02X", usr_page3[i]);
}
printf("\n");
hexdump(usr_page3, 32);
printf("\n Challenge: ");
for (int i = 0; i < 8; i++){
printf("%02X",challenge[i]);
}
printf("\n");
hexdump(challenge, 8);

// ROM NO
msg_len = 0;
memcpy(&message[msg_len],rom_id,8);
msg_len += 8;
// Page Data
memcpy(&message[msg_len], usr_page3, 32);
msg_len += 32;
// Challenge (Buffer)
memcpy(&message[msg_len], challenge, 32);
msg_len += 32;
// Page#
message[msg_len++] = 3;//
// MANID
memcpy(&message[msg_len], man_id, 2);
msg_len += 2;

// Calculate Hash
SHA256_Init(&sha256);
SHA256_Update(&sha256, message, strlen(message));
SHA256_Final(hash, &sha256);

if (NULL == BN_bin2bn((unsigned char*)sig_r, 32, ECDSA_SIG_get0_r(ec_sig))) {
printf("BN_bin2bn for r failed:\n");
printf("%s\n", ERR_error_string(ERR_get_error(), NULL));
}

if (NULL == BN_bin2bn((unsigned char*)sig_s, 32, ECDSA_SIG_get0_s(ec_sig))) {
printf("BN_bin2bn for s failed:\n");
printf("%s\n", ERR_error_string(ERR_get_error(), NULL));
}

s_r = BN_bn2hex(ECDSA_SIG_get0_r(ec_sig));
s_s = BN_bn2hex(ECDSA_SIG_get0_s(ec_sig));
printf("(sig->r, sig->s): (%s,%s)\n", s_r, s_s);

sig_ptr = signature_der;
sig_len = i2d_ECDSA_SIG(ec_sig, &sig_ptr);
printf("converted signature length = %d\n", sig_len);

// Validate ECDSA signature
if (1 != ECDSA_verify(0, hash, sizeof(hash), signature_der, sig_len, k)) {
printf("ECDSA_verify failed:\n");
printf("%s\n", ERR_error_string(ERR_get_error(), NULL));
printf("Signature:\n");
hexdump(signature_der, sizeof(signature_der));
}
}


</details>
# 答案1
**得分**: 1
你计算哈希值的方式存在问题。首先,你尝试从`challenge`中复制32字节,但实际上它只包含8字节。从技术上讲,这是未定义行为,实现可以执行任何操作,包括毁灭地球,但大多数情况下不会这样做;在实践中,通常的结果是要么崩溃,要么使用垃圾数据。由于你没有在这里观察到崩溃,所以你的实现可能是后者。其次,你使用`strlen(message)`作为数据的长度,但实际上并不是,因为上面提到的`challenge`中的垃圾数据可能包含空字节,如果没有,`man_id`肯定包含空字节。
然而,这只会导致签名验证失败,而不是你所询问的症状。对于它们:
```c
ECDSA_SIG* ec_sig = ECDSA_SIG_new();
...
if (NULL == BN_bin2bn((unsigned char*)sig_r, 32, ECDSA_SIG_get0_r(ec_sig))) {
printf("BN_bin2bn for r failed:\n");
printf("%s\n", ERR_error_string(ERR_get_error(), NULL));
}

这创建了一个新的ECDSA_SIG对象,但没有实际的签名,因此它的R和S字段为空(null)。当你调用_get0_r时,它只是返回字段而不进行检查,并且BN_bin2bn(buffer,len,null)创建一个新的BN对象来包含它返回的数字(作为指针),但你立即丢弃它。简而言之,这不会将R值放入ec_sig中。

if (NULL == BN_bin2bn((unsigned char*)sig_s, 32, ECDSA_SIG_get0_s(ec_sig))) {
    printf("BN_bin2bn for s failed:\n");
    printf("%s\n", ERR_error_string(ERR_get_error(), NULL));
}

对于S也是如此。

s_r = BN_bn2hex(ECDSA_SIG_get0_r(ec_sig));
s_s = BN_bn2hex(ECDSA_SIG_get0_s(ec_sig));
printf("(sig->r, sig->s): (%s,%s)\n", s_r, s_s);

如果你这样做,它会尝试将ec_sig中仍然为空的null指针视为BN对象并打印它们,但由于null指针不是实际对象,所以会崩溃。

sig_ptr = signature_der;
sig_len = i2d_ECDSA_SIG(ec_sig, &sig_ptr);

如果你这样做(而不是之前的方式),它会尝试编码一个实际上不是签名的“签名”,产生你观察到的虚假2字节编码。

你应该改为像这样做:

ECDSA_SIG *ec_sig = ECDSA_SIG_new();
BN *rbn = BN_bin2bn(rval, sizeof(rval), NULL); 
BN *sbn = BN_bin2bn(sval, sizeof(sval), NULL);
// 最佳风格是检查并处理r和/或s是否为NULL,但在任何合理的系统中这些调用都不会失败
(void) ECDSA_SIG_set0(ec_sig, rbn, sbn); 
// 再次,你可以检查返回值是否不等于1,但不会发生

当将unsigned char []变量(例如你的sig_rsig_s)传递给const unsigned char *参数(例如BN_bin2bn所需的参数)时,你不需要进行类型转换:任何具有类型[qualified] array of T的左值(除非作为sizeof offsetof _Alignof的操作数)都会“衰减”(自动转换)为[qualified] T的指针,任何指向未限定T的指针都可以自动“添加”资格const和/或volatile。因此,具有类型unsigned char[32]sig_r会自动变为unsigned char *,然后根据需要变为const unsigned char *。这在C语言编程(以及C++的“C子集”,如果你不完全切换到std::vector等)中是非常基本的,应该在课程的前两周或文本的前两章中介绍。

英文:

Preliminarily, you compute your hash incorrectly. First you (try to) copy 32 bytes from challenge which contains only 8 bytes. Technically this is Undefined Behavior and the implementation is permitted to do anything, including destroy the Earth -- but most don't; in practice the usual result is either to crash or to use garbage data, and since you didn't observe a crash here, your implementation probably did the latter. Second you then use strlen(message) as the length of your data, but it isn't, because the garbage from challenge per above probably contains a null byte, and if not, man_id certainly does.

However this would only cause the signature verification to fail, not the symptoms you are asking about. For them:

    ECDSA_SIG* ec_sig = ECDSA_SIG_new();
...
if (NULL == BN_bin2bn((unsigned char*)sig_r, 32, ECDSA_SIG_get0_r(ec_sig))) {
printf(&quot;BN_bin2bn for r failed:\n&quot;);
printf(&quot;%s\n&quot;, ERR_error_string(ERR_get_error(), NULL));
}

This creates a new ECDSA_SIG object, but no actual signature, so its R and S fields are empty (null). When you call _get0_r it simply returns the field without checking, and BN_bin2bn(buffer,len,null) creates a new BN object to contain the number which it returns (as a pointer) but you immediately discard. In short, this does NOT put the R value in ec_sig.

   if (NULL == BN_bin2bn((unsigned char*)sig_s, 32, ECDSA_SIG_get0_s(ec_sig))) {
printf(&quot;BN_bin2bn for s failed:\n&quot;);
printf(&quot;%s\n&quot;, ERR_error_string(ERR_get_error(), NULL));
}

Ditto for S.

   s_r = BN_bn2hex(ECDSA_SIG_get0_r(ec_sig));
s_s = BN_bn2hex(ECDSA_SIG_get0_s(ec_sig));
printf(&quot;(sig-&gt;r, sig-&gt;s): (%s,%s)\n&quot;, s_r, s_s);

If you do this, it tries to treat the null pointers still in ec_sig as BN objects and print them, but as null pointers are not actual objects it crashes.

   sig_ptr = signature_der;
sig_len = i2d_ECDSA_SIG(ec_sig, &amp;sig_ptr);

If you do this (and not the previous), it tries to encode a "signature" that is not actually a signature, producing the bogus 2-byte encoding you observed.

You should instead do something like:

    ECDSA_SIG *ec_sig = ECDSA_SIG_new();
BN *rbn = BN_bin2bn(rval,sizeof(rval),NULL); 
BN *sbn = BN_bin2bn(sval,sizeof(sval),NULL);
// best style is to check and handle if r and/or s is NULL
// but these calls can&#39;t actually fail in any sane system
(void) ECDSA_SIG_set0(ec_sig,rbn,sbn); 
// again you can check for return value != 1, but it won&#39;t happen

and of course correct the hash computation.

Aside: when passing an unsigned char [] variable (like your sig_r or sig_s) to a const unsigned char * parameter (like BN_bin2bn takes) you don't need a cast: any occurrence of an lvalue with type [qualified] array of T other than as the operand of sizeof offsetof _Alignof 'decays' (is automatically converted) to pointer to [qualified] T, and any pointer to unqualified T can automatically 'add' qualification const and/or volatile. Thus sig_r with type unsigned char[32] automatically becomes unsigned char * and then const unsigned char * as needed. This is extremely basic to programming in C (and the 'C subset' of C++, if you don't switch entirely to std::vector and the like) and should be covered within the first two weeks of any course, or two chapters of any text.

huangapple
  • 本文由 发表于 2023年5月26日 00:11:45
  • 转载请务必保留本文链接:https://go.coder-hub.com/76334336.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定