ECDSA 签名验证原理及C语言实现

这两天总算把ECDSA搞明白了,本来想造个ECDSA轮子,但最近有点忙,而ECDSA轮子又不像HASH那样简单,所以就直接拿现成的轮子来记录一些ECDSA学习心得。
这里贴上github上一个比较适合学习的ECDSA代码,当然这个版本的代码没有openssl等商业级的代码专业,但是它足够简单,用来学习ECDSA原理非常合适。
easy-ecc

非对称加密算法签名/验证无非包括三步:
1. 密钥生成keygen
2. 签名sign
3. 验证verify

后文都以ECDSA384为例。

1 密钥生成

密钥生成其实主要涉及椭圆曲线ECC的一些原理,在这篇小记里,就不再赘述原理,网上讲原理的文章非常多。我当时也是看了以下这篇文章算是入门ECC原理。非常好的一篇文章,分享给各位朋友。
椭圆曲线密码学简介
椭圆曲线密码学的简单介绍

椭圆曲线定义

从上文中我们就可以知道椭圆的通式为
y^2 = x^3 + a*x + b mod p 以及一个无穷大的数0,当然最正宗的定义还有限定条件,该曲线是在有限域上的,并且a,b和q之间也存在关系。

ECDSA的密钥的计算方式

  1. 使用椭圆曲线E, 其中
    模数为p
    系数为a和b
    生成素数阶n的循环群的点G
  2. 选择一个随机整数d,且0 < d < q。
  3. 计算B=dG
    其中d就是ECSDA的私钥,而点B(x,y)就是ECDSA的公钥。

P-384曲线

ECDSA384选用的P-384曲线。维基百科对P-384曲线描述如下:

P-384 is the elliptic curve currently specified in NSA Suite B Cryptography for the ECDSA and ECDH algorithms. It is a 384 bit curve with characteristic approximately. In binary, this mod is given by 111…1100…0011…11. That is, 288 1s followed by 64 0s followed by 32 1s. The curve is given by the equation y^2 = x^3 - 3x + b where b is given by a certain 384 bit number.
简而言之,这条曲线形式就是y^2 = x^3 - 3x + b mod p,而b和q以及素数阶n在ECDSA中都是固定的。

所以ECDSA384中密钥生成的一些条件我们都找到了,从上述式子中可以看到系数a=-3, 其他的参数我们从代码中看。从代码中可以看到B,P和N都是一些固定值,这个是ECDSA384所规定的数,都是密码学专家制定的。因为这里讲的是ECDSA384,所以所有参与运算的数都是384 bits。但由于计算机现在最长的类型就是64bits,所以一个64 bits的数组来描述,相关的运算也因此稍麻烦一点。

ecc.h

#define ECC_CURVE 48

#define Curve_P_48 {0x00000000FFFFFFFF, 0xFFFFFFFF00000000, 0xFFFFFFFFFFFFFFFE, 0xFFFFFFFFFFFFFFFF, 0xFFFFFFFFFFFFFFFF, 0xFFFFFFFFFFFFFFFF}

#define Curve_B_48 {0x2A85C8EDD3EC2AEF, 0xC656398D8A2ED19D, 0x0314088F5013875A, 0x181D9C6EFE814112, 0x988E056BE3F82D19, 0xB3312FA7E23EE7E4}

#define Curve_G_48 { \
    {0x3A545E3872760AB7, 0x5502F25DBF55296C, 0x59F741E082542A38, 0x6E1D3B628BA79B98, 0x8EB1C71EF320AD74, 0xAA87CA22BE8B0537}, \
    {0x7A431D7C90EA0E5F, 0x0A60B1CE1D7E819D, 0xE9DA3113B5F0B8C0, 0xF8F41DBD289A147C, 0x5D9E98BF9292DC29, 0x3617DE4A96262C6F}}

#define Curve_N_48 {0xECEC196ACCC52973, 0x581A0DB248B0A77A, 0xC7634D81F4372DDF, 0xFFFFFFFFFFFFFFFF, 0xFFFFFFFFFFFFFFFF, 0xFFFFFFFFFFFFFFFF}

