gabriel / muse public
Closed #99 Bug
filed by gabriel human · 35 days ago

muse auth register --agent-id registers a different key than keygen generated (HD path label vs actual KDF mismatch)

0 Anchors
— Blast radius
— Churn 30d
0 Proposals

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 stored hd_path string via derive_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

  1. Confirm whether the human (non-agent) keygen/register/auth show round-trip has the same bug, or whether it's agent-specific (the human path may use derive_path() consistently everywhere — verify).
  2. 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 same derive_agent_sub_seed + hub-scoped derivation keygen uses, or (b) make keygen actually derive via derive_path(seed, hd_path_str) so the printed path is literally accurate and re-walkable.
  3. 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.
  4. Audit for any other command that re-derives a key from a stored hd_path string via derive_path() for an agent identity — every one of those call sites has the same latent bug.

Acceptance criteria

  • muse auth keygen --agent-id X followed by muse auth register --agent-id X reports 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.
◎ Activity1
●
gabriel opened this issue 35 days ago
○
gabriel 35 days ago

Fixed in commit b88052fb (muse/core/keypair.py::derive_agent_identity_key, plus fixes in muse/cli/commands/auth.py and muse/core/identity.py) on dev/main. Shipped to staging as muse 0.2.1rc7.

Root cause: keygen's agent branch derives the real key via a two-stage KDF (derive_agent_sub_seed -> derive_identity_key), but register and resolve_signing_identity (used by muse commit --sign --agent-id) both treated the stored hd_path label as a literal, directly-walkable SLIP-10 path -- a different, incompatible derivation. All three now share one implementation (derive_agent_identity_key).

Verified end-to-end on the real installed 0.2.1rc7 CLI, fresh scratch repo, staging hub: keygen and register now report the identical fingerprint (sha256:6c76bd7ea0ad54b05c0af28c30cad7a3aeacffdd758442393f34e5885909ef90). Also verified the commit-signing leg separately (signer_public_key matches keygen's pubkey exactly).

Added TestAgentIdKeygenRegisterKeyConsistency (2 tests) to test_cmd_auth_keygen_register.py, both confirmed red before / green after. Full auth/identity/hdkeys/keypair/commit-signing surface: 584/584 passing, zero regressions.

Two things noted as explicitly out of scope in the fix commit, not addressed here: (1) muse auth register doesn't accept a BIP-39 passphrase at all, so an operator using a non-blank passphrase would hit a similarly-shaped mismatch for their own human key -- not reproduced/fixed. (2) hub_scoping_migration.py / domain_migration.py / migrate_cmd.py's derive_path() call sites are one-time migration tools, not audited for the same agent-entry issue in this pass.

Closing.

✓
closed this issue 35 days ago