Skip to content

Commit a2c8b90

Browse files
suzannajiwanicopybara-github
authored andcommitted
Modify Verifier API to support asynchronous calls
This is necessary to support challenge checking which may require extra time, for example checking a database. PiperOrigin-RevId: 824686748
1 parent 3705522 commit a2c8b90

3 files changed

Lines changed: 101 additions & 30 deletions

File tree

src/main/kotlin/Verifier.kt

Lines changed: 43 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -20,19 +20,24 @@ import com.android.keyattestation.verifier.provider.KeyAttestationCertPath
2020
import com.android.keyattestation.verifier.provider.KeyAttestationProvider
2121
import com.android.keyattestation.verifier.provider.ProvisioningMethod
2222
import com.android.keyattestation.verifier.provider.RevocationChecker
23+
import com.google.common.collect.ImmutableList
24+
import com.google.common.util.concurrent.ListenableFuture
2325
import com.google.errorprone.annotations.ThreadSafe
2426
import com.google.protobuf.ByteString
2527
import com.google.protobuf.kotlin.toByteString
2628
import java.security.PublicKey
2729
import java.security.Security
2830
import java.security.cert.CertPathValidator
2931
import java.security.cert.CertPathValidatorException
32+
import java.security.cert.CertificateException
3033
import java.security.cert.PKIXCertPathValidatorResult
3134
import java.security.cert.PKIXParameters
3235
import java.security.cert.TrustAnchor
3336
import java.security.cert.X509Certificate
3437
import java.util.Date
38+
import kotlinx.coroutines.CoroutineScope
3539
import kotlinx.coroutines.guava.await
40+
import kotlinx.coroutines.guava.future
3641
import kotlinx.coroutines.runBlocking
3742

3843
/** The result of verifying an Android Key Attestation certificate chain. */
@@ -58,6 +63,7 @@ sealed interface VerificationResult {
5863
}
5964

