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

Einen Lizenzschlüssel zu prüfen ist nicht gratis: die Signaturprüfung cachen, ohne eine veraltete Antwort zu cachen

Eine signierte Keyright-Lizenz wird mit einer RSA-Signaturprüfung verifiziert, und diese Prüfung kostet echte Mikrosekunden. Wer Validate() bei jeder Anfrage aufruft, hat Public-Key-Kryptographie ohne Grund auf seinen Hot Path gelegt — die Signatur hat sich seit dem Start nicht geändert. Aber man kann das Ergebnis auch nicht einfach für immer cachen, denn die Antwort ist zeitabhängig: ein Entitlement kann um zwölf Uhr ablaufen, ein Lease kann eingezogen werden, eine Verlängerung kann den Plan erweitern. Dieser Beitrag trennt die beiden Hälften einer Lizenzprüfung — die teure, cachebare Signaturverifikation und die billige, zwingend pro Aufruf stattfindende Auswertung der Lizenz zum aktuellen Zeitpunkt —, misst, was jede davon kostet, und zeigt eine zweistufige Memoisierung, die die Kryptographie vom Hot Path fernhält, ohne jemals eine Antwort zu liefern, die eine Minute veraltet ist.

Eine Keyright-Lizenz ist ein signiertes Dokument. Der Ausgabedienst hält einen privaten RSA-Schlüssel, signiert kanonische Nutzdaten — Edition, Entitlements, Node-Lock, Ablaufdatum —, und jede Kopie Ihrer Anwendung bettet den passenden öffentlichen Schlüssel ein und verifiziert diese Signatur offline. Diese Verifikation ist das gesamte Fundament des Schemas: Sie macht eine Lizenz, die man auf dem Client lesen kann, unfälschbar. Sie ist aber auch nicht gratis, und der häufigste Performance-Fehler bei jedem SDK für signierte Lizenzen ist, weit öfter dafür zu bezahlen, als die Fakten es verlangen.

Hier ist der Fehler in Reinform — eine Lizenzprüfung direkt in einen ASP.NET-Core-Autorisierungs-Handler verdrahtet:

public async Task HandleRequirementAsync(
    AuthorizationHandlerContext context, EntitlementRequirement requirement)
{
    // Called on every single request to a gated endpoint.
    LicenseInfo info = _client.Validate();          // ← RSA verify + parse, every time
    if (info.IsEnabled(requirement.Entitlement))
        context.Succeed(requirement);
}

Validate() ist reine, lokale Offline-Berechnung — hier findet kein Netzwerkaufruf statt, das Lease wurde bei der Aktivierung gecacht. Aber „lokal“ ist nicht dasselbe wie „billig genug, um es tausendmal pro Sekunde zu tun“. Jeder Aufruf führt die RSA-Signaturverifikation über die Lizenzbytes erneut aus, dekodiert die Base64-Nutzdaten erneut, parst sie erneut und baut das Entitlement-Set neu auf — nur um eine Frage zu beantworten, deren Eingaben sich seit dem Prozessstart nicht geändert haben.

Was der Aufruf tatsächlich kostet

Es hilft, konkret zu sein, wohin die Zeit geht. Ein Validate()-Aufruf tut der Reihe nach: den Lizenzstring Base64-dekodieren, die RSA-2048-Signatur über die kanonischen Nutzdaten verifizieren, die Nutzdaten parsen (UTF-8 in Felder), das Entitlement-Dictionary aufbauen und dann die Lizenz gegen die aktuelle Zeit auswerten. Nur der letzte Schritt hängt davon ab, wann Sie ihn aufrufen. Alles davor hängt nur von den Lizenzbytes und Ihrem öffentlichen Schlüssel ab.

Base64-Dekodierungder LizenzRSA-2048 verifyeinige zehn µsNutzdaten parsen +Entitlements aufbauenAuswertung zu jetztAblaufdaten vergleichen · nskonstant für eine Lizenz — das cachenzeitabhängig — nie cachen

