记Android开发中证书链问题的解决

在之前的帖子 问个关于Android模拟器的问题 中提到了因为Android证书链问题,导致React Native中AJAX功能无法访问由TrustAsia签发的SSL证书配置的HTTPS后端服务的问题,现在给出一个问题排查过程和解决方法。

以下内容节选自本人博客文章,文章完整链接附在文后。

在Android开发中遇到的问题

其实在Android开发过程中遇到的这个问题其实非常具有误导性。我的Android应用是使用React Native开发的,其中有一些功能需要访问RESTful形式的数据服务,所以也就需要使用fetch来完成基于HTTP的访问功能。为了方便调试,我将数据服务通过FRP映射到了用于开发的域名上,并且启用了HTTPS。然后在Android模拟器中运行的时候,fetch函数就报出了一个令人莫名其妙的Network request failed错误。

这个错误乍一看起来像是模拟器的网络不可用,而且模拟器上的Wifi和移动网络上的确也是挂着一个!标记。既然存在错误使设计功能不能正常运行,那么就需要研究一下了。

确定网络问题

经过在网络上的搜索,很多答案都集中到了模拟器的DNS设置上。因为模拟器的DNS始终展现为10.0.2.3,无法确定其是不是存在问题,所以只能通过adb shell来确认一下。

为了避免可能存在的问题,我在启动模拟器的时候就已经使用-dns-server "114.114.114.114,8.8.8.8"的参数为模拟器设置了DNS服务器,从院里上来说,如果这时在Shell里PING一些Ineternet上的网站,应该是没有问题的。

为了简化问题的排查,我直接选择了PING数据服务所在的域名。结果毫无疑问,网络是通畅的,不存在网络链路上的问题。

确定中间件问题

既然不是网络本身的问题,那么问题就集中在应用本身或者系统上了。

首先考虑的是应用中用来完成数据服务访问的是fetch,不是其他项目中常见的axios。所以更换一下数据访问的支持库来确定一下是否fetch在功能和实现上与其他项目中反映没有问题的axios存在区别。

替换的过程很简单,结果也很明确,更换成的axios同样报出了Network request failed错误。

尝试使用基于HTTP的明文数据服务

在网络上有一个搜索结果是反映,如果数据服务使用的是HTTP明文传输的话,是不存在这种问题的。那么这是可以比较容易验证的,只需要给数据服务主机上的Nginx配置调整一下即可。

这一次试验的结果,基本上把问题而核心锁定在了SSL证书上。因为在切换成使用基于HTTP的明文数据服务以后,fetch可以正常与数据服务通讯了。

尝试使用Native Module确定问题所在

fetch封装了非常多的内容,不可能用来完成进一步针对SSL证书的调试。想要完成进一步的检查,还需要使用更加贴近底层的方法。对于React Native来说,那就是使用OkHttp编写一个Native Module,自行控制网络访问。

如何编写一个基于OkHttp的Native Module,在网络上的示例很多,甚至ChatGPT也能给出一个能够直接使用的简短示例,所以这里不再赘述。

在使用Native Module访问基于HTTPS的数据服务以后,应用报出的错误发生了变化,从原来的Network request failed,变成了java.security.cert.CertPathValidatorException: Trust anchor for certification path not found。这种提示就已经非常明确了:无法找到可信的证书验证路径。换句话就是证书链不存在。

解决证书链不存在的问题

其实这个问题不是App或者React Native亦或是哪个中间件的问题,而是Android中存在的问题。这主要受Android严格的外部数据访问策略影响的,现目前比较常用的Android版本中,Android要求外部数据服务必须是基于HTTPS的。

这并不是说Android不允许访问基于HTTP的数据服务,而是如果要使用基于HTTP的数据服务,需要进行额外的配置。

但是现在能够提供免费SSL证书的机构有不少,这实际上对数据安全造成了一定的漏洞。为了安全起见,Androi系统中就没有预置这些机构的CA证书。所以如果数据服务使用的是这些证书颁发机构提供的SSL证书,那么在Android中就一定会报出这个缺少证书链的错误。

要解决这个问题主要有两种方法:

  1. 数据服务降级到HTTP使用,放弃更加安全的HTTPS通讯。
  2. 找一个能够补充证书链的方法。

很显然,第一种方法比较省事,但这并不是我所需要的。所以目标就集中在了如何向Android系统提供有效的证书链上。所幸,我所使用的证书颁发机构提供了一个现成的证书链,在我的文件系统中,这个证书链的文件名是fullchain.cert,看名字就知道这是服务器SSL证书的完整证书链。

在Android 7.0(API 24)以上的版本中,提供了一个细粒度管理网络安全的设置。这个配置需要在Android项目的res/xml目录中放置一个名为network_security_config.xml的文件来描述网络安全方面的配置。在这个文件中有一项功能就是为应用添加证书链,为了方便引用证书链文件,现在将fullchain.cert文件放置到Android项目目录的res/raw文件夹下。然后就可以编写以下配置。

首先是编写network_security_config.xml文件。

<?xml version="1.0" encoding="utf-8"?>
<network-security-config>
  <base-config cleartextTrafficPermitted="true">
    <trust-anchors>
      <certificates src="system" />
      <certificates src="@raw/fullchain" />
    </trust-anchors>
  </base-config>
</network-security-config>

在这个配置文件中声明了cleartexttrafficPermittedtrue,这是因为在React Native开发过程中有一些功能需要使用到明文传输,如果设置为false可能会出现应用无法加载的问题。配置文件中通过<trust-anchors>来配置仅针对应用真神起效的证书链,也就是应用本身信任的证书链。

这里将服务器CA证书颁发机构提供的证书链文件通过<certificates>引入,就完成了应用所使用证书链的扩展。接下来就是在AndroidManifest.xml文件中登记新编写的network_security_config.xml文件配置。

AndroidManifest.xml文件中找到<application>元素,在其中添加android:networkSecurityConfig配置项。

<manifest xmlns:android="http://schemas.android.com/apk/res/android">

  <uses-permission android:name="android.permission.INTERNET" />
  <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
  <uses-permission android:name="android.permission.READ_PHONE_STATE" />

  <application
      android:networkSecurityConfig="@xml/network_security_config"
      android:name=".MainApplication"
      android:label="@string/app_name"
      android:icon="@mipmap/ic_launcher"
      android:roundIcon="@mipmap/ic_launcher_round"
      android:allowBackup="false"
      android:theme="@style/AppTheme">
    <activity
        android:name=".MainActivity"
        android:label="@string/app_name"
        android:configChanges="keyboard|keyboardHidden|orientation|screenLayout|screenSize|smallestScreenSize|uiMode"
        android:launchMode="singleTask"
        android:windowSoftInputMode="adjustResize"
        android:exported="true">
      <intent-filter>
        <action android:name="android.intent.action.MAIN" />
        <category android:name="android.intent.category.LAUNCHER" />
      </intent-filter>
    </activity>
  </application>
</manifest>

完成增加这个配置以后,重新启动React Native应用,现在fetch已经可以正常的访问基于HTTPS的数据服务了。

以下是博客文章的链接,转载请注明来源。

ArchGrid - 架构知识网格 - 记一次处理Android证书链的问题

PS:本人博客已经增加了友情链接板块,欢迎交换链接。

3 个赞

实践出真知

1 个赞

From #dev to 开发调优