首頁>Club>
8
回覆列表
  • 1 # 使用者3190326292953

    以Nginx對OpenSSL的使用為入口,來分析OpenSSL的API的模型。OpenSSL是兩個庫,如果以握手為目的只會使用libssl.so這個庫,但是如果有加密的需求,會使用libcrypto.so這個庫。Nginx中對於OpenSSL的使用大部分是直接使用的libssl.so的介面API的,但是仍然會有少部分使用libcrypto.so。除了Nginx,本章還會分析一個s_server程式,透過這個程式的設計,能夠對OpenSSL的內部架構有一個初探。

    Nginx的Stream中SSL的實現

    Nginx的Stream Proxy中有對於SSL的Terminator的支援。這個終端的意思是可以在Nginx層面把SSL解掉,然後把明文傳輸給後端。也就是說支援SSL的Nginx的Stream模組實際上是一個TLS的握手代理,將TLS通道在本地解了再發送到後端,所以整個過程是一個純粹的握手過程,至於ALPN這種功能就需要後端與TLS的配合才可以,所以這種行為在stream 的SSL中是不能支援的。

    這是一個Nginx的Stream SSL模組相關的函式列表,主要的Stream模組特有的功能也都就在這個列表裡了。可以看到除去配置和模組的整體初始化函式,只剩下一個連線初始化,ssl的入口handler和握手的handler。顯然握手的handler是入口handler的深入部分。鑑於Nginx的非同步模型,可以很容易的想到是Nginx在收到一個連線的時候首先使用ssl_handler作為通用入口,在確定是SSL連線之後就會切換到handshaker_handler作為後續的握手handler函式。

    但是Nginx在支援SSL的時候並不是這樣的輕鬆,因為大量的SSL相關函式在ngx_event_openssl.c檔案裡,這個檔案裡的函式被HTTP模組和Stream模組或者其他需要SSL支援的模組共同使用。包括Session Cache等Nginx重新實現的OpenSSL功能。透過這個例子可以看到如果要自己實現一個SSL支援,我們需要兩個東西,一個是SSL的使用者端的介面封裝庫(ngx_event_openssl.c),一個是如何把封裝庫的邏輯嵌入到我們的程式碼流程的邏輯。Nginx作為一個強大的負載均衡裝置,這一部分的介面嵌入應該是要追求的最小化實現的。也就是說Stream模組相關的程式碼越少越好(ngx_stream_ssl_module.c)。所以我們可以看到幾乎就幾個鉤子函式的定義。

    無論是Stream還是HTTP模式,整個TLS握手的核心函式都是ngx_ssl_handshake函式。我們看這個函式就能看到一個企業級的握手介面的使用案例。以下是一個簡化版的函式流程:

    以上是一個同步版本的大體邏輯,非同步版本的就沒有顯示。可以看到主要的SSL握手的入口函式是SSL_do_handshake。如果握手正常,函式返回1之後,使用SSL_get_current_cipher或得到伺服器根據客戶端發來的密碼學引數的列表選擇得到的密碼學套件。這裡會返回伺服器選擇的那個,如果返回為空,那麼就代表了伺服器沒有找到匹配的套件,連線就不能繼續。SSL_CIPHER_description函式輸入活的指標,返回一個字串格式的套件的描述資訊,Nginx這裡使用了這個資訊,最後一步就是查詢當前的Session Cache中是否有可以複用的邏輯。這裡只是一個查詢,並不是就是複用的決定。因為是否複用是在連線建立之前由配置決定的,如果Nginx配置了不使用OpenSSL的Session Cache,這個查詢就會一直返回0,表示沒有被複用。而且這裡查詢的OpenSSL中是否有複用,並不代表Nginx內部是否有複用,Nginx內部還有一套自己的Session Cache實現,但是使用SSL_開頭的API函式都是OpenSSL的介面。

    這個簡單的介面可以看出對OpenSSL的API的使用的一些端倪。OpenSSL提供的API非常多,我們寫一個簡單的示例程式僅僅會用到幾個最簡單的介面,例如SSL_new等。但是一個正式的專案,會用到很多細節的API介面。由於OpenSSL只會暴露他認為應該暴露的API函數出來給呼叫者使用,其他的函式呼叫者是用不到的,並且OpenSSL內部的結構體外部也是不能使用的,所以使用者所有的行為都是要基於API進行設計。

    OpenSSL分為libcrypto.so和libssl.so兩個庫。在使用TLS握手的時候,主要的呼叫API都位於ssl.h檔案中定義,都是SSL_開頭的API。但是這並不意味著只能呼叫libssl.so的介面,高階的使用者並不是想要使用OpenSSL的TLS握手功能,完全可以直接呼叫libcrypto.so裡面的各種各樣的密碼學庫。總的來說libssl.so是一個TLS握手庫,而libcrypto.so是一個通用的密碼學的庫。只是libssl.so的握手使用的密碼學是完全依賴libcryto.so中提供的。也就是因此,在使用TLS握手的時候,是基本上不會直接用到libcrypto.so中的API的。

    s_server

    openssl s_server是一個簡單的SSL伺服器,雖然說是簡單,但是其中包含了大部分使用者SSL程式設計需要考慮的東西。證書,密碼,過期校驗,密碼學引數定製,隨機數定製等等。這是一個功能性的程式,用於驗證openssl內部的各項SSL握手伺服器的功能是否能夠正常使用,並不能用於直接服務於線上業務。

    s_server程式啟動的第一步是解析各種引數,在正常運作的時候,第一步是載入key。

    我們看到OpenSSL內部呼叫的函式和在使用OpenSSL庫介面的時候是不一樣的,OpenSSL的子程式會呼叫一些內部的介面。比如這裡使用了ENGINE_init,直接初始化了底層的引擎系統。ENGINE系統是OpenSSL為了適配下層不同的資料引擎設計的封裝層。有對應的一系列API,所有的ENGINE子系統的API都是ENGINE_開頭的。一個引擎代表了一種資料計算方式,比如核心的密碼學套件可以有一個專門的OpenSSL引擎呼叫到核心的密碼學程式碼,QAT硬體加速卡也會有一個專門的引擎,OpenSSL自己的例如RSA等加密演算法的實現本身也是一個引擎。這裡在載入key的時候直接初始化一個引擎,這個引擎在init之前還要先呼叫一個setup_engine函式,這個函式能夠設定這個將要被初始化的引擎的樣子。s_server之所以要自己用引擎的API介面是因為它支援從命令列輸入引擎的引數,指定使用的引擎。

    可以看到,如果指定了auto,就會載入所有預設的引擎。如果指定了特定ID的引擎,就只會載入特定的引擎。一個引擎下面是所有相關的密碼學的實現,載入key就是一個密碼學層面的操作,所以也要使用ENGINE提供的介面。事實上,最後都是分別呼叫了對應的ENGINE的具體實現,這中間都是透過方法表的指標的方式完成的。ENGINE定義的通用的介面還有很多,這裡只是用到了載入金鑰。

    表內都是對不同的EVP_CIPHER和EVP_MD的介面的定義。

    我們回到載入key的函式,繼續閱讀發現一個 key = bio_open_default(file, "r", format); 這個key是一個BIO型別的指標,這個BIO型別的指標就是另外一個OpenSSL的子系統,所有的IO操作都會被封裝到這個子系統之下。例如這裡使用的檔案IO,用於從檔案中讀取key的結果。BIO被設計為一個管道式的系統,類似於Shell指令碼中見到的管道的效果。有兩種型別的BIO,一種是source/sink型別的,就是我們最常見的讀取檔案或者Socket的方式。另外一種是管道BIO,就是兩個BIO可以透過一個管道BIO連線起來,形成一個數據流。所以BIO的方式是一個很重量級的IO系統的實現,只是目前只是被OpenSSL內部使用的比較多。

    繼續向下閱讀載入金鑰的函式,會發現PEM_read_bio_PrivateKey函式,這一步就是實際的從一個檔案中讀取金鑰了。我們現在已經有了代表檔案讀寫的BIO,代表密碼學在程式中的封裝EVP,中間缺的橋樑就是檔案中金鑰儲存的格式。這裡的以PEM_開頭的函式就代表了PEM格式的API。PEM是密碼學的儲存格式,PEM_開頭的API就是解析或者生成這種格式的API,當然它需要從檔案中讀取,所以引數中也會有BIO的結構體,PEM模組使用BIO模組提供的檔案服務按照定義的格式將金鑰載入到記憶體。

    OpenSSL的所有apps都會共享一些函式,這些函式的實現都在一個apps.c檔案中,以上的載入金鑰的函式也是其中的一個。s_server程式在呼叫完load_key之後會繼續呼叫load_cert來載入證書。load_cert使用的子系統與load_key非常類似,類似的還有後面的load_crl函式,CRL(Certificate revocation lists)是CA吊銷的證書列表,這項技術已經基本被OCSP淘汰。OpenSSL還提供一個隨機數檔案的功能,可以從檔案中載入隨機數。

    s_server在載入完相關的密碼學相關引數後,就會開始建立上下文,SSL_CTX_new函式的呼叫就代表了上下文的建立。這個上下文是後面所有SSL連線的母板,對SSL的配置設定都會體現在這個上下文的設定中。隨後,s_server會開始設定OpenSSL伺服器支援的TLS握手版本範圍,分別呼叫SSL_CTX_set_min_proto_version和SSL_CTX_set_max_proto_version兩個函式完成所有操作。

    OpenSSL在共享TLS握手的Session時,需要生成一個Session ID,預設的情況,OpenSSL會在內部決定Session ID怎麼生成。但是也提供了使用者設定這個生成演算法的API。s_server程式呼叫SSL_CTX_set_generate_session_id函式設定一個自己的回撥函式,在這個回撥函式中就可以完成Session ID的設定,從而取代掉OpenSSL自帶的內部Session ID的生成器。OpenSSL在證書協商的時候還會允許外部的庫使用者動態的修改採用的證書,這個機制是透過SSL_CTX_set_cert_cb來設定證書回撥函式實現的。s_server也有這個函式的設定。程式走到這裡,基本能看到OpenSSL的一個很大的特性,就是大部分的內部流程都會提供一個回撥函式給使用者來註冊,使用者可以按照自己的需求取代掉或者修改OpenSSL內部的功能。顯然這個s_server程式是一個功能展示的程式,會用上大量的函式回撥點。比如緊接著呼叫的SSL_CTX_set_info_callback函式就是在生成SSL的時候呼叫的,可以用於使用者獲得狀態。不但OpenSSL外部的機制可以在使用者端設定,使用者甚至可以設定加密演算法的引數。例如s_server就會接下來根據使用者是否提供DH引數來設定內部的引數。如果呼叫了SSL_CTX_set_dh_auto就意味著引數是使用內部的機制生成,這也是預設的行為。但是仍然可以提前提供,主要是為了效能的考慮,比如提前提供DH的大素數,DH演算法在運算的過程中需要一個取模操作,這個取模是對一個大素數進行取模的,而這個大素數預設是在執行的時候動態生成的,但是我們可以提供這個素數,從而以犧牲一定的安全性為代價換來效能的提高。

    s_server在設定完整個上下文之後,就會進入Socket監聽和處理的模式。由於BIO框架包含了Socket的能力,所以這一步本質上就是呼叫BIO的介面。

    這是一個典型的OpenSSL的Socket邏輯。BIO_sock_init這個函式在Linux下就是空函式,沒有意義。BIO_lookup是一個通用的獲取地址的方法,對於Socket就是IP:PORT的字串,對於檔案是檔案的目錄。BIO_socket意思就相當於在使用Socket變成的socket函式。BIO_listen也就自然對應listen函式,BIO_accept_ex和BIO_closesocket也是類似的意思。整個流程其實就與一個普通的Socket流程沒有太大區別,只是BIO多了一層封裝。因為OpenSSL是個跨平臺的庫,這層封裝更多的意義在於用在跨平臺的應用上的。

    透過一個簡單的s_server程式的分析可以看到整個OpenSSL的主要設計思路。它對外封裝了不同的模組,例如ENGINE,EVP,BIO之類的封裝。在大部分的流程上都提供了回撥函式API,使用者可以用回撥函式來修改OpenSSL原來的邏輯或者獲得其他的資訊。在使用OpenSSL的時候一般需要遵循類似的流程,就是建立上下文,然後配置上下文,然後執行服務。

  • 中秋節和大豐收的關聯?
  • 文言文情書有哪些?