Skip to content

Commit 65412e2

Browse files
sethmoocopybara-github
authored andcommitted
Make ConstraintConfig more generic by parameterizing the constrained attribute
This is the first step toward making the constraints truly generic. Instead of making each constraint take a specific attribute, make all the constraints accept common types. The contraints are them customize to extract the constrained attributes using mapping functions. PiperOrigin-RevId: 888423834
1 parent 87f2a9a commit 65412e2

File tree

5 files changed

+285
-150
lines changed

5 files changed

+285
-150
lines changed

src/main/kotlin/ConstraintConfig.kt

Lines changed: 155 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -17,46 +17,137 @@
1717
package com.android.keyattestation.verifier
1818

1919
import androidx.annotation.RequiresApi
20+
import com.google.common.collect.ImmutableList
2021
import com.google.errorprone.annotations.Immutable
2122
import com.google.errorprone.annotations.ThreadSafe
2223

24+
private typealias AttributeMapper = (KeyDescription) -> Any?
25+
26+
/** An individual limit to place on the KeyDescription from an attestation certificate. */
27+
@ThreadSafe
28+
sealed interface Constraint {
29+
sealed interface Result {}
30+
31+
data object Satisfied : Result
32+
33+
data class Violated(val failureMessage: String) : Result
34+
35+
/** Fixed label, suitable for logging or metrics. */
36+
val label: String
37+
38+
/** Verifies that [description] satisfies this [Constraint]. */
39+
fun check(description: KeyDescription): Result
40+
}
41+
2342
/**
2443
* Configuration for validating the attributes in an Android attestation certificate, as described
2544
* at https://source.android.com/docs/security/features/keystore/attestation.
2645
*/
2746
@ThreadSafe
28-
data class ConstraintConfig(
29-
val keyOrigin: ValidationLevel<Origin> = ValidationLevel.STRICT(Origin.GENERATED),
30-
val securityLevel: ValidationLevel<KeyDescription> = SecurityLevelValidationLevel.NOT_SOFTWARE,
31-
val rootOfTrust: ValidationLevel<RootOfTrust> = ValidationLevel.NOT_NULL,
32-
val authorizationListTagOrder: ValidationLevel<KeyDescription> = ValidationLevel.IGNORE,
33-
)
34-
35-
/** Configuration for validating a single attribute in an Android attestation certificate. */
47+
class ConstraintConfig(
48+
val keyOrigin: Constraint? = null,
49+
val securityLevel: Constraint? = null,
50+
val rootOfTrust: Constraint? = null,
51+
val additionalConstraints: ImmutableList<Constraint> = ImmutableList.of(),
52+
) {
53+
@RequiresApi(24)
54+
fun getConstraints() =
55+
ImmutableList.builder<Constraint>()
56+
.add(
57+
keyOrigin
58+
?: AttributeConstraint.STRICT("Origin", Origin.GENERATED) { it.hardwareEnforced.origin }
59+
)
60+
.add(securityLevel ?: SecurityLevelConstraint.NOT_SOFTWARE)
61+
.add(
62+
rootOfTrust
63+
?: AttributeConstraint.NOT_NULL("Root of trust") { it.hardwareEnforced.rootOfTrust }
64+
)
65+
.addAll(additionalConstraints)
66+
.build()
67+
}
68+
69+
/**
70+
* We need a builder to support creating a [ConstraintConfig], as it's a thread-safe object. A
71+
* Kotlin-idiomatic builder function is provided below.
72+
*/
73+
class ConstraintConfigBuilder() {
74+
var keyOrigin: Constraint? = null
75+
var securityLevel: Constraint? = null
76+
var rootOfTrust: Constraint? = null
77+
var additionalConstraints: MutableList<Constraint> = mutableListOf()
78+
79+
fun securityLevel(constraint: () -> Constraint) {
80+
this.securityLevel = constraint()
81+
}
82+
83+
fun keyOrigin(constraint: () -> Constraint) {
84+
this.keyOrigin = constraint()
85+
}
86+
87+
fun rootOfTrust(constraint: () -> Constraint) {
88+
this.rootOfTrust = constraint()
89+
}
90+
91+
fun additionalConstraint(constraint: () -> Constraint) {
92+
additionalConstraints.add(constraint())
93+
}
94+
95+
fun build(): ConstraintConfig =
96+
ConstraintConfig(
97+
keyOrigin,
98+
securityLevel,
99+
rootOfTrust,
100+
ImmutableList.copyOf(additionalConstraints),
101+
)
102+
}
103+
104+
/** Implements a Kotlin-style type safe builder for creating a [ConstraintConfig]. */
105+
fun constraintConfig(init: ConstraintConfigBuilder.() -> Unit): ConstraintConfig {
106+
val builder = ConstraintConfigBuilder()
107+
builder.init()
108+
return builder.build()
109+
}
110+
111+
/** Constraint that is always satisfied. */
112+
@Immutable
113+
data object IgnoredConstraint : Constraint {
114+
override val label = "Ignored"
115+
116+
override fun check(description: KeyDescription) = Constraint.Satisfied
117+
}
118+
119+
/** Constraint that checks a single attribute of the [KeyDescription]. */
36120
@Immutable(containerOf = ["T"])
37-
sealed interface ValidationLevel<out T> {
38-
/** Evaluates whether the [attribute] is satisfied by this [ValidationLevel]. */
39-
fun isSatisfiedBy(attribute: Any?): Boolean
121+
sealed class AttributeConstraint<out T>(override val label: String, val mapper: AttributeMapper?) :
122+
Constraint {
123+
/** Evaluates whether the [description] is satisfied by this [AttributeConstraint]. */
124+
override fun check(description: KeyDescription) =
125+
if (isSatisfied(mapper?.invoke(description))) {
126+
Constraint.Satisfied
127+
} else {
128+
Constraint.Violated(getFailureMessage(mapper?.invoke(description)))
129+
}
130+
131+
internal abstract fun isSatisfied(attribute: Any?): Boolean
132+
133+
internal open fun getFailureMessage(attribute: Any?): String =
134+
"$label violates constraint: value=$attribute, config=$this"
40135

41136
/**
42137
* Checks that the attribute exists and matches the expected value.
43138
*
44139
* @param expectedVal The expected value of the attribute.
45140
*/
46141
@Immutable(containerOf = ["T"])
47-
data class STRICT<T>(val expectedVal: T) : ValidationLevel<T> {
48-
override fun isSatisfiedBy(attribute: Any?): Boolean = attribute == expectedVal
142+
data class STRICT<T>(val l: String, val expectedVal: T, private val m: AttributeMapper) :
143+
AttributeConstraint<T>(l, m) {
144+
override fun isSatisfied(attribute: Any?): Boolean = attribute == expectedVal
49145
}
50146

51147
/* Check that the attribute exists. */
52-
@Immutable
53-
data object NOT_NULL : ValidationLevel<Nothing> {
54-
override fun isSatisfiedBy(attribute: Any?): Boolean = attribute != null
55-
}
56-
57-
@Immutable
58-
data object IGNORE : ValidationLevel<Nothing> {
59-
override fun isSatisfiedBy(attribute: Any?): Boolean = true
148+
data class NOT_NULL(val l: String, private val m: AttributeMapper) :
149+
AttributeConstraint<Nothing>(l, m) {
150+
override fun isSatisfied(attribute: Any?): Boolean = attribute != null
60151
}
61152
}
62153

@@ -65,74 +156,81 @@ sealed interface ValidationLevel<out T> {
65156
* Android attestation certificate.
66157
*/
67158
@Immutable
68-
sealed class SecurityLevelValidationLevel : ValidationLevel<KeyDescription> {
69-
@RequiresApi(24)
70-
fun areSecurityLevelsMatching(keyDescription: KeyDescription): Boolean {
71-
return keyDescription.attestationSecurityLevel == keyDescription.keyMintSecurityLevel
159+
@RequiresApi(24)
160+
sealed class SecurityLevelConstraint(val isSatisfied: (KeyDescription) -> Boolean) : Constraint {
161+
companion object {
162+
const val LABEL = "Security level"
72163
}
73164

165+
override val label = LABEL
166+
167+
override fun check(description: KeyDescription) =
168+
if (isSatisfied(description)) {
169+
Constraint.Satisfied
170+
} else {
171+
Constraint.Violated(getFailureMessage(description))
172+
}
173+
174+
fun getFailureMessage(description: KeyDescription): String =
175+
"Security level violates constraint: " +
176+
"keyMintSecurityLevel=${description.keyMintSecurityLevel}, " +
177+
"attestationSecurityLevel=${description.attestationSecurityLevel}, " +
178+
"config=$this"
179+
74180
/**
75181
* Checks that both the attestationSecurityLevel and keyMintSecurityLevel match the expected
76182
* value.
77183
*
78184
* @param expectedVal The expected value of the security level.
79185
*/
80186
@Immutable
81-
data class STRICT(val expectedVal: SecurityLevel) : SecurityLevelValidationLevel() {
82-
@RequiresApi(24)
83-
override fun isSatisfiedBy(attribute: Any?): Boolean {
84-
val keyDescription = attribute as? KeyDescription ?: return false
85-
val securityLevelIsExpected = keyDescription.attestationSecurityLevel == this.expectedVal
86-
return areSecurityLevelsMatching(keyDescription) && securityLevelIsExpected
87-
}
88-
}
187+
data class STRICT(val expectedVal: SecurityLevel) :
188+
SecurityLevelConstraint({
189+
it.keyMintSecurityLevel == expectedVal && it.attestationSecurityLevel == expectedVal
190+
})
89191

90192
/**
91193
* Checks that the attestationSecurityLevel is equal to the keyMintSecurityLevel, and that this
92194
* security level is not [SecurityLevel.SOFTWARE].
93195
*/
94196
@Immutable
95-
data object NOT_SOFTWARE : SecurityLevelValidationLevel() {
96-
@RequiresApi(24)
97-
override fun isSatisfiedBy(attribute: Any?): Boolean {
98-
val keyDescription = attribute as? KeyDescription ?: return false
99-
val securityLevelIsSoftware =
100-
keyDescription.attestationSecurityLevel == SecurityLevel.SOFTWARE
101-
return areSecurityLevelsMatching(keyDescription) && !securityLevelIsSoftware
102-
}
103-
}
197+
data object NOT_SOFTWARE :
198+
SecurityLevelConstraint({
199+
it.keyMintSecurityLevel == it.attestationSecurityLevel &&
200+
it.attestationSecurityLevel != SecurityLevel.SOFTWARE
201+
})
104202

105203
/**
106204
* Checks that the attestationSecurityLevel is equal to the keyMintSecurityLevel, regardless of
107205
* security level.
108206
*/
109207
@Immutable
110-
data object CONSISTENT : SecurityLevelValidationLevel() {
111-
@RequiresApi(24)
112-
override fun isSatisfiedBy(attribute: Any?): Boolean {
113-
val keyDescription = attribute as? KeyDescription ?: return false
114-
return areSecurityLevelsMatching(keyDescription)
115-
}
116-
}
208+
data object CONSISTENT :
209+
SecurityLevelConstraint({ it.attestationSecurityLevel == it.keyMintSecurityLevel })
117210
}
118211

119212
/**
120213
* Configuration for validating the ordering of the attributes in the AuthorizationList sequence in
121214
* an Android attestation certificate.
122215
*/
123216
@Immutable
124-
sealed interface TagOrderValidationLevel : ValidationLevel<KeyDescription> {
217+
@RequiresApi(24)
218+
sealed class TagOrderConstraint : Constraint {
219+
override val label = "Tag order"
220+
125221
/**
126222
* Checks that the attributes in the AuthorizationList sequence appear in the order specified by
127223
* https://source.android.com/docs/security/features/keystore/attestation#schema.
128224
*/
129225
@Immutable
130-
data object STRICT : TagOrderValidationLevel {
131-
@RequiresApi(24)
132-
override fun isSatisfiedBy(attribute: Any?): Boolean {
133-
val keyDescription = attribute as? KeyDescription ?: return false
134-
return keyDescription.softwareEnforced.areTagsOrdered &&
135-
keyDescription.hardwareEnforced.areTagsOrdered
136-
}
226+
data object STRICT : TagOrderConstraint() {
227+
override fun check(description: KeyDescription) =
228+
if (
229+
description.softwareEnforced.areTagsOrdered && description.hardwareEnforced.areTagsOrdered
230+
) {
231+
Constraint.Satisfied
232+
} else {
233+
Constraint.Violated("Authorization list tags must be in ascending order")
234+
}
137235
}
138236
}

src/main/kotlin/KeyAttestationReason.kt

Lines changed: 2 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -33,22 +33,8 @@ 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 origin violated the constraint provided in [ConstraintConfig].
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 [ConstraintConfig].
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 [ConstraintConfig].
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-
// [ConstraintConfig].
51-
AUTHORIZATION_LIST_ORDERING_CONSTRAINT_VIOLATION,
36+
// One of the constraints provided to the verifier was violated.
37+
CONSTRAINT_VIOLATION,
5238
// There was an error parsing the key description and an unknown tag number was encountered.
5339
UNKNOWN_TAG_NUMBER,
5440
}

src/main/kotlin/Verifier.kt

Lines changed: 10 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@ sealed interface VerificationResult {
6060

6161
data class ExtensionParsingFailure(val cause: ExtensionParsingException) : VerificationResult
6262

63-
data class ConstraintViolation(val cause: String, val reason: KeyAttestationReason) :
63+
data class ConstraintViolation(val constraintLabel: String, val cause: String) :
6464
VerificationResult
6565

6666
data object SoftwareAttestationUnsupported : VerificationResult
@@ -291,40 +291,21 @@ constructor(
291291
}
292292
}
293293

294-
val origin = keyDescription.hardwareEnforced.origin
295-
if (!constraintConfig.keyOrigin.isSatisfiedBy(origin)) {
296-
return VerificationResult.ConstraintViolation(
297-
"Origin violates constraint: value=${origin}, config=${constraintConfig.keyOrigin}",
298-
KeyAttestationReason.KEY_ORIGIN_CONSTRAINT_VIOLATION,
299-
)
294+
for (constraint in constraintConfig.getConstraints()) {
295+
val result = constraint.check(keyDescription)
296+
when (result) {
297+
is Constraint.Satisfied -> {}
298+
is Constraint.Violated -> {
299+
return VerificationResult.ConstraintViolation(constraint.label, result.failureMessage)
300+
}
301+
}
300302
}
301303

302304
val securityLevel =
303-
if (constraintConfig.securityLevel.isSatisfiedBy(keyDescription)) {
304-
minOf(keyDescription.attestationSecurityLevel, keyDescription.keyMintSecurityLevel)
305-
} else {
306-
return VerificationResult.ConstraintViolation(
307-
"Security level violates constraint: value=${keyDescription.attestationSecurityLevel}, config=${constraintConfig.securityLevel}",
308-
KeyAttestationReason.SECURITY_LEVEL_CONSTRAINT_VIOLATION,
309-
)
310-
}
311-
305+
minOf(keyDescription.attestationSecurityLevel, keyDescription.keyMintSecurityLevel)
312306
val rootOfTrust = keyDescription.hardwareEnforced.rootOfTrust
313-
if (!constraintConfig.rootOfTrust.isSatisfiedBy(rootOfTrust)) {
314-
return VerificationResult.ConstraintViolation(
315-
"Root of trust violates constraint: value=${rootOfTrust}, config=${constraintConfig.rootOfTrust}",
316-
KeyAttestationReason.ROOT_OF_TRUST_CONSTRAINT_VIOLATION,
317-
)
318-
}
319307
val verifiedBootState = rootOfTrust?.verifiedBootState ?: VerifiedBootState.UNVERIFIED
320308

321-
if (!constraintConfig.authorizationListTagOrder.isSatisfiedBy(keyDescription)) {
322-
return VerificationResult.ConstraintViolation(
323-
"Authorization list ordering violates constraint: config=${constraintConfig.authorizationListTagOrder}",
324-
KeyAttestationReason.AUTHORIZATION_LIST_ORDERING_CONSTRAINT_VIOLATION,
325-
)
326-
}
327-
328309
return VerificationResult.Success(
329310
pathValidationResult.publicKey,
330311
keyDescription.attestationChallenge,

0 commit comments

Comments
 (0)