Skip to content
Closed
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
2 changes: 2 additions & 0 deletions crates/lib/src/bootc_composefs/switch.rs
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ pub(crate) async fn switch_composefs(
bootc.operation = "switch",
bootc.target_image = target_imgref.to_string(),
bootc.apply_mode = opts.apply,
bootc.require_digest = opts.require_digest.as_ref().map(ToString::to_string),
"Starting composefs switch operation",
);

Expand Down Expand Up @@ -77,6 +78,7 @@ pub(crate) async fn switch_composefs(
apply: opts.apply,
download_only: false,
use_unified,
require_digest: opts.require_digest,
};

if let Some(cfg_verity) = image {
Expand Down
12 changes: 12 additions & 0 deletions crates/lib/src/bootc_composefs/update.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ use composefs_oci::image::create_filesystem;
use fn_error_context::context;
use ocidir::cap_std::ambient_authority;
use ostree_ext::container::ManifestDiff;
use ostree_ext::oci_spec::image::Digest;

use crate::bootc_composefs::gc::GCOpts;
use crate::spec::BootloaderKind;
Expand Down Expand Up @@ -210,6 +211,7 @@ pub(crate) struct DoUpgradeOpts {
pub(crate) download_only: bool,
/// Whether to use unified storage (containers-storage + composefs).
pub(crate) use_unified: bool,
pub(crate) require_digest: Option<Digest>,
}

async fn apply_upgrade(
Expand Down Expand Up @@ -263,6 +265,14 @@ pub(crate) async fn do_upgrade(
)
.await?;

if let Some(digest) = opts.require_digest.as_ref()
&& &manifest_digest.parse::<Digest>()? != digest
{
anyhow::bail!(
"Fetched digest does not match required digest:\n {manifest_digest} != {digest}"
);
}

// If the target image produces the same fs-verity digest as any existing
// deployment (booted, staged, rollback, or pinned), error out. Two images
// from different sources can have identical content; we cannot silently reuse
Expand Down Expand Up @@ -351,6 +361,7 @@ pub(crate) async fn upgrade_composefs(
bootc.apply_mode = opts.apply,
bootc.download_only = opts.download_only,
bootc.from_downloaded = opts.from_downloaded,
bootc.require_digest = opts.require_digest.as_ref().map(ToString::to_string),
"Starting composefs upgrade operation"
);

Expand All @@ -375,6 +386,7 @@ pub(crate) async fn upgrade_composefs(
apply: opts.apply,
download_only: opts.download_only,
use_unified: false,
require_digest: opts.require_digest,
};

if opts.from_downloaded {
Expand Down
39 changes: 39 additions & 0 deletions crates/lib/src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ use ostree_container::store::PrepareResult;
use ostree_ext::container as ostree_container;

use ostree_ext::keyfileext::KeyFileExt;
use ostree_ext::oci_spec::image::Digest;
use ostree_ext::ostree;
use ostree_ext::sysroot::SysrootLock;
use schemars::schema_for;
Expand Down Expand Up @@ -124,6 +125,14 @@ pub(crate) struct UpgradeOpts {
#[clap(long, conflicts_with_all = ["check", "download_only"])]
pub(crate) from_downloaded: bool,

/// Require the update to have the given digest, and fail if the digest does not match.
///
/// This can be useful if some external checks (such as provenance verification) have been done
/// on a specific version of the image, and one needs to ensure that the image being pulled is
/// the same version that was checked.
#[clap(long, value_name = "DIGEST")]
pub(crate) require_digest: Option<Digest>,

/// Upgrade to a different tag of the currently booted image.
///
/// This derives the target image by replacing the tag portion of the current
Expand Down Expand Up @@ -192,6 +201,14 @@ pub(crate) struct SwitchOpts {
/// Target image to use for the next boot.
pub(crate) target: String,

/// Require the update to have the given digest, and fail if the digest does not match.
///
/// This can be useful if some external checks (such as provenance verification) have been done
/// on a specific version of the image, and one needs to ensure that the image being pulled is
/// the same version that was checked.
#[clap(long, conflicts_with = "mutate_in_place", value_name = "DIGEST")]
pub(crate) require_digest: Option<Digest>,

#[clap(flatten)]
pub(crate) progress: ProgressOptions,
}
Expand Down Expand Up @@ -1244,6 +1261,12 @@ async fn upgrade(
println!(" Version: {version}");
}
println!(" Digest: {}", r.manifest_digest);
if opts
.require_digest
.is_some_and(|digest| digest != r.manifest_digest)
{
println!(" Warning: Manifest digest does not match provided digest!");
}
changed = true;
if let Some(previous_image) = booted_image.as_ref() {
let diff =
Expand Down Expand Up @@ -1279,6 +1302,13 @@ async fn upgrade(
let fetched_digest = &fetched.manifest_digest;
tracing::debug!("staged: {staged_digest:?}");
tracing::debug!("fetched: {fetched_digest}");
if let Some(digest) = opts.require_digest.as_ref()
&& fetched_digest != digest
{
anyhow::bail!(
"Fetched digest does not match required digest:\n {fetched_digest} != {digest}"
);
}
let staged_unchanged = staged_digest
.as_ref()
.map(|d| d == fetched_digest)
Expand Down Expand Up @@ -1457,6 +1487,15 @@ async fn switch_ostree(
)
.await?
};
let fetched_digest = &fetched.manifest_digest;
tracing::debug!("fetched: {fetched_digest}");
if let Some(digest) = opts.require_digest.as_ref()
&& fetched_digest != digest
{
anyhow::bail!(
"Fetched digest does not match required digest:\n {fetched_digest} != {digest}"
);
}

if !opts.retain {
// By default, we prune the previous ostree ref so it will go away after later upgrades
Expand Down
Loading