<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Zero Heartbeat</title>
    <description>The latest articles on DEV Community by Zero Heartbeat (@zero_heartbeat_06a3625d7a).</description>
    <link>https://dev.to/zero_heartbeat_06a3625d7a</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3782380%2Fa9edbbf7-36b9-434e-8c5d-238ab3ad0caf.png</url>
      <title>DEV Community: Zero Heartbeat</title>
      <link>https://dev.to/zero_heartbeat_06a3625d7a</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/zero_heartbeat_06a3625d7a"/>
    <language>en</language>
    <item>
      <title>Licensing for Air-Gapped and Offline Environments</title>
      <dc:creator>Zero Heartbeat</dc:creator>
      <pubDate>Tue, 29 Sep 2026 14:25:40 +0000</pubDate>
      <link>https://dev.to/zero_heartbeat_06a3625d7a/licensing-for-air-gapped-and-offline-environments-4j09</link>
      <guid>https://dev.to/zero_heartbeat_06a3625d7a/licensing-for-air-gapped-and-offline-environments-4j09</guid>
      <description>&lt;p&gt;The most nerve-wracking licensing question I've ever been asked came from a defence contractor: "Our production network has no internet, ever, and it never will. Can your licensing work in there?" It's a fair question that trips up a surprising number of licensing products, because so many of them quietly assume a machine can phone home. In a truly air-gapped environment — a classified network, an isolated industrial control system, a secure facility — there is no phone home. There is a USB stick and a security officer who inspects what's on it.&lt;/p&gt;

&lt;p&gt;The reassuring answer is that offline licensing isn't a degraded mode; done right, it's a fully legitimate one, built on the same cryptography as everything else. This post explains how air-gapped and offline activation actually works: signed license blobs, the manual activation dance, expiry and grace without a clock server, and revocation with no network.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Key takeaway:&lt;/strong&gt; Air-gapped licensing works because trust comes from &lt;em&gt;cryptography, not connectivity&lt;/em&gt;. A signed license blob verifies locally against an embedded public key with zero network calls, so a fully disconnected machine can prove a license is authentic, unaltered, bound to it, and unexpired — and reject a revoked one via a signed list you ship out of band.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why offline validation is possible at all
&lt;/h2&gt;

&lt;p&gt;If you've read &lt;a href="https://delta1labs.com/blog/offline-license-validation-explained/" rel="noopener noreferrer"&gt;offline license validation explained&lt;/a&gt;, you know the core asymmetry, so I'll keep this brief and build on it toward the air-gapped case. Your server holds a &lt;strong&gt;private key&lt;/strong&gt; that can &lt;em&gt;create&lt;/em&gt; signatures; the client embeds only the matching &lt;strong&gt;public key&lt;/strong&gt;, which can &lt;em&gt;verify&lt;/em&gt; a signature but never forge one. Verifying a signature is pure math on data the app already holds, so the client can confirm a license was signed by you — and hasn't been altered by a single byte — with no live server. That one property is what makes air-gapped licensing real rather than a compromise.&lt;/p&gt;

&lt;p&gt;Everything in this post is an application of that idea to the hardest deployment: a machine that will &lt;em&gt;never&lt;/em&gt; see the network, not even once.&lt;/p&gt;

&lt;h2&gt;
  
  
  The signed license blob
&lt;/h2&gt;

&lt;p&gt;The unit of an offline license is a &lt;strong&gt;signed blob&lt;/strong&gt;: a small payload of facts with a signature over it. Conceptually:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"licensee"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Acme Defense Systems"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"product"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"acme-analyzer"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"tier"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"enterprise"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"seats"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"expiryUtc"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2027-09-29T00:00:00Z"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"machineFingerprint"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"b3f1…c9a2"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"entitlements"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"export"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"true"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"max-projects"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"50"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"signature"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"MEUCIQ…"&lt;/span&gt;&lt;span class="w"&gt;   &lt;/span&gt;&lt;span class="err"&gt;//&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;over&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;every&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;field&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;above&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;made&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;with&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;the&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;vendor's&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;private&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;key&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The client verifies the signature against its embedded public key, and only then reads the now-trusted fields. Flip &lt;code&gt;tier&lt;/code&gt; from &lt;code&gt;enterprise&lt;/code&gt; to anything else, or edit &lt;code&gt;expiryUtc&lt;/code&gt;, and the signature no longer matches — validation fails. The &lt;code&gt;machineFingerprint&lt;/code&gt; field is what node-locks the blob to one machine so the same file can't be copied across an air-gapped fleet (the fingerprinting and swap-tolerance details are in &lt;a href="https://delta1labs.com/blog/node-locking-seat-management/" rel="noopener noreferrer"&gt;node-locking and seat management&lt;/a&gt;).&lt;/p&gt;

&lt;h2&gt;
  
  
  The manual activation flow
&lt;/h2&gt;

&lt;p&gt;Here's the part unique to air-gapped operation: how does a machine that can't make a request &lt;em&gt;get&lt;/em&gt; a signed blob bound to its fingerprint? Through a file exchange — the offline activation dance:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Generate a request on the offline machine.&lt;/strong&gt; The app produces a small request file containing the machine's fingerprint (and the license key or order reference). No network involved; it just writes a file.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Carry it out.&lt;/strong&gt; A USB stick, an approved file-transfer channel, whatever the facility permits. The request goes to a connected machine.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Submit it to the issuing service.&lt;/strong&gt; From the connected machine — a browser hitting the vendor's portal, or an admin console — the request is submitted. The service validates it, consumes a seat if appropriate, and returns a &lt;strong&gt;signed activation file&lt;/strong&gt; bound to that exact fingerprint.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Carry it back and import.&lt;/strong&gt; The signed file returns to the offline machine on the same channel and is imported. The app verifies the signature locally and activates.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;From then on the machine validates entirely offline, forever if need be. The connected step happened &lt;em&gt;once&lt;/em&gt;, on a &lt;em&gt;different&lt;/em&gt; machine, and moved only signed files — no live connection from the secure host ever existed. That's what satisfies a security officer: the air-gapped machine's network posture never changed.&lt;/p&gt;

&lt;p&gt;For deployments where even that round trip is impractical, the simpler variant is to skip the request/response and issue a &lt;strong&gt;pre-bound file&lt;/strong&gt; — the vendor issues a signed blob for a known fingerprint (or an unbound, non-node-locked file for a fixed number of installs) and ships it with the software. Less precise, but sometimes it's the only workable flow.&lt;/p&gt;

&lt;h2&gt;
  
  
  Expiry and the clock-tampering problem
&lt;/h2&gt;

&lt;p&gt;A subscription or trial carries an &lt;code&gt;expiryUtc&lt;/code&gt;, and offline validation just compares it to the current time — easy. The obvious attack is just as easy: roll the system clock backward and a trial never ends, or an expired license springs back to life. An air-gapped machine has no time server to appeal to, so the defence has to be local.&lt;/p&gt;

&lt;p&gt;The standard approach is &lt;strong&gt;monotonic time tracking&lt;/strong&gt;: the app remembers the latest time it has legitimately seen, and treats a clock that jumps meaningfully &lt;em&gt;backward&lt;/em&gt; as tampering, refusing to validate until the time is corrected. A small tolerance window (a day or so) absorbs honest clock adjustments and time-zone quirks without flagging them. Perpetual licenses, having no expiry, aren't subject to the check at all. It's not bulletproof — nothing client-side is — but it turns "just change the date" from a trivial bypass into something that visibly breaks the license.&lt;/p&gt;

&lt;h2&gt;
  
  
  Grace windows in disconnected setups
&lt;/h2&gt;

&lt;p&gt;Grace is usually discussed for &lt;em&gt;intermittently&lt;/em&gt; connected clients — cache a lease, survive a brief outage — and I covered that pattern in the offline-validation post. In a &lt;em&gt;fully&lt;/em&gt; air-gapped deployment the concept shifts: there's no lease to refresh, so "grace" becomes about how you handle the transition around expiry.&lt;/p&gt;

&lt;p&gt;Two humane practices matter here. First, &lt;strong&gt;warn early&lt;/strong&gt;: because there's no server to email a renewal reminder, the client itself should surface "your license expires in 14 days" well ahead, since re-issuing an air-gapped license is a multi-day physical process, not a one-click renewal. Second, consider a &lt;strong&gt;short read-only or reduced-function grace period&lt;/strong&gt; after expiry rather than an instant hard stop, so a lapsed license in a secure facility doesn't halt critical work while the paperwork for a new signed file works its way through. Both are about respecting that, in these environments, &lt;em&gt;renewal has real latency&lt;/em&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Revocation without a network
&lt;/h2&gt;

&lt;p&gt;The one thing offline genuinely can't do instantly is revoke. A blob signed last year knows nothing about a refund or leak last week; its payload is fixed at issue time. The offline answer is a &lt;strong&gt;signed revocation list&lt;/strong&gt;: a list of revoked license ids, itself signed by your private key so the client trusts it, distributed with a software update or as an out-of-band file the facility imports. The client checks incoming licenses against the list and drops revoked ones.&lt;/p&gt;

&lt;p&gt;It moves at the speed of your distribution — days or weeks, not seconds — but it means an air-gapped install isn't defenceless against a known-bad key. For most air-gapped customers, whose update cadence is deliberate anyway, that's an acceptable trade.&lt;/p&gt;

&lt;h3&gt;
  
  
  Being honest about the limits
&lt;/h3&gt;

&lt;p&gt;Offline validation proves authenticity, integrity, binding and expiry beautifully, and it's the &lt;em&gt;right&lt;/em&gt; tool for air-gapped and enterprise deployments — but it's not magic. The check runs on the customer's machine, so a determined attacker with a decompiler can patch it out, and offline revocation is never instant. That's not a reason to avoid offline licensing; it's a reason to be precise about its job and to pair the client check with obfuscation so patching costs real effort (see &lt;a href="https://delta1labs.com/blog/protect-dotnet-licensing-checks/" rel="noopener noreferrer"&gt;protecting your licensing checks&lt;/a&gt;). And remember the distinction from &lt;a href="https://delta1labs.com/blog/self-hosted-vs-cloud-licensing/" rel="noopener noreferrer"&gt;self-hosted vs cloud licensing&lt;/a&gt;: serving air-gapped &lt;em&gt;customers&lt;/em&gt; only needs signed offline files from your issuing service — you self-host the &lt;em&gt;service&lt;/em&gt; only when the issuing service itself must run disconnected.&lt;/p&gt;

&lt;h2&gt;
  
  
  Doing this with Keyright
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://delta1labs.com/keyright" rel="noopener noreferrer"&gt;Keyright&lt;/a&gt; is offline-first by design, so air-gapped deployments are a supported path, not an afterthought. It issues signed offline license files that verify locally against a public key you embed at build time — no network call, so they work on fully disconnected machines — node-locks them with tolerance, defends against clock tampering, and ships signed revocation lists you distribute out of band. Every tenant gets its own isolated signing key whose private half never leaves the server, and the SDKs for .NET, Node, Python and Java fail closed throughout. When the &lt;em&gt;issuing service itself&lt;/em&gt; must live inside an isolated network, Keyright &lt;a href="https://delta1labs.com/keyright/self-hosted" rel="noopener noreferrer"&gt;self-hosts&lt;/a&gt; — the identical build, fully air-gapped, on your own database.&lt;/p&gt;

&lt;p&gt;Start on the free managed plan to wire up offline validation before you pay a cent — &lt;a href="https://delta1labs.com/pricing?product=keyright" rel="noopener noreferrer"&gt;see the plans&lt;/a&gt; — or, if your deployment is fully air-gapped end to end, read &lt;a href="https://delta1labs.com/keyright/self-hosted" rel="noopener noreferrer"&gt;how self-hosting works&lt;/a&gt; and talk to us about an on-prem instance.&lt;/p&gt;

</description>
      <category>licensing</category>
      <category>security</category>
      <category>guide</category>
    </item>
    <item>
      <title>Software license types explained: perpetual, subscription, floating, trial</title>
      <dc:creator>Zero Heartbeat</dc:creator>
      <pubDate>Sun, 27 Sep 2026 06:07:41 +0000</pubDate>
      <link>https://dev.to/zero_heartbeat_06a3625d7a/software-license-types-explained-perpetual-subscription-floating-trial-4jn9</link>
      <guid>https://dev.to/zero_heartbeat_06a3625d7a/software-license-types-explained-perpetual-subscription-floating-trial-4jn9</guid>
      <description>&lt;p&gt;Software license types are the models that define how a customer is allowed to use your software: &lt;strong&gt;perpetual&lt;/strong&gt; (buy once, use forever), &lt;strong&gt;subscription/term&lt;/strong&gt; (valid for a fixed period), &lt;strong&gt;floating/concurrent&lt;/strong&gt; (a shared pool of seats), &lt;strong&gt;node-locked&lt;/strong&gt; (bound to one machine), &lt;strong&gt;trial/evaluation&lt;/strong&gt; (time-limited access), and &lt;strong&gt;volume/site&lt;/strong&gt; (many seats or a whole organization under one agreement). Most products combine several. This reference explains each one, when to use it, its trade-offs, and how it maps to what a modern licensing system enforces.&lt;/p&gt;

&lt;h2&gt;
  
  
  Comparison at a glance
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Model&lt;/th&gt;
&lt;th&gt;What it grants&lt;/th&gt;
&lt;th&gt;Best for&lt;/th&gt;
&lt;th&gt;Main trade-off&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Perpetual&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Use forever, one purchase&lt;/td&gt;
&lt;td&gt;Buyers who dislike recurring fees; on-prem tools&lt;/td&gt;
&lt;td&gt;No recurring revenue; needs a separate maintenance window for updates&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Subscription / term&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Use for a fixed period (e.g. 1 year)&lt;/td&gt;
&lt;td&gt;Most modern software; SaaS and cloud tools&lt;/td&gt;
&lt;td&gt;Customer loses access if they stop paying; needs expiry + renewal logic&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Floating / concurrent&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Shared pool; N active at once&lt;/td&gt;
&lt;td&gt;Teams sharing licenses across many users&lt;/td&gt;
&lt;td&gt;Requires a server to track who is active in real time&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Node-locked&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Bound to one machine&lt;/td&gt;
&lt;td&gt;Fixed installs, per-device pricing, air-gapped sites&lt;/td&gt;
&lt;td&gt;Hardware changes can lock users out without tolerance&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Trial / evaluation&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Full features for N days&lt;/td&gt;
&lt;td&gt;Driving conversion; letting buyers self-serve&lt;/td&gt;
&lt;td&gt;Must auto-expire and resist abuse (one per customer)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Volume / site&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Many seats or a whole org&lt;/td&gt;
&lt;td&gt;Enterprise and education deals&lt;/td&gt;
&lt;td&gt;Pricing and fulfillment complexity; needs bulk operations&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Perpetual licensing
&lt;/h2&gt;

&lt;p&gt;A &lt;strong&gt;perpetual&lt;/strong&gt; license is bought once and runs forever — there is no expiry. It's the model buyers like most, because they own what they paid for, and it's common for on-premise and developer tools.&lt;/p&gt;

&lt;p&gt;The catch is updates. A perpetual license usually pairs with a &lt;strong&gt;support-and-maintenance window&lt;/strong&gt;: a period (often a year) during which the customer is entitled to new versions and support. When that window lapses, the installed software keeps working, but the customer must renew maintenance to get further updates. This lets you sell perpetual licenses without giving away every future release for a single payment.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Use it when&lt;/strong&gt; your buyers strongly prefer one-time purchases, or you sell a tool that must keep running unchanged for years. &lt;strong&gt;The trade-off&lt;/strong&gt; is no recurring revenue and the need to manage a maintenance window separately from the license itself.&lt;/p&gt;

