-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
fix: async attestation call causes a deadlock on ssl context override
- Loading branch information
Showing
11 changed files
with
359 additions
and
237 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,15 @@ | ||
--- | ||
"evervault-android": minor | ||
--- | ||
|
||
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. | ||
|
||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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> |
127 changes: 127 additions & 0 deletions
127
...vault-enclaves/src/androidTest/java/com/evervault/sdk/enclaves/AttestationDocCacheTest.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | ||
} | ||
} |
24 changes: 0 additions & 24 deletions
24
...vault-enclaves/src/androidTest/java/com/evervault/sdk/enclaves/ExampleInstrumentedTest.kt
This file was deleted.
Oops, something went wrong.
91 changes: 59 additions & 32 deletions
91
evervault-enclaves/src/main/java/com/evervault/sdk/enclaves/AttestationDocCache.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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() ?: "") | ||
} | ||
} | ||
} | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.