gabriel / muse public
test_fd_guard.py python
163 lines 6.1 KB
Raw
sha256:81ae324db5ad375fbfe4834c6fcb378312cafad3cc92dec5d3e5c427306621a2 fix: remove commit_exists filter from have anchors — server… Sonnet 4.6 patch 21 days ago
1 """Tests for fd guard rails on --passphrase-fd and --mnemonic-fd.
2
3 Passing fd 0 (stdin), 1 (stdout), or 2 (stderr) to either flag is always a
4 mistake and can be destructive:
5
6 --passphrase-fd 1 → os.read(1, …) reads from stdout, os.close(1) kills it
7 --passphrase-fd 2 → os.read(2, …) reads from stderr, os.close(2) kills it
8 --mnemonic-fd 0 → os.fdopen(0) wraps stdin; fine in isolation but wrong
9 --mnemonic-fd 1 → os.fdopen(1, "r") on a write-only fd — undefined
10 --mnemonic-fd 2 → same
11
12 Both _resolve_passphrase and _read_mnemonic_securely must reject fd < 3 with
13 a clear error before touching the descriptor.
14
15 Coverage
16 --------
17 I --passphrase-fd rejects stdin / stdout / stderr
18 I1 --passphrase-fd 0 exits non-zero
19 I2 --passphrase-fd 1 exits non-zero
20 I3 --passphrase-fd 2 exits non-zero
21
22 II --mnemonic-fd rejects stdin / stdout / stderr
23 II1 recover --mnemonic-fd 0 exits non-zero
24 II2 recover --mnemonic-fd 1 exits non-zero
25 II3 recover --mnemonic-fd 2 exits non-zero
26
27 III Valid fds (>= 3) still work
28 III1 --passphrase-fd with a real pipe fd succeeds
29 III2 --mnemonic-fd with a real pipe fd succeeds
30 """
31
32 from __future__ import annotations
33
34 import os
35 import pathlib
36
37 import pytest
38
39 from tests.cli_test_helper import CliRunner
40 from muse.core import keypair as kp_module
41 from muse.core import identity as id_module
42
43 runner = CliRunner()
44
45 _HUB = "https://localhost:1337"
46 _MNEMONIC = (
47 "abandon abandon abandon abandon abandon abandon abandon abandon "
48 "abandon abandon abandon about"
49 )
50
51
52 @pytest.fixture()
53 def isolated(monkeypatch: pytest.MonkeyPatch, tmp_path: pathlib.Path) -> pathlib.Path:
54 fake_home = tmp_path / "home"
55 fake_home.mkdir(parents=True, exist_ok=True)
56 monkeypatch.setattr(pathlib.Path, "home", staticmethod(lambda: fake_home))
57 monkeypatch.setattr(kp_module, "_KEYS_DIR", fake_home / ".muse" / "keys")
58 monkeypatch.setattr(id_module, "_IDENTITY_DIR", fake_home / ".muse")
59 monkeypatch.setattr(id_module, "_IDENTITY_FILE", fake_home / ".muse" / "identity.toml")
60 return fake_home
61
62
63 @pytest.fixture()
64 def fixed_mnemonic(monkeypatch: pytest.MonkeyPatch) -> str:
65 from muse.core import bip39 as bip39_mod
66 monkeypatch.setattr(bip39_mod, "generate_mnemonic", lambda **kw: _MNEMONIC)
67 return _MNEMONIC
68
69
70 def _pipe(content: str) -> int:
71 r, w = os.pipe()
72 os.write(w, content.encode())
73 os.close(w)
74 return r
75
76
77 # ---------------------------------------------------------------------------
78 # I --passphrase-fd rejects stdin / stdout / stderr
79 # ---------------------------------------------------------------------------
80
81 class TestPassphraseFdGuard:
82 def test_I1_passphrase_fd_0_rejected(
83 self, isolated: pathlib.Path, fixed_mnemonic: str
84 ) -> None:
85 """I1: --passphrase-fd 0 must be rejected before touching the fd."""
86 r = runner.invoke(None, ["auth", "keygen", "--hub", _HUB, "--passphrase-fd", "0"])
87 assert r.exit_code != 0, "passphrase-fd 0 (stdin) must be rejected"
88
89 def test_I2_passphrase_fd_1_rejected(
90 self, isolated: pathlib.Path, fixed_mnemonic: str
91 ) -> None:
92 """I2: --passphrase-fd 1 must be rejected before os.read closes stdout."""
93 r = runner.invoke(None, ["auth", "keygen", "--hub", _HUB, "--passphrase-fd", "1"])
94 assert r.exit_code != 0, "passphrase-fd 1 (stdout) must be rejected"
95
96 def test_I3_passphrase_fd_2_rejected(
97 self, isolated: pathlib.Path, fixed_mnemonic: str
98 ) -> None:
99 """I3: --passphrase-fd 2 must be rejected before os.read closes stderr."""
100 r = runner.invoke(None, ["auth", "keygen", "--hub", _HUB, "--passphrase-fd", "2"])
101 assert r.exit_code != 0, "passphrase-fd 2 (stderr) must be rejected"
102
103
104 # ---------------------------------------------------------------------------
105 # II --mnemonic-fd rejects stdin / stdout / stderr
106 # ---------------------------------------------------------------------------
107
108 class TestMnemonicFdGuard:
109 def test_II1_mnemonic_fd_0_rejected(self, isolated: pathlib.Path) -> None:
110 """II1: --mnemonic-fd 0 must be rejected."""
111 r = runner.invoke(
112 None,
113 ["auth", "recover", "--hub", _HUB, "--force", "--mnemonic-fd", "0"],
114 )
115 assert r.exit_code != 0, "mnemonic-fd 0 (stdin) must be rejected"
116
117 def test_II2_mnemonic_fd_1_rejected(self, isolated: pathlib.Path) -> None:
118 """II2: --mnemonic-fd 1 must be rejected."""
119 r = runner.invoke(
120 None,
121 ["auth", "recover", "--hub", _HUB, "--force", "--mnemonic-fd", "1"],
122 )
123 assert r.exit_code != 0, "mnemonic-fd 1 (stdout) must be rejected"
124
125 def test_II3_mnemonic_fd_2_rejected(self, isolated: pathlib.Path) -> None:
126 """II3: --mnemonic-fd 2 must be rejected."""
127 r = runner.invoke(
128 None,
129 ["auth", "recover", "--hub", _HUB, "--force", "--mnemonic-fd", "2"],
130 )
131 assert r.exit_code != 0, "mnemonic-fd 2 (stderr) must be rejected"
132
133
134 # ---------------------------------------------------------------------------
135 # III Valid fds (>= 3) still work
136 # ---------------------------------------------------------------------------
137
138 class TestValidFdStillWorks:
139 def test_III1_passphrase_fd_valid_pipe_works(
140 self, isolated: pathlib.Path, fixed_mnemonic: str
141 ) -> None:
142 """III1: a real pipe fd >= 3 is accepted."""
143 r = runner.invoke(
144 None,
145 ["auth", "keygen", "--hub", _HUB, "--json",
146 "--passphrase-fd", str(_pipe("hunter2"))],
147 )
148 assert r.exit_code == 0, r.output
149
150 def test_III2_mnemonic_fd_valid_pipe_works(
151 self, isolated: pathlib.Path, fixed_mnemonic: str
152 ) -> None:
153 """III2: a real mnemonic pipe fd >= 3 is accepted."""
154 _keygen_first = runner.invoke(
155 None, ["auth", "keygen", "--hub", _HUB, "--json"]
156 )
157 assert _keygen_first.exit_code == 0
158 r = runner.invoke(
159 None,
160 ["auth", "recover", "--hub", _HUB, "--force", "--json",
161 "--mnemonic-fd", str(_pipe(_MNEMONIC))],
162 )
163 assert r.exit_code == 0, r.output
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 23 days ago
sha256:c06a9b9b9fee26c68ea725b44d54b2c0a171301ce9de746d5b656617b4463a9a fix: repair four test failures from post-migration audit Sonnet 4.6 patch 29 days ago
sha256:1900655993c83c4107067375548a7be823e471d2515830842f1a12cba4bd3cdf fix: unified object store migration — idempotent writes, JS… Sonnet 4.6 minor 29 days ago