深入淺出 SSL/TLS 協(xié)議
有沒有那么一個人,幾乎每天都在你身邊,但某天發(fā)生一些事情后你會突然發(fā)現(xiàn),自己完全不了解對方。對于筆者而言,這個人就是 TLS,雖然每天都會用到,卻并不十分清楚其中的貓膩。因此在碰壁多次后,終于決定認真學習一下 TLS,同時還是奉行 Learning by Teaching 的原則,因此也就有了這篇稍顯啰嗦的文章。
前言
關于 SSL/TLS 和 HTTPS 這些基本概念也就不多廢話了,相信看筆者文章的朋友都有一定基礎,因此本文的重點主要放在其中的握手流程。下文分別以目前最為常用的 TLS1.2 和 TLS1.3 協(xié)議分別進行分析,涉及到對稱加密和橢圓曲線加密的相關介紹可以參考之前的一些文章,比如:
??Elliptic Curve Cryptography: a gentle introduction[1]?(強烈推薦這篇)
TLS 1.2
在深入握手細節(jié)之前我們可以先抓一個 HTTPS 的包去對整個握手流程有個大致感受,如下所示:

其中綠色部分是為了便于理解已經解密了的 http-over-tls 明文。從上面的數(shù)據(jù)包中我們可以得到一些初步印象:
??從握手開始,到發(fā)送第一個數(shù)據(jù)包之前,客戶端與服務端之間經歷了 5 次傳輸;
??每次傳輸中帶有一個或者多個
數(shù)據(jù)包;
這里的數(shù)據(jù)包稱為?TLS Record,每個 Record 都是典型的 TLV (Type-Length-Value) 結構,而其中 Value 的具體類型又與 Record 的類型有關。
Record 的結構如下:
struct?{
????uint8?major;
????uint8?minor;
}?ProtocolVersion;
enum?{
????change_cipher_spec(20),?alert(21),?handshake(22),
????application_data(23),?(255)
}?ContentType;
struct?{
????ContentType?type;
????ProtocolVersion?version;
????uint16?length;
????opaque?fragment[TLSPlaintext.length];
}?TLSPlaintext;在實際的網絡傳輸中,一個 Record 可以分割為多個 Fragment,每個 Fragment 的大小最多不超過?2^14 = 16384,而 Record 的 length 字段為?uint16,最大可以到 65535 字節(jié)。之所以這么設計是因為對于加密的數(shù)據(jù),解密方需要收到整個 Record 才能開始解密,太大會影響解密性能,從而增加交互延時。
從上述定義可以看到 Record 的前兩字節(jié)是用于定義協(xié)議版本,但是從上圖我們發(fā)現(xiàn) TLS 1.2 對應的版本為 0303,這乍看起來有點別扭,但其實是歷史發(fā)展的結果。歷史上 TLS 由 SSL 進化而來,通常也統(tǒng)稱為 SSL/TLS,因此版本對應關系分別是:
??SSL 3.0 -> 0300
??TLS 1.0 -> 0301
??TLS 1.1 -> 0302
??TLS 1.2 -> 0303
??TLS 1.3 -> 0304
??...
只需要記住這個版本號即可,并不是很重要。后文也會將 SSL、TLS、SSL/TLS 混用,如無特殊說明,都是指代當前分析的 TLS 協(xié)議。
另外值得一提的是 Record 類型,在 TLS 1.2 中主要有 4 個,基本上都出現(xiàn)在上述 Wireshark 截圖里了。其中在實際傳輸明文(HTTP)數(shù)據(jù)之前的大部分 Record 都是?handshake?類型的子類型,但?change_cipher_spec?卻是一個單獨的 Record 類型,并不屬于?handshake,這點后面也會提到。
下面,我們就來開始分析 TLS 握手的具體實現(xiàn)。
注: 下文中所使用到的公、私鑰以及相關工具都來源于以下的倉庫,并且相關介紹也參考了其中的流程,因此感興趣的可以直接通過原文去進行學習:
??The Illustrated TLS 1.2 Connection: Every byte explained[2]
??The Illustrated TLS 1.2 Connection - Github[3]
Client Hello
在 TLS 握手中,總是以客戶端的 ClientHello 為起始,就像 TCP 握手總是以 SYN 為起始一樣,告訴服務器我們想建立一個 TLS 鏈接。在 ClientHello 請求的結構如下:
struct?{
????ProtocolVersion?client_version;
????Random?random;
????SessionID?session_id;
????CipherSuite?cipher_suites<2..2^16-2>;
????CompressionMethod?compression_methods<1..2^8-1>;
????select?(extensions_present)?{
????????case?false:
????????????struct?{};
????????case?true:
????????????Extension?extensions<0..2^16-1>;
????};
}?ClientHello;client_version?指客戶端版本,值為?0x0303,表示 TLS 1.2,前面已經說過。值得一提的是該值與 Record 中的版本不一定一致,后者由于兼容性的原因通常會設置為一個較舊的版本(比如 TLS 1.0),服務端應當以 ClientHello 中指定的版本為準。
random?是客戶端本地生成的?32 字節(jié)?隨機數(shù),在?RFC5246[4]?中提到隨機數(shù)的前四字節(jié)應該是客戶端的本地時間戳,但后來發(fā)現(xiàn)這樣會存在針對客戶端或者服務端的設備指紋標記[5],因此已經不建議使用時間戳了。為方便后續(xù)計算,這里將該隨機數(shù)固定為:
00?01?02?03?04?05?06?07?08?09?0a?0b?0c?0d?0e?0f?10?11?12?13?14?15?16?17?18?19?1a?1b?1c?1d?1e?1fsession_id?主要用于恢復加密鏈接,需要客戶端和服務端同時支持。由于秘鑰協(xié)商的過程中涉及到很多費時的操作,對于短鏈接而言將之前協(xié)商好的加密通道恢復可以大大減少運算資源。如果服務器支持恢復會話,那么后續(xù)可以直接進入加密通信,否則還是需要進行完整的握手協(xié)商。該字段的長度是可變的,占 1 字節(jié),也就是說數(shù)據(jù)部分最多可以長達 255 字節(jié)。
cipher_suites?表示客戶端所支持的加密套件,帶有 2 字節(jié)長度字段,每個加密套件用 2 字節(jié)表示,且優(yōu)先級高的排在前面。使用?openssl?可以查看實現(xiàn)的加密套件列表,如下所示:
$?openssl?ciphers?-V?|?column?-t
0x13,0x02??-??TLS_AES_256_GCM_SHA384?????????TLSv1.3??Kx=any???????Au=any????Enc=AESGCM(256)?????????????Mac=AEAD
0x13,0x03??-??TLS_CHACHA20_POLY1305_SHA256???TLSv1.3??Kx=any???????Au=any????Enc=CHACHA20/POLY1305(256)??Mac=AEAD
0x13,0x01??-??TLS_AES_128_GCM_SHA256?????????TLSv1.3??Kx=any???????Au=any????Enc=AESGCM(128)?????????????Mac=AEAD
0xC0,0x2C??-??ECDHE-ECDSA-AES256-GCM-SHA384??TLSv1.2??Kx=ECDH??????Au=ECDSA??Enc=AESGCM(256)?????????????Mac=AEAD
0xC0,0x30??-??ECDHE-RSA-AES256-GCM-SHA384????TLSv1.2??Kx=ECDH??????Au=RSA????Enc=AESGCM(256)?????????????Mac=AEAD
0x00,0x9F??-??DHE-RSA-AES256-GCM-SHA384??????TLSv1.2??Kx=DH????????Au=RSA????Enc=AESGCM(256)?????????????Mac=AEAD
...每個加密套件包含一個秘鑰交換算法、一個認證算法、一個對稱加密算法和一個用于完整性校驗的 MAC 算法。例如后文中協(xié)商出的加密套件?0xC0,0x13?表示?ECDHE-RSA-AES128-SHA。
compression_methods?表示客戶端所支持的一系列壓縮算法。數(shù)據(jù)需要先壓縮后加密,因為加密后的數(shù)據(jù)通常很難壓縮。但是壓縮的數(shù)據(jù)在加密中會受到類似?CRIME[6]?攻擊的影響,所以當前幾乎所有的實現(xiàn)都默認禁用了壓縮,而且在新版本的 TLS 中也將壓縮的特性移除了。
extensions?是 TLS 具備拓展性的一個功能,用于告知服務端一些額外的信息或者啟用某些新的特性。比如客戶端所支持的 TLS 版本列表,支持的橢圓曲線種類,支持的簽名算法等。其中一個常用的拓展為 SNI(Server Name Indication),包含了服務器的明文域名,主要用于后端服務器的反向代理。
Server Hello
服務器在收到 ClientHello 后必須回應 ServerHello 進行下一步握手協(xié)商,否則就需要回復 Alert 類型的 Record 告訴客戶端中斷握手并關閉連接。
ServerHello 的類型和 ClientHello 基本一致,差別在于回應的?cipher_suite?和?compression_method?是選擇后的固定值,而不是列表。另外需要注意的是服務端返回的 ServerHello 中的拓展必須是客戶端中所提供的拓展。假設服務端返回的?cipher_suite?為 0x0c13 (ECDHE-RSA-AES128-SHA),返回的隨機數(shù)為:
70?71?72?73?74?75?76?77?78?79?7a?7b?7c?7d?7e?7f?80?81?82?83?84?85?86?87?88?89?8a?8b?8c?8d?8e?8fServer Certificate
隨后服務端接著返回自身的證書信息,X509 格式,使用 ASN.1 DER 編碼。通常服務器會返回多個證書,因為當前域名往往不是由根證書直接簽名的,而是使用由于根證書所簽名的次級證書去簽發(fā)具體域名的證書。
如果使用了多級證書,那么返回的證書列表中第一個必須是對應域名的證書,而后每個證書都是前一個證書的 issuer,且最后一個證書是由系統(tǒng)中某個根證書簽發(fā)的,注意根證書本身并不會一起返回。
以 baidu.com 為例,實際返回的證書列表如下:
$?openssl?s_client?-connect?baidu.com:443
CONNECTED(00000006)
...
---
Certificate?chain
?0?s:/C=CN/ST=Beijing/O=BeiJing?Baidu?Netcom?Science?Technology?Co.,?Ltd/CN=www.baidu.cn
???i:/C=US/O=DigiCert?Inc/CN=DigiCert?Secure?Site?Pro?CN?CA?G3
?1?s:/C=US/O=DigiCert?Inc/CN=DigiCert?Secure?Site?Pro?CN?CA?G3
???i:/C=US/O=DigiCert?Inc/OU=www.digicert.com/CN=DigiCert?Global?Root?CA
---實際返回了兩個證書,最后一個證書由?DigiCert Global Root CA?簽發(fā)。如果服務端需要校驗客戶端證書的話,隨后會發(fā)送一個?Certificate Request?請求,然后客戶端返回對應的?Client Certificate?進行一輪額外的信息交換,當然這一步是可選的。
Server Key Exchange
服務器會在 server Certificate 消息之后發(fā)送 Server Key Exchange 消息,提供用于 ECDH 秘鑰交換的信息。
關于 ECDH 的原理可以閱讀文章開頭的參考文章,簡單來說,ECDH 可以在通信媒介不可信的情況下安全地完成秘鑰交換。假設 A、B 雙方的公私鑰分別是 PA、SA,PB、SB,那么有:
PA?*?SB?==?PB?*?SA雙方只需要知道對方的公鑰,可以在不暴露私鑰的情況下實現(xiàn)信息的交換,防止中間人攻擊,所交換的信息就是后續(xù)使用的對稱加密秘鑰。
更進一步,為了避免未來私鑰泄露導致以前的通信被解密,通常交換時并不直接使用原始公私鑰,而是一個隨機生成的新公私鑰對,只需要用原始私鑰進行認證。這種交換方式也稱為 ECDHE,其中?E?表示?Ephemeral,而這種做法所帶來的稱為 Forward Security,即前向安全。
令服務端端選擇的橢圓曲線為?x25519,那么首先需要生成一個臨時私鑰,長度為 32 字節(jié),只要隨機生成即可,這里假設為:
909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeaf那么對應的公鑰則是:
9fd7ad6dcff4298dd3f96d5b1b2af910a0535b1488d7f8fabb349a982880b615可以通過 openssl 計算得出:
$?openssl?pkey?-noout?-text?
X25519?Private-Key:
priv:
????90:91:92:93:94:95:96:97:98:99:9a:9b:9c:9d:9e:
????9f:a0:a1:a2:a3:a4:a5:a6:a7:a8:a9:aa:ab:ac:ad:
????ae:af
pub:
????9f:d7:ad:6d:cf:f4:29:8d:d3:f9:6d:5b:1b:2a:f9:
????10:a0:53:5b:14:88:d7:f8:fa:bb:34:9a:98:28:80:
????b6:15Server Key Exchange 的消息格式如下所示:
struct?{
select?(KeyExchangeAlgorithm)?{
????case?dh_anon:
????????ServerDHParams?params;
????case?dhe_dss:
????case?dhe_rsa:
????????ServerDHParams?params;
????????digitally-signed?struct?{
????????????opaque?client_random[32];
????????????opaque?server_random[32];
????????????ServerDHParams?params;
????????}?signed_params;
????case?rsa:
????case?dh_dss:
????case?dh_rsa:
????????struct?{}?;
????????/*?message?is?omitted?for?rsa,?dh_dss,?and?dh_rsa?*/
????/*?may?be?extended,?e.g.,?for?ECDH?--?see?[TLSECC]?*/
};
}?ServerKeyExchange;不同的加密套件有不同的格式,由于我們的是?dhe_rsa,因此在消息中應當包含橢圓曲線參數(shù),以及?ClientRandom+ServerRandom+參數(shù)?的簽名信息。使用原始私鑰來計算最終的簽名信息:
$?cat?client_random?>>?/tmp/compute
$?cat?server_random?>>?/tmp/compute
#?03?->?named_curve;?001d?->?curve?x25519
$?echo?-en?'\x03\x00\x1d'?>>?/tmp/compute
$?echo?-en?'\x20'?>>?/tmp/compute?#?臨時私鑰長度?32?字節(jié)
$?cat?server-ephemeral-public.key?>>?/tmp/compute?#?上述生成的臨時公鑰
$?openssl?dgst?-sign?server.key?-sha256?/tmp/compute?|?hexdump
0000000?04?02?b6?61?f7?c1?91?ee?59?be?45?37?66?39?bd?c3
...?snip?...
00000f0?7d?87?dc?33?18?64?35?71?22?6c?4d?d2?c2?ac?41?fbServer Hello Done
服務端表示自己這一邊的握手已經完成,接下來就等待客戶端計算相關握手信息了。
Client Key Exchange
客戶端收到 ClientKeyExchange 后,得知服務器選擇了 x25519 曲線,那么也類似的生成響應臨時秘鑰。隨機生成 32 字節(jié)私鑰:
202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f對應的公鑰計算方法與 Server Key Exchange 中介紹的一樣:
358072d6365880d1aeea329adf9121383851ed21a28e3b75e965d0d2cd166254Client Key Exchange 的數(shù)據(jù)格式如下:
struct?{
????select?(KeyExchangeAlgorithm)?{
????????case?rsa:
????????????EncryptedPreMasterSecret;
????????case?dhe_dss:
????????case?dhe_rsa:
????????case?dh_dss:
????????case?dh_rsa:
????????case?dh_anon:
????????????ClientDiffieHellmanPublic;
????}?exchange_keys;
}?ClientKeyExchange;其格式相對簡單,對于我們選擇的加密套件而言只需要包含臨時生成的 ECDH 公鑰。注意此處與 Server Key Exchange 不同,并沒有對客戶端的公鑰進行簽名,也就是說可以被中間人進行替換。不過協(xié)議設計的時候已經考慮到了這一點,因為此時雙方已經有足夠的信息去協(xié)商秘鑰并且進行驗證了,通過后文的計算過程也可以確認這一點。
Client Change Cipher Spec
該數(shù)據(jù)包告訴服務器客戶端已經計算好了共享秘鑰,并且后續(xù)客戶端發(fā)送給服務器的數(shù)據(jù)都將使用共享秘鑰進行加密。在下一個版本的 TLS 中該數(shù)據(jù)包類型將會被移除,因為加密數(shù)據(jù)是可以通過數(shù)據(jù)類型推斷的。
那么,客戶端是如何計算出共享秘鑰的呢?目前客戶端所已知的數(shù)據(jù)為:
??client_random
??server_random
??server-ephemeral-public.key
??client-ephemeral-private.key
首先根據(jù)前文對 ECDH 的介紹,通過對方的公鑰和自己的私鑰,可以計算出一個共同秘鑰,這里稱之為?PMS(Pre-Master-Secret),具體計算方法可以參考curve25519-mult.c[7]:
$?gcc?-o?curve25519-mult?curve25519-mult.c
$?./curve25519-mult?client-ephemeral-private.key?\
????????????????????server-ephemeral-public.key?|?hexdump
0000000?df?4a?29?1b?aa?1e?b7?cf?a6?93?4b?29?b4?74?ba?ad
0000010?26?97?e2?9f?1f?92?0d?cc?77?c8?a0?a0?88?44?76?24實際上服務端計算出的共享秘鑰也是一樣的:
$?./curve25519-mult?server-ephemeral-private.key?\
????????????????????client-ephemeral-public.key?|?hexdump
0000000?df?4a?29?1b?aa?1e?b7?cf?a6?93?4b?29?b4?74?ba?ad
0000010?26?97?e2?9f?1f?92?0d?cc?77?c8?a0?a0?88?44?76?24該共享秘鑰計算過程只涉及自身私鑰和對方的公鑰,為了進一步將共享秘鑰關聯(lián)當當前會話中,需要為其加入雙方的隨機數(shù),當然不能直接相加,需要增加隨機性,因此使用到了一個偽隨機函數(shù),稱為 PRF(pseudorandom function)。其計算方式如下:
seed?=?"master?secret"?+?client_random?+?server_random
a0?=?seed
a1?=?HMAC-SHA256(key=PreMasterSecret,?data=a0)
a2?=?HMAC-SHA256(key=PreMasterSecret,?data=a1)
p1?=?HMAC-SHA256(key=PreMasterSecret,?data=a1?+?seed)
p2?=?HMAC-SHA256(key=PreMasterSecret,?data=a2?+?seed)
MasterSecret?=?p1[all?32?bytes]?+?p2[first?16?bytes]所得到的的 48 字節(jié)拓展秘鑰稱為主密鑰(Master Secret),其值為:
916abf9da55973e13614ae0a3f5d3f37b023ba129aee02cc9134338127cd7049781c8e19fc1eb2a7387ac06ae237344c最后,我們需要將該主密鑰進行拓展(至任意長度),并將結果的不同部分分別用作不同秘鑰,如下所示:
seed?=?"key?expansion"?+?server_random?+?client_random
a0?=?seed
a1?=?HMAC-SHA256(key=MasterSecret,?data=a0)
a2?=?HMAC-SHA256(key=MasterSecret,?data=a1)
a3?=?HMAC-SHA256(key=MasterSecret,?data=a2)
a4?=?...
p1?=?HMAC-SHA256(key=MasterSecret,?data=a1?+?seed)
p2?=?HMAC-SHA256(key=MasterSecret,?data=a2?+?seed)
p3?=?HMAC-SHA256(key=MasterSecret,?data=a3?+?seed)
p4?=?...
p?=?p1?+?p2?+?p3?+?p4?...
client?write?mac?key?=?[first?20?bytes?of?p]
server?write?mac?key?=?[next?20?bytes?of?p]
client?write?key?=?[next?16?bytes?of?p]
server?write?key?=?[next?16?bytes?of?p]
client?write?IV?=?[next?16?bytes?of?p]
server?write?IV?=?[next?16?bytes?of?p]最終秘鑰分成了 6 個部分,分別是客戶端和服務端的 MAC 秘鑰、數(shù)據(jù)加密秘鑰和初始向量。這里涉及到幾個有趣的問題,比如:
??為什么客戶端和服務端要使用不同的數(shù)據(jù)加密秘鑰?
??為什么客戶端和服務端要使用不同的 MAC 秘鑰?
??為什么要單獨指定 IV?
根據(jù) RFC5246 中的介紹,使用不同的 MAC 秘鑰是為了防止來自一方的數(shù)據(jù)被注入到另一方中;對于使用流密鑰加密的情況,客戶端和服務端使用不同的秘鑰也能防止秘鑰重用攻擊。
在 TLS 1.0 中的 CBC 使用了前一部分 Record 的數(shù)據(jù)作為 IV 導致了選擇明文攻擊 (chosen plaintext attack),因此這在新版本中的 TLS 協(xié)議明確指定了 IV 的生成方法。注意這個 IV 只有部分需要額外指定 IV 的 AEAD 算法會用到。
總而言之,通過 ECDHE 秘鑰交換,客戶端計算出了下述秘鑰:
client?MAC?key:?1b7d117c7d5f690bc263cae8ef60af0f1878acc2
server?MAC?key:?2ad8bdd8c601a617126f63540eb20906f781fad2
client?write?key:?f656d037b173ef3e11169f27231a84b6
server?write?key:?752a18e7a9fcb7cbcdd8f98dd8f769eb
client?write?IV:?a0d2550c9238eebfef5c32251abb67d6
server?write?IV:?434528db4937d540d393135e06a11bb8PS: ChangeCipherSpec 消息并不是 Handshake(22) 消息的子結構,而是一個單獨的消息,與 Handshake 并列,類型為 20。這是為了解決握手期間管道阻塞的問題。
Client Handshake Finished
該數(shù)據(jù)包告訴服務器,客戶端的握手流程也已經完成。同時,還攜帶了一部分加密數(shù)據(jù),所加密的內容稱為?Verify Data,用以驗證握手成功且沒有被中間人修改過。
Verify Data?的內容是該消息之前的所有握手包的 HASH 經過 HMAC 計算出來的一個 12 字節(jié)數(shù)據(jù),其計算方法為:
seed?=?"client?finished"?+?SHA256(all?handshake?messages)
a0?=?seed
a1?=?HMAC-SHA256(key=MasterSecret,?data=a0)
p1?=?HMAC-SHA256(key=MasterSecret,?data=a1?+?seed)
verify_data?=?p1[first?12?bytes]在示例數(shù)據(jù)包中,verify_data?值為?cf919626f1360c536aaad73a,使用?client_write_key?進行加密,服務端收到后使用對應的秘鑰進行解密,所使用加解密算法由之前協(xié)商的加密套件決定,這里是?aes-128-cbc。
Server Change Cipher Spec
服務端收到上述加密后的數(shù)據(jù)為(Record Body):
404142434445464748494a4b4c4d4e4f
227bc9ba81ef30f2a8a78ff1df50844d
5804b7eeb2e214c32b6892aca3db7b78
077fdd90067c516bacb3ba90dedf720f為了進行驗證,服務端使用相同的方式計算出共享秘鑰 Pre Master Secret,由 ECDH 的特性以及前文的計算可以看到,服務端和客戶端計算出的 PMS 是相同的,因衍生出來的對稱加密秘鑰、IV、MAC 秘鑰也是相同的。
故,服務端收到加密數(shù)據(jù)后,可以使用協(xié)商出來的 client_write_key 對其進行解密:
hexdata=227bc9ba81ef30f2a8a78ff1df50844d5804b7eeb2e214c32b6892aca3db7b78077fdd90067c516bacb3ba90dedf720f
#?client?write?key
hexkey=f656d037b173ef3e11169f27231a84b6
#?record?iv,保存在加密數(shù)據(jù)之前
hexiv=404142434445464748494a4b4c4d4e4f
$?echo?-n?$hexdata?|?xxd?-r?-p?|?openssl?enc?-d?-nopad?-aes-128-cbc?-K?$hexkey?-iv?$hexiv?|?rax2?-S
1400000ccf919626f1360c536aaad73a
a5a03d233056e4ac6eba7fd9e5317fac
2db5b70e0b0b0b0b0b0b0b0b0b0b0b0b值得注意的是這里使用的 key 是協(xié)商的?client_write_key,但 IV 并不是?client_write_iv,而是一個隨機生成的針對當前 Record 的 IV,并且附加到加密數(shù)據(jù)的前方。
在解密后的數(shù)據(jù)中,?1400000c?是 Record 的子協(xié)議頭部,對應 Handshake/Finish,長度 0x0c 即 12 字節(jié),數(shù)據(jù)正好是前面計算出的?verify_data?的值,即?cf919626f1360c536aaad73a。
末尾還有 32 字節(jié)的數(shù)據(jù),是使用 client mac key 計算的簽名,用于確保所接收數(shù)據(jù)的完整性,計算方法為:
###?from?https://tools.ietf.org/html/rfc2246#section-6.2.3.1
$?sequence='0000000000000000'
$?rechdr='16?03?03'
$?datalen='00?10'
$?data='14?00?00?0c?cf?91?96?26?f1?36?0c?53?6a?aa?d7?3a'
###?client?MAC?key
$?mackey=1b7d117c7d5f690bc263cae8ef60af0f1878acc2
$?echo?$sequence?$rechdr?$datalen?$data?|?xxd?-r?-p?\
??|?openssl?dgst?-sha1?-mac?HMAC?-macopt?hexkey:$mackey
a5a03d233056e4ac6eba7fd9e5317fac2db5b70e再使用?PKCS#5?填充至 32 字節(jié)即為末尾添加的數(shù)據(jù)。服務端通過將客戶端發(fā)送的 verify_data 與自身計算的值進行比對,可確保整個握手流程的完整性;使用 HMAC 校驗當前數(shù)據(jù)可以保證消息沒有被中間人篡改。
在這些校驗都完成后,服務端給客戶端返回 Change Cipher Spec 消息,告知客戶端接下來發(fā)送的數(shù)據(jù)都將經過協(xié)商秘鑰進行加密。當然,和前文一樣,其實這條消息是多余的,在 TLS 1.3 中已經被移除了。
Server Handshake Finished
此時,服務端已經完成了握手的所有流程,并且也確認這個握手流程沒有被中間人篡改,但是客戶端還不知道?。∫虼?,類似于 Client Handshake Finished,服務端也要發(fā)送一個加密并驗簽的數(shù)據(jù)給客戶端,讓客戶端進行驗證并確認整個握手流程的正確性。
發(fā)送的數(shù)據(jù)格式和 Client Finished 幾乎一樣,只有幾點小差異。比如數(shù)據(jù)使用?server_write_key?進行加密(而不是 client_write_key),HMAC key 也是類似。另外計算?verify_data?與前者相比還多了一個?Client Finished?消息,畢竟協(xié)議中說的是用于驗證 "當前消息前的所有握手消息"。
客戶端收到 Server Finished 后,同樣進行解密并校驗 HMAC,如果確認無誤就可以開始發(fā)送應用數(shù)據(jù)了。
注: ChangeCipherSpec、Alert 以及其他非 Handshake 的消息都不參與 verify_data 的計算,另外根據(jù) RFC 的規(guī)定,HelloRequest 請求也不參與哈希計算。
Application Data
Application Data 是一個單獨類型的 Record (type=23),準確來說已經不屬于握手階段了,不過這里還是提一下。
該消息格式中主要是使用協(xié)商秘鑰加密的應用數(shù)據(jù),客戶端發(fā)送的數(shù)據(jù)使用 client write key 進行加密,服務端返回的數(shù)據(jù)使用 server write key 進行加密,并且明文數(shù)據(jù)末尾還加了 HMAC 校驗數(shù)據(jù),使用對應的 MAC key 進行簽名,加解密和簽名過程和 Client/Server Finished 消息的過程一致。因此每條應用數(shù)據(jù)都可以保證機密性和完整性。
在新版本的 Wireshark 中,TLS 流量 可以通過指定 keylog 文件進行解密,許多應用如 curl、Chrome 都可以通過指定?SSLKEYLOGFILE?環(huán)境變量指定 keylog 的位置,例如:
$?SSLKEYLOGFILE=~/SSLKEYLOGFILE.txt?curl?https://example.com
$?cat?~/SSLKEYLOGFILE.txt
CLIENT_RANDOM?54BA544490B18D32B4725E40493A0839CA64AACE14A3E119C0658C49E12A998C?EA6B59F06F9E2C219535A02C31FEDBE60053E235A91FC36701B2871477C18E99CE6F2F33144EB6FC28031BB7D51BEF00keylog 文件的格式為?NSS Key Log Format[8],上述輸出是針對 TLS 1.2 的會話秘鑰,第一部分為客戶端隨機數(shù),用于與具體的 TLS 流進行對應;第二部分則是 48 字節(jié)的?Pre Master Secret?值。根據(jù)我們前面的分析,通過該秘鑰可以獲取到雙方的對稱加密秘鑰,因此就可以解密對應的 TLS 數(shù)據(jù)了。
TLS 1.3
由于 TLS 1.3 是在 TLS 1.2 的基礎上優(yōu)化而來的,因此對于與上節(jié)實現(xiàn)相同的部分就不再詳細介紹了,而只關注其中不同的部分。
總體來看,TLS 1.3 與 TLS 1.2 相比,較大的差異有下面這些:
??去除了一大堆過時的對稱加密算法,只留下較為安全的 AEAD (Authenticated Encryption with Associated Data) 算法;加密套件(cipher suite) 的概念被修改為單獨的認證、秘鑰交換算法以及秘鑰拓展和 MAC 用到的哈希算法;
??去除了靜態(tài) RSA 和秘鑰交換算法套件,使目前所有基于公鑰的交換算法都能保證前向安全;
??引入了 0-RTT(round-trip time) 的模式,減少握手的消息往返次數(shù);
??
ServerHello?之后所有的握手消息都進行了加密;??修改了秘鑰拓展算法,稱為 HKDF (HMAC-based Extract-and-Expand Key Derivation Function);
??廢棄了 TLS 1.2 中的協(xié)議版本協(xié)商方法,改為使用 Extension 實現(xiàn);
? TLS 1.2 中的會話恢復功能現(xiàn)在采用了新的 PSK 交換實現(xiàn);
??......
下面就以一個完整的握手流程去分析這之中的差異,所使用的秘鑰和數(shù)據(jù)包可以在下面的鏈接中找到:
??The Illustrated TLS 1.3 Connection: Every byte explained[9]
??The Illustrated TLS 1.3 Connection - Github[10]
Client Hello
與 TLS 1.2 一樣,握手總是以 Client 發(fā)送 Hello 請求開始。但正如本節(jié)開頭所說,TLS 握手時的協(xié)議協(xié)商不再使用 Handshake/Hello 中的 version 字段,雖然是 1.3 版本,但請求中 version 還是指定 1.2 版本,這是因為有許多 web 中間件在設計時候會忽略不認識的 TLS 版本號,因此為了兼容性,版本號依舊保持不變。實際協(xié)商 TLS 版本是使用的是?Supported Versions?拓展實現(xiàn)的。
client_random?是客戶端生成的隨機數(shù),這里是:
00?01?02?03?04?05?06?07?08?09?0a?0b?0c?0d?0e?0f?10?11?12?13?14?15?16?17?18?19?1a?1b?1c?1d?1e?1fsession_id?字段在此前的版本中該字段被用于恢復 TLS 會話,不過在 TLS 1.3 中會話恢復使用了一種更為靈活的 PSK 秘鑰交換方式,因此這個字段在 TLS 1.3 中是沒有實際作用的。
在 Client Hello 消息中,有一個重要的拓展,即?Key Share,用于與服務器交換秘鑰。前文說到在 TLS 1.3 中,Server Hello 之后的所有消息都是加密的,為了雙方能夠正確加解密數(shù)據(jù),因此在 Client Hello 中通過該拓展告訴服務端自己的公鑰以及秘鑰交換算法。
這里客戶端還是指定了 x25519 橢圓曲線加密,并且生成一個臨時私鑰:
202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f對應的公鑰計算方法前文已經說過,這里再重復一遍:
$?openssl?pkey?-noout?-text?
X25519?Private-Key:
priv:
????20:21:22:23:24:25:26:27:28:29:2a:2b:2c:2d:2e:
????2f:30:31:32:33:34:35:36:37:38:39:3a:3b:3c:3d:
????3e:3f
pub:
????35:80:72:d6:36:58:80:d1:ae:ea:32:9a:df:91:21:
????38:38:51:ed:21:a2:8e:3b:75:e9:65:d0:d2:cd:16:
????62:54隨后,該公鑰就隨著 Client Hello 發(fā)送給了服務端。
Server Hello
服務端根據(jù)客戶端提供的選項,選擇一個好自己支持的 TLS 版本以及加密套件,這里選的是?TLS_AES_256_GCM_SHA384, 返回的?server_random?為:
70?71?72?73?74?75?76?77?78?79?7a?7b?7c?7d?7e?7f?80?81?82?83?84?85?86?87?88?89?8a?8b?8c?8d?8e?8f由于涉及到了秘鑰交換,服務端在收到請求后也需要先生成一對臨時公私鑰,所生成的私鑰為:
909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeaf對應的公鑰是:
9fd7ad6dcff4298dd3f96d5b1b2af910a0535b1488d7f8fabb349a982880b615Key Share?Extension 中返回的即為上述公鑰。如果還記得上文的 ECDH 秘鑰交換方法,這里就可以很容易計算出兩端的共享秘鑰:
$?./curve25519-mult?server-ephemeral-private.key?\
????????????????????client-ephemeral-public.key?|?hexdump
0000000?df?4a?29?1b?aa?1e?b7?cf?a6?93?4b?29?b4?74?ba?ad
0000010?26?97?e2?9f?1f?92?0d?cc?77?c8?a0?a0?88?44?76?24該秘鑰用于生成后續(xù)握手包所需的秘鑰,使用 HKDF 函數(shù)進行生成,如下所示:
early_secret?=?HKDF-Extract(salt:?00,?key:?00...)
empty_hash?=?SHA384("")
derived_secret?=?HKDF-Expand-Label(key:?early_secret,?label:?"derived",?ctx:?empty_hash,?len:?48)
handshake_secret?=?HKDF-Extract(salt:?derived_secret,?key:?shared_secret)
client_secret?=?HKDF-Expand-Label(key:?handshake_secret,?label:?"c?hs?traffic",?ctx:?hello_hash,?len:?48)
server_secret?=?HKDF-Expand-Label(key:?handshake_secret,?label:?"s?hs?traffic",?ctx:?hello_hash,?len:?48)
client_handshake_key?=?HKDF-Expand-Label(key:?client_secret,?label:?"key",?ctx:?"",?len:?32)
server_handshake_key?=?HKDF-Expand-Label(key:?server_secret,?label:?"key",?ctx:?"",?len:?32)
client_handshake_iv?=?HKDF-Expand-Label(key:?client_secret,?label:?"iv",?ctx:?"",?len:?12)
server_handshake_iv?=?HKDF-Expand-Label(key:?server_secret,?label:?"iv",?ctx:?"",?len:?12)得到以下秘鑰:
??handshake secret: bdbbe8757494bef20de932598294ea65b5e6bf6dc5c02a960a2de2eaa9b07c929078d2caa0936231c38d1725f179d299
??server handshake traffic secret: 23323da031634b241dd37d61032b62a4f450584d1f7f47983ba2f7cc0cdcc39a68f481f2b019f9403a3051908a5d1622.
??client handshake traffic secret: db89d2d6df0e84fed74a2288f8fd4d0959f790ff23946cdf4c26d85e51bebd42ae184501972f8d30c4a3e4a3693d0ef0.
??server handshake key: 9f13575ce3f8cfc1df64a77ceaffe89700b492ad31b4fab01c4792be1b266b7f
??server handshake IV: 9563bc8b590f671f488d2da3
??client handshake key: 1135b4826a9a70257e5a391ad93093dfd7c4214812f493b3e3daae1eb2b1ac69
??client handshake IV: 4256d2e0e88babdd05eb2f27
客戶端也可以計算出同樣的秘鑰值。
Server Encrypted Extensions
在計算完共享秘鑰后,后續(xù)的流量將使用上述秘鑰進行加密,因此對于 TLS 1.2 的情況服務端會先返回一個 ChangeCipherSpec,在 TLS 1.3 中可不必多此一舉,不過在兼容模式下為了防止某些中間件抽風還是會多這么一步。
我們這里直接看加密的數(shù)據(jù),服務端一般會先返回一個 Encrypted Extensions 類型的 Record 消息,該消息加密后存放在 Record(type=0x17),即 Application Data 的 Body 部分,同時(加密后數(shù)據(jù)的)末尾還添加了 16 字節(jié)的?Auth Tag,這是 AEAD 算法用來校驗加密消息完整性的數(shù)據(jù)。
數(shù)據(jù)使用?AES-256-GCM?進行加密和校驗,解密代碼可以參考?aes_256_gcm_decrypt.c[11],使用 server hanshake key/iv 進行解密的示例如下所示:
#?server?handshake?key
$?key=9f13575ce3f8cfc1df64a77ceaffe89700b492ad31b4fab01c4792be1b266b7f
#?server?handshake?iv
$?iv=9563bc8b590f671f488d2da3
###?from?this?record
$?recdata=1703030017
$?authtag=9ddef56f2468b90adfa25101ab0344ae
$?recordnum=0
###?may?need?to?add?-I?and?-L?flags?for?include?and?lib?dirs
$?cc?-o?aes_256_gcm_decrypt?aes_256_gcm_decrypt.c?-lssl?-lcrypto
$?echo?"6b?e0?2f?9d?a7?c2?dc"?|?xxd?-r?-p?>?/tmp/msg1
$?cat?/tmp/msg1?\
??|?./aes_256_gcm_decrypt?$iv?$recordnum?$key?$recdata?$authtag?\
??|?hexdump?-C
00000000??08?00?00?02?00?00?16??????????????????????????????|.......|這里解密后的拓展長度為空。一般與握手無關的額外拓展都會放在這里返回,這是為了能夠盡可能地減少握手階段的明文傳輸。
注: 如無特殊說明,后續(xù)的握手包都是使用相同方法進行加密的,并且只針對解密后的原數(shù)據(jù)進行分析。
Server Certificate
使用 server handshake key/iv 進行加密。解密后的數(shù)據(jù)與 TLS 1.2 的證書響應相同,因此不再贅述。
Server Certificate Verify
前文 Hello 階段進行 ECDHE 秘鑰交換的時候其實有個問題,即雙方只交換了公鑰,卻沒有認證這個秘鑰,因此如果存在網絡劫持,就可能被中間人進行攻擊,那加密似乎也只是加了個寂寞。
但無需擔心,這點早已在計劃之中。雖然之前沒有進行認證,但可以后面補上。Server Certificate Verify 就是這個作用。該消息將服務端證書的私鑰與之前生成的臨時公鑰進行綁定,準確來說是使用證書的私鑰對其進行簽名,并將簽名算法與結果返回給客戶端。由于客戶端可以認證證書的有消息,就間接地證實了之前所交換的秘鑰的真實性。
struct?{
????SignatureScheme?algorithm;
????opaque?signature<0..2^16-1>;
}?CertificateVerify;同理,如果服務端需要驗證客戶端的真實性,那么在提供客戶端證書后也同樣發(fā)送一個 Certificate Verify 消息即可。
Server Handshake Finished
至此服務端所需要發(fā)送的握手包已經發(fā)送完畢了,因此最后發(fā)送一個 Finished 數(shù)據(jù)給客戶端并等待對方的握手完成。在 Finished 數(shù)據(jù)中,消息體的內容和 TLS 1.2 類似,是通過此前所有的握手數(shù)據(jù)計算得到的?verify_data,并使用 HMAC 進行認證,進一步確保此前的消息沒有經過中間人修改。
計算方法如下:
finished_key?=?HKDF-Expand-Label(key:?server_secret,?label:?"finished",?ctx:?"",?len:?32)
finished_hash?=?SHA384(Client?Hello?...?Server?Cert?Verify)
verify_data?=?HMAC-SHA384(key:?finished_key,?msg:?finished_hash)server_secret 是指前文中協(xié)商得到的?server handshake traffic secret。
同時,服務端使用前面協(xié)商得到的?handshake secret?加上前面所有握手包的哈希重新計算出一個應用秘鑰,用于加密實際的應用數(shù)據(jù)。計算方法如下:
empty_hash?=?SHA384("")
derived_secret?=?HKDF-Expand-Label(key:?handshake_secret,?label:?"derived",?ctx:?empty_hash,?len:?48)
master_secret?=?HKDF-Extract(salt:?derived_secret,?key:?00...)
client_secret?=?HKDF-Expand-Label(key:?master_secret,?label:?"c?ap?traffic",?ctx:?handshake_hash,?len:?48)
server_secret?=?HKDF-Expand-Label(key:?master_secret,?label:?"s?ap?traffic",?ctx:?handshake_hash,?len:?48)
client_application_key?=?HKDF-Expand-Label(key:?client_secret,?label:?"key",?ctx:?"",?len:?32)
server_application_key?=?HKDF-Expand-Label(key:?server_secret,?label:?"key",?ctx:?"",?len:?32)
client_application_iv?=?HKDF-Expand-Label(key:?client_secret,?label:?"iv",?ctx:?"",?len:?12)
server_application_iv?=?HKDF-Expand-Label(key:?server_secret,?label:?"iv",?ctx:?"",?len:?12)之所以重新計算而不是使用 handshake key 是為了防止針對某些加密套件可能存在的選擇密文攻擊。最終得到:
??server application key: 01f78623f17e3edcc09e944027ba3218d57c8e0db93cd3ac419309274700ac27
??server application IV: 196a750b0c5049c0cc51a541
??client application key: de2f4c7672723a692319873e5c227606691a32d1c59d8b9f51dbb9352e9ca9cc
??client application IV: bb007956f474b25de902432f
相當于 TLS 1.2 中的 client/server write key/IV。
Client Handshake Finished
由于雙方的 handshake secret 相同,那么由此派生出來的 application key/iv 必然也是相同的。
客戶端在收到 Server Finished 之后會使用對應服務器證書對數(shù)據(jù)進行校驗,確認無誤后進行可選的 ChangeCipherSpec 將加密并簽名的 verify_data 在 Finished 請求中發(fā)送給服務器。
#?client?handshake?traffic?secret
finished_key?=?HKDF-Expand-Label(key:?client_secret,?label:?"finished",?ctx:?"",?len:?32)
finished_hash?=?SHA384(Client?Hello?...?Server?Finished)
verify_data?=?HMAC-SHA384(key:?finished_key,?msg:?finished_hash)可以這么理解,Server Finished 用來讓客戶端確認服務端沒有被中間人攻擊,而 Client Finished 則用來讓服務端確認客戶端沒有被中間人攻擊。雙向認證之后則可以保證 TLS 握手的真實性和完整性,成功建立加密信道。
Server Session Ticket
這一步通常是可選的。服務端在握手完成后會發(fā)送若干個 ticket 給客戶端,可以理解為 web 中的 cookie。客戶端在后續(xù)如果需要重新發(fā)起握手,可以帶上這個 ticket,用于恢復當前的 TLS 會話。從上面的握手流程可見 TLS 握手需要涉及許多計算和網絡請求,如果能夠恢復會話,將極大地降低云服務器資源和網絡延時。
ticket 消息的格式如下:
struct?{
????uint32?ticket_lifetime;
????uint32?ticket_age_add;
????opaque?ticket_nonce<0..255>;
????opaque?ticket<1..2^16-1>;
????Extension?extensions<0..2^16-2>;
}?NewSessionTicket;包含有效期、隨機數(shù)等信息。其中?ticket?字段對于客戶端是透明的,但對于服務端而言需要是有效的會話憑據(jù),可通過該數(shù)據(jù)恢復之前的 TLS 會話。
由于 ticket 是一次性的,綜合考慮時間和空間成本,一般服務端都會返回兩個 ticket 給客戶端。由于是服務端返回的數(shù)據(jù),因此使用 server application key/iv 進行加密。
Application Data
隨后客戶端發(fā)送的數(shù)據(jù)加密方式與 handshake 過程的加密類似,區(qū)別僅在于應用數(shù)據(jù)的加密使用的是 client application key/iv,服務端發(fā)送給客戶端的數(shù)據(jù)使用 server application key/iv。
前文說到 Wireshark 支持導入 keylog 解密 TLS 1.2 流量,其實對于 TLS 1.3 也可以,但由于后者在 Server Hello 之后的握手包都經過了加密,因此要解密 TLS 1.3 數(shù)據(jù)流還分別需要握手階段的秘鑰。
以本節(jié)所涉及到的握手包為例,其 keylog 內容如下:
$?openssl?s_client?-keylogfile?keylog.txt?-connect?example.com:443
$?cat?keylog.txt
SERVER_HANDSHAKE_TRAFFIC_SECRET?000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f?23323da031634b241dd37d61032b62a4f450584d1f7f47983ba2f7cc0cdcc39a68f481f2b019f9403a3051908a5d1622
CLIENT_HANDSHAKE_TRAFFIC_SECRET?000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f?db89d2d6df0e84fed74a2288f8fd4d0959f790ff23946cdf4c26d85e51bebd42ae184501972f8d30c4a3e4a3693d0ef0
EXPORTER_SECRET?000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f?5da16dd8325dd8279e4535363384d9ad0dbe370538fc3ad74e53d533b77ac35ee072d56c90871344e6857ccb2efc9e14
SERVER_TRAFFIC_SECRET_0?000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f?86c967fd7747a36a0685b4ed8d0e6b4c02b4ddaf3cd294aa44e9f6b0183bf911e89a189ba5dfd71fccffb5cc164901f8
CLIENT_TRAFFIC_SECRET_0?000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f?9e47af27cb60d818a9ea7d233cb5ed4cc525fcd74614fb24b0ee59acb8e5aa7ff8d88b89792114208fec291a6fa96bad同一個 TLS 流中的 RANDOM 值相同,且都是?client_random,使用 Label 來區(qū)分不同階段的秘鑰。值得一提的是 macOS 中的 LibreSSL 還不支持?-keylogfile?選項,因此上面的命令是在 Linux 中進行測試的。
至此,TLS 1.2 和 TLS 1.3 的常規(guī)握手流程基本上也分析完了,當然在這其中還存在一些減少 RTT 的優(yōu)化,篇幅原因就留到以后分析 mmtls 的時候再說了。
Android 證書 hash
還記得前文中介紹 Server Certificate 的時候,服務端返回一直到除了根證書以外的的證書鏈,那么從最后一個證書是如何定位到根證書的呢?一般認為是通過 issuer 字段,但 issuer 字段只是一段文字(ASN.1),很可能會重復,這樣豈不是有歧義?。其實分析 Android 的驗證流程就可以知道。
了解 Android 應用安全的朋友應該都知道系統(tǒng)的根證書存放在?/system/etc/security/cacerts/?之中,里面的證書都是?xxxx.0?格式,這是個傳統(tǒng)的 PEM 格式證書,但名字比較特殊,使用?openssl x509 -subject_hash_old?生成。
通過閱讀 AOSP 相關代碼會發(fā)現(xiàn),這個文件名的前半部分也正是證書的 subject 字段的 MD5,后半部分的?.0?則是為了防止哈希碰撞預留的計數(shù)字段,如果有重復就加一,相關代碼如下:
//?frameworks/base/core/java/android/security/net/config/DirectoryCertificateSource.java
private?Set?findCerts(X500Principal?subj,?CertSelector?selector)?{
????String?hash?=?getHash(subj);
????Set?certs?=?null;
????for?(int?index?=?0;?index?>=?0;?index++)?{
????????String?fileName?=?hash?+?"."?+?index;
????????if?(!new?File(mDir,?fileName).exists())?{
????????????break;
????????}
????????if?(isCertMarkedAsRemoved(fileName))?{
????????????continue;
????????}
????????X509Certificate?cert?=?readCertificate(fileName);
????????if?(cert?==?null)?{
????????????continue;
????????}
????????if?(!subj.equals(cert.getSubjectX500Principal()))?{
????????????continue;
????????}
????????if?(selector.match(cert))?{
????????????if?(certs?==?null)?{
????????????????certs?=?new?ArraySet();
????????????}
????????????certs.add(cert);
????????}
????}
????return?certs?!=?null???certs?:?Collections.emptySet();
} 我們也可以通過以下?Python?腳本去進行手動計算:
from?cryptography?import?x509
import?hashlib
import?struct
with?open("01419da9.0",?"rb")?as?f:
????pem?=?f.read()
cert?=?x509.load_pem_x509_certificate(pem)
d?=?hashlib.md5(cert.subject.public_bytes()).digest()
out?=?struct.unpack(",?d[:4])[0]
print("hash:?%08x"?%?out)
assert(out?==?0x01419da9)SSL Pinning
SSL Pinning 又稱為 Certificate Pinning,通常翻譯為證書綁定。但個人感覺這個翻譯其實不太準確,因為綁定關系通常是雙向的,而 Pinning 操作實質上只是客戶端應用限制了本身所信任的 CA 范圍,類似于圖釘將某個證書固定到對應域名上。
在 Android 應用中可以通過?NSC(Network Security Config)[12]?來設置證書綁定規(guī)則,例如:
res/xml/network_security_config.xml:
"1.0"?encoding="utf-8"?>
<network-security-config>
????<domain-config>
????????<domain?includeSubdomains="true">evilpan.comdomain>
????????<pin-set?expiration="2022-01-01">
????????????<pin?digest="SHA-256">7HIpactkIAq2Y49orFOOQKurWxmmSFZhBCoQYcRhJ3Y=pin>
????????????
????????????<pin?digest="SHA-256">fwza0LRMXouZHRC8Ei+4PyuldPDcf3UKgO/04cDM1oE=pin>
????????pin-set>
????domain-config>
network-security-config>由于證書有效期的問題,客戶端應用如果無法及時升級,可能會導致服務端證書更新后無法正常進行 HTTPS 通信,因此 Google 是不推薦應用使用證書綁定[13]的。如果一定要用的話,最好提供一個備用的可控 pin(比如自簽名的證書),用以保障通信的可用性。
當然不使用 NSC 也是可以實現(xiàn)證書綁定。因為本質上只是對服務端證書的單獨比對,將 TLS 握手過程中獲取的服務端證書或者其中的公鑰與本地保存的值進行額外的校驗。這個校驗過程可以放在 Java 層,也可以放在 native 層(比如抖音),但這都是具體實現(xiàn)問題了。
后記
本文主要介紹了 TLS 1.2 和 TLS 1.3 的常規(guī)握手過程,分別參考了?RFC5246[14]?和?RFC8446[15]。雖然有很多細節(jié)還沒有涉及到,但總的來說對 TLS 的理解又加深了一點,也算沒浪費這個周末。后續(xù)有時間的話會總結一些在客戶端安全研究時的網絡分析方法,以及一些關于 TLS 的有趣特性,Stay Tune!
參考資料
??The Illustrated TLS 1.2 Connection[16]
??The Illustrated TLS 1.3 Connection[17]
??TLS協(xié)議分析 與 現(xiàn)代加密通信協(xié)議設計[18]
??WeMobileDev: 基于TLS1.3的微信安全通信協(xié)議mmtls介紹[19]
引用鏈接
[1]?Elliptic Curve Cryptography: a gentle introduction:?https://andrea.corbellini.name/2015/05/17/elliptic-curve-cryptography-a-gentle-introduction/[2]?The Illustrated TLS 1.2 Connection: Every byte explained:?https://tls12.ulfheim.net/[3]?The Illustrated TLS 1.2 Connection - Github:?https://github.com/syncsynchalt/illustrated-tls[4]?RFC5246:?https://datatracker.ietf.org/doc/html/rfc5246[5]?針對客戶端或者服務端的設備指紋標記:?https://tools.ietf.org/html/draft-mathewson-no-gmtunixtime-00[6]?CRIME:?https://en.wikipedia.org/wiki/CRIME[7]?curve25519-mult.c:?https://tls12.ulfheim.net/files/curve25519-mult.c[8]?NSS Key Log Format:?https://firefox-source-docs.mozilla.org/security/nss/legacy/key_log_format/index.html[9]?The Illustrated TLS 1.3 Connection: Every byte explained:?https://tls13.ulfheim.net/[10]?The Illustrated TLS 1.3 Connection - Github:?https://github.com/syncsynchalt/illustrated-tls13[11]?aes_256_gcm_decrypt.c:?https://tls13.ulfheim.net/files/aes_256_gcm_decrypt.c[12]?NSC(Network Security Config):?https://developer.android.com/training/articles/security-config#CertificatePinning[13]?不推薦應用使用證書綁定:?https://developer.android.com/training/articles/security-ssl#Pinning[14]?RFC5246:?https://datatracker.ietf.org/doc/html/rfc5246[15]?RFC8446:?https://datatracker.ietf.org/doc/html/rfc8446[16]?The Illustrated TLS 1.2 Connection:?https://tls12.ulfheim.net/[17]?The Illustrated TLS 1.3 Connection:?https://tls13.ulfheim.net/[18]?TLS協(xié)議分析 與 現(xiàn)代加密通信協(xié)議設計:?https://blog.helong.info/blog/2015/09/07/tls-protocol-analysis-and-crypto-protocol-design/[19]?WeMobileDev: 基于TLS1.3的微信安全通信協(xié)議mmtls介紹:?https://github.com/liuqun/article/blob/master/%E5%9F%BA%E4%BA%8ETLS1.3%E7%9A%84%E5%BE%AE%E4%BF%A1%E5%AE%89%E5%85%A8%E9%80%9A%E4%BF%A1%E5%8D%8F%E8%AE%AEmmtls%E4%BB%8B%E7%BB%8D.md
