Skip to content
← All posts
· Delta1 Labs LicensingSecurityGuide

Designing a machine fingerprint that is stable but not brittle

Node-locking binds a license to a device, which means you need a fingerprint that is unique enough to identify a machine yet stable enough to survive a RAM upgrade. Here is how to build one from weighted hardware signals, match it with tolerance, hash it for privacy, and handle the VM and hardware-change cases that trip up naive implementations.

Node-locking — binding a license to a particular device — is only as good as the fingerprint it rests on. Get the fingerprint wrong in one direction and it is too loose: it collides across machines, and one key quietly runs everywhere. Get it wrong in the other direction and it is too brittle: it changes when the user adds RAM or swaps a network card, and a paying customer is suddenly locked out of software they legitimately own. A good machine fingerprint threads that needle — unique enough to identify a device, stable enough to survive the ordinary life of that device. Here is how to build one.

The tension: uniqueness vs. stability

Every hardware signal you could fingerprint sits somewhere on a spectrum between stable and unique, and the two pull against each other:

  • Stable, less unique: the OS install identifier (a GUID written once when the OS is installed), the machine name. These rarely change, but can repeat across cloned images.
  • Unique, less stable: a MAC address, a disk serial. Very distinguishing, but a MAC changes with a new adapter or a docking station, and disks get replaced.
  • In between: motherboard/BIOS identifiers and CPU details — fairly unique, fairly stable, but absent or shared on some virtual machines.

No single signal is both perfectly stable and perfectly unique, which is exactly why hashing one value — or worse, concatenating several into one hash — produces a brittle fingerprint. The fix is not to pick the “best” signal; it is to combine several and match them with tolerance.

Collect several signals, each weighted

Gather a handful of independent signals and assign each a weight reflecting how much you trust it to be stable. A sketch in C#:

record Signal(string Name, string Value, int Weight);

IEnumerable<Signal> CollectSignals() => new[]
{
    new Signal("os-install-id", ReadMachineGuid(),   40),  // very stable
    new Signal("board-id",      ReadBaseboardId(),   30),  // stable
    new Signal("cpu-id",        ReadCpuSignature(),  20),  // stable-ish
    new Signal("primary-mac",   ReadPrimaryMac(),    10),  // volatile
};

The weights matter more than the exact signals. The OS install GUID and motherboard carry most of the identity; the MAC contributes a little but is not allowed to decide the outcome on its own. Crucially, you keep the signals separate rather than folding them into one hash, because matching has to be able to see which signals changed.

Match with tolerance, not equality

The naive check — “does the stored fingerprint equal the current one?” — is the brittleness bug. Instead, compare signal-by-signal and sum the weight of the signals that still match. The device is “the same” if the matched weight clears a threshold:

int MatchScore(IReadOnlyList<Signal> stored, IReadOnlyList<Signal> current)
{
    int score = 0;
    foreach (var s in stored)
    {
        var c = current.FirstOrDefault(x => x.Name == s.Name);
        if (c is not null && FixedTimeEquals(c.Value, s.Value))
            score += s.Weight;
    }
    return score;   // 0..100
}

bool IsSameDevice(IReadOnlyList<Signal> stored, IReadOnlyList<Signal> current)
    => MatchScore(stored, current) >= 70;   // tolerate up to 30 points of drift

With the weights above, a RAM upgrade changes nothing in this set; a new network adapter drops the primary-mac (10 points) but still scores 90; even a CPU and MAC change still clears 70 as long as the OS install ID and board match. Meanwhile a genuinely different machine shares none of the high-weight anchors and scores far below the threshold. One number — the threshold — becomes the knob you tune between “too loose” and “too brittle.”

signals (weighted), total = 100os-install-id · 40board-id · 30cpu-id · 20mac·10threshold = 70matched after a new network adapter:score = 90 → accepted (MAC dropped, anchors still match)

Hash on the device, send only the opaque result

Fingerprinting touches hardware identifiers, so design it to minimize what leaves the machine. Compute the signals locally, and send the server an opaque per-signal hash — never the raw serial numbers. The server stores values it cannot reverse into “this customer owns this exact motherboard,” which gives you device binding without assembling a dossier of identifiable hardware.

// Salt with the product/key so the same hardware yields different
// fingerprints across products — no cross-product correlation.
string Hash(string raw, string salt) =>
    Convert.ToHexString(SHA256.HashData(Encoding.UTF8.GetBytes(salt + ":" + raw)));

Salting per product (or per license) means the same device produces an unrelated fingerprint for each product, so two vendors can never correlate “the same machine” from their stored fingerprints. Send the hashed signals and their weights; the match logic above works identically on hashes. This is the privacy-preserving default, and it is strictly better for you too — there is simply less sensitive data to hold.

The cases that trip up naive implementations

Two realities break fingerprints that only ever got tested on one developer’s laptop:

Virtual machines and containers. Cloned VM images can share an OS install GUID and board identifiers, making many “different” machines look identical — or, with per-clone randomization, making the same logical server look new on every boot. For virtualized or ephemeral environments, hardware fingerprinting is often the wrong tool entirely; a floating or concurrent model that counts simultaneous use fits better. (See floating licenses and node-locking and seat management for when to choose which.)

Legitimate hardware change. Eventually a motherboard dies, a laptop is replaced, a disk is swapped. Tolerance buys you time, but a full rebuild will cross the threshold — and it should, because that genuinely is a new device. The answer is a graceful re-activation path: when the score falls below threshold, don’t silently fail; treat it as a new activation against the key’s device allowance, and let the customer move their seat. The server already tracks activations per key for seat limits, so a drifted fingerprint is just a request to bind a new device, subject to the same seat budget — not a lockout.

The shape of a good fingerprint

Put together, a robust machine fingerprint is: several independent signals, each weighted by how stable it is; a match that sums matched weight against a tunable threshold rather than demanding exact equality; per-signal hashing on the device with a product-specific salt so only opaque values leave the machine; and an explicit re-activation path for the day the hardware really does change. Build it that way and node-locking does its job — one key, one device — without turning a RAM upgrade into a support ticket. Build it as a single hash of a MAC address, and you will spend more time apologizing to paying customers than stopping the few who share keys.

Try Nebula.NET

Harden your .NET code in minutes — start with the free edition.