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

Eine aktivierte Lizenz auf dem Client speichern: DPAPI, manipulationssicheres Caching und was man nicht vertrauen darf

Die Aktivierung gibt dem Client ein signiertes Lease; dann kommt die Frage, die niemand einplant: Wo legt man es ab? In eine einfache Datei geschrieben, editiert ein Benutzer das Ablaufdatum; schlecht verschlüsselt, hat man die App aus Versehen an eine einzige Maschine gebunden. Der Cache dient der Verfügbarkeit, nicht der Authentizität: Die Signatur entscheidet weiterhin, ob ein Lease echt ist, also hat der Speicher nur drei Aufgaben: einen Neustart überstehen, beiläufiges Editieren abwehren und ein Lease verweigern, das von einer anderen Maschine kopiert wurde. Hier erfahren Sie, wie Sie ein gecachtes Lease mit DPAPI versiegeln, wo es das gibt, es per HMAC an das Gerät binden, damit ein editierter oder transplantierter Cache erkannt wird, und warum Sie dem gecachten Ablaufdatum niemals mehr vertrauen dürfen als dem signierten.

Die Aktivierung ist der Teil, den alle sorgfältig entwerfen: Der Client weist sich aus, der Server stellt ein signiertes Lease aus, die App verifiziert es und läuft. Dann muss das Lease irgendwo leben, und das ist der Teil, der das sorgfältige Design leise untergräbt. Schreiben Sie es in eine einfache Datei, und ein Benutzer editiert das Ablaufdatum um ein Jahrzehnt nach hinten. Schützen Sie es ungeschickt vor Kopien, und Sie haben die App aus Versehen an eine Maschine gebunden, sodass eine Neuinstallation einen zahlenden Kunden aussperrt. Die Speicherschicht sieht aus wie Klempnerei und verhält sich wie Policy.

Die klärende Idee ist diese: Der Cache dient der Verfügbarkeit, nicht der Authentizität. Das signierte Lease ist bereits manipulationssicher – ändern Sie einen Claim, und die Signatur verifiziert nicht mehr. Der lokale Speicher hat also genau drei Aufgaben: einen Neustart überstehen, damit die App offline startet; beiläufiges Editieren abwehren, damit ein Benutzer nicht trivial daran herumspielen kann; und ein von einer anderen Maschine transplantiertes Lease verweigern. Er entscheidet nicht, ob das Lease gültig ist; das tut die Signatur. Dieser Beitrag zeigt, wie Keyright ein aktiviertes Lease persistiert: Versiegelung mit DPAPI, wo es das gibt, Bindung an das Gerät per HMAC und die eine Regel, die das Ganze ehrlich hält – dem gecachten Ablaufdatum niemals mehr vertrauen als dem signierten.

Die Form dessen, was Sie speichern

Nach der Aktivierung halten Sie ein signiertes Lease – Claims plus Signatur – und einige Metadaten darüber, wann es aufgefrischt werden soll. Speichern Sie das Lease unverändert; alles andere ist ein Hinweis:

public sealed record CachedLease(
    string  SignedLease,      // the lease exactly as issued: claims + signature, opaque to the client
    DateTimeOffset CachedAt,  // when we wrote it — a REFRESH hint, never an authority on validity
    string  DeviceTag);       // HMAC over SignedLease, keyed by the device fingerprint

Beachten Sie, was hier nicht steht: kein Klartext-expiresOn, kein tier, kein isValid-Boolean. In dem Moment, in dem Sie eine bequeme unsignierte Kopie eines Claims speichern, haben Sie etwas geschaffen, das ein Benutzer editieren kann und dem Ihr Code versucht sein könnte zu vertrauen. Das einzige Claims tragende Feld ist SignedLease, und Sie lesen Claims daraus erst nach Verifikation der Signatur.

Im Ruhezustand mit DPAPI versiegeln (wo es das gibt)

Unter Windows verschlüsselt ProtectedData den Blob mit einem aus den Anmeldedaten des aktuellen Benutzers abgeleiteten Schlüssel. Das bringt kostenlos Vertraulichkeit im Ruhezustand und eine schwache Geräte-/Benutzerbindung – dieselben Bytes lassen sich als anderer Benutzer oder auf einer anderen Maschine nicht entschlüsseln:

static byte[] Protect(byte[] plaintext) =>
    OperatingSystem.IsWindows()
        ? ProtectedData.Protect(plaintext, optionalEntropy: null, DataProtectionScope.CurrentUser)
        : KeychainOrFingerprintEncrypt(plaintext);   // Linux/macOS fallback

static byte[] Unprotect(byte[] sealed_) =>
    OperatingSystem.IsWindows()
        ? ProtectedData.Unprotect(sealed_, optionalEntropy: null, DataProtectionScope.CurrentUser)
        : KeychainOrFingerprintDecrypt(sealed_);

ProtectedData ist Windows-exklusiv – unter Linux und macOS wirft es eine PlatformNotSupportedException, also nutzt der Fallback den Schlüsselbund des Betriebssystems oder einen aus dem Geräte-Fingerabdruck abgeleiteten Schlüssel. Seien Sie ehrlich darüber, was diese Schicht ist: Vertraulichkeit im Ruhezustand, nicht Authentizität. Ein Benutzer mit eigenem Login kann seinen eigenen Cache weiterhin entschlüsseln. Das ist in Ordnung, denn Verschlüsselung war nie das, was ihn vom Editieren eines Lease abhält – das ist die Signatur.

