Skip to main content

Crate rpm

Crate rpm 

Source
Expand description

§rpm-rs

A pure Rust library for parsing and creating RPM files.

§Features

  • Easy to use API
  • Pure Rust for easy integration in larger projects
  • Independence of spec files - pure programmatic interface for packaging
  • Compatibility from Enterprise Linux 8 (RHEL, Alma, Rocky, CentOS Stream) to Fedora

All supported compression types are behind feature flags. All of them except bzip2 are enabled by default. They can be disabled if these compression algorithms are unused.

§Examples

§Read package and access metadata

§Check basic metadata
let pkg = rpm::Package::open("tests/assets/RPMS/v6/rpm-basic-2.3.4-5.el9.noarch.rpm")?;

let name = pkg.metadata.get_name()?;
let version = pkg.metadata.get_version()?;
let release = pkg.metadata.get_release()?;
let arch = pkg.metadata.get_arch()?;

println!("{}-{}-{}.{}", name, version, release, arch);

for changelog in pkg.metadata.get_changelog_entries()? {
    println!("{}\n{}\n", changelog.name, changelog.description);
}
§Query dependencies
let pkg = rpm::Package::open("tests/assets/RPMS/v6/rpm-rich-deps-1.0-1.noarch.rpm")?;

for dep in pkg.metadata.get_requires()? {
    println!("{dep}");
    // e.g. "glibc >= 2.17", "bash", "rpm-libs = 4.14.3-1.el8"
}

// Other dependency types: get_provides(), get_conflicts(), get_obsoletes(),
// get_recommends(), get_suggests(), get_enhances(), get_supplements()
§Inspect package signatures
use rpm::signature::pgp::SignatureVersion;

let pkg = rpm::Package::open("./tests/assets/RPMS/v6/signed/rpm-basic-with-rsa4k-2.3.4-5.el9.noarch.rpm")?;

for sig in pkg.signatures()? {
    println!("Version: {:?}", sig.version());
    println!("Algorithm: {:?}", sig.algorithm());
    println!("Hash algorithm: {:?}", sig.hash_algorithm());
    if let Some(fp) = sig.fingerprint() {
        println!("Fingerprint: {}", fp);
    }
    if let Some(kid) = sig.key_id() {
        println!("Key ID: {}", kid);
    }
    if sig.version() == SignatureVersion::V6 {
        println!("This is a v6 signature");
    }
}
§List and read file contents
let pkg = rpm::Package::open("tests/assets/RPMS/v6/rpm-basic-2.3.4-5.el9.noarch.rpm")?;

// List file metadata without reading the payload
for entry in pkg.metadata.get_file_entries()? {
    println!("{} ({} bytes, {:o})", entry.path.display(), entry.size, entry.mode.permissions());
}

// Iterate over file contents (decompresses the payload)
for entry in pkg.files()? {
    let file = entry?;
    println!("{}: {} bytes", file.metadata.path.display(), file.content.len());
}
§Extract package contents to disk

Extract all files, directories, and symlinks from the package payload into a target directory - files are written relative to the target directory (not installed to their absolute paths).

// The directory must not already exist and its parent must exist.
let pkg = rpm::Package::open("tests/assets/RPMS/v6/rpm-basic-2.3.4-5.el9.noarch.rpm")?;
pkg.extract("./extracted-pkg")?;
// Creates ./extracted-pkg/ with the package's file tree inside it

§Verify signatures

§Verify using a keyring with multiple certificates
use rpm::signature::pgp::Verifier;

// Keyring files containing multiple OpenPGP certificates are supported.
// The verifier will try each certificate until it finds one that matches.
let verifier = Verifier::from_asc_file("./tests/assets/signing_keys/v4/rpm-testkey-v4-keyring.asc")?;

let pkg = rpm::Package::open("./tests/assets/RPMS/v4/signed/rpm-basic-with-rsa4096-2.3.4-5.el9.noarch.rpm")?;
pkg.verify_signature(verifier)?;

// You can also narrow down to a specific certificate by fingerprint:
let verifier = Verifier::from_asc_file("./tests/assets/signing_keys/v4/rpm-testkey-v4-keyring.asc")?
    .with_key(&hex::decode("d996aedc0d64d1e621b95ad2e964f9fb30d073b5")?)?;
