165 lines
11 KiB
Markdown
165 lines
11 KiB
Markdown
# 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, 3–32 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).
|