Repository: ale18V/ZKTicket
Description: A privacy-preserving ticketing system developed by me during LauzHack2025
Primary language: TypeScript

# Anonymous WebAuthn Ticket PoC

This repository contains a minimal end-to-end demo of the Lauzhack specification: a printable and anonymous ticket that is bound to a hardware-backed WebAuthn credential and can be challenged by train inspectors without revealing personal data.

## Architecture Overview

```mermaid
flowchart LR
  subgraph Customer["Customer Browser"]
    CUICustomer[Customer UI]
    LedgerUI[Ledger page]
    MigrationUI[Migration portal]
  end
  subgraph Device["Secure Hardware"]
    AuthKey[WebAuthn Credential]
  end
  subgraph Backend["Backend API (Express)"]
    API[Ticket API]
    Store[(Persistent Store)]
  end
  subgraph Inspector["Inspector Browser"]
    InspUI[Inspector UI]
  end

  CUICustomer --> LedgerUI
  LedgerUI --> API
  CUICustomer --> MigrationUI
  MigrationUI --> API

  CUICustomer -- vouchers + trip --> API
  API --> CUICustomer
  CUICustomer -- navigator.credentials.create --> AuthKey
  AuthKey --> CUICustomer
  CUICustomer -- signed attestation --> API
  API --> Store
  API --> CUICustomer
  CUICustomer --> QR[Printable QR Payload]
  InspUI --> API
  API --> InspUI
  InspUI -- challenge ID --> CUICustomer
  CUICustomer -- navigator.credentials.get --> AuthKey
  CUICustomer --> API
  API --> InspUI
```

**How it works**

1. The customer UI collects trip details and voucher, fetches WebAuthn registration options from the backend, and creates a resident credential on secure hardware.
2. The backend verifies the attestation payload, signs a ticket manifest, and returns the printable QR metadata.
3. Inspectors scan the QR payload, request a challenge tied to the ticket’s credential, and share the generated `challengeId`.
4. The customer UI signs the challenge with the stored credential; the backend validates the signature and updates the challenge status for the inspector console.

> **Voucher workflow:** entering a voucher code still keeps the “one-use” promise, but the field is optional; if you leave it blank the backend skips redemption and keeps the demo friction-free.

## Design principles

- **Multi-check friendly:** Tickets are never invalidated mid-ride; they remain usable for repeated inspections until the `validUntil` window lapses.
- **Print-first artifacts:** QR payloads contain only public trip metadata so riders can print them safely, similar to paper passes highlighted by ZKPassport/ZKTPS.
- **ZK proof-of-ownership:** Tickets mint elliptic-curve commitments and riders answer inspections with Schnorr proofs so inspectors/backend only learn “someone holds a valid ticket”, never which ticket.
- **On-device wallet:** Each browser keeps an encrypted-ish wallet of ticket IDs locally (no central accounts). Riders can switch between saved tickets without reissuing credentials.
- **Guided planner:** `/plan` now includes Swiss “quick hop” presets, animated step indicators, and ride cards that must be selected before WebAuthn starts so riders always know which step they are on.
- **Seat-aware bookings:** Each mock ride carries a backend seat counter that decrements once a ticket is finalized; the planner shows live `seats left`, greys out sold-out trips, and the API refuses new reservations when capacity reaches zero.
- **Mock planning data:** `/plan` autocompletes a curated list of major Swiss stations (Bern, Zürich HB, Geneva, Lausanne, Basel, Lucerne) and serves multiple mock rides per day for each route.
- **Ticket lifecycle:** Wallet surfaces upcoming/active/expired chips, lets riders purge stale tickets, and offers “reschedule” + print-ready views without exposing PII.
- **Inspector handshake & receipts:** The inspector console emits rider-facing QR payloads, copy-to-clipboard challenge IDs, and screenshot-ready “verified” posters so staff can prove checks without leaking ticket IDs.
- **Offline readiness & ledger snapshots:** Inspectors can refresh a cached ledger of ticket commitments, snapshot it for offline auditing, and consult it alongside the verification log when connectivity is patchy.
- **Persistent inspector timeline:** The verification log now persists per shift so inspectors can revisit recent checks even if they refresh the page, fulfilling the “inspection timeline” requirement.
- **Dataset-powered planning:** `/api/stations` now streams the Swiss service point CSV (`data/actual_date-swiss-only-service_point-2025-11-20.csv`) for realistic autocomplete suggestions, while the UI still highlights the curated hubs.
- **Secure device migration:** Riders can mint a short-lived migration token, rebind the ticket to another browser via `/migrate`, and carry over the Schnorr secret without issuing a fresh voucher.
- **Self-service rescheduling:** Riders can rebind an issued ticket to a later mock departure, refreshing the QR + issuer signature without minting a new WebAuthn credential.

