Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
212 changes: 155 additions & 57 deletions src/main/kotlin/ConstraintConfig.kt
Original file line number Diff line number Diff line change
Expand Up @@ -17,46 +17,137 @@
package com.android.keyattestation.verifier

import androidx.annotation.RequiresApi
import com.google.common.collect.ImmutableList
import com.google.errorprone.annotations.Immutable
import com.google.errorprone.annotations.ThreadSafe

private typealias AttributeMapper = (KeyDescription) -> Any?

/** An individual limit to place on the KeyDescription from an attestation certificate. */
@ThreadSafe
sealed interface Constraint {
sealed interface Result {}

data object Satisfied : Result

data class Violated(val failureMessage: String) : Result

/** Fixed label, suitable for logging or metrics. */
val label: String

/** Verifies that [description] satisfies this [Constraint]. */
fun check(description: KeyDescription): Result
}

/**
* Configuration for validating the attributes in an Android attestation certificate, as described
* at https://source.android.com/docs/security/features/keystore/attestation.
*/
@ThreadSafe
data class ConstraintConfig(
val keyOrigin: ValidationLevel<Origin> = ValidationLevel.STRICT(Origin.GENERATED),
val securityLevel: ValidationLevel<KeyDescription> = SecurityLevelValidationLevel.NOT_SOFTWARE,
val rootOfTrust: ValidationLevel<RootOfTrust> = ValidationLevel.NOT_NULL,
val authorizationListTagOrder: ValidationLevel<KeyDescription> = ValidationLevel.IGNORE,
)

/** Configuration for validating a single attribute in an Android attestation certificate. */
class ConstraintConfig(
val keyOrigin: Constraint? = null,
val securityLevel: Constraint? = null,
val rootOfTrust: Constraint? = null,
val additionalConstraints: ImmutableList<Constraint> = ImmutableList.of(),
) {
@RequiresApi(24)
fun getConstraints() =
ImmutableList.builder<Constraint>()
.add(
keyOrigin
?: AttributeConstraint.STRICT("Origin", Origin.GENERATED) { it.hardwareEnforced.origin }
)
.add(securityLevel ?: SecurityLevelConstraint.NOT_SOFTWARE)
.add(
rootOfTrust
?: AttributeConstraint.NOT_NULL("Root of trust") { it.hardwareEnforced.rootOfTrust }
)
.addAll(additionalConstraints)
.build()
}

/**
* We need a builder to support creating a [ConstraintConfig], as it's a thread-safe object. A
* Kotlin-idiomatic builder function is provided below.
*/
class ConstraintConfigBuilder() {
var keyOrigin: Constraint? = null
var securityLevel: Constraint? = null
var rootOfTrust: Constraint? = null
var additionalConstraints: MutableList<Constraint> = mutableListOf()

fun securityLevel(constraint: () -> Constraint) {
this.securityLevel = constraint()
}

fun keyOrigin(constraint: () -> Constraint) {
this.keyOrigin = constraint()
}

fun rootOfTrust(constraint: () -> Constraint) {
this.rootOfTrust = constraint()
}

fun additionalConstraint(constraint: () -> Constraint) {
additionalConstraints.add(constraint())
}

fun build(): ConstraintConfig =
ConstraintConfig(
keyOrigin,
securityLevel,
rootOfTrust,
ImmutableList.copyOf(additionalConstraints),
)
}

/** Implements a Kotlin-style type safe builder for creating a [ConstraintConfig]. */
fun constraintConfig(init: ConstraintConfigBuilder.() -> Unit): ConstraintConfig {
val builder = ConstraintConfigBuilder()
builder.init()
return builder.build()
}

/** Constraint that is always satisfied. */
@Immutable
data object IgnoredConstraint : Constraint {
override val label = "Ignored"

override fun check(description: KeyDescription) = Constraint.Satisfied
}

