diff --git a/Cargo.lock b/Cargo.lock index 76e08e5..4a75e28 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -79,6 +79,15 @@ version = "2.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5c8214115b7bf84099f1309324e63141d4c5d7cc26862f97a0a857dbefe165bd" +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + [[package]] name = "cc" version = "1.2.17" @@ -149,6 +158,15 @@ version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5b63caa9aa9397e2d9480a9b13673856c78d8ac123288526c37d7839f2a86990" +[[package]] +name = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] + [[package]] name = "crc" version = "3.2.1" @@ -179,6 +197,26 @@ version = "0.8.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" +[[package]] +name = "crypto-common" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "crypto-common", +] + [[package]] name = "either" version = "1.15.0" @@ -191,6 +229,16 @@ version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + [[package]] name = "gptman" version = "1.1.4" @@ -331,6 +379,7 @@ dependencies = [ "serde", "serde_repr", "serial2", + "sha2", "xmltree", ] @@ -368,6 +417,16 @@ dependencies = [ "proc-macro2", ] +[[package]] +name = "qviptblgen" +version = "0.1.0" +dependencies = [ + "anyhow", + "clap", + "clap-num", + "qdl", +] + [[package]] name = "rusb" version = "0.9.4" @@ -420,6 +479,17 @@ dependencies = [ "winapi", ] +[[package]] +name = "sha2" +version = "0.10.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + [[package]] name = "shlex" version = "1.3.0" @@ -463,6 +533,12 @@ dependencies = [ "syn", ] +[[package]] +name = "typenum" +version = "1.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1dccffe3ce07af9386bfd29e80c0ab1a8205a2fc34e4bcd40364df902cfa8f3f" + [[package]] name = "unicode-ident" version = "1.0.18" @@ -481,6 +557,12 @@ version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + [[package]] name = "winapi" version = "0.3.9" diff --git a/Cargo.toml b/Cargo.toml index 2f5a928..a814092 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,3 +1,3 @@ [workspace] -members = ["cli", "qramdump", "qdl"] +members = ["cli", "qdl", "qramdump", "qviptblgen"] resolver = "2" diff --git a/cli/Cargo.toml b/cli/Cargo.toml index a408511..4cc9ee3 100644 --- a/cli/Cargo.toml +++ b/cli/Cargo.toml @@ -17,7 +17,7 @@ maintenance = { status = "actively-developed" } anyhow = "1.0.89" clap = { version = "4.5.18", features = ["derive"] } clap-num = "1.1.1" -qdl = { path = "../qdl/", features = ["serial", "usb"] } +qdl = { path = "../qdl/", features = ["serial", "usb", "vip"] } gptman = "1.1.2" indexmap = "2.5.0" owo-colors = "4.1.0" diff --git a/cli/src/main.rs b/cli/src/main.rs index 077833c..01f4842 100644 --- a/cli/src/main.rs +++ b/cli/src/main.rs @@ -1,6 +1,6 @@ // SPDX-License-Identifier: BSD-3-Clause // Copyright (c) Qualcomm Technologies, Inc. and/or its subsidiaries. -use anyhow::{Result, bail}; +use anyhow::{Context, Result, bail}; use clap::{Parser, Subcommand}; use clap_num::maybe_hex; use itertools::Itertools; @@ -18,6 +18,7 @@ use util::{ }; use std::fs::{self, File}; +use std::io::Write; use std::{path::Path, str::FromStr}; mod flasher; @@ -164,6 +165,12 @@ struct Args { #[arg(long, default_value = "false")] verbose_firehose: bool, + #[arg(short = 'M')] + vip_mbn_path: Option, + + #[arg(short = 'T')] + vip_aux_tbl_path: Option, + #[command(subcommand)] command: Command, } @@ -182,6 +189,25 @@ fn main() -> Result<()> { Err(e) => bail!("Couldn't open the programmer binary: {}", e.to_string()), }; + let vip_mbn = match args.vip_mbn_path { + Some(p) => match fs::read(p) { + Ok(m) => Some(m), + Err(e) => bail!("Couldn't read the VIP MBN: {}", e.to_string()), + }, + None => None, + }; + + let vip_aux = match args.vip_aux_tbl_path { + Some(p) => match fs::read(p) { + Ok(a) => a, + Err(e) => bail!( + "Couldn't read the VIP chained table file: {}", + e.to_string() + ), + }, + None => vec![], + }; + println!( "{} {}", env!("CARGO_PKG_NAME").green(), @@ -219,6 +245,8 @@ fn main() -> Result<()> { ..Default::default() }, reset_on_drop: false, + vip_digest_table: vip_aux, + fh_packet_counter: 0, }; // In case another program on the system has already consumed the HELLO packet, @@ -265,6 +293,13 @@ fn main() -> Result<()> { // If we're past Sahara, activate the Firehose reset-on-drop listener qdl_dev.reset_on_drop = true; + // If VIP is used, the hash table must be sent first, even before + if let Some(vip_mbn) = vip_mbn { + qdl_dev + .write_all(&vip_mbn) + .with_context(|| "Couldn't load the VIP MBN")?; + } + // Get any "welcome" logs firehose_read(&mut qdl_dev, firehose_parser_ack_nak)?; diff --git a/qdl/Cargo.toml b/qdl/Cargo.toml index ba300e1..17d7869 100644 --- a/qdl/Cargo.toml +++ b/qdl/Cargo.toml @@ -8,7 +8,7 @@ license = "BSD-3-Clause" readme = "README.md" repository = "https://github.com/qualcomm/qdlrs" categories = ["embedded"] -publish = false # TODO +publish = false # TODO [badges] maintenance = { status = "actively-developed" } @@ -28,8 +28,10 @@ rusb = { version = "0.9.4", optional = true } serde = { version = "1.0.210", features = ["derive"] } serde_repr = "0.1.19" serial2 = { version = "0.2.28", optional = true } +sha2 = { version = "0.10.9", optional = true } xmltree = { version = "0.11.0", features = ["attribute-order"] } [features] serial = ["dep:serial2"] usb = ["dep:rusb"] +vip = ["dep:sha2"] diff --git a/qdl/src/lib.rs b/qdl/src/lib.rs index 68da331..fd1a3ea 100644 --- a/qdl/src/lib.rs +++ b/qdl/src/lib.rs @@ -29,6 +29,8 @@ pub mod serial; pub mod types; #[cfg(feature = "usb")] pub mod usb; +#[cfg(feature = "vip")] +pub mod vip; pub fn setup_target_device( backend: QdlBackend, @@ -48,7 +50,7 @@ pub fn setup_target_device( } /// Wrapper for easily creating Firehose-y XML packets -fn firehose_xml_setup(op: &str, kvps: &[(&str, &str)]) -> anyhow::Result> { +pub fn firehose_xml_setup(op: &str, kvps: &[(&str, &str)]) -> anyhow::Result> { let mut xml = Element::new("data"); let mut op_node = Element::new(op); for kvp in kvps.iter() { @@ -233,7 +235,7 @@ pub fn firehose_configure( ("MemoryName", &config.storage_type.to_string()), ("AlwaysValidate", &(config.hash_packets as u32).to_string()), ("Verbose", &(config.verbose_firehose as u32).to_string()), - ("MaxDigestTableSizeInBytes", "8192"), // TODO: (low prio) + ("MaxDigestTableSizeInBytes", "8192"), ( "MaxPayloadSizeToTargetInBytes", &config.send_buffer_size.to_string(), diff --git a/qdl/src/types.rs b/qdl/src/types.rs index 9d932f3..6a1ca03 100644 --- a/qdl/src/types.rs +++ b/qdl/src/types.rs @@ -8,6 +8,7 @@ use std::{ use anyhow::{Error, bail}; use owo_colors::OwoColorize; +use sha2::Digest; use crate::firehose_reset; @@ -96,6 +97,8 @@ pub trait QdlReadWrite: Read + Write {} pub struct QdlDevice<'a> { pub rw: &'a mut dyn QdlReadWrite, pub fh_cfg: FirehoseConfiguration, + pub vip_digest_table: Vec, + pub fh_packet_counter: usize, pub reset_on_drop: bool, } @@ -107,7 +110,13 @@ impl Read for QdlDevice<'_> { impl Write for QdlDevice<'_> { fn write(&mut self, buf: &[u8]) -> std::io::Result { - self.rw.write(buf) + let ret = self.rw.write(buf); + if ret.is_ok() { + self.fh_packet_counter += 1; + self.write_next_hash()?; + } + + ret } fn flush(&mut self) -> std::io::Result<()> { @@ -125,6 +134,26 @@ impl QdlChan for QdlDevice<'_> { } } +impl QdlDevice<'_> { + fn write_next_hash(&mut self) -> std::io::Result { + // Hashes 0..53 come from the MBN image + if self.fh_packet_counter >= 54 && !self.vip_digest_table.is_empty() { + let buf = match self + .vip_digest_table + .chunks(sha2::Sha256::output_size()) + .nth(self.fh_packet_counter - 54) + { + Some(b) => Ok(b), + None => Err(std::io::Error::other("Faulty VIP chained table!")), + }?; + + return self.rw.write(buf); + } + + Ok(0) + } +} + impl Drop for QdlDevice<'_> { fn drop(&mut self) { // Avoid having the board be stuck in EDL limbo in case of errors diff --git a/qdl/src/vip.rs b/qdl/src/vip.rs new file mode 100644 index 0000000..fa8aa7b --- /dev/null +++ b/qdl/src/vip.rs @@ -0,0 +1,157 @@ +use anyhow::Result; +use bincode::serialize; +use serde::Serialize; +use sha2::{Digest, Sha256}; +use std::{ + fs::{self, File}, + io::{BufReader, Read, Write}, + path::Path, +}; +use xmltree::XMLNode; + +use crate::firehose_xml_setup; + +pub fn calc_hashes(xml_path: &Path, send_buffer_size: usize) -> Result>> { + let program_file = fs::read(xml_path)?; + let xml = xmltree::Element::parse(&program_file[..])?; + + let mut digests: Vec> = vec![]; + for node in xml.children.iter() { + if let XMLNode::Element(e) = node { + let args: Vec<(&str, &str)> = e + .attributes + .as_slice() + .into_iter() + .map(|(a, b)| (a.as_str(), b.as_str())) + .collect(); + let packet = firehose_xml_setup(&e.name.to_ascii_lowercase(), &args)?; + + let hash = Sha256::digest(packet); + digests.push(hash.to_vec()); + + // SAFETY: if the program file exists, it must have a parent dir + let xml_dir = xml_path.parent().unwrap(); + if let Some(filename) = &e.attributes.get("filename") { + let file_path = xml_dir.join(filename); + + if filename.is_empty() { + continue; + } else { + if !file_path.exists() { + println!("WARNING: {filename} doesn't exist - assuming that's intended"); + continue; + } + + println!("Processing {filename}..."); + } + let mut buf = vec![0u8; send_buffer_size]; + let mut br = BufReader::new(File::open(file_path)?); + loop { + let n = br.read(&mut buf)?; + if n == 0 { + break; + } + digests.push(Sha256::digest(&buf[..n]).to_vec()); + } + } + } + } + + Ok(digests) +} + +#[derive(Serialize)] +#[repr(C)] +struct MbnHeaderV3 { + image_id: u32, + header_ver_num: u32, + image_src: u32, + image_dest_ptr: u32, + image_size: u32, + code_size: u32, + signature_ptr: u32, + signature_size: u32, + cert_chain_ptr: u32, + cert_chain_size: u32, +} + +/// The number of hashes in a single table of digests +/// The 54th entry is reserved for hashing the other 53 +const MAX_DIGESTS_PER_FILE: usize = 54 - 1; + +pub fn gen_hash_tables( + digests: Vec>, + output_dir: &Path, + max_table_size: usize, +) -> Result<()> { + let chained_table_elem_count = max_table_size / Sha256::output_size(); + let mut processed_chained_tables: Vec> = vec![]; + let primary_digests: Vec>; + let aux_digests: Vec>; + + if digests.len() >= MAX_DIGESTS_PER_FILE { + primary_digests = digests[..MAX_DIGESTS_PER_FILE].to_vec(); + aux_digests = digests[MAX_DIGESTS_PER_FILE..].to_vec(); + } else { + primary_digests = digests; + aux_digests = vec![]; + } + + // The last digest in the table is the hash of the next table + // Add a - 1 to accomodate for the last entry being the next table's hash + let chained_tables = aux_digests.chunks(chained_table_elem_count - 1); + let mut hash: Vec = vec![]; + + // Note this loop starts from the last table + for tbl in chained_tables.rev() { + // Add the digests + let mut entry = tbl.concat(); + + // Add the hash of the table that follows (add nothing in the first iteration) + // TODO: use the explicit init/update/finalize to avoid sad copies + entry.append(&mut hash); + + processed_chained_tables.push(entry); + + // Hash the current table to include in the next one + // The variable will contain the hash of the first table at the end of + // execution (may be an empty vector) + hash = Sha256::digest(tbl.concat()).to_vec(); + } + + let mbn_table_size = match aux_digests.is_empty() { + true => size_of_val(&primary_digests), + false => size_of_val(&primary_digests) + Sha256::output_size(), + }; + + let hdr = MbnHeaderV3 { + image_id: 26, + header_ver_num: 3, + // Offset of the first hash table + image_src: 40, + image_dest_ptr: 0, + image_size: mbn_table_size as u32, + code_size: mbn_table_size as u32, + // The file will be signed externally, leave signature fields empty + signature_ptr: 0, + signature_size: 0, + cert_chain_ptr: 0, + cert_chain_size: 0, + }; + + if !output_dir.exists() { + std::fs::create_dir(output_dir)?; + } + + let mut mbn = File::create(output_dir.join("signme.mbn"))?; + mbn.write_all(&serialize(&hdr)?)?; + mbn.write_all(&primary_digests.concat())?; + if let Some(hash) = processed_chained_tables.last() { + mbn.write_all(hash)?; + + let mut aux_tbl_file = File::create(output_dir.join("tables.bin"))?; + aux_tbl_file.write_all(&aux_digests.concat())?; + } + + Ok(()) +} diff --git a/qramdump/src/main.rs b/qramdump/src/main.rs index 10e4aeb..a491be6 100644 --- a/qramdump/src/main.rs +++ b/qramdump/src/main.rs @@ -48,6 +48,8 @@ pub fn main() -> Result<()> { rw: rw_channel.as_mut(), fh_cfg: FirehoseConfiguration::default(), reset_on_drop: false, + vip_digest_table: vec![], + fh_packet_counter: 0, }; sahara_run( diff --git a/qviptblgen/Cargo.toml b/qviptblgen/Cargo.toml new file mode 100644 index 0000000..d08735a --- /dev/null +++ b/qviptblgen/Cargo.toml @@ -0,0 +1,20 @@ +[package] +name = "qviptblgen" +version = "0.1.0" +edition = "2024" +authors = ["Konrad Dybcio "] +license = "BSD-3-Clause" +# description = "XXXXX" +readme = "README.md" +repository = "https://github.com/qualcomm/qdlrs" +categories = ["command-line-utilities"] +publish = false # TODO + +[badges] +maintenance = { status = "actively-developed" } + +[dependencies] +anyhow = "1.0.89" +clap = { version = "4.5.18", features = ["derive"] } +clap-num = "1.1.1" +qdl = { path = "../qdl/", features = ["vip"] } diff --git a/qviptblgen/src/main.rs b/qviptblgen/src/main.rs new file mode 100644 index 0000000..f1cf953 --- /dev/null +++ b/qviptblgen/src/main.rs @@ -0,0 +1,30 @@ +// SPDX-License-Identifier: BSD-3-Clause +// Copyright (c) Qualcomm Technologies, Inc. and/or its subsidiaries. +use anyhow::Result; +use clap::{Parser, command}; +use qdl::{ + self, + vip::{calc_hashes, gen_hash_tables}, +}; +use std::path::Path; + +#[derive(Parser, Debug)] +#[command(version, about, long_about = None)] +struct Args { + #[arg()] + input_xml: String, + + #[arg(short, default_value = "out/")] + output_dir: String, + + #[arg(short, default_value = "1048576")] + send_buffer_size: usize, +} + +pub fn main() -> Result<()> { + let args = Args::parse(); + + let hashes = calc_hashes(Path::new(&args.input_xml), args.send_buffer_size)?; + + gen_hash_tables(hashes, Path::new(&args.output_dir), 8192) +}