291 lines
9.1 KiB
Rust
291 lines
9.1 KiB
Rust
#[macro_use] extern crate failure;
|
|
#[macro_use] extern crate nix;
|
|
#[macro_use] extern crate serde_derive;
|
|
|
|
|
|
extern crate libc;
|
|
extern crate clap;
|
|
extern crate serde;
|
|
extern crate toml;
|
|
extern crate ed25519_dalek;
|
|
extern crate sha2;
|
|
extern crate rand;
|
|
extern crate rustc_serialize;
|
|
|
|
|
|
thread_local! {
|
|
pub static VERBOSE: RefCell<bool> = RefCell::new(false);
|
|
pub static SYSOP: RefCell<bool> = RefCell::new(false);
|
|
}
|
|
|
|
pub fn verbose() -> bool {
|
|
VERBOSE.with(|f| {
|
|
*f.borrow()
|
|
})
|
|
}
|
|
|
|
fn sysop() -> bool {
|
|
SYSOP.with(|f| {
|
|
*f.borrow()
|
|
})
|
|
}
|
|
|
|
macro_rules! info {
|
|
($e:expr) => { if ::verbose() { println!("[+] {}", $e);} };
|
|
($fmt:expr, $($arg:tt)+) => { if ::verbose() { println!("[+] {}", format!($fmt, $($arg)+));} };
|
|
}
|
|
macro_rules! warn {
|
|
($e:expr) => { if ::verbose() { println!("WARNING: {}", $e);} };
|
|
($fmt:expr, $($arg:tt)+) => { if ::verbose() { println!("WARNING: {}", format!($fmt, $($arg)+));} };
|
|
}
|
|
|
|
macro_rules! notify {
|
|
($e:expr) => { println!("[+] {}", $e); };
|
|
($fmt:expr, $($arg:tt)+) => { println!("[+] {}", format!($fmt, $($arg)+)); };
|
|
}
|
|
|
|
use std::result;
|
|
use std::process::exit;
|
|
|
|
use failure::Error;
|
|
use clap::{App,Arg,ArgMatches, SubCommand};
|
|
use clap::AppSettings::*;
|
|
use sha2::Sha512;
|
|
use rand::OsRng;
|
|
use ed25519_dalek::Keypair;
|
|
use rustc_serialize::hex::ToHex;
|
|
use std::cell::RefCell;
|
|
use std::env;
|
|
|
|
|
|
pub use config::Config;
|
|
pub use metainfo::Metainfo;
|
|
pub use blockdev::BlockDev;
|
|
pub use partition::{Partition,MAX_METAINFO_LEN};
|
|
use unpacker::UpdateImageUnpacker;
|
|
use packer::UpdateImagePacker;
|
|
use boot::BootSelection;
|
|
|
|
mod boot;
|
|
mod metainfo;
|
|
mod partition;
|
|
mod blockdev;
|
|
mod config;
|
|
mod packer;
|
|
mod unpacker;
|
|
mod util;
|
|
|
|
pub type Result<T> = result::Result<T,Error>;
|
|
|
|
fn main() {
|
|
match env::var("CITADEL_SYSOP") {
|
|
Ok(_) => SYSOP.with(|f| *f.borrow_mut() = true),
|
|
_ => {},
|
|
};
|
|
|
|
let mut app = App::new("citadel-rootfs")
|
|
.about("Subgraph Citadel rootfs partition management")
|
|
.settings(&[ArgRequiredElseHelp, ColoredHelp, DisableHelpSubcommand, DisableVersion, DeriveDisplayOrder])
|
|
.arg(Arg::with_name("v")
|
|
.help("Verbose output")
|
|
.short("v")
|
|
.long("verbose"))
|
|
.arg(Arg::with_name("config")
|
|
.help("Optionally specify an alternate config file")
|
|
.takes_value(true)
|
|
.short("c") .long("config"))
|
|
|
|
.subcommand(SubCommand::with_name("list")
|
|
.about("Show information about all rootfs partitions"))
|
|
|
|
.subcommand(SubCommand::with_name("which-boot")
|
|
.about("Show which rootfs paritition would currently boot according to the boot selection algorithm"))
|
|
|
|
.subcommand(SubCommand::with_name("verify-update")
|
|
.about("Verify the signature of an update image"))
|
|
|
|
.subcommand(SubCommand::with_name("update")
|
|
.about("Download update if available and install")
|
|
.arg(Arg::with_name("download-only")
|
|
.help("Only download available update, don't install")
|
|
.long("download")))
|
|
|
|
.subcommand(SubCommand::with_name("install-update")
|
|
.about("Install an update image")
|
|
.arg(Arg::with_name("image")
|
|
.required(true)));
|
|
if sysop() {
|
|
|
|
app = app.subcommand(SubCommand::with_name("build-update")
|
|
.about("Create an update image from a raw citadel-image.ext2 file")
|
|
.arg(Arg::with_name("image")
|
|
.required(true)))
|
|
|
|
.subcommand(SubCommand::with_name("genkeys")
|
|
.about("Generate a new update keypair"));
|
|
|
|
}
|
|
|
|
let matches = app.get_matches();
|
|
|
|
|
|
let config = load_config(&matches);
|
|
|
|
if matches.is_present("v") {
|
|
VERBOSE.with(|f| *f.borrow_mut() = true);
|
|
}
|
|
|
|
let result = match matches.subcommand() {
|
|
("list", Some(_)) => list_cmd(&config),
|
|
("which-boot", Some(_)) => { which_boot_cmd(&config)},
|
|
("update", Some(m)) => update_cmd(&config, m),
|
|
("verify-update", Some(m)) => verify_update_cmd(&config, m),
|
|
("install-update", Some(m)) => install_update_cmd(&config, m),
|
|
("build-update", Some(m)) => build_update_cmd(&config, m),
|
|
("genkeys", Some(_)) => genkeys_cmd(),
|
|
("mount-rootfs", Some(m)) => mount_rootfs_cmd(&config, m),
|
|
(s, Some(_)) => {info!("subcommand: {}", s); Ok(())},
|
|
_ => Ok(()),
|
|
};
|
|
|
|
if let Err(e) = result {
|
|
println!("{}", e);
|
|
exit(1);
|
|
}
|
|
}
|
|
|
|
fn load_config(arg_matches: &ArgMatches) -> Config {
|
|
let config_load = match arg_matches.value_of("config") {
|
|
Some(path) => Config::load(path),
|
|
None => Config::load_default(),
|
|
};
|
|
match config_load {
|
|
Ok(config) => config,
|
|
Err(e) => {
|
|
println!("{}", e);
|
|
exit(1);
|
|
}
|
|
}
|
|
}
|
|
|
|
fn list_cmd(_config: &Config) -> Result<()> {
|
|
println!("{:^30} {:^14} {:^8} {:^8} {:^12}", "DEVICE PATH", "MOUNTED", "CHANNEL", "VERSION", "STATUS");
|
|
for p in Partition::rootfs_partitions()? {
|
|
let info = partition_info(&p);
|
|
println!("{:^30} {:^14} {:^8} {:^8} {:^12}", info.0, info.1, info.2, info.3, info.4);
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
|
|
fn partition_info(part: &Partition) -> (String,String,String,String,String) {
|
|
let mounted = if part.is_mounted() { "[Yes]".to_string() } else { String::new() };
|
|
let status = if part.is_initialized() { part.status_label() } else { "Not Initialized".to_string() };
|
|
let (channel, version) = match part.metainfo() {
|
|
Ok(meta) => (meta.channel().to_string(), meta.version().to_string()),
|
|
_ => (String::new(), String::new()),
|
|
};
|
|
(part.path_str().to_string(), mounted, channel, version, status)
|
|
}
|
|
|
|
fn mount_rootfs_cmd(_config: &Config, _matches: &ArgMatches) -> Result<()> {
|
|
// mounting installer rootfs should happen here too?
|
|
// perhaps based on cmd line flag
|
|
// maybe define guid like the gpt generator looks for
|
|
// for: rootfs inside luks, rootfs outside luks
|
|
let bs = BootSelection::load_partitions()?;
|
|
let _p = match bs.choose_boot_partition() {
|
|
Some(p) => p,
|
|
None => bail!("None of the rootfs partitions have a bootable image"),
|
|
};
|
|
Ok(())
|
|
}
|
|
|
|
fn which_boot_cmd(_config: &Config) -> Result<()> {
|
|
let select = BootSelection::load_partitions()?;
|
|
match select.choose_boot_partition() {
|
|
Some(part) => {
|
|
notify!("Next boot will be from partition: {}", part.path_str());
|
|
},
|
|
None => {
|
|
warn!("None of the rootfs partitions are currently in bootable state.");
|
|
warn!("Unless a valid image is installed, computer will fail to boot");
|
|
}
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
fn verify_update_cmd(_config: &Config, _matches: &ArgMatches) -> Result<()> {
|
|
println!("do verify_update");
|
|
Ok(())
|
|
}
|
|
|
|
fn update_cmd(_config: &Config, _matches: &ArgMatches) -> Result<()> {
|
|
// note to self, remember to verify that downloaded update image version matches the version
|
|
// which is expected.
|
|
Ok(())
|
|
|
|
}
|
|
|
|
fn install_update_cmd(config: &Config, matches: &ArgMatches) -> Result<()> {
|
|
let image_path = match matches.value_of("image") {
|
|
Some(val) => val,
|
|
None => bail!("install-update requires an image path"),
|
|
};
|
|
let unpack = UpdateImageUnpacker::open(image_path, config)?;
|
|
info!("unpacking image channel: {} version: {}", unpack.metainfo().channel(), unpack.metainfo().version());
|
|
unpack.unpack_disk_image()?;
|
|
info!("decompressing image");
|
|
unpack.decompress_disk_image()?;
|
|
info!("verifying shasum");
|
|
unpack.verify_shasum()?;
|
|
|
|
let bs = BootSelection::load_partitions()?;
|
|
let p = match bs.choose_install_partition() {
|
|
Some(p) => p,
|
|
None => bail!("None of the rootfs partitions are available to install update to"),
|
|
};
|
|
info!("installing to {}", p.path_str());
|
|
unpack.write_partition(p)?;
|
|
notify!("Update image successfully installed to {}", p.path_str());
|
|
|
|
|
|
//update::UpdateImage::new(path);
|
|
// 1) read header, extract metainfo
|
|
// 2) verify signature on metainfo
|
|
// 3) determine if this version/channel makes sense to be installed
|
|
// 4) xtrat image data to temporary file with https://crates.io/crates/lzma-rs
|
|
// 5) verify sha256 on image data
|
|
//
|
|
// 6) choose rootfs partition
|
|
// 7) write partition info block, setting status to INVALID
|
|
// 8) write update image to device
|
|
// 9) run verifyupdate using provided --salt
|
|
// 10) re-rewrite parition info block with status NEW
|
|
Ok(())
|
|
}
|
|
|
|
fn build_update_cmd(config: &Config, matches: &ArgMatches) -> Result<()> {
|
|
let image_path = match matches.value_of("image") {
|
|
Some(val) => val,
|
|
None => bail!("build-update requires an image path"),
|
|
};
|
|
let channel = match config.get_default_channel() {
|
|
Some(ch) => ch,
|
|
None => bail!("Could not determine default channel from config file"),
|
|
};
|
|
let mut builder = UpdateImagePacker::new(config, channel, image_path)?;
|
|
builder.build()?;
|
|
Ok(())
|
|
}
|
|
|
|
fn genkeys_cmd() -> Result<()> {
|
|
let mut rng = OsRng::new()?;
|
|
let keypair = Keypair::generate::<Sha512>(&mut rng);
|
|
|
|
println!("pubkey = \"{}\"", keypair.public.to_bytes().to_hex());
|
|
println!("privkey = \"{}\"", keypair.to_bytes().to_hex());
|
|
|
|
Ok(())
|
|
}
|