Skip to content

Commit

Permalink
0.11.0 - pgp verification has been added, plus some cleanup
Browse files Browse the repository at this point in the history
  • Loading branch information
cyr committed Oct 20, 2024
1 parent a50ef73 commit 7bc3a9f
Show file tree
Hide file tree
Showing 9 changed files with 1,225 additions and 78 deletions.
1,131 changes: 1,105 additions & 26 deletions Cargo.lock

Large diffs are not rendered by default.

3 changes: 2 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
[package]
name = "aptmirs"
description = "A simple tool for mirroring apt/deb repositories"
version = "0.10.0"
version = "0.11.0"
edition = "2021"

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
Expand All @@ -18,6 +18,7 @@ hex = "0.4.3"
indicatif = "0.17.8"
md5 = "0.7.0"
pathdiff = "0.2.1"
pgp = "0.14.0"
regex = "1.11.0"
reqwest = { version = "0.12.8", features = ["rustls-tls"] }
sha1 = "0.10.6"
Expand Down
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,12 @@ Multiple options can be added inside the same bracket.
deb [arch=amd64 arch=arm64 di_arch=amd64] http://ftp.se.debian.org/debian bookworm main contrib non-free non-free-firmware
```

If you want to require PGP signature verification, add the pgp_pub_key option pointing at the correct PGP public key. On failed verification the mirroring process will abort.

```
deb [arch=amd64 pgp_pub_key=/etc/apt/trusted.gpg.d/debian-archive-bookworm-stable.asc] http://ftp.se.debian.org/debian bookworm main contrib non-free non-free-firmware
```

## Usage

```
Expand Down
17 changes: 10 additions & 7 deletions src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@ pub struct MirrorOpts {
pub arch: Vec<CompactString>,
pub debian_installer_arch: Vec<CompactString>,
pub source: bool,
pub pgp_pub_key: Option<CompactString>,
}