&lt;h2&gt;
  
  
  Subscription / term licensing
&lt;/h2&gt;

&lt;p&gt;A &lt;strong&gt;subscription&lt;/strong&gt; (or &lt;strong&gt;term&lt;/strong&gt;) license is valid only for a fixed period — typically annual — and carries an &lt;strong&gt;expiry&lt;/strong&gt; the app enforces. When it lapses without renewal, the paid features lock. This is the default for most modern software because it ties revenue to ongoing value and funds continuous development.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Use it when&lt;/strong&gt; you ship regular updates or run any kind of service. &lt;strong&gt;The trade-offs&lt;/strong&gt; are that you now need reliable expiry enforcement (including defense against a customer rolling the system clock back to extend a trial or term) and a smooth &lt;strong&gt;renewal&lt;/strong&gt; path that extends the existing license rather than re-issuing a new key.&lt;/p&gt;

&lt;h2&gt;
  
  
  Floating / concurrent licensing
&lt;/h2&gt;

&lt;p&gt;A &lt;strong&gt;floating&lt;/strong&gt; (concurrent) license is a shared pool: an organization buys, say, ten seats, and any ten machines can be active at once. When a user finishes, their seat returns to the pool for someone else. It maximizes value for teams where far more people &lt;em&gt;could&lt;/em&gt; use the software than ever use it &lt;em&gt;at the same time&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Use it when&lt;/strong&gt; you sell to organizations that share access across shifts, labs, or large teams. &lt;strong&gt;The trade-off&lt;/strong&gt; is that true concurrency requires a server tracking active sessions in real time — the most infrastructure-heavy model. A common, simpler middle ground is &lt;strong&gt;seat-based&lt;/strong&gt; licensing: a fixed number of activations, each bound to a machine, with the ability to &lt;em&gt;release&lt;/em&gt; or &lt;em&gt;transfer&lt;/em&gt; a seat when a device is retired. That gives teams flexibility without a live concurrency broker.&lt;/p&gt;

&lt;h2&gt;
  
  
  Node-locked licensing
&lt;/h2&gt;

&lt;p&gt;A &lt;strong&gt;node-locked&lt;/strong&gt; license is bound to a specific machine through a &lt;strong&gt;hardware fingerprint&lt;/strong&gt; derived from stable device and OS attributes. It only works on that machine, which stops one license from being copied across an organization.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Use it when&lt;/strong&gt; you price per device, ship to fixed installs, or serve air-gapped and high-security environments where per-machine control matters. &lt;strong&gt;The trade-off&lt;/strong&gt; is tolerance: fingerprint too strictly and a customer who swaps a network card or upgrades a disk gets locked out; too loosely and the lock is meaningless. A good implementation allows a small amount of hardware drift and lets you control which components identify a machine for VMs and CI.&lt;/p&gt;

&lt;h2&gt;
  
  
  Trial / evaluation licensing
&lt;/h2&gt;

&lt;p&gt;A &lt;strong&gt;trial&lt;/strong&gt; is a time-limited license that unlocks the full paid experience for a set number of days, then expires. It's the most effective conversion tool you have, because it lets a buyer prove value before paying.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Use it when&lt;/strong&gt; your software's value is best demonstrated hands-on. &lt;strong&gt;The trade-offs&lt;/strong&gt; are abuse prevention and expiry: a trial should be &lt;strong&gt;one per customer per product&lt;/strong&gt;, idempotent so a refresh or a second click never resets the clock, auto-expiring, and ideally able to convert to a paid license with no reinstall. Air-gapped evaluators may need a signed &lt;em&gt;offline&lt;/em&gt; trial file instead of an online activation.&lt;/p&gt;

&lt;h2&gt;
  
  
  Volume / site licensing
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Volume&lt;/strong&gt; and &lt;strong&gt;site&lt;/strong&gt; licenses cover large deployments under one agreement. A &lt;strong&gt;volume&lt;/strong&gt; license is really about issuing many keys efficiently — in bulk, or automatically through your store's fulfillment — while a &lt;strong&gt;site&lt;/strong&gt; license is typically a single license with a very high seat count that covers an entire organization or location.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Use it when&lt;/strong&gt; you close enterprise or education deals. &lt;strong&gt;The trade-off&lt;/strong&gt; is operational: you need bulk issuance and management (bulk seat changes, bulk revocation, reporting) and clear terms for how the seats are counted.&lt;/p&gt;

&lt;h2&gt;
  
  
  How these map to Keyright
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://delta1labs.com/keyright" rel="noopener noreferrer"&gt;Keyright&lt;/a&gt; supports these models directly rather than forcing you into one:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Perpetual and term&lt;/strong&gt; — issue a license with no expiry for perpetual, or set an expiry for subscription/term. Perpetual licenses aren't subject to the clock-tamper check that protects time-limited ones.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Node-locked seats&lt;/strong&gt; — bind each seat to a machine fingerprint with configurable tolerance, enforce the seat count server-side, and let customers &lt;strong&gt;release&lt;/strong&gt; or &lt;strong&gt;transfer&lt;/strong&gt; a seat when they change devices — the practical, seat-based take on floating.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Entitlements and tiers&lt;/strong&gt; — define per-tier feature flags and numeric limits so one binary unlocks different capabilities per plan, and change what a plan includes without shipping new code.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Trials&lt;/strong&gt; — turn on self-service trials per product, one-per-email and idempotent, that activate through the exact same path as a paid key and convert seamlessly.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Volume / site&lt;/strong&gt; — issue keys via the admin API or automatic store fulfillment, manage them in bulk (bulk seat changes and bulk revocation), or cover an org with a single high-seat license.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Revocation&lt;/strong&gt; — kill a refunded or leaked key in one click; it reaches online clients on the next refresh and offline clients through a signed revocation list.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Every license and lease is signed with your tenant's own RSA key and verified offline by the SDK, so all of the above works air-gapped and &lt;strong&gt;fails closed&lt;/strong&gt; by design. See the full capability list on the &lt;a href="https://delta1labs.com/keyright/features" rel="noopener noreferrer"&gt;Keyright features page&lt;/a&gt;, or read the &lt;a href="https://delta1labs.com/docs/keyright" rel="noopener noreferrer"&gt;Keyright documentation&lt;/a&gt; to wire up the model that fits how you sell.&lt;/p&gt;

</description>
      <category>licensing</category>
      <category>dotnet</category>
      <category>guide</category>
    </item>
    <item>
      <title>How reverse engineering a .NET app works (and how to make it not worth it)</title>
      <dc:creator>Zero Heartbeat</dc:creator>
      <pubDate>Sun, 27 Sep 2026 06:07:40 +0000</pubDate>
      <link>https://dev.to/zero_heartbeat_06a3625d7a/how-reverse-engineering-a-net-app-works-and-how-to-make-it-not-worth-it-4mpl</link>
      <guid>https://dev.to/zero_heartbeat_06a3625d7a/how-reverse-engineering-a-net-app-works-and-how-to-make-it-not-worth-it-4mpl</guid>
      <description>&lt;p&gt;Reverse engineering a .NET app is straightforward by default: C# compiles to Intermediate Language (IL) that decompiles cleanly back to readable C#, so an attacker with a free tool can see your strings, license checks and business logic almost as clearly as you wrote them. The good news is that you can change that economics dramatically. This guide walks through exactly how a .NET binary gets reversed — and the defensive layers that make reversing it cost more than it is worth. &lt;a href="https://delta1labs.com/products/nebula" rel="noopener noreferrer"&gt;Nebula.NET&lt;/a&gt; is the tool we build at Delta1 Labs to apply those layers, and we will be honest throughout about what each one does and doesn't do.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why is .NET so easy to reverse engineer?
&lt;/h2&gt;

&lt;p&gt;Unlike C or Rust, which compile straight to native machine code, .NET languages compile to &lt;strong&gt;Intermediate Language&lt;/strong&gt; — a high-level, portable, stack-based instruction set that the runtime JIT-compiles on the target machine. To make that work, the compiler embeds a remarkable amount of information in your DLL or EXE: full type names, method names, parameter names, field names, and complete metadata describing every member.&lt;/p&gt;

&lt;p&gt;That metadata is exactly what a decompiler needs. It does not have to guess at structure the way a native disassembler does — the names and shapes are right there. So the decompiler's job is essentially to run the C# compiler in reverse: read the IL, match its patterns back to language constructs (a &lt;code&gt;foreach&lt;/code&gt;, an &lt;code&gt;async&lt;/code&gt; method, a LINQ query), and pretty-print the result. For an unprotected assembly, the output is frequently close enough to recompile.&lt;/p&gt;

&lt;h3&gt;
  
  
  What does an attacker actually see?
&lt;/h3&gt;

&lt;p&gt;Open an unprotected release build in a decompiler and here is what a reverse engineer walks away with:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;String literals in the clear&lt;/strong&gt; — API endpoints, connection strings, SQL, error messages, and too often things never meant to ship in cleartext.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Named logic.&lt;/strong&gt; Your &lt;code&gt;ValidateLicenseKey&lt;/code&gt; and &lt;code&gt;IsTrialExpired&lt;/code&gt; methods keep their names and read like your source.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Control flow.&lt;/strong&gt; The exact &lt;code&gt;if/else&lt;/code&gt; a license check runs, so an attacker can find the one branch to flip.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Type and API structure&lt;/strong&gt;, making it trivial to see how your app fits together and where to attack.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  How does the reverse-engineering workflow actually go?
&lt;/h2&gt;

&lt;p&gt;The tooling is mature and free. A typical session looks like this:&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Decompile to C
&lt;/h3&gt;

&lt;p&gt;The attacker opens the assembly in a static decompiler — &lt;strong&gt;ILSpy&lt;/strong&gt;, &lt;strong&gt;dotPeek&lt;/strong&gt;, our own &lt;a href="https://delta1labs.com/products/glass" rel="noopener noreferrer"&gt;Glass.NET&lt;/a&gt;, or the community-maintained &lt;strong&gt;dnSpyEx&lt;/strong&gt; — and browses the type tree. No source, PDB or special build required; a standard managed &lt;code&gt;.dll&lt;/code&gt; or &lt;code&gt;.exe&lt;/code&gt; is enough. Within seconds they have navigable C#. (Full steps: &lt;a href="https://delta1labs.com/blog/how-to-decompile-dotnet-dll-to-csharp/" rel="noopener noreferrer"&gt;how to decompile a .NET DLL to C#&lt;/a&gt;.)&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Find the interesting code
&lt;/h3&gt;

&lt;p&gt;They search for the obvious keywords — "license", "trial", "activate", "premium" — or grep the decompiled strings. On an unprotected binary this lands them on the relevant method immediately: named methods and readable literals turn a needle-in-a-haystack problem into a text search.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Understand, then patch or extract
&lt;/h3&gt;

&lt;p&gt;Once they can read the logic, they either extract what they wanted (an algorithm, an embedded key) or patch it. With &lt;strong&gt;dnSpyEx&lt;/strong&gt; they can even edit the IL and reassemble — flip a trial check to always return &lt;code&gt;false&lt;/code&gt;, or short-circuit a license validation to always return &lt;code&gt;true&lt;/code&gt; — and save a cracked binary.&lt;/p&gt;

&lt;p&gt;If you have never watched this happen to your own code, do it now. Take a release build, open it in &lt;a href="https://delta1labs.com/products/glass" rel="noopener noreferrer"&gt;Glass.NET&lt;/a&gt; — the free decompiler we ship precisely so you can see what an attacker sees — and browse to a class you care about. That moment of seeing your own logic laid bare is the honest starting point for deciding what to protect.&lt;/p&gt;

&lt;h2&gt;
  
  
  The defensive layers that raise the cost
&lt;/h2&gt;

&lt;p&gt;There is no single switch that "protects" an assembly. Real protection is a stack of techniques, each raising the attacker's cost a little more. Here is the ladder from lightest to strongest.&lt;/p&gt;

&lt;h3&gt;
  
  
  Identifier renaming
&lt;/h3&gt;

&lt;p&gt;The cheapest win. Renaming turns &lt;code&gt;ValidateLicenseKey&lt;/code&gt; and &lt;code&gt;_customerBalance&lt;/code&gt; into meaningless symbols like &lt;code&gt;a&lt;/code&gt; and &lt;code&gt;b&lt;/code&gt;. The IL runs identically, but the human-readable names that make decompiled code easy to navigate are gone. The trade-off: you cannot rename everything — public API surfaces, reflection targets and serialization contracts must be preserved. Nebula.NET does public-API-preserving renaming so your callable surface stays intact. Runtime cost: effectively zero.&lt;/p&gt;

&lt;h3&gt;
  
  
  String encryption
&lt;/h3&gt;

&lt;p&gt;Renaming hides what things are called; string encryption hides the data leaking through literals. It stores strings encrypted and decrypts them at runtime only when used, so searching the binary for a keyword turns up nothing. Be clear-eyed: the string is in memory at some point, so a debugger can still recover it — but the trivial "grep for the endpoint" attack dies. Nebula.NET encrypts string and credential literals and can key decryption per call-site.&lt;/p&gt;

&lt;h3&gt;
  
  
  Control-flow obfuscation
&lt;/h3&gt;

&lt;p&gt;This hides what the code &lt;em&gt;does&lt;/em&gt;. It rewrites each method — flattening straight-line logic into a dispatcher state machine, adding opaque predicates and bogus branches — so the decompiler emits a tangle of &lt;code&gt;goto&lt;/code&gt;s instead of clean C#. The realistic bar is not just confusing a human; it is surviving automated deobfuscators like de4dot, which Nebula.NET's transformation is designed to resist rather than be unwound in one pass. Runtime cost: small.&lt;/p&gt;

&lt;h3&gt;
  
  
  Anti-tamper and anti-debug
&lt;/h3&gt;

&lt;p&gt;Anti-tamper adds an integrity check: the assembly verifies at runtime that its own code has not been modified, so an attacker cannot patch out a check and have the binary keep running. Anti-debug raises the cost of the dynamic-analysis step. Neither is a wall; both mean an attacker cannot just flip one branch and win.&lt;/p&gt;

&lt;h3&gt;
  
  
  Method encryption and code virtualization — the heavy artillery
&lt;/h3&gt;

&lt;p&gt;For your crown jewels, two techniques remove the readable IL entirely:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://delta1labs.com/blog/dotnet-method-encryption/" rel="noopener noreferrer"&gt;Method encryption&lt;/a&gt;&lt;/strong&gt; stores a method's IL encrypted in the assembly and re-emits it at runtime, so a decompiler sees only a stub — no reconstructable C# or IL.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Code virtualization&lt;/strong&gt; translates a method into bytecode for a &lt;em&gt;custom&lt;/em&gt; virtual machine embedded in your assembly. There is no standard IL left to read, and an attacker must reverse-engineer your specific VM before they can even begin.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Both are expensive to break and, in return, meaningfully larger and slower — so you apply them surgically to the licensing check, the proprietary algorithm, the anti-cheat routine, not the whole assembly. You can explore the full set on the &lt;a href="https://delta1labs.com/products/nebula/features" rel="noopener noreferrer"&gt;Nebula.NET features page&lt;/a&gt;, with configuration in the &lt;a href="https://delta1labs.com/docs/nebula" rel="noopener noreferrer"&gt;docs&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  The honest part: nothing here is uncrackable
&lt;/h2&gt;

