Duck-Detector's grant-domain probes generate an attested key, then
reach it through a second access plane -- IKeystoreService.grant() then
getKeyEntry(Domain.GRANT, grantId) -- and compare the certificate
chains. We synthesized the owner key but never virtualized the GRANT
plane, so grant reads fell through to the real keystore2, which has no
record of the synthetic key. That single fall-through produced six RED
rows.
Virtualize the plane so every access path returns the same synthesized
KeyEntryResponse:
- SoftwareGrant state model in the shim companion: issue/resolve/
revoke/purge, caller-bound and access-vector-aware (Change 1).
- grant/ungrant/getKeyEntry(GRANT) handlers in Keystore2Interceptor.
resolveGrant() enforces caller-binding (non-grantee -> KEY_NOT_FOUND,
PR #57 probe 4) and the GET_INFO=0x4 access-vector gate (missing ->
PERMISSION_DENIED, PR #57 probe 3); a valid read returns the owner's
exact KeyEntryResponse for a coherent chain (Change 2).
- Purge grants on key teardown and clearAll, so grants die with the
key and re-key orphans them -- matching real keystore2 (Change 3).
The Domain.GRANT read is resolved before the package-scoped
shouldSkipUid filter: isolated grantees (bindIsolatedService) have no
package mapping and would otherwise be dropped to the real keystore2,
leaving three grant rows Unavailable. Caller-binding in resolveGrant()
is the real access gate, mirroring keystore2's grantee+id row keying.
Verified on-device (generate mode, build #237): all four grant rows
clean, TEE tamper score 28 -> 18, zero adjacent regression.
Refs Phase 9 .omc/plans/tee-fingerprint-phase-9-grant-plane-coherence.md