客户端-服务器加密交互使用 传输层安全 (TLS) 来保护您的应用数据。
本文讨论了与安全网络协议最佳实践和 公钥基础设施 (PKI) (PKI) 注意事项相关的最佳实践。阅读 Android 安全概述 以及 权限概述 以获取更多详细信息。
概念
具有 TLS 证书的服务器具有公钥和匹配的私钥。服务器在 TLS 握手期间使用 公钥加密 对其证书进行签名。
简单的握手只能证明服务器知道证书的私钥。为了解决这种情况,让客户端信任多个证书。如果给定服务器的证书未出现在客户端的受信任证书集中,则该服务器不可信。
但是,服务器可能会使用密钥轮换来使用新的公钥更改其证书的公钥。服务器配置更改需要更新客户端应用。如果服务器是第三方 Web 服务(例如 Web 浏览器或电子邮件应用),则更难以知道何时更新客户端应用。
服务器通常依赖于 证书颁发机构 (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 示例
假设您有一个 Web 服务器,其证书由众所周知的 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)
这可能由于多种原因导致,包括
以下部分讨论了如何在保持与服务器连接安全的同时解决这些问题。
未知证书颁发机构
由于系统不信任 CA,因此会出现SSLHandshakeException
。这可能是因为您拥有来自 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 ---
这表明服务器发送了mail.google.com的证书,该证书由Thawte SGC CA(一个中间 CA)签发,以及由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 连接中不受信任。根 CA 自 2016 年以来就没有颁发此类证书,并且它们在 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_FALLBACK_SCSV 密码套件已从最大协议为 TLS 1.2 或更高版本的连接尝试中省略。由于 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 之上内部构建的。