&lt;p&gt;This is the section most vendors skip. Every layer above runs on a machine you do not control, which means:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Your code still executes&lt;/strong&gt;, so at some moment it must be in a form the CPU understands. A patient attacker with a debugger and a memory dump can work around a great deal.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Secrets shipped in the binary are recoverable&lt;/strong&gt; — API keys, connection strings, signing keys — encryption or not. Keep them server-side.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Client-side license checks can be bypassed.&lt;/strong&gt; However well hidden, they run on the attacker's machine. Serious licensing needs server-side validation via online activation, with client protection making the bypass expensive rather than trivial.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;None of this makes protection pointless — it reframes the goal correctly. You are not building a vault; you are raising a wall higher than what is on the other side. For most commercial .NET software, that wall stops the casual "decompile, copy, ship" attack cold and pushes the serious attacker's cost past the point of worthwhile.&lt;/p&gt;

&lt;h2&gt;
  
  
  How to verify it worked
&lt;/h2&gt;

&lt;p&gt;Do not take any tool's word for it, including ours. The loop takes minutes: build and run your tests, protect the assembly, run your tests again against the protected build (behavior must be identical), then open the result in &lt;a href="https://delta1labs.com/products/glass" rel="noopener noreferrer"&gt;Glass.NET&lt;/a&gt; and try to read what you protected. Where you had clean C#, you should see renamed symbols, scrambled control flow, encrypted strings, and — for encrypted or virtualized methods — no reconstructable C# at all. Being able to audit the output yourself is why we ship a free decompiler alongside the protector.&lt;/p&gt;

&lt;h2&gt;
  
  
  The bottom line
&lt;/h2&gt;

&lt;p&gt;Reverse engineering a .NET app is easy by default and impossible to prevent entirely — be skeptical of anyone claiming otherwise. What you &lt;em&gt;can&lt;/em&gt; do is stack renaming, string encryption, control-flow obfuscation and anti-tamper across the assembly, reserve method encryption or code virtualization for your real intellectual property, and move secrets and license enforcement to a server you control. To see where your code stands today, &lt;a href="https://delta1labs.com/download?product=nebula" rel="noopener noreferrer"&gt;download Nebula.NET free&lt;/a&gt;, protect a representative build, and open it in &lt;a href="https://delta1labs.com/products/glass" rel="noopener noreferrer"&gt;Glass.NET&lt;/a&gt;. The before-and-after comparison is the only benchmark that matters.&lt;/p&gt;

</description>
      <category>dotnet</category>
      <category>security</category>
      <category>obfuscation</category>
    </item>
    <item>
      <title>How to license your software: a practical guide</title>
      <dc:creator>Zero Heartbeat</dc:creator>
      <pubDate>Sun, 27 Sep 2026 06:06:57 +0000</pubDate>
      <link>https://dev.to/zero_heartbeat_06a3625d7a/how-to-license-your-software-a-practical-guide-4ecp</link>
      <guid>https://dev.to/zero_heartbeat_06a3625d7a/how-to-license-your-software-a-practical-guide-4ecp</guid>
      <description>&lt;p&gt;To license your software, you pick a licensing model, issue each customer a &lt;em&gt;cryptographically signed&lt;/em&gt; license rather than a homegrown key string, deliver it at purchase, and verify it inside your app — an offline signature check, optionally paired with online activation to enforce seats and revocation — then gate features on entitlements and handle trials, renewals and revocation on the server. This guide walks that whole path end to end, product-neutral in the mechanics, so you can wire up real licensing whether you build it yourself or adopt a service.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 1 — Choose a licensing model
&lt;/h2&gt;

&lt;p&gt;Everything downstream follows from this choice, so make it first. The common models:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Perpetual&lt;/strong&gt; — the customer buys the software once and runs that version forever. Simple, but there is no recurring revenue and no built-in reason for the license to ever stop working.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Subscription / term&lt;/strong&gt; — the license is valid for a fixed window (typically a year) and carries an expiry the app enforces. This is the default for most modern software because it aligns revenue with ongoing value.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Seat-based&lt;/strong&gt; — the license permits a set number of activations, one per machine or user, and you enforce the cap. Standard for team and enterprise sales.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Trial / evaluation&lt;/strong&gt; — a time-limited license that unlocks the paid experience for a fixed number of days, then expires.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Most real products combine these: a subscription &lt;em&gt;with&lt;/em&gt; a seat count, or a perpetual license &lt;em&gt;with&lt;/em&gt; a support-and-updates window. You do not have to pick exactly one. (For a full reference on each model and when to use it, see our companion post, &lt;a href="https://delta1labs.com/blog/software-license-types-explained/" rel="noopener noreferrer"&gt;software license types explained&lt;/a&gt;.)&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 2 — Issue a signed license, not a key string
&lt;/h2&gt;

&lt;p&gt;The single most important decision is what a license &lt;em&gt;is&lt;/em&gt; on the wire. The tempting shortcut — generate a random or patterned key string and have your app accept anything matching that pattern — fails structurally: if your app can decide a key is valid by looking at the key, then everything needed to mint a valid key is inside your app, and a decompiler turns your validation code into a keygen.&lt;/p&gt;

&lt;p&gt;The correct unit is a &lt;strong&gt;signed license&lt;/strong&gt;: a small payload of facts — licensee, product, tier, seats, expiry, entitlements — with a cryptographic signature over it. Your server signs the payload with an &lt;strong&gt;RSA private key&lt;/strong&gt; that never leaves the server; your app embeds only the matching &lt;strong&gt;public key&lt;/strong&gt;. A public key can verify a signature but can never create one, so:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Decompiling your app reveals only the public key, which is useless for forgery.&lt;/li&gt;
&lt;li&gt;Changing one byte of the payload (bumping &lt;code&gt;tier&lt;/code&gt; from &lt;code&gt;free&lt;/code&gt; to &lt;code&gt;pro&lt;/code&gt;) breaks the signature, so validation fails.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is the same asymmetry behind TLS and code signing, pointed at licensing. In .NET the primitives ship in the box, which is exactly the trap — the &lt;em&gt;signature check&lt;/em&gt; is a few lines, so people assume the whole system is easy, and it isn't (Step 5 onward is where the real work lives).&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 3 — Deliver the license to your customer
&lt;/h2&gt;

&lt;p&gt;Once you can sign a license, you need to get it to the buyer without a manual email for every sale. In practice there are three delivery paths, and a real product uses more than one:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;On purchase, automatically.&lt;/strong&gt; Wire your checkout or Merchant-of-Record provider's "paid" event to your licensing backend so a successful payment mints and emails the key, and a refund or cancellation revokes it — no human in the loop.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;From an admin console&lt;/strong&gt;, for manual sales, resellers, and support-issued keys.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Self-service&lt;/strong&gt;, where the customer starts a trial from your own site and receives a key instantly (Step 8).&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The key you deliver is the string the customer later pastes into your app to activate. Everything else — the entitlements, the expiry, the seat count — travels inside the signed payload, not in the visible key.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 4 — Verify the license in your app
&lt;/h2&gt;

&lt;p&gt;Verification has two halves, and you almost always want both.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Offline signature check.&lt;/strong&gt; At startup, your app recomputes the signature check against its embedded public key and reads the now-trusted fields: product match, expiry, node-lock. This needs no network, so it works air-gapped and survives outages. The cardinal rule is to &lt;strong&gt;fail closed&lt;/strong&gt; — any problem (bad signature, wrong product, expired, revoked, a clock rolled backward to cheat a trial, a missing file) must resolve to the &lt;em&gt;unlicensed&lt;/em&gt; state, never throw and never unlock. If a check failed open, the easiest crack would be to make it fail on purpose.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Online activation.&lt;/strong&gt; When you need to enforce seats or revoke keys promptly, add one online step: the app exchanges the license key plus a stable machine id for a short-lived, signed &lt;strong&gt;lease&lt;/strong&gt; bound to that machine. The lease is verified offline and cached locally, so after a single activation the app keeps working offline until the lease nears expiry — at which point it reconciles seats and revocation with the server. A brief outage is covered by the still-valid cached lease, a grace window, so you don't lock out a paying customer over a flaky connection.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 5 — Enforce seats and node-locking
&lt;/h2&gt;

&lt;p&gt;You cannot count seats offline — a lone client has no idea how many other machines run the same key. The moment you promise "3 seats," you need a server that records activations, enforces the cap atomically, makes re-activating an already-bound machine idempotent, and lets a customer release a seat when they retire a device. Pair that with &lt;strong&gt;node-locking&lt;/strong&gt;: bind each seat to a machine fingerprint derived from stable hardware and OS attributes — with &lt;em&gt;tolerance&lt;/em&gt;, so a swapped network card or upgraded disk doesn't lock an honest user out and generate a support ticket.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 6 — Gate features with entitlements and tiers
&lt;/h2&gt;

&lt;p&gt;Ship &lt;em&gt;one&lt;/em&gt; binary and unlock different capabilities per plan by putting the capabilities in the license. Define a &lt;strong&gt;tier&lt;/strong&gt; (e.g. &lt;code&gt;pro&lt;/code&gt;, &lt;code&gt;enterprise&lt;/code&gt;) whose &lt;strong&gt;entitlement template&lt;/strong&gt; carries named flags (&lt;code&gt;"export": "true"&lt;/code&gt;) and numeric limits (&lt;code&gt;"max-projects": "10"&lt;/code&gt;), and gate features on those entitlements rather than a hard-coded tier check — &lt;code&gt;IsEnabled("export")&lt;/code&gt;, &lt;code&gt;GetLimit("max-projects", fallback: 1)&lt;/code&gt;. Then changing what a plan includes is a dashboard edit, not a new release. Every entitlement read should fail closed too: a missing flag reads as off, a missing limit returns your fallback.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 7 — Handle trials, renewals and revocation
&lt;/h2&gt;

&lt;p&gt;These three are what turn a license check into a business:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Trials&lt;/strong&gt; are time-limited keys that unlock the paid experience and auto-expire. Make them one-per-email-per-product and idempotent so a refresh never resets the clock, and let a trial convert to a paid license with no reinstall.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Renewals&lt;/strong&gt; extend an existing subscription's expiry (and can adjust seats) without issuing a new key — the customer keeps the same license.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Revocation&lt;/strong&gt; kills a refunded, charged-back or leaked key. Online, it takes effect on the next lease refresh; offline, you distribute a &lt;em&gt;signed&lt;/em&gt; revocation list your app honors with no network call.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Build it, or buy it
&lt;/h2&gt;

&lt;p&gt;The signature check is an afternoon. Everything from Step 3 onward — key management and rotation, server-side seat counting, revocation that reaches offline clients, trials with abuse controls, a customer portal so seat moves and re-downloads don't become support tickets — is a stateful, security-sensitive backend that no customer will ever thank you for. That's the real build-vs-buy question, and we break down the itemized cost in &lt;a href="https://delta1labs.com/blog/build-vs-buy-software-licensing-dotnet/" rel="noopener noreferrer"&gt;build vs buy: software licensing for .NET&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where Keyright fits
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://delta1labs.com/keyright" rel="noopener noreferrer"&gt;Keyright&lt;/a&gt; is the licensing infrastructure we build at Delta1 Labs, and it hands you this entire guide as a service. It issues signed &lt;strong&gt;offline license files&lt;/strong&gt; &lt;em&gt;and&lt;/em&gt; short-lived &lt;strong&gt;online activation leases&lt;/strong&gt; from one system; enforces &lt;strong&gt;node-locking and seats&lt;/strong&gt; server-side with self-service seat release; carries &lt;strong&gt;entitlements and tiers&lt;/strong&gt; so one binary unlocks per plan; supports &lt;strong&gt;perpetual and term&lt;/strong&gt; licenses, &lt;strong&gt;self-service trials&lt;/strong&gt;, &lt;strong&gt;renewals&lt;/strong&gt;, &lt;strong&gt;transfers&lt;/strong&gt;, and one-click &lt;strong&gt;revocation&lt;/strong&gt; that reaches both online and offline clients. Every tenant gets its own isolated RSA signing key — the private half stored encrypted server-side and never exposed — plus a hosted customer portal and SDKs for .NET, Node, Python and Java that all &lt;strong&gt;fail closed&lt;/strong&gt; by design.&lt;/p&gt;

&lt;p&gt;To wire it up end to end, start with the &lt;a href="https://delta1labs.com/docs/keyright/getting-started" rel="noopener noreferrer"&gt;Keyright getting-started guide&lt;/a&gt;; the free plan covers real licensing before you pay a cent, and you can &lt;a href="https://delta1labs.com/pricing?product=keyright" rel="noopener noreferrer"&gt;see the plans here&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>licensing</category>
      <category>dotnet</category>
      <category>guide</category>
    </item>
    <item>
      <title>Free .NET obfuscator: what you actually get (and what you don''t)</title>
      <dc:creator>Zero Heartbeat</dc:creator>
      <pubDate>Sun, 27 Sep 2026 06:06:21 +0000</pubDate>
      <link>https://dev.to/zero_heartbeat_06a3625d7a/free-net-obfuscator-what-you-actually-get-and-what-you-dont-148f</link>
      <guid>https://dev.to/zero_heartbeat_06a3625d7a/free-net-obfuscator-what-you-actually-get-and-what-you-dont-148f</guid>
      <description>&lt;p&gt;A free .NET obfuscator gives you identifier renaming, usually some string encryption and basic control-flow protection — enough to stop the casual "decompile and read" attack — but the features that make protection hold up commercially (full string encryption, anti-tamper, method encryption, code virtualization, CI integration, signing and stack-trace de-obfuscation) are almost always paid or simply absent. This guide gives you the honest breakdown so you can decide when free is genuinely enough and when it is not, using &lt;a href="https://delta1labs.com/products/nebula" rel="noopener noreferrer"&gt;Nebula.NET&lt;/a&gt;'s free edition as a concrete example.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why is "free .NET obfuscator" such a common search?
&lt;/h2&gt;

&lt;p&gt;Because .NET decompiles trivially. C# compiles to Intermediate Language plus full metadata — every type, method and field named — so any decompiler reconstructs near-original C# from an unprotected DLL in seconds. Once a developer sees their own logic laid bare in a tool like &lt;a href="https://delta1labs.com/products/glass" rel="noopener noreferrer"&gt;Glass.NET&lt;/a&gt;, the natural next move is to look for a free way to fix it. That instinct is right; the trick is knowing what "free" actually buys.&lt;/p&gt;

&lt;h2&gt;
  
  
  What a free .NET obfuscator gives you
&lt;/h2&gt;

&lt;p&gt;The lightweight transforms are runtime-cheap and comparatively simple to implement, so they show up in free tiers and open-source tools alike.&lt;/p&gt;

&lt;h3&gt;
  
  
  Identifier renaming
&lt;/h3&gt;

&lt;p&gt;The headline feature and the biggest single win. Renaming turns &lt;code&gt;ValidateLicenseKey&lt;/code&gt;, &lt;code&gt;_customerBalance&lt;/code&gt; and &lt;code&gt;CalculateDiscount&lt;/code&gt; into meaningless symbols like &lt;code&gt;a&lt;/code&gt;, &lt;code&gt;b&lt;/code&gt;, &lt;code&gt;c&lt;/code&gt;. The IL runs identically, but the human-readable names that make decompiled code easy to navigate are gone. A reverse engineer can no longer grep for "license" and land on your check. Runtime cost is effectively zero, and this alone defeats the lowest-effort attack.&lt;/p&gt;

