Skip to content

Licensing (issuer guide)

This is the internal guide for the person who mints Trimus licenses and holds the signing key. If you are an operator installing a license someone handed you, you want Licensing instead — this page is about minting tokens and key custody.

Licenses are offline, Ed25519-signed tokens (#136). You mint them with the trimus-license CLI, which holds the private signing key — the root of trust. The server binary embeds only the matching public key, so it can verify a token fully offline (air-gapped installs included) with no call back to you.

The private key is the root of trust

trimus-license and the key-*.priv files must never run on, or ship with, a customer instance. Only the key-*.pub files are committed and embedded. Losing the private key means you can no longer mint licenses; leaking it means anyone can mint one that your builds will trust.

What is actually enforced (v1 honesty)

Only expiry (expires) is enforced at runtime in v1. Everything else in the payload — tier, features, and limits — is carried in the signed body and seeded into instance_flags at boot (as license.tier, feature.<name>, and license.status) so that later feature- and limit-gating can turn on without re-issuing outstanding licenses. Setting -tier enterprise or -max-agents 200 today records intent; it does not currently restrict a running instance. Mint the values you actually mean, because they become authoritative the moment gating ships.

Payload schema

The signed body is internal/license/payload.go's Payload. Times are RFC3339 (Go's default time.Time JSON encoding). JSON field names are shown; the CLI flag that sets each is in the last column.

Field JSON Type Meaning Enforced in v1? Set by
LicenseID license_id string Opaque license identifier. Defaults to lic-<unix> when omitted. No -license-id
Customer customer string Customer name. Required at mint. No -customer
Issued issued RFC3339 time Issuance timestamp. Set to mint time (UTC). No (automatic)
Expires expires RFC3339 time Expiry instant. The one enforced field. Yes -days / -expires
Tier tier string License tier (see below). Seeded to license.tier. No (seeded) -tier
Features features []string Feature flags. Each seeded as feature.<name>=true. No (seeded) -features
Limits limits object Per-instance caps (see below). Carried, not applied. No -max-companies / -max-agents
Version version int Payload schema version. No -version (default 1)

Tiers

The tier constants defined in payload.go are exactly:

  • trial
  • standard (the CLI default)
  • enterprise

The CLI does not validate the string, so mint one of these three — anything else is seeded verbatim into license.tier and will not match future gating logic.

Limits sub-fields

Limits (JSON limits) has two integer sub-fields, both omitempty:

  • max_companies — cap on companies for the instance.
  • max_agents — cap on agents for the instance.

A 0 (the CLI default) is omitted from the encoded payload. These are recorded for later limit-gating; v1 does not apply them.

Minting & verifying

Build the tool (it is intentionally not part of the release artifacts):

go build -o trimus-license ./cmd/trimus-license

1. Generate a keypair (once per key id)

trimus-license keygen -kid 1 -out-dir .
  • -kid (default 1) — the integer key id; files are written as key-<kid>.pub and key-<kid>.priv.
  • -out-dir (default .) — where to write them.

Commit key-1.pub into internal/license/keys/; move key-1.priv into your secret manager. See Key custody & rotation.

2. Mint a token

trimus-license mint -key key-1.priv -kid 1 \
  -customer "Acme Corp" -tier standard -days 365 \
  -features batch,sso -max-companies 10 -max-agents 200
# prints the signed token to stdout

Mint flags:

Flag Default Notes
-key (required) Path to the Ed25519 private key PEM.
-kid 1 Key id — must match the embedded key-<kid>.pub; written into the token header.
-customer (required) Customer name.
-license-id generated lic-<unix> Explicit license id.
-tier standard trial | standard | enterprise.
-days 365 Days until expiry from now; ignored if -expires is set.
-expires (unset) Explicit RFC3339 expiry; overrides -days.
-features (none) Comma-separated flags (unenforced in v1).
-max-companies 0 Limit (unenforced in v1).
-max-agents 0 Limit (unenforced in v1).
-version 1 Payload schema version.

For a fixed calendar expiry rather than a rolling window:

trimus-license mint -key key-1.priv -kid 1 \
  -customer "Acme Corp" -expires 2027-01-01T00:00:00Z

Deliver the printed token to the operator, who installs it as TRIMUS_LICENSE (inline) or TRIMUS_LICENSE_FILE (a path). See Licensing.

3. Verify a token

trimus-license verify -license "<token>"      # or: -license @token.txt
  • -license (required) — the token, or @path to read it from a file.
  • -grace (default 7 days) — the post-expiry window still treated as operational.

verify checks the token against the public key(s) embedded in this build — the same set the server uses — and prints the decoded payload plus a status. It exits non-zero for any non-operational status. Statuses (from payload.go):

Status Meaning
valid Signature valid, not past expires.
grace Signature valid, past expires but within grace. Still operational.
expired Signature valid, past expires + grace.
tampered Malformed token or a signature that does not verify.
wrong_key The token's kid is not among the embedded public keys.
missing No token supplied.

A wrong_key result while verifying against a fresh build is the usual symptom of minting with a -kid whose .pub has not yet been committed and embedded.

Key custody & rotation

The whole scheme rests on one asymmetry: private keys never leave you, public keys ship in the binary.

  • keygen writes key-<kid>.pub (0644) and key-<kid>.priv (0600).
  • Only key-<kid>.pub is committed, into internal/license/keys/. Everything matching keys/*.pub is embedded at build time (internal/license/keys.go), keyed by the integer kid parsed from the filename — so multiple public keys co-exist in one binary.
  • Sign writes the kid into the token header (envelope.go).
  • Verify selects the embedded public key by that kid (verify.go); an unknown kid yields wrong_key and the token is rejected.

That per-kid selection is what makes rotation non-breaking: a new key can be added to a build alongside the old one, and every token keeps verifying under whichever kid signed it.

Rotation procedure

  1. Generate the next keypair: trimus-license keygen -kid 2 -out-dir .
  2. Commit key-2.pub into internal/license/keys/ alongside the existing key-1.pub. Move key-2.priv into your secret manager.
  3. Ship a release embedding BOTH key-1.pub and key-2.pub. Builds from this release trust tokens signed by either key.
  4. Mint new tokens with -kid 2. Existing -kid 1 licenses keep verifying, because the build still embeds key-1.pub.
  5. Retire key-1.pub only after every -kid 1 token has expired. Delete it from internal/license/keys/ and cut a release without it. Until then, removing it would strand still-valid -kid 1 licenses as wrong_key.

Because kid disambiguates keys at verify time, you never have to re-issue an outstanding license to rotate — you overlap the two keys for one license lifetime and drop the old one when nothing depends on it.

Security notes

Handling the signing key and minted tokens

  • Never commit or ship a key-*.priv. It lives only in your secret manager and is only ever read by the offline trimus-license CLI.
  • Never run trimus-license on a customer instance. Minting belongs to your controlled, offline environment.
  • Treat a minted token as exactly what it authorizes. It is a valid, signed license until it expires — there is no online revocation. To "revoke" early you must rotate the key it was signed under (which also invalidates every other token under that kid), so scope expiry windows accordingly.
  • This is honest-customer licensing, not DRM. A determined party with a binary they control can patch the check out; that is explicitly out of scope (#240). Nothing here promises unbreakable protection.