From d06e0911213fe2f6de67a1e80d425181aef1513b Mon Sep 17 00:00:00 2001 From: Valentin Haudiquet Date: Wed, 18 Mar 2026 15:23:57 +0100 Subject: [PATCH] apt/keyring: download 3 keyrings for sid --- src/apt/keyring.rs | 160 ++++++++++++++++++++++++++----------------- src/deb/ephemeral.rs | 14 ++-- src/distro_info.rs | 110 ++++++++++++++++++++++------- 3 files changed, 191 insertions(+), 93 deletions(-) diff --git a/src/apt/keyring.rs b/src/apt/keyring.rs index fd6ad96..8a7ef1a 100644 --- a/src/apt/keyring.rs +++ b/src/apt/keyring.rs @@ -16,99 +16,131 @@ struct LaunchpadPpaResponse { signing_key_fingerprint: String, } -/// Download a keyring to the application cache directory and return the path +/// Download keyrings to a shared keyring directory and return the directory path /// -/// This function downloads the keyring to a user-writable cache directory +/// This function downloads keyrings to a user-writable cache directory /// instead of the system apt keyring directory, allowing non-root usage. -/// The returned path can be passed to mmdebstrap via --keyring. +/// The returned directory path can be passed to mmdebstrap via --keyring=. /// -/// For Debian keyrings (which are ASCII-armored .asc files), the key is +/// For Debian keyrings (which are ASCII-armored .asc files), the keys are /// converted to binary GPG format using gpg --dearmor. /// +/// For 'sid' and 'experimental', this downloads keyrings from the 3 latest +/// releases since sid needs keys from all recent releases. +/// /// # Arguments /// * `ctx` - Optional context to use /// * `series` - The distribution series (e.g., "noble", "sid") /// /// # Returns -/// The path to the downloaded keyring file (in binary GPG format) -pub async fn download_cache_keyring( +/// The path to the keyring directory containing all downloaded keyring files +pub async fn download_cache_keyrings( ctx: Option>, series: &str, ) -> Result> { let ctx = ctx.unwrap_or_else(context::current); - // Obtain keyring URL from distro_info - let keyring_url = distro_info::get_keyring_url(series).await?; - log::debug!("Downloading keyring from: {}", keyring_url); + // Obtain keyring URLs from distro_info + let keyring_urls = distro_info::get_keyring_urls(series).await?; + log::debug!("Downloading keyrings from: {:?}", keyring_urls); - // Get the application cache directory - let proj_dirs = directories::ProjectDirs::from("com", "pkh", "pkh") - .ok_or("Could not determine project directories")?; - let cache_dir = proj_dirs.cache_dir(); + // Use system temp directory for keyrings since it's accessible from unshare mode + // The home directory may not be accessible from mmdebstrap's unshare namespace + let temp_dir = std::env::temp_dir(); + let keyring_dir = temp_dir.join("pkh-keyrings"); - // Create cache directory if it doesn't exist - if !ctx.exists(cache_dir)? { - ctx.command("mkdir").arg("-p").arg(cache_dir).status()?; + // Create keyring directory if it doesn't exist + if !ctx.exists(&keyring_dir)? { + ctx.command("mkdir").arg("-p").arg(&keyring_dir).status()?; } - // Extract the original filename from the keyring URL - let filename = keyring_url - .split('/') - .next_back() - .unwrap_or("pkh-{}.gpg") - .replace("{}", series); - let download_path = cache_dir.join(&filename); + // Make keyring directory world-accessible so mmdebstrap in unshare mode can access it + ctx.command("chmod") + .arg("a+rwx") + .arg(&keyring_dir) + .status()?; - // Download the keyring using curl - let mut curl_cmd = ctx.command("curl"); - curl_cmd - .arg("-s") - .arg("-f") - .arg("-L") - .arg(&keyring_url) - .arg("--output") - .arg(&download_path); + for keyring_url in keyring_urls { + // Extract the original filename from the keyring URL + let filename = keyring_url + .split('/') + .next_back() + .unwrap_or("pkh-{}.gpg") + .replace("{}", series); + let download_path = keyring_dir.join(&filename); - let status = curl_cmd.status()?; - if !status.success() { - return Err(format!("Failed to download keyring from {}", keyring_url).into()); - } + // Determine the binary keyring path + let binary_path = if filename.ends_with(".asc") { + // ASCII-armored key: convert to .gpg + let binary_filename = filename.strip_suffix(".asc").unwrap_or(&filename); + keyring_dir.join(format!("{}.gpg", binary_filename)) + } else { + download_path.clone() + }; - // If the downloaded file is an ASCII-armored key (.asc), convert it to binary GPG format - // mmdebstrap's --keyring option expects binary GPG keyrings - let keyring_path = if filename.ends_with(".asc") { - let binary_filename = filename.strip_suffix(".asc").unwrap_or(&filename); - let binary_path = cache_dir.join(format!("{}.gpg", binary_filename)); + // Skip download if the binary keyring already exists + if !ctx.exists(&binary_path)? { + // Download the keyring using curl + let mut curl_cmd = ctx.command("curl"); + curl_cmd + .arg("-s") + .arg("-f") + .arg("-L") + .arg(&keyring_url) + .arg("--output") + .arg(&download_path); - log::debug!("Converting ASCII-armored key to binary GPG format"); - let mut gpg_cmd = ctx.command("gpg"); - gpg_cmd - .arg("--dearmor") - .arg("--output") - .arg(&binary_path) - .arg(&download_path); + let status = curl_cmd.status()?; + if !status.success() { + return Err(format!("Failed to download keyring from {}", keyring_url).into()); + } - let status = gpg_cmd.status()?; - if !status.success() { - return Err("Failed to convert keyring to binary format" - .to_string() - .into()); + // If the downloaded file is an ASCII-armored key (.asc), convert it to binary GPG format + if filename.ends_with(".asc") { + log::debug!("Converting ASCII-armored key to binary GPG format"); + let mut gpg_cmd = ctx.command("gpg"); + gpg_cmd + .arg("--dearmor") + .arg("--output") + .arg(&binary_path) + .arg(&download_path); + + let status = gpg_cmd.status()?; + if !status.success() { + return Err("Failed to convert keyring to binary format" + .to_string() + .into()); + } + + // Remove the original .asc file + let _ = ctx.command("rm").arg("-f").arg(&download_path).status(); + } + + // Make the keyring file world-readable so mmdebstrap in unshare mode can access it + ctx.command("chmod").arg("a+r").arg(&binary_path).status()?; + + log::info!( + "Successfully downloaded keyring for {} to {}", + series, + binary_path.display() + ); + } else { + log::debug!( + "Keyring already exists at {}, skipping download", + binary_path.display() + ); + // Ensure existing keyring is world-readable + ctx.command("chmod").arg("a+r").arg(&binary_path).status()?; } - - // Remove the original .asc file - let _ = ctx.command("rm").arg("-f").arg(&download_path).status(); - - binary_path - } else { - download_path - }; + } log::info!( - "Successfully downloaded keyring for {} to {}", + "Keyrings for {} available in {}", series, - keyring_path.display() + keyring_dir.display() ); - Ok(keyring_path) + + Ok(keyring_dir) } /// Download and import a PPA key using Launchpad API diff --git a/src/deb/ephemeral.rs b/src/deb/ephemeral.rs index 1bb02cd..ab0ac21 100644 --- a/src/deb/ephemeral.rs +++ b/src/deb/ephemeral.rs @@ -143,9 +143,9 @@ impl EphemeralContextGuard { .arg(lockfile_path.to_string_lossy().to_string()) .status()?; - // Download the keyring to the cache directory - let keyring_path = - crate::apt::keyring::download_cache_keyring(Some(ctx.clone()), series).await?; + // Download the keyring(s) + let keyring_dir = + crate::apt::keyring::download_cache_keyrings(Some(ctx.clone()), series).await?; // Use mmdebstrap to download the tarball to the cache directory let mut cmd = ctx.command("mmdebstrap"); @@ -153,7 +153,13 @@ impl EphemeralContextGuard { .arg("--mode=unshare") .arg("--include=mount,curl,ca-certificates") .arg("--format=tar") - .arg(format!("--keyring={}", keyring_path.display())); + .arg(format!("--keyring={}", keyring_dir.display())) + // Setup hook to copy keyrings into the chroot so apt inside can use them + .arg("--setup-hook=mkdir -p \"$1/etc/apt/trusted.gpg.d\"") + .arg(format!( + "--setup-hook=cp {}/*.gpg \"$1/etc/apt/trusted.gpg.d/\"", + keyring_dir.display() + )); // Add architecture if specified if let Some(a) = arch { diff --git a/src/distro_info.rs b/src/distro_info.rs index 65dd029..d2ea4d5 100644 --- a/src/distro_info.rs +++ b/src/distro_info.rs @@ -134,6 +134,18 @@ pub async fn get_ordered_series_name(dist: &str) -> Result, Box Result> { + let latest = get_n_latest_released_series(dist, 1).await?; + latest + .first() + .cloned() + .ok_or("No released series found".into()) +} + +/// Get the N latest released series for a dist (excluding future releases and special cases like sid) +pub async fn get_n_latest_released_series( + dist: &str, + n: usize, +) -> Result, Box> { let series_info_list = get_ordered_series(dist).await?; let today = chrono::Local::now().date_naive(); @@ -153,11 +165,11 @@ pub async fn get_latest_released_series(dist: &str) -> Result String { DATA.dist.get(dist).unwrap().base_url.clone() } -/// Obtain the URL for the archive keyring of a distribution series -pub async fn get_keyring_url(series: &str) -> Result> { +/// Obtain the URLs for the archive keyrings of a distribution series +/// +/// For 'sid' and 'experimental', returns keyrings from the 3 latest releases +/// since sid needs keys from all recent releases. +pub async fn get_keyring_urls(series: &str) -> Result, Box> { let dist = get_dist_from_series(series).await?; let dist_data = DATA .dist @@ -212,24 +227,36 @@ pub async fn get_keyring_url(series: &str) -> Result> { // For Debian, we need the series number to form the keyring URL if dist == "debian" { - // Special case for 'sid' - use the latest released version + // Special case for 'sid' - use keyrings from the 3 latest released versions if series == "sid" || series == "experimental" { - let latest_released = get_latest_released_series("debian").await?; - let series_num = get_debian_series_number(&latest_released).await?.unwrap(); - // Replace {series_num} placeholder with the latest released series number - Ok(dist_data - .archive_keyring - .replace("{series_num}", &series_num)) + let latest_released = get_n_latest_released_series("debian", 3).await?; + let mut urls = Vec::new(); + for released_series in latest_released { + if let Some(series_num) = get_debian_series_number(&released_series).await? { + urls.push( + dist_data + .archive_keyring + .replace("{series_num}", &series_num), + ); + } + } + if urls.is_empty() { + Err("No keyring URLs found for sid/experimental".into()) + } else { + Ok(urls) + } } else { let series_num = get_debian_series_number(series).await?.unwrap(); // Replace {series_num} placeholder with the actual series number - Ok(dist_data - .archive_keyring - .replace("{series_num}", &series_num)) + Ok(vec![ + dist_data + .archive_keyring + .replace("{series_num}", &series_num), + ]) } } else { // For other distributions like Ubuntu, use the keyring directly - Ok(dist_data.archive_keyring.clone()) + Ok(vec![dist_data.archive_keyring.clone()]) } } @@ -347,14 +374,47 @@ mod tests { } #[tokio::test] - async fn test_get_keyring_url_sid() { - // Test that 'sid' uses the latest released version for keyring URL - let sid_keyring = get_keyring_url("sid").await.unwrap(); - let latest_released = get_latest_released_series("debian").await.unwrap(); - let latest_keyring = get_keyring_url(&latest_released).await.unwrap(); + async fn test_get_keyring_urls_sid() { + // Test that 'sid' returns keyrings from the 3 latest released versions + let sid_keyrings = get_keyring_urls("sid").await.unwrap(); - // The keyring URL for 'sid' should be the same as the latest released version - assert_eq!(sid_keyring, latest_keyring); + // Should have keyring URLs for sid + assert!(!sid_keyrings.is_empty()); + assert!(sid_keyrings.len() <= 3); + + // Each URL should be a valid Debian keyring URL + for url in &sid_keyrings { + assert!( + url.contains("ftp-master.debian.org/keys"), + "URL '{}' does not contain expected pattern", + url + ); + } + } + + #[tokio::test] + async fn test_get_keyring_url_regular_series() { + // Test that regular series (like bookworm) returns a single keyring URL + let bookworm_keyring = &get_keyring_urls("bookworm").await.unwrap()[0]; + assert!( + bookworm_keyring.contains("ftp-master.debian.org/keys"), + "URL '{}' does not contain expected pattern", + bookworm_keyring + ); + } + + #[tokio::test] + async fn test_get_n_latest_released_series() { + // Test getting 3 latest released series + let latest_3 = get_n_latest_released_series("debian", 3).await.unwrap(); + + // Should have at most 3 series + assert!(!latest_3.is_empty()); + assert!(latest_3.len() <= 3); + + // Should not contain 'sid' or 'experimental' + assert!(!latest_3.contains(&"sid".to_string())); + assert!(!latest_3.contains(&"experimental".to_string())); } #[tokio::test]