Module 12 — Device & App Integrity: Play Integrity, Key Attestation, and Root/Emulator Signals

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

MechanismWho issues proof?What it proves (at best)Where to enforce
Play IntegrityGoogle Play servicesSignals about device integrity (e.g., basicIntegrity, ctsProfileMatch), app identity (package, cert digest), timestampServer-side after verifying signature and nonce
Android Key AttestationDevice Keymaster/TEEA 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 itselfHeuristics about environment (root, debug, tamper) — bypassable on owned devicesUse 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)

  1. 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).
  1. Client (device):
  • Calls Play Integrity or Key Attestation API with the nonce.
  • Receives attestation token (opaque/JWT/CBOR depending on API).
  1. Client → Server: POST /attest/verify { nonce, token }
  2. 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: packageName and 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.
  • appPackageName and apkCertificateDigestSha256: 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 resultExample policy for banking
ctsProfileMatch = true (and identity matches)Normal operations allowed.
ctsProfileMatch = false, basicIntegrity = trueStep-up: MFA or limited features; allow low-risk reads, block high-risk transfers.
Both ctsProfileMatch=false & basicIntegrity=falseDeny sensitive actions; allow customer support flows to re-enroll devices; record telemetry.
Nonce mismatch / token expiredDeny outright; investigate possible replay/manipulation.
Package/cert digest mismatchDeny 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(), ptrace attempts, anti-Frida heuristics (library/process scans), checksum verification of classes.dex/libs.
  • Emulator hints: ro.kernel.qemu, hardware fingerprints, known files, device model patterns.
  • Hooking frameworks: suspicious loaded libs, modified linker behavior, 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 logcat with 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:

ScenarioDeviceExpectation
Stock device, official appReal hardwarectsProfileMatch true → allow
Emulator, official appAVDLikely ctsProfileMatch false → step-up or limited
Stock device, re-signed appReal hardwarePackage digest mismatch → deny
Token replay (nonce reused)AnyServer denies
Stale token (past TTL)AnyServer denies
Rooted device (userland root)Real hardwareAttestation likely fails; policy denies/steps-up
Custom kernel (KernelSU)Real hardwareTreat 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 (basicIntegrity only).
  • 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.