溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊(cè)×
其他方式登錄
點(diǎn)擊 登錄注冊(cè) 即表示同意《億速云用戶(hù)服務(wù)條款》

令人作嘔的OpenSSL

發(fā)布時(shí)間:2020-07-12 05:36:53 來(lái)源:網(wǎng)絡(luò) 閱讀:4740 作者:dog250 欄目:安全技術(shù)

在OpenSSL心臟出血之后,我相信很多人都出了血,并且流了淚...網(wǎng)上瞬間出現(xiàn)了大量吐嘈OpenSSL的文章或段子,仿佛內(nèi)心的窩火一瞬間被釋放了出來(lái),跟著這場(chǎng)瘋鬧,我也吐一下嘈,以雪這些年被OpenSSL蹂躪之辱,也許可以順便展現(xiàn)一下我的無(wú)知與愚昧,但只是也許...
      首先聲明的一點(diǎn)是,我并沒(méi)有惡意詆毀的意思,也并沒(méi)有針對(duì)什么,比起生活中的大喜大悲,比起工作中的大起大落,比起追求理想過(guò)程中的遭遇坎坷,OpenSSL的折磨其實(shí)是一種幸福,只是對(duì)幸福的解讀,有時(shí)可以認(rèn)為是,痛并快樂(lè)著,齊秦如是說(shuō)...
      OpenSSL代碼真的很爛,太爛,毫無(wú)章法的亂。
      實(shí)用主義者,或者中毒已深的人總是能給出一段代碼之所以這么寫(xiě)而不那么寫(xiě)的理由,并且理由還特別充分,以至于你也會(huì)認(rèn)為這么寫(xiě),寫(xiě)成這么爛是有理由的,其中一定藏著什么不易理解的玄機(jī),但是,作為非神學(xué)的世俗作品,它不是圣經(jīng),不易理解本身就是一個(gè)過(guò)錯(cuò),當(dāng)然,也許是我水平太水太菜,沒(méi)有達(dá)到OpenSSL要求的那種深度,如果這樣,這篇吐嘈就是寫(xiě)給和我相同水平的菜鳥(niǎo)看的,高手請(qǐng)默默離開(kāi),不要帶走一點(diǎn)悲哀,留下的這些悲哀,讓我們這些菜鳥(niǎo)的眼淚洗刷刷吧...存在就是合理的,好吧,西西弗斯的神話(huà)表示人生就是一場(chǎng)悲哀,收成抵不上成本,它是存在的,因此是合理的,請(qǐng)不要報(bào)怨OpenSSL,它也是合理的,是的,完全正確。
      開(kāi)源是偉大的,至少曾經(jīng)是偉大的,質(zhì)疑它的人,一定沒(méi)有體驗(yàn)過(guò)Linus Torvalds爆米且口的那份激動(dòng)與聽(tīng)眾受虐般的激情,也不一定擁有站在Richard Stallman或者極端的Eric S. Raymond腳下的那份敬畏和感動(dòng)。但是OpenSSL出現(xiàn)以后,表明開(kāi)源所表達(dá)的自由還有另外一層意思,那就是代碼擁有不受審查的自由,有爛的自由,更多的,每個(gè)人都有使用爛代碼的自由,更進(jìn)一步的,每個(gè)人都有把爛代碼說(shuō)成藝術(shù)的自由,而這份自由,被OpenSSL那黑翼般的力量煽動(dòng),帶給了每一個(gè)人,于是,心臟流血的時(shí)候,我攥起了拳頭...
      說(shuō)多了都是淚...突然看到了一個(gè)項(xiàng)目,OpenBSD發(fā)起一個(gè)清理OpenSSL代碼的項(xiàng)目,就想繼續(xù)淚下去,等看完我這篇吐嘈,請(qǐng)帶著淚去欣賞吧,鏈接在下面:

清爽鏈接1

