# Setup Last updated: 2026-04-25 This is a living setup/status file for the local ChromeCard workspace at `/home/user/chromecard`. Update this file whenever environment status or verified behavior changes. ## Repository Policy - Treat `/home/user/chromecard/CR_SDK_CK-main` as read-only in this workflow. - Do not add or modify helper/test scripts inside `CR_SDK_CK-main`. - Keep host-side helper scripts at workspace root (`/home/user/chromecard`). ## Documentation Maintenance - Canonical living status docs for this workspace are: - `/home/user/chromecard/Setup.md` - `/home/user/chromecard/Workplan.md` - After each meaningful execution step, update at least: - `Setup.md` for observed environment/runtime state - `Workplan.md` for phase progress and next blocking action - Keep helper script paths consistent in docs: - `/home/user/chromecard/fido2_probe.py` - `/home/user/chromecard/webauthn_local_demo.py` - Treat `CR_SDK_CK-main/README_HOST.md` as historical reference unless its script paths are aligned with this workspace policy. ## Scope - Experimental ChromeCard connected over USB. - Firmware source tree: `/home/user/chromecard/CR_SDK_CK-main`. - Host-side FIDO2 demo tools: - `/home/user/chromecard/fido2_probe.py` - `/home/user/chromecard/webauthn_local_demo.py` - Target runtime platform: Qubes OS with 3 AppVMs: - `k_client` (browser + enrollment process) - `k_proxy` (card-connected proxy/auth client) - `k_server` (protected resource/backend) ## Planned Transport Evolution - Current phase assumption: card is connected directly to `k_proxy` (USB). - Future target: card is connected to a phone, and `k_proxy` performs validation through a wireless link to that phone. - Design implication: keep authenticator transport behind an abstraction in `k_proxy` so USB-direct and phone-wireless backends can be swapped without changing client/server API contracts. ## Target Qubes Topology - Base template for all AppVMs: `debian-13-xfce`. - Allowed network paths: - `k_client` -> `k_proxy` over TLS - `k_proxy` -> `k_server` over TLS - Response traffic returns on those established connections. - Disallowed direct path: - `k_client` -> `k_server` (direct access should be blocked). Functional roles: - `k_client`: - Browser-only traffic client. - Runs a user enrollment process. - `k_proxy`: - Current: connected to the ChromeCard over USB. - Future: connects wirelessly to phone-attached card for validation. - Accepts TLS requests from `k_client`. - Uses card-backed FIDO2/WebAuthn operations to authenticate user/session. - Calls `k_server` over TLS after successful authorization. - Returns proxied data and session information to `k_client`. - `k_server`: - Hosts resource(s) requiring login via the proxy-mediated flow. - Provides a dummy protected resource for early integration testing (monotonic increasing number/counter). - May hold user/session state logic needed for authorization decisions. UI baseline for each AppVM (start-menu visible apps): - Firefox - XFCE Terminal - File Manager ## Target Request Flow 1. `k_client` sends HTTPS request to `k_proxy`. 2. `k_proxy` validates/authenticates user via card-backed flow. 3. If allowed, `k_proxy` opens HTTPS request to `k_server` resource. 4. `k_server` responds to `k_proxy`. 5. `k_proxy` returns response payload to `k_client` plus session state. 6. Subsequent requests reuse session state so card auth is not required every request. Implementation note: - `k_proxy` does not need a full web server stack; a minimal TLS API service is sufficient. - Session state should be integrity-protected (signed/encrypted token or server-side session ID) with TTL and revocation behavior defined. - `k_proxy` and `k_server` must be safe under concurrent access (thread-safe state handling). ## Minimum Service Behavior (Current Target) - `k_server`: - Expose protected endpoint returning an increasing integer value (dummy resource). - Increment behavior must remain correct under concurrent requests. - Optionally expose/maintain user/session validation logic. - `k_proxy`: - Accept concurrent HTTPS requests from one or more `k_client` instances. - Perform card-backed auth when no valid session is present. - Cache and validate session state so repeated requests avoid card access until expiry. - Forward authorized requests to `k_server` and return upstream data plus session info. Thread-safety expectation: - Shared mutable state (counter, session store, user state) must be protected against races. - Parallel requests must not corrupt session records or return duplicate/skipped counter values caused by unsafe updates. ## Test Topology Requirement - Support concurrency testing from multiple simultaneous clients: - multiple browser tabs/processes in one `k_client`, and/or - multiple `k_client` AppVM instances if available. - Validate both correctness and stability under load: - session reuse works as intended - unauthorized access stays blocked - protected counter/resource remains consistent. ## Current Status Snapshot (2026-04-24) - AppVM OS version is confirmed: Debian `13.4` (`k_server`, and same on `k_client`/`k_proxy`). - Python in AppVMs is available: `Python 3.13.5`. - `python3 /home/user/chromecard/fido2_probe.py --list` in `k_proxy` now detects ChromeCard on `/dev/hidraw0` (`vid:pid=4617:5`). - HID raw device nodes are now visible in `k_proxy`: - `/dev/hidraw0` -> `crw-rw----+` - `/dev/hidraw1` -> `crw-------` - `python3 /home/user/chromecard/fido2_probe.py --json` succeeds and returns CTAP2 `getInfo`: - versions: `["FIDO_2_0"]` - aaguid: `1234567890abcdef0123456789abcdef` - options: `rk=false`, `up=true`, `uv=true` - max_msg_size: `1024` - Local WebAuthn demo (`http://localhost:8765` in `k_proxy`) succeeded: - register: `ok=true`, `username=alice`, `credential_count=1` - login/auth: `ok=true`, `username=alice`, `authenticated=true` - Phase 5 prototype services are now available: - `/home/user/chromecard/k_proxy_app.py` - `/home/user/chromecard/k_server_app.py` - `/home/user/chromecard/PHASE5_RUNBOOK.md` - Remote VM access is now available via SSH/SCP aliases: - command execution: `ssh ` - file copy to VM home: `scp :~` - validated hosts: `k_client`, `k_proxy`, `k_server` - `west` is not currently installed/in PATH: `west not found`. - The checked-out `CR_SDK_CK-main` tree appears incomplete for documented sysbuild role layout: - missing: `mvp`, `setup`, `components`, `samples` - `CR_SDK_CK-main/scripts/build_flash_mvp.sh` exists, but it expects the above role directories. - Python helper scripts were intentionally moved out of `CR_SDK_CK-main/scripts` and are now maintained at workspace root. - Qubes AppVM baseline is now up: `k_client`, `k_proxy`, `k_server` can start and have terminals running. Implication: - Live FIDO2 connectivity from `k_proxy` to ChromeCard is confirmed over USB HID/CTAPHID. - Local browser WebAuthn register/login flow is confirmed working in `k_proxy`. - We cannot currently run the documented firmware build/flash flow. Session note (2026-04-24): - Markdown tracking was reviewed and normalized around `Setup.md` + `Workplan.md` as the active, continuously updated execution record. - AppVM template decision recorded: use `debian-13-xfce` for `k_client`, `k_proxy`, and `k_server`. - VM start attempt failed with Xen toolstack error: `libxenlight have failed to create new domain 'k_client'`. - VM start blocker was resolved by reducing VM memory to `400` MiB; all three AppVMs now start. - Runtime check from VMs: Debian `13.4` and Python `3.13.5`; `k_proxy` still shows `no hidraw devices`. - After USB assignment to `k_proxy`, `/dev/hidraw0` and `/dev/hidraw1` appeared. - CTAP probe re-run succeeded with detected ChromeCard device and valid CTAP2 `getInfo` response. - Local WebAuthn demo completed successfully for user `alice` (register + login). - Phase 5 starter implementation added with session TTL, logout/invalidation, and proxy->server protected counter forwarding. Session note (2026-04-24, doc maintenance): - Top-level Markdown files were re-scanned: `PHASE5_RUNBOOK.md`, `Setup.md`, `Workplan.md`. - `PHASE5_RUNBOOK.md` remains consistent with the current Phase 5 prototype paths and flow. - No plan/setup drift was found requiring behavioral changes; docs remain aligned. - SSH-based VM operation was validated for `k_client`, `k_proxy`, `k_server` (Debian `13.4` confirmed remotely). - SCP file transfer to `k_proxy` home directory was validated with read-back. Session note (2026-04-24, remote flow diagnostics): - VM script staging gap found: `/home/user/chromecard/k_proxy_app.py`, `k_server_app.py`, and helper files were missing on AppVMs and were copied via `scp`. - Services were started in VMs and verified locally: - `k_proxy` local health OK on `127.0.0.1:8770` and `127.0.0.1:8771` - `k_server` local health OK on `127.0.0.1:8780` - Verified VM IPs during this run: - `k_proxy`: `10.137.0.12` - `k_server`: `10.137.0.13` - `k_client`: `10.137.0.16` - Current chain failure is network pathing/firewall: - `k_client -> k_proxy` (`10.137.0.12:8771`) times out. - `k_proxy -> k_server` (`10.137.0.13:8780`) times out. - Proxy returns upstream error payload: `server unavailable: timed out`. Session note (2026-04-24, markdown re-scan): - Re-read top-level workspace Markdown files: `Setup.md`, `Workplan.md`, `PHASE5_RUNBOOK.md`. - Re-skimmed source-tree reference docs in `CR_SDK_CK-main`, including `BUILD.md`, `README.md`, `README_HOST.md`, `RELEASE.md`, and `distribute_bundle.md`. - Current workspace docs remain aligned with the verified execution record. - Source-tree doc drift remains unchanged: - `README_HOST.md` still points to `./scripts/fido2_probe.py` and `./scripts/webauthn_local_demo.py`. - Active workspace policy continues to treat those paths as historical; maintained helper paths remain `/home/user/chromecard/fido2_probe.py` and `/home/user/chromecard/webauthn_local_demo.py`. - Source-tree build docs continue to describe a full SDK layout with `mvp`, `setup`, `components`, and `samples`, which is still not present in the current local checkout snapshot. Session note (2026-04-24, policy retry): - Markdown re-scan was retried after local policy changes. - Re-running the workspace doc scan with a non-login shell completed cleanly, without the earlier SSH/socat startup noise in command output. Session note (2026-04-24, chain probe retry): - Re-probed the Qubes access path for `k_client -> k_proxy -> k_server`. - Local forwarded SSH listener ports still exist on the host: - `0.0.0.0:2222` -> `qrexec-client-vm 'k_client' qubes.ConnectTCP+22` - `0.0.0.0:2223` -> `qrexec-client-vm 'k_proxy' qubes.ConnectTCP+22` - `0.0.0.0:2224` -> `qrexec-client-vm 'k_server' qubes.ConnectTCP+22` - These forwarded SSH ports currently fail immediately: - `ssh k_client` / `ssh k_proxy` / `ssh k_server` close immediately on localhost forwarded ports. - Direct `qrexec-client-vm qubes.ConnectTCP+22` returns `Request refused`. - Chain ports are currently blocked at the same qrexec layer: - `qrexec-client-vm k_proxy qubes.ConnectTCP+8770` -> `Request refused` - `qrexec-client-vm k_server qubes.ConnectTCP+8780` -> `Request refused` - This means the current blocker is active qrexec policy/service refusal for `qubes.ConnectTCP`, not the Python service code in `k_proxy_app.py` or `k_server_app.py`. - Separate SSH config issue remains on the host: - `/etc/ssh/ssh_config.d/20-systemd-ssh-proxy.conf` is still owned `root:root` but mode `777`, which causes OpenSSH to reject it as insecure on the normal login-shell path. Session note (2026-04-25, post-restart probe): - Correct client-facing proxy port is `8771` for the current split-VM chain checks. - SSH to `k_proxy` is working again. - `k_proxy` card visibility is restored after VM restart and card reconnect: - `/dev/hidraw0` and `/dev/hidraw1` are present in `k_proxy` - Current service state after restart: - `k_proxy` has no listener on `127.0.0.1:8771` - `k_server` has no listener on `127.0.0.1:8780` - Current qrexec chain state after restart: - `qrexec-client-vm k_proxy qubes.ConnectTCP+8771` -> `Request refused` - `qrexec-client-vm k_server qubes.ConnectTCP+8780` -> `Request refused` - Practical meaning: - SSH and card attachment recovered - phase-5 app services are not currently running in the VMs - qrexec forwarding for the chain ports is still being refused Session note (2026-04-25, service restart): - `k_server_app.py` was restarted successfully in `k_server`: - PID `1320` - listening on `127.0.0.1:8780` - `/health` returns `{"ok": true, "service": "k_server", ...}` - `k_proxy_app.py` was restarted successfully in `k_proxy`: - PID `2774` - listening on `127.0.0.1:8771` - `/health` returns `{"ok": true, "service": "k_proxy", "active_sessions": 0, ...}` - Despite local service recovery, qrexec forwarding is still denied: - `qrexec-client-vm k_proxy qubes.ConnectTCP+8771` -> `Request refused` - `qrexec-client-vm k_server qubes.ConnectTCP+8780` -> `Request refused` Session note (2026-04-25, markdown refresh): - Re-read the active workspace markdown files: - `Setup.md` - `Workplan.md` - `PHASE5_RUNBOOK.md` - Corrected the Phase 5 runbook to distinguish the old same-VM quickstart from the current split-VM chain usage. - Current documented client-facing proxy port for split-VM tests is `8771`. - Current documented blocker remains unchanged: - local service health inside `k_proxy` and `k_server` is good - inter-VM forwarding via `qubes.ConnectTCP` is still refused Session note (2026-04-25, Phase 2 HTTPS bring-up): - Added direct TLS support to: - `/home/user/chromecard/k_proxy_app.py` - `/home/user/chromecard/k_server_app.py` - Added local certificate generator: - `/home/user/chromecard/generate_phase2_certs.py` - Generated local CA and service certs at: - `/home/user/chromecard/tls/phase2/ca.crt` - `/home/user/chromecard/tls/phase2/k_proxy.crt` - `/home/user/chromecard/tls/phase2/k_server.crt` - Certificate generation was corrected to include subject key identifier and authority key identifier so Python TLS verification succeeds. - Current validated HTTPS shape is Qubes-localhost forwarding, not raw VM-IP routing: - in `k_client`: `qvm-connect-tcp 9771:k_proxy:8771` - in `k_proxy`: `qvm-connect-tcp 9780:k_server:8780` - `k_proxy` listens on `https://127.0.0.1:8771` - `k_server` listens on `https://127.0.0.1:8780` - `k_proxy` upstream is `https://127.0.0.1:9780` - Verified HTTPS checks: - `k_client -> k_proxy` `/health` over TLS succeeds with `--cacert /home/user/chromecard/tls/phase2/ca.crt` - `k_proxy -> k_server` `/health` and `/resource/counter` over TLS succeed through the `9780` forwarder - end-to-end `k_client -> k_proxy -> k_server` login + session reuse succeeded over HTTPS - End-to-end verified results: - login returned `ok=true` for `alice` - first protected counter call returned value `1` - second protected counter call returned value `2` - session status remained valid after reuse Session note (2026-04-25, Phase 2.5 ownership and concurrency): - Current prototype state ownership is now explicit: - `k_proxy` is authoritative for session state - `k_server` is authoritative for protected resource state - `k_client` is not authoritative for either session validity or counter/resource state - Current session model in `k_proxy`: - server-side in-memory session store only - opaque bearer token generated by `secrets.token_urlsafe(32)` - per-session fields are `username` and `expires_at` - expiry is enforced in `k_proxy`; `k_server` does not validate client sessions directly - Current resource model in `k_server`: - in-memory monotonic counter guarded by a lock - access allowed only when request arrives from `k_proxy` with the expected `X-Proxy-Token` - Current concurrency model in code: - both services use `ThreadingHTTPServer` - `k_proxy` protects session-map mutations and garbage collection with a single lock - `k_server` protects counter increments with a single lock - TLS verification and upstream fetches happen outside the session lock in `k_proxy` - Current runtime assumptions and limits: - Qubes localhost forwarders are treated as transport plumbing, not as state authorities - if `k_proxy` restarts, in-memory sessions are lost - if `k_server` restarts, the in-memory counter resets - the current shared `X-Proxy-Token` is a prototype trust mechanism, not a final authorization design - Practical meaning: - race-free behavior is currently defined for session CRUD and counter increments inside one process per VM - persistence, distributed session authority, and multi-proxy/multi-server coordination are not implemented yet Session note (2026-04-25, Phase 6 client portal prototype): - Added browser-facing client process: - `/home/user/chromecard/k_client_portal.py` - Current Phase 6 prototype shape: - portal runs in `k_client` on `http://127.0.0.1:8766` - portal keeps local enrolled username state in `k_client` - portal calls `k_proxy` over the validated TLS forward `https://127.0.0.1:9771` - Current local enrollment model: - enrollment is a client-local username selection stored by the portal - no dedicated server-side enrollment API exists yet - Verified portal API flow in `k_client`: - `GET /health` returns `ok=true` - `POST /api/enroll` with `alice` succeeds - `POST /api/login` succeeds and returns a proxy session token - `POST /api/status` succeeds - `POST /api/resource/counter` succeeds twice with upstream values `3` and `4` - `POST /api/logout` succeeds - Current implication: - `k_client` now has a concrete client-side process instead of only runbook curls - browser-facing flow is now available through the local portal - next hardening step is to replace client-local enrollment with the intended enrollment contract and decide whether browser traffic should eventually talk to `k_proxy` directly or continue through a local client portal Session note (2026-04-25, Phase 6 enrollment contract): - Added proxy-side enrollment API and storage: - `POST /enroll/register` - `GET /enroll/status?username=` - persisted prototype store at `/home/user/chromecard/k_proxy_enrollments.json` in `k_proxy` - Current enrollment authority is now `k_proxy`, not the `k_client` portal. - Current portal behavior: - portal enrollment calls `k_proxy` over TLS - portal keeps only a preferred local username for convenience - portal login now depends on proxy-side enrollment existing - Verified behavior: - direct proxy login for unenrolled `bob` returns `{"ok": false, "error": "user not enrolled", ...}` - portal enrollment of `alice` succeeds and persists in proxy-side enrollment storage - proxy enrollment status for `alice` returns `ok=true` - portal login and protected counter access still succeed after enrollment - Practical meaning: - Phase 6 now has a real `k_client -> k_proxy` enrollment request path - the remaining gap is not basic routing; it is deciding the final enrollment semantics and whether the browser should stay behind a local portal or talk to `k_proxy` directly Session note (2026-04-25, browser target moved to k_proxy): - `k_proxy` now serves the browser-facing portal UI directly on `/` over `https://127.0.0.1:9771`. - `k_client_portal.py` is now a temporary bridge page: - it points users to `https://127.0.0.1:9771/` - it is no longer the primary browser target - Verified direct browser/API target behavior from `k_client`: - `GET https://127.0.0.1:9771/` returns the proxy portal HTML - `GET https://127.0.0.1:9771/health` returns `ok=true` - direct `POST /enroll/register` for `carol` succeeds - direct `POST /session/login` for `carol` succeeds - Current implication: - browser traffic is now intended to go straight to `k_proxy` - the `k_client` portal remains only as a temporary bridge/compatibility layer Session note (2026-04-25, provisional enrollment hardening): - The enrollment contract in `k_proxy` is now explicit but provisional. - Current prototype enrollment rules: - usernames are canonicalized to lowercase - allowed username pattern is `3-32` chars using lowercase letters, digits, `.`, `_`, `-` - optional `display_name` is allowed up to `64` chars - enrollment create is create-only and duplicate create returns `user already enrolled` - enrollment update is a separate operation - enrollment delete is a separate operation and removes any active sessions for that username - Current enrollment endpoints on `k_proxy`: - `POST /enroll/register` - `GET /enroll/status?username=` - `POST /enroll/update` - `POST /enroll/delete` - `GET /enroll/list` - Verified behavior from `k_client` against `https://127.0.0.1:9771`: - invalid username `A!` is rejected - create for `dave` with `display_name` succeeds - duplicate create for `dave` is rejected - update for `dave` succeeds - list returns enrolled users and metadata - delete for `dave` succeeds - login for deleted `dave` fails with `user not enrolled` - Deliberate current limit: - enrollment itself still does not require card presence; only login does - this was kept lightweight because the enrollment semantics are expected to change later Session note (2026-04-25, Phase 6.5 concurrency probe): - Added reproducible concurrency probe: - `/home/user/chromecard/phase65_concurrency_probe.py` - probe now supports `--max-workers` so client-side fan-out can be swept explicitly - Successful baseline run from `k_client` against direct proxy path: - `3` users - `4` protected requests per user - `12/12` requests succeeded - counter values were unique and contiguous from `6` to `17` - max observed latency was about `457 ms` - Larger follow-up run exposed current limit: - `5` users - `5` protected requests per user - `18/25` requests succeeded - failures returned TLS EOF / upstream unavailable errors - successful counter values were still unique and contiguous from `18` to `35` - max observed latency was about `758 ms` - Additional Phase 6.5 diagnosis: - fixed a keep-alive/body-drain bug in the HTTP/1.1 experiment so `k_server` no longer misparses follow-on requests as `{}POST` - added an upstream connection pool in `k_proxy`; current default/test setting clamps `k_proxy -> k_server` to one pooled TLS connection - despite that change, a full fan-out run with `25` in-flight protected calls still fails on client-observed TLS EOFs - a worker-limited run now passes cleanly: - `5` users - `5` protected requests per user - `25/25` requests succeeded with `--max-workers 10` - raising client-side fan-out still breaks: - `22/25` requests succeeded with `--max-workers 15` - `15/25` requests succeeded with fully unbounded `25` workers in the latest rerun - Current diagnosis: - the protected counter and session logic stay correct under load; successful values remain unique and contiguous - `k_proxy` and `k_server` can complete the requests that actually reach them - the primary collapse point in current testing is the client-facing Qubes forwarder on `9771` - `qvm_connect_9771.log` shows `qrexec-agent-data` / data-vchan failures and repeated `xs_transaction_start: No space left on device` - `qvm_connect_9780.log` also showed earlier qrexec failures, but the latest worker-threshold evidence points first to connection fan-out on `k_client -> k_proxy` - Practical meaning: - the application logic is good for moderate concurrent use in the current prototype - the direct browser path appears stable around `10` in-flight protected calls in the current Qubes setup - the current concurrency ceiling is being set by Qubes forwarding behavior rather than by the monotonic counter logic Session note (2026-04-25, in-VM forwarding test): - Tested the intended in-VM forwarding path with `qvm-connect-tcp` instead of host-side `qrexec-client-vm`. - Forwarders start and bind locally: - in `k_client`: `qvm-connect-tcp 8771:k_proxy:8771` binds `localhost:8771` - in `k_proxy`: `qvm-connect-tcp 8780:k_server:8780` binds `localhost:8780` - But the actual client->proxy connection is still refused when used: - `k_client` forward log shows `Request refused` - `socat` reports child exit status `126` and `Connection reset by peer` - Local login on `k_proxy` reaches the app but fails on the auth dependency: - `POST /session/login` to `http://127.0.0.1:8771` returns `401` - details: `Missing dependency: python-fido2 ... No module named 'fido2'` - `k_server` was not reached during this login test; current `k_server.log` only shows `/health`. Session note (2026-04-25, after python3-fido2 install): - `k_proxy` was restarted after `python3-fido2` installation and now listens again on `127.0.0.1:8771`. - The previous Python import blocker is resolved; local login now reaches the CTAP probe path. - Current local login result on `k_proxy`: - `{"ok": false, "error": "card auth failed", "details": "No CTAP HID devices found."}` - Current forwarded login result from `k_client` is still not completing: - `curl http://127.0.0.1:8771/session/login` -> `Empty reply from server` - `qvm_connect_8771.log` still shows repeated `Request refused` and child exit status `126` - Practical meaning: - Python dependency issue in `k_proxy` is fixed - card access inside `k_proxy` is currently missing again at CTAP/HID level - `k_client -> k_proxy` qrexec forwarding is still effectively denied/refused Session note (2026-04-25, card reattached): - Card visibility in `k_proxy` is restored again: - `/dev/hidraw0` and `/dev/hidraw1` present - `fido2_probe.py --list` detects ChromeCard on `/dev/hidraw0` - Local login on `k_proxy` now succeeds again: - `POST /session/login` on `127.0.0.1:8771` returns `200` - session creation for user `alice` succeeded - Remaining failure is isolated to the client-facing qrexec path: - `k_client` -> `localhost:8771` through `qvm-connect-tcp` still returns `Empty reply from server` - `qvm_connect_8771.log` still shows `Request refused` Session note (2026-04-25, clean forward retest): - Re-ran both forwards and exercised each hop immediately after local bind. - `k_proxy -> k_server`: - `qvm-connect-tcp 8780:k_server:8780` binds `localhost:8780` in `k_proxy` - first real `POST /resource/counter` through that forward returns `Empty reply from server` - `qvm_connect_8780.log` then records `Request refused` with child exit status `126` - `k_client -> k_proxy`: - `qvm-connect-tcp 8771:k_proxy:8771` binds `localhost:8771` in `k_client` - first real `POST /session/login` through that forward returns `Empty reply from server` - `qvm_connect_8771.log` records `Request refused` with child exit status `126` - Conclusion from this retest: - both forwards fail in the same way - local bind succeeds, but the actual qrexec `qubes.ConnectTCP` request is refused when the first connection is attempted Session note (2026-04-25, dom0 policy fix validated): - After changing dom0 policy to use explicit destination VMs instead of `@default` for `qubes.ConnectTCP`, both forwards now work. - Verified hop 1: - in `k_proxy`, `POST http://127.0.0.1:8780/resource/counter` with `X-Proxy-Token: dev-proxy-token` succeeds - response included counter value `1` - Verified hop 2: - in `k_client`, `POST http://127.0.0.1:8771/session/login` succeeds - session token is returned through the `k_client -> k_proxy` forward - Verified full end-to-end flow from `k_client`: - login succeeded and returned session token - `POST /session/status` succeeded - `POST /resource/counter` succeeded twice with upstream values `2` and `3` - `POST /session/logout` succeeded - post-logout `POST /resource/counter` correctly returned `401 invalid or expired session` - Current conclusion: - `k_client -> k_proxy -> k_server` chain is operational - session reuse and logout behavior are working in the current prototype Session note (2026-04-25, live chain re-validation and regression helper): - Re-validated the split-VM chain after restart using the current TLS/localhost-forward shape: - `k_client` local `9771` -> `k_proxy:8771` - `k_proxy` local `9780` -> `k_server:8780` - Verified live service state during this run: - `k_server` local `https://127.0.0.1:8780/health` returned `ok=true` - `k_proxy` local `https://127.0.0.1:8771/health` returned `ok=true` - `k_proxy` local `https://127.0.0.1:9780/health` reached `k_server` - `k_client` local `https://127.0.0.1:9771/health` reached `k_proxy` - Verified end-to-end behavior from `k_client`: - login for `alice` succeeded - session status succeeded - protected counter calls succeeded with session reuse - logout succeeded - post-logout protected access returned `401 invalid or expired session` - Added reproducible regression helper at: - `/home/user/chromecard/phase5_chain_regression.sh` - Verified the new helper end-to-end on 2026-04-25: - default run uses `20` requests at parallelism `8` - returned values were unique and gap-free - latest verified counter range from the helper was `43..62` - Practical meaning: - the current blocker is no longer Qubes forwarding for the base Phase 5 chain - the current next-step gap is auth semantics, not transport bring-up Session note (2026-04-25, direct FIDO2 auth attempt): - Added an experimental direct FIDO2 path in `/home/user/chromecard/k_proxy_app.py`: - runtime switch: `--auth-mode fido2-direct` - default runtime remains `probe` - Added a low-level CTAP helper at `/home/user/chromecard/raw_ctap_probe.py`: - purpose: bypass `Fido2Client` and exercise raw CTAP2 `makeCredential` / `getAssertion` - logs keepalive callbacks and exact transport exceptions for host-side debugging - Direct-mode intent: - replace the legacy `fido2_probe.py --json` session gate - perform real credential registration and real assertion verification locally in `k_proxy` with `python-fido2` - Current observed blocker on `k_proxy`: - direct `make_credential` fails with `No compatible PIN/UV protocols supported!` - reproduces outside the app in a minimal VM-side probe, so this is not just a handler bug - likely cause is the current card / `python-fido2` stack selecting a PIN/UV-dependent CTAP2 path for registration - Additional probe: - a forced CTAP1 fallback experiment did not fail immediately, but also did not complete quickly enough to treat as a usable working path in this turn - Latest live blocker (2026-04-25, after refactor/deploy): - direct probing is currently blocked before the card Yes/No UI stage because `k_proxy` no longer sees any CTAP HID device - `ssh k_proxy "python3 /home/user/chromecard/fido2_probe.py --list"` now returns `No CTAP HID devices found.` - `ssh k_proxy "ls -l /dev/hidraw*"` shows no `hidraw` nodes at the moment - Follow-up after card reattach (2026-04-25): - `k_proxy` again shows `/dev/hidraw0` and `/dev/hidraw1` - direct node-open check confirms `/dev/hidraw0` is readable as the normal user - `/dev/hidraw1` still returns `PermissionError: [Errno 13] Permission denied` - raw `makeCredential` probe still produced no on-card registration prompt, so the host path is hanging before the firmware Yes/No UI - hidraw mapping confirms `/dev/hidraw0` is the FIDO interface: - report descriptor begins with usage page `0xF1D0` - `get_descriptor('/dev/hidraw0')` returns `report_size_in=64`, `report_size_out=64` - `/dev/hidraw1` is a separate vendor HID interface with usage page `0xFF00` - stale Python probes holding `/dev/hidraw0` were cleared, but behavior did not change - a manual CTAPHID `INIT` packet sent directly to `/dev/hidraw0` writes successfully and still gets no response within `3s` - this places the current blocker below `python-fido2`: raw HID traffic is not getting a CTAPHID reply after the latest reattach - `webauthn_local_demo.py` was re-run inside `k_proxy` after reattach and still produced no card prompt on register - that confirms the current failure is below both the browser WebAuthn path and the direct `python-fido2` path - after a full power cycle and reattach, manual CTAPHID `INIT` on `/dev/hidraw0` started replying again - `webauthn_local_demo.py` register in `k_proxy` then succeeded again, confirming the card transport was recovered by the power cycle - direct host-side registration via `raw_ctap_probe.py --device-path /dev/hidraw0 make-credential --rp-id localhost` also succeeded again after pressing `yes` on the card - returned credential material included: - `fmt="none"` - credential id `7986cfcf45663f625eb7fc7b52640d83cf3d0e8a6627eeadaba3126406b1e0b8` - this confirms the recovered direct path now reaches the real card confirmation UI and completes CTAP2 `makeCredential` - `k_proxy_app.py --auth-mode fido2-direct` was then patched to: - use low-level CTAP2 instead of the higher-level `Fido2Client` registration/assertion calls - open the explicit FIDO node `/dev/hidraw0` instead of scanning devices - cache the direct device handle instead of reopening it for each operation - current remaining blocker: - was narrowed through repeated retries to a mix of hidraw node disappearance, older `python-fido2` response-mapping requirements, and CTAP payload-shape mismatches - latest verified state: - after reattach with healthy CTAPHID `INIT`, real app registration through `k_proxy_app.py --auth-mode fido2-direct` now succeeds - `/enroll/register` for `directtest` returned `ok=true` and `has_credential=true` - real app login through `/session/login` for `directtest` also now succeeds after card confirmation - returned `auth_mode` is `fido2_assertion` - session status succeeds - protected `/resource/counter` access succeeds again through `k_proxy -> k_server` - logout succeeds - post-logout protected access returns `401` - temporary direct-mode hidraw lifetime logging has been removed again after diagnosis - `/home/user/chromecard/phase5_chain_regression.sh` now supports the direct-auth baseline via: - `--interactive-card` - `--login-timeout` - `--expect-auth-mode fido2_assertion` - Practical outcome for this session: - the experimental direct mode is kept in code for follow-up work - the deployed `k_proxy` service was restored to default `probe` mode - verified `alice` login still works afterward, so the validated Phase 5 baseline remains intact ## Known FIDO2 Transport Boundary - FIDO2 on this firmware is handled via USB HID (CTAPHID), not Wi-Fi/BLE/MQTT. - Key code points in `CR_SDK_CK-main`: - `mgr_fido2.c`: `mgr_fido2_init()` registers `fido2_ctaphid_handle_packet`. - `ctaphid.c`: `fido2_ctaphid_handle_packet(...)`. - `cr_config.h`: FIDO2 HID report descriptor definitions. ## Host Bring-Up Steps (How To Get To A Working FIDO2 Check) 1. Confirm USB enumeration and HID visibility. - Replug card with a known data-capable cable. - Check: `ls -l /dev/hidraw*` 2. If needed, grant Linux HID access for this device. - Add rule at `/etc/udev/rules.d/70-chromecard-fido.rules`: ```udev SUBSYSTEM=="hidraw", ATTRS{idVendor}=="1209", ATTRS{idProduct}=="0005", MODE="0660", TAG+="uaccess" ``` - Reload/apply rules and replug the device. 3. Verify CTAP HID presence. - `python3 /home/user/chromecard/fido2_probe.py --list` - Then: - `python3 /home/user/chromecard/fido2_probe.py --json` - For raw CTAP debugging on `k_proxy`: - `python3 /home/user/chromecard/raw_ctap_probe.py info` - `python3 /home/user/chromecard/raw_ctap_probe.py make-credential --rp-id localhost` 4. Run local WebAuthn bring-up demo. - `python3 /home/user/chromecard/webauthn_local_demo.py` - Open `http://localhost:8765` (use `localhost`, not `127.0.0.1`). 5. Execute register/login test. - Register a user. - Login with the same user. - Confirm no origin/challenge mismatch errors. ## Build/Flash Prerequisites (How To Get To Firmware Build) 1. Ensure full SDK checkout layout exists under `CR_SDK_CK-main`: - `mvp` - `setup` - `components` - `samples` 2. Ensure toolchain is available in shell: - `west --version` - `nrfjprog --version` 3. Once layout/tooling are in place, run: - `cd /home/user/chromecard/CR_SDK_CK-main` - `./scripts/build_flash_mvp.sh` ## Open Gaps To Resolve - Whether a full `CR_SDK_CK-main` checkout (with role directories) is available locally. - Whether server-side code should be pulled now for broader CIP/WebAuthn integration testing. - Exact enrollment process interface running in `k_client` and how it reaches `k_proxy`. - Upgrade Phase 5 auth gate from card-presence probe to full WebAuthn assertion verification for session creation. - Determine the viable path for real credential registration on `k_proxy`: - enable whatever PIN/UV support the card expects for direct CTAP2 registration, or - adopt a different one-time enrollment path that can persist real credential material for later direct assertion verification. - Restore card visibility inside `k_proxy` so direct probes can reach the card UI again: - `/dev/hidraw*` must exist in `k_proxy` - `fido2_probe.py --list` must detect the card before the raw Yes/No probe can continue - Identify why the host probe hangs before card UI even with `/dev/hidraw0` readable: - determine why CTAPHID `INIT` on the correct FIDO hidraw node receives no reply after reattach - likely recovery targets are the Qubes USB mediation path, a fresh USB reassign, or a `k_proxy` VM/device reset - Precise ownership split of session/user state between `k_proxy` and `k_server`. - Concrete concurrency limits and acceptance criteria (requests/sec, parallel clients, latency/error thresholds).