§Check individual signatures and digests
use rpm::signature::pgp::Verifier;

let pkg = rpm::Package::open("./tests/assets/RPMS/v6/signed/rpm-basic-with-rsa4k-2.3.4-5.el9.noarch.rpm")?;
let verifier = Verifier::from_asc_file("./tests/assets/signing_keys/v6/rpm-testkey-v6-rsa4k.asc")?;

let report = pkg.check_signatures(verifier)?;

// Check overall pass/fail
assert!(report.is_ok());

// Or inspect individual digest results
if report.digests.sha256_header.is_verified() {
    println!("SHA-256 header digest: OK");
}
match &report.digests.sha3_256_header {
    rpm::DigestStatus::Verified => println!("SHA3-256 header digest: OK"),
    rpm::DigestStatus::NotPresent => println!("SHA3-256 header digest: not present"),
    rpm::DigestStatus::NotChecked => println!("SHA3-256 header digest: not checked"),
    rpm::DigestStatus::Mismatch { expected, actual } => {
        println!("SHA3-256 header digest: MISMATCH (expected {expected}, got {actual})");
    }
}

// Inspect each signature with its metadata and whether it was verified (only one signature must verify to "pass")
for sig in &report.signatures {
    let key_ref = sig.info.fingerprint()
        .or(sig.info.key_id())
        .unwrap_or("unknown");
    match sig.result() {
        Ok(()) => println!("Signature {key_ref}: OK"),
        Err(err) => println!("Signature {key_ref}: FAILED: {err}"),
    }
}

§Sign packages

§Sign an existing package and verify package signature
use rpm::signature::pgp::{Signer, Verifier};

let signer = Signer::from_asc_file("./tests/assets/signing_keys/v6/rpm-testkey-v6-rsa4k.secret")?;
let verifier = Verifier::from_asc_file("./tests/assets/signing_keys/v6/rpm-testkey-v6-rsa4k.asc")?;

let mut pkg = rpm::Package::open("./tests/assets/RPMS/v6/signed/rpm-basic-with-rsa4k-2.3.4-5.el9.noarch.rpm")?;
pkg.sign(signer)?;
pkg.verify_signature(verifier)?;
§Sign with a specific subkey
use rpm::signature::pgp::Signer;

let subkey_fingerprint = hex::decode("715619ae2365d909eb991ff97a509cd76a0bac92f0e17c1c2525812852cedfc5")?;

let signer = Signer::from_asc_file("./tests/assets/signing_keys/v6/rpm-testkey-v6-ed25519.secret")?
    .with_signing_key(&subkey_fingerprint)?;

let mut pkg = rpm::Package::open("./tests/assets/RPMS/v6/rpm-basic-2.3.4-5.el9.noarch.rpm")?;
pkg.sign(signer)?;

§Remote / HSM signing

There are two approaches for signing with keys that are not directly accessible as local key files (e.g. HSMs, remote signing services, or cloud KMS):

Option 1: Implement the signature::Signing trait. If the signing call can be made synchronously (even over a network round-trip), implement the trait and pass it to the standard Package::sign or Package::resign_in_place methods. BasicKeySigner bridges any [pgp::SigningKey] to the rpm-rs Signing trait, and the pgp::adapter module provides ready-made SigningKey implementations (RsaSigner, EcdsaSigner) that wrap any Rust Crypto [signature::Signer] — making it straightforward to plug in key backends such as PKCS#11 tokens or cloud KMS clients. See examples/hsm-signing.rs for a complete example.

Option 2: Split extract / sign / apply. If signing is fully asynchronous or out-of-band (different process, different machine, a queue or webhook), extract the header bytes, send them to the signer, and apply the returned signature separately:

use rpm::signature::pgp::{Signer, Verifier};
use rpm::signature::Signing;

// Step 1: Extract the header bytes to be signed.
// Only reads the metadata, not the payload.
let metadata = rpm::PackageMetadata::open("./tests/assets/RPMS/v6/rpm-basic-2.3.4-5.el9.noarch.rpm")?;
let header_bytes = metadata.header_bytes()?;

// Step 2: Sign the header bytes (this would normally happen on a remote system).
let signer = Signer::from_asc_file("./tests/assets/signing_keys/v6/rpm-testkey-v6-rsa4k.secret")?;
let signature = signer.sign(header_bytes.as_slice(), rpm::Timestamp(1_600_000_000))?;

