chore: initialize Android TLS verification

This commit is contained in:
Tomás Romero committed 2026-10-13 20:51:31 +00:00
1 parent 19d449ca39
commit 2ee87d1e07
7 files changed
+99 -1

No files matched your search

+2
View File
@@ -1803,6 +1803,8 @@ dependencies = [
"base64 0.23.1",
"flectar-mail",
"jni 0.21.1",
"jni 0.22.4",
"rustls-platform-verifier",
"serde_json",
"slint",
"tokio",
+2
View File
@@ -30,6 +30,8 @@ flectar-mail = { path = "../..", default-features = false, features = ["remote-c
async-trait = "0.1"
base64 = "0.23"
jni = "0.21"
jni-verifier = { package = "jni", version = "0.22" }
rustls-platform-verifier = "0.7"
serde_json = "1"
slint = { version = "1.18.1", default-features = false, features = [
"std",
@@ -86,6 +86,9 @@ tasks.named("preBuild").configure {
}
dependencies {
implementation("rustls:rustls-platform-verifier:0.1.1")
// The verifier AAR does not declare its Kotlin runtime dependency.
implementation("org.jetbrains.kotlin:kotlin-stdlib:1.9.21")
// AuthorizationClient is Google's supported API for authorizing an
// Android application to Gmail and other Google user data.
implementation("com.google.android.gms:play-services-auth:21.6.0")
@@ -0,0 +1,30 @@
package com.flectar.mail;
import static org.junit.Assert.assertNotEquals;
import android.content.Context;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.platform.app.InstrumentationRegistry;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import org.junit.Test;
import org.junit.runner.RunWith;
/** Check that the release dependency includes the JVM API called through Rust JNI. */
@RunWith(AndroidJUnit4.class)
public final class CertificateVerifierTest {
@Test
public void packagedVerifierRejectsMalformedCertificate() throws Exception {
Context context = InstrumentationRegistry.getInstrumentation().getTargetContext();
Class<?> verifier = context.getClassLoader()
.loadClass("org.rustls.platformverifier.CertificateVerifier");
Method verify = verifier.getDeclaredMethod("verifyCertificateChain", Context.class,
String.class, String.class, String[].class, byte[].class, long.class, byte[][].class);
verify.setAccessible(true);
Object result = verify.invoke(null, context, "example.test", "RSA", new String[0],
new byte[0], System.currentTimeMillis() / 1000, new byte[][]{new byte[]{0}});
Field code = result.getClass().getDeclaredField("code");
code.setAccessible(true);
assertNotEquals("Malformed certificates must fail verification", 0, code.getInt(result));
}
}
@@ -1,3 +1,5 @@
import groovy.json.JsonSlurper
pluginManagement {
repositories {
google()
@@ -11,6 +13,29 @@ dependencyResolutionManagement {
repositories {
google()
mavenCentral()
// Resolve the JVM bridge from the exact crate selected by Cargo.lock.
// It is shipped with the crate rather than published to Maven Central.
val metadata = providers.exec {
workingDir(rootDir.parentFile)
commandLine(
"cargo", "metadata", "--locked", "--format-version", "1",
"--filter-platform", "aarch64-linux-android", "--manifest-path", "Cargo.toml",
)
}.standardOutput.asText.get()
val packages = (JsonSlurper().parseText(metadata) as Map<*, *>)["packages"] as List<*>
val verifier = packages.map { it as Map<*, *> }
.single { it["name"] == "rustls-platform-verifier-android" }
check(verifier["version"] == "0.1.1") {
"Update the Gradle verifier dependency to match Cargo.lock"
}
exclusiveContent {
forRepository {
maven {
url = uri(File(File(verifier["manifest_path"] as String).parentFile, "maven"))
}
}
filter { includeModule("rustls", "rustls-platform-verifier") }
}
}
}
+7 -1
View File
@@ -111,10 +111,14 @@ fn app_cache_dir(app: &slint::android::AndroidApp) -> Result<std::path::PathBuf,
#[unsafe(no_mangle)]
fn android_main(app: slint::android::AndroidApp) {
observability::install();
// SAFETY: AndroidApp owns these live VM and Activity references.
unsafe { tls::initialize(app.vm_as_ptr().cast(), app.activity_as_ptr().cast()) }
.expect("failed to initialize Android TLS certificate verification");
let internal = app
.internal_data_path()
.expect("Android did not provide an app-private data directory");
let cache_dir = app_cache_dir(&app).expect("Android did not provide an app-private cache directory");
let cache_dir =
app_cache_dir(&app).expect("Android did not provide an app-private cache directory");
tracing::info!(internal_data_path = %internal.display(), "resolved Android app-private root");
let oauth_redirects = oauth::AndroidOAuthBroker::global(&app, &internal)
.expect("failed to initialize Android OAuth services");
@@ -140,6 +144,8 @@ fn android_main(app: slint::android::AndroidApp) {
mod credentials;
#[cfg(target_os = "android")]
mod oauth;
#[cfg(target_os = "android")]
mod tls;
#[cfg(target_os = "android")]
mod documents;
+30
View File
@@ -0,0 +1,30 @@
//! Initialize the process-wide verifier before any reqwest/JMAP networking.
use jni_verifier::{JavaVM, errors::Error, objects::JObject};
/// # Safety
/// `vm` must be the live process JVM and `activity` a live JNI reference to an
/// Android Context. Both must remain valid until this call returns.
pub(super) unsafe fn initialize(
vm: *mut jni_verifier::sys::JavaVM,
activity: jni_verifier::sys::jobject,
) -> Result<(), Error> {
// SAFETY: android_main supplies the live VM and NativeActivity global
// reference owned by AndroidApp, which outlives this call.
let vm = unsafe { JavaVM::from_raw(vm) };
vm.attach_current_thread(|env| {
// Borrow AndroidApp's global reference without taking ownership.
let activity = unsafe { env.as_cast_raw::<JObject>(&activity)? };
// Keep the Application, not an Activity, alive for the process-wide
// verifier. This also supplies the app class loader for its JVM bridge.
let context = env
.call_method(
&activity,
jni_verifier::jni_str!(jni = jni_verifier, "getApplicationContext"),
jni_verifier::jni_sig!(jni = jni_verifier, () -> android.content.Context),
&[],
)?
.l()?;
rustls_platform_verifier::android::init_with_env(env, context)
})
}