通过网络协议确保安全

客户端和服务器之间的加密互动使用传输层安全协议 (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 颁发证书的网络服务器,那么,您可以使用如下代码发起安全的请求:

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,则无法阻止此类攻击。因此,请不要这样做,即使是暂时性的也不例外。而是将您的应用设置为信任服务器证书的颁发机构。

自签名的服务器证书

其次,由于自签名证书使得服务器成为自己的 CA,可能会出现 SSLHandshakeException。这与证书授权机构未知的情况相似,因此请修改应用的“网络安全配置”以信任自签名证书。

缺少中间证书授权机构

第三,由于缺少中间 CA,会出现 SSLHandshakeException。公共 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
---

这表明服务器会为 mail.google.com 发送一个由 Thawte SGC CA(中间 CA)颁发的证书,为 Thawte SGC CA 发送另一个由 Verisign CA(Android 信任的主要 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 后,就不会再需要在证书链中使用它。

有些网站会特意针对提供资源的辅助网络服务器执行此操作。为了节省带宽,它们会使用具有完整证书链的服务器提供主要的HTML页面,而用没有 CA 的服务器提供图片、CSS 和 JavaScript 等资源。这些服务器偶尔会提供您正尝试从 Android 应用访问的某种网络服务,但遗憾的是,它们无法获得应用信任。

如需解决此问题,请将服务器配置为在服务器链中加入中间 CA。大多数 CA 都可以提供有关如何为常用网络服务器执行此操作的说明。

有关直接使用 SSLSocket 的警告

到目前为止,所举示例都侧重于使用 HttpsURLConnection 的 HTTPS。有时,应用需要单独使用 TLS 与 HTTPS。例如,某个电子邮件应用可能使用 TLS 的变体 SMTP、POP3 或 IMAP。在这些情况下,应用可以直接使用 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),会导致具有固定证书的应用无法连接到服务器,除非进行客户端软件更新。

如需将应用限制为仅接受您指定的证书,请务必添加多个备用 PIN 码(其中至少包括一个完全由您控制的密钥),并设置足够短的有效期以防止兼容性问题。网络安全配置中提供了这些固定功能。

客户端证书

本文重点介绍了如何使用 TLS 来确保与服务器之间的通信安全。TLS 也支持客户端证书的概念,客户端证书允许服务器验证客户端的身份。虽然这超出了本文的范围,但其中涉及的技术与指定自定义 TrustManager 类似。

Nogotofail:网络流量安全测试工具

在已知的 TLS/SSL 漏洞和错误配置方面,可以通过 Nogotofail 轻松确认您的应用是否安全。它是一款自动执行的工具,功能强大并且可扩展,用于测试通过它传送网络流量的任意设备的网络安全问题。

Nogotofail 可用于三个主要用例:

  • 查找 bug 和漏洞。
  • 验证修复并监测回归。
  • 了解哪些应用和设备正在生成哪些流量。

Nogotofail 适用于 Android、iOS、Linux、Windows、ChromeOS 和 macOS 操作系统。事实上,任何用于连接互联网的设备都可以使用 Nogotofail。Android 和 Linux 上提供了一个用于配置设置和获取通知的客户端,以及一个本身可作为路由器、VPN 服务器或代理部署的攻击引擎。

您可以在 Nogotofail 开源项目网站上访问此工具。

SSL 和 TLS 更新

Android 10

当 TLS 服务器在 TLS 握手中发送证书请求消息时,某些浏览器(如 Google Chrome)允许用户选择证书。从 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(),从而为每个连接启用或停用协议版本。

TLS 不信任使用 SHA-1 签名的证书

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

如果某网站使用的是 SHA-1 证书,则任何尝试连接该网站的操作都将失败。

KeyChain 行为变更和改进

当 TLS 服务器在 TLS 握手中发送证书请求消息时,某些浏览器(如 Google Chrome)允许用户选择证书。从 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 密钥库中的密钥)可在 TLS 中与 RSA-PSS 签名一起使用。

HTTPS 连接变更

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

Android 11

SSL 套接字默认情况下使用 Conscrypt SSL 引擎

Android 的默认 SSLSocket 实现基于 Conscrypt。从 Android 11 开始,该实现是基于 Conscrypt 的 SSLEngine 在内部构建而成的。