使用網路通訊協定確保安全性

用戶端與伺服器間已加密的互動會使用傳輸層安全標準 (TLS) 保護應用程式的資料。

本文將討論安全網路通訊協定的最佳做法和公開金鑰基礎架構 (PKI)的注意事項。詳情請參閱 Android 安全性總覽權限總覽

概念

含有 TLS 憑證的伺服器具有公開金鑰和相符的私密金鑰。伺服器會在 TLS 握手期間運用公開金鑰密碼學來簽署憑證。

簡單的握手只能證明伺服器知道憑證的私密金鑰。為處理這種情況,須讓用戶端信任多個憑證。如果特定伺服器的憑證未出現在用戶端的信任憑證組合中,即表示該伺服器不受信任。

不過,伺服器可能會使用金鑰輪替來更換憑證的公開金鑰。如要變更伺服器設定,必須更新用戶端應用程式。如果伺服器是第三方 Web 服務,例如網路瀏覽器或電子郵件應用程式,會難以判斷更新用戶端應用程式的時機。

伺服器通常依賴憑證授權單位 (CA) 憑證來核發憑證,這樣能長時間保持較穩定的用戶端設定。CA 會使用本身的私密金鑰簽署伺服器憑證。隨後用戶端就能檢查伺服器是否有平台已知的 CA 憑證。

主機平台通常會列出信任的 CA。Android 8.0 (API 級別 26) 包含超過 100 個 CA,分別在每個版本更新,不會在裝置間變更。

由於 CA 為許多伺服器提供憑證,用戶端應用程式需要某種驗證伺服器的機制。為識別伺服器,CA 憑證會使用特定名稱 (如 gmail.com),或使用萬用字元 (例如 *.google.com)。

如要查看網站的伺服器憑證資訊,請使用 openssl 工具的 s_client 指令,傳入通訊埠編號。根據預設,HTTPS 會使用通訊埠 443。

這個指令會將 openssl s_client 輸出內容傳輸至 openssl x509,並為憑證資訊套用 X.509 標準格式。這個指令會索取主題 (伺服器名稱) 和頒發機構 (CA)。

openssl s_client -connect WEBSITE-URL:443 | \
  openssl x509 -noout -subject -issuer

HTTPS 示例

假設您的網路伺服器擁有知名 CA 核發的憑證,就可以提出安全要求,如以下程式碼所示:

Kotlin

val url = URL("https://wikipedia.org")
val urlConnection: URLConnection = url.openConnection()
val inputStream: InputStream = urlConnection.getInputStream()
copyInputStreamToOutputStream(inputStream, System.out)

Java

URL url = new URL("https://wikipedia.org");
URLConnection urlConnection = url.openConnection();
InputStream in = urlConnection.getInputStream();
copyInputStreamToOutputStream(in, System.out);

如要自訂 HTTP 要求,請轉換為 HttpURLConnection。Android HttpURLConnection 說明文件提供多個示例,包含處理要求和回應標頭、發布內容、管理 Cookie、使用 Proxy、快取回應等。Android 架構會使用這些 API 驗證憑證和主機名稱。

請盡可能使用這些 API。下節說明需要不同解決方案的常見問題。

驗證伺服器憑證的常見問題

假設回傳內容不是 getInputStream(),而是擲回例外狀況:

javax.net.ssl.SSLHandshakeException: java.security.cert.CertPathValidatorException: Trust anchor for certification path not found.
        at org.apache.harmony.xnet.provider.jsse.OpenSSLSocketImpl.startHandshake(OpenSSLSocketImpl.java:374)
        at libcore.net.http.HttpConnection.setupSecureSocket(HttpConnection.java:209)
        at libcore.net.http.HttpsURLConnectionImpl$HttpsEngine.makeSslConnection(HttpsURLConnectionImpl.java:478)
        at libcore.net.http.HttpsURLConnectionImpl$HttpsEngine.connect(HttpsURLConnectionImpl.java:433)
        at libcore.net.http.HttpEngine.sendSocketRequest(HttpEngine.java:290)
        at libcore.net.http.HttpEngine.sendRequest(HttpEngine.java:240)
        at libcore.net.http.HttpURLConnectionImpl.getResponse(HttpURLConnectionImpl.java:282)
        at libcore.net.http.HttpURLConnectionImpl.getInputStream(HttpURLConnectionImpl.java:177)
        at libcore.net.http.HttpsURLConnectionImpl.getInputStream(HttpsURLConnectionImpl.java:271)