// Step 3: Apply the signature.
// For in-memory packages:
let mut pkg = rpm::Package::open("./tests/assets/RPMS/v6/rpm-basic-2.3.4-5.el9.noarch.rpm")?;
pkg.apply_signature(signature.clone())?;

// Or apply directly to an on-disk package without loading the payload:
rpm::Package::apply_signature_in_place(&pkg_path, signature)?;
§In-place signing and clearing of signatures

For large packages, it is often desirable to sign or clear signatures without reading or rewriting the payload. These methods modify only the signature header on disk, using the reserved space to keep the file size unchanged:

use rpm::signature::pgp::Signer;

// Re-sign a package on disk (reads only the metadata, not the payload)
let signer = Signer::from_asc_file("./tests/assets/signing_keys/v6/rpm-testkey-v6-rsa4k.secret")?;
rpm::Package::resign_in_place(&pkg_path, &signer)?;

// Remove all signatures, converting their space to reserved space
// so that signatures can be added back later
rpm::Package::clear_signatures_in_place(&pkg_path)?;

// Re-sign the cleared package — the reserved space from clearing is reused
rpm::Package::resign_in_place(&pkg_path, &signer)?;

§Build a new package

use rpm::signature::pgp::Signer;

// For reproducible builds, set source_date to the timestamp of the last commit in your VCS
let build_config = rpm::BuildConfig::default()
    .compression(rpm::CompressionType::Gzip)
    .source_date(1_600_000_000);
let signer = Signer::from_asc_file("./tests/assets/signing_keys/v6/rpm-testkey-v6-ed25519.secret")?;
let pkg = rpm::PackageBuilder::new("test", "1.0.0", "MIT", "x86_64", "some awesome package")
    .using_config(build_config)
    // set default ownership and permissions for files and directories, similar to %defattr
    // in an RPM spec file. Pass None for any field to leave it unchanged (like `-` in %defattr).
    .default_file_attrs(Some(0o644), Some("myuser".into()), Some("mygroup".into()))
    .default_dir_attrs(Some(0o755), Some("myuser".into()), Some("mygroup".into()))
    // add a file with no special options
    // by default, files will be owned by the "root" user and group, and inherit their permissions
    // from the on-disk file.
    .with_file(
        "./tests/assets/SOURCES/multiplication_tables.py",
        rpm::FileOptions::new("/usr/bin/awesome"),
    )?
    // you can set permissions, capabilities and other metadata (user, group, etc.) manually
    .with_file(
        "./tests/assets/SOURCES/example_config.toml",
        rpm::FileOptions::new("/etc/awesome/second.toml")
            .permissions(0o644)
            .caps("cap_sys_admin,cap_net_admin=pe")?
            .user("hugo"),
    )?
    // Add a file - setting flags on it equivalent to `%config(noreplace)`
    .with_file(
        "./tests/assets/SOURCES/example_config.toml",
        rpm::FileOptions::new("/etc/awesome/config.toml")
            .config().noreplace(),
    )?
    // symlinks don't require a source file
    .with_symlink(
        rpm::FileOptions::symlink("/usr/bin/awesome_link", "/usr/bin/awesome"),
    )?
    // directories can be created with explicit ownership and permissions
    // this does not add any directory contents, just declares a directory
    .with_dir_entry(
        rpm::FileOptions::dir("/var/log/awesome").permissions(0o750),
    )?
    // ghost files / directories are not included in the package payload, but their metadata
    // (ownership, permissions, etc.) is tracked by RPM. This is commonly used for files
    // created at runtime (e.g. log files, PID files).
    .with_ghost(
        rpm::FileOptions::ghost("/var/log/awesome/app.log"),
    )?
    .pre_install_script("echo preinst")
    // Alternatively, use scriptlet builder api to specify flags and interpreter/arguments
    .post_trans_script(
        rpm::Scriptlet::new("echo posttrans")
            .flags(rpm::ScriptletFlags::EXPAND)
            .prog(vec!["/bin/blah/bash", "-c"])
    )
    .build_host(gethostname::gethostname().to_str().unwrap_or("host"))
    .add_changelog_entry(
        "Max Mustermann <max@example.com> - 0.1-29",
        "- was awesome, eh?",
        chrono::DateTime::parse_from_rfc2822("Wed, 19 Apr 2023 23:16:09 GMT")
            .expect("Date 1 is correct. qed"),
    )
    .add_changelog_entry(
        "Charlie Yom <test2@example.com> - 0.1-28",
        "- yeah, it was",
        // Raw timestamp for 1996-08-14 05:20:00
        840_000_000,
    )
    .requires(rpm::Dependency::any("wget"))
    .vendor("corporation or individual")
    .url("www.github.com/repo")
    .vcs("git:repo=example_repo:branch=example_branch:sha=example_sha")
    .build_and_sign(signer)?;