## Frontend overview

- `/` — Wallet home showing every locally saved ticket with quick “Use/Remove” controls, a swipeable preview rail, a dedicated **Ledger** button, and a “Present” overlay for inspector-friendly, full-screen QR display.
- `/tickets/:ticketId` — Ticket detail page containing the printable QR, the inspector challenge panel, a rescheduling workspace, and migration controls to generate single-use transfer codes.
- `/plan` — Planner/checkout flow with station autocomplete, Swiss “quick hop” presets, a visible three-step progress indicator, and animated ride cards that must be tapped before issuing a ticket. Once a ride is locked, the flow redeems the voucher and mints the local ZK secret.
- `/migrate` — Minimal portal that accepts a migration code, runs WebAuthn registration on the new device, and restores both the ticket metadata and Schnorr secret.
- `/ledger` — Human-friendly ledger view for riders, operators, or judges who want to inspect every commitment or download the current snapshot.
- `/api/stations` — Backend search endpoint over the curated station list.
- `/api/rides` — Returns mock rides for a given origin/destination/date combination.
- `/api/challenges/:id/zk-verify` — Verifies the Schnorr proof derived from the local ticket secret.
- `/api/tickets/:ticketId/reschedule` — Rebinds a ticket to another mock ride (same origin/destination), recomputing the issuer signature + QR payload.
- `/api/tickets/:ticketId/migrations` — Generates a short-lived migration token that carries the ticket’s ZK secret to a new device.
- `/api/migrations/:id/registration-options` & `/api/migrations/:id/finalize` — Second-device handshake that mints a new WebAuthn credential and persists the migrated ticket.
- `/api/ledger` — Provides a live snapshot of every ticket commitment so inspectors can audit rails based on the same commitments we’ll eventually publish on-chain.
- `/ledger` — Public ledger page that mirrors the inspector view so riders/operators can inspect every commitment even without direct backend access.
- `/api/tickets/:ticketId/migrations` — Reveals current migration tokens (status + expiry) so the ticket UI can surface them in the migration section.
- The inspector ledger card includes a scrollable table that surfaces every stored commitment, along with caching/JSON-export controls for offline audits.
- Inspectors can also filter the ledger by origin/destination to focus on one route before screenshotting or downloading the snapshot.
- The inspector ledger card now lets staff cache the snapshot for offline use, clear the cache, or download the ledger JSON for auditing.

## ZK + blockchain preview

- Every ticket now receives a deterministic SHA-256 commitment computed from trip metadata + credential ID. You can fetch it via `GET /api/tickets/:id/commitment` and publish it on a blockchain (future work) for public auditing.
- The commitment is the building block for zk proofs: a rider can eventually prove “I control the credential bound to commitment C and the ticket is still valid” without sharing the credential itself.
- Planned milestones:
  1. Publish commitments + nullifiers on-chain to power anonymous revocation/audit trails.
  2. Replace WebAuthn assertions with zk proofs derived from these commitments.
  3. Extend the `/plan` flow so itinerary data flows into a zk circuit alongside voucher/resale logic.

## Requirements

- Node.js 20.18+ (Vite prints a warning until you upgrade to 20.19 or 22.12, but the build/dev servers still run on 20.18).
- Modern browser with WebAuthn and WebUSB/NFC/Biometric support (Chrome/Edge/Safari on desktop are fine) for the customer app.
- Set `AUTHENTICATOR_ATTACHMENT=platform` (or `cross-platform`) in the backend `.env` to enforce platform-key-only enrollment; the API already requires `userVerification: required` so inspectors get a stronger guarantee about hardware-based authenticators.

