Compare commits

..

2 Commits

35 changed files with 2509 additions and 1487 deletions

1828
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -19,3 +19,8 @@ byteorder = "1"
dbus = "0.8.4"
pwhash = "1.0"
tempfile = "3"
ed25519-dalek = {version = "2.1", features = ["pem"]}
anyhow = "1.0"
reqwest = {version = "0.12", features = ["blocking"]}
glob = "0.3"
serde_cbor = "0.11"

View File

@ -8,7 +8,7 @@ use std::path::Path;
mod live;
mod disks;
mod rootfs;
pub mod rootfs;
pub fn main(args: Vec<String>) {
if CommandLine::debug() {

View File

@ -94,7 +94,7 @@ fn choose_revert_partition(best: Option<Partition>) -> Option<Partition> {
best
}
fn choose_boot_partiton(scan: bool, revert_rootfs: bool) -> Result<Partition> {
pub fn choose_boot_partiton(scan: bool, revert_rootfs: bool) -> Result<Partition> {
let mut partitions = Partition::rootfs_partitions()?;
if scan {
@ -136,8 +136,11 @@ fn compare_boot_partitions(a: Option<Partition>, b: Partition) -> Option<Partiti
}
// Compare versions and channels
let a_v = a.metainfo().version();
let b_v = b.metainfo().version();
let bind_a = a.metainfo();
let bind_b = b.metainfo();
let a_v = bind_a.version();
let b_v = bind_b.version();
// Compare versions only if channels match
if a.metainfo().channel() == b.metainfo().channel() {

View File

@ -0,0 +1,293 @@
use crate::{update, Path};
use anyhow::{bail, Context, Result};
use clap::ArgMatches;
use ed25519_dalek::{pkcs8::DecodePublicKey, VerifyingKey};
use libcitadel::updates::UPDATE_SERVER_HOSTNAME;
use libcitadel::{updates, updates::CitadelVersionStruct};
use libcitadel::{OsRelease, ResourceImage};
use std::io::prelude::*;
use std::str::FromStr;
const UPDATE_SERVER_KEY_PATH: &str = "/etc/citadel/update_server_key.pub";
pub fn check() -> Result<()> {
let current_version = get_current_os_config()?;
let server_citadel_version = fetch_and_verify_version_cbor(&current_version)?;
let components_to_upgrade =
compare_citadel_versions(&current_version, &server_citadel_version)?;
if components_to_upgrade.len() == 1 {
println!(
"We found the following component to upgrade: {}",
components_to_upgrade[0]
);
} else if components_to_upgrade.len() > 1 {
println!("We found the following components to upgrade:");
for component in components_to_upgrade {
println!("{}", component);
}
} else {
println!("Your system is up to date!");
}
Ok(())
}
pub fn download(sub_matches: &ArgMatches) -> Result<()> {
let current_version = &get_current_os_config()?;
let server_citadel_version = &fetch_and_verify_version_cbor(&current_version)?;
let mut path = "";
if sub_matches.get_flag("rootfs") {
path = &server_citadel_version.component_version[0].file_path;
} else if sub_matches.get_flag("kernel") {
path = &server_citadel_version.component_version[1].file_path;
} else if sub_matches.get_flag("extra") {
path = &server_citadel_version.component_version[2].file_path;
}
download_file(path)?;
Ok(())
}
pub fn read_remote() -> Result<()> {
let server_citadel_version = fetch_and_verify_version_cbor(&get_current_os_config()?)?;
println!("Server offers:\n{server_citadel_version}");
Ok(())
}
pub fn upgrade() -> Result<()> {
// First get access to the current citadel's parameters
let current_version = &get_current_os_config()?;
let server_citadel_version = &fetch_and_verify_version_cbor(&current_version)?;
// What do we need to upgrade?
let components_to_upgrade =
compare_citadel_versions(&current_version, &server_citadel_version)?;
if components_to_upgrade.len() == 1 {
println!("We found a component to upgrade!");
let allow_download = prompt_user_for_permission_to_download(&components_to_upgrade[0])?;
if allow_download {
let save_path = download_file(&components_to_upgrade[0].file_path)?;
// run citadel-update to upgrade
println!("Installing image");
update::install_image(&save_path, 0)?;
println!("Image installed correctly");
} else {
println!("Ok! Maybe later");
}
} else if components_to_upgrade.len() > 1 {
println!("We found some components to upgrade!");
for component in components_to_upgrade {
let allow_download = prompt_user_for_permission_to_download(&component)?;
if allow_download {
let save_path = download_file(&component.file_path)?;
println!("Installing image");
update::install_image(&save_path, 0)?;
println!("Image installed correctly");
} else {
println!("Ok! Maybe later");
}
}
} else {
println!("Your system is up to date!");
}
Ok(())
}
pub fn reinstall(sub_matches: &ArgMatches) -> Result<()> {
let current_version = &get_current_os_config()?;
let server_citadel_version = &fetch_and_verify_version_cbor(&current_version)?;
let mut path = "";
if sub_matches.get_flag("rootfs") {
path = &server_citadel_version.component_version[0].file_path;
} else if sub_matches.get_flag("kernel") {
path = &server_citadel_version.component_version[1].file_path;
} else if sub_matches.get_flag("extra") {
path = &server_citadel_version.component_version[2].file_path;
}
let save_path = download_file(path)?;
update::install_image(&save_path, 0)?;
Ok(())
}
/// Returns a vec of ComponentVersion structs of the components which can be upgraded
fn compare_citadel_versions(
current: &CitadelVersionStruct,
offered: &CitadelVersionStruct,
) -> Result<Vec<updates::AvailableComponentVersion>> {
let mut update_vec: Vec<updates::AvailableComponentVersion> = Vec::new();
// safety checks
if current.channel != offered.channel {
panic!("Error: channels do not match");
} else if current.client != offered.client {
panic!("Error: clients do not match");
} else if current.publisher != offered.publisher {
panic!("Error: publishers do not match");
}
for i in 0..current.component_version.len() {
if current.component_version[i] < offered.component_version[i] {
update_vec.push(offered.component_version[i].clone());
}
}
Ok(update_vec)
}
// We need to get the version of the rootfs, kernel and extra images currently installed
fn get_current_os_config() -> Result<updates::CitadelVersionStruct> {
let client = OsRelease::citadel_client().context("Failed to find client of current system")?;
let channel = OsRelease::citadel_channel().context("Failed to find channel of current system")?;
let publisher = OsRelease::citadel_publisher().context("Failed to find publisher of current system")?;
let metainfo;
// choose best partion to boot from as the partition to compare versions with
let rootfs_version = match crate::boot::rootfs::choose_boot_partiton(false, false) {
Ok(part) => {metainfo = part.header().metainfo();
metainfo.version() }
Err(e) => bail!("Rootfs version not found. Error: {e}"),
};
// Get highest values of image versions
let kernel_resource = ResourceImage::find("kernel")?.metainfo();
let kernel_version = kernel_resource.version();
let extra_resource = ResourceImage::find("extra")?.metainfo();
let extra_version = extra_resource.version();
let mut component_version = Vec::new();
component_version.push(updates::AvailableComponentVersion {
component: updates::Component::Rootfs,
version: rootfs_version.to_owned(),
file_path: "".to_owned(),
});
component_version.push(updates::AvailableComponentVersion {
component: updates::Component::Kernel,
version: kernel_version.to_owned(),
file_path: "".to_owned(),
});
component_version.push(updates::AvailableComponentVersion {
component: updates::Component::Extra,
version: extra_version.to_owned(),
file_path: "".to_owned(),
});
let current_version_struct = updates::CitadelVersionStruct {
client: client.to_owned(),
channel: channel.to_owned(),
component_version,
publisher: publisher.to_owned(),
};
Ok(current_version_struct)
}
fn fetch_and_verify_version_cbor(
current_citadel_version: &updates::CitadelVersionStruct,
) -> Result<updates::CitadelVersionStruct> {
let url = format!(
"https://{}/{}/{}/version.cbor",
UPDATE_SERVER_HOSTNAME, current_citadel_version.client, current_citadel_version.channel
);
let version_file_bytes = reqwest::blocking::get(&url)?
.bytes()
.context(format!("Failed to get version_file_bytes from {url}"))?;
let crypto_container: updates::CryptoContainerFile =
serde_cbor::from_slice(&version_file_bytes)
.context(format!("Failed to parse version.cbor from {}", url))?;
// find update server public key kept in the rootfs
let mut file = std::fs::File::open(UPDATE_SERVER_KEY_PATH).context(format!(
"Failed to open update_server_key file from {}",
UPDATE_SERVER_KEY_PATH
))?;
let mut contents = String::new();
file.read_to_string(&mut contents)?;
let public_key = VerifyingKey::from_public_key_pem(&contents)
.context("Failed to parse public key from file.")?;
let signature = ed25519_dalek::Signature::from_str(&crypto_container.signature)?;
// verify signature
public_key.verify_strict(&crypto_container.serialized_citadel_version, &signature)
.context("We failed to verify the signature update release file. Please make sure the key at /etc/citade/update_server_key.pub matches the one publicly linked to your update provider.")?;
// construct the struct
let citadel_version_struct: updates::CitadelVersionStruct =
serde_cbor::from_slice(&crypto_container.serialized_citadel_version)?;
Ok(citadel_version_struct)
}
fn prompt_user_for_permission_to_download(
component: &updates::AvailableComponentVersion,
) -> Result<bool> {
println!(
"Would you like to download and install the new version of the {} image with version {}? (y/n)",
component.component, component.version
);
loop {
let stdin = std::io::stdin();
let mut user_input = String::new();
stdin.read_line(&mut user_input)?;
if user_input.trim() == "y" {
return Ok(true);
} else {
return Ok(false);
}
}
}
fn download_file(path: &str) -> Result<std::path::PathBuf> {
let client = reqwest::blocking::Client::new();
let url = format!("https://{UPDATE_SERVER_HOSTNAME}/{path}");
println!("Downloading from {url}");
let component_download_response = client.get(&url).send()?;
if !component_download_response.status().is_success() {
anyhow::bail!(
"Failed to download image from {}. Server returned error {}",
path,
component_download_response.status()
);
}
let path = Path::new(path);
let path = format!("/tmp/{}", path.file_name().unwrap().to_str().unwrap());
let mut content = std::io::Cursor::new(component_download_response.bytes()?);
let mut file =
std::fs::File::create(&path).context(format!("Failed to create file at {path}"))?;
std::io::copy(&mut content, &mut file)?;
println!("Saved file to {path}");
Ok(std::path::PathBuf::from(path))
}

View File

@ -0,0 +1,55 @@
use clap::{arg, command, ArgAction, Command};
use std::process::exit;
use libcitadel::util;
mod fetch;
pub fn main() {
if !util::is_euid_root() {
println!("Please run this program as root");
exit(1);
}
let matches = command!()
.subcommand_required(true)
.subcommand(Command::new("check").about("Check for updates from remote server"))
.subcommand(
Command::new("download")
.about("Download a specific file from the server")
.arg(arg!(-r --rootfs "rootfs component").action(ArgAction::SetTrue))
.arg(arg!(-k --kernel "kernel component").action(ArgAction::SetTrue))
.arg(arg!(-e --extra "extra component").action(ArgAction::SetTrue))
.arg_required_else_help(true),
)
.subcommand(
Command::new("read-remote")
.about("Read the remote server and print information on versions offered"),
)
.subcommand(
Command::new("upgrade")
.about("Download and install all components found on the server to be more recent than currently installed on system")
)
.subcommand(
Command::new("reinstall")
.about("Download and install a specific component even if the server's component version is not greater than currently installed")
.arg(arg!(-r --rootfs "rootfs component").action(ArgAction::SetTrue))
.arg(arg!(-k --kernel "kernel component").action(ArgAction::SetTrue))
.arg(arg!(-e --extra "extra component").action(ArgAction::SetTrue))
.arg_required_else_help(true),
)
.get_matches();
let result = match matches.subcommand() {
Some(("check", _sub_matches)) => fetch::check(),
Some(("download", sub_matches)) => fetch::download(sub_matches),
Some(("read-remote", _sub_matches)) => fetch::read_remote(),
Some(("upgrade", _sub_matches)) => fetch::upgrade(),
Some(("reinstall", sub_matches)) => fetch::reinstall(sub_matches),
_ => unreachable!("Please pass a subcommand"),
};
if let Err(ref e) = result {
println!("Error: {}", e);
exit(1);
}
}

View File

@ -250,9 +250,9 @@ fn install_image(arg_matches: &ArgMatches) -> Result<()> {
if kernel_version.chars().any(|c| c == '/') {
bail!("Kernel version field has / char");
}
format!("citadel-kernel-{}-{:03}.img", kernel_version, metainfo.version())
format!("citadel-kernel-{}-{}.img", kernel_version, metainfo.version())
} else {
format!("citadel-extra-{:03}.img", metainfo.version())
format!("citadel-extra-{}.img", metainfo.version())
};
if !metainfo.channel().chars().all(|c| c.is_ascii_lowercase()) {

View File

@ -16,6 +16,7 @@ mod mkimage;
mod realmfs;
mod sync;
mod update;
mod fetch;
fn main() {
let exe = match env::current_exe() {
@ -39,6 +40,8 @@ fn main() {
realmfs::main();
} else if exe == Path::new("/usr/bin/citadel-update") {
update::main(args);
} else if exe == Path::new("/usr/bin/citadel-fetch") {
fetch::main();
} else if exe == Path::new("/usr/libexec/citadel-desktop-sync") {
sync::main(args);
} else if exe == Path::new("/usr/libexec/citadel-run") {
@ -60,6 +63,7 @@ fn dispatch_command(args: Vec<String>) {
"image" => image::main(),
"realmfs" => realmfs::main(),
"update" => update::main(rebuild_args("citadel-update", args)),
"fetch" => update::main(rebuild_args("citadel-fetch", args)),
"mkimage" => mkimage::main(rebuild_args("citadel-mkimage", args)),
"sync" => sync::main(rebuild_args("citadel-desktop-sync", args)),
"run" => do_citadel_run(rebuild_args("citadel-run", args)),

View File

@ -38,15 +38,15 @@ impl UpdateBuilder {
}
fn target_filename(&self) -> String {
format!("citadel-{}-{}-{:03}.img", self.config.img_name(), self.config.channel(), self.config.version())
format!("citadel-{}-{}-{}.img", self.config.img_name(), self.config.channel(), self.config.version())
}
fn build_filename(config: &BuildConfig) -> String {
format!("citadel-{}-{}-{:03}", config.image_type(), config.channel(), config.version())
format!("citadel-{}-{}-{}", config.image_type(), config.channel(), config.version())
}
fn verity_filename(&self) -> String {
format!("verity-hash-{}-{:03}", self.config.image_type(), self.config.version())
format!("verity-hash-{}-{}", self.config.image_type(), self.config.version())
}
pub fn build(&mut self) -> Result<()> {
@ -154,7 +154,7 @@ impl UpdateBuilder {
bail!("failed to compress {:?}: {}", self.image(), err);
}
// Rename back to original image_data filename
util::rename(self.image().with_extension("xz"), self.image())?;
util::rename(util::append_to_path(self.image(), ".xz"), self.image())?;
}
Ok(())
}
@ -217,7 +217,7 @@ impl UpdateBuilder {
writeln!(v, "realmfs-name = \"{}\"", name)?;
}
writeln!(v, "channel = \"{}\"", self.config.channel())?;
writeln!(v, "version = {}", self.config.version())?;
writeln!(v, "version = \"{}\"", self.config.version())?;
writeln!(v, "timestamp = \"{}\"", self.config.timestamp())?;
writeln!(v, "nblocks = {}", self.nblocks.unwrap())?;
writeln!(v, "shasum = \"{}\"", self.shasum.as_ref().unwrap())?;

View File

@ -9,7 +9,7 @@ pub struct BuildConfig {
#[serde(rename = "image-type")]
image_type: String,
channel: String,
version: usize,
version: String,
timestamp: String,
source: String,
#[serde(default)]
@ -102,8 +102,8 @@ impl BuildConfig {
self.realmfs_name.as_ref().map(|s| s.as_str())
}
pub fn version(&self) -> usize {
self.version
pub fn version(&self) -> &str {
&self.version
}
pub fn channel(&self) -> &str {

View File

@ -93,7 +93,7 @@ fn create_tmp_copy(path: &Path) -> Result<PathBuf> {
Ok(path)
}
fn install_image(path: &Path, flags: u32) -> Result<()> {
pub fn install_image(path: &Path, flags: u32) -> Result<()> {
if !path.exists() || path.file_name().is_none() {
bail!("file path {} does not exist", path.display());
}
@ -140,7 +140,7 @@ fn prepare_image(image: &ResourceImage, flags: u32) -> Result<()> {
}
fn install_extra_image(image: &ResourceImage) -> Result<()> {
let filename = format!("citadel-extra-{:03}.img", image.header().metainfo().version());
let filename = format!("citadel-extra-{}.img", image.header().metainfo().version());
install_image_file(image, filename.as_str())?;
remove_old_extra_images(image)?;
Ok(())
@ -186,7 +186,7 @@ fn install_kernel_image(image: &mut ResourceImage) -> Result<()> {
info!("kernel version is {}", kernel_version);
install_kernel_file(image, &kernel_version)?;
let filename = format!("citadel-kernel-{}-{:03}.img", kernel_version, version);
let filename = format!("citadel-kernel-{}-{}.img", kernel_version, version);
install_image_file(image, &filename)?;
let all_versions = all_boot_kernel_versions()?;

View File

@ -7,12 +7,10 @@
<busconfig>
<policy user="root">
<allow own="com.subgraph.realms"/>
<allow own="com.subgraph.Realms2"/>
</policy>
<policy context="default">
<allow send_destination="com.subgraph.realms"/>
<allow send_destination="com.subgraph.Realms2"/>
<allow send_destination="com.subgraph.realms"
send_interface="org.freedesktop.DBus.Properties"/>
<allow send_destination="com.subgraph.realms"

View File

@ -20,6 +20,8 @@ walkdir = "2"
dbus = "0.6"
posix-acl = "1.0.0"
procfs = "0.12.0"
semver = "1.0"
clap = "4.5"
[dependencies.inotify]
version = "0.8"

View File

@ -84,8 +84,8 @@ impl OsRelease {
OsRelease::get_value("CITADEL_IMAGE_PUBKEY")
}
pub fn citadel_rootfs_version() -> Option<usize> {
OsRelease::get_int_value("CITADEL_ROOTFS_VERSION")
pub fn citadel_rootfs_version() -> Option<&'static str> {
OsRelease::get_value("CITADEL_ROOTFS_VERSION")
}
pub fn citadel_kernel_version() -> Option<&'static str> {
@ -96,6 +96,14 @@ impl OsRelease {
OsRelease::get_value("CITADEL_KERNEL_ID")
}
pub fn citadel_client() -> Option<&'static str> {
OsRelease::get_value("CITADEL_CLIENT")
}
pub fn citadel_publisher() -> Option<&'static str> {
OsRelease::get_value("CITADEL_PUBLISHER")
}
fn _get_value(&self, key: &str) -> Option<&str> {
self.vars.get(key).map(|v| v.as_str())
}

View File

@ -453,7 +453,7 @@ pub struct MetaInfo {
realmfs_owner: Option<String>,
#[serde(default)]
version: u32,
version: String,
#[serde(default)]
timestamp: String,
@ -508,8 +508,8 @@ impl MetaInfo {
Self::str_ref(&self.realmfs_owner)
}
pub fn version(&self) -> u32 {
self.version
pub fn version(&self) -> &str {
&self.version
}
pub fn timestamp(&self) -> &str {

View File

@ -20,6 +20,7 @@ pub mod symlink;
mod realm;
pub mod terminal;
mod system;
pub mod updates;
pub mod flatpak;

View File

@ -12,9 +12,7 @@ use dbus::{Connection, BusType, ConnectionItem, Message, Path};
use inotify::{Inotify, WatchMask, WatchDescriptor, Event};
pub enum RealmEvent {
Starting(Realm),
Started(Realm),
Stopping(Realm),
Stopped(Realm),
New(Realm),
Removed(Realm),
@ -24,9 +22,7 @@ pub enum RealmEvent {
impl Display for RealmEvent {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {
RealmEvent::Starting(ref realm) => write!(f, "RealmStarting({})", realm.name()),
RealmEvent::Started(ref realm) => write!(f, "RealmStarted({})", realm.name()),
RealmEvent::Stopping(ref realm) => write!(f, "RealmStopping({})", realm.name()),
RealmEvent::Stopped(ref realm) => write!(f, "RealmStopped({})", realm.name()),
RealmEvent::New(ref realm) => write!(f, "RealmNew({})", realm.name()),
RealmEvent::Removed(ref realm) => write!(f, "RealmRemoved({})", realm.name()),

View File

@ -194,13 +194,7 @@ impl RealmManager {
return Ok(());
}
info!("Starting realm {}", realm.name());
self.inner().events.send_event(RealmEvent::Starting(realm.clone()));
if let Err(err) = self._start_realm(realm, &mut HashSet::new()) {
self.inner().events.send_event(RealmEvent::Stopped(realm.clone()));
return Err(err);
}
self.inner().events.send_event(RealmEvent::Started(realm.clone()));
self._start_realm(realm, &mut HashSet::new())?;
if !Realms::is_some_realm_current() {
self.inner_mut().realms.set_realm_current(realm)
@ -298,7 +292,6 @@ impl RealmManager {
}
info!("Stopping realm {}", realm.name());
self.inner().events.send_event(RealmEvent::Stopping(realm.clone()));
if realm.config().flatpak() {
if let Err(err) = self.stop_gnome_software_sandbox(realm) {
@ -307,12 +300,8 @@ impl RealmManager {
}
realm.set_active(false);
if let Err(err) = self.systemd.stop_realm(realm) {
self.inner().events.send_event(RealmEvent::Stopped(realm.clone()));
return Err(err);
}
self.systemd.stop_realm(realm)?;
realm.cleanup_rootfs();
self.inner().events.send_event(RealmEvent::Stopped(realm.clone()));
if realm.is_current() {
self.choose_some_current_realm();

View File

@ -169,20 +169,6 @@ impl Realm {
self.inner.write().unwrap()
}
pub fn start(&self) -> Result<()> {
warn!("Realm({})::start()", self.name());
self.manager().start_realm(self)
}
pub fn stop(&self) -> Result<()> {
self.manager().stop_realm(self)
}
pub fn set_current(&self) -> Result<()> {
self.manager().set_current_realm(self)
}
pub fn is_active(&self) -> bool {
self.inner_mut().is_active()
}

View File

@ -40,6 +40,11 @@ impl ResourceImage {
pub fn find(image_type: &str) -> Result<Self> {
let channel = Self::rootfs_channel();
// search when citadel is installed
if let Some(image) = search_directory(format!("/storage/resources/{channel}/"), image_type, Some(&channel))? {
return Ok(image);
}
info!("Searching run directory for image {} with channel {}", image_type, channel);
if let Some(image) = search_directory(RUN_DIRECTORY, image_type, Some(&channel))? {
@ -353,6 +358,11 @@ impl ResourceImage {
if Mounts::is_source_mounted("/dev/mapper/citadel-storage")? {
return Ok(true);
}
if Mounts::is_source_mounted("/storage")? {
return Ok(true);
}
let path = Path::new("/dev/mapper/citadel-storage");
if !path.exists() {
return Ok(false);
@ -420,8 +430,11 @@ fn compare_images(a: Option<ResourceImage>, b: ResourceImage) -> Result<Resource
None => return Ok(b),
};
let ver_a = a.metainfo().version();
let ver_b = b.metainfo().version();
let bind_a = a.metainfo();
let bind_b = b.metainfo();
let ver_a = bind_a.version();
let ver_b = bind_b.version();
if ver_a > ver_b {
Ok(a)
@ -521,8 +534,14 @@ fn maybe_add_dir_entry(entry: &DirEntry,
return Ok(())
}
if image_type == "kernel" && (metainfo.kernel_version() != kernel_version || metainfo.kernel_id() != kernel_id) {
return Ok(());
if kernel_id.is_some() {
if image_type == "kernel" && (metainfo.kernel_version() != kernel_version || metainfo.kernel_id() != kernel_id) {
return Ok(());
}
} else { // in live mode, kernel_id is None
if image_type == "kernel" && metainfo.kernel_version() != kernel_version {
return Ok(());
}
}
images.push(ResourceImage::new(&path, header));

View File

@ -15,11 +15,12 @@ impl Mounts {
pub fn is_source_mounted<P: AsRef<Path>>(path: P) -> Result<bool> {
let path = path.as_ref();
let mounted = Self::load()?
.mounts()
.any(|m| m.source_path() == path);
Ok(mounted)
for i in Self::load()?.mounts() {
if i.line.contains(&path.display().to_string()) {
return Ok(true)
}
}
Ok(false)
}
pub fn is_target_mounted<P: AsRef<Path>>(path: P) -> Result<bool> {

97
libcitadel/src/updates.rs Normal file
View File

@ -0,0 +1,97 @@
use std::fmt;
use std::slice::Iter;
pub const UPDATE_SERVER_HOSTNAME: &str = "update.subgraph.com";
/// This struct embeds the CitadelVersion datastruct as well as the cryptographic validation of the that information
#[derive(Debug, Serialize, Deserialize)]
pub struct CryptoContainerFile {
pub serialized_citadel_version: Vec<u8>, // we serialize CitadelVersion
pub signature: String, // serialized CitadelVersion gets signed
pub signatory: String, // name of org or person who holds the key
}
/// This struct contains the entirety of the information needed to decide whether to update or not
#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord)]
pub struct CitadelVersionStruct {
pub client: String,
pub channel: String, // dev, prod ...
pub component_version: Vec<AvailableComponentVersion>,
pub publisher: String, // name of org or person who released this update
}
impl std::fmt::Display for CitadelVersionStruct {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
write!(
f,
"{} image with channel {} has components:\n",
self.client, self.channel
)?;
for i in &self.component_version {
write!(
f,
"\n{} with version {} at location {}",
i.component, i.version, i.file_path
)?;
}
Ok(())
}
}
#[derive(Debug, Serialize, Clone, Deserialize, PartialEq, Eq, Ord)]
pub struct AvailableComponentVersion {
pub component: Component, // rootfs, kernel or extra
pub version: String, // stored as semver
pub file_path: String,
}
impl PartialOrd for AvailableComponentVersion {
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
// absolutely require that the components be in the same order in all structs (rootfs, kernel, extra)
if &self.component != &other.component {
panic!("ComponentVersion comparison failed because comparing different components");
}
Some(
semver::Version::parse(&self.version)
.unwrap()
.cmp(&semver::Version::parse(&other.version).unwrap()),
)
}
}
impl std::fmt::Display for AvailableComponentVersion {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
write!(
f,
"{} image has version {}",
self.component, self.version
)
}
}
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Deserialize, Serialize, clap::ValueEnum)]
pub enum Component {
Rootfs,
Kernel,
Extra,
}
impl Component {
pub fn iterator() -> Iter<'static, Component> {
static COMPONENTS: [Component; 3] =
[Component::Rootfs, Component::Kernel, Component::Extra];
COMPONENTS.iter()
}
}
impl fmt::Display for Component {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {
Component::Rootfs => write!(f, "rootfs"),
Component::Kernel => write!(f, "kernel"),
&Component::Extra => write!(f, "extra"),
}
}
}

View File

@ -48,6 +48,12 @@ fn search_path(filename: &str) -> Result<PathBuf> {
bail!("could not find {} in $PATH", filename)
}
pub fn append_to_path(p: &Path, s: &str) -> PathBuf {
let mut p_osstr = p.as_os_str().to_owned();
p_osstr.push(s);
p_osstr.into()
}
pub fn ensure_command_exists(cmd: &str) -> Result<()> {
let path = Path::new(cmd);
if !path.is_absolute() {
@ -338,7 +344,6 @@ pub fn is_euid_root() -> bool {
}
}
fn utimes(path: &Path, atime: i64, mtime: i64) -> Result<()> {
let cstr = CString::new(path.as_os_str().as_bytes())
.expect("path contains null byte");

View File

@ -9,7 +9,7 @@ homepage = "https://subgraph.com"
[dependencies]
libcitadel = { path = "../libcitadel" }
rand = "0.8"
zvariant = "4.2.0"
zvariant = "2.7.0"
serde = { version = "1.0", features = ["derive"] }
zbus = "4.4.0"
zbus = "=2.0.0-beta.5"
gtk = { version = "0.14.0", features = ["v3_24"] }

View File

@ -1,10 +1,10 @@
use std::collections::HashMap;
use std::rc::Rc;
use zvariant::Type;
use zbus::dbus_proxy;
use zvariant::derive::Type;
use serde::{Serialize,Deserialize};
use zbus::proxy;
use zbus::blocking::Connection;
use crate::error::{Error, Result};
#[derive(Deserialize,Serialize,Type)]
@ -85,12 +85,10 @@ impl RealmConfig {
}
}
#[proxy(
#[dbus_proxy(
default_service = "com.subgraph.realms",
interface = "com.subgraph.realms.Manager",
default_path = "/com/subgraph/realms",
gen_blocking = true,
gen_async = false,
default_path = "/com/subgraph/realms"
)]
pub trait RealmsManager {
fn get_current(&self) -> zbus::Result<String>;
@ -104,7 +102,7 @@ pub trait RealmsManager {
impl RealmsManagerProxy<'_> {
pub fn connect() -> Result<Self> {
let connection = Connection::system()?;
let connection = zbus::Connection::new_system()?;
let proxy = RealmsManagerProxy::new(&connection)
.map_err(|_| Error::ManagerConnect)?;

View File

@ -6,11 +6,8 @@ edition = "2018"
[dependencies]
libcitadel = { path = "../libcitadel" }
async-io = "2.3.2"
blocking = "1.6.1"
event-listener = "5.3.1"
zbus = "4.4.0"
zvariant = "4.2.0"
zbus = "=2.0.0-beta.5"
zvariant = "2.7.0"
serde = { version = "1.0", features = ["derive"] }
serde_repr = "0.1.8"

View File

@ -1,16 +1,15 @@
use async_io::block_on;
use zbus::blocking::Connection;
use zbus::SignalContext;
use zbus::{Connection, ObjectServer};
use crate::realms_manager::{RealmsManagerServer, REALMS_SERVER_OBJECT_PATH, realm_status};
use libcitadel::{RealmEvent, Realm};
pub struct EventHandler {
connection: Connection,
realms_server: RealmsManagerServer,
}
impl EventHandler {
pub fn new(connection: Connection) -> Self {
EventHandler { connection }
pub fn new(connection: Connection, realms_server: RealmsManagerServer) -> Self {
EventHandler { connection, realms_server }
}
pub fn handle_event(&self, ev: &RealmEvent) {
@ -26,49 +25,44 @@ impl EventHandler {
RealmEvent::New(realm) => self.on_new(realm),
RealmEvent::Removed(realm) => self.on_removed(realm),
RealmEvent::Current(realm) => self.on_current(realm.as_ref()),
RealmEvent::Starting(_) => Ok(()),
RealmEvent::Stopping(_) => Ok(()),
}
}
fn with_signal_context<F>(&self, func: F) -> zbus::Result<()>
fn with_server<F>(&self, func: F) -> zbus::Result<()>
where
F: Fn(&SignalContext) -> zbus::Result<()>,
F: Fn(&RealmsManagerServer) -> zbus::Result<()>,
{
let object_server = self.connection.object_server();
let iface = object_server.interface::<_, RealmsManagerServer>(REALMS_SERVER_OBJECT_PATH)?;
let ctx = iface.signal_context();
func(ctx)
let mut object_server = ObjectServer::new(&self.connection);
object_server.at(REALMS_SERVER_OBJECT_PATH, self.realms_server.clone())?;
object_server.with(REALMS_SERVER_OBJECT_PATH, |iface: &RealmsManagerServer| func(iface))
}
fn on_started(&self, realm: &Realm) -> zbus::Result<()> {
let pid_ns = realm.pid_ns().unwrap_or(0);
let status = realm_status(realm);
self.with_signal_context(|ctx| block_on(RealmsManagerServer::realm_started(ctx, realm.name(), pid_ns, status)))
self.with_server(|server| server.realm_started(realm.name(), pid_ns, status))
}
fn on_stopped(&self, realm: &Realm) -> zbus::Result<()> {
let status = realm_status(realm);
self.with_signal_context(|ctx| block_on(RealmsManagerServer::realm_stopped(ctx, realm.name(), status)))
self.with_server(|server| server.realm_stopped(realm.name(), status))
}
fn on_new(&self, realm: &Realm) -> zbus::Result<()> {
let status = realm_status(realm);
let description = realm.notes().unwrap_or(String::new());
self.with_signal_context(|ctx| block_on(RealmsManagerServer::realm_new(ctx, realm.name(), &description, status)))
self.with_server(|server| server.realm_new(realm.name(), &description, status))
}
fn on_removed(&self, realm: &Realm) -> zbus::Result<()> {
self.with_signal_context(|ctx| block_on(RealmsManagerServer::realm_removed(ctx, realm.name())))
self.with_server(|server| server.realm_removed(realm.name()))
}
fn on_current(&self, realm: Option<&Realm>) -> zbus::Result<()> {
self.with_signal_context(|ctx| {
self.with_server(|server| {
match realm {
Some(realm) => block_on(RealmsManagerServer::realm_current(ctx, realm.name(), realm_status(realm))),
None => block_on(RealmsManagerServer::realm_current(ctx, "", 0)),
Some(realm) => server.realm_current(realm.name(), realm_status(realm)),
None => server.realm_current("", 0),
}
})
}

View File

@ -1,18 +1,14 @@
#[macro_use] extern crate libcitadel;
use std::env;
use std::sync::Arc;
use event_listener::{Event, Listener};
use zbus::blocking::Connection;
use zbus::fdo::ObjectManager;
use libcitadel::{Logger, LogLevel, Result, RealmManager};
use crate::next::{RealmsManagerServer2, REALMS2_SERVER_OBJECT_PATH};
use crate::realms_manager::{RealmsManagerServer, REALMS_SERVER_OBJECT_PATH};
use zbus::{Connection, fdo};
use libcitadel::{Logger, LogLevel, Result};
use crate::realms_manager::RealmsManagerServer;
mod realms_manager;
mod events;
mod next;
fn main() {
if let Err(e) = run_realm_manager() {
@ -20,43 +16,24 @@ fn main() {
}
}
fn register_realms_manager_server(connection: &Connection, realm_manager: &Arc<RealmManager>, quit_event: &Arc<Event>) -> Result<()> {
let server = RealmsManagerServer::load(&connection, realm_manager.clone(), quit_event.clone())
.map_err(context!("Loading realms server"))?;
connection.object_server().at(REALMS_SERVER_OBJECT_PATH, server).map_err(context!("registering realms manager object"))?;
Ok(())
}
fn register_realms2_manager_server(connection: &Connection, realm_manager: &Arc<RealmManager>, quit_event: &Arc<Event>) -> Result<()> {
let server2 = RealmsManagerServer2::load(&connection, realm_manager.clone(), quit_event.clone())
.map_err(context!("Loading realms2 server"))?;
connection.object_server().at(REALMS2_SERVER_OBJECT_PATH, server2).map_err(context!("registering realms manager object"))?;
connection.object_server().at(REALMS2_SERVER_OBJECT_PATH, ObjectManager).map_err(context!("registering ObjectManager"))?;
Ok(())
fn create_system_connection() -> zbus::Result<Connection> {
let connection = zbus::Connection::new_system()?;
fdo::DBusProxy::new(&connection)?.request_name("com.subgraph.realms", fdo::RequestNameFlags::AllowReplacement.into())?;
Ok(connection)
}
fn run_realm_manager() -> Result<()> {
Logger::set_log_level(LogLevel::Verbose);
let testing = env::args().skip(1).any(|s| s == "--testing");
let connection = create_system_connection()
.map_err(context!("ZBus Connection error"))?;
let connection = Connection::system()
.map_err(context!("ZBus Connection error"))?;
let mut object_server = RealmsManagerServer::register(&connection)?;
loop {
if let Err(err) = object_server.try_handle_next() {
warn!("Error handling DBus message: {}", err);
}
}
let realm_manager = RealmManager::load()?;
let quit_event = Arc::new(Event::new());
if testing {
register_realms2_manager_server(&connection, &realm_manager, &quit_event)?;
connection.request_name("com.subgraph.Realms2")
.map_err(context!("acquiring realms manager name"))?;
} else {
register_realms_manager_server(&connection, &realm_manager, &quit_event)?;
register_realms2_manager_server(&connection, &realm_manager, &quit_event)?;
connection.request_name("com.subgraph.realms")
.map_err(context!("acquiring realms manager name"))?;
};
quit_event.listen().wait();
Ok(())
}

View File

@ -1,201 +0,0 @@
use std::collections::HashMap;
use std::sync::Arc;
use std::sync::atomic::{AtomicBool, Ordering};
use serde::{Deserialize, Serialize};
use zbus::fdo;
use zvariant::Type;
use libcitadel::{OverlayType, Realm, GLOBAL_CONFIG};
use libcitadel::terminal::Base16Scheme;
use crate::next::manager::failed;
const BOOL_CONFIG_VARS: &[&str] = &[
"use-gpu", "use-wayland", "use-x11", "use-sound",
"use-shared-dir", "use-network", "use-kvm", "use-ephemeral-home",
"use-media-dir", "use-fuse", "use-flatpak", "use-gpu-card0"
];
fn is_bool_config_variable(variable: &str) -> bool {
BOOL_CONFIG_VARS.iter().any(|&s| s == variable)
}
#[derive(Deserialize,Serialize,Type)]
pub struct RealmConfigVars {
items: HashMap<String,String>,
}
impl RealmConfigVars {
fn new() -> Self {
RealmConfigVars { items: HashMap::new() }
}
pub fn new_global() -> Self {
Self::new_from_config(&GLOBAL_CONFIG)
}
fn new_from_realm(realm: &Realm) -> Self {
let config = realm.config();
Self::new_from_config(&config)
}
fn new_from_config(config: &libcitadel::RealmConfig) -> Self {
let mut vars = RealmConfigVars::new();
vars.add_bool("use-gpu", config.gpu());
vars.add_bool("use-gpu-card0", config.gpu_card0());
vars.add_bool("use-wayland", config.wayland());
vars.add_bool("use-x11", config.x11());
vars.add_bool("use-sound", config.sound());
vars.add_bool("use-shared-dir", config.shared_dir());
vars.add_bool("use-network", config.network());
vars.add_bool("use-kvm", config.kvm());
vars.add_bool("use-ephemeral-home", config.ephemeral_home());
vars.add_bool("use-media-dir", config.media_dir());
vars.add_bool("use-fuse", config.fuse());
vars.add_bool("use-flatpak", config.flatpak());
let overlay = match config.overlay() {
OverlayType::None => "none",
OverlayType::TmpFS => "tmpfs",
OverlayType::Storage => "storage",
};
vars.add("overlay", overlay);
let scheme = match config.terminal_scheme() {
Some(name) => name.to_string(),
None => String::new(),
};
vars.add("terminal-scheme", scheme);
vars.add("realmfs", config.realmfs());
vars
}
fn add_bool(&mut self, name: &str, val: bool) {
let valstr = if val { "true".to_string() } else { "false".to_string() };
self.add(name, valstr);
}
fn add<S,T>(&mut self, k: S, v: T) where S: Into<String>, T: Into<String> {
self.items.insert(k.into(), v.into());
}
}
#[derive(Clone)]
pub struct RealmConfig {
realm: Realm,
changed: Arc<AtomicBool>,
}
impl RealmConfig {
pub fn new(realm: Realm) -> Self {
let changed = Arc::new(AtomicBool::new(false));
RealmConfig { realm, changed }
}
fn mark_changed(&self) {
self.changed.store(true, Ordering::Relaxed);
}
fn is_changed(&self) -> bool {
self.changed.load(Ordering::Relaxed)
}
pub fn config_vars(&self) -> RealmConfigVars {
RealmConfigVars::new_from_realm(&self.realm)
}
fn set_bool_var(&mut self, var: &str, value: &str) -> fdo::Result<()> {
let v = match value {
"true" => true,
"false" => false,
_ => return failed(format!("Invalid boolean value '{}' for realm config variable '{}'", value, var)),
};
let mut has_changed = true;
self.realm.with_mut_config(|c| {
match var {
"use-gpu" if c.gpu() != v => c.use_gpu = Some(v),
_ => has_changed = false,
}
});
if has_changed {
self.mark_changed();
}
Ok(())
}
fn set_overlay(&mut self, value: &str) -> fdo::Result<()> {
let val = match value {
"tmpfs" => Some("tmpfs".to_string()),
"storage" => Some("storage".to_string()),
"none" => None,
_ => return failed(format!("Invalid value '{}' for overlay config", value)),
};
if self.realm.config().overlay != val {
self.realm.with_mut_config(|c| {
c.overlay = Some(value.to_string());
});
self.mark_changed();
}
Ok(())
}
fn set_terminal_scheme(&mut self, value: &str) -> fdo::Result<()> {
if Some(value) == self.realm.config().terminal_scheme() {
return Ok(())
}
let scheme = match Base16Scheme::by_name(value) {
Some(scheme) => scheme,
None => return failed(format!("Invalid terminal color scheme '{}'", value)),
};
let manager = self.realm.manager();
if let Err(err) = scheme.apply_to_realm(&manager, &self.realm) {
return failed(format!("Error applying terminal color scheme: {}", err));
}
self.realm.with_mut_config(|c| {
c.terminal_scheme = Some(value.to_string());
});
self.mark_changed();
Ok(())
}
fn set_realmfs(&mut self, value: &str) -> fdo::Result<()> {
let manager = self.realm.manager();
if manager.realmfs_by_name(value).is_none() {
return failed(format!("Failed to set 'realmfs' config for realm-{}: RealmFS named '{}' does not exist", self.realm.name(), value));
}
if self.realm.config().realmfs() != value {
self.realm.with_mut_config(|c| {
c.realmfs = Some(value.to_string())
});
self.mark_changed();
}
Ok(())
}
pub fn save_config(&self) -> fdo::Result<()> {
if self.is_changed() {
self.realm.config()
.write()
.map_err(|err| fdo::Error::Failed(format!("Error writing config file for realm-{}: {}", self.realm.name(), err)))?;
self.changed.store(false, Ordering::Relaxed);
}
Ok(())
}
pub fn set_var(&mut self, var: &str, value: &str) -> fdo::Result<()> {
if is_bool_config_variable(var) {
self.set_bool_var(var, value)
} else if var == "overlay" {
self.set_overlay(value)
} else if var == "terminal-scheme" {
self.set_terminal_scheme(value)
} else if var == "realmfs" {
self.set_realmfs(value)
} else {
failed(format!("Unknown realm configuration variable '{}'", var))
}
}
}

View File

@ -1,103 +0,0 @@
use std::sync::Arc;
use blocking::unblock;
use event_listener::{Event, EventListener};
use serde::Serialize;
use serde_repr::Serialize_repr;
use zbus::blocking::Connection;
use zbus::{fdo, interface};
use zvariant::Type;
use libcitadel::{PidLookupResult, RealmManager};
use crate::next::config::RealmConfigVars;
use crate::next::realm::RealmItemState;
use crate::next::realmfs::RealmFSState;
pub fn failed<T>(message: String) -> fdo::Result<T> {
Err(fdo::Error::Failed(message))
}
#[derive(Serialize_repr, Type, Debug, PartialEq)]
#[repr(u32)]
pub enum PidLookupResultCode {
Unknown = 1,
Realm = 2,
Citadel = 3,
}
#[derive(Debug, Type, Serialize)]
pub struct RealmFromCitadelPid {
code: PidLookupResultCode,
realm: String,
}
impl From<PidLookupResult> for RealmFromCitadelPid {
fn from(result: PidLookupResult) -> Self {
match result {
PidLookupResult::Unknown => RealmFromCitadelPid { code: PidLookupResultCode::Unknown, realm: String::new() },
PidLookupResult::Realm(realm) => RealmFromCitadelPid { code: PidLookupResultCode::Realm, realm: realm.name().to_string() },
PidLookupResult::Citadel => RealmFromCitadelPid { code: PidLookupResultCode::Citadel, realm: String::new() },
}
}
}
pub struct RealmsManagerServer2 {
realms: RealmItemState,
realmfs_state: RealmFSState,
manager: Arc<RealmManager>,
quit_event: Arc<Event>,
}
impl RealmsManagerServer2 {
fn new(connection: Connection, manager: Arc<RealmManager>, quit_event: Arc<Event>) -> Self {
let realms = RealmItemState::new(connection.clone());
let realmfs_state = RealmFSState::new(connection.clone());
RealmsManagerServer2 {
realms,
realmfs_state,
manager,
quit_event,
}
}
pub fn load(connection: &Connection, manager: Arc<RealmManager>, quit_event: Arc<Event>) -> zbus::Result<Self> {
let mut server = Self::new(connection.clone(), manager.clone(), quit_event);
server.realms.load_realms(&manager)?;
server.realmfs_state.load(&manager)?;
server.realms.populate_realmfs(&server.realmfs_state)?;
Ok(server)
}
}
#[interface(name = "com.subgraph.realms.Manager2")]
impl RealmsManagerServer2 {
async fn get_current(&self) -> u32 {
self.realms.get_current()
.map(|r| r.index())
.unwrap_or(0)
}
async fn realm_from_citadel_pid(&self, pid: u32) -> RealmFromCitadelPid {
let manager = self.manager.clone();
unblock(move || {
manager.realm_by_pid(pid).into()
}).await
}
async fn create_realm(&self, name: &str) -> fdo::Result<()> {
let manager = self.manager.clone();
let name = name.to_string();
unblock(move || {
let _ = manager.new_realm(&name).map_err(|err| fdo::Error::Failed(err.to_string()))?;
Ok(())
}).await
}
async fn get_global_config(&self) -> RealmConfigVars {
RealmConfigVars::new_global()
}
}

View File

@ -1,8 +0,0 @@
mod manager;
mod config;
mod realm;
mod realmfs;
pub use manager::RealmsManagerServer2;
pub const REALMS2_SERVER_OBJECT_PATH: &str = "/com/subgraph/Realms2";

View File

@ -1,374 +0,0 @@
use std::collections::HashMap;
use std::convert::TryInto;
use std::sync::{Arc, Mutex, MutexGuard};
use std::sync::atomic::{AtomicBool, AtomicI64, AtomicU32, Ordering};
use blocking::unblock;
use zbus::{interface, fdo};
use zbus::blocking::Connection;
use zbus::names::{BusName, InterfaceName};
use zvariant::{OwnedObjectPath, Value};
use libcitadel::{Realm, RealmEvent, RealmManager, Result};
use crate::next::config::{RealmConfig, RealmConfigVars};
use crate::next::realmfs::RealmFSState;
use crate::next::REALMS2_SERVER_OBJECT_PATH;
#[derive(Clone)]
pub struct RealmItem {
path: String,
index: u32,
realm: Realm,
config: RealmConfig,
in_run_transition: Arc<AtomicBool>,
realmfs_index: Arc<AtomicU32>,
last_timestamp: Arc<AtomicI64>,
}
#[derive(Copy,Clone)]
#[repr(u32)]
enum RealmRunStatus {
Stopped = 0,
Starting,
Running,
Current,
Stopping,
}
impl RealmRunStatus {
fn for_realm(realm: &Realm, in_transition: bool) -> Self {
if in_transition {
if realm.is_active() { Self::Stopping } else { Self::Starting }
} else if realm.is_active() {
if realm.is_current() { Self::Current } else {Self::Running }
} else {
Self::Stopped
}
}
}
impl RealmItem {
pub(crate) fn new_from_realm(index: u32, realm: Realm) -> RealmItem {
let path = format!("{}/Realm{}", REALMS2_SERVER_OBJECT_PATH, index);
let in_run_transition = Arc::new(AtomicBool::new(false));
let config = RealmConfig::new(realm.clone());
let realmfs_index = Arc::new(AtomicU32::new(0));
let last_timestamp = Arc::new(AtomicI64::new(realm.timestamp()));
RealmItem { path, index, realm, config, in_run_transition, realmfs_index, last_timestamp }
}
pub fn path(&self) -> &str {
&self.path
}
pub fn index(&self) -> u32 {
self.index
}
fn in_run_transition(&self) -> bool {
self.in_run_transition.load(Ordering::Relaxed)
}
fn get_run_status(&self) -> RealmRunStatus {
RealmRunStatus::for_realm(&self.realm, self.in_run_transition())
}
async fn do_start(&mut self) -> fdo::Result<()> {
if !self.realm.is_active() {
let realm = self.realm.clone();
let res = unblock(move || realm.start()).await;
if let Err(err) = res {
return Err(fdo::Error::Failed(format!("Failed to start realm: {}", err)));
}
}
Ok(())
}
async fn do_stop(&mut self) -> fdo::Result<()> {
if self.realm.is_active() {
let realm = self.realm.clone();
let res = unblock(move || realm.stop()).await;
if let Err(err) = res {
return Err(fdo::Error::Failed(format!("Failed to stop realm: {}", err)));
}
}
Ok(())
}
}
#[interface(
name = "com.subgraph.realms.Realm"
)]
impl RealmItem {
async fn start(
&mut self,
) -> fdo::Result<()> {
self.do_start().await?;
Ok(())
}
async fn stop(
&mut self,
) -> fdo::Result<()> {
self.do_stop().await?;
Ok(())
}
async fn restart(
&mut self,
) -> fdo::Result<()> {
self.do_stop().await?;
self.do_start().await?;
Ok(())
}
async fn set_current(&mut self) -> fdo::Result<()> {
let realm = self.realm.clone();
let res = unblock(move || realm.set_current()).await;
if let Err(err) = res {
return Err(fdo::Error::Failed(format!("Failed to set realm {} as current: {}", self.realm.name(), err)));
}
Ok(())
}
async fn get_config(&self) -> RealmConfigVars {
self.config.config_vars()
}
async fn set_config(&mut self, vars: Vec<(String, String)>) -> fdo::Result<()> {
for (var, val) in &vars {
self.config.set_var(var, val)?;
}
let config = self.config.clone();
unblock(move || config.save_config()).await
}
#[zbus(property, name = "RunStatus")]
fn run_status(&self) -> u32 {
self.get_run_status() as u32
}
#[zbus(property, name="IsSystemRealm")]
fn is_system_realm(&self) -> bool {
self.realm.is_system()
}
#[zbus(property, name = "Name")]
fn name(&self) -> &str {
self.realm.name()
}
#[zbus(property, name = "Description")]
fn description(&self) -> String {
self.realm.notes()
.unwrap_or(String::new())
}
#[zbus(property, name = "PidNS")]
fn pid_ns(&self) -> u64 {
self.realm.pid_ns().unwrap_or_default()
}
#[zbus(property, name = "RealmFS")]
fn realmfs(&self) -> u32 {
self.realmfs_index.load(Ordering::Relaxed)
}
#[zbus(property, name = "Timestamp")]
fn timestamp(&self) -> u64 {
self.realm.timestamp() as u64
}
}
#[derive(Clone)]
pub struct RealmItemState(Arc<Mutex<Inner>>);
struct Inner {
connection: Connection,
next_index: u32,
realms: HashMap<String, RealmItem>,
current_realm: Option<RealmItem>,
}
impl Inner {
fn new(connection: Connection) -> Self {
Inner {
connection,
next_index: 1,
realms:HashMap::new(),
current_realm: None,
}
}
fn load_realms(&mut self, manager: &RealmManager) -> zbus::Result<()> {
for realm in manager.realm_list() {
self.add_realm(realm)?;
}
Ok(())
}
pub fn populate_realmfs(&mut self, realmfs_state: &RealmFSState) -> zbus::Result<()> {
for item in self.realms.values_mut() {
if let Some(realmfs) = realmfs_state.realmfs_by_name(item.realm.config().realmfs()) {
item.realmfs_index.store(realmfs.index(), Ordering::Relaxed);
}
}
Ok(())
}
fn add_realm(&mut self, realm: Realm) -> zbus::Result<()> {
if self.realms.contains_key(realm.name()) {
warn!("Attempted to add duplicate realm '{}'", realm.name());
return Ok(())
}
let key = realm.name().to_string();
let item = RealmItem::new_from_realm(self.next_index, realm);
self.connection.object_server().at(item.path(), item.clone())?;
self.realms.insert(key, item);
self.next_index += 1;
Ok(())
}
fn remove_realm(&mut self, realm: &Realm) -> zbus::Result<()> {
if let Some(item) = self.realms.remove(realm.name()) {
self.connection.object_server().remove::<RealmItem, &str>(item.path())?;
} else {
warn!("Failed to find realm to remove with name '{}'", realm.name());
}
Ok(())
}
fn emit_property_changed(&self, object_path: OwnedObjectPath, propname: &str, value: Value<'_>) -> zbus::Result<()> {
let iface_name = InterfaceName::from_str_unchecked("com.subgraph.realms.Realm");
let changed = HashMap::from([(propname.to_string(), value)]);
let inval: &[&str] = &[];
self.connection.emit_signal(
None::<BusName<'_>>,
&object_path,
"org.freedesktop.Dbus.Properties",
"PropertiesChanged",
&(iface_name, changed, inval))?;
Ok(())
}
fn realm_status_changed(&self, realm: &Realm, transition: Option<bool>) -> zbus::Result<()> {
if let Some(realm) = self.realm_by_name(realm.name()) {
if let Some(transition) = transition {
realm.in_run_transition.store(transition, Ordering::Relaxed);
}
let object_path = realm.path().try_into().unwrap();
self.emit_property_changed(object_path, "RunStatus", Value::U32(realm.get_run_status() as u32))?;
let timestamp = realm.realm.timestamp();
if realm.last_timestamp.load(Ordering::Relaxed) != realm.realm.timestamp() {
realm.last_timestamp.store(timestamp, Ordering::Relaxed);
let object_path = realm.path().try_into().unwrap();
self.emit_property_changed(object_path, "Timestamp", Value::U64(timestamp as u64))?;
}
}
Ok(())
}
fn realm_by_name(&self, name: &str) -> Option<&RealmItem> {
let res = self.realms.get(name);
if res.is_none() {
warn!("Failed to find realm with name '{}'", name);
}
res
}
fn on_starting(&self, realm: &Realm) -> zbus::Result<()>{
self.realm_status_changed(realm, Some(true))?;
Ok(())
}
fn on_started(&self, realm: &Realm) -> zbus::Result<()>{
self.realm_status_changed(realm, Some(false))
}
fn on_stopping(&self, realm: &Realm) -> zbus::Result<()> {
self.realm_status_changed(realm, Some(true))
}
fn on_stopped(&self, realm: &Realm) -> zbus::Result<()> {
self.realm_status_changed(realm, Some(false))
}
fn on_new(&mut self, realm: &Realm) -> zbus::Result<()> {
self.add_realm(realm.clone())?;
Ok(())
}
fn on_removed(&mut self, realm: &Realm) -> zbus::Result<()> {
self.remove_realm(&realm)?;
Ok(())
}
fn on_current(&mut self, realm: Option<&Realm>) -> zbus::Result<()> {
if let Some(r) = self.current_realm.take() {
self.realm_status_changed(&r.realm, None)?;
}
if let Some(realm) = realm {
self.realm_status_changed(realm, None)?;
if let Some(item) = self.realm_by_name(realm.name()) {
self.current_realm = Some(item.clone());
}
}
Ok(())
}
}
impl RealmItemState {
pub fn new(connection: Connection) -> Self {
RealmItemState(Arc::new(Mutex::new(Inner::new(connection))))
}
pub fn load_realms(&self, manager: &RealmManager) -> zbus::Result<()> {
self.inner().load_realms(manager)?;
self.add_event_handler(manager)
.map_err(|err| zbus::Error::Failure(err.to_string()))?;
Ok(())
}
pub fn populate_realmfs(&self, realmfs_state: &RealmFSState) -> zbus::Result<()> {
self.inner().populate_realmfs(realmfs_state)
}
pub fn get_current(&self) -> Option<RealmItem> {
self.inner().current_realm.clone()
}
fn inner(&self) -> MutexGuard<Inner> {
self.0.lock().unwrap()
}
fn add_event_handler(&self, manager: &RealmManager) -> Result<()> {
let state = self.clone();
manager.add_event_handler(move |ev| {
if let Err(err) = state.handle_event(ev) {
warn!("Error handling {}: {}", ev, err);
}
});
manager.start_event_task()?;
Ok(())
}
fn handle_event(&self, ev: &RealmEvent) -> zbus::Result<()> {
match ev {
RealmEvent::Started(realm) => self.inner().on_started(realm)?,
RealmEvent::Stopped(realm) => self.inner().on_stopped(realm)?,
RealmEvent::New(realm) => self.inner().on_new(realm)?,
RealmEvent::Removed(realm) => self.inner().on_removed(realm)?,
RealmEvent::Current(realm) => self.inner().on_current(realm.as_ref())?,
RealmEvent::Starting(realm) => self.inner().on_starting(realm)?,
RealmEvent::Stopping(realm) => self.inner().on_stopping(realm)?,
};
Ok(())
}
}

View File

@ -1,120 +0,0 @@
use std::collections::HashMap;
use std::convert::TryInto;
use zbus::blocking::Connection;
use zbus::{fdo, interface};
use zvariant::{ObjectPath, OwnedObjectPath};
use libcitadel::{RealmFS, RealmManager};
use crate::next::REALMS2_SERVER_OBJECT_PATH;
const BLOCK_SIZE: u64 = 4096;
#[derive(Clone)]
pub struct RealmFSItem {
object_path: OwnedObjectPath,
index: u32,
realmfs: RealmFS,
}
impl RealmFSItem {
pub(crate) fn new_from_realmfs(index: u32, realmfs: RealmFS) -> RealmFSItem {
let object_path = format!("{}/RealmFS{}", REALMS2_SERVER_OBJECT_PATH, index).try_into().unwrap();
RealmFSItem {
object_path,
index,
realmfs,
}
}
pub fn index(&self) -> u32 {
self.index
}
pub fn object_path(&self) -> ObjectPath {
self.object_path.as_ref()
}
}
#[interface(
name = "com.subgraph.realms.RealmFS"
)]
impl RealmFSItem {
#[zbus(property, name = "Name")]
fn name(&self) -> &str {
self.realmfs.name()
}
#[zbus(property, name = "Activated")]
fn activated(&self) -> bool {
self.realmfs.is_activated()
}
#[zbus(property, name = "InUse")]
fn in_use(&self) -> bool {
self.realmfs.is_activated()
}
#[zbus(property, name = "Mountpoint")]
fn mountpoint(&self) -> String {
self.realmfs.mountpoint().to_string()
}
#[zbus(property, name = "Path")]
fn path(&self) -> String {
format!("{}", self.realmfs.path().display())
}
#[zbus(property, name = "FreeSpace")]
fn free_space(&self) -> fdo::Result<u64> {
let blocks = self.realmfs.free_size_blocks()
.map_err(|err| fdo::Error::Failed(err.to_string()))?;
Ok(blocks as u64 * BLOCK_SIZE)
}
#[zbus(property, name = "AllocatedSpace")]
fn allocated_space(&self) -> fdo::Result<u64> {
let blocks = self.realmfs.allocated_size_blocks()
.map_err(|err| fdo::Error::Failed(err.to_string()))?;
Ok(blocks as u64 * BLOCK_SIZE)
}
}
pub struct RealmFSState {
connection: Connection,
next_index: u32,
items: HashMap<String, RealmFSItem>,
}
impl RealmFSState {
pub fn new(connection: Connection) -> Self {
RealmFSState {
connection,
next_index: 1,
items: HashMap::new(),
}
}
pub fn load(&mut self, manager: &RealmManager) -> zbus::Result<()> {
for realmfs in manager.realmfs_list() {
self.add_realmfs(realmfs)?;
}
Ok(())
}
fn add_realmfs(&mut self, realmfs: RealmFS) -> zbus::Result<()> {
if !self.items.contains_key(realmfs.name()) {
let name = realmfs.name().to_string();
let item = RealmFSItem::new_from_realmfs(self.next_index, realmfs);
self.connection.object_server().at(item.object_path(), item.clone())?;
self.items.insert(name, item);
self.next_index += 1;
} else {
warn!("Attempted to add duplicate realmfs '{}'", realmfs.name());
}
Ok(())
}
pub fn realmfs_by_name(&self, name: &str) -> Option<&RealmFSItem> {
let res = self.items.get(name);
if res.is_none() {
warn!("Failed to find RealmFS with name '{}'", name);
}
res
}
}

View File

@ -1,14 +1,11 @@
use libcitadel::{RealmManager, Realm, OverlayType, Result, PidLookupResult};
use std::sync::Arc;
use zbus::blocking::Connection;
use zvariant::Type;
use zbus::{dbus_interface, ObjectServer,Connection};
use zvariant::derive::Type;
use std::thread;
use std::collections::HashMap;
use blocking::unblock;
use event_listener::{Event, EventListener};
use serde::{Serialize, Deserialize};
use serde::{Serialize,Deserialize};
use serde_repr::Serialize_repr;
use zbus::{interface, SignalContext};
use crate::events::EventHandler;
use libcitadel::terminal::Base16Scheme;
@ -42,7 +39,6 @@ impl From<PidLookupResult> for RealmFromCitadelPid {
#[derive(Clone)]
pub struct RealmsManagerServer {
manager: Arc<RealmManager>,
quit_event: Arc<Event>,
}
const BOOL_CONFIG_VARS: &[&str] = &[
@ -125,40 +121,40 @@ fn configure_realm(manager: &RealmManager, realm: &Realm, variable: &str, value:
impl RealmsManagerServer {
pub fn load(connection: &Connection, manager: Arc<RealmManager>, quit_event: Arc<Event>) -> Result<RealmsManagerServer> {
let server = RealmsManagerServer { manager, quit_event };
let events = EventHandler::new(connection.clone());
server.manager.add_event_handler(move |ev| events.handle_event(ev));
server.manager.start_event_task()?;
Ok(server)
fn register_events(&self, connection: &Connection) -> Result<()> {
let events = EventHandler::new(connection.clone(), self.clone());
self.manager.add_event_handler(move |ev| events.handle_event(ev));
self.manager.start_event_task()
}
pub fn register(connection: &Connection) -> Result<ObjectServer> {
let manager = RealmManager::load()?;
let iface = RealmsManagerServer { manager };
iface.register_events(connection)?;
let mut object_server = ObjectServer::new(connection);
object_server.at(REALMS_SERVER_OBJECT_PATH, iface).map_err(context!("ZBus error"))?;
Ok(object_server)
}
}
#[interface(name = "com.subgraph.realms.Manager")]
#[dbus_interface(name = "com.subgraph.realms.Manager")]
impl RealmsManagerServer {
async fn set_current(&self, name: &str) {
let manager = self.manager.clone();
let name = name.to_string();
unblock(move || {
if let Some(realm) = manager.realm_by_name(&name) {
if let Err(err) = manager.set_current_realm(&realm) {
warn!("set_current_realm({}) failed: {}", name, err);
}
fn set_current(&self, name: &str) {
if let Some(realm) = self.manager.realm_by_name(name) {
if let Err(err) = self.manager.set_current_realm(&realm) {
warn!("set_current_realm({}) failed: {}", name, err);
}
}).await
}
}
async fn get_current(&self) -> String {
let manager = self.manager.clone();
unblock(move || {
match manager.current_realm() {
Some(realm) => realm.name().to_string(),
None => String::new(),
}
}).await
fn get_current(&self) -> String {
match self.manager.current_realm() {
Some(realm) => realm.name().to_string(),
None => String::new(),
}
}
fn list(&self) -> Vec<RealmItem> {
@ -253,12 +249,8 @@ impl RealmsManagerServer {
});
}
async fn realm_from_citadel_pid(&self, pid: u32) -> RealmFromCitadelPid {
let manager = self.manager.clone();
unblock(move || {
manager.realm_by_pid(pid).into()
}).await
fn realm_from_citadel_pid(&self, pid: u32) -> RealmFromCitadelPid {
self.manager.realm_by_pid(pid).into()
}
fn realm_config(&self, name: &str) -> RealmConfig {
@ -269,7 +261,7 @@ impl RealmsManagerServer {
RealmConfig::new_from_realm(&realm)
}
async fn realm_set_config(&self, name: &str, vars: Vec<(String,String)>) {
fn realm_set_config(&self, name: &str, vars: Vec<(String,String)>) {
let realm = match self.manager.realm_by_name(name) {
Some(r) => r,
None => {
@ -278,12 +270,8 @@ impl RealmsManagerServer {
},
};
for var in vars {
let manager = self.manager.clone();
let realm = realm.clone();
unblock( move || {
configure_realm(&manager, &realm, &var.0, &var.1);
}).await;
for var in &vars {
configure_realm(&self.manager, &realm, &var.0, &var.1);
}
}
@ -291,18 +279,13 @@ impl RealmsManagerServer {
Realm::is_valid_name(name) && self.manager.realm_by_name(name).is_some()
}
async fn create_realm(&self, name: &str) -> bool {
let manager = self.manager.clone();
let name = name.to_string();
unblock(move || {
if let Err(err) = manager.new_realm(&name) {
warn!("Error creating realm ({}): {}", name, err);
false
} else {
true
}
}).await
fn create_realm(&self, name: &str) -> bool {
if let Err(err) = self.manager.new_realm(name) {
warn!("Error creating realm ({}): {}", name, err);
false
} else {
true
}
}
fn list_realm_f_s(&self) -> Vec<String> {
@ -316,23 +299,23 @@ impl RealmsManagerServer {
}
#[zbus(signal)]
pub async fn realm_started(ctx: &SignalContext<'_>, realm: &str, pid_ns: u64, status: u8) -> zbus::Result<()>;
#[dbus_interface(signal)]
pub fn realm_started(&self, realm: &str, pid_ns: u64, status: u8) -> zbus::Result<()> { Ok(()) }
#[zbus(signal)]
pub async fn realm_stopped(ctx: &SignalContext<'_>, realm: &str, status: u8) -> zbus::Result<()>;
#[dbus_interface(signal)]
pub fn realm_stopped(&self, realm: &str, status: u8) -> zbus::Result<()> { Ok(()) }
#[zbus(signal)]
pub async fn realm_new(ctx: &SignalContext<'_>, realm: &str, description: &str, status: u8) -> zbus::Result<()>;
#[dbus_interface(signal)]
pub fn realm_new(&self, realm: &str, description: &str, status: u8) -> zbus::Result<()> { Ok(()) }
#[zbus(signal)]
pub async fn realm_removed(ctx: &SignalContext<'_>, realm: &str) -> zbus::Result<()>;
#[dbus_interface(signal)]
pub fn realm_removed(&self, realm: &str) -> zbus::Result<()> { Ok(()) }
#[zbus(signal)]
pub async fn realm_current(ctx: &SignalContext<'_>, realm: &str, status: u8) -> zbus::Result<()>;
#[dbus_interface(signal)]
pub fn realm_current(&self, realm: &str, status: u8) -> zbus::Result<()> { Ok(()) }
#[zbus(signal)]
pub async fn service_started(ctx: &SignalContext<'_>) -> zbus::Result<()>;
#[dbus_interface(signal)]
pub fn service_started(&self) -> zbus::Result<()> { Ok(()) }
}

View File

@ -0,0 +1,529 @@
use anyhow::{Context, Result};
use clap::{Parser, Subcommand};
use ed25519_dalek::pkcs8::DecodePublicKey;
use ed25519_dalek::pkcs8::EncodePublicKey;
use ed25519_dalek::Signature;
use ed25519_dalek::Signer;
use ed25519_dalek::SigningKey;
use ed25519_dalek::VerifyingKey;
use ed25519_dalek::KEYPAIR_LENGTH;
use glob::glob;
use libcitadel::updates::Component;
use libcitadel::{updates, ImageHeader};
use rand::rngs::OsRng;
use sodiumoxide::crypto::pwhash;
use sodiumoxide::crypto::secretbox;
use sodiumoxide::crypto::stream;
use std::env;
use std::io::{Read, Write};
use std::path::Path;
use std::path::PathBuf;
use std::process::Command;
use std::str::FromStr;
use zeroize::Zeroize;
const SALSA_NONCE: &[u8] = &[
116, 138, 142, 103, 234, 105, 192, 48, 117, 29, 150, 214, 106, 116, 195, 64, 120, 251, 94, 20,
212, 118, 125, 189,
];
const PASSWORD_SALT: &[u8] = &[
18, 191, 168, 237, 156, 199, 54, 43, 122, 165, 35, 9, 89, 106, 36, 209, 145, 161, 90, 2, 121,
51, 242, 182, 14, 245, 47, 253, 237, 153, 251, 219,
];
const LAST_RESORT_CLIENT: &str = "public";
const LAST_RESORT_CHANNEL: &str = "prod";
#[derive(Parser)]
#[command(about = "Perform tasks needed to create an update and publish it")]
#[command(author, about, long_about = None)]
struct Cli {
#[command(subcommand)]
command: Option<Commands>,
}
#[derive(Subcommand, Debug)]
#[command(arg_required_else_help = true)]
enum Commands {
/// Generate a keypair to be used to sign version file. You will be asked to provide a mandatory password.
GenerateKeypair {
/// keypair filepath to save to
#[arg(short, long)]
keypair_filepath: Option<String>,
},
/// Generate the complete cbor file. If no components are passed, generate by reading image of each component
CreateSignedFile {
/// rootfs image semver version
#[arg(short, long)]
rootfs_image_version: Option<String>,
/// kernel image semver version
#[arg(short, long)]
kernel_image_version: Option<String>,
#[arg(short, long)]
extra_image_version: Option<String>,
/// keypair filepath
#[arg(short, long, value_name = "FILE")]
path_keypair: Option<String>,
/// command output complete filepath
#[arg(short, long, value_name = "FILE")]
versionfile_filepath: Option<String>,
},
/// Verify that the version file is correctly signed
VerifySignature {
/// public key filepath
#[arg(short, long, value_name = "FILE")]
publickey_filepath: Option<String>,
/// command output complete filepath
#[arg(short, long, value_name = "FILE")]
versionfile_filepath: Option<String>,
},
UploadToServer {
#[arg(long)]
component: Option<updates::Component>,
path: Option<PathBuf>,
},
}
fn main() -> Result<()> {
let cli = Cli::parse();
match &cli.command {
Some(Commands::GenerateKeypair { keypair_filepath }) => {
generate_keypair(keypair_filepath).context("Failed to generate keypair")?
}
Some(Commands::CreateSignedFile {
path_keypair,
rootfs_image_version,
kernel_image_version,
extra_image_version,
versionfile_filepath,
}) => create_signed_version_file(
path_keypair,
rootfs_image_version,
kernel_image_version,
extra_image_version,
versionfile_filepath,
)
.context("Failed to create signed file")?,
Some(Commands::VerifySignature {
publickey_filepath,
versionfile_filepath,
}) => verify_version_signature(publickey_filepath, versionfile_filepath)
.context("Failed to verify signature")?,
Some(Commands::UploadToServer { component, path }) => {
upload_components_to_server(component, path)
.context("Failed to upload to the server")?
}
None => {}
}
Ok(())
}
fn generate_keypair(keypair_filepath: &Option<String>) -> Result<()> {
// if the user did not pass a path, we save key files to current directory
let keypair_filepath = &keypair_filepath.clone().unwrap_or_else(|| ".".to_string());
let path = std::path::Path::new(keypair_filepath);
let mut password;
loop {
// get passphrase used to encrypt key from user
password = rpassword::prompt_password(
"Please enter the passphrase we will use to encrypt the private key with: ",
)
.unwrap();
let password_confirm = rpassword::prompt_password("Retype same passphrase: ").unwrap();
if password != password_confirm {
println!("\nPassphrases did not match. Please try again")
} else {
break;
}
}
// generate keypair
let mut csprng = OsRng;
let signing_key: SigningKey = SigningKey::generate(&mut csprng);
let keypair_fp: PathBuf;
let publickey_fp: PathBuf;
if path.is_dir() {
keypair_fp = path.join("keypair.priv");
publickey_fp = path.join("update_server_key.pub");
} else {
keypair_fp = path.to_path_buf();
publickey_fp = path.parent().unwrap().join("update_server_key.pub");
}
let mut keyfile = std::fs::File::create(&keypair_fp)?;
let mut public_key = std::fs::File::create(&publickey_fp)?;
// encrypt private key
let mut k = secretbox::Key([0; secretbox::KEYBYTES]);
let secretbox::Key(ref mut kb) = k;
let password_hash = pwhash::derive_key(
kb,
password.as_bytes(),
&sodiumoxide::crypto::pwhash::scryptsalsa208sha256::Salt::from_slice(PASSWORD_SALT)
.unwrap(),
pwhash::OPSLIMIT_INTERACTIVE,
pwhash::MEMLIMIT_INTERACTIVE,
)
.unwrap();
let plaintext = signing_key.to_keypair_bytes();
let key = sodiumoxide::crypto::stream::xsalsa20::Key::from_slice(password_hash)
.expect("failed to unwrap key");
let nonce = sodiumoxide::crypto::stream::xsalsa20::Nonce::from_slice(SALSA_NONCE)
.expect("failed to unwrap nonce");
// encrypt the plaintext
let ciphertext = stream::stream_xor(&plaintext, &nonce, &key);
keyfile.write_all(&ciphertext)?;
public_key.write_all(
&signing_key
.verifying_key()
.to_public_key_pem(ed25519_dalek::pkcs8::spki::der::pem::LineEnding::LF)
.unwrap()
.as_bytes(),
)?;
password.zeroize();
println!(
"Generated the keypair and wrote to {}. The public key is here: {}",
keypair_fp.display(),
publickey_fp.display()
);
println!(
"You may now move the public key from {} to the citadel build path at citadel/meta-citadel/recipes-citadel/citadel-config/files/citadel-fetch/update_server_key.pub",
publickey_fp.display()
);
Ok(())
}
fn create_signed_version_file(
signing_key_path: &Option<String>,
citadel_rootfs_version: &Option<String>,
citadel_kernel_version: &Option<String>,
citadel_extra_version: &Option<String>,
versionfile_filepath: &Option<String>,
) -> Result<()> {
let rootfs_version = match citadel_rootfs_version {
Some(v) => semver::Version::parse(v)
.expect("Error: Failed to parse rootfs semver version")
.to_string(),
None => get_imageheader_version(&Component::Rootfs).unwrap_or(String::from("0.0.0")),
};
let kernel_version = match citadel_kernel_version {
Some(v) => semver::Version::parse(v)
.expect("Error: Failed to parse kernel semver version")
.to_string(),
None => get_imageheader_version(&Component::Kernel).unwrap_or(String::from("0.0.0")),
};
let extra_version = match citadel_extra_version {
Some(v) => semver::Version::parse(v)
.expect("Error: Failed to parse extra semver version")
.to_string(),
None => get_imageheader_version(&Component::Extra).unwrap_or(String::from("0.0.0")),
};
let rootfs_path = get_component_path(&Component::Rootfs);
let channel;
if rootfs_path.is_ok() {
channel = match ImageHeader::from_file(rootfs_path?) {
Ok(image_header) => image_header.metainfo().channel().to_string(),
Err(_) => env::var("UPDATES_CHANNEL").unwrap_or(LAST_RESORT_CHANNEL.to_string()),
};
} else {
channel = env::var("UPDATES_CHANNEL").unwrap_or(LAST_RESORT_CHANNEL.to_string());
}
let component_version_vector = vec![
updates::AvailableComponentVersion {
component: updates::Component::Rootfs,
version: rootfs_version.clone(),
file_path: format!(
"{}/{}/{}_{}.img",
env::var("UPDATES_CLIENT").unwrap_or(LAST_RESORT_CLIENT.to_string()),
channel,
"rootfs",
rootfs_version
)
.to_string(),
},
updates::AvailableComponentVersion {
component: updates::Component::Kernel,
version: kernel_version.clone(),
file_path: format!(
"{}/{}/{}_{}.img",
env::var("UPDATES_CLIENT").unwrap_or(LAST_RESORT_CLIENT.to_string()),
channel,
"kernel",
kernel_version
)
.to_string(),
},
updates::AvailableComponentVersion {
component: updates::Component::Extra,
version: extra_version.clone(),
file_path: format!(
"{}/{}/{}_{}.img",
env::var("UPDATES_CLIENT").unwrap_or(LAST_RESORT_CLIENT.to_string()),
channel,
"extra",
extra_version
)
.to_string(),
},
];
// generate version file
let citadel_version = updates::CitadelVersionStruct {
client: env::var("UPDATES_CLIENT").unwrap_or(LAST_RESORT_CLIENT.to_string()),
channel: channel.to_string(),
component_version: component_version_vector,
publisher: "Subgraph".to_string(),
};
let fp = match signing_key_path {
Some(fp) => Path::new(fp),
None => Path::new("keypair.priv"),
};
// serialized to cbor
let serialized_citadel_version = serde_cbor::to_vec(&citadel_version)?;
// get signing_key_bytes from the file passed
let mut keyfile = std::fs::File::open(&fp)?;
let mut buf = [0; KEYPAIR_LENGTH];
keyfile.read_exact(&mut buf)?;
// prompt user for keypair decryption password
let mut password =
rpassword::prompt_password("Please enter the passphrase used to decrypt the private key: ")
.unwrap();
// decrypt private key
let mut k = secretbox::Key([0; secretbox::KEYBYTES]);
let secretbox::Key(ref mut kb) = k;
let password_hash = pwhash::derive_key(
kb,
password.as_bytes(),
&sodiumoxide::crypto::pwhash::scryptsalsa208sha256::Salt::from_slice(PASSWORD_SALT)
.unwrap(),
pwhash::OPSLIMIT_INTERACTIVE,
pwhash::MEMLIMIT_INTERACTIVE,
)
.unwrap();
let key = sodiumoxide::crypto::stream::xsalsa20::Key::from_slice(password_hash)
.expect("failed to unwrap key");
let nonce = sodiumoxide::crypto::stream::xsalsa20::Nonce::from_slice(SALSA_NONCE)
.expect("failed to unwrap nonce");
// decrypt the ciphertext
let plaintext = stream::stream_xor(&buf, &nonce, &key);
let signing_key = SigningKey::from_keypair_bytes(plaintext[0..64].try_into()?)?;
// sign serialized_citadel_version for inclusion in version_file
let signature: Signature = signing_key.sign(&serialized_citadel_version);
// generate signature of citadel_version cbor format (signed)
let version_file = updates::CryptoContainerFile {
serialized_citadel_version,
signature: signature.to_string(),
signatory: "Subgraph".to_string(),
};
let vf_fp = match versionfile_filepath {
Some(vf_fp) => {
if Path::new(vf_fp).is_dir() {
Path::new(vf_fp).join("version.cbor")
} else {
Path::new(vf_fp).to_path_buf()
}
}
None => Path::new("version.cbor").to_path_buf(),
};
let outfile = std::fs::File::create(&vf_fp)?;
serde_cbor::to_writer(outfile, &version_file)?;
password.zeroize();
Ok(())
}
/// Validate that the completed version file correctly verifies given the self-embedded signature and public key
fn verify_version_signature(
pubkey_filepath: &Option<String>,
versionfile_filepath: &Option<String>,
) -> Result<()> {
let pub_fp = match pubkey_filepath {
Some(pub_fp) => Path::new(pub_fp),
None => Path::new("update_server_key.pub"),
};
let version_fp = match versionfile_filepath {
Some(version_fp) => Path::new(version_fp),
None => Path::new("version.cbor"),
};
let mut pubkey_file = std::fs::File::open(&pub_fp)?;
let mut buf = String::new();
pubkey_file.read_to_string(&mut buf)?;
let verifying_key = VerifyingKey::from_public_key_pem(&buf)?;
let version_file = &std::fs::File::open(version_fp)?;
let crypto_container_struct: updates::CryptoContainerFile =
serde_cbor::from_reader(version_file)?;
let citadel_version_struct: updates::CitadelVersionStruct =
serde_cbor::from_slice(&crypto_container_struct.serialized_citadel_version)?;
let signature = ed25519_dalek::Signature::from_str(&crypto_container_struct.signature)?;
match verifying_key.verify_strict(
&crypto_container_struct.serialized_citadel_version,
&signature,
) {
Ok(_) => println!("Everythin ok. Signature verified correctly"),
Err(e) => panic!(
"Error: Signature was not able to be verified! Threw error: {}",
e
),
}
println!(
"The destructured version file contains the following information:\n{}",
citadel_version_struct
);
Ok(())
}
// Make sure to add your ssh key to the "updates" user on the server
fn send_with_scp(from: &PathBuf, to: &PathBuf) -> Result<()> {
Command::new("scp")
.arg(from)
.arg(format!(
"updates@{}:/updates/files/{}",
updates::UPDATE_SERVER_HOSTNAME,
to.to_string_lossy()
))
.spawn()
.context("scp command failed to run")?;
Ok(())
}
fn get_imageheader_version(component: &Component) -> Result<String> {
let version = match ImageHeader::from_file(get_component_path(component)?) {
Ok(image_header) => image_header.metainfo().version().to_string(),
Err(_) => String::from("0.0.0"),
};
Ok(version)
}
fn get_component_path(component: &updates::Component) -> Result<PathBuf> {
let mut gl = match component {
Component::Rootfs => glob("../build/images/citadel-rootfs*.img")?,
Component::Kernel => glob("../build/images/citadel-kernel*.img")?,
Component::Extra => glob("../build/images/citadel-extra*.img")?,
};
let first = gl.nth(0).context(format!(
"Failed to find citadel-{}*.img in ../build/images/",
component
))?;
Ok(PathBuf::from(first?))
}
fn upload_all_components() -> Result<()> {
for component in updates::Component::iterator() {
upload_component(component, &None)?
}
upload_cbor()?;
Ok(())
}
fn upload_component(component: &updates::Component, path: &Option<PathBuf>) -> Result<()> {
let final_path;
// uggliest if statement i've ever written
// if path was passed to this function
if let Some(p) = path {
final_path = p.to_path_buf();
} else {
// if the path wasn't passed to this function, search for the component path
if let Ok(p) = get_component_path(&component) {
final_path = p;
} else {
// if path was not passed and we failed to locate the component's path
println!(
"We failed to find the {} image we were looking for... Skipping...",
component
);
return Ok(());
}
}
let image_header = libcitadel::ImageHeader::from_file(&final_path)?;
let to = PathBuf::from(format!(
"{}/{}/{}_{}.img",
env::var("UPDATES_CLIENT").unwrap_or(LAST_RESORT_CLIENT.to_string()),
env::var("UPDATES_CHANNEL").unwrap_or(LAST_RESORT_CHANNEL.to_string()),
component,
image_header.metainfo().version()
));
send_with_scp(&final_path, &to)
}
// If no parameters are passed to this command, upload all components regardless of version on the server
// If a path is passed, upload that image to the server regardless version.
fn upload_components_to_server(
component: &Option<updates::Component>,
path: &Option<PathBuf>,
) -> Result<()> {
// check if a component is passed to this function:
match component {
Some(comp) => upload_component(comp, path)?, // This function handles the option of not passing a path
None => upload_all_components()?,
}
Ok(())
}
fn upload_cbor() -> Result<()> {
send_with_scp(
&PathBuf::from("version.cbor"),
&PathBuf::from(format!(
"{}/{}/version.cbor",
env::var("UPDATES_CLIENT").unwrap_or(LAST_RESORT_CLIENT.to_string()),
env::var("UPDATES_CHANNEL").unwrap_or(LAST_RESORT_CHANNEL.to_string()),
)),
)
}