Skip to content
← Alle Beiträge
· Delta1 Labs Lizenzierung.NETLeitfaden

Ein License Gate entwerfen: wo geprüft wird, was gecacht wird und wann fail-closed gilt

Ein einzelner Validate()-Aufruf ist der einfache Teil der Lizenzierung. Der schwierige Teil ist die Architektur: wo in Ihrer App die Prüfung lebt, wie oft sie läuft, was Sie zwischen den Läufen cachen und — die Frage, die entscheidet, ob ein Bug einen zahlenden Kunden aussperrt oder Ihre Software verschenkt — welche Fehler fail-closed und welche fail-open behandelt werden. Hier ist ein ausfallsicheres License Gate für .NET auf Basis von Keyright, mit einer Entscheidungstabelle Status für Status und einem gecachten Snapshot, der offline weiterlebt.

Keyright zu einer .NET-App hinzuzufügen sieht nach einer Zeile aus: client.Validate() liefert eine LicenseInfo, Sie prüfen IsValid, fertig. Diese Zeile ist der einfache Teil, und sie ist nicht die Stelle, an der Lizenzierung schiefgeht. Lizenzierung geht in der Architektur um diesen Aufruf herum schief — wo er lebt, wie oft er läuft, was Sie zwischen den Läufen aufbewahren und vor allem, was passiert, wenn er Ihnen keine saubere Antwort geben kann. Machen Sie das in der einen Richtung falsch, und ein vorübergehender Schluckauf sperrt einen Kunden aus, der Sie bezahlt hat. Machen Sie es in der anderen Richtung falsch, und eine manipulierte Binärdatei führt Ihre Pro-Features kostenlos aus.

Dieser Beitrag baut ein License Gate: ein einzelnes Objekt, das den einen Validierungsaufruf besitzt, einen unveränderlichen Snapshot des Ergebnisses cacht und pro Fehlergrund eine bewusste Entscheidung trifft, ob es fail-closed oder fail-open reagiert. Es ist das Stück, das die meisten Lizenzierungs-Tutorials überspringen, und es ist das Stück, das entscheidet, ob Ihre Lizenzierung ein Aktivposten oder eine Support-Warteschlange ist.

Ein Aufruf, ein Besitzer, ein Snapshot

Die erste Regel lautet: Genau ein Objekt ruft das SDK auf. Verstreuen Sie Validate() durch Ihre View-Models, und Sie bekommen eine erneute Verifikation bei jedem Button-Klick, inkonsistente Antworten, wenn die Uhr mitten in der Sitzung über den Ablauf tickt, und keine einzige Stelle, an der Sie die Richtlinie ändern können. Validieren Sie stattdessen einmal, frieren Sie die Antwort in einen Snapshot ein und lassen Sie den Rest der App den Snapshot lesen.

public sealed record LicenseSnapshot(
    bool AllowsPaid,
    Edition Edition,
    EntitlementSet Entitlements,
    LicenseStatus Status,
    DateTime TakenUtc)
{
    public static readonly LicenseSnapshot Free =
        new(false, Edition.Free, EntitlementSet.Empty, LicenseStatus.NoLicense, DateTime.UtcNow);
}

Der Snapshot ist aus gutem Grund ein record: Er ist unveränderlich, sodass kein Feature versehentlich den Lizenzierungszustand verändern kann, sobald das Gate ihn herausgibt, und eine Hintergrundaktualisierung tauscht die gesamte Referenz atomar aus, statt Felder zu bearbeiten, die andere Threads gerade lesen.

Das Gate selbst hält den KeyrightClient, den aktuellen Snapshot und die Richtlinie, die eine rohe LicenseInfo in einen Snapshot verwandelt:

public sealed class LicenseGate
{
    private readonly KeyrightClient _client;
    private volatile LicenseSnapshot _current = LicenseSnapshot.Free;

    public LicenseGate(KeyrightClient client) => _client = client;

    public LicenseSnapshot Current => _current;

    public bool Allows(string feature) =>
        _current.AllowsPaid && _current.Entitlements.IsEnabled(feature);

