gabriel / muse public
test_auth_register_integrity.py python
197 lines 7.5 KB
Raw
sha256:b636f72dcba9e190afb980bece906fa5b717fbde014b76ef023df8cb96e01eb9 docs: expand cache plan with all seven testing tiers and do… Sonnet 4.6 131 days ago
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
32 runner = CliRunner()
33
34 _FIXED_MNEMONIC = (
35 "abandon abandon abandon abandon abandon abandon abandon abandon "
36 "abandon abandon abandon about"
37 )
38 _HUB = "https://localhost:1337"
39 _HOSTNAME = "localhost:1337"
40
41
42 # ---------------------------------------------------------------------------
43 # Fixtures
44 # ---------------------------------------------------------------------------
45
46
47 def _patch_home(monkeypatch: pytest.MonkeyPatch, tmp_path: pathlib.Path) -> pathlib.Path:
48 fake_home = tmp_path / "home"
49 fake_home.mkdir(parents=True, exist_ok=True)
50 monkeypatch.setattr(pathlib.Path, "home", staticmethod(lambda: fake_home))
51 monkeypatch.setattr(kp_module, "_KEYS_DIR", fake_home / ".muse" / "keys")
52 monkeypatch.setattr(id_module, "_IDENTITY_DIR", fake_home / ".muse")
53 monkeypatch.setattr(id_module, "_IDENTITY_FILE", fake_home / ".muse" / "identity.toml")
54 monkeypatch.setattr("muse.cli.commands.auth._stderr_isatty", lambda: False)
55 return fake_home
56
57
58 def _patch_keychain(monkeypatch: pytest.MonkeyPatch) -> dict:
59 """Patch keychain with _FIXED_MNEMONIC pre-loaded."""
60 _kc: dict[str, str] = {"mnemonic": _FIXED_MNEMONIC}
61 monkeypatch.setattr("muse.core.keychain.is_available", lambda: True)
62 monkeypatch.setattr("muse.core.keychain.store", lambda m: _kc.__setitem__("mnemonic", m))
63 monkeypatch.setattr("muse.core.keychain.load", lambda: _kc.get("mnemonic"))
64 return _kc
65
66
67 def _fake_register_response(handle: str = "gabriel") -> dict:
68 """Minimal hub verify response."""
69 return {
70 "handle": handle,
71 "identityId": f"sha256:{'a' * 64}",
72 "isNewIdentity": True,
73 }
74
75
76 def _run_keygen(monkeypatch: pytest.MonkeyPatch) -> None:
77 """Run auth keygen with fixed mnemonic; no --force (reuses keychain)."""
78 import muse.core.bip39 as bip39_mod
79 monkeypatch.setattr(bip39_mod, "generate_mnemonic", lambda **kw: _FIXED_MNEMONIC)
80 result = runner.invoke(None, ["auth", "keygen", "--hub", _HUB])
81 assert result.exit_code == 0, f"keygen failed: {result.output}"
82
83
84 def _run_register(monkeypatch: pytest.MonkeyPatch) -> None:
85 """Run auth register with a mocked hub HTTP response."""
86 from muse.core.bip39 import mnemonic_to_seed
87 from muse.core.keypair import derive_hd_public_info
88 seed = mnemonic_to_seed(_FIXED_MNEMONIC)
89 pub_b64, fingerprint = derive_hd_public_info(seed)
90
91 challenge_resp = {"challengeToken": "deadbeef" * 8, "isNewKey": True}
92 verify_resp = _fake_register_response()
93
94 monkeypatch.setattr("muse.cli.commands.auth._post_challenge", lambda *a, **kw: challenge_resp)
95 monkeypatch.setattr("muse.cli.commands.auth._post_verify", lambda *a, **kw: verify_resp)
96
97 result = runner.invoke(None, ["auth", "register", "--hub", _HUB, "--handle", "gabriel"])
98 assert result.exit_code == 0, f"register failed: {result.output}"
99
100
101 # ---------------------------------------------------------------------------
102 # Tests — each is independent, running keygen then register
103 # ---------------------------------------------------------------------------
104
105
106 class TestRegisterPreservesHdPath:
107 """R1: hd_path written by keygen must survive the register write."""
108
109 def test_R1_hd_path_present_after_register(
110 self, monkeypatch: pytest.MonkeyPatch, tmp_path: pathlib.Path
111 ) -> None:
112 import tomllib
113 _patch_home(monkeypatch, tmp_path)
114 _patch_keychain(monkeypatch)
115 _run_keygen(monkeypatch)
116 _run_register(monkeypatch)
117
118 identity_file = id_module._IDENTITY_FILE
119 parsed = tomllib.loads(identity_file.read_text())
120 entry = parsed[_HOSTNAME]
121 assert "hd_path" in entry, (
122 f"hd_path was lost during register. Entry: {entry}"
123 )
124 assert entry["hd_path"].startswith("m/"), f"hd_path malformed: {entry['hd_path']}"
125
126
127 class TestRegisterNoKeyPath:
128 """R2: key_path must NOT appear in the entry after register."""
129
130 def test_R2_no_key_path_after_register(
131 self, monkeypatch: pytest.MonkeyPatch, tmp_path: pathlib.Path
132 ) -> None:
133 import tomllib
134 _patch_home(monkeypatch, tmp_path)
135 _patch_keychain(monkeypatch)
136 _run_keygen(monkeypatch)
137 _run_register(monkeypatch)
138
139 identity_file = id_module._IDENTITY_FILE
140 parsed = tomllib.loads(identity_file.read_text())
141 entry = parsed[_HOSTNAME]
142 assert "key_path" not in entry, (
143 f"key_path must not appear in identity entry after register. Entry: {entry}"
144 )
145
146
147 class TestRegisterFingerprintMatchesMnemonic:
148 """R3: fingerprint in the entry must match the mnemonic-derived key, not a stale PEM."""
149
150 def test_R3_fingerprint_matches_mnemonic_derivation(
151 self, monkeypatch: pytest.MonkeyPatch, tmp_path: pathlib.Path
152 ) -> None:
153 import tomllib
154 from muse.core.bip39 import mnemonic_to_seed
155 from muse.core.keypair import derive_hd_public_info
156
157 _patch_home(monkeypatch, tmp_path)
158 _patch_keychain(monkeypatch)
159 _run_keygen(monkeypatch)
160 _run_register(monkeypatch)
161
162 seed = mnemonic_to_seed(_FIXED_MNEMONIC)
163 _, expected_fingerprint = derive_hd_public_info(seed)
164
165 identity_file = id_module._IDENTITY_FILE
166 parsed = tomllib.loads(identity_file.read_text())
167 entry = parsed[_HOSTNAME]
168 stored_fp = entry.get("fingerprint", "")
169
170 assert stored_fp == expected_fingerprint, (
171 f"Fingerprint mismatch: stored={stored_fp} expected={expected_fingerprint}. "
172 "register wrote a stale PEM-derived fingerprint instead of the mnemonic-derived one."
173 )
174
175
176 class TestRegisterRoundTrip:
177 """R4: resolve_signing_identity must return a key after keygen + register."""
178
179 def test_R4_resolve_signing_identity_works_after_register(
180 self, monkeypatch: pytest.MonkeyPatch, tmp_path: pathlib.Path
181 ) -> None:
182 from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey
183 from muse.core.identity import resolve_signing_identity
184
185 _patch_home(monkeypatch, tmp_path)
186 _patch_keychain(monkeypatch)
187 _run_keygen(monkeypatch)
188 _run_register(monkeypatch)
189
190 result = resolve_signing_identity(_HUB)
191 assert result is not None, (
192 "resolve_signing_identity returned None after keygen + register. "
193 "The identity entry is missing hd_path or the mnemonic is not in the keychain."
194 )
195 handle, private_key = result
196 assert handle == "gabriel"
197 assert isinstance(private_key, Ed25519PrivateKey)
File History 1 commit
sha256:b636f72dcba9e190afb980bece906fa5b717fbde014b76ef023df8cb96e01eb9 docs: expand cache plan with all seven testing tiers and do… Sonnet 4.6 131 days ago