這個問題成功的吸引了我的注意。

起因:一個Flutter寫的app在iOS上偶爾會發生了界面卡頓甚至凍結10多秒,但在Android正常。

開始這個問題沒太引起重視,覺得是flutter的問題。但后來隨着dart的issue里面報告的人逐漸多起來,看起來不是那么簡單。不過非常令人迷惑的是這個現象只在iOS偶爾出現,Android從來不出現,這種不確定性使得重現和調試非常困難。

在issue里面發現報告的人大部分疑似是中國用戶,之后發現有人提到更換了阿里雲證書之后問題不再重現,這使得我們把問題方向放在https上。最終發現服務器的OCSP Stapling失效,造成了soft failure。之后的行為要看客戶端實現,有的瀏覽器接受soft failure,不進行客戶端檢查,一切正常。但有一些客戶端比如Safari會自己去檢查了OCSP狀態,從而造成界面無響應。檢查nginx log發現ocsp.int-x3.letsencrypt.org請求超時,隨后確認此域名遭到了DNS污染。

在服務器開啟OCSP Stapling對於提升速度幫助很大。所以無論如何也是應該開啟的。

但是仍然有兩個問題沒有解釋:

  1. 為什么Android沒問題,iOS有問題
  2. 為什么有時候可以重現,有時候不可以重現

為了回答這幾個問題,順便找一個解決方案,我順着讀了一圈代碼和協議,從nginx到openssl,從tls到ocsp。最后終於能回答這兩個問題了。

1 Android沒有問題的原因是,Google不滿意ocsp這個解決方案,所以所有google的產品,無論是android還是chrome都不進行ocsp檢查。

ocsp作用是檢查證書狀態,尤其是是否吊銷,Google認為檢查證書狀態並不能增加安全性,並且導致https請求時間變長,並且ocsp服務器本身也可能會出問題,這不是一個可靠的方案。Google通過分發一個列表到本地來解決證書檢查問題。當然有人提出爭議說分發列表這個過程會因為升級服務器被屏蔽而失效,Google認為如果能屏蔽我們的升級服務器,那么屏蔽ocsp服務器豈不是更容易?所以從2012年開始,Google就逐步取消了ocsp檢查。

2 為什么有時候可以,有時候不可以

讀nginx代碼,發現nginx會把ocsp請求結果放在內存里面,直到過期之前才會再次請求ocsp服務器更新狀態。但是如果重啟了nginx,內存里面的結果就丟掉了,下一次就會直接請求ocsp服務器。

letsencrypt使用akamai cdn分發ocsp狀態,實際上遭到DNS污染的似乎是akamai.net的某一部分節點,應該還有少量沒被污染。所以有時候還能取得正確的結果,一旦取得正確的結果之后,在下次nginx重啟/ocsp過期之前就會變得一切正常。這使得重現它更加困難。

代碼讀完之后,也就知道了解決方案:

  • 使用 ssl_stapling_file 配置,從一個外部文件獲取ocsp信息 ngx_ssl_stapling_file
  • 使用 ssl_stapling_responder配置,nginx會用這個設置覆蓋證書里面的Authority Information Access信息,使得請求ocsp被發送到設置的服務器

兩者之間我更傾向后者,后者靈活的多,也省去了跨機器更新文件的麻煩,順便還能解決以后其它麻煩。

我首先想按照ocsp協議寫一個簡單的responder,不過搜索之后發現有人很多年前寫過一段非常簡單的轉發代碼,直接把請求轉發給指定的服務器。雖然必須要設置一個固定的轉發服務器(因為原始的Authority Information Access信息被nginx覆蓋了)。我想更好的解決方案是修改一下nginx的代碼,在這個http請求中把原始的AIA放到header里面一起發給代理,不過考慮到大部分人都會把所有證書集中在一個供應商,設置一個轉發地址完全能解決問題。而且避免每次升級給nginx重新打補丁的麻煩。所以就不改了。

我稍微修改了一下這個代碼,讓程序可以從環境變量獲得轉發地址,以便於使用docker部署。新的代碼在這里: https://github.com/virushuo/ocsp-proxy

部署好了之后在nginx.conf里面增加配置:

1
2 3 4 
ssl_stapling on;
ssl_stapling_verify on;
ssl_trusted_certificate /etc/ssl/ca-certs.pem;
ssl_stapling_responder http://YOUR_PROXY_IP:8080/; 

 

轉自:https://jhuo.ca/post/ocsp-stapling-letsencrypt/