Bindings
A binding is a piece of evidence the licensed binary must provide at verification time. Every binding stamped into the licence becomes a mandatory check — verify rejects the licence if a single binding is missing or wrong.
Why bindings
A bare licence is just subject + audience + validity signed by
the issuer. Once leaked, anyone with the PEM and a matching
binary can run it. Bindings tie the licence to evidence only the
intended runtime environment can produce — the leaked PEM no
longer suffices.
The three binding kinds
| Kind | What the binary collects at verify | Stamped at issue time |
|---|---|---|
machine | hostid.Composite() of the running host | One or more host-id strings (the licence is OR-bound over the list) |
password | A passphrase typed by the user | Argon2id hash of the password + parameters |
totp | A 6-digit RFC 6238 code generated by an authenticator | The base32 secret (issuer-side only; verify checks the commitment) |
custom:<name> | Arbitrary bytes the binary chooses to feed | A list of accepted byte strings |
A licence may carry any combination. The "all must satisfy" rule is non-negotiable — verify ANDs the bindings, never ORs.
Wire shape (issuer side)
type BindingSpec struct {
Type string // "machine" | "password" | "totp" | "custom:<name>"
Values []string // host ids / password / custom values; "totp" expects 0
Argon *licensekg.BindingParams // optional, only meaningful for "password"
Label string // for TOTP account label
}
LicenseService.Issue translates these into the
licensekg.Binding values the
PEM carries.
Wire shape (verifier side)
The standalone license.Verify
accepts options the binary builds from runtime evidence:
v, err := license.Verify(pem, trusted,
license.WithMachineID(hostid.Composite()),
license.WithPassword(typedByUser),
license.WithTOTPCode(authenticatorCode),
)
Missing options for a stamped binding → fail. Extra options for absent bindings → ignored.
Argon2id parameters
Password bindings carry the Argon2id parameters that produced the stamped hash. This lets the issuer re-tune the cost without breaking existing licences:
| Field | Stamped in | Used at verify |
|---|---|---|
ArgonTime | Binding payload | Re-derive the hash with the same time cost |
ArgonMemory | Binding payload | Same |
ArgonThreads | Binding payload | Same |
ArgonKeyLen | Binding payload | Same |
The Settings screen has three pre-baked profiles (fast / default / paranoid) — see Argon preset (coming). Future licences pick the operator's currently-selected preset; old ones keep verifying with whatever they were stamped with.
TOTP secret handoff
When the wizard adds a totp binding, LicenseService.Issue
returns the plaintext secret in IssuedLicense.TOTPs[i].Secret
exactly once. After that the secret lives only in the KEK-wrapped
column of TOTPSecret. The TUI surfaces it inline in the wizard's
post-issue overlay (QR + URI + 6-digit sanity check) so the
operator can hand it off to the licensee out of band — paper,
1Password share, encrypted channel.
Tested in
examples/license-manager/02-issue-with-bindings/— issues a licence with all three bindings, then runs Verify with full / partial / wrong evidence to prove the AND semantics.examples/license-manager/06-totp-secret/— standalone TOTP issuance + 6-digit code round-trip.