Compare commits

..

2 Commits

Author SHA1 Message Date
isa
0ff03cb273 Add feature to use and generate remote updates 2024-09-12 15:11:18 -04:00
isa
ae41a71b60 Convert citadel image version numbers to semver 2024-09-12 12:45:33 -04:00
37 changed files with 2612 additions and 4321 deletions

5
.cargo/config.toml Normal file
View File

@ -0,0 +1,5 @@
[env]
# overide this with your name to send to a different path on the server
UPDATES_CLIENT = "public"
UPDATES_CHANNEL = "prod"

1836
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -1,5 +1,5 @@
[workspace]
members = ["citadel-realms", "citadel-installer-ui", "citadel-tool", "realmsd", "realm-config-ui", "launch-gnome-software" ]
members = ["citadel-realms", "citadel-installer-ui", "citadel-tool", "realmsd", "realm-config-ui", "launch-gnome-software", "update-generator" ]
[profile.release]
lto = true
codegen-units = 1

File diff suppressed because it is too large Load Diff

View File

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

View File

@ -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,328 @@
use crate::{update, Path};
use anyhow::{Context, Result};
use clap::ArgMatches;
use ed25519_dalek::{pkcs8::DecodePublicKey, VerifyingKey};
use libcitadel::updates::UPDATE_SERVER_HOSTNAME;
use libcitadel::ResourceImage;
use libcitadel::{updates, updates::CitadelVersionStruct};
use std::io::prelude::*;
use std::str::FromStr;
const OS_RELEASE_PATH: &str = "/etc/os-release";
const IMAGE_DIRECTORY_PATH: &str = "/run/citadel/images/";
const EXTRA_IMAGE_PATH: &str = "/run/citadel/images/citadel-extra.img";
const ROOTFS_IMAGE_PATH: &str = "/run/citadel/images/citadel-rootfs.img";
const UPDATE_SERVER_KEY_PATH: &str = "/etc/citadel/update_server_key.pub";
const LAST_RESORT_CLIENT: &str = "public";
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: \n");
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)
}
fn get_image_version<P: AsRef<Path>>(path: &P) -> Result<String> {
let resource_image = ResourceImage::from_path(path)?;
Ok(resource_image.metainfo().version().to_string())
}
fn get_current_os_config() -> Result<updates::CitadelVersionStruct> {
let os_release = rs_release::parse_os_release(OS_RELEASE_PATH).context(format!(
"Failed to get OS_RELEASE_PATH from {}",
OS_RELEASE_PATH
))?;
// We need to get the version of the rootfs, kernel and extra images
// We first check if there is an env var called UPDATES_CLIENT
let client = match std::env::var("UPDATES_CLIENT") {
Ok(a) => a,
Err(_) => os_release
.get("CLIENT")
.unwrap_or(&LAST_RESORT_CLIENT.to_string())
.to_string(),
};
let channel = os_release.get("CITADEL_CHANNEL");
// Search image dir for kernel image and extract version
let mut kernel_version = String::from("0.0.0");
for path in glob::glob(&format!("{}citadel-kernel*.img", IMAGE_DIRECTORY_PATH))
.expect("Failed to read glob pattern")
{
match path {
Ok(path) => {
kernel_version = get_image_version(&path).context(format!(
"Failed to get image version at path: {}",
path.display()
))?
}
Err(e) => anyhow::bail!("Failed to find kernel image. Error: {}", e),
}
}
let rootfs_version = get_image_version(&ROOTFS_IMAGE_PATH).context(format!(
"Failed to get rootfs img version at path: {}",
ROOTFS_IMAGE_PATH
))?;
let extra_version = get_image_version(&EXTRA_IMAGE_PATH).context(format!(
"Failed to get extra img version at path: {}",
EXTRA_IMAGE_PATH
))?;
let publisher = os_release.get("CITADEL_PUBLISHER");
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.unwrap_or(&"prod".to_owned()).to_owned(),
component_version,
publisher: publisher.unwrap_or(&"Subgraph".to_string()).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)?;
// 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 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,49 @@
use clap::{arg, command, ArgAction, Command};
use std::process::exit;
mod fetch;
pub fn main() {
let matches = command!() // requires `cargo` feature
.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") {

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,9 @@ walkdir = "2"
dbus = "0.6"
posix-acl = "1.0.0"
procfs = "0.12.0"
semver = "1.0"
anyhow = "1.0"
clap = "4.5"
[dependencies.inotify]
version = "0.8"

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

@ -420,8 +420,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)

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 logical 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,21 @@
[package]
name = "update_generator"
description = "Utility to create updates for citadel's components"
version = "0.1.0"
authors = ["isa <isa@subgraph.com>"]
edition = "2021"
[dependencies]
libcitadel = { path = "../libcitadel" }
ed25519-dalek = {version = "2.1", features = ["rand_core", "pem"]}
serde_cbor = "0.11"
serde = { version = "1.0", features = ["derive"] }
serde_derive = "1.0"
rand = "0.8"
clap = { version = "4.5", features = ["derive"] }
rpassword = "7.3"
semver = "1.0"
zeroize = "1.8"
sodiumoxide = "0.2"
anyhow = "1.0"
glob = "0.3"

