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(())
}