gabriel / muse public

test_auth_register_integrity.py file-level

at sha256:8 · View file ↗ · Intel ↗

History
1 files
1 commits
0 hotspots
0 🧊 dead
0 💥 blast risk
sha256:b adding issues docs to bust staging mpack prebuild cache. · gabriel · Jun 20, 2026
1 """Tests for identity data-integrity: keygen → register → resolve must be consistent.
2
3 Critical invariant
4 ------------------
5 After ``muse auth keygen`` followed by ``muse auth register``, the identity entry
6 in ``identity.toml`` must:
7
8 1. Contain ``hd_path`` (written by keygen, must survive the register write).
9 2. NOT contain ``key_path`` (no PEM path should appear post-Phase-4).
10 3. Have a ``fingerprint`` that matches the mnemonic-derived key, NOT a stale PEM.
11 4. Allow ``resolve_signing_identity`` to return a key (full round-trip).
12
13 These tests are RED until ``run_register`` is updated to:
14 - Derive the public key from the mnemonic (via ``resolve_signing_identity``) instead
15 of reading a PEM file.
16 - Write the entry without ``key_path``.
17 - Preserve ``hd_path`` from the keygen entry.
18 """
19
20 from __future__ import annotations
21
22 import json
23 import pathlib
24 from unittest.mock import MagicMock, patch
25
26 import pytest
27 from tests.cli_test_helper import CliRunner
28
29 import muse.core.keypair as kp_module
30 import muse.core.identity as id_module
31 from muse.core.types import long_id
32
33 runner = CliRunner()
34
35 type _KeychainStore = dict[str, str]
36 type _RegisterResp = dict[str, str | bool]
37
38 _FIXED_MNEMONIC = (
39 "abandon abandon abandon abandon abandon abandon abandon abandon "
40 "abandon abandon abandon about"
41 )
42 _HUB = "https://localhost:1337"
43 _HOSTNAME = "localhost:1337"
44
45
46 # ---------------------------------------------------------------------------
47 # Fixtures
48 # ---------------------------------------------------------------------------
49
50
51 def _patch_home(monkeypatch: pytest.MonkeyPatch, tmp_path: pathlib.Path) -> pathlib.Path:
52 fake_home = tmp_path / "home"
53 fake_home.mkdir(parents=True, exist_ok=True)
54 monkeypatch.setattr(pathlib.Path, "home", staticmethod(lambda: fake_home))
55 monkeypatch.setattr(kp_module, "_KEYS_DIR", fake_home / ".muse" / "keys")
56 monkeypatch.setattr(id_module, "_IDENTITY_DIR", fake_home / ".muse")
57 monkeypatch.setattr(id_module, "_IDENTITY_FILE", fake_home / ".muse" / "identity.toml")
58 monkeypatch.setattr("muse.cli.commands.auth._stderr_isatty", lambda: False)
59 return fake_home
60
61
62 def _patch_keychain(monkeypatch: pytest.MonkeyPatch) -> _KeychainStore:
63 """Patch keychain with _FIXED_MNEMONIC pre-loaded."""
64 _kc: dict[str, str] = {"mnemonic": _FIXED_MNEMONIC}
65 monkeypatch.setattr("muse.core.keychain.is_available", lambda: True)
66 monkeypatch.setattr("muse.core.keychain.store", lambda m: _kc.__setitem__("mnemonic", m))
67 monkeypatch.setattr("muse.core.keychain.load", lambda: _kc.get("mnemonic"))
68 return _kc
69
70
71 def _fake_register_response(handle: str = "gabriel") -> _RegisterResp:
72 """Minimal hub verify response."""
73 return {
74 "handle": handle,
75 "identity_id": long_id("a" * 64),
76 "is_new_identity": True,
77 }
78
79
80 def _run_keygen(monkeypatch: pytest.MonkeyPatch) -> None:
81 """Run auth keygen with fixed mnemonic; no --force (reuses keychain)."""
82 import muse.core.bip39 as bip39_mod
83 monkeypatch.setattr(bip39_mod, "generate_mnemonic", lambda **kw: _FIXED_MNEMONIC)
84 result = runner.invoke(None, ["auth", "keygen", "--hub", _HUB])
85 assert result.exit_code == 0, f"keygen failed: {result.output}"
86
87
88 def _run_register(monkeypatch: pytest.MonkeyPatch) -> None:
89 """Run auth register with a mocked hub HTTP response."""
90 from muse.core.bip39 import mnemonic_to_seed
91 from muse.core.keypair import derive_hd_public_info
92 seed = mnemonic_to_seed(_FIXED_MNEMONIC)
93 pub_b64, fingerprint = derive_hd_public_info(seed)
94
95 challenge_resp = {"challenge_token": "deadbeef" * 8, "is_new_key": True}
96 verify_resp = _fake_register_response()
97
98 monkeypatch.setattr("muse.cli.commands.auth._post_challenge", lambda *a, **kw: challenge_resp)
99 monkeypatch.setattr("muse.cli.commands.auth._post_verify", lambda *a, **kw: verify_resp)
100
101 result = runner.invoke(None, ["auth", "register", "--hub", _HUB, "--handle", "gabriel"])
102 assert result.exit_code == 0, f"register failed: {result.output}"
103
104
105 # ---------------------------------------------------------------------------
106 # Tests — each is independent, running keygen then register
107 # ---------------------------------------------------------------------------
108
109
110 class TestRegisterPreservesHdPath:
111 """R1: hd_path written by keygen must survive the register write."""
112
113 def test_R1_hd_path_present_after_register(
114 self, monkeypatch: pytest.MonkeyPatch, tmp_path: pathlib.Path
115 ) -> None:
116 import tomllib
117 _patch_home(monkeypatch, tmp_path)
118 _patch_keychain(monkeypatch)
119 _run_keygen(monkeypatch)
120 _run_register(monkeypatch)
121
122 identity_file = id_module._IDENTITY_FILE
123 parsed = tomllib.loads(identity_file.read_text())
124 entry = parsed[_HOSTNAME]
125 assert "hd_path" in entry, (
126 f"hd_path was lost during register. Entry: {entry}"
127 )
128 assert entry["hd_path"].startswith("m/"), f"hd_path malformed: {entry['hd_path']}"
129
130
131 class TestRegisterNoKeyPath:
132 """R2: key_path must NOT appear in the entry after register."""
133
134 def test_R2_no_key_path_after_register(
135 self, monkeypatch: pytest.MonkeyPatch, tmp_path: pathlib.Path
136 ) -> None:
137 import tomllib
138 _patch_home(monkeypatch, tmp_path)
139 _patch_keychain(monkeypatch)
140 _run_keygen(monkeypatch)
141 _run_register(monkeypatch)
142
143 identity_file = id_module._IDENTITY_FILE
144 parsed = tomllib.loads(identity_file.read_text())
145 entry = parsed[_HOSTNAME]
146 assert "key_path" not in entry, (
147 f"key_path must not appear in identity entry after register. Entry: {entry}"
148 )
149
150
151 class TestRegisterFingerprintMatchesMnemonic:
152 """R3: fingerprint in the entry must match the mnemonic-derived key, not a stale PEM."""
153
154 def test_R3_fingerprint_matches_mnemonic_derivation(
155 self, monkeypatch: pytest.MonkeyPatch, tmp_path: pathlib.Path
156 ) -> None:
157 import tomllib
158 from muse.core.bip39 import mnemonic_to_seed
159 from muse.core.keypair import derive_hd_public_info
160
161 _patch_home(monkeypatch, tmp_path)
162 _patch_keychain(monkeypatch)
163 _run_keygen(monkeypatch)
164 _run_register(monkeypatch)
165
166 seed = mnemonic_to_seed(_FIXED_MNEMONIC)
167 _, expected_fingerprint = derive_hd_public_info(seed)
168
169 identity_file = id_module._IDENTITY_FILE
170 parsed = tomllib.loads(identity_file.read_text())
171 entry = parsed[_HOSTNAME]
172 stored_fp = entry.get("fingerprint", "")
173
174 assert stored_fp == expected_fingerprint, (
175 f"Fingerprint mismatch: stored={stored_fp} expected={expected_fingerprint}. "
176 "register wrote a stale PEM-derived fingerprint instead of the mnemonic-derived one."
177 )
178
179
180 class TestRegisterRoundTrip:
181 """R4: resolve_signing_identity must return a key after keygen + register."""
182
183 def test_R4_resolve_signing_identity_works_after_register(
184 self, monkeypatch: pytest.MonkeyPatch, tmp_path: pathlib.Path
185 ) -> None:
186 from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey
187 from muse.core.identity import resolve_signing_identity
188
189 _patch_home(monkeypatch, tmp_path)
190 _patch_keychain(monkeypatch)
191 _run_keygen(monkeypatch)
192 _run_register(monkeypatch)
193
194 result = resolve_signing_identity(_HUB)
195 assert result is not None, (
196 "resolve_signing_identity returned None after keygen + register. "
197 "The identity entry is missing hd_path or the mnemonic is not in the keychain."
198 )
199 handle, private_key = result
200 assert handle == "gabriel"
201 assert isinstance(private_key, Ed25519PrivateKey)