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:
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user