Mycelium-address proof-of-control authentication library — challenge-response over the mycelium overlay, no shared secret required. Canonical hero_login pattern for every Hero service that authenticates users.
Find a file
mik-tf 6ee4caf0e8 Initial scaffold: hero_login_lib (mycelium message-round-trip proof-of-control)
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
2026-05-20 14:58:07 -04:00
crates/hero_login_lib Initial scaffold: hero_login_lib (mycelium message-round-trip proof-of-control) 2026-05-20 14:58:07 -04:00
.gitignore Initial scaffold: hero_login_lib (mycelium message-round-trip proof-of-control) 2026-05-20 14:58:07 -04:00
Cargo.lock Initial scaffold: hero_login_lib (mycelium message-round-trip proof-of-control) 2026-05-20 14:58:07 -04:00
Cargo.toml Initial scaffold: hero_login_lib (mycelium message-round-trip proof-of-control) 2026-05-20 14:58:07 -04:00
LICENSE Initial commit 2026-05-20 18:26:06 +00:00
README.md Initial scaffold: hero_login_lib (mycelium message-round-trip proof-of-control) 2026-05-20 14:58:07 -04:00

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:

  1. Client POSTs /login/challenge with address=<user's mycelium address>.
  2. Server generates a 32-byte nonce, packages it as an HMAC-signed challenge token, and POSTs to the local mycelium daemon's /api/v1/messages with dst={ip: address} + topic hero/login/v1 + payload containing the nonce.
  3. Mycelium routes the encrypted message; user's daemon decrypts.
  4. User reads the nonce via mycelium message receive (or curl localhost:8989/api/v1/messages?topic=...).
  5. Client POSTs /login/verify with challenge_token + nonce.
  6. 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.