RSA(RIVEST-Shamir-Adleman)是第一个公钥密码系统之一,广泛用于安全通信。 RSA算法将生成两个大的随机素数,然后使用它们生成公钥和私钥对,该对可用于进行加密,解密,数字签名生成和数字签名验证。 RSA算法建立在数字理论上,并且可以通过库的支持很容易地实现。

1 Introduction

1.1 Lab environment

实验室环境需要我们安装openssl,因为前期我已经安装过openssl了,所以在此不再赘述。

  • RSA算法涉及大量计算。这些计算无法使用程序中的简单算术运算符直接控制,因为这些操作员只能在原始数据类型上运行,例如32位整数和64位长整数类型。 RSA算法涉及的数字通常大于512位。例如,到多个两个32位整数A和B,我们只需要在我们的程序中使用* b。但是,如果他们是大数字,我们就不能再这样做了;相反,我们需要使用算法(即,函数)来计算其产品。

  • 有几个库可以在任意大小的整数上执行算术运算。在此实验室中,我们将使用OpenSSL提供的大数字库。要使用此库,我们将将每个大数字作为Bignum类型,然后使用库提供的API用于各种操作,例如添加,乘法,指数,模块化操作等。

  • 有大型API都可以从https://linux.die.net/man/3/bn找到。

Task 1: 推导RSA私钥

  • 首先我们需要理解RSA算法的原理


  • 已知相关参数求秘钥
  • 设p q e是三个素数。设n = p * q,我们用(e, n)作为公钥。请计算私钥d。下面列出了p、q和e的十六进制值。请注意,尽管在此任务中使用的p和q是相当大的数字,但它们不够大,不足以保证安全。为了简单,我们故意把它们做得很小。实际上,这些数字至少应该是512位长(这里使用的只有128位)。
//task1.c
#include <stdio.h>
#include <openssl/bn.h>
#define NBITS 128

void printBN(char *msg, BIGNUM *a, BIGNUM *b)
{
    char *number_str_a = BN_bn2hex(a);
    char *number_str_b = BN_bn2hex(b);
    printf("%s (%s,%s)\n", msg, number_str_a, number_str_b);
    OPENSSL_free(number_str_a);
    OPENSSL_free(number_str_b);
}

int main()
{
    // init
    BN_CTX *ctx = BN_CTX_new();
    BIGNUM *p = BN_new();
    BIGNUM *q = BN_new();
    BIGNUM *n = BN_new();
    BIGNUM *phi = BN_new();
    BIGNUM *e = BN_new();
    BIGNUM *d = BN_new();
    BIGNUM *res = BN_new();
    BIGNUM *p_minus_1 = BN_new();
    BIGNUM *q_minus_1 = BN_new();

    // assign value
    BN_hex2bn(&p, "F7E75FDC469067FFDC4E847C51F452DF");
    BN_hex2bn(&q, "E85CED54AF57E53E092113E62F436F4F");
    BN_hex2bn(&e, "0D88C3");

    // n = pq
    BN_mul(n, p, q, ctx);
    printBN("public key", e, n);

    // phi(n) = (p-1)*(q-1)
    BN_sub(p_minus_1, p, BN_value_one());
    BN_sub(q_minus_1, q, BN_value_one());
    BN_mul(phi, p_minus_1, q_minus_1, ctx);

    // check if e and phi(n) is relatively prime
    BN_gcd(res, phi, e, ctx);
    if (!BN_is_one(res))
    {
        printf("Error: e and phi(n) is not relatively prime \n ");
        exit(0);
    }

    BN_mod_inverse(d, e, phi, ctx);
    printBN("private key", d, n);

    BN_clear_free(p);
    BN_clear_free(q);
    BN_clear_free(n);
    BN_clear_free(res);
    BN_clear_free(phi);
    BN_clear_free(e);
    BN_clear_free(d);
    BN_clear_free(p_minus_1);
    BN_clear_free(q_minus_1);

    return 0;
}

其中:
//task1.c

#include <stdio.h>
#include <openssl/bn.h> // 包含OpenSSL库的BN(大数)头文件
#define NBITS 128 // 定义了128位的数字

