liminal-server 0.25.4

Standalone server for the liminal messaging bus
Documentation
//! Explicit bounds for certificate admission and far service projections.

use std::num::{NonZeroU64, NonZeroUsize};
use std::path::PathBuf;

/// Peer security is opt-in so recorded-key configurations retain their shape.
#[derive(Clone, Debug, PartialEq, Eq, serde::Deserialize)]
#[serde(deny_unknown_fields)]
pub struct PeersConfig {
    /// Concrete subject prefixes whose service contracts and calls may cross.
    /// The pass must still grant every act; this list adds no authority.
    pub service_prefixes: Vec<String>,
    /// The issued capability certificate, DER, read before each open so rotation
    /// needs no credential copied into a configuration file.
    pub certificate_path: Option<PathBuf>,
    /// Largest certificate accepted before parsing or allocating its claims.
    /// This confines a certified peer's X.509 parsing cost.
    pub max_certificate_bytes: NonZeroUsize,
    /// Largest response from the unauthenticated service-key route.
    /// This bounds allocation even if the configured issuer is compromised.
    pub max_key_response_bytes: NonZeroUsize,
    /// Largest signed proof header accepted before hex/COSE decoding.
    /// This bounds parser cost independently of certificate DER.
    pub max_proof_bytes: NonZeroUsize,
    /// Largest response to the single certificate-standing pass request.
    /// A compromised issuer cannot allocate an unbounded token response.
    pub max_pass_response_bytes: NonZeroUsize,
    /// Largest current certificate-log view returned by the signed read.
    /// This bounds retained history while checking one exact withdrawal.
    pub max_certificate_status_bytes: NonZeroUsize,
    /// Largest signing-time skew accepted for the nonce-bearing proof.
    /// At most Lys's own sixty-second request-signature window.
    pub proof_window_ms: NonZeroU64,
    /// Longest fresh pass accepted as certificate-standing evidence.
    /// This bounds withdrawal exposure when the grant feed cannot name certificates.
    pub max_certificate_pass_lifetime_secs: NonZeroU64,
    /// Largest complete far topology snapshot before JSON decoding.
    /// A peer cannot exhaust this exchange's heap with discovery metadata.
    pub max_topology_bytes: NonZeroUsize,
    /// Most stations retained from one snapshot, including unexported stations.
    /// Filtering cannot hide the cost of parsing a malicious discovery record.
    pub max_stations: NonZeroUsize,
    /// Most simultaneous discovered peers, so discovery memory is bounded.
    pub max_peers: NonZeroUsize,
    /// Most queued or in-flight calls per link; checked before dispatch.
    /// A slow far worker must not consume unbounded local reservations.
    pub max_pending_calls: NonZeroUsize,
    /// Most bytes in a call or reply, including its signed caller credential.
    /// This caps retained call data independently of the pending-entry count.
    pub max_call_bytes: NonZeroUsize,
}

impl PeersConfig {
    /// Checks the configured allocation bounds against the transport ceiling.
    ///
    /// # Errors
    /// A certificate/call cannot fit in a single sealed Noise message, or an
    /// optional certificate path is empty.
    pub fn validate(&self) -> Result<(), &'static str> {
        let ceiling =
            crate::link_handshake::NOISE_MAX_MESSAGE - crate::link_handshake::NOISE_OVERHEAD;
        if self.max_certificate_bytes.get() > ceiling || self.max_call_bytes.get() > ceiling {
            return Err("peer_bound_exceeds_noise_frame");
        }
        if self.proof_window_ms.get() > 60_000 {
            return Err("certificate_proof_window_exceeds_issuer");
        }
        if self
            .certificate_path
            .as_ref()
            .is_some_and(|path| path.as_os_str().is_empty())
        {
            return Err("certificate_path_empty");
        }
        for prefix in &self.service_prefixes {
            crate::auth_lys::SubjectBinding::new("wire".into(), prefix.clone(), prefix.clone())
                .map_err(|_| "peer_service_prefix_invalid")?;
            if prefix == "liminal"
                || prefix.starts_with("liminal.")
                || prefix == "console"
                || prefix.starts_with("console.")
            {
                return Err("peer_service_prefix_reserved");
            }
        }
        Ok(())
    }
}

/// Collects peer configuration errors without network access or credentials.
pub(crate) fn collect_errors(config: &super::types::ServerConfig, errors: &mut Vec<String>) {
    let Some(lys) = config.auth.as_ref().and_then(|auth| auth.lys.as_ref()) else {
        return;
    };
    let Some(machine) = &lys.machine else {
        return;
    };
    let Some(peers) = &machine.carrier_choice.peers else {
        return;
    };
    if let Err(error) = peers.validate() {
        errors.push(format!("auth.lys.machine.carrier_choice.peers: {error}"));
    }
    if !config.listen_address.ip().is_loopback()
        || config
            .extra_listen_addresses
            .iter()
            .any(|address| !address.ip().is_loopback())
    {
        errors.push("own-device client listeners must be loopback; peers use Noise links".into());
    }
    if config
        .websocket
        .as_ref()
        .is_some_and(|websocket| !websocket.listen_address.ip().is_loopback())
    {
        errors.push("own-device websocket client listener must be loopback".into());
    }
    if lys
        .api_url
        .as_deref()
        .is_none_or(|base| crate::link::certificate::service_key::endpoint(base).is_err())
    {
        errors.push(
            "auth.lys.api_url: peer certificates require a trusted service-key API base".into(),
        );
    }
    if !lys.live {
        errors.push(
            "auth.lys.live: peer certificate standing requires the issuer's live route".into(),
        );
    }
    if lys
        .grant_stream
        .as_ref()
        .is_none_or(|stream| !stream.enforce_revocation)
    {
        errors.push("auth.lys.grant_stream: own-device peers require enforce_revocation".into());
    }
    if peers.certificate_path.is_some() && machine.agent_id.is_empty() {
        errors.push(
            "auth.lys.machine.agent_id: a certificate offer must name its certified agent".into(),
        );
    }
}