diff --git a/crates/aqua-registry/src/lib.rs b/crates/aqua-registry/src/lib.rs index 24aafcc59f..b8c34869fc 100644 --- a/crates/aqua-registry/src/lib.rs +++ b/crates/aqua-registry/src/lib.rs @@ -12,7 +12,9 @@ pub use registry::{ AQUA_STANDARD_REGISTRY_FILES, AquaRegistry, DefaultRegistryFetcher, FileCacheStore, NoOpCacheStore, }; -pub use types::{AquaChecksumType, AquaMinisignType, AquaPackage, AquaPackageType, RegistryYaml}; +pub use types::{ + AquaChecksumType, AquaCosign, AquaMinisignType, AquaPackage, AquaPackageType, RegistryYaml, +}; use thiserror::Error; diff --git a/crates/aqua-registry/src/types.rs b/crates/aqua-registry/src/types.rs index 93eb2a6516..8f6177aef5 100644 --- a/crates/aqua-registry/src/types.rs +++ b/crates/aqua-registry/src/types.rs @@ -45,6 +45,7 @@ pub struct AquaPackage { version_filter_expr: Option, pub version_source: Option, pub checksum: Option, + pub cosign: Option, pub slsa_provenance: Option, pub minisign: Option, pub github_artifact_attestations: Option, @@ -201,6 +202,7 @@ impl Default for AquaPackage { version_filter_expr: None, version_source: None, checksum: None, + cosign: None, slsa_provenance: None, minisign: None, github_artifact_attestations: None, @@ -573,6 +575,17 @@ fn apply_override(mut orig: AquaPackage, avo: &AquaPackage) -> AquaPackage { } } + if let Some(avo_cosign) = avo.cosign.clone() { + match &mut orig.cosign { + Some(orig_cosign) => { + orig_cosign.merge(avo_cosign); + } + None => { + orig.cosign = Some(avo_cosign); + } + } + } + if let Some(avo_slsa_provenance) = avo.slsa_provenance.clone() { match &mut orig.slsa_provenance { Some(slsa_provenance) => { @@ -684,10 +697,14 @@ impl AquaChecksum { self.file_format = Some(file_format); } if let Some(cosign) = other.cosign { - if self.cosign.is_none() { - self.cosign = Some(cosign.clone()); + match &mut self.cosign { + Some(orig_cosign) => { + orig_cosign.merge(cosign); + } + None => { + self.cosign = Some(cosign); + } } - self.cosign.as_mut().unwrap().merge(cosign); } } } @@ -735,24 +752,34 @@ impl AquaCosign { } impl AquaCosignSignature { - pub fn url(&self, pkg: &AquaPackage, v: &str, os: &str, arch: &str) -> Result { + fn _url(&self, pkg: &AquaPackage, v: &str, os: &str, arch: &str) -> Result { pkg.parse_aqua_str(self.url.as_ref().unwrap(), v, &Default::default(), os, arch) } - pub fn asset(&self, pkg: &AquaPackage, v: &str, os: &str, arch: &str) -> Result { - pkg.parse_aqua_str( - self.asset.as_ref().unwrap(), - v, - &Default::default(), - os, - arch, - ) + fn asset( + &self, + pkg: &AquaPackage, + v: &str, + os: &str, + arch: &str, + filename: &str, + ) -> Result { + let mut ctx = HashMap::new(); + ctx.insert("Asset".to_string(), filename.to_string()); + pkg.parse_aqua_str(self.asset.as_ref().unwrap(), v, &ctx, os, arch) } - pub fn arg(&self, pkg: &AquaPackage, v: &str, os: &str, arch: &str) -> Result { + pub fn url( + &self, + pkg: &AquaPackage, + v: &str, + os: &str, + arch: &str, + filename: &str, + ) -> Result> { match self.r#type.as_deref().unwrap_or_default() { "github_release" => { - let asset = self.asset(pkg, v, os, arch)?; + let asset = self.asset(pkg, v, os, arch, filename)?; let repo_owner = self .repo_owner .clone() @@ -761,19 +788,18 @@ impl AquaCosignSignature { .repo_name .clone() .unwrap_or_else(|| pkg.repo_name.clone()); - let repo = format!("{repo_owner}/{repo_name}"); - Ok(format!( - "https://github.com/{repo}/releases/download/{v}/{asset}" - )) + Ok(Some(format!( + "https://github.com/{repo_owner}/{repo_name}/releases/download/{v}/{asset}" + ))) } - "http" => self.url(pkg, v, os, arch), + "http" => self._url(pkg, v, os, arch).map(Some), t => { log::warn!( "unsupported cosign signature type for {}/{}: {t}", pkg.repo_owner, pkg.repo_name ); - Ok(String::new()) + Ok(None) } } } diff --git a/e2e/backend/test_aqua_checksum_cosign b/e2e/backend/test_aqua_checksum_cosign new file mode 100644 index 0000000000..d3d151d98b --- /dev/null +++ b/e2e/backend/test_aqua_checksum_cosign @@ -0,0 +1,37 @@ +#!/usr/bin/env bash +# Test native Cosign verification for aqua packages checksums + +set -euo pipefail + +export MISE_EXPERIMENTAL=1 +export MISE_AQUA_COSIGN=true +export MISE_AQUA_SLSA=false + +echo "=== Testing Native Cosign Verification ===" + +# Test: Install sops which has cosign signatures configured (v3.8.0+) +echo "Installing sops with native Cosign verification..." + +# Capture the installation output to verify the native verification is being used +output=$(mise install aqua:getsops/sops@3.9.0 2>&1) +echo "$output" + +# Verify the native Cosign verification was used +if echo "$output" | grep -q "verify checksum with cosign"; then + echo "✅ Native Cosign verification was used" +else + echo "❌ ERROR: Cosign verification message not found in output" + echo "Output was:" + echo "$output" + exit 1 +fi + +# Verify the tool works +assert_contains "mise x aqua:getsops/sops@3.9.0 -- sops --version" "3.9.0" +echo "✓ sops installed and working correctly" + +# Cleanup +mise uninstall aqua:getsops/sops@3.9.0 || true + +echo "" +echo "=== Native Cosign Verification Test Passed ✓ ===" diff --git a/e2e/backend/test_aqua_cosign b/e2e/backend/test_aqua_cosign index 839b5e721e..1ad6a34e02 100644 --- a/e2e/backend/test_aqua_cosign +++ b/e2e/backend/test_aqua_cosign @@ -13,11 +13,11 @@ echo "=== Testing Native Cosign Verification ===" echo "Installing sops with native Cosign verification..." # Capture the installation output to verify the native verification is being used -output=$(mise install aqua:getsops/sops@3.9.0 2>&1) +output=$(mise install aqua:k0sproject/k0s@v1.33.4+k0s.0 2>&1) echo "$output" # Verify the native Cosign verification was used -if echo "$output" | grep -q "verify checksums with cosign"; then +if echo "$output" | grep -q "verify checksum with cosign"; then echo "✅ Native Cosign verification was used" else echo "❌ ERROR: Cosign verification message not found in output" @@ -27,11 +27,11 @@ else fi # Verify the tool works -assert_contains "mise x aqua:getsops/sops@3.9.0 -- sops --version" "3.9.0" -echo "✓ sops installed and working correctly" +assert_contains "mise x aqua:k0sproject/k0s@v1.33.4+k0s.0 -- k0s --version" "v1.33.4+k0s.0" +echo "✓ k0s installed and working correctly" # Cleanup -mise uninstall aqua:getsops/sops@3.9.0 || true +mise uninstall aqua:k0sproject/k0s@v1.33.4+k0s.0 || true echo "" echo "=== Native Cosign Verification Test Passed ✓ ===" diff --git a/src/aqua/aqua_registry_wrapper.rs b/src/aqua/aqua_registry_wrapper.rs index 9fc1cb6eb1..acb08d5272 100644 --- a/src/aqua/aqua_registry_wrapper.rs +++ b/src/aqua/aqua_registry_wrapper.rs @@ -117,4 +117,6 @@ fn fetch_latest_repo(repo: &Git) -> Result<()> { } // Re-export types and static for compatibility -pub use aqua_registry::{AquaChecksumType, AquaMinisignType, AquaPackage, AquaPackageType}; +pub use aqua_registry::{ + AquaChecksumType, AquaCosign, AquaMinisignType, AquaPackage, AquaPackageType, +}; diff --git a/src/backend/aqua.rs b/src/backend/aqua.rs index 58c644cd59..35a4ca7d4e 100644 --- a/src/backend/aqua.rs +++ b/src/backend/aqua.rs @@ -12,7 +12,7 @@ use crate::registry::REGISTRY; use crate::toolset::ToolVersion; use crate::{ aqua::aqua_registry_wrapper::{ - AQUA_REGISTRY, AquaChecksumType, AquaMinisignType, AquaPackage, AquaPackageType, + AQUA_REGISTRY, AquaChecksumType, AquaCosign, AquaMinisignType, AquaPackage, AquaPackageType, }, cache::{CacheManager, CacheManagerBuilder}, }; @@ -379,6 +379,7 @@ impl AquaBackend { v: &str, filename: &str, ) -> Result<()> { + self.verify_cosign(ctx, tv, pkg, v, filename).await?; self.verify_slsa(ctx, tv, pkg, v, filename).await?; self.verify_minisign(ctx, tv, pkg, v, filename).await?; self.verify_github_attestations(ctx, tv, pkg, v, filename) @@ -400,8 +401,16 @@ impl AquaBackend { let checksum_path = download_path.join(format!("{filename}.checksum")); HTTP.download_file(&url, &checksum_path, Some(ctx.pr.as_ref())) .await?; - self.cosign_checksums(ctx, pkg, v, tv, &checksum_path, &download_path) - .await?; + self.verify_checksum_cosign( + ctx, + tv, + pkg, + v, + filename, + &checksum_path, + &download_path, + ) + .await?; let mut checksum_file = file::read_to_string(&checksum_path)?; if checksum.file_format() == "regexp" { let pattern = checksum.pattern(); @@ -694,116 +703,164 @@ impl AquaBackend { Ok(()) } - async fn cosign_checksums( + async fn verify_cosign( &self, ctx: &InstallContext, + tv: &ToolVersion, pkg: &AquaPackage, v: &str, - tv: &ToolVersion, - checksum_path: &Path, - download_path: &Path, + filename: &str, ) -> Result<()> { if !Settings::get().aqua.cosign { return Ok(()); } - if let Some(cosign) = pkg.checksum.as_ref().and_then(|c| c.cosign.as_ref()) { + + if let Some(cosign) = &pkg.cosign { if cosign.enabled == Some(false) { debug!("cosign is disabled for {tv}"); return Ok(()); } + ctx.pr.set_message("verify cosign".to_string()); + let artifact_path = tv.download_path().join(filename); + self.verify_cosign_signature( + ctx, + tv, + pkg, + v, + filename, + &artifact_path, + &tv.download_path(), + cosign, + ) + .await?; + } + + Ok(()) + } + async fn verify_checksum_cosign( + &self, + ctx: &InstallContext, + tv: &ToolVersion, + pkg: &AquaPackage, + v: &str, + filename: &str, + artifact_path: &Path, + download_dir: &Path, + ) -> Result<()> { + if !Settings::get().aqua.cosign { + return Ok(()); + } + if let Some(cosign) = pkg.checksum.as_ref().and_then(|c| c.cosign.as_ref()) { + if cosign.enabled == Some(false) { + debug!("cosign for checksum is disabled for {tv}"); + return Ok(()); + } ctx.pr - .set_message("verify checksums with cosign".to_string()); + .set_message("verify checksum with cosign".to_string()); + self.verify_cosign_signature( + ctx, + tv, + pkg, + v, + filename, + artifact_path, + download_dir, + cosign, + ) + .await?; + } + Ok(()) + } - // Use native sigstore-verification crate - if let Some(key) = &cosign.key { - // Key-based verification - let key_arg = key.arg(pkg, v, os(), arch())?; - if !key_arg.is_empty() { - // Download or locate the public key - let key_path = if key_arg.starts_with("http") { - let key_path = download_path.join(get_filename_from_url(&key_arg)); - HTTP.download_file(&key_arg, &key_path, Some(ctx.pr.as_ref())) - .await?; - key_path - } else { - PathBuf::from(key_arg) - }; + #[allow(clippy::too_many_arguments)] + async fn verify_cosign_signature( + &self, + ctx: &InstallContext, + tv: &ToolVersion, + pkg: &AquaPackage, + v: &str, + filename: &str, + artifact_path: &Path, + download_path: &Path, + cosign: &AquaCosign, + ) -> Result<()> { + // Use native sigstore-verification crate + if let Some(key_url) = cosign + .key + .as_ref() + .map(|k| k.url(pkg, v, os(), arch(), filename)) + .transpose()? + .flatten() + { + // Key-based verification + // Download or locate the public key + let key_path = download_path.join(get_filename_from_url(&key_url)); + HTTP.download_file(key_url, &key_path, Some(ctx.pr.as_ref())) + .await?; - // Download signature if specified - let sig_path = if let Some(signature) = &cosign.signature { - let sig_arg = signature.arg(pkg, v, os(), arch())?; - if !sig_arg.is_empty() { - if sig_arg.starts_with("http") { - let sig_path = download_path.join(get_filename_from_url(&sig_arg)); - HTTP.download_file(&sig_arg, &sig_path, Some(ctx.pr.as_ref())) - .await?; - sig_path - } else { - PathBuf::from(sig_arg) - } - } else { - // Default signature path - checksum_path.with_extension("sig") - } - } else { - // Default signature path - checksum_path.with_extension("sig") - }; + // Download signature if specified + let sig_path = if let Some(sig_url) = cosign + .signature + .as_ref() + .map(|s| s.url(pkg, v, os(), arch(), filename)) + .transpose()? + .flatten() + { + let sig_path = download_path.join(get_filename_from_url(&sig_url)); + HTTP.download_file(sig_url, &sig_path, Some(ctx.pr.as_ref())) + .await?; + sig_path + } else { + // Default signature path + artifact_path.with_extension("sig") + }; - // Verify with key - match sigstore_verification::verify_cosign_signature_with_key( - checksum_path, - &sig_path, - &key_path, - ) - .await - { - Ok(true) => { - ctx.pr - .set_message("✓ Cosign signature verified with key".to_string()); - debug!("Cosign signature verified successfully with key for {tv}"); - } - Ok(false) => { - return Err(eyre!("Cosign signature verification failed for {tv}")); - } - Err(e) => { - return Err(eyre!("Cosign verification error for {tv}: {e}")); - } - } + // Verify with key + match sigstore_verification::verify_cosign_signature_with_key( + artifact_path, + &sig_path, + &key_path, + ) + .await + { + Ok(true) => { + ctx.pr + .set_message("✓ Cosign signature verified with key".to_string()); + debug!("Cosign signature verified successfully with key for {tv}"); } - } else if let Some(bundle) = &cosign.bundle { - // Bundle-based keyless verification - let bundle_arg = bundle.arg(pkg, v, os(), arch())?; - if !bundle_arg.is_empty() { - let bundle_path = if bundle_arg.starts_with("http") { - let bundle_path = download_path.join(get_filename_from_url(&bundle_arg)); - HTTP.download_file(&bundle_arg, &bundle_path, Some(ctx.pr.as_ref())) - .await?; - bundle_path - } else { - PathBuf::from(bundle_arg) - }; + Ok(false) => { + return Err(eyre!("Cosign signature verification failed for {tv}")); + } + Err(e) => { + return Err(eyre!("Cosign verification error for {tv}: {e}")); + } + } + } else if let Some(bundle_url) = cosign + .bundle + .as_ref() + .map(|b| b.url(pkg, v, os(), arch(), filename)) + .transpose()? + .flatten() + { + // Bundle-based keyless verification + let bundle_path = download_path.join(get_filename_from_url(&bundle_url)); + HTTP.download_file(bundle_url, &bundle_path, Some(ctx.pr.as_ref())) + .await?; - // Verify with bundle (keyless) - match sigstore_verification::verify_cosign_signature( - checksum_path, - &bundle_path, - ) - .await - { - Ok(true) => { - ctx.pr - .set_message("✓ Cosign bundle verified (keyless)".to_string()); - debug!("Cosign bundle verified successfully for {tv}"); - } - Ok(false) => { - return Err(eyre!("Cosign bundle verification failed for {tv}")); - } - Err(e) => { - return Err(eyre!("Cosign bundle verification error for {tv}: {e}")); - } - } + // Verify with bundle (keyless) + match sigstore_verification::verify_cosign_signature(artifact_path, &bundle_path).await + { + Ok(true) => { + ctx.pr + .set_message("✓ Cosign bundle verified (keyless)".to_string()); + debug!("Cosign bundle verified successfully for {tv}"); + } + Ok(false) => { + return Err(eyre!("Cosign bundle verification failed for {tv}")); + } + Err(e) => { + return Err(eyre!("Cosign bundle verification error for {tv}: {e}")); } } }