這可能由許多因素造成,包括:

  1. 核發伺服器憑證的 CA 不明
  2. 伺服器憑證不是 CA 簽署的憑證,而是自行簽署的憑證
  3. 伺服器設定缺少中介 CA

下列各節說明如何解決這些問題,同時確保與伺服器之間的連線安全無虞。

未知的憑證授權單位

由於系統不信任 CA,因此發生 SSLHandshakeException。這可能是因為憑證來自 Android 不信任的新 CA,或是應用程式在沒有 CA 的舊版本中運作。由於這項資訊不會公開,因此很難知道 CA。通常來說,CA 不明是因為該 CA 並非公開性質,而是政府、企業或教育機構等組織為供內部使用而核發的私密 CA。

如要在不變更應用程式程式碼的情況下信任自訂 CA,請變更網路安全性設定

注意:許多網站都會介紹一種不理想的替代解決方案,也就是安裝不執行作業的 TrustManager。這麼做會導致使用者在使用公用 Wi-Fi 無線基地台時容易遭受攻擊,因為攻擊者可能會運用 DNS 技巧,透過假冒您伺服器的 Proxy 傳送使用者流量,然後記錄密碼和其他個人資料。攻擊者之所以能得逞,是因為攻擊者可以產生憑證,而如果未使用 TrustManager 驗證憑證是否由信任來源核發,就無法阻斷這類攻擊。因此請不要這樣做,即使是暫時性的也不行。請改成將應用程式設為信任伺服器憑證的核發者。

自行簽署的伺服器憑證

第二,SSLHandshakeException 之所以發生,是因為有自行簽署的憑證,使伺服器成為自己的 CA。這類似與未知的憑證授權單位,因此請修改應用程式的網路安全性設定,信任自行簽署的憑證。

缺少中介憑證授權單位

第三,發生 SSLHandshakeException 是因為缺少中介 CA。公開 CA 很少簽署伺服器憑證,而是由根 CA 簽署中介 CA。

為了降低遭入侵的風險,CA 會讓根 CA 保持離線狀態。不過,Android 等作業系統一般只直接信任根 CA,這會導致由中介 CA 簽署的伺服器憑證與知道根 CA 的憑證驗證器之間,出現短暫不銜接的情況。

為解決此銜接問題,伺服器會在 TLS 握手期間透過任何中介,將伺服器 CA 的憑證鏈結傳送至信任的根 CA。

舉例來說,以下是透過 openssl s_client 指令查看的 mail.google.com 憑證鏈結:

$ openssl s_client -connect mail.google.com:443
---
Certificate chain
 0 s:/C=US/ST=California/L=Mountain View/O=Google LLC/CN=mail.google.com
   i:/C=ZA/O=Thawte Consulting (Pty) Ltd./CN=Thawte SGC CA
 1 s:/C=ZA/O=Thawte Consulting (Pty) Ltd./CN=Thawte SGC CA
   i:/C=US/O=VeriSign, Inc./OU=Class 3 Public Primary Certification Authority
---

在上述示例中,伺服器傳送了一個由 Thawte SGC CA (中介 CA) 核發給 mail.google.com 的憑證,以及另一個由 Verisign CA (Android 系統信任的主要 CA) 核發給 Thawte SGC CA 的憑證。

然而,伺服器可能不是設為包含必要的中介 CA。舉例來說,以下伺服器可能會導致 Android 瀏覽器發生錯誤,並造成 Android 應用程式出現例外狀況:

$ openssl s_client -connect egov.uscis.gov:443
---
Certificate chain
 0 s:/C=US/ST=District Of Columbia/L=Washington/O=U.S. Department of Homeland Security/OU=United States Citizenship and Immigration Services/OU=Terms of use at www.verisign.com/rpa (c)05/CN=egov.uscis.gov
   i:/C=US/O=VeriSign, Inc./OU=VeriSign Trust Network/OU=Terms of use at https://www.verisign.com/rpa (c)10/CN=VeriSign Class 3 International Server CA - G3
---

有別於不明 CA 或自行簽署的伺服器憑證,大多數電腦版瀏覽器在與這個伺服器通訊時,都不會產生錯誤。電腦版瀏覽器會快取信任的中介 CA,只要從某個網站得知中介 CA 後,就不必再將該中介 CA 加入憑證鏈結。

