diff --git a/CHANGELOG.md b/CHANGELOG.md index bf776ea0..55a3d310 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Added +- Support for setting file capabilities via the RPMTAGS_FILECAPS header. +- `PackageMetadata::get_file_entries` method can get capability headers for each file. + ## 0.12.0 ### Breaking Changes diff --git a/Cargo.toml b/Cargo.toml index 9a695d8f..70d97cb3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -53,10 +53,13 @@ itertools = "0.11" hex = { version = "0.4", features = ["std"] } zstd = "0.12" xz2 = "0.1" +capctl = "0.2.3" [dev-dependencies] rsa = { version = "0.8" } rsa-der = { version = "^0.3.0" } +# Pin time due to msrv +time = "=0.3.23" env_logger = "0.10.0" serial_test = "2.0" pretty_assertions = "1.3.0" diff --git a/README.md b/README.md index 96dbd91b..e19ffd94 100644 --- a/README.md +++ b/README.md @@ -50,6 +50,7 @@ let pkg = rpm::PackageBuilder::new("test", "1.0.0", "MIT", "x86_64", "some aweso // you can set a custom mode and custom user too rpm::FileOptions::new("/etc/awesome/second.toml") .mode(rpm::FileMode::regular(0o644)) + .caps("cap_sys_admin,cap_net_admin=pe")? .user("hugo"), )? .pre_install_script("echo preinst") diff --git a/src/errors.rs b/src/errors.rs index bc6b77b3..8c2abb4b 100644 --- a/src/errors.rs +++ b/src/errors.rs @@ -50,6 +50,9 @@ pub enum Error { #[error("invalid destination path {path} - {desc}")] InvalidDestinationPath { path: String, desc: &'static str }, + #[error("invalid capabilities specified {caps}")] + InvalidCapabilities { caps: String }, + #[error("signature packet not found in what is supposed to be a signature")] NoSignatureFound, diff --git a/src/rpm/builder.rs b/src/rpm/builder.rs index d1a9e69d..5b1ce21c 100644 --- a/src/rpm/builder.rs +++ b/src/rpm/builder.rs @@ -273,8 +273,8 @@ impl PackageBuilder { /// )? /// .with_file( /// "./awesome-config.toml", - /// // you can set a custom mode and custom user too - /// rpm::FileOptions::new("/etc/awesome/second.toml").mode(0o100744).user("hugo"), + /// // you can set a custom mode, capabilities and custom user too + /// rpm::FileOptions::new("/etc/awesome/second.toml").mode(0o100744).caps("cap_sys_admin=pe")?.user("hugo"), /// )? /// .build()?; /// # Ok(()) @@ -349,6 +349,10 @@ impl PackageBuilder { link: options.symlink, modified_at, dir: dir.clone(), + // Convert the caps to a string, so that we can store it in the header. + // We do this so that it's possible to verify that caps are correct when provided + // and then later check if any were set + caps: options.caps, sha_checksum, }; @@ -574,6 +578,7 @@ impl PackageBuilder { let files_len = self.files.len(); let mut file_sizes = Vec::with_capacity(files_len); let mut file_modes = Vec::with_capacity(files_len); + let mut file_caps = Vec::with_capacity(files_len); let mut file_rdevs = Vec::with_capacity(files_len); let mut file_mtimes = Vec::with_capacity(files_len); let mut file_hashes = Vec::with_capacity(files_len); @@ -594,6 +599,7 @@ impl PackageBuilder { combined_file_sizes += entry.size; file_sizes.push(entry.size); file_modes.push(entry.mode.into()); + file_caps.push(entry.caps.to_owned()); // I really do not know the difference. It seems like file_rdevice is always 0 and file_device number always 1. // Who knows, who cares. file_rdevs.push(0); @@ -970,6 +976,21 @@ impl PackageBuilder { IndexData::StringArray(self.directories.into_iter().collect()), ), ]); + if file_caps.iter().any(|caps| caps.is_some()) { + actual_records.extend([IndexEntry::new( + IndexTag::RPMTAG_FILECAPS, + offset, + IndexData::StringArray( + file_caps + .iter() + .map(|f| match f { + Some(caps) => caps.to_string(), + None => "".to_string(), + }) + .collect::>(), + ), + )]) + } } actual_records.extend([ diff --git a/src/rpm/headers/header.rs b/src/rpm/headers/header.rs index 821d1bca..69d5b87f 100644 --- a/src/rpm/headers/header.rs +++ b/src/rpm/headers/header.rs @@ -430,6 +430,8 @@ pub struct FileEntry { pub flags: FileFlags, // @todo SELinux context? how is that done? pub digest: Option, + /// Defines any capabilities on the file. + pub caps: Option, } fn parse_entry_data_number<'a, T, E, F>( diff --git a/src/rpm/headers/signature_builder.rs b/src/rpm/headers/signature_builder.rs index 6f0e65fb..4290fc6d 100644 --- a/src/rpm/headers/signature_builder.rs +++ b/src/rpm/headers/signature_builder.rs @@ -197,8 +197,8 @@ mod test { let sig_header_only = [0u8; 32]; - let digest_header_sha1 = hex::encode(&[0u8; 64]); - let digest_header_sha256: String = hex::encode(&[0u8; 64]); + let digest_header_sha1 = hex::encode([0u8; 64]); + let digest_header_sha256: String = hex::encode([0u8; 64]); let digest_header_and_archive = [0u8; 64]; diff --git a/src/rpm/headers/types.rs b/src/rpm/headers/types.rs index 95099b70..14fa96cf 100644 --- a/src/rpm/headers/types.rs +++ b/src/rpm/headers/types.rs @@ -1,5 +1,8 @@ //! A collection of types used in various header records. +use std::str::FromStr; + use crate::{constants::*, errors, Timestamp}; +use capctl::FileCaps; use digest::Digest; /// Offsets into an RPM Package (from the start of the file) demarking locations of each section @@ -25,6 +28,7 @@ pub struct PackageFileEntry { pub group: String, pub base_name: String, pub dir: String, + pub caps: Option, pub(crate) content: Vec, } @@ -171,6 +175,8 @@ impl From for u16 { /// Description of file modes. /// /// A subset + +#[derive(Debug)] pub struct FileOptions { pub(crate) destination: String, pub(crate) user: String, @@ -179,6 +185,7 @@ pub struct FileOptions { pub(crate) mode: FileMode, pub(crate) flag: FileFlags, pub(crate) inherit_permissions: bool, + pub(crate) caps: Option, } impl FileOptions { @@ -193,11 +200,13 @@ impl FileOptions { mode: FileMode::regular(0o664), flag: FileFlags::empty(), inherit_permissions: true, + caps: None, }, } } } +#[derive(Debug)] pub struct FileOptionsBuilder { inner: FileOptions, } @@ -224,6 +233,19 @@ impl FileOptionsBuilder { self } + pub fn caps(mut self, caps: impl Into) -> Result { + // verify capabilities + self.inner.caps = match FileCaps::from_str(&caps.into()) { + Ok(caps) => Some(caps), + Err(e) => { + return Err(errors::Error::InvalidCapabilities { + caps: e.to_string(), + }) + } + }; + Ok(self) + } + pub fn is_doc(mut self) -> Self { self.inner.flag = FileFlags::DOC; self @@ -345,7 +367,6 @@ impl std::io::Write for Sha256Writer { } mod test { - #[test] fn test_file_mode() -> Result<(), Box> { use super::*; @@ -441,4 +462,17 @@ mod test { } Ok(()) } + + #[test] + fn test_verify_capabilities_valid() { + let blank_file = crate::FileOptions::new("/usr/bin/awesome"); + blank_file.caps("cap_net_admin,cap_net_raw+p").unwrap(); + } + + #[test] + fn test_verify_capabilities_invalid() -> Result<(), crate::errors::Error> { + let blank_file = crate::FileOptions::new("/usr/bin/awesome"); + blank_file.caps("cap_net_an,cap_net_raw+p").unwrap_err(); + Ok(()) + } } diff --git a/src/rpm/package.rs b/src/rpm/package.rs index 7b4458b1..516c3853 100644 --- a/src/rpm/package.rs +++ b/src/rpm/package.rs @@ -832,11 +832,29 @@ impl PackageMetadata { let flags = self .header .get_entry_data_as_u32_array(IndexTag::RPMTAG_FILEFLAGS); - // @todo - // let caps = self.get_entry_i32_array_data(IndexTag::RPMTAG_FILECAPS)?; - match (modes, users, groups, digests, mtimes, sizes, flags) { - (Ok(modes), Ok(users), Ok(groups), Ok(digests), Ok(mtimes), Ok(sizes), Ok(flags)) => { + // Look for the file capabilities tag + // but it's not required so don't error out if it's not + let caps = match self + .header + .get_entry_data_as_string_array(IndexTag::RPMTAG_FILECAPS) + { + Ok(caps) => Ok(Some(caps)), + Err(Error::TagNotFound(_)) => Ok(None), + Err(e) => return Err(e), + }; + + match (modes, users, groups, digests, mtimes, sizes, flags, caps) { + ( + Ok(modes), + Ok(users), + Ok(groups), + Ok(digests), + Ok(mtimes), + Ok(sizes), + Ok(flags), + Ok(caps), + ) => { let paths = self.get_file_paths()?; let n = paths.len(); @@ -850,14 +868,19 @@ impl PackageMetadata { sizes, flags, )) + .enumerate() .try_fold::, _, Result<_, Error>>( Vec::with_capacity(n), - |mut acc, (path, user, group, mode, digest, mtime, size, flags)| { + |mut acc, (idx, (path, user, group, mode, digest, mtime, size, flags))| { let digest = if digest.is_empty() { None } else { Some(FileDigest::load_from_str(algorithm, digest)?) }; + let cap = match caps { + Some(caps) => caps.get(idx).map(|x| x.to_owned()), + None => None, + }; acc.push(FileEntry { path, ownership: FileOwnership { @@ -869,6 +892,7 @@ impl PackageMetadata { digest, flags: FileFlags::from_bits_retain(flags), size: size as usize, + caps: cap, }); Ok(acc) }, @@ -883,8 +907,9 @@ impl PackageMetadata { Err(Error::TagNotFound(_)), Err(Error::TagNotFound(_)), Err(Error::TagNotFound(_)), + Err(Error::TagNotFound(_)), ) => Ok(vec![]), - (modes, users, groups, digests, mtimes, sizes, flags) => { + (modes, users, groups, digests, mtimes, sizes, flags, caps) => { modes?; users?; groups?; @@ -892,6 +917,7 @@ impl PackageMetadata { mtimes?; sizes?; flags?; + caps?; unreachable!() } } diff --git a/src/tests.rs b/src/tests.rs index 60b861aa..41129a00 100644 --- a/src/tests.rs +++ b/src/tests.rs @@ -26,6 +26,7 @@ fn test_rpm_builder() -> Result<(), Box> { // you can set a custom mode and custom user too FileOptions::new("/etc/awesome/second.toml") .mode(0o100744) + .caps("cap_sys_admin,cap_sys_ptrace=pe")? .user("hugo"), )? .pre_install_script("echo preinst") @@ -45,6 +46,21 @@ fn test_rpm_builder() -> Result<(), Box> { pkg.verify_digests()?; + // check various metadata on the files + pkg.metadata.get_file_entries()?.iter().for_each(|f| { + if f.path.as_os_str() == "/etc/awesome/second.toml" { + assert_eq!( + f.clone().caps.unwrap(), + "cap_sys_ptrace,cap_sys_admin=ep".to_string() + ); + assert_eq!(f.ownership.user, "hugo".to_string()); + } else if f.path.as_os_str() == "/etc/awesome/config.toml" { + assert_eq!(f.caps, Some("".to_string())); + } else if f.path.as_os_str() == "/usr/bin/awesome" { + assert_eq!(f.mode, FileMode::from(0o100644)); + } + }); + Ok(()) } @@ -299,6 +315,11 @@ fn test_rpm_header() -> Result<(), Box> { assert_eq!(package.content.len(), second_pkg.content.len()); assert!(package.metadata == second_pkg.metadata); + // Verify that if there are no capabilities set then the caps field is None + package.metadata.get_file_entries()?.iter().for_each(|f| { + assert_eq!(f.clone().caps, None); + }); + package.verify_digests()?; Ok(()) diff --git a/tests/signatures.rs b/tests/signatures.rs index 9386a221..980cdeec 100644 --- a/tests/signatures.rs +++ b/tests/signatures.rs @@ -53,7 +53,7 @@ fn parse_externally_signed_rpm_and_verify() -> Result<(), Box Result<(), Box> { - let pkg = rpm::Package::open(&common::rpm_empty_path())?; + let pkg = rpm::Package::open(common::rpm_empty_path())?; // test RSA let verification_key = common::rsa_public_key(); @@ -161,7 +161,7 @@ fn build_parse_sign_and_verify( assert_eq!(3, pkg.metadata.get_epoch()?); // sign - let signer: Signer = Signer::load_from_asc_bytes(signing_key.as_ref())?; + let signer: Signer = Signer::load_from_asc_bytes(signing_key)?; pkg.sign(signer)?; let out_file = common::cargo_out_dir().join(pkg_out_path.as_ref()); @@ -169,7 +169,7 @@ fn build_parse_sign_and_verify( // verify let package = rpm::Package::open(&out_file)?; - let verifier = Verifier::load_from_asc_bytes(verification_key.as_ref())?; + let verifier = Verifier::load_from_asc_bytes(verification_key)?; package.verify_signature(verifier)?; Ok(())