走读OpenSSL代码从一张奇怪的证书说起(三)

本节我们开始查看OpenSSL代码,我们的目标仅限于找出证书验证失败的原因
而不是把整套代码完全弄懂或精读,故采用走读代码的方式。所用的版本是OpenSSL 0.9.8e
从哪里下手呢?就从其输出的错误信息开始吧。
5840:error:04077068:rsa routines:RSA_verify:bad signature:.\crypto\rsa\rsa_sign.c:235
打开VC工程,在rsa_sign.c文件的235行处下断点,并设置好命令参数和工作目录,按F5,程序中断

View Code
  1 int RSA_verify(int dtype, const unsigned char *m, unsigned int m_len,
2 unsigned char *sigbuf, unsigned int siglen, RSA *rsa)
3 {
4 int i,ret=0,sigtype;
5 unsigned char *s;
6 X509_SIG *sig=NULL;
7
8 if (siglen != (unsigned int)RSA_size(rsa))
9 {
10 RSAerr(RSA_F_RSA_VERIFY,RSA_R_WRONG_SIGNATURE_LENGTH);
11 return(0);
12 }
13
14 if((rsa->flags & RSA_FLAG_SIGN_VER) && rsa->meth->rsa_verify)
15 {
16 return rsa->meth->rsa_verify(dtype, m, m_len,
17 sigbuf, siglen, rsa);
18 }
19
20 s=(unsigned char *)OPENSSL_malloc((unsigned int)siglen);
21 if (s == NULL)
22 {
23 RSAerr(RSA_F_RSA_VERIFY,ERR_R_MALLOC_FAILURE);
24 goto err;
25 }
26 if((dtype == NID_md5_sha1) && (m_len != SSL_SIG_LENGTH) ) {
27 RSAerr(RSA_F_RSA_VERIFY,RSA_R_INVALID_MESSAGE_LENGTH);
28 goto err;
29 }
30 i=RSA_public_decrypt((int)siglen,sigbuf,s,rsa,RSA_PKCS1_PADDING);
31
32 if (i <= 0) goto err;
33
34 /* Special case: SSL signature */
35 if(dtype == NID_md5_sha1) {
36 if((i != SSL_SIG_LENGTH) || memcmp(s, m, SSL_SIG_LENGTH))
37 RSAerr(RSA_F_RSA_VERIFY,RSA_R_BAD_SIGNATURE);
38 else ret = 1;
39 } else {
40 const unsigned char *p=s;
41 sig=d2i_X509_SIG(NULL,&p,(long)i);
42
43 if (sig == NULL) goto err;
44
45 /* Excess data can be used to create forgeries */
46 if(p != s+i)
47 {
48 RSAerr(RSA_F_RSA_VERIFY,RSA_R_BAD_SIGNATURE);
49 goto err;
50 }
51
52 /* Parameters to the signature algorithm can also be used to
53 create forgeries */
54 if(sig->algor->parameter
55 && ASN1_TYPE_get(sig->algor->parameter) != V_ASN1_NULL)
56 {
57 RSAerr(RSA_F_RSA_VERIFY,RSA_R_BAD_SIGNATURE);
58 goto err;
59 }
60
61 sigtype=OBJ_obj2nid(sig->algor->algorithm);
62
63
64 #ifdef RSA_DEBUG
65 /* put a backward compatibility flag in EAY */
66 fprintf(stderr,"in(%s) expect(%s)\n",OBJ_nid2ln(sigtype),
67 OBJ_nid2ln(dtype));
68 #endif
69 if (sigtype != dtype)
70 {
71 if (((dtype == NID_md5) &&
72 (sigtype == NID_md5WithRSAEncryption)) ||
73 ((dtype == NID_md2) &&
74 (sigtype == NID_md2WithRSAEncryption)))
75 {
76 /* ok, we will let it through */
77 #if !defined(OPENSSL_NO_STDIO) && !defined(OPENSSL_SYS_WIN16)
78 fprintf(stderr,"signature has problems, re-make with post SSLeay045\n");
79 #endif
80 }
81 else
82 {
83 RSAerr(RSA_F_RSA_VERIFY,
84 RSA_R_ALGORITHM_MISMATCH);
85 goto err;
86 }
87 }
88 if ( ((unsigned int)sig->digest->length != m_len) ||
89 (memcmp(m,sig->digest->data,m_len) != 0))
90 {
91 RSAerr(RSA_F_RSA_VERIFY,RSA_R_BAD_SIGNATURE);
92 }
93 else
94 ret=1;
95 }
96 err:
97 if (sig != NULL) X509_SIG_free(sig);
98 if (s != NULL)
99 {
100 OPENSSL_cleanse(s,(unsigned int)siglen);
101 OPENSSL_free(s);
102 }
103 return(ret);
104 }

