Skip to content

Commit 862e7d5

Browse files
suzannajiwanicopybara-github
authored andcommitted
Add constraint for authorization list tag ordering
PiperOrigin-RevId: 850151679
1 parent 851433e commit 862e7d5

File tree

6 files changed

+361
-31
lines changed

6 files changed

+361
-31
lines changed

src/main/kotlin/Extension.kt

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -333,6 +333,7 @@ data class AuthorizationList(
333333
val bootPatchLevel: PatchLevel? = null,
334334
val attestationIdSecondImei: String? = null,
335335
val moduleHash: ByteString? = null,
336+
internal val areTagsOrdered: Boolean = true,
336337
) {
337338
/**
338339
* Converts the representation of an [AuthorizationList] to an ASN.1 sequence.
@@ -460,7 +461,8 @@ data class AuthorizationList(
460461
* 2. within each class of tags, the elements or alternatives shall appear in ascending order
461462
* of their tag numbers.
462463
*/
463-
if (!objects.keys.zipWithNext().all { (lhs, rhs) -> rhs > lhs }) {
464+
val areTagsOrdered = (objects.keys.zipWithNext().all { (lhs, rhs) -> rhs > lhs })
465+
if (!areTagsOrdered) {
464466
logFn("AuthorizationList tags should appear in ascending order")
465467
}
466468

@@ -507,6 +509,7 @@ data class AuthorizationList(
507509
bootPatchLevel = converter.parsePatchLevel(KeyMintTag.BOOT_PATCH_LEVEL, "boot"),
508510
attestationIdSecondImei = converter.parseStr(KeyMintTag.ATTESTATION_ID_SECOND_IMEI),
509511
moduleHash = converter.parseByteString(KeyMintTag.MODULE_HASH),
512+
areTagsOrdered = areTagsOrdered,
510513
)
511514
}
512515
}
Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
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 attenstation 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: SecurityLevelValidationLevel = SecurityLevelValidationLevel.STRICT(),
31+
val rootOfTrust: ValidationLevel<RootOfTrust> = ValidationLevel.STRICT(null),
32+
val authorizationListOrdering: AuthorizationListOrdering = AuthorizationListOrdering.IGNORE,
33+
)
34+
35+
/**
36+
* Configuration for validating a single extension in an Android attenstation certificate.
37+
*
38+
* @param expectedVal The expected value of the extension. If null, the extension is checked for
39+
* existence but not equality.
40+
*/
41+
@Immutable(containerOf = ["T"])
42+
sealed interface ValidationLevel<out T> {
43+
@Immutable(containerOf = ["T"]) data class STRICT<T>(val expectedVal: T?) : ValidationLevel<T>
44+
45+
@Immutable data object IGNORE : ValidationLevel<Nothing>
46+
}
47+
48+
/**
49+
* Configuration for validating the attestationSecurityLevel and keyMintSecurityLevel fields in an
50+
* Android attenstation certificate.
51+
*/
52+
@Immutable
53+
sealed interface SecurityLevelValidationLevel {
54+
/**
55+
* Checks that the attestationSecurityLevel is both (1) one of {TRUSTED_ENVIRONMENT, STRONG_BOX}
56+
* and (2) equal to the keyMintSecurityLevel.
57+
*
58+
* If expectedVal is provided, checks that both the attestationSecurityLevel and
59+
* keyMintSecurityLevel are equal to the expected value.
60+
*/
61+
@Immutable
62+
data class STRICT(val expectedVal: SecurityLevel? = null) : SecurityLevelValidationLevel {
63+
init {
64+
require(expectedVal != SecurityLevel.SOFTWARE) {
65+
"STRICT validation level cannot be used with SOFTWARE security level."
66+
}
67+
}
68+
}
69+
70+
/**
71+
* Checks that the attestationSecurityLevel is equal to the keyMintSecurityLevel, regardless of
72+
* security level
73+
*/
74+
@Immutable data object CONSISTENT : SecurityLevelValidationLevel
75+
76+
/**
77+
* Checks that attestationSecurityLevel and keyMintSecurityLevel both exist and are correctly
78+
* formed. If they are unequal, [Verifier.verify] will return the lower securityLevel.
79+
*/
80+
@Immutable data object EXISTS : SecurityLevelValidationLevel
81+
}
82+
83+
/**
84+
* Configuration for validating the ordering of the extensions in the AuthorizationList sequence in
85+
* an Android attenstation certificate.
86+
*/
87+
@Immutable
88+
sealed interface AuthorizationListOrdering {
89+
90+
/**
91+
* Checks that the extensions in the AuthorizationList sequence appear in the order specified by
92+
* https://source.android.com/docs/security/features/keystore/attestation#schema.
93+
*/
94+
@Immutable data object STRICT : AuthorizationListOrdering
95+
96+
/** Allows the extensions in the AuthorizationList sequence appear in any order. */
97+
@Immutable data object IGNORE : AuthorizationListOrdering
98+
}
99+
100+
/** Evaluates whether the [extension] is satisfied by the [ValidationLevel]. */
101+
fun <T> ValidationLevel<T>.isSatisfiedBy(extension: T?): Boolean =
102+
when (this) {
103+
is ValidationLevel.STRICT ->
104+
if (expectedVal == null) extension != null else extension == expectedVal
105+
is ValidationLevel.IGNORE -> true
106+
}
107+
108+
/** Evaluates whether the [keyDescription] is satisfied by the [SecurityLevelValidationLevel]. */
109+
@RequiresApi(24)
110+
fun SecurityLevelValidationLevel.isSatisfiedBy(keyDescription: KeyDescription): Boolean {
111+
val securityLevelsMatch =
112+
keyDescription.attestationSecurityLevel == keyDescription.keyMintSecurityLevel
113+
114+
return when (this) {
115+
is SecurityLevelValidationLevel.STRICT -> {
116+
val securityLevelIsExpected =
117+
if (this.expectedVal != null) keyDescription.attestationSecurityLevel == this.expectedVal
118+
else keyDescription.attestationSecurityLevel != SecurityLevel.SOFTWARE
119+
securityLevelsMatch && securityLevelIsExpected
120+
}
121+
is SecurityLevelValidationLevel.CONSISTENT -> securityLevelsMatch
122+
is SecurityLevelValidationLevel.EXISTS -> true
123+
}
124+
}
125+
126+
/** Evaluates whether the [keyDescription] is satisfied by the [AuthorizationListOrdering]. */
127+
@RequiresApi(24)
128+
fun AuthorizationListOrdering.isSatisfiedBy(keyDescription: KeyDescription): Boolean =
129+
when (this) {
130+
is AuthorizationListOrdering.STRICT ->
131+
keyDescription.softwareEnforced.areTagsOrdered &&
132+
keyDescription.hardwareEnforced.areTagsOrdered
133+
is AuthorizationListOrdering.IGNORE -> true
134+
}

