apt/keyring: download 3 keyrings for sid
This commit is contained in:
@@ -16,49 +16,78 @@ struct LaunchpadPpaResponse {
|
|||||||
signing_key_fingerprint: String,
|
signing_key_fingerprint: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Download a keyring to the application cache directory and return the path
|
/// Download keyrings and combine them into a single keyring file for the series
|
||||||
///
|
///
|
||||||
/// 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.
|
/// instead of the system apt keyring directory, allowing non-root usage.
|
||||||
/// The returned path can be passed to mmdebstrap via --keyring.
|
/// All keyrings are combined into a single file that can be passed to mmdebstrap
|
||||||
|
/// via --keyring= or using signed-by in the mirror specification.
|
||||||
///
|
///
|
||||||
/// 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.
|
/// 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, and combines them
|
||||||
|
/// into a single keyring file using gpg.
|
||||||
|
///
|
||||||
/// # Arguments
|
/// # Arguments
|
||||||
/// * `ctx` - Optional context to use
|
/// * `ctx` - Optional context to use
|
||||||
/// * `series` - The distribution series (e.g., "noble", "sid")
|
/// * `series` - The distribution series (e.g., "noble", "sid")
|
||||||
///
|
///
|
||||||
/// # Returns
|
/// # Returns
|
||||||
/// The path to the downloaded keyring file (in binary GPG format)
|
/// The path to the combined keyring file for the series
|
||||||
pub async fn download_cache_keyring(
|
pub async fn download_cache_keyrings(
|
||||||
ctx: Option<Arc<context::Context>>,
|
ctx: Option<Arc<context::Context>>,
|
||||||
series: &str,
|
series: &str,
|
||||||
) -> Result<PathBuf, Box<dyn Error>> {
|
) -> Result<PathBuf, Box<dyn Error>> {
|
||||||
let ctx = ctx.unwrap_or_else(context::current);
|
let ctx = ctx.unwrap_or_else(context::current);
|
||||||
|
|
||||||
// Obtain keyring URL from distro_info
|
// Obtain keyring URLs from distro_info
|
||||||
let keyring_url = distro_info::get_keyring_url(series).await?;
|
let keyring_urls = distro_info::get_keyring_urls(series).await?;
|
||||||
log::debug!("Downloading keyring from: {}", keyring_url);
|
log::debug!("Downloading keyrings from: {:?}", keyring_urls);
|
||||||
|
|
||||||
// Get the application cache directory
|
// Get the application cache directory
|
||||||
let proj_dirs = directories::ProjectDirs::from("com", "pkh", "pkh")
|
let proj_dirs = directories::ProjectDirs::from("com", "pkh", "pkh")
|
||||||
.ok_or("Could not determine project directories")?;
|
.ok_or("Could not determine project directories")?;
|
||||||
let cache_dir = proj_dirs.cache_dir();
|
let cache_dir = proj_dirs.cache_dir();
|
||||||
|
let keyring_dir = cache_dir.join("keyrings");
|
||||||
|
|
||||||
// Create cache directory if it doesn't exist
|
// Create cache and keyring directories if they don't exist
|
||||||
if !ctx.exists(cache_dir)? {
|
if !ctx.exists(cache_dir)? {
|
||||||
ctx.command("mkdir").arg("-p").arg(cache_dir).status()?;
|
ctx.command("mkdir").arg("-p").arg(cache_dir).status()?;
|
||||||
}
|
}
|
||||||
|
if !ctx.exists(&keyring_dir)? {
|
||||||
|
ctx.command("mkdir").arg("-p").arg(&keyring_dir).status()?;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Path for the combined keyring file for this series
|
||||||
|
let combined_keyring_path = keyring_dir.join(format!("{}-archive-keyring.gpg", series));
|
||||||
|
|
||||||
|
// Collect paths to individual keyring files
|
||||||
|
let mut individual_keyring_paths = Vec::new();
|
||||||
|
|
||||||
|
for keyring_url in keyring_urls {
|
||||||
// Extract the original filename from the keyring URL
|
// Extract the original filename from the keyring URL
|
||||||
let filename = keyring_url
|
let filename = keyring_url
|
||||||
.split('/')
|
.split('/')
|
||||||
.next_back()
|
.next_back()
|
||||||
.unwrap_or("pkh-{}.gpg")
|
.unwrap_or("pkh-{}.gpg")
|
||||||
.replace("{}", series);
|
.replace("{}", series);
|
||||||
let download_path = cache_dir.join(&filename);
|
let download_path = keyring_dir.join(&filename);
|
||||||
|
|
||||||
|
// Determine the binary keyring path
|
||||||
|
let binary_filename = if filename.ends_with(".asc") {
|
||||||
|
filename
|
||||||
|
.strip_suffix(".asc")
|
||||||
|
.unwrap_or(&filename)
|
||||||
|
.to_string()
|
||||||
|
} else {
|
||||||
|
filename.clone()
|
||||||
|
};
|
||||||
|
let binary_path = keyring_dir.join(format!("{}.gpg", binary_filename));
|
||||||
|
|
||||||
|
// Skip download if the binary keyring already exists
|
||||||
|
if !ctx.exists(&binary_path)? {
|
||||||
// Download the keyring using curl
|
// Download the keyring using curl
|
||||||
let mut curl_cmd = ctx.command("curl");
|
let mut curl_cmd = ctx.command("curl");
|
||||||
curl_cmd
|
curl_cmd
|
||||||
@@ -75,11 +104,7 @@ pub async fn download_cache_keyring(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// If the downloaded file is an ASCII-armored key (.asc), convert it to binary GPG format
|
// If the downloaded file is an ASCII-armored key (.asc), convert it to binary GPG format
|
||||||
// mmdebstrap's --keyring option expects binary GPG keyrings
|
if filename.ends_with(".asc") {
|
||||||
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");
|
log::debug!("Converting ASCII-armored key to binary GPG format");
|
||||||
let mut gpg_cmd = ctx.command("gpg");
|
let mut gpg_cmd = ctx.command("gpg");
|
||||||
gpg_cmd
|
gpg_cmd
|
||||||
@@ -97,18 +122,95 @@ pub async fn download_cache_keyring(
|
|||||||
|
|
||||||
// Remove the original .asc file
|
// Remove the original .asc file
|
||||||
let _ = ctx.command("rm").arg("-f").arg(&download_path).status();
|
let _ = ctx.command("rm").arg("-f").arg(&download_path).status();
|
||||||
|
}
|
||||||
binary_path
|
|
||||||
} else {
|
|
||||||
download_path
|
|
||||||
};
|
|
||||||
|
|
||||||
log::info!(
|
log::info!(
|
||||||
"Successfully downloaded keyring for {} to {}",
|
"Successfully downloaded keyring for {} to {}",
|
||||||
series,
|
series,
|
||||||
keyring_path.display()
|
binary_path.display()
|
||||||
);
|
);
|
||||||
Ok(keyring_path)
|
} else {
|
||||||
|
log::debug!(
|
||||||
|
"Keyring already exists at {}, skipping download",
|
||||||
|
binary_path.display()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
individual_keyring_paths.push(binary_path);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Combine all keyrings into a single file using gpg
|
||||||
|
if individual_keyring_paths.len() == 1 {
|
||||||
|
// If only one keyring, just use it directly (or copy if needed)
|
||||||
|
let single_path = individual_keyring_paths.remove(0);
|
||||||
|
if single_path != combined_keyring_path {
|
||||||
|
// Copy to the combined path
|
||||||
|
let _ = ctx
|
||||||
|
.command("cp")
|
||||||
|
.arg(&single_path)
|
||||||
|
.arg(&combined_keyring_path)
|
||||||
|
.status()?;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Multiple keyrings: combine using gpg
|
||||||
|
log::debug!(
|
||||||
|
"Combining {} keyrings into {}",
|
||||||
|
individual_keyring_paths.len(),
|
||||||
|
combined_keyring_path.display()
|
||||||
|
);
|
||||||
|
|
||||||
|
// Remove existing combined keyring if it exists
|
||||||
|
if ctx.exists(&combined_keyring_path)? {
|
||||||
|
let _ = ctx
|
||||||
|
.command("rm")
|
||||||
|
.arg("-f")
|
||||||
|
.arg(&combined_keyring_path)
|
||||||
|
.status()?;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create an empty keyring first
|
||||||
|
let _status = ctx
|
||||||
|
.command("gpg")
|
||||||
|
.arg("--no-default-keyring")
|
||||||
|
.arg("--keyring")
|
||||||
|
.arg(&combined_keyring_path)
|
||||||
|
.arg("--fingerprint")
|
||||||
|
.status()?;
|
||||||
|
|
||||||
|
// gpg --fingerprint on empty keyring may fail, but it creates the keyring file
|
||||||
|
// Check if the file was created
|
||||||
|
if !ctx.exists(&combined_keyring_path)? {
|
||||||
|
return Err("Failed to create empty keyring".into());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use gpg to combine keyrings by importing each one
|
||||||
|
for keyring_path in &individual_keyring_paths {
|
||||||
|
let status = ctx
|
||||||
|
.command("gpg")
|
||||||
|
.arg("--no-default-keyring")
|
||||||
|
.arg("--keyring")
|
||||||
|
.arg(&combined_keyring_path)
|
||||||
|
.arg("--import")
|
||||||
|
.arg(keyring_path)
|
||||||
|
.status()?;
|
||||||
|
|
||||||
|
if !status.success() {
|
||||||
|
return Err(format!(
|
||||||
|
"Failed to import keyring {} into combined keyring",
|
||||||
|
keyring_path.display()
|
||||||
|
)
|
||||||
|
.into());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
log::info!(
|
||||||
|
"Combined keyring for {} available at {}",
|
||||||
|
series,
|
||||||
|
combined_keyring_path.display()
|
||||||
|
);
|
||||||
|
|
||||||
|
Ok(keyring_dir)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Download and import a PPA key using Launchpad API
|
/// Download and import a PPA key using Launchpad API
|
||||||
|
|||||||
@@ -143,9 +143,9 @@ impl EphemeralContextGuard {
|
|||||||
.arg(lockfile_path.to_string_lossy().to_string())
|
.arg(lockfile_path.to_string_lossy().to_string())
|
||||||
.status()?;
|
.status()?;
|
||||||
|
|
||||||
// Download the keyring to the cache directory
|
// Download the keyrings to the shared keyring directory
|
||||||
let keyring_path =
|
let keyring_dir =
|
||||||
crate::apt::keyring::download_cache_keyring(Some(ctx.clone()), series).await?;
|
crate::apt::keyring::download_cache_keyrings(Some(ctx.clone()), series).await?;
|
||||||
|
|
||||||
// Use mmdebstrap to download the tarball to the cache directory
|
// Use mmdebstrap to download the tarball to the cache directory
|
||||||
let mut cmd = ctx.command("mmdebstrap");
|
let mut cmd = ctx.command("mmdebstrap");
|
||||||
@@ -153,7 +153,7 @@ impl EphemeralContextGuard {
|
|||||||
.arg("--mode=unshare")
|
.arg("--mode=unshare")
|
||||||
.arg("--include=mount,curl,ca-certificates")
|
.arg("--include=mount,curl,ca-certificates")
|
||||||
.arg("--format=tar")
|
.arg("--format=tar")
|
||||||
.arg(format!("--keyring={}", keyring_path.display()));
|
.arg(format!("--keyring={}", keyring_dir.display()));
|
||||||
|
|
||||||
// Add architecture if specified
|
// Add architecture if specified
|
||||||
if let Some(a) = arch {
|
if let Some(a) = arch {
|
||||||
|
|||||||
@@ -134,6 +134,18 @@ pub async fn get_ordered_series_name(dist: &str) -> Result<Vec<String>, Box<dyn
|
|||||||
|
|
||||||
/// Get the latest released series for a dist (excluding future releases and special cases like sid)
|
/// Get the latest released series for a dist (excluding future releases and special cases like sid)
|
||||||
pub async fn get_latest_released_series(dist: &str) -> Result<String, Box<dyn Error>> {
|
pub async fn get_latest_released_series(dist: &str) -> Result<String, Box<dyn Error>> {
|
||||||
|
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<Vec<String>, Box<dyn Error>> {
|
||||||
let series_info_list = get_ordered_series(dist).await?;
|
let series_info_list = get_ordered_series(dist).await?;
|
||||||
|
|
||||||
let today = chrono::Local::now().date_naive();
|
let today = chrono::Local::now().date_naive();
|
||||||
@@ -153,11 +165,11 @@ pub async fn get_latest_released_series(dist: &str) -> Result<String, Box<dyn Er
|
|||||||
// Sort by release date descending (newest first)
|
// Sort by release date descending (newest first)
|
||||||
released_series.sort_by(|a, b| b.release.cmp(&a.release));
|
released_series.sort_by(|a, b| b.release.cmp(&a.release));
|
||||||
|
|
||||||
if let Some(latest) = released_series.first() {
|
Ok(released_series
|
||||||
Ok(latest.series.clone())
|
.iter()
|
||||||
} else {
|
.take(n)
|
||||||
Err("No released series found".into())
|
.map(|s| s.series.clone())
|
||||||
}
|
.collect())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Obtain the distribution (eg. debian, ubuntu) from a distribution series (eg. noble, bookworm)
|
/// Obtain the distribution (eg. debian, ubuntu) from a distribution series (eg. noble, bookworm)
|
||||||
@@ -202,8 +214,11 @@ pub fn get_base_url(dist: &str) -> String {
|
|||||||
DATA.dist.get(dist).unwrap().base_url.clone()
|
DATA.dist.get(dist).unwrap().base_url.clone()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Obtain the URL for the archive keyring of a distribution series
|
/// Obtain the URLs for the archive keyrings of a distribution series
|
||||||
pub async fn get_keyring_url(series: &str) -> Result<String, Box<dyn Error>> {
|
///
|
||||||
|
/// 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<Vec<String>, Box<dyn Error>> {
|
||||||
let dist = get_dist_from_series(series).await?;
|
let dist = get_dist_from_series(series).await?;
|
||||||
let dist_data = DATA
|
let dist_data = DATA
|
||||||
.dist
|
.dist
|
||||||
@@ -212,24 +227,36 @@ pub async fn get_keyring_url(series: &str) -> Result<String, Box<dyn Error>> {
|
|||||||
|
|
||||||
// For Debian, we need the series number to form the keyring URL
|
// For Debian, we need the series number to form the keyring URL
|
||||||
if dist == "debian" {
|
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" {
|
if series == "sid" || series == "experimental" {
|
||||||
let latest_released = get_latest_released_series("debian").await?;
|
let latest_released = get_n_latest_released_series("debian", 3).await?;
|
||||||
let series_num = get_debian_series_number(&latest_released).await?.unwrap();
|
let mut urls = Vec::new();
|
||||||
// Replace {series_num} placeholder with the latest released series number
|
for released_series in latest_released {
|
||||||
Ok(dist_data
|
if let Some(series_num) = get_debian_series_number(&released_series).await? {
|
||||||
|
urls.push(
|
||||||
|
dist_data
|
||||||
.archive_keyring
|
.archive_keyring
|
||||||
.replace("{series_num}", &series_num))
|
.replace("{series_num}", &series_num),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if urls.is_empty() {
|
||||||
|
Err("No keyring URLs found for sid/experimental".into())
|
||||||
|
} else {
|
||||||
|
Ok(urls)
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
let series_num = get_debian_series_number(series).await?.unwrap();
|
let series_num = get_debian_series_number(series).await?.unwrap();
|
||||||
// Replace {series_num} placeholder with the actual series number
|
// Replace {series_num} placeholder with the actual series number
|
||||||
Ok(dist_data
|
Ok(vec![
|
||||||
|
dist_data
|
||||||
.archive_keyring
|
.archive_keyring
|
||||||
.replace("{series_num}", &series_num))
|
.replace("{series_num}", &series_num),
|
||||||
|
])
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// For other distributions like Ubuntu, use the keyring directly
|
// 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]
|
#[tokio::test]
|
||||||
async fn test_get_keyring_url_sid() {
|
async fn test_get_keyring_urls_sid() {
|
||||||
// Test that 'sid' uses the latest released version for keyring URL
|
// Test that 'sid' returns keyrings from the 3 latest released versions
|
||||||
let sid_keyring = get_keyring_url("sid").await.unwrap();
|
let sid_keyrings = get_keyring_urls("sid").await.unwrap();
|
||||||
let latest_released = get_latest_released_series("debian").await.unwrap();
|
|
||||||
let latest_keyring = get_keyring_url(&latest_released).await.unwrap();
|
|
||||||
|
|
||||||
// The keyring URL for 'sid' should be the same as the latest released version
|
// Should have keyring URLs for sid
|
||||||
assert_eq!(sid_keyring, latest_keyring);
|
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]
|
#[tokio::test]
|
||||||
|
|||||||
Reference in New Issue
Block a user