Skip to content

Commit

Permalink
image-rs: add image block device dm-verity and mount
Browse files Browse the repository at this point in the history
Signed-off-by: ChengyuZhu6 <chengyu.zhu@intel.com>
  • Loading branch information
ChengyuZhu6 committed Jul 11, 2023
1 parent af35c06 commit fca8f07
Show file tree
Hide file tree
Showing 4 changed files with 251 additions and 2 deletions.
2 changes: 2 additions & 0 deletions image-rs/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -25,12 +25,14 @@ hex = { version = "0.4.3", optional = true }
lazy_static = { version = "1.4.0", optional = true }
libc = "0.2"
log = "0.4.14"
loopdev ="0.1.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"
ocicrypt-rs = { path = "../ocicrypt-rs", default-features = false, features = ["async-io"], optional = true }
prost = { version = "0.11", optional = true }
protobuf = { version = "3.2.0", optional = true }
regex = "1.5"
sequoia-openpgp = { version = "1.7.0", default-features = false, features = ["compression", "crypto-rust", "allow-experimental-crypto", "allow-variable-time-crypto"], optional = true }
serde = { version = ">=1.0.27", features = ["serde_derive", "rc"] }
serde_json = ">=1.0.9"
Expand Down
110 changes: 108 additions & 2 deletions image-rs/src/image.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand All @@ -39,7 +41,6 @@ use crate::nydus::{service, utils};
/// The reason for using the `/run` directory here is that in general HW-TEE,
/// the `/run` directory is mounted in `tmpfs`, which is located in the encrypted memory protected by HW-TEE.
pub const IMAGE_SECURITY_CONFIG_DIR: &str = "/run/image-security";

/// The metadata info for container image layer.
#[derive(Clone, Debug, Default, Deserialize, PartialEq, Eq)]
pub struct LayerMeta {
Expand Down Expand Up @@ -421,6 +422,33 @@ impl ImageClient {

Ok(image_id)
}
/// set_image creates a mapping backed by image block device <source_device_path> and
/// decoding <verity_options> for in-kernel verification. And mount the verity device
/// to <mount_path> with <mount_type>.
/// It will return the verity device path if succeeds and return an error if fails .
pub async fn mount_image_block_with_integrity(
&mut self,
verity_options: &str,
source_device_path: &Path,
mount_path: &Path,
mount_type: &str,
) -> Result<String> {
let parsed_data = verity::decode_verity_options(verity_options)?;
let verity_device_path = verity::create_verity_device(
&parsed_data,
source_device_path
.to_str()
.ok_or_else(|| anyhow::anyhow!("Failed to convert source device path to string"))?,
)?;
nix::mount::mount(
Some(verity_device_path.as_str()),
mount_path,
Some(mount_type),
MsFlags::MS_RDONLY,
None::<&str>,
)?;
Ok(verity_device_path)
}
}

