# Deploymentcenter-Anbindung — Durchsicht > **Nachtrag 2026-08-08 — die Anbindung ist umgestellt, Server und SDK stehen auf 2.1.** > Abschnitt 4 und 5 sind abgearbeitet; wie es jetzt aussieht, steht in > [Deploymentcenter-Integration](Deploymentcenter-Integration.md). > > Mit **SDK 2.1 erledigt** (waren Befunde aus Abschnitt 3 bzw. aus der Durchsicht der > 2.0-Anbindung): > > - `HttpClient` ohne Zeitgrenze → intern 15 s. Unsere Umgehung (eigener Client mit > 8 s) ist zurückgebaut. > - HTTP 429/5xx entzogen die Lizenz, ohne den Zwischenspeicher zu befragen → jeder > Nicht-Erfolg führt jetzt in denselben Offline-Zweig, `IsTransient` macht den > Unterschied sichtbar. Unsere Behelfsprüfung auf `unknown_error` ist entfernt. > - `cache_ttl_hours` wurde ignoriert, die Gnadenfrist war faktisch unbegrenzt. > - `app_version` fest `"1.0.0"` → kommt jetzt aus `ReleaseInfo.Version`. > - `BuildInfo.targets` war nicht einbindbar (CS0433/CS0103) → erzeugt die Klasse im > eigenen Namensraum, ist eingebunden. > - `UpdateClient`: API-Zweig las snake_case in ein camelCase-Modell → eigenes Modell > `ApiReleaseInfo`, `is_critical` von der obersten Ebene. > - `DeactivateAsync` schickte den Shared Key zusätzlich als `X-Watchdog-Key`. > - Kein `CancellationToken` in der Lizenz-API. > > **Weiterhin offen** — betrifft das Deploymentcenter, nicht ClawdDotNet: > > - **2.1 (keine Signaturprüfung)** — unverändert. `LicenseInfo.PublicKeyBase64` ist > gestrichen, damit nichts Totes stehenbleibt und niemand Schutz vermutet, wo keiner > ist. Kommt die Signatur, kommt das Feld mit ihr zurück. > - **2.2 (v1-Ersatzhash)** und **2.3 (Klartext-Rückfall)** — unverändert, beides im > SDK zu beheben. > - **3 (HW-ID bei jedem Aufruf neu)** — clientseitig umgangen: einmal berechnet und > behalten. > - **`parent_source` ist nur eine `source`, kein Paar** — damit schließen sich „ein > Monitor je Instanz" und instanzweise Alarmunterdrückung gegenseitig aus. Stand: 2026-08-06. Geprüft: `J:\Softwareprojekte\Deploymentcenter` (Client, Server, Schema, beide Integrationsleitfäden) gegen den [HW-ID-v2-Vorschlag](Lizenz-HardwareId-v2-Implementierungsvorschlag.md) und die [Linux-Analyse](Linux-Portierung-Analyse.md). **Ergebnis vorweg: Die Lizenz blockiert den Linux-Umzug nicht mehr.** Alles, was an Hardware-ID v2 plattformrelevant war, ist da und richtig. Was hier steht, sind Punkte aus derselben Durchsicht — drei davon würden beim Ausrollen wehtun. --- ## 1. Was erledigt ist | Punkt aus dem Vorschlag | Umsetzung | |---|---| | Format `2::` | [HardwareId.cs:125](../../Deploymentcenter/client-dotnet/Deploymentcenter.Client/HardwareId.cs) | | **Kein `MachineName` im Hash** | `ComputeV2Hash`, `:138` — der wichtigste Punkt, sauber umgesetzt | | Quellenkette Windows/Linux | `:42–107`, inklusive `dmi-uuid` | | `IsPlausibleMachineId` (Länge, `uninitialized`, nur Nullen) | `:161` | | MAC-Filter über locally-administered-Bit | `:224` | | `/sys/class/net//device`-Prüfung | `:228` | | Erweiterte Stoppwortliste | `:23` — inkl. `br-`, `virbr`, `cni`, `cali` | | `machine.key` mit `0600` | `:290`, `SetUnixPermissions` mit `#if NET8_0_OR_GREATER` | | Vorgabe per Umgebungsvariable | `LicenseConfig.HardwareIdOverride`, beide Namen | | XDG-Auflösungskette, nie leerer Pfad | [LicenseConfig.cs:28](../../Deploymentcenter/client-dotnet/Deploymentcenter.Client/LicenseConfig.cs), mit `ValidateNonEmpty` | | Mehrfachziel `netstandard2.0;net8.0` | csproj, BouncyCastle nur im netstandard-Zweig | | `LLS2`-Hülle, AES-GCM, HKDF | [StateStore.cs:169](../../Deploymentcenter/client-dotnet/Deploymentcenter.Client/StateStore.cs) — Schlüssel aus HW-ID abgeleitet, bindet den Cache also echt an die Maschine | | `ILicensePrompt` + Konsolenfassung | vorhanden — genau das, was der kopflose Host braucht | | Servermigration v1→v2 | [LicenseService.php:108](../../Deploymentcenter/src/Modules/License/LicenseService.php), mit Prüfprotokolleintrag `hwid_migrated` | | Schema `hwid_version`/`hwid_source`/`platform` | `sql/migrations/v2_hardware_id.sql`, rückwärtskompatibel | | Verwaltungsansicht zeigt Quelle/Plattform | `public/index.php:1227` | `OperatingSystemHelpers` nutzt jetzt `RuntimeInformation`. Der Client hat auf dem Linux-Pfad keine Windows-Laufzeitabhängigkeit — `ProtectedData` wird nur unter `IsWindows()` aufgerufen. **Für die Portierung heißt das:** Punkt 4 aus der Entscheidungsliste der Linux-Analyse („Erlaubt LicenseLabrador den Wechsel der Hardware-ID?") ist beantwortet. Der Aufwandsblock „Lizenz" schrumpft von 3–5 PT auf **2–3 PT** — das ist jetzt reine Anschlussarbeit in ClawdDotNet, keine Konzeptarbeit mehr. --- ## 2. Drei Befunde, die vor dem Ausrollen geklärt sein sollten ### 2.1 Es wird nichts signiert — die Lizenzprüfung ist eine Vertrauensfrage an DNS [LicenseClient.cs:62](../../Deploymentcenter/client-dotnet/Deploymentcenter.Client/LicenseClient.cs): ```csharp string status = root.TryGetProperty("status", out var sProp) ? sProp.GetString() ?? "unknown" : "unknown"; if (status.Equals("valid", StringComparison.OrdinalIgnoreCase)) { // → gültig } ``` Das ist die vollständige Prüfung. Es gibt im neuen Client **kein `Signature.cs`, keinen hinterlegten öffentlichen Schlüssel, keine Hüllenprüfung** — die Dateien `Signature.cs`, `LicenseResult.cs` und `LicenseState.cs` aus dem alten LicenseLabrador-SDK sind beim Umzug nicht mitgekommen. Folge: Wer die HTTP-Anfrage umlenken kann, hat eine gültige Lizenz. Ein Eintrag in `/etc/hosts`, ein Proxy, ein eigener DNS — die Antwort `{"status":"valid"}` genügt. Auf einem Linux-Server, den der Betreiber ohnehin vollständig kontrolliert, ist das kein Kunststück. Serverseitig sieht es passend dazu aus. `public/index.php:45`: ```php 'signature' => 'ED25519_SIG_' . base64_encode(hash('sha256', $lic['license_key'] . 'DC_OFFLINE_SECRET', true)) ``` Das ist ein SHA-256 über den Lizenzschlüssel plus eine fest verdrahtete Zeichenkette — keine Signatur, sondern ein Wert, der jeder erzeugen kann, der den Quelltext kennt. Und `public/index.php:1993` im JavaScript: ```javascript "ED25519_SIG_" + btoa(key + hwId).substring(0, 32) ``` Base64 der Eingabe, abgeschnitten. Auch kein Hash. Das ist erkennbar ein Platzhalter — nur trägt er einen Namen, der nach fertigem Verfahren klingt, und darauf verlässt sich [LicenseGate](Services/LicenseGate.cs) mit seiner harten Startsperre. **Es ist keine Portierungsfrage** (unter Windows gilt heute dasselbe) und auch kein Grund, den Linux-Umzug aufzuhalten — aber es sollte eine bewusste Entscheidung sein und nicht in dem Glauben untergehen, die Signaturprüfung sei bereits da. Wenn das Verfahren zurückkommen soll: Ed25519 über die kanonisch serialisierte Antwort, öffentlicher Schlüssel im Client einkompiliert, `nonce` aus der Anfrage in der signierten Nutzlast gegenprüfen (gegen Wiedereinspielung). Der alte `Signer.php` und `Signature.cs` sind im LicenseLabrador-Repo noch vorhanden und lassen sich als Vorlage nehmen. ### 2.2 Der v1-Ersatzhash trifft die alten Aktivierungen nicht Der Migrationsweg ist auf beiden Seiten korrekt gebaut — er wird nur nie auslösen, weil der Client eine andere v1-ID berechnet als die, die in der Datenbank steht. Alt ([LicenseLabrador/HardwareId.cs:20](../../LicenseLabrador/client-dotnet/LicenseLabrador.Client/HardwareId.cs)): ```csharp rawBuilder.Append(machineId); // MachineGuid, sonst MAC rawBuilder.Append(Environment.MachineName); // direkt angehängt, kein Trenner → sha256(machineGuid + machineName) ``` Neu ([HardwareId.cs:149](../../Deploymentcenter/client-dotnet/Deploymentcenter.Client/HardwareId.cs)): ```csharp string raw = $"{Environment.MachineName}:{firstMac}"; → sha256(machineName + ":" + mac) ``` Andere Reihenfolge, anderer Trenner, und **MAC statt MachineGuid**. Auf jedem Windows-Rechner, auf dem `MachineGuid` lesbar war — also praktisch allen — stimmen die Hashes nicht überein. Der Server sucht die Altaktivierung, findet nichts und legt eine neue an: **genau der Platzverbrauch, den die Migration verhindern sollte.** Bei `max_activations = 2` ist danach ein Platz für den Linux-Server weniger da. Auch `GetFirstPhysicalMacLegacy` (`:254`) weicht ab: keine Stoppwortfilterung, keine Sortierung, erste Schnittstelle in Aufzählungsreihenfolge. Die alte Fassung nahm die alphabetisch erste *gefilterte* MAC. Zu tun: `GetLegacyHardwareId()` muss den v1-Algorithmus zeichengenau nachbilden — inklusive der alten Stichwortliste (`virtual`, `veth`, `docker`, `hyper-v`, `wsl`, `mullvad`, `wireguard`, `tap`, `tun`, `vpn`, `bluetooth`, `vmware`, `box`, `pseudo`, `loopback`, `npcap`, `pcap`), `OrderBy(…, Ordinal)` und `FirstOrDefault()`. Der Code steht im LicenseLabrador-Repo noch da und kann weitgehend übernommen werden. Am besten mit einem Test absichern, der einen bekannten Eingabewert gegen den erwarteten v1-Hash prüft — sonst fällt eine Abweichung erst auf, wenn die Aktivierungsplätze schon verbraucht sind. ### 2.3 Der Klartext-Rückfall ist noch da, nur woanders [StateStore.cs:79](../../Deploymentcenter/client-dotnet/Deploymentcenter.Client/StateStore.cs) — „Legacy Migration Check": ```csharp string legacyJson = Encoding.UTF8.GetString(payloadBytes); var legacyData = JsonSerializer.Deserialize(legacyJson); if (legacyData != null) { legacyData.SchemaVersion = 2; Save(productSlug, hardwareId, legacyData); return legacyData; } ``` Der LLS2-Zweig darüber ist genau richtig — Entschlüsselung fehlgeschlagen heißt Cache-Fehltreffer, kein Klartext. Der Zweig darunter hebt das wieder auf: Jede Datei ohne `LLS2`-Kennung wird als JSON gelesen und, wenn sie sich deserialisieren lässt, **übernommen und anschließend verschlüsselt neu geschrieben**. Durchgespielt: Eine von Hand angelegte `state.dat` mit ```json {"SchemaVersion":2,"Status":"valid","ExpiresAt":99999999999,"MaxSeenTime":0} ``` wird angenommen. In `ValidateAsync` greift bei fehlender Verbindung der Cache-Zweig (`:110`): `Status == "valid"` ✓, `now < MaxSeenTime` ✗, `now > ExpiresAt` ✗ → **`IsValid = true`**. Die Bindung an die Hardware, die `DeriveKey(hardwareId, …)` sonst herstellt, ist auf diesem Weg umgangen; die Datei ist zwischen Maschinen übertragbar. Der Zweig hilft dabei nicht einmal beim eigentlichen Zweck. Die alte `LocalCacheData` hieß `last_envelope`, `max_seen_time`, `endpoints`, `last_license_key`; die neue `SchemaVersion`, `Status`, `ExpiresAt`, … Kein gemeinsames Feld, und `JsonSerializer` ist ohne `PropertyNameCaseInsensitive`/`JsonPropertyName` bei den Namen streng. Eine echte v1-Datei ergibt also ein Objekt mit lauter Vorgabewerten (`Status = "invalid"`) und ist als Cache wertlos. **Empfehlung: den Zweig ersatzlos streichen.** Er kostet Sicherheit und leistet nichts. Alte Cachedateien sollen verworfen werden — eine einmalige Online-Prüfung ist der ganze Preis. Nebenbei: `Checksum = hwInfo.HardwareId` ([LicenseClient.cs:79](../../Deploymentcenter/client-dotnet/Deploymentcenter.Client/LicenseClient.cs)) ist keine Prüfsumme, sondern eine Kopie der HW-ID. Das Feld ist damit ohne Funktion — entweder mit einem HMAC über die übrigen Felder füllen oder entfernen, damit niemand später Schutz vermutet, wo keiner ist. --- ## 3. Kleinere Punkte | Fundstelle | Sache | |---|---| | [LicenseClient.cs:26](../../Deploymentcenter/client-dotnet/Deploymentcenter.Client/LicenseClient.cs) | Eigener `HttpClient` je Instanz, nie freigegeben, **ohne Zeitgrenze** (Vorgabe 100 s). Der alte `LicenseConfig.HttpTimeout` war 6 s. In `LicenseGate.RunStartupCheck` bedeutet das bis zu 100 s Standbild beim Start, wenn der Server nicht antwortet. | | `:107` | `catch (Exception ex)` um den gesamten Block: Auch ein Fehler beim Auswerten einer *erfolgreichen* Antwort landet im Offline-Zweig. Ein defekter Server gilt dann als „offline". | | `:47` | `app_version = "1.0.0"` fest verdrahtet. ClawdDotNet hat `BuildInfo.Build` — sollte Parameter sein, sonst steht in der Verwaltungsansicht bei jeder Instanz dasselbe. | | `:32`, `:172` | `HardwareId.GetHardwareId()` bei jedem Aufruf neu: liest unter Linux Dateien und zählt Netzwerkschnittstellen auf. Einmal berechnen und halten. | | `HardwareId.cs:205` | MAC-Auswahl überspringt Schnittstellen, die nicht `Up` oder `Unknown` sind. Ein Kabel, das beim Start nicht steckt, ändert damit die Hardware-ID. Für die Ausweichlösung sollte der Betriebszustand keine Rolle spielen — sonst ist sie genau in dem Moment instabil, in dem sie gebraucht wird. | | `HardwareId.cs:231` | `/sys/class/net//device` ist ein Symlink. `Directory.Exists`/`File.Exists` folgen ihm — funktioniert, ist aber Zufall und sollte kommentiert sein. | --- ## 4. Watchdog: die Anbindung passt noch nicht Kein Linux-Thema, fällt aber in dieselbe Umbauarbeit. [WatchdogClient.cs](src/ClawdDotNet.Core/Watchdog/WatchdogClient.cs) sendet an: | ClawdDotNet | Deploymentcenter | |---|---| | `POST /api/heartbeat` | `POST /api/watchdog/v1/ping` (nimmt auch `/heartbeat`) | | `POST /api/event` | `POST /api/watchdog/v1/event` | | `POST /api/register` | **existiert nicht** | Die Pfade sind also alle um `/watchdog/v1` zu ergänzen. Der Kopfzeilenname passt: `public/api/watchdog/v1/index.php:30` akzeptiert `X-Watchdog-Key`, `Authorization` und `X-Agent-Token`. Der Selbstregistrierungsweg aus [Program.cs:334](Program.cs:334) — mit dem Master-Token einen eigenen Agent-Token holen und in der Instanzkonfiguration zwischenspeichern — hat serverseitig kein Gegenstück mehr. Zu klären: Tokens künftig von Hand in der Verwaltung anlegen und in die Instanzkonfiguration eintragen, oder `/register` im Deploymentcenter nachziehen. Für den ersten Weg spricht, dass er den Master-Token gar nicht erst auf die Instanzen verteilt. Die Feldnamen des Ping-Rumpfs (`source`, `instance`, `type`, `status`, `message`, `interval`, `group`, `os`) sind gegen [InstanceHealthProvider](src/ClawdDotNet.Core/Watchdog/InstanceHealthProvider.cs) abzugleichen. --- ## 5. Was in ClawdDotNet zu tun ist | Datei | Was | |---|---| | [ClawdDotNet.csproj](ClawdDotNet.csproj) | Projektverweis von `..\LicenseLabrador\client-dotnet\…` auf `..\Deploymentcenter\client-dotnet\Deploymentcenter.Client\…` umhängen. Langfristig als Submodul unter `external/` — der Kommentar dazu steht schon im csproj. | | [Services/LicenseGate.cs](Services/LicenseGate.cs) | Neu gegen `LicenseValidationResult` schreiben. `LicenseState` gibt es nicht mehr, `Status` ist jetzt eine Zeichenkette — `DescribeProblem` (`:125`) muss auf `revoked`/`expired`/`activation_limit`/`not_found` umgestellt werden. `MessageBox` durch `ILicensePrompt` ersetzen; die Konsolenfassung bringt der Client mit. | | [Services/LicenseInfo.cs](Services/LicenseInfo.cs) | `PublicKeyBase64` hat ohne Signaturprüfung keine Funktion mehr — entweder mit 2.1 zurückholen oder streichen, damit nichts Totes stehenbleibt. | | [Program.cs:112](Program.cs:112) | Lizenzprüfung so verlagern, dass sie ohne Fenster auskommt (kopfloser Host). | | Host (neu) | `--license-status`, `--license-set-key`, `--license-deactivate` — der Client bringt alles Nötige mit. | | [WatchdogClient.cs](src/ClawdDotNet.Core/Watchdog/WatchdogClient.cs) | Pfade auf `/api/watchdog/v1/…`; Registrierungsweg klären (Abschnitt 4). | | `docs/Integrationsplan-WatchDog-LicenseLabrador.md` | Abgelöst durch [Deploymentcenter-Integration](Deploymentcenter-Integration.md). | --- ## 6. Antwort auf die Ausgangsfrage **Ja — Avalonia und Linux sind damit machbar.** Die einzige Frage, die ich als möglicher Blocker außerhalb unserer Hand markiert hatte, ist geklärt: Der Client läuft auf beiden Plattformen, zielt auf `net8.0` (von net10.0 problemlos verwendbar), löst seinen Ablageort auch ohne `HOME` auf, und die HW-ID ist container- und umbenennungsfest. Der Lizenzblock in der Aufwandsschätzung fällt von 3–5 PT auf **2–3 PT**. Die Gesamtspanne bleibt bei **50–80 PT**, weil die Lizenz nie der große Posten war — das sind PropertyGrid und Chat-Ansicht. Zwei Dinge sollten aber vor dem Ausrollen erledigt sein, unabhängig von Linux: - **2.2 (v1-Ersatzhash)** — klein, aber wenn es beim Ausrollen falsch ist, sind Aktivierungsplätze verbraucht und man bekommt sie nur einzeln über die Verwaltung zurück. Das ist der Punkt mit dem schlechtesten Verhältnis von Aufwand zu Schaden. - **2.3 (Klartext-Rückfall)** — eine Zeile weniger Code, dafür wieder das Verhalten, das der `LLS2`-Umbau eigentlich herstellen sollte. **2.1 (keine Signaturprüfung)** ist eine eigene Entscheidung mit eigenem Umfang und hält den Umzug nicht auf. Sie sollte nur getroffen und nicht übersehen werden — der Name `ED25519_SIG_` im Serverquelltext legt sonst nahe, dass die Sache erledigt sei.