1
0
forked from brl/citadel-tools

2 Commits

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

5
.cargo/config.toml Normal file
View File

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

100
Cargo.lock generated
View File

@@ -101,9 +101,9 @@ dependencies = [
[[package]]
name = "anyhow"
version = "1.0.89"
version = "1.0.88"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "86fdf8605db99b54d3cd748a44c6d04df638eb5dafb219b135d0149bd0db01f6"
checksum = "4e1496f8fb1fbf272686b8d37f523dab3e4a7443300055e74cdaa449f3114356"
[[package]]
name = "array-macro"
@@ -308,9 +308,9 @@ checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b"
[[package]]
name = "bytes"
version = "1.7.2"
version = "1.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "428d9aa8fbc0670b7b8d6030a7fadd0f86151cae55e4dbbece15f3780a3dfaf3"
checksum = "8318a53db07bb3f8dca91a600466bdb3f2eaadeedfdbcf02e1accbad9271ba50"
[[package]]
name = "cairo-rs"
@@ -338,9 +338,9 @@ dependencies = [
[[package]]
name = "cc"
version = "1.1.21"
version = "1.1.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "07b1695e2c7e8fc85310cde85aeaab7e3097f593c91d209d3f9df76c928100f0"
checksum = "b62ac837cdb5cb22e10a256099b4fc502b1dfe560cb282963a974d7abd80e476"
dependencies = [
"shlex",
]
@@ -434,6 +434,7 @@ dependencies = [
"pwhash",
"reqwest",
"rpassword",
"rs-release",
"serde",
"serde_cbor",
"serde_derive",
@@ -443,9 +444,9 @@ dependencies = [
[[package]]
name = "clap"
version = "4.5.18"
version = "4.5.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b0956a43b323ac1afaffc053ed5c4b7c1f1800bacd1683c353aabbb752515dd3"
checksum = "3e5a21b8495e732f1b3c364c9949b201ca7bae518c502c80256c96ad79eaf6ac"
dependencies = [
"clap_builder",
"clap_derive",
@@ -453,9 +454,9 @@ dependencies = [
[[package]]
name = "clap_builder"
version = "4.5.18"
version = "4.5.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4d72166dd41634086d5803a47eb71ae740e61d84709c36f3c34110173db3961b"
checksum = "8cf2dd12af7a047ad9d6da2b6b249759a22a7abc0f474c1dae1777afa4b21a73"
dependencies = [
"anstream",
"anstyle",
@@ -465,9 +466,9 @@ dependencies = [
[[package]]
name = "clap_derive"
version = "4.5.18"
version = "4.5.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4ac6a0c7b1a9e9a5186361f67dfa1b88213572f427fb9ab038efb2bd8c582dab"
checksum = "501d359d5f3dcaf6ecdeee48833ae73ec6e42723a1e52419c79abf9507eec0a0"
dependencies = [
"heck 0.5.0",
"proc-macro2 1.0.86",
@@ -731,6 +732,7 @@ checksum = "4a3daa8e81a3963a60642bcc1f90a670680bd4a77535faa384e9d1c79d620871"
dependencies = [
"curve25519-dalek",
"ed25519 2.2.3",
"rand_core",
"serde",
"sha2 0.10.8",
"subtle",
@@ -1494,9 +1496,9 @@ dependencies = [
[[package]]
name = "iana-time-zone"
version = "0.1.61"
version = "0.1.60"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "235e081f3925a06703c2d0117ea8b91f042756fd6e7a6e5d901e8ca1a996b220"
checksum = "e7ffbb5a1b541ea2561f8c41c087286cc091e21e556a4f09a8f6cbf17b69b141"
dependencies = [
"android_system_properties",
"core-foundation-sys",
@@ -1642,14 +1644,15 @@ checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe"
[[package]]
name = "libc"
version = "0.2.159"
version = "0.2.158"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "561d97a539a36e26a9a5fad1ea11a3039a67714694aaa379433e580854bc3dc5"
checksum = "d8adc4bb1803a324070e64a98ae98f38934d91957a99cfb3a43dcbc01bc56439"
[[package]]
name = "libcitadel"
version = "0.1.0"
dependencies = [
"anyhow",
"bincode",
"byteorder",
"clap",
@@ -2083,9 +2086,9 @@ dependencies = [
[[package]]
name = "pkg-config"
version = "0.3.31"
version = "0.3.30"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "953ec861398dccce10c670dfeaf3ec4911ca479e9c02154b3a215178c5f566f2"
checksum = "d231b230927b5e4ad203db57bbcbee2802f6bce620b1e4a9024a07d94e2907ec"
[[package]]
name = "polling"
@@ -2380,6 +2383,12 @@ dependencies = [
"windows-sys 0.48.0",
]
[[package]]
name = "rs-release"
version = "0.1.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5004605c85f4ba88d5d02ed03d2ea460356322fd3fe4500d9b33d037edd1c5c1"
[[package]]
name = "rtoolbox"
version = "0.0.2"
@@ -2493,9 +2502,9 @@ dependencies = [
[[package]]
name = "security-framework-sys"
version = "2.12.0"
version = "2.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ea4a292869320c0272d7bc55a5a6aafaff59b4f63404a003887b679a2e05b4b6"
checksum = "75da29fe9b9b08fe9d6b22b5b4bcbc75d8db3aa31e639aa56bb62e9d46bfceaf"
dependencies = [
"core-foundation-sys",
"libc",
@@ -2847,7 +2856,7 @@ dependencies = [
"proc-macro2 1.0.86",
"quote 1.0.37",
"syn 1.0.109",
"unicode-xid 0.2.6",
"unicode-xid 0.2.5",
]
[[package]]
@@ -2931,18 +2940,18 @@ dependencies = [
[[package]]
name = "thiserror"
version = "1.0.64"
version = "1.0.63"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d50af8abc119fb8bb6dbabcfa89656f46f84aa0ac7688088608076ad2b459a84"
checksum = "c0342370b38b6a11b6cc11d6a805569958d54cfa061a29969c3b5ce2ea405724"
dependencies = [
"thiserror-impl",
]
[[package]]
name = "thiserror-impl"
version = "1.0.64"
version = "1.0.63"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "08904e7672f5eb876eaaf87e0ce17857500934f4981c4a0ab2b4aa98baac7fc3"
checksum = "a4558b58466b9ad7ca0f102865eccc95938dca1a74a856f2b57b6629050da261"
dependencies = [
"proc-macro2 1.0.86",
"quote 1.0.37",
@@ -3029,7 +3038,7 @@ dependencies = [
"serde",
"serde_spanned",
"toml_datetime",
"toml_edit 0.22.21",
"toml_edit 0.22.20",
]
[[package]]
@@ -3054,9 +3063,9 @@ dependencies = [
[[package]]
name = "toml_edit"
version = "0.22.21"
version = "0.22.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3b072cee73c449a636ffd6f32bd8de3a9f7119139aff882f44943ce2986dc5cf"
checksum = "583c44c02ad26b0c3f3066fe629275e50627026c51ac2e595cca4c230ce1ce1d"
dependencies = [
"indexmap",
"serde",
@@ -3137,24 +3146,24 @@ checksum = "e91b56cd4cadaeb79bbf1a5645f6b4f8dc5bde8834ad5894a8db35fda9efa1fe"
[[package]]
name = "unicode-normalization"
version = "0.1.24"
version = "0.1.23"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5033c97c4262335cded6d6fc3e5c18ab755e1a3dc96376350f3d8e9f009ad956"
checksum = "a56d1686db2308d901306f92a263857ef59ea39678a5458e7cb17f01415101f5"
dependencies = [
"tinyvec",
]
[[package]]
name = "unicode-segmentation"
version = "1.12.0"
version = "1.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493"
checksum = "d4c87d22b6e3f4a18d4d40ef354e97c90fcb14dd91d7dc0aa9d8a1172ebf7202"
[[package]]
name = "unicode-width"
version = "0.1.14"
version = "0.1.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af"
checksum = "0336d538f7abc86d282a4189614dfaa90810dfc2c6f6427eaf88e16311dd225d"
[[package]]
name = "unicode-xid"
@@ -3164,9 +3173,28 @@ checksum = "fc72304796d0818e357ead4e000d19c9c174ab23dc11093ac919054d20a6a7fc"
[[package]]
name = "unicode-xid"
version = "0.2.6"
version = "0.2.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853"
checksum = "229730647fbc343e3a80e463c1db7f78f3855d3f3739bee0dda773c9a037c90a"
[[package]]
name = "update_generator"
version = "0.1.0"
dependencies = [
"anyhow",
"clap",
"ed25519-dalek",
"glob",
"libcitadel",
"rand",
"rpassword",
"semver",
"serde",
"serde_cbor",
"serde_derive",
"sodiumoxide",
"zeroize",
]
[[package]]
name = "url"

View File

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

File diff suppressed because it is too large Load Diff

View File

@@ -21,6 +21,7 @@ pwhash = "1.0"
tempfile = "3"
ed25519-dalek = {version = "2.1", features = ["pem"]}
anyhow = "1.0"
rs-release = "0.1"
reqwest = {version = "0.12", features = ["blocking"]}
glob = "0.3"
serde_cbor = "0.11"
serde_cbor = "0.11"

View File

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

View File

@@ -94,7 +94,7 @@ fn choose_revert_partition(best: Option<Partition>) -> Option<Partition> {
best
}
pub fn choose_boot_partiton(scan: bool, revert_rootfs: bool) -> Result<Partition> {
fn choose_boot_partiton(scan: bool, revert_rootfs: bool) -> Result<Partition> {
let mut partitions = Partition::rootfs_partitions()?;
if scan {

View File

@@ -1,15 +1,20 @@
use crate::{update, Path};
use anyhow::{bail, Context, Result};
use anyhow::{Context, Result};
use clap::ArgMatches;
use ed25519_dalek::{pkcs8::DecodePublicKey, VerifyingKey};
use libcitadel::updates::UPDATE_SERVER_HOSTNAME;
use libcitadel::ResourceImage;
use libcitadel::{updates, updates::CitadelVersionStruct};
use libcitadel::{OsRelease, ResourceImage};
use std::io::prelude::*;
use std::str::FromStr;
const OS_RELEASE_PATH: &str = "/etc/os-release";
const IMAGE_DIRECTORY_PATH: &str = "/run/citadel/images/";
const EXTRA_IMAGE_PATH: &str = "/run/citadel/images/citadel-extra.img";
const ROOTFS_IMAGE_PATH: &str = "/run/citadel/images/citadel-rootfs.img";
const UPDATE_SERVER_KEY_PATH: &str = "/etc/citadel/update_server_key.pub";
const LAST_RESORT_CLIENT: &str = "public";
pub fn check() -> Result<()> {
let current_version = get_current_os_config()?;
@@ -25,7 +30,7 @@ pub fn check() -> Result<()> {
components_to_upgrade[0]
);
} else if components_to_upgrade.len() > 1 {
println!("We found the following components to upgrade:");
println!("We found the following components to upgrade: \n");
for component in components_to_upgrade {
println!("{}", component);
@@ -58,7 +63,7 @@ pub fn download(sub_matches: &ArgMatches) -> Result<()> {
pub fn read_remote() -> Result<()> {
let server_citadel_version = fetch_and_verify_version_cbor(&get_current_os_config()?)?;
println!("Server offers:\n{server_citadel_version}");
println!("Server offers:\n{}", server_citadel_version);
Ok(())
}
@@ -152,27 +157,57 @@ fn compare_citadel_versions(
Ok(update_vec)
}
// We need to get the version of the rootfs, kernel and extra images currently installed
fn get_current_os_config() -> Result<updates::CitadelVersionStruct> {
let client = OsRelease::citadel_client().context("Failed to find client of current system")?;
let channel = OsRelease::citadel_channel().context("Failed to find channel of current system")?;
let publisher = OsRelease::citadel_publisher().context("Failed to find publisher of current system")?;
fn get_image_version<P: AsRef<Path>>(path: &P) -> Result<String> {
let resource_image = ResourceImage::from_path(path)?;
let metainfo;
// choose best partion to boot from as the partition to compare versions with
let rootfs_version = match crate::boot::rootfs::choose_boot_partiton(false, false) {
Ok(part) => {metainfo = part.header().metainfo();
metainfo.version() }
Err(e) => bail!("Rootfs version not found. Error: {e}"),
Ok(resource_image.metainfo().version().to_string())
}
fn get_current_os_config() -> Result<updates::CitadelVersionStruct> {
let os_release = rs_release::parse_os_release(OS_RELEASE_PATH).context(format!(
"Failed to get OS_RELEASE_PATH from {}",
OS_RELEASE_PATH
))?;
// We need to get the version of the rootfs, kernel and extra images
// We first check if there is an env var called UPDATES_CLIENT
let client = match std::env::var("UPDATES_CLIENT") {
Ok(a) => a,
Err(_) => os_release
.get("CLIENT")
.unwrap_or(&LAST_RESORT_CLIENT.to_string())
.to_string(),
};
// Get highest values of image versions
let kernel_resource = ResourceImage::find("kernel")?.metainfo();
let kernel_version = kernel_resource.version();
let channel = os_release.get("CITADEL_CHANNEL");
// Search image dir for kernel image and extract version
let mut kernel_version = String::from("0.0.0");
for path in glob::glob(&format!("{}citadel-kernel*.img", IMAGE_DIRECTORY_PATH))
.expect("Failed to read glob pattern")
{
match path {
Ok(path) => {
kernel_version = get_image_version(&path).context(format!(
"Failed to get image version at path: {}",
path.display()
))?
}
Err(e) => anyhow::bail!("Failed to find kernel image. Error: {}", e),
}
}
let rootfs_version = get_image_version(&ROOTFS_IMAGE_PATH).context(format!(
"Failed to get rootfs img version at path: {}",
ROOTFS_IMAGE_PATH
))?;
let extra_version = get_image_version(&EXTRA_IMAGE_PATH).context(format!(
"Failed to get extra img version at path: {}",
EXTRA_IMAGE_PATH
))?;
let publisher = os_release.get("CITADEL_PUBLISHER");
let extra_resource = ResourceImage::find("extra")?.metainfo();
let extra_version = extra_resource.version();
let mut component_version = Vec::new();
component_version.push(updates::AvailableComponentVersion {
component: updates::Component::Rootfs,
@@ -192,9 +227,9 @@ fn get_current_os_config() -> Result<updates::CitadelVersionStruct> {
let current_version_struct = updates::CitadelVersionStruct {
client: client.to_owned(),
channel: channel.to_owned(),
channel: channel.unwrap_or(&"prod".to_owned()).to_owned(),
component_version,
publisher: publisher.to_owned(),
publisher: publisher.unwrap_or(&"Subgraph".to_string()).to_owned(),
};
Ok(current_version_struct)
@@ -210,7 +245,7 @@ fn fetch_and_verify_version_cbor(
let version_file_bytes = reqwest::blocking::get(&url)?
.bytes()
.context(format!("Failed to get version_file_bytes from {url}"))?;
.context(format!("Failed to get version_file_bytes from {}", url))?;
let crypto_container: updates::CryptoContainerFile =
serde_cbor::from_slice(&version_file_bytes)
@@ -231,8 +266,7 @@ fn fetch_and_verify_version_cbor(
let signature = ed25519_dalek::Signature::from_str(&crypto_container.signature)?;
// verify signature
public_key.verify_strict(&crypto_container.serialized_citadel_version, &signature)
.context("We failed to verify the signature update release file. Please make sure the key at /etc/citade/update_server_key.pub matches the one publicly linked to your update provider.")?;
public_key.verify_strict(&crypto_container.serialized_citadel_version, &signature)?;
// construct the struct
let citadel_version_struct: updates::CitadelVersionStruct =
@@ -245,7 +279,7 @@ fn prompt_user_for_permission_to_download(
component: &updates::AvailableComponentVersion,
) -> Result<bool> {
println!(
"Would you like to download and install the new version of the {} image with version {}? (y/n)",
"Would you like to download the new version of the {} image with version {}? (y/n)",
component.component, component.version
);
@@ -266,8 +300,9 @@ fn prompt_user_for_permission_to_download(
fn download_file(path: &str) -> Result<std::path::PathBuf> {
let client = reqwest::blocking::Client::new();
let url = format!("https://{UPDATE_SERVER_HOSTNAME}/{path}");
println!("Downloading from {url}");
let url = format!("https://{}/{}", UPDATE_SERVER_HOSTNAME, path);
println!("Downloading from {}", url);
let component_download_response = client.get(&url).send()?;
@@ -284,10 +319,10 @@ fn download_file(path: &str) -> Result<std::path::PathBuf> {
let mut content = std::io::Cursor::new(component_download_response.bytes()?);
let mut file =
std::fs::File::create(&path).context(format!("Failed to create file at {path}"))?;
std::fs::File::create(&path).context(format!("Failed to create file at {}", path))?;
std::io::copy(&mut content, &mut file)?;
println!("Saved file to {path}");
println!("Saved file to {}", path);
Ok(std::path::PathBuf::from(path))
}

View File

@@ -1,16 +1,10 @@
use clap::{arg, command, ArgAction, Command};
use std::process::exit;
use libcitadel::util;
mod fetch;
pub fn main() {
if !util::is_euid_root() {
println!("Please run this program as root");
exit(1);
}
let matches = command!()
let matches = command!() // requires `cargo` feature
.subcommand_required(true)
.subcommand(Command::new("check").about("Check for updates from remote server"))
.subcommand(
@@ -52,4 +46,4 @@ pub fn main() {
println!("Error: {}", e);
exit(1);
}
}
}

View File

@@ -63,7 +63,6 @@ fn dispatch_command(args: Vec<String>) {
"image" => image::main(),
"realmfs" => realmfs::main(),
"update" => update::main(rebuild_args("citadel-update", args)),
"fetch" => update::main(rebuild_args("citadel-fetch", args)),
"mkimage" => mkimage::main(rebuild_args("citadel-mkimage", args)),
"sync" => sync::main(rebuild_args("citadel-desktop-sync", args)),
"run" => do_citadel_run(rebuild_args("citadel-run", args)),

View File

@@ -38,7 +38,7 @@ impl UpdateBuilder {
}
fn target_filename(&self) -> String {
format!("citadel-{}-{}-{}.img", self.config.img_name(), self.config.channel(), self.config.version())
format!("citadel-{}-{}-{:}.img", self.config.img_name(), self.config.channel(), self.config.version())
}
fn build_filename(config: &BuildConfig) -> String {

View File

@@ -21,6 +21,7 @@ dbus = "0.6"
posix-acl = "1.0.0"
procfs = "0.12.0"
semver = "1.0"
anyhow = "1.0"
clap = "4.5"
[dependencies.inotify]

View File

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

View File

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

View File

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

View File

@@ -11,7 +11,7 @@ pub struct CryptoContainerFile {
pub signatory: String, // name of org or person who holds the key
}
/// This struct contains the entirety of the information needed to decide whether to update or not
/// 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,
@@ -65,7 +65,7 @@ impl std::fmt::Display for AvailableComponentVersion {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
write!(
f,
"{} image has version {}",
"({} image has version: {})",
self.component, self.version
)
}

View File

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

View File

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

View File

@@ -36,7 +36,7 @@ const LAST_RESORT_CLIENT: &str = "public";
const LAST_RESORT_CHANNEL: &str = "prod";
#[derive(Parser)]
#[command(about = "Perform tasks needed to create an update and publish it")]
#[command(about = "Generate the file we use to inform clients of updates")]
#[command(author, about, long_about = None)]
struct Cli {
#[command(subcommand)]
@@ -424,6 +424,7 @@ fn verify_version_signature(
// Make sure to add your ssh key to the "updates" user on the server
fn send_with_scp(from: &PathBuf, to: &PathBuf) -> Result<()> {
Command::new("scp")
.arg("-P 22514")
.arg(from)
.arg(format!(
"updates@{}:/updates/files/{}",