muse auth register --agent-id registers a different key than muse auth keygen generated
Severity
High / security-relevant. The fingerprint a user is shown and told to trust
at keygen time is not the key that ends up registered with the hub for
that agent. Anyone auditing an agent's identity by its keygen-time
fingerprint would be checking the wrong key entirely.
Background
Discovered while reviewing the "When an agent needs its own key" section of
the Build with Muse episode 04 script — the printed HD path is identical
across keygen and register output, but the reported key fingerprint
differs between the two commands for what's supposed to be the same key.
Repro (reproduced twice, clean scratch repos, zero passphrase interaction)
muse init
muse auth keygen --agent-id ci-bot-demo --hub https://staging.musehub.ai < /dev/null
# Fingerprint (SHA-256): sha256:71d461bf302a907a40e7977b9a08fdc1b0c5ab86a45c0a8f7ce1cf072b3ccab2
# HD path: m/1075233755'/1660078172'/1'/2106982183'/0'/1449979433'/0' (slot 2106982183)
muse auth register --hub https://staging.musehub.ai --agent-id ci-bot-demo \
--handle ci-bot-demo-test --provisioned-by gabriel < /dev/null
# Identity ID: sha256:04585f7c6f178ce9159f976de3d5a2cc5d60cb145d7e8e16b3d1f214154bf783
# Auth method: ed25519 (key fingerprint: sha256:04585f7c6…)
# HD path: m/1075233755'/1660078172'/1'/2106982183'/0'/1449979433'/0'
Identical printed HD path. Completely different fingerprint. Reproduced a second time with different random values, same divergence pattern.
Root cause
muse/cli/commands/auth.py:
keygen's agent-key path (~line 1078) derives the real key via a
custom KDF, not a literal HD path walk:
operator_seed = mnemonic_to_seed(operator_mnemonic, passphrase)
slot = agent_id_to_slot(agent_id)
agent_sub_seed = derive_agent_sub_seed(operator_seed, DOMAIN_IDENTITY, slot)
pub_b64, fingerprint = derive_hd_public_info(agent_sub_seed, hub=hub_idx)
hd_path_str = muse_path(DOMAIN_IDENTITY, ENTITY_AGENT, slot, hub=hub_idx)
hd_path_str here is a cosmetic label — built from muse_path(...)
purely for display/storage — describing the derivation in human-readable
form. It was never meant to be independently re-walked as a literal SLIP-10
path; the actual key material comes from derive_agent_sub_seed() +
derive_hd_public_info().
register's path (~line 1910), when re-deriving the key from a stored
identity entry to sign the challenge nonce, does the opposite: it takes
that stored hd_path string and treats it as a literal path to walk:
seed = mnemonic_to_seed(mnemonic)
dk = derive_path(seed, hd_path_for_register)
private_key = to_ed25519_private_key(dk)
derive_path() (plain SLIP-10 path derivation) and
derive_agent_sub_seed() -> derive_hd_public_info() (custom KDF) are
different algorithms. Feeding the same nominal path string into both
produces two different keys from the same mnemonic. register ends up
generating and registering a key that was never shown to the user, was
never validated against Fingerprint (SHA-256) at keygen time, and has no
relationship to the identity the user believed they were provisioning.
Impact
- Any provenance/trust claim based on the
keygen-time fingerprint is false for agent keys — the registered key is different. muse auth show/ any code path that re-derives from the storedhd_pathstring viaderive_path()for an agent entry is affected the same way. (Human entries may or may not hit this same divergence — needs checking; the human keygen path was not exercised in this repro.)- Downstream: any commit signed by the mis-derived key still verifies fine cryptographically (self-consistent), but the identity chain from "the key gabriel was shown" to "the key musehub has on file" is broken.
Scope
- Confirm whether the human (non-agent)
keygen/register/auth showround-trip has the same bug, or whether it's agent-specific (the human path may usederive_path()consistently everywhere — verify). - Pick one canonical derivation for agent keys and make every code path
that re-derives from a stored identity entry use it consistently:
either (a) make
register/auth show/anywhere else that re-derives agent keys call the samederive_agent_sub_seed+ hub-scoped derivation keygen uses, or (b) makekeygenactually derive viaderive_path(seed, hd_path_str)so the printed path is literally accurate and re-walkable. - Add a regression test that keygens an agent key, registers it, and asserts the fingerprint printed/returned by both commands is identical — this exact bug would have been caught immediately by such a test.
- Audit for any other command that re-derives a key from a stored
hd_pathstring viaderive_path()for an agent identity — every one of those call sites has the same latent bug.
Acceptance criteria
muse auth keygen --agent-id Xfollowed bymuse auth register --agent-id Xreports the same fingerprint at both steps, verified by a new automated test.- Existing agent identities already registered under the mismatched derivation are audited — decide whether they need re-provisioning.