// Write to a specific file
pkg.write_to(dir.path().join("awesome.rpm"))?;

// Or write to a directory with auto-generated filename (`test-1.0.0-1.x86_64.rpm`)
pkg.write_to(dir.path())?;

Re-exports§

pub use ::chrono;

Modules§

signature

Structs§

BuildConfig
ChangelogEntry
User facing accessor type for a changelog entry
ChecksummingWriter
A wrapper for calculating the sha256 checksum of the contents written to it
Dependency
Description of a dependency as present in a RPM header record.
DependencyFlags
DigestReport
Results of verifying all digests in the package.
Evr
A full RPM “version” specifier has 3 different components - Epoch, Version, and Release.
FileCaps
FileDefaults
Default ownership and permissions applied to file entries when not explicitly overridden.
FileDigest
FileEntry
User facing accessor type for a file entry with contextual information
FileFlags
FileIterator
FileMode
A file mode combining file type and permission bits, as stored in RPM headers.
FileOptions
FileOptionsBuilder
FileOwnership
User facing accessor type representing ownership of a file
FileVerifyFlags
Header
Nevra
A full RPM “NEVRA” consists of 5 different components - Name, Epoch, Version, Release, and Architecture.
Package
A complete rpm file.
PackageBuilder
Create an RPM file by specifying metadata and files using the builder pattern.
PackageFileEntry
Describes a file present in the rpm file.
PackageMetadata
PackageSegmentOffsets
Offsets into an RPM Package (from the start of the file) demarking locations of each section
RpmFile
Scriptlet
Description of a scriptlet as present in a RPM header record
ScriptletFlags
Flags to configure scriptlet execution,
SignatureCheckResult
The result of verifying a single signature against the provided keys.
SignatureHeaderBuilder
base signature header builder
SignatureReport
Results of verifying all digests and signatures in the package.
Timestamp
Timestamp as a number of seconds that have elapsed since January 1, 1970 (midnight UTC/GMT), not counting leap seconds (in ISO 8601: 1970-01-01T00:00:00Z).
Trigger
A single trigger entry parsed from an RPM header.
TriggerCondition
A single condition within a trigger entry.

Enums§

CompressionType
Supported payload compression types.
CompressionWithLevel
Supported compression types, with an associated compression level. This is used for setting a custom compression configuration during RPM building.
Compressor
ContentSource
Define rpm content type source, from file path or raw bytes data.
DigestAlgorithm
DigestStatus
The result of checking a single digest.
Error
FileType
The type of a file entry in an RPM package.
HashKind
IndexData
Data as present in a IndexEntry .
IndexSignatureTag
IndexTag
RpmFormat
TimestampError

Constants§

DIR_FILE_TYPE
HEADER_I18NTABLE
HEADER_IMAGE
HEADER_IMMUTABLE
HEADER_MAGIC
header magic recognition (not the lead!)
HEADER_REGIONS
HEADER_SIGBASE
HEADER_SIGNATURES
HEADER_SIGTOP
HEADER_TAGBASE
INDEX_ENTRY_SIZE
Size (in bytes) of each entry in the index
INDEX_HEADER_SIZE
Size (in bytes) of the index header (the fixed portion of each header)
LEAD_SIZE
Size (in bytes) of the package “lead” section
REGULAR_FILE_TYPE
RPM_MAGIC
rpm magic as part of the lead header
SYMBOLIC_LINK_FILE_TYPE

Traits§

Tag
Header tag.

Functions§

rpm_evr_compare
Compare two strings as RPM EVR values
validate_caps_text