liminal-server 0.25.4

Standalone server for the liminal messaging bus
Documentation
//! A capability certificate proves a key and a holder, never a permission.

use crate::link_handshake::MachineIdentity;
use lys_core::ca::{
    LYS_OID_ARC, certificate_signing_key, decode_extension, verify_certificate_chain_at,
};
use std::time::{Duration, UNIX_EPOCH};

#[path = "link_certificate/offer.rs"]
pub mod offer;
#[path = "link_certificate/service_key.rs"]
pub mod service_key;
#[path = "link_certificate/setup.rs"]
pub mod setup;
#[path = "link_certificate/standing.rs"]
pub mod standing;
#[path = "link_certificate/status.rs"]
pub mod status;
#[cfg(test)]
#[path = "link_certificate_tests.rs"]
mod tests;

/// Stable refusal names exclude certificate bytes and untrusted parser text.
#[derive(Clone, Copy, Debug, PartialEq, Eq, thiserror::Error)]
pub enum CertificateRefusal {
    /// Certificate allocation or encoding exceeded its configured limit.
    #[error("certificate_too_large")]
    Bound,
    /// The exact DER, including its outer length, could not be read.
    #[error("certificate_encoding_refused")]
    Encoding,
    /// The configured service key did not verify the certificate.
    #[error("certificate_signature_refused")]
    Signature,
    /// The certificate no longer stands at the operation's time.
    #[error("certificate_expired")]
    Expired,
    /// The certificate cannot yet stand at the operation's time.
    #[error("certificate_not_yet_valid")]
    NotYetValid,
    /// Signed capability claims name another holder.
    #[error("certificate_holder_mismatch")]
    HolderMismatch,
    /// The certified key differs from the Noise offer's static identity.
    #[error("certificate_key_mismatch")]
    KeyMismatch,
    /// Signed capability claims were absent or malformed.
    #[error("certificate_claims_refused")]
    Claims,
    /// No trusted current withdrawal observation exists.
    #[error("certificate_status_unavailable")]
    StatusUnavailable,
    /// The issuer's current observation withdrew this certificate.
    #[error("certificate_withdrawn")]
    Withdrawn,
    /// The configured issuer route, response or service key was refused.
    #[error("certificate_service_key_refused")]
    ServiceKey,
    /// The configured certificate cannot be read.
    #[error("certificate_path_refused")]
    Path,
    /// The current proof was malformed, stale or signed with another key.
    #[error("certificate_proof_refused")]
    Proof,
}

/// The result of signature, lifetime, holder and Noise-key checks.
/// Withdrawal and current grants are separate decisions at admission.
pub struct VerifiedCertificate {
    machine: MachineIdentity,
    digest: blake3::Hash,
    not_after: u64,
}

impl std::fmt::Debug for VerifiedCertificate {
    fn fmt(&self, out: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        out.debug_struct("VerifiedCertificate")
            .field("digest", &self.digest)
            .field("not_after", &self.not_after)
            .finish_non_exhaustive()
    }
}

impl VerifiedCertificate {
    /// The static identity the signed certificate binds.
    #[must_use]
    pub const fn machine(&self) -> MachineIdentity {
        self.machine
    }
    /// A digest identifies the certificate without retaining its payload.
    #[must_use]
    pub const fn digest(&self) -> &blake3::Hash {
        &self.digest
    }
    /// The last valid Unix second, for the open link's expiry fence.
    #[must_use]
    pub const fn not_after(&self) -> u64 {
        self.not_after
    }
}

/// Verifies only with the service key fetched from the trusted API base.
/// Historical capability grants are deliberately never inspected for rights.
///
/// # Errors
/// A named bound, encoding, signature, validity, holder or key refusal.
pub fn verify(
    der: &[u8],
    service_key: &[u8; 32],
    holder: &str,
    offered: MachineIdentity,
    now: u64,
    bound: usize,
) -> Result<VerifiedCertificate, CertificateRefusal> {
    if bound == 0 || der.len() > bound {
        return Err(CertificateRefusal::Bound);
    }
    exact_der(der)?;
    let key = certificate_signing_key(der).map_err(|_| CertificateRefusal::Encoding)?;
    if i128::from(now) < i128::from(key.not_before) {
        return Err(CertificateRefusal::NotYetValid);
    }
    if i128::from(now) > i128::from(key.not_after) {
        return Err(CertificateRefusal::Expired);
    }
    let at = UNIX_EPOCH
        .checked_add(Duration::from_secs(now))
        .ok_or(CertificateRefusal::Encoding)?;
    verify_certificate_chain_at(der, service_key, at.into())
        .map_err(|_| CertificateRefusal::Signature)?;
    let mut oid = LYS_OID_ARC.to_vec();
    oid.push(1);
    let claims = decode_extension(der, &oid)
        .map_err(|_| CertificateRefusal::Claims)?
        .ok_or(CertificateRefusal::Claims)?;
    let claims: serde_json::Value =
        serde_json::from_slice(&claims).map_err(|_| CertificateRefusal::Claims)?;
    if claims["agent"].as_str() != Some(holder) {
        return Err(CertificateRefusal::HolderMismatch);
    }
    if key.public_key != *offered.ed25519_public() {
        return Err(CertificateRefusal::KeyMismatch);
    }
    Ok(VerifiedCertificate {
        machine: offered,
        digest: blake3::hash(der),
        not_after: u64::try_from(key.not_after).map_err(|_| CertificateRefusal::Encoding)?,
    })
}

fn exact_der(der: &[u8]) -> Result<(), CertificateRefusal> {
    let [0x30, first, rest @ ..] = der else {
        return Err(CertificateRefusal::Encoding);
    };
    let (length, header) = if *first < 128 {
        (usize::from(*first), 2)
    } else {
        let digits = usize::from(first & 0x7f);
        if digits == 0 || digits > std::mem::size_of::<usize>() {
            return Err(CertificateRefusal::Encoding);
        }
        let encoded = rest.get(..digits).ok_or(CertificateRefusal::Encoding)?;
        if encoded[0] == 0 {
            return Err(CertificateRefusal::Encoding);
        }
        let length = encoded
            .iter()
            .try_fold(0_usize, |length, byte| {
                length.checked_mul(256)?.checked_add(usize::from(*byte))
            })
            .ok_or(CertificateRefusal::Encoding)?;
        if length < 128 {
            return Err(CertificateRefusal::Encoding);
        }
        (length, 2 + digits)
    };
    if length.checked_add(header) != Some(der.len()) {
        return Err(CertificateRefusal::Encoding);
    }
    Ok(())
}