using System.Security.Cryptography; using System.Text; namespace ClawdDotNet.Core.Instances; /// /// Der Nachweis, dass ein lizenzierter Instanzmanager diese Instanz gestartet hat (#535). /// /// Das Loch, das hier zugeht. Mit E-M3 wanderte die Lizenzprüfung in den /// Manager: Eine geführte Instanz prüft nicht mehr selbst, weil der Manager es zentral /// getan hat. Woran sie „geführt" erkannte, war der Schalter --managed — und ein /// Schalter ist keine Erkennung. ClawdDotNet --managed startete damit eine Instanz /// ganz ohne Lizenzprüfung. Vor E-M3 gab es die Lücke nicht, weil jede Instanz selbst /// geprüft hat. /// /// Wie es jetzt läuft. Der Manager kommt an diese Stelle nur, wenn seine /// Lizenzprüfung bestanden hat (ManagerCore.StartInstanceAsync weist vorher mit /// StartRefusal.NoLicense ab). Er unterschreibt dort das Instanzverzeichnis und /// einen Ablaufzeitpunkt; die Instanz rechnet dieselbe Unterschrift nach. Stimmt sie /// nicht, gilt die Instanz als nicht geführt und prüft die Lizenz selbst — der /// Zustand von vor E-M3 also, und damit kein neuer Fehlerfall, sondern der alte Weg. /// /// Was das aushält und was nicht. Der Schlüssel steht in dieser Datei und /// liegt damit in jeder ausgelieferten Programmdatei; wer ihn sucht, findet ihn. Das ist /// bewusst so: Es geht nicht darum, jemanden abzuwehren, der Werkzeuge und Willen /// mitbringt, sondern darum, dass der gewöhnliche Weg um die Lizenz herum verschlossen /// ist. Ein Schalter, den man in der Hilfe liest, ist etwas anderes als ein HMAC, den man /// nachbauen muss. /// /// Und deshalb über die Umgebung, nicht über die Befehlszeile. Eine /// Befehlszeile steht in jeder Prozessliste — die Unterschrift wäre für jeden abzulesen /// und abzuschreiben. Die Umgebung eines fremden Prozesses ist das nicht. Bindung an das /// Verzeichnis und die kurze Gültigkeit halten den Schaden auch dann klein, wenn doch /// jemand mitliest. /// public static class ManagerHandshake { /// Über diese Umgebungsvariable gibt der Manager die Unterschrift weiter. public const string EnvironmentVariable = "CLAWD_MANAGER_TOKEN"; /// /// Der gemeinsame Schlüssel. Siehe die Einordnung oben: Er ist auffindbar, und das /// ist Teil der Abwägung, keine Nachlässigkeit. /// private static readonly byte[] Secret = Encoding.UTF8.GetBytes("clawd-instanzmanager-handschlag-2026-09-v1"); /// /// Wie lange eine Unterschrift gilt. /// /// Fünf Minuten: Die Instanz prüft sie beim Start, und zwischen /// Process.Start und dem Erreichen dieser Prüfung liegen Sekunden. Großzügig /// genug für eine langsame Maschine und zu kurz, um sie aufzuheben. /// public static TimeSpan Lifetime { get; } = TimeSpan.FromMinutes(5); /// /// Unterschreibt den Start dieser Instanz. Nur der Manager ruft das auf — und nur, /// nachdem seine Lizenzprüfung bestanden hat. /// public static string Sign(string instanceDirectory, DateTimeOffset? now = null) { var ablauf = (now ?? DateTimeOffset.UtcNow) + Lifetime; return Sign(instanceDirectory, ablauf.ToUnixTimeSeconds()); } /// /// Prüft die Unterschrift gegen das Verzeichnis, in dem diese Instanz tatsächlich /// läuft. /// /// Gegen das tatsächliche Verzeichnis, nicht gegen das behauptete. Sonst /// unterschriebe man das Argument statt der Sache: Eine Unterschrift für Instanz A /// ließe eine Instanz B starten, indem man beim Prüfen A vorzeigt. /// public static bool Verify(string instanceDirectory, string? token, DateTimeOffset? now = null) { if (string.IsNullOrWhiteSpace(token)) return false; var teile = token.Split('.'); if (teile.Length != 3 || teile[0] != "v1") return false; if (!long.TryParse(teile[1], out var ablauf)) return false; if (DateTimeOffset.FromUnixTimeSeconds(ablauf) < (now ?? DateTimeOffset.UtcNow)) return false; var erwartet = Sign(instanceDirectory, ablauf); // Zeitkonstant, weil es nichts kostet. Ein Vergleich, der beim ersten // abweichenden Zeichen aussteigt, verraet dem Geduldigen die Unterschrift // Zeichen fuer Zeichen. return CryptographicOperations.FixedTimeEquals( Encoding.UTF8.GetBytes(erwartet), Encoding.UTF8.GetBytes(token)); } private static string Sign(string instanceDirectory, long expiresUnix) { var nachricht = $"clawd-manager-v1|{Canonical(instanceDirectory)}|{expiresUnix}"; var signatur = HMACSHA256.HashData(Secret, Encoding.UTF8.GetBytes(nachricht)); return $"v1.{expiresUnix}.{Convert.ToHexStringLower(signatur)}"; } /// /// Derselbe Pfad muss beiderseits dieselbe Zeichenkette ergeben — sonst scheitert der /// Handschlag daran, dass einer C:\Instanzen\Alpha\ und der andere /// c:\instanzen\alpha geschrieben hat. Gleiche Regel wie in /// . /// private static string Canonical(string instanceDirectory) { var voll = Path.GetFullPath(instanceDirectory).TrimEnd( Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar); return OperatingSystem.IsWindows() || OperatingSystem.IsMacOS() ? voll.ToLowerInvariant() : voll; } }