Skip to content

Commit

Permalink
Add support for RPMTAG_FILECAPS (#166)
Browse files Browse the repository at this point in the history
* Pin time to 0.3.23 to avoid MSRV breakages

* Add support for setting and reading from the RPMTAG_FILECAPS header
  • Loading branch information
dsteeley authored Aug 16, 2023
1 parent 500b9db commit f6d2c8e
Show file tree
Hide file tree
Showing 11 changed files with 129 additions and 14 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
3 changes: 3 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down
3 changes: 3 additions & 0 deletions src/errors.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,

Expand Down
25 changes: 23 additions & 2 deletions src/rpm/builder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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(())
Expand Down Expand Up @@ -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,
};

Expand Down Expand Up @@ -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);
Expand All @@ -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);
Expand Down Expand Up @@ -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::<Vec<String>>(),
),
)])
}
}

actual_records.extend([
Expand Down
2 changes: 2 additions & 0 deletions src/rpm/headers/header.rs
Original file line number Diff line number Diff line change
Expand Up @@ -430,6 +430,8 @@ pub struct FileEntry {
pub flags: FileFlags,
// @todo SELinux context? how is that done?
pub digest: Option<FileDigest>,
/// Defines any capabilities on the file.
pub caps: Option<String>,
}

fn parse_entry_data_number<'a, T, E, F>(
Expand Down
4 changes: 2 additions & 2 deletions src/rpm/headers/signature_builder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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];

Expand Down
36 changes: 35 additions & 1 deletion src/rpm/headers/types.rs
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -25,6 +28,7 @@ pub struct PackageFileEntry {
pub group: String,
pub base_name: String,
pub dir: String,
pub caps: Option<FileCaps>,
pub(crate) content: Vec<u8>,
}

Expand Down Expand Up @@ -171,6 +175,8 @@ impl From<FileMode> for u16 {
/// Description of file modes.
///
/// A subset
#[derive(Debug)]
pub struct FileOptions {
pub(crate) destination: String,
pub(crate) user: String,
Expand All @@ -179,6 +185,7 @@ pub struct FileOptions {
pub(crate) mode: FileMode,
pub(crate) flag: FileFlags,
pub(crate) inherit_permissions: bool,
pub(crate) caps: Option<FileCaps>,
}

impl FileOptions {
Expand All @@ -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,
}
Expand All @@ -224,6 +233,19 @@ impl FileOptionsBuilder {
self
}

pub fn caps(mut self, caps: impl Into<String>) -> Result<Self, errors::Error> {
// 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
Expand Down Expand Up @@ -345,7 +367,6 @@ impl<W: std::io::Write> std::io::Write for Sha256Writer<W> {
}

mod test {

#[test]
fn test_file_mode() -> Result<(), Box<dyn std::error::Error>> {
use super::*;
Expand Down Expand Up @@ -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(())
}
}
38 changes: 32 additions & 6 deletions src/rpm/package.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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();

Expand All @@ -850,14 +868,19 @@ impl PackageMetadata {
sizes,
flags,
))
.enumerate()
.try_fold::<Vec<FileEntry>, _, 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 {
Expand All @@ -869,6 +892,7 @@ impl PackageMetadata {
digest,
flags: FileFlags::from_bits_retain(flags),
size: size as usize,
caps: cap,
});
Ok(acc)
},
Expand All @@ -883,15 +907,17 @@ 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?;
digests?;
mtimes?;
sizes?;
flags?;
caps?;
unreachable!()
}
}
Expand Down
21 changes: 21 additions & 0 deletions src/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ fn test_rpm_builder() -> Result<(), Box<dyn std::error::Error>> {
// 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")
Expand All @@ -45,6 +46,21 @@ fn test_rpm_builder() -> Result<(), Box<dyn std::error::Error>> {

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(())
}

Expand Down Expand Up @@ -299,6 +315,11 @@ fn test_rpm_header() -> Result<(), Box<dyn std::error::Error>> {
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(())
Expand Down
6 changes: 3 additions & 3 deletions tests/signatures.rs
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ fn parse_externally_signed_rpm_and_verify() -> Result<(), Box<dyn std::error::Er
/// Test an attempt to verify the signature of a package that is not signed
#[test]
fn test_verify_unsigned_package() -> Result<(), Box<dyn std::error::Error>> {
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();
Expand Down Expand Up @@ -161,15 +161,15 @@ 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());
pkg.write_file(&out_file)?;

// 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(())
Expand Down

0 comments on commit f6d2c8e

Please sign in to comment.