23 KiB
Hardware-ID v2 — Implementierungsvorschlag
Stand: 2026-08-06. Betrifft LicenseLabrador (Client + Server) und die
Aufrufseite in ClawdDotNet (Services/LicenseGate.cs).
Anlass: Für den Linux-Umzug muss die Hardware-Bindung auf beiden Plattformen funktionieren. Bei der Durchsicht sind dabei zwei Probleme aufgefallen, die nichts mit Linux zu tun haben, aber denselben Code betreffen — die sollten in einem Zug mit erledigt werden.
1. Befund
1.1 Der Rechnername steckt im Hash — das ist das eigentliche Problem
rawBuilder.Append(Environment.MachineName); // "for system isolation"
Folge: Ein umbenannter Rechner ist eine neue Maschine. Er verbraucht einen
weiteren Aktivierungsplatz, und der alte bleibt für immer belegt
(max_activations ist standardmäßig 2 — nach zwei Umbenennungen ist die Lizenz
dicht). Das gilt bereits heute unter Windows.
Unter Linux wird daraus ein Totalausfall: In einem Container ist der Hostname
standardmäßig die gekürzte Container-ID, also bei jedem Start ein anderer.
Die Lizenz wäre nach dem zweiten docker run verbraucht.
Die Absicht („system isolation") ist auch nicht erfüllt: Der Rechnername steht
ohnehin im Feld hostname, das der Server bei jeder Prüfung mitschreibt
(LicenseService.php:112).
Diagnostisch verlieren wir nichts, wenn er aus dem Hash verschwindet.
1.2 Die MAC-Ausweichlösung ist unter Linux instabil
HardwareId.cs:86 nimmt die alphabetisch erste physische MAC. Unter Linux:
- Die Stoppwortliste kennt
dockerundveth, aber nichtbr-(Bridges),virbr(libvirt),cni,flannel,cali(Kubernetes),zt(ZeroTier). NetworkInterfaceTypemeldet unter Linux für die meisten virtuellen Geräte schlichtEthernet— die Typprüfung greift also nicht.- Bridge- und veth-MACs werden von systemd je Boot neu zufällig vergeben.
Sortiert man solche Adressen mit, wechselt die Hardware-ID beim Neustart. Die Ausweichlösung ist damit unter Linux schlimmer als keine.
1.3 Der Zustandsspeicher fällt still auf Klartext zurück
StateStore.cs:41
beim Lesen und :90 beim Schreiben:
try { decryptedData = ProtectedData.Unprotect(rawData, null, ...); }
catch { decryptedData = rawData; } // ← Klartext wird akzeptiert
Unter Linux wirft DPAPI immer, also läuft alles über den Klartextzweig. Zwei Folgen:
SECURITY.mdbehauptet, der Cache sei „strikt an diehardware_idgebunden". Das stimmt für die Hülle (die Prüfung in LicenseClient.cs:198), nicht für die Cache-Datei selbst.- Ernster:
max_seen_timeist die Uhr-Rückdreh-Sperre (StateStore.cs:107). Wer einestate.datvon Hand schreiben kann, setzt den Wert auf 0 und stellt die Systemuhr zurück. Der Klartext-Rückfall beim Lesen macht das möglich, und zwar auf jeder Plattform, auf der DPAPI nicht greift.
1.4 Ablageort bricht bei einem systemd-Dienst weg
LicenseConfig.cs:22
verwendet Environment.GetFolderPath(SpecialFolder.ApplicationData). Läuft der
Dienst unter User=clawd ohne Heimatverzeichnis, ist HOME nicht gesetzt und
GetFolderPath liefert einen leeren String. Path.Combine("", slug, "license") ergibt einen relativen Pfad — die Lizenz landet im Arbeitsverzeichnis
oder gar nicht.
1.5 Kein Formatkennzeichen, keine Plattformangabe
Die Hardware-ID ist heute ein nackter SHA-256-Hex-String. Es gibt keine Möglichkeit, im Server zu erkennen, aus welcher Quelle oder von welchem Betriebssystem eine Aktivierung stammt — und keinen Weg, das Format je zu wechseln, ohne alle bestehenden Aktivierungen zu verlieren.
Randnotiz: OperatingSystemHelpers.IsWindows() nutzt
Environment.OSVersion.Platform == PlatformID.Win32NT. Das funktioniert
zufällig richtig (Linux liefert Unix), ist aber die veraltete API.
RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ist in netstandard2.0
verfügbar und der korrekte Weg.
2. Zielbild: das Format
2:<plattform>:<64 Hex-Zeichen>
Beispiele:
2:win:9f3ab7c1… (Windows, MachineGuid)
2:lin:41e0d5aa… (Linux, /etc/machine-id)
2:lin:7c9182ff… (Linux, Vorgabe per Umgebungsvariable)
68 Zeichen — passt in activations.hardware_id VARCHAR(128) ohne
Schemaänderung. Der Doppelpunkt ist unproblematisch, die Spalte ist
utf8mb4_unicode_ci und wird nur verglichen.
Der Hash selbst:
sha256( "LicenseLabrador-HWID-v2" ‖ "\n" ‖ plattform ‖ "\n" ‖ quelle ‖ "\n" ‖ rohwert )
- Kein
MachineName. (1.1) - Die Domänenzeichenkette verhindert, dass derselbe Rohwert in anderem Zusammenhang wiederverwendbar ist.
quellegeht mit in den Hash: Findet der Client später eine bessere Quelle, ändert sich die ID bewusst und nachvollziehbar, statt zufällig.
Zusätzlich gehen drei neue Felder mit in die Anfrage — nicht in den Hash, nur zur Diagnose und für die Migration:
| Feld | Beispiel | Zweck |
|---|---|---|
hwid_version |
2 |
Formaterkennung serverseitig |
hwid_source |
machine-id |
Admin sieht, wie stabil die Bindung ist |
legacy_hardware_id |
<v1-Hash> |
Migration ohne Platzverlust (Abschnitt 4) |
3. Quellen je Plattform
Reihenfolge = Priorität. Die erste Quelle, die einen nichtleeren, plausiblen Wert liefert, gewinnt.
3.1 Vorgabe (alle Plattformen, höchste Priorität)
LicenseConfig.HardwareIdOverride (Code)
LICENSELABRADOR_HWID (Umgebungsvariable)
Quelle: override. Der Rohwert wird trotzdem gehasht, damit das Format
einheitlich bleibt.
Das ist der ehrliche Weg für Container und Serverbetrieb. Heuristik kann dort nicht gewinnen — in einem Container gibt es keine Hardware, an die man binden könnte. Der Betreiber setzt einen stabilen Wert, hinterlegt ihn im Deployment-Geheimnis, und die Bindung ist so verlässlich wie dieser Wert. Eine Zeile in der systemd-Unit statt eines Ratespiels.
3.2 Windows
| # | Quelle | hwid_source |
|---|---|---|
| 1 | HKLM\SOFTWARE\Microsoft\Cryptography\MachineGuid (Registry64) |
machine-guid |
| 2 | Stabile physische MAC (Abschnitt 3.4) | mac |
| 3 | Erzeugte Datei (Abschnitt 3.5) | keyfile |
Unverändert zu heute — nur ohne MachineName im Hash.
3.3 Linux
| # | Quelle | hwid_source |
Anmerkung |
|---|---|---|---|
| 1 | /etc/machine-id |
machine-id |
Von systemd bei der Installation erzeugt, überlebt Neustarts und Kernel-Updates. Die richtige Wahl auf einem echten System. |
| 2 | /var/lib/dbus/machine-id |
dbus-machine-id |
Ältere Systeme ohne systemd. |
| 3 | /sys/class/dmi/id/product_uuid |
dmi-uuid |
SMBIOS-UUID, echte Hardware-Bindung. Meist nur für root lesbar (0400) — Versuch in try einpacken, kein Fehler wenn nicht lesbar. Bei VMs vom Hypervisor gesetzt und dort stabil. |
| 4 | Stabile physische MAC (Abschnitt 3.4) | mac |
|
| 5 | Erzeugte Datei (Abschnitt 3.5) | keyfile |
Zwei Fallen bei /etc/machine-id, die geprüft werden müssen:
- Leer oder nur Zeilenumbruch. Auf Systemen mit
systemd-firstbootoder in manchen Images existiert die Datei, ist aber leer. Muss als „nicht vorhanden" behandelt werden, nicht als gültiger Wert — sonst haben alle diese Installationen dieselbe ID. - Der Wert
uninitialized. Genau diese Zeichenkette schreibt systemd, wenn die ID im laufenden Betrieb noch nicht festgelegt ist. Ebenfalls verwerfen.
private static bool IsPlausibleMachineId(string? v)
=> !string.IsNullOrWhiteSpace(v)
&& v.Trim().Length >= 16
&& !v.Trim().Equals("uninitialized", StringComparison.OrdinalIgnoreCase)
&& v.Trim().Trim('0').Length > 0; // nicht alles Nullen
3.4 MAC-Ausweichlösung, überarbeitet
Die heutige Fassung nimmt FirstOrDefault() der sortierten Liste. Wenn eine
Schnittstelle dazukommt oder wegfällt, kann sich damit die gewählte MAC ändern.
Besser: alle gültigen MACs sortiert verketten — dann ändert sich der Wert
nur, wenn sich die Netzwerkausstattung wirklich ändert, und nicht schon, weil
eine Adresse hinzukommt, die vorher sortiert davor lag.
Stoppwortliste erweitern um: br-, virbr, cni, flannel, cali, weave,
zt, tailscale, ipsec, sit, gre, dummy, bond, macvlan, ovs.
Zusätzlich hart ausschließen (unabhängig vom Namen):
- Schnittstellen mit gesetztem „locally administered"-Bit (zweites Bit des
ersten Oktetts,
mac[0] & 0x02). Genau das setzt systemd bei zufällig erzeugten MACs für veth und Bridges. Ein sauberer, namensunabhängiger Filter — und der wirksamste von allen. - Unter Linux zusätzlich prüfen: existiert
/sys/class/net/<name>/device? Fehlt das Verzeichnis, hat die Schnittstelle kein physisches Gerät und ist virtuell. Das ist zuverlässiger als jede Namensliste.
// Namensunabhängig: zufällig erzeugte MACs tragen dieses Bit.
private static bool IsLocallyAdministered(PhysicalAddress addr)
{
var b = addr.GetAddressBytes();
return b.Length > 0 && (b[0] & 0x02) != 0;
}
3.5 Erzeugte Datei als letzte Stufe
<StorageDirectory>/machine.key — 32 Zufallsbytes, Base64, Dateirechte 0600.
Wird nur angelegt, wenn keine Quelle davor greift.
Das ist eine Installations- und keine Hardware-Bindung. Für einen Container
ohne Vorgabe ist das aber die Wahrheit, und mit einem gemounteten Datenverzeichnis
bleibt sie über Container-Neustarts stabil. hwid_source = keyfile macht dem
Admin sichtbar, dass diese Aktivierung schwächer gebunden ist als die anderen.
Wichtig: Die Datei gehört ins Datenverzeichnis, nicht neben die Programmdatei. Sonst ist sie bei jedem Deployment weg.
4. Migration v1 → v2 ohne Platzverlust
Der Kern: Der Client kennt beide IDs und schickt beide mit. Der Server zieht die alte Aktivierung auf die neue ID um, statt eine zweite anzulegen.
Client — HardwareId bekommt neben GetHardwareId() (v2) ein
GetLegacyHardwareId(), das die heutige v1-Berechnung unverändert beibehält
(inklusive MachineName, damit sie zu bestehenden Aktivierungen passt). Beides
geht in die Anfrage:
hardware_id = "2:lin:41e0…",
legacy_hardware_id = "8fa2…", // nur solange v1-Aktivierungen existieren
hwid_version = 2,
hwid_source = "machine-id",
Server — in LicenseService::validate, an der Stelle der heutigen Suche
(LicenseService.php:100):
1. Aktivierung mit hardware_id = <v2> suchen
→ gefunden: normaler Weg (last_seen, hostname, app_version aktualisieren)
2. nicht gefunden, und legacy_hardware_id ist gesetzt:
Aktivierung mit hardware_id = <v1> suchen
→ gefunden: UPDATE activations SET hardware_id = <v2>, hwid_version = 2,
hwid_source = <quelle> WHERE id = …
+ audit_log-Eintrag 'hwid_migrated'
→ weiter wie unter 1. KEIN neuer Platz verbraucht.
3. weder noch: neue Aktivierung anlegen, max_activations prüfen (wie heute)
Damit wandern alle bestehenden Windows-Installationen beim ersten Start nach dem
Update lautlos auf v2 — niemand merkt etwas, kein Aktivierungsplatz geht
verloren. Das legacy_hardware_id-Feld kann nach einer Übergangszeit (etwa zwei
Veröffentlichungen) aus dem Client fallen.
4.1 Der lokale Cache muss einmal verworfen werden
Nicht übersehen: Die Hardware-ID geht in zwei weitere Berechnungen ein —
CalculateHmac(state, licenseKey, _hardwareId) für die Prüfsumme in
LicenseResult
(LicenseClient.cs:272)
und den Seed des Speicherschutzes (:287). Nach dem Formatwechsel schlägt
VerifyChecksum für jede zwischengespeicherte Hülle fehl.
Das ist kein Fehler, sondern erwartet — muss aber als Cache-Fehltreffer
behandelt werden (einmal online neu prüfen), nicht als
TamperSuspected. Sonst sperrt sich jede bestehende Installation beim ersten
Start nach dem Update selbst aus. Der Weg dorthin: Cache-Version im
LocalCacheData mitführen (schema_version: 2) und einen Datensatz mit
abweichender Version verwerfen, bevor die Prüfsumme überhaupt geprüft wird.
4.2 Schemaerweiterung
ALTER TABLE activations
ADD COLUMN hwid_version TINYINT NOT NULL DEFAULT 1 AFTER hardware_id,
ADD COLUMN hwid_source VARCHAR(32) NULL AFTER hwid_version,
ADD COLUMN platform VARCHAR(8) NULL AFTER hwid_source;
Alles mit Vorgabewerten, also rückwärtskompatibel — ein alter Client, der die Felder nicht schickt, funktioniert unverändert weiter.
5. Umzug Windows → Linux
Das ist etwas anderes als die Formatmigration: hier wechselt die Maschine wirklich, die ID muss sich also ändern. Drei Wege, alle drei sinnvoll parallel:
5.1 Der Normalfall braucht gar nichts
max_activations ist standardmäßig 2. Ein Windows-Entwicklungsrechner und
ein Linux-Server passen also ohne jeden Eingriff hinein. Für den anstehenden
Umzug ist das wahrscheinlich die ganze Antwort — die anderen beiden Punkte sind
für den Fall danach.
5.2 Abmelden vor dem Umzug (existiert, aber nicht erreichbar)
LicenseService::deactivate löscht die Aktivierungszeile und gibt den Platz frei
(LicenseService.php:153),
und LicenseClient.DeactivateAsync ruft es auf. In ClawdDotNet ist die Methode
aber nur über LicenseGate.cs:39 erreichbar und dort
an die GUI gebunden.
Nachzuliefern: ein Kommandozeilenschalter am Host, damit das auch ohne Oberfläche geht.
clawddotnet --license-deactivate
Das braucht der kopflose Betrieb ohnehin (siehe
Linux-Portierung-Analyse.md, 2.6 — der
Lizenzdialog ist ein MessageBox, der einen Dienst blockieren würde).
5.3 Umbinden aus der Verwaltung (fehlt noch)
Für den Fall, dass die alte Maschine schon weg ist: In
public/admin/license_detail.php je Aktivierungszeile eine Schaltfläche
„Aktivierung freigeben" (löscht die Zeile, gibt den Platz frei). Ein echtes
„Umbinden" auf eine bekannte neue ID ist unnötig — Freigeben plus Neuaktivierung
auf dem Zielsystem ist derselbe Vorgang mit weniger Code und einer klareren
Spur im Prüfprotokoll.
Beides sollte in audit_log landen, mit altem und neuem Wert.
6. Zustandsspeicher härten
Zusammen mit dem HW-ID-Umbau, weil dieselbe Datei betroffen ist und die Verschlüsselung den HW-ID als Schlüsselmaterial braucht.
Format — feste Hülle statt „mal so, mal so":
Magic "LLS2" (4 Byte) │ Nonce (12) │ Ciphertext │ GCM-Tag (16)
- AES-GCM, Schlüssel abgeleitet aus HW-ID +
ProductSlugper HKDF-SHA256. - Auf Windows das Ergebnis zusätzlich in DPAPI wickeln (Gürtel und Hosenträger, kostet nichts).
- Dateirechte
0600auf Unix.
Der entscheidende Punkt: den Klartext-Rückfall beim Lesen entfernen. Eine Datei, die sich nicht entschlüsseln oder nicht authentifizieren lässt, ist kein Cache — sie wird verworfen und der Client prüft online. Nicht als Klartext akzeptieren. Genau dieser Rückfall macht heute die Uhr-Rückdreh-Sperre umgehbar (1.3).
Einmalig weiterhin lesbar bleiben muss das alte Format (Datei ohne LLS2-Magic):
einlesen, in v2 neu schreiben, fertig. Nach einer Veröffentlichung kann der Pfad
weg.
6.1 netstandard2.0 hat kein AesGcm — Empfehlung: mehrfach zielen
System.Security.Cryptography.AesGcm gibt es erst ab .NET Core 3.0, HKDF erst
ab .NET 5, File.SetUnixFileMode erst ab .NET 7. Das Projekt zielt heute auf
netstandard2.0.
Zwei Wege:
<TargetFrameworks>netstandard2.0;net8.0</TargetFrameworks>— empfohlen. ClawdDotNet (net10.0) zieht automatisch das net8.0-Ziel und bekommtAesGcm,HKDFundFile.SetUnixFileModeohne Umwege. Der netstandard2.0-Zweig bleibt für andere Abnehmer erhalten und nutzt dort BouncyCastle. Kosten: ein paar#if NET8_0_OR_GREATER-Blöcke an genau drei Stellen.- Durchgängig BouncyCastle (
GcmBlockCipher,HkdfBytesGenerator) — die Bibliothek ist mitBouncyCastle.Cryptographybereits als Abhängigkeit da, also kein neues Paket. Kein Mehrfachziel nötig, aber die Dateirechte bleiben ein Problem:chmodmüsste per P/Invoke laufen.
Weg 1 ist sauberer, weil er nebenbei das Dateirechte-Problem löst.
7. Ablageort (1.4)
Auflösungskette in LicenseConfig.StorageDirectory, erste nutzbare gewinnt:
- Explizit gesetzter Wert (ClawdDotNet setzt ihn künftig — der Host hat ohnehin eine eigene XDG-Auflösung).
LICENSELABRADOR_STORAGE_DIR.- Unix:
$XDG_CONFIG_HOME/<slug>/license, sonst$HOME/.config/<slug>/license. - Windows:
SpecialFolder.ApplicationDatawie heute. - Letzter Ausweg:
<AppContext.BaseDirectory>/license.
Und in jedem Fall: nie einen leeren Pfad durchlassen. Der heutige Code kann
Path.Combine("", …) erzeugen, ohne dass es auffällt. Ein if (string.IsNullOrEmpty(...)) throw an dieser Stelle ist besser als eine
Lizenzdatei, die im Arbeitsverzeichnis landet und beim nächsten Start nicht mehr
gefunden wird.
8. Änderungsliste
LicenseLabrador — Client
| Datei | Was |
|---|---|
HardwareId.cs |
Neuschreiben: v2-Format, Quellenkette je Plattform, GetLegacyHardwareId(), HwidSource/Platform als Eigenschaften, MAC-Filter (locally-administered-Bit, /sys/class/net/*/device), Plausibilitätsprüfung für machine-id, machine.key-Erzeugung |
LicenseConfig.cs |
HardwareIdOverride, Auflösungskette für StorageDirectory, leeren Pfad ausschließen |
StateStore.cs |
LLS2-Hülle, AES-GCM, schema_version, Klartext-Rückfall beim Lesen entfernen, v1-Einmalmigration, 0600 |
LicenseClient.cs |
Neue Felder in validate/deactivate senden; Cache mit abweichender schema_version als Fehltreffer behandeln, nicht als TamperSuspected |
OperatingSystemHelpers |
RuntimeInformation.IsOSPlatform, dazu IsLinux()/IsMacOs() |
LicenseLabrador.Client.csproj |
netstandard2.0;net8.0 |
LicenseLabrador — Server
| Datei | Was |
|---|---|
sql/schema.sql + Migrationsskript |
hwid_version, hwid_source, platform |
src/LicenseService.php |
legacy_hardware_id entgegennehmen; Migrationssuche (Abschnitt 4); neue Felder speichern |
src/Audit.php |
Ereignisart hwid_migrated, activation_released |
public/admin/license_detail.php |
Quelle/Plattform je Aktivierung anzeigen, „Aktivierung freigeben" |
docs/SECURITY.md |
Aussage zur Cache-Bindung korrigieren (1.3) |
ClawdDotNet
| Datei | Was |
|---|---|
| Services/LicenseGate.cs | StorageDirectory explizit setzen; MessageBox/frm_License hinter eine Schnittstelle (ILicensePrompt) legen, damit der kopflose Host eine Konsolenfassung einsetzen kann |
| Host (neu) | --license-deactivate, --license-set-key, --license-status |
docs/Integrationsplan-WatchDog-LicenseLabrador.md |
Format v2 und Migrationsweg nachtragen |
9. Testplan
Das Wichtigste zuerst — die Fälle, die heute schiefgehen würden:
| Fall | Erwartung |
|---|---|
| Rechner umbenennen | ID unverändert (Kern von 1.1) |
Container zweimal starten, /etc/machine-id im Abbild |
beide Male dieselbe ID |
Container ohne machine-id, Datenverzeichnis gemountet |
ID über Neustarts stabil, hwid_source = keyfile |
Container ohne machine-id, ohne Mount |
ID wechselt — muss so sein, und im Protokoll erkennbar |
LICENSELABRADOR_HWID gesetzt |
gewinnt gegen alles, hwid_source = override |
/etc/machine-id leer bzw. uninitialized |
wird verworfen, nächste Quelle greift |
| Docker-Bridge und veth vorhanden, keine machine-id | MAC-Wahl ignoriert sie, ID über Neustart stabil |
| Bestehende v1-Windows-Aktivierung, Client aktualisiert | Zeile wird auf v2 umgeschrieben, max_activations unverändert, Prüfprotokolleintrag |
| v1-Cache-Datei nach dem Update | einmal online geprüft, dann v2-Cache — kein TamperSuspected |
state.dat von Hand mit max_seen_time = 0 |
Datei wird verworfen, Uhr-Rückdreh-Sperre bleibt wirksam |
systemd-Dienst ohne HOME |
Ablageort auflösbar, keine Datei im Arbeitsverzeichnis |
| Alter Client gegen neuen Server | funktioniert unverändert (Felder haben Vorgabewerte) |
| Neuer Client gegen alten Server | funktioniert, Zusatzfelder werden ignoriert |
Die letzten beiden Zeilen sind nicht optional: Client und Server werden nicht gleichzeitig ausgerollt.
10. Aufwand
| Block | PT |
|---|---|
HardwareId v2 samt Quellenkette, MAC-Filter, machine.key |
2–3 |
Client mehrfach zielen + StateStore-Härtung |
2–3 |
| Server: Migrationssuche, Schema, Prüfprotokoll, Verwaltungsansicht | 2–3 |
ClawdDotNet: ILicensePrompt, Lizenz-Kommandozeile |
1–2 |
| Tests (Container-Fälle brauchen echtes Docker) und Abnahme | 1–2 |
| Summe | 8–13 |
Das ist mehr als die 3–5 PT, die in der Linux-Analyse für „Lizenz" standen — weil dort nur die Plattformverträglichkeit gerechnet war. Die Punkte 1.1 und 1.3 sind bestehende Fehler, die unabhängig vom Umzug behoben werden sollten; sie machen den Unterschied aus.
Der Block ist unabhängig vom übrigen Linux-Umzug und kann sofort beginnen — er hängt an keiner der offenen GUI-Entscheidungen.
11. Was ich anders machen würde als heute — kurz begründet
Drei Entscheidungen im Vorschlag verdienen eine Begründung, weil sie vom bisherigen Ansatz abweichen:
Rechnername raus. Er ist der Grund, warum die heutige Bindung fragiler ist
als nötig, und er trägt nichts bei, was activations.hostname nicht schon
festhält. Eine Bindung, die bei einer Umbenennung bricht, bindet nicht an
Hardware, sondern an eine Konfiguration.
Vorgabe per Umgebungsvariable statt besserer Heuristik für Container. Man kann eine Container-Umgebung nicht sinnvoll erraten — es gibt dort keine Hardware. Jede zusätzliche Heuristik verschiebt nur, wo es falsch wird. Eine explizite Vorgabe ist ein bewusster Betreiberentscheid, in der Unit-Datei sichtbar, im Prüfprotokoll nachvollziehbar.
Kein Klartext-Rückfall, auch nicht „zur Sicherheit". Der heutige Rückfall sollte Robustheit bringen, kostet aber genau die Eigenschaft, für die der Cache existiert. Ein verworfener Cache bedeutet: einmal online prüfen. Das ist der mildere Schaden — und wer keine Verbindung hat, hat immer noch die Offline-Gnadenfrist aus der signierten Hülle, die von dieser Datei nicht abhängt.