    public long Limit(string feature, long fallback) =>
        _current.AllowsPaid ? _current.Entitlements.GetLimit(feature, fallback) : fallback;

    public void Refresh()
    {
        LicenseInfo info;
        try
        {
            info = _client.Validate();               // offline: prüft Signatur + Lease lokal
        }
        catch (Exception ex)
        {
            // Eine geworfene Ausnahme ist ein *unbekannter* Zustand, kein lizenzierter.
            _current = LicenseSnapshot.Free with { Status = LicenseStatus.Malformed };
            Log.Warning(ex, "license validation threw; treating as unlicensed");
            return;
        }
        _current = Decide(info);
    }
}

Beachten Sie das volatile-Feld und den Austausch des ganzen Objekts: Ein Leser im UI-Thread sieht immer entweder den alten Snapshot oder den neuen, niemals eine halb aktualisierte Struktur. Und beachten Sie, dass eine geworfene Ausnahme zur kostenlosen Edition auflöst, niemals zu „lizenziert”. Ein unbekannter Lizenzierungszustand ist niemals ein bezahlter — dieses Prinzip ist der ganze Beitrag in einer Zeile.

LicenseGatebesitzt Validate() + SnapshotKeyright-SDKValidate() — lokalprüfen, kein Netzwerkimmutabler SnapshotAllowsPaid · EditionEntitlements · StatusFeature-Codegate.Allows("export")liest nur den Snapshotruft einmal auffriert Ergebnis einfragt ab

Die Entscheidungstabelle ist das Gate

LicenseInfo.Status ist kein Boolean. Keyright meldet acht verschiedene Ergebnisse, und sie auf IsValid zusammenzufalten wirft genau die Information weg, die Sie brauchen, um eine Reaktion zu wählen:

public enum LicenseStatus
{
    Valid = 0,            // Signatur + Lease geprüft, nicht abgelaufen, Maschine passt
    NoLicense = 1,        // nichts zu prüfen
    Malformed = 2,        // Lizenzartefakt gefunden, aber nicht parsebar
    SignatureInvalid = 3, // geparst, aber die Signatur passt nicht zum eingebetteten Schlüssel
    Expired = 4,          // gültige Signatur, aber abgelaufen
    MachineMismatch = 5,  // gültig, aber an eine andere Maschine gebunden
    Revoked = 6,          // serverseitig explizit widerrufen und bei einer Lease-Erneuerung gesehen
    ClockTampered = 7,    // lokale Uhr gegenüber einem vertrauenswürdigen Zeitstempel zurückgedreht
}

Die Richtlinie des Gate ist ein einzelnes switch über diese Werte, und es auszuschreiben zwingt Sie, jede Fail-open-/Fail-closed-Entscheidung explizit zu treffen statt aus Versehen:

private static LicenseSnapshot Decide(LicenseInfo info)
{
    switch (info.Status)
    {
        // Sauber bestanden — der einzige Zustand, der bezahlte Features gewährt.
        case LicenseStatus.Valid:
            return new(true, info.Edition, info.Entitlements(),
                       info.Status, DateTime.UtcNow);

        // Schuldlose Abwesenheit — das ist ein Free-Nutzer, kein Angreifer. Fail OPEN zur kostenlosen Edition.
        case LicenseStatus.NoLicense:
            return LicenseSnapshot.Free;

        // Ein abgelaufenes Abonnement. ExpiryUtc ist echt und signiert; gewähren Sie ein
        // Kulanzfenster, damit eine laufende Verlängerung oder ein noch nicht erneuertes
        // Lease keinen zahlenden Kunden mitten in der Sitzung aussperrt. Fail OPEN, kurz und begrenzt.
        case LicenseStatus.Expired when WithinGrace(info, days: 14):
            return new(true, info.Edition, info.Entitlements(),
                       info.Status, DateTime.UtcNow);
        case LicenseStatus.Expired:
            return LicenseSnapshot.Free;

        // Alles darunter ist ein Hinweis auf Manipulation oder eine Lizenz, die nie
        // für Sie bestimmt war. Es gibt keine harmlose Deutung. Fail CLOSED, hart.
        case LicenseStatus.Malformed:
        case LicenseStatus.SignatureInvalid:
        case LicenseStatus.MachineMismatch:
        case LicenseStatus.Revoked:
        case LicenseStatus.ClockTampered:
        default:
            return LicenseSnapshot.Free with { Status = info.Status };
    }
}

