Skip to content

Commit

Permalink
Verify whole ZIP file
Browse files Browse the repository at this point in the history
  • Loading branch information
Kijewski committed Sep 12, 2023
1 parent 0351f45 commit 96acaa6
Show file tree
Hide file tree
Showing 4 changed files with 110 additions and 34 deletions.
5 changes: 2 additions & 3 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"]
Expand All @@ -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)
Expand Down
2 changes: 2 additions & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,8 @@ use std::path;
#[macro_use]
extern crate log;

#[cfg(feature = "signatures")]
mod signatures;
#[macro_use]
mod macros;
pub mod backends;
Expand Down
103 changes: 103 additions & 0 deletions src/signatures.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
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;

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 File,
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 prehashed_message = Sha512::new();
match signature_at_eof {
false => {
copy(exe, &mut prehashed_message)?;
}
true => {
exe.seek(SeekFrom::Start(0))?;
copy(&mut exe.take(content_size), &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
Expand Up @@ -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... ")?;
Expand Down

0 comments on commit 96acaa6

Please sign in to comment.