Skip to content
Merged
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
69 changes: 54 additions & 15 deletions src/main/kotlin/ConstraintConfig.kt
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
package com.android.keyattestation.verifier

import androidx.annotation.RequiresApi
import com.android.keyattestation.verifier.provider.KeyAttestationCertPath
import com.google.common.collect.ImmutableList
import com.google.errorprone.annotations.Immutable
import com.google.errorprone.annotations.ThreadSafe
Expand All @@ -36,7 +37,7 @@ sealed interface Constraint {
val label: String

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

/**
Expand Down Expand Up @@ -113,15 +114,16 @@ fun constraintConfig(init: ConstraintConfigBuilder.() -> Unit): ConstraintConfig
data object IgnoredConstraint : Constraint {
override val label = "Ignored"

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

/** Constraint that checks a single attribute of the [KeyDescription]. */
@Immutable(containerOf = ["T"])
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) =
override fun check(description: KeyDescription, certPath: KeyAttestationCertPath) =
if (isSatisfied(mapper?.invoke(description))) {
Constraint.Satisfied
} else {
Expand Down Expand Up @@ -157,21 +159,26 @@ sealed class AttributeConstraint<out T>(override val label: String, val mapper:
*/
@Immutable
@RequiresApi(24)
sealed class SecurityLevelConstraint(val isSatisfied: (KeyDescription) -> Boolean) : Constraint {
sealed class SecurityLevelConstraint(
val isSatisfied: (KeyDescription, KeyAttestationCertPath) -> Boolean
) : Constraint {
companion object {
const val LABEL = "Security level"
}

override val label = LABEL

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

fun getFailureMessage(description: KeyDescription): String =
open fun getFailureMessage(
description: KeyDescription,
certPath: KeyAttestationCertPath,
): String =
"Security level violates constraint: " +
"keyMintSecurityLevel=${description.keyMintSecurityLevel}, " +
"attestationSecurityLevel=${description.attestationSecurityLevel}, " +
Expand All @@ -185,8 +192,8 @@ sealed class SecurityLevelConstraint(val isSatisfied: (KeyDescription) -> Boolea
*/
@Immutable
data class STRICT(val expectedVal: SecurityLevel) :
SecurityLevelConstraint({
it.keyMintSecurityLevel == expectedVal && it.attestationSecurityLevel == expectedVal
SecurityLevelConstraint({ desc, _ ->
desc.keyMintSecurityLevel == expectedVal && desc.attestationSecurityLevel == expectedVal
})

/**
Expand All @@ -195,9 +202,9 @@ sealed class SecurityLevelConstraint(val isSatisfied: (KeyDescription) -> Boolea
*/
@Immutable
data object NOT_SOFTWARE :
SecurityLevelConstraint({
it.keyMintSecurityLevel == it.attestationSecurityLevel &&
it.attestationSecurityLevel != SecurityLevel.SOFTWARE
SecurityLevelConstraint({ desc, _ ->
desc.keyMintSecurityLevel == desc.attestationSecurityLevel &&
desc.attestationSecurityLevel != SecurityLevel.SOFTWARE
})

/**
Expand All @@ -206,7 +213,39 @@ sealed class SecurityLevelConstraint(val isSatisfied: (KeyDescription) -> Boolea
*/
@Immutable
data object CONSISTENT :
SecurityLevelConstraint({ it.attestationSecurityLevel == it.keyMintSecurityLevel })
SecurityLevelConstraint({ desc, _ ->
desc.attestationSecurityLevel == desc.keyMintSecurityLevel
})

/**
* Checks that the keyMintSecurityLevel matches the security level claimed by the certificate.
* this constraint may be used in conjunction with other security level constraints. e.g. it may
* be combined with [STRICT] to verify that the keyMintSecurityLevel is precisely
* [SecurityLevel.STRONG_BOX] and that the security level matches the value claimed by the
* Google-signed certificate.
*/
@Immutable
data object MATCHES_CERTIFICATE :
SecurityLevelConstraint({ desc, certPath ->
when (desc.keyMintSecurityLevel) {
SecurityLevel.SOFTWARE ->
certPath.securityLevel() == null || certPath.securityLevel() == SecurityLevel.SOFTWARE
// Older cert chains do not make a TEE security level claim, so allow null. StrongBox certs
// always explicitly claim the StrongBox security level, so there's no risk of a TEE cert
// chain claiming to be StrongBox.
SecurityLevel.TRUSTED_ENVIRONMENT ->
certPath.securityLevel() == null ||
certPath.securityLevel() == SecurityLevel.TRUSTED_ENVIRONMENT
SecurityLevel.STRONG_BOX -> certPath.securityLevel() == SecurityLevel.STRONG_BOX
}
}) {
override fun getFailureMessage(
description: KeyDescription,
certPath: KeyAttestationCertPath,
): String =
"Security level of KeyMint (${description.keyMintSecurityLevel}) does not match " +
"attestation certificate (${certPath.securityLevel()})"
}
}

/**
Expand All @@ -224,7 +263,7 @@ sealed class TagOrderConstraint : Constraint {
*/
@Immutable
data object STRICT : TagOrderConstraint() {
override fun check(description: KeyDescription) =
override fun check(description: KeyDescription, certPath: KeyAttestationCertPath) =
if (
description.softwareEnforced.areTagsOrdered && description.hardwareEnforced.areTagsOrdered
) {
Expand Down
3 changes: 2 additions & 1 deletion src/main/kotlin/Verifier.kt
Original file line number Diff line number Diff line change
Expand Up @@ -303,7 +303,7 @@ constructor(
}

for (constraint in constraintConfig.getConstraints()) {
val result = constraint.check(keyDescription)
val result = constraint.check(keyDescription, certPath)
when (result) {
is Constraint.Satisfied -> {}
is Constraint.Violated -> {
Expand All @@ -314,6 +314,7 @@ constructor(

val securityLevel =
minOf(keyDescription.attestationSecurityLevel, keyDescription.keyMintSecurityLevel)

val rootOfTrust = keyDescription.hardwareEnforced.rootOfTrust
val verifiedBootState = rootOfTrust?.verifiedBootState ?: VerifiedBootState.UNVERIFIED
val deviceLocked = rootOfTrust?.deviceLocked ?: false
Expand Down
22 changes: 18 additions & 4 deletions src/main/kotlin/provider/KeyAttestationCertPath.kt
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import java.security.cert.X509Certificate
import java.security.interfaces.DSAPublicKey
import java.security.interfaces.ECPublicKey
import java.security.interfaces.RSAPublicKey
import java.util.Locale
import javax.crypto.interfaces.DHPublicKey
import javax.security.auth.x500.X500Principal

Expand Down Expand Up @@ -103,14 +104,27 @@ class KeyAttestationCertPath(certs: List<X509Certificate>) : CertPath("X.509") {
*/
fun securityLevel() =
when (provisioningMethod()) {
ProvisioningMethod.FACTORY_PROVISIONED ->
parseDN(intermediateCert().subjectX500Principal.getName(X500Principal.RFC1779))[TITLE_OID]
.toSecurityLevel()
ProvisioningMethod.UNKNOWN -> null
ProvisioningMethod.REMOTELY_PROVISIONED ->
parseDN(attestationCert().subjectX500Principal.getName(X500Principal.RFC1779))["O"]
.toSecurityLevel()
else -> SecurityLevel.SOFTWARE
ProvisioningMethod.FACTORY_PROVISIONED -> securityLevelFromFactoryChain()
}

private fun securityLevelFromFactoryChain(): SecurityLevel? {
check(isFactoryProvisioned()) { "securityLevelFromFactoryChain() called on non-factory chain" }

// Only StrongBox factory chains are guaranteed to encode the security level. The logic here is
// based on the Compatibility Test Suite (CTS) verification logic for StrongBox certs:
// https://cs.android.com/android/platform/superproject/+/android-latest-release:cts/tests/tests/keystore/src/android/keystore/cts/KeyAttestationTest.java;drc=0ef830e0aa7c51200c1e4c7a24169fede350f1ba;l=2430
for (cert in arrayOf(attestationCert(), intermediateCert())) {
val name = cert.subjectX500Principal.getName(X500Principal.RFC1779)
if (name.lowercase(Locale.ROOT).contains("strongbox")) {
return SecurityLevel.STRONG_BOX
}
}
return null
}

/**
* Returns the leaf certificate from the certificate chain.
Expand Down
20 changes: 20 additions & 0 deletions src/main/kotlin/testing/Certs.kt
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,26 @@ object CertLists {
)
}

/* A chain with TEE in the last intermediate certificate but keymint attributes claiming StrongBox. */
val teeIntermediateWithStrongBoxKeyMintAttributes by lazy {
listOf(
certFactory.generateLeafCert(extension = certFactory.STRONG_BOX_KEY_DESCRIPTION_EXT),
Certs.factoryAttestation,
Certs.factoryIntermediate,
Certs.root,
)
}

/* A chain with StrongBox in the last intermediate certificate but keymint attributes claiming TEE. */
val strongBoxIntermediateWithTeeKeyMintAttributes by lazy {
listOf(
certFactory.generateLeafCert(),
certFactory.generateAttestationCert(issuer = certFactory.strongBoxIntermediate.subject),
certFactory.strongBoxIntermediate,
Certs.root,
)
}

/* A valid remotely provisioned chain for TEE keys. */
val validRemotelyProvisioned by lazy {
val rkpAttestationCert =
Expand Down
63 changes: 43 additions & 20 deletions src/test/kotlin/ConstraintConfigTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -64,15 +64,19 @@ class ConstraintConfigTest {
val kd =
keyDescriptionWithSoftwareSecurityLevels.copy(uniqueId = ByteString.copyFromUtf8("foo"))

assertIs<Constraint.Satisfied>(level.check(kd))
assertIs<Constraint.Violated>(level.check(kd.copy(uniqueId = ByteString.copyFromUtf8("bar"))))
assertIs<Constraint.Satisfied>(level.check(kd, testCertPath))
assertIs<Constraint.Violated>(
level.check(kd.copy(uniqueId = ByteString.copyFromUtf8("bar")), testCertPath)
)
}

@Test
fun AttributeConstraintIsSatisfied_notNull_allowsAnyValue() {
val level = AttributeConstraint.NOT_NULL("Root of trust") { it.hardwareEnforced.rootOfTrust }

assertIs<Constraint.Violated>(level.check(keyDescriptionWithSoftwareSecurityLevels))
assertIs<Constraint.Violated>(
level.check(keyDescriptionWithSoftwareSecurityLevels, testCertPath)
)

val kdWithRot =
keyDescriptionWithSoftwareSecurityLevels.copy(
Expand All @@ -81,48 +85,66 @@ class ConstraintConfigTest {
rootOfTrust = RootOfTrust(ByteString.empty(), false, VerifiedBootState.VERIFIED)
)
)
assertIs<Constraint.Satisfied>(level.check(kdWithRot))
assertIs<Constraint.Satisfied>(level.check(kdWithRot, testCertPath))
}

@Test
fun SecurityLevelConstraintIsSatisfied_strictWithExpectedValue() {
val level = SecurityLevelConstraint.STRICT(SecurityLevel.STRONG_BOX)

assertIs<Constraint.Satisfied>(level.check(keyDescriptionWithStrongBoxSecurityLevels))
assertIs<Constraint.Violated>(level.check(keyDescriptionWithTeeSecurityLevels))
assertIs<Constraint.Violated>(level.check(keyDescriptionWithMismatchedSecurityLevels))
assertIs<Constraint.Satisfied>(
level.check(keyDescriptionWithStrongBoxSecurityLevels, testCertPath)
)
assertIs<Constraint.Violated>(level.check(keyDescriptionWithTeeSecurityLevels, testCertPath))
assertIs<Constraint.Violated>(
level.check(keyDescriptionWithMismatchedSecurityLevels, testCertPath)
)
}

@Test
fun SecurityLevelConstraintIsSatisfied_notSoftware_allowsAnyNonSoftwareMatchingLevels() {
val level = SecurityLevelConstraint.NOT_SOFTWARE

assertIs<Constraint.Satisfied>(level.check(keyDescriptionWithStrongBoxSecurityLevels))
assertIs<Constraint.Satisfied>(level.check(keyDescriptionWithTeeSecurityLevels))
assertIs<Constraint.Violated>(level.check(keyDescriptionWithSoftwareSecurityLevels))
assertIs<Constraint.Violated>(level.check(keyDescriptionWithMismatchedSecurityLevels))
assertIs<Constraint.Satisfied>(
level.check(keyDescriptionWithStrongBoxSecurityLevels, testCertPath)
)
assertIs<Constraint.Satisfied>(level.check(keyDescriptionWithTeeSecurityLevels, testCertPath))
assertIs<Constraint.Violated>(
level.check(keyDescriptionWithSoftwareSecurityLevels, testCertPath)
)
assertIs<Constraint.Violated>(
level.check(keyDescriptionWithMismatchedSecurityLevels, testCertPath)
)
}

@Test
fun SecurityLevelConstraintIsSatisfied_consistent_allowsAnyMatchingLevels() {
val level = SecurityLevelConstraint.CONSISTENT

assertIs<Constraint.Satisfied>(level.check(keyDescriptionWithStrongBoxSecurityLevels))
assertIs<Constraint.Satisfied>(level.check(keyDescriptionWithTeeSecurityLevels))
assertIs<Constraint.Satisfied>(level.check(keyDescriptionWithSoftwareSecurityLevels))
assertIs<Constraint.Violated>(level.check(keyDescriptionWithMismatchedSecurityLevels))
assertIs<Constraint.Satisfied>(
level.check(keyDescriptionWithStrongBoxSecurityLevels, testCertPath)
)
assertIs<Constraint.Satisfied>(level.check(keyDescriptionWithTeeSecurityLevels, testCertPath))
assertIs<Constraint.Satisfied>(
level.check(keyDescriptionWithSoftwareSecurityLevels, testCertPath)
)
assertIs<Constraint.Violated>(
level.check(keyDescriptionWithMismatchedSecurityLevels, testCertPath)
)
}

@Test
fun AuthorizationListOrderingIsSatisfied_strictWithUnorderedTags_fails() {
val ordering = TagOrderConstraint.STRICT

assertIs<Constraint.Satisfied>(ordering.check(keyDescriptionWithStrongBoxSecurityLevels))
assertIs<Constraint.Satisfied>(
ordering.check(keyDescriptionWithStrongBoxSecurityLevels, testCertPath)
)

val kdUnordered =
KeyDescription.parseFrom(readCertPath("invalid/tags_not_in_ascending_order.pem").leafCert())!!

assertIs<Constraint.Violated>(ordering.check(kdUnordered))
assertIs<Constraint.Violated>(ordering.check(kdUnordered, testCertPath))
}

@Test
Expand All @@ -131,7 +153,7 @@ class ConstraintConfigTest {
val kd =
keyDescriptionWithSoftwareSecurityLevels.copy(uniqueId = ByteString.copyFromUtf8("bar"))

val violation = assertIs<Constraint.Violated>(level.check(kd))
val violation = assertIs<Constraint.Violated>(level.check(kd, testCertPath))
assertThat(violation.failureMessage)
.isEqualTo("Unique ID violates constraint: value=bar, config=$level")
}
Expand All @@ -140,7 +162,8 @@ class ConstraintConfigTest {
fun securityLevelConstraint_withViolation_returnsCorrectMessage() {
val level = SecurityLevelConstraint.STRICT(SecurityLevel.STRONG_BOX)

val violation = assertIs<Constraint.Violated>(level.check(keyDescriptionWithTeeSecurityLevels))
val violation =
assertIs<Constraint.Violated>(level.check(keyDescriptionWithTeeSecurityLevels, testCertPath))
assertThat(violation.failureMessage)
.isEqualTo(
"Security level violates constraint: keyMintSecurityLevel=TRUSTED_ENVIRONMENT, " +
Expand All @@ -154,7 +177,7 @@ class ConstraintConfigTest {
val kdUnordered =
KeyDescription.parseFrom(readCertPath("invalid/tags_not_in_ascending_order.pem").leafCert())!!

val violation = assertIs<Constraint.Violated>(level.check(kdUnordered))
val violation = assertIs<Constraint.Violated>(level.check(kdUnordered, testCertPath))
assertThat(violation.failureMessage)
.isEqualTo("Authorization list tags must be in ascending order")
}
Expand Down
Loading
Loading