Compare commits
2 Commits
master
...
update_too
Author | SHA1 | Date | |
---|---|---|---|
0ff03cb273 | |||
ae41a71b60 |
5
.cargo/config.toml
Normal file
5
.cargo/config.toml
Normal 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"
|
||||||
|
|
1809
Cargo.lock
generated
1809
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
@ -1,5 +1,5 @@
|
|||||||
[workspace]
|
[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]
|
[profile.release]
|
||||||
lto = true
|
lto = true
|
||||||
codegen-units = 1
|
codegen-units = 1
|
||||||
|
2854
citadel-installer-ui/Cargo.lock
generated
2854
citadel-installer-ui/Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
@ -19,3 +19,9 @@ byteorder = "1"
|
|||||||
dbus = "0.8.4"
|
dbus = "0.8.4"
|
||||||
pwhash = "1.0"
|
pwhash = "1.0"
|
||||||
tempfile = "3"
|
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"
|
@ -136,8 +136,11 @@ fn compare_boot_partitions(a: Option<Partition>, b: Partition) -> Option<Partiti
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Compare versions and channels
|
// Compare versions and channels
|
||||||
let a_v = a.metainfo().version();
|
let bind_a = a.metainfo();
|
||||||
let b_v = b.metainfo().version();
|
let bind_b = b.metainfo();
|
||||||
|
|
||||||
|
let a_v = bind_a.version();
|
||||||
|
let b_v = bind_b.version();
|
||||||
|
|
||||||
// Compare versions only if channels match
|
// Compare versions only if channels match
|
||||||
if a.metainfo().channel() == b.metainfo().channel() {
|
if a.metainfo().channel() == b.metainfo().channel() {
|
||||||
|
328
citadel-tool/src/fetch/fetch.rs
Normal file
328
citadel-tool/src/fetch/fetch.rs
Normal file
@ -0,0 +1,328 @@
|
|||||||
|
use crate::{update, Path};
|
||||||
|
use anyhow::{Context, Result};
|
||||||
|
use clap::ArgMatches;
|
||||||
|
use ed25519_dalek::{pkcs8::DecodePublicKey, VerifyingKey};
|
||||||
|
use libcitadel::updates::UPDATE_SERVER_HOSTNAME;
|
||||||
|
use libcitadel::ResourceImage;
|
||||||
|
use libcitadel::{updates, updates::CitadelVersionStruct};
|
||||||
|
|
||||||
|
use std::io::prelude::*;
|
||||||
|
use std::str::FromStr;
|
||||||
|
|
||||||
|
const OS_RELEASE_PATH: &str = "/etc/os-release";
|
||||||
|
const IMAGE_DIRECTORY_PATH: &str = "/run/citadel/images/";
|
||||||
|
const EXTRA_IMAGE_PATH: &str = "/run/citadel/images/citadel-extra.img";
|
||||||
|
const ROOTFS_IMAGE_PATH: &str = "/run/citadel/images/citadel-rootfs.img";
|
||||||
|
const UPDATE_SERVER_KEY_PATH: &str = "/etc/citadel/update_server_key.pub";
|
||||||
|
const LAST_RESORT_CLIENT: &str = "public";
|
||||||
|
|
||||||
|
pub fn check() -> Result<()> {
|
||||||
|
let current_version = get_current_os_config()?;
|
||||||
|
|
||||||
|
let server_citadel_version = fetch_and_verify_version_cbor(¤t_version)?;
|
||||||
|
|
||||||
|
let components_to_upgrade =
|
||||||
|
compare_citadel_versions(¤t_version, &server_citadel_version)?;
|
||||||
|
|
||||||
|
if components_to_upgrade.len() == 1 {
|
||||||
|
println!(
|
||||||
|
"We found the following component to upgrade: {}",
|
||||||
|
components_to_upgrade[0]
|
||||||
|
);
|
||||||
|
} else if components_to_upgrade.len() > 1 {
|
||||||
|
println!("We found the following components to upgrade: \n");
|
||||||
|
|
||||||
|
for component in components_to_upgrade {
|
||||||
|
println!("{}", component);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
println!("Your system is up to date!");
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn download(sub_matches: &ArgMatches) -> Result<()> {
|
||||||
|
let current_version = &get_current_os_config()?;
|
||||||
|
let server_citadel_version = &fetch_and_verify_version_cbor(¤t_version)?;
|
||||||
|
|
||||||
|
let mut path = "";
|
||||||
|
if sub_matches.get_flag("rootfs") {
|
||||||
|
path = &server_citadel_version.component_version[0].file_path;
|
||||||
|
} else if sub_matches.get_flag("kernel") {
|
||||||
|
path = &server_citadel_version.component_version[1].file_path;
|
||||||
|
} else if sub_matches.get_flag("extra") {
|
||||||
|
path = &server_citadel_version.component_version[2].file_path;
|
||||||
|
}
|
||||||
|
|
||||||
|
download_file(path)?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn read_remote() -> Result<()> {
|
||||||
|
let server_citadel_version = fetch_and_verify_version_cbor(&get_current_os_config()?)?;
|
||||||
|
|
||||||
|
println!("Server offers:\n{}", server_citadel_version);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn upgrade() -> Result<()> {
|
||||||
|
// First get access to the current citadel's parameters
|
||||||
|
let current_version = &get_current_os_config()?;
|
||||||
|
let server_citadel_version = &fetch_and_verify_version_cbor(¤t_version)?;
|
||||||
|
|
||||||
|
// What do we need to upgrade?
|
||||||
|
let components_to_upgrade =
|
||||||
|
compare_citadel_versions(¤t_version, &server_citadel_version)?;
|
||||||
|
|
||||||
|
if components_to_upgrade.len() == 1 {
|
||||||
|
println!("We found a component to upgrade!");
|
||||||
|
let allow_download = prompt_user_for_permission_to_download(&components_to_upgrade[0])?;
|
||||||
|
|
||||||
|
if allow_download {
|
||||||
|
let save_path = download_file(&components_to_upgrade[0].file_path)?;
|
||||||
|
|
||||||
|
// run citadel-update to upgrade
|
||||||
|
println!("Installing image");
|
||||||
|
update::install_image(&save_path, 0)?;
|
||||||
|
println!("Image installed correctly");
|
||||||
|
} else {
|
||||||
|
println!("Ok! Maybe later");
|
||||||
|
}
|
||||||
|
} else if components_to_upgrade.len() > 1 {
|
||||||
|
println!("We found some components to upgrade!");
|
||||||
|
for component in components_to_upgrade {
|
||||||
|
let allow_download = prompt_user_for_permission_to_download(&component)?;
|
||||||
|
|
||||||
|
if allow_download {
|
||||||
|
let save_path = download_file(&component.file_path)?;
|
||||||
|
|
||||||
|
println!("Installing image");
|
||||||
|
update::install_image(&save_path, 0)?;
|
||||||
|
println!("Image installed correctly");
|
||||||
|
} else {
|
||||||
|
println!("Ok! Maybe later");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
println!("Your system is up to date!");
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn reinstall(sub_matches: &ArgMatches) -> Result<()> {
|
||||||
|
let current_version = &get_current_os_config()?;
|
||||||
|
let server_citadel_version = &fetch_and_verify_version_cbor(¤t_version)?;
|
||||||
|
|
||||||
|
let mut path = "";
|
||||||
|
if sub_matches.get_flag("rootfs") {
|
||||||
|
path = &server_citadel_version.component_version[0].file_path;
|
||||||
|
} else if sub_matches.get_flag("kernel") {
|
||||||
|
path = &server_citadel_version.component_version[1].file_path;
|
||||||
|
} else if sub_matches.get_flag("extra") {
|
||||||
|
path = &server_citadel_version.component_version[2].file_path;
|
||||||
|
}
|
||||||
|
|
||||||
|
let save_path = download_file(path)?;
|
||||||
|
update::install_image(&save_path, 0)?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns a vec of ComponentVersion structs of the components which can be upgraded
|
||||||
|
fn compare_citadel_versions(
|
||||||
|
current: &CitadelVersionStruct,
|
||||||
|
offered: &CitadelVersionStruct,
|
||||||
|
) -> Result<Vec<updates::AvailableComponentVersion>> {
|
||||||
|
let mut update_vec: Vec<updates::AvailableComponentVersion> = Vec::new();
|
||||||
|
|
||||||
|
// safety checks
|
||||||
|
if current.channel != offered.channel {
|
||||||
|
panic!("Error: channels do not match");
|
||||||
|
} else if current.client != offered.client {
|
||||||
|
panic!("Error: clients do not match");
|
||||||
|
} else if current.publisher != offered.publisher {
|
||||||
|
panic!("Error: publishers do not match");
|
||||||
|
}
|
||||||
|
|
||||||
|
for i in 0..current.component_version.len() {
|
||||||
|
if current.component_version[i] < offered.component_version[i] {
|
||||||
|
update_vec.push(offered.component_version[i].clone());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(update_vec)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_image_version<P: AsRef<Path>>(path: &P) -> Result<String> {
|
||||||
|
let resource_image = ResourceImage::from_path(path)?;
|
||||||
|
|
||||||
|
Ok(resource_image.metainfo().version().to_string())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_current_os_config() -> Result<updates::CitadelVersionStruct> {
|
||||||
|
let os_release = rs_release::parse_os_release(OS_RELEASE_PATH).context(format!(
|
||||||
|
"Failed to get OS_RELEASE_PATH from {}",
|
||||||
|
OS_RELEASE_PATH
|
||||||
|
))?;
|
||||||
|
|
||||||
|
// We need to get the version of the rootfs, kernel and extra images
|
||||||
|
// We first check if there is an env var called UPDATES_CLIENT
|
||||||
|
let client = match std::env::var("UPDATES_CLIENT") {
|
||||||
|
Ok(a) => a,
|
||||||
|
Err(_) => os_release
|
||||||
|
.get("CLIENT")
|
||||||
|
.unwrap_or(&LAST_RESORT_CLIENT.to_string())
|
||||||
|
.to_string(),
|
||||||
|
};
|
||||||
|
|
||||||
|
let channel = os_release.get("CITADEL_CHANNEL");
|
||||||
|
|
||||||
|
// Search image dir for kernel image and extract version
|
||||||
|
let mut kernel_version = String::from("0.0.0");
|
||||||
|
for path in glob::glob(&format!("{}citadel-kernel*.img", IMAGE_DIRECTORY_PATH))
|
||||||
|
.expect("Failed to read glob pattern")
|
||||||
|
{
|
||||||
|
match path {
|
||||||
|
Ok(path) => {
|
||||||
|
kernel_version = get_image_version(&path).context(format!(
|
||||||
|
"Failed to get image version at path: {}",
|
||||||
|
path.display()
|
||||||
|
))?
|
||||||
|
}
|
||||||
|
Err(e) => anyhow::bail!("Failed to find kernel image. Error: {}", e),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let rootfs_version = get_image_version(&ROOTFS_IMAGE_PATH).context(format!(
|
||||||
|
"Failed to get rootfs img version at path: {}",
|
||||||
|
ROOTFS_IMAGE_PATH
|
||||||
|
))?;
|
||||||
|
let extra_version = get_image_version(&EXTRA_IMAGE_PATH).context(format!(
|
||||||
|
"Failed to get extra img version at path: {}",
|
||||||
|
EXTRA_IMAGE_PATH
|
||||||
|
))?;
|
||||||
|
|
||||||
|
let publisher = os_release.get("CITADEL_PUBLISHER");
|
||||||
|
|
||||||
|
let mut component_version = Vec::new();
|
||||||
|
component_version.push(updates::AvailableComponentVersion {
|
||||||
|
component: updates::Component::Rootfs,
|
||||||
|
version: rootfs_version.to_owned(),
|
||||||
|
file_path: "".to_owned(),
|
||||||
|
});
|
||||||
|
component_version.push(updates::AvailableComponentVersion {
|
||||||
|
component: updates::Component::Kernel,
|
||||||
|
version: kernel_version.to_owned(),
|
||||||
|
file_path: "".to_owned(),
|
||||||
|
});
|
||||||
|
component_version.push(updates::AvailableComponentVersion {
|
||||||
|
component: updates::Component::Extra,
|
||||||
|
version: extra_version.to_owned(),
|
||||||
|
file_path: "".to_owned(),
|
||||||
|
});
|
||||||
|
|
||||||
|
let current_version_struct = updates::CitadelVersionStruct {
|
||||||
|
client: client.to_owned(),
|
||||||
|
channel: channel.unwrap_or(&"prod".to_owned()).to_owned(),
|
||||||
|
component_version,
|
||||||
|
publisher: publisher.unwrap_or(&"Subgraph".to_string()).to_owned(),
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(current_version_struct)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn fetch_and_verify_version_cbor(
|
||||||
|
current_citadel_version: &updates::CitadelVersionStruct,
|
||||||
|
) -> Result<updates::CitadelVersionStruct> {
|
||||||
|
let url = format!(
|
||||||
|
"https://{}/{}/{}/version.cbor",
|
||||||
|
UPDATE_SERVER_HOSTNAME, current_citadel_version.client, current_citadel_version.channel
|
||||||
|
);
|
||||||
|
|
||||||
|
let version_file_bytes = reqwest::blocking::get(&url)?
|
||||||
|
.bytes()
|
||||||
|
.context(format!("Failed to get version_file_bytes from {}", url))?;
|
||||||
|
|
||||||
|
let crypto_container: updates::CryptoContainerFile =
|
||||||
|
serde_cbor::from_slice(&version_file_bytes)
|
||||||
|
.context(format!("Failed to parse version.cbor from {}", url))?;
|
||||||
|
|
||||||
|
// find update server public key kept in the rootfs
|
||||||
|
let mut file = std::fs::File::open(UPDATE_SERVER_KEY_PATH).context(format!(
|
||||||
|
"Failed to open update_server_key file from {}",
|
||||||
|
UPDATE_SERVER_KEY_PATH
|
||||||
|
))?;
|
||||||
|
|
||||||
|
let mut contents = String::new();
|
||||||
|
file.read_to_string(&mut contents)?;
|
||||||
|
|
||||||
|
let public_key = VerifyingKey::from_public_key_pem(&contents)
|
||||||
|
.context("Failed to parse public key from file.")?;
|
||||||
|
|
||||||
|
let signature = ed25519_dalek::Signature::from_str(&crypto_container.signature)?;
|
||||||
|
|
||||||
|
// verify signature
|
||||||
|
public_key.verify_strict(&crypto_container.serialized_citadel_version, &signature)?;
|
||||||
|
|
||||||
|
// construct the struct
|
||||||
|
let citadel_version_struct: updates::CitadelVersionStruct =
|
||||||
|
serde_cbor::from_slice(&crypto_container.serialized_citadel_version)?;
|
||||||
|
|
||||||
|
Ok(citadel_version_struct)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn prompt_user_for_permission_to_download(
|
||||||
|
component: &updates::AvailableComponentVersion,
|
||||||
|
) -> Result<bool> {
|
||||||
|
println!(
|
||||||
|
"Would you like to download the new version of the {} image with version {}? (y/n)",
|
||||||
|
component.component, component.version
|
||||||
|
);
|
||||||
|
|
||||||
|
loop {
|
||||||
|
let stdin = std::io::stdin();
|
||||||
|
let mut user_input = String::new();
|
||||||
|
|
||||||
|
stdin.read_line(&mut user_input)?;
|
||||||
|
|
||||||
|
if user_input.trim() == "y" {
|
||||||
|
return Ok(true);
|
||||||
|
} else {
|
||||||
|
return Ok(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn download_file(path: &str) -> Result<std::path::PathBuf> {
|
||||||
|
let client = reqwest::blocking::Client::new();
|
||||||
|
|
||||||
|
let url = format!("https://{}/{}", UPDATE_SERVER_HOSTNAME, path);
|
||||||
|
|
||||||
|
println!("Downloading from {}", url);
|
||||||
|
|
||||||
|
let component_download_response = client.get(&url).send()?;
|
||||||
|
|
||||||
|
if !component_download_response.status().is_success() {
|
||||||
|
anyhow::bail!(
|
||||||
|
"Failed to download image from {}. Server returned error {}",
|
||||||
|
path,
|
||||||
|
component_download_response.status()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
let path = Path::new(path);
|
||||||
|
let path = format!("/tmp/{}", path.file_name().unwrap().to_str().unwrap());
|
||||||
|
|
||||||
|
let mut content = std::io::Cursor::new(component_download_response.bytes()?);
|
||||||
|
let mut file =
|
||||||
|
std::fs::File::create(&path).context(format!("Failed to create file at {}", path))?;
|
||||||
|
std::io::copy(&mut content, &mut file)?;
|
||||||
|
|
||||||
|
println!("Saved file to {}", path);
|
||||||
|
|
||||||
|
Ok(std::path::PathBuf::from(path))
|
||||||
|
}
|
49
citadel-tool/src/fetch/mod.rs
Normal file
49
citadel-tool/src/fetch/mod.rs
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
use clap::{arg, command, ArgAction, Command};
|
||||||
|
use std::process::exit;
|
||||||
|
|
||||||
|
mod fetch;
|
||||||
|
|
||||||
|
pub fn main() {
|
||||||
|
let matches = command!() // requires `cargo` feature
|
||||||
|
.subcommand_required(true)
|
||||||
|
.subcommand(Command::new("check").about("Check for updates from remote server"))
|
||||||
|
.subcommand(
|
||||||
|
Command::new("download")
|
||||||
|
.about("Download a specific file from the server")
|
||||||
|
.arg(arg!(-r --rootfs "rootfs component").action(ArgAction::SetTrue))
|
||||||
|
.arg(arg!(-k --kernel "kernel component").action(ArgAction::SetTrue))
|
||||||
|
.arg(arg!(-e --extra "extra component").action(ArgAction::SetTrue))
|
||||||
|
.arg_required_else_help(true),
|
||||||
|
)
|
||||||
|
.subcommand(
|
||||||
|
Command::new("read-remote")
|
||||||
|
.about("Read the remote server and print information on versions offered"),
|
||||||
|
)
|
||||||
|
.subcommand(
|
||||||
|
Command::new("upgrade")
|
||||||
|
.about("Download and install all components found on the server to be more recent than currently installed on system")
|
||||||
|
)
|
||||||
|
.subcommand(
|
||||||
|
Command::new("reinstall")
|
||||||
|
.about("Download and install a specific component even if the server's component version is not greater than currently installed")
|
||||||
|
.arg(arg!(-r --rootfs "rootfs component").action(ArgAction::SetTrue))
|
||||||
|
.arg(arg!(-k --kernel "kernel component").action(ArgAction::SetTrue))
|
||||||
|
.arg(arg!(-e --extra "extra component").action(ArgAction::SetTrue))
|
||||||
|
.arg_required_else_help(true),
|
||||||
|
)
|
||||||
|
.get_matches();
|
||||||
|
|
||||||
|
let result = match matches.subcommand() {
|
||||||
|
Some(("check", _sub_matches)) => fetch::check(),
|
||||||
|
Some(("download", sub_matches)) => fetch::download(sub_matches),
|
||||||
|
Some(("read-remote", _sub_matches)) => fetch::read_remote(),
|
||||||
|
Some(("upgrade", _sub_matches)) => fetch::upgrade(),
|
||||||
|
Some(("reinstall", sub_matches)) => fetch::reinstall(sub_matches),
|
||||||
|
_ => unreachable!("Please pass a subcommand"),
|
||||||
|
};
|
||||||
|
|
||||||
|
if let Err(ref e) = result {
|
||||||
|
println!("Error: {}", e);
|
||||||
|
exit(1);
|
||||||
|
}
|
||||||
|
}
|
@ -250,9 +250,9 @@ fn install_image(arg_matches: &ArgMatches) -> Result<()> {
|
|||||||
if kernel_version.chars().any(|c| c == '/') {
|
if kernel_version.chars().any(|c| c == '/') {
|
||||||
bail!("Kernel version field has / char");
|
bail!("Kernel version field has / char");
|
||||||
}
|
}
|
||||||
format!("citadel-kernel-{}-{:03}.img", kernel_version, metainfo.version())
|
format!("citadel-kernel-{}-{}.img", kernel_version, metainfo.version())
|
||||||
} else {
|
} else {
|
||||||
format!("citadel-extra-{:03}.img", metainfo.version())
|
format!("citadel-extra-{}.img", metainfo.version())
|
||||||
};
|
};
|
||||||
|
|
||||||
if !metainfo.channel().chars().all(|c| c.is_ascii_lowercase()) {
|
if !metainfo.channel().chars().all(|c| c.is_ascii_lowercase()) {
|
||||||
|
@ -16,6 +16,7 @@ mod mkimage;
|
|||||||
mod realmfs;
|
mod realmfs;
|
||||||
mod sync;
|
mod sync;
|
||||||
mod update;
|
mod update;
|
||||||
|
mod fetch;
|
||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
let exe = match env::current_exe() {
|
let exe = match env::current_exe() {
|
||||||
@ -39,6 +40,8 @@ fn main() {
|
|||||||
realmfs::main();
|
realmfs::main();
|
||||||
} else if exe == Path::new("/usr/bin/citadel-update") {
|
} else if exe == Path::new("/usr/bin/citadel-update") {
|
||||||
update::main(args);
|
update::main(args);
|
||||||
|
} else if exe == Path::new("/usr/bin/citadel-fetch") {
|
||||||
|
fetch::main();
|
||||||
} else if exe == Path::new("/usr/libexec/citadel-desktop-sync") {
|
} else if exe == Path::new("/usr/libexec/citadel-desktop-sync") {
|
||||||
sync::main(args);
|
sync::main(args);
|
||||||
} else if exe == Path::new("/usr/libexec/citadel-run") {
|
} else if exe == Path::new("/usr/libexec/citadel-run") {
|
||||||
|
@ -38,15 +38,15 @@ impl UpdateBuilder {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn target_filename(&self) -> String {
|
fn target_filename(&self) -> String {
|
||||||
format!("citadel-{}-{}-{:03}.img", self.config.img_name(), self.config.channel(), self.config.version())
|
format!("citadel-{}-{}-{:}.img", self.config.img_name(), self.config.channel(), self.config.version())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn build_filename(config: &BuildConfig) -> String {
|
fn build_filename(config: &BuildConfig) -> String {
|
||||||
format!("citadel-{}-{}-{:03}", config.image_type(), config.channel(), config.version())
|
format!("citadel-{}-{}-{}", config.image_type(), config.channel(), config.version())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn verity_filename(&self) -> String {
|
fn verity_filename(&self) -> String {
|
||||||
format!("verity-hash-{}-{:03}", self.config.image_type(), self.config.version())
|
format!("verity-hash-{}-{}", self.config.image_type(), self.config.version())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn build(&mut self) -> Result<()> {
|
pub fn build(&mut self) -> Result<()> {
|
||||||
@ -154,7 +154,7 @@ impl UpdateBuilder {
|
|||||||
bail!("failed to compress {:?}: {}", self.image(), err);
|
bail!("failed to compress {:?}: {}", self.image(), err);
|
||||||
}
|
}
|
||||||
// Rename back to original image_data filename
|
// Rename back to original image_data filename
|
||||||
util::rename(self.image().with_extension("xz"), self.image())?;
|
util::rename(util::append_to_path(self.image(), ".xz"), self.image())?;
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
@ -217,7 +217,7 @@ impl UpdateBuilder {
|
|||||||
writeln!(v, "realmfs-name = \"{}\"", name)?;
|
writeln!(v, "realmfs-name = \"{}\"", name)?;
|
||||||
}
|
}
|
||||||
writeln!(v, "channel = \"{}\"", self.config.channel())?;
|
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, "timestamp = \"{}\"", self.config.timestamp())?;
|
||||||
writeln!(v, "nblocks = {}", self.nblocks.unwrap())?;
|
writeln!(v, "nblocks = {}", self.nblocks.unwrap())?;
|
||||||
writeln!(v, "shasum = \"{}\"", self.shasum.as_ref().unwrap())?;
|
writeln!(v, "shasum = \"{}\"", self.shasum.as_ref().unwrap())?;
|
||||||
|
@ -9,7 +9,7 @@ pub struct BuildConfig {
|
|||||||
#[serde(rename = "image-type")]
|
#[serde(rename = "image-type")]
|
||||||
image_type: String,
|
image_type: String,
|
||||||
channel: String,
|
channel: String,
|
||||||
version: usize,
|
version: String,
|
||||||
timestamp: String,
|
timestamp: String,
|
||||||
source: String,
|
source: String,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
@ -102,8 +102,8 @@ impl BuildConfig {
|
|||||||
self.realmfs_name.as_ref().map(|s| s.as_str())
|
self.realmfs_name.as_ref().map(|s| s.as_str())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn version(&self) -> usize {
|
pub fn version(&self) -> &str {
|
||||||
self.version
|
&self.version
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn channel(&self) -> &str {
|
pub fn channel(&self) -> &str {
|
||||||
|
@ -93,7 +93,7 @@ fn create_tmp_copy(path: &Path) -> Result<PathBuf> {
|
|||||||
Ok(path)
|
Ok(path)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn install_image(path: &Path, flags: u32) -> Result<()> {
|
pub fn install_image(path: &Path, flags: u32) -> Result<()> {
|
||||||
if !path.exists() || path.file_name().is_none() {
|
if !path.exists() || path.file_name().is_none() {
|
||||||
bail!("file path {} does not exist", path.display());
|
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<()> {
|
fn install_extra_image(image: &ResourceImage) -> Result<()> {
|
||||||
let filename = format!("citadel-extra-{:03}.img", image.header().metainfo().version());
|
let filename = format!("citadel-extra-{}.img", image.header().metainfo().version());
|
||||||
install_image_file(image, filename.as_str())?;
|
install_image_file(image, filename.as_str())?;
|
||||||
remove_old_extra_images(image)?;
|
remove_old_extra_images(image)?;
|
||||||
Ok(())
|
Ok(())
|
||||||
@ -186,7 +186,7 @@ fn install_kernel_image(image: &mut ResourceImage) -> Result<()> {
|
|||||||
info!("kernel version is {}", kernel_version);
|
info!("kernel version is {}", kernel_version);
|
||||||
install_kernel_file(image, &kernel_version)?;
|
install_kernel_file(image, &kernel_version)?;
|
||||||
|
|
||||||
let filename = format!("citadel-kernel-{}-{:03}.img", kernel_version, version);
|
let filename = format!("citadel-kernel-{}-{}.img", kernel_version, version);
|
||||||
install_image_file(image, &filename)?;
|
install_image_file(image, &filename)?;
|
||||||
|
|
||||||
let all_versions = all_boot_kernel_versions()?;
|
let all_versions = all_boot_kernel_versions()?;
|
||||||
|
@ -20,6 +20,9 @@ walkdir = "2"
|
|||||||
dbus = "0.6"
|
dbus = "0.6"
|
||||||
posix-acl = "1.0.0"
|
posix-acl = "1.0.0"
|
||||||
procfs = "0.12.0"
|
procfs = "0.12.0"
|
||||||
|
semver = "1.0"
|
||||||
|
anyhow = "1.0"
|
||||||
|
clap = "4.5"
|
||||||
|
|
||||||
[dependencies.inotify]
|
[dependencies.inotify]
|
||||||
version = "0.8"
|
version = "0.8"
|
||||||
|
@ -453,7 +453,7 @@ pub struct MetaInfo {
|
|||||||
realmfs_owner: Option<String>,
|
realmfs_owner: Option<String>,
|
||||||
|
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
version: u32,
|
version: String,
|
||||||
|
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
timestamp: String,
|
timestamp: String,
|
||||||
@ -508,8 +508,8 @@ impl MetaInfo {
|
|||||||
Self::str_ref(&self.realmfs_owner)
|
Self::str_ref(&self.realmfs_owner)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn version(&self) -> u32 {
|
pub fn version(&self) -> &str {
|
||||||
self.version
|
&self.version
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn timestamp(&self) -> &str {
|
pub fn timestamp(&self) -> &str {
|
||||||
|
@ -20,6 +20,7 @@ pub mod symlink;
|
|||||||
mod realm;
|
mod realm;
|
||||||
pub mod terminal;
|
pub mod terminal;
|
||||||
mod system;
|
mod system;
|
||||||
|
pub mod updates;
|
||||||
|
|
||||||
pub mod flatpak;
|
pub mod flatpak;
|
||||||
|
|
||||||
|
@ -420,8 +420,11 @@ fn compare_images(a: Option<ResourceImage>, b: ResourceImage) -> Result<Resource
|
|||||||
None => return Ok(b),
|
None => return Ok(b),
|
||||||
};
|
};
|
||||||
|
|
||||||
let ver_a = a.metainfo().version();
|
let bind_a = a.metainfo();
|
||||||
let ver_b = b.metainfo().version();
|
let bind_b = b.metainfo();
|
||||||
|
|
||||||
|
let ver_a = bind_a.version();
|
||||||
|
let ver_b = bind_b.version();
|
||||||
|
|
||||||
if ver_a > ver_b {
|
if ver_a > ver_b {
|
||||||
Ok(a)
|
Ok(a)
|
||||||
|
97
libcitadel/src/updates.rs
Normal file
97
libcitadel/src/updates.rs
Normal file
@ -0,0 +1,97 @@
|
|||||||
|
use std::fmt;
|
||||||
|
use std::slice::Iter;
|
||||||
|
|
||||||
|
pub const UPDATE_SERVER_HOSTNAME: &str = "update.subgraph.com";
|
||||||
|
|
||||||
|
/// This struct embeds the CitadelVersion datastruct as well as the cryptographic validation of the that information
|
||||||
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
|
pub struct CryptoContainerFile {
|
||||||
|
pub serialized_citadel_version: Vec<u8>, // we serialize CitadelVersion
|
||||||
|
pub signature: String, // serialized CitadelVersion gets signed
|
||||||
|
pub signatory: String, // name of org or person who holds the key
|
||||||
|
}
|
||||||
|
|
||||||
|
/// This struct contains the entirety of the logical information needed to decide whether to update or not
|
||||||
|
#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord)]
|
||||||
|
pub struct CitadelVersionStruct {
|
||||||
|
pub client: String,
|
||||||
|
pub channel: String, // dev, prod ...
|
||||||
|
pub component_version: Vec<AvailableComponentVersion>,
|
||||||
|
pub publisher: String, // name of org or person who released this update
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::fmt::Display for CitadelVersionStruct {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
|
||||||
|
write!(
|
||||||
|
f,
|
||||||
|
"{} image with channel {} has components:\n",
|
||||||
|
self.client, self.channel
|
||||||
|
)?;
|
||||||
|
|
||||||
|
for i in &self.component_version {
|
||||||
|
write!(
|
||||||
|
f,
|
||||||
|
"\n{} with version {} at location {}",
|
||||||
|
i.component, i.version, i.file_path
|
||||||
|
)?;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize, Clone, Deserialize, PartialEq, Eq, Ord)]
|
||||||
|
pub struct AvailableComponentVersion {
|
||||||
|
pub component: Component, // rootfs, kernel or extra
|
||||||
|
pub version: String, // stored as semver
|
||||||
|
pub file_path: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PartialOrd for AvailableComponentVersion {
|
||||||
|
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
|
||||||
|
// absolutely require that the components be in the same order in all structs (rootfs, kernel, extra)
|
||||||
|
if &self.component != &other.component {
|
||||||
|
panic!("ComponentVersion comparison failed because comparing different components");
|
||||||
|
}
|
||||||
|
|
||||||
|
Some(
|
||||||
|
semver::Version::parse(&self.version)
|
||||||
|
.unwrap()
|
||||||
|
.cmp(&semver::Version::parse(&other.version).unwrap()),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::fmt::Display for AvailableComponentVersion {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
|
||||||
|
write!(
|
||||||
|
f,
|
||||||
|
"({} image has version: {})",
|
||||||
|
self.component, self.version
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Deserialize, Serialize, clap::ValueEnum)]
|
||||||
|
pub enum Component {
|
||||||
|
Rootfs,
|
||||||
|
Kernel,
|
||||||
|
Extra,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Component {
|
||||||
|
pub fn iterator() -> Iter<'static, Component> {
|
||||||
|
static COMPONENTS: [Component; 3] =
|
||||||
|
[Component::Rootfs, Component::Kernel, Component::Extra];
|
||||||
|
COMPONENTS.iter()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl fmt::Display for Component {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||||
|
match self {
|
||||||
|
Component::Rootfs => write!(f, "rootfs"),
|
||||||
|
Component::Kernel => write!(f, "kernel"),
|
||||||
|
&Component::Extra => write!(f, "extra"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -48,6 +48,12 @@ fn search_path(filename: &str) -> Result<PathBuf> {
|
|||||||
bail!("could not find {} in $PATH", filename)
|
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<()> {
|
pub fn ensure_command_exists(cmd: &str) -> Result<()> {
|
||||||
let path = Path::new(cmd);
|
let path = Path::new(cmd);
|
||||||
if !path.is_absolute() {
|
if !path.is_absolute() {
|
||||||
@ -338,7 +344,6 @@ pub fn is_euid_root() -> bool {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
fn utimes(path: &Path, atime: i64, mtime: i64) -> Result<()> {
|
fn utimes(path: &Path, atime: i64, mtime: i64) -> Result<()> {
|
||||||
let cstr = CString::new(path.as_os_str().as_bytes())
|
let cstr = CString::new(path.as_os_str().as_bytes())
|
||||||
.expect("path contains null byte");
|
.expect("path contains null byte");
|
||||||
|
21
update-generator/Cargo.toml
Normal file
21
update-generator/Cargo.toml
Normal 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"
|
63
update-generator/README.md
Normal file
63
update-generator/README.md
Normal 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
|
530
update-generator/src/main.rs
Normal file
530
update-generator/src/main.rs
Normal file
@ -0,0 +1,530 @@
|
|||||||
|
use anyhow::{Context, Result};
|
||||||
|
use clap::{Parser, Subcommand};
|
||||||
|
use ed25519_dalek::pkcs8::DecodePublicKey;
|
||||||
|
use ed25519_dalek::pkcs8::EncodePublicKey;
|
||||||
|
use ed25519_dalek::Signature;
|
||||||
|
use ed25519_dalek::Signer;
|
||||||
|
use ed25519_dalek::SigningKey;
|
||||||
|
use ed25519_dalek::VerifyingKey;
|
||||||
|
use ed25519_dalek::KEYPAIR_LENGTH;
|
||||||
|
use glob::glob;
|
||||||
|
use libcitadel::updates::Component;
|
||||||
|
use libcitadel::{updates, ImageHeader};
|
||||||
|
use rand::rngs::OsRng;
|
||||||
|
use sodiumoxide::crypto::pwhash;
|
||||||
|
use sodiumoxide::crypto::secretbox;
|
||||||
|
use sodiumoxide::crypto::stream;
|
||||||
|
use std::env;
|
||||||
|
use std::io::{Read, Write};
|
||||||
|
use std::path::Path;
|
||||||
|
use std::path::PathBuf;
|
||||||
|
use std::process::Command;
|
||||||
|
use std::str::FromStr;
|
||||||
|
use zeroize::Zeroize;
|
||||||
|
|
||||||
|
const SALSA_NONCE: &[u8] = &[
|
||||||
|
116, 138, 142, 103, 234, 105, 192, 48, 117, 29, 150, 214, 106, 116, 195, 64, 120, 251, 94, 20,
|
||||||
|
212, 118, 125, 189,
|
||||||
|
];
|
||||||
|
|
||||||
|
const PASSWORD_SALT: &[u8] = &[
|
||||||
|
18, 191, 168, 237, 156, 199, 54, 43, 122, 165, 35, 9, 89, 106, 36, 209, 145, 161, 90, 2, 121,
|
||||||
|
51, 242, 182, 14, 245, 47, 253, 237, 153, 251, 219,
|
||||||
|
];
|
||||||
|
|
||||||
|
const LAST_RESORT_CLIENT: &str = "public";
|
||||||
|
const LAST_RESORT_CHANNEL: &str = "prod";
|
||||||
|
|
||||||
|
#[derive(Parser)]
|
||||||
|
#[command(about = "Generate the file we use to inform clients of updates")]
|
||||||
|
#[command(author, about, long_about = None)]
|
||||||
|
struct Cli {
|
||||||
|
#[command(subcommand)]
|
||||||
|
command: Option<Commands>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Subcommand, Debug)]
|
||||||
|
#[command(arg_required_else_help = true)]
|
||||||
|
enum Commands {
|
||||||
|
/// Generate a keypair to be used to sign version file. You will be asked to provide a mandatory password.
|
||||||
|
GenerateKeypair {
|
||||||
|
/// keypair filepath to save to
|
||||||
|
#[arg(short, long)]
|
||||||
|
keypair_filepath: Option<String>,
|
||||||
|
},
|
||||||
|
|
||||||
|
/// Generate the complete cbor file. If no components are passed, generate by reading image of each component
|
||||||
|
CreateSignedFile {
|
||||||
|
/// rootfs image semver version
|
||||||
|
#[arg(short, long)]
|
||||||
|
rootfs_image_version: Option<String>,
|
||||||
|
/// kernel image semver version
|
||||||
|
#[arg(short, long)]
|
||||||
|
kernel_image_version: Option<String>,
|
||||||
|
#[arg(short, long)]
|
||||||
|
extra_image_version: Option<String>,
|
||||||
|
/// keypair filepath
|
||||||
|
#[arg(short, long, value_name = "FILE")]
|
||||||
|
path_keypair: Option<String>,
|
||||||
|
/// command output complete filepath
|
||||||
|
#[arg(short, long, value_name = "FILE")]
|
||||||
|
versionfile_filepath: Option<String>,
|
||||||
|
},
|
||||||
|
|
||||||
|
/// Verify that the version file is correctly signed
|
||||||
|
VerifySignature {
|
||||||
|
/// public key filepath
|
||||||
|
#[arg(short, long, value_name = "FILE")]
|
||||||
|
publickey_filepath: Option<String>,
|
||||||
|
/// command output complete filepath
|
||||||
|
#[arg(short, long, value_name = "FILE")]
|
||||||
|
versionfile_filepath: Option<String>,
|
||||||
|
},
|
||||||
|
|
||||||
|
UploadToServer {
|
||||||
|
#[arg(long)]
|
||||||
|
component: Option<updates::Component>,
|
||||||
|
path: Option<PathBuf>,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
fn main() -> Result<()> {
|
||||||
|
let cli = Cli::parse();
|
||||||
|
|
||||||
|
match &cli.command {
|
||||||
|
Some(Commands::GenerateKeypair { keypair_filepath }) => {
|
||||||
|
generate_keypair(keypair_filepath).context("Failed to generate keypair")?
|
||||||
|
}
|
||||||
|
Some(Commands::CreateSignedFile {
|
||||||
|
path_keypair,
|
||||||
|
rootfs_image_version,
|
||||||
|
kernel_image_version,
|
||||||
|
extra_image_version,
|
||||||
|
versionfile_filepath,
|
||||||
|
}) => create_signed_version_file(
|
||||||
|
path_keypair,
|
||||||
|
rootfs_image_version,
|
||||||
|
kernel_image_version,
|
||||||
|
extra_image_version,
|
||||||
|
versionfile_filepath,
|
||||||
|
)
|
||||||
|
.context("Failed to create signed file")?,
|
||||||
|
Some(Commands::VerifySignature {
|
||||||
|
publickey_filepath,
|
||||||
|
versionfile_filepath,
|
||||||
|
}) => verify_version_signature(publickey_filepath, versionfile_filepath)
|
||||||
|
.context("Failed to verify signature")?,
|
||||||
|
Some(Commands::UploadToServer { component, path }) => {
|
||||||
|
upload_components_to_server(component, path)
|
||||||
|
.context("Failed to upload to the server")?
|
||||||
|
}
|
||||||
|
None => {}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn generate_keypair(keypair_filepath: &Option<String>) -> Result<()> {
|
||||||
|
// if the user did not pass a path, we save key files to current directory
|
||||||
|
let keypair_filepath = &keypair_filepath.clone().unwrap_or_else(|| ".".to_string());
|
||||||
|
let path = std::path::Path::new(keypair_filepath);
|
||||||
|
|
||||||
|
let mut password;
|
||||||
|
|
||||||
|
loop {
|
||||||
|
// get passphrase used to encrypt key from user
|
||||||
|
password = rpassword::prompt_password(
|
||||||
|
"Please enter the passphrase we will use to encrypt the private key with: ",
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let password_confirm = rpassword::prompt_password("Retype same passphrase: ").unwrap();
|
||||||
|
|
||||||
|
if password != password_confirm {
|
||||||
|
println!("\nPassphrases did not match. Please try again")
|
||||||
|
} else {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// generate keypair
|
||||||
|
let mut csprng = OsRng;
|
||||||
|
let signing_key: SigningKey = SigningKey::generate(&mut csprng);
|
||||||
|
|
||||||
|
let keypair_fp: PathBuf;
|
||||||
|
let publickey_fp: PathBuf;
|
||||||
|
|
||||||
|
if path.is_dir() {
|
||||||
|
keypair_fp = path.join("keypair.priv");
|
||||||
|
publickey_fp = path.join("update_server_key.pub");
|
||||||
|
} else {
|
||||||
|
keypair_fp = path.to_path_buf();
|
||||||
|
publickey_fp = path.parent().unwrap().join("update_server_key.pub");
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut keyfile = std::fs::File::create(&keypair_fp)?;
|
||||||
|
let mut public_key = std::fs::File::create(&publickey_fp)?;
|
||||||
|
|
||||||
|
// encrypt private key
|
||||||
|
let mut k = secretbox::Key([0; secretbox::KEYBYTES]);
|
||||||
|
|
||||||
|
let secretbox::Key(ref mut kb) = k;
|
||||||
|
let password_hash = pwhash::derive_key(
|
||||||
|
kb,
|
||||||
|
password.as_bytes(),
|
||||||
|
&sodiumoxide::crypto::pwhash::scryptsalsa208sha256::Salt::from_slice(PASSWORD_SALT)
|
||||||
|
.unwrap(),
|
||||||
|
pwhash::OPSLIMIT_INTERACTIVE,
|
||||||
|
pwhash::MEMLIMIT_INTERACTIVE,
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let plaintext = signing_key.to_keypair_bytes();
|
||||||
|
let key = sodiumoxide::crypto::stream::xsalsa20::Key::from_slice(password_hash)
|
||||||
|
.expect("failed to unwrap key");
|
||||||
|
let nonce = sodiumoxide::crypto::stream::xsalsa20::Nonce::from_slice(SALSA_NONCE)
|
||||||
|
.expect("failed to unwrap nonce");
|
||||||
|
|
||||||
|
// encrypt the plaintext
|
||||||
|
let ciphertext = stream::stream_xor(&plaintext, &nonce, &key);
|
||||||
|
|
||||||
|
keyfile.write_all(&ciphertext)?;
|
||||||
|
public_key.write_all(
|
||||||
|
&signing_key
|
||||||
|
.verifying_key()
|
||||||
|
.to_public_key_pem(ed25519_dalek::pkcs8::spki::der::pem::LineEnding::LF)
|
||||||
|
.unwrap()
|
||||||
|
.as_bytes(),
|
||||||
|
)?;
|
||||||
|
|
||||||
|
password.zeroize();
|
||||||
|
|
||||||
|
println!(
|
||||||
|
"Generated the keypair and wrote to {}. The public key is here: {}",
|
||||||
|
keypair_fp.display(),
|
||||||
|
publickey_fp.display()
|
||||||
|
);
|
||||||
|
println!(
|
||||||
|
"You may now move the public key from {} to the citadel build path at citadel/meta-citadel/recipes-citadel/citadel-config/files/citadel-fetch/update_server_key.pub",
|
||||||
|
publickey_fp.display()
|
||||||
|
);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn create_signed_version_file(
|
||||||
|
signing_key_path: &Option<String>,
|
||||||
|
citadel_rootfs_version: &Option<String>,
|
||||||
|
citadel_kernel_version: &Option<String>,
|
||||||
|
citadel_extra_version: &Option<String>,
|
||||||
|
versionfile_filepath: &Option<String>,
|
||||||
|
) -> Result<()> {
|
||||||
|
let rootfs_version = match citadel_rootfs_version {
|
||||||
|
Some(v) => semver::Version::parse(v)
|
||||||
|
.expect("Error: Failed to parse rootfs semver version")
|
||||||
|
.to_string(),
|
||||||
|
None => get_imageheader_version(&Component::Rootfs).unwrap_or(String::from("0.0.0")),
|
||||||
|
};
|
||||||
|
|
||||||
|
let kernel_version = match citadel_kernel_version {
|
||||||
|
Some(v) => semver::Version::parse(v)
|
||||||
|
.expect("Error: Failed to parse kernel semver version")
|
||||||
|
.to_string(),
|
||||||
|
None => get_imageheader_version(&Component::Kernel).unwrap_or(String::from("0.0.0")),
|
||||||
|
};
|
||||||
|
|
||||||
|
let extra_version = match citadel_extra_version {
|
||||||
|
Some(v) => semver::Version::parse(v)
|
||||||
|
.expect("Error: Failed to parse extra semver version")
|
||||||
|
.to_string(),
|
||||||
|
None => get_imageheader_version(&Component::Extra).unwrap_or(String::from("0.0.0")),
|
||||||
|
};
|
||||||
|
|
||||||
|
let rootfs_path = get_component_path(&Component::Rootfs);
|
||||||
|
let channel;
|
||||||
|
|
||||||
|
if rootfs_path.is_ok() {
|
||||||
|
channel = match ImageHeader::from_file(rootfs_path?) {
|
||||||
|
Ok(image_header) => image_header.metainfo().channel().to_string(),
|
||||||
|
Err(_) => env::var("UPDATES_CHANNEL").unwrap_or(LAST_RESORT_CHANNEL.to_string()),
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
channel = env::var("UPDATES_CHANNEL").unwrap_or(LAST_RESORT_CHANNEL.to_string());
|
||||||
|
}
|
||||||
|
|
||||||
|
let component_version_vector = vec![
|
||||||
|
updates::AvailableComponentVersion {
|
||||||
|
component: updates::Component::Rootfs,
|
||||||
|
version: rootfs_version.clone(),
|
||||||
|
file_path: format!(
|
||||||
|
"{}/{}/{}_{}.img",
|
||||||
|
env::var("UPDATES_CLIENT").unwrap_or(LAST_RESORT_CLIENT.to_string()),
|
||||||
|
channel,
|
||||||
|
"rootfs",
|
||||||
|
rootfs_version
|
||||||
|
)
|
||||||
|
.to_string(),
|
||||||
|
},
|
||||||
|
updates::AvailableComponentVersion {
|
||||||
|
component: updates::Component::Kernel,
|
||||||
|
version: kernel_version.clone(),
|
||||||
|
file_path: format!(
|
||||||
|
"{}/{}/{}_{}.img",
|
||||||
|
env::var("UPDATES_CLIENT").unwrap_or(LAST_RESORT_CLIENT.to_string()),
|
||||||
|
channel,
|
||||||
|
"kernel",
|
||||||
|
kernel_version
|
||||||
|
)
|
||||||
|
.to_string(),
|
||||||
|
},
|
||||||
|
updates::AvailableComponentVersion {
|
||||||
|
component: updates::Component::Extra,
|
||||||
|
version: extra_version.clone(),
|
||||||
|
file_path: format!(
|
||||||
|
"{}/{}/{}_{}.img",
|
||||||
|
env::var("UPDATES_CLIENT").unwrap_or(LAST_RESORT_CLIENT.to_string()),
|
||||||
|
channel,
|
||||||
|
"extra",
|
||||||
|
extra_version
|
||||||
|
)
|
||||||
|
.to_string(),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
// generate version file
|
||||||
|
let citadel_version = updates::CitadelVersionStruct {
|
||||||
|
client: env::var("UPDATES_CLIENT").unwrap_or(LAST_RESORT_CLIENT.to_string()),
|
||||||
|
channel: channel.to_string(),
|
||||||
|
component_version: component_version_vector,
|
||||||
|
publisher: "Subgraph".to_string(),
|
||||||
|
};
|
||||||
|
|
||||||
|
let fp = match signing_key_path {
|
||||||
|
Some(fp) => Path::new(fp),
|
||||||
|
None => Path::new("keypair.priv"),
|
||||||
|
};
|
||||||
|
|
||||||
|
// serialized to cbor
|
||||||
|
let serialized_citadel_version = serde_cbor::to_vec(&citadel_version)?;
|
||||||
|
|
||||||
|
// get signing_key_bytes from the file passed
|
||||||
|
let mut keyfile = std::fs::File::open(&fp)?;
|
||||||
|
let mut buf = [0; KEYPAIR_LENGTH];
|
||||||
|
keyfile.read_exact(&mut buf)?;
|
||||||
|
|
||||||
|
// prompt user for keypair decryption password
|
||||||
|
let mut password =
|
||||||
|
rpassword::prompt_password("Please enter the passphrase used to decrypt the private key: ")
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
// decrypt private key
|
||||||
|
let mut k = secretbox::Key([0; secretbox::KEYBYTES]);
|
||||||
|
|
||||||
|
let secretbox::Key(ref mut kb) = k;
|
||||||
|
let password_hash = pwhash::derive_key(
|
||||||
|
kb,
|
||||||
|
password.as_bytes(),
|
||||||
|
&sodiumoxide::crypto::pwhash::scryptsalsa208sha256::Salt::from_slice(PASSWORD_SALT)
|
||||||
|
.unwrap(),
|
||||||
|
pwhash::OPSLIMIT_INTERACTIVE,
|
||||||
|
pwhash::MEMLIMIT_INTERACTIVE,
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let key = sodiumoxide::crypto::stream::xsalsa20::Key::from_slice(password_hash)
|
||||||
|
.expect("failed to unwrap key");
|
||||||
|
let nonce = sodiumoxide::crypto::stream::xsalsa20::Nonce::from_slice(SALSA_NONCE)
|
||||||
|
.expect("failed to unwrap nonce");
|
||||||
|
|
||||||
|
// decrypt the ciphertext
|
||||||
|
let plaintext = stream::stream_xor(&buf, &nonce, &key);
|
||||||
|
|
||||||
|
let signing_key = SigningKey::from_keypair_bytes(plaintext[0..64].try_into()?)?;
|
||||||
|
|
||||||
|
// sign serialized_citadel_version for inclusion in version_file
|
||||||
|
let signature: Signature = signing_key.sign(&serialized_citadel_version);
|
||||||
|
|
||||||
|
// generate signature of citadel_version cbor format (signed)
|
||||||
|
let version_file = updates::CryptoContainerFile {
|
||||||
|
serialized_citadel_version,
|
||||||
|
signature: signature.to_string(),
|
||||||
|
signatory: "Subgraph".to_string(),
|
||||||
|
};
|
||||||
|
|
||||||
|
let vf_fp = match versionfile_filepath {
|
||||||
|
Some(vf_fp) => {
|
||||||
|
if Path::new(vf_fp).is_dir() {
|
||||||
|
Path::new(vf_fp).join("version.cbor")
|
||||||
|
} else {
|
||||||
|
Path::new(vf_fp).to_path_buf()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None => Path::new("version.cbor").to_path_buf(),
|
||||||
|
};
|
||||||
|
|
||||||
|
let outfile = std::fs::File::create(&vf_fp)?;
|
||||||
|
|
||||||
|
serde_cbor::to_writer(outfile, &version_file)?;
|
||||||
|
|
||||||
|
password.zeroize();
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Validate that the completed version file correctly verifies given the self-embedded signature and public key
|
||||||
|
fn verify_version_signature(
|
||||||
|
pubkey_filepath: &Option<String>,
|
||||||
|
versionfile_filepath: &Option<String>,
|
||||||
|
) -> Result<()> {
|
||||||
|
let pub_fp = match pubkey_filepath {
|
||||||
|
Some(pub_fp) => Path::new(pub_fp),
|
||||||
|
None => Path::new("update_server_key.pub"),
|
||||||
|
};
|
||||||
|
|
||||||
|
let version_fp = match versionfile_filepath {
|
||||||
|
Some(version_fp) => Path::new(version_fp),
|
||||||
|
None => Path::new("version.cbor"),
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut pubkey_file = std::fs::File::open(&pub_fp)?;
|
||||||
|
|
||||||
|
let mut buf = String::new();
|
||||||
|
pubkey_file.read_to_string(&mut buf)?;
|
||||||
|
|
||||||
|
let verifying_key = VerifyingKey::from_public_key_pem(&buf)?;
|
||||||
|
|
||||||
|
let version_file = &std::fs::File::open(version_fp)?;
|
||||||
|
let crypto_container_struct: updates::CryptoContainerFile =
|
||||||
|
serde_cbor::from_reader(version_file)?;
|
||||||
|
|
||||||
|
let citadel_version_struct: updates::CitadelVersionStruct =
|
||||||
|
serde_cbor::from_slice(&crypto_container_struct.serialized_citadel_version)?;
|
||||||
|
|
||||||
|
let signature = ed25519_dalek::Signature::from_str(&crypto_container_struct.signature)?;
|
||||||
|
|
||||||
|
match verifying_key.verify_strict(
|
||||||
|
&crypto_container_struct.serialized_citadel_version,
|
||||||
|
&signature,
|
||||||
|
) {
|
||||||
|
Ok(_) => println!("Everythin ok. Signature verified correctly"),
|
||||||
|
Err(e) => panic!(
|
||||||
|
"Error: Signature was not able to be verified! Threw error: {}",
|
||||||
|
e
|
||||||
|
),
|
||||||
|
}
|
||||||
|
|
||||||
|
println!(
|
||||||
|
"The destructured version file contains the following information:\n{}",
|
||||||
|
citadel_version_struct
|
||||||
|
);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
// Make sure to add your ssh key to the "updates" user on the server
|
||||||
|
fn send_with_scp(from: &PathBuf, to: &PathBuf) -> Result<()> {
|
||||||
|
Command::new("scp")
|
||||||
|
.arg("-P 22514")
|
||||||
|
.arg(from)
|
||||||
|
.arg(format!(
|
||||||
|
"updates@{}:/updates/files/{}",
|
||||||
|
updates::UPDATE_SERVER_HOSTNAME,
|
||||||
|
to.to_string_lossy()
|
||||||
|
))
|
||||||
|
.spawn()
|
||||||
|
.context("scp command failed to run")?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_imageheader_version(component: &Component) -> Result<String> {
|
||||||
|
let version = match ImageHeader::from_file(get_component_path(component)?) {
|
||||||
|
Ok(image_header) => image_header.metainfo().version().to_string(),
|
||||||
|
Err(_) => String::from("0.0.0"),
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(version)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_component_path(component: &updates::Component) -> Result<PathBuf> {
|
||||||
|
let mut gl = match component {
|
||||||
|
Component::Rootfs => glob("../build/images/citadel-rootfs*.img")?,
|
||||||
|
Component::Kernel => glob("../build/images/citadel-kernel*.img")?,
|
||||||
|
Component::Extra => glob("../build/images/citadel-extra*.img")?,
|
||||||
|
};
|
||||||
|
|
||||||
|
let first = gl.nth(0).context(format!(
|
||||||
|
"Failed to find citadel-{}*.img in ../build/images/",
|
||||||
|
component
|
||||||
|
))?;
|
||||||
|
|
||||||
|
Ok(PathBuf::from(first?))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn upload_all_components() -> Result<()> {
|
||||||
|
for component in updates::Component::iterator() {
|
||||||
|
upload_component(component, &None)?
|
||||||
|
}
|
||||||
|
upload_cbor()?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn upload_component(component: &updates::Component, path: &Option<PathBuf>) -> Result<()> {
|
||||||
|
let final_path;
|
||||||
|
|
||||||
|
// uggliest if statement i've ever written
|
||||||
|
// if path was passed to this function
|
||||||
|
if let Some(p) = path {
|
||||||
|
final_path = p.to_path_buf();
|
||||||
|
} else {
|
||||||
|
// if the path wasn't passed to this function, search for the component path
|
||||||
|
if let Ok(p) = get_component_path(&component) {
|
||||||
|
final_path = p;
|
||||||
|
} else {
|
||||||
|
// if path was not passed and we failed to locate the component's path
|
||||||
|
println!(
|
||||||
|
"We failed to find the {} image we were looking for... Skipping...",
|
||||||
|
component
|
||||||
|
);
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let image_header = libcitadel::ImageHeader::from_file(&final_path)?;
|
||||||
|
|
||||||
|
let to = PathBuf::from(format!(
|
||||||
|
"{}/{}/{}_{}.img",
|
||||||
|
env::var("UPDATES_CLIENT").unwrap_or(LAST_RESORT_CLIENT.to_string()),
|
||||||
|
env::var("UPDATES_CHANNEL").unwrap_or(LAST_RESORT_CHANNEL.to_string()),
|
||||||
|
component,
|
||||||
|
image_header.metainfo().version()
|
||||||
|
));
|
||||||
|
send_with_scp(&final_path, &to)
|
||||||
|
}
|
||||||
|
|
||||||
|
// If no parameters are passed to this command, upload all components regardless of version on the server
|
||||||
|
// If a path is passed, upload that image to the server regardless version.
|
||||||
|
fn upload_components_to_server(
|
||||||
|
component: &Option<updates::Component>,
|
||||||
|
path: &Option<PathBuf>,
|
||||||
|
) -> Result<()> {
|
||||||
|
// check if a component is passed to this function:
|
||||||
|
match component {
|
||||||
|
Some(comp) => upload_component(comp, path)?, // This function handles the option of not passing a path
|
||||||
|
None => upload_all_components()?,
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn upload_cbor() -> Result<()> {
|
||||||
|
send_with_scp(
|
||||||
|
&PathBuf::from("version.cbor"),
|
||||||
|
&PathBuf::from(format!(
|
||||||
|
"{}/{}/version.cbor",
|
||||||
|
env::var("UPDATES_CLIENT").unwrap_or(LAST_RESORT_CLIENT.to_string()),
|
||||||
|
env::var("UPDATES_CHANNEL").unwrap_or(LAST_RESORT_CHANNEL.to_string()),
|
||||||
|
)),
|
||||||
|
)
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user