1. 程式人生 > >來自Facebook的KTLS Kernel SSL/TLS 原理和例項

來自Facebook的KTLS Kernel SSL/TLS 原理和例項

分享一下我老師大神的人工智慧教程!零基礎,通俗易懂!http://blog.csdn.net/jiangjunshow

也歡迎大家轉載本篇文章。分享知識,造福人民,實現我們中華民族偉大復興!

                抽了點時間研究了下KTLS,這源自於跟同事交流的一個問題,那就是現如今的HTTPS伺服器以及scp命令傳輸本地檔案的時候,無法使用sendfile系統呼叫!
        這個話題讓我想起了很多的老同事,為了不騷擾到他們,本文中我一律使用只有我們自己才知道的暱稱。
        我後悔當初為什麼沒有想到一個讓HTTPS/scp支援sendfile/splice/tee呼叫族的方案,我表示後悔是因為這乃是比OpenVPN更加能體現業界影響力以及個人價值的途徑,不管是對公司還是對個人,都將是價值無限的。並且,這是一個融合了系統呼叫,網路技術,加密解密技術於一體的方向。
        我在2010年初經歷了第二次迷茫之後的一年,苦苦尋求方向,在我一頭扎進半瓶子的OpenVPN細節以及一知半解地搞定的原始版國標ECC加密演算法與OpenSSL的融合之後,我選擇了OpenVPN,之後我等於說再也沒有在SSL協議方面有所突破,一去就是四五年!我本來可以做一個類似AF_KTLS的東西的,但是一次又一次的與之擦肩而過,直到現在,我已經離開了原來的公司,並且甚至都不再做任何與SSL/TLS以及PKI相關的事情,卻突然讓我知道了有一個來自Facebook現成的方案,這已經到了2016年!Facebook的方案來自2015年,其實我們可以更早的,但是沒有,所以我表示非常悔恨!我之所以這麼說,是因為幾乎不用你仔細想就能Step by step跟上Dave Watson的思路,一切都顯得那麼自然和直接,就像KTLS是一個理所當然的方案一樣,我曾經發表過《OpenVPN的Linux核心版,鬼魅的殘缺》這一系列文章,並且做了相同的事,但是差那麼一點,如果我當時不是在優化VPN,而是在優化正向代理或者反向代理,或者在優化HTTPS伺服器本身,那麼我認為移進核心的應該就是TLS記錄協議了。我摘錄一句來自文章《
OpenVPN的Linux核心版,鬼魅的殘缺 part IV:Normal Method
》的話:
1.核心模組:執行一個核心模式的TCP伺服器,收到資料後按照資料包的第一個位元組的值區分是控制通道資料還是資料通道資料,如果是控制通道資料,則通過一個字元裝置路由給使用者態的一個程序,如果是資料通道資料,則直接在核心處理;
那是在2014年!
        另外一個插曲那就是,在2010年,我並不知道OpenVPN有自己的封裝協議,我一直以為OpenVPN就是用SSL記錄協議來封裝資料的,這顯然是最初對OpenVPN的錯誤認知,但如果我一直錯下去並一直錯到2014年往核心移植資料通道處理邏輯時,我依然也會把TLS記錄協議扔進核心。雖然,我使用了Netfilter模組機制而不是socket機制...
        正文之前,煽點情吧,溫州皮鞋廠老闆,王姐姐,小雨,小群群,木經理,主音吉他手,華叔...唉,九味雜陳啊...
        另外,KTLS已經出來一年多了,網上的資源非常有限,除了google可以搜到的幾個patch就是來自Dave Watson的github本身了,中文社群幾乎沒有討論這個的,百度搜索的結果更是渺渺,所以我寫了這篇文章,就像關於TCP BBR和TCP CDG的科普文章一般,我感覺我又一次佔了沙發。
        以上乃例行感慨,沒有感慨是不成文的,每一篇文章都是情感的迸發,再加上半夜起來發現天有下雨,就不再睡了,我本人是比較喜歡下雨的。
