Skip to main content

HWID Lock

HWID is a client-side machine fingerprint that pins a credential to one device, so a leaked license or password cannot roam across installs.

Format

The HWID is a 32-character lowercase hex string. It is the first 32 hex characters of SHA-256(id_part_1 | id_part_2 | ...), joined with a literal | before hashing.

Both the Python and C# SDKs emit that same shape. They collect different source lists though, so the same physical machine produces a different HWID under Python than it does under C#.

The value is computed once when the SDK object is constructed and cached on the instance for the lifetime of the process. Nothing writes it to disk.

Python SDK sources

Read in order, joined with |:

OrderSourceHow it is read
1Windows MachineGuidHKLM\SOFTWARE\Microsoft\Cryptography\MachineGuid via winreg with KEY_WOW64_64KEY
2System UUIDwmic csproduct get uuid
3CPU IDwmic cpu get processorid
4Disk serialwmic diskdrive get serialnumber
FallbackHostnameobsidian-nohw-<platform.node().lower()> when every source above returns empty; if platform.node() itself raises, the fallback becomes obsidian-nohw-unknown

Each source that returns nothing is skipped rather than appended as an empty string. See Python SDK for the enclosing class.

C# SDK sources

Read in order, empty values filtered out before the join:

OrderSourceHow it is read
1Windows MachineGuidRegistryHive.LocalMachine / RegistryView.Registry64, key SOFTWARE\Microsoft\Cryptography
2System UUIDWMI SELECT UUID FROM Win32_ComputerSystemProduct
3CPU IDWMI SELECT ProcessorId FROM Win32_Processor
4Disk serialWMI SELECT SerialNumber FROM Win32_DiskDrive
5Motherboard serialWMI SELECT SerialNumber FROM Win32_BaseBoard
6Machine nameEnvironment.MachineName
7OS descriptionRuntimeInformation.OSDescription
8ArchitectureRuntimeInformation.ProcessArchitecture

The C# SDK depends on System.Management, which is Windows-only. See C# SDK.

C++ SDK sources

The C++ SDK ships an obs_compute_hwid(char* out, size_t out_len, uint32_t* out_sources_mask) helper that produces a 32-character lowercase hex fingerprint the same shape as the other SDKs. Each source that returned something contributes both to the concatenation that gets hashed and to the returned out_sources_mask bitmask.

OrderSourceBitmaskNotes
1Windows MachineGuid0x01Same registry key the C# and Python SDKs read.
2SMBIOS system UUID0x02Read from firmware directly, not through WMI.
3Disk 0 serial0x04First physical disk.
4First physical NIC MAC0x08Filtered to IF_TYPE_ETHERNET_CSMACD or IF_TYPE_IEEE80211. Virtual, VPN, and docking-station adapters are skipped.
5CPUID0x10Raw 16-byte block from the cpuid instruction.
FallbackLiteral obs-no-hwid-source-available0x00Only when every probe above fails.

The helper is optional. Every auth entry point (obsidian_login_v2, obsidian_register_v2, obsidian_license_v2) takes an explicit const char* hwid parameter, so a caller that wants to derive its own fingerprint (a TPM-backed value, a hardware dongle serial, a hash of a licensed dataset) can pass whatever string it likes.

info

The Python and C# SDKs deliberately skip MAC addresses because docking stations, VPN clients, and virtual switches move NICs around too aggressively for them to be stable. The C++ SDK includes the first physical Ethernet or 802.11 MAC because native code can filter to IF_TYPE_ETHERNET_CSMACD / IF_TYPE_IEEE80211 and drop virtual adapters cleanly. If your product runs on hardware that hot-swaps physical NICs, mask bit 0x08 off in the caller or pass your own hwid to the auth calls.

See C++ SDK for the full signatures.

Enabling the lock

HWID enforcement is a per-application toggle, not per-user. Flip it on the target app's Security tab in the dashboard; see Applications for where the switch lives.

When enabled, the setting is echoed back in every POST /api/client/init response as app.hwid_lock, so the client can read the current mode if it wants to display it. The full contract is documented in Client Endpoints.

Enforcement on /api/client/login

The server only enforces the check when hwid_lock is on for the app.

If the end user record has no hwid stored yet, the login binds the submitted value and continues. If a stored hwid exists and does not match, the server records a client.hwid_mismatch audit event at severity danger, counts a failed login toward the lockout counter, and returns 401 Invalid username or password.

tip

The error string is deliberately identical to a wrong-password response. An attacker probing usernames cannot tell whether they hit a bad password or a bad HWID.

Enforcement on /api/client/license

If the license has a bound hwid and it does not match the submitted value, the server records client.hwid_mismatch at danger and returns 403 HWID mismatch. Key locked to another device.

If the license has never been used, the current HWID is bound during activation regardless of the toggle. The value is stored inside the license row itself.

Registration is a special case: it always binds the submitted HWID to both the new end user and the consumed license, even when hwid_lock is off. Turning the lock on later still catches drift because the value is already recorded.

Resetting a bind

Reset means clearing the stored HWID so the next successful auth binds a fresh one.

For a username-based account, open the app's Users tab, find the row, and use the reset HWID action. The next login from that user records whatever HWID is presented.

For a license-only key, open Manage Keys, find the key, and reset its HWID field. The next /api/client/license call locks the key to whatever device shows up.

tip

Reset is one of the most common support requests once you have real users. If you sell high volume through resellers, put it behind a self-service reseller action; see Team & Resellers.

Legitimate reasons a match will fail

The following scenarios all change one or more source identifiers, and the server has no way to distinguish them from an actual key-share attempt.

ScenarioWhich sources change
Fresh OS install on the same hardwareMachineGuid is regenerated; UUID, CPU ID, and disk serial are preserved.
Booting into a second Windows install on the same boxDifferent MachineGuid. Everything else identical, but the join still produces a new SHA-256.
Motherboard swapSystem UUID and Win32_BaseBoard serial change (C# rows 2 and 5; Python row 2).
CPU swapProcessorId changes.
Disk replacementWin32_DiskDrive serial changes.
VM cloningMachineGuid and system UUID are usually cloned with the disk, but hypervisors that regenerate machine identifiers on first boot will produce a new HWID.
Linked-clone VMsDepends on the hypervisor. Vagrant, Multipass, and most --linked VMware clones inherit the parent's UUID and pass. Hyper-V's default "new differencing disk" flow can regenerate.
Sysprep or image deploymentEvery source is intentionally regenerated. Expect a full reset.
Wine, WSL, or CrossOverNone of the WMI queries return, so the Python SDK falls through to obsidian-nohw-<hostname>. Non-Windows users will collide on the same shape.

If any of these are normal for your customers, either leave hwid_lock off for that application or switch to the license-only pattern where the key itself is the credential.

What the lock does not do

warning

HWID is a user-mode fingerprint. It is a friction layer, not a root-of-trust.

All identifiers come from user-mode APIs that a determined user can spoof with tools like AMIDEWIN, SMBIOS editors, or WMI hooks. Nothing here reaches into the kernel or the TPM.

It does not stop VM abuse on its own. Pair it with the heartbeat protocol and the anti-abuse counters described in Security Model for real coverage.

It does not identify a person. Two accounts on the same machine will always share an HWID; that is a property of the design, not a bug.