diff --git a/process/drivers.rs b/process/drivers.rs
index e90eba89..2bf99072 100644
--- a/process/drivers.rs
+++ b/process/drivers.rs
@@ -61,7 +61,6 @@ mod bootc_driver;
mod buildah_driver;
mod cosign_driver;
mod docker_driver;
-mod functions;
mod github_driver;
mod gitlab_driver;
mod local_driver;
diff --git a/process/drivers/cosign_driver.rs b/process/drivers/cosign_driver.rs
index badb7d36..cca3e21e 100644
--- a/process/drivers/cosign_driver.rs
+++ b/process/drivers/cosign_driver.rs
@@ -12,11 +12,13 @@ use miette::{Context, IntoDiagnostic, Result, bail};
use semver::VersionReq;
use serde::Deserialize;
-use crate::drivers::{DriverVersion, opts::VerifyType};
+use crate::drivers::{
+ DriverVersion,
+ opts::{PrivateKey, VerifyType},
+};
use super::{
SigningDriver,
- functions::get_private_key,
opts::{CheckKeyPairOpts, GenerateKeyPairOpts, SignOpts, VerifyOpts},
};
@@ -86,7 +88,7 @@ impl SigningDriver for CosignDriver {
fn check_signing_files(opts: CheckKeyPairOpts) -> Result<()> {
let path = opts.dir.unwrap_or_else(|| Path::new("."));
- let priv_key = get_private_key(path)?;
+ let priv_key = PrivateKey::new(path)?;
let output = {
let c = cmd!(
@@ -156,14 +158,14 @@ impl SigningDriver for CosignDriver {
Ok(())
}
- fn sign(opts: SignOpts) -> Result<()> {
- if opts.image.digest().is_none() {
- bail!(
- "Image ref {} is not a digest ref",
- opts.image.to_string().bold().red(),
- );
- }
-
+ fn sign(
+ SignOpts {
+ image,
+ metadata,
+ key,
+ }: SignOpts,
+ ) -> Result<()> {
+ let image = image.clone_with_digest(metadata.digest().into());
let status = {
let c = cmd!(
env {
@@ -172,13 +174,13 @@ impl SigningDriver for CosignDriver {
};
"cosign",
"sign",
- if let Some(key) = opts.key => format!("--key={key}"),
+ if let Some(key) = key => format!("--key={key}"),
if Self::is_v3() => [
"--new-bundle-format=false",
"--use-signing-config=false",
],
"--recursive",
- opts.image.to_string(),
+ image.to_string(),
);
trace!("{c:?}");
c
@@ -187,7 +189,7 @@ impl SigningDriver for CosignDriver {
.into_diagnostic()?;
if !status.success() {
- bail!("Failed to sign {}", opts.image.to_string().bold().red());
+ bail!("Failed to sign {}", image.to_string().bold().red());
}
Ok(())
diff --git a/process/drivers/functions.rs b/process/drivers/functions.rs
deleted file mode 100644
index 7631a96e..00000000
--- a/process/drivers/functions.rs
+++ /dev/null
@@ -1,52 +0,0 @@
-use std::path::Path;
-
-use blue_build_utils::{
- constants::{BB_PRIVATE_KEY, COSIGN_PRIV_PATH, COSIGN_PRIVATE_KEY, COSIGN_PUB_PATH},
- get_env_var, string,
-};
-use miette::{Result, bail};
-
-use super::opts::PrivateKey;
-
-pub(super) fn get_private_key
(path: P) -> Result
-where
- P: AsRef,
-{
- let path = path.as_ref();
-
- Ok(
- match (
- path.join(COSIGN_PUB_PATH).exists(),
- get_env_var(BB_PRIVATE_KEY).ok(),
- get_env_var(COSIGN_PRIVATE_KEY).ok(),
- path.join(COSIGN_PRIV_PATH),
- ) {
- (true, Some(private_key), _, _) if !private_key.is_empty() => {
- PrivateKey::Env(string!(BB_PRIVATE_KEY))
- }
- (true, _, Some(cosign_priv_key), _) if !cosign_priv_key.is_empty() => {
- PrivateKey::Env(string!(COSIGN_PRIVATE_KEY))
- }
- (true, _, _, cosign_priv_key_path) if cosign_priv_key_path.exists() => {
- PrivateKey::Path(cosign_priv_key_path)
- }
- _ => {
- bail!(
- help = format!(
- "{}{}{}{}{}{}",
- format_args!("Make sure you have a `{COSIGN_PUB_PATH}`\n"),
- format_args!(
- "in the root of your repo and have either {COSIGN_PRIVATE_KEY}\n"
- ),
- format_args!("set in your env variables or a `{COSIGN_PRIV_PATH}`\n"),
- "file in the root of your repo.\n\n",
- "See https://blue-build.org/how-to/cosign/ for more information.\n\n",
- "If you don't want to sign your image, use the `--no-sign` flag.",
- ),
- "{}",
- "Unable to find private/public key pair",
- )
- }
- },
- )
-}
diff --git a/process/drivers/oci_client_driver.rs b/process/drivers/oci_client_driver.rs
index 4c805cd8..42bfedd2 100644
--- a/process/drivers/oci_client_driver.rs
+++ b/process/drivers/oci_client_driver.rs
@@ -2,7 +2,7 @@ use blue_build_utils::credentials::Credentials;
use cached::proc_macro::cached;
use log::trace;
use miette::{IntoDiagnostic, Result};
-use oci_client::{Reference, client::ClientConfig, secrets::RegistryAuth};
+use oci_client::{Reference, client::ClientConfig, manifest::OciManifest, secrets::RegistryAuth};
use crate::{
ASYNC_RUNTIME,
@@ -29,20 +29,47 @@ impl InspectDriver for OciClientDriver {
let (manifest, digest) = ASYNC_RUNTIME
.block_on(client.pull_manifest(image, &auth))
.into_diagnostic()?;
- let (image_manifest, _image_digest) = ASYNC_RUNTIME
- .block_on(client.pull_image_manifest(image, &auth))
- .into_diagnostic()?;
- let config = {
- let mut c: Vec = vec![];
- ASYNC_RUNTIME
- .block_on(client.pull_blob(image, &image_manifest.config, &mut c))
- .into_diagnostic()?;
- c
+
+ let manifest_digests = match &manifest {
+ OciManifest::Image(_) => vec![&digest],
+ OciManifest::ImageIndex(index) => {
+ index.manifests.iter().map(|entry| &entry.digest).collect()
+ }
};
+
+ trace!("Found digests: {manifest_digests:#?}");
+
+ let configs = manifest_digests
+ .into_iter()
+ .map(|digest| {
+ let image = &image.clone_with_digest(digest.clone());
+ let (image_manifest, _) = ASYNC_RUNTIME
+ .block_on(client.pull_image_manifest(image, &auth))
+ .into_diagnostic()?;
+
+ let config = {
+ let mut c: Vec = vec![];
+ ASYNC_RUNTIME
+ .block_on(client.pull_blob(image, &image_manifest.config, &mut c))
+ .into_diagnostic()?;
+ c
+ };
+ Ok((
+ image_manifest.config.digest,
+ serde_json::from_slice(&config).into_diagnostic()?,
+ ))
+ })
+ .collect::>>()?;
+
+ trace!(
+ "Config digests: {:#?}",
+ configs.iter().map(|(digest, _)| digest)
+ );
+
Ok(ImageMetadata::builder()
.manifest(manifest)
.digest(digest)
- .config(serde_json::from_slice(&config).into_diagnostic()?)
+ .configs(configs)
.build())
}
trace!("OciClientDriver::get_metadata({opts:?})");
diff --git a/process/drivers/opts/signing.rs b/process/drivers/opts/signing.rs
index c8c8b610..2a8d01b4 100644
--- a/process/drivers/opts/signing.rs
+++ b/process/drivers/opts/signing.rs
@@ -3,18 +3,77 @@ use std::{
path::{Path, PathBuf},
};
-use blue_build_utils::{get_env_var, platform::Platform};
+use blue_build_utils::{
+ constants::{BB_PRIVATE_KEY, COSIGN_PRIV_PATH, COSIGN_PRIVATE_KEY, COSIGN_PUB_PATH},
+ get_env_var,
+ platform::Platform,
+ string,
+};
use bon::Builder;
-use miette::{IntoDiagnostic, Result};
+use miette::{IntoDiagnostic, Result, bail};
use oci_client::Reference;
use zeroize::{Zeroize, Zeroizing};
+use crate::drivers::types::ImageMetadata;
+
#[derive(Debug)]
pub enum PrivateKey {
Env(String),
Path(PathBuf),
}
+impl PrivateKey {
+ /// Create a `PrivateKey` object that tracks where the public key is.
+ ///
+ /// Contents of the `PrivateKey` are lazy loaded when `PrivateKey::contents` is called.
+ ///
+ /// # Errors
+ ///
+ /// Will error if the private key location cannot be found.
+ pub fn new(path: P) -> Result
+ where
+ P: AsRef,
+ {
+ let path = path.as_ref();
+
+ Ok(
+ match (
+ path.join(COSIGN_PUB_PATH).exists(),
+ get_env_var(BB_PRIVATE_KEY).ok(),
+ get_env_var(COSIGN_PRIVATE_KEY).ok(),
+ path.join(COSIGN_PRIV_PATH),
+ ) {
+ (true, Some(private_key), _, _) if !private_key.is_empty() => {
+ Self::Env(string!(BB_PRIVATE_KEY))
+ }
+ (true, _, Some(cosign_priv_key), _) if !cosign_priv_key.is_empty() => {
+ Self::Env(string!(COSIGN_PRIVATE_KEY))
+ }
+ (true, _, _, cosign_priv_key_path) if cosign_priv_key_path.exists() => {
+ Self::Path(cosign_priv_key_path)
+ }
+ _ => {
+ bail!(
+ help = format!(
+ "{}{}{}{}{}{}",
+ format_args!("Make sure you have a `{COSIGN_PUB_PATH}`\n"),
+ format_args!(
+ "in the root of your repo and have either {COSIGN_PRIVATE_KEY}\n"
+ ),
+ format_args!("set in your env variables or a `{COSIGN_PRIV_PATH}`\n"),
+ "file in the root of your repo.\n\n",
+ "See https://blue-build.org/how-to/cosign/ for more information.\n\n",
+ "If you don't want to sign your image, use the `--no-sign` flag.",
+ ),
+ "{}",
+ "Unable to find private/public key pair",
+ )
+ }
+ },
+ )
+ }
+}
+
impl std::fmt::Display for PrivateKey {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(
@@ -70,8 +129,8 @@ pub struct CheckKeyPairOpts<'scope> {
#[builder(derive(Debug, Clone))]
pub struct SignOpts<'scope> {
pub image: &'scope Reference,
+ pub metadata: &'scope ImageMetadata,
pub key: Option<&'scope PrivateKey>,
- pub dir: Option<&'scope Path>,
}
#[derive(Debug, Clone, Copy)]
diff --git a/process/drivers/sigstore_driver.rs b/process/drivers/sigstore_driver.rs
index 30665a73..e183443a 100644
--- a/process/drivers/sigstore_driver.rs
+++ b/process/drivers/sigstore_driver.rs
@@ -1,23 +1,22 @@
-use std::{fs, path::Path};
+use std::{collections::BTreeMap, fs, path::Path};
use crate::{
ASYNC_RUNTIME,
- drivers::opts::{PrivateKeyContents, VerifyType},
+ drivers::opts::{PrivateKey, PrivateKeyContents, VerifyType},
};
use super::{
SigningDriver,
- functions::get_private_key,
opts::{CheckKeyPairOpts, GenerateKeyPairOpts, SignOpts, VerifyOpts},
};
use blue_build_utils::{
- constants::{COSIGN_PRIV_PATH, COSIGN_PUB_PATH},
+ BUILD_ID,
+ constants::{BUILD_ID_LABEL, COSIGN_PASSWORD, COSIGN_PRIV_PATH, COSIGN_PUB_PATH},
credentials::Credentials,
retry,
};
-use colored::Colorize;
-use log::{debug, trace};
-use miette::{Context, IntoDiagnostic, bail, miette};
+use log::{debug, info, trace};
+use miette::{Context, IntoDiagnostic, Result, bail, miette};
use sigstore::{
cosign::{
ClientBuilder, Constraint, CosignCapabilities, SignatureLayer,
@@ -33,7 +32,7 @@ use zeroize::Zeroizing;
pub struct SigstoreDriver;
impl SigningDriver for SigstoreDriver {
- fn generate_key_pair(opts: GenerateKeyPairOpts) -> miette::Result<()> {
+ fn generate_key_pair(opts: GenerateKeyPairOpts) -> Result<()> {
let path = opts.dir.unwrap_or_else(|| Path::new("."));
let priv_key_path = path.join(COSIGN_PRIV_PATH);
let pub_key_path = path.join(COSIGN_PUB_PATH);
@@ -70,7 +69,7 @@ impl SigningDriver for SigstoreDriver {
Ok(())
}
- fn check_signing_files(opts: CheckKeyPairOpts) -> miette::Result<()> {
+ fn check_signing_files(opts: CheckKeyPairOpts) -> Result<()> {
trace!("SigstoreDriver::check_signing_files({opts:?})");
let path = opts.dir.unwrap_or_else(|| Path::new("."));
@@ -82,14 +81,19 @@ impl SigningDriver for SigstoreDriver {
debug!("Retrieved public key from {COSIGN_PUB_PATH}");
trace!("{pub_key}");
- let key: Zeroizing = get_private_key(path)
+ let key: Zeroizing> = PrivateKey::new(path)
.context("Failed to get private key")?
.contents()?;
debug!("Retrieved private key");
- let keypair = SigStoreKeyPair::from_encrypted_pem(key.as_bytes(), b"")
- .into_diagnostic()
- .context("Failed to generate key pair from private key")?;
+ let keypair = SigStoreKeyPair::from_encrypted_pem(
+ &key,
+ blue_build_utils::get_env_var(COSIGN_PASSWORD)
+ .unwrap_or_default()
+ .as_bytes(),
+ )
+ .into_diagnostic()
+ .context("Failed to generate key pair from private key")?;
let gen_pub = keypair
.public_key_to_pem()
.into_diagnostic()
@@ -105,33 +109,39 @@ impl SigningDriver for SigstoreDriver {
}
}
- fn sign(opts: SignOpts) -> miette::Result<()> {
- trace!("SigstoreDriver::sign({opts:?})");
-
- if opts.image.digest().is_none() {
- bail!(
- "Image ref {} is not a digest ref",
- opts.image.to_string().bold().red(),
- );
- }
+ fn sign(
+ SignOpts {
+ image,
+ metadata,
+ key,
+ }: SignOpts,
+ ) -> Result<()> {
+ trace!("SigstoreDriver::sign({image}, {metadata:?})");
+
+ let Some(key) = key else {
+ bail!("Private key is required to sign");
+ };
- let path = opts.dir.unwrap_or_else(|| Path::new("."));
let mut client = ClientBuilder::default().build().into_diagnostic()?;
- let image_digest: OciReference = opts.image.to_string().parse().into_diagnostic()?;
let signing_scheme = SigningScheme::default();
- let key: Zeroizing> = get_private_key(path)?.contents()?;
+ let key: Zeroizing> = key.contents()?;
debug!("Retrieved private key");
let signer = PrivateKeySigner::new_with_signer(
- SigStoreKeyPair::from_encrypted_pem(&key, b"")
- .into_diagnostic()?
- .to_sigstore_signer(&signing_scheme)
- .into_diagnostic()?,
+ SigStoreKeyPair::from_encrypted_pem(
+ &key,
+ blue_build_utils::get_env_var(COSIGN_PASSWORD)
+ .unwrap_or_default()
+ .as_bytes(),
+ )
+ .into_diagnostic()?
+ .to_sigstore_signer(&signing_scheme)
+ .into_diagnostic()?,
);
debug!("Created signer");
- let auth = match Credentials::get(image_digest.registry()) {
+ let auth = match Credentials::get(image.registry()) {
Some(Credentials::Basic { username, password }) => {
Auth::Basic(username, password.value().into())
}
@@ -139,44 +149,50 @@ impl SigningDriver for SigstoreDriver {
};
debug!("Credentials retrieved");
- let (cosign_signature_image, source_image_digest) = retry(2, 5, || {
- ASYNC_RUNTIME
- .block_on(client.triangulate(&image_digest, &auth))
- .into_diagnostic()
- .with_context(|| format!("Failed to triangulate image {image_digest}"))
- })?;
- debug!("Triangulating image");
- trace!("{cosign_signature_image}, {source_image_digest}");
-
- let mut signature_layer =
- SignatureLayer::new_unsigned(&image_digest, &source_image_digest).into_diagnostic()?;
- signer
- .add_constraint(&mut signature_layer)
- .into_diagnostic()?;
- debug!("Created signing layer");
-
- debug!("Pushing signature");
- retry(2, 5, || {
+ let digests = metadata.all_digests();
+ trace!("Found digests: {digests:#?}");
+
+ let signature_layers = digests
+ .into_iter()
+ .map(|digest| {
+ let image_ref = OciReference::with_digest(
+ image.registry().to_string(),
+ image.repository().to_string(),
+ digest,
+ );
+ let (cosign_sig_image, cosign_digest) = ASYNC_RUNTIME
+ .block_on(client.triangulate(&image_ref, &auth))
+ .into_diagnostic()?;
+ let mut sig_layer =
+ SignatureLayer::new_unsigned(&image_ref, &cosign_digest).into_diagnostic()?;
+ signer.add_constraint(&mut sig_layer).into_diagnostic()?;
+ Ok((cosign_sig_image, sig_layer))
+ })
+ .collect::>>()?;
+ debug!("Created signing layers");
+
+ info!("Pushing signatures");
+
+ for (ref cosign_sig_image, sig_layer) in signature_layers {
ASYNC_RUNTIME
.block_on(client.push_signature(
- None,
+ Some(BTreeMap::from_iter([(
+ BUILD_ID_LABEL.into(),
+ BUILD_ID.to_string(),
+ )])),
&auth,
- &cosign_signature_image,
- vec![signature_layer.clone()],
+ cosign_sig_image,
+ vec![sig_layer],
))
.into_diagnostic()
- .with_context(|| {
- format!(
- "Failed to push signature {cosign_signature_image} for image {image_digest}"
- )
- })
- })?;
- debug!("Successfully pushed signature");
+ .with_context(|| format!("Failed to push signatures for image {image}"))?;
+ }
+ info!("Successfully pushed signatures");
Ok(())
}
- fn verify(opts: VerifyOpts) -> miette::Result<()> {
+ fn verify(opts: VerifyOpts) -> Result<()> {
let mut client = ClientBuilder::default().build().into_diagnostic()?;
let image_digest: OciReference = opts.image.to_string().parse().into_diagnostic()?;
@@ -229,7 +245,7 @@ impl SigningDriver for SigstoreDriver {
)
}
- fn signing_login(_server: &str) -> miette::Result<()> {
+ fn signing_login(_server: &str) -> Result<()> {
Ok(())
}
}
diff --git a/process/drivers/traits.rs b/process/drivers/traits.rs
index ab34796e..64b0bfa2 100644
--- a/process/drivers/traits.rs
+++ b/process/drivers/traits.rs
@@ -22,7 +22,6 @@ use semver::VersionReq;
use super::{
Driver,
- functions::get_private_key,
opts::{
BuildChunkedOciOpts, BuildOpts, BuildRechunkTagPushOpts, BuildTagPushOpts,
CheckKeyPairOpts, ContainerOpts, CopyOciOpts, CreateContainerOpts, GenerateImageNameOpts,
@@ -38,7 +37,9 @@ use super::{
SigningDriverType,
},
};
-use crate::{logging::CommandLogging, signal_handler::DetachedContainer};
+use crate::{
+ drivers::opts::PrivateKey, logging::CommandLogging, signal_handler::DetachedContainer,
+};
trait PrivateDriver {}
@@ -945,7 +946,7 @@ pub trait SigningDriver: PrivateDriver {
.map_or_else(|| PathBuf::from("."), |d| d.to_path_buf());
let cosign_file_path = path.join(COSIGN_PUB_PATH);
- let image_digest = Driver::get_metadata(
+ let metadata = Driver::get_metadata(
GetMetadataOpts::builder()
.image(opts.image)
.no_cache(true)
@@ -954,11 +955,11 @@ pub trait SigningDriver: PrivateDriver {
let image_digest = Reference::with_digest(
opts.image.resolve_registry().into(),
opts.image.repository().into(),
- image_digest.digest().into(),
+ metadata.digest().into(),
);
let issuer = Driver::oidc_provider();
let identity = Driver::keyless_cert_identity();
- let priv_key = get_private_key(&path);
+ let priv_key = PrivateKey::new(&path);
let (sign_opts, verify_opts) =
match (Driver::get_ci_driver(), &priv_key, &issuer, &identity) {
@@ -966,8 +967,8 @@ pub trait SigningDriver: PrivateDriver {
(_, Ok(priv_key), _, _) => (
SignOpts::builder()
.image(&image_digest)
- .dir(&path)
.key(priv_key)
+ .metadata(&metadata)
.build(),
VerifyOpts::builder()
.image(&image_digest)
@@ -976,7 +977,10 @@ pub trait SigningDriver: PrivateDriver {
),
// Gitlab keyless
(CiDriverType::Github | CiDriverType::Gitlab, _, Ok(issuer), Ok(identity)) => (
- SignOpts::builder().dir(&path).image(&image_digest).build(),
+ SignOpts::builder()
+ .metadata(&metadata)
+ .image(&image_digest)
+ .build(),
VerifyOpts::builder()
.image(&image_digest)
.verify_type(VerifyType::Keyless { issuer, identity })
diff --git a/process/drivers/types/metadata.rs b/process/drivers/types/metadata.rs
index 9721f070..4043f877 100644
--- a/process/drivers/types/metadata.rs
+++ b/process/drivers/types/metadata.rs
@@ -1,3 +1,5 @@
+use std::iter::once;
+
use blue_build_utils::{constants::IMAGE_VERSION_LABEL, semver::Version};
use bon::Builder;
use miette::{Context, Result, miette};
@@ -13,7 +15,7 @@ pub struct ImageConfig {
pub struct ImageMetadata {
manifest: OciManifest,
digest: String,
- config: ImageConfig,
+ configs: Vec<(String, ImageConfig)>,
}
impl ImageMetadata {
@@ -27,18 +29,31 @@ impl ImageMetadata {
&self.manifest
}
+ #[must_use]
+ pub fn all_digests(&self) -> Vec {
+ let iter = once(self.digest.clone());
+ if let OciManifest::ImageIndex(index) = &self.manifest {
+ iter.chain(
+ index
+ .manifests
+ .iter()
+ .flat_map(|manifest| vec![manifest.digest.clone()]),
+ )
+ .collect::>()
+ } else {
+ iter.collect()
+ }
+ }
+
/// Get the version from the label if possible.
///
/// # Errors
/// Will error if labels don't exist, the version label
/// doen't exist, or the version cannot be parsed.
pub fn get_version(&self) -> Result {
- self.config
- .config
- .labels
- .as_ref()
- .ok_or_else(|| miette!("No labels found"))?
- .get(IMAGE_VERSION_LABEL)
+ self.configs
+ .iter()
+ .find_map(|(_, config)| config.config.labels.as_ref()?.get(IMAGE_VERSION_LABEL))
.ok_or_else(|| miette!("No version label found"))
.and_then(|v| {
v.parse::()