清爽鏈接2

      同樣值得欣賞的是,Open×××的代碼,同樣狠爛!欣賞鏈接之前,請(qǐng)讓我拋塊磚,來(lái)點(diǎn)小菜。我們開(kāi)始吧!
如果一個(gè)函數(shù)聲明為返回int數(shù)據(jù),可是在它的實(shí)現(xiàn)中卻:
{
    if ()
       return ret;
     else if ()
       return ret2;
}
這樣合理嗎?代碼當(dāng)然是正確的,但是不明朗,不光人看得不明朗,有些編譯器也會(huì)抱怨...OpenSSL中大量這種代碼,悲哀的是,還不是OpenSSL的全部代碼都這樣!
      我知道,在使用指針的時(shí)候,判斷一下是否為NULL可以防止SIGSEGV的發(fā)送,但是如果你能明確它不為NULL的地方,再判斷就顯得多余了,否則就會(huì)到處都是這種判斷了,OpenSSL中大量冗余的非NULL判斷,表明表明了什么?我將繼續(xù)苦苦思索。
      我無(wú)師自通地學(xué)會(huì)了魔術(shù)字的使用,這使得我寫(xiě)的代碼帶有瞬時(shí)可理解性,當(dāng)我看了OpenSSL之后,發(fā)現(xiàn)魔術(shù)字要是用得恰到好處,本身就能起到加密的功能。OpenSSL定義了太多的變量以及變量的組合,以至于整個(gè)OpenSSL都是在做“什么時(shí)候?qū)⒆兞抠x給誰(shuí)”這種事,實(shí)用主義者以及喜歡事后論事的家伙會(huì)說(shuō),不得不這么做,OpenSSL別無(wú)選擇!也許吧,OpenSSL是別無(wú)選擇,同樣實(shí)現(xiàn)SSL的其它庫(kù)卻有太多的選擇!另外我曾經(jīng)喜歡用int變量來(lái)控制邏輯,比如
for (...) {
    if () {
        flag = 1;
    }
   ...
    if (flag2 == 2) {
       flag = 2;
    }
    ...
}
if (flag == 3 || flag2 == 1) {
...
}
我曾經(jīng)及其痛苦地在魔術(shù)字和flags之間進(jìn)行選擇,因?yàn)槲襎MD根本就不懂軟件開(kāi)發(fā),我天真地以為軟件開(kāi)發(fā)就是編程,就是讓代碼跑起來(lái),直到我看到了OpenSSL,發(fā)現(xiàn)軟件開(kāi)發(fā)要做的就是讓代碼跑起來(lái)這么簡(jiǎn)單!!OpenSSL就能跑起來(lái)!前面說(shuō)了,OpenSSL定義了太多的變量,但是卻還不夠多,因?yàn)榈教帟?huì)出現(xiàn)if (var == 2),var2=3,var3 < 5,之類(lèi)的代碼,2,3,5代表什么意思呢?OpenSSL的注釋同樣很多,但是還不夠多,該有的注釋沒(méi)有,晦澀的地方一般都是jjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjj
      請(qǐng)注意以下代碼,它展示了C語(yǔ)言塊的本質(zhì),并非一定要是一個(gè)完整的函數(shù),完整的條件判斷邏輯,完整的循環(huán)邏輯,我覺(jué)得這種教人什么是“C語(yǔ)言塊”的方式只能存在于譚浩強(qiáng)的書(shū)中,但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;
            }
