scosh-core 0.1.0

Portable session state and terminal event core for scosh
Documentation
//! Semantic bootstrap boundary for host-owned SSH adapters.
//!
//! The core verifies the binding and consumes the capability. It neither
//! invokes an SSH executable nor accepts private-key bytes or wire frames.

use std::{
    fmt,
    time::{Duration, Instant},
};

use crate::errors::SdkError;

const CAPABILITY_LEN: usize = 32;
const SPKI_DIGEST_LEN: usize = 32;

#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum BootstrapPurpose {
    Attach,
    Recover,
}

/// An opaque process owner. The value is intentionally not serializable.
#[derive(Clone, Copy, Eq, Hash, PartialEq)]
pub struct ProcessOwner([u8; 16]);

impl fmt::Debug for ProcessOwner {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        f.write_str("ProcessOwner(<opaque>)")
    }
}

impl ProcessOwner {
    /// Create a fresh owner nonce for one live client process.
    pub fn generate() -> Result<Self, SdkError> {
        let mut bytes = [0_u8; 16];
        getrandom::fill(&mut bytes).map_err(|_| SdkError::Authentication)?;
        Ok(Self(bytes))
    }

    pub(crate) const fn from_test_bytes(bytes: [u8; 16]) -> Self {
        Self(bytes)
    }
}

/// A verified data-plane identity. Only a host adapter that completed its
/// OpenSSH host-key check may construct one through the crate-private helper.
#[derive(Clone, Copy, Eq, PartialEq)]
pub struct VerifiedServer([u8; SPKI_DIGEST_LEN]);

impl fmt::Debug for VerifiedServer {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        f.write_str("VerifiedServer(<spki-redacted>)")
    }
}

impl VerifiedServer {
    pub(crate) const fn from_test_digest(digest: [u8; SPKI_DIGEST_LEN]) -> Self {
        Self(digest)
    }
}

#[derive(Clone, Copy, Eq, PartialEq)]
pub struct BootstrapCapability([u8; CAPABILITY_LEN]);

impl fmt::Debug for BootstrapCapability {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        f.write_str("BootstrapCapability(<redacted>)")
    }
}

impl BootstrapCapability {
    pub(crate) const fn from_test_bytes(bytes: [u8; CAPABILITY_LEN]) -> Self {
        Self(bytes)
    }
}

/// Result returned by the host-owned SSH adapter after authentication and
/// strict host verification. No bearer material is exposed to the public API.
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct BootstrapGrant {
    owner: ProcessOwner,
    epoch: u64,
    purpose: BootstrapPurpose,
    server: VerifiedServer,
    capability: BootstrapCapability,
    expires_at: Instant,
}

impl BootstrapGrant {
    pub(crate) const fn from_verified(
        owner: ProcessOwner,
        epoch: u64,
        purpose: BootstrapPurpose,
        server: VerifiedServer,
        capability: BootstrapCapability,
        expires_at: Instant,
    ) -> Self {
        Self {
            owner,
            epoch,
            purpose,
            server,
            capability,
            expires_at,
        }
    }
}

/// One-use gate for a verified bootstrap result.
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct BootstrapGate {
    owner: ProcessOwner,
    epoch: u64,
    purpose: BootstrapPurpose,
    expected_server: VerifiedServer,
    consumed: bool,
    zero_rtt: bool,
}

impl BootstrapGate {
    pub(crate) const fn new(
        owner: ProcessOwner,
        epoch: u64,
        purpose: BootstrapPurpose,
        expected_server: VerifiedServer,
    ) -> Self {
        Self {
            owner,
            epoch,
            purpose,
            expected_server,
            consumed: false,
            zero_rtt: false,
        }
    }

    /// QUIC early data is deliberately disabled for capability delivery.
    pub const fn zero_rtt_enabled(self) -> bool {
        self.zero_rtt
    }

    pub fn consume(&mut self, grant: BootstrapGrant, now: Instant) -> Result<(), SdkError> {
        if self.consumed {
            return Err(SdkError::CapabilityReplayed);
        }
        if grant.expires_at <= now {
            return Err(SdkError::CapabilityExpired);
        }
        if grant.owner != self.owner || grant.epoch != self.epoch || grant.purpose != self.purpose {
            return Err(SdkError::CapabilityBindingMismatch);
        }
        if grant.server != self.expected_server {
            return Err(SdkError::HostKeyRejected);
        }
        // Keep the capability in the core-owned gate only for the duration of
        // this check. The transport adapter receives no raw bytes here.
        let _ = grant.capability;
        self.consumed = true;
        Ok(())
    }
}

/// Bounded semantic request sent to a host bootstrap adapter.
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct BootstrapRequest {
    pub purpose: BootstrapPurpose,
    pub epoch: u64,
    pub timeout: Duration,
}

impl BootstrapRequest {
    pub const fn new(purpose: BootstrapPurpose, epoch: u64, timeout: Duration) -> Self {
        Self {
            purpose,
            epoch,
            timeout,
        }
    }
}

/// Deterministic constructors used by conformance tests. Hidden from normal
/// API documentation; production adapters receive grants from their host
/// bootstrap implementation instead.
#[doc(hidden)]
pub mod test_support {
    use super::*;

    pub fn owner(value: u8) -> ProcessOwner {
        ProcessOwner::from_test_bytes([value; 16])
    }

    pub fn server(value: u8) -> VerifiedServer {
        VerifiedServer::from_test_digest([value; SPKI_DIGEST_LEN])
    }

    pub fn grant(
        owner: ProcessOwner,
        epoch: u64,
        purpose: BootstrapPurpose,
        server: VerifiedServer,
        value: u8,
        expires_at: Instant,
    ) -> BootstrapGrant {
        BootstrapGrant::from_verified(
            owner,
            epoch,
            purpose,
            server,
            BootstrapCapability::from_test_bytes([value; CAPABILITY_LEN]),
            expires_at,
        )
    }

    pub fn gate(
        owner: ProcessOwner,
        epoch: u64,
        purpose: BootstrapPurpose,
        server: VerifiedServer,
    ) -> BootstrapGate {
        BootstrapGate::new(owner, epoch, purpose, server)
    }
}