SSL/TLS協議詳解(下)——TLS握手協議

原文鏈接:https://xz.aliyun.com/t/2531

SSL/TLS協議詳解(下)——TLS握手協議


本文翻譯自:https://www.wst.space/ssl-part-4-tls-handshake-protocol/


  在博客系列的第2部分中,對證書頒發機構進行了深入的討論.在這篇文章中,將會探索整個SSL/TLS握手過程,在此之前,先簡述下最後這塊內容的關鍵要點:

  • TLS適用於對稱密鑰
  • 對稱密鑰可以通過安全密鑰交換算法共享
  • 如果請求被截獲,密鑰交換可能會被欺騙
  • 使用數字簽名進行身份驗證
  • 證書頒發機構和信任鏈。

  在這篇文章中,使用WireShark的工具來查看網絡流量,我個人使用的是Linux(Ubuntu 16.04)系統,可以使用以下命令輕鬆安裝WireShark:

$sudo apt install wireshark

  以sudo的權限打開WireShark並選擇提供互聯網連接的接口,我這裏是eth0,然後點擊WireShark右上角的“開始捕獲數據包”按鈕,Wireshark將立即開始抓取通過機器的所有流量。現在我們從瀏覽器中加載github.com。Github使用TLS進行所有通信,將重新定向到https並加載。現在關閉瀏覽器,看看WireShark抓到了什麼。

DNS解析

  這並不是TLS的一部分,但我們在WireShark中看到了它。

  我已將Google DNS設置爲我的DNS服務器,它的地址是8.8.8.8,在圖像中可以看到請求已發送到8.8.8.8查詢github.com的A記錄,也就是我們想要連接的Github的IP地址。

  DNS服務器使用github.com的IP響應爲192.30.253.113,藍色表示選擇顯示相應的部分,現在,瀏覽器已獲取了將要用來連接服務器的目標IP。

發起TLS握手

  解析IP後,瀏覽器將通過http請求頁面,如果服務器支持TLS,那麼它將發送協議升級請求來響應瀏覽器,這個新的地址https://github.com ,將使用端口號443來指定,隨後瀏覽器將啓動TLS握手請求。大多數現代瀏覽器都存有與Web服務器的最後一次連接的記錄,如果最後一次連接是通過https進行的,那麼下次瀏覽器將自動啓動https請求而無需等待服務器。
TLS握手分爲以下幾個步驟:

  • 客戶端發送Hello報文
  • 服務器接收Hello報文
  • 共享證書和服務器密鑰交換
  • 更改密碼規範
  • 加密握手

客戶端發送Hello報文

  從這裏開始,我將會重點討論圖片中標記爲藍色的主題,Client發送的Hello報文如下圖所示。

  我們知道TLS是在TCP之上實現的協議,TLS本身是一層協議並且它的底層叫做記錄協議(Record protocol),這意味着所有數據都被記錄。典型的記錄格式如下:

HH V1:V2 L1:L2 data
  • HH是單個字節,表示記錄中的數據類型。共定義了四種類型:change_cipher_spec(20),alert(21),handshake(22)和application_data(23)。
  • V1:V2是協議版本,用兩個以上的字節表示。對於當前定義的所有版本,V1的值爲0x03,而對於SSLv3,V2的值爲0x00,對於TLS 1.0爲0x01,對於TLS 1.1爲0x02,對於TLS 1.2爲0x03。
  • L1:L2是數據的長度,以字節爲單位(使用big-endian約定:長度爲256 * L1 + L2),數據的總長度不能超過18432字節,但實際上它無法達到這個值。

  在圖中,可以看出內容類型是Handshake,TLS版本1.0,數據長度爲512.真實數據位於名爲 Handshake Protocol:Client Hello的下拉列表中。我們繼續觀察下Client Hello中共享的數據。

客戶端發送Hello報文的內容

  瀏覽器與服務器共享以下詳細信息

客戶端版本

  按優先順序列出的客戶端支持的協議版本,首選客戶希望支持的最新協議版本。

客戶端的隨機數

  一個32字節的數據,其中前4個字節表示epoch格式的當前日期時間。紀元時間是自1970年1月1日以來的秒數。其餘28個字節由加密強隨機數生成器生成(例如,Linux中的/dev/urandom),客戶端隨機會在後面用到,請先記住這點。

