roughtime 0.1.0

A no_std-capable Roughtime secure time-sync client with pluggable crypto backends
Documentation
//! The [`SystemClock`] abstraction over reading/setting a system wall clock.
//!
//! This module has no OS or `std` dependency, so bare-metal/kernel consumers can implement
//! [`SystemClock`] themselves (e.g. against a hypervisor RTC) without pulling in `libc` or
//! enabling the `os-clock` feature. The concrete Linux implementation lives in
//! [`crate::os_clock::linux`], behind the `os-clock` feature.

use crate::error::Error;
use crate::floor::EFFECTIVE_FLOOR_SECS;

/// A point in time expressed as whole seconds and nanoseconds since the Unix epoch.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Time {
    /// Whole seconds since the Unix epoch.
    pub secs: u64,
    /// Nanoseconds within the second.
    pub nanos: u32,
}

/// Errors from reading or setting the system/OS clock.
///
/// These represent normal, expected outcomes (missing privilege, unsupported platform) — a
/// [`SystemClock`] implementation must never panic on account of them.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum ClockError {
    /// The calling process lacks the privilege to set the clock (e.g. missing `CAP_SYS_TIME`).
    PermissionDenied,
    /// This target/platform has no supported clock implementation.
    Unsupported,
    /// The underlying OS call failed for another reason, carrying its raw error code.
    Os(i32),
}

/// Abstraction over reading/setting a system wall clock.
///
/// Kernel/bare-metal consumers implement this trait against their own environment (e.g. a
/// hypervisor-provided RTC) instead of using the bundled Linux implementation.
pub trait SystemClock {
    /// Reads the current wall-clock time.
    ///
    /// # Errors
    ///
    /// Returns a [`ClockError`] if the clock cannot be read.
    fn get() -> Result<Time, ClockError>;

    /// Sets the wall-clock time.
    ///
    /// # Errors
    ///
    /// Returns a [`ClockError`] — never panics — on missing privilege or an unsupported target.
    fn set(time: Time) -> Result<(), ClockError>;
}

/// Reads `C`'s current time and, if it predates the crate's anti-rollback floor, sets it
/// forward to the floor.
///
/// This generalizes the common "enforce a time floor on the OS clock" pattern used by
/// bootloaders and kernels that need a sane wall clock before performing cryptographic
/// verification (e.g. of TLS certificates) that depends on the current time.
///
/// # Errors
///
/// Returns [`Error::ClockUnavailable`] if the clock cannot be read or set.
pub fn enforce_floor<C: SystemClock>() -> Result<(), Error> {
    let now = C::get().map_err(Error::ClockUnavailable)?;
    if now.secs < EFFECTIVE_FLOOR_SECS {
        C::set(Time {
            secs: EFFECTIVE_FLOOR_SECS,
            nanos: 0,
        })
        .map_err(Error::ClockUnavailable)?;
    }
    Ok(())
}

#[cfg(test)]
mod tests {
    #![allow(clippy::unwrap_used)]

    extern crate std;

    use core::cell::Cell;

    use std::thread_local;

    use super::{ClockError, EFFECTIVE_FLOOR_SECS, SystemClock, Time, enforce_floor};
    use crate::Error;

    // A `SystemClock` under test must be a bare type (no instance state, per the trait's
    // `fn(...) -> ...` shape), so scenario state is smuggled in through `thread_local!`.
    thread_local! {
        static CURRENT: Cell<u64> = const { Cell::new(0) };
        static SET_TO: Cell<Option<u64>> = const { Cell::new(None) };
        static GET_FAILS: Cell<bool> = const { Cell::new(false) };
        static SET_FAILS: Cell<bool> = const { Cell::new(false) };
    }

    struct MockClock;

    impl SystemClock for MockClock {
        fn get() -> Result<Time, ClockError> {
            if GET_FAILS.get() {
                return Err(ClockError::Unsupported);
            }
            Ok(Time {
                secs: CURRENT.get(),
                nanos: 0,
            })
        }

        fn set(time: Time) -> Result<(), ClockError> {
            if SET_FAILS.get() {
                return Err(ClockError::PermissionDenied);
            }
            SET_TO.set(Some(time.secs));
            Ok(())
        }
    }

    fn reset() {
        CURRENT.set(0);
        SET_TO.set(None);
        GET_FAILS.set(false);
        SET_FAILS.set(false);
    }

    #[test]
    fn leaves_a_clock_already_past_the_floor_untouched() {
        reset();
        CURRENT.set(EFFECTIVE_FLOOR_SECS + 100);
        enforce_floor::<MockClock>().unwrap();
        assert_eq!(SET_TO.get(), None);
    }

    #[test]
    fn ratchets_a_clock_before_the_floor_forward() {
        reset();
        CURRENT.set(EFFECTIVE_FLOOR_SECS - 100);
        enforce_floor::<MockClock>().unwrap();
        assert_eq!(SET_TO.get(), Some(EFFECTIVE_FLOOR_SECS));
    }

    #[test]
    fn propagates_a_get_failure() {
        reset();
        GET_FAILS.set(true);
        let err = enforce_floor::<MockClock>().unwrap_err();
        assert_eq!(err, Error::ClockUnavailable(ClockError::Unsupported));
    }

    #[test]
    fn propagates_a_set_failure() {
        reset();
        CURRENT.set(EFFECTIVE_FLOOR_SECS - 100);
        SET_FAILS.set(true);
        let err = enforce_floor::<MockClock>().unwrap_err();
        assert_eq!(err, Error::ClockUnavailable(ClockError::PermissionDenied));
    }
}