今天我抓了個 HTTPS 的包
之前寫過一篇講 HTTPS 的思想的文章。
后來又寫了篇用更凝練的語言總體描述了 HTTPS 的主干。
想必通過這兩篇文章,HTTPS 為什么要這么設計,以及它是用來解決什么問題的,大家已經心中有數了。
那接下來就是細節了。
由於之前已經把思想講過了,本篇就通過抓包的方式,專注於 HTTPS 的過程,我會像個無情的流水賬一樣,給大家把一個 HTTPS 的包挖干凈。
先普及一個知識點,HTTPS = TLS + HTTP。
HTTP 我們很熟悉了,所以我們想知道的 HTTPS 的知識,本質上是想知道 TLS 協議的規范,以及為什么這樣設計。所以我們本文展開講解 TLSv1.2 協議的內容。
我們開始吧!
直接 postman 發起一個 HTTPS 的 POST 請求,這個 IP 是微博首頁。

wireshark 抓包,過濾出 tls 協議的包,看到如下結果。

一下就可以看到整個 HTTPS 握手的過程了。
這個抓包數據可以加我好友,朋友圈有下載鏈接。但其實你自己隨便訪問一個網站用 wireshark 抓一下也行。
學一個協議,最科學也是最方便的辦法,就是看官方文檔。
我們看 TLS 1.2 的官方文檔,RFC-5246,其中 section-7.3 為我們描繪了整個握手過程。

由於我們只是客戶端驗證服務端證書,而沒有服務端驗證客戶端證書的過程,所以我們的包里,是不包含 Server CertificateRequest(請求客戶端證書)、Client Certificate(客戶端證書)、CertificateVerify(客戶端證書有效性驗證)這三項的。
下面我們一個一個過程拆開來看。
ClientHello
當 client 連接到一個 server 時,第一個發送的包就是它。

具體包含以下內容:
client_version
客戶端希望的,也是客戶端能支持的最高的 TLS 版本號,這里是 TLS 1.2 版本。
random
由客戶端生成的隨機數,之后會用到,我們稱之為隨機數 1。
ee8880e816ac14ca5b69bde656c188f37a08bcf2052a550b7867b041f6c1ab48
session_id
用於復用 TLS 連接,防止資源的浪費。但這個要服務端支持才行。
cipher_suites
客戶端支持的密碼學套件,按客戶端偏好排序,如果服務端沒有可支持的,那就回應錯誤(returns a handshake failure alert)並關閉連接。
本次一共發了 18 個密碼學套件

我們拿其中一個密碼學套件舉例
TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256
我們知道,HTTPS 的原理就是用非對稱加密的方式來交換秘鑰,用對稱加密的方式來通信,然后里面夾雜着哈希算法用於驗證簽名等。
所以這個密碼學套件就包含了這三個部分。
ECDHE_RSA 指的是秘鑰協商算法AES_128_GCM 是最終通信的對稱加密算法SHA256 是哈希算法
以此來確定整個握手過程所需要的算法都用什么。
compression_methods
壓縮算法
extensions
擴展字段
由此看出,本次 clientHello 最重要的信息就倆,一個隨機數,一組支持的密碼學套件。
接着往下看
ServerHello
服務端給客戶端發送的包,響應 ClientHello。

具體包含以下內容:
server_version
TLS 的版本,具體是服務端支持的最高版本以及客戶端支持的最低版本。
random
服務端生成的隨機數,且生成規則不能依賴於客戶端的隨機數,我們稱為隨機數 2。
3ad03af5b8a5ebfe7902a250406b2e99d2667e37e524e0e5c333c0e0b9a637e8
session_id
服務端返回的會話 ID。
如果客戶端剛剛發過來的 session_id 服務端已經有了緩存,並且同意復用連接,則返回一個和客戶端剛剛發來的相同的 session_id。
也可以發送一個新的 session_id,以便客戶端下次將其攜帶並且復用。
也可以回復一個空值,表示不緩存 session_id,因此也不會復用。
cipher_suite
選擇的加密套件。
剛剛客戶端傳來 18 個加密套件,服務端選擇了一個回應,此處回應的是。
0xc02f
表示
TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256
這個剛剛解釋過了,表示用 ECDHE_RSA 作為秘鑰交換算法,用 AES_128 作為通信時的對稱加密算法,用 SHA256 作為哈希算法。
compression_method
選擇的壓縮算法,同上
extensions
擴展字段,同上
由此看出,本次 serverHello 最重要的信息也是倆,和上面的 clientHello 一樣,也是一個隨機數,還有一個從客戶端發來的一組密碼學套件中選擇的一個。
至此,服務端和客戶端都擁有了隨機數 1 和隨機數 2,並且選定了共同確定的密碼學套件。
雙方互相 hello 的過程就此結束了。
接着往下看
Server Certificate
服務端發完上面的 ServerHello 后立即發這個包,這個包非常簡單。

