gabriel / muse public
test_passphrase_env_warn.py python
280 lines 10.5 KB
Raw
sha256:81ae324db5ad375fbfe4834c6fcb378312cafad3cc92dec5d3e5c427306621a2 fix: remove commit_exists filter from have anchors — server… Sonnet 4.6 patch 21 days ago
1 """Tests for MUSE_BIP39_PASSPHRASE env-var security warning — Rank 3.
2
3 ``MUSE_BIP39_PASSPHRASE`` is visible to the process owner (and root) in
4 ``/proc/pid/environ`` — less dangerous than ``--passphrase PHRASE`` (which
5 was removed because it was world-readable in ``ps``), but still a meaningful
6 exposure. Operators should be nudged toward ``--passphrase-fd`` for
7 production use.
8
9 When the env var is actually consumed as the passphrase source, a
10 ``logger.warning`` is emitted on ``muse.cli.commands.auth`` so the operator
11 sees it in any log setup that surfaces WARNING-level messages.
12
13 The warning must NOT fire when:
14 - the env var is not set (no passphrase at all — nothing to warn about)
15 - ``--passphrase-fd`` is provided (env var is ignored; fd is the source)
16
17 Coverage
18 --------
19 I Warning fires when env var is the active passphrase source
20 I1 warning logged when MUSE_BIP39_PASSPHRASE is set and used by keygen
21 I2 warning logged when MUSE_BIP39_PASSPHRASE is set and used by recover
22 I3 warning logged when MUSE_BIP39_PASSPHRASE is set and used by rotate
23 I4 warning text mentions MUSE_BIP39_PASSPHRASE by name
24 I5 warning text mentions --passphrase-fd as the safer alternative
25
26 II Warning suppressed when env var is not the active source
27 II1 no warning when MUSE_BIP39_PASSPHRASE is not set
28 II2 no warning when --passphrase-fd is provided (fd takes priority)
29
30 III Functional correctness unaffected
31 III1 derivation result is identical before and after the warning
32 """
33
34 from __future__ import annotations
35
36 import json
37 import logging
38 import os
39 import pathlib
40
41 import pytest
42
43 from tests.cli_test_helper import CliRunner, InvokeResult
44 from muse.core import keypair as kp_module
45 from muse.core import identity as id_module
46
47 runner = CliRunner()
48
49 _HUB = "https://localhost:1337"
50 _MNEMONIC = (
51 "abandon abandon abandon abandon abandon abandon abandon abandon "
52 "abandon abandon abandon about"
53 )
54 _LOGGER = "muse.cli.commands.auth"
55
56
57 # ---------------------------------------------------------------------------
58 # Fixtures
59 # ---------------------------------------------------------------------------
60
61
62 @pytest.fixture()
63 def isolated(monkeypatch: pytest.MonkeyPatch, tmp_path: pathlib.Path) -> pathlib.Path:
64 fake_home = tmp_path / "home"
65 fake_home.mkdir(parents=True, exist_ok=True)
66 monkeypatch.setattr(pathlib.Path, "home", staticmethod(lambda: fake_home))
67 monkeypatch.setattr(kp_module, "_KEYS_DIR", fake_home / ".muse" / "keys")
68 monkeypatch.setattr(id_module, "_IDENTITY_DIR", fake_home / ".muse")
69 monkeypatch.setattr(id_module, "_IDENTITY_FILE", fake_home / ".muse" / "identity.toml")
70 return fake_home
71
72
73 @pytest.fixture()
74 def fixed_mnemonic(monkeypatch: pytest.MonkeyPatch) -> str:
75 from muse.core import bip39 as bip39_mod
76 monkeypatch.setattr(bip39_mod, "generate_mnemonic", lambda **kw: _MNEMONIC)
77 return _MNEMONIC
78
79
80 def _pipe_passphrase(passphrase: str) -> int:
81 r_fd, w_fd = os.pipe()
82 os.write(w_fd, passphrase.encode())
83 os.close(w_fd)
84 return r_fd
85
86
87 def _keygen(extra: list[str] | None = None) -> InvokeResult:
88 return runner.invoke(None, ["auth", "keygen", "--hub", _HUB, "--json"] + (extra or []))
89
90
91 def _recover(extra: list[str] | None = None) -> InvokeResult:
92 return runner.invoke(
93 None,
94 ["auth", "recover", "--hub", _HUB, "--force", "--json"] + (extra or []),
95 input=_MNEMONIC + "\n",
96 )
97
98
99 def _rotate(extra: list[str] | None = None) -> InvokeResult:
100 return runner.invoke(
101 None,
102 ["auth", "rotate", "--hub", _HUB, "--json"] + (extra or []),
103 input=_MNEMONIC + "\n",
104 )
105
106
107 def _fp(result: InvokeResult) -> str:
108 return json.loads(result.output.splitlines()[0])["fingerprint"] # type: ignore[union-attr]
109
110
111 def _passphrase_warnings(caplog: pytest.LogCaptureFixture) -> list[str]:
112 return [
113 r.getMessage()
114 for r in caplog.records
115 if r.levelno >= logging.WARNING and "MUSE_BIP39_PASSPHRASE" in r.getMessage()
116 ]
117
118
119 # ---------------------------------------------------------------------------
120 # I Warning fires when env var is the active passphrase source
121 # ---------------------------------------------------------------------------
122
123
124 class TestEnvVarWarningFires:
125 def test_I1_warning_on_keygen_with_env_var(
126 self,
127 isolated: pathlib.Path,
128 fixed_mnemonic: str,
129 monkeypatch: pytest.MonkeyPatch,
130 caplog: pytest.LogCaptureFixture,
131 ) -> None:
132 """I1: logger.warning fires when env var is used as passphrase source in keygen."""
133 monkeypatch.setenv("MUSE_BIP39_PASSPHRASE", "secret")
134 with caplog.at_level(logging.WARNING, logger=_LOGGER):
135 r = _keygen()
136 assert r.exit_code == 0, r.output # type: ignore[union-attr]
137 assert _passphrase_warnings(caplog), (
138 "Expected a warning about MUSE_BIP39_PASSPHRASE — got none"
139 )
140
141 def test_I2_warning_on_recover_with_env_var(
142 self,
143 isolated: pathlib.Path,
144 fixed_mnemonic: str,
145 monkeypatch: pytest.MonkeyPatch,
146 caplog: pytest.LogCaptureFixture,
147 ) -> None:
148 """I2: warning fires when env var is used in recover."""
149 _keygen()
150 monkeypatch.setenv("MUSE_BIP39_PASSPHRASE", "secret")
151 with caplog.at_level(logging.WARNING, logger=_LOGGER):
152 r = _recover()
153 assert r.exit_code == 0, r.output # type: ignore[union-attr]
154 assert _passphrase_warnings(caplog), (
155 "Expected a warning about MUSE_BIP39_PASSPHRASE in recover — got none"
156 )
157
158 def test_I3_warning_on_rotate_with_env_var(
159 self,
160 isolated: pathlib.Path,
161 fixed_mnemonic: str,
162 monkeypatch: pytest.MonkeyPatch,
163 caplog: pytest.LogCaptureFixture,
164 ) -> None:
165 """I3: warning fires when env var is used in rotate."""
166 _keygen()
167 monkeypatch.setenv("MUSE_BIP39_PASSPHRASE", "secret")
168 monkeypatch.setattr("muse.cli.commands.auth._post_challenge",
169 lambda *a, **kw: {"challenge_token": "ab" * 32, "is_new_key": True})
170 monkeypatch.setattr("muse.cli.commands.auth._json_post_raw", lambda *a, **kw: {})
171 monkeypatch.setattr("muse.cli.commands.auth._hub_delete", lambda *a, **kw: None)
172 with caplog.at_level(logging.WARNING, logger=_LOGGER):
173 r = _rotate()
174 assert r.exit_code == 0, r.output # type: ignore[union-attr]
175 assert _passphrase_warnings(caplog), (
176 "Expected a warning about MUSE_BIP39_PASSPHRASE in rotate — got none"
177 )
178
179 def test_I4_warning_names_the_env_var(
180 self,
181 isolated: pathlib.Path,
182 fixed_mnemonic: str,
183 monkeypatch: pytest.MonkeyPatch,
184 caplog: pytest.LogCaptureFixture,
185 ) -> None:
186 """I4: warning text names MUSE_BIP39_PASSPHRASE explicitly."""
187 monkeypatch.setenv("MUSE_BIP39_PASSPHRASE", "secret")
188 with caplog.at_level(logging.WARNING, logger=_LOGGER):
189 _keygen()
190 warnings = _passphrase_warnings(caplog)
191 assert warnings, "No warning found"
192 assert any("MUSE_BIP39_PASSPHRASE" in w for w in warnings)
193
194 def test_I5_warning_mentions_passphrase_fd(
195 self,
196 isolated: pathlib.Path,
197 fixed_mnemonic: str,
198 monkeypatch: pytest.MonkeyPatch,
199 caplog: pytest.LogCaptureFixture,
200 ) -> None:
201 """I5: warning text recommends --passphrase-fd as the safer alternative."""
202 monkeypatch.setenv("MUSE_BIP39_PASSPHRASE", "secret")
203 with caplog.at_level(logging.WARNING, logger=_LOGGER):
204 _keygen()
205 warnings = _passphrase_warnings(caplog)
206 assert warnings, "No warning found"
207 combined = " ".join(warnings)
208 assert "--passphrase-fd" in combined, (
209 f"Warning must recommend --passphrase-fd: {combined}"
210 )
211
212
213 # ---------------------------------------------------------------------------
214 # II Warning suppressed when env var is not the active source
215 # ---------------------------------------------------------------------------
216
217
218 class TestEnvVarWarningSuppressed:
219 def test_II1_no_warning_when_env_var_not_set(
220 self,
221 isolated: pathlib.Path,
222 fixed_mnemonic: str,
223 monkeypatch: pytest.MonkeyPatch,
224 caplog: pytest.LogCaptureFixture,
225 ) -> None:
226 """II1: no warning when MUSE_BIP39_PASSPHRASE is not set."""
227 monkeypatch.delenv("MUSE_BIP39_PASSPHRASE", raising=False)
228 with caplog.at_level(logging.WARNING, logger=_LOGGER):
229 r = _keygen()
230 assert r.exit_code == 0, r.output # type: ignore[union-attr]
231 assert not _passphrase_warnings(caplog), (
232 "Must not warn when env var is not set"
233 )
234
235 def test_II2_no_warning_when_fd_provided(
236 self,
237 isolated: pathlib.Path,
238 fixed_mnemonic: str,
239 monkeypatch: pytest.MonkeyPatch,
240 caplog: pytest.LogCaptureFixture,
241 ) -> None:
242 """II2: no warning when --passphrase-fd is used (env var is not the source)."""
243 monkeypatch.setenv("MUSE_BIP39_PASSPHRASE", "secret")
244 with caplog.at_level(logging.WARNING, logger=_LOGGER):
245 r = _keygen(["--passphrase-fd", str(_pipe_passphrase("secret"))])
246 assert r.exit_code == 0, r.output # type: ignore[union-attr]
247 assert not _passphrase_warnings(caplog), (
248 "Must not warn when --passphrase-fd is provided (env var not consumed)"
249 )
250
251
252 # ---------------------------------------------------------------------------
253 # III Functional correctness unaffected
254 # ---------------------------------------------------------------------------
255
256
257 class TestEnvVarWarningFunctional:
258 def test_III1_derivation_correct_with_warning(
259 self,
260 isolated: pathlib.Path,
261 fixed_mnemonic: str,
262 monkeypatch: pytest.MonkeyPatch,
263 caplog: pytest.LogCaptureFixture,
264 ) -> None:
265 """III1: warning is informational — derivation result is unchanged."""
266 # Reference: use fd (no warning)
267 r_fd = _keygen(["--passphrase-fd", str(_pipe_passphrase("secret"))])
268 assert r_fd.exit_code == 0
269 fp_fd = _fp(r_fd)
270
271 # Now use env var (warning fires) — fingerprint must match
272 monkeypatch.setenv("MUSE_BIP39_PASSPHRASE", "secret")
273 with caplog.at_level(logging.WARNING, logger=_LOGGER):
274 r_env = _keygen(["--force"])
275 assert r_env.exit_code == 0, r_env.output # type: ignore[union-attr]
276 fp_env = _fp(r_env)
277
278 assert fp_fd == fp_env, (
279 "Warning must not affect derivation — same passphrase must give same fingerprint"
280 )
File History 4 commits
sha256:81ae324db5ad375fbfe4834c6fcb378312cafad3cc92dec5d3e5c427306621a2 fix: remove commit_exists filter from have anchors — server… Sonnet 4.6 patch 21 days ago
sha256:36c3cb3e76619d4c30a6d9bf81b5ec4ff148e30dcfed913e3114ca7b43b81c7e fix: rename objects→blobs in push client and all stale test… Sonnet 4.6 patch 22 days ago
sha256:c06a9b9b9fee26c68ea725b44d54b2c0a171301ce9de746d5b656617b4463a9a fix: repair four test failures from post-migration audit Sonnet 4.6 patch 28 days ago
sha256:1900655993c83c4107067375548a7be823e471d2515830842f1a12cba4bd3cdf fix: unified object store migration — idempotent writes, JS… Sonnet 4.6 minor 29 days ago