Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Create & verify PAYLOADDIGEST, PAYLOADDIGESTALT #124

Merged
merged 4 commits into from
Apr 22, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
`get_entry_data_as_u16_array()`, `get_entry_data_as_u32()`, `get_entry_data_as_u32_array()`,
`get_entry_data_as_u64()`, `get_entry_data_as_u64_array()`, `get_entry_data_as_string_array()`,
`get_entry_data_as_i18n_string()`
- Added `verify_signature()` and `verify_digests()` to `RPMPackage` to enable checking the integrity
and provenance of packages.

### Fixed

Expand All @@ -33,6 +35,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
did not have any files associated.
- Ensured that digests are always added to built RPMs. Previously they would not be included unless
the "signature-meta" (or "signature-pgp") features were enabled.
- Added `PAYLOADDIGEST`, `PAYLOADDIGESTALT`, and `PAYLOADDIGESTALGO` tags to built packages.

### Breaking Changes

Expand Down
4 changes: 1 addition & 3 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -42,10 +42,10 @@ enum-display-derive = "0.1"
cpio = "0.2"
# consider migrating to flate2
libflate = "1"
digest = "0.10"
sha2 = "0.10"
md-5 = "0.10"
sha1 = "0.10"
rand = { version = "0.8" }
pgp = { version = "0.9", optional = true }
chrono = "0.4"
log = "0.4"
Expand All @@ -60,8 +60,6 @@ async-std = { version = "1.12.0", optional = true }
tokio = { version = "1", optional = true }
tokio-util = { version = "0.7.4", features = ["compat"], optional = true }

digest = "0.10.6"

