"""muse#100 — `muse auth show --public-key`: recover an already-registered public key without touching internal Python modules. `muse sign verify --public-key-b64` needs the raw base64url Ed25519 public key (no `ed25519:` prefix). The only place that value was ever printed was at keygen/register time -- `muse auth whoami`, `muse auth show`, and `muse auth key list` all expose only the one-way fingerprint, which cannot be reversed back to the public key bytes. Coverage: - unit: _resolve_public_key computes the same (pub_b64, fingerprint) pair `muse sign` itself uses, via the identical get_signing_identity() / _public_key_info() resolution path -- not a reimplementation that could silently drift from what `muse sign verify` actually expects. - integration: `muse auth show --public-key` CLI round-trip, text and --json. - error path: no identity registered for the hub -> clean error, not a crash. """ from __future__ import annotations import json import pathlib import pytest from tests.cli_test_helper import CliRunner import muse.core.keypair as kp_mod runner = CliRunner() HUB = "https://localhost:1337" HOSTNAME = "localhost:1337" FAKE_MNEMONIC = ( "abandon abandon abandon abandon abandon abandon " "abandon abandon abandon abandon abandon about" ) FAKE_HD_PATH = "m/1075233755'/0'/0'/0'/0'/0'" FAKE_FINGERPRINT = "a" * 64 FAKE_HANDLE = "gabriel" def _patch_home(monkeypatch: pytest.MonkeyPatch, tmp_path: pathlib.Path) -> pathlib.Path: fake_home = tmp_path / "home" fake_muse = fake_home / ".muse" fake_keys = fake_muse / "keys" fake_keys.mkdir(parents=True) identity_file = fake_muse / "identity.toml" import muse.core.identity as id_mod monkeypatch.setattr(id_mod, "_IDENTITY_FILE", identity_file) monkeypatch.setattr(id_mod, "_IDENTITY_DIR", fake_muse) monkeypatch.setattr(kp_mod, "_KEYS_DIR", fake_keys) return fake_home def _write_hd_identity( monkeypatch: pytest.MonkeyPatch, handle: str = FAKE_HANDLE ) -> None: """Write an HD identity entry with mnemonic stored in an in-memory keychain -- the real resolution path (resolve_signing_identity / get_signing_identity) derives the private key from this mnemonic at call time, exactly as it does for `muse sign`.""" _kc: dict[str, str] = {} monkeypatch.setattr("muse.core.keychain.is_available", lambda: True) monkeypatch.setattr("muse.core.keychain.store", lambda m: _kc.__setitem__("mnemonic", m)) monkeypatch.setattr("muse.core.keychain.load", lambda: _kc.get("mnemonic")) entry = { "type": "human", "handle": handle, "key_path": str(kp_mod._KEYS_DIR / f"{HOSTNAME}.pem"), "algorithm": "ed25519", "fingerprint": FAKE_FINGERPRINT, "hd_path": FAKE_HD_PATH, } from muse.core.identity import save_identity save_identity(f"https://{HOSTNAME}", entry, mnemonic=FAKE_MNEMONIC) # --------------------------------------------------------------------------- # Unit: the resolution path matches muse sign's own public-key derivation # --------------------------------------------------------------------------- def test_auth_show_public_key_matches_sign_commands_own_derivation( monkeypatch: pytest.MonkeyPatch, tmp_path: pathlib.Path ) -> None: """The public key `muse auth show --public-key` prints must be byte-identical to what `muse sign`'s own _public_key_info derives for the same identity -- not a parallel reimplementation that could silently diverge.""" _patch_home(monkeypatch, tmp_path) _write_hd_identity(monkeypatch) from muse.cli.config import get_signing_identity from muse.cli.commands.sign import _public_key_info signing = get_signing_identity(remote_url=HUB) assert signing is not None expected_pub_b64, expected_fingerprint = _public_key_info(signing) from muse.cli.commands.auth import _resolve_public_key pub_b64, fingerprint, handle = _resolve_public_key(HUB, agent_id=None) assert pub_b64 == expected_pub_b64 assert fingerprint == expected_fingerprint assert handle == FAKE_HANDLE assert not pub_b64.startswith("ed25519:"), ( "must be the raw base64url muse sign verify --public-key-b64 expects, " "not the ed25519:-prefixed canonical form" ) def test_auth_show_public_key_no_identity_returns_none( monkeypatch: pytest.MonkeyPatch, tmp_path: pathlib.Path ) -> None: _patch_home(monkeypatch, tmp_path) from muse.cli.commands.auth import _resolve_public_key assert _resolve_public_key(HUB, agent_id=None) is None # --------------------------------------------------------------------------- # Integration: CLI round-trip # --------------------------------------------------------------------------- def test_cli_auth_show_public_key_text( monkeypatch: pytest.MonkeyPatch, tmp_path: pathlib.Path, capsys: pytest.CaptureFixture[str] ) -> None: _patch_home(monkeypatch, tmp_path) _write_hd_identity(monkeypatch) result = runner.invoke(None, ["auth", "show", "--hub", HUB, "--public-key"]) assert result.exit_code == 0, result.output printed = result.output.strip() assert printed assert not printed.startswith("ed25519:") # Must be usable verbatim as `muse sign verify --public-key-b64 `. assert " " not in printed assert "\n" not in printed def test_cli_auth_show_public_key_json( monkeypatch: pytest.MonkeyPatch, tmp_path: pathlib.Path ) -> None: _patch_home(monkeypatch, tmp_path) _write_hd_identity(monkeypatch) result = runner.invoke(None, ["auth", "show", "--hub", HUB, "--public-key", "--json"]) assert result.exit_code == 0, result.output out = json.loads(result.output) assert out["public_key_b64"] assert not out["public_key_b64"].startswith("ed25519:") # Computed live from the derived public key, not echoed from the (possibly # stale) stored identity.toml fingerprint field -- intentionally does NOT # equal FAKE_FINGERPRINT, which is a placeholder set at identity-write time. assert out["fingerprint"].startswith("sha256:") assert out["handle"] == FAKE_HANDLE assert out["hub"] == HOSTNAME def test_cli_auth_show_public_key_no_identity_clean_error( monkeypatch: pytest.MonkeyPatch, tmp_path: pathlib.Path ) -> None: """No crash, no traceback -- a clean, structured error.""" _patch_home(monkeypatch, tmp_path) result = runner.invoke(None, ["auth", "show", "--hub", HUB, "--public-key", "--json"]) assert result.exit_code != 0 assert "Traceback" not in result.output assert "Traceback" not in result.stderr out = json.loads(result.output) assert "error" in out def test_cli_auth_show_public_key_roundtrips_through_sign_verify( monkeypatch: pytest.MonkeyPatch, tmp_path: pathlib.Path ) -> None: """End-to-end acceptance criterion: the printed value must be exactly what `muse sign verify --public-key-b64` accepts for a header signed by the same identity.""" _patch_home(monkeypatch, tmp_path) _write_hd_identity(monkeypatch) show_result = runner.invoke(None, ["auth", "show", "--hub", HUB, "--public-key"]) assert show_result.exit_code == 0, show_result.output pub_b64 = show_result.output.strip() from muse.core.msign import build_msign_header, verify_msign_header from muse.cli.config import get_signing_identity signing = get_signing_identity(remote_url=HUB) assert signing is not None url = f"{HUB}/x/y/push" header = build_msign_header(signing, "POST", url, b"") ok, reason = verify_msign_header(header, "POST", url, b"", pub_b64) assert ok, reason