// 打印BIGNUM类型数字的值
void printBN(char *msg, BIGNUM *a, BIGNUM *b)
{
    char *number_str_a = BN_bn2hex(a);
    char *number_str_b = BN_bn2hex(b);
    printf("%s (%s,%s)\n", msg, number_str_a, number_str_b);
    OPENSSL_free(number_str_a);
    OPENSSL_free(number_str_b);
}

// 主函数
int main()
{
    // 初始化BN_CTX和BIGNUM类型数字
    BN_CTX *ctx = BN_CTX_new();
    BIGNUM *p = BN_new();
    BIGNUM *q = BN_new();
    BIGNUM *n = BN_new();
    BIGNUM *phi = BN_new();
    BIGNUM *e = BN_new();
    BIGNUM *d = BN_new();
    BIGNUM *res = BN_new();
    BIGNUM *p_minus_1 = BN_new();
    BIGNUM *q_minus_1 = BN_new();

    // 给p,q,e赋初始值
    BN_hex2bn(&p, "F7E75FDC469067FFDC4E847C51F452DF");
    BN_hex2bn(&q, "E85CED54AF57E53E092113E62F436F4F");
    BN_hex2bn(&e, "0D88C3");

    // 计算n=pq
    BN_mul(n, p, q, ctx);
    printBN("public key", e, n);

    // 计算phi(n)=(p-1)(q-1)
    BN_sub(p_minus_1, p, BN_value_one()); // BN_value_one()返回1
    BN_sub(q_minus_1, q, BN_value_one());
    BN_mul(phi, p_minus_1, q_minus_1, ctx);

    // 检查e和phi(n)是否互质
    BN_gcd(res, phi, e, ctx);
    if (!BN_is_one(res))
    {
        printf("Error: e and phi(n) is not relatively prime \n ");
        exit(0);
    }

    // 计算d=e^(-1)(mod phi(n))
    BN_mod_inverse(d, e, phi, ctx);
    printBN("private key", d, n);

    // 清空BN_CTX和BIGNUM类型数字
    BN_clear_free(p);
    BN_clear_free(q);
    BN_clear_free(n);
    BN_clear_free(res);
    BN_clear_free(phi);
    BN_clear_free(e);
    BN_clear_free(d);
    BN_clear_free(p_minus_1);
    BN_clear_free(q_minus_1);

    return 0;
}

  • 所以我们已知p,q,e,可以推导出公钥和私钥。

TASK2:加密消息

  • 设(e,n) 为公钥。请加密消息“A top secret!”引文不包括在内)。我们需要将此 ASCII 字符串转换为十六进制字符串,然后使用 hex-to-bn API BN hex2bn() 将十六进制字符串转换为 BIGNUM。以下 python 命令可用于将普通 ASCII 字符串转换为十六进制字符串。

  • 我按照指导书输入以下的指令,发现出现了报错。
    python -c ' print("A top secret!".encode("hex")) ') '

  • 是因为这个命令在 Python2 中是正确的,但在 Python3 中不支持该encode函数和hex函数的使用。正确的 Python3 命令是:
    python3 -c 'import binascii; print(binascii.hexlify(b"A top secret!"))'

  • 在 Python3 中,encode被替换为bytes类型,并且hex函数被替换为binascii.hexlify。因此,我们需要使用binascii.hexlify函数将字节数组编码为十六进制字符串,如上所示。

  • 最后结果如下图所示:

  • 公钥如下所示(十六进制)。我们还提供私钥d,以帮助您验证您的加密结果。

    • n = DCBFFE3E51F62E09CE7032E2677A78946A849DC4CDDE3A4D0CB81629242FB1A5
    • e = `010001 (this hex value equals to decimal 65537)```
    • M = A top secret!
    • d = 74D806F9F3A62BAE331FFE3F0A68AFE35B3D2E4794148AACBC26AA381CD7D30D