...
}
在函數(shù)中間夾了一個(gè)塊,夾得緊緊的,舒服嗎?可能是因?yàn)樽髡呤褂昧瞬煌腃標(biāo)準(zhǔn),又想聲明新的變量,又不想動(dòng)原來(lái)的代碼,不加新塊又編譯不過(guò),只好這么玩了...但只是可能而已,事實(shí)上作者可能根本就沒(méi)有想這么多,我個(gè)人也喜歡這么干,有時(shí)我的想法是嘗試一個(gè)新點(diǎn)子,如果不行的話(huà)又方便恢復(fù)成原來(lái)的,又討厭使用宏,主要是打字成本太高了,事實(shí)上直到不久之前,我才知道在一個(gè)塊中,變量聲明的位置并不能是隨意的,當(dāng)然,標(biāo)準(zhǔn)不同,限制也不同...
      C語(yǔ)言的宏是個(gè)好東西,但是也能造成流血事件,早些年的時(shí)候,我的一個(gè)經(jīng)理在開(kāi)會(huì)的時(shí)候說(shuō)要用大量的宏?duì)I造出一些不同的編譯結(jié)果,后來(lái)因?yàn)槟切┖暝斐闪丝膳碌暮甑鬲z,我們每周都要加班,后來(lái)我的另外一個(gè)同事把那個(gè)領(lǐng)導(dǎo)給打了,就在辦公室,真的打出血了,我不知道是不是跟大量的宏有關(guān),我真的不知道。只問(wèn)你看了以下的代碼,想打人嗎?
#define ARGV Argv

int
main(int Argc, char *ARGV[])
這么做的藝術(shù)性何在?我將繼續(xù)苦逼地上下而求索。
      OpenSSL代碼中大量的#if 0不說(shuō),還有以下奇葩的,注釋都給宏定義屏蔽了,編譯器迷惑了,注釋的第一行看得出是個(gè)注釋?zhuān)菂s找不到*/,哦,原來(lái)如此,注釋的后面部分被#if 0這個(gè)宏屏蔽了...
#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
注意上面注釋第一行的那個(gè)“(and”,看得出是作者故意這么做的,以表現(xiàn)一下自己的立體主義??
      OpenSSL毫無(wú)一致的風(fēng)格,不管是縮進(jìn)還是代碼本身,甚至在一個(gè)函數(shù)中都沒(méi)有一致的風(fēng)格,類(lèi)似以下這樣:
int func()
    {
    ...
    a=b;
    c = d;
}
如果我寫(xiě)出這種代碼,又要被罵了,但是慢慢的,我不覺(jué)得因此被罵是一種讓人痛苦的事,就像OpenSSL一樣將自虐當(dāng)成了快感的來(lái)源!
      若不是我把下面的這段代碼的業(yè)務(wù)部分摳去,它絕對(duì)可以參加IOCCC了,事實(shí)上摳業(yè)務(wù)代碼的過(guò)程是痛苦的,完全沒(méi)有庖丁解牛那樣的快感,相反,就像摳屁股眼子一樣痛苦...
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);
}
代碼是有點(diǎn)長(zhǎng)了,但是實(shí)際的代碼就是如此!簡(jiǎn)直就是宏的地獄,if (0)這種代碼的目的就是為了膠合諸多宏之間的互斥關(guān)系,讓互斥代碼的某部分不執(zhí)行??唉,宏與宏之間發(fā)生了關(guān)系,你就不再是C編程,而是宏編程...話(huà)說(shuō),上述的代碼實(shí)際上是一個(gè)不含業(yè)務(wù)的邏輯框架,就像鋼混框架結(jié)構(gòu)建筑的那個(gè)大架子一樣,和IOCCC獲獎(jiǎng)代碼還是天上地下的,真正的IOCCC代碼是無(wú)框架的,框架隱藏于的業(yè)務(wù)本身,它的美感類(lèi)似于類(lèi)似海洋軟體動(dòng)物的那種美。
      事實(shí)證明,C語(yǔ)言的代碼跳轉(zhuǎn)機(jī)制是多種多樣的,只會(huì)用goto那叫井底之蛙,但是有些時(shí)候,某個(gè)代碼段只能用goto達(dá)到,這不是逼著人用goto的嗎?請(qǐng)看下面的代碼:
        if(!ok) goto end;
        if (0)
                {
end:
                X509_get_pubkey_parameters(NULL,ctx->chain);
                }
