HD derivation path has no per-hub scoping — same key presented to every hub by default
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.