diff --git a/README.md b/README.md index 7749daf..506a6f5 100644 --- a/README.md +++ b/README.md @@ -46,8 +46,30 @@ val challengeChecker = ChallengeMatcher("challenge123") val result = verifier.verify(certificateChain, challengeChecker) ``` -If the implementations in challengecheckers/ don't fit your needs, simply extend -the `ChallengeChecker` interface. +If there are multiple checks to perform on the challenge, use a +`ChainedChallengeChecker` to encompass all the individual `ChallengeCheckers`. +Checks in the `ChainedChallengeChecker` halt after the first failure, so take +advantage of this behavior by putting "less expensive" checks first. +For example, if your use case requires the challenge to be equal to an expected +challenge _and_ not seen already (stale), then combine the `ChallengeMatcher` +with an `InMemoryLruCache` like in this sample: + +```kotlin +val cacheSize = 100 + +// Create a ChainedChallengeChecker with desired ChallengeCheckers +val challengeChecker = + ChainedChallengeChecker.of(ChallengeMatcher("expectedChallenge"), InMemoryLruCache(cacheSize)) + +// Verify an attestation certificate chain with the checker +val result = verifier.verify(certificateChain, challengeChecker) +``` + +Here, the `ChallengeMatcher` is used first, so we can avoid the cost of checking +against the `InMemoryLruCache` if the challenge doesn't match. + +If the implementations in `challengecheckers/` don't fit your needs, simply +extend the `ChallengeChecker` interface. ## Building diff --git a/src/main/kotlin/challengecheckers/ChainedChallengeChecker.kt b/src/main/kotlin/challengecheckers/ChainedChallengeChecker.kt new file mode 100644 index 0000000..f8dd393 --- /dev/null +++ b/src/main/kotlin/challengecheckers/ChainedChallengeChecker.kt @@ -0,0 +1,42 @@ +package com.android.keyattestation.verifier.challengecheckers + +import com.android.keyattestation.verifier.ChallengeChecker +import com.google.protobuf.ByteString + +/** + * A [ChallengeChecker] that checks a list of [ChallengeChecker]s in order. + * + * Checks are ordered and halt after the first failure. + */ +class ChainedChallengeChecker(private val challengeCheckers: List) : + ChallengeChecker { + + /** + * Checks the given challenge for validity. + * + * @param challenge the challenge being checked + * @return true if the challenge is valid, else false + */ + override fun checkChallenge(challenge: ByteString): Boolean { + // Manually loop instead of using .all() since we want to ensure order of checks and early + // return on failure. + for (challengeChecker in challengeCheckers) { + if (!challengeChecker.checkChallenge(challenge)) { + return false + } + } + return true + } + + companion object { + /** + * Creates a [ChainedChallengeChecker] with the given [ChallengeChecker]s. + * + * @param challengeCheckers the [ChallengeChecker]s to chain + * @return a [ChainedChallengeChecker] with the given [ChallengeChecker]s + */ + fun of(vararg challengeCheckers: ChallengeChecker): ChainedChallengeChecker { + return ChainedChallengeChecker(challengeCheckers.toList()) + } + } +} diff --git a/src/test/kotlin/challengecheckers/ChainedChallengeCheckerTest.kt b/src/test/kotlin/challengecheckers/ChainedChallengeCheckerTest.kt new file mode 100644 index 0000000..4f6e987 --- /dev/null +++ b/src/test/kotlin/challengecheckers/ChainedChallengeCheckerTest.kt @@ -0,0 +1,79 @@ +package com.android.keyattestation.verifier.challengecheckers + +import com.android.keyattestation.verifier.ChallengeChecker +import com.google.common.truth.Truth.assertThat +import com.google.protobuf.ByteString +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.JUnit4 + +private class TestChallengeChecker(private val result: Boolean) : ChallengeChecker { + var wasCalled = false + + override fun checkChallenge(challenge: ByteString): Boolean { + wasCalled = true + return result + } +} + +@RunWith(JUnit4::class) +class ChainedChallengeCheckerTest { + companion object { + private val testChallenge = ByteString.copyFromUtf8("challenge") + private val falseChecker = + object : ChallengeChecker { + override fun checkChallenge(challenge: ByteString): Boolean = false + } + private val trueChecker = + object : ChallengeChecker { + override fun checkChallenge(challenge: ByteString): Boolean = true + } + } + + @Test + fun checkChallenge_emptyCheckers_returnsTrue() { + val challengeChecker = ChainedChallengeChecker.of() + assertThat(challengeChecker.checkChallenge(testChallenge)).isTrue() + } + + @Test + fun checkChallenge_allCheckersTrue_returnsTrue() { + val challengeChecker = + ChainedChallengeChecker.of(ChallengeMatcher(testChallenge), InMemoryLruCache(10)) + assertThat(challengeChecker.checkChallenge(testChallenge)).isTrue() + } + + @Test + fun checkChallenge_allCheckersFalse_returnsFalse() { + val challengeCheckers: MutableList = mutableListOf() + for (i in 1..10) { + challengeCheckers.add(falseChecker) + } + val challengeChecker = ChainedChallengeChecker(challengeCheckers) + + assertThat(challengeChecker.checkChallenge(testChallenge)).isFalse() + } + + @Test + fun checkChallenge_lastCheckerFalse_returnsFalse() { + val challengeCheckers: MutableList = mutableListOf() + for (i in 1..10) { + challengeCheckers.add(trueChecker) + } + challengeCheckers.add(falseChecker) + val challengeChecker = ChainedChallengeChecker(challengeCheckers) + + assertThat(challengeChecker.checkChallenge(testChallenge)).isFalse() + } + + @Test + fun checkChallenge_firstCheckerFalse_returnsFalseAndStopsEarly() { + val checker2 = TestChallengeChecker(true) + val checker3 = TestChallengeChecker(true) + val challengeChecker = ChainedChallengeChecker.of(falseChecker, checker2, checker3) + + assertThat(challengeChecker.checkChallenge(testChallenge)).isFalse() + assertThat(checker2.wasCalled).isFalse() + assertThat(checker3.wasCalled).isFalse() + } +}