從Chrome原始碼看HTTPS

人人網FED發表於2018-02-26

我在《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_HandShake
hs->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_SHA256
TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256
TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384
TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384
TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256
TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256
TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA
TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA
TLS_RSA_WITH_AES_128_GCM_SHA256
TLS_RSA_WITH_AES_256_GCM_SHA384
TLS_RSA_WITH_AES_128_CBC_SHA
TLS_RSA_WITH_AES_256_CBC_SHA
TLS_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 https://g.co/chrome/symantecpkicerts 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 = 115792089129476408780076832771566570560534619664239564663761773211729002495996
b = 99593677540221402957765480916910020772520766868399186769503856397241456836063
n = 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_X25519
Peer Public Key (64B):   8b4364a862a7a7f19404973237079b692c1208b8ecf7828d9eae2b76e68e5012
Chrome Public Key (64B): cddd4c2d0c9d49903438a953076fb3baebd38cfa4a3b18144365b67756b4c075
Share Key (78B):         653d6e28202ff88dff92db77c91406b7992a0f15325b0192f17a317e7ff71930404dc7d4857f03
Master Key (96B):        eb584819ae738a45fe9a2e60734d0ae833dfb2d63a1900ee820a36db27a3844e5b6259e2c84e06fd1474c7e1857989ad

連線域名:www.baidu.com
加密方式:TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256
曲線名稱:SSL_CURVE_SECP256R1
Peer Public Key (142B):   04ac277ce63eb420e9e973c96cdf67e37a5956b949af4b053ca5b1b4b1f884b7f6cadbe2d64a91d43a2e280da528d6b6505bc6be10455e70aeabe569562ccc7bdebc7b5df80705
Chrome Public Key (130B): 04941ec80392f0bf13268a9791e7ee673df0a00af6e59335655b0519fbc575bfcb39eabd80f81118dca4906f776c801aee26f8f4fc195917dc94f9c324886bebc4
Share Key (78B):          52d0f6fc4ecd83107fb8c1cc7fa3f978152c0936c58d8d62d6885f7a672cf87c21212121212103
Mater 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連線的前幾毫秒發生了什麼


相關文章