k_card/CLAUDE.md

165 lines
11 KiB
Markdown
Raw Permalink 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.

# CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
## Repository policy
- `CR_SDK_CK-main/` is the firmware SDK source tree. Treat it as **read-only**. Do not add or edit files there.
- All host-side scripts live at the workspace root (`/home/user/chromecard`).
- `Setup.md` and `Workplan.md` are the canonical living docs. After each meaningful execution step, update `Setup.md` for environment/runtime state and `Workplan.md` for phase progress and next blocking action.
## Running tests
```bash
# Python unit tests (no VMs or card required, 122 tests)
python3 -m unittest tests/test_k_proxy.py
# Playwright browser regression (requires services running and forwarded portal)
PORTAL_BASE_URL=http://127.0.0.1:18766 npm run test:k-client
# Split-VM end-to-end regression helper (runs from host via SSH into k_client)
./phase5_chain_regression.sh
./phase5_chain_regression.sh --interactive-card --expect-auth-mode fido2_assertion
```
## Probing the card (on k_proxy)
```bash
python3 /home/user/chromecard/fido2_probe.py --list
python3 /home/user/chromecard/fido2_probe.py --json
python3 /home/user/chromecard/raw_ctap_probe.py info
python3 /home/user/chromecard/raw_ctap_probe.py make-credential --rp-id localhost
python3 /home/user/chromecard/webauthn_local_demo.py # opens http://localhost:8765
```
## Generating TLS certificates
```bash
python3 generate_phase2_certs.py
# Writes tls/phase2/ca.crt, k_proxy.crt/.key, k_server.crt/.key
```
## Starting services (split-VM shape)
**k_server VM:**
```bash
python3 /home/user/chromecard/k_server_app.py \
--host 127.0.0.1 --port 8780 \
--proxy-token dev-proxy-token \
--tls-certfile /home/user/chromecard/tls/phase2/k_server.crt \
--tls-keyfile /home/user/chromecard/tls/phase2/k_server.key
```
**k_proxy VM:**
```bash
qvm-connect-tcp 9780:k_server:8780
python3 /home/user/chromecard/k_proxy_app.py \
--host 127.0.0.1 --port 8771 --session-ttl 300 \
--server-base-url https://127.0.0.1:9780 \
--server-ca-file /home/user/chromecard/tls/phase2/ca.crt \
--proxy-token dev-proxy-token \
--tls-certfile /home/user/chromecard/tls/phase2/k_proxy.crt \
--tls-keyfile /home/user/chromecard/tls/phase2/k_proxy.key
# Add --auth-mode fido2-direct for real CTAP2 (default is probe mode)
```
**k_client VM:**
```bash
qvm-connect-tcp 9771:k_proxy:8771
python3 /home/user/chromecard/k_client_portal.py \
--proxy-base-url https://127.0.0.1:9771
# Browser demo page at http://127.0.0.1:8766
```
Files are deployed to VMs via `scp <file> <host>:~` and run via `ssh <host> <cmd>`.
## Architecture
### Target production architecture (4-device system)
Four physical devices: optional client computer, phone, chromecard, server.
**Devices:**
- **Client (optional):** Computer with Component 3 installed. No browser proxy configuration needed.
- **Phone:** Central hub. Runs Component 1 and Component 2, hosts registration page, connects to chromecard via USB or WiFi.
- **Chromecard:** FIDO2 hardware security module. All crypto happens on-card; private keys never leave. Two fingerprint types: *user* (login) and *admin* (registration/deletion).
- **Server:** Accepts TLS only. Runs WebAuthn service that validates FIDO2 tokens before granting access to protected resources.
**Components on the phone:**
- **Component 1 — Proxy + gating filter:** Listens on a local port. Receives requests from the phone's own browser and from external clients via Component 3. Binary decision per request: host is gated → forward to Component 2, receive WebAuthn token back, then call the endpoint with the token (TLS); host is not gated → forward directly to internet on port 80 (no TLS).
- **Component 2 — WebAuthn client + URL recognition:** Receives requests from Component 1. Always returns a WebAuthn token to the caller — never calls endpoints itself. Detects registration-URL → triggers admin registration flow (admin fingerprint); other gated URLs → triggers FIDO2 assertion flow (contacts card, gets token, returns token to Component 1).
- **Registration page:** Local web app on phone. Requires admin fingerprint on the card for enrollment/deletion.
**Component 3 (on external client):**
- Installed on external client computers; replaces the old browser-proxy-configuration approach.
- Finds the phone on the network (currently via hardcoded IP+port — TODO: rendezvous mechanism).
- Forwards validation requests to Component 1, receives WebAuthn token back, calls the protected endpoint directly, and returns the response to the browser.
- Must be a compiled binary that runs without a specific runtime. Recommended: **Go** (single static binary, cross-platform). Alternative: Rust (stronger memory guarantees, higher implementation complexity).
**Three flows:**
- **Flow A (authenticated access — phone browser):** Browser → Component 1 → Component 2 → Card (user fingerprint, generates FIDO2 token) → token returned to Component 1 → Component 1 calls endpoint (TLS) → resource returned.
- **Flow A (authenticated access — external client):** Browser → Component 3 → Component 1 → Component 2 → Card (user fingerprint) → token returned to Component 1 → token returned to Component 3 → Component 3 calls endpoint (TLS) → resource returned to browser.
- **Flow B (registration):** Browser → Component 1 → Component 2 (detects registration URL) → Card (admin fingerprint) → user created/deleted on card.
- **Flow C (unauthenticated):** Host not gated → Component 1 forwards directly to internet via port 80 (unencrypted, bypasses Component 2 and card). By design for normal web traffic.
**Open architectural decisions:**
- PIN on card (in addition to biometrics) — not yet decided
- User database location: on-card only vs. external — not yet decided
- Network-level access control on registration page — not yet decided
- Rendezvous mechanism for Component 3 to discover the phone — not yet decided
- iOS requires a push-relay component (APNs) for background operation; Android does not — platform priority not yet decided
### Development topology (Qubes 3-VM)
**Qubes 3-VM topology:** `k_client``k_proxy``k_server`, each a Debian 13 AppVM.
Inter-VM transport uses `qvm-connect-tcp` localhost forwarding (not raw VM-IP routing). Validated chain:
- `k_client localhost:9771``k_proxy:8771`
- `k_proxy localhost:9780``k_server:8780`
**k_proxy_app.py** — session gateway and FIDO2 auth bridge. Two auth modes:
- `probe` (default): validates card presence by subprocess-calling `fido2_probe.py --json`
- `fido2-direct`: performs real CTAP2 `makeCredential`/`getAssertion` against the physical card via `python-fido2`; auto-detects the FIDO hidraw device
`ProxyState` holds all server-side state: in-memory session store (guarded by one lock), enrollment DB (JSON file), and an `UpstreamPool` of persistent TLS connections to k_server. Sessions are lost on restart.
**k_server_app.py** — protected resource backend. Exposes a monotonic counter behind `X-Proxy-Token` auth. Counter state is in-memory only; resets on restart. Lock guards counter increments.
**k_client_portal.py** — thin browser-facing portal in k_client. Delegates all auth and resource calls to k_proxy. Holds only a local preferred username; enrollment and session state live in k_proxy.
**FIDO2 transport:** Card communicates over USB HID (CTAPHID) on `/dev/hidraw0` (FIDO interface, usage page `0xF1D0`). `/dev/hidraw1` is a separate vendor HID interface. If the card re-enumerates, k_proxy auto-detects the correct node. If CTAPHID stops responding, a full USB power cycle is the recovery path.
**CardEmulator** (`tests/card_emulator.py`) — software emulator of the card for unit tests. Implements `make_credential`/`get_assertion` with real P-256 crypto; `user_confirms=False` simulates card rejection. Wire it into tests by patching `_with_direct_ctap2` and `_drop_direct_device` on `ProxyState`. See the module docstring for the exact patch pattern.
**Key enrollment endpoints on k_proxy:** `POST /enroll/register`, `GET /enroll/status`, `POST /enroll/update`, `POST /enroll/delete`, `GET /enroll/list`. Usernames are normalized to lowercase, 332 chars `[a-z0-9._-]`.
**Key session endpoints on k_proxy:** `POST /session/login`, `POST /session/status`, `POST /session/logout`, `POST /resource/counter`.
### k_phone Flutter app (Phase 9 — replaces k_proxy)
**`k_phone/lib/filter_proxy.dart`** — Component 1. Raw-socket HTTP proxy with gating filter. Per-connection: gated host → fetches bearer token from Component 2 (`POST /auth/get-token`), then calls endpoint directly with `Authorization: Bearer`; non-gated → direct to target. HTTPS CONNECT to gated host: relays CONNECT through Component 2 (session-gate check). Gated hosts loaded from `gated_hosts.txt` in app documents dir; defaults to `httpbin.org`. Use `setGatedEntries()` in tests to inject entries directly.
**`k_phone/lib/proxy_service.dart`** — Component 2. Background-service HTTP server (port 8771). Handles enrollment, session (login/status/logout), `/auth/get-token`, and CONNECT tunnels. Returns bearer token to caller via `/auth/get-token`; never calls endpoints itself. For CONNECT: checks `hasAnyActiveSession()`, connects to the actual upstream host:port, detaches the socket, and pipes bytes bidirectionally.
**`k_phone/lib/portal_html.dart`** — HTML string constants (`kPortalHtml`, `kEnrollHtml`) and pre-encoded byte lists (`kPortalHtmlBytes`, `kEnrollHtmlBytes`) for the portal and enrollment pages served by Component 2.
**`k_phone/lib/session_manager.dart`** — in-memory session store. `hasAnyActiveSession()` is the gate check for proxied traffic (personal-device model: one live session authorises all gated requests). `SessionManager.ttlSeconds` is the public TTL constant (300 s).
**`k_phone/lib/fido2_ops.dart`** — `makeCredential`, `getAssertion`, ECDSA-P256 assertion verification against the card via CTAPHID.
**`k_phone/lib/ctaphid_channel.dart`** — CTAPHID framing over USB (Kotlin platform channel) or emulator bridge TCP socket (`card_emulator_bridge.py` on `10.0.2.2:8772`).
**`k_phone/lib/enrollment_db.dart`** — enrollment model + JSON persistence via path_provider.
**Tests:** `flutter test test/filter_proxy_test.dart` and `flutter test test/enrollment_test.dart` (no device needed).
## Known limits and blockers
- Concurrency ceiling on the browser-facing forwarded path is ~10 in-flight requests; higher fan-out triggers Qubes vchan failures (`xs_transaction_start: No space left on device`).
- If CTAPHID `INIT` packets get no reply after a card reattach, a full USB power cycle recovers the transport.
- `CR_SDK_CK-main` is missing role directories (`mvp`, `setup`, `components`, `samples`) required for the firmware build/flash flow (`./scripts/build_flash_mvp.sh`). `west` and `nrfjprog` must also be installed.
- Phase 7 (firmware build): blocked on Chrome Roads (card vendor).
- Phase 9 (phone): Component 2 CONNECT handler implemented. HTTPS tunnels to gated hosts are gated by `hasAnyActiveSession()`; the tunnel connects to the actual upstream target (not k_server).