Phase 1.5 — Mycelium-address proof-of-control login (hero_login_lib + onboarding wiring) #2

Open
opened 2026-05-20 18:25:33 +00:00 by mik-tf · 0 comments
Owner

Parent: #1 (Phase 1.5 block)
Session: s2-003 (Track B, hero_work workspace)
Decision: https://forge.ourworld.tf/lhumina_code/hero_onboarding/blob/development/decisions/D-12-mycelium-proof-of-control-mechanism.md (workspace-internal; not in this repo — lives at hero_work/decisions/D-12-*.md)

Scope

Replace the s2-002 mycelium-address login stub (accepts any string) with real proof-of-control. The user proves they hold the priv_key behind a claimed mycelium address by completing a server-issued challenge.

Ships as:

  • New repo lhumina_code/hero_loginhero_login_lib crate (issue_challenge + verify_response) + optional hero_login_cli for headless smoke testing. Canonical Hero workspace shape.
  • hero_onboarding_server wired to hero_login_lib: new routes POST /login/challenge (form address) and POST /login/verify (form challenge_token, nonce). Legacy /login stub removed.
  • Workspace dep injection through the meta-workspace Cargo.toml in hero_onboarding/.

Mechanism

D-12 locks: message round-trip via mycelium daemon.

Three options were evaluated; the original prompt2.md §3 leaned signed-challenge but Phase B surfaced that mycelium uses x25519 (Diffie-Hellman), not ed25519 — there is no native signing primitive and no /api/v1/admin/sign endpoint. Full enumeration in the workspace-internal memory/investigation_mycelium_auth_api.md.

Option Cryptographic feasibility UX Verdict
(a-ed25519) Signed challenge Requires extending mycelium with an ed25519 keypair alongside x25519 (workspace-wide change) CLI helper to sign + paste signature Rejected — out of s2-003 scope
(a-decrypt) Decryption challenge Server encrypts nonce to user pubkey via DH+AES-GCM; user decrypts Requires user to provide 44-char base64 pubkey + decrypt helper Rejected — duplicates mycelium crypto + UX friction
(b) Message round-trip Mycelium handles DH+AES-GCM internally; nonce flows over the overlay User reads inbound message via existing mycelium message receive CHOSEN — D-12
(c) Local-daemon trust Same-host only Zero UX friction Acceptable as opt-in dev mode (env var); rejected for production

Flow

  1. Browser POST /login/challenge address=<user-addr>
  2. Server generates 32-byte nonce, packages as HMAC-signed challenge token (address, nonce, expires_at)
  3. Server POSTs to local localhost:8989/api/v1/messages with dst={ip: address} + topic hero/login/v1 + payload nonce-hex
  4. Mycelium routes the encrypted message; user's daemon decrypts via priv_key
  5. Server returns HTML showing the challenge token + textarea for the nonce + paste-the-command snippet (curl localhost:8989/api/v1/messages?topic=...&timeout=10)
  6. User retrieves the nonce from their inbox, pastes it in
  7. Browser POST /login/verify challenge_token=<token>&nonce=<hex>
  8. Server HMAC-recomputes the token, checks expiry, constant-time-compares the nonce → 303 to /dashboard + session cookie, OR 401

Server-side stateless — no database row per pending challenge. HMAC-signed tokens carry their own state; reaped naturally on expiry.

Acceptance bar

  1. cargo build --release clean on both lhumina_code/hero_login/ and lhumina_code/hero_onboarding/
  2. E2E smoke against the workstation's mycelium daemon: full flow completes with this machine's own address as both the server's send-target and the user's receive-target
  3. Expiry test: wait 6 minutes after /login/challenge/login/verify returns 401
  4. Tamper test: alter one byte of the challenge token → /login/verify returns 401 ("HMAC mismatch")
  5. Constant-time nonce compare via subtle::ConstantTimeEq (code-review gate)

Out of scope (future sub-issues)

  • Browser-side helper UI to call mycelium message receive for the user (today they paste manually)
  • WebSocket / SSE long-poll for nonce auto-arrival
  • Multi-address auth (linking multiple addresses to one user account)
  • mycelium_sdk UDS-JSON-RPC variant of hero_login_lib (REST-only for now since the workstation runs the legacy daemon)

Tracking

Closes the Phase 1.5 block in #1

