本节中我们快速浏览一下证书验证的主干代码。读者可以采用上节中生成的VC工程进行验证。

下面列出关键部分代码,为方便阅读,仅保留与证书验证强相关的代码,去掉了诸如变量定义、错误处理、资源释放等非主要代码,并修改了排版格式。

// 初始入口为 apps\verify.c 中的 MAIN 函数
// 为利于代码阅读,下面尝试将相关代码放在一起(采用函数调用栈的形式,被调用函数的代码排版缩进一层),希望能讲得更为清楚
int MAIN(int argc, char **argv)
{X509_STORE *cert_ctx=NULL;X509_LOOKUP *lookup=NULL;cert_ctx=X509_STORE_new(); // 创建 X509 证书库// 解析命令行参数// 创建 X509_LOOKUP, 该结构的 store_ctx 成员关联刚刚创建的证书库 cert_ctxlookup=X509_STORE_add_lookup(cert_ctx,X509_LOOKUP_file());// 省去前行的 if (CAfile)i=X509_LOOKUP_load_file(lookup,CAfile,X509_FILETYPE_PEM); // 所有证书都是 PEM 格式// 实际上是宏 -- #define X509_LOOKUP_load_file(x,name,type) X509_LOOKUP_ctrl((x),X509_L_FILE_LOAD,(name),(long)(type),NULL)// 原型: int X509_LOOKUP_ctrl(X509_LOOKUP *ctx, int cmd, const char *argc, long argl, char **ret) -- 解析CA文件, 一个文件中可以包含多个CA证书{return ctx->method->ctrl(ctx,cmd,argc,argl,ret);// 函数指针实际指向 by_file_ctrl 函数// 原型: static int by_file_ctrl(X509_LOOKUP *ctx, int cmd, const char *argp, long argl, char **ret){ok = (X509_load_cert_crl_file(ctx,argp,X509_FILETYPE_PEM) != 0);// 原型: int X509_load_cert_crl_file(X509_LOOKUP *ctx, const char *file, int type){STACK_OF(X509_INFO) *inf;X509_INFO *itmp;BIO *in;int i, count = 0;in = BIO_new_file(file, "r");inf = PEM_X509_INFO_read_bio(in, NULL, NULL, NULL); // 创建 STACK_OF(X509_INFO), 以文件中 CA 证书的出现顺序压栈// 原型: STACK_OF(X509_INFO) *PEM_X509_INFO_read_bio(BIO *bp, STACK_OF(X509_INFO) *sk, pem_password_cb *cb, void *u){X509_INFO *xi=NULL;STACK_OF(X509_INFO) *ret=NULL;ret = sk_X509_INFO_new_null() // 创建 X509_INFO 证书栈 retxi = X509_INFO_new() // 创建 X509_INFO, 其成员 xi->x509 == NULL, 为进入下面的 for 循环作准备for(;;){i = PEM_read_bio(bp,&name,&header,&data,&len); // 从 PEM 文件读证书, 一次读入一个证书(文件中可以包含多个证书)start:// 省略其他不相关的 if 分支if (   (strcmp(name,PEM_STRING_X509) == 0)|| (strcmp(name,PEM_STRING_X509_OLD) == 0)) // 发现是证书类型: name == "BEGIN CERTIFICATE", 来自 -----BEGIN CERTIFICATE-----{d2i=(D2I_OF(void))d2i_X509; // 证书信息内部转换格式函数 d2i_X509if (xi->x509 != NULL) // 上一次循环中已解析过证书,首次调用 PEM_read_bio 则不进入{sk_X509_INFO_push(ret,xi) // 将已解析的证书信息压栈 retxi=X509_INFO_new() // 重新分配 X509_INFO,为后面的 d2i 调用做准备goto start; // 跳转回去重新执行}pp=&(xi->x509); // 设置出参}...... // 省略不相关代码if (d2i != NULL){p=data;d2i(pp,&p,len) // 调用 d2i_X509 将证书信息转化为内部格式(结果放在 xi->x509 中)}......}sk_X509_INFO_push(ret,xi) // 最后一个证书压栈 retok=1;......return(ret); // 返回 CA 证书栈}for(i = 0; i < sk_X509_INFO_num(inf); i++) { // 将栈中的证书加入到 X509_STOREitmp = sk_X509_INFO_value(inf, i);if(itmp->x509) {X509_STORE_add_cert(ctx->store_ctx, itmp->x509);// 将 X509_INFO 中的 X509 用 X509_OBJECT 形式封装,压栈到 X509_STORE 的成员 objscount++;}}return count; // 返回从 const char *file 读出的证书个数}}}// 省去 for (i=0; i<argc; i++) -- argc 为待验证证书个数check(cert_ctx,argv[i], untrusted, trusted, purpose, e); // 证书验证函数, argv[i] 指向当前要验证的证书// 原型: static int check(X509_STORE *ctx, char *file, STACK_OF(X509) *uchain, STACK_OF(X509) *tchain, int purpose, ENGINE *e){X509 *x=NULL;int i=0,ret=0;X509_STORE_CTX *csc;x = load_cert(bio_err, file, FORMAT_PEM, NULL, e, "certificate file"); // 待验证证书转化为 X509 结构csc = X509_STORE_CTX_new(); // 创建证书库上下文结构 X509_STORE_CTXX509_STORE_CTX_init(csc,ctx,x,uchain) // 关联 X509_STORE(其 objs 成员包含 CA 证书)和待验证证书(X509_STORE_CTX.cert 成员)i=X509_verify_cert(csc); // 验证 -- 如果校验成功 打印 "OK\n", 否则 ERR_print_errors(bio_err);}
}