## Setup

```bash
npm install
cp frontend/customer/.env.example frontend/customer/.env
cp frontend/inspector/.env.example frontend/inspector/.env
```

The default `.env` files point the apps to `http://localhost:4000`. Override `VITE_API_BASE_URL` if you proxy the API elsewhere. The backend accepts optional `PORT`, `RP_ID`, `CUSTOMER_ORIGIN`, `INSPECTOR_ORIGIN`, or `ISSUER_PRIVATE_KEY` env vars.
For hackathon velocity the backend currently allows CORS from any origin; place it behind a gateway or tighten the middleware before exposing it publicly.

## Running the stack

In three terminal tabs:

1. **Backend**
   ```bash
   cd backend
   npm run dev
   ```
   Exposes REST API + WebAuthn helpers on `http://localhost:4000`.

2. **Customer frontend**
   ```bash
   cd frontend/customer
   npm run dev -- --port 5173
   ```
   Visit `http://localhost:5173` over HTTPS or `http://localhost` (Chrome treats localhost as secure) to buy tickets.

3. **Inspector frontend**
   ```bash
   cd frontend/inspector
   npm run dev -- --port 5174
   ```
   Open `http://localhost:5174` to request challenges and poll verification status. If you need mobile camera access (iOS/Safari), serve the inspector UI over HTTPS (e.g., `npm run dev -- --host 0.0.0.0 --https`) because browsers block camera APIs on plain HTTP.

For the customer UI, scanning an inspector challenge QR also requires HTTPS/localhost; host it the same way when testing on mobile.

> Note: The backend now defaults to requiring `userVerification` and will respect `AUTHENTICATOR_ATTACHMENT`. Passkeys in some browsers that do not expose hardware tokens may need `cross-platform` if platform-only credentials are unavailable.
## Demo flow

1. On `/plan`, enter trip metadata and a demo voucher (e.g. `VCHR-DEMO-001`), then click **Issue Ticket**. Approve the WebAuthn prompt when the browser asks you to touch your authenticator. The app mints a local ZK secret, navigates to the new ticket page, and never uploads that secret.
2. On the inspector UI, paste the QR payload (simulating a scan). Verify backend data, then click **Issue challenge**. Show the rider-facing QR or copy the payload so the passenger can ingest the challenge with a single scan.
3. On the ticket detail page, scan the inspector’s challenge QR (or paste the ID) and hit **Prove Ownership (ZK)**. The browser creates a Schnorr proof from the local secret, and the backend verifies it against the ride’s commitment set. From the same view you can download the print-ready PDF to hand to inspectors that prefer paper.
4. The inspector refreshes the challenge status to see whether the signature was valid. A ✅ indicates the passenger’s device holds the bound credential; once verified the console renders a high-contrast poster that can be screenshot for auditing.
5. Inspectors or riders can open `/ledger` to replay the public commitment list, filter by route, or download the current snapshot without touching the inspector cockpit.
5. Need to switch devices? Generate a migration code from `/tickets/:id`, then visit `/migrate` on the target device to rebind the WebAuthn credential + ZK secret.


## Useful scripts