會話id(Session id)

  如果客戶端第一次連接到服務器,那麼這個字段就會保持爲空。在上圖中,您可以看到Session id正在給服務器發送東西,之所以會發生這種情況是由於我之前是通過https連接到github.com的,在此期間,服務器將使用Session id映射對稱密鑰,並將Session id存儲在客戶端瀏覽器中,爲映射設置一個時間限。如果瀏覽器將來連接到同一臺服務器(當然要在時間限到期之前),它將發送Session id,服務器將對映射的Session進行驗證,並使用以前用過的對稱密鑰來恢復Session,這種情況下,就不需要完全握手。

密碼套件

  客戶端還將發送自己已經知道的密碼套件列表,這個是由客戶按優先順序排列的,但完全由服務器來決定發送與否。TLS中使用的密碼套件有一種標準格式。

我們從列表中用一個例子來進行分析.

Cipher Suite: TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256 (0xc02b)
  • TLS:指使用的協議是TLS
  • ECDHE:密鑰交換算法
  • ECDSA:簽名或驗證算法
  • AES_128_GCM:稱爲批量加密算法。對稱密鑰加密算法是AES,密鑰長度爲128位,AES是塊密碼,也就是對輸入的純文本用固定長度的塊來進行加密,加密後的每個塊按再順序發送,最後按類似的方式來進行解密。按標準規定,AES塊固定長度爲128位,但是輸入的明文不要求必須是128的倍數,所以我們可能需要對最後一個塊中進行填充,使其爲固定的長度128位。除此之外,爲了提高平均信息量,通常在加密之前會添加一些隨機的比特到明文中,我們稱爲初始化矢量(IV)。有很多算法都可以在塊上添加IV實現填充。在我們的例子Galois/Counter Mode(GCM)中用到過。或許詳細解釋GCM模式會使事情變得複雜,可見這並不是一個好主意。
    SHA256:消息驗證代碼(MAC)算法。我們將詳細討論MAC。

壓縮數據

  爲了減少帶寬,可以進行壓縮。但從成功攻擊TLS的事例中來看,其中使用壓縮時的攻擊可以捕獲到用HTTP頭髮送的參數,這個攻擊可以劫持Cookie,這個漏洞我們稱爲CRIME。從TLS 1.3開始,協議就禁用了TLS壓縮。

擴展名

  其他參數(如服務器名稱,填充,支持的簽名算法等)可以作爲擴展名使用,我們可以任意對用作擴展名的內容研究一番。

  這些是客戶端問候的一部分,如果已收到客戶端問候,接下來就是服務器的確認,服務器將發送服務器問候。

服務器接收Hello報文

  收到客戶端問候之後服務器必須發送服務器問候信息,服務器會檢查指定諸如TLS版本和算法的客戶端問候的條件,如果服務器接受並支持所有條件,它將發送其證書以及其他詳細信息,否則,服務器將發送握手失敗消息。

圖中,我們可以看到服務器響應0x0303表示服務器同意使用TLS 1.2,我們來檢查一下服務器問候中的記錄。

服務器接收Hello報文的內容

  服務器問候消息包含以下信息。

加下來我們會在這裏討論其中一些重要的參數。

服務器版本

  如果客戶端可以支持,則服務器將選擇客戶端指定的TLS版本,這裏選擇了TLS 1.2

服務器的隨機數

  類似於客戶端隨機,服務器隨機也佔32字節,前4個字節表示服務器的Unix紀元時間,後面加上28字節的隨機數。客戶端和服務器隨機將用來創建加密密鑰,我待會兒會解釋。

密碼套件

  還記得我們已經將發送支持的密碼套件發送到客戶端問候中的github.com嗎?Github從名單中選出了第一個,也就是:

TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256

