diff --git a/src/main/kotlin/ConstraintConfig.kt b/src/main/kotlin/ConstraintConfig.kt index bf0c8c8..8e4565f 100644 --- a/src/main/kotlin/ConstraintConfig.kt +++ b/src/main/kotlin/ConstraintConfig.kt @@ -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 @@ -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 } /** @@ -113,7 +114,8 @@ 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]. */ @@ -121,7 +123,7 @@ data object IgnoredConstraint : Constraint { sealed class AttributeConstraint(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 { @@ -157,21 +159,26 @@ sealed class AttributeConstraint(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}, " + @@ -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 }) /** @@ -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 }) /** @@ -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()})" + } } /** @@ -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 ) { diff --git a/src/main/kotlin/Verifier.kt b/src/main/kotlin/Verifier.kt index f3ae21e..fc69182 100644 --- a/src/main/kotlin/Verifier.kt +++ b/src/main/kotlin/Verifier.kt @@ -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 -> { @@ -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 diff --git a/src/main/kotlin/provider/KeyAttestationCertPath.kt b/src/main/kotlin/provider/KeyAttestationCertPath.kt index cb18a30..257eb36 100644 --- a/src/main/kotlin/provider/KeyAttestationCertPath.kt +++ b/src/main/kotlin/provider/KeyAttestationCertPath.kt @@ -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 @@ -103,14 +104,27 @@ class KeyAttestationCertPath(certs: List) : 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. diff --git a/src/main/kotlin/testing/Certs.kt b/src/main/kotlin/testing/Certs.kt index bef942c..2f6d917 100644 --- a/src/main/kotlin/testing/Certs.kt +++ b/src/main/kotlin/testing/Certs.kt @@ -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 = diff --git a/src/test/kotlin/ConstraintConfigTest.kt b/src/test/kotlin/ConstraintConfigTest.kt index 05b9e6e..638aa50 100644 --- a/src/test/kotlin/ConstraintConfigTest.kt +++ b/src/test/kotlin/ConstraintConfigTest.kt @@ -64,15 +64,19 @@ class ConstraintConfigTest { val kd = keyDescriptionWithSoftwareSecurityLevels.copy(uniqueId = ByteString.copyFromUtf8("foo")) - assertIs(level.check(kd)) - assertIs(level.check(kd.copy(uniqueId = ByteString.copyFromUtf8("bar")))) + assertIs(level.check(kd, testCertPath)) + assertIs( + 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(level.check(keyDescriptionWithSoftwareSecurityLevels)) + assertIs( + level.check(keyDescriptionWithSoftwareSecurityLevels, testCertPath) + ) val kdWithRot = keyDescriptionWithSoftwareSecurityLevels.copy( @@ -81,48 +85,66 @@ class ConstraintConfigTest { rootOfTrust = RootOfTrust(ByteString.empty(), false, VerifiedBootState.VERIFIED) ) ) - assertIs(level.check(kdWithRot)) + assertIs(level.check(kdWithRot, testCertPath)) } @Test fun SecurityLevelConstraintIsSatisfied_strictWithExpectedValue() { val level = SecurityLevelConstraint.STRICT(SecurityLevel.STRONG_BOX) - assertIs(level.check(keyDescriptionWithStrongBoxSecurityLevels)) - assertIs(level.check(keyDescriptionWithTeeSecurityLevels)) - assertIs(level.check(keyDescriptionWithMismatchedSecurityLevels)) + assertIs( + level.check(keyDescriptionWithStrongBoxSecurityLevels, testCertPath) + ) + assertIs(level.check(keyDescriptionWithTeeSecurityLevels, testCertPath)) + assertIs( + level.check(keyDescriptionWithMismatchedSecurityLevels, testCertPath) + ) } @Test fun SecurityLevelConstraintIsSatisfied_notSoftware_allowsAnyNonSoftwareMatchingLevels() { val level = SecurityLevelConstraint.NOT_SOFTWARE - assertIs(level.check(keyDescriptionWithStrongBoxSecurityLevels)) - assertIs(level.check(keyDescriptionWithTeeSecurityLevels)) - assertIs(level.check(keyDescriptionWithSoftwareSecurityLevels)) - assertIs(level.check(keyDescriptionWithMismatchedSecurityLevels)) + assertIs( + level.check(keyDescriptionWithStrongBoxSecurityLevels, testCertPath) + ) + assertIs(level.check(keyDescriptionWithTeeSecurityLevels, testCertPath)) + assertIs( + level.check(keyDescriptionWithSoftwareSecurityLevels, testCertPath) + ) + assertIs( + level.check(keyDescriptionWithMismatchedSecurityLevels, testCertPath) + ) } @Test fun SecurityLevelConstraintIsSatisfied_consistent_allowsAnyMatchingLevels() { val level = SecurityLevelConstraint.CONSISTENT - assertIs(level.check(keyDescriptionWithStrongBoxSecurityLevels)) - assertIs(level.check(keyDescriptionWithTeeSecurityLevels)) - assertIs(level.check(keyDescriptionWithSoftwareSecurityLevels)) - assertIs(level.check(keyDescriptionWithMismatchedSecurityLevels)) + assertIs( + level.check(keyDescriptionWithStrongBoxSecurityLevels, testCertPath) + ) + assertIs(level.check(keyDescriptionWithTeeSecurityLevels, testCertPath)) + assertIs( + level.check(keyDescriptionWithSoftwareSecurityLevels, testCertPath) + ) + assertIs( + level.check(keyDescriptionWithMismatchedSecurityLevels, testCertPath) + ) } @Test fun AuthorizationListOrderingIsSatisfied_strictWithUnorderedTags_fails() { val ordering = TagOrderConstraint.STRICT - assertIs(ordering.check(keyDescriptionWithStrongBoxSecurityLevels)) + assertIs( + ordering.check(keyDescriptionWithStrongBoxSecurityLevels, testCertPath) + ) val kdUnordered = KeyDescription.parseFrom(readCertPath("invalid/tags_not_in_ascending_order.pem").leafCert())!! - assertIs(ordering.check(kdUnordered)) + assertIs(ordering.check(kdUnordered, testCertPath)) } @Test @@ -131,7 +153,7 @@ class ConstraintConfigTest { val kd = keyDescriptionWithSoftwareSecurityLevels.copy(uniqueId = ByteString.copyFromUtf8("bar")) - val violation = assertIs(level.check(kd)) + val violation = assertIs(level.check(kd, testCertPath)) assertThat(violation.failureMessage) .isEqualTo("Unique ID violates constraint: value=bar, config=$level") } @@ -140,7 +162,8 @@ class ConstraintConfigTest { fun securityLevelConstraint_withViolation_returnsCorrectMessage() { val level = SecurityLevelConstraint.STRICT(SecurityLevel.STRONG_BOX) - val violation = assertIs(level.check(keyDescriptionWithTeeSecurityLevels)) + val violation = + assertIs(level.check(keyDescriptionWithTeeSecurityLevels, testCertPath)) assertThat(violation.failureMessage) .isEqualTo( "Security level violates constraint: keyMintSecurityLevel=TRUSTED_ENVIRONMENT, " + @@ -154,7 +177,7 @@ class ConstraintConfigTest { val kdUnordered = KeyDescription.parseFrom(readCertPath("invalid/tags_not_in_ascending_order.pem").leafCert())!! - val violation = assertIs(level.check(kdUnordered)) + val violation = assertIs(level.check(kdUnordered, testCertPath)) assertThat(violation.failureMessage) .isEqualTo("Authorization list tags must be in ascending order") } diff --git a/src/test/kotlin/VerifierTest.kt b/src/test/kotlin/VerifierTest.kt index 41f72e9..379c7ab 100644 --- a/src/test/kotlin/VerifierTest.kt +++ b/src/test/kotlin/VerifierTest.kt @@ -216,6 +216,47 @@ class VerifierTest { assertThat(result.cause).contains("Security level violates constraint") } + @Test + fun teeIntermediateWithStrongBoxKeyMintAttributes_throwsConstraintViolation() { + val verifier = + Verifier( + { prodAnchors + TrustAnchor(Certs.root, null) }, + { setOf() }, + { FakeCalendar.DEFAULT.now() }, + constraintConfig { additionalConstraint { SecurityLevelConstraint.MATCHES_CERTIFICATE } }, + ) + val result = + assertIs( + verifier.verify(CertLists.teeIntermediateWithStrongBoxKeyMintAttributes) + ) + assertThat(result.constraintLabel).isEqualTo("Security level") + assertThat(result.cause) + .contains( + "Security level of KeyMint (STRONG_BOX) does not match attestation certificate " + "(null)" + ) + } + + @Test + fun strongBoxIntermediateWithTeeKeyMintAttributes_throwsConstraintViolation() { + val verifier = + Verifier( + { prodAnchors + TrustAnchor(Certs.root, null) }, + { setOf() }, + { FakeCalendar.DEFAULT.now() }, + constraintConfig { additionalConstraint { SecurityLevelConstraint.MATCHES_CERTIFICATE } }, + ) + val result = + assertIs( + verifier.verify(CertLists.strongBoxIntermediateWithTeeKeyMintAttributes) + ) + assertThat(result.constraintLabel).isEqualTo("Security level") + assertThat(result.cause) + .contains( + "Security level of KeyMint (TRUSTED_ENVIRONMENT) does not match attestation certificate " + + "(STRONG_BOX)" + ) + } + @Test fun mismatchedSecurityLevels_customConfig_succeeds() { val verifier = diff --git a/src/test/kotlin/provider/KeyAttestationCertPathTest.kt b/src/test/kotlin/provider/KeyAttestationCertPathTest.kt index bfc99fc..374c880 100644 --- a/src/test/kotlin/provider/KeyAttestationCertPathTest.kt +++ b/src/test/kotlin/provider/KeyAttestationCertPathTest.kt @@ -133,11 +133,11 @@ class KeyAttestationCertPathTest { assertThat(certPath.securityLevel()).isEqualTo(testCase.expected) } - enum class SecurityLevelTestCase(val path: String, val expected: SecurityLevel) { - FACTORY_PROVISIONED("blueline/sdk28/TEE_EC_NONE", SecurityLevel.TRUSTED_ENVIRONMENT), + enum class SecurityLevelTestCase(val path: String, val expected: SecurityLevel?) { + FACTORY_PROVISIONED("blueline/sdk28/TEE_EC_NONE", null), STRONG_BOX_FACTORY_PROVISIONED("blueline/sdk28/SB_RSA_NONE", SecurityLevel.STRONG_BOX), REMOTELY_PROVISIONED("caiman/sdk36/TEE_EC_RKP", SecurityLevel.TRUSTED_ENVIRONMENT), STRONG_BOX_REMOTELY_PROVISIONED("caiman/sdk36/SB_EC_RKP", SecurityLevel.STRONG_BOX), - SOFTWARE("marlin/sdk29/TEE_EC_NONE", SecurityLevel.SOFTWARE), + SOFTWARE("marlin/sdk29/TEE_EC_NONE", null), } }