use dashmap::DashMap;
use ed25519_dalek::Signature;
use std::sync::atomic::{AtomicBool, Ordering as AtomicOrdering};
use std::sync::Arc;
use std::time::{SystemTime, UNIX_EPOCH};
use super::entity::{EntityId, EntityKeypair};
use crate::adapter::net::channel::ChannelHash;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct TokenScope {
bits: u32,
}
impl TokenScope {
pub const NONE: Self = Self { bits: 0 };
pub const PUBLISH: Self = Self { bits: 0b0001 };
pub const SUBSCRIBE: Self = Self { bits: 0b0010 };
pub const ADMIN: Self = Self { bits: 0b0100 };
pub const DELEGATE: Self = Self { bits: 0b1000 };
pub const WILDCARD: Self = Self { bits: 0b1_0000 };
pub const ALL: Self = Self { bits: 0b1111 };
#[inline]
pub const fn from_bits(bits: u32) -> Self {
Self { bits }
}
#[inline]
pub const fn bits(self) -> u32 {
self.bits
}
#[inline]
pub const fn contains(self, other: Self) -> bool {
if other.bits == 0 {
return false;
}
(self.bits & other.bits) == other.bits
}
#[inline]
pub const fn intersect(self, other: Self) -> Self {
Self {
bits: self.bits & other.bits,
}
}
#[inline]
pub const fn union(self, other: Self) -> Self {
Self {
bits: self.bits | other.bits,
}
}
pub fn with_channel(self, channel_hash: ChannelHash) -> ScopedToken {
ScopedToken {
scope: self,
channel_hash: Some(channel_hash),
}
}
}
#[derive(Debug, Clone, Copy)]
pub struct ScopedToken {
pub scope: TokenScope,
pub channel_hash: Option<ChannelHash>,
}
#[derive(Clone)]
pub struct PermissionToken {
pub issuer: EntityId,
pub subject: EntityId,
pub scope: TokenScope,
pub channel_hash: ChannelHash,
pub issuer_generation: u32,
pub not_before: u64,
pub not_after: u64,
pub delegation_depth: u8,
pub nonce: u64,
pub signature: [u8; 64],
}
impl PermissionToken {
const SIGNED_PAYLOAD_SIZE: usize = 32 + 32 + 4 + 8 + 4 + 8 + 8 + 1 + 8;
pub const WIRE_SIZE: usize = Self::SIGNED_PAYLOAD_SIZE + 64;
pub fn issue(
issuer_keypair: &EntityKeypair,
subject: EntityId,
scope: TokenScope,
channel_hash: ChannelHash,
duration_secs: u64,
delegation_depth: u8,
) -> Self {
match Self::try_issue(
issuer_keypair,
subject,
scope,
channel_hash,
duration_secs,
delegation_depth,
) {
Ok(token) => token,
Err(TokenError::ReadOnly) => {
panic!("PermissionToken::issue called with a public-only keypair — use try_issue")
}
Err(TokenError::ZeroTtl) => {
panic!("PermissionToken::issue called with duration_secs == 0 — use try_issue")
}
Err(e) => panic!("PermissionToken::issue failed: {e:?} — use try_issue"),
}
}
pub fn try_issue(
issuer_keypair: &EntityKeypair,
subject: EntityId,
scope: TokenScope,
channel_hash: ChannelHash,
duration_secs: u64,
delegation_depth: u8,
) -> Result<Self, TokenError> {
if duration_secs == 0 {
return Err(TokenError::ZeroTtl);
}
let now = current_timestamp();
let mut nonce_bytes = [0u8; 8];
if let Err(e) = getrandom::fill(&mut nonce_bytes) {
eprintln!(
"FATAL: PermissionToken nonce getrandom failure ({e:?}); aborting to avoid predictable token nonce"
);
std::process::abort();
}
let nonce = u64::from_le_bytes(nonce_bytes);
let mut token = Self {
issuer: issuer_keypair.entity_id().clone(),
subject,
scope,
channel_hash,
issuer_generation: 0,
not_before: now,
not_after: now.saturating_add(duration_secs),
delegation_depth,
nonce,
signature: [0u8; 64],
};
let payload = token.signed_payload();
let sig = issuer_keypair
.try_sign(&payload)
.map_err(|_| TokenError::ReadOnly)?;
token.signature = sig.to_bytes();
Ok(token)
}
pub fn verify(&self) -> Result<(), TokenError> {
let payload = self.signed_payload();
let sig = Signature::from_bytes(&self.signature);
self.issuer
.verify(&payload, &sig)
.map_err(|_| TokenError::InvalidSignature)
}
pub fn is_valid(&self) -> Result<(), TokenError> {
self.is_valid_with_skew(0)
}
pub fn is_valid_with_skew(&self, skew_secs: u64) -> Result<(), TokenError> {
self.verify()?;
let now = current_timestamp();
if now < self.not_before.saturating_sub(skew_secs) {
return Err(TokenError::NotYetValid);
}
if now >= self.not_after.saturating_add(skew_secs) {
return Err(TokenError::Expired);
}
Ok(())
}
pub fn is_expired(&self) -> bool {
current_timestamp() >= self.not_after
}
pub fn authorizes(&self, action: TokenScope, channel: ChannelHash) -> bool {
if !self.scope.contains(action) {
return false;
}
if self.scope.contains(TokenScope::WILDCARD) {
return true;
}
self.channel_hash == channel
}
pub fn delegate(
&self,
signer: &EntityKeypair,
new_subject: EntityId,
restricted_scope: TokenScope,
) -> Result<Self, TokenError> {
self.is_valid()?;
if self.delegation_depth == 0 {
return Err(TokenError::DelegationExhausted);
}
if !self.scope.contains(TokenScope::DELEGATE) {
return Err(TokenError::DelegationNotAllowed);
}
if signer.entity_id() != &self.subject {
return Err(TokenError::NotAuthorized);
}
let new_scope = self.scope.intersect(restricted_scope);
let now = current_timestamp();
let mut nonce_bytes = [0u8; 8];
if let Err(e) = getrandom::fill(&mut nonce_bytes) {
eprintln!(
"FATAL: PermissionToken nonce getrandom failure ({e:?}); aborting to avoid predictable token nonce"
);
std::process::abort();
}
let nonce = u64::from_le_bytes(nonce_bytes);
let mut child = Self {
issuer: signer.entity_id().clone(),
subject: new_subject,
scope: new_scope,
channel_hash: self.channel_hash,
issuer_generation: self.issuer_generation,
not_before: now,
not_after: self.not_after,
delegation_depth: self.delegation_depth - 1,
nonce,
signature: [0u8; 64],
};
let payload = child.signed_payload();
let sig = signer
.try_sign(&payload)
.map_err(|_| TokenError::ReadOnly)?;
child.signature = sig.to_bytes();
Ok(child)
}
pub(crate) fn signed_payload(&self) -> [u8; Self::SIGNED_PAYLOAD_SIZE] {
let mut buf = [0u8; Self::SIGNED_PAYLOAD_SIZE];
let mut off = 0;
buf[off..off + 32].copy_from_slice(self.issuer.as_bytes());
off += 32;
buf[off..off + 32].copy_from_slice(self.subject.as_bytes());
off += 32;
buf[off..off + 4].copy_from_slice(&self.scope.bits().to_le_bytes());
off += 4;
buf[off..off + 8].copy_from_slice(&self.channel_hash.to_le_bytes());
off += 8;
buf[off..off + 4].copy_from_slice(&self.issuer_generation.to_le_bytes());
off += 4;
buf[off..off + 8].copy_from_slice(&self.not_before.to_le_bytes());
off += 8;
buf[off..off + 8].copy_from_slice(&self.not_after.to_le_bytes());
off += 8;
buf[off] = self.delegation_depth;
off += 1;
buf[off..off + 8].copy_from_slice(&self.nonce.to_le_bytes());
buf
}
pub fn to_bytes(&self) -> Vec<u8> {
let mut buf = Vec::with_capacity(Self::WIRE_SIZE);
buf.extend_from_slice(&self.signed_payload());
buf.extend_from_slice(&self.signature);
buf
}
#[expect(
clippy::unwrap_used,
reason = "data.len() == WIRE_SIZE checked above; fixed-offset slices into the buffer convert infallibly to fixed-size arrays"
)]
pub fn from_bytes(data: &[u8]) -> Result<Self, TokenError> {
if data.len() != Self::WIRE_SIZE {
return Err(TokenError::InvalidFormat);
}
let issuer = EntityId::from_bytes(data[0..32].try_into().unwrap());
let subject = EntityId::from_bytes(data[32..64].try_into().unwrap());
let scope = TokenScope::from_bits(u32::from_le_bytes(data[64..68].try_into().unwrap()));
let channel_hash = ChannelHash::from_le_bytes(data[68..76].try_into().unwrap());
let issuer_generation = u32::from_le_bytes(data[76..80].try_into().unwrap());
let not_before = u64::from_le_bytes(data[80..88].try_into().unwrap());
let not_after = u64::from_le_bytes(data[88..96].try_into().unwrap());
let delegation_depth = data[96];
let nonce = u64::from_le_bytes(data[97..105].try_into().unwrap());
let mut signature = [0u8; 64];
signature.copy_from_slice(&data[105..169]);
Ok(Self {
issuer,
subject,
scope,
channel_hash,
issuer_generation,
not_before,
not_after,
delegation_depth,
nonce,
signature,
})
}
}
impl std::fmt::Debug for PermissionToken {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("PermissionToken")
.field("issuer", &self.issuer)
.field("subject", &self.subject)
.field("scope", &format!("{:04b}", self.scope.bits()))
.field("channel_hash", &format!("{:08x}", self.channel_hash))
.field("delegation_depth", &self.delegation_depth)
.field("nonce", &self.nonce)
.finish()
}
}
pub const MAX_TOKEN_SLOTS: usize = 65_536;
pub const MAX_TOKENS_PER_SLOT: usize = 32;
pub const TOKEN_CLOCK_SKEW_SECS_RECOMMENDED: u64 = 60;
pub struct TokenCache {
tokens: DashMap<([u8; 32], ChannelHash), Vec<PermissionToken>>,
revocation: Arc<RevocationRegistry>,
clock_skew_secs: u64,
wildcard_inserted: AtomicBool,
}
#[derive(Debug, Default)]
pub struct RevocationRegistry {
floors: DashMap<[u8; 32], u32>,
}
impl RevocationRegistry {
pub fn new() -> Self {
Self::default()
}
pub fn revoke_below(&self, issuer: &EntityId, generation: u32) {
let key = *issuer.as_bytes();
self.floors
.entry(key)
.and_modify(|cur| {
if generation > *cur {
*cur = generation;
}
})
.or_insert(generation);
}
pub fn floor(&self, issuer: &EntityId) -> u32 {
self.floors
.get(issuer.as_bytes())
.map(|r| *r.value())
.unwrap_or(0)
}
#[inline]
pub fn is_revoked(&self, token: &PermissionToken) -> bool {
token.issuer_generation < self.floor(&token.issuer)
}
}
impl TokenCache {
pub fn new() -> Self {
Self {
tokens: DashMap::new(),
revocation: Arc::new(RevocationRegistry::new()),
clock_skew_secs: 0,
wildcard_inserted: AtomicBool::new(false),
}
}
pub fn with_clock_skew(skew_secs: u64) -> Self {
Self {
tokens: DashMap::new(),
revocation: Arc::new(RevocationRegistry::new()),
clock_skew_secs: skew_secs,
wildcard_inserted: AtomicBool::new(false),
}
}
pub fn with_revocation_registry(revocation: Arc<RevocationRegistry>) -> Self {
Self {
tokens: DashMap::new(),
revocation,
clock_skew_secs: 0,
wildcard_inserted: AtomicBool::new(false),
}
}
pub fn set_clock_skew(&mut self, skew_secs: u64) {
self.clock_skew_secs = skew_secs;
}
pub fn clock_skew_secs(&self) -> u64 {
self.clock_skew_secs
}
pub fn revocation(&self) -> &Arc<RevocationRegistry> {
&self.revocation
}
pub fn insert(&self, token: PermissionToken) -> Result<(), TokenError> {
token.verify()?;
self.insert_unchecked(token);
Ok(())
}
pub fn insert_unchecked(&self, token: PermissionToken) {
let is_wildcard = token.scope.contains(TokenScope::WILDCARD);
let slot_channel = if is_wildcard { 0 } else { token.channel_hash };
let key = (*token.subject.as_bytes(), slot_channel);
if is_wildcard {
self.wildcard_inserted.store(true, AtomicOrdering::Relaxed);
}
let inserted_novel_key = {
let mut entry = self.tokens.entry(key).or_default();
let was_empty = entry.is_empty();
if let Some(slot) = entry.iter_mut().find(|t| t.scope == token.scope) {
*slot = token;
} else if entry.len() < MAX_TOKENS_PER_SLOT {
entry.push(token);
}
was_empty
};
if inserted_novel_key && self.tokens.len() > MAX_TOKEN_SLOTS {
self.tokens.remove(&key);
}
}
pub fn check(
&self,
subject: &EntityId,
action: TokenScope,
channel_hash: ChannelHash,
) -> Result<(), TokenError> {
if let Some(slot) = self.tokens.get(&(*subject.as_bytes(), channel_hash)) {
if slot.value().iter().any(|t| {
t.is_valid_with_skew(self.clock_skew_secs).is_ok()
&& !self.revocation.is_revoked(t)
&& t.subject.as_bytes() == subject.as_bytes()
&& t.authorizes(action, channel_hash)
}) {
return Ok(());
}
}
if !self.wildcard_inserted.load(AtomicOrdering::Relaxed) {
return Err(TokenError::NotAuthorized);
}
if let Some(slot) = self.tokens.get(&(*subject.as_bytes(), 0)) {
if slot.value().iter().any(|t| {
t.is_valid_with_skew(self.clock_skew_secs).is_ok()
&& !self.revocation.is_revoked(t)
&& t.subject.as_bytes() == subject.as_bytes()
&& t.authorizes(action, channel_hash)
}) {
return Ok(());
}
}
Err(TokenError::NotAuthorized)
}
pub fn get(&self, subject: &EntityId, channel_hash: ChannelHash) -> Option<PermissionToken> {
let slot = self.tokens.get(&(*subject.as_bytes(), channel_hash))?;
let tokens = slot.value();
tokens
.iter()
.find(|t| t.is_valid().is_ok())
.or_else(|| tokens.first())
.cloned()
}
pub fn evict_expired(&self) {
let now = current_timestamp();
self.tokens.retain(|_, slot| {
slot.retain(|t| t.not_after > now);
!slot.is_empty()
});
}
pub fn len(&self) -> usize {
self.tokens.iter().map(|e| e.value().len()).sum()
}
pub fn is_empty(&self) -> bool {
self.tokens.is_empty()
}
}
impl Default for TokenCache {
fn default() -> Self {
Self::new()
}
}
impl std::fmt::Debug for TokenCache {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("TokenCache")
.field("count", &self.tokens.len())
.finish()
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum TokenError {
InvalidSignature,
NotYetValid,
Expired,
DelegationExhausted,
DelegationNotAllowed,
NotAuthorized,
InvalidFormat,
ReadOnly,
ZeroTtl,
}
impl std::fmt::Display for TokenError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::InvalidSignature => write!(f, "invalid token signature"),
Self::NotYetValid => write!(f, "token not yet valid"),
Self::Expired => write!(f, "token expired"),
Self::DelegationExhausted => write!(f, "delegation depth exhausted"),
Self::DelegationNotAllowed => write!(f, "delegation not allowed by scope"),
Self::NotAuthorized => write!(f, "not authorized"),
Self::InvalidFormat => write!(f, "invalid token format"),
Self::ReadOnly => write!(f, "signer keypair is public-only"),
Self::ZeroTtl => write!(f, "token TTL must be > 0 seconds"),
}
}
}
impl std::error::Error for TokenError {}
fn current_timestamp() -> u64 {
SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap_or_default()
.as_secs()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_issue_and_verify() {
let issuer = EntityKeypair::generate();
let subject = EntityKeypair::generate();
let token = PermissionToken::issue(
&issuer,
subject.entity_id().clone(),
TokenScope::PUBLISH
.union(TokenScope::SUBSCRIBE)
.union(TokenScope::WILDCARD),
0, 3600,
0,
);
assert!(token.verify().is_ok());
assert!(token.is_valid().is_ok());
}
#[test]
fn try_issue_rejects_zero_ttl() {
let issuer = EntityKeypair::generate();
let subject = EntityKeypair::generate();
let err = PermissionToken::try_issue(
&issuer,
subject.entity_id().clone(),
TokenScope::PUBLISH,
0,
0, 0,
)
.unwrap_err();
assert_eq!(err, TokenError::ZeroTtl, "expected ZeroTtl, got {:?}", err);
}
#[test]
#[should_panic(expected = "duration_secs == 0")]
fn issue_zero_ttl_panic_message_blames_ttl_not_keypair() {
let issuer = EntityKeypair::generate();
let subject = EntityKeypair::generate();
let _ = PermissionToken::issue(
&issuer,
subject.entity_id().clone(),
TokenScope::PUBLISH,
0,
0, 0,
);
}
#[test]
#[should_panic(expected = "public-only keypair")]
fn issue_public_only_keypair_panic_message_blames_keypair() {
let full = EntityKeypair::generate();
let issuer = EntityKeypair::public_only(full.entity_id().clone());
let subject = EntityKeypair::generate();
let _ = PermissionToken::issue(
&issuer,
subject.entity_id().clone(),
TokenScope::PUBLISH,
0,
3600,
0,
);
}
#[test]
fn try_issue_accepts_one_second_ttl() {
let issuer = EntityKeypair::generate();
let subject = EntityKeypair::generate();
let token = PermissionToken::try_issue(
&issuer,
subject.entity_id().clone(),
TokenScope::PUBLISH,
0,
1, 0,
)
.expect("ttl=1 must mint cleanly (boundary)");
assert!(token.is_valid().is_ok());
}
#[test]
fn test_tampered_token() {
let issuer = EntityKeypair::generate();
let subject = EntityKeypair::generate();
let mut token = PermissionToken::issue(
&issuer,
subject.entity_id().clone(),
TokenScope::PUBLISH,
0,
3600,
0,
);
token.scope = TokenScope::ADMIN;
assert!(token.verify().is_err());
}
#[test]
fn test_expired_token() {
let issuer = EntityKeypair::generate();
let subject = EntityKeypair::generate();
let mut token = PermissionToken::issue(
&issuer,
subject.entity_id().clone(),
TokenScope::PUBLISH,
0,
1,
0,
);
token.not_after = 0;
let payload = token.signed_payload();
token.signature = issuer.sign(&payload).to_bytes();
assert!(token.verify().is_ok(), "signature is valid");
assert!(
token.is_expired(),
"backdated token must report expired under inclusive-expiry",
);
assert!(
matches!(token.is_valid(), Err(TokenError::Expired)),
"is_valid must agree with is_expired at the boundary",
);
}
#[test]
fn is_valid_and_is_expired_agree_at_not_after_boundary() {
let issuer = EntityKeypair::generate();
let subject = EntityKeypair::generate();
let mut token = PermissionToken::issue(
&issuer,
subject.entity_id().clone(),
TokenScope::PUBLISH,
0,
3600,
0,
);
token.not_after = current_timestamp();
let payload = token.signed_payload();
token.signature = issuer.sign(&payload).to_bytes();
assert!(
token.is_expired(),
"is_expired must return true at now == not_after (inclusive)",
);
assert!(
matches!(token.is_valid(), Err(TokenError::Expired)),
"is_valid must agree: Expired at now == not_after (strict)",
);
let cache = TokenCache::new();
cache.insert_unchecked(token);
cache.evict_expired();
assert_eq!(
cache.len(),
0,
"evict_expired must drop a boundary token — all three code paths \
(is_valid, is_expired, evict_expired) must agree on the boundary",
);
}
#[test]
fn is_expired_ignores_signature_tampering() {
let issuer = EntityKeypair::generate();
let subject = EntityKeypair::generate();
let mut token = PermissionToken::issue(
&issuer,
subject.entity_id().clone(),
TokenScope::PUBLISH,
0,
3600,
0,
);
assert!(!token.is_expired(), "fresh token is not expired");
token.not_after = 0;
token.signature[0] ^= 0xFF;
assert!(
token.verify().is_err(),
"mutated payload / signature must fail verify",
);
assert!(
!matches!(token.is_valid(), Err(TokenError::Expired)),
"captures the pre-fix pattern: is_valid() short-circuits \
on signature, never reaches the time check",
);
assert!(
token.is_expired(),
"is_expired() must be a pure time check, independent \
of signature validity",
);
}
#[test]
fn test_channel_filter() {
let issuer = EntityKeypair::generate();
let subject = EntityKeypair::generate();
let token = PermissionToken::issue(
&issuer,
subject.entity_id().clone(),
TokenScope::PUBLISH,
0xABCD, 3600,
0,
);
assert!(token.authorizes(TokenScope::PUBLISH, 0xABCD));
assert!(!token.authorizes(TokenScope::PUBLISH, 0x1234)); assert!(!token.authorizes(TokenScope::SUBSCRIBE, 0xABCD)); }
#[test]
fn test_wildcard_channel() {
let issuer = EntityKeypair::generate();
let subject = EntityKeypair::generate();
let token = PermissionToken::issue(
&issuer,
subject.entity_id().clone(),
TokenScope::PUBLISH.union(TokenScope::WILDCARD),
0,
3600,
0,
);
assert!(token.authorizes(TokenScope::PUBLISH, 0xABCD));
assert!(token.authorizes(TokenScope::PUBLISH, 0x1234));
assert!(token.authorizes(TokenScope::PUBLISH, 0));
}
#[test]
fn test_regression_channel_hash_zero_is_not_wildcard() {
let issuer = EntityKeypair::generate();
let subject = EntityKeypair::generate();
let token = PermissionToken::issue(
&issuer,
subject.entity_id().clone(),
TokenScope::PUBLISH, 0, 3600,
0,
);
assert!(token.authorizes(TokenScope::PUBLISH, 0));
assert!(
!token.authorizes(TokenScope::PUBLISH, 0xABCD),
"channel_hash=0 without WILDCARD must not grant access to arbitrary channels"
);
assert!(
!token.authorizes(TokenScope::PUBLISH, 0x1234),
"channel_hash=0 without WILDCARD must not grant access to arbitrary channels"
);
}
#[test]
fn test_delegation() {
let root = EntityKeypair::generate();
let node_a = EntityKeypair::generate();
let node_b = EntityKeypair::generate();
let token_a = PermissionToken::issue(
&root,
node_a.entity_id().clone(),
TokenScope::ALL,
0,
3600,
2,
);
assert!(token_a.is_valid().is_ok());
let token_b = token_a
.delegate(
&node_a,
node_b.entity_id().clone(),
TokenScope::PUBLISH.union(TokenScope::DELEGATE),
)
.unwrap();
assert!(token_b.is_valid().is_ok());
assert_eq!(token_b.delegation_depth, 1);
assert!(token_b.authorizes(TokenScope::PUBLISH, 0));
assert!(!token_b.authorizes(TokenScope::ADMIN, 0)); }
#[test]
fn test_delegation_depth_exhausted() {
let root = EntityKeypair::generate();
let node_a = EntityKeypair::generate();
let node_b = EntityKeypair::generate();
let token = PermissionToken::issue(
&root,
node_a.entity_id().clone(),
TokenScope::ALL,
0,
3600,
0, );
let result = token.delegate(&node_a, node_b.entity_id().clone(), TokenScope::PUBLISH);
assert_eq!(result.unwrap_err(), TokenError::DelegationExhausted);
}
#[test]
fn test_delegation_wrong_signer() {
let root = EntityKeypair::generate();
let node_a = EntityKeypair::generate();
let node_b = EntityKeypair::generate();
let imposter = EntityKeypair::generate();
let token = PermissionToken::issue(
&root,
node_a.entity_id().clone(),
TokenScope::ALL,
0,
3600,
1,
);
let result = token.delegate(&imposter, node_b.entity_id().clone(), TokenScope::PUBLISH);
assert_eq!(result.unwrap_err(), TokenError::NotAuthorized);
}
#[test]
fn test_serialization_roundtrip() {
let issuer = EntityKeypair::generate();
let subject = EntityKeypair::generate();
let token = PermissionToken::issue(
&issuer,
subject.entity_id().clone(),
TokenScope::PUBLISH.union(TokenScope::SUBSCRIBE),
0xBEEF,
3600,
3,
);
let bytes = token.to_bytes();
assert_eq!(bytes.len(), PermissionToken::WIRE_SIZE);
let parsed = PermissionToken::from_bytes(&bytes).unwrap();
assert!(parsed.verify().is_ok());
assert_eq!(parsed.issuer, token.issuer);
assert_eq!(parsed.subject, token.subject);
assert_eq!(parsed.scope.bits(), token.scope.bits());
assert_eq!(parsed.channel_hash, 0xBEEF);
assert_eq!(parsed.delegation_depth, 3);
assert_eq!(parsed.nonce, token.nonce);
}
#[test]
fn token_scope_does_not_contain_none() {
for s in [
TokenScope::PUBLISH,
TokenScope::SUBSCRIBE,
TokenScope::ADMIN,
TokenScope::DELEGATE,
TokenScope::WILDCARD,
TokenScope::ALL,
TokenScope::PUBLISH.union(TokenScope::SUBSCRIBE),
] {
assert!(
!s.contains(TokenScope::NONE),
"scope {:?} must not contain NONE",
s.bits(),
);
}
assert!(
!TokenScope::NONE.contains(TokenScope::NONE),
"NONE.contains(NONE) must be false (no token authorizes the no-op action)",
);
assert!(TokenScope::ALL.contains(TokenScope::PUBLISH));
assert!(!TokenScope::PUBLISH.contains(TokenScope::ADMIN));
assert!(TokenScope::PUBLISH
.union(TokenScope::SUBSCRIBE)
.contains(TokenScope::SUBSCRIBE));
}
#[test]
fn test_token_cache() {
let issuer = EntityKeypair::generate();
let subject = EntityKeypair::generate();
let cache = TokenCache::new();
let token = PermissionToken::issue(
&issuer,
subject.entity_id().clone(),
TokenScope::PUBLISH,
0xABCD,
3600,
0,
);
let _ = cache.insert(token);
assert_eq!(cache.len(), 1);
assert!(cache
.check(subject.entity_id(), TokenScope::PUBLISH, 0xABCD)
.is_ok());
assert!(cache
.check(subject.entity_id(), TokenScope::PUBLISH, 0x1234)
.is_err());
assert!(cache
.check(subject.entity_id(), TokenScope::ADMIN, 0xABCD)
.is_err());
let unknown = EntityKeypair::generate();
assert!(cache
.check(unknown.entity_id(), TokenScope::PUBLISH, 0xABCD)
.is_err());
}
#[test]
fn revocation_floor_bump_invalidates_outstanding_tokens() {
let issuer = EntityKeypair::generate();
let subject = EntityKeypair::generate();
let cache = TokenCache::new();
let token = PermissionToken::issue(
&issuer,
subject.entity_id().clone(),
TokenScope::PUBLISH,
0xABCD_EF00_AAAA_BBBB,
3600,
0,
);
assert_eq!(token.issuer_generation, 0);
cache.insert(token).expect("token should verify");
assert!(cache
.check(
subject.entity_id(),
TokenScope::PUBLISH,
0xABCD_EF00_AAAA_BBBB,
)
.is_ok());
cache.revocation().revoke_below(issuer.entity_id(), 1);
assert!(cache
.check(
subject.entity_id(),
TokenScope::PUBLISH,
0xABCD_EF00_AAAA_BBBB,
)
.is_err());
}
#[test]
fn is_valid_with_skew_accepts_inside_window_rejects_outside() {
let issuer = EntityKeypair::generate();
let subject = EntityKeypair::generate();
let skew: u64 = 60;
let mut token = PermissionToken::issue(
&issuer,
subject.entity_id().clone(),
TokenScope::PUBLISH,
0,
3600,
0,
);
token.not_after = current_timestamp() - skew / 2;
let payload = token.signed_payload();
token.signature = issuer.sign(&payload).to_bytes();
assert!(
token.is_valid_with_skew(skew).is_ok(),
"is_valid_with_skew must accept tokens inside the past-skew window",
);
assert!(
matches!(token.is_valid(), Err(TokenError::Expired)),
"strict is_valid must reject the same token",
);
token.not_after = current_timestamp() - skew - 5;
let payload = token.signed_payload();
token.signature = issuer.sign(&payload).to_bytes();
assert!(
matches!(token.is_valid_with_skew(skew), Err(TokenError::Expired)),
"is_valid_with_skew must reject tokens past the past-skew window",
);
token.not_after = current_timestamp() + 3600;
token.not_before = current_timestamp() + skew / 2;
let payload = token.signed_payload();
token.signature = issuer.sign(&payload).to_bytes();
assert!(
token.is_valid_with_skew(skew).is_ok(),
"is_valid_with_skew must accept tokens inside the future-skew window",
);
assert!(
matches!(token.is_valid(), Err(TokenError::NotYetValid)),
"strict is_valid must reject the same token",
);
token.not_before = current_timestamp() + skew + 5;
let payload = token.signed_payload();
token.signature = issuer.sign(&payload).to_bytes();
assert!(
matches!(token.is_valid_with_skew(skew), Err(TokenError::NotYetValid),),
"is_valid_with_skew must reject tokens past the future-skew window",
);
}
#[test]
fn check_skips_wildcard_slot_when_no_wildcard_ever_inserted() {
let issuer = EntityKeypair::generate();
let subject = EntityKeypair::generate();
let cache = TokenCache::new();
let token = PermissionToken::issue(
&issuer,
subject.entity_id().clone(),
TokenScope::PUBLISH,
0xAAAA,
3600,
0,
);
cache.insert(token).unwrap();
assert!(cache
.check(subject.entity_id(), TokenScope::PUBLISH, 0xBBBB)
.is_err());
}
#[test]
fn check_walks_wildcard_slot_after_any_wildcard_insert() {
let issuer = EntityKeypair::generate();
let subject = EntityKeypair::generate();
let cache = TokenCache::new();
let wildcard = PermissionToken::issue(
&issuer,
subject.entity_id().clone(),
TokenScope::PUBLISH.union(TokenScope::WILDCARD),
0,
3600,
0,
);
cache.insert(wildcard).unwrap();
assert!(cache
.check(subject.entity_id(), TokenScope::PUBLISH, 0xDEAD)
.is_ok());
assert!(cache
.check(subject.entity_id(), TokenScope::PUBLISH, 0xBEEF)
.is_ok());
}
#[test]
fn token_cache_with_clock_skew_admits_inside_window() {
let issuer = EntityKeypair::generate();
let subject = EntityKeypair::generate();
let channel: ChannelHash = 0x1234_5678_9ABC_DEF0;
let mut token = PermissionToken::issue(
&issuer,
subject.entity_id().clone(),
TokenScope::PUBLISH,
channel,
3600,
0,
);
token.not_after = current_timestamp() - 5;
let payload = token.signed_payload();
token.signature = issuer.sign(&payload).to_bytes();
let strict = TokenCache::new();
strict.insert_unchecked(token.clone());
assert!(strict
.check(subject.entity_id(), TokenScope::PUBLISH, channel)
.is_err());
let lenient = TokenCache::with_clock_skew(60);
lenient.insert_unchecked(token);
assert!(lenient
.check(subject.entity_id(), TokenScope::PUBLISH, channel)
.is_ok());
}
#[test]
fn check_rejects_token_keyed_under_mismatched_subject() {
let issuer = EntityKeypair::generate();
let real_subject = EntityKeypair::generate();
let foreign_subject = EntityKeypair::generate();
let channel: ChannelHash = 0x1234_5678_9ABC_DEF0;
let token = PermissionToken::issue(
&issuer,
real_subject.entity_id().clone(),
TokenScope::PUBLISH,
channel,
3600,
0,
);
let cache = TokenCache::new();
cache
.tokens
.entry((*foreign_subject.entity_id().as_bytes(), channel))
.or_default()
.push(token);
assert!(cache
.check(foreign_subject.entity_id(), TokenScope::PUBLISH, channel)
.is_err());
}
#[test]
fn revocation_floor_is_monotonic() {
let issuer = EntityKeypair::generate();
let registry = RevocationRegistry::new();
registry.revoke_below(issuer.entity_id(), 5);
assert_eq!(registry.floor(issuer.entity_id()), 5);
registry.revoke_below(issuer.entity_id(), 2);
assert_eq!(registry.floor(issuer.entity_id()), 5);
registry.revoke_below(issuer.entity_id(), 10);
assert_eq!(registry.floor(issuer.entity_id()), 10);
}
#[test]
fn delegate_inherits_parent_issuer_generation() {
let issuer = EntityKeypair::generate();
let intermediate = EntityKeypair::generate();
let leaf = EntityKeypair::generate();
let mut parent = PermissionToken::issue(
&issuer,
intermediate.entity_id().clone(),
TokenScope::PUBLISH.union(TokenScope::DELEGATE),
0xCAFE_BABE,
3600,
2,
);
parent.issuer_generation = 7;
let payload = parent.signed_payload();
parent.signature = issuer.sign(&payload).to_bytes();
let child = parent
.delegate(&intermediate, leaf.entity_id().clone(), TokenScope::PUBLISH)
.expect("delegate should succeed");
assert_eq!(
child.issuer_generation, 7,
"child must inherit parent's issuer_generation"
);
}
#[test]
fn token_cache_check_distinguishes_u32_aliased_u64_hashes() {
let issuer = EntityKeypair::generate();
let subject = EntityKeypair::generate();
let cache = TokenCache::new();
let h_a: ChannelHash = 0x0000_0001_DEAD_BEEF;
let h_b: ChannelHash = 0xDEAD_BEEF_DEAD_BEEF;
assert_ne!(h_a, h_b);
assert_eq!(h_a as u32, h_b as u32, "test setup: low 32 must alias");
let token = PermissionToken::issue(
&issuer,
subject.entity_id().clone(),
TokenScope::PUBLISH,
h_a,
3600,
0,
);
cache.insert(token).expect("token should verify");
assert!(cache
.check(subject.entity_id(), TokenScope::PUBLISH, h_a)
.is_ok());
assert!(
cache
.check(subject.entity_id(), TokenScope::PUBLISH, h_b)
.is_err(),
"token bound to h_a must not authorize h_b that aliases on low 32 bits"
);
}
#[test]
fn test_token_cache_wildcard() {
let issuer = EntityKeypair::generate();
let subject = EntityKeypair::generate();
let cache = TokenCache::new();
let token = PermissionToken::issue(
&issuer,
subject.entity_id().clone(),
TokenScope::PUBLISH.union(TokenScope::WILDCARD),
0,
3600,
0,
);
let _ = cache.insert(token);
assert!(cache
.check(subject.entity_id(), TokenScope::PUBLISH, 0xABCD)
.is_ok());
assert!(cache
.check(subject.entity_id(), TokenScope::PUBLISH, 0x1234)
.is_ok());
}
#[test]
fn test_regression_wildcard_fallback_not_blocked_by_expired_channel_token() {
let issuer = EntityKeypair::generate();
let subject = EntityKeypair::generate();
let cache = TokenCache::new();
let mut expired_token = PermissionToken::issue(
&issuer,
subject.entity_id().clone(),
TokenScope::PUBLISH,
0xABCD,
1,
0,
);
expired_token.not_after = 0;
let payload = expired_token.signed_payload();
expired_token.signature = issuer.sign(&payload).to_bytes();
cache.insert_unchecked(expired_token);
let wildcard_token = PermissionToken::issue(
&issuer,
subject.entity_id().clone(),
TokenScope::PUBLISH.union(TokenScope::WILDCARD),
0,
3600,
0,
);
cache.insert_unchecked(wildcard_token);
assert!(
cache
.check(subject.entity_id(), TokenScope::PUBLISH, 0xABCD)
.is_ok(),
"wildcard fallback must not be blocked by expired channel-specific token"
);
}
#[test]
fn test_regression_delegate_rejects_expired_parent() {
let root = EntityKeypair::generate();
let node_a = EntityKeypair::generate();
let node_b = EntityKeypair::generate();
let mut token = PermissionToken::issue(
&root,
node_a.entity_id().clone(),
TokenScope::ALL,
0,
3600,
2,
);
token.not_after = 0;
let payload = token.signed_payload();
token.signature = root.sign(&payload).to_bytes();
let result = token.delegate(&node_a, node_b.entity_id().clone(), TokenScope::PUBLISH);
assert_eq!(
result.unwrap_err(),
TokenError::Expired,
"delegation from expired parent must be rejected"
);
}
#[test]
fn test_regression_insert_rejects_tampered_token() {
let issuer = EntityKeypair::generate();
let subject = EntityKeypair::generate();
let mut token = PermissionToken::issue(
&issuer,
subject.entity_id().clone(),
TokenScope::PUBLISH,
0,
3600,
0,
);
token.scope = TokenScope::ADMIN;
let cache = TokenCache::new();
assert!(
cache.insert(token).is_err(),
"insert must reject tampered token"
);
assert_eq!(cache.len(), 0, "tampered token must not be cached");
}
#[test]
fn cache_coexists_tokens_of_different_scopes_for_same_channel() {
let issuer = EntityKeypair::generate();
let subject = EntityKeypair::generate();
let channel = 0xABCD;
let publish_tok = PermissionToken::issue(
&issuer,
subject.entity_id().clone(),
TokenScope::PUBLISH,
channel,
3600,
0,
);
let subscribe_tok = PermissionToken::issue(
&issuer,
subject.entity_id().clone(),
TokenScope::SUBSCRIBE,
channel,
3600,
0,
);
let cache = TokenCache::new();
cache.insert(publish_tok).expect("insert publish");
cache.insert(subscribe_tok).expect("insert subscribe");
assert!(
cache
.check(subject.entity_id(), TokenScope::PUBLISH, channel)
.is_ok(),
"publish auth lost after subscribe insert",
);
assert!(
cache
.check(subject.entity_id(), TokenScope::SUBSCRIBE, channel)
.is_ok(),
"subscribe auth lost",
);
}
#[test]
fn cache_len_reports_total_tokens_not_slot_count() {
let issuer = EntityKeypair::generate();
let subject = EntityKeypair::generate();
let channel = 0xFEED;
let cache = TokenCache::new();
assert_eq!(cache.len(), 0);
cache
.insert(PermissionToken::issue(
&issuer,
subject.entity_id().clone(),
TokenScope::PUBLISH,
channel,
3600,
0,
))
.expect("insert publish");
cache
.insert(PermissionToken::issue(
&issuer,
subject.entity_id().clone(),
TokenScope::SUBSCRIBE,
channel,
3600,
0,
))
.expect("insert subscribe");
assert_eq!(
cache.len(),
2,
"len() must sum per-slot Vec lengths — two scopes in one slot means two tokens",
);
cache
.insert(PermissionToken::issue(
&issuer,
subject.entity_id().clone(),
TokenScope::PUBLISH,
0xBEEF,
3600,
0,
))
.expect("insert publish-other");
assert_eq!(
cache.len(),
3,
"len() after a second slot must reflect 3 tokens total, not 2 slots",
);
}
#[test]
fn cache_same_scope_reinsert_replaces_not_stacks() {
let issuer = EntityKeypair::generate();
let subject = EntityKeypair::generate();
let channel = 0xABCD;
let cache = TokenCache::new();
for _ in 0..10 {
let tok = PermissionToken::issue(
&issuer,
subject.entity_id().clone(),
TokenScope::SUBSCRIBE,
channel,
3600,
0,
);
cache.insert(tok).expect("insert");
}
assert_eq!(
cache.len(),
1,
"repeated inserts with the same scope must replace, not stack",
);
}
#[test]
fn from_bytes_rejects_trailing_garbage() {
let issuer = EntityKeypair::generate();
let subject = EntityKeypair::generate();
let tok = PermissionToken::issue(
&issuer,
subject.entity_id().clone(),
TokenScope::PUBLISH,
0,
3600,
0,
);
let mut bytes = tok.to_bytes();
assert_eq!(bytes.len(), PermissionToken::WIRE_SIZE);
assert!(PermissionToken::from_bytes(&bytes).is_ok());
bytes.push(0xFF);
assert!(
matches!(
PermissionToken::from_bytes(&bytes),
Err(TokenError::InvalidFormat)
),
"trailing byte must reject as InvalidFormat",
);
let truncated = &tok.to_bytes()[..PermissionToken::WIRE_SIZE - 1];
assert!(matches!(
PermissionToken::from_bytes(truncated),
Err(TokenError::InvalidFormat)
));
}
#[test]
fn issue_with_huge_ttl_saturates_rather_than_panics() {
let issuer = EntityKeypair::generate();
let subject = EntityKeypair::generate();
let tok = PermissionToken::issue(
&issuer,
subject.entity_id().clone(),
TokenScope::PUBLISH,
0,
u64::MAX,
0,
);
assert_eq!(
tok.not_after,
u64::MAX,
"TTL=u64::MAX must saturate, not wrap or panic",
);
assert!(!tok.is_expired());
assert!(tok.verify().is_ok());
}
#[test]
fn delegate_preserves_parent_not_after() {
let a = EntityKeypair::generate();
let b = EntityKeypair::generate();
let c = EntityKeypair::generate();
let parent = PermissionToken::issue(
&a,
b.entity_id().clone(),
TokenScope::PUBLISH.union(TokenScope::DELEGATE),
0,
3600,
2,
);
let child = parent
.delegate(&b, c.entity_id().clone(), TokenScope::PUBLISH)
.expect("delegate");
assert_eq!(
child.not_after, parent.not_after,
"child's not_after must equal parent's, not some smaller value \
derived from a second clock read",
);
assert!(child.not_before >= parent.not_before);
assert!(child.verify().is_ok());
}
#[test]
fn concurrent_insert_check_evict_is_panic_free() {
use std::sync::{Arc, Barrier};
use std::thread;
let cache = Arc::new(TokenCache::new());
let issuer = EntityKeypair::generate();
let subject_kp = EntityKeypair::generate();
let subject_id = subject_kp.entity_id().clone();
let channel_hash: ChannelHash = 0xABCD;
let iters = 500u32;
let start = Arc::new(Barrier::new(3));
let inserter = {
let cache = cache.clone();
let issuer = issuer.clone();
let subject_id = subject_id.clone();
let start = start.clone();
thread::spawn(move || {
start.wait();
for _ in 0..iters {
let token = PermissionToken::issue(
&issuer,
subject_id.clone(),
TokenScope::SUBSCRIBE,
channel_hash,
300,
0,
);
cache.insert_unchecked(token);
}
})
};
let checker = {
let cache = cache.clone();
let subject_id = subject_id.clone();
let start = start.clone();
thread::spawn(move || {
start.wait();
for _ in 0..iters {
let _ = cache.check(&subject_id, TokenScope::SUBSCRIBE, channel_hash);
}
})
};
let evictor = {
let cache = cache.clone();
let start = start.clone();
thread::spawn(move || {
start.wait();
for _ in 0..iters {
cache.evict_expired();
}
})
};
inserter.join().expect("inserter panicked");
checker.join().expect("checker panicked");
evictor.join().expect("evictor panicked");
assert!(
cache
.check(&subject_id, TokenScope::SUBSCRIBE, channel_hash)
.is_ok(),
"terminal check must succeed — the last insert's token is unexpired",
);
assert_eq!(
cache.len(),
1,
"exactly one token should remain (same-scope replace path); got {}",
cache.len(),
);
}
#[test]
fn evict_expired_races_with_check_without_panic() {
use std::sync::{Arc, Barrier};
use std::thread;
use std::time::Duration;
let cache = Arc::new(TokenCache::new());
let issuer = EntityKeypair::generate();
let subject_kp = EntityKeypair::generate();
let subject_id = subject_kp.entity_id().clone();
let channel_hash: ChannelHash = 0xBEEF;
let token = PermissionToken::issue(
&issuer,
subject_id.clone(),
TokenScope::PUBLISH,
channel_hash,
3, 0,
);
cache.insert_unchecked(token);
assert!(
cache
.check(&subject_id, TokenScope::PUBLISH, channel_hash)
.is_ok(),
"pre-expiry check should succeed",
);
let start = Arc::new(Barrier::new(2));
let checker = {
let cache = cache.clone();
let subject_id = subject_id.clone();
let start = start.clone();
thread::spawn(move || {
start.wait();
for _ in 0..2_000 {
let _ = cache.check(&subject_id, TokenScope::PUBLISH, channel_hash);
}
})
};
let evictor = {
let cache = cache.clone();
let start = start.clone();
thread::spawn(move || {
start.wait();
for _ in 0..2_000 {
cache.evict_expired();
}
})
};
thread::sleep(Duration::from_millis(3_500));
checker.join().expect("checker panicked");
evictor.join().expect("evictor panicked");
cache.evict_expired();
match cache.check(&subject_id, TokenScope::PUBLISH, channel_hash) {
Err(TokenError::NotAuthorized) => {}
other => panic!("expected NotAuthorized after TTL + evict; got {other:?}"),
}
}
fn issue_token_for(seed: u64, channel_hash: ChannelHash, scope: TokenScope) -> PermissionToken {
let issuer = EntityKeypair::generate();
let _ = seed;
let subject = EntityKeypair::generate();
PermissionToken::issue(
&issuer,
subject.entity_id().clone(),
scope,
channel_hash,
3600,
0,
)
}
#[test]
fn insert_unchecked_drops_novel_slot_when_at_max_token_slots() {
let issuer = EntityKeypair::generate();
let subject = EntityKeypair::generate();
let cache = TokenCache::new();
let template = PermissionToken::issue(
&issuer,
subject.entity_id().clone(),
TokenScope::PUBLISH,
0,
3600,
0,
);
for ch in 0u32..MAX_TOKEN_SLOTS as u32 {
let mut t = template.clone();
t.channel_hash = ch as ChannelHash;
cache.insert_unchecked(t);
}
let len_before_overflow = cache.tokens.len();
assert_eq!(
len_before_overflow, MAX_TOKEN_SLOTS,
"test setup: cache must be filled to capacity",
);
let other_subject = EntityKeypair::generate();
let novel = PermissionToken::issue(
&issuer,
other_subject.entity_id().clone(),
TokenScope::PUBLISH,
0,
3600,
0,
);
cache.insert_unchecked(novel);
assert_eq!(
cache.tokens.len(),
MAX_TOKEN_SLOTS,
"novel slot must be rejected at MAX_TOKEN_SLOTS cap",
);
}
#[test]
fn insert_unchecked_replays_existing_subject_when_slot_cap_is_full() {
let issuer = EntityKeypair::generate();
let subject = EntityKeypair::generate();
let cache = TokenCache::new();
let template = PermissionToken::issue(
&issuer,
subject.entity_id().clone(),
TokenScope::PUBLISH,
0,
3600,
0,
);
for ch in 0u32..MAX_TOKEN_SLOTS as u32 {
let mut t = template.clone();
t.channel_hash = ch as ChannelHash;
cache.insert_unchecked(t);
}
assert_eq!(cache.tokens.len(), MAX_TOKEN_SLOTS);
let mut refresh = template.clone();
refresh.channel_hash = 42;
refresh.nonce = 9999;
cache.insert_unchecked(refresh);
assert_eq!(cache.tokens.len(), MAX_TOKEN_SLOTS, "slot count unchanged");
let slot = cache
.tokens
.get(&(*subject.entity_id().as_bytes(), 42 as ChannelHash))
.unwrap();
assert_eq!(slot.value().len(), 1, "still one token in slot");
assert_eq!(slot.value()[0].nonce, 9999, "refresh replaced the token");
}
#[test]
fn insert_unchecked_caps_within_slot_token_count() {
let issuer = EntityKeypair::generate();
let subject = EntityKeypair::generate();
let cache = TokenCache::new();
let channel: ChannelHash = 0xCAFE;
let template = PermissionToken::issue(
&issuer,
subject.entity_id().clone(),
TokenScope::PUBLISH,
channel,
3600,
0,
);
for i in 0..MAX_TOKENS_PER_SLOT as u32 {
let mut t = template.clone();
t.scope = TokenScope::from_bits(0x10_0000 | (i << 8));
cache.insert_unchecked(t);
}
let slot_before = cache
.tokens
.get(&(*subject.entity_id().as_bytes(), channel))
.unwrap();
assert_eq!(
slot_before.value().len(),
MAX_TOKENS_PER_SLOT,
"test setup: slot must be packed to within-slot cap",
);
drop(slot_before);
let novel_scope_bits = 0x20_0000u32;
let mut over = template.clone();
over.scope = TokenScope::from_bits(novel_scope_bits);
cache.insert_unchecked(over);
let slot_after = cache
.tokens
.get(&(*subject.entity_id().as_bytes(), channel))
.unwrap();
assert_eq!(
slot_after.value().len(),
MAX_TOKENS_PER_SLOT,
"novel scope must be rejected at MAX_TOKENS_PER_SLOT",
);
assert!(
slot_after
.value()
.iter()
.all(|t| t.scope.bits() != novel_scope_bits),
"the dropped scope must not be present in the slot",
);
let _ = issue_token_for; drop(slot_after);
let mut refresh = template.clone();
refresh.scope = TokenScope::from_bits(0x10_0000);
refresh.nonce = 1111;
cache.insert_unchecked(refresh);
let slot_after_refresh = cache
.tokens
.get(&(*subject.entity_id().as_bytes(), channel))
.unwrap();
let refreshed = slot_after_refresh
.value()
.iter()
.find(|t| t.scope.bits() == 0x10_0000)
.expect("scope 0x10_0000 must still be present");
assert_eq!(
refreshed.nonce, 1111,
"refresh-of-existing-scope must succeed at cap"
);
}
#[test]
fn insert_unchecked_does_not_overshoot_under_concurrent_novel_inserts() {
use std::sync::Arc;
use std::thread;
const SLACK: usize = 4;
const THREADS: usize = 32;
let issuer = EntityKeypair::generate();
let subject = EntityKeypair::generate();
let cache = Arc::new(TokenCache::new());
let template = PermissionToken::issue(
&issuer,
subject.entity_id().clone(),
TokenScope::PUBLISH,
0,
3600,
0,
);
let prefill = MAX_TOKEN_SLOTS - SLACK;
for ch in 0u32..prefill as u32 {
let mut t = template.clone();
t.channel_hash = ch as ChannelHash;
cache.tokens.insert(
(*subject.entity_id().as_bytes(), ch as ChannelHash),
vec![t],
);
}
assert_eq!(cache.tokens.len(), prefill);
let barrier = Arc::new(std::sync::Barrier::new(THREADS));
let mut handles = Vec::with_capacity(THREADS);
for tid in 0..THREADS {
let cache = Arc::clone(&cache);
let mut novel = template.clone();
let mut subj_bytes = *subject.entity_id().as_bytes();
subj_bytes[0] ^= (tid as u8).wrapping_add(1);
subj_bytes[1] ^= ((tid >> 8) as u8).wrapping_add(1);
novel.subject = EntityId::from_bytes(subj_bytes);
novel.channel_hash = (prefill + tid) as ChannelHash;
let barrier = Arc::clone(&barrier);
handles.push(thread::spawn(move || {
barrier.wait();
cache.insert_unchecked(novel);
}));
}
for h in handles {
h.join().unwrap();
}
let final_len = cache.tokens.len();
assert!(
final_len <= MAX_TOKEN_SLOTS,
"cache overshot cap under concurrent novel inserts: {final_len} > {MAX_TOKEN_SLOTS}",
);
assert!(
final_len >= prefill,
"prefill leaked: {final_len} < {prefill}",
);
}
#[test]
fn try_issue_returns_read_only_on_public_only_keypair() {
let full = EntityKeypair::generate();
let public_only = EntityKeypair::public_only(full.entity_id().clone());
assert!(public_only.try_sign(b"x").is_err());
let subject = EntityKeypair::generate();
let result = PermissionToken::try_issue(
&public_only,
subject.entity_id().clone(),
TokenScope::PUBLISH,
0,
3600,
0,
);
assert!(
matches!(result, Err(TokenError::ReadOnly)),
"try_issue must surface public-only keypair as ReadOnly, got {:?}",
result.map(|_| "Ok"),
);
}
#[test]
fn delegate_returns_read_only_on_public_only_signer() {
let issuer = EntityKeypair::generate();
let subject_full = EntityKeypair::generate();
let target = EntityKeypair::generate();
let parent = PermissionToken::issue(
&issuer,
subject_full.entity_id().clone(),
TokenScope::PUBLISH.union(TokenScope::DELEGATE),
0xCAFE,
3600,
3,
);
let subject_pub = EntityKeypair::public_only(subject_full.entity_id().clone());
let result = parent.delegate(
&subject_pub,
target.entity_id().clone(),
TokenScope::PUBLISH,
);
assert!(
matches!(result, Err(TokenError::ReadOnly)),
"delegate must surface public-only signer as ReadOnly, got {:?}",
result.map(|_| "Ok"),
);
}
#[test]
fn try_issue_succeeds_with_full_keypair() {
let issuer = EntityKeypair::generate();
let subject = EntityKeypair::generate();
let token = PermissionToken::try_issue(
&issuer,
subject.entity_id().clone(),
TokenScope::PUBLISH,
0,
3600,
0,
)
.expect("try_issue must succeed with a full keypair");
assert!(token.verify().is_ok());
}
}