From 73b7878ca03e1f1571e4ffe824197664d13bea71 Mon Sep 17 00:00:00 2001 From: Bruce Leidl Date: Tue, 11 Dec 2018 11:19:12 -0500 Subject: [PATCH] initial commit --- .gitignore | 2 + Cargo.lock | 270 ++++++++++++++++++++++++++++++++++++++++++++++++++ Cargo.toml | 12 +++ src/build.rs | 227 ++++++++++++++++++++++++++++++++++++++++++ src/config.rs | 97 ++++++++++++++++++ src/main.rs | 182 ++++++++++++++++++++++++++++++++++ src/util.rs | 112 +++++++++++++++++++++ 7 files changed, 902 insertions(+) create mode 100644 .gitignore create mode 100644 Cargo.lock create mode 100644 Cargo.toml create mode 100644 src/build.rs create mode 100644 src/config.rs create mode 100644 src/main.rs create mode 100644 src/util.rs diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..53eaa21 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +/target +**/*.rs.bk diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..a2d9cc5 --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,270 @@ +[[package]] +name = "ansi_term" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "winapi 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "atty" +version = "0.2.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "libc 0.2.45 (registry+https://github.com/rust-lang/crates.io-index)", + "termion 1.5.1 (registry+https://github.com/rust-lang/crates.io-index)", + "winapi 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "backtrace" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "backtrace-sys 0.1.24 (registry+https://github.com/rust-lang/crates.io-index)", + "cfg-if 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)", + "libc 0.2.45 (registry+https://github.com/rust-lang/crates.io-index)", + "rustc-demangle 0.1.9 (registry+https://github.com/rust-lang/crates.io-index)", + "winapi 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "backtrace-sys" +version = "0.1.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "cc 1.0.25 (registry+https://github.com/rust-lang/crates.io-index)", + "libc 0.2.45 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "bitflags" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "cc" +version = "1.0.25" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "cfg-if" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "citadel-mkimage" +version = "0.1.0" +dependencies = [ + "clap 2.32.0 (registry+https://github.com/rust-lang/crates.io-index)", + "failure 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)", + "serde 1.0.82 (registry+https://github.com/rust-lang/crates.io-index)", + "serde_derive 1.0.82 (registry+https://github.com/rust-lang/crates.io-index)", + "toml 0.4.10 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "clap" +version = "2.32.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "ansi_term 0.11.0 (registry+https://github.com/rust-lang/crates.io-index)", + "atty 0.2.11 (registry+https://github.com/rust-lang/crates.io-index)", + "bitflags 1.0.4 (registry+https://github.com/rust-lang/crates.io-index)", + "strsim 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)", + "textwrap 0.10.0 (registry+https://github.com/rust-lang/crates.io-index)", + "unicode-width 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)", + "vec_map 0.8.1 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "failure" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "backtrace 0.3.9 (registry+https://github.com/rust-lang/crates.io-index)", + "failure_derive 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "failure_derive" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "proc-macro2 0.4.24 (registry+https://github.com/rust-lang/crates.io-index)", + "quote 0.6.10 (registry+https://github.com/rust-lang/crates.io-index)", + "syn 0.15.22 (registry+https://github.com/rust-lang/crates.io-index)", + "synstructure 0.10.1 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "libc" +version = "0.2.45" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "proc-macro2" +version = "0.4.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "unicode-xid 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "quote" +version = "0.6.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "proc-macro2 0.4.24 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "redox_syscall" +version = "0.1.43" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "redox_termios" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "redox_syscall 0.1.43 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "rustc-demangle" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "serde" +version = "1.0.82" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "serde_derive" +version = "1.0.82" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "proc-macro2 0.4.24 (registry+https://github.com/rust-lang/crates.io-index)", + "quote 0.6.10 (registry+https://github.com/rust-lang/crates.io-index)", + "syn 0.15.22 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "strsim" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "syn" +version = "0.15.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "proc-macro2 0.4.24 (registry+https://github.com/rust-lang/crates.io-index)", + "quote 0.6.10 (registry+https://github.com/rust-lang/crates.io-index)", + "unicode-xid 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "synstructure" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "proc-macro2 0.4.24 (registry+https://github.com/rust-lang/crates.io-index)", + "quote 0.6.10 (registry+https://github.com/rust-lang/crates.io-index)", + "syn 0.15.22 (registry+https://github.com/rust-lang/crates.io-index)", + "unicode-xid 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "termion" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "libc 0.2.45 (registry+https://github.com/rust-lang/crates.io-index)", + "redox_syscall 0.1.43 (registry+https://github.com/rust-lang/crates.io-index)", + "redox_termios 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "textwrap" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "unicode-width 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "toml" +version = "0.4.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "serde 1.0.82 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "unicode-width" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "unicode-xid" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "vec_map" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "winapi" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "winapi-i686-pc-windows-gnu 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)", + "winapi-x86_64-pc-windows-gnu 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[metadata] +"checksum ansi_term 0.11.0 (registry+https://github.com/rust-lang/crates.io-index)" = "ee49baf6cb617b853aa8d93bf420db2383fab46d314482ca2803b40d5fde979b" +"checksum atty 0.2.11 (registry+https://github.com/rust-lang/crates.io-index)" = "9a7d5b8723950951411ee34d271d99dddcc2035a16ab25310ea2c8cfd4369652" +"checksum backtrace 0.3.9 (registry+https://github.com/rust-lang/crates.io-index)" = "89a47830402e9981c5c41223151efcced65a0510c13097c769cede7efb34782a" +"checksum backtrace-sys 0.1.24 (registry+https://github.com/rust-lang/crates.io-index)" = "c66d56ac8dabd07f6aacdaf633f4b8262f5b3601a810a0dcddffd5c22c69daa0" +"checksum bitflags 1.0.4 (registry+https://github.com/rust-lang/crates.io-index)" = "228047a76f468627ca71776ecdebd732a3423081fcf5125585bcd7c49886ce12" +"checksum cc 1.0.25 (registry+https://github.com/rust-lang/crates.io-index)" = "f159dfd43363c4d08055a07703eb7a3406b0dac4d0584d96965a3262db3c9d16" +"checksum cfg-if 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)" = "082bb9b28e00d3c9d39cc03e64ce4cea0f1bb9b3fde493f0cbc008472d22bdf4" +"checksum clap 2.32.0 (registry+https://github.com/rust-lang/crates.io-index)" = "b957d88f4b6a63b9d70d5f454ac8011819c6efa7727858f458ab71c756ce2d3e" +"checksum failure 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)" = "6dd377bcc1b1b7ce911967e3ec24fa19c3224394ec05b54aa7b083d498341ac7" +"checksum failure_derive 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)" = "64c2d913fe8ed3b6c6518eedf4538255b989945c14c2a7d5cbff62a5e2120596" +"checksum libc 0.2.45 (registry+https://github.com/rust-lang/crates.io-index)" = "2d2857ec59fadc0773853c664d2d18e7198e83883e7060b63c924cb077bd5c74" +"checksum proc-macro2 0.4.24 (registry+https://github.com/rust-lang/crates.io-index)" = "77619697826f31a02ae974457af0b29b723e5619e113e9397b8b82c6bd253f09" +"checksum quote 0.6.10 (registry+https://github.com/rust-lang/crates.io-index)" = "53fa22a1994bd0f9372d7a816207d8a2677ad0325b073f5c5332760f0fb62b5c" +"checksum redox_syscall 0.1.43 (registry+https://github.com/rust-lang/crates.io-index)" = "679da7508e9a6390aeaf7fbd02a800fdc64b73fe2204dd2c8ae66d22d9d5ad5d" +"checksum redox_termios 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "7e891cfe48e9100a70a3b6eb652fef28920c117d366339687bd5576160db0f76" +"checksum rustc-demangle 0.1.9 (registry+https://github.com/rust-lang/crates.io-index)" = "bcfe5b13211b4d78e5c2cadfebd7769197d95c639c35a50057eb4c05de811395" +"checksum serde 1.0.82 (registry+https://github.com/rust-lang/crates.io-index)" = "6fa52f19aee12441d5ad11c9a00459122bd8f98707cadf9778c540674f1935b6" +"checksum serde_derive 1.0.82 (registry+https://github.com/rust-lang/crates.io-index)" = "96a7f9496ac65a2db5929afa087b54f8fc5008dcfbe48a8874ed20049b0d6154" +"checksum strsim 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)" = "bb4f380125926a99e52bc279241539c018323fab05ad6368b56f93d9369ff550" +"checksum syn 0.15.22 (registry+https://github.com/rust-lang/crates.io-index)" = "ae8b29eb5210bc5cf63ed6149cbf9adfc82ac0be023d8735c176ee74a2db4da7" +"checksum synstructure 0.10.1 (registry+https://github.com/rust-lang/crates.io-index)" = "73687139bf99285483c96ac0add482c3776528beac1d97d444f6e91f203a2015" +"checksum termion 1.5.1 (registry+https://github.com/rust-lang/crates.io-index)" = "689a3bdfaab439fd92bc87df5c4c78417d3cbe537487274e9b0b2dce76e92096" +"checksum textwrap 0.10.0 (registry+https://github.com/rust-lang/crates.io-index)" = "307686869c93e71f94da64286f9a9524c0f308a9e1c87a583de8e9c9039ad3f6" +"checksum toml 0.4.10 (registry+https://github.com/rust-lang/crates.io-index)" = "758664fc71a3a69038656bee8b6be6477d2a6c315a6b81f7081f591bffa4111f" +"checksum unicode-width 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)" = "882386231c45df4700b275c7ff55b6f3698780a650026380e72dabe76fa46526" +"checksum unicode-xid 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "fc72304796d0818e357ead4e000d19c9c174ab23dc11093ac919054d20a6a7fc" +"checksum vec_map 0.8.1 (registry+https://github.com/rust-lang/crates.io-index)" = "05c78687fb1a80548ae3250346c3db86a80a7cdd77bda190189f2d0a0987c81a" +"checksum winapi 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)" = "92c1eb33641e276cfa214a0522acad57be5c56b10cb348b3c5117db75f3ac4b0" +"checksum winapi-i686-pc-windows-gnu 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" +"checksum winapi-x86_64-pc-windows-gnu 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..c7e9d49 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "citadel-mkimage" +version = "0.1.0" +authors = ["Bruce Leidl "] +homepage = "https://github.com/subgraph/citadel" + +[dependencies] +clap = "2.32.0" +failure = "0.1.3" +serde_derive = "1.0.82" +serde = "1.0.82" +toml = "0.4.10" diff --git a/src/build.rs b/src/build.rs new file mode 100644 index 0000000..ab3a70a --- /dev/null +++ b/src/build.rs @@ -0,0 +1,227 @@ +use std::path::PathBuf; +use std::fs::OpenOptions; +use std::path::Path; +use std::fs::{self,File}; +use std::io::{self,Write}; + +use failure::ResultExt; + +use Result; +use BuildConfig; +use util; + +pub struct UpdateBuilder { + config: BuildConfig, + + nblocks: Option, + shasum: Option, + verity_salt: Option, + verity_root: Option, +} + + +const BLOCK_SIZE: usize = 4096; +fn align(sz: usize, n: usize) -> usize { + (sz + (n - 1)) & !(n - 1) +} + + +impl UpdateBuilder { + + pub fn new(config: BuildConfig) -> Result { + let builder = UpdateBuilder { + config, + nblocks: None, shasum: None, verity_salt: None, + verity_root: None, + }; + + builder.copy_source_image()?; + Ok(builder) + } + + pub fn build(&mut self) -> Result<()> { + self.pad_image() + .context("failed writing padding to image")?; + + self.generate_verity() + .context("failed generating dm-verity hash tree")?; + + self.calculate_shasum()?; + self.compress_image()?; + + self.write_final_image() + .context("failed to write final image file")?; + + Ok(()) + } + + fn target(&self, ext: Option<&str>) -> PathBuf { + match ext { + Some(s) => self.config.workdir_path(&format!("citadel-{}-{}-{:03}.{}", self.config.img_name(), self.config.channel(), self.config.version(), s)), + None => self.config.workdir_path(&format!("citadel-{}-{}-{:03}", self.config.img_name(), self.config.channel(), self.config.version())) + } + } + + fn copy_source_image(&self) -> Result<()> { + sanity_check_source(self.config.source())?; + let target = self.target(None); + info!("Copying source file to {}", target.display()); + let mut from = File::open(self.config.source())?; + let mut to = File::create(&target)?; + io::copy(&mut from, &mut to)?; + Ok(()) + } + + fn pad_image(&mut self) -> Result<()> { + let target = self.target(None); + let meta = target.metadata()?; + let len = meta.len() as usize; + let padlen = align(len, BLOCK_SIZE) - len; + + if padlen > 0 { + info!("Padding image with {} zero bytes to 4096 byte block boundary", padlen); + let zeros = vec![0u8; padlen]; + let mut file = OpenOptions::new() + .append(true) + .open(&target)?; + file.write_all(&zeros)?; + } + + let nblocks = (len + padlen) / 4096; + info!("Image contains {} blocks of data", nblocks); + self.nblocks = Some(nblocks); + + Ok(()) + } + + fn calculate_shasum(&mut self) -> Result<()> { + let shasum = util::sha256(self.target(None))?; + info!("Sha256 of image data is {}", shasum); + self.shasum = Some(shasum); + Ok(()) + } + + fn generate_verity(&mut self) -> Result<()> { + let hashfile = self.config.workdir_path(&format!("verity-hash-{}-{:03}", self.config.image_type(), self.config.version())); + let outfile = self.config.workdir_path("verity-format.out"); + + let verity = util::verity_initial_hashtree(self.target(None), &hashfile)?; + + + fs::write(outfile, verity.output()) + .context("failed to write veritysetup command output to a file")?; + + let root = match verity.root_hash() { + Some(s) => s.to_owned(), + None => bail!("no root hash found in verity format output"), + }; + + let salt = match verity.salt() { + Some(s) => s.to_owned(), + None => bail!("no verity salt found in verity format output"), + }; + + info!("Verity hash tree calculated, verity-root = {}", root); + + self.verity_salt = Some(salt); + self.verity_root = Some(root); + + Ok(()) + + } + + fn compress_image(&self) -> Result<()> { + info!("Compressing image data"); + util::xz_compress(self.target(None)) + } + + fn write_final_image(&self) -> Result<()> { + let header = self.generate_header()?; + let mut path = self.target(None); + let fname = path.file_name().unwrap().to_str().unwrap().to_owned(); + path.set_file_name(fname + ".img"); + + let mut out = File::create(&path) + .context(format!("could not open output file {}", path.display()))?; + + out.write_all(&header)?; + + path.set_extension("xz"); + let mut data = File::open(&path) + .context(format!("could not open compressed image data file {}", path.display()))?; + io::copy(&mut data, &mut out) + .context("error copying image data to output file")?; + Ok(()) + } + + + /// + /// The Image Header structure is stored in a 4096 byte block at the start of + /// every resource image file. When an image is installed to a partition it + /// is stored at the last 4096 byte block of the block device for the partition. + /// + /// The layout of this structure is the following: + /// + /// field size (bytes) offset + /// ----- ------------ ------ + /// + /// magic 4 0 + /// status 1 4 + /// flags 1 5 + /// length 2 6 + /// + /// metainfo 8 + /// + /// signature 64 8 + length + /// + fn generate_header(&self) -> Result> { + let mut hdr = vec![0u8; BLOCK_SIZE]; + hdr[0..4].copy_from_slice(b"SGOS"); + hdr[5] = 0x04; // FLAG_DATA_COMPRESSED + + let metainfo = self.generate_metainfo(); + let metalen = metainfo.len(); + hdr[6] = (metalen >> 8) as u8; + hdr[7] = metalen as u8; + hdr[8..8+metalen].copy_from_slice(&metainfo); + + Ok(hdr) + } + + fn generate_metainfo(&self) -> Vec { + // writes to Vec can't fail, unwrap once to avoid clutter + self._generate_metainfo().unwrap() + } + + fn _generate_metainfo(&self) -> Result> { + assert!(self.verity_salt.is_some() && self.verity_root.is_some(), + "no verity-salt/verity-root in generate_metainfo()"); + + let mut v = Vec::new(); + writeln!(v, "image-type = \"{}\"", self.config.image_type())?; + writeln!(v, "channel = \"{}\"", self.config.channel())?; + writeln!(v, "version = {}", self.config.version())?; + writeln!(v, "nblocks = {}", self.nblocks.unwrap())?; + writeln!(v, "shasum = \"{}\"", self.shasum.as_ref().unwrap())?; + writeln!(v, "verity-salt = \"{}\"", self.verity_salt.as_ref().unwrap())?; + writeln!(v, "verity-root = \"{}\"", self.verity_root.as_ref().unwrap())?; + Ok(v) + } +} + +fn sanity_check_source>(src: P) -> Result<()> { + let src: &Path = src.as_ref(); + let meta = match src.metadata() { + Ok(md) => md, + Err(e) => bail!("Could not load image file {}: {}", src.display(), e), + }; + + if !meta.file_type().is_file() { + bail!("Image file {} exists but is not a regular file", src.display()); + } + + if meta.len() % 512 != 0 { + bail!("Image file size is not a multiple of sector size (512 bytes)"); + } + Ok(()) +} diff --git a/src/config.rs b/src/config.rs new file mode 100644 index 0000000..18ce82e --- /dev/null +++ b/src/config.rs @@ -0,0 +1,97 @@ + +use std::path::{Path,PathBuf}; +use std::fs::File; +use std::io::Read; + +use Result; +use toml; + +#[derive(Deserialize)] +pub struct BuildConfig { + #[serde (rename="image-type")] + image_type: String, + channel: String, + version: usize, + source: String, + #[serde (rename="kernel-version")] + kernel_version: Option, + + #[serde(skip)] + basedir: PathBuf, + #[serde(skip)] + src_path: PathBuf, + #[serde(skip)] + img_name: String, +} + +impl BuildConfig { + pub fn load>(path: P) -> Result { + let mut path = path.as_ref().to_owned(); + if path.is_dir() { + path.push("mkimage.conf"); + } + + let mut config = match BuildConfig::from_path(&path) { + Ok(config) => config, + Err(e) => bail!("Failed to load config file {}: {}", path.display(), e), + }; + + path.pop(); + config.basedir = path; + config.src_path = PathBuf::from(&config.source); + config.img_name = match config.kernel_version { + Some(ref version) => format!("{}-{}", &config.image_type, version), + None => config.image_type.to_owned(), + }; + Ok(config) + } + + fn from_path(path: &Path) -> Result { + let mut f = File::open(path)?; + let mut s = String::new(); + f.read_to_string(&mut s)?; + let config = toml::from_str::(&s)?; + config.validate()?; + Ok(config) + } + + fn validate(&self) -> Result<()> { + let itype = self.image_type.as_str(); + if itype != "extra" && itype != "rootfs" && itype != "modules" { + bail!("Invalid image type '{}'", self.image_type); + }; + let src = Path::new(&self.source); + if !src.is_file() { + bail!("Source path '{}' does not exist or is not a regular file"); + } + if self.image_type == "modules" && self.kernel_version.is_none() { + bail!("Cannot build 'modules' image without kernel-version field"); + } + + Ok(()) + } + + pub fn source(&self) -> &Path { + &self.src_path + } + + pub fn workdir_path(&self, filename: &str) -> PathBuf { + self.basedir.join(filename) + } + + pub fn img_name(&self) -> &str { + &self.img_name + } + + pub fn version(&self) -> usize { + self.version + } + + pub fn channel(&self) -> &str { + &self.channel + } + + pub fn image_type(&self) -> &str { + &self.image_type + } +} diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..27307b9 --- /dev/null +++ b/src/main.rs @@ -0,0 +1,182 @@ +#[macro_use] extern crate failure; +#[macro_use] extern crate serde_derive; + +#[macro_export] +macro_rules! info { + ($e:expr) => { if $crate::verbose() { println!("[+] {}", $e);} }; + ($fmt:expr, $($arg:tt)+) => { if $crate::verbose() { println!("[+] {}", format!($fmt, $($arg)+));} }; +} +#[macro_export] +macro_rules! warn { + ($e:expr) => { println!("WARNING: {}", $e); }; + ($fmt:expr, $($arg:tt)+) => { println!("WARNING: {}", format!($fmt, $($arg)+)); }; +} + +#[macro_export] +macro_rules! notify { + ($e:expr) => { println!("[+] {}", $e); }; + ($fmt:expr, $($arg:tt)+) => { println!("[+] {}", format!($fmt, $($arg)+)); }; +} + +thread_local! { + pub static VERBOSE: RefCell = RefCell::new(false); +} + +pub fn verbose() -> bool { + VERBOSE.with(|f| { + *f.borrow() + }) +} + +pub fn set_verbose(val: bool) { + VERBOSE.with(|f| { + *f.borrow_mut() = val + }); +} + +extern crate clap; +extern crate toml; + +use std::process::exit; +use std::result; +use std::cell::RefCell; + +use clap::{App,Arg,SubCommand,ArgMatches}; +use clap::AppSettings::*; +use failure::Error; + +pub type Result = result::Result; +use build::UpdateBuilder; +use config::BuildConfig; + +mod build; +mod config; +mod util; + + + +fn main() { + let app = App::new("citadel-mkimage") + .about("Citadel update image builder") + .settings(&[ArgRequiredElseHelp,ColoredHelp, DisableHelpSubcommand, DisableVersion, DeriveDisplayOrder]) + + .subcommand(SubCommand::with_name("build") + .about("Build an update image specified by a configuration file") + .arg(Arg::with_name("build-file") + .required(true) + .help("Path to image build config file"))); + + set_verbose(true); + let matches = app.get_matches(); + + let result = match matches.subcommand() { + ("build", Some(m)) => build_image(m), + _ => Ok(()), + }; + + if let Err(ref e) = result { + println!("Error: {}", format_error(e)); + exit(1); + } +} + +fn format_error(err: &Error) -> String { + let mut output = err.to_string(); + let mut prev = err.as_fail(); + while let Some(next) = prev.cause() { + output.push_str(": "); + output.push_str(&next.to_string()); + prev = next; + } + output +} + +/* +fn find_source_image(update_type: &str, filename: &str) -> Result { + let mut path = PathBuf::from(filename); + let meta = path.metadata() + .context(format!("could not find source file or directory {}", filename))?; + + if meta.file_type().is_file() { + return Ok(path); + } + if !meta.file_type().is_dir() { + bail!("source not found: {}", filename); + } + + path.push(format!("build/images/citadel-{}-image-intel-corei7-64.ext2", update_type)); + let meta = path.metadata() + .context(format!("could not find source file {}", path.display()))?; + + if !meta.file_type().is_file() { + bail!("source {} exists but is not a file", path.display()); + } + + let canonical = fs::canonicalize(&path) + .context(format!("failed to resolve {} to absolute path", path.display()))?; + Ok(canonical) +} + +fn parse_version(arg_matches: &ArgMatches) -> Result { + let v = arg_matches.value_of("version") + .unwrap_or("0") + .parse::() + .context("Unable to parse version argument")?; + Ok(v) +} +*/ + +fn build_image(arg_matches: &ArgMatches) -> Result<()> { + let build_file = arg_matches.value_of("build-file").unwrap(); + let config = BuildConfig::load(build_file)?; + let mut builder = UpdateBuilder::new(config)?; + builder.build()?; + Ok(()) + +} + +/* +fn build_update(update_type: &str, arg_matches: &ArgMatches) -> Result<()> { + let config = Config::load("/usr/share/citadel/citadel-image.conf")?; + let channel = config.get_default_channel().unwrap(); // XXX unwrap() + + let source_name = arg_matches.value_of("citadel-base").unwrap(); + let source = find_source_image(update_type, source_name)?; + let version = parse_version(arg_matches)?; + + info!("Building file {} as {} update image for channel {} with version {}", source.display(), update_type, channel.name(), version); + let conf = BuildConfig::load("").unwrap(); + let mut builder = UpdateBuilder::new(conf)?; + builder.build()?; + Ok(()) +} + +fn rootfs_update(arg_matches: &ArgMatches) -> Result<()> { + build_update("rootfs", arg_matches) +} + +fn modules_update(arg_matches: &ArgMatches) -> Result<()> { + build_update("modules", arg_matches) +} + +fn extra_update(arg_matches: &ArgMatches) -> Result<()> { + build_update("extra", arg_matches) +} + +fn generate_verity(_arg_matches: &ArgMatches) -> Result<()> { + Ok(()) +} + +fn generate_keys() -> Result<()> { + let keys = SigningKeys::generate()?; + println!("pubkey = \"{}\"", keys.to_public_hex()); + println!("keypair = \"{}\"", keys.to_hex()); + Ok(()) +} + +fn image_info(arg_matches: &ArgMatches) -> Result<()> { + let image = arg_matches.value_of("image-file").unwrap(); + let config = Config::load("/usr/share/citadel/citadel-image.conf")?; + info::show_info(image, &config) +} +*/ diff --git a/src/util.rs b/src/util.rs new file mode 100644 index 0000000..cd0cb61 --- /dev/null +++ b/src/util.rs @@ -0,0 +1,112 @@ + +use std::process::{Command,Stdio}; +use std::path::Path; +use std::collections::HashMap; + +use failure::ResultExt; + +use Result; + + +pub fn sha256>(path: P) -> Result { + let output = exec_command_with_output("/usr/bin/sha256sum", &[pathstr(path.as_ref())]) + .context(format!("failed to calculate sha256 on {}", path.as_ref().display()))?; + + let v: Vec<&str> = output.split_whitespace().collect(); + Ok(v[0].trim().to_owned()) +} + +pub fn xz_compress>(path: P) -> Result<()> { + exec_command("/usr/bin/xz", &["-T0", pathstr(path.as_ref())]) + .context(format!("failed to compress {}", path.as_ref().display()))?; + Ok( ()) +} + +pub fn verity_initial_hashtree, Q: AsRef>(path: P, hashfile: Q) -> Result { + let output = exec_command_with_output("/usr/sbin/veritysetup", + &["format",pathstr(path.as_ref()), pathstr(hashfile.as_ref())]) + .context("veritysetup format command failed")?; + + Ok(VerityOutput::parse(&output)) +} + +fn pathstr(path: &Path) -> &str { + path.to_str().unwrap() +} + +fn exec_command(cmd_path: &str, args: &[&str]) -> Result<()> { + let status = Command::new(cmd_path) + .args(args) + .stderr(Stdio::inherit()) + .status() + .context(format!("unable to execute {}", cmd_path))?; + + if !status.success() { + match status.code() { + Some(code) => bail!("command {} failed with exit code: {}", cmd_path, code), + None => bail!("command {} failed with no exit code", cmd_path), + } + } + Ok(()) +} + +fn exec_command_with_output(cmd_path: &str, args: &[&str]) -> Result { + let res = Command::new(cmd_path) + .args(args) + .stderr(Stdio::inherit()) + .output() + .context(format!("unable to execute {}", cmd_path))?; + + if !res.status.success() { + match res.status.code() { + Some(code) => bail!("command {} failed with exit code: {}", cmd_path, code), + None => bail!("command {} failed with no exit code", cmd_path), + } + } + + Ok(String::from_utf8(res.stdout).unwrap().trim().to_owned()) +} + +/// The output from the `veritysetup format` command can be parsed as key/value +/// pairs. This class parses the output and stores it in a map for querying. +pub struct VerityOutput { + output: String, + map: HashMap, +} + +impl VerityOutput { + /// Parse the string `output` as standard output from the dm-verity + /// `veritysetup format` command. + fn parse(output: &str) -> VerityOutput { + let mut vo = VerityOutput { + output: output.to_owned(), + map: HashMap::new(), + }; + for line in output.lines() { + vo.parse_line(line); + } + vo + } + + fn parse_line(&mut self, line: &str) { + let v = line.split(':') + .map(|s| s.trim()) + .collect::>(); + + if v.len() == 2 { + self.map.insert(v[0].to_owned(), v[1].to_owned()); + } + } + + pub fn root_hash(&self) -> Option<&str> { + self.map.get("Root hash").map(|s| s.as_str()) + } + + pub fn salt(&self) -> Option<&str> { + self.map.get("Salt").map(|s| s.as_str()) + } + + pub fn output(&self) -> &str { + &self.output + } +}