1. 程式人生 > >OpenSSL 程式設計入門

OpenSSL 程式設計入門

如果你急切的想構建一個簡單的Web客戶端和伺服器對,這時你就需要使用SSL了..

      SSL是一種保護基於TCP協議的網路應用最快而且最簡單的的方法,如果你正在用C語言做開發,那麼對於你來說,最好的選擇可能就是使用OpenSSL了. OpenSSL是在Eric Young的SSLeay包的基礎上對TSL/SSL的一個免費的執行(類似於BSD方式的License).然而, 不幸運的事情是, 伴隨OpenSSL一起釋出的文件和示例程式碼並不是很完全, 使用它的人需要更多的東西.在OpenSSL被使用之處, man手冊都相當優秀,可是這些手冊失去了大的背景 因為它們只是參考資料而不是教程.


OpenSSL的API多而複雜, 因此我們在此並不會作出一個完整的講述. 相反,我的目的只是教會你如何去高效的使用man手冊.在本文中, 我們將會通過構建一個簡單的Web客戶端和伺服器來演示OpenSSL的基本特點. 而在後續的第二篇中我們將會介紹OpenSSL的一些高階特性, 比如會話恢復和客戶端認證等.


      在話題開始之前, 我會認為你已經熟悉SSL和HTTP了, 或者最起碼在概念層上應該有一些瞭解. 如果你對此一無所知, 推薦一個比較好的方法,那就是參考RFC(參見附錄).
由於篇幅原因, 本文只包涵了原始碼的一些摘錄, 完整的程式碼可以從作者的站點http://www.rtfm.com/openssl-examples/

