From 2ee87d1e07ef6af6b57025e596f5d9de6541bbde Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Romero?= Date: Tue, 13 Oct 2026 20:51:31 +0000 Subject: [PATCH] chore: initialize Android TLS verification --- platform/android/Cargo.lock | 2 ++ platform/android/Cargo.toml | 2 ++ platform/android/gradle/app/build.gradle.kts | 3 ++ .../flectar/mail/CertificateVerifierTest.java | 30 +++++++++++++++++++ platform/android/gradle/settings.gradle.kts | 25 ++++++++++++++++ platform/android/src/lib.rs | 8 ++++- platform/android/src/tls.rs | 30 +++++++++++++++++++ 7 files changed, 99 insertions(+), 1 deletion(-) create mode 100644 platform/android/gradle/app/src/androidTest/java/com/flectar/mail/CertificateVerifierTest.java create mode 100644 platform/android/src/tls.rs diff --git a/platform/android/Cargo.lock b/platform/android/Cargo.lock index d4581712..b1089323 100644 --- a/platform/android/Cargo.lock +++ b/platform/android/Cargo.lock @@ -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", diff --git a/platform/android/Cargo.toml b/platform/android/Cargo.toml index c4099ad3..62e4f243 100644 --- a/platform/android/Cargo.toml +++ b/platform/android/Cargo.toml @@ -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", diff --git a/platform/android/gradle/app/build.gradle.kts b/platform/android/gradle/app/build.gradle.kts index 6e46f519..713c0669 100644 --- a/platform/android/gradle/app/build.gradle.kts +++ b/platform/android/gradle/app/build.gradle.kts @@ -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") diff --git a/platform/android/gradle/app/src/androidTest/java/com/flectar/mail/CertificateVerifierTest.java b/platform/android/gradle/app/src/androidTest/java/com/flectar/mail/CertificateVerifierTest.java new file mode 100644 index 00000000..84c34d65 --- /dev/null +++ b/platform/android/gradle/app/src/androidTest/java/com/flectar/mail/CertificateVerifierTest.java @@ -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)); + } +} diff --git a/platform/android/gradle/settings.gradle.kts b/platform/android/gradle/settings.gradle.kts index 900fe383..fbf54961 100644 --- a/platform/android/gradle/settings.gradle.kts +++ b/platform/android/gradle/settings.gradle.kts @@ -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") } + } } } diff --git a/platform/android/src/lib.rs b/platform/android/src/lib.rs index 1d8ef009..25494593 100644 --- a/platform/android/src/lib.rs +++ b/platform/android/src/lib.rs @@ -111,10 +111,14 @@ fn app_cache_dir(app: &slint::android::AndroidApp) -> Result 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::(&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) + }) +}