编写代码:
#include <stdio.h>
#include <openssl/bn.h>
#define NBITS 128
void printBN(char *msg, BIGNUM *a)
{ /* Use BN_bn2hex(a) for hex string
	* Use BN_bn2dec(a) for decimal string */
    char *number_str = BN_bn2hex(a);
    printf("%s %s\n", msg, number_str);
    OPENSSL_free(number_str);
}
int main()
{
    BN_CTX *ctx = BN_CTX_new();

    BIGNUM *n = BN_new();
    BIGNUM *e = BN_new();
    BIGNUM *d = BN_new();
    BIGNUM *m = BN_new(); //massage
    //BIGNUM* p = BN_new(); //plaintxt
    BIGNUM *c = BN_new(); //cyphertxt

    BN_hex2bn(&n, "DCBFFE3E51F62E09CE7032E2677A78946A849DC4CDDE3A4D0CB81629242FB1A5");
    BN_hex2bn(&e, "010001");
    BN_hex2bn(&d, "74D806F9F3A62BAE331FFE3F0A68AFE35B3D2E4794148AACBC26AA381CD7D30D");
    BN_hex2bn(&m, "4120746f702073656372657421");

    BN_mod_exp(c, m, e, n, ctx);
    //BN_mod_exp(p, c, d, n, ctx);
    printBN("cyphertxt:", c);
    //printBN("plaintxt:", p);
    return 0;
}
  • 运行结果如下:

  • 加密结果为:6FB078DA550B2650832661E14F4F8D2CFAEF475A0DF3A75CACDC5DE5CFC5FADC

TASK3解密消息

  • 此任务中使用的公钥/私钥与task 2中使用的公钥/私钥相同。请解密以下密文C,并将其转换为纯ASCII字符串。
    • C=8C0F971DF2F3672B28811407E2DABBE1DA0FEBBBDFC7DCB67396567EA1E2493F
  • 可以使用以下python命令将十六进制字符串转换为普通ASCII字符串。
    python -c 'print("4120746f702073656372657421".decode("hex"))'
    结果为:A top secret!
  • 但是在python3中,应该使用 bytes.fromhex() 方法将十六进制字符串转换为 bytes 类型,然后再使用 decode() 方法将其转换为字符串,如下所示:
    python3 -c 'print(bytes.fromhex("4120746f702073656372657421").decode("utf-8"))'
编写程序:
#include <stdio.h>
#include <openssl/bn.h>
#define NBITS 128
void printBN(char *msg, BIGNUM *a)
{ /* Use BN_bn2hex(a) for hex string
	* Use BN_bn2dec(a) for decimal string */
    char *number_str = BN_bn2hex(a);
    printf("%s %s\n", msg, number_str);
    OPENSSL_free(number_str);
}
int main()
{
    BN_CTX *ctx = BN_CTX_new();

    BIGNUM *n = BN_new();
    BIGNUM *e = BN_new();
    BIGNUM *d = BN_new();
    BIGNUM *p = BN_new(); //plaintxt
    BIGNUM *c = BN_new(); //cyphertxt

    BN_hex2bn(&n, "DCBFFE3E51F62E09CE7032E2677A78946A849DC4CDDE3A4D0CB81629242FB1A5");
    BN_hex2bn(&e, "010001");
    BN_hex2bn(&d, "74D806F9F3A62BAE331FFE3F0A68AFE35B3D2E4794148AACBC26AA381CD7D30D");
    BN_hex2bn(&c, "8C0F971DF2F3672B28811407E2DABBE1DA0FEBBBDFC7DCB67396567EA1E2493F");

    BN_mod_exp(p, c, d, n, ctx);
    printBN("plaintxt:", p);
    return 0;
}
  • 运行结果如下:

  • 解密结果为:50617373776F72642069732064656573

  • 使用python将其转换成ASCII字符串:

TASK4签名

  • 通过签署数字文件提供真实性证明

  • 在M使用私钥对M应用私钥操作,并获得一个数字,每个人都可以使用我们的公钥来从s回来对于需要签名的消息:
    数字签名=m的d次方 mod n

  • 在实践中,消息可能很长,从而产生长签名和更多的计算时间,因此,我们从原始信息中生成加密哈希值,并且只签署哈希值

  • 此任务中使用的公钥/私钥与TASK2中使用的公钥/私钥相同。请为以下消息生成签名(请直接签名此消息,而不是签名其哈希值):

M = I owe you $2000.

  • 请对M消息做一点小小的修改,比如把2000改成3000,并在修改后的消息上签名。比较两个签名并描述你所观察到的。
    M = I owe you $3000.
  • 将两个签名转换。
  • python3下:
"I owe you $2000".encode("utf-8").hex()
"I owe you $3000".encode("utf-8").hex()

  • 得到:
49206f776520796f75202432303030
49206f776520796f75202433303030
编写程序:
#include <stdio.h>
#include <openssl/bn.h>
#define NBITS 128
void printBN(char *msg, BIGNUM *a)
{ /* Use BN_bn2hex(a) for hex string
	* Use BN_bn2dec(a) for decimal string */
    char *number_str = BN_bn2hex(a);
    printf("%s %s\n", msg, number_str);
    OPENSSL_free(number_str);
}
int main()
{
    BN_CTX *ctx = BN_CTX_new();

    BIGNUM *n = BN_new();
    BIGNUM *e = BN_new();
    BIGNUM *d = BN_new();
    BIGNUM *m1 = BN_new();
    BIGNUM *m2 = BN_new();
    BIGNUM *sig1 = BN_new();
    BIGNUM *sig2 = BN_new();

    BN_hex2bn(&n, "DCBFFE3E51F62E09CE7032E2677A78946A849DC4CDDE3A4D0CB81629242FB1A5");
    BN_hex2bn(&e, "010001");
    BN_hex2bn(&d, "74D806F9F3A62BAE331FFE3F0A68AFE35B3D2E4794148AACBC26AA381CD7D30D");
    BN_hex2bn(&m1, "49206f776520796f75202432303030");
    BN_hex2bn(&m2, "49206f776520796f75202433303030");

    BN_mod_exp(sig1, m1, d, n, ctx);
    BN_mod_exp(sig2, m2, d, n, ctx);
    printBN("signature of m1:", sig1);
    printBN("signature of m2:", sig2);
    return 0;
}
  • 运行:

    • signature of m1: 80A55421D72345AC199836F60D51DC9594E2BDB4AE20C804823FB71660DE7B82
    • signature of m2: 04FC9C53ED7BBE4ED4BE2C24B0BDF7184B96290B4ED4E3959F58E94B1ECEA2EB
  • 可见,虽然信息只做了稍微的改动,签名结果也会发生很大的变化。

TASK5:验证签名

  • Bob收到来自Alice的消息M = "Launch a missile.",其签名为s。我们知道Alice的公钥是(e, n),请验证该签名是否确实是Alice的。公钥和签名(十六进制)如下所示:
    • M = Launch a missile.
    • S = 643D6F34902D9C7EC90CB0B2BCA36C47FA37165C0005CAB026C0542CBDB6802F
    • e = 010001 (this hex value equals to decimal 65537)
    • n = AE1CD4DC432798D933779FBD46C6E1247F0CF1233595113AA51B450F18116115
  • 假设in的签名已损坏,签名的最后一个字节从2F更改为3F,即只有一个比特的更改。请重复这个任务,并描述验证过程会发生什么。
编写代码:
#include <stdio.h>
#include <openssl/bn.h>
#define NBITS 128
void printBN(char *msg, BIGNUM *a)
{ /* Use BN_bn2hex(a) for hex string
	* Use BN_bn2dec(a) for decimal string */
    char *number_str = BN_bn2hex(a);
    printf("%s %s\n", msg, number_str);
    OPENSSL_free(number_str);
}
int main()
{
    BN_CTX *ctx = BN_CTX_new();

    BIGNUM *n = BN_new();
    BIGNUM *e = BN_new();
    BIGNUM *M = BN_new();
    BIGNUM *m1 = BN_new();
    BIGNUM *m2 = BN_new();
    BIGNUM *sig1 = BN_new();
    BIGNUM *sig2 = BN_new();
       %TRHrnT#ARGXB   
    BN_hex2bn(&M, "4c61756e63682061206d697373696c652e");
    BN_hex2bn(&n, "AE1CD4DC432798D933779FBD46C6E1247F0CF1233595113AA51B450F18116115");
    BN_hex2bn(&e, "010001");
    BN_hex2bn(&sig1, "643D6F34902D9C7EC90CB0B2BCA36C47FA37165C0005CAB026C0542CBDB6802F");
    BN_hex2bn(&sig2, "643D6F34902D9C7EC90CB0B2BCA36C47FA37165C0005CAB026C0542CBDB6803F");

    BN_mod_exp(m1, sig1, e, n, ctx);
    BN_mod_exp(m2, sig2, e, n, ctx);

    printf("verifying of signature1:");
    if (BN_cmp(m1, M) == 0)
    {
        printf("valid!\n");
    }
    else
    {
        printf("invalid!\n");
    }
    printf("verifying of signature2:");
    if (BN_cmp(m2, M) == 0)
    {
        printf("valid!\n");
    }
    else
    {
        printf("invalid!\n");
    }

    return 0;
}