-----------------------------------
我們知道,sendfile可以讓檔案系統直接和另一個檔案系統通訊,用通訊網路的術語來講,使用者程式僅僅處在控制平面,資料平面完全在核心中進行,這樣可以避免記憶體拷貝以及各種切換,從而大大提高效能。一般的WEB伺服器或者檔案伺服器都是採用sendfile來將一個檔案直接傳送到一個socket的,但是對於HTTPS,這樣不行,因為核心中無法處理加密,你總是要把資料拉到使用者態,將其加密,然後再封裝成TLS記錄協議,然後再Write到socket...
        且慢!不是有AF_ALG socket嗎?不錯,這正是本文的內容之一,現在還不是時候詳解。
        不光HTTPS,就連繫統開發或者運維人員平日非常常用SSH套件中scp,也無法使用sendfile,理由同上!我們可以從下面的圖中看出究竟。




同樣的思路,請看一個關於在BSD系統優化HTTPS的介紹《Optimizing TLS for High–Bandwidth Applications in FreeBSD》。但是本文無意詳述sendfile/splice/tee這個系統呼叫族,這些只是一個引子,我想就這個引子展開針對核心態TLS的討論。本文的結構大致如下:

0.例行的感慨

1.介紹KTLS的原理

2.介紹如何把Dave的例子跑通

3.對KTLS簡單的進行評價

4.另外一種替代的方案

-----------------------------------
這次是Facebook帶來了福音,參見《
TLS in the kernel
》,來自Facebook的Dave Watson引入了一個新型的socket,即AF_KTLS,它可以附著在一個TCP/UDP套接字上在核心態直接加密並且做TLS封裝,其框架如下圖所示:




按照作者所述,TLS握手邏輯還是在使用者態完成,這個握手協議事實上是控制平面的事情。在AF_KTLS socket中,除了完成加密/解密以及記錄協議封裝/解封裝之外,其它操作全部都由其附著於其上的TCP/UDP socket來完成,因此AF_KTLS socket暴露給使用者態的介面就跟標準的AF_INET socket的TCP/UDP套接字一樣,你當然可以通過sendfile給它傳送檔案!
        原理就介紹到這裡,非常簡單!複雜的東西在於加密,解密,HMAC那些操作,這方面我不是專家,在PKI公司混跡了5年有餘也只是略懂,所以說就不在這裡裝逼了。溫州老闆號稱比我懂的多,但也只是號稱,王姐姐那是真懂。
-----------------------------------
下面我們來看一下如何讓Dave的例子跑起來。

1.KTLS原始碼下載

我們從KTLS的github上把原始碼Downlaod下來,並且編譯,其地址如下: https://github.com/ktls/af_ktls

2.編譯和載入

這個步驟我分為以下幾個步驟,對於熟悉的人來講,超級簡單。
2.1.選擇核心版本
我選擇的是最新的Linux 4.9的核心,正好現在搞BBR演算法,也是用的這個核心。估計忙完這一陣子,我要跟2.6.32核心告別了。
2.2.打補丁並重啟到新核心
進入4.9核心根目錄,直接執行 patch -p1 <$rfc5288.patch的路徑即可,然後例行地 make all -j20 && make modules_install && make install即可。編譯成功後重啟到新核心,並且安排好新的4.9核心標頭檔案路徑,準備編譯模組。
2.3.編譯核心模組
此時開始編譯af_ktls.c,直接make即可,skb_splice_bits這個函式的引數會報too many,按照4.9的核心fix掉即可。然後載入編譯生成的af_ktls.ko。

3.KTLS例項下載

Dave順便寫了一個非常簡單的使用KTLS進行記錄協議封裝的例子,基於OpenSSL的。這個例子的結構非常簡單:
Server Thread:執行一個基於OpenSSL的服務端,在一個迴圈中呼叫SSL_write/SSL_read進行資料的讀寫,並輸出結果;
Client Thread:執行一個基於OpenSSL的客戶端,首先使用標準的SSL_read/SSL_write進行資料讀寫,然後呼叫AF_ALG socket進行資料讀寫,並在Server Thread觀測輸出的結果。

注意:Dave的Demo就是使用的AF_ALG socket而不是AF_KTLS,起初我已經他搞錯了,後來發現這是兩個版本。
        它內建了一個TLS服務端和一個TLS客戶端,TLS客戶端與TLS服務端成功完成TLS握手之後,客戶端將用標準的OpenSSL呼叫SSL_read/SSL_write以及KTLS socket的send/recv/sendfile兩種方式與服務端通訊,旨在證明這兩種方式的效果是等同的,從而就可以說明KTLS實現與OpenSSL的協議相容性。
