pub struct PermissionToken {
pub issuer: EntityId,
pub subject: EntityId,
pub scope: TokenScope,
pub channel_hash: u64,
pub issuer_generation: u32,
pub not_before: u64,
pub not_after: u64,
pub delegation_depth: u8,
pub nonce: u64,
pub signature: [u8; 64],
}Expand description
A signed, delegatable permission token.
Wire format (169 bytes):
issuer: 32 bytes (EntityId)
subject: 32 bytes (EntityId)
scope: 4 bytes (u32)
channel_hash: 8 bytes (ChannelHash, u64; combine with WILDCARD scope for "all channels")
issuer_generation: 4 bytes (u32; floor below which the issuer revokes outstanding tokens)
not_before: 8 bytes (u64 unix timestamp)
not_after: 8 bytes (u64 unix timestamp)
delegation_depth: 1 byte (u8)
nonce: 8 bytes (u64)
--- signed above ---
signature: 64 bytes (ed25519)issuer_generation participates in revocation: an issuer that
wants to invalidate every outstanding token (including delegated
children) bumps its floor in the RevocationRegistry; the
cache rejects any token whose generation is below the current
floor. Children inherit their parent’s generation at delegation
time, so revoking a parent transitively revokes its descendants
without a parent-chain walk.
Fields§
§issuer: EntityIdWho issued this token.
subject: EntityIdWho this token authorizes.
scope: TokenScopeWhat actions are permitted.
channel_hash: u64Channel restriction (canonical ChannelHash; combine with
TokenScope::WILDCARD for cross-channel grants).
issuer_generation: u32Issuer-rotation floor. Tokens with issuer_generation < current floor in the RevocationRegistry are rejected by
TokenCache::check; bumping the floor invalidates every
outstanding token from that issuer (including delegated
children, which inherit the value from their parent).
not_before: u64Valid from (unix timestamp seconds).
not_after: u64Valid until (unix timestamp seconds).
delegation_depth: u8How many times this token can be re-delegated.
nonce: u64Unique nonce for revocation.
signature: [u8; 64]Ed25519 signature over all preceding fields.
Implementations§
Source§impl PermissionToken
impl PermissionToken
Sourcepub fn issue(
issuer_keypair: &EntityKeypair,
subject: EntityId,
scope: TokenScope,
channel_hash: u64,
duration_secs: u64,
delegation_depth: u8,
) -> PermissionToken
pub fn issue( issuer_keypair: &EntityKeypair, subject: EntityId, scope: TokenScope, channel_hash: u64, duration_secs: u64, delegation_depth: u8, ) -> PermissionToken
Issue a new token.
duration_secs is clamped: a value that would overflow
now + duration_secs saturates not_after at u64::MAX,
producing a functionally-never-expiring token rather than
wrapping the timestamp or panicking. Callers who want to
reject pathological TTLs should range-check at the SDK
layer.
Panics if issuer_keypair is public-only (the migration-
source path zeroizes its keypair after ActivateAck, leaving
such a keypair). FFI callers and any path that may receive a
public-only keypair must use Self::try_issue instead;
issue is preserved as a convenience wrapper for callers
(notably tests) that own a freshly-generated keypair and
know it has its signing half.
Sourcepub fn try_issue(
issuer_keypair: &EntityKeypair,
subject: EntityId,
scope: TokenScope,
channel_hash: u64,
duration_secs: u64,
delegation_depth: u8,
) -> Result<PermissionToken, TokenError>
pub fn try_issue( issuer_keypair: &EntityKeypair, subject: EntityId, scope: TokenScope, channel_hash: u64, duration_secs: u64, delegation_depth: u8, ) -> Result<PermissionToken, TokenError>
Fallible counterpart to Self::issue: returns
TokenError::ReadOnly when the issuer keypair lacks its
signing half (post-migration / public-only keypair) instead
of panicking. The FFI bindings route through this function
so a panic doesn’t unwind across extern "C" into
C/Go-cgo/NAPI/PyO3 callers — undefined behaviour.
Sourcepub fn verify(&self) -> Result<(), TokenError>
pub fn verify(&self) -> Result<(), TokenError>
Verify the token’s signature against the issuer’s public key.
Sourcepub fn is_valid(&self) -> Result<(), TokenError>
pub fn is_valid(&self) -> Result<(), TokenError>
Check if the token is currently valid (signature + time bounds).
Both bounds are inclusive-expiry: the token is live while
not_before <= now < not_after. At now == not_after the
token is already expired. The cache sweep
(TokenCache::evict_expired) has always used this convention
(retain(|t| t.not_after > now) drops boundary entries);
the earlier is_valid / is_expired wording accidentally
treated not_after as the last valid second, giving every
token a one-second bonus over what the sweep believed.
Aligning everything on strict “< not_after” removes the
off-by-one and makes the token lifetime exactly
duration_secs seconds as issue() promises.
Sourcepub fn is_valid_with_skew(&self, skew_secs: u64) -> Result<(), TokenError>
pub fn is_valid_with_skew(&self, skew_secs: u64) -> Result<(), TokenError>
Same as Self::is_valid but applies skew_secs of clock-
skew tolerance to both bounds. A token is accepted while
now >= not_before - skew AND now < not_after + skew.
TokenCache::check uses this via the cache’s configured
clock_skew_secs (default 0); direct FFI / UI callers stick
with Self::is_valid.
Sourcepub fn is_expired(&self) -> bool
pub fn is_expired(&self) -> bool
Pure time-bound check: true iff the host wall-clock has
reached not_after. Deliberately does not touch the
signature — callers wanting end-to-end validity use
Self::is_valid, and signature integrity alone is
Self::verify. This separation matters because a
tampered-but-expired token is still expired, and every
binding’s token_is_expired helper documents itself as a
pure time check.
Boundary: now == not_after ⇒ expired (matches
Self::is_valid and the cache’s eviction convention).
Check if this token authorizes a specific action on a channel.
Returns true iff the token’s scope contains the requested
action AND either:
- the token has the
TokenScope::WILDCARDbit set (authorized on every channel regardless ofchannel_hash), OR - the token’s
channel_hashmatches the suppliedchannel.
The previous convention — channel_hash == 0 meaning “wildcard,
all channels” — is no longer honored. A legitimate channel
whose xxh3-truncated ChannelHash hashes to 0 would otherwise
accidentally turn a narrowly-scoped token into a universal
grant, which an attacker able to register channel names could
brute-force since xxh3 is non-cryptographic.
Sourcepub fn delegate(
&self,
signer: &EntityKeypair,
new_subject: EntityId,
restricted_scope: TokenScope,
) -> Result<PermissionToken, TokenError>
pub fn delegate( &self, signer: &EntityKeypair, new_subject: EntityId, restricted_scope: TokenScope, ) -> Result<PermissionToken, TokenError>
Delegate this token to another entity with restricted scope.
Returns None if delegation is not allowed (depth exhausted or
DELEGATE not in scope).
The child’s not_after is copied from the parent verbatim,
NOT derived from parent.not_after - now. The subtract-then-
re-read-clock approach lost multiple seconds of validity
when the parent was near expiry — the child’s issue() call
re-reads current_timestamp() and computes
now + (parent.not_after - previous_now), which rounds down
by the wall-clock delta between the two reads. Copying
not_after avoids the double-read and guarantees the
child’s lifetime is parent.not_after - child.not_before
exactly.
Sourcepub fn from_bytes(data: &[u8]) -> Result<PermissionToken, TokenError>
pub fn from_bytes(data: &[u8]) -> Result<PermissionToken, TokenError>
Deserialize from wire format.
Rejects buffers whose length is anything other than exactly
Self::WIRE_SIZE. Previously this method only guarded the
lower bound, silently accepting concatenated or trailing-
garbage payloads — which weakened the wire-format contract
and let malformed blobs parse as valid tokens. Callers
framing tokens inside a larger message must slice to exactly
WIRE_SIZE before calling this.
Trait Implementations§
Source§impl Clone for PermissionToken
impl Clone for PermissionToken
Source§fn clone(&self) -> PermissionToken
fn clone(&self) -> PermissionToken
1.0.0 (const: unstable) · Source§fn clone_from(&mut self, source: &Self)
fn clone_from(&mut self, source: &Self)
source. Read more