Skip to main content

CC6.2 — User authentication and device attestation

Status: Partially implemented (server-side Play Integrity enforcement live; StrongBox attestation tracked sprint 3) Owner: Agent #38 (Senior Compliance Lead, SOC 2 + ISO 27001) Last reviewed: 2026-05-28 Next review: 2026-08-28

Trust Services Criteria reference

Prior to issuing system credentials and granting access, the entity registers and authorises new internal and external users whose access is administered by the entity. For external users, this includes the registration of devices and the verification that the device is in a trusted state. The control covers the registration ceremony, the device-attestation primitives (TEE-backed key attestation, OS-integrity attestation), and the binding of credentials to devices.

How ZeroAuth meets this control

User authentication for the developer console uses the email + password + HttpOnly-cookie-JWT path described in CC6.1. Console signup is gated by the per-IP rate limit (authLimiter at src/routes/console.ts, 10 attempts per 15 min) and the password policy (12 chars, letter+digit, denylist of common passwords) per threat-model row A-05.

End-user authentication is anchored in zero-knowledge identity verification, not in shared secrets. The flow:

  1. The user enrolls a biometric. The biometric is hashed (SHA-256 → DID, Poseidon commitment), the raw input buffer is GC'd immediately, and the commitment is anchored on Base Sepolia via DIDRegistry. The repository's non-goals (CLAUDE.md) explicitly forbid raw biometric over the wire and raw biometric logging.
  2. At verification time, the user generates a Groth16 proof on-device. The proof shows knowledge of a commitment-preimage matching the registered DID — without revealing the biometric. src/services/zkp.ts verifies the proof off-chain via snarkjs; the on-chain Groth16Verifier is the optional second-layer check.
  3. The boot-time vkey hash check (ADR 0015, commit e98d158) guarantees the verifier is running the circuit it claims to be running. A version-bump requires an ADR + a trusted-setup ceremony + a verifier redeploy + a cryptographer-reviewer approve.

Device attestation for the Android prover:

  • Play Integrity API server-side enforcement. Commit 0224be4 ("w3: server-side Play Integrity enforcement on /submit") enforces the Play Integrity attestation on the /v1/proof-pairing/submit endpoint server-side. Device must present a verdict from Google Play with MEETS_DEVICE_INTEGRITY + MEETS_BASIC_INTEGRITY; otherwise the submission is rejected. Compliance-relevant for RBI DPS §7 (mobile application security).
  • WebView process isolation. Commit e2579df adds android:process=":prover" so the snarkjs WebView prover runs in a separate process from the host app — bounding the impact of any WebView-level compromise to the prover surface.
  • Pinned prover-asset hashes. ADR 0010 (adr/0010-android-webview-snarkjs-bundling.md) defines the prover-asset bundling; commit d18460f pins the asset hashes with a Gradle gate so a tampered prover bundle fails the build.
  • AndroidKeystoreManager + BiometricGate (Robolectric tests). Commit f07a397 introduces the AndroidKeystoreManager + AndroidBiometricGate + Robolectric tests. The keystore manager wraps the Android Keystore; the biometric gate requires biometric confirmation before a key operation completes.

The full StrongBox-backed Android Keystore migration (TEE-backed key attestation; AES-GCM-256 with biometric-bound user-authentication) is tracked as audit finding C-2 ("TRACKED-TO-PHASE-1-SPRINT-3"). Real Android prover with rapidsnark JNI + StrongBox-backed keystore lands C-104 (Phase 1 Sprint 3). Real biometric capture (CameraX face + R307 USB-OTG) lands C-143 / C-167. The grep test tests/no-fake-prover.test.ts closes C-2 at C-149.

The proof-pairing protocol (ADR 0009, adr/0009-qr-proof-pairing-protocol.md) is the QR + Bluetooth-LE-fallback pairing channel between the prover device and the verifier endpoint. Commits b2fb2f7 + f277b82 implement the endpoints and tests; commit 7ff0755 lands the backend service. The protocol carries Play Integrity verdict + proof + device-attested nonce; the verifier server-side re-checks all three.

Demo-bypass removal (audit finding C-1, commit 02e1734) closed the channel where any did:zeroauth:demo:* DID was accepted without crypto verification — the pairing_demo_mode field on TenantSecurityPolicy is now @deprecated. Threat-model row A-27.

Evidence references

  • Commit 0224be4 — server-side Play Integrity enforcement on /submit.
  • Commit e2579df — WebView process isolation via bound :prover service.
  • Commit d18460f — pinned prover asset hashes in ADR-0010 + Gradle gate.
  • Commit f07a397 — AndroidKeystoreManager + AndroidBiometricGate + Robolectric tests.
  • Commit 02e1734 — demo-bypass removal (C-1).
  • Commit b2fb2f7, f277b82, 7ff0755 — proof-pairing endpoints + tests + backend service.
  • ADR 0009-qr-proof-pairing-protocol.md — pairing protocol spec.
  • ADR 0010-android-webview-snarkjs-bundling.md — Android prover bundling.
  • ADR 0015-circuit-version-pinning.md (commit 27ed93c) + commit e98d158 — verifier-side circuit integrity.
  • docs/threat_model.md rows A-05, A-27 — credential + demo-bypass surfaces.

Open gaps + remediation roadmap

  • Real Android prover with rapidsnark JNI + StrongBox-backed keystore — audit finding C-2, target Phase 1 Sprint 3 (C-104).
  • Real biometric capture (CameraX face + R307 USB-OTG) — audit findings C-143 + C-167.
  • tests/no-fake-prover.test.ts regression guard — closes C-2 at C-149.
  • WebAuthn / FIDO2 fallback for the console — not in v1 scope; tracked for v2 as a console-auth modernisation.

Test or audit query

cat src/services/proof-pairing.ts | grep -c "playIntegrity\\|attestation" should be > 0 — verifies attestation-handling code is present. git log --oneline -- mobile/ android/ shows the device-side codebase is under version control. cat tests/biometric-rejection.test.ts shows the no-raw-biometric guard.