其github地址如下: https://github.com/djwatson/ktls。照例我們應該把它下載到本地並編譯執行。
-----------------------------------
然而,遺憾的是,這個例子並非針對上述AF_KTLS socket實現的,而是針對另一個基於AF_ALG socket的等效機制的(algif_tls patch,後面會談)。我不知道Dave為什麼重新基於AF_KTLS再Fork一個該Demo,雖然針對AF_KTLS基於OpenSSL的例子沒有完成,但是Dave基於GUNTLS完成了一個更加正式的例子,它的地址如下: https://github.com/ktls/af_ktls-tool。通過編譯執行這個例子,你會發現KTLS確實是正確的且高效的。然而,我相信還是熟悉OpenSSL的比熟悉GNUTLS的要多。並且,ktls這個例子非常簡單,就一個C檔案,這種風格是我最喜歡的,簡單清晰,不必陷入程式碼結構,語言以及工具的細節,所以說,我想完成上面那個基於OpenSSL的測試AF_KTLS的例子。
        後來我花了點時間,終於把Dave的那個測試algif_tls的例子改成了測試AF_KTLS的例子並且調通了,並Fork了出來,地址在: https://github.com/marywangran/ktls/tree/patch-1
        除錯的過程非常乏味,主要就是“如何從OpenSSL的EVP_AES_GCM_CTX結構中取出Key,IV以及Salt問題”,這些都是在王姐姐的建議和指導下完成的。不得不說,除錯加密演算法是非常複雜的事情,這種事情我也幹過,但時間不長,而王姐姐是這方面的老手!即便對於Dave Watson而言,可能真的就是因為加密演算法,HAMC演算法非常複雜才迫使其不得不先出一個僅僅支援GCM(aes) 128bit的簡化版本的吧...不過聽王姐姐說,RFC5288所描述的 AES Galois Counter Mode (GCM) Cipher Suites for TLS要比CBC模式快很多,而安全性相當,並且不再需要獨立計算HMAC,相信Dave也懂這個。所以說,Dave的所謂“簡化版”KTLS並非因為這樣實現簡單。
        關於加密演算法就到此為止吧,這個週末抽點時間研究一下GCM的原理。
----------------
我的修改主要是把AF_ALG socket的操作改成了AF_KTLS socket操作:
.... // 等價的OpenSSL操作/* Kernel TLS tests */  int tfmfd = socket(AF_KTLS, SOCK_STREAM, 0);  if (tfmfd == -1) {    perror("socket error:");    exit(-1);  }   struct sockaddr_ktls sa = {                .sa_cipher = KTLS_CIPHER_AES_GCM_128, /* 指定cipher suit*/                .sa_socket = server, /* 指定附著的TCP socket */                .sa_version = KTLS_VERSION_1_2,  };  if (bind(tfmfd, (struct sockaddr *)&sa, sizeof(sa)) == -1) {    perror("AF_ALG: bind failed");    close(tfmfd);    exit(-1);  }  EVP_CIPHER_CTX * writeCtx = ssl->enc_write_ctx;  EVP_CIPHER_CTX * readCtx = ssl->enc_read_ctx;  EVP_AES_GCM_CTX* gcmWrite = (EVP_AES_GCM_CTX*)(writeCtx->cipher_data);  EVP_AES_GCM_CTX* gcmRead = (EVP_AES_GCM_CTX*)(readCtx->cipher_data);  unsigned char* writeKey = (unsigned char*)(gcmWrite->gcm.key);  unsigned char* readKey = (unsigned char*)(gcmRead->gcm.key);  unsigned char* writeIV = gcmWrite->iv;  unsigned char* readIV = gcmRead->iv;  char keyiv[20] = {0};  memcpy(keyiv, writeKey, 16);  if (setsockopt(tfmfd, AF_KTLS, KTLS_SET_KEY_SEND, keyiv, 16)) {    perror("AF_ALG: set write key failed\n");    exit(-1);  }  memcpy(keyiv, writeIV, 4);  if (setsockopt(tfmfd, AF_KTLS, KTLS_SET_SALT_SEND, keyiv, 4)) {    perror("AF_ALG: set write iv failed\n");    exit(-1);  }  uint64_t writeSeq;  unsigned char* writeSeqNum = ssl->s3->write_sequence;  memcpy(&writeSeq, writeSeqNum, sizeof(writeSeq));  if (setsockopt(tfmfd, AF_KTLS, KTLS_SET_IV_SEND, (unsigned char*)&writeSeq, 8)) {    perror("AF_ALG: set write salt failed\n");    exit(-1);  }  memcpy(keyiv, readKey, 16);  if (setsockopt(tfmfd, AF_KTLS, KTLS_SET_KEY_RECV, keyiv, 16)) {    perror("AF_ALG: set read key failed\n");    exit(-1);  }  memcpy(keyiv, readIV, 4);  if (setsockopt(tfmfd, AF_KTLS, KTLS_SET_SALT_RECV, keyiv, 4)) {    perror("AF_ALG: set read iv failed\n");    exit(-1);  }  uint64_t readSeq;  unsigned char* readSeqNum = ssl->s3->read_sequence;  memcpy(&readSeq, readSeqNum, sizeof(readSeq));  if (setsockopt(tfmfd, AF_KTLS, KTLS_SET_IV_RECV, (unsigned char*)&readSeq, 8)) {    perror("AF_ALG: set read salt failed\n");    exit(-1);  }  ....// 在tfmfd socket上的send/recv/sendfile,由tfmfd socket完成記錄加密解密和協議的封裝解封裝,與其下的TCP/UDP socket互動。