- `./scripts/run-dev.sh` – start backend+customer+inspector dev servers in parallel (requires `npm install` already run).
- `npm run dev --workspace backend|customer|inspector` – start one workspace locally.
- `HOST=0.0.0.0 npm run dev --workspace backend` – expose the backend on all interfaces for multi-device demos (the Express server now binds to `HOST` from `backend/src/config.ts`).
- `scripts/setup-mkcert.sh` – runs `mkcert` to generate TLS certificates for every host/IP you need (defaults to `0.0.0.0`, `localhost`, `127.0.0.1`) and reminds you to install the CA on mobile devices so the HTTPS demo keeps WebAuthn happy.
- `npm run build` – type-checks + builds every workspace (customer/inspector builds emit a Node 20.19 warning if you stay on 20.18).
- `npm run lint --workspace backend` – lint backend sources; add similar scripts to each frontend if you enable linting.
- `npm run test --workspace backend` – run the Vitest suite covering persistence helpers, commitments, and Schnorr verification logic.
- `node scripts/check-ledger.mjs` – sanity-check the ledger endpoint from the command line (set `API_BASE` env if your API runs elsewhere).
- `node scripts/export-state.mjs` – dump counts for vouchers/tickets/challenges from the persisted store to verify demo data.
- `node scripts/reset-state.mjs` – remove `backend/data/state.json` so the demo starts with fresh vouchers and no tickets (restart the backend afterwards).
- `node scripts/print-stations.mjs` – print the first 20 station names extracted from the Swiss dataset to verify the loader.
- `node scripts/check-migrations.mjs <ticketId>` – inspect active migration tokens for a ticket so you can verify their statuses.
- `node scripts/check-rides.mjs [from] [to] [date]` – verify `/api/rides` returns mock journeys for a given route+day (defaults to Lausanne→Zürich today).
- `node scripts/ledger-summary.mjs` – print the top three routes in the ledger for quick audit checks.
- `node scripts/check-challenges.mjs <challengeId>` – inspect the status of a challenge so you can confirm the ledger logs the verification.
- `node scripts/print-vouchers.mjs` – list the remaining vouchers in `backend/data/state.json` (run after the backend is up).

Ticket data is persisted inside `backend/data/state.json`. Delete that file if you need to reset vouchers, pending tickets, or challenge history between demos (issuer keys still reset unless you provide `ISSUER_PRIVATE_KEY`/`ISSUER_PUBLIC_KEY`).

Automated Vitest coverage now exists for backend helpers (store lifecycle, commitment hashing, and Schnorr verification). Run `npm run test --workspace backend` after dependency changes or before demos so you can confidently demo the persistence + ZK flow.

## Further reading

- `docs/zkpassport-comparison.md` – evolving comparison against the ZKPassport project’s architecture and privacy guarantees.
- `docs/inspector-playbook.md` – step-by-step instructions for sharing challenges, caching ledgers, and explaining the inspector UX.
- `docs/migration-playbook.md` – focused steps for generating/using migration codes and verifying their history.
- `docs/issuer-ops.md` – key management guidance for signing tickets, rotating issuer keys, and keeping revocation transparent.
- `docs/resale-ideas.md` – concept sketches for anonymous resale / voucher escrow that might follow the MVP.
- `docs/testing.md` – checklist for manual flows and lint commands to verify the stack.
- `docs/snark-roadmap.md` – ideas for replacing the Schnorr inspector proof with a zk-SNARK or STARK circuit.
- `docs/deployment.md` – checklist for env vars, launching each workspace, and resetting the persisted state between runs.
- `docs/dataset-guide.md` – how we parse the Swiss service-point CSV and what each column means for our planner UI.
- `docs/vision.md` – narrative backlog guiding the hackathon path from MVP to blockchain + resale + NFC ambitions.
- `docs/pitch-notes.md` – quick highlights to mention during the hackathon demo.
- `docs/privacy.md` – a short summary of how the platform stays anonymous for riders while satisfying inspections.
- `docs/faq.md` – common questions about ledger snapshots, migration, and how the Utah approach matches ZKPassport.
- `docs/architecture.md` – compact overview of frontend, backend, and persistence responsibilities.
- `docs/ux-heuristics.md` – quick design rules to keep everything minimal, secret-safe, and inspector-friendly.
- `docs/product-scenarios.md` – narrative flows for buying, inspecting, and migrating tickets during the demo.
- `docs/monitoring.md` – operations checklist for ledger growth, migrations, and backend state health.
- `docs/webrtc-ideas.md` – brainstorming how to pair inspector and rider devices via WebRTC if QR sharing proves limiting.
- `docs/known-issues.md` – tracked lint warnings (fast-refresh + loadTickets deps) that still need resolving.
- `docs/commitment-model.md` – describes the SHA-256 ticket commitment and how inspectors can cross-reference it with the ledger.
- `docs/ftue.md` – first-time user checklist for the demo stack and scripts.