首先声明的一点是,我并没有恶意诋毁的意思,也并没有针对什么,比起生活中的大喜大悲。比起工作中的大起大落,比起追求理想过程中的遭遇坎坷,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 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_bytesssl3_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并没有明白地凝视,总是保留一点解释的空间。所以不要看它的凝视,还是看代码吧,假设你想自虐的话...