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:
trialstandard(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):
1. Generate a keypair (once per key id)¶
-kid(default1) — the integer key id; files are written askey-<kid>.pubandkey-<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:
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¶
-license(required) — the token, or@pathto 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.
keygenwriteskey-<kid>.pub(0644) andkey-<kid>.priv(0600).- Only
key-<kid>.pubis committed, intointernal/license/keys/. Everything matchingkeys/*.pubis 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. Signwrites thekidinto the token header (envelope.go).Verifyselects the embedded public key by thatkid(verify.go); an unknownkidyieldswrong_keyand 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¶
- Generate the next keypair:
trimus-license keygen -kid 2 -out-dir . - Commit
key-2.pubintointernal/license/keys/alongside the existingkey-1.pub. Movekey-2.privinto your secret manager. - Ship a release embedding BOTH
key-1.pubandkey-2.pub. Builds from this release trust tokens signed by either key. - Mint new tokens with
-kid 2. Existing-kid 1licenses keep verifying, because the build still embedskey-1.pub. - Retire
key-1.pubonly after every-kid 1token has expired. Delete it frominternal/license/keys/and cut a release without it. Until then, removing it would strand still-valid-kid 1licenses aswrong_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 offlinetrimus-licenseCLI. - Never run
trimus-licenseon 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.