事實(shí)上想玩好if (0)只有兩種方法,第一就是使用宏把if (0)屏蔽掉,第二就是使用goto把if (0)強(qiáng)暴掉,不過(guò)還有一種方式,把0的意義改掉。大量的#if 0,#if 1,if (0), if (1)的存在,外加一些令人看到“世界在進(jìn)步”的注釋?zhuān)瑢penSSL變成了一座僵尸博物館,這些永遠(yuǎn)都不會(huì)被執(zhí)行到的代碼旁邊都會(huì)有一些個(gè)注釋?zhuān)忈屩鼈冊(cè)?jīng)的光輝和日前為何變成了木乃伊。可是為何不把它們直接刪掉呢?既然已經(jīng)知道了它們已然無(wú)用并且知道了為什么已然無(wú)用,還留著它們,我想作者們都是些懷舊之士吧。這使我們這些后來(lái)人在讀代碼或者改代碼的時(shí)候不得不先預(yù)處理一遍。對(duì)于我個(gè)人來(lái)講,我不喜歡預(yù)處理,我直接手工刪掉那些永不被執(zhí)行的代碼,我甚至將此事作為當(dāng)成一種無(wú)聊時(shí)的消遣,和展Windows注冊(cè)表一展一下午一樣獲得一種升華意義的快感!我真的曾經(jīng)展過(guò)注冊(cè)表,展了一下午都沒(méi)有展完...
      3年前,我曾經(jīng)在OpenSSL的一個(gè)engine里面大量使用下面的代碼:
do {
...
if (...)
    break;
...
}while(0);
我因這種代碼而被罵狗屎,不過(guò)當(dāng)時(shí)我并沒(méi)有生氣,反而和另一個(gè)同事在旁邊偷笑,聽(tīng)說(shuō),笑能長(zhǎng)壽,看來(lái)以后要多看看OpenSSL的代碼了。
      笑固然好,可是哭是另一種釋放壓力的手段,有時(shí)會(huì)比笑的效果更好。但是只是上面這些還是無(wú)法把我弄哭的,能把我弄哭的是一段代碼的實(shí)現(xiàn)邏輯,事情是這樣的...老子雖不是什么高人,起碼也作為碼農(nóng)辛勤耕耘好記載了,被OpenSSL如此蹂躪真的是說(shuō)不出來(lái)的苦啊!
      SSL數(shù)據(jù)是留式的,即它沒(méi)有邊界,不像數(shù)據(jù)報(bào)協(xié)議,在底層,SSL紀(jì)錄協(xié)議是封裝在一個(gè)recode塊里面的,可以認(rèn)為在底層SSL是有邊界的,但是在上層它和TCP一樣,沒(méi)有邊界。但是我偏偏要用它來(lái)傳輸有邊界的IP數(shù)據(jù)報(bào),OpenSSL的SSL_write/SSL_read接口又沒(méi)有暴露出SSL record的概念,我是多么希望SSL_write每次將傳入的buff作為一個(gè)record發(fā)送,而SSL_read則每次僅將一個(gè)record數(shù)據(jù)返回調(diào)用者啊,然而沒(méi)有任何標(biāo)準(zhǔn)規(guī)定它應(yīng)該這么做,因此我就不能奢望OpenSSL是如此實(shí)現(xiàn)的。
      幸好OpenSSL它是開(kāi)源的,代碼可以自己看,RTFSC!正如Linus大神說(shuō)的那樣。但是看看ssl3_write_bytes的實(shí)現(xiàn):
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;
       // 我覺(jué)得這是個(gè)核心函數(shù)
        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;
        }
    }
