天天看點

openssl的證書鍊驗證

使用openssl驗證證書鍊可以用以下指令:

debian:/home/zhaoya/openssl#openssl verify -CAfile ROOT_CERT USER_CERT

其中的ROOT_CERT可以包含很多證書,可以用cat指令将多級的ca證書合并到一個檔案裡面,然後程式啟動以後會加載ROOT_CERT,ROOT_CERT會在記憶體中形成一個堆棧結構,各個證書的順序和檔案裡面的一樣。但是如果檔案中的諸多證書有若幹個是同名的話,使用上述指令就會出現問題,為了改正這個不是問題的問題,還是查代碼吧。為何說這個問題不是問題呢,因為不提倡ca同名,或者即使可以同名,那也要可以通過别的字段将其區分開來。不管怎麼說查代碼總不是壞事,起碼能對openssl有個新的了解。下面是這個混亂的證書鍊體系結構,()括起來的是證書CN項的名字,而[]表示了頒發關系:

ROOT1(CN=ROOT)[SubCA1_1(CN=SubCA1_1)[User1_1(CN=User1_1)],SubCA1_2(CN=SubCA1_1)[User1_2(CN=User1_2)],SubCA1_3(CN=SubCA1_3)[User1_3(CN=User1_3)]]

ROOT2(CN=ROOT)[SubCA2_1(CN=SubCA1_1)[User2_1(CN=User2_1)],SubCA2_2(CN=SubCA2_2)[User2_2(CN=User2_2)]]

ROOT3(CN=ROOT3)[SubCA3_1(CN=SubCA3_1)[User3_1(CN=User3_1)]]

#ifdef ZHAOYA

#define MAX_DEPTH  6

struct list_entity {

    int num;

    struct list_x *first[MAX_DEPTH];

}

struct list_xit {

    void * ptr;

    struct list_xit *prev;

