令人作呕的OpenSSL
生活随笔
收集整理的這篇文章主要介紹了
令人作呕的OpenSSL
小編覺得挺不錯的,現在分享給大家,幫大家做個參考.
在OpenSSL心臟出血之后,我相信非常多人都出了血,而且流了淚...網上瞬間出現了大量吐嘈OpenSSL的文章或段子,仿佛內心的窩火一瞬間被釋放了出來,跟著這場瘋鬧,我也吐一下嘈,以雪這些年被OpenSSL蹂躪之辱,或許能夠順便展現一下我的無知與愚昧,但僅僅是或許...
?????? 首先聲明的一點是,我并沒有惡意詆毀的意思,也并沒有針對什么,比起生活中的大喜大悲,比起工作中的大起大落,比起追求理想過程中的遭遇坎坷,OpenSSL的折磨其實是一種幸福,僅僅是對幸福的解讀,有時能夠認為是,痛并快樂著,齊秦如是說...
?????? OpenSSL代碼真的非常爛,太爛,毫無章法的亂。
?????? 有用主義者,或者中毒已深的人總是能給出一段代碼之所以這么寫而不那么寫的理由,而且理由還特別充分,以至于你也會認為這么寫,寫成這么爛是有理由的,當中一定藏著什么不易理解的玄機,可是,作為非神學的世俗作品,它不是圣經,不易理解本身就是一個過錯,當然,或許是我水平太水太菜,沒有達到OpenSSL要求的那種深度,假設這樣,這篇吐嘈就是寫給和我相同水平的菜鳥看的,高手請默默離開,不要帶走一點悲哀,留下的這些悲哀,讓我們這些菜鳥的眼淚洗刷刷吧...存在就是合理的,好吧,西西弗斯的神話表示人生就是一場悲哀,收成抵不上成本,它是存在的,因此是合理的,請不要報怨OpenSSL,它也是合理的,是的,全然正確。
?????? 開源是偉大的,至少以前是偉大的,質疑它的人,一定沒有體驗過Linus Torvalds爆米且口的那份激動與聽眾受虐般的激情,也不一定擁有站在Richard Stallman或者極端的Eric S. Raymond腳下的那份敬畏和感動。可是OpenSSL出現以后,表明開源所表達的自由還有另外一層意思,那就是代碼擁有不受審查的自由,有爛的自由,很多其他的,每一個人都有使用爛代碼的自由,更進一步的,每一個人都有把爛代碼說成藝術的自由,而這份自由,被OpenSSL那黑翼般的力量煽動,帶給了每一個人,于是,心臟流血的時候,我攥起了拳頭...
?????? 說多了都是淚...突然看到了一個項目,OpenBSD發起一個清理OpenSSL代碼的項目,就想繼續淚下去,等看完我這篇吐嘈,請帶著淚去贊賞吧,鏈接在以下:
假設一個函數聲明為返回int數據,可是在它的實現中卻:
{if ()return ret;else if ()return ret2; }這樣合理嗎?代碼當然是正確的,可是不明朗,不光人看得不明朗,有些編譯器也會抱怨...OpenSSL中大量這樣的代碼,悲哀的是,還不是OpenSSL的全部代碼都這樣!
?????? 我知道,在使用指針的時候,推斷一下是否為NULL能夠防止SIGSEGV的發送,可是假設你能明白它不為NULL的地方,再推斷就顯得多余了,否則就會到處都是這樣的推斷了,OpenSSL中大量冗余的非NULL推斷,表明表明了什么?我將繼續苦苦思索。
?????? 我無師自通地學會了魔術字的使用,這使得我寫的代碼帶有瞬時可理解性,當我看了OpenSSL之后,發現魔術字要是用得恰到優點,本身就能起到加密的功能。OpenSSL定義了太多的變量以及變量的組合,以至于整個OpenSSL都是在做“什么時候將變量賦給誰”這樣的事,有用主義者以及喜歡事后論事的家伙會說,不得不這么做,OpenSSL別無選擇!或許吧,OpenSSL是別無選擇,相同實現SSL的其他庫卻有太多的選擇!另外我以前喜歡用int變量來控制邏輯,比方
for (...) {if () {flag = 1;}...if (flag2 == 2) {flag = 2;}... } if (flag == 3 || flag2 == 1) { ... }我以前及其痛苦地在魔術字和flags之間進行選擇,由于我TMD根本就不懂軟件開發,我天真地以為軟件開發就是編程,就是讓代碼跑起來,直到我看到了OpenSSL,發現軟件開發要做的就是讓代碼跑起來這么簡單!!OpenSSL就能跑起來!前面說了,OpenSSL定義了太多的變量,可是卻還不夠多,由于到處會出現if (var == 2),var2=3,var3 < 5,之類的代碼,2,3,5代表什么意思呢?OpenSSL的凝視相同非常多,可是還不夠多,該有的凝視沒有,晦澀的地方一般都是jjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjj
?????? 請注意以下代碼,它展示了C語言塊的本質,并不是一定要是一個完整的函數,完整的條件推斷邏輯,完整的循環邏輯,我認為這樣的教人什么是“C語言塊”的方式僅僅能存在于譚浩強的書中,但OpenSSL做得更好:
some_function(...) { ...return(n);}/* If we get here, then type != rr->type; if we have a handshake* message, then it was unexpected (Hello Request or Client Hello). *//* In case of record types for which we have 'fragment' storage,* fill that so that we can process the data at a fixed place.*/{unsigned int dest_maxlen = 0;unsigned char *dest = NULL;unsigned int *dest_len = NULL;if (rr->type == SSL3_RT_HANDSHAKE){dest_maxlen = sizeof s->s3->handshake_fragment;dest = s->s3->handshake_fragment;dest_len = &s->s3->handshake_fragment_len;} ... }在函數中間夾了一個塊,夾得緊緊的,舒服嗎?可能是由于作者使用了不同的C標準,又想聲明新的變量,又不想動原來的代碼,不加新塊又編譯只是,僅僅好這么玩了...但僅僅是可能而已,其實作者可能根本就沒有想這么多,我個人也喜歡這么干,有時我的想法是嘗試一個新點子,假設不行的話又方便恢復成原來的,又討厭使用宏,主要是打字成本太高了,其實直到不久之前,我才知道在一個塊中,變量聲明的位置并不能是隨意的,當然,標準不同,限制也不同...
?????? C語言的宏是個好東西,可是也能造成流血事件,早些年的時候,我的一個經理在開會的時候說要用大量的宏營造出一些不同的編譯結果,后來由于那些宏造成了可怕的宏地獄,我們每周都要加班,后來我的另外一個同事把那個領導給打了,就在辦公室,真的打出血了,我不知道是不是跟大量的宏有關,我真的不知道。僅僅問你看了以下的代碼,想打人嗎?
#define ARGV Argvint main(int Argc, char *ARGV[])這么做的藝術性何在?我將繼續苦逼地上下而求索。
?????? OpenSSL代碼中大量的#if 0不說,還有以下奇葩的,凝視都給宏定義屏蔽了,編譯器迷惑了,凝視的第一行看得出是個凝視,可是卻找不到*/,哦,原來如此,凝視的后面部分被#if 0這個宏屏蔽了...
#if 0 /* worked only because C operator preferences are not as expected (and* because this is not really needed for clients except for detecting* protocol violations): */s->state=SSL_ST_BEFORE|(s->server)?SSL_ST_ACCEPT:SSL_ST_CONNECT; #elses->state = s->server ? SSL_ST_ACCEPT : SSL_ST_CONNECT; #endif注意上面凝視第一行的那個“(and”,看得出是作者有益這么做的,以表現一下自己的立體主義??
?????? OpenSSL毫無一致的風格,無論是縮進還是代碼本身,甚至在一個函數中都沒有一致的風格,相似以下這樣:
int func(){...a=b;c = d; }假設我寫出這樣的代碼,又要被罵了,可是慢慢的,我不認為因此被罵是一種讓人痛苦的事,就像OpenSSL一樣將自虐當成了快感的來源!
?????? 若不是我把以下的這段代碼的業務部分摳去,它絕對能夠參加IOCCC了,其實摳業務代碼的過程是痛苦的,全然沒有庖丁解牛那樣的快感,相反,就像摳屁股眼子一樣痛苦...
some_function(...) {...if (s->session->sess_cert != NULL){ #ifndef OPENSSL_NO_RSAif (s->session->sess_cert->peer_rsa_tmp != NULL){...} #endif...}else{...;}... #ifndef OPENSSL_NO_RSAif (alg & SSL_kRSA){...} #else /* OPENSSL_NO_RSA */if (0); #endif #ifndef OPENSSL_NO_DHelse if (alg & SSL_kEDH){... #ifndef OPENSSL_NO_RSAif (alg & SSL_aRSA)... #elseif (0); #endif #ifndef OPENSSL_NO_DSAelse if (alg & SSL_aDSS)...; #endif/* else anonymous DH, so no certificate or pkey. */...}else if ((alg & SSL_kDHr) || (alg & SSL_kDHd)){...goto f_err;} #endif /* !OPENSSL_NO_DH */#ifndef OPENSSL_NO_ECDHelse if (alg & SSL_kECDHE){...if (0) ; #ifndef OPENSSL_NO_RSAelse if (alg & SSL_aRSA)...; #endif #ifndef OPENSSL_NO_ECDSAelse if (alg & SSL_aECDSA)...; #endif/* else anonymous ECDH, so no certificate or pkey. */...}else if (alg & SSL_kECDH){...goto f_err;} #endif /* !OPENSSL_NO_ECDH */if (alg & SSL_aFZA){...goto f_err;}/* p points to the next byte, there are 'n' bytes left *//* if it was signed, check the signature */if (pkey != NULL){...if ((i != n) || (n > j) || (n <= 0)){/* wrong packet length */...goto f_err;}#ifndef OPENSSL_NO_RSAif (pkey->type == EVP_PKEY_RSA){...for (num=2; num > 0; num--){...}...if (i < 0){...goto f_err;}if (i == 0){/* bad signature */...goto f_err;}}else #endif #ifndef OPENSSL_NO_DSAif (pkey->type == EVP_PKEY_DSA){/* lets do DSS */...if (EVP_VerifyFinal(&md_ctx,p,(int)n,pkey) <= 0){/* bad signature */...goto f_err;}}else #endif #ifndef OPENSSL_NO_ECDSAif (pkey->type == EVP_PKEY_EC){/* let's do ECDSA */...if (EVP_VerifyFinal(&md_ctx,p,(int)n,pkey) <= 0){/* bad signature */...goto f_err;}}else #endif{...goto err;}}else{/* still data left over */if (!(alg & SSL_aNULL)){...goto err;}if (n != 0){...goto f_err;}}...return(1); f_err:...; err:...; #ifndef OPENSSL_NO_RSAif (rsa != NULL)RSA_free(rsa); #endif #ifndef OPENSSL_NO_DHif (dh != NULL)DH_free(dh); #endif #ifndef OPENSSL_NO_ECDH...;if (ecdh != NULL)EC_KEY_free(ecdh); #endif...;return(-1); }代碼是有點長了,可是實際的代碼就是如此!簡直就是宏的地獄,if (0)這樣的代碼的目的就是為了膠合諸多宏之間的相互排斥關系,讓相互排斥代碼的某部分不運行??唉,宏與宏之間發生了關系,你就不再是C編程,而是宏編程...話說,上述的代碼實際上是一個不含業務的邏輯框架,就像鋼混框架結構建筑的那個大架子一樣,和IOCCC獲獎代碼還是天上地下的,真正的IOCCC代碼是無框架的,框架隱藏于的業務本身,它的美感相似于相似海洋軟體動物的那種美。
?????? 事實證明,C語言的代碼跳轉機制是多種多樣的,僅僅會用goto那叫井底之蛙,可是有些時候,某個代碼段僅僅能用goto達到,這不是逼著人用goto的嗎?請看以下的代碼:
if(!ok) goto end;if (0){ end:X509_get_pubkey_parameters(NULL,ctx->chain);}其實想玩好if (0)僅僅有兩種方法,第一就是使用宏把if (0)屏蔽掉,第二就是使用goto把if (0)強暴掉,只是還有一種方式,把0的意義改掉。大量的#if 0,#if 1,if (0), if (1)的存在,外加一些令人看到“世界在進步”的凝視,將OpenSSL變成了一座僵尸博物館,這些永遠都不會被運行到的代碼旁邊都會有一些個凝視,詮釋著它們以前的光輝和日前為何變成了木乃伊。可是為何不把它們直接刪掉呢?既然已經知道了它們已然無用而且知道了為什么已然無用,還留著它們,我想作者們都是些懷舊之士吧。這使我們這些后來人在讀代碼或者改代碼的時候不得不先預處理一遍。對于我個人來講,我不喜歡預處理,我直接手工刪掉那些永不被運行的代碼,我甚至將此事作為當成一種無聊時的消遣,和展Windows注冊表一展一下午一樣獲得一種升華意義的快感!我真的以前展過注冊表,展了一下午都沒有展完...
?????? 3年前,我以前在OpenSSL的一個engine里面大量使用以下的代碼:
do { ... if (...)break; ... }while(0);我因這樣的代碼而被罵狗屎,只是當時我并沒有生氣,反而和還有一個同事在旁邊偷笑,聽說,笑能長壽,看來以后要多看看OpenSSL的代碼了。
?????? 笑固然好,可是哭是還有一種釋放壓力的手段,有時會比笑的效果更好。可是僅僅是上面這些還是無法把我弄哭的,能把我弄哭的是一段代碼的實現邏輯,事情是這樣的...老子雖不是什么高人,起碼也作為碼農辛勤耕耘好記載了,被OpenSSL如此蹂躪真的是說不出來的苦啊!
?????? SSL數據是留式的,即它沒有邊界,不像數據報協議,在底層,SSL紀錄協議是封裝在一個recode塊里面的,能夠認為在底層SSL是有邊界的,可是在上層它和TCP一樣,沒有邊界。可是我偏偏要用它來傳輸有邊界的IP數據報,OpenSSL的SSL_write/SSL_read接口又沒有暴露出SSL record的概念,我是多么希望SSL_write每次將傳入的buff作為一個record發送,而SSL_read則每次僅將一個record數據返回調用者啊,然而沒有不論什么標準規定它應該這么做,因此我就不能奢望OpenSSL是如此實現的。
?????? 幸好OpenSSL它是開源的,代碼能夠自己看,RTFSC!正如Linus大神說的那樣。可是看看ssl3_write_bytes的實現:
int ssl3_write_bytes(SSL *s, int type, const void *buf_, int len){...n=(len-tot);for (;;){if (n > SSL3_RT_MAX_PLAIN_LENGTH)nw=SSL3_RT_MAX_PLAIN_LENGTH;elsenw=n;// 我認為這是個核心函數i=do_ssl3_write(s, type, &(buf[tot]), nw, 0);if (i <= 0){s->s3->wnum=tot;return i;}if ((i == (int)n) ||(type == SSL3_RT_APPLICATION_DATA &&(s->mode & SSL_MODE_ENABLE_PARTIAL_WRITE))){/* next chunk of data should get another prepended empty fragment* in ciphersuites with known-IV weakness: */s->s3->empty_fragment_done = 0;return tot+i;}n-=i;tot+=i;}}看到這段代碼,一般人會怎么想?當然深深中了OpenSSL邪毒的那幫人不屬于一般人。一般人看了會認為,一個buff可能會分為多次發送,所以有了一個for(;;),直到發送完為止,假設接口行為定義良好,我應該放棄希望了,由于依照以上它的實現邏輯,一個buff可能會被切割為多段,每段調用do_ssl3_write發送,這樣一個buff就會形成多個record,從而打破了我的幻想,此時我想哭,由于我不得不再次去操家伙攪狗屎,噢,多么痛的領悟,多么直白的坦言。
?????? 幸好有高人相助,告訴我,理論上應該是一次write構造一個record的,我對此人的神乎膜拜促使我深入了do_ssl3_write函數內部,然后我打個個噴嚏,一眨巴淚眼,鼻涕吸到了嗓子里,咸咸的,但不苦...
static int do_ssl3_write(SSL *s, int type, const unsigned char *buf,unsigned int len, int create_empty_fragment){unsigned char *p,*plen;int i,mac_size,clear=0;int prefix_len = 0;SSL3_RECORD *wr;SSL3_BUFFER *wb;SSL_SESSION *sess;/* first check if there is a SSL3_BUFFER still being written* out. This will happen with non blocking IO */if (s->s3->wbuf.left != 0) // 在一開始的位置,處理邏輯就被劫持了,因此我就必須注意left在什么情況下不為0// 這個運行流跳轉得非常詭異!太詭異!return(ssl3_write_pending(s,type,buf,len));/* If we have an alert to send, lets send it */if (s->s3->alert_dispatch){i=s->method->ssl_dispatch_alert(s);if (i <= 0)return(i);/* if it went, fall through and send more stuff */}// create_empty_fragment?難道還有不這樣做的?Fxxxing,在上層調用的時候,這個參數為0,這就意味著// 肯定有什么地方以1為參數調用了本函數。這個empty fragment我后面會解釋。if (len == 0 && !create_empty_fragment)return 0;wr= &(s->s3->wrec);wb= &(s->s3->wbuf);sess=s->session;...if (clear)mac_size=0;elsemac_size=EVP_MD_size(s->write_hash);/* 'create_empty_fragment' is true only when this function calls itself */if (!clear && !create_empty_fragment && !s->s3->empty_fragment_done){/* countermeasure against known-IV weakness in CBC ciphersuites* (see http://www.openssl.org/~bodo/tls-cbc.txt) */if (s->s3->need_empty_fragments && type == SSL3_RT_APPLICATION_DATA){/* recursive function call with 'create_empty_fragment' set;* this prepares and buffers the data for an empty fragment* (these 'prefix_len' bytes are sent out later* together with the actual payload) */// 遞歸調用?我kao,這個函數居然有兩段邏輯:// 1.默默創建一個新的record;// 2.創建封裝buf的record并和遞歸調用中默默創建的那個record一起發送prefix_len = do_ssl3_write(s, type, buf, 0, 1);if (prefix_len <= 0)goto err;if (s->s3->wbuf.len < (size_t)prefix_len + SSL3_RT_MAX_PACKET_SIZE){/* insufficient space */SSLerr(SSL_F_DO_SSL3_WRITE, ERR_R_INTERNAL_ERROR);goto err;}}s->s3->empty_fragment_done = 1;}// wb->buf是和SSL綁定的一個發送buf,事先已經malloc好了內存,真TM大方!// 一個prefix_len表示在真正的record發送前緊接著的那個默默創建的record,調用者并不知道// 會創建并發送這樣一個recordp = wb->buf + prefix_len;/* write the header */// 這段代碼還算清晰// 可是,記住,在須要empty fragment的情況下會跑到這里兩次*(p++)=type&0xff;wr->type=type;*(p++)=(s->version>>8);*(p++)=s->version&0xff;/* field where we are to write out packet length */plen=p;p+=2;/* lets setup the record stuff. */wr->data=p;wr->length=(int)len;wr->input=(unsigned char *)buf;/* we now 'read' from wr->input, wr->length bytes into* wr->data *//* first we compress */if (s->compress != NULL){if (!ssl3_do_compress(s)){SSLerr(SSL_F_DO_SSL3_WRITE,SSL_R_COMPRESSION_FAILURE);goto err;}}else{memcpy(wr->data,wr->input,wr->length);wr->input=wr->data;}/* we should still have the output to wr->data and the input* from wr->input. Length should be wr->length.* wr->data still points in the wb->buf */if (mac_size != 0){s->method->ssl3_enc->mac(s,&(p[wr->length]),1);wr->length+=mac_size;wr->input=p;wr->data=p;}/* ssl3_enc can only have an error on read */s->method->ssl3_enc->enc(s,1);/* record length after mac and block padding */s2n(wr->length,plen);/* we should now have* wr->data pointing to the encrypted data, which is* wr->length long */wr->type=type; /* not needed but helps for debugging */wr->length+=SSL3_RT_HEADER_LENGTH;if (create_empty_fragment){/* we are in a recursive call;* just return the length, don't write out anything here*/// 假設是默默創建的那個record,則并不直接發送,目的是想將真實的record在內存上// 緊隨這個默默構造好的record作為一個buffer直接發送給下層BIO。為何不分別發送兩個// record呢?我想是為了緊湊使用SSL的s3->wbuf緩沖區吧,該緩沖區事先建立,而且還// 真不小:16K+!唉,真不認為實現者想不出更好的辦法了啊return wr->length;}/* now let's set up wb */wb->left = prefix_len + wr->length;wb->offset = 0;/* memorize arguments so that ssl3_write_pending can detect bad write retries later */s->s3->wpend_tot=len;s->s3->wpend_buf=buf;s->s3->wpend_type=type;s->s3->wpend_ret=len;/* we now just need to write the buffer */return ssl3_write_pending(s,type,buf,len); err:return -1;}上面的函數調用運行到最后的return ssl3_write_pending(s,type,buf,len)前,就會得到以下的一共wb->left大小的緩沖區:
|empty record header|empty record data|real record header|real record data|
終于的buff構造好了,能夠發送了吧,好的,能夠發送了!可是底層機制又來找茬了...在非堵塞IO模式下,底層的BIO并不一定能保證發完wb->left這么多數據,那么發多少返回多少,這也正常,關鍵是返回到了ssl3_write_bytes函數,也就是那個for(;;)調用do_ssl3_write的函數,然后一大堆if推斷,要么繼續,要么直接終于返回給SSL_write,無論如何,在你下次調用ssl3_write_bytes里面的do_ssl3_write的時候,僅僅要這兩個個record沒有寫完,即SSL的s3->wbuf.left不為0,就會在do_ssl3_write的最開始處直接調用ssl3_write_pending來保證一個record的寫入完成。
?????? 全部的問題在于,do_ssl3_write太復雜了,做的事情太多了,它做了3件事:1.構造empty fragment;2.構造真實record;3.保證這兩個record發送完成。邏輯太復雜,因此才邀請各種跳轉上陣...在給出我認為合理的邏輯之前,先簡單說下什么是empty fragment。它實際上是一個缺陷的修復,即針對CBC IV的攻擊,empty frag機制在每次發送record前先發送一個empty frag record,內部一些沒用的數據,接收端能夠在SSL協議層解密后隨意處理,它的目的就是在數據中間插入一些隨機因素以加大CBC模式的IV推測的難度。
?????? 我想不明白,發送上次未完成的數據為何要放在這么深的位置,我也想不明白,為何要用遞歸...難道就不能封裝一個build_record的函數嗎?難道就不能封裝一個write_raw函數嗎?既然empty fragment是一個安全加固機制,為何要隱藏它呢?直接:
build_record {操作SSL的s3->wbuf。我認為好,就繼續用 } write_raw {往下層BIO寫入SSL的s3->wbuf.buf的某一段 } do_ssl3_build {if (need_empty) {build_record;} build_record; ... }這樣是不是比遞歸更清晰呢?至于那個for (;;),我保留,僅僅是改動一下ssl3_write_bytes
ssl3_write_bytes {if (left) {write_pending}do_ssl3_buildfor (;;) {write_raw;} }you can you up,no can no BB!我怕死無葬身之地,這個話題就此打住,who can who up!只是我要說一點,那就是polarssl的實現,看看人家的ssl_write接口:
ssl_write() {if( ssl->state != SSL_HANDSHAKE_OVER ) {handshack;}if (left) {flush_pending and return <=0}build and write record, return num }這樣調用邏輯會比較簡單,更加清爽:
static int write_ssl_data( ssl_context *ssl, unsigned char *buf, size_t len ) {int ret;printf("\n%s", buf);while( len && ( ret = ssl_write( ssl, buf, len ) ) <= 0 ){if( ret != POLARSSL_ERR_NET_WANT_READ && ret != POLARSSL_ERR_NET_WANT_WRITE ){printf( " failed\n ! ssl_write returned %d\n\n", ret );return -1;}}return( 0 ); }看到這樣的代碼,我想憐香惜玉的人誰也不忍心增加if (0)逼著后來者用goto吧!
?????? 我對ssl3_write_pending的理解真的非常對嗎?不,我錯了!ssl3_write_pending真的會在沒有寫完record數據的情況下將left清0,那就是在DTLS的情況下,此時其調用者的那句凝視就說對了:
/* next chunk of data should get another prepended empty fragment
??? ??? ??? ?* in ciphersuites with known-IV weakness: */
這就是OpenSSL的全部,連凝視說的都不是全部情況。當然OpenSSL并沒有明白地凝視,總是保留一點解釋的空間,所以不要看它的凝視,還是看代碼吧,假設你想自虐的話...
?????? 首先聲明的一點是,我并沒有惡意詆毀的意思,也并沒有針對什么,比起生活中的大喜大悲,比起工作中的大起大落,比起追求理想過程中的遭遇坎坷,OpenSSL的折磨其實是一種幸福,僅僅是對幸福的解讀,有時能夠認為是,痛并快樂著,齊秦如是說...
?????? OpenSSL代碼真的非常爛,太爛,毫無章法的亂。
?????? 有用主義者,或者中毒已深的人總是能給出一段代碼之所以這么寫而不那么寫的理由,而且理由還特別充分,以至于你也會認為這么寫,寫成這么爛是有理由的,當中一定藏著什么不易理解的玄機,可是,作為非神學的世俗作品,它不是圣經,不易理解本身就是一個過錯,當然,或許是我水平太水太菜,沒有達到OpenSSL要求的那種深度,假設這樣,這篇吐嘈就是寫給和我相同水平的菜鳥看的,高手請默默離開,不要帶走一點悲哀,留下的這些悲哀,讓我們這些菜鳥的眼淚洗刷刷吧...存在就是合理的,好吧,西西弗斯的神話表示人生就是一場悲哀,收成抵不上成本,它是存在的,因此是合理的,請不要報怨OpenSSL,它也是合理的,是的,全然正確。
?????? 開源是偉大的,至少以前是偉大的,質疑它的人,一定沒有體驗過Linus Torvalds爆米且口的那份激動與聽眾受虐般的激情,也不一定擁有站在Richard Stallman或者極端的Eric S. Raymond腳下的那份敬畏和感動。可是OpenSSL出現以后,表明開源所表達的自由還有另外一層意思,那就是代碼擁有不受審查的自由,有爛的自由,很多其他的,每一個人都有使用爛代碼的自由,更進一步的,每一個人都有把爛代碼說成藝術的自由,而這份自由,被OpenSSL那黑翼般的力量煽動,帶給了每一個人,于是,心臟流血的時候,我攥起了拳頭...
?????? 說多了都是淚...突然看到了一個項目,OpenBSD發起一個清理OpenSSL代碼的項目,就想繼續淚下去,等看完我這篇吐嘈,請帶著淚去贊賞吧,鏈接在以下:
清爽鏈接1
清爽鏈接2
?????? 相同值得贊賞的是,OpenVPN的代碼,相同狠爛!贊賞鏈接之前,請讓我拋塊磚,來點小菜。我們開始吧!假設一個函數聲明為返回int數據,可是在它的實現中卻:
{if ()return ret;else if ()return ret2; }這樣合理嗎?代碼當然是正確的,可是不明朗,不光人看得不明朗,有些編譯器也會抱怨...OpenSSL中大量這樣的代碼,悲哀的是,還不是OpenSSL的全部代碼都這樣!
?????? 我知道,在使用指針的時候,推斷一下是否為NULL能夠防止SIGSEGV的發送,可是假設你能明白它不為NULL的地方,再推斷就顯得多余了,否則就會到處都是這樣的推斷了,OpenSSL中大量冗余的非NULL推斷,表明表明了什么?我將繼續苦苦思索。
?????? 我無師自通地學會了魔術字的使用,這使得我寫的代碼帶有瞬時可理解性,當我看了OpenSSL之后,發現魔術字要是用得恰到優點,本身就能起到加密的功能。OpenSSL定義了太多的變量以及變量的組合,以至于整個OpenSSL都是在做“什么時候將變量賦給誰”這樣的事,有用主義者以及喜歡事后論事的家伙會說,不得不這么做,OpenSSL別無選擇!或許吧,OpenSSL是別無選擇,相同實現SSL的其他庫卻有太多的選擇!另外我以前喜歡用int變量來控制邏輯,比方
for (...) {if () {flag = 1;}...if (flag2 == 2) {flag = 2;}... } if (flag == 3 || flag2 == 1) { ... }我以前及其痛苦地在魔術字和flags之間進行選擇,由于我TMD根本就不懂軟件開發,我天真地以為軟件開發就是編程,就是讓代碼跑起來,直到我看到了OpenSSL,發現軟件開發要做的就是讓代碼跑起來這么簡單!!OpenSSL就能跑起來!前面說了,OpenSSL定義了太多的變量,可是卻還不夠多,由于到處會出現if (var == 2),var2=3,var3 < 5,之類的代碼,2,3,5代表什么意思呢?OpenSSL的凝視相同非常多,可是還不夠多,該有的凝視沒有,晦澀的地方一般都是jjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjj
?????? 請注意以下代碼,它展示了C語言塊的本質,并不是一定要是一個完整的函數,完整的條件推斷邏輯,完整的循環邏輯,我認為這樣的教人什么是“C語言塊”的方式僅僅能存在于譚浩強的書中,但OpenSSL做得更好:
some_function(...) { ...return(n);}/* If we get here, then type != rr->type; if we have a handshake* message, then it was unexpected (Hello Request or Client Hello). *//* In case of record types for which we have 'fragment' storage,* fill that so that we can process the data at a fixed place.*/{unsigned int dest_maxlen = 0;unsigned char *dest = NULL;unsigned int *dest_len = NULL;if (rr->type == SSL3_RT_HANDSHAKE){dest_maxlen = sizeof s->s3->handshake_fragment;dest = s->s3->handshake_fragment;dest_len = &s->s3->handshake_fragment_len;} ... }在函數中間夾了一個塊,夾得緊緊的,舒服嗎?可能是由于作者使用了不同的C標準,又想聲明新的變量,又不想動原來的代碼,不加新塊又編譯只是,僅僅好這么玩了...但僅僅是可能而已,其實作者可能根本就沒有想這么多,我個人也喜歡這么干,有時我的想法是嘗試一個新點子,假設不行的話又方便恢復成原來的,又討厭使用宏,主要是打字成本太高了,其實直到不久之前,我才知道在一個塊中,變量聲明的位置并不能是隨意的,當然,標準不同,限制也不同...
?????? C語言的宏是個好東西,可是也能造成流血事件,早些年的時候,我的一個經理在開會的時候說要用大量的宏營造出一些不同的編譯結果,后來由于那些宏造成了可怕的宏地獄,我們每周都要加班,后來我的另外一個同事把那個領導給打了,就在辦公室,真的打出血了,我不知道是不是跟大量的宏有關,我真的不知道。僅僅問你看了以下的代碼,想打人嗎?
#define ARGV Argvint main(int Argc, char *ARGV[])這么做的藝術性何在?我將繼續苦逼地上下而求索。
?????? OpenSSL代碼中大量的#if 0不說,還有以下奇葩的,凝視都給宏定義屏蔽了,編譯器迷惑了,凝視的第一行看得出是個凝視,可是卻找不到*/,哦,原來如此,凝視的后面部分被#if 0這個宏屏蔽了...
#if 0 /* worked only because C operator preferences are not as expected (and* because this is not really needed for clients except for detecting* protocol violations): */s->state=SSL_ST_BEFORE|(s->server)?SSL_ST_ACCEPT:SSL_ST_CONNECT; #elses->state = s->server ? SSL_ST_ACCEPT : SSL_ST_CONNECT; #endif注意上面凝視第一行的那個“(and”,看得出是作者有益這么做的,以表現一下自己的立體主義??
?????? OpenSSL毫無一致的風格,無論是縮進還是代碼本身,甚至在一個函數中都沒有一致的風格,相似以下這樣:
int func(){...a=b;c = d; }假設我寫出這樣的代碼,又要被罵了,可是慢慢的,我不認為因此被罵是一種讓人痛苦的事,就像OpenSSL一樣將自虐當成了快感的來源!
?????? 若不是我把以下的這段代碼的業務部分摳去,它絕對能夠參加IOCCC了,其實摳業務代碼的過程是痛苦的,全然沒有庖丁解牛那樣的快感,相反,就像摳屁股眼子一樣痛苦...
some_function(...) {...if (s->session->sess_cert != NULL){ #ifndef OPENSSL_NO_RSAif (s->session->sess_cert->peer_rsa_tmp != NULL){...} #endif...}else{...;}... #ifndef OPENSSL_NO_RSAif (alg & SSL_kRSA){...} #else /* OPENSSL_NO_RSA */if (0); #endif #ifndef OPENSSL_NO_DHelse if (alg & SSL_kEDH){... #ifndef OPENSSL_NO_RSAif (alg & SSL_aRSA)... #elseif (0); #endif #ifndef OPENSSL_NO_DSAelse if (alg & SSL_aDSS)...; #endif/* else anonymous DH, so no certificate or pkey. */...}else if ((alg & SSL_kDHr) || (alg & SSL_kDHd)){...goto f_err;} #endif /* !OPENSSL_NO_DH */#ifndef OPENSSL_NO_ECDHelse if (alg & SSL_kECDHE){...if (0) ; #ifndef OPENSSL_NO_RSAelse if (alg & SSL_aRSA)...; #endif #ifndef OPENSSL_NO_ECDSAelse if (alg & SSL_aECDSA)...; #endif/* else anonymous ECDH, so no certificate or pkey. */...}else if (alg & SSL_kECDH){...goto f_err;} #endif /* !OPENSSL_NO_ECDH */if (alg & SSL_aFZA){...goto f_err;}/* p points to the next byte, there are 'n' bytes left *//* if it was signed, check the signature */if (pkey != NULL){...if ((i != n) || (n > j) || (n <= 0)){/* wrong packet length */...goto f_err;}#ifndef OPENSSL_NO_RSAif (pkey->type == EVP_PKEY_RSA){...for (num=2; num > 0; num--){...}...if (i < 0){...goto f_err;}if (i == 0){/* bad signature */...goto f_err;}}else #endif #ifndef OPENSSL_NO_DSAif (pkey->type == EVP_PKEY_DSA){/* lets do DSS */...if (EVP_VerifyFinal(&md_ctx,p,(int)n,pkey) <= 0){/* bad signature */...goto f_err;}}else #endif #ifndef OPENSSL_NO_ECDSAif (pkey->type == EVP_PKEY_EC){/* let's do ECDSA */...if (EVP_VerifyFinal(&md_ctx,p,(int)n,pkey) <= 0){/* bad signature */...goto f_err;}}else #endif{...goto err;}}else{/* still data left over */if (!(alg & SSL_aNULL)){...goto err;}if (n != 0){...goto f_err;}}...return(1); f_err:...; err:...; #ifndef OPENSSL_NO_RSAif (rsa != NULL)RSA_free(rsa); #endif #ifndef OPENSSL_NO_DHif (dh != NULL)DH_free(dh); #endif #ifndef OPENSSL_NO_ECDH...;if (ecdh != NULL)EC_KEY_free(ecdh); #endif...;return(-1); }代碼是有點長了,可是實際的代碼就是如此!簡直就是宏的地獄,if (0)這樣的代碼的目的就是為了膠合諸多宏之間的相互排斥關系,讓相互排斥代碼的某部分不運行??唉,宏與宏之間發生了關系,你就不再是C編程,而是宏編程...話說,上述的代碼實際上是一個不含業務的邏輯框架,就像鋼混框架結構建筑的那個大架子一樣,和IOCCC獲獎代碼還是天上地下的,真正的IOCCC代碼是無框架的,框架隱藏于的業務本身,它的美感相似于相似海洋軟體動物的那種美。
?????? 事實證明,C語言的代碼跳轉機制是多種多樣的,僅僅會用goto那叫井底之蛙,可是有些時候,某個代碼段僅僅能用goto達到,這不是逼著人用goto的嗎?請看以下的代碼:
if(!ok) goto end;if (0){ end:X509_get_pubkey_parameters(NULL,ctx->chain);}其實想玩好if (0)僅僅有兩種方法,第一就是使用宏把if (0)屏蔽掉,第二就是使用goto把if (0)強暴掉,只是還有一種方式,把0的意義改掉。大量的#if 0,#if 1,if (0), if (1)的存在,外加一些令人看到“世界在進步”的凝視,將OpenSSL變成了一座僵尸博物館,這些永遠都不會被運行到的代碼旁邊都會有一些個凝視,詮釋著它們以前的光輝和日前為何變成了木乃伊。可是為何不把它們直接刪掉呢?既然已經知道了它們已然無用而且知道了為什么已然無用,還留著它們,我想作者們都是些懷舊之士吧。這使我們這些后來人在讀代碼或者改代碼的時候不得不先預處理一遍。對于我個人來講,我不喜歡預處理,我直接手工刪掉那些永不被運行的代碼,我甚至將此事作為當成一種無聊時的消遣,和展Windows注冊表一展一下午一樣獲得一種升華意義的快感!我真的以前展過注冊表,展了一下午都沒有展完...
?????? 3年前,我以前在OpenSSL的一個engine里面大量使用以下的代碼:
do { ... if (...)break; ... }while(0);我因這樣的代碼而被罵狗屎,只是當時我并沒有生氣,反而和還有一個同事在旁邊偷笑,聽說,笑能長壽,看來以后要多看看OpenSSL的代碼了。
?????? 笑固然好,可是哭是還有一種釋放壓力的手段,有時會比笑的效果更好。可是僅僅是上面這些還是無法把我弄哭的,能把我弄哭的是一段代碼的實現邏輯,事情是這樣的...老子雖不是什么高人,起碼也作為碼農辛勤耕耘好記載了,被OpenSSL如此蹂躪真的是說不出來的苦啊!
?????? SSL數據是留式的,即它沒有邊界,不像數據報協議,在底層,SSL紀錄協議是封裝在一個recode塊里面的,能夠認為在底層SSL是有邊界的,可是在上層它和TCP一樣,沒有邊界。可是我偏偏要用它來傳輸有邊界的IP數據報,OpenSSL的SSL_write/SSL_read接口又沒有暴露出SSL record的概念,我是多么希望SSL_write每次將傳入的buff作為一個record發送,而SSL_read則每次僅將一個record數據返回調用者啊,然而沒有不論什么標準規定它應該這么做,因此我就不能奢望OpenSSL是如此實現的。
?????? 幸好OpenSSL它是開源的,代碼能夠自己看,RTFSC!正如Linus大神說的那樣。可是看看ssl3_write_bytes的實現:
int ssl3_write_bytes(SSL *s, int type, const void *buf_, int len){...n=(len-tot);for (;;){if (n > SSL3_RT_MAX_PLAIN_LENGTH)nw=SSL3_RT_MAX_PLAIN_LENGTH;elsenw=n;// 我認為這是個核心函數i=do_ssl3_write(s, type, &(buf[tot]), nw, 0);if (i <= 0){s->s3->wnum=tot;return i;}if ((i == (int)n) ||(type == SSL3_RT_APPLICATION_DATA &&(s->mode & SSL_MODE_ENABLE_PARTIAL_WRITE))){/* next chunk of data should get another prepended empty fragment* in ciphersuites with known-IV weakness: */s->s3->empty_fragment_done = 0;return tot+i;}n-=i;tot+=i;}}看到這段代碼,一般人會怎么想?當然深深中了OpenSSL邪毒的那幫人不屬于一般人。一般人看了會認為,一個buff可能會分為多次發送,所以有了一個for(;;),直到發送完為止,假設接口行為定義良好,我應該放棄希望了,由于依照以上它的實現邏輯,一個buff可能會被切割為多段,每段調用do_ssl3_write發送,這樣一個buff就會形成多個record,從而打破了我的幻想,此時我想哭,由于我不得不再次去操家伙攪狗屎,噢,多么痛的領悟,多么直白的坦言。
?????? 幸好有高人相助,告訴我,理論上應該是一次write構造一個record的,我對此人的神乎膜拜促使我深入了do_ssl3_write函數內部,然后我打個個噴嚏,一眨巴淚眼,鼻涕吸到了嗓子里,咸咸的,但不苦...
static int do_ssl3_write(SSL *s, int type, const unsigned char *buf,unsigned int len, int create_empty_fragment){unsigned char *p,*plen;int i,mac_size,clear=0;int prefix_len = 0;SSL3_RECORD *wr;SSL3_BUFFER *wb;SSL_SESSION *sess;/* first check if there is a SSL3_BUFFER still being written* out. This will happen with non blocking IO */if (s->s3->wbuf.left != 0) // 在一開始的位置,處理邏輯就被劫持了,因此我就必須注意left在什么情況下不為0// 這個運行流跳轉得非常詭異!太詭異!return(ssl3_write_pending(s,type,buf,len));/* If we have an alert to send, lets send it */if (s->s3->alert_dispatch){i=s->method->ssl_dispatch_alert(s);if (i <= 0)return(i);/* if it went, fall through and send more stuff */}// create_empty_fragment?難道還有不這樣做的?Fxxxing,在上層調用的時候,這個參數為0,這就意味著// 肯定有什么地方以1為參數調用了本函數。這個empty fragment我后面會解釋。if (len == 0 && !create_empty_fragment)return 0;wr= &(s->s3->wrec);wb= &(s->s3->wbuf);sess=s->session;...if (clear)mac_size=0;elsemac_size=EVP_MD_size(s->write_hash);/* 'create_empty_fragment' is true only when this function calls itself */if (!clear && !create_empty_fragment && !s->s3->empty_fragment_done){/* countermeasure against known-IV weakness in CBC ciphersuites* (see http://www.openssl.org/~bodo/tls-cbc.txt) */if (s->s3->need_empty_fragments && type == SSL3_RT_APPLICATION_DATA){/* recursive function call with 'create_empty_fragment' set;* this prepares and buffers the data for an empty fragment* (these 'prefix_len' bytes are sent out later* together with the actual payload) */// 遞歸調用?我kao,這個函數居然有兩段邏輯:// 1.默默創建一個新的record;// 2.創建封裝buf的record并和遞歸調用中默默創建的那個record一起發送prefix_len = do_ssl3_write(s, type, buf, 0, 1);if (prefix_len <= 0)goto err;if (s->s3->wbuf.len < (size_t)prefix_len + SSL3_RT_MAX_PACKET_SIZE){/* insufficient space */SSLerr(SSL_F_DO_SSL3_WRITE, ERR_R_INTERNAL_ERROR);goto err;}}s->s3->empty_fragment_done = 1;}// wb->buf是和SSL綁定的一個發送buf,事先已經malloc好了內存,真TM大方!// 一個prefix_len表示在真正的record發送前緊接著的那個默默創建的record,調用者并不知道// 會創建并發送這樣一個recordp = wb->buf + prefix_len;/* write the header */// 這段代碼還算清晰// 可是,記住,在須要empty fragment的情況下會跑到這里兩次*(p++)=type&0xff;wr->type=type;*(p++)=(s->version>>8);*(p++)=s->version&0xff;/* field where we are to write out packet length */plen=p;p+=2;/* lets setup the record stuff. */wr->data=p;wr->length=(int)len;wr->input=(unsigned char *)buf;/* we now 'read' from wr->input, wr->length bytes into* wr->data *//* first we compress */if (s->compress != NULL){if (!ssl3_do_compress(s)){SSLerr(SSL_F_DO_SSL3_WRITE,SSL_R_COMPRESSION_FAILURE);goto err;}}else{memcpy(wr->data,wr->input,wr->length);wr->input=wr->data;}/* we should still have the output to wr->data and the input* from wr->input. Length should be wr->length.* wr->data still points in the wb->buf */if (mac_size != 0){s->method->ssl3_enc->mac(s,&(p[wr->length]),1);wr->length+=mac_size;wr->input=p;wr->data=p;}/* ssl3_enc can only have an error on read */s->method->ssl3_enc->enc(s,1);/* record length after mac and block padding */s2n(wr->length,plen);/* we should now have* wr->data pointing to the encrypted data, which is* wr->length long */wr->type=type; /* not needed but helps for debugging */wr->length+=SSL3_RT_HEADER_LENGTH;if (create_empty_fragment){/* we are in a recursive call;* just return the length, don't write out anything here*/// 假設是默默創建的那個record,則并不直接發送,目的是想將真實的record在內存上// 緊隨這個默默構造好的record作為一個buffer直接發送給下層BIO。為何不分別發送兩個// record呢?我想是為了緊湊使用SSL的s3->wbuf緩沖區吧,該緩沖區事先建立,而且還// 真不小:16K+!唉,真不認為實現者想不出更好的辦法了啊return wr->length;}/* now let's set up wb */wb->left = prefix_len + wr->length;wb->offset = 0;/* memorize arguments so that ssl3_write_pending can detect bad write retries later */s->s3->wpend_tot=len;s->s3->wpend_buf=buf;s->s3->wpend_type=type;s->s3->wpend_ret=len;/* we now just need to write the buffer */return ssl3_write_pending(s,type,buf,len); err:return -1;}上面的函數調用運行到最后的return ssl3_write_pending(s,type,buf,len)前,就會得到以下的一共wb->left大小的緩沖區:
|empty record header|empty record data|real record header|real record data|
終于的buff構造好了,能夠發送了吧,好的,能夠發送了!可是底層機制又來找茬了...在非堵塞IO模式下,底層的BIO并不一定能保證發完wb->left這么多數據,那么發多少返回多少,這也正常,關鍵是返回到了ssl3_write_bytes函數,也就是那個for(;;)調用do_ssl3_write的函數,然后一大堆if推斷,要么繼續,要么直接終于返回給SSL_write,無論如何,在你下次調用ssl3_write_bytes里面的do_ssl3_write的時候,僅僅要這兩個個record沒有寫完,即SSL的s3->wbuf.left不為0,就會在do_ssl3_write的最開始處直接調用ssl3_write_pending來保證一個record的寫入完成。
?????? 全部的問題在于,do_ssl3_write太復雜了,做的事情太多了,它做了3件事:1.構造empty fragment;2.構造真實record;3.保證這兩個record發送完成。邏輯太復雜,因此才邀請各種跳轉上陣...在給出我認為合理的邏輯之前,先簡單說下什么是empty fragment。它實際上是一個缺陷的修復,即針對CBC IV的攻擊,empty frag機制在每次發送record前先發送一個empty frag record,內部一些沒用的數據,接收端能夠在SSL協議層解密后隨意處理,它的目的就是在數據中間插入一些隨機因素以加大CBC模式的IV推測的難度。
?????? 我想不明白,發送上次未完成的數據為何要放在這么深的位置,我也想不明白,為何要用遞歸...難道就不能封裝一個build_record的函數嗎?難道就不能封裝一個write_raw函數嗎?既然empty fragment是一個安全加固機制,為何要隱藏它呢?直接:
build_record {操作SSL的s3->wbuf。我認為好,就繼續用 } write_raw {往下層BIO寫入SSL的s3->wbuf.buf的某一段 } do_ssl3_build {if (need_empty) {build_record;} build_record; ... }這樣是不是比遞歸更清晰呢?至于那個for (;;),我保留,僅僅是改動一下ssl3_write_bytes
ssl3_write_bytes {if (left) {write_pending}do_ssl3_buildfor (;;) {write_raw;} }you can you up,no can no BB!我怕死無葬身之地,這個話題就此打住,who can who up!只是我要說一點,那就是polarssl的實現,看看人家的ssl_write接口:
ssl_write() {if( ssl->state != SSL_HANDSHAKE_OVER ) {handshack;}if (left) {flush_pending and return <=0}build and write record, return num }這樣調用邏輯會比較簡單,更加清爽:
static int write_ssl_data( ssl_context *ssl, unsigned char *buf, size_t len ) {int ret;printf("\n%s", buf);while( len && ( ret = ssl_write( ssl, buf, len ) ) <= 0 ){if( ret != POLARSSL_ERR_NET_WANT_READ && ret != POLARSSL_ERR_NET_WANT_WRITE ){printf( " failed\n ! ssl_write returned %d\n\n", ret );return -1;}}return( 0 ); }看到這樣的代碼,我想憐香惜玉的人誰也不忍心增加if (0)逼著后來者用goto吧!
?????? 我對ssl3_write_pending的理解真的非常對嗎?不,我錯了!ssl3_write_pending真的會在沒有寫完record數據的情況下將left清0,那就是在DTLS的情況下,此時其調用者的那句凝視就說對了:
/* next chunk of data should get another prepended empty fragment
??? ??? ??? ?* in ciphersuites with known-IV weakness: */
這就是OpenSSL的全部,連凝視說的都不是全部情況。當然OpenSSL并沒有明白地凝視,總是保留一點解釋的空間,所以不要看它的凝視,還是看代碼吧,假設你想自虐的話...
轉載于:https://www.cnblogs.com/bhlsheji/p/4306183.html
總結
以上是生活随笔為你收集整理的令人作呕的OpenSSL的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: BE病毒8.8E+1&nbsp;
- 下一篇: [Flex] 组件Tree系列 —— 阻