SSL及GMVPN握手協議詳解
之前寫過一篇文章搞懂密碼學基礎及SSL/TLS協議,主要介紹了加密學的基礎,並從整體上對SSL協議做了介紹。由於篇幅原因,SSL握手的詳細流程沒有深入介紹。本文將拆解握手流程,在訊息級別對握手進行詳細地介紹。還沒有加密學基本概念的、或者不清楚SSL協議的基本情況的建議先看一下前面一篇的內容。另外,本文主要是針對TLSv1.2和GMVPN的情況,對於TLSv1.3暫不涉及。
本部落格已遷移至CatBro's Blog,那是我自己搭建的個人部落格,歡迎關注。本文連結
概覽
本文將分如下幾個部分展開,其中金鑰交換和身份認證部分會著重進行講解。
我們都知道SSL協議最主要的作用就是用來協商通訊雙方的安全引數,然後基於協商的安全引數進行安全通訊。所以首當其衝地,第一個要解決的問題就是如何進行金鑰交換。考慮到對稱密碼和非對稱密碼的效能差異顯著,在實際使用中往往使用對稱密碼對資料進行加密,而使用非對稱密碼來完成重要的金鑰交換和身份認證。
基本握手流程
下圖是一個我們通常看到的SSL握手流程圖,SSL協議分為兩個階段:握手階段和應用階段。握手階段主要負責協商安全引數,應用階段則基於前面協商的安全引數進行資料通訊。
這裡先不對每個訊息進行詳細的介紹,後面的內容中會陸續涉及到。其中帶*號的表示可選訊息或者根據前面訊息的情況而定。ChangeCipherSpec作為單獨的一類訊息只是表明握手協議已經完成,後續使用協商的加密引數進行通訊。Finished訊息就是第一個加密的訊息,用於驗證握手已經順利完成。這裡稍微提一下ServerHelloDone這個訊息,這個訊息並沒有什麼實際的內容,它存在的主要原因就是因為前面的訊息CertificateRequest是可選的,所以需要明確地告訴客戶端服務端這邊的訊息發完了,否則客戶端無從知道是否該等待CertificateRequest訊息。
如何進行金鑰交換
金鑰交換的方法可以分成兩大類:一類是基於加密、一類是基於DH。前者有RSA演算法、GM的ECC演算法;後者則有ECDHE、GM的ECDHE。
雖然前面一篇已經講過了,這裡還是稍微帶一下這兩者的基本原理,以方便在後面實際的握手流程中進行對照。
金鑰交換原理
公鑰加密的基本原理如下:
公鑰密碼有兩個金鑰,其中一個是公開金鑰,公開金鑰可以隨意傳播,另一個是私有金鑰,需要自己嚴密保管。比如Bob要發訊息給Alice,Bob用Alice的公鑰對訊息進行加密,然後傳送給Alice,Alice則用自己的私鑰進行解密。
DH類演算法的原理可以用下圖來形象地解釋:
首先雙方協商一個相同的底色(演算法引數),然後各自生成自己私有的顏色(相當於私鑰),並通過混合得到對應的公有顏色(相當於公鑰)。隨後雙方交換各自的公有顏色,並與自己的私鑰顏色混合,最終協商出一個相同的顏色(即交換的金鑰)。竊聽者就算得到了雙方交換的這些資訊,也無法生成相同的金鑰,求解離散物件問題的困難度
TLS RSA金鑰交換
這種情況的典型演算法套件是AES256-SHA。(相關的訊息都已經標成藍色了,連到訊息塊上的實線箭頭表示該訊息中帶了相應的內容。)
這種情況比較簡單,首先客戶端生成一個隨機數在ClientHello訊息中帶過去,服務端同樣也生成一個隨機數在ServerHello訊息中帶過去。然後服務端在Certificate訊息中將它的證書傳送給客戶端,證書中包含了它的公鑰。客戶端收到之後就用服務端的公鑰加密一個隨機生成的預主金鑰,通過ClientKeyExchange訊息傳送給服務端,服務端則用自己的私鑰進行解密得到預主金鑰。這裡就是應用了前面提到的公鑰加密的原理。
通過金鑰交換之後,雙方都得到了預主金鑰,再加上前面交換的兩個隨機數,這3個材料一起進行金鑰派生得到主金鑰。主金鑰再結合兩個隨機數派生出最終的會話金鑰。
這裡先丟擲幾個問題供大家思考。為什麼不直接用預主金鑰,而是要跟兩個隨機數派生出主金鑰?為什麼不直接用主金鑰,而是再跟兩個隨機數派生出會話金鑰?我暫時不做解答,留到後面分解。
TLS ECDHE金鑰交換
這個情況的典型演算法套件如ECDHE-RSA-AES256-GCM-SHA384、ECDHE-ECDSA-AES256-GCM-SHA384。這裡的EC表示橢圓曲線,DH表示基於DH演算法,最後一個E則表示使用臨時金鑰進行金鑰交換,而不是證書相關的非臨時金鑰。
我們來比較下跟前一種情況的區別,首先ClientHello和ServerHello訊息是一樣的,都帶了一個隨機數。區別在於服務端是通過ServerKeyExchange訊息傳送了一個臨時DH引數給對方(也就是前面將DH原理時的公有顏色),類似地客戶端也通過ClientKeyExchange訊息把它的臨時DH引數傳送給服務端。這樣雙方就交換了彼此的臨時DH公鑰,然後他們各自利用自己的臨時私鑰和對方的臨時公鑰計算出預主金鑰。與前一種情況的最大區別就在於此,前者是客戶端加密預主金鑰傳送給服務端,後者是雙方交換臨時DH公鑰,然後各自計算出預主金鑰。
得到預主金鑰後,後續的流程就一樣了。結合兩個隨機數派生出主金鑰,然後再派生出會話金鑰。
GM ECC金鑰交換
接下來我們看GM ECC金鑰交換的情況,這種情況的典型演算法套件為ECC-SM4-SM3。GMVPN協議一個最大的區別就是它引入了雙證書體系,每一方都有兩個證書(對應地就有兩個私鑰),一個加密證書負責進行金鑰交換,一個簽名證書負責進行身份認證。
跟TLS RSA金鑰交換的主要區別是,這種情況的證書訊息中包含了雙證書,客戶端在加密預主金鑰時是用服務端加密證書中的公鑰。相應地,服務端在解密時則用自己的加密私鑰。後續的金鑰派生流程都是一樣的,這裡就不再贅述了。
至於GM為什麼要引入雙證書,是因為這樣你的加密私鑰就在CA那裡有留檔,必要時它就可以解密你的訊息。Big brother is watching you!
GM ECDHE金鑰交換
這種情況的典型演算法套件為ECDHE-SM4-SM3。同樣是雙證書,ServerKeyExchange訊息中帶了服務端的臨時DH引數,客戶端也將雙證書以及它的臨時DH引數傳送給服務端。注意到在計算預主金鑰時,有4個材料參與了運算,對方加密證書中的公鑰以及臨時公鑰,自己的加密私鑰以及臨時私鑰,由這4個材料一起計算出預主金鑰。作為對比,普通TLS的ECDHE只有自己的臨時私鑰和對方的臨時公鑰參與計算。
GM ECDHE最大的區別就在於此,所以GM ECDHE的演算法套件必須是雙向認證的,因為在金鑰交換時也需要客戶端的加密證書參與。從這裡也可以看出GM協議在設計上的不嚴謹,哪有演算法套件只允許雙向認證的。而且GM ECDHE因為也有臨時金鑰參與計算預主金鑰,所以就算CA有加密私鑰也是無法解密的,這又與雙證書的最初目的相悖。。。
TLS1.2 金鑰派生流程
金鑰派生的流程前面其實已經畫出來了,基於加密或者DH雙方交換得到預主金鑰,預主金鑰結合兩個隨機數派生得到主金鑰,TLS1.2中派生是通過PRF進行的,其中secret就是預主金鑰,label是一個特定的字串,seed是前面的兩個隨機數。
master_secret = PRF(pre_master_secret, "master secret",
ClientHello.random + ServerHello.random)
[0..47];
得到主金鑰之後在結合兩個隨機數派生出會話金鑰,同樣使用PRF,不過此時secret是主金鑰,label字串不同,seed還是兩個隨機數。
key_block = PRF(SecurityParameters.master_secret,
"key expansion",
SecurityParameters.server_random +
SecurityParameters.client_random);
得到會話金鑰之後,再按照需要切分成MAC key和對稱加密的key。圖中把IV畫成了虛線,因為TLS1.2中一般不需要這個。因為自從TLS1.1改成顯式IV之後(為了防止CBC明文選擇攻擊),iv都是在記錄中顯式帶過去的,只有當使用AEAD演算法,需要隱式nonce時還需需要這個。
至於前面丟擲的兩個問題,為什麼不直接用預主金鑰?一方面是為了統一長度,因為基於加密的金鑰交換預主金鑰時48位元組,而基於DH的金鑰交換預主金鑰長度取決於具體的演算法。更重要的一點,雙方的隨機數加入計算,可以防重放攻擊,還可以增加隨機數的熵源,增加安全性。
那麼為什麼不直接用主金鑰,還要再次派生出會話金鑰呢?這個主要是生命週期的考慮,兩者有不同生命週期,主金鑰的生命週期較長,會話金鑰則較短,只在當前會話有效。後面在講session重用時就可以看出其區別。
如何進行身份認證
前面討論瞭如何進行金鑰交換,但是金鑰交換隻管協商出金鑰,並不考慮對方是誰?你如何確認對方的身份,防止被中間人攻擊呢?所以就引入了身份認證。
身份認證需要解決兩個問題:
- 確認對方擁有公鑰對應的私鑰
- 確認對方的身份
第一個問題,使用單純的數字簽名就可以解決。通過驗證對方的數字簽名,可以確認對方擁有相應的私鑰。
而第二個問題,則需要引入數字證書和PKI了。數字證書,說白了就是將一個公鑰跟身份資訊進行繫結,然後由第三方可信機構(CA)給你做個證明(通過CA簽名)。那麼CA本身的身份又由誰來證明呢,再通過更上級的CA進行證明。最終的根CA只能通過其他手段進行認證,否則就無限迴圈了。
數字簽名原理
數字簽名的基本原理也很簡單,與前面的公鑰加密相對,這裡是使用私鑰對資料進行簽名,對方則用公鑰對簽名資料進行驗籤。
TLS RSA金鑰交換時的RSA認證
典型演算法套件仍然是AES256-SHA。
服務端的認證只涉及Certificate訊息,服務端把證書和CA證書鏈傳送給客戶端,客戶端只需要對證書和證書鏈進行驗證。最終的Root CA需要是本機信任的,否則就存在安全風險,可能受到中間人攻擊。
如果需要雙向認證,即服務端也需要認證客戶端,還涉及圖中橙色的3個訊息。服務端會發送CertificateRequest訊息,告訴客戶端支援的證書型別、簽名雜湊演算法、以及CA的DN項。客戶端根據這些資訊選擇適合的客戶端證書,然後在Certificate訊息中將證書和CA證書鏈傳送給服務端,另外還需要用自己的私鑰做一個簽名,以證明自己擁有證書對應的私鑰。這裡簽名的內容是前面所有的握手訊息(從ClientHello開始到當前訊息之前的所有訊息)。然後服務端對客戶端的證書鏈以及簽名進行驗證。
TLS ECDHE金鑰交換時的認證
典型演算法套件如ECDHE-RSA-AES256-GCM-SHA384、ECDHE-ECDSA-AES256-GCM-SHA384。
跟前一種情況下比,服務端多了一個簽名。因為這種情況金鑰交換是通過臨時DH引數完成的,並沒有服務端證書對應的私鑰參與,所以需要用證書對應的私鑰額外做一個簽名,以證明自己確實擁有證書對應私鑰。這裡簽名的內容是兩個hello隨機數以及服務端的臨時DH引數,其實就是參與計算預主金鑰的三個材料。
客戶端除了驗證證書鏈之外,還需要對這個簽名進行驗證。
客戶端認證的部分跟前一種情況完全一樣,不再進行贅述。
GM ECC金鑰交換時的認證
典型演算法套件還是ECC-SM4-SM3。
前面TLS RSA的情況,服務端不需要做一個額外的簽名。因為金鑰交換和身份認證都是通過同一個證書來進行的,服務端能夠完成金鑰交換(解密出預主金鑰)就已經說明了它有對應的私鑰。
而GM ECC則則不然,因為雙證書的關係,金鑰交換和身份認證是通過不同證書進行的。所以服務端仍然需要做一個簽名證明其私鑰持有性。簽名是通過簽名私鑰來進行的,簽名的內容有所改變,是兩個hello隨機數和服務端的加密證書。客戶端除了驗證證書鏈,還需要用服務端的簽名證書對簽名進行驗證。
客戶端認證的流程還是類似,只不過傳送的也是雙證書,然後在簽名時也是用的簽名私鑰。服務端除了驗證證書鏈,還需要用客戶端的簽名證書對簽名進行驗證。
GM ECDHE金鑰交換時的認證
典型演算法套件還是ECDHE-SM4-SM3。
跟前一種情況相比,主要是服務端的簽名內容有一點區別,是兩個hello隨機數+服務端的臨時DH引數。我們注意到前面幾種情況,服務端簽名的內容都是參與計算預主金鑰的材料。從這個邏輯上來說,這邊在協議設計上也有些問題,因為服務端的加密證書實際上也參與了金鑰交換,按照協議設計一致性也應該包含到簽名內容中。
其他部分跟前一種情況完全一樣,不再進行贅述。
如何協商協議演算法
前面討論了幾種不同的情況,可以看到不同的協議版本、不同的演算法套件,它們在握手的處理流程上是不一樣的,那麼雙方如何對此達成一致呢?
於是就需要引入一次Hello互動了。這也是為什麼完整的SSL握手需要兩次互動的主要原因。通過這次Hello互動對使用的協議版本和演算法套件達成一致。另外得益於SSL協議引入的擴充套件機制,不僅僅是協議版本演算法套件,雙方還可以協商除此之外的很多東西,甚至是使用者自定義的擴充套件項。
不過最基本的還是協議版本、演算法套件之類的。客戶端傳送它支援的版本、演算法套件列表、如果是橢圓曲線還會指定支援的曲線列表、簽名演算法等,服務端基於客戶端的資訊,選擇最終使用的協議版本、演算法套件、EC點格式等。
如何提升效能
完整的SSL握手需要兩個RTT,而且還需要耗時的非對稱運算。協議在設計上也考慮瞭如何提升效能的問題。在握手流程優化方面,主要就是通過會話重用來簡化握手流程。(對於TLSv1.3則有PSK、1-RTT和0-RTT,不過本文將不涉及TLS1.3,)這裡主要介紹下會話重用的情況。
會話重用基本流程
會話重用的基本流程如上所示。首先是一個完整的握手,然後客戶端如果想重用前面的會話,在ClientHello進行相應的指示告訴服務端,服務端如果同意在ServerHello中進行答覆,然後就直接進行簡化的握手,不需要再進行金鑰交換和身份認證。不但省了一次互動,也省去了費時的非對稱運算。
Session ID
會話重用有兩種方式,首先來看下Session ID的流程:
前一次握手中,服務端在ServerHello訊息中將Session ID告訴客戶端,握手正常完成之後,服務端會將對應的Session儲存,其中就包含了主金鑰。
後續客戶端想要重用之前那個Session,可以在ClientHello中帶上之前的Session ID,服務端收到之後會根據Session ID進行查詢,如果找到了且未過期,那麼進行會話重用,服務端將Session ID再原樣傳送給客戶端,進入簡化的握手流程;否則,服務端還是隨機生成新的Session ID傳送給客戶端,回退到完整的握手流程。
會話重用時,使用之前Session的主金鑰來推導會話金鑰。這裡就可以看出其生命週期的不同了,會話重用時主金鑰是用的同一個,但是會話金鑰每次都是重新生成的。而且注意到,這時也是有兩個隨機數參與金鑰派生的,同樣也是出於防重放的考慮。
Session Ticket
前一種情況服務端需要儲存 session cache,會消耗記憶體資源,如果是叢集的話還會帶來cache同步的問題。而session ticket的出現正是為了解決這些問題。
我們來看下它的流程,首先客戶端在ClientHello的擴充套件中帶上session_ticket擴充套件表示它想使用session_ticket功能,服務端如果同意則在ServerHello中回覆session_ticket擴充套件項。服務端不用儲存session,而是加密之後通過NewSessionTicket訊息傳送給客戶端。這裡session ticket key實際上是個對稱金鑰,它只有服務端自己知道。
後續客戶端想要重用該Session,在ClientHello擴充套件中把之前那個session_ticket塞進去,服務端成功解密且驗證通過之後就進行會話重用,否則回退到完整的握手。然後還是同樣地根據主金鑰重新派生出會話金鑰。
這樣服務端沒有了儲存session的負擔,但是天下沒有免費的午餐,session ticket對前向安全性會帶來一定的損害。因為session ticket只是單純使用session ticket key進行加密的,如果session ticket key洩漏了,那麼之前基於會話重用的握手就都可以被破解了。
所以在實際使用時,session ticket key應該經常更換,減小前向安全性方面的風險。
如何保證安全性
簡單回顧來握手中是如何保證安全性的。
首先通過兩個hello隨機數來實現防重放。防止中間人攻擊是通過對服務端的認證了完成的。防篡改是通過最後的Finished來完成的,Finished訊息中包含了一個verify_data,它也是由PRF計算得到的,其中的seed是前面所有握手訊息(ClientHello開始,到當前Finished訊息前)的摘要值。
verify_data:
PRF(master_secret, finished_label,
Hash(handshake_messages))
[0..verify_data_length-1];
前向安全性也是需要特別注意的點,事實上TLS1.3中砍掉了所有沒有前向安全性的演算法套件,只留下了DHE或ECDHE的演算法套件。另外前面也提到了session ticket也會對前向安全性有一定的損害。
最後,要特別強調下隨機數的重要性。一個好的加密演算法,其安全性完全是基於金鑰的安全性,如果隨機數本身質量不過關,比如可以被預測,那麼前面所有的一切都是白忙活。隨機數可以說是所有這些的基石。
最後的最後,安全領域其實也適用木桶理論,一個通訊的安全性取決於其最薄弱的環節。無論是協議設計、程式碼實現還是在使用者使用上,任何一處紕漏都可能導致巨大的安全問題。