private static bool WithinGrace(LicenseInfo info, int days) =>
    info.ExpiryUtc is { } exp && DateTime.UtcNow <= exp.AddDays(days);

Die Form der Richtlinie ist der Punkt. Drei der acht Zustände sind schuldlos: Valid gewährt, NoLicense ist einfach ein Free-Nutzer, und ein frisch Expired-Abonnement läuft durch ein begrenztes Kulanzfenster weiter, sodass eine Verlängerung, die sich noch verbreitet, niemals eine Aussperrung erzeugt. Die anderen fünf — Malformed, SignatureInvalid, MachineMismatch, Revoked, ClockTampered — haben keine harmlose Interpretation, also fallen sie ohne Kulanz direkt auf die kostenlose Edition. Sie sind nicht kundenfeindlich, wenn Sie diese fail-closed behandeln; Sie weigern sich, einem Artefakt Vertrauen zu gewähren, das nicht beweisen konnte, dass es welches verdient.

StatusReaktionTendenzValidbezahlte Features gewährendurchlassenNoLicenseauf Free-Edition zurückfallenoffen — schuldlosExpired (innerhalb 14 Tage Kulanz)Features behalten, begrenztoffen — begrenztExpired (nach Kulanz)auf Free-Edition zurückfallenoffen — schuldlosMalformed · SignatureInvalidMachineMismatch · RevokedClockTamperedSperren — kein VertrauenGESCHLOSSEN — nie harmlos

Warum ClockTampered fail-closed sein muss, obwohl es harmlos aussieht

Der eine Status, der Entwickler zu fail-open verleitet, ist ClockTampered. Es fühlt sich wie ein ehrlicher Fehler an — ein Laptop mit leerer CMOS-Batterie, eine aus einem Snapshot wiederhergestellte VM, ein Nutzer in der falschen Zeitzone. Warum das bestrafen?

Weil die Uhr das Einzige ist, was zwischen einer zeitlich begrenzten Lizenz und einer unendlichen steht. Keyright zeichnet jedes Mal, wenn es eine monoton fortschreitende Uhr sieht (bei der Aktivierung, bei jeder Lease-Erneuerung), einen vertrauenswürdigen Höchststand-Zeitstempel auf. ClockTampered bedeutet, dass die lokale Uhr jetzt hinter diesem Höchststand liegt — die Wanduhr ist rückwärts gelaufen. Die harmlosen Erklärungen sind real, aber der Angriff auch: Drehen Sie die Uhr auf letzten Monat zurück, und eine Testversion, die gestern abgelaufen ist, ist wieder jung. Ein Gate, das bei ClockTampered fail-open reagiert, hat überhaupt keinen Ablauf, denn jeder Ablauf lässt sich durch Zurückstellen des Datums rückgängig machen. Also schließt das Gate, und der Weg zur Wiederherstellung besteht darin, dass der Nutzer seine Uhr korrigiert und neu startet — eine überprüfbare Handlung — statt dass Ihr Code rät, welche Rücksetzungen ehrlich sind.

Aktualisieren ohne Netzwerkabhängigkeit

Der Snapshot wird einmal beim Start aufgenommen und dann über einen langsamen Timer. Zwei Dinge sind hier wichtig. Erstens ist die Aktualisierung offline — Validate() prüft die signierte Lizenz und das gecachte Lease erneut gegen den eingebetteten öffentlichen Schlüssel, ohne Netzwerkaufruf, sodass eine Aktualisierung auch im Flugzeug funktioniert. Zweitens ist das Lease das, was einen langlebigen Prozess sicher macht: Die Aktivierung hat ein signiertes, maschinengebundenes Lease mit 14 Tagen Gültigkeit beschafft, und solange das Lease nicht abgelaufen ist, liefert das Gate weiter Valid, ohne jemals Ihren ausstellenden Dienst zu erreichen.

