From 411188a4d7b798e3c9c0cefb7c37322e92f0ebfe Mon Sep 17 00:00:00 2001 From: Carmen Hanish Date: Fri, 25 Sep 2026 03:34:56 -0700 Subject: [PATCH] Add AttestationApplicationId checking. PiperOrigin-RevId: 988165088 --- README.md | 63 +++++-- src/main/kotlin/ConstraintConfig.kt | 107 +++++++++++- src/main/kotlin/Extension.kt | 24 +++ src/main/kotlin/Verifier.kt | 42 ++++- src/test/kotlin/ConstraintConfigTest.kt | 215 +++++++++++++++++++++++- src/test/kotlin/VerifierTest.kt | 77 ++++++--- 6 files changed, 486 insertions(+), 42 deletions(-) diff --git a/README.md b/README.md index 51b081c..4416767 100644 --- a/README.md +++ b/README.md @@ -10,11 +10,17 @@ A Kotlin library for verifying Android key attestation certificate chains. val verifier = Verifier( GoogleTrustAnchors, // Trust anchors source ::getGoogleRevocationStatusFromWeb, // Revoked serials source - { Instant.now() } // Time source + { Instant.now() }, // Time source + ConstraintConfig(attestationApplicationId = + AttestationApplicationIdConstraint.ALLOW_UNKNOWN_PACKAGE(expectedAppId)), + ) // Verify an attestation certificate chain -val result = verifier.verify(certificateChain) +val result = verifier.verify( + certificateChain, + challengeChecker, +) // Handle the verification result when (result) { @@ -29,21 +35,47 @@ when (result) { is VerificationResult.PathValidationFailure -> // Handle validation failure is VerificationResult.ChainParsingFailure -> // Handle parsing failure is VerificationResult.ExtensionParsingFailure -> // Handle extension parsing issues - is VerificationResult.ExtensionConstraintViolation -> // Handle constraint violations + is VerificationResult.ConstraintViolation -> // Handle constraint violations } ``` -If there is additional verification you'd like to perform on the challenge -associated with the attestation certificate chain, pass in a `ChallengeChecker` -when verifying. For example, if you expect the challenge to be equal to -"challenge123", then usage would look like +### Choose a strong attestation challenge {#use-the-challenge} + +Correctly generating your attestation challenges can: + + * Prevent replay attacks (where attackers use an attestation more than once). + * Set a time-bound on a replay/relay (where attackers use an attestation from + one device on another device) attacks. + +**Important:** A challenge alone, unless it is bound to the protocol in some +other way, cannot entirely prevent relay attacks. + +#### Include information unique to the request in the challenge + +See the +[PIA](https://developer.android.com/google/play/integrity/standard#protect-requests) +documentation for guidance on how to choose a strong challenge by including +information from the request in the challenge. Note that the equivalent of the +attestation challenge is `requestHash` in the PIA context. + +#### Set time bounds for attestation validity + +It’s important that the attestation not be valid for eternity. The longer an +attestation lives, the more likely it is to be used for a replay or relay +attack. + +The easiest way to do this is to include a timestamp signed by your server-side +code in the challenge. When you verify the attestation's challenge, you'll check +the challenge signature and then make sure the timestamp is sufficiently fresh. + +#### Example implementations + +For example, if you expect the challenge to be equal to "challenge123", then +usage would look like ```kotlin // Create a ChallengeChecker val challengeChecker = ChallengeMatcher(ByteString.copyFromUtf8("challenge123")) - -// Verify an attestation certificate chain with the checker -val result = verifier.verify(certificateChain, challengeChecker) ``` If there are multiple checks to perform on the challenge, use a @@ -76,6 +108,17 @@ against the `InMemoryLruCache` if the challenge doesn't match. If the implementations in `challengecheckers/` don't fit your needs, simply extend the `ChallengeChecker` interface. +### Getting the expected Attestation Application Id + +It is important to check the attestation application ID when verifying a key +attestation. This assures that you don't accept attestations for keys controlled +by other applications, and can provide some assurance against relay attacks. + +The package list should be the names of all applications you expect to verify +against and their minimum accepted version numbers. You can get the signature +digests to put in in `signatures` from the Play Console as the app certificate +digests. + ## Building ```bash diff --git a/src/main/kotlin/ConstraintConfig.kt b/src/main/kotlin/ConstraintConfig.kt index 4cfa543..9f90d1d 100644 --- a/src/main/kotlin/ConstraintConfig.kt +++ b/src/main/kotlin/ConstraintConfig.kt @@ -21,6 +21,7 @@ import com.android.keyattestation.verifier.provider.ProvisioningMethod import com.google.common.collect.ImmutableList import com.google.errorprone.annotations.Immutable import com.google.errorprone.annotations.ThreadSafe +import java.math.BigInteger private typealias AttributeMapper = (KeyDescription) -> Any? @@ -50,13 +51,23 @@ class ConstraintConfig @JvmOverloads constructor( val allowSoftwareRoot: Boolean = false, + val attestationApplicationId: AttestationApplicationIdConstraint = + AttestationApplicationIdConstraint.ALLOW_UNKNOWN_PACKAGE( + AttestationApplicationIdConstraint.DEFAULT_MAX_OS_VERSION + ), val keyOrigin: Constraint? = null, val securityLevel: Constraint? = null, val rootOfTrust: Constraint? = null, val additionalConstraints: ImmutableList = ImmutableList.of(), val inputLimits: InputLimits = InputLimits(), ) { - fun getConstraints() = + /** + * Returns the list of constraints for this [ConstraintConfig] which can be checked using only the + * [KeyDescription] and [KeyAttestationCertPath]. + * + * @return The list of generic constraints. + */ + fun getGenericConstraints(): ImmutableList = ImmutableList.builder() .add( keyOrigin @@ -81,6 +92,10 @@ constructor( */ class ConstraintConfigBuilder() { var allowSoftwareRoot: Boolean = false + var attestationApplicationId: AttestationApplicationIdConstraint = + AttestationApplicationIdConstraint.ALLOW_UNKNOWN_PACKAGE( + AttestationApplicationIdConstraint.DEFAULT_MAX_OS_VERSION + ) var keyOrigin: Constraint? = null var securityLevel: Constraint? = null var rootOfTrust: Constraint? = null @@ -99,6 +114,10 @@ class ConstraintConfigBuilder() { this.rootOfTrust = constraint() } + fun attestationApplicationId(constraint: () -> AttestationApplicationIdConstraint) { + this.attestationApplicationId = constraint() + } + fun additionalConstraint(constraint: () -> Constraint) { additionalConstraints.add(constraint()) } @@ -110,6 +129,7 @@ class ConstraintConfigBuilder() { fun build(): ConstraintConfig = ConstraintConfig( allowSoftwareRoot, + attestationApplicationId, keyOrigin, securityLevel, rootOfTrust, @@ -347,3 +367,88 @@ sealed class ProvisioningMethodConstraint(val provisioningMethod: ProvisioningMe @Immutable data object REMOTE : ProvisioningMethodConstraint(ProvisioningMethod.REMOTELY_PROVISIONED) } + +/** + * Configuration for validating the attestation application ID in an Android attestation + * certificate. + */ +@Immutable +sealed class AttestationApplicationIdConstraint( + val isSatisfied: (AttestationApplicationId?, AttestationApplicationId?, BigInteger?) -> Boolean +) : Constraint { + companion object { + const val LABEL = "Attestation application ID" + val DEFAULT_MAX_OS_VERSION = BigInteger.valueOf(35) + + fun hasUnknownPackage(id: AttestationApplicationId?): Boolean = + id != null && id.packages.any { it.name == "UnknownPackage" && it.version == BigInteger.ONE } + } + + override val label = LABEL + + /** + * This should never be called. + * + * [AttestationApplicationIdConstraint] should be checked outside of the genericConstraints list, + * using [check(KeyDescription, AttestationApplicationId)]. + */ + override fun check(description: KeyDescription, certPath: KeyAttestationCertPath) = + Constraint.Violated( + "This should never be called this way, please use the check method that takes an AttestationApplicationId." + ) + + fun check( + expectedAttestationApplicationId: AttestationApplicationId?, + description: KeyDescription, + ): Constraint.Result = + if ( + isSatisfied( + expectedAttestationApplicationId, + description.softwareEnforced.attestationApplicationId, + description.hardwareEnforced.osVersion, + ) + ) { + Constraint.Satisfied + } else { + Constraint.Violated(getFailureMessage(description.softwareEnforced.attestationApplicationId)) + } + + open fun getFailureMessage(attestationApplicationId: AttestationApplicationId?): String = + "$LABEL violates constraint: attestationApplicationId=$attestationApplicationId, config=$this" + + /** + * Checks that the attestation application ID matches the expected value. + * + * This is the strictest form of attestation application ID check. There is no leniency for the OS + * version so it may fail on older devices. + * + * @param expectedId The expected value of the attestation application ID. + */ + @Immutable + data object STRICT : + AttestationApplicationIdConstraint({ expectedId, actualId, _ -> + expectedId?.isSatisfiedBy(actualId) ?: true + }) + + /** + * Checks that the attestation application ID matches the expected value or that the OS version is + * at or below the maximum OS version. + * + * This is a lenient form of attestation application ID check. It allows for older devices to pass + * the check if they have an unknown package name. + * + * @param expectedId The expected value of the attestation application ID. + * @param maxOsVersion The maximum OS version that the device can be on to pass the check. + */ + @Immutable + data class ALLOW_UNKNOWN_PACKAGE(val maxOsVersion: BigInteger) : + AttestationApplicationIdConstraint({ expectedId, actualId, osVersion -> + expectedId?.isSatisfiedBy(actualId) ?: true || + (osVersion != null && osVersion <= maxOsVersion && hasUnknownPackage(actualId)) + }) + + /** + * Does not check the attestation application ID. This should only be used for testing purposes. + */ + @Immutable data object NONE : AttestationApplicationIdConstraint({ _, _, _ -> true }) +} diff --git a/src/main/kotlin/Extension.kt b/src/main/kotlin/Extension.kt index 0bbe37f..6d50f67 100644 --- a/src/main/kotlin/Extension.kt +++ b/src/main/kotlin/Extension.kt @@ -657,6 +657,26 @@ data class AttestationApplicationId( } .let { DERSequence(it.toTypedArray()) } + /** + * Checks if the AttestationApplicationId is satisfied by the [candidate] + * AttestationApplicationId. + * + * @param candidate The actual AttestationApplicationId. + * @return True if the candidate satisfies the AttestationApplicationId, false otherwise. + */ + fun isSatisfiedBy(candidate: AttestationApplicationId?): Boolean { + if (candidate == null) return false + + if (packages.isNotEmpty()) { + if (packages.none { it.isSatisfiedBy(candidate.packages) }) return false + } + + if (signatures.isNotEmpty()) { + if (signatures.none { it in candidate.signatures }) return false + } + return true + } + companion object { internal fun from( seq: ASN1Sequence, @@ -701,6 +721,10 @@ data class AttestationPackageInfo(val name: String, val version: BigInteger) { } .let { DERSequence(it.toTypedArray()) } + internal fun isSatisfiedBy(candidate: Set) = candidate.any { + it.name == name && it.version >= version + } + internal companion object { fun from(attestationPackageInfo: ASN1Sequence): AttestationPackageInfo { require(attestationPackageInfo.size() == 2) { diff --git a/src/main/kotlin/Verifier.kt b/src/main/kotlin/Verifier.kt index ae3f488..1986655 100644 --- a/src/main/kotlin/Verifier.kt +++ b/src/main/kotlin/Verifier.kt @@ -151,6 +151,8 @@ constructor( private val trustAnchorsSource: () -> Set, private val revokedSerialsSource: () -> Set, private val instantSource: InstantSource, + // TODO(google-internal bug): Make required, though still allow null value. + private val expectedAttestationApplicationId: AttestationApplicationId? = null, private val constraintConfig: ConstraintConfig = ConstraintConfig(), ) { init { @@ -181,13 +183,16 @@ constructor( fun verify( chain: List, challengeChecker: ChallengeChecker? = null, + additionalAttestationApplicationId: AttestationApplicationId? = null, log: LogHook? = null, ): VerificationResult { val requestLog = log?.createRequestLog() val result = try { val certPath = KeyAttestationCertPath(chain) - runBlocking { internalVerify(certPath, challengeChecker, requestLog) } + runBlocking { + internalVerify(certPath, challengeChecker, additionalAttestationApplicationId, requestLog) + } } catch (e: CertificateException) { requestLog?.logInputChain(chain.map { it.getEncoded().toByteString() }) VerificationResult.ChainParsingFailure(e) @@ -203,6 +208,8 @@ constructor( * @param chain The attestation certificate chain to verify. * @param coroutineScope The coroutine scope from which to run the verification. * @param challengeChecker The challenge checker to use for additional challenge validation. + * @param additionalAttestationApplicationId An additional attestation application ID to use for + * constraint checking. * @param log The log hook to use for logging. * @return A [ListenableFuture] containing the [VerificationResult]. */ @@ -211,6 +218,7 @@ constructor( coroutineScope: CoroutineScope, chain: List, challengeChecker: ChallengeChecker? = null, + additionalAttestationApplicationId: AttestationApplicationId? = null, log: LogHook? = null, ): ListenableFuture { val immutableChain = ImmutableList.copyOf(chain) @@ -219,7 +227,7 @@ constructor( val result = try { val certPath = KeyAttestationCertPath(immutableChain) - internalVerify(certPath, challengeChecker, requestLog) + internalVerify(certPath, challengeChecker, additionalAttestationApplicationId, requestLog) } catch (e: CertificateException) { requestLog?.logInputChain(immutableChain.map { it.getEncoded().toByteString() }) VerificationResult.ChainParsingFailure(e) @@ -233,6 +241,7 @@ constructor( private suspend fun internalVerify( certPath: KeyAttestationCertPath, challengeChecker: ChallengeChecker? = null, + additionalAttestationApplicationId: AttestationApplicationId? = null, log: VerifyRequestLog? = null, ): VerificationResult { log?.logInputChain(certPath.certificatesWithAnchor.map { it.getEncoded().toByteString() }) @@ -305,7 +314,7 @@ constructor( } } - for (constraint in constraintConfig.getConstraints()) { + for (constraint in constraintConfig.getGenericConstraints()) { val result = constraint.check(keyDescription, certPath) when (result) { is Constraint.Satisfied -> {} @@ -315,6 +324,21 @@ constructor( } } + val attestationApplicationIdCheckResult = + constraintConfig.attestationApplicationId.check( + combine(expectedAttestationApplicationId, additionalAttestationApplicationId), + keyDescription, + ) + when (attestationApplicationIdCheckResult) { + is Constraint.Satisfied -> {} + is Constraint.Violated -> { + return VerificationResult.ConstraintViolation( + constraintConfig.attestationApplicationId.label, + attestationApplicationIdCheckResult.failureMessage, + ) + } + } + val securityLevel = minOf(keyDescription.attestationSecurityLevel, keyDescription.keyMintSecurityLevel) @@ -333,3 +357,15 @@ constructor( ) } } + +private fun combine( + stored: AttestationApplicationId?, + additional: AttestationApplicationId?, +): AttestationApplicationId? { + if (stored == null) return additional + if (additional == null) return stored + return AttestationApplicationId( + stored.packages + additional.packages, + stored.signatures + additional.signatures, + ) +} diff --git a/src/test/kotlin/ConstraintConfigTest.kt b/src/test/kotlin/ConstraintConfigTest.kt index de88da0..0b922c9 100644 --- a/src/test/kotlin/ConstraintConfigTest.kt +++ b/src/test/kotlin/ConstraintConfigTest.kt @@ -19,6 +19,8 @@ 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 com.google.protobuf.kotlin.toByteStringUtf8 +import java.math.BigInteger import kotlin.test.assertIs import org.junit.Assert.assertThrows import org.junit.Test @@ -29,21 +31,37 @@ import org.junit.runners.JUnit4 class ConstraintConfigTest { private companion object { + const val TEST_PACKAGE_NAME = "com.example.app" + val TEST_PACKAGE_VERSION = BigInteger.valueOf(10) + val TEST_PACKAGE_INFO = AttestationPackageInfo(TEST_PACKAGE_NAME, TEST_PACKAGE_VERSION) + val TEST_SIGNATURE = ByteString.copyFromUtf8("test-signature") + val TEST_APP_ID = + AttestationApplicationId( + packages = setOf(TEST_PACKAGE_INFO), + signatures = setOf(TEST_SIGNATURE), + ) + val UNKNOWN_PACKAGE = AttestationPackageInfo("UnknownPackage", BigInteger.ONE) + val MAX_OS_VERSION = BigInteger.valueOf(140000) + val authorizationList = - AuthorizationList(purposes = setOf(1.toBigInteger()), algorithms = 1.toBigInteger()) + AuthorizationList( + purposes = setOf(1.toBigInteger()), + algorithms = 1.toBigInteger(), + osVersion = MAX_OS_VERSION, + ) fun createTestKeyDescription( attestationSecurityLevel: SecurityLevel, keyMintSecurityLevel: SecurityLevel, ) = KeyDescription( - attestationVersion = 1.toBigInteger(), + attestationVersion = 400.toBigInteger(), attestationSecurityLevel = attestationSecurityLevel, - keyMintVersion = 1.toBigInteger(), + keyMintVersion = 400.toBigInteger(), keyMintSecurityLevel = keyMintSecurityLevel, attestationChallenge = ByteString.empty(), uniqueId = ByteString.empty(), - softwareEnforced = authorizationList, + softwareEnforced = AuthorizationList(attestationApplicationId = TEST_APP_ID), hardwareEnforced = authorizationList, ) } @@ -432,6 +450,193 @@ class ConstraintConfigTest { fun constraintConfig_provisioningMethod_configuredViaAdditionalConstraint() { val config = constraintConfig { additionalConstraint { ProvisioningMethodConstraint.FACTORY } } assertThat(config.additionalConstraints).contains(ProvisioningMethodConstraint.FACTORY) - assertThat(config.getConstraints()).contains(ProvisioningMethodConstraint.FACTORY) + assertThat(config.getGenericConstraints()).contains(ProvisioningMethodConstraint.FACTORY) + } + + @Test + fun attestationApplicationId_strict_matchingAppId_returnsSatisfied() { + assertIs( + AttestationApplicationIdConstraint.STRICT.check( + TEST_APP_ID, + keyDescriptionWithTeeSecurityLevels, + ) + ) + } + + @Test + fun attestationApplicationId_strict_higherVersion_returnsSatisfied() { + assertIs( + AttestationApplicationIdConstraint.STRICT.check( + AttestationApplicationId( + setOf(AttestationPackageInfo(TEST_PACKAGE_NAME, TEST_PACKAGE_VERSION - BigInteger.ONE)), + setOf(TEST_SIGNATURE), + ), + keyDescriptionWithTeeSecurityLevels, + ) + ) + } + + @Test + fun attestationApplicationId_strict_noSignatureRequirements_returnsSatisfied() { + assertIs( + AttestationApplicationIdConstraint.STRICT.check( + AttestationApplicationId(packages = setOf(TEST_PACKAGE_INFO), signatures = emptySet()), + keyDescriptionWithTeeSecurityLevels, + ) + ) + } + + @Test + fun attestationApplicationId_strict_lowerVersion_returnsViolated() { + assertIs( + AttestationApplicationIdConstraint.STRICT.check( + AttestationApplicationId( + setOf(AttestationPackageInfo(TEST_PACKAGE_NAME, TEST_PACKAGE_VERSION + BigInteger.ONE)), + setOf(TEST_SIGNATURE), + ), + keyDescriptionWithTeeSecurityLevels, + ) + ) + } + + @Test + fun attestationApplicationId_strict_mismatchedPackageName_returnsViolated() { + assertIs( + AttestationApplicationIdConstraint.STRICT.check( + AttestationApplicationId( + setOf( + AttestationPackageInfo(name = "different.package.name", version = TEST_PACKAGE_VERSION) + ), + emptySet(), + ), + keyDescriptionWithTeeSecurityLevels, + ) + ) + } + + @Test + fun attestationApplicationId_strict_mismatchedSignature_returnsViolated() { + assertIs( + AttestationApplicationIdConstraint.STRICT.check( + AttestationApplicationId( + setOf(AttestationPackageInfo(TEST_PACKAGE_NAME, TEST_PACKAGE_VERSION)), + setOf("other-signature".toByteStringUtf8()), + ), + keyDescriptionWithTeeSecurityLevels, + ) + ) + } + + @Test + fun attestationApplicationId_lenient_matchingAppId_returnsSatisfiedRegardlessOfOsVersion() { + assertIs( + AttestationApplicationIdConstraint.ALLOW_UNKNOWN_PACKAGE(MAX_OS_VERSION) + .check(TEST_APP_ID, keyDescriptionWithTeeSecurityLevels) + ) + } + + @Test + fun attestationApplicationId_lenient_mismatchedAppId_withUnknownPackage_andLowerOrEqualOsVersion_returnsSatisfied() { + val actualAppId = + AttestationApplicationId(packages = setOf(UNKNOWN_PACKAGE), signatures = setOf()) + + val kdEqualOs = + keyDescriptionWithTeeSecurityLevels.copy( + softwareEnforced = authorizationList.copy(attestationApplicationId = actualAppId), + hardwareEnforced = authorizationList.copy(osVersion = MAX_OS_VERSION), + ) + assertIs( + AttestationApplicationIdConstraint.ALLOW_UNKNOWN_PACKAGE(MAX_OS_VERSION) + .check(TEST_APP_ID, kdEqualOs) + ) + + val kdLowerOs = + keyDescriptionWithTeeSecurityLevels.copy( + softwareEnforced = authorizationList.copy(attestationApplicationId = actualAppId), + hardwareEnforced = authorizationList.copy(osVersion = BigInteger.valueOf(130000)), + ) + assertIs( + AttestationApplicationIdConstraint.ALLOW_UNKNOWN_PACKAGE(MAX_OS_VERSION) + .check(TEST_APP_ID, kdLowerOs) + ) + } + + @Test + fun attestationApplicationId_lenient_mismatchedAppId_withUnknownPackage_andHigherOsVersion_returnsViolated() { + val actualAppId = + AttestationApplicationId(packages = setOf(UNKNOWN_PACKAGE), signatures = setOf()) + val kd = + keyDescriptionWithTeeSecurityLevels.copy( + softwareEnforced = authorizationList.copy(attestationApplicationId = actualAppId), + hardwareEnforced = authorizationList.copy(osVersion = BigInteger.valueOf(150000)), + ) + + assertIs( + AttestationApplicationIdConstraint.ALLOW_UNKNOWN_PACKAGE(MAX_OS_VERSION) + .check(TEST_APP_ID, kd) + ) + } + + @Test + fun attestationApplicationId_lenient_mismatchedAppId_withoutUnknownPackage_andLowerOsVersion_returnsViolated() { + assertIs( + AttestationApplicationIdConstraint.ALLOW_UNKNOWN_PACKAGE(MAX_OS_VERSION) + .check( + AttestationApplicationId( + packages = setOf(AttestationPackageInfo("com.other.app", BigInteger.valueOf(10))), + signatures = setOf(TEST_SIGNATURE), + ), + keyDescriptionWithTeeSecurityLevels, + ) + ) + } + + @Test + fun attestationApplicationId_lenient_mismatchedAppId_withUnknownPackageWrongVersion_returnsViolated() { + val actualAppId = + AttestationApplicationId( + packages = setOf(AttestationPackageInfo("UnknownPackage", BigInteger.valueOf(2))), + signatures = setOf(), + ) + val kd = + keyDescriptionWithTeeSecurityLevels.copy( + softwareEnforced = authorizationList.copy(attestationApplicationId = actualAppId), + hardwareEnforced = authorizationList.copy(osVersion = BigInteger.valueOf(130000)), + ) + + assertIs( + AttestationApplicationIdConstraint.ALLOW_UNKNOWN_PACKAGE(MAX_OS_VERSION) + .check(TEST_APP_ID, kd) + ) + } + + @Test + fun attestationApplicationId_none_alwaysReturnsSatisfied() { + assertIs( + AttestationApplicationIdConstraint.NONE.check( + TEST_APP_ID, + keyDescriptionWithTeeSecurityLevels, + ) + ) + } + + @Test + fun attestationApplicationIdConstraint_withViolation_returnsCorrectMessage() { + val expectedAppId = + AttestationApplicationId( + packages = setOf(AttestationPackageInfo("com.other.app", BigInteger.valueOf(10))), + signatures = setOf(TEST_SIGNATURE), + ) + val constraint = AttestationApplicationIdConstraint.STRICT + + val violation = + assertIs( + constraint.check(expectedAppId, keyDescriptionWithTeeSecurityLevels) + ) + assertThat(violation.failureMessage) + .isEqualTo( + "Attestation application ID violates constraint: attestationApplicationId=$TEST_APP_ID, config=$constraint" + ) + assertThat(constraint.label).isEqualTo("Attestation application ID") } } diff --git a/src/test/kotlin/VerifierTest.kt b/src/test/kotlin/VerifierTest.kt index 59c3b97..39fd223 100644 --- a/src/test/kotlin/VerifierTest.kt +++ b/src/test/kotlin/VerifierTest.kt @@ -43,6 +43,7 @@ import com.google.testing.junit.testparameterinjector.TestParameters import com.google.testing.junit.testparameterinjector.TestParameters.TestParametersValues import com.google.testing.junit.testparameterinjector.TestParametersValuesProvider import com.google.testing.junit.testparameterinjector.TestParametersValuesProvider.Context +import java.math.BigInteger import java.security.cert.PKIXReason import java.security.cert.TrustAnchor import java.time.Instant @@ -98,11 +99,13 @@ class VerifierTest { { prodAnchors + SOFTWARE_ROOTS.map { TrustAnchor(it, null) } }, { setOf() }, { timestamp }, - ConstraintConfig( - allowSoftwareRoot = true, - securityLevel = IgnoredConstraint, - rootOfTrust = IgnoredConstraint, - ), + json.softwareEnforced.attestationApplicationId, + constraintConfig { + allowSoftwareRoot = true + securityLevel { IgnoredConstraint } + rootOfTrust { IgnoredConstraint } + attestationApplicationId { AttestationApplicationIdConstraint.STRICT } + }, ) val chain = readCertList("${subpath}.pem") val result = assertIs(verifier.verify(chain)) @@ -173,6 +176,26 @@ class VerifierTest { ) } + @Test + fun verify_attestationApplicationIdConstraintFails_returnsConstraintViolation() { + val verifier = + Verifier( + { prodAnchors + TrustAnchor(Certs.root, null) }, + { setOf() }, + { FakeCalendar.DEFAULT.now() }, + AttestationApplicationId( + packages = setOf(AttestationPackageInfo("com.wrong.package", BigInteger.ONE)), + signatures = emptySet(), + ), + constraintConfig { attestationApplicationId { AttestationApplicationIdConstraint.STRICT } }, + ) + val chain = readCertList("blueline/sdk28/TEE_EC_NONE.pem") + + val result = assertIs(verifier.verify(chain)) + + assertThat(result.constraintLabel).isEqualTo("Attestation application ID") + } + @Test fun verifyAsync_unexpectedRootKey_returnsPathValidationFailure() = runBlocking { val result = @@ -230,7 +253,8 @@ class VerifierTest { { prodAnchors + TrustAnchor(Certs.root, null) }, { setOf() }, { FakeCalendar.DEFAULT.now() }, - constraintConfig { additionalConstraint { SecurityLevelConstraint.MATCHES_CERTIFICATE } }, + constraintConfig = + constraintConfig { additionalConstraint { SecurityLevelConstraint.MATCHES_CERTIFICATE } }, ) val result = assertIs( @@ -250,7 +274,8 @@ class VerifierTest { { prodAnchors + TrustAnchor(Certs.root, null) }, { setOf() }, { FakeCalendar.DEFAULT.now() }, - constraintConfig { additionalConstraint { SecurityLevelConstraint.MATCHES_CERTIFICATE } }, + constraintConfig = + constraintConfig { additionalConstraint { SecurityLevelConstraint.MATCHES_CERTIFICATE } }, ) val result = assertIs( @@ -271,7 +296,7 @@ class VerifierTest { { prodAnchors + TrustAnchor(Certs.root, null) }, { setOf() }, { FakeCalendar.DEFAULT.now() }, - constraintConfig { securityLevel { IgnoredConstraint } }, + constraintConfig = constraintConfig { securityLevel { IgnoredConstraint } }, ) val result = assertIs(verifier.verify(CertLists.mismatchedSecurityLevels)) @@ -285,7 +310,8 @@ class VerifierTest { { prodAnchors + TrustAnchor(Certs.root, null) }, { setOf() }, { FakeCalendar.DEFAULT.now() }, - constraintConfig { additionalConstraint { ProvisioningMethodConstraint.REMOTE } }, + constraintConfig = + constraintConfig { additionalConstraint { ProvisioningMethodConstraint.REMOTE } }, ) val result = assertIs(verifier.verify(CertLists.validFactoryProvisioned)) assertThat(result.constraintLabel).isEqualTo("Provisioning method") @@ -302,7 +328,8 @@ class VerifierTest { { prodAnchors + TrustAnchor(Certs.root, null) }, { setOf() }, { FakeCalendar.DEFAULT.now() }, - constraintConfig { additionalConstraint { ProvisioningMethodConstraint.FACTORY } }, + constraintConfig = + constraintConfig { additionalConstraint { ProvisioningMethodConstraint.FACTORY } }, ) assertIs(verifier.verify(CertLists.validFactoryProvisioned)) } @@ -314,7 +341,8 @@ class VerifierTest { { prodAnchors + TrustAnchor(Certs.root, null) }, { setOf() }, { FakeCalendar.DEFAULT.now() }, - constraintConfig { additionalConstraint { ProvisioningMethodConstraint.FACTORY } }, + constraintConfig = + constraintConfig { additionalConstraint { ProvisioningMethodConstraint.FACTORY } }, ) val result = assertIs(verifier.verify(CertLists.validRemotelyProvisioned)) assertThat(result.constraintLabel).isEqualTo("Provisioning method") @@ -331,7 +359,8 @@ class VerifierTest { { prodAnchors + TrustAnchor(Certs.root, null) }, { setOf() }, { FakeCalendar.DEFAULT.now() }, - constraintConfig { additionalConstraint { ProvisioningMethodConstraint.REMOTE } }, + constraintConfig = + constraintConfig { additionalConstraint { ProvisioningMethodConstraint.REMOTE } }, ) assertIs(verifier.verify(CertLists.validRemotelyProvisioned)) } @@ -343,11 +372,12 @@ class VerifierTest { { prodAnchors + TrustAnchor(Certs.root, null) }, { setOf() }, { FakeCalendar.DEFAULT.now() }, - constraintConfig { - keyOrigin { - AttributeConstraint.STRICT("Test", Origin.IMPORTED) { it.hardwareEnforced.origin } - } - }, + constraintConfig = + constraintConfig { + keyOrigin { + AttributeConstraint.STRICT("Test", Origin.IMPORTED) { it.hardwareEnforced.origin } + } + }, ) assertIs(verifier.verify(CertLists.importedOrigin)) } @@ -359,7 +389,7 @@ class VerifierTest { { prodAnchors + TrustAnchor(Certs.root, null) }, { setOf() }, { FakeCalendar.DEFAULT.now() }, - constraintConfig { rootOfTrust { IgnoredConstraint } }, + constraintConfig = constraintConfig { rootOfTrust { IgnoredConstraint } }, ) assertIs(verifier.verify(CertLists.missingRootOfTrust)) } @@ -371,10 +401,11 @@ class VerifierTest { { prodAnchors + TrustAnchor(Certs.root, null) }, { setOf() }, { FakeCalendar.DEFAULT.now() }, - constraintConfig { - additionalConstraint { TagOrderConstraint.STRICT } - additionalConstraint { IgnoredConstraint } - }, + constraintConfig = + constraintConfig { + additionalConstraint { TagOrderConstraint.STRICT } + additionalConstraint { IgnoredConstraint } + }, ) val result = assertIs(verifier.verify(CertLists.unorderedTags)) assertThat(result.constraintLabel).isEqualTo("Tag order") @@ -390,7 +421,7 @@ class VerifierTest { this, CertLists.wrongTrustAnchor, ChallengeMatcher(ByteString.copyFromUtf8("challenge")), - logHook, + log = logHook, ) .await() )