只有一個 Certificates 結構體,就是我們常說的證書。
而后綴還加了個 s,因此翻譯成證書鏈,是由一組證書組成的,最上面的是服務端本身的證書。
我們把這個服務端的證書的關鍵信息都展開看一下:

這個證書在默認情況下都是 X.509 格式的,除非明確協商說明。
別懷疑,RFC-5246 中就是這樣寫的。

這種格式的結構定義在 RFC-1422 的 3.3 節中有說明。

別廢話了,展開講一下吧。
version
證書版本號,v3
serial number
證書序列號,這個每個頒發機構是唯一的,此處為:
0x0de81066db219caef5ecb01ba273cad1
signature
簽名算法,僅僅是一個算法喲。
此處是 1.2.840.113549.1.1.11
表示 sha256WithRSAEncryption
這就表示用 sha256 這個哈希算法對證書進行哈希生成摘要,然后再用 RSA 這個非對稱加密算法,用 CA 的私鑰加密剛剛生成的摘要,形成數字簽名。
issuer name
頒發者信息,我們展開看一下
RDNSequence item: 1 item (id-at-countryName=US) RDNSequence item: 1 item (id-at-organizationName=DigiCert Inc) RDNSequence item: 1 item (id-at-organizationalUnitName=www.digicert.com) RDNSequence item: 1 item (id-at-commonName=GeoTrust CN RSA CA G1)
validity period
證書有效期,比如本案例中的
notBefore: utcTime (0) utcTime: 20-06-09 00:00:00 (UTC) notAfter: utcTime (0) utcTime: 22-05-15 12:00:00 (UTC)
subject name
證書持有者信息
rdnSequence: 5 items RDNSequence item: 1 item (id-at-countryName=CN) RelativeDistinguishedName item (id-at-countryName=CN) Id: 2.5.4.6 (id-at-countryName) CountryName: CN RDNSequence item: 1 item (id-at-stateOrProvinceName=Beijing) RelativeDistinguishedName item (id-at-stateOrProvinceName=Beijing) Id: 2.5.4.8 (id-at-stateOrProvinceName) DirectoryString: printableString (1) RDNSequence item: 1 item (id-at-organizationName=Sina.com Technology(China)Co.,ltd) RelativeDistinguishedName item (id-at-organizationName=Sina.com Technology(China)Co.,ltd) Id: 2.5.4.10 (id-at-organizationName) DirectoryString: printableString (1) RDNSequence item: 1 item (id-at-organizationalUnitName=Sina.com Technology(China)Co.,ltd) RelativeDistinguishedName item (id-at-organizationalUnitName=Sina.com Technology(China)Co.,ltd) Id: 2.5.4.11 (id-at-organizationalUnitName) DirectoryString: printableString (1) RDNSequence item: 1 item (id-at-commonName=weibo.cn) RelativeDistinguishedName item (id-at-commonName=weibo.cn) Id: 2.5.4.3 (id-at-commonName) DirectoryString: printableString (1) printableString: weibo.cn
太亂了,簡化下就是
countryName=CN
stateOrProvinceName=Beijing
organizationName=Sina.com Technology(China)Co.,ltd
organizationalUnitName=Sina.com Technology(China)Co.,ltd
commonName=weibo.cn
顧名思義,就是微博網站這個頒發機構的信息
subject public key
證書的公鑰信息,本案例中是:
3082010a0282010100c4c84ff479214c5875037500cfc453d676cec0e64c7ab5f14e0284d8b49b6f23ec70f853d38eb60dc91a6fa826d49d188fd20158c3aaa101b4b6a0c89d4df824fe755ff2cfd4f876bb2dcefe760d6f9ec5e9e2990cab4367949f27062857ca26f2303f07f6c6c953f382cb8a379ae7b28c6234983fd61739550dc6b502c4feb9c9991459265f61471b91e3b592ad3e21a276d14321f462c820477e2b34a7ea16da1f3ffa760d9065ceb5a98ffc3d19da519133d542f74dd70c4366d98d16c36c27e4384cf31130614a1398621c64c260ad91d0de32900e2ac2589029b35d21eacd078bea5cb0a9db4bc3b7ba644d0459c2e0489ae62215cc525c36784191d94b0203010001
除此之外,還剩下兩項,證書簽名算法(誒?這個剛剛不是已經傳過了么),以及證書的簽名值