    struct list_xit *next;

//注釋0 (幹這行并且做底層的都會明白為何從0開始而不是1,正如有人數到9之後不是10,而是a一樣)

typedef struct list_entity list_e;

typedef struct list_xit list_x;

int get_isuser_all (X509 **issuer, X509_STORE_CTX *ctx, X509 *x, list_e * le) 

{

    X509_NAME *xn;

    X509_OBJECT obj, *pobj;

    int i, ok, idx;

    xn=X509_get_issuer_name(x); //得到頒發者的名字

    idx = X509_OBJECT_idx_by_subject(ctx->ctx->objs, X509_LU_X509, xn);//注釋4

    X509 *xyz = NULL;

    for (i = idx; i < sk_X509_OBJECT_num(ctx->ctx->objs); i++)

    {

        pobj = sk_X509_OBJECT_value(ctx->ctx->objs, i);

        if (ctx->check_issued(ctx, x, pobj->data.x509))

        {    //得到同一級别的ca中所有名稱比對的ca證書,依次加入到連結清單中

            xyz = pobj->data.x509;

            list_x * lx = (list_x*)calloc(sizeof(list_x));

            lx->ptr = pobj->data.x509;

            list_x * header = le->first[le->num];

            lx->next = header;

            le->first[le->num] = lx;

        }

    }    

    if (xyz) {

        le->num ++;

        *issuer = xyz;

        return 1;

    }

    return 0;

int vfy(X509_STORE_CTX *ctx, X509 * xi, X509 * xs) 

    EVP_PKEY *pkey=NULL;

    int ok;

    int (*cb)(int xok,X509_STORE_CTX *xctx); //注釋3

    cb=ctx->verify_cb;

    if (xs->valid) goto last;

    if ((pkey=X509_get_pubkey(xi)) == NULL) {

        ctx->error=X509_V_ERR_UNABLE_TO_DECODE_ISSUER_PUBLIC_KEY;

        ctx->current_cert=xi;

        ok=(*cb)(0,ctx);

        if (!ok)

            return -1;

    } else if (X509_verify(xs,pkey) <= 0) {

        ctx->error=X509_V_ERR_CERT_SIGNATURE_FAILURE;

        ctx->current_cert=xs;

        EVP_PKEY_free(pkey);  

        if (!ok) {

    xs->valid = 1;

last:

    ok = check_cert_time(ctx, xs);

    if (!ok)

        return -1;

    ctx->current_issuer=xi;

    ctx->current_cert=xs;

    ok = (*cb)(1,ctx);

    EVP_PKEY_free(pkey);

    return ok ? 1:-1;

int all_cert_verify(X509_STORE_CTX *ctx, list_e * le)

    int ok=0,;

    X509 *xs,*xi;

    ctx->error_depth=n-1;

    list_x * lx =  le->first[0];

    xs = lx->ptr;

    int co = 1, res;

    for (; co < le->num; co++) {

        ctx->error_depth = co;

        list_x *lx =  le->first[co];

        while (lx) {

            xi = lx->ptr;

            res = vfy(ctx, xi, xs);

            if (res ==1) {

                xs = xi;

                break;

            } else if (res == -1 && ctx->error != X509_V_ERR_CERT_SIGNATURE_FAILURE) 

                return 0;  //注意如果是由于非簽名錯誤引發的别的錯誤的話,直接退出

            lx = lx->next;

        if (lx == NULL) { //如果同一層沒有任何證書能驗證下層的證書,那麼直接退出,依據是證書鍊是單路徑的

            ok = 0;

            break;

    if (ctx->check_issued(ctx, xi, xi) && co == le->num) { //最後驗證根證書,也就是一個自簽名的證書

        ok = vfy(ctx, xi, xi);

    return ok;

#endif

int X509_verify_cert(X509_STORE_CTX *ctx)

    X509 *x,*xtmp,*chain_ss=NULL;

    int bad_chain = 0;

    X509_VERIFY_PARAM *param = ctx->param;

    int depth,i,ok=0;

    int (*cb)(int xok,X509_STORE_CTX *xctx);

    STACK_OF(X509) *sktmp=NULL;

    list_e    * le = NULL;

    if (ctx->chain == NULL) {

            ctx->chain=sk_X509_new_null());     

            sk_X509_push(ctx->chain,ctx->cert);

            le = (list_e*)calloc(sizeof(list_e));

            le->num = 0;

            list_x * lx = (list_x*)malloc(sizeof(list_x));

            memset(lx, 0, sizeof(list_x));

            lx->ptr = ctx->cert;

            lx->next =NULL;

            le->num ++;

        CRYPTO_add(&ctx->cert->references,1,CRYPTO_LOCK_X509);

        ctx->last_untrusted=1;

    if (ctx->untrusted != NULL

        && (sktmp=sk_X509_dup(ctx->untrusted)) == NULL){

        ...

    num=sk_X509_num(ctx->chain);

    x=sk_X509_value(ctx->chain,num-1);

    depth=param->depth;

    for (;;) {  //注釋1

        if (depth < num) break; 

        xn=X509_get_issuer_name(x);

        if (ctx->check_issued(ctx, x,x)) break;

        if (ctx->untrusted != NULL) {

            xtmp=find_issuer(ctx, sktmp,x);

            if (xtmp != NULL) {

                if (!sk_X509_push(ctx->chain,xtmp)){...

                }

                CRYPTO_add(&xtmp->references,1,CRYPTO_LOCK_X509);

                sk_X509_delete_ptr(sktmp,xtmp);

                ctx->last_untrusted++;

                x=xtmp;

                num++;

                continue;

            }

        break;

    i=sk_X509_num(ctx->chain);  //得到目前構造的證書鍊中證書的個數

    x=sk_X509_value(ctx->chain,i-1);  //取得目前構造的證書鍊最上面的證書

    xn = X509_get_subject_name(x);

    if (ctx->check_issued(ctx, x, x)) {

        if (sk_X509_num(ctx->chain) == 1) {

            //最上面的證書是自簽名的情況,該情況下不用繼續在CA集合中尋找它的頒發者,因為不可能找到,它是自簽發的。但是代碼還是要走到下面的internal_verify裡面的。

    //以下的for循環在證書的集合當中按照從下到上的順序根據isuser字段查找證書

    int co = 0;

    for (;;) {

        co += 1;

        if (depth < num) break;

        if (ctx->check_issued(ctx,x,x))

                 break;

        ok = get_isuser_all(&xtmp, ctx, x, le);  //得到同一層次的所有的證書,而不是僅僅得到一個就傳回。

#else

        ok = ctx->get_issuer(&xtmp, ctx, x); //僅僅得到一個“看似合理”的證書就傳回,而實際上這裡僅僅根據

CN名稱查找上級CA憑證

        if (ok < 0) return ok;

        if (ok == 0) break;

        x = xtmp; //層層向上查找,直到一個自簽名的根為止。

        if (!sk_X509_push(ctx->chain,x)) {

            X509_free(xtmp);

            X509err(X509_F_X509_VERIFY_CERT,ERR_R_MALLOC_FAILURE);

            return 0;

        num++;

    xn=X509_get_issuer_name(x);

    if (!ctx->check_issued(ctx,x,x)) {  //察看最上層的證書是否是自簽名的證書

        if ((chain_ss == NULL) || !ctx->check_issued(ctx, x, chain_ss)) {

            if (ctx->last_untrusted >= num)

                ctx->error=X509_V_ERR_UNABLE_TO_GET_ISSUER_CERT_LOCALLY;

            else

                ctx->error=X509_V_ERR_UNABLE_TO_GET_ISSUER_CERT;

            ctx->current_cert=x;

        } else {

            sk_X509_push(ctx->chain,chain_ss);

            num++;

            ctx->last_untrusted=num;

            ctx->current_cert=chain_ss;

            ctx->error=X509_V_ERR_SELF_SIGNED_CERT_IN_CHAIN;

            chain_ss=NULL;

        ctx->error_depth=num-1;

        bad_chain = 1;

        ok=cb(0,ctx);

        if (!ok) goto end;

#ifndef ZHAOYA  //注釋2

    if (ctx->verify != NULL){ 

        ok=ctx->verify(ctx);

    else 

        ok=internal_verify(ctx);

        ok = all_cert_verify (ctx, le);

    if(!ok) goto end;

end:

    int count = 0;

    for(; count < le->num; count++) {

        list_x *x = le->first[count];

        while (x) {

            free (x);

            x = x->next;

    free (le);

注釋0:證書鍊的擴充資料結構,設計得很粗糙,更好的設計方式是使用linux核心中的hlist_head這種嵌入式連結清單,之是以用hlist而不是list_head是因為前者是一個吊鍊結構,而從本地記憶體或者磁盤加載的證書邏輯上也是一個吊鍊結構,如果證書的名字分别是1,2,3,這三個名字的證書的個數分别是2,3,1,那麼記憶體中的證書可以按照如下的順序排列:112223,也可以123221,...雖然實體上很無序,但是邏輯上卻是吊鍊,主鍊上有3個元素,分别代表3個名字的證書,吊鍊上是名字重複的證書,類似哈希沖突連結清單,于是證書鍊可以采用hlist的方式組織。

注釋1:優先使用傳入的證書鍊,這麼做可以優先排除諸如黑名單裡面的證書。實際上就是最不被信任的證書最優先處理,一般而言,這裡的untrusted連結清單裡面的證書不是本地的證書,而大多是從遠端傳來的證書,比如ssl中伺服器驗證用戶端證書,然而用戶端證書一般都是N級CA頒發的,于是如果伺服器端沒有用戶端證書頒發者的CA憑證的話,那麼驗證就會失敗,于是用戶端不僅僅将自己的用戶端證書傳給伺服器,它的證書的頒發者CA憑證也一并傳遞過去,很顯然,這個N級CA的證書對于伺服器來說是不被信任的,優先使用這個證書可以優先拒絕惡意攻擊,這裡的關系有點微妙,如果伺服器端有這個N級CA憑證,而這個用戶端僅僅想惡作劇而修改了這個N級證書,然後連同自己的用戶端證書一并發給了伺服器,那麼伺服器會發現這個惡作劇并且讓用戶端付出代價。這個untrusted鍊稍微正常些的用途是排除黑名單,還是上面的那個例子,如果用戶端證書的頒發者CA因為某原因被列入了黑名單,這件事并沒有通知用戶端,用戶端也沒有主動查詢,而伺服器知道了這件事,那麼伺服器端就會存在一個黑名單連結清單,用戶端證書頒發者的CA憑證就會在其中,當驗證用戶端證書的時候,伺服器端證書鍊加載順序為黑名單的證書優于用戶端傳來的CA憑證,用戶端傳來的CA憑證優于伺服器本地的被信任的證書,于是客戶段的CA憑證在黑名單中這件事就會首先被探測到。以上僅僅是兩個例子,untrusted連結清單的本質類似于ACL,先比對拒絕的再比對接受的。

注釋2:這裡是正常驗證法,如果不是由于不能修改結構體和函數形參,完全可以不用宏來進行分支管理,這其實是為了保證二進制相容性而退一步的做法,list_e指針完全可以放入到X509_STORE_CTX結構體中,這樣的話,就可以實作一個verify回調函數來管理,也就不用預編譯宏來判斷了。all_cert_verify實作了從下到上的驗證邏輯,而internal_verify卻是單條連結清單的自上而下的驗證,注意僅僅是單條證書鍊,這樣可能會使一些證書在存在同名ca的情況下永遠失去被驗證通過的機會,而實際上是可以被同名ca中的某一個驗證通過的,具體能否被同名的ca驗證通過取決于這些同名ca的加載順序,一個解決方案就是在實際驗證的時候周遊所有的被加載的證書,這樣就不存在構造鍊的操作了,我這裡沒有實作,那樣的話代碼量會少很多,當然周遊的時候時間複雜度也會增加,算法實作會在後面給出,一個更合理的解決方案是我寫的all_cert_verify,可以看到裡面實際上并沒有用到ctx->chain這個堆棧,之是以這麼寫是為了更少的改動原有的代碼,否則的話,直接删除關于ctx->chain的所有代碼即可。all_cert_verify實作的還是很單純,我相信有很多異常情況沒有處理,給出的僅僅是算法的思想罷了。另外在構造list_e的時候也沒有處理untrusted連結清單,不過我相信加上是很簡單的。

注釋3:如果簽名驗證失敗了,那麼就調用ctx的verify_cb函數,如果回調函數的提供者不關心這個錯誤,那麼就可能清除該錯誤進而使驗證繼續下去,反之如果簽名驗證成功,那麼回調函數的提供者也可能用自己的政策終止驗證,同樣也在回調函數中得到展現。回調函數的第一個參數是最近的結果值,内部實作中你可以随意設定ctx的error值以及是否可以忽略這個error,如果可以忽略,那麼就清空ctx的error然後回調函數傳回1即可,當然也可以根據ctx的目前設定,在第一個參數為1的情況下,設定ctx的error,然後傳回0,終止這次驗證操作。

注釋4:X509_OBJECT_idx_by_subject這個函數作了一點小小的優化,使得周遊的時候不用從一開始進行,而是從第一個名稱比對的證書開始。

 本文轉自 dog250 51CTO部落格,原文連結:http://blog.51cto.com/dog250/1273300

繼續閱讀