代码解释:具体来说,该代码验证两个数字签名sig1和sig2是否由指定的RSA私钥持有者对消息m进行数字签名而得到的。
代码中的主要步骤如下:

创建BN_CTX对象以进行BIGNUM变量的上下文管理。
使用BN_hex2bn()函数将十六进制字符串表示的n、e、M、sig1和sig2解析为BIGNUM类型的变量。
使用BN_mod_exp()函数对sig1和sig2进行数字签名验证,并计算出原始消息的哈希值m1和m2。
使用BN_cmp()函数比较哈希值和原始消息,判断数字签名是否有效。
输出验证结果。

  • 运行代码,结果如下:

TASK6:手动验证X.509证书

  • 在这个任务中,我们将使用我们的程序手动验证X.509证书。509包含关于公钥的数据和数据上的发布者签名。我们将从web服务器下载一个真实的X.509证书,获取其颁发者的公钥,然后使用这个公钥来验证证书上的签名。

  • 首先理解X.509证书的原理

  • X.509是公钥基础设施(PKI)的标准格式。X.509证书就是基于国际电信联盟(ITU)制定的X.509标准的数字证书。X.509证书主要用于识别互联网通信和计算机网络中的身份,保护数据传输安全。X.509证书无处不在,比如我们每天使用的网站、移动应用程序、电子文档以及连接的设备等都有它的身影。

  • X.509标准基于一种被称为抽象语法表示法 (ASN.1)的接口描述语言,这种接口描述语言定义了可以以跨平台方式序列化和反序列化的数据结构。利用ASN,X.509证书格式可以使用公钥和私钥来加密和解密信息。

  • 结构优势

  • X.509证书的结构优势在于它是由公钥和私钥组成的密钥对而构建的。公钥和私钥能够用于加密和解密信息,验证发送者的身份和确保消息本身的安全性。基于X.509的PKI最常见的用例是使用SSL证书让网站与用户之间实现HTTPS安全浏览。X.509协议同样也适用于应用程序安全的代码签名、数字签名和其他重要的互联网协议。

  • X.509证书内容:

  • X.509证书包含有关颁发者CA和证书使用者的信息。X.509证书标准字段包括以下内容:

步骤1:从真实的web服务器下载证书。
  • 我们使用www.example.org服务器这个文档。学生应该选择拥有不同证书的不同的网络服务器
  • 本文档中使用的证书(需要注意的是www.example.com与www.example.org共享相同的证书)。我们可以使用浏览器下载证书,也可以使用以下命令下载证书:
    openssl s_client -connect www.example.org:443 -showcerts
  • 结果如下:

命令执行结果中包含两个证书。证书的主题字段(以s:开头的条目)是www.example.org,也就是说,这是www.example.org的证书。issuer字段(以i:开头的条目)提供了发行者的信息。第二个证书的主题字段与第一个证书的颁发者字段相同。基本上,第二个证书属于中间CA。在本任务中,我们将使用CA的证书来验证服务器证书。
如果使用上面的命令只能获得一个证书,这意味着获得的证书是由根CA签名的。根CA的证书可以从安装在我们预构建的虚拟机中的Firefox浏览器中获得。点击编辑→首选项→隐私,然后点击安全→查看证书。请搜索颁发者名称并下载其证书。
将每个证书(包含“Begin certificate”行和包含“END certificate”行之间的文本,包括这两行)复制并粘贴到一个文件中。我们称第一个为c0.pem和第二个c1.pem。