從中可以讀出,簽名算法就是 sha256WithRSAEncryption,簽名值我提取出來,如下:
220f0a0d15fa3c3909bb1e9f4d1d78cb9a41983cc9e549a4f8781f483fc18c679421eb84875354e00d86877eb1e80fb691eb0133208a0bee641ca0a7585b0e85818e88557a50f3f6241eebbc9cf49be40dc21f1d82a0cf30de30643cf236b290e74b6dee9bfdb71dab5f03b5cfd965bc16e139bc66f37119fcfc73aaf4c50fda1111bd948f507f85dd239012be73c953234328e332091c2fa38c482b6b4fdba52a26a1cc557a9c95edacea1d7b62f8996c934a5b3d762dd4f3cb88d405b805b7f604c07bd518665940f34fcb9e54121ac724a1ea3a58f42c9556f25058b19afa8c233fdf881bfeff32186051ec104fa23d4024b16b672f8eb33e359c3f813aa1
至於它是怎么算出來的,之前也畫過一張圖,我就直接放過來了(注意,這里的給服務端,是指 CA 機構給服務端,然后服務端現在又給了客戶端)。

還記得剛剛的簽名算法么?sha256WithRSAEncryption
這個圖里的哈希摘要用的算法就是 sha256,而 CA 私鑰加密用的算法就是 RSA。
好了,全部證書相關的信息就講完了,同時也是 Server Certificate 這個環節的唯一信息。
證書一方面可以通過服務端給客戶端傳遞的包解析來看,另一方面,由於瀏覽器要解析這個證書信息做驗證,所以通常瀏覽器有更直觀的方式可以查看,就不用我們費心思了。
點開瀏覽器地址欄的小鎖頭。

看,和我們剛剛抓包分析出來的信息,一毛一樣。
我們繼續看下一個包。
我相信你已經不記得整個流程到哪里了,好心的我給你放之前的圖。

剛剛進行完兩個 hello,以及一個傳遞證書的包 Certificate,接下來就要進行協商對稱秘鑰的過程了。
這個過程,最簡單就是 RSA 算法,用服務端公鑰直接加密客戶端隨機生成的一個對稱加密秘鑰,發給服務端。
但現在基本上都用更為復雜的秘鑰交換算法,我們往下看。
Server Key Exchange
用於 premaster secret 生成的

之前說了,秘鑰交換算法是 ECDHE 算法,這里隱含着包含了好多信息。
首先選擇的橢圓曲線是 named_curve 類型,並指定了基點生成一個私鑰,這個我們抓包看不見根據私鑰和基點,計算出公鑰,然后把這個公鑰用服務端公鑰加密,發送給服務端,這個我們能看到,就是里面的 Pubkey ,值為
2ce174dbdb6f481b6ab9fd37446dca95b6ade3613afba03243d163360f63713b
至於 ECDHE 用到的橢圓曲線秘鑰交換算法的細節,這里就不展開講了,因為我也不會,就知道它最終是為了和服務端協商出來一個 premaster_secret 就好。
接着往下看。
Server CertificateRequest
請求客戶端證書,此案例中沒有,一般銀行等需要客戶端也加密的才有,比如 U 盾。
Server ServerHelloDone
標識着 serverHello 這個握手過程結束了。

Client Certificate
客戶端證書。本案例中沒有,也說明了上面服務端確實沒有發送 CertificateRequest
Client ClientKeyExchange
緊接着 ServerHelloDone 發送,用於協商出 premaster_secret,同之前的 ServerKeyExchange 配合使用的。

