"""Security tests — key file permission and ownership check before loading. Verifies that _load_private_key_from_path() refuses keys with overly-permissive file modes or foreign ownership, and logs the appropriate warning with the fix command. """ from __future__ import annotations import os import pathlib import stat from unittest.mock import patch import pytest from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey from cryptography.hazmat.primitives.serialization import ( Encoding, NoEncryption, PrivateFormat, ) from muse.core.identity import _load_private_key_from_path # --------------------------------------------------------------------------- # Helpers # --------------------------------------------------------------------------- def _generate_pem_key() -> bytes: """Generate a fresh Ed25519 private key and return PEM bytes.""" private_key = Ed25519PrivateKey.generate() return private_key.private_bytes( encoding=Encoding.PEM, format=PrivateFormat.PKCS8, encryption_algorithm=NoEncryption(), ) def _write_key(path: pathlib.Path, mode: int) -> None: """Write a valid PEM key to *path* with the given file mode.""" path.write_bytes(_generate_pem_key()) os.chmod(path, mode) # --------------------------------------------------------------------------- # Tests # --------------------------------------------------------------------------- def test_key_0600_loads(tmp_path: pathlib.Path) -> None: """0o600 key file → loads successfully and returns a key object.""" key_file = tmp_path / "key.pem" _write_key(key_file, 0o600) result = _load_private_key_from_path(str(key_file)) assert result is not None assert isinstance(result, Ed25519PrivateKey) def test_key_0644_refused(tmp_path: pathlib.Path, caplog: pytest.LogCaptureFixture) -> None: """0o644 key file → returns None and logs a warning.""" import logging key_file = tmp_path / "key.pem" _write_key(key_file, 0o644) with caplog.at_level(logging.WARNING, logger="muse.core.identity"): result = _load_private_key_from_path(str(key_file)) assert result is None assert any("0644" in r.message or "644" in r.message for r in caplog.records) def test_key_0640_refused(tmp_path: pathlib.Path, caplog: pytest.LogCaptureFixture) -> None: """0o640 key file → returns None and logs a warning.""" import logging key_file = tmp_path / "key.pem" _write_key(key_file, 0o640) with caplog.at_level(logging.WARNING, logger="muse.core.identity"): result = _load_private_key_from_path(str(key_file)) assert result is None assert any("0640" in r.message or "640" in r.message for r in caplog.records) def test_key_wrong_owner_refused( tmp_path: pathlib.Path, caplog: pytest.LogCaptureFixture, ) -> None: """Key owned by different UID → returns None.""" import logging key_file = tmp_path / "key.pem" _write_key(key_file, 0o600) current_uid = os.getuid() other_uid = current_uid + 1 real_path_stat = pathlib.Path.stat def fake_path_stat(self: pathlib.Path, **kwargs: object) -> os.stat_result: result = real_path_stat(self, **kwargs) if self.resolve() == key_file.resolve(): return os.stat_result(( result.st_mode, result.st_ino, result.st_dev, result.st_nlink, other_uid, result.st_gid, result.st_size, result.st_atime, result.st_mtime, result.st_ctime, )) return result with ( patch.object(pathlib.Path, "stat", fake_path_stat), caplog.at_level(logging.WARNING, logger="muse.core.identity"), ): result = _load_private_key_from_path(str(key_file)) assert result is None assert any( "owned by" in r.message.lower() or "uid" in r.message.lower() for r in caplog.records ) def test_root_skips_ownership_check( tmp_path: pathlib.Path, caplog: pytest.LogCaptureFixture, ) -> None: """uid==0 → ownership check skipped (but permissions still checked).""" import logging key_file = tmp_path / "key.pem" _write_key(key_file, 0o600) current_uid = os.getuid() other_uid = current_uid + 999 # clearly different owner real_path_stat = pathlib.Path.stat def fake_path_stat(self: pathlib.Path, **kwargs: object) -> os.stat_result: result = real_path_stat(self, **kwargs) if self.resolve() == key_file.resolve(): return os.stat_result(( result.st_mode, result.st_ino, result.st_dev, result.st_nlink, other_uid, result.st_gid, result.st_size, result.st_atime, result.st_mtime, result.st_ctime, )) return result with ( patch("os.getuid", return_value=0), patch.object(pathlib.Path, "stat", fake_path_stat), caplog.at_level(logging.WARNING, logger="muse.core.identity"), ): # Root + other owner + 0600 mode → should load successfully. result = _load_private_key_from_path(str(key_file)) assert result is not None, "Root should bypass ownership check" def test_warning_includes_chmod_command( tmp_path: pathlib.Path, caplog: pytest.LogCaptureFixture, ) -> None: """Warning message includes 'chmod 600' fix command.""" import logging key_file = tmp_path / "key.pem" _write_key(key_file, 0o644) with caplog.at_level(logging.WARNING, logger="muse.core.identity"): _load_private_key_from_path(str(key_file)) assert any("chmod 600" in r.message for r in caplog.records)