Skip to content

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

@kalin-toshev

Description

@kalin-toshev

Overview

On Android, when using http_client with HTTPs (SSL), if we have network security config (for example - cert pinning) with one or more domain-config elements, the platform cert verification (fallback) method (verify_cert_chain_platform_specific) fails with JNI exception (CertificateException).

Below is an example 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>

Note that this happens when default OpenSSL validation fails (for example, due to missing CA configured) and we need to fallback to platform verification (via verify_cert_chain_platform_specific). As a result, SSL connection fails.

Details

The code here calls X509TrustManager/checkServerTrusted, obtained via factory method. But the overload being called does not contain the hostName parameter. Underneath, in this case, X509CertificateManager 'resolves' to RootTrustManager (aosp/system/frameworks/base/core/java/android/security/net/config/RootTrustManager.java) and this class has an overload, which takes the hostName. On the other hand, the overload which does not take a hostName (as last parameter), will throw exception if domain specific config(s) are present (mConfig.hasPerDomainConfigs() == true). Note that the second method handles hostName ok and locates the corresponding NetworkSecurityConfig based on it.

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);
    }


…

The suggestion here is to call the second overload from verify_X509_cert_chain, which already has the hostName (as input argument) and pass it forward.

Note that the second overload is not part of the (base) X509TrustManager interface, but we can use X509TrustManagerExtensions wrapper - checkServerTrusted, which takes hostName and underneath will locate the correct method.

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);
…

Workaround

The current workaround is to make OpenSSL succeed and never fallback to the platform specific verification. This can be achieved, for example, via SSL_CERT_FILE environment variable and 'spilling' the CA pem during startup and thus bypass the platform verification code.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions