Android安全防护--Volley/OkHttp SSL Pinning(证书固定)可以这样做

上次写了iOS开发--在AFNetworking中实现 SSL pinning的文章
有兴趣的可以顺着网线爬过去看看哈
书接上一回
我手头上一个Android APP因为功能不是很复杂,也不会涉及到上传下载的功能,所以第一手Android开发团队用的是volley框架来进行网络通信。

Volley
Volley is an HTTP library that makes networking for Android apps easier and, most importantly, faster.

网上冲浪了一波也没有找到资料说volley有支持ssl pinning的功能
这个时候最该想到的就是开发文档爸爸which最靠谱儿
Android developers 网络安全配置文档提到👇

添加网络安全配置
借助网络安全配置功能,应用可以在一个安全的声明性配置文件中自定义其网络安全设置,而无需修改应用代码。您可以针对特定网域和特定应用配置这些设置。

  • 自定义信任锚:针对应用的安全连接自定义哪些证书授权机构 (CA) 值得信赖。例如,信任特定的自签名证书或限制应用信任的公共 CA 集。
  • 证书固定:限制应用仅安全连接到特定的证书。

跟随开发文档

4个步骤实现ssl pinning让你的app更安全

要向您的应用添加网络安全配置文件,请按以下步骤操作:

1. 获取服务器证书

我们需要pem 或 der 格式的自签名or(非)公共 CA 证书,可以跟服务器端索取。
如果你拿到的是.cer 证书,可以使用一下命令进行转换

openssl x509 -inform der -in 你的cer证书名字.cer -out 自定义输出的pem证书名字.pem 
api.pem

好,我们拿到证书了。

2. 将证书导入项目

将目录调整为Project模式显示
在app->src-res 目录下新建raw文件夹
直接将pem证书拖进去


image.png
3. 新建network_security_config.xml

新建app->src-res/xml/network_security_config.xml:

<?xml version="1.0" encoding="utf-8"?>
<network-security-config>
    <domain-config>
        <domain includeSubdomains="true">example.com</domain>
<!--        配置自定义 CA-->
<!--        假设您要连接到使用自签名 SSL 证书的主机,或者连接到其 SSL 证书是由您信任的非公共 CA(如公司的内部 CA)签发的主机。-->
        <trust-anchors>
            <certificates src="@raw/api"/>
        </trust-anchors>
<!--        固定证书-->
<!--        一般情况下,应用信任所有预装 CA。如果有预装 CA 签发欺诈性证书,则应用将面临被中间人攻击的风险。有些应用通过限制信任的 CA 集或通过固定证书,选择限制其接受的证书集。-->
        <!--        此外,可以设置证书固定的到期时间,在该时间之后不再固定证书。这有助于防止尚未更新的应用出现连接性问题。不过,设置证书固定的到期时间可能会绕过证书固定。-->
        <pin-set expiration="2021-01-01">
            <!--        要固定证书,您可以通过按公钥的哈希值(X.509 证书的 SubjectPublicKeyInfo)提供证书集。然后,只有至少包含一个已固定公钥的证书链才有效。-->
            <pin digest="SHA-256">7HIpactkIAq2Y49orFOOQKurWxZhBCoQYcRhJ3Y=</pin>
            <!-- backup pin -->
            <!--        请注意,固定证书时,您应始终包含一个备份密钥,这样,当您被强制切换到新密钥或更改 CA 时(固定到某个 CA 证书或该 CA 的中间证书时),应用的连接性不会受到影响。否则,您必须推送应用更新以恢复连接性。-->
            <pin digest="SHA-256">fwza0LRMXouZHRC8Ei+4Pyuld3UKgO/04cDM1oE=</pin>
        </pin-set>
    </domain-config>
</network-security-config>
关于怎样获得证书公钥的哈希值

将你的hostname拷贝到这个网站按submit就能看到

配置文件你还可以这样写:
限制可信 CA 集

如果应用不想信任系统信任的所有 CA,则可以自行指定,缩减要信任的 CA 集。这样可防止应用信任任何其他 CA 签发的欺诈性证书。
限制可信 CA 集的配置与针对特定网域信任自定义 CA 相似,不同的是,前者要在资源中提供多个 CA。

    <?xml version="1.0" encoding="utf-8"?>
    <network-security-config>
        <domain-config>
            <domain includeSubdomains="true">secure.example.com</domain>
            <domain includeSubdomains="true">cdn.example.com</domain>
            <trust-anchors>
                <certificates src="@raw/trusted_roots"/>
            </trust-anchors>
        </domain-config>
    </network-security-config>

以 PEM 或 DER 格式将可信 CA 添加到 res/raw/trusted_roots。请注意,如果使用 PEM 格式,文件必须仅包含 PEM 数据,没有额外的文本。您还可以提供多个 <certificates> 元素,而不是只提供一个元素。

信任其他 CA

