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 |:
| Order | Source | How it is read |
|---|---|---|
| 1 | Windows MachineGuid | HKLM\SOFTWARE\Microsoft\Cryptography\MachineGuid via winreg with KEY_WOW64_64KEY |
| 2 | System UUID | wmic csproduct get uuid |
| 3 | CPU ID | wmic cpu get processorid |
| 4 | Disk serial | wmic diskdrive get serialnumber |
| Fallback | Hostname | obsidian-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:
| Order | Source | How it is read |
|---|---|---|
| 1 | Windows MachineGuid | RegistryHive.LocalMachine / RegistryView.Registry64, key SOFTWARE\Microsoft\Cryptography |
| 2 | System UUID | WMI SELECT UUID FROM Win32_ComputerSystemProduct |
| 3 | CPU ID | WMI SELECT ProcessorId FROM Win32_Processor |
| 4 | Disk serial | WMI SELECT SerialNumber FROM Win32_DiskDrive |
| 5 | Motherboard serial | WMI SELECT SerialNumber FROM Win32_BaseBoard |
| 6 | Machine name | Environment.MachineName |
| 7 | OS description | RuntimeInformation.OSDescription |
| 8 | Architecture | RuntimeInformation.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.
| Order | Source | Bitmask | Notes |
|---|---|---|---|
| 1 | Windows MachineGuid | 0x01 | Same registry key the C# and Python SDKs read. |
| 2 | SMBIOS system UUID | 0x02 | Read from firmware directly, not through WMI. |
| 3 | Disk 0 serial | 0x04 | First physical disk. |
| 4 | First physical NIC MAC | 0x08 | Filtered to IF_TYPE_ETHERNET_CSMACD or IF_TYPE_IEEE80211. Virtual, VPN, and docking-station adapters are skipped. |
| 5 | CPUID | 0x10 | Raw 16-byte block from the cpuid instruction. |
| Fallback | Literal obs-no-hwid-source-available | 0x00 | Only 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.
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.
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.
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.
| Scenario | Which sources change |
|---|---|
| Fresh OS install on the same hardware | MachineGuid is regenerated; UUID, CPU ID, and disk serial are preserved. |
| Booting into a second Windows install on the same box | Different MachineGuid. Everything else identical, but the join still produces a new SHA-256. |
| Motherboard swap | System UUID and Win32_BaseBoard serial change (C# rows 2 and 5; Python row 2). |
| CPU swap | ProcessorId changes. |
| Disk replacement | Win32_DiskDrive serial changes. |
| VM cloning | MachineGuid 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 VMs | Depends 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 deployment | Every source is intentionally regenerated. Expect a full reset. |
| Wine, WSL, or CrossOver | None 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
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.