diff --git a/.github/workflows/gradle.yml b/.github/workflows/gradle.yml index f8cc599..84545f6 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@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + - uses: actions/setup-java@de7274f081f381c8f8158605e0321c36c376e2e6 # v6.0.1 with: distribution: "temurin" java-version: 21 - - uses: gradle/actions/setup-gradle@v4 + - 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 new file mode 100644 index 0000000..885724e --- /dev/null +++ b/.github/workflows/release.yml @@ -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. + +name: Release + +on: + push: + tags: + - 'v*' + +permissions: + contents: write + +jobs: + build-maven-repo: + runs-on: ubuntu-latest + steps: + - 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@9c971963bec38e04b3d30dcc455b5382be2fdbfb # v6.3.0 + - 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: 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 + with: + name: keyattestation_m2repo + path: keyattestation_m2repo diff --git a/README.md b/README.md index 506a6f5..4416767 100644 --- a/README.md +++ b/README.md @@ -6,15 +6,21 @@ 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 + 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("challenge123") - -// Verify an attestation certificate chain with the checker -val result = verifier.verify(certificateChain, challengeChecker) +val challengeChecker = ChallengeMatcher(ByteString.copyFromUtf8("challenge123")) ``` If there are multiple checks to perform on the challenge, use a @@ -57,9 +89,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) @@ -71,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 @@ -95,7 +143,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 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. diff --git a/RELEASE.md b/RELEASE.md new file mode 100644 index 0000000..13631f9 --- /dev/null +++ b/RELEASE.md @@ -0,0 +1,18 @@ +# Release Notes + +## 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. + +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 diff --git a/build.gradle.kts b/build.gradle.kts index c80c53f..b2dc0e7 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -17,7 +17,9 @@ plugins { id("com.adarshr.test-logger") version "4.0.0" id("org.jetbrains.kotlin.jvm") version "2.2.0" - id("com.gradleup.shadow") version "9.3.2" + id("com.gradleup.shadow") version "9.3.2" + `java-library` + `maven-publish` } repositories { @@ -29,14 +31,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.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") - 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") @@ -46,7 +48,10 @@ dependencies { testRuntimeOnly("org.junit.vintage:junit-vintage-engine") } -java { toolchain { languageVersion = JavaLanguageVersion.of(21) } } +java { + toolchain { languageVersion = JavaLanguageVersion.of(21) } + withSourcesJar() +} tasks { test { @@ -59,20 +64,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 @@ -93,16 +97,49 @@ 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" + 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" + val repoDir = + findProperty("KeyAttestationMavenRepo") as? String + ?: rootProject.layout.buildDirectory + .dir("keyattestation_m2repo") + .get() + .asFile + .absolutePath + url = uri(repoDir) + } + } +} diff --git a/src/main/kotlin/CertPrinter.kt b/src/main/kotlin/CertPrinter.kt index 73c4893..5edfaa1 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]. * @@ -156,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)) } } } @@ -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()}") @@ -202,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") } @@ -290,6 +305,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/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 new file mode 100644 index 0000000..9f90d1d --- /dev/null +++ b/src/main/kotlin/ConstraintConfig.kt @@ -0,0 +1,454 @@ +/* + * Copyright 2025 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 + +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 +import java.math.BigInteger + +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 {} + + 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, certPath: KeyAttestationCertPath): Result +} + +/** + * Configuration for validating the attributes in an Android attestation certificate, as described + * at https://source.android.com/docs/security/features/keystore/attestation. + */ +@ThreadSafe +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 = + 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() + + companion object { + fun testDefault(): ConstraintConfig = ConstraintConfig(allowSoftwareRoot = true) + } +} + +/** + * 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 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 + var additionalConstraints: MutableList = mutableListOf() + var inputLimits: InputLimits = InputLimits() + + fun securityLevel(constraint: () -> Constraint) { + this.securityLevel = constraint() + } + + fun keyOrigin(constraint: () -> Constraint) { + this.keyOrigin = constraint() + } + + fun rootOfTrust(constraint: () -> Constraint) { + this.rootOfTrust = constraint() + } + + fun attestationApplicationId(constraint: () -> AttestationApplicationIdConstraint) { + this.attestationApplicationId = constraint() + } + + fun additionalConstraint(constraint: () -> Constraint) { + additionalConstraints.add(constraint()) + } + + fun inputLimits(init: () -> InputLimits) { + this.inputLimits = init() + } + + fun build(): ConstraintConfig = + ConstraintConfig( + allowSoftwareRoot, + attestationApplicationId, + keyOrigin, + securityLevel, + rootOfTrust, + ImmutableList.copyOf(additionalConstraints), + inputLimits, + ) +} + +/** 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, certPath: KeyAttestationCertPath) = + 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?) : + Constraint { + /** Evaluates whether the [description] is satisfied by this [AttributeConstraint]. */ + override fun check(description: KeyDescription, certPath: KeyAttestationCertPath) = + 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. + * + * @param expectedVal The expected value of the attribute. + */ + @Immutable(containerOf = ["T"]) + 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. */ + data class NOT_NULL(val l: String, private val m: AttributeMapper) : + AttributeConstraint(l, m) { + override fun isSatisfied(attribute: Any?): Boolean = attribute != null + } +} + +/** + * Configuration for validating the attestationSecurityLevel and keyMintSecurityLevel fields in an + * Android attestation certificate. + */ +@Immutable +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, certPath: KeyAttestationCertPath) = + if (isSatisfied(description, certPath)) { + Constraint.Satisfied + } else { + Constraint.Violated(getFailureMessage(description, certPath)) + } + + open fun getFailureMessage( + description: KeyDescription, + certPath: KeyAttestationCertPath, + ): String = + "Security level violates constraint: " + + "keyMintSecurityLevel=${description.keyMintSecurityLevel}, " + + "attestationSecurityLevel=${description.attestationSecurityLevel}, " + + "config=$this" + + /** + * Checks that both the attestationSecurityLevel and keyMintSecurityLevel match the expected + * 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 + }) + + /** + * Checks that the attestationSecurityLevel is equal to the keyMintSecurityLevel, and that this + * security level is not [SecurityLevel.SOFTWARE]. + */ + @Immutable + data object NOT_SOFTWARE : + SecurityLevelConstraint({ desc, _ -> + desc.keyMintSecurityLevel == desc.attestationSecurityLevel && + desc.attestationSecurityLevel != SecurityLevel.SOFTWARE + }) + + /** + * Checks that the attestationSecurityLevel is equal to the keyMintSecurityLevel, regardless of + * security level. + */ + @Immutable + data object CONSISTENT : + 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()})" + } +} + +/** + * Configuration for validating the ordering of the attributes in the AuthorizationList sequence in + * an Android attestation certificate. + */ +@Immutable +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 : TagOrderConstraint() { + override fun check(description: KeyDescription, certPath: KeyAttestationCertPath) = + if ( + description.softwareEnforced.areTagsOrdered && description.hardwareEnforced.areTagsOrdered + ) { + Constraint.Satisfied + } else { + Constraint.Violated("Authorization list tags must be in ascending order") + } + } +} + +/** + * 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) +} + +/** + * 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 64c9272..6d50f67 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 @@ -58,10 +59,42 @@ 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 = 32, + 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(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( @@ -82,13 +115,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) } @@ -171,25 +207,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(), @@ -198,8 +246,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), ) } } @@ -249,16 +297,19 @@ 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), 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), @@ -280,6 +331,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 } @@ -306,15 +365,18 @@ 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, 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, @@ -335,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. @@ -351,6 +413,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)) } @@ -360,6 +423,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)) @@ -370,6 +434,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)) @@ -443,14 +511,17 @@ data class AuthorizationList( parse(tag) { it.toPatchLevel(partition, logFn) } } - 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 + 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}" } + KeyMintTag.from(it.tagNo) to it.explicitBaseObject + } /** * X.680 section 8.6 @@ -477,27 +548,31 @@ 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), 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), 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 = 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), @@ -533,7 +608,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) } @@ -580,11 +657,46 @@ data class AttestationApplicationId( } .let { DERSequence(it.toTypedArray()) } - internal companion object { - fun from(seq: ASN1Sequence): AttestationApplicationId { + /** + * 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, + 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(), @@ -609,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) { @@ -645,12 +761,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, @@ -681,28 +797,36 @@ 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) -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 } @@ -740,11 +864,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) @@ -765,7 +889,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 @@ -775,11 +899,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) @@ -793,9 +921,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}" @@ -811,16 +941,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/main/kotlin/ExtensionConstraintConfig.kt b/src/main/kotlin/ExtensionConstraintConfig.kt deleted file mode 100644 index 3bc4c21..0000000 --- a/src/main/kotlin/ExtensionConstraintConfig.kt +++ /dev/null @@ -1,138 +0,0 @@ -/* - * Copyright 2025 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 - -import androidx.annotation.RequiresApi -import com.google.errorprone.annotations.Immutable -import com.google.errorprone.annotations.ThreadSafe - -/** - * Configuration for validating the extensions in an Android attestation certificate, as described - * at https://source.android.com/docs/security/features/keystore/attestation. - */ -@ThreadSafe -data class ExtensionConstraintConfig( - 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. */ -@Immutable(containerOf = ["T"]) -sealed interface ValidationLevel { - /** Evaluates whether the [extension] is satisfied by this [ValidationLevel]. */ - fun isSatisfiedBy(extension: Any?): Boolean - - /** - * Checks that the extension exists and matches the expected value. - * - * @param expectedVal The expected value of the extension. - */ - @Immutable(containerOf = ["T"]) - data class STRICT(val expectedVal: T) : ValidationLevel { - override fun isSatisfiedBy(extension: Any?): Boolean = extension == expectedVal - } - - /* Check that the extension exists. */ - @Immutable - data object NOT_NULL : ValidationLevel { - override fun isSatisfiedBy(extension: Any?): Boolean = extension != null - } - - @Immutable - data object IGNORE : ValidationLevel { - override fun isSatisfiedBy(extension: Any?): Boolean = true - } -} - -/** - * Configuration for validating the attestationSecurityLevel and keyMintSecurityLevel fields in an - * Android attestation certificate. - */ -@Immutable -sealed class SecurityLevelValidationLevel : ValidationLevel { - @RequiresApi(24) - fun areSecurityLevelsMatching(keyDescription: KeyDescription): Boolean { - return keyDescription.attestationSecurityLevel == keyDescription.keyMintSecurityLevel - } - - /** - * Checks that both the attestationSecurityLevel and keyMintSecurityLevel match the expected - * value. - * - * @param expectedVal The expected value of the security level. - */ - @Immutable - data class STRICT(val expectedVal: SecurityLevel) : SecurityLevelValidationLevel() { - @RequiresApi(24) - override fun isSatisfiedBy(extension: Any?): Boolean { - val keyDescription = extension as? KeyDescription ?: return false - val securityLevelIsExpected = keyDescription.attestationSecurityLevel == this.expectedVal - return areSecurityLevelsMatching(keyDescription) && securityLevelIsExpected - } - } - - /** - * 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(extension: Any?): Boolean { - val keyDescription = extension as? KeyDescription ?: return false - val securityLevelIsSoftware = - keyDescription.attestationSecurityLevel == SecurityLevel.SOFTWARE - return areSecurityLevelsMatching(keyDescription) && !securityLevelIsSoftware - } - } - - /** - * Checks that the attestationSecurityLevel is equal to the keyMintSecurityLevel, regardless of - * security level. - */ - @Immutable - data object CONSISTENT : SecurityLevelValidationLevel() { - @RequiresApi(24) - override fun isSatisfiedBy(extension: Any?): Boolean { - val keyDescription = extension as? KeyDescription ?: return false - return areSecurityLevelsMatching(keyDescription) - } - } -} - -/** - * Configuration for validating the ordering of the extensions 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 - * 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 - return keyDescription.softwareEnforced.areTagsOrdered && - keyDescription.hardwareEnforced.areTagsOrdered - } - } -} 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 diff --git a/src/main/kotlin/KeyAttestationReason.kt b/src/main/kotlin/KeyAttestationReason.kt index 3b4428e..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 [ExtensionConstraintConfig]. - // 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]. - // 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]. - // 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]. - 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/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 eb17906..1986655 100644 --- a/src/main/kotlin/Verifier.kt +++ b/src/main/kotlin/Verifier.kt @@ -16,7 +16,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 @@ -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 @@ -60,7 +61,7 @@ sealed interface VerificationResult { data class ExtensionParsingFailure(val cause: ExtensionParsingException) : VerificationResult - data class ExtensionConstraintViolation(val cause: String, val reason: KeyAttestationReason) : + data class ConstraintViolation(val constraintLabel: String, val cause: String) : VerificationResult data object SoftwareAttestationUnsupported : VerificationResult @@ -82,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 @@ -97,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] @@ -106,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 @@ -115,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() {} } /** @@ -142,15 +151,19 @@ constructor( private val trustAnchorsSource: () -> Set, private val revokedSerialsSource: () -> Set, private val instantSource: InstantSource, - private val extensionConstraintConfig: ExtensionConstraintConfig = ExtensionConstraintConfig(), + // TODO(google-internal bug): Make required, though still allow null value. + private val expectedAttestationApplicationId: AttestationApplicationId? = null, + private val constraintConfig: ConstraintConfig = ConstraintConfig(), ) { 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." + ) + } } } } @@ -167,17 +180,19 @@ constructor( * @return [VerificationResult] */ @JvmOverloads - @RequiresApi(24) 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) @@ -187,13 +202,14 @@ constructor( return result } - @RequiresApi(24) /** * 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 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]. */ @@ -202,6 +218,7 @@ constructor( coroutineScope: CoroutineScope, chain: List, challengeChecker: ChallengeChecker? = null, + additionalAttestationApplicationId: AttestationApplicationId? = null, log: LogHook? = null, ): ListenableFuture { val immutableChain = ImmutableList.copyOf(chain) @@ -210,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) @@ -221,10 +238,10 @@ constructor( } } - @RequiresApi(24) private suspend fun internalVerify( certPath: KeyAttestationCertPath, challengeChecker: ChallengeChecker? = null, + additionalAttestationApplicationId: AttestationApplicationId? = null, log: VerifyRequestLog? = null, ): VerificationResult { log?.logInputChain(certPath.certificatesWithAnchor.map { it.getEncoded().toByteString() }) @@ -239,7 +256,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 @@ -266,12 +283,18 @@ constructor( return VerificationResult.PathValidationFailure(e) } + log?.logCertSigningAlgorithms(certPath.signingAlgorithms()) + 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(). + // Should never happen since the extension's presence is checked by validate(). "Key attestation extension not found" } } catch (e: ExtensionParsingException) { @@ -291,47 +314,58 @@ constructor( } } - val origin = keyDescription.hardwareEnforced.origin - if (!extensionConstraintConfig.keyOrigin.isSatisfiedBy(origin)) { - return VerificationResult.ExtensionConstraintViolation( - "Origin violates constraint: value=${origin}, config=${extensionConstraintConfig.keyOrigin}", - KeyAttestationReason.KEY_ORIGIN_CONSTRAINT_VIOLATION, - ) + for (constraint in constraintConfig.getGenericConstraints()) { + val result = constraint.check(keyDescription, certPath) + when (result) { + is Constraint.Satisfied -> {} + is Constraint.Violated -> { + return VerificationResult.ConstraintViolation(constraint.label, result.failureMessage) + } + } } - val securityLevel = - if (extensionConstraintConfig.securityLevel.isSatisfiedBy(keyDescription)) { - minOf(keyDescription.attestationSecurityLevel, keyDescription.keyMintSecurityLevel) - } else { - return VerificationResult.ExtensionConstraintViolation( - "Security level violates constraint: value=${keyDescription.attestationSecurityLevel}, config=${extensionConstraintConfig.securityLevel}", - KeyAttestationReason.SECURITY_LEVEL_CONSTRAINT_VIOLATION, + 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) val rootOfTrust = keyDescription.hardwareEnforced.rootOfTrust - if (!extensionConstraintConfig.rootOfTrust.isSatisfiedBy(rootOfTrust)) { - return VerificationResult.ExtensionConstraintViolation( - "Root of trust violates constraint: value=${rootOfTrust}, config=${extensionConstraintConfig.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}", - KeyAttestationReason.AUTHORIZATION_LIST_ORDERING_CONSTRAINT_VIOLATION, - ) - } + val deviceLocked = rootOfTrust?.deviceLocked ?: false return VerificationResult.Success( pathValidationResult.publicKey, keyDescription.attestationChallenge, securityLevel, verifiedBootState, + deviceLocked, deviceInformation, DeviceIdentity.parseFrom(keyDescription), ) } } + +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/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/X509CertificateExt.kt b/src/main/kotlin/X509CertificateExt.kt index 67d0feb..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,4 +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() = ProvisioningInfoMap.parseFrom(this) +fun X509Certificate.provisioningInfo( + inputLimits: InputLimits = InputLimits() +): ProvisioningInfoMap? = ProvisioningInfoMap.parseFrom(this, inputLimits) diff --git a/src/main/kotlin/provider/KeyAttestationCertPath.kt b/src/main/kotlin/provider/KeyAttestationCertPath.kt index c26d1ed..44d69b6 100644 --- a/src/main/kotlin/provider/KeyAttestationCertPath.kt +++ b/src/main/kotlin/provider/KeyAttestationCertPath.kt @@ -19,9 +19,15 @@ 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 java.util.Locale +import javax.crypto.interfaces.DHPublicKey import javax.security.auth.x500.X500Principal /** @@ -72,6 +78,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 @@ -80,21 +98,34 @@ 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. */ 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. * @@ -111,7 +142,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. @@ -152,6 +183,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/provider/KeyAttestationCertPathValidator.kt b/src/main/kotlin/provider/KeyAttestationCertPathValidator.kt index e9a0857..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. * @@ -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) { @@ -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, @@ -330,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 71f1de7..ffb9e4a 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 @@ -54,6 +58,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, + ) } /** @@ -76,7 +90,7 @@ object CertLists { certFactory.generateLeafCert( certFactory.leafKey.public, certFactory.leafKey.private, - leafCert.subject, + issuer = leafCert.subject, ), leafCert, Certs.factoryAttestation, @@ -106,6 +120,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 = @@ -195,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( @@ -366,7 +413,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( @@ -481,9 +528,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 +542,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( @@ -567,4 +632,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/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/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/main/kotlin/testing/KeyAttestationCertFactory.kt b/src/main/kotlin/testing/KeyAttestationCertFactory.kt index 4529deb..7e5a639 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 hardcodedRootKey: 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 = hardcodedRootKey ?: 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 = @@ -84,7 +88,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 +96,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), ) @@ -117,12 +121,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, @@ -254,6 +262,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") } } 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 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/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/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/ConstraintConfigTest.kt b/src/test/kotlin/ConstraintConfigTest.kt new file mode 100644 index 0000000..0b922c9 --- /dev/null +++ b/src/test/kotlin/ConstraintConfigTest.kt @@ -0,0 +1,642 @@ +/* + * Copyright 2025 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 + +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 +import org.junit.runner.RunWith +import org.junit.runners.JUnit4 + +@RunWith(JUnit4::class) +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, + ) + + fun createTestKeyDescription( + attestationSecurityLevel: SecurityLevel, + keyMintSecurityLevel: SecurityLevel, + ) = + KeyDescription( + attestationVersion = 400.toBigInteger(), + attestationSecurityLevel = attestationSecurityLevel, + keyMintVersion = 400.toBigInteger(), + keyMintSecurityLevel = keyMintSecurityLevel, + attestationChallenge = ByteString.empty(), + uniqueId = ByteString.empty(), + softwareEnforced = AuthorizationList(attestationApplicationId = TEST_APP_ID), + hardwareEnforced = authorizationList, + ) + } + + val keyDescriptionWithStrongBoxSecurityLevels = + createTestKeyDescription(SecurityLevel.STRONG_BOX, SecurityLevel.STRONG_BOX) + val keyDescriptionWithTeeSecurityLevels = + createTestKeyDescription(SecurityLevel.TRUSTED_ENVIRONMENT, SecurityLevel.TRUSTED_ENVIRONMENT) + val keyDescriptionWithSoftwareSecurityLevels = + createTestKeyDescription(SecurityLevel.SOFTWARE, SecurityLevel.SOFTWARE) + val keyDescriptionWithMismatchedSecurityLevels = + 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() { + val level = AttributeConstraint.STRICT("Unique ID", "foo") { it.uniqueId.toStringUtf8() } + val kd = + keyDescriptionWithSoftwareSecurityLevels.copy(uniqueId = ByteString.copyFromUtf8("foo")) + + 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, testCertPath) + ) + + val kdWithRot = + keyDescriptionWithSoftwareSecurityLevels.copy( + hardwareEnforced = + keyDescriptionWithSoftwareSecurityLevels.hardwareEnforced.copy( + rootOfTrust = RootOfTrust(ByteString.empty(), false, VerifiedBootState.VERIFIED) + ) + ) + assertIs(level.check(kdWithRot, testCertPath)) + } + + @Test + fun SecurityLevelConstraintIsSatisfied_strictWithExpectedValue() { + val strongBoxCertPath = readCertPath("blueline/sdk28/SB_RSA_NONE.pem") + val strongBoxLevel = SecurityLevelConstraint.STRICT(SecurityLevel.STRONG_BOX) + + assertIs( + strongBoxLevel.check(keyDescriptionWithStrongBoxSecurityLevels, strongBoxCertPath) + ) + assertIs( + 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) + ) + } + + @Test + fun SecurityLevelConstraintIsSatisfied_notSoftware_allowsAnyNonSoftwareMatchingLevels() { + val level = SecurityLevelConstraint.NOT_SOFTWARE + + 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, 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, testCertPath) + ) + + val kdUnordered = + KeyDescription.parseFrom(readCertPath("invalid/tags_not_in_ascending_order.pem").leafCert())!! + + assertIs(ordering.check(kdUnordered, testCertPath)) + } + + @Test + 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, testCertPath)) + assertThat(violation.failureMessage) + .isEqualTo("Unique ID violates constraint: value=bar, config=$level") + } + + @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(keyDescriptionWithStrongBoxSecurityLevels, testCertPath) + ) + assertThat(violation.failureMessage) + .isEqualTo( + "Security level of KeyMint (STRONG_BOX) does not match attestation certificate (TRUSTED_ENVIRONMENT)" + ) + } + + @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, testCertPath)) + 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) + } + } + + @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( + 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.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/ExtensionConstraintConfigTest.kt b/src/test/kotlin/ExtensionConstraintConfigTest.kt deleted file mode 100644 index 112a0b1..0000000 --- a/src/test/kotlin/ExtensionConstraintConfigTest.kt +++ /dev/null @@ -1,126 +0,0 @@ -/* - * Copyright 2025 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 - -import com.android.keyattestation.verifier.testing.TestUtils.readCertPath -import com.google.common.truth.Truth.assertThat -import com.google.protobuf.ByteString -import org.junit.Test -import org.junit.runner.RunWith -import org.junit.runners.JUnit4 - -@RunWith(JUnit4::class) -class ExtensionConstraintConfigTest { - - private companion object { - val authorizationList = - AuthorizationList(purposes = setOf(1.toBigInteger()), algorithms = 1.toBigInteger()) - - fun createTestKeyDescription( - attestationSecurityLevel: SecurityLevel, - keyMintSecurityLevel: SecurityLevel, - ) = - KeyDescription( - attestationVersion = 1.toBigInteger(), - attestationSecurityLevel = attestationSecurityLevel, - keyMintVersion = 1.toBigInteger(), - keyMintSecurityLevel = keyMintSecurityLevel, - attestationChallenge = ByteString.empty(), - uniqueId = ByteString.empty(), - softwareEnforced = authorizationList, - hardwareEnforced = authorizationList, - ) - } - - val keyDescriptionWithStrongBoxSecurityLevels = - createTestKeyDescription(SecurityLevel.STRONG_BOX, SecurityLevel.STRONG_BOX) - val keyDescriptionWithTeeSecurityLevels = - createTestKeyDescription(SecurityLevel.TRUSTED_ENVIRONMENT, SecurityLevel.TRUSTED_ENVIRONMENT) - val keyDescriptionWithSoftwareSecurityLevels = - createTestKeyDescription(SecurityLevel.SOFTWARE, SecurityLevel.SOFTWARE) - val keyDescriptionWithMismatchedSecurityLevels = - createTestKeyDescription(SecurityLevel.STRONG_BOX, SecurityLevel.TRUSTED_ENVIRONMENT) - - @Test - fun ValidationLevelIsSatisfiedBy_strictWithExpectedValue() { - val level = ValidationLevel.STRICT("foo") - - assertThat(level.isSatisfiedBy("foo")).isTrue() - assertThat(level.isSatisfiedBy("bar")).isFalse() - assertThat(level.isSatisfiedBy(null)).isFalse() - } - - @Test - fun ValidationLevelIsSatisfiedBy_notNull_allowsAnyValue() { - val level = ValidationLevel.NOT_NULL - - assertThat(level.isSatisfiedBy("foo")).isTrue() - assertThat(level.isSatisfiedBy(null)).isFalse() - } - - @Test - fun ValidationLevelIsSatisfiedBy_ignore_allowsAnyValue() { - val level = ValidationLevel.IGNORE - - assertThat(level.isSatisfiedBy("foo")).isTrue() - assertThat(level.isSatisfiedBy(null)).isTrue() - } - - @Test - fun SecurityLevelValidationLevelIsSatisfiedBy_strictWithExpectedValue() { - val level = SecurityLevelValidationLevel.STRICT(SecurityLevel.STRONG_BOX) - - assertThat(level.isSatisfiedBy(keyDescriptionWithStrongBoxSecurityLevels)).isTrue() - assertThat(level.isSatisfiedBy(keyDescriptionWithTeeSecurityLevels)).isFalse() - assertThat(level.isSatisfiedBy(keyDescriptionWithMismatchedSecurityLevels)).isFalse() - } - - @Test - fun SecurityLevelValidationLevelIsSatisfiedBy_notSoftware_allowsAnyNonSoftwareMatchingLevels() { - val level = SecurityLevelValidationLevel.NOT_SOFTWARE - - assertThat(level.isSatisfiedBy(keyDescriptionWithStrongBoxSecurityLevels)).isTrue() - assertThat(level.isSatisfiedBy(keyDescriptionWithTeeSecurityLevels)).isTrue() - assertThat(level.isSatisfiedBy(keyDescriptionWithSoftwareSecurityLevels)).isFalse() - assertThat(level.isSatisfiedBy(keyDescriptionWithMismatchedSecurityLevels)).isFalse() - } - - @Test - fun SecurityLevelValidationLevelIsSatisfiedBy_consistent_allowsAnyMatchingLevels() { - val level = SecurityLevelValidationLevel.CONSISTENT - - assertThat(level.isSatisfiedBy(keyDescriptionWithStrongBoxSecurityLevels)).isTrue() - assertThat(level.isSatisfiedBy(keyDescriptionWithTeeSecurityLevels)).isTrue() - assertThat(level.isSatisfiedBy(keyDescriptionWithSoftwareSecurityLevels)).isTrue() - assertThat(level.isSatisfiedBy(keyDescriptionWithMismatchedSecurityLevels)).isFalse() - } - - @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() - ) - ) - ) - .isFalse() - } -} diff --git a/src/test/kotlin/ExtensionTest.kt b/src/test/kotlin/ExtensionTest.kt index be7ebcb..3ac1ebe 100644 --- a/src/test/kotlin/ExtensionTest.kt +++ b/src/test/kotlin/ExtensionTest.kt @@ -15,11 +15,16 @@ */ 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 +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 @@ -29,15 +34,17 @@ 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.math.BigInteger +import java.time.Instant 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 +import org.junit.Assert.assertThrows import org.junit.Test import org.junit.runner.RunWith @@ -53,7 +60,10 @@ class ExtensionTest { certificatesIssued = 1, ) val provisioningInfo = - ProvisioningInfoMap.parseFrom(Chains.validRemotelyProvisioned.attestationCert()) + ProvisioningInfoMap.parseFrom( + Chains.validRemotelyProvisioned.attestationCert(), + InputLimits(), + ) assertThat(provisioningInfo).isEqualTo(expectedProvisioningInfo) } @@ -72,32 +82,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 @@ -109,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 @@ -137,14 +133,17 @@ 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(), 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, @@ -206,6 +205,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() @@ -221,4 +237,84 @@ 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) + } + } + + @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/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) = diff --git a/src/test/kotlin/VerifierTest.kt b/src/test/kotlin/VerifierTest.kt index 3c077e3..8bc506e 100644 --- a/src/test/kotlin/VerifierTest.kt +++ b/src/test/kotlin/VerifierTest.kt @@ -16,7 +16,9 @@ package com.android.keyattestation.verifier -import com.android.keyattestation.verifier.VerificationResult.ExtensionConstraintViolation +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 import com.android.keyattestation.verifier.challengecheckers.ChallengeMatcher @@ -24,6 +26,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 +35,22 @@ 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.math.BigInteger 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 +61,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,39 +82,56 @@ 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) + @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") + 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 }, + json.softwareEnforced.attestationApplicationId, + constraintConfig { + allowSoftwareRoot = true + securityLevel { IgnoredConstraint } + rootOfTrust { IgnoredConstraint } + attestationApplicationId { AttestationApplicationIdConstraint.STRICT } + }, + ) + 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 + ) + 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 @@ -149,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 = @@ -170,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)) @@ -179,22 +232,67 @@ class VerifierTest { @Test fun rootOfTrustMissing_givesRootOfTrustMissingReason() { - val result = - assertIs(verifier.verify(CertLists.missingRootOfTrust)) - assertThat(result.reason).isEqualTo(KeyAttestationReason.ROOT_OF_TRUST_CONSTRAINT_VIOLATION) + val result = assertIs(verifier.verify(CertLists.missingRootOfTrust)) + 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) + val result = assertIs(verifier.verify(CertLists.importedOrigin)) + 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.constraintLabel).isEqualTo("Security level") + 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 = + 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 = + constraintConfig { additionalConstraint { SecurityLevelConstraint.MATCHES_CERTIFICATE } }, + ) val result = - assertIs(verifier.verify(CertLists.mismatchedSecurityLevels)) - assertThat(result.reason).isEqualTo(KeyAttestationReason.SECURITY_LEVEL_CONSTRAINT_VIOLATION) + 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 @@ -204,13 +302,104 @@ class VerifierTest { { prodAnchors + TrustAnchor(Certs.root, null) }, { setOf() }, { FakeCalendar.DEFAULT.now() }, - ExtensionConstraintConfig(securityLevel = ValidationLevel.NOT_NULL), + constraintConfig = constraintConfig { securityLevel { IgnoredConstraint } }, ) val result = assertIs(verifier.verify(CertLists.mismatchedSecurityLevels)) assertThat(result.securityLevel).isEqualTo(SecurityLevel.SOFTWARE) } + @Test + fun factoryProvisionedChain_withRemoteConstraint_failsVerification() { + val verifier = + Verifier( + { prodAnchors + TrustAnchor(Certs.root, null) }, + { setOf() }, + { FakeCalendar.DEFAULT.now() }, + constraintConfig = + 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 = + 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 = + 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 = + constraintConfig { additionalConstraint { ProvisioningMethodConstraint.REMOTE } }, + ) + assertIs(verifier.verify(CertLists.validRemotelyProvisioned)) + } + + @Test + fun importedOrigins_customConfig_succeeds() { + val verifier = + Verifier( + { prodAnchors + TrustAnchor(Certs.root, null) }, + { setOf() }, + { FakeCalendar.DEFAULT.now() }, + constraintConfig = + 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 = constraintConfig { rootOfTrust { IgnoredConstraint } }, + ) + assertIs(verifier.verify(CertLists.missingRootOfTrust)) + } + @Test fun unorderedTags_customConfig_throwsCertPathValidatorException() { val verifier = @@ -218,15 +407,19 @@ class VerifierTest { { prodAnchors + TrustAnchor(Certs.root, null) }, { setOf() }, { FakeCalendar.DEFAULT.now() }, - ExtensionConstraintConfig(authorizationListTagOrder = TagOrderValidationLevel.STRICT), + constraintConfig = + constraintConfig { + additionalConstraint { TagOrderConstraint.STRICT } + additionalConstraint { IgnoredConstraint } + }, ) - val result = assertIs(verifier.verify(CertLists.unorderedTags)) - assertThat(result.reason) - .isEqualTo(KeyAttestationReason.AUTHORIZATION_LIST_ORDERING_CONSTRAINT_VIOLATION) + val result = assertIs(verifier.verify(CertLists.unorderedTags)) + assertThat(result.constraintLabel).isEqualTo("Tag order") + assertThat(result.cause).contains("Authorization list tags must be in ascending order") } @Test - fun verifyAsync_failure_inputChainLogged() = runBlocking { + fun verifyAsync_failure_inputChainLogged(): Unit = runBlocking { val logHook = FakeLogHook() assertIs( verifier @@ -234,7 +427,7 @@ class VerifierTest { this, CertLists.wrongTrustAnchor, ChallengeMatcher(ByteString.copyFromUtf8("challenge")), - logHook, + log = logHook, ) .await() ) @@ -243,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()) @@ -252,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)) }, @@ -266,6 +459,32 @@ 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_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") @@ -278,7 +497,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() }, + ) } } } diff --git a/src/test/kotlin/provider/KeyAttestationCertPathTest.kt b/src/test/kotlin/provider/KeyAttestationCertPathTest.kt index 09e8836..374c880 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") @@ -108,6 +118,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), @@ -119,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), } } diff --git a/src/test/kotlin/provider/KeyAttestationCertPathValidatorTest.kt b/src/test/kotlin/provider/KeyAttestationCertPathValidatorTest.kt index afb9f56..6445ca6 100644 --- a/src/test/kotlin/provider/KeyAttestationCertPathValidatorTest.kt +++ b/src/test/kotlin/provider/KeyAttestationCertPathValidatorTest.kt @@ -18,10 +18,12 @@ 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 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 +36,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 +88,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)), ) } @@ -164,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 @@ -205,8 +222,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 +236,39 @@ 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 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/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) + } +} 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----- -MIIFHDCCAwSgAwIBAgIJANUP8luj8tazMA0GCSqGSIb3DQEBCwUAMBsxGTAXBgNVBAUTEGY5MjAwOWU4NTNiNmIwNDUwHhcNMTkxMTIyMjAzNzU4WhcNMzQxMTE4MjAzNzU4WjAbMRkwFwYDVQQFExBmOTIwMDllODUzYjZiMDQ1MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAr7bHgiuxpwHsK7Qui8xUFmOr75gvMsd/dTEDDJdSSxtf6An7xyqpRR90PL2abxM1dEqlXnf2tqw1Ne4Xwl5jlRfdnJLmN0pTy/4lj4/7tv0Sk3iiKkypnEUtR6WfMgH0QZfKHM1+di+y9TFRtv6y//0rb+T+W8a9nsNL/ggjnar86461qO0rOs2cXjp3kOG1FEJ5MVmFmBGtnrKpa73XpXyTqRxB/M0n1n/W9nGqC4FSYa04T6N5RIZGBN2z2MT5IKGbFlbC8UrW0DxW7AYImQQcHtGl/m00QLVWutHQoVJYnFPlXTcHYvASLu+RhhsbDmxMgJJ0mcDpvsC4PjvB+TxywElgS70vE0XmLD+OJtvsBslHZvPBKCOdT0MS+tgSOIfga+z1Z1g7+DVagf7quvmag8jfPioyKvxnK/EgsTUVi2ghzq8wm27ud/mIM7AY2qEORR8Go3TVB4HzWQgpZrt3i5MIlCaY504LzSRiigHCzAPlHws+W0rB5N+er5/2pJKnfBSDiCiFAVtCLOZ7gLiMm0jhO2B6tUXHI/+MRPjy02i59lINMRRev56GKtcd9qO/0kUJWdZTdA2XoS82ixPvZtXQpUpuL12ab+9EaDK8Z4RHJYYfCT3Q5vNAXaiWQ+8PTWm2QgBR/bkwSWc+NpUFgNPN9PvQi8WEg5UmAGMCAwEAAaNjMGEwHQYDVR0OBBYEFDZh4QB8iAUJUYtEbEf/GkzJ6k8SMB8GA1UdIwQYMBaAFDZh4QB8iAUJUYtEbEf/GkzJ6k8SMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgIEMA0GCSqGSIb3DQEBCwUAA4ICAQBOMaBc8oumXb2voc7XCWnuXKhBBK3e2KMGz39t7lA3XXRe2ZLLAkLM5y3J7tURkf5a1SutfdOyXAmeE6SRo83Uh6WszodmMkxK5GM4JGrnt4pBisu5igXEydaW7qq2CdC6DOGjG+mEkN8/TA6p3cnoL/sPyz6evdjLlSeJ8rFBH6xWyIZCbrcpYEJzXaUOEaxxXxgYz5/cTiVKN2M1G2okQBUIYSY6bjEL4aUN5cfo7ogP3UvliEo3Eo0YgwuzR2v0KR6C1cZqZJSTnghIC/vAD32KdNQ+c3N+vl2OTsUVMC1GiWkngNx1OO1+kXW+YTnnTUOtOIswUP/Vqd5SYgAImMAfY8U9/iIgkQj6T2W6FsScy94IN9fFhE1UtzmLoBIuUFsVXJMTz+Jucth+IqoWFua9v1R93/k98p41pjtFX+H8DslVgfP097vju4KDlqN64xV1grw3ZLl4CiOe/A91oeLm2UHOq6wn3esB4r2EIQKb6jTVGu5sYCcdWpXr0AUVqcABPdgL+H7qJguBw09ojm6xNIrw2OocrDKsudk/okr/AwqEyPKw9WnMlQgLIKw1rODG2NvU9oR3GVGdMkUBZutL8VuFkERQGt6vQ2OCw0sV47VMkuYbacK/xyZFiRcrPJPb41zgbQj9XAEyLKCHex0SdDrx+tWUDqG8At2JHA== +MIIFHDCCAwSgAwIBAgIJANUP8luj8tazMA0GCSqGSIb3DQEBCwUAMBsxGTAXBgNV +BAUTEGY5MjAwOWU4NTNiNmIwNDUwHhcNMTkxMTIyMjAzNzU4WhcNMzQxMTE4MjAz +NzU4WjAbMRkwFwYDVQQFExBmOTIwMDllODUzYjZiMDQ1MIICIjANBgkqhkiG9w0B +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/BAQDAgIEMA0GCSqGSIb3DQEBCwUAA4ICAQBOMaBc8oumXb2voc7XCWnu +XKhBBK3e2KMGz39t7lA3XXRe2ZLLAkLM5y3J7tURkf5a1SutfdOyXAmeE6SRo83U +h6WszodmMkxK5GM4JGrnt4pBisu5igXEydaW7qq2CdC6DOGjG+mEkN8/TA6p3cno +L/sPyz6evdjLlSeJ8rFBH6xWyIZCbrcpYEJzXaUOEaxxXxgYz5/cTiVKN2M1G2ok +QBUIYSY6bjEL4aUN5cfo7ogP3UvliEo3Eo0YgwuzR2v0KR6C1cZqZJSTnghIC/vA +D32KdNQ+c3N+vl2OTsUVMC1GiWkngNx1OO1+kXW+YTnnTUOtOIswUP/Vqd5SYgAI +mMAfY8U9/iIgkQj6T2W6FsScy94IN9fFhE1UtzmLoBIuUFsVXJMTz+Jucth+IqoW +Fua9v1R93/k98p41pjtFX+H8DslVgfP097vju4KDlqN64xV1grw3ZLl4CiOe/A91 +oeLm2UHOq6wn3esB4r2EIQKb6jTVGu5sYCcdWpXr0AUVqcABPdgL+H7qJguBw09o +jm6xNIrw2OocrDKsudk/okr/AwqEyPKw9WnMlQgLIKw1rODG2NvU9oR3GVGdMkUB +ZutL8VuFkERQGt6vQ2OCw0sV47VMkuYbacK/xyZFiRcrPJPb41zgbQj9XAEyLKCH +ex0SdDrx+tWUDqG8At2JHA== -----END CERTIFICATE----- \ No newline at end of file diff --git a/testdata/complete_authlist.json b/testdata/complete_authlist.json new file mode 100644 index 0000000..bd176f3 --- /dev/null +++ b/testdata/complete_authlist.json @@ -0,0 +1,85 @@ +{ + "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", + "mlDsaVariant": "1", + "rsaPublicExponent": "65537", + "rsaOaepMgfDigests": ["1", "2"], + "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", + "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" + } +} 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 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----- 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..ba5d61d --- /dev/null +++ b/testdata/sony-xperia10-iii/sdk33/TEE_EC.json @@ -0,0 +1,39 @@ +{ + "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="] + } + }, + "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" + } +} 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----- 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 new file mode 100644 index 0000000..2db7c9d --- /dev/null +++ b/testdata/tegu/sdk37/TEE_MAX_USAGE_COUNT.json @@ -0,0 +1,43 @@ +{ + "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=" + }, + "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" + } +} 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..c357fad --- /dev/null +++ b/testdata/tegu/sdk37/TEE_TRUSTED_CONF.json @@ -0,0 +1,43 @@ +{ + "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=" + }, + "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" + } +} 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----- diff --git a/testdata/tokay/sdk37/TEE_MLDSA_FACTORY.json b/testdata/tokay/sdk37/TEE_MLDSA_FACTORY.json new file mode 100644 index 0000000..01c7eb5 --- /dev/null +++ b/testdata/tokay/sdk37/TEE_MLDSA_FACTORY.json @@ -0,0 +1,34 @@ +{ + "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" + } +} 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..dae8682 --- /dev/null +++ b/testdata/tokay/sdk37/TEE_MLDSA_RKP.json @@ -0,0 +1,34 @@ +{ + "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" + } +} 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----- 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