Skip to main content

PermissionToken

Struct PermissionToken 

Source
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: EntityId

Who issued this token.

§subject: EntityId

Who this token authorizes.

§scope: TokenScope

What actions are permitted.

§channel_hash: u64

Channel restriction (canonical ChannelHash; combine with TokenScope::WILDCARD for cross-channel grants).

§issuer_generation: u32

Issuer-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: u64

Valid from (unix timestamp seconds).

§not_after: u64

Valid until (unix timestamp seconds).

§delegation_depth: u8

How many times this token can be re-delegated.

§nonce: u64

Unique nonce for revocation.

§signature: [u8; 64]

Ed25519 signature over all preceding fields.

Implementations§

Source§

impl PermissionToken

Source

pub const WIRE_SIZE: usize

Total serialized size.

Source

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.

Source

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.

Source

pub fn verify(&self) -> Result<(), TokenError>

Verify the token’s signature against the issuer’s public key.

Source

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.

Source

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.

Source

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).

Source

pub fn authorizes(&self, action: TokenScope, channel: u64) -> bool

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::WILDCARD bit set (authorized on every channel regardless of channel_hash), OR
  • the token’s channel_hash matches the supplied channel.

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.

Source

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.

Source

pub fn to_bytes(&self) -> Vec<u8>

Serialize to wire format.

Source

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

Source§

fn clone(&self) -> PermissionToken

Returns a duplicate of the value. Read more
1.0.0 (const: unstable) · Source§

fn clone_from(&mut self, source: &Self)

Performs copy-assignment from source. Read more
Source§

impl Debug for PermissionToken

Source§

fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), Error>

Formats the value using the given formatter. Read more

Auto Trait Implementations§

Blanket Implementations§

Source§

impl<T> Any for T
where T: 'static + ?Sized,

Source§

fn type_id(&self) -> TypeId

Gets the TypeId of self. Read more
Source§

impl<T> Borrow<T> for T
where T: ?Sized,

Source§

fn borrow(&self) -> &T

Immutably borrows from an owned value. Read more
Source§

impl<T> BorrowMut<T> for T
where T: ?Sized,

Source§

fn borrow_mut(&mut self) -> &mut T

Mutably borrows from an owned value. Read more
Source§

impl<T> CloneToUninit for T
where T: Clone,

Source§

unsafe fn clone_to_uninit(&self, dest: *mut u8)

🔬This is a nightly-only experimental API. (clone_to_uninit)
Performs copy-assignment from self to dest. Read more
Source§

impl<T> DynClone for T
where T: Clone,

Source§

fn __clone_box(&self, _: Private) -> *mut ()

Source§

impl<T> From<T> for T

Source§

fn from(t: T) -> T

Returns the argument unchanged.

Source§

impl<T> Instrument for T

Source§

fn instrument(self, span: Span) -> Instrumented<Self>

Instruments this type with the provided Span, returning an Instrumented wrapper. Read more
Source§

fn in_current_span(self) -> Instrumented<Self>

Instruments this type with the current Span, returning an Instrumented wrapper. Read more
Source§

impl<T, U> Into<U> for T
where U: From<T>,

Source§

fn into(self) -> U

Calls U::from(self).

That is, this conversion is whatever the implementation of From<T> for U chooses to do.

Source§

impl<T> Same for T

Source§

type Output = T

Should always be Self
Source§

impl<T> ToOwned for T
where T: Clone,

Source§

type Owned = T

The resulting type after obtaining ownership.
Source§

fn to_owned(&self) -> T

Creates owned data from borrowed data, usually by cloning. Read more
Source§

fn clone_into(&self, target: &mut T)

Uses borrowed data to replace owned data, usually by cloning. Read more
Source§

impl<T, U> TryFrom<U> for T
where U: Into<T>,

Source§

type Error = Infallible

The type returned in the event of a conversion error.
Source§

fn try_from(value: U) -> Result<T, <T as TryFrom<U>>::Error>

Performs the conversion.
Source§

impl<T, U> TryInto<U> for T
where U: TryFrom<T>,

Source§

type Error = <U as TryFrom<T>>::Error

The type returned in the event of a conversion error.
Source§

fn try_into(self) -> Result<U, <U as TryFrom<T>>::Error>

Performs the conversion.
Source§

impl<T> WithSubscriber for T

Source§

fn with_subscriber<S>(self, subscriber: S) -> WithDispatch<Self>
where S: Into<Dispatch>,

Attaches the provided Subscriber to this type, returning a WithDispatch wrapper. Read more
Source§

fn with_current_subscriber(self) -> WithDispatch<Self>

Attaches the current default Subscriber to this type, returning a WithDispatch wrapper. Read more