我们只摘抄了部分代码,源文件中的235行相当于上面的91行(后面的行号都用部分代码中的显示行号)
同时给出此时的函数调用栈如下:
> openssl.exe!RSA_verify
  openssl.exe!EVP_VerifyFinal
  openssl.exe!ASN1_item_verify
  openssl.exe!X509_verify
  openssl.exe!internal_verify
  openssl.exe!X509_verify_cert
  openssl.exe!check
  openssl.exe!verify_main
  openssl.exe!do_cmd
  openssl.exe!main
走到91行,是因为触发了88行的if条件,查看变量,发现 sig->digest->length 与 m_len 相等,都是20
因此推断是 sig->digest->data 与 m 不相等导致证书验证错误。为什么这两项不等会导致出错?
我们回忆下证书验证的公式
(证书的签名部分signatureValue)e=SHA1(证书的待签名部分tbsCertificate)(mod CA的公钥模)
只有左右两边的值相等才认为证书是正常的。
眼前的代码告诉我们 sig->digest->data 与 m 不相等导致失败,我们自然猜测
sig->digest->data 与 m 分别表示验证公式的两边部分(只是由于它们的值不等,才导致验证失败)
接下来就要确定----sig->digest->data 和 m,谁是验证公式的左边项,谁是右边项。
先看变量 m,是函数RSA_verify的入参,暂时看不出来。
再转向变量 sig->digest->data,它来源于41行的函数d2i_X509_SIG的入参p
p(来自s)又是从函数RSA_public_decrypt得到(30行),RSA_public_decrypt是干什么的?
从其函数名看,是用RSA公钥进行“解密”,对于RSA算法,既然用到公钥,只能是用e进行模幂计算。
到此,我们可以初步判断,sig->digest->data 应该是公式左边项,从而 m 应该是公式右边项。
注意这是判断,究竟对不对还需要事实验证(当然后面的结果表明,我们的判断是正确的)。
用VC的内存查看功能查看 sig->digest->data 的内容
sig->digest->data: 11 12 f6 24 22 52 f0 b8 fd 59 33 1a 1e 5f 50 ca 5a 7c a1 11(十六进制)
这恰恰就是SHA1(tbsCertificate)----不记得的同学请看上一节----说明公式左边项计算正确。从而
m 代表的右边公式项可能计算有误。
继续追踪 m,作为函数RSA_verify的入参,m 由调用函数 EVP_VerifyFinal 提供

