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

Ein Lizenzschlüssel, drei Produkte: der Product-Claim, Suite-Bundles als Entitlements und das Gate, durch das ein Lite-Schlüssel Pro freischaltet

Sie liefern einen Designer, einen Renderer und eine CLI aus, und Ihr Kunde möchte einen Schlüssel für alle drei, nicht drei. Die naheliegende Abkürzung – jeder App denselben Product-Slug geben und den Rest die Signatur erledigen lassen – ist genau die Abkürzung, die Ihre teuerste App klammheimlich zum Preis Ihrer günstigsten verkauft, denn ein Schlüssel, der für die Produktlinie verifiziert, verifiziert in jeder App der Linie. Dieser Beitrag liest, was der Product-Claim einer Keyright-Lizenz tatsächlich prüft (es ist eine Audience-Prüfung, keine Feature-Prüfung), legt die drei Wege dar, eine Suite zu lizenzieren, zeigt das Loch im Shared-Slug-Entwurf mitsamt dem exakten Gate, das es öffnet, und baut die Variante, die hält: ein Slug für die Linie, die App-Zugehörigkeit als namensraumierte Entitlements und ein Gate in jeder App, das fragt „bin ich in diesem Bundle?“, bevor es irgendetwas anderes fragt. Er endet mit dem Upgrade-Pfad vom Einzel-App-Schlüssel zum Suite-Schlüssel und dem Zwei-Client-Fallback für Teams, die bereits einen Slug pro App ausgeliefert haben.

Sie liefern drei Dinge aus: einen Designer, einen Renderer und eine CLI. Sie werden einzeln als Lite-Stufen und gemeinsam als Suite verkauft, und das Erste, was ein Suite-Kunde fragt, ist, warum er drei Lizenzschlüssel in drei Apps einfügen muss. Berechtigte Frage. Ein Schlüssel pro Kunde ist die richtige Antwort, und der Weg, auf dem die meisten Teams dorthin gelangen – jeder App denselben Product-Slug geben und den Rest die Signatur erledigen lassen –, ist der Weg, der Ihren Renderer klammheimlich zum Preis Ihres Designers verkauft.

In diesem Beitrag geht es um das Gate, das einen Schlüssel pro Kunde hält: was der Product-Claim einer Keyright-Lizenz wirklich prüft, wo die Shared-Slug-Abkürzung leckt und wie App-Zugehörigkeit als Entitlements das Leck schließt.

Der Product-Claim ist eine Audience-Prüfung

Jede Keyright-Lizenz trägt einen Product-Slug, der mit dem Rest der Nutzlast signiert ist. Jeder Keyright-Client trägt den Slug, den er erwartet. Die beiden treffen bei der Validierung aufeinander:

static readonly KeyrightClient License = KeyrightClient.Initialize(new KeyrightOptions
{
    Product         = "acme-renderer",                     // the audience THIS app accepts
    PublicKeyBase64 = AcmeKeys.LicensePublicKey,           // public key only; safe to ship
    ServiceUrl      = "https://licensing.acme.example",
});

LicenseInfo info = License.Validate();   // fails closed if the key's product != "acme-renderer"

Fügt ein Kunde einen Designer-Schlüssel in den Renderer ein, ist die Signatur in Ordnung, das Ablaufdatum ist in Ordnung, und die Validierung schlägt trotzdem fehl – info.IsValid ist false und info.Message besagt, dass der Schlüssel für ein anderes Produkt ausgestellt wurde. Das ist genau die Rolle eines aud-Claims in einem JWT: ist dieses Token für mich bestimmt? Es wird geprüft, bevor irgendein anderer Claim gelesen wird.

Was der Product-Claim nicht tut, ist zu sagen, wofür der Lizenznehmer bezahlt hat. Er ist Identität, nicht Berechtigung. Halten Sie an dieser Unterscheidung fest, denn das Loch weiter unten entsteht dadurch, dass man den Product-Claim eine Aufgabe erledigen lässt, für die er nie entworfen wurde.

