Skip to content

Commit acfc27e

Browse files
Android HW Trust Teamcopybara-github
authored andcommitted
Add cause to VerificationResult.PathValidationFailure
PiperOrigin-RevId: 780683473
1 parent 7b74918 commit acfc27e

File tree

6 files changed

+143
-16
lines changed

6 files changed

+143
-16
lines changed
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: 23 additions & 8 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
@@ -46,7 +45,7 @@ sealed interface VerificationResult {
4645

4746
data object ChallengeMismatch : VerificationResult
4847

49-
data object PathValidationFailure : VerificationResult
48+
data class PathValidationFailure(val cause: CertPathValidatorException) : VerificationResult
5049

5150
data object ChainParsingFailure : VerificationResult
5251

@@ -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 {
@@ -103,7 +118,7 @@ open class Verifier(
103118
try {
104119
certPathValidator.validate(certPath, certPathParameters) as PKIXCertPathValidatorResult
105120
} catch (e: CertPathValidatorException) {
106-
return VerificationResult.PathValidationFailure
121+
return VerificationResult.PathValidationFailure(e)
107122
}
108123

109124
val keyDescription =
@@ -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: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
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 matches the expected challenge.
9+
*/
10+
class ChallengeMatcher(private val expectedChallenge: ByteString) : ChallengeChecker {
11+
12+
override fun checkChallenge(challenge: ByteString): Boolean = challenge.equals(expectedChallenge)
13+
}

src/main/kotlin/testing/Certs.kt

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616

1717
package com.android.keyattestation.verifier.testing
1818

19+
import com.android.keyattestation.verifier.ProvisioningInfoMap
1920
import com.android.keyattestation.verifier.provider.KeyAttestationCertPath
2021
import java.security.cert.TrustAnchor
2122
import org.bouncycastle.asn1.ASN1ObjectIdentifier
@@ -86,7 +87,15 @@ object CertLists {
8687
signingKey = rkpKey.private,
8788
issuer = rkpIntermediate.subject,
8889
extraExtension =
89-
Extension(ObjectIds.PROVISIONING_INFO, /* critical= */ false, byteArrayOf()),
90+
Extension(
91+
ObjectIds.PROVISIONING_INFO,
92+
/* critical= */ false,
93+
ProvisioningInfoMap(
94+
certificatesIssued = 1,
95+
manufacturer = "manufacturer",
96+
)
97+
.encodeToAsn1(),
98+
),
9099
)
91100
listOf(
92101
certFactory.generateLeafCert(),

src/test/kotlin/VerifierTest.kt

Lines changed: 25 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616

1717
package com.android.keyattestation.verifier
1818

19+
import com.android.keyattestation.verifier.challengecheckers.ChallengeMatcher
1920
import com.android.keyattestation.verifier.testing.CertLists
2021
import com.android.keyattestation.verifier.testing.TestUtils.prodRoot
2122
import com.android.keyattestation.verifier.testing.TestUtils.readCertPath
@@ -41,8 +42,7 @@ class VerifierTest {
4142
@Test
4243
fun verify_validChain_returnsSuccess() {
4344
val chain = readCertPath("blueline/sdk28/TEE_EC_NONE.pem")
44-
val result =
45-
assertIs<VerificationResult.Success>(verifier.verify(chain, "challenge".toByteArray()))
45+
val result = assertIs<VerificationResult.Success>(verifier.verify(chain))
4646
assertThat(result.publicKey).isEqualTo(chain.leafCert().publicKey)
4747
assertThat(result.challenge).isEqualTo(ByteString.copyFromUtf8("challenge"))
4848
assertThat(result.securityLevel).isEqualTo(SecurityLevel.TRUSTED_ENVIRONMENT)
@@ -52,8 +52,7 @@ class VerifierTest {
5252
@Test
5353
fun verify_validChain_returnsDeviceIdentity() {
5454
val chain = readCertPath("blueline/sdk28/TEE_RSA_BASE+IMEI.pem")
55-
val result =
56-
assertIs<VerificationResult.Success>(verifier.verify(chain, "challenge".toByteArray()))
55+
val result = assertIs<VerificationResult.Success>(verifier.verify(chain))
5756
assertThat(result.attestedDeviceIds)
5857
.isEqualTo(
5958
DeviceIdentity(
@@ -70,15 +69,34 @@ class VerifierTest {
7069
}
7170

7271
@Test
73-
fun verify_unexpectedChallenge_returnsChallengeMismatch() {
72+
fun verify_challengeCheckerReturnsTrue_returnsSuccess() {
73+
val challengeChecker: ChallengeChecker =
74+
object : ChallengeChecker {
75+
override fun checkChallenge(challenge: ByteString): Boolean = true
76+
}
77+
7478
val chain = readCertPath("blueline/sdk28/TEE_EC_NONE.pem")
75-
assertIs<VerificationResult.ChallengeMismatch>(verifier.verify(chain, "foo".toByteArray()))
79+
assertIs<VerificationResult.Success>(verifier.verify(chain, challengeChecker))
80+
}
81+
82+
@Test
83+
fun verify_challengeCheckerReturnsFalse_returnsChallengeMismatch() {
84+
val challengeChecker: ChallengeChecker =
85+
object : ChallengeChecker {
86+
override fun checkChallenge(challenge: ByteString): Boolean = false
87+
}
88+
89+
val chain = readCertPath("blueline/sdk28/TEE_EC_NONE.pem")
90+
assertIs<VerificationResult.ChallengeMismatch>(verifier.verify(chain, challengeChecker))
7691
}
7792

7893
@Test
7994
fun verify_unexpectedRootKey_returnsPathValidationFailure() {
8095
assertIs<VerificationResult.PathValidationFailure>(
81-
verifier.verify(CertLists.wrongTrustAnchor, "challenge".toByteArray())
96+
verifier.verify(
97+
CertLists.wrongTrustAnchor,
98+
ChallengeMatcher(ByteString.copyFromUtf8("challenge")),
99+
)
82100
)
83101
}
84102
}
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
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+
@Test
20+
fun checkChallenge_matchingChallenge_returnsTrue() {
21+
val challengeChecker = ChallengeMatcher(ByteString.copyFromUtf8("challenge"))
22+
assertThat(challengeChecker.checkChallenge(ByteString.copyFromUtf8("challenge"))).isTrue()
23+
}
24+
25+
@Test
26+
fun checkChallenge_mismatchedChallenge_returnsFalse() {
27+
val challengeChecker = ChallengeMatcher(ByteString.copyFromUtf8("challenge"))
28+
assertThat(challengeChecker.checkChallenge(ByteString.copyFromUtf8("foo"))).isFalse()
29+
}
30+
31+
@Test
32+
fun verify_expectedChallenge_returnsSuccess() {
33+
val verifier =
34+
Verifier(
35+
{ setOf(TrustAnchor(prodRoot, /* nameConstraints= */ null)) },
36+
{ setOf<String>() },
37+
{ Instant.now() },
38+
)
39+
val chain = readCertPath("blueline/sdk28/TEE_EC_NONE.pem")
40+
assertIs<VerificationResult.Success>(
41+
verifier.verify(chain, ChallengeMatcher(ByteString.copyFromUtf8("challenge")))
42+
)
43+
}
44+
45+
@Test
46+
fun verify_unexpectedChallenge_returnsChallengeMismatch() {
47+
val verifier =
48+
Verifier(
49+
{ setOf(TrustAnchor(prodRoot, /* nameConstraints= */ null)) },
50+
{ setOf<String>() },
51+
{ Instant.now() },
52+
)
53+
val chain = readCertPath("blueline/sdk28/TEE_EC_NONE.pem")
54+
assertIs<VerificationResult.ChallengeMismatch>(
55+
verifier.verify(chain, ChallengeMatcher(ByteString.copyFromUtf8("foo")))
56+
)
57+
}
58+
}

0 commit comments

Comments
 (0)