src/main/kotlin/KeyAttestationReason.kt

Lines changed: 16 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -33,15 +33,22 @@ 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,
49+
// The authorization list ordering violated the constraint provided in
50+
// [ExtensionConstraintConfig].
51+
AUTHORIZATION_LIST_ORDERING_CONSTRAINT_VIOLATION,
4552
// There was an error parsing the key description and an unknown tag number was encountered.
4653
UNKNOWN_TAG_NUMBER,
4754
}

src/main/kotlin/Verifier.kt

Lines changed: 31 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())
@@ -284,43 +287,53 @@ open class Verifier(
284287
return VerificationResult.ExtensionParsingFailure(ExtensionParsingException(e.toString()))
285288
}
286289
log?.logKeyDescription(keyDescription)
290+
291+
if (!extensionConstraintConfig.authorizationListOrdering.isSatisfiedBy(keyDescription)) {
292+
return VerificationResult.ExtensionConstraintViolation(
293+
"Authorization list ordering violates constraint: config=${extensionConstraintConfig.authorizationListOrdering}",
294+
KeyAttestationReason.AUTHORIZATION_LIST_ORDERING_CONSTRAINT_VIOLATION,
295+
)
296+
}
297+
287298
if (challengeChecker != null) {
288299
val checkResult = challengeChecker.checkChallenge(keyDescription.attestationChallenge).await()
289300
if (!checkResult) {
290301
return VerificationResult.ChallengeMismatch
291302
}
292303
}
293304

294-
if (
295-
keyDescription.hardwareEnforced.origin == null ||
296-
keyDescription.hardwareEnforced.origin != Origin.GENERATED
297-
) {
305+
val origin = keyDescription.hardwareEnforced.origin
306+
if (!extensionConstraintConfig.keyOrigin.isSatisfiedBy(origin)) {
298307
return VerificationResult.ExtensionConstraintViolation(
299-
"origin != GENERATED: ${keyDescription.hardwareEnforced.origin}",
300-
KeyAttestationReason.KEY_ORIGIN_NOT_GENERATED,
308+
"Origin violates constraint: value=${origin}, config=${extensionConstraintConfig.keyOrigin}",
309+
KeyAttestationReason.KEY_ORIGIN_CONSTRAINT_VIOLATION,
301310
)
302311
}
303312

304313
val securityLevel =
305-
if (keyDescription.attestationSecurityLevel == keyDescription.keyMintSecurityLevel) {
306-
keyDescription.attestationSecurityLevel
314+
if (extensionConstraintConfig.securityLevel.isSatisfiedBy(keyDescription)) {
315+
minOf(keyDescription.attestationSecurityLevel, keyDescription.keyMintSecurityLevel)
307316
} else {
308317
return VerificationResult.ExtensionConstraintViolation(
309-
"attestationSecurityLevel != keyMintSecurityLevel: ${keyDescription.attestationSecurityLevel} != ${keyDescription.keyMintSecurityLevel}",
310-
KeyAttestationReason.MISMATCHED_SECURITY_LEVELS,
318+
"Security level violates constraint: value=${keyDescription.attestationSecurityLevel}, config=${extensionConstraintConfig.securityLevel}",
319+
KeyAttestationReason.SECURITY_LEVEL_CONSTRAINT_VIOLATION,
311320
)
312321
}
313-
val rootOfTrust =
314-
keyDescription.hardwareEnforced.rootOfTrust
315-
?: return VerificationResult.ExtensionConstraintViolation(
316-
"hardwareEnforced.rootOfTrust is null",
317-
KeyAttestationReason.ROOT_OF_TRUST_MISSING,
318-
)
322+
323+
val rootOfTrust = keyDescription.hardwareEnforced.rootOfTrust
324+
if (!extensionConstraintConfig.rootOfTrust.isSatisfiedBy(rootOfTrust)) {
325+
return VerificationResult.ExtensionConstraintViolation(
326+
"Root of trust violates constraint: value=${rootOfTrust}, config=${extensionConstraintConfig.rootOfTrust}",
327+
KeyAttestationReason.ROOT_OF_TRUST_CONSTRAINT_VIOLATION,
328+
)
329+
}
330+
val verifiedBootState = rootOfTrust?.verifiedBootState ?: VerifiedBootState.UNVERIFIED
331+
319332
return VerificationResult.Success(
320333
pathValidationResult.publicKey,
321334
keyDescription.attestationChallenge,
322335
securityLevel,
323-
rootOfTrust.verifiedBootState,
336+
verifiedBootState,
324337
deviceInformation,
325338
DeviceIdentity.parseFrom(keyDescription),
326339
)

0 commit comments

Comments
 (0)