會話id(Session id)

  服務器將約定的Session參數存儲在TLS緩存中,並生成與其對應的Session id。它與Server Hello一起發送到客戶端。客戶端可以寫入約定的參數到此Session id,並給定到期時間。客戶端將在Client Hello中包含此id。如果客戶端在此到期時間之前再次連接到服務器,則服務器可以檢查與Session id對應的緩存參數,並重用它們而無需完全握手。這非常有用,因爲服務器和客戶端都可以節省大量的計算成本。

  在涉及亞馬遜和谷歌等流量巨大的應用程序時,這種方法存在缺點。每天都有數百萬人連接到服務器,服務器必須使用Session密鑰保留所有Session參數的TLS緩存。這是一個巨大的開銷。爲了解決之前介紹過的Session Tickets的問題, 在這裏,客戶端可以在client hello中指定它是否支持Session Ticket。然後,服務器將創建一個新的會話票證(Session Ticket),並使用只有服務器知道的經過私鑰加密的Session參數。它將存儲在客戶端上,因此所有Session數據僅存儲在客戶端計算機上,但Ticket仍然是安全的,因爲該密鑰只有服務器知道。

  此數據可以作爲名爲Session Ticket的擴展包含在Client Hello中。在我們的例子中,此參數爲空,因爲這是第一次連接到github.com或前一個Session的瀏覽器已過期。

壓縮數據

  如果支持,服務器將同意客戶端的首選壓縮方法。在這裏,您可以看到服務器響應爲空響應,則意味着不需要壓縮。

  服務器不在ServerHello消息中發送任何證書; 它會在正確命名的證書消息中發送證書。

服務器證書的信息


  在我們的例子中,證書消息長度爲3080字節。毫無疑問,這是包含所有信息的服務器證書。服務器按信任鏈的順序發送完整的證書列表。該鏈中的第一個是服務器證書,接着是頒發服務器證書的intermediate CA 的證書,然後是下一個intermediate CA 的證書......直到Root CA的證書。服務器不可以發送Root CA證書,因爲在大多數情況下,瀏覽器可以從任何intermediate CA 識別Root CA。

  在我們的例子中,您可以看到第一個證書是github.com,第二個證書是中間件Digicert SHA2擴展驗證Server CA。 檢查下圖中的id-at-commonName參數。

讓我們分析證書的內容,看看瀏覽器如何驗證它。

證書的內容

  證書被髮送到瀏覽器,因此我們可以在訪問github.com時查看Github的證書。來自Firefox的CA證書內容:

可以通過單擊"詳細信息"選項卡查看github的intermediate CA 和Root CA.

讓我們瞭解這些領域是什麼以及它們的用途。

版本和序列號

  版本表示使用的是哪個版本的X.509標準。X.509是用於定義公鑰證書格式的標準。X.509有3個版本,github使用最新版本version 3。

  從RFC 5280開始,CA爲每個證書分配的序列號必須是正整數。因此對於每個發佈CA證書,它必須是唯一的(即頒發者名稱和序列號標識唯一的證書)。所以,CA必須強制serialNumber爲非負整數。

證書的簽名算法與值

  瀏覽器需要知道簽名算法以驗證簽名。如果使用的是RSA簽名,則需要相同的算法來驗證簽名。對於Github,使用的是PKCS#1 SHA-256和RSA加密,即SHA-256用於生成散列,RSA用於簽名。

  從我們上一篇文章中,證書數據使用SHA-256算法進行哈希處理,並使用RSA加密過Github的私鑰對此哈希進行簽名。

頒佈機構

  此字段包含頒發證書的頒發機構的詳細信息。Github的證書由Digicert的intermediate CA 頒發。

合法性

  該字段有兩個值Not Before 和Not After 。如果當前日期時間不在這些值之間,則證書無效。瀏覽器就不會信任該證書。

子公鑰信息(Subject Public Key Info)

  該字段攜帶公鑰和用於生成公鑰的算法。此密鑰用於交換密鑰,我們將在稍後討論。

指紋

  瀏覽器生成了兩個指紋SHA 1和SHA-256,而且不會發送到服務器。這些指紋分別是通過SHA 1和SHA-256函數散列DER格式的證書產生的。我們可以通過將證書下載到我們的機器並應用哈希函數來驗證這一點。

  單擊詳細信息選項卡左下角的“ 導出” 按鈕以下載證書,保存爲.crt 擴展名,並在終端上運行以下命令以生成證書的指紋。

$ openssl x509 -noout -fingerprint -sha256 -inform pem -in [certificate-file.crt]

