Skip to content

Commit 6ff1eec

Browse files
suzannajiwanicopybara-github
authored andcommitted
Define mechanism for configurable extension checking
PiperOrigin-RevId: 832427405
1 parent 5a7f9f8 commit 6ff1eec

5 files changed

Lines changed: 280 additions & 30 deletions

File tree

Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
/*
2+
* Copyright 2025 Google LLC
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package com.android.keyattestation.verifier
18+
19+
import androidx.annotation.RequiresApi
20+
import com.google.errorprone.annotations.Immutable
21+
import com.google.errorprone.annotations.ThreadSafe
22+
23+
/**
24+
* Configuration for validating the extensions in an Android attestation certificate, as described
25+
* at https://source.android.com/docs/security/features/keystore/attestation.
26+
*/
27+
@ThreadSafe
28+
data class ExtensionConstraintConfig(
29+
val keyOrigin: ValidationLevel<Origin> = ValidationLevel.STRICT(Origin.GENERATED),
30+
val securityLevel: ValidationLevel<KeyDescription> =
31+
SecurityLevelValidationLevel.STRICT(SecurityLevel.TRUSTED_ENVIRONMENT),
32+
val rootOfTrust: ValidationLevel<RootOfTrust> = ValidationLevel.NOT_NULL,
33+
)
34+
35+
/** Configuration for validating a single extension in an Android attestation certificate. */
36+
@Immutable(containerOf = ["T"])
37+
sealed interface ValidationLevel<out T> {
38+
/** Evaluates whether the [extension] is satisfied by this [ValidationLevel]. */
39+
fun isSatisfiedBy(extension: Any?): Boolean
40+
41+
/**
42+
* Checks that the extension exists and matches the expected value.
43+
*
44+
* @param expectedVal The expected value of the extension.
45+
*/
46+
@Immutable(containerOf = ["T"])
47+
data class STRICT<T>(val expectedVal: T) : ValidationLevel<T> {
48+
override fun isSatisfiedBy(extension: Any?): Boolean = extension == expectedVal
49+
}
50+
51+
/* Check that the extension exists. */
52+
@Immutable
53+
data object NOT_NULL : ValidationLevel<Nothing> {
54+
override fun isSatisfiedBy(extension: Any?): Boolean = extension != null
55+
}
56+
57+
@Immutable
58+
data object IGNORE : ValidationLevel<Nothing> {
59+
override fun isSatisfiedBy(extension: Any?): Boolean = true
60+
}
61+
}
62+
63+
/**
64+
* Configuration for validating the attestationSecurityLevel and keyMintSecurityLevel fields in an
65+
* Android attestation certificate.
66+
*/
67+
@Immutable
68+
sealed class SecurityLevelValidationLevel : ValidationLevel<KeyDescription> {
69+
@RequiresApi(24)
70+
fun areSecurityLevelsMatching(keyDescription: KeyDescription): Boolean {
71+
return keyDescription.attestationSecurityLevel == keyDescription.keyMintSecurityLevel
72+
}
73+
74+
/**
75+
* Checks that both the attestationSecurityLevel and keyMintSecurityLevel match the expected
76+
* value.
77+
*
78+
* @param expectedVal The expected value of the security level.
79+
*/
80+
@Immutable
81+
data class STRICT(val expectedVal: SecurityLevel) : SecurityLevelValidationLevel() {
82+
@RequiresApi(24)
83+
override fun isSatisfiedBy(extension: Any?): Boolean {
84+
val keyDescription = extension as? KeyDescription ?: return false
85+
val securityLevelIsExpected = keyDescription.attestationSecurityLevel == this.expectedVal
86+
return areSecurityLevelsMatching(keyDescription) && securityLevelIsExpected
87+
}
88+
}
89+
90+
/**
91+
* Checks that the attestationSecurityLevel is equal to the keyMintSecurityLevel, and that this
92+
* security level is not [SecurityLevel.SOFTWARE].
93+
*/
94+
@Immutable
95+
data object NOT_SOFTWARE : SecurityLevelValidationLevel() {
96+
@RequiresApi(24)
97+
override fun isSatisfiedBy(extension: Any?): Boolean {
98+
val keyDescription = extension as? KeyDescription ?: return false
99+
val securityLevelIsSoftware =
100+
keyDescription.attestationSecurityLevel == SecurityLevel.SOFTWARE
101+
return areSecurityLevelsMatching(keyDescription) && !securityLevelIsSoftware
102+
}
103+
}
104+
105+
/**
106+
* Checks that the attestationSecurityLevel is equal to the keyMintSecurityLevel, regardless of
107+
* security level.
108+
*/
109+
@Immutable
110+
data object CONSISTENT : SecurityLevelValidationLevel() {
111+
@RequiresApi(24)
112+
override fun isSatisfiedBy(extension: Any?): Boolean {
113+
val keyDescription = extension as? KeyDescription ?: return false
114+
return areSecurityLevelsMatching(keyDescription)
115+
}
116+
}
117+
}

