diff --git a/src/Cargo.lock b/src/Cargo.lock index 69427a126..cf4a45f23 100644 --- a/src/Cargo.lock +++ b/src/Cargo.lock @@ -1910,6 +1910,13 @@ dependencies = [ "zeroize", ] +[[package]] +name = "qos_clock" +version = "0.6.1" +dependencies = [ + "qos_system", +] + [[package]] name = "qos_core" version = "0.6.1" @@ -2052,6 +2059,13 @@ dependencies = [ "qos_p256", ] +[[package]] +name = "qos_system" +version = "0.1.0" +dependencies = [ + "libc", +] + [[package]] name = "qos_test_primitives" version = "0.6.1" diff --git a/src/Cargo.toml b/src/Cargo.toml index 42a5c7eef..641c13b41 100644 --- a/src/Cargo.toml +++ b/src/Cargo.toml @@ -3,6 +3,7 @@ members = [ "integration", "qos_bridge", "qos_client", + "qos_clock", "qos_core", "qos_crypto", "qos_host", @@ -21,6 +22,7 @@ default-members = [ "integration", "qos_bridge", "qos_client", + "qos_clock", "qos_core", "qos_crypto", "qos_host", @@ -103,6 +105,7 @@ zeroize = { version = "1.8", default-features = false } integration = { path = "integration" } qos_host = { path = "qos_host", default-features = false } qos_client = { path = "qos_client", version = "0.6.1", default-features = false } +qos_clock = { path = "qos_clock", version = "0.6.1", default-features = false } qos_core = { path = "qos_core", version = "0.6.1", default-features = false } qos_crypto = { path = "qos_crypto", version = "0.6.1", default-features = false } qos_hex = { path = "qos_hex", version = "0.6.1", default-features = false } diff --git a/src/qos_aws/src/lib.rs b/src/qos_aws/src/lib.rs index e8398a62f..6eecbb6c6 100644 --- a/src/qos_aws/src/lib.rs +++ b/src/qos_aws/src/lib.rs @@ -1,4 +1,6 @@ -use qos_system::{check_hwrng, dmesg, poweroff}; +use qos_system::{ + check_clocksource, check_hwrng, detect_ptp_clock_device, dmesg, poweroff, +}; /// Signal to Nitro hypervisor that booting was successful fn nitro_heartbeat() { @@ -38,4 +40,23 @@ pub fn init_platform() { poweroff(); } }; + + match check_clocksource("kvm-clock") { + Ok(()) => dmesg("Validated clock source is kvm-clock".to_string()), + Err(e) => { + eprintln!("{}", e); + dmesg(format!("Clock source validation failed: {}", e.message)); + poweroff(); + } + }; + + match detect_ptp_clock_device() { + Some(path) => dmesg(format!( + "PTP hardware clock device is visible at {path}; synchronization is not configured by init" + )), + None => dmesg( + "No PTP hardware clock device is exposed to the enclave; synchronization is not configured by init" + .to_string(), + ), + } } diff --git a/src/qos_clock/Cargo.toml b/src/qos_clock/Cargo.toml new file mode 100644 index 000000000..91a92cb70 --- /dev/null +++ b/src/qos_clock/Cargo.toml @@ -0,0 +1,18 @@ +[package] +name = "qos_clock" +version.workspace = true +edition.workspace = true +rust-version.workspace = true +license.workspace = true +description = "Clock abstractions for QuorumOS" +repository = "https://github.com/tkhq/qos" +keywords = ["quorumos", "clock", "time"] +categories = ["os"] + +[lints] +workspace = true + +[dependencies] + +[target.'cfg(target_os = "linux")'.dependencies] +qos_system = { path = "../qos_system" } diff --git a/src/qos_clock/src/lib.rs b/src/qos_clock/src/lib.rs new file mode 100644 index 000000000..52e2c7a10 --- /dev/null +++ b/src/qos_clock/src/lib.rs @@ -0,0 +1,57 @@ +//! Wall-clock access for Unix time inside QOS. + +use std::{fmt, time::SystemTime}; + +/// Error returned when reading wall-clock time fails or overflows `u64`. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum WallClockError { + /// Converting between time units failed. + TimeConversionFailed, + /// The system clock was before the Unix epoch. + TimeBeforeUnixEpoch, + /// The returned value did not fit in `u64`. + TimeOverflow, +} + +impl fmt::Display for WallClockError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::TimeConversionFailed => { + write!(f, "system clock conversion failed") + } + Self::TimeBeforeUnixEpoch => { + write!(f, "system clock was before the Unix epoch") + } + Self::TimeOverflow => { + write!(f, "system clock value overflowed u64") + } + } + } +} + +/// Source of Unix wall-clock time for expiry and freshness checks. +pub trait WallClock: Send + Sync { + /// Returns Unix time in whole milliseconds. + fn unix_time_millis(&self) -> Result; + + /// Returns Unix time in whole seconds. + fn unix_time_seconds(&self) -> Result { + self.unix_time_millis()? + .checked_div(1_000) + .ok_or(WallClockError::TimeConversionFailed) + } +} + +/// Wall clock backed by the host operating system. +pub struct SystemWallClock; + +impl WallClock for SystemWallClock { + fn unix_time_millis(&self) -> Result { + let duration = SystemTime::now() + .duration_since(SystemTime::UNIX_EPOCH) + .map_err(|_| WallClockError::TimeBeforeUnixEpoch)?; + + u64::try_from(duration.as_millis()) + .map_err(|_| WallClockError::TimeOverflow) + } +} diff --git a/src/qos_system/src/lib.rs b/src/qos_system/src/lib.rs index 5a48bdc7b..7bd3dfbd0 100644 --- a/src/qos_system/src/lib.rs +++ b/src/qos_system/src/lib.rs @@ -8,6 +8,10 @@ use std::{ use libc::{c_int, c_ulong, c_void}; +const HWRNG_CURRENT_PATH: &str = "/sys/class/misc/hw_random/rng_current"; +const CLOCKSOURCE_CURRENT_PATH: &str = + "/sys/devices/system/clocksource/clocksource0/current_clocksource"; + #[derive(Debug)] pub struct SystemError { pub message: String, @@ -168,6 +172,44 @@ pub fn check_hwrng(rng_expected: &str) -> Result<(), SystemError> { Ok(()) } +fn read_trimmed(path: &str) -> Result { + use std::fs::read_to_string; + + read_to_string(path) + .map(|value| value.trim().to_string()) + .map_err(|_| SystemError { message: format!("Failed to read {path}") }) +} + +/// Verify the expected clock source is active. +pub fn check_clocksource(clock_expected: &str) -> Result<(), SystemError> { + let clock_current = read_trimmed(CLOCKSOURCE_CURRENT_PATH)?; + if clock_expected != clock_current { + return Err(SystemError { + message: format!( + "Clock source was {} instead of {}", + clock_current, clock_expected + ), + }); + } + + Ok(()) +} + +/// Detect whether a PTP hardware clock device is exposed in the enclave. +/// +/// AWS documents both `/dev/ptp` and the stable `/dev/ptp_ena` +/// symlink for supported instances: +/// https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/configure-ec2-ntp.html +pub fn detect_ptp_clock_device() -> Option<&'static str> { + for path in ["/dev/ptp_ena", "/dev/ptp0"] { + if File::open(path).is_ok() { + return Some(path); + } + } + + None +} + #[cfg(target_env = "musl")] type IoctlNumType = ::libc::c_int; #[cfg(not(target_env = "musl"))]