openssl 证书验证

本节中我们快速浏览一下证书验证的主干代码。读者可以采用上节中生成的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_ctx
    lookup=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 证书栈 ret
                    xi = 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_X509
                            if (xi->x509 != NULL) // 上一次循环中已解析过证书,首次调用 PEM_read_bio 则不进入
                            {
                                sk_X509_INFO_push(ret,xi) // 将已解析的证书信息压栈 ret
                                xi=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) // 最后一个证书压栈 ret
                    ok=1;
                    ......
                    return(ret); // 返回 CA 证书栈
                }

                for(i = 0; i < sk_X509_INFO_num(inf); i++) { // 将栈中的证书加入到 X509_STORE
                    itmp = 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 的成员 objs
                        count++;
                    }
                }

                return count; // 返回从 const char *file 读出的证书个数
            }
        }
    }

    // 省去 for (i=0; i 
  

复制代码

验证证书的重任落在函数 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->untrusted

    num=sk_X509_num(ctx->chain); // num == 1
    x=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_verify
    else
        ok=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 == Issuer
    else // 按理说不会走到 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_CTX
    EVP_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); // 喂入 tbsCertificate
    EVP_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)