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..85fbe7b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -27,15 +27,17 @@ regex = "1" log = "0.4" urlencoding = "2.1" self-replace = "1" +zipsign-api = { version = "0.1.0-a.3", 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 = ["dep:zipsign-api"] [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..b007399 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; zipsign_api::PUBLIC_KEY_LENGTH]>, } impl UpdateBuilder { @@ -385,6 +387,24 @@ impl UpdateBuilder { self } + /// 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>, + ) -> &mut Self { + #[cfg(feature = "signatures")] + { + self.verifying_keys = keys.into(); + } + #[cfg(not(feature = "signatures"))] + drop(keys); + self + } + /// Confirm config and create a ready-to-use `Update` /// /// * Errors: @@ -440,6 +460,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 +484,8 @@ pub struct Update { progress_template: String, progress_chars: String, auth_token: Option, + #[cfg(feature = "signatures")] + verifying_keys: Vec<[u8; zipsign_api::PUBLIC_KEY_LENGTH]>, } impl Update { /// Initialize a new `Update` builder @@ -566,6 +590,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; zipsign_api::PUBLIC_KEY_LENGTH]] { + &self.verifying_keys + } } impl Default for UpdateBuilder { @@ -586,6 +615,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..0f9be47 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; zipsign_api::PUBLIC_KEY_LENGTH]>, } impl UpdateBuilder { @@ -398,6 +400,24 @@ impl UpdateBuilder { self } + /// 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>, + ) -> &mut Self { + #[cfg(feature = "signatures")] + { + self.verifying_keys = keys.into(); + } + #[cfg(not(feature = "signatures"))] + drop(keys); + self + } + /// Confirm config and create a ready-to-use `Update` /// /// * Errors: @@ -450,6 +470,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 +495,8 @@ pub struct Update { progress_chars: String, auth_token: Option, custom_url: Option, + #[cfg(feature = "signatures")] + verifying_keys: Vec<[u8; zipsign_api::PUBLIC_KEY_LENGTH]>, } impl Update { /// Initialize a new `Update` builder @@ -590,6 +614,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; zipsign_api::PUBLIC_KEY_LENGTH]] { + &self.verifying_keys + } } impl Default for UpdateBuilder { @@ -611,6 +640,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..4ea59f5 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; zipsign_api::PUBLIC_KEY_LENGTH]>, } impl UpdateBuilder { @@ -382,6 +384,24 @@ impl UpdateBuilder { self } + /// 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>, + ) -> &mut Self { + #[cfg(feature = "signatures")] + { + self.verifying_keys = keys.into(); + } + #[cfg(not(feature = "signatures"))] + drop(keys); + self + } + /// Confirm config and create a ready-to-use `Update` /// /// * Errors: @@ -433,6 +453,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 +477,8 @@ pub struct Update { progress_template: String, progress_chars: String, auth_token: Option, + #[cfg(feature = "signatures")] + verifying_keys: Vec<[u8; zipsign_api::PUBLIC_KEY_LENGTH]>, } impl Update { /// Initialize a new `Update` builder @@ -564,6 +588,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; zipsign_api::PUBLIC_KEY_LENGTH]] { + &self.verifying_keys + } } impl Default for UpdateBuilder { @@ -584,6 +613,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..58b72a2 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; zipsign_api::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,24 @@ impl UpdateBuilder { self } + /// 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>, + ) -> &mut Self { + #[cfg(feature = "signatures")] + { + self.verifying_keys = keys.into(); + } + #[cfg(not(feature = "signatures"))] + drop(keys); + self + } + /// Confirm config and create a ready-to-use `Update` /// /// * Errors: @@ -366,6 +388,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 +413,8 @@ pub struct Update { progress_template: String, progress_chars: String, auth_token: Option, + #[cfg(feature = "signatures")] + verifying_keys: Vec<[u8; zipsign_api::PUBLIC_KEY_LENGTH]>, } impl Update { @@ -493,6 +519,11 @@ impl ReleaseUpdate for Update { fn auth_token(&self) -> Option { self.auth_token.clone() } + + #[cfg(feature = "signatures")] + fn verifying_keys(&self) -> &[[u8; zipsign_api::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..7e19981 100644 --- a/src/errors.rs +++ b/src/errors.rs @@ -20,6 +20,12 @@ pub enum Error { Reqwest(reqwest::Error), SemVer(semver::Error), ArchiveNotEnabled(String), + #[cfg(feature = "signatures")] + NoSignatures(crate::ArchiveKind), + #[cfg(feature = "signatures")] + Signature(zipsign_api::ZipsignError), + #[cfg(feature = "signatures")] + NonUTF8, } impl std::fmt::Display for Error { @@ -37,6 +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), + #[cfg(feature = "signatures")] + NoSignatures(kind) => { + write!(f, "No signature verification implemented for {:?} files", kind) + } + #[cfg(feature = "signatures")] + 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"), } } } @@ -46,24 +60,14 @@ impl std::error::Error for Error { "Self Update 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, - _ => 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::Signature(ref e) => e, _ => return None, }) } @@ -99,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 0857311..f0be67b 100644 --- a/src/update.rs +++ b/src/update.rs @@ -121,6 +121,11 @@ pub trait ReleaseUpdate { /// Authorisation token for communicating with backend fn auth_token(&self) -> Option; + /// 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 { let mut headers = header::HeaderMap::new(); @@ -230,11 +235,15 @@ pub trait ReleaseUpdate { download.download_to(&mut tmp_archive)?; + #[cfg(feature = "signatures")] + verify_signature(&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); + println(show_output, "Done"); print_flush(show_output, "Replacing binary file... ")?; @@ -259,3 +268,48 @@ 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 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(()); + } + _ => {} + } + } + Err(Error::NoSignatures(archive_kind)) +}