Files
PolyTraderSharp/Licensing/LicenseGate.cs
T
RichardandClaude Opus 5 ca750a06f5 Watchdog-Heartbeat + LicenseLabrador-Lizenzpruefung eingebunden
Slices W-A/W-B/W-C und L-A aus UMSETZUNGSPLAN-Watchdog-LicenseLabrador-Integration.

Watchdog (Dead-Man's-Switch, externer Server):
- WatchdogHeartbeatService als BackgroundService + DI-Singleton; jeder Sendeversuch
  gekapselt, ein Ausfall des Watchdogs beeintraechtigt PolyTrader nie.
- Eigene Implementierung statt Test-Client des Fremdprojekts: TLS-Pruefung bleibt
  aktiv, http:// nur fuer localhost (Agent-Token nicht im Klartext ins Netz).
- Status aus dem App-Log abgeleitet (Error mit 5-Minuten-Sticky-Fenster, entprellt),
  Lifecycle-Events started/stopping.
- Konfiguration in ServerSettings; Agent-Token [Browsable(false)] mit maskierter
  Eingabe + Statusanzeige, bei gesetztem Master-Key verschluesselt (enc:v1:).

Lizenz (LicenseLabrador, Ed25519):
- LicenseGate.RunStartupGate prueft beim Start; bei ungueltiger Lizenz wird die
  Modulliste leer gebaut, sodass nur die Core-Shell (Terminal/Einstellungen)
  startet. Bewusst kein Environment.Exit - ein Trading-Bot darf nicht mitten im
  Lauf hart sterben. TamperSuspected gilt als nicht nutzbar.
- LicenseDialog (partial + .Designer.cs) fuer Start- und Verwalten-Modus, mit
  Hardware-ID zum Kopieren; Smoke-UI konstruiert beide Modi headless.
- Master-Key wird jetzt VOR dem Host-Build geladen, da auch der Lizenzschluessel
  entschluesselt werden muss; derselbe TerminalLogger wird als Singleton
  weitergereicht, damit die Startmeldungen im Terminal-Fenster erscheinen.

Der Lizenz-SDK-Client wird per Cross-Repo-Projektreferenz auf
..\..\LicenseLabrador eingebunden, damit SDK-Fixes ohne Kopie einfliessen.

484 Zeilen Tests fuer den Heartbeat; Suite gruen (438 Tests).

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

177 lines
7.8 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using System;
using LicenseLabrador.Client;
using PolyTrader.Core.Security;
using PolyTraderSharp.Models;
using PolyTraderSharp.Services;
namespace PolyTraderSharp.Licensing
{
/// <summary>
/// Lizenz-Torwächter für den Programmstart (B.4). Prüft beim Start die LicenseLabrador-Lizenz und
/// entscheidet, ob PolyTrader voll (mit allen Modulen) oder eingeschränkt (nur Core-Shell, damit
/// Terminal und Einstellungen zum Eintragen einer Lizenz erreichbar bleiben) startet.
///
/// **Grundsatz:** kein harter Abbruch (<c>Environment.Exit</c>) bei ungültiger Lizenz. Ein
/// Trading-Bot mit offenen Positionen darf nicht mitten im Lauf hart abgeschossen werden im
/// Zweifel startet die Shell ohne die Trading-/Analyse-Module weiter. Der Gate wirft nie; jeder
/// Fehler in der Prüfung führt zum eingeschränkten Modus, nicht zum Absturz.
///
/// Die eigentliche Kryptografie (Ed25519-Signaturprüfung, Nonce, Offline-Kulanz, Hardware-Bindung)
/// liegt im <see cref="LicenseClient"/> des SDK; dieser Gate ist nur die Anwendungslogik drumherum.
/// </summary>
public static class LicenseGate
{
/// <summary>
/// Öffentlicher Ed25519-Schlüssel des Lizenzservers (license.mhdf.de), Base64. Bewusst fest
/// eingebettet: nur mit diesem Schlüssel lässt sich eine Server-Antwort verifizieren ein
/// Angreifer kann ohne den (nur serverseitigen) privaten Schlüssel kein „valid" fälschen.
/// </summary>
private const string PublicKeyBase64 = "L7YR1wMKk8+lNefatzL+DMvAtHFVkZWYXAxXGrro+/U=";
/// <summary>Produkt-Slug im LicenseLabrador-Admin.</summary>
private const string ProductSlug = "pt";
/// <summary>
/// Lizenzserver-Endpunkt. Ausschließlich HTTPS über http:// gingen Lizenzschlüssel und
/// Hardware-ID im Klartext übers Netz (die signierte Antwort bliebe zwar fälschungssicher,
/// der Schlüssel würde aber geleakt).
/// </summary>
private static readonly string[] Endpoints = { "https://license.mhdf.de" };
/// <summary>Offline-Kulanz, falls der Server das Feld nicht liefert (7 Tage).</summary>
private const int OfflineGraceHoursFallback = 168;
public static LicenseConfig BuildConfig() => new LicenseConfig
{
ProductSlug = ProductSlug,
PublicKeyBase64 = PublicKeyBase64,
Endpoints = Endpoints,
OfflineGraceHoursFallback = OfflineGraceHoursFallback
};
/// <summary>
/// Führt die Startprüfung durch. Rückgabe <c>true</c> = App darf voll (mit Modulen) starten.
/// Zeigt bei ungültiger Lizenz einen modalen Dialog (Schlüssel eingeben, Hardware-ID anzeigen,
/// eingeschränkt starten). Wirft nie.
/// </summary>
public static bool RunStartupGate(ServerSettings settings, TerminalLogger logger, string settingsPath)
{
LicenseClient client;
try
{
client = new LicenseClient(BuildConfig());
}
catch (Exception ex)
{
logger.Error($"❌ Lizenzprüfung nicht initialisierbar: {ex.Message}. Start im eingeschränkten Modus.");
return false;
}
string storedKey = "";
try
{
// Erlaubt einen mit dem Master-Key verschlüsselten Schlüssel (enc:v1:…); Klartext
// wird unverändert durchgereicht.
storedKey = SecretProtection.Unprotect(settings.LicenseKey);
}
catch (Exception ex)
{
logger.Warning($"⚠️ Gespeicherter Lizenzschlüssel nicht lesbar ({ex.Message}) behandle als 'keine Lizenz'.");
}
LicenseResult result = EvaluateBlocking(client, storedKey);
if (result.IsUsable)
{
LogUsable(result, logger);
return true;
}
logger.Warning($"⚠️ Keine gültige Lizenz: {Describe(result)}. Öffne Lizenzdialog.");
LicenseResult? dialogResult;
string? dialogKey;
try
{
using var dlg = new PolyTraderSharp.Ui.LicenseDialog(client, storedKey, result);
dlg.ShowDialog();
dialogResult = dlg.ValidatedResult;
dialogKey = dlg.ValidatedKey;
}
catch (Exception ex)
{
logger.Error($"❌ Lizenzdialog fehlgeschlagen: {ex.Message}. Start im eingeschränkten Modus.");
return false;
}
if (dialogResult != null && dialogResult.IsUsable && !string.IsNullOrWhiteSpace(dialogKey))
{
try
{
// Bei gesetztem Master-Key verschlüsselt speichern; sonst Klartext (Passthrough).
settings.LicenseKey = SecretProtection.Protect(dialogKey!.Trim());
settings.Save(settingsPath);
logger.Info("🔑 Lizenz validiert und gespeichert.");
}
catch (Exception ex)
{
logger.Error($"Lizenz gültig, aber Speichern des Schlüssels fehlgeschlagen: {ex.Message}.");
}
LogUsable(dialogResult, logger);
return true;
}
logger.Warning(
"⚠️ Start im EINGESCHRÄNKTEN Modus keine Module aktiv, nur Terminal und Einstellungen. " +
"Lizenz in den Einstellungen setzen und PolyTrader neu starten.");
return false;
}
/// <summary>
/// Validiert synchron (blockierend). Das SDK arbeitet durchgängig mit ConfigureAwait(false),
/// daher ist das Blockieren vor der Message-Loop unbedenklich. Ein leerer Schlüssel ergibt
/// planmäßig <see cref="LicenseState.NoLicense"/>.
/// </summary>
internal static LicenseResult EvaluateBlocking(LicenseClient client, string? key)
{
return client.ValidateAsync(key ?? string.Empty).GetAwaiter().GetResult();
}
private static void LogUsable(LicenseResult result, TerminalLogger logger)
{
switch (result.State)
{
case LicenseState.Valid:
logger.Info("🔑 Lizenz gültig (online geprüft).");
break;
case LicenseState.ValidOffline:
logger.Warning($"🔑 Lizenz im Offline-Kulanzmodus gültig bis {result.GraceUntil:g} " +
"(Server nicht erreichbar).");
break;
case LicenseState.ValidLocalFile:
logger.Info("🔑 Lizenz über lokale Offline-Lizenzdatei gültig.");
break;
default:
logger.Info($"🔑 Lizenz nutzbar ({result.State}).");
break;
}
}
/// <summary>Kurzbeschreibung eines nicht nutzbaren Ergebnisses für das Log/den Dialog.</summary>
internal static string Describe(LicenseResult result)
{
return result.State switch
{
LicenseState.NoLicense => "kein oder ungültiger Schlüssel / Server nicht erreichbar",
LicenseState.NotFound => "Schlüssel dem Server unbekannt",
LicenseState.Revoked => "Lizenz wurde widerrufen",
LicenseState.Expired => "Lizenz abgelaufen",
LicenseState.ActivationLimit => "maximale Aktivierungen erreicht",
LicenseState.TamperSuspected => "Manipulation erkannt (Signatur/Uhr) Prüfung verweigert",
_ => result.State.ToString()
};
}
}
}