Skip to content

Commit

Permalink
Use layer_digest instead of diff_id and also support bulk import of i…
Browse files Browse the repository at this point in the history
…mages
  • Loading branch information
jiria committed Dec 19, 2024
1 parent baa7759 commit 8ab7127
Show file tree
Hide file tree
Showing 4 changed files with 98 additions and 72 deletions.
25 changes: 20 additions & 5 deletions src/tools/sign-oci-layer-root-hashes/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
//

use std::{
fs,
path::PathBuf,
process::{Command, Stdio},
};
Expand All @@ -29,7 +30,7 @@ struct ImageInfo {

#[derive(Serialize, Deserialize)]
struct LayerInfo {
diff_id: String,
digest: String,
root_hash: String,
signature: String,
salt: String,
Expand Down Expand Up @@ -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,
Expand All @@ -137,9 +138,23 @@ async fn get_container_image_root_hashes(
}

async fn get_root_hashes(config: &utils::Config) -> Result<Vec<ImageInfo>, Error> {
let mut image_tags: Vec<String> = 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::<Vec<String>>()
.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)),
)
Expand Down
74 changes: 40 additions & 34 deletions src/tools/sign-oci-layer-root-hashes/src/registry.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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; <Sha256 as OutputSizeUser>::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.
Expand Down Expand Up @@ -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,
}
Expand Down Expand Up @@ -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,
});
Expand All @@ -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<VerityHash> {
let temp_dir = tempfile::tempdir_in(".")?;
Expand All @@ -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 {
Expand All @@ -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)
Expand All @@ -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,
});
Expand All @@ -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<Option<VerityHash>> {
pub(crate) fn read_verity_from_store(cache_file: &str, digest: &str) -> Result<Option<VerityHash>> {
match OpenOptions::new().read(true).open(cache_file) {
Ok(file) => match serde_json::from_reader(file) {
Result::<Vec<ImageLayer>, _>::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,
}));
}
}
}
Expand Down
56 changes: 26 additions & 30 deletions src/tools/sign-oci-layer-root-hashes/src/registry_containerd.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};
Expand Down Expand Up @@ -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 {
Expand All @@ -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<VerityHash> {
let temp_dir = tempfile::tempdir_in(".")?;
Expand All @@ -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),
Expand All @@ -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
}
Expand Down
15 changes: 12 additions & 3 deletions src/tools/sign-oci-layer-root-hashes/src/utils.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<String>,
image: Option<Vec<String>>,

#[clap(
short,
long,
help = "Path to a file containing a newline-separated list of image tags"
)]
images: Option<PathBuf>,

#[clap(
short,
Expand Down Expand Up @@ -51,7 +58,8 @@ struct CommandLineOptions {
pub struct Config {
pub use_cache: bool,

pub image: Vec<String>,
pub image: Option<Vec<String>>,
pub images: Option<PathBuf>,

pub containerd_socket_path: Option<String>,
pub version: bool,
Expand All @@ -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,
Expand Down

0 comments on commit 8ab7127

Please sign in to comment.