令人作呕的OpenSSL
在OpenSSL心脏出血之后,我相信很多人都出了血,并且流了泪...网上瞬间出现了大量吐嘈OpenSSL的文章或段子,仿佛内心的窝火一瞬间被释放了出来,跟着这场疯闹,我也吐一下嘈,以雪这些年被OpenSSL蹂躏之辱,也许可以顺便展现一下我的无知与愚昧,但只是也许...
首先声明的一点是,我并没有恶意诋毁的意思,也并没有针对什么,比起生活中的大喜大悲,比起工作中的大起大落,比起追求理想过程中的遭遇坎坷,OpenSSL的折磨其实是一种幸福,只是对幸福的解读,有时可以认为是,痛并快乐着,齐秦如是说...
OpenSSL代码真的很烂,太烂,毫无章法的乱。
实用主义者,或者中毒已深的人总是能给出一段代码之所以这么写而不那么写的理由,并且理由还特别充分,以至于你也会认为这么写,写成这么烂是有理由的,其中一定藏着什么不易理解的玄机,但是,作为非神学的世俗作品,它不是圣经,不易理解本身就是一个过错,当然,也许是我水平太水太菜,没有达到OpenSSL要求的那种深度,如果这样,这篇吐嘈就是写给和我相同水平的菜鸟看的,高手请默默离开,不要带走一点悲哀,留下的这些悲哀,让我们这些菜鸟的眼泪洗刷刷吧...存在就是合理的,好吧,西西弗斯的神话表示人生就是一场悲哀,收成抵不上成本,它是存在的,因此是合理的,请不要报怨OpenSSL,它也是合理的,是的,完全正确。
开源是伟大的,至少曾经是伟大的,质疑它的人,一定没有体验过Linus Torvalds爆米且口的那份激动与听众受虐般的激情,也不一定拥有站在Richard Stallman或者极端的Eric S. Raymond脚下的那份敬畏和感动。但是OpenSSL出现以后,表明开源所表达的自由还有另外一层意思,那就是代码拥有不受审查的自由,有烂的自由,更多的,每个人都有使用烂代码的自由,更进一步的,每个人都有把烂代码说成艺术的自由,而这份自由,被OpenSSL那黑翼般的力量煽动,带给了每一个人,于是,心脏流血的时候,我攥起了拳头...
说多了都是泪...突然看到了一个项目,OpenBSD发起一个清理OpenSSL代码的项目,就想继续泪下去,等看完我这篇吐嘈,请带着泪去欣赏吧,链接在下面:
成都创新互联是一家专注于网站建设、成都网站制作与策划设计,永川网站建设哪家好?成都创新互联做网站,专注于网站建设十年,网设计领域的专业建站公司;建站业务涵盖:永川等地区。永川做网站价格咨询:13518219792
清爽链接1
清爽链接2
同样值得欣赏的是,Open×××的代码,同样狠烂!欣赏链接之前,请让我抛块砖,来点小菜。我们开始吧!如果一个函数声明为返回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 Argv int 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; #else s->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_RSA if (s->session->sess_cert->peer_rsa_tmp != NULL) { ... } #endif ... } else { ...; } ... #ifndef OPENSSL_NO_RSA if (alg & SSL_kRSA) { ... } #else /* OPENSSL_NO_RSA */ if (0) ; #endif #ifndef OPENSSL_NO_DH else if (alg & SSL_kEDH) { ... #ifndef OPENSSL_NO_RSA if (alg & SSL_aRSA) ... #else if (0) ; #endif #ifndef OPENSSL_NO_DSA else 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_ECDH else if (alg & SSL_kECDHE) { ... if (0) ; #ifndef OPENSSL_NO_RSA else if (alg & SSL_aRSA) ...; #endif #ifndef OPENSSL_NO_ECDSA else 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_RSA if (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_DSA if (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_ECDSA if (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_RSA if (rsa != NULL) RSA_free(rsa); #endif #ifndef OPENSSL_NO_DH if (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; else nw=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; else mac_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,调用者并不知道 // 会创建并发送这样一个record p = 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_build for (;;) { 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
本文地址:http://scjbc.cn/article/jdejjh.html