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:
@@ -281,10 +281,9 @@ Dateisystem um. Reihenfolge, wenn es kommt: anhalten, zurückspielen, starten. U
|
||||
nie ohne Rückfrage. Der Manager kann das inzwischen: Er kennt den Zustand jeder
|
||||
Instanz über ihren Socket und hält sie geordnet an.
|
||||
|
||||
**Wie stellt eine Instanz fest, dass ein Manager da ist?** Vorschlag: Der Manager
|
||||
startet sie und gibt ihr den Socketpfad mit; die Instanz verbindet sich einmal
|
||||
zurück und beendet sich, wenn das nicht gelingt. Kein Selbstzweck-Handschlag,
|
||||
sondern die Prüfung aus E-M3.
|
||||
**Wie stellt eine Instanz fest, dass ein Manager da ist? — Entschieden**
|
||||
(2026-09-10, #535): über eine Unterschrift, die der Manager beim Start mitgibt.
|
||||
Siehe Abschnitt 9.9.
|
||||
|
||||
**Startet der Manager eine abgestürzte Instanz wieder? — Entschieden** (2026-09-08):
|
||||
ja, aber nur, wenn die Instanz es verlangt. Der Schalter heißt „Autostart durch
|
||||
@@ -914,3 +913,47 @@ Die Instanz gibt sich zwei Minuten zum Auslaufen, der Manager wartet bis zu fün
|
||||
kürzer, schösse er mitten in das Aufräumen hinein, um das er gerade gebeten hat. Und
|
||||
trotzdem endlich: Eine Instanz, die dauerhaft „ich räume noch auf" sagt, hängt genauso wie
|
||||
eine, die schweigt.
|
||||
|
||||
### 9.9 Der Handschlag: woran eine Instanz einen Manager erkennt
|
||||
|
||||
Behoben am 2026-09-10 (#535).
|
||||
|
||||
**Das Loch.** 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 eine Instanz ganz ohne Lizenzprüfung. Vor E-M3 gab es die
|
||||
Lücke nicht, weil jede Instanz selbst geprüft hat; sie ist mit der Zentralisierung
|
||||
entstanden.
|
||||
|
||||
**Die Lösung.** `ManagerHandshake` in `Core/Instances`. Der Manager unterschreibt
|
||||
Instanzverzeichnis und Ablaufzeitpunkt mit HMAC-SHA256; die Instanz rechnet dieselbe
|
||||
Unterschrift nach. An diese Stelle kommt der Manager nur, wenn seine Lizenzprüfung
|
||||
bestanden hat — `ManagerCore.StartInstanceAsync` weist vorher mit
|
||||
`StartRefusal.NoLicense` ab. **Dass die Unterschrift existiert, ist der Beweis.**
|
||||
|
||||
**Vier Eigenschaften, und jede hat einen Grund:**
|
||||
|
||||
| | Warum |
|
||||
|---|---|
|
||||
| **Gebunden an das Verzeichnis** — und geprüft wird gegen das, in dem die Instanz *tatsächlich* läuft | Sonst unterschriebe man das Argument statt der Sache: Eine Unterschrift für Alpha ließe Beta starten, indem man beim Prüfen Alpha vorzeigt |
|
||||
| **Fünf Minuten gültig**, Ablauf geht in die Unterschrift ein | Sonst höbe jemand eine Unterschrift auf und startete damit nächste Woche eine Instanz ohne Lizenz. Am Ablauf zu drehen bricht die Unterschrift |
|
||||
| **Über die Umgebung, nicht die Befehlszeile** | Eine Befehlszeile steht in jeder Prozessliste — die Unterschrift wäre abzulesen und abzuschreiben |
|
||||
| **Ungültig heißt nicht geführt**, nicht Abbruch | Dann prüft die Instanz ihre Lizenz eben selbst — der Weg von vor E-M3. Kein neuer Fehlerfall, sondern der alte. Wer eine Lizenz hat, merkt nichts |
|
||||
|
||||
**Was das aushält und was nicht — ausdrücklich.** Der Schlüssel steht in der Quelldatei und
|
||||
liegt damit in jeder ausgelieferten Programmdatei; wer ihn sucht, findet ihn. Das ist die
|
||||
bewusste Abwägung und keine Nachlässigkeit: 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.
|
||||
|
||||
**Abgenommen an echten Prozessen**, nicht nur im Test — und die Unterschrift für den
|
||||
Positivfall wurde unabhängig in Python gerechnet, damit nicht der C#-Code sich selbst
|
||||
bestätigt:
|
||||
|
||||
| Lauf | Ergebnis |
|
||||
|---|---|
|
||||
| `--managed`, keine Unterschrift | „Lizenz erforderlich" — kein Start |
|
||||
| `--managed` + gültige Unterschrift | Instanz kommt hoch, meldet ihren Named Pipe |
|
||||
| Unterschrift für ein anderes Verzeichnis | „Lizenz erforderlich" |
|
||||
| Gültige Unterschrift, Ablauf hochgedreht | „Lizenz erforderlich" |
|
||||
|
||||
@@ -116,6 +116,17 @@ public sealed class AppHost : IAsyncDisposable
|
||||
/// </summary>
|
||||
public LicenseGate? License { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Ob diese Instanz nachweislich vom Instanzmanager geführt wird (#535).
|
||||
///
|
||||
/// <para><b>Der geprüfte Befund, nicht der Schalter.</b> <c>--managed</c> ist eine
|
||||
/// Behauptung, die jeder aufstellen kann; hier steht das Ergebnis der Prüfung von
|
||||
/// <see cref="ManagerHandshake"/>. Wer daran eine Entscheidung hängt — ob die Instanz
|
||||
/// sich selbst wechseln darf, ob die Lizenz zentral geprüft wurde —, fragt diesen
|
||||
/// Wert und nicht die Befehlszeile.</para>
|
||||
/// </summary>
|
||||
public bool IsManaged { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Die laufende Nachprüfung. Der Aufrufer hängt sich an
|
||||
/// <see cref="LicenseWatch.Revoked"/> und beendet die Anwendung, wenn es feuert.
|
||||
@@ -196,7 +207,15 @@ public sealed class AppHost : IAsyncDisposable
|
||||
/// <summary>Wie der Lizenz-Torwächter mit dem Benutzer spricht. Null im geführten Modus.</summary>
|
||||
public ILicensePrompt? License { get; init; }
|
||||
|
||||
/// <summary>Wahr, wenn die Instanz durch den Instanzmanager gestartet und geführt wird.</summary>
|
||||
/// <summary>
|
||||
/// Die Behauptung, vom Instanzmanager geführt zu werden — der Schalter
|
||||
/// <c>--managed</c>.
|
||||
///
|
||||
/// <para><b>Eine Behauptung, kein Befund.</b> Ob sie stimmt, entscheidet allein
|
||||
/// die Unterschrift des Managers (<see cref="ManagerHandshake"/>); dieser Schalter
|
||||
/// sagt nur, dass überhaupt jemand den Anspruch erhebt. Ohne gültige Unterschrift
|
||||
/// prüft die Instanz ihre Lizenz selbst.</para>
|
||||
/// </summary>
|
||||
public bool IsManaged { get; init; }
|
||||
|
||||
/// <summary>
|
||||
@@ -289,8 +308,28 @@ public sealed class AppHost : IAsyncDisposable
|
||||
}
|
||||
|
||||
// ─── 4. Lizenz ───
|
||||
//
|
||||
// Gefuehrt ist, wer es nachweisen kann (#535). Der Schalter --managed allein
|
||||
// genuegte frueher und war damit der Weg an der Lizenz vorbei: Die Instanz
|
||||
// uebersprang die Pruefung, weil sie glaubte, der Manager habe sie schon
|
||||
// erledigt. Jetzt zaehlt die Unterschrift, und zwar gegen das Verzeichnis, in
|
||||
// dem diese Instanz tatsaechlich laeuft.
|
||||
var gefuehrt = callbacks.IsManaged
|
||||
&& ManagerHandshake.Verify(
|
||||
instancePath, Environment.GetEnvironmentVariable(
|
||||
ManagerHandshake.EnvironmentVariable));
|
||||
|
||||
if (callbacks.IsManaged && !gefuehrt)
|
||||
{
|
||||
// Kein Abbruch: Ohne Nachweis ist die Instanz eben nicht gefuehrt und prueft
|
||||
// selbst - der Weg von vor E-M3. Wer eine Lizenz hat, merkt davon nichts.
|
||||
logger.LogWarning(
|
||||
"Start mit --managed, aber ohne gültige Unterschrift des Instanzmanagers. "
|
||||
+ "Die Instanz gilt als eigenständig und prüft ihre Lizenz selbst.");
|
||||
}
|
||||
|
||||
LicenseGate? license = null;
|
||||
if (!callbacks.IsManaged)
|
||||
if (!gefuehrt)
|
||||
{
|
||||
var prompt = callbacks.License ?? new NonInteractiveLicensePrompt(Console.Error.WriteLine);
|
||||
license = new LicenseGate(
|
||||
@@ -308,7 +347,9 @@ public sealed class AppHost : IAsyncDisposable
|
||||
}
|
||||
else
|
||||
{
|
||||
logger.LogInformation("Instanz wird vom Manager geführt (--managed): Lizenzprüfung erfolgt zentral im Manager.");
|
||||
logger.LogInformation(
|
||||
"Instanz wird vom Instanzmanager geführt (Unterschrift geprüft): "
|
||||
+ "Die Lizenzprüfung ist dort schon erfolgt.");
|
||||
}
|
||||
|
||||
// ─── 5. Werkzeuge ───
|
||||
@@ -334,6 +375,7 @@ public sealed class AppHost : IAsyncDisposable
|
||||
|
||||
// Nichts freizugeben: Der Torwächter nutzt den gemeinsamen HttpClient des SDK.
|
||||
host.License = license;
|
||||
host.IsManaged = gefuehrt;
|
||||
|
||||
if (license is not null && license.IsEnforcementConfigured)
|
||||
{
|
||||
|
||||
@@ -0,0 +1,120 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -145,8 +145,11 @@ public partial class App : Application
|
||||
|
||||
var mainViewModel = new MainWindowViewModel(_host)
|
||||
{
|
||||
// Wer beaufsichtigt wird, wechselt nicht selbst - siehe _managed.
|
||||
CanSwitchInstance = !_managed
|
||||
// Wer beaufsichtigt wird, wechselt nicht selbst. Der geprüfte Befund des
|
||||
// Aufbaus, nicht der Schalter der Befehlszeile (#535): Wer --managed ohne
|
||||
// gueltige Unterschrift setzt, ist eine eigenstaendige Instanz und darf
|
||||
// wechseln wie jede andere.
|
||||
CanSwitchInstance = !_host.IsManaged
|
||||
};
|
||||
|
||||
// Der Wechsel gehoert hierher und nicht ins Fenster: Er braucht den Aufbau und
|
||||
@@ -414,7 +417,7 @@ public partial class App : Application
|
||||
{
|
||||
if (_host is null) return;
|
||||
|
||||
if (_managed)
|
||||
if (_host.IsManaged)
|
||||
{
|
||||
await new AvaloniaLicensePrompt().ShowInfoAsync(
|
||||
"ClawdDotNet – Instanzwechsel",
|
||||
@@ -843,6 +846,11 @@ public partial class App : Application
|
||||
if (preselected is not null && Directory.Exists(preselected))
|
||||
return preselected;
|
||||
|
||||
// Hier steht bewusst der Schalter und nicht der geprüfte Befund: Diese Methode
|
||||
// läuft mitten im Aufbau, bevor es einen AppHost gibt, der geprüft haben könnte.
|
||||
// Sie entscheidet auch nur, ob ein Dialog erscheint — wer --managed ohne gültige
|
||||
// Unterschrift setzt, bekommt ihn nicht und wird gleich darauf trotzdem nach
|
||||
// seiner Lizenz gefragt (#535).
|
||||
if (!_managed)
|
||||
{
|
||||
var choice = await AskStartModeAsync();
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using System.Diagnostics;
|
||||
using ClawdDotNet.Core.Instances;
|
||||
|
||||
namespace ClawdDotNet.Manager;
|
||||
|
||||
@@ -92,6 +93,14 @@ public sealed class ProcessInstanceLauncher(string executablePath, bool headless
|
||||
start.ArgumentList.Add("--instance");
|
||||
start.ArgumentList.Add(Path.GetFullPath(instanceDirectory));
|
||||
|
||||
// Der Nachweis, dass hier ein lizenzierter Manager startet (#535). Ueber die
|
||||
// Umgebung und nicht als Argument: Eine Befehlszeile steht in jeder Prozessliste.
|
||||
//
|
||||
// Dass diese Zeile ueberhaupt erreicht wird, ist der Beweis - ManagerCore weist
|
||||
// vorher mit StartRefusal.NoLicense ab, wenn die Lizenz nicht traegt.
|
||||
start.Environment[ManagerHandshake.EnvironmentVariable] =
|
||||
ManagerHandshake.Sign(instanceDirectory);
|
||||
|
||||
var process = Process.Start(start)
|
||||
?? throw new InvalidOperationException(
|
||||
$"Der Instanzprozess ließ sich nicht starten: {ExecutablePath}");
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user