上下載.我們的客戶端是一個簡單的HTTPS(見 RFC 281客戶端,它在初始化了一個到達伺服器的SSL連線之後便通過這個連線將HTTP請求傳送給HTTP伺服器. 然後等待伺服器端的響應,並將響應資訊列印在螢幕上.這是對通常那些”獲取並且列印資訊”的程式功能更簡化的一個例子.
      伺服器端程式是一個簡單的HTTPS 伺服器, 它等待從客戶端發出的TCP連線, 每當接收到一個連線時,它會磋商這個連線(的合法性). 一旦這個連線被確定下來, 它會讀取客戶端的HTTP請求, 並將HTTP請求的響應資訊傳輸給客戶端. 當響應傳輸完畢時它會關閉這個連線.我們的第一個任務就是建立一個上下文物件(一個SSL_CTX), 這個上下文物件會在每次需要建立新的SSL連線的時候被用來建立一個新的連線物件. 而這些連線物件則用於SSL的握手,讀和寫.
(使用上下文物件)這種方法有兩個優點: 首先, 上下文物件允許一次初始化多個結構體, 這樣就提提高了效能. 在大多數應用中, 每一個SSL連線都使用相同的加密演算法(keying material)和CA(certificate authority)列表等. 而採用上面這種方法, 我們就不需要在每次連線的時都去載入這些資訊(加密演算法和證書), 而只需要在程式啟動時將它們載入進上下文物件中. 然後,當我們需要建立一個新的連線時, 只需要將新的連線簡單的指向這個上下文物件就可以了. 使用一個簡單的上下文物件的第二個好處就是它允許多個SSL連線之間共享資料, 比如用於SSL會話恢復的SSL緩衝(cache). 上下文初始化由       主要的四個任務組成, 通過列表1所示的initialize_ctx()函式來完成.
列表 1 initialize_ctx().在應用OpenSSL之前, 整個庫需要進行初始化, 這個過程通過SSL_library_init()函式來完成,它主要載入OpenSSL將會用到的演算法, 如果我們想要很好的報告差錯資訊, 同樣需要通過SSL_load_error_strings()來載入錯誤字串, 否則, 就不能夠將OpenSSL的錯誤對映為字串.
      我們同樣需要建立一個物件來作為錯誤列印的上下文. OpenSSL為輸入和輸出抽象了一個叫做BIO物件的概念.這樣可以使得程式設計師針對不同種類的IO通道(socket, 中斷,記憶體緩衝等)使用相同的函式,而唯一的差別就是在函式中使用的是不同種類的BIO物件.在本例中,我們通過將一個BIO物件與標準錯誤stderr繫結來列印錯誤資訊.
      如果你正在寫一個能夠執行客戶端認證的伺服器或者客戶端程式, 你就需要載入自己的公鑰或者私鑰以及相關的證書.證書儲存空隙中, 並且通過SSL_CTX_use_certificate_chain_file()函式與CA證書一起被載入形成證書連結串列. SSL_CTX_use_PrivateKey_file()函式用來載入私鑰.出於安全原因, 私鑰通常通過密碼來加密, 如果使用密碼加密的話, 密碼回撥函式(通過SSL_CTX_set_default_passwd_cb()來設定)將會在獲取密碼時被呼叫的.
      如果你需要認證已經連線到你的客戶端, OpenSSL需要知道你信任哪些CA, SSL_CTX_load_verify_locations()呼叫用來載入CA.
為了保證安全, OpenSSL需要一個好的強性隨機數源, 通常,為隨機數生成器(RNG)提供種子原料是應用本身的責任, 然而,如果/dev/urandom可用的話,OPenSSL會自動的使用/var/urandom來為RNG播種, 由於/dev/urandom在Linux是標準化的, 我們不需要為它做任何事情, 這個就很方便了, 因為收集隨機數很詭異,而且很容易引起系統抖動上升. 注意,如果你在一臺不是Linux的系統上,你可能會在某些時刻得到錯誤資料, 因為隨機數產生器沒有被播種, OpenSSL的rand(3) man手冊為你提供了更多可以參考的資訊.
   客戶端
        當SSL完成了對SSL上下文物件的初始化後,它已經為連線到伺服器做好準備。OpenSSL要求我們自己建立一條從客戶端到伺服器的TCP連線,然後使用這個TCP套接字建立一個SSL套接字.為了方便期間,我們把TCP連線的建立劃分到函式tcp_connect()(這裡沒有給出實現,但是在下載的程式碼中可以看的到)中去實現。
當TCP連線建立好以後,我們建立一個SSL物件來處理這個連線。這個物件需要與套接字繫結,注意,我們並不是直接把SSL物件繫結到套接字上,而是建立一個使用這個套接字的BIO物件, 然後將SSL物件繫結到這個BIO上。
這個抽象層允許你通過各種通道來使用OpenSSL而不是套接字,前提是你已經有了
       一個合適的BIO物件。例如,有一個OpenSSL測試程式純粹通過記憶體緩衝區來連線SSL客戶端和伺服器。一個比較實用的做法就是支援一些套接字根本無法訪問協議來進行連線。例如,你可以通過一個連續行(serial line)來執行SSL。
SSL連線的第一步就是執行SSL握手,握手鑑別伺服器(也可以選擇鑑別客戶端客戶端)並且建立保護剩餘傳輸所需要的加密演算法。SSL_connect() 呼叫用來執行SSL握手.由於我們使用
的是阻塞式的套接字,所以SSL_connect()函式在SSL握手沒有完成或者沒有檢測到一個差錯之前是不會返回的。成功時,這個函式返回1,返回0或者負數表示出錯。


呼叫如下:

/* Connect the TCP socket*/
sock=tcp_connect(host,port);
/* Connect the SSL socket */
ssl=SSL_new(ctx);
sbio=BIO_new_socket(sock,BIO_NOCLOSE);
SSL_set_bio(ssl,sbio,sbio);
if(SSL_connect(ssl)<=0)
  berr_exit("SSL connect error");
if(require_server_auth)
  check_cert(ssl,host);


      當我們初始化到達伺服器的SSL連線時,我們需要先校驗伺服器的證書鏈。OpenSSL為我們做一些這樣的校驗,但是不幸的是,其他的校驗工作總是與具體的應用相關(所以不能通過OpenSSL來完成),因此,我們需要自己去做這些工作。我們的例子程式碼做的主要測試就是檢驗伺服器的身份。這個通過列表2的函式check_cert()來實現。
列表2 check_cert函式
當檢測到伺服器的證書鏈有效的時候,你需要驗證你正在檢視的證書與你期望的伺服器所擁有的身份是否相匹配。在大多數情況下,這意味著伺服器的DNS名字出現在證書中,或者在Subject Name 的Common Name域,或者位於證書的擴充套件部分(certificate extension).儘管每種協議在識別伺服器身份的時候有少許的差別,但是RFC 2818包括了通過SSL/TSL識別HTTP伺服器身份的規則。如果你沒有什麼很明瞭的意圖去做其他事情,按照RFC 2818 的規則去做是一個很好的做法.
由於大多數證書還一直將域名放在Common Name欄位而不是擴充套件部分。所以我們只進行了
Common 欄位的校驗,我們通過SSL_get_peer_certificate() 函式來提取伺服器的證書,然後將證書的Common Name欄位與我們連線的客戶機名稱進行比對,如果不匹配的話,肯定出錯了,我們退出程式。
在0.9.5版本之前,OpenSLL容易遭到一種證書擴充套件攻擊,為了方便理解,我們考慮下面的情況,一個伺服器鑑別由Bob簽名的證書,如圖1示,Bob並不是你的一個CA,但是它的證書卻是由你信任的一個CA簽名的。
如果你接受這個證書,你可能會有很大的麻煩,但是CA簽名了Bob的證書這個事實卻意味著它通過了對Bob的身份確認,但卻不是說Bob可以被信任.如果你知道你想要與Bob做生意,那很好,但是如果你想要與Alice(你從從來沒聽說過,但是Bob為她擔保)和Bob一起做生意,那這些資訊就沒有用了。
        通常,保護你免於此類攻擊的唯一方法就是限制證書鏈的長度,目的就是使你明確你所觀看的證書就是CA簽名的。V3版的X.509證書包含了一種方法,它使得CA能夠在一些證書上做標籤說明這些證書是其他CA的。這種方法允許一個CA有一個簡單的根CA,然後根CA可以認證其他的輔助CA。
當前的OpenSSL(v0.9.5和以後的)都會校驗這些擴充套件,因此不論你是否校驗證書鏈的長度,這些擴充套件攻擊都會被自動防禦掉。比0.9.5更早的OpenSSL一點都不作這些擴充套件部分校驗,所以如果要使用這個版本之前的OpenSSL的話,你必須自己限制鏈的長度。0.9.5版的OpenSSL在校驗上有一些問題,所以如果你正在使用它你也許應該進行一些更新。initialize_ctx() 函式中,程式碼#ifdefed提供了對老版本鏈長度的校驗,我們使用SSL_CTX_set_verify_depth()函式強迫OpenSSL去校驗鏈的長度。總之,強烈建議你升級到0.9.6,主要是因為比較長的鏈(但是也有可能是故意構造的)越來越流行了,絕對最新和最好的版本就是OpenSSL 0.9.66了
我們使用列表3的程式碼來寫HTTP請求,出於演示的目的,我們使用了或多或少的在REQUEST_TEMPLATE變數中可以找到的硬線路(HardWired)HTTP請求。由於連線的機器可能會改變,我們不需要填充Host這個頭資訊。這個通過snprintf來實現。然後我們通過SSL_write()函式來發送資料到伺服器端,SSL_write()的API或多或少與Write()函式類似,區別就是在write中我們傳遞檔案描述符,而在前者中傳遞SSL物件。
        列表3 寫HTTP請求。
      有經驗的TCP程式設計師都知道,我們在函式的返回值不等於我們想要寫入的位元組數時丟擲一個錯誤,而不是迴圈的呼叫些函式. 在阻塞模式下,SSL_write()函式已經足夠,因為在所有的資料都被寫完或者發生差錯之前,這個呼叫是不會返回的。然而write()函式卻可能只會寫入一部分資料,我們可以通過設定SSL_MODE_ENABLE_PARTIAL_WRITE標誌位來允許部分寫(本文沒有應用),在這種情況下,你需要迴圈的呼叫寫函式。
在老版本的HTTP1.0中,伺服器傳輸它的HTTP響應然後關閉連線。在後來的版本中,引入的持續連線,支援同一連線上多個連續的事務。為了方便和簡潔,在本文並不使用這種持續的連線。我們忽略(允許設定持續連線的)頭部資訊,使伺服器通過關閉連線來通知響應的結束。在操作上,意味著我們只需要持續讀,直到檔案結束,這樣也相應的簡化了事務。
OpenSSL使用SSL_read() 函式來讀取資料,正如列表4中所示,跟使用read()一樣,我們只需要簡單的選擇一個合適大小的緩衝,然後將它傳遞給SSL_read()函式。注意到緩衝區的大小在此處並沒有多麼的重要,SSL_read() 和read()一樣,返回可用的資料,哪怕它比請求的資料量小. 另外,如果沒有資料可以讀取,讀函式將會阻塞。
列表4 讀取響應
   BUFSIZZ的大小, 基本上可以說與效能是持平的, 這種效能持平與我們簡單的從普通的套接字讀取是不同的. 在那種情況下,對read的每一次呼叫都需要上下文切換到核心態去,由於上下文在核心態和使用者態之間切換是非常昂貴的, 程式設計師在讀取資料的時候都儘量使用較大的緩衝從而減少讀取的次數(從而減少了上下文切換的次數). 然而當我們在使用SSL的時候, 對read()的呼叫次數, 也就是上下文在核心態和使用者態切換的次數, 在很大程度上取決於資料寫入的記錄數而不是SSL_read()的呼叫次數.
例如,如果客戶端寫入了1000Byte的記錄, 然後我們呼叫SSL_read()每次讀取1Byte, 那麼對SSL_read()的第一次呼叫會使得所有的記錄被讀入, 然後剩下的呼叫就只是將記錄從SSL緩衝中讀出來.因此,在使用SSL而不是普通的套接字讀取資料時,緩衝區的大小選擇並不是特別的重要. 如果資料被寫成一系列小的記錄, 你可能想通過對Read的一次單獨的呼叫來讀取所有的記錄. 這時候, OpenSSL為你提供了一個標誌位,那就是SSL_CTRL_SET_READ_AHEAD, 通過設定這個標誌位就可以開啟這種讀的開關.
注意本文中使用switch語句來處理SSL_get_error()函式返回值這種用法,使用普通的套接字的一個方便之處就是任何的負的返回值(最典型的是-1)都代表失敗,然後你可以檢測errno去檢視真正發生了什麼事情。但是errno在這裡並不起什麼作用,因為它只代表了一個系統錯誤,而我們想要做的是對SSL錯誤進行處理。同樣程式設計時需要對errno進行細心的處理,以便實現執行緒安全。
在OpenSSL中提供了SSL_get_error()呼叫而不是errno, 這個呼叫使得我們可以檢測返回值以確定是否有錯誤發生,如果有錯誤發生,是什麼錯誤。如果返回值是一個正數,說明我們讀取到了一定的資料,這時候將它簡單的列印到螢幕上. 一個真正的客戶端會解析HTTP響應然後或者顯示資料或者將資料儲存到磁碟。但是對OpenSSL而言,這些並沒有多大意義,所以我們在此處不會涉及對響應資訊的具體處理。
       如果返回值是0,並不表示沒有資料可以讀取,因為在沒有資料可以讀取的情況下,正如上面已經討論過的一樣, 我們的函式肯定會被阻塞住的。所以,此處返回的0表明這個套接字已經被關閉了,當然也就沒有任何資料可以讀取了,所以我們退出迴圈。
如果返回值是一個負值,這時肯定發生了某種錯誤。我們只關心兩種型別的錯誤:普通錯誤和提前關閉的錯誤,我們使用SSL_get_error()函式來決定得到的是那種型別的錯誤,差錯處理在客戶端的程式中非常的簡單。所以對於大多數錯誤,我們僅僅使用berr_exit()函式來列印一行錯誤資訊然後退出,然後,提前關閉這種錯誤需要進行特殊的處理.
TCP使用FIN片斷來表明傳送者已經發送完所有的資料. SSL v2 允許任何一端通過傳送TCP FIN欄位來結束SSL連線. 但是,這種原則卻會遭受一種截斷攻擊,攻擊者可以自己偽造一個TCP FIN來終止連線,使得傳送的資料比實際要傳送的少. 除非受害者有某種方法知道他將要接收多長的資料,否則她/他很容易會認為接收到的那一部分長度的資料已經是所有的了。
為了解決此種安全隱患,SSL v3引入了close_notify()警報機制,close_notify是一個SSL訊息(因此是安全的)但卻不是SSL資料流的一部分,因此應用程式並不能看到它。在close_notify訊息被髮送出去之後,任何資料將不能再傳輸了。
因此當SSL_read()返回0表示套接字已經被結束時,這其實意味這close_notify訊息已經收到,如果客戶端在收到close_notify訊息之前收到一個FIN,SSL_read()將會返回一個錯誤,這種情況叫做提前關閉。
一個比較簡單的客戶端可能在遇到任何一個提前關閉的情況時都會報告一個錯誤然後退出.
這個處理是SSL V3採取的預設處理方式,但是,不幸的是,對於客戶端來說,傳送提前關閉訊息是一個很常見的差錯。所以,如果你不是為了一直彙報錯誤的話,你最好忽略這些提前關閉訊息。我們的程式碼進行了特別的處理,它報告錯誤卻不隨著錯誤而退出程式。

        如果在讀取響應的時候沒有發生任何錯誤,這時我們就需要傳送自己的close_notify 訊息給伺服器端,這個是通過SSL_shutdown() 函式來實現的,在討論伺服器端的時候我們會仔細的研究這個函式的。但是大體的思想卻很簡單:返回1表示完全關閉,0表示不完全關閉,-1表示出錯。由於我們已經收到了伺服器端傳送的close_notify訊息,所以唯一可能出現的問題就是我們在傳送我們自己的close_notify訊息時出了差錯,要不然的話,SSL_shutdown()函式將會成功的(返回值為1)
最後,我們需要銷燬申請的變數物件,因為這個程式最終要退出的,釋放這個操作並不是嚴格意義上必須的進行的,但是在更一般的程式中它卻是必要的。
        伺服器
      我們的Web伺服器除了比客戶端更復雜點外, 可以說是客戶端的一個映象,首先,為了伺服器能處理多個客戶端,要要呼叫fork()來建立子程序,然後我們用OpenSSL使用的BIO物件的API來一次一行的讀取客戶端的請求,同樣用BIO來實現對客戶端的緩衝寫,最後,伺服器端的關閉過程有些複雜。
通常在一臺Linux系統上,伺服器處理多個客戶端連線最簡單的方法就是為每個客戶端fork()出一個子程序,我們在這裡是在accept()返回之後通過fork()來建立子程序。每一個子程序獨立執行並且在對客戶端進行服務以後自行退出。儘管這種方法在比較繁忙的Web伺服器上可能相當慢,但是在此處卻是可以接受的,列表5是主伺服器的accept迴圈
      列表5 伺服器接收連線迴圈
    在fork()和建立SSL物件之後,伺服器呼叫SSL_accept()函式,從而引起OpenSSL執行了伺服器端的SSL握手,跟使用SSL_connect()一樣,由於我麼使用的是阻塞式的套接字,所以SSL_accept()函式將會一直阻塞直到握手完成為止。因此,SSL_accept()返回的唯一情況就是握手完成或者檢測到錯誤。SSL_accept()返回1表示成功,返回0或者負數表示失敗。OpenSSL的BIO物件在某種程度上有棧的特性,因此我們可以把SSL物件封裝在BIO(SSL_BIO物件)中,然後把那個BIO封裝在一個緩衝的BIO物件中,如下所示:

                                 io=BIO_new(BIO_f_buffer());
                    ssl_bio=BIO_new(BIO_f_ssl());
                     BIO_set_ssl(ssl_bio,ssl,BIO_CLOSE);
                    BIO_push(io,ssl_bio);


       這種方法允許我們使用BIO_* 函式族來操作新型別的IO物件,從而實現對SSL連線的緩衝讀和寫。在此處,你也許會問,為什麼這個用法更好?(或者這有什麼好的)。主要的原因是,這種方法程式設計起來很舒服,它使得程式設計師能夠去處理一種更自然的單元(行或者字元等)而不是SSL記錄。
請求
    HTTP請求由請求資訊行後面跟著一堆頭資訊行再加上一個可選體組成。頭資訊行是通過空行來結束的(例如,一對CRLF,有時候崩潰的客戶端會發送一對LF),最舒服的讀取請求資訊行和頭資訊行的方式就是一次讀取一行,直到讀取到空行為止。我們可以使用列表6中的OpenSSL_BIO_gets()呼叫來實現這個操作。
列表6 讀取請求
OpenSSL_BIO_gets()呼叫表現的類似於標準輸入輸出呼叫fgets(),它使用任意大小的緩衝區和長度從SSL連線中讀取一行資料到緩衝中去,讀取的結果通常以空字元結束(但也包括結束符LF)。因此,我們每次簡單的讀取一行,直到讀到某一行包括一個簡單的LF或者CRLF。
    由於我們使用固定大小的緩衝,所以有可能,也許不太可能,我們會讀取到很長的一行,在這種
情況下,這一行將被分解成兩行,在極端不可能的情況下,分隔正好在CRLF之前發生,這樣的話,從前一行讀取到的第二行就只包括一個CRLF了,這時候我們就會迷惑,認為頭序列提前結束了。一個真正的Web伺服器會檢測這種情況的,但是在這裡卻不值得去做。注意,不管到達的行數是多少,都不會有緩衝區溢位的情況發生。所有可能發生的就是我們會錯誤的解析頭資訊。
注意到我們並不需要用HTTP請求做任何事情,所以只是讀取然後將它丟棄。真正的實現將會讀取請求資訊行和頭資訊行,計算是否有一個訊息體存在然後讀取這個訊息體。
下一步就是寫HTTP響應並且關閉連線:

if((r=BIO_puts
         (io,"HTTP/1.0 200 OK//r//n"))<0)
          err_exit("Write error");
        if((r=BIO_puts
         (io,"Server: EKRServer//r//n//r//n"))<0)
          err_exit("Write error");
        if((r=BIO_puts
         (io,"Server test page//r//n"))<0)
          err_exit("Write error");
        if((r=BIO_flush(io))<0)
         err_exit("Error flushing BIO");


       注意我們在程式中使用BIO_puts()而不是SSL_write()。這樣我們就可以一次寫一行響應訊息,而把所有的行當作一條SSL記錄傳送出去,這種做法是很重要的,因為準備(計算完整性,校驗,加密等)一個SSL傳輸記錄的花銷是非常大的。因此,使一條傳輸的記錄儘可能的大是一個很好的主意。
我們有必要留意一下所使用的緩衝寫方法. 首先,在關閉之前你需要衝掉緩衝區,SSL物件並不知道你已經在它上面佈置了一層BIO,所以,如果你破壞了SSL連線,將會使得剩餘截斷的資料留在緩衝區中。BIO_flush() 函式是用來處理這個的。同樣,預設情況下,OpenSSL為BIO物件使用了1024Byte大小的緩衝區,由於SSL記錄大小可以長達16K,所以使用1024Byte大的緩衝可能會引起過多的碎片(從而使效率下降),你可以使用BIO_ctrl() 函式來增加緩衝區的大小。
一旦完成了響應的傳送,我們需要傳送close_notify訊息,前面已經講到了,是通過SSL_shutdown來實現的,不幸的是,當伺服器首先關閉的時候,情況變得遊戲蹊蹺。我們對SSL_shutdown() 的第一次呼叫會發送close_notify訊息,但是在另一端卻不會取尋找它。所以它會很快的以0作為結果返回,表明關閉過程沒有完成。然後,就需要應用程式自身再一次呼叫SSL_shutdown()函數了。
這裡也可以存在兩種觀點,我們能夠肯定已經看到了自己關注的HTTP請求的整個部分,然後對其他都不感興趣,因此,我們可以並不在乎客戶端是否傳送了close_notify訊息,相反,如果嚴格的遵守協議並且要求其它人也這麼作的話,我們也就就需要收到一個close_notify訊息。
       如果堅持第一種觀點的話,一切將會變得很簡單,我們簡單的呼叫SSL_shutdown() 函式傳送我們的close_notify訊息,然後不管客戶端是否傳送一個close_notify 訊息,就立刻退出。如果堅持第二種觀點的話(本文的例子伺服器就是這麼做的),事情就變得比較複雜了,因為客戶端通常都不會表現的多麼正常。
我們面臨的第一個問題就是客戶端通常都不會發送close_notifys訊息。事實上,有些客戶端在它們收到HTTP響應時便會立即關閉連線(有些IE是這麼做的),當我們在傳送close_notify時,另一端可能正在傳送一個TCP RST欄位,在這種情況下,程式將會捕獲SIGPIPE訊號,在本文, 我們將會在函式initialize_ctx()中安裝一個虛設的SIGPIPE訊號處理器來避免這種情況的發生。 
我們面臨的另外一個問題就是客戶端可能不會立即傳送一個close_notify訊息來作為對伺服器端close_notify訊息的迴應,一些版本的Netscape 要求你首先發送一個TCP FIN標誌。因此我們在第二次呼叫SSL_shutdown()之前呼叫了shutdown(s,1)函式,當我們使用1作為第一個引數時,shutdown()函式傳送了
一個FIN標誌,但是卻使得套接字處於開啟並且讀的狀態。伺服器端關閉的程式碼如列表7所示.
列表7 訪問SSL_shutdown()
       在本文,我們只是提及了使用OpenSSL時的一些表面的觀點,下面是更多的一些觀點:
一種更復雜的檢測證書中伺服器名字的方法就是使用X.509當中的subjectAltName擴充套件部分。為了做這個檢測,我們需要從證書中提取出這個部分來,然後根據hostname檢測這個部分,同樣,能夠在證書中根據wild-carded 名字來檢測主機名也是非常有意思的事情。