gabriel / muse public
test_security_key_permissions.py python
164 lines 5.5 KB
Raw
sha256:660fcac1df3ab28f61862e961890bd2ca8b754fa0242079d93ca1e25037ec8a6 chore(tests): add docstring to tests/__init__.py so rc14 tr… Human 26 days ago
1 """Security tests — key file permission and ownership check before loading.
2
3 Verifies that _load_private_key_from_path() refuses keys with overly-permissive
4 file modes or foreign ownership, and logs the appropriate warning with the fix
5 command.
6 """
7
8 from __future__ import annotations
9
10 import os
11 import pathlib
12 import stat
13 from unittest.mock import patch
14
15 import pytest
16 from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey
17 from cryptography.hazmat.primitives.serialization import (
18 Encoding,
19 NoEncryption,
20 PrivateFormat,
21 )
22
23 from muse.core.identity import _load_private_key_from_path
24
25
26 # ---------------------------------------------------------------------------
27 # Helpers
28 # ---------------------------------------------------------------------------
29
30 def _generate_pem_key() -> bytes:
31 """Generate a fresh Ed25519 private key and return PEM bytes."""
32 private_key = Ed25519PrivateKey.generate()
33 return private_key.private_bytes(
34 encoding=Encoding.PEM,
35 format=PrivateFormat.PKCS8,
36 encryption_algorithm=NoEncryption(),
37 )
38
39
40 def _write_key(path: pathlib.Path, mode: int) -> None:
41 """Write a valid PEM key to *path* with the given file mode."""
42 path.write_bytes(_generate_pem_key())
43 os.chmod(path, mode)
44
45
46 # ---------------------------------------------------------------------------
47 # Tests
48 # ---------------------------------------------------------------------------
49
50
51 def test_key_0600_loads(tmp_path: pathlib.Path) -> None:
52 """0o600 key file → loads successfully and returns a key object."""
53 key_file = tmp_path / "key.pem"
54 _write_key(key_file, 0o600)
55 result = _load_private_key_from_path(str(key_file))
56 assert result is not None
57 assert isinstance(result, Ed25519PrivateKey)
58
59
60 def test_key_0644_refused(tmp_path: pathlib.Path, caplog: pytest.LogCaptureFixture) -> None:
61 """0o644 key file → returns None and logs a warning."""
62 import logging
63 key_file = tmp_path / "key.pem"
64 _write_key(key_file, 0o644)
65 with caplog.at_level(logging.WARNING, logger="muse.core.identity"):
66 result = _load_private_key_from_path(str(key_file))
67 assert result is None
68 assert any("0644" in r.message or "644" in r.message for r in caplog.records)
69
70
71 def test_key_0640_refused(tmp_path: pathlib.Path, caplog: pytest.LogCaptureFixture) -> None:
72 """0o640 key file → returns None and logs a warning."""
73 import logging
74 key_file = tmp_path / "key.pem"
75 _write_key(key_file, 0o640)
76 with caplog.at_level(logging.WARNING, logger="muse.core.identity"):
77 result = _load_private_key_from_path(str(key_file))
78 assert result is None
79 assert any("0640" in r.message or "640" in r.message for r in caplog.records)
80
81
82 def test_key_wrong_owner_refused(
83 tmp_path: pathlib.Path,
84 caplog: pytest.LogCaptureFixture,
85 ) -> None:
86 """Key owned by different UID → returns None."""
87 import logging
88 key_file = tmp_path / "key.pem"
89 _write_key(key_file, 0o600)
90
91 current_uid = os.getuid()
92 other_uid = current_uid + 1
93
94 real_path_stat = pathlib.Path.stat
95
96 def fake_path_stat(self: pathlib.Path, **kwargs: object) -> os.stat_result:
97 result = real_path_stat(self, **kwargs)
98 if self.resolve() == key_file.resolve():
99 return os.stat_result((
100 result.st_mode, result.st_ino, result.st_dev,
101 result.st_nlink, other_uid, result.st_gid,
102 result.st_size, result.st_atime, result.st_mtime, result.st_ctime,
103 ))
104 return result
105
106 with (
107 patch.object(pathlib.Path, "stat", fake_path_stat),
108 caplog.at_level(logging.WARNING, logger="muse.core.identity"),
109 ):
110 result = _load_private_key_from_path(str(key_file))
111
112 assert result is None
113 assert any(
114 "owned by" in r.message.lower() or "uid" in r.message.lower()
115 for r in caplog.records
116 )
117
118
119 def test_root_skips_ownership_check(
120 tmp_path: pathlib.Path,
121 caplog: pytest.LogCaptureFixture,
122 ) -> None:
123 """uid==0 → ownership check skipped (but permissions still checked)."""
124 import logging
125 key_file = tmp_path / "key.pem"
126 _write_key(key_file, 0o600)
127
128 current_uid = os.getuid()
129 other_uid = current_uid + 999 # clearly different owner
130
131 real_path_stat = pathlib.Path.stat
132
133 def fake_path_stat(self: pathlib.Path, **kwargs: object) -> os.stat_result:
134 result = real_path_stat(self, **kwargs)
135 if self.resolve() == key_file.resolve():
136 return os.stat_result((
137 result.st_mode, result.st_ino, result.st_dev,
138 result.st_nlink, other_uid, result.st_gid,
139 result.st_size, result.st_atime, result.st_mtime, result.st_ctime,
140 ))
141 return result
142
143 with (
144 patch("os.getuid", return_value=0),
145 patch.object(pathlib.Path, "stat", fake_path_stat),
146 caplog.at_level(logging.WARNING, logger="muse.core.identity"),
147 ):
148 # Root + other owner + 0600 mode → should load successfully.
149 result = _load_private_key_from_path(str(key_file))
150
151 assert result is not None, "Root should bypass ownership check"
152
153
154 def test_warning_includes_chmod_command(
155 tmp_path: pathlib.Path,
156 caplog: pytest.LogCaptureFixture,
157 ) -> None:
158 """Warning message includes 'chmod 600' fix command."""
159 import logging
160 key_file = tmp_path / "key.pem"
161 _write_key(key_file, 0o644)
162 with caplog.at_level(logging.WARNING, logger="muse.core.identity"):
163 _load_private_key_from_path(str(key_file))
164 assert any("chmod 600" in r.message for r in caplog.records)
File History 2 commits
sha256:660fcac1df3ab28f61862e961890bd2ca8b754fa0242079d93ca1e25037ec8a6 chore(tests): add docstring to tests/__init__.py so rc14 tr… Human 26 days ago
sha256:d8316ffae901be06347e16ab55be11868eb519dd16ade3e8aa16a99e662f7e62 baseline: rc14 re-baseline after rc3 store corruption recovery Human patch 26 days ago