&lt;h3&gt;
  
  
  Some string encryption
&lt;/h3&gt;

&lt;p&gt;Most free tiers include &lt;em&gt;limited&lt;/em&gt; string encryption — a fixed number of literals, or a basic scheme. It stores strings encrypted and decrypts them at runtime, so searching the binary for an endpoint or an error message turns up nothing. This is genuinely useful, but note the word "limited": the cap is often where the free tier draws its line.&lt;/p&gt;

&lt;h3&gt;
  
  
  Basic control-flow protection
&lt;/h3&gt;

&lt;p&gt;Some free obfuscators apply light control-flow obfuscation to a small number of methods — flattening straight-line logic so the decompiler emits &lt;code&gt;goto&lt;/code&gt;s instead of clean &lt;code&gt;if/else&lt;/code&gt;. Free-tier control flow is often capped in count and lighter in strength than the paid tier.&lt;/p&gt;

&lt;h3&gt;
  
  
  The catch with free open-source tools
&lt;/h3&gt;

&lt;p&gt;Open-source obfuscators like ConfuserEx are genuinely free and capable, but they come with trade-offs worth naming: many are archived or community-maintained rather than actively supported, their output is a frequent target for public deobfuscators like de4dot, and there is no support line when a protected build misbehaves. We cover the specifics in our &lt;a href="https://delta1labs.com/blog/confuserex-alternative/" rel="noopener noreferrer"&gt;ConfuserEx comparison&lt;/a&gt;. Free is not the same as maintained.&lt;/p&gt;

&lt;h2&gt;
  
  
  What free typically does NOT give you
&lt;/h2&gt;

&lt;p&gt;This is the part that matters most, because it is where protection goes from "speed bump" to "actually holds up." These layers cost real engineering to build and maintain, so they live in paid tiers.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Uncapped, full string encryption&lt;/strong&gt; — every literal in the assembly, not a handful, with per-call-site keying so one dumped routine does not reveal everything.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Anti-tamper and anti-debug.&lt;/strong&gt; Without anti-tamper, an attacker can patch out a check — a trial expiry, a license test — and the binary keeps running as if nothing happened. This is one of the most important paid features, because renaming a check the attacker can still patch does not stop them.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Method encryption.&lt;/strong&gt; Storing a method's IL encrypted and re-emitting it at runtime, so a decompiler sees only a stub. See &lt;a href="https://delta1labs.com/blog/dotnet-method-encryption/" rel="noopener noreferrer"&gt;method encryption in .NET&lt;/a&gt; for how it works.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Code virtualization.&lt;/strong&gt; Compiling sensitive methods to a custom bytecode VM so there is no IL left to decompile — the strongest common protection, typically a top-tier feature.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;MSBuild / CI integration.&lt;/strong&gt; Protection that runs automatically as part of &lt;code&gt;dotnet build&lt;/code&gt; on your build server, so there is no manual step to forget. Free tiers usually make you run the tool by hand.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Authenticode signing&lt;/strong&gt; integrated into the protection step, for distribution integrity.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Stack-trace de-obfuscation.&lt;/strong&gt; Once you rename everything, your crash reports come back renamed too. Reading them back to original names needs a mapping file and tooling — almost always a paid capability.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  How Nebula.NET's free edition fits
&lt;/h2&gt;

&lt;p&gt;Nebula.NET comes in three editions — &lt;strong&gt;Free&lt;/strong&gt;, &lt;strong&gt;Licensed&lt;/strong&gt; and &lt;strong&gt;Enterprise&lt;/strong&gt; — all built on the same tested engine. (Our &lt;a href="https://delta1labs.com/blog/which-nebula-edition-is-right-for-you/" rel="noopener noreferrer"&gt;which edition is right for you&lt;/a&gt; guide covers the full decision.) The free edition is deliberately &lt;em&gt;not&lt;/em&gt; a rename-only stub:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Identifier renaming&lt;/strong&gt;, with public-API preservation so your callable surface stays intact.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A couple of encrypted strings&lt;/strong&gt; and &lt;strong&gt;a couple of control-flow-protected methods&lt;/strong&gt; — enough to see the transforms working on your own code.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Both the CLI and the desktop GUI&lt;/strong&gt;, so you can explore settings visually or script a run.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Choose Free if&lt;/strong&gt; you are evaluating the tool, or shipping a small or non-commercial project where basic renaming is enough. It is fully functional for that — &lt;a href="https://delta1labs.com/download?product=nebula" rel="noopener noreferrer"&gt;download it here&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;You will outgrow it when&lt;/strong&gt; you need full string encryption, control-flow flattening on more than a couple of methods, anti-tamper, method encryption, MSBuild/CI integration, or the ability to read back crash stack traces from protected builds. At that point you register a license key to unlock the &lt;strong&gt;Licensed&lt;/strong&gt; edition's full suite — no reinstall required. Pricing is transparent and per-seat, with annual and perpetual options on the &lt;a href="https://delta1labs.com/pricing?product=nebula" rel="noopener noreferrer"&gt;pricing page&lt;/a&gt;; there is no quote process and no "contact sales."&lt;/p&gt;

&lt;h2&gt;
  
  
  When is free enough, and when do you need paid?
&lt;/h2&gt;

&lt;p&gt;Decide on your threat model and what you ship, not on a feature count.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Free is genuinely enough when:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;It is a hobby project, an internal tool, or a proof of concept.&lt;/li&gt;
&lt;li&gt;Your goal is to stop the casual observer from reading your code, and renaming does that.&lt;/li&gt;
&lt;li&gt;You are evaluating a protector before committing budget — run the whole loop on your own binary first.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;You need paid when:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;You sell or distribute commercial software, so someone has a financial motive to crack it.&lt;/li&gt;
&lt;li&gt;You have client-side checks (trial, license, feature gating) that must resist patching — that is anti-tamper, and it is not free.&lt;/li&gt;
&lt;li&gt;Your product &lt;em&gt;is&lt;/em&gt; an algorithm or a piece of proprietary logic you cannot afford to have read — that is method encryption or code virtualization.&lt;/li&gt;
&lt;li&gt;You run CI and want protection to happen automatically on every build, not as a manual step someone forgets.&lt;/li&gt;
&lt;li&gt;You need to diagnose crashes in protected builds, which requires stack-trace de-obfuscation.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Verify before you trust — free or paid
&lt;/h2&gt;

&lt;p&gt;Whichever way you go, audit the result yourself. Build your assembly, protect it, run your tests against the protected build (behavior must be identical), then open it in &lt;a href="https://delta1labs.com/products/glass" rel="noopener noreferrer"&gt;Glass.NET&lt;/a&gt; and try to read what you protected. Where you had clean, named C#, you should see renamed symbols, scrambled control flow and encrypted strings. Being able to check the output in a free decompiler is the only honest benchmark — and it is why we ship one alongside the protector.&lt;/p&gt;

&lt;h2&gt;
  
  
  The bottom line
&lt;/h2&gt;

&lt;p&gt;A free .NET obfuscator is a real and useful thing: renaming and light string and control-flow protection stop the lowest-effort attack, and Nebula.NET's free edition gives you all of that on your own code without a rename-only bait-and-switch. What free does not give you is the layers that make protection commercially durable — full string encryption, anti-tamper, method encryption, code virtualization, CI integration and stack-trace de-obfuscation. If you ship commercial software with something worth cracking, that is where paid earns its keep. &lt;a href="https://delta1labs.com/download?product=nebula" rel="noopener noreferrer"&gt;Download Nebula.NET free&lt;/a&gt;, protect a representative build, and see for yourself where you land.&lt;/p&gt;

</description>
      <category>dotnet</category>
      <category>obfuscation</category>
      <category>guide</category>
    </item>
    <item>
      <title>How to debug a .NET app when you don''t have the source</title>
      <dc:creator>Zero Heartbeat</dc:creator>
      <pubDate>Sun, 27 Sep 2026 06:06:05 +0000</pubDate>
      <link>https://dev.to/zero_heartbeat_06a3625d7a/how-to-debug-a-net-app-when-you-dont-have-the-source-5d1p</link>
      <guid>https://dev.to/zero_heartbeat_06a3625d7a/how-to-debug-a-net-app-when-you-dont-have-the-source-5d1p</guid>
      <description>&lt;p&gt;To debug a .NET app when you don't have the source, open the compiled assembly in a decompiler like &lt;a href="https://delta1labs.com/products/glass" rel="noopener noreferrer"&gt;Glass.NET&lt;/a&gt;: it reads the IL and metadata inside the DLL or EXE and reconstructs readable C# you can navigate, search and export — no source files, PDBs or special build required. That turns "I have no idea what this dependency is doing" into "let me read the method and find out." Here is how to work through the common no-source scenarios, and where the honest limits are.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why you end up debugging without source in the first place
&lt;/h2&gt;

&lt;p&gt;It happens to everyone eventually:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;A bug in a third-party NuGet package.&lt;/strong&gt; The behavior is wrong, the docs don't explain it, and the package ships without symbols. You need to see what the method actually does, not what you assume it does.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A legacy DLL with no source.&lt;/strong&gt; An internal library from a team that has moved on, a component whose repository is long gone, or a vendor assembly you depend on and cannot get source for.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No symbols for a production build.&lt;/strong&gt; A crash comes in with a stack trace pointing into an assembly you can't step into, and you need to understand the code path.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In every case the compiled assembly is right in front of you — and because .NET ships IL plus rich metadata, that assembly carries a near-complete, recoverable description of the program. A decompiler is how you read it.&lt;/p&gt;

&lt;h2&gt;
  
  
  How does a decompiler get you readable C#?
&lt;/h2&gt;

&lt;p&gt;A .NET assembly is not machine code. The compiler emits &lt;strong&gt;Intermediate Language&lt;/strong&gt; — a stack-based instruction set — plus &lt;strong&gt;metadata&lt;/strong&gt; naming every type, method, field and parameter. The decompiler runs the compiler in reverse: it reads the IL, matches its patterns back to C# constructs (a &lt;code&gt;foreach&lt;/code&gt;, an &lt;code&gt;async&lt;/code&gt; method, a LINQ query), and pretty-prints the result. For most assemblies the output is accurate and often recompilable. (The full walkthrough is in &lt;a href="https://delta1labs.com/blog/how-to-decompile-dotnet-dll-to-csharp/" rel="noopener noreferrer"&gt;how to decompile a .NET DLL to C#&lt;/a&gt;.)&lt;/p&gt;

&lt;p&gt;Open the assembly (in Glass: &lt;strong&gt;File ▸ Open Assembly&lt;/strong&gt;, or drag it onto the window), expand the &lt;strong&gt;namespaces ▸ types ▸ members&lt;/strong&gt; tree, and click the type you care about to decompile it on demand. Within seconds you are reading C# for code you never had the source to.&lt;/p&gt;

&lt;h2&gt;
  
  
  Navigating unfamiliar code without getting lost
&lt;/h2&gt;

&lt;p&gt;Reading one method is easy; understanding how it fits together is the real debugging work. This is where a navigation-first decompiler earns its place, and it is what &lt;a href="https://delta1labs.com/products/glass" rel="noopener noreferrer"&gt;Glass.NET&lt;/a&gt; is built around.&lt;/p&gt;

&lt;h3&gt;
  
  
  Follow the logic with Go to Definition and Find All References
&lt;/h3&gt;

&lt;p&gt;When a method calls into something you don't understand, &lt;strong&gt;Go to Definition&lt;/strong&gt; (F12) jumps straight to it, and &lt;strong&gt;Find All References&lt;/strong&gt; (Shift+F12) shows every caller — so you can answer "what actually calls this, and with what?" instead of guessing. References resolve from the decompiler's own syntax tree, so you land on the right symbol, not a text match.&lt;/p&gt;

&lt;h3&gt;
  
  
  See the shape with call and type hierarchy
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Call hierarchy&lt;/strong&gt; and &lt;strong&gt;type hierarchy&lt;/strong&gt; let you walk up and down the structure — who calls the buggy method, what overrides what, where an interface is implemented. For tracing how a value reaches the code that mishandles it, this is far faster than scrolling.&lt;/p&gt;

&lt;h3&gt;
  
  
  Jump anywhere with Go To All
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Go To All&lt;/strong&gt; (Ctrl+T) is a fuzzy quick-open across every type and member in the assembly. When a stack trace names a method, type it in and you are there. A namespace ▸ type ▸ member breadcrumb and navigation history keep you oriented as you move.&lt;/p&gt;

&lt;h3&gt;
  
  
  Drop to IL when the C# looks surprising
&lt;/h3&gt;

&lt;p&gt;Sometimes the reconstructed C# does something you didn't expect. Switch the same member to the &lt;strong&gt;IL view&lt;/strong&gt; to confirm what the code actually does at the instruction level — invaluable when a decompiler's pattern-matching produces something that reads oddly but is technically correct.&lt;/p&gt;

&lt;h2&gt;
  
  
  Finding a regression: compare two versions
&lt;/h2&gt;

&lt;p&gt;One of the most practical no-source debugging moves: a dependency worked in version 2.1 and broke in 2.2, and there is no changelog that explains it. Decompile both assemblies and &lt;strong&gt;compare them side by side&lt;/strong&gt;. Glass has an assembly compare with a line diff, so you can see precisely what changed in the method you suspect — the altered condition, the reordered call, the new null check that isn't null-safe. Reading the diff of the reconstructed C# often pinpoints a regression in minutes, without any source at all.&lt;/p&gt;

&lt;h2&gt;
  
  
  Getting the code onto disk
&lt;/h2&gt;

&lt;p&gt;When you want to grep it, annotate it, or open it in Visual Studio, &lt;strong&gt;export to a buildable project&lt;/strong&gt; (in the GUI, typically &lt;strong&gt;File ▸ Export to Project&lt;/strong&gt;), which writes a &lt;code&gt;.csproj&lt;/code&gt; plus the reconstructed &lt;code&gt;.cs&lt;/code&gt; files. Glass also ships a &lt;code&gt;glass export&lt;/code&gt; CLI subcommand so you can script it. A small assembly usually compiles straight back; a large one may need manual fixes for unresolved references or compiler-generated constructs. Once it is a project, all your normal tools — search, static analysis, even setting up a local reproduction — are back on the table. See the &lt;a href="https://delta1labs.com/products/glass/features" rel="noopener noreferrer"&gt;Glass.NET features page&lt;/a&gt; for the full navigation and export toolset.&lt;/p&gt;

&lt;h2&gt;
  
  
  The honest limits
&lt;/h2&gt;

