//! Bounded service-name ownership model. `caller` must come from the trusted //! process boundary; request bytes must never supply their own caller ID. pub const MAX_NAME: usize = 24; pub const WIRE_HEADER_BYTES: usize = 8; pub const WIRE_RESPONSE_BYTES: usize = 17; pub const WIRE_VERSION: u8 = 1; const MAX_WIRE_PAYLOAD: usize = 1 + MAX_NAME + 8; #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub enum WireError { Truncated, Header, Version, Length, Kind, ResponseSpace, Registry(Error), } enum WireRequest { Publish(Name, u64), Unpublish(Name), } fn decode_v1(bytes: &[u8]) -> Result { if bytes.len() < WIRE_HEADER_BYTES { return Err(WireError::Truncated); } if &bytes[..2] != b"SR" || bytes[6] != 0 || bytes[7] != 0 { return Err(WireError::Header); } if bytes[2] != WIRE_VERSION { return Err(WireError::Version); } let payload_len = u16::from_le_bytes([bytes[4], bytes[5]]) as usize; if payload_len > MAX_WIRE_PAYLOAD || bytes.len() != WIRE_HEADER_BYTES + payload_len { return Err(WireError::Length); } let payload = &bytes[WIRE_HEADER_BYTES..]; if payload.is_empty() { return Err(WireError::Length); } let name_len = payload[0] as usize; if name_len == 0 || name_len > MAX_NAME || payload.len() < 1 + name_len { return Err(WireError::Length); } let name = Name::parse(&payload[1..1 + name_len]) .map_err(WireError::Registry)?; match bytes[3] { 1 if payload.len() == 1 + name_len + 8 => { let start = 1 + name_len; let endpoint = u64::from_le_bytes(payload[start..start + 8] .try_into().map_err(|_| WireError::Length)?); Ok(WireRequest::Publish(name, endpoint)) } 2 if payload.len() == 1 + name_len => Ok(WireRequest::Unpublish(name)), 1 | 2 => Err(WireError::Length), _ => Err(WireError::Kind), } } #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub enum Error { InvalidName, InvalidEndpoint, Access, Occupied, NotFound, StaleSession, } #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub struct Name { bytes: [u8; MAX_NAME], len: u8 } impl Name { pub fn parse(input: &[u8]) -> Result { if input.is_empty() || input.len() > MAX_NAME || !input[0].is_ascii_lowercase() || input.iter().any(|byte| !byte.is_ascii_lowercase() && !byte.is_ascii_digit() && *byte != b'-') || input.last() == Some(&b'-') { return Err(Error::InvalidName); } let mut bytes = [0; MAX_NAME]; bytes[..input.len()].copy_from_slice(input); Ok(Self {bytes, len: input.len() as u8}) } pub fn as_bytes(&self) -> &[u8] { &self.bytes[..self.len as usize] } } #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub struct PublisherId { slot: u32, generation: u64 } impl PublisherId { pub fn new(slot: u32, generation: u64) -> Result { if slot == 0 || generation == 0 { return Err(Error::Access); } Ok(Self {slot, generation}) } pub const fn slot(self) -> u32 { self.slot } pub const fn generation(self) -> u64 { self.generation } } #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub struct Binding { pub name: Name, pub publisher: PublisherId, pub endpoint: u64, } #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub struct Session { binding: Binding } impl Session { pub const fn publisher(self) -> PublisherId { self.binding.publisher } pub const fn endpoint(self) -> u64 { self.binding.endpoint } } #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub struct Grant { pub name: Name, pub publisher: PublisherId } impl Grant { pub const fn new(name: Name, publisher: PublisherId) -> Self { Self {name, publisher} } } /// Callers supply this identity only through the authenticated process /// boundary. The registry remains private to the service process. pub type PrincipalId = PublisherId; #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub enum Namespace { Public, Management } #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub struct VisibilityGrant { principal: PrincipalId, namespace: Namespace, name: Name, } impl VisibilityGrant { pub const fn new(principal: PrincipalId, namespace: Namespace, name: Name) -> Self { Self {principal, namespace, name} } } #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub struct Visibility { grants: [VisibilityGrant; N] } impl Visibility { /// A trusted startup policy provides the grants. Duplicates are refused /// before publication; there is no fallible state change after this check. pub fn with_grants(grants: [VisibilityGrant; N]) -> Result { for (index, grant) in grants.iter().enumerate() { if grants[..index].contains(grant) { return Err(Error::Occupied); } } Ok(Self {grants}) } /// Hidden and nonexistent names are deliberately indistinguishable. /// Only the service process may call Registry::lookup directly. pub fn query(&self, registry: &Registry, principal: PrincipalId, namespace: Namespace, name: Name) -> Result { let allowed = self.grants.iter().any(|grant| grant.principal == principal && grant.namespace == namespace && grant.name == name); if !allowed { return Err(Error::NotFound); } registry.lookup(name) } } #[derive(Clone, Copy)] struct Slot { grant: Grant, endpoint: Option } pub struct Registry { slots: [Slot; N] } impl Registry { /// The caller is authenticated by the process boundary, never by wire bytes. /// Every fallible wire check happens before the registry is changed; after /// a successful publish/unpublish the bounded response write cannot fail. pub fn handle_v1(&mut self, caller: PublisherId, request: &[u8], response: &mut [u8]) -> Result { let operation = decode_v1(request)?; if response.len() < WIRE_RESPONSE_BYTES { return Err(WireError::ResponseSpace); } let (kind, binding) = match operation { WireRequest::Publish(name, endpoint) => (0x81, self.publish(caller, name, endpoint) .map_err(WireError::Registry)?), WireRequest::Unpublish(name) => (0x82, self.unpublish(caller, name) .map_err(WireError::Registry)?), }; response[..8].copy_from_slice(&[b'S', b'R', WIRE_VERSION, kind, 9, 0, 0, 0]); response[8] = 0; response[9..17].copy_from_slice(&binding.endpoint.to_le_bytes()); Ok(WIRE_RESPONSE_BYTES) } /// `grants` are supplied by the trusted startup policy, not a service request. pub fn with_grants(grants: [Grant; N]) -> Result { for (index, grant) in grants.iter().enumerate() { if grants[..index].iter().any(|prior| prior.name == grant.name) { return Err(Error::Occupied); } } Ok(Self {slots: grants.map(|grant| Slot {grant, endpoint: None})}) } pub fn grants(&self) -> [Grant; N] { core::array::from_fn(|index| self.slots[index].grant) } pub fn snapshot(&self) -> [Option; N] { core::array::from_fn(|index| { let slot = self.slots[index]; slot.endpoint.map(|endpoint| Binding { name: slot.grant.name, publisher: slot.grant.publisher, endpoint, }) }) } pub fn count(&self) -> usize { self.slots.iter().filter(|slot| slot.endpoint.is_some()).count() } pub fn lookup(&self, name: Name) -> Result { self.snapshot().into_iter().flatten() .find(|entry| entry.name == name).ok_or(Error::NotFound) } /// The trusted lifecycle boundary calls this only after it has observed /// the old process stop and revoked its endpoint resources. Failed checks /// leave both the binding and grant unchanged. The returned old binding /// identifies the endpoint that the lifecycle owner must have revoked. pub fn rebind_stopped(&mut self, name: Name, stopped: PublisherId, replacement: PublisherId) -> Result, Error> { let slot = self.slots.iter_mut().find(|slot| slot.grant.name == name) .ok_or(Error::NotFound)?; if slot.grant.publisher != stopped || replacement == stopped { return Err(Error::Access); } let old = slot.endpoint.map(|endpoint| Binding { name, publisher: stopped, endpoint, }); slot.endpoint = None; slot.grant.publisher = replacement; Ok(old) } pub fn open_session(&self, name: Name) -> Result { Ok(Session {binding: self.lookup(name)?}) } pub fn validate_session(&self, session: Session) -> Result { match self.lookup(session.binding.name) { Ok(current) if current == session.binding => Ok(current), _ => Err(Error::StaleSession), } } pub fn publish(&mut self, caller: PublisherId, name: Name, endpoint: u64) -> Result { let slot = self.slots.iter_mut().find(|slot| slot.grant.name == name) .ok_or(Error::Access)?; if slot.grant.publisher != caller { return Err(Error::Access); } if endpoint == 0 { return Err(Error::InvalidEndpoint); } if slot.endpoint.is_some() { return Err(Error::Occupied); } let binding = Binding {name, publisher: caller, endpoint}; slot.endpoint = Some(endpoint); Ok(binding) } pub fn unpublish(&mut self, caller: PublisherId, name: Name) -> Result { let slot = self.slots.iter_mut().find(|slot| slot.grant.name == name) .ok_or(Error::NotFound)?; if slot.grant.publisher != caller { return Err(Error::Access); } let endpoint = slot.endpoint.take().ok_or(Error::NotFound)?; let binding = Binding {name, publisher: caller, endpoint}; Ok(binding) } }