Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Android's certificate verification JNI layer #2251

Merged
merged 12 commits into from
May 24, 2022
2 changes: 2 additions & 0 deletions library/common/jni/BUILD
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ cc_library(
],
deps = [
"//library/common/jni/import:jni_import_lib",
"//library/common/strings:string_conversions_lib",
"//library/common/types:c_types_lib",
"@envoy//source/common/common:assert_lib",
],
Expand Down Expand Up @@ -160,6 +161,7 @@ cc_library(
],
deps = [
":java_jni_support",
"//library/common/strings:string_conversions_lib",
"//library/common/jni/import:jni_import_lib",
"//library/common:envoy_main_interface_lib",
"//library/common/types:c_types_lib",
Expand Down
125 changes: 125 additions & 0 deletions library/common/jni/jni_interface.cc
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
#include <ares.h>

#include <string>
#include <vector>

#include "library/common/api/c_types.h"
#include "library/common/extensions/filters/http/platform_bridge/c_types.h"
Expand Down Expand Up @@ -1031,3 +1032,127 @@ Java_io_envoyproxy_envoymobile_engine_JniLibrary_setPreferredNetwork(JNIEnv* env
return set_preferred_network(static_cast<envoy_engine_t>(engine),
static_cast<envoy_network_t>(network));
}

bool jvm_cert_is_issued_by_known_root(JNIEnv* env, jobject result) {
jclass jcls_AndroidCertVerifyResult = env->FindClass("org/chromium/net/AndroidCertVerifyResult");
StefanoDuo marked this conversation as resolved.
Show resolved Hide resolved
jmethodID jmid_isIssuedByKnownRoot =
env->GetMethodID(jcls_AndroidCertVerifyResult, "isIssuedByKnownRoot", "()Z");
return env->CallBooleanMethod(jcls_AndroidCertVerifyResult, jmid_isIssuedByKnownRoot, result);
}

envoy_cert_verify_status_android_t jvm_cert_get_status(JNIEnv* env, jobject result) {
jclass jcls_AndroidCertVerifyResult = env->FindClass("org/chromium/net/AndroidCertVerifyResult");
StefanoDuo marked this conversation as resolved.
Show resolved Hide resolved
jmethodID jmid_getStatus = env->GetMethodID(jcls_AndroidCertVerifyResult, "getStatus", "()I");

return static_cast<envoy_cert_verify_status_android_t>(
env->CallIntMethod(jcls_AndroidCertVerifyResult, jmid_getStatus, result));
}

jobjectArray jvm_cert_get_certificate_chain_encoded(JNIEnv* env, jobject result) {
jclass jcls_AndroidCertVerifyResult = env->FindClass("org/chromium/net/AndroidCertVerifyResult");
StefanoDuo marked this conversation as resolved.
Show resolved Hide resolved
jmethodID jmid_getCertificateChainEncoded =
env->GetMethodID(jcls_AndroidCertVerifyResult, "getCertificateChainEncoded", "()[[B");

return static_cast<jobjectArray>(
env->CallObjectMethod(jcls_AndroidCertVerifyResult, jmid_getCertificateChainEncoded, result));
}

// 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.
void ExtractCertVerifyResult(JNIEnv* env, jobject result,
StefanoDuo marked this conversation as resolved.
Show resolved Hide resolved
envoy_cert_verify_status_android_t* status,
bool* is_issued_by_known_root,
std::vector<std::string>* 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);
}

static jobject call_jvm_verify_x509_cert_chain(const std::vector<std::string>& cert_chain,
StefanoDuo marked this conversation as resolved.
Show resolved Hide resolved
std::string auth_type, std::string host) {
jni_log("[Envoy]", "jvm_verify_x509_cert_chain");
JNIEnv* env = get_env();
jclass jcls_AndroidNetworkLibrary = env->FindClass("org/chromium/net/AndroidNetworkLibrary");
StefanoDuo marked this conversation as resolved.
Show resolved Hide resolved
jmethodID jmid_verifyServerCertificates = env->GetStaticMethodID(
jcls_AndroidNetworkLibrary, "verifyServerCertificates",
"([[BLjava/lang/String;Ljava/lang/String;)Lorg/chromium/net/AndroidCertVerifyResult;");

jobjectArray chain_byte_array = ToJavaArrayOfByteArray(env, cert_chain);
jstring auth_string = ConvertUTF8ToJavaString(env, auth_type);
jstring host_string = ConvertUTF8ToJavaString(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);
return result;
}

static void jvm_verify_x509_cert_chain(const std::vector<std::string>& cert_chain,
std::string auth_type, std::string host,
envoy_cert_verify_status_android_t* status,
bool* is_issued_by_known_root,
std::vector<std::string>* verified_chain) {
jobject result = call_jvm_verify_x509_cert_chain(cert_chain, auth_type, host);
ExtractCertVerifyResult(get_env(), result, status, is_issued_by_known_root, verified_chain);
}

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

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

