Skip to content

Commit

Permalink
fix: async attestation call causes a deadlock on ssl context override
Browse files Browse the repository at this point in the history
  • Loading branch information
boilsquid committed Dec 6, 2024
1 parent 2ac2d8c commit 22f7d66
Show file tree
Hide file tree
Showing 11 changed files with 359 additions and 237 deletions.
15 changes: 15 additions & 0 deletions .changeset/fair-pots-suffer.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
---
"evervault-android": patch
---

A deadlock can occur when `AttestationTrustManagerGA` is initialized and a request to get the
attestation doc from the enclave are made simultaneously. This is dues to the modification of the
`SSLContext` when a request is made which would lead to a read timeout in OkHTTP Client.

- [3a93fedf814c37ed698fa46432e758ac4f3bd885](https://github.com/evervault/evervault-android/commit/3a93fedf814c37ed698fa46432e758ac4f3bd885):
`enclavesTrustManager` will block to initialize the cache before modifying the
`AttestationTrustManagerGA` and subsequently the `SSLContext`
- The polling logic has been changed to delay before populating the cache to prevent duplicate requests
- `get()` calls to the cache are now always synchronous to as it is not called from a coroutine.


13 changes: 10 additions & 3 deletions evervault-enclaves/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -59,23 +59,30 @@ android {
}
}

val kotlinVersion = "1.8.0"
val kotlinCoroutineVersion = "1.7.3"

dependencies {
implementation("com.evervault.sdk:evervault-core:1.2")
implementation("androidx.core:core-ktx:1.8.0")
implementation("androidx.core:core-ktx:$kotlinVersion")
implementation(platform("org.jetbrains.kotlin:kotlin-bom:1.8.0"))
implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.3.1")
implementation("androidx.activity:activity-compose:1.5.1")
implementation("net.java.dev.jna:jna:5.7.0@aar")
implementation("com.squareup.okhttp3:okhttp:4.11.0")
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.3.2")
implementation("com.squareup.okhttp3:okhttp:4.9.3")
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.5.2")
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:$kotlinCoroutineVersion")
testImplementation("junit:junit:4.13.2")
testImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:1.7.3")
testImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:$kotlinCoroutineVersion")
testImplementation("org.mockito.kotlin:mockito-kotlin:5.0.0")
testImplementation("com.squareup.retrofit2:converter-gson:2.9.0")
androidTestImplementation("com.squareup.okhttp3:mockwebserver:4.12.0")
androidTestImplementation("androidx.test.ext:junit:1.1.5")
androidTestImplementation("androidx.test.espresso:espresso-core:3.5.1")
androidTestImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:$kotlinCoroutineVersion")
androidTestImplementation("androidx.test:runner:1.5.2")
androidTestImplementation("androidx.test.ext:junit:1.1.5")
}