View Code
 1 int EVP_VerifyFinal(EVP_MD_CTX *ctx, const unsigned char *sigbuf,
2 unsigned int siglen, EVP_PKEY *pkey)
3 {
4 unsigned char m[EVP_MAX_MD_SIZE];
5 unsigned int m_len;
6 int i,ok=0,v;
7 MS_STATIC EVP_MD_CTX tmp_ctx;
8
9 for (i=0; i<4; i++)
10 {
11 v=ctx->digest->required_pkey_type[i];
12 if (v == 0) break;
13 if (pkey->type == v)
14 {
15 ok=1;
16 break;
17 }
18 }
19 if (!ok)
20 {
21 EVPerr(EVP_F_EVP_VERIFYFINAL,EVP_R_WRONG_PUBLIC_KEY_TYPE);
22 return(-1);
23 }
24 EVP_MD_CTX_init(&tmp_ctx);
25 EVP_MD_CTX_copy_ex(&tmp_ctx,ctx);
26 EVP_DigestFinal_ex(&tmp_ctx,&(m[0]),&m_len);
27 EVP_MD_CTX_cleanup(&tmp_ctx);
28 if (ctx->digest->verify == NULL)
29 {
30 EVPerr(EVP_F_EVP_VERIFYFINAL,EVP_R_NO_VERIFY_FUNCTION_CONFIGURED);
31 return(0);
32 }
33
34 return(ctx->digest->verify(ctx->digest->type,m,m_len,
35 sigbuf,siglen,pkey->pkey.ptr));
36 }

34行即是调用 RSA_verify 的地方(以函数指针的方式),m 的内容由 EVP_DigestFinal_ex 函数(26行)得到
EVP_DigestFinal_ex 是摘要计算函数,而25行语句告诉我们,摘要计算的上下文数据结构ctx来自tmp_ctx的复制
换句话说,ctx保存了之前tmp_ctx对某些消息进行摘要计算的中间状态。
我们再看tmp_ctx,它是函数EVP_VerifyFinal的入参,后者又是由函数ASN1_item_verify调用

View Code
 1 int ASN1_item_verify(const ASN1_ITEM *it, X509_ALGOR *a, ASN1_BIT_STRING *signature,
2 void *asn, EVP_PKEY *pkey)
3 {
4 EVP_MD_CTX ctx;
5 const EVP_MD *type;
6 unsigned char *buf_in=NULL;
7 int ret= -1,i,inl;
8
9 EVP_MD_CTX_init(&ctx);
10 i=OBJ_obj2nid(a->algorithm);
11 type=EVP_get_digestbyname(OBJ_nid2sn(i));
12 if (type == NULL)
13 {
14 ASN1err(ASN1_F_ASN1_ITEM_VERIFY,ASN1_R_UNKNOWN_MESSAGE_DIGEST_ALGORITHM);
15 goto err;
16 }
17
18 if (!EVP_VerifyInit_ex(&ctx,type, NULL))
19 {
20 ASN1err(ASN1_F_ASN1_ITEM_VERIFY,ERR_R_EVP_LIB);
21 ret=0;
22 goto err;
23 }
24
25 inl = ASN1_item_i2d(asn, &buf_in, it);
26
27 if (buf_in == NULL)
28 {
29 ASN1err(ASN1_F_ASN1_ITEM_VERIFY,ERR_R_MALLOC_FAILURE);
30 goto err;
31 }
32
33 EVP_VerifyUpdate(&ctx,(unsigned char *)buf_in,inl);
34
35 OPENSSL_cleanse(buf_in,(unsigned int)inl);
36 OPENSSL_free(buf_in);
37
38 if (EVP_VerifyFinal(&ctx,(unsigned char *)signature->data,
39 (unsigned int)signature->length,pkey) <= 0)
40 {
41 ASN1err(ASN1_F_ASN1_ITEM_VERIFY,ERR_R_EVP_LIB);
42 ret=0;
43 goto err;
44 }
45 /* we don't need to zero the 'ctx' because we just checked
46 * public information */
47 /* memset(&ctx,0,sizeof(ctx)); */
48 ret=1;
49 err:
50 EVP_MD_CTX_cleanup(&ctx);
51 return(ret);
52 }