extern "C" JNIEXPORT jobject JNICALL
Java_io_envoyproxy_envoymobile_engine_JniLibrary_callCertificateVerificationFromNative(
JNIEnv* env, jclass, jobjectArray certChain, jstring authType, jstring jhost) {
std::vector<std::string> cert_chain;
std::string auth_type;
std::string host;

JavaArrayOfByteArrayToStringVector(env, certChain, &cert_chain);
ConvertJavaStringToUTF8(env, authType, &auth_type);
ConvertJavaStringToUTF8(env, jhost, &host);

return call_jvm_verify_x509_cert_chain(cert_chain, auth_type, host);
}

extern "C" JNIEXPORT void JNICALL
Java_io_envoyproxy_envoymobile_engine_JniLibrary_callAddTestRootCertificateFromNative(
JNIEnv* env, jclass, jbyteArray jcert) {
std::vector<uint8_t> 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();
}
72 changes: 72 additions & 0 deletions library/common/jni/jni_utility.cc
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@

#include "library/common/jni/jni_support.h"
#include "library/common/jni/jni_version.h"
#include "library/common/strings/string_conversions.h"

// NOLINT(namespace-envoy)

Expand Down Expand Up @@ -255,3 +256,74 @@ envoy_map to_native_map(JNIEnv* env, jobjectArray entries) {
envoy_map native_map = {length / 2, entry_array};
return native_map;
}

