Skip to content

Commit d8ecfe2

Browse files
committed
test(signing): cover key-pinning signature requirement on re-registration
Three direct Hub.register_publisher cases: unsigned manifest echoing the public key is rejected, a manifest signed under a different key is rejected, and the genuine owner re-signing keeps the same api_key.
1 parent 037785d commit d8ecfe2

1 file changed

Lines changed: 81 additions & 0 deletions

File tree

tests/test_signing.py

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -192,3 +192,84 @@ async def test_unsigned_publish_still_works(hub_port):
192192
break
193193
await asyncio.sleep(0.1)
194194
assert pub.api_key, "unsigned manifest should still register (backwards compat)"
195+
196+
197+
# --- key-pinning unit tests (direct Hub.register_publisher) -----------------
198+
# The public_key is served openly at /<name>/manifest.json, so pinning must
199+
# require a *valid signature* on re-registration, not just a matching
200+
# public_key. These drive the registration logic directly without a live hub.
201+
202+
def _new_storage():
203+
import os
204+
import tempfile
205+
from zhub.persistence import Storage
206+
fd, db = tempfile.mkstemp(suffix=".db")
207+
os.close(fd)
208+
return Storage(db)
209+
210+
211+
@pytest.mark.asyncio
212+
async def test_pinning_rejects_unsigned_reregister_with_copied_public_key():
213+
"""Stolen api_key + the (public) public_key, but no signature, must NOT
214+
take over a signed registration."""
215+
if not SERVER_AVAILABLE:
216+
pytest.skip("fastapi/uvicorn not installed")
217+
from zhub.server import Hub
218+
hub = Hub(storage=_new_storage())
219+
sk, pk = generate_keypair()
220+
signed = sign_manifest(
221+
{"name": "victim", "description": "real", "schema_version": "0.1", "capabilities": []}, sk
222+
)
223+
_, api_key = await hub.register_publisher("victim", signed, websocket=None)
224+
225+
forged = {"name": "victim", "description": "PWNED", "schema_version": "0.1",
226+
"capabilities": [], "public_key": pk} # copied public key, no signature
227+
with pytest.raises(PermissionError, match="must be signed"):
228+
await hub.register_publisher("victim", forged, websocket=None, desired_api_key=api_key)
229+
assert hub.publishers["victim"].manifest["description"] == "real"
230+
231+
232+
@pytest.mark.asyncio
233+
async def test_pinning_rejects_reregister_signed_with_different_key():
234+
"""A validly-signed manifest under a *different* keypair must not take over
235+
a registration pinned to the original key."""
236+
if not SERVER_AVAILABLE:
237+
pytest.skip("fastapi/uvicorn not installed")
238+
from zhub.server import Hub
239+
hub = Hub(storage=_new_storage())
240+
sk, _pk = generate_keypair()
241+
signed = sign_manifest(
242+
{"name": "victim", "description": "real", "schema_version": "0.1", "capabilities": []}, sk
243+
)
244+
_, api_key = await hub.register_publisher("victim", signed, websocket=None)
245+
246+
attacker_sk, _ = generate_keypair()
247+
other = sign_manifest(
248+
{"name": "victim", "description": "PWNED", "schema_version": "0.1", "capabilities": []},
249+
attacker_sk,
250+
)
251+
with pytest.raises(PermissionError, match="public_key does not match"):
252+
await hub.register_publisher("victim", other, websocket=None, desired_api_key=api_key)
253+
assert hub.publishers["victim"].manifest["description"] == "real"
254+
255+
256+
@pytest.mark.asyncio
257+
async def test_pinning_allows_legit_resigned_reregister():
258+
"""The genuine owner re-registers with their own freshly-signed manifest
259+
(as the publish client does on every reconnect) and keeps the same key."""
260+
if not SERVER_AVAILABLE:
261+
pytest.skip("fastapi/uvicorn not installed")
262+
from zhub.server import Hub
263+
hub = Hub(storage=_new_storage())
264+
sk, _pk = generate_keypair()
265+
signed = sign_manifest(
266+
{"name": "victim", "description": "v1", "schema_version": "0.1", "capabilities": []}, sk
267+
)
268+
_, api_key = await hub.register_publisher("victim", signed, websocket=None)
269+
270+
resigned = sign_manifest(
271+
{"name": "victim", "description": "v2", "schema_version": "0.1", "capabilities": []}, sk
272+
)
273+
_, key2 = await hub.register_publisher("victim", resigned, websocket=None, desired_api_key=api_key)
274+
assert key2 == api_key
275+
assert hub.publishers["victim"].manifest["description"] == "v2"

0 commit comments

Comments
 (0)