diff --git a/backend/src/account.rs b/backend/src/account.rs index 1f8d86e91..0c06f2a5f 100644 --- a/backend/src/account.rs +++ b/backend/src/account.rs @@ -1,4 +1,5 @@ -use digest::Digest; +use std::time::SystemTime; + use ed25519_dalek::SecretKey; use openssl::pkey::{PKey, Private}; use openssl::x509::X509; @@ -29,11 +30,11 @@ pub struct AccountInfo { pub root_ca_cert: X509, } impl AccountInfo { - pub fn new(password: &str) -> Result { + pub fn new(password: &str, start_time: SystemTime) -> Result { let server_id = generate_id(); let hostname = generate_hostname(); let root_ca_key = generate_key()?; - let root_ca_cert = make_root_cert(&root_ca_key, &hostname)?; + let root_ca_cert = make_root_cert(&root_ca_key, &hostname, start_time)?; Ok(Self { server_id, hostname, diff --git a/backend/src/context/rpc.rs b/backend/src/context/rpc.rs index abb122d6e..f235572e4 100644 --- a/backend/src/context/rpc.rs +++ b/backend/src/context/rpc.rs @@ -15,6 +15,7 @@ use serde::Deserialize; use sqlx::postgres::PgConnectOptions; use sqlx::PgPool; use tokio::sync::{broadcast, oneshot, Mutex, RwLock}; +use tokio::time::Instant; use tracing::instrument; use super::setup::CURRENT_SECRET; @@ -29,7 +30,7 @@ use crate::install::cleanup::{cleanup_failed, uninstall}; use crate::manager::ManagerMap; use crate::middleware::auth::HashSessionToken; use crate::net::net_controller::NetController; -use crate::net::ssl::SslManager; +use crate::net::ssl::{root_ca_start_time, SslManager}; use crate::net::wifi::WpaCli; use crate::notifications::NotificationManager; use crate::shutdown::Shutdown; @@ -123,6 +124,7 @@ pub struct RpcContextSeed { pub current_secret: Arc, pub client: Client, pub hardware: Hardware, + pub start_time: Instant, } pub struct Hardware { @@ -158,7 +160,7 @@ impl RpcContext { base.dns_bind .as_deref() .unwrap_or(&[SocketAddr::from(([127, 0, 0, 1], 53))]), - SslManager::new(&account)?, + SslManager::new(&account, root_ca_start_time().await?)?, &account.hostname, &account.key, ) @@ -214,6 +216,7 @@ impl RpcContext { .build() .with_kind(crate::ErrorKind::ParseUrl)?, hardware: Hardware { devices, ram }, + start_time: Instant::now(), }); let res = Self(seed.clone()); diff --git a/backend/src/db/model.rs b/backend/src/db/model.rs index 950c6505a..6ce3f8add 100644 --- a/backend/src/db/model.rs +++ b/backend/src/db/model.rs @@ -79,7 +79,7 @@ impl Database { .iter() .map(|x| format!("{x:X}")) .join(":"), - system_start_time: Utc::now().to_rfc3339(), + ntp_synced: false, zram: true, }, package_data: AllPackageData::default(), @@ -125,7 +125,8 @@ pub struct ServerInfo { pub password_hash: String, pub pubkey: String, pub ca_fingerprint: String, - pub system_start_time: String, + #[serde(default)] + pub ntp_synced: bool, #[serde(default)] pub zram: bool, } diff --git a/backend/src/init.rs b/backend/src/init.rs index fdbc41212..0308f684c 100644 --- a/backend/src/init.rs +++ b/backend/src/init.rs @@ -1,7 +1,7 @@ use std::fs::Permissions; use std::os::unix::fs::PermissionsExt; use std::path::Path; -use std::time::Duration; +use std::time::{Duration, SystemTime}; use color_eyre::eyre::eyre; use helpers::NonDetachingJoinHandle; @@ -19,7 +19,6 @@ use crate::install::PKG_ARCHIVE_DIR; use crate::middleware::auth::LOCAL_AUTH_COOKIE_PATH; use crate::prelude::*; use crate::sound::BEP; -use crate::system::time; use crate::util::cpupower::{ current_governor, get_available_governors, set_governor, GOVERNOR_PERFORMANCE, }; @@ -361,15 +360,28 @@ pub async fn init(cfg: &RpcContextConfig) -> Result { } } - let mut warn_time_not_synced = true; - for _ in 0..60 { + let mut time_not_synced = true; + let mut not_made_progress = 0u32; + for _ in 0..1800 { if check_time_is_synchronized().await? { - warn_time_not_synced = false; + time_not_synced = false; break; } + let t = SystemTime::now(); tokio::time::sleep(Duration::from_secs(1)).await; + if t.elapsed() + .map(|t| t > Duration::from_secs_f64(1.1)) + .unwrap_or(true) + { + not_made_progress = 0; + } else { + not_made_progress += 1; + } + if not_made_progress > 30 { + break; + } } - if warn_time_not_synced { + if time_not_synced { tracing::warn!("Timed out waiting for system time to synchronize"); } else { tracing::info!("Syncronized system clock"); @@ -385,7 +397,20 @@ pub async fn init(cfg: &RpcContextConfig) -> Result { backup_progress: None, }; - server_info.system_start_time = time().await?; + server_info.ntp_synced = if time_not_synced { + let db = db.clone(); + tokio::spawn(async move { + while !check_time_is_synchronized().await.unwrap() { + tokio::time::sleep(Duration::from_secs(30)).await; + } + db.mutate(|v| v.as_server_info_mut().as_ntp_synced_mut().ser(&true)) + .await + .unwrap() + }); + false + } else { + true + }; db.mutate(|v| { v.as_server_info_mut().ser(&server_info)?; diff --git a/backend/src/lib.rs b/backend/src/lib.rs index 67f34d785..141ef1780 100644 --- a/backend/src/lib.rs +++ b/backend/src/lib.rs @@ -17,6 +17,9 @@ lazy_static::lazy_static! { ARCH.to_string() } }; + pub static ref SOURCE_DATE: SystemTime = { + std::fs::metadata(std::env::current_exe().unwrap()).unwrap().modified().unwrap() + }; } pub mod account; @@ -62,6 +65,8 @@ pub mod util; pub mod version; pub mod volume; +use std::time::SystemTime; + pub use config::Config; pub use error::{Error, ErrorKind, ResultExt}; use rpc_toolkit::command; diff --git a/backend/src/net/ssl.rs b/backend/src/net/ssl.rs index c2cab3355..ba2f314b9 100644 --- a/backend/src/net/ssl.rs +++ b/backend/src/net/ssl.rs @@ -4,8 +4,8 @@ use std::net::IpAddr; use std::path::Path; use std::time::{SystemTime, UNIX_EPOCH}; - use futures::FutureExt; +use libc::time_t; use openssl::asn1::{Asn1Integer, Asn1Time}; use openssl::bn::{BigNum, MsbOption}; use openssl::ec::{EcGroup, EcKey}; @@ -19,15 +19,22 @@ use tokio::sync::{Mutex, RwLock}; use tracing::instrument; use crate::account::AccountInfo; -use crate::context::{RpcContext}; +use crate::context::RpcContext; use crate::hostname::Hostname; +use crate::init::check_time_is_synchronized; use crate::net::dhcp::ips; use crate::net::keys::{Key, KeyInfo}; - -use crate::{Error, ErrorKind, ResultExt}; +use crate::{Error, ErrorKind, ResultExt, SOURCE_DATE}; static CERTIFICATE_VERSION: i32 = 2; // X509 version 3 is actually encoded as '2' in the cert because fuck you. +fn unix_time(time: SystemTime) -> time_t { + time.duration_since(UNIX_EPOCH) + .map(|d| d.as_secs() as time_t) + .or_else(|_| UNIX_EPOCH.elapsed().map(|d| -(d.as_secs() as time_t))) + .unwrap_or_default() +} + #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)] pub struct CertPair { pub ed25519: X509, @@ -57,9 +64,13 @@ impl CertPair { }), ); if cert - .not_after() - .compare(Asn1Time::days_from_now(30)?.as_ref())? - == Ordering::Greater + .not_before() + .compare(Asn1Time::days_from_now(0)?.as_ref())? + == Ordering::Less + && cert + .not_after() + .compare(Asn1Time::days_from_now(30)?.as_ref())? + == Ordering::Greater && ips.is_superset(&ip) { return Ok(cert.clone()); @@ -82,6 +93,14 @@ impl CertPair { } } +pub async fn root_ca_start_time() -> Result { + Ok(if check_time_is_synchronized().await? { + SystemTime::now() + } else { + *SOURCE_DATE + }) +} + #[derive(Debug)] pub struct SslManager { hostname: Hostname, @@ -91,9 +110,13 @@ pub struct SslManager { cert_cache: RwLock>, } impl SslManager { - pub fn new(account: &AccountInfo) -> Result { + pub fn new(account: &AccountInfo, start_time: SystemTime) -> Result { let int_key = generate_key()?; - let int_cert = make_int_cert((&account.root_ca_key, &account.root_ca_cert), &int_key)?; + let int_cert = make_int_cert( + (&account.root_ca_key, &account.root_ca_cert), + &int_key, + start_time, + )?; Ok(Self { hostname: account.hostname.clone(), root_cert: account.root_ca_cert.clone(), @@ -162,14 +185,20 @@ pub fn generate_key() -> Result, Error> { } #[instrument(skip_all)] -pub fn make_root_cert(root_key: &PKey, hostname: &Hostname) -> Result { +pub fn make_root_cert( + root_key: &PKey, + hostname: &Hostname, + start_time: SystemTime, +) -> Result { let mut builder = X509Builder::new()?; builder.set_version(CERTIFICATE_VERSION)?; - let embargo = Asn1Time::days_from_now(0)?; + let unix_start_time = unix_time(start_time); + + let embargo = Asn1Time::from_unix(unix_start_time)?; builder.set_not_before(&embargo)?; - let expiration = Asn1Time::days_from_now(3650)?; + let expiration = Asn1Time::from_unix(unix_start_time + (10 * 365 * 86400))?; builder.set_not_after(&expiration)?; builder.set_serial_number(&*rand_serial()?)?; @@ -216,14 +245,17 @@ pub fn make_root_cert(root_key: &PKey, hostname: &Hostname) -> Result, &X509), applicant: &PKey, + start_time: SystemTime, ) -> Result { let mut builder = X509Builder::new()?; builder.set_version(CERTIFICATE_VERSION)?; - let embargo = Asn1Time::days_from_now(0)?; + let unix_start_time = unix_time(start_time); + + let embargo = Asn1Time::from_unix(unix_start_time)?; builder.set_not_before(&embargo)?; - let expiration = Asn1Time::days_from_now(3650)?; + let expiration = Asn1Time::from_unix(unix_start_time + (10 * 365 * 86400))?; builder.set_not_after(&expiration)?; builder.set_serial_number(&*rand_serial()?)?; @@ -346,14 +378,7 @@ pub fn make_leaf_cert( let mut builder = X509Builder::new()?; builder.set_version(CERTIFICATE_VERSION)?; - let embargo = Asn1Time::from_unix( - SystemTime::now() - .duration_since(UNIX_EPOCH) - .map(|d| d.as_secs() as i64) - .or_else(|_| UNIX_EPOCH.elapsed().map(|d| -(d.as_secs() as i64))) - .unwrap_or_default() - - 86400, - )?; + let embargo = Asn1Time::from_unix(unix_time(SystemTime::now()) - 86400)?; builder.set_not_before(&embargo)?; // Google Apple and Mozilla reject certificate horizons longer than 397 days diff --git a/backend/src/setup.rs b/backend/src/setup.rs index f9e897d01..64c324095 100644 --- a/backend/src/setup.rs +++ b/backend/src/setup.rs @@ -31,6 +31,7 @@ use crate::disk::REPAIR_DISK_PATH; use crate::hostname::Hostname; use crate::init::{init, InitResult}; use crate::middleware::encrypt::EncryptedWire; +use crate::net::ssl::root_ca_start_time; use crate::prelude::*; use crate::util::io::{dir_copy, dir_size, Counter}; use crate::{Error, ErrorKind, ResultExt}; @@ -378,7 +379,7 @@ async fn fresh_setup( ctx: &SetupContext, embassy_password: &str, ) -> Result<(Hostname, OnionAddressV3, X509), Error> { - let account = AccountInfo::new(embassy_password)?; + let account = AccountInfo::new(embassy_password, root_ca_start_time().await?)?; let sqlite_pool = ctx.secret_store().await?; account.save(&sqlite_pool).await?; sqlite_pool.close().await; diff --git a/backend/src/system.rs b/backend/src/system.rs index aee32b50a..249ade9c3 100644 --- a/backend/src/system.rs +++ b/backend/src/system.rs @@ -1,6 +1,7 @@ use std::fmt; use chrono::Utc; +use clap::ArgMatches; use color_eyre::eyre::eyre; use futures::FutureExt; use rpc_toolkit::command; @@ -84,9 +85,65 @@ pub async fn zram(#[context] ctx: RpcContext, #[arg] enable: bool) -> Result<(), Ok(()) } -#[command] -pub async fn time() -> Result { - Ok(Utc::now().to_rfc3339()) +#[derive(Serialize, Deserialize)] +pub struct TimeInfo { + now: String, + uptime: u64, +} + +fn display_time(arg: TimeInfo, matches: &ArgMatches) { + use std::fmt::Write; + + use prettytable::*; + + if matches.is_present("format") { + return display_serializable(arg, matches); + } + + let days = arg.uptime / (24 * 60 * 60); + let days_s = arg.uptime % (24 * 60 * 60); + let hours = days_s / (60 * 60); + let hours_s = arg.uptime % (60 * 60); + let minutes = hours_s / 60; + let seconds = arg.uptime % 60; + let mut uptime_string = String::new(); + if days > 0 { + write!(&mut uptime_string, "{days} days").unwrap(); + } + if hours > 0 { + if !uptime_string.is_empty() { + uptime_string += ", "; + } + write!(&mut uptime_string, "{hours} hours").unwrap(); + } + if minutes > 0 { + if !uptime_string.is_empty() { + uptime_string += ", "; + } + write!(&mut uptime_string, "{minutes} minutes").unwrap(); + } + if !uptime_string.is_empty() { + uptime_string += ", "; + } + write!(&mut uptime_string, "{seconds} seconds").unwrap(); + + let mut table = Table::new(); + table.add_row(row![bc -> "NOW", &arg.now]); + table.add_row(row![bc -> "UPTIME", &uptime_string]); + table.print_tty(false).unwrap(); +} + +#[command(display(display_time))] +pub async fn time( + #[context] ctx: RpcContext, + #[allow(unused_variables)] + #[arg(long = "format")] + format: Option, +) -> Result { + Ok(TimeInfo { + now: Utc::now().to_rfc3339(), + uptime: ctx.start_time.elapsed().as_secs(), + }) } #[command( diff --git a/libs/helpers/src/rsync.rs b/libs/helpers/src/rsync.rs index c09ac3d64..1ac24c8b2 100644 --- a/libs/helpers/src/rsync.rs +++ b/libs/helpers/src/rsync.rs @@ -71,7 +71,7 @@ impl Rsync { cmd.arg(format!("--exclude={}", exclude)); } let mut command = cmd - .arg("-acAXH") + .arg("-actAXH") .arg("--info=progress2") .arg("--no-inc-recursive") .arg(src.as_ref())