Purpose & scope: This module gives you a complete, defensible workflow to design, test, and audit Android device/app integrity. You’ll learn how Play Integrity, Android Key Attestation, and local integrity signals (root/emulator/debug/instrumentation checks) should be implemented, how to validate them server-side, how to interpret vendor claims (“we bypassed Play Integrity / RootBeer”), and how to collect forensic-grade evidence. The emphasis is on defense, verification, and repeatable lab testing—not on facilitating misuse against production systems.
12.0 Learning objectives
By the end you will be able to:
- Explain what each integrity mechanism actually provides (and what it doesn’t).
- Design a nonce-based, server-validated attestation flow for Play Integrity and Key Attestation.
- Parse and validate attestation tokens; bind results to package name, signing cert digest, and device integrity flags.
- Correlate client-side heuristics (root/emulator/debug/Frida detection) with server policy and telemetry.
- Evaluate third-party “bypass” claims with a forensic checklist and reproducible lab steps.
- Produce developer-ready remediation plans and operational runbooks for integrity failures.
12.1 Integrity building blocks — what each does
| Mechanism | Who issues proof? | What it proves (at best) | Where to enforce |
|---|---|---|---|
| Play Integrity | Google Play services | Signals about device integrity (e.g., basicIntegrity, ctsProfileMatch), app identity (package, cert digest), timestamp | Server-side after verifying signature and nonce |
| Android Key Attestation | Device Keymaster/TEE | A key was generated in secure hardware; metadata binds to app/device and a challenge (nonce) | Server-side after chain validation |
| Local checks (RootBeer, custom checks, anti-Frida) | The app itself | Heuristics about environment (root, debug, tamper) — bypassable on owned devices | Use as telemetry → Server policy decides |
Principle: Client is untrusted. Treat client-side checks as signals, not final gates. All final decisions (allow, step-up, block) happen on the server using validated tokens and policy.
12.2 Threat models & implications
- Owned device adversary (root, custom kernel, KernelSU/Magisk, namespaces): can tamper userland, hook methods, manipulate syscall surfaces. Expect client-only checks to be bypassed; rely on server-validated proofs and backend policies.
- Emulator adversary: can vary build props; some signals differ on emulators (e.g., Play Integrity may fail or provide “basic only”). Use emulators for dev/test but validate on real hardware for final decisions.
- MitM: Pinning reduces network interception, but pinning ≠ integrity. Use pinning and attestation.
- Supply-chain: If an attacker re-signs an APK, server must reject tokens where package signing digest doesn’t match your release key.
12.3 Server-validated attestation flow (canonical pattern)
- Client → Server:
GET /attest/nonce
- Server generates cryptographically random nonce, stores it with TTL (e.g., 60–120 s), binds it to account/session + app version + device ID (hash).
- Client (device):
- Calls Play Integrity or Key Attestation API with the nonce.
- Receives attestation token (opaque/JWT/CBOR depending on API).
- Client → Server:
POST /attest/verify { nonce, token } - Server (validation steps):
- Verify signature/chain against platform root (Google / device Keymaster).
- Check nonce equals the one it issued and is unused + fresh.
- Check app identity:
packageNameand signing certificate digest (SHA-256) match your release manifest. - Examine integrity flags:
ctsProfileMatch,basicIntegrity, advice/signal bits. - Check timestamp is within skew window (e.g., ±2 min) and token age ≤ policy.
- Persist result with trace ID; optionally bind to device public key (if using Key Attestation / device-binding).
- Decide policy (allow, step-up auth, deny, flag) based on signals + risk.
Never accept client decisions (“device is fine”) without server validation.
12.4 What’s inside attestation tokens (conceptual map)
Field names vary by API/version; map your implementation to current docs. Typical concepts:
nonce: must match exactly what the server issued.timestamp/evaluationType: clock & evaluation context.appPackageNameandapkCertificateDigestSha256: bind to your identity.ctsProfileMatch/basicIntegrity/ integrity verdicts: device checks; policy-dependent.- Device model / OS version fields (sometimes): informational; use with care.
- Signature / certificate chain: used to verify authenticity.
Validation outcome should be binary (accept/deny) plus structured reasons (flags). Store the full token (or a hash + secure reference) for audits.
12.5 Policy design — how to use results
Create a risk matrix combining attestation results with business context:
| Integrity result | Example policy for banking |
|---|---|
ctsProfileMatch = true (and identity matches) | Normal operations allowed. |
ctsProfileMatch = false, basicIntegrity = true | Step-up: MFA or limited features; allow low-risk reads, block high-risk transfers. |
Both ctsProfileMatch=false & basicIntegrity=false | Deny sensitive actions; allow customer support flows to re-enroll devices; record telemetry. |
| Nonce mismatch / token expired | Deny outright; investigate possible replay/manipulation. |
| Package/cert digest mismatch | Deny and flag repackaging attempt; require reinstall from trusted store. |
Tip: Start with soft enforcement (telemetry + step-up) and graduate to hard blocks once false positives are well-understood.
12.6 Local integrity signals (client heuristics)
Client heuristics are valuable signals but not authoritative:
- Root indicators: presence of
su, mount checks,Build.TAGS(test-keys), known package names (magisk), writable system dirs, SELinux permissive states. - Debug / tamper:
Debug.isDebuggerConnected(),ptraceattempts, anti-Frida heuristics (library/process scans), checksum verification ofclasses.dex/libs. - Emulator hints:
ro.kernel.qemu, hardware fingerprints, known files, device model patterns. - Hooking frameworks: suspicious loaded libs, modified
linkerbehavior, code-sign anomalies.
Usage pattern: compute these signals locally → package into an integrity report → send to server alongside a validated attestation token. The server correlates signals over time (device reputation).
12.7 Kernel & privilege escalation considerations (KernelSU/Magisk/namespaces)
Advanced adversaries may use custom kernels (e.g., KernelSU), escalated shell, or namespaces to hide artifacts and constrain detection surfaces:
- Implications: Local probes (file existence, process lists) can be falsified or hidden; syscall behavior can be mediated; user namespaces may isolate visible mounts.
- Defensive posture: Assume local probes are bypassable. Attestations validated off-device and server-bound are your trust anchor. Combine with behavioral analytics (impossible travel, unusual device churn, anomalous transaction patterns).
- Telemetry: Record kernel/build fingerprints (from token/device props if available), app version, and anomaly counts; use to tune policy rather than trust them outright.
12.8 Evidence model — proving integrity (and bypass claims)
When a vendor claims a bypass, require reproducible artifacts:
- Exact APK tested + SHA-256.
- Device profile: model, Android version, kernel version, bootloader locked state.
- Attestation tokens (base64/raw) + nonces used (timestamps).
- Proxy/PCAP captures and
adb logcatwith timestamps. - Script/tool versions (Frida/Objection if instrumentation was involved).
- Steps to reproduce in a lab + expected/observed server decision.
You then validate server-side by re-verifying tokens and matching nonce issuance logs (should show single-use and freshness). If your server rejected a valid token or accepted an invalid one, adjust implementation/policy.
12.9 Server validation — reference logic (pseudo-code)
Illustrative only; adapt to your API’s exact token format.
Python-like pseudo-code
def verify_play_integrity(token_b64: str, issued_nonce: str, ctx: RequestContext) -> VerificationResult:
# 1. Parse & verify signature chain to Google root / platform root
jwt = parse_and_verify(token_b64) # raises if signature invalid
claims = jwt.claims
# 2. Nonce and freshness
if claims["nonce"] != issued_nonce or nonce_is_used_or_expired(issued_nonce, ctx.session_id):
return deny("nonce_invalid")
# 3. App identity checks
if claims["packageName"] != ctx.expected_package:
return deny("package_mismatch")
if sha256_b64(claims["apkCertificateDigestSha256"]) != ctx.expected_cert_sha256:
return deny("cert_digest_mismatch")
# 4. Integrity flags (policy)
if not claims.get("ctsProfileMatch", False):
return step_up_or_deny("cts_failed")
# 5. Timestamp / skew window
if abs(now_utc() - claims["timestampMs"]/1000.0) > MAX_SKEW_SECONDS:
return deny("stale_token")
# 6. Log & return
trace_id = persist_verification_artifacts(ctx.user_id, claims, token_b64)
return allow(trace_id)
Node-like pseudo-code
async function verifyKeyAttestation(certChain, challenge, ctx) {
if (!verifyCertChain(certChain, PLATFORM_ROOTS)) return deny("bad_chain");
const leaf = parseLeaf(certChain);
if (!timingSafeEqual(leaf.challenge, challenge)) return deny("nonce_mismatch");
if (!equal(leaf.packageName, ctx.expectedPackage)) return deny("pkg_mismatch");
if (!equal(leaf.apkDigestSha256, ctx.expectedApkDigest)) return deny("apk_digest_mismatch");
return allow(persistArtifacts(ctx, leaf));
}
Key properties: nonce single-use; strict package & certificate binding; deterministic decision with loggable reasons.
12.10 Error handling & UX patterns
- Consistent error codes:
ATT_NONCE_INVALID,ATT_STALE,ATT_PKG_MISMATCH,ATT_CTS_FAIL. - Human-readable guidance: “Please update the app from the official store”; “Your device can’t complete integrity checks; contact support.”
- Graceful degrade: Offer read-only access or support handoff instead of hard lockouts when appropriate.
- Telemetry hook: Every error → a structured event to your SIEM with the trace ID.
12.11 CI/CD and operational controls
- Release manifest: Publish package name + signing cert SHA-256 + expected APK digest (per version) to a secure store; server pulls from here to validate tokens.
- Key rotation: Plan for signing-key rotation and pin rollovers; keep backup pins and new cert digest ready in server config.
- Canary/gradual enforcement: Start logging integrity results in monitor mode before enforcing; compare false-positive rates across device families.
- Clock source: Ensure server uses NTP-synced time; skew can break validation.
12.12 Testing matrix (lab)
Create a grid of scenarios to validate logic and UX:
| Scenario | Device | Expectation |
|---|---|---|
| Stock device, official app | Real hardware | ctsProfileMatch true → allow |
| Emulator, official app | AVD | Likely ctsProfileMatch false → step-up or limited |
| Stock device, re-signed app | Real hardware | Package digest mismatch → deny |
| Token replay (nonce reused) | Any | Server denies |
| Stale token (past TTL) | Any | Server denies |
| Rooted device (userland root) | Real hardware | Attestation likely fails; policy denies/steps-up |
| Custom kernel (KernelSU) | Real hardware | Treat local signals as unreliable; rely on attestation; expect fail/deny |
Record: device model, OS version, kernel, bootloader state, app version, APK hash, full decision trail with trace ID.
12.13 Logging & forensics schema (server)
Minimum structured fields per verification event:
{
"trace_id": "att-2025-09-27-...-001",
"user_id": "anon-123",
"session_id": "s-...",
"ts": "2025-09-27T14:11:00Z",
"app_package": "com.bank.app",
"cert_digest_sha256": "ABCD...",
"nonce_id": "n-...",
"decision": "allow|deny|step_up",
"reasons": ["cts_failed", "nonce_ok", "pkg_ok"],
"device_flags": {
"ctsProfileMatch": false,
"basicIntegrity": true
},
"client_signals": {
"root": true,
"emulator": false,
"debug": false
},
"token_age_ms": 850,
"source_ip": "redacted",
"app_version": "12.3.0"
}
Store raw tokens encrypted with strict retention; expose only trace IDs in user-facing systems.
12.14 Developer hardening checklist (attestation & integrity)
- Nonce-based attestation; single-use, short TTL, bound to session/user.
- Server validates signature chain, nonce, timestamp, package, signing cert digest.
- Short-lived acceptance window; cache results for minimal time.
- Package binding: server knows current release cert digests and APK digest.
- Fallback policy defined for partial signals (
basicIntegrityonly). - Client integrity heuristics reported (not trusted) and correlated server-side.
- Telemetry & forensics: trace IDs, structured reasons, encrypted token storage.
- Rotation plans: signing key change, pin rollover, API versioning.
- Canary mode before enforcement; monitor false positives.
- Clear UX flows for denial/step-up, support escalation.
12.15 Pentest/audit checklist (what to verify)
- Attestation token signature validation is implemented and rejects tampered tokens.
- Nonce replay is impossible (single-use; server enforces).
- Package name and signing cert digest in token match the official release.
- Server-side has consistent policy mapping for
ctsProfileMatch/integrity fields. - Re-signed APK results in server denial.
- Telemetry contains trace ID, reasons, and versioned app identity.
- Vendor bypass claims are accompanied by tokens + nonces + logs and can be re-verified independently.
12.16 Labs (authorized, reproducible)
Lab 12-A — Play Integrity end-to-end (mock server)
- Implement
/attest/nonce(random 16–32 bytes, TTL 90s) and/attest/verify. - Modify a lab app to fetch nonce → request attestation → POST token.
- Test cases: valid flow; wrong nonce; expired token; package digest mismatch.
- Deliverables: server logs (trace IDs), accept/deny reasons, token age stats.
Lab 12-B — Key Attestation with device binding
- Generate a Keystore key with
setAttestationChallenge(nonce). - Submit key attestation cert chain + public key to server; server verifies chain & challenge and stores the public key thumbprint as device binding.
- Subsequent requests must be signed (or tied) to this device key.
- Deliverables: verification artifacts, key thumbprint registry, negative tests (wrong challenge).
Lab 12-C — Local integrity signals → server policy
- Implement client telemetry package (
root,debug,fridaDetected,dexHash). - Send telemetry + attestation token to server; simulate: stock device, rooted device (lab), emulator.
- Evaluate policy: step-up vs deny; monitor false positives.
Lab 12-D — Repackaging denial
- Re-sign a lab APK with a different key; try to authenticate.
- Verify server denies based on cert digest mismatch; verify UX message.
- Deliverables: denial logs with
cert_digest_mismatch.
12.17 Common pitfalls & how to avoid them
- Client-only decisions → Move all final decisions server-side.
- Nonce reuse → Enforce single-use and short TTL; mark used on first verification attempt.
- Ignoring package/cert binding → Attackers can re-sign APKs; always check cert digest.
- Over-penalizing emulators → Use emulators for QA/dev; tune policy to avoid blocking legitimate testing while keeping production strict.
- Unlogged denial reasons → Without structured reasons & trace IDs, you can’t resolve vendor disputes or audit incidents.
- Assuming pinning = integrity → Pinning prevents MitM; it does not validate device/app integrity.
12.18 Deliverables from Module 12
- Integrity Verification Spec (server): endpoints, token fields, verification steps, error codes.
- Release Identity Manifest: package, signing cert digests, APK digests per version.
- Policy Matrix: attestation results → server action mapping (allow/step-up/deny).
- Telemetry Schema: fields, retention, privacy notes, SIEM mappings.
- Forensic Checklist: artifacts to require for any bypass claim; reproduction instructions.
12.19 “At a glance” cheat sheet
- Trust anchor: server-validated attestation, not client heuristics.
- Bind identity: package name + cert digest must match.
- Nonce: single-use, short TTL, logged.
- Policy: ctsProfileMatch false → step-up or deny; emulator/root → telemetry + policy.
- Evidence: keep tokens (encrypted), reasons, and trace IDs for audits.
- Rotation: plan for signing key/pin changes.
