Files
tesm-license/packages/tesm-licensing/src/tesm_licensing/fingerprint.py
T
alientimandClaude Opus 5 f7805a2180 TESM-Lizenzserver 2.0.0 -- Neubau
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>
2026-09-02 18:01:23 +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()