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);
ok=(*cb)(0,ctx);
if (!ok) {
return -1;
}
}
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;
X509_NAME *xn;
int bad_chain = 0;
X509_VERIFY_PARAM *param = ctx->param;
int depth,i,ok=0;
int num;
int (*cb)(int xok,X509_STORE_CTX *xctx);
STACK_OF(X509) *sktmp=NULL;
#ifdef ZHAOYA
list_e * le = NULL;
#endif
cb=ctx->verify_cb;
if (ctx->chain == NULL) {
ctx->chain=sk_X509_new_null());
sk_X509_push(ctx->chain,ctx->cert);
#ifdef ZHAOYA
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));
le->first[le->num] = lx;
lx->ptr = ctx->cert;
lx->next =NULL;
le->num ++;
#endif
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;
#ifdef ZHAOYA
ok = get_isuser_all(&xtmp, ctx, x, le); //得到同一層次的所有的證書,而不是僅僅得到一個就返回。
#else
ok = ctx->get_issuer(&xtmp, ctx, x); //僅僅得到一個“看似合理”的證書就返回,而實際上這裏僅僅根據
CN名稱查找上級CA證書
#endif
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);
#else
ok = all_cert_verify (ctx, le);
#endif
if(!ok) goto end;
end:
#ifdef ZHAOYA
int count = 0;
for(; count < le->num; count++) {
list_x *x = le->first[count];
while (x) {
free (x);
x = x->next;
}
}
free (le);
#endif
return ok;
}
註釋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這個函數作了一點小小的優化,使得遍歷的時候不用從一開始進行,而是從第一個名稱匹配的證書開始。
再分享一下我老師大神的人工智能教程吧。零基礎!通俗易懂!風趣幽默!還帶黃段子!希望你也加入到我們人工智能的隊伍中來!https://blog.csdn.net/jiangjunshow
openssl的證書鏈驗證