$ openssl x509 -noout -fingerprint -sha1 -inform pem -in [certificate-file.crt]

  這應該產生與您在瀏覽器中看到的結果相同的結果。這些值不是證書的一部分,而是根據證書計算出來的。Root CA證書的指紋將在瀏覽器中進行硬編碼,因此可以輕鬆地進行交叉驗證。除此之外,這些指紋主要用於識別和組織證書。不要將Signature與指紋混淆

  我們在這裏討論的證書信息是關於github.com的服務器證書。Github的intermediate CA 證書也將在同一請求中發送給客戶,所有上述字段也適用於該證書。您可以通過轉到詳細信息選項卡並單擊intermediate CA 來檢查,如下所示。

服務器端密鑰交換

  隨後是Server Hello和證書消息(Certificate message),服務器密鑰交換(Server Key Exchange)是可選的。僅當服務器提供的證書不足以允許客戶端交換預主密鑰時,纔會發送此消息。讓我們看看爲什麼github.com必須發送服務器密鑰交換消息。

  我們可以看到github.com首選Session的密碼套件是TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256。這意味着雙方使用Elliptic Curve Diffie Hellman算法來交換密鑰。在Diffie-Hellman中,客戶端無法自行計算預主密鑰; 雙方都有助於計算它,因此客戶端需要從服務器獲取Diffie-Hellman公鑰。(不要對"Pre-Master Secret"一詞感到困惑,我們將在下面深入討論它。)當使用Elliptic Curve Diffie-Hellman時,該公鑰不在證書中。因此,服務器必須在單獨的消息中向客戶端發送其DH公鑰,以便客戶端可以計算預主密鑰。這可以在上面的圖像中看到。請注意,此密鑰交換也由簽名保護。

  服務器密鑰交換完成後,服務器將發送Server Hello Done 消息。客戶端將開始計算Pre-Master Secret。我們來看看如何。

如何計算Pre-Master Secret

  Pre-Master Secret計算取決於商定的密鑰交換算法的類型。當使用RSA進行密鑰交換時,從客戶端(即瀏覽器)計算預主密鑰,客戶端通過連接協議版本(2個字節)和客戶端隨機生成的一些字節(46個字節)來生成48字節的預主密鑰。客戶端從加密安全的僞隨機數發生器(PRNG)獲得這46個字節。實際上,這意味着使用操作系統提供的PRNG,例如/dev/urandom。然後,使用服務器的公共和共享對此Pre-Master密鑰進行加密,以便服務器稍後可以使用它來創建主密鑰

  但是,在Github的情況下,如上所述,Diffie-Hellman算法用於密鑰交換。這裏的情況略有不同。服務器立即生成一對DH私鑰 - 公鑰。然後,與客戶共享公鑰。這是如上所述的"服務器密鑰交換消息( Server Key Exchange)"。作爲響應,客戶端還將創建DH密鑰對,並通過客戶端密鑰交換消息與服務器共享公鑰,如下所示。

  您可以看到共享的客戶端公鑰。現在,如果您瞭解Diffie-Hellman算法的工作原理,您就知道客戶端和服務器可以從這些共享公鑰到達公共密鑰。新生成的密鑰稱爲Pre-Master密鑰。

  使用Diffie Hellman算法進行TLS密鑰交換具有優勢。客戶端和服務器都爲每個新會話生成一個新密鑰對。一旦計算出預主密鑰,將立即刪除客戶端和服務器的私鑰。這意味着私鑰永遠不會被竊取,確保完美的前向保密

客戶端密鑰交換

  我們已經在上面討論過,客戶端的DH公鑰通過客戶端密鑰交換消息共享給服務器。但是如果使用RSA,則客戶端將如上所述通過其自己計算預主密鑰,使用服務器的公鑰(RSA公鑰)對其進行加密,並通過客戶端密鑰交換消息將其發送回服務器。然後,服務器可以使用其私鑰解密它。無論算法是什麼,此時客戶端和服務器都達到了共同的Pre-Master Secert 。完成此操作後,客戶端將發送Change Cipher Spec 消息,如下所示。

讓我們往下走,看看如何在主密鑰從預備主密鑰來計算。

