Skip to content
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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");
Comment thread
StefanoDuo marked this conversation as 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");
Comment thread
StefanoDuo marked this conversation as 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");
Comment thread
StefanoDuo marked this conversation as 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,
Comment thread
StefanoDuo marked this conversation as resolved.
Outdated
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,
Comment thread
StefanoDuo marked this conversation as resolved.
Outdated
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");
Comment thread
StefanoDuo marked this conversation as 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
Copy Markdown
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
Copy Markdown
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
Copy Markdown
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
Copy Markdown
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
Copy Markdown
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
Copy Markdown
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
Copy Markdown
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 = 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 = UTF16ToUTF8(reinterpret_cast<const char16_t*>(utf16), len);
*result = utf8;
env->ReleaseStringChars(str, utf16);
}
Comment thread
StefanoDuo marked this conversation as resolved.
Outdated
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.
Comment thread
StefanoDuo marked this conversation as 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"],
)
14 changes: 14 additions & 0 deletions library/common/strings/string_conversions.cc
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
#include "library/common/strings/string_conversions.h"

#include <codecvt>
#include <locale>

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);
}
7 changes: 7 additions & 0 deletions library/common/strings/string_conversions.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
#pragma once

#include <string>

Comment thread
StefanoDuo marked this conversation as resolved.
Outdated
std::string UTF16ToUTF8(const char16_t* utf16, size_t len);
Comment thread
StefanoDuo marked this conversation as resolved.
Outdated

std::u16string UTF8ToUTF16(const char* utf8, size_t len);
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
Copy Markdown
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
Copy Markdown
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