Skip to content

Commit

Permalink
Signed .tar files
Browse files Browse the repository at this point in the history
  • Loading branch information
Kijewski committed Sep 7, 2023
1 parent debff22 commit d79950c
Show file tree
Hide file tree
Showing 4 changed files with 318 additions and 1 deletion.
23 changes: 23 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,10 @@ authors = ["René Kijewski <[email protected]>"]
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 = ["serde", "rand_core"] }
flate2 = "1.0.27"
is_executable = "1.0.1"
memmap2 = "0.7.1"
num-traits = "0.2.16"
Expand All @@ -17,4 +19,5 @@ 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"] }
8 changes: 7 additions & 1 deletion src/main.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
mod generate;
mod sign;
mod tar;
mod verify;
mod zip;

Expand All @@ -11,6 +12,7 @@ fn main() -> Result<(), MainError> {
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(())
Expand All @@ -30,7 +32,9 @@ 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)]
Expand All @@ -41,6 +45,8 @@ enum MainError {
Verify(#[from] verify::Error),
#[error("could not sign file")]
Sign(#[from] sign::Error),
#[error("could not zip file")]
#[error("could not create signed .tar file")]
Tar(#[from] tar::Error),
#[error("could not create signed .zip file")]
Zip(#[from] zip::Error),
}
285 changes: 285 additions & 0 deletions src/tar.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,285 @@
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<File, std::io::Error> = 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 | zstd, *=default)
#[arg(short, long)]
method: Option<CompressionMethod>,
/// Compression level (0 - *9, *=default)
#[arg(short, long)]
level: Option<u8>,
/// Unix-style permissions, default: 0o755 if "FILE" is executable, otherwise 0o644
#[arg(short, long)]
permissions: Option<Permissions>,
}

enum CompressionStream {
Uncompressed(BufWriter<File>),
Bzip2(BzEncoder<File>),
Gzip(GzEncoder<File>),
Xz(XzEncoder<File>),
}

impl Write for CompressionStream {
fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
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<usize> {
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 = <u16 as num_traits::Num>::FromStrRadixErr;

fn from_str(s: &str) -> Result<Self, Self::Err> {
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),
}

0 comments on commit d79950c

Please sign in to comment.