fix(keystore): reorder auths off genmode sentinel
Duck's generate-mode parcel fingerprint reads the reply with a flat 12-byte stride and flags the sentinel tuple the device's native ALGORITHM-first auth order lands on, at count 12 and 13. Real A16 hardware trips it too, so faithful mirroring stays flagged. Add InterceptorUtils.normalizeAuthorizationLayout: marshal the auth array, run Duck's exact predicate, and only when it would match, reorder by a deterministic minimal move until it clears. Order carries no keystore semantics and the cert chain is a separate field, so count, values, security levels, and attestation are all preserved. Applied on both the patch and forge reply paths; it keys on the byte condition, never on any package.
This commit is contained in:
+105
-2
@@ -8,6 +8,8 @@ import android.os.Parcelable
|
||||
import android.security.KeyStore
|
||||
import android.security.keystore.KeystoreResponse
|
||||
import android.system.keystore2.Authorization
|
||||
import java.nio.ByteBuffer
|
||||
import java.nio.ByteOrder
|
||||
import org.matrix.TEESimulator.interception.core.BinderInterceptor
|
||||
import org.matrix.TEESimulator.logging.SystemLogger
|
||||
import org.matrix.TEESimulator.util.AndroidDeviceUtils
|
||||
@@ -26,6 +28,11 @@ object InterceptorUtils {
|
||||
|
||||
private const val EX_SERVICE_SPECIFIC = -8
|
||||
|
||||
private const val FLAT_STRIDE_HEADER = 12
|
||||
private const val MAX_AUTH_COUNT = 256
|
||||
private const val SENTINEL_MODTIME = 4_294_967_297L
|
||||
private const val HIGH_MODTIME = 4_999_999_999L
|
||||
|
||||
private fun synthesizeSseMessage(errorCode: Int): String =
|
||||
when (errorCode) {
|
||||
2 -> "Error::Rc(SYSTEM_ERROR)"
|
||||
@@ -197,8 +204,8 @@ object InterceptorUtils {
|
||||
val vendorPatch = AndroidDeviceUtils.getVendorPatchLevelLong(callingUid)
|
||||
val bootPatch = AndroidDeviceUtils.getBootPatchLevelLong(callingUid)
|
||||
|
||||
return authorizations
|
||||
.map { auth ->
|
||||
val patched =
|
||||
authorizations.map { auth ->
|
||||
val replacement =
|
||||
when (auth.keyParameter.tag) {
|
||||
Tag.OS_PATCHLEVEL ->
|
||||
@@ -224,5 +231,101 @@ object InterceptorUtils {
|
||||
}
|
||||
}
|
||||
.toTypedArray()
|
||||
return normalizeAuthorizationLayout(patched)
|
||||
}
|
||||
|
||||
/**
|
||||
* Reorders a generateKey reply's authorizations only when the marshalled reply would read, to a
|
||||
* flat 12-byte-stride parcel fingerprint, as the TEE-simulator sentinel: a last slot of
|
||||
* securityLevel 4 or 256, tag 1, union 32 with the 0x1_0000_0001 pseudo-timestamp, or any
|
||||
* timestamp past [HIGH_MODTIME]. Real Android 16 hardware emits a 13-authorization layout for
|
||||
* single-purpose EC keys that lands on that sentinel, so mirroring hardware byte-for-byte is
|
||||
* itself flagged. Clients resolve authorizations by tag and the certificate chain is a separate
|
||||
* field, so reordering is the minimal capability-preserving way to clear the false positive for
|
||||
* any caller. Already-clean replies are returned unchanged.
|
||||
*/
|
||||
fun normalizeAuthorizationLayout(authorizations: Array<Authorization>): Array<Authorization> {
|
||||
if (authorizations.size < 2) return authorizations
|
||||
if (!flatStrideFingerprintMatches(marshalTypedArray(authorizations))) return authorizations
|
||||
val n = authorizations.size
|
||||
for (src in 1 until n) {
|
||||
val candidate = moveAuthorization(authorizations, src, 0)
|
||||
if (!flatStrideFingerprintMatches(marshalTypedArray(candidate))) return candidate
|
||||
}
|
||||
for (src in 0 until n) {
|
||||
for (dst in 0 until n) {
|
||||
if (src == dst) continue
|
||||
val candidate = moveAuthorization(authorizations, src, dst)
|
||||
if (!flatStrideFingerprintMatches(marshalTypedArray(candidate))) return candidate
|
||||
}
|
||||
}
|
||||
return authorizations
|
||||
}
|
||||
|
||||
private fun moveAuthorization(
|
||||
authorizations: Array<Authorization>,
|
||||
src: Int,
|
||||
dst: Int,
|
||||
): Array<Authorization> {
|
||||
val reordered = authorizations.toMutableList()
|
||||
reordered.add(dst, reordered.removeAt(src))
|
||||
return reordered.toTypedArray()
|
||||
}
|
||||
|
||||
private fun marshalTypedArray(authorizations: Array<Authorization>): ByteArray {
|
||||
val parcel = Parcel.obtain()
|
||||
return try {
|
||||
// keystore2 AIDL compile stubs omit the Parcelable supertype these types carry at runtime.
|
||||
parcel.writeTypedArray(authorizations.map { it as Parcelable }.toTypedArray(), 0)
|
||||
parcel.marshall()
|
||||
} finally {
|
||||
parcel.recycle()
|
||||
}
|
||||
}
|
||||
|
||||
private fun flatStrideFingerprintMatches(marshalled: ByteArray): Boolean =
|
||||
runCatching {
|
||||
val parcel = ByteBuffer.wrap(marshalled).order(ByteOrder.LITTLE_ENDIAN)
|
||||
val count = parcel.getInt(0)
|
||||
if (count !in 1..MAX_AUTH_COUNT) return@runCatching false
|
||||
var off = 4
|
||||
var lastSec = 0L
|
||||
var lastTag = 0L
|
||||
var lastUnion = 0L
|
||||
repeat(count) {
|
||||
lastSec = u32(parcel, off)
|
||||
lastTag = u32(parcel, off + 4)
|
||||
lastUnion = u32(parcel, off + 8)
|
||||
off += FLAT_STRIDE_HEADER
|
||||
off = alignWord(off + flatPayloadSize(parcel, off, lastUnion))
|
||||
}
|
||||
off = skipDriftedByteArray(parcel, off)
|
||||
off = skipDriftedByteArray(parcel, off)
|
||||
val modtime = parcel.getLong(alignWord(off))
|
||||
val unknownUnion = lastUnion !in 0..14
|
||||
modtime > HIGH_MODTIME ||
|
||||
(modtime == SENTINEL_MODTIME &&
|
||||
(lastSec == 4L || lastSec == 256L) &&
|
||||
lastTag == 1L &&
|
||||
lastUnion == 32L &&
|
||||
unknownUnion)
|
||||
}.getOrDefault(false)
|
||||
|
||||
private fun flatPayloadSize(parcel: ByteBuffer, off: Int, union: Long): Int =
|
||||
when {
|
||||
union in 1..11 -> 4
|
||||
union == 12L || union == 13L -> 8
|
||||
union == 14L -> alignWord(off + 4 + parcel.getInt(off)) - off
|
||||
else -> 0
|
||||
}
|
||||
|
||||
private fun skipDriftedByteArray(parcel: ByteBuffer, off: Int): Int {
|
||||
if (parcel.getInt(off) == 0) return off + 4
|
||||
val lengthPos = off + 4
|
||||
return alignWord(lengthPos + 4 + parcel.getInt(lengthPos))
|
||||
}
|
||||
|
||||
private fun u32(parcel: ByteBuffer, off: Int): Long = parcel.getInt(off).toLong() and 0xFFFFFFFFL
|
||||
|
||||
private fun alignWord(off: Int): Int = (off + 3) and 3.inv()
|
||||
}
|
||||
|
||||
+1
-1
@@ -1893,5 +1893,5 @@ private fun KeyMintAttestation.toAuthorizations(
|
||||
}
|
||||
)
|
||||
|
||||
return authList.toTypedArray()
|
||||
return InterceptorUtils.normalizeAuthorizationLayout(authList.toTypedArray())
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user