[dev-dependencies]
rsa = { version = "0.8" }
rsa-der = { version = "^0.3.0" }
Expand Down
15 changes: 8 additions & 7 deletions src/constants.rs
Original file line number Diff line number Diff line change
Expand Up @@ -335,12 +335,12 @@ pub enum IndexTag {
RPMTAG_TRANSFILETRIGGERTYPE = 5089,
RPMTAG_FILESIGNATURES = 5090,
RPMTAG_FILESIGNATURELENGTH = 5091,
RPMTAG_PAYLOADDIGEST = 5092,
RPMTAG_PAYLOADDIGEST = 5092, // hex-encoded string representing the digest of the payload
RPMTAG_PAYLOADDIGESTALGO = 5093,
RPMTAG_AUTOINSTALLED = 5094,
RPMTAG_IDENTITY = 5095,
RPMTAG_MODULARITYLABEL = 5096,
RPMTAG_PAYLOADDIGESTALT = 5097,
RPMTAG_PAYLOADDIGESTALT = 5097, // hex-encoded string representing the digest of the payload without compression
RPMTAG_ARCHSUFFIX = 5098,
RPMTAG_SPEC = 5099,
RPMTAG_TRANSLATIONURL = 5100,
Expand Down Expand Up @@ -375,11 +375,12 @@ pub enum IndexSignatureTag {
/// This tag specifies the uncompressed size of the Payload archive, including the cpio headers.
RPMSIGTAG_PAYLOADSIZE = HEADER_TAGBASE + 7,

/// This index contains the SHA1 checksum of the entire Header Section,
/// including the Header Record, Index Records and Header store.
/// The SHA1 checksum of the entire Header Section, including the Header Record, Index Records and
/// Header store, stored as a hex-encoded string.
RPMSIGTAG_SHA1 = 269,

/// This tag specifies the 128-bit MD5 checksum of the combined Header and Archive sections.
/// This tag specifies the 128-bit MD5 checksum of the combined Header and Archive sections, stored as
/// a binary representation.
RPMSIGTAG_MD5 = 1004,

/// The tag contains the DSA signature of the Header section.
Expand Down Expand Up @@ -415,8 +416,8 @@ pub enum IndexSignatureTag {
/// The data is formatted as a Version 3 Signature Packet as specified in RFC 2440: OpenPGP Message Format.
RPMSIGTAG_GPG = 1005,

/// This index contains the SHA256 checksum of the entire Header Section,
/// including the Header Record, Index Records and Header store.
/// This index contains the SHA256 checksum of the entire Header Section, including the Header Record,
/// Index Records and Header store, stored as a hex-encoded string.
RPMSIGTAG_SHA256 = IndexTag::RPMTAG_SHA256HEADER as u32,

/// A silly tag for a date.
Expand Down
3 changes: 3 additions & 0 deletions src/errors.rs
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,9 @@ pub enum RPMError {
key_ref: String,
},

#[error("digests from content did not match those in the header")]
DigestMismatchError,

#[error("unable to find key with key-ref: {key_ref}")]
KeyNotFoundError { key_ref: String },

Expand Down
52 changes: 42 additions & 10 deletions src/rpm/builder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ use std::os::unix::fs::PermissionsExt;
use std::path::{Path, PathBuf};
use std::time::{SystemTime, UNIX_EPOCH};

use digest::Digest;
use gethostname::gethostname;

use super::compressor::Compressor;
Expand Down Expand Up @@ -328,8 +329,6 @@ impl RPMBuilder {
modified_at: u32,
options: RPMFileOptions,
) -> Result<(), RPMError> {
use sha2::Digest;

let dest = options.destination;
if !dest.starts_with("./") && !dest.starts_with('/') {
return Err(RPMError::InvalidDestinationPath {
Expand Down Expand Up @@ -501,7 +500,7 @@ impl RPMBuilder {
let Digests {
header_and_content_digest: header_and_content_digest_md5,
header_digest: header_digest_sha1,
} = RPMPackage::create_digests_from_readers(
} = RPMPackage::create_legacy_header_digests(
&mut header.as_slice(),
header_and_content_cursor,
)?;
Expand Down Expand Up @@ -546,7 +545,7 @@ impl RPMBuilder {
let Digests {
header_and_content_digest: header_and_content_digest_md5,
header_digest: header_digest_sha1,
} = RPMPackage::create_digests_from_readers(
} = RPMPackage::create_legacy_header_digests(
&mut header.as_slice(),
header_and_content_cursor,
)?;
Expand Down Expand Up @@ -592,6 +591,13 @@ impl RPMBuilder {
// Lead is not important. just build it here

let lead = Lead::new(&self.name);
let possible_compression_details = self.compressor.get_details();

// Make the borrow checker happy
let is_zstd = matches!(self.compressor, Compressor::Zstd(_));
// Calculate the sha256 of the archive as we write it into the compressor, so that we don't
// need to keep two copies in memory simultaneously.
let mut archive = Sha256Writer::new(&mut self.compressor);

let mut ino_index = 1;

Expand Down Expand Up @@ -645,13 +651,14 @@ impl RPMBuilder {
.ino(ino_index)
.uid(self.uid.unwrap_or(0))
.gid(self.gid.unwrap_or(0))
.write(&mut self.compressor, content.len() as u32);
.write(&mut archive, content.len() as u32);

writer.write_all(&content)?;
writer.finish()?;

ino_index += 1;
}
cpio::newc::trailer(&mut archive)?;

self.provides
.push(Dependency::eq(self.name.clone(), self.version.clone()));
Expand All @@ -675,7 +682,7 @@ impl RPMBuilder {
"4.0-1".to_string(),
));

if matches!(self.compressor, Compressor::Zstd(_)) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we impl fn as_dependency(&self) -> Option<Dependency> for the compressor we can simply call self.requires.extend(compressor.as_dependency()) which is much cleaner.

Copy link
Collaborator Author

@dralley dralley Apr 18, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure that pattern would generalize well enough to be worthwhile. In fact this might be the only case where that is possible. With FileDigests or RichDeps or any other feature requires there isn't a particular object you could attach it to.

if is_zstd {
self.requires.push(Dependency::rpmlib(
"rpmlib(PayloadIsZstd)".to_string(),
"5.4.18-1".to_string(),
Expand Down Expand Up @@ -960,7 +967,34 @@ impl RPMBuilder {
),
]);

let possible_compression_details = self.compressor.get_details();
// digest of the uncompressed raw archive calculated on the inner writer
let raw_archive_digest_sha256 = hex::encode(archive.into_digest());
let payload = self.compressor.finish_compression()?;

// digest of the post-compression archive (payload)
let payload_digest_sha256 = {
let mut hasher = sha2::Sha256::default();
hasher.update(payload.as_slice());
hex::encode(hasher.finalize())
};

actual_records.extend([
IndexEntry::new(
IndexTag::RPMTAG_PAYLOADDIGEST,
offset,
IndexData::StringArray(vec![payload_digest_sha256]),
),
IndexEntry::new(
IndexTag::RPMTAG_PAYLOADDIGESTALGO,
offset,
IndexData::Int32(vec![FileDigestAlgorithm::Sha2_256 as u32]),
),
IndexEntry::new(
IndexTag::RPMTAG_PAYLOADDIGESTALT,
offset,
IndexData::StringArray(vec![raw_archive_digest_sha256]),
),
]);

if let Some(details) = possible_compression_details {
actual_records.push(IndexEntry::new(
Expand Down Expand Up @@ -1184,9 +1218,7 @@ impl RPMBuilder {
}

let header = Header::from_entries(actual_records, IndexTag::RPMTAG_HEADERIMMUTABLE);
self.compressor = cpio::newc::trailer(self.compressor)?;
let content = self.compressor.finish_compression()?;

Ok((lead, header, content))
Ok((lead, header, payload))
}
}
14 changes: 7 additions & 7 deletions src/rpm/headers/header.rs
Original file line number Diff line number Diff line change
Expand Up @@ -416,15 +416,15 @@ impl Default for FileCategory {
pub enum FileDigestAlgorithm {
// broken and very broken
Md5 = constants::PGPHASHALGO_MD5,
Sha1 = constants::PGPHASHALGO_SHA1,
Md2 = constants::PGPHASHALGO_MD2,
// Sha1 = constants::PGPHASHALGO_SHA1,
// Md2 = constants::PGPHASHALGO_MD2,

// not proven to be broken, weaker variants broken
#[allow(non_camel_case_types)]
Haval_5_160 = constants::PGPHASHALGO_HAVAL_5_160, // not part of PGP
Ripemd160 = constants::PGPHASHALGO_RIPEMD160,
// // not proven to be broken, weaker variants broken
// #[allow(non_camel_case_types)]
// Haval_5_160 = constants::PGPHASHALGO_HAVAL_5_160, // not part of PGP
// Ripemd160 = constants::PGPHASHALGO_RIPEMD160,

Tiger192 = constants::PGPHASHALGO_TIGER192, // not part of PGP
// Tiger192 = constants::PGPHASHALGO_TIGER192, // not part of PGP
Sha2_256 = constants::PGPHASHALGO_SHA256,
Sha2_384 = constants::PGPHASHALGO_SHA384,
Sha2_512 = constants::PGPHASHALGO_SHA512,
Expand Down
31 changes: 31 additions & 0 deletions src/rpm/headers/types.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
//! A collection of types used in various header records.
use crate::{constants::*, errors};
use digest::Digest;

/// Describes a file present in the rpm file.
pub struct RPMFileEntry {
Expand Down Expand Up @@ -315,6 +316,36 @@ impl Dependency {
}
}

/// A wrapper for calculating the sha256 checksum of the contents written to it
pub struct Sha256Writer<W> {
writer: W,
hasher: sha2::Sha256,
}

impl<W> Sha256Writer<W> {
pub fn new(writer: W) -> Self {
Sha256Writer {
writer,
hasher: sha2::Sha256::new(),
}
}

pub fn into_digest(self) -> impl AsRef<[u8]> {
self.hasher.finalize()
}
}

impl<W: std::io::Write> std::io::Write for Sha256Writer<W> {
fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
self.hasher.update(buf);
self.writer.write(buf)
}

fn flush(&mut self) -> std::io::Result<()> {
self.writer.flush()
}
}

mod test {

#[test]
Expand Down
Loading