gabriel / musehub public
Open #221 Bug
filed by gabriel human · 27 days ago

HD derivation path has no per-hub scoping — same key presented to every hub by default

0 Anchors
— Blast radius
— Churn 30d
0 Proposals

Found while fact-checking Episode 04 ("Identity Without Passwords") of the Build With Muse series against a real ~/.muse/identity.toml — the script claims "localhost gets one key. Staging gets a different one. Production, a third," and that compromising one hub's key leaves the others "mathematically unrelated." That claim is false as currently implemented.

Confirmed via the actual derivation code, muse/core/hdkeys.py:399-454:

def muse_path(domain, entity_type=ENTITY_HUMAN, entity_id=0, role=ROLE_SIGN, index=0) -> str:
    return (
        f"m/{MUSE_PURPOSE}'"
        f"/{domain}'"
        f"/{entity_type}'"
        f"/{entity_id}'"
        f"/{role}'"
        f"/{index}'"
    )

The six HD path levels are purpose / domain / entity_type / entity_id / role / index. None of them represents "which hub." domain' is a fixed capability namespace (Identity, Payments, Code, ...) — not per-server.

I checked every call site in muse/cli/commands/auth.py (run_keygen, run_rotate, run_register, run_whoami). The hub URL is consumed only via hostname_from_url(hub_url), used purely as the dict key for reading/writing sections of ~/.muse/identity.toml — it is never hashed into muse_path, derive_key, derive_identity_key, or derive_path.

Reproduced live, real ~/.muse/identity.toml:

["localhost:1337"]
fingerprint = "sha256:d8faf8dfe4550a01610d2f39850c88700f3f6fb5c3a755f34c5bbaf6d401833f"
hd_path     = "m/1075233755'/1660078172'/0'/0'/0'/0'"

["musehub.ai"]
fingerprint = "sha256:d8faf8dfe4550a01610d2f39850c88700f3f6fb5c3a755f34c5bbaf6d401833f"
hd_path     = "m/1075233755'/1660078172'/0'/0'/0'/0'"

["staging.musehub.ai"]
fingerprint = "sha256:d8c00e0af82f15cc123d6c4f6f745a28d45be38cc6c465a172673c5d459869d6"
hd_path     = "m/1075233755'/1660078172'/0'/0'/0'/1'"

localhost:1337 and musehub.ai (production) are bit-for-bit the same key — identical fingerprint, identical hd_path, both at index 0'. staging only differs because muse auth rotate --hub staging.musehub.ai was run manually at some point in the past, bumping only that hostname's locally stored index by one (run_rotate reads current_index from the existing identity.toml entry for that specific hostname — confirmed in muse/cli/commands/auth.py).

Impact: at the default index (0'), the exact same private key is presented to every hub a user registers with. "Different hub → different key" is not a cryptographic guarantee anywhere in this codebase — it is purely an artifact of a human remembering to run muse auth rotate against one specific --hub value. Nothing enforces it, nothing warns when it hasn't happened, and nothing prevents production silently staying keyed identically to localhost forever. Compromising a localhost/dev key today would, in practice, also compromise production.

Suggested fix: add a real hub-scoping level to the HD path, following the exact precedent already set by domain' — a hardened value computed as int.from_bytes(sha256(canonical_hostname.encode())[:4], "big") & 0x7FFFFFFF, so the math structurally cannot connect keys across hubs, the same way it already can't connect keys across domains. This needs to sit as its own level (likely between role' and index') rather than repurposing index' itself, since index' already has a distinct, existing meaning (manual rotation count) that a hub-hash would collide with and make ambiguous.

Scope note — this is real design + migration work, not a quick patch: adding a path level changes the six-level schema, which needs the same kind of migration path already built for the Phase 1 domain-integer change (muse migrate domain-integers) — existing identities registered at index 0' against multiple hubs today need a defined backfill/re-registration story, not a silent behavior change that invalidates existing registrations.

Deferred per usual until after the season wraps, but flagging now since Episode 04's script currently states the false claim as fact and needs to be held back until this is resolved one way or another.

◎ Activity
●
gabriel opened this issue 27 days ago
◎
No activity yet. Use the CLI to comment.