Files
pkh/src/apt/keyring.rs
T
vhaudiquet d06e091121
CI / build (push) Failing after 21m24s
CI / snap (push) Has been skipped
apt/keyring: download 3 keyrings for sid
2026-03-18 15:23:57 +01:00

249 lines
8.1 KiB
Rust

//! 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 keyrings to a shared keyring directory and return the directory path
///
/// This function downloads keyrings to a user-writable cache directory
/// instead of the system apt keyring directory, allowing non-root usage.
/// The returned directory path can be passed to mmdebstrap via --keyring=.
///
/// 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 keyring directory containing all downloaded keyring files
pub async fn download_cache_keyrings(
ctx: Option<Arc<context::Context>>,
series: &str,
) -> Result<PathBuf, Box<dyn Error>> {
let ctx = ctx.unwrap_or_else(context::current);
// Obtain keyring URLs from distro_info
let keyring_urls = distro_info::get_keyring_urls(series).await?;
log::debug!("Downloading keyrings from: {:?}", keyring_urls);
// 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 keyring directory if it doesn't exist
if !ctx.exists(&keyring_dir)? {
ctx.command("mkdir").arg("-p").arg(&keyring_dir).status()?;
}
// Make keyring directory world-accessible so mmdebstrap in unshare mode can access it
ctx.command("chmod")
.arg("a+rwx")
.arg(&keyring_dir)
.status()?;
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);
// 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()
};
// 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);
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
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()?;
}
}
log::info!(
"Keyrings for {} available in {}",
series,
keyring_dir.display()
);
Ok(keyring_dir)
}
/// 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<Arc<context::Context>>,
ppa_owner: &str,
ppa_name: &str,
) -> Result<(), Box<dyn Error>> {
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(())
}