diff --git a/citadel-realms/src/help.rs b/citadel-realms/src/help.rs index d0785d7..e8829dd 100644 --- a/citadel-realms/src/help.rs +++ b/citadel-realms/src/help.rs @@ -31,7 +31,6 @@ pub fn help_panel(screen: usize) -> impl View { .child(help_header("RealmsFS Image Commands")) .child(DummyView) .child(help_item("n", "Create new RealmFS as fork of selected image.")) - .child(help_item("s", "Seal selected RealmFS image.")) .child(help_item("u", "Open shell to update selected RealmFS image.")) .child(help_item(".", "Toggle display of system RealmFS images.")) .child(DummyView) diff --git a/citadel-realms/src/realm/mod.rs b/citadel-realms/src/realm/mod.rs index 2e6de44..ae91230 100644 --- a/citadel-realms/src/realm/mod.rs +++ b/citadel-realms/src/realm/mod.rs @@ -219,11 +219,9 @@ impl <'a> RealmInfoRender <'a> { None => return false, }; - if let Some(activation) = realmfs.activation() { - if activation.is_mountpoint(&mountpoint) { - return false; - } - }; + if realmfs.is_activated() && realmfs.mountpoint() == mountpoint { + return false; + } true } diff --git a/citadel-realms/src/realmfs/actions.rs b/citadel-realms/src/realmfs/actions.rs index 494f418..1d791fb 100644 --- a/citadel-realms/src/realmfs/actions.rs +++ b/citadel-realms/src/realmfs/actions.rs @@ -11,12 +11,12 @@ use crate::item_list::ItemList; use crate::realmfs::fork_dialog::ForkDialog; use crate::notes::NotesDialog; -type ActionCallback = Fn(&RealmFS)+Send+Sync; +type ActionCallback = dyn Fn(&RealmFS)+Send+Sync; #[derive(Clone)] pub struct RealmFSAction { realmfs: RealmFS, - sink: Sender>, + sink: Sender>, callback: Arc } @@ -37,9 +37,7 @@ impl RealmFSAction { } EventResult::with_cb(|s| { - let action = RealmFSAction::new(s, Arc::new(|r| { - Self::log_fail("deactivating realmfs", || r.deactivate()); - })); + let action = RealmFSAction::new(s, Arc::new(|r| { r.deactivate(); })); if action.realmfs.is_in_use() { s.add_layer(Dialog::info("RealmFS is in use and cannot be deactivated").title("Cannot Deactivate")); @@ -65,45 +63,6 @@ impl RealmFSAction { EventResult::Consumed(None) } - pub fn seal_realmfs(sealed: bool) -> EventResult { - if sealed { - return EventResult::Consumed(None); - } - - EventResult::with_cb(|s| { - let action = RealmFSAction::new(s, Arc::new(|r| { - Self::log_fail("sealing realmfs", || r.seal(None)); - })); - if action.realmfs.is_sealed() { - return; - } - if action.realmfs.is_activated() { - s.add_layer(Dialog::info("Cannot seal realmfs because it is currently activated. Deactivate first").title("Cannot Seal")); - return; - } - if !action.realmfs.has_sealing_keys() { - s.add_layer(Dialog::info("Cannot seal realmfs because no keys are available to sign image.").title("Cannot Seal")); - return; - } - let title = "Seal RealmFS?"; - let msg = format!("Would you like to seal RealmFS '{}'?", action.realmfs.name()); - let dialog = confirm_dialog(title, &msg, move |_| action.run_action()); - s.add_layer(dialog); - }) - } - - pub fn unseal_realmfs(sealed: bool) -> EventResult { - if !sealed { - return EventResult::Consumed(None); - } - let title = "Unseal RealmFS?"; - let msg = "Do you want to unseal '$REALMFS'"; - - Self::confirm_action(title, msg, |r| { - Self::log_fail("unsealing realmfs", || r.unseal()); - }) - } - pub fn delete_realmfs(user: bool) -> EventResult { if !user { return EventResult::Consumed(None); @@ -112,8 +71,7 @@ impl RealmFSAction { let msg = "Are you sure you want to delete '$REALMFS'?"; let cb = Self::wrap_callback(|r| { - let manager = r.manager(); - if let Err(e) = manager.delete_realmfs(r) { + if let Err(e) = r.delete() { warn!("error deleting realmfs: {}", e); } }); @@ -210,19 +168,6 @@ impl RealmFSAction { Arc::new(callback) } - pub fn confirm_action(title: &'static str, message: &'static str, callback: F) -> EventResult - where F: Fn(&RealmFS), F: 'static + Send+Sync, - { - let callback = Arc::new(callback); - - EventResult::with_cb(move |s| { - let action = RealmFSAction::new(s, callback.clone()); - let message = message.replace("$REALMFS", action.realmfs.name()); - let dialog = confirm_dialog(title, &message, move |_| action.run_action()); - s.add_layer(dialog); - }) - } - fn new(s: &mut Cursive, callback: Arc) -> RealmFSAction { let realmfs = Self::current_realmfs(s); let sink = s.cb_sink().clone(); diff --git a/citadel-realms/src/realmfs/mod.rs b/citadel-realms/src/realmfs/mod.rs index 7122943..6726f62 100644 --- a/citadel-realms/src/realmfs/mod.rs +++ b/citadel-realms/src/realmfs/mod.rs @@ -85,8 +85,8 @@ impl ItemListContent for RealmFSListContent { } fn on_event(&mut self, item: Option<&RealmFS>, event: Event) -> EventResult { - let (activated,sealed,user) = item.map(|r| (r.is_activated(), r.is_sealed(), r.is_user_realmfs())) - .unwrap_or((false, false, false)); + let (activated,user) = item.map(|r| (r.is_activated(), r.is_user_realmfs())) + .unwrap_or((false, false)); match event { Event::Key(Key::Enter) => RealmFSAction::activate_realmfs(activated), @@ -96,8 +96,6 @@ impl ItemListContent for RealmFSListContent { Event::Char('r') => RealmFSAction::resize_realmfs(), Event::Char('u') => RealmFSAction::update_realmfs(), Event::Char('n') => RealmFSAction::fork_realmfs(), - Event::Char('s') => RealmFSAction::seal_realmfs(sealed), - Event::Char('S') => RealmFSAction::unseal_realmfs(sealed), Event::Char('e') => RealmFSAction::edit_notes(), Event::Char('.') => { self.show_system = !self.show_system; @@ -129,17 +127,15 @@ impl <'a> RealmFSInfoRender <'a> { fn render_realmfs(&mut self) { let r = self.realmfs; - if r.is_sealed() && r.is_user_realmfs() { - self.heading("Sealed RealmFS"); - } else if r.is_sealed() { - self.heading("System RealmFS"); + if r.is_user_realmfs() { + self.heading("User RealmFS"); } else { - self.heading("Unsealed RealmFS"); + self.heading("System RealmFS"); } self.print(" ").render_name(); - if r.is_sealed() && !r.is_user_realmfs() { + if !r.is_user_realmfs() { self.print(format!(" (channel={})", r.metainfo().channel())); } @@ -171,7 +167,7 @@ impl <'a> RealmFSInfoRender <'a> { match sizes(r) { Ok((free,allocated)) => { - let size = r.metainfo_nblocks(); + let size = r.metainfo().nblocks() + 1; let used = size - free; let used_percent = (used as f64 * 100.0) / (size as f64); @@ -203,14 +199,12 @@ impl <'a> RealmFSInfoRender <'a> { } fn render_activation(&mut self) { + if !self.realmfs.is_activated() { + return; + } - let activation = match self.realmfs.activation() { - Some(activation) => activation, - None => return, - }; - - let realms = self.realmfs.manager() - .realms_for_activation(&activation); + let mountpoint = self.realmfs.mountpoint(); + let realms = self.realmfs.manager().realms_for_mountpoint(&mountpoint); if !realms.is_empty() { self.heading("In Use") @@ -225,25 +219,18 @@ impl <'a> RealmFSInfoRender <'a> { self.heading("Active").newlines(2); } + let devname = self.realmfs.mountpoint().verity_device(); self.print(" Device : ") .dim_style() - .println(activation.device()) + .println(devname) .pop(); - let mount = if activation.mountpoint_rw().is_some() { "Mounts" } else { "Mount "}; - self.print(format!(" {} : ", mount)) + self.print(" Mount : ") .dim_style() - .print(format!("{}", activation.mountpoint())) + .print(format!("{}", self.realmfs.mountpoint())) .pop() .newline(); - if let Some(rw) = activation.mountpoint_rw() { - self.print(" ") - .dim_style() - .print(format!("{}", rw)) - .pop() - .newline(); - } self.newline(); } diff --git a/citadel-realms/src/ui.rs b/citadel-realms/src/ui.rs index 898885b..e55b8f0 100644 --- a/citadel-realms/src/ui.rs +++ b/citadel-realms/src/ui.rs @@ -10,11 +10,10 @@ use crate::theme::{ThemeHandler, ThemeChooser}; use crate::terminal::TerminalTools; use crate::logview::TextContentLogOutput; use std::sync::{Arc,RwLock, RwLockReadGuard, RwLockWriteGuard}; -use std::{mem, io}; +use std::mem; use crate::item_list::ItemList; use crate::realm::RealmListContent; use crate::realmfs::RealmFSListContent; -use std::io::Write; #[derive(Clone)] pub enum DeferredAction { @@ -325,54 +324,6 @@ impl RealmUI { } fn run_realmfs_update(&self, realmfs: &RealmFS) -> Result<()> { - self.with_termtools(|tt| { - tt.apply_base16_by_slug("icy"); - tt.set_window_title(format!("Update {}-realmfs.img", realmfs.name())); - tt.clear_screen(); - }); - - let mut update = realmfs.update(); - update.setup()?; - - if let Some(size) = update.auto_resize_size() { - println!("Resizing image to {} gb", size.size_in_gb()); - update.apply_resize(size)?; - } - - println!(); - println!("Opening update shell for '{}-realmfs.img'", realmfs.name()); - println!(); - println!("Exit shell with ctrl-d or 'exit' to return to realm manager"); - println!(); - update.open_update_shell()?; - - if realmfs.is_sealed() { - if self.prompt_user("Apply changes?", true)? { - update.apply_update() - } else { - update.cleanup() - } - } else { - update.apply_update()?; - if !realmfs.is_activated() && self.prompt_user("Seal RealmFS?", true)? { - realmfs.seal(None)?; - } - Ok(()) - } + realmfs.interactive_update(Some("icy")) } - - fn prompt_user(&self, prompt: &str, default_y: bool) -> Result { - let yn = if default_y { "(Y/n)" } else { "(y/N)" }; - print!("{} {} : ", prompt, yn); - io::stdout().flush()?; - let mut line = String::new(); - io::stdin().read_line(&mut line)?; - - let yes = match line.trim().chars().next() { - Some(c) => c == 'Y' || c == 'y', - None => default_y, - }; - Ok(yes) - } - } diff --git a/citadel-tool/src/mkimage/build.rs b/citadel-tool/src/mkimage/build.rs index c9d64cf..4130431 100644 --- a/citadel-tool/src/mkimage/build.rs +++ b/citadel-tool/src/mkimage/build.rs @@ -121,7 +121,8 @@ impl UpdateBuilder { let hashfile = self.config.workdir_path(self.verity_filename()); let outfile = self.config.workdir_path("verity-format.out"); - let output = Verity::new(self.image()).generate_initial_hashtree(&hashfile)?; + let verity = Verity::new(self.image())?; + let output = verity.generate_initial_hashtree(&hashfile)?; fs::write(outfile, output.output()) .context("failed to write veritysetup command output to a file")?; diff --git a/citadel-tool/src/realmfs/mod.rs b/citadel-tool/src/realmfs/mod.rs index e6e0c27..c750b44 100644 --- a/citadel-tool/src/realmfs/mod.rs +++ b/citadel-tool/src/realmfs/mod.rs @@ -2,6 +2,7 @@ use clap::App; use clap::ArgMatches; use libcitadel::{Result,RealmFS,Logger,LogLevel}; +use libcitadel::util::is_euid_root; use clap::SubCommand; use clap::AppSettings::*; use clap::Arg; @@ -45,12 +46,6 @@ is the final absolute size of the image.") .help("Name of new image to create") .required(true))) - .subcommand(SubCommand::with_name("seal") - .about("Seal an unsealed RealmFS image") - .arg(Arg::with_name("image") - .help("Path or name of RealmFS image to seal") - .required(true))) - .subcommand(SubCommand::with_name("autoresize") .about("Increase size of RealmFS image if not enough free space remains") .arg(Arg::with_name("image") @@ -85,7 +80,6 @@ is the final absolute size of the image.") ("resize", Some(m)) => resize(m), ("autoresize", Some(m)) => autoresize(m), ("fork", Some(m)) => fork(m), - ("seal", Some(m)) => seal(m), ("update", Some(m)) => update(m), ("activate", Some(m)) => activate(m), ("deactivate", Some(m)) => deactivate(m), @@ -98,7 +92,6 @@ is the final absolute size of the image.") } } - fn realmfs_image(arg_matches: &ArgMatches) -> Result { let image = match arg_matches.value_of("image") { Some(s) => s, @@ -186,28 +179,12 @@ fn fork(arg_matches: &ArgMatches) -> Result<()> { Ok(()) } -fn seal(arg_matches: &ArgMatches) -> Result<()> { - let img = realmfs_image(arg_matches)?; - let img_arg = arg_matches.value_of("image").unwrap(); - - if img.is_sealed() { - info!("RealmFS image {} is already sealed", img_arg); - } else if img.is_activated() { - info!("RealmFS image {} cannot be sealed because it is currently activated", img_arg); - } else { - img.seal(None)?; - } - - Ok(()) -} - fn update(arg_matches: &ArgMatches) -> Result<()> { + if !is_euid_root() { + bail!("RealmFS updates must be run as root"); + } let img = realmfs_image(arg_matches)?; - let mut update = img.update(); - update.setup()?; - update.open_update_shell()?; - update.apply_update()?; - update.cleanup()?; + img.interactive_update(Some("icy"))?; Ok(()) } @@ -215,17 +192,12 @@ fn activate(arg_matches: &ArgMatches) -> Result<()> { let img = realmfs_image(arg_matches)?; let img_arg = arg_matches.value_of("image").unwrap(); - let activation = if let Some(activation) = img.activation() { + if img.is_activated() { info!("RealmFS image {} is already activated", img_arg); - activation } else { - info!("Activating {}", img_arg); - img.activate()? - }; - info!("Read-Only mountpoint: {}", activation.mountpoint()); - if let Some(rw) = activation.mountpoint_rw() { - info!("Read-Write mountpoint: {}", rw); + img.activate()?; } + info!("Mountpoint: {}", img.mountpoint()); Ok(()) } @@ -238,7 +210,7 @@ fn deactivate(arg_matches: &ArgMatches) -> Result<()> { info!("Cannot deactivate RealmFS image {} because it is currently in use", img_arg); } else { info!("Deactivating {}", img_arg); - img.deactivate()?; + img.deactivate(); } Ok(()) } diff --git a/libcitadel/src/header.rs b/libcitadel/src/header.rs index 705725c..4c01c2c 100644 --- a/libcitadel/src/header.rs +++ b/libcitadel/src/header.rs @@ -20,7 +20,7 @@ const METAINFO_OFFSET: usize = 8; const SIGNATURE_LENGTH: usize = 64; /// Maximum amount of space in block for metainfo document -const MAX_METAINFO_LEN: usize = (ImageHeader::HEADER_SIZE - (METAINFO_OFFSET + SIGNATURE_LENGTH)); +const MAX_METAINFO_LEN: usize = ImageHeader::HEADER_SIZE - (METAINFO_OFFSET + SIGNATURE_LENGTH); fn is_valid_status_code(code: u8) -> bool { code <= ImageHeader::STATUS_BAD_META @@ -333,6 +333,12 @@ impl ImageHeader { self.read_u16(6) as usize } + pub fn update_metainfo>(&self, metainfo_bytes: &[u8], signature: &[u8], path: P) -> Result<()> { + self.set_metainfo_bytes(metainfo_bytes)?; + self.set_signature(signature)?; + self.write_header_to(path) + } + pub fn set_metainfo_bytes(&self, bytes: &[u8]) -> Result<()> { let metainfo = MetaInfo::parse_bytes(bytes) .ok_or_else(|| format_err!("Could not parse metainfo bytes as valid metainfo document"))?; @@ -524,8 +530,8 @@ impl MetaInfo { &self.verity_salt } - pub fn verity_tag(&self) -> String { - self.verity_root().chars().take(8).collect() + pub fn verity_tag(&self) -> &str { + &self.verity_root()[..8] } } diff --git a/libcitadel/src/lib.rs b/libcitadel/src/lib.rs index 4f704bc..a55c125 100644 --- a/libcitadel/src/lib.rs +++ b/libcitadel/src/lib.rs @@ -35,7 +35,6 @@ mod realm; pub mod terminal; mod system; - pub use crate::config::OsRelease; pub use crate::blockdev::BlockDev; pub use crate::cmdline::CommandLine; @@ -43,10 +42,10 @@ pub use crate::header::{ImageHeader,MetaInfo}; pub use crate::partition::Partition; pub use crate::resource::ResourceImage; pub use crate::keys::{KeyPair,PublicKey,Signature}; -pub use crate::realmfs::{RealmFS,Mountpoint,Activation}; +pub use crate::realmfs::{RealmFS,Mountpoint}; pub use crate::keyring::{KeyRing,KernelKey}; pub use crate::exec::{Exec,FileRange}; -pub use crate::realmfs::resizer::{ImageResizer,ResizeSize}; +pub use crate::realmfs::resizer::ResizeSize; pub use crate::realm::overlay::RealmOverlay; pub use crate::realm::realm::Realm; pub use crate::realm::config::{RealmConfig,OverlayType,GLOBAL_CONFIG}; diff --git a/libcitadel/src/realm/config.rs b/libcitadel/src/realm/config.rs index c6e1174..caf7de5 100644 --- a/libcitadel/src/realm/config.rs +++ b/libcitadel/src/realm/config.rs @@ -99,9 +99,6 @@ pub struct RealmConfig { pub realmfs: Option, - #[serde(rename="realmfs-write")] - pub realmfs_write: Option, - #[serde(rename="terminal-scheme")] pub terminal_scheme: Option, @@ -205,7 +202,6 @@ impl RealmConfig { extra_bindmounts_ro: None, realm_depends: None, realmfs: Some(DEFAULT_REALMFS.into()), - realmfs_write: Some(false), overlay: Some(DEFAULT_OVERLAY.into()), terminal_scheme: None, netns: None, @@ -235,7 +231,6 @@ impl RealmConfig { realm_depends: None, ephemeral_persistent_dirs: None, realmfs: None, - realmfs_write: None, overlay: None, terminal_scheme: None, netns: None, @@ -381,11 +376,6 @@ impl RealmConfig { self.str_value(|c| c.realmfs.as_ref()).unwrap_or(DEFAULT_REALMFS) } - pub fn realmfs_write(&self) -> bool { - self.bool_value(|c| c.realmfs_write) - } - - /// Name of a terminal color scheme to use in this realm. pub fn terminal_scheme(&self) -> Option<&str> { self.str_value(|c| c.terminal_scheme.as_ref()) diff --git a/libcitadel/src/realm/manager.rs b/libcitadel/src/realm/manager.rs index 84053a3..cd44c55 100644 --- a/libcitadel/src/realm/manager.rs +++ b/libcitadel/src/realm/manager.rs @@ -3,7 +3,7 @@ use std::fs; use std::path::{Path, PathBuf}; use std::sync::{Arc, RwLock, RwLockReadGuard, RwLockWriteGuard}; -use crate::{Mountpoint, Activation,Result, Realms, RealmFS, Realm, util}; +use crate::{Mountpoint, Result, Realms, RealmFS, Realm, util}; use crate::realmfs::realmfs_set::RealmFSSet; use super::systemd::Systemd; @@ -125,14 +125,11 @@ impl RealmManager { self.inner().realms.active(ignore_system) } - /// Return a list of Realms that are using the `activation` - pub fn realms_for_activation(&self, activation: &Activation) -> Vec { + /// Return a list of Realms that are using `mountpoint` + pub fn realms_for_mountpoint(&self, mountpoint: &Mountpoint) -> Vec { self.active_realms(false) .into_iter() - .filter(|r| { - r.realmfs_mountpoint() - .map_or(false, |mp| activation.is_mountpoint(&mp)) - }) + .filter(|r| r.has_mountpoint(mountpoint)) .collect() } @@ -148,30 +145,15 @@ impl RealmManager { self.inner().realmfs_set.by_name(name) } - /// Notify `RealmManager` that `mountpoint` has been released by a - /// `Realm`. + /// If mountpoint is no longer in use by another `Realm` deactivate it. pub fn release_mountpoint(&self, mountpoint: &Mountpoint) { info!("releasing mountpoint: {}", mountpoint); if !mountpoint.is_valid() { warn!("bad mountpoint {} passed to release_mountpoint()", mountpoint); return; } - - if let Some(realmfs) = self.realmfs_by_name(mountpoint.realmfs()) { - if realmfs.release_mountpoint(mountpoint) { - return; - } - } - - if let Some(activation) = Activation::for_mountpoint(mountpoint) { - let active = self.active_mountpoints(); - if let Err(e) = activation.deactivate(&active) { - warn!("error on detached deactivation for {}: {}",activation.device(), e); - } else { - info!("Deactivated detached activation for device {}", activation.device()); - } - } else { - warn!("No activation found for released mountpoint {}", mountpoint); + if !self.realmfs_mountpoint_in_use(mountpoint) { + mountpoint.deactivate(); } } @@ -184,6 +166,12 @@ impl RealmManager { .collect() } + pub fn realmfs_mountpoint_in_use(&self, mountpoint: &Mountpoint) -> bool { + self.active_realms(false) + .iter() + .any(|r| r.has_mountpoint(mountpoint)) + } + pub fn start_boot_realms(&self) -> Result<()> { if let Some(realm) = self.default_realm() { if let Err(e) = self.start_realm(&realm) { @@ -387,7 +375,7 @@ impl RealmManager { if realmfs.is_in_use() { bail!("Cannot delete realmfs because it is in use"); } - realmfs.deactivate()?; + realmfs.deactivate(); if realmfs.is_activated() { bail!("Unable to deactive Realmfs, cannot delete"); } diff --git a/libcitadel/src/realm/realm.rs b/libcitadel/src/realm/realm.rs index 9bd934c..db15edc 100644 --- a/libcitadel/src/realm/realm.rs +++ b/libcitadel/src/realm/realm.rs @@ -6,12 +6,12 @@ use std::os::unix::fs::MetadataExt; use super::overlay::RealmOverlay; -use super::config::{RealmConfig,GLOBAL_CONFIG,OverlayType}; +use super::config::{RealmConfig,GLOBAL_CONFIG}; use super::realms::Realms; use super::systemd::Systemd; -use crate::realmfs::{Mountpoint, Activation}; -use crate::{symlink, util, Result, RealmFS, CommandLine, RealmManager}; +use crate::realmfs::Mountpoint; +use crate::{symlink, util, Result, RealmFS, CommandLine, RealmManager, OverlayType}; const MAX_REALM_NAME_LEN:usize = 128; @@ -138,6 +138,15 @@ impl Realm { &self.name } + // Return true if this `Realm` has an active RealmFS mountpoint and + // that mountpoint matches 'mountpoint' + pub fn has_mountpoint(&self, mountpoint: &Mountpoint) -> bool { + self.realmfs_mountpoint() + .map(|mp| &mp == mountpoint) + .unwrap_or(false) + } + + // Return a `Mountpoint` instance for the RealmFS used by this `Realm` pub fn realmfs_mountpoint(&self) -> Option { symlink::read(self.realmfs_mountpoint_symlink()) .map(Into::into) @@ -204,19 +213,17 @@ impl Realm { /// /// 1) Find the RealmFS for this realm and activate it if not yet activated. /// 2) If this realm is configured to use an overlay, set it up. - /// 3) If the RealmFS is unsealed, choose between ro/rw mountpoints - /// 4) create 'rootfs' symlink in realm run path pointing to rootfs base - /// 5) create 'realmfs-mountpoint' symlink pointing to realmfs mount + /// 3) create 'rootfs' symlink in realm run path pointing to rootfs base + /// 4) create 'realmfs-mountpoint' symlink pointing to realmfs mount /// pub fn setup_rootfs(&self) -> Result { let realmfs = self.get_named_realmfs(self.config().realmfs())?; - let activation = realmfs.activate()?; - let writeable = self.use_writable_mountpoint(&realmfs); - let mountpoint = self.choose_mountpoint(writeable, &activation)?; + realmfs.activate()?; + let mountpoint = realmfs.mountpoint(); let rootfs = match RealmOverlay::for_realm(self) { - Some(ref overlay) if !writeable => overlay.create(mountpoint.path())?, + Some(ref overlay) => overlay.create(mountpoint.path())?, _ => mountpoint.path().to_owned(), }; @@ -227,16 +234,6 @@ impl Realm { Ok(rootfs) } - fn choose_mountpoint<'a>(&self, writeable: bool, activation: &'a Activation) -> Result<&'a Mountpoint> { - if !writeable { - Ok(activation.mountpoint()) - } else if let Some(mountpoint) = activation.mountpoint_rw() { - Ok(mountpoint) - } else { - Err(format_err!("RealmFS activation does not have writable mountpoint as expected")) - } - } - /// Clean up the rootfs created when starting this realm. /// /// 1) If an overlay was created, remove it. @@ -266,13 +263,6 @@ impl Realm { } } - fn use_writable_mountpoint(&self, realmfs: &RealmFS) -> bool { - match realmfs.metainfo().realmfs_owner() { - Some(name) => !realmfs.is_sealed() && name == self.name(), - None => false, - } - } - /// Return named RealmFS instance if it already exists. /// /// Otherwise, create it as a fork of the 'default' image. @@ -385,15 +375,13 @@ impl Realm { result } - /// Return `true` if this realm is configured to use a read-only RealmFS mount. + /// Return `true` if this realm is configured to use a read-only root filesytem. + /// + /// Since either type of overlay will produce a writable root filesystem the + /// rootfs is only read-only if no overlay is configured. + /// pub fn readonly_rootfs(&self) -> bool { - if self.config().overlay() != OverlayType::None { - false - } else if CommandLine::sealed() { - true - } else { - !self.config().realmfs_write() - } + self.config().overlay() == OverlayType::None } /// Return path to root directory as seen by mount namespace inside the realm container @@ -449,7 +437,6 @@ impl Realm { /// is the run path of the realm. pub fn is_current(&self) -> bool { Realms::read_current_realm_symlink() == Some(self.run_path()) - //Realms::current_realm_name().as_ref() == Some(&self.name) } /// Return `true` if this realm is the default realm. diff --git a/libcitadel/src/realmfs/activator.rs b/libcitadel/src/realmfs/activator.rs deleted file mode 100644 index 06d36da..0000000 --- a/libcitadel/src/realmfs/activator.rs +++ /dev/null @@ -1,365 +0,0 @@ -use std::collections::HashSet; -use std::path::Path; - -use crate::{RealmFS, Result, ImageHeader, CommandLine, PublicKey, LoopDevice}; -use crate::realmfs::mountpoint::Mountpoint; -use std::sync::{Arc, RwLock, RwLockReadGuard, RwLockWriteGuard}; -use crate::verity::Verity; - -/// Holds the activation status for a `RealmFS` and provides a thread-safe -/// interface to it. -/// -/// If `state` is `None` then the `RealmFS` is not currently activated. -/// -pub struct ActivationState { - state: RwLock>>, -} - -impl ActivationState { - - pub fn new() -> Self { - let state = RwLock::new(None); - ActivationState { state } - } - - /// Load an unknown activation state for `realmfs` by examining - /// the state of the system to determine if the RealmFS is activated. - pub fn load(&self, realmfs: &RealmFS) { - let activation = if realmfs.is_sealed() { - let header = realmfs.header(); - let activator = VerityActivator::new(realmfs, header); - activator.activation() - } else { - let activator = LoopActivator::new(realmfs); - activator.activation() - }; - *self.state_mut() = activation.map(Arc::new) - } - - /// If currently activated return the corresponding `Activation` instance - /// otherwise return `None` - pub fn get(&self) -> Option> { - self.state().clone() - } - - /// Return `true` if currently activated. - pub fn is_activated(&self) -> bool { - self.state().is_some() - } - - /// Activate `realmfs` or if already activated return current `Activation`. - pub fn activate(&self, realmfs: &RealmFS) -> Result> { - let header = realmfs.header(); - let mut lock = self.state_mut(); - if let Some(ref activation) = *lock { - return Ok(activation.clone()); - } else { - let activation = self._activate(realmfs, header)?; - let activation = Arc::new(activation); - *lock = Some(activation.clone()); - Ok(activation) - } - } - - fn _activate(&self, realmfs: &RealmFS, header: &ImageHeader) -> Result { - if realmfs.is_sealed() { - let activator = VerityActivator::new(realmfs, header); - activator.activate() - } else { - let activator = LoopActivator::new(realmfs); - activator.activate() - } - } - - /// Deactivate `Activation` only if not in use. - /// - /// Returns `true` if state changes from activated to not-activated. - /// - pub fn deactivate(&self, active_set: &HashSet) -> Result { - let mut lock = self.state_mut(); - if let Some(ref activation) = *lock { - if activation.deactivate(active_set)? { - *lock = None; - return Ok(true); - } - } - Ok(false) - } - - /// Return `true` if an `Activation` exists and is currently in-use by some `Realm` - pub fn is_in_use(&self, active_set: &HashSet) -> bool { - self.state() - .as_ref() - .map(|a| a.in_use(active_set)) - .unwrap_or(false) - } - - fn state(&self) -> RwLockReadGuard>> { - self.state.read().unwrap() - } - - fn state_mut(&self) -> RwLockWriteGuard>>{ - self.state.write().unwrap() - } -} - -/// Represents a `RealmFS` in an activated state. The activation can be one of: -/// -/// `Activation::Loop` if the `RealmFS` is unsealed -/// `Activation::Verity` if the `RealmFS` is sealed -/// -#[derive(Debug)] -pub enum Activation { - /// - /// A RealmFS in the unsealed state is activated by creating a /dev/loop - /// device and mounting it twice as both a read-only and read-write tree. - /// - Loop { - ro_mountpoint: Mountpoint, - rw_mountpoint: Mountpoint, - device: LoopDevice, - }, - /// - /// A RealmFS in the sealed state is activated by configuring a dm-verity - /// device and mounting it. - /// `mountpoint` is the filesystem location at which the device is mounted. - /// `device` is a path to a device in /dev/mapper/ - /// - Verity { - mountpoint: Mountpoint, - device: String, - }, -} - -impl Activation { - - fn new_loop(ro_mountpoint: Mountpoint, rw_mountpoint: Mountpoint, device: LoopDevice) -> Self { - Activation::Loop { ro_mountpoint, rw_mountpoint, device } - } - - fn new_verity(mountpoint: Mountpoint, device: String) -> Self { - Activation::Verity{ mountpoint, device } - } - - /// Converts an entry read from RealmFS:RUN_DIRECTORY into an `Activation` instance. - /// - /// Return an `Activation` corresponding to `mountpoint` if valid activation exists. - /// - pub fn for_mountpoint(mountpoint: &Mountpoint) -> Option { - if mountpoint.tag() == "rw" || mountpoint.tag() == "ro" { - LoopDevice::find_mounted_loop(mountpoint.path()).map(|loopdev| { - let (ro,rw) = Mountpoint::new_loop_pair(mountpoint.realmfs()); - Self::new_loop(ro, rw, loopdev) - }) - } else { - let device = Verity::device_name_for_mountpoint(mountpoint); - if Path::new("/dev/mapper").join(&device).exists() { - Some(Self::new_verity(mountpoint.clone(), device)) - } else { - None - } - } - } - - /// Deactivate `Activation` only if not in use. - /// - /// Returns `true` if state changes from activated to not-activated. - /// - pub fn deactivate(&self, active_set: &HashSet) -> Result { - if !self.in_use(active_set) { - self._deactivate()?; - Ok(true) - } else { - Ok(false) - } - } - - fn _deactivate(&self) -> Result<()> { - match self { - Activation::Loop { ro_mountpoint, rw_mountpoint, device } => { - ro_mountpoint.deactivate()?; - rw_mountpoint.deactivate()?; - info!("Removing loop device {}", device); - device.detach() - }, - Activation::Verity { mountpoint, device } => { - mountpoint.deactivate()?; - Verity::close_device(&device) - }, - } - } - - /// Return `true` if `mp` is a `Mountpoint` belonging to this `Activation`. - pub fn is_mountpoint(&self, mp: &Mountpoint) -> bool { - match self { - Activation::Loop { ro_mountpoint, rw_mountpoint, ..} => { - mp == ro_mountpoint || mp == rw_mountpoint - }, - Activation::Verity { mountpoint, .. } => { - mp == mountpoint - } - } - } - - /// Return read-only `Mountpoint` for this `Activation` - pub fn mountpoint(&self) -> &Mountpoint { - match self { - Activation::Loop { ro_mountpoint, ..} => &ro_mountpoint, - Activation::Verity { mountpoint, ..} => &mountpoint, - } - } - - /// Return read-write `Mountpoint` if present for this `Activation` type. - pub fn mountpoint_rw(&self) -> Option<&Mountpoint> { - match self { - Activation::Loop { rw_mountpoint, ..} => Some(&rw_mountpoint), - Activation::Verity { .. } => None, - } - } - - - pub fn device(&self) -> &str{ - match self { - Activation::Loop { device, ..} => device.device_str(), - Activation::Verity { device, ..} => &device, - } - } - - /// Return `true` if `Activation` is currently in-use by some `Realm` - /// - /// `active_set` is a set of mountpoints needed to determine if an activation is - /// in use. This set is obtained by calling `active_mountpoints()` on a `RealmManager` - /// instance. - /// - pub fn in_use(&self, active_set: &HashSet) -> bool { - match self { - Activation::Loop {ro_mountpoint: ro, rw_mountpoint: rw, ..} => { - active_set.contains(ro) || active_set.contains(rw) - }, - Activation::Verity { mountpoint, ..} => { - active_set.contains(mountpoint) - }, - } - } -} - - -struct VerityActivator<'a> { - realmfs: &'a RealmFS, - header: &'a ImageHeader, -} - - -impl <'a> VerityActivator <'a> { - fn new(realmfs: &'a RealmFS, header: &'a ImageHeader) -> Self { - VerityActivator { realmfs, header } - } - - // Determine if `self.realmfs` is already activated by searching for verity mountpoint and - // device name. If found return an `Activation::Verity` - fn activation(&self) -> Option { - let mountpoint = self.mountpoint(); - if mountpoint.exists() { - let devname = Verity::device_name(&self.realmfs.metainfo()); - Some(Activation::new_verity(self.mountpoint(), devname)) - } else { - None - } - } - - // Perform a verity activation of `self.realmfs` and return an `Activation::Verity` - fn activate(&self) -> Result { - info!("Starting verity activation for {}", self.realmfs.name()); - let mountpoint = self.mountpoint(); - if !mountpoint.exists() { - mountpoint.create_dir()?; - } - let device_name = self.setup_verity_device()?; - info!("verity device created.."); - cmd!("/usr/bin/mount", "-oro /dev/mapper/{} {}", device_name, mountpoint)?; - - Ok(Activation::new_verity(mountpoint, device_name)) - } - - fn mountpoint(&self) -> Mountpoint { - Mountpoint::new(self.realmfs.name(), &self.realmfs.metainfo().verity_tag()) - } - - fn setup_verity_device(&self) -> Result { - if !CommandLine::nosignatures() { - self.verify_signature()?; - } - - if !self.header.has_flag(ImageHeader::FLAG_HASH_TREE) { - self.generate_verity()?; - } - Verity::new(self.realmfs.path()).setup(&self.header.metainfo()) - } - - fn generate_verity(&self) -> Result<()> { - info!("Generating verity hash tree"); - Verity::new(self.realmfs.path()).generate_image_hashtree(&self.header.metainfo())?; - info!("Writing header..."); - self.header.set_flag(ImageHeader::FLAG_HASH_TREE); - self.header.write_header_to(self.realmfs.path())?; - info!("Done generating verity hash tree"); - Ok(()) - } - - fn verify_signature(&self) -> Result<()> { - let pubkey = self.public_key()?; - if !self.realmfs.header().verify_signature(pubkey) { - bail!("header signature verification failed on realmfs image '{}'", self.realmfs.name()); - } - info!("header signature verified on realmfs image '{}'", self.realmfs.name()); - Ok(()) - } - - fn public_key(&self) -> Result { - let pubkey = if self.realmfs.metainfo().channel() == RealmFS::USER_KEYNAME { - self.realmfs.sealing_keys()?.public_key() - } else { - match self.realmfs.header().public_key()? { - Some(pubkey) => pubkey, - None => bail!("No public key available for channel {}", self.realmfs.metainfo().channel()), - } - }; - Ok(pubkey) - } -} - -struct LoopActivator<'a> { - realmfs: &'a RealmFS, -} - -impl <'a> LoopActivator<'a> { - fn new(realmfs: &'a RealmFS) -> Self { - LoopActivator{ realmfs } - } - - // Determine if `self.realmfs` is presently activated by searching for mountpoints. If - // loop activation mountpoints are present return an `Activation::Loop` - fn activation(&self) -> Option { - let (ro,rw) = Mountpoint::new_loop_pair(self.realmfs.name()); - if ro.exists() && rw.exists() { - Activation::for_mountpoint(&ro) - } else { - None - } - } - - // Perform a loop activation of `self.realmfs` and return an `Activation::Loop` - fn activate(&self) -> Result { - - let (ro,rw) = Mountpoint::new_loop_pair(self.realmfs.name()); - ro.create_dir()?; - rw.create_dir()?; - - let loopdev = LoopDevice::create(self.realmfs.path(), Some(4096), false)?; - - loopdev.mount_pair(rw.path(), ro.path())?; - - Ok(Activation::new_loop(ro, rw, loopdev)) - } -} - diff --git a/libcitadel/src/realmfs/mod.rs b/libcitadel/src/realmfs/mod.rs index 6023c04..646864d 100644 --- a/libcitadel/src/realmfs/mod.rs +++ b/libcitadel/src/realmfs/mod.rs @@ -1,5 +1,4 @@ pub(crate) mod resizer; -mod activator; mod mountpoint; mod update; pub(crate) mod realmfs_set; @@ -8,4 +7,3 @@ mod realmfs; pub use self::realmfs::RealmFS; pub use self::mountpoint::Mountpoint; -pub use self::activator::Activation; diff --git a/libcitadel/src/realmfs/mountpoint.rs b/libcitadel/src/realmfs/mountpoint.rs index 37751bb..04d8472 100644 --- a/libcitadel/src/realmfs/mountpoint.rs +++ b/libcitadel/src/realmfs/mountpoint.rs @@ -1,16 +1,28 @@ -use std::fs::{self, DirEntry}; +use std::ffi::OsStr; use std::fmt; +use std::fs::{self, DirEntry}; use std::path::{PathBuf, Path}; -use crate::{Result, RealmFS}; -use std::ffi::OsStr; +use crate::{Result, RealmFS, CommandLine, ImageHeader}; +use crate::verity::Verity; -/// A RealmFS activation mountpoint +/// Represents the path at which a RealmFS is mounted and manages RealmFS activation and +/// deactivation. +/// +/// Activation of a RealmFS involves: +/// +/// 1. create mountpoint directory +/// 2. create loop and dm-verity device for image file +/// 3. Mount dm-verity device at mountpoint directory +/// +/// Deactivation reverses these steps. +/// #[derive(Clone,Eq,PartialEq,Hash,Debug)] pub struct Mountpoint(PathBuf); impl Mountpoint { + const MOUNT: &'static str = "/usr/bin/mount"; const UMOUNT: &'static str = "/usr/bin/umount"; /// Read `RealmFS::RUN_DIRECTORY` to collect all current mountpoints @@ -24,13 +36,6 @@ impl Mountpoint { Ok(all) } - /// Return a read-only/read-write mountpoint pair. - pub fn new_loop_pair(realmfs: &str) -> (Self,Self) { - let ro = Self::new(realmfs, "ro"); - let rw = Self::new(realmfs, "rw"); - (ro, rw) - } - /// Build a new `Mountpoint` from the provided realmfs `name` and `tag`. /// /// The directory name of the mountpoint will have the structure: @@ -46,21 +51,108 @@ impl Mountpoint { self.0.exists() } - pub fn create_dir(&self) -> Result<()> { + fn create_dir(&self) -> Result<()> { fs::create_dir_all(self.path())?; Ok(()) } - /// Deactivate this mountpoint by unmounting it and removing the directory. - pub fn deactivate(&self) -> Result<()> { - if self.exists() { - info!("Unmounting {} and removing directory", self); - cmd!(Self::UMOUNT, "{}", self)?; - fs::remove_dir(self.path())?; + pub fn is_mounted(&self) -> bool { + // test for an arbitrary expected directory + self.path().join("etc").exists() + } + + fn mount>(&self, source: P) -> Result<()> { + cmd!(Self::MOUNT, "-oro {} {}", + source.as_ref().display(), + self.path().display() + ) + } + + pub fn activate(&self, realmfs: &RealmFS) -> Result<()> { + if self.is_mounted() { + return Ok(()) } + + if !self.exists() { + self.create_dir()?; + } + let verity_path = self.verity_device_path(); + if verity_path.exists() { + warn!("dm-verity device {:?} already exists which was not expected", verity_path); + } else if let Err(err) = self.setup_verity(realmfs) { + let _ = fs::remove_dir(self.path()); + return Err(err); + } + + if let Err(err) = self.mount(verity_path) { + self.deactivate(); + Err(err) + } else { + Ok(()) + } + } + + fn setup_verity(&self, realmfs: &RealmFS) -> Result<()> { + if !CommandLine::nosignatures() { + realmfs.verify_signature()?; + } + if !realmfs.header().has_flag(ImageHeader::FLAG_HASH_TREE) { + self.generate_verity(realmfs)?; + } + let verity = Verity::new(realmfs.path())?; + verity.setup()?; Ok(()) } + fn generate_verity(&self, realmfs: &RealmFS) -> Result<()> { + info!("Generating verity hash tree"); + let verity = Verity::new(realmfs.path())?; + verity.generate_image_hashtree()?; + realmfs.header().set_flag(ImageHeader::FLAG_HASH_TREE); + realmfs.header().write_header_to(self.path())?; + info!("Done generating verity hash tree"); + Ok(()) + } + + /// Deactivate this mountpoint by unmounting it and removing the directory. + pub fn deactivate(&self) { + if !self.exists() { + return; + } + info!("Unmounting {} and removing directory", self); + + // 1. Unmount directory + if self.is_mounted() { + if let Err(err) = cmd!(Self::UMOUNT, "{}", self) { + warn!("Failed to unmount directory {}: {}", self, err); + } + } + + // 2. Remove dm-verity device + let verity = self.verity_device_path(); + if verity.exists() { + if let Err(err) = Verity::close_device(self.verity_device().as_str()) { + warn!("Failed to remove dm-verity device {:?}: {}", verity, err); + } + } + + // 3. Remove directory + if let Err(err) = fs::remove_dir(self.path()) { + warn!("Failed to remove mountpoint directory {}: {}", self, err); + } + + } + + fn verity_device_path(&self) -> PathBuf { + Path::new("/dev/mapper") + .join(self.verity_device()) + } + + // Return the name of the dm-verity device associated with this mountpoint + pub fn verity_device(&self) -> String { + format!("verity-realmfs-{}-{}", self.realmfs(), self.tag()) + } + /// Full `&Path` of mountpoint. pub fn path(&self) -> &Path { self.0.as_path() diff --git a/libcitadel/src/realmfs/realmfs.rs b/libcitadel/src/realmfs/realmfs.rs index 0fba4d9..798d5cb 100644 --- a/libcitadel/src/realmfs/realmfs.rs +++ b/libcitadel/src/realmfs/realmfs.rs @@ -3,46 +3,36 @@ use std::fs; use std::io::Write; use std::os::unix::fs::MetadataExt; use std::path::{Path,PathBuf}; +use std::sync::{Arc, Weak, RwLock}; -use sodiumoxide::randombytes::randombytes; -use hex; - -use crate::{CommandLine, ImageHeader, MetaInfo, Result, KeyRing, KeyPair, Signature, util, RealmManager}; - -use super::resizer::{ImageResizer,ResizeSize}; -use super::update::Update; +use crate::{ImageHeader, MetaInfo, Result, KeyRing, KeyPair, util, RealmManager, PublicKey, ResizeSize}; use crate::realmfs::resizer::Superblock; -use std::sync::{Arc, Weak}; -use super::activator::Activation; +use crate::realmfs::update::Update; use super::mountpoint::Mountpoint; -use crate::realmfs::activator::ActivationState; -use crate::verity::Verity; // Maximum length of a RealmFS name const MAX_REALMFS_NAME_LEN: usize = 40; -// The maximum number of backup copies the rotate() method will create -const NUM_BACKUPS: usize = 2; - /// /// Representation of a RealmFS disk image file. /// /// RealmFS images contain the root filesystem for one or more realms. A single RealmFS /// image may be shared by multiple running realm instances. /// -/// A RealmFS image can be in a state where it includes all the metadata needed to mount the -/// image with dm-verity to securely enforce read-only access to the image. An image in this state -/// is called 'sealed' and it may be signed either with regular channel keys or with a special -/// key generated upon installation and stored in the kernel keyring. -/// -/// An image which is not sealed is called 'unsealed'. In this state, the image can be mounted into -/// a realm with write access, but only one realm can write to the image. All other realms -/// use read-only views of the image. +/// A RealmFS image header includes metadata needed to mount the image with dm-verity to securely +/// securely enforce read-only access to the image. This header is signed with either regular +/// channel keys or with a user-controlled key generated upon installation and stored in the kernel +/// keyring. /// /// RealmFS images are normally stored in the directory `BASE_PATH` (/storage/realms/realmfs-images), /// and images stored in this directory can be loaded by name rather than needing the exact path /// to the image. /// +/// RealmFS image files in this directory are named $NAME-realmfs.img so the full path to a RealmFS +/// image with name 'main' would be: +/// +/// /storage/realms/realmfs-images/main-realmfs.img +/// #[derive(Clone)] pub struct RealmFS { // RealmFS name @@ -51,8 +41,8 @@ pub struct RealmFS { path: Arc, // current RealmFS image file header header: Arc, - - activation_state: Arc, + // mountpoint of the path this realmfs is mounted at when activated + mountpoint: Arc>, manager: Weak, } @@ -80,45 +70,34 @@ impl RealmFS { /// Load RealmFS image from an exact path. pub fn load_from_path(path: impl AsRef) -> Result { - Self::_load_from_path(path.as_ref(), true) + let header = Self::load_realmfs_header(path.as_ref())?; + let metainfo = header.metainfo(); + + let name = metainfo.realmfs_name() + .expect("RealmFS does not have a name"); + + let mountpoint = Mountpoint::new(name, metainfo.verity_tag()); + + Ok(RealmFS::new(name, path.as_ref(), header, mountpoint)) } - fn _load_from_path(path: &Path, load_activation: bool) -> Result { - let path = Arc::new(path.to_owned()); - let header = Self::load_realmfs_header(&path)?; - let name = header.metainfo().realmfs_name() - .expect("RealmFS does not have a name") - .to_owned(); - let name = Arc::new(name); - let header = Arc::new(header); - let manager = Weak::new(); - - let activation_state = Arc::new(ActivationState::new()); - - let realmfs = RealmFS { - name, path, header, activation_state, manager - }; - - if load_activation { - realmfs.load_activation(); + fn new(name: &str, path: &Path, header: ImageHeader, mountpoint: Mountpoint) -> Self { + RealmFS { + name: Arc::new(name.to_owned()), + path: Arc::new(path.to_owned()), + header: Arc::new(header), + mountpoint: Arc::new(RwLock::new(mountpoint)), + manager: Weak::new(), } - Ok(realmfs) } - pub fn set_manager(&mut self, manager: Arc) { + pub(super) fn set_manager(&mut self, manager: Arc) { self.manager = Arc::downgrade(&manager); } - fn load_activation(&self) { - self.activation_state.load(self); - } - pub fn manager(&self) -> Arc { - if let Some(manager) = self.manager.upgrade() { - manager - } else { - panic!("No manager set on realmfs {}", self.name); - } + self.manager.upgrade() + .expect(&format!("No manager set on realmfs {}", self.name)) } fn with_manager(&self, f: F) @@ -185,6 +164,15 @@ impl RealmFS { self.path.as_ref() } + pub fn mountpoint(&self) -> Mountpoint { + let lock = self.mountpoint.read().unwrap(); + lock.clone() + } + + pub fn delete(&self) -> Result<()> { + self.manager().delete_realmfs(self) + } + /// Return a new `PathBuf` based on the path of the current image by appending /// the string `ext` as an extension to the filename. If the current filename /// ends with '.img' then the specified extension is appended to this as '.img.ext' @@ -235,219 +223,136 @@ impl RealmFS { self.header().metainfo() } + // Each time RealmFS header is accessed, verify that the header on disk has not changed. + // If the header changes generate a new mountpoint instance because the verity tag may + // have changed. + fn check_stale_header(&self) -> Result<()> { + if self.header.reload_if_stale(self.path())? { + let mut lock = self.mountpoint.write().unwrap(); + *lock = Mountpoint::new(self.name(), self.header.metainfo().verity_tag()); + } + Ok(()) + } + pub fn header(&self) -> &ImageHeader { - match self.header.reload_if_stale(self.path()) { - Ok(true) => self.load_activation(), - Err(e) => warn!("error reloading stale image header: {}", e), - _ => {}, - }; + if let Err(err) = self.check_stale_header() { + warn!("error reloading stale image header: {}", err); + } &self.header } + /// Return true if this RealmFS is sealed with user signing keys. pub fn is_user_realmfs(&self) -> bool { - !self.is_sealed() || self.metainfo().channel() == Self::USER_KEYNAME - } - - /// Return `true` if this RealmFS is 'activated'. - /// - /// A RealmFS is activated if the device for the image has been created and mounted. - /// Sealed images create dm-verity devices in /dev/mapper and unsealed images create - /// /dev/loop devices. - pub fn is_activated(&self) -> bool { - self.activation_state.is_activated() - } - - /// If this RealmFS is activated return `Activation` instance - pub fn activation(&self) -> Option> { - self.activation_state.get() + self.metainfo().channel() == Self::USER_KEYNAME } /// Return `true` if RealmFS is activated and some Realm is currently using /// it. A RealmFS which is in use cannot be deactivated. pub fn is_in_use(&self) -> bool { - let active = self.manager().active_mountpoints(); - self.activation_state.is_in_use(&active) - } - - /// Activate this RealmFS image if not yet activated. - pub fn activate(&self) -> Result> { - if CommandLine::sealed() && !self.is_sealed() && !self.is_update_copy() { - bail!("Cannot activate unsealed realmfs '{}' because citadel.sealed is enabled", self.name()); - } - self.activation_state.activate(self) + self.manager().realmfs_mountpoint_in_use(&self.mountpoint()) } /// Deactivate this RealmFS image if currently activated, but not in use. /// Return `true` if deactivation occurs. - pub fn deactivate(&self) -> Result { - let active = self.manager().active_mountpoints(); - self.activation_state.deactivate(&active) - } - - pub fn fork(&self, new_name: &str) -> Result { - self._fork(new_name, true) - } - - /// Create an unsealed copy of this RealmFS image with a new image name. - /// - pub fn fork_unsealed(&self, new_name: &str) -> Result { - Self::validate_name(new_name)?; - info!("forking RealmFS image '{}' to new name '{}'", self.name(), new_name); - - let new_path = self.path_with_filename(format!("{}-realmfs.img", new_name)); - - if new_path.exists() { - bail!("RealmFS image for name {} already exists", new_name); + pub fn deactivate(&self) { + if !self.is_in_use() { + self.mountpoint().deactivate(); } - - let new_realmfs = self.copy_image(&new_path, new_name, false)?; - self.with_manager(|m| m.realmfs_added(&new_realmfs)); - Ok(new_realmfs) } - fn _fork(&self, new_name: &str, sealed_fork: bool) -> Result { - Self::validate_name(new_name)?; - info!("forking RealmFS image '{}' to new name '{}'", self.name(), new_name); - let new_path = self.path_with_filename(format!("{}-realmfs.img", new_name)); - if new_path.exists() { - bail!("RealmFS image for name {} already exists", new_name); - } - - let new_realmfs = self.copy_image(&new_path, new_name, sealed_fork)?; - - self.with_manager(|m| m.realmfs_added(&new_realmfs)); - Ok(new_realmfs) - + pub fn interactive_update(&self, scheme: Option<&str>) -> Result<()> { + let mut update = Update::create(self)?; + update.run_interactive_update(scheme) } - pub fn update(&self) -> Update { - Update::new(self) - } - - fn is_update_copy(&self) -> bool { - self.path().extension() == Some(OsStr::new("update")) - } - - pub(crate) fn update_copy(&self) -> Result { - let path = self.path_with_extension("update"); - let name = self.name().to_string() + "-update"; - self.copy_image(&path, &name, false) - } - - fn copy_image(&self, path: &Path, name: &str, sealed_copy: bool) -> Result { - if path.exists() { - bail!("Cannot create sealed copy because target path '{}' already exists", path.display()); - } - cmd!("/usr/bin/cp", "--reflink=auto {} {}", self.path.display(), path.display())?; - let mut realmfs = Self::_load_from_path(path, false)?; - self.with_manager(|m| realmfs.set_manager(m)); - realmfs.name = Arc::new(name.to_owned()); - - let result = if sealed_copy { - realmfs.write_sealed_copy_header() + // Return the public key for verifying the signature on this image + fn public_key(&self) -> Result { + let pubkey = if self.metainfo().channel() == RealmFS::USER_KEYNAME { + self.sealing_keys()?.public_key() } else { - realmfs.unseal() + match self.header().public_key()? { + Some(pubkey) => pubkey, + None => bail!("No public key available for channel {}", self.metainfo().channel()), + } }; - - result.map_err(|e| - if let Err(e) = fs::remove_file(path) { - format_err!("failed to remove {} after realmfs fork/copy failed with: {}", path.display(), e) - } else { e })?; - - Ok(realmfs) + Ok(pubkey) } - fn write_sealed_copy_header(&self) -> Result<()> { - let keys = match self.sealing_keys() { - Ok(keys) => keys, - Err(err) => bail!("Cannot seal realmfs image, no sealing keys available: {}", err), - }; - let metainfo = self.metainfo(); - let metainfo_bytes = self.generate_sealed_metainfo(self.name(), metainfo.verity_salt(), metainfo.verity_root()); - let sig = keys.sign(&metainfo_bytes); - self.write_new_metainfo(&metainfo_bytes, Some(sig)) - } - - /// Convert to unsealed RealmFS image by removing dm-verity metadata and hash tree - pub fn unseal(&self) -> Result<()> { - let bytes = Self::generate_unsealed_metainfo(self.name(), self.metainfo().nblocks(), None); - self.write_new_metainfo(&bytes, None)?; - if self.has_verity_tree() { - self.truncate_verity()?; + pub(super) fn verify_signature(&self) -> Result<()> { + let pubkey = self.public_key()?; + if !self.header().verify_signature(pubkey) { + bail!("header signature verification failed on realmfs image '{}'", self.name()); } + info!("header signature verified on realmfs image '{}'", self.name()); Ok(()) } - pub fn set_owner_realm(&self, owner_realm: &str) -> Result<()> { - if self.is_sealed() { - bail!("Cannot set owner realm because RealmFS is sealed"); + + pub fn fork(&self, new_name: &str) -> Result { + Self::validate_name(new_name)?; + let new_path = self.path_with_filename(format!("{}-realmfs.img", new_name)); + if new_path.exists() { + bail!("RealmFS image for name {} already exists", new_name); } - if let Some(activation) = self.activation() { - let rw_mountpoint = activation.mountpoint_rw() - .ok_or_else(|| format_err!("unsealed activation expected"))?; - if self.manager().active_mountpoints().contains(rw_mountpoint) { - bail!("Cannot set owner realm because RW mountpoint is in use (by current owner?)"); + + let keys = match self.sealing_keys() { + Ok(keys) => keys, + Err(err) => bail!("Cannot fork realmfs image, no signing keys available: {}", err), + }; + + info!("forking RealmFS image '{}' to new name '{}'", self.name(), new_name); + + let forked = match self.fork_to_path(new_name, &new_path, keys) { + Ok(forked) => forked, + Err(err) => { + if new_path.exists() { + let _ = fs::remove_file(&new_path); + } + bail!("Failed to fork RealmFS '{}' to '{}': {}", self.name, new_name, err); } - } - let nblocks = self.metainfo().nblocks(); - self.update_unsealed_metainfo(self.name(), nblocks, Some(owner_realm.to_owned())) + }; + + self.with_manager(|m| m.realmfs_added(&forked)); + Ok(forked) } - pub fn update_unsealed_metainfo(&self, name: &str, nblocks: usize, owner_realm: Option) -> Result<()> { - if self.is_sealed() { - bail!("Cannot update metainfo on sealed realmfs image"); - } - let metainfo_bytes = Self::generate_unsealed_metainfo(name, nblocks, owner_realm); - self.write_new_metainfo(&metainfo_bytes, None) + // copy source image file to new name and install updated header + fn fork_to_path(&self, new_name: &str, new_path: &Path, keys: KeyPair) -> Result { + self.copy_image_file(new_path)?; + let metainfo_bytes = self.fork_metainfo(new_name); + let sig = keys.sign(&metainfo_bytes); + let forked = Self::load_from_path(new_path)?; + forked.header().update_metainfo(&metainfo_bytes, sig.to_bytes(), new_path)?; + Ok(forked) } - fn write_new_metainfo(&self, bytes: &[u8], sig: Option) -> Result<()> { - self.header.set_metainfo_bytes(bytes)?; - if let Some(sig) = sig { - self.header.set_signature(sig.to_bytes())?; + pub(super) fn copy_image_file(&self, to: &Path) -> Result<()> { + if to.exists() { + bail!("Cannot copy image file to {} because it already exists", to.display()); } - self.header.write_header_to(self.path()) + cmd!("/usr/bin/cp", "--reflink=auto {} {}", self.path.display(), to.display())?; + Ok(()) } - fn generate_unsealed_metainfo(name: &str, nblocks: usize, owner_realm: Option) -> Vec { + fn fork_metainfo(&self, new_name: &str) -> Vec { + // when creating a realmfs fork, only the name will change + let metainfo = self.metainfo(); + Self::generate_metainfo(new_name, metainfo.nblocks(), metainfo.verity_salt(), metainfo.verity_root()) + } + + pub(super) fn generate_metainfo(name: &str, nblocks: usize, verity_salt: &str, verity_root: &str) -> Vec { let mut v = Vec::new(); writeln!(v, "image-type = \"realmfs\"").unwrap(); writeln!(v, "realmfs-name = \"{}\"", name).unwrap(); writeln!(v, "nblocks = {}", nblocks).unwrap(); - if let Some(owner) = owner_realm { - writeln!(v, "realmfs-owner = \"{}\"", owner).unwrap(); - } - v - } - - fn generate_sealed_metainfo(&self, name: &str, verity_salt: &str, verity_root: &str) -> Vec { - let mut v = Self::generate_unsealed_metainfo(name, self.metainfo().nblocks(), None); writeln!(v, "channel = \"{}\"", Self::USER_KEYNAME).unwrap(); writeln!(v, "verity-salt = \"{}\"", verity_salt).unwrap(); writeln!(v, "verity-root = \"{}\"", verity_root).unwrap(); v } - // Remove verity tree from image file by truncating file to the number of blocks in metainfo - fn truncate_verity(&self) -> Result<()> { - let file_nblocks = self.file_nblocks()?; - let expected = self.metainfo_nblocks(); - - if self.has_verity_tree() { - let f = fs::OpenOptions::new().write(true).open(self.path())?; - let lock = self.header(); - lock.clear_flag(ImageHeader::FLAG_HASH_TREE); - lock.write_header(&f)?; - debug!("Removing appended dm-verity hash tree by truncating image from {} blocks to {} blocks", file_nblocks, expected); - f.set_len((expected * 4096) as u64)?; - } else if file_nblocks > expected { - warn!("RealmFS image size was greater than length indicated by metainfo.nblocks but FLAG_HASH_TREE not set"); - } - Ok(()) - } - // Return the length in blocks of the actual image file on disk - fn file_nblocks(&self) -> Result { + pub fn file_nblocks(&self) -> Result { let meta = self.path.metadata()?; let len = meta.len() as usize; if len % 4096 != 0 { @@ -460,82 +365,6 @@ impl RealmFS { Ok(nblocks) } - fn has_verity_tree(&self) -> bool { - self.header().has_flag(ImageHeader::FLAG_HASH_TREE) - } - - pub fn is_sealed(&self) -> bool { - !self.metainfo().verity_root().is_empty() - } - - pub fn seal(&self, new_name: Option<&str>) -> Result<()> { - if self.is_sealed() { - info!("RealmFS {} is already sealed. Doing nothing.", self.name()); - return Ok(()) - } - - let keys = match self.sealing_keys() { - Ok(keys) => keys, - Err(err) => bail!("Cannot seal realmfs image, no sealing keys available: {}", err), - }; - - if self.is_activated() { - bail!("Cannot seal RealmFS because it is currently activated"); - } - - if self.has_verity_tree() { - warn!("unsealed RealmFS already has a verity hash tree, removing it"); - self.truncate_verity()?; - } - - let tmp = self.path_with_extension("sealing"); - if tmp.exists() { - info!("Temporary copy of realmfs image {} already exists, removing it.", self.name()); - fs::remove_file(&tmp)?; - } - - info!("Creating temporary copy of realmfs image"); - cmd!("/usr/bin/cp", "--reflink=auto {} {}", self.path.display(), tmp.display())?; - - let name = new_name.unwrap_or_else(|| self.name()); - - let mut realmfs = Self::load_from_path(&tmp)?; - realmfs.set_manager(self.manager()); - - let finish = || { - realmfs.generate_sealing_verity(&keys, name)?; - verbose!("Rename {} to {}", self.path().display(), self.path_with_extension("old").display()); - fs::rename(self.path(), self.path_with_extension("old"))?; - verbose!("Rename {} to {}", realmfs.path().display(), self.path().display()); - fs::rename(realmfs.path(), self.path())?; - Ok(()) - }; - - if let Err(err) = finish() { - if tmp.exists() { - let _ = fs::remove_file(tmp); - } - return Err(err); - } - Ok(()) - } - - fn generate_sealing_verity(&self, keys: &KeyPair, name: &str) -> Result<()> { - info!("Generating verity hash tree for sealed realmfs ({})", self.path().display()); - let salt = hex::encode(randombytes(32)); - let output = Verity::new(self.path()).generate_image_hashtree_with_salt(&self.metainfo(), &salt)?; - let root_hash = output.root_hash() - .ok_or_else(|| format_err!("no root hash returned from verity format operation"))?; - info!("root hash is {}", output.root_hash().unwrap()); - - info!("Signing new image with user realmfs keys"); - let metainfo_bytes = self.generate_sealed_metainfo(name, &salt, &root_hash); - let sig = keys.sign(&metainfo_bytes); - - self.header().set_flag(ImageHeader::FLAG_HASH_TREE); - self.write_new_metainfo(&metainfo_bytes, Some(sig)) - } - pub fn has_sealing_keys(&self) -> bool { self.sealing_keys().is_ok() } @@ -544,31 +373,22 @@ impl RealmFS { KeyRing::get_kernel_keypair(Self::USER_KEYNAME) } - pub fn rotate(&self, new_file: &Path) -> Result<()> { - let backup = |n: usize| Path::new(Self::BASE_PATH).join(format!("{}-realmfs.img.{}", self.name(), n)); - - for i in (1..NUM_BACKUPS).rev() { - let from = backup(i - 1); - if from.exists() { - fs::rename(from, backup(i))?; - } - } - fs::rename(self.path(), backup(0))?; - fs::rename(new_file, self.path())?; - Ok(()) - } - pub fn auto_resize_size(&self) -> Option { - ImageResizer::auto_resize_size(self) + ResizeSize::auto_resize_size(&self) } pub fn resize_grow_to(&self, size: ResizeSize) -> Result<()> { info!("Resizing to {} blocks", size.nblocks()); - ImageResizer::new(self).grow_to(size) + let mut update = Update::create(self)?; + update.grow_to(size); + update.resize() } pub fn resize_grow_by(&self, size: ResizeSize) -> Result<()> { - ImageResizer::new(self).grow_by(size) + info!("Resizing to an increase of {} blocks", size.nblocks()); + let mut update = Update::create(self)?; + update.grow_by(size); + update.resize() } pub fn free_size_blocks(&self) -> Result { @@ -581,23 +401,18 @@ impl RealmFS { Ok(meta.blocks() as usize / 8) } - /// Size of image file in blocks (including header block) based on metainfo `nblocks` field. - pub fn metainfo_nblocks(&self) -> usize { - self.metainfo().nblocks() + 1 + /// Activate this RealmFS image if not yet activated. + pub fn activate(&self) -> Result<()> { + self.mountpoint().activate(self) } - /// Return `true` if mountpoint belongs to current `Activation` state of - /// this `RealmFS` - pub fn release_mountpoint(&self, mountpoint: &Mountpoint) -> bool { - let is_ours = self.activation() - .map_or(false, |a| a.is_mountpoint(mountpoint)); - - if is_ours { - if let Err(e) = self.deactivate() { - warn!("error deactivating mountpoint: {}", e); - } - } - is_ours + /// Return `true` if this RealmFS is 'activated'. + /// + /// A RealmFS is activated if the device for the image has been created and mounted. + /// Sealed images create dm-verity devices in /dev/mapper and unsealed images create + /// /dev/loop devices. + pub fn is_activated(&self) -> bool { + self.mountpoint().is_mounted() } - } + diff --git a/libcitadel/src/realmfs/realmfs_set.rs b/libcitadel/src/realmfs/realmfs_set.rs index fa2999b..4b148ab 100644 --- a/libcitadel/src/realmfs/realmfs_set.rs +++ b/libcitadel/src/realmfs/realmfs_set.rs @@ -1,7 +1,8 @@ use std::collections::HashMap; -use crate::{RealmFS, RealmManager, Result}; -use std::sync::Arc; use std::fs; +use std::sync::Arc; + +use crate::{RealmFS, RealmManager, Result}; pub struct RealmFSSet { realmfs_map: HashMap, diff --git a/libcitadel/src/realmfs/resizer.rs b/libcitadel/src/realmfs/resizer.rs index 64dd671..1e8583e 100644 --- a/libcitadel/src/realmfs/resizer.rs +++ b/libcitadel/src/realmfs/resizer.rs @@ -1,26 +1,20 @@ -use std::fs::{File,OpenOptions}; +use std::fs::File; use std::io::{Read,Seek,SeekFrom}; use std::path::Path; use byteorder::{ByteOrder,LittleEndian}; -use crate::{RealmFS,Result,LoopDevice}; +use crate::{RealmFS,Result}; const BLOCK_SIZE: usize = 4096; const BLOCKS_PER_MEG: usize = (1024 * 1024) / BLOCK_SIZE; const BLOCKS_PER_GIG: usize = 1024 * BLOCKS_PER_MEG; -const E2FSCK: &str = "e2fsck"; -const RESIZE2FS: &str = "resize2fs"; - // If less than 1gb remaining space const AUTO_RESIZE_MINIMUM_FREE: ResizeSize = ResizeSize(BLOCKS_PER_GIG); // ... add 4gb to size of image const AUTO_RESIZE_INCREASE_SIZE: ResizeSize = ResizeSize(4 * BLOCKS_PER_GIG); -pub struct ImageResizer<'a> { - image: &'a RealmFS, -} #[derive(Copy,Clone)] pub struct ResizeSize(usize); @@ -50,89 +44,9 @@ impl ResizeSize { pub fn size_in_mb(&self) -> usize { self.0 / BLOCKS_PER_MEG } -} - -impl <'a> ImageResizer<'a> { - - pub fn new(image: &'a RealmFS) -> ImageResizer<'a> { - ImageResizer { image } - } - - pub fn grow_to(&mut self, size: ResizeSize) -> Result<()> { - let target_nblocks = size.nblocks(); - let current_nblocks = self.image.metainfo_nblocks(); - if current_nblocks >= target_nblocks { - info!("RealmFS image is already larger than requested size, doing nothing"); - } else { - let size = ResizeSize::blocks(target_nblocks - current_nblocks); - self.grow_by(size)?; - } - Ok(()) - } - - pub fn grow_by(&mut self, size: ResizeSize) -> Result<()> { - let nblocks = size.nblocks(); - let new_nblocks = self.image.metainfo_nblocks() + nblocks; - if self.image.is_sealed() { - bail!("Cannot resize sealed image '{}'. unseal first", self.image.name()); - } - self.resize(new_nblocks) - } - - fn resize(&self, new_nblocks: usize) -> Result<()> { - if new_nblocks < self.image.metainfo_nblocks() { - bail!("Cannot shrink image") - } - - if (new_nblocks - self.image.metainfo_nblocks()) > ResizeSize::gigs(8).nblocks() { - bail!("Can only increase size of RealmFS image by a maximum of 8gb at one time"); - } - - ImageResizer::resize_image_file(self.image.path(), new_nblocks)?; - - if let Some(open_loop) = self.notify_open_loops()? { - info!("Running e2fsck {:?}", open_loop); - cmd!(E2FSCK,"{} {} {}","-f","-p",open_loop.device().display())?; - info!("Running resize2fs {:?}", open_loop); - cmd!(RESIZE2FS, "{}", open_loop.device().display())?; - } else { - LoopDevice::with_loop(self.image.path(), Some(4096), false, |loopdev| { - info!("Running e2fsck {:?}", loopdev); - cmd!(E2FSCK,"{} {} {}","-f","-p",loopdev.device().display())?; - info!("Running resize2fs {:?}", loopdev); - cmd!(RESIZE2FS, "{}", loopdev.device().display())?; - Ok(()) - })?; - } - let owner = self.image.metainfo().realmfs_owner().map(|s| s.to_owned()); - self.image.update_unsealed_metainfo(self.image.name(), new_nblocks - 1, owner)?; - Ok(()) - } - - fn resize_image_file(file: &Path, nblocks: usize) -> Result<()> { - let len = nblocks * BLOCK_SIZE; - info!("Resizing image file to {}", len); - OpenOptions::new() - .write(true) - .open(file)? - .set_len(len as u64)?; - Ok(()) - } - - fn notify_open_loops(&self) -> Result> { - let mut open_loop = None; - for loopdev in LoopDevice::find_devices_for(self.image.path())? { - loopdev.resize() - .unwrap_or_else(|err| warn!("Error running losetup -c {:?}: {}", loopdev, err)); - open_loop = Some(loopdev); - } - Ok(open_loop) - } - /// If the RealmFS needs to be resized to a larger size, returns the - /// recommended size. Pass this value to `ImageResizer.grow_to()` to - /// complete the resize. + /// recommended size. pub fn auto_resize_size(realmfs: &RealmFS) -> Option { let sb = match Superblock::load(realmfs.path(), 4096) { Ok(sb) => sb, @@ -145,10 +59,11 @@ impl <'a> ImageResizer<'a> { sb.free_block_count(); let free_blocks = sb.free_block_count() as usize; if free_blocks < AUTO_RESIZE_MINIMUM_FREE.nblocks() { - let increase_multiple = realmfs.metainfo_nblocks() / AUTO_RESIZE_INCREASE_SIZE.nblocks(); - let grow_size = (increase_multiple + 1) * AUTO_RESIZE_INCREASE_SIZE.nblocks(); - let mask = grow_size - 1; - let grow_blocks = (free_blocks + mask) & !mask; + let metainfo_nblocks = realmfs.metainfo().nblocks() + 1; + let increase_multiple = metainfo_nblocks / AUTO_RESIZE_INCREASE_SIZE.nblocks(); + let grow_size = (increase_multiple + 1) * AUTO_RESIZE_INCREASE_SIZE.nblocks(); + let mask = grow_size - 1; + let grow_blocks = (free_blocks + mask) & !mask; Some(ResizeSize::blocks(grow_blocks)) } else { None diff --git a/libcitadel/src/realmfs/update.rs b/libcitadel/src/realmfs/update.rs index 3610592..19e6b67 100644 --- a/libcitadel/src/realmfs/update.rs +++ b/libcitadel/src/realmfs/update.rs @@ -1,78 +1,307 @@ use std::fs; +use std::io::{self, Write}; +use std::path::{PathBuf, Path}; use std::process::Command; -use crate::{Result, RealmFS }; -use crate::realmfs::Mountpoint; +use sodiumoxide::randombytes::randombytes; + +use crate::{Result, RealmFS, FileLock, ImageHeader, LoopDevice, ResizeSize, util}; use crate::realm::BridgeAllocator; -use crate::ResizeSize; +use crate::util::is_euid_root; +use crate::terminal::TerminalRestorer; +use crate::verity::Verity; -enum UpdateType { - NotSetup, - Sealed(RealmFS), - Unsealed, -} +const BLOCK_SIZE: usize = 4096; +// The maximum number of backup copies the rotate() method will create +const NUM_BACKUPS: usize = 2; + +const E2FSCK: &str = "e2fsck"; +const RESIZE2FS: &str = "resize2fs"; + +/// Manages the process of updating or resizing a `RealmFS` image file. +/// pub struct Update<'a> { - realmfs: &'a RealmFS, + realmfs: &'a RealmFS, // RealmFS being updated + name: String, // name for nspawn instance + target: PathBuf, // Path to the update copy of realmfs image + mountpath: PathBuf, // Path at which update copy is mounted + _lock: FileLock, + resize: Option, // If the image needs to be resized, the resize size is stored here network_allocated: bool, - update_type: UpdateType, } impl <'a> Update<'a> { - pub fn new(realmfs: &'a RealmFS) -> Self { - Update { realmfs, network_allocated: false, update_type: UpdateType::NotSetup } + fn new(realmfs: &'a RealmFS, lock: FileLock) -> Self { + + let metainfo = realmfs.metainfo(); + let tag = metainfo.verity_tag(); + let mountpath = Path::new(RealmFS::RUN_DIRECTORY) + .join(format!("realmfs-{}-{}.update", realmfs.name(), tag)); + + Update { + realmfs, + name: format!("{}-{}-update", realmfs.name(), tag), + target: realmfs.path().with_extension("update"), + mountpath, + _lock: lock, + resize: ResizeSize::auto_resize_size(realmfs), + network_allocated: false, + } } - pub fn setup(&mut self) -> Result<()> { - self.update_type = self.create_update_type()?; + pub fn create(realmfs: &'a RealmFS) -> Result { + let lock = FileLock::nonblocking_acquire(realmfs.path().with_extension("lock"))? + .ok_or(format_err!("Unable to obtain file lock to update realmfs image: {}", realmfs.name()))?; + + if !realmfs.has_sealing_keys() { + bail!("Cannot seal realmfs image, no sealing keys available"); + } + + Ok(Update::new(realmfs, lock)) + } + + fn name(&self) -> &str { + &self.name + } + + fn target(&self) -> &Path { + &self.target + } + + fn create_update_copy(&self) -> Result<()> { + if self.target.exists() { + info!("Update file {} already exists, removing it", self.target.display()); + fs::remove_file(&self.target)?; + } + self.realmfs.copy_image_file(self.target())?; + + self.truncate_verity()?; + self.resize_image_file()?; Ok(()) } - pub fn auto_resize_size(&self) -> Option { - self.target_image().auto_resize_size() + fn setup(&mut self) -> Result<()> { + self.create_update_copy()?; + self.truncate_verity()?; + self.resize_image_file()?; + self.mount_update_image()?; + Ok(()) } - pub fn apply_resize(&self, size: ResizeSize) -> Result<()> { - self.target_image().resize_grow_to(size) + pub fn resize(&mut self) -> Result<()> { + if self.resize.is_none() { + return Ok(()) + } + + self.create_update_copy()?; + self.truncate_verity()?; + self.resize_image_file()?; + + LoopDevice::with_loop(self.target(), Some(BLOCK_SIZE), false, |loopdev| { + self.resize_device(loopdev) + }) } - fn target_image(&self) -> &RealmFS { - if let UpdateType::Sealed(ref image) = self.update_type { - image - } else { - &self.realmfs + fn mount_update_image(&mut self) -> Result<()> { + LoopDevice::with_loop(self.target(), Some(BLOCK_SIZE), false, |loopdev| { + if self.resize.is_some() { + self.resize_device(loopdev)?; + } + if !self.mountpath.exists() { + fs::create_dir_all(&self.mountpath)?; + } + util::mount(loopdev.device_str(), &self.mountpath, Some("-orw,noatime"))?; + Ok(()) + }) + } + + // Return size of image file in blocks based on metainfo `nblocks` field. + // Include header block in count so add one block + fn metainfo_nblock_size(&self) -> usize { + self.realmfs.metainfo().nblocks() + 1 + } + + fn unmount_update_image(&mut self) { + if self.mountpath.exists() { + if let Err(err) = util::umount(&self.mountpath) { + warn!("Failed to unmount directory {:?}: {}", self.mountpath, err); + + } + if let Err(err) = fs::remove_dir(&self.mountpath) { + warn!("Failed to remove mountpoint directory {:?}: {}", self.mountpath, err); + } } } - fn create_update_type(&self) -> Result { - if self.realmfs.is_sealed() { - let update_image = self.realmfs.update_copy()?; - Ok(UpdateType::Sealed(update_image)) + fn resize_device(&self, loopdev: &LoopDevice) -> Result<()> { + info!("Running e2fsck {:?}", loopdev); + cmd!(E2FSCK,"{} {} {}","-f","-p", loopdev.device().display())?; + info!("Running resize2fs {:?}", loopdev); + cmd!(RESIZE2FS, "{}", loopdev.device().display())?; + Ok(()) + } + + pub fn grow_to(&mut self, size: ResizeSize) { + let target_nblocks = size.nblocks(); + let current_nblocks = self.metainfo_nblock_size(); + if current_nblocks >= target_nblocks { + info!("RealmFS image is already larger than requested size, doing nothing"); } else { - Ok(UpdateType::Unsealed) + self.set_resize(target_nblocks); } } - pub fn open_update_shell(&mut self) -> Result<()> { - self.run_update_shell("/usr/libexec/configure-host0.sh && exec /bin/bash") + pub fn grow_by(&mut self, size: ResizeSize) { + let nblocks = size.nblocks(); + self.set_resize(self.metainfo_nblock_size() + nblocks); } - fn mountpoint(&self) -> Result { - let target = self.target_image(); - let activation = target.activate() - .map_err(|e| format_err!("failed to activate update image: {}", e))?; + fn set_resize(&mut self, nblocks: usize) { + self.resize = Some(ResizeSize::blocks(nblocks)); + } - activation.mountpoint_rw().cloned() - .ok_or_else(|| format_err!("Update image activation does not have a writeable mountpoint")) + fn set_target_len(&self, nblocks: usize) -> Result<()> { + let len = (nblocks * BLOCK_SIZE) as u64; + let f = fs::OpenOptions::new() + .write(true) + .open(&self.target)?; + f.set_len(len)?; + Ok(()) + } + + // Remove dm-verity hash tree from update copy of image file. + fn truncate_verity(&self) -> Result<()> { + let file_nblocks = self.realmfs.file_nblocks()?; + let metainfo_nblocks = self.metainfo_nblock_size(); + + if self.realmfs.header().has_flag(ImageHeader::FLAG_HASH_TREE) { + self.set_target_len(metainfo_nblocks)?; + } else if file_nblocks > metainfo_nblocks { + warn!("RealmFS image size was greater than length indicated by metainfo.nblocks but FLAG_HASH_TREE not set"); + } + Ok(()) + } + + // If resize was requested, adjust size of update copy of image file. + fn resize_image_file(&self) -> Result<()> { + let nblocks = match self.resize { + Some(rs) => rs.nblocks() + 1, + None => return Ok(()), + }; + + if nblocks < self.metainfo_nblock_size() { + bail!("Cannot shrink image") + } + // This is an arbitrary restriction which is probably not needed + if (nblocks - self.metainfo_nblock_size()) > ResizeSize::gigs(8).nblocks() { + bail!("Can only increase size of RealmFS image by a maximum of 8gb at one time"); + } + self.set_target_len(nblocks) + } + + pub fn cleanup(&mut self) { + if self.mountpath.exists() { + self.unmount_update_image(); + } + + if self.target().exists() { + if let Err(err) = fs::remove_file(self.target()) { + warn!("Failed to remove update image copy {:?}: {}", self.target(), err); + } + } + + // If an IP address was allocated, free it + if self.network_allocated { + if let Err(err) = BridgeAllocator::default_bridge() + .and_then(|mut allocator| allocator.free_allocation_for(&self.name())) { + + warn!("Error releasing address allocation for RealmFS ({}) update: {}", self.realmfs.name(), err); + } + self.network_allocated = false; + } + } + + fn seal(&mut self) -> Result<()> { + let nblocks = match self.resize { + Some(rs) => rs.nblocks(), + None => self.metainfo_nblock_size() - 1, + }; + + let salt = hex::encode(randombytes(32)); + let verity = Verity::new(&self.target)?; + let output = verity.generate_image_hashtree_with_salt(&salt, nblocks)?; + // XXX passes metainfo for nblocks + //let output = Verity::new(&self.target).generate_image_hashtree_with_salt(&self.realmfs.metainfo(), &salt)?; + let root_hash = output.root_hash() + .ok_or_else(|| format_err!("no root hash returned from verity format operation"))?; + info!("root hash is {}", output.root_hash().unwrap()); + + /* + let nblocks = match self.resize { + Some(rs) => rs.nblocks(), + None => self.metainfo_nblock_size() - 1, + }; + + */ + + info!("Signing new image with user realmfs keys"); + let metainfo_bytes = RealmFS::generate_metainfo(self.realmfs.name(), nblocks, salt.as_str(), root_hash); + let keys = self.realmfs.sealing_keys().expect("No sealing keys"); + let sig = keys.sign(&metainfo_bytes); + let header = ImageHeader::new(); + header.set_flag(ImageHeader::FLAG_HASH_TREE); + header.update_metainfo(&metainfo_bytes, sig.to_bytes(), &self.target) + } + + + fn prompt_user(prompt: &str, default_y: bool) -> Result { + let yn = if default_y { "(Y/n)" } else { "(y/N)" }; + print!("{} {} : ", prompt, yn); + io::stdout().flush()?; + let mut line = String::new(); + io::stdin().read_line(&mut line)?; + + let yes = match line.trim().chars().next() { + Some(c) => c == 'Y' || c == 'y', + None => default_y, + }; + Ok(yes) + } + + pub fn run_interactive_update(&mut self, scheme: Option<&str>) -> Result<()> { + if !is_euid_root() { + bail!("RealmFS updates must be run as root"); + } + let mut term = TerminalRestorer::new(); + if let Some(scheme) = scheme { + term.save_palette(); + term.apply_base16_by_slug(scheme); + } + + self.setup()?; + + println!(); + println!("Opening update shell for '{}-realmfs.img'", self.realmfs.name()); + println!(); + println!("Exit shell with ctrl-d or 'exit' to return to realm manager"); + println!(); + + self.run_update_shell("/usr/libexec/configure-host0.sh && exec /bin/bash")?; + + if Self::prompt_user("Apply changes?", true)? { + if let Err(err) = self.apply_update() { + warn!("Failed to apply update changes: {}", err); + } + } + + self.cleanup(); + + Ok(()) } pub fn run_update_shell(&mut self, command: &str) -> Result<()> { - let mountpoint = self.mountpoint().map_err(|e| { - let _ = self.cleanup(); - format_err!("Could not run update shell: {}", e) - })?; - let mut alloc = BridgeAllocator::default_bridge()?; let addr = alloc.allocate_address_for(&self.name())?; let gw = alloc.gateway(); @@ -82,7 +311,7 @@ impl <'a> Update<'a> { .arg(format!("--setenv=IFCONFIG_GW={}", gw)) .arg("--quiet") .arg(format!("--machine={}", self.name())) - .arg(format!("--directory={}", mountpoint)) + .arg(format!("--directory={}", &self.mountpath.display())) .arg("--network-zone=clear") .arg("/bin/bash") .arg("-c") @@ -92,54 +321,36 @@ impl <'a> Update<'a> { let _ = self.cleanup(); e })?; - self.deactivate_update()?; Ok(()) } - fn deactivate_update(&self) -> Result<()> { - match self.update_type { - UpdateType::Sealed(ref update_image) => update_image.deactivate()?, - UpdateType::Unsealed => self.realmfs.deactivate()?, - UpdateType::NotSetup => return Ok(()), - }; + fn apply_update(&mut self) -> Result<()> { + self.unmount_update_image(); + self.seal()?; + self.rotate()?; Ok(()) } - pub fn apply_update(&mut self) -> Result<()> { - match self.update_type { - UpdateType::Sealed(ref update_image) => { - update_image.seal(Some(self.realmfs.name()))?; - fs::rename(update_image.path(), self.realmfs.path())?; - self.cleanup() - }, - UpdateType::Unsealed => self.cleanup(), - UpdateType::NotSetup => Ok(()), - } - } - fn name(&self) -> String { - format!("{}-update", self.realmfs.name()) - } + fn rotate(&self) -> Result<()> { + let backup = |n: usize| + Path::new(RealmFS::BASE_PATH) + .join(format!("{}-realmfs.img.{}", self.realmfs.name(), n)); - pub fn cleanup(&mut self) -> Result<()> { - match self.update_type { - UpdateType::Sealed(ref update_image) => { - update_image.deactivate()?; - if update_image.path().exists() { - fs::remove_file(update_image.path())?; - } - }, - UpdateType::Unsealed => { - self.realmfs.deactivate()?; + for i in (1..NUM_BACKUPS).rev() { + let from = backup(i - 1); + if from.exists() { + fs::rename(from, backup(i))?; } - _ => {}, - } - self.update_type = UpdateType::NotSetup; - - if self.network_allocated { - BridgeAllocator::default_bridge()? - .free_allocation_for(&self.name())?; - self.network_allocated = false; } + fs::rename(self.realmfs.path(), backup(0))?; + fs::rename(self.target(), self.realmfs.path())?; Ok(()) } } + +impl <'a> Drop for Update<'a> { + fn drop(&mut self) { + self.cleanup(); + } +} + diff --git a/libcitadel/src/resource.rs b/libcitadel/src/resource.rs index 34a9f48..5b3f790 100644 --- a/libcitadel/src/resource.rs +++ b/libcitadel/src/resource.rs @@ -91,7 +91,7 @@ impl ResourceImage { &self.path } - fn verity(&self) -> Verity { + fn verity(&self) -> Result { Verity::new(self.path()) } @@ -171,12 +171,6 @@ impl ResourceImage { info!("writing rootfs image to {}", partition.path().display()); cmd_with_output!("/bin/dd", "if={} of={} bs=4096 skip=1", self.path.display(), partition.path().display())?; - /* - let args = format!("if={} of={} bs=4096 skip=1", - self.path.display(), partition.path().display()); - util::exec_cmdline_quiet("/bin/dd", args)?; - */ - self.header.set_status(ImageHeader::STATUS_NEW); self.header.write_partition(partition.path())?; @@ -212,7 +206,8 @@ impl ResourceImage { if !self.has_verity_hashtree() { self.generate_verity_hashtree()?; } - self.verity().setup(&self.metainfo()) + let verity = self.verity()?; + verity.setup() } pub fn generate_verity_hashtree(&self) -> Result<()> { @@ -223,8 +218,8 @@ impl ResourceImage { self.decompress()?; } info!("Generating dm-verity hash tree for image {}", self.path.display()); -// verity::generate_image_hashtree(self.path(), self.metainfo().nblocks(), self.metainfo().verity_salt())?; - self.verity().generate_image_hashtree(&self.metainfo())?; + let verity = self.verity()?; + verity.generate_image_hashtree()?; self.header.set_flag(ImageHeader::FLAG_HASH_TREE); self.header.write_header_to(self.path())?; Ok(()) @@ -235,8 +230,8 @@ impl ResourceImage { self.generate_verity_hashtree()?; } info!("Verifying dm-verity hash tree"); - self.verity().verify(&self.metainfo()) -// verity::verify_image(self.path(), &self.metainfo()) + let verity = self.verity()?; + verity.verify() } pub fn generate_shasum(&self) -> Result { diff --git a/libcitadel/src/system/lock.rs b/libcitadel/src/system/lock.rs index 0b6f764..07f0d42 100644 --- a/libcitadel/src/system/lock.rs +++ b/libcitadel/src/system/lock.rs @@ -5,6 +5,15 @@ use std::path::{Path,PathBuf}; use crate::Result; +/// +/// Create a lockfile and acquire an exclusive lock with flock(2) +/// +/// The lock can either be acquired by blocking until available or +/// by failing immediately if the lock is already held. +/// +/// The lock is released and the lockfile is removed when `FileLock` +/// instance is dropped. +/// pub struct FileLock { file: File, path: PathBuf, @@ -12,11 +21,25 @@ pub struct FileLock { impl FileLock { + pub fn nonblocking_acquire>(path: P) -> Result> { + let file = Self::open_lockfile(path.as_ref())?; + let flock = FileLock { + file, + path: path.as_ref().into(), + }; + + if flock.lock(false)? { + Ok(Some(flock)) + } else { + Ok(None) + } + } + pub fn acquire>(path: P) -> Result { let path = path.as_ref().to_path_buf(); let file = Self::open_lockfile(&path)?; let flock = FileLock { file, path }; - flock.lock()?; + flock.lock(true)?; Ok(flock) } @@ -27,6 +50,9 @@ impl FileLock { } } + // Make a few attempts just in case we try to open lockfile + // at exact moment another process is releasing and deleting + // file. for _ in 0..3 { if let Some(file) = Self::try_create_lockfile(path)? { return Ok(file); @@ -35,7 +61,7 @@ impl FileLock { return Ok(file); } } - Err(format_err!("unable to acquire lockfile {}", path.display() )) + Err(format_err!("unable to open lockfile {}", path.display() )) } fn try_create_lockfile(path: &Path) -> Result> { @@ -55,19 +81,32 @@ impl FileLock { } fn unlock(&self) -> Result<()> { - self.flock(libc::LOCK_UN) - } - - fn lock(&self) -> Result<()> { - self.flock(libc::LOCK_EX) - } - - fn flock(&self, flag: libc::c_int) -> Result<()> { - if unsafe { libc::flock(self.file.as_raw_fd(), flag) } < 0 { - return Err(Error::last_os_error().into()); - } + self.flock(libc::LOCK_UN, true)?; Ok(()) } + + fn lock(&self, block: bool) -> Result { + if block { + self.flock(libc::LOCK_EX, true) + } else { + self.flock(libc::LOCK_EX | libc::LOCK_NB, false) + } + } + + fn flock(&self, flag: libc::c_int, block: bool) -> Result { + if unsafe { libc::flock(self.file.as_raw_fd(), flag) } < 0 { + let errno = Self::last_errno(); + if !block && errno == libc::EWOULDBLOCK { + return Ok(false); + } + return Err(Error::from_raw_os_error(errno).into()); + } + Ok(true) + } + + pub fn last_errno() -> i32 { + unsafe { *libc::__errno_location() } + } } impl Drop for FileLock { diff --git a/libcitadel/src/util.rs b/libcitadel/src/util.rs index e17d40d..ac75993 100644 --- a/libcitadel/src/util.rs +++ b/libcitadel/src/util.rs @@ -209,3 +209,9 @@ pub fn chown_tree(base: &Path, chown_to: (u32,u32), include_base: bool) -> Resul } Ok(()) } + +pub fn is_euid_root() -> bool { + unsafe { + libc::geteuid() == 0 + } +} diff --git a/libcitadel/src/verity.rs b/libcitadel/src/verity.rs index 0f02d7d..6280f1b 100644 --- a/libcitadel/src/verity.rs +++ b/libcitadel/src/verity.rs @@ -3,19 +3,25 @@ use std::collections::HashMap; use std::fs::{self, OpenOptions,File}; use std::io; -use crate::{Result, MetaInfo, Partition, LoopDevice, Mountpoint}; +use crate::{Result, MetaInfo, Partition, LoopDevice, ImageHeader}; +use std::sync::Arc; pub struct Verity { image: PathBuf, + metainfo: Arc, } impl Verity { const VERITYSETUP: &'static str = "/sbin/veritysetup"; - pub fn new(image: impl AsRef) -> Self { + pub fn new(image: impl AsRef) -> Result { + let header = ImageHeader::from_file(image.as_ref())?; let image = image.as_ref().to_path_buf(); - Verity { image } + Ok(Verity { + image, + metainfo: header.metainfo(), + }) } pub fn generate_initial_hashtree(&self, output: impl AsRef) -> Result { @@ -25,15 +31,15 @@ impl Verity { Ok(VerityOutput::parse(&output)) } - pub fn generate_image_hashtree(&self, metainfo: &MetaInfo) -> Result { - let verity_salt = metainfo.verity_salt(); - self.generate_image_hashtree_with_salt(metainfo, verity_salt) + pub fn generate_image_hashtree(&self) -> Result { + let verity_salt = self.metainfo.verity_salt(); + let nblocks = self.metainfo.nblocks(); + self.generate_image_hashtree_with_salt(verity_salt, nblocks) } - pub fn generate_image_hashtree_with_salt(&self, metainfo: &MetaInfo, salt: &str) -> Result { + pub fn generate_image_hashtree_with_salt(&self, salt: &str, nblocks: usize) -> Result { let verityfile = self.image.with_extension("verity"); - let nblocks = metainfo.nblocks(); // Make sure file size is correct or else verity tree will be appended in wrong place let meta = self.image.metadata()?; @@ -54,19 +60,20 @@ impl Verity { Ok(vout) } - pub fn verify(&self, metainfo: &MetaInfo) -> Result { + pub fn verify(&self) -> Result { LoopDevice::with_loop(self.path(), Some(4096), true, |loopdev| { cmd_ok!(Self::VERITYSETUP, "--hash-offset={} verify {} {} {}", - metainfo.nblocks() * 4096, - loopdev, loopdev, metainfo.verity_root()) + self.metainfo.nblocks() * 4096, + loopdev, loopdev, self.metainfo.verity_root()) }) } - pub fn setup(&self, metainfo: &MetaInfo) -> Result { + pub fn setup(&self) -> Result { + info!("creating loop and dm-verity devices for {:?}", self.path()); LoopDevice::with_loop(self.path(), Some(4096), true, |loopdev| { - let devname = Self::device_name(metainfo); + let devname = self.device_name(); let srcdev = loopdev.to_string(); - Self::setup_device(&srcdev, &devname, metainfo)?; + Self::setup_device(&srcdev, &devname, &self.metainfo)?; Ok(devname) }) } @@ -82,21 +89,17 @@ impl Verity { cmd!(Self::VERITYSETUP, "close {}", device_name) } - pub fn device_name(metainfo: &MetaInfo) -> String { - if metainfo.image_type() == "rootfs" { + fn device_name(&self) -> String { + if self.metainfo.image_type() == "rootfs" { String::from("rootfs") - } else if metainfo.image_type() == "realmfs" { - let name = metainfo.realmfs_name().unwrap_or("unknown"); - format!("verity-realmfs-{}-{}", name, metainfo.verity_tag()) + } else if self.metainfo.image_type() == "realmfs" { + let name = self.metainfo.realmfs_name().unwrap_or("unknown"); + format!("verity-realmfs-{}-{}", name, self.metainfo.verity_tag()) } else { - format!("verity-{}-{}", metainfo.image_type(), metainfo.verity_tag()) + format!("verity-{}-{}", self.metainfo.image_type(), self.metainfo.verity_tag()) } } - pub fn device_name_for_mountpoint(mountpoint: &Mountpoint) -> String { - format!("verity-realmfs-{}-{}", mountpoint.realmfs(), mountpoint.tag()) - } - fn setup_device(srcdev: &str, devname: &str, metainfo: &MetaInfo) -> Result<()> { let nblocks = metainfo.nblocks(); let verity_root = metainfo.verity_root();