- 主体信息(s),即证书所认证的实体信息,包括国家/地区、州/省、城市、机构和通用名称等。
- 颁发者(d),即签发该证书的数字证书认证机构(CA)的信息,包括国家/地区、组织和通用名称等。
- 有效期时间范围,即证书的开始时间(s)和结束时间(e)。
- 公钥信息中包含的密钥算法类型,以及公钥值。
- 数字签名信息,包括签名算法类型和数字签名值,用于验证该证书的有效性。
  • 证书链中包含颁发者颁发的一级证书和中间CA证书。这个证书用于验证服务器www.example.org的身份,

  • 将第一个证书复制到文件:c0.pem,无需调整换行,带上
    -----BEGIN CERTIFICATE-----...-----END CERTIFICATE-----

  • 将第二个证书复制到文件:c1.pem

  • 为了避免格式问题,尽量在seed中编辑文件

步骤2:从颁发者的证书中提取公钥(e , n)。
  • Openssl提供了从x509证书中提取某些属性的命令。我们可以用-模求出n的值。没有特定的命令来提取e,但是我们可以打印出所有的字段,并且可以很容易地找到e的值。
For  modulus   (n):
$  openssl  x509  -in  c1.pem  -noout  -modulus
Print  out  all  the  fields,  find  the  exponent   (e):
$  openssl  x509  -in  c1.pem  -text  -noout
  • 使用命令得到公钥的模数n:
  • openssl x509 -in c1.pem -noout -modulus

  • 得到:

Modulus=C14BB3654770BCDD4F58DBEC9CEDC366E51F311354AD4A66461F2C0AEC6407E52EDCDCB90A20EDDFE3C4D09E9AA97A1D8288E51156DB1E9F58C251E72C340D2ED292E156CBF1795FB3BB87CA25037B9A52416610604F571349F0E8376783DFE7D34B674C2251A6DF0E9910ED57517426E27DC7CA622E131B7F238825536FC13458008B84FFF8BEA75849227B96ADA2889B15BCA07CDFE951A8D5B0ED37E236B4824B62B5499AECC767D6E33EF5E3D6125E44F1BF71427D58840380B18101FAF9CA32BBB48E278727C52B74D4A8D697DEC364F9CACE53A256BC78178E490329AEFB494FA415B9CEF25C19576D6B79A72BA2272013B5D03D40D321300793EA99F5

  • 得到,e=0x10001
步骤3:从服务器的证书中提取签名。
  • 没有特定的openssl命令来提取签名字段。但是,我们可以打印出所有的字段,然后复制并粘贴签名
    • (注意:如果证书中使用的签名算法不是基于RSA的,您可以找到另一个证书)。

$ openssl x509 -in c0.pem -text -noout
...
Signature Algorithm: sha256WithRSAEncryption
59:e4:4a:d8:a9:82:ba:9a:4a:f1:63:0c:6d:76:26:75:b3:3c:
74:be:c5:f7:3d:a7:91:92:f8:cf:06:2d:58:10:ed:f3:b8:d6:
fc:6c:ff:13:96:32💿4f:e9:87:24:85:0b:74:a2:c2:f6:0f:
f5:a7:d8:7d:76:8a:ae:e9:c9:58:2b:6e:00:6f:b9💿24:ee:
c4:42:c5:4c:16:85:9d:34:61:39:23:bf:c6:8e:95:c9:84:a9:
b2:e5:41:0f:44:78:d7:95:b9:cf:d9:74:bf:58:4f:e7:16:ff:
...

  • 我们需要从数据中删除空格和冒号,这样就可以得到一个十六进制字符串,并将其输入到程序中。下面的命令可以实现这个目标。tr命令是一个用于字符串操作的Linux实用工具。在本例中,-d选项用于删除数据的":"和"space"。
    cat signature | tr -d [:space:]:

  • 得到signature:

59e44ad8a982ba9a4af1630c6d762675b33c74bec5f73da79192f8cf062d5810edf3b8d6fc6cff139632cd4fe98724850b74a2c2f60ff5a7d87d768aaee9c9582b6e006fb9cd24eec442c54c16859d34613923bfc68e95c984a9b2e5410f4478d795b9cfd974bf584fe716ff7c4030c46c4e224dcb83673a93bf2bc5c59c1af243a1253b84f6f7536ea885aede14749130060df207d4c408ba4364c5e23fdaacc541afa437e8427674f713bb4a7d3659819bc744df8973b93342e860c24d615d125a10f6efff33891450e8d69fc6b95c2b35dbadeddd36b625f2958aac693f9afe1af815286dea185ac2d26218af4078b5fa5e098f53f9ccf823a1833123f4c6

