# 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:<plattform>:<hex>` | [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/<name>/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<LocalCacheData>(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/<name>/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.
