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;
}
}