liminal-server 0.25.4

Standalone server for the liminal messaging bus
Documentation
//! A fresh request on Lys's certified-agent pass route proves current key standing.
//! The certificate signature is still verified offline with the service key.

use super::{CertificateRefusal, MachineIdentity};
use crate::auth_lys::{LysPassConsumer, lys_pass_adapter::LysPassAdapter};
use crate::config::peers::PeersConfig;
use lys_core::attestation::verify_attestation_bytes_by_signer;
use std::sync::Arc;
use std::time::Duration;
use zeroize::Zeroizing;

/// An issuer route and its verifier, with no holder token retained here.
pub struct Standing {
    base: reqwest::Url,
    http: reqwest::Client,
    handle: tokio::runtime::Handle,
    adapter: Arc<LysPassAdapter>,
    limits: PeersConfig,
}
impl std::fmt::Debug for Standing {
    fn fmt(&self, out: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        out.debug_struct("CertificateStanding")
            .finish_non_exhaustive()
    }
}
#[derive(serde::Deserialize)]
#[serde(deny_unknown_fields)]
struct Answer {
    pass: String,
    expires_at: u64,
    audience: String,
}

/// A verified short-lived pass supplies a deadline and is zeroized after admission.
pub struct StandingPass {
    token: Zeroizing<String>,
    expires_at: u64,
}
impl std::fmt::Debug for StandingPass {
    fn fmt(&self, out: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        out.debug_struct("StandingPass")
            .field("bytes", &self.token.len())
            .field("expires_at", &self.expires_at)
            .finish_non_exhaustive()
    }
}
impl StandingPass {
    /// The original issuer bytes, borrowed only for ordinary admission.
    #[must_use]
    pub fn as_bytes(&self) -> &[u8] {
        self.token.as_bytes()
    }
    /// No new crossing may use this proof at or after this Unix second.
    #[must_use]
    pub const fn expires_at(&self) -> u64 {
        self.expires_at
    }
}