src/main/kotlin/KeyAttestationReason.kt

Lines changed: 13 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -33,15 +33,19 @@ enum class KeyAttestationReason : CertPathValidatorException.Reason {
3333
// extension. This likely indicates that an attacker is trying to manipulate the key and
3434
// device properties.
3535
CHAIN_EXTENDED_WITH_FAKE_ATTESTATION_EXTENSION,
36-
// The key was not generated. The verifier cannot know that the key has always been in the
37-
// secure environment.
38-
KEY_ORIGIN_NOT_GENERATED,
39-
// The attestation and the KeyMint security levels do not match.
40-
// This likely indicates that the attestation was generated in software and so cannot be trusted.
41-
MISMATCHED_SECURITY_LEVELS,
42-
// The key description is missing the root of trust.
43-
// An Android key attestation chain without a root of trust is malformed.
44-
ROOT_OF_TRUST_MISSING,
36+
// The origin violated the constraint provided in [ExtensionConstraintConfig].
37+
// Using the default config, this means the key was not generated, so the verifier cannot know
38+
// that the key has always been in the secure environment.
39+
KEY_ORIGIN_CONSTRAINT_VIOLATION,
40+
// The security level violated the constraint provided in [ExtensionConstraintConfig].
41+
// Using the default config, this means the attestation and the KeyMint security levels do not
42+
// match, which likely indicates that the attestation was generated in software and so cannot be
43+
// trusted.
44+
SECURITY_LEVEL_CONSTRAINT_VIOLATION,
45+
// The root of trust violated the constraint provided in [ExtensionConstraintConfig].
46+
// Using the default config, this means the key description is missing the root of trust, and an
47+
// Android key attestation chain without a root of trust is malformed.
48+
ROOT_OF_TRUST_CONSTRAINT_VIOLATION,
4549
// There was an error parsing the key description and an unknown tag number was encountered.
4650
UNKNOWN_TAG_NUMBER,
4751
}

src/main/kotlin/Verifier.kt

