客户端-服务器加密交互使用传输层安全协议 (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)
这可能由多种原因造成,包括:
以下部分将讨论如何在保持与服务器连接安全的同时解决这些问题。
未知证书颁发机构
出现 SSLHandshakeException
是因为系统不信任 CA。这可能是因为您拥有来自 Android 不信任的新 CA 的证书,或者因为您的应用在没有该 CA 的早期版本上运行。由于它是私有的,CA 很少为人所知。更常见的情况是,CA 未知是因为它不是公共 CA,而是由政府、公司或教育机构等组织为其自身用途颁发的私有 CA。
要信任自定义 CA,而无需更改应用的代码,请更改您的 网络安全配置。
警告:许多网站描述了一种糟糕的替代解决方案,即安装一个什么都不做的 TrustManager
。这样做会让您的用户在使用公共 Wi-Fi 热点时容易受到攻击,因为攻击者可以使用 DNS 技巧将用户的流量通过充当您的服务器的代理发送。然后,攻击者可以记录密码和其他个人数据。这是可行的,因为攻击者可以生成证书,如果没有 TrustManager
验证证书来自受信任的来源,则无法阻止此类攻击。因此,即使是暂时也不要这样做。相反,请让您的应用信任服务器证书的颁发者。
自签名服务器证书
其次,由于自签名证书,可能会出现 SSLHandshakeException
,这使得服务器成为它自己的 CA。这类似于未知证书颁发机构,因此请修改应用程序的网络安全配置以信任您的自签名证书。
缺少中间证书颁发机构
第三,由于缺少中间 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 ---
这表明服务器发送了由 *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
。因此,如果您需要将自定义 TrustManager
与 SSLSocket
一起使用,请按照相同的步骤操作,并使用该 SSLSocketFactory
创建您的 SSLSocket
。
警告:SSLSocket
**不执行** 主机名验证。由您的应用执行自身的主机名验证,最好是通过使用预期主机名调用 getDefaultHostnameVerifier()
。此外,请注意 HostnameVerifier.verify()
不会在出错时抛出异常。相反,它会返回一个布尔结果,您必须显式检查。
被阻止的 CA
TLS 依靠 CA 向已验证的服务器和域所有者颁发证书。在极少数情况下,CA 可能会被欺骗,或者像 Comodo 或 DigiNotar 一样遭到入侵,导致主机名的证书被颁发给服务器或域的所有者以外的人。
为了减轻这种风险,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 连接启用 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 年以来,根 CA 就没有再颁发此类证书,并且它们在 Chrome 或其他主要浏览器中也不再受信任。
如果连接的目标站点提供使用 SHA-1 的证书,则任何连接尝试都会失败。
密钥链行为变更和改进
某些浏览器(例如 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 扩展。
- 不透明签名密钥(例如来自 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 之上。