diff --git a/library/common/jni/jni_interface.cc b/library/common/jni/jni_interface.cc index 9ebe1c0e70..4adea5c57f 100644 --- a/library/common/jni/jni_interface.cc +++ b/library/common/jni/jni_interface.cc @@ -1,6 +1,7 @@ #include #include +#include #include "library/common/api/c_types.h" #include "library/common/extensions/filters/http/platform_bridge/c_types.h" @@ -1031,3 +1032,137 @@ Java_io_envoyproxy_envoymobile_engine_JniLibrary_setPreferredNetwork(JNIEnv* env return set_preferred_network(static_cast(engine), static_cast(network)); } + +bool jvm_cert_is_issued_by_known_root(JNIEnv* env, jobject result) { + jclass jcls_AndroidCertVerifyResult = env->FindClass("org/chromium/net/AndroidCertVerifyResult"); + jmethodID jmid_isIssuedByKnownRoot = + env->GetMethodID(jcls_AndroidCertVerifyResult, "isIssuedByKnownRoot", "()Z"); + bool is_issued_by_known_root = + env->CallBooleanMethod(jcls_AndroidCertVerifyResult, jmid_isIssuedByKnownRoot, result); + env->DeleteLocalRef(jcls_AndroidCertVerifyResult); + return is_issued_by_known_root; +} + +envoy_cert_verify_status_t jvm_cert_get_status(JNIEnv* env, jobject j_result) { + jclass jcls_AndroidCertVerifyResult = env->FindClass("org/chromium/net/AndroidCertVerifyResult"); + jmethodID jmid_getStatus = env->GetMethodID(jcls_AndroidCertVerifyResult, "getStatus", "()I"); + envoy_cert_verify_status_t result = static_cast( + env->CallIntMethod(jcls_AndroidCertVerifyResult, jmid_getStatus, j_result)); + env->DeleteLocalRef(jcls_AndroidCertVerifyResult); + return result; +} + +jobjectArray jvm_cert_get_certificate_chain_encoded(JNIEnv* env, jobject result) { + jclass jcls_AndroidCertVerifyResult = env->FindClass("org/chromium/net/AndroidCertVerifyResult"); + jmethodID jmid_getCertificateChainEncoded = + env->GetMethodID(jcls_AndroidCertVerifyResult, "getCertificateChainEncoded", "()[[B"); + jobjectArray certificate_chain = static_cast( + env->CallObjectMethod(jcls_AndroidCertVerifyResult, jmid_getCertificateChainEncoded, result)); + env->DeleteLocalRef(jcls_AndroidCertVerifyResult); + return certificate_chain; +} + +// Once we have a better picture of how Android's certificate verification will +// be plugged into EM, we should decide where this function should really live. +// Context: as of now JNI functions declared in this file are not exported through any +// header files, instead they are stored as callbacks into plain function +// tables. For this reason, this function, which would ideally be defined in +// jni_utility.cc, is currently defined here. +static void ExtractCertVerifyResult(JNIEnv* env, jobject result, envoy_cert_verify_status_t* status, + bool* is_issued_by_known_root, + std::vector* verified_chain) { + *status = jvm_cert_get_status(env, result); + + *is_issued_by_known_root = jvm_cert_is_issued_by_known_root(env, result); + + jobjectArray chain_byte_array = jvm_cert_get_certificate_chain_encoded(env, result); + JavaArrayOfByteArrayToStringVector(env, chain_byte_array, verified_chain); +} + +// `auth_type` and `host` are expected to be UTF-8 encoded. +static jobject call_jvm_verify_x509_cert_chain(JNIEnv* env, + const std::vector& cert_chain, + std::string auth_type, std::string host) { + jni_log("[Envoy]", "jvm_verify_x509_cert_chain"); + jclass jcls_AndroidNetworkLibrary = env->FindClass("org/chromium/net/AndroidNetworkLibrary"); + jmethodID jmid_verifyServerCertificates = + env->GetStaticMethodID(jcls_AndroidNetworkLibrary, "verifyServerCertificates", + "([[B[B[B)Lorg/chromium/net/AndroidCertVerifyResult;"); + + jobjectArray chain_byte_array = ToJavaArrayOfByteArray(env, cert_chain); + jbyteArray auth_string = ToJavaByteArray(env, auth_type); + jbyteArray host_string = ToJavaByteArray(env, host); + jobject result = + env->CallStaticObjectMethod(jcls_AndroidNetworkLibrary, jmid_verifyServerCertificates, + chain_byte_array, auth_string, host_string); + + env->DeleteLocalRef(chain_byte_array); + env->DeleteLocalRef(auth_string); + env->DeleteLocalRef(host_string); + env->DeleteLocalRef(jcls_AndroidNetworkLibrary); + return result; +} + +// `auth_type` and `host` are expected to be UTF-8 encoded. +static void jvm_verify_x509_cert_chain(const std::vector& cert_chain, + std::string auth_type, std::string host, + envoy_cert_verify_status_t* status, + bool* is_issued_by_known_root, + std::vector* verified_chain) { + JNIEnv* env = get_env(); + jobject result = call_jvm_verify_x509_cert_chain(env, cert_chain, auth_type, host); + ExtractCertVerifyResult(get_env(), result, status, is_issued_by_known_root, verified_chain); + env->DeleteLocalRef(result); +} + +static void jvm_add_test_root_certificate(const uint8_t* cert, size_t len) { + jni_log("[Envoy]", "jvm_add_test_root_certificate"); + JNIEnv* env = get_env(); + jclass jcls_AndroidNetworkLibrary = env->FindClass("org/chromium/net/AndroidNetworkLibrary"); + jmethodID jmid_addTestRootCertificate = + env->GetStaticMethodID(jcls_AndroidNetworkLibrary, "addTestRootCertificate", "([B)V"); + + jbyteArray cert_array = ToJavaByteArray(env, cert, len); + env->CallStaticVoidMethod(jcls_AndroidNetworkLibrary, jmid_addTestRootCertificate, cert_array); + env->DeleteLocalRef(cert_array); + env->DeleteLocalRef(jcls_AndroidNetworkLibrary); +} + +static void jvm_clear_test_root_certificate() { + jni_log("[Envoy]", "jvm_clear_test_root_certificate"); + JNIEnv* env = get_env(); + jclass jcls_AndroidNetworkLibrary = env->FindClass("org/chromium/net/AndroidNetworkLibrary"); + jmethodID jmid_clearTestRootCertificates = + env->GetStaticMethodID(jcls_AndroidNetworkLibrary, "clearTestRootCertificates", "()V"); + + env->CallStaticVoidMethod(jcls_AndroidNetworkLibrary, jmid_clearTestRootCertificates); + env->DeleteLocalRef(jcls_AndroidNetworkLibrary); +} + +extern "C" JNIEXPORT jobject JNICALL +Java_io_envoyproxy_envoymobile_engine_JniLibrary_callCertificateVerificationFromNative( + JNIEnv* env, jclass, jobjectArray certChain, jbyteArray jauthType, jbyteArray jhost) { + std::vector cert_chain; + std::string auth_type; + std::string host; + + JavaArrayOfByteArrayToStringVector(env, certChain, &cert_chain); + JavaArrayOfByteToString(env, jauthType, &auth_type); + JavaArrayOfByteToString(env, jhost, &host); + + return call_jvm_verify_x509_cert_chain(env, cert_chain, auth_type, host); +} + +extern "C" JNIEXPORT void JNICALL +Java_io_envoyproxy_envoymobile_engine_JniLibrary_callAddTestRootCertificateFromNative( + JNIEnv* env, jclass, jbyteArray jcert) { + std::vector cert; + JavaArrayOfByteToBytesVector(env, jcert, &cert); + jvm_add_test_root_certificate(cert.data(), cert.size()); +} + +extern "C" JNIEXPORT void JNICALL +Java_io_envoyproxy_envoymobile_engine_JniLibrary_callClearTestRootCertificateFromNative(JNIEnv*, + jclass) { + jvm_clear_test_root_certificate(); +} diff --git a/library/common/jni/jni_utility.cc b/library/common/jni/jni_utility.cc index 1bb6c7e905..4a156927f5 100644 --- a/library/common/jni/jni_utility.cc +++ b/library/common/jni/jni_utility.cc @@ -255,3 +255,65 @@ envoy_map to_native_map(JNIEnv* env, jobjectArray entries) { envoy_map native_map = {length / 2, entry_array}; return native_map; } + +jobjectArray ToJavaArrayOfByteArray(JNIEnv* env, const std::vector& v) { + jclass jcls_byte_array = env->FindClass("[B"); + jobjectArray joa = env->NewObjectArray(v.size(), jcls_byte_array, nullptr); + + for (size_t i = 0; i < v.size(); ++i) { + jbyteArray byte_array = + ToJavaByteArray(env, reinterpret_cast(v[i].data()), v[i].length()); + env->SetObjectArrayElement(joa, i, byte_array); + } + return joa; +} + +jbyteArray ToJavaByteArray(JNIEnv* env, const uint8_t* bytes, size_t len) { + jbyteArray byte_array = env->NewByteArray(len); + const jbyte* jbytes = reinterpret_cast(bytes); + env->SetByteArrayRegion(byte_array, /*start=*/0, len, jbytes); + return byte_array; +} + +jbyteArray ToJavaByteArray(JNIEnv* env, const std::string& str) { + const uint8_t* str_bytes = reinterpret_cast(str.data()); + return ToJavaByteArray(env, str_bytes, str.size()); +} + +void JavaArrayOfByteArrayToStringVector(JNIEnv* env, jobjectArray array, + std::vector* out) { + size_t len = env->GetArrayLength(array); + out->resize(len); + + for (size_t i = 0; i < len; ++i) { + jbyteArray bytes_array = static_cast(env->GetObjectArrayElement(array, i)); + jsize bytes_len = env->GetArrayLength(bytes_array); + // It doesn't matter if the array returned by GetByteArrayElements is a copy + // or not, as the data will be simply be copied into C++ owned memory below. + jbyte* bytes = env->GetByteArrayElements(bytes_array, /*isCopy=*/nullptr); + (*out)[i].assign(reinterpret_cast(bytes), bytes_len); + // There is nothing to write back, it is always safe to JNI_ABORT. + env->ReleaseByteArrayElements(bytes_array, bytes, JNI_ABORT); + // Explicitly delete to keep the local ref count low. + env->DeleteLocalRef(bytes_array); + } +} + +void JavaArrayOfByteToString(JNIEnv* env, jbyteArray jbytes, std::string* out) { + std::vector bytes; + JavaArrayOfByteToBytesVector(env, jbytes, &bytes); + *out = std::string(bytes.begin(), bytes.end()); +} + +void JavaArrayOfByteToBytesVector(JNIEnv* env, jbyteArray array, std::vector* out) { + const size_t len = env->GetArrayLength(array); + out->resize(len); + + // It doesn't matter if the array returned by GetByteArrayElements is a copy + // or not, as the data will be simply be copied into C++ owned memory below. + jbyte* jbytes = env->GetByteArrayElements(array, /*isCopy=*/nullptr); + uint8_t* bytes = reinterpret_cast(jbytes); + std::copy(bytes, bytes + len, out->begin()); + // There is nothing to write back, it is always safe to JNI_ABORT. + env->ReleaseByteArrayElements(array, jbytes, JNI_ABORT); +} diff --git a/library/common/jni/jni_utility.h b/library/common/jni/jni_utility.h index f75529a3a5..3e1e4b0db0 100644 --- a/library/common/jni/jni_utility.h +++ b/library/common/jni/jni_utility.h @@ -1,5 +1,8 @@ #pragma once +#include +#include + #include "library/common/jni/import/jni_import.h" #include "library/common/types/c_types.h" @@ -63,3 +66,20 @@ envoy_headers* to_native_headers_ptr(JNIEnv* env, jobjectArray headers); envoy_stats_tags to_native_tags(JNIEnv* env, jobjectArray tags); envoy_map to_native_map(JNIEnv* env, jobjectArray entries); + +/** + * Utilities to translate C++ std library constructs to their Java counterpart. + * The underlying data is always copied to disentangle C++ and Java objects lifetime. + */ +jobjectArray ToJavaArrayOfByteArray(JNIEnv* env, const std::vector& v); + +jbyteArray ToJavaByteArray(JNIEnv* env, const uint8_t* bytes, size_t len); + +jbyteArray ToJavaByteArray(JNIEnv* env, const std::string& str); + +void JavaArrayOfByteArrayToStringVector(JNIEnv* env, jobjectArray array, + std::vector* out); + +void JavaArrayOfByteToBytesVector(JNIEnv* env, jbyteArray array, std::vector* out); + +void JavaArrayOfByteToString(JNIEnv* env, jbyteArray jbytes, std::string* out); diff --git a/library/common/types/c_types.h b/library/common/types/c_types.h index 22f1fc4dba..acc127f112 100644 --- a/library/common/types/c_types.h +++ b/library/common/types/c_types.h @@ -488,3 +488,27 @@ typedef struct { // Context passed through to callbacks to provide dispatch and execution state. const void* context; } envoy_event_tracker; + +/** + * The list of certificate verification results returned from Java side to the + * C++ side. + * A Java counterpart lives in org.chromium.net.CertVerifyStatusAndroid.java + */ +typedef enum { + // Certificate is trusted. + CERT_VERIFY_STATUS_OK = 0, + // Certificate verification could not be conducted. + CERT_VERIFY_STATUS_FAILED = -1, + // Certificate is not trusted due to non-trusted root of the certificate + // chain. + CERT_VERIFY_STATUS_NO_TRUSTED_ROOT = -2, + // Certificate is not trusted because it has expired. + CERT_VERIFY_STATUS_EXPIRED = -3, + // Certificate is not trusted because it is not valid yet. + CERT_VERIFY_STATUS_NOT_YET_VALID = -4, + // Certificate is not trusted because it could not be parsed. + CERT_VERIFY_STATUS_UNABLE_TO_PARSE = -5, + // Certificate is not trusted because it has an extendedKeyUsage field, but + // its value is not correct for a web server. + CERT_VERIFY_STATUS_INCORRECT_KEY_USAGE = -6, +} envoy_cert_verify_status_t; diff --git a/library/java/io/envoyproxy/envoymobile/engine/JniLibrary.java b/library/java/io/envoyproxy/envoymobile/engine/JniLibrary.java index 0729dd1c07..3a9ddbcc66 100644 --- a/library/java/io/envoyproxy/envoymobile/engine/JniLibrary.java +++ b/library/java/io/envoyproxy/envoymobile/engine/JniLibrary.java @@ -331,4 +331,30 @@ protected static native int registerStringAccessor(String accessorName, * @return The resulting status of the operation. */ protected static native int setPreferredNetwork(long engine, int network); + + /** + * Mimic a call to AndroidNetworkLibrary#verifyServerCertificates from native code. + * To be used for testing only. + * + * @param certChain The ASN.1 DER encoded bytes for certificates. + * @param authType The key exchange algorithm name (e.g. RSA). + * @param host The hostname of the server. + * @return Android certificate verification result code. + */ + public static native Object callCertificateVerificationFromNative(byte[][] certChain, + byte[] authType, byte[] host); + /** + * Mimic a call to AndroidNetworkLibrary#addTestRootCertificate from native code. + * To be used for testing only. + * + * @param rootCert DER encoded bytes of the certificate. + */ + public static native void callAddTestRootCertificateFromNative(byte[] cert); + + /** + * Mimic a call to AndroidNetworkLibrary#clearTestRootCertificate from native code. + * To be used for testing only. + * + */ + public static native void callClearTestRootCertificateFromNative(); } diff --git a/library/java/org/chromium/net/AndroidNetworkLibrary.java b/library/java/org/chromium/net/AndroidNetworkLibrary.java index 4fe6909dfd..d299f9a8ac 100644 --- a/library/java/org/chromium/net/AndroidNetworkLibrary.java +++ b/library/java/org/chromium/net/AndroidNetworkLibrary.java @@ -1,47 +1,10 @@ package org.chromium.net; -import android.Manifest; -import android.content.Context; -import android.content.Intent; -import android.content.IntentFilter; -import android.content.pm.PackageManager; -import android.net.ConnectivityManager; -import android.net.LinkProperties; -import android.net.Network; -import android.net.NetworkCapabilities; -import android.net.NetworkInfo; -import android.net.TrafficStats; -import android.net.TransportInfo; -import android.net.wifi.WifiInfo; -import android.net.wifi.WifiManager; -import android.os.Build; -import android.os.Build.VERSION_CODES; -import android.os.ParcelFileDescriptor; -import android.os.Process; -import android.telephony.TelephonyManager; -import android.util.Log; - -import androidx.annotation.RequiresApi; -import androidx.annotation.VisibleForTesting; - -import java.io.FileDescriptor; -import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; -import java.lang.reflect.InvocationTargetException; -import java.lang.reflect.Method; -import java.net.InetAddress; -import java.net.NetworkInterface; -import java.net.Socket; -import java.net.SocketAddress; -import java.net.SocketException; -import java.net.SocketImpl; -import java.net.URLConnection; import java.security.KeyStoreException; import java.security.NoSuchAlgorithmException; import java.security.cert.CertificateException; -import java.util.Enumeration; -import java.util.List; + +import java.nio.charset.StandardCharsets; /** * This class implements net utilities required by the net component. @@ -49,19 +12,38 @@ public final class AndroidNetworkLibrary { private static final String TAG = "AndroidNetworkLibrary"; + private static boolean mUseFakeCertificateVerification; + + /** + * Whether a fake should be used in place of X509Util. This allows to easily test the JNI + * call interaction in robolectric tests. + * + * @param useFakeCertificateVerification Whether FakeX509Util should be used or not. + */ + public static void + setFakeCertificateVerificationForTesting(boolean useFakeCertificateVerification) { + mUseFakeCertificateVerification = useFakeCertificateVerification; + } + /** * Validate the server's certificate chain is trusted. Note that the caller * must still verify the name matches that of the leaf certificate. + * This is called from native code. * * @param certChain The ASN.1 DER encoded bytes for certificates. - * @param authType The key exchange algorithm name (e.g. RSA). - * @param host The hostname of the server. + * @param authType Bytes representing the UTF-8 encoding of the key exchange algorithm name (e.g. + * RSA). + * @param host Bytes representing the UTF-8 encoding of the hostname of the server. * @return Android certificate verification result code. */ - // TODO(stefanoduo): Hook envoy-mobile JNI. - //@CalledByNative - public static AndroidCertVerifyResult verifyServerCertificates(byte[][] certChain, - String authType, String host) { + public static AndroidCertVerifyResult + verifyServerCertificates(byte[][] certChain, byte[] authTypeBytes, byte[] hostBytes) { + String authType = new String(authTypeBytes, StandardCharsets.UTF_8); + String host = new String(hostBytes, StandardCharsets.UTF_8); + if (mUseFakeCertificateVerification) { + return FakeX509Util.verifyServerCertificates(certChain, authType, host); + } + try { return X509Util.verifyServerCertificates(certChain, authType, host); } catch (KeyStoreException e) { @@ -75,23 +57,30 @@ public static AndroidCertVerifyResult verifyServerCertificates(byte[][] certChai /** * Adds a test root certificate to the local trust store. + * This is called from native code. + * * @param rootCert DER encoded bytes of the certificate. */ - // TODO(stefanoduo): Hook envoy-mobile JNI. - //@CalledByNativeUnchecked public static void addTestRootCertificate(byte[] rootCert) throws CertificateException, KeyStoreException, NoSuchAlgorithmException { - X509Util.addTestRootCertificate(rootCert); + if (mUseFakeCertificateVerification) { + FakeX509Util.addTestRootCertificate(rootCert); + } else { + X509Util.addTestRootCertificate(rootCert); + } } /** * Removes all test root certificates added by |addTestRootCertificate| calls from the local * trust store. + * This is called from native code. */ - // TODO(stefanoduo): Hook envoy-mobile JNI. - //@CalledByNativeUnchecked public static void clearTestRootCertificates() throws NoSuchAlgorithmException, CertificateException, KeyStoreException { - X509Util.clearTestRootCertificates(); + if (mUseFakeCertificateVerification) { + FakeX509Util.clearTestRootCertificates(); + } else { + X509Util.clearTestRootCertificates(); + } } } diff --git a/library/java/org/chromium/net/FakeX509Util.java b/library/java/org/chromium/net/FakeX509Util.java new file mode 100644 index 0000000000..efdfa36d6f --- /dev/null +++ b/library/java/org/chromium/net/FakeX509Util.java @@ -0,0 +1,56 @@ +package org.chromium.net; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashSet; +import java.util.Set; + +/** + * Fake utility functions to verify X.509 certificates. + * + * FakeX509Util is not particularly clever: from its perspective a certificate is just a string and + * its contents have no particular meaning. For a verification to succeed: + * - all certificates in a chain must have been previously registered as root certificates + * - host and authentication type must match the expected hardcoded values + * This doesn't make much sense w.r.t. how X.509 certificates are really validated, but we're not + * interested in mimicking that, we just want something to confirm that JNI calls have taken place. + */ +public final class FakeX509Util { + private static final Set validFakeCerts = new HashSet(); + + public static final String expectedAuthType = "RSA"; + public static final String expectedHost = "www.example.com"; + + public static void addTestRootCertificate(byte[] rootCertBytes) { + String fakeCertificate = new String(rootCertBytes); + validFakeCerts.add(fakeCertificate); + } + + public static void clearTestRootCertificates() { validFakeCerts.clear(); } + + /** + * Performs fake certificate chain verification. Returns CertVerifyStatusAndroid.NO_TRUSTED_ROOT + * if at least one of the certificates in the chain has not been previously registered as a root. + * Returns CertVerifyStatusAndroid.OK if authType and host match respectively expectedAuthType and + * expectedHost; CertVerifyStatusAndroid.FAILED otherwise. + */ + public static AndroidCertVerifyResult verifyServerCertificates(byte[][] certChain, + String authType, String host) { + if (certChain == null || certChain.length == 0 || certChain[0] == null) { + throw new IllegalArgumentException( + "Expected non-null and non-empty certificate " + + "chain passed as |certChain|. |certChain|=" + Arrays.deepToString(certChain)); + } + + for (byte[] cert : certChain) { + String fakeCert = new String(cert); + if (!validFakeCerts.contains(fakeCert)) { + return new AndroidCertVerifyResult(CertVerifyStatusAndroid.NO_TRUSTED_ROOT); + } + } + + return authType.equals(expectedAuthType) && host.equals(expectedHost) + ? new AndroidCertVerifyResult(CertVerifyStatusAndroid.OK) + : new AndroidCertVerifyResult(CertVerifyStatusAndroid.FAILED); + } +} diff --git a/test/java/org/chromium/net/BUILD b/test/java/org/chromium/net/BUILD index 17f33ec87f..97fc92370c 100644 --- a/test/java/org/chromium/net/BUILD +++ b/test/java/org/chromium/net/BUILD @@ -36,6 +36,22 @@ envoy_mobile_android_test( ], ) +envoy_mobile_android_test( + name = "certificate_verification_tests", + srcs = [ + "CertificateVerificationTest.java", + ], + native_deps = [ + "//library/common/jni:libndk_envoy_jni.so", + "//library/common/jni:libndk_envoy_jni.jnilib", + ], + deps = [ + "//library/java/io/envoyproxy/envoymobile/engine:envoy_base_engine_lib", + "//library/java/io/envoyproxy/envoymobile/engine:envoy_engine_lib", + "//library/java/org/chromium/net", + ], +) + envoy_mobile_android_test( name = "cronet_url_request_context_test", srcs = [ diff --git a/test/java/org/chromium/net/CertificateVerificationTest.java b/test/java/org/chromium/net/CertificateVerificationTest.java new file mode 100644 index 0000000000..86575c97d5 --- /dev/null +++ b/test/java/org/chromium/net/CertificateVerificationTest.java @@ -0,0 +1,146 @@ +package org.chromium.net; + +import static org.junit.Assert.assertEquals; + +import io.envoyproxy.envoymobile.engine.AndroidJniLibrary; +import io.envoyproxy.envoymobile.engine.JniLibrary; + +import java.nio.charset.StandardCharsets; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; + +/** + * Simple test for Certificate verification JNI layer. + * The objective is not to test the certificate verification logic (which is faked) but instead to + * confirm that all JNI calls go through (confirmed by checking for the fake implementation side + * effects). + */ +@RunWith(RobolectricTestRunner.class) +public final class CertificateVerificationTest { + static { + AndroidJniLibrary.loadTestLibrary(); + JniLibrary.load(); + } + + private static final byte[] host = FakeX509Util.expectedHost.getBytes(StandardCharsets.UTF_8); + private static final byte[] authType = + FakeX509Util.expectedAuthType.getBytes(StandardCharsets.UTF_8); + + @Before + public void setUp() throws Exception { + AndroidNetworkLibrary.setFakeCertificateVerificationForTesting(true); + } + + @After + public void tearDown() throws Exception { + JniLibrary.callClearTestRootCertificateFromNative(); + AndroidNetworkLibrary.setFakeCertificateVerificationForTesting(true); + } + + @Test + public void testChainWithNonRootCertificate() throws Exception { + final String[] fakeCertChain = new String[] {"fake cert"}; + final byte[][] certChain = new byte[][] {fakeCertChain[0].getBytes()}; + + AndroidCertVerifyResult result = + (AndroidCertVerifyResult)JniLibrary.callCertificateVerificationFromNative(certChain, host, + authType); + assertEquals(result.getStatus(), CertVerifyStatusAndroid.NO_TRUSTED_ROOT); + } + + @Test + public void testChainWithRootCertificate() throws Exception { + final String[] fakeCertChain = new String[] {"fake cert"}; + final byte[][] certChain = new byte[][] {fakeCertChain[0].getBytes()}; + + JniLibrary.callAddTestRootCertificateFromNative(certChain[0]); + AndroidCertVerifyResult result = + (AndroidCertVerifyResult)JniLibrary.callCertificateVerificationFromNative(certChain, + authType, host); + assertEquals(result.getStatus(), CertVerifyStatusAndroid.OK); + } + + @Test + public void testChainWithRootCertificateWrongHostname() throws Exception { + final String[] fakeCertChain = new String[] {"fake cert"}; + final byte[][] certChain = new byte[][] {fakeCertChain[0].getBytes()}; + final String host = "wrong host"; + final byte[] hostBytes = host.getBytes(StandardCharsets.UTF_8); + + JniLibrary.callAddTestRootCertificateFromNative(certChain[0]); + AndroidCertVerifyResult result = + (AndroidCertVerifyResult)JniLibrary.callCertificateVerificationFromNative( + certChain, authType, hostBytes); + assertEquals(result.getStatus(), CertVerifyStatusAndroid.FAILED); + } + + @Test + public void testChainWithRootCertificateWrongAuthType() throws Exception { + final String[] fakeCertChain = new String[] {"fake cert"}; + final byte[][] certChain = new byte[][] {fakeCertChain[0].getBytes()}; + final String authType = "wrong auth type"; + final byte[] authTypeBytes = authType.getBytes(StandardCharsets.UTF_8); + + JniLibrary.callAddTestRootCertificateFromNative(certChain[0]); + AndroidCertVerifyResult result = + (AndroidCertVerifyResult)JniLibrary.callCertificateVerificationFromNative( + certChain, authTypeBytes, host); + assertEquals(result.getStatus(), CertVerifyStatusAndroid.FAILED); + } + + @Test + public void testClearTestRootCertificate() throws Exception { + final String[] fakeCertChain = new String[] {"fake cert"}; + final byte[][] certChain = new byte[][] {fakeCertChain[0].getBytes()}; + + JniLibrary.callAddTestRootCertificateFromNative(certChain[0]); + JniLibrary.callClearTestRootCertificateFromNative(); + AndroidCertVerifyResult result = + (AndroidCertVerifyResult)JniLibrary.callCertificateVerificationFromNative(certChain, + authType, host); + assertEquals(result.getStatus(), CertVerifyStatusAndroid.NO_TRUSTED_ROOT); + } + + @Test + public void testChainWithMultipleNonRootCertificates() throws Exception { + final String[] fakeCertChain = new String[] {"fake cert", "another fake cert"}; + final byte[][] certChain = + new byte[][] {fakeCertChain[0].getBytes(), fakeCertChain[1].getBytes()}; + + AndroidCertVerifyResult result = + (AndroidCertVerifyResult)JniLibrary.callCertificateVerificationFromNative(certChain, + authType, host); + assertEquals(result.getStatus(), CertVerifyStatusAndroid.NO_TRUSTED_ROOT); + } + + @Test + public void testChainWithMixedCertificates() throws Exception { + final String[] fakeCertChain = new String[] {"fake cert", "another fake cert"}; + final byte[][] certChain = + new byte[][] {fakeCertChain[0].getBytes(), fakeCertChain[1].getBytes()}; + + JniLibrary.callAddTestRootCertificateFromNative(certChain[0]); + AndroidCertVerifyResult result = + (AndroidCertVerifyResult)JniLibrary.callCertificateVerificationFromNative(certChain, + authType, host); + assertEquals(result.getStatus(), CertVerifyStatusAndroid.NO_TRUSTED_ROOT); + } + + @Test + public void testChainWithMultipleRootCertificates() throws Exception { + final String[] fakeCertChain = new String[] {"fake cert", "another fake cert"}; + final byte[][] certChain = + new byte[][] {fakeCertChain[0].getBytes(), fakeCertChain[1].getBytes()}; + + JniLibrary.callAddTestRootCertificateFromNative(certChain[0]); + JniLibrary.callAddTestRootCertificateFromNative(certChain[1]); + AndroidCertVerifyResult result = + (AndroidCertVerifyResult)JniLibrary.callCertificateVerificationFromNative(certChain, + authType, host); + assertEquals(result.getStatus(), CertVerifyStatusAndroid.OK); + } +}