publishing {
Expand Down
4 changes: 4 additions & 0 deletions evervault-enclaves/src/androidTest/AndroidManifest.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<uses-permission android:name="android.permission.INTERNET"/>
</manifest>
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
import android.util.Base64
import androidx.test.ext.junit.runners.AndroidJUnit4
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.UnconfinedTestDispatcher
import kotlinx.coroutines.test.runTest
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
import okhttp3.mockwebserver.MockResponse
import okhttp3.mockwebserver.MockWebServer
import org.junit.After
import org.junit.Assert.assertArrayEquals
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import java.util.concurrent.TimeUnit
import kotlin.time.Duration.Companion.seconds

@OptIn(ExperimentalCoroutinesApi::class)
@RunWith(AndroidJUnit4::class)
class AttestationDocCacheTest {
private lateinit var mockWebServer: MockWebServer
private lateinit var cache: AttestationDocCache
private val testDispatcher = UnconfinedTestDispatcher()

@Before
fun setup() {
mockWebServer = MockWebServer()
mockWebServer.start()

cache = AttestationDocCache(
enclaveName = "test-enclave",
appUuid = "test-uuid",
enclaveUrl = mockWebServer.url("/").toString()
)
}

@After
fun tearDown() {
mockWebServer.shutdown()
}

@Test
fun initializeFetchesAndStoresAttestationDoc() = runTest(testDispatcher) {
val testDoc = "test-attestation-doc"
val encodedDoc = Base64.encodeToString(
testDoc.toByteArray(),
Base64.DEFAULT
)
val attestationDoc = AttestationDoc(attestationDoc = encodedDoc)

mockWebServer.enqueue(
MockResponse()
.setResponseCode(200)
.setBody(Json.encodeToString(attestationDoc))
)

cache.initialize()

assertArrayEquals(testDoc.toByteArray(), cache.get())
}

@Test(expected = IllegalStateException::class)
fun getThrowsWhenNotInitialized() {
cache.get()
}

@Test
fun initializeRetriesOnFailure() = runTest(testDispatcher) {
val testDoc = "test-attestation-doc"
val encodedDoc = Base64.encodeToString(
testDoc.toByteArray(),
Base64.DEFAULT
)
val attestationDoc = AttestationDoc(attestationDoc = encodedDoc)

mockWebServer.enqueue(MockResponse().setResponseCode(500))
mockWebServer.enqueue(MockResponse().setResponseCode(500))
mockWebServer.enqueue(
MockResponse()
.setResponseCode(200)
.setBody(Json.encodeToString(attestationDoc))
)

cache.initialize()

assert(mockWebServer.requestCount == 3)
assertArrayEquals(testDoc.toByteArray(), cache.get())
}

@Test
fun initializeGivesUpAfterMaxAttempts() = runTest(testDispatcher) {
repeat(3) {
mockWebServer.enqueue(MockResponse().setResponseCode(500))
}

cache.initialize()

assert(mockWebServer.requestCount == 3)

try {
cache.get()
assert(false) { "Expected IllegalStateException" }
} catch (e: IllegalStateException) {
// Expected
}
}

@Test
fun requestTimeoutsAreRespected() = runTest(testDispatcher, timeout = 60.seconds) {
val testDoc = "test-attestation-doc"
val encodedDoc = Base64.encodeToString(
testDoc.toByteArray(),
Base64.DEFAULT
)
val attestationDoc = AttestationDoc(attestationDoc = encodedDoc)

mockWebServer.enqueue(
MockResponse()
.setBodyDelay(11, TimeUnit.SECONDS)
.setResponseCode(200)
.setBody(Json.encodeToString(attestationDoc))
)

cache.initialize()
assert(mockWebServer.requestCount == 3)
}
}

This file was deleted.

Original file line number Diff line number Diff line change
@@ -1,76 +1,103 @@
import android.content.ContentValues.TAG
import java.io.IOException
import java.util.concurrent.locks.ReentrantReadWriteLock
import kotlin.concurrent.read
import kotlin.concurrent.write
import android.util.Base64
import android.util.Log
import kotlinx.coroutines.*
import kotlinx.serialization.SerialName
import okhttp3.OkHttpClient
import okhttp3.Request
import kotlinx.serialization.Serializable
import kotlinx.serialization.json.Json
import android.util.Base64
import android.util.Log
import java.lang.Exception
import okhttp3.Interceptor.*
import okhttp3.OkHttpClient
import okhttp3.Request
import java.io.IOException
import java.util.concurrent.TimeUnit
import java.util.concurrent.locks.ReentrantReadWriteLock
import kotlin.concurrent.read
import kotlin.concurrent.write


@Serializable
data class AttestationDoc(
@SerialName("attestation_doc") val attestationDoc: String,
)
@OptIn(DelicateCoroutinesApi::class)
class AttestationDocCache(private val enclaveName: String, private val appUuid: String) {
class AttestationDocCache(
private val enclaveName: String,
private val appUuid: String,
private val enclaveHostname: String? = "enclave.evervault.com",
) {
private var attestationDoc: ByteArray = ByteArray(0)
private val lock = ReentrantReadWriteLock()
private val maxAttempts = 3
private val initialDelayMs = 1000L
private var pollingJob: Job? = null
private val client: OkHttpClient = OkHttpClient.Builder()
.callTimeout(10000, TimeUnit.MILLISECONDS)
.connectTimeout(10000, TimeUnit.MILLISECONDS)
.readTimeout(10000, TimeUnit.MILLISECONDS)
.build()

init {
GlobalScope.launch(Dispatchers.IO) {
storeDoc(2)
poll(300)
}
}
private suspend fun storeDocWithBackoff() {
var currentAttempt = 0

private fun storeDoc(retries: Int) {
if(retries >= 0) {
while (currentAttempt < maxAttempts) {
try {
val url =
"https://${enclaveName}.${appUuid}.enclave.evervault.com/.well-known/attestation"
val url = "https://${enclaveName}.${appUuid}.${enclaveHostname}/.well-known/attestation"
val response = getDocFromEnclave(url)
val decodedDoc = Base64.decode(response.attestationDoc, Base64.DEFAULT)
set(decodedDoc)
return
} catch (e: Exception) {
Log.w(TAG, "Failed to get attestationdoc ${e.message}")
storeDoc(retries - 1)
currentAttempt++
if (currentAttempt == maxAttempts) {
return
}

val delayMs = initialDelayMs * (1 shl (currentAttempt - 1))
Log.d("AttestationDocCache", "Populating doc from cached failed retry count: $currentAttempt, delay $delayMs")
Log.d("AttestationDocCache", "Exception getting attestation doc", e)
delay(delayMs)
}
}
}

suspend fun initialize() {
if (attestationDoc.isEmpty()) {
withContext(Dispatchers.IO) {
storeDocWithBackoff()
}

pollingJob = pollingJob ?: GlobalScope.launch(Dispatchers.IO) {
poll(300)
}
}
}

fun get(): ByteArray {
return lock.read {
if(attestationDoc.isEmpty()) {
storeDoc(2)
throw IllegalStateException("Attestation doc not initialized. Call initialize() first")
}
attestationDoc
}
}

private fun set(value: ByteArray) {
lock.write { attestationDoc = value }
}

private suspend fun poll(n: Long) {
while (true) {
storeDoc(2)
delay(n * 1000)
Log.d("AttestationDocCache", "Populate cache with doc from polling")
storeDocWithBackoff()
}
}

private fun set(value: ByteArray) {
lock.write { attestationDoc = value }
}

private fun getDocFromEnclave(url: String): AttestationDoc {
val client = OkHttpClient()
val request = Request.Builder().url(url).build()

Log.d("AttestationDocCache", "Retrieving attestation doc from: $url")
client.newCall(request).execute().use { response ->
if (!response.isSuccessful) throw IOException("Unexpected code $response")
return Json.decodeFromString(response.body?.string() ?: "")
}
}
}

Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package com.evervault.sdk.enclaves

import AttestationDocCache
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.runBlocking
import okhttp3.OkHttpClient
import uniffi.bindings.PcRs
import uniffi.bindings.attestEnclave
Expand Down Expand Up @@ -53,8 +55,13 @@ class AttestationTrustManagerGA(private val enclaveAttestationData: AttestationD
}
}

fun OkHttpClient.Builder.enclavesTrustManager(enclaveAttestationData: AttestationData, appUuid: String): OkHttpClient.Builder {
val cache = AttestationDocCache(enclaveAttestationData.enclaveName, appUuid)
fun OkHttpClient.Builder.enclavesTrustManager(enclaveAttestationData: AttestationData, appUuid: String, enclaveHostname: String? = null): OkHttpClient.Builder {
val cache = AttestationDocCache(enclaveAttestationData.enclaveName, appUuid, enclaveHostname)

runBlocking(Dispatchers.IO) {
cache.initialize()
}

val attestEnclaveCallback: AttestEnclaveCallback = { remoteCertificateData, expectedPCRs, attestationDoc ->
attestEnclave(remoteCertificateData, expectedPCRs, attestationDoc)
}
Expand Down
8 changes: 5 additions & 3 deletions sampleapplication/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,10 @@ android {
useSupportLibrary = true
}

buildConfigField("String", "ENCLAVE_UUID", "\"hello-enclave\"")
buildConfigField("String", "APP_UUID", "\"app-33b88ca7da0d\"")
buildConfigField("String", "PCR_CALLBACK_URL", "\"https://blackhole.posterior.io/0xljnh\"")
buildConfigField("String", "ENCLAVE_UUID", "\"\"")
buildConfigField("String", "APP_UUID", "\"\"")
buildConfigField("String", "PCR_CALLBACK_URL", "\"\"")
buildConfigField("String", "ENCLAVE_HOSTNAME", "\"\"")
}

buildTypes {
Expand Down Expand Up @@ -78,6 +79,7 @@ dependencies {
androidTestImplementation("androidx.test.espresso:espresso-core:3.4.0")
androidTestImplementation(platform("androidx.compose:compose-bom:2022.10.00"))
androidTestImplementation("androidx.compose.ui:ui-test-junit4")
androidTestImplementation("androidx.test.uiautomator:uiautomator:2.3.0-alpha03")
debugImplementation("androidx.compose.ui:ui-tooling")
debugImplementation("androidx.compose.ui:ui-test-manifest")
}
Loading

0 comments on commit 22f7d66

Please sign in to comment.