k_card/Setup.md

816 lines
50 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Setup
Last updated: 2026-05-08
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 <host> <cmd>`
- file copy to VM home: `scp <file> <host>:~`
- 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 <target> 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=<name>`
- 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, k_client browser flow page):
- `k_client_portal.py` now also serves a local browser demo page again on `http://127.0.0.1:8766` inside `k_client`.
- The page is useful as an operator/demo surface:
- register user
- login with card approval or denial in `k_proxy`
- call the protected `k_server` counter
- logout
- The page now also exposes current proxy enrollment state:
- shows the registered users visible in `k_proxy`
- lets the operator select a listed user into the username field
- lets the operator unregister users from the browser page
- login now uses the current username field instead of only the portal's last remembered user
- Added a browser regression harness for the `k_client` page:
- `/home/user/chromecard/tests/k_client_portal.spec.js`
- `/home/user/chromecard/playwright.config.js`
- `/home/user/chromecard/package.json`
- intended flow: register, login, call `k_server`, logout, unregister
- verified passing live on 2026-04-25 from this host via forwarded portal URL:
- `PORTAL_BASE_URL=http://127.0.0.1:18766 npm run test:k-client`
- It also makes the negative path explicit:
- if login is denied on the card, the page reports that `k_server` was not called
- Primary browser-facing app logic still lives on `k_proxy`, but the `k_client` page is now a concrete demo/control surface rather than just a redirect.
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=<name>`
- `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`
- direct mode no longer depends on a fixed `/dev/hidraw0` path
- after a later re-enumeration where the card appeared on `/dev/hidraw1`, `k_proxy_app.py` was patched to probe available `/dev/hidraw*` nodes and select the first working CTAPHID device automatically
- browser registration then worked again without changing the configured `--direct-device-path`
- 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
Session note (2026-04-27, fido2-direct end-to-end browser validation):
- Deployed all three services (k_server, k_proxy, k_client_portal) in split-VM chain via SSH/SCP.
- k_proxy restarted with --auth-mode fido2-direct.
- Full browser flow verified from k_client at http://127.0.0.1:8766 with real card:
- Register: makeCredential triggered on card, button press confirmed.
- Login: getAssertion triggered on card, button press confirmed.
- Counter: k_server returned incremented value.
- Logout: session correctly invalidated.
- Confirmed: probe mode showed stale directtest enrollment (no credential_data_b64) from earlier session; that is expected.
- Bug found and fixed: clicking Register after Login cleared the client-side session token but left the server-side session alive; fix adds a best-effort /session/logout call to k_proxy before re-enrolling.
- Current deployed service state:
- k_server: https://127.0.0.1:8780, TLS, proxy-token dev-proxy-token
- k_proxy: https://127.0.0.1:8771, TLS, --auth-mode fido2-direct, upstream https://127.0.0.1:9780
- k_client: http://127.0.0.1:8766, proxy-base-url https://127.0.0.1:9771
- Forwards: k_proxy 9780->k_server:8780, k_client 9771->k_proxy:8771
- Unit test suite added: tests/test_k_proxy.py (100 tests, all passing, run locally with python3 -m unittest tests/test_k_proxy.py).
Session note (2026-04-26, markdown maintenance re-scan):
- Re-read the maintained workspace markdown set:
- `/home/user/chromecard/Setup.md`
- `/home/user/chromecard/Workplan.md`
- `/home/user/chromecard/PHASE5_RUNBOOK.md`
- Re-checked that the currently referenced runtime artifacts still exist in the workspace:
- `k_proxy_app.py`
- `k_server_app.py`
- `k_client_portal.py`
- `phase5_chain_regression.sh`
- `raw_ctap_probe.py`
- `generate_phase2_certs.py`
- `tls/phase2/ca.crt`
- `tls/phase2/k_proxy.crt`
- `tls/phase2/k_server.crt`
- Current documentation conclusion:
- the workspace still supports the HTTPS localhost-forwarded split-VM chain as the active baseline
- direct FIDO2 enrollment/login support exists in code and is documented as an optional follow-up path, not the default deployed runtime
- the main unresolved engineering limit is still the higher-fan-out Qubes forwarding ceiling on the browser-facing path, not basic chain bring-up
Session note (2026-04-27, card emulator and bug fixes):
- Added software emulator of the ChromeCard FIDO2 authenticator:
- `/home/user/chromecard/tests/card_emulator.py`
- implements `make_credential` and `get_assertion` with real P-256 cryptography
- in-memory credential store keyed by credential ID (matching firmware layout)
- auth_data byte layout and COSE key encoding mirror `fido_make_cred.c` / `fido_get_assertion.c` exactly
- `user_confirms=True/False` parameter simulates the card's Yes/No confirmation dialog
- `refusing()` method returns a wrapper that forces `user_confirms=False` for integration test paths
- `forget_user(username)` simulates card-side credential removal
- module docstring is the usage guide
- Fixed two bugs in `k_proxy_app.py` that were silently breaking fido2-direct mode:
- `RegistrationResponse(id=..., ...)``RegistrationResponse(raw_id=..., ...)` (fido2 2.2.0 API)
- `AuthenticationResponse(id=..., ...)``AuthenticationResponse(raw_id=..., ...)` (same)
- both calls raised `TypeError` at runtime, caught by the surrounding `except`, so register and
authenticate in fido2-direct mode always returned failure without any visible error
- Extended test suite: 22 new tests across `TestCardEmulatorUnit` and `TestCardEmulatorIntegration`
- covers: register, authenticate, user-says-no (register and auth), forget, two-user isolation,
sign-count monotonicity, wrong RP rejection, empty allow-list rejection
- total test count is now 122, all passing locally without card or VMs
Session note (2026-05-09, Android Playwright tests passing):
- All 4 tests in `tests/k_phone_android.spec.js` now pass (16 s total on emulator).
- Root cause 1: `playwright.android.devices()[0].launchBrowser()` hangs indefinitely on Chrome 145 in the emulator. Replaced with: write `--proxy-server=127.0.0.1:8888` to `/data/local/tmp/chrome-command-line` via adb, force-stop + restart Chrome, forward `tcp:9222 localabstract:chrome_devtools_remote`, and connect via `chromium.connectOverCDP()`. CDP becomes ready within 23 s; a polling retry loop (max 15 s) handles variance.
- Root cause 2: `card_emulator_bridge.py` TCP socket in the Flutter app becomes stale when the bridge process is restarted. `_cardAttached` and `_cardCid` remained set in `proxy_service.dart` even after the Dart socket `onDone` fired. Added `_ensureCardOpen()` in `proxy_service.dart`, called before `makeCredential` (enrollment) and `getAssertion` (login and `/auth/get-token`). The method calls `isCardAttached()` and, if the socket is closed, re-runs `_tryOpenCard()` to reconnect.
- Global Playwright test timeout reduced from 180 s to 60 s in `playwright.config.js`. No test should need more than 60 s (FIDO2 assertion via CardEmulator bridge is instant).
- `adb` path: discovered at `~/Library/Android/sdk/platform-tools/adb` (not in system PATH). The spec file now auto-detects it without requiring a modified PATH.
- `card_emulator_bridge.py` must be running before the first card operation. The bridge does not need restarting between test runs — `_ensureCardOpen()` in the Flutter app reconnects automatically after a bridge restart.
Session note (2026-05-08, per-request token binding + Playwright acceptance tests):
- Per-request FIDO2 token binding implemented across the full stack:
- `k_phone/lib/fido2_ops.dart`: `GetAssertionResult` carries `clientDataJson`; `getAssertion()` accepts optional bound challenge.
- `k_phone/lib/proxy_service.dart`: `_handleAuthGetToken` rewritten — accepts `{url, method, nonce}`, derives `challenge = SHA256(url|method|nonce)`, returns a self-contained assertion bundle as base64url Bearer token. No session created.
- `k_phone/lib/filter_proxy.dart`: `_getAuthToken(uri, method)` generates a 16-byte secure nonce, POSTs `{url, method, nonce}` to Component 2.
- `component3/phone.go`: rewritten as stateless `GetTokenForRequest(url, method)` — no session cache, no mutex.
- `k_server_app.py`: `_verify_assertion_token()` added — verifies path+method, challenge, and ECDSA-P256 signature from the self-contained bundle. `_is_proxy_authorized()` accepts legacy `X-Proxy-Token` or `Authorization: Bearer <bundle>`.
- Test coverage added:
- `tests/test_k_server.py`: 17 Python tests for `_verify_assertion_token` — 12 unit + 5 CardEmulator round-trips. All pass.
Run: `uv run --python 3.12 --with fido2 --with cbor2 --with cryptography python3 -m unittest tests/test_k_server.py`
- `k_phone/test/filter_proxy_test.dart`: 2 new tests verify `/auth/get-token` body fields. 48/48 pass.
- Playwright acceptance tests added (three specs, all in `tests/`):
- `k_phone_portal.spec.js`: portal UI flow — enroll → login → status → list → logout → delete. DOM assertions only; no phone screen needed.
Run: `K_PHONE_BASE_URL=http://phone-ip:8771 npx playwright test tests/k_phone_portal.spec.js`
- `k_phone_proxy.spec.js`: 4 serial proxy-routing tests using Node `http` module.
1. No users → non-gated passes. 2. No users → gated rejected (407). 3. Enroll (card) → non-gated still passes. 4. Gated succeeds with card assertion (200 + Bearer token in response).
Run: `K_PHONE_PROXY=http://127.0.0.1:8888 K_PHONE_BASE_URL=http://127.0.0.1:8771 npx playwright test tests/k_phone_proxy.spec.js` (requires `adb forward tcp:8888 tcp:8888 && adb forward tcp:8771 tcp:8771`)
- `k_phone_android.spec.js`: same 4 tests but Chrome runs inside the Android emulator via Playwright Android (`playwright.android.devices()`). No adb port-forward needed — `127.0.0.1:8888` is Component 1 from inside the emulator. Auto-skips if no ADB device found.
Prerequisite: `npm install playwright` + card_emulator_bridge.py running.
Run: `npx playwright test tests/k_phone_android.spec.js [--headed]`
- card_emulator_bridge.py auto-approves all FIDO2 operations instantly — no physical fingerprint or card needed for emulator tests. The `CARD_REGISTRATION_TIMEOUT_MS` / `CARD_LOGIN_TIMEOUT_MS` timeouts exist only for physical ChromeCard use.
- Flutter analyze: no issues. `go build ./...`: clean. 48/48 Flutter tests pass.
Session note (2026-04-29, Phase 9 k_phone bring-up):
- Phase 9 approved and started: Flutter Android app (`k_phone`) replaces `k_proxy` in the auth chain.
- Development is happening on Mac (not Qubes) — Android emulator is incompatible with Qubes' Xen hypervisor.
- Mac environment:
- Flutter SDK installed (stable channel)
- Android Studio installed with API 37 emulator (`Pixel_7_Pro_API_37`)
- Python package manager: `brew install uv` used as workaround — macOS 26 beta broke `pip` on both Python 3.14 (Homebrew default) and Python 3.12 due to libexpat ABI mismatch
- `k_phone` Flutter project scaffolded at `/Users/mortenv.christiansen/Desktop/chromecard/k_phone/`
- Kotlin `MainActivity.kt` registers USB HID platform channel (`com.chromecard.kphone/usb_hid`)
- `lib/ctaphid_channel.dart`: CTAPHID framing/fragmentation + two transports (USB MethodChannel and emulator TCP socket)
- `lib/proxy_service.dart`: background service HTTP proxy (flutter_background_service v5)
- `lib/session_manager.dart`: in-memory bearer token sessions with TTL
- `lib/k_server_client.dart`: HTTP forwarder to k_server (:8780)
- `android/app/src/main/kotlin/com/chromecard/kphone/MainActivity.kt`: USB HID platform channel implementation
- Build issues resolved (10+ iterations):
- AGP bumped to 8.7.3, Gradle wrapper to 8.10.2, Kotlin to 2.1.0
- Foreground service type changed from `connectedDevice` to `dataSync` for emulator compatibility
- Notification channel created natively in `MainActivity.onCreate()` before service starts
- `MissingPluginException` caught in all USB channel calls (USB plugin not registered in background isolate)
- Core library desugaring enabled with `desugar_jdk_libs:2.1.4`
- Network security config added to allow cleartext to `10.0.2.2` (Mac host alias in Android emulator)
- Card emulator bridge added: `tests/card_emulator_bridge.py`
- asyncio TCP server on `127.0.0.1:8772`
- bridges CTAPHID packets from Android emulator to Python `CardEmulator`
- handles CTAPHID INIT (CID allocation), multi-packet reassembly, CBOR dispatch to `CardEmulator`
- run with: `uv run --python 3.12 --with fido2 --with cbor2 --with cryptography tests/card_emulator_bridge.py`
- End-to-end bridge verified: app reports `Card open, CID=0x1` — CTAPHID handshake with CardEmulator confirmed
- Current status (2026-04-29, emulator FIDO2 verified):
- App builds and runs on Android emulator
- Service auto-starts (`autoStart: true` for testing; revert to `false` for production)
- USB transport falls back to emulator TCP bridge on `10.0.2.2:8772`
- FIDO2 endpoints fully implemented (enrollment_db.dart, fido2_ops.dart, proxy_service.dart)
- Three bugs fixed during emulator integration:
1. CTAP2 command prefix bytes missing from CTAPHID CBOR payload (fido2_ops.dart)
2. Socket single-subscription stream bug — `await for ... break` cannot be reused (ctaphid_channel.dart)
3. `on StateError` catch masked socket write errors as "user already enrolled" (proxy_service.dart)
- Verified end-to-end on emulator with CardEmulator bridge:
- `/enroll/register` → makeCredential → `has_credential: true`
- `/session/login` → getAssertion + ECDSA verify → `auth_mode: fido2_assertion`
- `/session/status`, `/session/logout`, post-logout 401 — all correct
- `/resource/counter` fails (k_server not running in Mac test env — expected)
- Next step: deploy to real Android phone, test USB HID path with physical ChromeCard
## 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).