diff --git a/Cargo.lock b/Cargo.lock index 9b7079e..981bc19 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -122,6 +122,7 @@ dependencies = [ "const-oid", "ctrlc", "flate2", + "gf256", "hex", "lz4_flex", "memchr", @@ -304,7 +305,7 @@ dependencies = [ "heck", "proc-macro2", "quote", - "syn", + "syn 2.0.29", ] [[package]] @@ -434,6 +435,41 @@ dependencies = [ "windows-sys", ] +[[package]] +name = "darling" +version = "0.12.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f2c43f534ea4b0b049015d00269734195e6d3f0f6635cb692251aca6f9f8b3c" +dependencies = [ + "darling_core", + "darling_macro", +] + +[[package]] +name = "darling_core" +version = "0.12.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e91455b86830a1c21799d94524df0845183fa55bafd9aa137b01c7d1065fa36" +dependencies = [ + "fnv", + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn 1.0.109", +] + +[[package]] +name = "darling_macro" +version = "0.12.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29b5acf0dea37a7f66f7b25d2c5e93fd46f8f6968b1a5d7a3e02e97768afc95a" +dependencies = [ + "darling_core", + "quote", + "syn 1.0.109", +] + [[package]] name = "der" version = "0.7.8" @@ -455,7 +491,7 @@ checksum = "5fe87ce4529967e0ba1dcf8450bab64d97dfd5010a6256187ffe2e43e6f0e049" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.29", ] [[package]] @@ -520,6 +556,12 @@ dependencies = [ "libc", ] +[[package]] +name = "evalexpr" +version = "6.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfc25fd417983cc7f203394ebb89eba18e2df1b0ac1be2673091b5aca52b595f" + [[package]] name = "fastrand" version = "2.0.0" @@ -593,6 +635,29 @@ dependencies = [ "wasi", ] +[[package]] +name = "gf256" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00ad9760c1b289e26006c2ed8cc950807c842e6c51f64532c806bc2ef7a34c79" +dependencies = [ + "cfg-if", + "gf256-macros", +] + +[[package]] +name = "gf256-macros" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b084923ea97216d84ff299a428609b85e68c130c5320a86ed778c204f9e19cd" +dependencies = [ + "darling", + "evalexpr", + "proc-macro2", + "quote", + "syn 1.0.109", +] + [[package]] name = "hashbrown" version = "0.14.0" @@ -646,6 +711,12 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "140a09c9305e6d5e557e2ed7cbc68e05765a7d4213975b87cb04920689cc6219" +[[package]] +name = "ident_case" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" + [[package]] name = "idna" version = "0.4.0" @@ -900,7 +971,7 @@ checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.29", ] [[package]] @@ -986,7 +1057,7 @@ dependencies = [ "phf_shared", "proc-macro2", "quote", - "syn", + "syn 2.0.29", ] [[package]] @@ -1322,7 +1393,7 @@ checksum = "4eca7ac642d82aa35b60049a6eccb4be6be75e599bd2e9adb5f875a737654af2" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.29", ] [[package]] @@ -1412,6 +1483,17 @@ version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "81cdd64d312baedb58e21336b31bc043b77e01cc99033ce76ef539f78e965ebc" +[[package]] +name = "syn" +version = "1.0.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + [[package]] name = "syn" version = "2.0.29" @@ -1453,7 +1535,7 @@ checksum = "6bb623b56e39ab7dcd4b1b98bb6c8f8d907ed255b18de254088016b27a8ee19b" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.29", ] [[package]] @@ -1608,7 +1690,7 @@ dependencies = [ "once_cell", "proc-macro2", "quote", - "syn", + "syn 2.0.29", "wasm-bindgen-shared", ] @@ -1630,7 +1712,7 @@ checksum = "54681b18a46765f095758388f2d0cf16eb8d4169b639ab575a8f5693af210c7b" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.29", "wasm-bindgen-backend", "wasm-bindgen-shared", ] diff --git a/README.extra.md b/README.extra.md index 46d0a76..100d0d9 100644 --- a/README.extra.md +++ b/README.extra.md @@ -20,7 +20,7 @@ This subcommand shows all of the vbmeta header and footer fields. `vbmeta` parti avbroot avb verify -i -p ``` -This subcommand verifies the vbmeta header signature and the hashes for all vbmeta descriptors (including hashtree descriptors). If the vbmeta image has a chain descriptor for another partition, that partition image will be verified as well (recursively). All partitions are expected to be in the same directory as the vbmeta image being verified. +This subcommand verifies the vbmeta header signature and the hashes for all vbmeta descriptors (including hash tree descriptors). If the vbmeta image has a chain descriptor for another partition, that partition image will be verified as well (recursively). All partitions are expected to be in the same directory as the vbmeta image being verified. If `-p` is omitted, the signatures and hashes are checked only for validity, not that they are trusted. @@ -58,6 +58,63 @@ avbroot boot info -i All of the `boot` subcommands show the boot image information. This specific subcommand just does it without performing any other operation. To show avbroot's internal representation of the information, pass in `-d`. +## `avbroot fec` + +This set of commands is for working with dm-verity FEC (forward error correction) data. The FEC data allows small errors in partition data to be corrected. This increases reliability of the system because when dm-verity encounters data that doesn't match the expected checksum, it will either trigger a kernel panic or reboot the system. + +The same raw FEC data can be stored in several ways: + +* cryptsetup's `veritysetup` does not use any file format at all. It must be told the FEC location and parameters using the `--fec-*` options. +* AOSP's AVB 2.0 stores the FEC data inside the partition as `[Partition data][Hash tree][FEC data]`. The location and parameters are stored in the vbmeta hash tree descriptors. +* AOSP's `fec` tool stores the FEC data in a standalone file with a header containing the FEC parameters. + +The `avbroot fec` commands use AOSP's standalone FEC file format. + +The FEC data is not generated from a sequential read of the input file, but rather from an interleaved read. If the input file's offsets are visualized as a 2D table: + +``` +| 0 1 2 3 ... 4095 | +| 4096 4097 4098 4099 ... 8191 | +| 8192 8193 8194 8195 ... 12287 | +| .... .... .... .... ... ..... | +``` + +then the file access pattern can be thought of as being column-by-column instead of row-by-row. + +Data correction happens at the codeword level. A Reed-Solomon codeword is 255 bytes where some portion is file data and the rest is parity data. AOSP and avbroot both default to 253 bytes of data and 2 bytes of parity information. Each column in the table represents the 253-byte data portion of the codeword. Larger files have more columns. + +A contiguous sequence of corrupted data will span multiple columns. Since error correction happens at the column level, this interleaving increases the chances of recovery. For more details about the specifics, see the implementation in [`fec.rs`](./avbroot/src/format/fec.rs). + +### Generating FEC data + +```bash +avbroot fec generate -i -f +``` + +The default behavior is to use 2 bytes of parity information per 253 bytes of input data. Within each 253-byte column described above, this is sufficient for correcting a single corrupted byte in the column (`⌊parity / 2⌋` bytes in general). + +The number of parity bytes (between 2 and 24, inclusive) can be configured using `--parity`. + +### Verifying a file + +```bash +avbroot fec verify -i -f +``` + +This will check if the input file has any corrupted bytes. This command runs significantly faster than `avbroot fec repair` and is useful if only detection of corrupted data is needed. + +Note that FEC is **not** a replacement for checksums, like SHA-256. When there are too many errors, there can be false positives where the corrupted data is reported as being valid. + +### Repairing a file + +```bash +avbroot fec repair -i -f +``` + +This will repair the file in place. As described above, in each column, up to `parity / 2` bytes can be corrected. + +Note that FEC is **not** a replacement for checksums, like SHA-256. When there are too many errors, the file can potentially be "successfully repaired" to some incorrect data. + ## `avbroot ramdisk` ### Dumping a cpio archive diff --git a/avbroot/Cargo.toml b/avbroot/Cargo.toml index a418244..751f02a 100644 --- a/avbroot/Cargo.toml +++ b/avbroot/Cargo.toml @@ -18,6 +18,7 @@ cms = { version = "0.2.2", features = ["std"] } const-oid = "0.9.5" ctrlc = "3.4.0" flate2 = "1.0.27" +gf256 = { version = "0.3.0", features = ["rs"] } hex = "0.4.3" lz4_flex = "0.11.1" memchr = "2.6.0" diff --git a/avbroot/src/cli/args.rs b/avbroot/src/cli/args.rs index 7c75baa..8e116f2 100644 --- a/avbroot/src/cli/args.rs +++ b/avbroot/src/cli/args.rs @@ -8,7 +8,7 @@ use std::sync::atomic::AtomicBool; use anyhow::Result; use clap::{Parser, Subcommand}; -use crate::cli::{avb, boot, completion, key, ota, ramdisk}; +use crate::cli::{avb, boot, completion, fec, key, ota, ramdisk}; #[allow(clippy::large_enum_variant)] #[derive(Debug, Subcommand)] @@ -16,6 +16,7 @@ pub enum Command { Avb(avb::AvbCli), Boot(boot::BootCli), Completion(completion::CompletionCli), + Fec(fec::FecCli), Key(key::KeyCli), Ota(ota::OtaCli), Ramdisk(ramdisk::RamdiskCli), @@ -41,6 +42,7 @@ pub fn main(cancel_signal: &AtomicBool) -> Result<()> { Command::Avb(c) => avb::avb_main(&c, cancel_signal), Command::Boot(c) => boot::boot_main(&c), Command::Completion(c) => completion::completion_main(&c), + Command::Fec(c) => fec::fec_main(&c, cancel_signal), Command::Key(c) => key::key_main(&c), Command::Ota(c) => ota::ota_main(&c, cancel_signal), Command::Ramdisk(c) => ramdisk::ramdisk_main(&c), diff --git a/avbroot/src/cli/avb.rs b/avbroot/src/cli/avb.rs index 5f48684..5185c69 100644 --- a/avbroot/src/cli/avb.rs +++ b/avbroot/src/cli/avb.rs @@ -34,7 +34,7 @@ fn ensure_name_is_safe(name: &str) -> Result<()> { /// Recursively verify an image's vbmeta header and all of the chained images. /// `seen` is used to prevent cycles. `descriptors` will contain all of the hash -/// and hashtree descriptors that need to be verified. +/// and hash tree descriptors that need to be verified. pub fn verify_headers( directory: &Path, name: &str, @@ -81,7 +81,7 @@ pub fn verify_headers( }; match descriptor { - avb::Descriptor::Hashtree(_) | avb::Descriptor::Hash(_) => { + avb::Descriptor::HashTree(_) | avb::Descriptor::Hash(_) => { if let Some(prev) = descriptors.get(target_name) { if prev != descriptor { bail!("{name} descriptor does not match previous encounter"); @@ -128,13 +128,15 @@ pub fn verify_descriptors( }; match descriptor { - Descriptor::Hashtree(d) => { - status!("Verifying hashtree descriptor for: {name}"); + Descriptor::HashTree(d) => { + status!("Verifying hash tree descriptor for: {name}"); d.verify( || Ok(Box::new(BufReader::new(reader.clone()))), cancel_signal, ) - .with_context(|| format!("Failed to verify hashtree descriptor for: {name}"))?; + .with_context(|| { + format!("Failed to verify hash tree descriptor for: {name}") + })?; } Descriptor::Hash(d) => { status!("Verifying hash descriptor for: {name}"); @@ -209,6 +211,9 @@ struct DumpCli { } /// Verify vbmeta signatures. +/// +/// For hash tree descriptors, the FEC (forward error correction) data will also +/// be verified. #[derive(Debug, Parser)] struct VerifyCli { /// Path to input image. diff --git a/avbroot/src/cli/fec.rs b/avbroot/src/cli/fec.rs new file mode 100644 index 0000000..3e5e2a9 --- /dev/null +++ b/avbroot/src/cli/fec.rs @@ -0,0 +1,153 @@ +/* + * SPDX-FileCopyrightText: 2023 Andrew Gunnerson + * SPDX-License-Identifier: GPL-3.0-only + */ + +use std::{ + fs::{File, OpenOptions}, + io::{BufReader, BufWriter, Write}, + path::{Path, PathBuf}, + sync::atomic::AtomicBool, +}; + +use anyhow::{Context, Result}; +use clap::{Parser, Subcommand}; + +use crate::{ + format::fec::FecImage, + stream::{FromReader, PSeekFile, ToWriter}, +}; + +fn open_input(path: &Path, rw: bool) -> Result { + OpenOptions::new() + .read(true) + .write(rw) + .open(path) + .map(PSeekFile::new) + .with_context(|| format!("Failed to open file: {path:?}")) +} + +fn read_fec(path: &Path) -> Result { + let reader = File::open(path) + .map(BufReader::new) + .with_context(|| format!("Failed to open for reading: {path:?}"))?; + let fec = FecImage::from_reader(reader) + .with_context(|| format!("Failed to read FEC data: {path:?}"))?; + + Ok(fec) +} + +fn write_fec(path: &Path, fec: &FecImage) -> Result<()> { + let mut writer = File::create(path) + .map(BufWriter::new) + .with_context(|| format!("Failed to open for writing: {path:?}"))?; + fec.to_writer(&mut writer) + .with_context(|| format!("Failed to write FEC data: {path:?}"))?; + writer.flush()?; + + Ok(()) +} + +fn generate_subcommand(cli: &GenerateCli, cancel_signal: &AtomicBool) -> Result<()> { + let input = open_input(&cli.input, false)?; + + let fec = FecImage::generate(|| Ok(Box::new(input.clone())), cli.parity, cancel_signal) + .context("Failed to generate FEC data")?; + + write_fec(&cli.fec, &fec)?; + + Ok(()) +} + +fn verify_subcommand(cli: &VerifyCli, cancel_signal: &AtomicBool) -> Result<()> { + let input = open_input(&cli.input, false)?; + let fec = read_fec(&cli.fec)?; + + fec.verify(|| Ok(Box::new(input.clone())), cancel_signal) + .context("Failed to verify data")?; + + Ok(()) +} + +fn repair_subcommand(cli: &RepairCli, cancel_signal: &AtomicBool) -> Result<()> { + let input = open_input(&cli.input, true)?; + let fec = read_fec(&cli.fec)?; + + // The separate buffered readers and writers are safe because the function + // guarantees that every thread touches disjoint offsets and every offset is + // read and written at most once. + fec.repair( + || Ok(Box::new(input.clone())), + || Ok(Box::new(input.clone())), + cancel_signal, + ) + .context("Failed to repair file")?; + + Ok(()) +} + +pub fn fec_main(cli: &FecCli, cancel_signal: &AtomicBool) -> Result<()> { + match &cli.command { + FecCommand::Generate(c) => generate_subcommand(c, cancel_signal), + FecCommand::Verify(c) => verify_subcommand(c, cancel_signal), + FecCommand::Repair(c) => repair_subcommand(c, cancel_signal), + } +} + +/// Generate FEC data for a file. +#[derive(Debug, Parser)] +struct GenerateCli { + /// Path to input data. + #[arg(short, long, value_name = "FILE", value_parser)] + input: PathBuf, + + /// Path to output FEC data. + #[arg(short, long, value_name = "FILE", value_parser)] + fec: PathBuf, + + /// Number of parity bytes per RS block (min 2, max 24). + #[arg(short, long, value_name = "BYTES", default_value = "2")] + parity: u8, +} + +/// Verify that a file contains no errors. +#[derive(Debug, Parser)] +struct VerifyCli { + /// Path to input data. + #[arg(short, long, value_name = "FILE", value_parser)] + input: PathBuf, + + /// Path to input FEC data. + #[arg(short, long, value_name = "FILE", value_parser)] + fec: PathBuf, +} + +/// Repair a file. +#[derive(Debug, Parser)] +struct RepairCli { + /// Path to data. + /// + /// The file will be modified in place. + #[arg(short, long, value_name = "FILE", value_parser)] + input: PathBuf, + + /// Path to input FEC data. + #[arg(short, long, value_name = "FILE", value_parser)] + fec: PathBuf, +} + +#[derive(Debug, Subcommand)] +enum FecCommand { + Generate(GenerateCli), + Verify(VerifyCli), + Repair(RepairCli), +} + +/// Generate dm-verity FEC data and verify/repair files. +/// +/// These commands operate on FEC files with AOSP's header format. +#[derive(Debug, Parser)] +pub struct FecCli { + #[command(subcommand)] + command: FecCommand, +} diff --git a/avbroot/src/cli/mod.rs b/avbroot/src/cli/mod.rs index ba167ed..053e66b 100644 --- a/avbroot/src/cli/mod.rs +++ b/avbroot/src/cli/mod.rs @@ -7,6 +7,7 @@ pub mod args; pub mod avb; pub mod boot; pub mod completion; +pub mod fec; pub mod key; pub mod ota; pub mod ramdisk; diff --git a/avbroot/src/cli/ota.rs b/avbroot/src/cli/ota.rs index 97512d6..17954d8 100644 --- a/avbroot/src/cli/ota.rs +++ b/avbroot/src/cli/ota.rs @@ -382,7 +382,7 @@ fn update_vbmeta_descriptors( (Descriptor::Hash(pd), Descriptor::Hash(d)) => { *pd = d.clone(); } - (Descriptor::Hashtree(pd), Descriptor::Hashtree(d)) => { + (Descriptor::HashTree(pd), Descriptor::HashTree(d)) => { *pd = d.clone(); } _ => { diff --git a/avbroot/src/format/avb.rs b/avbroot/src/format/avb.rs index 5c40ea6..130c9da 100644 --- a/avbroot/src/format/avb.rs +++ b/avbroot/src/format/avb.rs @@ -7,7 +7,7 @@ use std::{ cmp, fmt, io::{self, Cursor, Read, Seek, SeekFrom, Write}, str, - sync::atomic::{AtomicBool, Ordering}, + sync::atomic::AtomicBool, }; use byteorder::{BigEndian, ReadBytesExt, WriteBytesExt}; @@ -20,7 +20,10 @@ use sha2::{Digest, Sha256, Sha512}; use thiserror::Error; use crate::{ - format::padding, + format::{ + fec::{self, Fec}, + padding, + }, stream::{ self, CountingReader, FromReader, ReadDiscardExt, ReadSeek, ReadStringExt, ToWriter, WriteStringExt, WriteZerosExt, @@ -54,8 +57,6 @@ pub enum Error { PaddingTooLong, #[error("{0:?} field padding contains non-zero bytes")] PaddingNotZero(&'static str), - #[error("{0:?} field + {1:?} field is out of bounds")] - OutOfBounds(&'static str, &'static str), #[error("{0:?} field size does not equal size of contained items")] IncorrectCombinedSize(&'static str), #[error("Invalid VBMeta header magic: {0:?}")] @@ -68,20 +69,31 @@ pub enum Error { UnsupportedAlgorithm(AlgorithmType), #[error("Hashing algorithm not supported: {0:?}")] UnsupportedHashAlgorithm(String), - #[error("Incorrect key size ({0} bytes) for algorithm {1:?} ({2} bytes)")] - IncorrectKeySize(usize, AlgorithmType, usize), - #[error("Expected root digest {0}, but have {1}")] - InvalidRootDigest(String, String), - #[error("Expected hash tree {0}, but have {1}")] - InvalidHashtree(String, String), + #[error("Incorrect key size ({key_size} bytes) for algorithm {algo:?} ({} bytes)", algo.public_key_len())] + IncorrectKeySize { + key_size: usize, + algo: AlgorithmType, + }, + #[error("Expected root digest {expected}, but have {actual}")] + InvalidRootDigest { expected: String, actual: String }, + #[error("Expected hash tree {expected}, but have {actual}")] + InvalidHashTree { expected: String, actual: String }, + #[error("Hash tree does not immediately follow image data")] + HashTreeGap, + #[error("FEC data does not immediately follow hash tree")] + FecDataGap, + #[error("FEC requires data block size ({data}) and hash block size ({hash}) to match")] + MismatchedFecBlockSizes { data: u32, hash: u32 }, #[error("Failed to RSA sign digest")] - RsaSignError(rsa::Error), + RsaSign(rsa::Error), #[error("Failed to RSA verify signature")] - RsaVerifyError(rsa::Error), + RsaVerify(rsa::Error), #[error("{0} byte image size is too small to fit header or footer")] ImageSizeTooSmall(u64), + #[error("FEC error")] + Fec(#[from] fec::Error), #[error("I/O error")] - IoError(#[from] io::Error), + Io(#[from] io::Error), } type Result = std::result::Result; @@ -172,11 +184,11 @@ impl AlgorithmType { Self::None | Self::Unknown(_) => vec![], Self::Sha256Rsa2048 | Self::Sha256Rsa4096 | Self::Sha256Rsa8192 => { let scheme = Pkcs1v15Sign::new::(); - key.sign(scheme, digest).map_err(Error::RsaSignError)? + key.sign(scheme, digest).map_err(Error::RsaSign)? } Self::Sha512Rsa2048 | Self::Sha512Rsa4096 | Self::Sha512Rsa8192 => { let scheme = Pkcs1v15Sign::new::(); - key.sign(scheme, digest).map_err(Error::RsaSignError)? + key.sign(scheme, digest).map_err(Error::RsaSign)? } }; @@ -189,12 +201,12 @@ impl AlgorithmType { Self::Sha256Rsa2048 | Self::Sha256Rsa4096 | Self::Sha256Rsa8192 => { let scheme = Pkcs1v15Sign::new::(); key.verify(scheme, digest, signature) - .map_err(Error::RsaVerifyError)?; + .map_err(Error::RsaVerify)?; } Self::Sha512Rsa2048 | Self::Sha512Rsa4096 | Self::Sha512Rsa8192 => { let scheme = Pkcs1v15Sign::new::(); key.verify(scheme, digest, signature) - .map_err(Error::RsaVerifyError)?; + .map_err(Error::RsaVerify)?; } } @@ -281,7 +293,7 @@ impl ToWriter for PropertyDescriptor { } #[derive(Clone, Eq, PartialEq)] -pub struct HashtreeDescriptor { +pub struct HashTreeDescriptor { pub dm_verity_version: u32, pub image_size: u64, pub tree_offset: u64, @@ -299,9 +311,9 @@ pub struct HashtreeDescriptor { pub reserved: [u8; 60], } -impl fmt::Debug for HashtreeDescriptor { +impl fmt::Debug for HashTreeDescriptor { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - f.debug_struct("HashtreeDescriptor") + f.debug_struct("HashTreeDescriptor") .field("dm_verity_version", &self.dm_verity_version) .field("image_size", &self.image_size) .field("tree_offset", &self.tree_offset) @@ -321,7 +333,7 @@ impl fmt::Debug for HashtreeDescriptor { } } -impl HashtreeDescriptor { +impl HashTreeDescriptor { /// Calculate the hash tree digests for a single level of the tree. If the /// reader's position is block-aligned and `image_size` is a multiple of the /// block size, then this function can also be used to calculate the digests @@ -342,12 +354,7 @@ impl HashtreeDescriptor { let mut result = vec![]; while image_size > 0 { - if cancel_signal.load(Ordering::SeqCst) { - return Err(io::Error::new( - io::ErrorKind::Interrupted, - "Received cancel signal", - )); - } + stream::check_cancel(cancel_signal)?; let n = image_size.min(buf.len() as u64) as usize; reader.read_exact(&mut buf[..n])?; @@ -505,10 +512,14 @@ impl HashtreeDescriptor { )?; if self.root_digest != actual_root_digest { - return Err(Error::InvalidRootDigest( - hex::encode(&self.root_digest), - hex::encode(actual_root_digest), - )); + return Err(Error::InvalidRootDigest { + expected: hex::encode(&self.root_digest), + actual: hex::encode(actual_root_digest), + }); + } + + if self.tree_offset != self.image_size { + return Err(Error::HashTreeGap); } let mut reader = open_input()?; @@ -522,21 +533,55 @@ impl HashtreeDescriptor { let expected = ring::digest::digest(algorithm, &hash_tree); let actual = ring::digest::digest(algorithm, &actual_hash_tree); - return Err(Error::InvalidRootDigest( - hex::encode(expected), - hex::encode(actual), - )); + return Err(Error::InvalidHashTree { + expected: hex::encode(expected), + actual: hex::encode(actual), + }); + } + + // The FEC data section is optional. + if self.fec_size != 0 { + if self.fec_offset != self.tree_offset + self.tree_size { + return Err(Error::FecDataGap); + } else if self.data_block_size != self.hash_block_size { + return Err(Error::MismatchedFecBlockSizes { + data: self.data_block_size, + hash: self.hash_block_size, + }); + } + + let fec_size = self + .fec_size + .to_usize() + .ok_or_else(|| Error::IntegerTooLarge("fec_size"))?; + let parity = self + .fec_num_roots + .to_u8() + .ok_or_else(|| Error::IntegerTooLarge("fec_num_roots"))?; + + // The FEC covers the hash tree as well. + let fec = Fec::new( + self.tree_offset + self.tree_size, + self.data_block_size, + parity, + )?; + + let mut fec_data = vec![0u8; fec_size]; + // Already seeked to FEC. + reader.read_exact(&mut fec_data)?; + + fec.verify(open_input, &fec_data, cancel_signal)?; } Ok(()) } } -impl DescriptorTag for HashtreeDescriptor { +impl DescriptorTag for HashTreeDescriptor { const TAG: u64 = 1; } -impl FromReader for HashtreeDescriptor { +impl FromReader for HashTreeDescriptor { type Error = Error; fn from_reader(mut reader: R) -> Result { @@ -598,7 +643,7 @@ impl FromReader for HashtreeDescriptor { } } -impl ToWriter for HashtreeDescriptor { +impl ToWriter for HashTreeDescriptor { type Error = Error; fn to_writer(&self, mut writer: W) -> Result<()> { @@ -701,10 +746,10 @@ impl HashDescriptor { let digest = context.finish(); if self.root_digest != digest.as_ref() { - return Err(Error::InvalidRootDigest( - hex::encode(&self.root_digest), - hex::encode(digest), - )); + return Err(Error::InvalidRootDigest { + expected: hex::encode(&self.root_digest), + actual: hex::encode(digest), + }); } Ok(()) @@ -939,7 +984,7 @@ impl ToWriter for ChainPartitionDescriptor { #[derive(Clone, Debug, Eq, PartialEq)] pub enum Descriptor { Property(PropertyDescriptor), - Hashtree(HashtreeDescriptor), + HashTree(HashTreeDescriptor), Hash(HashDescriptor), KernelCmdline(KernelCmdlineDescriptor), ChainPartition(ChainPartitionDescriptor), @@ -949,7 +994,7 @@ pub enum Descriptor { impl Descriptor { pub fn partition_name(&self) -> Option<&str> { match self { - Self::Hashtree(d) => Some(&d.partition_name), + Self::HashTree(d) => Some(&d.partition_name), Self::Hash(d) => Some(&d.partition_name), Self::ChainPartition(d) => Some(&d.partition_name), _ => None, @@ -971,9 +1016,9 @@ impl FromReader for Descriptor { let d = PropertyDescriptor::from_reader(&mut inner_reader)?; Self::Property(d) } - HashtreeDescriptor::TAG => { - let d = HashtreeDescriptor::from_reader(&mut inner_reader)?; - Self::Hashtree(d) + HashTreeDescriptor::TAG => { + let d = HashTreeDescriptor::from_reader(&mut inner_reader)?; + Self::HashTree(d) } HashDescriptor::TAG => { let d = HashDescriptor::from_reader(&mut inner_reader)?; @@ -1019,7 +1064,7 @@ impl ToWriter for Descriptor { d.to_writer(&mut inner_writer)?; d.get_tag() } - Self::Hashtree(d) => { + Self::HashTree(d) => { d.to_writer(&mut inner_writer)?; d.get_tag() } @@ -1210,11 +1255,10 @@ impl Header { } if key_raw.len() != self.algorithm_type.public_key_len() { - return Err(Error::IncorrectKeySize( - key_raw.len(), - self.algorithm_type, - self.algorithm_type.public_key_len(), - )); + return Err(Error::IncorrectKeySize { + key_size: key_raw.len(), + algo: self.algorithm_type, + }); } // The public key and the sizes of the hash and signature are included @@ -1381,7 +1425,7 @@ impl FromReader for Header { .checked_add(hash_size) .map_or(false, |s| s > auth_block.len()) { - return Err(Error::OutOfBounds("hash_offset", "hash_size")); + return Err(Error::IntegerTooLarge("hash_offset + hash_size")); } let hash = &auth_block[hash_offset..hash_offset + hash_size]; @@ -1389,7 +1433,7 @@ impl FromReader for Header { .checked_add(signature_size) .map_or(false, |s| s > auth_block.len()) { - return Err(Error::OutOfBounds("signature_offset", "signature_size")); + return Err(Error::IntegerTooLarge("signature_offset + signature_size")); } let signature = &auth_block[signature_offset..signature_offset + signature_size]; @@ -1399,7 +1443,9 @@ impl FromReader for Header { .checked_add(public_key_size) .map_or(false, |s| s > aux_block.len()) { - return Err(Error::OutOfBounds("public_key_offset", "public_key_size")); + return Err(Error::IntegerTooLarge( + "public_key_offset + public_key_size", + )); } let public_key = &aux_block[public_key_offset..public_key_offset + public_key_size]; @@ -1407,9 +1453,8 @@ impl FromReader for Header { .checked_add(public_key_metadata_size) .map_or(false, |s| s > aux_block.len()) { - return Err(Error::OutOfBounds( - "public_key_metadata_offset", - "public_key_metadata_size", + return Err(Error::IntegerTooLarge( + "public_key_metadata_offset + public_key_metadata_size", )); } let public_key_metadata = &aux_block @@ -1582,7 +1627,7 @@ pub fn decode_public_key(data: &[u8]) -> Result { let modulus = BigUint::from_bytes_be(&modulus_raw); let public_key = - RsaPublicKey::new(modulus, BigUint::from(65537u32)).map_err(Error::RsaVerifyError)?; + RsaPublicKey::new(modulus, BigUint::from(65537u32)).map_err(Error::RsaVerify)?; Ok(public_key) } @@ -1597,7 +1642,7 @@ pub fn load_image(mut reader: impl Read + Seek) -> Result<(Header, Option