如何計算主祕鑰

  現在客戶端和服務器都有哪些隨機數據呢?根據RFC 5346標準,在問候消息期間客戶端和服務器共享的預主密鑰和隨機值(還記得嗎?)都會使用PRF(僞隨機函數)產生的值來計算主密鑰。

master_secret = PRF(pre_master_secret,“master secret”,ClientHello.random + ServerHello.random)[0..47];

這裏,

pre_master_secret - 雙方計算的48字節Pre-Master密碼。

“master secret” - 它只是一個使用ASCII字節的字符串。

ClientHello.random - 客戶端hello中共享的隨機值

ServerHello.random - 服務器hello中共享的隨機值。

  主密鑰的大小共48個字節,好吧,到目前爲止還不是太亂。雙方都可以使用主密鑰加密數據並來回發送,確實如此,但程序還沒結束。你認爲雙方使用相同的祕鑰是個好辦法嗎?當然不是!TLS爲客戶端和服務器分配了單獨的密鑰,它們都來自主密鑰本身,換句話說,主密鑰不直接用於加密數據,而是將單獨的加密密鑰用於客戶端和服務器。由於雙方都有兩個密鑰,服務器用其密鑰加密的數據可以由客戶端輕鬆解密,反之亦然。

  還沒完,TLS還具有用於對稱密鑰加密的附加安全機制。

消息驗證代碼(MAC)和TLS數據完整性

  竊聽者可以對傳輸中的加密數據進行兩種可能的攻擊:嘗試解密數據或嘗試修改數據。只要密鑰安全,我們就可以認爲解密基本上是不可能的,但如果是修改數據呢?客戶端和服務器是怎麼知道攻擊者沒有修改過數據呢?如上所述,TLS不僅僅是加密數據,還可以保護數據,使其免受未檢測到的修改,換句話說,TLS可以檢查數據的完整性。讓我們看看它是怎麼做到的。

  當服務器或客戶端使用主密鑰加密數據時,它還會計算明文數據的校驗和(哈希值),這個校驗和稱爲消息驗證代碼(MAC)。然後在發送之前將MAC包含在加密數據中。密鑰用於從數據中生成MAC,以確保傳輸過程中攻擊者無法從數據中生成相同的MAC,故而MAC被稱爲HMAC(哈希消息認證碼)。另一方面,在接收到消息時,解密器將MAC與明文分開,然後用它的密鑰計算明文的校驗和,並將其與接收到的MAC進行比較,如果匹配,那我們就可以得出結論:數據在傳輸過程中沒有被篡改。

  客戶端和服務器必須使用相同的散列算法來創建以及驗證MAC,還記得Github同意的密碼套件的最後一部分嗎?
TLS_ECDHE_ECDSA_WITH_AES_128_GCM_ SHA256。即SHA256 是用於處理HMAC的哈希函數,爲了提高安全性,客戶端和服務器使用MAC密鑰。讓我們看看這些是什麼。

MAC密鑰和IV密鑰

  根據要求,有4個密鑰用於加密和驗證每個消息的完整性,他們是:

  • 客戶端寫入加密密鑰:客戶端用賴加密數據,服務器用來解密數據。
  • 服務器寫入加密密鑰:服務器用來加密數據,客戶端用來解密數據。
  • 客戶端寫入MAC密鑰:客戶端用來創建MAC,服務器用來驗證MAC。
  • 服務器寫入MAC密鑰:服務器用來創建MAC,客戶端用來驗證MAC。

  這些密鑰塊由主密鑰上的相同的PRF反覆地生成,直至密鑰有了足夠的字節。

key_block = PRF(SecurityParameters.master_secret,“密鑰擴展”,SecurityParameters.server_random + SecurityParameters.client_random);

  如您所見,除了客戶端 - 服務器隨機值和字符串“密鑰擴展”之外,主密鑰還用來增加密鑰的平均信息量。PRF可以生成任意長度的密鑰,這點是很有用的,因爲默認情況下不同的散列函數具有不同的長度。在我們的例子中用的是SHA256,它是256位,但MD5的默認長度爲128位。

  除此之外,我們知道我們使用的AES和GCM算法是一種分組密碼,它需要一組比特來作爲初始化向量(IV)。在討論密碼套件時,我們已經提到IV用於改善AES加密的平均信息量,換句話說,當多次加密同一文件時,IV能夠生成不同的密文,這些隨機的字節也由相同的PRF生成,並且被稱爲客戶端寫入IV 和服務器寫入IV ,術語是自解釋的。我不會對IV的細節再進行更多講解,因爲它是一個很大的主題,超出了本文的範圍。