static uint64_t curve_p[NUM_ECC_DIGITS] = CONCAT(Curve_P_, ECC_CURVE);
static uint64_t curve_b[NUM_ECC_DIGITS] = CONCAT(Curve_B_, ECC_CURVE);
static EccPoint curve_G = CONCAT(Curve_G_, ECC_CURVE);
static uint64_t curve_n[NUM_ECC_DIGITS] = CONCAT(Curve_N_, ECC_CURVE);
  • curve_b就是椭圆曲线的系数b
  • curve_p就是椭圆曲线的模数p
  • curve_G就是椭生成素数阶curve_n的循环群的点G

代码实现

然后就可以直接看代码中是如何生成密钥对,其API是

int ecc_make_key(uint8_t p_publicKey[ECC_BYTES+1], uint8_t p_privateKey[ECC_BYTES])

p_privateKey就是生成的私钥,而p_publicKey就是生成的公钥。有人问公钥不是点B的坐标吗,那点B不是需要x和y吗,x和y又分别是384位的,那为什么这里公钥是384 + 8位呢?这主要是这份代码的作者实现的小技巧,实际上通过坐标x就可以算出坐标y,所以这里只存了点B的x坐标即可,那多了8个位用作校验计算的准确性。当然可以直接把这个p_publicKey设置为p_publicKey[ECC_BYTES * 2],也不做任务压缩,直接将x和y坐标存到公钥中,那使用的时候就不需要通过x坐标去算y坐标,这只是个人实现的喜好而已。

int ecc_make_key(uint8_t p_publicKey[ECC_BYTES+1], uint8_t p_privateKey[ECC_BYTES])
{
    uint64_t l_private[NUM_ECC_DIGITS];
    EccPoint l_public;
    unsigned l_tries = 0;

    do
    {
        /*1.生成随机整数d,即私钥*/
        if(!getRandomNumber(l_private) || (l_tries++ >= MAX_TRIES))
        {
            return 0;
        }
        /*1.1生成的随机数不能为0*/
        if(vli_isZero(l_private))
        {
            continue;
        }

        /*1.2 生成的随机数必须满足 0 < d < n*/
        /* Make sure the private key is in the range [1, n-1].
           For the supported curves, n is always large enough that we only need to subtract once at most. */
        if(vli_cmp(curve_n, l_private) != 1)
        {
            vli_sub(l_private, l_private, curve_n);
        }
        /*2.根据p-384曲线计算公钥B=dG*/
        EccPoint_mult(&l_public, &curve_G, l_private, NULL);
    } while(EccPoint_isZero(&l_public));
    /*将私钥和公钥分别转换为大端的字节数组并返回给上层*/
    ecc_native2bytes(p_privateKey, l_private);
    /*可以看到公钥B的x坐标是存在在p_publicKey[1]开始的位置,p_publicKey[0]存放了一个y坐标校验值*/
    ecc_native2bytes(p_publicKey + 1, l_public.x);
    p_publicKey[0] = 2 + (l_public.y[0] & 0x01);
    return 1;
}

上面代码就列出来,根据中文注释可以看到跟描述的生成步骤是一一对应的。其中一些大数的运算以及Ecc曲线的运算就不展开了,这里面都是编程体力活,原理很简单,但代码实现比较繁琐。

2 签名

与DSA一样,ECDSA签名由一对整数(r, s)组成,其中每个值的位长度都与q相同,这也有助于实现十分简洁的签名。使用公钥和私钥计算消息x的签名的方式如下

  • 选择一个整数作为随机临时密钥Ke, 且0 < Ke < n。
  • 计算R = Ke * A
  • 设置r = Xr
  • 计算s = (h(x) + d * r)/Ke mod q。

其中h(x)是待签名的数据的哈希值,ECDSA384的哈希算法是SHA384。在步骤3中,点R的x坐标赋给变量r。这样签名整数对(r,s)就计算出来了。
所以其实如果理解了椭圆曲线的原理之后,理解ECDSA还是很容易的,直接看代码,很直观得可以看到这个ECDSA签名流程。

签名的接口如下, p_privateKey是签名所用的私钥,p_hash是待签名数据的哈希值,p_signature存放计算出来的签名整数对(r,s),p_signature[0:ECC_BYTES-1] 存放r,p_signature[ECC_BYTES, ECC_BYTES * 2 - 1]存放s。

