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>
This commit is contained in:
Richard
2026-09-10 15:11:49 +02:00
co-authored by Claude Opus 5
parent dcffcdcff5
commit e3403087c2
6 changed files with 346 additions and 10 deletions
@@ -0,0 +1,114 @@
using ClawdDotNet.Core.Instances;
using Shouldly;
namespace ClawdDotNet.Core.Tests.Instances;
/// <summary>
/// Der Nachweis, dass ein lizenzierter Instanzmanager die Instanz gestartet hat (#535).
///
/// <para><b>Das Loch, das hier zugeht.</b> Mit E-M3 prüft eine geführte Instanz die Lizenz
/// nicht mehr selbst — der Manager hat es zentral getan. Woran sie „geführt" erkannte, war
/// der Schalter <c>--managed</c>, und ein Schalter ist keine Erkennung.</para>
///
/// <para>Es geht nicht darum, jemanden abzuwehren, der Werkzeuge mitbringt: Der Schlüssel
/// steht in der Programmdatei. Es geht darum, dass der gewöhnliche Weg an der Lizenz
/// vorbei verschlossen ist.</para>
/// </summary>
public sealed class ManagerHandshakeTests
{
private const string Instanz = "/daten/Instances/Alpha";
private const string Andere = "/daten/Instances/Beta";
[Fact]
public void Eine_frische_Unterschrift_traegt()
{
var token = ManagerHandshake.Sign(Instanz);
ManagerHandshake.Verify(Instanz, token).ShouldBeTrue();
}
/// <summary>
/// Der Fall, der überhaupt der Anlass war: <c>--managed</c> ohne Unterschrift.
/// </summary>
[Theory]
[InlineData(null)]
[InlineData("")]
[InlineData(" ")]
[InlineData("--managed")]
[InlineData("v1")]
[InlineData("v1.999.")]
[InlineData("v2.99999999999.abcdef")]
[InlineData("v1.keinezahl.abcdef")]
public void Ohne_brauchbare_Unterschrift_gilt_die_Instanz_als_ungefuehrt(string? token)
{
ManagerHandshake.Verify(Instanz, token).ShouldBeFalse();
}
/// <summary>
/// Eine erfundene Unterschrift trägt nicht — auch wenn der Aufbau stimmt.
/// </summary>
[Fact]
public void Eine_erfundene_Unterschrift_traegt_nicht()
{
var ablauf = DateTimeOffset.UtcNow.AddMinutes(5).ToUnixTimeSeconds();
ManagerHandshake.Verify(Instanz, $"v1.{ablauf}.{new string('a', 64)}")
.ShouldBeFalse();
}
/// <summary>
/// Geprüft wird gegen das Verzeichnis, in dem die Instanz <b>tatsächlich</b> läuft.
///
/// <para>Sonst unterschriebe man das Argument statt der Sache: Eine Unterschrift für
/// Alpha ließe Beta starten, indem man beim Prüfen Alpha vorzeigt.</para>
/// </summary>
[Fact]
public void Eine_Unterschrift_gilt_nur_fuer_ihre_eigene_Instanz()
{
var token = ManagerHandshake.Sign(Instanz);
ManagerHandshake.Verify(Andere, token).ShouldBeFalse();
}
/// <summary>
/// Abgelaufen ist abgelaufen — sonst hübe jemand eine Unterschrift auf und startete
/// damit nächste Woche eine Instanz ohne Lizenz.
/// </summary>
[Fact]
public void Eine_abgelaufene_Unterschrift_traegt_nicht()
{
var vorhin = DateTimeOffset.UtcNow.AddHours(-1);
var token = ManagerHandshake.Sign(Instanz, vorhin);
ManagerHandshake.Verify(Instanz, token, vorhin).ShouldBeTrue("Damals galt sie.");
ManagerHandshake.Verify(Instanz, token).ShouldBeFalse("Jetzt nicht mehr.");
}
/// <summary>
/// Am Ablaufzeitpunkt herumzudrehen hilft nicht: Er geht in die Unterschrift ein.
/// </summary>
[Fact]
public void Ein_verlaengerter_Ablauf_macht_die_Unterschrift_ungueltig()
{
var token = ManagerHandshake.Sign(Instanz);
var teile = token.Split('.');
var spaeter = DateTimeOffset.UtcNow.AddYears(1).ToUnixTimeSeconds();
ManagerHandshake.Verify(Instanz, $"v1.{spaeter}.{teile[2]}").ShouldBeFalse();
}
/// <summary>
/// Derselbe Pfad in anderer Schreibweise muss dieselbe Unterschrift ergeben — sonst
/// scheitert der Handschlag daran, dass einer einen Schrägstrich mehr geschrieben hat.
/// </summary>
[Fact]
public void Schreibweisen_desselben_Pfades_ergeben_dieselbe_Unterschrift()
{
var basis = Path.Combine(Path.GetTempPath(), "clawd-handschlag");
var token = ManagerHandshake.Sign(basis);
ManagerHandshake.Verify(basis + Path.DirectorySeparatorChar, token).ShouldBeTrue();
ManagerHandshake.Verify(Path.Combine(basis, "."), token).ShouldBeTrue();
}
}