看到這段代碼,一般人會(huì)怎么想?當(dāng)然深深中了OpenSSL邪毒的那幫人不屬于一般人。一般人看了會(huì)覺(jué)得,一個(gè)buff可能會(huì)分為多次發(fā)送,所以有了一個(gè)for(;;),直到發(fā)送完為止,如果接口行為定義良好,我應(yīng)該放棄希望了,因?yàn)榘凑找陨纤膶?shí)現(xiàn)邏輯,一個(gè)buff可能會(huì)被分割為多段,每段調(diào)用do_ssl3_write發(fā)送,這樣一個(gè)buff就會(huì)形成多個(gè)record,從而打破了我的幻想,此時(shí)我想哭,因?yàn)槲也坏貌辉俅稳ゲ偌一飻嚬肥?,噢,多么痛的領(lǐng)悟,多么直白的坦言。
      幸好有高人相助,告訴我,理論上應(yīng)該是一次write構(gòu)造一個(gè)record的,我對(duì)此人的神乎膜拜促使我深入了do_ssl3_write函數(shù)內(nèi)部,然后我打個(gè)個(gè)噴嚏,一眨巴淚眼,鼻涕吸到了嗓子里,咸咸的,但不苦...
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)  // 在一開(kāi)始的位置,處理邏輯就被劫持了,因此我就必須注意left在什么情況下不為0
       // 這個(gè)執(zhí)行流跳轉(zhuǎn)得很詭異!太詭異!
        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,在上層調(diào)用的時(shí)候,這個(gè)參數(shù)為0,這就意味著
   // 肯定有什么地方以1為參數(shù)調(diào)用了本函數(shù)。這個(gè)empty fragment我后面會(huì)解釋。
    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) */
            // 遞歸調(diào)用?我kao,這個(gè)函數(shù)竟然有兩段邏輯:
            // 1.默默創(chuàng)建一個(gè)新的record;
            // 2.創(chuàng)建封裝buf的record并和遞歸調(diào)用中默默創(chuàng)建的那個(gè)record一起發(fā)送
            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綁定的一個(gè)發(fā)送buf,事先已經(jīng)malloc好了內(nèi)存,真TM慷慨!
   // 一個(gè)prefix_len表示在真正的record發(fā)送前緊接著的那個(gè)默默創(chuàng)建的record,調(diào)用者并不知道
   // 會(huì)創(chuàng)建并發(fā)送這樣一個(gè)record
    p = wb->buf + prefix_len;

    /* write the header */
    // 這段代碼還算清晰
    // 但是,記住,在需要empty fragment的情況下會(huì)跑到這里兩次
    *(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
         */
       // 如果是默默創(chuàng)建的那個(gè)record,則并不直接發(fā)送,目的是想將真實(shí)的record在內(nèi)存上
       // 緊隨這個(gè)默默構(gòu)造好的record作為一個(gè)buffer直接發(fā)送給下層BIO。為何不分別發(fā)送兩個(gè)
       // record呢?我想是為了緊湊使用SSL的s3->wbuf緩沖區(qū)吧,該緩沖區(qū)事先建立,而且還
       // 真不?。?6K+!唉,真不覺(jué)得實(shí)現(xiàn)者想不出更好的辦法了啊
        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;
    }
