- Rust 100%
Canonical Hero workspace shape (single-crate). issue_challenge() POSTs an encrypted nonce to the local mycelium daemon's /api/v1/messages, addressed to the user's claimed address. verify_response() HMAC-recomputes the challenge token and constant-time-compares the returned nonce. Mechanism locked in hero_work/decisions/D-12-mycelium-proof-of-control- mechanism.md. Mycelium uses x25519 Diffie-Hellman (NOT ed25519); message- round-trip leverages mycelium's existing DH+AES-GCM payload encryption as the proof primitive — no extra crypto code on either side, no CLI helper to ship. Server-side stateless: challenge tokens are base64url(hmac || "|" || inner) where inner = address|nonce_hex|expires_at. "|" is the field delimiter because IPv6 addresses contain ":". Tested: cargo test --workspace 6 unit tests passing (token roundtrip, tampered-token rejected, verify-success, wrong-nonce rejected, expired rejected, ipv6 prefix- containment for dev-mode local-trust) Tracks: lhumina_code/hero_onboarding#2 |
||
|---|---|---|
| crates/hero_login_lib | ||
| .gitignore | ||
| Cargo.lock | ||
| Cargo.toml | ||
| LICENSE | ||
| README.md | ||
hero_login
Mycelium-address proof-of-control authentication for Hero services.
Why
Every Hero service that authenticates users does so against the same identity surface: the user's mycelium address. hero_login_lib is the canonical primitive that proves a caller controls the priv_key behind a claimed address — without inventing a new identity system, without depending on an external auth provider, and without requiring the user to register a password.
The proof flows over the mycelium overlay itself: the server sends an encrypted nonce to the user's address; only the priv_key holder can decrypt the message. Returning the nonce closes the loop.
Mechanism
Locked in hero_work/decisions/D-12-mycelium-proof-of-control-mechanism.md. Tracked at lhumina_code/hero_onboarding#2. Briefly:
- Client POSTs
/login/challengewithaddress=<user's mycelium address>. - Server generates a 32-byte nonce, packages it as an HMAC-signed challenge token, and POSTs to the local mycelium daemon's
/api/v1/messageswithdst={ip: address}+ topichero/login/v1+ payload containing the nonce. - Mycelium routes the encrypted message; user's daemon decrypts.
- User reads the nonce via
mycelium message receive(orcurl localhost:8989/api/v1/messages?topic=...). - Client POSTs
/login/verifywithchallenge_token+nonce. - Server HMAC-recomputes the token, checks expiry, constant-time-compares the nonce → 303 on success, 401 on failure.
Server-side stateless — no database row per pending challenge. HMAC-signed tokens carry their own state; reaped naturally on expiry.
API
use hero_login_lib::{LoginConfig, issue_challenge, verify_response};
let cfg = LoginConfig::new(hmac_secret_from_hero_proc_secrets);
let challenge = issue_challenge(&cfg, claimed_address).await?;
// → returns ChallengeToken; ship to browser
let identity = verify_response(&cfg, &returned_token, &returned_nonce)?;
// → returns MyceliumIdentity { address }
See crates/hero_login_lib/src/lib.rs for the full surface.
Crypto correction (important)
The original hero_work/prompt2.md §3 plan assumed mycelium uses ed25519 and exposes a /admin/sign endpoint. Phase B disproved both: mycelium uses x25519 Diffie-Hellman exclusively (no signing primitive), and there is no sign endpoint. hero_login_lib therefore leverages mycelium's message-routing as the proof primitive — the daemon's existing DH + AES-256-GCM scheme protects the nonce in transit. Full reasoning in hero_work/memory/investigation_mycelium_auth_api.md.
Build
cargo build --release
cargo test --workspace
Targets latest stable Rust (edition 2024).
Status
Phase 1 of the hero_onboarding meta-issue (lhumina_code/hero_onboarding#1) shipped a stubbed login at s2-002. This crate is Phase 1.5 (lhumina_code/hero_onboarding#2) — the real proof-of-control.