验证证书的重任落在函数 X509_verify_cert 身上,我们单独把该函数拎出来,进行讲解。

int X509_verify_cert(X509_STORE_CTX *ctx)
{sk_X509_push(ctx->chain,ctx->cert);// ctx->cert 作为不信任证书压入 ctx->chain// STACK_OF(X509) *chain 将被构造为证书链, 并最终送到 internal_verify() 中去验证, 链内容如下//   data[0] -- 待验证证书 ctx->cert          位置称呼//   data[1] -- 签发 data[0] 的上级 CA 证书   证书链链首(最底端)//   data[2] -- 签发 data[1] 的上级 CA 证书//   ...//   data[n] -- 自签名证书(根 CA 证书)        证书链链尾(最顶端)(最后一张)if (ctx->untrusted != NULL) // 如果有不信任证书列表(例如在 SSL 连接中获取的对端证书), 复制一份sktmp=sk_X509_dup(ctx->untrusted); // verify 命令后跟的 -untrusted 参数也会填充 ctx->untrustednum=sk_X509_num(ctx->chain); // num == 1x=sk_X509_value(ctx->chain,num-1); // 取出被验证证书(位于 chain 证书链链首)for (;;) // 如果有不信任证书列表, 继续构造(延长) chain 证书链 -- 不信任证书1, 不信任证书2 ...{if (ctx->check_issued(ctx, x,x)) break; // 当前证书是自签名证书(已到达证书链最顶端), 退出if (ctx->untrusted != NULL) // 存在不信任证书列表{xtmp=find_issuer(ctx, sktmp,x); // 当前证书是否由 不信任证书列表中的证书 颁发if (xtmp != NULL) // 当前证书 是由 不信任证书(CA证书) 颁发{sk_X509_push(ctx->chain,xtmp); // 将不信任的 CA 证书加入 chain 证书链x=xtmp; // 置 CA 证书为当前证书continue; // 下一轮循环}}break; // 不存在不信任证书列表 或 存在不信任证书列表但 chain 证书链增长到顶(找不到上级 CA)}// 检查 chain 证书链的最顶端证书i=sk_X509_num(ctx->chain);x=sk_X509_value(ctx->chain,i-1);if (ctx->check_issued(ctx, x, x)) // 最顶端证书是自签名证书{if (sk_X509_num(ctx->chain) == 1) // 证书链中只有一张证书, 此时只能是被验证证书, 而且是自签名证书{ok = ctx->get_issuer(&xtmp, ctx, x); // 在信任证书列表 X509_STORE *ctx 中查找证书 xtmp, 满足: xtmp 签发 被验证的自签名证书if ((ok <= 0) || X509_cmp(x, xtmp))  // 没找到(ok <= 0) 或者 虽然找到但 xtmp 与 x 不是同一本证书{// 失败回调函数 -- self signed certificate}else{x = xtmp;sk_X509_set(ctx->chain, i - 1, x); // 用信任证书替换被验证证书(实际为同一张证书)}}else // 证书链的最顶端是自签名证书 且 证书链长度>1, 剔除自签名证书 -- 不相信对方传来的自签名证书{chain_ss=sk_X509_pop(ctx->chain); // 弹出自签名证书ctx->last_untrusted--;num--;x=sk_X509_value(ctx->chain,num-1); // x 是弹出后证书链上的最顶端证书}}// 利用信任证书列表, 继续构建 chain 证书链 -- 不信任证书1 ... 不信任证书n, 信任证书1, 信任证书2 ...for (;;) // x 指向当前待验证证书{if (ctx->check_issued(ctx,x,x)) break; // x 是自签名证书, 退出循环ok = ctx->get_issuer(&xtmp, ctx, x); // 在 信任证书列表中 查找 x 的上级 CA 证书if (ok < 0) return ok; // 出错,返回if (ok == 0) break; // 没找到,退出循环x = xtmp; // 上级 CA 证书设置为当前证书sk_X509_push(ctx->chain,x) // 上级 CA 证书压栈}// 检查 chain 是否构成一条完整的证书链 -- x 是证书链最后一张证书if (!ctx->check_issued(ctx,x,x)) // 证书链不完整 -- x 不是自签名证书{if ((chain_ss == NULL) || !ctx->check_issued(ctx, x, chain_ss)){   // [剔除的自签名证书 chain_ss 不存在] 或 [chain_ss 存在但未签发证书 x] -- chain_ss 见上面注释// 这两种情况都导致 chain 无法构成完整的证书链if (ctx->last_untrusted >= num) // ctx->last_untrusted -- 链中不信任证书总数, num -- 链中证书总数(信任与不信任总和)// 错误信息: unable to get local issuer certificate -- 命令[openssl verify openssl.cert.verify.error.pem]将走到此处else// 错误信息: unable to get issuer certificate// 如果证书链结构为: 根CA-->subca.pem-->user.pem, 则命令[openssl verify -CAfile subca.pem user.pem]将走到此处}else // 剔除的自签名证书 chain_ss 签发了证书 x{sk_X509_push(ctx->chain,chain_ss); // chain_ss 重新加到链中, 错误信息: self signed certificate in certificate chain// 命令[openssl verify -untrusted cacert.pem openssl.cert.verify.error.pem]将走到此处}ok=cb(0,ctx); // 错误回调函数}// 现在已经拥有完整的证书链, 作进一步的检查check_chain_extensions(ctx);check_trust(ctx);X509_get_pubkey_parameters(NULL,ctx->chain);ctx->check_revocation(ctx);// 前面的工作是构造证书链, 下面开始验证证书链if (ctx->verify != NULL)ok=ctx->verify(ctx); // 实际上调用的是 internal_verifyelseok=internal_verify(ctx); // internal_verify 验证 ctx->chain 证书链......
}

函数 X509_verify_cert 的一大功能是构造证书链(被验证证书 <-- 不信任证书列表 <-- 信任证书列表)
构造完成后, X509_verify_cert 调用函数 internal_verify 对证书链进行验证,见下面的说明

static int internal_verify(X509_STORE_CTX *ctx) // 验证证书链 ctx->chain
{n=sk_X509_num(ctx->chain); // chain 是证书堆栈(证书链)n--;                       // 索引从小到大顺序: 被验证证书-->根 CA(自签名)证书xi=sk_X509_value(ctx->chain,n);if (ctx->check_issued(ctx, xi, xi)) // 最顶端为根 CA 证书xs=xi; // 自签名证书: Subject == Issuerelse // 按理说不会走到 else 分支{    // 因为调用者 X509_verify_cert 已经保证: if (!ctx->check_issued(ctx,x,x))if (n <= 0){// 出错处理: 无法验证叶子证书}else{n--; // 取下级证书xs=sk_X509_value(ctx->chain,n);}}while (n >= 0) // 从证书链最顶端开始, 逐层向下验证, 直到链终端的用户证书{if (!xs->valid) // 如果当前证书未经验证{pkey=X509_get_pubkey(xi) // 取得颁发者证书的公钥X509_verify(xs,pkey) // 用公钥验证证书 -- 如果验证失败将返回 0}xs->valid = 1; // 验证通过后打上标记ok = check_cert_time(ctx, xs); // 检查有效时间n--;if (n >= 0){xi=xs; // 当前验证通过的证书作为上级 CA 证书xs=sk_X509_value(ctx->chain,n); // 下级证书作为被验证证书}}ok=1;
end:return ok;
}

压力继续传递给函数 X509_verify 及后续函数

int X509_verify(X509 *a, EVP_PKEY *r)
{   // 用颁发者公钥 r 验证证书信息 a->cert_info 对应签名 a->signature 的合法性return(ASN1_item_verify(ASN1_ITEM_rptr(X509_CINF),a->sig_alg,a->signature,a->cert_info,r));
}int ASN1_item_verify(const ASN1_ITEM *it, X509_ALGOR *a, ASN1_BIT_STRING *signature,void *asn, EVP_PKEY *pkey)
{// 由签名算法 X509_ALGOR 得到 HASH 类型并初始化 EVP_MD_CTXEVP_MD_CTX_init(&ctx);i=OBJ_obj2nid(a->algorithm);type=EVP_get_digestbyname(OBJ_nid2sn(i));EVP_VerifyInit_ex(&ctx,type, NULL)//【恢复出 X509_CINF(即证书的 tbsCertificate)的 ASN1 编码, 但发现出错】inl = ASN1_item_i2d(asn, &buf_in, it);EVP_VerifyUpdate(&ctx,(unsigned char *)buf_in,inl); // 喂入 tbsCertificateEVP_VerifyFinal(&ctx,(unsigned char *)signature->data, (unsigned int)signature->length,pkey); // 继续验证
}int EVP_VerifyFinal(EVP_MD_CTX *ctx, const unsigned char *sigbuf,unsigned int siglen, EVP_PKEY *pkey)
{EVP_MD_CTX_init(&tmp_ctx); // 验证前准备: 计算 HASH(tbsCertificate)EVP_MD_CTX_copy_ex(&tmp_ctx,ctx);EVP_DigestFinal_ex(&tmp_ctx,&(m[0]),&m_len);return(ctx->digest->verify(ctx->digest->type,m,m_len,sigbuf,siglen,pkey->pkey.ptr)); // 实际调用 RSA_verify 完成最后一击
}int RSA_verify(int dtype, const unsigned char *m, unsigned int m_len, // 进行 RSA 签名验证unsigned char *sigbuf, unsigned int siglen, RSA *rsa)
{// 用公钥还原得到 signatureValue^e (并去掉 PKCS1 PADDING)i=RSA_public_decrypt((int)siglen,sigbuf,s,rsa,RSA_PKCS1_PADDING);const unsigned char *p=s;sig=d2i_X509_SIG(NULL,&p,(long)i); // X509_SIG 的内部表示// 比较 signatureValue^e 和 HASH(tbsCertificate)if ( ((unsigned int)sig->digest->length != m_len) ||(memcmp(m,sig->digest->data,m_len) != 0    )){RSAerr(RSA_F_RSA_VERIFY,RSA_R_BAD_SIGNATURE);}else // 验证成功ret=1;
}

到此为止,所有主要函数流程讲解完毕。与此同时,最大的嫌疑还未解开:到底是什么原因导致证书验证出错?

在继续之前,我们总结一下 OpenSSL 的证书验证步骤
(1) 将证书内容从文件中读出,并转换保存在内部数据结构中(见代码中的 d2i_X509 函数)
(2) 将证书内部数据的 X509_CINF(tbsCertificate) 部分转换为 DER 编码格式(见代码中的 ASN1_item_i2d 函数)
(3) 依据证书验证公式进行校验

由于在步骤(2)后就发现了错误,因此只能有两种可能:步骤(1)出错 或者 步骤(2)出错。

正常的逻辑就是从步骤(1)开始,顺藤摸瓜,找出真正的“幕后凶手”。这就是我们后面章节的主要思路。
在这之前,我们先顺便解决一个小问题。

openssl 证书验证相关推荐

  1. linux openssl 证书,Linux上的openSSL证书验证

    JKJS 得到了我自己的问题的答案: 1)通过以下命令创建根CA证书: openssl req -newkey rsa:1024 -sha1 -keyout rootkey.pem -out root ...

  2. 速修复!OpenSSL 披露DoS 和证书验证高危漏洞,可导致服务器崩溃

     聚焦源代码安全,网罗国内外最新资讯! 编译:奇安信代码卫士团队 刚刚,OpenSSL 项目发布安全公告指出,OpenSSL 产品中存在两个漏洞(CVE-2021-3449和CVE-2021-3450 ...

  3. openssl 签发sm2证书_首个NSA公开披露的软件系统漏洞——CVE20200601数字证书验证漏洞分析与实验...

    文/林璟锵 刘广祺 孟令佳 万会庆 王琼霄 王伟 王文杰 徐博文 中国科学院数据与通信保护研究教育中心 中国科学院信息工程研究所 信息安全国家重点实验室 1. CVE-2020-0601漏洞 2020 ...

  4. 微信支付HTTPS服务器证书验证(PHP)

    PHP代码验证是否包含CA证书 如果无法通过验证,请点击连接查看官方操作,安装相应的ca证书即可 # test_wechat_ca.php <?php $data = array('mch_id ...

  5. Kubernetes安装之证书验证

    前言 昨晚(Apr 9,2017)金山软件的opsnull发布了一个开源项目和我一步步部署kubernetes集群,下文是结合我之前部署kubernetes的过程打造的kubernetes环境和ops ...

  6. python 指定证书验证_如何在python中验证SSL证书?

    我需要验证我的自定义CA签署了证书.使用OpenSSL命令行实用程序很容易做到: # Custom CA file: ca-cert.pem # Cert signed by above CA: bo ...

  7. Android 根证书管理与证书验证

    PKI 体系依赖证书执行极为关键的身份验证,以此确认服务端的可信任性.证书验证在 SSL/TLS 握手过程中完成,验证过程通常包含三个步骤: 验证证书的合法性:这一步主要是验证证书是由合法有效的 CA ...

  8. 密码学专题 证书和CA指令 申请证书|建立CA|CA操作|使用证书|验证证书

    Req指令介绍 功能概述和指令格式 req指令一般来说应该是提供给证书申请用户的工具,用来生成证书请求以便交给CA验证和签发证书.但是,OpenSSL的req指令的功能远比这样的要求强大得多,它不仅可 ...

  9. pythonrequests证书_requests的ssl证书验证、身份认证、cert文件证书

    SSL证书:Requests 可以为 HTTPS 请求验证 SSL 证书,就像 web 浏览器一样.SSL 验证默认是开启的,如果证书验证失败,Requests 会抛出 SSLError.在该域名re ...

最新文章

  1. db2删除大量数据_Python 连接数据库的多种方法
  2. Silverlight C# 游戏开发:方向键的组合,八方向实现
  3. Android之 AndroidManifest xml 文件解析
  4. FPGA学习之路—Vivado与Modelsim联合仿真
  5. .NET6之MiniAPI(十):基于策略的身份验证和授权
  6. 360浏览器设置多标签操作步骤
  7. python的re模块是自带的吗_python内置模块手册 python中的re模块是自带的吗
  8. 机器学习基础算法22-提升理论-GBDT、XGBoost、Adaboost、方差与偏方
  9. table内容超出宽度时隐藏并显示省略标记
  10. 推荐两个非常不错的公众号
  11. mysql 主从机器 触发器 的测试,完全正常 没有问题
  12. Atitit USRqc62204 证书管理器标准化规范
  13. 【语音去噪】基于matlab GUI软阈值+硬阈值+软硬折中阈值语音去噪【含Matlab源码 1810期】
  14. 在R语言中显示数学公式
  15. windows10磁盘100%解决方法
  16. 计算机桌面文件删除不掉是怎么了,文件删不掉怎么办?如何删除一个删不掉的文件?...
  17. 带你深入了解Web3开发者堆栈
  18. java awt生成签名图片消除锯齿化
  19. Delta, Hudi, Iceberg对比
  20. 【财务分析】ERP软件的财务管理系统具有哪些功能?

热门文章

  1. Arduino开发实例-DIY空气粉尘密度检测仪
  2. autojs,按键精灵等辅助,快手极速版代码分享(真实可运行)
  3. 有没有一个桌面日历点击某天就可以直接填写便签内容
  4. 校企合作,共创教育辉煌
  5. react这几年重大意义的变化
  6. WM6不支持SendRequestEx,怎么上传HTTP表单?
  7. Windows Server 2016 AD域(一)禁用USB存储设备
  8. 创建文件 c语言,汇编语言CreateFile函数:创建新文件或者打开已有文件
  9. 智慧城市建设热潮下怎么抢占先机?
  10. Scaffold 详解