PKI 体系依赖证书执行极为关键的身份验证,以此确认服务端的可信任性。证书验证在 SSL/TLS 握手过程中完成,验证过程通常包含三个步骤:
验证证书的合法性:这一步主要是验证证书是由合法有效的 CA 签发的。在客户端预先保存一个可靠的 CA 的根证书库,比如 FiexFox、Chrome、Android、Microsoft 等都有维护自己的根证书库,并据此验证服务端证书链的合法性。PKI 体系借助于可靠的中心化身份验证系统,即 CA,为服务端的身份合法性背书。根证书库的安全是 PKI 系统正常工作非常关键的部分。
验证证书域名的匹配性:服务端的证书都是为特定域名签发的,证书就像是网站的身份证一样。通过验证域名匹配性,可以有效的防止身份的仿冒,比如经营着 A 网站的经营者,拦截用户请求,并冒充 B 网站的身份,盗取信息。如果客户端不对域名的匹配性做检查,则将造成极大的攻击面,拿到任何一个域名的合法证书的人都将可以仿冒目标服务器。
证书钉扎验证:这是 PKI 体系中比较新的一种增强安全性的机制。目前的证书签发机构 CA 非常多,总数大概有几百个上千个,每个 CA 都可以为任何域名签发合法有效的证书,因而众多的 CA 就造成了非常大的攻击面。比如某个 CA 被攻破,或者犯了其它什么错误,为攻击者签发了 www.google.com 等域名的证书,则攻击者将可以仿冒这些网站。证书钉扎机制正是为了解决这一问题而产生——证书钉扎机制中,在客户端将特定域名的证书与特定的签发者绑定,即客户端只承认特定签发者签发的某个域名的证书,而不承认其它 CA 为该域名签发的证书。通过这种方式,来解除大量 CA 这个攻击面的威胁。
在 Android 系统的 Java 应用程序中,证书验证通常由不同层面的多个组件完成。第一步的证书合法性验证,主要由 Java 标准库的 javax.net.ssl.SSLSocket 在 startHandshake() 方法中完成,后面两个步骤由更上层的组件完成,比如 HTTPS 库 OkHttp 等。
本文主要讨论 Android 中根证书库的管理和证书的合法性验证。(本文分析说明主要依据 android-7.1.1/android-7.1.2 系统的行为,可以通过 Google 的 OpenGrok 服务器 阅读 Android 系统的源码。)
Android 的根证书管理
在 AOSP 源码库中,CA 根证书主要存放在 system/ca-certificates 目录下,而在 Android 系统中,则存放在 /system/etc/security/ 目录下,以 Android 7.1.1 系统的 Pixel 设备为例:
其中 cacerts_google 目录下的根证书,主要用于 system/update_engine、external/libbrillo 和 system/core/crash_reporter 等模块,cacerts 目录下的根证书则用于所有的应用。cacerts 目录下的根证书,即 Android 系统的根证书库,像下面这样:
它们都是 PEM 格式的 X.509 证书。Android 系统通过 SystemCertificateSource、DirectoryCertificateSource 和 CertificateSource 等类管理系统根证书库。CertificateSource定义(位于frameworks/base/core/java/android/security/net/config/CertificateSource.java)了可以对根证书库执行的操作,主要是对根证书的获取和查找:
DirectoryCertificateSource 类则基于文件系统上分开存放的根证书文件的形式保存的根证书库,提供证书的创建、获取和查找操作,这个类的定义(位于frameworks/base/core/java/android/security/net/config/DirectoryCertificateSource.java)如下:
获取根证书库的 getCertificates() 操作在第一次被调用时,遍历文件系统,并加载系统所有的根证书文件,并缓存起来,以备后面访问。根证书的查找操作,主要依据证书文件的文件名进行,证书文件被要求以 [SubjectName 的哈希值].[Index] 的形式命名。
SystemCertificateSource 类主要定义(位于frameworks/base/core/java/android/security/net/config/SystemCertificateSource.java)了系统根证书库的路径,以及无效一个根证书的机制:
Android 系统的根证书位于 /system/etc/security/cacerts/ 目录下。用户可以通过将特定根证书复制到用户配置目录的 cacerts-removed 目录下来无效一个根证书。
Android framework 还提供了另外一个用于加载并访问用户根证书库的组件 UserCertificateSource,这个类的定义(位于 frameworks/base/core/java/android/security/net/config/UserCertificateSource.java)如下:
这个组件与 SystemCertificateSource 类似,只是它定义了用户根证书库的路径。
相关的几个组件结构如下图:

证书链合法性验证
有了根证书库之后,根证书库又是如何被用于 SSL/TLS 握手的证书验证过程的呢?
证书的合法性由 Java 标准库的 javax.net.ssl.SSLSocket 在 startHandshake() 方法中完成。对于 Android 系统而言,SSLSocket 基于 OpenSSL 库实现,这一实现由 external/conscrypt 模块提供,SSLSocket 的实现为 OpenSSLSocketImpl 类(位于external/conscrypt/src/main/java/org/conscrypt/OpenSSLSocketImpl.java)。
OpenSSLSocketImpl.startHandshake() 中的 SSL/TLS 握手是一个极为精巧的过程,我们略过详细的握手过程,主要关注证书验证的部分。
OpenSSLSocketImpl.startHandshake() 通过 NativeCrypto 类(位于external/conscrypt/src/main/java/org/conscrypt/NativeCrypto.java)中的静态本地层方法 SSL_do_handshake() 方法执行握手操作:
NativeCrypto 类内部定义了一组将会在本地层由与 SSL 握手相关的 OpenSSL C/C++ 代码调用的回调 SSLHandshakeCallbacks,在上面的 SSL_do_handshake() 方法中,这组回调作为参数传入本地层。
SSLHandshakeCallbacks 定义如下:
其中 verifyCertificateChain() 回调用于服务端证书的验证。Android 系统通过这一回调,将根证书库的管理模块和底层 OpenSSL 的 SSL/TLS 握手及身份验证连接起来。
SSLHandshakeCallbacks 回调由 OpenSSLSocketImpl 实现,verifyCertificateChain() 的实现如下:
OpenSSLSocketImpl 的 verifyCertificateChain() 从 sslParameters 获得 X509TrustManager,然后在 Platform.checkServerTrusted() (com.android.org.conscrypt.Platform,位于 external/conscrypt/src/compat/java/org/conscrypt/Platform.java)中执行服务端证书合法有效性的检查:
Platform.checkServerTrusted() 通过执行 X509TrustManager 的 checkServerTrusted() 方法执行证书有合法性检查。
X509TrustManager 来自于 OpenSSLSocketImpl 的 sslParameters,那 sslParameters 又来自于哪里呢?OpenSSLSocketImpl 的 sslParameters 由对象的创建者传入:
也就是说,OpenSSLSocketImpl 的 sslParameters 来自于 javax.net.ssl.SSLSocketFactory,即 OpenSSLSocketFactoryImpl。OpenSSLSocketFactoryImpl 定义(位于 external/conscrypt/src/main/java/org/conscrypt/OpenSSLSocketFactoryImpl.java)如下:
OpenSSLSocketImpl 最主要的职责,即是将 SSL/TLS 参数 SSLParametersImpl 与 SSLSocket 粘起来。主要来看默认情况下 SSLParametersImpl 的 X509TrustManager 是什么(位于external/conscrypt/src/main/java/org/conscrypt/SSLParametersImpl.java ):
将 createDefaultX509TrustManager() 的代码复制到我们的应用程序中,就像下面这样:
在应用程序执行时打断点,借助于 Android Studio 确认系统默认的 X509TrustManager 是什么,不难确认,它是 android.security.net.config.RootTrustManager。android.security.net.config.RootTrustManager 的 checkServerTrusted() 定义(位于 frameworks/base/core/java/android/security/net/config/RootTrustManager.java)如下:
NetworkSecurityConfig 的 getTrustManager() 定义(位于 frameworks/base/core/java/android/security/net/config/NetworkSecurityConfig.java)如下:
NetworkSecurityConfig 将管根证书库的组件 SystemCertificateSource 、 UserCertificateSource 和执行证书合法性验证的 NetworkSecurityTrustManager 粘起来:
同时 NetworkSecurityConfig 还提供了一些根据特定条件查找根证书的操作:
真正执行证书合法性验证的还不是 NetworkSecurityTrustManager,而是 TrustManagerImpl(位于 external/conscrypt/src/platform/java/org/conscrypt/TrustManagerImpl.java),由 NetworkSecurityTrustManager 的定义(位于frameworks/base/core/java/android/security/net/config/NetworkSecurityTrustManager.java)不难看出这一点:
TrustedCertificateStoreAdapter 为根证书库提供了 TrustedCertificateStore 接口的查找操作,以方便 TrustManagerImpl 使用(位于frameworks/base/core/java/android/security/net/config/TrustedCertificateStoreAdapter.java):
不难看出 Android 中 Java 层证书验证的过程如下图所示:
OpenSSLSocketImpl.startHandshake() 和 NativeCrypto.SSL_do_handshake() 执行完整的 SSL/TLS 握手过程。证书合法性验证作为 SSL/TLS 握手的一个重要步骤,通过本地层调用的 Java 层的回调方法 SSLHandshakeCallbacks.verifyCertificateChain() 完成,OpenSSLSocketImpl 实现这一回调。OpenSSLSocketImpl.verifyCertificateChain()、Platform.checkServerTrusted()、RootTrustManager.checkServerTrusted() 和NetworkSecurityTrustManager.checkServerTrusted() 用于将真正的根据系统根证书库执行证书合法性验证的 TrustManagerImpl 和 SSL/TLS 握手过程粘起来。OpenSSLSocketFactoryImpl 将 OpenSSLSocketImpl 和 SSLParametersImpl 粘起来。SSLParametersImpl 将 OpenSSLSocketImpl 和 RootTrustManager 粘起来。
NetworkSecurityConfig 将 RootTrustManager 和 NetworkSecurityTrustManager 粘起来。NetworkSecurityConfig、NetworkSecurityTrustManager 和 TrustedCertificateStoreAdapter 将 TrustManagerImpl 和管理系统根证书库的 SystemCertificateSource 粘起来。
TrustManagerImpl 是证书合法性验证的核心,它会查找系统根证书库,并对服务端证书的合法性做验证。
这个过程的调用栈如下:
还有两个问题,一是 SSLParametersImpl 是如何找到的 RootTrustManager;二是如何定制或者影响证书合法性的验证过程。
TrustManager 的查找
Java 加密体系架构(JCA)是一个非常灵活的架构,它的整体结构如下图:

Java 应用程序通过接口层访问加密服务,接口层的组成包括 JAAS(Java Authentication Authorization Service,Java验证和授权API)、JSSE(Java Secure Socket Extension,Java 安全 套接字扩展)、JGSS(Java Generic Security Service )和 CertPath等。具体的组件如我们前面看到的 CertificateFactory、TrustManagerFactory 和 SSLSocketFactory 等。
JCA 还定义了一组加密服务 Provider 接口,如 javax.net.ssl.SSLContextSpi 和 javax.net.ssl.TrustManagerFactorySpi 等。加密服务的实现者实现这些接口,并通过 java.security.Security 提供的接口注册进 JCA 框架。
对于 Android 系统来说,TrustManagerFactory 加密服务的注册是在 ActivityThread 的 handleBindApplication() 中做的,相关代码(位于 frameworks/base/core/java/android/app/ActivityThread.java)如下:
NetworkSecurityConfigProvider 类的定义(位于 frameworks/base/core/java/android/security/net/config/NetworkSecurityConfigProvider.java)如下:
在 NetworkSecurityConfigProvider.install() 方法中,通过 Security.insertProviderAt() 将 NetworkSecurityConfigProvider 注册进 JCA 框架中。从 NetworkSecurityConfigProvider 的构造函数可以看到,它将 android.security.net.config.RootTrustManagerFactorySpi 带进 JCA 框架。
android.security.net.config.RootTrustManagerFactorySpi 的定义(位于 frameworks/base/core/java/android/security/net/config/RootTrustManagerFactorySpi.java)如下:
RootTrustManagerFactorySpi 的 TrustManager 来自于 ApplicationConfig,ApplicationConfig 中 TrustManager 相关的代码(位于 frameworks/base/core/java/android/security/net/config/ApplicationConfig.java)如下:
ApplicationConfig 的 TrustManager 是 RootTrustManager。
再来看 JCA 接口层的 javax.net.ssl.TrustManagerFactory 的定义:
TrustManagerFactory 通过 JCA 框架提供的 sun.security.jca.GetInstance 找到注册的 javax.net.ssl.TrustManagerFactorySpi。应用程序通过 javax.net.ssl.TrustManagerFactory -> android.security.net.config.RootTrustManagerFactorySpi -> android.security.net.config.ApplicationConfig 得到 android.security.net.config.RootTrustManager,即 X509TrustManager。
私有 CA 签名证书的应用
自签名证书是无需别的证书为其签名来证明其合法性的证书,根证书都是自签名证书。私有 CA 签名证书则是指,为域名证书签名的 CA,其合法有效性没有得到广泛的认可,该 CA 的根证书没有被内置到系统中。
在实际的开发过程中,有时为了节省昂贵的购买证书的费用,而想要自己给自己的服务器的域名签发域名证书,这即是私有 CA 签名的证书。为了能够使用这种证书,需要在客户端预埋根证书,并对客户端证书合法性验证的过程进行干预,通过我们预埋的根证书为服务端的证书做合法性验证,而不依赖系统的根证书库。
自定义 javax.net.ssl.SSLSocket 的代价太高,通常不会通过自定义 javax.net.ssl.SSLSocket 来修改服务端证书的合法性验证过程。以此为基础,从上面的分析中不难看出,要想定制 OpenSSLSocketImpl 的证书验证过程,则必然要改变 SSLParametersImpl,要改变 OpenSSLSocketImpl 的 SSLParametersImpl,则必然需要修改 SSLSocketFactory。修改 SSLSocketFactory 常常是一个不错的方法。
在 Java 中,SSLContext 正是被设计用于这一目的。创建定制了 SSLParametersImpl,即定制了 TrustManager 的 SSLSocketFactory 的方法如下:
SSLContext 的相关方法实现(位于libcore/ojluni/src/main/java/javax/net/ssl/SSLContext.java)如下:
其中 SSLContextSpi 为 OpenSSLContextImpl,该类的实现(位于external/conscrypt/src/main/java/org/conscrypt/OpenSSLContextImpl.java)如下:
如我们前面讨论,验证服务端证书合法性是 PKI 体系中,保障系统安全极为关键的环节。如果不验证服务端证书的合法性,则即使部署了 HTTPS,HTTPS 也将形同虚设,毫无价值。因而在我们自己实现的 X509TrustManager 中,加载预埋的根证书,并据此验证服务端证书的合法性必不可少,这一检查在 checkServerTrusted() 中完成。然而为了使我们实现的 X509TrustManager 功能更完备,在根据我们预埋的根证书验证失败后,我们再使用系统默认的 X509TrustManager 做验证,像下面这样:
此外,也可以不自己实现 X509TrustManager,而仅仅修改 X509TrustManager 所用的根证书库,就像下面这样:
自己实现 X509TrustManager 接口和通过 TrustManagerFactory,仅定制 KeyStore 这两种创建 X509TrustManager 对象的方式,当然是后一种方式更好一些了。如我们前面看到的,系统的 X509TrustManager 实现 RootTrustManager 集成自 X509ExtendedTrustManager,而不是直接实现的 X509TrustManager 接口 。JCA 的接口层也在随着新的安全协议和 SSL 库的发展在不断扩展,在具体的 Java 加密服务实现中,可能会实现并依赖这些扩展的功能,如上面看到的 X509TrustManager,而且加密服务的实现中常常通过反射,来动态依赖一些扩展的接口。因而,自己实现 X509TrustManager 接口时,以及其它加密相关的接口时,如 SSLSocket 等,可能会破坏一些功能。
很多时候可以看到,为了使用私有 CA 签名的证书,而定制域名匹配验证的逻辑,即自己实现 HostnameVerifier。不过通常情况下,网络库都会按照规范对域名与证书的匹配性做严格的检查,因而不是那么地有必要,除非域名证书有什么不那么规范的地方。
关于证书钉扎,在使用私有 CA 签名的证书时,通常似乎也没有那么必要。
参考文章:
Android https 自定义 证书 问题
Android实现https网络通信之添加指定信任证书/信任所有证书
HTTPS(含SNI)业务场景“IP直连”方案说明
HTTP Public Key Pinning 介绍
Java https请求 HttpsURLConnection
Done。