&lt;p&gt;A decompiler is powerful, but it is not magic, and pretending otherwise wastes your time.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Decompiled ≠ original.&lt;/strong&gt; You get accurate C# for what the code &lt;em&gt;does&lt;/em&gt;, but comments are gone, most local variable names are lost unless a PDB supplies them, and constructs like async state machines and iterators are reconstructed, not recovered verbatim. Names like &lt;code&gt;num2&lt;/code&gt; and &lt;code&gt;flag&lt;/code&gt; are common.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;It is static, not a live debugger.&lt;/strong&gt; Glass, ILSpy and dotPeek read and reconstruct code; they do not run it. If you need to step through the assembly at runtime, watching values change, that is a different job — &lt;strong&gt;dnSpyEx&lt;/strong&gt; is the community-maintained tool for live managed debugging, or a decompiler with symbol-server / PDB generation (dotPeek) can let Visual Studio step in.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Obfuscated code is hard on purpose.&lt;/strong&gt; If the assembly was protected, renamed identifiers, encrypted strings and flattened control flow all survive decompilation, so you get valid but hard-to-follow C#. You can still trace structure and behavior, but named-symbol navigation loses much of its value.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Only analyze what you have the right to.&lt;/strong&gt; Debugging your own code, or a dependency you have a licence or legal right to inspect, is routine. Decompiling third-party software can be restricted by its EULA or local law — check before you dig.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Which tool should you reach for?
&lt;/h2&gt;

&lt;p&gt;For reading and navigating an assembly you don't have source for, any capable free decompiler will get you accurate C#. If you specifically need &lt;strong&gt;live debugging&lt;/strong&gt;, use dnSpyEx. If you want a fast, navigation-first reading experience — Go to Definition, Find All References, Go To All, call and type hierarchy, C# and IL views, assembly compare and project export — &lt;a href="https://delta1labs.com/products/glass" rel="noopener noreferrer"&gt;Glass.NET&lt;/a&gt; is built for exactly this workflow. It is built on the same &lt;strong&gt;ICSharpCode.Decompiler&lt;/strong&gt; engine as ILSpy, so the C# accuracy is on par; what Glass adds is the experience around the code, so moving through an unfamiliar assembly feels like navigating your own project.&lt;/p&gt;

&lt;h2&gt;
  
  
  The bottom line
&lt;/h2&gt;

&lt;p&gt;No source is not a dead end. Decompile the assembly, read the reconstructed C#, follow the logic with real navigation, diff two versions to isolate a regression, and export to a project when you want it on disk — and you can debug a third-party package or a legacy DLL almost as if you had its source. Just keep the limits in mind: you are reading what the code does, not the original file. &lt;a href="https://delta1labs.com/download?product=glass" rel="noopener noreferrer"&gt;Download Glass.NET free&lt;/a&gt; — no licence, no seats, no sign-up — open the assembly that is giving you trouble, and press F12.&lt;/p&gt;

</description>
      <category>dotnet</category>
      <category>decompiler</category>
      <category>debugging</category>
    </item>
    <item>
      <title>Node-locking and seat management for software licenses</title>
      <dc:creator>Zero Heartbeat</dc:creator>
      <pubDate>Fri, 25 Sep 2026 13:43:01 +0000</pubDate>
      <link>https://dev.to/zero_heartbeat_06a3625d7a/node-locking-and-seat-management-for-software-licenses-5hhb</link>
      <guid>https://dev.to/zero_heartbeat_06a3625d7a/node-locking-and-seat-management-for-software-licenses-5hhb</guid>
      <description>&lt;p&gt;Node-locking binds a license to a specific machine so a single key can't be copied everywhere, and seat management lets you say "this key runs on N machines at once" and actually enforce it — but the second one only works with an online activation server, because a machine on its own can't count how many others share its key. This post walks through both mechanics honestly: how a machine fingerprint with swap tolerance keeps honest users from getting locked out, how a server enforces a seat cap, how customers move their own seats, and why online activation is the piece that turns "3 seats" from a promise into a rule.&lt;/p&gt;

&lt;h2&gt;
  
  
  What node-locking actually is
&lt;/h2&gt;

&lt;p&gt;A node-lock ties a license to a &lt;strong&gt;machine fingerprint&lt;/strong&gt; — a value the SDK derives from stable hardware and OS attributes on the device. When you activate a key, that fingerprint is recorded against the license. Every later validation recomputes the fingerprint locally and checks it matches. Copy the same key to a second machine and the fingerprint won't match, so the license simply doesn't apply there.&lt;/p&gt;

&lt;p&gt;That's the whole idea, and it's genuinely useful: it means a key handed to one customer doesn't unlock a thousand installs. But the naive version has a well-known failure mode, and getting past it is the difference between a lock customers tolerate and a lock that fills your support queue.&lt;/p&gt;

&lt;h3&gt;
  
  
  The swap-tolerance problem
&lt;/h3&gt;

&lt;p&gt;If your fingerprint hashes too many hardware attributes, the lock is brittle. A customer swaps a network card, upgrades an SSD, or updates a driver, the fingerprint shifts, and a paying user is suddenly locked out of software they own — followed immediately by an angry ticket. Hash too few attributes and the lock is trivial to spoof.&lt;/p&gt;

&lt;p&gt;The fix is &lt;strong&gt;tolerance&lt;/strong&gt;: allow a small amount of drift before the machine is considered a different one. Keyright's &lt;code&gt;.NET&lt;/code&gt; SDK derives a stable fingerprint and applies a configurable &lt;code&gt;NodeLockTolerance&lt;/code&gt; (default &lt;code&gt;1&lt;/code&gt;), so an ordinary single-component change — a new NIC, a replaced disk — doesn't trip the lock, while wholesale changes still read as a different machine. For unusual environments like VMs, containers, or CI runners, you can control exactly which components identify a machine by implementing &lt;code&gt;IMachineComponents&lt;/code&gt; and setting &lt;code&gt;MachineComponents&lt;/code&gt; on the SDK options. See the &lt;a href="https://delta1labs.com/docs/keyright/integration-dotnet" rel="noopener noreferrer"&gt;.NET SDK integration guide&lt;/a&gt; for the exact surface.&lt;/p&gt;

&lt;h2&gt;
  
  
  Seats: N machines, one key
&lt;/h2&gt;

&lt;p&gt;A seat count is a promise — "this license runs on up to 3 machines" — and the moment you make it, you've walked into territory a client-side check can't handle. A single machine validating a license offline has no idea how many &lt;em&gt;other&lt;/em&gt; machines are running the same key right now. Seat counting is a shared, stateful decision, and shared state lives on a server.&lt;/p&gt;

