OpenSSL是坑貨寫的
(注* 通過Heartbleed這個致使漏洞,任何人都可遠程訪問運行了OpenSSL服務器的內存,參見: heart bleed 其實早在09年就有人噴過OpenSSL)
我知道這聽起來很刺耳,但讓我們調查一下事實吧。
首先,讓我們避免別人抱怨的方式在自己身上發生。嗨,我是Marco Peereboom,我寫開源代碼是興趣所在。我已經參與過一些項目了,谷歌會告訴你我參與了哪些項目。我不是一個偉大的程序員,但我離此也不遠,我已經寫了一些東西。
最近我參與編寫一些與通信安全有關的代碼,我認為除了用眾所周知的被廣大用戶使用的庫以外沒有更好的辦法了。基本上我的問題歸結為以下幾點:
- 寫一個提供CA(認證授權)認證服務的程序。
- 將所有的證書存儲在一個LDAP樹上。
我們討論的不是關于這個想法的優點,但這是我們旅途的開始。這聽起來簡單極了,因此去網絡上找相應的文檔和代碼段,這簡直就像是在公園里散步,因為所有的這些問題以前就有人解決過了。
和這些代碼倒騰了近一個月之后,我決定詳細記載一下這些內容,希望可以省一些精力。我已經得出結論了,OpenSSL就像猴子在墻上潑糞一樣。它是我用過最爛的庫,沒有之一。我簡直不敢相信互聯網居然運行在這樣一個荒唐復雜且莫名愚蠢的代碼之上的。自1998年以來幾乎全世界都認為他們的安全通信是建立在這個堅不可摧的自稱為“OpenSSL”的爛項目上的。我敢打賭,在這個問題上,醫生也開不出任何處方。
第一天:
根 本沒法找到完整的代碼。我的尋找對CA問題一點用也沒有。我找到的東西基本上就是一大堆“怎么做”,但他們幾乎都沒有提到最關鍵的問題。他們是好意的,但 只提供方法而沒有實質的東西確實傷害到了我。好吧,讓我們去Barnes & Nobles(美國最大的實體書店)找找看吧,看我們能找到些什么 東西。
坑啊!什么都沒有,真的。我找到兩本書,兩本書都是基本上都是關于使用命令行工具的。其中一本書上有一些范例代碼,但真沒有什么值得借鑒的地方。顯然,人們似乎只關心使用openssl這個“工具”而已。
我看了一下代碼,但這些代碼完全把我給搞暈了,所以我還是選擇離開了。不管怎么說,現在我開始懷疑這東西可能沒有我想象中那么簡單。不管怎樣還是先回家喝杯冷飲吧。
第二天:
好吧,管它的!我還是要看下那些討厭的代碼;我指的是網絡上使用的所有書本和范例。至少它能給我指出來我需要用到哪些函數,然后我就可以在更好的文檔上查找這些函數。首先我顯然需要找出使用openssl這個“工具”的辦法。
8小時之后.......
嗬,閱讀了無數個“怎么做”之后我找到了一些有用的東西。那份文檔寫的都是人們為何這么說?真的很差勁!讓我來分享一下我用愛做出的成果吧。我創建了3個腳本來舉例說明我用CA簽署的客戶端和服務器證書的問題。
1.create_ca,這個腳本創建了CA。
2.create_server,這個腳本創建了服務器證書和密鑰。
3.create_client,這個腳本創建了客戶端證書和密鑰。
create_ca
#/bin/sh mkdir -p ca/private chmod 700 ca/private openssl req -x509 -days 3650 -newkey rsa:1024 -keyout ca/private/ca.key -out ca/ca.crt
create_server
#/bin/sh mkdir -p server/private chmod 700 server/private openssl genrsa -out server/private/server.key 1024 openssl req -new -key server/private/server.key -out server/server.csr
create_client
#/bin/sh mkdir -p client/private chmod 700 client/private openssl genrsa -out client/private/client.key 1024 openssl req -new -key client/private/client.key -out client/client.csr
今天做的已經夠了。該回家了。
第三天:
好吧,openssl“工具”這個東西讓我們偏離了深挖那段代碼的路線。額,主函數在哪?它怎么調用那些單一的模塊。噢,用大小寫加下劃線啊。這個主宏太棒了,*WASH_Eyes_outWith_soap*
好 了,現在我們需要一些重要的標簽以便來操作這些混亂的代碼。通過幾小時的深挖代碼之后,我找到了竅門(感謝vim!)。現在是時候開始重新組織這些代碼 了,看看我不用openssl這個“工具”能不能生成CA。又幾個小時過去了,不完整的操作手冊,沒有操作手冊,寫得很差的操作手冊這些東西確實開始困擾 著我了。沒有谷歌,必應,雅虎這些搜索引擎的幫助。完全沒有文檔且就算有文檔,里面的內容都是過時的而且沒有實際聯系的。管它的,我該回家了!
第四天:
我開始寫一些基于我在openssl里找到的東西的代碼。由于令人尷尬的不良風格,代碼的壓縮且#ifdef阻止我開始,我的進度極其緩慢。說到#ifdef,我看到一些能吞掉指令的不穩定的東西。例如:
#ifdef (OMG) if (moo) { ... } else #endif /* OMG */ yeah();
實際上,這已經是比較不錯的版本了。我用的版本是經過壓縮的幾百行代碼構成的,這能把男子漢給搞哭。同樣地讓我們以不同尋常的方式得到另外的一些東西。如果你認為
if (moo) { dome_something_dumb(); } else { or_not(); } or if ( moo) { blah(); } if (bad) goto err; ... if (0) { err: do_something_horrible(); }
可讀性好,那么我建議你還是去檢測下視力吧。甚至可能找一下直腸科醫師。讓我們來看一些真實的案例:
if ((OBJ_obj2nid(obj) == NID_pkcs9_emailAddress) && (str->type != V_ASN1_IA5STRING)) { BIO_printf(bio_err,"\nemailAddress type needs to be of type IA5STRING\n"); goto err; } if ((str->type != V_ASN1_BMPSTRING) && (str->type != V_ASN1_UTF8STRING)) { j=ASN1_PRINTABLE_type(str->data,str->length); if ( ((j == V_ASN1_T61STRING) && (str->type != V_ASN1_T61STRING)) || ((j == V_ASN1_IA5STRING) && (str->type == V_ASN1_PRINTABLESTRING))) { BIO_printf(bio_err,"\nThe string contains characters that are illegal for the ASN.1 type\n"); goto err; } }
下面是一個函數堆棧的例子。
if (!SSL_CTX_use_certificate_file(ctx, "server/server.crt", SSL_FILETYPE_PEM)) ctrl ] ... else if (type == SSL_FILETYPE_PEM) { j=ERR_R_PEM_LIB; x=PEM_read_bio_X509(in,NULL,ctx->default_passwd_callback,ctx->default_passwd_callback_userdata); ctrl ] ... #define PEM_read_bio_X509(bp,x,cb,u) (X509 *)PEM_ASN1_read_bio( \ (char *(*)())d2i_X509,PEM_STRING_X509,bp,(char **)x,cb,u) ctrl ] ... if (!PEM_bytes_read_bio(&data, &len, NULL, name, bp, cb, u)) return NULL; ctrl ] ... if (!PEM_read_bio(bp,&nm,&header,&data,&len)) { ctrl ] ... i=BIO_gets(bp,buf,254); ctrl ] ... i=b->method->bgets(b,in,inl);
它跨過了5個文件,6個間接文件,都是為了打開文件 fget到文件的內容。我們仍然在使用間接調用。當所有我想要的是一個能將PEM(不在一個文件中) cert翻譯成 X509結構的函數時,所有這些工 作和障礙都存在著。但百萬左右的函數不方便那樣存在。我多少有點懷疑,但由于沒有文檔,我真的只能猜測了。我也不能從這個gem把你們搶走:
#ifndef OPENSSL_NO_STDIO /*! * Load CA certs from a file into a ::STACK. Note that it is somewhat misnamed; * it doesn't really have anything to do with clients (except that a common use * for a stack of CAs is to send it to the client). Actually, it doesn't have * much to do with CAs, either, since it will load any old cert. * \param file the file containing one or more certs. * \return a ::STACK containing the certs. */ STACK_OF(X509_NAME) *SSL_load_client_CA_file(const char *file) { BIO *in; X509 *x=NULL; X509_NAME *xn=NULL; STACK_OF(X509_NAME) *ret = NULL,*sk; sk=sk_X509_NAME_new(xname_cmp); in=BIO_new(BIO_s_file_internal()); if ((sk == NULL) || (in == NULL)) { SSLerr(SSL_F_SSL_LOAD_CLIENT_CA_FILE,ERR_R_MALLOC_FAILURE); goto err; } if (!BIO_read_filename(in,file)) goto err; for (;;) { if (PEM_read_bio_X509(in,&x,NULL,NULL) == NULL) break; if (ret == NULL) { ret = sk_X509_NAME_new_null(); if (ret == NULL) { SSLerr(SSL_F_SSL_LOAD_CLIENT_CA_FILE,ERR_R_MALLOC_FAILURE); goto err; } } if ((xn=X509_get_subject_name(x)) == NULL) goto err; /* check for duplicates */ xn=X509_NAME_dup(xn); if (xn == NULL) goto err; if (sk_X509_NAME_find(sk,xn) >= 0) X509_NAME_free(xn); else { sk_X509_NAME_push(sk,xn); sk_X509_NAME_push(ret,xn); } } if (0) { err: if (ret != NULL) sk_X509_NAME_pop_free(ret,X509_NAME_free); ret=NULL; } if (sk != NULL) sk_X509_NAME_free(sk); if (in != NULL) BIO_free(in); if (x != NULL) X509_free(x); if (ret != NULL) ERR_clear_error(); return(ret); } #endif
哇!所有的東西都放在里面實現了!不可讀,對于函數如何實現來說,它有太多的間接調用和不清晰的直接調用。不得不喜歡if (0)這個結構!我指的是顯然它贏得了所有的關于美的獎。你必須要很高興地知道這個風格的代碼支撐起我們許多的“安全”網絡。
我真傻,只想需要一個函數從LDAP或內存讀取證書以便我能自己寫LDAP的代碼。沒有寫太多的代碼,我需要回家喝上兩杯。
第五天:
它開始讓我反感了!伴隨著各種頭疼的東西,我開始著手代碼部分了。經過幾個小時的閱讀和反復閱讀openssl這個“工具”的代碼,我想出了這個:
int create_ca(char *retstr, size_t retlen) { int rv = 1; int days = 365 * 10; char *password = NULL; EVP_PKEY pkey, *tmppkey = NULL; BIGNUM bn; RSA *rsa = NULL; X509_REQ *req = NULL; X509_NAME *subj; X509 *x509 = NULL; BIO *out = NULL; /* generate private key */ if ((rsa = RSA_new()) == NULL) ERROR_OUT(ERR_SSL, done); bzero(&bn, sizeof bn); if (BN_set_word(&bn, 0x10001) == 0) ERROR_OUT(ERR_SSL, done); if (RSA_generate_key_ex(rsa, 1024, &bn, NULL) == 0) ERROR_OUT(ERR_SSL, done); bzero(&pkey, sizeof pkey); if (EVP_PKEY_assign_RSA(&pkey, rsa) == 0) ERROR_OUT(ERR_SSL, done); /* setup req for certificate */ if ((req = X509_REQ_new()) == NULL) ERROR_OUT(ERR_SSL, done); if (X509_REQ_set_version(req, 0) == 0) ERROR_OUT(ERR_SSL, done); subj = X509_REQ_get_subject_name(req); if (validate_canew(subj, &password)) { snprintf(last_error, sizeof last_error, "validate_canew failed"); ERROR_OUT(ERR_OWN, done); } /* set public key to req */ if (X509_REQ_set_pubkey(req, &pkey) == 0) ERROR_OUT(ERR_SSL, done); /* generate 509 cert */ if ((x509 = X509_new()) == NULL) ERROR_OUT(ERR_SSL, done); bzero(&bn, sizeof bn); if (BN_pseudo_rand(&bn, 64 /* bits */, 0, 0) == 0) ERROR_OUT(ERR_SSL, done); if (BN_to_ASN1_INTEGER(&bn, X509_get_serialNumber(x509)) == 0) ERROR_OUT(ERR_SSL, done); if (X509_set_issuer_name(x509, X509_REQ_get_subject_name(req)) == 0) ERROR_OUT(ERR_SSL, done); if (X509_gmtime_adj(X509_get_notBefore(x509), 0) == 0) ERROR_OUT(ERR_SSL, done); if (days == 0) { snprintf(last_error, sizeof last_error, "not enough days for certificate"); ERROR_OUT(ERR_OWN, done); } days *= 60 * 60 * 24; if (X509_gmtime_adj(X509_get_notAfter(x509), days) == 0) ERROR_OUT(ERR_SSL, done); if (X509_set_subject_name(x509, X509_REQ_get_subject_name(req)) == 0) ERROR_OUT(ERR_SSL, done); if ((tmppkey = X509_REQ_get_pubkey(req)) == NULL) ERROR_OUT(ERR_SSL, done); if (X509_set_pubkey(x509, tmppkey) == 0) ERROR_OUT(ERR_SSL, done); if (X509_sign(x509, &pkey, EVP_sha1()) == 0) ERROR_OUT(ERR_SSL, done); /* write private key */ out = BIO_new(BIO_s_file()); if (BIO_write_filename(out, CA_PKEY) <= 0) ERROR_OUT(ERR_SSL, done); if (chmod(CA_PKEY, S_IRWXU)) ERROR_OUT(ERR_LIBC, done); if (PEM_write_bio_PrivateKey(out, &pkey, EVP_des_ede3_cbc(), NULL, 0, NULL, password) == 0) ERROR_OUT(ERR_SSL, done); BIO_free_all(out); /* write cert */ out = BIO_new(BIO_s_file()); if (BIO_write_filename(out, CA_CERT) <= 0) ERROR_OUT(ERR_SSL, done); if (PEM_write_bio_X509(out, x509) == 0) ERROR_OUT(ERR_SSL, done); BIO_free_all(out); rv = 0; done: if (tmppkey) EVP_PKEY_free(tmppkey); if (x509) X509_free(x509); if (req) X509_REQ_free(req); if (rsa) RSA_free(rsa); return (rv); }
正如你能看到的,我創建了一個可怕的機制,至少它能得到一些可用的錯誤堆棧來追蹤錯誤。這面就是這個令人可怕的宏:
/* errors */ #define ERR_LIBC (0) #define ERR_SSL (1) #define ERR_OWN (2) #define ERROR_OUT(e, g) do { push_error(__FILE__, __FUNCTION__, __LINE__, e); goto g; } while(0)
親愛的 $DEITY ,我請求你原諒我犯的罪。
讓我來展示一下其余的內容來完成這項工作,且希望我能幫助那些忍受這東西而丟了魂的人。函數與這個廢物一同工作:
char * geterror(int et) { char *es; switch (et) { case ERR_LIBC: strlcpy(last_error, strerror(errno), sizeof last_error); break; case ERR_SSL: es = (char *)ERR_lib_error_string(ERR_get_error()); if (es) strlcpy(last_error, es, sizeof last_error); else strlcpy(last_error, "unknown SSL error", sizeof last_error); break; default: strlcpy(last_error, "unknown error", sizeof last_error); /* FALLTHROUGH */ case ERR_OWN: break; } return (last_error); } void push_error(char *file, char *func, int line, int et) { struct error *ce; if ((ce = calloc(1, sizeof *ce)) == NULL) fatal("push_error ce"); if ((ce->file = strdup(file)) == NULL) fatal("push_error ce->file"); if ((ce->func = strdup(func)) == NULL) fatal("push_error ce->func"); if ((ce->errstr = strdup(geterror(et))) == NULL) fatal("push_error ce->errstr"); ce->line = line; SLIST_INSERT_HEAD(&ces, ce, dlink); }
下面是能讓它完全“工作”的剩下的實用功能:
int cert_find_put(char *entry, X509_NAME *subj, ssize_t min, ssize_t max) { struct valnode *v; int rv = 1; v = find_valtree(entry); if (v && v->length > 0) { if (min != -1 && v->length < min) { snprintf(last_error, sizeof last_error, "%s minimum constraint not met %lu < %lu", entry, v->length, min); ERROR_OUT(ERR_OWN, done); } if (max != -1 && v->length > max) { snprintf(last_error, sizeof last_error, "%s maximum constraint not met %lu > %lu", entry, v->length, max); ERROR_OUT(ERR_OWN, done); } if (X509_NAME_add_entry_by_txt(subj, entry, MBSTRING_ASC, v->value, -1, -1, 0) == 0) ERROR_OUT(ERR_SSL, done); } else { log_debug("cert_find_put: %s not found", entry); goto done; } rv = 0; done: return (rv); } int validate_canew(X509_NAME *subj, char **pwd) { struct valnode *password, *password2; int rv = 1; password = find_valtree("password"); password2 = find_valtree("password2"); if (password && password2) { if (strcmp(password->value, password2->value) || password->length == 0) { snprintf(last_error, sizeof last_error, "invalid password"); ERROR_OUT(ERR_OWN, done); } *pwd = password->value; } if (password == NULL && password2 == NULL) { snprintf(last_error, sizeof last_error, "password can't be NULL"); ERROR_OUT(ERR_OWN, done); } if (cert_find_put("C", subj, 2, 2)) { snprintf(last_error, sizeof last_error, "invalid country"); ERROR_OUT(ERR_OWN, done); } cert_find_put("ST", subj, -1, -1); cert_find_put("L", subj, -1, -1); cert_find_put("O", subj, -1, -1); cert_find_put("OU", subj, -1, -1); cert_find_put("CN", subj, -1, -1); cert_find_put("emailAddress", subj, -1, -1); rv = 0; done: return (rv); }
慶祝一下!我的天吶,我們有CA了。哈哈,該回家了!
回到家,我洗了個涼且哭了,等等,那是血嗎???
第六天:
我過后會寫一些LDAP的東西。我暫時需要處理別的東西。所以接下來,需要一個協商SSL或TLS的客戶端或服務器應用。剛開始,我測試了網上各種各樣的例子。
第一次測試失敗
好吧,我們需要看看操作手冊,我認為它們都會有例子的。
第二次失敗
我在網絡,例子,和許多時間中找到的東西都不奏效。
第三次失敗
夠了,我要回家了。
第七天:
好吧,是時候回到目前為止我最愛的代碼段上了。Openssl這個“工具”有S_server和s_client且如果你點動按鈕,他們似乎能工作。下面這些是我想出來的神奇命令:
openssl s_server -CAfile ca/ca.crt -cert server/server.crt -key server/private/server.key -Verify 1 openssl s_client -CAfile ca/ca.crt -cert client/client.crt -key client/private/client.key
越過SSL或TLS的連接是通過它實現的,而且它似乎對tcpdump也同樣有效。所以讓我們開始敲代碼吧!我將在這給你介紹我的代碼,再次地,我希望給其它小伙伴們開一個好的頭,而且希望能避免一些歧途。首先建立服務器:
#include <stdio.h> #include <stdlib.h> #include <err.h> #include <sys/types.h> #include <sys/socket.h> #include <netinet/in.h> #include "openssl/bio.h" #include "openssl/ssl.h" #include "openssl/err.h" void fatalx(char *s) { ERR_print_errors_fp(stderr); errx(1, s); } int main(int argc, char *argv[]) { SSL_CTX *ctx; BIO *sbio; SSL *ssl; int sock, s, r, val = -1; struct sockaddr_in sin; SSL_load_error_strings(); OpenSSL_add_ssl_algorithms(); ctx = SSL_CTX_new(SSLv23_server_method()); if (ctx == NULL) fatalx("ctx"); if (!SSL_CTX_load_verify_locations(ctx, "ca/ca.crt", NULL)) fatalx("verify"); SSL_CTX_set_client_CA_list(ctx, SSL_load_client_CA_file("ca/ca.crt")); if (!SSL_CTX_use_certificate_file(ctx, "server/server.crt", SSL_FILETYPE_PEM)) fatalx("cert"); if (!SSL_CTX_use_PrivateKey_file(ctx, "server/private/server.key", SSL_FILETYPE_PEM)) fatalx("key"); if (!SSL_CTX_check_private_key(ctx)) fatalx("cert/key"); SSL_CTX_set_mode(ctx, SSL_MODE_AUTO_RETRY); SSL_CTX_set_verify(ctx, SSL_VERIFY_PEER | SSL_VERIFY_FAIL_IF_NO_PEER_CERT, NULL); SSL_CTX_set_verify_depth(ctx, 1); /* setup socket */ if ((sock = socket(AF_INET, SOCK_STREAM, 0)) == -1) err(1, "socket"); bzero(&sin, sizeof sin); sin.sin_addr.s_addr = INADDR_ANY; sin.sin_family = AF_INET; sin.sin_port = htons(4433); setsockopt(sock, SOL_SOCKET, SO_REUSEADDR, &val, sizeof val); if (bind(sock, (struct sockaddr *)&sin, sizeof sin) == -1) err(1, "bind"); listen(sock, 0); for (;;) { if ((s = accept(sock, 0, 0)) == -1) err(1, "accept"); sbio = BIO_new_socket(s, BIO_NOCLOSE); ssl = SSL_new(ctx); SSL_set_bio(ssl, sbio, sbio); if ((r = SSL_accept(ssl)) == -1) fatalx("SSL_accept"); printf("handle it!\n"); } return (0); }
接下來是客戶端:
#include <stdio.h> #include <stdlib.h> #include <err.h> #include <sys/types.h> #include <sys/socket.h> #include <netinet/in.h> #include "openssl/bio.h" #include "openssl/ssl.h" #include "openssl/err.h" void fatalx(char *s) { ERR_print_errors_fp(stderr); errx(1, s); } int main(int argc, char *argv[]) { SSL_CTX *ctx; BIO *sbio; SSL *ssl; struct sockaddr_in addr; struct hostent *hp; int sock; SSL_load_error_strings(); OpenSSL_add_ssl_algorithms(); ctx = SSL_CTX_new(SSLv23_client_method()); if (ctx == NULL) fatalx("ctx"); if (!SSL_CTX_load_verify_locations(ctx, "ca/ca.crt", NULL)) fatalx("verify"); if (!SSL_CTX_use_certificate_file(ctx, "client/client.crt", SSL_FILETYPE_PEM)) fatalx("cert"); if (!SSL_CTX_use_PrivateKey_file(ctx, "client/private/client.key", SSL_FILETYPE_PEM)) fatalx("key"); if (!SSL_CTX_check_private_key(ctx)) fatalx("cert/key"); SSL_CTX_set_mode(ctx, SSL_MODE_AUTO_RETRY); SSL_CTX_set_verify(ctx, SSL_VERIFY_PEER, NULL); SSL_CTX_set_verify_depth(ctx, 1); /* setup connection */ if ((hp = gethostbyname("localhost")) == NULL) err(1, "gethostbyname"); bzero(&addr, sizeof addr); addr.sin_addr = *(struct in_addr *)hp->h_addr_list[0]; addr.sin_family = AF_INET; addr.sin_port = htons(4433); if ((sock = socket(AF_INET, SOCK_STREAM, 0)) == -1) err(1, "socket"); if (connect(sock, (struct sockaddr *)&addr, sizeof addr) == -1) err(1, "connect"); /* go do ssl magic */ ssl = SSL_new(ctx); sbio = BIO_new_socket(sock, BIO_NOCLOSE); SSL_set_bio(ssl, sbio, sbio); if (SSL_connect(ssl) <= 0) fatalx("SSL_connect"); if (SSL_get_verify_result(ssl) != X509_V_OK) fatalx("cert"); return (0); }
這不是我最好的狀態,但它還是能行的,且一些人可能會受益于此。回家!
第八天:
多看一些關于如何讓這些可惡的文件在LDAP內工作的東西。在會議和其它一些很遜的事之間,我放棄了且寫了這篇文章。隨著功能的完善,我會繼續更新的。我應該克服OpenSSL這個滿身臭味的家伙,做出自己的來。
<span id="shareA4" class="fl"> </span> </div>