Vollstaendiger Neubau der Anwendung. Der vorherige Stand bleibt unveraendert im Zweig SONNET5 erhalten. Aufbau: apps/tesm-license (Anwendung), packages/tesm-core (gemeinsamer Kern), packages/tesm-licensing (Lizenzprotokoll), deploy (Installation, systemd, privilegierter Helfer), docs, tests. Die verwaltete Anwendung liegt in ihrem eigenen Repository; beide Repositorien bringen die gemeinsamen Pakete mit, damit sich jedes allein installieren laesst. Die wichtigsten Unterschiede zum Vorgaenger: * Keine doppelte licensing.py -- ein Paket, das beide Anwendungen installieren, statt zweier Dateien, die byte-identisch bleiben sollen. * Der Webprozess laeuft unprivilegiert; alles, was Root braucht, geht ueber einen einzigen Helfer mit Positivlisten fuer jedes Argument. * CSRF-Schutz ueberhaupt -- der Vorgaenger hatte keinen. * Rechte werden serverseitig geprueft, nicht nur im Template ausgeblendet. * Keine Lizenz ohne master_endpoint: eine Ausstellung ohne Endpunkt wird abgelehnt statt eine Lizenz zu erzeugen, die sich nie aktivieren kann. * Offline-Aktivierung in beide Richtungen; die Lizenz bleibt als "Aktivierung offen" markiert, bis sie zurueckkommt. * Getrennte Signaturkontexte je Nachrichtenart, Nonce gegen Wiedereinspielung, seq gegen das Zurueckrollen auf eine aeltere Lizenz. * Kein Hostname im Maschinen-Fingerabdruck. * Verschachtelte Datenbankverbindungen sind ein Fehler, kein Deadlock. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
55 lines
1.9 KiB
Python
55 lines
1.9 KiB
Python
"""Maschinen-Fingerprint der lizenzierten Instanz.
|
|
|
|
Jede Bindungs-/Autorisierungsentscheidung des Protokolls laeuft ueber diesen
|
|
Wert -- nie ueber ``hostname`` oder ``license_id`` allein.
|
|
|
|
Aenderung gegenueber dem Vorgaenger (bewusst): der Hostname ist **nicht** mehr
|
|
Teil des Hashes. Im Vorgaengerprojekt war ``sha256(machine_id + ":" + hostname)``
|
|
im Einsatz, wodurch eine reine Umbenennung des Hosts den Fingerprint aenderte
|
|
und die Lizenz "already_bound_elsewhere" lief -- ein Support-Fall ohne
|
|
Sicherheitsgewinn. Jetzt zaehlt ausschliesslich die stabile Maschinenidentitaet
|
|
(``/etc/machine-id``, ergaenzt um die DMI-Produkt-UUID, falls lesbar).
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import hashlib
|
|
import socket
|
|
from pathlib import Path
|
|
|
|
_MACHINE_ID_PATHS = (Path("/etc/machine-id"), Path("/var/lib/dbus/machine-id"))
|
|
_DMI_UUID_PATH = Path("/sys/class/dmi/id/product_uuid")
|
|
|
|
FINGERPRINT_CONTEXT = b"tesm/machine-fingerprint/v2\x00"
|
|
|
|
|
|
def _read_first(paths: tuple[Path, ...]) -> str:
|
|
for path in paths:
|
|
try:
|
|
value = path.read_text(encoding="utf-8", errors="strict").strip()
|
|
except OSError:
|
|
continue
|
|
if value:
|
|
return value
|
|
return ""
|
|
|
|
|
|
def machine_identity() -> str:
|
|
"""Rohe, ungehashte Maschinenidentitaet -- nur fuer Diagnose/Tests."""
|
|
machine_id = _read_first(_MACHINE_ID_PATHS)
|
|
dmi_uuid = ""
|
|
try:
|
|
dmi_uuid = _DMI_UUID_PATH.read_text(encoding="utf-8").strip()
|
|
except OSError:
|
|
pass
|
|
if machine_id or dmi_uuid:
|
|
return f"{machine_id}|{dmi_uuid}"
|
|
# Letzter Ausweg (Windows/Container ohne machine-id): Hostname. Produktivziel
|
|
# ist Linux, dort greift dieser Zweig nicht.
|
|
return f"hostname:{socket.gethostname()}"
|
|
|
|
|
|
def system_fingerprint() -> str:
|
|
"""SHA-256-Hex-Digest der Maschinenidentitaet, domaenengetrennt gehasht."""
|
|
return hashlib.sha256(FINGERPRINT_CONTEXT + machine_identity().encode("utf-8")).hexdigest()
|