//! APT keyring management for mmdebstrap and PPA packages //! //! Provides functions to ensure that archive keyrings are available //! for mmdebstrap operations and for PPA packages by downloading them. use crate::context; use crate::distro_info; use serde::Deserialize; use std::error::Error; use std::path::{Path, PathBuf}; use std::sync::Arc; /// Launchpad API response structure for PPA information #[derive(Deserialize)] struct LaunchpadPpaResponse { signing_key_fingerprint: String, } /// Download a keyring to the application cache directory and return the path /// /// This function downloads the keyring 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. /// /// For Debian keyrings (which are ASCII-armored .asc files), the key is /// converted to binary GPG format using gpg --dearmor. /// /// # 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( 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); // 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(); // Create cache directory if it doesn't exist if !ctx.exists(cache_dir)? { ctx.command("mkdir").arg("-p").arg(cache_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); // 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); let status = curl_cmd.status()?; if !status.success() { return Err(format!("Failed to download keyring from {}", keyring_url).into()); } // 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)); 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(); binary_path } else { download_path }; log::info!( "Successfully downloaded keyring for {} to {}", series, keyring_path.display() ); Ok(keyring_path) } /// Download and import a PPA key using Launchpad API /// /// # Arguments /// * `ctx` - Optional context to use /// * `ppa_owner` - PPA owner (username) /// * `ppa_name` - PPA name /// /// # Returns /// Result indicating success or failure pub async fn download_trust_ppa_key( ctx: Option>, ppa_owner: &str, ppa_name: &str, ) -> Result<(), Box> { let ctx = ctx.unwrap_or_else(context::current); // Create trusted.gpg.d directory if it doesn't exist let trusted_gpg_d = "/etc/apt/trusted.gpg.d"; if !ctx.exists(Path::new(trusted_gpg_d))? { ctx.command("mkdir").arg("-p").arg(trusted_gpg_d).status()?; } let key_filename = format!("{}-{}.asc", ppa_owner, ppa_name); let key_path = format!("{}/{}", trusted_gpg_d, key_filename); log::debug!( "Retrieving PPA key for {}/{} using Launchpad API", ppa_owner, ppa_name ); // Get PPA information from Launchpad API to get signing key fingerprint // Use the correct devel API endpoint let api_url = format!( "https://api.launchpad.net/1.0/~{}/+archive/ubuntu/{}", ppa_owner, ppa_name ); log::debug!("Querying Launchpad API: {}", api_url); let api_response = ctx .command("curl") .arg("-s") .arg("-f") .arg("-H") .arg("Accept: application/json") .arg(&api_url) .output()?; if !api_response.status.success() { return Err(format!( "Failed to query Launchpad API for PPA {}/{}", ppa_owner, ppa_name ) .into()); } // Parse the JSON response to extract the signing key fingerprint let api_response_str = String::from_utf8_lossy(&api_response.stdout); let ppa_response: LaunchpadPpaResponse = serde_json::from_str(&api_response_str).map_err(|e| { format!( "Failed to parse JSON response from Launchpad API for {}/{}: {}", ppa_owner, ppa_name, e ) })?; let fingerprint = ppa_response.signing_key_fingerprint; log::debug!("Found PPA signing key fingerprint: {}", fingerprint); // Download the actual key from the keyserver using the fingerprint let keyserver_url = format!( "https://keyserver.ubuntu.com/pks/lookup?op=get&search=0x{}", fingerprint ); log::debug!("Downloading key from keyserver: {}", keyserver_url); let mut curl_cmd = ctx.command("curl"); curl_cmd .arg("-s") .arg("-f") .arg("-L") .arg(&keyserver_url) .arg("--output") .arg(&key_path); let status = curl_cmd.status()?; if !status.success() { return Err(format!( "Failed to download PPA key from keyserver for fingerprint {}", fingerprint ) .into()); } log::info!( "Successfully downloaded and installed PPA key for {}/{} (fingerprint: {}) to {}", ppa_owner, ppa_name, fingerprint, key_path ); Ok(()) }