果不其然,33行 EVP_VerifyUpdate(&ctx,(unsigned char *)buf_in,inl); 表示进行摘要生成(HASH)操作
buf_in,inl分别是被HASH的内容和长度。
前面我们猜测,m表示验证公式右侧,即SHA1(证书的待签名部分tbsCertificate),到此处终于得到了印证
我们自然想到,buf_in就是证书的tbsCertificate字段,inl就是tbsCertificate部分的长度。
在调用堆栈窗口双击鼠标,切换到ASN1_item_verify的函数栈
再在内存查看窗口输入buf_in回车,下面是显示的部分 buf_in 内容(同时可以查看inl为511)
 30 21 30 09 06 05 2b 0e 03 02 1a 05 00 04 14 11  0!0...+.........
 12 f6 24 22 52 f0 b8 fd 59 33 1a 1e 5f 50 ca 5a  ..$"R...Y3.._P.Z
 7c a1 11 cd cd cd cd cd cd cd cd cd cd cd cd cd  |...............
 cd cd cd cd cd cd cd cd cd cd cd cd cd cd cd cd  ................
 cd cd cd cd cd cd cd cd cd cd cd cd cd cd cd cd  ................
 ......
出现了很多字节0xCD,熟悉VC的朋友应该知道,在调试版本中,未初始化的内存空间会被置成0xCD
看起来根本不像证书的tbsCertificate内容,为什么会这样?难道我们的猜测有误?
仔细看下代码,变量buf_in是个指针,它在25行 inl = ASN1_item_i2d(asn, &buf_in, it); 中作为出参
指向的是一个临时缓冲区(后面的语句 OPENSSL_free(buf_in);已经说明)
而 EVP_VerifyUpdate 正是对这个临时缓冲区进行HASH操作。
由于当前的执行点已经过了33行(还记得吗?执行点在断点处)
所以buf_in所指的区域已经不是当时被HASH的内容,而是别的缓冲区。
我们如何要看到真正的buf_in,很简单,在33行设置断点,并重新启动调试程序
(VC就是方便,看不清楚,可以重新再来),再让程序断在这行
buf_in内容如下
 30 82 02 2b a0 03 02 01 02 02 09 00 9d a2 42 4a  0..+..........BJ
 a2 6a 51 de 30 0d 06 09 2a 86 48 86 f7 0d 01 01  .jQ.0...*.H.....
 05 05 00 30 4b 31 0b 30 09 06 03 55 04 06 13 02  ...0K1.0...U....
 43 4e 31 0b 30 09 06 03 55 04 08 13 02 42 4a 31  CN1.0...U....BJ1
 14 30 12 06 03 55 04 0a 13 0b 4e 65 74 53 65 63  .0...U....NetSec
 75 72 69 74 79 31 0c 30 0a 06 03 55 04 0b 13 03  urity1.0...U....
 53 53 4c 31 0b 30 09 06 03 55 04 03 13 02 43 41  SSL1.0...U....CA
 30 1e 17 0d 31 31 31 30 32 39 31 36 33 36 34 37  0...111029163647
 5a 17 0d 31 34 31 30 32 38 31 36 33 36 34 37 5a  Z..141028163647Z
 30 4b 31 0b 30 09 06 03 55 04 06 13 02 43 4e 31  0K1.0...U....CN1
 0b 30 09 06 03 55 04 08 13 02 42 4a 31 14 30 12  .0...U....BJ1.0.
 06 03 55 04 0a 13 0b 4e 65 74 53 65 63 75 72 69  ..U....NetSecuri
 74 79 31 0c 30 0a 06 03 55 04 0b 13 03 53 53 4c  ty1.0...U....SSL
 31 0b 30 09 06 03 55 04 03 13 02 43 41 30 81 9f  1.0...U....CA0..
 ......
 4e 65 74 53 65 63 75 72 69 74 79 31 0c 30 0a 06  NetSecurity1.0..
 03 55 04 0b 13 03 53 53 4c 31 0b 30 09 06 03 55  .U....SSL1.0...U
 04 03 13 02 43 41 82 09 00 9d a2 42 4a a2 6a 51  ....CA.....BJ.jQ
 de 30 0c 06 03 55 1d 13 04 05 30 03 01 01 ff     .0...U....0....
但查看变量inl,长度变成了559,不是当初的511,这是为什么呢?请看下节

原文地址:https://www.cnblogs.com/efzju/p/2397104.html