Skip to content
Closed
Show file tree
Hide file tree
Changes from all 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
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,9 @@ Some manual setup is required, outside of `cargo`, to use this crate on Android.
use Android's certificate verifier, the crate needs to call into the JVM. A small Kotlin
component must be included in your app's build to support `rustls-platform-verifier`.

Certificate verification runs on the calling thread unless it is Android's main thread. In that case, verification runs on a Rust worker thread because Android's revocation checks may perform network I/O. Verification is still synchronous, because the calling thread waits for the result.
Run TLS handshakes on a background thread or dispatcher to keep the Android UI responsive.

#### Gradle Setup

`rustls-platform-verifier` distributes the required native components in a Maven-compatible format via GitHub, but the project must be setup to locate them
Expand Down
Original file line number Diff line number Diff line change
@@ -1,13 +1,16 @@
package org.rustls.platformverifier

import android.content.Context
import android.os.Looper
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.platform.app.InstrumentationRegistry
import org.junit.Assert.assertEquals
import org.junit.Assert.assertTrue
import org.junit.BeforeClass
import org.junit.Test
import org.junit.runner.RunWith
import java.util.concurrent.FutureTask
import kotlin.concurrent.thread

private const val SUCCESS_MARKER: String = "success"
private const val FAILURE_MSG: String = "A test failed. Check the logs above for Rust panics."
Expand All @@ -33,9 +36,47 @@ class CertificateVerifierTests {

@Test
fun runMockTestSuite() {
assertEquals(null, Looper.myLooper())
runMockTestSuiteOnCallingThread()
}

private fun runMockTestSuiteOnCallingThread() {
assertTrue(Looper.myLooper() != Looper.getMainLooper())
val context = InstrumentationRegistry.getInstrumentation().targetContext
val result = mockTests(context)
assertEquals(FAILURE_MSG, SUCCESS_MARKER, result)
CertificateVerifier.expectedVerificationThread = Thread.currentThread()
try {
val result = mockTests(context)
assertEquals(FAILURE_MSG, SUCCESS_MARKER, result)
} finally {
CertificateVerifier.expectedVerificationThread = null
}
}

@Test
fun runMockTestSuiteOnBackgroundThreadWithLooper() {
val task = FutureTask {
Looper.prepare()
assertTrue(Looper.myLooper() != null)
runMockTestSuiteOnCallingThread()
}
val worker = thread { task.run() }
try {
task.get()
} finally {
worker.join()
}
}

@Test
fun runMockTestSuiteOnMainThread() {
val instrumentation = InstrumentationRegistry.getInstrumentation()
instrumentation.runOnMainSync {
assertEquals(Looper.getMainLooper(), Looper.myLooper())
// The test build of CertificateVerifier checks that JNI verification runs off the main
// thread, even when the Rust caller is on it.
val result = mockTests(instrumentation.targetContext)
assertEquals(FAILURE_MSG, SUCCESS_MARKER, result)
}
}

@Test
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import android.annotation.SuppressLint
import android.content.Context
import android.net.http.X509TrustManagerExtensions
import android.os.Build
import android.os.Looper
import android.util.Log
import java.io.ByteArrayInputStream
import java.io.File
Expand Down Expand Up @@ -96,6 +97,9 @@ internal object CertificateVerifier {
// Ideally, all of this will be optimized out at compile time due to not being accessed
// in release builds.

@Volatile
var expectedVerificationThread: Thread? = null

@get:Synchronized
private val mockKeystore: KeyStore = KeyStore.getInstance(KeyStore.getDefaultType())

Expand Down Expand Up @@ -188,6 +192,17 @@ internal object CertificateVerifier {
time: Long,
certChain: Array<ByteArray>,
): VerificationResult {
if (BuildConfig.TEST) {
check(Looper.myLooper() != Looper.getMainLooper()) {
"Certificate verification must run off the main thread"
}
expectedVerificationThread?.let { expected ->
check(Thread.currentThread() === expected) {
"Background verification must stay on the calling thread"
}
}
}

// Convert the array of (supposedly) DER bytes into certificates.
val certificateChain = mutableListOf<X509Certificate>()
certChain.forEach { certBytes ->
Expand Down
28 changes: 28 additions & 0 deletions rustls-platform-verifier/src/android.rs
Original file line number Diff line number Diff line change
Expand Up @@ -204,6 +204,34 @@ impl<'env> LocalContext<'_, 'env> {
}
}

/// Checks whether the current thread is Android's main thread.
pub(super) fn is_main_thread() -> Result<bool, Error> {
global().vm().attach_current_thread_for_scope(|env| {
let looper = env.find_class(jni_str!("android/os/Looper"))?;
let current = env
.call_static_method(
&looper,
jni_str!("myLooper"),
jni_sig!(() -> android.os.Looper),
&[],
)?
.l()?;
if current.is_null() {
return Ok(false);
}

let main = env
.call_static_method(
&looper,
jni_str!("getMainLooper"),
jni_sig!(() -> android.os.Looper),
&[],
)?
.l()?;
Ok(env.is_same_object(current, main)?)
})
}

/// Borrow the Android application context and execute the closure
/// `with_context, ensuring locals are properly freed and exceptions
/// are cleared.
Expand Down
33 changes: 31 additions & 2 deletions rustls-platform-verifier/src/verification/android.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,10 @@ use rustls::{
CertificateError, DigitallySignedStruct, Error as TlsError, OtherError, SignatureScheme,
};
use std::sync::Arc;
use std::thread;

use super::{log_server_cert, ALLOWED_EKUS};
use crate::android::{with_context, CachedClass};
use crate::android::{is_main_thread, with_context, CachedClass};

static CERT_VERIFIER_CLASS: CachedClass =
CachedClass::new(jni_str!("org.rustls.platformverifier.CertificateVerifier"));
Expand Down Expand Up @@ -293,7 +294,35 @@ impl ServerCertVerifier for Verifier {
None
};

match self.verify_certificate(end_entity, intermediates, server_name, ocsp_data, now) {
let verify =
|| self.verify_certificate(end_entity, intermediates, server_name, ocsp_data, now);

// JNI calls run on the calling thread. Android's revocation checks can perform network
// I/O, so move them to a worker thread when called on Android's main thread to avoid
// NetworkOnMainThreadException.
let result = match is_main_thread() {
Ok(false) => verify(),
Ok(true) => thread::scope(|scope| {
let worker = thread::Builder::new()
.name("rustls-platform-verifier".into())
.spawn_scoped(scope, verify)
.map_err(|e| {
TlsError::General(format!(
"failed to spawn certificate verification thread: {e}"
))
})?;

match worker.join() {
Ok(result) => result,
Err(panic) => std::panic::resume_unwind(panic),
}
}),
Err(e) => Err(TlsError::General(format!(
"failed to determine Android verification thread: {e:?}"
))),
};

match result {
Ok(()) => Ok(rustls::client::danger::ServerCertVerified::assertion()),
Err(e) => {
// This error only tells us what the system errored with, so it doesn't leak anything
Expand Down