Hardware binding
A CertifiEd license can be tied to a specific customer machine. The binding lives on the license itself — the hwBinding mode and the hwFingerprint value — and is checked on every activation. The fingerprint travels inside the signed token (hwfp), so it cannot be forged on the client.
Implementation: HwBindingMode (CertifiEd.Domain/Enums), HwBindingNames (CertifiEd.Application/Licensing); the check lives in ClientLicenseService.CheckBinding, issuance and rebinding in LicenseService.
The three modes
| Mode (wire) | Fingerprint at issuance | Who may activate |
|---|---|---|
none | forbidden | any machine; hwFingerprint is optional on activate |
fixed | mandatory | only the machine with that fingerprint |
firstActivation | forbidden | any machine, but the first activation pins the fingerprint; from then on it behaves like fixed |
The JSON names are exactly none, fixed, firstActivation (parsing is case-insensitive). An unknown value yields license.invalid_hw_binding (400). When hwBinding is omitted the mode is inferred from hwFingerprint for backwards compatibility: present → fixed, absent → none.
When to use which
- `none` — SaaS-like and floating licenses, containers and autoscaling, where the hardware changes on its own. Constrain the seat count instead:
limits.maxSeatsin theconfig. - `fixed` — a customer server you already know, because you collected its fingerprint during onboarding. The strictest option: a foreign machine never activates.
- `firstActivation` — the typical on-prem delivery: the fingerprint is unknown up front, but the license must settle on one machine. The customer installs the product and the first activation claims the node.
The fingerprint
The fingerprint is computed by the client. By default the SDK uses HwFingerprint.Get() — a SHA-256 over the machine name, the OS description, the first stable MAC address and the platform machine id. Your own value goes into CertifiEdClientOptions.HwFingerprint — see the Client SDK.
The server treats the fingerprint as an opaque string: it trims surrounding whitespace and compares byte for byte (StringComparison.Ordinal). Case matters.
1. Issuance
# fixed — the fingerprint is mandatorycurl -s -b cj.txt -X POST $BASE/api/v1/panel/licenses \ -H 'Content-Type: application/json' \ -d '{"companyId":"'"$COID"'","templateId":"'"$TPLID"'", "config":"{\"features\":[\"core\"],\"limits\":{\"maxSeats\":1}}", "hwBinding":"fixed","hwFingerprint":"fp-machine-A"}'
# firstActivation — the fingerprint is forbidden, it is captured on first usecurl -s -b cj.txt -X POST $BASE/api/v1/panel/licenses \ -H 'Content-Type: application/json' \ -d '{"companyId":"'"$COID"'","templateId":"'"$TPLID"'", "config":"{\"features\":[\"core\"]}","hwBinding":"firstActivation"}'Validation failures:
{ "code": "license.hw_fingerprint_required", "message": "A 'fixed' hardware binding requires hwFingerprint.", "status": 400 }
{ "code": "license.hw_fingerprint_not_allowed", "message": "A 'firstActivation' binding must not carry hwFingerprint — it is captured on the first activation.", "status": 400 }The same rules apply to bulk issuance at POST /api/v1/panel/licenses/bulk — hwBinding and hwFingerprint are set for the whole batch.
2. Activation
curl -s -X POST $BASE/api/v1/client/activate \ -H 'Content-Type: application/json' \ -d '{"licenseKey":"'"$KEY"'","hwFingerprint":"fp-machine-A","machineName":"erp-01"}'fixed, or an already-pinnedfirstActivation: the fingerprint is compared; a mismatch yieldsactivation.fingerprint_mismatch(400).- A not-yet-pinned
firstActivation:hwFingerprintis mandatory, otherwiseactivation.fingerprint_required(400). On success it is written tolicense.hwFingerprintand audited aslicense.hw_bound. none: nothing is checked.
3. Machine replacement: rebinding
The customer swapped the server. An operator or a company Admin performs the rebind:
curl -s -b cj.txt -X POST $BASE/api/v1/panel/licenses/$LICID/rebind \ -H 'Content-Type: application/json' \ -d '{"hwFingerprint":"fp-machine-B","reason":"motherboard replaced"}'- Rights (Admin on the license's company; operators always qualify) and status are checked.
hwFingerprintandhwBindingare updated.- Every active activation is deactivated — their seats are freed.
- The license is re-signed:
currentVersion + 1, a fresh signed version carrying the newhwfpin its payload. - A
license.reboundwebhook event is published and an audit entry of the same name is written — with the old and new fingerprint, both modes, thereasonand the number of activations dropped.
{ "id": "019f7ef0-b274-7b01-8417-8181c1d2facf", "licenseKey": "CFED-5WEE-KE7C-FP7A-C89M", "status": "Active", "currentVersion": 2, "isTrial": false, "hwBinding": "fixed", "hwFingerprint": "fp-machine-B"}4. Releasing the binding
curl -s -b cj.txt -X POST $BASE/api/v1/panel/licenses/$LICID/rebind \ -H 'Content-Type: application/json' \ -d '{"hwFingerprint":null,"reason":"released"}'
# fixed -> firstActivation (re-pins on the next activation)# none -> none (nothing to release)hwBinding in the request overrides that inference: {"hwFingerprint":null,"hwBinding":"none"}, for instance, drops hardware control entirely. The fixed → firstActivation transition is exactly what you want when handing a license out into the field: the customer pins it to the new server on first run.
Limitations
- A revoked (
license.rebind_revoked, 409) or expired (license.rebind_expired, 409) license cannot be rebound — reissue it instead. - The API will not create a
fixedlicense without a fingerprint: both issuance and rebinding rejecthwBinding: "fixed"with nohwFingerprint. A defensive branch on activation still exists and answersactivation.not_bound(409). - Binding does not cap the seat count — that is the separate
limits.maxSeatsmechanism inside the signedconfig.hwBinding: "none"plusmaxSeats: 5is a perfectly valid five-machine floating license. - The fingerprint is computed by the client, so this protects against casually copying a license, not against a determined reverse-engineer. The cryptographic guarantee comes from the token signature, not from the binding.
- A rebind bumps the license version;
currentVersionalso grows on transfers. There is no cap on the number of rebinds — every one of them shows up in the audit log.
Error diagnostics
| Code | HTTP | Where | Cause | What to do |
|---|---|---|---|---|
license.hw_fingerprint_required | 400 | issue / bulk / rebind | fixed without a fingerprint | Pass hwFingerprint or pick another mode |
license.hw_fingerprint_not_allowed | 400 | issue / bulk / rebind | A fingerprint with none or firstActivation | Drop hwFingerprint |
license.invalid_hw_binding | 400 | issue / bulk / rebind | Unknown mode | Only none, fixed, firstActivation |
activation.fingerprint_mismatch | 400 | activate | Wrong machine | Rebind the license and re-issue the .ced |
activation.fingerprint_required | 400 | activate | firstActivation, the client sent no fingerprint | Check that the SDK computes one and it is not overridden with an empty string |
activation.not_bound | 409 | activate | fixed with no fingerprint | Operator: run a rebind with a fingerprint |
activation.seat_limit | 409 | activate | limits.maxSeats exhausted | Free a seat: deactivate, an activation reset, or reissue with a larger limit |
license.rebind_revoked | 409 | rebind | The license is revoked | Reissue |
license.rebind_expired | 409 | rebind | The license has expired | Reissue |
Useful places to investigate:
# current mode, fingerprint and versioncurl -s -b cj.txt $BASE/api/v1/panel/licenses/$LICID
# which machines activated, and when they were releasedcurl -s -b cj.txt $BASE/api/v1/panel/licenses/$LICID/activations
# full history: license.rebound / license.hw_bound / activation.rejectedcurl -s -b cj.txt "$BASE/api/v1/panel/audit?entityId=$LICID"See also
- API reference — the full endpoint reference,
rebindandbulkincluded. - Client SDK — binding from the integrator's point of view.
- License protocol — where
hwfplives inside the token and the marker. - Webhooks — the
license.reboundevent.