网络协议安全

客户端-服务器加密交互使用 传输层安全协议 (TLS) 来保护您应用的数据。

本文讨论与安全网络协议最佳实践和 公钥基础设施 (PKI) 考量相关的最佳实践。请阅读 Android 安全概览以及 权限概览 以获取更多详细信息。

概念

拥有 TLS 证书的服务器有一个公钥和一个匹配的私钥。服务器使用 公钥密码学 在 TLS 握手期间签署其证书。

一个简单的握手只能证明服务器知道证书的私钥。为了解决这种情况,让客户端信任多个证书。如果给定服务器的证书未出现在客户端的受信任证书集中,则该服务器不可信。

然而,服务器可能会使用密钥轮换将其证书的公钥更改为新的公钥。服务器配置更改需要更新客户端应用。如果服务器是第三方网络服务,例如网页浏览器或电子邮件应用,则更难知道何时更新客户端应用。

服务器通常依赖于 证书颁发机构 (CA) 证书来颁发证书,这使得客户端配置随时间推移更加稳定。CA 使用其私钥 签署 服务器证书。客户端随后可以检查服务器是否具有平台已知的 CA 证书。

受信任的 CA 通常列在主机平台上。Android 8.0(API 级别 26)包含 100 多个 CA,这些 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 颁发证书的 Web 服务器,您可以发出安全请求,如以下代码所示

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、使用代理、缓存响应等示例。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.

以下各节讨论如何在保持与服务器连接安全的同时解决这些问题。

未知证书颁发机构

SSLHandshakeException 的发生是因为系统不信任该 CA。这可能是因为您有一个 Android 不信任的新 CA 的证书,或者您的应用正在旧版本上运行而没有该 CA。由于是私有的,CA 很少为人所知。更多情况下,CA 未知是因为它不是公共 CA,而是由政府、公司或教育机构等组织出于自身用途颁发的私有 CA。

要在不需要更改应用代码的情况下信任自定义 CA,请更改您的网络安全配置

注意:许多网站描述了一种糟糕的替代解决方案,即安装一个不执行任何操作的 TrustManager。这样做会使您的用户在使用公共 Wi-Fi 热点时容易受到攻击,因为攻击者可以使用 DNS 技巧将您的用户流量通过伪装成您的服务器的代理发送。攻击者随后可以记录密码和其他个人数据。这之所以有效,是因为攻击者可以生成证书,如果没有 TrustManager 验证证书是否来自受信任的来源,您就无法阻止这种攻击。所以,即使是暂时也不要这样做。相反,让您的应用信任服务器证书的颁发者。

自签名服务器证书

其次,SSLHandshakeException 可能由于自签名证书而发生,这使得服务器成为其自身的 CA。这类似于未知证书颁发机构,因此请修改您的应用程序的网络安全配置以信任您的自签名证书。

缺少中间证书颁发机构

第三, SSLHandshakeException 的发生是由于缺少中间 CA。公共 CA 很少直接签署服务器证书。相反,根 CA 签署中间 CA。

为了降低泄露风险,CA 会将根 CA 保持离线状态。然而,Android 等操作系统通常只直接信任根 CA,这在服务器证书(由中间 CA 签名)和识别根 CA 的证书验证器之间留下了短暂的信任空白。

为了消除这个信任空白,服务器在 TLS 握手期间发送从服务器 CA 经过所有中间 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 后,浏览器将不再需要它出现在证书链中。

有些站点为了服务资源而故意这样做,例如辅助 Web 服务器。为了节省带宽,它们可能会从具有完整证书链的服务器提供其主 HTML 页面,但其图片、CSS 和 JavaScript 则不带 CA。不幸的是,偶尔这些服务器可能会提供您尝试从 Android 应用访问的 Web 服务,而 Android 应用在这方面并不那么宽容。

要解决此问题,请配置服务器以在服务器链中包含中间 CA。大多数 CA 都提供了针对常见 Web 服务器的操作说明。

直接使用 SSLSocket 的警告

到目前为止,示例都集中于使用 HttpsURLConnection 的 HTTPS。有时应用需要独立于 HTTPS 使用 TLS。例如,电子邮件应用可能会使用 SMTP、POP3 或 IMAP 的 TLS 变体。在这种情况下,应用可以直接使用 SSLSocket,就像 HttpsURLConnection 内部所做的那样。

