從Chrome源碼看HTTPS

我在《https連接的前幾毫秒發生了什麼》詳細地介紹了https連接的過程,該篇通過抓包工具分析整個過程,本篇將從Chrome源碼的角度著重介紹加密和解密的過程,並補充更多的細節。

Chrome/Chromium是使用BoringSSL做為TLS層的庫,它是OpenSSL的一個fork,是Chrome改於openssl以適應自己產品的特點,代碼位於src/third_party/boringssl/.

HTTPS連接的第一步——發送Client Hello,瀏覽器在Client Hello報文裡面填充了使用的TLS版本、client隨機數、加密列表(cipher suites)和包含了hostname的擴展。

瀏覽器支持的TLS版本總共有5個:

#define SSL3_VERSION 0x0300 // 3.0#define TLS1_VERSION 0x0301 // 3.1#define TLS1_1_VERSION 0x0302 // 3.2#define TLS1_2_VERSION 0x0303 // 3.3 (TLS 1.2)#define TLS1_3_VERSION 0x0304 // 3.4 (TLS 1.3)

最新的版本為TLS 1.3,目前只有Chrome和Firefox支持,nginx 1.13(非穩定版本)/cloudflare支持,當前使用比較廣泛的還是TLS 1.2版本。Chrome在Client Hello裡面設置的TLS為1.2:

// hs為SSL_HandShakehs->client_version = hs->max_version >= TLS1_2_VERSION ? TLS1_2_VERSION : hs->max_version;

除了TLS外,還有支持UDP的DTLS:

#define DTLS1_VERSION 0xfeff#define DTLS1_2_VERSION 0xfefd

列印出來的加密列表cipher suite總共有13個:

TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256TLS_ECDHE_RSA_WITH_AES_128_CBC_SHATLS_ECDHE_RSA_WITH_AES_256_CBC_SHATLS_RSA_WITH_AES_128_GCM_SHA256TLS_RSA_WITH_AES_256_GCM_SHA384TLS_RSA_WITH_AES_128_CBC_SHATLS_RSA_WITH_AES_256_CBC_SHATLS_RSA_WITH_3DES_EDE_CBC_SHA

這是瀏覽器支持的加密方式,放在Client Hello裡面發給服務端選擇一個。上面的每一個加密方式都是用的兩個位元組的數字編號表示,如第一個編號為0xc02B,這個是在RFC5289進行的規定。

這一長串的加密名字表示什麼呢?以TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256為例,如下圖所示:

密鑰交換使用ECDHE演算法,服務身份驗證使用RSA演算法,數據傳輸加密使用AES(+GCM),握手使用SHA256檢驗。

換句話說,證書籤名使用RSA,如果證書驗證正確,那麼將使用ECDHE演算法進行密鑰交換,保證瀏覽器和服務擁有相同的私有密鑰,然後一方使用這把密鑰進行AES數據加密,另一方使用相同的密鑰進行AES數據解密。驗證證書籤名合法性和密鑰交換的身份確認都是使用SHA256這個哈希演算法進行檢驗。具體過程下文展開描述。

接下來,服務端進行Server Hello的響應,包括服務端要使用TLS版本,我們訪問google.com的時候谷歌返回的版本為TLS 1.2(0x303,即十進位的771):

如果服務返回的TLS版本為1.3,那麼Chrome將使用1.3版本。

Server Hello還返回一個32個位元組的隨機數server random,和瀏覽器發送的隨機數client random相似,這種隨機數叫做nonce,用於一次性使用,通常會帶有時間戳,在後面生成master key的時候用到。

還會返回一個session id用於下次復用當前握手的信息,避免短時間內重複握手。

同時返回所選擇的加密方式,如下圖所示:

谷歌伺服器使用了上面舉例的加密方式,根據觀察,這也是很多伺服器選擇的方式,這應該是權衡了安全性和計算複雜度的一種比較好的方式。知道了加密方式之後(包括證書是使用RSA簽名的),接下來等收到服務發過來的證書後,讀取證書並檢驗證書的合法性

