From f5c3825882b04ce816d8200c615abdcd248cc10c Mon Sep 17 00:00:00 2001 From: ChengyuZhu6 Date: Sat, 8 Jul 2023 00:08:21 +0800 Subject: [PATCH] image-rs: add image block device dm-verity and mount Implement image block device integrity check using dm-verity and mount/umount the verity device in image-rs. Signed-off-by: ChengyuZhu6 --- .github/workflows/image_rs_build.yml | 5 +- image-rs/Cargo.toml | 2 + image-rs/src/image.rs | 148 ++++++++++++ image-rs/src/lib.rs | 1 + image-rs/src/verity.rs | 327 +++++++++++++++++++++++++++ 5 files changed, 482 insertions(+), 1 deletion(-) create mode 100644 image-rs/src/verity.rs diff --git a/.github/workflows/image_rs_build.yml b/.github/workflows/image_rs_build.yml index 05783b02d..538cb25d2 100644 --- a/.github/workflows/image_rs_build.yml +++ b/.github/workflows/image_rs_build.yml @@ -56,7 +56,10 @@ jobs: run: | sudo apt-get update sudo apt-get install -y libtss2-dev - + - name: Install dm-verity dependencies + run: | + sudo apt-get update + sudo apt-get install -y libdevmapper-dev - name: Run cargo fmt check uses: actions-rs/cargo@v1 with: diff --git a/image-rs/Cargo.toml b/image-rs/Cargo.toml index 855cb02d6..383e083e7 100644 --- a/image-rs/Cargo.toml +++ b/image-rs/Cargo.toml @@ -15,6 +15,7 @@ async-trait.workspace = true attestation_agent = { path = "../attestation-agent/lib", optional = true } base64.workspace = true cfg-if = { workspace = true, optional = true } +devicemapper = "0.33.5" dircpy = { version = "0.3.12", optional = true } flate2 = "1.0" fs_extra = { version = "1.2.0", optional = true } @@ -24,6 +25,7 @@ hex = { version = "0.4.3", optional = true } lazy_static = { workspace = true, optional = true } libc = "0.2" log = "0.4.14" +loopdev ="0.4.0" nix = { version = "0.26", optional = true } oci-distribution = { git = "https://github.com/krustlet/oci-distribution.git", rev = "f44124c", default-features = false, optional = true } oci-spec = "0.5.8" diff --git a/image-rs/src/image.rs b/image-rs/src/image.rs index 976731e49..5345c40a6 100644 --- a/image-rs/src/image.rs +++ b/image-rs/src/image.rs @@ -4,6 +4,7 @@ use anyhow::{anyhow, bail, Result}; use log::warn; +use nix::mount::MsFlags; use oci_distribution::manifest::{OciDescriptor, OciImageManifest}; use oci_distribution::secrets::RegistryAuth; use oci_distribution::Reference; @@ -21,6 +22,7 @@ use crate::config::{ImageConfig, CONFIGURATION_FILE_PATH}; use crate::decoder::Compression; use crate::meta_store::{MetaStore, METAFILE}; use crate::pull::PullClient; +use crate::verity; #[cfg(feature = "snapshot-unionfs")] use crate::snapshots::occlum::unionfs::Unionfs; @@ -432,6 +434,38 @@ impl ImageClient { } } +/// mount_image_block_with_integrity creates a mapping backed by image block device and +/// decoding for in-kernel verification. And mount the verity device +/// to with . +/// It will return the verity device path if succeeds and return an error if fails . +pub fn mount_image_block_with_integrity( + verity_options: &str, + source_device_path: &Path, + mount_path: &Path, + mount_type: &str, +) -> Result { + let parsed_data = verity::decode_verity_options(verity_options)?; + let verity_device_path = verity::create_verity_device(&parsed_data, source_device_path)?; + + nix::mount::mount( + Some(verity_device_path.as_str()), + mount_path, + Some(mount_type), + MsFlags::MS_RDONLY, + None::<&str>, + )?; + Ok(verity_device_path) +} +/// umount_image_block_with_integrity umounts the filesystem and closes the verity device named verity_device_name. +pub fn umount_image_block_with_integrity( + mount_path: &Path, + verity_device_name: String, +) -> Result<()> { + nix::mount::umount(mount_path)?; + verity::close_verity_device(verity_device_name)?; + Ok(()) +} + /// Create image meta object with the image info /// Return the image meta object, oci descriptors of the unique layers, and unique diff ids. fn create_image_meta( @@ -507,6 +541,9 @@ fn create_bundle( #[cfg(test)] mod tests { use super::*; + use base64::Engine; + use std::fs; + use std::process::Command; #[tokio::test] async fn test_pull_image() { @@ -577,7 +614,118 @@ mod tests { nydus_images.len() ); } + #[tokio::test] + async fn test_mount_and_umount_image_block_with_integrity() { + const VERITYSETUP_PATH: &[&str] = &["/sbin/veritysetup", "/usr/sbin/veritysetup"]; + //create a disk image file + let work_dir = tempfile::tempdir().unwrap(); + let mount_dir = tempfile::tempdir().unwrap(); + let file_name: std::path::PathBuf = work_dir.path().join("test.file"); + let default_hash_type = "sha256"; + let default_data_block_size: u64 = 512; + let default_data_block_num: u64 = 1024; + let data_device_size = default_data_block_size * default_data_block_num; + let default_hash_size: u64 = 4096; + let default_resize_size: u64 = data_device_size * 4; + let data = vec![0u8; data_device_size as usize]; + fs::write(&file_name, &data) + .unwrap_or_else(|err| panic!("Failed to write to file: {}", err)); + Command::new("mkfs") + .args(&["-t", "ext4", file_name.to_str().unwrap()]) + .output() + .map_err(|err| format!("Failed to format disk image: {}", err)) + .unwrap_or_else(|err| panic!("{}", err)); + + Command::new("truncate") + .args(&[ + "-s", + default_resize_size.to_string().as_str(), + file_name.to_str().unwrap(), + ]) + .output() + .map_err(|err| format!("Failed to resize disk image: {}", err)) + .unwrap_or_else(|err| panic!("{}", err)); + + //find an unused loop device and attach the file to the device + let loop_control = loopdev::LoopControl::open().unwrap_or_else(|err| panic!("{}", err)); + let loop_device = loop_control + .next_free() + .unwrap_or_else(|err| panic!("{}", err)); + loop_device + .with() + .autoclear(true) + .attach(file_name.to_str().unwrap()) + .unwrap_or_else(|err| panic!("{}", err)); + let loop_device_path = loop_device + .path() + .unwrap_or_else(|| panic!("failed to get loop device path")); + let loop_device_path_str = loop_device_path + .to_str() + .unwrap_or_else(|| panic!("failed to get path string")); + + let mut verity_option = verity::DmVerityOption { + hashtype: default_hash_type.to_string(), + blocksize: default_data_block_size, + hashsize: default_hash_size, + blocknum: default_data_block_num, + offset: data_device_size, + hash: "".to_string(), + }; + // Calculates and permanently stores hash verification data for data_device. + let veritysetup_bin = VERITYSETUP_PATH + .iter() + .find(|&path| Path::new(path).exists()) + .copied() + .unwrap_or_else(|| panic!("Veritysetup path not found")); + let output = Command::new(veritysetup_bin) + .args(&[ + "format", + "--no-superblock", + "--format=1", + "-s", + "", + &format!("--hash={}", verity_option.hashtype), + &format!("--data-block-size={}", verity_option.blocksize), + &format!("--hash-block-size={}", verity_option.hashsize), + "--data-blocks", + &format!("{}", verity_option.blocknum), + "--hash-offset", + &format!("{}", verity_option.offset), + loop_device_path_str, + loop_device_path_str, + ]) + .output() + .unwrap_or_else(|err| panic!("{}", err)); + if output.status.success() { + let stdout = String::from_utf8_lossy(&output.stdout); + let lines: Vec<&str> = stdout.lines().collect(); + let hash_strings: Vec<&str> = lines[lines.len() - 1].split_whitespace().collect(); + verity_option.hash = hash_strings[2].to_string() + } else { + let error_message = String::from_utf8_lossy(&output.stderr); + panic!("Failed to create hash device: {}", error_message); + } + + let serialized_option = serde_json::to_vec(&verity_option) + .unwrap_or_else(|_| panic!("failed to serialize the options")); + let encoded_option = base64::engine::general_purpose::STANDARD.encode(serialized_option); + let res = mount_image_block_with_integrity( + encoded_option.as_str(), + &loop_device_path, + mount_dir.path(), + "ext4", + ) + .unwrap_or_else(|err| panic!("Failed to mount image block with integrity {:?}", err)); + assert!(res.contains("/dev/mapper")); + let verity_device_name = match verity::get_verity_device_name(encoded_option.as_str()) { + Ok(name) => name, + Err(err) => { + panic!("Error getting verity device name: {}", err); + } + }; + assert!(umount_image_block_with_integrity(mount_dir.path(), verity_device_name).is_ok()); + } #[tokio::test] async fn test_image_reuse() { let work_dir = tempfile::tempdir().unwrap(); diff --git a/image-rs/src/lib.rs b/image-rs/src/lib.rs index 7d25c9078..e1fe34a2c 100644 --- a/image-rs/src/lib.rs +++ b/image-rs/src/lib.rs @@ -24,3 +24,4 @@ pub mod signature; pub mod snapshots; pub mod stream; pub mod unpack; +pub mod verity; diff --git a/image-rs/src/verity.rs b/image-rs/src/verity.rs new file mode 100644 index 000000000..f088f3403 --- /dev/null +++ b/image-rs/src/verity.rs @@ -0,0 +1,327 @@ +// Copyright (c) 2023 Intel Corporation +// +// SPDX-License-Identifier: Apache-2.0 + +use anyhow::{bail, Result}; +use base64::Engine; +use devicemapper::{DevId, DmFlags, DmName, DmOptions, DM}; +use serde::{Deserialize, Serialize}; +use serde_json; +use std::path::Path; + +const SECTOR_SHIFT: u64 = 9; +const HASH_ALGORITHMS: &[&str] = &["sha1", "sha224", "sha256", "sha384", "sha512", "ripemd160"]; + +#[derive(Debug, Deserialize, Serialize)] +pub struct DmVerityOption { + /// Hash algorithm for dm-verity. + pub hashtype: String, + /// Used block size for the data device. + pub blocksize: u64, + /// Used block size for the hash device. + pub hashsize: u64, + /// Size of data device used in verification. + pub blocknum: u64, + /// Offset of hash area/superblock on hash_device. + pub offset: u64, + /// Root hash for device verification or activation. + pub hash: String, +} + +/// Creates a mapping with backed by data_device +/// and using hash_device for in-kernel verification. +/// It will return the verity block device Path "/dev/mapper/" +/// Notes: the data device and the hash device are the same one. +pub fn create_verity_device( + verity_option: &DmVerityOption, + source_device_path: &Path, +) -> Result { + let dm = DM::new()?; + let verity_name = DmName::new(&verity_option.hash)?; + let id = DevId::Name(verity_name); + let opts = DmOptions::default().set_flags(DmFlags::DM_READONLY); + let hash_start_block: u64 = + (verity_option.offset + verity_option.hashsize - 1) / verity_option.hashsize; + + // verity parameters: + // + // version: on-disk hash version + // 0 is the original format used in the Chromium OS. + // 1 is the current format that should be used for new devices. + // data_device: device containing the data the integrity of which needs to be checked. + // It may be specified as a path, like /dev/vdX, or a device number, major:minor. + // hash_device: device that that supplies the hash tree data. + // It is specified similarly to the data device path and is the same device in the function of create_verity_device. + // The hash_start should be outside of the dm-verity configured device size. + // data_blk_size: The block size in bytes on a data device. + // hash_blk_size: The size of a hash block in bytes. + // blocks: The number of data blocks on the data device. + // hash_start: offset, in hash_blk_size blocks, from the start of hash_device to the root block of the hash tree. + // algorithm: The cryptographic hash algorithm used for this device. This should be the name of the algorithm, like "sha256". + // root_hash: The hexadecimal encoding of the cryptographic hash of the root hash block and the salt. + // salt: The hexadecimal encoding of the salt value. + let verity_params = format!( + "1 {} {} {} {} {} {} {} {} {}", + source_device_path.display(), + source_device_path.display(), + verity_option.blocksize, + verity_option.hashsize, + verity_option.blocknum, + hash_start_block, + verity_option.hashtype, + verity_option.hash, + "-", + ); + // Mapping table in device mapper: : + // is 0 + // is size of device in sectors, and one sector is equal to 512 bytes. + // is name of mapping target, here "verity" for dm-verity + // are parameters for verity target + let verity_table = vec![(0, verity_option.blocknum, "verity".into(), verity_params)]; + + dm.device_create(verity_name, None, opts)?; + dm.table_load(&id, verity_table.as_slice(), opts)?; + dm.device_suspend(&id, opts)?; + Ok(format!("{}{}", "/dev/mapper/", &verity_option.hash)) +} + +pub fn get_verity_device_name(verity_options: &str) -> Result { + let parsed_data = decode_verity_options(verity_options)?; + Ok(parsed_data.hash) +} +pub fn check_verity_options(option: &DmVerityOption) -> Result<()> { + if !HASH_ALGORITHMS.contains(&option.hashtype.as_str()) { + bail!("Unsupported hash algorithm: {}", option.hashtype); + } + if verity_block_size_ok(option.blocksize) || verity_block_size_ok(option.hashsize) { + bail!( + "Unsupported verity block size: data_block_size = {},hash_block_size = {}", + option.blocksize, + option.hashsize + ); + } + if misaligned_512(option.offset) { + bail!("Unsupported verity hash offset: {}", option.offset); + } + if option.blocknum == 0 || option.offset != option.blocksize * option.blocknum { + bail!( + "Offset is not equal to blocksize * blocknum and blocknum can not be set 0: offset = {}, blocknum = {},blocksize*blocknum = {}", + option.offset, + option.blocknum, + option.blocksize * option.blocknum + ); + } + + Ok(()) +} +pub fn decode_verity_options(verity_options: &str) -> Result { + let decoded = base64::engine::general_purpose::STANDARD.decode(verity_options)?; + let parsed_data = serde_json::from_slice::(&decoded)?; + match check_verity_options(&parsed_data) { + Ok(()) => Ok(parsed_data), + Err(e) => bail!("check_verity_options error: {}", e), + } +} + +pub fn close_verity_device(verity_device_name: String) -> Result<()> { + let dm = devicemapper::DM::new()?; + let name = devicemapper::DmName::new(&verity_device_name)?; + dm.device_remove( + &devicemapper::DevId::Name(name), + devicemapper::DmOptions::default(), + )?; + Ok(()) +} +fn verity_block_size_ok(block_size: u64) -> bool { + (block_size) % 512 != 0 || !(512..=(512 * 1024)).contains(&(block_size)) +} +fn misaligned_512(offset: u64) -> bool { + (offset) & ((1 << SECTOR_SHIFT) - 1) != 0 +} + +#[cfg(test)] +mod tests { + use super::*; + use std::fs; + + #[tokio::test] + async fn test_decode_verity_options() { + let verity_option = DmVerityOption { + hashtype: "sha256".to_string(), + blocksize: 512, + hashsize: 512, + blocknum: 16384, + offset: 8388608, + hash: "9de18652fe74edfb9b805aaed72ae2aa48f94333f1ba5c452ac33b1c39325174".to_string(), + }; + let encoded = base64::engine::general_purpose::STANDARD + .encode(serde_json::to_string(&verity_option).unwrap()); + let decoded = decode_verity_options(&encoded).unwrap_or_else(|err| panic!("{}", err)); + assert_eq!(decoded.hashtype, verity_option.hashtype); + assert_eq!(decoded.blocksize, verity_option.blocksize); + assert_eq!(decoded.hashsize, verity_option.hashsize); + assert_eq!(decoded.blocknum, verity_option.blocknum); + assert_eq!(decoded.offset, verity_option.offset); + assert_eq!(decoded.hash, verity_option.hash); + } + + #[tokio::test] + async fn test_check_verity_options() { + let tests = &[ + DmVerityOption { + hashtype: "md5".to_string(), // "md5" is not a supported hash algorithm + blocksize: 512, + hashsize: 512, + blocknum: 16384, + offset: 8388608, + hash: "9de18652fe74edfb9b805aaed72ae2aa48f94333f1ba5c452ac33b1c39325174" + .to_string(), + }, + DmVerityOption { + hashtype: "sha256".to_string(), + blocksize: 3000, // Invalid block size, not a power of 2. + hashsize: 512, + blocknum: 16384, + offset: 8388608, + hash: "9de18652fe74edfb9b805aaed72ae2aa48f94333f1ba5c452ac33b1c39325174" + .to_string(), + }, + DmVerityOption { + hashtype: "sha256".to_string(), + blocksize: 0, // Invalid block size, less than 512. + hashsize: 512, + blocknum: 16384, + offset: 8388608, + hash: "9de18652fe74edfb9b805aaed72ae2aa48f94333f1ba5c452ac33b1c39325174" + .to_string(), + }, + DmVerityOption { + hashtype: "sha256".to_string(), + blocksize: 524800, // Invalid block size, greater than 524288. + hashsize: 512, + blocknum: 16384, + offset: 8388608, + hash: "9de18652fe74edfb9b805aaed72ae2aa48f94333f1ba5c452ac33b1c39325174" + .to_string(), + }, + DmVerityOption { + hashtype: "sha256".to_string(), + blocksize: 512, + hashsize: 3000, // Invalid hash block size, not a power of 2. + blocknum: 16384, + offset: 8388608, + hash: "9de18652fe74edfb9b805aaed72ae2aa48f94333f1ba5c452ac33b1c39325174" + .to_string(), + }, + DmVerityOption { + hashtype: "sha256".to_string(), + blocksize: 512, + hashsize: 0, // Invalid hash block size, less than 512. + blocknum: 16384, + offset: 8388608, + hash: "9de18652fe74edfb9b805aaed72ae2aa48f94333f1ba5c452ac33b1c39325174" + .to_string(), + }, + DmVerityOption { + hashtype: "sha256".to_string(), + blocksize: 512, + hashsize: 524800, // Invalid hash block size, greater than 524288. + blocknum: 16384, + offset: 8388608, + hash: "9de18652fe74edfb9b805aaed72ae2aa48f94333f1ba5c452ac33b1c39325174" + .to_string(), + }, + DmVerityOption { + hashtype: "sha256".to_string(), + blocksize: 512, + hashsize: 512, + blocknum: 0, // Invalid blocknum, it must be greater than 0. + offset: 8388608, + hash: "9de18652fe74edfb9b805aaed72ae2aa48f94333f1ba5c452ac33b1c39325174" + .to_string(), + }, + DmVerityOption { + hashtype: "sha256".to_string(), + blocksize: 512, + hashsize: 512, + blocknum: 16384, + offset: 0, // Invalid offset, it must be greater than 0. + hash: "9de18652fe74edfb9b805aaed72ae2aa48f94333f1ba5c452ac33b1c39325174" + .to_string(), + }, + DmVerityOption { + hashtype: "sha256".to_string(), + blocksize: 512, + hashsize: 512, + blocknum: 16384, + offset: 8193, // Invalid offset, it must be aligned to 512. + hash: "9de18652fe74edfb9b805aaed72ae2aa48f94333f1ba5c452ac33b1c39325174" + .to_string(), + }, + DmVerityOption { + hashtype: "sha256".to_string(), + blocksize: 512, + hashsize: 512, + blocknum: 16384, + offset: 8389120, // Invalid offset, it must be equal to blocksize * blocknum. + hash: "9de18652fe74edfb9b805aaed72ae2aa48f94333f1ba5c452ac33b1c39325174" + .to_string(), + }, + ]; + for d in tests.iter() { + let result = check_verity_options(&d); + assert!(result.is_err()); + } + let test_data = DmVerityOption { + hashtype: "sha256".to_string(), + blocksize: 512, + hashsize: 512, + blocknum: 16384, + offset: 8388608, + hash: "9de18652fe74edfb9b805aaed72ae2aa48f94333f1ba5c452ac33b1c39325174".to_string(), + }; + let result = check_verity_options(&test_data); + assert!(result.is_ok()); + } + + #[tokio::test] + async fn test_create_verity_device() { + let work_dir = tempfile::tempdir().unwrap(); + let file_name: std::path::PathBuf = work_dir.path().join("test.file"); + let data = vec![0u8; 1048576]; + fs::write(&file_name, &data) + .unwrap_or_else(|err| panic!("Failed to write to file: {}", err)); + + let loop_control = loopdev::LoopControl::open().unwrap_or_else(|err| panic!("{}", err)); + let loop_device = loop_control + .next_free() + .unwrap_or_else(|err| panic!("{}", err)); + loop_device + .with() + .autoclear(true) + .attach(file_name.to_str().unwrap()) + .unwrap_or_else(|err| panic!("{}", err)); + let loop_device_path = loop_device + .path() + .unwrap_or_else(|| panic!("failed to get loop device path")); + + let verity_option = DmVerityOption { + hashtype: "sha256".to_string(), + blocksize: 512, + hashsize: 4096, + blocknum: 1024, + offset: 524288, + hash: "fc65e84aa2eb12941aeaa29b000bcf1d9d4a91190bd9b10b5f51de54892952c6".to_string(), + }; + let verity_device_path = create_verity_device(&verity_option, &loop_device_path) + .unwrap_or_else(|err| panic!("{}", err)); + assert_eq!( + verity_device_path, + "/dev/mapper/fc65e84aa2eb12941aeaa29b000bcf1d9d4a91190bd9b10b5f51de54892952c6" + ); + assert!(close_verity_device( + "fc65e84aa2eb12941aeaa29b000bcf1d9d4a91190bd9b10b5f51de54892952c6".to_string() + ) + .is_ok()); + } +}