96 lines
3.4 KiB
Python
96 lines
3.4 KiB
Python
"""rustdesk-server — identity-key round-trip across three independent surfaces (§4.3-style).
|
|
|
|
hbbs mints an ed25519 identity keypair on first boot into the shared rustdesk_data volume;
|
|
the PUBLIC key is what clients configure to pin the server. This is the recipe's defining
|
|
generated state, and we read it back through three genuinely different paths:
|
|
|
|
1. HTTPS through Traefik + the cc-ci status sidecar: GET /id_ed25519.pub (the serving path
|
|
a remote client could observe),
|
|
2. hbbs's OWN startup telemetry: the `Key: <b64>` log line (docker service logs),
|
|
3. the volume file itself, read inside the status sidecar (cat /pub/id_ed25519.pub).
|
|
|
|
All three must AGREE and be a valid base64-encoded 32-byte ed25519 public key. A wedged or
|
|
crash-looping hbbs fails (1) with 404 forever (no key file); a key mismatch across surfaces
|
|
would mean we are reading different deployments' state — either way this test names the
|
|
broken layer. Non-vacuous: every surface carries app-generated state, nothing is echoed from
|
|
config.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import base64
|
|
import os
|
|
import re
|
|
import subprocess
|
|
import sys
|
|
|
|
import pytest
|
|
|
|
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "..", "runner"))
|
|
from harness import lifecycle # noqa: E402
|
|
|
|
_KEY_RE = re.compile(rb"Key: ([A-Za-z0-9+/]{43}=)")
|
|
|
|
|
|
def _stack(domain: str) -> str:
|
|
return domain.replace(".", "_")
|
|
|
|
|
|
def _https_key(domain: str) -> tuple[int, str]:
|
|
return lifecycle.http_fetch(domain, "/id_ed25519.pub")
|
|
|
|
|
|
def _log_key(domain: str) -> bytes | None:
|
|
out = subprocess.run(
|
|
["docker", "service", "logs", "--tail", "200", f"{_stack(domain)}_app"],
|
|
capture_output=True,
|
|
text=True,
|
|
timeout=60,
|
|
).stdout
|
|
m = _KEY_RE.search(out.encode())
|
|
return m.group(1) if m else None
|
|
|
|
|
|
def _volume_key(domain: str) -> str:
|
|
return lifecycle.exec_in_app(domain, ["cat", "/pub/id_ed25519.pub"], service="status").strip()
|
|
|
|
|
|
def test_identity_key_agrees_across_https_log_and_volume(live_app):
|
|
https_status, https_body = _https_key(live_app)
|
|
assert https_status == 200, (
|
|
f"status route /id_ed25519.pub not serving (HTTP {https_status}) — hbbs never minted "
|
|
"its key, or the status sidecar/routing is broken"
|
|
)
|
|
https_key = https_body.strip().encode()
|
|
|
|
log_key = _log_key(live_app)
|
|
assert log_key is not None, "hbbs startup log carries no 'Key: <b64>' line (telemetry)"
|
|
|
|
vol_key = _volume_key(live_app).encode()
|
|
|
|
assert https_key == log_key == vol_key, (
|
|
"identity key disagrees across surfaces — "
|
|
f"https={https_key[:20]!r} log={log_key[:20]!r} volume={vol_key[:20]!r} "
|
|
"(reading different deployments' state?)"
|
|
)
|
|
|
|
raw = base64.b64decode(https_key, validate=True)
|
|
assert len(raw) == 32, f"not a 32-byte ed25519 public key ({len(raw)} bytes)"
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"path",
|
|
[
|
|
"/",
|
|
"/id_ed25519", # the PRIVATE key lives in the same volume — must never be served
|
|
"/db_v2.sqlite3",
|
|
"/db_v2.sqlite3-wal",
|
|
],
|
|
)
|
|
def test_status_route_serves_only_the_public_key(live_app, path):
|
|
"""Anything but the exact .pub location 404s (default surface = deny). The private key and
|
|
the sqlite peer DB share the status sidecar's volume — a 200 on any of these paths would
|
|
be a real exposure."""
|
|
status, _ = lifecycle.http_fetch(live_app, path)
|
|
assert status == 404, f"status route served {path} (HTTP {status}) — exposure, not health"
|