From 17d0db9865fe815f9ea0ec5ac4105219fb05f14c Mon Sep 17 00:00:00 2001 From: Xynnn007 Date: Thu, 3 Apr 2025 12:43:45 +0800 Subject: [PATCH] agent: add initdata parse logic Kata-agent now will check if a device /dev/vd* with 'initdata' magic number exists. If it exists, kata-agent will try to read it. Bytes 9~16 are the length of the compressed initdata toml in little endine. Bytes starting from 17 is the compressed initdata. The initdata image device layout looks like 0 8 16 16+length ... EOF 'initdata' length gzip(initdata toml) paddings The initdata will be parsed and put as aa.toml, cdh.toml and policy.rego to /run/confidential-containers/initdata. When AgentPolicy is initialized, the default policy will be overwritten by that. When AA is to be launched, if initdata is once processed, the launch arg will include --initdata parameter. Also, if /run/confidential-containers/initdata/aa.toml exists, the launch args will include -c /run/confidential-containers/initdata/aa.toml. When CDH is to be launched, if initdata is once processed, the launch args will include -c /run/confidential-containers/initdata/cdh.toml Signed-off-by: Xynnn007 --- src/agent/Cargo.lock | 7 +- src/agent/Cargo.toml | 7 +- src/agent/src/initdata.rs | 191 ++++++++++++++++++++++++++++++++ src/agent/src/main.rs | 85 +++++++++++--- src/agent/testdata/initdata.img | Bin 0 -> 512 bytes 5 files changed, 270 insertions(+), 20 deletions(-) create mode 100644 src/agent/src/initdata.rs create mode 100644 src/agent/testdata/initdata.img diff --git a/src/agent/Cargo.lock b/src/agent/Cargo.lock index c81667cf3..4e533fe63 100644 --- a/src/agent/Cargo.lock +++ b/src/agent/Cargo.lock @@ -263,9 +263,9 @@ dependencies = [ [[package]] name = "async-compression" -version = "0.4.20" +version = "0.4.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "310c9bcae737a48ef5cdee3174184e6d548b292739ede61a1f955ef76a738861" +checksum = "59a194f9d963d8099596278594b3107448656ba73831c9d8c783e613ce86da64" dependencies = [ "flate2", "futures-core", @@ -3041,9 +3041,11 @@ name = "kata-agent" version = "0.1.0" dependencies = [ "anyhow", + "async-compression", "async-recursion 0.3.2", "async-std", "async-trait", + "base64 0.22.1", "capctl", "cdi", "cfg-if 1.0.0", @@ -3083,6 +3085,7 @@ dependencies = [ "serde", "serde_json", "serial_test", + "sha2", "slog", "slog-scope", "slog-stdlog", diff --git a/src/agent/Cargo.toml b/src/agent/Cargo.toml index f8d1474bb..f2bb54e12 100644 --- a/src/agent/Cargo.toml +++ b/src/agent/Cargo.toml @@ -85,6 +85,11 @@ image-rs = { git = "https://github.com/confidential-containers/guest-components" cdi = { git = "https://github.com/cncf-tags/container-device-interface-rs", rev = "fba5677a8e7cc962fc6e495fcec98d7d765e332a" } kata-agent-policy = { path = "policy", optional = true } +# Initdata +base64 = "0.22" +sha2 = "0.10.8" +async-compression = { version = "0.4.22", features = ["tokio", "gzip"] } + [dev-dependencies] tempfile = "3.1.0" test-utils = { path = "../libs/test-utils" } @@ -103,7 +108,7 @@ lto = true default-pull = [] seccomp = ["rustjail/seccomp"] standard-oci-runtime = ["rustjail/standard-oci-runtime"] -agent-policy = [ "kata-agent-policy" ] +agent-policy = ["kata-agent-policy"] guest-pull = ["image-rs/kata-cc-rustls-tls"] [[bin]] diff --git a/src/agent/src/initdata.rs b/src/agent/src/initdata.rs new file mode 100644 index 000000000..7afc89997 --- /dev/null +++ b/src/agent/src/initdata.rs @@ -0,0 +1,191 @@ +//! # Initdata Module +//! +//! This module will do the following things if a proper initdata device with initdata exists. +//! 1. Parse the initdata block device and extract the config files to [`INITDATA_PATH`]. +//! 2. Return the initdata and the policy (if any). + +// Copyright (c) 2025 Alibaba Cloud +// +// SPDX-License-Identifier: Apache-2.0 +// + +use std::{os::unix::fs::FileTypeExt, path::Path}; + +use anyhow::{bail, Context, Result}; +use async_compression::tokio::bufread::GzipDecoder; +use base64::{engine::general_purpose::STANDARD, Engine}; +use const_format::concatcp; +use serde::Deserialize; +use sha2::{Digest, Sha256, Sha384, Sha512}; +use slog::Logger; +use tokio::io::{AsyncReadExt, AsyncSeekExt}; + +/// This is the target directory to store the extracted initdata. +pub const INITDATA_PATH: &str = "/run/confidential-containers/initdata"; + +/// The path of AA's config file +pub const AA_CONFIG_PATH: &str = concatcp!(INITDATA_PATH, "/aa.toml"); + +/// The path of CDH's config file +pub const CDH_CONFIG_PATH: &str = concatcp!(INITDATA_PATH, "/cdh.toml"); + +/// Magic number of initdata device +pub const INITDATA_MAGIC_NUMBER: &[u8] = b"initdata"; + +/// Now only initdata `0.1.0` is defined. +const INITDATA_VERSION: &str = "0.1.0"; + +/// Initdata defined in +/// +#[derive(Deserialize)] +pub struct Initdata { + version: String, + algorithm: String, + data: DefinedFields, +} + +/// Well-defined keys for initdata of kata/CoCo +#[derive(Deserialize, Default)] +#[serde(deny_unknown_fields)] +pub struct DefinedFields { + #[serde(rename = "aa.toml")] + aa_config: Option, + #[serde(rename = "cdh.toml")] + cdh_config: Option, + #[serde(rename = "policy.rego")] + policy: Option, +} + +async fn detect_initdata_device(logger: &Logger) -> Result> { + let dev_dir = Path::new("/dev"); + let mut read_dir = tokio::fs::read_dir(dev_dir).await?; + while let Some(entry) = read_dir.next_entry().await? { + let filename = entry.file_name(); + let filename = filename.to_string_lossy(); + debug!(logger, "Initdata check device `{filename}`"); + if !filename.starts_with("vd") { + continue; + } + let path = entry.path(); + + debug!(logger, "Initdata find potential device: `{path:?}`"); + let metadata = std::fs::metadata(path.clone())?; + if !metadata.file_type().is_block_device() { + continue; + } + + let mut file = tokio::fs::File::open(&path).await?; + let mut magic = [0; 8]; + match file.read_exact(&mut magic).await { + Ok(_) => { + debug!( + logger, + "Initdata read device `{filename}` first 8 bytes: {magic:?}" + ); + if magic == INITDATA_MAGIC_NUMBER { + let path = path.as_path().to_string_lossy().to_string(); + debug!(logger, "Found initdata device {path}"); + return Ok(Some(path)); + } + } + Err(e) => debug!(logger, "Initdata read device `{filename}` failed: {e:?}"), + } + } + + Ok(None) +} + +pub async fn read_initdata(device_path: &str) -> Result> { + let initdata_devfile = tokio::fs::File::open(device_path).await?; + let mut buf_reader = tokio::io::BufReader::new(initdata_devfile); + // skip the magic number "initdata" + buf_reader.seek(std::io::SeekFrom::Start(8)).await?; + + let mut len_buf = [0u8; 8]; + buf_reader.read_exact(&mut len_buf).await?; + let length = u64::from_le_bytes(len_buf) as usize; + + let mut buf = vec![0; length]; + buf_reader.read_exact(&mut buf).await?; + let mut gzip_decoder = GzipDecoder::new(&buf[..]); + + let mut initdata = Vec::new(); + let _ = gzip_decoder.read_to_end(&mut initdata).await?; + Ok(initdata) +} + +pub struct InitdataReturnValue { + pub digest: Vec, + pub _policy: Option, +} + +pub async fn initialize_initdata(logger: &Logger) -> Result> { + let logger = logger.new(o!("subsystem" => "initdata")); + let Some(initdata_device) = detect_initdata_device(&logger).await? else { + info!( + logger, + "Initdata device not found, skip initdata initialization" + ); + return Ok(None); + }; + + tokio::fs::create_dir_all(INITDATA_PATH) + .await + .inspect_err(|e| error!(logger, "Failed to create initdata dir: {e:?}"))?; + + let initdata_content = read_initdata(&initdata_device) + .await + .inspect_err(|e| error!(logger, "Failed to read initdata: {e:?}"))?; + + let initdata: Initdata = + toml::from_slice(&initdata_content).context("parse initdata failed")?; + info!(logger, "Initdata version: {}", initdata.version); + + if initdata.version != INITDATA_VERSION { + bail!("Unsupported initdata version"); + } + + let digest = match &initdata.algorithm[..] { + "sha256" => Sha256::digest(&initdata_content).to_vec(), + "sha384" => Sha384::digest(&initdata_content).to_vec(), + "sha512" => Sha512::digest(&initdata_content).to_vec(), + others => bail!("Unsupported hash algorithm {others}"), + }; + + if let Some(config) = initdata.data.aa_config { + tokio::fs::write(AA_CONFIG_PATH, config) + .await + .context("write aa config failed")?; + info!(logger, "write AA config from initdata"); + } + + if let Some(config) = initdata.data.cdh_config { + tokio::fs::write(CDH_CONFIG_PATH, config) + .await + .context("write cdh config failed")?; + info!(logger, "write CDH config from initdata"); + } + + debug!(logger, "Initdata digest: {}", STANDARD.encode(&digest)); + + let res = InitdataReturnValue { + digest, + _policy: initdata.data.policy, + }; + + Ok(Some(res)) +} + +#[cfg(test)] +mod tests { + use crate::initdata::read_initdata; + + const INITDATA_IMG_PATH: &str = "testdata/initdata.img"; + const INITDATA_PLAINTEXT: &[u8] = b"some content"; + + #[tokio::test] + async fn parse_initdata() { + let initdata = read_initdata(INITDATA_IMG_PATH).await.unwrap(); + assert_eq!(initdata, INITDATA_PLAINTEXT); + } +} diff --git a/src/agent/src/main.rs b/src/agent/src/main.rs index 2e7698706..cc9fa53ad 100644 --- a/src/agent/src/main.rs +++ b/src/agent/src/main.rs @@ -18,10 +18,12 @@ extern crate scopeguard; #[macro_use] extern crate slog; -use anyhow::{anyhow, Context, Result}; +use anyhow::{anyhow, bail, Context, Result}; +use base64::Engine; use cfg_if::cfg_if; use clap::{AppSettings, Parser}; use const_format::{concatcp, formatcp}; +use initdata::{InitdataReturnValue, AA_CONFIG_PATH, CDH_CONFIG_PATH}; use nix::fcntl::OFlag; use nix::sys::reboot::{reboot, RebootMode}; use nix::sys::socket::{self, AddressFamily, SockFlag, SockType, VsockAddr}; @@ -33,7 +35,6 @@ use std::os::unix::fs::{self as unixfs, FileTypeExt}; use std::os::unix::io::AsRawFd; use std::path::Path; use std::process::exit; -use std::process::Command; use std::sync::Arc; use tracing::{instrument, span}; @@ -42,6 +43,7 @@ mod config; mod console; mod device; mod features; +mod initdata; mod linux_abi; mod metrics; mod mount; @@ -419,6 +421,8 @@ async fn start_sandbox( let (tx, rx) = tokio::sync::oneshot::channel(); sandbox.lock().await.sender = Some(tx); + let initdata_return_value = initdata::initialize_initdata(logger).await?; + let gc_procs = config.guest_components_procs; if !attestation_binaries_available(logger, &gc_procs) { warn!( @@ -426,7 +430,21 @@ async fn start_sandbox( "attestation binaries requested for launch not available" ); } else { - init_attestation_components(logger, config).await?; + init_attestation_components(logger, config, &initdata_return_value).await?; + } + + // if policy is given via initdata, use it + #[cfg(feature = "agent-policy")] + if let Some(initdata_return_value) = initdata_return_value { + if let Some(policy) = &initdata_return_value._policy { + info!(logger, "using policy from initdata"); + AGENT_POLICY + .lock() + .await + .set_policy(policy) + .await + .context("Failed to set policy from initdata")?; + } } let mut oma = None; @@ -472,19 +490,34 @@ fn attestation_binaries_available(logger: &Logger, procs: &GuestComponentsProcs) true } -async fn launch_guest_component_procs(logger: &Logger, config: &AgentConfig) -> Result<()> { +async fn launch_guest_component_procs( + logger: &Logger, + config: &AgentConfig, + initdata_return_value: &Option, +) -> Result<()> { if config.guest_components_procs == GuestComponentsProcs::None { return Ok(()); } debug!(logger, "spawning attestation-agent process {}", AA_PATH); + let mut aa_args = vec!["--attestation_sock", AA_ATTESTATION_URI]; + let initdata_parameter; + if let Some(initdata_return_value) = initdata_return_value { + initdata_parameter = + base64::engine::general_purpose::STANDARD.encode(&initdata_return_value.digest); + aa_args.push("--initdata"); + aa_args.push(&initdata_parameter); + } + launch_process( logger, AA_PATH, - &vec!["--attestation_sock", AA_ATTESTATION_URI], + aa_args, + Some(AA_CONFIG_PATH), AA_ATTESTATION_SOCKET, DEFAULT_LAUNCH_PROCESS_TIMEOUT, ) + .await .map_err(|e| anyhow!("launch_process {} failed: {:?}", AA_PATH, e))?; // skip launch of confidential-data-hub and api-server-rest @@ -500,10 +533,12 @@ async fn launch_guest_component_procs(logger: &Logger, config: &AgentConfig) -> launch_process( logger, CDH_PATH, - &vec![], + vec![], + Some(CDH_CONFIG_PATH), CDH_SOCKET, DEFAULT_LAUNCH_PROCESS_TIMEOUT, ) + .await .map_err(|e| anyhow!("launch_process {} failed: {:?}", CDH_PATH, e))?; // skip launch of api-server-rest @@ -519,10 +554,12 @@ async fn launch_guest_component_procs(logger: &Logger, config: &AgentConfig) -> launch_process( logger, API_SERVER_PATH, - &vec!["--features", &features.to_string()], + vec!["--features", &features.to_string()], + None, "", 0, ) + .await .map_err(|e| anyhow!("launch_process {} failed: {:?}", API_SERVER_PATH, e))?; Ok(()) @@ -532,8 +569,12 @@ async fn launch_guest_component_procs(logger: &Logger, config: &AgentConfig) -> // and the corresponding procs are enabled in the agent configuration. the process will be // launched in the background and the function will return immediately. // If the CDH is started, a CDH client will be instantiated and returned. -async fn init_attestation_components(logger: &Logger, config: &AgentConfig) -> Result<()> { - launch_guest_component_procs(logger, config).await?; +async fn init_attestation_components( + logger: &Logger, + config: &AgentConfig, + initdata_return_value: &Option, +) -> Result<()> { + launch_guest_component_procs(logger, config, initdata_return_value).await?; // If a CDH socket exists, initialize the CDH client and enable ocicrypt match tokio::fs::metadata(CDH_SOCKET).await { @@ -555,11 +596,11 @@ async fn init_attestation_components(logger: &Logger, config: &AgentConfig) -> R Ok(()) } -fn wait_for_path_to_exist(logger: &Logger, path: &str, timeout_secs: i32) -> Result<()> { +async fn wait_for_path_to_exist(logger: &Logger, path: &str, timeout_secs: i32) -> Result<()> { let p = Path::new(path); let mut attempts = 0; loop { - std::thread::sleep(std::time::Duration::from_secs(1)); + tokio::time::sleep(std::time::Duration::from_secs(1)).await; if p.exists() { return Ok(()); } @@ -576,22 +617,32 @@ fn wait_for_path_to_exist(logger: &Logger, path: &str, timeout_secs: i32) -> Res Err(anyhow!("wait for {} to exist timeout.", path)) } -fn launch_process( +async fn launch_process( logger: &Logger, path: &str, - args: &Vec<&str>, + mut args: Vec<&str>, + config: Option<&str>, unix_socket_path: &str, timeout_secs: i32, ) -> Result<()> { if !Path::new(path).exists() { - return Err(anyhow!("path {} does not exist.", path)); + bail!("path {} does not exist.", path); } + + if let Some(config_path) = config { + if Path::new(config_path).exists() { + args.push("-c"); + args.push(config_path); + } + } + if !unix_socket_path.is_empty() && Path::new(unix_socket_path).exists() { - fs::remove_file(unix_socket_path)?; + tokio::fs::remove_file(unix_socket_path).await?; } - Command::new(path).args(args).spawn()?; + + tokio::process::Command::new(path).args(args).spawn()?; if !unix_socket_path.is_empty() && timeout_secs > 0 { - wait_for_path_to_exist(logger, unix_socket_path, timeout_secs)?; + wait_for_path_to_exist(logger, unix_socket_path, timeout_secs).await?; } Ok(()) diff --git a/src/agent/testdata/initdata.img b/src/agent/testdata/initdata.img new file mode 100644 index 0000000000000000000000000000000000000000..3c5fe59181e0516aa6cfe4ce74c0832debb84242 GIT binary patch literal 512 zcmd1I%PdJrEJ;*hfB^Y!4hZAF*17Y(p&sYXYkQv6W@KSt`2XMDP~Mpbs&14T9w7h# D9wQCE literal 0 HcmV?d00001