背景介绍
在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来实现,从而绕过平台验证代码。