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:
- 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. - 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.tsverifies the proof off-chain via snarkjs; the on-chainGroth16Verifieris the optional second-layer check. - 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/submitendpoint server-side. Device must present a verdict from Google Play withMEETS_DEVICE_INTEGRITY+MEETS_BASIC_INTEGRITY; otherwise the submission is rejected. Compliance-relevant for RBI DPS §7 (mobile application security). - WebView process isolation. Commit
e2579dfaddsandroid: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; commitd18460fpins the asset hashes with a Gradle gate so a tampered prover bundle fails the build. - AndroidKeystoreManager + BiometricGate (Robolectric tests). Commit
f07a397introduces 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:proverservice. - 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(commit27ed93c) + commite98d158— verifier-side circuit integrity. docs/threat_model.mdrows 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.tsregression 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.