fix: reorder hal-enforced auths to evade duck detector

Duck-Detector's generate-mode parser walks the reply parcel at
12-byte strides and matches (secLevel=256, tag=1, unionTag=32) at
slot[count-1]. Those bytes are actually KEY_SIZE.value=256 followed
by the next Authorization's presence flag and size header, an
emergent fingerprint from misaligned parsing, not a fake value.

Reorder toAuthorizations so PURPOSE/ALGORITHM/KEY_SIZE come first,
mirroring AOSP keymint reference HAL output. KEY_SIZE moves from
auth#4 to auth#2, so its int payload no longer lands at byte 224.
Verified across 31 fresh duckdetector probes: zero matches (was
15/36 before).

Also add gen-mode wire-byte diagnostic to InterceptorUtils and the
generate-mode entry point, debug-gated, dumping request and reply
parcels to /data/local/tmp for offline decode.
This commit is contained in:
Enginex0
2026-05-20 06:52:04 +01:00
parent be4c418893
commit 108e027a98
2 changed files with 29 additions and 4 deletions
@@ -116,12 +116,21 @@ object InterceptorUtils {
fun <T : Parcelable?> createTypedObjectReply( fun <T : Parcelable?> createTypedObjectReply(
obj: T, obj: T,
flags: Int = 0, flags: Int = 0,
diagnosticTag: String? = null,
): BinderInterceptor.TransactionResult.OverrideReply { ): BinderInterceptor.TransactionResult.OverrideReply {
val parcel = val parcel =
Parcel.obtain().apply { Parcel.obtain().apply {
writeNoException() writeNoException()
writeTypedObject(obj, flags) writeTypedObject(obj, flags)
} }
if (diagnosticTag != null && SystemLogger.isDebugBuild) {
val savedPos = parcel.dataPosition()
val wire = parcel.marshall()
parcel.setDataPosition(savedPos)
val path = "/data/local/tmp/teesim-$diagnosticTag-${System.nanoTime()}.bin"
runCatching { java.io.File(path).writeBytes(wire) }
SystemLogger.debug("[$diagnosticTag] reply len=${wire.size} path=$path")
}
return BinderInterceptor.TransactionResult.OverrideReply(parcel) return BinderInterceptor.TransactionResult.OverrideReply(parcel)
} }
@@ -416,6 +416,14 @@ class KeyMintSecurityLevelInterceptor(
} }
private fun handleGenerateKey(txId: Long, callingUid: Int, callingPid: Int, data: Parcel): TransactionResult { private fun handleGenerateKey(txId: Long, callingUid: Int, callingPid: Int, data: Parcel): TransactionResult {
if (SystemLogger.isDebugBuild) {
val savedPos = data.dataPosition()
val req = data.marshall()
data.setDataPosition(savedPos)
val path = "/data/local/tmp/teesim-gen-mode-req-uid${callingUid}-tx${txId}-${System.nanoTime()}.bin"
runCatching { java.io.File(path).writeBytes(req) }
SystemLogger.debug("[gen-mode-req] uid=$callingUid txId=$txId len=${req.size} path=$path")
}
val oversized = data.dataSize() > MAX_ALIAS_LENGTH val oversized = data.dataSize() > MAX_ALIAS_LENGTH
return runCatching { return runCatching {
@@ -615,7 +623,7 @@ class KeyMintSecurityLevelInterceptor(
TeeLatencySimulator.simulateGenerateKeyDelay(parsedParams.algorithm, System.nanoTime() - genStartNanos) TeeLatencySimulator.simulateGenerateKeyDelay(parsedParams.algorithm, System.nanoTime() - genStartNanos)
} }
return InterceptorUtils.createTypedObjectReply(metadata) return InterceptorUtils.createTypedObjectReply(metadata, diagnosticTag = "gen-mode-sym")
} }
val keyData = if (NativeCertGen.isAvailable && attestationKey == null) { val keyData = if (NativeCertGen.isAvailable && attestationKey == null) {
@@ -688,7 +696,7 @@ class KeyMintSecurityLevelInterceptor(
TeeLatencySimulator.simulateGenerateKeyDelay(parsedParams.algorithm, System.nanoTime() - genStartNanos) TeeLatencySimulator.simulateGenerateKeyDelay(parsedParams.algorithm, System.nanoTime() - genStartNanos)
} }
return InterceptorUtils.createTypedObjectReply(response.metadata) return InterceptorUtils.createTypedObjectReply(response.metadata, diagnosticTag = "gen-mode-asym")
} }
private fun generateAttestedKeyPairNative( private fun generateAttestedKeyPairNative(
@@ -1237,15 +1245,23 @@ private fun KeyMintAttestation.toAuthorizations(
} }
} }
// HAL-enforced authorization ordering mirrors AOSP keymint reference
// HAL output: PURPOSE → ALGORITHM → KEY_SIZE → curve → mode params →
// exponent. Duck-Detector's generate-mode fingerprint walks the reply
// parcel at 12-byte parser strides and matches when slot[count-1] reads
// (secLevel=256, tag=1, unionTag=32) — which emerges in the original
// order because EC P-256's KEY_SIZE.value=256 lands at byte 224 (auth#4
// value field). Reordering moves KEY_SIZE to auth#2, so byte 224 reads
// a different field entirely.
this.purpose.forEach { authList.add(createAuth(Tag.PURPOSE, KeyParameterValue.keyPurpose(it))) }
authList.add(createAuth(Tag.ALGORITHM, KeyParameterValue.algorithm(this.algorithm))) authList.add(createAuth(Tag.ALGORITHM, KeyParameterValue.algorithm(this.algorithm)))
authList.add(createAuth(Tag.KEY_SIZE, KeyParameterValue.integer(this.keySize)))
if (this.ecCurve != null) { if (this.ecCurve != null) {
authList.add(createAuth(Tag.EC_CURVE, KeyParameterValue.ecCurve(this.ecCurve))) authList.add(createAuth(Tag.EC_CURVE, KeyParameterValue.ecCurve(this.ecCurve)))
} }
this.purpose.forEach { authList.add(createAuth(Tag.PURPOSE, KeyParameterValue.keyPurpose(it))) }
this.blockMode.forEach { authList.add(createAuth(Tag.BLOCK_MODE, KeyParameterValue.blockMode(it))) } this.blockMode.forEach { authList.add(createAuth(Tag.BLOCK_MODE, KeyParameterValue.blockMode(it))) }
this.digest.forEach { authList.add(createAuth(Tag.DIGEST, KeyParameterValue.digest(it))) } this.digest.forEach { authList.add(createAuth(Tag.DIGEST, KeyParameterValue.digest(it))) }
this.padding.forEach { authList.add(createAuth(Tag.PADDING, KeyParameterValue.paddingMode(it))) } this.padding.forEach { authList.add(createAuth(Tag.PADDING, KeyParameterValue.paddingMode(it))) }
authList.add(createAuth(Tag.KEY_SIZE, KeyParameterValue.integer(this.keySize)))
if (this.rsaPublicExponent != null) { if (this.rsaPublicExponent != null) {
authList.add(createAuth(Tag.RSA_PUBLIC_EXPONENT, KeyParameterValue.longInteger(this.rsaPublicExponent.toLong()))) authList.add(createAuth(Tag.RSA_PUBLIC_EXPONENT, KeyParameterValue.longInteger(this.rsaPublicExponent.toLong())))
} }