From 0f77cae415800e2450dd2a7db44ea1e82561436f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Kijewski?= Date: Tue, 12 Sep 2023 04:14:39 +0200 Subject: [PATCH] Sign entire file, support multiple keys --- Cargo.lock | 353 ++---------------------------------------------- Cargo.toml | 12 +- README.md | 58 +++++--- src/generate.rs | 14 ++ src/main.rs | 18 +-- src/sign.rs | 185 ++++++++++++++++++++----- src/tar.rs | 285 -------------------------------------- src/verify.rs | 185 ++++++++++++++++++------- src/zip.rs | 155 --------------------- 9 files changed, 358 insertions(+), 907 deletions(-) delete mode 100644 src/tar.rs delete mode 100644 src/zip.rs diff --git a/Cargo.lock b/Cargo.lock index d8d2e73..a05db26 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,23 +2,6 @@ # It is not intended for manual editing. version = 3 -[[package]] -name = "adler" -version = "1.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" - -[[package]] -name = "aes" -version = "0.8.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ac1f845298e95f983ff1944b728ae08b8cebab80d684f0a832ed0fc74dfa27e2" -dependencies = [ - "cfg-if", - "cipher", - "cpufeatures", -] - [[package]] name = "anstream" version = "0.5.0" @@ -35,9 +18,9 @@ dependencies = [ [[package]] name = "anstyle" -version = "1.0.2" +version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "15c4c2c83f81532e5845a733998b6971faca23490340a418e9b72a3ec9de12ea" +checksum = "b84bf0a05bbb2a83e5eb6fa36bb6e87baa08193c35ff52bbf6b38d8af2890e46" [[package]] name = "anstyle-parse" @@ -67,12 +50,6 @@ dependencies = [ "windows-sys", ] -[[package]] -name = "autocfg" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" - [[package]] name = "base64ct" version = "1.6.0" @@ -94,53 +71,12 @@ version = "1.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610" -[[package]] -name = "bzip2" -version = "0.4.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bdb116a6ef3f6c3698828873ad02c3014b3c85cadb88496095628e3ef1e347f8" -dependencies = [ - "bzip2-sys", - "libc", -] - -[[package]] -name = "bzip2-sys" -version = "0.1.11+1.0.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "736a955f3fa7875102d57c82b8cac37ec45224a07fd32d58f9f7a186b6cd4cdc" -dependencies = [ - "cc", - "libc", - "pkg-config", -] - -[[package]] -name = "cc" -version = "1.0.83" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1174fb0b6ec23863f8b971027804a42614e347eafb0a95bf0b12cdae21fc4d0" -dependencies = [ - "jobserver", - "libc", -] - [[package]] name = "cfg-if" version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" -[[package]] -name = "cipher" -version = "0.4.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" -dependencies = [ - "crypto-common", - "inout", -] - [[package]] name = "clap" version = "4.4.2" @@ -193,12 +129,6 @@ version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "28c122c3980598d243d63d9a704629a2d748d101f278052ff068be5a4423ab6f" -[[package]] -name = "constant_time_eq" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "245097e9a4535ee1e3e3931fcfcd55a796a44c643e8596ff6566d68f09b87bbc" - [[package]] name = "cpufeatures" version = "0.2.9" @@ -274,12 +204,6 @@ dependencies = [ "zeroize", ] -[[package]] -name = "deranged" -version = "0.3.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2696e8a945f658fd14dc3b87242e6b80cd0f36ff04ea560fa39082368847946" - [[package]] name = "digest" version = "0.10.7" @@ -288,7 +212,6 @@ checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" dependencies = [ "block-buffer", "crypto-common", - "subtle", ] [[package]] @@ -312,6 +235,7 @@ dependencies = [ "rand_core", "serde", "sha2", + "signature", "zeroize", ] @@ -327,16 +251,6 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d0870c84016d4b481be5c9f323c24f65e31e901ae618f0e80f4308fb00de1d2d" -[[package]] -name = "flate2" -version = "1.0.27" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c6c98ee8095e9d1dcbf2fcc6d95acccb90d1c81db1e44725c6a984b1dbdfb010" -dependencies = [ - "crc32fast", - "miniz_oxide", -] - [[package]] name = "generic-array" version = "0.14.7" @@ -370,15 +284,6 @@ version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" -[[package]] -name = "hmac" -version = "0.12.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" -dependencies = [ - "digest", -] - [[package]] name = "indexmap" version = "2.0.0" @@ -389,121 +294,24 @@ dependencies = [ "hashbrown", ] -[[package]] -name = "inout" -version = "0.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a0c10553d664a4d0bcff9f4215d0aac67a639cc68ef660840afe309b807bc9f5" -dependencies = [ - "generic-array", -] - -[[package]] -name = "is_executable" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa9acdc6d67b75e626ad644734e8bc6df893d9cd2a834129065d3dd6158ea9c8" -dependencies = [ - "winapi", -] - -[[package]] -name = "jobserver" -version = "0.1.26" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "936cfd212a0155903bcbc060e316fb6cc7cbf2e1907329391ebadc1fe0ce77c2" -dependencies = [ - "libc", -] - [[package]] name = "libc" version = "0.2.147" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b4668fb0ea861c1df094127ac5f1da3409a82116a4ba74fca2e58ef927159bb3" -[[package]] -name = "lzma-sys" -version = "0.1.20" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5fda04ab3764e6cde78b9974eec4f779acaba7c4e84b36eca3cf77c581b85d27" -dependencies = [ - "cc", - "libc", - "pkg-config", -] - [[package]] name = "memchr" version = "2.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f232d6ef707e1956a43342693d2a31e72989554d58299d7a88738cc95b0d35c" -[[package]] -name = "memmap2" -version = "0.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f49388d20533534cd19360ad3d6a7dadc885944aa802ba3995040c5ec11288c6" -dependencies = [ - "libc", -] - -[[package]] -name = "miniz_oxide" -version = "0.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e7810e0be55b428ada41041c41f32c9f1a42817901b4ccf45fa3d4b6561e74c7" -dependencies = [ - "adler", -] - -[[package]] -name = "num-traits" -version = "0.2.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f30b0abd723be7e2ffca1272140fac1a2f084c77ec3e123c192b66af1ee9e6c2" -dependencies = [ - "autocfg", -] - [[package]] name = "once_cell" version = "1.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dd8b5dd2ae5ed71462c540258bedcb51965123ad7e7ccf4b9a8cafaa4a63576d" -[[package]] -name = "parse_int" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2d695b79916a2c08bcff7be7647ab60d1402885265005a6658ffe6d763553c5a" -dependencies = [ - "num-traits", -] - -[[package]] -name = "password-hash" -version = "0.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7676374caaee8a325c9e7a2ae557f216c5563a171d6997b0ef8a65af35147700" -dependencies = [ - "base64ct", - "rand_core", - "subtle", -] - -[[package]] -name = "pbkdf2" -version = "0.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "83a0692ec44e4cf1ef28ca317f14f8f07da2d95ec3fa01f86e4467b725e60917" -dependencies = [ - "digest", - "hmac", - "password-hash", - "sha2", -] - [[package]] name = "pkcs8" version = "0.10.2" @@ -514,12 +322,6 @@ dependencies = [ "spki", ] -[[package]] -name = "pkg-config" -version = "0.3.27" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26072860ba924cbfa98ea39c8c19b4dd6a4a25423dbdf219c1eca91aa0cf6964" - [[package]] name = "platforms" version = "3.1.2" @@ -592,12 +394,6 @@ dependencies = [ "semver", ] -[[package]] -name = "rustversion" -version = "1.0.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ffc183a10b4478d04cbbbfc96d0873219d962dd5accaff2ffbd4ceb7df837f4" - [[package]] name = "semver" version = "1.0.18" @@ -624,17 +420,6 @@ dependencies = [ "syn", ] -[[package]] -name = "sha1" -version = "0.10.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f04293dc80c3993519f2d7f6f511707ee7094fe0c6d3406feb330cdb3540eba3" -dependencies = [ - "cfg-if", - "cpufeatures", - "digest", -] - [[package]] name = "sha2" version = "0.10.7" @@ -648,9 +433,12 @@ dependencies = [ [[package]] name = "signature" -version = "2.1.0" +version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e1788eed21689f9cf370582dfc467ef36ed9c707f073528ddafa8d83e3b8500" +checksum = "8fe458c98333f9c8152221191a77e2a44e8325d0193484af2e9421a53019e57d" +dependencies = [ + "digest", +] [[package]] name = "spki" @@ -668,28 +456,6 @@ version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" -[[package]] -name = "strum" -version = "0.25.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "290d54ea6f91c969195bdbcd7442c8c2a2ba87da8bf60a7ee86a235d4bc1e125" -dependencies = [ - "strum_macros", -] - -[[package]] -name = "strum_macros" -version = "0.25.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ad8d03b598d3d0fff69bf533ee3ef19b8eeb342729596df84bcc7e1f96ec4059" -dependencies = [ - "heck", - "proc-macro2", - "quote", - "rustversion", - "syn", -] - [[package]] name = "subtle" version = "2.5.0" @@ -698,9 +464,9 @@ checksum = "81cdd64d312baedb58e21336b31bc043b77e01cc99033ce76ef539f78e965ebc" [[package]] name = "syn" -version = "2.0.31" +version = "2.0.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "718fa2415bcb8d8bd775917a1bf12a7931b6dfa890753378538118181e0cb398" +checksum = "239814284fd6f1a4ffe4ca893952cdd93c224b6a1571c9a9eadd670295c0c9e2" dependencies = [ "proc-macro2", "quote", @@ -727,23 +493,6 @@ dependencies = [ "syn", ] -[[package]] -name = "time" -version = "0.3.28" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "17f6bb557fd245c28e6411aa56b6403c689ad95061f50e4be16c274e70a17e48" -dependencies = [ - "deranged", - "serde", - "time-core", -] - -[[package]] -name = "time-core" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7300fbefb4dadc1af235a9cef3737cea692a9d97e1b9cbcd4ebdae6f8868e6fb" - [[package]] name = "toml_datetime" version = "0.6.3" @@ -752,9 +501,9 @@ checksum = "7cda73e2f1397b1262d6dfdcef8aafae14d1de7748d66822d3bfeeb6d03e5e4b" [[package]] name = "toml_edit" -version = "0.19.14" +version = "0.19.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8123f27e969974a3dfba720fdb560be359f57b44302d280ba72e76a74480e8a" +checksum = "1b5bb770da30e5cbfde35a2d7b9b8a2c4b8ef89548a7a6aeab5c9a576e3e7421" dependencies = [ "indexmap", "toml_datetime", @@ -791,28 +540,6 @@ version = "0.11.0+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" -[[package]] -name = "winapi" -version = "0.3.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" -dependencies = [ - "winapi-i686-pc-windows-gnu", - "winapi-x86_64-pc-windows-gnu", -] - -[[package]] -name = "winapi-i686-pc-windows-gnu" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" - -[[package]] -name = "winapi-x86_64-pc-windows-gnu" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" - [[package]] name = "windows-sys" version = "0.48.0" @@ -888,15 +615,6 @@ dependencies = [ "memchr", ] -[[package]] -name = "xz2" -version = "0.1.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "388c44dc09d76f1536602ead6d325eb532f5c122f17782bd57fb47baeeb767e2" -dependencies = [ - "lzma-sys", -] - [[package]] name = "zeroize" version = "1.6.0" @@ -909,66 +627,19 @@ version = "0.6.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "760394e246e4c28189f19d488c058bf16f564016aefac5d32bb1f3b51d5e9261" dependencies = [ - "aes", "byteorder", - "bzip2", - "constant_time_eq", "crc32fast", "crossbeam-utils", - "flate2", - "hmac", - "pbkdf2", - "sha1", - "time", - "zstd", ] [[package]] name = "zipsign" version = "0.1.0" dependencies = [ - "bzip2", "clap", "ed25519-dalek", - "flate2", - "is_executable", - "memmap2", - "num-traits", - "parse_int", "pretty-error-debug", "rand_core", - "strum", "thiserror", - "xz2", "zip", ] - -[[package]] -name = "zstd" -version = "0.11.2+zstd.1.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "20cc960326ece64f010d2d2107537f26dc589a6573a316bd5b1dba685fa5fde4" -dependencies = [ - "zstd-safe", -] - -[[package]] -name = "zstd-safe" -version = "5.0.2+zstd.1.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d2a5585e04f9eea4b2a3d1eca508c4dee9592a89ef6f450c11719da0726f4db" -dependencies = [ - "libc", - "zstd-sys", -] - -[[package]] -name = "zstd-sys" -version = "2.0.8+zstd.1.5.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5556e6ee25d32df2586c098bbfa278803692a20d0ab9565e049480d52707ec8c" -dependencies = [ - "cc", - "libc", - "pkg-config", -] diff --git a/Cargo.toml b/Cargo.toml index 7d74cc2..65f357a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -7,17 +7,9 @@ authors = ["René Kijewski "] license = "MIT OR Apache-2.0 WITH LLVM-exception" [dependencies] -bzip2 = "0.4.4" clap = { version = "4.4.2", features = ["derive"] } -ed25519-dalek = { version = "2.0.0", features = ["rand_core"] } -flate2 = "1.0.27" -is_executable = "1.0.1" -memmap2 = "0.7.1" -num-traits = "0.2.16" -parse_int = { version = "0.6.0", features = ["implicit-octal"] } +ed25519-dalek = { version = "2.0.0", features = ["digest", "rand_core", "zeroize"] } pretty-error-debug = "0.3.0" rand_core = { version = "0.6.4", features = ["getrandom"] } -strum = { version = "0.25.0", features = ["derive"] } thiserror = "1.0.48" -xz2 = { version = "0.1.7", features = ["static"] } -zip = { version = "0.6.6", features = ["bzip2", "deflate", "zstd"] } +zip = { version = "0.6.6", default-features = false } diff --git a/README.md b/README.md index e7deae3..2790ac3 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -## zipsign: Sign a file with an ed25519 signing key +## zipsign: Sign a file with an ed25519ph signing key ### Install @@ -6,15 +6,30 @@ cargo install --git https://github.com/Kijewski/zipsign ``` -### Verify a signature +### Example -Usage: `zipsign verify ` +```sh +# Generate key pair: +$ zipsign gen-key priv.key pub.key -Arguments: +# ZIP a file and list the content of the ZIP file: +$ 7z a Cargo.lock.zip Cargo.lock +$ 7z l Cargo.lock.zip +Cargo.lock -* `VERIFYING_KEY`: Verifying key -* `FILE`: Signed file -* `SIGNATURE`: Signature file or .zip file generated with "zip" command +# Sign the ZIP file: +$ zipsign sign -k priv.key -i Cargo.lock.zip -o Cargo.lock.tmp -c Cargo.lock --zip +$ mv Cargo.lock.tmp Cargo.lock.zip +$ 7z l Cargo.lock.zip +Cargo.lock + +# Verify that the ZIP that the generated signature is valid: +$ zipsign verify -k pub.key -i Cargo.lock.zip -c Cargo.lock +OK + +# Verify the signed ZIP file: +$ zipsign verify -k pub.key -i Cargo.lock.zip -c Cargo.lock +``` ### Generate key @@ -25,28 +40,29 @@ Arguments: * `PRIVATE_KEY`: Private key file to create * `VERIFYING_KEY`: Verifying key (public key) file to create -### Zip a file and store its signature in the .zip +### Generate signatures -Usage: `zipsign zip [OPTIONS] ` +Usage: `zipsign sign -k -i -o [OPTIONS]` Arguments: -* `PRIVATE_KEY`: Private key -* `FILE`: File to sign -* `ZIP`: ZIP file to (over)write - Options: -* `--method `: Compression method (stored | \*deflated | bzip2 | zstd, \*=default) -* `--level `: Compression level -* `--permissions `: Unix-style permissions, default: 0o755 if "FILE" is executable, otherwise 0o644 +* `-i`, `--input `: File to verify +* `-o`, `--signature `: Signature to (over)write +* `-k`, `--private-key …`: One or more files containing private keys +* `-c`, `--context `: Context (an arbitrary string used to salt the input, e.g. the basename of ``) +* `-z`, `--zip`: `` is a ZIP file. Copy its data into the output +* `-e`, `--end-of-file`: Signatures at end of file (.tar files) -### Generate signature in new file +### Verify a signature -Usage: `zipsign sign ` +Usage: `zipsign verify -k -i [-o ] [OPTIONS]` Arguments: -* `PRIVATE_KEY`: Private key -* `FILE`: File to sign -* `SIGNATURE`: Signature to (over)write +* `-i`, `--input `: File to verify +* `-o`, `--signature `: Signature file. If absent the signature it is read from `` +* `-k`, `--verifying-key …`: One or more files containing verifying keys +* `-z`, `--zip`: `` is a ZIP file. Copy its data into the output +* `-e`, `--end-of-file`: Signatures at end of file (.tar files) diff --git a/src/generate.rs b/src/generate.rs index 23ab030..94d8a07 100644 --- a/src/generate.rs +++ b/src/generate.rs @@ -1,11 +1,23 @@ use std::fs::OpenOptions; use std::io::Write; +#[cfg(unix)] +use std::os::unix::prelude::OpenOptionsExt; use std::path::PathBuf; use clap::Parser; use ed25519_dalek::SigningKey; use rand_core::OsRng; +trait NotUnixOpenOptionsExt { + #[inline(always)] + fn mode(&mut self, _mode: u32) -> &mut Self { + self + } +} + +#[cfg(not(unix))] +impl NotUnixOpenOptionsExt for OpenOptions {} + pub fn main(args: Cli) -> Result<(), Error> { let key: SigningKey = SigningKey::generate(&mut OsRng); @@ -13,6 +25,7 @@ pub fn main(args: Cli) -> Result<(), Error> { .write(true) .create(true) .truncate(true) + .mode(0o400) .open(&args.private_key); let mut f = match result { Ok(f) => f, @@ -26,6 +39,7 @@ pub fn main(args: Cli) -> Result<(), Error> { .write(true) .create(true) .truncate(true) + .mode(0o444) .open(&args.verifying_key); let mut f = match result { Ok(f) => f, diff --git a/src/main.rs b/src/main.rs index 04ed52e..aeb4c4b 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,19 +1,22 @@ mod generate; mod sign; -mod tar; mod verify; -mod zip; use clap::{Parser, Subcommand}; +// "\x0c\x04\x01" -- form feed, end of text, start of header +// "ed25519ph" -- used algorithm +// "\x00\x00" -- version number in network byte order +const MAGIC_HEADER: &[u8; 14] = b"\x0c\x04\x01ed25519ph\x00\x00"; +const HEADER_SIZE: usize = 16; +type SignatureCountLeInt = u16; + fn main() -> Result<(), MainError> { let args = Cli::parse(); match args.subcommand { CliSubcommand::GenKey(args) => generate::main(args)?, CliSubcommand::Verify(args) => verify::main(args)?, CliSubcommand::Sign(args) => sign::main(args)?, - CliSubcommand::Tar(args) => tar::main(args)?, - CliSubcommand::Zip(args) => zip::main(args)?, } Ok(()) } @@ -32,9 +35,6 @@ enum CliSubcommand { GenKey(generate::Cli), Verify(verify::Cli), Sign(sign::Cli), - Tar(tar::Cli), - Zip(zip::Cli), - // TODO: verify .tar file } #[derive(pretty_error_debug::Debug, thiserror::Error)] @@ -45,8 +45,4 @@ enum MainError { Verify(#[from] verify::Error), #[error("could not sign file")] Sign(#[from] sign::Error), - #[error("could not create signed .tar file")] - Tar(#[from] tar::Error), - #[error("could not create signed .zip file")] - Zip(#[from] zip::Error), } diff --git a/src/sign.rs b/src/sign.rs index 36ba58d..454dc17 100644 --- a/src/sign.rs +++ b/src/sign.rs @@ -1,60 +1,163 @@ use std::fs::OpenOptions; -use std::io::{Read, Write}; +use std::io::{copy, BufReader, BufWriter, Read, Seek, SeekFrom, Write}; use std::path::PathBuf; use clap::Parser; -use ed25519_dalek::{SignatureError, Signer, SigningKey}; -use memmap2::Mmap; +use ed25519_dalek::{Digest, Sha512, SignatureError, SigningKey, KEYPAIR_LENGTH, SIGNATURE_LENGTH}; +use zip::result::ZipError; +use zip::{ZipArchive, ZipWriter}; + +use crate::{SignatureCountLeInt, HEADER_SIZE, MAGIC_HEADER}; pub fn main(args: Cli) -> Result<(), Error> { - // read signing key - let mut key = [0; 64]; - let mut f = match OpenOptions::new().read(true).open(&args.private_key) { - Ok(f) => f, - Err(err) => return Err(Error::OpenRead(err, args.private_key)), - }; - if let Err(err) = f.read_exact(&mut key) { - return Err(Error::Read(err, args.private_key)); + if args.private_key.len() > SignatureCountLeInt::MAX as usize { + return Err(Error::TooManyKeys); } - let key = SigningKey::from_keypair_bytes(&key) - .map_err(|err| Error::KeyValidate(err, args.private_key))?; - drop(f); + let signature_bytes = SIGNATURE_LENGTH * args.private_key.len() + HEADER_SIZE; + let context = args.context.map(String::into_bytes); - // map "file" - let f = match OpenOptions::new().read(true).open(&args.file) { + // read signing keys + let mut keys = args + .private_key + .into_iter() + .map(|key_file| { + let mut key = [0; KEYPAIR_LENGTH]; + let mut f = match OpenOptions::new().read(true).open(&key_file) { + Ok(f) => f, + Err(err) => return Err(Error::OpenRead(err, key_file)), + }; + if let Err(err) = f.read_exact(&mut key) { + return Err(Error::Read(err, key_file)); + } + SigningKey::from_keypair_bytes(&key).map_err(|err| Error::KeyInvalid(err, key_file)) + }) + .collect::, _>>()?; + keys.sort_by(|l, r| { + l.verifying_key() + .as_bytes() + .cmp(r.verifying_key().as_bytes()) + }); + + // open input file + let mut input = match OpenOptions::new().read(true).open(&args.input) { Ok(f) => f, - Err(err) => return Err(Error::OpenRead(err, args.file)), - }; - let file = match unsafe { Mmap::map(&f) } { - Ok(file) => file, - Err(err) => return Err(Error::Mmap(err, args.file)), + Err(err) => return Err(Error::OpenRead(err, args.input)), }; - drop(f); - // write signature - let signature = key.try_sign(&file).map_err(Error::FileSign)?; - let result = OpenOptions::new() + // open output file + // TODO: FIXME: O_TMPFILE? + // TODO: FIXME: tempdir? + let mut output = match OpenOptions::new() + .read(true) .write(true) .create(true) .truncate(true) - .open(&args.signature); - let mut f = match result { + .open(&args.signature) + { Ok(f) => f, Err(err) => return Err(Error::OpenWrite(err, args.signature)), }; - f.write_all(&signature.to_bytes()) - .map_err(|err| Error::Write(err, args.signature)) + + // Copy ZIP file. + // The file headers inside a ZIP file contain references to the absolute position in the file, + // so the checksum of the copy will be different from its original. + if args.zip { + let signature_bytes = signature_bytes.try_into().unwrap(); + if let Err(err) = output.set_len(signature_bytes) { + return Err(Error::Write(err, args.signature)); + } + if let Err(err) = output.seek(SeekFrom::Start(signature_bytes)) { + return Err(Error::Seek(err, args.signature)); + } + + let mut input = match ZipArchive::new(BufReader::new(&mut input)) { + Ok(input) => input, + Err(err) => return Err(Error::Zip(err, args.input)), + }; + + let mut output = ZipWriter::new(BufWriter::new(&mut output)); + output.set_raw_comment(input.comment().to_owned()); + for idx in 0..input.len() { + let file = match input.by_index_raw(idx) { + Ok(entry) => entry, + Err(err) => return Err(Error::ZipRead(err, args.input, idx)), + }; + if let Err(err) = output.raw_copy_file(file) { + return Err(Error::ZipWrite(err, args.signature, idx)); + } + } + if let Err(err) = output.finish() { + return Err(Error::ZipFinish(err, args.signature)); + } + } + + // the header is hashed, too, to the set of keys cannot be easily changed after the fact + let mut header = [0; HEADER_SIZE]; + header[..MAGIC_HEADER.len()].copy_from_slice(MAGIC_HEADER); + header[MAGIC_HEADER.len()..] + .copy_from_slice(&(keys.len() as SignatureCountLeInt).to_le_bytes()); + + // pre-hash input + let mut prehashed_message = Sha512::new(); + if args.zip { + let signature_bytes = signature_bytes.try_into().unwrap(); + if let Err(err) = output.seek(SeekFrom::Start(signature_bytes)) { + return Err(Error::Seek(err, args.input)); + } + if let Err(err) = copy(&mut output, &mut prehashed_message) { + return Err(Error::Read(err, args.signature)); + } + } else if let Err(err) = copy(&mut input, &mut prehashed_message) { + return Err(Error::Read(err, args.input)); + } + prehashed_message.update(header); + + // write signatures + let mut signatures_buf = Vec::with_capacity(signature_bytes); + if !args.end_of_file { + signatures_buf.extend(header); + } + for key in keys { + let signature = key + .sign_prehashed(prehashed_message.clone(), context.as_deref()) + .map_err(Error::FileSign)?; + signatures_buf.extend(signature.to_bytes()); + } + if args.end_of_file { + signatures_buf.extend(header); + } + if args.zip { + if let Err(err) = output.seek(SeekFrom::Start(0)) { + return Err(Error::Seek(err, args.signature)); + } + } + if let Err(err) = output.write_all(&signatures_buf) { + return Err(Error::Write(err, args.signature)); + } + Ok(()) } /// Generate signature for a file #[derive(Debug, Parser)] pub struct Cli { - /// Private key - private_key: PathBuf, - /// File to sign - file: PathBuf, + /// File to verify + #[arg(long, short = 'i')] + input: PathBuf, /// Signature to (over)write + #[arg(long, short = 'o')] signature: PathBuf, + /// One or more files containing private keys + #[arg(long, short = 'k', num_args = 1..)] + private_key: Vec, + /// Context (an arbitrary string used to salt the input, e.g. the basename of ``) + #[arg(long, short = 'c')] + context: Option, + /// `` is a ZIP file. Copy its data into the output. + #[arg(long, short = 'z')] + zip: bool, + /// Signatures at end of file (.tar files) + #[arg(long, short = 'e')] + end_of_file: bool, } #[derive(Debug, thiserror::Error)] @@ -67,10 +170,20 @@ pub enum Error { Read(#[source] std::io::Error, PathBuf), #[error("could not write to {1:?}")] Write(#[source] std::io::Error, PathBuf), - #[error("could not mmap {1:?} for reading")] - Mmap(#[source] std::io::Error, PathBuf), + #[error("could not not seek in file {1:?}")] + Seek(#[source] std::io::Error, PathBuf), #[error("private key {1:?} was invalid")] - KeyValidate(#[source] SignatureError, PathBuf), + KeyInvalid(#[source] SignatureError, PathBuf), #[error("could not sign file")] FileSign(#[source] SignatureError), + #[error("could not read ZIP file {1:?}")] + Zip(#[source] ZipError, PathBuf), + #[error("could not read entry #{2:?} of ZIP file {1:?}")] + ZipRead(#[source] ZipError, PathBuf, usize), + #[error("could not write entry #{2:?} input output file {1:?}")] + ZipWrite(#[source] ZipError, PathBuf, usize), + #[error("could not finalize output file {1:?}")] + ZipFinish(ZipError, PathBuf), + #[error("cannot have more than 65535 keys")] + TooManyKeys, } diff --git a/src/tar.rs b/src/tar.rs deleted file mode 100644 index 3f9db2b..0000000 --- a/src/tar.rs +++ /dev/null @@ -1,285 +0,0 @@ -use std::fs::{File, OpenOptions}; -use std::io::{BufWriter, Read, Write}; -use std::path::PathBuf; -use std::str::FromStr; - -use bzip2::write::BzEncoder; -use clap::Parser; -use ed25519_dalek::{Signature, SignatureError, Signer, SigningKey}; -use flate2::write::GzEncoder; -use memmap2::Mmap; -use xz2::write::XzEncoder; - -pub fn main(args: Cli) -> Result<(), Error> { - let name = args - .file - .file_name() - .ok_or(Error::NoFileName)? - .to_str() - .ok_or(Error::NoFileName)?; - if name.len() >= 100 { - return Err(Error::NoFileName); - } - - let level = match args.level.unwrap_or(9) { - level @ 0..=9 => level as u32, - level => return Err(Error::CompressionLevel(level)), - }; - - // read signing key - let mut key = [0; 64]; - let mut f = match OpenOptions::new().read(true).open(&args.private_key) { - Ok(f) => f, - Err(err) => return Err(Error::OpenRead(err, args.private_key)), - }; - if let Err(err) = f.read_exact(&mut key) { - return Err(Error::Read(err, args.private_key)); - } - let key = SigningKey::from_keypair_bytes(&key) - .map_err(|err| Error::KeyValidate(err, args.private_key))?; - drop(f); - - // map "file" - let f = match OpenOptions::new().read(true).open(&args.file) { - Ok(f) => f, - Err(err) => return Err(Error::OpenRead(err, args.file)), - }; - let src = match unsafe { Mmap::map(&f) } { - Ok(src) => src, - Err(err) => return Err(Error::Mmap(err, args.file)), - }; - drop(f); - let signature = key.try_sign(&src).map_err(Error::FileSign)?; - - // get permissions - let permissions = match args.permissions { - Some(permissions) => permissions.0 as u32, - None => match is_executable::is_executable(&args.file) { - true => 0o755, - false => 0o644, - }, - }; - - // write .tar file - let dest: Result = OpenOptions::new() - .write(true) - .create(true) - .truncate(true) - .open(&args.tar); - let dest = match dest { - Ok(dest) => dest, - Err(err) => return Err(Error::OpenWrite(err, args.tar)), - }; - let dest = match args.method.unwrap_or_default() { - CompressionMethod::Uncompressed => CompressionStream::Uncompressed(BufWriter::new(dest)), - CompressionMethod::Bzip2 => { - let level = bzip2::Compression::new(level); - CompressionStream::Bzip2(BzEncoder::new(dest, level)) - }, - CompressionMethod::Gzip => { - let level = flate2::Compression::new(level); - CompressionStream::Gzip(GzEncoder::new(dest, level)) - }, - CompressionMethod::Xz => CompressionStream::Xz(XzEncoder::new(dest, level)), - }; - write_tar(dest, src, name, permissions, signature) - .map_err(|err| Error::Write(err, args.tar))?; - - Ok(()) -} - -fn write_tar( - mut dest: impl Write, - src: Mmap, - name: &str, - permissions: u32, - signature: Signature, -) -> std::io::Result<()> { - // #[repr(C, packed(1))] - // struct Header { - // name: [u8; 100], - // mode: [u8; 8], - // uid: [u8; 8], - // gid: [u8; 8], - // size: [u8; 12], - // mtime: [u8; 12], - // chksum: [u8; 8], - // typeflag: [u8; 1], - // linkname: [u8; 100], - // magic: [u8; 6], - // version: [u8; 2], - // uname: [u8; 32], - // gname: [u8; 32], - // devmajor: [u8; 8], - // devminor: [u8; 8], - // prefix: [u8; 155], - // _padding: [u8; 12], - // } - - let mut header = [0u8; 512]; - write!(&mut header[0x0..][..100], "{}", name)?; // name - write!(&mut header[0x64..][..8], "{:07o}", permissions & 0o777)?; // mode - write!(&mut header[0x6c..][..8], "{:07o}", 0)?; // uid - write!(&mut header[0x74..][..8], "{:07o}", 0)?; // gid - write!(&mut header[0x7c..][..12], "{:011o}", src.len())?; // size - write!(&mut header[0x88..][..12], "{:011o}", 978303600)?; // mtime (2001-01-01 00:00:00 Z) - write!(&mut header[0x94..][..8], "{:<8}", "")?; // chksum - // typeflag ('\0') - // linkname ("") - write!(&mut header[0x101..][..6], "ustar")?; // magic - write!(&mut header[0x107..][..2], "00")?; // version - write!(&mut header[0x109..][..32], "root")?; // uname - write!(&mut header[0x129..][..32], "root")?; // gname - write!(&mut header[0x149..][..8], "{:07o}", 0)?; // devmajor - write!(&mut header[0x151..][..8], "{:07o}", 0)?; // devminor - // prefix ("") - let cksum: u32 = header.iter().map(|&v| v as u32).sum(); - write!(&mut header[0x94..][..8], "{cksum:06o}\0")?; // chksum - - dest.write_all(&header)?; - dest.write_all(&src)?; - - const BLOCKSIZE: usize = 512; - const EXTRA: usize = Signature::BYTE_SIZE; - - let pos = BLOCKSIZE + src.len(); - // rounded up to next multiple of 512 - let new_pos = if pos & (BLOCKSIZE - 1) != 0 { - (pos | (BLOCKSIZE - 1)) + 1 - } else { - pos - }; - // is there enough room to put the signature at the end of the block? - let new_pos = if new_pos - pos < EXTRA { - new_pos - } else { - new_pos - EXTRA - }; - // pad with zeroes - match new_pos - pos { - 0 => {}, - bytes => { - const PAD: &[u8; BLOCKSIZE - EXTRA] = &[0; BLOCKSIZE - EXTRA]; - dest.write_all(&PAD[..bytes])?; - }, - } - // write signature - dest.write_all(&signature.to_bytes()) -} - -/// .tar a file and store the signature -#[derive(Debug, Parser)] -pub struct Cli { - /// Private key - private_key: PathBuf, - /// File to sign - file: PathBuf, - /// .tar file to (over)write - tar: PathBuf, - /// Compression method (*uncompressed | bzip2 | gzip | xz, *=default) - #[arg(short, long)] - method: Option, - /// Compression level (0 - *9, *=default) - #[arg(short, long)] - level: Option, - /// Unix-style permissions, default: 0o755 if "FILE" is executable, otherwise 0o644 - #[arg(short, long)] - permissions: Option, -} - -enum CompressionStream { - Uncompressed(BufWriter), - Bzip2(BzEncoder), - Gzip(GzEncoder), - Xz(XzEncoder), -} - -impl Write for CompressionStream { - fn write(&mut self, buf: &[u8]) -> std::io::Result { - match self { - CompressionStream::Uncompressed(s) => s.write(buf), - CompressionStream::Bzip2(s) => s.write(buf), - CompressionStream::Gzip(s) => s.write(buf), - CompressionStream::Xz(s) => s.write(buf), - } - } - - fn flush(&mut self) -> std::io::Result<()> { - match self { - CompressionStream::Uncompressed(s) => s.flush(), - CompressionStream::Bzip2(s) => s.flush(), - CompressionStream::Gzip(s) => s.flush(), - CompressionStream::Xz(s) => s.flush(), - } - } - - fn write_vectored(&mut self, bufs: &[std::io::IoSlice<'_>]) -> std::io::Result { - match self { - CompressionStream::Uncompressed(s) => s.write_vectored(bufs), - CompressionStream::Bzip2(s) => s.write_vectored(bufs), - CompressionStream::Gzip(s) => s.write_vectored(bufs), - CompressionStream::Xz(s) => s.write_vectored(bufs), - } - } - - fn write_all(&mut self, buf: &[u8]) -> std::io::Result<()> { - match self { - CompressionStream::Uncompressed(s) => s.write_all(buf), - CompressionStream::Bzip2(s) => s.write_all(buf), - CompressionStream::Gzip(s) => s.write_all(buf), - CompressionStream::Xz(s) => s.write_all(buf), - } - } - - fn write_fmt(&mut self, fmt: std::fmt::Arguments<'_>) -> std::io::Result<()> { - match self { - CompressionStream::Uncompressed(s) => s.write_fmt(fmt), - CompressionStream::Bzip2(s) => s.write_fmt(fmt), - CompressionStream::Gzip(s) => s.write_fmt(fmt), - CompressionStream::Xz(s) => s.write_fmt(fmt), - } - } -} - -#[derive(Debug, Clone, Copy, Default, strum::EnumString)] -#[strum(serialize_all = "snake_case")] -enum CompressionMethod { - #[default] - Uncompressed, - Bzip2, - Gzip, - Xz, -} - -#[derive(Debug, Clone, Copy)] -struct Permissions(u16); - -impl FromStr for Permissions { - type Err = ::FromStrRadixErr; - - fn from_str(s: &str) -> Result { - Ok(Self(parse_int::parse(s)?)) - } -} - -#[derive(Debug, thiserror::Error)] -pub enum Error { - #[error("input file has no UTF-8 name or name is longer than 99 bytes")] - NoFileName, - #[error("could not sign file")] - FileSign(#[source] SignatureError), - #[error("could not open {1:?} for reading")] - OpenRead(#[source] std::io::Error, PathBuf), - #[error("could not open {1:?} for writing")] - OpenWrite(#[source] std::io::Error, PathBuf), - #[error("could not read from {1:?}")] - Read(#[source] std::io::Error, PathBuf), - #[error("could not write to {1:?}")] - Write(#[source] std::io::Error, PathBuf), - #[error("could not mmap {1:?} for reading")] - Mmap(#[source] std::io::Error, PathBuf), - #[error("private key {1:?} was invalid")] - KeyValidate(#[source] SignatureError, PathBuf), - #[error("illgal compression level {0:?} not in 0..=9")] - CompressionLevel(u8), -} diff --git a/src/verify.rs b/src/verify.rs index df67e9d..ee6548f 100644 --- a/src/verify.rs +++ b/src/verify.rs @@ -1,77 +1,166 @@ use std::fs::OpenOptions; -use std::io::Read; +use std::io::{copy, Read, Seek, SeekFrom}; use std::path::PathBuf; use clap::Parser; -use ed25519_dalek::{Signature, SignatureError, VerifyingKey}; -use memmap2::Mmap; +use ed25519_dalek::{ + Digest, Sha512, Signature, SignatureError, VerifyingKey, PUBLIC_KEY_LENGTH, SIGNATURE_LENGTH, +}; + +use crate::{SignatureCountLeInt, HEADER_SIZE, MAGIC_HEADER}; pub fn main(args: Cli) -> Result<(), Error> { - // read key - let mut key = [0; 32]; - let mut f = match OpenOptions::new().read(true).open(&args.verifying_key) { + let context = args.context.map(String::into_bytes); + + // open signatures + let signature_file = args.signature.as_deref().unwrap_or(&args.input); + let mut f = match OpenOptions::new().read(true).open(signature_file) { Ok(f) => f, - Err(err) => return Err(Error::Open(err, args.verifying_key)), + Err(err) => { + return Err(Error::OpenRead(err, args.signature.unwrap_or(args.input))); + }, }; - if let Err(err) = f.read_exact(&mut key) { - return Err(Error::Read(err, args.verifying_key)); + + // read header + if args.end_of_file { + if let Err(err) = f.seek(SeekFrom::End(-(HEADER_SIZE as i64))) { + return Err(Error::Seek(err, args.signature.unwrap_or(args.input))); + } } - let key = match VerifyingKey::from_bytes(&key) { - Ok(key) => key, - Err(err) => return Err(Error::VerifyingKeyInvalid(err, args.verifying_key)), - }; - drop(f); - // read signature - let mut sign = [0; 64]; - let mut f = match OpenOptions::new().read(true).open(&args.signature) { - Ok(f) => f, - Err(err) => return Err(Error::Open(err, args.signature)), - }; - if let Err(err) = f.read_exact(&mut sign) { - return Err(Error::Read(err, args.signature)); + let mut header = [0; HEADER_SIZE]; + if let Err(err) = f.read_exact(&mut header) { + return Err(Error::Read(err, args.signature.unwrap_or(args.input))); + } + if header[..MAGIC_HEADER.len()] != MAGIC_HEADER[..] { + return Err(Error::MagicHeader(args.signature.unwrap_or(args.input))); } - let sign = Signature::from_bytes(&sign); - drop(f); + let signature_count = header[MAGIC_HEADER.len()..].try_into().unwrap(); + let signature_count = SignatureCountLeInt::from_le_bytes(signature_count) as usize; - // map "file" - let f = match OpenOptions::new().read(true).open(&args.file) { - Ok(f) => f, - Err(err) => return Err(Error::Open(err, args.file)), + if args.end_of_file { + let signature_bytes = signature_count * SIGNATURE_LENGTH + HEADER_SIZE; + if let Err(err) = f.seek(SeekFrom::End(-(signature_bytes as i64))) { + return Err(Error::Seek(err, args.signature.unwrap_or(args.input))); + } + } + + let signature_bytes = signature_count * SIGNATURE_LENGTH + HEADER_SIZE; + let mut signatures = vec![0; signature_bytes - HEADER_SIZE]; + if let Err(err) = f.read_exact(&mut signatures) { + return Err(Error::Read(err, args.signature.unwrap_or(args.input))); }; - let file = match unsafe { Mmap::map(&f) } { - Ok(file) => file, - Err(err) => return Err(Error::Mmap(err, args.file)), + let signatures = signatures + .chunks_exact(SIGNATURE_LENGTH) + .enumerate() + .map(|(idx, bytes)| { + Signature::from_slice(bytes).map_err(|err| Error::IllegalSignature(err, idx)) + }) + .collect::, _>>()?; + + // pre-hash input + let prehashed_message = { + let mut f = match OpenOptions::new().read(true).open(&args.input) { + Ok(f) => f, + Err(err) => return Err(Error::OpenRead(err, args.input)), + }; + + let mut prehashed_message = Sha512::new(); + let result = match (args.signature.is_none(), args.end_of_file) { + (false, _) => { + // signature is stored in extra file: read entire + copy(&mut f, &mut prehashed_message) + }, + (true, false) => { + // signature is stored in , skip start of file + if let Err(err) = f.seek(SeekFrom::Start(signature_bytes as u64)) { + return Err(Error::Seek(err, args.signature.unwrap_or(args.input))); + } + copy(&mut f, &mut prehashed_message) + }, + (true, true) => { + // signature is stored in , omit end of file + let len = match f.metadata() { + Ok(m) => m.len(), + Err(err) => return Err(Error::Read(err, args.input)), + }; + let mut f = f.take(len - signature_bytes as u64); + copy(&mut f, &mut prehashed_message) + }, + }; + if let Err(err) = result { + return Err(Error::Read(err, args.input)); + } + prehashed_message.update(header); + prehashed_message }; - drop(f); - // verify signature - key.verify_strict(&file, &sign).map_err(Error::Signature)?; - println!("OK"); - Ok(()) + // read verifying keys + let keys = args + .verifying_key + .into_iter() + .map(|key_file| { + let mut key = [0; PUBLIC_KEY_LENGTH]; + let mut f = match OpenOptions::new().read(true).open(&key_file) { + Ok(f) => f, + Err(err) => return Err(Error::OpenRead(err, key_file)), + }; + if let Err(err) = f.read_exact(&mut key) { + return Err(Error::Read(err, key_file)); + } + VerifyingKey::from_bytes(&key).map_err(|err| Error::KeyInvalid(err, key_file)) + }) + .collect::, _>>()?; + + // try to find `(signature, verifying key)` match + for key in &keys { + for signature in &signatures { + if key + .verify_prehashed_strict(prehashed_message.clone(), context.as_deref(), signature) + .is_ok() + { + println!("OK"); + return Ok(()); + } + } + } + Err(Error::NoMatch) } /// Verify a signature #[derive(Debug, Parser)] pub struct Cli { - /// Verifying key - verifying_key: PathBuf, - /// Signed file - file: PathBuf, - /// Signature file or .zip file generated with "zip" command - signature: PathBuf, + /// File to verify + #[arg(long, short = 'i')] + input: PathBuf, + /// Signature file. If absent the signature it is read from `` + #[arg(long, short = 'o')] + signature: Option, + /// One or more files containing verifying keys + #[arg(long, short = 'k', num_args = 1..)] + verifying_key: Vec, + /// Context (an arbitrary string used to salt the input, e.g. the basename of ``) + #[arg(long, short = 'c')] + context: Option, + /// Signatures at end of file (.tar files) + #[arg(long, short = 'e')] + end_of_file: bool, } #[derive(Debug, thiserror::Error)] pub enum Error { + #[error("no matching (signature, verifying_key) pair was found")] + NoMatch, #[error("could not open {1:?} for reading")] - Open(#[source] std::io::Error, PathBuf), + OpenRead(#[source] std::io::Error, PathBuf), #[error("could not read from {1:?}")] Read(#[source] std::io::Error, PathBuf), - #[error("could not mmap {1:?} for reading")] - Mmap(#[source] std::io::Error, PathBuf), + #[error("could not not seek in file {1:?}")] + Seek(#[source] std::io::Error, PathBuf), #[error("verify key {1:?} invalid")] - VerifyingKeyInvalid(#[source] SignatureError, PathBuf), - #[error("wrong signature")] - Signature(#[source] SignatureError), + KeyInvalid(#[source] SignatureError, PathBuf), + #[error("illegal signature #{1}")] + IllegalSignature(#[source] SignatureError, usize), + #[error("illegal, unknown or missing header in {0:?}")] + MagicHeader(PathBuf), } diff --git a/src/zip.rs b/src/zip.rs deleted file mode 100644 index 54058c3..0000000 --- a/src/zip.rs +++ /dev/null @@ -1,155 +0,0 @@ -use std::fs::OpenOptions; -use std::io::{Read, Write}; -use std::path::PathBuf; -use std::str::FromStr; - -use clap::Parser; -use ed25519_dalek::{SignatureError, Signer, SigningKey}; -use memmap2::Mmap; -use zip::result::ZipError; -use zip::write::FileOptions; -use zip::{CompressionMethod, ZipWriter}; - -pub fn main(args: Cli) -> Result<(), Error> { - let name = args - .file - .file_name() - .ok_or(Error::NoFileName)? - .to_str() - .ok_or(Error::NoFileName)?; - - // read signing key - let mut key = [0; 64]; - let mut f = match OpenOptions::new().read(true).open(&args.private_key) { - Ok(f) => f, - Err(err) => return Err(Error::OpenRead(err, args.private_key)), - }; - if let Err(err) = f.read_exact(&mut key) { - return Err(Error::Read(err, args.private_key)); - } - let key = SigningKey::from_keypair_bytes(&key) - .map_err(|err| Error::KeyValidate(err, args.private_key))?; - drop(f); - - // map "file" - let f = match OpenOptions::new().read(true).open(&args.file) { - Ok(f) => f, - Err(err) => return Err(Error::OpenRead(err, args.file)), - }; - let file = match unsafe { Mmap::map(&f) } { - Ok(file) => file, - Err(err) => return Err(Error::Mmap(err, args.file)), - }; - drop(f); - - // write signature - let signature = key.try_sign(&file).map_err(Error::FileSign)?; - let result = OpenOptions::new() - .write(true) - .create(true) - .truncate(true) - .open(&args.zip); - let mut zip_file = match result { - Ok(zip_file) => zip_file, - Err(err) => return Err(Error::OpenWrite(err, args.zip)), - }; - if let Err(err) = zip_file.write_all(&signature.to_bytes()) { - return Err(Error::Write(err, args.zip)); - } - - // get permissions - let permissions = match args.permissions { - Some(permissions) => permissions.0 as u32, - None => match is_executable::is_executable(&args.file) { - true => 0o755, - false => 0o644, - }, - }; - - // write ZIP content - let mut zip_file = ZipWriter::new(zip_file); - let method = match args.method.unwrap_or_default() { - NamedCompressionMethod::Stored => CompressionMethod::Stored, - NamedCompressionMethod::Deflated => CompressionMethod::Deflated, - NamedCompressionMethod::Bzip2 => CompressionMethod::Bzip2, - NamedCompressionMethod::Zstd => CompressionMethod::Zstd, - }; - let options = FileOptions::default() - .compression_method(method) - .compression_level(args.level) - .unix_permissions(permissions); - if let Err(err) = zip_file.start_file(name, options) { - return Err(Error::Zip(err, args.zip)); - } - if let Err(err) = zip_file.write_all(&file) { - return Err(Error::Write(err, args.zip)); - } - if let Err(err) = zip_file.finish() { - return Err(Error::Zip(err, args.zip)); - } - - Ok(()) -} - -/// ZIP a file and store the signature -#[derive(Debug, Parser)] -pub struct Cli { - /// Private key - private_key: PathBuf, - /// File to sign - file: PathBuf, - /// ZIP file to (over)write - zip: PathBuf, - /// Compression method (stored | *deflated | bzip2 | zstd, *=default) - #[arg(short, long)] - method: Option, - /// Compression level - #[arg(short, long)] - level: Option, - /// Unix-style permissions, default: 0o755 if "FILE" is executable, otherwise 0o644 - #[arg(short, long)] - permissions: Option, -} - -#[derive(Debug, Clone, Copy, Default, strum::EnumString)] -#[strum(serialize_all = "snake_case")] -enum NamedCompressionMethod { - Stored, - #[default] - Deflated, - Bzip2, - Zstd, -} - -#[derive(Debug, Clone, Copy)] -struct Permissions(u16); - -impl FromStr for Permissions { - type Err = ::FromStrRadixErr; - - fn from_str(s: &str) -> Result { - Ok(Self(parse_int::parse(s)?)) - } -} - -#[derive(Debug, thiserror::Error)] -pub enum Error { - #[error("input file has no UTF-8 name")] - NoFileName, - #[error("could not sign file")] - FileSign(#[source] SignatureError), - #[error("could not open {1:?} for reading")] - OpenRead(#[source] std::io::Error, PathBuf), - #[error("could not open {1:?} for writing")] - OpenWrite(#[source] std::io::Error, PathBuf), - #[error("could not read from {1:?}")] - Read(#[source] std::io::Error, PathBuf), - #[error("could not write to {1:?}")] - Write(#[source] std::io::Error, PathBuf), - #[error("could not mmap {1:?} for reading")] - Mmap(#[source] std::io::Error, PathBuf), - #[error("private key {1:?} was invalid")] - KeyValidate(#[source] SignatureError, PathBuf), - #[error("could not write to ZIP file {1:?}")] - Zip(#[source] ZipError, PathBuf), -}