use blake2::{
digest::{consts::U32, Mac},
Blake2sMac,
};
use dashmap::DashMap;
use ed25519_dalek::Signature;
use super::id::{TopologySubnetId, MAX_DEPTH};
use crate::adapter::net::identity::{EntityId, EntityKeypair};
pub const SUBNET_GRANT_SIG_DOMAIN: &[u8] = b"net.subnet.grant.v1";
pub const SUBNET_ISSUER_GRANT_SIG_DOMAIN: &[u8] = b"net.subnet.issuer-grant.v1";
pub const SUBNET_FLOOR_SIG_DOMAIN: &[u8] = b"net.subnet.floor.v1";
const SUBNET_CREDSET_HASH_DOMAIN: &[u8] = b"net.subnet.credset.v1";
pub const MAX_SUBNET_GRANT_LIFETIME_SECS: u64 = 30 * 24 * 60 * 60;
pub use crate::adapter::net::identity::MAX_TOKEN_CLOCK_SKEW_SECS;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct SubnetRights(u8);
impl SubnetRights {
pub const ATTACH: Self = Self(1 << 0);
pub const ROUTE: Self = Self(1 << 1);
pub const EXPORT: Self = Self(1 << 2);
pub const ALL: Self = Self(0b0111);
const KNOWN_MASK: u8 = 0b0111;
pub fn try_from_bits(bits: u8) -> Result<Self, SubnetAuthError> {
if bits == 0 || bits & !Self::KNOWN_MASK != 0 {
return Err(SubnetAuthError::InvalidRights);
}
Ok(Self(bits))
}
#[inline]
pub const fn bits(self) -> u8 {
self.0
}
#[inline]
pub const fn contains(self, other: Self) -> bool {
self.0 & other.0 == other.0
}
#[inline]
pub const fn union(self, other: Self) -> Self {
Self(self.0 | other.0)
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct SubnetRef {
pub authority: EntityId,
pub path: TopologySubnetId,
}
impl SubnetRef {
#[inline]
pub fn contains(&self, target: &SubnetRef) -> bool {
self.authority == target.authority && self.path.is_ancestor_or_self_of(target.path)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SubnetAuthError {
UnknownAuthority,
WrongSubject,
WrongAuthority,
WrongTopologyEpoch,
ScopeNotAncestor,
InvalidRights,
Expired,
NotYetValid,
LifetimeTooWide,
Revoked,
IssuerNotAuthorized,
IssuerAttenuationBroadened,
RightNotGranted,
WrongSession,
WrongVerifier,
WrongChallenge,
IdentityPinConflict,
TooManyGatewayContexts,
MixedGatewayEpochs,
InvalidSignature,
InvalidFormat,
InvalidValidityWindow,
ClockSkewTooLarge,
}
impl SubnetAuthError {
pub const ALL: &'static [SubnetAuthError] = &[
Self::UnknownAuthority,
Self::WrongSubject,
Self::WrongAuthority,
Self::WrongTopologyEpoch,
Self::ScopeNotAncestor,
Self::InvalidRights,
Self::Expired,
Self::NotYetValid,
Self::LifetimeTooWide,
Self::Revoked,
Self::IssuerNotAuthorized,
Self::IssuerAttenuationBroadened,
Self::RightNotGranted,
Self::WrongSession,
Self::WrongVerifier,
Self::WrongChallenge,
Self::IdentityPinConflict,
Self::TooManyGatewayContexts,
Self::MixedGatewayEpochs,
Self::InvalidSignature,
Self::InvalidFormat,
Self::InvalidValidityWindow,
Self::ClockSkewTooLarge,
];
pub const fn wire_kind(&self) -> &'static str {
match self {
Self::UnknownAuthority => "unknown_authority",
Self::WrongSubject => "wrong_subject",
Self::WrongAuthority => "wrong_authority",
Self::WrongTopologyEpoch => "wrong_topology_epoch",
Self::ScopeNotAncestor => "scope_not_ancestor",
Self::InvalidRights => "invalid_rights",
Self::Expired => "expired",
Self::NotYetValid => "not_yet_valid",
Self::LifetimeTooWide => "lifetime_too_wide",
Self::Revoked => "revoked",
Self::IssuerNotAuthorized => "issuer_not_authorized",
Self::IssuerAttenuationBroadened => "issuer_attenuation_broadened",
Self::RightNotGranted => "right_not_granted",
Self::WrongSession => "wrong_session",
Self::WrongVerifier => "wrong_verifier",
Self::WrongChallenge => "wrong_challenge",
Self::IdentityPinConflict => "identity_pin_conflict",
Self::TooManyGatewayContexts => "too_many_gateway_contexts",
Self::MixedGatewayEpochs => "mixed_gateway_epochs",
Self::InvalidSignature => "invalid_signature",
Self::InvalidFormat => "invalid_format",
Self::InvalidValidityWindow => "invalid_validity_window",
Self::ClockSkewTooLarge => "clock_skew_too_large",
}
}
}
impl std::fmt::Display for SubnetAuthError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(self.wire_kind())
}
}
impl std::error::Error for SubnetAuthError {}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SubnetGrant {
pub version: u8,
pub authority: EntityId,
pub scope: TopologySubnetId,
pub topology_epoch: u32,
pub issuer: EntityId,
pub subject: EntityId,
pub rights: SubnetRights,
pub generation: u32,
pub not_before: u64,
pub not_after: u64,
pub nonce: u64,
pub signature: [u8; 64],
}
impl SubnetGrant {
pub const SIGNED_PAYLOAD_SIZE: usize = 134;
pub const WIRE_SIZE: usize = Self::SIGNED_PAYLOAD_SIZE + 64;
const SIGNING_INPUT_SIZE: usize = SUBNET_GRANT_SIG_DOMAIN.len() + Self::SIGNED_PAYLOAD_SIZE;
#[expect(
clippy::too_many_arguments,
reason = "explicit wire fields; a params struct would only rename them"
)]
pub fn try_issue(
issuer_keypair: &EntityKeypair,
authority: EntityId,
scope: TopologySubnetId,
topology_epoch: u32,
subject: EntityId,
rights: SubnetRights,
generation: u32,
not_before: u64,
duration_secs: u64,
) -> Result<Self, SubnetAuthError> {
if duration_secs == 0 {
return Err(SubnetAuthError::InvalidValidityWindow);
}
if duration_secs > MAX_SUBNET_GRANT_LIFETIME_SECS {
return Err(SubnetAuthError::LifetimeTooWide);
}
let mut nonce_bytes = [0u8; 8];
if let Err(e) = getrandom::fill(&mut nonce_bytes) {
eprintln!(
"FATAL: SubnetGrant nonce getrandom failure ({e:?}); aborting to avoid predictable nonce"
);
std::process::abort();
}
let mut grant = Self {
version: 1,
authority,
scope,
topology_epoch,
issuer: issuer_keypair.entity_id().clone(),
subject,
rights,
generation,
not_before,
not_after: not_before.saturating_add(duration_secs),
nonce: u64::from_le_bytes(nonce_bytes),
signature: [0u8; 64],
};
let sig = issuer_keypair
.try_sign(&grant.signing_input())
.map_err(|_| SubnetAuthError::InvalidSignature)?;
grant.signature = sig.to_bytes();
Ok(grant)
}
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] = self.version;
off += 1;
buf[off..off + 32].copy_from_slice(self.authority.as_bytes());
off += 32;
buf[off..off + 4].copy_from_slice(&self.scope.raw().to_le_bytes());
off += 4;
buf[off..off + 4].copy_from_slice(&self.topology_epoch.to_le_bytes());
off += 4;
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] = self.rights.bits();
off += 1;
buf[off..off + 4].copy_from_slice(&self.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..off + 8].copy_from_slice(&self.nonce.to_le_bytes());
buf
}
fn signing_input(&self) -> [u8; Self::SIGNING_INPUT_SIZE] {
let mut buf = [0u8; Self::SIGNING_INPUT_SIZE];
buf[..SUBNET_GRANT_SIG_DOMAIN.len()].copy_from_slice(SUBNET_GRANT_SIG_DOMAIN);
buf[SUBNET_GRANT_SIG_DOMAIN.len()..].copy_from_slice(&self.signed_payload());
buf
}
pub fn to_bytes(&self) -> Vec<u8> {
let mut out = Vec::with_capacity(Self::WIRE_SIZE);
out.extend_from_slice(&self.signed_payload());
out.extend_from_slice(&self.signature);
out
}
pub fn from_bytes(bytes: &[u8]) -> Result<Self, SubnetAuthError> {
if bytes.len() != Self::WIRE_SIZE {
return Err(SubnetAuthError::InvalidFormat);
}
let mut off = 0;
let version = bytes[off];
off += 1;
if version != 1 {
return Err(SubnetAuthError::InvalidFormat);
}
let authority = EntityId::from_bytes(read_32(bytes, &mut off));
let scope = TopologySubnetId::from_raw(read_u32(bytes, &mut off));
let topology_epoch = read_u32(bytes, &mut off);
let issuer = EntityId::from_bytes(read_32(bytes, &mut off));
let subject = EntityId::from_bytes(read_32(bytes, &mut off));
let rights = SubnetRights::try_from_bits(bytes[off])?;
off += 1;
let generation = read_u32(bytes, &mut off);
let not_before = read_u64(bytes, &mut off);
let not_after = read_u64(bytes, &mut off);
let nonce = read_u64(bytes, &mut off);
let mut signature = [0u8; 64];
signature.copy_from_slice(&bytes[off..off + 64]);
let grant = Self {
version,
authority,
scope,
topology_epoch,
issuer,
subject,
rights,
generation,
not_before,
not_after,
nonce,
signature,
};
if grant.not_after <= grant.not_before {
return Err(SubnetAuthError::InvalidValidityWindow);
}
Ok(grant)
}
pub fn verify(&self) -> Result<(), SubnetAuthError> {
if self.not_after <= self.not_before {
return Err(SubnetAuthError::InvalidValidityWindow);
}
if self.not_after - self.not_before > MAX_SUBNET_GRANT_LIFETIME_SECS {
return Err(SubnetAuthError::LifetimeTooWide);
}
let sig = Signature::from_bytes(&self.signature);
self.issuer
.verify(&self.signing_input(), &sig)
.map_err(|_| SubnetAuthError::InvalidSignature)
}
pub fn check_time_bounds_at(&self, now: u64, skew_secs: u64) -> Result<(), SubnetAuthError> {
if skew_secs > MAX_TOKEN_CLOCK_SKEW_SECS {
return Err(SubnetAuthError::ClockSkewTooLarge);
}
if now < self.not_before.saturating_sub(skew_secs) {
return Err(SubnetAuthError::NotYetValid);
}
if now >= self.not_after.saturating_add(skew_secs) {
return Err(SubnetAuthError::Expired);
}
Ok(())
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SubnetIssuerGrant {
pub version: u8,
pub authority: EntityId,
pub scope: TopologySubnetId,
pub topology_epoch: u32,
pub issuer: EntityId,
pub maximum_rights: SubnetRights,
pub generation: u32,
pub not_before: u64,
pub not_after: u64,
pub signature: [u8; 64],
}
impl SubnetIssuerGrant {
pub const SIGNED_PAYLOAD_SIZE: usize = 94;
pub const WIRE_SIZE: usize = Self::SIGNED_PAYLOAD_SIZE + 64;
const SIGNING_INPUT_SIZE: usize =
SUBNET_ISSUER_GRANT_SIG_DOMAIN.len() + Self::SIGNED_PAYLOAD_SIZE;
#[expect(
clippy::too_many_arguments,
reason = "explicit wire fields; a params struct would only rename them"
)]
pub fn try_issue(
root_keypair: &EntityKeypair,
authority: EntityId,
scope: TopologySubnetId,
topology_epoch: u32,
issuer: EntityId,
maximum_rights: SubnetRights,
generation: u32,
not_before: u64,
duration_secs: u64,
) -> Result<Self, SubnetAuthError> {
if duration_secs == 0 {
return Err(SubnetAuthError::InvalidValidityWindow);
}
if duration_secs > MAX_SUBNET_GRANT_LIFETIME_SECS {
return Err(SubnetAuthError::LifetimeTooWide);
}
let mut grant = Self {
version: 1,
authority,
scope,
topology_epoch,
issuer,
maximum_rights,
generation,
not_before,
not_after: not_before.saturating_add(duration_secs),
signature: [0u8; 64],
};
let sig = root_keypair
.try_sign(&grant.signing_input())
.map_err(|_| SubnetAuthError::InvalidSignature)?;
grant.signature = sig.to_bytes();
Ok(grant)
}
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] = self.version;
off += 1;
buf[off..off + 32].copy_from_slice(self.authority.as_bytes());
off += 32;
buf[off..off + 4].copy_from_slice(&self.scope.raw().to_le_bytes());
off += 4;
buf[off..off + 4].copy_from_slice(&self.topology_epoch.to_le_bytes());
off += 4;
buf[off..off + 32].copy_from_slice(self.issuer.as_bytes());
off += 32;
buf[off] = self.maximum_rights.bits();
off += 1;
buf[off..off + 4].copy_from_slice(&self.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());
buf
}
fn signing_input(&self) -> [u8; Self::SIGNING_INPUT_SIZE] {
let mut buf = [0u8; Self::SIGNING_INPUT_SIZE];
buf[..SUBNET_ISSUER_GRANT_SIG_DOMAIN.len()].copy_from_slice(SUBNET_ISSUER_GRANT_SIG_DOMAIN);
buf[SUBNET_ISSUER_GRANT_SIG_DOMAIN.len()..].copy_from_slice(&self.signed_payload());
buf
}
pub fn to_bytes(&self) -> Vec<u8> {
let mut out = Vec::with_capacity(Self::WIRE_SIZE);
out.extend_from_slice(&self.signed_payload());
out.extend_from_slice(&self.signature);
out
}
pub fn from_bytes(bytes: &[u8]) -> Result<Self, SubnetAuthError> {
if bytes.len() != Self::WIRE_SIZE {
return Err(SubnetAuthError::InvalidFormat);
}
let mut off = 0;
let version = bytes[off];
off += 1;
if version != 1 {
return Err(SubnetAuthError::InvalidFormat);
}
let authority = EntityId::from_bytes(read_32(bytes, &mut off));
let scope = TopologySubnetId::from_raw(read_u32(bytes, &mut off));
let topology_epoch = read_u32(bytes, &mut off);
let issuer = EntityId::from_bytes(read_32(bytes, &mut off));
let maximum_rights = SubnetRights::try_from_bits(bytes[off])?;
off += 1;
let generation = read_u32(bytes, &mut off);
let not_before = read_u64(bytes, &mut off);
let not_after = read_u64(bytes, &mut off);
let mut signature = [0u8; 64];
signature.copy_from_slice(&bytes[off..off + 64]);
let grant = Self {
version,
authority,
scope,
topology_epoch,
issuer,
maximum_rights,
generation,
not_before,
not_after,
signature,
};
if grant.not_after <= grant.not_before {
return Err(SubnetAuthError::InvalidValidityWindow);
}
Ok(grant)
}
pub fn verify_signed_by(&self, root: &EntityId) -> Result<(), SubnetAuthError> {
if self.not_after <= self.not_before {
return Err(SubnetAuthError::InvalidValidityWindow);
}
if self.not_after - self.not_before > MAX_SUBNET_GRANT_LIFETIME_SECS {
return Err(SubnetAuthError::LifetimeTooWide);
}
let sig = Signature::from_bytes(&self.signature);
root.verify(&self.signing_input(), &sig)
.map_err(|_| SubnetAuthError::InvalidSignature)
}
pub fn check_time_bounds_at(&self, now: u64, skew_secs: u64) -> Result<(), SubnetAuthError> {
if skew_secs > MAX_TOKEN_CLOCK_SKEW_SECS {
return Err(SubnetAuthError::ClockSkewTooLarge);
}
if now < self.not_before.saturating_sub(skew_secs) {
return Err(SubnetAuthError::NotYetValid);
}
if now >= self.not_after.saturating_add(skew_secs) {
return Err(SubnetAuthError::Expired);
}
Ok(())
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SubnetRevocationFloor {
pub version: u8,
pub scope: SubnetRef,
pub topology_epoch: u32,
pub issuer: EntityId,
pub minimum_generation: u32,
pub revision: u64,
pub issued_at: u64,
pub signature: [u8; 64],
}
impl SubnetRevocationFloor {
pub const SIGNED_PAYLOAD_SIZE: usize = 93;
pub const WIRE_SIZE: usize = Self::SIGNED_PAYLOAD_SIZE + 64;
const SIGNING_INPUT_SIZE: usize = SUBNET_FLOOR_SIG_DOMAIN.len() + Self::SIGNED_PAYLOAD_SIZE;
pub fn try_issue(
root_keypair: &EntityKeypair,
scope: SubnetRef,
topology_epoch: u32,
minimum_generation: u32,
revision: u64,
issued_at: u64,
) -> Result<Self, SubnetAuthError> {
let mut floor = Self {
version: 1,
scope,
topology_epoch,
issuer: root_keypair.entity_id().clone(),
minimum_generation,
revision,
issued_at,
signature: [0u8; 64],
};
let sig = root_keypair
.try_sign(&floor.signing_input())
.map_err(|_| SubnetAuthError::InvalidSignature)?;
floor.signature = sig.to_bytes();
Ok(floor)
}
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] = self.version;
off += 1;
buf[off..off + 32].copy_from_slice(self.scope.authority.as_bytes());
off += 32;
buf[off..off + 4].copy_from_slice(&self.scope.path.raw().to_le_bytes());
off += 4;
buf[off..off + 4].copy_from_slice(&self.topology_epoch.to_le_bytes());
off += 4;
buf[off..off + 32].copy_from_slice(self.issuer.as_bytes());
off += 32;
buf[off..off + 4].copy_from_slice(&self.minimum_generation.to_le_bytes());
off += 4;
buf[off..off + 8].copy_from_slice(&self.revision.to_le_bytes());
off += 8;
buf[off..off + 8].copy_from_slice(&self.issued_at.to_le_bytes());
buf
}
fn signing_input(&self) -> [u8; Self::SIGNING_INPUT_SIZE] {
let mut buf = [0u8; Self::SIGNING_INPUT_SIZE];
buf[..SUBNET_FLOOR_SIG_DOMAIN.len()].copy_from_slice(SUBNET_FLOOR_SIG_DOMAIN);
buf[SUBNET_FLOOR_SIG_DOMAIN.len()..].copy_from_slice(&self.signed_payload());
buf
}
pub fn to_bytes(&self) -> Vec<u8> {
let mut out = Vec::with_capacity(Self::WIRE_SIZE);
out.extend_from_slice(&self.signed_payload());
out.extend_from_slice(&self.signature);
out
}
pub fn from_bytes(bytes: &[u8]) -> Result<Self, SubnetAuthError> {
if bytes.len() != Self::WIRE_SIZE {
return Err(SubnetAuthError::InvalidFormat);
}
let mut off = 0;
let version = bytes[off];
off += 1;
if version != 1 {
return Err(SubnetAuthError::InvalidFormat);
}
let authority = EntityId::from_bytes(read_32(bytes, &mut off));
let path = TopologySubnetId::from_raw(read_u32(bytes, &mut off));
let topology_epoch = read_u32(bytes, &mut off);
let issuer = EntityId::from_bytes(read_32(bytes, &mut off));
let minimum_generation = read_u32(bytes, &mut off);
let revision = read_u64(bytes, &mut off);
let issued_at = read_u64(bytes, &mut off);
let mut signature = [0u8; 64];
signature.copy_from_slice(&bytes[off..off + 64]);
Ok(Self {
version,
scope: SubnetRef { authority, path },
topology_epoch,
issuer,
minimum_generation,
revision,
issued_at,
signature,
})
}
pub fn verify(&self) -> Result<(), SubnetAuthError> {
let sig = Signature::from_bytes(&self.signature);
self.issuer
.verify(&self.signing_input(), &sig)
.map_err(|_| SubnetAuthError::InvalidSignature)
}
}
#[derive(Debug, Default)]
pub struct SubnetFloorRegistry {
floors: DashMap<([u8; 32], u32, u32), FloorEntry>,
auth_epochs: DashMap<[u8; 32], u64>,
}
#[derive(Debug, Clone, Copy)]
struct FloorEntry {
minimum_generation: u32,
revision: u64,
}
impl SubnetFloorRegistry {
pub fn new() -> Self {
Self::default()
}
pub fn apply(
&self,
floor: &SubnetRevocationFloor,
config: &SubnetAuthorityConfig,
) -> Result<bool, SubnetAuthError> {
if floor.scope.authority != config.authority {
return Err(SubnetAuthError::WrongAuthority);
}
if config.roots.is_empty() {
return Err(SubnetAuthError::UnknownAuthority);
}
if !config.roots.contains(&floor.issuer) {
return Err(SubnetAuthError::IssuerNotAuthorized);
}
floor.verify()?;
let key = (
*floor.scope.authority.as_bytes(),
floor.topology_epoch,
floor.scope.path.raw(),
);
let mut changed = false;
self.floors
.entry(key)
.and_modify(|e| {
if floor.revision > e.revision && floor.minimum_generation > e.minimum_generation {
e.revision = floor.revision;
e.minimum_generation = floor.minimum_generation;
changed = true;
}
})
.or_insert_with(|| {
changed = floor.minimum_generation > 0;
FloorEntry {
minimum_generation: floor.minimum_generation,
revision: floor.revision,
}
});
if changed {
self.auth_epochs
.entry(*floor.scope.authority.as_bytes())
.and_modify(|e| *e += 1)
.or_insert(1);
}
Ok(changed)
}
pub fn max_floor(
&self,
authority: &EntityId,
topology_epoch: u32,
scope_path: TopologySubnetId,
) -> u32 {
let auth = *authority.as_bytes();
let mut max = 0u32;
let mut cursor = scope_path;
loop {
if let Some(e) = self.floors.get(&(auth, topology_epoch, cursor.raw())) {
max = max.max(e.minimum_generation);
}
if cursor.is_global() {
break;
}
cursor = cursor.parent();
}
max
}
pub fn auth_epoch(&self, authority: &EntityId) -> u64 {
self.auth_epochs
.get(authority.as_bytes())
.map(|e| *e)
.unwrap_or(0)
}
}
#[derive(Debug, Clone)]
pub struct SubnetAuthorityConfig {
pub authority: EntityId,
pub roots: Vec<EntityId>,
pub maximum_grant_lifetime_secs: u64,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum SubnetCredentialSet {
Direct(SubnetGrant),
OneHop {
issuer_grant: SubnetIssuerGrant,
leaf: SubnetGrant,
},
}
impl SubnetCredentialSet {
const TAG_DIRECT: u8 = 1;
const TAG_ONE_HOP: u8 = 2;
pub fn leaf(&self) -> &SubnetGrant {
match self {
Self::Direct(leaf) => leaf,
Self::OneHop { leaf, .. } => leaf,
}
}
pub fn to_bytes(&self) -> Vec<u8> {
match self {
Self::Direct(leaf) => {
let mut out = Vec::with_capacity(1 + SubnetGrant::WIRE_SIZE);
out.push(Self::TAG_DIRECT);
out.extend_from_slice(&leaf.to_bytes());
out
}
Self::OneHop { issuer_grant, leaf } => {
let mut out =
Vec::with_capacity(1 + SubnetIssuerGrant::WIRE_SIZE + SubnetGrant::WIRE_SIZE);
out.push(Self::TAG_ONE_HOP);
out.extend_from_slice(&issuer_grant.to_bytes());
out.extend_from_slice(&leaf.to_bytes());
out
}
}
}
pub fn from_bytes(bytes: &[u8]) -> Result<Self, SubnetAuthError> {
let (&tag, rest) = bytes.split_first().ok_or(SubnetAuthError::InvalidFormat)?;
match tag {
Self::TAG_DIRECT => Ok(Self::Direct(SubnetGrant::from_bytes(rest)?)),
Self::TAG_ONE_HOP => {
if rest.len() != SubnetIssuerGrant::WIRE_SIZE + SubnetGrant::WIRE_SIZE {
return Err(SubnetAuthError::InvalidFormat);
}
let (ig, leaf) = rest.split_at(SubnetIssuerGrant::WIRE_SIZE);
Ok(Self::OneHop {
issuer_grant: SubnetIssuerGrant::from_bytes(ig)?,
leaf: SubnetGrant::from_bytes(leaf)?,
})
}
_ => Err(SubnetAuthError::InvalidFormat),
}
}
#[expect(
clippy::expect_used,
reason = "Blake2sMac::new_from_slice rejects only keys longer than 32 bytes; the domain label is a short compile-time constant"
)]
pub fn credential_set_hash(&self) -> [u8; 32] {
let mut mac = <Blake2sMac<U32> as Mac>::new_from_slice(SUBNET_CREDSET_HASH_DOMAIN)
.expect("BLAKE2s accepts variable-length keys");
Mac::update(&mut mac, &self.to_bytes());
let mut out = [0u8; 32];
out.copy_from_slice(&mac.finalize().into_bytes());
out
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct VerifiedSubnetAuthority {
pub authority: EntityId,
pub scope: TopologySubnetId,
pub topology_epoch: u32,
pub subject: EntityId,
pub rights: SubnetRights,
pub generation: u32,
pub expires_at: u64,
pub subnet_auth_epoch: u64,
pub credential_set_hash: [u8; 32],
}
pub fn verify_credential_set(
set: &SubnetCredentialSet,
expected_subject: &EntityId,
config: &SubnetAuthorityConfig,
current_topology_epoch: u32,
floors: &SubnetFloorRegistry,
now_secs: u64,
skew_secs: u64,
) -> Result<VerifiedSubnetAuthority, SubnetAuthError> {
let leaf = set.leaf();
if leaf.authority != config.authority {
return Err(SubnetAuthError::WrongAuthority);
}
if config.roots.is_empty() {
return Err(SubnetAuthError::UnknownAuthority);
}
if &leaf.subject != expected_subject {
return Err(SubnetAuthError::WrongSubject);
}
if leaf.topology_epoch != current_topology_epoch {
return Err(SubnetAuthError::WrongTopologyEpoch);
}
match set {
SubnetCredentialSet::Direct(leaf) => {
if !config.roots.contains(&leaf.issuer) {
return Err(SubnetAuthError::IssuerNotAuthorized);
}
leaf.verify()?;
}
SubnetCredentialSet::OneHop { issuer_grant, leaf } => {
if issuer_grant.authority != config.authority {
return Err(SubnetAuthError::WrongAuthority);
}
if issuer_grant.topology_epoch != current_topology_epoch {
return Err(SubnetAuthError::WrongTopologyEpoch);
}
if !config
.roots
.iter()
.any(|root| issuer_grant.verify_signed_by(root).is_ok())
{
return Err(SubnetAuthError::IssuerNotAuthorized);
}
if leaf.issuer != issuer_grant.issuer {
return Err(SubnetAuthError::IssuerNotAuthorized);
}
leaf.verify()?;
if !issuer_grant.scope.is_ancestor_or_self_of(leaf.scope) {
return Err(SubnetAuthError::ScopeNotAncestor);
}
if !issuer_grant.maximum_rights.contains(leaf.rights) {
return Err(SubnetAuthError::IssuerAttenuationBroadened);
}
if leaf.not_before < issuer_grant.not_before || leaf.not_after > issuer_grant.not_after
{
return Err(SubnetAuthError::IssuerAttenuationBroadened);
}
issuer_grant.check_time_bounds_at(now_secs, skew_secs)?;
let issuer_floor = floors.max_floor(
&config.authority,
current_topology_epoch,
issuer_grant.scope,
);
if issuer_grant.generation < issuer_floor {
return Err(SubnetAuthError::Revoked);
}
}
}
leaf.check_time_bounds_at(now_secs, skew_secs)?;
if leaf.not_after - leaf.not_before > config.maximum_grant_lifetime_secs {
return Err(SubnetAuthError::LifetimeTooWide);
}
let floor = floors.max_floor(&config.authority, current_topology_epoch, leaf.scope);
if leaf.generation < floor {
return Err(SubnetAuthError::Revoked);
}
Ok(VerifiedSubnetAuthority {
authority: leaf.authority.clone(),
scope: leaf.scope,
topology_epoch: leaf.topology_epoch,
subject: leaf.subject.clone(),
rights: leaf.rights,
generation: leaf.generation,
expires_at: leaf.not_after,
subnet_auth_epoch: floors.auth_epoch(&config.authority),
credential_set_hash: set.credential_set_hash(),
})
}
pub const SUBNET_PRESENTATION_SIG_DOMAIN: &[u8] = b"net.subnet.presentation.v1";
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SubnetAuthPresentation {
pub version: u8,
pub subject: EntityId,
pub credential_set_hash: [u8; 32],
pub session_id: u64,
pub verifier: EntityId,
pub verifier_nonce: [u8; 32],
pub target: SubnetRef,
pub requested_rights: SubnetRights,
pub signature: [u8; 64],
}
impl SubnetAuthPresentation {
pub const SIGNED_PAYLOAD_SIZE: usize = 174;
pub const WIRE_SIZE: usize = Self::SIGNED_PAYLOAD_SIZE + 64;
const SIGNING_INPUT_SIZE: usize =
SUBNET_PRESENTATION_SIG_DOMAIN.len() + Self::SIGNED_PAYLOAD_SIZE;
pub fn try_issue(
subject_keypair: &EntityKeypair,
credential_set_hash: [u8; 32],
session_id: u64,
verifier: EntityId,
verifier_nonce: [u8; 32],
target: SubnetRef,
requested_rights: SubnetRights,
) -> Result<Self, SubnetAuthError> {
let mut p = Self {
version: 1,
subject: subject_keypair.entity_id().clone(),
credential_set_hash,
session_id,
verifier,
verifier_nonce,
target,
requested_rights,
signature: [0u8; 64],
};
let sig = subject_keypair
.try_sign(&p.signing_input())
.map_err(|_| SubnetAuthError::InvalidSignature)?;
p.signature = sig.to_bytes();
Ok(p)
}
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] = self.version;
off += 1;
buf[off..off + 32].copy_from_slice(self.subject.as_bytes());
off += 32;
buf[off..off + 32].copy_from_slice(&self.credential_set_hash);
off += 32;
buf[off..off + 8].copy_from_slice(&self.session_id.to_le_bytes());
off += 8;
buf[off..off + 32].copy_from_slice(self.verifier.as_bytes());
off += 32;
buf[off..off + 32].copy_from_slice(&self.verifier_nonce);
off += 32;
buf[off..off + 32].copy_from_slice(self.target.authority.as_bytes());
off += 32;
buf[off..off + 4].copy_from_slice(&self.target.path.raw().to_le_bytes());
off += 4;
buf[off] = self.requested_rights.bits();
buf
}
fn signing_input(&self) -> [u8; Self::SIGNING_INPUT_SIZE] {
let mut buf = [0u8; Self::SIGNING_INPUT_SIZE];
buf[..SUBNET_PRESENTATION_SIG_DOMAIN.len()].copy_from_slice(SUBNET_PRESENTATION_SIG_DOMAIN);
buf[SUBNET_PRESENTATION_SIG_DOMAIN.len()..].copy_from_slice(&self.signed_payload());
buf
}
pub fn to_bytes(&self) -> Vec<u8> {
let mut out = Vec::with_capacity(Self::WIRE_SIZE);
out.extend_from_slice(&self.signed_payload());
out.extend_from_slice(&self.signature);
out
}
pub fn from_bytes(bytes: &[u8]) -> Result<Self, SubnetAuthError> {
if bytes.len() != Self::WIRE_SIZE {
return Err(SubnetAuthError::InvalidFormat);
}
let mut off = 0;
let version = bytes[off];
off += 1;
if version != 1 {
return Err(SubnetAuthError::InvalidFormat);
}
let subject = EntityId::from_bytes(read_32(bytes, &mut off));
let credential_set_hash = read_32(bytes, &mut off);
let session_id = read_u64(bytes, &mut off);
let verifier = EntityId::from_bytes(read_32(bytes, &mut off));
let verifier_nonce = read_32(bytes, &mut off);
let target_authority = EntityId::from_bytes(read_32(bytes, &mut off));
let target_path = TopologySubnetId::from_raw(read_u32(bytes, &mut off));
let requested_rights = SubnetRights::try_from_bits(bytes[off])?;
off += 1;
let mut signature = [0u8; 64];
signature.copy_from_slice(&bytes[off..off + 64]);
Ok(Self {
version,
subject,
credential_set_hash,
session_id,
verifier,
verifier_nonce,
target: SubnetRef {
authority: target_authority,
path: target_path,
},
requested_rights,
signature,
})
}
pub fn verify(&self) -> Result<(), SubnetAuthError> {
let sig = Signature::from_bytes(&self.signature);
self.subject
.verify(&self.signing_input(), &sig)
.map_err(|_| SubnetAuthError::InvalidSignature)
}
}
#[derive(Debug, Clone)]
pub struct ExpectedBinding {
pub session_id: u64,
pub verifier: EntityId,
pub verifier_nonce: [u8; 32],
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct VerifiedSubnetContext {
pub authority: EntityId,
pub attachment: TopologySubnetId,
pub scope: TopologySubnetId,
pub topology_epoch: u32,
pub subject: EntityId,
pub subject_node: u64,
pub session_id: u64,
pub rights: SubnetRights,
pub generation: u32,
pub subnet_auth_epoch: u64,
pub expires_at: u64,
pub credential_set_hash: [u8; 32],
}
impl VerifiedSubnetContext {
#[inline]
pub fn allows(
&self,
current_topology_epoch: u32,
current_subnet_auth_epoch: u64,
now_secs: u64,
target: TopologySubnetId,
right: SubnetRights,
) -> bool {
self.topology_epoch == current_topology_epoch
&& self.subnet_auth_epoch == current_subnet_auth_epoch
&& now_secs < self.expires_at
&& self.scope.is_ancestor_or_self_of(target)
&& self.rights.contains(right)
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct VerifiedGatewayContext {
pub authority: EntityId,
pub attachment: TopologySubnetId,
pub scope: TopologySubnetId,
pub topology_epoch: u32,
pub subject: EntityId,
pub rights: SubnetRights,
pub generation: u32,
pub subnet_auth_epoch: u64,
pub expires_at: u64,
pub credential_set_hash: [u8; 32],
}
pub const MAX_GATEWAY_CONTEXTS_PER_AUTHORITY: usize = 32;
#[derive(Debug, Clone, PartialEq, Eq)]
struct GatewayScopeIndex {
by_scope: Box<[(u32, SubnetRights)]>,
}
impl GatewayScopeIndex {
fn build(entries: &[VerifiedGatewayContext]) -> Self {
let mut by_scope: Vec<(u32, SubnetRights)> =
entries.iter().map(|e| (e.scope.raw(), e.rights)).collect();
by_scope.sort_unstable_by_key(|(scope, _)| *scope);
Self {
by_scope: by_scope.into_boxed_slice(),
}
}
#[inline]
fn is_empty(&self) -> bool {
self.by_scope.is_empty()
}
#[inline]
fn rights_at(&self, scope: TopologySubnetId) -> Option<SubnetRights> {
self.by_scope
.binary_search_by_key(&scope.raw(), |(candidate, _)| *candidate)
.ok()
.map(|i| self.by_scope[i].1)
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct VerifiedGatewayContextSet {
authority: EntityId,
entries: Box<[VerifiedGatewayContext]>,
topology_epoch: u32,
subnet_auth_epoch: u64,
earliest_expiry: u64,
index: GatewayScopeIndex,
}
pub const MAX_TRANSITION_LOOKUPS: u32 = 4 * MAX_DEPTH as u32;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SubnetBoundarySet {
authority: EntityId,
topology_epoch: u32,
boundaries: Box<[TopologySubnetId]>,
}
impl SubnetBoundarySet {
pub fn new(
authority: EntityId,
topology_epoch: u32,
boundaries: impl IntoIterator<Item = TopologySubnetId>,
) -> Self {
let mut sorted: Vec<TopologySubnetId> = boundaries.into_iter().collect();
sorted.sort_unstable_by_key(|b| b.raw());
sorted.dedup();
Self {
authority,
topology_epoch,
boundaries: sorted.into_boxed_slice(),
}
}
pub fn authority(&self) -> &EntityId {
&self.authority
}
pub fn topology_epoch(&self) -> u32 {
self.topology_epoch
}
pub fn boundaries(&self) -> &[TopologySubnetId] {
&self.boundaries
}
#[inline]
fn is_boundary(&self, scope: TopologySubnetId) -> bool {
self.boundaries
.binary_search_by_key(&scope.raw(), |b| b.raw())
.is_ok()
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SubnetExportBinding {
subnet: SubnetRef,
topology_epoch: u32,
}
impl SubnetExportBinding {
pub fn new(subnet: SubnetRef, topology_epoch: u32) -> Self {
Self {
subnet,
topology_epoch,
}
}
pub fn subnet(&self) -> &SubnetRef {
&self.subnet
}
pub fn topology_epoch(&self) -> u32 {
self.topology_epoch
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ForwardDenial {
ContextNotCurrent,
AttachMissing,
ExportMissing,
RouteMissing,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct TransitionDecision {
pub verdict: Result<(), ForwardDenial>,
pub lookup_calls: u32,
}
impl VerifiedGatewayContextSet {
pub fn authority(&self) -> &EntityId {
&self.authority
}
pub fn entries(&self) -> &[VerifiedGatewayContext] {
&self.entries
}
pub fn topology_epoch(&self) -> u32 {
self.topology_epoch
}
pub fn subnet_auth_epoch(&self) -> u64 {
self.subnet_auth_epoch
}
pub fn earliest_expiry(&self) -> u64 {
self.earliest_expiry
}
}
impl VerifiedGatewayContextSet {
pub fn authorize_transition(
&self,
ingress: &VerifiedSubnetContext,
egress: &VerifiedSubnetContext,
boundaries: &SubnetBoundarySet,
current_topology_epoch: u32,
current_subnet_auth_epoch: u64,
now_secs: u64,
) -> Result<(), ForwardDenial> {
self.authorize_transition_counted(
ingress,
egress,
boundaries,
current_topology_epoch,
current_subnet_auth_epoch,
now_secs,
)
.verdict
}
pub fn authorize_service_export(
&self,
binding: &SubnetExportBinding,
boundaries: &SubnetBoundarySet,
current_topology_epoch: u32,
current_subnet_auth_epoch: u64,
now_secs: u64,
) -> Result<(), ForwardDenial> {
let scope = binding.subnet();
if scope.authority != self.authority || scope.authority != *boundaries.authority() {
return Err(ForwardDenial::ContextNotCurrent);
}
if binding.topology_epoch() != current_topology_epoch
|| self.topology_epoch != current_topology_epoch
|| boundaries.topology_epoch() != current_topology_epoch
{
return Err(ForwardDenial::ContextNotCurrent);
}
if self.subnet_auth_epoch != current_subnet_auth_epoch {
return Err(ForwardDenial::ContextNotCurrent);
}
if now_secs >= self.earliest_expiry {
return Err(ForwardDenial::ContextNotCurrent);
}
if !boundaries.is_boundary(scope.path) {
return Err(ForwardDenial::ExportMissing);
}
match self.index.rights_at(scope.path) {
Some(rights) if rights.contains(SubnetRights::EXPORT) => Ok(()),
_ => Err(ForwardDenial::ExportMissing),
}
}
pub fn authorize_transition_counted(
&self,
ingress: &VerifiedSubnetContext,
egress: &VerifiedSubnetContext,
boundaries: &SubnetBoundarySet,
current_topology_epoch: u32,
current_subnet_auth_epoch: u64,
now_secs: u64,
) -> TransitionDecision {
let refuse = |denial| TransitionDecision {
verdict: Err(denial),
lookup_calls: 0,
};
if boundaries.authority != self.authority
|| boundaries.topology_epoch != current_topology_epoch
{
return refuse(ForwardDenial::ContextNotCurrent);
}
for peer in [ingress, egress] {
if peer.authority != self.authority
|| peer.topology_epoch != current_topology_epoch
|| peer.subnet_auth_epoch != current_subnet_auth_epoch
|| now_secs >= peer.expires_at
{
return refuse(ForwardDenial::ContextNotCurrent);
}
if !peer.rights.contains(SubnetRights::ATTACH) {
return refuse(ForwardDenial::AttachMissing);
}
}
if !self.index.is_empty()
&& (self.topology_epoch != current_topology_epoch
|| self.subnet_auth_epoch != current_subnet_auth_epoch
|| now_secs >= self.earliest_expiry)
{
return refuse(ForwardDenial::ContextNotCurrent);
}
let source = ingress.attachment;
let target = egress.attachment;
let common = source.common_ancestor(target);
let mut lookup_calls = 0u32;
let mut crossed_any = false;
for endpoint in [source, target] {
for scope in endpoint.ancestor_path() {
if scope == common {
break;
}
lookup_calls += 1;
if !boundaries.is_boundary(scope) {
continue;
}
crossed_any = true;
lookup_calls += 1;
let exports = self
.index
.rights_at(scope)
.is_some_and(|r| r.contains(SubnetRights::EXPORT));
if !exports {
return TransitionDecision {
verdict: Err(ForwardDenial::ExportMissing),
lookup_calls,
};
}
}
}
if crossed_any {
return TransitionDecision {
verdict: Ok(()),
lookup_calls,
};
}
for scope in common.ancestor_path() {
lookup_calls += 1;
let routes = self
.index
.rights_at(scope)
.is_some_and(|r| r.contains(SubnetRights::ROUTE));
if routes {
return TransitionDecision {
verdict: Ok(()),
lookup_calls,
};
}
}
TransitionDecision {
verdict: Err(ForwardDenial::RouteMissing),
lookup_calls,
}
}
}
#[expect(
clippy::too_many_arguments,
reason = "mirrors verify_credential_set's explicit trusted-input list; a struct would hide \
which inputs the caller must supply from local state"
)]
pub fn compile_gateway_context(
set: &SubnetCredentialSet,
local_entity: &EntityId,
local_attachment: TopologySubnetId,
config: &SubnetAuthorityConfig,
current_topology_epoch: u32,
floors: &SubnetFloorRegistry,
now_secs: u64,
skew_secs: u64,
) -> Result<VerifiedGatewayContext, SubnetAuthError> {
if &set.leaf().subject != local_entity {
return Err(SubnetAuthError::WrongSubject);
}
let verified = verify_credential_set(
set,
local_entity,
config,
current_topology_epoch,
floors,
now_secs,
skew_secs,
)?;
let scope_contains_attachment = verified.scope.is_ancestor_or_self_of(local_attachment);
if verified.rights.contains(SubnetRights::ATTACH) {
if !scope_contains_attachment {
return Err(SubnetAuthError::ScopeNotAncestor);
}
} else if !scope_contains_attachment && !local_attachment.is_ancestor_or_self_of(verified.scope)
{
return Err(SubnetAuthError::ScopeNotAncestor);
}
Ok(VerifiedGatewayContext {
authority: verified.authority,
attachment: local_attachment,
scope: verified.scope,
topology_epoch: verified.topology_epoch,
subject: verified.subject,
rights: verified.rights,
generation: verified.generation,
subnet_auth_epoch: verified.subnet_auth_epoch,
expires_at: verified.expires_at,
credential_set_hash: verified.credential_set_hash,
})
}
pub fn build_gateway_context_set(
authority: &EntityId,
compiled: Vec<VerifiedGatewayContext>,
) -> Result<VerifiedGatewayContextSet, SubnetAuthError> {
let mut entries: Vec<VerifiedGatewayContext> = Vec::with_capacity(compiled.len());
for entry in compiled {
if &entry.authority != authority {
return Err(SubnetAuthError::WrongAuthority);
}
if let Some(first) = entries.first() {
if entry.topology_epoch != first.topology_epoch
|| entry.subnet_auth_epoch != first.subnet_auth_epoch
{
return Err(SubnetAuthError::MixedGatewayEpochs);
}
}
match entries.iter_mut().find(|e| e.scope == entry.scope) {
Some(existing) => {
existing.rights = existing.rights.union(entry.rights);
existing.expires_at = existing.expires_at.min(entry.expires_at);
}
None => {
entries.push(entry);
if entries.len() > MAX_GATEWAY_CONTEXTS_PER_AUTHORITY {
return Err(SubnetAuthError::TooManyGatewayContexts);
}
}
}
}
let topology_epoch = entries.first().map_or(0, |e| e.topology_epoch);
let subnet_auth_epoch = entries.first().map_or(0, |e| e.subnet_auth_epoch);
let earliest_expiry = entries
.iter()
.map(|e| e.expires_at)
.min()
.unwrap_or(u64::MAX);
let index = GatewayScopeIndex::build(&entries);
Ok(VerifiedGatewayContextSet {
authority: authority.clone(),
entries: entries.into_boxed_slice(),
topology_epoch,
subnet_auth_epoch,
earliest_expiry,
index,
})
}
#[expect(
clippy::too_many_arguments,
reason = "every parameter is a distinct verifier-owned input; bundling them into a struct \
would hide which ones the caller must supply from trusted state"
)]
pub fn verify_admission(
presentation: &SubnetAuthPresentation,
set: &SubnetCredentialSet,
expected: &ExpectedBinding,
config: &SubnetAuthorityConfig,
current_topology_epoch: u32,
floors: &SubnetFloorRegistry,
now_secs: u64,
skew_secs: u64,
) -> Result<VerifiedSubnetContext, SubnetAuthError> {
if presentation.session_id != expected.session_id {
return Err(SubnetAuthError::WrongSession);
}
if presentation.verifier != expected.verifier {
return Err(SubnetAuthError::WrongVerifier);
}
if !bool::from(subtle::ConstantTimeEq::ct_eq(
&presentation.verifier_nonce[..],
&expected.verifier_nonce[..],
)) {
return Err(SubnetAuthError::WrongChallenge);
}
if presentation.credential_set_hash != set.credential_set_hash() {
return Err(SubnetAuthError::WrongChallenge);
}
if presentation.target.authority != config.authority {
return Err(SubnetAuthError::WrongAuthority);
}
let leaf_subject = set.leaf().subject.clone();
if presentation.subject != leaf_subject {
return Err(SubnetAuthError::WrongSubject);
}
presentation.verify()?;
let verified = verify_credential_set(
set,
&leaf_subject,
config,
current_topology_epoch,
floors,
now_secs,
skew_secs,
)?;
if !verified
.scope
.is_ancestor_or_self_of(presentation.target.path)
{
return Err(SubnetAuthError::ScopeNotAncestor);
}
if !verified.rights.contains(presentation.requested_rights) {
return Err(SubnetAuthError::RightNotGranted);
}
Ok(VerifiedSubnetContext {
authority: verified.authority,
attachment: presentation.target.path,
scope: verified.scope,
topology_epoch: verified.topology_epoch,
subject_node: verified.subject.node_id(),
subject: verified.subject,
session_id: expected.session_id,
rights: presentation.requested_rights,
generation: verified.generation,
subnet_auth_epoch: verified.subnet_auth_epoch,
expires_at: verified.expires_at,
credential_set_hash: verified.credential_set_hash,
})
}
#[expect(
clippy::unwrap_used,
reason = "callers pre-check exact buffer length; fixed-width slicing is statically infallible"
)]
pub(super) fn read_32(bytes: &[u8], off: &mut usize) -> [u8; 32] {
let out: [u8; 32] = bytes[*off..*off + 32].try_into().unwrap();
*off += 32;
out
}
#[expect(
clippy::unwrap_used,
reason = "callers pre-check exact buffer length; fixed-width slicing is statically infallible"
)]
pub(super) fn read_u32(bytes: &[u8], off: &mut usize) -> u32 {
let out = u32::from_le_bytes(bytes[*off..*off + 4].try_into().unwrap());
*off += 4;
out
}
#[expect(
clippy::unwrap_used,
reason = "callers pre-check exact buffer length; fixed-width slicing is statically infallible"
)]
pub(super) fn read_u64(bytes: &[u8], off: &mut usize) -> u64 {
let out = u64::from_le_bytes(bytes[*off..*off + 8].try_into().unwrap());
*off += 8;
out
}
#[cfg(test)]
mod wire_kind_tests {
use super::SubnetAuthError as E;
#[test]
fn all_is_complete_and_tokens_are_distinct() {
fn assert_exhaustive(e: E) -> u32 {
match e {
E::UnknownAuthority => 0,
E::WrongSubject => 1,
E::WrongAuthority => 2,
E::WrongTopologyEpoch => 3,
E::ScopeNotAncestor => 4,
E::InvalidRights => 5,
E::Expired => 6,
E::NotYetValid => 7,
E::LifetimeTooWide => 8,
E::Revoked => 9,
E::IssuerNotAuthorized => 10,
E::IssuerAttenuationBroadened => 11,
E::RightNotGranted => 12,
E::WrongSession => 13,
E::WrongVerifier => 14,
E::WrongChallenge => 15,
E::IdentityPinConflict => 16,
E::TooManyGatewayContexts => 17,
E::MixedGatewayEpochs => 18,
E::InvalidSignature => 19,
E::InvalidFormat => 20,
E::InvalidValidityWindow => 21,
E::ClockSkewTooLarge => 22,
}
}
const EXPECTED: usize = 23;
assert_eq!(
E::ALL.len(),
EXPECTED,
"a variant was added to the enum but not to SubnetAuthError::ALL",
);
let mut ordinals: Vec<u32> = E::ALL.iter().map(|e| assert_exhaustive(*e)).collect();
ordinals.sort_unstable();
assert_eq!(ordinals, (0..EXPECTED as u32).collect::<Vec<_>>());
let tokens: std::collections::BTreeSet<&str> =
E::ALL.iter().map(|e| e.wire_kind()).collect();
assert_eq!(
tokens.len(),
EXPECTED,
"reason-code tokens must be distinct"
);
assert!(E::ALL.iter().all(|e| !e.wire_kind().is_empty()));
}
#[test]
fn display_is_wire_kind_for_every_variant() {
for e in E::ALL {
assert_eq!(e.to_string(), e.wire_kind());
}
}
}