gabriel / muse public
test_auth_show_public_key.py python
195 lines 7.5 KB
Raw
sha256:0d3de0ee53b63667de053f3ad712184124142df86473b80939347b487a65d6af fix(diff): bare path scopes the diff instead of failing as … Sonnet 5 patch 5 minutes ago
1 """muse#100 — `muse auth show --public-key`: recover an already-registered
2 public key without touching internal Python modules.
3
4 `muse sign verify --public-key-b64` needs the raw base64url Ed25519 public
5 key (no `ed25519:` prefix). The only place that value was ever printed was
6 at keygen/register time -- `muse auth whoami`, `muse auth show`, and
7 `muse auth key list` all expose only the one-way fingerprint, which cannot
8 be reversed back to the public key bytes.
9
10 Coverage:
11 - unit: _resolve_public_key computes the same (pub_b64, fingerprint) pair
12 `muse sign` itself uses, via the identical get_signing_identity() /
13 _public_key_info() resolution path -- not a reimplementation that could
14 silently drift from what `muse sign verify` actually expects.
15 - integration: `muse auth show --public-key` CLI round-trip, text and --json.
16 - error path: no identity registered for the hub -> clean error, not a crash.
17 """
18 from __future__ import annotations
19
20 import json
21 import pathlib
22
23 import pytest
24 from tests.cli_test_helper import CliRunner
25
26 import muse.core.keypair as kp_mod
27
28 runner = CliRunner()
29
30 HUB = "https://localhost:1337"
31 HOSTNAME = "localhost:1337"
32 FAKE_MNEMONIC = (
33 "abandon abandon abandon abandon abandon abandon "
34 "abandon abandon abandon abandon abandon about"
35 )
36 FAKE_HD_PATH = "m/1075233755'/0'/0'/0'/0'/0'"
37 FAKE_FINGERPRINT = "a" * 64
38 FAKE_HANDLE = "gabriel"
39
40
41 def _patch_home(monkeypatch: pytest.MonkeyPatch, tmp_path: pathlib.Path) -> pathlib.Path:
42 fake_home = tmp_path / "home"
43 fake_muse = fake_home / ".muse"
44 fake_keys = fake_muse / "keys"
45 fake_keys.mkdir(parents=True)
46 identity_file = fake_muse / "identity.toml"
47
48 import muse.core.identity as id_mod
49 monkeypatch.setattr(id_mod, "_IDENTITY_FILE", identity_file)
50 monkeypatch.setattr(id_mod, "_IDENTITY_DIR", fake_muse)
51 monkeypatch.setattr(kp_mod, "_KEYS_DIR", fake_keys)
52 return fake_home
53
54
55 def _write_hd_identity(
56 monkeypatch: pytest.MonkeyPatch, handle: str = FAKE_HANDLE
57 ) -> None:
58 """Write an HD identity entry with mnemonic stored in an in-memory keychain
59 -- the real resolution path (resolve_signing_identity / get_signing_identity)
60 derives the private key from this mnemonic at call time, exactly as it
61 does for `muse sign`."""
62 _kc: dict[str, str] = {}
63 monkeypatch.setattr("muse.core.keychain.is_available", lambda: True)
64 monkeypatch.setattr("muse.core.keychain.store", lambda m: _kc.__setitem__("mnemonic", m))
65 monkeypatch.setattr("muse.core.keychain.load", lambda: _kc.get("mnemonic"))
66
67 entry = {
68 "type": "human",
69 "handle": handle,
70 "key_path": str(kp_mod._KEYS_DIR / f"{HOSTNAME}.pem"),
71 "algorithm": "ed25519",
72 "fingerprint": FAKE_FINGERPRINT,
73 "hd_path": FAKE_HD_PATH,
74 }
75 from muse.core.identity import save_identity
76 save_identity(f"https://{HOSTNAME}", entry, mnemonic=FAKE_MNEMONIC)
77
78
79 # ---------------------------------------------------------------------------
80 # Unit: the resolution path matches muse sign's own public-key derivation
81 # ---------------------------------------------------------------------------
82
83
84 def test_auth_show_public_key_matches_sign_commands_own_derivation(
85 monkeypatch: pytest.MonkeyPatch, tmp_path: pathlib.Path
86 ) -> None:
87 """The public key `muse auth show --public-key` prints must be byte-identical
88 to what `muse sign`'s own _public_key_info derives for the same identity --
89 not a parallel reimplementation that could silently diverge."""
90 _patch_home(monkeypatch, tmp_path)
91 _write_hd_identity(monkeypatch)
92
93 from muse.cli.config import get_signing_identity
94 from muse.cli.commands.sign import _public_key_info
95
96 signing = get_signing_identity(remote_url=HUB)
97 assert signing is not None
98 expected_pub_b64, expected_fingerprint = _public_key_info(signing)
99
100 from muse.cli.commands.auth import _resolve_public_key
101 pub_b64, fingerprint, handle = _resolve_public_key(HUB, agent_id=None)
102
103 assert pub_b64 == expected_pub_b64
104 assert fingerprint == expected_fingerprint
105 assert handle == FAKE_HANDLE
106 assert not pub_b64.startswith("ed25519:"), (
107 "must be the raw base64url muse sign verify --public-key-b64 expects, "
108 "not the ed25519:-prefixed canonical form"
109 )
110
111
112 def test_auth_show_public_key_no_identity_returns_none(
113 monkeypatch: pytest.MonkeyPatch, tmp_path: pathlib.Path
114 ) -> None:
115 _patch_home(monkeypatch, tmp_path)
116 from muse.cli.commands.auth import _resolve_public_key
117 assert _resolve_public_key(HUB, agent_id=None) is None
118
119
120 # ---------------------------------------------------------------------------
121 # Integration: CLI round-trip
122 # ---------------------------------------------------------------------------
123
124
125 def test_cli_auth_show_public_key_text(
126 monkeypatch: pytest.MonkeyPatch, tmp_path: pathlib.Path, capsys: pytest.CaptureFixture[str]
127 ) -> None:
128 _patch_home(monkeypatch, tmp_path)
129 _write_hd_identity(monkeypatch)
130
131 result = runner.invoke(None, ["auth", "show", "--hub", HUB, "--public-key"])
132 assert result.exit_code == 0, result.output
133 printed = result.output.strip()
134 assert printed
135 assert not printed.startswith("ed25519:")
136 # Must be usable verbatim as `muse sign verify --public-key-b64 <this>`.
137 assert " " not in printed
138 assert "\n" not in printed
139
140
141 def test_cli_auth_show_public_key_json(
142 monkeypatch: pytest.MonkeyPatch, tmp_path: pathlib.Path
143 ) -> None:
144 _patch_home(monkeypatch, tmp_path)
145 _write_hd_identity(monkeypatch)
146
147 result = runner.invoke(None, ["auth", "show", "--hub", HUB, "--public-key", "--json"])
148 assert result.exit_code == 0, result.output
149 out = json.loads(result.output)
150 assert out["public_key_b64"]
151 assert not out["public_key_b64"].startswith("ed25519:")
152 # Computed live from the derived public key, not echoed from the (possibly
153 # stale) stored identity.toml fingerprint field -- intentionally does NOT
154 # equal FAKE_FINGERPRINT, which is a placeholder set at identity-write time.
155 assert out["fingerprint"].startswith("sha256:")
156 assert out["handle"] == FAKE_HANDLE
157 assert out["hub"] == HOSTNAME
158
159
160 def test_cli_auth_show_public_key_no_identity_clean_error(
161 monkeypatch: pytest.MonkeyPatch, tmp_path: pathlib.Path
162 ) -> None:
163 """No crash, no traceback -- a clean, structured error."""
164 _patch_home(monkeypatch, tmp_path)
165
166 result = runner.invoke(None, ["auth", "show", "--hub", HUB, "--public-key", "--json"])
167 assert result.exit_code != 0
168 assert "Traceback" not in result.output
169 assert "Traceback" not in result.stderr
170 out = json.loads(result.output)
171 assert "error" in out
172
173
174 def test_cli_auth_show_public_key_roundtrips_through_sign_verify(
175 monkeypatch: pytest.MonkeyPatch, tmp_path: pathlib.Path
176 ) -> None:
177 """End-to-end acceptance criterion: the printed value must be exactly what
178 `muse sign verify --public-key-b64` accepts for a header signed by the
179 same identity."""
180 _patch_home(monkeypatch, tmp_path)
181 _write_hd_identity(monkeypatch)
182
183 show_result = runner.invoke(None, ["auth", "show", "--hub", HUB, "--public-key"])
184 assert show_result.exit_code == 0, show_result.output
185 pub_b64 = show_result.output.strip()
186
187 from muse.core.msign import build_msign_header, verify_msign_header
188 from muse.cli.config import get_signing_identity
189
190 signing = get_signing_identity(remote_url=HUB)
191 assert signing is not None
192 url = f"{HUB}/x/y/push"
193 header = build_msign_header(signing, "POST", url, b"")
194 ok, reason = verify_msign_header(header, "POST", url, b"", pub_b64)
195 assert ok, reason
File History 1 commit
sha256:0d3de0ee53b63667de053f3ad712184124142df86473b80939347b487a65d6af fix(diff): bare path scopes the diff instead of failing as … Sonnet 5 patch 5 minutes ago