有些網站會刻意這麼做,用於提供資源的次要網路伺服器。為節省頻寬,可以利用具備完整憑證鏈結的伺服器提供主要 HTML 網頁,但提供圖片、CSS 和 JavaScript 時則不含 CA。遺憾的是,這些伺服器有時能提供您嘗試透過 Android 應用程式存取的 Web 服務,但這較不容許。

如要修正這個問題,請設定伺服器,在伺服器鏈結中加入中介 CA。大部分 CA 都會提供操作說明,告知如何針對常見的網路伺服器執行這項作業。

有關直接使用 SSLSocket 的警告

目前為止,所有示例都主要是對 HTTPS 使用 HttpsURLConnection。有時應用程式需要分開使用 TLS 和 HTTPS。舉例來說,電子郵件應用程式可能會使用 SMTP、POP3 或 IMAP 的 TLS 變化版本。在這種情況下,應用程式可以直接使用 SSLSocket,就如同在內部採用 HttpsURLConnection

上述用來解決憑證驗證問題的方式,同樣適用於 SSLSocket。事實上,使用自訂 TrustManager 時,傳遞至 HttpsURLConnection 的是 SSLSocketFactory。因此,如需搭配使用 SSLSocket 與自訂 TrustManager,請按照相同步驟操作,並使用該 SSLSocketFactory 建立 SSLSocket

注意:SSLSocket 不會執行主機名稱驗證。主機名稱驗證是由應用程式自行執行,建議您透過所需的主機名稱叫用 getDefaultHostnameVerifier()。另請注意,HostnameVerifier.verify() 不會針對錯誤擲回例外狀況,而是傳回布林值結果,請務必明確檢查。

已封鎖的 CA

TLS 需要使用 CA,才能只核發憑證給已驗證的伺服器和網域擁有者。在極少數情況下,CA 可能遭到欺騙,或在使用 ComodoDigiNotar 的情況下遭到破壞,導致主機名稱的憑證傳送給伺服器或網域擁有者以外的對象。

為降低這種風險,Android 可以將某些憑證,甚至整個 CA 列入拒絕清單。雖然這個拒絕清單一直內建於作業系統中,但自 Android 4.2 起,這個清單可以從遠端更新,方便處理日後可能發生的入侵問題。

限制應用程式只能使用特定憑證

注意:我們不建議對 Android 應用程式採用憑證綁定方法,這種做法是只將先前已授權的憑證視為有效的應用程式憑證。日後如有伺服器設定變更 (例如變更為其他 CA),會導致使用綁定憑證的應用程式必須收到用戶端軟體更新,才能連線至伺服器。

如要限制應用程式只接受您指定的憑證,請務必加入多個備用的綁定憑證,包括至少一組由您全權掌控的金鑰,並設定足夠短的效期,以免發生相容性問題。網路安全性設定提供這些功能的綁定方法。

用戶端憑證

本文著重介紹如何使用 TLS 保障與伺服器之間的通訊。TLS 也支援用戶端憑證的概念,供伺服器驗證用戶端身分。雖然其中牽涉的技術超出本文範圍,不過其實類似於指定自訂 TrustManager

Nogotofail:網路流量安全性測試工具

利用 Nogotofail 這項工具,您可以輕鬆確認應用程式是否不受已知的 TLS/SSL 安全漏洞和設定錯誤影響。這項自動化工具功能強大且可擴充,能在網路流量可供連線的任何裝置上測試網路安全性問題。

Nogotofail 適合用於以下三種主要用途:

  • 找出錯誤和安全漏洞。
  • 驗證修正結果及觀察迴歸問題。
  • 瞭解各應用程式和裝置產生的流量。

Nogotofail 適用於 Android、iOS、Linux、Windows、ChromeOS 和 macOS,而且其實可用於任何能連上網際網路的裝置。用戶端可在 Android 和 Linux 上調整設定及接收通知,攻擊引擎本身也可以部署為路由器、VPN 伺服器或 Proxy。

您可以在 Nogotofail 開放原始碼專案中使用這項工具。

SSL 和 TLS 更新

Android 10

Google Chrome 等部分瀏覽器會在 TLS 握手期間,趁著 TLS 伺服器傳送憑證要求訊息時,允許使用者選擇憑證。在 Android 10,KeyChain 物件會在呼叫 KeyChain.choosePrivateKeyAlias() 來向使用者顯示憑證選擇提示時,遵循頒發單位和金鑰規格參數。具體來說,此提示含有的選項皆符合伺服器規格。

