SSL cert chain validation (platform fallback) not working with (domain) network security config(xml)

背景介绍

在Android平台,当我们使用HTTPs(SSL)进行网络请求的时候,需要配置network_security_config.xml,如果我们在一个或者多个domain-config节点中配置网络安全属性,例如cert pinning,系统的SSL校验程序(verity_cert_chain_platform_specific)就会爆出JNI异常(CertificateException)。下面是一个network_security_config示例

<network-security-config>
    <domain-config cleartextTrafficPermitted="false">
        <domain includeSubdomains="true">azurewebsites.net</domain>
        <pin-set expiration="2021-09-24">
            <pin digest="SHA-256">...</pin>
            <pin digest="SHA-256">...</pin>
        </pin-set>
    </domain-config>

</network-security-config>

有一点需要注意,上述异常的发生场景如下:默认的OpenSSL校验方法失败了,而我们希望系统校验程序(verify_cert_chain_platform_specific)可以帮我们兜底的时候,异常就发生了。

原因分析

https://github.com/microsoft/cpprestsdk/blob/96e7d20e398b629de2935f9ac32cfa2780cd0b0b/Release/src/http/client/x509_cert_utilities.cpp#L268 这里的代码调用通过工厂方法获得的X509TrustManager/checkServerTrusted。但是被调用的重载不包含hostName参数。在本例中,X509CertificateManager“解析”为RootTrustManager(aosp/system/frameworks/base/core/java/android/security/net/config/RootTrustManager.java)这个类有一个重载,它接受hostName。另一方面,如果存在特定于域的配置,则不接受主机名(作为最后一个参数)的重载将引发异常(mConfig.hasPerDomainConfigs()== true)。请注意,第二个方法处理hostName 并基于它定位相应的NetworkSecurityConfig。

aosp/system/frameworks/base/core/java/android/security/net/config/RootTrustManager.java

 @Override
    public void checkServerTrusted(X509Certificate[] certs, String authType)
            throws CertificateException {
        if (mConfig.hasPerDomainConfigs()) {
            throw new CertificateException(
                    "Domain specific configurations require that hostname aware"
                    + " checkServerTrusted(X509Certificate[], String, String) is used");
        }
        NetworkSecurityConfig config = mConfig.getConfigForHostname("");
        config.getTrustManager().checkServerTrusted(certs, authType);
    }

    /**
     * Hostname aware version of {@link #checkServerTrusted(X509Certificate[], String)}.
     * This interface is used by conscrypt and android.net.http.X509TrustManagerExtensions do not
     * modify without modifying those callers.
     */
    @UnsupportedAppUsage
    public List<X509Certificate> checkServerTrusted(X509Certificate[] certs, String authType,
            String hostname) throws CertificateException {
        if (hostname == null && mConfig.hasPerDomainConfigs()) {
            throw new CertificateException(
                    "Domain specific configurations require that the hostname be provided");
        }
        NetworkSecurityConfig config = mConfig.getConfigForHostname(hostname);
        return config.getTrustManager().checkServerTrusted(certs, authType, hostname);
    }

这里的建议是从verify_X509_cert_chain调用第二个重载,它已经有hostName(作为输入参数)并将其转发。

请注意,第二个重载不是X509TrustManager接口的一部分,但是我们可以使用x509trustmanagerxtensionswrapper-checkServer

Trusted,它接受hostName并在下面找到正确的方法。

bool verify_X509_cert_chain(const std::vector<std::string>& certChain, const std::string& hostName)

…
    // X509TrustManager
    java_local_ref<jclass> X509TrustManagerClass(env->FindClass("javax/net/ssl/X509TrustManager"));
    CHECK_JREF(env, X509TrustManagerClass);
    jmethodID X509TrustManagerCheckServerTrustedMethod =
        env->GetMethodID(X509TrustManagerClass.get(),
                         "checkServerTrusted",
                         "([Ljava/security/cert/X509Certificate;Ljava/lang/String;)V");
…

   // Validate certificate chain.
    java_local_ref<jstring> RSAString(env->NewStringUTF("RSA"));
    CHECK_JREF(env, RSAString);
    env->CallVoidMethod(
        trustManager.get(), X509TrustManagerCheckServerTrustedMethod, certsArray.get(), RSAString.get());
    CHECK_JNI(env);
…

建议

当前的解决方法是使OpenSSL成功,并且永远不会回退到特定于平台的验证。例如,这可以通过 SSL_CERT_FILE 环境变量和在启动期间生成CA pem来实现,从而绕过平台验证代码。


版权声明:本文为JoeySheng原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接和本声明。