Files
tesm/packages/tesm-licensing/src/tesm_licensing/fingerprint.py
T
alientimandClaude Opus 5 7354ff352b TESM 2.0.0 -- Neubau
Vollstaendiger Neubau der Anwendung. Der vorherige Stand bleibt unveraendert
im Zweig SONNET5 erhalten.

Aufbau: apps/tesm (Anwendung), packages/tesm-core (gemeinsamer Kern),
packages/tesm-licensing (Lizenzprotokoll), deploy (Installation, systemd,
privilegierter Helfer), docs, tests. Der Lizenzserver liegt in seinem eigenen
Repository; beide Repositorien bringen die gemeinsamen Pakete mit, damit sich
jedes allein installieren laesst.

Die wichtigsten Unterschiede zum Vorgaenger, jeweils an der Stelle im Code
kommentiert, an der der Fehler entstanden ist:

* 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.
* Die nginx-Site wird bei jedem Lauf inhaltlich verglichen und erneuert.
* Jede erzeugte Konfiguration wird vor dem Uebernehmen geprueft (nginx, Kea).
* Kein Hostname im Lizenz-Fingerabdruck.
* Zwei Installationen auf einem Host stoeren sich nicht (eigener SITE_KEY).
* Verschachtelte Datenbankverbindungen sind ein Fehler, kein Deadlock.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-02 18:00:06 +02:00

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()