impl Standing {
    /// Builds from the trusted API base and the same verifier used for Connect.
    /// The positive deadline bounds one request, never an idle wait or retry.
    ///
    /// # Errors
    /// Invalid endpoint, bounds or HTTP client setup.
    pub fn new(
        base: &str,
        adapter: Arc<LysPassAdapter>,
        handle: tokio::runtime::Handle,
        limits: PeersConfig,
        deadline: Duration,
    ) -> Result<Arc<Self>, CertificateRefusal> {
        limits.validate().map_err(|_| CertificateRefusal::Bound)?;
        if deadline.is_zero() {
            return Err(CertificateRefusal::StatusUnavailable);
        }
        let mut base = super::service_key::endpoint(base)?;
        let path = base
            .path()
            .strip_suffix("service-key")
            .ok_or(CertificateRefusal::ServiceKey)?
            .to_owned();
        base.set_path(&path);
        let http = reqwest::Client::builder()
            .redirect(reqwest::redirect::Policy::none())
            .timeout(deadline)
            .build()
            .map_err(|_| CertificateRefusal::StatusUnavailable)?;
        Ok(Arc::new(Self {
            base,
            http,
            handle,
            adapter,
            limits,
        }))
    }
    /// The exact issuer request path this holder must sign, prefix included.
    ///
    /// # Errors
    /// The holder is not a single safe route segment.
    pub fn endpoint(&self, holder: &str) -> Result<reqwest::Url, CertificateRefusal> {
        if holder.is_empty()
            || !holder
                .bytes()
                .all(|byte| byte.is_ascii_alphanumeric() || matches!(byte, b'-' | b'_'))
        {
            return Err(CertificateRefusal::HolderMismatch);
        }
        self.base
            .join(&format!("agents/{holder}/pass"))
            .map_err(|_| CertificateRefusal::ServiceKey)
    }
    /// The existing MCP door that accepts a certified agent's signed read.
    ///
    /// # Errors
    /// The configured issuer base cannot resolve its MCP door.
    pub fn status_endpoint(&self) -> Result<reqwest::Url, CertificateRefusal> {
        self.base
            .join("mcp")
            .map_err(|_| CertificateRefusal::ServiceKey)
    }
    /// The signature path used by the issuer's MCP verifier, which strips `/api`.
    #[must_use]
    pub fn status_signature_path(endpoint: &reqwest::Url) -> &str {
        endpoint
            .path()
            .strip_prefix("/api")
            .unwrap_or_else(|| endpoint.path())
    }
    /// The issuer's exact app-pass request body; it grants nothing by itself.
    #[must_use]
    pub const fn body() -> &'static [u8] {
        br#"{"audience":"liminal"}"#
    }

    /// Checks the proof against the certified Noise key before forwarding it.
    /// Lys then checks that key against its non-withdrawn certificate keys.
    ///
    /// # Errors
    /// Bound, key/holder/time/signature mismatch, withdrawal, unavailable issuer,
    /// wrong audience/holder, invalid pass or an excessive standing lifetime.
    pub fn prove(
        &self,
        holder: &str,
        machine: MachineIdentity,
        der: &[u8],
        header: &str,
        now: u64,
    ) -> Result<StandingPass, CertificateRefusal> {
        if header.len() > self.limits.max_proof_bytes.get()
            || der.len() > self.limits.max_certificate_bytes.get()
        {
            return Err(CertificateRefusal::Bound);
        }
        if der.is_empty() {
            return Err(CertificateRefusal::Encoding);
        }
        let (pass_header, status_header) =
            header.split_once('\n').ok_or(CertificateRefusal::Proof)?;
        if status_header.contains(['\n', '\r']) {
            return Err(CertificateRefusal::Proof);
        }
        let endpoint = self.endpoint(holder)?;
        verify_proof(
            holder,
            machine,
            pass_header,
            endpoint.path(),
            now,
            self.limits.max_proof_bytes.get(),
            self.limits.proof_window_ms.get(),
        )?;
        let status_endpoint = self.status_endpoint()?;
        let status_body = super::status::request(holder)?;
        verify_request(ProofRequest {
            holder,
            machine,
            header: status_header,
            path: Self::status_signature_path(&status_endpoint),
            body: &status_body,
            now,
            bound: self.limits.max_proof_bytes.get(),
            window_ms: self.limits.proof_window_ms.get(),
        })?;
        let token = std::thread::scope(|scope| {
            scope
                .spawn(|| {
                    self.handle.block_on(async {
                        self.ask_status(status_endpoint, status_header, status_body, holder, der)
                            .await?;
                        self.ask(endpoint, pass_header).await
                    })
                })
                .join()
        })
        .map_err(|_| CertificateRefusal::StatusUnavailable)??;
        // The issuer stamps the fresh pass during the request, so verification
        // uses the response's observation time rather than the earlier ask.
        let observed = std::time::SystemTime::now()
            .duration_since(std::time::UNIX_EPOCH)
            .map_err(|_| CertificateRefusal::StatusUnavailable)?
            .as_secs();
        let now = now.max(observed);
        let pass = self
            .adapter
            .verify_offline(token.as_bytes(), "liminal", now)
            .map_err(|_| CertificateRefusal::Proof)?;
        let claims = pass.verified().claims();
        if claims.holder.id != holder
            || claims.holder.kind != "agent"
            || claims.exp <= now
            || claims.iat > now
            || claims.exp != token.expires_at
            || claims.exp.saturating_sub(claims.iat)
                > self.limits.max_certificate_pass_lifetime_secs.get()
        {
            return Err(CertificateRefusal::Proof);
        }
        Ok(token)
    }
    async fn ask_status(
        &self,
        endpoint: reqwest::Url,
        header: &str,
        body: Vec<u8>,
        holder: &str,
        der: &[u8],
    ) -> Result<(), CertificateRefusal> {
        let mut response = self
            .http
            .post(endpoint)
            .header("lys-agent-signature", header)
            .header("accept", "application/json, text/event-stream")
            .header("content-type", "application/json")
            .body(body)
            .send()
            .await
            .map_err(|_| CertificateRefusal::StatusUnavailable)?;
        let status = response.status();
        let mut bytes = Zeroizing::new(Vec::new());
        while let Some(chunk) = response
            .chunk()
            .await
            .map_err(|_| CertificateRefusal::StatusUnavailable)?
        {
            if bytes
                .len()
                .checked_add(chunk.len())
                .is_none_or(|length| length > self.limits.max_certificate_status_bytes.get())
            {
                return Err(CertificateRefusal::Bound);
            }
            bytes.extend_from_slice(&chunk);
        }
        if !status.is_success() {
            return Err(issuer_refusal(&bytes)?);
        }
        super::status::response(&bytes, holder, der)
    }
    async fn ask(
        &self,
        endpoint: reqwest::Url,
        header: &str,
    ) -> Result<StandingPass, CertificateRefusal> {
        let mut response = self
            .http
            .post(endpoint)
            .header("lys-agent-signature", header)
            .header("content-type", "application/json")
            .body(Self::body())
            .send()
            .await
            .map_err(|_| CertificateRefusal::StatusUnavailable)?;
        let status = response.status();
        let mut bytes = Zeroizing::new(Vec::new());
        while let Some(chunk) = response
            .chunk()
            .await
            .map_err(|_| CertificateRefusal::StatusUnavailable)?
        {
            if bytes
                .len()
                .checked_add(chunk.len())
                .is_none_or(|length| length > self.limits.max_pass_response_bytes.get())
            {
                return Err(CertificateRefusal::Bound);
            }
            bytes.extend_from_slice(&chunk);
        }
        if !status.is_success() {
            return Err(issuer_refusal(&bytes)?);
        }
        let answer: Answer =
            serde_json::from_slice(&bytes).map_err(|_| CertificateRefusal::StatusUnavailable)?;
        let token = Zeroizing::new(answer.pass);
        if answer.audience != "liminal" || answer.expires_at == 0 || token.is_empty() {
            return Err(CertificateRefusal::Proof);
        }
        Ok(StandingPass {
            token,
            expires_at: answer.expires_at,
        })
    }
}

fn issuer_refusal(bytes: &[u8]) -> Result<CertificateRefusal, CertificateRefusal> {
    let reason: serde_json::Value =
        serde_json::from_slice(bytes).map_err(|_| CertificateRefusal::StatusUnavailable)?;
    Ok(
        if reason["refusal"] == "AgentSignatureRefused"
            && reason["reason"] == "AgentSignatureRefused: the signature does not verify"
        {
            CertificateRefusal::Withdrawn
        } else {
            CertificateRefusal::StatusUnavailable
        },
    )
}

/// Verifies the unchanged Lys signature contract under the certified key.
///
/// # Errors
/// Bound, time, holder, nonce or strict COSE signature refusal.
pub fn verify_proof(
    holder: &str,
    machine: MachineIdentity,
    header: &str,
    path: &str,
    now: u64,
    bound: usize,
    window_ms: u64,
) -> Result<(), CertificateRefusal> {
    verify_request(ProofRequest {
        holder,
        machine,
        header,
        path,
        body: Standing::body(),
        now,
        bound,
        window_ms,
    })
}

#[derive(Clone, Copy)]
struct ProofRequest<'a> {
    holder: &'a str,
    machine: MachineIdentity,
    header: &'a str,
    path: &'a str,
    body: &'a [u8],
    now: u64,
    bound: usize,
    window_ms: u64,
}

fn verify_request(proof: ProofRequest<'_>) -> Result<(), CertificateRefusal> {
    let ProofRequest {
        holder,
        machine,
        header,
        path,
        body,
        now,
        bound,
        window_ms,
    } = proof;
    if bound == 0 || header.len() > bound {
        return Err(CertificateRefusal::Bound);
    }
    let mut words = header.split_ascii_whitespace();
    let agent = words.next().ok_or(CertificateRefusal::Proof)?;
    let at: u64 = words
        .next()
        .ok_or(CertificateRefusal::Proof)?
        .parse()
        .map_err(|_| CertificateRefusal::Proof)?;
    let nonce = words.next().ok_or(CertificateRefusal::Proof)?;
    let signature = words.next().ok_or(CertificateRefusal::Proof)?;
    let observed = now.checked_mul(1000).ok_or(CertificateRefusal::Proof)?;
    if agent != holder
        || words.next().is_some()
        || window_ms == 0
        || observed.abs_diff(at) > window_ms
        || !hex::decode(nonce).is_ok_and(|bytes| bytes.len() == 16)
    {
        return Err(CertificateRefusal::Proof);
    }
    let cose = hex::decode(signature).map_err(|_| CertificateRefusal::Proof)?;
    let payload = crate::link_pass::cose::payload(path, body, at, nonce);
    verify_attestation_bytes_by_signer(&cose, &payload, machine.ed25519_public())
        .map(|_| ())
        .map_err(|_| CertificateRefusal::Proof)
}