int ecdsa_sign(const uint8_t p_privateKey[ECC_BYTES], const uint8_t p_hash[ECC_BYTES], uint8_t p_signature[ECC_BYTES*2])
选择一个整数作为随机临时密钥Ke, 且0 < Ke < n。

从代码中可以看到, ecdsa_sign一开始就生成一个随机数k,并且必须满足0 < k < n。

int ecdsa_sign(const uint8_t p_privateKey[ECC_BYTES], const uint8_t p_hash[ECC_BYTES], uint8_t p_signature[ECC_BYTES*2])
{
    uint64_t k[NUM_ECC_DIGITS];
    uint64_t l_tmp[NUM_ECC_DIGITS];
    uint64_t l_s[NUM_ECC_DIGITS];
    EccPoint p;
    unsigned l_tries = 0;

    do
    {
        if(!getRandomNumber(k) || (l_tries++ >= MAX_TRIES))
        {
            return 0;
        }
        if(vli_isZero(k))
        {
            continue;
        }

        if(vli_cmp(curve_n, k) != 1)
        {
            vli_sub(k, k, curve_n);
        }
     ......
 }
计算R = Ke * A

同样计算出来的点R的x坐标也不能比q大。

int ecdsa_sign(const uint8_t p_privateKey[ECC_BYTES], const uint8_t p_hash[ECC_BYTES], uint8_t p_signature[ECC_BYTES*2])
{
        ......
     /* tmp = k * G */
        EccPoint_mult(&p, &curve_G, k, NULL);

        /* r = x1 (mod n) */
        if(vli_cmp(curve_n, p.x) != 1)
        {
            vli_sub(p.x, p.x, curve_n);
        }
        ......
}
设置r = Xr

将r以大端字节数组的形式存放到签名p_signature的前半部分

int ecdsa_sign(const uint8_t p_privateKey[ECC_BYTES], const uint8_t p_hash[ECC_BYTES], uint8_t p_signature[ECC_BYTES*2])
{
        ......
        ecc_native2bytes(p_signature, p.x);
        ......
}
计算s = (h(x) + d * r)/Ke mod q

原理很简单,但由于计算方法稍微复杂一点这个计算分成三步走

  • s = (r*d) mod n
  • s = (h(x) + r*d) mod n
  • s = (h(x) + r*d) / k mod n
int ecdsa_sign(const uint8_t p_privateKey[ECC_BYTES], const uint8_t p_hash[ECC_BYTES], uint8_t p_signature[ECC_BYTES*2])
{
    ......
    ecc_bytes2native(l_tmp, p_privateKey);
    vli_modMult(l_s, p.x, l_tmp, curve_n); /* s = (r*d) mod n */
    ecc_bytes2native(l_tmp, p_hash);
    vli_modAdd(l_s, l_tmp, l_s, curve_n); /* s = (h(x) + r*d) mod n */
    vli_modInv(k, k, curve_n); /* k = 1 / k */
    vli_modMult(l_s, l_s, k, curve_n); /* s = (h(x) + r*d) / k mod n */
    ecc_native2bytes(p_signature + ECC_BYTES, l_s);

    return 1;
}

最终将算出来的s存放到签名p_signature 的后半部分。

3 签名验证

签名验证过程如下:
- 计算辅助值W = 1 / s mod q
- 计算辅助值u1 = w * h(x) mod q
- 计算辅助值u2 = w * r mod q
- 计算P = u1 * A + u2 * B
- 验证verify(x, (r, s)), 如果P的x坐标 = r mod q,那么就是有效的签名,否则就是无效的签名。

关于验证的证明可以参考《深入浅出密码学-常用加密技术原理及应用》P268的证明。

验证的接口如下:
- p_publicKey就是验证所要用到的公钥,即点B的坐标。
- p_hash是待验证数据的哈希值。
- p_signature 就是签名数据。

int ecdsa_verify(const uint8_t p_publicKey[ECC_BYTES+1], const uint8_t p_hash[ECC_BYTES], const uint8_t p_signature[ECC_BYTES*2])
计算辅助值W = 1 / s mod q
int ecdsa_verify(const uint8_t p_publicKey[ECC_BYTES+1], const uint8_t p_hash[ECC_BYTES], const uint8_t p_signature[ECC_BYTES*2])
{
    uint64_t u1[NUM_ECC_DIGITS], u2[NUM_ECC_DIGITS];
    uint64_t z[NUM_ECC_DIGITS];
    EccPoint l_public, l_sum;
    uint64_t rx[NUM_ECC_DIGITS];
    uint64_t ry[NUM_ECC_DIGITS];
    uint64_t tx[NUM_ECC_DIGITS];
    uint64_t ty[NUM_ECC_DIGITS];
    uint64_t tz[NUM_ECC_DIGITS];

    uint64_t l_r[NUM_ECC_DIGITS], l_s[NUM_ECC_DIGITS];
    /*先根据公钥B的x坐标计算坐标y,然后从签名中提取出r和s*/
    ecc_point_decompress(&l_public, p_publicKey);
    ecc_bytes2native(l_r, p_signature);
    ecc_bytes2native(l_s, p_signature + ECC_BYTES);
    ......
    /* 1.计算w = s ^ -1 mod q */
    vli_modInv(z, l_s, curve_n); /* Z = s^-1 */
    .......
}
计算辅助值u1 = w * h(x) mod q 和 u2 = w * r mod q
int ecdsa_verify(const uint8_t p_publicKey[ECC_BYTES+1], const uint8_t p_hash[ECC_BYTES], const uint8_t p_signature[ECC_BYTES*2])
{
    ......
    ecc_bytes2native(u1, p_hash);
    vli_modMult(u1, u1, z, curve_n); /* u1 = w * h(x) mod q = h(x) / s mod q */
    vli_modMult(u2, l_r, z, curve_n); /* u2 = w * r mod q= r / s mod q */
    ......
}
计算P = u1 * A + u2 * B

这里面的计算代码上实现有点繁琐,主要是大数运算和椭圆曲线的运算,就不一一展开了,最终就是将计算出来的点P的x坐标和y坐标存放到rx和ry这两个变量中。

int ecdsa_verify(const uint8_t p_publicKey[ECC_BYTES+1], const uint8_t p_hash[ECC_BYTES], const uint8_t p_signature[ECC_BYTES*2])
{
    ......
        /* Calculate l_sum = G + Q. */
    vli_set(l_sum.x, l_public.x);
    vli_set(l_sum.y, l_public.y);
    vli_set(tx, curve_G.x);
    vli_set(ty, curve_G.y);
    vli_modSub(z, l_sum.x, tx, curve_p); /* Z = x2 - x1 */
    XYcZ_add(tx, ty, l_sum.x, l_sum.y);
    vli_modInv(z, z, curve_p); /* Z = 1/Z */
    apply_z(l_sum.x, l_sum.y, z);

    /* Use Shamir's trick to calculate u1*G + u2*Q */
    EccPoint *l_points[4] = {NULL, &curve_G, &l_public, &l_sum};
    uint l_numBits = umax(vli_numBits(u1), vli_numBits(u2));

    EccPoint *l_point = l_points[(!!vli_testBit(u1, l_numBits-1)) | ((!!vli_testBit(u2, l_numBits-1)) << 1)];
    vli_set(rx, l_point->x);
    vli_set(ry, l_point->y);
    vli_clear(z);
    z[0] = 1;

    int i;
    for(i = l_numBits - 2; i >= 0; --i)
    {
        EccPoint_double_jacobian(rx, ry, z);

        int l_index = (!!vli_testBit(u1, i)) | ((!!vli_testBit(u2, i)) << 1);
        EccPoint *l_point = l_points[l_index];
        if(l_point)
        {
            vli_set(tx, l_point->x);
            vli_set(ty, l_point->y);
            apply_z(tx, ty, z);
            vli_modSub(tz, rx, tx, curve_p); /* Z = x2 - x1 */
            XYcZ_add(tx, ty, rx, ry);
            vli_modMult_fast(z, z, tz);
        }
    }

    vli_modInv(z, z, curve_p); /* Z = 1/Z */
    apply_z(rx, ry, z);
    ......
}
验证verify(x, (r, s)), 如果P的x坐标 = r mod q,那么就是有效的签名,否则就是无效的签名。

最终比较计算出来点P的x坐标是否与签名中的r相同来判断签名是否成功。

int ecdsa_verify(const uint8_t p_publicKey[ECC_BYTES+1], const uint8_t p_hash[ECC_BYTES], const uint8_t p_signature[ECC_BYTES
    ......
    /* Accept only if v == r. */
    return (vli_cmp(rx, l_r) == 0);
}

应用

定义一个hash模拟一个数据的哈希值。首先创建密钥对,然后分别签名验证。
main.c

  1 #include "ecc.h"
  2 #include <stdint.h>
  3 #include <stdlib.h>
  4 #include <stdio.h>
  5
  6 int main()
  7 {
  8     uint8_t public_key[ECC_BYTES + 1];
  9     uint8_t private_key[ECC_BYTES];
 10     uint8_t hash[ECC_BYTES] = {0x2};
 11     uint8_t signature[ECC_BYTES * 2];
 12     uint32_t i = 0;
 13     int ret = 0;
 14
 15     ret = ecc_make_key(public_key, private_key);
 16     if (ret == 0) {
 17         printf("ecc_make_key failure\n");
 18     }
 19
 20     printf("##############public key###############\n");
 21     for (i = 0;i < ECC_BYTES + 1;i++) {
 22         printf("%x ", public_key[i]);
 23     }
 24
 25     printf("\n\n");
 26     printf("##############private key###############\n");
 27     for (i = 0;i < ECC_BYTES;i++) {
 28         printf("%x ", private_key[i]);
 29     }
 30     printf("\n\n");
 31
 32     ret = ecdsa_sign(private_key, hash, signature);
 33     if (ret == 0) {
 34         printf("ecdsa_sign failure\n");
 35     }
 36
 37     ret = ecdsa_verify(public_key, hash, signature);
 38     if (ret == 1) {
 39         printf("verify passed\n");
 40     } else {
 41         printf("verify failed\n");
 42     }
 43
 44     return 0;
 45 }

运行结果

############secret key#############
662eea0239ee28f 62693fced7eb10f1 3dd1e1815fe2e2b6 af68ec328b437369 737206cce4206de3 7650b5ce554851b6

##############public key###############
2 8a 5c c7 24 28 ae 49 da c1 8 e7 a8 80 8a fd 5f ef a 79 ca d9 57 bf cc f9 92 98 85 5f 68 c4 5a 77 e2 2 d1 56 e4 4f 1d c5 94 1c bb 62 8e 2b a2

##############private key###############
76 50 b5 ce 55 48 51 b6 73 72 6 cc e4 20 6d e3 af 68 ec 32 8b 43 73 69 3d d1 e1 81 5f e2 e2 b6 62 69 3f ce d7 eb 10 f1 6 62 ee a0 23 9e e2 8f

l_read:48, l_left:48
verify passed

假如我们篡改了数据,那么验证就会失败,这里可以通过修改哈希值来模拟数据篡改,应该当数据一旦发生变化,其相应的哈希值就会发生变化。修改main函数如下,在验证签名之前篡改数据。

 37     hash[0] = 0x3;
 38     ret = ecdsa_verify(public_key, hash, signature);
 39     if (ret == 1) {
 40         printf("verify passed\n");
 41     } else {
 42         printf("verify failed\n");
 43     }
 44
 45     return 0;
 46 }

再运行一把就可以看到验证签名失败。

############secret key#############
4f512226dd0a47e3 e7c9bc62fd8f37e6 f200140c350bb743 7fa11d17b04448fb aa4b146001ab1143 b132949e3715217c

##############public key###############
2 ea cb d1 46 84 60 89 2f cb c9 6 d8 da 9a 1d b4 21 7d 17 2 e4 b5 2b 6 58 d9 e8 75 e2 4b ae 30 84 7c 70 cf 41 7 fe 8c f4 d2 a8 37 50 e1 4 92

##############private key###############
b1 32 94 9e 37 15 21 7c aa 4b 14 60 1 ab 11 43 7f a1 1d 17 b0 44 48 fb f2 0 14 c 35 b b7 43 e7 c9 bc 62 fd 8f 37 e6 4f 51 22 26 dd a 47 e3

l_read:48, l_left:48
verify failed

猜你喜欢

转载自blog.csdn.net/u011280717/article/details/79689205