/** Constraint that checks a single attribute of the [KeyDescription]. */
@Immutable(containerOf = ["T"])
sealed interface ValidationLevel<out T> {
/** Evaluates whether the [attribute] is satisfied by this [ValidationLevel]. */
fun isSatisfiedBy(attribute: Any?): Boolean
sealed class AttributeConstraint<out T>(override val label: String, val mapper: AttributeMapper?) :
Constraint {
/** Evaluates whether the [description] is satisfied by this [AttributeConstraint]. */
override fun check(description: KeyDescription) =
if (isSatisfied(mapper?.invoke(description))) {
Constraint.Satisfied
} else {
Constraint.Violated(getFailureMessage(mapper?.invoke(description)))
}

internal abstract fun isSatisfied(attribute: Any?): Boolean

internal open fun getFailureMessage(attribute: Any?): String =
"$label violates constraint: value=$attribute, config=$this"

/**
* Checks that the attribute exists and matches the expected value.
*
* @param expectedVal The expected value of the attribute.
*/
@Immutable(containerOf = ["T"])
data class STRICT<T>(val expectedVal: T) : ValidationLevel<T> {
override fun isSatisfiedBy(attribute: Any?): Boolean = attribute == expectedVal
data class STRICT<T>(val l: String, val expectedVal: T, private val m: AttributeMapper) :
AttributeConstraint<T>(l, m) {
override fun isSatisfied(attribute: Any?): Boolean = attribute == expectedVal
}

/* Check that the attribute exists. */
@Immutable
data object NOT_NULL : ValidationLevel<Nothing> {
override fun isSatisfiedBy(attribute: Any?): Boolean = attribute != null
}

@Immutable
data object IGNORE : ValidationLevel<Nothing> {
override fun isSatisfiedBy(attribute: Any?): Boolean = true
data class NOT_NULL(val l: String, private val m: AttributeMapper) :
AttributeConstraint<Nothing>(l, m) {
override fun isSatisfied(attribute: Any?): Boolean = attribute != null
}
}

Expand All @@ -65,74 +156,81 @@ sealed interface ValidationLevel<out T> {
* Android attestation certificate.
*/
@Immutable
sealed class SecurityLevelValidationLevel : ValidationLevel<KeyDescription> {
@RequiresApi(24)
fun areSecurityLevelsMatching(keyDescription: KeyDescription): Boolean {
return keyDescription.attestationSecurityLevel == keyDescription.keyMintSecurityLevel
@RequiresApi(24)
sealed class SecurityLevelConstraint(val isSatisfied: (KeyDescription) -> Boolean) : Constraint {
companion object {
const val LABEL = "Security level"
}

override val label = LABEL

override fun check(description: KeyDescription) =
if (isSatisfied(description)) {
Constraint.Satisfied
} else {
Constraint.Violated(getFailureMessage(description))
}

fun getFailureMessage(description: KeyDescription): String =
"Security level violates constraint: " +
"keyMintSecurityLevel=${description.keyMintSecurityLevel}, " +
"attestationSecurityLevel=${description.attestationSecurityLevel}, " +
"config=$this"

/**
* Checks that both the attestationSecurityLevel and keyMintSecurityLevel match the expected
* value.
*
* @param expectedVal The expected value of the security level.
*/
@Immutable
data class STRICT(val expectedVal: SecurityLevel) : SecurityLevelValidationLevel() {
@RequiresApi(24)
override fun isSatisfiedBy(attribute: Any?): Boolean {
val keyDescription = attribute as? KeyDescription ?: return false
val securityLevelIsExpected = keyDescription.attestationSecurityLevel == this.expectedVal
return areSecurityLevelsMatching(keyDescription) && securityLevelIsExpected
}
}
data class STRICT(val expectedVal: SecurityLevel) :
SecurityLevelConstraint({
it.keyMintSecurityLevel == expectedVal && it.attestationSecurityLevel == expectedVal
})

/**
* Checks that the attestationSecurityLevel is equal to the keyMintSecurityLevel, and that this
* security level is not [SecurityLevel.SOFTWARE].
*/
@Immutable
data object NOT_SOFTWARE : SecurityLevelValidationLevel() {
@RequiresApi(24)
override fun isSatisfiedBy(attribute: Any?): Boolean {
val keyDescription = attribute as? KeyDescription ?: return false
val securityLevelIsSoftware =
keyDescription.attestationSecurityLevel == SecurityLevel.SOFTWARE
return areSecurityLevelsMatching(keyDescription) && !securityLevelIsSoftware
}
}
data object NOT_SOFTWARE :
SecurityLevelConstraint({
it.keyMintSecurityLevel == it.attestationSecurityLevel &&
it.attestationSecurityLevel != SecurityLevel.SOFTWARE
})

/**
* Checks that the attestationSecurityLevel is equal to the keyMintSecurityLevel, regardless of
* security level.
*/
@Immutable
data object CONSISTENT : SecurityLevelValidationLevel() {
@RequiresApi(24)
override fun isSatisfiedBy(attribute: Any?): Boolean {
val keyDescription = attribute as? KeyDescription ?: return false
return areSecurityLevelsMatching(keyDescription)
}
}
data object CONSISTENT :
SecurityLevelConstraint({ it.attestationSecurityLevel == it.keyMintSecurityLevel })
}

/**
* Configuration for validating the ordering of the attributes in the AuthorizationList sequence in
* an Android attestation certificate.
*/
@Immutable
sealed interface TagOrderValidationLevel : ValidationLevel<KeyDescription> {
@RequiresApi(24)
sealed class TagOrderConstraint : Constraint {
override val label = "Tag order"

/**
* Checks that the attributes in the AuthorizationList sequence appear in the order specified by
* https://source.android.com/docs/security/features/keystore/attestation#schema.
*/
@Immutable
data object STRICT : TagOrderValidationLevel {
@RequiresApi(24)
override fun isSatisfiedBy(attribute: Any?): Boolean {
val keyDescription = attribute as? KeyDescription ?: return false
return keyDescription.softwareEnforced.areTagsOrdered &&
keyDescription.hardwareEnforced.areTagsOrdered
}
data object STRICT : TagOrderConstraint() {
override fun check(description: KeyDescription) =
if (
description.softwareEnforced.areTagsOrdered && description.hardwareEnforced.areTagsOrdered
) {
Constraint.Satisfied
} else {
Constraint.Violated("Authorization list tags must be in ascending order")
}
}
}
18 changes: 2 additions & 16 deletions src/main/kotlin/KeyAttestationReason.kt
Original file line number Diff line number Diff line change
Expand Up @@ -33,22 +33,8 @@ enum class KeyAttestationReason : CertPathValidatorException.Reason {
// extension. This likely indicates that an attacker is trying to manipulate the key and
// device properties.
CHAIN_EXTENDED_WITH_FAKE_ATTESTATION_EXTENSION,
// The origin violated the constraint provided in [ConstraintConfig].
// Using the default config, this means the key was not generated, so the verifier cannot know
// that the key has always been in the secure environment.
KEY_ORIGIN_CONSTRAINT_VIOLATION,
// The security level violated the constraint provided in [ConstraintConfig].
// Using the default config, this means the attestation and the KeyMint security levels do not
// match, which likely indicates that the attestation was generated in software and so cannot be
// trusted.
SECURITY_LEVEL_CONSTRAINT_VIOLATION,
// The root of trust violated the constraint provided in [ConstraintConfig].
// Using the default config, this means the key description is missing the root of trust, and an
// Android key attestation chain without a root of trust is malformed.
ROOT_OF_TRUST_CONSTRAINT_VIOLATION,
// The authorization list ordering violated the constraint provided in
// [ConstraintConfig].
AUTHORIZATION_LIST_ORDERING_CONSTRAINT_VIOLATION,
// One of the constraints provided to the verifier was violated.
CONSTRAINT_VIOLATION,
// There was an error parsing the key description and an unknown tag number was encountered.
UNKNOWN_TAG_NUMBER,
}
39 changes: 10 additions & 29 deletions src/main/kotlin/Verifier.kt
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ sealed interface VerificationResult {

data class ExtensionParsingFailure(val cause: ExtensionParsingException) : VerificationResult

data class ConstraintViolation(val cause: String, val reason: KeyAttestationReason) :
data class ConstraintViolation(val constraintLabel: String, val cause: String) :
VerificationResult

data object SoftwareAttestationUnsupported : VerificationResult
Expand Down Expand Up @@ -291,40 +291,21 @@ constructor(
}
}

val origin = keyDescription.hardwareEnforced.origin
if (!constraintConfig.keyOrigin.isSatisfiedBy(origin)) {
return VerificationResult.ConstraintViolation(
"Origin violates constraint: value=${origin}, config=${constraintConfig.keyOrigin}",
KeyAttestationReason.KEY_ORIGIN_CONSTRAINT_VIOLATION,
)
for (constraint in constraintConfig.getConstraints()) {
val result = constraint.check(keyDescription)
when (result) {
is Constraint.Satisfied -> {}
is Constraint.Violated -> {
return VerificationResult.ConstraintViolation(constraint.label, result.failureMessage)
}
}
}

val securityLevel =
if (constraintConfig.securityLevel.isSatisfiedBy(keyDescription)) {
minOf(keyDescription.attestationSecurityLevel, keyDescription.keyMintSecurityLevel)
} else {
return VerificationResult.ConstraintViolation(
"Security level violates constraint: value=${keyDescription.attestationSecurityLevel}, config=${constraintConfig.securityLevel}",
KeyAttestationReason.SECURITY_LEVEL_CONSTRAINT_VIOLATION,
)
}

minOf(keyDescription.attestationSecurityLevel, keyDescription.keyMintSecurityLevel)
val rootOfTrust = keyDescription.hardwareEnforced.rootOfTrust
if (!constraintConfig.rootOfTrust.isSatisfiedBy(rootOfTrust)) {
return VerificationResult.ConstraintViolation(
"Root of trust violates constraint: value=${rootOfTrust}, config=${constraintConfig.rootOfTrust}",
KeyAttestationReason.ROOT_OF_TRUST_CONSTRAINT_VIOLATION,
)
}
val verifiedBootState = rootOfTrust?.verifiedBootState ?: VerifiedBootState.UNVERIFIED

if (!constraintConfig.authorizationListTagOrder.isSatisfiedBy(keyDescription)) {
return VerificationResult.ConstraintViolation(
"Authorization list ordering violates constraint: config=${constraintConfig.authorizationListTagOrder}",
KeyAttestationReason.AUTHORIZATION_LIST_ORDERING_CONSTRAINT_VIOLATION,
)
}

return VerificationResult.Success(
pathValidationResult.publicKey,
keyDescription.attestationChallenge,
Expand Down
Loading
Loading