上面的函數(shù)調(diào)用執(zhí)行到最后的return ssl3_write_pending(s,type,buf,len)前,就會(huì)得到下面的一共wb->left大小的緩沖區(qū):
|empty record header|empty record data|real record header|real record data|
最終的buff構(gòu)造好了,可以發(fā)送了吧,好的,可以發(fā)送了!但是底層機(jī)制又來(lái)找茬了...在非阻塞IO模式下,底層的BIO并不一定能保證發(fā)完wb->left這么多數(shù)據(jù),那么發(fā)多少返回多少,這也正常,關(guān)鍵是返回到了ssl3_write_bytes函數(shù),也就是那個(gè)for(;;)調(diào)用do_ssl3_write的函數(shù),然后一大堆if判斷,要么繼續(xù),要么直接最終返回給SSL_write,不管怎樣,在你下次調(diào)用ssl3_write_bytes里面的do_ssl3_write的時(shí)候,只要這兩個(gè)個(gè)record沒(méi)有寫(xiě)完,即SSL的s3->wbuf.left不為0,就會(huì)在do_ssl3_write的最開(kāi)始處直接調(diào)用ssl3_write_pending來(lái)保證一個(gè)record的寫(xiě)入完畢。
      所有的問(wèn)題在于,do_ssl3_write太復(fù)雜了,做的事情太多了,它做了3件事:1.構(gòu)造empty fragment;2.構(gòu)造真實(shí)record;3.保證這兩個(gè)record發(fā)送完畢。邏輯太復(fù)雜,因此才邀請(qǐng)各種跳轉(zhuǎn)上陣...在給出我認(rèn)為合理的邏輯之前,先簡(jiǎn)單說(shuō)下什么是empty fragment。它實(shí)際上是一個(gè)缺陷的修復(fù),即針對(duì)CBC IV的***,empty frag機(jī)制在每次發(fā)送record前先發(fā)送一個(gè)empty frag record,內(nèi)部一些無(wú)用的數(shù)據(jù),接收端可以在SSL協(xié)議層解密后任意處理,它的目的就是在數(shù)據(jù)中間插入一些隨機(jī)因素以加大CBC模式的IV猜測(cè)的難度。
      我想不明白,發(fā)送上次未完成的數(shù)據(jù)為何要放在這么深的位置,我也想不明白,為何要用遞歸...難道就不能封裝一個(gè)build_record的函數(shù)嗎?難道就不能封裝一個(gè)write_raw函數(shù)嗎?既然empty fragment是一個(gè)安全加固機(jī)制,為何要隱藏它呢?直接:
build_record {
    操作SSL的s3->wbuf。我覺(jué)得好,就繼續(xù)用
}
write_raw {
    往下層BIO寫(xiě)入SSL的s3->wbuf.buf的某一段
}
do_ssl3_build {
    if (need_empty) {
        build_record;
    }
build_record;
...
}
這樣是不是比遞歸更清晰呢?至于那個(gè)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!我怕死無(wú)葬身之地,這個(gè)話(huà)題就此打住,who can who up!不過(guò)我要說(shuō)一點(diǎn),那就是polarssl的實(shí)現(xiàn),看看人家的ssl_write接口:
ssl_write()
{
    if( ssl->state != SSL_HANDSHAKE_OVER ) {
        handshack;
    }
    if (left) {
        flush_pending and return <=0
    }
    build and write record, return num
}
這樣調(diào)用邏輯會(huì)比較簡(jiǎn)單,更加清爽:
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 );
}
看到這樣的代碼,我想憐香惜玉的人誰(shuí)也不忍心加入if (0)逼著后來(lái)者用goto吧!
      我對(duì)ssl3_write_pending的理解真的很對(duì)嗎?不,我錯(cuò)了!ssl3_write_pending真的會(huì)在沒(méi)有寫(xiě)完record數(shù)據(jù)的情況下將left清0,那就是在DTLS的情況下,此時(shí)其調(diào)用者的那句注釋就說(shuō)對(duì)了:
/* next chunk of data should get another prepended empty fragment
            * in ciphersuites with known-IV weakness: */

這就是OpenSSL的所有,連注釋說(shuō)的都不是所有情況。當(dāng)然OpenSSL并沒(méi)有明確地注釋?zhuān)偸潜A粢稽c(diǎn)解釋的空間,所以不要看它的注釋?zhuān)€是看代碼吧,如果你想自虐的話(huà)...


向AI問(wèn)一下細(xì)節(jié)

免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀(guān)點(diǎn)不代表本網(wǎng)站立場(chǎng),如果涉及侵權(quán)請(qǐng)聯(lián)系站長(zhǎng)郵箱:is@yisu.com進(jìn)行舉報(bào),并提供相關(guān)證據(jù),一經(jīng)查實(shí),將立刻刪除涉嫌侵權(quán)內(nèi)容。

AI