diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d82da87..805eabb 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -183,8 +183,10 @@ jobs: tar czf Cargo.lock.tgz Cargo.lock zipsign sign tar Cargo.lock.tgz priv.key zipsign verify tar Cargo.lock.tgz pub.key + zipsign unsign tar Cargo.lock.tgz # Windows doesn't have a "zip" command jar -cfM Cargo.lock.zip Cargo.lock zipsign sign zip Cargo.lock.zip priv.key zipsign verify zip Cargo.lock.zip pub.key + zipsign unsign zip Cargo.lock.zip diff --git a/README.md b/README.md index 6fd52b9..6924d75 100644 --- a/README.md +++ b/README.md @@ -89,7 +89,7 @@ Options: * `-o`, `--output `: Signed file to generate (if omitted, the input is overwritten) * `-c`, `--context `: Arbitrary string used to salt the input, defaults to file name of `` -* `-f`, `--force`: Overwrite output file if it exists +* `-f`, `--force`: Overwrite output file if it exists Arguments: @@ -115,6 +115,24 @@ Arguments: * ``: Signed `.zip` or `.tar.gz` file * `...`: One or more files containing verifying keys +### Remove signatures + +Usage: `zipsign unsign [zip|tar] [-o ] ` + +Subcommands: + +* `zip`: Removed signatures from `.zip` file +* `tar`: Removed signatures from `.tar.gz` file + +Arguments: + +* ``: Signed `.zip` or `.tar.gz` file + +Options: + +* `-o`, `--output `: Unsigned file to generate (if omitted, the input is overwritten) +* `-f`, `--force`: Overwrite output file if it exists + ### How does it work? The files are signed with one or more private keys using [ed25519ph](https://datatracker.ietf.org/doc/html/rfc8032#section-5.1). diff --git a/api/Cargo.toml b/api/Cargo.toml index 6ca64ce..2e4e5af 100644 --- a/api/Cargo.toml +++ b/api/Cargo.toml @@ -19,11 +19,14 @@ default = ["tar", "zip"] verify-tar = ["dep:base64"] verify-zip = [] +unsign-tar = ["dep:base64"] +unsign-zip = ["dep:zip"] + sign-tar = ["dep:base64"] sign-zip = ["dep:zip"] -tar = ["sign-tar", "verify-tar"] -zip = ["sign-zip", "verify-zip"] +tar = ["sign-tar", "unsign-tar", "verify-tar"] +zip = ["sign-zip", "unsign-zip", "verify-zip"] [package.metadata.docs.rs] all-features = true diff --git a/api/src/constants.rs b/api/src/constants.rs index 371aebb..5ae23ee 100644 --- a/api/src/constants.rs +++ b/api/src/constants.rs @@ -15,7 +15,7 @@ pub(crate) type SignatureCountLeInt = u16; /// Followed by base64 encoded signatures string, the current stream position before this block /// encoded as zero-padded 16 bytes hexadecimal string, and [`GZIP_END`] /// [`GZIP_END`] -#[cfg(any(feature = "sign-tar", feature = "verify-tar"))] +#[cfg(any(feature = "sign-tar", feature = "unsign-tar", feature = "verify-tar"))] pub(crate) const GZIP_START: &[u8; 10] = { const EPOCH: u32 = 978_307_200; // 2001-01-01 00:00:00 Z @@ -31,7 +31,7 @@ pub(crate) const GZIP_START: &[u8; 10] = { }; /// Suffix of the signature block in a signed .tar.gz file -#[cfg(any(feature = "sign-tar", feature = "verify-tar"))] +#[cfg(any(feature = "sign-tar", feature = "unsign-tar", feature = "verify-tar"))] pub(crate) const GZIP_END: &[u8; 14] = &[ 0x00, // deflate: NUL terminator, end of comments 0x01, // deflate: block header (final block, uncompressed) diff --git a/api/src/lib.rs b/api/src/lib.rs index a41df4c..18126ed 100644 --- a/api/src/lib.rs +++ b/api/src/lib.rs @@ -10,7 +10,7 @@ #![warn(missing_docs)] #![warn(non_ascii_idents)] #![warn(noop_method_call)] -#![warn(rust_2018_idioms)] +#![warn(rust_2021_idioms)] #![warn(single_use_lifetimes)] #![warn(trivial_casts)] #![warn(unreachable_pub)] @@ -23,7 +23,12 @@ mod constants; pub mod sign; +#[cfg(any(feature = "sign-zip", feature = "unsign-zip"))] +mod sign_unsign_zip; +pub mod unsign; pub mod verify; +#[cfg(any(feature = "verify-tar", feature = "unsign-tar"))] +mod verify_unsign_tar; use std::fmt; use std::io::{self, Read}; @@ -108,6 +113,15 @@ pub enum ZipsignError { #[cfg_attr(docsrs, doc(cfg(feature = "verify-zip")))] VerifyZip(#[from] self::verify::VerifyZipError), + /// An error returned by [`copy_and_unsign_tar()`][self::unsign::copy_and_unsign_tar] + #[cfg(feature = "unsign-tar")] + #[cfg_attr(docsrs, doc(cfg(feature = "unsign-tar")))] + UnsignTar(#[from] self::unsign::UnsignTarError), + /// An error returned by [`copy_and_unsign_zip()`][self::unsign::copy_and_unsign_zip] + #[cfg(feature = "unsign-zip")] + #[cfg_attr(docsrs, doc(cfg(feature = "unsign-zip")))] + UnsignZip(#[from] self::unsign::UnsignZipError), + /// An I/O occurred Io(#[from] io::Error), } @@ -124,7 +138,7 @@ macro_rules! Error { ),+ $(,)? } ) => { $(#[$meta])+ - $vis struct $outer($inner); + $vis struct $outer(Box<$inner>); #[derive(Debug, thiserror::Error)] enum $inner { $( @@ -138,21 +152,21 @@ macro_rules! Error { impl std::fmt::Debug for $outer { #[inline] fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - std::fmt::Debug::fmt(&self.0, f) + std::fmt::Debug::fmt(&*self.0, f) } } impl std::fmt::Display for $outer { #[inline] fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - std::fmt::Display::fmt(&self.0, f) + std::fmt::Display::fmt(&*self.0, f) } } impl From<$inner> for $outer { #[inline] fn from(value: $inner) -> Self { - Self(value) + Self(Box::new(value)) } } diff --git a/api/src/sign/tar.rs b/api/src/sign/tar.rs index a2d3a91..89ffdfe 100644 --- a/api/src/sign/tar.rs +++ b/api/src/sign/tar.rs @@ -40,8 +40,8 @@ pub fn copy_and_sign_tar( context: Option<&[u8]>, ) -> Result<(), SignTarError> where - I: ?Sized + Read + Seek, - O: ?Sized + Read + Seek + Write, + I: ?Sized + Seek + Read, + O: ?Sized + Seek + Write, { if keys.len() > SignatureCountLeInt::MAX as usize { return Err(Error::TooManyKeys.into()); diff --git a/api/src/sign/zip.rs b/api/src/sign/zip.rs index 44fc346..3007ab1 100644 --- a/api/src/sign/zip.rs +++ b/api/src/sign/zip.rs @@ -1,21 +1,17 @@ #![cfg_attr(docsrs, doc(cfg(feature = "sign-zip")))] -use std::io::{BufReader, BufWriter, IoSlice, Read, Seek, SeekFrom, Write}; - -use zip::result::ZipError; -use zip::{ZipArchive, ZipWriter}; +use std::io::{IoSlice, Read, Seek, SeekFrom, Write}; use super::{gather_signature_data, GatherSignatureDataError}; use crate::constants::{SignatureCountLeInt, BUF_LIMIT, HEADER_SIZE}; +use crate::sign_unsign_zip::{copy_zip, CopyZipError}; use crate::{Prehash, SigningKey, SIGNATURE_LENGTH}; crate::Error! { /// An error returned by [`copy_and_sign_zip()`] pub struct SignZipError(Error) { - #[error("could not read input ZIP")] - InputZip(#[source] ZipError), - #[error("could not read file #{1} inside input ZIP")] - InputZipIndex(#[source] ZipError, usize), + #[error("could not copy ZIP data")] + Copy(#[source] CopyZipError), #[error("could not write to output, device full?")] OutputFull, #[error("could not read output")] @@ -24,10 +20,6 @@ crate::Error! { OutputSeek(#[source] std::io::Error), #[error("could not write to output")] OutputWrite(#[source] std::io::Error), - #[error("could not write ZIP file #{1} to output")] - OutputZip(#[source] ZipError, usize), - #[error("could not finish writing output ZIP")] - OutputZipFinish(#[source] ZipError), #[error("could not gather signature data")] Sign(#[source] GatherSignatureDataError), #[error("too many keys")] @@ -56,7 +48,7 @@ where // copy ZIP write_padding(signature_bytes, output)?; - copy_zip(input, output)?; + copy_zip(input, output).map_err(Error::Copy)?; // gather signature let _ = output @@ -67,9 +59,8 @@ where // write signature output.rewind().map_err(Error::OutputSeek)?; - output - .write_all(&buf) - .map_err(|err| SignZipError(Error::OutputWrite(err))) + output.write_all(&buf).map_err(Error::OutputWrite)?; + Ok(()) } fn write_padding(mut padding_to_write: usize, output: &mut O) -> Result<(), Error> @@ -95,29 +86,3 @@ where } Ok(()) } - -fn copy_zip(input: &mut I, output: &mut O) -> Result<(), Error> -where - I: ?Sized + Read + Seek, - O: ?Sized + Write + Seek, -{ - let mut input = ZipArchive::new(BufReader::new(input)).map_err(Error::InputZip)?; - let mut output = ZipWriter::new(BufWriter::new(output)); - - output.set_raw_comment(input.comment().to_owned()); - for idx in 0..input.len() { - let file = input - .by_index_raw(idx) - .map_err(|err| Error::InputZipIndex(err, idx))?; - output - .raw_copy_file(file) - .map_err(|err| Error::OutputZip(err, idx))?; - } - output - .finish() - .map_err(Error::OutputZipFinish)? - .flush() - .map_err(Error::OutputWrite)?; - - Ok(()) -} diff --git a/api/src/sign_unsign_zip.rs b/api/src/sign_unsign_zip.rs new file mode 100644 index 0000000..2f2f91f --- /dev/null +++ b/api/src/sign_unsign_zip.rs @@ -0,0 +1,44 @@ +use std::io::{BufReader, BufWriter, Read, Seek, Write}; + +use zip::result::ZipError; +use zip::{ZipArchive, ZipWriter}; + +#[derive(Debug, thiserror::Error)] +pub(crate) enum CopyZipError { + #[error("could not read input ZIP")] + InputZip(#[source] ZipError), + #[error("could not read file #{1} inside input ZIP")] + InputZipIndex(#[source] ZipError, usize), + #[error("could not write to output")] + OutputWrite(#[source] std::io::Error), + #[error("could not write ZIP file #{1} to output")] + OutputZip(#[source] ZipError, usize), + #[error("could not finish writing output ZIP")] + OutputZipFinish(#[source] ZipError), +} + +pub(crate) fn copy_zip(input: &mut I, output: &mut O) -> Result<(), CopyZipError> +where + I: ?Sized + Seek + Read, + O: ?Sized + Seek + Write, +{ + let mut input = ZipArchive::new(BufReader::new(input)).map_err(CopyZipError::InputZip)?; + let mut output = ZipWriter::new(BufWriter::new(output)); + + output.set_raw_comment(input.comment().to_owned()); + for idx in 0..input.len() { + let file = input + .by_index_raw(idx) + .map_err(|err| CopyZipError::InputZipIndex(err, idx))?; + output + .raw_copy_file(file) + .map_err(|err| CopyZipError::OutputZip(err, idx))?; + } + output + .finish() + .map_err(CopyZipError::OutputZipFinish)? + .flush() + .map_err(CopyZipError::OutputWrite)?; + + Ok(()) +} diff --git a/api/src/unsign/mod.rs b/api/src/unsign/mod.rs new file mode 100644 index 0000000..8661532 --- /dev/null +++ b/api/src/unsign/mod.rs @@ -0,0 +1,11 @@ +//! Functions to remove signatures from a file + +#[cfg(feature = "unsign-tar")] +mod tar; +#[cfg(feature = "unsign-zip")] +mod zip; + +#[cfg(feature = "unsign-tar")] +pub use self::tar::{copy_and_unsign_tar, UnsignTarError}; +#[cfg(feature = "unsign-zip")] +pub use self::zip::{copy_and_unsign_zip, UnsignZipError}; diff --git a/api/src/unsign/tar.rs b/api/src/unsign/tar.rs new file mode 100644 index 0000000..e5c4568 --- /dev/null +++ b/api/src/unsign/tar.rs @@ -0,0 +1,42 @@ +#![cfg_attr(docsrs, doc(cfg(feature = "unsign-tar")))] + +use std::io::{copy, Read, Seek, Write}; + +use crate::verify_unsign_tar::{ + tar_find_data_start_and_len, tar_read_signatures, TarFindDataStartAndLenError, + TarReadSignaturesError, +}; + +crate::Error! { + /// An error returned by [`copy_and_unsign_tar()`] + pub struct UnsignTarError(Error) { + #[error("could not copy data")] + Copy(#[source] std::io::Error), + #[error("could not find read signatures in .tar.gz file")] + FindDataStartAndLen(#[source] TarFindDataStartAndLenError), + #[error("could not find read signatures in .tar.gz file")] + ReadSignatures(#[source] TarReadSignaturesError), + #[error("could not seek inside the input")] + Seek(#[source] std::io::Error), + } +} + +/// Copy a signed `.tar.gz` file without the signatures +pub fn copy_and_unsign_tar(input: &mut I, output: &mut O) -> Result<(), UnsignTarError> +where + I: ?Sized + Seek + Read, + O: ?Sized + Seek + Write, +{ + // seek to start of base64 encoded signatures + let (data_start, data_len) = + tar_find_data_start_and_len(input).map_err(Error::FindDataStartAndLen)?; + + // read base64 encoded signatures + let _ = tar_read_signatures(data_start, data_len, input).map_err(Error::ReadSignatures)?; + + // copy data + input.rewind().map_err(Error::Seek)?; + let _ = copy(&mut input.take(data_start), output).map_err(Error::Copy)?; + + Ok(()) +} diff --git a/api/src/unsign/zip.rs b/api/src/unsign/zip.rs new file mode 100644 index 0000000..669e94e --- /dev/null +++ b/api/src/unsign/zip.rs @@ -0,0 +1,23 @@ +#![cfg_attr(docsrs, doc(cfg(feature = "unsign-zip")))] + +use std::io::{Read, Seek, Write}; + +use crate::sign_unsign_zip::{copy_zip, CopyZipError}; + +crate::Error! { + /// An error returned by [`copy_and_unsign_zip()`] + pub struct UnsignZipError(Error) { + #[error("could not copy ZIP data")] + Copy(#[source] CopyZipError), + } +} + +/// Copy a signed `.zip` file without the signatures +pub fn copy_and_unsign_zip(input: &mut I, output: &mut O) -> Result<(), UnsignZipError> +where + I: ?Sized + Read + Seek, + O: ?Sized + Read + Seek + Write, +{ + copy_zip(input, output).map_err(Error::Copy)?; + Ok(()) +} diff --git a/api/src/verify/tar.rs b/api/src/verify/tar.rs index 960ff68..e1c53d2 100644 --- a/api/src/verify/tar.rs +++ b/api/src/verify/tar.rs @@ -1,40 +1,27 @@ #![cfg_attr(docsrs, doc(cfg(feature = "verify-tar")))] -use std::io::{Read, Seek, SeekFrom}; -use std::mem::size_of; - -use base64::prelude::BASE64_STANDARD; -use base64::Engine; +use std::io::{Read, Seek}; use super::{find_match, NoMatch}; -use crate::constants::{ - SignatureCountLeInt, BUF_LIMIT, GZIP_END, GZIP_START, HEADER_SIZE, MAGIC_HEADER, +use crate::verify_unsign_tar::{ + tar_find_data_start_and_len, tar_read_signatures, TarFindDataStartAndLenError, + TarReadSignaturesError, }; -use crate::{Prehash, Signature, SignatureError, VerifyingKey, SIGNATURE_LENGTH}; +use crate::{Prehash, VerifyingKey}; crate::Error! { /// An error returned by [`verify_tar()`] pub struct VerifyTarError(Error) { - #[error("the input contained invalid base64 encoded data")] - Base64, - #[error("the input contained no signatures")] - Empty, - #[error("the expected last GZIP block was missing or corrupted")] - Gzip, - #[error("the encoded length did not fit the expected length")] - LengthMismatch, - #[error("the expected magic header was missing or corrupted")] - MagicHeader, + #[error("could not find read signatures in .tar.gz file")] + FindDataStartAndLen(#[source] TarFindDataStartAndLenError), #[error("no matching key/signature pair found")] NoMatch(NoMatch), #[error("could not read input")] Read(#[source] std::io::Error), + #[error("could not find read signatures in .tar.gz file")] + ReadSignatures(#[source] TarReadSignaturesError), #[error("could not seek inside the input")] Seek(#[source] std::io::Error), - #[error("the input contained an illegal signature at index #{1}")] - Signature(#[source] SignatureError, usize), - #[error("too many signatures in input")] - TooManySignatures, } } @@ -48,106 +35,20 @@ pub fn verify_tar( where I: ?Sized + Read + Seek, { - let (prehashed_message, signatures) = read_tar(input)?; - let (key_idx, _) = - find_match(keys, &signatures, &prehashed_message, context).map_err(Error::NoMatch)?; - Ok(key_idx) -} - -fn read_tar( - input: &mut I, -) -> Result<(Prehash, Vec), VerifyTarError> { // seek to start of base64 encoded signatures - let (data_start, data_len) = find_data_start_and_len(input)?; + let (data_start, data_len) = + tar_find_data_start_and_len(input).map_err(Error::FindDataStartAndLen)?; // read base64 encoded signatures - let signatures = read_signatures(data_start, data_len, input)?; + let signatures = + tar_read_signatures(data_start, data_len, input).map_err(Error::ReadSignatures)?; // pre-hash file input.rewind().map_err(Error::Seek)?; let prehashed_message = Prehash::calculate(&mut input.take(data_start)).map_err(Error::Read)?; - Ok((prehashed_message, signatures)) -} - -fn find_data_start_and_len(input: &mut I) -> Result<(u64, usize), VerifyTarError> -where - I: ?Sized + Read + Seek, -{ - let mut tail = [0; u64::BITS as usize / 4 + GZIP_END.len()]; - let data_end = input - .seek(SeekFrom::End(-(tail.len() as i64))) - .map_err(Error::Seek)?; - - input.read_exact(&mut tail).map_err(Error::Read)?; - if tail[u64::BITS as usize / 4..] != *GZIP_END { - return Err(Error::Gzip.into()); - } - let Ok(gzip_start) = std::str::from_utf8(&tail[..16]) else { - return Err(Error::Gzip.into()); - }; - let Ok(gzip_start) = u64::from_str_radix(gzip_start, 16) else { - return Err(Error::Gzip.into()); - }; - let Some(data_start) = gzip_start.checked_add(10) else { - return Err(Error::Gzip.into()); - }; - let Some(data_len) = data_end.checked_sub(data_start) else { - return Err(Error::Gzip.into()); - }; - let Ok(data_len) = usize::try_from(data_len) else { - return Err(Error::Gzip.into()); - }; - if data_len > BUF_LIMIT { - return Err(Error::TooManySignatures.into()); - } - - Ok((gzip_start, data_len + GZIP_START.len())) -} - -fn read_signatures( - data_start: u64, - data_len: usize, - input: &mut I, -) -> Result, VerifyTarError> -where - I: ?Sized + Read + Seek, -{ - let _: u64 = input - .seek(SeekFrom::Start(data_start)) - .map_err(Error::Read)?; - - let mut data = vec![0; data_len]; - input.read_exact(&mut data).map_err(Error::Read)?; - - if data[..GZIP_START.len()] != *GZIP_START { - return Err(Error::Gzip.into()); - } - let Ok(data) = BASE64_STANDARD.decode(&data[GZIP_START.len()..]) else { - return Err(Error::Base64.into()); - }; - if data.len() < HEADER_SIZE { - return Err(Error::MagicHeader.into()); - } - if data[..MAGIC_HEADER.len()] != *MAGIC_HEADER { - return Err(Error::MagicHeader.into()); - } - - let signature_count = data[MAGIC_HEADER.len()..][..size_of::()] - .try_into() - .unwrap(); - let signature_count = SignatureCountLeInt::from_le_bytes(signature_count) as usize; - if signature_count == 0 { - return Err(Error::Empty.into()); - } - if data.len() != HEADER_SIZE + signature_count * SIGNATURE_LENGTH { - return Err(Error::LengthMismatch.into()); - } - - let signatures = data[HEADER_SIZE..] - .chunks_exact(SIGNATURE_LENGTH) - .enumerate() - .map(|(idx, bytes)| Signature::from_slice(bytes).map_err(|err| Error::Signature(err, idx))) - .collect::, _>>()?; - Ok(signatures) + // find match + let (key_idx, _) = + find_match(keys, &signatures, &prehashed_message, context).map_err(Error::NoMatch)?; + Ok(key_idx) } diff --git a/api/src/verify_unsign_tar.rs b/api/src/verify_unsign_tar.rs new file mode 100644 index 0000000..72c4ef2 --- /dev/null +++ b/api/src/verify_unsign_tar.rs @@ -0,0 +1,130 @@ +use std::io::{Read, Seek, SeekFrom}; +use std::mem::size_of; + +use base64::prelude::BASE64_STANDARD; +use base64::Engine; +use ed25519_dalek::{Signature, SignatureError, SIGNATURE_LENGTH}; + +use crate::constants::{ + SignatureCountLeInt, BUF_LIMIT, GZIP_END, GZIP_START, HEADER_SIZE, MAGIC_HEADER, +}; + +#[derive(Debug, thiserror::Error)] +pub(crate) enum TarFindDataStartAndLenError { + #[error("the expected last GZIP block was missing or corrupted")] + Gzip, + #[error("could not read input")] + Read(#[source] std::io::Error), + #[error("could not seek inside the input")] + Seek(#[source] std::io::Error), + #[error("too many signatures in input")] + TooManySignatures, +} + +pub(crate) fn tar_find_data_start_and_len( + input: &mut I, +) -> Result<(u64, usize), TarFindDataStartAndLenError> +where + I: ?Sized + Read + Seek, +{ + let mut tail = [0; u64::BITS as usize / 4 + GZIP_END.len()]; + let data_end = input + .seek(SeekFrom::End(-(tail.len() as i64))) + .map_err(TarFindDataStartAndLenError::Seek)?; + + input + .read_exact(&mut tail) + .map_err(TarFindDataStartAndLenError::Read)?; + if tail[u64::BITS as usize / 4..] != *GZIP_END { + return Err(TarFindDataStartAndLenError::Gzip); + } + let Ok(gzip_start) = std::str::from_utf8(&tail[..16]) else { + return Err(TarFindDataStartAndLenError::Gzip); + }; + let Ok(gzip_start) = u64::from_str_radix(gzip_start, 16) else { + return Err(TarFindDataStartAndLenError::Gzip); + }; + let Some(data_start) = gzip_start.checked_add(10) else { + return Err(TarFindDataStartAndLenError::Gzip); + }; + let Some(data_len) = data_end.checked_sub(data_start) else { + return Err(TarFindDataStartAndLenError::Gzip); + }; + let Ok(data_len) = usize::try_from(data_len) else { + return Err(TarFindDataStartAndLenError::Gzip); + }; + if data_len > BUF_LIMIT { + return Err(TarFindDataStartAndLenError::TooManySignatures); + } + + Ok((gzip_start, data_len + GZIP_START.len())) +} + +#[derive(Debug, thiserror::Error)] +pub(crate) enum TarReadSignaturesError { + #[error("the input contained invalid base64 encoded data")] + Base64, + #[error("the input contained no signatures")] + Empty, + #[error("the expected last GZIP block was missing or corrupted")] + Gzip, + #[error("the encoded length did not fit the expected length")] + LengthMismatch, + #[error("the expected magic header was missing or corrupted")] + MagicHeader, + #[error("could not read input")] + Read(#[source] std::io::Error), + #[error("the input contained an illegal signature at index #{1}")] + Signature(#[source] SignatureError, usize), +} + +pub(crate) fn tar_read_signatures( + data_start: u64, + data_len: usize, + input: &mut I, +) -> Result, TarReadSignaturesError> +where + I: ?Sized + Read + Seek, +{ + let _: u64 = input + .seek(SeekFrom::Start(data_start)) + .map_err(TarReadSignaturesError::Read)?; + + let mut data = vec![0; data_len]; + input + .read_exact(&mut data) + .map_err(TarReadSignaturesError::Read)?; + + if data[..GZIP_START.len()] != *GZIP_START { + return Err(TarReadSignaturesError::Gzip); + } + let Ok(data) = BASE64_STANDARD.decode(&data[GZIP_START.len()..]) else { + return Err(TarReadSignaturesError::Base64); + }; + if data.len() < HEADER_SIZE { + return Err(TarReadSignaturesError::MagicHeader); + } + if data[..MAGIC_HEADER.len()] != *MAGIC_HEADER { + return Err(TarReadSignaturesError::MagicHeader); + } + + let signature_count = data[MAGIC_HEADER.len()..][..size_of::()] + .try_into() + .unwrap(); + let signature_count = SignatureCountLeInt::from_le_bytes(signature_count) as usize; + if signature_count == 0 { + return Err(TarReadSignaturesError::Empty); + } + if data.len() != HEADER_SIZE + signature_count * SIGNATURE_LENGTH { + return Err(TarReadSignaturesError::LengthMismatch); + } + + let signatures = data[HEADER_SIZE..] + .chunks_exact(SIGNATURE_LENGTH) + .enumerate() + .map(|(idx, bytes)| { + Signature::from_slice(bytes).map_err(|err| TarReadSignaturesError::Signature(err, idx)) + }) + .collect::, _>>()?; + Ok(signatures) +} diff --git a/src/main.rs b/src/main.rs index 89ff4b3..92da6d3 100644 --- a/src/main.rs +++ b/src/main.rs @@ -10,7 +10,7 @@ #![warn(missing_docs)] #![warn(non_ascii_idents)] #![warn(noop_method_call)] -#![warn(rust_2018_idioms)] +#![warn(rust_2021_idioms)] #![warn(single_use_lifetimes)] #![warn(trivial_casts)] #![warn(unreachable_pub)] @@ -22,6 +22,7 @@ mod generate; mod sign; +mod unsign; mod verify; use std::path::Path; @@ -42,6 +43,7 @@ enum CliSubcommand { GenKey(generate::Cli), Verify(verify::Cli), Sign(sign::Cli), + Unsign(unsign::Cli), } #[derive(pretty_error_debug::Debug, thiserror::Error)] @@ -52,6 +54,8 @@ enum MainError { Verify(#[from] verify::Error), #[error("could not sign file")] Sign(#[from] sign::Error), + #[error("could not remove sign from file")] + Unsign(#[from] unsign::Error), } fn main() -> Result<(), MainError> { @@ -60,6 +64,7 @@ fn main() -> Result<(), MainError> { CliSubcommand::GenKey(args) => generate::main(args)?, CliSubcommand::Verify(args) => verify::main(args)?, CliSubcommand::Sign(args) => sign::main(args)?, + CliSubcommand::Unsign(args) => unsign::main(args)?, } Ok(()) } diff --git a/src/sign.rs b/src/sign.rs index 038b57e..f0af398 100644 --- a/src/sign.rs +++ b/src/sign.rs @@ -4,6 +4,7 @@ use std::path::{Path, PathBuf}; use clap::{Args, Parser, Subcommand}; use normalize_path::NormalizePath; +use tempfile::tempdir_in; use zipsign_api::sign::{ copy_and_sign_tar, copy_and_sign_zip, gather_signature_data, read_signing_keys, GatherSignatureDataError, ReadSigningKeysError, SignTarError, SignZipError, @@ -105,10 +106,11 @@ pub(crate) fn main(args: Cli) -> Result<(), Error> { return Err(Error::Exists); } let output_dir = output_path.parent().unwrap_or(Path::new(".")); + let tempdir = tempdir_in(output_dir).map_err(Error::Tempfile)?; let mut output_file = tempfile::Builder::new() .prefix(".zipsign.") .suffix(".tmp") - .tempfile_in(output_dir) + .tempfile_in(&tempdir) .map_err(Error::Tempfile)?; let mut input = File::open(&args.input).map_err(Error::InputOpen)?; diff --git a/src/unsign.rs b/src/unsign.rs new file mode 100644 index 0000000..8676ff4 --- /dev/null +++ b/src/unsign.rs @@ -0,0 +1,96 @@ +use std::fs::{rename, File}; +use std::path::{Path, PathBuf}; + +use clap::{Args, Parser, Subcommand}; +use normalize_path::NormalizePath; +use tempfile::tempdir_in; +use zipsign_api::unsign::{ + copy_and_unsign_tar, copy_and_unsign_zip, UnsignTarError, UnsignZipError, +}; + +/// Generate signature for a file +#[derive(Debug, Parser, Clone)] +pub(crate) struct Cli { + #[command(subcommand)] + subcommand: CliKind, +} + +impl CliKind { + fn split(self) -> (ArchiveKind, CommonArgs) { + match self { + CliKind::Zip(common) => (ArchiveKind::Zip, common), + CliKind::Tar(common) => (ArchiveKind::Tar, common), + } + } +} + +#[derive(Debug, Subcommand, Clone)] +enum CliKind { + /// `` is a .zip file. + /// Its data is copied and the signatures are stored next to the data. + Zip(#[command(flatten)] CommonArgs), + /// `` is a gzipped .tar file. + /// Its data is copied and the signatures are stored next to the data. + Tar(#[command(flatten)] CommonArgs), +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +enum ArchiveKind { + Zip, + Tar, +} + +#[derive(Debug, Args, Clone)] +struct CommonArgs { + /// Input file to sign + input: PathBuf, + /// Signed file to generate (if omitted, the input is overwritten) + #[arg(long, short = 'o')] + output: Option, + /// Overwrite output file if it exists + #[arg(long, short = 'f')] + force: bool, +} + +#[derive(Debug, thiserror::Error)] +pub(crate) enum Error { + #[error("output exists, use `--force` allow replacing a file")] + Exists, + #[error("could not open input file")] + InputOpen(#[source] std::io::Error), + #[error("could not rename output file")] + OutputRename(#[source] std::io::Error), + #[error(transparent)] + Tar(#[from] UnsignTarError), + #[error("could not create temporary file in output directory")] + Tempfile(#[source] std::io::Error), + #[error(transparent)] + Zip(#[from] UnsignZipError), +} + +pub(crate) fn main(args: Cli) -> Result<(), Error> { + let (kind, args) = args.subcommand.split(); + + let output_path = args.output.as_deref().unwrap_or(&args.input).normalize(); + if args.output.is_some() && !args.force { + return Err(Error::Exists); + } + let output_dir = output_path.parent().unwrap_or(Path::new(".")); + let tempdir = tempdir_in(output_dir).map_err(Error::Tempfile)?; + let mut output_file = tempfile::Builder::new() + .prefix(".zipsign.") + .suffix(".tmp") + .tempfile_in(&tempdir) + .map_err(Error::Tempfile)?; + + let mut input = File::open(&args.input).map_err(Error::InputOpen)?; + match kind { + ArchiveKind::Zip => copy_and_unsign_zip(&mut input, &mut output_file)?, + ArchiveKind::Tar => copy_and_unsign_tar(&mut input, &mut output_file)?, + } + // drop input so it can be overwritten if input=output + drop(input); + + rename(output_file.into_temp_path(), output_path).map_err(Error::OutputRename)?; + Ok(()) +}