From b1bf43755ab40d087d6c9159a8a0f8d85a242d39 Mon Sep 17 00:00:00 2001 From: Seth Moore Date: Thu, 26 Mar 2026 08:51:24 -0700 Subject: [PATCH 01/48] Rename "ExtensionConstraintConfig" to "ConstraintConfig" We can apply constraints to anything, not just extensions. Rename things appropriately to make room for constraints on things other than extensions. PiperOrigin-RevId: 889867873 --- ...onstraintConfig.kt => ConstraintConfig.kt} | 42 +++++++++---------- src/main/kotlin/KeyAttestationReason.kt | 8 ++-- src/main/kotlin/Verifier.kt | 28 ++++++------- ...tConfigTest.kt => ConstraintConfigTest.kt} | 2 +- src/test/kotlin/VerifierTest.kt | 16 ++++--- 5 files changed, 47 insertions(+), 49 deletions(-) rename src/main/kotlin/{ExtensionConstraintConfig.kt => ConstraintConfig.kt} (75%) rename src/test/kotlin/{ExtensionConstraintConfigTest.kt => ConstraintConfigTest.kt} (99%) diff --git a/src/main/kotlin/ExtensionConstraintConfig.kt b/src/main/kotlin/ConstraintConfig.kt similarity index 75% rename from src/main/kotlin/ExtensionConstraintConfig.kt rename to src/main/kotlin/ConstraintConfig.kt index 3bc4c21..b01bcd6 100644 --- a/src/main/kotlin/ExtensionConstraintConfig.kt +++ b/src/main/kotlin/ConstraintConfig.kt @@ -21,42 +21,42 @@ import com.google.errorprone.annotations.Immutable import com.google.errorprone.annotations.ThreadSafe /** - * Configuration for validating the extensions in an Android attestation certificate, as described + * 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 ExtensionConstraintConfig( +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 extension in an Android attestation certificate. */ +/** Configuration for validating a single attribute in an Android attestation certificate. */ @Immutable(containerOf = ["T"]) sealed interface ValidationLevel { - /** Evaluates whether the [extension] is satisfied by this [ValidationLevel]. */ - fun isSatisfiedBy(extension: Any?): Boolean + /** Evaluates whether the [attribute] is satisfied by this [ValidationLevel]. */ + fun isSatisfiedBy(attribute: Any?): Boolean /** - * Checks that the extension exists and matches the expected value. + * Checks that the attribute exists and matches the expected value. * - * @param expectedVal The expected value of the extension. + * @param expectedVal The expected value of the attribute. */ @Immutable(containerOf = ["T"]) data class STRICT(val expectedVal: T) : ValidationLevel { - override fun isSatisfiedBy(extension: Any?): Boolean = extension == expectedVal + override fun isSatisfiedBy(attribute: Any?): Boolean = attribute == expectedVal } - /* Check that the extension exists. */ + /* Check that the attribute exists. */ @Immutable data object NOT_NULL : ValidationLevel { - override fun isSatisfiedBy(extension: Any?): Boolean = extension != null + override fun isSatisfiedBy(attribute: Any?): Boolean = attribute != null } @Immutable data object IGNORE : ValidationLevel { - override fun isSatisfiedBy(extension: Any?): Boolean = true + override fun isSatisfiedBy(attribute: Any?): Boolean = true } } @@ -80,8 +80,8 @@ sealed class SecurityLevelValidationLevel : ValidationLevel { @Immutable data class STRICT(val expectedVal: SecurityLevel) : SecurityLevelValidationLevel() { @RequiresApi(24) - override fun isSatisfiedBy(extension: Any?): Boolean { - val keyDescription = extension as? KeyDescription ?: return false + override fun isSatisfiedBy(attribute: Any?): Boolean { + val keyDescription = attribute as? KeyDescription ?: return false val securityLevelIsExpected = keyDescription.attestationSecurityLevel == this.expectedVal return areSecurityLevelsMatching(keyDescription) && securityLevelIsExpected } @@ -94,8 +94,8 @@ sealed class SecurityLevelValidationLevel : ValidationLevel { @Immutable data object NOT_SOFTWARE : SecurityLevelValidationLevel() { @RequiresApi(24) - override fun isSatisfiedBy(extension: Any?): Boolean { - val keyDescription = extension as? KeyDescription ?: return false + override fun isSatisfiedBy(attribute: Any?): Boolean { + val keyDescription = attribute as? KeyDescription ?: return false val securityLevelIsSoftware = keyDescription.attestationSecurityLevel == SecurityLevel.SOFTWARE return areSecurityLevelsMatching(keyDescription) && !securityLevelIsSoftware @@ -109,28 +109,28 @@ sealed class SecurityLevelValidationLevel : ValidationLevel { @Immutable data object CONSISTENT : SecurityLevelValidationLevel() { @RequiresApi(24) - override fun isSatisfiedBy(extension: Any?): Boolean { - val keyDescription = extension as? KeyDescription ?: return false + override fun isSatisfiedBy(attribute: Any?): Boolean { + val keyDescription = attribute as? KeyDescription ?: return false return areSecurityLevelsMatching(keyDescription) } } } /** - * Configuration for validating the ordering of the extensions in the AuthorizationList sequence in + * Configuration for validating the ordering of the attributes in the AuthorizationList sequence in * an Android attestation certificate. */ @Immutable sealed interface TagOrderValidationLevel : ValidationLevel { /** - * Checks that the extensions in the AuthorizationList sequence appear in the order specified by + * 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(extension: Any?): Boolean { - val keyDescription = extension as? KeyDescription ?: return false + override fun isSatisfiedBy(attribute: Any?): Boolean { + val keyDescription = attribute as? KeyDescription ?: return false return keyDescription.softwareEnforced.areTagsOrdered && keyDescription.hardwareEnforced.areTagsOrdered } diff --git a/src/main/kotlin/KeyAttestationReason.kt b/src/main/kotlin/KeyAttestationReason.kt index 3b4428e..9da4e3c 100644 --- a/src/main/kotlin/KeyAttestationReason.kt +++ b/src/main/kotlin/KeyAttestationReason.kt @@ -33,21 +33,21 @@ 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 [ExtensionConstraintConfig]. + // 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 [ExtensionConstraintConfig]. + // 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 [ExtensionConstraintConfig]. + // 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 - // [ExtensionConstraintConfig]. + // [ConstraintConfig]. AUTHORIZATION_LIST_ORDERING_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 eb17906..63439b0 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 ExtensionConstraintViolation(val cause: String, val reason: KeyAttestationReason) : + data class ConstraintViolation(val cause: String, val reason: KeyAttestationReason) : VerificationResult data object SoftwareAttestationUnsupported : VerificationResult @@ -142,7 +142,7 @@ constructor( private val trustAnchorsSource: () -> Set, private val revokedSerialsSource: () -> Set, private val instantSource: InstantSource, - private val extensionConstraintConfig: ExtensionConstraintConfig = ExtensionConstraintConfig(), + private val constraintConfig: ConstraintConfig = ConstraintConfig(), ) { init { Security.addProvider(KeyAttestationProvider()) @@ -292,35 +292,35 @@ constructor( } val origin = keyDescription.hardwareEnforced.origin - if (!extensionConstraintConfig.keyOrigin.isSatisfiedBy(origin)) { - return VerificationResult.ExtensionConstraintViolation( - "Origin violates constraint: value=${origin}, config=${extensionConstraintConfig.keyOrigin}", + if (!constraintConfig.keyOrigin.isSatisfiedBy(origin)) { + return VerificationResult.ConstraintViolation( + "Origin violates constraint: value=${origin}, config=${constraintConfig.keyOrigin}", KeyAttestationReason.KEY_ORIGIN_CONSTRAINT_VIOLATION, ) } val securityLevel = - if (extensionConstraintConfig.securityLevel.isSatisfiedBy(keyDescription)) { + if (constraintConfig.securityLevel.isSatisfiedBy(keyDescription)) { minOf(keyDescription.attestationSecurityLevel, keyDescription.keyMintSecurityLevel) } else { - return VerificationResult.ExtensionConstraintViolation( - "Security level violates constraint: value=${keyDescription.attestationSecurityLevel}, config=${extensionConstraintConfig.securityLevel}", + return VerificationResult.ConstraintViolation( + "Security level violates constraint: value=${keyDescription.attestationSecurityLevel}, config=${constraintConfig.securityLevel}", KeyAttestationReason.SECURITY_LEVEL_CONSTRAINT_VIOLATION, ) } val rootOfTrust = keyDescription.hardwareEnforced.rootOfTrust - if (!extensionConstraintConfig.rootOfTrust.isSatisfiedBy(rootOfTrust)) { - return VerificationResult.ExtensionConstraintViolation( - "Root of trust violates constraint: value=${rootOfTrust}, config=${extensionConstraintConfig.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 (!extensionConstraintConfig.authorizationListTagOrder.isSatisfiedBy(keyDescription)) { - return VerificationResult.ExtensionConstraintViolation( - "Authorization list ordering violates constraint: config=${extensionConstraintConfig.authorizationListTagOrder}", + if (!constraintConfig.authorizationListTagOrder.isSatisfiedBy(keyDescription)) { + return VerificationResult.ConstraintViolation( + "Authorization list ordering violates constraint: config=${constraintConfig.authorizationListTagOrder}", KeyAttestationReason.AUTHORIZATION_LIST_ORDERING_CONSTRAINT_VIOLATION, ) } diff --git a/src/test/kotlin/ExtensionConstraintConfigTest.kt b/src/test/kotlin/ConstraintConfigTest.kt similarity index 99% rename from src/test/kotlin/ExtensionConstraintConfigTest.kt rename to src/test/kotlin/ConstraintConfigTest.kt index 112a0b1..fefda2b 100644 --- a/src/test/kotlin/ExtensionConstraintConfigTest.kt +++ b/src/test/kotlin/ConstraintConfigTest.kt @@ -24,7 +24,7 @@ import org.junit.runner.RunWith import org.junit.runners.JUnit4 @RunWith(JUnit4::class) -class ExtensionConstraintConfigTest { +class ConstraintConfigTest { private companion object { val authorizationList = diff --git a/src/test/kotlin/VerifierTest.kt b/src/test/kotlin/VerifierTest.kt index 3c077e3..8cdf65c 100644 --- a/src/test/kotlin/VerifierTest.kt +++ b/src/test/kotlin/VerifierTest.kt @@ -16,7 +16,7 @@ package com.android.keyattestation.verifier -import com.android.keyattestation.verifier.VerificationResult.ExtensionConstraintViolation +import com.android.keyattestation.verifier.VerificationResult.ConstraintViolation import com.android.keyattestation.verifier.VerificationResult.ExtensionParsingFailure import com.android.keyattestation.verifier.VerificationResult.PathValidationFailure import com.android.keyattestation.verifier.challengecheckers.ChallengeMatcher @@ -179,21 +179,19 @@ class VerifierTest { @Test fun rootOfTrustMissing_givesRootOfTrustMissingReason() { - val result = - assertIs(verifier.verify(CertLists.missingRootOfTrust)) + val result = assertIs(verifier.verify(CertLists.missingRootOfTrust)) assertThat(result.reason).isEqualTo(KeyAttestationReason.ROOT_OF_TRUST_CONSTRAINT_VIOLATION) } @Test fun keyOriginNotGenerated_throwsCertPathValidatorException() { - val result = assertIs(verifier.verify(CertLists.importedOrigin)) + val result = assertIs(verifier.verify(CertLists.importedOrigin)) assertThat(result.reason).isEqualTo(KeyAttestationReason.KEY_ORIGIN_CONSTRAINT_VIOLATION) } @Test fun mismatchedSecurityLevels_throwsCertPathValidatorException() { - val result = - assertIs(verifier.verify(CertLists.mismatchedSecurityLevels)) + val result = assertIs(verifier.verify(CertLists.mismatchedSecurityLevels)) assertThat(result.reason).isEqualTo(KeyAttestationReason.SECURITY_LEVEL_CONSTRAINT_VIOLATION) } @@ -204,7 +202,7 @@ class VerifierTest { { prodAnchors + TrustAnchor(Certs.root, null) }, { setOf() }, { FakeCalendar.DEFAULT.now() }, - ExtensionConstraintConfig(securityLevel = ValidationLevel.NOT_NULL), + ConstraintConfig(securityLevel = ValidationLevel.NOT_NULL), ) val result = assertIs(verifier.verify(CertLists.mismatchedSecurityLevels)) @@ -218,9 +216,9 @@ class VerifierTest { { prodAnchors + TrustAnchor(Certs.root, null) }, { setOf() }, { FakeCalendar.DEFAULT.now() }, - ExtensionConstraintConfig(authorizationListTagOrder = TagOrderValidationLevel.STRICT), + ConstraintConfig(authorizationListTagOrder = TagOrderValidationLevel.STRICT), ) - val result = assertIs(verifier.verify(CertLists.unorderedTags)) + val result = assertIs(verifier.verify(CertLists.unorderedTags)) assertThat(result.reason) .isEqualTo(KeyAttestationReason.AUTHORIZATION_LIST_ORDERING_CONSTRAINT_VIOLATION) } From 87f2a9aba4413554133e8c3597c8cb81ca95ce73 Mon Sep 17 00:00:00 2001 From: Suzanna Jiwani Date: Thu, 2 Apr 2026 12:36:16 -0700 Subject: [PATCH 02/48] Reformat test certs Make test certs more RFC 7468 compliant by ensuring each line consists of no more than 64 characters. PiperOrigin-RevId: 893644193 --- testdata/caiman/sdk36/SB_EC_RKP.pem | 89 +++++++++++++++++++++++++-- testdata/caiman/sdk36/TEE_EC_RKP.pem | 90 ++++++++++++++++++++++++++-- 2 files changed, 169 insertions(+), 10 deletions(-) diff --git a/testdata/caiman/sdk36/SB_EC_RKP.pem b/testdata/caiman/sdk36/SB_EC_RKP.pem index 29de253..9076a5b 100644 --- a/testdata/caiman/sdk36/SB_EC_RKP.pem +++ b/testdata/caiman/sdk36/SB_EC_RKP.pem @@ -1,15 +1,94 @@ -----BEGIN CERTIFICATE----- -MIIC+TCCAqCgAwIBAgIBATAKBggqhkjOPQQDAjA/MSkwJwYDVQQDEyA2NWQyOTQ5NTM2OTI0ZGE2OTVmNWFlMWViMjkwY2Q0ZDESMBAGA1UEChMJU3Ryb25nQm94MB4XDTcwMDEwMTAwMDAwMFoXDTQ4MDEwMTAwMDAwMFowHzEdMBsGA1UEAxMUQW5kcm9pZCBLZXlzdG9yZSBLZXkwWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAAT4aXtujlYt/PUkhSD7ksPGhJFpwpXSQV5HG2gEiypcbPR16uSar055ljQ92LpRQ33poq2IDPsvDivCwN5ujdWio4IBqzCCAacwggGTBgorBgEEAdZ5AgERBIIBgzCCAX8CAgEsCgECAgIBLAoBAgQkN2NjYWMxZWEtNDg0NS00ODJlLTg1OGQtZjZmYTlhYThjMjk1BAAwX7+FPQgCBgGZhqXxt7+FRU8ETTBLMSUwIwQeY29tLmdvb2dsZS5hbmRyb2lkLmF0dGVzdGF0aW9uAgEAMSIEIBA5OO5FN+WejueS9lRQT7g0b8azRtC7xEFfwzn8/I7BMIHloQgxBgIBAgIBA6IDAgEDowQCAgEApQUxAwIBBKoDAgEBv4N3AgUAv4U+AwIBAL+FQEwwSgQgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAf8KAQAEIAaiOSW2VH7BJAhspe3dNcNfWM5utooTr9/UGVxBxh7Uv4VBBQIDAnEAv4VCBQIDAxcPv4VGCAQGZ29vZ2xlv4VHCAQGY2FpbWFuv4VICAQGY2FpbWFuv4VMCAQGR29vZ2xlv4VNDQQLUGl4ZWwgOSBQcm+/hU4GAgQBNQHhv4VPBgIEATUB4TAOBgNVHQ8BAf8EBAMCB4AwCgYIKoZIzj0EAwIDRwAwRAIgZA8b/m1jeVo83eBT5QN4MssWQmrmZCz++HktkJP20lcCIC8Y1KaQHHd75ZYe1TiiBXTGIPJjXEOr0zF+yZPZVXod +MIIC+TCCAqCgAwIBAgIBATAKBggqhkjOPQQDAjA/MSkwJwYDVQQDEyA2NWQyOTQ5 +NTM2OTI0ZGE2OTVmNWFlMWViMjkwY2Q0ZDESMBAGA1UEChMJU3Ryb25nQm94MB4X +DTcwMDEwMTAwMDAwMFoXDTQ4MDEwMTAwMDAwMFowHzEdMBsGA1UEAxMUQW5kcm9p +ZCBLZXlzdG9yZSBLZXkwWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAAT4aXtujlYt +/PUkhSD7ksPGhJFpwpXSQV5HG2gEiypcbPR16uSar055ljQ92LpRQ33poq2IDPsv +DivCwN5ujdWio4IBqzCCAacwggGTBgorBgEEAdZ5AgERBIIBgzCCAX8CAgEsCgEC +AgIBLAoBAgQkN2NjYWMxZWEtNDg0NS00ODJlLTg1OGQtZjZmYTlhYThjMjk1BAAw +X7+FPQgCBgGZhqXxt7+FRU8ETTBLMSUwIwQeY29tLmdvb2dsZS5hbmRyb2lkLmF0 +dGVzdGF0aW9uAgEAMSIEIBA5OO5FN+WejueS9lRQT7g0b8azRtC7xEFfwzn8/I7B +MIHloQgxBgIBAgIBA6IDAgEDowQCAgEApQUxAwIBBKoDAgEBv4N3AgUAv4U+AwIB +AL+FQEwwSgQgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAf8KAQAE +IAaiOSW2VH7BJAhspe3dNcNfWM5utooTr9/UGVxBxh7Uv4VBBQIDAnEAv4VCBQID +AxcPv4VGCAQGZ29vZ2xlv4VHCAQGY2FpbWFuv4VICAQGY2FpbWFuv4VMCAQGR29v +Z2xlv4VNDQQLUGl4ZWwgOSBQcm+/hU4GAgQBNQHhv4VPBgIEATUB4TAOBgNVHQ8B +Af8EBAMCB4AwCgYIKoZIzj0EAwIDRwAwRAIgZA8b/m1jeVo83eBT5QN4MssWQmrm +ZCz++HktkJP20lcCIC8Y1KaQHHd75ZYe1TiiBXTGIPJjXEOr0zF+yZPZVXod -----END CERTIFICATE----- -----BEGIN CERTIFICATE----- -MIIB6TCCAY6gAwIBAgIQZdKUlTaSTaaV9a4espDNTTAKBggqhkjOPQQDAjApMRMwEQYDVQQKEwpHb29nbGUgTExDMRIwEAYDVQQDEwlEcm9pZCBDQTMwHhcNMjUwOTI0MTUzMDQ1WhcNMjUxMDAzMTUzMDQ1WjA/MSkwJwYDVQQDEyA2NWQyOTQ5NTM2OTI0ZGE2OTVmNWFlMWViMjkwY2Q0ZDESMBAGA1UEChMJU3Ryb25nQm94MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEm1mmFF0yUkistkOE8Iey0/6ClW4hoRDNIZ1PlzZ2pDZgmYbQVZ2CmdAtSnQjfS5CTr7twH5N4LMnt2Rbz1HHMaOBgTB/MB0GA1UdDgQWBBR2Kn1Fi+hIfz7pHAwdoStbn7RvGjAfBgNVHSMEGDAWgBQXNQhYsE5H95CTt5U2kJWgm8CuyzAPBgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwICBDAcBgorBgEEAdZ5AgEeBA6jARggAvUDZkdvb2dsZTAKBggqhkjOPQQDAgNJADBGAiEAkW8to5Eb6+LHTtWuc7oQhtgtg3wbiBLohn6C585AovcCIQDaDZWgWbQfffged8h8hdOt6j3QiMwVWRjf1iLNq553zA== +MIIB6TCCAY6gAwIBAgIQZdKUlTaSTaaV9a4espDNTTAKBggqhkjOPQQDAjApMRMw +EQYDVQQKEwpHb29nbGUgTExDMRIwEAYDVQQDEwlEcm9pZCBDQTMwHhcNMjUwOTI0 +MTUzMDQ1WhcNMjUxMDAzMTUzMDQ1WjA/MSkwJwYDVQQDEyA2NWQyOTQ5NTM2OTI0 +ZGE2OTVmNWFlMWViMjkwY2Q0ZDESMBAGA1UEChMJU3Ryb25nQm94MFkwEwYHKoZI +zj0CAQYIKoZIzj0DAQcDQgAEm1mmFF0yUkistkOE8Iey0/6ClW4hoRDNIZ1PlzZ2 +pDZgmYbQVZ2CmdAtSnQjfS5CTr7twH5N4LMnt2Rbz1HHMaOBgTB/MB0GA1UdDgQW +BBR2Kn1Fi+hIfz7pHAwdoStbn7RvGjAfBgNVHSMEGDAWgBQXNQhYsE5H95CTt5U2 +kJWgm8CuyzAPBgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwICBDAcBgorBgEE +AdZ5AgEeBA6jARggAvUDZkdvb2dsZTAKBggqhkjOPQQDAgNJADBGAiEAkW8to5Eb +6+LHTtWuc7oQhtgtg3wbiBLohn6C585AovcCIQDaDZWgWbQfffged8h8hdOt6j3Q +iMwVWRjf1iLNq553zA== -----END CERTIFICATE----- -----BEGIN CERTIFICATE----- -MIIB1zCCAV2gAwIBAgIUAPWcCuKdBClmeztF0xn6DWTJkZQwCgYIKoZIzj0EAwMwKTETMBEGA1UEChMKR29vZ2xlIExMQzESMBAGA1UEAxMJRHJvaWQgQ0EyMB4XDTI1MDkyNTIyNTMwOFoXDTI1MTIwNDIyNTMwN1owKTETMBEGA1UEChMKR29vZ2xlIExMQzESMBAGA1UEAxMJRHJvaWQgQ0EzMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAESLbTU2zRJtolshOA/8Z7RaSfeoiqMMJne9le0h1QUILTT+mUwzffXKGZ+9BwhnvhNmIxql3ph1LQpTVOpJdEy6NjMGEwDgYDVR0PAQH/BAQDAgIEMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFBc1CFiwTkf3kJO3lTaQlaCbwK7LMB8GA1UdIwQYMBaAFLv4Nq2Jrmzi5Z6U8NWy19J65HxBMAoGCCqGSM49BAMDA2gAMGUCMCxuhhEnEvVOANjpvt8oZAdxnzOw1kv2/idFtTo0fIN/6jibS9eAwkss0FCOFW8Q2AIxAKm6vAxW+gjOJ806LguNGgQ2E40teabFsyW5EPl71Hp5gFjK4QxdOHpIMb+3brmxrQ== +MIIB1zCCAV2gAwIBAgIUAPWcCuKdBClmeztF0xn6DWTJkZQwCgYIKoZIzj0EAwMw +KTETMBEGA1UEChMKR29vZ2xlIExMQzESMBAGA1UEAxMJRHJvaWQgQ0EyMB4XDTI1 +MDkyNTIyNTMwOFoXDTI1MTIwNDIyNTMwN1owKTETMBEGA1UEChMKR29vZ2xlIExM +QzESMBAGA1UEAxMJRHJvaWQgQ0EzMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE +SLbTU2zRJtolshOA/8Z7RaSfeoiqMMJne9le0h1QUILTT+mUwzffXKGZ+9Bwhnvh +NmIxql3ph1LQpTVOpJdEy6NjMGEwDgYDVR0PAQH/BAQDAgIEMA8GA1UdEwEB/wQF +MAMBAf8wHQYDVR0OBBYEFBc1CFiwTkf3kJO3lTaQlaCbwK7LMB8GA1UdIwQYMBaA +FLv4Nq2Jrmzi5Z6U8NWy19J65HxBMAoGCCqGSM49BAMDA2gAMGUCMCxuhhEnEvVO +ANjpvt8oZAdxnzOw1kv2/idFtTo0fIN/6jibS9eAwkss0FCOFW8Q2AIxAKm6vAxW ++gjOJ806LguNGgQ2E40teabFsyW5EPl71Hp5gFjK4QxdOHpIMb+3brmxrQ== -----END CERTIFICATE----- -----BEGIN CERTIFICATE----- -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 -----END CERTIFICATE----- -----BEGIN CERTIFICATE----- -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 +ex0SdDrx+tWUDqG8At2JHA== -----END CERTIFICATE----- \ No newline at end of file diff --git a/testdata/caiman/sdk36/TEE_EC_RKP.pem b/testdata/caiman/sdk36/TEE_EC_RKP.pem index dad0d39..b5a863b 100644 --- a/testdata/caiman/sdk36/TEE_EC_RKP.pem +++ b/testdata/caiman/sdk36/TEE_EC_RKP.pem @@ -1,15 +1,95 @@ -----BEGIN CERTIFICATE----- -MIIDGzCCAsGgAwIBAgIBATAKBggqhkjOPQQDAjA5MSkwJwYDVQQDEyBmMTY1ODQ5ZWYwOGI0NjU4ZGQwYThhYjk1YmU1MzAwNjEMMAoGA1UEChMDVEVFMB4XDTcwMDEwMTAwMDAwMFoXDTQ4MDEwMTAwMDAwMFowHzEdMBsGA1UEAxMUQW5kcm9pZCBLZXlzdG9yZSBLZXkwWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAAT6bLfF+PF+L/HsMoOx12w4NIbCX6Fq1Gg6UB/qCDX1IBxdLr8sbFb2yUqCXaplKBKzIs1r4XDpNk3tJg/90dnqo4IB0jCCAc4wggG6BgorBgEEAdZ5AgERBIIBqjCCAaYCAgGQCgEBAgIBkAoBAQQkZDY4OGQ3NjMtNjExOC00Y2E2LTk0YjItZTZjZDllZDdlNGU0BAAwgYW/hT0IAgYBmYameQS/hUVPBE0wSzElMCMEHmNvbS5nb29nbGUuYW5kcm9pZC5hdHRlc3RhdGlvbgIBADEiBCAQOTjuRTflno7nkvZUUE+4NG/Gs0bQu8RBX8M5/PyOwb+FVCIEIBvKF+5u4Uh7X6ghXXADv2pKNjJwPSo6AlI3I1um/d5hMIHloQgxBgIBAgIBA6IDAgEDowQCAgEApQUxAwIBBKoDAgEBv4N3AgUAv4U+AwIBAL+FQEwwSgQgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAf8KAQAEIAaiOSW2VH7BJAhspe3dNcNfWM5utooTr9/UGVxBxh7Uv4VBBQIDAnEAv4VCBQIDAxcPv4VGCAQGZ29vZ2xlv4VHCAQGY2FpbWFuv4VICAQGY2FpbWFuv4VMCAQGR29vZ2xlv4VNDQQLUGl4ZWwgOSBQcm+/hU4GAgQBNQHhv4VPBgIEATUB4TAOBgNVHQ8BAf8EBAMCB4AwCgYIKoZIzj0EAwIDSAAwRQIhALnjvaKmThTUE2FYIelBRAbVFhHEUVepNWwsiO89hOWyAiAUNRIISFPVhzDmxJ3Kg+7WZuZhvqCbmIRNEc9T2Wj5+w== +MIIDGzCCAsGgAwIBAgIBATAKBggqhkjOPQQDAjA5MSkwJwYDVQQDEyBmMTY1ODQ5 +ZWYwOGI0NjU4ZGQwYThhYjk1YmU1MzAwNjEMMAoGA1UEChMDVEVFMB4XDTcwMDEw +MTAwMDAwMFoXDTQ4MDEwMTAwMDAwMFowHzEdMBsGA1UEAxMUQW5kcm9pZCBLZXlz +dG9yZSBLZXkwWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAAT6bLfF+PF+L/HsMoOx +12w4NIbCX6Fq1Gg6UB/qCDX1IBxdLr8sbFb2yUqCXaplKBKzIs1r4XDpNk3tJg/9 +0dnqo4IB0jCCAc4wggG6BgorBgEEAdZ5AgERBIIBqjCCAaYCAgGQCgEBAgIBkAoB +AQQkZDY4OGQ3NjMtNjExOC00Y2E2LTk0YjItZTZjZDllZDdlNGU0BAAwgYW/hT0I +AgYBmYameQS/hUVPBE0wSzElMCMEHmNvbS5nb29nbGUuYW5kcm9pZC5hdHRlc3Rh +dGlvbgIBADEiBCAQOTjuRTflno7nkvZUUE+4NG/Gs0bQu8RBX8M5/PyOwb+FVCIE +IBvKF+5u4Uh7X6ghXXADv2pKNjJwPSo6AlI3I1um/d5hMIHloQgxBgIBAgIBA6ID +AgEDowQCAgEApQUxAwIBBKoDAgEBv4N3AgUAv4U+AwIBAL+FQEwwSgQgAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAf8KAQAEIAaiOSW2VH7BJAhspe3d +NcNfWM5utooTr9/UGVxBxh7Uv4VBBQIDAnEAv4VCBQIDAxcPv4VGCAQGZ29vZ2xl +v4VHCAQGY2FpbWFuv4VICAQGY2FpbWFuv4VMCAQGR29vZ2xlv4VNDQQLUGl4ZWwg +OSBQcm+/hU4GAgQBNQHhv4VPBgIEATUB4TAOBgNVHQ8BAf8EBAMCB4AwCgYIKoZI +zj0EAwIDSAAwRQIhALnjvaKmThTUE2FYIelBRAbVFhHEUVepNWwsiO89hOWyAiAU +NRIISFPVhzDmxJ3Kg+7WZuZhvqCbmIRNEc9T2Wj5+w== -----END CERTIFICATE----- -----BEGIN CERTIFICATE----- -MIIB4zCCAYmgAwIBAgIRAPFlhJ7wi0ZY3QqKuVvlMAYwCgYIKoZIzj0EAwIwKTETMBEGA1UEChMKR29vZ2xlIExMQzESMBAGA1UEAxMJRHJvaWQgQ0EzMB4XDTI1MDkyNDE1MzExOVoXDTI1MTAwMzE1MzExOVowOTEpMCcGA1UEAxMgZjE2NTg0OWVmMDhiNDY1OGRkMGE4YWI5NWJlNTMwMDYxDDAKBgNVBAoTA1RFRTBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABMXjddGHbvIEnm/KY0eiet1Mb3/PwyTRf4hvnd7FCZh6x1Vx3KWmWosHt3Q+0h/OjSE84sNSPCXY5WL+a+Hs57OjgYEwfzAdBgNVHQ4EFgQUtOjdcSqT5PMzMGmKK9o8FGuGiL4wHwYDVR0jBBgwFoAUutLawp4XdVbW8jDTgo17DIffDLUwDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAgQwHAYKKwYBBAHWeQIBHgQOowEYQAL1A2ZHb29nbGUwCgYIKoZIzj0EAwIDSAAwRQIhAJI+ghAJeQWKL+1WQrPM9xa8gJWFd2XMDanE3l18k7GkAiBpAmE4CuZAeFdMCjTA9GyKrY1W3K5LJ1s+GeUWBbO7XQ== +MIIB4zCCAYmgAwIBAgIRAPFlhJ7wi0ZY3QqKuVvlMAYwCgYIKoZIzj0EAwIwKTET +MBEGA1UEChMKR29vZ2xlIExMQzESMBAGA1UEAxMJRHJvaWQgQ0EzMB4XDTI1MDky +NDE1MzExOVoXDTI1MTAwMzE1MzExOVowOTEpMCcGA1UEAxMgZjE2NTg0OWVmMDhi +NDY1OGRkMGE4YWI5NWJlNTMwMDYxDDAKBgNVBAoTA1RFRTBZMBMGByqGSM49AgEG +CCqGSM49AwEHA0IABMXjddGHbvIEnm/KY0eiet1Mb3/PwyTRf4hvnd7FCZh6x1Vx +3KWmWosHt3Q+0h/OjSE84sNSPCXY5WL+a+Hs57OjgYEwfzAdBgNVHQ4EFgQUtOjd +cSqT5PMzMGmKK9o8FGuGiL4wHwYDVR0jBBgwFoAUutLawp4XdVbW8jDTgo17DIff +DLUwDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAgQwHAYKKwYBBAHWeQIB +HgQOowEYQAL1A2ZHb29nbGUwCgYIKoZIzj0EAwIDSAAwRQIhAJI+ghAJeQWKL+1W +QrPM9xa8gJWFd2XMDanE3l18k7GkAiBpAmE4CuZAeFdMCjTA9GyKrY1W3K5LJ1s+ +GeUWBbO7XQ== -----END CERTIFICATE----- -----BEGIN CERTIFICATE----- -MIIB1zCCAV2gAwIBAgIUAO10hmNysHkc8UeLOfrQ91VZOtMwCgYIKoZIzj0EAwMwKTETMBEGA1UEChMKR29vZ2xlIExMQzESMBAGA1UEAxMJRHJvaWQgQ0EyMB4XDTI1MDkyNTE3MTMwMloXDTI1MTIwNDE3MTMwMVowKTETMBEGA1UEChMKR29vZ2xlIExMQzESMBAGA1UEAxMJRHJvaWQgQ0EzMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE8VRCMsPUXUvH89X9fyVSDuvtQ96mqHah3/9DEP8mVPWlWJByYezjHM9eV8ZGYCmA46zT5YTQHHimgDVYwtTpg6NjMGEwDgYDVR0PAQH/BAQDAgIEMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFLrS2sKeF3VW1vIw04KNewyH3wy1MB8GA1UdIwQYMBaAFLv4Nq2Jrmzi5Z6U8NWy19J65HxBMAoGCCqGSM49BAMDA2gAMGUCMFCdzj76ChMqxLRWHQCZIifu/UjzXlL+43gCkME8Msfdwx7bWs83q958TNhztyW5CQIxANt/PMwxizHfWvLrEM2ztgEMt/ocuhxV/M9NtsB9YqsjpOow0wVN/ouT90XxuVMCrA== +MIIB1zCCAV2gAwIBAgIUAO10hmNysHkc8UeLOfrQ91VZOtMwCgYIKoZIzj0EAwMw +KTETMBEGA1UEChMKR29vZ2xlIExMQzESMBAGA1UEAxMJRHJvaWQgQ0EyMB4XDTI1 +MDkyNTE3MTMwMloXDTI1MTIwNDE3MTMwMVowKTETMBEGA1UEChMKR29vZ2xlIExM +QzESMBAGA1UEAxMJRHJvaWQgQ0EzMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE +8VRCMsPUXUvH89X9fyVSDuvtQ96mqHah3/9DEP8mVPWlWJByYezjHM9eV8ZGYCmA +46zT5YTQHHimgDVYwtTpg6NjMGEwDgYDVR0PAQH/BAQDAgIEMA8GA1UdEwEB/wQF +MAMBAf8wHQYDVR0OBBYEFLrS2sKeF3VW1vIw04KNewyH3wy1MB8GA1UdIwQYMBaA +FLv4Nq2Jrmzi5Z6U8NWy19J65HxBMAoGCCqGSM49BAMDA2gAMGUCMFCdzj76ChMq +xLRWHQCZIifu/UjzXlL+43gCkME8Msfdwx7bWs83q958TNhztyW5CQIxANt/PMwx +izHfWvLrEM2ztgEMt/ocuhxV/M9NtsB9YqsjpOow0wVN/ouT90XxuVMCrA== -----END CERTIFICATE----- -----BEGIN CERTIFICATE----- -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 -----END CERTIFICATE----- -----BEGIN CERTIFICATE----- -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 +ex0SdDrx+tWUDqG8At2JHA== -----END CERTIFICATE----- \ No newline at end of file From a1bf5ab6ea068ad9c12325cf7caa6b6c93f183a6 Mon Sep 17 00:00:00 2001 From: Seth Moore Date: Tue, 14 Apr 2026 09:24:26 -0700 Subject: [PATCH 03/48] Make ConstraintConfig more generic by parameterizing the constrained attribute This is the first step toward making the constraints truly generic. Instead of making each constraint take a specific attribute, make all the constraints accept common types. The contraints are them customize to extract the constrained attributes using mapping functions. PiperOrigin-RevId: 899625172 --- src/main/kotlin/ConstraintConfig.kt | 212 +++++++++++++++++------- src/main/kotlin/KeyAttestationReason.kt | 18 +- src/main/kotlin/Verifier.kt | 39 ++--- src/test/kotlin/ConstraintConfigTest.kt | 117 ++++++++----- src/test/kotlin/VerifierTest.kt | 49 +++++- 5 files changed, 285 insertions(+), 150 deletions(-) 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 From cd45eb4988861dad4829290898d31eac162e6232 Mon Sep 17 00:00:00 2001 From: Carmen Hanish Date: Mon, 20 Apr 2026 05:00:41 -0700 Subject: [PATCH 04/48] Wrap CharacterCodingException. PiperOrigin-RevId: 902573134 --- src/main/kotlin/Extension.kt | 35 +++++++++++++++++++++-------------- 1 file changed, 21 insertions(+), 14 deletions(-) diff --git a/src/main/kotlin/Extension.kt b/src/main/kotlin/Extension.kt index 64c9272..87ebf43 100644 --- a/src/main/kotlin/Extension.kt +++ b/src/main/kotlin/Extension.kt @@ -58,10 +58,14 @@ import org.bouncycastle.asn1.DERSet import org.bouncycastle.asn1.DERTaggedObject import org.bouncycastle.asn1.x509.Extension -@Immutable @RequiresApi(24) -data class ExtensionParsingException(val msg: String, val reason: KeyAttestationReason? = null) : - Exception(msg) +data class ExtensionParsingException +@JvmOverloads +constructor( + override val message: String, + val reason: KeyAttestationReason? = null, + override val cause: Throwable? = null, +) : Exception(message, cause) @Immutable data class ProvisioningInfoMap( @@ -444,13 +448,12 @@ data class AuthorizationList( } fun from(seq: ASN1Sequence, logFn: (String) -> Unit = { _ -> }): AuthorizationList { - val objects = - seq.associate { - require(it is ASN1TaggedObject) { - "Must be an ASN1TaggedObject, was ${it::class.simpleName}" - } - KeyMintTag.from(it.tagNo) to it.explicitBaseObject + val objects = seq.associate { + require(it is ASN1TaggedObject) { + "Must be an ASN1TaggedObject, was ${it::class.simpleName}" } + KeyMintTag.from(it.tagNo) to it.explicitBaseObject + } /** * X.680 section 8.6 @@ -775,11 +778,15 @@ private inline fun ASN1Encodable.toSet(): Set { @RequiresApi(24) private fun ASN1Encodable.toStr() = - UTF_8.newDecoder() - .onMalformedInput(CodingErrorAction.REPORT) - .onUnmappableCharacter(CodingErrorAction.REPORT) - .decode(this.toByteBuffer()) - .toString() + try { + UTF_8.newDecoder() + .onMalformedInput(CodingErrorAction.REPORT) + .onUnmappableCharacter(CodingErrorAction.REPORT) + .decode(this.toByteBuffer()) + .toString() + } catch (e: CharacterCodingException) { + throw ExtensionParsingException("error decoding ASN.1: ${e.message}", cause = e) + } private fun ASN1Encodable.toTaggedObject(tag: KeyMintTag) = DERTaggedObject(tag.value, this) From 31fbda479cdf3e07b5ca6d7cebb84cb0fa955a85 Mon Sep 17 00:00:00 2001 From: Seth Moore Date: Mon, 20 Apr 2026 13:34:22 -0700 Subject: [PATCH 05/48] Ignore expiry checks for factory certificate chains Some of the oldest factory chains are about to expires, but there is not a practical mechanism to rotate. Given that, ignore the expiry on these certs. If we find some day that the certs are not trustworthy, we'll revoke them. PiperOrigin-RevId: 902798819 --- .../KeyAttestationCertPathValidator.kt | 4 ++ src/main/kotlin/testing/Certs.kt | 22 +++++++++- .../testing/KeyAttestationCertFactory.kt | 4 ++ src/main/kotlin/testing/X509CertificateExt.kt | 2 +- .../KeyAttestationCertPathValidatorTest.kt | 40 +++++++++++++++++-- 5 files changed, 65 insertions(+), 7 deletions(-) diff --git a/src/main/kotlin/provider/KeyAttestationCertPathValidator.kt b/src/main/kotlin/provider/KeyAttestationCertPathValidator.kt index e9a0857..bdba1f4 100644 --- a/src/main/kotlin/provider/KeyAttestationCertPathValidator.kt +++ b/src/main/kotlin/provider/KeyAttestationCertPathValidator.kt @@ -303,6 +303,10 @@ private class BasicChecker( BasicReason.NOT_YET_VALID, ) } catch (e: CertificateExpiredException) { + // Ignore validity on factory-provisioned certificate chains because it is not possible to + // safely rotate the keys. + if (certPath.provisioningMethod() == ProvisioningMethod.FACTORY_PROVISIONED) return + throw CertPathValidatorException( "Validity check failed", e, diff --git a/src/main/kotlin/testing/Certs.kt b/src/main/kotlin/testing/Certs.kt index 71f1de7..3f10567 100644 --- a/src/main/kotlin/testing/Certs.kt +++ b/src/main/kotlin/testing/Certs.kt @@ -481,9 +481,9 @@ object Chains { ) } - /* A chain where the attestation certificate has expired. */ + /* A factory-provisioned chain where the attestation certificate has expired. */ @JvmStatic - val expired by lazy { + val expiredFactoryProvisioned by lazy { KeyAttestationCertPath( certFactory.generateLeafCert(), certFactory.generateAttestationCert( @@ -495,6 +495,24 @@ object Chains { ) } + /* A remotely-provisioned chain where the attestation certificate has expired. */ + @JvmStatic + val expiredRemotelyProvisioned by lazy { + val rkpAttestationCert = + certFactory.generateRkpAttestationCert( + serialNumber = BigInteger.valueOf(0x1234567890), + notBefore = fakeCalendar.lastWeek(), + notAfter = fakeCalendar.lastWeek(), + ) + KeyAttestationCertPath( + certFactory.generateLeafCert(issuer = rkpAttestationCert.subject), + rkpAttestationCert, + certFactory.rkpIntermediate, + Certs.remoteIntermediate, + certFactory.root, + ) + } + /* A chain where the leaf certificate has expired. This will pass. */ val expiredLeaf by lazy { KeyAttestationCertPath( diff --git a/src/main/kotlin/testing/KeyAttestationCertFactory.kt b/src/main/kotlin/testing/KeyAttestationCertFactory.kt index 4529deb..d741c16 100644 --- a/src/main/kotlin/testing/KeyAttestationCertFactory.kt +++ b/src/main/kotlin/testing/KeyAttestationCertFactory.kt @@ -117,12 +117,16 @@ internal class KeyAttestationCertFactory(val fakeCalendar: FakeCalendar = FakeCa internal fun generateRkpAttestationCert( securityLevel: SecurityLevel = SecurityLevel.TRUSTED_ENVIRONMENT, serialNumber: BigInteger, + notBefore: Date = fakeCalendar.lastWeek(), + notAfter: Date = fakeCalendar.nextWeek(), ) = generateAttestationCert( signingKey = rkpKey.private, subject = rkpAttestationName(securityLevel, serialNumber), issuer = rkpIntermediate.subject, serialNumber, + notBefore, + notAfter, extraExtension = Extension( ProvisioningInfoMap.OID, diff --git a/src/main/kotlin/testing/X509CertificateExt.kt b/src/main/kotlin/testing/X509CertificateExt.kt index 97a7abe..087df80 100644 --- a/src/main/kotlin/testing/X509CertificateExt.kt +++ b/src/main/kotlin/testing/X509CertificateExt.kt @@ -23,5 +23,5 @@ import org.bouncycastle.cert.jcajce.JcaX500NameUtil private fun X500Principal.asX500Name() = JcaX500NameUtil.getX500Name(this) -internal val X509Certificate.subject: X500Name +val X509Certificate.subject: X500Name get() = subjectX500Principal.asX500Name() diff --git a/src/test/kotlin/provider/KeyAttestationCertPathValidatorTest.kt b/src/test/kotlin/provider/KeyAttestationCertPathValidatorTest.kt index afb9f56..f683a68 100644 --- a/src/test/kotlin/provider/KeyAttestationCertPathValidatorTest.kt +++ b/src/test/kotlin/provider/KeyAttestationCertPathValidatorTest.kt @@ -22,6 +22,7 @@ import com.android.keyattestation.verifier.testing.Certs.rootAnchor as testAncho import com.android.keyattestation.verifier.testing.Chains import com.android.keyattestation.verifier.testing.FakeCalendar import com.android.keyattestation.verifier.testing.TestUtils.prodAnchors +import com.android.keyattestation.verifier.testing.TestUtils.readCertPath import com.google.common.truth.Truth.assertThat import java.security.InvalidAlgorithmParameterException import java.security.Security @@ -34,6 +35,8 @@ import java.security.cert.PKIXCertPathChecker import java.security.cert.PKIXCertPathValidatorResult import java.security.cert.PKIXParameters import java.security.cert.PKIXReason +import java.security.cert.TrustAnchor +import java.time.LocalDate import kotlin.test.assertFailsWith import org.junit.BeforeClass import org.junit.Test @@ -84,14 +87,14 @@ class KeyAttestationCertPathValidatorTest { val exception = assertFailsWith { certPathValidator.validate( - Chains.validFactoryProvisioned, + Chains.validRemotelyProvisioned, PKIXParameters(setOf(testAnchor)), ) } val pkixException = assertFailsWith { pkixCertPathValidator.validate( - Chains.validFactoryProvisioned, + Chains.validRemotelyProvisioned, PKIXParameters(setOf(testAnchor)), ) } @@ -205,8 +208,13 @@ class KeyAttestationCertPathValidatorTest { } @Test - fun expired_throwsCertPathValidatorException() { - val certPath = Chains.expired + fun expiredFactory_succeeds() { + certPathValidator.validate(Chains.expiredFactoryProvisioned, testParams) + } + + @Test + fun expiredRkp_throwsCertPathValidatorException() { + val certPath = Chains.expiredRemotelyProvisioned val exception = assertFailsWith { certPathValidator.validate(certPath, testParams) @@ -214,6 +222,30 @@ class KeyAttestationCertPathValidatorTest { assertThat(exception.reason).isEqualTo(BasicReason.EXPIRED) } + @Test + fun bluelineSdk28_factoryProvisioned_expiryIgnored() { + val certPath = readCertPath("blueline/sdk28/TEE_EC_NONE.pem") + val root = certPath.certificatesWithAnchor.last() + val params = + PKIXParameters(setOf(TrustAnchor(root, null))).apply { + date = FakeCalendar(LocalDate.of(2030, 1, 1)).today() + } + certPathValidator.validate(certPath, params) + } + + @Test + fun caimanSdk36_remoteProvisioned_expiryHonored() { + val certPath = readCertPath("caiman/sdk36/TEE_EC_RKP.pem") + val root = certPath.certificatesWithAnchor.last() + val params = + PKIXParameters(setOf(TrustAnchor(root, null))).apply { + date = FakeCalendar(LocalDate.of(2030, 1, 1)).today() + } + val exception = + assertFailsWith { certPathValidator.validate(certPath, params) } + assertThat(exception.reason).isEqualTo(BasicReason.EXPIRED) + } + @Test fun forgedKeybox_throwsCertPathValidatorException() { val certPath = Chains.forgedKeybox From f39ec0d5b15cdb4c0aad1e856fdb5bb05f2c8256 Mon Sep 17 00:00:00 2001 From: Suzanna Jiwani Date: Tue, 21 Apr 2026 11:36:42 -0700 Subject: [PATCH 06/48] Add example authlist with all extensions parsed PiperOrigin-RevId: 903340853 --- testdata/complete_authlist.json | 82 +++++++++++++++++++++++++++++++++ 1 file changed, 82 insertions(+) create mode 100644 testdata/complete_authlist.json diff --git a/testdata/complete_authlist.json b/testdata/complete_authlist.json new file mode 100644 index 0000000..172a972 --- /dev/null +++ b/testdata/complete_authlist.json @@ -0,0 +1,82 @@ +{ + "attestationVersion": "400", + "attestationSecurityLevel": "TRUSTED_ENVIRONMENT", + "keyMintVersion": "400", + "keyMintSecurityLevel": "TRUSTED_ENVIRONMENT", + "attestationChallenge": "Y2hhbGxlbmdl", + "uniqueId": "dW5pcXVlSWQ=", + "softwareEnforced": { + "purposes": ["1", "2"], + "algorithms": "1", + "keySize": "2048", + "blockModes": ["1", "2"], + "digests": ["1", "2"], + "paddings": ["1", "2"], + "ecCurve": "10", + "rsaPublicExponent": "65537", + "rsaOaepMgfDigests": ["1", "2"], + "activeDateTime": "1000", + "originationExpireDateTime": "2000", + "usageExpireDateTime": "3000", + "noAuthRequired": true, + "userAuthType": "1", + "authTimeout": "60", + "allowWhileOnBody": true, + "trustedUserPresenceRequired": true, + "unlockedDeviceRequired": true, + "creationDateTime": "123456789", + "origin": "GENERATED", + "rollbackResistant": true, + "rootOfTrust": { + "verifiedBootKey": "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=", + "deviceLocked": true, + "verifiedBootState": "VERIFIED", + "verifiedBootHash": "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=" + }, + "osVersion": "110000", + "osPatchLevel": "202101", + "attestationApplicationId": { + "packages": [ + {"name": "package1", "version": "1"} + ], + "signatures": ["c2lnbmF0dXJlMQ=="] + }, + "attestationIdBrand": "brand", + "attestationIdDevice": "device", + "attestationIdProduct": "product", + "attestationIdSerial": "serial", + "attestationIdImei": "imei", + "attestationIdMeid": "meid", + "attestationIdManufacturer": "manufacturer", + "attestationIdModel": "model", + "vendorPatchLevel": "20210101", + "bootPatchLevel": "20210101", + "attestationIdSecondImei": "secondImei", + "moduleHash": "bW9kdWxlSGFzaA==" + }, + "hardwareEnforced": { + "purposes": ["2"], + "algorithms": "1", + "keySize": "2048", + "rsaPublicExponent": "65537", + "noAuthRequired": true, + "origin": "GENERATED", + "rootOfTrust": { + "verifiedBootKey": "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=", + "deviceLocked": false, + "verifiedBootState": "UNVERIFIED", + "verifiedBootHash": "iCWIV2R1rsyzkpgv4vvF9ixpyfyEunPmxTzAUqEWFYY=" + }, + "osVersion": "140000", + "osPatchLevel": "202408", + "attestationIdBrand": "google", + "attestationIdDevice": "akita", + "attestationIdProduct": "akita", + "attestationIdImei": "351163520096208", + "attestationIdManufacturer": "Google", + "attestationIdModel": "Pixel 8a", + "vendorPatchLevel": "20240805", + "bootPatchLevel": "20240805", + "attestationIdSecondImei": "351163520096216" + } +} From 8ebb29078d27007ddfa65999717ddea555295483 Mon Sep 17 00:00:00 2001 From: Android HW Trust Team Date: Tue, 5 May 2026 13:22:51 -0700 Subject: [PATCH 07/48] Add `deviceLocked` to `VerificationResult.Success` PiperOrigin-RevId: 910876241 --- src/main/kotlin/Verifier.kt | 3 +++ src/test/kotlin/VerifierTest.kt | 2 ++ 2 files changed, 5 insertions(+) diff --git a/src/main/kotlin/Verifier.kt b/src/main/kotlin/Verifier.kt index 8b83a49..ed38e6d 100644 --- a/src/main/kotlin/Verifier.kt +++ b/src/main/kotlin/Verifier.kt @@ -48,6 +48,7 @@ sealed interface VerificationResult { val challenge: ByteString, val securityLevel: SecurityLevel, val verifiedBootState: VerifiedBootState, + val deviceLocked: Boolean, val deviceInformation: ProvisioningInfoMap?, val attestedDeviceIds: DeviceIdentity, ) : VerificationResult @@ -305,12 +306,14 @@ constructor( minOf(keyDescription.attestationSecurityLevel, keyDescription.keyMintSecurityLevel) val rootOfTrust = keyDescription.hardwareEnforced.rootOfTrust val verifiedBootState = rootOfTrust?.verifiedBootState ?: VerifiedBootState.UNVERIFIED + val deviceLocked = rootOfTrust?.deviceLocked ?: false return VerificationResult.Success( pathValidationResult.publicKey, keyDescription.attestationChallenge, securityLevel, verifiedBootState, + deviceLocked, deviceInformation, DeviceIdentity.parseFrom(keyDescription), ) diff --git a/src/test/kotlin/VerifierTest.kt b/src/test/kotlin/VerifierTest.kt index 7b88a06..7cd9eb1 100644 --- a/src/test/kotlin/VerifierTest.kt +++ b/src/test/kotlin/VerifierTest.kt @@ -82,6 +82,8 @@ class VerifierTest { assertThat(result.securityLevel).isEqualTo(json.attestationSecurityLevel) assertThat(result.verifiedBootState) .isEqualTo(json.hardwareEnforced.rootOfTrust?.verifiedBootState) + assertThat(result.deviceLocked) + .isEqualTo(json.hardwareEnforced.rootOfTrust?.deviceLocked ?: false) } enum class TestCase(val path: String, val timestamp: Instant) { From 0683504bbc70aabda918087949bf43e4b403ecfb Mon Sep 17 00:00:00 2001 From: Suzanna Jiwani Date: Wed, 6 May 2026 08:28:57 -0700 Subject: [PATCH 08/48] Fix README code examples + Extension.kt error messages Patched from https://github.com/android/keyattestation/pull/78, originally authored by @thomasbuilds PiperOrigin-RevId: 911352815 --- README.md | 11 ++++++++--- src/main/kotlin/Extension.kt | 6 ++++-- 2 files changed, 12 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 506a6f5..97b2651 100644 --- a/README.md +++ b/README.md @@ -40,7 +40,7 @@ when verifying. For example, if you expect the challenge to be equal to ```kotlin // Create a ChallengeChecker -val challengeChecker = ChallengeMatcher("challenge123") +val challengeChecker = ChallengeMatcher(ByteString.copyFromUtf8("challenge123")) // Verify an attestation certificate chain with the checker val result = verifier.verify(certificateChain, challengeChecker) @@ -57,9 +57,14 @@ with an `InMemoryLruCache` like in this sample: ```kotlin val cacheSize = 100 -// Create a ChainedChallengeChecker with desired ChallengeCheckers +// Create a ChainedChallengeChecker with desired ChallengeCheckers. The +// coroutineScope is used to run each individual checker. val challengeChecker = - ChainedChallengeChecker.of(ChallengeMatcher("expectedChallenge"), InMemoryLruCache(cacheSize)) + ChainedChallengeChecker.of( + coroutineScope, + ChallengeMatcher(ByteString.copyFromUtf8("expectedChallenge")), + InMemoryLruCache(cacheSize), + ) // Verify an attestation certificate chain with the checker val result = verifier.verify(certificateChain, challengeChecker) diff --git a/src/main/kotlin/Extension.kt b/src/main/kotlin/Extension.kt index 87ebf43..8f660c9 100644 --- a/src/main/kotlin/Extension.kt +++ b/src/main/kotlin/Extension.kt @@ -536,7 +536,9 @@ data class PatchLevel(val yearMonth: YearMonth, val version: Int? = null) { partitionName: String = "", logFn: (String) -> Unit = { _ -> }, ): PatchLevel? { - check(patchLevel is ASN1Integer) { "Must be an ASN1Integer, was ${this::class.simpleName}" } + check(patchLevel is ASN1Integer) { + "Must be an ASN1Integer, was ${patchLevel::class.simpleName}" + } return from(patchLevel.value.toString(), partitionName, logFn) } @@ -768,7 +770,7 @@ private inline fun ASN1Encodable.toSet(): Set { return this.map { if (it !is T) { throw ExtensionParsingException( - "Object must be a ${T::class.simpleName}, was ${this::class.simpleName}" + "Object must be a ${T::class.simpleName}, was ${it::class.simpleName}" ) } it From a83ff03aa1c1c03ab5090bfcdc352aa8ff5eeb60 Mon Sep 17 00:00:00 2001 From: Carmen Hanish Date: Wed, 13 May 2026 06:21:13 -0700 Subject: [PATCH 09/48] Add support for ML-DSA. PiperOrigin-RevId: 914825126 --- src/main/kotlin/CertPrinter.kt | 13 +++ src/main/kotlin/Extension.kt | 4 + src/test/kotlin/ExtensionTest.kt | 1 + testdata/complete_authlist.json | 1 + testdata/tokay/sdk37/TEE_MLDSA_FACTORY.json | 36 ++++++ testdata/tokay/sdk37/TEE_MLDSA_FACTORY.pem | 121 ++++++++++++++++++++ testdata/tokay/sdk37/TEE_MLDSA_RKP.json | 36 ++++++ testdata/tokay/sdk37/TEE_MLDSA_RKP.pem | 116 +++++++++++++++++++ 8 files changed, 328 insertions(+) create mode 100644 testdata/tokay/sdk37/TEE_MLDSA_FACTORY.json create mode 100644 testdata/tokay/sdk37/TEE_MLDSA_FACTORY.pem create mode 100644 testdata/tokay/sdk37/TEE_MLDSA_RKP.json create mode 100644 testdata/tokay/sdk37/TEE_MLDSA_RKP.pem diff --git a/src/main/kotlin/CertPrinter.kt b/src/main/kotlin/CertPrinter.kt index 73c4893..0f0e9fd 100644 --- a/src/main/kotlin/CertPrinter.kt +++ b/src/main/kotlin/CertPrinter.kt @@ -80,6 +80,16 @@ enum class EcCurve(val value: Int) { } } +/** Representation of the MLDsaVariant enum contained within [AuthorizationList]. */ +enum class MlDsaVariant(val value: Int) { + ML_DSA_65(1), + ML_DSA_87(2); + + companion object { + fun fromInt(value: Int) = values().firstOrNull { it.value == value } + } +} + /** * Representation of the BlockMode enum contained within [AuthorizationList]. * @@ -193,6 +203,7 @@ object CertPrinter { if (digests != null) appendIndentedLine("digests: ${digests?.toDigests()}") if (paddings != null) appendIndentedLine("paddings: $paddings") if (ecCurve != null) appendIndentedLine("ecCurve: ${ecCurve?.toEcCurve()}") + if (mlDsaVariant != null) appendIndentedLine("mlDsaVariant: ${mlDsaVariant.toMlDsaVariant()}") if (rsaPublicExponent != null) appendIndentedLine("rsaPublicExponent: $rsaPublicExponent") if (rsaOaepMgfDigests != null) { appendIndentedLine("rsaOaepMgfDigests: ${rsaOaepMgfDigests?.toDigests()}") @@ -290,6 +301,8 @@ object CertPrinter { private fun BigInteger.toEcCurve(): EcCurve? = EcCurve.fromInt(this.toInt()) + private fun BigInteger.toMlDsaVariant(): MlDsaVariant? = MlDsaVariant.fromInt(this.toInt()) + private fun BigInteger.toDateTime(): String = if (this.toLong() < 0L) "" else Instant.ofEpochMilli(this.toLong()).toString() } diff --git a/src/main/kotlin/Extension.kt b/src/main/kotlin/Extension.kt index 8f660c9..fcb531f 100644 --- a/src/main/kotlin/Extension.kt +++ b/src/main/kotlin/Extension.kt @@ -253,6 +253,7 @@ enum class KeyMintTag(val value: Int) { DIGEST(5), PADDING(6), EC_CURVE(10), + ML_DSA_VARIANT(11), RSA_PUBLIC_EXPONENT(200), RSA_OAEP_MGF_DIGEST(203), ACTIVE_DATE_TIME(400), @@ -310,6 +311,7 @@ data class AuthorizationList( @SuppressWarnings("Immutable") val digests: Set? = null, @SuppressWarnings("Immutable") val paddings: Set? = null, val ecCurve: BigInteger? = null, + val mlDsaVariant: BigInteger? = null, val rsaPublicExponent: BigInteger? = null, @SuppressWarnings("Immutable") val rsaOaepMgfDigests: Set? = null, val activeDateTime: BigInteger? = null, @@ -355,6 +357,7 @@ data class AuthorizationList( digests?.toAsn1()?.let { add(it.toTaggedObject(KeyMintTag.DIGEST)) } paddings?.toAsn1()?.let { add(it.toTaggedObject(KeyMintTag.PADDING)) } ecCurve?.toAsn1()?.let { add(it.toTaggedObject(KeyMintTag.EC_CURVE)) } + mlDsaVariant?.toAsn1()?.let { add(it.toTaggedObject(KeyMintTag.ML_DSA_VARIANT)) } rsaPublicExponent?.toAsn1()?.let { add(it.toTaggedObject(KeyMintTag.RSA_PUBLIC_EXPONENT)) } rsaOaepMgfDigests?.toAsn1()?.let { add(it.toTaggedObject(KeyMintTag.RSA_OAEP_MGF_DIGEST)) } activeDateTime?.toAsn1()?.let { add(it.toTaggedObject(KeyMintTag.ACTIVE_DATE_TIME)) } @@ -480,6 +483,7 @@ data class AuthorizationList( digests = converter.parseIntSet(KeyMintTag.DIGEST), paddings = converter.parseIntSet(KeyMintTag.PADDING), ecCurve = converter.parseInt(KeyMintTag.EC_CURVE), + mlDsaVariant = converter.parseInt(KeyMintTag.ML_DSA_VARIANT), rsaPublicExponent = converter.parseInt(KeyMintTag.RSA_PUBLIC_EXPONENT), rsaOaepMgfDigests = converter.parseIntSet(KeyMintTag.RSA_OAEP_MGF_DIGEST), activeDateTime = converter.parseInt(KeyMintTag.ACTIVE_DATE_TIME), diff --git a/src/test/kotlin/ExtensionTest.kt b/src/test/kotlin/ExtensionTest.kt index be7ebcb..275db1f 100644 --- a/src/test/kotlin/ExtensionTest.kt +++ b/src/test/kotlin/ExtensionTest.kt @@ -137,6 +137,7 @@ class ExtensionTest { digests = setOf(1.toBigInteger()), paddings = setOf(1.toBigInteger()), ecCurve = 3.toBigInteger(), + mlDsaVariant = 2.toBigInteger(), rsaPublicExponent = 4.toBigInteger(), activeDateTime = 5.toBigInteger(), originationExpireDateTime = 6.toBigInteger(), diff --git a/testdata/complete_authlist.json b/testdata/complete_authlist.json index 172a972..a3db9fa 100644 --- a/testdata/complete_authlist.json +++ b/testdata/complete_authlist.json @@ -13,6 +13,7 @@ "digests": ["1", "2"], "paddings": ["1", "2"], "ecCurve": "10", + "mlDsaVariant": "1", "rsaPublicExponent": "65537", "rsaOaepMgfDigests": ["1", "2"], "activeDateTime": "1000", diff --git a/testdata/tokay/sdk37/TEE_MLDSA_FACTORY.json b/testdata/tokay/sdk37/TEE_MLDSA_FACTORY.json new file mode 100644 index 0000000..fe23f29 --- /dev/null +++ b/testdata/tokay/sdk37/TEE_MLDSA_FACTORY.json @@ -0,0 +1,36 @@ +{ + "attestationVersion": "500", + "attestationSecurityLevel": "TRUSTED_ENVIRONMENT", + "keyMintVersion": "500", + "keyMintSecurityLevel": "TRUSTED_ENVIRONMENT", + "attestationChallenge": "Y2hhbGxlbmdl", + "uniqueId": "", + "softwareEnforced": { + "creationDateTime": "1777375215126", + "attestationApplicationId": { + "packages": [ + {"name":"android.keystore.cts", "version":"37" } + ], + "signatures": ["bOzFDjSuMb+1Z4mG1tbTc2xXHe0vJFlSd5Ph8FTrDJs="] + }, + "moduleHash": "FaidWkxztCor58kSH+BtPV6/tFSP0MSgkePA7fFzTfw=" + }, + "hardwareEnforced": { + "purposes": ["2"], + "algorithms": "4", + "digests": ["0"], + "mlDsaVariant": "1", + "noAuthRequired": true, + "origin": "GENERATED", + "rootOfTrust": { + "verifiedBootKey": "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=", + "deviceLocked": false, + "verifiedBootState": "UNVERIFIED", + "verifiedBootHash": "Y+0pwpIRxL66kj3crhTN6iuQy/53qKIFY93M2gRyu0A=" + }, + "osVersion": "170000", + "osPatchLevel": "202606", + "vendorPatchLevel": "20260605", + "bootPatchLevel": "20260605" + } +} \ No newline at end of file diff --git a/testdata/tokay/sdk37/TEE_MLDSA_FACTORY.pem b/testdata/tokay/sdk37/TEE_MLDSA_FACTORY.pem new file mode 100644 index 0000000..b87bf23 --- /dev/null +++ b/testdata/tokay/sdk37/TEE_MLDSA_FACTORY.pem @@ -0,0 +1,121 @@ +-----BEGIN CERTIFICATE----- +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 +romsmQuT+Rm/c6hjsXWQgDy0KC6wO30cW9E= +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIB9TCCAXqgAwIBAgIRALFzWIe+YcYaa1TF39AZpKQwCgYIKoZIzj0EAwIwOTEM +MAoGA1UEDAwDVEVFMSkwJwYDVQQFEyA1NmEwMDUwMTRlZTZjNmYzNmM0MDJhM2Ex +YTM2MTg5MzAeFw0yMzEwMTYyMzMzNTlaFw0zMzEwMTMyMzMzNTlaMDkxDDAKBgNV +BAwMA1RFRTEpMCcGA1UEBRMgMmQyNjVjOTNhYzJhMzZiMmJiNjZlMzk0ZTgxNTQz +ZGMwWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAASd0xbAQN6NpO9P0Ul9ousT1TUk +NfRtBCBajq85zgQB3BKInZoXY0uE3OjYjzKxQNdV/36feyKyDjthNFfvlmB2o2Mw +YTAdBgNVHQ4EFgQU5VwlFSlHoELl60Fo+f6gOjNehmcwHwYDVR0jBBgwFoAUdbUc +cU5dlh7UByZBL8FPyndPRgYwDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMC +AgQwCgYIKoZIzj0EAwIDaQAwZgIxALOD9ULVS5REpYIC5vLvJPWoXdzQKwmFGnsv +qFMpCKtGDHY4PS9uKijIiWxX/0Ro1QIxANQV2lbuHALGd4zlip2BE673rhyNPAwW +ALJUcwmyz9KAs7Wj4cc2tXzKk+WJtRj7TA== +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIDlDCCAXygAwIBAgIRAKDuFruozgRMCgoJLepjEWswDQYJKoZIhvcNAQELBQAw +GzEZMBcGA1UEBRMQZjkyMDA5ZTg1M2I2YjA0NTAeFw0yMzEwMTYyMzMwNTBaFw0z +MzEwMTMyMzMwNTBaMDkxDDAKBgNVBAwMA1RFRTEpMCcGA1UEBRMgNTZhMDA1MDE0 +ZWU2YzZmMzZjNDAyYTNhMWEzNjE4OTMwdjAQBgcqhkjOPQIBBgUrgQQAIgNiAAQb +IHgl7CDQ6nebg5sMg6CNHxp6P1a1DvHUci/95DMrDlzk7hmU/o642XKTdN5CEluH +5A3orCfeZsQOZdR98/FC6U5+ocyMjJ2w6VW8OFQq0AIyD+DQfgSW9edRm6Ib6JOj +YzBhMB0GA1UdDgQWBBR1tRxxTl2WHtQHJkEvwU/Kd09GBjAfBgNVHSMEGDAWgBQ2 +YeEAfIgFCVGLRGxH/xpMyepPEjAPBgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQE +AwICBDANBgkqhkiG9w0BAQsFAAOCAgEADzuMcJ+Xn5NngxIsu1OVBwkqsN3V+E+q +ZSok6YVAqtODZgiuAEnaUWOkWMK8cF1kFpxWxfTE01g1Eg3ohd9sgAG6TVKJENde +FMrlu1ApK4oWW5JE+YNMIftYU0X0H2vZ1h4ekyJ/2qKg8zjH3l7MYWqh+oAml2+a +ZX5HdIc91m3/aTCxlSOaGvvw+8EQUo3JcfN6/vgXdGbtXwX9zB0SGAyQvjr0k9sN +IeN2SQ7cSC3VfSy6rCWMOovWCUJ70vxTyogiiZB0jVcvxumKMn51uYkklM1UOzy4 +lHQo1IJlBsbY2bBzxZJHi8CfICFwWQOas8MdRoua6n13N8166/dS4tF186bkr9hS +DqF/1dgCJ5BDmYMstrW/jSEQlFnnimMnXjeCuoV6qLwhdQNxRR0keksKahQs1mvO +hTW9gvqebpfsdCEib9nIyfnO3tdZ+CCjMZ/dmm8u2us+D35bcnYlq4Jvkd4z+q0a +GA2nm9NGBPqvZG1+W0NfR3QaphNAgKAltOXOhLs3vw9a2na5aWph63lH+bBJrGES +nB29W7iqh3Swkk1YPhUuE6y3a0W7ghlxmGHbIL1sqUmFuBrIdtBPlEt5wwM4xry2 +3rZdiOIus8jICjT7oqOYZNMLq8qM+mUfBvXRO4od6y6OIdlgYLK0ocdtdW3lFIoE +D9mCOASBu7s= +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIFHDCCAwSgAwIBAgIJAPHBcqaZ6vUdMA0GCSqGSIb3DQEBCwUAMBsxGTAXBgNV +BAUTEGY5MjAwOWU4NTNiNmIwNDUwHhcNMjIwMzIwMTgwNzQ4WhcNNDIwMzE1MTgw +NzQ4WjAbMRkwFwYDVQQFExBmOTIwMDllODUzYjZiMDQ1MIICIjANBgkqhkiG9w0B +AQEFAAOCAg8AMIICCgKCAgEAr7bHgiuxpwHsK7Qui8xUFmOr75gvMsd/dTEDDJdS +Sxtf6An7xyqpRR90PL2abxM1dEqlXnf2tqw1Ne4Xwl5jlRfdnJLmN0pTy/4lj4/7 +tv0Sk3iiKkypnEUtR6WfMgH0QZfKHM1+di+y9TFRtv6y//0rb+T+W8a9nsNL/ggj +nar86461qO0rOs2cXjp3kOG1FEJ5MVmFmBGtnrKpa73XpXyTqRxB/M0n1n/W9nGq +C4FSYa04T6N5RIZGBN2z2MT5IKGbFlbC8UrW0DxW7AYImQQcHtGl/m00QLVWutHQ +oVJYnFPlXTcHYvASLu+RhhsbDmxMgJJ0mcDpvsC4PjvB+TxywElgS70vE0XmLD+O +JtvsBslHZvPBKCOdT0MS+tgSOIfga+z1Z1g7+DVagf7quvmag8jfPioyKvxnK/Eg +sTUVi2ghzq8wm27ud/mIM7AY2qEORR8Go3TVB4HzWQgpZrt3i5MIlCaY504LzSRi +igHCzAPlHws+W0rB5N+er5/2pJKnfBSDiCiFAVtCLOZ7gLiMm0jhO2B6tUXHI/+M +RPjy02i59lINMRRev56GKtcd9qO/0kUJWdZTdA2XoS82ixPvZtXQpUpuL12ab+9E +aDK8Z4RHJYYfCT3Q5vNAXaiWQ+8PTWm2QgBR/bkwSWc+NpUFgNPN9PvQi8WEg5Um +AGMCAwEAAaNjMGEwHQYDVR0OBBYEFDZh4QB8iAUJUYtEbEf/GkzJ6k8SMB8GA1Ud +IwQYMBaAFDZh4QB8iAUJUYtEbEf/GkzJ6k8SMA8GA1UdEwEB/wQFMAMBAf8wDgYD +VR0PAQH/BAQDAgIEMA0GCSqGSIb3DQEBCwUAA4ICAQB8cMqTllHc8U+qCrOlg3H7 +174lmaCsbo/bJ0C17JEgMLb4kvrqsXZs01U3mB/qABg/1t5Pd5AORHARs1hhqGIC +W/nKMav574f9rZN4PC2ZlufGXb7sIdJpGiO9ctRhiLuYuly10JccUZGEHpHSYM2G +tkgYbZba6lsCPYAAP83cyDV+1aOkTf1RCp/lM0PKvmxYN10RYsK631jrleGdcdkx +oSK//mSQbgcWnmAEZrzHoF1/0gso1HZgIn0YLzVhLSA/iXCX4QT2h3J5z3znluKG +1nv8NQdxei2DIIhASWfu804CA96cQKTTlaae2fweqXjdN1/v2nqOhngNyz1361mF +mr4XmaKH/ItTwOe72NI9ZcwS1lVaCvsIkTDCEXdm9rCNPAY10iTunIHFXRh+7KPz +lHGewCq/8TOohBRn0/NNfh7uRslOSZ/xKbN9tMBtw37Z8d2vvnXq/YWdsm1+JLVw +n6yYD/yacNJBlwpddla8eaVMjsF6nBnIgQOf9zKSe06nSTqvgwUHosgOECZJZ1Eu +zbH4yswbt02tKtKEFhx+v+OTge/06V+jGsqTWLsfrOCNLuA8H++z+pUENmpqnnHo +vaI47gC+TNpkgYGkkBT6B/m/U01BuOBBTzhIlMEZq9qkDWuM2cA5kW5V3FJUcfHn +w1IdYIg2Wxg7yHcQZemFQg== +-----END CERTIFICATE----- \ No newline at end of file diff --git a/testdata/tokay/sdk37/TEE_MLDSA_RKP.json b/testdata/tokay/sdk37/TEE_MLDSA_RKP.json new file mode 100644 index 0000000..08387da --- /dev/null +++ b/testdata/tokay/sdk37/TEE_MLDSA_RKP.json @@ -0,0 +1,36 @@ +{ + "attestationVersion": "500", + "attestationSecurityLevel": "TRUSTED_ENVIRONMENT", + "keyMintVersion": "500", + "keyMintSecurityLevel": "TRUSTED_ENVIRONMENT", + "attestationChallenge": "Y2hhbGxlbmdl", + "uniqueId": "", + "softwareEnforced": { + "creationDateTime": "1777384250243", + "attestationApplicationId": { + "packages": [ + {"name":"android.keystore.cts", "version":"37" } + ], + "signatures": ["bOzFDjSuMb+1Z4mG1tbTc2xXHe0vJFlSd5Ph8FTrDJs="] + }, + "moduleHash": "FaidWkxztCor58kSH+BtPV6/tFSP0MSgkePA7fFzTfw=" + }, + "hardwareEnforced": { + "purposes": ["2"], + "algorithms": "4", + "digests": ["0"], + "mlDsaVariant": "1", + "noAuthRequired": true, + "origin": "GENERATED", + "rootOfTrust": { + "verifiedBootKey": "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=", + "deviceLocked": false, + "verifiedBootState": "UNVERIFIED", + "verifiedBootHash": "Y+0pwpIRxL66kj3crhTN6iuQy/53qKIFY93M2gRyu0A=" + }, + "osVersion": "170000", + "osPatchLevel": "202606", + "vendorPatchLevel": "20260605", + "bootPatchLevel": "20260605" + } +} \ No newline at end of file diff --git a/testdata/tokay/sdk37/TEE_MLDSA_RKP.pem b/testdata/tokay/sdk37/TEE_MLDSA_RKP.pem new file mode 100644 index 0000000..7df0826 --- /dev/null +++ b/testdata/tokay/sdk37/TEE_MLDSA_RKP.pem @@ -0,0 +1,116 @@ +-----BEGIN CERTIFICATE----- +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 +tMIO/CHfo4+TfUGSmccZ7nVkqbCYl3sEU/k= +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIB3zCCAYWgAwIBAgIRANR7q9gCITO0I5d2RMvdHPEwCgYIKoZIzj0EAwIwKTET +MBEGA1UEChMKR29vZ2xlIExMQzESMBAGA1UEAxMJRHJvaWQgQ0EzMB4XDTI2MDQy +NjEzNDY0N1oXDTI2MDUwOTA1MTQ1OFowOTEMMAoGA1UEChMDVEVFMSkwJwYDVQQD +EyBkNDdiYWJkODAyMjEzM2I0MjM5Nzc2NDRjYmRkMWNmMTBZMBMGByqGSM49AgEG +CCqGSM49AwEHA0IABPTqdau/jCAS3kjjk/GcMinj+pTCPSRL5Dgfll2vnb34shEc +/NYbpCOT2qr/QR/DTOMmr14vYBgVryVgGAjv6yijfjB8MB0GA1UdDgQWBBSgqKus +BemAm2z7NwZgGBfGgeDw/zAfBgNVHSMEGDAWgBSAK+F/w1XZj3zCe963T5d6Y25w +ljAPBgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwICBDAZBgorBgEEAdZ5AgEe +BAuiAQgDZkdvb2dsZTAKBggqhkjOPQQDAgNIADBFAiB/y4lvJ10ZBrj17/zioCHD +LYF8hFAb5aae/7cBRsIO8AIhAPKTPkFojQyX/7KhfZ1c/uI6MdaFNGkPrdOosHF4 +8apO +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIC7jCCAnWgAwIBAgITfCjWNRYyUMt+4iWfZb6vPuiIqzAKBggqhkjOPQQDAzAp +MRMwEQYDVQQKEwpHb29nbGUgTExDMRIwEAYDVQQDEwlEcm9pZCBDQTIwHhcNMjYw +NDI2MDkwNjI3WhcNMjYwNzA1MDkwNjI2WjApMRMwEQYDVQQKEwpHb29nbGUgTExD +MRIwEAYDVQQDEwlEcm9pZCBDQTMwWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAATz +4iq6VKf2YG/TclAux+kwj85yxyLOHozvqAQpEhKVCI3QU4Z0Ktm06qdLs+3q2OwH +bvL/AzbIhG2J14fLnDANo4IBejCCAXYwDgYDVR0PAQH/BAQDAgIEMA8GA1UdEwEB +/wQFMAMBAf8wHQYDVR0OBBYEFIAr4X/DVdmPfMJ73rdPl3pjbnCWMB8GA1UdIwQY +MBaAFPuU7nThtXAWlYmOhiIonUwP4cMdMIGNBggrBgEFBQcBAQSBgDB+MHwGCCsG +AQUFBzAChnBodHRwOi8vcHJpdmF0ZWNhLWNvbnRlbnQtNjk5NDg5YWMtMDAwMC0y +NTkwLWJiNGQtODgzZDI0ZjMyZjBjLnN0b3JhZ2UuZ29vZ2xlYXBpcy5jb20vMjRm +MDlhNWRiNDdlYmRjZDNkOGMvY2EuY3J0MIGCBgNVHR8EezB5MHegdaBzhnFodHRw +Oi8vcHJpdmF0ZWNhLWNvbnRlbnQtNjk5NDg5YWMtMDAwMC0yNTkwLWJiNGQtODgz +ZDI0ZjMyZjBjLnN0b3JhZ2UuZ29vZ2xlYXBpcy5jb20vMjRmMDlhNWRiNDdlYmRj +ZDNkOGMvY3JsLmNybDAKBggqhkjOPQQDAwNnADBkAjBhc8VKtIBFWxncg/oTEoQU +uZMjIDZyTdmG3Y+efMKcVA70LpNTOswcC5PYeve1tO4CMFRYmShY4ItzFP/+rvEJ +zWNigutmmk/vS4QithtPwU4narR6iIxWTWfzYXockgrkYg== +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIICZDCCAeugAwIBAgIRAPLC/gLfzdARgeSj5rNpoowwCgYIKoZIzj0EAwMwUjEc +MBoGA1UEAwwTS2V5IEF0dGVzdGF0aW9uIENBMTEQMA4GA1UECwwHQW5kcm9pZDET +MBEGA1UECgwKR29vZ2xlIExMQzELMAkGA1UEBhMCVVMwHhcNMjYwMjA5MjAwMTEx +WhcNMjkwMjA4MjAwMTExWjApMRMwEQYDVQQKEwpHb29nbGUgTExDMRIwEAYDVQQD +EwlEcm9pZCBDQTIwdjAQBgcqhkjOPQIBBgUrgQQAIgNiAATkwn4jOZw/zpxhsBn4 +27C8Xz684+3Ajq5zsIzXwYlQPGieyFBuNxkUUFSa4YzZObqTOrgI9iFcfTBqOuOl +yEtIuipjVowV9UCddBKO5ndqPTEk8Dd2RWn4yMtUTnyMMpGjga0wgaowHwYDVR0j +BBgwFoAUUjK7LPtGQ5vc1oGpDmVm4DRB6kAwDwYDVR0TAQH/BAUwAwEB/zAdBgNV +HQ4EFgQU+5TudOG1cBaViY6GIiidTA/hwx0wDgYDVR0PAQH/BAQDAgEGMEcGA1Ud +HwRAMD4wPKA6oDiGNmh0dHBzOi8vYW5kcm9pZC5nb29nbGVhcGlzLmNvbS9hdHRl +c3RhdGlvbi9rZXlfY2ExLmNybDAKBggqhkjOPQQDAwNnADBkAjArwb7NmSVBlasM +dMRjY0FFEum0b+SUZTMmvBT5AGYzk8xGCi2Mj2NZdchxZfxHUJgCMDseaiAzoixN +ISk40rfoR4vMvs7n9r4fgEgmb+9KQbpDgdq0+90mzcAL4vKr4hWSxA== +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIICIjCCAaigAwIBAgIRAISp0Cl7DrWK5/8OgN52BgUwCgYIKoZIzj0EAwMwUjEc +MBoGA1UEAwwTS2V5IEF0dGVzdGF0aW9uIENBMTEQMA4GA1UECwwHQW5kcm9pZDET +MBEGA1UECgwKR29vZ2xlIExMQzELMAkGA1UEBhMCVVMwHhcNMjUwNzE3MjIzMjE4 +WhcNMzUwNzE1MjIzMjE4WjBSMRwwGgYDVQQDDBNLZXkgQXR0ZXN0YXRpb24gQ0Ex +MRAwDgYDVQQLDAdBbmRyb2lkMRMwEQYDVQQKDApHb29nbGUgTExDMQswCQYDVQQG +EwJVUzB2MBAGByqGSM49AgEGBSuBBAAiA2IABCPaI3FO3z5bBQo8cuiEas4HjqCt +G/mLFfRT0MsIssPBEEU5Cfbt6sH5yOAxqEi5QagpU1yX4HwnGb7OtBYpDTB57uH5 +Eczm34A5FNijV3s0/f0UPl7zbJcTx6xwqMIRq6NCMEAwDwYDVR0TAQH/BAUwAwEB +/zAOBgNVHQ8BAf8EBAMCAQYwHQYDVR0OBBYEFFIyuyz7RkOb3NaBqQ5lZuA0QepA +MAoGCCqGSM49BAMDA2gAMGUCMETfjPO/HwqReR2CS7p0ZWoD/LHs6hDi422opifH +EUaYLxwGlT9SLdjkVpz0UUOR5wIxAIoGyxGKRHVTpqpGRFiJtQEOOTp/+s1GcxeY +uR2zh/80lQyu9vAFCj6E4AXc+osmRg== +-----END CERTIFICATE----- From fcf416d17942f07e6744b446233fb4588aacf998 Mon Sep 17 00:00:00 2001 From: Suzanna Jiwani Date: Mon, 1 Jun 2026 10:00:05 -0700 Subject: [PATCH 10/48] Test Verifier against all testdata PiperOrigin-RevId: 924767967 --- src/main/kotlin/testing/TestUtils.kt | 28 +++++++++ src/test/kotlin/ExtensionTest.kt | 38 ++++-------- src/test/kotlin/VerifierTest.kt | 87 ++++++++++++++++------------ 3 files changed, 89 insertions(+), 64 deletions(-) diff --git a/src/main/kotlin/testing/TestUtils.kt b/src/main/kotlin/testing/TestUtils.kt index 62aeb6c..b63b944 100644 --- a/src/main/kotlin/testing/TestUtils.kt +++ b/src/main/kotlin/testing/TestUtils.kt @@ -41,6 +41,9 @@ import java.security.cert.TrustAnchor import java.security.cert.X509Certificate import java.util.Base64 import kotlin.io.path.Path +import kotlin.io.path.isDirectory +import kotlin.io.path.listDirectoryEntries +import kotlin.io.path.name import kotlin.io.path.reader import org.bouncycastle.cert.X509CertificateHolder import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter @@ -98,6 +101,31 @@ object TestUtils { private fun readFile(path: Path) = path.reader() private fun readFile(path: String) = readFile(Path(path)) + + data class TestCaseInfo(val model: String, val sdk: Int) + + fun getTestCases(): List { + val root = Path(TESTDATA_PATH) + val testCases = + root + .listDirectoryEntries() + .filter { it.isDirectory() } + .flatMap { modelDir -> + modelDir + .listDirectoryEntries("sdk*") + .filter { it.isDirectory() } + .mapNotNull { sdkDir -> + val sdkVersion = sdkDir.name.removePrefix("sdk").toIntOrNull() + if (sdkVersion == null) { + null + } else { + TestCaseInfo(modelDir.name, sdkVersion) + } + } + } + check(testCases.isNotEmpty()) { "No test cases found in $root" } + return testCases + } } object Base64ByteStringAdapter : JsonDeserializer, JsonSerializer { diff --git a/src/test/kotlin/ExtensionTest.kt b/src/test/kotlin/ExtensionTest.kt index 275db1f..8b3ed57 100644 --- a/src/test/kotlin/ExtensionTest.kt +++ b/src/test/kotlin/ExtensionTest.kt @@ -18,6 +18,7 @@ package com.android.keyattestation.verifier import com.android.keyattestation.verifier.testing.Chains import com.android.keyattestation.verifier.testing.FakeLogHook import com.android.keyattestation.verifier.testing.TestUtils.TESTDATA_PATH +import com.android.keyattestation.verifier.testing.TestUtils.getTestCases import com.android.keyattestation.verifier.testing.TestUtils.readCertPath import com.android.keyattestation.verifier.testing.V3Extensions import com.android.keyattestation.verifier.testing.toKeyDescription @@ -29,12 +30,11 @@ import com.google.testing.junit.testparameterinjector.TestParameterInjector 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.time.YearMonth import kotlin.io.path.Path import kotlin.io.path.inputStream -import kotlin.io.path.isDirectory import kotlin.io.path.listDirectoryEntries -import kotlin.io.path.name import kotlin.io.path.nameWithoutExtension import kotlin.io.path.readText import kotlin.io.path.reader @@ -72,32 +72,14 @@ class ExtensionTest { } class TestCaseProvider : TestParametersValuesProvider() { - override fun provideValues(context: Context): List { - val root = Path("testdata") - val parameters = - root - .listDirectoryEntries() - .filter { it.isDirectory() } - .flatMap { modelDir -> - modelDir - .listDirectoryEntries("sdk*") - .filter { it.isDirectory() } - .mapNotNull { sdkDir -> - val sdkVersion = sdkDir.name.removePrefix("sdk").toIntOrNull() - if (sdkVersion == null) { - null - } else { - TestParametersValues.builder() - .name("${modelDir.name}_sdk$sdkVersion") - .addParameter("model", modelDir.name) - .addParameter("sdk", sdkVersion) - .build() - } - } - } - assertThat(parameters).isNotEmpty() - return parameters - } + override fun provideValues(context: Context): List = + getTestCases().map { testCase -> + TestParametersValues.builder() + .name("${testCase.model}_sdk${testCase.sdk}") + .addParameter("model", testCase.model) + .addParameter("sdk", testCase.sdk) + .build() + } } @Test diff --git a/src/test/kotlin/VerifierTest.kt b/src/test/kotlin/VerifierTest.kt index 7cd9eb1..41f72e9 100644 --- a/src/test/kotlin/VerifierTest.kt +++ b/src/test/kotlin/VerifierTest.kt @@ -16,6 +16,7 @@ package com.android.keyattestation.verifier +import kotlin.io.path.Path import com.android.keyattestation.verifier.VerificationResult.ConstraintViolation import com.android.keyattestation.verifier.VerificationResult.ExtensionParsingFailure import com.android.keyattestation.verifier.VerificationResult.PathValidationFailure @@ -24,6 +25,7 @@ import com.android.keyattestation.verifier.testing.CertLists import com.android.keyattestation.verifier.testing.Certs import com.android.keyattestation.verifier.testing.FakeCalendar import com.android.keyattestation.verifier.testing.FakeLogHook +import com.android.keyattestation.verifier.testing.TestUtils.TESTDATA_PATH import com.android.keyattestation.verifier.testing.TestUtils.falseChecker import com.android.keyattestation.verifier.testing.TestUtils.prodAnchors import com.android.keyattestation.verifier.testing.TestUtils.readCertList @@ -32,17 +34,21 @@ import com.android.keyattestation.verifier.testing.TestUtils.trueChecker import com.google.common.truth.Truth.assertThat import com.google.common.util.concurrent.Futures import com.google.common.util.concurrent.ListenableFuture + import com.google.protobuf.ByteString import com.google.protobuf.kotlin.toByteString -import com.google.testing.junit.testparameterinjector.TestParameter import com.google.testing.junit.testparameterinjector.TestParameterInjector +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.security.cert.PKIXReason import java.security.cert.TrustAnchor import java.time.Instant -import java.time.LocalDate -import java.time.ZoneOffset import java.util.concurrent.Executors import java.util.concurrent.TimeUnit +import kotlin.io.path.listDirectoryEntries +import kotlin.io.path.nameWithoutExtension import kotlin.test.assertFailsWith import kotlin.test.assertIs import kotlinx.coroutines.guava.await @@ -53,6 +59,8 @@ import org.junit.runner.RunWith /** Unit tests for [Verifier]. */ @RunWith(TestParameterInjector::class) class VerifierTest { + private val testData = Path("testdata") + private val verifier = Verifier( { prodAnchors + TrustAnchor(Certs.root, null) }, @@ -72,41 +80,48 @@ class VerifierTest { } @Test - fun verify_validChain_returnsSuccess(@TestParameter testCase: TestCase) { - val verifier = Verifier({ prodAnchors }, { setOf() }, { testCase.timestamp }) - val chain = readCertList("${testCase.path}.pem") - val json = readJson("${testCase.path}.json") - val result = assertIs(verifier.verify(chain)) - assertThat(result.publicKey).isEqualTo(chain[0].publicKey) - assertThat(result.challenge).isEqualTo(json.attestationChallenge) - assertThat(result.securityLevel).isEqualTo(json.attestationSecurityLevel) - assertThat(result.verifiedBootState) - .isEqualTo(json.hardwareEnforced.rootOfTrust?.verifiedBootState) - assertThat(result.deviceLocked) - .isEqualTo(json.hardwareEnforced.rootOfTrust?.deviceLocked ?: false) + @TestParameters(valuesProvider = TestCaseProvider::class) + fun verify_validChain_returnsSuccess(model: String, sdk: Int) { + val path = testData.resolve("${model}/sdk${sdk}") + val pemFiles = path.listDirectoryEntries("*.pem") + for (pemPath in pemFiles) { + val subpath = "${model}/sdk${sdk}/${pemPath.nameWithoutExtension}" + val json = readJson("${subpath}.json") + + // TODO(google-internal bug): update here once sw root is supported. + if (json.attestationSecurityLevel == SecurityLevel.SOFTWARE) { + continue + } + + val creationDateTime = + json.softwareEnforced.creationDateTime ?: json.hardwareEnforced.creationDateTime + assertThat(creationDateTime).isNotNull() + val timestamp = Instant.ofEpochMilli(creationDateTime!!.toLong()) + + val verifier = Verifier({ prodAnchors }, { setOf() }, { timestamp }) + val chain = readCertList("${subpath}.pem") + val result = assertIs(verifier.verify(chain)) + assertThat(result.publicKey).isEqualTo(chain[0].publicKey) + assertThat(result.challenge).isEqualTo(json.attestationChallenge) + assertThat(result.securityLevel).isEqualTo(json.attestationSecurityLevel) + assertThat(result.verifiedBootState) + .isEqualTo(json.hardwareEnforced.rootOfTrust?.verifiedBootState) + assertThat(result.deviceLocked) + .isEqualTo(json.hardwareEnforced.rootOfTrust?.deviceLocked ?: false) + } } - enum class TestCase(val path: String, val timestamp: Instant) { - PIXEL_3_SDK28( - "blueline/sdk28/TEE_EC_NONE", - LocalDate.of(2024, 10, 1).atStartOfDay(ZoneOffset.UTC).toInstant(), - ), - PIXEL_8A_SDK34( - "akita/sdk34/TEE_EC_NONE", - LocalDate.of(2024, 10, 1).atStartOfDay(ZoneOffset.UTC).toInstant(), - ), - PIXEL_9PRO_SDK36( - "caiman/sdk36/TEE_EC_RKP", - LocalDate.of(2025, 9, 30).atStartOfDay(ZoneOffset.UTC).toInstant(), - ), - PIXEL_9A_SDK36( - "tegu/sdk36/TEE_EC_2026_ROOT", - LocalDate.of(2026, 2, 23).atStartOfDay(ZoneOffset.UTC).toInstant(), - ), - PIXEL_9A_SDK36_STRONGBOX( - "tegu/sdk36/SB_EC_2026_ROOT", - LocalDate.of(2026, 2, 24).atStartOfDay(ZoneOffset.UTC).toInstant(), - ), + class TestCaseProvider : TestParametersValuesProvider() { + override fun provideValues(context: Context): List { + val testCases = com.android.keyattestation.verifier.testing.TestUtils.getTestCases() + return testCases.map { testCase -> + TestParametersValues.builder() + .name("${testCase.model}_sdk${testCase.sdk}") + .addParameter("model", testCase.model) + .addParameter("sdk", testCase.sdk) + .build() + } + } } @Test From 6a80b361734dc3afc9db2c8b3a72602c391238fe Mon Sep 17 00:00:00 2001 From: Suzanna Jiwani Date: Fri, 5 Jun 2026 09:48:15 -0700 Subject: [PATCH 11/48] Update subject extended test cert PiperOrigin-RevId: 927348379 --- src/main/kotlin/testing/Certs.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/kotlin/testing/Certs.kt b/src/main/kotlin/testing/Certs.kt index 3f10567..daa066e 100644 --- a/src/main/kotlin/testing/Certs.kt +++ b/src/main/kotlin/testing/Certs.kt @@ -76,7 +76,7 @@ object CertLists { certFactory.generateLeafCert( certFactory.leafKey.public, certFactory.leafKey.private, - leafCert.subject, + issuer = leafCert.subject, ), leafCert, Certs.factoryAttestation, From d6209c05f58ceec92e54d6a8c6370b241f1a2e99 Mon Sep 17 00:00:00 2001 From: Suzanna Jiwani Date: Mon, 8 Jun 2026 09:38:27 -0700 Subject: [PATCH 12/48] Fix TrustAnchor validation. Should compare the intermediate's issuer to the anchor's subject. PiperOrigin-RevId: 928611724 --- .../provider/KeyAttestationCertPathValidator.kt | 2 +- src/main/kotlin/testing/Certs.kt | 10 ++++++++++ .../KeyAttestationCertPathValidatorTest.kt | 14 ++++++++++++++ 3 files changed, 25 insertions(+), 1 deletion(-) diff --git a/src/main/kotlin/provider/KeyAttestationCertPathValidator.kt b/src/main/kotlin/provider/KeyAttestationCertPathValidator.kt index bdba1f4..e749e2b 100644 --- a/src/main/kotlin/provider/KeyAttestationCertPathValidator.kt +++ b/src/main/kotlin/provider/KeyAttestationCertPathValidator.kt @@ -92,7 +92,7 @@ class KeyAttestationCertPathValidator : CertPathValidatorSpi() { sigProvider: String?, ): CertPathValidatorResult { val certList = certPath.toCertList() - val selector = X509CertSelector().apply { issuer = certList.first().issuerX500Principal } + val selector = X509CertSelector().apply { subject = certList.first().issuerX500Principal } var lastException: CertPathValidatorException? = null for (anchor in trustAnchors) { diff --git a/src/main/kotlin/testing/Certs.kt b/src/main/kotlin/testing/Certs.kt index daa066e..bef942c 100644 --- a/src/main/kotlin/testing/Certs.kt +++ b/src/main/kotlin/testing/Certs.kt @@ -54,6 +54,16 @@ object Certs { val factoryIntermediate = certFactory.factoryIntermediate val remoteIntermediate = certFactory.remoteIntermediate val factoryAttestation = certFactory.factoryAttestation + val notSelfIssuedAnchor = + TrustAnchor( + certFactory.generateIntermediateCertificate( + publicKey = certFactory.rootKey.public, + signingKey = certFactory.rootKey.private, + subject = certFactory.root.subject, + issuer = certFactory.factoryIntermediate.subject, + ), + null, + ) } /** diff --git a/src/test/kotlin/provider/KeyAttestationCertPathValidatorTest.kt b/src/test/kotlin/provider/KeyAttestationCertPathValidatorTest.kt index f683a68..7b8bdba 100644 --- a/src/test/kotlin/provider/KeyAttestationCertPathValidatorTest.kt +++ b/src/test/kotlin/provider/KeyAttestationCertPathValidatorTest.kt @@ -18,6 +18,7 @@ package com.android.keyattestation.verifier.provider import com.android.keyattestation.verifier.KeyAttestationReason import com.android.keyattestation.verifier.testing.CertLists +import com.android.keyattestation.verifier.testing.Certs.notSelfIssuedAnchor import com.android.keyattestation.verifier.testing.Certs.rootAnchor as testAnchor import com.android.keyattestation.verifier.testing.Chains import com.android.keyattestation.verifier.testing.FakeCalendar @@ -167,6 +168,19 @@ class KeyAttestationCertPathValidatorTest { assertThat(exception.reason).isEqualTo(PKIXReason.NO_TRUST_ANCHOR) } + @Test + fun notSelfIssuedTrustAnchor_succeeds() { + val params = + PKIXParameters(setOf(notSelfIssuedAnchor)).apply { date = FakeCalendar.DEFAULT.today() } + val result = + certPathValidator.validate(Chains.validFactoryProvisioned, params) + as PKIXCertPathValidatorResult + assertThat(result.trustAnchor).isEqualTo(notSelfIssuedAnchor) + assertThat(result.policyTree).isNull() + assertThat(result.publicKey) + .isEqualTo(Chains.validFactoryProvisioned.certificates.first().publicKey) + } + @Test fun wrongIssuer_throwsCertPathValidatorException() { val certPath = Chains.wrongIssuer From 7f9905509599bd3722ab5bc889acd9ce9227fe11 Mon Sep 17 00:00:00 2001 From: Seth Moore Date: Thu, 11 Jun 2026 13:58:58 -0700 Subject: [PATCH 13/48] Fix factory provisioned cert detection to catch "really old" certs Factory certificates changed format at some point between 2016 and 2021, which breaks the "isFactoryProvisioned" check. Make the check a bit more lax so that we consider more cert types as factory provisioned. This is safe because we are not relaxing the isRemoteProvisioned check at all, and that is the check that attackers want to spoof. PiperOrigin-RevId: 930722556 --- .../kotlin/provider/KeyAttestationCertPath.kt | 2 +- .../provider/KeyAttestationCertPathTest.kt | 4 + testdata/sony-xperia10-iii/sdk33/TEE_EC.json | 41 +++++++++ testdata/sony-xperia10-iii/sdk33/TEE_EC.pem | 85 +++++++++++++++++++ 4 files changed, 131 insertions(+), 1 deletion(-) create mode 100644 testdata/sony-xperia10-iii/sdk33/TEE_EC.json create mode 100644 testdata/sony-xperia10-iii/sdk33/TEE_EC.pem diff --git a/src/main/kotlin/provider/KeyAttestationCertPath.kt b/src/main/kotlin/provider/KeyAttestationCertPath.kt index c26d1ed..8ce5fc6 100644 --- a/src/main/kotlin/provider/KeyAttestationCertPath.kt +++ b/src/main/kotlin/provider/KeyAttestationCertPath.kt @@ -111,7 +111,7 @@ class KeyAttestationCertPath(certs: List) : CertPath("X.509") { private fun isFactoryProvisioned(): Boolean { val rdn = parseDN(this.intermediateCert().subjectX500Principal.getName(X500Principal.RFC1779)) - return rdn.containsKey(SERIAL_NUMBER_OID) && rdn[TITLE_OID] in setOf("TEE", "StrongBox") + return rdn.containsKey(SERIAL_NUMBER_OID) } // TODO(google-internal bug): Update this to use fields in the RKP root. diff --git a/src/test/kotlin/provider/KeyAttestationCertPathTest.kt b/src/test/kotlin/provider/KeyAttestationCertPathTest.kt index 09e8836..cf73b69 100644 --- a/src/test/kotlin/provider/KeyAttestationCertPathTest.kt +++ b/src/test/kotlin/provider/KeyAttestationCertPathTest.kt @@ -108,6 +108,10 @@ class KeyAttestationCertPathTest { } enum class ProvisioningMethodTestCase(val path: String, val expected: ProvisioningMethod) { + FACTORY_PROVISIONED_OLD_STYLE( + "sony-xperia10-iii/sdk33/TEE_EC", + ProvisioningMethod.FACTORY_PROVISIONED, + ), FACTORY_PROVISIONED("blueline/sdk28/TEE_EC_NONE", ProvisioningMethod.FACTORY_PROVISIONED), REMOTELY_PROVISIONED("caiman/sdk36/TEE_EC_RKP", ProvisioningMethod.REMOTELY_PROVISIONED), UNKNOWN("marlin/sdk29/TEE_EC_NONE", ProvisioningMethod.UNKNOWN), diff --git a/testdata/sony-xperia10-iii/sdk33/TEE_EC.json b/testdata/sony-xperia10-iii/sdk33/TEE_EC.json new file mode 100644 index 0000000..442a7b6 --- /dev/null +++ b/testdata/sony-xperia10-iii/sdk33/TEE_EC.json @@ -0,0 +1,41 @@ +{ + "attestationVersion": "3", + "attestationSecurityLevel": "TRUSTED_ENVIRONMENT", + "keyMintVersion": "41", + "keyMintSecurityLevel": "TRUSTED_ENVIRONMENT", + "attestationChallenge": "Pq/k1d0AkN5aQrQytCSBr1zimWNlayWExZpJLeFtAMk=", + "uniqueId": "", + "softwareEnforced": { + "creationDateTime": "1780585145000", + "attestationApplicationId": { + "packages": [{ "name": "com.android.vending", "version": "85162330" }], + "signatures": ["8P1sW0EPJcslw7UzRsiXL64w+O50Ed+RBICtay1g24M="] + }, + "areTagsOrdered": true + }, + "hardwareEnforced": { + "purposes": ["2"], + "algorithms": "3", + "keySize": "256", + "digests": ["6"], + "ecCurve": "1", + "noAuthRequired": true, + "origin": "GENERATED", + "rootOfTrust": { + "verifiedBootKey": "gdG7IUVTlNoNf2DCV7dUWYDtUt/XyKiBbM88pwdDb54=", + "deviceLocked": true, + "verifiedBootState": "VERIFIED", + "verifiedBootHash": "UNZsaZbE8OV1KFQV9dBC0iDGeN7N1Bc79PHTAhz55KE=" + }, + "osVersion": "130000", + "osPatchLevel": "202307", + "attestationIdBrand": "docomo", + "attestationIdDevice": "SO-52B", + "attestationIdProduct": "SO-52B", + "attestationIdManufacturer": "Sony", + "attestationIdModel": "SO-52B", + "vendorPatchLevel": "20230701", + "bootPatchLevel": "20230701", + "areTagsOrdered": true + } +} diff --git a/testdata/sony-xperia10-iii/sdk33/TEE_EC.pem b/testdata/sony-xperia10-iii/sdk33/TEE_EC.pem new file mode 100644 index 0000000..70e2c51 --- /dev/null +++ b/testdata/sony-xperia10-iii/sdk33/TEE_EC.pem @@ -0,0 +1,85 @@ +-----BEGIN CERTIFICATE----- +MIICvzCCAmagAwIBAgIBATAKBggqhkjOPQQDAjAbMRkwFwYDVQQFExAzZTdmYjZh +MWVlNGJkNTY4MCAXDTcwMDEwMTAwMDAwMFoYDzIxMDYwMjA3MDYyODE1WjAfMR0w +GwYDVQQDDBRBbmRyb2lkIEtleXN0b3JlIEtleTBZMBMGByqGSM49AgEGCCqGSM49 +AwEHA0IABLrQPJVjzcg/dVotjVbI3VkDyJj/HomhIkxDWA8rS9LM+ZOVEkk/9Pls +nybD1ZsWN9kyvQaK2oKLYAW7Cq53iY2jggGTMIIBjzAOBgNVHQ8BAf8EBAMCB4Aw +ggF7BgorBgEEAdZ5AgERBIIBazCCAWcCAQMKAQECASkKAQEEID6v5NXdAJDeWkK0 +MrQkga9c4pljZWslhMWaSS3hbQDJBAAwV7+FPQgCBgGekyUiqL+FRUcERTBDMR0w +GwQTY29tLmFuZHJvaWQudmVuZGluZwIEBRN5WjEiBCDw/WxbQQ8lyyXDtTNGyJcv +rjD47nQR35EEgK1rLWDbgzCB26EFMQMCAQKiAwIBA6MEAgIBAKUFMQMCAQaqAwIB +Ab+DdwIFAL+FPgMCAQC/hUBMMEoEIIHRuyFFU5TaDX9gwle3VFmA7VLf18iogWzP +PKcHQ2+eAQH/CgEABCBQ1mxplsTw5XUoVBX10ELSIMZ43s3UFzv08dMCHPnkob+F +QQUCAwH70L+FQgUCAwMWQ7+FRggEBmRvY29tb7+FRwgEBlNPLTUyQr+FSAgEBlNP +LTUyQr+FTAYEBFNvbnm/hU0IBAZTTy01MkK/hU4GAgQBNLItv4VPBgIEATSyLTAK +BggqhkjOPQQDAgNHADBEAiAc30NT6OoIUR00Vm6x3BKWPa5BNfSdZ29uFI5suUsg +NQIgdYS61mQHaR2IgajOFO+nxMLhzj3/P4D0NZSnn7CgC14= +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIICKzCCAbKgAwIBAgIKFlgHaDNVWQMWBTAKBggqhkjOPQQDAjAbMRkwFwYDVQQF +ExA4N2Y0NTE0NDc1YmEwYTJiMB4XDTE2MDUyNjE3MTkwMFoXDTI2MDUyNDE3MTkw +MFowGzEZMBcGA1UEBRMQM2U3ZmI2YTFlZTRiZDU2ODBZMBMGByqGSM49AgEGCCqG +SM49AwEHA0IABAb+gStr8TAHqc4ueIBBQwkvDzcbYkIRf0qhu7hev7G9XPtRL8+D +MXUMy/JzFnpiiSt1QdcwrS2jK3lpS7DoA9Ojgd0wgdowHQYDVR0OBBYEFMqTzYTD +BCXs2/vajnn3MZXDBn5YMB8GA1UdIwQYMBaAFDBEI+Wi9gbhUKt3XxYWu5HMY8ZZ +MAwGA1UdEwEB/wQCMAAwDgYDVR0PAQH/BAQDAgeAMCQGA1UdHgQdMBugGTAXghVp +bnZhbGlkO2VtYWlsOmludmFsaWQwVAYDVR0fBE0wSzBJoEegRYZDaHR0cHM6Ly9h +bmRyb2lkLmdvb2dsZWFwaXMuY29tL2F0dGVzdGF0aW9uL2NybC8xNjU4MDc2ODMz +NTU1OTAzMTYwNTAKBggqhkjOPQQDAgNnADBkAjAMv+GZm51nvmPVl/AWjFqFsriO +oEeQQAzessWJ/cHWmZTr4VfoVkTTf8mqi/X1ytoCMGrosF800OaNbU8gFoP7R6fM +mnnwl2/5WLV3uhP5Yz6AQUWWTYLuF2EbxY3dNNaGGg== +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIDwzCCAaugAwIBAgIKA4gmZ2BliZaFdTANBgkqhkiG9w0BAQsFADAbMRkwFwYD +VQQFExBmOTIwMDllODUzYjZiMDQ1MB4XDTE2MDUyNjE3MDE1MVoXDTI2MDUyNDE3 +MDE1MVowGzEZMBcGA1UEBRMQODdmNDUxNDQ3NWJhMGEyYjB2MBAGByqGSM49AgEG +BSuBBAAiA2IABGQ7VmgdJ/rEgs9sIE3rzvApXDUMAaqMMn8+1fRJrvQpZkJfOT2E +djtdrVaxDQRZxixqT5MlVqiSk8PRTqLx3+8OPLoicqMiOeGytH2sVQurvFynVeKq +SGKK1jx2/2fccqOBtjCBszAdBgNVHQ4EFgQUMEQj5aL2BuFQq3dfFha7kcxjxlkw +HwYDVR0jBBgwFoAUNmHhAHyIBQlRi0RsR/8aTMnqTxIwDwYDVR0TAQH/BAUwAwEB +/zAOBgNVHQ8BAf8EBAMCAYYwUAYDVR0fBEkwRzBFoEOgQYY/aHR0cHM6Ly9hbmRy +b2lkLmdvb2dsZWFwaXMuY29tL2F0dGVzdGF0aW9uL2NybC9FOEZBMTk2MzE0RDJG +QTE4MA0GCSqGSIb3DQEBCwUAA4ICAQBAOYqLNryTmbOlnrjnIvDoXxzaLOgCXu29 +l7KpbFHacVLxgYuGRiIEQqzZBqUYSt9Pgx+P2KvoHtz99sEZr2xTe0Dw6CTHTAmx +WXUFdrlvEMm2GySfvJRfMNCuX1oIS/M5PfREY2YZHyLq/sn1sJr3FjbKMdUMBo5A +camcD3H8wl9O/6qfhX+57iXzoK6yMzJRG/Mlkm58/sFk0pjayUBchmUJL0FQ6IhK +Ygy8RKE2UDyXKOE7+ZMSMUUkAdzyn2PFv7TvQtDk0ge2mkVrNrfPSglMzBNvrSDH +PBmTktXzwseVagIRT5WI91OrUOYPFgostsfH42hs5wJtAFGPwDg/1mNa8UyH9k1b +MrRq3Srez1XG0Ju7SGN/uNX5dkcwvfAmadtmM7Pp+l2VHRYRR600jAcM2+7bl8eg +qfM/A7vyDLZqPIxDwkLXj2eN99nJZJVaGfB9dHyFOqBqBM6SdyV6MSIr3AHoo6u+ +BWIX9+q8n1qg5I6JWeEe+K58SbRDVoNQgsKP9/iPruXMU5rm2ywPxICVGysl1GgA +P+FJ3X6oP0tXFWQlYoWdSloSVHNZQqj2ev/69sMnGsTeJw1V7I0gR+eZNEfxe+vZ +D4KP88KxuiPCe94rp+Aqs5/YwuCo6rQ+HGi5OZNBsQXYIufClSBje+OpjQb7HJgi +hJdzo2/IBw== +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIFYDCCA0igAwIBAgIJAOj6GWMU0voYMA0GCSqGSIb3DQEBCwUAMBsxGTAXBgNV +BAUTEGY5MjAwOWU4NTNiNmIwNDUwHhcNMTYwNTI2MTYyODUyWhcNMjYwNTI0MTYy +ODUyWjAbMRkwFwYDVQQFExBmOTIwMDllODUzYjZiMDQ1MIICIjANBgkqhkiG9w0B +AQEFAAOCAg8AMIICCgKCAgEAr7bHgiuxpwHsK7Qui8xUFmOr75gvMsd/dTEDDJdS +Sxtf6An7xyqpRR90PL2abxM1dEqlXnf2tqw1Ne4Xwl5jlRfdnJLmN0pTy/4lj4/7 +tv0Sk3iiKkypnEUtR6WfMgH0QZfKHM1+di+y9TFRtv6y//0rb+T+W8a9nsNL/ggj +nar86461qO0rOs2cXjp3kOG1FEJ5MVmFmBGtnrKpa73XpXyTqRxB/M0n1n/W9nGq +C4FSYa04T6N5RIZGBN2z2MT5IKGbFlbC8UrW0DxW7AYImQQcHtGl/m00QLVWutHQ +oVJYnFPlXTcHYvASLu+RhhsbDmxMgJJ0mcDpvsC4PjvB+TxywElgS70vE0XmLD+O +JtvsBslHZvPBKCOdT0MS+tgSOIfga+z1Z1g7+DVagf7quvmag8jfPioyKvxnK/Eg +sTUVi2ghzq8wm27ud/mIM7AY2qEORR8Go3TVB4HzWQgpZrt3i5MIlCaY504LzSRi +igHCzAPlHws+W0rB5N+er5/2pJKnfBSDiCiFAVtCLOZ7gLiMm0jhO2B6tUXHI/+M +RPjy02i59lINMRRev56GKtcd9qO/0kUJWdZTdA2XoS82ixPvZtXQpUpuL12ab+9E +aDK8Z4RHJYYfCT3Q5vNAXaiWQ+8PTWm2QgBR/bkwSWc+NpUFgNPN9PvQi8WEg5Um +AGMCAwEAAaOBpjCBozAdBgNVHQ4EFgQUNmHhAHyIBQlRi0RsR/8aTMnqTxIwHwYD +VR0jBBgwFoAUNmHhAHyIBQlRi0RsR/8aTMnqTxIwDwYDVR0TAQH/BAUwAwEB/zAO +BgNVHQ8BAf8EBAMCAYYwQAYDVR0fBDkwNzA1oDOgMYYvaHR0cHM6Ly9hbmRyb2lk +Lmdvb2dsZWFwaXMuY29tL2F0dGVzdGF0aW9uL2NybC8wDQYJKoZIhvcNAQELBQAD +ggIBACDIw41L3KlXG0aMiS//cqrG+EShHUGo8HNsw30W1kJtjn6UBwRM6jnmiwfB +Pb8VA91chb2vssAtX2zbTvqBJ9+LBPGCdw/E53Rbf86qhxKaiAHOjpvAy5Y3m00m +qC0w/Zwvju1twb4vhLaJ5NkUJYsUS7rmJKHHBnETLi8GFqiEsqTWpG/6ibYCv7rY +DBJDcR9W62BW9jfIoBQcxUCUJouMPH25lLNcDc1ssqvC2v7iUgI9LeoM1sNovqPm +QUiG9rHli1vXxzCyaMTjwftkJLkf6724DFhuKug2jITV0QkXvaJWF4nUaHOTNA4u +JU9WDvZLI1j83A+/xnAJUucIv/zGJ1AMH2boHqF8CY16LpsYgBt6tKxxWH00XcyD +CdW2KlBCeqbQPcsFmWyWugxdcekhYsAWyoSf818NUsZdBWBaR/OukXrNLfkQ79Iy +ZohZbvabO/X+MVT3rriAoKc8oE2Uws6DF+60PV7/WIPjNvXySdqspImSN78mflxD +qwLqRBYkA3I75qppLGG9rp7UCdRjxMl8ZDBld+7yvHVgt1cVzJx9xnyGCC23Uaic +MDSXYrB4I4WHXPGjxhZuCuPBLTdOLU8YRvMYdEvYebWHMpvwGCF6bAx3JBpIeOQ1 +wDB5y0USicV3YgYGmi+NZfhA4URSh77Yd6uuJOJENRaNVTzk +-----END CERTIFICATE----- From 40574312f8c643d027ac1f93287f2026e755eb18 Mon Sep 17 00:00:00 2001 From: Carmen Hanish Date: Fri, 12 Jun 2026 05:04:42 -0700 Subject: [PATCH 14/48] Generate roots valid from long ago to far in the future. PiperOrigin-RevId: 931089191 --- src/main/kotlin/testing/FakeCalendar.kt | 4 ++++ src/main/kotlin/testing/KeyAttestationCertFactory.kt | 7 ++++--- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/src/main/kotlin/testing/FakeCalendar.kt b/src/main/kotlin/testing/FakeCalendar.kt index 8005a91..7019103 100644 --- a/src/main/kotlin/testing/FakeCalendar.kt +++ b/src/main/kotlin/testing/FakeCalendar.kt @@ -30,6 +30,10 @@ class FakeCalendar(val today: LocalDate = LocalDate.of(2024, 10, 20)) { fun nextWeek(): Date = today.plusDays(7).toDate() + internal fun longAgo(): Date = today.minusYears(5).toDate() + + internal fun farInTheFuture(): Date = today.plusYears(5).toDate() + private fun Instant.toDate() = Date.from(this) private fun LocalDate.toDate() = this.atStartOfDay(ZoneId.of("UTC")).toInstant().toDate() diff --git a/src/main/kotlin/testing/KeyAttestationCertFactory.kt b/src/main/kotlin/testing/KeyAttestationCertFactory.kt index d741c16..41b3c74 100644 --- a/src/main/kotlin/testing/KeyAttestationCertFactory.kt +++ b/src/main/kotlin/testing/KeyAttestationCertFactory.kt @@ -84,7 +84,7 @@ internal class KeyAttestationCertFactory(val fakeCalendar: FakeCalendar = FakeCa internal fun generateRootCertificate( keyPair: KeyPair = rootKey, - subject: X500Name = X500Name("SERIALNUMBER=badc0de"), + subject: X500Name = RKP_ROOT_SUBJECT, ) = generateCertificate( keyPair.public, @@ -92,8 +92,8 @@ internal class KeyAttestationCertFactory(val fakeCalendar: FakeCalendar = FakeCa subject = subject, issuer = subject, serialNumber = BigInteger.valueOf(0xca11cafe), - notBefore = fakeCalendar.lastWeek(), - notAfter = fakeCalendar.nextWeek(), + notBefore = fakeCalendar.longAgo(), + notAfter = fakeCalendar.farInTheFuture(), extensions = listOf(BASIC_CONSTRAINTS_EXT), ) @@ -258,6 +258,7 @@ internal class KeyAttestationCertFactory(val fakeCalendar: FakeCalendar = FakeCa ) val RKP_INTERMEDIATE_SUBJECT = X500Name("O=Google LLC, CN=Droid CA3") val REMOTE_INTERMEDIATE_SUBJECT = X500Name("CN=Droid CA2, O=Google LLC") + val RKP_ROOT_SUBJECT = X500Name("CN=Test Key Attestation CA1, OU=Android, O=Google LLC, C=US") } } From 1666512573ce9fef3bc0f3f029a5aa74eb721f09 Mon Sep 17 00:00:00 2001 From: Seth Moore Date: Mon, 6 Jul 2026 14:41:12 -0700 Subject: [PATCH 15/48] Add missing tags As noted by https://github.com/android/keyattestation/issues/87, we were missing tags 508 and 405. Oops! Fixes #87 PiperOrigin-RevId: 943503422 --- src/main/kotlin/CertPrinter.kt | 4 + src/main/kotlin/Extension.kt | 20 +++++ src/test/kotlin/CertPrinterTest.kt | 23 ++++++ src/test/kotlin/ExtensionTest.kt | 2 + testdata/complete_authlist.json | 2 + testdata/tegu/sdk37/TEE_MAX_USAGE_COUNT.json | 45 +++++++++++ testdata/tegu/sdk37/TEE_MAX_USAGE_COUNT.pem | 79 ++++++++++++++++++++ testdata/tegu/sdk37/TEE_TRUSTED_CONF.json | 45 +++++++++++ testdata/tegu/sdk37/TEE_TRUSTED_CONF.pem | 79 ++++++++++++++++++++ 9 files changed, 299 insertions(+) create mode 100644 testdata/tegu/sdk37/TEE_MAX_USAGE_COUNT.json create mode 100644 testdata/tegu/sdk37/TEE_MAX_USAGE_COUNT.pem create mode 100644 testdata/tegu/sdk37/TEE_TRUSTED_CONF.json create mode 100644 testdata/tegu/sdk37/TEE_TRUSTED_CONF.pem diff --git a/src/main/kotlin/CertPrinter.kt b/src/main/kotlin/CertPrinter.kt index 0f0e9fd..e9705e2 100644 --- a/src/main/kotlin/CertPrinter.kt +++ b/src/main/kotlin/CertPrinter.kt @@ -213,12 +213,16 @@ object CertPrinter { appendIndentedLine("originationExpireDateTime: $originationExpireDateTime") } if (usageExpireDateTime != null) appendIndentedLine("usageExpireDateTime: $usageExpireDateTime") + if (usageCountLimit != null) appendIndentedLine("usageCountLimit: $usageCountLimit") if (noAuthRequired != null) appendIndentedLine("noAuthRequired: $noAuthRequired") if (userAuthType != null) appendIndentedLine("userAuthType: $userAuthType") if (authTimeout != null) appendIndentedLine("authTimeout: $authTimeout") if (trustedUserPresenceRequired != null) { appendIndentedLine("trustedUserPresenceRequired: $trustedUserPresenceRequired") } + if (trustedConfirmationRequired != null) { + appendIndentedLine("trustedConfirmationRequired: $trustedConfirmationRequired") + } if (unlockedDeviceRequired != null) { appendIndentedLine("unlockedDeviceRequired: $unlockedDeviceRequired") } diff --git a/src/main/kotlin/Extension.kt b/src/main/kotlin/Extension.kt index fcb531f..c3842fa 100644 --- a/src/main/kotlin/Extension.kt +++ b/src/main/kotlin/Extension.kt @@ -259,11 +259,13 @@ enum class KeyMintTag(val value: Int) { ACTIVE_DATE_TIME(400), ORIGINATION_EXPIRE_DATE_TIME(401), USAGE_EXPIRE_DATE_TIME(402), + USAGE_COUNT_LIMIT(405), NO_AUTH_REQUIRED(503), USER_AUTH_TYPE(504), AUTH_TIMEOUT(505), ALLOW_WHILE_ON_BODY(506), TRUSTED_USER_PRESENCE_REQUIRED(507), + TRUSTED_CONFIRMATION_REQUIRED(508), UNLOCKED_DEVICE_REQUIRED(509), CREATION_DATE_TIME(701), ORIGIN(702), @@ -285,6 +287,14 @@ enum class KeyMintTag(val value: Int) { ATTESTATION_ID_SECOND_IMEI(723), MODULE_HASH(724); + // The following tags are intentionally unsupported: + // 7 (callerNonce): Used in symmetric ciphers only + // 8 (minMacLength): Used in symmetric ciphers only + // 303 (rollbackResistance): Not usable by 3p apps (framework API is hidden) + // 305 (earlyBootOnly): Not usable by 3p apps + // 502 (userSecureId): Not usable by 3p apps (framework API is hidden) + // 720 (deviceUniqueAttestation): Not widely used. + companion object { fun from(value: Int) = values().firstOrNull { it.value == value } @@ -317,10 +327,12 @@ data class AuthorizationList( val activeDateTime: BigInteger? = null, val originationExpireDateTime: BigInteger? = null, val usageExpireDateTime: BigInteger? = null, + val usageCountLimit: BigInteger? = null, val noAuthRequired: Boolean? = null, val userAuthType: BigInteger? = null, val authTimeout: BigInteger? = null, val trustedUserPresenceRequired: Boolean? = null, + val trustedConfirmationRequired: Boolean? = null, val unlockedDeviceRequired: Boolean? = null, val creationDateTime: BigInteger? = null, val origin: Origin? = null, @@ -367,6 +379,7 @@ data class AuthorizationList( usageExpireDateTime?.toAsn1()?.let { add(it.toTaggedObject(KeyMintTag.USAGE_EXPIRE_DATE_TIME)) } + usageCountLimit?.toAsn1()?.let { add(it.toTaggedObject(KeyMintTag.USAGE_COUNT_LIMIT)) } if (noAuthRequired != null) { check(noAuthRequired) { "noAuthRequired must be null or true" } add(DERNull.INSTANCE.toTaggedObject(KeyMintTag.NO_AUTH_REQUIRED)) @@ -377,6 +390,10 @@ data class AuthorizationList( check(trustedUserPresenceRequired) { "trustedUserPresenceRequired must be null or true" } add(DERNull.INSTANCE.toTaggedObject(KeyMintTag.TRUSTED_USER_PRESENCE_REQUIRED)) } + if (trustedConfirmationRequired != null) { + check(trustedConfirmationRequired) { "trustedConfirmationRequired must be null or true" } + add(DERNull.INSTANCE.toTaggedObject(KeyMintTag.TRUSTED_CONFIRMATION_REQUIRED)) + } if (unlockedDeviceRequired != null) { check(unlockedDeviceRequired) { "unlockedDeviceRequired must be null or true" } add(DERNull.INSTANCE.toTaggedObject(KeyMintTag.UNLOCKED_DEVICE_REQUIRED)) @@ -489,11 +506,14 @@ data class AuthorizationList( activeDateTime = converter.parseInt(KeyMintTag.ACTIVE_DATE_TIME), originationExpireDateTime = converter.parseInt(KeyMintTag.ORIGINATION_EXPIRE_DATE_TIME), usageExpireDateTime = converter.parseInt(KeyMintTag.USAGE_EXPIRE_DATE_TIME), + usageCountLimit = converter.parseInt(KeyMintTag.USAGE_COUNT_LIMIT), noAuthRequired = if (objects.containsKey(KeyMintTag.NO_AUTH_REQUIRED)) true else null, userAuthType = converter.parseInt(KeyMintTag.USER_AUTH_TYPE), authTimeout = converter.parseInt(KeyMintTag.AUTH_TIMEOUT), trustedUserPresenceRequired = if (objects.containsKey(KeyMintTag.TRUSTED_USER_PRESENCE_REQUIRED)) true else null, + trustedConfirmationRequired = + if (objects.containsKey(KeyMintTag.TRUSTED_CONFIRMATION_REQUIRED)) true else null, unlockedDeviceRequired = if (objects.containsKey(KeyMintTag.UNLOCKED_DEVICE_REQUIRED)) true else null, creationDateTime = converter.parseInt(KeyMintTag.CREATION_DATE_TIME), diff --git a/src/test/kotlin/CertPrinterTest.kt b/src/test/kotlin/CertPrinterTest.kt index 877ea6c..eb969dc 100644 --- a/src/test/kotlin/CertPrinterTest.kt +++ b/src/test/kotlin/CertPrinterTest.kt @@ -16,18 +16,24 @@ package com.android.keyattestation.verifier +import kotlin.io.path.Path +import com.android.keyattestation.verifier.testing.TestUtils.TESTDATA_PATH +import com.android.keyattestation.verifier.testing.TestUtils.readCertPath import com.google.common.truth.Truth.assertThat + import com.google.protobuf.ByteString import java.io.ByteArrayInputStream import java.math.BigInteger import java.security.cert.CertificateFactory import java.security.cert.X509Certificate +import kotlin.io.path.reader import org.junit.Test import org.junit.runner.RunWith import org.junit.runners.JUnit4 @RunWith(JUnit4::class) class CertPrinterTest { + private val testData = Path("testdata") @Test fun prettyPrint_certWithKeyDescription_containsExpectedFields() { @@ -172,4 +178,21 @@ class CertPrinterTest { .trimMargin() assertThat(infoString).isEqualTo(expectedString) } + + @Test + fun prettyPrint_certWithTrustedConfirmationRequired_containsExpectedFields() { + val cert = readCertPath(testData.resolve("tegu/sdk37/TEE_TRUSTED_CONF.pem").reader()).leafCert() + val certString = CertPrinter.prettyString(cert) + + assertThat(certString).contains("trustedConfirmationRequired: true") + } + + @Test + fun prettyPrint_certWithUsageCountLimit_containsExpectedFields() { + val cert = + readCertPath(testData.resolve("tegu/sdk37/TEE_MAX_USAGE_COUNT.pem").reader()).leafCert() + val certString = CertPrinter.prettyString(cert) + + assertThat(certString).contains("usageCountLimit: 42") + } } diff --git a/src/test/kotlin/ExtensionTest.kt b/src/test/kotlin/ExtensionTest.kt index 8b3ed57..6f560e5 100644 --- a/src/test/kotlin/ExtensionTest.kt +++ b/src/test/kotlin/ExtensionTest.kt @@ -124,10 +124,12 @@ class ExtensionTest { activeDateTime = 5.toBigInteger(), originationExpireDateTime = 6.toBigInteger(), usageExpireDateTime = 7.toBigInteger(), + usageCountLimit = 8.toBigInteger(), noAuthRequired = true, userAuthType = 1.toBigInteger(), authTimeout = 9.toBigInteger(), trustedUserPresenceRequired = true, + trustedConfirmationRequired = true, creationDateTime = 10.toBigInteger(), origin = Origin.GENERATED, rollbackResistant = true, diff --git a/testdata/complete_authlist.json b/testdata/complete_authlist.json index a3db9fa..bd176f3 100644 --- a/testdata/complete_authlist.json +++ b/testdata/complete_authlist.json @@ -19,11 +19,13 @@ "activeDateTime": "1000", "originationExpireDateTime": "2000", "usageExpireDateTime": "3000", + "usageCountLimit": "10", "noAuthRequired": true, "userAuthType": "1", "authTimeout": "60", "allowWhileOnBody": true, "trustedUserPresenceRequired": true, + "trustedConfirmationRequired": true, "unlockedDeviceRequired": true, "creationDateTime": "123456789", "origin": "GENERATED", diff --git a/testdata/tegu/sdk37/TEE_MAX_USAGE_COUNT.json b/testdata/tegu/sdk37/TEE_MAX_USAGE_COUNT.json new file mode 100644 index 0000000..612e81d --- /dev/null +++ b/testdata/tegu/sdk37/TEE_MAX_USAGE_COUNT.json @@ -0,0 +1,45 @@ +{ + "attestationVersion": "500", + "attestationSecurityLevel": "TRUSTED_ENVIRONMENT", + "keyMintVersion": "500", + "keyMintSecurityLevel": "TRUSTED_ENVIRONMENT", + "attestationChallenge": "NWMwOTZmMGYtZTk5OC00MDU5LWJkZWMtYmUzNmQ5MjhiZDhk", + "uniqueId": "", + "softwareEnforced": { + "usageCountLimit": "42", + "creationDateTime": "1783361716745", + "attestationApplicationId": { + "packages": [ + { "name": "com.google.android.attestation", "version": "1" } + ], + "signatures": ["EDk47kU35Z6O55L2VFBPuDRvxrNG0LvEQV/DOfz8jsE="] + }, + "moduleHash": "al4AdvgYUvh6qnkfO7Wmn25Qtfs9I+pp4bbUBMm7N+4=", + "areTagsOrdered": true + }, + "hardwareEnforced": { + "purposes": ["2", "3"], + "algorithms": "3", + "keySize": "256", + "digests": ["4"], + "ecCurve": "1", + "noAuthRequired": true, + "origin": "GENERATED", + "rootOfTrust": { + "verifiedBootKey": "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=", + "deviceLocked": false, + "verifiedBootState": "UNVERIFIED", + "verifiedBootHash": "UXikZWsan/XtVtixCSRIFR1OcFjTW3kzCaiHo2z23zY=" + }, + "osVersion": "170000", + "osPatchLevel": "202608", + "attestationIdBrand": "google", + "attestationIdDevice": "tegu", + "attestationIdProduct": "tegu", + "attestationIdManufacturer": "Google", + "attestationIdModel": "Pixel 9a", + "vendorPatchLevel": "20260805", + "bootPatchLevel": "20260805", + "areTagsOrdered": true + } +} diff --git a/testdata/tegu/sdk37/TEE_MAX_USAGE_COUNT.pem b/testdata/tegu/sdk37/TEE_MAX_USAGE_COUNT.pem new file mode 100644 index 0000000..28e1849 --- /dev/null +++ b/testdata/tegu/sdk37/TEE_MAX_USAGE_COUNT.pem @@ -0,0 +1,79 @@ +-----BEGIN CERTIFICATE----- +MIIDGzCCAsGgAwIBAgIBATAKBggqhkjOPQQDAjA5MQwwCgYDVQQKEwNURUUxKTAn +BgNVBAMTIDliNWU3YzMzMDI1MmZhMGM2OThhM2YyM2QzMWE0YTY0MB4XDTcwMDEw +MTAwMDAwMFoXDTQ4MDEwMTAwMDAwMFowHzEdMBsGA1UEAxMUQW5kcm9pZCBLZXlz +dG9yZSBLZXkwWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAARFwvPOP50WwOCp4naL +xQPlmgz1ZknhHT6kIt133jk66CW+Waxas3A7b4XhPFYwVrs7M8gir1/1uBRD7KYQ +oxlRo4IB0jCCAc4wDgYDVR0PAQH/BAQDAgeAMIIBugYKKwYBBAHWeQIBEQSCAaow +ggGmAgIB9AoBAQICAfQKAQEEJDVjMDk2ZjBmLWU5OTgtNDA1OS1iZGVjLWJlMzZk +OTI4YmQ4ZAQAMIGMv4MVAwIBKr+FPQgCBgGfOKRCCb+FRU8ETTBLMSUwIwQeY29t +Lmdvb2dsZS5hbmRyb2lkLmF0dGVzdGF0aW9uAgEBMSIEIBA5OO5FN+WejueS9lRQ +T7g0b8azRtC7xEFfwzn8/I7Bv4VUIgQgal4AdvgYUvh6qnkfO7Wmn25Qtfs9I+pp +4bbUBMm7N+4wgd6hCDEGAgECAgEDogMCAQOjBAICAQClBTEDAgEEqgMCAQG/g3cC +BQC/hT4DAgEAv4VATDBKBCAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAEBAAoBAgQgUXikZWsan/XtVtixCSRIFR1OcFjTW3kzCaiHo2z23za/hUEFAgMC +mBC/hUIFAgMDF3C/hUYIBAZnb29nbGW/hUcGBAR0ZWd1v4VIBgQEdGVndb+FTAgE +Bkdvb2dsZb+FTQoECFBpeGVsIDlhv4VOBgIEATUnxb+FTwYCBAE1J8UwCgYIKoZI +zj0EAwIDSAAwRQIhAOU1RMOy63BMRqKPcs04Z7R+x/2Y57rFXyKyfvMi4w5dAiBW +n7K6FqPSni9aeceAa4E2WOW3xQkT6oY6DGpzR13Dag== +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIB4DCCAYagAwIBAgIRAJtefDMCUvoMaYo/I9MaSmQwCgYIKoZIzj0EAwIwKTET +MBEGA1UEChMKR29vZ2xlIExMQzESMBAGA1UEAxMJRHJvaWQgQ0EzMB4XDTI2MDcw +NDE4MDQ1MVoXDTI2MDcxODIwMjIwNFowOTEMMAoGA1UEChMDVEVFMSkwJwYDVQQD +EyA5YjVlN2MzMzAyNTJmYTBjNjk4YTNmMjNkMzFhNGE2NDBZMBMGByqGSM49AgEG +CCqGSM49AwEHA0IABEi/O1zevCLR+M8NUjl28JJZEGWN5lHtH4fsaDSIPRXMXDBF +TmxqCQC6miw8HKcX221Z98Y27mdWYPWiLqD6TIOjfzB9MB0GA1UdDgQWBBTAgFTi +pHNqqPjhN1h228Yafkjm3TAfBgNVHSMEGDAWgBT4fpA9v89ERJ4Y363a5PfjP43h +ojAPBgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwICBDAaBgorBgEEAdZ5AgEe +BAyiARhAA2ZHb29nbGUwCgYIKoZIzj0EAwIDSAAwRQIhAPg3pTn1EQjFNZm0TfYs +2MQsn9WQXZEt5Kp3pt0Qp9RiAiAHRml/xPm148ozwgffSYiubn/dYAece2MkbZy4 +p4MtCw== +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIC7zCCAnWgAwIBAgITLs5V2cvAv404tD1I6pmGCOQyIDAKBggqhkjOPQQDAzAp +MRMwEQYDVQQKEwpHb29nbGUgTExDMRIwEAYDVQQDEwlEcm9pZCBDQTIwHhcNMjYw +NzA0MDczNDE2WhcNMjYwOTEyMDczNDE1WjApMRMwEQYDVQQKEwpHb29nbGUgTExD +MRIwEAYDVQQDEwlEcm9pZCBDQTMwWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAASs +dB6wBxJ7d9ZXIAyKBnb0cc/h2tGXyTC1AV0SvxXPAJntU83bXFTNq0ggOP04Cm5d +lFAHlwTKHnFbIRBdAkxHo4IBejCCAXYwDgYDVR0PAQH/BAQDAgIEMA8GA1UdEwEB +/wQFMAMBAf8wHQYDVR0OBBYEFPh+kD2/z0REnhjfrdrk9+M/jeGiMB8GA1UdIwQY +MBaAFEUgMj4fpvmPHNXDRy7UelD+O6jgMIGNBggrBgEFBQcBAQSBgDB+MHwGCCsG +AQUFBzAChnBodHRwOi8vcHJpdmF0ZWNhLWNvbnRlbnQtNjllMzUyMjktMDAwMC0y +OGZmLWI1MDYtMTRjMTRlZjVhYzU4LnN0b3JhZ2UuZ29vZ2xlYXBpcy5jb20vMDFk +MWQxYmNiNzMyNjg1NzlhNWYvY2EuY3J0MIGCBgNVHR8EezB5MHegdaBzhnFodHRw +Oi8vcHJpdmF0ZWNhLWNvbnRlbnQtNjllMzUyMjktMDAwMC0yOGZmLWI1MDYtMTRj +MTRlZjVhYzU4LnN0b3JhZ2UuZ29vZ2xlYXBpcy5jb20vMDFkMWQxYmNiNzMyNjg1 +NzlhNWYvY3JsLmNybDAKBggqhkjOPQQDAwNoADBlAjEA1PPAlofqbh/K1r+sM+iE +P6OKvoJgi8AJ9njDe7dgmBrnrsMhlaF6zhCmFfGeJxy+AjAsiI5c4mNvSxqBVcl1 +jqJcSLVFCA5gnPxOuYrmx6wsPvRDu6dkf9ZiFLKvnScrcq8= +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIICZTCCAeugAwIBAgIRALGEywXsUP3JhfDsUyl8+CMwCgYIKoZIzj0EAwMwUjEc +MBoGA1UEAwwTS2V5IEF0dGVzdGF0aW9uIENBMTEQMA4GA1UECwwHQW5kcm9pZDET +MBEGA1UECgwKR29vZ2xlIExMQzELMAkGA1UEBhMCVVMwHhcNMjYwMjA5MTk1NzEw +WhcNMjkwMjA4MTk1NzEwWjApMRMwEQYDVQQKEwpHb29nbGUgTExDMRIwEAYDVQQD +EwlEcm9pZCBDQTIwdjAQBgcqhkjOPQIBBgUrgQQAIgNiAAT19+tRAlmwuauVyRrg +HqykkymaEkOS1IYSoXAQyBRvUNEnY5FGqmi44dOWcqMxu0uIbB3in5TD3GsR1NBm +i3f//mI0aiARbBtdP3YaIff8yy076NY9dPMnBiCMIwjRR2Cjga0wgaowRwYDVR0f +BEAwPjA8oDqgOIY2aHR0cHM6Ly9hbmRyb2lkLmdvb2dsZWFwaXMuY29tL2F0dGVz +dGF0aW9uL2tleV9jYTEuY3JsMB0GA1UdDgQWBBRFIDI+H6b5jxzVw0cu1HpQ/juo +4DAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/BAUwAwEB/zAfBgNVHSMEGDAWgBRS +Mrss+0ZDm9zWgakOZWbgNEHqQDAKBggqhkjOPQQDAwNoADBlAjBfOXmY1+SCcT/o +WL17AuVS7uoxMXssLksjChHT+VhTMpWu9J42x3G20hHVGTj+3ZICMQDBYZjd7+vf +uyLVCZkTX7wlvjcRJjvrEWyxvkZE5vlq8c3lwH9JyNBP3OeTd3o8IJs= +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIICIjCCAaigAwIBAgIRAISp0Cl7DrWK5/8OgN52BgUwCgYIKoZIzj0EAwMwUjEc +MBoGA1UEAwwTS2V5IEF0dGVzdGF0aW9uIENBMTEQMA4GA1UECwwHQW5kcm9pZDET +MBEGA1UECgwKR29vZ2xlIExMQzELMAkGA1UEBhMCVVMwHhcNMjUwNzE3MjIzMjE4 +WhcNMzUwNzE1MjIzMjE4WjBSMRwwGgYDVQQDDBNLZXkgQXR0ZXN0YXRpb24gQ0Ex +MRAwDgYDVQQLDAdBbmRyb2lkMRMwEQYDVQQKDApHb29nbGUgTExDMQswCQYDVQQG +EwJVUzB2MBAGByqGSM49AgEGBSuBBAAiA2IABCPaI3FO3z5bBQo8cuiEas4HjqCt +G/mLFfRT0MsIssPBEEU5Cfbt6sH5yOAxqEi5QagpU1yX4HwnGb7OtBYpDTB57uH5 +Eczm34A5FNijV3s0/f0UPl7zbJcTx6xwqMIRq6NCMEAwDwYDVR0TAQH/BAUwAwEB +/zAOBgNVHQ8BAf8EBAMCAQYwHQYDVR0OBBYEFFIyuyz7RkOb3NaBqQ5lZuA0QepA +MAoGCCqGSM49BAMDA2gAMGUCMETfjPO/HwqReR2CS7p0ZWoD/LHs6hDi422opifH +EUaYLxwGlT9SLdjkVpz0UUOR5wIxAIoGyxGKRHVTpqpGRFiJtQEOOTp/+s1GcxeY +uR2zh/80lQyu9vAFCj6E4AXc+osmRg== +-----END CERTIFICATE----- diff --git a/testdata/tegu/sdk37/TEE_TRUSTED_CONF.json b/testdata/tegu/sdk37/TEE_TRUSTED_CONF.json new file mode 100644 index 0000000..501a3d2 --- /dev/null +++ b/testdata/tegu/sdk37/TEE_TRUSTED_CONF.json @@ -0,0 +1,45 @@ +{ + "attestationVersion": "500", + "attestationSecurityLevel": "TRUSTED_ENVIRONMENT", + "keyMintVersion": "500", + "keyMintSecurityLevel": "TRUSTED_ENVIRONMENT", + "attestationChallenge": "YjE2MzFkYWItMmU2Yy00NjVmLThhN2YtMjRhNjE1MmE1MThh", + "uniqueId": "", + "softwareEnforced": { + "creationDateTime": "1782942965900", + "attestationApplicationId": { + "packages": [ + { "name": "com.google.android.attestation", "version": "1" } + ], + "signatures": ["EDk47kU35Z6O55L2VFBPuDRvxrNG0LvEQV/DOfz8jsE="] + }, + "moduleHash": "al4AdvgYUvh6qnkfO7Wmn25Qtfs9I+pp4bbUBMm7N+4=", + "areTagsOrdered": true + }, + "hardwareEnforced": { + "purposes": ["2", "3"], + "algorithms": "3", + "keySize": "256", + "digests": ["4"], + "ecCurve": "1", + "noAuthRequired": true, + "trustedConfirmationRequired": true, + "origin": "GENERATED", + "rootOfTrust": { + "verifiedBootKey": "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=", + "deviceLocked": false, + "verifiedBootState": "UNVERIFIED", + "verifiedBootHash": "UXikZWsan/XtVtixCSRIFR1OcFjTW3kzCaiHo2z23zY=" + }, + "osVersion": "170000", + "osPatchLevel": "202608", + "attestationIdBrand": "google", + "attestationIdDevice": "tegu", + "attestationIdProduct": "tegu", + "attestationIdManufacturer": "Google", + "attestationIdModel": "Pixel 9a", + "vendorPatchLevel": "20260805", + "bootPatchLevel": "20260805", + "areTagsOrdered": true + } +} diff --git a/testdata/tegu/sdk37/TEE_TRUSTED_CONF.pem b/testdata/tegu/sdk37/TEE_TRUSTED_CONF.pem new file mode 100644 index 0000000..0488291 --- /dev/null +++ b/testdata/tegu/sdk37/TEE_TRUSTED_CONF.pem @@ -0,0 +1,79 @@ +-----BEGIN CERTIFICATE----- +MIIDGzCCAsCgAwIBAgIBATAKBggqhkjOPQQDAjA5MQwwCgYDVQQKEwNURUUxKTAn +BgNVBAMTIDE4ZmM4MGU5Yzg3ZmMzMzIwNzkzZDZmOGU1ZDk5Njc4MB4XDTcwMDEw +MTAwMDAwMFoXDTQ4MDEwMTAwMDAwMFowHzEdMBsGA1UEAxMUQW5kcm9pZCBLZXlz +dG9yZSBLZXkwWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAARJIPDlEzti8zm6CLOL +xk5mQuSoYiA7Vpb0Gbq5o3uGltaV2z5PhqyggTzrm+5/ZArLvKfpbj1qatDYrLke +Hdlxo4IB0TCCAc0wDgYDVR0PAQH/BAQDAgeAMIIBuQYKKwYBBAHWeQIBEQSCAakw +ggGlAgIB9AoBAQICAfQKAQEEJGIxNjMxZGFiLTJlNmMtNDY1Zi04YTdmLTI0YTYx +NTJhNTE4YQQAMIGFv4U9CAIGAZ8frqCMv4VFTwRNMEsxJTAjBB5jb20uZ29vZ2xl +LmFuZHJvaWQuYXR0ZXN0YXRpb24CAQExIgQgEDk47kU35Z6O55L2VFBPuDRvxrNG +0LvEQV/DOfz8jsG/hVQiBCBqXgB2+BhS+HqqeR87taafblC1+z0j6mnhttQEybs3 +7jCB5KEIMQYCAQICAQOiAwIBA6MEAgIBAKUFMQMCAQSqAwIBAb+DdwIFAL+DfAIF +AL+FPgMCAQC/hUBMMEoEIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AQEACgECBCBReKRlaxqf9e1W2LEJJEgVHU5wWNNbeTMJqIejbPbfNr+FQQUCAwKY +EL+FQgUCAwMXcL+FRggEBmdvb2dsZb+FRwYEBHRlZ3W/hUgGBAR0ZWd1v4VMCAQG +R29vZ2xlv4VNCgQIUGl4ZWwgOWG/hU4GAgQBNSfFv4VPBgIEATUnxTAKBggqhkjO +PQQDAgNJADBGAiEA7Js835rXZfd1vf37yHjtC1Ih4EVJJCKEs6aqS34LNlgCIQCi +iBJxEomOe5CuPIs0JE+fSNJTDbz9PiFc45kCqG/FpA== +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIB4DCCAYWgAwIBAgIQGPyA6ch/wzIHk9b45dmWeDAKBggqhkjOPQQDAjApMRMw +EQYDVQQKEwpHb29nbGUgTExDMRIwEAYDVQQDEwlEcm9pZCBDQTMwHhcNMjYwNjI5 +MjE1NTIwWhcNMjYwNzE1MDM0MzI4WjA5MQwwCgYDVQQKEwNURUUxKTAnBgNVBAMT +IDE4ZmM4MGU5Yzg3ZmMzMzIwNzkzZDZmOGU1ZDk5Njc4MFkwEwYHKoZIzj0CAQYI +KoZIzj0DAQcDQgAEk9L28wglQe9mXAGkvQL3kbQf4zYjrJ194++69VQr62GMPax4 +U99IIXEjpLIMhiwppwKaNHjy5skJWZq/qjDnDqN/MH0wHQYDVR0OBBYEFK6UG6KG +eHLSuaoGaS3GeA8uAes0MB8GA1UdIwQYMBaAFNPp0+Hvte2kmSzU7e6EKNcmyGRb +MA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgIEMBoGCisGAQQB1nkCAR4E +DKIBGCADZkdvb2dsZTAKBggqhkjOPQQDAgNJADBGAiEAgeUDjlTRcvPpSl1UQmO3 +BmlOeyPjcbz9S45NDqpkHTYCIQDDuvw4IPY1wf7Cq6EBstkJYKHBfcfpj/sJVH5M +h0c31w== +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIC7jCCAnWgAwIBAgITMjUdZNnPlstQn8FEmxYeV4SoxTAKBggqhkjOPQQDAzAp +MRMwEQYDVQQKEwpHb29nbGUgTExDMRIwEAYDVQQDEwlEcm9pZCBDQTIwHhcNMjYw +NjA1MDUwMTI5WhcNMjYwODE0MDUwMTI4WjApMRMwEQYDVQQKEwpHb29nbGUgTExD +MRIwEAYDVQQDEwlEcm9pZCBDQTMwWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAAQW +oGCy7OmlomOGCVd4RkHL6AK7Ryp5BqJhXGUgSJCCNSuCBCZkZpQlWzQN7zzenjl2 +RT16LYWNrnWeOCmf4KzRo4IBejCCAXYwDgYDVR0PAQH/BAQDAgIEMA8GA1UdEwEB +/wQFMAMBAf8wHQYDVR0OBBYEFNPp0+Hvte2kmSzU7e6EKNcmyGRbMB8GA1UdIwQY +MBaAFPuU7nThtXAWlYmOhiIonUwP4cMdMIGNBggrBgEFBQcBAQSBgDB+MHwGCCsG +AQUFBzAChnBodHRwOi8vcHJpdmF0ZWNhLWNvbnRlbnQtNjk5NDIyNDEtMDAwMC0y +ODNmLWJlMjEtMzRjN2U5MjEyZjhiLnN0b3JhZ2UuZ29vZ2xlYXBpcy5jb20vMjk3 +MDJjZjExOTA4OTNlY2Q2YjQvY2EuY3J0MIGCBgNVHR8EezB5MHegdaBzhnFodHRw +Oi8vcHJpdmF0ZWNhLWNvbnRlbnQtNjk5NDIyNDEtMDAwMC0yODNmLWJlMjEtMzRj +N2U5MjEyZjhiLnN0b3JhZ2UuZ29vZ2xlYXBpcy5jb20vMjk3MDJjZjExOTA4OTNl +Y2Q2YjQvY3JsLmNybDAKBggqhkjOPQQDAwNnADBkAjAni4bT4fL0SIv2A0vmzPQw +hjkBBolCEq+TwNWvwCYiBMM1NTGnK53o42QJqYVNtXUCMFleyB1SRCTu9E8CX7ws +2RdLA+QMc5m3zWhdqRxjY7Fudtjkp90JNnN9CXFyArLTUQ== +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIICZDCCAeugAwIBAgIRAPLC/gLfzdARgeSj5rNpoowwCgYIKoZIzj0EAwMwUjEc +MBoGA1UEAwwTS2V5IEF0dGVzdGF0aW9uIENBMTEQMA4GA1UECwwHQW5kcm9pZDET +MBEGA1UECgwKR29vZ2xlIExMQzELMAkGA1UEBhMCVVMwHhcNMjYwMjA5MjAwMTEx +WhcNMjkwMjA4MjAwMTExWjApMRMwEQYDVQQKEwpHb29nbGUgTExDMRIwEAYDVQQD +EwlEcm9pZCBDQTIwdjAQBgcqhkjOPQIBBgUrgQQAIgNiAATkwn4jOZw/zpxhsBn4 +27C8Xz684+3Ajq5zsIzXwYlQPGieyFBuNxkUUFSa4YzZObqTOrgI9iFcfTBqOuOl +yEtIuipjVowV9UCddBKO5ndqPTEk8Dd2RWn4yMtUTnyMMpGjga0wgaowHwYDVR0j +BBgwFoAUUjK7LPtGQ5vc1oGpDmVm4DRB6kAwDwYDVR0TAQH/BAUwAwEB/zAdBgNV +HQ4EFgQU+5TudOG1cBaViY6GIiidTA/hwx0wDgYDVR0PAQH/BAQDAgEGMEcGA1Ud +HwRAMD4wPKA6oDiGNmh0dHBzOi8vYW5kcm9pZC5nb29nbGVhcGlzLmNvbS9hdHRl +c3RhdGlvbi9rZXlfY2ExLmNybDAKBggqhkjOPQQDAwNnADBkAjArwb7NmSVBlasM +dMRjY0FFEum0b+SUZTMmvBT5AGYzk8xGCi2Mj2NZdchxZfxHUJgCMDseaiAzoixN +ISk40rfoR4vMvs7n9r4fgEgmb+9KQbpDgdq0+90mzcAL4vKr4hWSxA== +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIICIjCCAaigAwIBAgIRAISp0Cl7DrWK5/8OgN52BgUwCgYIKoZIzj0EAwMwUjEc +MBoGA1UEAwwTS2V5IEF0dGVzdGF0aW9uIENBMTEQMA4GA1UECwwHQW5kcm9pZDET +MBEGA1UECgwKR29vZ2xlIExMQzELMAkGA1UEBhMCVVMwHhcNMjUwNzE3MjIzMjE4 +WhcNMzUwNzE1MjIzMjE4WjBSMRwwGgYDVQQDDBNLZXkgQXR0ZXN0YXRpb24gQ0Ex +MRAwDgYDVQQLDAdBbmRyb2lkMRMwEQYDVQQKDApHb29nbGUgTExDMQswCQYDVQQG +EwJVUzB2MBAGByqGSM49AgEGBSuBBAAiA2IABCPaI3FO3z5bBQo8cuiEas4HjqCt +G/mLFfRT0MsIssPBEEU5Cfbt6sH5yOAxqEi5QagpU1yX4HwnGb7OtBYpDTB57uH5 +Eczm34A5FNijV3s0/f0UPl7zbJcTx6xwqMIRq6NCMEAwDwYDVR0TAQH/BAUwAwEB +/zAOBgNVHQ8BAf8EBAMCAQYwHQYDVR0OBBYEFFIyuyz7RkOb3NaBqQ5lZuA0QepA +MAoGCCqGSM49BAMDA2gAMGUCMETfjPO/HwqReR2CS7p0ZWoD/LHs6hDi422opifH +EUaYLxwGlT9SLdjkVpz0UUOR5wIxAIoGyxGKRHVTpqpGRFiJtQEOOTp/+s1GcxeY +uR2zh/80lQyu9vAFCj6E4AXc+osmRg== +-----END CERTIFICATE----- From 3d9b718f17b406ff9dd405582490aaf5d63e41f0 Mon Sep 17 00:00:00 2001 From: Suzanna Jiwani Date: Tue, 7 Jul 2026 18:44:16 +0000 Subject: [PATCH 16/48] Add p256+sha384 test cert Test signature validation against a fake cert chain whose intermediate certificate signs the leaf with a p256 key using sha384. Generated using openssl. PiperOrigin-RevId: 944011042 Change-Id: I93a349fda01615cab043566295a48be4888c9352 --- .../KeyAttestationCertPathValidatorTest.kt | 9 ++++++ testdata/p256_sha384_intermediate.pem | 32 +++++++++++++++++++ 2 files changed, 41 insertions(+) create mode 100644 testdata/p256_sha384_intermediate.pem diff --git a/src/test/kotlin/provider/KeyAttestationCertPathValidatorTest.kt b/src/test/kotlin/provider/KeyAttestationCertPathValidatorTest.kt index 7b8bdba..6445ca6 100644 --- a/src/test/kotlin/provider/KeyAttestationCertPathValidatorTest.kt +++ b/src/test/kotlin/provider/KeyAttestationCertPathValidatorTest.kt @@ -260,6 +260,15 @@ class KeyAttestationCertPathValidatorTest { assertThat(exception.reason).isEqualTo(BasicReason.EXPIRED) } + @Test + fun p256Sha384Intermediate_succeeds() { + val certPath = readCertPath("p256_sha384_intermediate.pem") + val root = certPath.certificatesWithAnchor.last() + val params = + PKIXParameters(setOf(TrustAnchor(root, null))).apply { date = FakeCalendar.DEFAULT.today() } + certPathValidator.validate(certPath, params) + } + @Test fun forgedKeybox_throwsCertPathValidatorException() { val certPath = Chains.forgedKeybox diff --git a/testdata/p256_sha384_intermediate.pem b/testdata/p256_sha384_intermediate.pem new file mode 100644 index 0000000..df26045 --- /dev/null +++ b/testdata/p256_sha384_intermediate.pem @@ -0,0 +1,32 @@ +-----BEGIN CERTIFICATE----- +MIIBdzCCAR2gAwIBAgIBAjAKBggqhkjOPQQDAzAcMRowGAYDVQQDDBFUZXN0IElu +dGVybWVkaWF0ZTAeFw0yNDAxMDEwMDAwMDBaFw0zNDAxMDEwMDAwMDBaMBQxEjAQ +BgNVBAMMCVRlc3QgTGVhZjBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABBqQpJH6 +/KMGJcJyFrn1O5UFKfgFY0IpfJ6CFoijMxgd0wEkHfJxmr+raceuTykXXVVxdOpy +UoQmZiXBmtw1PNujWDBWMBQGCisGAQQB1nkCAREEBgQEBAIFADAdBgNVHQ4EFgQU +bIoAns4i7eKpY7/wWAY3dBcDKTcwHwYDVR0jBBgwFoAU2PaTsAJyUGeWGHR1//Xk +G5yCfJAwCgYIKoZIzj0EAwMDSAAwRQIhAKkafkMZhoVFTrOE7T8o5oRFV79FzO0/ +P7n8s6KuwiLXAiA5qUmDUXEiI94HddoysULan5oiq73YDh2pwKOxkRv5Qw== +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIBgjCCASigAwIBAgIBATAKBggqhkjOPQQDAzAUMRIwEAYDVQQDDAlUZXN0IFJv +b3QwHhcNMjQwMTAxMDAwMDAwWhcNMzQwMTAxMDAwMDAwWjAcMRowGAYDVQQDDBFU +ZXN0IEludGVybWVkaWF0ZTBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABFjV02Xx +9WHqlJLqliDXgGlIlLenTYGdpCZtDIc5k4ixDBLvQfbMrnjemjjeUssvfJvO8gkj +zO+esSPhHUSIf2ujYzBhMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgEG +MB0GA1UdDgQWBBTY9pOwAnJQZ5YYdHX/9eQbnIJ8kDAfBgNVHSMEGDAWgBSMoD+h +inW5a/FoQO+IyTuL1BuCTDAKBggqhkjOPQQDAwNIADBFAiA7KrlpJpR3GAbeINO8 +pCN4ESFvs4DFGFAJacJdssl9kwIhAIDq2rk9DL1p/FV1mKWFNVyuZqt5HNxhJTiF +qpgCnAgv +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIBfTCCASOgAwIBAgIUMvJabJaz5RJp0wnaNaOtJSEo/GIwCgYIKoZIzj0EAwMw +FDESMBAGA1UEAwwJVGVzdCBSb290MB4XDTI0MDEwMTAwMDAwMFoXDTM0MDEwMTAw +MDAwMFowFDESMBAGA1UEAwwJVGVzdCBSb290MFkwEwYHKoZIzj0CAQYIKoZIzj0D +AQcDQgAE2kjHtGifLOvcHsIo14W46o39Du/ObLitndTPoXyLsNKSmCyNuOXzMVGp +EpXW0fC5KsV+8NqMu0eI3Qm8rOdit6NTMFEwHQYDVR0OBBYEFIygP6GKdblr8WhA +74jJO4vUG4JMMB8GA1UdIwQYMBaAFIygP6GKdblr8WhA74jJO4vUG4JMMA8GA1Ud +EwEB/wQFMAMBAf8wCgYIKoZIzj0EAwMDSAAwRQIgYK7jkjOwDYdpCfSOLtGBYCsx +usE1jbE31QkzBWkXakUCIQCFni4F7fIU4FmfRS+9ct6GYUv2z4qCxLFSx07hOnWB +EA== +-----END CERTIFICATE----- From dac71f982d185551aba42b920d52db5f340be754 Mon Sep 17 00:00:00 2001 From: Carmen Hanish Date: Thu, 9 Jul 2026 02:59:55 -0700 Subject: [PATCH 17/48] Add support to pass in hardcoded root/key. PiperOrigin-RevId: 945004516 --- src/main/kotlin/testing/KeyAttestationCertFactory.kt | 10 +++++++--- .../kotlin/testing/KeyAttestationCertPathFactory.kt | 12 ++++++++++-- 2 files changed, 17 insertions(+), 5 deletions(-) diff --git a/src/main/kotlin/testing/KeyAttestationCertFactory.kt b/src/main/kotlin/testing/KeyAttestationCertFactory.kt index 41b3c74..74926d4 100644 --- a/src/main/kotlin/testing/KeyAttestationCertFactory.kt +++ b/src/main/kotlin/testing/KeyAttestationCertFactory.kt @@ -45,7 +45,11 @@ import org.bouncycastle.cert.jcajce.JcaX509v3CertificateBuilder import org.bouncycastle.operator.ContentSigner import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder -internal class KeyAttestationCertFactory(val fakeCalendar: FakeCalendar = FakeCalendar.DEFAULT) { +internal class KeyAttestationCertFactory( + val fakeCalendar: FakeCalendar = FakeCalendar.DEFAULT, + val hardodedRootKey: KeyPair? = null, + val hardcodedRoot: X509Certificate? = null, +) { private val ecKeyPairGenerator = KeyPairGenerator.getInstance("EC").apply { initialize(ECGenParameterSpec("secp256r1"), FakeSecureRandom()) @@ -60,13 +64,13 @@ internal class KeyAttestationCertFactory(val fakeCalendar: FakeCalendar = FakeCa internal fun generateRsaKeyPair() = rsaKeyPairGenerator.generateKeyPair() - val rootKey = ecKeyPairGenerator.generateKeyPair() + val rootKey = hardodedRootKey ?: ecKeyPairGenerator.generateKeyPair() val intermediateKey = ecKeyPairGenerator.generateKeyPair() val rkpKey = ecKeyPairGenerator.generateKeyPair() val attestationKey = ecKeyPairGenerator.generateKeyPair() val leafKey: KeyPair = ecKeyPairGenerator.generateKeyPair() - val root: X509Certificate = generateRootCertificate() + val root: X509Certificate = hardcodedRoot ?: generateRootCertificate() val factoryIntermediate = generateIntermediateCertificate() val remoteIntermediate = generateIntermediateCertificate(subject = REMOTE_INTERMEDIATE_SUBJECT) val rkpIntermediate = diff --git a/src/main/kotlin/testing/KeyAttestationCertPathFactory.kt b/src/main/kotlin/testing/KeyAttestationCertPathFactory.kt index a6f3f5a..7a3a329 100644 --- a/src/main/kotlin/testing/KeyAttestationCertPathFactory.kt +++ b/src/main/kotlin/testing/KeyAttestationCertPathFactory.kt @@ -20,16 +20,24 @@ import com.android.keyattestation.verifier.KeyDescription import com.android.keyattestation.verifier.SecurityLevel import com.android.keyattestation.verifier.provider.KeyAttestationCertPath import java.math.BigInteger +import java.security.KeyPair import java.security.PublicKey +import java.security.cert.X509Certificate /** * Factory for creating valid [KeyAttestationCertPath] chains for testing. * * @param fakeCalendar the fake calendar for the certificate chains validity period. */ -class KeyAttestationCertPathFactory(val fakeCalendar: FakeCalendar = FakeCalendar()) { +class KeyAttestationCertPathFactory +@JvmOverloads +constructor( + val fakeCalendar: FakeCalendar = FakeCalendar(), + val hardcodedRootKey: KeyPair? = null, + val hardcodedRoot: X509Certificate? = null, +) { private val certFactory: KeyAttestationCertFactory = - KeyAttestationCertFactory(fakeCalendar = fakeCalendar) + KeyAttestationCertFactory(fakeCalendar, hardcodedRootKey, hardcodedRoot) /* The root certificate of all generated certificate chains. */ val root = certFactory.root From 18abe6c426a34d7f07517405d0f714dbd4e8b7b3 Mon Sep 17 00:00:00 2001 From: Carmen Hanish Date: Mon, 13 Jul 2026 05:08:41 -0700 Subject: [PATCH 18/48] Log the signing algorithms of the certificate chain. This change also gives the LogHook methods. Supporting what can be logged should not be a requirement. PiperOrigin-RevId: 946969655 --- src/main/kotlin/Verifier.kt | 24 ++++++++++++----- .../kotlin/provider/KeyAttestationCertPath.kt | 26 +++++++++++++++++++ src/main/kotlin/testing/FakeLogHook.kt | 5 ++++ .../provider/KeyAttestationCertPathTest.kt | 10 +++++++ 4 files changed, 58 insertions(+), 7 deletions(-) diff --git a/src/main/kotlin/Verifier.kt b/src/main/kotlin/Verifier.kt index ed38e6d..f3ae21e 100644 --- a/src/main/kotlin/Verifier.kt +++ b/src/main/kotlin/Verifier.kt @@ -83,14 +83,14 @@ interface VerifyRequestLog { * * @param inputChain The certificate chain which is being verified. */ - fun logInputChain(inputChain: List) + fun logInputChain(inputChain: List) {} /** * Logs the result of the verification. Called for each call to [verify]. * * @param result The result of the verification. */ - fun logResult(result: VerificationResult) + fun logResult(result: VerificationResult) {} /** * Logs the key description of the leaf certificate. Called if [verify] reaches the point where @@ -98,7 +98,7 @@ interface VerifyRequestLog { * * @param keyDescription The key description of the leaf certificate. */ - fun logKeyDescription(keyDescription: KeyDescription) + fun logKeyDescription(keyDescription: KeyDescription) {} /** * Logs the provisioning info map extension of the attestation certificate. Called if [verify] @@ -107,7 +107,7 @@ interface VerifyRequestLog { * * @param provisioningInfoMap The provisioning info map extension of the leaf certificate. */ - fun logProvisioningInfoMap(provisioningInfoMap: ProvisioningInfoMap) + fun logProvisioningInfoMap(provisioningInfoMap: ProvisioningInfoMap) {} /** * Logs the serial numbers of the intermediate certificates in the certificate chain. Called if @@ -116,17 +116,25 @@ interface VerifyRequestLog { * @param certSerialNumbers The serial numbers of the intermediate certificates in the certificate * chain. */ - fun logCertSerialNumbers(certSerialNumbers: List) + fun logCertSerialNumbers(certSerialNumbers: List) {} + + /** + * Logs the certificate signing algorithms of the intermediate certificates in the certificate + * chain. Called if [verify] reaches the point where the certificate chain is validated. + * + * @param certSigningAlgorithms The certificate signing algorithms in the chain. + */ + fun logCertSigningAlgorithms(certSigningAlgorithms: Set) {} /** * Logs an info level message. May be called throughout the verification process. * * @param infoMessage The info level message to log. */ - fun logInfoMessage(infoMessage: String) + fun logInfoMessage(infoMessage: String) {} /* Flushes any buffered logs. Called just before [verify] returns. */ - fun flush() + fun flush() {} } /** @@ -267,6 +275,8 @@ constructor( return VerificationResult.PathValidationFailure(e) } + log?.logCertSigningAlgorithms(certPath.signingAlgorithms()) + val keyDescription = try { checkNotNull( diff --git a/src/main/kotlin/provider/KeyAttestationCertPath.kt b/src/main/kotlin/provider/KeyAttestationCertPath.kt index 8ce5fc6..cb18a30 100644 --- a/src/main/kotlin/provider/KeyAttestationCertPath.kt +++ b/src/main/kotlin/provider/KeyAttestationCertPath.kt @@ -19,9 +19,14 @@ package com.android.keyattestation.verifier.provider import com.android.keyattestation.verifier.SecurityLevel import com.android.keyattestation.verifier.asX509Certificate import com.google.protobuf.ByteString +import java.security.PublicKey import java.security.cert.CertPath import java.security.cert.CertificateException import java.security.cert.X509Certificate +import java.security.interfaces.DSAPublicKey +import java.security.interfaces.ECPublicKey +import java.security.interfaces.RSAPublicKey +import javax.crypto.interfaces.DHPublicKey import javax.security.auth.x500.X500Principal /** @@ -72,6 +77,18 @@ class KeyAttestationCertPath(certs: List) : CertPath("X.509") { */ fun serialNumbers() = certificatesWithAnchor.map { it.serialNumber.toString(16) } + /** + * Returns the signing algorithms of the certificates in the certificate chain. + * + * The format is "algNameKeySizeN". + * + * @return the signing algorithms of the certificates in the certificate chain. + */ + fun signingAlgorithms() = + certificatesWithAnchor + .zipWithNext { cert, issuer -> "${cert.sigAlgName}KeySize${issuer.publicKey.keySize()}" } + .toSet() + fun provisioningMethod() = when { isFactoryProvisioned() -> ProvisioningMethod.FACTORY_PROVISIONED @@ -152,6 +169,15 @@ private fun parseDN(dn: String): Map { return attributes } +private fun PublicKey.keySize() = + when (this) { + is RSAPublicKey -> this.modulus.bitLength().toString() + is ECPublicKey -> this.params.curve.field.fieldSize.toString() + is DSAPublicKey -> this.y.bitLength().toString() + is DHPublicKey -> this.y.bitLength().toString() + else -> "unknown" + } + private fun String?.toSecurityLevel() = when (this) { "TEE" -> SecurityLevel.TRUSTED_ENVIRONMENT diff --git a/src/main/kotlin/testing/FakeLogHook.kt b/src/main/kotlin/testing/FakeLogHook.kt index 5a969bb..e4ac99b 100644 --- a/src/main/kotlin/testing/FakeLogHook.kt +++ b/src/main/kotlin/testing/FakeLogHook.kt @@ -51,6 +51,7 @@ class FakeVerifyRequestLog : VerifyRequestLog { var provisioningInfoMap: ProvisioningInfoMap? = null var certSerialNumbers = mutableListOf() var infoMessages = mutableListOf() + var certSigningAlgorithms = mutableSetOf() override fun logInputChain(inputChain: List) { this.inputChain.addAll(inputChain) @@ -72,6 +73,10 @@ class FakeVerifyRequestLog : VerifyRequestLog { this.certSerialNumbers.addAll(certSerialNumbers) } + override fun logCertSigningAlgorithms(certSigningAlgorithms: Set) { + this.certSigningAlgorithms.addAll(certSigningAlgorithms) + } + override fun logInfoMessage(infoMessage: String) { this.infoMessages.add(infoMessage) } diff --git a/src/test/kotlin/provider/KeyAttestationCertPathTest.kt b/src/test/kotlin/provider/KeyAttestationCertPathTest.kt index cf73b69..bfc99fc 100644 --- a/src/test/kotlin/provider/KeyAttestationCertPathTest.kt +++ b/src/test/kotlin/provider/KeyAttestationCertPathTest.kt @@ -101,6 +101,16 @@ class KeyAttestationCertPathTest { .inOrder() } + @Test + fun signingAlgorithms_returnsExpectedSigningAlgorithms() { + assertThat(readCertPath("blueline/sdk28/TEE_EC_NONE.pem").signingAlgorithms()) + .containsExactly( + "SHA256withECDSAKeySize256", + "SHA256withECDSAKeySize384", + "SHA256withRSAKeySize4096", + ) + } + @Test fun provisioningMethod_returnsExpectedType(@TestParameter testCase: ProvisioningMethodTestCase) { val certPath = readCertPath("${testCase.path}.pem") From 27dcd9f9283c9c95bdfa03127de35e9989816c90 Mon Sep 17 00:00:00 2001 From: Seth Moore Date: Wed, 15 Jul 2026 10:40:53 -0700 Subject: [PATCH 19/48] Add constraint enforcing SecurityLevel matching It's possible that a vulnerable keymint implementation could lie about the security level. e.g. a vulnerable trusted OS could be coerced into claiming it is actually StrongBox. All certificates issued to StrongBox implementations have a StrongBox claim in their certificate (carried within the subject). Add a constraint that checks for this claim and matches it against the Google-signed certificate(s). PiperOrigin-RevId: 948421025 --- src/main/kotlin/ConstraintConfig.kt | 69 +++++++++++++++---- src/main/kotlin/Verifier.kt | 3 +- .../kotlin/provider/KeyAttestationCertPath.kt | 22 ++++-- src/main/kotlin/testing/Certs.kt | 20 ++++++ src/test/kotlin/ConstraintConfigTest.kt | 63 +++++++++++------ src/test/kotlin/VerifierTest.kt | 41 +++++++++++ .../provider/KeyAttestationCertPathTest.kt | 6 +- 7 files changed, 181 insertions(+), 43 deletions(-) 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), } } From d9264f62b4a740faf8ec7904d21d2278be2f18ba Mon Sep 17 00:00:00 2001 From: Carmen Hanish Date: Mon, 20 Jul 2026 03:47:35 -0700 Subject: [PATCH 20/48] Add reference to revocation list to example. PiperOrigin-RevId: 950733549 --- README.md | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 97b2651..5ac3186 100644 --- a/README.md +++ b/README.md @@ -6,11 +6,11 @@ A Kotlin library for verifying Android key attestation certificate chains. ```kotlin // Create a verifier with default, Google-rooted trust anchors, revocation -// info, and time source +// info, and time source. val verifier = Verifier( - GoogleTrustAnchors, // Trust anchors source - { setOf() }, // Revoked serials source - { Instant.now() } // Time source + GoogleTrustAnchors, // Trust anchors source + ::getGoogleRevocationStatusFromWeb, // Revoked serials source + { Instant.now() } // Time source ) // Verify an attestation certificate chain @@ -100,7 +100,15 @@ Android Key Attestation root certificates are documented ## Getting Revoked Serials -The revoked serials may be retrieved from https://android.googleapis.com/attestation/status. +It's important to check the revoked serials list to prevent allowing accepting +fraudulent attestations from known leaked keys. The revoked serials may be +retrieved from https://android.googleapis.com/attestation/status. This list is +updated relatively frequently so it's important to use a fresh copy of the +revocation list. + +The above example will make a network call and parse the revoked serials list +every time `verify` is called. For regularly updated binaries, it may make sense +to build the revoked serial list into the binary. See [here](https://developer.android.com/privacy-and-security/security-key-attestation#certificate_status) for more information about the format of the data. From 2751293c8eecbeaa60e0c94ca1be4b1bbc6975b8 Mon Sep 17 00:00:00 2001 From: Carmen Hanish Date: Mon, 20 Jul 2026 08:08:35 -0700 Subject: [PATCH 21/48] Support software root for testing. PiperOrigin-RevId: 950846227 --- src/main/kotlin/ConstraintConfig.kt | 7 +++ src/main/kotlin/SoftwareRoot.kt | 67 ++++++++++++++++++++--------- src/main/kotlin/Verifier.kt | 13 +++--- src/test/kotlin/VerifierTest.kt | 29 +++++++++---- 4 files changed, 81 insertions(+), 35 deletions(-) diff --git a/src/main/kotlin/ConstraintConfig.kt b/src/main/kotlin/ConstraintConfig.kt index 8e4565f..7339e8d 100644 --- a/src/main/kotlin/ConstraintConfig.kt +++ b/src/main/kotlin/ConstraintConfig.kt @@ -46,6 +46,7 @@ sealed interface Constraint { */ @ThreadSafe class ConstraintConfig( + val allowSoftwareRoot: Boolean = false, val keyOrigin: Constraint? = null, val securityLevel: Constraint? = null, val rootOfTrust: Constraint? = null, @@ -65,6 +66,10 @@ class ConstraintConfig( ) .addAll(additionalConstraints) .build() + + companion object { + fun testDefault(): ConstraintConfig = ConstraintConfig(allowSoftwareRoot = true) + } } /** @@ -72,6 +77,7 @@ class ConstraintConfig( * Kotlin-idiomatic builder function is provided below. */ class ConstraintConfigBuilder() { + var allowSoftwareRoot: Boolean = false var keyOrigin: Constraint? = null var securityLevel: Constraint? = null var rootOfTrust: Constraint? = null @@ -95,6 +101,7 @@ class ConstraintConfigBuilder() { fun build(): ConstraintConfig = ConstraintConfig( + allowSoftwareRoot, keyOrigin, securityLevel, rootOfTrust, diff --git a/src/main/kotlin/SoftwareRoot.kt b/src/main/kotlin/SoftwareRoot.kt index f89e81b..bc544be 100644 --- a/src/main/kotlin/SoftwareRoot.kt +++ b/src/main/kotlin/SoftwareRoot.kt @@ -18,26 +18,51 @@ package com.android.keyattestation.verifier import java.security.cert.X509Certificate -// The software root certificate used by the Android Key Attestation standard. -// https://android.googlesource.com/platform/system/core/+/refs/heads/main/trusty/keymaster/set_attestation_key/keymaster_soft_attestation_keys.xml#97 -private const val GOOGLE_SOFTWARE_ROOT = - """-----BEGIN CERTIFICATE----- -MIICizCCAjKgAwIBAgIJAKIFntEOQ1tXMAoGCCqGSM49BAMCMIGYMQswCQYDVQQG -EwJVUzETMBEGA1UECAwKQ2FsaWZvcm5pYTEWMBQGA1UEBwwNTW91bnRhaW4gVmll -dzEVMBMGA1UECgwMR29vZ2xlLCBJbmMuMRAwDgYDVQQLDAdBbmRyb2lkMTMwMQYD -VQQDDCpBbmRyb2lkIEtleXN0b3JlIFNvZnR3YXJlIEF0dGVzdGF0aW9uIFJvb3Qw -HhcNMTYwMTExMDA0MzUwWhcNMzYwMTA2MDA0MzUwWjCBmDELMAkGA1UEBhMCVVMx -EzARBgNVBAgMCkNhbGlmb3JuaWExFjAUBgNVBAcMDU1vdW50YWluIFZpZXcxFTAT -BgNVBAoMDEdvb2dsZSwgSW5jLjEQMA4GA1UECwwHQW5kcm9pZDEzMDEGA1UEAwwq -QW5kcm9pZCBLZXlzdG9yZSBTb2Z0d2FyZSBBdHRlc3RhdGlvbiBSb290MFkwEwYH -KoZIzj0CAQYIKoZIzj0DAQcDQgAE7l1ex+HA220Dpn7mthvsTWpdamguD/9/SQ59 -dx9EIm29sa/6FsvHrcV30lacqrewLVQBXT5DKyqO107sSHVBpKNjMGEwHQYDVR0O -BBYEFMit6XdMRcOjzw0WEOR5QzohWjDPMB8GA1UdIwQYMBaAFMit6XdMRcOjzw0W -EOR5QzohWjDPMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgKEMAoGCCqG -SM49BAMCA0cAMEQCIDUho++LNEYenNVg8x1YiSBq3KNlQfYNns6KGYxmSGB7AiBN -C/NR2TB8fVvaNTQdqEcbY6WFZTytTySn502vQX3xvw== ------END CERTIFICATE-----""" +public object SoftwareRoot { -val SOFTWARE_ROOT: X509Certificate by lazy { GOOGLE_SOFTWARE_ROOT.asX509Certificate() } + // The software root certificates used by the Android Key Attestation standard. + // https://android.googlesource.com/platform/system/core/+/refs/heads/main/trusty/keymaster/set_attestation_key/keymaster_soft_attestation_keys.xml + private val GOOGLE_SOFTWARE_ROOTS = + listOf( + """-----BEGIN CERTIFICATE----- + MIICizCCAjKgAwIBAgIJAKIFntEOQ1tXMAoGCCqGSM49BAMCMIGYMQswCQYDVQQG + EwJVUzETMBEGA1UECAwKQ2FsaWZvcm5pYTEWMBQGA1UEBwwNTW91bnRhaW4gVmll + dzEVMBMGA1UECgwMR29vZ2xlLCBJbmMuMRAwDgYDVQQLDAdBbmRyb2lkMTMwMQYD + VQQDDCpBbmRyb2lkIEtleXN0b3JlIFNvZnR3YXJlIEF0dGVzdGF0aW9uIFJvb3Qw + HhcNMTYwMTExMDA0MzUwWhcNMzYwMTA2MDA0MzUwWjCBmDELMAkGA1UEBhMCVVMx + EzARBgNVBAgMCkNhbGlmb3JuaWExFjAUBgNVBAcMDU1vdW50YWluIFZpZXcxFTAT + BgNVBAoMDEdvb2dsZSwgSW5jLjEQMA4GA1UECwwHQW5kcm9pZDEzMDEGA1UEAwwq + QW5kcm9pZCBLZXlzdG9yZSBTb2Z0d2FyZSBBdHRlc3RhdGlvbiBSb290MFkwEwYH + KoZIzj0CAQYIKoZIzj0DAQcDQgAE7l1ex+HA220Dpn7mthvsTWpdamguD/9/SQ59 + dx9EIm29sa/6FsvHrcV30lacqrewLVQBXT5DKyqO107sSHVBpKNjMGEwHQYDVR0O + BBYEFMit6XdMRcOjzw0WEOR5QzohWjDPMB8GA1UdIwQYMBaAFMit6XdMRcOjzw0W + EOR5QzohWjDPMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgKEMAoGCCqG + SM49BAMCA0cAMEQCIDUho++LNEYenNVg8x1YiSBq3KNlQfYNns6KGYxmSGB7AiBN + C/NR2TB8fVvaNTQdqEcbY6WFZTytTySn502vQX3xvw== + -----END CERTIFICATE-----""", + """-----BEGIN CERTIFICATE----- + MIICpzCCAhCgAwIBAgIJAP+U2d2fB8gMMA0GCSqGSIb3DQEBCwUAMGMxCzAJBgNV + BAYTAlVTMRMwEQYDVQQIDApDYWxpZm9ybmlhMRYwFAYDVQQHDA1Nb3VudGFpbiBW + aWV3MRUwEwYDVQQKDAxHb29nbGUsIEluYy4xEDAOBgNVBAsMB0FuZHJvaWQwHhcN + MTYwMTA0MTIzMTA4WhcNMzUxMjMwMTIzMTA4WjBjMQswCQYDVQQGEwJVUzETMBEG + A1UECAwKQ2FsaWZvcm5pYTEWMBQGA1UEBwwNTW91bnRhaW4gVmlldzEVMBMGA1UE + CgwMR29vZ2xlLCBJbmMuMRAwDgYDVQQLDAdBbmRyb2lkMIGfMA0GCSqGSIb3DQEB + AQUAA4GNADCBiQKBgQCia63rbi5EYe/VDoLmt5TRdSMfd5tjkWP/96r/C3JHTsAs + Q+wzfNes7UA+jCigZtX3hwszl94OuE4TQKuvpSe/lWmgMdsGUmX4RFlXYfC78hdL + t0GAZMAoDo9Sd47b0ke2RekZyOmLw9vCkT/X11DEHTVm+Vfkl5YLCazOkjWFmwID + AQABo2MwYTAdBgNVHQ4EFgQUKfrxrMxN0kyWQCd1trDpMuUH/i4wHwYDVR0jBBgw + FoAUKfrxrMxN0kyWQCd1trDpMuUH/i4wDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8B + Af8EBAMCAoQwDQYJKoZIhvcNAQELBQADgYEAT3LzNlmNDsG5dFsxWfbwjSVJMJ6j + HBwp0kUtILlNX2S06IDHeHqcOd6os/W/L3BfRxBcxebrTQaZYdKumgf/93y4q+uc + DyQHXrF/unlx/U1bnt8Uqf7f7XzAiF343ZtkMlbVNZriE/mPzsF83O+kqrJVw4Op + Lvtc9mL1J1IXvmM= + -----END CERTIFICATE-----""", + ) -internal fun X509Certificate.isSoftwareRoot() = this.publicKey == SOFTWARE_ROOT.publicKey + val SOFTWARE_ROOTS: List by lazy { + GOOGLE_SOFTWARE_ROOTS.map { it.asX509Certificate() } + } + + internal fun X509Certificate.isSoftwareRoot() = + SoftwareRoot.SOFTWARE_ROOTS.map { it.publicKey }.contains(this.publicKey) +} diff --git a/src/main/kotlin/Verifier.kt b/src/main/kotlin/Verifier.kt index fc69182..01cbb9d 100644 --- a/src/main/kotlin/Verifier.kt +++ b/src/main/kotlin/Verifier.kt @@ -17,6 +17,7 @@ package com.android.keyattestation.verifier import androidx.annotation.RequiresApi +import com.android.keyattestation.verifier.SoftwareRoot.isSoftwareRoot import com.android.keyattestation.verifier.provider.KeyAttestationCertPath import com.android.keyattestation.verifier.provider.KeyAttestationProvider import com.android.keyattestation.verifier.provider.ProvisioningMethod @@ -155,11 +156,13 @@ constructor( ) { init { Security.addProvider(KeyAttestationProvider()) - for (anchor in trustAnchorsSource()) { - if (anchor.trustedCert?.isSoftwareRoot() == true) { - throw IllegalArgumentException( - "Software attestation root cannot be used as a trust anchor." - ) + if (!constraintConfig.allowSoftwareRoot) { + for (anchor in trustAnchorsSource()) { + if (anchor.trustedCert?.isSoftwareRoot() == true) { + throw IllegalArgumentException( + "Software attestation root cannot be used as a trust anchor." + ) + } } } } diff --git a/src/test/kotlin/VerifierTest.kt b/src/test/kotlin/VerifierTest.kt index 379c7ab..0b08445 100644 --- a/src/test/kotlin/VerifierTest.kt +++ b/src/test/kotlin/VerifierTest.kt @@ -17,6 +17,7 @@ package com.android.keyattestation.verifier import kotlin.io.path.Path +import com.android.keyattestation.verifier.SoftwareRoot.SOFTWARE_ROOTS import com.android.keyattestation.verifier.VerificationResult.ConstraintViolation import com.android.keyattestation.verifier.VerificationResult.ExtensionParsingFailure import com.android.keyattestation.verifier.VerificationResult.PathValidationFailure @@ -87,25 +88,31 @@ class VerifierTest { for (pemPath in pemFiles) { val subpath = "${model}/sdk${sdk}/${pemPath.nameWithoutExtension}" val json = readJson("${subpath}.json") - - // TODO(google-internal bug): update here once sw root is supported. - if (json.attestationSecurityLevel == SecurityLevel.SOFTWARE) { - continue - } - val creationDateTime = json.softwareEnforced.creationDateTime ?: json.hardwareEnforced.creationDateTime assertThat(creationDateTime).isNotNull() val timestamp = Instant.ofEpochMilli(creationDateTime!!.toLong()) - val verifier = Verifier({ prodAnchors }, { setOf() }, { timestamp }) + val verifier = + Verifier( + { prodAnchors + SOFTWARE_ROOTS.map { TrustAnchor(it, null) } }, + { setOf() }, + { timestamp }, + ConstraintConfig( + allowSoftwareRoot = true, + securityLevel = IgnoredConstraint, + rootOfTrust = IgnoredConstraint, + ), + ) val chain = readCertList("${subpath}.pem") val result = assertIs(verifier.verify(chain)) assertThat(result.publicKey).isEqualTo(chain[0].publicKey) assertThat(result.challenge).isEqualTo(json.attestationChallenge) assertThat(result.securityLevel).isEqualTo(json.attestationSecurityLevel) assertThat(result.verifiedBootState) - .isEqualTo(json.hardwareEnforced.rootOfTrust?.verifiedBootState) + .isEqualTo( + json.hardwareEnforced.rootOfTrust?.verifiedBootState ?: VerifiedBootState.UNVERIFIED + ) assertThat(result.deviceLocked) .isEqualTo(json.hardwareEnforced.rootOfTrust?.deviceLocked ?: false) } @@ -369,7 +376,11 @@ class VerifierTest { @Test fun init_softwareRootAsTrustAnchor_fails() { assertFailsWith { - Verifier({ setOf(TrustAnchor(SOFTWARE_ROOT, null)) }, { setOf() }, { Instant.now() }) + Verifier( + { setOf(TrustAnchor(SOFTWARE_ROOTS.first(), null)) }, + { setOf() }, + { Instant.now() }, + ) } } } From bf3ccbfc2c535fc4e02413747bd9db820e51db45 Mon Sep 17 00:00:00 2001 From: Seth Moore Date: Mon, 20 Jul 2026 11:10:19 -0700 Subject: [PATCH 22/48] Support software root for testing. PiperOrigin-RevId: 950945125 --- src/main/kotlin/ConstraintConfig.kt | 7 --- src/main/kotlin/SoftwareRoot.kt | 67 +++++++++-------------------- src/main/kotlin/Verifier.kt | 13 +++--- src/test/kotlin/VerifierTest.kt | 29 ++++--------- 4 files changed, 35 insertions(+), 81 deletions(-) diff --git a/src/main/kotlin/ConstraintConfig.kt b/src/main/kotlin/ConstraintConfig.kt index 7339e8d..8e4565f 100644 --- a/src/main/kotlin/ConstraintConfig.kt +++ b/src/main/kotlin/ConstraintConfig.kt @@ -46,7 +46,6 @@ sealed interface Constraint { */ @ThreadSafe class ConstraintConfig( - val allowSoftwareRoot: Boolean = false, val keyOrigin: Constraint? = null, val securityLevel: Constraint? = null, val rootOfTrust: Constraint? = null, @@ -66,10 +65,6 @@ class ConstraintConfig( ) .addAll(additionalConstraints) .build() - - companion object { - fun testDefault(): ConstraintConfig = ConstraintConfig(allowSoftwareRoot = true) - } } /** @@ -77,7 +72,6 @@ class ConstraintConfig( * Kotlin-idiomatic builder function is provided below. */ class ConstraintConfigBuilder() { - var allowSoftwareRoot: Boolean = false var keyOrigin: Constraint? = null var securityLevel: Constraint? = null var rootOfTrust: Constraint? = null @@ -101,7 +95,6 @@ class ConstraintConfigBuilder() { fun build(): ConstraintConfig = ConstraintConfig( - allowSoftwareRoot, keyOrigin, securityLevel, rootOfTrust, diff --git a/src/main/kotlin/SoftwareRoot.kt b/src/main/kotlin/SoftwareRoot.kt index bc544be..f89e81b 100644 --- a/src/main/kotlin/SoftwareRoot.kt +++ b/src/main/kotlin/SoftwareRoot.kt @@ -18,51 +18,26 @@ package com.android.keyattestation.verifier import java.security.cert.X509Certificate -public object SoftwareRoot { +// The software root certificate used by the Android Key Attestation standard. +// https://android.googlesource.com/platform/system/core/+/refs/heads/main/trusty/keymaster/set_attestation_key/keymaster_soft_attestation_keys.xml#97 +private const val GOOGLE_SOFTWARE_ROOT = + """-----BEGIN CERTIFICATE----- +MIICizCCAjKgAwIBAgIJAKIFntEOQ1tXMAoGCCqGSM49BAMCMIGYMQswCQYDVQQG +EwJVUzETMBEGA1UECAwKQ2FsaWZvcm5pYTEWMBQGA1UEBwwNTW91bnRhaW4gVmll +dzEVMBMGA1UECgwMR29vZ2xlLCBJbmMuMRAwDgYDVQQLDAdBbmRyb2lkMTMwMQYD +VQQDDCpBbmRyb2lkIEtleXN0b3JlIFNvZnR3YXJlIEF0dGVzdGF0aW9uIFJvb3Qw +HhcNMTYwMTExMDA0MzUwWhcNMzYwMTA2MDA0MzUwWjCBmDELMAkGA1UEBhMCVVMx +EzARBgNVBAgMCkNhbGlmb3JuaWExFjAUBgNVBAcMDU1vdW50YWluIFZpZXcxFTAT +BgNVBAoMDEdvb2dsZSwgSW5jLjEQMA4GA1UECwwHQW5kcm9pZDEzMDEGA1UEAwwq +QW5kcm9pZCBLZXlzdG9yZSBTb2Z0d2FyZSBBdHRlc3RhdGlvbiBSb290MFkwEwYH +KoZIzj0CAQYIKoZIzj0DAQcDQgAE7l1ex+HA220Dpn7mthvsTWpdamguD/9/SQ59 +dx9EIm29sa/6FsvHrcV30lacqrewLVQBXT5DKyqO107sSHVBpKNjMGEwHQYDVR0O +BBYEFMit6XdMRcOjzw0WEOR5QzohWjDPMB8GA1UdIwQYMBaAFMit6XdMRcOjzw0W +EOR5QzohWjDPMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgKEMAoGCCqG +SM49BAMCA0cAMEQCIDUho++LNEYenNVg8x1YiSBq3KNlQfYNns6KGYxmSGB7AiBN +C/NR2TB8fVvaNTQdqEcbY6WFZTytTySn502vQX3xvw== +-----END CERTIFICATE-----""" - // The software root certificates used by the Android Key Attestation standard. - // https://android.googlesource.com/platform/system/core/+/refs/heads/main/trusty/keymaster/set_attestation_key/keymaster_soft_attestation_keys.xml - private val GOOGLE_SOFTWARE_ROOTS = - listOf( - """-----BEGIN CERTIFICATE----- - MIICizCCAjKgAwIBAgIJAKIFntEOQ1tXMAoGCCqGSM49BAMCMIGYMQswCQYDVQQG - EwJVUzETMBEGA1UECAwKQ2FsaWZvcm5pYTEWMBQGA1UEBwwNTW91bnRhaW4gVmll - dzEVMBMGA1UECgwMR29vZ2xlLCBJbmMuMRAwDgYDVQQLDAdBbmRyb2lkMTMwMQYD - VQQDDCpBbmRyb2lkIEtleXN0b3JlIFNvZnR3YXJlIEF0dGVzdGF0aW9uIFJvb3Qw - HhcNMTYwMTExMDA0MzUwWhcNMzYwMTA2MDA0MzUwWjCBmDELMAkGA1UEBhMCVVMx - EzARBgNVBAgMCkNhbGlmb3JuaWExFjAUBgNVBAcMDU1vdW50YWluIFZpZXcxFTAT - BgNVBAoMDEdvb2dsZSwgSW5jLjEQMA4GA1UECwwHQW5kcm9pZDEzMDEGA1UEAwwq - QW5kcm9pZCBLZXlzdG9yZSBTb2Z0d2FyZSBBdHRlc3RhdGlvbiBSb290MFkwEwYH - KoZIzj0CAQYIKoZIzj0DAQcDQgAE7l1ex+HA220Dpn7mthvsTWpdamguD/9/SQ59 - dx9EIm29sa/6FsvHrcV30lacqrewLVQBXT5DKyqO107sSHVBpKNjMGEwHQYDVR0O - BBYEFMit6XdMRcOjzw0WEOR5QzohWjDPMB8GA1UdIwQYMBaAFMit6XdMRcOjzw0W - EOR5QzohWjDPMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgKEMAoGCCqG - SM49BAMCA0cAMEQCIDUho++LNEYenNVg8x1YiSBq3KNlQfYNns6KGYxmSGB7AiBN - C/NR2TB8fVvaNTQdqEcbY6WFZTytTySn502vQX3xvw== - -----END CERTIFICATE-----""", - """-----BEGIN CERTIFICATE----- - MIICpzCCAhCgAwIBAgIJAP+U2d2fB8gMMA0GCSqGSIb3DQEBCwUAMGMxCzAJBgNV - BAYTAlVTMRMwEQYDVQQIDApDYWxpZm9ybmlhMRYwFAYDVQQHDA1Nb3VudGFpbiBW - aWV3MRUwEwYDVQQKDAxHb29nbGUsIEluYy4xEDAOBgNVBAsMB0FuZHJvaWQwHhcN - MTYwMTA0MTIzMTA4WhcNMzUxMjMwMTIzMTA4WjBjMQswCQYDVQQGEwJVUzETMBEG - A1UECAwKQ2FsaWZvcm5pYTEWMBQGA1UEBwwNTW91bnRhaW4gVmlldzEVMBMGA1UE - CgwMR29vZ2xlLCBJbmMuMRAwDgYDVQQLDAdBbmRyb2lkMIGfMA0GCSqGSIb3DQEB - AQUAA4GNADCBiQKBgQCia63rbi5EYe/VDoLmt5TRdSMfd5tjkWP/96r/C3JHTsAs - Q+wzfNes7UA+jCigZtX3hwszl94OuE4TQKuvpSe/lWmgMdsGUmX4RFlXYfC78hdL - t0GAZMAoDo9Sd47b0ke2RekZyOmLw9vCkT/X11DEHTVm+Vfkl5YLCazOkjWFmwID - AQABo2MwYTAdBgNVHQ4EFgQUKfrxrMxN0kyWQCd1trDpMuUH/i4wHwYDVR0jBBgw - FoAUKfrxrMxN0kyWQCd1trDpMuUH/i4wDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8B - Af8EBAMCAoQwDQYJKoZIhvcNAQELBQADgYEAT3LzNlmNDsG5dFsxWfbwjSVJMJ6j - HBwp0kUtILlNX2S06IDHeHqcOd6os/W/L3BfRxBcxebrTQaZYdKumgf/93y4q+uc - DyQHXrF/unlx/U1bnt8Uqf7f7XzAiF343ZtkMlbVNZriE/mPzsF83O+kqrJVw4Op - Lvtc9mL1J1IXvmM= - -----END CERTIFICATE-----""", - ) +val SOFTWARE_ROOT: X509Certificate by lazy { GOOGLE_SOFTWARE_ROOT.asX509Certificate() } - val SOFTWARE_ROOTS: List by lazy { - GOOGLE_SOFTWARE_ROOTS.map { it.asX509Certificate() } - } - - internal fun X509Certificate.isSoftwareRoot() = - SoftwareRoot.SOFTWARE_ROOTS.map { it.publicKey }.contains(this.publicKey) -} +internal fun X509Certificate.isSoftwareRoot() = this.publicKey == SOFTWARE_ROOT.publicKey diff --git a/src/main/kotlin/Verifier.kt b/src/main/kotlin/Verifier.kt index 01cbb9d..fc69182 100644 --- a/src/main/kotlin/Verifier.kt +++ b/src/main/kotlin/Verifier.kt @@ -17,7 +17,6 @@ package com.android.keyattestation.verifier import androidx.annotation.RequiresApi -import com.android.keyattestation.verifier.SoftwareRoot.isSoftwareRoot import com.android.keyattestation.verifier.provider.KeyAttestationCertPath import com.android.keyattestation.verifier.provider.KeyAttestationProvider import com.android.keyattestation.verifier.provider.ProvisioningMethod @@ -156,13 +155,11 @@ constructor( ) { init { Security.addProvider(KeyAttestationProvider()) - if (!constraintConfig.allowSoftwareRoot) { - for (anchor in trustAnchorsSource()) { - if (anchor.trustedCert?.isSoftwareRoot() == true) { - throw IllegalArgumentException( - "Software attestation root cannot be used as a trust anchor." - ) - } + for (anchor in trustAnchorsSource()) { + if (anchor.trustedCert?.isSoftwareRoot() == true) { + throw IllegalArgumentException( + "Software attestation root cannot be used as a trust anchor." + ) } } } diff --git a/src/test/kotlin/VerifierTest.kt b/src/test/kotlin/VerifierTest.kt index 0b08445..379c7ab 100644 --- a/src/test/kotlin/VerifierTest.kt +++ b/src/test/kotlin/VerifierTest.kt @@ -17,7 +17,6 @@ package com.android.keyattestation.verifier import kotlin.io.path.Path -import com.android.keyattestation.verifier.SoftwareRoot.SOFTWARE_ROOTS import com.android.keyattestation.verifier.VerificationResult.ConstraintViolation import com.android.keyattestation.verifier.VerificationResult.ExtensionParsingFailure import com.android.keyattestation.verifier.VerificationResult.PathValidationFailure @@ -88,31 +87,25 @@ class VerifierTest { for (pemPath in pemFiles) { val subpath = "${model}/sdk${sdk}/${pemPath.nameWithoutExtension}" val json = readJson("${subpath}.json") + + // TODO(google-internal bug): update here once sw root is supported. + if (json.attestationSecurityLevel == SecurityLevel.SOFTWARE) { + continue + } + val creationDateTime = json.softwareEnforced.creationDateTime ?: json.hardwareEnforced.creationDateTime assertThat(creationDateTime).isNotNull() val timestamp = Instant.ofEpochMilli(creationDateTime!!.toLong()) - val verifier = - Verifier( - { prodAnchors + SOFTWARE_ROOTS.map { TrustAnchor(it, null) } }, - { setOf() }, - { timestamp }, - ConstraintConfig( - allowSoftwareRoot = true, - securityLevel = IgnoredConstraint, - rootOfTrust = IgnoredConstraint, - ), - ) + val verifier = Verifier({ prodAnchors }, { setOf() }, { timestamp }) val chain = readCertList("${subpath}.pem") val result = assertIs(verifier.verify(chain)) assertThat(result.publicKey).isEqualTo(chain[0].publicKey) assertThat(result.challenge).isEqualTo(json.attestationChallenge) assertThat(result.securityLevel).isEqualTo(json.attestationSecurityLevel) assertThat(result.verifiedBootState) - .isEqualTo( - json.hardwareEnforced.rootOfTrust?.verifiedBootState ?: VerifiedBootState.UNVERIFIED - ) + .isEqualTo(json.hardwareEnforced.rootOfTrust?.verifiedBootState) assertThat(result.deviceLocked) .isEqualTo(json.hardwareEnforced.rootOfTrust?.deviceLocked ?: false) } @@ -376,11 +369,7 @@ class VerifierTest { @Test fun init_softwareRootAsTrustAnchor_fails() { assertFailsWith { - Verifier( - { setOf(TrustAnchor(SOFTWARE_ROOTS.first(), null)) }, - { setOf() }, - { Instant.now() }, - ) + Verifier({ setOf(TrustAnchor(SOFTWARE_ROOT, null)) }, { setOf() }, { Instant.now() }) } } } From 47f970d044ae4da7b00da068e7e1a4e952b0fd2f Mon Sep 17 00:00:00 2001 From: Carmen Hanish Date: Tue, 21 Jul 2026 13:06:43 -0700 Subject: [PATCH 23/48] Support software root for testing. PiperOrigin-RevId: 951655871 --- src/main/kotlin/ConstraintConfig.kt | 7 +++ src/main/kotlin/SoftwareRoot.kt | 73 ++++++++++++++++++++--------- src/main/kotlin/Verifier.kt | 13 +++-- src/test/kotlin/VerifierTest.kt | 29 ++++++++---- 4 files changed, 87 insertions(+), 35 deletions(-) diff --git a/src/main/kotlin/ConstraintConfig.kt b/src/main/kotlin/ConstraintConfig.kt index 8e4565f..7339e8d 100644 --- a/src/main/kotlin/ConstraintConfig.kt +++ b/src/main/kotlin/ConstraintConfig.kt @@ -46,6 +46,7 @@ sealed interface Constraint { */ @ThreadSafe class ConstraintConfig( + val allowSoftwareRoot: Boolean = false, val keyOrigin: Constraint? = null, val securityLevel: Constraint? = null, val rootOfTrust: Constraint? = null, @@ -65,6 +66,10 @@ class ConstraintConfig( ) .addAll(additionalConstraints) .build() + + companion object { + fun testDefault(): ConstraintConfig = ConstraintConfig(allowSoftwareRoot = true) + } } /** @@ -72,6 +77,7 @@ class ConstraintConfig( * Kotlin-idiomatic builder function is provided below. */ class ConstraintConfigBuilder() { + var allowSoftwareRoot: Boolean = false var keyOrigin: Constraint? = null var securityLevel: Constraint? = null var rootOfTrust: Constraint? = null @@ -95,6 +101,7 @@ class ConstraintConfigBuilder() { fun build(): ConstraintConfig = ConstraintConfig( + allowSoftwareRoot, keyOrigin, securityLevel, rootOfTrust, diff --git a/src/main/kotlin/SoftwareRoot.kt b/src/main/kotlin/SoftwareRoot.kt index f89e81b..0fe9714 100644 --- a/src/main/kotlin/SoftwareRoot.kt +++ b/src/main/kotlin/SoftwareRoot.kt @@ -18,26 +18,57 @@ package com.android.keyattestation.verifier import java.security.cert.X509Certificate -// The software root certificate used by the Android Key Attestation standard. -// https://android.googlesource.com/platform/system/core/+/refs/heads/main/trusty/keymaster/set_attestation_key/keymaster_soft_attestation_keys.xml#97 -private const val GOOGLE_SOFTWARE_ROOT = - """-----BEGIN CERTIFICATE----- -MIICizCCAjKgAwIBAgIJAKIFntEOQ1tXMAoGCCqGSM49BAMCMIGYMQswCQYDVQQG -EwJVUzETMBEGA1UECAwKQ2FsaWZvcm5pYTEWMBQGA1UEBwwNTW91bnRhaW4gVmll -dzEVMBMGA1UECgwMR29vZ2xlLCBJbmMuMRAwDgYDVQQLDAdBbmRyb2lkMTMwMQYD -VQQDDCpBbmRyb2lkIEtleXN0b3JlIFNvZnR3YXJlIEF0dGVzdGF0aW9uIFJvb3Qw -HhcNMTYwMTExMDA0MzUwWhcNMzYwMTA2MDA0MzUwWjCBmDELMAkGA1UEBhMCVVMx -EzARBgNVBAgMCkNhbGlmb3JuaWExFjAUBgNVBAcMDU1vdW50YWluIFZpZXcxFTAT -BgNVBAoMDEdvb2dsZSwgSW5jLjEQMA4GA1UECwwHQW5kcm9pZDEzMDEGA1UEAwwq -QW5kcm9pZCBLZXlzdG9yZSBTb2Z0d2FyZSBBdHRlc3RhdGlvbiBSb290MFkwEwYH -KoZIzj0CAQYIKoZIzj0DAQcDQgAE7l1ex+HA220Dpn7mthvsTWpdamguD/9/SQ59 -dx9EIm29sa/6FsvHrcV30lacqrewLVQBXT5DKyqO107sSHVBpKNjMGEwHQYDVR0O -BBYEFMit6XdMRcOjzw0WEOR5QzohWjDPMB8GA1UdIwQYMBaAFMit6XdMRcOjzw0W -EOR5QzohWjDPMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgKEMAoGCCqG -SM49BAMCA0cAMEQCIDUho++LNEYenNVg8x1YiSBq3KNlQfYNns6KGYxmSGB7AiBN -C/NR2TB8fVvaNTQdqEcbY6WFZTytTySn502vQX3xvw== ------END CERTIFICATE-----""" +object SoftwareRoot { -val SOFTWARE_ROOT: X509Certificate by lazy { GOOGLE_SOFTWARE_ROOT.asX509Certificate() } + // The software root certificates used by the Android Key Attestation standard. + // https://android.googlesource.com/platform/system/core/+/refs/heads/main/trusty/keymaster/set_attestation_key/keymaster_soft_attestation_keys.xml + private val GOOGLE_SOFTWARE_ROOTS = + listOf( + """ + -----BEGIN CERTIFICATE----- + MIICizCCAjKgAwIBAgIJAKIFntEOQ1tXMAoGCCqGSM49BAMCMIGYMQswCQYDVQQG + EwJVUzETMBEGA1UECAwKQ2FsaWZvcm5pYTEWMBQGA1UEBwwNTW91bnRhaW4gVmll + dzEVMBMGA1UECgwMR29vZ2xlLCBJbmMuMRAwDgYDVQQLDAdBbmRyb2lkMTMwMQYD + VQQDDCpBbmRyb2lkIEtleXN0b3JlIFNvZnR3YXJlIEF0dGVzdGF0aW9uIFJvb3Qw + HhcNMTYwMTExMDA0MzUwWhcNMzYwMTA2MDA0MzUwWjCBmDELMAkGA1UEBhMCVVMx + EzARBgNVBAgMCkNhbGlmb3JuaWExFjAUBgNVBAcMDU1vdW50YWluIFZpZXcxFTAT + BgNVBAoMDEdvb2dsZSwgSW5jLjEQMA4GA1UECwwHQW5kcm9pZDEzMDEGA1UEAwwq + QW5kcm9pZCBLZXlzdG9yZSBTb2Z0d2FyZSBBdHRlc3RhdGlvbiBSb290MFkwEwYH + KoZIzj0CAQYIKoZIzj0DAQcDQgAE7l1ex+HA220Dpn7mthvsTWpdamguD/9/SQ59 + dx9EIm29sa/6FsvHrcV30lacqrewLVQBXT5DKyqO107sSHVBpKNjMGEwHQYDVR0O + BBYEFMit6XdMRcOjzw0WEOR5QzohWjDPMB8GA1UdIwQYMBaAFMit6XdMRcOjzw0W + EOR5QzohWjDPMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgKEMAoGCCqG + SM49BAMCA0cAMEQCIDUho++LNEYenNVg8x1YiSBq3KNlQfYNns6KGYxmSGB7AiBN + C/NR2TB8fVvaNTQdqEcbY6WFZTytTySn502vQX3xvw== + -----END CERTIFICATE----- + """ + .trimIndent(), + """ + -----BEGIN CERTIFICATE----- + MIICpzCCAhCgAwIBAgIJAP+U2d2fB8gMMA0GCSqGSIb3DQEBCwUAMGMxCzAJBgNV + BAYTAlVTMRMwEQYDVQQIDApDYWxpZm9ybmlhMRYwFAYDVQQHDA1Nb3VudGFpbiBW + aWV3MRUwEwYDVQQKDAxHb29nbGUsIEluYy4xEDAOBgNVBAsMB0FuZHJvaWQwHhcN + MTYwMTA0MTIzMTA4WhcNMzUxMjMwMTIzMTA4WjBjMQswCQYDVQQGEwJVUzETMBEG + A1UECAwKQ2FsaWZvcm5pYTEWMBQGA1UEBwwNTW91bnRhaW4gVmlldzEVMBMGA1UE + CgwMR29vZ2xlLCBJbmMuMRAwDgYDVQQLDAdBbmRyb2lkMIGfMA0GCSqGSIb3DQEB + AQUAA4GNADCBiQKBgQCia63rbi5EYe/VDoLmt5TRdSMfd5tjkWP/96r/C3JHTsAs + Q+wzfNes7UA+jCigZtX3hwszl94OuE4TQKuvpSe/lWmgMdsGUmX4RFlXYfC78hdL + t0GAZMAoDo9Sd47b0ke2RekZyOmLw9vCkT/X11DEHTVm+Vfkl5YLCazOkjWFmwID + AQABo2MwYTAdBgNVHQ4EFgQUKfrxrMxN0kyWQCd1trDpMuUH/i4wHwYDVR0jBBgw + FoAUKfrxrMxN0kyWQCd1trDpMuUH/i4wDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8B + Af8EBAMCAoQwDQYJKoZIhvcNAQELBQADgYEAT3LzNlmNDsG5dFsxWfbwjSVJMJ6j + HBwp0kUtILlNX2S06IDHeHqcOd6os/W/L3BfRxBcxebrTQaZYdKumgf/93y4q+uc + DyQHXrF/unlx/U1bnt8Uqf7f7XzAiF343ZtkMlbVNZriE/mPzsF83O+kqrJVw4Op + Lvtc9mL1J1IXvmM= + -----END CERTIFICATE----- + """ + .trimIndent(), + ) -internal fun X509Certificate.isSoftwareRoot() = this.publicKey == SOFTWARE_ROOT.publicKey + val SOFTWARE_ROOTS: List by lazy { + GOOGLE_SOFTWARE_ROOTS.map { it.asX509Certificate() } + } + + internal fun X509Certificate.isSoftwareRoot() = + SoftwareRoot.SOFTWARE_ROOTS.map { it.publicKey }.contains(this.publicKey) +} diff --git a/src/main/kotlin/Verifier.kt b/src/main/kotlin/Verifier.kt index fc69182..01cbb9d 100644 --- a/src/main/kotlin/Verifier.kt +++ b/src/main/kotlin/Verifier.kt @@ -17,6 +17,7 @@ package com.android.keyattestation.verifier import androidx.annotation.RequiresApi +import com.android.keyattestation.verifier.SoftwareRoot.isSoftwareRoot import com.android.keyattestation.verifier.provider.KeyAttestationCertPath import com.android.keyattestation.verifier.provider.KeyAttestationProvider import com.android.keyattestation.verifier.provider.ProvisioningMethod @@ -155,11 +156,13 @@ constructor( ) { init { Security.addProvider(KeyAttestationProvider()) - for (anchor in trustAnchorsSource()) { - if (anchor.trustedCert?.isSoftwareRoot() == true) { - throw IllegalArgumentException( - "Software attestation root cannot be used as a trust anchor." - ) + if (!constraintConfig.allowSoftwareRoot) { + for (anchor in trustAnchorsSource()) { + if (anchor.trustedCert?.isSoftwareRoot() == true) { + throw IllegalArgumentException( + "Software attestation root cannot be used as a trust anchor." + ) + } } } } diff --git a/src/test/kotlin/VerifierTest.kt b/src/test/kotlin/VerifierTest.kt index 379c7ab..0b08445 100644 --- a/src/test/kotlin/VerifierTest.kt +++ b/src/test/kotlin/VerifierTest.kt @@ -17,6 +17,7 @@ package com.android.keyattestation.verifier import kotlin.io.path.Path +import com.android.keyattestation.verifier.SoftwareRoot.SOFTWARE_ROOTS import com.android.keyattestation.verifier.VerificationResult.ConstraintViolation import com.android.keyattestation.verifier.VerificationResult.ExtensionParsingFailure import com.android.keyattestation.verifier.VerificationResult.PathValidationFailure @@ -87,25 +88,31 @@ class VerifierTest { for (pemPath in pemFiles) { val subpath = "${model}/sdk${sdk}/${pemPath.nameWithoutExtension}" val json = readJson("${subpath}.json") - - // TODO(google-internal bug): update here once sw root is supported. - if (json.attestationSecurityLevel == SecurityLevel.SOFTWARE) { - continue - } - val creationDateTime = json.softwareEnforced.creationDateTime ?: json.hardwareEnforced.creationDateTime assertThat(creationDateTime).isNotNull() val timestamp = Instant.ofEpochMilli(creationDateTime!!.toLong()) - val verifier = Verifier({ prodAnchors }, { setOf() }, { timestamp }) + val verifier = + Verifier( + { prodAnchors + SOFTWARE_ROOTS.map { TrustAnchor(it, null) } }, + { setOf() }, + { timestamp }, + ConstraintConfig( + allowSoftwareRoot = true, + securityLevel = IgnoredConstraint, + rootOfTrust = IgnoredConstraint, + ), + ) val chain = readCertList("${subpath}.pem") val result = assertIs(verifier.verify(chain)) assertThat(result.publicKey).isEqualTo(chain[0].publicKey) assertThat(result.challenge).isEqualTo(json.attestationChallenge) assertThat(result.securityLevel).isEqualTo(json.attestationSecurityLevel) assertThat(result.verifiedBootState) - .isEqualTo(json.hardwareEnforced.rootOfTrust?.verifiedBootState) + .isEqualTo( + json.hardwareEnforced.rootOfTrust?.verifiedBootState ?: VerifiedBootState.UNVERIFIED + ) assertThat(result.deviceLocked) .isEqualTo(json.hardwareEnforced.rootOfTrust?.deviceLocked ?: false) } @@ -369,7 +376,11 @@ class VerifierTest { @Test fun init_softwareRootAsTrustAnchor_fails() { assertFailsWith { - Verifier({ setOf(TrustAnchor(SOFTWARE_ROOT, null)) }, { setOf() }, { Instant.now() }) + Verifier( + { setOf(TrustAnchor(SOFTWARE_ROOTS.first(), null)) }, + { setOf() }, + { Instant.now() }, + ) } } } From 4d1362ed6e98a61eba7cd69ccd836fe0a3592ea3 Mon Sep 17 00:00:00 2001 From: Seth Moore Date: Wed, 19 Aug 2026 11:49:24 -0700 Subject: [PATCH 24/48] Remove RequiresApi annotations as we no longer need them PiperOrigin-RevId: 967327969 --- src/main/kotlin/ConstraintConfig.kt | 4 ---- src/main/kotlin/Verifier.kt | 4 ---- 2 files changed, 8 deletions(-) diff --git a/src/main/kotlin/ConstraintConfig.kt b/src/main/kotlin/ConstraintConfig.kt index 7339e8d..e7101aa 100644 --- a/src/main/kotlin/ConstraintConfig.kt +++ b/src/main/kotlin/ConstraintConfig.kt @@ -16,7 +16,6 @@ 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 @@ -52,7 +51,6 @@ class ConstraintConfig( val rootOfTrust: Constraint? = null, val additionalConstraints: ImmutableList = ImmutableList.of(), ) { - @RequiresApi(24) fun getConstraints() = ImmutableList.builder() .add( @@ -165,7 +163,6 @@ sealed class AttributeConstraint(override val label: String, val mapper: * Android attestation certificate. */ @Immutable -@RequiresApi(24) sealed class SecurityLevelConstraint( val isSatisfied: (KeyDescription, KeyAttestationCertPath) -> Boolean ) : Constraint { @@ -260,7 +257,6 @@ sealed class SecurityLevelConstraint( * an Android attestation certificate. */ @Immutable -@RequiresApi(24) sealed class TagOrderConstraint : Constraint { override val label = "Tag order" diff --git a/src/main/kotlin/Verifier.kt b/src/main/kotlin/Verifier.kt index 01cbb9d..56a4ed2 100644 --- a/src/main/kotlin/Verifier.kt +++ b/src/main/kotlin/Verifier.kt @@ -16,7 +16,6 @@ package com.android.keyattestation.verifier -import androidx.annotation.RequiresApi import com.android.keyattestation.verifier.SoftwareRoot.isSoftwareRoot import com.android.keyattestation.verifier.provider.KeyAttestationCertPath import com.android.keyattestation.verifier.provider.KeyAttestationProvider @@ -179,7 +178,6 @@ constructor( * @return [VerificationResult] */ @JvmOverloads - @RequiresApi(24) fun verify( chain: List, challengeChecker: ChallengeChecker? = null, @@ -199,7 +197,6 @@ constructor( return result } - @RequiresApi(24) /** * Verifies an Android Key Attestation certificate chain asynchronously. * @@ -233,7 +230,6 @@ constructor( } } - @RequiresApi(24) private suspend fun internalVerify( certPath: KeyAttestationCertPath, challengeChecker: ChallengeChecker? = null, From a69e6a6b6bec6d274e3491546f86ce1995751c93 Mon Sep 17 00:00:00 2001 From: Carmen Hanish Date: Wed, 19 Aug 2026 14:02:42 -0700 Subject: [PATCH 25/48] Add workflow to build Maven repo PiperOrigin-RevId: 967397038 --- .github/workflows/gradle.yml | 20 ++++++++++++--- .github/workflows/release.yml | 47 +++++++++++++++++++++++++++++++++++ 2 files changed, 64 insertions(+), 3 deletions(-) create mode 100644 .github/workflows/release.yml diff --git a/.github/workflows/gradle.yml b/.github/workflows/gradle.yml index f8cc599..c437039 100644 --- a/.github/workflows/gradle.yml +++ b/.github/workflows/gradle.yml @@ -1,3 +1,17 @@ +# Copyright 2026 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + name: Gradle Build and Test on: @@ -10,12 +24,12 @@ jobs: build: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 - - uses: actions/setup-java@v4 + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + - uses: actions/setup-java@7a6d8a8234af8eb26422e24e3006232cccaa061b # v4.6.0 with: distribution: "temurin" java-version: 21 - - uses: gradle/actions/setup-gradle@v4 + - uses: gradle/actions/setup-gradle@ed408507eac070d1f99cc633dbcf757c94c7933a # v4.4.3 - name: Build run: ./gradlew build - name: Test diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..91f7256 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,47 @@ +# Copyright 2026 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +name: Release + +on: + push: + tags: + - 'v*' + +permissions: + contents: write + +jobs: + build-maven-repo: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + - uses: actions/setup-java@7a6d8a8234af8eb26422e24e3006232cccaa061b # v4.6.0 + with: + distribution: "temurin" + java-version: 21 + - uses: gradle/actions/setup-gradle@ed408507eac070d1f99cc633dbcf757c94c7933a # v4.4.3 + - name: "Set environment variables" + run: | + echo "ORG_GRADLE_PROJECT_KeyAttestationMavenRepo=$GITHUB_WORKSPACE/keyattestation_m2repo" >> "$GITHUB_ENV" + - name: Build Maven repo + env: + REF_NAME: ${{ github.ref_name }} + run: ./gradlew -PkeyAttestationReleaseVersion="$REF_NAME" publishAllPublicationsToLocalDirRepository + + - name: Upload + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 + with: + name: keyattestation_m2repo + path: keyattestation_m2repo From 2e3561b8197a0e70659dd6df051d90f3e0c04e88 Mon Sep 17 00:00:00 2001 From: Seth Moore Date: Tue, 25 Aug 2026 09:49:38 -0700 Subject: [PATCH 26/48] Move function-local revocation status types up a level Proguard ruthlessly targets function-local types, which breaks gson. Fix it. PiperOrigin-RevId: 970616422 --- src/main/kotlin/GoogleRevocationList.kt | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/main/kotlin/GoogleRevocationList.kt b/src/main/kotlin/GoogleRevocationList.kt index 9f460ad..aa2b801 100644 --- a/src/main/kotlin/GoogleRevocationList.kt +++ b/src/main/kotlin/GoogleRevocationList.kt @@ -68,6 +68,10 @@ fun getRevocationStatusFromWeb( } } +internal data class StatusEntry(val status: String) + +internal data class StatusFile(val entries: Map) + /** * Parses a revocation status list from an input stream. * @@ -76,9 +80,6 @@ fun getRevocationStatusFromWeb( * @return A set of revoked serial numbers. */ fun parseAttestationStatus(input: InputStream): Set { - data class StatusEntry(val status: String) - data class StatusFile(val entries: Map) - return Gson() .fromJson(InputStreamReader(input), StatusFile::class.java) .entries From 2b0f4f300592a38139d5592ac1c27e2ef70566c2 Mon Sep 17 00:00:00 2001 From: Seth Moore Date: Wed, 26 Aug 2026 08:42:01 -0700 Subject: [PATCH 27/48] Add bounds checks to AttestationApplicationId Block "obviously invalid" input values. PiperOrigin-RevId: 971306073 --- src/main/kotlin/CertPrinter.kt | 2 +- src/main/kotlin/ConstraintConfig.kt | 11 ++- src/main/kotlin/Extension.kt | 113 +++++++++++++++++++----- src/main/kotlin/Verifier.kt | 8 +- src/main/kotlin/X509CertificateExt.kt | 3 +- src/main/kotlin/testing/Certs.kt | 14 +++ src/test/kotlin/ConstraintConfigTest.kt | 26 ++++++ src/test/kotlin/ExtensionTest.kt | 51 ++++++++++- 8 files changed, 200 insertions(+), 28 deletions(-) diff --git a/src/main/kotlin/CertPrinter.kt b/src/main/kotlin/CertPrinter.kt index e9705e2..5edfaa1 100644 --- a/src/main/kotlin/CertPrinter.kt +++ b/src/main/kotlin/CertPrinter.kt @@ -166,7 +166,7 @@ object CertPrinter { appendIndentedLine("Custom Extensions:") indent { KeyDescription.parseFrom(cert)?.let { append(prettyString(it)) } - ProvisioningInfoMap.parseFrom(cert)?.let { append(prettyString(it)) } + ProvisioningInfoMap.parseFrom(cert, InputLimits())?.let { append(prettyString(it)) } } } diff --git a/src/main/kotlin/ConstraintConfig.kt b/src/main/kotlin/ConstraintConfig.kt index e7101aa..5c1cad0 100644 --- a/src/main/kotlin/ConstraintConfig.kt +++ b/src/main/kotlin/ConstraintConfig.kt @@ -44,12 +44,15 @@ sealed interface Constraint { * at https://source.android.com/docs/security/features/keystore/attestation. */ @ThreadSafe -class ConstraintConfig( +class ConstraintConfig +@JvmOverloads +constructor( val allowSoftwareRoot: Boolean = false, val keyOrigin: Constraint? = null, val securityLevel: Constraint? = null, val rootOfTrust: Constraint? = null, val additionalConstraints: ImmutableList = ImmutableList.of(), + val inputLimits: InputLimits = InputLimits(), ) { fun getConstraints() = ImmutableList.builder() @@ -80,6 +83,7 @@ class ConstraintConfigBuilder() { var securityLevel: Constraint? = null var rootOfTrust: Constraint? = null var additionalConstraints: MutableList = mutableListOf() + var inputLimits: InputLimits = InputLimits() fun securityLevel(constraint: () -> Constraint) { this.securityLevel = constraint() @@ -97,6 +101,10 @@ class ConstraintConfigBuilder() { additionalConstraints.add(constraint()) } + fun inputLimits(init: () -> InputLimits) { + this.inputLimits = init() + } + fun build(): ConstraintConfig = ConstraintConfig( allowSoftwareRoot, @@ -104,6 +112,7 @@ class ConstraintConfigBuilder() { securityLevel, rootOfTrust, ImmutableList.copyOf(additionalConstraints), + inputLimits, ) } diff --git a/src/main/kotlin/Extension.kt b/src/main/kotlin/Extension.kt index c3842fa..01ff0cc 100644 --- a/src/main/kotlin/Extension.kt +++ b/src/main/kotlin/Extension.kt @@ -58,6 +58,34 @@ import org.bouncycastle.asn1.DERSet import org.bouncycastle.asn1.DERTaggedObject import org.bouncycastle.asn1.x509.Extension +/** + * Limits on input sizes when parsing extensions from attestation certificates. + * + * @param maxPackages Maximum number of package infos allowed in an [AttestationApplicationId]. + * @param maxSignatures Maximum number of signatures allowed in an [AttestationApplicationId]. + * Default matches the value in ApkSigner in AOSP (APK v3 only allows 1 signature, so 10 is for + * legacy APKs). + * @param maxCborPreallocationSize Maximum of any pre-allocated CBOR parser buffers. These buffers + * are an optimization only. If the input data is larger than this value, the parser will allocate + * larger buffers as needed while parsing the input. + */ +@Immutable +data class InputLimits +@JvmOverloads +constructor( + val maxPackages: Int = 8, + val maxSignatures: Int = 10, + val maxCborPreallocationSize: Int = 4096, +) { + init { + require(maxPackages > 0) { "maxPackages must be > 0, was $maxPackages" } + require(maxSignatures > 0) { "maxSignatures must be > 0, was $maxSignatures" } + require(maxCborPreallocationSize > 0) { + "maxCborPreallocationSize must be > 0, was $maxCborPreallocationSize" + } + } +} + @RequiresApi(24) data class ExtensionParsingException @JvmOverloads @@ -86,13 +114,16 @@ data class ProvisioningInfoMap( @JvmField val OID = ASN1ObjectIdentifier("1.3.6.1.4.1.11129.2.1.30") @JvmStatic - fun parseFrom(cert: X509Certificate) = cert.getExtensionValue(OID.id)?.let { parseFrom(it) } + @JvmOverloads + fun parseFrom(cert: X509Certificate, inputLimits: InputLimits = InputLimits()) = + cert.getExtensionValue(OID.id)?.let { parseFrom(it, inputLimits) } @JvmStatic - fun parseFrom(bytes: ByteArray?) = + @JvmOverloads + fun parseFrom(bytes: ByteArray?, inputLimits: InputLimits = InputLimits()) = try { val cborBytes = ASN1OctetString.getInstance(bytes).octets - from(cborDecode(cborBytes).asMap()) + from(cborDecode(cborBytes, inputLimits).asMap()) } catch (e: CborException) { throw IllegalArgumentException(e) } @@ -175,25 +206,37 @@ data class KeyDescription( @JvmStatic @JvmOverloads @Throws(ExtensionParsingException::class) - fun parseFrom(cert: X509Certificate, logFn: (String) -> Unit = {}) = + fun parseFrom( + cert: X509Certificate, + logFn: (String) -> Unit = {}, + inputLimits: InputLimits = InputLimits(), + ) = cert .getExtensionValue(OID.id) ?.let { ASN1OctetString.getInstance(it).octets } - ?.let { parseFrom(it, logFn) } + ?.let { parseFrom(it, logFn, inputLimits) } @JvmStatic @JvmOverloads @Throws(ExtensionParsingException::class) - fun parseFrom(bytes: ByteArray, logFn: (String) -> Unit = {}) = + fun parseFrom( + bytes: ByteArray, + logFn: (String) -> Unit = {}, + inputLimits: InputLimits = InputLimits(), + ) = try { - from(ASN1Sequence.getInstance(bytes), logFn) + from(ASN1Sequence.getInstance(bytes), logFn, inputLimits) } catch (e: NullPointerException) { // Workaround for a NPE in BouncyCastle. // https://github.com/bcgit/bc-java/blob/228211ecb973fe87fdd0fc4ab16ba0446ec1a29c/core/src/main/java/org/bouncycastle/asn1/ASN1UniversalType.java#L24 throw IllegalArgumentException(e) } - private fun from(seq: ASN1Sequence, logFn: (String) -> Unit = {}): KeyDescription { + private fun from( + seq: ASN1Sequence, + logFn: (String) -> Unit = {}, + inputLimits: InputLimits = InputLimits(), + ): KeyDescription { require(seq.size() == 8) return KeyDescription( attestationVersion = seq.getObjectAt(0).toInt(), @@ -202,8 +245,8 @@ data class KeyDescription( keyMintSecurityLevel = seq.getObjectAt(3).toSecurityLevel(), attestationChallenge = seq.getObjectAt(4).toByteString(), uniqueId = seq.getObjectAt(5).toByteString(), - softwareEnforced = seq.getObjectAt(6).toAuthorizationList(logFn), - hardwareEnforced = seq.getObjectAt(7).toAuthorizationList(logFn), + softwareEnforced = seq.getObjectAt(6).toAuthorizationList(logFn, inputLimits), + hardwareEnforced = seq.getObjectAt(7).toAuthorizationList(logFn, inputLimits), ) } } @@ -467,7 +510,11 @@ data class AuthorizationList( parse(tag) { it.toPatchLevel(partition, logFn) } } - fun from(seq: ASN1Sequence, logFn: (String) -> Unit = { _ -> }): AuthorizationList { + fun from( + seq: ASN1Sequence, + logFn: (String) -> Unit = { _ -> }, + inputLimits: InputLimits = InputLimits(), + ): AuthorizationList { val objects = seq.associate { require(it is ASN1TaggedObject) { "Must be an ASN1TaggedObject, was ${it::class.simpleName}" @@ -524,7 +571,7 @@ data class AuthorizationList( osPatchLevel = converter.parsePatchLevel(KeyMintTag.OS_PATCH_LEVEL, "OS"), attestationApplicationId = converter.parse(KeyMintTag.ATTESTATION_APPLICATION_ID) { - it.toAttestationApplicationId() + it.toAttestationApplicationId(inputLimits) }, attestationIdBrand = converter.parseStr(KeyMintTag.ATTESTATION_ID_BRAND), attestationIdDevice = converter.parseStr(KeyMintTag.ATTESTATION_ID_DEVICE), @@ -609,11 +656,26 @@ data class AttestationApplicationId( } .let { DERSequence(it.toTypedArray()) } - internal companion object { - fun from(seq: ASN1Sequence): AttestationApplicationId { + companion object { + internal fun from( + seq: ASN1Sequence, + inputLimits: InputLimits = InputLimits(), + ): AttestationApplicationId { require(seq.size() == 2) - val attestationPackageInfos = (seq.getObjectAt(0).toSet()) - val signatureDigests = seq.getObjectAt(1).toSet() + val packagesSet = seq.getObjectAt(0) + if (packagesSet is ASN1Set && packagesSet.size() > inputLimits.maxPackages) { + throw ExtensionParsingException( + "AttestationApplicationId contains too many packages (${packagesSet.size()} > ${inputLimits.maxPackages})" + ) + } + val signaturesSet = seq.getObjectAt(1) + if (signaturesSet is ASN1Set && signaturesSet.size() > inputLimits.maxSignatures) { + throw ExtensionParsingException( + "AttestationApplicationId contains too many signatures (${signaturesSet.size()} > ${inputLimits.maxSignatures})" + ) + } + val attestationPackageInfos = (packagesSet.toSet()) + val signatureDigests = (signaturesSet.toSet()) return AttestationApplicationId( attestationPackageInfos.map { AttestationPackageInfo.from(it) }.toSet(), signatureDigests.map { it.toByteString() }.toSet(), @@ -710,21 +772,26 @@ enum class VerifiedBootState(val value: Int) { } @RequiresApi(24) -private fun ASN1Encodable.toAttestationApplicationId(): AttestationApplicationId { +private fun ASN1Encodable.toAttestationApplicationId( + inputLimits: InputLimits = InputLimits() +): AttestationApplicationId { if (this !is ASN1OctetString) { throw ExtensionParsingException( "Object must be an ASN1OctetString, was ${this::class.simpleName}" ) } - return AttestationApplicationId.from(ASN1Sequence.getInstance(this.octets)) + return AttestationApplicationId.from(ASN1Sequence.getInstance(this.octets), inputLimits) } @RequiresApi(24) -private fun ASN1Encodable.toAuthorizationList(logFn: (String) -> Unit): AuthorizationList { +private fun ASN1Encodable.toAuthorizationList( + logFn: (String) -> Unit, + inputLimits: InputLimits = InputLimits(), +): AuthorizationList { if (this !is ASN1Sequence) { throw ExtensionParsingException("Object must be an ASN1Sequence, was ${this::class.simpleName}") } - return AuthorizationList.from(this, logFn) + return AuthorizationList.from(this, logFn, inputLimits) } @RequiresApi(24) @@ -826,9 +893,11 @@ private fun Set.toAsn1() = DERSet(this.map { it.toAsn1() }.toTypedAr private fun String.toAsn1() = DEROctetString(this.toByteArray(UTF_8)) -fun cborDecode(data: ByteArray): DataItem { +fun cborDecode(data: ByteArray, inputLimits: InputLimits): DataItem { val bais = ByteArrayInputStream(data) - val dataItems = CborDecoder(bais).decode() + val decoder = CborDecoder(bais) + decoder.setMaxPreallocationSize(inputLimits.maxCborPreallocationSize) + val dataItems = decoder.decode() if (dataItems.size != 1) { throw CborException( "Byte stream cannot be decoded properly. Expected 1 item, found ${dataItems.size}" diff --git a/src/main/kotlin/Verifier.kt b/src/main/kotlin/Verifier.kt index 56a4ed2..4300f40 100644 --- a/src/main/kotlin/Verifier.kt +++ b/src/main/kotlin/Verifier.kt @@ -247,7 +247,7 @@ constructor( val deviceInformation = if (certPath.provisioningMethod() == ProvisioningMethod.REMOTELY_PROVISIONED) { try { - certPath.attestationCert().provisioningInfo() + certPath.attestationCert().provisioningInfo(constraintConfig.inputLimits) } catch (e: Exception) { log?.logInfoMessage("Failed to parse provisioning info map: ${e.message}") null @@ -279,7 +279,11 @@ constructor( val keyDescription = try { checkNotNull( - KeyDescription.parseFrom(certPath.leafCert(), { msg -> log?.logInfoMessage(msg) }) + KeyDescription.parseFrom( + certPath.leafCert(), + { msg -> log?.logInfoMessage(msg) }, + inputLimits = constraintConfig.inputLimits, + ) ) { // Should never happen since the extension's presence is checked by by validate(). "Key attestation extension not found" diff --git a/src/main/kotlin/X509CertificateExt.kt b/src/main/kotlin/X509CertificateExt.kt index 67d0feb..ac3cf17 100644 --- a/src/main/kotlin/X509CertificateExt.kt +++ b/src/main/kotlin/X509CertificateExt.kt @@ -45,4 +45,5 @@ fun InputStream.asX509Certificate() = * @return the DER-encoded OCTET string containing the ProvisioningInfo sequence or null if the * extension is not present in the certificate. */ -fun X509Certificate.provisioningInfo() = ProvisioningInfoMap.parseFrom(this) +fun X509Certificate.provisioningInfo(inputLimits: InputLimits = InputLimits()) = + ProvisioningInfoMap.parseFrom(this, inputLimits) diff --git a/src/main/kotlin/testing/Certs.kt b/src/main/kotlin/testing/Certs.kt index 2f6d917..0884798 100644 --- a/src/main/kotlin/testing/Certs.kt +++ b/src/main/kotlin/testing/Certs.kt @@ -16,6 +16,7 @@ package com.android.keyattestation.verifier.testing +import com.android.keyattestation.verifier.AttestationPackageInfo import com.android.keyattestation.verifier.AuthorizationList import com.android.keyattestation.verifier.KeyDescription import com.android.keyattestation.verifier.KeyMintTag @@ -25,6 +26,7 @@ import com.android.keyattestation.verifier.RootOfTrust import com.android.keyattestation.verifier.SecurityLevel import com.android.keyattestation.verifier.VerifiedBootState import com.android.keyattestation.verifier.provider.KeyAttestationCertPath +import com.google.protobuf.ByteString import com.google.protobuf.kotlin.toByteStringUtf8 import java.math.BigInteger import java.security.cert.TrustAnchor @@ -33,8 +35,10 @@ import org.bouncycastle.asn1.ASN1EncodableVector import org.bouncycastle.asn1.ASN1Enumerated import org.bouncycastle.asn1.ASN1Integer import org.bouncycastle.asn1.ASN1ObjectIdentifier +import org.bouncycastle.asn1.ASN1Sequence import org.bouncycastle.asn1.DEROctetString import org.bouncycastle.asn1.DERSequence +import org.bouncycastle.asn1.DERSet import org.bouncycastle.asn1.DERTaggedObject import org.bouncycastle.asn1.x500.X500Name import org.bouncycastle.asn1.x509.Extension @@ -615,4 +619,14 @@ object V3Extensions { ) ) .encoded + + fun buildAttestationApplicationIdExtension( + packages: Set, + signatures: Set, + ): ASN1Sequence = + buildList { + add(DERSet(packages.map { it.toAsn1() }.toTypedArray())) + add(DERSet(signatures.map { DEROctetString(it.toByteArray()) }.toTypedArray())) + } + .let { DERSequence(it.toTypedArray()) } } diff --git a/src/test/kotlin/ConstraintConfigTest.kt b/src/test/kotlin/ConstraintConfigTest.kt index 638aa50..4e40533 100644 --- a/src/test/kotlin/ConstraintConfigTest.kt +++ b/src/test/kotlin/ConstraintConfigTest.kt @@ -20,6 +20,7 @@ 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.Assert.assertThrows import org.junit.Test import org.junit.runner.RunWith import org.junit.runners.JUnit4 @@ -181,4 +182,29 @@ class ConstraintConfigTest { assertThat(violation.failureMessage) .isEqualTo("Authorization list tags must be in ascending order") } + + @Test + fun constraintConfig_inputLimits_customValue() { + val config = constraintConfig { + inputLimits { InputLimits(maxPackages = 50, maxSignatures = 25) } + } + assertThat(config.inputLimits.maxPackages).isEqualTo(50) + assertThat(config.inputLimits.maxSignatures).isEqualTo(25) + } + + @Test + fun inputLimits_zeroOrNegative_throws() { + assertThrows(IllegalArgumentException::class.java) { + InputLimits(maxPackages = 0, maxSignatures = 10) + } + assertThrows(IllegalArgumentException::class.java) { + InputLimits(maxPackages = -1, maxSignatures = 10) + } + assertThrows(IllegalArgumentException::class.java) { + InputLimits(maxPackages = 10, maxSignatures = 0) + } + assertThrows(IllegalArgumentException::class.java) { + InputLimits(maxPackages = 10, maxSignatures = -5) + } + } } diff --git a/src/test/kotlin/ExtensionTest.kt b/src/test/kotlin/ExtensionTest.kt index 6f560e5..082d948 100644 --- a/src/test/kotlin/ExtensionTest.kt +++ b/src/test/kotlin/ExtensionTest.kt @@ -38,6 +38,7 @@ import kotlin.io.path.listDirectoryEntries import kotlin.io.path.nameWithoutExtension import kotlin.io.path.readText import kotlin.io.path.reader +import org.junit.Assert.assertThrows import org.junit.Test import org.junit.runner.RunWith @@ -53,7 +54,10 @@ class ExtensionTest { certificatesIssued = 1, ) val provisioningInfo = - ProvisioningInfoMap.parseFrom(Chains.validRemotelyProvisioned.attestationCert()) + ProvisioningInfoMap.parseFrom( + Chains.validRemotelyProvisioned.attestationCert(), + InputLimits(), + ) assertThat(provisioningInfo).isEqualTo(expectedProvisioningInfo) } @@ -206,4 +210,49 @@ class ExtensionTest { assertThat(logHook.fakeVerifyRequestLog.infoMessages) .contains("Exception when parsing key_size: Must be an ASN1Integer, was DEROctetString") } + + @Test + fun attestationApplicationId_maxPackages_success() { + val limits = InputLimits() + val packages = + (1..limits.maxPackages) + .map { AttestationPackageInfo("com.example.pkg$it", it.toBigInteger()) } + .toSet() + val id = AttestationApplicationId(packages = packages, signatures = setOf(ByteString.empty())) + assertThat(id.packages).hasSize(limits.maxPackages) + } + + @Test + fun attestationApplicationId_fromAsn1_exceedsMaxPackages_throws() { + val limits = InputLimits(maxPackages = 1) + val packages = + (1..2).map { AttestationPackageInfo("com.example.pkg$it", it.toBigInteger()) }.toSet() + val seq = + V3Extensions.buildAttestationApplicationIdExtension(packages, setOf(ByteString.empty())) + assertThrows(ExtensionParsingException::class.java) { + AttestationApplicationId.from(seq, inputLimits = limits) + } + } + + @Test + fun attestationApplicationId_fromAsn1_maxSignatures_success() { + val limits = InputLimits() + val packages = setOf(AttestationPackageInfo("com.example.pkg", 1.toBigInteger())) + val signatures = + (1..limits.maxSignatures).map { ByteString.copyFrom(byteArrayOf(it.toByte())) }.toSet() + val seq = V3Extensions.buildAttestationApplicationIdExtension(packages, signatures) + val id = AttestationApplicationId.from(seq, inputLimits = limits) + assertThat(id.signatures).hasSize(limits.maxSignatures) + } + + @Test + fun attestationApplicationId_fromAsn1_exceedsMaxSignatures_throws() { + val limits = InputLimits(maxSignatures = 1) + val packages = setOf(AttestationPackageInfo("com.example.pkg", 1.toBigInteger())) + val signatures = (1..2).map { ByteString.copyFrom(byteArrayOf(it.toByte())) }.toSet() + val seq = V3Extensions.buildAttestationApplicationIdExtension(packages, signatures) + assertThrows(ExtensionParsingException::class.java) { + AttestationApplicationId.from(seq, inputLimits = limits) + } + } } From 2dd870822dea184bdce73a2b63fcde7dd3314ca1 Mon Sep 17 00:00:00 2001 From: Seth Moore Date: Wed, 26 Aug 2026 18:18:55 -0700 Subject: [PATCH 28/48] Bump default maxPackages to 32 It turns out there are commonly package lists longer than I expected! Bump the default limit to 32 to hopefully address the most common upper bound. I don't have data on this, so I'm admittedly guessing a bit here! PiperOrigin-RevId: 971621632 --- src/main/kotlin/Extension.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/kotlin/Extension.kt b/src/main/kotlin/Extension.kt index 01ff0cc..bb1e4a5 100644 --- a/src/main/kotlin/Extension.kt +++ b/src/main/kotlin/Extension.kt @@ -73,7 +73,7 @@ import org.bouncycastle.asn1.x509.Extension data class InputLimits @JvmOverloads constructor( - val maxPackages: Int = 8, + val maxPackages: Int = 32, val maxSignatures: Int = 10, val maxCborPreallocationSize: Int = 4096, ) { From a7d217a943c0b2a34b3d84980c7ca08cf48d2e2b Mon Sep 17 00:00:00 2001 From: Suzanna Jiwani Date: Thu, 27 Aug 2026 12:50:46 -0700 Subject: [PATCH 29/48] Including a test cert from before Keymaster 4.0 Since these cert chains do not have verified boot hash in the root of trust PiperOrigin-RevId: 972105230 --- testdata/walleye/sdk27/TEE_EC_NONE.json | 33 ++++++++++ testdata/walleye/sdk27/TEE_EC_NONE.pem | 83 +++++++++++++++++++++++++ 2 files changed, 116 insertions(+) create mode 100644 testdata/walleye/sdk27/TEE_EC_NONE.json create mode 100644 testdata/walleye/sdk27/TEE_EC_NONE.pem diff --git a/testdata/walleye/sdk27/TEE_EC_NONE.json b/testdata/walleye/sdk27/TEE_EC_NONE.json new file mode 100644 index 0000000..9539a14 --- /dev/null +++ b/testdata/walleye/sdk27/TEE_EC_NONE.json @@ -0,0 +1,33 @@ +{ + "attestationVersion": "2", + "attestationSecurityLevel": "TRUSTED_ENVIRONMENT", + "keyMintVersion": "3", + "keyMintSecurityLevel": "TRUSTED_ENVIRONMENT", + "attestationChallenge": "OTEyODk1ZTAtM2UxOS00MzI3LWEwZmItOTFlMmYzZjQ2Y2Ri", + "uniqueId": "", + "softwareEnforced": { + "creationDateTime": "1787857347615", + "attestationApplicationId": { + "packages": [ + { "name": "com.google.android.attestation", "version": "0" } + ], + "signatures": ["EDk47kU35Z6O55L2VFBPuDRvxrNG0LvEQV/DOfz8jsE="] + } + }, + "hardwareEnforced": { + "purposes": ["2", "3"], + "algorithms": "3", + "keySize": "256", + "digests": ["5"], + "ecCurve": "1", + "noAuthRequired": true, + "origin": "GENERATED", + "rootOfTrust": { + "verifiedBootKey": "nBLP3ATHRYTXh6w9I3chMsGFJLx6so3sQhm4/FtCX3A=", + "deviceLocked": false, + "verifiedBootState": "UNVERIFIED" + }, + "osVersion": "80100", + "osPatchLevel": "201807" + } +} diff --git a/testdata/walleye/sdk27/TEE_EC_NONE.pem b/testdata/walleye/sdk27/TEE_EC_NONE.pem new file mode 100644 index 0000000..74915dc --- /dev/null +++ b/testdata/walleye/sdk27/TEE_EC_NONE.pem @@ -0,0 +1,83 @@ +-----BEGIN CERTIFICATE----- +MIICXTCCAgSgAwIBAgIBATAKBggqhkjOPQQDAjAbMRkwFwYDVQQFExBjNjA0NzU3 +MWQ4ZjBkMTdjMCAXDTcwMDEwMTAwMDAwMFoYDzIxMDYwMjA3MDYyODE1WjAfMR0w +GwYDVQQDDBRBbmRyb2lkIEtleXN0b3JlIEtleTBZMBMGByqGSM49AgEGCCqGSM49 +AwEHA0IABHMeM3rJ+Hfz577oiXOki3LSRntFyWNOo3AHPITTq69cs8mmPYqvOuad +y5n3w49gVbd+UkO55byIN37CPA9bS/OjggExMIIBLTAOBgNVHQ8BAf8EBAMCB4Aw +ggEZBgorBgEEAdZ5AgERBIIBCTCCAQUCAQIKAQECAQMKAQEEJDkxMjg5NWUwLTNl +MTktNDMyNy1hMGZiLTkxZTJmM2Y0NmNkYgQAMF+/hT0IAgYBoESaJB+/hUVPBE0w +SzElMCMEHmNvbS5nb29nbGUuYW5kcm9pZC5hdHRlc3RhdGlvbgIBADEiBCAQOTju +RTflno7nkvZUUE+4NG/Gs0bQu8RBX8M5/PyOwTBuoQgxBgIBAgIBA6IDAgEDowQC +AgEApQUxAwIBBaoDAgEBv4N3AgUAv4U+AwIBAL+FQCowKAQgnBLP3ATHRYTXh6w9 +I3chMsGFJLx6so3sQhm4/FtCX3ABAQAKAQK/hUEFAgMBOOS/hUIFAgMDFE8wCgYI +KoZIzj0EAwIDRwAwRAIgBRIbMIefN81VKWeWqqxYzDBUSlND34BPrbXkGYvyfS0C +IADlNmUTJuBlZrzgt9oavgaXfTpt1w7rxYY8TCvRDz2j +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIICKzCCAbKgAwIBAgIKEXhzFQJ5hgIAEDAKBggqhkjOPQQDAjAbMRkwFwYDVQQF +ExA4N2Y0NTE0NDc1YmEwYTJiMB4XDTE2MDUyNjE3MTUwMloXDTI2MDUyNDE3MTUw +MlowGzEZMBcGA1UEBRMQYzYwNDc1NzFkOGYwZDE3YzBZMBMGByqGSM49AgEGCCqG +SM49AwEHA0IABOoJkrjZcbPu6IcDxyrvlugASVQm5MX7OGGT0T34rzlwwbR9UV2A +Tu6aMiEa8uuQdP3iy5qSUYeCzUuneIdo7dujgd0wgdowHQYDVR0OBBYEFHlfwP7+ +91r1xLPq/o7/eYXAU9ocMB8GA1UdIwQYMBaAFDBEI+Wi9gbhUKt3XxYWu5HMY8ZZ +MAwGA1UdEwEB/wQCMAAwDgYDVR0PAQH/BAQDAgeAMCQGA1UdHgQdMBugGTAXghVp +bnZhbGlkO2VtYWlsOmludmFsaWQwVAYDVR0fBE0wSzBJoEegRYZDaHR0cHM6Ly9h +bmRyb2lkLmdvb2dsZWFwaXMuY29tL2F0dGVzdGF0aW9uL2NybC8xMTc4NzMxNTAy +Nzk4NjAyMDAxMDAKBggqhkjOPQQDAgNnADBkAjAMOvX7podpWf2gJjzut3Woz/bq +1B42pC7Bu511pv1zj4jbtsdhhYCo/u/pnylG3LMCMCgdkdZQBPOEaJuBTYmxGiWr +qVFe6vTsX60SJ4vqa1PruSZzEFcyukXMckPn1wcz8A== +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIDwzCCAaugAwIBAgIKA4gmZ2BliZaFdTANBgkqhkiG9w0BAQsFADAbMRkwFwYD +VQQFExBmOTIwMDllODUzYjZiMDQ1MB4XDTE2MDUyNjE3MDE1MVoXDTI2MDUyNDE3 +MDE1MVowGzEZMBcGA1UEBRMQODdmNDUxNDQ3NWJhMGEyYjB2MBAGByqGSM49AgEG +BSuBBAAiA2IABGQ7VmgdJ/rEgs9sIE3rzvApXDUMAaqMMn8+1fRJrvQpZkJfOT2E +djtdrVaxDQRZxixqT5MlVqiSk8PRTqLx3+8OPLoicqMiOeGytH2sVQurvFynVeKq +SGKK1jx2/2fccqOBtjCBszAdBgNVHQ4EFgQUMEQj5aL2BuFQq3dfFha7kcxjxlkw +HwYDVR0jBBgwFoAUNmHhAHyIBQlRi0RsR/8aTMnqTxIwDwYDVR0TAQH/BAUwAwEB +/zAOBgNVHQ8BAf8EBAMCAYYwUAYDVR0fBEkwRzBFoEOgQYY/aHR0cHM6Ly9hbmRy +b2lkLmdvb2dsZWFwaXMuY29tL2F0dGVzdGF0aW9uL2NybC9FOEZBMTk2MzE0RDJG +QTE4MA0GCSqGSIb3DQEBCwUAA4ICAQBAOYqLNryTmbOlnrjnIvDoXxzaLOgCXu29 +l7KpbFHacVLxgYuGRiIEQqzZBqUYSt9Pgx+P2KvoHtz99sEZr2xTe0Dw6CTHTAmx +WXUFdrlvEMm2GySfvJRfMNCuX1oIS/M5PfREY2YZHyLq/sn1sJr3FjbKMdUMBo5A +camcD3H8wl9O/6qfhX+57iXzoK6yMzJRG/Mlkm58/sFk0pjayUBchmUJL0FQ6IhK +Ygy8RKE2UDyXKOE7+ZMSMUUkAdzyn2PFv7TvQtDk0ge2mkVrNrfPSglMzBNvrSDH +PBmTktXzwseVagIRT5WI91OrUOYPFgostsfH42hs5wJtAFGPwDg/1mNa8UyH9k1b +MrRq3Srez1XG0Ju7SGN/uNX5dkcwvfAmadtmM7Pp+l2VHRYRR600jAcM2+7bl8eg +qfM/A7vyDLZqPIxDwkLXj2eN99nJZJVaGfB9dHyFOqBqBM6SdyV6MSIr3AHoo6u+ +BWIX9+q8n1qg5I6JWeEe+K58SbRDVoNQgsKP9/iPruXMU5rm2ywPxICVGysl1GgA +P+FJ3X6oP0tXFWQlYoWdSloSVHNZQqj2ev/69sMnGsTeJw1V7I0gR+eZNEfxe+vZ +D4KP88KxuiPCe94rp+Aqs5/YwuCo6rQ+HGi5OZNBsQXYIufClSBje+OpjQb7HJgi +hJdzo2/IBw== +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIFYDCCA0igAwIBAgIJAOj6GWMU0voYMA0GCSqGSIb3DQEBCwUAMBsxGTAXBgNV +BAUTEGY5MjAwOWU4NTNiNmIwNDUwHhcNMTYwNTI2MTYyODUyWhcNMjYwNTI0MTYy +ODUyWjAbMRkwFwYDVQQFExBmOTIwMDllODUzYjZiMDQ1MIICIjANBgkqhkiG9w0B +AQEFAAOCAg8AMIICCgKCAgEAr7bHgiuxpwHsK7Qui8xUFmOr75gvMsd/dTEDDJdS +Sxtf6An7xyqpRR90PL2abxM1dEqlXnf2tqw1Ne4Xwl5jlRfdnJLmN0pTy/4lj4/7 +tv0Sk3iiKkypnEUtR6WfMgH0QZfKHM1+di+y9TFRtv6y//0rb+T+W8a9nsNL/ggj +nar86461qO0rOs2cXjp3kOG1FEJ5MVmFmBGtnrKpa73XpXyTqRxB/M0n1n/W9nGq +C4FSYa04T6N5RIZGBN2z2MT5IKGbFlbC8UrW0DxW7AYImQQcHtGl/m00QLVWutHQ +oVJYnFPlXTcHYvASLu+RhhsbDmxMgJJ0mcDpvsC4PjvB+TxywElgS70vE0XmLD+O +JtvsBslHZvPBKCOdT0MS+tgSOIfga+z1Z1g7+DVagf7quvmag8jfPioyKvxnK/Eg +sTUVi2ghzq8wm27ud/mIM7AY2qEORR8Go3TVB4HzWQgpZrt3i5MIlCaY504LzSRi +igHCzAPlHws+W0rB5N+er5/2pJKnfBSDiCiFAVtCLOZ7gLiMm0jhO2B6tUXHI/+M +RPjy02i59lINMRRev56GKtcd9qO/0kUJWdZTdA2XoS82ixPvZtXQpUpuL12ab+9E +aDK8Z4RHJYYfCT3Q5vNAXaiWQ+8PTWm2QgBR/bkwSWc+NpUFgNPN9PvQi8WEg5Um +AGMCAwEAAaOBpjCBozAdBgNVHQ4EFgQUNmHhAHyIBQlRi0RsR/8aTMnqTxIwHwYD +VR0jBBgwFoAUNmHhAHyIBQlRi0RsR/8aTMnqTxIwDwYDVR0TAQH/BAUwAwEB/zAO +BgNVHQ8BAf8EBAMCAYYwQAYDVR0fBDkwNzA1oDOgMYYvaHR0cHM6Ly9hbmRyb2lk +Lmdvb2dsZWFwaXMuY29tL2F0dGVzdGF0aW9uL2NybC8wDQYJKoZIhvcNAQELBQAD +ggIBACDIw41L3KlXG0aMiS//cqrG+EShHUGo8HNsw30W1kJtjn6UBwRM6jnmiwfB +Pb8VA91chb2vssAtX2zbTvqBJ9+LBPGCdw/E53Rbf86qhxKaiAHOjpvAy5Y3m00m +qC0w/Zwvju1twb4vhLaJ5NkUJYsUS7rmJKHHBnETLi8GFqiEsqTWpG/6ibYCv7rY +DBJDcR9W62BW9jfIoBQcxUCUJouMPH25lLNcDc1ssqvC2v7iUgI9LeoM1sNovqPm +QUiG9rHli1vXxzCyaMTjwftkJLkf6724DFhuKug2jITV0QkXvaJWF4nUaHOTNA4u +JU9WDvZLI1j83A+/xnAJUucIv/zGJ1AMH2boHqF8CY16LpsYgBt6tKxxWH00XcyD +CdW2KlBCeqbQPcsFmWyWugxdcekhYsAWyoSf818NUsZdBWBaR/OukXrNLfkQ79Iy +ZohZbvabO/X+MVT3rriAoKc8oE2Uws6DF+60PV7/WIPjNvXySdqspImSN78mflxD +qwLqRBYkA3I75qppLGG9rp7UCdRjxMl8ZDBld+7yvHVgt1cVzJx9xnyGCC23Uaic +MDSXYrB4I4WHXPGjxhZuCuPBLTdOLU8YRvMYdEvYebWHMpvwGCF6bAx3JBpIeOQ1 +wDB5y0USicV3YgYGmi+NZfhA4URSh77Yd6uuJOJENRaNVTzk +-----END CERTIFICATE----- \ No newline at end of file From fd8c9374287b5b96a7d9b79b55310780872b5b61 Mon Sep 17 00:00:00 2001 From: Seth Moore Date: Fri, 4 Sep 2026 09:39:34 -0700 Subject: [PATCH 30/48] Add ProvisioningMethodConstraint This new constraint type allows constraining chains to being factory or remotely provisioned. PiperOrigin-RevId: 976360263 --- src/main/kotlin/ConstraintConfig.kt | 29 ++++++++++ src/test/kotlin/ConstraintConfigTest.kt | 76 +++++++++++++++++++++++++ src/test/kotlin/VerifierTest.kt | 58 +++++++++++++++++++ 3 files changed, 163 insertions(+) diff --git a/src/main/kotlin/ConstraintConfig.kt b/src/main/kotlin/ConstraintConfig.kt index 5c1cad0..aab0e4d 100644 --- a/src/main/kotlin/ConstraintConfig.kt +++ b/src/main/kotlin/ConstraintConfig.kt @@ -17,6 +17,7 @@ package com.android.keyattestation.verifier import com.android.keyattestation.verifier.provider.KeyAttestationCertPath +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 @@ -285,3 +286,31 @@ sealed class TagOrderConstraint : Constraint { } } } + +/** + * Configuration for validating the provisioning method in an Android attestation certificate chain. + */ +@Immutable +sealed class ProvisioningMethodConstraint(val provisioningMethod: ProvisioningMethod) : Constraint { + override val label = "Provisioning method" + + override fun check(description: KeyDescription, certPath: KeyAttestationCertPath) = + if (certPath.provisioningMethod() == provisioningMethod) { + Constraint.Satisfied + } else { + Constraint.Violated(getFailureMessage(certPath)) + } + + open fun getFailureMessage(certPath: KeyAttestationCertPath): String = + "Provisioning method violates constraint: " + + "provisioningMethod=${certPath.provisioningMethod()}, " + + "config=$this" + + /** Checks that the certificate chain is factory provisioned. */ + @Immutable + data object FACTORY : ProvisioningMethodConstraint(ProvisioningMethod.FACTORY_PROVISIONED) + + /** Checks that the certificate chain is remotely provisioned (RKP). */ + @Immutable + data object REMOTE : ProvisioningMethodConstraint(ProvisioningMethod.REMOTELY_PROVISIONED) +} diff --git a/src/test/kotlin/ConstraintConfigTest.kt b/src/test/kotlin/ConstraintConfigTest.kt index 4e40533..798bcea 100644 --- a/src/test/kotlin/ConstraintConfigTest.kt +++ b/src/test/kotlin/ConstraintConfigTest.kt @@ -58,6 +58,9 @@ class ConstraintConfigTest { createTestKeyDescription(SecurityLevel.STRONG_BOX, SecurityLevel.TRUSTED_ENVIRONMENT) private val testCertPath = readCertPath("akita/sdk34/TEE_EC_NONE.pem") + private val factoryCertPath = readCertPath("blueline/sdk28/TEE_EC_NONE.pem") + private val rkpCertPath = readCertPath("caiman/sdk36/TEE_EC_RKP.pem") + private val unknownCertPath = readCertPath("marlin/sdk29/TEE_EC_NONE.pem") @Test fun AttributeConstraintIsSatisfied_strictWithExpectedValue() { @@ -207,4 +210,77 @@ class ConstraintConfigTest { InputLimits(maxPackages = 10, maxSignatures = -5) } } + + @Test + fun ProvisioningMethodConstraintIsSatisfied_factory_allowsFactoryCertPath() { + assertIs( + ProvisioningMethodConstraint.FACTORY.check( + keyDescriptionWithTeeSecurityLevels, + factoryCertPath, + ) + ) + assertIs( + ProvisioningMethodConstraint.FACTORY.check(keyDescriptionWithTeeSecurityLevels, rkpCertPath) + ) + assertIs( + ProvisioningMethodConstraint.FACTORY.check( + keyDescriptionWithTeeSecurityLevels, + unknownCertPath, + ) + ) + } + + @Test + fun ProvisioningMethodConstraintIsSatisfied_rkp_allowsRkpCertPath() { + assertIs( + ProvisioningMethodConstraint.REMOTE.check(keyDescriptionWithTeeSecurityLevels, rkpCertPath) + ) + assertIs( + ProvisioningMethodConstraint.REMOTE.check( + keyDescriptionWithTeeSecurityLevels, + factoryCertPath, + ) + ) + assertIs( + ProvisioningMethodConstraint.REMOTE.check( + keyDescriptionWithTeeSecurityLevels, + unknownCertPath, + ) + ) + } + + @Test + fun provisioningMethodConstraint_factory_withViolation_returnsCorrectMessage() { + val factoryConstraint = ProvisioningMethodConstraint.FACTORY + val rkpViolation = + assertIs( + factoryConstraint.check(keyDescriptionWithTeeSecurityLevels, rkpCertPath) + ) + assertThat(rkpViolation.failureMessage) + .isEqualTo( + "Provisioning method violates constraint: provisioningMethod=REMOTELY_PROVISIONED, config=$factoryConstraint" + ) + assertThat(factoryConstraint.label).isEqualTo("Provisioning method") + } + + @Test + fun provisioningMethodConstraint_remote_withViolation_returnsCorrectMessage() { + val remoteConstraint = ProvisioningMethodConstraint.REMOTE + val factoryViolation = + assertIs( + remoteConstraint.check(keyDescriptionWithTeeSecurityLevels, factoryCertPath) + ) + assertThat(factoryViolation.failureMessage) + .isEqualTo( + "Provisioning method violates constraint: provisioningMethod=FACTORY_PROVISIONED, config=$remoteConstraint" + ) + assertThat(remoteConstraint.label).isEqualTo("Provisioning method") + } + + @Test + fun constraintConfig_provisioningMethod_configuredViaAdditionalConstraint() { + val config = constraintConfig { additionalConstraint { ProvisioningMethodConstraint.FACTORY } } + assertThat(config.additionalConstraints).contains(ProvisioningMethodConstraint.FACTORY) + assertThat(config.getConstraints()).contains(ProvisioningMethodConstraint.FACTORY) + } } diff --git a/src/test/kotlin/VerifierTest.kt b/src/test/kotlin/VerifierTest.kt index 0b08445..832a3eb 100644 --- a/src/test/kotlin/VerifierTest.kt +++ b/src/test/kotlin/VerifierTest.kt @@ -278,6 +278,64 @@ class VerifierTest { assertThat(result.securityLevel).isEqualTo(SecurityLevel.SOFTWARE) } + @Test + fun factoryProvisionedChain_withRemoteConstraint_failsVerification() { + val verifier = + Verifier( + { prodAnchors + TrustAnchor(Certs.root, null) }, + { setOf() }, + { FakeCalendar.DEFAULT.now() }, + constraintConfig { additionalConstraint { ProvisioningMethodConstraint.REMOTE } }, + ) + val result = assertIs(verifier.verify(CertLists.validFactoryProvisioned)) + assertThat(result.constraintLabel).isEqualTo("Provisioning method") + assertThat(result.cause) + .isEqualTo( + "Provisioning method violates constraint: provisioningMethod=FACTORY_PROVISIONED, config=REMOTE" + ) + } + + @Test + fun factoryProvisionedChain_withFactoryConstraint_succeedsVerification() { + val verifier = + Verifier( + { prodAnchors + TrustAnchor(Certs.root, null) }, + { setOf() }, + { FakeCalendar.DEFAULT.now() }, + constraintConfig { additionalConstraint { ProvisioningMethodConstraint.FACTORY } }, + ) + assertIs(verifier.verify(CertLists.validFactoryProvisioned)) + } + + @Test + fun rkpProvisionedChain_withFactoryConstraint_failsVerification() { + val verifier = + Verifier( + { prodAnchors + TrustAnchor(Certs.root, null) }, + { setOf() }, + { FakeCalendar.DEFAULT.now() }, + constraintConfig { additionalConstraint { ProvisioningMethodConstraint.FACTORY } }, + ) + val result = assertIs(verifier.verify(CertLists.validRemotelyProvisioned)) + assertThat(result.constraintLabel).isEqualTo("Provisioning method") + assertThat(result.cause) + .isEqualTo( + "Provisioning method violates constraint: provisioningMethod=REMOTELY_PROVISIONED, config=FACTORY" + ) + } + + @Test + fun rkpProvisionedChain_withRemoteConstraint_succeedsVerification() { + val verifier = + Verifier( + { prodAnchors + TrustAnchor(Certs.root, null) }, + { setOf() }, + { FakeCalendar.DEFAULT.now() }, + constraintConfig { additionalConstraint { ProvisioningMethodConstraint.REMOTE } }, + ) + assertIs(verifier.verify(CertLists.validRemotelyProvisioned)) + } + @Test fun importedOrigins_customConfig_succeeds() { val verifier = From a48898a68337b920cbd368eab5824f696d7bbf3d Mon Sep 17 00:00:00 2001 From: Seth Moore Date: Fri, 4 Sep 2026 11:47:55 -0700 Subject: [PATCH 31/48] Make SecurityLevel.STRICT constraint even more strict 1. Add a CompositeConstraint to allow for easily composing multiple constraints. 2. Make STRICT a CompositConstraint to enforce both the expected security level and that the security level matches what is in the cert. PiperOrigin-RevId: 976421083 --- src/main/kotlin/ConstraintConfig.kt | 35 ++++- src/test/kotlin/ConstraintConfigTest.kt | 165 +++++++++++++++++++++++- 2 files changed, 192 insertions(+), 8 deletions(-) diff --git a/src/main/kotlin/ConstraintConfig.kt b/src/main/kotlin/ConstraintConfig.kt index aab0e4d..caad15a 100644 --- a/src/main/kotlin/ConstraintConfig.kt +++ b/src/main/kotlin/ConstraintConfig.kt @@ -26,6 +26,7 @@ private typealias AttributeMapper = (KeyDescription) -> Any? /** An individual limit to place on the KeyDescription from an attestation certificate. */ @ThreadSafe +@Immutable sealed interface Constraint { sealed interface Result {} @@ -133,6 +134,34 @@ data object IgnoredConstraint : Constraint { Constraint.Satisfied } +/** Constraint that composes multiple input constraints and sequentially evaluates each one. */ +@Immutable +open class CompositeConstraint( + override val label: String, + val constraints: ImmutableList, +) : Constraint { + constructor( + label: String, + vararg constraints: Constraint, + ) : this( + label, + ImmutableList.copyOf(constraints), + ) + + override fun check( + description: KeyDescription, + certPath: KeyAttestationCertPath, + ): Constraint.Result { + for (constraint in constraints) { + val result = constraint.check(description, certPath) + if (result is Constraint.Violated) { + return result + } + } + return Constraint.Satisfied + } +} + /** Constraint that checks a single attribute of the [KeyDescription]. */ @Immutable(containerOf = ["T"]) sealed class AttributeConstraint(override val label: String, val mapper: AttributeMapper?) : @@ -200,12 +229,16 @@ sealed class SecurityLevelConstraint( /** * Checks that both the attestationSecurityLevel and keyMintSecurityLevel match the expected - * value. + * value, and that the keyMintSecurityLevel matches the certificate. * * @param expectedVal The expected value of the security level. */ @Immutable data class STRICT(val expectedVal: SecurityLevel) : + CompositeConstraint(LABEL, MATCHES_CERTIFICATE, MATCHES_EXPECTED(expectedVal)) + + @Immutable + internal data class MATCHES_EXPECTED(val expectedVal: SecurityLevel) : SecurityLevelConstraint({ desc, _ -> desc.keyMintSecurityLevel == expectedVal && desc.attestationSecurityLevel == expectedVal }) diff --git a/src/test/kotlin/ConstraintConfigTest.kt b/src/test/kotlin/ConstraintConfigTest.kt index 798bcea..de88da0 100644 --- a/src/test/kotlin/ConstraintConfigTest.kt +++ b/src/test/kotlin/ConstraintConfigTest.kt @@ -94,14 +94,28 @@ class ConstraintConfigTest { @Test fun SecurityLevelConstraintIsSatisfied_strictWithExpectedValue() { - val level = SecurityLevelConstraint.STRICT(SecurityLevel.STRONG_BOX) + val strongBoxCertPath = readCertPath("blueline/sdk28/SB_RSA_NONE.pem") + val strongBoxLevel = SecurityLevelConstraint.STRICT(SecurityLevel.STRONG_BOX) assertIs( - level.check(keyDescriptionWithStrongBoxSecurityLevels, testCertPath) + strongBoxLevel.check(keyDescriptionWithStrongBoxSecurityLevels, strongBoxCertPath) ) - assertIs(level.check(keyDescriptionWithTeeSecurityLevels, testCertPath)) assertIs( - level.check(keyDescriptionWithMismatchedSecurityLevels, testCertPath) + strongBoxLevel.check(keyDescriptionWithStrongBoxSecurityLevels, testCertPath) + ) + assertIs( + strongBoxLevel.check(keyDescriptionWithTeeSecurityLevels, testCertPath) + ) + assertIs( + strongBoxLevel.check(keyDescriptionWithMismatchedSecurityLevels, testCertPath) + ) + + val teeLevel = SecurityLevelConstraint.STRICT(SecurityLevel.TRUSTED_ENVIRONMENT) + assertIs( + teeLevel.check(keyDescriptionWithTeeSecurityLevels, testCertPath) + ) + assertIs( + teeLevel.check(keyDescriptionWithTeeSecurityLevels, strongBoxCertPath) ) } @@ -164,14 +178,30 @@ class ConstraintConfigTest { @Test fun securityLevelConstraint_withViolation_returnsCorrectMessage() { + val level = SecurityLevelConstraint.NOT_SOFTWARE + + val violation = + assertIs( + level.check(keyDescriptionWithSoftwareSecurityLevels, testCertPath) + ) + assertThat(violation.failureMessage) + .isEqualTo( + "Security level violates constraint: keyMintSecurityLevel=SOFTWARE, " + + "attestationSecurityLevel=SOFTWARE, config=$level" + ) + } + + @Test + fun securityLevelConstraintStrict_withCertificateMismatch_returnsCorrectMessage() { val level = SecurityLevelConstraint.STRICT(SecurityLevel.STRONG_BOX) val violation = - assertIs(level.check(keyDescriptionWithTeeSecurityLevels, testCertPath)) + assertIs( + level.check(keyDescriptionWithStrongBoxSecurityLevels, testCertPath) + ) assertThat(violation.failureMessage) .isEqualTo( - "Security level violates constraint: keyMintSecurityLevel=TRUSTED_ENVIRONMENT, " + - "attestationSecurityLevel=TRUSTED_ENVIRONMENT, config=$level" + "Security level of KeyMint (STRONG_BOX) does not match attestation certificate (TRUSTED_ENVIRONMENT)" ) } @@ -211,6 +241,127 @@ class ConstraintConfigTest { } } + @Test + fun SecurityLevelConstraintIsSatisfied_matchesCertificate_nullClaim() { + val factoryCertPathWithNoClaim = readCertPath("blueline/sdk28/TEE_EC_NONE.pem") + + assertIs( + SecurityLevelConstraint.MATCHES_CERTIFICATE.check( + keyDescriptionWithTeeSecurityLevels, + factoryCertPathWithNoClaim, + ) + ) + assertIs( + SecurityLevelConstraint.MATCHES_CERTIFICATE.check( + keyDescriptionWithSoftwareSecurityLevels, + factoryCertPathWithNoClaim, + ) + ) + assertIs( + SecurityLevelConstraint.MATCHES_CERTIFICATE.check( + keyDescriptionWithStrongBoxSecurityLevels, + factoryCertPathWithNoClaim, + ) + ) + } + + @Test + fun SecurityLevelConstraintIsSatisfied_matchesCertificate_trustedEnvironmentClaim() { + assertIs( + SecurityLevelConstraint.MATCHES_CERTIFICATE.check( + keyDescriptionWithTeeSecurityLevels, + testCertPath, + ) + ) + assertIs( + SecurityLevelConstraint.MATCHES_CERTIFICATE.check( + keyDescriptionWithSoftwareSecurityLevels, + testCertPath, + ) + ) + assertIs( + SecurityLevelConstraint.MATCHES_CERTIFICATE.check( + keyDescriptionWithStrongBoxSecurityLevels, + testCertPath, + ) + ) + } + + @Test + fun SecurityLevelConstraintIsSatisfied_matchesCertificate_strongBoxClaim() { + val strongBoxCertPath = readCertPath("blueline/sdk28/SB_RSA_NONE.pem") + + assertIs( + SecurityLevelConstraint.MATCHES_CERTIFICATE.check( + keyDescriptionWithStrongBoxSecurityLevels, + strongBoxCertPath, + ) + ) + assertIs( + SecurityLevelConstraint.MATCHES_CERTIFICATE.check( + keyDescriptionWithTeeSecurityLevels, + strongBoxCertPath, + ) + ) + assertIs( + SecurityLevelConstraint.MATCHES_CERTIFICATE.check( + keyDescriptionWithSoftwareSecurityLevels, + strongBoxCertPath, + ) + ) + } + + @Test + fun matchesCertificate_withViolation_returnsCorrectMessage() { + val violation = + assertIs( + SecurityLevelConstraint.MATCHES_CERTIFICATE.check( + keyDescriptionWithStrongBoxSecurityLevels, + testCertPath, + ) + ) + assertThat(violation.failureMessage) + .isEqualTo( + "Security level of KeyMint (STRONG_BOX) does not match attestation certificate (TRUSTED_ENVIRONMENT)" + ) + } + + @Test + fun compositeConstraint_allSatisfied_returnsSatisfied() { + val composite = + CompositeConstraint( + "test constraint", + SecurityLevelConstraint.CONSISTENT, + SecurityLevelConstraint.NOT_SOFTWARE, + ) + + assertIs( + composite.check(keyDescriptionWithTeeSecurityLevels, testCertPath) + ) + assertThat(composite.label).isEqualTo("test constraint") + } + + @Test + fun compositeConstraint_stopsOnFirstViolation() { + val composite = + CompositeConstraint("test", SecurityLevelConstraint.NOT_SOFTWARE, TagOrderConstraint.STRICT) + + val violation = + assertIs( + composite.check(keyDescriptionWithSoftwareSecurityLevels, testCertPath) + ) + assertThat(violation.failureMessage).contains("Security level violates constraint") + } + + @Test + fun compositeConstraint_emptyDefaultLabel() { + val composite = CompositeConstraint("test composite") + assertThat(composite.label).isEqualTo("test composite") + assertIs( + composite.check(keyDescriptionWithTeeSecurityLevels, testCertPath) + ) + } + @Test fun ProvisioningMethodConstraintIsSatisfied_factory_allowsFactoryCertPath() { assertIs( From c0464605bbc169a19c1b7c822c37a8f48b432c90 Mon Sep 17 00:00:00 2001 From: Carmen Hanish Date: Thu, 10 Sep 2026 08:27:12 -0700 Subject: [PATCH 32/48] Add release document to track release changes. PiperOrigin-RevId: 979199864 --- RELEASE.md | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 RELEASE.md diff --git a/RELEASE.md b/RELEASE.md new file mode 100644 index 0000000..51a5385 --- /dev/null +++ b/RELEASE.md @@ -0,0 +1,7 @@ +# Release Notes + +## v0.1 +A test release. + +It is a working version of the library at a given point in time, before any +BETA/1.0 release. \ No newline at end of file From 2faf60eacc825b33ba6ef3a44b8e1d4c17f48925 Mon Sep 17 00:00:00 2001 From: Carmen Hanish Date: Thu, 10 Sep 2026 09:25:54 -0700 Subject: [PATCH 33/48] Add more information about v0.1. PiperOrigin-RevId: 979228512 --- RELEASE.md | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/RELEASE.md b/RELEASE.md index 51a5385..9ddea70 100644 --- a/RELEASE.md +++ b/RELEASE.md @@ -4,4 +4,8 @@ A test release. It is a working version of the library at a given point in time, before any -BETA/1.0 release. \ No newline at end of file +BETA/1.0 release. + +We plan to export more parameters and accept more checks but verification is +supported and the information for the minimum verification use case is +returned. \ No newline at end of file From 100dedaea2387d9705d29b2be3177466c33ae17b Mon Sep 17 00:00:00 2001 From: Carmen Hanish Date: Fri, 11 Sep 2026 08:14:43 -0700 Subject: [PATCH 34/48] Fix a bunch of misspellings and bad grammar. PiperOrigin-RevId: 979828575 --- README.md | 2 +- src/main/kotlin/ChallengeChecker.kt | 2 +- src/main/kotlin/ConstraintConfig.kt | 2 +- src/main/kotlin/Verifier.kt | 4 ++-- src/main/kotlin/VerifierCli.kt | 2 +- src/main/kotlin/provider/KeyAttestationCertPath.kt | 2 +- src/main/kotlin/provider/KeyAttestationCertPathValidator.kt | 4 ++-- src/main/kotlin/provider/KeyAttestationProvider.kt | 4 ++-- src/main/kotlin/testing/Certs.kt | 2 +- src/main/kotlin/testing/KeyAttestationCertFactory.kt | 4 ++-- src/test/kotlin/VerifierCliTest.kt | 2 +- 11 files changed, 15 insertions(+), 15 deletions(-) diff --git a/README.md b/README.md index 5ac3186..51b081c 100644 --- a/README.md +++ b/README.md @@ -100,7 +100,7 @@ Android Key Attestation root certificates are documented ## Getting Revoked Serials -It's important to check the revoked serials list to prevent allowing accepting +It's important to check the revoked serials list to prevent accepting fraudulent attestations from known leaked keys. The revoked serials may be retrieved from https://android.googleapis.com/attestation/status. This list is updated relatively frequently so it's important to use a fresh copy of the diff --git a/src/main/kotlin/ChallengeChecker.kt b/src/main/kotlin/ChallengeChecker.kt index 7d091aa..51bc5e1 100644 --- a/src/main/kotlin/ChallengeChecker.kt +++ b/src/main/kotlin/ChallengeChecker.kt @@ -31,7 +31,7 @@ interface ChallengeChecker { /** * Checks the given [challenge] for validity. * - * @return A ListenableFuture containing True if the challenge is valid, else false. + * @return A ListenableFuture containing true if the challenge is valid, else false. */ fun checkChallenge(challenge: ByteString): ListenableFuture } diff --git a/src/main/kotlin/ConstraintConfig.kt b/src/main/kotlin/ConstraintConfig.kt index caad15a..4cfa543 100644 --- a/src/main/kotlin/ConstraintConfig.kt +++ b/src/main/kotlin/ConstraintConfig.kt @@ -266,7 +266,7 @@ sealed class SecurityLevelConstraint( /** * 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 + * 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. diff --git a/src/main/kotlin/Verifier.kt b/src/main/kotlin/Verifier.kt index 4300f40..ae3f488 100644 --- a/src/main/kotlin/Verifier.kt +++ b/src/main/kotlin/Verifier.kt @@ -201,7 +201,7 @@ constructor( * Verifies an Android Key Attestation certificate chain asynchronously. * * @param chain The attestation certificate chain to verify. - * @param coroutineScope The coroutine scope to from which to run the verification. + * @param coroutineScope The coroutine scope from which to run the verification. * @param challengeChecker The challenge checker to use for additional challenge validation. * @param log The log hook to use for logging. * @return A [ListenableFuture] containing the [VerificationResult]. @@ -285,7 +285,7 @@ constructor( inputLimits = constraintConfig.inputLimits, ) ) { - // Should never happen since the extension's presence is checked by by validate(). + // Should never happen since the extension's presence is checked by validate(). "Key attestation extension not found" } } catch (e: ExtensionParsingException) { diff --git a/src/main/kotlin/VerifierCli.kt b/src/main/kotlin/VerifierCli.kt index 6d151ba..d8fc0e3 100644 --- a/src/main/kotlin/VerifierCli.kt +++ b/src/main/kotlin/VerifierCli.kt @@ -21,7 +21,7 @@ import java.io.PrintStream import java.security.cert.X509Certificate import java.time.Instant -// Any chain shorter than this is not possibly valid Key Attestation chain. +// Any chain shorter than this cannot possibly be a valid Key Attestation chain. private const val MIN_CERTS_IN_VALID_CHAIN = 3 class VerifierCli(private val output: PrintStream, private val instantSource: InstantSource) { diff --git a/src/main/kotlin/provider/KeyAttestationCertPath.kt b/src/main/kotlin/provider/KeyAttestationCertPath.kt index 257eb36..44d69b6 100644 --- a/src/main/kotlin/provider/KeyAttestationCertPath.kt +++ b/src/main/kotlin/provider/KeyAttestationCertPath.kt @@ -98,7 +98,7 @@ class KeyAttestationCertPath(certs: List) : CertPath("X.509") { } /* - * The security level of the certitificate chain. + * The security level of the certificate chain. * * This should match the attestation security level in the key description. */ diff --git a/src/main/kotlin/provider/KeyAttestationCertPathValidator.kt b/src/main/kotlin/provider/KeyAttestationCertPathValidator.kt index e749e2b..a84e48c 100644 --- a/src/main/kotlin/provider/KeyAttestationCertPathValidator.kt +++ b/src/main/kotlin/provider/KeyAttestationCertPathValidator.kt @@ -45,7 +45,7 @@ import javax.security.auth.x500.X500Principal * A [CertPathValidatorSpi] for verifying Android Key Attestation certificate chains. * * Older Android devices produce Key Attestation certificate chains that do not fully conform to RFC - * 5280 and thus cannot be validating using [sun.security.provider.certpath.PKIXCertPathValidator]. + * 5280 and thus cannot be validated using [sun.security.provider.certpath.PKIXCertPathValidator]. * This provider implements a more permissive [CertPathValidatorSpi] implementation that is able to * validate these chains. * @@ -334,7 +334,7 @@ private class BasicChecker( // TODO(google-internal bug): Add support for ATTEST_KEY chains. if (cert.hasAttestationExtension()) { throw CertPathValidatorException( - "Only the target certificate should contain an attestation extension, attestation extenion found in ${step}", + "Only the target certificate should contain an attestation extension, attestation extension found in ${step}", /* cause = */ null, certPath, /* index = */ remainingCerts, diff --git a/src/main/kotlin/provider/KeyAttestationProvider.kt b/src/main/kotlin/provider/KeyAttestationProvider.kt index 3c7f9e0..a6da2a8 100644 --- a/src/main/kotlin/provider/KeyAttestationProvider.kt +++ b/src/main/kotlin/provider/KeyAttestationProvider.kt @@ -20,7 +20,7 @@ import java.security.Provider import java.security.ProviderException /** - * A JCA provider for verifying Android Key Attestation certificates chains. + * A JCA provider for verifying Android Key Attestation certificate chains. * * https://docs.oracle.com/en/java/javase/21/security/howtoimplaprovider.html */ @@ -33,7 +33,7 @@ class KeyAttestationProvider : Provider("KeyAttestation", 0.1, "Android Key Atte this, "CertPathValidator", "KeyAttestation", - "com.google.wireless.android.security.attestationverifier.provider.KeyAttestationCertPathValidator", + "com.android.keyattestation.verifier.provider.KeyAttestationCertPathValidator", ) ) } diff --git a/src/main/kotlin/testing/Certs.kt b/src/main/kotlin/testing/Certs.kt index 0884798..0292fef 100644 --- a/src/main/kotlin/testing/Certs.kt +++ b/src/main/kotlin/testing/Certs.kt @@ -400,7 +400,7 @@ object CertLists { val REVOKED_SERIAL_NUMBER_LONG_STRING = "c35747a084470c3135aeefe2b8d40cd6".toBigInteger(16) @JvmField val REVOKED_SERIAL_NUMBER_ODD_LENGTH = 1228286566665971148.toBigInteger() - /* A chain where the attesstation certificate has {@link REVOKED_SERIAL_NUMBER}. */ + /* A chain where the attestation certificate has {@link REVOKED_SERIAL_NUMBER}. */ @JvmStatic val revoked by lazy { listOf( diff --git a/src/main/kotlin/testing/KeyAttestationCertFactory.kt b/src/main/kotlin/testing/KeyAttestationCertFactory.kt index 74926d4..7e5a639 100644 --- a/src/main/kotlin/testing/KeyAttestationCertFactory.kt +++ b/src/main/kotlin/testing/KeyAttestationCertFactory.kt @@ -47,7 +47,7 @@ import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder internal class KeyAttestationCertFactory( val fakeCalendar: FakeCalendar = FakeCalendar.DEFAULT, - val hardodedRootKey: KeyPair? = null, + val hardcodedRootKey: KeyPair? = null, val hardcodedRoot: X509Certificate? = null, ) { private val ecKeyPairGenerator = @@ -64,7 +64,7 @@ internal class KeyAttestationCertFactory( internal fun generateRsaKeyPair() = rsaKeyPairGenerator.generateKeyPair() - val rootKey = hardodedRootKey ?: ecKeyPairGenerator.generateKeyPair() + val rootKey = hardcodedRootKey ?: ecKeyPairGenerator.generateKeyPair() val intermediateKey = ecKeyPairGenerator.generateKeyPair() val rkpKey = ecKeyPairGenerator.generateKeyPair() val attestationKey = ecKeyPairGenerator.generateKeyPair() diff --git a/src/test/kotlin/VerifierCliTest.kt b/src/test/kotlin/VerifierCliTest.kt index 06002e1..efbe399 100644 --- a/src/test/kotlin/VerifierCliTest.kt +++ b/src/test/kotlin/VerifierCliTest.kt @@ -31,7 +31,7 @@ import org.junit.runners.JUnit4 @RunWith(JUnit4::class) class VerifierCliTest { companion object { - // For tests have no time dependency + // For tests which have no time dependency val INCONSEQUENTIAL_TIME = Instant.EPOCH private fun resolveTestData(path: String) = From 33cca483cccf29c040f2acde6fbde42c638ca52f Mon Sep 17 00:00:00 2001 From: Carmen Hanish Date: Tue, 15 Sep 2026 02:02:20 -0700 Subject: [PATCH 35/48] Add publishing info to build.gradle.kts. PiperOrigin-RevId: 981664058 --- build.gradle.kts | 69 ++++++++++++++++++++++++++++++++++-------------- 1 file changed, 49 insertions(+), 20 deletions(-) diff --git a/build.gradle.kts b/build.gradle.kts index 325ff5d..16b8f6d 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -17,6 +17,7 @@ plugins { id("com.adarshr.test-logger") version "4.0.0" id("org.jetbrains.kotlin.jvm") version "2.2.0" + `maven-publish` } repositories { @@ -45,7 +46,10 @@ dependencies { testRuntimeOnly("org.junit.vintage:junit-vintage-engine") } -java { toolchain { languageVersion = JavaLanguageVersion.of(21) } } +java { + toolchain { languageVersion = JavaLanguageVersion.of(21) } + withSourcesJar() +} tasks { test { @@ -58,20 +62,19 @@ tasks { val generatedSourcesDir = layout.buildDirectory.dir("generated") -val googleTrustAnchors by - tasks.registering { - val jsonFile = file("roots.json") - val json = jsonFile.readText() - val generatedFile = generatedSourcesDir.get().file("main/kotlin/GoogleTrustAnchors.kt") +val googleTrustAnchors by tasks.registering { + val jsonFile = file("roots.json") + val json = jsonFile.readText() + val generatedFile = generatedSourcesDir.get().file("main/kotlin/GoogleTrustAnchors.kt") - inputs.files(jsonFile) - outputs.file(generatedFile) + inputs.files(jsonFile) + outputs.file(generatedFile) - doLast { - generatedFile - .getAsFile() - .writeText( - """ + doLast { + generatedFile + .getAsFile() + .writeText( + """ package com.android.keyattestation.verifier import com.android.keyattestation.verifier.asX509Certificate @@ -92,16 +95,42 @@ val googleTrustAnchors by } } """ - ) - } + ) } +} -val generateSources by - tasks.registering { - outputs.dir(generatedSourcesDir) - dependsOn(tasks.named("googleTrustAnchors")) - } +val generateSources by tasks.registering { + outputs.dir(generatedSourcesDir) + dependsOn(tasks.named("googleTrustAnchors")) +} sourceSets { main { kotlin.srcDir(generateSources) } } tasks.named("compileKotlin").configure { dependsOn("generateSources") } + +publishing { + publications { + create("keyattestation") { + from(components["java"]) + groupId = "com.android.keyattestation" + artifactId = "keyattestation" + version = + (findProperty("keyAttestationReleaseVersion") as? String)?.removePrefix("v") + ?: "0.1-SNAPSHOT" + } + } + + repositories { + maven { + name = "localDir" + val repoDir = + findProperty("KeyAttestationMavenRepo") as? String + ?: rootProject.layout.buildDirectory + .dir("keyattestation_m2repo") + .get() + .asFile + .absolutePath + url = uri(repoDir) + } + } +} From b8a3b3d6b07f6b0a09d97adf6c1b95fa72079263 Mon Sep 17 00:00:00 2001 From: Kevin Chao Date: Thu, 17 Sep 2026 08:05:30 -0700 Subject: [PATCH 36/48] Add DataItem.asLong() extension function and refactor asInt function. PiperOrigin-RevId: 983201340 --- src/main/kotlin/Extension.kt | 31 ++++++++-- src/test/kotlin/ExtensionTest.kt | 40 ++++++++++++ testdata/frankel/sdk37/TEE_EC_2026.json | 37 +++++++++++ testdata/frankel/sdk37/TEE_EC_2026.pem | 81 +++++++++++++++++++++++++ 4 files changed, 183 insertions(+), 6 deletions(-) create mode 100644 testdata/frankel/sdk37/TEE_EC_2026.json create mode 100644 testdata/frankel/sdk37/TEE_EC_2026.pem diff --git a/src/main/kotlin/Extension.kt b/src/main/kotlin/Extension.kt index bb1e4a5..a0036dd 100644 --- a/src/main/kotlin/Extension.kt +++ b/src/main/kotlin/Extension.kt @@ -38,6 +38,7 @@ import java.math.BigInteger import java.nio.ByteBuffer import java.nio.charset.CodingErrorAction import java.security.cert.X509Certificate +import java.time.Instant import java.time.YearMonth import java.time.format.DateTimeFormatter import java.time.format.DateTimeParseException @@ -913,16 +914,34 @@ fun cborEncode(dataItem: DataItem): ByteArray { return baos.toByteArray() } -fun DataItem.asInteger(): Int { - if (this.majorType == MajorType.UNSIGNED_INTEGER) { - return (this as UnsignedInteger).value.toInt() +private fun BigInteger.toIntExact(): Int { + if (bitLength() < Integer.SIZE) { + return toInt() } - if (this.majorType == MajorType.NEGATIVE_INTEGER) { - return (this as NegativeInteger).value.toInt() + throw CborException("BigInteger out of int range") +} + +private fun BigInteger.toLongExact(): Long { + if (bitLength() < java.lang.Long.SIZE) { + return toLong() } - throw CborException("Expected a number, got ${this.majorType}") + throw CborException("BigInteger out of long range") } +fun DataItem.asInteger(): Int = + when (this) { + is UnsignedInteger -> value.toIntExact() + is NegativeInteger -> value.toIntExact() + else -> throw CborException("Expected a number, got $majorType") + } + +fun DataItem.asLong(): Long = + when (this) { + is UnsignedInteger -> value.toLongExact() + is NegativeInteger -> value.toLongExact() + else -> throw CborException("Expected a number, got $majorType") + } + fun Int.asDataItem() = when { this >= 0 -> UnsignedInteger(this.toLong()) diff --git a/src/test/kotlin/ExtensionTest.kt b/src/test/kotlin/ExtensionTest.kt index 082d948..59648af 100644 --- a/src/test/kotlin/ExtensionTest.kt +++ b/src/test/kotlin/ExtensionTest.kt @@ -15,6 +15,9 @@ */ package com.android.keyattestation.verifier +import co.nstant.`in`.cbor.CborException +import co.nstant.`in`.cbor.model.NegativeInteger +import co.nstant.`in`.cbor.model.UnsignedInteger import com.android.keyattestation.verifier.testing.Chains import com.android.keyattestation.verifier.testing.FakeLogHook import com.android.keyattestation.verifier.testing.TestUtils.TESTDATA_PATH @@ -31,6 +34,8 @@ 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.time.Instant import java.time.YearMonth import kotlin.io.path.Path import kotlin.io.path.inputStream @@ -255,4 +260,39 @@ class ExtensionTest { AttestationApplicationId.from(seq, inputLimits = limits) } } + + @Test + fun asInteger_validValues_success() { + assertThat(UnsignedInteger(0).asInteger()).isEqualTo(0) + assertThat(UnsignedInteger(Int.MAX_VALUE.toLong()).asInteger()).isEqualTo(Int.MAX_VALUE) + assertThat(NegativeInteger(-1).asInteger()).isEqualTo(-1) + assertThat(NegativeInteger(Int.MIN_VALUE.toLong()).asInteger()).isEqualTo(Int.MIN_VALUE) + } + + @Test + fun asInteger_outOfBounds_throws() { + val unsignedOutOfRange = UnsignedInteger(Int.MAX_VALUE.toLong() + 1) + assertThrows(CborException::class.java) { unsignedOutOfRange.asInteger() } + + val negativeOutOfRange = NegativeInteger(Int.MIN_VALUE.toLong() - 1) + assertThrows(CborException::class.java) { negativeOutOfRange.asInteger() } + } + + @Test + fun asLong_validValues_success() { + assertThat(UnsignedInteger(0).asLong()).isEqualTo(0L) + assertThat(UnsignedInteger(Long.MAX_VALUE).asLong()).isEqualTo(Long.MAX_VALUE) + assertThat(NegativeInteger(-1).asLong()).isEqualTo(-1L) + assertThat(NegativeInteger(Long.MIN_VALUE).asLong()).isEqualTo(Long.MIN_VALUE) + } + + @Test + fun asLong_outOfBounds_throws() { + val unsignedOutOfRange = UnsignedInteger(BigInteger.valueOf(Long.MAX_VALUE).add(BigInteger.ONE)) + assertThrows(CborException::class.java) { unsignedOutOfRange.asLong() } + + val negativeOutOfRange = + NegativeInteger(BigInteger.valueOf(Long.MIN_VALUE).subtract(BigInteger.ONE)) + assertThrows(CborException::class.java) { negativeOutOfRange.asLong() } + } } diff --git a/testdata/frankel/sdk37/TEE_EC_2026.json b/testdata/frankel/sdk37/TEE_EC_2026.json new file mode 100644 index 0000000..d37e9cd --- /dev/null +++ b/testdata/frankel/sdk37/TEE_EC_2026.json @@ -0,0 +1,37 @@ +{ + "attestationVersion": "500", + "attestationSecurityLevel": "TRUSTED_ENVIRONMENT", + "keyMintVersion": "500", + "keyMintSecurityLevel": "TRUSTED_ENVIRONMENT", + "attestationChallenge": "ZTg2NTJlYzItYzMyMS00YTNjLWIxMTUtOTViNTljZmY2ODM1", + "uniqueId": "", + "softwareEnforced": { + "creationDateTime": "1788561021019", + "attestationApplicationId": { + "packages": [ + { "name": "com.google.android.attestation", "version": "0" } + ], + "signatures": ["EDk47kU35Z6O55L2VFBPuDRvxrNG0LvEQV/DOfz8jsE="] + }, + "moduleHash": "VcwTOKRmy0LeH74U/mlOH8fJr/wugrIfIYsn7VXyWMI=" + }, + "hardwareEnforced": { + "purposes": ["2", "3"], + "algorithms": "3", + "keySize": "256", + "digests": ["5"], + "ecCurve": "1", + "noAuthRequired": true, + "origin": "GENERATED", + "rootOfTrust": { + "verifiedBootKey": "dXxiaiqR/oUlNlRgSNfKP1DfbHRcAm25/4nMRwPFlIE=", + "deviceLocked": true, + "verifiedBootState": "VERIFIED", + "verifiedBootHash": "aX1ZF95j1nf0HO4ZokxEa+6aGWsWuUyL5M9Dy4FnDRA=" + }, + "osVersion": "170000", + "osPatchLevel": "202609", + "vendorPatchLevel": "20260905", + "bootPatchLevel": "20260905" + } +} diff --git a/testdata/frankel/sdk37/TEE_EC_2026.pem b/testdata/frankel/sdk37/TEE_EC_2026.pem new file mode 100644 index 0000000..f4ef20d --- /dev/null +++ b/testdata/frankel/sdk37/TEE_EC_2026.pem @@ -0,0 +1,81 @@ +-----BEGIN CERTIFICATE----- +MIIC2zCCAoCgAwIBAgIBATAKBggqhkjOPQQDAjA5MQwwCgYDVQQKEwNURUUxKTAn +BgNVBAMTIDJiM2ViYTAwMmFiMzA4MTU3YWYwMTQ1NDlkZjFjODNiMB4XDTcwMDEw +MTAwMDAwMFoXDTQ4MDEwMTAwMDAwMFowHzEdMBsGA1UEAxMUQW5kcm9pZCBLZXlz +dG9yZSBLZXkwWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAARq5J8IVjtehkQpB+Fv +Mi1bA43gzRuQ7/dyNLZF4xtHnrsUYK6ig0h7Vb3dp2X/WwJjCmI5cMBFwqhmxmAb +8m69o4IBkTCCAY0wDgYDVR0PAQH/BAQDAgeAMIIBeQYKKwYBBAHWeQIBEQSCAWkw +ggFlAgIB9AoBAQICAfQKAQEEJGU4NjUyZWMyLWMzMjEtNGEzYy1iMTE1LTk1YjU5 +Y2ZmNjgzNQQAMIGFv4U9CAIGAaBui1hbv4VFTwRNMEsxJTAjBB5jb20uZ29vZ2xl +LmFuZHJvaWQuYXR0ZXN0YXRpb24CAQAxIgQgEDk47kU35Z6O55L2VFBPuDRvxrNG +0LvEQV/DOfz8jsG/hVQiBCBVzBM4pGbLQt4fvhT+aU4fx8mv/C6Csh8hiyftVfJY +wjCBpKEIMQYCAQICAQOiAwIBA6MEAgIBAKUFMQMCAQWqAwIBAb+DdwIFAL+FPgMC +AQC/hUBMMEoEIHV8Ymoqkf6FJTZUYEjXyj9Q32x0XAJtuf+JzEcDxZSBAQH/CgEA +BCBpfVkX3mPWd/Qc7hmiTERr7poZaxa5TIvkz0PLgWcNEL+FQQUCAwKYEL+FQgUC +AwMXcb+FTgYCBAE1KCm/hU8GAgQBNSgpMAoGCCqGSM49BAMCA0kAMEYCIQD9S4+I +DM3e5MMtop20CM1MHdMdsTLBtNNVHpJ3urpwtQIhAIGgRmEnQ4uAITQLqhxrkl37 +yJhuz/s4Lo045okdIzSW +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIChTCCAiugAwIBAgIQKz66ACqzCBV68BRUnfHIOzAKBggqhkjOPQQDAjApMRMw +EQYDVQQKEwpHb29nbGUgTExDMRIwEAYDVQQDEwlEcm9pZCBDQTMwHhcNMjYwOTAy +MjI1NTM3WhcNMjYwOTE1MDAwNzU5WjA5MQwwCgYDVQQKEwNURUUxKTAnBgNVBAMT +IDJiM2ViYTAwMmFiMzA4MTU3YWYwMTQ1NDlkZjFjODNiMFkwEwYHKoZIzj0CAQYI +KoZIzj0DAQcDQgAEHjH4P2R+K9c6wf9jkabPPkMdc1dzGVcXTrjVOoANzV2eBgwQ +vFN82Zsgx8oGfhOIO7NW5V6ycVdUnMMmWQl09KOCASMwggEfMB0GA1UdDgQWBBRD +9of3bQKmV+g+y4insCxx2d1NHjAfBgNVHSMEGDAWgBRorxySXNQcnevbU9HfpwT6 +SPcPZTAPBgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwICBDCBuwYKKwYBBAHW +eQIBHgSBrKMBGIADZkdvb2dsZQWkAWhBUFBST1ZFRAJ4QDY5N2Q1OTE3ZGU2M2Q2 +NzdmNDFjZWUxOWEyNGM0NDZiZWU5YTE5NmIxNmI5NGM4YmU0Y2Y0M2NiODE2NzBk +MTADaEFQUFJPVkVEBHhEZ29vZ2xlL2ZyYW5rZWwvZnJhbmtlbDoxNy9DUDNBLjI2 +MDkwNS4wMDkvMTYwOTE2MTQ6dXNlci9yZWxlYXNlLWtleXMwCgYIKoZIzj0EAwID +SAAwRQIhAIhoqyfqQzLFTDdoIiNWL3naUVB8laG7W4rabr8EPurKAiAZZIxGbTMt +MkFdb3UztsEBHJqzYldh7GvmAo2uGV3M1Q== +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIC7zCCAnWgAwIBAgITCgl60lYA2+yLTRsaqGoyPJX4YjAKBggqhkjOPQQDAzAp +MRMwEQYDVQQKEwpHb29nbGUgTExDMRIwEAYDVQQDEwlEcm9pZCBDQTIwHhcNMjYw +ODI1MTEzMzE1WhcNMjYxMTAzMTEzMzE0WjApMRMwEQYDVQQKEwpHb29nbGUgTExD +MRIwEAYDVQQDEwlEcm9pZCBDQTMwWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAAQa +3dB6JcyU/E4u94vUu+twif1wDTUtcjfUtXY/9Ow4DKTdpMKTzYfXKlMb7iA+tAqU ++nrxp1XX+I5q2sRpOGIdo4IBejCCAXYwDgYDVR0PAQH/BAQDAgIEMA8GA1UdEwEB +/wQFMAMBAf8wHQYDVR0OBBYEFGivHJJc1Byd69tT0d+nBPpI9w9lMB8GA1UdIwQY +MBaAFPuU7nThtXAWlYmOhiIonUwP4cMdMIGNBggrBgEFBQcBAQSBgDB+MHwGCCsG +AQUFBzAChnBodHRwOi8vcHJpdmF0ZWNhLWNvbnRlbnQtNjk5NDIyNDEtMDAwMC0y +ODNmLWJlMjEtMzRjN2U5MjEyZjhiLnN0b3JhZ2UuZ29vZ2xlYXBpcy5jb20vMjk3 +MDJjZjExOTA4OTNlY2Q2YjQvY2EuY3J0MIGCBgNVHR8EezB5MHegdaBzhnFodHRw +Oi8vcHJpdmF0ZWNhLWNvbnRlbnQtNjk5NDIyNDEtMDAwMC0yODNmLWJlMjEtMzRj +N2U5MjEyZjhiLnN0b3JhZ2UuZ29vZ2xlYXBpcy5jb20vMjk3MDJjZjExOTA4OTNl +Y2Q2YjQvY3JsLmNybDAKBggqhkjOPQQDAwNoADBlAjEA/Nm+tk/PbqpS6qZ1kK/J +pVrH4EVcpiqLzt7zzc69/KNOXPxE0X9Po5yOEcCVTnZ5AjBSnqDgUCJqqPpMe0t4 +uDP+o8n1PllPp343BV5MR5zYXtw5kE7MGj5HNEBj3cX+z7I= +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIICZDCCAeugAwIBAgIRAPLC/gLfzdARgeSj5rNpoowwCgYIKoZIzj0EAwMwUjEc +MBoGA1UEAwwTS2V5IEF0dGVzdGF0aW9uIENBMTEQMA4GA1UECwwHQW5kcm9pZDET +MBEGA1UECgwKR29vZ2xlIExMQzELMAkGA1UEBhMCVVMwHhcNMjYwMjA5MjAwMTEx +WhcNMjkwMjA4MjAwMTExWjApMRMwEQYDVQQKEwpHb29nbGUgTExDMRIwEAYDVQQD +EwlEcm9pZCBDQTIwdjAQBgcqhkjOPQIBBgUrgQQAIgNiAATkwn4jOZw/zpxhsBn4 +27C8Xz684+3Ajq5zsIzXwYlQPGieyFBuNxkUUFSa4YzZObqTOrgI9iFcfTBqOuOl +yEtIuipjVowV9UCddBKO5ndqPTEk8Dd2RWn4yMtUTnyMMpGjga0wgaowHwYDVR0j +BBgwFoAUUjK7LPtGQ5vc1oGpDmVm4DRB6kAwDwYDVR0TAQH/BAUwAwEB/zAdBgNV +HQ4EFgQU+5TudOG1cBaViY6GIiidTA/hwx0wDgYDVR0PAQH/BAQDAgEGMEcGA1Ud +HwRAMD4wPKA6oDiGNmh0dHBzOi8vYW5kcm9pZC5nb29nbGVhcGlzLmNvbS9hdHRl +c3RhdGlvbi9rZXlfY2ExLmNybDAKBggqhkjOPQQDAwNnADBkAjArwb7NmSVBlasM +dMRjY0FFEum0b+SUZTMmvBT5AGYzk8xGCi2Mj2NZdchxZfxHUJgCMDseaiAzoixN +ISk40rfoR4vMvs7n9r4fgEgmb+9KQbpDgdq0+90mzcAL4vKr4hWSxA== +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIICIjCCAaigAwIBAgIRAISp0Cl7DrWK5/8OgN52BgUwCgYIKoZIzj0EAwMwUjEc +MBoGA1UEAwwTS2V5IEF0dGVzdGF0aW9uIENBMTEQMA4GA1UECwwHQW5kcm9pZDET +MBEGA1UECgwKR29vZ2xlIExMQzELMAkGA1UEBhMCVVMwHhcNMjUwNzE3MjIzMjE4 +WhcNMzUwNzE1MjIzMjE4WjBSMRwwGgYDVQQDDBNLZXkgQXR0ZXN0YXRpb24gQ0Ex +MRAwDgYDVQQLDAdBbmRyb2lkMRMwEQYDVQQKDApHb29nbGUgTExDMQswCQYDVQQG +EwJVUzB2MBAGByqGSM49AgEGBSuBBAAiA2IABCPaI3FO3z5bBQo8cuiEas4HjqCt +G/mLFfRT0MsIssPBEEU5Cfbt6sH5yOAxqEi5QagpU1yX4HwnGb7OtBYpDTB57uH5 +Eczm34A5FNijV3s0/f0UPl7zbJcTx6xwqMIRq6NCMEAwDwYDVR0TAQH/BAUwAwEB +/zAOBgNVHQ8BAf8EBAMCAQYwHQYDVR0OBBYEFFIyuyz7RkOb3NaBqQ5lZuA0QepA +MAoGCCqGSM49BAMDA2gAMGUCMETfjPO/HwqReR2CS7p0ZWoD/LHs6hDi422opifH +EUaYLxwGlT9SLdjkVpz0UUOR5wIxAIoGyxGKRHVTpqpGRFiJtQEOOTp/+s1GcxeY +uR2zh/80lQyu9vAFCj6E4AXc+osmRg== +-----END CERTIFICATE----- \ No newline at end of file From bc8737c33470dcba53042512e0290ae9e059e086 Mon Sep 17 00:00:00 2001 From: Suzanna Jiwani Date: Fri, 18 Sep 2026 01:44:07 -0700 Subject: [PATCH 37/48] Remove areTagsOrdered from generated json It's not included in the actual cert chains so it shouldn't be in the jsons representing test data. PiperOrigin-RevId: 983716755 --- src/main/kotlin/Extension.kt | 2 +- src/test/kotlin/ExtensionTest.kt | 18 ++++++++++++++++++ testdata/sony-xperia10-iii/sdk33/TEE_EC.json | 6 ++---- testdata/tegu/sdk36/SB_EC_2026_ROOT.json | 6 ++---- testdata/tegu/sdk36/TEE_EC_2026_ROOT.json | 6 ++---- testdata/tegu/sdk37/TEE_MAX_USAGE_COUNT.json | 6 ++---- testdata/tegu/sdk37/TEE_TRUSTED_CONF.json | 6 ++---- testdata/tokay/sdk37/TEE_MLDSA_FACTORY.json | 6 ++---- testdata/tokay/sdk37/TEE_MLDSA_RKP.json | 6 ++---- 9 files changed, 33 insertions(+), 29 deletions(-) diff --git a/src/main/kotlin/Extension.kt b/src/main/kotlin/Extension.kt index a0036dd..2f16779 100644 --- a/src/main/kotlin/Extension.kt +++ b/src/main/kotlin/Extension.kt @@ -397,7 +397,7 @@ data class AuthorizationList( val bootPatchLevel: PatchLevel? = null, val attestationIdSecondImei: String? = null, val moduleHash: ByteString? = null, - @get:JvmName("areTagsOrdered") internal val areTagsOrdered: Boolean = true, + @Transient @get:JvmName("areTagsOrdered") internal val areTagsOrdered: Boolean = true, ) { /** * Converts the representation of an [AuthorizationList] to an ASN.1 sequence. diff --git a/src/test/kotlin/ExtensionTest.kt b/src/test/kotlin/ExtensionTest.kt index 59648af..64279f9 100644 --- a/src/test/kotlin/ExtensionTest.kt +++ b/src/test/kotlin/ExtensionTest.kt @@ -24,6 +24,7 @@ import com.android.keyattestation.verifier.testing.TestUtils.TESTDATA_PATH import com.android.keyattestation.verifier.testing.TestUtils.getTestCases import com.android.keyattestation.verifier.testing.TestUtils.readCertPath import com.android.keyattestation.verifier.testing.V3Extensions +import com.android.keyattestation.verifier.testing.toJson import com.android.keyattestation.verifier.testing.toKeyDescription import com.google.common.truth.Truth.assertThat @@ -200,6 +201,23 @@ class ExtensionTest { assertThat(KeyDescription.parseFrom(keyDescription.encodeToAsn1())).isEqualTo(keyDescription) } + @Test + fun keyDescriptionToJson_ignoresAreTagsOrdered() { + val authorizationList = AuthorizationList(areTagsOrdered = true) + val keyDescription = + KeyDescription( + attestationVersion = 1.toBigInteger(), + attestationSecurityLevel = SecurityLevel.SOFTWARE, + keyMintVersion = 1.toBigInteger(), + keyMintSecurityLevel = SecurityLevel.SOFTWARE, + attestationChallenge = ByteString.empty(), + uniqueId = ByteString.empty(), + softwareEnforced = authorizationList, + hardwareEnforced = authorizationList, + ) + assertThat(keyDescription.toJson()).doesNotContain("areTagsOrdered") + } + @Test fun keyDescriptionParseFrom_malformedAuthorizationListExtension_successAndLogs() { val logHook = FakeLogHook() diff --git a/testdata/sony-xperia10-iii/sdk33/TEE_EC.json b/testdata/sony-xperia10-iii/sdk33/TEE_EC.json index 442a7b6..ba5d61d 100644 --- a/testdata/sony-xperia10-iii/sdk33/TEE_EC.json +++ b/testdata/sony-xperia10-iii/sdk33/TEE_EC.json @@ -10,8 +10,7 @@ "attestationApplicationId": { "packages": [{ "name": "com.android.vending", "version": "85162330" }], "signatures": ["8P1sW0EPJcslw7UzRsiXL64w+O50Ed+RBICtay1g24M="] - }, - "areTagsOrdered": true + } }, "hardwareEnforced": { "purposes": ["2"], @@ -35,7 +34,6 @@ "attestationIdManufacturer": "Sony", "attestationIdModel": "SO-52B", "vendorPatchLevel": "20230701", - "bootPatchLevel": "20230701", - "areTagsOrdered": true + "bootPatchLevel": "20230701" } } diff --git a/testdata/tegu/sdk36/SB_EC_2026_ROOT.json b/testdata/tegu/sdk36/SB_EC_2026_ROOT.json index b7eb490..9a6d446 100644 --- a/testdata/tegu/sdk36/SB_EC_2026_ROOT.json +++ b/testdata/tegu/sdk36/SB_EC_2026_ROOT.json @@ -12,8 +12,7 @@ { "name": "com.google.android.attestation", "version": "0" } ], "signatures": ["EDk47kU35Z6O55L2VFBPuDRvxrNG0LvEQV/DOfz8jsE="] - }, - "areTagsOrdered": true + } }, "hardwareEnforced": { "purposes": ["2", "3"], @@ -32,7 +31,6 @@ "osVersion": "160000", "osPatchLevel": "202602", "vendorPatchLevel": "20260205", - "bootPatchLevel": "20260205", - "areTagsOrdered": true + "bootPatchLevel": "20260205" } } diff --git a/testdata/tegu/sdk36/TEE_EC_2026_ROOT.json b/testdata/tegu/sdk36/TEE_EC_2026_ROOT.json index 7c61ee9..e6122e3 100644 --- a/testdata/tegu/sdk36/TEE_EC_2026_ROOT.json +++ b/testdata/tegu/sdk36/TEE_EC_2026_ROOT.json @@ -14,8 +14,7 @@ ], "signatures": ["EDk47kU35Z6O55L2VFBPuDRvxrNG0LvEQV/DOfz8jsE="] }, - "moduleHash": "9LgYqeXS71yyjWDapgmLq8vfI/9ugHeO+C1+Qe9Ill4=", - "areTagsOrdered": true + "moduleHash": "9LgYqeXS71yyjWDapgmLq8vfI/9ugHeO+C1+Qe9Ill4=" }, "hardwareEnforced": { "purposes": ["2", "3"], @@ -34,9 +33,8 @@ "osVersion": "160000", "osPatchLevel": "202602", "vendorPatchLevel": "20260205", - "bootPatchLevel": "20260205", + "bootPatchLevel": "20260205" // Not all device fields are populated, as a pre-production test // device was used. This device was not fully provisioned. - "areTagsOrdered": true } } diff --git a/testdata/tegu/sdk37/TEE_MAX_USAGE_COUNT.json b/testdata/tegu/sdk37/TEE_MAX_USAGE_COUNT.json index 612e81d..2db7c9d 100644 --- a/testdata/tegu/sdk37/TEE_MAX_USAGE_COUNT.json +++ b/testdata/tegu/sdk37/TEE_MAX_USAGE_COUNT.json @@ -14,8 +14,7 @@ ], "signatures": ["EDk47kU35Z6O55L2VFBPuDRvxrNG0LvEQV/DOfz8jsE="] }, - "moduleHash": "al4AdvgYUvh6qnkfO7Wmn25Qtfs9I+pp4bbUBMm7N+4=", - "areTagsOrdered": true + "moduleHash": "al4AdvgYUvh6qnkfO7Wmn25Qtfs9I+pp4bbUBMm7N+4=" }, "hardwareEnforced": { "purposes": ["2", "3"], @@ -39,7 +38,6 @@ "attestationIdManufacturer": "Google", "attestationIdModel": "Pixel 9a", "vendorPatchLevel": "20260805", - "bootPatchLevel": "20260805", - "areTagsOrdered": true + "bootPatchLevel": "20260805" } } diff --git a/testdata/tegu/sdk37/TEE_TRUSTED_CONF.json b/testdata/tegu/sdk37/TEE_TRUSTED_CONF.json index 501a3d2..c357fad 100644 --- a/testdata/tegu/sdk37/TEE_TRUSTED_CONF.json +++ b/testdata/tegu/sdk37/TEE_TRUSTED_CONF.json @@ -13,8 +13,7 @@ ], "signatures": ["EDk47kU35Z6O55L2VFBPuDRvxrNG0LvEQV/DOfz8jsE="] }, - "moduleHash": "al4AdvgYUvh6qnkfO7Wmn25Qtfs9I+pp4bbUBMm7N+4=", - "areTagsOrdered": true + "moduleHash": "al4AdvgYUvh6qnkfO7Wmn25Qtfs9I+pp4bbUBMm7N+4=" }, "hardwareEnforced": { "purposes": ["2", "3"], @@ -39,7 +38,6 @@ "attestationIdManufacturer": "Google", "attestationIdModel": "Pixel 9a", "vendorPatchLevel": "20260805", - "bootPatchLevel": "20260805", - "areTagsOrdered": true + "bootPatchLevel": "20260805" } } diff --git a/testdata/tokay/sdk37/TEE_MLDSA_FACTORY.json b/testdata/tokay/sdk37/TEE_MLDSA_FACTORY.json index fe23f29..01c7eb5 100644 --- a/testdata/tokay/sdk37/TEE_MLDSA_FACTORY.json +++ b/testdata/tokay/sdk37/TEE_MLDSA_FACTORY.json @@ -8,9 +8,7 @@ "softwareEnforced": { "creationDateTime": "1777375215126", "attestationApplicationId": { - "packages": [ - {"name":"android.keystore.cts", "version":"37" } - ], + "packages": [{ "name": "android.keystore.cts", "version": "37" }], "signatures": ["bOzFDjSuMb+1Z4mG1tbTc2xXHe0vJFlSd5Ph8FTrDJs="] }, "moduleHash": "FaidWkxztCor58kSH+BtPV6/tFSP0MSgkePA7fFzTfw=" @@ -33,4 +31,4 @@ "vendorPatchLevel": "20260605", "bootPatchLevel": "20260605" } -} \ No newline at end of file +} diff --git a/testdata/tokay/sdk37/TEE_MLDSA_RKP.json b/testdata/tokay/sdk37/TEE_MLDSA_RKP.json index 08387da..dae8682 100644 --- a/testdata/tokay/sdk37/TEE_MLDSA_RKP.json +++ b/testdata/tokay/sdk37/TEE_MLDSA_RKP.json @@ -8,9 +8,7 @@ "softwareEnforced": { "creationDateTime": "1777384250243", "attestationApplicationId": { - "packages": [ - {"name":"android.keystore.cts", "version":"37" } - ], + "packages": [{ "name": "android.keystore.cts", "version": "37" }], "signatures": ["bOzFDjSuMb+1Z4mG1tbTc2xXHe0vJFlSd5Ph8FTrDJs="] }, "moduleHash": "FaidWkxztCor58kSH+BtPV6/tFSP0MSgkePA7fFzTfw=" @@ -33,4 +31,4 @@ "vendorPatchLevel": "20260605", "bootPatchLevel": "20260605" } -} \ No newline at end of file +} From 2462d0173133ebde6c2560fea46cf0a582e7b56f Mon Sep 17 00:00:00 2001 From: Suzanna Jiwani Date: Wed, 23 Sep 2026 13:11:03 -0700 Subject: [PATCH 38/48] Log for malformed booleans Log when encountering malformed bools in key attestation extension RootOfTrust. PiperOrigin-RevId: 986955350 --- src/main/kotlin/Extension.kt | 15 +++++++++------ src/test/kotlin/ExtensionTest.kt | 8 ++++++-- src/test/kotlin/VerifierTest.kt | 11 +++++++++++ 3 files changed, 26 insertions(+), 8 deletions(-) diff --git a/src/main/kotlin/Extension.kt b/src/main/kotlin/Extension.kt index 2f16779..0bbe37f 100644 --- a/src/main/kotlin/Extension.kt +++ b/src/main/kotlin/Extension.kt @@ -567,7 +567,7 @@ data class AuthorizationList( creationDateTime = converter.parseInt(KeyMintTag.CREATION_DATE_TIME), origin = converter.parse(KeyMintTag.ORIGIN) { it.toOrigin() }, rollbackResistant = if (objects.containsKey(KeyMintTag.ROLLBACK_RESISTANT)) true else null, - rootOfTrust = converter.parse(KeyMintTag.ROOT_OF_TRUST) { it.toRootOfTrust() }, + rootOfTrust = converter.parse(KeyMintTag.ROOT_OF_TRUST) { it.toRootOfTrust(logFn) }, osVersion = converter.parseInt(KeyMintTag.OS_VERSION), osPatchLevel = converter.parsePatchLevel(KeyMintTag.OS_PATCH_LEVEL, "OS"), attestationApplicationId = @@ -737,12 +737,12 @@ data class RootOfTrust( .let { DERSequence(it.toTypedArray()) } internal companion object { - fun from(rootOfTrust: ASN1Sequence): RootOfTrust { + fun from(rootOfTrust: ASN1Sequence, logFn: (String) -> Unit = { _ -> }): RootOfTrust { require(rootOfTrust.size() == 3 || rootOfTrust.size() == 4) val verifiedBootState = rootOfTrust.getObjectAt(2).toEnumerated() return RootOfTrust( verifiedBootKey = rootOfTrust.getObjectAt(0).toByteString(), - deviceLocked = rootOfTrust.getObjectAt(1).toBoolean(), + deviceLocked = rootOfTrust.getObjectAt(1).toBoolean(logFn), VerifiedBootState.from(verifiedBootState), verifiedBootHash = if (rootOfTrust.size() > 3) rootOfTrust.getObjectAt(3).toByteString() else null, @@ -796,10 +796,13 @@ private fun ASN1Encodable.toAuthorizationList( } @RequiresApi(24) -private fun ASN1Encodable.toBoolean(): Boolean { +private fun ASN1Encodable.toBoolean(logFn: (String) -> Unit = { _ -> }): Boolean { if (this !is ASN1Boolean) { throw ExtensionParsingException("Must be an ASN1Boolean, was ${this::class.simpleName}") } + if (this !== ASN1Boolean.TRUE && this !== ASN1Boolean.FALSE) { + logFn("Non-DER encoded boolean in RootOfTrust.deviceLocked: ${this.encoded.last()}") + } return this.isTrue } @@ -837,11 +840,11 @@ private fun ASN1Encodable.toPatchLevel( ): PatchLevel? = PatchLevel.from(this, partitionName, logFn) @RequiresApi(24) -private fun ASN1Encodable.toRootOfTrust(): RootOfTrust { +private fun ASN1Encodable.toRootOfTrust(logFn: (String) -> Unit = { _ -> }): RootOfTrust { if (this !is ASN1Sequence) { throw ExtensionParsingException("Object must be an ASN1Sequence, was ${this::class.simpleName}") } - return RootOfTrust.from(this) + return RootOfTrust.from(this, logFn) } @RequiresApi(24) diff --git a/src/test/kotlin/ExtensionTest.kt b/src/test/kotlin/ExtensionTest.kt index 64279f9..3ac1ebe 100644 --- a/src/test/kotlin/ExtensionTest.kt +++ b/src/test/kotlin/ExtensionTest.kt @@ -101,15 +101,19 @@ class ExtensionTest { } @Test - fun parseFrom_malformedRotDeviceLocked_successfullyParsed() { + fun parseFrom_malformedRotDeviceLocked_successfullyParsedAndLogs() { + val logHook = FakeLogHook() val keyDescription = KeyDescription.parseFrom( testData .resolve("invalid/malformed_rot_device_locked.pem") .inputStream() - .asX509Certificate() + .asX509Certificate(), + logFn = logHook.fakeVerifyRequestLog::logInfoMessage, ) assertThat(keyDescription?.hardwareEnforced?.rootOfTrust?.deviceLocked).isTrue() + assertThat(logHook.fakeVerifyRequestLog.infoMessages) + .contains("Non-DER encoded boolean in RootOfTrust.deviceLocked: 1") } @Test diff --git a/src/test/kotlin/VerifierTest.kt b/src/test/kotlin/VerifierTest.kt index 832a3eb..59c3b97 100644 --- a/src/test/kotlin/VerifierTest.kt +++ b/src/test/kotlin/VerifierTest.kt @@ -422,6 +422,17 @@ class VerifierTest { assertThat(logHook.fakeVerifyRequestLog.infoMessages).isNotEmpty() } + @Test + fun verifyAsync_malformedDeviceLocked_logsInfo(): Unit = runBlocking { + val logHook = FakeLogHook() + val chain = readCertList("invalid/malformed_rot_device_locked.pem") + val result = + assertIs(verifier.verifyAsync(this, chain, log = logHook).await()) + assertThat(result.deviceLocked).isTrue() + assertThat(logHook.fakeVerifyRequestLog.infoMessages) + .contains("Non-DER encoded boolean in RootOfTrust.deviceLocked: 1") + } + @Test fun verifyAsync_longDelay_successfullyAwaitsChallengeCheck(): Unit = runBlocking { val chain = readCertList("blueline/sdk28/TEE_EC_NONE.pem") From d5b000175672af346284f2156046bb8d28950a9f Mon Sep 17 00:00:00 2001 From: Carmen Hanish Date: Thu, 24 Sep 2026 03:49:51 -0700 Subject: [PATCH 39/48] Add AttestationApplicationId checking. PiperOrigin-RevId: 987391156 --- 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() ) From 0cb2c37bd19c500ccdc9c26e2556108077a747cd Mon Sep 17 00:00:00 2001 From: Android HW Trust Team Date: Thu, 24 Sep 2026 04:46:25 -0700 Subject: [PATCH 40/48] Add AttestationApplicationId checking. PiperOrigin-RevId: 987415741 --- 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, 42 insertions(+), 486 deletions(-) diff --git a/README.md b/README.md index 4416767..51b081c 100644 --- a/README.md +++ b/README.md @@ -10,17 +10,11 @@ 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 - ConstraintConfig(attestationApplicationId = - AttestationApplicationIdConstraint.ALLOW_UNKNOWN_PACKAGE(expectedAppId)), - + { Instant.now() } // Time source ) // Verify an attestation certificate chain -val result = verifier.verify( - certificateChain, - challengeChecker, -) +val result = verifier.verify(certificateChain) // Handle the verification result when (result) { @@ -35,47 +29,21 @@ when (result) { is VerificationResult.PathValidationFailure -> // Handle validation failure is VerificationResult.ChainParsingFailure -> // Handle parsing failure is VerificationResult.ExtensionParsingFailure -> // Handle extension parsing issues - is VerificationResult.ConstraintViolation -> // Handle constraint violations + is VerificationResult.ExtensionConstraintViolation -> // Handle constraint violations } ``` -### 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 +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 ```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 @@ -108,17 +76,6 @@ 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 9f90d1d..4cfa543 100644 --- a/src/main/kotlin/ConstraintConfig.kt +++ b/src/main/kotlin/ConstraintConfig.kt @@ -21,7 +21,6 @@ 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? @@ -51,23 +50,13 @@ 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(), ) { - /** - * 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 = + fun getConstraints() = ImmutableList.builder() .add( keyOrigin @@ -92,10 +81,6 @@ 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 @@ -114,10 +99,6 @@ class ConstraintConfigBuilder() { this.rootOfTrust = constraint() } - fun attestationApplicationId(constraint: () -> AttestationApplicationIdConstraint) { - this.attestationApplicationId = constraint() - } - fun additionalConstraint(constraint: () -> Constraint) { additionalConstraints.add(constraint()) } @@ -129,7 +110,6 @@ class ConstraintConfigBuilder() { fun build(): ConstraintConfig = ConstraintConfig( allowSoftwareRoot, - attestationApplicationId, keyOrigin, securityLevel, rootOfTrust, @@ -367,88 +347,3 @@ 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 6d50f67..0bbe37f 100644 --- a/src/main/kotlin/Extension.kt +++ b/src/main/kotlin/Extension.kt @@ -657,26 +657,6 @@ 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, @@ -721,10 +701,6 @@ 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 1986655..ae3f488 100644 --- a/src/main/kotlin/Verifier.kt +++ b/src/main/kotlin/Verifier.kt @@ -151,8 +151,6 @@ 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 { @@ -183,16 +181,13 @@ 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, additionalAttestationApplicationId, requestLog) - } + runBlocking { internalVerify(certPath, challengeChecker, requestLog) } } catch (e: CertificateException) { requestLog?.logInputChain(chain.map { it.getEncoded().toByteString() }) VerificationResult.ChainParsingFailure(e) @@ -208,8 +203,6 @@ 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]. */ @@ -218,7 +211,6 @@ constructor( coroutineScope: CoroutineScope, chain: List, challengeChecker: ChallengeChecker? = null, - additionalAttestationApplicationId: AttestationApplicationId? = null, log: LogHook? = null, ): ListenableFuture { val immutableChain = ImmutableList.copyOf(chain) @@ -227,7 +219,7 @@ constructor( val result = try { val certPath = KeyAttestationCertPath(immutableChain) - internalVerify(certPath, challengeChecker, additionalAttestationApplicationId, requestLog) + internalVerify(certPath, challengeChecker, requestLog) } catch (e: CertificateException) { requestLog?.logInputChain(immutableChain.map { it.getEncoded().toByteString() }) VerificationResult.ChainParsingFailure(e) @@ -241,7 +233,6 @@ 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() }) @@ -314,7 +305,7 @@ constructor( } } - for (constraint in constraintConfig.getGenericConstraints()) { + for (constraint in constraintConfig.getConstraints()) { val result = constraint.check(keyDescription, certPath) when (result) { is Constraint.Satisfied -> {} @@ -324,21 +315,6 @@ 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) @@ -357,15 +333,3 @@ 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 0b922c9..de88da0 100644 --- a/src/test/kotlin/ConstraintConfigTest.kt +++ b/src/test/kotlin/ConstraintConfigTest.kt @@ -19,8 +19,6 @@ 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 @@ -31,37 +29,21 @@ 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(), - osVersion = MAX_OS_VERSION, - ) + AuthorizationList(purposes = setOf(1.toBigInteger()), algorithms = 1.toBigInteger()) fun createTestKeyDescription( attestationSecurityLevel: SecurityLevel, keyMintSecurityLevel: SecurityLevel, ) = KeyDescription( - attestationVersion = 400.toBigInteger(), + attestationVersion = 1.toBigInteger(), attestationSecurityLevel = attestationSecurityLevel, - keyMintVersion = 400.toBigInteger(), + keyMintVersion = 1.toBigInteger(), keyMintSecurityLevel = keyMintSecurityLevel, attestationChallenge = ByteString.empty(), uniqueId = ByteString.empty(), - softwareEnforced = AuthorizationList(attestationApplicationId = TEST_APP_ID), + softwareEnforced = authorizationList, hardwareEnforced = authorizationList, ) } @@ -450,193 +432,6 @@ class ConstraintConfigTest { fun constraintConfig_provisioningMethod_configuredViaAdditionalConstraint() { val config = constraintConfig { additionalConstraint { ProvisioningMethodConstraint.FACTORY } } assertThat(config.additionalConstraints).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") + assertThat(config.getConstraints()).contains(ProvisioningMethodConstraint.FACTORY) } } diff --git a/src/test/kotlin/VerifierTest.kt b/src/test/kotlin/VerifierTest.kt index 39fd223..59c3b97 100644 --- a/src/test/kotlin/VerifierTest.kt +++ b/src/test/kotlin/VerifierTest.kt @@ -43,7 +43,6 @@ 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 @@ -99,13 +98,11 @@ class VerifierTest { { prodAnchors + SOFTWARE_ROOTS.map { TrustAnchor(it, null) } }, { setOf() }, { timestamp }, - json.softwareEnforced.attestationApplicationId, - constraintConfig { - allowSoftwareRoot = true - securityLevel { IgnoredConstraint } - rootOfTrust { IgnoredConstraint } - attestationApplicationId { AttestationApplicationIdConstraint.STRICT } - }, + ConstraintConfig( + allowSoftwareRoot = true, + securityLevel = IgnoredConstraint, + rootOfTrust = IgnoredConstraint, + ), ) val chain = readCertList("${subpath}.pem") val result = assertIs(verifier.verify(chain)) @@ -176,26 +173,6 @@ 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 = @@ -253,8 +230,7 @@ class VerifierTest { { prodAnchors + TrustAnchor(Certs.root, null) }, { setOf() }, { FakeCalendar.DEFAULT.now() }, - constraintConfig = - constraintConfig { additionalConstraint { SecurityLevelConstraint.MATCHES_CERTIFICATE } }, + constraintConfig { additionalConstraint { SecurityLevelConstraint.MATCHES_CERTIFICATE } }, ) val result = assertIs( @@ -274,8 +250,7 @@ class VerifierTest { { prodAnchors + TrustAnchor(Certs.root, null) }, { setOf() }, { FakeCalendar.DEFAULT.now() }, - constraintConfig = - constraintConfig { additionalConstraint { SecurityLevelConstraint.MATCHES_CERTIFICATE } }, + constraintConfig { additionalConstraint { SecurityLevelConstraint.MATCHES_CERTIFICATE } }, ) val result = assertIs( @@ -296,7 +271,7 @@ class VerifierTest { { prodAnchors + TrustAnchor(Certs.root, null) }, { setOf() }, { FakeCalendar.DEFAULT.now() }, - constraintConfig = constraintConfig { securityLevel { IgnoredConstraint } }, + constraintConfig { securityLevel { IgnoredConstraint } }, ) val result = assertIs(verifier.verify(CertLists.mismatchedSecurityLevels)) @@ -310,8 +285,7 @@ class VerifierTest { { prodAnchors + TrustAnchor(Certs.root, null) }, { setOf() }, { FakeCalendar.DEFAULT.now() }, - constraintConfig = - constraintConfig { additionalConstraint { ProvisioningMethodConstraint.REMOTE } }, + constraintConfig { additionalConstraint { ProvisioningMethodConstraint.REMOTE } }, ) val result = assertIs(verifier.verify(CertLists.validFactoryProvisioned)) assertThat(result.constraintLabel).isEqualTo("Provisioning method") @@ -328,8 +302,7 @@ class VerifierTest { { prodAnchors + TrustAnchor(Certs.root, null) }, { setOf() }, { FakeCalendar.DEFAULT.now() }, - constraintConfig = - constraintConfig { additionalConstraint { ProvisioningMethodConstraint.FACTORY } }, + constraintConfig { additionalConstraint { ProvisioningMethodConstraint.FACTORY } }, ) assertIs(verifier.verify(CertLists.validFactoryProvisioned)) } @@ -341,8 +314,7 @@ class VerifierTest { { prodAnchors + TrustAnchor(Certs.root, null) }, { setOf() }, { FakeCalendar.DEFAULT.now() }, - constraintConfig = - constraintConfig { additionalConstraint { ProvisioningMethodConstraint.FACTORY } }, + constraintConfig { additionalConstraint { ProvisioningMethodConstraint.FACTORY } }, ) val result = assertIs(verifier.verify(CertLists.validRemotelyProvisioned)) assertThat(result.constraintLabel).isEqualTo("Provisioning method") @@ -359,8 +331,7 @@ class VerifierTest { { prodAnchors + TrustAnchor(Certs.root, null) }, { setOf() }, { FakeCalendar.DEFAULT.now() }, - constraintConfig = - constraintConfig { additionalConstraint { ProvisioningMethodConstraint.REMOTE } }, + constraintConfig { additionalConstraint { ProvisioningMethodConstraint.REMOTE } }, ) assertIs(verifier.verify(CertLists.validRemotelyProvisioned)) } @@ -372,12 +343,11 @@ class VerifierTest { { prodAnchors + TrustAnchor(Certs.root, null) }, { setOf() }, { FakeCalendar.DEFAULT.now() }, - constraintConfig = - constraintConfig { - keyOrigin { - AttributeConstraint.STRICT("Test", Origin.IMPORTED) { it.hardwareEnforced.origin } - } - }, + constraintConfig { + keyOrigin { + AttributeConstraint.STRICT("Test", Origin.IMPORTED) { it.hardwareEnforced.origin } + } + }, ) assertIs(verifier.verify(CertLists.importedOrigin)) } @@ -389,7 +359,7 @@ class VerifierTest { { prodAnchors + TrustAnchor(Certs.root, null) }, { setOf() }, { FakeCalendar.DEFAULT.now() }, - constraintConfig = constraintConfig { rootOfTrust { IgnoredConstraint } }, + constraintConfig { rootOfTrust { IgnoredConstraint } }, ) assertIs(verifier.verify(CertLists.missingRootOfTrust)) } @@ -401,11 +371,10 @@ class VerifierTest { { prodAnchors + TrustAnchor(Certs.root, null) }, { setOf() }, { FakeCalendar.DEFAULT.now() }, - constraintConfig = - constraintConfig { - additionalConstraint { TagOrderConstraint.STRICT } - additionalConstraint { IgnoredConstraint } - }, + constraintConfig { + additionalConstraint { TagOrderConstraint.STRICT } + additionalConstraint { IgnoredConstraint } + }, ) val result = assertIs(verifier.verify(CertLists.unorderedTags)) assertThat(result.constraintLabel).isEqualTo("Tag order") @@ -421,7 +390,7 @@ class VerifierTest { this, CertLists.wrongTrustAnchor, ChallengeMatcher(ByteString.copyFromUtf8("challenge")), - log = logHook, + logHook, ) .await() ) From 20e2d11dce746fe2ceec480e6f4d2c36ded32268 Mon Sep 17 00:00:00 2001 From: Carmen Hanish Date: Thu, 24 Sep 2026 08:05:04 -0700 Subject: [PATCH 41/48] Update dependencies for github workflows. PiperOrigin-RevId: 987512226 --- .github/workflows/gradle.yml | 6 +++--- .github/workflows/release.yml | 8 ++++---- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/.github/workflows/gradle.yml b/.github/workflows/gradle.yml index c437039..84545f6 100644 --- a/.github/workflows/gradle.yml +++ b/.github/workflows/gradle.yml @@ -24,12 +24,12 @@ jobs: build: runs-on: ubuntu-latest steps: - - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - - uses: actions/setup-java@7a6d8a8234af8eb26422e24e3006232cccaa061b # v4.6.0 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + - uses: actions/setup-java@de7274f081f381c8f8158605e0321c36c376e2e6 # v6.0.1 with: distribution: "temurin" java-version: 21 - - uses: gradle/actions/setup-gradle@ed408507eac070d1f99cc633dbcf757c94c7933a # v4.4.3 + - uses: gradle/actions/setup-gradle@9c971963bec38e04b3d30dcc455b5382be2fdbfb # v6.3.0 - name: Build run: ./gradlew build - name: Test diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 91f7256..bcb2fcf 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -26,12 +26,12 @@ jobs: build-maven-repo: runs-on: ubuntu-latest steps: - - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - - uses: actions/setup-java@7a6d8a8234af8eb26422e24e3006232cccaa061b # v4.6.0 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + - uses: actions/setup-java@de7274f081f381c8f8158605e0321c36c376e2e6 # v6.0.1 with: distribution: "temurin" java-version: 21 - - uses: gradle/actions/setup-gradle@ed408507eac070d1f99cc633dbcf757c94c7933a # v4.4.3 + - uses: gradle/actions/setup-gradle@9c971963bec38e04b3d30dcc455b5382be2fdbfb # v6.3.0 - name: "Set environment variables" run: | echo "ORG_GRADLE_PROJECT_KeyAttestationMavenRepo=$GITHUB_WORKSPACE/keyattestation_m2repo" >> "$GITHUB_ENV" @@ -41,7 +41,7 @@ jobs: run: ./gradlew -PkeyAttestationReleaseVersion="$REF_NAME" publishAllPublicationsToLocalDirRepository - name: Upload - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: name: keyattestation_m2repo path: keyattestation_m2repo From 411188a4d7b798e3c9c0cefb7c37322e92f0ebfe Mon Sep 17 00:00:00 2001 From: Carmen Hanish Date: Fri, 25 Sep 2026 03:34:56 -0700 Subject: [PATCH 42/48] 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() ) From a508aefbafb5d614e7e814eb0ff2386a702d7812 Mon Sep 17 00:00:00 2001 From: Suzanna Jiwani Date: Fri, 25 Sep 2026 12:06:30 -0700 Subject: [PATCH 43/48] Additional malformed extension testing Assert unparseableExtension fails validation while invalidKeySize succeeds. PiperOrigin-RevId: 988417393 --- src/main/kotlin/testing/Certs.kt | 13 +++++++++++++ src/test/kotlin/VerifierTest.kt | 27 ++++++++++++++++++++++++--- 2 files changed, 37 insertions(+), 3 deletions(-) diff --git a/src/main/kotlin/testing/Certs.kt b/src/main/kotlin/testing/Certs.kt index 0292fef..ffb9e4a 100644 --- a/src/main/kotlin/testing/Certs.kt +++ b/src/main/kotlin/testing/Certs.kt @@ -229,6 +229,19 @@ object CertLists { ) } + /* A chain that has a malformed key size (OCTET STRING instead of INTEGER). */ + val invalidKeySize by lazy { + listOf( + generateValidLeafCertWithAppendedTag( + KeyMintTag.KEY_SIZE.value, + DEROctetString("not an int".toByteArray()), + ), + certFactory.factoryAttestation, + certFactory.factoryIntermediate, + certFactory.root, + ) + } + /* A chain where the key tags are out of order. */ val unorderedTags by lazy { listOf( diff --git a/src/test/kotlin/VerifierTest.kt b/src/test/kotlin/VerifierTest.kt index 39fd223..8bc506e 100644 --- a/src/test/kotlin/VerifierTest.kt +++ b/src/test/kotlin/VerifierTest.kt @@ -217,6 +217,12 @@ class VerifierTest { assertThat(result.cause.reason).isEqualTo(KeyAttestationReason.UNKNOWN_TAG_NUMBER) } + @Test + fun unparseableExtension_extensionParsingFailure() { + val result = assertIs(verifier.verify(CertLists.unparseableExtension)) + assertThat(result.cause.reason).isNull() + } + @Test fun targetMissingAttestationExtension_givesTargetMissingAttestationExtensionReason() { val result = assertIs(verifier.verify(CertLists.missingExtension)) @@ -413,7 +419,7 @@ class VerifierTest { } @Test - fun verifyAsync_failure_inputChainLogged() = runBlocking { + fun verifyAsync_failure_inputChainLogged(): Unit = runBlocking { val logHook = FakeLogHook() assertIs( verifier @@ -430,7 +436,7 @@ class VerifierTest { } @Test - fun verifyAsync_success_keyDescriptionLogged() = runBlocking { + fun verifyAsync_success_keyDescriptionLogged(): Unit = runBlocking { val logHook = FakeLogHook() val chain = readCertList("blueline/sdk28/TEE_EC_NONE.pem") assertIs(verifier.verifyAsync(this, chain, log = logHook).await()) @@ -439,7 +445,7 @@ class VerifierTest { } @Test - fun verifyAsync_malformedPatchLevel_logsInfo() = runBlocking { + fun verifyAsync_malformedPatchLevel_logsInfo(): Unit = runBlocking { val verifierWithTestRoot = Verifier( { setOf(TrustAnchor(Certs.root, null)) }, @@ -464,6 +470,21 @@ class VerifierTest { .contains("Non-DER encoded boolean in RootOfTrust.deviceLocked: 1") } + @Test + fun verifyAsync_invalidKeySize_logsInfo(): Unit = runBlocking { + val verifierWithTestRoot = + Verifier( + { setOf(TrustAnchor(Certs.root, null)) }, + { setOf() }, + { FakeCalendar.DEFAULT.now() }, + ) + val logHook = FakeLogHook() + assertIs( + verifierWithTestRoot.verifyAsync(this, CertLists.invalidKeySize, log = logHook).await() + ) + assertThat(logHook.fakeVerifyRequestLog.infoMessages).isNotEmpty() + } + @Test fun verifyAsync_longDelay_successfullyAwaitsChallengeCheck(): Unit = runBlocking { val chain = readCertList("blueline/sdk28/TEE_EC_NONE.pem") From 55c35040a1b5b72e6d63bfb150c5c68a175c1462 Mon Sep 17 00:00:00 2001 From: Carmen Hanish Date: Mon, 28 Sep 2026 03:26:16 -0700 Subject: [PATCH 44/48] Add standalone Gradle Kotlin consumer test for m2repo PiperOrigin-RevId: 989527382 --- .github/workflows/release.yml | 4 +++ testapp/build.gradle.kts | 52 +++++++++++++++++++++++++++ testapp/settings.gradle.kts | 19 ++++++++++ testapp/src/test/kotlin/M2RepoTest.kt | 51 ++++++++++++++++++++++++++ 4 files changed, 126 insertions(+) create mode 100644 testapp/build.gradle.kts create mode 100644 testapp/settings.gradle.kts create mode 100644 testapp/src/test/kotlin/M2RepoTest.kt diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index bcb2fcf..885724e 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -39,6 +39,10 @@ jobs: env: REF_NAME: ${{ github.ref_name }} run: ./gradlew -PkeyAttestationReleaseVersion="$REF_NAME" publishAllPublicationsToLocalDirRepository + - name: Test Maven repo + env: + REF_NAME: ${{ github.ref_name }} + run: ./gradlew -p testapp -PkeyAttestationReleaseVersion="$REF_NAME" test - name: Upload uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 diff --git a/testapp/build.gradle.kts b/testapp/build.gradle.kts new file mode 100644 index 0000000..1f9cf28 --- /dev/null +++ b/testapp/build.gradle.kts @@ -0,0 +1,52 @@ +/* + * Copyright 2026 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +plugins { + id("com.adarshr.test-logger") version "4.0.0" + id("org.jetbrains.kotlin.jvm") version "2.2.0" +} + +repositories { + val repoDir = + findProperty("KeyAttestationMavenRepo") as? String + ?: layout.projectDirectory.dir("../build/keyattestation_m2repo").asFile.absolutePath + maven { url = uri(repoDir) } + mavenCentral() + google() +} + +val keyAttestationVersion = + (findProperty("keyAttestationReleaseVersion") as? String)?.removePrefix("v") ?: "+" + +dependencies { + implementation("com.android.keyattestation:keyattestation:$keyAttestationVersion") + + testImplementation(kotlin("test")) + testImplementation("com.google.truth:truth:1.4.4") + + // Required to run JUnit 4 tests. + testRuntimeOnly("org.junit.vintage:junit-vintage-engine") +} + +java { toolchain { languageVersion = JavaLanguageVersion.of(21) } } + +tasks { + test { + workingDir = layout.projectDirectory.dir("..").asFile + useJUnitPlatform() + testLogging { exceptionFormat = org.gradle.api.tasks.testing.logging.TestExceptionFormat.FULL } + } +} diff --git a/testapp/settings.gradle.kts b/testapp/settings.gradle.kts new file mode 100644 index 0000000..531c30d --- /dev/null +++ b/testapp/settings.gradle.kts @@ -0,0 +1,19 @@ +/* + * Copyright 2026 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +plugins { id("org.gradle.toolchains.foojay-resolver-convention") version "0.8.0" } + +rootProject.name = "keyattestation-testapp" diff --git a/testapp/src/test/kotlin/M2RepoTest.kt b/testapp/src/test/kotlin/M2RepoTest.kt new file mode 100644 index 0000000..79b3969 --- /dev/null +++ b/testapp/src/test/kotlin/M2RepoTest.kt @@ -0,0 +1,51 @@ +/* + * Copyright 2026 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.keyattestation.verifier.testapp + +import com.android.keyattestation.verifier.GoogleTrustAnchors +import com.android.keyattestation.verifier.SecurityLevel +import com.android.keyattestation.verifier.VerificationResult +import com.android.keyattestation.verifier.VerifiedBootState +import com.android.keyattestation.verifier.Verifier +import com.android.keyattestation.verifier.testing.TestUtils +import com.google.common.truth.Truth.assertThat +import kotlin.test.assertIs +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.JUnit4 + +@RunWith(JUnit4::class) +class M2RepoTest { + @Test + fun verify_validChainFromM2Repo_returnsSuccess() { + val certs = TestUtils.readCertList("tegu/sdk36/TEE_EC_2026_ROOT.pem") + val notBefore = certs[1].notBefore.toInstant() + val notAfter = certs[1].notAfter.toInstant() + val validTime = notBefore.plusMillis((notAfter.toEpochMilli() - notBefore.toEpochMilli()) / 2) + + val verifier = + Verifier( + trustAnchorsSource = GoogleTrustAnchors, + revokedSerialsSource = { emptySet() }, + instantSource = { validTime }, + ) + val result = assertIs(verifier.verify(certs)) + + assertThat(result.securityLevel).isEqualTo(SecurityLevel.TRUSTED_ENVIRONMENT) + assertThat(result.verifiedBootState).isEqualTo(VerifiedBootState.VERIFIED) + } +} From 3f550f94a9e3c010dda7cdcad8bca764cd76d4f4 Mon Sep 17 00:00:00 2001 From: Carmen Hanish Date: Thu, 8 Oct 2026 08:25:53 -0700 Subject: [PATCH 45/48] Add license to generated POM file. PiperOrigin-RevId: 995851424 --- build.gradle.kts | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/build.gradle.kts b/build.gradle.kts index 16b8f6d..215cbd8 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -117,9 +117,16 @@ publishing { version = (findProperty("keyAttestationReleaseVersion") as? String)?.removePrefix("v") ?: "0.1-SNAPSHOT" + pom { + licenses { + license { + name.set("The Apache Software License, Version 2.0") + url.set("http://www.apache.org/licenses/LICENSE-2.0.txt") + } + } + } } } - repositories { maven { name = "localDir" From e1ca91a3645acacc9c6f110039c69bd7ec089e87 Mon Sep 17 00:00:00 2001 From: Liam Miller-Cushon Date: Mon, 12 Oct 2026 03:17:59 -0700 Subject: [PATCH 46/48] Automated Code Change PiperOrigin-RevId: 997803738 --- src/main/kotlin/X509CertificateExt.kt | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/main/kotlin/X509CertificateExt.kt b/src/main/kotlin/X509CertificateExt.kt index ac3cf17..333717e 100644 --- a/src/main/kotlin/X509CertificateExt.kt +++ b/src/main/kotlin/X509CertificateExt.kt @@ -25,7 +25,7 @@ import java.security.cert.X509Certificate private val certificateFactory = CertificateFactory.getInstance("X.509") /** Returns an [X509Certificate] from a [String]. */ -fun String.asX509Certificate() = this.byteInputStream().asX509Certificate() +fun String.asX509Certificate(): X509Certificate = this.byteInputStream().asX509Certificate() @Throws(CertificateException::class) fun InputStream.asX509Certificate() = @@ -37,7 +37,8 @@ fun InputStream.asX509Certificate() = * @return the DER-encoded OCTET string containing the KeyDescription sequence or null if the * extension is not present in the certificate. */ -@RequiresApi(24) fun X509Certificate.keyDescription() = KeyDescription.parseFrom(this) +@RequiresApi(24) +fun X509Certificate.keyDescription(): KeyDescription? = KeyDescription.parseFrom(this) /** * Returns the Android Key Attestation extension for provisioning info. @@ -45,5 +46,6 @@ fun InputStream.asX509Certificate() = * @return the DER-encoded OCTET string containing the ProvisioningInfo sequence or null if the * extension is not present in the certificate. */ -fun X509Certificate.provisioningInfo(inputLimits: InputLimits = InputLimits()) = - ProvisioningInfoMap.parseFrom(this, inputLimits) +fun X509Certificate.provisioningInfo( + inputLimits: InputLimits = InputLimits() +): ProvisioningInfoMap? = ProvisioningInfoMap.parseFrom(this, inputLimits) From 621149c4ebaeeea844d6f43098687c73c4d370a3 Mon Sep 17 00:00:00 2001 From: Carmen Hanish Date: Tue, 13 Oct 2026 03:05:28 -0700 Subject: [PATCH 47/48] Make types in public method signatures api(). PiperOrigin-RevId: 998506333 --- RELEASE.md | 11 +++++++++-- build.gradle.kts | 13 +++++++------ 2 files changed, 16 insertions(+), 8 deletions(-) diff --git a/RELEASE.md b/RELEASE.md index 9ddea70..13631f9 100644 --- a/RELEASE.md +++ b/RELEASE.md @@ -1,7 +1,14 @@ # Release Notes -## v0.1 -A test release. +## Pre v1 releases + +### Before v0.4 + +Test releases, may have minor issues. + +### v0.4 + +Test release. It is a working version of the library at a given point in time, before any BETA/1.0 release. diff --git a/build.gradle.kts b/build.gradle.kts index 215cbd8..9f0ac47 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -17,6 +17,7 @@ plugins { id("com.adarshr.test-logger") version "4.0.0" id("org.jetbrains.kotlin.jvm") version "2.2.0" + `java-library` `maven-publish` } @@ -29,14 +30,14 @@ dependencies { implementation("androidx.annotation:annotation:1.9.1") implementation("co.nstant.in:cbor:0.9") implementation("com.google.code.gson:gson:2.11.0") - implementation("com.google.errorprone:error_prone_annotations:2.41.0") - implementation("com.google.protobuf:protobuf-javalite:4.28.3") + api("com.google.errorprone:error_prone_annotations:2.41.0") + api("com.google.protobuf:protobuf-javalite:4.28.3") implementation("com.google.protobuf:protobuf-kotlin-lite:4.28.3") - implementation("org.bouncycastle:bcpkix-jdk18on:1.78.1") - implementation("org.jetbrains.kotlin:kotlin-stdlib:2.2.0") - implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.10.2") + api("org.bouncycastle:bcpkix-jdk18on:1.78.1") + api("org.jetbrains.kotlin:kotlin-stdlib:2.2.0") + api("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.10.2") implementation("org.jetbrains.kotlinx:kotlinx-coroutines-guava:1.10.2") - implementation("com.google.guava:guava:33.5.0-jre") + api("com.google.guava:guava:33.5.0-jre") testImplementation(kotlin("test")) testImplementation("com.google.testparameterinjector:test-parameter-injector:1.18") From 9bee3f97b0a684cd3ede0f295b12d284fb0b5138 Mon Sep 17 00:00:00 2001 From: Lazar Bulic Date: Thu, 5 Nov 2026 18:43:53 +0100 Subject: [PATCH 48/48] Update org.bouncycastle:bcpkix-jdk18on to 1.85 --- build.gradle.kts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle.kts b/build.gradle.kts index f5a051c..b2dc0e7 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -34,7 +34,7 @@ dependencies { api("com.google.errorprone:error_prone_annotations:2.41.0") api("com.google.protobuf:protobuf-javalite:4.28.3") implementation("com.google.protobuf:protobuf-kotlin-lite:4.28.3") - api("org.bouncycastle:bcpkix-jdk18on:1.78.1") + api("org.bouncycastle:bcpkix-jdk18on:1.85") api("org.jetbrains.kotlin:kotlin-stdlib:2.2.0") api("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.10.2") implementation("org.jetbrains.kotlinx:kotlinx-coroutines-guava:1.10.2")