證書的檢驗是Post一個Task給TaskScheduler線程獨立檢驗的,其它的握手操作都是在Chrome的IO線程進行,應該是考慮到證書的檢驗比較複雜,所以搞成非同步的。

證書的檢驗Chrome沒有使用BoringSSL提供的API,而是自己實現的,在src/net/cert目錄。這個過程是這樣的,首先會檢驗是否在黑名單里,這個黑名單如下源碼的注釋:

// CloudFlare revoked all certificates issued prior to April 2nd, 2014. Thus // all certificates where the CN ends with ".cloudflare.com" with a prior // issuance date are rejected. // // The old certs had a lifetime of five years, so this can be removed April // 2nd, 2019.

大意是說證書的通用名(通常就是證書的域名)是以.cloudflare.com結尾的證書,並且是2014.4.2前簽發的,已經被取消掉了,這些證書有5年的有效期,現在仍然處於有效期,所以需要認為是無效的。

接著檢驗證書籤名的合法性,在Mac上Chrome是調的系統函數SecTrustEvaluate做的檢驗。檢驗的過程我在《https連接的前幾毫秒發生了什麼》已做了詳細介紹,大概來說,先對證書進行SHA256得到一個哈希值,然後用證書的公鑰對證書的簽名進行解密從中取得另一個哈希值,如果這兩個哈希值相等,說明證書沒有被篡改過,確實是權威機構頒發。

一般來說,所謂數字簽名,就是對所發送的內容做一個哈希,然後接收方用內容計算一個哈希值,如果這個值等於簽名里的哈希,就說明內容沒有被第三方篡改過。而這個簽名通常是加密的,在證書裡面,這個簽名是使用證書的私鑰進行加密,任何人都可以拿證書里提供的公鑰進行解密,但是任何人沒有私鑰無法正確地加密,因為私鑰和公鑰是一一配對的,如果拿另外一把私鑰進行加密,再拿原先的公鑰進行解密必定不是原先的內容。

所以如果簽名檢驗正確,那麼發送的內容即證書是合法的(證書裡面有域名、公鑰等信息)。如果這一步的檢驗不合法,將返回CERT_STATUS_AUTHORITY_INVALID的錯誤。

再接著檢驗證書里指定的Common Name通用名是否匹配,如下圖所示:

當前訪問的hostname為www.google.co.kr,而證書裡面的通用名為*.google.co.kr:

www.google.co.kr包含在通配符*.google.co.kr里的,所以這個檢驗是通過的。如果不通過瀏覽器將會顯示CERT_STATUS_COMMON_NAME_INVALID的錯誤。

關於這個通配符,有一個小細節,如果通配符是*.com這種頂級域名的那麼認為是不合法的,只允許私人註冊的域:(這種支持泛域名的證書會比只支持固定域名的貴)

// Do not allow wildcards for public/ICANN registry controlled domains -// that is, prevent *.com or *.co.uk as valid presented names, but do not// prevent *.appspot.com (a private registry controlled domain).

然後檢測證書是否在公共的黑名單裡面:

如果是的話返回證書被取消的狀態:CERT_STATUS_REVOKED,這些黑名單列表可見blacklist。這些黑名單包括China Internet Network Information Center (CNNIC)等,因為公鑰固定導致不安全的原因,具體可以見文檔附上的鏈接說明。

再接著檢查證書是否使用了弱簽名演算法如SHA1/MD5:

如果的的話,返回CERT_STATUS_WEAK_SIGNATURE_ALGORITHM,因為SHA1和MD5都被認為是不安全的哈希演算法,容易被碰撞攻擊(如2017年2月23日,Google宣布了一個成功的SHA-1碰撞攻擊,發布了兩份內容不同但SHA-1散列值相同的PDF文件作為概念證明,詳見維基百科)。