這回輪到客戶端給服務端一個用於 ECDHE 算法的公鑰了。
f04e0743377afb5e9bf0a84aec5c7257957b85daee98fc48fb8971a26b457077
而同時客戶端與這個公鑰配對的私鑰,我們也無法通過抓包看出來。
生成最終通信的對稱加密秘鑰
master_secret
這一步不是抓包的信息,而是客戶端和服務端此時都在自己端內所做的事情,非常關鍵。
就是計算出最終對稱加密用的秘鑰 master_secret,這也是整個花里胡哨的過程,最終且唯一的一個目的,並且兩端算出來的結果肯定是一樣的。
HTTPS 的目的,不就是雙方協商出一個共同的對稱加密秘鑰么,怕被中間人攔截到,所以做的證書呀,非對稱加密算法呀,秘鑰協商算法等復雜的規定。
那 master_secret 是怎么計算出來的呢?
還記不記得之前我們得到了三個隨機數:
隨機數 1(客戶端隨機數) :在 ClientHello 消息里,由客戶端生成的隨機數,是 ee8880e816ac14ca5b69bde656c188f37a08bcf2052a550b7867b041f6c1ab48隨機數 2(服務端隨機數) :在 ServerHello 消息里,由服務端生成的隨機數,是3ad03af5b8a5ebfe7902a250406b2e99d2667e37e524e0e5c333c0e0b9a637e8隨機數 3(pre_master) :通過秘鑰交換算法 ECDHE 計算出的,我們叫它 pre_master。
最終的對稱加密秘鑰 master_secret,就是根據這三個隨機數共同計算出來的。
一旦雙方協商出來了這個相同的對稱秘鑰,那就可以開始愉快地安全通信了,TLS 層的工作也就圓滿完成。
所以可想而知,接下來的工作,就都是收尾工作了,因為秘鑰已經協商好了。
Client CertificateVerify
驗證客戶端證書有效性,本案例中沒有。
Client ChangeCipherSpec
秘鑰改變通知,此時客戶端已經生成了 master_secret,之后的消息將都通過 master secret 來加密。

可以看到,就是個標識,沒有攜帶什么有用的信息。
Client Finish
這一步對應的是 Client Finish 消息,客戶端將前面的握手消息生成摘要再用協商好的秘鑰加密,這是客戶端發出的第一條加密消息。服務端接收后會用秘鑰解密,能解出來說明前面協商出來的秘鑰是一致的。
Server ChangeCipherSpec
也是秘鑰改變通知,此時服務端也已經生成了 master_secret 了,后面的通信都用此值加密。
Server Finish
同 Client Finish,服務器端發送握手結束通知,同時會帶上前面所發內容的簽名到客戶端,保證前面通信數據的正確性。
Application Data
之后就是真正加密的數據了。
總結
我們去掉客戶端證書這個部分,整個過程簡化來說一遍。1. client --> server ClientHello客戶端生成隨機數,並發送一組密碼學套件供服務端選
2. server--> client ServerHello服務端生成隨機數,並從上述密碼學套件組里選一個3. server--> client Certificate服務端發給客戶端證書
4. server--> client ServerKeyExchange服務端發給客戶端秘鑰交換算法所需的值
5. server--> client ServerHelloDone服務端 hello 階段結束
6. client --> server ClientKeyExchange客戶端發給服務端秘鑰交換算法所需的值7. client --> server ChangeCipherSpec客戶端告訴服務端,我已經知道秘鑰了,之后的消息我就都加密發送了。
8. client --> server Finish結束並驗證
7. server --> server ChangeCipherSpec服務端告訴客戶端,我已經知道秘鑰了,之后的消息我就都加密發送了。9. server--> client Finish
結束並驗證
如果不看客戶端證書,不看復雜的 premaster_secret 協商算法,不看壓縮算法這些細節,其實簡單說只有三大步驟。
首先第一步,客戶端對服務端說 hello,並且發一組密碼套件。第二步,服務端對客戶端說 hello,並且選擇一組密碼套件,同時把附帶公鑰的證書發給客戶端。第三步,客戶端驗證證書,並且把對稱加密的秘鑰用服務端的公鑰加密,發給服務端,服務端用私鑰解密出就是對稱加密秘鑰了。(當然實際情況沒這么簡單,比如我們本次抓包是用 ECDHE 秘鑰交換算法,這也是目前大部分網站的做法)。經此三步之后,客戶端與服務端就都擁有了相同的對稱加密秘鑰,進行簡單的收尾工作,也就是通知對方秘鑰已生成好的信息,之后就可以開始通信了。最后說一句,本案例的抓包數據可以加我好友,朋友圈有下載鏈接,當然你也可以自己用 wireshark 抓,隨便訪問一個網站幾乎都是 https 的。
參考資料:
RFC-5246 The Transport Layer Security (TLS) Protocol Version 1.2
RFC-1422 Privacy Enhancement for Internet Electronic Mail Part II: Certificate-Based Key Management
公眾號 - 低並發編程
