Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ composefs = { version = "0.3.0", path = "crates/composefs", default-features = f
composefs-oci = { version = "0.3.0", path = "crates/composefs-oci", default-features = false }
composefs-boot = { version = "0.3.0", path = "crates/composefs-boot", default-features = false }
composefs-http = { version = "0.3.0", path = "crates/composefs-http", default-features = false }
composefs-ostree = { version = "0.3.0", path = "crates/composefs-ostree", default-features = false }

[profile.dev.package.sha2]
# this is *really* slow otherwise
Expand Down
4 changes: 3 additions & 1 deletion crates/cfsctl/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,10 @@ rust-version.workspace = true
version.workspace = true

[features]
default = ['pre-6.15', 'oci']
default = ['pre-6.15', 'oci','ostree']
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

missing space. surprised some sort of linter didn't pick on that...

http = ['composefs-http']
oci = ['composefs-oci']
ostree = ['composefs-ostree']
rhel9 = ['composefs/rhel9']
'pre-6.15' = ['composefs/pre-6.15']

Expand All @@ -24,6 +25,7 @@ composefs = { workspace = true }
composefs-boot = { workspace = true }
composefs-oci = { workspace = true, optional = true }
composefs-http = { workspace = true, optional = true }
composefs-ostree = { workspace = true, optional = true }
env_logger = { version = "0.11.0", default-features = false }
hex = { version = "0.4.0", default-features = false }
rustix = { version = "1.0.0", default-features = false, features = ["fs", "process"] }
Expand Down
99 changes: 87 additions & 12 deletions crates/cfsctl/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ pub struct App {
enum OciCommand {
/// Stores a tar file as a splitstream in the repository.
ImportLayer {
sha256: String,
digest: String,
name: Option<String>,
},
/// Lists the contents of a tar stream
Expand Down Expand Up @@ -98,14 +98,39 @@ enum OciCommand {
},
}

#[cfg(feature = "ostree")]
#[derive(Debug, Subcommand)]
enum OstreeCommand {
PullLocal {
repo_path: PathBuf,
ostree_ref: String,
#[clap(long)]
base_name: Option<String>,
},
Pull {
repo_url: String,
ostree_ref: String,
#[clap(long)]
base_name: Option<String>,
},
CreateImage {
commit_name: String,
#[clap(long)]
image_name: Option<String>,
},
Inspect {
commit_name: String,
},
}

#[derive(Debug, Subcommand)]
enum Command {
/// Take a transaction lock on the repository.
/// This prevents garbage collection from occurring.
Transaction,
/// Reconstitutes a split stream and writes it to stdout
Cat {
/// the name of the stream to cat, either a sha256 digest or prefixed with 'ref/'
/// the name of the stream to cat, either a content identifier or prefixed with 'ref/'
name: String,
},
/// Perform garbage collection
Expand All @@ -120,9 +145,15 @@ enum Command {
#[clap(subcommand)]
cmd: OciCommand,
},
/// Commands for dealing with OSTree commits
#[cfg(feature = "ostree")]
Ostree {
#[clap(subcommand)]
cmd: OstreeCommand,
},
/// Mounts a composefs, possibly enforcing fsverity of the image
Mount {
/// the name of the image to mount, either a sha256 digest or prefixed with 'ref/'
/// the name of the image to mount, either an fs-verity hash or prefixed with 'ref/'
name: String,
/// the mountpoint
mountpoint: String,
Expand Down Expand Up @@ -194,18 +225,18 @@ async fn main() -> Result<()> {
}
}
Command::Cat { name } => {
repo.merge_splitstream(&name, None, &mut std::io::stdout())?;
repo.merge_splitstream(&name, None, None, &mut std::io::stdout())?;
}
Command::ImportImage { reference } => {
let image_id = repo.import_image(&reference, &mut std::io::stdin())?;
println!("{}", image_id.to_id());
}
#[cfg(feature = "oci")]
Command::Oci { cmd: oci_cmd } => match oci_cmd {
OciCommand::ImportLayer { name, sha256 } => {
OciCommand::ImportLayer { name, digest } => {
let object_id = composefs_oci::import_layer(
&Arc::new(repo),
&composefs::util::parse_sha256(sha256)?,
&digest,
name.as_deref(),
&mut std::io::stdin(),
)?;
Expand Down Expand Up @@ -253,20 +284,20 @@ async fn main() -> Result<()> {
println!("{}", image_id.to_id());
}
OciCommand::Pull { ref image, name } => {
let (sha256, verity) =
let (digest, verity) =
composefs_oci::pull(&Arc::new(repo), image, name.as_deref(), None).await?;

println!("sha256 {}", hex::encode(sha256));
println!("config {digest}");
println!("verity {}", verity.to_hex());
}
OciCommand::Seal {
ref config_name,
ref config_verity,
} => {
let verity = verity_opt(config_verity)?;
let (sha256, verity) =
let (digest, verity) =
composefs_oci::seal(&Arc::new(repo), config_name, verity.as_ref())?;
println!("sha256 {}", hex::encode(sha256));
println!("config {digest}");
println!("verity {}", verity.to_id());
}
OciCommand::Mount {
Expand Down Expand Up @@ -317,6 +348,50 @@ async fn main() -> Result<()> {
create_dir_all(state.join("etc/work"))?;
}
},
#[cfg(feature = "ostree")]
Command::Ostree { cmd: ostree_cmd } => match ostree_cmd {
OstreeCommand::PullLocal {
ref repo_path,
ref ostree_ref,
base_name,
} => {
let verity = composefs_ostree::pull_local(
&Arc::new(repo),
repo_path,
ostree_ref,
base_name.as_deref(),
)
.await?;

println!("verity {}", verity.to_hex());
}
OstreeCommand::Pull {
ref repo_url,
ref ostree_ref,
base_name,
} => {
let verity = composefs_ostree::pull(
&Arc::new(repo),
repo_url,
ostree_ref,
base_name.as_deref(),
)
.await?;

println!("verity {}", verity.to_hex());
}
OstreeCommand::CreateImage {
ref commit_name,
ref image_name,
} => {
let mut fs = composefs_ostree::create_filesystem(&repo, commit_name)?;
let image_id = fs.commit_image(&repo, image_name.as_deref())?;
println!("{}", image_id.to_id());
}
OstreeCommand::Inspect { ref commit_name } => {
composefs_ostree::inspect(&repo, commit_name)?;
}
},
Command::ComputeId {
ref path,
bootable,
Expand Down Expand Up @@ -367,8 +442,8 @@ async fn main() -> Result<()> {
}
#[cfg(feature = "http")]
Command::Fetch { url, name } => {
let (sha256, verity) = composefs_http::download(&url, &name, Arc::new(repo)).await?;
println!("sha256 {}", hex::encode(sha256));
let (digest, verity) = composefs_http::download(&url, &name, Arc::new(repo)).await?;
println!("content {digest}");
println!("verity {}", verity.to_hex());
}
}
Expand Down
30 changes: 10 additions & 20 deletions crates/composefs-http/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@
use std::{
collections::{HashMap, HashSet},
fs::File,
io::Read,
sync::Arc,
};

Expand All @@ -19,10 +18,7 @@ use sha2::{Digest, Sha256};
use tokio::task::JoinSet;

use composefs::{
fsverity::FsVerityHashValue,
repository::Repository,
splitstream::{DigestMapEntry, SplitStreamReader},
util::Sha256Digest,
fsverity::FsVerityHashValue, repository::Repository, splitstream::SplitStreamReader,
};

struct Downloader<ObjectID: FsVerityHashValue> {
Expand Down Expand Up @@ -66,17 +62,11 @@ impl<ObjectID: FsVerityHashValue> Downloader<ObjectID> {
}
}

fn open_splitstream(&self, id: &ObjectID) -> Result<SplitStreamReader<File, ObjectID>> {
SplitStreamReader::new(File::from(self.repo.open_object(id)?))
fn open_splitstream(&self, id: &ObjectID) -> Result<SplitStreamReader<ObjectID>> {
SplitStreamReader::new(File::from(self.repo.open_object(id)?), None)
}

fn read_object(&self, id: &ObjectID) -> Result<Vec<u8>> {
let mut data = vec![];
File::from(self.repo.open_object(id)?).read_to_end(&mut data)?;
Ok(data)
}

async fn ensure_stream(self: &Arc<Self>, name: &str) -> Result<(Sha256Digest, ObjectID)> {
async fn ensure_stream(self: &Arc<Self>, name: &str) -> Result<(String, ObjectID)> {
let progress = ProgressBar::new(2); // the first object gets "ensured" twice
progress.set_style(
ProgressStyle::with_template(
Expand Down Expand Up @@ -113,8 +103,8 @@ impl<ObjectID: FsVerityHashValue> Downloader<ObjectID> {

// this part is fast: it only touches the header
let mut reader = self.open_splitstream(&id)?;
for DigestMapEntry { verity, body } in &reader.refs.map {
match splitstreams.insert(verity.clone(), Some(*body)) {
for (body, verity) in reader.iter_named_refs() {
match splitstreams.insert(verity.clone(), Some(body.to_string())) {
// This is the (normal) case if we encounter a splitstream we didn't see yet...
None => {
splitstreams_todo.push(verity.clone());
Expand All @@ -125,7 +115,7 @@ impl<ObjectID: FsVerityHashValue> Downloader<ObjectID> {
// verify the SHA-256 content hashes later (after we get all the objects) so we
// need to make sure that all referents of this stream agree on what that is.
Some(Some(previous)) => {
if previous != *body {
if previous != body {
bail!(
"Splitstream with verity {verity:?} has different body hashes {} and {}",
hex::encode(previous),
Expand Down Expand Up @@ -208,8 +198,8 @@ impl<ObjectID: FsVerityHashValue> Downloader<ObjectID> {
for (id, expected_checksum) in splitstreams {
let mut reader = self.open_splitstream(&id)?;
let mut context = Sha256::new();
reader.cat(&mut context, |id| self.read_object(id))?;
let measured_checksum: Sha256Digest = context.finalize().into();
reader.cat(&self.repo, &mut context)?;
let measured_checksum = format!("sha256:{}", hex::encode(context.finalize()));

if let Some(expected) = expected_checksum {
if measured_checksum != expected {
Expand Down Expand Up @@ -265,7 +255,7 @@ pub async fn download<ObjectID: FsVerityHashValue>(
url: &str,
name: &str,
repo: Arc<Repository<ObjectID>>,
) -> Result<(Sha256Digest, ObjectID)> {
) -> Result<(String, ObjectID)> {
let downloader = Arc::new(Downloader {
client: Client::new(),
repo,
Expand Down
30 changes: 24 additions & 6 deletions crates/composefs-oci/src/image.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,14 +11,15 @@
use std::{ffi::OsStr, os::unix::ffi::OsStrExt, rc::Rc};

use anyhow::{ensure, Context, Result};
use oci_spec::image::ImageConfiguration;
use sha2::{Digest, Sha256};

use composefs::{
fsverity::FsVerityHashValue,
repository::Repository,
tree::{Directory, FileSystem, Inode, Leaf},
};

use crate::skopeo::TAR_LAYER_CONTENT_TYPE;
use crate::tar::{TarEntry, TarItem};

/// Processes a single tar entry and adds it to the filesystem.
Expand Down Expand Up @@ -84,21 +85,38 @@ pub fn process_entry<ObjectID: FsVerityHashValue>(

/// Creates a filesystem from the given OCI container. No special transformations are performed to
/// make the filesystem bootable.
///
/// If `config_verity` is given it is used to get the OCI config splitstream by its fs-verity ID
/// and the entire process is substantially faster. If it is not given, the config and layers will
/// be hashed to ensure that they match their claimed blob IDs.
pub fn create_filesystem<ObjectID: FsVerityHashValue>(
repo: &Repository<ObjectID>,
config_name: &str,
config_verity: Option<&ObjectID>,
) -> Result<FileSystem<ObjectID>> {
let mut filesystem = FileSystem::default();

let mut config_stream = repo.open_stream(config_name, config_verity)?;
let config = ImageConfiguration::from_reader(&mut config_stream)?;
let (config, map) = crate::open_config(repo, config_name, config_verity)?;

for diff_id in config.rootfs().diff_ids() {
let layer_sha256 = super::sha256_from_digest(diff_id)?;
let layer_verity = config_stream.lookup(&layer_sha256)?;
let layer_verity = map
.get(diff_id.as_str())
.context("OCI config splitstream missing named ref to layer {diff_id}")?;

if config_verity.is_none() {
// We don't have any proof that the named references in the config splitstream are
// trustworthy. We have no choice but to perform expensive validation of the layer
// stream.
let mut layer_stream =
repo.open_stream("", Some(layer_verity), Some(TAR_LAYER_CONTENT_TYPE))?;
let mut context = Sha256::new();
layer_stream.cat(repo, &mut context)?;
let content_hash = format!("sha256:{}", hex::encode(context.finalize()));
ensure!(content_hash == *diff_id, "Layer has incorrect checksum");
}

let mut layer_stream = repo.open_stream(&hex::encode(layer_sha256), Some(layer_verity))?;
let mut layer_stream =
repo.open_stream("", Some(layer_verity), Some(TAR_LAYER_CONTENT_TYPE))?;
while let Some(entry) = crate::tar::get_entry(&mut layer_stream)? {
process_entry(&mut filesystem, entry)?;
}
Expand Down
Loading
Loading