**Parent:** https://forge.ourworld.tf/lhumina_code/hero_onboarding/issues/1 (Phase 1.5 block) **Session:** s2-003 (Track B, hero_work workspace) **Decision:** https://forge.ourworld.tf/lhumina_code/hero_onboarding/blob/development/decisions/D-12-mycelium-proof-of-control-mechanism.md (workspace-internal; not in this repo — lives at `hero_work/decisions/D-12-*.md`) ## Scope Replace the s2-002 mycelium-address login stub (accepts any string) with real proof-of-control. The user proves they hold the priv_key behind a claimed mycelium address by completing a server-issued challenge. Ships as: - New repo `lhumina_code/hero_login` — `hero_login_lib` crate (`issue_challenge` + `verify_response`) + optional `hero_login_cli` for headless smoke testing. Canonical Hero workspace shape. - `hero_onboarding_server` wired to `hero_login_lib`: new routes `POST /login/challenge` (form `address`) and `POST /login/verify` (form `challenge_token`, `nonce`). Legacy `/login` stub removed. - Workspace dep injection through the meta-workspace `Cargo.toml` in `hero_onboarding/`. ## Mechanism **D-12 locks: message round-trip via mycelium daemon.** Three options were evaluated; the original `prompt2.md` §3 leaned signed-challenge but Phase B surfaced that **mycelium uses x25519 (Diffie-Hellman), not ed25519** — there is no native signing primitive and no `/api/v1/admin/sign` endpoint. Full enumeration in the workspace-internal `memory/investigation_mycelium_auth_api.md`. | Option | Cryptographic feasibility | UX | Verdict | |---|---|---|---| | (a-ed25519) Signed challenge | Requires extending mycelium with an ed25519 keypair alongside x25519 (workspace-wide change) | CLI helper to sign + paste signature | **Rejected** — out of s2-003 scope | | (a-decrypt) Decryption challenge | Server encrypts nonce to user pubkey via DH+AES-GCM; user decrypts | Requires user to provide 44-char base64 pubkey + decrypt helper | **Rejected** — duplicates mycelium crypto + UX friction | | (b) Message round-trip | Mycelium handles DH+AES-GCM internally; nonce flows over the overlay | User reads inbound message via existing `mycelium message receive` | **CHOSEN** — D-12 | | (c) Local-daemon trust | Same-host only | Zero UX friction | Acceptable as opt-in dev mode (env var); rejected for production | ## Flow 1. Browser POST `/login/challenge` `address=<user-addr>` 2. Server generates 32-byte nonce, packages as HMAC-signed challenge token `(address, nonce, expires_at)` 3. Server POSTs to local `localhost:8989/api/v1/messages` with `dst={ip: address}` + topic `hero/login/v1` + payload `nonce-hex` 4. Mycelium routes the encrypted message; user's daemon decrypts via priv_key 5. Server returns HTML showing the challenge token + textarea for the nonce + paste-the-command snippet (`curl localhost:8989/api/v1/messages?topic=...&timeout=10`) 6. User retrieves the nonce from their inbox, pastes it in 7. Browser POST `/login/verify` `challenge_token=<token>&nonce=<hex>` 8. Server HMAC-recomputes the token, checks expiry, constant-time-compares the nonce → 303 to `/dashboard` + session cookie, OR 401 Server-side **stateless** — no database row per pending challenge. HMAC-signed tokens carry their own state; reaped naturally on expiry. ## Acceptance bar 1. `cargo build --release` clean on both `lhumina_code/hero_login/` and `lhumina_code/hero_onboarding/` 2. E2E smoke against the workstation's mycelium daemon: full flow completes with this machine's own address as both the server's send-target and the user's receive-target 3. Expiry test: wait 6 minutes after `/login/challenge` → `/login/verify` returns 401 4. Tamper test: alter one byte of the challenge token → `/login/verify` returns 401 ("HMAC mismatch") 5. Constant-time nonce compare via `subtle::ConstantTimeEq` (code-review gate) ## Out of scope (future sub-issues) - Browser-side helper UI to call `mycelium message receive` for the user (today they paste manually) - WebSocket / SSE long-poll for nonce auto-arrival - Multi-address auth (linking multiple addresses to one user account) - `mycelium_sdk` UDS-JSON-RPC variant of `hero_login_lib` (REST-only for now since the workstation runs the legacy daemon) ## Tracking Closes the Phase 1.5 block in https://forge.ourworld.tf/lhumina_code/hero_onboarding/issues/1
Sign in to join this conversation.
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
lhumina_code/hero_onboarding#2
No description provided.