/// Create image meta object with the image info
Expand Down Expand Up @@ -498,6 +526,8 @@ fn create_bundle(
#[cfg(test)]
mod tests {
use super::*;
use std::fs;
use std::process::Command;

#[tokio::test]
async fn test_pull_image() {
Expand Down Expand Up @@ -568,7 +598,83 @@ mod tests {
nydus_images.len()
);
}

#[tokio::test]
async fn test_mount_image_block_with_integrity() {
//create a disk image file
let work_dir = tempfile::tempdir().unwrap();
let mount_dir = tempfile::tempdir().unwrap();
let mut image_client = ImageClient::default();
let file_name = work_dir.path().join("test.file");
let default_hash_type = "sha256";
let default_data_block_size = 512;
let default_data_block_num = 1024;
let data_device_size = default_data_block_size * default_data_block_num;
let default_hash_size = 4096;
let default_resize_size = data_device_size * 4;
let data = vec![0u8; data_device_size];
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_device = verity::attach_data_to_loop_device(file_name.to_str().unwrap())
.unwrap_or_else(|err| panic!(err));
let loop_device_path = loop_device
.get_path()
.unwrap_or_else(|| panic!("failed to get loop device path"));
let mut verity_option = verity::DmVerityOption {
hashtype: default_hash_type.to_string(),
blocksize: default_data_block_size.to_string(),
hashsize: default_hash_size.to_string(),
blocknum: default_data_block_num.to_string(),
offset: data_device_size.to_string(),
hash: "".to_string(),
};
let hash_string = verity::create_hash_device(
&mut verity_option,
loop_device_path
.as_path()
.to_str()
.unwrap_or_else(|| panic!("failed to get path string")),
)
.unwrap_or_else(|err| panic!(err));

verity_option.hash = hash_string;
let serialized_option = serde_json::to_vec(&verity_option)
.unwrap_or_else(|_| panic!("failed to serialize the options"));
let encoded_option = base64::encode(serialized_option);

let res = image_client
.mount_image_block_with_integrity(
encoded_option.as_str(),
&loop_device_path,
mount_dir.path(),
"ext4",
)
.await
.unwrap_or_else(|err| panic!("Failed to set image{:?}", err));
assert!(res.contains("/dev/mapper"));
assert!(nix::mount::umount(mount_dir.path()).is_ok());

let file_name = Path::new(res.as_str())
.file_name()
.unwrap_or_else(|| panic!("Failed to extract file name"));
assert!(verity::close_verity_device(Path::new(file_name)).is_ok());
assert!(loop_device.detach().is_ok());
}
#[tokio::test]
async fn test_image_reuse() {
let work_dir = tempfile::tempdir().unwrap();
Expand Down
1 change: 1 addition & 0 deletions image-rs/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,3 +21,4 @@ pub mod signature;
pub mod snapshots;
pub mod stream;
pub mod unpack;
pub mod verity;
140 changes: 140 additions & 0 deletions image-rs/src/verity.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
use anyhow::{anyhow, bail, Result};
use base64;
use loopdev::LoopDevice;
use regex::Regex;
use serde::{Deserialize, Serialize};
use serde_json;
use std::path::Path;
use std::process::Command;

pub const VERITYSETUP_PATH: &[&str] = &["/sbin/veritysetup", "/usr/sbin/veritysetup"];

#[derive(Debug, Deserialize, Serialize)]
pub struct DmVerityOption {
/// Hash algorithm for dm-verity.
pub hashtype: String,
/// Used block size for the data device.
pub blocksize: String,
/// Used block size for the hash device.
pub hashsize: String,
/// Size of data device used in verification.
pub blocknum: String,
/// Offset of hash area/superblock on hash_device.
pub offset: String,
/// Root hash for device verification or activation.
pub hash: String,
}

/// Creates a mapping with <name> backed by data_device <source_device_path>
/// and using hash_device for in-kernel verification.
/// It will return the verity block device Path "/dev/mapper/<name>"
/// Notes: the data device and the hash device are the same one.
pub fn create_verity_device(
verity_option: &DmVerityOption,
source_device_path: &str,
) -> Result<String> {
let veritysetup_path = veritysetup_exists()?;

let output = Command::new(veritysetup_path)
.args(&[
"open",
"--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",
verity_option.blocknum.as_str(),
"--hash-offset",
verity_option.offset.as_str(),
source_device_path,
verity_option.hash.as_str(),
source_device_path,
verity_option.hash.as_str(),
])
.output()?;
if output.status.success() {
return Ok(format!("{}{}", "/dev/mapper/", verity_option.hash.as_str()));
} else {
let error_message = String::from_utf8_lossy(&output.stderr);
bail!("Failed to create dm-verity device: {}", error_message);
}
}
pub fn decode_verity_options(verity_options: &str) -> Result<DmVerityOption> {
let decoded = base64::decode(verity_options)?;
let parsed_data = serde_json::from_slice::<DmVerityOption>(&decoded)?;
Ok(parsed_data)
}

pub fn attach_data_to_loop_device(data_path: &str) -> Result<LoopDevice> {
let lc = loopdev::LoopControl::open()?;
let ld = lc.next_free()?;
ld.attach(data_path, 0)?;
Ok(ld)
}
/// Calculates and permanently stores hash verification data for data_device.
/// It will return the root hash.
pub fn create_hash_device(
verity_options: &mut DmVerityOption,
data_device_path: &str,
) -> Result<String> {
let veritysetup_path: &str = veritysetup_exists()?;
let output = Command::new(veritysetup_path)
.args(&[
"format",
"--no-superblock",
"--format=1",
"-s",
"",
&format!("--hash={}", verity_options.hashtype),
&format!("--data-block-size={}", verity_options.blocksize),
&format!("--hash-block-size={}", verity_options.hashsize),
"--data-blocks",
verity_options.blocknum.as_str(),
"--hash-offset",
verity_options.offset.as_str(),
data_device_path,
data_device_path,
])
.output()?;
if output.status.success() {
let stdout_string = String::from_utf8_lossy(&output.stdout).to_string();
let re = Regex::new(r"Root hash:\s+([a-f0-9]+)")
.map_err(|err| anyhow::anyhow!("Failed to create regex: {}", err))?;
if let Some(captures) = re.captures(stdout_string.as_str()) {
if let Some(root_hash) = captures.get(1).map(|m| m.as_str()) {
return Ok(root_hash.to_string());
}
}
bail!("Failed to find root hash");
} else {
let error_message = String::from_utf8_lossy(&output.stderr);
bail!("Failed to create hash device: {}", error_message);
}
}
pub fn close_verity_device(verity_device_path: &Path) -> Result<()> {
let veritysetup_path: &str = veritysetup_exists()?;
let file_name = verity_device_path
.file_name()
.and_then(|name| name.to_str())
.ok_or_else(|| anyhow::anyhow!("Failed to extract file name"))?;
let output = Command::new(veritysetup_path)
.args(&["close", file_name])
.output()?;

if output.status.success() {
Ok(())
} else {
let error_message = String::from_utf8_lossy(&output.stderr);
bail!("Failed to close verity device: {}", error_message);
}
}
pub fn veritysetup_exists() -> Result<&'static str> {
VERITYSETUP_PATH
.iter()
.find(|&path| Path::new(path).exists())
.copied()
.ok_or_else(|| anyhow!("Veritysetup path not found"))
}

0 comments on commit fca8f07

Please sign in to comment.