Drei Wege, eine Suite zu lizenzieren

Es gibt nur drei Formen, und zwei davon sind aus den falschen Gründen verlockend.

Ein Slug pro App, drei Schlüssel. acme-designer, acme-renderer, acme-cli, jeweils separat ausgestellt. Das ist sicher – ein Designer-Schlüssel kann den Renderer nicht anrühren – und es ist das, was Sie standardmäßig erhalten, wenn Sie Apps nach und nach mit Lizenzierung versehen. Der Preis ist das Support-Ticket, mit dem wir begonnen haben: drei Schlüssel zum Ausliefern, drei zum Einfügen, drei zum Verlängern.

Ein Slug für die Linie, sonst nichts. Überall acme-suite; der Lite-Kunde erhält einen acme-suite-Schlüssel mit weniger Feature-Entitlements, der Suite-Kunde einen acme-suite-Schlüssel mit allen. Ein Schlüssel pro Kunde. Das ist der, der leckt, und der nächste Abschnitt zeigt genau, wie.

Ein Slug für die Linie, plus Zugehörigkeits-Entitlements pro App. Überall acme-suite, und der Schlüssel sagt, welche Apps er abdeckt. Das ist der Entwurf, der überlebt, und er kostet einen IsEnabled-Aufruf pro App.

Das Loch, konkret

Nehmen Sie die zweite Form. Hier ist ein Lite-Schlüssel – nur Designer –, ausgestellt unter dem Slug der Linie. An ihm ist nichts falsch:

{
  "product":  "acme-suite",
  "licensee": "acct_8f21",
  "expiry":   "2027-10-16T00:00:00Z",
  "entitlements": {
    "export":   true,
    "max-dpi":  300
  }
}

Und hier ist das Gate des Renderers, geschrieben so, wie Gates üblicherweise geschrieben werden – validieren, dann auf IsValid verzweigen:

// Renderer: initialised with Product = "acme-suite"
LicenseInfo info = License.Validate();

if (info.IsValid)             // signature ok, product ok, not expired → true
    RunLicensed();            // ...and the Lite customer is now running the Renderer
else
    RunEvaluation();

Gehen Sie es durch. Das Produkt des Schlüssels ist acme-suite; der Renderer erwartet acme-suite; die Signatur verifiziert; das Ablaufdatum liegt ein Jahr in der Zukunft. Jede Prüfung besteht. IsValid ist true, und der Renderer läuft voll lizenziert auf einem Schlüssel, der als „nur Designer“ verkauft wurde. Keine Fälschung, keine Manipulation – der Kunde hat die günstigste SKU gekauft, und der Product-Claim hat, indem er genau seine Aufgabe erledigte, dem Renderer mitgeteilt, der Schlüssel sei für ihn bestimmt. Das war er. Der Schlüssel hat nur nie gesagt, welche Apps er abdeckt, und niemand hat gefragt.

Ein Schlüssel, drei Apps: Der Product-Claim ist überall erfülltLite-Schlüssel (signiert)product: acme-suiteapp:designer = trueapp:cli = true(kein app:renderer)Designer · Product=acme-suiteIsValid ✓ IsEnabled("app:designer") ✓CLI · Product=acme-suiteIsValid ✓ IsEnabled("app:cli") ✓Renderer · Product=acme-suiteIsValid ✓ (Product-Claim erfüllt)Gate nur auf IsValid → SCHALTET FREIGate auf app:renderer → verweigertlizenziertlizenziertLeckkorrektZwei FragenProduct-Claim:„ist dieser Schlüsselfür meine Linie?“Zugehörigkeits-Entitlement:„ist DIESE App imBundle?“beide sind signiert;keines lässt sichvon Hand ändern

Das Gate, das hält: Zugehörigkeit als Entitlement

Die Lösung ist nicht ein Slug pro App – das wirft das Ein-Schlüssel-Ziel weg. Sie besteht darin, das Bundle auf den Schlüssel zu legen, als Entitlements, und jede App ihr eigenes prüfen zu lassen. Der Suite-Schlüssel:

{
  "product":  "acme-suite",
  "licensee": "acct_8f21",
  "expiry":   "2027-10-16T00:00:00Z",
  "entitlements": {
    "app:designer": true,
    "app:renderer": true,
    "app:cli":      true,
    "export":       true,
    "max-dpi":      1200
  }
}

Der Lite-Schlüssel hat dieselbe Form mit nur gesetztem app:designer. Das app:-Präfix ist eine Konvention, kein Keyright-Feature – es hält Zugehörigkeit sichtbar getrennt von Features wie export und max-dpi, damit niemand später „welche Apps“ mit „welche Fähigkeiten“ verwechselt.

Jede App stellt nun zwei Fragen, der Reihe nach, und die zweite ist das eigentliche Gate:

// Renderer. Same client, same slug — plus the question that was missing.
LicenseInfo info = License.Validate();

bool licensed = info.IsValid                       // 1. is this key for my product line?
             && License.IsEnabled("app:renderer"); // 2. is THIS app in the bundle?

if (!licensed) { RunEvaluation(info); return; }

// Feature gates come after membership, never instead of it.
if (License.IsEnabled("export"))             EnableExport();
int maxDpi = (int)License.GetLimit("max-dpi", fallback: 150);

Lassen Sie den Lite-Schlüssel durch diesen Renderer laufen. Der Product-Claim besteht, die Signatur besteht, IsEnabled("app:renderer") ist false, licensed ist false. Der Designer, der IsEnabled("app:designer") fragt, schaltet frei. Ein Schlüssel; jede App sieht nur das Stück, das ihr verkauft wurde. Und weil die Entitlements innerhalb der signierten Nutzlast liegen, kann ein Kunde einem Lite-Schlüssel ebenso wenig "app:renderer": true hinzufügen, wie er das Ablaufdatum hinausschieben kann – ein geändertes Byte, und die Signatur schlägt fehl.

Halten Sie die Prüfung pro App auf genau eine Zeile beschränkt, an genau einer Stelle, und führen Sie sie vor jedem Feature-Gate aus. Wenn je ein Feature-Gate läuft, ohne dass das Zugehörigkeits-Gate bestanden wurde, haben Sie das Loch mit zusätzlichen Schritten neu gebaut.

Ausstellung: eine SKU, ein Entitlement-Satz

Auf der Ausstellungsseite ist eine SKU nur ein benannter Entitlement-Satz, also sind die drei Stufen drei Vorlagen, nicht drei Produkte:

{
  "product": "acme-suite",
  "skus": {
    "designer-lite": { "app:designer": true, "export": false, "max-dpi": 300  },
    "renderer-lite": { "app:renderer": true, "export": true,  "max-dpi": 600  },
    "suite":         { "app:designer": true, "app:renderer": true, "app:cli": true,
                       "export": true, "max-dpi": 1200 }
  }
}

Einen Kunden von Designer Lite auf Suite hochzustufen, ist eine Neuausstellung unter demselben Slug mit dem suite-Satz. Die Apps ändern sich nicht, nichts wird neu ausgerollt, und der Kunde fügt einen neuen Schlüssel in eine beliebige der drei Apps ein – die gespeicherte Lizenz wird geteilt – und alle drei leuchten auf.

Wenn Sie bereits einen Slug pro App ausgeliefert haben

Viele Teams haben Lizenzierung App für App hinzugefügt und haben acme-designer-, acme-renderer- und acme-cli-Schlüssel im Feld. Sie können trotzdem ohne Stichtag zu einem Schlüssel pro Kunde gelangen. Jede App akzeptiert weiterhin ihren Legacy-Slug und akzeptiert zusätzlich den Suite-Slug – aber nur mit ihrem eigenen Zugehörigkeits-Entitlement:

static readonly KeyrightClient Suite  = KeyrightClient.Initialize(new KeyrightOptions
    { Product = "acme-suite",    PublicKeyBase64 = AcmeKeys.LicensePublicKey, ServiceUrl = Url });