步骤4:提取服务器证书的主体

证书颁发机构(CA)首先计算证书的哈希值,然后对哈希值进行签名,从而为服务器证书生成签名。为了验证签名,我们还需要从证书生成散列。由于哈希是在计算签名之前生成的,所以在计算哈希时需要排除证书的签名块。如果不能很好地理解证书的格式,那么要找出证书的哪一部分用于生成散列是非常困难的。

509证书是使用ASN.1(抽象语法符号1)标准编码的,所以如果我们能解析ASN.1结构,我们就能很容易地从证书中提取任何字段。Openssl有一个名为asn1parse的命令,可以用来解析X.509证书。

  • 下面我们来解析一下:
0:d=0  hl=4 l=1866 cons: SEQUENCE          
    4:d=1  hl=4 l=1586 cons:  SEQUENCE          
    8:d=2  hl=2 l=   3 cons:   cont [ 0 ]        
   10:d=3  hl=2 l=   1 prim:    INTEGER           :02
   13:d=2  hl=2 l=  16 prim:   INTEGER           :0C1FCB184518C7E3866741236D6B73F1
   31:d=2  hl=2 l=  13 cons:   SEQUENCE          
   33:d=3  hl=2 l=   9 prim:    OBJECT            :sha256WithRSAEncryption
   44:d=3  hl=2 l=   0 prim:    NULL              
   46:d=2  hl=2 l=  79 cons:   SEQUENCE          
   48:d=3  hl=2 l=  11 cons:    SET               
   50:d=4  hl=2 l=   9 cons:     SEQUENCE          
   52:d=5  hl=2 l=   3 prim:      OBJECT            :countryName
   57:d=5  hl=2 l=   2 prim:      PRINTABLESTRING   :US
   61:d=3  hl=2 l=  21 cons:    SET               
   63:d=4  hl=2 l=  19 cons:     SEQUENCE          
   65:d=5  hl=2 l=   3 prim:      OBJECT            :organizationName
   70:d=5  hl=2 l=  12 prim:      PRINTABLESTRING   :DigiCert Inc
   84:d=3  hl=2 l=  41 cons:    SET               
   86:d=4  hl=2 l=  39 cons:     SEQUENCE          
   88:d=5  hl=2 l=   3 prim:      OBJECT            :commonName
  • 这是一个 X.509 证书的 DER 编码数据。下面是对各个字段的简单解释:

    • 第 1 行:SEQUENCE,整个 DER 编码数据的最外层结构。
    • 第 2 ~ 7 行:SEQUENCE,证书的 TBS(To Be Signed)部分。从SEQUENCE开始的字段是用来生成哈希的证书的主体.
    • 第 4 行:cont [ 0 ],版本信息。该证书的版本为 2。
    • 第 5 行:INTEGER,证书的序列号。
    • 第 6 行:SEQUENCE,证书签名算法的标识。
    • 第 7 行:NULL,证书签名算法的参数。
    • 第 8 ~ 13 行:SEQUENCE,证书的持有者信息(Issuer)。
    • 第 14 ~ 16 行:UTCTIME,证书的起始时间。
    • 第 17 ~ 19 行:UTCTIME,证书的终止时间。
    • 第 20 ~ 最后行:SEQUENCE,证书的主体信息(Subject)。
  • Issuer 和 Subject 字段中,分别包含了国家/地区名(countryName)、组织名(organizationName)、通用名称(commonName)等信息。这些信息一般用于证书的验证和识别,以确保证书的合法性和安全性。

  • 对于X.509证书,起始偏移量总是相同的(即4),但结束偏移量取决于证书的内容长度。我们可以使用-strparse选项从偏移量4获取字段,这将为我们提供证书的主体,不包括签名块。