生成測試數據

  現在雙方都有了加密密鑰,我們準備加密,但是在將TLS放到應用層之前,我們需要像每個進程一樣來測試並驗證客戶端加密數據是否可以由服務器解密,反之亦然。爲此,客戶端將使用僞隨機函數(PRF)計算12字節的verify_data,如下所示。

verify_data = PRF(master_secret, "client finished", MD5(handshake_messages) + SHA-1(handshake_messages) ) [12]

  其中handshake_messages 是所有握手消息的緩衝區,以上版本適用於版本1.2的TLS。版本1.2略有變化,即verify_data的長度取決於密碼套件而不總是12字節,任何未明確指定verify_data_length的密碼套件都等於12。此外,僞隨機函數(PRF)中的MD5 / SHA-1組合具有已被密碼套件指定的PRF替換。所以根據最新規範,

Verify_data = PRF(master_secret, finished_label, Hash(handshake_messages)) [0..verify_data_length-1];

  因此我們有測試數據,用密鑰和算法來加密測試數據。客戶端所需要做的就是用客戶端加密密鑰(或簡稱客戶端寫入密鑰)使用AES算法加密測試數據,如上所述還得計算HMAC,客戶端獲取結果並添加記錄頭字節“0x14”表明“已完成”,再通過客戶端生成消息並且發送到服務器。這是由實體和客戶端發送的最後一次握手消息之間協商的算法和密鑰保護的第一條消息。由於消息是完全加密的,因此WireShark只會看到加密的內容,並通過名稱爲加密握手的消息來調用完成的握手信息,如下所示。

驗證磋商

  服務器處理過程也幾乎相同。它發出一個Change Cipher Spec ,然後發送一條包含所有握手消息的已完成信息。更改標記在該服務器切換到新協商的加密套件和鍵點的密碼SPEC消息,然後再加密後續客戶端的記錄。除此之外,服務器的完成消息將包含對客戶端的完成消息進行解密的版本,一旦客戶端收到此數據,它將使用服務器寫入密鑰對其進行解密。故而這就向客戶證明了服務器能夠成功解密我們的消息。KABOOM!我們完成了TLS握手。

  所有的加密都是基於協商的算法。在我們的例子中,算法是AES_128_GCM,這裏沒有必要進行進一步的解釋,因爲當涉及到其他網站時,服務器指定的算法可能會有所不同。如果您有興趣瞭解這些算法的工作原理,維基百科有一個列表。我也是通過TLS基礎知識來學習密碼學。

加密應用程序數據

  我們現在在應用層。如果你有一箇中等速度的互聯網連接,我們只是連接了幾百毫秒。想象一下,在如此短的時間內會發生多少事情?

我要求的頁面是homepade aka www.github.com

。所以在Mozilla Firefox的開發者工具中顯示的純文本請求是,

GET https://github.com/

Host: github.com

User-Agent: Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:61.0) Gecko/20100101 Firefox/61.0

Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8

Accept-Language: en-US,en;q=0.5

Accept-Encoding: gzip, deflate, br

Connection: keep-alive

Upgrade-Insecure-Requests: 1

Cache-Control: max-age=0

請參閱下圖:

前3個字節17 03 03 表示內容的數據類型(應用程序數據)和TLS版本(TLS 1.2)。

尾聲

對,就是這樣。我們結束了。

  在本系列的下一部分中,我會添加一些在本文中無法包含的別的內容。我還發布了結構化的參考鏈接,這對於學習TLS中的密碼學還是很有用的。
  我想在這裏再寫點什麼。整篇文章寫的都是我對TLS的理解上的興趣,就是說我所學到/理解的一切都在這裏了,或許並不完整,或許會有錯誤,或許各位的看法和我有出入。總之,不管是什麼,歡迎您在評論區中分享,我很高興能和諸位一起學習更多的東西!

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章