"""Tests for MUSE_BIP39_PASSPHRASE env-var security warning — Rank 3. ``MUSE_BIP39_PASSPHRASE`` is visible to the process owner (and root) in ``/proc/pid/environ`` — less dangerous than ``--passphrase PHRASE`` (which was removed because it was world-readable in ``ps``), but still a meaningful exposure. Operators should be nudged toward ``--passphrase-fd`` for production use. When the env var is actually consumed as the passphrase source, a ``logger.warning`` is emitted on ``muse.cli.commands.auth`` so the operator sees it in any log setup that surfaces WARNING-level messages. The warning must NOT fire when: - the env var is not set (no passphrase at all — nothing to warn about) - ``--passphrase-fd`` is provided (env var is ignored; fd is the source) Coverage -------- I Warning fires when env var is the active passphrase source I1 warning logged when MUSE_BIP39_PASSPHRASE is set and used by keygen I2 warning logged when MUSE_BIP39_PASSPHRASE is set and used by recover I3 warning logged when MUSE_BIP39_PASSPHRASE is set and used by rotate I4 warning text mentions MUSE_BIP39_PASSPHRASE by name I5 warning text mentions --passphrase-fd as the safer alternative II Warning suppressed when env var is not the active source II1 no warning when MUSE_BIP39_PASSPHRASE is not set II2 no warning when --passphrase-fd is provided (fd takes priority) III Functional correctness unaffected III1 derivation result is identical before and after the warning """ from __future__ import annotations import json import logging import os import pathlib import pytest from tests.cli_test_helper import CliRunner, InvokeResult from muse.core import keypair as kp_module from muse.core import identity as id_module runner = CliRunner() _HUB = "https://localhost:1337" _MNEMONIC = ( "abandon abandon abandon abandon abandon abandon abandon abandon " "abandon abandon abandon about" ) _LOGGER = "muse.cli.commands.auth" # --------------------------------------------------------------------------- # Fixtures # --------------------------------------------------------------------------- @pytest.fixture() def isolated(monkeypatch: pytest.MonkeyPatch, tmp_path: pathlib.Path) -> pathlib.Path: fake_home = tmp_path / "home" fake_home.mkdir(parents=True, exist_ok=True) monkeypatch.setattr(pathlib.Path, "home", staticmethod(lambda: fake_home)) monkeypatch.setattr(kp_module, "_KEYS_DIR", fake_home / ".muse" / "keys") monkeypatch.setattr(id_module, "_IDENTITY_DIR", fake_home / ".muse") monkeypatch.setattr(id_module, "_IDENTITY_FILE", fake_home / ".muse" / "identity.toml") return fake_home @pytest.fixture() def fixed_mnemonic(monkeypatch: pytest.MonkeyPatch) -> str: from muse.core import bip39 as bip39_mod monkeypatch.setattr(bip39_mod, "generate_mnemonic", lambda **kw: _MNEMONIC) return _MNEMONIC def _pipe_passphrase(passphrase: str) -> int: r_fd, w_fd = os.pipe() os.write(w_fd, passphrase.encode()) os.close(w_fd) return r_fd def _keygen(extra: list[str] | None = None) -> InvokeResult: return runner.invoke(None, ["auth", "keygen", "--hub", _HUB, "--json"] + (extra or [])) def _recover(extra: list[str] | None = None) -> InvokeResult: return runner.invoke( None, ["auth", "recover", "--hub", _HUB, "--force", "--json"] + (extra or []), input=_MNEMONIC + "\n", ) def _rotate(extra: list[str] | None = None) -> InvokeResult: return runner.invoke( None, ["auth", "rotate", "--hub", _HUB, "--json"] + (extra or []), input=_MNEMONIC + "\n", ) def _fp(result: InvokeResult) -> str: return json.loads(result.output.splitlines()[0])["fingerprint"] # type: ignore[union-attr] def _passphrase_warnings(caplog: pytest.LogCaptureFixture) -> list[str]: return [ r.getMessage() for r in caplog.records if r.levelno >= logging.WARNING and "MUSE_BIP39_PASSPHRASE" in r.getMessage() ] # --------------------------------------------------------------------------- # I Warning fires when env var is the active passphrase source # --------------------------------------------------------------------------- class TestEnvVarWarningFires: def test_I1_warning_on_keygen_with_env_var( self, isolated: pathlib.Path, fixed_mnemonic: str, monkeypatch: pytest.MonkeyPatch, caplog: pytest.LogCaptureFixture, ) -> None: """I1: logger.warning fires when env var is used as passphrase source in keygen.""" monkeypatch.setenv("MUSE_BIP39_PASSPHRASE", "secret") with caplog.at_level(logging.WARNING, logger=_LOGGER): r = _keygen() assert r.exit_code == 0, r.output # type: ignore[union-attr] assert _passphrase_warnings(caplog), ( "Expected a warning about MUSE_BIP39_PASSPHRASE — got none" ) def test_I2_warning_on_recover_with_env_var( self, isolated: pathlib.Path, fixed_mnemonic: str, monkeypatch: pytest.MonkeyPatch, caplog: pytest.LogCaptureFixture, ) -> None: """I2: warning fires when env var is used in recover.""" _keygen() monkeypatch.setenv("MUSE_BIP39_PASSPHRASE", "secret") with caplog.at_level(logging.WARNING, logger=_LOGGER): r = _recover() assert r.exit_code == 0, r.output # type: ignore[union-attr] assert _passphrase_warnings(caplog), ( "Expected a warning about MUSE_BIP39_PASSPHRASE in recover — got none" ) def test_I3_warning_on_rotate_with_env_var( self, isolated: pathlib.Path, fixed_mnemonic: str, monkeypatch: pytest.MonkeyPatch, caplog: pytest.LogCaptureFixture, ) -> None: """I3: warning fires when env var is used in rotate.""" _keygen() monkeypatch.setenv("MUSE_BIP39_PASSPHRASE", "secret") monkeypatch.setattr("muse.cli.commands.auth._post_challenge", lambda *a, **kw: {"challenge_token": "ab" * 32, "is_new_key": True}) monkeypatch.setattr("muse.cli.commands.auth._json_post_raw", lambda *a, **kw: {}) monkeypatch.setattr("muse.cli.commands.auth._hub_delete", lambda *a, **kw: None) with caplog.at_level(logging.WARNING, logger=_LOGGER): r = _rotate() assert r.exit_code == 0, r.output # type: ignore[union-attr] assert _passphrase_warnings(caplog), ( "Expected a warning about MUSE_BIP39_PASSPHRASE in rotate — got none" ) def test_I4_warning_names_the_env_var( self, isolated: pathlib.Path, fixed_mnemonic: str, monkeypatch: pytest.MonkeyPatch, caplog: pytest.LogCaptureFixture, ) -> None: """I4: warning text names MUSE_BIP39_PASSPHRASE explicitly.""" monkeypatch.setenv("MUSE_BIP39_PASSPHRASE", "secret") with caplog.at_level(logging.WARNING, logger=_LOGGER): _keygen() warnings = _passphrase_warnings(caplog) assert warnings, "No warning found" assert any("MUSE_BIP39_PASSPHRASE" in w for w in warnings) def test_I5_warning_mentions_passphrase_fd( self, isolated: pathlib.Path, fixed_mnemonic: str, monkeypatch: pytest.MonkeyPatch, caplog: pytest.LogCaptureFixture, ) -> None: """I5: warning text recommends --passphrase-fd as the safer alternative.""" monkeypatch.setenv("MUSE_BIP39_PASSPHRASE", "secret") with caplog.at_level(logging.WARNING, logger=_LOGGER): _keygen() warnings = _passphrase_warnings(caplog) assert warnings, "No warning found" combined = " ".join(warnings) assert "--passphrase-fd" in combined, ( f"Warning must recommend --passphrase-fd: {combined}" ) # --------------------------------------------------------------------------- # II Warning suppressed when env var is not the active source # --------------------------------------------------------------------------- class TestEnvVarWarningSuppressed: def test_II1_no_warning_when_env_var_not_set( self, isolated: pathlib.Path, fixed_mnemonic: str, monkeypatch: pytest.MonkeyPatch, caplog: pytest.LogCaptureFixture, ) -> None: """II1: no warning when MUSE_BIP39_PASSPHRASE is not set.""" monkeypatch.delenv("MUSE_BIP39_PASSPHRASE", raising=False) with caplog.at_level(logging.WARNING, logger=_LOGGER): r = _keygen() assert r.exit_code == 0, r.output # type: ignore[union-attr] assert not _passphrase_warnings(caplog), ( "Must not warn when env var is not set" ) def test_II2_no_warning_when_fd_provided( self, isolated: pathlib.Path, fixed_mnemonic: str, monkeypatch: pytest.MonkeyPatch, caplog: pytest.LogCaptureFixture, ) -> None: """II2: no warning when --passphrase-fd is used (env var is not the source).""" monkeypatch.setenv("MUSE_BIP39_PASSPHRASE", "secret") with caplog.at_level(logging.WARNING, logger=_LOGGER): r = _keygen(["--passphrase-fd", str(_pipe_passphrase("secret"))]) assert r.exit_code == 0, r.output # type: ignore[union-attr] assert not _passphrase_warnings(caplog), ( "Must not warn when --passphrase-fd is provided (env var not consumed)" ) # --------------------------------------------------------------------------- # III Functional correctness unaffected # --------------------------------------------------------------------------- class TestEnvVarWarningFunctional: def test_III1_derivation_correct_with_warning( self, isolated: pathlib.Path, fixed_mnemonic: str, monkeypatch: pytest.MonkeyPatch, caplog: pytest.LogCaptureFixture, ) -> None: """III1: warning is informational — derivation result is unchanged.""" # Reference: use fd (no warning) r_fd = _keygen(["--passphrase-fd", str(_pipe_passphrase("secret"))]) assert r_fd.exit_code == 0 fp_fd = _fp(r_fd) # Now use env var (warning fires) — fingerprint must match monkeypatch.setenv("MUSE_BIP39_PASSPHRASE", "secret") with caplog.at_level(logging.WARNING, logger=_LOGGER): r_env = _keygen(["--force"]) assert r_env.exit_code == 0, r_env.output # type: ignore[union-attr] fp_env = _fp(r_env) assert fp_fd == fp_env, ( "Warning must not affect derivation — same passphrase must give same fingerprint" )