jstring ConvertUTF8ToJavaString(JNIEnv* env, const std::string& str) {
// JNI's NewStringUTF expects "modified" UTF8 so instead convert the string to UTF16 and pass it
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OOC, what is "modified UTF8"?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

AFAIU it's what is used by the JVM under the hood https://docs.oracle.com/javase/1.5.0/docs/guide/jni/spec/types.html#wp16542.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks Wowwwww! That is wacky! I didn't realize "Modified UTF-8" was term of art. Wikipedia also has a good summary.

Modified UTF-8 (MUTF-8) originated in the Java programming language. In Modified UTF-8, the null character (U+0000) uses the two-byte overlong encoding 11000000 10000000 (hexadecimal C0 80), instead of 00000000 (hexadecimal 00).[79] Modified UTF-8 strings never contain any actual null bytes but can contain all Unicode code points including U+0000,[80] which allows such strings (with a null byte appended) to be processed by traditional null-terminated string functions.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To avoid surprises due to the weirdness of Modified UTF-8, the convention we've adopted a this layer is to pass true UTF-8 byte arrays across the JNI and then encode/decode them to java.lang.Strings on the Java side. For simplicity's sake (and perhaps to avoid the risk of extra logic manipulating buffers) it would be nice to maintain that convention, if possible, rather than introduce Modified UTF-8 handling in C++.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh I wasn't aware of that, my bad. Can you point me to a specific example so I can more closely follow your approach?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a simple example that shows the JNI side (this supports an implementation of a simple extension to retrieve runtime-defined platform Strings):

static envoy_data jvm_get_string(const void* context) {

And here is the Java side:

https://github.com/envoyproxy/envoy-mobile/blob/aa0beeea7079dbc6c9021f9bd6fd4801dbb1381a/library/java/io/envoyproxy/envoymobile/engine/JvmStringAccessorContext.java

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've updated the code to pass true UTF-8 byte arrays across the JNI interface as you suggested.

I don't think I need to wrap stuff in an accessor for my use case (the strings are only needed for the duration of the JNI call). Let me know if this looks okay to you!

// to NewString.
std::u16string utf16 = Envoy::Strings::UTF8ToUTF16(str.data(), str.size());
const jchar* jutf16 = reinterpret_cast<const jchar*>(utf16.data());
return env->NewString(jutf16, utf16.length());
}

jobjectArray ToJavaArrayOfByteArray(JNIEnv* env, const std::vector<std::string>& 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<const uint8_t*>(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<const jbyte*>(bytes);
env->SetByteArrayRegion(byte_array, /*start=*/0, len, jbytes);
return byte_array;
}

void JavaArrayOfByteArrayToStringVector(JNIEnv* env, jobjectArray array,
std::vector<std::string>* out) {
size_t len = env->GetArrayLength(array);
out->resize(len);

for (size_t i = 0; i < len; ++i) {
jbyteArray bytes_array = static_cast<jbyteArray>(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<const char*>(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 JavaArrayOfByteToBytesVector(JNIEnv* env, jbyteArray array, std::vector<uint8_t>* 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<uint8_t*>(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);
}

void ConvertJavaStringToUTF8(JNIEnv* env, jstring str, std::string* result) {
// JNI's GetStringUTFChars() returns strings in Java "modified" UTF8, so
// instead get the String in UTF16 and manually convert that to UTF8.
// We don't care whether the returned string is a copy or not as we're simply
// copying it into C++ owned memory.
const jchar* utf16 = env->GetStringChars(str, /*isCopy=*/nullptr);
size_t len = env->GetStringLength(str);
std::string utf8 = Envoy::Strings::UTF16ToUTF8(reinterpret_cast<const char16_t*>(utf16), len);
*result = utf8;
env->ReleaseStringChars(str, utf16);
}
StefanoDuo marked this conversation as resolved.
Show resolved Hide resolved
20 changes: 20 additions & 0 deletions library/common/jni/jni_utility.h
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
#pragma once

#include <string>
#include <vector>

#include "library/common/jni/import/jni_import.h"
#include "library/common/types/c_types.h"

Expand Down Expand Up @@ -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.
StefanoDuo marked this conversation as resolved.
Show resolved Hide resolved
* The underlying data is always copied to disentangle C++ and Java objects lifetime.
*/
jstring ConvertUTF8ToJavaString(JNIEnv* env, const std::string& str);

jobjectArray ToJavaArrayOfByteArray(JNIEnv* env, const std::vector<std::string>& v);

jbyteArray ToJavaByteArray(JNIEnv* env, const uint8_t* bytes, size_t len);

void JavaArrayOfByteArrayToStringVector(JNIEnv* env, jobjectArray array,
std::vector<std::string>* out);

void JavaArrayOfByteToBytesVector(JNIEnv* env, jbyteArray array, std::vector<uint8_t>* out);

void ConvertJavaStringToUTF8(JNIEnv* env, jstring str, std::string* result);
10 changes: 10 additions & 0 deletions library/common/strings/BUILD
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
load("@rules_cc//cc:defs.bzl", "cc_library")

licenses(["notice"]) # Apache 2

cc_library(
name = "string_conversions_lib",
srcs = ["string_conversions.cc"],
hdrs = ["string_conversions.h"],
visibility = ["//visibility:public"],
)
20 changes: 20 additions & 0 deletions library/common/strings/string_conversions.cc
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
#include "library/common/strings/string_conversions.h"

#include <codecvt>
#include <locale>

namespace Envoy {
namespace Strings {

std::string UTF16ToUTF8(const char16_t* utf16, size_t len) {
return std::wstring_convert<std::codecvt_utf8_utf16<char16_t>, char16_t>{}.to_bytes(utf16,
utf16 + len);
}

std::u16string UTF8ToUTF16(const char* utf8, size_t len) {
return std::wstring_convert<std::codecvt_utf8_utf16<char16_t>, char16_t>{}.from_bytes(utf8,
utf8 + len);
}

} // namespace Strings
} // namespace Envoy
25 changes: 25 additions & 0 deletions library/common/strings/string_conversions.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
#pragma once

#include <string>

StefanoDuo marked this conversation as resolved.
Show resolved Hide resolved
namespace Envoy {
namespace Strings {

/**
* Converts a UTF-16 string to an UTF-8 one.
* @param utf16, the array of code units composing the UTF-16 string
* @param len, the number of code units
* @return the input string encoded in UTF-8
*/
std::string UTF16ToUTF8(const char16_t* utf16, size_t len);
StefanoDuo marked this conversation as resolved.
Show resolved Hide resolved

/**
* Converts a UTF-8 string to an UTF-16 one.
* @param utf8, the array of code units composing the UTF-8 string
* @param len, the number of code units
* @return the input string encoded in UTF-16
*/
std::u16string UTF8ToUTF16(const char* utf8, size_t len);

} // namespace Strings
} // namespace Envoy
24 changes: 24 additions & 0 deletions library/common/types/c_types.h
Original file line number Diff line number Diff line change
Expand Up @@ -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_ANDROID_OK = 0,
// Certificate verification could not be conducted.
CERT_VERIFY_STATUS_ANDROID_FAILED = -1,
// Certificate is not trusted due to non-trusted root of the certificate
// chain.
CERT_VERIFY_STATUS_ANDROID_NO_TRUSTED_ROOT = -2,
// Certificate is not trusted because it has expired.
CERT_VERIFY_STATUS_ANDROID_EXPIRED = -3,
// Certificate is not trusted because it is not valid yet.
CERT_VERIFY_STATUS_ANDROID_NOT_YET_VALID = -4,
// Certificate is not trusted because it could not be parsed.
CERT_VERIFY_STATUS_ANDROID_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_ANDROID_INCORRECT_KEY_USAGE = -6,
} envoy_cert_verify_status_android_t;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can this enum be made generic such that it could be utilized by other platforms (currently iOS and C++)?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've removed the "android qualifier". I'm not entirely sure if other platform semantics will/do differ, but I think this should be a reasonable first iteration, thoughts?

26 changes: 26 additions & 0 deletions library/java/io/envoyproxy/envoymobile/engine/JniLibrary.java
Original file line number Diff line number Diff line change
Expand Up @@ -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,
String authType, String 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();
}
Loading