1
0
forked from brl/citadel-tools

4 Commits

Author SHA1 Message Date
isa
2a0735883a merge upstream 2025-10-01 19:46:11 +00:00
isa
729c197dcc Correct timezone setting 2025-08-08 16:35:19 -04:00
isa
c4308b3532 Fix unnecessary public scope 2025-08-08 15:20:44 -04:00
isa
0dbfaaffab Add timezone install dbus method and replace dbus with zbus 2025-08-08 15:13:10 -04:00
32 changed files with 891 additions and 4117 deletions

1279
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -159,7 +159,7 @@ impl <'a> RealmFSInfoRender <'a> {
fn render_image(&mut self) {
fn sizes(r: &RealmFS) -> Result<(usize,usize)> {
let free = r.free_size_blocks()?;
let allocated = r.allocated_size_blocks();
let allocated = r.allocated_size_blocks()?;
Ok((free,allocated))
}

View File

@@ -17,7 +17,6 @@ toml = "0.9"
hex = "0.4"
byteorder = "1"
pwhash = "1.0"
rand = "0.8"
tempfile = "3"
zbus = "5.9.0"
anyhow = "1.0"
@@ -25,17 +24,4 @@ log = "0.4"
zbus_macros = "5.9"
event-listener = "5.4"
futures-timer = "3.0"
tokio = { version = "1", features = ["full"] }
rs-release = "0.1"
glob = "0.3"
serde_cbor = "0.11"
ed25519-dalek = { version = "2.2", features = ["pem", "rand_core"] }
base64ct = "=1.7.3"
ureq = { version = "3.1" }
sha2 = "0.10"
nix = "0.30"
dialoguer = "0.12"
indicatif = "0.18"
serde_json = "1.0"
chrono = "0.4"
env_logger = "0.11"
tokio = { version = "1", features = ["full"] }

View File

@@ -136,10 +136,8 @@ fn compare_boot_partitions(a: Option<Partition>, b: Partition) -> Option<Partiti
}
// Compare versions and channels
let bind_a = a.metainfo();
let bind_b = b.metainfo();
let a_v = bind_a.version();
let b_v = bind_b.version();
let a_v = a.metainfo().version();
let b_v = b.metainfo().version();
// Compare versions only if channels match
if a.metainfo().channel() == b.metainfo().channel() {

View File

@@ -1,949 +0,0 @@
use super::config::Config;
use super::keyring::ChannelKey;
use super::metadata::*;
use anyhow::{Context, Result};
use ed25519_dalek::{Signature, Verifier, VerifyingKey};
use indicatif::{ProgressBar, ProgressStyle};
use libcitadel::ImageHeader;
use serde::Serialize;
use sha2::{Digest, Sha256};
use std::collections::HashMap;
use std::convert::TryInto;
use std::fs::{self, File};
use std::io::{Read, Write};
use std::path::{Path, PathBuf};
const METADATA_CACHE_DIR: &str = "/storage/citadel-state/tuf-cache/metadata";
const TARGETS_CACHE_DIR: &str = "/storage/citadel-state/tuf-cache/targets";
const EMBEDDED_ROOT: &str = "/etc/citadel/root.json";
pub struct TufClient {
config: Config,
metadata_dir: PathBuf,
targets_dir: PathBuf,
root: Option<SignedMetadata<RootMetadata>>,
timestamp: Option<SignedMetadata<TimestampMetadata>>,
snapshot: Option<SignedMetadata<SnapshotMetadata>>,
targets: Option<SignedMetadata<TargetsMetadata>>,
channel_targets: HashMap<String, SignedMetadata<TargetsMetadata>>,
os_release_info: HashMap<String, String>,
}
#[derive(Debug, Clone)]
pub struct UpdateInfo {
pub component: String,
pub current_version: String,
pub new_version: String,
pub target_path: String,
pub download_size: Option<u64>,
}
#[derive(Debug, Clone)]
pub struct ChannelInfo {
pub name: String,
pub key_id: String,
}
impl TufClient {
pub fn new(config: &Config) -> Result<Self> {
let metadata_dir = PathBuf::from(METADATA_CACHE_DIR);
let targets_dir = PathBuf::from(TARGETS_CACHE_DIR);
fs::create_dir_all(&metadata_dir)?;
fs::create_dir_all(&targets_dir)?;
let root = load_or_bootstrap_root(&metadata_dir)?;
let os_release_info =
super::config::parse_conf_file(Path::new("/etc/os-release")).unwrap_or_default();
Ok(TufClient {
config: config.clone(),
metadata_dir,
targets_dir,
root: Some(root),
timestamp: None,
snapshot: None,
targets: None,
channel_targets: HashMap::new(),
os_release_info,
})
}
pub fn refresh_metadata(&mut self) -> Result<()> {
log::info!("Refreshing all metadata");
self.update_root()?;
self.update_timestamp()?;
self.update_snapshot()?;
self.update_targets()?;
log::info!("Metadata refresh complete");
Ok(())
}
fn update_root(&mut self) -> Result<()> {
log::info!("Updating root metadata");
let mut current_root = self.root.clone().unwrap();
let mut version = current_root.signed.version;
loop {
let next_version = version + 1;
log::debug!("Attempting to fetch root metadata version {}", next_version);
let url = format!(
"{}/{}.root.json",
self.config.repository_url(),
next_version
);
match fetch_url(&url) {
Ok(content) => {
log::debug!(
"Successfully fetched root metadata version {}",
next_version
);
let new_root: SignedMetadata<RootMetadata> =
serde_json::from_str(&content).context("Failed to parse root.json")?;
if new_root.signed.version != next_version {
anyhow::bail!("Root version mismatch");
}
check_expiry(&new_root.signed.expires)?;
// Verify with current root's keys
log::debug!("Verifying new root with old root keys");
self.verify_root_signature(&new_root, &current_root)?;
// Update for next iteration
current_root = new_root.clone();
version = next_version;
// Save to cache
let cache_path = self.metadata_dir.join("root.json");
fs::write(&cache_path, &content)?;
log::debug!("Saved new root metadata to cache");
// Update self.root
self.root = Some(new_root);
}
Err(e) => {
log::debug!(
"Failed to fetch root version {}: {}. Assuming this is the latest version.",
next_version,
e
);
break;
}
}
}
Ok(())
}
fn update_timestamp(&mut self) -> Result<()> {
log::info!("Updating timestamp metadata");
let url = format!("{}/timestamp.json", self.config.repository_url());
let content = fetch_url(&url).context("Failed to fetch timestamp.json")?;
let timestamp: SignedMetadata<TimestampMetadata> =
serde_json::from_str(&content).context("Failed to parse timestamp.json")?;
log::debug!("Verifying timestamp metadata signature");
self.verify_role_signature(&timestamp, "timestamp")?;
check_expiry(&timestamp.signed.expires)?;
if let Some(cached) = &self.timestamp {
if timestamp.signed.version < cached.signed.version {
log::error!(
"Timestamp rollback detected! Cached version: {}, new version: {}",
cached.signed.version,
timestamp.signed.version
);
anyhow::bail!("Timestamp rollback detected");
}
}
let cache_path = self.metadata_dir.join("timestamp.json");
fs::write(&cache_path, &content)?;
log::debug!("Saved new timestamp metadata to cache");
self.timestamp = Some(timestamp);
Ok(())
}
fn update_snapshot(&mut self) -> Result<()> {
log::info!("Updating snapshot metadata");
let timestamp = self.timestamp.as_ref().unwrap();
let snapshot_meta = timestamp
.signed
.meta
.get("snapshot.json")
.ok_or_else(|| anyhow::anyhow!("No snapshot.json in timestamp"))?;
let url = format!("{}/snapshot.json", self.config.repository_url());
let content = fetch_url(&url).context("Failed to fetch snapshot.json")?;
if let Some(hashes) = &snapshot_meta.hashes {
if let Some(expected) = hashes.get("sha256") {
let actual = hex::encode(Sha256::digest(content.as_bytes()));
if &actual != expected {
log::error!(
"Snapshot hash mismatch! Expected: {}, actual: {}",
expected,
actual
);
anyhow::bail!("Snapshot hash mismatch");
}
log::debug!("Snapshot hash verified");
}
}
let snapshot: SignedMetadata<SnapshotMetadata> =
serde_json::from_str(&content).context("Failed to parse snapshot.json")?;
log::debug!("Verifying snapshot metadata signature");
self.verify_role_signature(&snapshot, "snapshot")?;
check_expiry(&snapshot.signed.expires)?;
let cache_path = self.metadata_dir.join("snapshot.json");
fs::write(&cache_path, &content)?;
log::debug!("Saved new snapshot metadata to cache");
self.snapshot = Some(snapshot);
Ok(())
}
fn update_targets(&mut self) -> Result<()> {
log::info!("Updating targets metadata");
let snapshot = self.snapshot.as_ref().unwrap();
let _targets_meta = snapshot
.signed
.meta
.get("targets.json")
.ok_or_else(|| anyhow::anyhow!("No targets.json in snapshot"))?;
let url = format!("{}/targets.json", self.config.repository_url());
let content = fetch_url(&url).context("Failed to fetch targets.json")?;
let targets: SignedMetadata<TargetsMetadata> =
serde_json::from_str(&content).context("Failed to parse targets.json")?;
log::debug!("Verifying targets metadata signature");
self.verify_role_signature(&targets, "targets")?;
check_expiry(&targets.signed.expires)?;
let cache_path = self.metadata_dir.join("targets.json");
fs::write(&cache_path, &content)?;
log::debug!("Saved new targets metadata to cache");
self.targets = Some(targets);
Ok(())
}
fn load_channel_targets(&mut self, channel: &str) -> Result<()> {
if self.channel_targets.contains_key(channel) {
log::debug!("Channel targets for '{}' already loaded", channel);
return Ok(());
}
log::info!("Loading channel targets for '{}'", channel);
let url = format!("{}/{}.json", self.config.repository_url(), channel);
let content =
fetch_url(&url).with_context(|| format!("Failed to fetch {}.json", channel))?;
let channel_targets: SignedMetadata<TargetsMetadata> =
serde_json::from_str(&content).context("Failed to parse channel targets")?;
log::debug!(
"Verifying channel targets metadata signature for '{}'",
channel
);
self.verify_delegation_signature(&channel_targets, channel)?;
check_expiry(&channel_targets.signed.expires)?;
let cache_path = self.metadata_dir.join(format!("{}.json", channel));
fs::write(&cache_path, &content)?;
log::debug!(
"Saved new channel targets metadata for '{}' to cache",
channel
);
self.channel_targets
.insert(channel.to_string(), channel_targets);
Ok(())
}
pub fn check_for_updates(&mut self, channel: &str) -> Result<Vec<UpdateInfo>> {
log::info!("Checking for updates for channel '{}'", channel);
self.load_channel_targets(channel)?;
let channel_targets = self.channel_targets.get(channel).unwrap();
let mut updates = Vec::new();
for (target_path, info) in &channel_targets.signed.targets {
log::debug!("Checking target: {}", target_path);
let custom = match &info.custom {
Some(c) => c,
None => {
log::debug!("Target {} has no custom info, skipping", target_path);
continue;
}
};
let component = &custom.image_type;
log::debug!("Target component: {}", component);
let current_version = match component.as_str() {
"rootfs" => {
let version = self
.os_release_info
.get("CITADEL_ROOTFS_VERSION")
.map(|s| s.as_str())
.unwrap_or("0.0.0")
.to_string();
log::debug!("Current rootfs version from os-release: {}", version);
version
}
"kernel" => {
let base_path = format!("/storage/resources/{}", self.config.channel);
// Try to find any citadel-kernel*.img file and pick the highest version
if let Some((image_path, version)) =
find_highest_version_image(&base_path, "citadel-kernel")
{
log::debug!(
"Found kernel image at {} with version {}",
image_path.display(),
version
);
version
} else {
log::debug!("No kernel image found in {}", base_path);
"0.0.0".to_string()
}
}
"extra" => {
let base_path = format!("/storage/resources/{}", self.config.channel);
// Try to find any citadel-extra*.img file and pick the highest version
if let Some((image_path, version)) =
find_highest_version_image(&base_path, "citadel-extra")
{
log::debug!(
"Found extra image at {} with version {}",
image_path.display(),
version
);
version
} else {
log::debug!("No extra image found in {}", base_path);
"0.0.0".to_string()
}
}
_ => {
log::warn!("Unknown component type: {}", component);
"0.0.0".to_string()
}
};
log::debug!(
"Comparing new version {} with current version {}",
&custom.version,
&current_version
);
if version_gt(&custom.version, &current_version) {
// Check min_version requirement
if let Some(min) = &custom.min_version {
if version_gt(min, &current_version) {
log::warn!("Update for {} available, but current version {} is less than minimum required version {}. Skipping.", component, &current_version, min);
continue;
}
}
log::info!(
"Found update for {}: {} -> {}",
component,
&current_version,
&custom.version
);
updates.push(UpdateInfo {
component: component.to_string(),
current_version: current_version.to_string(),
new_version: custom.version.clone(),
target_path: target_path.clone(),
download_size: Some(info.length),
});
} else {
log::debug!("Component {} is up to date", component);
}
}
Ok(updates)
}
pub fn download_target(&mut self, channel: &str, target_path: &str) -> Result<PathBuf> {
log::info!(
"Downloading target '{}' from channel '{}'",
target_path,
channel
);
self.load_channel_targets(channel)?;
let channel_targets = self.channel_targets.get(channel).unwrap();
let target_info = channel_targets
.signed
.targets
.get(target_path)
.ok_or_else(|| anyhow::anyhow!("Target not found: {}", target_path))?;
let expected_hash = target_info
.hashes
.get("sha256")
.ok_or_else(|| anyhow::anyhow!("No sha256 hash for target"))?;
let filename = Path::new(target_path)
.file_name()
.unwrap()
.to_string_lossy();
let url = format!("{}/targets/{}", self.config.repository_url(), target_path);
let local_path = self.targets_dir.join(&*filename);
log::debug!("Downloading from {} to {}", url, local_path.display());
download_file(&url, &local_path, target_info.length)?;
// Verify hash
log::debug!(
"Verifying hash for downloaded file {}",
local_path.display()
);
let actual_hash = hash_file(&local_path)?;
if actual_hash != *expected_hash {
fs::remove_file(&local_path).ok();
log::error!(
"Hash mismatch for {}! Expected: {}, actual: {}",
local_path.display(),
expected_hash,
actual_hash
);
anyhow::bail!(
"Hash mismatch!\n Expected: {}\n Actual: {}",
expected_hash,
actual_hash
);
}
log::info!("Hash verified for {}", local_path.display());
Ok(local_path)
}
pub fn list_channels(&self) -> Result<Vec<ChannelInfo>> {
log::info!("Listing available channels");
let targets = self
.targets
.as_ref()
.ok_or_else(|| anyhow::anyhow!("Targets not loaded"))?;
let mut channels = Vec::new();
if let Some(delegations) = &targets.signed.delegations {
for role in &delegations.roles {
let key_id = role.keyids.first().cloned().unwrap_or_default();
log::debug!("Found channel: {} with key ID: {}", role.name, key_id);
channels.push(ChannelInfo {
name: role.name.clone(),
key_id,
});
}
}
log::debug!("Found {} channels", channels.len());
Ok(channels)
}
pub fn get_channel_key(&self, channel: &str) -> Result<ChannelKey> {
log::info!("Getting channel key for '{}'", channel);
let targets = self
.targets
.as_ref()
.ok_or_else(|| anyhow::anyhow!("Targets not loaded"))?;
let delegations = targets
.signed
.delegations
.as_ref()
.ok_or_else(|| anyhow::anyhow!("No delegations in targets"))?;
let role = delegations
.roles
.iter()
.find(|r| r.name == channel)
.ok_or_else(|| anyhow::anyhow!("Channel '{}' not found", channel))?;
let key_id = role
.keyids
.first()
.ok_or_else(|| anyhow::anyhow!("No key for channel '{}'", channel))?;
let key = delegations
.keys
.get(key_id)
.ok_or_else(|| anyhow::anyhow!("Key {} not found", key_id))?;
log::debug!(
"Retrieved key for channel '{}': key ID = {}, public key = {}...",
channel,
key_id,
&key.keyval.public[..16]
);
Ok(ChannelKey {
key_id: key_id.clone(),
public_key: key.keyval.public.clone(),
})
}
fn verify_root_signature(
&self,
new_root: &SignedMetadata<RootMetadata>,
old_root: &SignedMetadata<RootMetadata>,
) -> Result<()> {
log::debug!("Verifying root signature");
// Verify with old root's keys first
let role_def = old_root
.signed
.roles
.get("root")
.ok_or_else(|| anyhow::anyhow!("No root role in root.json"))?;
log::debug!("Verifying new root with old root's keys");
verify_signatures(
new_root,
&role_def.keyids,
role_def.threshold,
&old_root.signed.keys,
)?;
// Then verify with new root's keys
let new_role_def = new_root
.signed
.roles
.get("root")
.ok_or_else(|| anyhow::anyhow!("No root role in new root.json"))?;
log::debug!("Verifying new root with new root's keys");
verify_signatures(
new_root,
&new_role_def.keyids,
new_role_def.threshold,
&new_root.signed.keys,
)
}
fn verify_role_signature<T: Serialize>(
&self,
signed: &SignedMetadata<T>,
role: &str,
) -> Result<()> {
log::debug!("Verifying role signature for role: {}", role);
let root = self
.root
.as_ref()
.ok_or_else(|| anyhow::anyhow!("Root not loaded"))?;
let role_def = root
.signed
.roles
.get(role)
.ok_or_else(|| anyhow::anyhow!("Role '{}' not found", role))?;
log::debug!(
"Role '{}' found. Key IDs: {:?}, Threshold: {}",
role,
role_def.keyids,
role_def.threshold
);
verify_signatures(
signed,
&role_def.keyids,
role_def.threshold,
&root.signed.keys,
)
}
fn verify_delegation_signature<T: Serialize>(
&self,
signed: &SignedMetadata<T>,
delegation_name: &str,
) -> Result<()> {
log::debug!(
"Verifying delegation signature for delegation: {}",
delegation_name
);
let targets = self
.targets
.as_ref()
.ok_or_else(|| anyhow::anyhow!("Targets not loaded"))?;
let delegations = targets
.signed
.delegations
.as_ref()
.ok_or_else(|| anyhow::anyhow!("No delegations"))?;
let role = delegations
.roles
.iter()
.find(|r| r.name == delegation_name)
.ok_or_else(|| anyhow::anyhow!("Delegation '{}' not found", delegation_name))?;
log::debug!(
"Delegation '{}' found. Key IDs: {:?}, Threshold: {}",
delegation_name,
role.keyids,
role.threshold
);
log::debug!(
"Available delegation keys: {:?}",
delegations.keys.keys().collect::<Vec<_>>()
);
verify_signatures(signed, &role.keyids, role.threshold, &delegations.keys)
}
}
fn verify_signatures<T: Serialize>(
signed: &SignedMetadata<T>,
authorized_keyids: &[String],
threshold: u32,
keys: &HashMap<String, TufKey>,
) -> Result<()> {
let canonical_bytes = canonicalize_json(&signed.signed)?;
log::debug!(
"Canonical JSON (first 200 bytes): {:?}",
&canonical_bytes[..std::cmp::min(200, canonical_bytes.len())]
);
let authorized_set: std::collections::HashSet<&String> = authorized_keyids.iter().collect();
let mut valid_count = 0u32;
log::debug!("Verifying signatures. Threshold: {}", threshold);
log::debug!("Authorized key IDs: {:?}", authorized_keyids);
for sig in &signed.signatures {
log::debug!("Processing signature with key ID: {}", sig.keyid);
if !authorized_set.contains(&sig.keyid) {
log::debug!(
"Signature with key ID {} is not in the authorized set, skipping.",
sig.keyid
);
continue;
}
let tuf_key = match keys.get(&sig.keyid) {
Some(k) => {
log::debug!("Found key for ID {}", sig.keyid);
k
}
None => {
log::warn!("Key not found for ID {}", sig.keyid);
continue;
}
};
if tuf_key.keytype != "ed25519" {
log::warn!(
"Unsupported key type for key ID {}: {}",
sig.keyid,
tuf_key.keytype
);
continue;
}
let pub_bytes = match hex::decode(&tuf_key.keyval.public) {
Ok(b) => b,
Err(e) => {
log::warn!(
"Failed to decode public key for key ID {}: {}",
sig.keyid,
e
);
continue;
}
};
let pub_array: [u8; 32] = match pub_bytes.try_into() {
Ok(a) => a,
Err(_) => {
log::warn!("Public key for key ID {} has incorrect length", sig.keyid);
continue;
}
};
let verifying_key = match VerifyingKey::from_bytes(&pub_array) {
Ok(k) => k,
Err(e) => {
log::warn!(
"Failed to construct verifying key from public key for key ID {}: {}",
sig.keyid,
e
);
continue;
}
};
let sig_bytes = match hex::decode(&sig.sig) {
Ok(b) => b,
Err(e) => {
log::warn!("Failed to decode signature for key ID {}: {}", sig.keyid, e);
continue;
}
};
let sig_array: [u8; 64] = match sig_bytes.try_into() {
Ok(a) => a,
Err(_) => {
log::warn!("Signature for key ID {} has incorrect length", sig.keyid);
continue;
}
};
let signature = Signature::from_bytes(&sig_array);
if verifying_key.verify(&canonical_bytes, &signature).is_ok() {
log::debug!("Signature verified successfully for key ID {}", sig.keyid);
valid_count += 1;
} else {
log::warn!("Signature verification failed for key ID {}", sig.keyid);
}
}
log::debug!(
"Final verification count: valid_count={}, threshold={}",
valid_count,
threshold
);
if valid_count >= threshold {
log::debug!(
"Signature verification successful. Got {} valid signatures, threshold is {}",
valid_count,
threshold
);
Ok(())
} else {
log::error!(
"Signature verification failed: got {} valid, need {}",
valid_count,
threshold
);
anyhow::bail!(
"Signature verification failed: got {} valid, need {}",
valid_count,
threshold
)
}
}
fn canonicalize_json<T: Serialize>(value: &T) -> Result<Vec<u8>> {
let json_value = serde_json::to_value(value)?;
let canonical = sort_json_keys(json_value);
Ok(serde_json::to_vec(&canonical)?)
}
fn sort_json_keys(value: serde_json::Value) -> serde_json::Value {
use serde_json::Value;
use std::collections::BTreeMap;
match value {
Value::Object(map) => {
let sorted: BTreeMap<String, Value> = map
.into_iter()
.map(|(k, v)| (k, sort_json_keys(v)))
.collect();
Value::Object(sorted.into_iter().collect())
}
Value::Array(arr) => Value::Array(arr.into_iter().map(sort_json_keys).collect()),
other => other,
}
}
fn load_or_bootstrap_root(metadata_dir: &Path) -> Result<SignedMetadata<RootMetadata>> {
let cached_path = metadata_dir.join("root.json");
if cached_path.exists() {
let content = fs::read_to_string(&cached_path)?;
if let Ok(root) = serde_json::from_str(&content) {
return Ok(root);
}
}
if Path::new(EMBEDDED_ROOT).exists() {
let content = fs::read_to_string(EMBEDDED_ROOT)?;
let root: SignedMetadata<RootMetadata> = serde_json::from_str(&content)?;
fs::write(&cached_path, &content)?;
return Ok(root);
}
anyhow::bail!(
"No TUF root.json found at {} or {}",
cached_path.display(),
EMBEDDED_ROOT
)
}
fn check_expiry(expires: &str) -> Result<()> {
use chrono::{DateTime, Utc};
let expiry: DateTime<Utc> = expires.parse().context("Failed to parse expiry")?;
if expiry < Utc::now() {
anyhow::bail!("Metadata expired: {}", expires);
}
Ok(())
}
fn fetch_url(url: &str) -> Result<String> {
let mut response = ureq::get(url)
.call()
.with_context(|| format!("Failed to fetch {}", url))?;
response
.body_mut()
.read_to_string()
.with_context(|| format!("Failed to read response from {}", url))
}
fn download_file(url: &str, dest: &Path, expected_size: u64) -> Result<()> {
println!(" Downloading from {}", url);
// Create progress bar
let pb = ProgressBar::new(expected_size);
pb.set_style(
ProgressStyle::default_bar()
.template(" [{bar:40.cyan/blue}] {bytes}/{total_bytes} ({eta})")
.unwrap()
.progress_chars("#>-"),
);
let mut response = ureq::get(url).call().context("Failed to download file")?;
let mut file =
File::create(dest).with_context(|| format!("Failed to create file: {}", dest.display()))?;
let mut downloaded = 0u64;
let mut buffer = [0u8; 8192];
let mut reader = response.body_mut().as_reader();
loop {
match reader.read(&mut buffer) {
Ok(0) => break,
Ok(n) => {
file.write_all(&buffer[..n])?;
downloaded += n as u64;
pb.set_position(downloaded);
}
Err(e) => return Err(e.into()),
}
}
pb.finish_and_clear();
// Verify size
if downloaded != expected_size {
fs::remove_file(dest).ok();
anyhow::bail!(
"Size mismatch: expected {} bytes, got {}",
expected_size,
downloaded
);
}
println!(" ✓ Downloaded {} bytes", downloaded);
Ok(())
}
fn hash_file(path: &Path) -> Result<String> {
let mut file = File::open(path)?;
let mut hasher = Sha256::new();
let mut buffer = [0u8; 8192];
loop {
let n = file.read(&mut buffer)?;
if n == 0 {
break;
}
hasher.update(&buffer[..n]);
}
Ok(hex::encode(hasher.finalize()))
}
/// Find the highest versioned image file matching the pattern
/// e.g., citadel-kernel-*.img or citadel-extra-*.img
fn find_highest_version_image(
base_path: &str,
component_prefix: &str,
) -> Option<(PathBuf, String)> {
use glob::glob;
let pattern = format!("{}/{}*.img", base_path, component_prefix);
log::debug!("Searching for images with pattern: {}", pattern);
let mut best_path: Option<PathBuf> = None;
let mut best_version = "0.0.0".to_string();
if let Ok(entries) = glob(&pattern) {
for entry in entries.flatten() {
log::debug!("Found image file: {}", entry.display());
// Try to read version from the image header
if let Ok(header) = ImageHeader::from_file(&entry) {
let version = if component_prefix.contains("kernel") {
header
.metainfo()
.kernel_version()
.map(|v| v.to_string())
.unwrap_or_else(|| "0.0.0".to_string())
} else {
header.metainfo().version().to_string()
};
log::debug!(" Image version: {}", version);
if version_gt(&version, &best_version) {
best_version = version;
best_path = Some(entry);
}
}
}
}
best_path.map(|path| {
log::debug!(
"Selected highest version image: {} (version: {})",
path.display(),
best_version
);
(path, best_version)
})
}
fn version_gt(a: &str, b: &str) -> bool {
let parse = |s: &str| -> Vec<u32> { s.split('.').filter_map(|p| p.parse().ok()).collect() };
let va = parse(a);
let vb = parse(b);
for (a, b) in va.iter().zip(vb.iter()) {
if a > b {
return true;
}
if a < b {
return false;
}
}
va.len() > vb.len()
}

View File

@@ -1,110 +0,0 @@
use anyhow::{Context, Result};
use std::collections::HashMap;
use std::fs;
use std::path::Path;
const UPDATE_CONF_PATH: &str = "/etc/citadel/update.conf";
const USER_CONF_PATH: &str = "/storage/citadel-state/citadel.conf";
const OS_RELEASE_PATH: &str = "/etc/os-release";
#[derive(Debug, Clone)]
pub struct Config {
pub base_url: String,
pub client: String,
pub channel: String,
pub min_root_version: u32,
pub require_signatures: bool,
}
impl Config {
pub fn load() -> Result<Self> {
let system_conf = parse_conf_file(Path::new(UPDATE_CONF_PATH)).unwrap_or_default();
let user_conf = parse_conf_file(Path::new(USER_CONF_PATH)).unwrap_or_default();
let os_release = parse_conf_file(Path::new(OS_RELEASE_PATH)).unwrap_or_default();
// User config overrides system config, which overrides os-release
let channel = user_conf
.get("UPDATE_CHANNEL")
.or_else(|| system_conf.get("DEFAULT_CHANNEL"))
.or_else(|| os_release.get("CITADEL_CHANNEL"))
.cloned()
.unwrap_or_else(|| "stable".to_string());
let client = system_conf
.get("UPDATE_CLIENT")
.cloned()
.unwrap_or_else(|| "public".to_string());
let base_url = system_conf
.get("UPDATE_BASE_URL")
.cloned()
.unwrap_or_else(|| "https://update.subgraph.com".to_string());
let min_root_version = system_conf
.get("MIN_ROOT_VERSION")
.and_then(|s| s.parse().ok())
.unwrap_or(1);
let require_signatures = system_conf
.get("REQUIRE_SIGNATURES")
.map(|s| s == "true" || s == "1")
.unwrap_or(true);
Ok(Config {
base_url,
client,
channel,
min_root_version,
require_signatures,
})
}
pub fn save(&self) -> Result<()> {
let dir = Path::new(USER_CONF_PATH).parent().unwrap();
fs::create_dir_all(dir)?;
let mut content = String::new();
// Read existing config to preserve other settings
if let Ok(existing) = fs::read_to_string(USER_CONF_PATH) {
for line in existing.lines() {
if !line.starts_with("UPDATE_CHANNEL=") {
content.push_str(line);
content.push('\n');
}
}
}
content.push_str(&format!("UPDATE_CHANNEL=\"{}\"\n", self.channel));
fs::write(USER_CONF_PATH, content)?;
Ok(())
}
pub fn repository_url(&self) -> String {
format!("{}/{}", self.base_url, self.client)
}
}
pub fn parse_conf_file(path: &Path) -> Result<HashMap<String, String>> {
let content =
fs::read_to_string(path).with_context(|| format!("Failed to read {}", path.display()))?;
let mut conf = HashMap::new();
for line in content.lines() {
let line = line.trim();
if line.is_empty() || line.starts_with('#') {
continue;
}
if let Some(pos) = line.find('=') {
let key = line[..pos].trim().to_string();
let value = line[pos + 1..].trim().trim_matches('"').to_string();
conf.insert(key, value);
}
}
Ok(conf)
}

View File

@@ -1,210 +0,0 @@
use anyhow::{Context, Result};
use chrono::Utc;
use serde::{Deserialize, Serialize};
use sha2::{Digest, Sha256};
use std::collections::HashMap;
use std::fs;
use std::path::Path;
const KEYRING_PATH: &str = "/storage/citadel-state/trusted-keys/keyring.json";
const BUILTIN_KEYS_DIR: &str = "/usr/share/citadel/keys";
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Keyring {
pub version: u32,
pub trusted_keys: HashMap<String, TrustedKey>,
#[serde(default)]
pub default_channels: Vec<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TrustedKey {
pub key_id: String,
pub public_key: String,
pub added_at: String,
pub trust_level: TrustLevel,
pub comment: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "snake_case")]
pub enum TrustLevel {
Default,
UserApproved,
Pending,
}
#[derive(Debug, Clone)]
pub enum TrustStatus {
TrustedDefault,
TrustedUser,
Pending,
Unknown,
KeyMismatch { expected: String, actual: String },
}
#[derive(Debug, Clone)]
pub struct ChannelKey {
pub key_id: String,
pub public_key: String,
}
impl Keyring {
pub fn load() -> Result<Self> {
let mut keyring = if Path::new(KEYRING_PATH).exists() {
let content = fs::read_to_string(KEYRING_PATH).context("Failed to read keyring")?;
serde_json::from_str(&content).context("Failed to parse keyring")?
} else {
Keyring {
version: 1,
trusted_keys: HashMap::new(),
default_channels: Vec::new(),
}
};
// Load built-in keys from rootfs
keyring.load_builtin_keys()?;
Ok(keyring)
}
fn load_builtin_keys(&mut self) -> Result<()> {
let keys_dir = Path::new(BUILTIN_KEYS_DIR);
if !keys_dir.exists() {
return Ok(());
}
for entry in fs::read_dir(keys_dir)? {
let entry = entry?;
let path = entry.path();
// Look for channel public keys (not image keys)
let filename = path.file_stem().and_then(|s| s.to_str()).unwrap_or("");
// Skip image signing keys (*_image.pub)
if filename.ends_with("_image") {
continue;
}
if path.extension().map_or(false, |e| e == "pub") {
let channel = filename.to_string();
let public_key = fs::read_to_string(&path)?.trim().to_string();
let key_id = compute_key_id(&public_key);
// Built-in keys don't override user keys
if !self.trusted_keys.contains_key(&channel) {
self.trusted_keys.insert(
channel.clone(),
TrustedKey {
key_id,
public_key,
added_at: "built-in".to_string(),
trust_level: TrustLevel::Default,
comment: Some("Shipped with OS".to_string()),
},
);
if !self.default_channels.contains(&channel) {
self.default_channels.push(channel);
}
}
}
}
Ok(())
}
pub fn save(&self) -> Result<()> {
let dir = Path::new(KEYRING_PATH).parent().unwrap();
fs::create_dir_all(dir)?;
// Only save non-default keys (default keys come from rootfs)
let saveable: HashMap<_, _> = self
.trusted_keys
.iter()
.filter(|(_, k)| k.trust_level != TrustLevel::Default)
.map(|(k, v)| (k.clone(), v.clone()))
.collect();
let save_keyring = Keyring {
version: self.version,
trusted_keys: saveable,
default_channels: Vec::new(), // Don't save default list
};
let content = serde_json::to_string_pretty(&save_keyring)?;
fs::write(KEYRING_PATH, content)?;
Ok(())
}
pub fn check_trust(&self, channel: &str, key_id: &str) -> TrustStatus {
match self.trusted_keys.get(channel) {
Some(key) => {
if key.key_id == key_id {
match key.trust_level {
TrustLevel::Default => TrustStatus::TrustedDefault,
TrustLevel::UserApproved => TrustStatus::TrustedUser,
TrustLevel::Pending => TrustStatus::Pending,
}
} else {
TrustStatus::KeyMismatch {
expected: key.key_id.clone(),
actual: key_id.to_string(),
}
}
}
None => TrustStatus::Unknown,
}
}
pub fn get_key(&self, channel: &str) -> Option<&TrustedKey> {
self.trusted_keys.get(channel)
}
pub fn is_default(&self, channel: &str) -> bool {
self.default_channels.contains(&channel.to_string())
}
pub fn add_trusted(
&mut self,
channel: &str,
key: &ChannelKey,
level: TrustLevel,
) -> Result<()> {
self.trusted_keys.insert(
channel.to_string(),
TrustedKey {
key_id: key.key_id.clone(),
public_key: key.public_key.clone(),
added_at: Utc::now().to_rfc3339(),
trust_level: level,
comment: None,
},
);
self.save()
}
pub fn add_pending(&mut self, channel: &str, key: &ChannelKey) -> Result<()> {
self.add_trusted(channel, key, TrustLevel::Pending)
}
pub fn approve(&mut self, channel: &str) -> Result<()> {
if let Some(key) = self.trusted_keys.get_mut(channel) {
key.trust_level = TrustLevel::UserApproved;
key.added_at = Utc::now().to_rfc3339();
key.comment = Some("Approved by user".to_string());
}
self.save()
}
pub fn remove(&mut self, channel: &str) -> Result<()> {
self.trusted_keys.remove(channel);
self.save()
}
}
fn compute_key_id(public_key: &str) -> String {
let hash = Sha256::digest(public_key.as_bytes());
hex::encode(&hash[..8])
}

View File

@@ -1,121 +0,0 @@
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SignedMetadata<T> {
pub signatures: Vec<Signature>,
pub signed: T,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Signature {
pub keyid: String,
pub sig: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RootMetadata {
#[serde(rename = "_type")]
pub metadata_type: String,
pub spec_version: String,
pub version: u32,
pub expires: String,
pub keys: HashMap<String, TufKey>,
pub roles: HashMap<String, RoleDefinition>,
pub consistent_snapshot: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TufKey {
pub keytype: String,
pub scheme: String,
pub keyval: KeyValue,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct KeyValue {
pub public: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RoleDefinition {
pub keyids: Vec<String>,
pub threshold: u32,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TimestampMetadata {
#[serde(rename = "_type")]
pub metadata_type: String,
pub spec_version: String,
pub version: u32,
pub expires: String,
pub meta: HashMap<String, MetaFile>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct MetaFile {
pub version: u32,
#[serde(skip_serializing_if = "Option::is_none")]
pub length: Option<u64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub hashes: Option<HashMap<String, String>>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SnapshotMetadata {
#[serde(rename = "_type")]
pub metadata_type: String,
pub spec_version: String,
pub version: u32,
pub expires: String,
pub meta: HashMap<String, MetaFile>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TargetsMetadata {
#[serde(rename = "_type")]
pub metadata_type: String,
pub spec_version: String,
pub version: u32,
pub expires: String,
pub targets: HashMap<String, TargetInfo>,
#[serde(skip_serializing_if = "Option::is_none")]
pub delegations: Option<Delegations>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TargetInfo {
pub length: u64,
pub hashes: HashMap<String, String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub custom: Option<TargetCustom>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TargetCustom {
pub version: String,
pub image_type: String,
pub channel: String,
pub timestamp: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub min_version: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub release_notes_url: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Delegations {
pub keys: HashMap<String, TufKey>,
pub roles: Vec<DelegatedRole>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DelegatedRole {
pub name: String,
pub keyids: Vec<String>,
pub threshold: u32,
pub paths: Vec<String>,
#[serde(default)]
pub terminating: bool,
}

View File

@@ -1,699 +0,0 @@
mod client;
mod config;
mod keyring;
mod metadata;
use anyhow::{Context, Result};
use clap::{Parser, Subcommand};
use std::path::Path;
#[derive(Parser)]
#[command(name = "citadel-fetch")]
#[command(about = "Citadel update client")]
struct Cli {
#[command(subcommand)]
command: Commands,
}
#[derive(Debug, Subcommand)]
enum Commands {
/// Check for available updates
Check,
/// Download and install updates
Update {
/// Don't prompt for confirmation
#[arg(short, long)]
yes: bool,
/// Only download specific component
#[arg(short, long)]
component: Option<String>,
},
/// Show current update status
Status,
/// Manage update channels
Channel {
#[command(subcommand)]
command: ChannelCommands,
},
/// Manage trusted signing keys
Keyring {
#[command(subcommand)]
command: KeyringCommands,
},
/// Refresh TUF metadata from server
Refresh,
}
#[derive(Debug, Subcommand)]
enum ChannelCommands {
/// Show current channel
Show,
/// List available channels
List,
/// Switch to a different channel
Set {
/// Channel name to switch to
channel: String,
},
}
#[derive(Debug, Subcommand)]
enum KeyringCommands {
/// List all trusted keys
List,
/// Show details of a specific channel's key
Show {
/// Channel name
channel: String,
},
/// Add a key from the remote repository
Add {
/// Channel name
channel: String,
},
/// Remove a key from the keyring
Remove {
/// Channel name
channel: String,
},
/// Discover available channels from remote
Discover,
}
pub fn main() {
env_logger::init();
if let Err(e) = run() {
eprintln!("Error: {:#}", e);
std::process::exit(1);
}
}
fn run() -> Result<()> {
let cli = Cli::parse();
log::debug!("Executing command: {:?}", cli.command);
match cli.command {
Commands::Check => cmd_check(),
Commands::Update { yes, component } => cmd_update(yes, component),
Commands::Status => cmd_status(),
Commands::Channel { command } => match command {
ChannelCommands::Show => cmd_channel_show(),
ChannelCommands::List => cmd_channel_list(),
ChannelCommands::Set { channel } => cmd_channel_set(&channel),
},
Commands::Keyring { command } => match command {
KeyringCommands::List => cmd_keyring_list(),
KeyringCommands::Show { channel } => cmd_keyring_show(&channel),
KeyringCommands::Add { channel } => cmd_keyring_add(&channel),
KeyringCommands::Remove { channel } => cmd_keyring_remove(&channel),
KeyringCommands::Discover => cmd_keyring_discover(),
},
Commands::Refresh => cmd_refresh(),
}
}
fn cmd_check() -> Result<()> {
log::debug!("Executing cmd_check");
let config = config::Config::load()?;
let mut client = client::TufClient::new(&config)?;
println!("Checking for updates...");
println!(" Repository: {}", config.repository_url());
println!(" Channel: {}", config.channel);
client.refresh_metadata()?;
let updates = client.check_for_updates(&config.channel)?;
if updates.is_empty() {
println!("✓ System is up to date");
} else {
println!("Updates available:");
for update in &updates {
println!(
" {} {}{}",
update.component, update.current_version, update.new_version
);
if let Some(size) = update.download_size {
println!(" Download size: {:.1} MB", size as f64 / 1_048_576.0);
}
}
println!("Run 'citadel-fetch update' to install updates");
}
Ok(())
}
fn cmd_update(yes: bool, component: Option<String>) -> Result<()> {
log::debug!(
"Executing cmd_update with yes={}, component={:?}",
yes,
component
);
let config = config::Config::load()?;
let mut keyring = keyring::Keyring::load()?;
let mut client = client::TufClient::new(&config)?;
println!("Checking for updates...");
client.refresh_metadata()?;
// Verify channel key trust
let channel_key = client.get_channel_key(&config.channel)?;
let trust_status = keyring.check_trust(&config.channel, &channel_key.key_id);
println!(
"Channel '{}' trust status: {:?}",
config.channel, trust_status
);
match &trust_status {
keyring::TrustStatus::TrustedDefault => {
println!(
" Channel '{}' signed by: {} (default key)",
config.channel,
&channel_key.key_id[..16]
);
}
keyring::TrustStatus::TrustedUser => {
println!(
" Channel '{}' signed by: {} (user approved)",
config.channel,
&channel_key.key_id[..16]
);
}
keyring::TrustStatus::Unknown => {
println!(
"\n⚠ WARNING: Channel '{}' key not in trusted keyring",
config.channel
);
println!(" Key ID: {}", channel_key.key_id);
println!(" Public Key: {}...", &channel_key.public_key[..48]);
println!("\n This key was verified by the TUF root certificate,");
println!(" but you haven't explicitly trusted it yet.");
if !yes && !prompt_yes_no("\nTrust this key and continue?")? {
println!("Update cancelled by user");
return Ok(());
}
keyring.add_trusted(
&config.channel,
&channel_key,
keyring::TrustLevel::UserApproved,
)?;
println!(
"✓ Channel key for '{}' added to trusted keyring",
config.channel
);
}
keyring::TrustStatus::KeyMismatch { expected, actual } => {
println!("\n🚨 WARNING: Channel key has changed!");
println!(" Expected: {}", expected);
println!(" Received: {}", actual);
if !yes {
println!("\nThis could indicate a security issue. Contact channel maintainer.");
if !prompt_yes_no("Accept new key anyway?")? {
println!("Update cancelled by user due to key mismatch");
return Ok(());
}
}
keyring.add_trusted(
&config.channel,
&channel_key,
keyring::TrustLevel::UserApproved,
)?;
println!(
"✓ New channel key for '{}' accepted and added to trusted keyring",
config.channel
);
}
keyring::TrustStatus::Pending => {
println!("\n⚠ Channel key pending approval");
if !yes && !prompt_yes_no("Approve this key and continue?")? {
println!("Update cancelled by user");
return Ok(());
}
keyring.approve(&config.channel)?;
println!("✓ Channel key for '{}' approved", config.channel);
}
}
let updates = client.check_for_updates(&config.channel)?;
// Filter by component if specified
let updates: Vec<_> = if let Some(ref comp) = component {
println!("Filtering updates for component: {}", comp);
updates
.into_iter()
.filter(|u| &u.component == comp)
.collect()
} else {
updates
};
if updates.is_empty() {
println!("\n✓ System is up to date");
return Ok(());
}
println!("\nUpdates to install:");
let mut total_size: u64 = 0;
for update in &updates {
println!(
" {} {}{}",
update.component, update.current_version, update.new_version
);
if let Some(size) = update.download_size {
total_size += size;
}
}
println!(
"\nTotal download: {:.1} MB",
total_size as f64 / 1_048_576.0
);
if !yes && !prompt_yes_no("\nProceed with update?")? {
println!("Update cancelled by user");
return Ok(());
}
println!("Initiating download and installation...");
for update in &updates {
println!("Downloading {}...", update.component);
let path = client.download_target(&config.channel, &update.target_path)?;
println!(" ✓ Downloaded to {}", path.display());
println!("Installing {}...", update.component);
install_update(&update.component, &path)?;
println!(" ✓ Installed");
}
println!("\n═══════════════════════════════════════════════════════════");
println!(" ✓ UPDATE COMPLETE");
println!(" Reboot to apply changes");
println!("═══════════════════════════════════════════════════════════");
Ok(())
}
fn cmd_status() -> Result<()> {
log::debug!("Executing cmd_status");
let config = config::Config::load()?;
let os_release_info = config::parse_conf_file(Path::new("/etc/os-release")).unwrap_or_default();
println!("Citadel Update Status");
println!("═══════════════════════════════════════════════════════════");
println!("System Information:");
println!(
" Distro: {} {}",
os_release_info
.get("NAME")
.unwrap_or(&"Unknown".to_string()),
os_release_info
.get("VERSION_ID")
.unwrap_or(&"Unknown".to_string())
);
println!(" Channel: {}", config.channel);
println!("Update Server:");
println!(" URL: {}", config.repository_url());
Ok(())
}
fn cmd_channel_show() -> Result<()> {
log::debug!("Executing cmd_channel_show");
let config = config::Config::load()?;
let keyring = keyring::Keyring::load()?;
println!("Current channel: {}", config.channel);
if let Some(key) = keyring.get_key(&config.channel) {
println!(" Key ID: {}", key.key_id);
println!(" Trust: {:?}", key.trust_level);
} else {
println!(" No key found for current channel in keyring.");
}
Ok(())
}
fn cmd_channel_list() -> Result<()> {
log::debug!("Executing cmd_channel_list");
let config = config::Config::load()?;
let mut client = client::TufClient::new(&config)?;
let keyring = keyring::Keyring::load()?;
client.refresh_metadata()?;
let channels = client.list_channels()?;
println!("Available channels:");
println!("───────────────────────────────────────────────────────────");
if channels.is_empty() {
println!(" No channels found.");
} else {
for channel in channels {
let current = if channel.name == config.channel {
" (current)"
} else {
""
};
let trust = match keyring.get_key(&channel.name) {
Some(k) => match k.trust_level {
keyring::TrustLevel::Default => "✓ default",
keyring::TrustLevel::UserApproved => "✓ trusted",
keyring::TrustLevel::Pending => "○ pending",
},
None => "○ unknown",
};
println!(
" {:<12} {} [{}]{}",
channel.name,
&channel.key_id[..16],
trust,
current
);
}
}
Ok(())
}
fn cmd_channel_set(channel: &str) -> Result<()> {
log::debug!("Executing cmd_channel_set for channel '{}'", channel);
let mut config = config::Config::load()?;
let mut keyring = keyring::Keyring::load()?;
let mut client = client::TufClient::new(&config)?;
println!("Switching to channel '{}'...", channel);
// Refresh metadata to get channel info
client.refresh_metadata()?;
// Get the channel's signing key
let channel_key = client
.get_channel_key(channel)
.with_context(|| format!("Channel '{}' not found", channel))?;
let trust_status = keyring.check_trust(channel, &channel_key.key_id);
println!("Channel '{}' trust status: {:?}", channel, trust_status);
match trust_status {
keyring::TrustStatus::TrustedDefault => {
println!(
"\n Key: {} (default, shipped with OS)",
&channel_key.key_id[..16]
);
}
keyring::TrustStatus::TrustedUser => {
println!(
"\n Key: {} (previously approved)",
&channel_key.key_id[..16]
);
}
keyring::TrustStatus::Pending => {
println!("\n════════════════════════════════════════════════════════");
println!(" PENDING KEY APPROVAL");
println!("════════════════════════════════════════════════════════");
println!("\n Channel: {}", channel);
println!(" Key ID: {}", channel_key.key_id);
println!("\n This key was fetched but not yet approved.\n");
if !prompt_yes_no("Approve this key?")? {
println!("Channel switch cancelled by user");
return Ok(());
}
keyring.approve(channel)?;
println!("Channel key for '{}' approved", channel);
}
keyring::TrustStatus::Unknown => {
println!("\n════════════════════════════════════════════════════════");
println!(" NEW CHANNEL KEY");
println!("════════════════════════════════════════════════════════");
println!("\n Channel: {}", channel);
println!(" Key ID: {}", channel_key.key_id);
println!(" Key: {}...", &channel_key.public_key[..48]);
println!("\n ⚠ This key is not in your trusted keyring.");
println!(" Only trust keys from verified sources.\n");
if !prompt_yes_no("Trust this key?")? {
println!("Channel switch cancelled by user");
return Ok(());
}
keyring.add_trusted(channel, &channel_key, keyring::TrustLevel::UserApproved)?;
println!(
"New channel key for '{}' trusted and added to keyring",
channel
);
}
keyring::TrustStatus::KeyMismatch { expected, actual } => {
println!("\n════════════════════════════════════════════════════════");
println!(" 🚨 KEY MISMATCH WARNING");
println!("════════════════════════════════════════════════════════");
println!("\n Channel: {}", channel);
println!(" Expected: {}", expected);
println!(" Received: {}", actual);
println!("\n The signing key for this channel has changed.");
println!(" This could indicate:");
println!(" - Legitimate key rotation");
println!(" - A potential security issue");
println!("\n Verify with channel maintainer before accepting.\n");
if !prompt_yes_no("Accept new key?")? {
println!("Channel switch cancelled by user due to key mismatch");
return Ok(());
}
keyring.add_trusted(channel, &channel_key, keyring::TrustLevel::UserApproved)?;
println!(
"New key for channel '{}' accepted despite mismatch",
channel
);
}
}
// Save new channel to config
config.channel = channel.to_string();
config.save()?;
println!("\n✓ Channel switched to '{}'", channel);
Ok(())
}
fn cmd_keyring_list() -> Result<()> {
println!("Executing cmd_keyring_list");
let keyring = keyring::Keyring::load()?;
println!("Trusted Channel Keys");
println!("═══════════════════════════════════════════════════════════");
if keyring.trusted_keys.is_empty() {
println!(" No keys in keyring");
return Ok(());
}
for (channel, key) in &keyring.trusted_keys {
let trust = match key.trust_level {
keyring::TrustLevel::Default => "[default]",
keyring::TrustLevel::UserApproved => "[user] ",
keyring::TrustLevel::Pending => "[pending]",
};
let comment = key.comment.as_deref().unwrap_or("");
println!(
" {:<12} {} {} {}",
channel,
&key.key_id[..16],
trust,
comment
);
}
Ok(())
}
fn cmd_keyring_show(channel: &str) -> Result<()> {
log::debug!("Executing cmd_keyring_show for channel '{}'", channel);
let keyring = keyring::Keyring::load()?;
match keyring.get_key(channel) {
Some(key) => {
println!("Channel: {}", channel);
println!("═══════════════════════════════════════════════════════════");
println!(" Key ID: {}", key.key_id);
println!(" Public Key: {}", key.public_key);
println!(" Trust: {:?}", key.trust_level);
println!(" Added: {}", key.added_at);
if let Some(comment) = &key.comment {
println!(" Comment: {}", comment);
}
}
None => {
println!("No key found for channel '{}'", channel);
println!("Use 'citadel-fetch keyring add {}' to add it", channel);
}
}
Ok(())
}
fn cmd_keyring_add(channel: &str) -> Result<()> {
log::debug!("Executing cmd_keyring_add for channel '{}'", channel);
let config = config::Config::load()?;
let mut keyring = keyring::Keyring::load()?;
let mut client = client::TufClient::new(&config)?;
println!("Fetching key for channel '{}'...", channel);
client.refresh_metadata()?;
let channel_key = client
.get_channel_key(channel)
.with_context(|| format!("Channel '{}' not found", channel))?;
println!("\n Channel: {}", channel);
println!(" Key ID: {}", channel_key.key_id);
println!(" Key: {}...", &channel_key.public_key[..48]);
if keyring.get_key(channel).is_some() {
println!("\n ⚠ A key for this channel already exists.");
if !prompt_yes_no("Replace existing key?")? {
println!("Key add cancelled by user");
return Ok(());
}
}
if !prompt_yes_no("\nTrust this key?")? {
println!("Key add cancelled by user");
return Ok(());
}
keyring.add_trusted(channel, &channel_key, keyring::TrustLevel::UserApproved)?;
println!("\n✓ Key added to keyring");
Ok(())
}
fn cmd_keyring_remove(channel: &str) -> Result<()> {
log::debug!("Executing cmd_keyring_remove for channel '{}'", channel);
let mut keyring = keyring::Keyring::load()?;
if keyring.get_key(channel).is_none() {
println!("No key found for channel '{}'", channel);
return Ok(());
}
if keyring.is_default(channel) {
println!("⚠ '{}' is a default key shipped with the OS.", channel);
println!(" Removing it may prevent updates from this channel.");
}
if !prompt_yes_no(&format!("Remove key for '{}'?", channel))? {
println!("Key removal cancelled by user");
return Ok(());
}
keyring.remove(channel)?;
println!("✓ Key removed");
Ok(())
}
fn cmd_keyring_discover() -> Result<()> {
log::debug!("Executing cmd_keyring_discover");
let config = config::Config::load()?;
let mut client = client::TufClient::new(&config)?;
let keyring = keyring::Keyring::load()?;
println!("Discovering channels from {}...\n", config.base_url);
client.refresh_metadata()?;
let channels = client.list_channels()?;
println!("Available Channels");
println!("═══════════════════════════════════════════════════════════");
if channels.is_empty() {
println!(" No channels found in repository.");
} else {
for channel in channels {
let status = match keyring.get_key(&channel.name) {
Some(k) => match k.trust_level {
keyring::TrustLevel::Default => "✓ trusted (default)",
keyring::TrustLevel::UserApproved => "✓ trusted (user)",
keyring::TrustLevel::Pending => "○ pending approval",
},
None => "○ not in keyring",
};
println!(
" {:<12} {} {}",
channel.name,
&channel.key_id[..16],
status
);
}
}
println!("\nAdd a key: citadel-fetch keyring add <channel>");
Ok(())
}
fn cmd_refresh() -> Result<()> {
log::debug!("Executing cmd_refresh");
let config = config::Config::load()?;
let mut client = client::TufClient::new(&config)?;
println!("Refreshing TUF metadata...");
client.refresh_metadata()?;
println!("✓ Metadata refreshed");
Ok(())
}
fn prompt_yes_no(prompt: &str) -> Result<bool> {
use std::io::{self, Write};
print!("{} (y/N): ", prompt);
io::stdout().flush()?;
let mut input = String::new();
io::stdin().read_line(&mut input)?;
let answer = input.trim().to_lowercase();
Ok(answer == "y" || answer == "yes")
}
fn install_update(component: &str, image_path: &std::path::Path) -> Result<()> {
// This integrates with libcitadel's image installation
// For now, just copy to the appropriate location
use std::process::Command;
let dest = match component {
"rootfs" => "/run/citadel/images/citadel-rootfs.img",
"kernel" => "/run/citadel/images/citadel-kernel.img",
"extra" => "/run/citadel/images/citadel-extra.img",
_ => return Err(anyhow::anyhow!("Unknown component: {}", component)),
};
// Use citadel-image to install
let status = Command::new("/usr/bin/citadel-image")
.args(["install", &image_path.to_string_lossy(), dest])
.status()?;
if !status.success() {
anyhow::bail!("Failed to install {} image", component);
}
Ok(())
}

View File

@@ -1,11 +1,10 @@
use clap::{command, ArgAction, Command};
use clap::{Arg, ArgMatches};
use hex;
use std::path::Path;
use std::process::exit;
use clap::{Arg,ArgMatches};
use clap::{command, ArgAction, Command};
use hex;
use libcitadel::public_key_for_channel;
use libcitadel::{util, ImageHeader, KeyPair, Partition, ResourceImage, Result};
use libcitadel::{Result, ResourceImage, Logger, LogLevel, Partition, KeyPair, ImageHeader, util};
pub fn main() {
let matches = command!()
@@ -104,7 +103,7 @@ pub fn main() {
fn info(arg_matches: &ArgMatches) -> Result<()> {
let img = load_image(arg_matches)?;
print!("{}", String::from_utf8(img.header().metainfo_bytes())?);
print!("{}",String::from_utf8(img.header().metainfo_bytes())?);
info_signature(&img)?;
Ok(())
}
@@ -114,22 +113,22 @@ fn info_signature(img: &ResourceImage) -> Result<()> {
println!("Signature: {}", hex::encode(&img.header().signature()));
} else {
println!("Signature: No Signature");
return Ok(());
}
let pubkey = public_key_for_channel(img.metainfo().channel())?;
if img.header().verify_signature(pubkey) {
println!("Signature is valid");
} else {
println!("Signature verify FAILED");
match img.header().public_key()? {
Some(pubkey) => {
if img.header().verify_signature(pubkey) {
println!("Signature is valid");
} else {
println!("Signature verify FAILED");
}
},
None => { println!("No public key found for channel '{}'", img.metainfo().channel()) },
}
Ok(())
Ok(())
}
fn metainfo(arg_matches: &ArgMatches) -> Result<()> {
let img = load_image(arg_matches)?;
print!("{}", String::from_utf8(img.header().metainfo_bytes())?);
print!("{}",String::from_utf8(img.header().metainfo_bytes())?);
Ok(())
}
@@ -168,8 +167,7 @@ fn verify_shasum(arg_matches: &ArgMatches) -> Result<()> {
}
fn load_image(arg_matches: &ArgMatches) -> Result<ResourceImage> {
let path = arg_matches
.get_one::<String>("path")
let path = arg_matches.get_one::<String>("path")
.expect("path argument missing");
if !Path::new(path).exists() {
@@ -185,7 +183,7 @@ fn load_image(arg_matches: &ArgMatches) -> Result<ResourceImage> {
fn install_rootfs(arg_matches: &ArgMatches) -> Result<()> {
if arg_matches.get_flag("choose") {
let _ = choose_install_partition(true)?;
return Ok(());
return Ok(())
}
let img = load_image(arg_matches)?;
@@ -224,18 +222,14 @@ fn sign_image(arg_matches: &ArgMatches) -> Result<()> {
}
fn install_image(arg_matches: &ArgMatches) -> Result<()> {
let source = arg_matches
.get_one::<String>("path")
let source = arg_matches.get_one::<String>("path")
.expect("path argument missing");
let img = load_image(arg_matches)?;
let _hdr = img.header();
let metainfo = img.metainfo();
// Use existing function from libcitadel
let pubkey = public_key_for_channel(metainfo.channel())?;
if !img.header().verify_signature(pubkey) {
bail!("Image signature verification failed");
}
// XXX verify signature?
if !(metainfo.image_type() == "kernel" || metainfo.image_type() == "extra") {
bail!("Cannot install image type {}", metainfo.image_type());
@@ -256,20 +250,13 @@ fn install_image(arg_matches: &ArgMatches) -> Result<()> {
if kernel_version.chars().any(|c| c == '/') {
bail!("Kernel version field has / char");
}
format!(
"citadel-kernel-{}-{}.img",
kernel_version,
metainfo.version()
)
format!("citadel-kernel-{}-{:03}.img", kernel_version, metainfo.version())
} else {
format!("citadel-extra-{}.img", metainfo.version())
format!("citadel-extra-{:03}.img", metainfo.version())
};
if !metainfo.channel().chars().all(|c| c.is_ascii_lowercase()) {
bail!(
"Refusing to build path from strange channel name {}",
metainfo.channel()
);
bail!("Refusing to build path from strange channel name {}", metainfo.channel());
}
let image_dir = Path::new("/storage/resources").join(metainfo.channel());
let image_dest = image_dir.join(filename);

View File

@@ -12,7 +12,6 @@ use libcitadel::RealmFS;
use libcitadel::Result;
use libcitadel::OsRelease;
use libcitadel::KeyRing;
use libcitadel::ResourceImage;
use libcitadel::terminal::Base16Scheme;
use libcitadel::UtsName;
@@ -480,29 +479,11 @@ impl Installer {
}
fn setup_storage_resources(&self) -> Result<()> {
// Get the channel from the extra image metadata rather than OsRelease
// because during installation in live mode, OsRelease reads from initramfs
// which doesn't have CITADEL_CHANNEL set, but the images have it in metadata
let extra_path = self.artifact_path(EXTRA_IMAGE_NAME);
let channel = if extra_path.exists() {
match ResourceImage::from_path(&extra_path) {
Ok(img) => img.metainfo().channel().to_string(),
Err(_) => {
// Fallback to OsRelease if we can't read the image
OsRelease::citadel_channel()
.ok_or_else(|| {
format_err!("Failed to determine channel from extra image or OsRelease")
})?
.to_string()
}
}
} else {
OsRelease::citadel_channel()
.ok_or_else(|| format_err!("Failed to determine channel from OsRelease"))?
.to_string()
let channel = match OsRelease::citadel_channel() {
Some(channel) => channel,
None => "dev",
};
let resources = self.storage().join("resources").join(&channel);
let resources = self.storage().join("resources").join(channel);
util::create_dir(&resources)?;
self.sparse_copy_artifact(EXTRA_IMAGE_NAME, &resources)?;

View File

@@ -1,18 +1,14 @@
#[macro_use]
extern crate libcitadel;
#[macro_use]
extern crate serde_derive;
#[macro_use]
extern crate lazy_static;
#[macro_use] extern crate libcitadel;
#[macro_use] extern crate serde_derive;
#[macro_use] extern crate lazy_static;
use libcitadel::RealmManager;
use std::env;
use std::path::Path;
use std::ffi::OsStr;
use std::iter;
use std::path::Path;
use libcitadel::RealmManager;
mod boot;
mod fetch;
mod image;
mod install;
mod install_backend;
@@ -26,7 +22,7 @@ fn main() {
Ok(path) => path,
Err(_e) => {
return;
}
},
};
let args = env::args().collect::<Vec<String>>();
@@ -40,11 +36,9 @@ fn main() {
} else if exe == Path::new("/usr/bin/citadel-image") {
image::main();
} else if exe == Path::new("/usr/bin/citadel-realmfs") {
realmfs::main(args);
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") {
@@ -64,9 +58,8 @@ fn dispatch_command(args: Vec<String>) {
"boot" => boot::main(rebuild_args("citadel-boot", args)),
"install" => install::main(rebuild_args("citadel-install", args)),
"image" => image::main(),
"realmfs" => realmfs::main(rebuild_args("citadel-realmfs", args)),
"realmfs" => realmfs::main(),
"update" => update::main(rebuild_args("citadel-update", args)),
"fetch" => fetch::main(),
"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

@@ -1,13 +1,13 @@
use std::fs::OpenOptions;
use std::fs::{self, File};
use std::io::{self, Write};
use std::path::PathBuf;
use std::fs::OpenOptions;
use std::fs::{self,File};
use std::io::{self,Write};
use libcitadel::{devkeys, keypair_for_channel_signing, util, ImageHeader, Result};
use libcitadel::{Result, ImageHeader, devkeys, util};
use super::config::BuildConfig;
use libcitadel::verity::Verity;
use std::path::Path;
use libcitadel::verity::Verity;
pub struct UpdateBuilder {
config: BuildConfig,
@@ -19,12 +19,15 @@ pub struct UpdateBuilder {
verity_root: Option<String>,
}
const BLOCK_SIZE: usize = 4096;
fn align(sz: usize, n: usize) -> usize {
(sz + (n - 1)) & !(n - 1)
}
impl UpdateBuilder {
pub fn new(config: BuildConfig) -> UpdateBuilder {
let image_data = config.workdir_path(UpdateBuilder::build_filename(&config));
UpdateBuilder {
@@ -35,29 +38,15 @@ impl UpdateBuilder {
}
fn target_filename(&self) -> String {
format!(
"citadel-{}-{}-{}.img",
self.config.img_name(),
self.config.channel(),
self.config.version()
)
format!("citadel-{}-{}-{:03}.img", self.config.img_name(), self.config.channel(), self.config.version())
}
fn build_filename(config: &BuildConfig) -> String {
format!(
"citadel-{}-{}-{}",
config.image_type(),
config.channel(),
config.version()
)
format!("citadel-{}-{}-{:03}", config.image_type(), config.channel(), config.version())
}
fn verity_filename(&self) -> String {
format!(
"verity-hash-{}-{}",
self.config.image_type(),
self.config.version()
)
format!("verity-hash-{}-{:03}", self.config.image_type(), self.config.version())
}
pub fn build(&mut self) -> Result<()> {
@@ -165,7 +154,7 @@ impl UpdateBuilder {
bail!("failed to compress {:?}: {}", self.image(), err);
}
// Rename back to original image_data filename
util::rename(util::append_to_path(self.image(), ".xz"), self.image())?;
util::rename(self.image().with_extension("xz"), self.image())?;
}
Ok(())
}
@@ -203,19 +192,6 @@ impl UpdateBuilder {
if self.config.channel() == "dev" {
let sig = devkeys().sign(&metainfo);
hdr.set_signature(sig.to_bytes());
} else {
let private_key_path_str = match self.config.private_key_path() {
Some(path) => path,
None => bail!("private-key-path not found in config for non-dev channel"),
};
let private_key_path = Path::new(private_key_path_str);
let sig = keypair_for_channel_signing(private_key_path).sign(&metainfo);
info!("Generated signature: {}", hex::encode(sig.to_bytes()));
let generated_signature_bytes = sig.to_bytes();
if generated_signature_bytes.iter().all(|&b| b == 0) {
bail!("Generated signature is all zeros. Signing failed!");
}
hdr.set_signature(&generated_signature_bytes);
}
Ok(hdr)
}
@@ -241,33 +217,12 @@ 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())?;
writeln!(
v,
"verity-salt = \"{}\"",
self.verity_salt.as_ref().unwrap()
)?;
writeln!(
v,
"verity-root = \"{}\"",
self.verity_root.as_ref().unwrap()
)?;
if let Some(path) = self.config.public_key_path() {
if let Ok(key) = fs::read_to_string(path) {
writeln!(v, "public-key = \"{}\"", key.trim())?;
}
}
if let Some(path) = self.config.certificate_path() {
if let Ok(cert) = fs::read_to_string(path) {
writeln!(v, "authorizing-signature = \"{}\"", cert.trim())?;
}
}
writeln!(v, "verity-salt = \"{}\"", self.verity_salt.as_ref().unwrap())?;
writeln!(v, "verity-root = \"{}\"", self.verity_root.as_ref().unwrap())?;
Ok(v)
}
}

View File

@@ -1,5 +1,7 @@
use std::path::{Path, PathBuf};
use toml;
use libcitadel::{Result, util};
#[derive(Deserialize)]
@@ -7,7 +9,7 @@ pub struct BuildConfig {
#[serde(rename = "image-type")]
image_type: String,
channel: String,
version: String,
version: usize,
timestamp: String,
source: String,
#[serde(default)]
@@ -20,15 +22,6 @@ pub struct BuildConfig {
#[serde(rename = "realmfs-name")]
realmfs_name: Option<String>,
#[serde(rename = "private-key-path")]
private_key_path: Option<String>,
#[serde(rename = "public-key-path")]
public_key_path: Option<String>,
#[serde(rename = "certificate-path")]
certificate_path: Option<String>,
#[serde(skip)]
basedir: PathBuf,
#[serde(skip)]
@@ -53,43 +46,6 @@ impl BuildConfig {
Some(ref version) => format!("{}-{}", &config.image_type, version),
None => config.image_type.to_owned(),
};
// Auto-detect private key path if not specified
if config.private_key_path.is_none() && config.channel != "dev" {
let default_key_path = format!("/usr/share/citadel/keys/{}_image.key", config.channel);
info!("No private-key-path specified, using default: {}", default_key_path);
config.private_key_path = Some(default_key_path);
}
// Auto-detect public key and certificate paths if not specified
if config.public_key_path.is_none() && config.channel != "dev" {
// First check relative to config file (for build machine)
let local_path = config.basedir.join(format!("{}_image.pub", config.channel));
if local_path.exists() {
config.public_key_path = Some(local_path.to_string_lossy().into_owned());
} else {
// Fallback to runtime path
let path = format!("/usr/share/citadel/keys/{}_image.pub", config.channel);
if Path::new(&path).exists() {
config.public_key_path = Some(path);
}
}
}
if config.certificate_path.is_none() && config.channel != "dev" {
// First check relative to config file (for build machine)
let local_path = config.basedir.join(format!("{}_image.cert", config.channel));
if local_path.exists() {
config.certificate_path = Some(local_path.to_string_lossy().into_owned());
} else {
// Fallback to runtime path
let path = format!("/usr/share/citadel/keys/{}_image.cert", config.channel);
if Path::new(&path).exists() {
config.certificate_path = Some(path);
}
}
}
Ok(config)
}
@@ -146,8 +102,8 @@ impl BuildConfig {
self.realmfs_name.as_ref().map(|s| s.as_str())
}
pub fn version(&self) -> &str {
&self.version
pub fn version(&self) -> usize {
self.version
}
pub fn channel(&self) -> &str {
@@ -161,16 +117,4 @@ impl BuildConfig {
pub fn compress(&self) -> bool {
self.compress
}
pub fn private_key_path(&self) -> Option<&str> {
self.private_key_path.as_ref().map(|s| s.as_str())
}
pub fn public_key_path(&self) -> Option<&str> {
self.public_key_path.as_ref().map(|s| s.as_str())
}
pub fn certificate_path(&self) -> Option<&str> {
self.certificate_path.as_ref().map(|s| s.as_str())
}
}

View File

@@ -6,7 +6,7 @@ use libcitadel::util::is_euid_root;
use libcitadel::ResizeSize;
use std::process::exit;
pub fn main(args: Vec<String>) {
pub fn main() {
Logger::set_log_level(LogLevel::Debug);
@@ -65,7 +65,11 @@ is the final absolute size of the image.")
.help("Path or name of RealmFS image to deactivate")
.required(true)))
.get_matches_from(args);
.arg(Arg::new("image")
.help("Name of or path to RealmFS image to display information about")
.required(true))
.get_matches();
let result = match matches.subcommand() {
Some(("resize", m)) => resize(m),

View File

@@ -93,7 +93,7 @@ fn create_tmp_copy(path: &Path) -> Result<PathBuf> {
Ok(path)
}
pub fn install_image(path: &Path, flags: u32) -> Result<()> {
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-{}.img", image.header().metainfo().version());
let filename = format!("citadel-extra-{:03}.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-{}-{}.img", kernel_version, version);
let filename = format!("citadel-kernel-{}-{:03}.img", kernel_version, version);
install_image_file(image, &filename)?;
let all_versions = all_boot_kernel_versions()?;

View File

@@ -20,12 +20,6 @@ walkdir = "2"
dbus = "0.6"
posix-acl = "1.0.0"
procfs = "0.12.0"
anyhow = "1.0"
clap = "4.5"
tempfile = "3.21"
semver = "1.0"
sha2 = "0.10"
ed25519-dalek = { version = "2.1", features = ["pkcs8"] }
[dependencies.inotify]
version = "0.8"

View File

@@ -382,33 +382,8 @@ impl ImageHeader {
self.set_signature(&zeros);
}
pub fn public_key(&self) -> Result<PublicKey> {
let metainfo = self.metainfo();
// 1. Try Hierarchical Verification if fields are present
if let (Some(pk_hex), Some(sig_hex)) = (metainfo.public_key(), metainfo.authorizing_signature()) {
let root_pubkey = match crate::root_image_public_key() {
Ok(rk) => rk,
Err(e) => {
warn!("Could not load Root Image Key for hierarchical verification: {}", e);
return public_key_for_channel(metainfo.channel());
}
};
let channel_pubkey = PublicKey::from_hex(pk_hex)?;
let sig_bytes = hex::decode(sig_hex).map_err(context!("Invalid authorizing-signature hex"))?;
let pk_bytes = hex::decode(pk_hex).map_err(context!("Invalid public-key hex"))?;
if root_pubkey.verify(&pk_bytes, &sig_bytes) {
info!("Channel key authorization SUCCESS...");
return Ok(channel_pubkey);
} else {
bail!("Security Critical: Channel key signature verification FAILED. Aborting.");
}
}
// 2. Fallback to standard channel-based lookup (key must be on disk)
public_key_for_channel(metainfo.channel())
pub fn public_key(&self) -> Result<Option<PublicKey>> {
public_key_for_channel(self.metainfo().channel())
}
pub fn verify_signature(&self, pubkey: PublicKey) -> bool {
@@ -478,7 +453,7 @@ pub struct MetaInfo {
realmfs_owner: Option<String>,
#[serde(default)]
version: String,
version: u32,
#[serde(default)]
timestamp: String,
@@ -494,12 +469,6 @@ pub struct MetaInfo {
#[serde(default, rename = "verity-root")]
verity_root: String,
#[serde(rename = "public-key")]
public_key: Option<String>,
#[serde(rename = "authorizing-signature")]
authorizing_signature: Option<String>,
}
impl MetaInfo {
@@ -539,8 +508,8 @@ impl MetaInfo {
Self::str_ref(&self.realmfs_owner)
}
pub fn version(&self) -> &str {
&self.version
pub fn version(&self) -> u32 {
self.version
}
pub fn timestamp(&self) -> &str {
@@ -566,13 +535,5 @@ impl MetaInfo {
pub fn verity_tag(&self) -> &str {
&self.verity_root()[..8]
}
pub fn public_key(&self) -> Option<&str> {
self.public_key.as_deref()
}
pub fn authorizing_signature(&self) -> Option<&str> {
self.authorizing_signature.as_deref()
}
}

View File

@@ -35,24 +35,9 @@ impl PublicKey {
}
pub fn verify(&self, data: &[u8], signature: &[u8]) -> bool {
let sig = match sign::Signature::try_from(signature) {
Ok(s) => s,
Err(_) => {
warn!("Invalid signature length: {}", signature.len());
return false;
}
};
let is_valid = sign::verify_detached(&sig, data, &self.0);
if !is_valid {
warn!("Header signature verification FAILED!");
warn!(" Public Key: {}", self.to_hex());
warn!(" Data (header): {}", hex::encode(data));
warn!(" Signature: {}", hex::encode(signature));
} else {
info!("Header signature verification SUCCESS.");
}
is_valid
let sig = sign::Signature::try_from(signature)
.expect("Signature::from_slice() failed");
sign::verify_detached(&sig, data, &self.0)
}
}

View File

@@ -2,9 +2,6 @@
#[macro_use] extern crate serde_derive;
#[macro_use] extern crate lazy_static;
use std::fs;
use std::path::Path;
#[macro_use] pub mod error;
#[macro_use] mod log;
#[macro_use] mod exec;
@@ -23,7 +20,6 @@ pub mod symlink;
mod realm;
pub mod terminal;
mod system;
pub mod updates;
pub mod flatpak;
@@ -58,51 +54,28 @@ pub fn devkeys() -> KeyPair {
.expect("Error parsing built in dev channel keys")
}
pub fn keypair_for_channel_signing(private_key_path: &Path) -> KeyPair {
let hex_key = fs::read_to_string(private_key_path)
.expect(&format!("Error reading secret key from {}", private_key_path.display()));
KeyPair::from_hex(hex_key.trim())
.expect(&format!("Error parsing secret key from {}", private_key_path.display()))
}
pub fn public_key_for_channel(channel: &str) -> Result<PublicKey> {
// Validate input first
if !channel.chars().all(|c| c.is_ascii_alphanumeric() || c == '-') {
bail!("Invalid channel name: {}", channel);
pub fn public_key_for_channel(channel: &str) -> Result<Option<PublicKey>> {
if channel == "dev" {
return Ok(Some(devkeys().public_key()));
}
// Kernel command line override for developers
// Look in /etc/os-release
if Some(channel) == OsRelease::citadel_channel() {
if let Some(hex) = OsRelease::citadel_image_pubkey() {
let pubkey = PublicKey::from_hex(hex)?;
return Ok(Some(pubkey));
}
}
// Does kernel command line have citadel.channel=name:[hex encoded pubkey]
if Some(channel) == CommandLine::channel_name() {
if let Some(hex) = CommandLine::channel_pubkey() {
let pubkey = PublicKey::from_hex(hex)?;
return Ok(pubkey);
return Ok(Some(pubkey))
}
}
let key_path = Path::new("/usr/share/citadel/keys/").join(format!("{}_image.pub", channel));
if !key_path.exists() {
if channel == "dev" {
return Ok(devkeys().public_key());
}
bail!("Public key not found for channel '{}' at {}", channel, key_path.display());
}
let hex_key = fs::read_to_string(&key_path)
.map_err(context!("could not read public key from {}", key_path.display()))?;
let pubkey = PublicKey::from_hex(hex_key.trim())?;
Ok(pubkey)
}
pub fn root_image_public_key() -> Result<PublicKey> {
let key_path = Path::new("/usr/share/citadel/keys/root_image.pub");
if !key_path.exists() {
bail!("Root image public key not found at {}", key_path.display());
}
let hex_key = fs::read_to_string(&key_path)
.map_err(context!("could not read root public key from {}", key_path.display()))?;
PublicKey::from_hex(hex_key.trim())
Ok(None)
}
pub use error::{Result,Error};

View File

@@ -15,7 +15,8 @@ pub struct Partition {
#[derive(Clone)]
struct HeaderInfo {
header: Arc<ImageHeader>,
pubkey: PublicKey,
// None if no public key available for channel named in metainfo
pubkey: Option<PublicKey>,
}
impl Partition {
@@ -42,7 +43,13 @@ impl Partition {
}
let metainfo = header.metainfo();
let pubkey = public_key_for_channel(metainfo.channel())?;
let pubkey = match public_key_for_channel(metainfo.channel()) {
Ok(result) => result,
Err(err) => {
warn!("Error parsing pubkey for channel '{}': {}", metainfo.channel(), err);
None
}
};
let header = Arc::new(header);
Ok(Some(HeaderInfo {
@@ -97,15 +104,21 @@ impl Partition {
pub fn is_signature_valid(&self) -> bool {
if let Some(ref hinfo) = self.hinfo {
return hinfo.pubkey.verify(
&self.header().metainfo_bytes(),
&self.header().signature())
if let Some(ref pubkey) = hinfo.pubkey {
return pubkey.verify(
&self.header().metainfo_bytes(),
&self.header().signature())
}
}
false
}
pub fn has_public_key(&self) -> bool {
self.hinfo.is_some()
if let Some(ref h) = self.hinfo {
h.pubkey.is_some()
} else {
false
}
}
pub fn write_status(&mut self, status: u8) -> Result<()> {

View File

@@ -1,6 +1,7 @@
use std::ffi::OsStr;
use std::fs;
use std::io::Write;
use std::os::unix::fs::MetadataExt;
use std::path::{Path,PathBuf};
use std::sync::{Arc, Weak, RwLock};
@@ -56,8 +57,6 @@ impl RealmFS {
// Name used to retrieve key by 'description' from kernel key storage
pub const USER_KEYNAME: &'static str = "realmfs-user";
const BLOCK_SIZE: u64 = 4096;
/// Locate a RealmFS image by name in the default location using the standard name convention
pub fn load_by_name(name: &str) -> Result<Self> {
Self::validate_name(name)?;
@@ -282,7 +281,10 @@ impl RealmFS {
let pubkey = if self.metainfo().channel() == RealmFS::USER_KEYNAME {
self.sealing_keys()?.public_key()
} else {
self.header().public_key()?
match self.header().public_key()? {
Some(pubkey) => pubkey,
None => bail!("No public key available for channel {}", self.metainfo().channel()),
}
};
Ok(pubkey)
}
@@ -311,7 +313,7 @@ impl RealmFS {
info!("forking RealmFS image '{}' to new name '{}'", self.name(), new_name);
let mut forked = match self.fork_to_path(new_name, &new_path, keys) {
let forked = match self.fork_to_path(new_name, &new_path, keys) {
Ok(forked) => forked,
Err(err) => {
if new_path.exists() {
@@ -321,10 +323,7 @@ impl RealmFS {
}
};
self.with_manager(|m| {
m.realmfs_added(&forked);
forked.set_manager(m);
});
self.with_manager(|m| m.realmfs_added(&forked));
Ok(forked)
}
@@ -369,11 +368,11 @@ impl RealmFS {
pub fn file_nblocks(&self) -> Result<usize> {
let meta = self.path.metadata()
.map_err(context!("failed to read metadata from realmfs image file {:?}", self.path))?;
let len = meta.len();
if len % Self::BLOCK_SIZE != 0 {
let len = meta.len() as usize;
if len % 4096 != 0 {
bail!("realmfs image file '{}' has size which is not a multiple of block size", self.path.display());
}
let nblocks = (len / Self::BLOCK_SIZE) as usize;
let nblocks = len / 4096;
if nblocks < (self.metainfo().nblocks() + 1) {
bail!("realmfs image file '{}' has shorter length than nblocks field of image header", self.path.display());
}
@@ -407,20 +406,18 @@ impl RealmFS {
}
pub fn free_size_blocks(&self) -> Result<usize> {
let sb = Superblock::load(self.path(), Self::BLOCK_SIZE)?;
let sb = Superblock::load(self.path(), 4096)?;
Ok(sb.free_block_count() as usize)
}
pub fn allocated_size_blocks(&self) -> usize {
self.metainfo().nblocks()
pub fn allocated_size_blocks(&self) -> Result<usize> {
let meta = self.path().metadata()
.map_err(context!("failed to read metadata from realmfs image file {:?}", self.path()))?;
Ok(meta.blocks() as usize / 8)
}
/// Activate this RealmFS image if not yet activated.
pub fn activate(&self) -> Result<()> {
// Ensure that mountpoint matches header information of image
if let Err(err) = self.check_stale_header(false) {
warn!("error reloading stale image header: {}", err);
}
self.mountpoint().activate(self)
}

View File

@@ -27,6 +27,17 @@ impl RealmFSSet {
}
Ok(())
})?;
/*
let entries = fs::read_dir(RealmFS::BASE_PATH)
.map_err(context!("error reading realmfs directory {}", RealmFS::BASE_PATH))?;
for entry in entries {
let entry = entry.map_err(context!("error reading directory entry"))?;
if let Some(realmfs) = Self::entry_to_realmfs(&entry) {
v.push(realmfs)
}
}
*/
Ok(v)
}
@@ -36,8 +47,6 @@ impl RealmFSSet {
let name = filename.trim_end_matches("-realmfs.img");
if RealmFS::is_valid_name(name) && RealmFS::named_image_exists(name) {
return RealmFS::load_by_name(name).ok();
} else {
warn!("Rejecting realmfs '{}' as invalid name or invalid image", name);
}
}
}

View File

@@ -108,10 +108,7 @@ impl RealmFSUpdate {
LoopDevice::with_loop(self.target(), Some(BLOCK_SIZE), false, |loopdev| {
self.resize_device(loopdev)
})?;
self.apply_update()?;
self.cleanup();
Ok(())
})
}
fn mount_update_image(&mut self) -> Result<()> {
@@ -217,29 +214,22 @@ impl RealmFSUpdate {
self.set_target_len(nblocks)
}
fn remount_read_only(&mut self) {
if self.mountpath.exists() {
if let Err(err) = cmd!("/usr/bin/mount", "-o remount,ro {}", self.mountpath.display()) {
warn!("Failed to remount read-only directory {}: {}", self.mountpath.display(), err);
} else {
info!("Directory {} remounted as read-only", self.mountpath.display());
}
}
}
fn shutdown_container(&mut self) {
fn shutdown_container(&mut self) -> Result<()> {
if let Some(update) = self.container.take() {
if let Err(err) = update.stop_container() {
warn!("Error shutting down update container: {}", err);
}
update.stop_container()?;
}
Ok(())
}
pub fn cleanup(&mut self) {
// if a container was started, stop it
self.shutdown_container();
if let Err(err) = self.shutdown_container() {
warn!("Error shutting down update container: {}", err);
}
self.unmount_update_image();
if self.mountpath.exists() {
self.unmount_update_image();
}
if self.target().exists() {
if let Err(err) = fs::remove_file(self.target()) {
@@ -314,10 +304,6 @@ impl RealmFSUpdate {
}
pub fn commit_update(&mut self) -> Result<()> {
// First shutdown container so writable mount can be removed in apply_update()
self.shutdown_container();
// Ensure no further writes
self.remount_read_only();
let result = self.apply_update();
self.cleanup();
result

View File

@@ -42,21 +42,7 @@ impl ResourceImage {
info!("Searching run directory for image {} with channel {}", image_type, channel);
let run_channel = if CommandLine::live_mode() || CommandLine::install_mode() {
info!("Live/Install mode: searching {} without channel filter", RUN_DIRECTORY);
None
} else {
info!("Normal mode: searching {} with channel filter: {}", RUN_DIRECTORY, channel);
Some(channel)
};
// In live/install mode, skip strict kernel version/id matching since we're
// using whatever kernel image was provided on the boot media
let skip_kernel_match = CommandLine::live_mode() || CommandLine::install_mode();
info!("Searching in {}", RUN_DIRECTORY);
if let Some(image) = search_directory(RUN_DIRECTORY, image_type, run_channel, skip_kernel_match)? {
info!("Found image in {}: {}", RUN_DIRECTORY, image.path().display());
if let Some(image) = search_directory(RUN_DIRECTORY, image_type, Some(&channel))? {
return Ok(image);
}
@@ -66,9 +52,8 @@ impl ResourceImage {
let storage_path = Path::new(STORAGE_BASEDIR).join(&channel);
if let Some(image) = search_directory(&storage_path, image_type, Some(&channel), false)? {
info!("Found image in storage: {}", image.path().display());
return Ok(image);
if let Some(image) = search_directory(storage_path, image_type, Some(&channel))? {
return Ok(image);
}
bail!("failed to find resource image of type: {}", image_type)
@@ -81,7 +66,7 @@ impl ResourceImage {
/// Locate a rootfs image in /run/citadel/images and return it
pub fn find_rootfs() -> Result<Self> {
match search_directory(RUN_DIRECTORY, "rootfs", None, false)? {
match search_directory(RUN_DIRECTORY, "rootfs", None)? {
Some(image) => Ok(image),
None => bail!("failed to find rootfs resource image"),
}
@@ -214,11 +199,15 @@ impl ResourceImage {
pub fn setup_verity_device(&self) -> Result<String> {
if !CommandLine::nosignatures() {
let pubkey = self.header.public_key()?;
if !self.header.verify_signature(pubkey) {
bail!("header signature verification failed");
match self.header.public_key()? {
Some(pubkey) => {
if !self.header.verify_signature(pubkey) {
bail!("header signature verification failed");
}
info!("Image header signature is valid");
}
None => bail!("cannot verify header signature because no public key for channel {} is available", self.metainfo().channel())
}
info!("Image header signature is valid");
}
info!("Setting up dm-verity device for image");
if !self.has_verity_hashtree() {
@@ -361,13 +350,6 @@ impl ResourceImage {
// If the /storage directory is not mounted, attempt to mount it.
// Return true if already mounted or if the attempt to mount it succeeds.
pub fn ensure_storage_mounted() -> Result<bool> {
// In live/install mode, storage is a tmpfs and /dev/mapper/citadel-storage
// doesn't exist. All resources should be in /run/citadel/images.
if CommandLine::live_mode() || CommandLine::install_mode() {
info!("Live/Install mode: skipping storage mount (using tmpfs)");
return Ok(false);
}
if Mounts::is_source_mounted("/dev/mapper/citadel-storage")? {
return Ok(true);
}
@@ -389,30 +371,9 @@ impl ResourceImage {
}
fn rootfs_channel() -> &'static str {
let cmdline_channel = CommandLine::channel_name();
info!("CommandLine::channel_name() = {:?}", cmdline_channel);
match cmdline_channel {
Some(channel) => {
info!("Using channel from kernel command line: {}", channel);
channel
},
None => {
let osrelease_channel = OsRelease::citadel_channel();
info!("OsRelease::citadel_channel() = {:?}", osrelease_channel);
match osrelease_channel {
Some(channel) => {
info!("Using channel from OsRelease: {}", channel);
channel
},
None => {
info!("No channel found, defaulting to 'dev'");
"dev"
}
}
},
match CommandLine::channel_name() {
Some(channel) => channel,
None => "dev",
}
}
}
@@ -422,24 +383,15 @@ impl ResourceImage {
// in the image header metainfo. If multiple matches are found, return the image
// with the highest version number. If multiple images have the same highest version
// number, return the image with the newest file creation time.
// If skip_kernel_match is true, kernel version/id matching is skipped for kernel images.
fn search_directory<P: AsRef<Path>>(dir: P, image_type: &str, channel: Option<&str>, skip_kernel_match: bool) -> Result<Option<ResourceImage>> {
info!("search_directory: dir={}, image_type={}, channel={:?}, skip_kernel_match={}",
dir.as_ref().display(), image_type, channel, skip_kernel_match);
fn search_directory<P: AsRef<Path>>(dir: P, image_type: &str, channel: Option<&str>) -> Result<Option<ResourceImage>> {
if !dir.as_ref().exists() {
return Ok(None)
}
let mut best = None;
let mut matches = all_matching_images(dir.as_ref(), image_type, channel, skip_kernel_match)?;
info!("Found {} matching images in {}", matches.len(), dir.as_ref().display());
for (i, img) in matches.iter().enumerate() {
info!(" Match {}: {} (channel: {}, version: {})",
i, img.path().display(), img.metainfo().channel(), img.metainfo().version());
}
let mut matches = all_matching_images(dir.as_ref(), image_type, channel)?;
debug!("Found {} matching images", matches.len());
if channel.is_none() {
if matches.is_empty() {
@@ -468,10 +420,8 @@ fn compare_images(a: Option<ResourceImage>, b: ResourceImage) -> Result<Resource
None => return Ok(b),
};
let bind_a = a.metainfo();
let bind_b = b.metainfo();
let ver_a = bind_a.version();
let ver_b = bind_b.version();
let ver_a = a.metainfo().version();
let ver_b = b.metainfo().version();
if ver_a > ver_b {
Ok(a)
@@ -505,21 +455,17 @@ fn current_kernel_version() -> String {
//
// Read a directory search for ResourceImages which match the channel
// and image_type. If skip_kernel_match is true, skip kernel version/id matching.
// and image_type.
//
fn all_matching_images(dir: &Path, image_type: &str, channel: Option<&str>, skip_kernel_match: bool) -> Result<Vec<ResourceImage>> {
fn all_matching_images(dir: &Path, image_type: &str, channel: Option<&str>) -> Result<Vec<ResourceImage>> {
let kernel_version = current_kernel_version();
let kv = if image_type == "kernel" && !skip_kernel_match {
let kv = if image_type == "kernel" {
Some(kernel_version.as_str())
} else {
None
};
let kernel_id = if !skip_kernel_match {
OsRelease::citadel_kernel_id()
} else {
None
};
let kernel_id = OsRelease::citadel_kernel_id();
let mut v = Vec::new();
util::read_directory(dir, |dent| {
@@ -531,9 +477,8 @@ fn all_matching_images(dir: &Path, image_type: &str, channel: Option<&str>, skip
// Examine a directory entry to determine if it is a resource image which
// matches a given channel and image_type. If the image_type is "kernel"
// then also match the kernel-version and kernel-id fields (unless those
// parameters are None, in which case skip version/id checking).
// If channel is None then don't consider the channel in the match.
// then also match the kernel-version and kernel-id fields. If channel
// is None then don't consider the channel in the match.
//
// If the entry is a match, then instantiate a ResourceImage and add it to
// the images vector.
@@ -545,12 +490,9 @@ fn maybe_add_dir_entry(entry: &DirEntry,
images: &mut Vec<ResourceImage>) -> Result<()> {
let path = entry.path();
info!("Examining directory entry: {}", path.display());
let meta = entry.metadata()
.map_err(context!("failed to read metadata for {:?}", entry.path()))?;
if !meta.is_file() {
info!(" Skipping: not a regular file");
if !meta.is_file() || meta.len() < ImageHeader::HEADER_SIZE as u64 {
return Ok(())
}
let header = match ImageHeader::from_file(&path) {
@@ -562,38 +504,25 @@ fn maybe_add_dir_entry(entry: &DirEntry,
};
if !header.is_magic_valid() {
info!(" Skipping: invalid magic header");
return Ok(())
}
let metainfo = header.metainfo();
info!("Found an image type={} channel={} kernel={:?}", metainfo.image_type(), metainfo.channel(), metainfo.kernel_version());
debug!("Found an image type={} channel={} kernel={:?}", metainfo.image_type(), metainfo.channel(), metainfo.kernel_version());
if let Some(channel) = channel {
if metainfo.channel() != channel {
info!(" Skipping: channel mismatch (want {}, got {})", channel, metainfo.channel());
return Ok(());
}
}
if image_type != metainfo.image_type() {
info!(" Skipping: image_type mismatch (want {}, got {})", image_type, metainfo.image_type());
return Ok(())
}
// Only check kernel version/id if they are specified (Some)
// kernel_version must match if specified
// kernel_id must match only if specified (if None, any kernel_id is acceptable)
if image_type == "kernel" && kernel_version.is_some() {
let version_matches = metainfo.kernel_version() == kernel_version;
let id_matches = kernel_id.is_none() || metainfo.kernel_id() == kernel_id;
if !version_matches || !id_matches {
info!(" Skipping: kernel version/id mismatch (want version={:?}, id={:?}; got version={:?}, id={:?})",
kernel_version, kernel_id, metainfo.kernel_version(), metainfo.kernel_id());
return Ok(());
}
if image_type == "kernel" && (metainfo.kernel_version() != kernel_version || metainfo.kernel_id() != kernel_id) {
return Ok(());
}
images.push(ResourceImage::new(&path, header));

View File

@@ -1,183 +0,0 @@
use anyhow::Context;
use std::fmt;
use std::io::Write;
use std::slice::Iter;
pub const UPDATE_SERVER_HOSTNAME: &str = "update.subgraph.com";
const CITADEL_CONFIG_PATH: &str = "/storage/citadel-state/citadel.conf";
/// This struct embeds the CitadelVersion datastruct as well as the cryptographic validation of 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, stable ...
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,
pub sha256_hash: 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"),
}
}
}
/// Reads a specific key from an os-release formatted file.
/// The value is returned without any surrounding quotes.
pub fn get_citadel_conf(key: &str) -> anyhow::Result<Option<String>> {
// Read the entire file into a string.
let content = std::fs::read_to_string(CITADEL_CONFIG_PATH)
.context(format!("Failed to read {}", CITADEL_CONFIG_PATH))?;
// Search each line for the key.
for line in content.lines() {
// Check if the line starts with "KEY="
if let Some(value) = line.trim().strip_prefix(&format!("{}=", key)) {
// If found, trim whitespace and quotes from the value and return.
let value = value.trim().trim_matches('"').to_string();
return Ok(Some(value));
}
}
// If the loop finishes without finding the key, return None.
Ok(None)
}
pub fn get_os_release(key: &str) -> anyhow::Result<Option<String>> {
// Read the entire file into a string.
let content = std::fs::read_to_string("/etc/os-release")
.context(format!("Failed to read {}", "/etc/os-release"))?;
// Search each line for the key.
for line in content.lines() {
// Check if the line starts with "KEY="
if let Some(value) = line.trim().strip_prefix(&format!("{}=", key)) {
// If found, trim whitespace and quotes from the value and return.
let value = value.trim().trim_matches('"').to_string();
return Ok(Some(value));
}
}
// If the loop finishes without finding the key, return None.
Ok(None)
}
/// Safely modifies a key-value pair in the citadel config os-release-formated file.
/// If the key does not exist, it will be added to the end of the file.
pub fn set_citadel_conf(key: &str, value: &str) -> anyhow::Result<()> {
// Read the existing os-release file.
let content = std::fs::read_to_string(CITADEL_CONFIG_PATH)
.context(format!("Failed to read {}", CITADEL_CONFIG_PATH))?;
let mut lines: Vec<String> = Vec::new();
let mut key_updated = false;
let key_prefix = format!("{}=", key);
let new_line = format!("{}{}", key_prefix, value);
// Process each line to update the key if it exists.
for line in content.lines() {
if line.starts_with(&key_prefix) {
lines.push(new_line.clone());
key_updated = true;
} else {
lines.push(line.to_string());
}
}
// If the key was not found, add it to the end.
if !key_updated {
lines.push(new_line);
}
// Write the changes back safely using a temporary file and an atomic rename.
let mut temp_file = tempfile::Builder::new()
.prefix("citadel.conf")
.suffix(".tmp")
.tempfile_in(std::path::Path::new(CITADEL_CONFIG_PATH).parent().unwrap())?;
temp_file.write_all(lines.join("\n").as_bytes())?;
temp_file.write_all(b"\n")?; // Ensure the file ends with a newline.
temp_file.persist(CITADEL_CONFIG_PATH).context(format!(
"Failed to overwrite {}. Are you running as root?",
CITADEL_CONFIG_PATH
))?;
Ok(())
}

View File

@@ -48,12 +48,6 @@ 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() {
@@ -410,4 +404,4 @@ pub fn drop_privileges(uid: u32, gid: u32) -> Result<()> {
}
}
Ok(())
}
}

View File

@@ -7,12 +7,13 @@ use serde_repr::Serialize_repr;
use zbus::blocking::fdo::DBusProxy;
use zbus::blocking::Connection;
use zbus::names::BusName;
use zbus::zvariant::{ObjectPath, Type};
use zbus::zvariant::Type;
use zbus::{fdo, interface};
use libcitadel::{PidLookupResult, Realm, RealmManager};
use libcitadel::terminal::Base16Scheme;
use libcitadel::{PidLookupResult, RealmManager};
use crate::next::config::RealmConfigVars;
use crate::next::state::RealmsManagerState;
use crate::next::realm::RealmItemState;
use super::realmfs::RealmFSState;
pub fn failed<T>(message: String) -> fdo::Result<T> {
Err(fdo::Error::Failed(message))
@@ -42,10 +43,10 @@ impl From<PidLookupResult> for RealmFromCitadelPid {
}
}
#[derive(Clone)]
pub struct RealmsManagerServer2 {
state: RealmsManagerState,
realms: RealmItemState,
realmfs_state: RealmFSState,
manager: Arc<RealmManager>,
quit_event: Arc<Event>,
}
@@ -54,9 +55,11 @@ pub struct RealmsManagerServer2 {
impl RealmsManagerServer2 {
fn new(connection: Connection, manager: Arc<RealmManager>, quit_event: Arc<Event>) -> Self {
let state = RealmsManagerState::new(connection.clone());
let realms = RealmItemState::new(connection.clone());
let realmfs_state = RealmFSState::new(connection.clone());
RealmsManagerServer2 {
state,
realms,
realmfs_state,
manager,
quit_event,
}
@@ -70,12 +73,13 @@ impl RealmsManagerServer2 {
let args = sig.args()?;
match &args.name {
BusName::Unique(unique_name) if args.new_owner().is_none() => {
self.state.client_disconnected(unique_name);
self.realmfs_state.client_disconnected(unique_name);
},
_ => {},
}
}
Ok(())
}
fn listen_name_owner_changed(&self, connection: &Connection) {
@@ -90,28 +94,12 @@ impl RealmsManagerServer2 {
pub fn load(connection: &Connection, manager: Arc<RealmManager>, quit_event: Arc<Event>) -> zbus::Result<Self> {
let server = Self::new(connection.clone(), manager.clone(), quit_event);
server.state.load(&manager)?;
server.realms.load_realms(&manager)?;
server.realmfs_state.load(&manager)?;
server.realms.populate_realmfs(&server.realmfs_state)?;
server.listen_name_owner_changed(connection);
Ok(server)
}
fn setup_new_realm(manager: &RealmManager, realm: Realm, realmfs_name: &str) {
if let Some(realmfs) = manager.realmfs_by_name(&realmfs_name) {
realm.with_mut_config(|c| c.realmfs = Some(realmfs.name().to_string()));
} else {
warn!("Cannot set RealmFS '{}' on realm because it does not exist", realmfs_name);
}
let config = realm.config();
if let Err(err) = config.write() {
warn!("error writing config file for new realm: {}", err);
}
let scheme_name = config.terminal_scheme().unwrap_or("default-dark");
if let Some(scheme) = Base16Scheme::by_name(scheme_name) {
if let Err(e) = scheme.apply_to_realm(&manager, &realm) {
warn!("error writing scheme files: {}", e);
}
}
}
}
@@ -120,7 +108,7 @@ impl RealmsManagerServer2 {
async fn get_current(&self) -> u32 {
self.state.get_current()
self.realms.get_current()
.map(|r| r.index())
.unwrap_or(0)
}
@@ -132,13 +120,11 @@ impl RealmsManagerServer2 {
}).await
}
async fn create_realm(&self, name: &str, realmfs: &str) -> fdo::Result<()> {
async fn create_realm(&self, name: &str) -> fdo::Result<()> {
let manager = self.manager.clone();
let name = name.to_string();
let realmfs_name = realmfs.to_string();
unblock(move || {
let realm = manager.new_realm(&name).map_err(|err| fdo::Error::Failed(err.to_string()))?;
RealmsManagerServer2::setup_new_realm(&manager, realm, &realmfs_name);
let _ = manager.new_realm(&name).map_err(|err| fdo::Error::Failed(err.to_string()))?;
Ok(())
}).await
}
@@ -157,16 +143,8 @@ impl RealmsManagerServer2 {
}).await
}
async fn fork_realmfs(&self, name: &str, new_name: &str) -> fdo::Result<ObjectPath<'_>> {
let state = self.state.clone();
let name = name.to_string();
let new_name = new_name.to_string();
unblock(move || {
state.fork_realmfs(&name, &new_name)
}).await
}
async fn get_global_config(&self) -> RealmConfigVars {
RealmConfigVars::new_global()
}
}

View File

@@ -4,7 +4,5 @@ mod config;
mod realm;
mod realmfs;
mod state;
pub use manager::RealmsManagerServer2;
pub const REALMS2_SERVER_OBJECT_PATH: &str = "/com/subgraph/Realms2";

View File

@@ -1,17 +1,18 @@
use std::collections::HashMap;
use crate::next::config::{RealmConfig, RealmConfigVars};
use crate::next::REALMS2_SERVER_OBJECT_PATH;
use blocking::unblock;
use libcitadel::Realm;
use std::convert::TryInto;
use std::os::unix::process::CommandExt;
use std::process::Command;
use std::sync::{Arc, Mutex, MutexGuard};
use std::sync::atomic::{AtomicBool, AtomicI64, AtomicU32, Ordering};
use std::sync::Arc;
use async_io::block_on;
use zbus::{fdo, interface, Connection};
use blocking::unblock;
use zbus::zvariant::{OwnedObjectPath, Value};
use zbus::{interface, fdo};
use zbus::blocking::Connection;
use zbus::names::{BusName, InterfaceName};
use zbus::zvariant::Value;
use crate::next::state::RealmFSNameToId;
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 {
@@ -21,13 +22,12 @@ pub struct RealmItem {
config: RealmConfig,
in_run_transition: Arc<AtomicBool>,
realmfs_index: Arc<AtomicU32>,
realmfs_name_to_id: RealmFSNameToId,
last_timestamp: Arc<AtomicI64>,
}
#[derive(Copy,Clone)]
#[repr(u32)]
pub enum RealmRunStatus {
enum RealmRunStatus {
Stopped = 0,
Starting,
Running,
@@ -48,14 +48,13 @@ impl RealmRunStatus {
}
impl RealmItem {
pub(crate) fn new_from_realm(index: u32, realm: Realm, realmfs_name_to_id: RealmFSNameToId) -> 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 = realmfs_name_to_id.lookup(realm.config().realmfs());
let realmfs_index = Arc::new(AtomicU32::new(realmfs_index));
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_name_to_id, realmfs_index, last_timestamp }
RealmItem { path, index, realm, config, in_run_transition, realmfs_index, last_timestamp }
}
pub fn path(&self) -> &str {
@@ -70,26 +69,10 @@ impl RealmItem {
self.in_run_transition.load(Ordering::Relaxed)
}
pub fn last_timestamp(&self) -> i64 {
self.last_timestamp.load(Ordering::Relaxed)
}
pub fn set_last_timestamp(&self, ts: i64) {
self.last_timestamp.store(ts, Ordering::Relaxed);
}
pub fn get_run_status(&self) -> RealmRunStatus {
fn get_run_status(&self) -> RealmRunStatus {
RealmRunStatus::for_realm(&self.realm, self.in_run_transition())
}
pub fn realm(&self) -> &Realm {
&self.realm
}
pub fn set_in_run_transition(&self, in_run_transition: bool) {
self.in_run_transition.store(in_run_transition, Ordering::Relaxed);
}
async fn do_start(&mut self) -> fdo::Result<()> {
if !self.realm.is_active() {
let realm = self.realm.clone();
@@ -115,20 +98,6 @@ impl RealmItem {
}
Ok(())
}
pub fn emit_property_changed(&self, connection: &Connection, propname: &str, value: Value<'_>) -> fdo::Result<()> {
let iface_name = InterfaceName::from_str_unchecked("com.subgraph.realms.Realm");
let changed = HashMap::from([(propname.to_string(), value)]);
let inval: &[&str] = &[];
block_on(
connection.emit_signal(
None::<BusName<'_>>,
self.path(),
"org.freedesktop.DBus.Properties",
"PropertiesChanged",
&(iface_name, changed, inval)))?;
Ok(())
}
}
#[interface(
@@ -172,20 +141,9 @@ impl RealmItem {
self.config.config_vars()
}
async fn set_config(&mut self,
#[zbus(connection)]
connection: &Connection,
vars: Vec<(String, String)>) -> fdo::Result<()> {
async fn set_config(&mut self, vars: Vec<(String, String)>) -> fdo::Result<()> {
for (var, val) in &vars {
self.config.set_var(var, val)?;
if var == "realmfs" {
let index = self.realmfs_name_to_id.lookup(val);
if index != self.realmfs_index.load(Ordering::Relaxed) {
self.realmfs_index.store(index, Ordering::Relaxed);
self.emit_property_changed(connection, "RealmFS", Value::U32(index))?;
}
}
}
let config = self.config.clone();
@@ -245,4 +203,193 @@ impl RealmItem {
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,13 +1,13 @@
use crate::next::REALMS2_SERVER_OBJECT_PATH;
use libcitadel::{RealmFS, RealmFSUpdate, ResizeSize};
use std::collections::HashMap;
use std::convert::TryInto;
use std::sync::{Arc, Mutex, MutexGuard};
use blocking::unblock;
use zbus::blocking::Connection;
use zbus::message::Header;
use zbus::names::UniqueName;
use zbus::zvariant::{ObjectPath, OwnedObjectPath};
use zbus::{fdo, interface};
use zbus::object_server::SignalEmitter;
use libcitadel::{RealmFS, RealmManager,RealmFSUpdate};
use crate::next::REALMS2_SERVER_OBJECT_PATH;
struct UpdateState(Option<(UniqueName<'static>, RealmFSUpdate)>);
@@ -40,8 +40,12 @@ impl UpdateState {
}
}
fn take_update(&mut self) -> Option<(RealmFSUpdate)> {
self.0.take().map(|(_,update)| update)
fn commit_update(&mut self) {
if let Some((_name, mut update)) = self.0.take() {
if let Err(err) = update.commit_update() {
warn!("Error committing RealmFS update: {}", err);
}
}
}
}
@@ -56,12 +60,13 @@ pub struct RealmFSItem {
impl RealmFSItem {
fn update_state(&self) -> MutexGuard<'_, UpdateState> {
fn update_state(&self) -> MutexGuard<UpdateState> {
self.update_state.lock().unwrap()
}
pub fn client_disconnected(&mut self, name: &UniqueName) {
fn client_disconnected(&mut self, name: &UniqueName) {
//debug!("disconnect {} {}", self.object_path, name);
let mut state = self.update_state();
if state.matches(name) {
@@ -79,15 +84,11 @@ impl RealmFSItem {
}
}
pub fn realmfs(&self) -> &RealmFS {
&self.realmfs
}
pub fn index(&self) -> u32 {
self.index
}
pub fn object_path(&self) -> ObjectPath<'_> {
pub fn object_path(&self) -> ObjectPath {
self.object_path.as_ref()
}
}
@@ -101,88 +102,39 @@ impl RealmFSItem {
&mut self,
#[zbus(header)]
hdr: Header<'_>,
#[zbus(signal_emitter)]
emitter: SignalEmitter<'_>,
shared_directory: bool,
) -> fdo::Result<String> {
let mut update_container = String::new();
{
let mut state = self.update_state();
if state.is_active() {
return Err(fdo::Error::Failed("An update is already in progress".to_owned()));
}
let mut state = self.update_state();
let sender = match hdr.sender() {
Some(sender) => sender,
None => return Err(fdo::Error::Failed("No sender in prepare_update()".into())),
};
let mut update = self.realmfs.update()
.map_err(|err| fdo::Error::Failed(err.to_string()))?;
update.prepare_update(shared_directory)
.map_err(|err| fdo::Error::Failed(err.to_string()))?;
update_container.push_str(update.name());
debug!("Update from {}, container: {}", sender, update_container);
state.activate(sender.to_owned(), update);
if state.is_active() {
return Err(fdo::Error::Failed("An update is already in progress".to_owned()));
}
self.is_update_in_progress_changed(&emitter).await?;
let sender = match hdr.sender() {
Some(sender) => sender,
None => todo!(),
};
let mut update = self.realmfs.update()
.map_err(|err| fdo::Error::Failed(err.to_string()))?;
update.prepare_update(shared_directory)
.map_err(|err| fdo::Error::Failed(err.to_string()))?;
let update_container = update.name().to_string();
debug!("Update from {}, container: {}", sender, update_container);
state.activate(sender.to_owned(), update);
Ok(update_container)
}
async fn commit_update(&mut self,
#[zbus(signal_emitter)]
emitter: SignalEmitter<'_>
) -> fdo::Result<()> {
let mut update = match self.update_state().take_update() {
None => {
warn!("CommitUpdate called when no update in progress");
return Ok(());
},
Some(update) => update,
};
unblock(move || {
if let Err(err) = update.commit_update() {
warn!("Error committing RealmFS update: {}", err);
}
}).await;
self.is_update_in_progress_changed(&emitter).await?;
self.free_space_changed(&emitter).await?;
async fn commit_update(&mut self) -> fdo::Result<()> {
self.update_state().commit_update();
Ok(())
}
async fn abandon_update(&mut self,
#[zbus(signal_emitter)]
emitter: SignalEmitter<'_>) -> fdo::Result<()> {
async fn abandon_update(&mut self) -> fdo::Result<()> {
self.update_state().cleanup_update();
self.is_update_in_progress_changed(&emitter).await?;
Ok(())
}
async fn resize_grow_by(&mut self,
#[zbus(signal_emitter)]
emitter: SignalEmitter<'_>,
nblocks: u64) -> fdo::Result<()> {
let nblocks = nblocks as usize;
let current = self.realmfs.allocated_size_blocks();
let new_size = current + nblocks;
let realmfs = self.realmfs.clone();
unblock(move || {
realmfs.resize_grow_to(ResizeSize::blocks(new_size))
.map_err(|err| fdo::Error::Failed(err.to_string()))
}).await?;
self.allocated_space_changed(&emitter).await?;
self.free_space_changed(&emitter).await?;
Ok(())
}
@@ -200,12 +152,6 @@ impl RealmFSItem {
fn in_use(&self) -> bool {
self.realmfs.is_activated()
}
#[zbus(property, name = "IsUpdateInProgress")]
fn is_update_in_progress(&self) -> bool {
self.update_state().is_active()
}
#[zbus(property, name = "Mountpoint")]
fn mountpoint(&self) -> String {
self.realmfs.mountpoint().to_string()
@@ -225,7 +171,81 @@ impl RealmFSItem {
#[zbus(property, name = "AllocatedSpace")]
fn allocated_space(&self) -> fdo::Result<u64> {
let blocks = self.realmfs.allocated_size_blocks();
let blocks = self.realmfs.allocated_size_blocks()
.map_err(|err| fdo::Error::Failed(err.to_string()))?;
Ok(blocks as u64 * BLOCK_SIZE)
}
}
}
#[derive(Clone)]
pub struct RealmFSState(Arc<Mutex<Inner>>);
impl RealmFSState {
pub fn new(connection: Connection) -> Self {
RealmFSState(Arc::new(Mutex::new(Inner::new(connection))))
}
fn inner(&self) -> MutexGuard<Inner> {
self.0.lock().unwrap()
}
pub(crate) fn load(&self, manager: &RealmManager) -> zbus::Result<()> {
self.inner().load(manager)
}
pub fn realmfs_by_name(&self, name: &str) -> Option<RealmFSItem> {
self.inner().realmfs_by_name(name)
}
pub fn client_disconnected(&self, client_name: &UniqueName) {
let mut lock = self.inner();
for (_,v) in &mut lock.items {
v.client_disconnected(client_name);
}
println!("client disconnected: {client_name}")
}
}
struct Inner {
connection: Connection,
next_index: u32,
items: HashMap<String, RealmFSItem>,
}
impl Inner {
fn new(connection: Connection) -> Self {
Inner {
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(())
}
fn realmfs_by_name(&self, name: &str) -> Option<RealmFSItem> {
let res = self.items.get(name).cloned();
if res.is_none() {
warn!("Failed to find RealmFS with name '{}'", name);
}
res
}
}

View File

@@ -1,261 +0,0 @@
use crate::next::manager::failed;
use crate::next::realm::RealmItem;
use crate::next::realmfs::RealmFSItem;
use libcitadel::{Realm, RealmEvent, RealmFS, RealmManager, Result};
use std::collections::HashMap;
use std::sync::{Arc, Mutex, MutexGuard};
use zbus::blocking::Connection;
use zbus::fdo;
use zbus::names::UniqueName;
use zbus::zvariant::{ObjectPath, Value};
/// Maintains a mapping of RealmFS names to the DBus object
/// index values for the corresponding RealmFS objects.
///
/// This is used in the Realm objects to look up the correct
/// realmfs object index for a realmfs name in the realm configuration.
///
#[derive(Clone)]
pub struct RealmFSNameToId(Arc<Mutex<HashMap<String, u32>>>);
impl RealmFSNameToId {
pub fn new() -> Self {
Self(Arc::new(Mutex::new(HashMap::new())))
}
pub fn add(&mut self, name: &str, id: u32) {
self.0.lock().unwrap().insert(name.to_string(), id);
}
pub fn lookup(&self, name: &str) -> u32 {
match self.0.lock().unwrap().get(name) {
None => {
warn!("Failed to map realmfs name '{}' to an object index", name);
0
}
Some(&idx) => idx,
}
}
}
#[derive(Clone)]
pub struct RealmsManagerState(Arc<Mutex<StateInner>>);
impl RealmsManagerState {
pub fn new(connection: Connection) -> Self {
Self(Arc::new(Mutex::new(StateInner::new(connection))))
}
pub fn load(&self, manager: &RealmManager) -> zbus::Result<()> {
self.inner().load(manager)?;
self.add_event_handler(manager)
.map_err(|err| zbus::Error::Failure(err.to_string()))
}
fn inner(&self) -> MutexGuard<'_, StateInner> {
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!("Failed to handle event {}: {:?}", ev, err);
}
});
manager.start_event_task()?;
Ok(())
}
fn on_starting(&self, realm: &Realm) -> fdo::Result<()>{
self.inner().realm_status_changed(realm, Some(true))
}
fn on_started(&self, realm: &Realm) -> fdo::Result<()>{
self.inner().realm_status_changed(realm, Some(false))
}
fn on_stopping(&self, realm: &Realm) -> fdo::Result<()> {
self.inner().realm_status_changed(realm, Some(true))
}
fn on_stopped(&self, realm: &Realm) -> fdo::Result<()>{
self.inner().realm_status_changed(realm, Some(false))
}
fn on_new(&self, realm: &Realm) -> fdo::Result<()>{
self.inner().add_realm(realm.clone())
}
fn on_removed(&self, realm: &Realm) -> fdo::Result<()>{
self.inner().remove_realm(realm)
}
fn on_current(&self, realm: Option<&Realm>) -> fdo::Result<()> {
self.inner().set_current_realm(realm)
}
fn handle_event(&self, ev: &RealmEvent) -> fdo::Result<()> {
match ev {
RealmEvent::Started(realm) => self.on_started(realm)?,
RealmEvent::Stopped(realm) => self.on_stopped(realm)?,
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(realm) => self.on_starting(realm)?,
RealmEvent::Stopping(realm) => self.on_stopping(realm)?,
};
Ok(())
}
pub fn get_current(&self) -> Option<RealmItem> {
self.inner().current_realm.clone()
}
pub fn client_disconnected(&self, client_name: &UniqueName) {
self.inner().client_disconnected(client_name);
}
pub fn fork_realmfs(&self, name: &str, new_name: &str) -> fdo::Result<ObjectPath<'static>> {
self.inner().fork_realmfs(name, new_name)
}
}
struct StateInner {
connection: Connection,
next_realm_index: u32,
realm_items: HashMap<String, RealmItem>,
current_realm: Option<RealmItem>,
next_realmfs_index: u32,
realmfs_items: HashMap<String, RealmFSItem>,
realmfs_name_to_id: RealmFSNameToId,
}
impl StateInner {
fn new(connection: Connection) -> StateInner {
StateInner {
connection,
next_realm_index: 1,
realm_items: HashMap::new(),
current_realm: None,
next_realmfs_index: 1,
realmfs_items: HashMap::new(),
realmfs_name_to_id: RealmFSNameToId::new(),
}
}
fn load(&mut self, manager: &RealmManager) -> fdo::Result<()> {
for realmfs in manager.realmfs_list() {
self.add_realmfs(realmfs);
}
for realm in manager.realm_list() {
self.add_realm(realm)?;
}
Ok(())
}
fn add_realm(&mut self, realm: Realm) -> fdo::Result<()> {
if self.realm_items.contains_key(realm.name()) {
warn!("Attempted to add duplicate realm '{}'", realm.name());
return Ok(())
}
info!("Adding realm-{} with obj index {}", realm.name(), self.next_realm_index);
let key = realm.name().to_string();
let item = RealmItem::new_from_realm(self.next_realm_index, realm, self.realmfs_name_to_id.clone());
self.connection.object_server().at(item.path(), item.clone())?;
self.realm_items.insert(key, item);
self.next_realm_index += 1;
Ok(())
}
fn remove_realm(&mut self, realm: &Realm) -> fdo::Result<()> {
if let Some(item) = self.realm_items.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 add_realmfs(&mut self, realmfs: RealmFS) -> Option<RealmFSItem> {
if !self.realmfs_items.contains_key(realmfs.name()) {
info!("Adding realmfs-{} with object index {}", realmfs.name(), self.next_realmfs_index);
let name = realmfs.name().to_string();
let item = RealmFSItem::new_from_realmfs(self.next_realmfs_index, realmfs);
if let Err(err) = self.connection.object_server().at(item.object_path(), item.clone()) {
warn!("Failed to publish object at path {}: {} ", item.object_path(), err);
} else {
self.realmfs_items.insert(name.clone(), item);
self.realmfs_name_to_id.add(&name, self.next_realmfs_index);
self.next_realmfs_index += 1;
}
self.realmfs_by_name(&name)
} else {
warn!("Attempted to add duplicate realmfs '{}'", realmfs.name());
None
}
}
fn realmfs_by_name(&self, name: &str) -> Option<RealmFSItem> {
let res = self.realmfs_items.get(name).cloned();
if res.is_none() {
warn!("Failed to find RealmFS with name '{}'", name);
}
res
}
fn fork_realmfs(&mut self, name: &str, new_name: &str) -> fdo::Result<ObjectPath<'static>> {
let item = match self.realmfs_by_name(name) {
None => return Err(fdo::Error::Failed(format!("Could not fork {}-realmfs, realmfs not found", name))),
Some(item) => item,
};
let new_realmfs = item.realmfs().fork(new_name).
map_err(|err| fdo::Error::Failed(format!("Failed to fork realmfs-{} to '{}': {}", name, new_name, err)))?;
match self.add_realmfs(new_realmfs) {
None => Err(fdo::Error::Failed(format!("Failed adding new realmfs while forking realmfs-{} to {}", name, new_name))),
Some(new_item) => {
let path = new_item.object_path().to_owned();
Ok(path)
},
}
}
fn realm_by_name(&self, name: &str) -> Option<&RealmItem> {
let res = self.realm_items.get(name);
if res.is_none() {
warn!("Failed to find realm with name '{}'", name);
}
res
}
fn client_disconnected(&mut self, client_name: &UniqueName) {
for v in self.realmfs_items.values_mut() {
v.client_disconnected(client_name);
}
}
fn realm_status_changed(&self, realm: &Realm, transition: Option<bool>) -> fdo::Result<()> {
if let Some(realm) = self.realm_by_name(realm.name()) {
if let Some(transition) = transition {
realm.set_in_run_transition(transition);
}
realm.emit_property_changed(self.connection.inner(), "RunStatus", Value::U32(realm.get_run_status() as u32))?;
let timestamp = realm.realm().timestamp();
if timestamp != realm.last_timestamp() {
realm.set_last_timestamp(timestamp);
realm.emit_property_changed(self.connection.inner(), "Timestamp", Value::U64(timestamp as u64))?;
}
Ok(())
} else {
failed(format!("Unknown realm {}", realm.name()))
}
}
fn set_current_realm(&mut self, realm: Option<&Realm>) -> fdo::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(())
}
}