Die Kopie an das Gerät binden

Die Gerätebindung von DPAPI ist nebensächlich und Windows-exklusiv; machen Sie sie explizit und plattformübergreifend mit einem HMAC über das gespeicherte Lease, geschlüsselt mit einem aus dem Geräte-Fingerabdruck abgeleiteten Wert. Beim Laden berechnen Sie ihn aus dem Fingerabdruck dieser Maschine neu und vergleichen:

static string DeviceTag(string signedLease, DeviceFingerprint fp)
{
    using var h = new HMACSHA256(fp.DeriveKey());          // key bound to this device
    var mac = h.ComputeHash(Encoding.UTF8.GetBytes(signedLease));
    return Convert.ToBase64String(mac);
}

static bool BelongsHere(CachedLease c, DeviceFingerprint fp) =>
    CryptographicOperations.FixedTimeEquals(
        Convert.FromBase64String(c.DeviceTag),
        Convert.FromBase64String(DeviceTag(c.SignedLease, fp)));

Kopieren Sie die gesamte Cache-Datei auf eine andere Maschine, und der Fingerabdruck unterscheidet sich, sodass das neu berechnete Tag nicht zu c.DeviceTag passt; BelongsHere liefert false, Sie verwerfen den Cache, und die App aktiviert sich erneut – was der Server verweigern kann, wenn die neue Maschine die Sitzplatzanzahl überschreitet. Verwenden Sie FixedTimeEquals, nicht ==, damit der Vergleich kein Timing preisgibt. Das ist nicht unknackbar: Ein Angreifer, der auch die Fingerabdruck-Eingaben repliziert, umgeht es. Der Sinn ist, aus „eine Datei kopieren” ein „auch die Geräteidentität klonen” zu machen – genau die Kostensteigerung, für die Node-Locking existiert.

Der Ladepfad, in der einzig sicheren Reihenfolge

Bringen Sie die drei Aufgaben in eine Sequenz, und die eine Regel – Authentizität kommt von der Signatur, nie vom Cache – ergibt sich von selbst:

1 · Datei lesen + per DPAPI entschlüsseln2 · Geräte-HMAC neu berechnen, vergleichen3 · Lease-SIGNATUR verifizieren4 · Ablauf + Stufe aus CLAIMS lesendas verifizierte Lease – die Quelle der Wahrheitein Tor scheitert →Cache verwerfen,online neu aktivieren
public LicenseClaims LoadOrActivate(DeviceFingerprint fp)
{
    if (TryReadCache(out var raw) &&
        TryUnprotect(raw, out var cached) &&          // gate 1: at-rest seal intact
        BelongsHere(cached, fp))                       // gate 2: this machine
    {
        if (_keySet.TryVerify(cached.SignedLease, out var claims))  // gate 3: authenticity
            return claims;                             // gate 4: trust CLAIMS, not the file
    }
    return Activate(fp);   // any failure: re-activate; the server enforces seats + revocation
}

Die Regel, die es ehrlich hält

Lesen Sie das Ablaufdatum, das Sie durchsetzen, aus claims – dem Ergebnis einer erfolgreichen Signaturverifikation – und nirgendwo sonst. Das CachedAt des Caches ist nur nützlich, um zu entscheiden, wann aufgefrischt wird, nie ob die Lizenz gültig ist. In dem Moment, in dem Sie „jetzt” gegen einen unsignierten Zeitstempel neben dem Blob vergleichen, haben Sie dem Benutzer ein Feld zum Editieren in die Hand gegeben. Datums-Rollback ist ein eigener Angriff mit eigener Verteidigung (der Uhr vertrauen); der Beitrag der Speicherschicht besteht schlicht darin, niemals ein unsigniertes Ablaufdatum einzuführen, an dem jemand manipulieren könnte.

Dieselbe Disziplin gilt für Fehlschläge. Scheitert ein Tor, flicken Sie den Cache nicht – Sie verwerfen ihn und aktivieren erneut. Die erneute Aktivierung ist der Moment, in dem der Server Sitzplatzlimits und Widerruf anwenden kann, sodass ein transplantierter oder editierter Cache nicht nur lokal scheitert; er leitet den Client zurück durch die eine Prüfung, die „nein” sagen kann.

Was Sie mitnehmen sollten

Eine aktivierte Lizenz muss persistiert werden, und der Speicher ist der Ort, an dem sorgfältige Lizenzierung leise leckt, wenn Sie es zulassen. Beschränken Sie den Cache auf seine eigentliche Aufgabe – Verfügbarkeit, nicht Authentizität. Speichern Sie das signierte Lease unverändert, ohne bequeme unsignierte Kopien seiner Claims. Versiegeln Sie es im Ruhezustand mit DPAPI unter Windows (und einem Schlüsselbund oder fingerabdruck-abgeleiteten Schlüssel anderswo), binden Sie die Kopie per HMAC an das Gerät, damit eine transplantierte Datei erkannt wird, und verifizieren Sie die Signatur, bevor Sie einen einzigen Claim lesen. Setzen Sie das Ablaufdatum aus den verifizierten Claims durch, behandeln Sie die Zeitstempel der Datei nur als Refresh-Hinweise, und verwerfen Sie bei jedem Fehlschlag und aktivieren erneut, damit der Server die Autorität über Sitzplätze und Widerruf bleibt. Tun Sie das, und die Speicherschicht hört auf, die weiche Stelle unter einem ansonsten soliden Aktivierungsdesign zu sein.

Nebula.NET testen

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