Files
ClawdDotNet/src/ClawdDotNet.Core/Instances/ManagerHandshake.cs
T
RichardandClaude Opus 5 e3403087c2 Handschlag: --managed hebelt die Lizenzpruefung nicht mehr aus (#535)
Mit E-M3 wanderte die Lizenzpruefung in den Manager: Eine gefuehrte Instanz prueft
nicht mehr selbst, weil der Manager es zentral getan hat. Woran sie "gefuehrt"
erkannte, war der Schalter --managed - und ein Schalter ist keine Erkennung.
ClawdDotNet --managed startete damit eine Instanz ganz ohne Lizenzpruefung. Vor
E-M3 gab es die Luecke nicht; sie ist mit der Zentralisierung entstanden.

ManagerHandshake unterschreibt Instanzverzeichnis und Ablauf mit HMAC-SHA256. An
die Signierstelle kommt der Manager nur, wenn seine Lizenzpruefung bestanden hat -
StartInstanceAsync weist vorher mit StartRefusal.NoLicense ab. Dass die
Unterschrift existiert, ist der Beweis.

- Gebunden an das Verzeichnis, und geprueft wird gegen das tatsaechliche, nicht
  gegen das behauptete: Sonst unterschriebe man das Argument statt der Sache.
- Fuenf Minuten gueltig, der Ablauf geht in die Unterschrift ein.
- Ueber die Umgebung statt ueber die Befehlszeile: Eine Befehlszeile steht in
  jeder Prozessliste.
- Ungueltig heisst "nicht gefuehrt", nicht Abbruch. Dann prueft die Instanz ihre
  Lizenz selbst - der Weg von vor E-M3, also kein neuer Fehlerfall.

AppHost.IsManaged ist jetzt der gepruefte Befund. CanSwitchInstance und der
Wechseldialog haengen daran statt am Schalter.

Die Grenze steht ausdruecklich im Konzept (9.9): Der Schluessel liegt in jeder
ausgelieferten Programmdatei. Es geht um den gewoehnlichen Weg an der Lizenz
vorbei, nicht um jemanden mit Werkzeugen und Willen.

Abgenommen an echten Prozessen, Unterschrift fuer den Positivfall unabhaengig in
Python gerechnet: ohne Unterschrift "Lizenz erforderlich"; mit gueltiger kommt die
Instanz hoch; Unterschrift fuer ein fremdes Verzeichnis und hochgedrehter Ablauf
scheitern beide.

820 Tests gruen, 0 Warnungen.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-10 15:11:49 +02:00

121 lines
5.6 KiB
C#

using System.Security.Cryptography;
using System.Text;
namespace ClawdDotNet.Core.Instances;
/// <summary>
/// Der Nachweis, dass ein lizenzierter Instanzmanager diese Instanz gestartet hat (#535).
///
/// <para><b>Das Loch, das hier zugeht.</b> 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 <c>--managed</c> — und ein
/// Schalter ist keine Erkennung. <c>ClawdDotNet --managed</c> startete damit eine Instanz
/// ganz ohne Lizenzprüfung. Vor E-M3 gab es die Lücke nicht, weil jede Instanz selbst
/// geprüft hat.</para>
///
/// <para><b>Wie es jetzt läuft.</b> Der Manager kommt an diese Stelle nur, wenn seine
/// Lizenzprüfung bestanden hat (<c>ManagerCore.StartInstanceAsync</c> weist vorher mit
/// <c>StartRefusal.NoLicense</c> ab). Er unterschreibt dort das Instanzverzeichnis und
/// einen Ablaufzeitpunkt; die Instanz rechnet dieselbe Unterschrift nach. Stimmt sie
/// nicht, gilt die Instanz als <b>nicht geführt</b> und prüft die Lizenz selbst — der
/// Zustand von vor E-M3 also, und damit kein neuer Fehlerfall, sondern der alte Weg.</para>
///
/// <para><b>Was das aushält und was nicht.</b> 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.</para>
///
/// <para><b>Und deshalb über die Umgebung, nicht über die Befehlszeile.</b> 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.</para>
/// </summary>
public static class ManagerHandshake
{
/// <summary>Über diese Umgebungsvariable gibt der Manager die Unterschrift weiter.</summary>
public const string EnvironmentVariable = "CLAWD_MANAGER_TOKEN";
/// <summary>
/// Der gemeinsame Schlüssel. Siehe die Einordnung oben: Er ist auffindbar, und das
/// ist Teil der Abwägung, keine Nachlässigkeit.
/// </summary>
private static readonly byte[] Secret =
Encoding.UTF8.GetBytes("clawd-instanzmanager-handschlag-2026-09-v1");
/// <summary>
/// Wie lange eine Unterschrift gilt.
///
/// <para>Fünf Minuten: Die Instanz prüft sie beim Start, und zwischen
/// <c>Process.Start</c> und dem Erreichen dieser Prüfung liegen Sekunden. Großzügig
/// genug für eine langsame Maschine und zu kurz, um sie aufzuheben.</para>
/// </summary>
public static TimeSpan Lifetime { get; } = TimeSpan.FromMinutes(5);
/// <summary>
/// Unterschreibt den Start dieser Instanz. Nur der Manager ruft das auf — und nur,
/// nachdem seine Lizenzprüfung bestanden hat.
/// </summary>
public static string Sign(string instanceDirectory, DateTimeOffset? now = null)
{
var ablauf = (now ?? DateTimeOffset.UtcNow) + Lifetime;
return Sign(instanceDirectory, ablauf.ToUnixTimeSeconds());
}
/// <summary>
/// Prüft die Unterschrift gegen das Verzeichnis, in dem diese Instanz tatsächlich
/// läuft.
///
/// <para><b>Gegen das tatsächliche Verzeichnis, nicht gegen das behauptete.</b> 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.</para>
/// </summary>
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)}";
}
/// <summary>
/// Derselbe Pfad muss beiderseits dieselbe Zeichenkette ergeben — sonst scheitert der
/// Handschlag daran, dass einer <c>C:\Instanzen\Alpha\</c> und der andere
/// <c>c:\instanzen\alpha</c> geschrieben hat. Gleiche Regel wie in
/// <see cref="InstanceEndpoint"/>.
/// </summary>
private static string Canonical(string instanceDirectory)
{
var voll = Path.GetFullPath(instanceDirectory).TrimEnd(
Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar);
return OperatingSystem.IsWindows() || OperatingSystem.IsMacOS()
? voll.ToLowerInvariant()
: voll;
}
}