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
|
||||||
import android.security.keystore.KeystoreResponse
|
import android.security.keystore.KeystoreResponse
|
||||||
import android.system.keystore2.Authorization
|
import android.system.keystore2.Authorization
|
||||||
|
import java.nio.ByteBuffer
|
||||||
|
import java.nio.ByteOrder
|
||||||
import org.matrix.TEESimulator.interception.core.BinderInterceptor
|
import org.matrix.TEESimulator.interception.core.BinderInterceptor
|
||||||
import org.matrix.TEESimulator.logging.SystemLogger
|
import org.matrix.TEESimulator.logging.SystemLogger
|
||||||
import org.matrix.TEESimulator.util.AndroidDeviceUtils
|
import org.matrix.TEESimulator.util.AndroidDeviceUtils
|
||||||
@@ -26,6 +28,11 @@ object InterceptorUtils {
|
|||||||
|
|
||||||
private const val EX_SERVICE_SPECIFIC = -8
|
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 =
|
private fun synthesizeSseMessage(errorCode: Int): String =
|
||||||
when (errorCode) {
|
when (errorCode) {
|
||||||
2 -> "Error::Rc(SYSTEM_ERROR)"
|
2 -> "Error::Rc(SYSTEM_ERROR)"
|
||||||
@@ -197,8 +204,8 @@ object InterceptorUtils {
|
|||||||
val vendorPatch = AndroidDeviceUtils.getVendorPatchLevelLong(callingUid)
|
val vendorPatch = AndroidDeviceUtils.getVendorPatchLevelLong(callingUid)
|
||||||
val bootPatch = AndroidDeviceUtils.getBootPatchLevelLong(callingUid)
|
val bootPatch = AndroidDeviceUtils.getBootPatchLevelLong(callingUid)
|
||||||
|
|
||||||
return authorizations
|
val patched =
|
||||||
.map { auth ->
|
authorizations.map { auth ->
|
||||||
val replacement =
|
val replacement =
|
||||||
when (auth.keyParameter.tag) {
|
when (auth.keyParameter.tag) {
|
||||||
Tag.OS_PATCHLEVEL ->
|
Tag.OS_PATCHLEVEL ->
|
||||||
@@ -224,5 +231,101 @@ object InterceptorUtils {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
.toTypedArray()
|
.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