From 0351f45334f3cf50c0bce7c5a5b9884b17f9b93b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Kijewski?= Date: Thu, 7 Sep 2023 15:12:53 +0200 Subject: [PATCH 1/5] WIP: Verify file signatures --- .gitattributes | 1 + Cargo.toml | 3 +++ examples/github-private.key | 1 + examples/github-public.key | 1 + examples/github.rs | 5 +++-- src/backends/gitea.rs | 22 ++++++++++++++++++++++ src/backends/github.rs | 22 ++++++++++++++++++++++ src/backends/gitlab.rs | 22 ++++++++++++++++++++++ src/backends/s3.rs | 22 ++++++++++++++++++++++ src/errors.rs | 2 ++ src/update.rs | 35 +++++++++++++++++++++++++++++++++++ 11 files changed, 134 insertions(+), 2 deletions(-) create mode 100644 .gitattributes create mode 100644 examples/github-private.key create mode 100644 examples/github-public.key diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..4d16f0f --- /dev/null +++ b/.gitattributes @@ -0,0 +1 @@ +*.key binary=true -diff diff --git a/Cargo.toml b/Cargo.toml index abb6e32..93dfd78 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -27,6 +27,8 @@ regex = "1" log = "0.4" urlencoding = "2.1" self-replace = "1" +ed25519-dalek = { version = "2", optional = true } +memmap2 = { version = "0.7", optional = true } [features] default = ["reqwest/default-tls"] @@ -36,6 +38,7 @@ compression-zip-deflate = ["zip/deflate"] # archive-tar = ["tar"] compression-flate2 = ["flate2", "either"] # rustls = ["reqwest/rustls-tls"] +signatures = ["ed25519-dalek", "memmap2"] [package.metadata.docs.rs] # Whether to pass `--all-features` to Cargo (default: false) diff --git a/examples/github-private.key b/examples/github-private.key new file mode 100644 index 0000000..f18d251 --- /dev/null +++ b/examples/github-private.key @@ -0,0 +1 @@ +S��Đ*E��ʦ](Խ��V� ���yBփ�AsWVf2�N�w�� �E��<�$^}��п \ No newline at end of file diff --git a/examples/github-public.key b/examples/github-public.key new file mode 100644 index 0000000..0778fe3 --- /dev/null +++ b/examples/github-public.key @@ -0,0 +1 @@ +փ�AsWVf2�N�w�� �E��<�$^}��п \ No newline at end of file diff --git a/examples/github.rs b/examples/github.rs index 2cffd62..57d35d2 100644 --- a/examples/github.rs +++ b/examples/github.rs @@ -8,7 +8,7 @@ extern crate self_update; fn run() -> Result<(), Box> { let releases = self_update::backends::github::ReleaseList::configure() - .repo_owner("jaemk") + .repo_owner("Kijewski") .repo_name("self_update") .build()? .fetch()?; @@ -16,7 +16,7 @@ fn run() -> Result<(), Box> { println!("{:#?}\n", releases); let status = self_update::backends::github::Update::configure() - .repo_owner("jaemk") + .repo_owner("Kijewski") .repo_name("self_update") .bin_name("github") .show_download_progress(true) @@ -30,6 +30,7 @@ fn run() -> Result<(), Box> { // or prompting the user for input //.auth_token(env!("DOWNLOAD_AUTH_TOKEN")) .current_version(cargo_crate_version!()) + .verifying_keys([*include_bytes!("github-public.key")]) .build()? .update()?; println!("Update status: `{}`!", status.version()); diff --git a/src/backends/gitea.rs b/src/backends/gitea.rs index 3a64fed..95e9d00 100644 --- a/src/backends/gitea.rs +++ b/src/backends/gitea.rs @@ -241,6 +241,8 @@ pub struct UpdateBuilder { progress_template: String, progress_chars: String, auth_token: Option, + #[cfg(feature = "signatures")] + verifying_keys: Vec<[u8; ed25519_dalek::PUBLIC_KEY_LENGTH]>, } impl UpdateBuilder { @@ -385,6 +387,15 @@ impl UpdateBuilder { self } + #[cfg(feature = "signatures")] + pub fn verifying_keys( + &mut self, + keys: impl Into>, + ) -> &mut Self { + self.verifying_keys = keys.into(); + self + } + /// Confirm config and create a ready-to-use `Update` /// /// * Errors: @@ -440,6 +451,8 @@ impl UpdateBuilder { show_output: self.show_output, no_confirm: self.no_confirm, auth_token: self.auth_token.clone(), + #[cfg(feature = "signatures")] + verifying_keys: self.verifying_keys.clone(), })) } } @@ -462,6 +475,8 @@ pub struct Update { progress_template: String, progress_chars: String, auth_token: Option, + #[cfg(feature = "signatures")] + verifying_keys: Vec<[u8; ed25519_dalek::PUBLIC_KEY_LENGTH]>, } impl Update { /// Initialize a new `Update` builder @@ -566,6 +581,11 @@ impl ReleaseUpdate for Update { fn api_headers(&self, auth_token: &Option) -> Result { api_headers(auth_token) } + + #[cfg(feature = "signatures")] + fn verifying_keys(&self) -> &[[u8; ed25519_dalek::PUBLIC_KEY_LENGTH]] { + &self.verifying_keys + } } impl Default for UpdateBuilder { @@ -586,6 +606,8 @@ impl Default for UpdateBuilder { progress_template: DEFAULT_PROGRESS_TEMPLATE.to_string(), progress_chars: DEFAULT_PROGRESS_CHARS.to_string(), auth_token: None, + #[cfg(feature = "signatures")] + verifying_keys: vec![], } } } diff --git a/src/backends/github.rs b/src/backends/github.rs index e8097d1..c337cb5 100644 --- a/src/backends/github.rs +++ b/src/backends/github.rs @@ -244,6 +244,8 @@ pub struct UpdateBuilder { progress_chars: String, auth_token: Option, custom_url: Option, + #[cfg(feature = "signatures")] + verifying_keys: Vec<[u8; ed25519_dalek::PUBLIC_KEY_LENGTH]>, } impl UpdateBuilder { @@ -398,6 +400,15 @@ impl UpdateBuilder { self } + #[cfg(feature = "signatures")] + pub fn verifying_keys( + &mut self, + keys: impl Into>, + ) -> &mut Self { + self.verifying_keys = keys.into(); + self + } + /// Confirm config and create a ready-to-use `Update` /// /// * Errors: @@ -450,6 +461,8 @@ impl UpdateBuilder { no_confirm: self.no_confirm, auth_token: self.auth_token.clone(), custom_url: self.custom_url.clone(), + #[cfg(feature = "signatures")] + verifying_keys: self.verifying_keys.clone(), })) } } @@ -473,6 +486,8 @@ pub struct Update { progress_chars: String, auth_token: Option, custom_url: Option, + #[cfg(feature = "signatures")] + verifying_keys: Vec<[u8; ed25519_dalek::PUBLIC_KEY_LENGTH]>, } impl Update { /// Initialize a new `Update` builder @@ -590,6 +605,11 @@ impl ReleaseUpdate for Update { fn api_headers(&self, auth_token: &Option) -> Result { api_headers(auth_token) } + + #[cfg(feature = "signatures")] + fn verifying_keys(&self) -> &[[u8; ed25519_dalek::PUBLIC_KEY_LENGTH]] { + &self.verifying_keys + } } impl Default for UpdateBuilder { @@ -611,6 +631,8 @@ impl Default for UpdateBuilder { progress_chars: DEFAULT_PROGRESS_CHARS.to_string(), auth_token: None, custom_url: None, + #[cfg(feature = "signatures")] + verifying_keys: vec![], } } } diff --git a/src/backends/gitlab.rs b/src/backends/gitlab.rs index 92988af..f4dd8ca 100644 --- a/src/backends/gitlab.rs +++ b/src/backends/gitlab.rs @@ -238,6 +238,8 @@ pub struct UpdateBuilder { progress_template: String, progress_chars: String, auth_token: Option, + #[cfg(feature = "signatures")] + verifying_keys: Vec<[u8; ed25519_dalek::PUBLIC_KEY_LENGTH]>, } impl UpdateBuilder { @@ -382,6 +384,15 @@ impl UpdateBuilder { self } + #[cfg(feature = "signatures")] + pub fn verifying_keys( + &mut self, + keys: impl Into>, + ) -> &mut Self { + self.verifying_keys = keys.into(); + self + } + /// Confirm config and create a ready-to-use `Update` /// /// * Errors: @@ -433,6 +444,8 @@ impl UpdateBuilder { show_output: self.show_output, no_confirm: self.no_confirm, auth_token: self.auth_token.clone(), + #[cfg(feature = "signatures")] + verifying_keys: self.verifying_keys.clone(), })) } } @@ -455,6 +468,8 @@ pub struct Update { progress_template: String, progress_chars: String, auth_token: Option, + #[cfg(feature = "signatures")] + verifying_keys: Vec<[u8; ed25519_dalek::PUBLIC_KEY_LENGTH]>, } impl Update { /// Initialize a new `Update` builder @@ -564,6 +579,11 @@ impl ReleaseUpdate for Update { fn api_headers(&self, auth_token: &Option) -> Result { api_headers(auth_token) } + + #[cfg(feature = "signatures")] + fn verifying_keys(&self) -> &[[u8; ed25519_dalek::PUBLIC_KEY_LENGTH]] { + &self.verifying_keys + } } impl Default for UpdateBuilder { @@ -584,6 +604,8 @@ impl Default for UpdateBuilder { progress_template: DEFAULT_PROGRESS_TEMPLATE.to_string(), progress_chars: DEFAULT_PROGRESS_CHARS.to_string(), auth_token: None, + #[cfg(feature = "signatures")] + verifying_keys: vec![], } } } diff --git a/src/backends/s3.rs b/src/backends/s3.rs index 70f5592..8fad7e4 100644 --- a/src/backends/s3.rs +++ b/src/backends/s3.rs @@ -153,6 +153,8 @@ pub struct UpdateBuilder { progress_template: String, progress_chars: String, auth_token: Option, + #[cfg(feature = "signatures")] + verifying_keys: Vec<[u8; ed25519_dalek::PUBLIC_KEY_LENGTH]>, } impl Default for UpdateBuilder { @@ -174,6 +176,8 @@ impl Default for UpdateBuilder { progress_template: DEFAULT_PROGRESS_TEMPLATE.to_string(), progress_chars: DEFAULT_PROGRESS_CHARS.to_string(), auth_token: None, + #[cfg(feature = "signatures")] + verifying_keys: vec![], } } } @@ -318,6 +322,15 @@ impl UpdateBuilder { self } + #[cfg(feature = "signatures")] + pub fn verifying_keys( + &mut self, + keys: impl Into>, + ) -> &mut Self { + self.verifying_keys = keys.into(); + self + } + /// Confirm config and create a ready-to-use `Update` /// /// * Errors: @@ -366,6 +379,8 @@ impl UpdateBuilder { show_output: self.show_output, no_confirm: self.no_confirm, auth_token: self.auth_token.clone(), + #[cfg(feature = "signatures")] + verifying_keys: self.verifying_keys.clone(), })) } } @@ -389,6 +404,8 @@ pub struct Update { progress_template: String, progress_chars: String, auth_token: Option, + #[cfg(feature = "signatures")] + verifying_keys: Vec<[u8; ed25519_dalek::PUBLIC_KEY_LENGTH]>, } impl Update { @@ -493,6 +510,11 @@ impl ReleaseUpdate for Update { fn auth_token(&self) -> Option { self.auth_token.clone() } + + #[cfg(feature = "signatures")] + fn verifying_keys(&self) -> &[[u8; ed25519_dalek::PUBLIC_KEY_LENGTH]] { + &self.verifying_keys + } } /// Obtain list of releases from AWS S3 API, from bucket and region specified, diff --git a/src/errors.rs b/src/errors.rs index 1ded8fc..537672f 100644 --- a/src/errors.rs +++ b/src/errors.rs @@ -20,6 +20,7 @@ pub enum Error { Reqwest(reqwest::Error), SemVer(semver::Error), ArchiveNotEnabled(String), + NoValidSignature, } impl std::fmt::Display for Error { @@ -37,6 +38,7 @@ impl std::fmt::Display for Error { #[cfg(feature = "archive-zip")] Zip(ref e) => write!(f, "ZipError: {}", e), ArchiveNotEnabled(ref s) => write!(f, "ArchiveNotEnabled: Archive extension '{}' not supported, please enable 'archive-{}' feature!", s, s), + NoValidSignature => write!(f, "No valid signature found"), } } } diff --git a/src/update.rs b/src/update.rs index 0857311..c67e0fb 100644 --- a/src/update.rs +++ b/src/update.rs @@ -121,6 +121,9 @@ pub trait ReleaseUpdate { /// Authorisation token for communicating with backend fn auth_token(&self) -> Option; + #[cfg(feature = "signatures")] + fn verifying_keys(&self) -> &[[u8; ed25519_dalek::PUBLIC_KEY_LENGTH]]; + /// Construct a header with an authorisation entry if an auth token is provided fn api_headers(&self, auth_token: &Option) -> Result { let mut headers = header::HeaderMap::new(); @@ -235,6 +238,38 @@ pub trait ReleaseUpdate { 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... ")?; From 2460b5955c0b083a796c283230d3ee571f5c2e49 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Kijewski?= Date: Tue, 12 Sep 2023 17:35:31 +0200 Subject: [PATCH 2/5] Verify whole ZIP file --- Cargo.toml | 5 +-- src/lib.rs | 2 + src/signatures.rs | 100 ++++++++++++++++++++++++++++++++++++++++++++++ src/update.rs | 34 ++-------------- 4 files changed, 107 insertions(+), 34 deletions(-) create mode 100644 src/signatures.rs diff --git a/Cargo.toml b/Cargo.toml index 93dfd78..6c275a1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -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) diff --git a/src/lib.rs b/src/lib.rs index 79cb6b6..c8c1279 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -141,6 +141,8 @@ use std::path; #[macro_use] extern crate log; +#[cfg(feature = "signatures")] +mod signatures; #[macro_use] mod macros; pub mod backends; diff --git a/src/signatures.rs b/src/signatures.rs new file mode 100644 index 0000000..bb7186e --- /dev/null +++ b/src/signatures.rs @@ -0,0 +1,100 @@ +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::, _>>() + .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::, _>>() + .map_err(|_| Error::NoValidSignature)?; + + let mut prehashed_message = Sha512::new(); + if signature_at_eof { + exe.seek(SeekFrom::Start(0))?; + copy(&mut exe.take(content_size), &mut prehashed_message)?; + } else { + 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) +} diff --git a/src/update.rs b/src/update.rs index c67e0fb..9a4c152 100644 --- a/src/update.rs +++ b/src/update.rs @@ -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... ")?; From 8180eedcaaddb74b04894b695bd86f40de68f8e4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Kijewski?= Date: Wed, 13 Sep 2023 03:40:16 +0200 Subject: [PATCH 3/5] Use zipsign-api --- Cargo.toml | 14 +++--- src/backends/gitea.rs | 8 ++-- src/backends/github.rs | 8 ++-- src/backends/gitlab.rs | 8 ++-- src/backends/s3.rs | 8 ++-- src/errors.rs | 38 +++++++++++----- src/lib.rs | 2 - src/signatures.rs | 100 ----------------------------------------- src/update.rs | 36 ++++++++++++++- 9 files changed, 83 insertions(+), 139 deletions(-) delete mode 100644 src/signatures.rs diff --git a/Cargo.toml b/Cargo.toml index 6c275a1..9dfb7fc 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -27,17 +27,17 @@ regex = "1" log = "0.4" urlencoding = "2.1" self-replace = "1" -ed25519-dalek = { version = "2", features = ["digest"], optional = true } +zipsign-api = { version = "0.1.0", git = "https://github.com/Kijewski/zipsign.git", default-features = false, optional = true } [features] default = ["reqwest/default-tls"] -archive-zip = ["zip"] -compression-zip-bzip2 = ["zip/bzip2"] # -compression-zip-deflate = ["zip/deflate"] # -archive-tar = ["tar"] -compression-flate2 = ["flate2", "either"] # +archive-zip = ["zip", "zipsign-api?/verify-zip"] +compression-zip-bzip2 = ["archive-zip", "zip/bzip2"] +compression-zip-deflate = ["archive-zip", "zip/deflate"] +archive-tar = ["tar", "zipsign-api?/verify-tar"] +compression-flate2 = ["archive-tar", "flate2", "either"] rustls = ["reqwest/rustls-tls"] -signatures = ["ed25519-dalek"] +signatures = ["dep:zipsign-api"] [package.metadata.docs.rs] # Whether to pass `--all-features` to Cargo (default: false) diff --git a/src/backends/gitea.rs b/src/backends/gitea.rs index 95e9d00..34e2aec 100644 --- a/src/backends/gitea.rs +++ b/src/backends/gitea.rs @@ -242,7 +242,7 @@ pub struct UpdateBuilder { progress_chars: String, auth_token: Option, #[cfg(feature = "signatures")] - verifying_keys: Vec<[u8; ed25519_dalek::PUBLIC_KEY_LENGTH]>, + verifying_keys: Vec<[u8; zipsign_api::PUBLIC_KEY_LENGTH]>, } impl UpdateBuilder { @@ -390,7 +390,7 @@ impl UpdateBuilder { #[cfg(feature = "signatures")] pub fn verifying_keys( &mut self, - keys: impl Into>, + keys: impl Into>, ) -> &mut Self { self.verifying_keys = keys.into(); self @@ -476,7 +476,7 @@ pub struct Update { progress_chars: String, auth_token: Option, #[cfg(feature = "signatures")] - verifying_keys: Vec<[u8; ed25519_dalek::PUBLIC_KEY_LENGTH]>, + verifying_keys: Vec<[u8; zipsign_api::PUBLIC_KEY_LENGTH]>, } impl Update { /// Initialize a new `Update` builder @@ -583,7 +583,7 @@ impl ReleaseUpdate for Update { } #[cfg(feature = "signatures")] - fn verifying_keys(&self) -> &[[u8; ed25519_dalek::PUBLIC_KEY_LENGTH]] { + fn verifying_keys(&self) -> &[[u8; zipsign_api::PUBLIC_KEY_LENGTH]] { &self.verifying_keys } } diff --git a/src/backends/github.rs b/src/backends/github.rs index c337cb5..55a5773 100644 --- a/src/backends/github.rs +++ b/src/backends/github.rs @@ -245,7 +245,7 @@ pub struct UpdateBuilder { auth_token: Option, custom_url: Option, #[cfg(feature = "signatures")] - verifying_keys: Vec<[u8; ed25519_dalek::PUBLIC_KEY_LENGTH]>, + verifying_keys: Vec<[u8; zipsign_api::PUBLIC_KEY_LENGTH]>, } impl UpdateBuilder { @@ -403,7 +403,7 @@ impl UpdateBuilder { #[cfg(feature = "signatures")] pub fn verifying_keys( &mut self, - keys: impl Into>, + keys: impl Into>, ) -> &mut Self { self.verifying_keys = keys.into(); self @@ -487,7 +487,7 @@ pub struct Update { auth_token: Option, custom_url: Option, #[cfg(feature = "signatures")] - verifying_keys: Vec<[u8; ed25519_dalek::PUBLIC_KEY_LENGTH]>, + verifying_keys: Vec<[u8; zipsign_api::PUBLIC_KEY_LENGTH]>, } impl Update { /// Initialize a new `Update` builder @@ -607,7 +607,7 @@ impl ReleaseUpdate for Update { } #[cfg(feature = "signatures")] - fn verifying_keys(&self) -> &[[u8; ed25519_dalek::PUBLIC_KEY_LENGTH]] { + fn verifying_keys(&self) -> &[[u8; zipsign_api::PUBLIC_KEY_LENGTH]] { &self.verifying_keys } } diff --git a/src/backends/gitlab.rs b/src/backends/gitlab.rs index f4dd8ca..8ac4b29 100644 --- a/src/backends/gitlab.rs +++ b/src/backends/gitlab.rs @@ -239,7 +239,7 @@ pub struct UpdateBuilder { progress_chars: String, auth_token: Option, #[cfg(feature = "signatures")] - verifying_keys: Vec<[u8; ed25519_dalek::PUBLIC_KEY_LENGTH]>, + verifying_keys: Vec<[u8; zipsign_api::PUBLIC_KEY_LENGTH]>, } impl UpdateBuilder { @@ -387,7 +387,7 @@ impl UpdateBuilder { #[cfg(feature = "signatures")] pub fn verifying_keys( &mut self, - keys: impl Into>, + keys: impl Into>, ) -> &mut Self { self.verifying_keys = keys.into(); self @@ -469,7 +469,7 @@ pub struct Update { progress_chars: String, auth_token: Option, #[cfg(feature = "signatures")] - verifying_keys: Vec<[u8; ed25519_dalek::PUBLIC_KEY_LENGTH]>, + verifying_keys: Vec<[u8; zipsign_api::PUBLIC_KEY_LENGTH]>, } impl Update { /// Initialize a new `Update` builder @@ -581,7 +581,7 @@ impl ReleaseUpdate for Update { } #[cfg(feature = "signatures")] - fn verifying_keys(&self) -> &[[u8; ed25519_dalek::PUBLIC_KEY_LENGTH]] { + fn verifying_keys(&self) -> &[[u8; zipsign_api::PUBLIC_KEY_LENGTH]] { &self.verifying_keys } } diff --git a/src/backends/s3.rs b/src/backends/s3.rs index 8fad7e4..f65d692 100644 --- a/src/backends/s3.rs +++ b/src/backends/s3.rs @@ -154,7 +154,7 @@ pub struct UpdateBuilder { progress_chars: String, auth_token: Option, #[cfg(feature = "signatures")] - verifying_keys: Vec<[u8; ed25519_dalek::PUBLIC_KEY_LENGTH]>, + verifying_keys: Vec<[u8; zipsign_api::PUBLIC_KEY_LENGTH]>, } impl Default for UpdateBuilder { @@ -325,7 +325,7 @@ impl UpdateBuilder { #[cfg(feature = "signatures")] pub fn verifying_keys( &mut self, - keys: impl Into>, + keys: impl Into>, ) -> &mut Self { self.verifying_keys = keys.into(); self @@ -405,7 +405,7 @@ pub struct Update { progress_chars: String, auth_token: Option, #[cfg(feature = "signatures")] - verifying_keys: Vec<[u8; ed25519_dalek::PUBLIC_KEY_LENGTH]>, + verifying_keys: Vec<[u8; zipsign_api::PUBLIC_KEY_LENGTH]>, } impl Update { @@ -512,7 +512,7 @@ impl ReleaseUpdate for Update { } #[cfg(feature = "signatures")] - fn verifying_keys(&self) -> &[[u8; ed25519_dalek::PUBLIC_KEY_LENGTH]] { + fn verifying_keys(&self) -> &[[u8; zipsign_api::PUBLIC_KEY_LENGTH]] { &self.verifying_keys } } diff --git a/src/errors.rs b/src/errors.rs index 537672f..09ed3e2 100644 --- a/src/errors.rs +++ b/src/errors.rs @@ -20,7 +20,12 @@ pub enum Error { Reqwest(reqwest::Error), SemVer(semver::Error), ArchiveNotEnabled(String), - NoValidSignature, + #[cfg(feature = "signatures")] + NoSignatures(crate::ArchiveKind), + #[cfg(feature = "signatures")] + SignatureError(zipsign_api::verify::Error), + #[cfg(feature = "signatures")] + NonUTF8, } impl std::fmt::Display for Error { @@ -38,7 +43,14 @@ impl std::fmt::Display for Error { #[cfg(feature = "archive-zip")] Zip(ref e) => write!(f, "ZipError: {}", e), ArchiveNotEnabled(ref s) => write!(f, "ArchiveNotEnabled: Archive extension '{}' not supported, please enable 'archive-{}' feature!", s, s), - NoValidSignature => write!(f, "No valid signature found"), + #[cfg(feature = "signatures")] + NoSignatures(kind) => { + write!(f, "No signature verification implemented for {:?} files", kind) + } + #[cfg(feature = "signatures")] + SignatureError(ref e) => write!(f, "SignatureError: {}", e), + #[cfg(feature = "signatures")] + NonUTF8 => write!(f, "Cannot verify signature of a file with a non-UTF-8 name"), } } } @@ -49,23 +61,25 @@ impl std::error::Error for Error { } fn cause(&self) -> Option<&dyn std::error::Error> { - use Error::*; Some(match *self { - Io(ref e) => e, - Json(ref e) => e, - Reqwest(ref e) => e, - SemVer(ref e) => e, + Error::Io(ref e) => e, + Error::Json(ref e) => e, + Error::Reqwest(ref e) => e, + Error::SemVer(ref e) => e, + #[cfg(feature = "signatures")] + Error::SignatureError(ref e) => e, _ => return None, }) } fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { - use Error::*; Some(match *self { - Io(ref e) => e, - Json(ref e) => e, - Reqwest(ref e) => e, - SemVer(ref e) => e, + Error::Io(ref e) => e, + Error::Json(ref e) => e, + Error::Reqwest(ref e) => e, + Error::SemVer(ref e) => e, + #[cfg(feature = "signatures")] + Error::SignatureError(ref e) => e, _ => return None, }) } diff --git a/src/lib.rs b/src/lib.rs index c8c1279..79cb6b6 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -141,8 +141,6 @@ use std::path; #[macro_use] extern crate log; -#[cfg(feature = "signatures")] -mod signatures; #[macro_use] mod macros; pub mod backends; diff --git a/src/signatures.rs b/src/signatures.rs deleted file mode 100644 index bb7186e..0000000 --- a/src/signatures.rs +++ /dev/null @@ -1,100 +0,0 @@ -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::, _>>() - .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::, _>>() - .map_err(|_| Error::NoValidSignature)?; - - let mut prehashed_message = Sha512::new(); - if signature_at_eof { - exe.seek(SeekFrom::Start(0))?; - copy(&mut exe.take(content_size), &mut prehashed_message)?; - } else { - 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) -} diff --git a/src/update.rs b/src/update.rs index 9a4c152..d49b361 100644 --- a/src/update.rs +++ b/src/update.rs @@ -122,7 +122,7 @@ pub trait ReleaseUpdate { fn auth_token(&self) -> Option; #[cfg(feature = "signatures")] - fn verifying_keys(&self) -> &[[u8; ed25519_dalek::PUBLIC_KEY_LENGTH]]; + fn verifying_keys(&self) -> &[[u8; zipsign_api::PUBLIC_KEY_LENGTH]]; /// Construct a header with an authorisation entry if an auth token is provided fn api_headers(&self, auth_token: &Option) -> Result { @@ -234,7 +234,7 @@ pub trait ReleaseUpdate { download.download_to(&mut tmp_archive)?; #[cfg(feature = "signatures")] - crate::signatures::verify(&tmp_archive_path, self.verifying_keys())?; + verify_signature(&tmp_archive_path, self.verifying_keys())?; print_flush(show_output, "Extracting archive... ")?; let bin_path_in_archive = self.bin_path_in_archive(); @@ -266,3 +266,35 @@ fn println(show_output: bool, msg: &str) { println!("{}", msg); } } + +#[cfg(feature = "signatures")] +fn verify_signature( + archive_path: &std::path::Path, + keys: &[[u8; zipsign_api::PUBLIC_KEY_LENGTH]], +) -> crate::Result<()> { + if keys.is_empty() { + return Ok(()); + } + + println!("Verifying downloaded file..."); + + let file_name = archive_path + .file_name() + .and_then(|s| s.to_str()) + .map(|s| s.as_bytes()) + .ok_or(Error::NonUTF8)?; + let archive_kind = crate::detect_archive(&archive_path)?; + + let mut exe = std::fs::File::open(&archive_path)?; + + let result = match archive_kind { + #[cfg(feature = "archive-tar")] + crate::ArchiveKind::Tar(Some(crate::Compression::Gz)) => { + zipsign_api::verify::verify_tar(&mut exe, keys, Some(file_name)) + } + #[cfg(feature = "archive-zip")] + crate::ArchiveKind::Zip => zipsign_api::verify::verify_zip(&mut exe, keys, Some(file_name)), + archive_kind => return Err(Error::NoSignatures(archive_kind)), + }; + result.map(|_| ()).map_err(Error::SignatureError) +} From a856c0156be83da6cff082e3798124eb358d0dff Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Kijewski?= Date: Thu, 14 Sep 2023 20:42:54 +0200 Subject: [PATCH 4/5] Simpler verifying_keys() user interface --- Cargo.toml | 2 +- src/backends/gitea.rs | 15 +++++++++--- src/backends/github.rs | 15 +++++++++--- src/backends/gitlab.rs | 15 +++++++++--- src/backends/s3.rs | 15 +++++++++--- src/errors.rs | 25 ++++++++----------- src/lib.rs | 3 +++ src/update.rs | 55 +++++++++++++++++++++++++++--------------- 8 files changed, 97 insertions(+), 48 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 9dfb7fc..5058e6e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -27,7 +27,7 @@ regex = "1" log = "0.4" urlencoding = "2.1" self-replace = "1" -zipsign-api = { version = "0.1.0", git = "https://github.com/Kijewski/zipsign.git", default-features = false, optional = true } +zipsign-api = { version = "0.1.0-a.1", default-features = false, optional = true } [features] default = ["reqwest/default-tls"] diff --git a/src/backends/gitea.rs b/src/backends/gitea.rs index 34e2aec..b007399 100644 --- a/src/backends/gitea.rs +++ b/src/backends/gitea.rs @@ -387,12 +387,21 @@ impl UpdateBuilder { self } - #[cfg(feature = "signatures")] + /// Specify a slice of ed25519ph verifying keys to validate a download's authenticy + /// + /// Unless the feature `"signatures"` is activated, this methods does nothing. + /// If the feature is activated AND at least one key was provided, a download is verifying. + /// At least one key has to match. pub fn verifying_keys( &mut self, - keys: impl Into>, + keys: impl Into>, ) -> &mut Self { - self.verifying_keys = keys.into(); + #[cfg(feature = "signatures")] + { + self.verifying_keys = keys.into(); + } + #[cfg(not(feature = "signatures"))] + drop(keys); self } diff --git a/src/backends/github.rs b/src/backends/github.rs index 55a5773..0f9be47 100644 --- a/src/backends/github.rs +++ b/src/backends/github.rs @@ -400,12 +400,21 @@ impl UpdateBuilder { self } - #[cfg(feature = "signatures")] + /// Specify a slice of ed25519ph verifying keys to validate a download's authenticy + /// + /// Unless the feature `"signatures"` is activated, this methods does nothing. + /// If the feature is activated AND at least one key was provided, a download is verifying. + /// At least one key has to match. pub fn verifying_keys( &mut self, - keys: impl Into>, + keys: impl Into>, ) -> &mut Self { - self.verifying_keys = keys.into(); + #[cfg(feature = "signatures")] + { + self.verifying_keys = keys.into(); + } + #[cfg(not(feature = "signatures"))] + drop(keys); self } diff --git a/src/backends/gitlab.rs b/src/backends/gitlab.rs index 8ac4b29..4ea59f5 100644 --- a/src/backends/gitlab.rs +++ b/src/backends/gitlab.rs @@ -384,12 +384,21 @@ impl UpdateBuilder { self } - #[cfg(feature = "signatures")] + /// Specify a slice of ed25519ph verifying keys to validate a download's authenticy + /// + /// Unless the feature `"signatures"` is activated, this methods does nothing. + /// If the feature is activated AND at least one key was provided, a download is verifying. + /// At least one key has to match. pub fn verifying_keys( &mut self, - keys: impl Into>, + keys: impl Into>, ) -> &mut Self { - self.verifying_keys = keys.into(); + #[cfg(feature = "signatures")] + { + self.verifying_keys = keys.into(); + } + #[cfg(not(feature = "signatures"))] + drop(keys); self } diff --git a/src/backends/s3.rs b/src/backends/s3.rs index f65d692..58b72a2 100644 --- a/src/backends/s3.rs +++ b/src/backends/s3.rs @@ -322,12 +322,21 @@ impl UpdateBuilder { self } - #[cfg(feature = "signatures")] + /// Specify a slice of ed25519ph verifying keys to validate a download's authenticy + /// + /// Unless the feature `"signatures"` is activated, this methods does nothing. + /// If the feature is activated AND at least one key was provided, a download is verifying. + /// At least one key has to match. pub fn verifying_keys( &mut self, - keys: impl Into>, + keys: impl Into>, ) -> &mut Self { - self.verifying_keys = keys.into(); + #[cfg(feature = "signatures")] + { + self.verifying_keys = keys.into(); + } + #[cfg(not(feature = "signatures"))] + drop(keys); self } diff --git a/src/errors.rs b/src/errors.rs index 09ed3e2..7e19981 100644 --- a/src/errors.rs +++ b/src/errors.rs @@ -23,7 +23,7 @@ pub enum Error { #[cfg(feature = "signatures")] NoSignatures(crate::ArchiveKind), #[cfg(feature = "signatures")] - SignatureError(zipsign_api::verify::Error), + Signature(zipsign_api::ZipsignError), #[cfg(feature = "signatures")] NonUTF8, } @@ -48,7 +48,7 @@ impl std::fmt::Display for Error { write!(f, "No signature verification implemented for {:?} files", kind) } #[cfg(feature = "signatures")] - SignatureError(ref e) => write!(f, "SignatureError: {}", e), + Signature(ref e) => write!(f, "SignatureError: {}", e), #[cfg(feature = "signatures")] NonUTF8 => write!(f, "Cannot verify signature of a file with a non-UTF-8 name"), } @@ -60,18 +60,6 @@ impl std::error::Error for Error { "Self Update Error" } - fn cause(&self) -> Option<&dyn std::error::Error> { - Some(match *self { - Error::Io(ref e) => e, - Error::Json(ref e) => e, - Error::Reqwest(ref e) => e, - Error::SemVer(ref e) => e, - #[cfg(feature = "signatures")] - Error::SignatureError(ref e) => e, - _ => return None, - }) - } - fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { Some(match *self { Error::Io(ref e) => e, @@ -79,7 +67,7 @@ impl std::error::Error for Error { Error::Reqwest(ref e) => e, Error::SemVer(ref e) => e, #[cfg(feature = "signatures")] - Error::SignatureError(ref e) => e, + Error::Signature(ref e) => e, _ => return None, }) } @@ -115,3 +103,10 @@ impl From for Error { Error::Zip(e) } } + +#[cfg(feature = "signatures")] +impl From for Error { + fn from(e: zipsign_api::ZipsignError) -> Error { + Error::Signature(e) + } +} diff --git a/src/lib.rs b/src/lib.rs index 79cb6b6..d4eb1bc 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -154,6 +154,9 @@ pub const DEFAULT_PROGRESS_CHARS: &str = "=>-"; use errors::*; +/// The length of a verifying key for `feature = "signatures"` +pub const PUBLIC_KEY_LENGTH: usize = 32; + /// Get the current target triple. /// /// Returns a target triple (e.g. `x86_64-unknown-linux-gnu` or `i686-pc-windows-msvc`) diff --git a/src/update.rs b/src/update.rs index d49b361..f0be67b 100644 --- a/src/update.rs +++ b/src/update.rs @@ -121,8 +121,10 @@ pub trait ReleaseUpdate { /// Authorisation token for communicating with backend fn auth_token(&self) -> Option; - #[cfg(feature = "signatures")] - fn verifying_keys(&self) -> &[[u8; zipsign_api::PUBLIC_KEY_LENGTH]]; + /// ed25519ph verifying keys to validate a download's authenticy + fn verifying_keys(&self) -> &[[u8; crate::PUBLIC_KEY_LENGTH]] { + &[] + } /// Construct a header with an authorisation entry if an auth token is provided fn api_headers(&self, auth_token: &Option) -> Result { @@ -278,23 +280,36 @@ fn verify_signature( println!("Verifying downloaded file..."); - let file_name = archive_path - .file_name() - .and_then(|s| s.to_str()) - .map(|s| s.as_bytes()) - .ok_or(Error::NonUTF8)?; - let archive_kind = crate::detect_archive(&archive_path)?; - - let mut exe = std::fs::File::open(&archive_path)?; - - let result = match archive_kind { - #[cfg(feature = "archive-tar")] - crate::ArchiveKind::Tar(Some(crate::Compression::Gz)) => { - zipsign_api::verify::verify_tar(&mut exe, keys, Some(file_name)) + let archive_kind = crate::detect_archive(archive_path)?; + #[cfg(any(feature = "archive-tar", feature = "archive-zip"))] + { + let context = archive_path + .file_name() + .and_then(|s| s.to_str()) + .map(|s| s.as_bytes()) + .ok_or(Error::NonUTF8)?; + + let keys = keys.iter().copied().map(Ok); + let keys = + zipsign_api::verify::collect_keys(keys).map_err(zipsign_api::ZipsignError::from)?; + + let mut exe = std::fs::File::open(&archive_path)?; + + match archive_kind { + #[cfg(feature = "archive-tar")] + crate::ArchiveKind::Tar(Some(crate::Compression::Gz)) => { + zipsign_api::verify::verify_tar(&mut exe, &keys, Some(context)) + .map_err(zipsign_api::ZipsignError::from)?; + return Ok(()); + } + #[cfg(feature = "archive-zip")] + crate::ArchiveKind::Zip => { + zipsign_api::verify::verify_zip(&mut exe, &keys, Some(context)) + .map_err(zipsign_api::ZipsignError::from)?; + return Ok(()); + } + _ => {} } - #[cfg(feature = "archive-zip")] - crate::ArchiveKind::Zip => zipsign_api::verify::verify_zip(&mut exe, keys, Some(file_name)), - archive_kind => return Err(Error::NoSignatures(archive_kind)), - }; - result.map(|_| ()).map_err(Error::SignatureError) + } + Err(Error::NoSignatures(archive_kind)) } From c37966c05512660832316ebd5856bbad61c6ca11 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Kijewski?= Date: Sat, 16 Sep 2023 17:07:21 +0200 Subject: [PATCH 5/5] Update to zipsign-api v0.1.0-a.3 --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index 5058e6e..85fbe7b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -27,7 +27,7 @@ regex = "1" log = "0.4" urlencoding = "2.1" self-replace = "1" -zipsign-api = { version = "0.1.0-a.1", default-features = false, optional = true } +zipsign-api = { version = "0.1.0-a.3", default-features = false, optional = true } [features] default = ["reqwest/default-tls"]