// In einer WPF-/WinForms-App: ein niederfrequenter Timer, kein Hook auf dem heißen Pfad.
var timer = new System.Threading.Timer(_ => gate.Refresh(),
    null, dueTime: TimeSpan.Zero, period: TimeSpan.FromHours(6));

// Auch nach dem Aufwachen der Maschine aktualisieren, wo sich Uhr und Lease bewegt haben können.
SystemEvents.PowerModeChanged += (_, e) =>
{
    if (e.Mode == PowerModes.Resume) gate.Refresh();
};

Das einzige Mal, dass das Netzwerk ins Spiel kommt, ist eine Lease-Erneuerung — wenn das Lease kurz vor seinem Ablauf steht, erneuert das SDK es beim nächsten Online-Moment gegen Ihren ausstellenden Dienst. Ist der Dienst nicht erreichbar, lebt das Lease einfach seine verbleibende Kulanz aus; das Gate sagt die ganze Zeit weiter Valid und fällt erst, wenn das Lease ohne Erneuerung wirklich erlischt. Ausfallzeit auf Ihrer Seite degradiert daher sanft zu Offline-Betrieb statt zu einem Ausfall für den Kunden — was nur deshalb gilt, weil das Gate ein gecachtes, signiertes Lease liest, statt bei jeder Prüfung nach Hause zu telefonieren.

Features beschränken, nicht nur die App

Mit dem Gate an Ort und Stelle ist Feature-Gating ein Einzeiler, und weil Entitlements in die Lizenz signiert sind, liefern Sie eine Binärdatei für jede Stufe aus:

// Boolesche Fähigkeit.
exportButton.IsEnabled = gate.Allows("export-to-pdf");

// Numerisches Kontingent — "unlimited" in den signierten Nutzdaten wird als long.MaxValue zurückgelesen.
int maxSeats = (int)Math.Min(gate.Limit("seats", fallback: 1), int.MaxValue);

// Ein ganzes Pro-exklusives Panel, einmal an den Snapshot gebunden.
proPanel.Visibility = gate.Current.AllowsPaid ? Visibility.Visible : Visibility.Collapsed;

Alles läuft durch das Gate, also gibt es genau eine Stelle, die die Richtlinie kennt, eine Stelle, die cacht, und eine Stelle zum Prüfen, wenn jemand fragt: „Was passiert, wenn die Lizenz widerrufen wird, während die App läuft?” Die Antwort: Bei der nächsten Aktualisierung wird der Status Revoked, Decide bildet ihn auf Free ab, der Snapshot wird atomar ausgetauscht, und das Pro-Panel klappt zu — keine Inline-Prüfung irgendwo sonst in der Codebasis musste davon wissen.

Die eine Regel

Wenn Sie sich eine Sache merken, dann die Invariante, die das Gate überall durchsetzt: Ein unbekannter Lizenzierungszustand ist niemals ein lizenzierter. Eine geworfene Ausnahme, ein nicht parsebares Artefakt, eine schlechte Signatur, eine Uhr, die rückwärts lief — nichts davon ist „wahrscheinlich in Ordnung”. Es ist die Abwesenheit eines Beweises, und die Abwesenheit eines Beweises schließt. Die Handvoll Zustände, die tatsächlich offen bleiben — eine fehlende Lizenz, ein Abonnement innerhalb seines Kulanzfensters — sind die, für die Sie laut einen schuldlosen Grund nennen können. Alles andere fällt auf Free und wartet darauf, dass der Nutzer eine Lizenz vorlegt, die verifiziert. Diese eine Regel, angewandt in einem Gate statt in hundert verstreuten Prüfungen, ist der Unterschied zwischen Lizenzierung, der Sie vertrauen können, und Lizenzierung, die Sie babysitten müssen.

Nebula.NET testen

Härten Sie Ihren .NET-Code in wenigen Minuten — starten Sie mit der kostenlosen Edition.