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

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

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

概念

具有 TLS 憑證的伺服器具有公開金鑰和相符的私密金鑰。伺服器會在傳輸層安全標準 (TLS) 握手期間使用公開金鑰密碼編譯來簽署憑證。

簡單的握手只能證明伺服器知道憑證的私密金鑰。為解決這個問題,請讓用戶端信任多個憑證。如果特定伺服器的憑證未出現在用戶端的信任憑證組合中,即表示該伺服器不值得信任。

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

伺服器通常會依賴憑證授權單位 (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,請變更網路安全性設定

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

自我簽署伺服器憑證

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

缺少中繼憑證授權單位

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

為了降低遭入侵的風險,CA 會讓根 CA 離線。不過,Android 等作業系統通常只會直接信任根 CA,在伺服器憑證 (由中繼 CA 簽署) 與憑證驗證器之間產生短信任差距。

為了消除這種信任差距,伺服器會在 TLS 握手期間,透過任何中繼憑證從伺服器 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。舉例來說,以下伺服器可能會導致 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。瀏覽器從某個網站得知中繼憑證授權單位後,瀏覽器就不需要在憑證鏈中再次使用該憑證授權單位。

有些網站刻意進行這項操作,以處理提供次要資源的次要網路伺服器。為節省頻寬,他們可以透過具備完整憑證鏈結的伺服器提供主要 HTML 網頁,但不含 CA 中的圖片、CSS 和 JavaScript。然而,這些伺服器有時可能提供您嘗試透過 Android 應用程式存取的網路服務,但這並不適合透過 Android 應用程式存取。

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

直接使用 SSLSocket 的警告

目前為止,示例以使用 HttpsURLConnection 的 HTTPS 為主。有時應用程式需要使用傳輸層安全標準 (TLS) 和 HTTPS。舉例來說,電子郵件應用程式可能會使用 SMTP、POP3 或 IMAP 的 TLS 變體。在這種情況下,應用程式可以直接使用 SSLSocket,與 HttpsURLConnection 內部的方式大致相同。

此處所說明可處理憑證驗證問題的技術也適用於 SSLSocket。事實上,使用自訂 TrustManager 時,傳遞至 HttpsURLConnectionSSLSocketFactory。因此,如果您需要搭配 SSLSocket 使用自訂 TrustManager,請按照相同步驟,並使用該 SSLSocketFactory 建立 SSLSocket

注意:SSLSocket「不會」執行主機名稱驗證。主機名稱驗證完全由您決定,最好透過所需的主機名稱呼叫 getDefaultHostnameVerifier()。另請注意,HostnameVerifier.verify() 不會在錯誤上擲回例外狀況。而是傳回布林值結果,您必須明確檢查。

已封鎖的 CA

TLS 需依賴 CA 只核發憑證給伺服器和網域的已驗證擁有者。在極少數情況下,憑證授權單位可能會受騙,或是在 ComodoDigiNotar 的情況下遭到破壞,導致主機名稱的憑證核發給伺服器或網域擁有者以外的人。

為降低這種風險,Android 可以將特定憑證,甚至是整個 CA 加入拒絕清單。雖然這份清單一直是作業系統內建的功能,但自 Android 4.2 版起,這份清單可以遠端更新,以因應未來可能出現的威脅。

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

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

如果您想限制應用程式只接受您指定的憑證,請務必加入多個備用 PIN 碼,包括至少一個可全權掌控的金鑰,以及足夠的有效期限,以免發生相容性問題。網路安全性設定提供的是這些功能的固定項目。

用戶端憑證

本文的重點是介紹如何使用傳輸層安全標準 (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 中,使用 SHA-1 雜湊演算法的憑證不會在傳輸層安全標準 (TLS) 連線中受到信任。 根憑證授權單位自 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 為基礎。