Skip to content
Open
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
5 changes: 4 additions & 1 deletion src/main/kotlin/Extension.kt
Original file line number Diff line number Diff line change
Expand Up @@ -333,6 +333,7 @@ data class AuthorizationList(
val bootPatchLevel: PatchLevel? = null,
val attestationIdSecondImei: String? = null,
val moduleHash: ByteString? = null,
internal val areTagsOrdered: Boolean = true,
) {
/**
* Converts the representation of an [AuthorizationList] to an ASN.1 sequence.
Expand Down Expand Up @@ -460,7 +461,8 @@ data class AuthorizationList(
* 2. within each class of tags, the elements or alternatives shall appear in ascending order
* of their tag numbers.
*/
if (!objects.keys.zipWithNext().all { (lhs, rhs) -> rhs > lhs }) {
val areTagsOrdered = (objects.keys.zipWithNext().all { (lhs, rhs) -> rhs > lhs })
if (!areTagsOrdered) {
logFn("AuthorizationList tags should appear in ascending order")
}

Expand Down Expand Up @@ -507,6 +509,7 @@ data class AuthorizationList(
bootPatchLevel = converter.parsePatchLevel(KeyMintTag.BOOT_PATCH_LEVEL, "boot"),
attestationIdSecondImei = converter.parseStr(KeyMintTag.ATTESTATION_ID_SECOND_IMEI),
moduleHash = converter.parseByteString(KeyMintTag.MODULE_HASH),
areTagsOrdered = areTagsOrdered,
)
}
}
Expand Down
133 changes: 133 additions & 0 deletions src/main/kotlin/ExtensionConstraintConfig.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
/*
* 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 attenstation 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: SecurityLevelValidationLevel = SecurityLevelValidationLevel.STRICT(),
val rootOfTrust: ValidationLevel<RootOfTrust> = ValidationLevel.STRICT(null),
val authorizationListOrdering: AuthorizationListOrdering = AuthorizationListOrdering.IGNORE,
)

/**
* Configuration for validating a single extension in an Android attenstation certificate.
*
* @param expectedVal The expected value of the extension. If null, the extension is checked for
* existence but not equality.
*/
@Immutable(containerOf = ["T"])
sealed interface ValidationLevel<out T> {
@Immutable(containerOf = ["T"]) data class STRICT<T>(val expectedVal: T?) : ValidationLevel<T>

@Immutable data object IGNORE : ValidationLevel<Nothing>
}

/**
* Configuration for validating the attestationSecurityLevel and keyMintSecurityLevel fields in an
* Android attenstation certificate.
*/
@Immutable
sealed interface SecurityLevelValidationLevel {
/**
* Checks that the attestationSecurityLevel is both (1) one of {TRUSTED_ENVIRONMENT, STRONG_BOX}
* and (2) equal to the keyMintSecurityLevel.
*
* If expectedVal is provided, checks that both the attestationSecurityLevel and
* keyMintSecurityLevel are equal to the expected value.
*/
@Immutable
data class STRICT(val expectedVal: SecurityLevel? = null) : SecurityLevelValidationLevel {
init {
require(expectedVal != SecurityLevel.SOFTWARE) {
"STRICT validation level cannot be used with SOFTWARE security level."
}
}
}

/**
* Checks that the attestationSecurityLevel is equal to the keyMintSecurityLevel, regardless of
* security level
*/
@Immutable data object CONSISTENT : SecurityLevelValidationLevel

/**
* Checks that attestationSecurityLevel and keyMintSecurityLevel both exist and are correctly
* formed. If they are unequal, [Verifier.verify] will return the lower securityLevel.
*/
@Immutable data object EXISTS : SecurityLevelValidationLevel
}

/**
* Configuration for validating the ordering of the extensions in the AuthorizationList sequence in
* an Android attenstation certificate.
*/
enum class AuthorizationListOrdering {

/**
* Checks that the extensions in the AuthorizationList sequence appear in the order specified by
* https://source.android.com/docs/security/features/keystore/attestation#schema.
*/
STRICT,

/** Allows the extensions in the AuthorizationList sequence appear in any order. */
IGNORE,
}

/** Evaluates whether the [extension] is satisfied by the [ValidationLevel]. */
fun <T> ValidationLevel<T>.isSatisfiedBy(extension: T?): Boolean =
when (this) {
is ValidationLevel.STRICT ->
if (expectedVal == null) extension != null else extension == expectedVal
is ValidationLevel.IGNORE -> true
}

/** Evaluates whether the [keyDescription] is satisfied by the [SecurityLevelValidationLevel]. */
@RequiresApi(24)
fun SecurityLevelValidationLevel.isSatisfiedBy(keyDescription: KeyDescription): Boolean {
val securityLevelsMatch =
keyDescription.attestationSecurityLevel == keyDescription.keyMintSecurityLevel

return when (this) {
is SecurityLevelValidationLevel.STRICT -> {
val securityLevelIsExpected =
if (this.expectedVal != null) keyDescription.attestationSecurityLevel == this.expectedVal
else keyDescription.attestationSecurityLevel != SecurityLevel.SOFTWARE
securityLevelsMatch && securityLevelIsExpected
}
is SecurityLevelValidationLevel.CONSISTENT -> securityLevelsMatch
is SecurityLevelValidationLevel.EXISTS -> true
}
}

/** Evaluates whether the [keyDescription] is satisfied by the [AuthorizationListOrdering]. */
@RequiresApi(24)
fun AuthorizationListOrdering.isSatisfiedBy(keyDescription: KeyDescription): Boolean =
when (this) {
AuthorizationListOrdering.STRICT ->
keyDescription.softwareEnforced.areTagsOrdered &&
keyDescription.hardwareEnforced.areTagsOrdered
AuthorizationListOrdering.IGNORE -> true
}
25 changes: 16 additions & 9 deletions src/main/kotlin/KeyAttestationReason.kt
Original file line number Diff line number Diff line change
Expand Up @@ -33,15 +33,22 @@ 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,
// The authorization list ordering violated the constraint provided in
// [ExtensionConstraintConfig].
AUTHORIZATION_LIST_ORDERING_CONSTRAINT_VIOLATION,
// There was an error parsing the key description and an unknown tag number was encountered.
UNKNOWN_TAG_NUMBER,
}
49 changes: 31 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 @@ -284,43 +287,53 @@ open class Verifier(
return VerificationResult.ExtensionParsingFailure(ExtensionParsingException(e.toString()))
}
log?.logKeyDescription(keyDescription)

if (!extensionConstraintConfig.authorizationListOrdering.isSatisfiedBy(keyDescription)) {
return VerificationResult.ExtensionConstraintViolation(
"Authorization list ordering violates constraint: config=${extensionConstraintConfig.authorizationListOrdering}",
KeyAttestationReason.AUTHORIZATION_LIST_ORDERING_CONSTRAINT_VIOLATION,
)
}

if (challengeChecker != null) {
val checkResult = challengeChecker.checkChallenge(keyDescription.attestationChallenge).await()
if (!checkResult) {
return VerificationResult.ChallengeMismatch
}
}

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
Loading