应用可能需要信任系统不信任的其他 CA,出现此情况的原因可能是系统还未包含此 CA,或 CA 不符合添加到 Android 系统中的要求。应用可以通过为一个配置指定多个证书源来实现此目的。

    <?xml version="1.0" encoding="utf-8"?>
    <network-security-config>
        <base-config>
            <trust-anchors>
                <certificates src="@raw/extracas"/>
                <certificates src="system"/>
            </trust-anchors>
        </base-config>
    </network-security-config>
配置用于调试的 CA

调试通过 HTTPS 连接的应用时,您可能需要连接到没有为生产服务器提供 SSL 证书的本地开发服务器。若要无需应用代码而支持此操作,您可以通过使用 debug-overrides 来指定仅在 android:debuggabletrue 时才信任的仅调试 CA。通常,IDE 和编译工具会自动为非发布版本设置此标记。

这比一般的条件代码更安全,因为出于安全考虑,应用商店不接受被标记为可调试的应用。

    <?xml version="1.0" encoding="utf-8"?>
    <network-security-config>
        <debug-overrides>
            <trust-anchors>
                <certificates src="@raw/debug_cas"/>
            </trust-anchors>
        </debug-overrides>
    </network-security-config>
4. 在AndroidManifest.xml中指向上述网络安全配置文件network_security_config.xml

application节点上面添加👇

android:networkSecurityConfig="@xml/network_security_config"  

搞定!

如果你用的是okhttp

那就更简单了

创建一个CertificatePinner对象add一个假的哈希值,返回的exception会提供真正公钥的哈希值
这里要注意⚠️的是哈希值的长度是固定的,所以造假的哈希值 "sha256/"后面一定要整28个字符,否则报的exception会不一样

    public void okHttpRequest() {

        CertificatePinner certificatePinner = new CertificatePinner.Builder()
                .add("www.baidu.com", "sha256/AAAAAAAAAAAAAAAAAAAAAAAAAAA=")
                .build();
        OkHttpClient client = (new OkHttpClient.Builder()).certificatePinner(certificatePinner).build();

        //创建Request请求,这里是get
        Request request = new Request.Builder().url("https://www.baidu.com").get().build();

        //通过客户端创建Call
        Call call = client.newCall(request);
        //进行异步请求
        call.enqueue(new Callback() {
            @Override
            public void onFailure(Call call, IOException e) {
                Log.d("OkHttp", e.getMessage());
            }

            @Override
            public void onResponse(Call call, okhttp3.Response response) throws IOException {
                Log.d("OkHttp", response.body().string());
            }

        });
    }

获得真正的公钥哈希值之后,重新给CertificatePinner对象add上去

    public void okHttpRequest() {

        CertificatePinner certificatePinner = new CertificatePinner.Builder()
                .add("www.baidu.com", "sha256/YBo/npMPiC3PCrMqVUOvC+PTwfJ9iwLSapvdzSs4=")
                .add("www.baidu.com", "sha256/IQBnNBEiFuhj+8x6X8XLgh01V9Ic3IRQLNFFc7v4=")
                .add("www.baidu.com", "sha256/K87oWBWM9UZfyddvDfoxL+8lUB2ptGtn0fv6G2Q=")
                .add("www.baidu.com", "sha256/YBo/npMPiC3PCrMqVUOvC+PTfJ9iwLSapvdzSs41")
                .build();
        OkHttpClient client = (new OkHttpClient.Builder()).certificatePinner(certificatePinner).build();

        //创建Request请求,这里是get
        Request request = new Request.Builder().url("https://www.baidu.com").get().build();

        //通过客户端创建Call
        Call call = client.newCall(request);
        //进行异步请求
        call.enqueue(new Callback() {
            @Override
            public void onFailure(Call call, IOException e) {
                Looper.prepare();
                Toast.makeText(MainActivity.this, e.getMessage(), Toast.LENGTH_LONG).show();
                Looper.loop();
                Log.d("OkHttp", e.getMessage());
            }

            @Override
            public void onResponse(Call call, okhttp3.Response response) throws IOException {
                Looper.prepare();
                Toast.makeText(MainActivity.this, response.body().string(), Toast.LENGTH_LONG).show();
                Looper.loop();
                Log.d("OkHttp", response.body().string());
            }

        });
    }

Done!

这里要强调的是www.baidu.com只是用来举例的,我在iOS和Android分别创建了demo,用各种办法都pin不了百度这个网站,Charles和fiddle依然能抓到app请求的数据,原因可能是百度的证书我是直接在Chrome下的,anyway,如果你的项目上需要做ssl pinning,请直接向后台索取。

效果截图

这是我项目上的app做了ssl pinning的结果


Before.png
After.png

本文参考资料(感谢🙏)

网络安全配置(developers)
Volley网络请求框架使用
How can I implement SSL Certificate Pinning while using React Native

写作初心

梳理,积累,分享,交流

靴靴你能看到这里
欢迎交流
下一篇见 ᕕ(ᐛ)ᕗ