From 8ab712776ee966460db1e2e2c0910e6d154ef0c2 Mon Sep 17 00:00:00 2001 From: Jiri Appl Date: Wed, 18 Dec 2024 16:55:48 -0800 Subject: [PATCH] Use layer_digest instead of diff_id and also support bulk import of images --- .../sign-oci-layer-root-hashes/src/main.rs | 25 +++++-- .../src/registry.rs | 74 ++++++++++--------- .../src/registry_containerd.rs | 56 +++++++------- .../sign-oci-layer-root-hashes/src/utils.rs | 15 +++- 4 files changed, 98 insertions(+), 72 deletions(-) diff --git a/src/tools/sign-oci-layer-root-hashes/src/main.rs b/src/tools/sign-oci-layer-root-hashes/src/main.rs index 21d9bcd7d338..ec32bfdcc442 100644 --- a/src/tools/sign-oci-layer-root-hashes/src/main.rs +++ b/src/tools/sign-oci-layer-root-hashes/src/main.rs @@ -4,6 +4,7 @@ // use std::{ + fs, path::PathBuf, process::{Command, Stdio}, }; @@ -29,7 +30,7 @@ struct ImageInfo { #[derive(Serialize, Deserialize)] struct LayerInfo { - diff_id: String, + digest: String, root_hash: String, signature: String, salt: String, @@ -116,12 +117,12 @@ async fn get_container_image_root_hashes( let hash_signatures = layers .iter() .map(|layer| { - let diff_id = layer.diff_id.clone(); + let digest = layer.digest.clone(); let root_hash = layer.verity_hash.clone(); let signature = sign_hash(&root_hash, &config.key, &config.passphrase, &config.signer) .context("Failed to sign hash")?; Ok(LayerInfo { - diff_id, + digest, root_hash, salt: hex::encode(layer.salt), signature, @@ -137,9 +138,23 @@ async fn get_container_image_root_hashes( } async fn get_root_hashes(config: &utils::Config) -> Result, Error> { + let mut image_tags: Vec = vec![]; + if let Some(images) = &config.images { + image_tags.append( + fs::read_to_string(images) + .context("Failed to read image tags file")? + .lines() + .map(|line| line.to_string()) + .collect::>() + .as_mut(), + ); + } else if let Some(images) = &config.image { + image_tags.append(images.clone().as_mut()); + } else { + return Err(anyhow::anyhow!("No images specified")); + }; let images = future::try_join_all( - config - .image + image_tags .iter() .map(|image| get_container_image_root_hashes(&image, config)), ) diff --git a/src/tools/sign-oci-layer-root-hashes/src/registry.rs b/src/tools/sign-oci-layer-root-hashes/src/registry.rs index c55a79cee8f4..1b8a21226d40 100644 --- a/src/tools/sign-oci-layer-root-hashes/src/registry.rs +++ b/src/tools/sign-oci-layer-root-hashes/src/registry.rs @@ -17,20 +17,20 @@ use log::{debug, info, LevelFilter}; use oci_distribution::client::{linux_amd64_resolver, ClientConfig}; use oci_distribution::{manifest, secrets::RegistryAuth, Client, Reference}; use rand::rngs::ThreadRng; +use rand::Rng; use serde::{Deserialize, Serialize}; use sha2::{digest::typenum::Unsigned, digest::OutputSizeUser, Sha256}; use std::fs::OpenOptions; use std::io::BufWriter; use std::{io, io::Seek, io::Write, path::Path}; use tokio::io::AsyncWriteExt; -use rand::Rng; pub(crate) type Salt = [u8; ::OutputSize::USIZE]; #[derive(Debug)] pub(crate) struct VerityHash { pub root_hash: String, - pub salt: Salt + pub salt: Salt, } /// Container image properties obtained from an OCI repository. @@ -69,7 +69,7 @@ pub struct DockerRootfs { /// This application's image layer properties. #[derive(Clone, Debug, Serialize, Deserialize)] pub struct ImageLayer { - pub diff_id: String, + pub digest: String, pub verity_hash: String, pub salt: Salt, } @@ -153,17 +153,11 @@ async fn get_image_layers( || layer.media_type.eq(manifest::IMAGE_LAYER_GZIP_MEDIA_TYPE) { if layer_index < config_layer.rootfs.diff_ids.len() { - let verity_hash = get_verity_hash( - use_cached_files, - client, - reference, - &layer.digest, - &config_layer.rootfs.diff_ids[layer_index].clone(), - &mut rng, - ) - .await?; + let verity_hash = + get_verity_hash(use_cached_files, &layer.digest, client, reference, &mut rng) + .await?; layers.push(ImageLayer { - diff_id: config_layer.rootfs.diff_ids[layer_index].clone(), + digest: layer.digest.clone(), verity_hash: verity_hash.root_hash, salt: verity_hash.salt, }); @@ -180,10 +174,9 @@ async fn get_image_layers( async fn get_verity_hash( use_cached_files: bool, + layer_digest: &str, client: &mut Client, reference: &Reference, - layer_digest: &str, - diff_id: &str, rng: &mut ThreadRng, ) -> Result { let temp_dir = tempfile::tempdir_in(".")?; @@ -197,15 +190,15 @@ async fn get_verity_hash( let mut compressed_path = decompressed_path.clone(); compressed_path.set_extension("gz"); - // get value from store and return if it exists let verity_hash = if use_cached_files { - let verity_hash = read_verity_from_store(cache_file, diff_id)?; + let verity_hash = read_verity_from_store(cache_file, layer_digest)?; info!("Using cache file"); - info!("dm-verity root hash: {:#?}", verity_hash); verity_hash - } else {None}; + } else { + None + }; // create the layer files let verity_hash_result = match verity_hash { @@ -218,33 +211,43 @@ async fn get_verity_hash( &decompressed_path, &compressed_path, ) - .await.context("Failed to create verity hash for {layer_digest}")?; + .await + .context("Failed to create verity hash for {layer_digest}")?; let salt: Salt = rng.gen(); - let root_hash = get_verity_hash_value(&decompressed_path, &salt).context("Failed to get verity hash")?; - let verity_hash = VerityHash{root_hash, salt}; + let root_hash = get_verity_hash_value(&decompressed_path, &salt) + .context("Failed to get verity hash")?; + let verity_hash = VerityHash { root_hash, salt }; if use_cached_files { - add_verity_to_store(cache_file, diff_id, &verity_hash)?; + add_verity_to_store(cache_file, layer_digest, &verity_hash)?; } - info!("dm-verity root hash: {:#?}", verity_hash); Ok(verity_hash) } }; temp_dir.close()?; - if verity_hash_result.is_err() { - // remove the cache file if we're using it - if use_cached_files { - std::fs::remove_file(cache_file)?; + match &verity_hash_result { + Ok(v) => { + info!("dm-verity root hash: {}", v.root_hash); } - } + Err(_) => { + // remove the cache file if we're using it + if use_cached_files { + std::fs::remove_file(cache_file)?; + } + } + }; verity_hash_result } // the store is a json file that matches layer hashes to verity hashes -pub(crate) fn add_verity_to_store(cache_file: &str, diff_id: &str, verity_hash: &VerityHash) -> Result<()> { +pub(crate) fn add_verity_to_store( + cache_file: &str, + digest: &str, + verity_hash: &VerityHash, +) -> Result<()> { // open the json file in read mode, create it if it doesn't exist let read_file = OpenOptions::new() .read(true) @@ -261,7 +264,7 @@ pub(crate) fn add_verity_to_store(cache_file: &str, diff_id: &str, verity_hash: // Add new data to the deserialized JSON data.push(ImageLayer { - diff_id: diff_id.to_string(), + digest: digest.into(), verity_hash: verity_hash.root_hash.clone(), salt: verity_hash.salt, }); @@ -288,13 +291,16 @@ pub(crate) fn add_verity_to_store(cache_file: &str, diff_id: &str, verity_hash: // helper function to read the verity hash from the store // returns empty string if not found or file does not exist -pub(crate) fn read_verity_from_store(cache_file: &str, diff_id: &str) -> Result> { +pub(crate) fn read_verity_from_store(cache_file: &str, digest: &str) -> Result> { match OpenOptions::new().read(true).open(cache_file) { Ok(file) => match serde_json::from_reader(file) { Result::, _>::Ok(layers) => { for layer in layers { - if layer.diff_id == diff_id { - return Ok(Some(VerityHash { root_hash: layer.verity_hash, salt: layer.salt })); + if layer.digest == digest { + return Ok(Some(VerityHash { + root_hash: layer.verity_hash, + salt: layer.salt, + })); } } } diff --git a/src/tools/sign-oci-layer-root-hashes/src/registry_containerd.rs b/src/tools/sign-oci-layer-root-hashes/src/registry_containerd.rs index 6d936ec9bd2b..e3621fa6cf14 100644 --- a/src/tools/sign-oci-layer-root-hashes/src/registry_containerd.rs +++ b/src/tools/sign-oci-layer-root-hashes/src/registry_containerd.rs @@ -5,9 +5,7 @@ // Allow Docker image config field names. #![allow(non_snake_case)] -use crate::registry::{ - self, Container, DockerConfigLayer, ImageLayer, Salt, VerityHash -}; +use crate::registry::{self, Container, DockerConfigLayer, ImageLayer, Salt, VerityHash}; use anyhow::{anyhow, Context, Result}; use containerd_client::{services::v1::GetImageRequest, with_namespace}; @@ -259,18 +257,13 @@ pub async fn get_image_layers( || layer_media_type.eq("application/vnd.oci.image.layer.v1.tar+gzip") { if layer_index < config_layer.rootfs.diff_ids.len() { - let verity_hash = get_verity_hash( - use_cached_files, - layer["digest"].as_str().unwrap(), - client, - &config_layer.rootfs.diff_ids[layer_index].clone(), - &mut rng, - ) - .await?; + let layer_digest = layer["digest"].as_str().unwrap(); + let verity_hash = + get_verity_hash(use_cached_files, layer_digest, client, &mut rng).await?; let imageLayer = ImageLayer { - diff_id: config_layer.rootfs.diff_ids[layer_index].clone(), + digest: layer_digest.to_string(), verity_hash: verity_hash.root_hash, - salt: verity_hash.salt + salt: verity_hash.salt, }; layersVec.push(imageLayer); } else { @@ -287,7 +280,6 @@ async fn get_verity_hash( use_cached_files: bool, layer_digest: &str, client: &containerd_client::Client, - diff_id: &str, rng: &mut ThreadRng, ) -> Result { let temp_dir = tempfile::tempdir_in(".")?; @@ -302,12 +294,13 @@ async fn get_verity_hash( compressed_path.set_extension("gz"); let verity_hash = if use_cached_files { - let verity_hash = registry::read_verity_from_store(cache_file, diff_id)?; + let verity_hash = registry::read_verity_from_store(cache_file, layer_digest)?; info!("Using cache file"); - info!("dm-verity root hash: {:#?}", verity_hash); verity_hash - } else { None }; + } else { + None + }; let verity_hash_result = match verity_hash { Some(v) => Ok(v), @@ -319,29 +312,32 @@ async fn get_verity_hash( &decompressed_path, &compressed_path, ) - .await.context("Failed to create verity hash for {layer_digest}")?; + .await + .context("Failed to create verity hash for {layer_digest}")?; let salt: Salt = rng.gen(); - let root_hash = registry::get_verity_hash_value(&decompressed_path, &salt).context("Failed to get verity hash")?; - let verity_hash = VerityHash { - root_hash, - salt, - }; + let root_hash = registry::get_verity_hash_value(&decompressed_path, &salt) + .context("Failed to get verity hash")?; + let verity_hash = VerityHash { root_hash, salt }; if use_cached_files { - registry::add_verity_to_store(cache_file, diff_id, &verity_hash)?; + registry::add_verity_to_store(cache_file, layer_digest, &verity_hash)?; } - info!("dm-verity root hash: {:#?}", verity_hash); Ok(verity_hash) } }; temp_dir.close()?; - if verity_hash_result.is_err() { - // remove the cache file if we're using it - if use_cached_files { - std::fs::remove_file(cache_file)?; + match &verity_hash_result { + Ok(v) => { + info!("dm-verity root hash: {}", v.root_hash); } - } + Err(_) => { + // remove the cache file if we're using it + if use_cached_files { + std::fs::remove_file(cache_file)?; + } + } + }; verity_hash_result } diff --git a/src/tools/sign-oci-layer-root-hashes/src/utils.rs b/src/tools/sign-oci-layer-root-hashes/src/utils.rs index 7fe312973c38..b0c265fc9060 100644 --- a/src/tools/sign-oci-layer-root-hashes/src/utils.rs +++ b/src/tools/sign-oci-layer-root-hashes/src/utils.rs @@ -3,14 +3,21 @@ // SPDX-License-Identifier: Apache-2.0 // -use std::path::{PathBuf}; +use std::path::PathBuf; use clap::Parser; #[derive(Debug, Parser)] struct CommandLineOptions { #[clap(short, long, help = "Image tag")] - image: Vec, + image: Option>, + + #[clap( + short, + long, + help = "Path to a file containing a newline-separated list of image tags" + )] + images: Option, #[clap( short, @@ -51,7 +58,8 @@ struct CommandLineOptions { pub struct Config { pub use_cache: bool, - pub image: Vec, + pub image: Option>, + pub images: Option, pub containerd_socket_path: Option, pub version: bool, @@ -67,6 +75,7 @@ impl Config { Self { use_cache: args.use_cached_files, image: args.image, + images: args.images, containerd_socket_path: args.containerd_socket_path, version: args.version, signer: args.signer,