Skip to content
Merged
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
26 changes: 24 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
42 changes: 42 additions & 0 deletions src/main/kotlin/challengecheckers/ChainedChallengeChecker.kt
Original file line number Diff line number Diff line change
@@ -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>) :
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())
}
}
}
79 changes: 79 additions & 0 deletions src/test/kotlin/challengecheckers/ChainedChallengeCheckerTest.kt
Original file line number Diff line number Diff line change
@@ -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<ChallengeChecker> = 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<ChallengeChecker> = 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()
}
}