Skip to content

Commit dac4f50

Browse files
suzannajiwanicopybara-github
authored andcommitted
Introduce ChallengeChecker to keyattestation library
PiperOrigin-RevId: 777707059
1 parent 1e0e9fe commit dac4f50

File tree

9 files changed

+175
-40
lines changed

9 files changed

+175
-40
lines changed

README.md

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,8 @@ val verifier = Verifier(
1212
{ Instant.now() } // Time source
1313
)
1414

15-
// Verify an attestation certificate chain with challenge
16-
val result = verifier.verify(certificateChain, challenge)
15+
// Verify an attestation certificate chain
16+
val result = verifier.verify(certificateChain)
1717

1818
// Handle the verification result
1919
when (result) {
@@ -32,6 +32,22 @@ when (result) {
3232
}
3333
```
3434

35+
If there is additional verification you'd like to perform on the challenge
36+
associated with the attestation certificate chain, pass in a `ChallengeChecker`
37+
when verifying. For example, if you expect the challenge to be equal to
38+
"challenge123", then usage would look like
39+
40+
```kotlin
41+
// Create a ChallengeChecker
42+
val challengeChecker = ChallengeMatcher("challenge123")
43+
44+
// Verify an attestation certificate chain with the checker
45+
val result = verifier.verify(certificateChain, challengeChecker)
46+
```
47+
48+
If the implementations in challengecheckers/ don't fit your needs, simply extend
49+
the `ChallengeChecker` interface.
50+
3551
## Building
3652

3753
```bash

roots.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
[
2+
"-----BEGIN CERTIFICATE-----\nMIIFHDCCAwSgAwIBAgIJAPHBcqaZ6vUdMA0GCSqGSIb3DQEBCwUAMBsxGTAXBgNV\nBAUTEGY5MjAwOWU4NTNiNmIwNDUwHhcNMjIwMzIwMTgwNzQ4WhcNNDIwMzE1MTgw\nNzQ4WjAbMRkwFwYDVQQFExBmOTIwMDllODUzYjZiMDQ1MIICIjANBgkqhkiG9w0B\nAQEFAAOCAg8AMIICCgKCAgEAr7bHgiuxpwHsK7Qui8xUFmOr75gvMsd/dTEDDJdS\nSxtf6An7xyqpRR90PL2abxM1dEqlXnf2tqw1Ne4Xwl5jlRfdnJLmN0pTy/4lj4/7\ntv0Sk3iiKkypnEUtR6WfMgH0QZfKHM1+di+y9TFRtv6y//0rb+T+W8a9nsNL/ggj\nnar86461qO0rOs2cXjp3kOG1FEJ5MVmFmBGtnrKpa73XpXyTqRxB/M0n1n/W9nGq\nC4FSYa04T6N5RIZGBN2z2MT5IKGbFlbC8UrW0DxW7AYImQQcHtGl/m00QLVWutHQ\noVJYnFPlXTcHYvASLu+RhhsbDmxMgJJ0mcDpvsC4PjvB+TxywElgS70vE0XmLD+O\nJtvsBslHZvPBKCOdT0MS+tgSOIfga+z1Z1g7+DVagf7quvmag8jfPioyKvxnK/Eg\nsTUVi2ghzq8wm27ud/mIM7AY2qEORR8Go3TVB4HzWQgpZrt3i5MIlCaY504LzSRi\nigHCzAPlHws+W0rB5N+er5/2pJKnfBSDiCiFAVtCLOZ7gLiMm0jhO2B6tUXHI/+M\nRPjy02i59lINMRRev56GKtcd9qO/0kUJWdZTdA2XoS82ixPvZtXQpUpuL12ab+9E\naDK8Z4RHJYYfCT3Q5vNAXaiWQ+8PTWm2QgBR/bkwSWc+NpUFgNPN9PvQi8WEg5Um\nAGMCAwEAAaNjMGEwHQYDVR0OBBYEFDZh4QB8iAUJUYtEbEf/GkzJ6k8SMB8GA1Ud\nIwQYMBaAFDZh4QB8iAUJUYtEbEf/GkzJ6k8SMA8GA1UdEwEB/wQFMAMBAf8wDgYD\nVR0PAQH/BAQDAgIEMA0GCSqGSIb3DQEBCwUAA4ICAQB8cMqTllHc8U+qCrOlg3H7\n174lmaCsbo/bJ0C17JEgMLb4kvrqsXZs01U3mB/qABg/1t5Pd5AORHARs1hhqGIC\nW/nKMav574f9rZN4PC2ZlufGXb7sIdJpGiO9ctRhiLuYuly10JccUZGEHpHSYM2G\ntkgYbZba6lsCPYAAP83cyDV+1aOkTf1RCp/lM0PKvmxYN10RYsK631jrleGdcdkx\noSK//mSQbgcWnmAEZrzHoF1/0gso1HZgIn0YLzVhLSA/iXCX4QT2h3J5z3znluKG\n1nv8NQdxei2DIIhASWfu804CA96cQKTTlaae2fweqXjdN1/v2nqOhngNyz1361mF\nmr4XmaKH/ItTwOe72NI9ZcwS1lVaCvsIkTDCEXdm9rCNPAY10iTunIHFXRh+7KPz\nlHGewCq/8TOohBRn0/NNfh7uRslOSZ/xKbN9tMBtw37Z8d2vvnXq/YWdsm1+JLVw\nn6yYD/yacNJBlwpddla8eaVMjsF6nBnIgQOf9zKSe06nSTqvgwUHosgOECZJZ1Eu\nzbH4yswbt02tKtKEFhx+v+OTge/06V+jGsqTWLsfrOCNLuA8H++z+pUENmpqnnHo\nvaI47gC+TNpkgYGkkBT6B/m/U01BuOBBTzhIlMEZq9qkDWuM2cA5kW5V3FJUcfHn\nw1IdYIg2Wxg7yHcQZemFQg==\n-----END CERTIFICATE-----",
3+
"-----BEGIN CERTIFICATE-----\nMIICIjCCAaigAwIBAgIRAISp0Cl7DrWK5/8OgN52BgUwCgYIKoZIzj0EAwMwUjEc\nMBoGA1UEAwwTS2V5IEF0dGVzdGF0aW9uIENBMTEQMA4GA1UECwwHQW5kcm9pZDET\nMBEGA1UECgwKR29vZ2xlIExMQzELMAkGA1UEBhMCVVMwHhcNMjUwNzE3MjIzMjE4\nWhcNMzUwNzE1MjIzMjE4WjBSMRwwGgYDVQQDDBNLZXkgQXR0ZXN0YXRpb24gQ0Ex\nMRAwDgYDVQQLDAdBbmRyb2lkMRMwEQYDVQQKDApHb29nbGUgTExDMQswCQYDVQQG\nEwJVUzB2MBAGByqGSM49AgEGBSuBBAAiA2IABCPaI3FO3z5bBQo8cuiEas4HjqCt\nG/mLFfRT0MsIssPBEEU5Cfbt6sH5yOAxqEi5QagpU1yX4HwnGb7OtBYpDTB57uH5\nEczm34A5FNijV3s0/f0UPl7zbJcTx6xwqMIRq6NCMEAwDwYDVR0TAQH/BAUwAwEB\n/zAOBgNVHQ8BAf8EBAMCAQYwHQYDVR0OBBYEFFIyuyz7RkOb3NaBqQ5lZuA0QepA\nMAoGCCqGSM49BAMDA2gAMGUCMETfjPO/HwqReR2CS7p0ZWoD/LHs6hDi422opifH\nEUaYLxwGlT9SLdjkVpz0UUOR5wIxAIoGyxGKRHVTpqpGRFiJtQEOOTp/+s1GcxeY\nuR2zh/80lQyu9vAFCj6E4AXc+osmRg==\n-----END CERTIFICATE-----"
4+
]
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
package com.android.keyattestation.verifier
2+
3+
import com.google.protobuf.ByteString
4+
5+
/** An interface to handle checking validity of challenges. */
6+
interface ChallengeChecker {
7+
/**
8+
* Checks the given challenge for validity.
9+
*
10+
* @param challenge The challenge being check.
11+
* @return True if the challenge is valid, else false.
12+
*/
13+
fun checkChallenge(challenge: ByteString): Boolean
14+
}

src/main/kotlin/Verifier.kt

Lines changed: 21 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,6 @@ import com.android.keyattestation.verifier.provider.ProvisioningMethod
2222
import com.android.keyattestation.verifier.provider.RevocationChecker
2323
import com.google.errorprone.annotations.ThreadSafe
2424
import com.google.protobuf.ByteString
25-
import java.nio.ByteBuffer
2625
import java.security.PublicKey
2726
import java.security.Security
2827
import java.security.cert.CertPathValidator
@@ -73,26 +72,42 @@ open class Verifier(
7372
Security.addProvider(KeyAttestationProvider())
7473
}
7574

76-
fun verify(chain: List<X509Certificate>, challenge: ByteArray? = null): VerificationResult {
75+
/**
76+
* Verifies an Android Key Attestation certificate chain.
77+
*
78+
* @param chain The attestation certificate chain to verify.
79+
* @param challengeChecker The challenge checker to use for additional challenge validation.
80+
* @return [VerificationResult]
81+
*/
82+
@JvmOverloads
83+
fun verify(
84+
chain: List<X509Certificate>,
85+
challengeChecker: ChallengeChecker? = null,
86+
): VerificationResult {
7787
val certPath =
7888
try {
7989
KeyAttestationCertPath(chain)
8090
} catch (e: Exception) {
8191
return VerificationResult.ChainParsingFailure
8292
}
83-
return verify(certPath, challenge)
93+
return verify(certPath, challengeChecker)
8494
}
8595

8696
/**
8797
* Verifies an Android Key Attestation certificate chain.
8898
*
8999
* @param chain The attestation certificate chain to verify.
100+
* @param challengeChecker The challenge checker to use for additional validation of the challenge
101+
* in the attestation chain.
90102
* @return [VerificationResult]
91103
*
92104
* TODO: b/366058500 - Make the challenge required after Apparat's changes are rollback safe.
93105
*/
94106
@JvmOverloads
95-
fun verify(certPath: KeyAttestationCertPath, challenge: ByteArray? = null): VerificationResult {
107+
fun verify(
108+
certPath: KeyAttestationCertPath,
109+
challengeChecker: ChallengeChecker? = null,
110+
): VerificationResult {
96111
val certPathValidator = CertPathValidator.getInstance("KeyAttestation")
97112
val certPathParameters =
98113
PKIXParameters(trustAnchorsSource()).apply {
@@ -114,8 +129,8 @@ open class Verifier(
114129
}
115130

116131
if (
117-
challenge != null &&
118-
keyDescription.attestationChallenge.asReadOnlyByteBuffer() != ByteBuffer.wrap(challenge)
132+
challengeChecker != null &&
133+
!challengeChecker.checkChallenge(keyDescription.attestationChallenge)
119134
) {
120135
return VerificationResult.ChallengeMismatch
121136
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
package com.android.keyattestation.verifier.challengecheckers
2+
3+
import com.android.keyattestation.verifier.ChallengeChecker
4+
import com.google.protobuf.ByteString
5+
6+
/**
7+
* A basic implementation of [ChallengeChecker] that checks if the challenge in the attestation
8+
* certificate is equal to the expected challenge.
9+
*/
10+
class ChallengeMatcher(private val expectedChallenge: ByteString) : ChallengeChecker {
11+
12+
constructor(expectedChallenge: ByteArray) : this(ByteString.copyFrom(expectedChallenge))
13+
14+
override fun checkChallenge(challenge: ByteString): Boolean = challenge.equals(expectedChallenge)
15+
}

src/main/kotlin/testing/TestUtils.kt

Lines changed: 7 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -38,9 +38,8 @@ import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter
3838
import org.bouncycastle.openssl.PEMParser
3939

4040
object TestUtils {
41-
private const val PROD_ROOT_PATH =
42-
"googledata/html/external_content/android_googleapis_com/attestation/root"
43-
const val TESTDATA_PATH = "third_party/java/keyattestation/testdata"
41+
private const val PROD_ROOT_PATH = "roots.json"
42+
const val TESTDATA_PATH = "testdata"
4443

4544
fun readCertPath(subpath: String): KeyAttestationCertPath =
4645
readCertPath(readFile(Path(base = TESTDATA_PATH, /* subpaths...= */ subpath)))
@@ -60,14 +59,13 @@ object TestUtils {
6059
.let { KeyAttestationCertPath(it) }
6160
}
6261

63-
val prodRoot by lazy {
64-
val certs = Gson().fromJson(readFile(PROD_ROOT_PATH), Array<String>::class.java).toSet()
65-
check(certs.size == 1) { "Multiple certificates in the root file are not yet supported" }
66-
certs.first().asX509Certificate()
62+
val prodAnchors by lazy {
63+
Gson()
64+
.fromJson(readFile(PROD_ROOT_PATH), Array<String>::class.java)
65+
.map { TrustAnchor(it.asX509Certificate(), null) }
66+
.toSet()
6767
}
6868

69-
val prodAnchor = TrustAnchor(prodRoot, null)
70-
7169
private fun readFile(path: Path) = path.reader()
7270
private fun readFile(path: String) = path.reader()
7371
}

src/test/kotlin/VerifierTest.kt

Lines changed: 27 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -16,12 +16,12 @@
1616

1717
package com.android.keyattestation.verifier
1818

19+
import com.android.keyattestation.verifier.challengecheckers.ChallengeMatcher
1920
import com.android.keyattestation.verifier.testing.CertLists
20-
import com.android.keyattestation.verifier.testing.TestUtils.prodRoot
21+
import com.android.keyattestation.verifier.testing.TestUtils.prodAnchors
2122
import com.android.keyattestation.verifier.testing.TestUtils.readCertPath
2223
import com.google.common.truth.Truth.assertThat
2324
import com.google.protobuf.ByteString
24-
import java.security.cert.TrustAnchor
2525
import java.time.Instant
2626
import kotlin.test.assertIs
2727
import org.junit.Test
@@ -31,18 +31,12 @@ import org.junit.runners.JUnit4
3131
/** Unit tests for [Verifier]. */
3232
@RunWith(JUnit4::class)
3333
class VerifierTest {
34-
private val verifier =
35-
Verifier(
36-
{ setOf(TrustAnchor(prodRoot, /* nameConstraints= */ null)) },
37-
{ setOf<String>() },
38-
{ Instant.now() },
39-
)
34+
private val verifier = Verifier({ prodAnchors }, { setOf<String>() }, { Instant.now() })
4035

4136
@Test
4237
fun verify_validChain_returnsSuccess() {
4338
val chain = readCertPath("blueline/sdk28/TEE_EC_NONE.pem")
44-
val result =
45-
assertIs<VerificationResult.Success>(verifier.verify(chain, "challenge".toByteArray()))
39+
val result = assertIs<VerificationResult.Success>(verifier.verify(chain))
4640
assertThat(result.publicKey).isEqualTo(chain.leafCert().publicKey)
4741
assertThat(result.challenge).isEqualTo(ByteString.copyFromUtf8("challenge"))
4842
assertThat(result.securityLevel).isEqualTo(SecurityLevel.TRUSTED_ENVIRONMENT)
@@ -52,8 +46,7 @@ class VerifierTest {
5246
@Test
5347
fun verify_validChain_returnsDeviceIdentity() {
5448
val chain = readCertPath("blueline/sdk28/TEE_RSA_BASE+IMEI.pem")
55-
val result =
56-
assertIs<VerificationResult.Success>(verifier.verify(chain, "challenge".toByteArray()))
49+
val result = assertIs<VerificationResult.Success>(verifier.verify(chain))
5750
assertThat(result.attestedDeviceIds)
5851
.isEqualTo(
5952
DeviceIdentity(
@@ -70,15 +63,34 @@ class VerifierTest {
7063
}
7164

7265
@Test
73-
fun verify_unexpectedChallenge_returnsChallengeMismatch() {
66+
fun verify_challengeCheckerReturnsTrue_returnsSuccess() {
67+
val challengeChecker: ChallengeChecker =
68+
object : ChallengeChecker {
69+
override fun checkChallenge(challenge: ByteString) = true
70+
}
71+
72+
val chain = readCertPath("blueline/sdk28/TEE_EC_NONE.pem")
73+
assertIs<VerificationResult.Success>(verifier.verify(chain, challengeChecker))
74+
}
75+
76+
@Test
77+
fun verify_challengeCheckerReturnsFalse_returnsChallengeMismatch() {
78+
val challengeChecker: ChallengeChecker =
79+
object : ChallengeChecker {
80+
override fun checkChallenge(challenge: ByteString) = false
81+
}
82+
7483
val chain = readCertPath("blueline/sdk28/TEE_EC_NONE.pem")
75-
assertIs<VerificationResult.ChallengeMismatch>(verifier.verify(chain, "foo".toByteArray()))
84+
assertIs<VerificationResult.ChallengeMismatch>(verifier.verify(chain, challengeChecker))
7685
}
7786

7887
@Test
7988
fun verify_unexpectedRootKey_returnsPathValidationFailure() {
8089
assertIs<VerificationResult.PathValidationFailure>(
81-
verifier.verify(CertLists.wrongTrustAnchor, "challenge".toByteArray())
90+
verifier.verify(
91+
CertLists.wrongTrustAnchor,
92+
ChallengeMatcher(ByteString.copyFromUtf8("challenge")),
93+
)
8294
)
8395
}
8496
}
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
package com.android.keyattestation.verifier.challengecheckers
2+
3+
import com.android.keyattestation.verifier.VerificationResult
4+
import com.android.keyattestation.verifier.Verifier
5+
import com.android.keyattestation.verifier.testing.TestUtils.prodRoot
6+
import com.android.keyattestation.verifier.testing.TestUtils.readCertPath
7+
import com.google.common.truth.Truth.assertThat
8+
import com.google.protobuf.ByteString
9+
import java.security.cert.TrustAnchor
10+
import java.time.Instant
11+
import kotlin.test.assertIs
12+
import org.junit.Test
13+
import org.junit.runner.RunWith
14+
import org.junit.runners.JUnit4
15+
16+
@RunWith(JUnit4::class)
17+
class ChallengeMatcherTest {
18+
19+
companion object {
20+
private val testChallenge = ByteString.copyFromUtf8("challenge")
21+
}
22+
23+
@Test
24+
fun checkChallenge_matchingChallenge_returnsTrue() {
25+
val challengeChecker = ChallengeMatcher(testChallenge)
26+
assertThat(challengeChecker.checkChallenge(testChallenge)).isTrue()
27+
}
28+
29+
@Test
30+
fun checkChallenge_mismatchedChallenge_returnsFalse() {
31+
val challengeChecker = ChallengeMatcher(testChallenge)
32+
assertThat(challengeChecker.checkChallenge(ByteString.copyFromUtf8("foo"))).isFalse()
33+
}
34+
35+
@Test
36+
fun verify_expectedChallenge_returnsSuccess() {
37+
val verifier =
38+
Verifier(
39+
{ setOf(TrustAnchor(prodRoot, /* nameConstraints= */ null)) },
40+
{ setOf<String>() },
41+
{ Instant.now() },
42+
)
43+
val chain = readCertPath("blueline/sdk28/TEE_EC_NONE.pem")
44+
assertIs<VerificationResult.Success>(verifier.verify(chain, ChallengeMatcher(testChallenge)))
45+
}
46+
47+
@Test
48+
fun verify_unexpectedChallenge_returnsChallengeMismatch() {
49+
val verifier =
50+
Verifier(
51+
{ setOf(TrustAnchor(prodRoot, /* nameConstraints= */ null)) },
52+
{ setOf<String>() },
53+
{ Instant.now() },
54+
)
55+
val chain = readCertPath("blueline/sdk28/TEE_EC_NONE.pem")
56+
assertIs<VerificationResult.ChallengeMismatch>(
57+
verifier.verify(chain, ChallengeMatcher(ByteString.copyFromUtf8("foo")))
58+
)
59+
}
60+
}

src/test/kotlin/provider/KeyAttestationCertPathValidatorTest.kt

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ package com.android.keyattestation.verifier.provider
1919
import com.android.keyattestation.verifier.testing.Certs.rootAnchor as testAnchor
2020
import com.android.keyattestation.verifier.testing.Chains
2121
import com.android.keyattestation.verifier.testing.FakeCalendar
22-
import com.android.keyattestation.verifier.testing.TestUtils.prodAnchor
22+
import com.android.keyattestation.verifier.testing.TestUtils.prodAnchors
2323
import com.google.common.truth.Truth.assertThat
2424
import java.security.InvalidAlgorithmParameterException
2525
import java.security.Security
@@ -42,7 +42,7 @@ import org.junit.runners.JUnit4
4242
class KeyAttestationCertPathValidatorTest {
4343
private val certPathValidator = CertPathValidator.getInstance("KeyAttestation")
4444
private val pkixCertPathValidator = CertPathValidator.getInstance("PKIX")
45-
private val prodParams = PKIXParameters(setOf(prodAnchor))
45+
private val prodParams = PKIXParameters(prodAnchors)
4646
private val testParams =
4747
PKIXParameters(setOf(testAnchor)).apply { date = FakeCalendar.DEFAULT.today() }
4848

@@ -116,10 +116,10 @@ class KeyAttestationCertPathValidatorTest {
116116
}
117117

118118
@Test
119-
fun multipleAnchors_returnsSuccess() {
119+
fun additionalAnchors_returnsSuccess() {
120120
val certPath = Chains.validFactoryProvisioned
121-
val params =
122-
PKIXParameters(setOf(prodAnchor, testAnchor)).apply { date = FakeCalendar.DEFAULT.today() }
121+
val moreAnchors = prodAnchors.union(setOf(testAnchor))
122+
val params = PKIXParameters(moreAnchors).apply { date = FakeCalendar.DEFAULT.today() }
123123
val result = certPathValidator.validate(certPath, params) as PKIXCertPathValidatorResult
124124
assertThat(result.trustAnchor).isEqualTo(testAnchor)
125125
assertThat(result.policyTree).isNull()
@@ -136,7 +136,7 @@ class KeyAttestationCertPathValidatorTest {
136136
}
137137

138138
@Test
139-
fun wrongAnchor_throwsCertPathValidatorException() {
139+
fun multipleWrongAnchors_throwsCertPathValidatorException() {
140140
val certPath = Chains.validFactoryProvisioned
141141
val exception =
142142
assertFailsWith<CertPathValidatorException> {
@@ -146,14 +146,15 @@ class KeyAttestationCertPathValidatorTest {
146146
assertFailsWith<CertPathValidatorException> {
147147
pkixCertPathValidator.validate(certPath, prodParams)
148148
}
149+
assertThat(prodParams.trustAnchors.size).isAtLeast(2)
149150
assertThat(exception.reason).isEqualTo(PKIXReason.NO_TRUST_ANCHOR)
150151
assertThat(pkixException.reason).isEqualTo(PKIXReason.NO_TRUST_ANCHOR)
151152
}
152153

153154
@Test
154-
fun multipleWrongAnchors_throwsCertPathValidatorException() {
155+
fun singleWrongAnchor_throwsCertPathValidatorException() {
155156
val params =
156-
PKIXParameters(setOf(prodAnchor, prodAnchor)).apply { date = FakeCalendar.DEFAULT.today() }
157+
PKIXParameters(setOf(prodAnchors.first())).apply { date = FakeCalendar.DEFAULT.today() }
157158
val exception =
158159
assertFailsWith<CertPathValidatorException> {
159160
certPathValidator.validate(Chains.validFactoryProvisioned, params)

0 commit comments

Comments
 (0)