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

KindWhat the binary collects at verifyStamped at issue time
machinehostid.Composite() of the running hostOne or more host-id strings (the licence is OR-bound over the list)
passwordA passphrase typed by the userArgon2id hash of the password + parameters
totpA 6-digit RFC 6238 code generated by an authenticatorThe base32 secret (issuer-side only; verify checks the commitment)
custom:<name>Arbitrary bytes the binary chooses to feedA 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)

service.BindingSpec:

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:

FieldStamped inUsed at verify
ArgonTimeBinding payloadRe-derive the hash with the same time cost
ArgonMemoryBinding payloadSame
ArgonThreadsBinding payloadSame
ArgonKeyLenBinding payloadSame

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