Lines changed: 23 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -136,10 +136,13 @@ interface VerifyRequestLog {
136136
* @param anchor a [TrustAnchor] to use for certificate path verification.
137137
*/
138138
@ThreadSafe
139-
open class Verifier(
139+
open class Verifier
140+
@JvmOverloads
141+
constructor(
140142
private val trustAnchorsSource: () -> Set<TrustAnchor>,
141143
private val revokedSerialsSource: () -> Set<String>,
142144
private val instantSource: InstantSource,
145+
private val extensionConstraintConfig: ExtensionConstraintConfig = ExtensionConstraintConfig(),
143146
) {
144147
init {
145148
Security.addProvider(KeyAttestationProvider())
@@ -287,36 +290,38 @@ open class Verifier(
287290
}
288291
}
289292

290-
if (
291-
keyDescription.hardwareEnforced.origin == null ||
292-
keyDescription.hardwareEnforced.origin != Origin.GENERATED
293-
) {
293+
val origin = keyDescription.hardwareEnforced.origin
294+
if (!extensionConstraintConfig.keyOrigin.isSatisfiedBy(origin)) {
294295
return VerificationResult.ExtensionConstraintViolation(
295-
"origin != GENERATED: ${keyDescription.hardwareEnforced.origin}",
296-
KeyAttestationReason.KEY_ORIGIN_NOT_GENERATED,
296+
"Origin violates constraint: value=${origin}, config=${extensionConstraintConfig.keyOrigin}",
297+
KeyAttestationReason.KEY_ORIGIN_CONSTRAINT_VIOLATION,
297298
)
298299
}
299300

300301
val securityLevel =
301-
if (keyDescription.attestationSecurityLevel == keyDescription.keyMintSecurityLevel) {
302-
keyDescription.attestationSecurityLevel
302+
if (extensionConstraintConfig.securityLevel.isSatisfiedBy(keyDescription)) {
303+
minOf(keyDescription.attestationSecurityLevel, keyDescription.keyMintSecurityLevel)
303304
} else {
304305
return VerificationResult.ExtensionConstraintViolation(
305-
"attestationSecurityLevel != keyMintSecurityLevel: ${keyDescription.attestationSecurityLevel} != ${keyDescription.keyMintSecurityLevel}",
306-
KeyAttestationReason.MISMATCHED_SECURITY_LEVELS,
306+
"Security level violates constraint: value=${keyDescription.attestationSecurityLevel}, config=${extensionConstraintConfig.securityLevel}",
307+
KeyAttestationReason.SECURITY_LEVEL_CONSTRAINT_VIOLATION,
307308
)
308309
}
309-
val rootOfTrust =
310-
keyDescription.hardwareEnforced.rootOfTrust
311-
?: return VerificationResult.ExtensionConstraintViolation(
312-
"hardwareEnforced.rootOfTrust is null",
313-
KeyAttestationReason.ROOT_OF_TRUST_MISSING,
314-
)
310+
311+
val rootOfTrust = keyDescription.hardwareEnforced.rootOfTrust
312+
if (!extensionConstraintConfig.rootOfTrust.isSatisfiedBy(rootOfTrust)) {
313+
return VerificationResult.ExtensionConstraintViolation(
314+
"Root of trust violates constraint: value=${rootOfTrust}, config=${extensionConstraintConfig.rootOfTrust}",
315+
KeyAttestationReason.ROOT_OF_TRUST_CONSTRAINT_VIOLATION,
316+
)
317+
}
318+
val verifiedBootState = rootOfTrust?.verifiedBootState ?: VerifiedBootState.UNVERIFIED
319+
315320
return VerificationResult.Success(
316321
pathValidationResult.publicKey,
317322
keyDescription.attestationChallenge,
318323
securityLevel,
319-
rootOfTrust.verifiedBootState,
324+
verifiedBootState,
320325
deviceInformation,
321326
DeviceIdentity.parseFrom(keyDescription),
322327
)
Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
/*
2+
* Copyright 2025 Google LLC
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package com.android.keyattestation.verifier
18+
19+
import com.google.common.truth.Truth.assertThat
20+
import com.google.protobuf.ByteString
21+
import org.junit.Test
22+
import org.junit.runner.RunWith
23+
import org.junit.runners.JUnit4
24+
25+
@RunWith(JUnit4::class)
26+
class ExtensionConstraintConfigTest {
27+
28+
private companion object {
29+
val authorizationList =
30+
AuthorizationList(purposes = setOf(1.toBigInteger()), algorithms = 1.toBigInteger())
31+
32+
fun createTestKeyDescription(
33+
attestationSecurityLevel: SecurityLevel,
34+
keyMintSecurityLevel: SecurityLevel,
35+
) =
36+
KeyDescription(
37+
attestationVersion = 1.toBigInteger(),
38+
attestationSecurityLevel = attestationSecurityLevel,
39+
keyMintVersion = 1.toBigInteger(),
40+
keyMintSecurityLevel = keyMintSecurityLevel,
41+
attestationChallenge = ByteString.empty(),
42+
uniqueId = ByteString.empty(),
43+
softwareEnforced = authorizationList,
44+
hardwareEnforced = authorizationList,
45+
)
46+
}
47+
48+
val keyDescriptionWithStrongBoxSecurityLevels =
49+
createTestKeyDescription(SecurityLevel.STRONG_BOX, SecurityLevel.STRONG_BOX)
50+
val keyDescriptionWithTeeSecurityLevels =
51+
createTestKeyDescription(SecurityLevel.TRUSTED_ENVIRONMENT, SecurityLevel.TRUSTED_ENVIRONMENT)
52+
val keyDescriptionWithSoftwareSecurityLevels =
53+
createTestKeyDescription(SecurityLevel.SOFTWARE, SecurityLevel.SOFTWARE)
54+
val keyDescriptionWithMismatchedSecurityLevels =
55+
createTestKeyDescription(SecurityLevel.STRONG_BOX, SecurityLevel.TRUSTED_ENVIRONMENT)
56+
57+
@Test
58+
fun ValidationLevelIsSatisfiedBy_strictWithExpectedValue() {
59+
val level = ValidationLevel.STRICT("foo")
60+
61+
assertThat(level.isSatisfiedBy("foo")).isTrue()
62+
assertThat(level.isSatisfiedBy("bar")).isFalse()
63+
assertThat(level.isSatisfiedBy(null)).isFalse()
64+
}
65+
66+
@Test
67+
fun ValidationLevelIsSatisfiedBy_notNull_allowsAnyValue() {
68+
val level = ValidationLevel.NOT_NULL
69+
70+
assertThat(level.isSatisfiedBy("foo")).isTrue()
71+
assertThat(level.isSatisfiedBy(null)).isFalse()
72+
}
73+
74+
@Test
75+
fun ValidationLevelIsSatisfiedBy_ignore_allowsAnyValue() {
76+
val level = ValidationLevel.IGNORE
77+
78+
assertThat(level.isSatisfiedBy("foo")).isTrue()
79+
assertThat(level.isSatisfiedBy(null)).isTrue()
80+
}
81+
82+
@Test
83+
fun SecurityLevelValidationLevelIsSatisfiedBy_strictWithExpectedValue() {
84+
val level = SecurityLevelValidationLevel.STRICT(SecurityLevel.STRONG_BOX)
85+
86+
assertThat(level.isSatisfiedBy(keyDescriptionWithStrongBoxSecurityLevels)).isTrue()
87+
assertThat(level.isSatisfiedBy(keyDescriptionWithTeeSecurityLevels)).isFalse()
88+
assertThat(level.isSatisfiedBy(keyDescriptionWithMismatchedSecurityLevels)).isFalse()
89+
}
90+
91+
@Test
92+
fun SecurityLevelValidationLevelIsSatisfiedBy_notSoftware_allowsAnyNonSoftwareMatchingLevels() {
93+
val level = SecurityLevelValidationLevel.NOT_SOFTWARE
94+
95+
assertThat(level.isSatisfiedBy(keyDescriptionWithStrongBoxSecurityLevels)).isTrue()
96+
assertThat(level.isSatisfiedBy(keyDescriptionWithTeeSecurityLevels)).isTrue()
97+
assertThat(level.isSatisfiedBy(keyDescriptionWithSoftwareSecurityLevels)).isFalse()
98+
assertThat(level.isSatisfiedBy(keyDescriptionWithMismatchedSecurityLevels)).isFalse()
99+
}
100+
101+
@Test
102+
fun SecurityLevelValidationLevelIsSatisfiedBy_consistent_allowsAnyMatchingLevels() {
103+
val level = SecurityLevelValidationLevel.CONSISTENT
104+
105+
assertThat(level.isSatisfiedBy(keyDescriptionWithStrongBoxSecurityLevels)).isTrue()
106+
assertThat(level.isSatisfiedBy(keyDescriptionWithTeeSecurityLevels)).isTrue()
107+
assertThat(level.isSatisfiedBy(keyDescriptionWithSoftwareSecurityLevels)).isTrue()
108+
assertThat(level.isSatisfiedBy(keyDescriptionWithMismatchedSecurityLevels)).isFalse()
109+
}
110+
}

src/test/kotlin/VerifierTest.kt

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -173,20 +173,34 @@ class VerifierTest {
173173
fun rootOfTrustMissing_givesRootOfTrustMissingReason() {
174174
val result =
175175
assertIs<ExtensionConstraintViolation>(verifier.verify(CertLists.missingRootOfTrust))
176-
assertThat(result.reason).isEqualTo(KeyAttestationReason.ROOT_OF_TRUST_MISSING)
176+
assertThat(result.reason).isEqualTo(KeyAttestationReason.ROOT_OF_TRUST_CONSTRAINT_VIOLATION)
177177
}
178178

179179
@Test
180180
fun keyOriginNotGenerated_throwsCertPathValidatorException() {
181181
val result = assertIs<ExtensionConstraintViolation>(verifier.verify(CertLists.importedOrigin))
182-
assertThat(result.reason).isEqualTo(KeyAttestationReason.KEY_ORIGIN_NOT_GENERATED)
182+
assertThat(result.reason).isEqualTo(KeyAttestationReason.KEY_ORIGIN_CONSTRAINT_VIOLATION)
183183
}
184184

185185
@Test
186186
fun mismatchedSecurityLevels_throwsCertPathValidatorException() {
187187
val result =
188188
assertIs<ExtensionConstraintViolation>(verifier.verify(CertLists.mismatchedSecurityLevels))
189-
assertThat(result.reason).isEqualTo(KeyAttestationReason.MISMATCHED_SECURITY_LEVELS)
189+
assertThat(result.reason).isEqualTo(KeyAttestationReason.SECURITY_LEVEL_CONSTRAINT_VIOLATION)
190+
}
191+
192+
@Test
193+
fun mismatchedSecurityLevels_customConfig_succeeds() {
194+
val verifier =
195+
Verifier(
196+
{ prodAnchors + TrustAnchor(Certs.root, null) },
197+
{ setOf<String>() },
198+
{ FakeCalendar.DEFAULT.now() },
199+
ExtensionConstraintConfig(securityLevel = ValidationLevel.NOT_NULL),
200+
)
201+
val result =
202+
assertIs<VerificationResult.Success>(verifier.verify(CertLists.mismatchedSecurityLevels))
203+
assertThat(result.securityLevel).isEqualTo(SecurityLevel.SOFTWARE)
190204
}
191205

192206
@Test

0 commit comments

Comments
 (0)