Rotation
The allowlist revokes one tenant at a time. Sometimes you need the opposite: invalidate everything in a trust domain at once, because you are re-keying, responding to a compromise, or enforcing a periodic re-issue. Editing the allowlist tenant by tenant is the wrong tool for that. Epochs are the right one.
The operator token and the epoch counter
An operator can publish a self-signed operator token: a policy statement about its own trust domain. It carries an epoch counter (a monotonically advancing integer) and an optional validity window. When a verifier is configured with an operator token, it accepts only account and user tokens stamped with the operator’s current epoch.
opTok, _ := valiss.IssueOperator(operator,
valiss.WithName("prod-us"), valiss.WithEpoch(3))
acct, _ := valiss.IssueAccount(operator, tenantPub,
valiss.WithName("acme"), valiss.WithEpoch(3))
verifier := valiss.NewVerifier(operatorPub, allowlist,
valiss.WithOperatorToken(opTok))Every level in a chain must agree on the epoch. If the operator policy says epoch 3, an account or user token stamped epoch 2 is rejected. The check is cryptographic and needs no external state: the epoch is a signed claim, and the verifier simply compares.
Two honesty notes. On the request path the epoch is enforced only when the
verifier holds an operator token or keyring; a bare
NewVerifier(operatorPub, allowlist) pins the anchor and checks the allowlist
but never compares epochs, so unstamped or stale-epoch credentials pass until
you configure a policy. Message verification has no such gap: VerifyMessage
always requires the chain’s account, user, and message tokens to agree on their
epoch, and additionally binds them to the domain epoch when you supply an
operator policy.
Note
Request-path epoch checks apply only when the verifier holds an operator token
or a keyring. A bare NewVerifier(operatorPub, allowlist) never compares
epochs, so advancing the counter changes nothing there. Message verification
always checks epoch agreement.
Rotating a whole domain at once
To rotate the trust domain, bump the operator’s epoch and re-issue. Publish an operator token at epoch 4, re-issue the account (and user) tokens at epoch 4, and every token from an earlier epoch is rejected on its next use. No allowlist edits, no per-tenant work, no waiting for old tokens to expire. One counter advance retires the entire generation of credentials beneath it.
Driving this ceremony is issuer-side work. The valiss CLI (early development) is
designed to hold the operator identity in its per-operator store and roll it
forward with operator rotate, re-issuing beneath it rather than each token
by hand; that command is not yet runnable (a stub today). Until it
lands, the ceremony is the library issuance calls (IssueOperator at the new
epoch, then IssueAccount/IssueUser beneath it) or examples/minter.
The operator token’s own exp bounds the whole domain: once it lapses, nothing
in the domain verifies until a fresh operator token is published, which forces a
periodic rotation ceremony rather than letting a domain drift indefinitely on
one key state.
Two things do not change during rotation. The pinned operator public key stays the trust anchor; the operator token only carries policy signed by that key, it does not replace it. And selective revocation stays with the allowlist. The two levers are complementary: epochs for domain-wide rotation, the allowlist for cutting off individual tenants.
Grace periods with a keyring
A hard epoch flip assumes every producer can re-issue at the same instant. When they cannot, a verifier can trust a keyring instead of a single operator token. A keyring holds several full operator tokens, and one operator key may appear at more than one epoch at the same time.
k, _ := valiss.NewKeyring(prodUSEpoch3Token, prodUSEpoch4Token)
verifier := valiss.NewKeyringVerifier(k, allowlist)Registering both the outgoing and the incoming epoch side by side is the
receiver-side grace period: producers re-issue at their own pace, and both
epochs verify meanwhile. You bound the window by giving the transitional
old-epoch operator token a short exp, which closes the grace cryptographically
when it lapses. Entries are selected by the operator named in the credential
rather than by trial, so an unknown operator, or a known operator at an
unregistered epoch, fails immediately.
Both epochs verify while the outgoing operator token is unexpired; its exp
closes the grace:
flowchart TD
subgraph KR["Keyring during grace"]
E3["Operator token<br/>epoch 3, short exp"]
E4["Operator token<br/>epoch 4"]
end
C["Credential"] -->|"epoch 3, selected by operator + epoch"| E3
C -->|"epoch 4, selected by operator + epoch"| E4
E3 -.->|"short exp lapses"| CLOSED["Only epoch 4 verifies"]
The same keyring also lets one verifier trust several independent operators at
once; a handler reads the trust domain from id.Operator, and the keyring
guarantees a name maps to exactly one operator key.
Related
- Extensions: the typed, signed authorization claims the transports enforce down the chain.