gabriel / muse public
test_passphrase_env_warn.py python
276 lines 10.1 KB
Raw
sha256:b636f72dcba9e190afb980bece906fa5b717fbde014b76ef023df8cb96e01eb9 docs: expand cache plan with all seven testing tiers and do… Sonnet 4.6 131 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
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):
88 return runner.invoke(None, ["auth", "keygen", "--hub", _HUB, "--json"] + (extra or []))
89
90
91 def _recover(extra: list[str] | None = None):
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):
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) -> 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 with caplog.at_level(logging.WARNING, logger=_LOGGER):
169 r = _rotate()
170 assert r.exit_code == 0, r.output # type: ignore[union-attr]
171 assert _passphrase_warnings(caplog), (
172 "Expected a warning about MUSE_BIP39_PASSPHRASE in rotate — got none"
173 )
174
175 def test_I4_warning_names_the_env_var(
176 self,
177 isolated: pathlib.Path,
178 fixed_mnemonic: str,
179 monkeypatch: pytest.MonkeyPatch,
180 caplog: pytest.LogCaptureFixture,
181 ) -> None:
182 """I4: warning text names MUSE_BIP39_PASSPHRASE explicitly."""
183 monkeypatch.setenv("MUSE_BIP39_PASSPHRASE", "secret")
184 with caplog.at_level(logging.WARNING, logger=_LOGGER):
185 _keygen()
186 warnings = _passphrase_warnings(caplog)
187 assert warnings, "No warning found"
188 assert any("MUSE_BIP39_PASSPHRASE" in w for w in warnings)
189
190 def test_I5_warning_mentions_passphrase_fd(
191 self,
192 isolated: pathlib.Path,
193 fixed_mnemonic: str,
194 monkeypatch: pytest.MonkeyPatch,
195 caplog: pytest.LogCaptureFixture,
196 ) -> None:
197 """I5: warning text recommends --passphrase-fd as the safer alternative."""
198 monkeypatch.setenv("MUSE_BIP39_PASSPHRASE", "secret")
199 with caplog.at_level(logging.WARNING, logger=_LOGGER):
200 _keygen()
201 warnings = _passphrase_warnings(caplog)
202 assert warnings, "No warning found"
203 combined = " ".join(warnings)
204 assert "--passphrase-fd" in combined, (
205 f"Warning must recommend --passphrase-fd: {combined}"
206 )
207
208
209 # ---------------------------------------------------------------------------
210 # II Warning suppressed when env var is not the active source
211 # ---------------------------------------------------------------------------
212
213
214 class TestEnvVarWarningSuppressed:
215 def test_II1_no_warning_when_env_var_not_set(
216 self,
217 isolated: pathlib.Path,
218 fixed_mnemonic: str,
219 monkeypatch: pytest.MonkeyPatch,
220 caplog: pytest.LogCaptureFixture,
221 ) -> None:
222 """II1: no warning when MUSE_BIP39_PASSPHRASE is not set."""
223 monkeypatch.delenv("MUSE_BIP39_PASSPHRASE", raising=False)
224 with caplog.at_level(logging.WARNING, logger=_LOGGER):
225 r = _keygen()
226 assert r.exit_code == 0, r.output # type: ignore[union-attr]
227 assert not _passphrase_warnings(caplog), (
228 "Must not warn when env var is not set"
229 )
230
231 def test_II2_no_warning_when_fd_provided(
232 self,
233 isolated: pathlib.Path,
234 fixed_mnemonic: str,
235 monkeypatch: pytest.MonkeyPatch,
236 caplog: pytest.LogCaptureFixture,
237 ) -> None:
238 """II2: no warning when --passphrase-fd is used (env var is not the source)."""
239 monkeypatch.setenv("MUSE_BIP39_PASSPHRASE", "secret")
240 with caplog.at_level(logging.WARNING, logger=_LOGGER):
241 r = _keygen(["--passphrase-fd", str(_pipe_passphrase("secret"))])
242 assert r.exit_code == 0, r.output # type: ignore[union-attr]
243 assert not _passphrase_warnings(caplog), (
244 "Must not warn when --passphrase-fd is provided (env var not consumed)"
245 )
246
247
248 # ---------------------------------------------------------------------------
249 # III Functional correctness unaffected
250 # ---------------------------------------------------------------------------
251
252
253 class TestEnvVarWarningFunctional:
254 def test_III1_derivation_correct_with_warning(
255 self,
256 isolated: pathlib.Path,
257 fixed_mnemonic: str,
258 monkeypatch: pytest.MonkeyPatch,
259 caplog: pytest.LogCaptureFixture,
260 ) -> None:
261 """III1: warning is informational — derivation result is unchanged."""
262 # Reference: use fd (no warning)
263 r_fd = _keygen(["--passphrase-fd", str(_pipe_passphrase("secret"))])
264 assert r_fd.exit_code == 0
265 fp_fd = _fp(r_fd)
266
267 # Now use env var (warning fires) — fingerprint must match
268 monkeypatch.setenv("MUSE_BIP39_PASSPHRASE", "secret")
269 with caplog.at_level(logging.WARNING, logger=_LOGGER):
270 r_env = _keygen(["--force"])
271 assert r_env.exit_code == 0, r_env.output # type: ignore[union-attr]
272 fp_env = _fp(r_env)
273
274 assert fp_fd == fp_env, (
275 "Warning must not affect derivation — same passphrase must give same fingerprint"
276 )
File History 2 commits
sha256:b636f72dcba9e190afb980bece906fa5b717fbde014b76ef023df8cb96e01eb9 docs: expand cache plan with all seven testing tiers and do… Sonnet 4.6 131 days ago
sha256:7f9e2ef5286aedad9c1e6011b4c46ca27f39dbdad6e3409357e36b26e46b3b7c docs: docstring sprint for-each-ref→hotspots — idiomatic ru… Sonnet 4.6 patch 137 days ago