-----------------------------------
在繼續另一個基於AF_ALG的KTLS方案之前,我這裡插幾句評價。
        我個人覺得,KTLS並不僅僅是一個優化TLS的patch,而應該是一個名副其實的TLS,即傳輸層安全!在KTLS之前,基於OpenSSL等使用者態庫進行的封裝都只能被成為“buffer安全”,並非真正的傳輸層安全,因為封裝後的資料僅僅是一個socket的buffer。我個人比較關注整體架構,所以我才在這裡咬文嚼字。
        可能是因為Facebook沒有Google那麼大的影響力吧,加之Dave Watson這個人比較低調,所以KTLS才沒有引來一陣吹捧甚至盲目跟風,相反,質疑的聲音卻不斷,某種意義上這倒是好事,可以讓人冷靜地思考這麼做是否真的有必要,真的有必要把TLS協議放在核心中實現嗎?
        誠然,Dave Watson本人也並不建議將整個TLS協議全部在核心層實現,而僅僅針對對稱加密操作以及封裝操作這種固定且穩定的操作。之所以將對稱加密以及封裝操作向下解除安裝,完全是因為這類操作是“固定的一套序列化的操作”。這就跟TCP將計算校驗和的操作以及分段操作解除安裝到網絡卡而不把TCP三次握手以及擁塞控制解除安裝到網絡卡的道理一樣,人們在希望通過向下解除安裝的手段達到優化的目的之前,必須權衡實現的複雜性以及效率,並在兩者中間尋找一個平衡點。另外要考慮的就是,一旦解除安裝的部分在標準上發生了改變,升級操作的代價有多大。
        TLS協議和TCP協議的發展非常類似,如果願意,你也可以把TCP協議分為握手協議和記錄協議,和TCP計算校驗碼以及分段一樣,TLS的記錄協議部分的操作非常固定,AES/DES這種對稱加密演算法非常穩定且固定,很久都不會發生改變,與之不同的是,TLS握手以及證書的處理卻既複雜又多變。正如作者所希望的,KTLS希望乘著將對稱操作從使用者態程式庫的剝離這種一鼓作氣之風,最終推動網絡卡廠商來實現這個硬體解除安裝操作,即將對稱加密和TLS記錄協議封裝的操作完全解除安裝到網絡卡!
        不管怎樣,KTLS可能還是不夠火候,在文章《 TLS in the kernel》的最後,有這麼一段話:
Overall, it looks like it will take some more convincing arguments before putting TLS in the kernel will be seriously considered. For some specialized situations, it might make sense to do so, but even the limited version Watson posted adds more than 1200 lines of code to the kernel—for dubious gains. Over time, more and more crypto has been added to the kernel, though, so maybe TLS will eventually find its way in too.
我和溫州皮鞋廠老闆的對話就不貼了,溫州老闆特別想搞一個這樣的專案製造點影響力,我表示我會全力支援。