緊接著檢驗證書是否是賽門鐵克頒發的:

// Distrust Symantec-issued certificates, as described at// https://security.googleblog.com/2017/09/chromes-plan-to-distrust-symantec.html

如果是Symantec頒發的,將會在Chrome 66版本(2018.4.17發布穩定版本)取消信任,賽門鐵克是全球幾大證書機構之一,旗下的根證書包括GeoTrust、VeriSign等:

為什麼谷歌要取消對它的信任,谷歌的blog是這麼說的:

During the subsequent investigation, it was revealed that Symantec had entrusted several organizations with the ability to issue certificates without the appropriate or necessary oversight, and had been aware of security deficiencies at these organizations for some time.

大意是說經過調查,在沒有被監督的情況下它隨意委任幾家機構頒發證書。當我們打開某些網站,控制台提示:

The SSL certificate used to load resources from https://***.com will be distrusted in M70. Once distrusted, users will be prevented from loading these resources. See g.co/chrome/symantecpki for more information.

就是因為它們使用了GeoTrust頒發的證書。

Chrome還會進行其它的檢驗,包括證書的有效期是否過長,如下源碼注釋:

// For certificates issued after 1 July 2012: 60 months.// For certificates issued after 1 April 2015: 39 months.// For certificates issued after 1 March 2018: 825 days.

還有證書本身的格式是否合法(CERT_STATUS_INVALID)等等。如果是EV增強型證書還有一些特殊的檢驗,有些證書需要使用在線證書狀態協議(OCSP)進行檢驗。

檢驗證書的合法性和握手(HandShake)是同步進行的,因為它是運行在獨立的線程。正常來說在Server Hello之後服務發送證書給瀏覽器進行檢驗,檢驗成功才進行下一步的操作,可能Chrome考慮到檢驗比較耗時,所以弄成非同步的。

不管怎麼樣,在Server Hello之後便進行密鑰交換,密鑰交換的目的是為了雙方共享密鑰,使用同一把密鑰進行加密和解密。密鑰交換的方式有兩種RSA和ECDHE,RSA的方式比較簡單,瀏覽器生成一把密鑰,然後使用證書RSA的公鑰進行加密發給服務端,服務再使用它的密鑰進行解密得到密鑰,這樣就能夠共享密鑰了,它的缺點是攻擊者雖然在發送的過程中無法破解,但是如果它保存了所有加密的數據,等到證書到期沒有被維護之類的原因導致私鑰泄露,那麼它就可以使用這把私鑰去解密之前傳送過的所有數據。而使用ECDHE是一種更安全的密鑰交換演算法。

ECDHE的全稱叫Elliptic Curve Diffie–Hellman key Exchange橢圓曲線迪非-赫爾曼密鑰交換,它是迪非-赫爾曼密鑰交換的變種,使用橢圓曲線加密提高安全性。

迪非-赫爾曼密鑰交換的過程是這樣的:交換密鑰雙方甲和乙選取一個基數g,例如g = 2,然後甲和乙產生自己的密鑰a和b,甲發送A = g^a和g給乙,乙收到後計算得到共享密鑰K = A ^ b = g^(ab),同時把B = g ^ b發給甲,這樣甲也能得到共享密鑰 K = B ^ a = g ^ (ab)。如下圖所示:

由於a和b通常會很大,做a或b次冪會是一個天文數字,所以需要模以一個大素數p。

竊聽者能夠知道g、A、B,但是不知道任何一方的密鑰a或者b,所以他無法知道共享密鑰K是什麼。為了保證傳遞的信息不會被人篡改,密鑰交換的數據需要使用證書的RSA進行簽名。更詳細的說明可參見維基百科。

