gabriel / musehub public

test_security_server_audit.py file-level

at sha256:3 · View file ↗ · Intel ↗

History
1 files
1 commits
0 hotspots
0 🧊 dead
0 💥 blast risk
sha256:0 fix: fall back to any indexed mpack in read_object_bytes when push mpac… · gabriel · Jun 17, 2026
1 """Phase 7 — MuseHub server security audit.
2
3 Invariants
4 ----------
5 The MuseHub server must NEVER touch Ed25519 private key material.
6 All authentication is challenge-response: the server holds only public keys
7 and verifies signatures produced by the client.
8
9 P7-1 crypto/keys.py exports no private key loading or generation functions.
10 P7-2 musehub_auth_models.MusehubAuthKey stores only public_key_b64 and
11 fingerprint — no private_key, key_path, or mnemonic columns.
12 P7-3 request_signing._verify_msign reads public keys from DB; it never calls
13 any private-key function.
14 P7-4 No source file under musehub/ (excluding deploy/, docs/, tests/) contains
15 the string "private_key" in a live code path — only in comments/docstrings.
16 P7-5 musehub/mcp/prompts.py contains no stale reference to ~/.muse/keys/*.pem.
17 P7-6 musehub/auth/request_signing.py does not import any PEM-reading symbol.
18 """
19
20 from __future__ import annotations
21
22 import ast
23 import pathlib
24
25
26 MUSEHUB_ROOT = pathlib.Path(__file__).parent.parent / "musehub"
27
28
29 # ---------------------------------------------------------------------------
30 # P7-1 — crypto/keys.py exports no private-key functions
31 # ---------------------------------------------------------------------------
32
33
34 class TestCryptoKeysPublicOnly:
35 def test_P7_1_no_private_key_generation(self) -> None:
36 """crypto/keys.py must not export any private-key generation or loading."""
37 keys_src = (MUSEHUB_ROOT / "crypto" / "keys.py").read_text()
38 forbidden = [
39 "generate_private_key",
40 "Ed25519PrivateKey",
41 "from_private_bytes",
42 "load_pem_private_key",
43 "load_der_private_key",
44 ]
45 for symbol in forbidden:
46 assert symbol not in keys_src, (
47 f"crypto/keys.py must not reference '{symbol}' — "
48 "server never handles private key material"
49 )
50
51 def test_P7_1b_verify_signature_uses_public_key_only(self) -> None:
52 """verify_signature must accept public_key_bytes, not private_key_bytes."""
53 from musehub.crypto.keys import verify_signature
54 import inspect
55 sig = inspect.signature(verify_signature)
56 param_names = set(sig.parameters)
57 assert "public_key_bytes" in param_names
58 assert "private_key_bytes" not in param_names
59 assert "private_key" not in param_names
60
61
62 # ---------------------------------------------------------------------------
63 # P7-2 — DB model stores only public key data
64 # ---------------------------------------------------------------------------
65
66
67 class TestAuthKeyModelPublicOnly:
68 def test_P7_2_no_private_key_columns(self) -> None:
69 """MusehubAuthKey must have no private-key, key_path, or mnemonic columns."""
70 from musehub.db.musehub_auth_models import MusehubAuthKey
71 from sqlalchemy import inspect as sa_inspect
72
73 mapper = sa_inspect(MusehubAuthKey)
74 column_names = {col.key for col in mapper.mapper.columns}
75
76 forbidden_columns = {"private_key", "key_path", "mnemonic", "secret", "pem"}
77 overlap = forbidden_columns & column_names
78 assert not overlap, (
79 f"MusehubAuthKey must not have private-key columns; found: {overlap}"
80 )
81
82 def test_P7_2b_public_key_b64_column_exists(self) -> None:
83 """MusehubAuthKey must have public_key_b64 and fingerprint columns."""
84 from musehub.db.musehub_auth_models import MusehubAuthKey
85 from sqlalchemy import inspect as sa_inspect
86
87 mapper = sa_inspect(MusehubAuthKey)
88 column_names = {col.key for col in mapper.mapper.columns}
89
90 assert "public_key_b64" in column_names
91 assert "fingerprint" in column_names
92
93
94 # ---------------------------------------------------------------------------
95 # P7-3 — request_signing imports no private-key symbols
96 # ---------------------------------------------------------------------------
97
98
99 class TestRequestSigningImports:
100 def test_P7_3_no_private_key_imports(self) -> None:
101 """request_signing.py must not import any private-key symbol."""
102 src = (MUSEHUB_ROOT / "auth" / "request_signing.py").read_text()
103 forbidden_imports = [
104 "Ed25519PrivateKey",
105 "load_pem_private_key",
106 "load_der_private_key",
107 "generate_private_key",
108 "load_private_key",
109 ]
110 for symbol in forbidden_imports:
111 assert symbol not in src, (
112 f"request_signing.py must not import '{symbol}' — "
113 "server only verifies signatures, never signs"
114 )
115
116 def test_P7_6_no_pem_reader_imports(self) -> None:
117 """request_signing.py must not import any PEM-reading function."""
118 src = (MUSEHUB_ROOT / "auth" / "request_signing.py").read_text()
119 assert ".pem" not in src
120 assert "load_pem" not in src
121
122
123 # ---------------------------------------------------------------------------
124 # P7-4 — No live private-key code paths in musehub/ application source
125 # ---------------------------------------------------------------------------
126
127
128 class TestNoPrivateKeyInAppCode:
129 _SKIP_DIRS = {"deploy", "tests", "templates"}
130 _SKIP_EXTS = {".md", ".html", ".sh", ".conf", ".txt", ".toml", ".json"}
131
132 def _app_py_files(self) -> list[pathlib.Path]:
133 files = []
134 for p in MUSEHUB_ROOT.rglob("*.py"):
135 if any(part in self._SKIP_DIRS for part in p.parts):
136 continue
137 files.append(p)
138 return files
139
140 def test_P7_4_no_ed25519_private_key_in_app_code(self) -> None:
141 """No application .py file may instantiate or import Ed25519PrivateKey."""
142 violations: list[str] = []
143 for path in self._app_py_files():
144 src = path.read_text()
145 tree = ast.parse(src, filename=str(path))
146 for node in ast.walk(tree):
147 # Check string literals and names — not comments or docstrings
148 if isinstance(node, ast.Name) and node.id == "Ed25519PrivateKey":
149 violations.append(f"{path.relative_to(MUSEHUB_ROOT.parent)}:{node.lineno}")
150 elif isinstance(node, ast.Attribute) and node.attr == "Ed25519PrivateKey":
151 violations.append(f"{path.relative_to(MUSEHUB_ROOT.parent)}:{node.lineno}")
152 assert not violations, (
153 f"Ed25519PrivateKey referenced in musehub application code:\n{'\n'.join(violations)}"
154 )
155
156 def test_P7_4b_no_load_private_key_calls(self) -> None:
157 """No application .py file may call load_private_key or load_pem_private_key."""
158 violations: list[str] = []
159 for path in self._app_py_files():
160 src = path.read_text()
161 for symbol in ("load_private_key", "load_pem_private_key", "load_der_private_key"):
162 if symbol in src:
163 violations.append(f"{path.relative_to(MUSEHUB_ROOT.parent)}: {symbol}")
164 assert not violations, (
165 f"Private key loading found in musehub application code:\n{'\n'.join(violations)}"
166 )
167
168
169 # ---------------------------------------------------------------------------
170 # P7-5 — No stale ~/.muse/keys/*.pem reference in MCP prompts
171 # ---------------------------------------------------------------------------
172
173
174 class TestNoStalePemReferences:
175 def test_P7_5_mcp_prompts_no_pem_path(self) -> None:
176 """musehub/mcp/prompts.py must not reference ~/.muse/keys/*.pem (stale architecture)."""
177 src = (MUSEHUB_ROOT / "mcp" / "prompts.py").read_text()
178 assert "~/.muse/keys/" not in src, (
179 "musehub/mcp/prompts.py contains a stale reference to ~/.muse/keys/ — "
180 "keys are now derived from the OS-keychain mnemonic, no PEM files"
181 )
182
183 def test_P7_5b_identity_template_no_key_path_column(self) -> None:
184 """The identity docs template must not describe key_path as a valid field."""
185 template = (
186 MUSEHUB_ROOT.parent / "musehub" / "templates" / "musehub" / "pages"
187 / "docs_muse_identity.html"
188 )
189 if not template.exists():
190 return # template may not exist in all environments
191 src = template.read_text()
192 assert "key_path" not in src, (
193 "docs_muse_identity.html still describes key_path — "
194 "this field was removed in Phase 3 of the key-material security migration"
195 )
196 assert "/.muse/keys/" not in src, (
197 "docs_muse_identity.html still references ~/.muse/keys/ PEM paths"
198 )