Skip to content

Commit

Permalink
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Verify whole ZIP file
Browse files Browse the repository at this point in the history
Kijewski committed Sep 12, 2023
1 parent 0351f45 commit db23e56
Showing 4 changed files with 121 additions and 34 deletions.
5 changes: 2 additions & 3 deletions Cargo.toml
Original file line number Diff line number Diff line change
@@ -27,8 +27,7 @@ regex = "1"
log = "0.4"
urlencoding = "2.1"
self-replace = "1"
ed25519-dalek = { version = "2", optional = true }
memmap2 = { version = "0.7", optional = true }
ed25519-dalek = { version = "2", features = ["digest"], optional = true }

[features]
default = ["reqwest/default-tls"]
@@ -38,7 +37,7 @@ compression-zip-deflate = ["zip/deflate"] #
archive-tar = ["tar"]
compression-flate2 = ["flate2", "either"] #
rustls = ["reqwest/rustls-tls"]
signatures = ["ed25519-dalek", "memmap2"]
signatures = ["ed25519-dalek"]

[package.metadata.docs.rs]
# Whether to pass `--all-features` to Cargo (default: false)
2 changes: 2 additions & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
@@ -141,6 +141,8 @@ use std::path;
#[macro_use]
extern crate log;

#[cfg(feature = "signatures")]
mod signatures;
#[macro_use]
mod macros;
pub mod backends;
114 changes: 114 additions & 0 deletions src/signatures.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
use std::convert::TryInto;
use std::fs::File;
use std::io::{copy, Read, Seek, SeekFrom};
use std::path::Path;

use ed25519_dalek::{Digest, Sha512, Signature, VerifyingKey, PUBLIC_KEY_LENGTH, SIGNATURE_LENGTH};

use crate::errors::Error;
use crate::{detect_archive, ArchiveKind};

const MAGIC_HEADER: &[u8; 14] = b"\x0c\x04\x01ed25519ph\x00\x00";
const HEADER_SIZE: usize = 16;
type SignatureCountLeInt = u16;

trait ReadSeek: Read + Seek {
fn mut_read(&mut self) -> &mut dyn Read;
}

impl<T: Read + Seek> ReadSeek for T {
fn mut_read(&mut self) -> &mut dyn Read {
self
}
}

pub(crate) fn verify(archive_path: &Path, keys: &[[u8; PUBLIC_KEY_LENGTH]]) -> crate::Result<()> {
if keys.is_empty() {
return Ok(());
}

println!("Verifying downloaded file...");

let keys = keys
.into_iter()
.map(VerifyingKey::from_bytes)
.collect::<Result<Vec<_>, _>>()
.map_err(|_| Error::NoValidSignature)?;
let file_name = archive_path
.file_name()
.and_then(|s| s.to_str())
.map(|s| s.as_bytes())
.ok_or(Error::NoValidSignature)?;
let archive_kind = detect_archive(&archive_path)?;

let mut exe = File::open(&archive_path)?;

match archive_kind {
ArchiveKind::Plain(_) => {
unimplemented!("Can only check signatures for .zip and .tar* files.")
}
#[cfg(feature = "archive-tar")]
ArchiveKind::Tar(_) => do_verify(&mut exe, &keys, file_name, true),
#[cfg(feature = "archive-zip")]
ArchiveKind::Zip => do_verify(&mut exe, &keys, file_name, false),
}
}

fn do_verify(
exe: &mut dyn ReadSeek,
keys: &[VerifyingKey],
context: &[u8],
signature_at_eof: bool,
) -> Result<(), Error> {
if signature_at_eof {
exe.seek(SeekFrom::End(-(HEADER_SIZE as i64)))?;
}

let mut header = [0; HEADER_SIZE];
exe.read_exact(&mut header)?;
if header[..MAGIC_HEADER.len()] != MAGIC_HEADER[..] {
println!("Signature header was not found.");
return Err(Error::NoValidSignature);
}
let signature_count = header[MAGIC_HEADER.len()..].try_into().unwrap();
let signature_count = SignatureCountLeInt::from_le_bytes(signature_count) as usize;
let signature_size = signature_count * SIGNATURE_LENGTH;

let content_size = match signature_at_eof {
false => 0,
true => exe.seek(SeekFrom::End(-((HEADER_SIZE + signature_size) as i64)))?
};

let mut signatures = vec![0; signature_size];
exe.read_exact(&mut signatures)?;
let signatures = signatures
.chunks_exact(SIGNATURE_LENGTH)
.map(Signature::from_slice)
.collect::<Result<Vec<_>, _>>()
.map_err(|_| Error::NoValidSignature)?;

let mut take_exe;
let exe = match signature_at_eof {
false => exe.mut_read(),
true => {
exe.seek(SeekFrom::Start(0))?;
take_exe = exe.take(content_size);
&mut take_exe
}
};
let mut prehashed_message = Sha512::new();
copy(exe, &mut prehashed_message)?;

for key in keys {
for signature in &signatures {
if key
.verify_prehashed_strict(prehashed_message.clone(), Some(context), signature)
.is_ok()
{
println!("OK");
return Ok(());
}
}
}
Err(Error::NoValidSignature)
}
34 changes: 3 additions & 31 deletions src/update.rs
Original file line number Diff line number Diff line change
@@ -233,43 +233,15 @@ pub trait ReleaseUpdate {

download.download_to(&mut tmp_archive)?;

#[cfg(feature = "signatures")]
crate::signatures::verify(&tmp_archive_path, self.verifying_keys())?;

print_flush(show_output, "Extracting archive... ")?;
let bin_path_in_archive = self.bin_path_in_archive();
Extract::from_source(&tmp_archive_path)
.extract_file(tmp_archive_dir.path(), &bin_path_in_archive)?;
let new_exe = tmp_archive_dir.path().join(&bin_path_in_archive);

#[cfg(feature = "signatures")]
{
use std::io::Read;

let verifying_keys = self.verifying_keys();
if !verifying_keys.is_empty() {
// TODO: FIXME: this only works for signed .zip files, not .tar
let mut signature = [0; ed25519_dalek::SIGNATURE_LENGTH];
fs::File::open(&tmp_archive_path)?.read_exact(&mut signature)?;
let signature = ed25519_dalek::Signature::from_bytes(&signature);

let exe = fs::File::open(&new_exe)?;
let exe = unsafe { memmap2::Mmap::map(&exe)? };

let mut valid_signature = false;
for (idx, bytes) in verifying_keys.into_iter().enumerate() {
let key = match ed25519_dalek::VerifyingKey::from_bytes(&bytes) {
Ok(key) => key,
Err(_) => panic!("Key #{} is invalid", idx),
};
if key.verify_strict(&exe, &signature).is_ok() {
valid_signature = true;
break;
}
}
if !valid_signature {
return Err(Error::NoValidSignature);
}
}
}

println(show_output, "Done");

print_flush(show_output, "Replacing binary file... ")?;

0 comments on commit db23e56

Please sign in to comment.