Pre-Issued Grants¶
Beta routes (v1.6.0); the signed records are stable. Off by default
(EVIDENCE_GRANTS=on).
A grant is an authorisation the Network Authority (NA) signs in advance, so a device or controller can make a change while it cannot reach the NA. Where break-glass (Emergency Changes and Break-Glass) records a change made without a decision and judges it afterwards, a grant authorises the change beforehand: the NA checks, when it issues the grant, that every policy that applies allows every request the grant permits. This page covers when to use grants, how they are issued, used, uploaded and revoked, and what offline trust cannot promise. The worked example is Example: Pre-Issued Grants; the routes are in Trust API — HTTP Reference (Pre-issued grants).
Grants are off until EVIDENCE_GRANTS=on. That needs
EVIDENCE_OUT_OF_BAND=on, which needs EVIDENCE_STORE=on; all three are off
by default. Holding grants and time tokens, acting within a grant and
recording its uses live in the TypeScript and Rust SDKs and in the edge agent
(The Edge Agent). The Go and .NET SDKs only check that a grant they are
shown carries the NA’s signature.
Turn it on once every verifier that reads your exports, packs and revocation
feed runs 1.6: a 1.5 verifier refuses the new entry kinds and the feed’s
revoked_grant_ids. Turning it off again is covered in
Turning grants off.
Grants or break-glass¶
Situation |
Use |
Why |
|---|---|---|
A known, bounded kind of change a device must be able to make while it is disconnected (rotate its own credential, scale within limits) |
a grant |
the change is authorised before it is made, within an envelope the policies were checked against; it is governed ( |
An urgent change nobody planned for, while the NA does not answer |
break-glass |
nothing authorised it; it is recorded with a justification and judged afterwards |
A grant has ended, is spent or revoked, and the NA does not answer |
break-glass |
the TypeScript or Rust SDK, or the edge agent, falls back to it: an urgent change is recorded and judged, never blocked |
The NA answers |
a decision |
ask for one; a grant is for when the NA cannot be reached |
A grant never overrides a DENY: an envelope a policy could deny is not granted. Keep grants for changes you would allow anyway, made where the network is unreliable.
Issuing a grant¶
POST /admin/grants (privileged) names:
the bearer: one registered executor key (
bearer_key_id). Only its holder can use the grant; one key per grant means its budget cannot be spent on several devices. A key with a resource prefix may use the grant only for resources inside it;the capabilities (exact IDs) and the basis: an attestation this NA issued (
attestation_id) or an agreement it trusts (agreement);the validity (
valid_from, by default now, andvalid_until, RFC 3339): at most 24 hours, and at mostNA_GRANT_MAX_VALIDITY_SECONDS;the budget (
max_uses, required, at mostNA_GRANT_MAX_USES) and the uses allowed per time token (max_uses_per_time_token, at mostNA_GRANT_MAX_USES_PER_TIME_TOKEN);the parameter envelope: every request parameter a use may carry, each with
one_of(a finite set of scalars),subset_of(a list of strings drawn from a set) ornumber_range(integerornumber, with inclusive or exclusive bounds), and whether it isrequired. The envelope is closed: a use with any other parameter is refused. Every use carries these names and values, so an envelope that looks like secret material (a parameter named like a secret, a PEM block, a key or token) is refused (invalid_grant_envelope).
The NA then checks the basis (its status, a validity that covers the whole
grant, each capability, freshness) and every active policy, and signs the
grant, or refuses it naming the first problem and listing them all
(error.details.problems). Inside the write that stores the grant it checks
again that nothing the grant rests on changed since (the active policy
versions, the bearer key, the attestation, the agreement’s trust). If
something did, it checks the grant once more; if something changes again,
it refuses with 409 grant_issuance_conflict, and the operator retries.
How policies are decided over an envelope¶
The NA decides each gate of each policy that applies for every request the envelope permits, at every time of the grant’s validity:
Gate type |
Decided when |
|---|---|
|
the parameter is required and every value it may take is a number on the right side of the bound. Booleans are never numbers; exclusive bounds count; numbers are compared exactly (an integer range up to 2^53 + 1 exceeds a bound of 2^53) |
|
every value (a set, or the integers of a range) is, or is not, listed, compared type-strictly: |
|
the parameter’s set ( |
|
the parameter is required and never null |
|
the parameter is pinned ( |
|
the grant’s whole validity lies inside the window |
|
every value is in the claim of the grant’s attestation, and none is null (the gate fails on a null value) |
A gate on a fact the grant fixes (the capability, the parties, the basis, the
attestation’s facts, and parent_kind, which is grant) is evaluated on
that one value. A selector’s parameter_equals narrows the envelope to the
requests it selects, and a policy selecting parent_kinds applies to a grant
when it names grant or the kind of the grant’s basis (attestation, or
agreement and direct). A policy can forbid grants with a denylist.v1
gate on parent_kind listing grant.
Why the NA refuses¶
Problems are reported in this order, for each capability and each policy in turn:
Reason |
Meaning |
|---|---|
|
the attestation is missing, not signed by this NA, revoked, or not the requester’s |
|
the grant’s validity is not within the attestation’s or agreement’s |
|
the basis does not grant a capability |
|
the agreement’s freshness commitment is not met |
|
the active policy set cannot be trusted (an unsigned or invalid policy, two active versions of one policy, a stored policy that fails its digest): no grant is issued |
|
a policy that applies requires every change to be reviewed (or is an emergency policy); a grant cannot pre-authorise such a change |
|
a policy that would apply starts inside the grant’s validity |
|
a policy that applies ends before the grant |
|
a gate or selector reads |
|
a gate or selector reads |
|
a gate other than |
|
a gate type other than the nine above (an operator’s own), whose behaviour over a range is unknown |
|
an enforce-mode gate may deny a request inside the envelope |
Observe-mode gates never refuse a grant: how each was decided (pass,
may_fail, undecidable) is recorded in the grant’s policy_binding, with
the policy versions it was checked against. Other refusals:
The request (
400):invalid_grant_request,invalid_grant_validity,invalid_grant_envelope,grant_limit_exceeded,ambiguous_basis,unexpected_field,invalid_agreement.The bearer key (
422):grant_bearer_key_unknown,grant_bearer_key_not_executor,grant_bearer_key_retired.A change during issuance (
409):grant_issuance_conflict.
The issuance check runs only at the NA: SDKs neither run it nor port the
policy engine, and the issuance vectors of the grants conformance suite
are for the NA alone.
Using a grant¶
The holder keeps the grant and the time token the NA returned with it.
Before each change it checks, offline: the capability, the envelope, the budget (the uses it made), the validity, the uses its time token still allows, its key’s resource prefix, and the revocation feed it last read.
It records the use: a
GrantExecutionEvidencesigned by the bearer key, with a UUID as itsuse_id, the grant’s digest, its place in the grant’s use chain (use_sequence,prev_use_digest), the latest time token, the request parameters, the resource and what was done (metadata only, as for execution evidence). It checks the metadata for secret material before signing: the NA never stores a use that carries any, so a holder never chains a use on one. The TypeScript and Rust SDKs, and the edge agent, keep the use in their outbox.When it can reach the NA it asks for a fresh time token (
POST /grants/time-tokens, a request its bearer key signs naming the head of its use chain) and uploads its uses in order (POST /evidence/grant-executions).
The NA admits each use in its chain’s order and gives it the next position of
its resource’s chain: positions are assigned under the lock every write to
the store takes, so two devices that changed the same resource while
disconnected never fork its chain. Execution evidence made afterwards links
to the last use (GET /admin/evidence/resource-heads/<resource_id> names it,
with its entry_kind and record_id).
A refused use keeps its place¶
A use the NA refuses for good is kept as a quarantine entry, and when it
continued the holder’s chain (it is at the next position and links to the
chain’s head) it stays in the chain: the holder’s next use links to it and is
judged on its own. Concretely:
the chain is the admitted uses and the quarantined uses that continued it; a time token may name a quarantined use as the head;
the budget counts admitted uses only;
the order and time rules compare a use with the last admitted use;
a use refused without being stored (
grant_use_secret_material,grant_use_chain_gap,grant_use_in_future) takes no position: upload the earlier uses first, or retry a future-dated use once the NA’s clock reaches it;a use refused as a conflict or a chain mismatch is kept but takes no position, since it does not continue the chain;
uploading a refused use again gives the same refusal (and the same
quarantine_id), and uploading an admitted use again is a duplicate (200,"status": "duplicate").
Time¶
A disconnected holder controls its own clock, so the NA bounds what it can claim:
each use carries the latest time token the NA signed for the holder, bound to the head of its use chain when it asked. A use is never dated more than 300 seconds before its token, and at most
max_uses_per_time_tokenuses follow the use the token names. This rule needs only the use and its token, so the NA, the export verifiers (genesis-mesh evidence verify-export,genesis-mesh-verify) and the TypeScript and Rust SDKs apply it alike, and several tokens taken at one head allow no more uses than one. The Go and .NET SDKs do not apply it: they only check a grant’s NA signature;a token is never older than its grant;
a use is never dated before the previous admitted use, nor with an older token than it;
a use is dated within the grant’s validity, and no more than 300 seconds after the NA records it;
a use is uploaded no later than the grant’s validity after it was made.
What the NA refuses¶
In the order it checks. “Kept” means the authentic use is stored as a
quarantine entry (named in error.details.quarantine_id), since the change
it describes already happened; “in the chain” means a kept use takes its
position in the holder’s chain.
The record itself:
Reason |
Kept |
Meaning |
|---|---|---|
|
no |
not a |
|
no |
the NA holds no such grant |
|
no |
not signed by the grant’s bearer key |
|
no (never stored) |
its parameters or metadata look like a secret |
The chain:
Reason |
Kept |
Meaning |
|---|---|---|
|
yes, not in the chain |
its position is taken, or its |
|
no |
an earlier use is not uploaded yet: upload it first |
|
yes, not in the chain |
does not link to the chain’s head |
The use against its grant (kept, in the chain):
Reason |
Meaning |
|---|---|
|
the bearer’s key, but another executor sovereign |
|
the bearer key is retired |
|
the bearer key has a resource prefix and the use names no resource or one outside it |
|
another grant digest than the stored grant’s |
|
a capability the grant does not name |
|
a parameter outside the envelope, or a required one missing |
|
the grant’s admitted uses already reach its budget |
|
the time token is not the NA’s for this grant, or is older than the grant |
|
the time token was issued after a later use, or names another use than the store holds |
|
an older token than the previous admitted use’s |
|
more than |
|
dated more than 300 seconds before its time token |
|
dated before the previous admitted use |
|
dated outside the grant’s validity |
|
dated more than 300 seconds after the NA records it (not kept: retried) |
|
uploaded later than the grant’s validity after it was made |
|
dated at or after the grant’s revocation |
Expiry is not a block¶
When a grant has ended, its budget or time token is spent, or it is revoked,
and the NA cannot be reached (a network error, a timeout, 5xx, 429), the
SDK falls back to break-glass (Emergency Changes and Break-Glass): the change runs,
its BreakGlassRecord is kept in the outbox, and the NA judges it after the
fact when it is reachable again. An urgent change is recorded and judged,
never blocked. A DENY is never broken through: an NA that answers decides.
The edge agent is stricter. It breaks the glass only when the NA cannot be
asked at all: no connection, a timeout, or a gateway’s own error page. A
429 or other 5xx from the NA allows only a covering grant
(The Edge Agent, How the agent decides).
Revocation¶
A grant is revoked from a time; a use made before it stands, one made at or
after it is refused (grant_revoked, kept). The NA revokes a grant:
Reason |
When |
|---|---|
|
|
|
its basis attestation is revoked |
|
its basis agreement is no longer signed by parties this NA trusts: a recognition treaty that vouched for a party was revoked, or has expired |
|
a policy version it is bound to is deactivated, or replaced by another version |
|
a policy is activated that may deny within its envelope: the NA checks every usable grant the policy may cover again, over the rest of its validity, and revokes those it may deny |
|
its bearer key is retired |
Each revocation is a grant_revocation entry in the store. A revocation that
follows another change runs after that change commits. The NA checks every
usable grant again (its key, its policy versions, its basis, the active
policies) at start, and at most once a minute while it serves the grant
routes and the revocation feed, so one an interrupted request or a failed
write missed is made then. One grant failing its check never stops the
others.
Holders learn of revocations from the revocation feed
(GET /sovereign-revocation-feed): with grants on, it lists in
revoked_grant_ids the revoked grants not yet past their end. Its sequence
counts every grant revocation as well as every revoked attestation, with
grants on or off, so it never goes back in normal operation. A database
restore to an older backup does take it back; see Database Restore in
Incident Response Runbooks. A holder refuses a revoked grant once
its copy of the feed is fresh.
A compromised device: retire its executor key. Every use the key signs is then refused, whenever it claims to have been made, and every grant it bears is revoked. Revoking the grant alone still admits uses dated before the revocation.
Turning grants off¶
With EVIDENCE_GRANTS=off the NA issues no grant, admits no use, issues no
time token and leaves revoked_grant_ids out of the revocation feed, so a
holder could neither upload its uses nor learn that a grant was revoked. The
NA therefore refuses to start with grants off (or with
EVIDENCE_OUT_OF_BAND=off) while any grant it issued has not ended; the
error names how many and when the last one ends. To turn grants off:
stop issuing grants and let the last ones end (their
valid_until), or revoke them and wait for their end;turn
EVIDENCE_GRANTSoff.
Uses made under an ended grant and not uploaded by then are refused while
grants are off; turn grants back on to admit them, within each grant’s
late-upload window. The feed’s sequence keeps counting the grant
revocations already made.
Limits of offline trust¶
A grant moves the decision before the change; it cannot make a disconnected device honest. What remains:
Backdating within the time rules. After a revocation it has not seen, a holder can make at most
max_uses_per_time_tokenuses beyond the use its last time token names, each dated before the revocation, whatever tokens it kept. Each is still bounded by its token’s time, the previous admitted use, the grant’s validity and the late-upload window. Keep budgets and per-token limits small, and validities short.A stolen key spends the remaining budget until its key is retired. One bearer key per grant and a small budget keep that bounded.
Revocation reaches a holder only when it reads the feed. Between a revocation and the holder’s next fresh feed, a holder that has not seen it may act. Retiring the bearer key refuses those uses when they are uploaded; revoking the grant refuses only those dated after it.
Policies change after issuance. A grant was checked against the policies active when it was issued; a stricter policy revokes it once activated, but a holder offline until then acts on the grant.
Every use is still recorded, signed and chained, quarantined when refused, and visible in the evidence store and in audit packs.
Monitoring and records¶
GET /admin/grants(read tier) lists grants, newest first byissued_atand thengrant_id. Itsstatusfilter acceptsactive(neither revoked nor ended),expiredandrevoked. Each summary’sstatusisactive,scheduled(not valid yet),exhausted(its admitted uses reach its budget),expiredorrevoked; theactivefilter includesscheduledandexhaustedgrants. Pages are stable:next_beforeis the last grant’s ID, passed back asbefore.GET /admin/grants/<grant_id>shows one grant, itsuses(admitted),remaining_uses,chain_length(admitted and quarantined positions) and revocation.GET /admin/grants/<grant_id>/useslists its use chain, newest first: each use’suse_id,use_sequence,state(admittedorquarantined, with itsrejection_code),executed_at,recorded_at,store_sequence,resource_id,resource_sequenceandoutcome(limit,beforeause_sequence,next_before).GET /admin/evidence/statusreportsevidence_grantsand, with grants on,grants_issued,grants_active,grants_revokedandgrant_uses.GET /admin/evidence/changes/<resource_id>shows each use as a changegoverned_by: grant. An observer’s report of a change made under a grant is matched to the use (by version, as for execution evidence) and is governed by the grant; one the observer’s own facts deny is flagged, and its remediation armed.Retention keeps a grant with its uses, its quarantined uses and its revocation until no more can be uploaded (its end plus its validity;
held_backwith"reason": "usable_grants"), and carries the resource’s head in its checkpoint.Audit packs hold the grants, uses and revocations of their range. For each grant a range’s uses (or quarantined uses) belong to, they cite the grant, its whole use chain before the range (the admitted uses and the quarantined uses that continued it) and its revocation, so the budget, the time-token heads and the chain are checked. They check the policy versions a grant is bound to and its basis: the attestation, or the agreement (held in
trust/agreements.jsonl), and report grants and revocations (Audit Pack Format).
Configuration¶
Variable |
Default |
Meaning |
|---|---|---|
|
|
|
|
|
the longest validity this NA grants (60 to 86400) |
|
|
the largest budget (1 to 10000) |
|
|
the most uses that may follow the use a time token names (1 to 1000); also the default of a grant that names none |
The late-upload window is each grant’s own validity, and the clock skew (for uses dated in the future, and before their time token) is 300 seconds, fixed so every verifier applies the same rule.