diff --git a/src/main/kotlin/ConstraintConfig.kt b/src/main/kotlin/ConstraintConfig.kt index b01bcd6..bf0c8c8 100644 --- a/src/main/kotlin/ConstraintConfig.kt +++ b/src/main/kotlin/ConstraintConfig.kt @@ -17,26 +17,121 @@ 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 = ValidationLevel.STRICT(Origin.GENERATED), - val securityLevel: ValidationLevel = SecurityLevelValidationLevel.NOT_SOFTWARE, - val rootOfTrust: ValidationLevel = ValidationLevel.NOT_NULL, - val authorizationListTagOrder: ValidationLevel = 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 = ImmutableList.of(), +) { + @RequiresApi(24) + fun getConstraints() = + ImmutableList.builder() + .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 = 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 { - /** Evaluates whether the [attribute] is satisfied by this [ValidationLevel]. */ - fun isSatisfiedBy(attribute: Any?): Boolean +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) = + 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. @@ -44,19 +139,15 @@ sealed interface ValidationLevel { * @param expectedVal The expected value of the attribute. */ @Immutable(containerOf = ["T"]) - data class STRICT(val expectedVal: T) : ValidationLevel { - override fun isSatisfiedBy(attribute: Any?): Boolean = attribute == expectedVal + data class STRICT(val l: String, val expectedVal: T, private val m: AttributeMapper) : + AttributeConstraint(l, m) { + override fun isSatisfied(attribute: Any?): Boolean = attribute == expectedVal } /* Check that the attribute exists. */ - @Immutable - data object NOT_NULL : ValidationLevel { - override fun isSatisfiedBy(attribute: Any?): Boolean = attribute != null - } - - @Immutable - data object IGNORE : ValidationLevel { - override fun isSatisfiedBy(attribute: Any?): Boolean = true + data class NOT_NULL(val l: String, private val m: AttributeMapper) : + AttributeConstraint(l, m) { + override fun isSatisfied(attribute: Any?): Boolean = attribute != null } } @@ -65,12 +156,27 @@ sealed interface ValidationLevel { * Android attestation certificate. */ @Immutable -sealed class SecurityLevelValidationLevel : ValidationLevel { - @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. @@ -78,42 +184,29 @@ sealed class SecurityLevelValidationLevel : ValidationLevel { * @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 }) } /** @@ -121,18 +214,23 @@ sealed class SecurityLevelValidationLevel : ValidationLevel { * an Android attestation certificate. */ @Immutable -sealed interface TagOrderValidationLevel : ValidationLevel { +@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") + } } } diff --git a/src/main/kotlin/KeyAttestationReason.kt b/src/main/kotlin/KeyAttestationReason.kt index 9da4e3c..b7b3f31 100644 --- a/src/main/kotlin/KeyAttestationReason.kt +++ b/src/main/kotlin/KeyAttestationReason.kt @@ -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, } diff --git a/src/main/kotlin/Verifier.kt b/src/main/kotlin/Verifier.kt index 63439b0..8b83a49 100644 --- a/src/main/kotlin/Verifier.kt +++ b/src/main/kotlin/Verifier.kt @@ -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 @@ -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, diff --git a/src/test/kotlin/ConstraintConfigTest.kt b/src/test/kotlin/ConstraintConfigTest.kt index fefda2b..05b9e6e 100644 --- a/src/test/kotlin/ConstraintConfigTest.kt +++ b/src/test/kotlin/ConstraintConfigTest.kt @@ -19,6 +19,7 @@ package com.android.keyattestation.verifier import com.android.keyattestation.verifier.testing.TestUtils.readCertPath import com.google.common.truth.Truth.assertThat import com.google.protobuf.ByteString +import kotlin.test.assertIs import org.junit.Test import org.junit.runner.RunWith import org.junit.runners.JUnit4 @@ -55,72 +56,106 @@ class ConstraintConfigTest { val keyDescriptionWithMismatchedSecurityLevels = createTestKeyDescription(SecurityLevel.STRONG_BOX, SecurityLevel.TRUSTED_ENVIRONMENT) + private val testCertPath = readCertPath("akita/sdk34/TEE_EC_NONE.pem") + @Test - fun ValidationLevelIsSatisfiedBy_strictWithExpectedValue() { - val level = ValidationLevel.STRICT("foo") + fun AttributeConstraintIsSatisfied_strictWithExpectedValue() { + val level = AttributeConstraint.STRICT("Unique ID", "foo") { it.uniqueId.toStringUtf8() } + val kd = + keyDescriptionWithSoftwareSecurityLevels.copy(uniqueId = ByteString.copyFromUtf8("foo")) - assertThat(level.isSatisfiedBy("foo")).isTrue() - assertThat(level.isSatisfiedBy("bar")).isFalse() - assertThat(level.isSatisfiedBy(null)).isFalse() + assertIs(level.check(kd)) + assertIs(level.check(kd.copy(uniqueId = ByteString.copyFromUtf8("bar")))) } @Test - fun ValidationLevelIsSatisfiedBy_notNull_allowsAnyValue() { - val level = ValidationLevel.NOT_NULL + fun AttributeConstraintIsSatisfied_notNull_allowsAnyValue() { + val level = AttributeConstraint.NOT_NULL("Root of trust") { it.hardwareEnforced.rootOfTrust } + + assertIs(level.check(keyDescriptionWithSoftwareSecurityLevels)) - assertThat(level.isSatisfiedBy("foo")).isTrue() - assertThat(level.isSatisfiedBy(null)).isFalse() + val kdWithRot = + keyDescriptionWithSoftwareSecurityLevels.copy( + hardwareEnforced = + keyDescriptionWithSoftwareSecurityLevels.hardwareEnforced.copy( + rootOfTrust = RootOfTrust(ByteString.empty(), false, VerifiedBootState.VERIFIED) + ) + ) + assertIs(level.check(kdWithRot)) } @Test - fun ValidationLevelIsSatisfiedBy_ignore_allowsAnyValue() { - val level = ValidationLevel.IGNORE + fun SecurityLevelConstraintIsSatisfied_strictWithExpectedValue() { + val level = SecurityLevelConstraint.STRICT(SecurityLevel.STRONG_BOX) - assertThat(level.isSatisfiedBy("foo")).isTrue() - assertThat(level.isSatisfiedBy(null)).isTrue() + assertIs(level.check(keyDescriptionWithStrongBoxSecurityLevels)) + assertIs(level.check(keyDescriptionWithTeeSecurityLevels)) + assertIs(level.check(keyDescriptionWithMismatchedSecurityLevels)) } @Test - fun SecurityLevelValidationLevelIsSatisfiedBy_strictWithExpectedValue() { - val level = SecurityLevelValidationLevel.STRICT(SecurityLevel.STRONG_BOX) + fun SecurityLevelConstraintIsSatisfied_notSoftware_allowsAnyNonSoftwareMatchingLevels() { + val level = SecurityLevelConstraint.NOT_SOFTWARE - assertThat(level.isSatisfiedBy(keyDescriptionWithStrongBoxSecurityLevels)).isTrue() - assertThat(level.isSatisfiedBy(keyDescriptionWithTeeSecurityLevels)).isFalse() - assertThat(level.isSatisfiedBy(keyDescriptionWithMismatchedSecurityLevels)).isFalse() + assertIs(level.check(keyDescriptionWithStrongBoxSecurityLevels)) + assertIs(level.check(keyDescriptionWithTeeSecurityLevels)) + assertIs(level.check(keyDescriptionWithSoftwareSecurityLevels)) + assertIs(level.check(keyDescriptionWithMismatchedSecurityLevels)) } @Test - fun SecurityLevelValidationLevelIsSatisfiedBy_notSoftware_allowsAnyNonSoftwareMatchingLevels() { - val level = SecurityLevelValidationLevel.NOT_SOFTWARE + fun SecurityLevelConstraintIsSatisfied_consistent_allowsAnyMatchingLevels() { + val level = SecurityLevelConstraint.CONSISTENT - assertThat(level.isSatisfiedBy(keyDescriptionWithStrongBoxSecurityLevels)).isTrue() - assertThat(level.isSatisfiedBy(keyDescriptionWithTeeSecurityLevels)).isTrue() - assertThat(level.isSatisfiedBy(keyDescriptionWithSoftwareSecurityLevels)).isFalse() - assertThat(level.isSatisfiedBy(keyDescriptionWithMismatchedSecurityLevels)).isFalse() + assertIs(level.check(keyDescriptionWithStrongBoxSecurityLevels)) + assertIs(level.check(keyDescriptionWithTeeSecurityLevels)) + assertIs(level.check(keyDescriptionWithSoftwareSecurityLevels)) + assertIs(level.check(keyDescriptionWithMismatchedSecurityLevels)) } @Test - fun SecurityLevelValidationLevelIsSatisfiedBy_consistent_allowsAnyMatchingLevels() { - val level = SecurityLevelValidationLevel.CONSISTENT + fun AuthorizationListOrderingIsSatisfied_strictWithUnorderedTags_fails() { + val ordering = TagOrderConstraint.STRICT - assertThat(level.isSatisfiedBy(keyDescriptionWithStrongBoxSecurityLevels)).isTrue() - assertThat(level.isSatisfiedBy(keyDescriptionWithTeeSecurityLevels)).isTrue() - assertThat(level.isSatisfiedBy(keyDescriptionWithSoftwareSecurityLevels)).isTrue() - assertThat(level.isSatisfiedBy(keyDescriptionWithMismatchedSecurityLevels)).isFalse() + assertIs(ordering.check(keyDescriptionWithStrongBoxSecurityLevels)) + + val kdUnordered = + KeyDescription.parseFrom(readCertPath("invalid/tags_not_in_ascending_order.pem").leafCert())!! + + assertIs(ordering.check(kdUnordered)) } @Test - fun AuthorizationListOrderingIsSatisfiedBy_strictWithUnorderedTags_fails() { - val ordering = TagOrderValidationLevel.STRICT - - assertThat(ordering.isSatisfiedBy(keyDescriptionWithStrongBoxSecurityLevels)).isTrue() - assertThat( - ordering.isSatisfiedBy( - KeyDescription.parseFrom( - readCertPath("invalid/tags_not_in_ascending_order.pem").leafCert() - ) - ) + fun attributeConstraint_withViolation_returnsCorrectMessage() { + val level = AttributeConstraint.STRICT("Unique ID", "foo") { it.uniqueId.toStringUtf8() } + val kd = + keyDescriptionWithSoftwareSecurityLevels.copy(uniqueId = ByteString.copyFromUtf8("bar")) + + val violation = assertIs(level.check(kd)) + assertThat(violation.failureMessage) + .isEqualTo("Unique ID violates constraint: value=bar, config=$level") + } + + @Test + fun securityLevelConstraint_withViolation_returnsCorrectMessage() { + val level = SecurityLevelConstraint.STRICT(SecurityLevel.STRONG_BOX) + + val violation = assertIs(level.check(keyDescriptionWithTeeSecurityLevels)) + assertThat(violation.failureMessage) + .isEqualTo( + "Security level violates constraint: keyMintSecurityLevel=TRUSTED_ENVIRONMENT, " + + "attestationSecurityLevel=TRUSTED_ENVIRONMENT, config=$level" ) - .isFalse() + } + + @Test + fun tagOrderConstraint_withViolation_returnsCorrectMessage() { + val level = TagOrderConstraint.STRICT + val kdUnordered = + KeyDescription.parseFrom(readCertPath("invalid/tags_not_in_ascending_order.pem").leafCert())!! + + val violation = assertIs(level.check(kdUnordered)) + 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 8cdf65c..7b88a06 100644 --- a/src/test/kotlin/VerifierTest.kt +++ b/src/test/kotlin/VerifierTest.kt @@ -180,19 +180,23 @@ class VerifierTest { @Test fun rootOfTrustMissing_givesRootOfTrustMissingReason() { val result = assertIs(verifier.verify(CertLists.missingRootOfTrust)) - assertThat(result.reason).isEqualTo(KeyAttestationReason.ROOT_OF_TRUST_CONSTRAINT_VIOLATION) + assertThat(result.constraintLabel).isEqualTo("Root of trust") + assertThat(result.cause).contains("Root of trust") + assertThat(result.cause).contains("Root of trust violates constraint") } @Test fun keyOriginNotGenerated_throwsCertPathValidatorException() { val result = assertIs(verifier.verify(CertLists.importedOrigin)) - assertThat(result.reason).isEqualTo(KeyAttestationReason.KEY_ORIGIN_CONSTRAINT_VIOLATION) + assertThat(result.constraintLabel).isEqualTo("Origin") + assertThat(result.cause).contains("Origin violates constraint") } @Test fun mismatchedSecurityLevels_throwsCertPathValidatorException() { val result = assertIs(verifier.verify(CertLists.mismatchedSecurityLevels)) - assertThat(result.reason).isEqualTo(KeyAttestationReason.SECURITY_LEVEL_CONSTRAINT_VIOLATION) + assertThat(result.constraintLabel).isEqualTo("Security level") + assertThat(result.cause).contains("Security level violates constraint") } @Test @@ -202,13 +206,41 @@ class VerifierTest { { prodAnchors + TrustAnchor(Certs.root, null) }, { setOf() }, { FakeCalendar.DEFAULT.now() }, - ConstraintConfig(securityLevel = ValidationLevel.NOT_NULL), + constraintConfig { securityLevel { IgnoredConstraint } }, ) val result = assertIs(verifier.verify(CertLists.mismatchedSecurityLevels)) assertThat(result.securityLevel).isEqualTo(SecurityLevel.SOFTWARE) } + @Test + fun importedOrigins_customConfig_succeeds() { + val verifier = + Verifier( + { prodAnchors + TrustAnchor(Certs.root, null) }, + { setOf() }, + { FakeCalendar.DEFAULT.now() }, + constraintConfig { + keyOrigin { + AttributeConstraint.STRICT("Test", Origin.IMPORTED) { it.hardwareEnforced.origin } + } + }, + ) + assertIs(verifier.verify(CertLists.importedOrigin)) + } + + @Test + fun softwareRootOfTrust_customConfig_succeeds() { + val verifier = + Verifier( + { prodAnchors + TrustAnchor(Certs.root, null) }, + { setOf() }, + { FakeCalendar.DEFAULT.now() }, + constraintConfig { rootOfTrust { IgnoredConstraint } }, + ) + assertIs(verifier.verify(CertLists.missingRootOfTrust)) + } + @Test fun unorderedTags_customConfig_throwsCertPathValidatorException() { val verifier = @@ -216,11 +248,14 @@ class VerifierTest { { prodAnchors + TrustAnchor(Certs.root, null) }, { setOf() }, { FakeCalendar.DEFAULT.now() }, - ConstraintConfig(authorizationListTagOrder = TagOrderValidationLevel.STRICT), + constraintConfig { + additionalConstraint { TagOrderConstraint.STRICT } + additionalConstraint { IgnoredConstraint } + }, ) val result = assertIs(verifier.verify(CertLists.unorderedTags)) - assertThat(result.reason) - .isEqualTo(KeyAttestationReason.AUTHORIZATION_LIST_ORDERING_CONSTRAINT_VIOLATION) + assertThat(result.constraintLabel).isEqualTo("Tag order") + assertThat(result.cause).contains("Authorization list tags must be in ascending order") } @Test