----------------
我個人非常贊同Dave Watson的觀點。但是,你可曾想到,假設這個KTLS被應用,那麼將會有什麼必須改變?!需要改變的是Apache,Nginx這種主流伺服器的HTTPS處理邏輯,將所有呼叫SSL_read/write的地方改成基於KTLS的recv/send/sendfile,無疑,改變的代價非常巨大!

        我稱讚KTLS的樸素原因可能源自於我對OpenSSL的偏見,這屬於個人情感的範疇,就不做煽動之辭了。

        隨著安全越來越重要,加密通訊再也不是一個額外的元件了,它會內化成網路協議的一部分,這可以從HTTPS的逐漸風靡看出來,“buffer安全”還是“傳輸層安全”,哪個是你的追求呢?請繼續管中窺豹吧!

-----------------------------------
現在該談一下另一種方案了。
        要實現KTLS,除了像上述那般直接實現一個AF_KTLS socket,還有一種方案,就是直接基於AF_ALG socket來實現,讓AF_ALG socket可以做sendfile的fd_in引數!因此
事實上,這也是Dave Watson的那個algif_tls方案,關於該方案的patch以及討論的patchwork地址是: https://patchwork.kernel.org/patch/7684551/
        要想理解這種做法的合理性,我需要稍微花點篇幅來介紹一個Linux核心的AF_ALG機制。
        AF_ALG是一種socket的型別,與AF_INET並列,該socket型別旨在匯出一族使用者介面,應用程式可以通過呼叫socket介面的方式來進行加密,解密操作。
        關於AF_ALG的最初的patch介紹,請先看一下Lwn文章《 crypto: af_alg - User-space interface for Crypto API》。如果你已經研究了程式碼,那麼不難理出下圖所示的關係:




首先,我們可以看出,最初通過socket建立的一個AF_ALG socket只是一個設定crypt機制的socket,最終需要呼叫accept來獲取一個操作socket,這個過程非常類似TCP以及Linux的INIT程序!其次,我們注意到af_alg_type這個結構體非常重要,它存在的本意是讓你可以實現不同的Crypto API,比如呼叫不同的加密演算法,但它也使得自定義任何操作成了可能,你可以讓一個AF_ALG socket在呼叫send的時候僅僅完成加密解密,然後在recv的時候將結果匯出,也可以將一個AF_ALG OP socket附著在一個TCP socket或者UDP socket上,在呼叫send的時候,首先完成加密,然後將結果通過其附著的TCP socket或者UDP socket發出到網路,隨便怎麼做,取決於af_alg_type這個裡面的回撥你怎麼實現。
        AF_ALG Frame socket本身只是一個抽象的框架,具體的工作要通過將其與一個af_alg_type相關聯,由其生成的一個OP socket來完成。這個原理為Dave實現algif_tls這個patch提供了依據,事實上,他就是實現了一個algif_tls結構體:
static struct proto_ops algif_tls_ops = {        .family         =       PF_ALG,        .....        // sendmsg/page 1.加密訊息;2.通過TLS記錄協議封裝訊息;3.傳送到底層附著的TCP/UDP socket        .sendmsg        =       tls_sendmsg,        .sendpage       =       tls_sendpage,        // recvmsg 1.獲取底層附著的TCP/UDP socket的資料;2.解封裝資料;3.解密資料後拷貝到使用者態緩衝區        .recvmsg        =       tls_recvmsg,        .poll           =       sock_no_poll,};static const struct af_alg_type algif_type_tls = {        // bing 建立底層crypto_aead基礎設施        .bind           =       tls_bind,        .release        =       tls_release,        // setkey 在底層crypto_aead設施上設定金鑰        .setkey         =       tls_setkey,        .setauthsize    =       tls_setauthsize,        // accept 獲取一個OP socket用於實際的讀寫操作,並將以下的ops欄位作為其操作回撥集合        .accept         =       tls_accept_parent,        // ops 實際的OP socket的回撥集合        .ops            =       &algif_tls_ops,        // name 在Frame socket進行bind的時候,可以通過這個name找到該algif_tls_ops,用於索引        .name           =       "tls",        .owner          =       THIS_MODULE};

我們來通過Dave的 tls.c這個例子看一下到底如何操作:

/* Kernel TLS tests */  // 首先建立Frame socket  int tfmfd = socket(AF_ALG, SOCK_SEQPACKET, 0);  // 建立一個索引結構體,用於查詢已經註冊進核心的af_alg_type  struct sockaddr_alg sa = {    .salg_family = AF_ALG,    .salg_type = "tls", /* this selects the hash logic in the kernel */    .salg_name = "rfc5288(gcm(aes))" /* this is the cipher name */  };  // 根據上述的sa建立Frame socket與af_alg_type的關聯  if (bind(tfmfd, (struct sockaddr *)&sa, sizeof(sa)) == -1)  // 獲取一個操作實際IO的OP socket  int opfd = accept(tfmfd, NULL, 0);  ...// 以下OP socket的行為完全取決於af_alg_type中的ops欄位指示的proto_ops回撥集了。

以上就是AF_ALG socket的框架以及Dave Watson在此框架上實現的不同於AF_KTLS的另一種KTLS方案。但是這並不是我要說的全部,既然AF_ALG的框架這麼靈活,那為什麼還要單獨搞一個新的socket型別AF_KTLS呢?這可能是一個形而上的問題,我把 這篇文章中的內容摘錄如下,來自af_alg的作者Herbert Xu:
On Mon, Nov 23, 2015 at 09:43:02AM -0800, Dave Watson wrote:
> Userspace crypto interface for TLS.  Currently supports gcm(aes) 128bit only,
> however the interface is the same as the rest of the SOCK_ALG interface, so it
> should be possible to add more without any user interface changes.

SOCK_ALG exists to export crypto algorithms to user-space.  So if
we decided to support TLS as an algorithm then I guess this makes
sense.

However, I must say that it wouldn't have been my first pick.  I'd
imagine a TLS socket to look more like a TCP socket, or perhaps a
KCM socket as proposed by Tom.


於是,AF_KTLS難道就是一個更加合理的選擇嗎?
        現在繼續AF_KTLS和AF_ALG+自定義af_alg_type孰優孰劣的形而上討論。個人認為還是AF_ALG+自定義af_alg_type更加靈活,你只需要自己實現一個af_alg_type並且註冊進核心,就可以實現任何形式的封裝,甚至實現一個完整的HTTP協議的處理過程。完全不像AF_KTLS那樣自己實現一個完整的socket型別,那樣會讓你陷入類似ioctl分發類似的泥潭。誠然,sendfile將2.4核心的Kernel Web Server踢出了核心,但是AF_ALG+自定義af_alg_type則是更加通用的機制,它可以實現任何應用層協議!
-----------------------------------
還記得我在2014年到2015年間將OpenVPN的資料通道協議移植到了核心態,github地址在: https://github.com/marywangran/OpenVPN-Linux-kernel

        這個工作與Dave Watson的工作非常類似,仔細研究了Dave的KTLS之後,我本來想花這個週末的時間重新再重構一下這個KOpenVPN了,但是顯然感到無力,所以我退而求其次,我只是想基於AF_ALG+自定義af_alg_type來實現一個簡單的HTTP協議,僅用來傳輸檔案,大體邏輯非常簡單,即通過sendfile將一個檔案傳送給一個AF_ALG OP socket,然後實現一個af_alg_type完成HTTP頭的新增後再繼續發給底層的INET socket。如果換一種方式,我也可以單獨寫一個AF_MYHTTP模組來用一個新型的socket更加直接地實現上述需求。

------------------------------------

在完成了這篇文章後,我的下一個挑戰就是,2016年12月31日要在小小幼兒園的小區開闊場地開唱《新長征路上的搖滾》(這源自於幾周前小小秋遊時在路上我被瘋子逼著唱了一首《假行僧》,然後幼兒園老師就非要讓我在新年聯歡會上再來一首搖滾...)...有點緊張,決定喝瓶酒再上!好在瘋子可以陪我上去唱《One night in BeiJing》....有人陪我丟人,總比一個人丟人強吧...總之,加油吧!為了孩子,啥都可以付出,唱個歌丟一回人算個毛線啊!

           

給我老師的人工智慧教程打call!http://blog.csdn.net/jiangjunshow

這裡寫圖片描述