通過冪方的計算值傳遞,較容易被破解得到雙方各自的密鑰,這種的安全係數不是很高,所以引入了曲線橢圓加密ECC。ECC和RSA一樣也可以當作證書的加密演算法,ECC和RSA的共同特點是加密步驟很簡單,但是解密非常困難,RSA的困難之處在於把一個大數拆成兩個素數相乘,而ECC的難點在於找到一個點的係數。不同點是ECC的破解難度要遠遠大於RSA,舉例來說2048位的RSA的破解難度相當於224位的ECC,長度越短就意味著CPU計算消耗越少,速度越快。ECC在很高級別的加密場合有較廣泛的應用。越來越多的證書使用ECC加密,如*.google.com的域名都是使用的EC加密的證書,相對於其它RSA證書2048位的公鑰,EC證書只有256位:

具體來說,所謂橢圓曲線就是指以下方程:

y^3 = x ^ 2 + ax +b

如下圖所示:

上圖由一個起始點P計算2P——先畫一條線與P點相切,與曲線的-2P點相交,做這個點的反射與曲線的交點就是2P,而計算3P就是2P + P,如下圖所示,連接P與2P,與曲線的第三個交點就是-3P,反射一下就得到3P:(任意一條直線與橢圓曲線最多只有3個交點)

依此類推,4P = 3P + P,連接3P與P與曲線的交點的反射就是4P。如果經過n次後最後連線與x軸垂直,說明所有的點已用完,總共有n(或者叫order)個點,在這個計算過程中會取一個大數p用來做模數,當點的坐標值大於p時就模一下,起始點P(x, y)叫Generator點,再加上方程參數的兩個係數ab——{a, b, order, x, y}就構成了一組橢圓曲線的基本參數。

橢圓曲線難以破解的地方在於——給定點P和Q,Q = kP (1 < k < n),想要推導出k是一件很困難的事情(通常n會很大)。

因此使用橢圓曲線加密的密鑰交換過程就變成:

中間人或者竊聽者能夠知道Q1和Q2以及方程係數a、b和起始點P,但是它無法推導出雙方各自的密鑰x、y,因此它沒有辦法計算得到共享密鑰K = xyP。並且這個破解的難度要遠遠大於使用冪方的方式。這個就是ECDHE。更詳細的信息可以查看這個視頻教程。

在實際的實現里,基本參數不是在密鑰交換中傳遞的,而是約定的固定的曲線,在調試過程中,我們發現Chrome總共支持3種曲線 :

static const uint16_t kDefaultGroups[] = { SSL_CURVE_X25519, SSL_CURVE_SECP256R1, SSL_CURVE_SECP384R1,};

www.google.co.kr使用的是Curve X25519,X25519使用的曲線方程為:

y^2 = x^3 + 486662x2 + x

而*.google.com使用的是Curve secp256r1,簡稱為P-256,這個是在Server Key Exchange裡面指定的:

它的參數組是這樣的:

如果轉換成十進位的話:

a = 115792089129476408780076832771566570560534619664239564663761773211729002495996b = 99593677540221402957765480916910020772520766868399186769503856397241456836063n = 115792089210356248762697446949407573529996955224135760342422259061068512044369

我們看到n是一個78位的數字,所以暴力破解k(P = kG,1 < k < n)基本上是不可能的。

確定基本方程後,雙方Q1和Q2值是在Server Key Exchange和Client Key Exchange以公鑰的形式進行交換。

為了確保密鑰交換不會被篡改,需要進行簽名,如果簽名使用的是RSA的話,那麼方法和驗證證書有效性一樣。如果證書是EC的證書,那麼會使用ECDSA(ecdsa_secp256r1_sha256(0x0403,))進行簽名:

具體驗證的函數是使用的ECDSA_do_verify這個函數,過程說明可參考維基百科,步驟比較多,這裡不深入討論。EC證書也有公鑰和密鑰,最後驗證合法的標準是使用公鑰解密的簽名裡面的r值如果等於手動計算的值,則說明正確。

