Entities
Every participant in valiss is an Ed25519 keypair. There are three delegation levels, plus an optional fourth level that exists only per message:
- Operator. The root of a trust domain. Its public key is the single value a server pins; everything else verifies against it. An operator signs account tokens.
- Account (tenant). One per customer. The operator signs each account an account token. An account signs user tokens, so a tenant can issue its own users without asking you.
- User. An end user or a service under an account. A user signs per-request proofs, and, if it needs to, per-message proofs.
- Message (optional). A user key can additionally issue short-lived, self-signed proofs of origin for individual artifacts it emits (webhooks, queue messages, exported documents). Message tokens are proofs, never credentials: possessing one grants nothing, and a request verifier never accepts one.
The delegation only ever flows downward, and never widens: an account can grant its users no more than it holds itself, and revoking an account cuts off everything beneath it. That property is what lets you hand a tenant one account credential and stop thinking about how they subdivide it.
The signing chain, one operation per hop:
flowchart TD
O["Operator (O…)"] -->|"signs account token"| A["Account (A…)"]
A -->|"signs user token"| U["User (U…)"]
U -->|"signs message token"| M["Message token"]
O -->|"self-signs operator token"| O
Keys are nkeys
Identities are encoded in the NATS nkey text format: uppercase base32 of the role prefix, the raw 32-byte public key, and a CRC-16 checksum. The important consequence is that the role is part of the key string. Public keys and seeds render with a recognizable leading letter:
| level | seed prefix | public prefix |
|---|---|---|
| operator | SO… | O… |
| account | SA… | A… |
| user | SU… | U… |
Because the role travels with the key, a verifier can check at every hop of the
chain that an operator key signed the account token, an account key signed the
user token, and so on, from the key material alone. The role prefix is not by
itself a barrier: the same 32-byte public key re-encodes under any role prefix
with a valid checksum, so a user key can be written as an account nkey. What
rules out cross-level confusion (a user key presented where an account key
belongs) is the verifier’s per-hop role check, which requires each iss and
sub to carry the role its position in the chain demands, backed by the
signature chain that must hold at every hop. The checksum also makes the
encoding copy-paste-safe for the
places keys actually live: config files, environment variables, and CLI flags.
A corrupted key fails to parse rather than failing verification in some
confusing way later.
The public key is the on-wire identity. A subject proves possession of the matching private key either per request (a request signature) or, for bearer user tokens, by presenting the token alone.
Names are labels, not identities
A token may carry a human-readable name (set with valiss.WithName), but the
name is issuer-asserted and is never checked for uniqueness at issuance. It is a
display label, not a key. When a token has no name, consumers show the subject’s
public key in its place.
acct, _ := valiss.IssueAccount(operator, tenantPub, valiss.WithName("acme"))Uniqueness, where you need it, is the job of whatever collection holds several
entities side by side. If you segment tenant data by account name, it is on you
to keep those names distinct within your operator. Across different operators,
names may collide freely: two independent producers can each have a tenant
called acme, and you distinguish them by operator, not by name alone.
Related
- Tokens: the signed artifacts that bind these keys into a delegation chain, each signed by the key one level up.