如果沒有可供使用者選取的憑證,例如在憑證不符合伺服器規格,或裝置未安裝任何憑證的情況下,則系統根本不會顯示憑證選取提示。

此外,Android 10 以上版本不必設定裝置螢幕鎖定,即可將金鑰或 CA 憑證匯入 KeyChain 物件。

預設啟用 TLS 1.3

在 Android 10 以上版本中,所有 TLS 連線均預設啟用 TLS 1.3。以下是有關實作 TLS 1.3 的幾項重要細節:

  • TLS 1.3 加密套件不開放自訂。啟用 TLS 1.3 時,一律會啟用支援的 TLS 1.3 加密套件。若嘗試呼叫 setEnabledCipherSuites() 來停用這些功能,系統會予以忽略。
  • 協商 TLS 1.3 時,系統會先呼叫 HandshakeCompletedListener 物件,再將工作階段加入工作階段快取。(在 TLS 1.2 和其他舊版本中,這些物件是在工作階段新增至工作階段快取後才呼叫。)
  • 在某些情況下,如果 SSLEngine 例項在舊版 Android 上擲回 SSLHandshakeException,這些例項會在 Android 10 以上版本擲回 SSLProtocolException
  • 不支援 0-RTT 模式。

如有需要,您可以呼叫 SSLContext.getInstance("TLSv1.2"),取得已停用 TLS 1.3 的 SSLContext。您也可以對適當物件呼叫 setEnabledProtocols(),根據個別連線啟用或停用通訊協定版本。

使用 SHA-1 簽署的憑證不受 TLS 信任

在 Android 10 中,TLS 連線不會信任使用 SHA-1 雜湊演算法的憑證。根 CA 自 2016 年起就未曾核發這類憑證,Chrome 或其他主要瀏覽器也不再信任這類憑證。

如果連線目標為使用 SHA-1 憑證的網站,則任何連線嘗試都會失敗。

KeyChain 行為變更和強化措施

Google Chrome 等部分瀏覽器會在 TLS 握手期間,趁著 TLS 伺服器傳送憑證要求訊息時,允許使用者選擇憑證。在 Android 10,KeyChain 物件會在呼叫 KeyChain.choosePrivateKeyAlias() 來向使用者顯示憑證選擇提示時,遵循頒發單位和金鑰規格參數。具體來說,此提示含有的選項皆符合伺服器規格。

如果沒有可供使用者選取的憑證,例如在憑證不符合伺服器規格,或裝置未安裝任何憑證的情況下,則系統根本不會顯示憑證選取提示。

此外,Android 10 以上版本不必設定裝置螢幕鎖定,即可將金鑰或 CA 憑證匯入 KeyChain 物件。

其他 TLS 和密碼學變更

TLS 和密碼學程式庫有幾項小幅變更,會在 Android 10 生效:

  • AES/GCM/NoPadding 和 ChaCha20/Poly1305/NoPadding 加密演算法會從 getOutputSize() 傳回更準確的緩衝區大小。
  • 如果嘗試連線時使用的最高通訊協定版本為 TLS 1.2 以上版本,系統會省略 TLS_FALLBACK_SCSV 加密套件。TLS 伺服器實作程序已經過提升,因此不建議嘗試採用 TLS 外部備用機制,而應採用 TLS 版本協商。
  • ChaCha20-Poly1305 是 ChaCha20/Poly1305/NoPadding 的別名。
  • 以點結尾的主機名稱並非有效的 SNI 主機名稱。
  • 選擇憑證回應的簽署金鑰時,系統會依循 CertificateRequest 中的 supported_signature_algorithms 擴充功能。
  • 在 TLS 中,Android KeyStore 金鑰等不透明的簽署金鑰可以與 RSA-PSS 簽署搭配使用。

HTTPS 連線變更

如果執行 Android 10 的應用程式將空值傳遞至 setSSLSocketFactory(),就會發生 IllegalArgumentException。在先前版本中,將空值傳遞至 setSSLSocketFactory() 的效果與傳入目前預設工廠的效果相同。

Android 11

SSL 通訊端預設使用 Conscrypt SSL 引擎

Android 的預設 SSLSocket 實作項目是以 Conscrypt 為基礎。自 Android 11 起,該實作項目是以 Conscrypt SSLEngine 為基礎的內建功能。