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 <xynnn@linux.alibaba.com>
This commit is contained in:
Xynnn007 2025-04-03 12:43:45 +08:00
parent 0dbf4ec39f
commit 17d0db9865
5 changed files with 270 additions and 20 deletions

7
src/agent/Cargo.lock generated
View File

@ -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",

View File

@ -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]]

191
src/agent/src/initdata.rs Normal file
View File

@ -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
/// <https://github.com/confidential-containers/trustee/blob/47d7a2338e0be76308ac19be5c0c172c592780aa/kbs/docs/initdata.md>
#[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<String>,
#[serde(rename = "cdh.toml")]
cdh_config: Option<String>,
#[serde(rename = "policy.rego")]
policy: Option<String>,
}
async fn detect_initdata_device(logger: &Logger) -> Result<Option<String>> {
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<Vec<u8>> {
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<u8>,
pub _policy: Option<String>,
}
pub async fn initialize_initdata(logger: &Logger) -> Result<Option<InitdataReturnValue>> {
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);
}
}

View File

@ -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<InitdataReturnValue>,
) -> 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<InitdataReturnValue>,
) -> 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(())

BIN
src/agent/testdata/initdata.img vendored Normal file

Binary file not shown.