Keine dieser Zahlen ist für sich alarmierend — einige zehn Mikrosekunden für die Verifikation, ein paar kurzlebige Allokationen für das Parsen. Der Schaden liegt in der Wiederholung. Legen Sie das auf einen Pfad pro Anfrage bei tausend Anfragen pro Sekunde, und Sie verbrennen die Zeit eines ganzen Kerns und erzeugen einen stetigen Strom von Garbage, um eine Signatur erneut zu beweisen, die bereits gültig war. Die Lösung ist keine schnellere Kryptobibliothek. Sie besteht darin, die teure Frage nicht mehr zu stellen, wenn Sie die Antwort schon kennen.

Einmal verifizieren, jedes Mal auswerten

Die zentrale Einsicht ist, dass die beiden Hälften völlig unterschiedliche Cache-Lebensdauern haben. Signaturverifikation und Parsen sind unveränderlich, solange der Prozess dieselben Lizenzbytes hält — dieselben Bytes ein zweites Mal gegen denselben öffentlichen Schlüssel zu verifizieren, kann kein anderes Ergebnis liefern. Die Auswertung zum aktuellen Zeitpunkt ist das Gegenteil: Sie ist genau der Teil, der sich ändern soll, weil ein Entitlement sein eigenes Ablaufdatum tragen kann und die validierte Uhr voranschreitet.

Also trennen Sie sie. Einmal verifizieren und parsen, den resultierenden LicenseInfo-Snapshot halten, und bei jedem Aufruf nur die billige, zeitabhängige Auswertung gegen diesen Snapshot durchführen. LicenseInfo ist ein unveränderlicher Wert — das SDK erzeugt ihn und verändert ihn nie —, er kann also ohne Lock sicher zwischen Threads geteilt werden.

public sealed class LicenseSnapshot
{
    private readonly KeyrightClient _client;
    private volatile LicenseInfo _verified;   // signature-verified, parsed once
    private long _verifiedAtTicks;
    private readonly long _refreshTicks = TimeSpan.FromSeconds(2).Ticks;

    public LicenseSnapshot(KeyrightClient client)
    {
        _client = client;
        _verified = client.Validate();                 // pay the RSA cost once
        _verifiedAtTicks = DateTime.UtcNow.Ticks;
    }

    private LicenseInfo Current()
    {
        long now = DateTime.UtcNow.Ticks;
        if (now - Volatile.Read(ref _verifiedAtTicks) > _refreshTicks)
        {
            _verified = _client.Validate();            // re-verify on a short interval
            Volatile.Write(ref _verifiedAtTicks, now);
        }
        return _verified;
    }

    // Hot path: a field read plus a time comparison. No crypto, no allocation.
    public bool IsEnabled(string entitlement) => Current().IsEnabled(entitlement);
}

Zwei Dinge machen das korrekt und nicht nur schnell. Erstens ist IsEnabled auf dem Snapshot die billige Hälfte — bei einem Keyright-Entitlement mit eigenem Ablaufdatum vergleicht IsEnabled dieses Ablaufdatum zum Aufrufzeitpunkt mit der validierten Uhr, sodass ein Feature, das um zwölf Uhr ausläuft, um 12:00:01 als deaktiviert gelesen wird, obwohl der Snapshot beim Frühstück verifiziert wurde. Die teure Signaturprüfung ist gecacht; die zeitkritische Entscheidung nicht. Zweitens ist das Refresh-Intervall kurz — Sekunden, nicht Minuten. Es existiert nur, um ein geändertes Lease aufzugreifen (eine Verlängerung, die den Plan erweitert hat, einen Widerruf, der ihn auf Free zusammengestutzt hat), nicht um CPU zu rationieren — denn die CPU-Kosten wurden bereits dadurch beseitigt, dass der Snapshot zwischen den Refreshes gecacht wird.

Die Falle: die Antwort statt der Verifikation cachen

Der Fehlermodus, den man laut benennen sollte, ist der, der wie eine Optimierung aussieht und in Wirklichkeit ein Korrektheitsfehler ist. Es ist verlockend, den Boolean zu cachen — „ist export aktiviert?“ —, weil das der Wert ist, den die Aufrufstelle haben will:

// WRONG: caches the time-dependent answer.
private readonly ConcurrentDictionary<string, bool> _answers = new();
public bool IsEnabled(string ent) =>
    _answers.GetOrAdd(ent, e => _client.Validate().IsEnabled(e));

Das ist schnell und falsch. In dem Moment, in dem export abläuft, liefert dieses Dictionary für den Rest des Prozesslebens weiter true, weil nichts das Ablaufdatum neu auswertet. Sie haben das Einzige gecacht, das sich ändern sollte, und das Einzige, das sich nie ändert — die Signaturverifikation —, bei jedem Cache-Miss laufen lassen. Die korrekte Trennung cacht den Snapshot (die Invariante) und führt die Auswertung (die Variable) erneut aus. Halten Sie die geparste Lizenz; halten Sie niemals das Urteil.

Langsame StufeSignatur neu verifizierenalle ~2 s / bei AktivierungLicenseInfounveränderlicher SnapshotSchnelle Stufe — Hot PathIsEnabled(e) at now()Feld lesen + Ablauf vergleichenNanosekunden, keine Allokationschreibtliest

Wenn Sie trotzdem abkürzen wollen

Darauf aufbauend gibt es eine legitime Mikrooptimierung, und sie besteht nicht darin, das Urteil zu cachen — sondern darin, die Refresh-Prüfung zu überspringen, wenn die Uhr offensichtlich nicht weit genug vorangeschritten ist, um eine Rolle zu spielen. Die Current()-Methode oben macht bereits die billigste mögliche Schranke: ein DateTime.UtcNow-Lesen und ein Integer-Vergleich. Wenn das Profiling sagt, dass selbst das auf einem extremen Hot Path zu viel ist, reichen Sie den Snapshot selbst an den Aufrufer weiter und lassen eine enge innere Schleife Entitlements direkt daraus lesen, mit Refresh nur an den Grenzen eines Batches:

// Batch boundary — refresh once, then evaluate N times with zero overhead.
LicenseInfo info = _snapshot.Current();
foreach (var job in batch)
    if (info.IsEnabled("bulk-process"))
        Process(job);

Das ist genau deshalb sicher, weil der Batch im Verhältnis zum Refresh-Intervall kurz ist, sodass die Uhr währenddessen nicht nennenswert voranschreiten kann. Für alles Langlebigere — einen Request-Handler, den Tick eines Hintergrunddienstes — gehen Sie wieder über Current(), damit ein frisch abgelaufenes Entitlement gesehen wird.

Die Regel

Eine Prüfung einer signierten Lizenz sind zwei Aufgaben unter einem Methodennamen. Die Signatur zu verifizieren beweist, dass die Lizenz authentisch ist, kostet echte Mikrosekunden und hat eine Antwort, die sich nicht ändert, solange der Prozess die Lizenz hält — also einmal verifizieren und den Snapshot halten. Die Lizenz auszuwerten sagt Ihnen, was jetzt gerade wahr ist, kostet Nanosekunden und ist genau der Teil, der sich ändern soll — also nie cachen. Wer die Trennung richtig macht, nimmt Public-Key-Kryptographie vom Hot Path, ohne jemals eine veraltete Antwort zu liefern — und genau darum geht es: schnell und ehrlich, nicht das eine auf Kosten des anderen.

Keyrights SDK liefert Ihnen das Rohmaterial dafür — ein Offline-Validate(), das eine unveränderliche LicenseInfo zurückgibt, und ein gecachtes Lease, sodass Sie nie wieder ins Netz müssen. Siehe Entitlements und Feature-Flags für das Capability-Modell, das der Snapshot trägt, Ablauf pro Entitlement dafür, warum die Auswertung pro Aufruf bleiben muss, und Fail-closed-Gating dafür, wozu sich der Snapshot auflöst, wenn eine Lizenz fehlt oder widerrufen ist.

Nebula.NET testen

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