$ openssl asn1parse -i -in c0.pem -strparse 4 -out c0_body.bin -noout

  • 一旦我们获得证书的主体,我们可以使用以下命令计算它的哈希值:
    $ sha256sum c0_body.bin
  • 使用命令解析服务器的证书:
    openssl asn1parse -i -in c0.pem
  • 用来生成哈希的证书的主体.哈希值为:
    bbc2a75949c896bd66db4e636aab8b2cbaa970bc8302d8d02c99104ab04f4dd6
步骤5:验证签名。
  • 现在我们拥有了所有信息,包括CA的公钥、CA的签名和服务器证书的主体。我们可以运行自己的程序来验证签名是否有效。Openssl确实为我们提供了一个验证证书的命令,但要求学生使用自己的程序来验证证书,否则,他们在这项任务中得到的分数为零。

  • 原理如下:

1.用签名方提供的公钥中的模数n和指数e,构造RSA公钥。
2.对于接收方收到的消息m和数字签名sig,使用RSA公钥对数字签名sig进行解密,得到签名的明文m’。
3.将明文m’与收到的消息m进行比较,如果它们相等,则表明数字签名sig是由私钥持有者对消息m进行数字签名而得到的;否则,数字签名sig是无效的。
#include <stdio.h>
#include <openssl/bn.h>
#define NBITS 128
void printBN(char *msg, BIGNUM *a)
{ /* Use BN_bn2hex(a) for hex string
	* Use BN_bn2dec(a) for decimal string */
    char *number_str = BN_bn2hex(a);
    printf("%s %s\n", msg, number_str);
    OPENSSL_free(number_str);
}
int main()
{
    BN_CTX *ctx = BN_CTX_new();

    BIGNUM *n = BN_new();
    BIGNUM *e = BN_new();
    BIGNUM *sig = BN_new();
    BIGNUM *m = BN_new();

    BN_hex2bn(&n, "C14BB3654770BCDD4F58DBEC9CEDC366E51F311354AD4A66461F2C0AEC6407E52EDCDCB90A20EDDFE3C4D09E9AA97A1D8288E51156DB1E9F58C251E72C340D2ED292E156CBF1795FB3BB87CA25037B9A52416610604F571349F0E8376783DFE7D34B674C2251A6DF0E9910ED57517426E27DC7CA622E131B7F238825536FC13458008B84FFF8BEA75849227B96ADA2889B15BCA07CDFE951A8D5B0ED37E236B4824B62B5499AECC767D6E33EF5E3D6125E44F1BF71427D58840380B18101FAF9CA32BBB48E278727C52B74D4A8D697DEC364F9CACE53A256BC78178E490329AEFB494FA415B9CEF25C19576D6B79A72BA2272013B5D03D40D321300793EA99F5");
    BN_hex2bn(&e, "010001");
    BN_hex2bn(&sig, "59e44ad8a982ba9a4af1630c6d762675b33c74bec5f73da79192f8cf062d5810edf3b8d6fc6cff139632cd4fe98724850b74a2c2f60ff5a7d87d768aaee9c9582b6e006fb9cd24eec442c54c16859d34613923bfc68e95c984a9b2e5410f4478d795b9cfd974bf584fe716ff7c4030c46c4e224dcb83673a93bf2bc5c59c1af243a1253b84f6f7536ea885aede14749130060df207d4c408ba4364c5e23fdaacc541afa437e8427674f713bb4a7d3659819bc744df8973b93342e860c24d615d125a10f6efff33891450e8d69fc6b95c2b35dbadeddd36b625f2958aac693f9afe1af815286dea185ac2d26218af4078b5fa5e098f53f9ccf823a1833123f4c6");

    BN_mod_exp(m, sig, e, n, ctx);

    printBN("message:", m);

    return 0;
}
  • 运行结果如下:

message: 01FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF003031300D060960864801650304020105000420BBC2A75949C896BD66DB4E636AAB8B2CBAA970BC8302D8D02C99104AB04F4DD6

  • 根据sha256WithRSAEncryption可知,发放证书用到的hash算法是sha256,即得到的hash值共有256位,256/4=64。再根据于X.509证书签名验证的流程可知,只需要将上述结果的后64个十六进制数与step4得到的hash值做比较即可。
    hash值为: BBC2A75949C896BD66DB4E636AAB8B2CBAA970BC8302D8D02C99104AB04F4DD
  • 用python进行验证: