走讀OpenSSL代碼----從一張奇怪的證書說起(七)


本節中我們快速瀏覽一下證書驗證的主干代碼。讀者可以采用上節中生成的VC工程進行驗證。

下面列出關鍵部分代碼,為方便閱讀,僅保留與證書驗證強相關的代碼,去掉了諸如變量定義、錯誤處理、資源釋放等非主要代碼,並修改了排版格式。

View Code
// 初始入口為 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<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_CTX
        X509_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 身上,我們單獨把該函數拎出來,進行講解。

View Code
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 對證書鏈進行驗證,見下面的說明

View Code
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 及后續函數

View Code
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)開始,順藤摸瓜,找出真正的“幕后凶手”。這就是我們后面章節的主要思路。
在這之前,我們先順便解決一個小問題。

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2026 CODEPRJ.COM