接著Client Key Exchange,Chrome根據曲線類型(x25519或P-256)使用相應的參數和演算法生成公鑰和密鑰對,如X25519的密鑰是使用隨機數生成的:

有了密鑰再計算配套的公鑰,然後把公鑰保存起來發出去,並計算共享密鑰,最核心的代碼應該是以下幾行:

// Compute the x-coordinate of |peer_key| * |private_key_|.EC_POINT_mul(group.get(), result.get(), NULL, peer_point.get(), private_key_.get(), bn_ctx.get()

使用對方的公鑰peer_key * 自己的私鑰private_key_,得到K = yQ1.

緊接著用這個共享密鑰經過PRF計算得到主密鑰master key。我們可以把某次握手得到的密鑰列印出來,如下所示:

連接域名:www.google.com加密方式:TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256曲線名稱:SSL_CURVE_X25519Peer Public Key (64B): 8b4364a862a7a7f19404973237079b692c1208b8ecf7828d9eae2b76e68e5012Chrome Public Key (64B): cddd4c2d0c9d49903438a953076fb3baebd38cfa4a3b18144365b67756b4c075Share Key (78B): 653d6e28202ff88dff92db77c91406b7992a0f15325b0192f17a317e7ff71930404dc7d4857f03Master Key (96B): eb584819ae738a45fe9a2e60734d0ae833dfb2d63a1900ee820a36db27a3844e5b6259e2c84e06fd1474c7e1857989ad連接域名:www.baidu.com加密方式:TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256曲線名稱:SSL_CURVE_SECP256R1Peer Public Key (142B): 04ac277ce63eb420e9e973c96cdf67e37a5956b949af4b053ca5b1b4b1f884b7f6cadbe2d64a91d43a2e280da528d6b6505bc6be10455e70aeabe569562ccc7bdebc7b5df80705Chrome Public Key (130B): 04941ec80392f0bf13268a9791e7ee673df0a00af6e59335655b0519fbc575bfcb39eabd80f81118dca4906f776c801aee26f8f4fc195917dc94f9c324886bebc4Share Key (78B): 52d0f6fc4ecd83107fb8c1cc7fa3f978152c0936c58d8d62d6885f7a672cf87c21212121212103Mater Key (96B): 1e95a25c356a170c6829ec27a0216c50738b758f93606e8503a2e306796fd99db6ec49f65818a125bba6449b07648262

密鑰交換之後,雙方已經有了相同的密鑰,然後通過發送Change Cipher Spec通知對方下一個包將會使用之前約定的方式進行加密。由於傳送數據指定的是GCM加密,它是一種AEAD的加密方式,Chrome會在Change Cipher的過程中做AEAD的配置,這個加密方式的特點是會給數據添加認證標籤,如果標籤對得上說明數據完整沒有被破壞。

至此整個TLS握手完成,然後就是發送HTTP請求和接收響應數據了。

數據傳送使用的AES加密的特點是使用一把密鑰加密,再使用相同的密鑰就可以解密,具體加密和解密的過程比較複雜,這裡不深入研究。不過我們可以把加密前和加密後的數據列印出來,如下圖所示:

可以看到這是一個HTTP請求,加密前的數據有572B,加密後的數據有601B,體積增長了5%。

這個請求收到以下解密後的響應數據:

還有緊接著的gzip壓縮的數據。

至此整個過程就說明完了,本篇重點說了Chrome是怎麼檢驗證書合法性的、Diff-Hellman演算法是怎麼樣的、橢圓曲線是怎麼加密,怎樣使用ECDHE進行密鑰交換,等等。本文很多東西沒有講得很深入,都是點到為止,看完本篇應該對HTTPS整一個加密的過程有一個輪廓的了解,並且對一些加密演算法原理有所了解。

相關閱讀:

  1. 為什麼要把網站升級到HTTPS
  2. https連接的前幾毫秒發生了什麼

推薦閱讀:

TAG:HTTPS | 源碼閱讀 | GoogleChrome |