在我開發 Zoraxy 之前我也不知道 https 證書這東西對於 reverse proxy 伺服器來說到底有多複雜。一開始的時候因為我都是用一個域名(domain name),所以 reverse proxy 裡面也是只需要處理一個主機名稱(host name)。而初代的 Zoraxy 則是直接用 TLS hello info 裡面的 server name 作為 certificate 的 key 來找到合適的 certificate 並回傳給客戶端,所以時間複雜度(time complexity)而言是 O(1) 的速度。換成程式碼大概長這樣:
http.ListenAndServeTLS(":443", "server.crt", "server.key", nil)
到後來發現 virtual directory 帶來的麻煩後,Zoraxy v2 加入了 sub-domain 的支援。為了解決找 certificate 的問題, v2 的做法是讓使用者把每個 sub-domain 都 map 到一張 certificate 上面,所以結果就是同一個域名下會有很多張證書(例如說 a.example.com 會有 a.example.com 的證書、b.example.com 會有 b.example.com 的證書)。這樣設計的好處是一來找 certificate 的演算法比較簡單,二來我們可以根據 hello info 的 server name 進行也是 O(1) 時間複雜度的 certificate lookup,不用任何存取 file system 的 loop 便可以直接找到證書。
if fileExists(helloInfo.ServerName+".pem") && fileExists( helloInfo.ServerName+".key") {
//Direct hit
pubKey = helloInfo.ServerName+".pem"
priKey = helloInfo.ServerName+".key"
}
然後在 Zoraxy v2 用了這一年多之後就又出現問題了。coauthor 的其中一張證書裡面包含了幾個不同的 domain 跟 subdomain,也有使用者的證書是含 wildcard 的,也有是舊版用 Common Name 來定義(而不是 DNS entry)來標記 host name 的,結果就是一大堆 issue 就出現在 repo 上面。
SNI 的基本概念
SNI 的原理大概就是由伺服器端跟據 TLS Hello Info 的 Server Name 來自動回傳合適的 certificate 給 Client (這裡因為 Zoraxy 是一個 http proxy,那我就以瀏覽器為 client 的例子)。對於很直接的域名例如說 v1 跟 v2 裡面出現的,基本上就是只要把 certificate map 到一個 string 轉 certificate 的資料結構裡面就可以了。但是對於 v3 之後的複雜案例,則是需要一些更複雜的邏輯來處理。
相信來得我部落格的人不是工程師應該都是技術大佬,所以我就直接把 code 拿出來講好了。這裡是 Zoraxy v3 TLS 解釋器裡面最重要的 function
func(m * Manager) CertMatchExists(serverName string) bool {
for _, certCacheEntry: = range m.LoadedCerts {
if certCacheEntry.Cert.VerifyHostname(serverName) == nil || certCacheEntry.Cert.Issuer.CommonName == serverName {
return true
}
}
return false
}
func(m * Manager) GetCertByX509CNHostname(serverName string)(string, string) {
for _, certCacheEntry: = range m.LoadedCerts {
if certCacheEntry.Cert.VerifyHostname(serverName) == nil || certCacheEntry.Cert.Issuer.CommonName == serverName {
return certCacheEntry.PubKey, certCacheEntry.PriKey
}
}
return "", ""
}
這裡有兩個很重要的點,就是
- 雖然說現代的 certificate 一般都是用 DNS 作為儲存 host name 的欄位,但是有一些較舊的 certificate / certificate generator 還是使用 Common Name (通稱 CN)來放 host name
- 由於從 x509 Certificate 抓出來的 host name 有機會是 wildcard、甚至在上面還會有 exclusion rules,所以是沒辦法把所有可能性都抓出來建 map 進行 O(1) time complexity 的搜尋
所以唯一能做來加速 O(n) 運算的就只有 in-memory 的核對跟 caching 了(哭喔
(噢,你說這兩個 function 可以整合做一個? O(n) + O(n) 的 time complexity 都是 O(n) 沒差啦,而且那樣又不符合 clean code 設計原則了)
Zoraxy v3 的做法總結
所以 Zoraxy v3 保留了原先 v1 跟 v2 的做法,並在 v1 跟 v2 都沒有 hit 的時候再跑上面那兩段 function 來檢查有哪張證書是能用的,如果都沒有就會掉到去最下面使用 fall-back certificate 。雖然說這樣子 worst case 的 TLS certificate resolve time complexity 掉到了 O(n),但是一般而言對於像我的網站這種簡單 user case 一般都會落在 O(1) 的速度,再加上使用了 in-memory 的 certificate validation (就是預先把 certificate 都載入到 RAM 裡面),所以就算是 O(n) 速度還算是可以接受的水平。如果之後還是覺得慢的話,還可以加入基於 sync.Map 的 cache 來把最近 resolve 過的 hostname 和對應的 certificate 塞進去,但是現在來說這速度已經夠用了。