Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
117 changes: 117 additions & 0 deletions src/main/kotlin/ExtensionConstraintConfig.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
/*
* 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<Origin> = ValidationLevel.STRICT(Origin.GENERATED),
val securityLevel: ValidationLevel<KeyDescription> =
SecurityLevelValidationLevel.STRICT(SecurityLevel.TRUSTED_ENVIRONMENT),
val rootOfTrust: ValidationLevel<RootOfTrust> = ValidationLevel.NOT_NULL,
)

/** Configuration for validating a single extension in an Android attestation certificate. */
@Immutable(containerOf = ["T"])
sealed interface ValidationLevel<out T> {
/** 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<T>(val expectedVal: T) : ValidationLevel<T> {
override fun isSatisfiedBy(extension: Any?): Boolean = extension == expectedVal
}

/* Check that the extension exists. */
@Immutable
data object NOT_NULL : ValidationLevel<Nothing> {
override fun isSatisfiedBy(extension: Any?): Boolean = extension != null
}

@Immutable
data object IGNORE : ValidationLevel<Nothing> {
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<KeyDescription> {
@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)
}
}
}
22 changes: 13 additions & 9 deletions src/main/kotlin/KeyAttestationReason.kt
Original file line number Diff line number Diff line change
Expand Up @@ -33,15 +33,19 @@ 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 key was not generated. The verifier cannot know that the key has always been in the
// secure environment.
KEY_ORIGIN_NOT_GENERATED,
// The attestation and the KeyMint security levels do not match.
// This likely indicates that the attestation was generated in software and so cannot be trusted.
MISMATCHED_SECURITY_LEVELS,
// The key description is missing the root of trust.
// An Android key attestation chain without a root of trust is malformed.
ROOT_OF_TRUST_MISSING,
// 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,
// There was an error parsing the key description and an unknown tag number was encountered.
UNKNOWN_TAG_NUMBER,
}
41 changes: 23 additions & 18 deletions src/main/kotlin/Verifier.kt
Original file line number Diff line number Diff line change
Expand Up @@ -136,10 +136,13 @@ interface VerifyRequestLog {
* @param anchor a [TrustAnchor] to use for certificate path verification.
*/
@ThreadSafe
open class Verifier(
open class Verifier
@JvmOverloads
constructor(
private val trustAnchorsSource: () -> Set<TrustAnchor>,
private val revokedSerialsSource: () -> Set<String>,
private val instantSource: InstantSource,
private val extensionConstraintConfig: ExtensionConstraintConfig = ExtensionConstraintConfig(),
) {
init {
Security.addProvider(KeyAttestationProvider())
Expand Down Expand Up @@ -287,36 +290,38 @@ open class Verifier(
}
}

if (
keyDescription.hardwareEnforced.origin == null ||
keyDescription.hardwareEnforced.origin != Origin.GENERATED
) {
val origin = keyDescription.hardwareEnforced.origin
if (!extensionConstraintConfig.keyOrigin.isSatisfiedBy(origin)) {
return VerificationResult.ExtensionConstraintViolation(
"origin != GENERATED: ${keyDescription.hardwareEnforced.origin}",
KeyAttestationReason.KEY_ORIGIN_NOT_GENERATED,
"Origin violates constraint: value=${origin}, config=${extensionConstraintConfig.keyOrigin}",
KeyAttestationReason.KEY_ORIGIN_CONSTRAINT_VIOLATION,
)
}

val securityLevel =
if (keyDescription.attestationSecurityLevel == keyDescription.keyMintSecurityLevel) {
keyDescription.attestationSecurityLevel
if (extensionConstraintConfig.securityLevel.isSatisfiedBy(keyDescription)) {
minOf(keyDescription.attestationSecurityLevel, keyDescription.keyMintSecurityLevel)
} else {
return VerificationResult.ExtensionConstraintViolation(
"attestationSecurityLevel != keyMintSecurityLevel: ${keyDescription.attestationSecurityLevel} != ${keyDescription.keyMintSecurityLevel}",
KeyAttestationReason.MISMATCHED_SECURITY_LEVELS,
"Security level violates constraint: value=${keyDescription.attestationSecurityLevel}, config=${extensionConstraintConfig.securityLevel}",
KeyAttestationReason.SECURITY_LEVEL_CONSTRAINT_VIOLATION,
)
}
val rootOfTrust =
keyDescription.hardwareEnforced.rootOfTrust
?: return VerificationResult.ExtensionConstraintViolation(
"hardwareEnforced.rootOfTrust is null",
KeyAttestationReason.ROOT_OF_TRUST_MISSING,
)

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

return VerificationResult.Success(
pathValidationResult.publicKey,
keyDescription.attestationChallenge,
securityLevel,
rootOfTrust.verifiedBootState,
verifiedBootState,
deviceInformation,
DeviceIdentity.parseFrom(keyDescription),
)
Expand Down
110 changes: 110 additions & 0 deletions src/test/kotlin/ExtensionConstraintConfigTest.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
/*
* 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.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()
}
}
20 changes: 17 additions & 3 deletions src/test/kotlin/VerifierTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -173,20 +173,34 @@ class VerifierTest {
fun rootOfTrustMissing_givesRootOfTrustMissingReason() {
val result =
assertIs<ExtensionConstraintViolation>(verifier.verify(CertLists.missingRootOfTrust))
assertThat(result.reason).isEqualTo(KeyAttestationReason.ROOT_OF_TRUST_MISSING)
assertThat(result.reason).isEqualTo(KeyAttestationReason.ROOT_OF_TRUST_CONSTRAINT_VIOLATION)
}

@Test
fun keyOriginNotGenerated_throwsCertPathValidatorException() {
val result = assertIs<ExtensionConstraintViolation>(verifier.verify(CertLists.importedOrigin))
assertThat(result.reason).isEqualTo(KeyAttestationReason.KEY_ORIGIN_NOT_GENERATED)
assertThat(result.reason).isEqualTo(KeyAttestationReason.KEY_ORIGIN_CONSTRAINT_VIOLATION)
}

@Test
fun mismatchedSecurityLevels_throwsCertPathValidatorException() {
val result =
assertIs<ExtensionConstraintViolation>(verifier.verify(CertLists.mismatchedSecurityLevels))
assertThat(result.reason).isEqualTo(KeyAttestationReason.MISMATCHED_SECURITY_LEVELS)
assertThat(result.reason).isEqualTo(KeyAttestationReason.SECURITY_LEVEL_CONSTRAINT_VIOLATION)
}

@Test
fun mismatchedSecurityLevels_customConfig_succeeds() {
val verifier =
Verifier(
{ prodAnchors + TrustAnchor(Certs.root, null) },
{ setOf<String>() },
{ FakeCalendar.DEFAULT.now() },
ExtensionConstraintConfig(securityLevel = ValidationLevel.NOT_NULL),
)
val result =
assertIs<VerificationResult.Success>(verifier.verify(CertLists.mismatchedSecurityLevels))
assertThat(result.securityLevel).isEqualTo(SecurityLevel.SOFTWARE)
}

@Test
Expand Down