How to add software licensing to a .NET app A developer detailed a method for adding software licensing to .NET applications using the Keyright.NET SDK, in which a server signs a JSON license payload with a private RSA key while the app verifies it offline against an embedded public key. The approach gates paid features on tier entitlements and can be paired with online activation to enforce seat counts and key revocation, though the writeup notes offline validation alone is only a speed bump against determined attackers. To add licensing to a .NET app you issue a signed license key from a server, embed the matching public key in your app, and verify that key offline against it at startup — then gate your paid features on the tier or entitlements the license carries. If you need to enforce seat counts or revoke keys in the field, you layer online activation on top. This how-to walks the whole path with light, illustrative code using the Keyright.NET SDK https://delta1labs.com/docs/keyright/integration-dotnet , and it is honest about which parts are real enforcement and which are only a speed bump. Licensing built on public-key signatures has one asymmetry doing all the work. A private key lives on your server and is the only thing that can create a valid license. The matching public key ships inside your app and can only verify signatures — it can never mint them. That's why embedding the public key in your binary is safe: an attacker can decompile your app, read the public key, even publish it, and still cannot forge a license, because forging requires the private key they don't have. So the flow is: your server signs a license a small JSON payload — licensee, product, tier, seats, expiry, entitlements — plus an RSA signature over it ; your app verifies that signature offline against the embedded public key; and everything you unlock hangs off the fields inside a license you've confirmed is authentic. Keys are minted server-side. With Keyright you issue one from the dashboard or the admin API against a product and tier: curl -X POST $BASE/admin/licenses -H "X-Admin-Token: $TOKEN" -H "content-type: application/json" \ -d '{"licensee":"Acme Inc.","product":"acme-app","tier":"pro","seats":3,"email":"owner@acme.com"}' - { "id": "LIC-XXXXXXXX...", ... } this is the key the customer pastes into your app The tier here pro carries an entitlement template — the named flags and limits every license of that tier inherits, which you'll check in Step 4. The private signing key that signs this license never leaves the server. Grab your tenant's public key the dashboard's Integration tab, or GET /admin/public-key and paste it into the SDK options. Construct exactly one client at startup: using Keyright.Client; static readonly KeyrightClient License = KeyrightClient.Initialize new KeyrightOptions { Product = "acme-app", // must match the slug you issue keys for PublicKeyBase64 = "MIIBIjANBgkq...", // the public key from Step 1's tenant ServiceUrl = "https://keyright.delta1labs.com", // omit if you ship offline files only } ; Product and PublicKeyBase64 are the only required options. ServiceUrl is only needed if you'll activate online Step 5 . This public key is not a secret — it ships in your compiled binary and all the security comes from the private key staying server-side. Call Validate . It finds the best available license an explicit string, a license file, an env var, or a cached activation lease , verifies the RSA signature, the product match, the node-lock, expiry, an optional shipped revocation list, and trial/clock state — all offline , with no network call . Critically, it never throws : on any problem it returns a LicenseInfo in the Free edition carrying the reason. LicenseInfo info = License.Validate ; if info.IsPaid // true for any edition above Free { // unlock paid features } Being honest here: an offline check runs entirely on the user's machine, so a determined attacker can patch it out. Offline validation is the right tool for air-gapped and enterprise installs and for a fast startup check, but on its own it is a speed bump. Real enforcement comes from pairing it with online activation Step 5 so seats and revocation are decided server-side. Rather than branching on the edition name, gate individual features on entitlements — named flags and numeric limits baked into the license by its tier. That way you can change what a plan unlocks from the dashboard without shipping new code: // Boolean flag if License.IsEnabled "export" ShowExportCommand ; // Numeric limit — you pass the fail-closed fallback long maxProjects = License.GetLimit "max-projects", fallback: 1 ; if currentProjectCount = maxProjects PromptToUpgrade ; Both IsEnabled and GetLimit validate on the spot and fail closed : a missing flag reads as disabled, and a missing or unparseable limit returns the fallback you supply. If you're checking several entitlements at once, validate once and reuse the result: LicenseInfo info = License.Validate ; bool canExport = info.Entitlements.IsEnabled "export" ; long maxSeats = info.Entitlements.GetLimit "max-seats", 1 ; When a customer pastes their key, call ActivateAsync . It posts the key plus a stable machine id to your service, which consumes a seat and returns a short-lived signed lease bound to that machine. The SDK verifies the lease against your embedded public key and caches it locally , so every later Validate succeeds offline until the lease's grace window elapses. LicenseInfo info = await License.ActivateAsync customerEnteredKey, ct ; if info.IsValid && info.IsPaid { // Activated. Lease cached; the app now works offline until it nears expiry. ShowLicensedUi info.StatusBadge ; // e.g. "Pro" or "Enterprise Trial" } else { ShowActivationError info.Message ; // "All seats for this license are in use.", etc. } Like Validate , ActivateAsync does not throw for ordinary failures bad key, seat limit exhausted, offline, revoked — it returns a fail-closed result you inspect. A few properties worth knowing: ServiceUrl or an empty key. This is the step that turns "a check on the honor system" into real enforcement: the seat count and revocation status are decided on a server you control, not on the attacker's machine. Trials flow through the exact same activation path — there's no separate trial code in your app. A trial key returns a lease with IsTrial set, and you surface it straight off LicenseInfo : if info.IsTrial ShowBadge $"{info.StatusBadge} — {info.DaysRemaining} days left" ; A trial counts down from first activation and is superseded seamlessly when the customer later activates a paid key — no reinstall. You can even let customers start a trial from your own marketing site with one public API call; see self-service free trials https://delta1labs.com/docs/keyright/trials/ . Revocation is how you kill a leaked or refunded key. Revoke it server-side POST /admin/licenses/{id}/revoke or a dashboard click and the client drops to Free on its next lease refresh. For purely offline apps, ship a signed revocation list with your build so even a disconnected client honors it. The most important test is that things break toward locked, not open: Validate returns Free and paid features stay locked. ClockTampered and refuses to validate until the time is corrected. Adding licensing to a .NET app is two moving parts: verify a signed key offline against an embedded public key, and — when you need seats or revocation — activate online for a short-lived lease. Client-side checks alone are a speed bump that an attacker can eventually patch; the real enforcement is the server deciding seats and revocation, with the SDK failing closed everywhere in between. The Keyright.NET SDK https://delta1labs.com/docs/keyright/integration-dotnet gives you both halves — offline Validate and online ActivateAsync with entitlements, trials, node-locking, and revocation — and multi-targets from .NET Framework 4.8 to current .NET. You can wire the whole flow end to end on the free plan before paying anything.