到目前为止描述的用于处理证书验证问题的技术也适用于 SSLSocket。事实上,当使用自定义 TrustManager 时,传递给 HttpsURLConnection 的是一个 SSLSocketFactory。因此,如果您需要将自定义 TrustManagerSSLSocket 一起使用,请遵循相同的步骤并使用该 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 服务器或代理。

您可以在 Nogotofail 开源项目中访问该工具。

SSL 和 TLS 更新

Android 10

某些浏览器(例如 Google Chrome)允许用户在 TLS 服务器作为 TLS 握手的一部分发送证书请求消息时选择证书。从 Android 10 开始,KeyChain 对象在调用 KeyChain.choosePrivateKeyAlias() 以向用户显示证书选择提示时,会遵守颁发者和密钥规范参数。特别是,此提示不包含不符合服务器规范的选择。

如果没有用户可选择的证书可用(例如,当没有证书与服务器规范匹配或设备未安装任何证书时),则根本不会出现证书选择提示。

此外,在 Android 10 或更高版本上,导入密钥或 CA 证书到 KeyChain 对象不再需要设备屏幕锁定。

TLS 1.3 默认启用

在 Android 10 及更高版本中,TLS 1.3 默认对所有 TLS 连接启用。以下是关于我们 TLS 1.3 实现的一些重要细节

  • TLS 1.3 密码套件无法自定义。当 TLS 1.3 启用时,支持的 TLS 1.3 密码套件始终启用。任何通过调用 setEnabledCipherSuites() 禁用它们的尝试都将被忽略。
  • 当 TLS 1.3 协商时,HandshakeCompletedListener 对象会在会话添加到会话缓存之前被调用。(在 TLS 1.2 和其他先前版本中,这些对象会在会话添加到会话缓存之后被调用。)
  • 在 Android 10 及更高版本上,在某些情况下,SSLEngine 实例在 Android 的早期版本上会抛出 SSLHandshakeException,而现在会抛出 SSLProtocolException
  • 不支持 0-RTT 模式。

如果需要,您可以通过调用 SSLContext.getInstance("TLSv1.2") 来获取一个已禁用 TLS 1.3 的 SSLContext。您还可以通过在适当的对象上调用 setEnabledProtocols() 来按连接启用或禁用协议版本。

SHA-1 签名的证书在 TLS 中不受信任

在 Android 10 中,使用 SHA-1 哈希算法的证书在 TLS 连接中不受信任。自 2016 年以来,根 CA 就不再颁发此类证书,并且在 Chrome 或其他主流浏览器中也不再受信任。

如果连接到使用 SHA-1 呈现证书的站点,任何尝试连接都将失败。

KeyChain 行为变更和改进

某些浏览器(例如 Google Chrome)允许用户在 TLS 服务器作为 TLS 握手的一部分发送证书请求消息时选择证书。从 Android 10 开始,KeyChain 对象在调用 KeyChain.choosePrivateKeyAlias() 以向用户显示证书选择提示时,会遵守颁发者和密钥规范参数。特别是,此提示不包含不符合服务器规范的选择。

如果没有用户可选择的证书可用(例如,当没有证书与服务器规范匹配或设备未安装任何证书时),则根本不会出现证书选择提示。

此外,在 Android 10 或更高版本上,导入密钥或 CA 证书到 KeyChain 对象不再需要设备屏幕锁定。

其他 TLS 和密码学变更

Android 10 中 TLS 和密码学库有几项次要更改生效

  • 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 扩展。
  • 不透明签名密钥(例如来自 Android Keystore 的密钥)可用于 TLS 中的 RSA-PSS 签名。

HTTPS 连接更改

如果运行 Android 10 的应用将 null 传递给 setSSLSocketFactory(),则会发生 IllegalArgumentException。在以前的版本中,将 null 传递给 setSSLSocketFactory() 的效果与传入当前 默认工厂 的效果相同。

Android 11

SSL 套接字默认使用 Conscrypt SSL 引擎

Android 的默认 SSLSocket 实现基于 Conscrypt。自 Android 11 以来,该实现内部构建在 Conscrypt 的 SSLEngine 之上。