Add checkOperationAuthorizations to the AuthorizeCreate chain so the
interceptor rejects operations whose parameters are incompatible with
the key, matching real KeyMint HAL behavior:
- block mode, padding, digest, and RSA-OAEP MGF digest must each be a
subset of the key's authorized set;
- AES-GCM rejects a requested MAC length below the key minimum;
- RSA-OAEP requires a digest.
Add the four backing KeyMint error codes (INCOMPATIBLE_BLOCK_MODE,
INCOMPATIBLE_PADDING_MODE, INCOMPATIBLE_DIGEST, INVALID_MAC_LENGTH) to
KeystoreErrorCodes, resolved at runtime with AOSP-correct fallbacks.
The check reads the raw request params (AuthorizeCreate.check is called
with parsedParams), so no op-param construction change is needed, and
execution is unaffected: our SoftwareOperation already runs GCM (128-bit
tag) and OAEP.
Extend our grant model to the updateSubcomponent path. A grantee holding
a grant with the UPDATE access-vector bit can now update the cert/chain
subcomponent of the owner's synthetic key; previously a Domain.GRANT
updateSubcomponent fell through to the real keystore2 and failed for
synthetic keys.
Reuse the existing grant machinery (resolveGrant / softwareGrants /
getGeneratedKeyResponse) already backing the getKeyEntry Domain.GRANT
read, gated on KEY_PERMISSION_UPDATE (0x80) instead of GET_INFO (0x4).
Extract a shared updateResponseSubcomponent() helper so the grant and
owner paths share one cert-swap plus re-persist body.
Add a boot_props_mode control so global ro.boot.* property spoofing
can be tuned per device.
BootStateManager reads /data/adb/tricky_store/boot_props_mode
(auto/force/disable). In auto, Oplus-family devices
(OnePlus/OPPO/realme/Oplus) skip the global ro.boot.* spoof so vendor
TEE services such as ultrasonic fingerprint calibration keep working.
AndroidDeviceUtils routes the bootKey/bootHash resetprop writes
through a setBootProperty() gate honoring the same mode; the forged
value is still persisted and returned, so attestation is unaffected.
Tradeoff: in auto, skipping ro.boot.* leaves direct system-property
boot-state checks truthful on Oplus devices (compatibility over
stealth). Latent, not a reported issue.
getAttestVersion/getKeymasterVersion now read the device VINTF manifest
and derive attestationVersion=keymasterVersion=aidl*100 (KeyMint) or the
legacy HIDL Keymaster pair, so the forged attestation certificate's
version matches the device's declared IKeyMintDevice interface. This
removes the version MISMATCH that Duck Detector flags (attested 400 vs
declared 300). Falls back to the existing cache/SDK-map/400 chain when
VINTF is absent or unreadable.
Refs #40
- Freeze changelog.md and update.json at v6.0.1-282
- Add refactoring note to changelog top
- Untrack local planning files (CLAUDE.md, bucket/) via gitignore
The probe's signSessionOk requires resultCode==0 (SoterCapabilityProbe
.kt:107), so writeInt(SOTER_OK) for initSigh is not optional. Cite the
probe line in the comment so a future cleanup does not drop it on the
false "detector ignores resultCode" belief, true only for the export
path.
The supervisor mounted the forge on the happy path but could not
re-attempt: mount() returned silently on inject/handshake failure, and a
live-but-uninjected binding never died to trigger a rebind, stranding the
forge for the life of that soterserver process (audit F1).
Route every unmounted outcome through scheduleRetry(): inject failure,
post-inject handshake-null, and register failure now schedule a re-bind
instead of returning. Add onNullBinding (F2) and exponential backoff
capped at 30s, reset on a clean mount (F3). register() now returns
whether the transact succeeded so mount() retries on a false reply (F4);
existing keystore callers ignore the new return.
Audit remediation. compileDebugKotlin clean.
Start SoterProcessSupervisor from App.main() so the Layer-A forge
mounts on the on-demand soterserver process. prepareEnvironment() now
returns the system Context it previously discarded; main() hands it to
start() after keystore init and before Looper.loop(). start() runs on
its own HandlerThread and returns at once, so neither the blocking
keystore loop nor the message loop is affected.
The stub types ActivityThread.getSystemContext() as a bare ContextImpl,
so the Context cast warns CAST_NEVER_SUCCEEDS; the real ContextImpl does
extend Context, so it is runtime-safe and the warning is suppressed.
Checkpoint 10.W. compileDebugKotlin clean.
The release build is meant to nuke any debug NDJSON directory on
install, but the sweep never ran. customize.sh keyed the debug vs
release decision on unzip's exit code:
if unzip -qqjo "$ZIPFILE" "diag.sh" ...; then ...
Info-ZIP returns 11 when the entry is absent, but the busybox/toybox
unzip in the Magisk/KSU install environment exits 0, so on a release
ZIP (which correctly omits diag.sh) the branch was wrongly taken and
the rm -rf in else never fired.
Detect presence by the extracted file instead: run unzip, then test
[ -f "$MODPATH/diag.sh" ]. Robust to any unzip implementation.
Injection into the soterserver app (platform_app domain, per recon)
needs ptrace under SELinux enforcing. Add the grant mirroring the
keystore one, in the base rule so it applies to both variants:
allow crash_dump platform_app process *
The per-UID NDJSON write grant is debug-only: appended for debug
builds in build.gradle.kts's isDebug doLast, mirroring the existing
keystore media_rw_data_file grant. Keeping it out of the base rule
stops an external-storage write from leaking into release.
No soter_server SELinux type exists; platform_app is the soterserver
app domain. Runtime policy (KSU/magiskpolicy) grants this past the
compile-time neverallow; on-device avc verification is 10.V.
Checkpoint 10.C.
soterserver is Intent-bound and on-demand, so the one-shot pidof +
inject the always-alive keystore path uses never lands. Bind the
service to both poke its start and obtain the ISoterService binder
(the identity the native MITM registry keys on), inject
libTEESimulator.so, confirm the landing via the 0xdeadbeef handshake,
then register the forge.
Re-binds and re-injects on every respawn instead of exiting like the
keystore one-shot. Runs on its own HandlerThread so it never stalls
keystore init or the daemon looper; lifecycle logging is debug-gated.
Not yet wired into App.kt (that is 10.W).
Checkpoint 10.B.
Forge healthy com.tencent.soter.soterserver.ISoterService AIDL replies
from inside the injected soterserver process so the SOTER capability
probe reads available=true / damaged=false on a bootloader-unlocked
device whose SOTER TA can no longer use its factory ATTK.
Hardcodes the 13 obfuscation-stable transaction codes (R8 stripped the
Stub) and fills the 5 parcelable payloads with detector-valid values:
the export blob is a little-endian length-framed SOTER pubkey envelope
that the SDK's retrieveJsonFromExportedData parses to a non-null model.
Every request and forged reply is captured to per-UID NDJSON,
debug-gated.
Checkpoints 10.A (forge) and 10.M (reply marshalling).
c2552ba gated AUTO forge on isRsaAttestable, but the probe minted its
key without setIsStrongBoxBacked, so it measured only the TEE. A device
whose TEE provisions an RSA attestation key while its StrongBox cannot
(OnePlus PJZ110, Android 16) had StrongBox RSA requests PATCHed against
the real keystore, which has no StrongBox attestation key and returns
-74 (ATTESTATION_KEYS_NOT_PROVISIONED).
Probe each (algorithm, security-level) pair independently and have
dispatch consult the verdict matching the request's security level, for
both RSA and EC. StrongBox-incapable requests forge; capable ones keep
the genuine TEE chain via PATCH.
Refs #37
NDK 29's Clang-21 libc++ makes libTEESimulator.so reference
__cxa_init_primary_exception, which the platform libc++ inside
keystore2 does not export. The injected lib resolves its C++ ABI
symbols against the target process at dlopen time, so injection
failed with "cannot locate symbol" on every retry. keystore2 ran
unhooked and every app saw the raw TEE chain (KeyAttestation showed
the real unlocked bootloader; per-UID NDJSON never created).
libc++ began emitting that symbol from std::exception_ptr
construction in Clang 19, so 27.3 (Clang 18) is the last toolchain
that builds a loadable lib. The exception_ptr machinery enters via
the AOSP/binder stub headers, not module code.
Verified on device: lib injects (3 maps in keystore2), KeyAttestation
generateKey -> PATCH with deviceLocked=true, verifiedBootState=Verified.
NDK 27.3.13750724's sysroot is corrupted (missing sys/cdefs.h and the
aarch64 asm headers), breaking the native-certgen build. NDK 29 is
installed and healthy, so move the toolchain to it.
Define load-bearing terms on first use, cut marketing phrasing, and
remove every em dash. Add an ASCII flow diagram to "How it works".
Trim credits to JingMatrix, ring, fatalcoder524, and huguangares.
Reword the tagline and update the build requirement to NDK 29.
Remove orphaned native logging that nothing reached:
- The /sdcard/Download zip dump (NativeCertGen.dump and the dumpLogs
JNI, dump_logs_inner, dump.rs, pub mod dump), superseded by the
diag.sh export.
- The verbose-marker helpers (sysfs.rs, pub mod sysfs); the manual
.verbose toggle still works via mod.rs::init's inline check.
Drop the now-unused direct deps zip and libc and the orphaned jstring
import. cargo ndk build is warning-clean.
Move debug diagnostics off /data/local/tmp/teesim to
/data/media/0/TEESimulator (visible at /sdcard/TEESimulator), so users
can pull them without a root explorer. The logging code runs in the
keystore SELinux domain, so a debug-only media_rw_data_file grant plus
a debug-only diag.sh fragment gate the plane: diag.sh's presence is the
signal service.sh (setup) and action.sh (export) test. customize.sh
extracts diag.sh on debug installs or sweeps the dir on release, since
the release keystore domain cannot remove it itself.
Replace the per-call .bin parcel dumps (a fresh undecodable file per
generateKey) with one NDJSON record per event on the UID's own file,
carrying decoded fields plus the raw parcel as base64 for the offline
parsers. computeIfAbsent makes per-UID writer creation atomic.
The RSA capability probe cached any first-call failure for the process
lifetime via by-lazy plus a catch-all false, so a transient keystore
hiccup could freeze the device into forging an attestation the real TEE
could serve, silently re-creating the issue #37 regression with no
self-heal.
Memoize only a definitive verdict: a successful probe, or a permanent
KeyStoreException per the framework's own isTransientFailure(). Transient
and non-keystore failures fail open, reporting the device attestable so
dispatch PATCHes the genuine chain, and re-probe on the next read. An
AtomicBoolean guard keeps at most one probe in flight with no lock held
across the keygen. Delete the probe key best-effort in finally.
Refs #37
v282 forged every AUTO attestation that carried a challenge, so a
strict app that validates attestation server-side, such as Kraken,
rejected the software-forged chain where it accepted a patched
real-TEE chain, breaking login. The trigger was algorithm-blind: the
AUTO capability probe only mints an EC key, so it could not tell an
EC-capable TEE from one that cannot provision RSA attestation keys.
Add an isRsaAttestable probe and forge AUTO attestation only for RSA
the real TEE cannot provision. EC and RSA-capable devices keep their
genuine TEE chain via PATCH, restoring the v280 behavior strict apps
depend on while preserving the RSA red fix on incapable devices.
Refs #37
OperationInterceptor rejected non-AEAD updateAad with INVALID_TAG
unconditionally, while SoftwareOperation's VendorQuirks gate returns
success on Samsung and Xiaomi-MTK. On those devices the real-key and
forged-key paths disagreed, and the genuine TEE accepts the call, so
the inconsistency fingerprinted the injection layer through Duck
Detector's operation error-path probe.
Apply the same gate to the real-op path: a void success reply where
nonAeadUpdateAadSucceeds(), else the INVALID_TAG reply. Promote
VendorQuirks to internal so both paths share one decision.
Refs #36
Plain attestation (Use-attest-key OFF, challenge present) on an AUTO-mode target was routed to PATCH, deferring to the real TEE. The AUTO probe (checkTeeFunctionality) only proves the device can mint one EC key, so devices that cannot attest RSA or device-ID, or whose patched chain fails RSA verify, surfaced as KeyAttestation reds (ATTESTATION_KEYS_NOT_PROVISIONED/-49, BLOCK_TYPE_IS_NOT_01).
Forge these from the keybox instead, gated on isAutoMode + attestationChallenge, matching the attest-key-ON path that already yields a green Google-rooted chain. Non-attestation keys still pass through to real hardware, so KeyDetector hardware-backed checks are unaffected.
Verified offline against real FORGE captures with scripts/keyatt_conformance.py: uid10389/uid10154 chains are GREEN and the root SPKI byte-matches GOOGLE_ROOT_PUBLIC_KEY.
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.
The packaging task rewrites update.json to gitCommitCount on every
build; this records the v6.0.1-277 artifacts and supersedes the manual
271 bump made before the build counter was understood.
At the attest-key signing instant, log signer key algorithm, served
leaf algorithm, chain depth, and issuer (debug, targeted uid) so an EC
attest-key run pins the mismatched edge of the two-root chain.
Bucket a16-ec-attestkey-red, task T01.
A persisted record whose private-key algorithm disagrees with its
served leaf public key (EC private under an RSA leaf) makes every
signature fail as DATA_TOO_LARGE_FOR_MODULUS: the A16 EC two-root.
Require the two to match on restore and drop the record otherwise, so
the next generateKey rebirths a coherent key. Awaiting an EC device
capture to confirm the red originates from a restored record.
Bucket a16-ec-attestkey-red, task T01.
The A16 test device's KeyMint HAL reports attestVersion 100 (KeyMint
1.0); the lazy cache stored that and it shadowed the map's BAKLAVA->400
in getAttestVersion. fetchAttestationData now caches
AndroidDeviceUtils.aospAttestVersion (attestVersionMap[SDK_INT]),
falling back to the parsed device value only when the SDK is unmapped,
so the forge presents the AOSP-correct 400. attestVersionMap unchanged.
Bucket a16-ec-attestkey-red, task T02.
keystore2 replaces a key when generateKey reuses an alias. Mirror that:
drop any cached chain for the alias so a later getKeyEntry serves the
current key, not a stale FORGE from a prior generation (an
attest-key-mode leaf cached, then re-generated without an attest key).
Log each cert chain the module hands the app so an attestation
verification failure is provable from the per-UID log, not inferred.
- formatChainVerification verifies every edge of a produced chain and
reports RSA signature-vs-modulus sizes (the DATA_TOO_LARGE condition).
- formatChainKeys and logServedChain record the chain served back on
each getKeyEntry, keyed by alias, since the app reassembles its chain
from the leaf alias plus the attest-key alias.
Debug-build only, gated by isUidLogged.
Un-targeted privileged callers (e.g. KeyAttestation via Shizuku) have
their attest-key and device-id generateKey requests force-forged, but
getKeyEntry blanket-skipped those UIDs before the owned-key lookup, so
the framework's attestKeyAlias resolution in
AndroidKeyStoreKeyPairGeneratorSpi.initialize() returned KEY_NOT_FOUND
and surfaced as "Invalid attestKeyAlias".
Let getKeyEntry reach the owned-key lookup for skipped UIDs; a non-owned
key still skips post-processing so an un-targeted app's real key is
never patched.
Decide the effective generateKey params once via .let when the caller lacks gen_unique_id / REQUEST_UNIQUE_ID_ATTESTATION, instead of mutating var params/parsedParams deep in handleGenerateKey and re-parsing KeyMintAttestation a second time. isAttestKeyRequest now derives from the final parsedParams, closing the staleness flagged in PR #27 review r3308356496.
Behavior is unchanged: no gate between the parse and the old strip site reads INCLUDE_UNIQUE_ID, and the && short-circuits so the permission lookups still run only when the tag is present.
Per-UID dossier logs moved from /data/adb/tricky_store/logs to
/data/local/tmp/teesim, beside the .bin dumps, so the whole debug
trail comes off the device in one `adb pull /data/local/tmp/teesim/`
with no root and no /sdcard hop.
The release purge now sweeps .log/.log.1 from the diagnostic dir and
keeps legacy sweeps of both old locations (loose /data/local/tmp and
the module config dir) so upgrading to a release build leaves nothing
behind. Repoint package.sh --clear-logs to the new path.
The attestation dossier only fired on a successfully produced chain, so
the StrongBox/BHIM failures left nothing on the per-UID plane and had to
be reconstructed from marshalled .bin dumps offline. Add three records,
all debug- and target-gated like the existing dossier:
- keybox-pick: which keybox signs the forge (requested algo, exact match
vs EC fail-safe, signer subject) -- makes an EC-only-keybox RSA
fallback visible instead of silent.
- forge-fail: emit the failure reason on the per-UID plane when a forge
throws (e.g. ATTESTATION_KEYS_NOT_PROVISIONED), paired with dispatch.
- auth-shape: the emitted authorization list (count, ordered tags,
per-auth securityLevel) -- the surface the duck generate-mode parcel
fingerprint stride-walks, readable without offline decode.
The asymmetric and symmetric result dumps wrote a single fixed
filename (teesim-gen-mode-asym.bin / -sym.bin), so each forge
overwrote the previous one and only the final reply survived a
capture -- which is why a tester's zip showed one app's good chain
while the failing chain was already gone.
Tag both dumps with uid and tx, matching the request dumps, so every
forged chain is retained and correlatable with its request.
The forge keybox selector matched the requested algorithm exactly and
threw -75 ATTESTATION_KEYS_NOT_PROVISIONED on a miss, while the patch
path already falls back to any usable key (EC preferred). An RSA
ATTEST_KEY request on an EC-only keybox therefore never rooted: the
caller's attest-key chain could not reach the Google root and verifiers
reported "unknown certificate".
Fall back to getAnyAttestationKey when no algorithm-matching keybox
exists. An EC attestation key validly ECDSA-signs an RSA-subject leaf,
so the EC keybox roots the RSA forge. No-op when the keybox is dual.
Two gaps in attestation generation surfaced by a tester's Key Attestation
app runs on build 259.
Device-ID attestation (IMEI/serial) via Shizuku arrives as a privileged
UID (shell/system) absent from target.txt, so it was skipped and the real
TEE rejected it with CANNOT_ATTEST_IDS (-66). Stop skipping requests that
carry device-ID tags, and force the forge path for them (the real TEE
cannot attest IDs, so there is no chain to patch). The permission gate
still rejects ordinary apps, mirroring a real device.
'Use attest key' produced WRONG_PUBLIC_KEY_TYPE: a reused persistent attest
key is designated by KEY_ID with a null alias, so the lookup missed and the
leaf was silently re-rooted under the keybox, double-rooting the chain the
caller assembles. Resolve the attest key by KEY_ID as well as alias, and
refuse to emit a leaf rather than fall back to the keybox when a designated
attest key cannot be resolved.
The debug-only .bin dumps wrote loose into /data/local/tmp, cluttering a
directory shared with every other tool. Route both writers through a
shared DIAGNOSTIC_DIR (/data/local/tmp/teesim) with mkdir-on-write, and
extend the release purge to sweep the new folder plus any loose leftovers
from older debug installs.
An EC-only Google keybox could not re-root an RSA-keyed attestation: the keybox lookup asked for an RSA signing key, got null, and threw. patchCertificateChain caught the throw and returned the original chain untouched, leaking the device's real unlocked Root of Trust for RSA keys while EC keys patched correctly.
Fall back to any available keybox key (preferring EC) and sign the patched leaf with the keybox key's own algorithm rather than the original leaf's. A leaf's signature algorithm is independent of its subject key, so the RSA leaf re-signs validly under the EC keybox and the chain still roots to the Google keybox with the forged, locked RoT.
Add KeyBoxManager.getAnyAttestationKey; drop the now-dead sigAlgName param and normalizeSignatureAlgorithm helper.
Add a debug-only per-UID diagnostic plane gated on BuildConfig.DEBUG.
For apps in target.txt it records every keystore interaction and the
forged attestation it produces to teesim-uid-<uid>.log: decoded cert
chain (FORGE and PATCH paths), key params, keybox, and prop sources,
with the calling UID threaded through the C++ binder hook and Rust
certgen. Release builds stay silent (R8 strips the write plane and the
runtime gate short-circuits). Adds --clear-logs to package.sh.
The serial log added previously lived in parseKeysFromXml, which
getAttestationKey runs only on a cache miss -- so it emitted at most
once per boot and scrolled off the buffer before it could be read.
Move it into getAttestationKey so the keybox attestation cert serials
are logged on every fetch, on the live native cert-gen path.
Verified on device: "Using RSA keybox keybox.xml; attestation cert
serials (hex): ..." now prints on each forge.
Mirror Duck Detector's OperationErrorPathProbe: real Samsung and
Xiaomi-MTK TEEs return success for updateAad on a non-AEAD operation,
while other vendors reject it with INVALID_TAG. The shim reads the same
Build identity the probe reads and answers accordingly, in both the
CryptoPrimitive default (sign/verify) and CipherPrimitive paths.
Forward hardening for the #28 detector: the prior unconditional
ServiceSpecificException throw already passes the probe on every vendor,
so this guards against stricter future probes rather than fixing a
current failure.
Emit each loaded keybox's certificate-chain serials (lowercase hex) at
parse time. A revoked or leaked keybox is then visible from logcat
alone, since Google's CRL and Duck Detector's "mass abuse" check both
match by certificate serial.
Diagnostic aid for the revoked-keybox danger in #28; the actual fix is
rotating to a non-revoked keybox.
Run the project ktfmt kotlinLangStyle formatter over app/ to bring the
tree into canonical form. Formatting only -- no logic change.
Verified semantic-neutral: ktfmt(working tree) is byte-identical to
ktfmt(committed HEAD) across all of app/src, so the prior uncommitted
WIP carried zero behavioral change.
Add a debug-only structured log line per generateKey, emitted at every
outcome (SKIP, the four REJECTs, FORWARD_HAL, FORGE, PATCH, PASSTHROUGH).
Each line carries the resolving package (via the cached
ConfigurationManager.getPackagesForUid), alias, algorithm, StrongBox
flag, the attestation tag set, and the verdict, so triaging "app X
broke" becomes a single logcat grep instead of decoding parcel dumps.
Gated on SystemLogger.isDebugBuild: release builds return before any
string is built, keeping the path silent and artifact-free. Logs to
logcat only, never to files.
The canAttestDeviceIds gate (3575c74) probed the live TEE to decide
whether to honor device-property/ID attestation. That probe is gated on
isTeeFunctional, which is false on every dead-TEE device the module
serves, so GENERATE mode rejected all such requests with
CANNOT_ATTEST_IDS, including GMS Play Integrity's hardware path, which
broke BHIM and any UPI/Play-Integrity-gated app.
Remove the gate. Device-property attestation (BRAND/MODEL/...) now forges
unconditionally, as genuine devices universally attest it. Device-ID
attestation stays governed by the pre-existing caller-permission check,
the real KeyMint rule: privileged callers get it, ordinary apps do not.
Drop the now-unused DeviceAttestationService.canAttestDeviceIds probe.
Grant-plane coherence (Android 16 incl.), Google Wallet + fingerprint
compatibility (PR #26/#27), and removal of the in-module PIF/bulletin
resolvers. Frozen at gitCommitCount 251.