impl Ord for MirrorOpts {
Expand Down Expand Up @@ -131,6 +132,7 @@ impl MirrorOpts {
pub fn try_from(mut line: &str) -> Result<MirrorOpts> {
let mut arch = Vec::new();
let mut debian_installer_arch = Vec::new();
let mut pgp_pub_key: Option<CompactString> = None;

let mut source = false;

Expand All @@ -150,21 +152,21 @@ impl MirrorOpts {
return Err(MirsError::Config { msg: CompactString::new("options bracket is not closed") })
};

let options_line = (&line[1..bracket_end]).trim();
let options_line = line[1..bracket_end].trim();
line = &line[bracket_end+1..];

for part in options_line.split_whitespace() {
let Some((opt_key, opt_val)) = part.split_once('=') else {
return Err(MirsError::Config { msg: CompactString::new("invalid format of options bracket") })
};

if opt_key == "arch" {
arch.push(opt_val.to_compact_string())
match opt_key {
"arch" => arch.extend(opt_val.split(',').map(|v|v.to_compact_string())),
"di_arch" => debian_installer_arch.extend(opt_val.split(',').map(|v|v.to_compact_string())),
"pgp_pub_key" => pgp_pub_key = Some(opt_val.to_compact_string()),
_ => ()
}

if opt_key == "di_arch" {
debian_installer_arch.push(opt_val.to_compact_string())
}
}
}

Expand Down Expand Up @@ -198,7 +200,8 @@ impl MirrorOpts {
components,
arch,
debian_installer_arch,
source
source,
pgp_pub_key
})
}

Expand Down
11 changes: 10 additions & 1 deletion src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -95,5 +95,14 @@ pub enum MirsError {
Finalize { inner: Box<MirsError> },

#[error("error reading {path}: {inner}")]
ReadingPackage { path: FilePath, inner: Box<MirsError> }
ReadingPackage { path: FilePath, inner: Box<MirsError> },

#[error("PGP error: {inner}")]
Pgp { #[from] inner: pgp::errors::Error },

#[error("unable to read PGP pub key: {inner}")]
PgpPubKey { inner: Box<MirsError> },

#[error("this repository does not provide a PGP signature, yet a public key has been provided - no verification can be made")]
PgpNotSupported,
}
13 changes: 9 additions & 4 deletions src/metadata.rs
Original file line number Diff line number Diff line change
Expand Up @@ -48,16 +48,22 @@ impl FilePath {
&self.0
}

pub fn file_stem(&self) -> Option<&OsStr> {
pub fn file_stem(&self) -> &str {
let p: &Path = self.as_ref();

p.file_stem()
.expect("a FilePath should have a filename")
.to_str()
.expect("a FilePath name should be utf8")
}

pub fn file_name(&self) -> Option<&OsStr> {
pub fn file_name(&self) -> &str {
let p: &Path = self.as_ref();

p.file_name()
.expect("a FilePath should have a filename")
.to_str()
.expect("a FilePath name should be utf8")
}

pub fn exists(&self) -> bool {
Expand Down Expand Up @@ -120,8 +126,7 @@ impl IndexSource {

impl From<FilePath> for IndexSource {
fn from(value: FilePath) -> Self {
match value.file_name().expect("indices should have names")
.to_str().expect("the file name of indices should be valid utf8") {
match value.file_name() {
v if v.starts_with("Packages") => IndexSource::Packages(value),
v if v.starts_with("Sources") => IndexSource::Sources(value),
_ => unreachable!("implementation error; non-index file as IndexSource")
Expand Down
8 changes: 1 addition & 7 deletions src/metadata/sum_file.rs
Original file line number Diff line number Diff line change
Expand Up @@ -32,13 +32,7 @@ impl TryFrom<FilePath> for SumFile {
type Error = MirsError;

fn try_from(value: FilePath) -> Result<Self> {
let Some(name) = value.file_name() else {
return Err(MirsError::UnrecognizedSumFile { path: value })
};

let name = name.to_str().expect("file names need to be valid utf8");

let file = match name {
let file = match value.file_name() {
"MD5SUMS" => SumFile::Md5(value),
"SHA1SUMS" => SumFile::Sha1(value),
"SHA256SUMS" => SumFile::Sha256(value),
Expand Down
56 changes: 33 additions & 23 deletions src/mirror.rs
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
use std::collections::BTreeMap;
use std::ffi::OsStr;
use std::fmt::Display;
use std::os::unix::ffi::OsStrExt;
use std::fs::File;
use std::str::FromStr;
use std::sync::atomic::Ordering;

use compact_str::format_compact;
use indicatif::{MultiProgress, HumanBytes};
use pgp::cleartext::CleartextSignedMessage;
use pgp::{Deserializable, StandaloneSignature};

use crate::metadata::diff_index_file::DiffIndexFile;
use crate::metadata::sum_file::{to_strongest_by_checksum, SumFileEntry};
Expand Down Expand Up @@ -46,7 +48,7 @@ impl Display for MirrorResult {
}

pub async fn mirror(opts: &MirrorOpts, cli_opts: &CliOpts, downloader: &mut Downloader) -> Result<MirrorResult> {
let repo = Repository::build(&opts.url, &opts.suite, &cli_opts.output)?;
let repo = Repository::build(opts, cli_opts)?;

let mut progress = downloader.progress();
progress.reset();
Expand Down Expand Up @@ -211,33 +213,23 @@ pub async fn download_debian_installer(repo: &Repository, downloader: &mut Downl

let mut paths_to_delete = Vec::with_capacity(sum_files.len());

eprintln!("stuff!");

for sum_file in sum_files.iter() {
eprintln!("it's a sum file: {}", sum_file.path());
let base = sum_file.path().parent()
.expect("there should always be a parent");

eprintln!("sum file rel: {base}");
let rel_path = repo.strip_tmp_base(base).expect("sum files should be in tmp");

eprintln!("sum file rel: {rel_path}");
let current_image = repo.rebase_to_root(rel_path);

eprintln!("sum file: {current_image}");

paths_to_delete.push(current_image);
}

let mut progress_bar = progress.create_download_progress_bar().await;

eprintln!("let's do sum files again");

for sum_file in sum_files {
let base_path = sum_file.path().parent()
.expect("sum files should have a parent");

eprintln!("sum file base_path: {base_path}");
let base_path = FilePath::from_str(base_path)?;

for entry in sum_file.try_into_iter()? {
Expand All @@ -248,14 +240,9 @@ pub async fn download_debian_installer(repo: &Repository, downloader: &mut Downl
let new_rel_path = repo.strip_tmp_base(new_path)
.expect("the new path should be in tmp");

//eprintln!("sum_file content to {new_path}");

let url = repo.to_url_in_root(new_rel_path.as_str());
let target_path = repo.to_path_in_tmp(&url);

//eprintln!("sum_file content url {url}");
//eprintln!("sum_file content target_path {target_path}");

let download = repo.create_raw_download(target_path, url, Some(checksum));

downloader.queue(download).await?;
Expand Down Expand Up @@ -316,11 +303,11 @@ pub async fn download_from_indices(repo: &Repository, downloader: &mut Downloade
let mut existing_indices = BTreeMap::<FilePath, FilePath>::new();

for index_file_path in indices.into_iter().filter(|f| f.exists()) {
let file_stem = index_file_path.file_stem().unwrap();
let file_stem = index_file_path.file_stem();
let path_with_stem = FilePath(format_compact!(
"{}/{}",
index_file_path.parent().unwrap(),
file_stem.to_str().unwrap()
file_stem
));

if let Some(val) = existing_indices.get_mut(&path_with_stem) {
Expand Down Expand Up @@ -404,6 +391,32 @@ pub async fn download_release(repository: &Repository, downloader: &mut Download

progress_bar.finish_using_style();

if repository.verify_pgp_requirement() {
if let Some(inrelease_file) = files.iter()
.find(|v| v.file_name() == "InRelease") {
let content = std::fs::read_to_string(inrelease_file)?;

let (msg, _) = CleartextSignedMessage::from_string(&content)?;

repository.verify_message(&msg)?;
} else {
let Some(release_file) = files.iter().find(|v| v.file_name() == "Release") else {
return Err(MirsError::PgpNotSupported)
};

let Some(release_file_signature) = files.iter().find(|v| v.file_name() == "Release.pgp") else {
return Err(MirsError::PgpNotSupported)
};

let sign_handle = File::open(release_file_signature)?;
let content = std::fs::read_to_string(release_file)?;

let (signature, _) = StandaloneSignature::from_reader_single(&sign_handle)?;

repository.verify_message_with_standlone_signature(&content, &signature)?;
}
}

let Some(release_file) = get_release_file(&files) else {
return Err(MirsError::NoReleaseFile)
};
Expand Down Expand Up @@ -477,10 +490,7 @@ fn is_sources_file(path: &str) -> bool {

fn get_release_file(files: &Vec<FilePath>) -> Option<&FilePath> {
for file in files {
let file_name = file.file_name()
.expect("release files should be files");

if let b"InRelease" | b"Release" = file_name.as_bytes() {
if let "InRelease" | "Release" = file.file_name() {
return Some(file)
}
}
Expand Down
58 changes: 49 additions & 9 deletions src/mirror/repository.rs
Original file line number Diff line number Diff line change
@@ -1,38 +1,54 @@
use std::{path::Path, str::FromStr};
use std::{fs::File, path::Path, str::FromStr};

use compact_str::{format_compact, CompactString, ToCompactString};
use pgp::{cleartext::CleartextSignedMessage, Deserializable, SignedPublicKey, StandaloneSignature};
use reqwest::Url;

use super::downloader::Download;

use crate::{error::{MirsError, Result}, metadata::{checksum::Checksum, release::FileEntry, FilePath, IndexFileEntry}};
use crate::{config::MirrorOpts, error::{MirsError, Result}, metadata::{checksum::Checksum, release::FileEntry, FilePath, IndexFileEntry}, CliOpts};

pub struct Repository {
root_url: CompactString,
root_dir: FilePath,
dist_url: CompactString,
tmp_dir: FilePath,
pgp_pub_key: Option<SignedPublicKey>,
}

impl Repository {
pub fn build(archive_root: &str, suite: &str, base_dir: &FilePath) -> Result<Self> {
let root_url = match archive_root.strip_prefix('/') {
pub fn build(mirror_opts: &MirrorOpts, cli_opts: &CliOpts) -> Result<Self> {
let root_url = match &mirror_opts.url.as_str().strip_prefix('/') {
Some(url) => url.to_compact_string(),
None => archive_root.to_compact_string(),
None => mirror_opts.url.clone(),
};
let dist_url = format_compact!("{root_url}/dists/{suite}");

let dist_url = format_compact!("{root_url}/dists/{}", mirror_opts.suite);

let parsed_url = Url::parse(&root_url)
.map_err(|_| MirsError::UrlParsing { url: root_url.clone() })?;

let root_dir = local_dir_from_archive_url(&parsed_url, base_dir)?;
let tmp_dir = create_tmp_dir(&parsed_url, suite, base_dir)?;
let pgp_pub_key = if let Some(pgp_signing_key) = &mirror_opts.pgp_pub_key {
let key_file = File::open(pgp_signing_key)
.map_err(|e| MirsError::PgpPubKey { inner: Box::new(e.into()) })?;

let (signed_public_key, _) = SignedPublicKey::from_reader_single(&key_file)
.map_err(|e| MirsError::PgpPubKey { inner: Box::new(e.into()) })?;

Some(signed_public_key)
} else {
None
};

let root_dir = local_dir_from_archive_url(&parsed_url, &cli_opts.output)?;
let tmp_dir = create_tmp_dir(&parsed_url, &mirror_opts.suite, &cli_opts.output)?;

Ok(Self {
root_url,
root_dir,
dist_url,
tmp_dir
tmp_dir,
pgp_pub_key
})
}

Expand Down Expand Up @@ -184,6 +200,30 @@ impl Repository {
always_download: false
}))
}

pub fn verify_pgp_requirement(&self) -> bool {
self.pgp_pub_key.is_some()
}

pub fn verify_message(&self, msg: &CleartextSignedMessage) -> Result<()> {
let Some(pgp_pub_key) = &self.pgp_pub_key else {
panic!("trying to verify signature without a pgp_pub_key set")
};

_ = msg.verify(&pgp_pub_key)?;

Ok(())
}

pub fn verify_message_with_standlone_signature(&self, msg: &str, signature: &StandaloneSignature) -> Result<()> {
let Some(pgp_pub_key) = &self.pgp_pub_key else {
panic!("trying to verify signature without a pgp_pub_key set")
};

signature.verify(pgp_pub_key, msg.as_bytes())?;

Ok(())
}
}

fn sanitize_path_part(part: &str) -> CompactString {
Expand Down

0 comments on commit 7bc3a9f

Please sign in to comment.