6065
/** Interface for logging info level key attestation events and information. */
66+
@ThreadSafe
6167
interface LogHook {
6268

6369
/**
@@ -133,6 +139,7 @@ open class Verifier(
133139
*
134140
* @param chain The attestation certificate chain to verify.
135141
* @param challengeChecker The challenge checker to use for additional challenge validation.
142+
* @param log The log hook to use for logging.
136143
* @return [VerificationResult]
137144
*/
138145
@JvmOverloads
@@ -141,20 +148,49 @@ open class Verifier(
141148
challengeChecker: ChallengeChecker? = null,
142149
log: LogHook? = null,
143150
): VerificationResult {
144-
val certPath =
151+
val result =
145152
try {
146-
KeyAttestationCertPath(chain)
147-
} catch (e: Exception) {
148-
val result = VerificationResult.ChainParsingFailure(e)
153+
val certPath = KeyAttestationCertPath(chain)
154+
runBlocking { internalVerify(certPath, challengeChecker, log) }
155+
} catch (e: CertificateException) {
149156
log?.logInputChain(chain.map { it.getEncoded().toByteString() })
150-
log?.logResult(result)
151-
return result
157+
VerificationResult.ChainParsingFailure(e)
152158
}
153-
val result = runBlocking { internalVerify(certPath, challengeChecker, log) }
154159
log?.logResult(result)
155160
return result
156161
}
157162

163+
/**
164+
* Verifies an Android Key Attestation certificate chain asynchronously.
165+
*
166+
* @param chain The attestation certificate chain to verify.
167+
* @param coroutineScope The coroutine scope to from which to run the verification.
168+
* @param challengeChecker The challenge checker to use for additional challenge validation.
169+
* @param log The log hook to use for logging.
170+
* @return A [ListenableFuture] containing the [VerificationResult].
171+
*/
172+
@JvmOverloads
173+
fun verifyAsync(
174+
coroutineScope: CoroutineScope,
175+
chain: List<X509Certificate>,
176+
challengeChecker: ChallengeChecker? = null,
177+
log: LogHook? = null,
178+
): ListenableFuture<VerificationResult> {
179+
val immutableChain = ImmutableList.copyOf(chain)
180+
return coroutineScope.future {
181+
val result =
182+
try {
183+
val certPath = KeyAttestationCertPath(immutableChain)
184+
internalVerify(certPath, challengeChecker, log)
185+
} catch (e: CertificateException) {
186+
log?.logInputChain(immutableChain.map { it.getEncoded().toByteString() })
187+
VerificationResult.ChainParsingFailure(e)
188+
}
189+
log?.logResult(result)
190+
result
191+
}
192+
}
193+
158194
private suspend fun internalVerify(
159195
certPath: KeyAttestationCertPath,
160196
challengeChecker: ChallengeChecker? = null,

src/main/kotlin/testing/FakeLogHook.kt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import com.android.keyattestation.verifier.KeyDescription
2020
import com.android.keyattestation.verifier.LogHook
2121
import com.android.keyattestation.verifier.ProvisioningInfoMap
2222
import com.android.keyattestation.verifier.VerificationResult
23+
import com.google.errorprone.annotations.ThreadSafe
2324
import com.google.protobuf.ByteString
2425

2526
/**

src/test/kotlin/VerifierTest.kt

Lines changed: 57 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,8 @@ import com.android.keyattestation.verifier.testing.TestUtils.readCertList
2727
import com.android.keyattestation.verifier.testing.TestUtils.readJson
2828
import com.android.keyattestation.verifier.testing.TestUtils.trueChecker
2929
import com.google.common.truth.Truth.assertThat
30+
import com.google.common.util.concurrent.Futures
31+
import com.google.common.util.concurrent.ListenableFuture
3032
import com.google.protobuf.ByteString
3133
import com.google.protobuf.kotlin.toByteString
3234
import com.google.testing.junit.testparameterinjector.TestParameter
@@ -36,14 +38,29 @@ import java.security.cert.TrustAnchor
3638
import java.time.Instant
3739
import java.time.LocalDate
3840
import java.time.ZoneOffset
41+
import java.util.concurrent.Executors
42+
import java.util.concurrent.TimeUnit
3943
import kotlin.test.assertIs
44+
import kotlinx.coroutines.guava.await
45+
import kotlinx.coroutines.runBlocking
4046
import org.junit.Test
4147
import org.junit.runner.RunWith
4248

4349
/** Unit tests for [Verifier]. */
4450
@RunWith(TestParameterInjector::class)
4551
class VerifierTest {
4652
private val verifier = Verifier({ prodAnchors }, { setOf<String>() }, { Instant.now() })
53+
private val delayedAlwaysTrueChecker =
54+
object : ChallengeChecker {
55+
override fun checkChallenge(challenge: ByteString): ListenableFuture<Boolean> {
56+
return Futures.scheduleAsync(
57+
{ Futures.immediateFuture(true) },
58+
5,
59+
TimeUnit.SECONDS,
60+
Executors.newSingleThreadScheduledExecutor(),
61+
)
62+
}
63+
}
4764

4865
@Test
4966
fun verify_validChain_returnsSuccess(@TestParameter testCase: TestCase) {
@@ -74,16 +91,16 @@ class VerifierTest {
7491
}
7592

7693
@Test
77-
fun verify_validChainUsingGeneratedTrustAnchors_returnsSuccess() {
94+
fun verifyAsync_validChainUsingGeneratedTrustAnchors_returnsSuccess(): Unit = runBlocking {
7895
val verifier = Verifier(GoogleTrustAnchors, { setOf<String>() }, { Instant.now() })
7996
val chain = readCertList("blueline/sdk28/TEE_EC_NONE.pem")
80-
assertIs<VerificationResult.Success>(verifier.verify(chain))
97+
assertIs<VerificationResult.Success>(verifier.verifyAsync(this, chain).await())
8198
}
8299

83100
@Test
84-
fun verify_validChain_returnsDeviceIdentity() {
101+
fun verifyAsync_validChain_returnsDeviceIdentity() = runBlocking {
85102
val chain = readCertList("blueline/sdk28/TEE_RSA_BASE+IMEI.pem")
86-
val result = assertIs<VerificationResult.Success>(verifier.verify(chain))
103+
val result = assertIs<VerificationResult.Success>(verifier.verifyAsync(this, chain).await())
87104
assertThat(result.attestedDeviceIds)
88105
.isEqualTo(
89106
DeviceIdentity(
@@ -100,55 +117,63 @@ class VerifierTest {
100117
}
101118

102119
@Test
103-
fun verify_challengeCheckerReturnsTrue_returnsSuccess() {
120+
fun verifyAsync_challengeCheckerReturnsTrue_returnsSuccess(): Unit = runBlocking {
104121
val chain = readCertList("blueline/sdk28/TEE_EC_NONE.pem")
105122

106-
assertIs<VerificationResult.Success>(verifier.verify(chain, trueChecker))
123+
assertIs<VerificationResult.Success>(verifier.verifyAsync(this, chain, trueChecker).await())
107124
}
108125

109126
@Test
110-
fun verify_challengeCheckerReturnsFalse_returnsChallengeMismatch() {
127+
fun verifyAsync_challengeCheckerReturnsFalse_returnsChallengeMismatch(): Unit = runBlocking {
111128
val chain = readCertList("blueline/sdk28/TEE_EC_NONE.pem")
112129

113-
assertIs<VerificationResult.ChallengeMismatch>(verifier.verify(chain, falseChecker))
130+
assertIs<VerificationResult.ChallengeMismatch>(
131+
verifier.verifyAsync(this, chain, falseChecker).await()
132+
)
114133
}
115134

116135
@Test
117-
fun verify_unexpectedRootKey_returnsPathValidationFailure() {
136+
fun verifyAsync_unexpectedRootKey_returnsPathValidationFailure() = runBlocking {
118137
val result =
119138
assertIs<VerificationResult.PathValidationFailure>(
120-
verifier.verify(
121-
CertLists.wrongTrustAnchor,
122-
ChallengeMatcher(ByteString.copyFromUtf8("challenge")),
123-
)
139+
verifier
140+
.verifyAsync(
141+
this,
142+
CertLists.wrongTrustAnchor,
143+
ChallengeMatcher(ByteString.copyFromUtf8("challenge")),
144+
)
145+
.await()
124146
)
125147
assertThat(result.cause.reason).isEqualTo(PKIXReason.NO_TRUST_ANCHOR)
126148
}
127149

128150
@Test
129-
fun verify_failure_inputChainLogged() {
151+
fun verifyAsync_failure_inputChainLogged() = runBlocking {
130152
val logHook = FakeLogHook()
131153
assertIs<VerificationResult.PathValidationFailure>(
132-
verifier.verify(
133-
CertLists.wrongTrustAnchor,
134-
ChallengeMatcher(ByteString.copyFromUtf8("challenge")),
135-
logHook,
136-
)
154+
verifier
155+
.verifyAsync(
156+
this,
157+
CertLists.wrongTrustAnchor,
158+
ChallengeMatcher(ByteString.copyFromUtf8("challenge")),
159+
logHook,
160+
)
161+
.await()
137162
)
138163
assertThat(logHook.inputChain)
139164
.isEqualTo(CertLists.wrongTrustAnchor.map { it.encoded.toByteString() })
140165
}
141166

142167
@Test
143-
fun verify_success_keyDescriptionLogged() {
168+
fun verifyAsync_success_keyDescriptionLogged() = runBlocking {
144169
val logHook = FakeLogHook()
145170
val chain = readCertList("blueline/sdk28/TEE_EC_NONE.pem")
146-
assertIs<VerificationResult.Success>(verifier.verify(chain, log = logHook))
171+
assertIs<VerificationResult.Success>(verifier.verifyAsync(this, chain, log = logHook).await())
147172
assertThat(logHook.keyDescription).isEqualTo(chain[0].keyDescription())
148173
}
149174

150175
@Test
151-
fun verify_malformedPatchLevel_logsInfo() {
176+
fun verifyAsync_malformedPatchLevel_logsInfo() = runBlocking {
152177
val verifierWithTestRoot =
153178
Verifier(
154179
{ setOf(TrustAnchor(Certs.root, null)) },
@@ -157,8 +182,17 @@ class VerifierTest {
157182
)
158183
val logHook = FakeLogHook()
159184
assertIs<VerificationResult.Success>(
160-
verifierWithTestRoot.verify(CertLists.invalidBootPatchLevel, log = logHook)
185+
verifierWithTestRoot.verifyAsync(this, CertLists.invalidBootPatchLevel, log = logHook).await()
161186
)
162187
assertThat(logHook.infoMessages).isNotEmpty()
163188
}
189+
190+
@Test
191+
fun verifyAsync_longDelay_successfullyAwaitsChallengeCheck(): Unit = runBlocking {
192+
val chain = readCertList("blueline/sdk28/TEE_EC_NONE.pem")
193+
194+
assertIs<VerificationResult.Success>(
195+
verifier.verifyAsync(this, chain, delayedAlwaysTrueChecker).await()
196+
)
197+
}
164198
}

0 commit comments

Comments
 (0)