static readonly KeyrightClient Legacy = KeyrightClient.Initialize(new KeyrightOptions
    { Product = "acme-renderer", PublicKeyBase64 = AcmeKeys.LicensePublicKey, ServiceUrl = Url });

static (LicenseInfo info, KeyrightClient via) Gate()
{
    var s = Suite.Validate();
    if (s.IsValid && Suite.IsEnabled("app:renderer")) return (s, Suite);   // suite key, in bundle

    var l = Legacy.Validate();
    if (l.IsValid) return (l, Legacy);                                      // old per-app key

    return (s, Suite);                                                      // fail closed; report the suite diagnosis
}

Neue Kunden erhalten Suite-Schlüssel. Bestehende Kunden arbeiten weiter. Wenn ein Legacy-Kunde upgradet, stellen Sie einen Suite-Schlüssel aus, und der Legacy-Pfad ist schlicht nicht mehr der, der zuerst antwortet. Verfolgen Sie über Ihre Ausstellungs-Aufzeichnungen, wie viele Legacy-Schlüssel verbleiben, und wenn es null erreicht, löschen Sie Legacy. Das Einzige, was Sie nicht tun dürfen, ist, eine Legacy-App zu „vereinfachen“, indem Sie ihren einzigen Client auf den Suite-Slug umstellen und das Gate auf IsValid belassen – das ist wieder die zweite Form, mit dem Loch.

Testen Sie das Leck, nicht nur den Happy Path

Der Test, auf den es ankommt, ist der, der den Shared-Slug-Entwurf abgefangen hätte. Stellen Sie einen Lite-Schlüssel mit einem Test-Signaturschlüssel aus und prüfen Sie, dass das Gate des Renderers ihn verweigert:

[Fact]
public void Renderer_refuses_a_designer_only_key()
{
    string lite = TestIssuer.Issue(product: "acme-suite",
        entitlements: new() { ["app:designer"] = true, ["export"] = true });
    string path = Path.Combine(Path.GetTempPath(), $"{Guid.NewGuid():N}.lic");
    File.WriteAllText(path, lite);

    var renderer = KeyrightClient.Initialize(new KeyrightOptions
        { Product = "acme-suite", PublicKeyBase64 = TestIssuer.PublicKeyBase64, LicenseFilePath = path });

    var info = renderer.Validate();
    Assert.True(info.IsValid);                           // product claim passes — by design
    Assert.False(renderer.IsEnabled("app:renderer"));   // membership does not — the real gate
}

Beachten Sie, was die erste Assertion dokumentiert: IsValid ist true, und das ist korrekt. Der Test existiert, um sicherzustellen, dass niemand je einen Build ausliefert, in dem das das Einzige ist, was der Renderer prüft. Paaren Sie ihn mit dem Gegenteil – ein Suite-Schlüssel aktiviert alle drei – und mit einem gefälschten Ausstellungsdienst, falls Sie auch den Aktivierungs-Roundtrip abgedeckt haben wollen.

Das Fazit

Der Product-Claim einer Lizenz ist eine Audience-Prüfung – „ist dieser Schlüssel für meine Produktlinie?“ – und er wird zuerst geprüft, vor jedem anderen Claim, was ihn genau zum falschen Werkzeug dafür macht, zu entscheiden, welche Apps dieser Linie ein Kunde gekauft hat. Geben Sie der ganzen Linie einen Slug, damit ein Kunde einen Schlüssel hält, legen Sie das Bundle als Zugehörigkeits-Entitlements pro App auf den Schlüssel, und lassen Sie jede App auf ihre eigene Zugehörigkeit gaten, bevor irgendein Feature-Gate läuft. Das verwandelt Suites, Lite-Stufen und Upgrades auf der Ausstellungsseite in Entitlement-Sätze, hält die Ein-Zeilen-Prüfung in jeder App und schließt die Abkürzung, die andernfalls Ihren Renderer zum Preis Ihres Designers verkaufen würde.

Nebula.NET testen

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