Nur noch, was zum Installieren und Betreiben von TESM noetig ist

Entfernt: Tests, Architekturdokumentation und die Serverhaelfte des
Lizenzprotokolls. Beides liegt im Entwicklungsrepository alientim/TESM-DEV.

Das Lizenzprotokoll ist geteilt. TESM braucht nur die gemeinsame Haelfte
(tesm-licensing): Lizenzen verifizieren, Status bewerten, Anfragen stellen,
Antworten pruefen. Ausstellen, erneuern, Antworten signieren und
Schluesselerzeugung liegen jetzt in tesm-licensing-server und damit
ausschliesslich beim Lizenzserver -- ein Client soll den Code zum Ausstellen
nicht einmal mitbringen. Nachgeprueft: kein Modul von TESM oder tesm-core
importiert eine der verschobenen Funktionen.

install.sh installiert entsprechend je Anwendung nur die noetigen Pakete und
bricht mit klarer Meldung ab, wenn die verlangte Anwendung nicht im Baum liegt.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-09-02 18:37:11 +02:00
co-authored by Claude Opus 5
parent 7354ff352b
commit 64e25bb9ae
18 changed files with 56 additions and 6000 deletions
@@ -95,7 +95,7 @@ def build_client_request(
return payload
def _check_freshness(timestamp: str, now: datetime, what: str) -> None:
def check_freshness(timestamp: str, now: datetime, what: str) -> None:
try:
stamp = parse_iso(timestamp)
except (ValueError, TypeError) as exc:
@@ -106,89 +106,8 @@ def _check_freshness(timestamp: str, now: datetime, what: str) -> None:
raise ReplayDetected(f"{what}: Zeitstempel ist zu alt")
def verify_client_request(
request: Mapping[str, Any],
license_public_key: str,
*,
expected_action: str | None = None,
now: datetime | None = None,
seen_nonce: Callable[[str], bool] | None = None,
) -> None:
"""Verifiziert eine Client-Anfrage serverseitig. Wirft bei Problemen.
``seen_nonce`` ist ein Callback, der ``True`` zurueckgibt, wenn die Nonce
bereits verbraucht ist. Der Aufrufer entscheidet ueber die Persistenz
(in dieser Codebasis: Tabelle ``license_nonces``).
"""
if not isinstance(request, Mapping):
raise ProtocolViolation("Anfrage ist kein Objekt")
if int(request.get("fmt") or 0) != FORMAT_VERSION:
raise ProtocolViolation("Nicht unterstuetzte Protokollversion")
action = str(request.get("action") or "")
if action not in ACTIONS:
raise ProtocolViolation("Unbekannte Aktion")
if expected_action and action != expected_action:
raise ProtocolViolation("Aktion passt nicht zum Endpunkt")
nonce = str(request.get("nonce") or "")
if not _NONCE_RE.match(nonce):
raise ProtocolViolation("Nonce hat ein unerwartetes Format")
fingerprint = str(request.get("fingerprint") or "")
if not _FINGERPRINT_RE.match(fingerprint):
raise ProtocolViolation("Fingerprint hat ein unerwartetes Format")
payload = {k: v for k, v in request.items() if k != "signature"}
if not verify(
payload, str(request.get("signature") or ""), license_public_key, SIG_CONTEXT_REQUEST
):
raise SignatureInvalid("Signatur der Anfrage ist ungueltig")
# Frische und Replay erst *nach* der Signaturpruefung: unsignierte Muellanfragen
# sollen keine Nonce-Eintraege in der Datenbank erzeugen koennen.
moment = now or utcnow()
_check_freshness(str(request.get("timestamp") or ""), moment, "Anfrage")
if seen_nonce is not None and seen_nonce(nonce):
raise ReplayDetected("Diese Anfrage wurde bereits verarbeitet")
report = request.get("report")
if report is not None:
if not isinstance(report, Mapping):
raise ProtocolViolation("Bericht ist kein Objekt")
total = sum(len(report.get(key) or ()) for key in ("customers", "tickets", "licenses"))
if total > MAX_REPORT_ROWS:
raise ProtocolViolation("Bericht ueberschreitet die Groessengrenze")
def build_master_response(
action: str,
*,
license_id: str,
fingerprint: str,
request_nonce: str,
master_private_key: str,
status: str = "ok",
detail: str = "",
license_update: Mapping[str, Any] | None = None,
now: datetime | None = None,
) -> dict[str, Any]:
"""Baut eine signierte Master-Antwort, gebunden an die Nonce der Anfrage."""
if status not in RESPONSE_STATES:
raise ProtocolViolation(f"Unbekannter Antwortstatus: {status!r}")
payload: dict[str, Any] = {
"fmt": FORMAT_VERSION,
"license_id": license_id,
"fingerprint": fingerprint,
"action": action,
"status": status,
"detail": detail,
"nonce": request_nonce,
"timestamp": iso(now or utcnow()),
}
if license_update is not None:
payload["license_update"] = dict(license_update)
payload["signature"] = sign(payload, master_private_key, SIG_CONTEXT_RESPONSE)
return payload
def verify_master_response(
@@ -230,7 +149,7 @@ def verify_master_response(
if str(response.get("status") or "") not in RESPONSE_STATES:
raise ProtocolViolation("Unbekannter Antwortstatus")
_check_freshness(str(response.get("timestamp") or ""), now or utcnow(), "Antwort")
check_freshness(str(response.get("timestamp") or ""), now or utcnow(), "Antwort")
update = response.get("license_update")
if update is not None: