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::()