View File

@ -0,0 +1,63 @@
# Introduction
This code is used to produce the files needed to update citadel.
The citadel-fetch command is installed in citadel and may be run manually to check for updates.
N.B.
This update framework is at the moment not production ready. It defends against arbitrary installation and rollback attacks. It partially defends against endless data attacks.
It does not defend against fast-forward or indefinite freeze attacks. This system is vulnerable to key compromise and does not provide a method to update the root key through the update mechanism.
Further work needs to be done to prevent these issues. Consider using libraries from the Update Framework (TUF).
# Usage
Within the citadel-tools directory, run `cargo run --bin update_generator generate-keypair` to generate a keypair. Replace the existing public key file by moving update_server_key.pub to citadel/meta-citadel/recipes-citadel/citadel-config/files/citadel-fetch/update_server_key.pub .
The private key is available in the same directory as keypair.priv.
Run `cargo run --bin update_generator create-signed-file -r 1.0.0 -k 6.5.3 -e 1.0.0` replacing the version numbers with yours and a version.cbor file will be generated.
Alternatively, the `create-signed-file` can be run with no parameters in which case it will read the version number of the available images in build/images and generate a version.cbor automagically.
Finally, the files generated (version.cbor and images) must be uploaded/ Run `cargo run --bin update_generator upload-to-server` to automatically upload all the files to the server.
To perform the upload manually, put this version.cbor file to the server at: {UPDATE_SERVER_NAME}/{CLIENT}/{CHANNEL}/. For example: https://update.subgraph.com/public/dev/ .
Upload the image files to {UPDATE_SERVER_NAME}/{CLIENT}/{CHANNEL}/. For example: https://update.subgraph.com/public/dev/rootfs_1.0.0.img
More information is available in the `update_generator` help menu.
For debugging, testing and internal dev use, the config.toml file provides a way to change the path on the server where the files are saved by changing the CLIENT and CHANNEL.
# Basic structure
We host the files used by this program as a file server. The current hostname is update.subgraph.com.
An installed citadel server will automatically check for updates by reading the file at https://update.subgraph.com/{CLIENT}/{CHANNEL}/version.cbor
{CLIENT} will default to "public" but is read from the currently running system.
{CHANNEL} will default to "prod" but is read from the currently running system.
The version.cbor file is signed with Subgraph-controlled keys or equivalent client-controlled keys. The corresponding public key will be embedded in the rootfs image during build. The public key is called update_server_key.pub and is stored in /etc/citadel/ .
# Update File Structure
## version.cbor file
The version.cbor file is the only file read during update and contains all information required for the user's system to decide to update or not as well as where the update files are located on the server.
This file is merely a container which provides a cryptographic guarantee that the serialized_citadel_version struct (which contains the actual information we need to disseminate) is authentic.
1. serialized_citadel_version (CitadelVersionStruct): the serialized data containing the actionable information we need to read to make decisions on the update
2. signature: the ed25519 signature of the above serialized data
3. signatory: the name of the org or person who produced the version.cbor file
## CitadelVersionStruct
1. client: the Subgraph client making the request
2. channel: whether this is a production release or other
3. component_version (Vec<ComponentVersion>): a vector containing structures called ComponentVersion which contain the information on each component's version number and the location of the download file of the component
4. publisher: the name of the org or person who released this update
## ComponentVersion
1. component: the name of the image we may update. Either the rootfs, the kernel or the extra image
2. version: a string which contains the semver describing the version of the component
3. file_path: where on the update server can the file be downloaded from. This is relative to the domain name we are currently fetching from

View File

@ -0,0 +1,530 @@
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 = "Generate the file we use to inform clients of updates")]
#[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("-P 22514")
.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()),
)),
)
}