diff --git a/srv/tesm/VERSION b/srv/tesm/VERSION index 0495c4a..e8ea05d 100644 --- a/srv/tesm/VERSION +++ b/srv/tesm/VERSION @@ -1 +1 @@ -1.2.3 +1.2.4 diff --git a/srv/tesm/licensing.py b/srv/tesm/licensing.py index 0c6cf46..a0c03e7 100644 --- a/srv/tesm/licensing.py +++ b/srv/tesm/licensing.py @@ -103,14 +103,30 @@ def _parse_iso(s: str) -> float: return datetime.datetime.strptime(s, "%Y-%m-%dT%H:%M:%SZ").replace(tzinfo=datetime.timezone.utc).timestamp() +# Sentinel-Ablaufdatum fuer "Lifetime"-Lizenzen. Bewusst ein fixes, weit in +# der Zukunft liegendes Datum statt eines Sonderwerts wie None/0 fuer +# expires_at -- dadurch bleibt JEDE bestehende Datumsvergleichs-/ +# Differenzrechnung (hier, im Master-Dashboard, UND im TESM-Client) ohne +# jede Sonderbehandlung korrekt: "in > 90 Tagen faellig" o.ae. ist fuer ein +# Datum im Jahr 2099 schlicht immer wahr. Ob eine Lizenz als "Lifetime" +# geflaggt ist, wird zusaetzlich explizit in der Master-DB gespeichert +# (licenses.lifetime) -- dieses Sentinel-Datum ist nur die tatsaechlich in +# die signierte Lizenzdatei eingebettete, fuer den Client sichtbare Reprae- +# sentation davon. +LIFETIME_EXPIRES_AT = "2099-12-31T00:00:00Z" + + # ================================================================ Lizenz == def issue_license(*, customer, license_type, modules, valid_days, master_private_key_b64, master_public_key_b64, - master_endpoint, vendor, license_id=None, now=None): + master_endpoint, vendor, license_id=None, now=None, lifetime=False): """Vom MASTER aufgerufen: erzeugt eine neue, signierte Lizenz samt frischem Pro-Lizenz-Schlüsselpaar. + lifetime=True ignoriert valid_days komplett und setzt stattdessen das + feste Sentinel-Datum LIFETIME_EXPIRES_AT -- siehe dortigen Kommentar. + Rückgabe: (license_file, license_pubkey) -- license_file ist die komplette, an den Kunden auszuhändigende Datei (inkl. dem PRIVATEN Lizenzschlüssel); license_pubkey ist NUR für die Master-Datenbank @@ -130,7 +146,7 @@ def issue_license(*, customer, license_type, modules, valid_days, "type": license_type, "modules": sorted(modules), "issued_at": _iso(now), - "expires_at": _iso(now + valid_days * 86400), + "expires_at": LIFETIME_EXPIRES_AT if lifetime else _iso(now + valid_days * 86400), "license_pubkey": license_pub, "master_pubkey": master_public_key_b64, "master_endpoint": master_endpoint, @@ -225,6 +241,13 @@ def build_client_request(action, license_file, nonce=None) -> dict: payload = { "license_id": license_file["license_id"], "fingerprint": system_fingerprint(), + # Rein informativ fuer die Dashboard-Kacheln des Masters (siehe + # dortiges index()) -- fliesst NICHT in irgendeine Sicherheits- + # entscheidung ein (die bleibt allein Sache des Fingerprints), wird + # aber wie alle anderen Felder mitsigniert, damit ein Angreifer + # ohne den privaten Lizenzschluessel keinen falschen Hostnamen + # unterschieben kann. + "hostname": socket.gethostname(), "action": action, "nonce": nonce or uuid.uuid4().hex, "timestamp": _iso(time.time()),