&lt;p&gt;Here's the flow when it's done properly:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Activation records a seat.&lt;/strong&gt; When a machine activates a key, the server records the activation (the machine's fingerprint and metadata) against the license and returns a short-lived signed &lt;strong&gt;lease&lt;/strong&gt; bound to that machine.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The cap is enforced server-side.&lt;/strong&gt; Activate more machines than the license allows and the server refuses — it returns a seat-limit result and no lease, so the extra machine stays in the free/unlicensed state. There's no client-side counter to patch around.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Re-activation is idempotent.&lt;/strong&gt; A machine that's already bound can re-activate without burning a second seat. Restarting the app or refreshing the lease doesn't slowly leak your seat allowance away.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In Keyright, activation is a POST to the runtime &lt;code&gt;/v1/activate&lt;/code&gt; endpoint carrying the key and the machine id; the server consumes a seat and hands back the lease. From the client you never touch that endpoint directly — you call &lt;code&gt;ActivateAsync(key)&lt;/code&gt; and inspect the returned &lt;code&gt;LicenseInfo&lt;/code&gt;. If seats are exhausted it comes back fail-closed with a message like &lt;em&gt;"All seats for this license are in use,"&lt;/em&gt; not an exception.&lt;/p&gt;

&lt;h2&gt;
  
  
  Letting customers move a seat themselves
&lt;/h2&gt;

&lt;p&gt;Seats create a support problem the day after you ship them: a customer retires a laptop, buys a new one, and can't activate because all their seats are taken by machines they no longer use. If moving a seat means emailing you, every hardware refresh is a ticket.&lt;/p&gt;

&lt;p&gt;The answer is &lt;strong&gt;self-service deactivation&lt;/strong&gt; — let the customer free a seat without involving you:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Your app can release the seat the current machine holds by calling the runtime &lt;code&gt;/v1/free-seat&lt;/code&gt; endpoint, so a "deactivate this device" button in your own UI just works.&lt;/li&gt;
&lt;li&gt;Keyright's hosted &lt;strong&gt;customer portal&lt;/strong&gt; lets a licensee view their activations and free a seat on a device they no longer use, then activate the new one — no ticket to you.&lt;/li&gt;
&lt;li&gt;On your side, an admin can deactivate a specific machine from the dashboard's per-license &lt;strong&gt;activations&lt;/strong&gt; view, which also shows device metadata so you can see where seats are actually in use.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Revocation: the other thing only a server can do
&lt;/h2&gt;

&lt;p&gt;Seats aren't the only enforcement that needs a server. A refunded, charged-back, or leaked key has to die, and a signed license file issued last year knows nothing about a refund last week — its payload is fixed at issue time. Online activation closes that gap: revoke a key (&lt;code&gt;POST /admin/licenses/{id}/revoke&lt;/code&gt;, or one click in the dashboard) and the client drops to the free edition on its &lt;strong&gt;next lease refresh&lt;/strong&gt;. Because the lease is short-lived, "next refresh" comes around on its own.&lt;/p&gt;

&lt;p&gt;For purely offline installs, Keyright can also build and ship a &lt;strong&gt;signed revocation list&lt;/strong&gt; your app honors without a network call — slower, moving at the speed of your releases, but it means even a disconnected client isn't defenseless against a known-bad key.&lt;/p&gt;

&lt;h2&gt;
  
  
  Offline-first, online where it counts
&lt;/h2&gt;

&lt;p&gt;None of this means abandoning offline validation — it means pairing the two. Keyright is offline-first: your app verifies a signed license or a cached lease locally, against an embedded public key, with zero network. Activation happens &lt;strong&gt;once&lt;/strong&gt;; after that the cached lease keeps the app working offline for the life of the lease, and only near expiry does it reach the server again to reconcile seats and revocations. If the server is briefly unreachable, a still-valid cached lease covers the gap — a grace window — so an outage doesn't lock out a paying user.&lt;/p&gt;

&lt;p&gt;Be honest about the boundary, though. Node-locking and offline checks run on the user's machine, so a determined attacker with a decompiler can patch them out — the same truth that applies to any client-side protection. That's why the enforcement that matters — seat caps and revocation — lives on a server the attacker doesn't control, and why you pair the client check with obfuscation to make patching costly. The client check is a fast, honest gate; the server is where the rules are real.&lt;/p&gt;

&lt;h2&gt;
  
  
  Wiring it up with Keyright
&lt;/h2&gt;

&lt;p&gt;Keyright gives you node-locking with swap tolerance, server-enforced seats, self-service deactivation, and instant revocation as one system, with a &lt;code&gt;.NET&lt;/code&gt; SDK (plus Node, Python, and Java) that fails closed by design. The client half is a few lines — initialize once, call &lt;code&gt;ActivateAsync&lt;/code&gt; when a customer enters a key, and read the result. See the &lt;a href="https://delta1labs.com/docs/keyright/integration-dotnet" rel="noopener noreferrer"&gt;.NET SDK integration guide&lt;/a&gt; for the exact code, or the &lt;a href="https://delta1labs.com/keyright" rel="noopener noreferrer"&gt;Keyright product page&lt;/a&gt; for the whole picture. The free plan is enough to wire real seat enforcement end to end before you pay anything.&lt;/p&gt;

</description>
      <category>licensing</category>
      <category>dotnet</category>
      <category>guide</category>
    </item>
    <item>
      <title>A dotPeek alternative: Glass.NET, a free .NET decompiler</title>
      <dc:creator>Zero Heartbeat</dc:creator>
      <pubDate>Fri, 25 Sep 2026 13:42:08 +0000</pubDate>
      <link>https://dev.to/zero_heartbeat_06a3625d7a/a-dotpeek-alternative-glassnet-a-free-net-decompiler-1c2j</link>
      <guid>https://dev.to/zero_heartbeat_06a3625d7a/a-dotpeek-alternative-glassnet-a-free-net-decompiler-1c2j</guid>
      <description>&lt;p&gt;Looking for a dotPeek alternative? &lt;a href="https://delta1labs.com/products/glass" rel="noopener noreferrer"&gt;Glass.NET&lt;/a&gt; is a free .NET decompiler built on the same ILSpy decompilation engine, wrapped in a navigation-first UI with project export, a detailed assembly-information view and an exposure report — and it runs beyond Windows. JetBrains dotPeek is a genuinely good, free tool, so this is a fair comparison rather than a takedown: here's where each fits.&lt;/p&gt;

&lt;h2&gt;
  
  
  What is dotPeek good at?
&lt;/h2&gt;

&lt;p&gt;dotPeek is JetBrains' free standalone .NET decompiler, and it's earned its reputation. It's a mature, polished tool from a company that builds developer tooling for a living, and a lot of .NET developers already have it installed. Its strengths include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Solid decompilation&lt;/strong&gt; to readable C# using JetBrains' own engine.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Good navigation&lt;/strong&gt; — quick symbol search, find usages, and the kind of "go to everything" flow JetBrains users expect.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Export to project&lt;/strong&gt; so you can pull decompiled code into a buildable solution.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Symbol-server features&lt;/strong&gt; — it can act as a symbol server and generate PDBs, which is handy when debugging third-party code in Visual Studio.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Free&lt;/strong&gt;, with no licence required.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you're already in the JetBrains ecosystem or on Windows and comfortable with dotPeek, it's a reasonable default. We're not pretending otherwise.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where Glass.NET differs
&lt;/h2&gt;

&lt;p&gt;Glass.NET approaches the same core job — reading and understanding an unfamiliar assembly — with a few different priorities.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The ILSpy engine, out in the open.&lt;/strong&gt; Glass is built on &lt;strong&gt;ICSharpCode.Decompiler&lt;/strong&gt;, the same engine behind ILSpy. The C# it produces is accurate and tracks modern language features. This isn't a "smarter decompiler" claim — the engine is excellent and widely used, and we build on it rather than reinventing it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Navigation as the whole point.&lt;/strong&gt; Go to Definition (F12, plus Ctrl+Click), Find All References (Shift+F12) streaming into a results panel, Go To All (Ctrl+T) fuzzy quick-open across every type and member, call and type hierarchy, navigation history and a namespace ▸ type ▸ member breadcrumb. References resolve from the decompiler's own syntax tree, so you land on the right symbol, not a text match.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A full assembly-information view.&lt;/strong&gt; Select an assembly and see its identity (name, version, culture, public-key token, strong-name status), PE/runtime traits (target framework, architecture, PE format), product attributes, referenced assemblies, embedded resources and metadata statistics — all in one place.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;C# and IL views&lt;/strong&gt;, code folding, and matching dark and light themes.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Export to a buildable project&lt;/strong&gt;, plus &lt;strong&gt;assembly compare&lt;/strong&gt; with a line diff and an &lt;strong&gt;exposure report&lt;/strong&gt; that summarises what a given assembly reveals — useful for checking what an obfuscator actually produced.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Beyond Windows.&lt;/strong&gt; Glass is built on a cross-platform UI toolkit, so it isn't tied to a single OS the way a traditional Windows-only decompiler is.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;And like dotPeek, Glass is strictly &lt;strong&gt;static analysis&lt;/strong&gt; — it reads metadata and IL and reconstructs C#; it never runs the assembly it inspects.&lt;/p&gt;

&lt;h2&gt;
  
  
  How does Glass.NET compare with dotPeek?
&lt;/h2&gt;

&lt;p&gt;A quick side-by-side. Decompiler features move over time, so &lt;strong&gt;confirm current specifics on each vendor's own site&lt;/strong&gt; — this reflects our understanding at time of writing, not a guarantee.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;Glass.NET&lt;/th&gt;
&lt;th&gt;dotPeek&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Price&lt;/td&gt;
&lt;td&gt;Free&lt;/td&gt;
&lt;td&gt;Free&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Decompiler engine&lt;/td&gt;
&lt;td&gt;ICSharpCode.Decompiler (ILSpy)&lt;/td&gt;
&lt;td&gt;JetBrains' own&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Go to Definition / Find References&lt;/td&gt;
&lt;td&gt;✓&lt;/td&gt;
&lt;td&gt;✓&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Go To All (fuzzy quick-open)&lt;/td&gt;
&lt;td&gt;✓&lt;/td&gt;
&lt;td&gt;✓&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Call / type hierarchy&lt;/td&gt;
&lt;td&gt;✓&lt;/td&gt;
&lt;td&gt;✓&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;C# and IL views&lt;/td&gt;
&lt;td&gt;✓&lt;/td&gt;
&lt;td&gt;✓&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Export to buildable project&lt;/td&gt;
&lt;td&gt;✓&lt;/td&gt;
&lt;td&gt;✓&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Assembly-information view&lt;/td&gt;
&lt;td&gt;✓ (detailed)&lt;/td&gt;
&lt;td&gt;Partial&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Assembly compare + line diff&lt;/td&gt;
&lt;td&gt;✓&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Exposure report&lt;/td&gt;
&lt;td&gt;✓&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Symbol server / PDB generation&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;✓&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cross-platform (beyond Windows)&lt;/td&gt;
&lt;td&gt;✓&lt;/td&gt;
&lt;td&gt;Windows-focused&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Live debugging&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Where dotPeek may fit better for you
&lt;/h2&gt;

&lt;p&gt;Credibility means naming where the other tool wins. If any of these describe you, dotPeek may be the better pick:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;You want symbol-server / PDB generation.&lt;/strong&gt; dotPeek can generate PDBs and act as a symbol server so Visual Studio can step into third-party code. Glass is focused on reading and navigating, not feeding a debugger — if the symbol-server workflow is central to you, dotPeek delivers it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;You're deep in the JetBrains ecosystem&lt;/strong&gt; and want a tool that matches ReSharper/Rider muscle memory and conventions.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;You need live debugging of a managed binary.&lt;/strong&gt; Neither dotPeek nor Glass does that — it's a different job. The community-maintained dnSpyEx remains the usual choice there.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  How should you decide?
&lt;/h2&gt;

&lt;p&gt;Don't decide on a feature list — decide on &lt;em&gt;your&lt;/em&gt; workflow. Both tools are free, so the cost of trying is only your time:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Open an assembly you actually work with in each tool.&lt;/li&gt;
&lt;li&gt;Try the things you do most — jump to a definition, find every caller of a method, quick-open a type by name, read the IL behind a tricky method, export to a project.&lt;/li&gt;
&lt;li&gt;See which one lets you &lt;em&gt;move&lt;/em&gt; through the code without friction.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;For most read-and-understand work — inspecting your own binaries, understanding a dependency, checking what an obfuscator produced, security review, or recovering your own lost source — Glass.NET is designed to make navigation feel like reading your own source. (Use any decompiler only on software you have the right to analyze.)&lt;/p&gt;

&lt;p&gt;That "checking what an obfuscator produced" case is worth calling out: it's exactly why Glass pairs well with &lt;a href="https://delta1labs.com/products/nebula" rel="noopener noreferrer"&gt;Nebula.NET&lt;/a&gt;. Protect a build, open it in Glass, and confirm for yourself that the names are gone, the control flow is flattened, and the encrypted methods show only a stub — the exposure report makes it a one-click check.&lt;/p&gt;

&lt;h2&gt;
  
  
  Get Glass.NET
&lt;/h2&gt;

&lt;p&gt;Glass.NET is completely free — no licence, no seats, no sign-up — and actively developed. &lt;a href="https://delta1labs.com/download?product=glass" rel="noopener noreferrer"&gt;Download Glass.NET&lt;/a&gt;, open an assembly, and press F12. If you want the wider field, we also compare it with &lt;a href="https://delta1labs.com/blog/glass-net-vs-ilspy-dnspy-dotpeek/" rel="noopener noreferrer"&gt;ILSpy, dnSpy and dotPeek together&lt;/a&gt;. Found a bug or have a feature request? &lt;a href="https://delta1labs.com/support#contact" rel="noopener noreferrer"&gt;Tell us&lt;/a&gt; — we're building it in the open.&lt;/p&gt;

</description>
      <category>dotnet</category>
      <category>decompiler</category>
      <category>comparison</category>
    </item>
    <item>
      <title>Method encryption in .NET: how it protects your most sensitive code</title>
      <dc:creator>Zero Heartbeat</dc:creator>
      <pubDate>Fri, 25 Sep 2026 13:42:05 +0000</pubDate>
      <link>https://dev.to/zero_heartbeat_06a3625d7a/method-encryption-in-net-how-it-protects-your-most-sensitive-code-4721</link>
      <guid>https://dev.to/zero_heartbeat_06a3625d7a/method-encryption-in-net-how-it-protects-your-most-sensitive-code-4721</guid>
      <description>&lt;p&gt;Method encryption is the strongest static-decompilation defence Nebula.NET applies to an individual method: the method's IL is stored &lt;strong&gt;encrypted&lt;/strong&gt; inside your assembly and only decrypted and re-emitted at runtime, so a decompiler opening your DLL finds no readable C# and no readable IL for that method — just a stub that hands control to a runtime helper. This article explains how that works, how it differs from renaming, string encryption and control-flow obfuscation, when it's the right tool, and the trade-offs you need to respect. For the broader picture, our guide to &lt;a href="https://delta1labs.com/blog/how-to-protect-dotnet-code-from-decompilation/" rel="noopener noreferrer"&gt;protecting .NET code from decompilation&lt;/a&gt; sets the scene.&lt;/p&gt;

&lt;h2&gt;
  
  
  What does "whole-method encryption" actually mean?
&lt;/h2&gt;

&lt;p&gt;When you compile C#, F# or VB.NET, each method body is emitted as Intermediate Language (IL) and stored in the assembly's metadata in the clear. That IL is precisely what decompilers read to reconstruct near-original C#. Method encryption breaks that chain.&lt;/p&gt;

&lt;p&gt;At protection time, Nebula.NET takes the chosen method's IL, encrypts it, and stores the ciphertext in the assembly. The method's original body is replaced with a small stub. There is no longer any standard IL for that method for a decompiler to interpret — &lt;a href="https://delta1labs.com/products/glass" rel="noopener noreferrer"&gt;Glass.NET&lt;/a&gt;, ILSpy or dotPeek see the stub and a blob of encrypted bytes that mean nothing without the key and the decryptor.&lt;/p&gt;

&lt;p&gt;At runtime, the first time that method is needed, an injected helper decrypts the stored IL, re-emits it into a live dynamic method the JIT can compile, and dispatches to it. The observable behaviour is identical to the original — same inputs, outputs and exceptions — because it &lt;em&gt;is&lt;/em&gt; the original IL, just reconstructed in memory at the last possible moment rather than sitting on disk. That's the whole point: &lt;strong&gt;the plaintext logic only ever exists while the method actually runs&lt;/strong&gt;, never as something a static tool can read.&lt;/p&gt;

&lt;h2&gt;
  
  
  Now fully self-contained — no extra DLL to ship
&lt;/h2&gt;

&lt;p&gt;Earlier method-encryption implementations (Nebula's included) leaned on an external runtime assembly copied next to your output. That's no longer the case. The re-emit runtime is now &lt;strong&gt;injected directly into your protected assembly&lt;/strong&gt; — no &lt;code&gt;Nebula.Runtime.dll&lt;/code&gt; or any other companion file to remember, deploy, or accidentally leave behind. You ship exactly the one assembly you were already shipping, and the decrypt-and-emit machinery travels inside it. For single-file publishing and simple xcopy deployment, that removes a whole class of packaging mistakes.&lt;/p&gt;

&lt;h2&gt;
  
  
  How is it different from the other obfuscation layers?
&lt;/h2&gt;

&lt;p&gt;Method encryption doesn't replace the rest of your protection stack — it sits on top of it, targeting a different weakness.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Identifier renaming&lt;/strong&gt; hides &lt;em&gt;what things are called&lt;/em&gt; by turning &lt;code&gt;ValidateLicenseKey&lt;/code&gt; into &lt;code&gt;a&lt;/code&gt;. The IL is still fully present and readable; you've just removed the helpful names. Method encryption removes the IL itself.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;String encryption&lt;/strong&gt; hides literal data — endpoints, messages, keys — so searching the binary for a keyword turns up nothing. It says nothing about your logic. See &lt;a href="https://delta1labs.com/blog/string-encryption-dotnet/" rel="noopener noreferrer"&gt;string encryption in .NET&lt;/a&gt; for where that layer fits.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Control-flow obfuscation&lt;/strong&gt; keeps the IL readable but &lt;em&gt;scrambles its structure&lt;/em&gt; — flattening straight-line code into a dispatcher state machine so the decompiler emits a tangle of &lt;code&gt;goto&lt;/code&gt;s instead of clean &lt;code&gt;if/else&lt;/code&gt;. A skilled analyst (or an automated deobfuscator) can still work through it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Method encryption&lt;/strong&gt; removes the method body from static view entirely. There's nothing to structurally deobfuscate because there's no IL on disk to read.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;They're complementary. A sensible build renames everything internal, encrypts strings everywhere and flattens control flow broadly — then reserves method encryption (or &lt;a href="https://delta1labs.com/articles/code-virtualization" rel="noopener noreferrer"&gt;code virtualization&lt;/a&gt;) for the handful of methods that genuinely matter.&lt;/p&gt;

&lt;h3&gt;
  
  
  Method encryption vs. code virtualization
&lt;/h3&gt;

&lt;p&gt;These two are the heavy hitters, and they're easy to confuse. Both leave a decompiler with nothing readable for the protected method, but the mechanism differs:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Virtualization&lt;/strong&gt; translates the method into bytecode for a &lt;em&gt;custom&lt;/em&gt; virtual machine embedded in your assembly. The original IL never exists anywhere; an attacker must reverse-engineer your specific VM before they can even begin. It's the highest cost to break, at the price of larger, meaningfully slower methods.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Method encryption&lt;/strong&gt; keeps the &lt;em&gt;real&lt;/em&gt; IL but stores it encrypted and rebuilds it at runtime. It reproduces the exact original semantics with no VM to design around, and is lighter on size. Its hard requirement is a runtime that can emit IL.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you can run a full JIT and want the exact original method faithfully restored, method encryption is a clean fit. For the maximum static &lt;em&gt;and&lt;/em&gt; dynamic barrier, virtualization goes further.&lt;/p&gt;

&lt;h2&gt;
  
  
  When should you use method encryption?
&lt;/h2&gt;

&lt;p&gt;The same discipline applies as with virtualization: &lt;strong&gt;protect your crown jewels, not your whole assembly.&lt;/strong&gt; Good candidates are a small number of high-value methods:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;License and activation checks&lt;/strong&gt; — where you don't want an attacker reading the exact comparison and patching around it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Proprietary algorithms&lt;/strong&gt; — the pricing model, matching heuristic or signal-processing kernel that is the actual product.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Anti-tamper and integrity logic&lt;/strong&gt; — the code that verifies your binary hasn't been modified, which you specifically don't want on display.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;There are eligibility limits worth knowing up front. Method encryption in Nebula covers &lt;strong&gt;static, non-generic methods without exception handlers or by-ref/pointer parameters&lt;/strong&gt; — the shapes that re-emit cleanly and reliably. That's a deliberate constraint: correctness first. For methods outside it, lean on control-flow obfuscation and, where licensed, virtualization. You select targets explicitly, keeping your hot paths fast and untouched. See the &lt;a href="https://delta1labs.com/docs/nebula" rel="noopener noreferrer"&gt;Nebula.NET docs&lt;/a&gt; for the configuration keys.&lt;/p&gt;

&lt;h2&gt;
  
  
  The trade-offs — read these honestly
&lt;/h2&gt;

&lt;p&gt;Method encryption's one big dependency is a &lt;strong&gt;runtime that can emit IL&lt;/strong&gt;. Re-emitting the decrypted method body uses the runtime's dynamic code-generation path (Reflection.Emit), which needs a full JIT. That draws a hard line across .NET targets:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Works:&lt;/strong&gt; .NET Framework, .NET (Core) on desktop and server, and single-file / self-contained publishes — anywhere a normal JIT is present.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Does not work:&lt;/strong&gt; &lt;strong&gt;Blazor WebAssembly&lt;/strong&gt; and &lt;strong&gt;NativeAOT&lt;/strong&gt;. Blazor WASM runs under a trimmed WebAssembly runtime, and NativeAOT compiles everything ahead of time to native code — neither has runtime IL emit for the re-emit step. For Blazor WASM, use the dedicated &lt;a href="https://delta1labs.com/blog/obfuscate-blazor-webassembly-webcil/" rel="noopener noreferrer"&gt;WebCil-based obfuscation&lt;/a&gt; instead.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Beyond target support, keep the usual caveats in mind:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;A small first-call cost.&lt;/strong&gt; Decrypting and emitting a method the first time it runs adds a one-time overhead before the JIT takes over — negligible for the few sensitive methods you'd apply this to.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;It's not encryption of your program.&lt;/strong&gt; The decrypted IL is in memory while the method executes, so a determined attacker with a debugger and a memory dump can, in principle, capture it. Method encryption defeats &lt;em&gt;static&lt;/em&gt; decompilation outright and raises the bar on dynamic analysis — it does not make your code uncrackable.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;It doesn't enforce licensing by itself.&lt;/strong&gt; A client-side check, however well hidden, runs on the attacker's machine. Method encryption makes bypassing it expensive, not impossible.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Where does the real security boundary live?
&lt;/h2&gt;

&lt;p&gt;This is the part vendors skip, so we'll say it plainly: any protection that runs on a machine you don't control raises cost — it doesn't create an impassable wall. The honest goal is economic. You want reversing your logic to cost more than the result is worth, so the casual "decompile, copy, ship" attack dies immediately and the serious attacker finds the return not worth the effort.&lt;/p&gt;

&lt;p&gt;Method encryption is excellent at that job — it takes your most sensitive methods off the static-decompilation table entirely, at low runtime cost, with no extra file to deploy. But the real secrets and enforcement belong on a server you control: validate licenses through online activation, keep master keys and privileged logic server-side, and treat the client protection as the expensive-to-climb wall around them — not the vault itself.&lt;/p&gt;

&lt;h2&gt;
  
  
  Trying it with Nebula.NET
&lt;/h2&gt;

&lt;p&gt;Method encryption is one transform in &lt;a href="https://delta1labs.com/products/nebula" rel="noopener noreferrer"&gt;Nebula.NET&lt;/a&gt;, alongside renaming, string encryption, control-flow obfuscation, anti-tamper and (Enterprise) code virtualization. The workflow to evaluate it is the only benchmark that matters:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Build your assembly and confirm your tests pass.&lt;/li&gt;
&lt;li&gt;Enable method encryption on a few chosen methods and rebuild.&lt;/li&gt;
&lt;li&gt;Run your test suite against the protected build — behaviour must be identical.&lt;/li&gt;
&lt;li&gt;Open the result in &lt;a href="https://delta1labs.com/products/glass" rel="noopener noreferrer"&gt;Glass.NET&lt;/a&gt; and browse to those methods. You should see a stub and encrypted bytes where clean C# used to be.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;That fourth step is why we ship a free decompiler alongside the protector — you audit our work rather than take our word for it. The &lt;a href="https://delta1labs.com/download" rel="noopener noreferrer"&gt;free edition&lt;/a&gt; runs the whole loop on your own binary. Read the configuration details in the &lt;a href="https://delta1labs.com/docs/nebula" rel="noopener noreferrer"&gt;Nebula.NET documentation&lt;/a&gt;, pick your crown-jewel methods, and see the before-and-after for yourself.&lt;/p&gt;

</description>
      <category>dotnet</category>
      <category>obfuscation</category>
      <category>security</category>
    </item>
    <item>
      <title>License key generator for .NET: why you shouldn''t roll your own</title>
      <dc:creator>Zero Heartbeat</dc:creator>
      <pubDate>Fri, 25 Sep 2026 13:41:33 +0000</pubDate>
      <link>https://dev.to/zero_heartbeat_06a3625d7a/license-key-generator-for-net-why-you-shouldnt-roll-your-own-3071</link>
      <guid>https://dev.to/zero_heartbeat_06a3625d7a/license-key-generator-for-net-why-you-shouldnt-roll-your-own-3071</guid>
      <description>&lt;p&gt;If you searched for a ".NET license key generator," the honest answer is that you don't want a key generator — you want a signed license system, and the two are not the same thing. A generator produces a key string your app validates by its shape, which means the logic that decides "this key is valid" is the same secret an attacker extracts to build a keygen. A real license is a small signed document: the server signs it with a private key that never ships, your app verifies the signature against an embedded public key, and forgery becomes cryptographically impossible. This post explains why the homegrown route fails and what the proper version looks like in .NET.&lt;/p&gt;

&lt;h2&gt;
  
  
  What people mean by "key generator" — and why it's the wrong mental model
&lt;/h2&gt;

&lt;p&gt;The classic design goes like this: generate keys in a recognizable pattern (grouped characters, a checksum digit, maybe a segment derived from the customer name), hand one to each buyer, and have the app accept any key that matches the pattern. It feels like security because the keys look official.&lt;/p&gt;

&lt;p&gt;It isn't, and the reason is structural. &lt;strong&gt;If your app can decide a key is valid by inspecting the key itself, then everything needed to make a valid key is inside your app.&lt;/strong&gt; A decompiler turns your validation routine into a generation routine — that's literally what a "keygen" is. Because .NET assemblies decompile cleanly back to readable C#, this isn't a theoretical risk; it's a first-afternoon result for anyone who bothers.&lt;/p&gt;

&lt;p&gt;And even if the format were somehow un-guessable, a format-based key has no answers to the questions that actually run a software business:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;How does it expire?&lt;/strong&gt; A subscription needs an end date the key carries and the app enforces.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;How is it bound to a machine?&lt;/strong&gt; Nothing stops one key being pasted onto a thousand installs.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;How do you unlock different features per plan?&lt;/strong&gt; The key is opaque; it can't say "this customer gets export but not the API."&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;How do you kill a leaked or refunded key?&lt;/strong&gt; You can't recall a string that's already in the wild.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A generator produces none of that. It produces a string.&lt;/p&gt;

&lt;h2&gt;
  
  
  What a real license is: a signed document
&lt;/h2&gt;

&lt;p&gt;Replace "generate a key" with "sign a license." The unit isn't a random string — it's a small payload of facts (licensee, product, tier, seats, expiry, entitlements) with a cryptographic &lt;strong&gt;signature&lt;/strong&gt; attached.&lt;/p&gt;

&lt;p&gt;Here's the asymmetry that makes it work. Your server holds an &lt;strong&gt;RSA private key&lt;/strong&gt; and uses it to sign the payload. Your app embeds only the matching &lt;strong&gt;public key&lt;/strong&gt;. A public key can &lt;em&gt;verify&lt;/em&gt; that a signature matches a payload, but it can never &lt;em&gt;create&lt;/em&gt; one. So:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Decompile the app, read the public key, publish it on a billboard — an attacker still can't forge a license, because forging requires the private key that never left your server.&lt;/li&gt;
&lt;li&gt;Change one byte of the payload — bump &lt;code&gt;tier&lt;/code&gt; from &lt;code&gt;free&lt;/code&gt; to &lt;code&gt;pro&lt;/code&gt; — and the signature no longer matches. Validation fails.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is the exact mechanism behind TLS and code signing, pointed at licensing. In .NET the primitives ship in the box (&lt;code&gt;System.Security.Cryptography.RSA&lt;/code&gt;), and verifying a signature is a few lines. Which is precisely the trap: the &lt;em&gt;signature check&lt;/em&gt; is easy, so people assume the whole system is easy, and it isn't.&lt;/p&gt;

&lt;h2&gt;
  
  
  The parts a signature alone doesn't give you
&lt;/h2&gt;

&lt;p&gt;A valid signature proves the license is genuine and unaltered. It does not, by itself, do any of the following — and a real system needs all of them:&lt;/p&gt;

&lt;h3&gt;
  
  
  Expiry and clock-tamper defense
&lt;/h3&gt;

&lt;p&gt;A subscription or trial carries an expiry the app compares against the current time. That immediately invites the obvious attack — roll the clock back and a trial never ends — so offline validation has to remember the latest time it legitimately saw and treat a big backward jump as tampering. Keyright's SDK does this: moving the clock back beyond a &lt;code&gt;ClockTamperToleranceHours&lt;/code&gt; window (default 24h) on a time-limited license yields a &lt;code&gt;ClockTampered&lt;/code&gt; status and refuses until the time is corrected. Perpetual licenses aren't subject to it.&lt;/p&gt;

&lt;h3&gt;
  
  
  Node-locking
&lt;/h3&gt;

&lt;p&gt;Binding a license to a machine needs a stable fingerprint &lt;em&gt;with tolerance&lt;/em&gt;, so a customer who swaps a NIC or disk isn't locked out. Too strict and you generate support tickets; too loose and the lock is meaningless. Keyright derives the fingerprint and applies a configurable &lt;code&gt;NodeLockTolerance&lt;/code&gt; (default &lt;code&gt;1&lt;/code&gt;).&lt;/p&gt;

&lt;h3&gt;
  
  
  Entitlements
&lt;/h3&gt;

&lt;p&gt;Shipping one binary that unlocks different capabilities per plan means the license has to &lt;em&gt;carry&lt;/em&gt; those capabilities. Keyright bakes an &lt;strong&gt;entitlement template&lt;/strong&gt; into each tier — named flags (&lt;code&gt;"export": "true"&lt;/code&gt;) and numeric limits (&lt;code&gt;"max-projects": "10"&lt;/code&gt;) — and the SDK reads them locally: &lt;code&gt;IsEnabled("export")&lt;/code&gt;, &lt;code&gt;GetLimit("max-projects", fallback: 1)&lt;/code&gt;. Gate features on entitlements, not on a hard-coded tier check, and changing a plan doesn't mean shipping new code.&lt;/p&gt;

&lt;h3&gt;
  
  
  Revocation
&lt;/h3&gt;

&lt;p&gt;A leaked, refunded, or charged-back license has to die — and a signed payload issued last year knows nothing about a refund last week. Online, Keyright revokes a key server-side and the client drops to free on its next lease refresh. Offline, it can ship a &lt;strong&gt;signed revocation list&lt;/strong&gt; your app honors with no network call. A format-based key generator has no revocation story at all.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the proper .NET flow looks like
&lt;/h2&gt;

&lt;p&gt;With Keyright the "generator" is replaced by a server that signs, and your client verifies. Every tenant gets its own isolated RSA key pair; the private half is stored AES-256-GCM-encrypted server-side and never exposed, so your signatures never share a key with anyone else. Your app embeds only the public half:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;KeyrightClient&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Initialize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="n"&gt;KeyrightOptions&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;Product&lt;/span&gt;         &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"acme-app"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;PublicKeyBase64&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"MIIBIjANBgkq..."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;   &lt;span class="c1"&gt;// not a secret — ships in your binary&lt;/span&gt;
    &lt;span class="n"&gt;ServiceUrl&lt;/span&gt;      &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"https://keyright.delta1labs.com"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="c1"&gt;// Offline: verify the signature, product, node-lock, expiry — no network, never throws.&lt;/span&gt;
&lt;span class="n"&gt;LicenseInfo&lt;/span&gt; &lt;span class="n"&gt;info&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Validate&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;info&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;IsPaid&lt;/span&gt; &lt;span class="p"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;IsEnabled&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"export"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="cm"&gt;/* unlock the feature */&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;Validate()&lt;/code&gt; verifies the signature against the embedded public key, checks node-lock and expiry, honors any shipped revocation list, and &lt;strong&gt;fails closed&lt;/strong&gt; — any problem resolves to the free edition carrying a &lt;code&gt;Status&lt;/code&gt; (&lt;code&gt;SignatureInvalid&lt;/code&gt;, &lt;code&gt;Expired&lt;/code&gt;, &lt;code&gt;Revoked&lt;/code&gt;, &lt;code&gt;MachineMismatch&lt;/code&gt;, …) rather than throwing or, worse, unlocking. For seat enforcement and prompt revocation you add one online &lt;code&gt;ActivateAsync(key)&lt;/code&gt; call, which exchanges the key for a machine-bound lease.&lt;/p&gt;

&lt;h2&gt;
  
  
  Be honest about the limit
&lt;/h2&gt;

&lt;p&gt;Signing kills forgery dead — nobody keygens a license they can't sign. But the code that reads the result still runs on the user's machine, and the branch that unlocks a feature can be patched out by a determined attacker with a decompiler. That's not an argument against signing; it's the reason you pair it with two things: &lt;strong&gt;online activation&lt;/strong&gt;, so seats and revocation are enforced on a server the attacker doesn't control, and &lt;strong&gt;obfuscation&lt;/strong&gt;, so patching the client gate is expensive. Signing defeats the forger; the rest defeats the patcher.&lt;/p&gt;

&lt;h2&gt;
  
  
  Skip the generator, ship the system
&lt;/h2&gt;

&lt;p&gt;The takeaway isn't "you can't write an RSA verify" — you can, in an afternoon. It's that a license system is signing plus expiry, node-lock, entitlements, revocation, seats, and a fail-closed client, and a random key generator gives you none of it while looking like it does. &lt;a href="https://delta1labs.com/keyright" rel="noopener noreferrer"&gt;Keyright&lt;/a&gt; is that system, built: per-tenant RSA signing, offline verify, entitlements, and revocation, with a .NET-first SDK. The &lt;a href="https://delta1labs.com/docs/keyright/getting-started" rel="noopener noreferrer"&gt;getting-started guide&lt;/a&gt; walks the full path from an empty workspace to a shipping licensed app, and the free plan covers real licensing before you pay a cent.&lt;/p&gt;

</description>
      <category>licensing</category>
      <category>dotnet</category>
      <category>security</category>
    </item>
    <item>
      <title>How to add a free trial to your software (that doesn''t leak)</title>
      <dc:creator>Zero Heartbeat</dc:creator>
      <pubDate>Fri, 25 Sep 2026 13:41:17 +0000</pubDate>
      <link>https://dev.to/zero_heartbeat_06a3625d7a/how-to-add-a-free-trial-to-your-software-that-doesnt-leak-2mp0</link>
      <guid>https://dev.to/zero_heartbeat_06a3625d7a/how-to-add-a-free-trial-to-your-software-that-doesnt-leak-2mp0</guid>
      <description>&lt;p&gt;To add a free trial that doesn't leak, issue a &lt;strong&gt;signed, time-limited license key&lt;/strong&gt; that auto-expires, node-lock it so one machine gets one trial, keep it revocable and tracked, and let it convert to paid without re-keying — the opposite of a registry flag or a date file, both of which a user resets in seconds. This post covers what "done right" means, why the naive approaches fail, and how to wire a self-service trial button on your own site so you never email a license by hand.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why the easy trial always leaks
&lt;/h2&gt;

&lt;p&gt;The tempting way to build a trial is to record when the software first ran and count down from there. Two variants, both broken:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The registry / date-file trial.&lt;/strong&gt; On first launch you write a timestamp to a registry value or a file, and on later launches you compare against it. Deleting that value — or reinstalling into a fresh VM — resets the clock. There's nothing signed, so the app has no trustworthy memory of whether the trial was already used.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The clock-comparison trial.&lt;/strong&gt; You store an expiry and compare it against &lt;code&gt;DateTime.UtcNow&lt;/code&gt;. Roll the system clock backward and the trial never ends. Without a signed reference time, the app can't tell a real date from a rolled-back one.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Both share the same root flaw: the trial state lives somewhere the user fully controls, in a form nothing can verify. The fix isn't a cleverer hiding spot — it's a signed key the app can actually trust.&lt;/p&gt;

&lt;h2&gt;
  
  
  What a leak-resistant trial looks like
&lt;/h2&gt;

&lt;p&gt;A proper trial is just a license with a short life. It carries the same protections a paid license does, so the same cheats don't work.&lt;/p&gt;

&lt;h3&gt;
  
  
  It's a signed, auto-expiring key
&lt;/h3&gt;

&lt;p&gt;The trial key is a payload (product, tier, seats, &lt;code&gt;expiryUtc&lt;/code&gt;, a &lt;code&gt;trial&lt;/code&gt; flag) signed server-side with your private key. Your app verifies the signature against an embedded public key and reads the expiry from the &lt;em&gt;signed&lt;/em&gt; payload — not from a value on disk the user can edit. When the expiry passes, the key stops validating and the app fails closed to the free/unlicensed state. No server call is needed for it to expire; the expiry is baked in and tamper-evident.&lt;/p&gt;

&lt;p&gt;Because it's a real signed license, the clock-rollback cheat is covered too. Keyright's SDK remembers the latest time it legitimately saw and treats a large backward jump on a time-limited license as tampering — a &lt;code&gt;ClockTampered&lt;/code&gt; status that refuses to validate until the clock is corrected (see the &lt;a href="https://delta1labs.com/docs/keyright/integration-dotnet" rel="noopener noreferrer"&gt;.NET SDK integration guide&lt;/a&gt;).&lt;/p&gt;

&lt;h3&gt;
  
  
  It's node-locked — one trial per machine
&lt;/h3&gt;

&lt;p&gt;A signed key with an expiry is still copyable, so bind it to the machine. Node-locking ties the trial to a stable machine fingerprint (with a small swap tolerance so a hardware change doesn't lock an honest evaluator out), which means one device gets one trial rather than one key seeding a hundred installs.&lt;/p&gt;

&lt;h3&gt;
  
  
  It's issued one-per-identity
&lt;/h3&gt;

&lt;p&gt;Node-locking stops one key spreading; issuing controls stop one person taking endless &lt;em&gt;new&lt;/em&gt; trials. Keyright's self-service trial is &lt;strong&gt;one trial per email per product&lt;/strong&gt; — re-requesting with the same email returns the &lt;em&gt;same&lt;/em&gt; key (&lt;code&gt;created: false&lt;/code&gt;) instead of minting a new one. A page refresh or a second click never resets the clock or stacks trials. It's idempotent by design, which is also what makes it abuse-resistant.&lt;/p&gt;

&lt;h3&gt;
  
  
  It's revocable and tracked
&lt;/h3&gt;

&lt;p&gt;A trial you can't kill is a liability. Keyright trial keys are fully revocable like any license, and every trial is tracked in your dashboard — the Home overview counts them, and the Licenses table shows each one with its expiry and per-machine activations. You can see who's trialing, when their key lapses, and reach out before it does.&lt;/p&gt;

&lt;h3&gt;
  
  
  It converts to paid without re-keying
&lt;/h3&gt;

&lt;p&gt;The single most important property: a trial should become a purchase with no friction. Because a Keyright trial key activates through the &lt;em&gt;exact same&lt;/em&gt; path as a paid key, when the customer buys and activates a non-trial key it &lt;strong&gt;supersedes&lt;/strong&gt; the leftover trial with no reinstall and no re-keying. The app flips from &lt;em&gt;"Pro Trial — 27 days left"&lt;/em&gt; to the paid edition on the next activation. Your app reads the state straight off &lt;code&gt;LicenseInfo&lt;/code&gt; — &lt;code&gt;info.IsTrial&lt;/code&gt;, &lt;code&gt;info.DaysRemaining&lt;/code&gt;, &lt;code&gt;info.StatusBadge&lt;/code&gt; — so the trial countdown and the upgrade prompt are a few property reads, not a separate code path.&lt;/p&gt;

&lt;h2&gt;
  
  
  Self-service: a trial button on your own site
&lt;/h2&gt;

&lt;p&gt;The best trial funnel doesn't involve you at all. Keyright's trial endpoint, &lt;code&gt;POST /v1/trial&lt;/code&gt;, is public and CORS-open, so a plain form on your marketing site can call it directly and the customer gets a key by email — no dashboard clicks, no hand-sent license files.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;button&lt;/span&gt; &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"trial"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Start free trial&lt;span class="nt"&gt;&amp;lt;/button&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;script&amp;gt;&lt;/span&gt;
&lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getElementById&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;trial&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;onclick&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;https://keyright.delta1labs.com/v1/trial&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;POST&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;content-type&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;application/json&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="na"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;product&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;acme-app&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;email&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;userEmail&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;company&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;userCompany&lt;/span&gt; &lt;span class="p"&gt;}),&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ok&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nf"&gt;show&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`Your trial key: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; (expires &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;expiresUtc&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;)`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="nf"&gt;show&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/script&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Trials are &lt;strong&gt;off by default&lt;/strong&gt; — a product offers one only once you opt in by setting a trial length, tier, and seat count (in the dashboard's Products tab, or &lt;code&gt;POST /admin/products/{slug}/trial&lt;/code&gt;). The response returns the key, its tier, and &lt;code&gt;expiresUtc&lt;/code&gt;, and Keyright emails it to the address. The full flow, field reference, and status codes are in the &lt;a href="https://delta1labs.com/docs/keyright/trials" rel="noopener noreferrer"&gt;self-service free trials guide&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;For an air-gapped or enterprise evaluator that can't reach the network, you can hand out a &lt;strong&gt;signed, time-limited offline trial file&lt;/strong&gt; instead — same trial semantics, delivered as a file the SDK verifies locally against your public key.&lt;/p&gt;

&lt;h2&gt;
  
  
  The customer activates it like any key
&lt;/h2&gt;

&lt;p&gt;There's no separate "trial mode" in your app. The trial key activates through the same &lt;code&gt;ActivateAsync(key)&lt;/code&gt; call as a paid key: it performs an online activation, binds the machine, and caches a signed lease with &lt;code&gt;trial: true&lt;/code&gt; and the trial's expiry. Thanks to that cached lease the app &lt;strong&gt;keeps working offline until the trial ends&lt;/strong&gt;, then fails closed. One code path handles trial and paid alike.&lt;/p&gt;

&lt;h2&gt;
  
  
  Be honest about the ceiling
&lt;/h2&gt;

&lt;p&gt;A signed, node-locked, tracked trial defeats the casual cheats — deleting a registry key, rolling the clock, refreshing for a new trial — comprehensively. What it can't do alone is stop a determined attacker who decompiles your app and patches the check out, because that check runs on the user's machine. That's the same limit every client-side protection has, and the same answer applies: pair the trial's client check with &lt;strong&gt;online activation&lt;/strong&gt;, so expiry, seats, and revocation are decided on a server the attacker doesn't control, and with obfuscation so patching the client is expensive. The trial gate is honest and hard to reset; the server is where it's enforced.&lt;/p&gt;

&lt;h2&gt;
  
  
  Add a trial this week
&lt;/h2&gt;

&lt;p&gt;A trial done right is a signed, expiring, node-locked, revocable key that converts to paid on its own — and &lt;a href="https://delta1labs.com/keyright" rel="noopener noreferrer"&gt;Keyright&lt;/a&gt; gives you all of it, self-service, from one API call. Turn on trials for your product, drop a button on your site, and read the state off &lt;code&gt;LicenseInfo&lt;/code&gt; in your app. &lt;a href="https://delta1labs.com/keyright/signup" rel="noopener noreferrer"&gt;Sign up free&lt;/a&gt; and follow the &lt;a href="https://delta1labs.com/docs/keyright/trials" rel="noopener noreferrer"&gt;self-service free trials guide&lt;/a&gt; to wire it end to end — the free plan is enough to ship a real, leak-resistant trial before you pay anything.&lt;/p&gt;

</description>
      <category>licensing</category>
      <category>dotnet</category>
      <category>guide</category>
    </item>
    <item>
      <title>Offline license validation with signed keys, explained</title>
      <dc:creator>Zero Heartbeat</dc:creator>
      <pubDate>Wed, 23 Sep 2026 14:17:56 +0000</pubDate>
      <link>https://dev.to/zero_heartbeat_06a3625d7a/offline-license-validation-with-signed-keys-explained-462l</link>
      <guid>https://dev.to/zero_heartbeat_06a3625d7a/offline-license-validation-with-signed-keys-explained-462l</guid>
      <description>&lt;p&gt;Offline license validation is the ability to confirm a license is authentic and still valid without any network call — and it works because of one asymmetry in public-key cryptography: your server holds a private key that can &lt;em&gt;create&lt;/em&gt; signatures, while your app holds only the matching public key, which can &lt;em&gt;verify&lt;/em&gt; a signature but never forge one. That single property lets a client prove a license was issued by you, and hasn't been tampered with, entirely on its own machine. This post explains how that works end to end — signatures, node-locking, expiry, revocation, and grace windows — and why the whole thing has to &lt;strong&gt;fail closed&lt;/strong&gt; to be worth anything.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why a signature is enough to trust a license offline
&lt;/h2&gt;

&lt;p&gt;Imagine your app just checked whether a license file said &lt;code&gt;"tier": "pro"&lt;/code&gt;. Trivially defeated — anyone can edit a text file. The signature is what makes the file trustworthy.&lt;/p&gt;

&lt;p&gt;When you issue a license, the server takes the license payload (licensee, product, tier, seats, expiry, entitlements) and computes an &lt;strong&gt;RSA signature&lt;/strong&gt; over it with a &lt;strong&gt;private key&lt;/strong&gt;. That signature is attached to the license. Your app embeds the matching &lt;strong&gt;public key&lt;/strong&gt; and, at validation time, recomputes the check: does this signature correspond to this exact payload under this public key? If yes, two things are simultaneously proven — the license was signed by &lt;em&gt;your&lt;/em&gt; private key (authenticity), and not a single byte has changed since (integrity). Flip one character in the tier field and the signature no longer matches; validation fails.&lt;/p&gt;

&lt;p&gt;The reason this needs no server is that verification is pure math on data the app already has. The public key isn't a secret and can ship right inside your binary — decompile the app, read the key, publish it, and an attacker still can't mint a license, because minting requires the private key that never left your server. This is the same asymmetry behind TLS and code signing, applied to licensing.&lt;/p&gt;

&lt;h2&gt;
  
  
  The checks that run after the signature
&lt;/h2&gt;

&lt;p&gt;A valid signature only proves the license is genuine and unaltered. Offline validation then reads the now-trusted fields and enforces them locally.&lt;/p&gt;

&lt;h3&gt;
  
  
  Node-locking to a machine fingerprint
&lt;/h3&gt;

&lt;p&gt;To stop one license from being copied to a thousand machines, a license can be &lt;strong&gt;node-locked&lt;/strong&gt;: bound to a fingerprint the app derives from stable hardware and OS attributes. At validation the app recomputes the fingerprint and checks it against the one baked into the license. Mismatch, and the license doesn't apply here.&lt;/p&gt;

&lt;p&gt;The subtlety is tolerance. Fingerprint too strictly and a customer who swaps a network card or upgrades a disk gets locked out. So a good implementation allows a small amount of drift — Keyright's SDK uses a configurable &lt;code&gt;NodeLockTolerance&lt;/code&gt; (default &lt;code&gt;1&lt;/code&gt;) so an ordinary hardware change doesn't trip the lock — and lets you control which components identify a machine for unusual environments like VMs and CI runners.&lt;/p&gt;

&lt;h3&gt;
  
  
  Expiry and clock tampering
&lt;/h3&gt;

&lt;p&gt;A subscription or trial carries an &lt;code&gt;expiryUtc&lt;/code&gt;, and offline validation simply compares it to the current time. Which raises the obvious attack: roll the system clock backward and a trial never ends. Offline validation has to defend against that without a time server. The standard approach is to remember the latest time the app has legitimately seen and treat a clock that jumps meaningfully backward as tampering. Keyright's SDK does exactly this — moving the clock back beyond a &lt;code&gt;ClockTamperToleranceHours&lt;/code&gt; window (default 24h) on a time-limited license yields a &lt;code&gt;ClockTampered&lt;/code&gt; status and refuses to validate until the time is corrected. Perpetual licenses, having no expiry, aren't subject to the check.&lt;/p&gt;

&lt;h3&gt;
  
  
  Revocation, offline
&lt;/h3&gt;

&lt;p&gt;Here is where offline-only shows its one real limitation, and how to close it. A license signed last year knows nothing about a refund or leak that happened last week — the signed payload is fixed at issue time. Online, revocation is a server flag picked up on the next activation refresh. Offline, you distribute a &lt;strong&gt;signed revocation list&lt;/strong&gt;: a list of revoked license ids, itself signed by your private key so the client can trust it, shipped with an app update. A purely offline client checks incoming licenses against that list and drops revoked ones. It's not instant — it moves at the speed of your releases — but it means a disconnected app isn't defenceless against a known-bad key. Keyright can build and sign a distributable revocation list you ship with &lt;code&gt;RevocationListJson&lt;/code&gt; / &lt;code&gt;RevocationListPath&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Offline-first, with an optional online grace window
&lt;/h2&gt;

&lt;p&gt;Pure offline validation is perfect for air-gapped installs and fast local gating, but it can't count seats — a client has no idea how many other machines run the same key — and it can't reflect a revocation newer than the last update. The common, honest answer is &lt;strong&gt;offline-first with an optional online check&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Keyright's model works like this: your app can validate a shipped offline license file with zero network, or it can &lt;strong&gt;activate online&lt;/strong&gt; once — exchanging the license key for a short-lived &lt;strong&gt;signed lease&lt;/strong&gt; bound to this machine. That lease is itself verified offline against your embedded public key and &lt;strong&gt;cached locally&lt;/strong&gt;, so after a single activation the app keeps validating offline for the life of the lease. Only when the lease nears expiry does it need to reach the server again, at which point seat counts and revocations are reconciled. If the server is briefly unreachable, a still-valid cached lease covers the gap — a &lt;strong&gt;grace window&lt;/strong&gt; — so an outage doesn't lock out an honest paying user. You get offline resilience &lt;em&gt;and&lt;/em&gt; server-enforced seats and revocation from the same system, instead of choosing one.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why "fail closed" is the whole game
&lt;/h2&gt;

&lt;p&gt;Every check above shares one design rule, and it is the rule that makes offline validation actually protective rather than decorative: on &lt;em&gt;any&lt;/em&gt; problem, resolve to the unlicensed state — not an exception, and never an unlocked one.&lt;/p&gt;

&lt;p&gt;Consider the alternative. If a license check failed &lt;em&gt;open&lt;/em&gt; — treating an error as "allow" — the easiest crack in the world would be to make the check error on purpose: delete the license file, corrupt a byte, block a call, and enjoy the software. Failing &lt;strong&gt;closed&lt;/strong&gt; inverts that: a bad signature, a wrong product, an expired or revoked key, a tampered clock, a missing or unreadable file — every one of them lands in the free/unlicensed edition. The safe outcome is the default outcome, so breaking the check gets the attacker nothing.&lt;/p&gt;

&lt;p&gt;Keyright's SDK is built around this. &lt;code&gt;Validate()&lt;/code&gt; never throws; on any failure it returns a &lt;code&gt;LicenseInfo&lt;/code&gt; in the &lt;code&gt;Free&lt;/code&gt; edition carrying a &lt;code&gt;Status&lt;/code&gt; (&lt;code&gt;NoLicense&lt;/code&gt;, &lt;code&gt;SignatureInvalid&lt;/code&gt;, &lt;code&gt;Expired&lt;/code&gt;, &lt;code&gt;MachineMismatch&lt;/code&gt;, &lt;code&gt;Revoked&lt;/code&gt;, &lt;code&gt;ClockTampered&lt;/code&gt;, …) and a human-readable message you can show in a "Register" dialog. Online &lt;code&gt;ActivateAsync()&lt;/code&gt; follows the same rule for its ordinary failure paths — bad key, seats exhausted, offline, revoked — returning a fail-closed result rather than throwing. Your &lt;code&gt;if (info.IsPaid)&lt;/code&gt; branch is the only thing that unlocks anything, and it only runs when everything genuinely checked out.&lt;/p&gt;

&lt;h2&gt;
  
  
  Being honest about the limits
&lt;/h2&gt;

&lt;p&gt;Offline validation proves authenticity and integrity beautifully, but it is not magic. Because the check ultimately runs on the user's machine, a determined attacker with a decompiler and a debugger can patch it out — the same truth that applies to any client-side protection. That's not a reason to skip it; it's a reason to be precise about its job. Offline validation is the right, strong tool for air-gapped and enterprise deployments and a fast, resilient local gate everywhere else. When you need to &lt;em&gt;enforce&lt;/em&gt; — count seats, kill a leaked key promptly — you pair it with online activation so those decisions live on a server the attacker doesn't control, and you make the client hard to patch with obfuscation. Offline validation and online activation aren't rivals; they're two halves of a licensing system that's both resilient and enforceable.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where Keyright fits
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://delta1labs.com/keyright" rel="noopener noreferrer"&gt;Keyright&lt;/a&gt; is offline-first by design: every tenant gets its own RSA signing key (private half stored encrypted server-side, never exposed), issues signed offline license files &lt;em&gt;and&lt;/em&gt; online activation leases, node-locks with tolerance, defends against clock tampering, ships signed revocation lists, and fails closed throughout its &lt;code&gt;.NET&lt;/code&gt;, Node, Python, and Java SDKs. If you want the full mechanics with real SDK code, read the &lt;a href="https://delta1labs.com/docs/keyright" rel="noopener noreferrer"&gt;Keyright documentation&lt;/a&gt; — the getting-started and .NET integration guides walk the exact &lt;code&gt;Validate()&lt;/code&gt; and &lt;code&gt;ActivateAsync()&lt;/code&gt; paths described here.&lt;/p&gt;

</description>
      <category>licensing</category>
      <category>dotnet</category>
      <category>security</category>
      <category>guide</category>
    </item>
  </channel>
</rss>
