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。