diff --git a/src/rpm/headers/signature_builder.rs b/src/rpm/headers/signature_builder.rs index 7b2122a8..6f0e65fb 100644 --- a/src/rpm/headers/signature_builder.rs +++ b/src/rpm/headers/signature_builder.rs @@ -259,6 +259,7 @@ mod test { .is_ok()); } + // @todo: this test is kind of duplicative, probably not necessary? #[cfg(feature = "signature-meta")] #[test] fn signature_header_build() { diff --git a/src/rpm/package.rs b/src/rpm/package.rs index d3b9c630..767fa837 100644 --- a/src/rpm/package.rs +++ b/src/rpm/package.rs @@ -9,11 +9,11 @@ use num_traits::FromPrimitive; use crate::{constants::*, errors::*, CompressionType}; -#[cfg(feature = "signature-meta")] -use crate::{signature, Timestamp}; #[cfg(feature = "signature-pgp")] use crate::signature::pgp::Verifier; #[cfg(feature = "signature-meta")] +use crate::{signature, Timestamp}; +#[cfg(feature = "signature-meta")] use std::{fmt::Debug, io::Read}; use super::headers::*; @@ -117,7 +117,7 @@ impl RPMPackage { /// # Examples /// ``` /// # fn main() -> Result<(), Box> { - /// let mut package = rpm::RPMPackage::open("test_assets/monkeysphere-0.37-1.el7.noarch.rpm")?; + /// let mut package = rpm::RPMPackage::open("test_assets/ima_signed.rpm")?; /// let raw_secret_key = std::fs::read("./test_assets/secret_key.asc")?; /// let signer = rpm::signature::pgp::Signer::load_from_asc_bytes(&raw_secret_key)?; /// // It's recommended to use timestamp of last commit in your VCS @@ -178,7 +178,7 @@ impl RPMPackage { Ok(()) } - /// Return the key id (issuer) of the signature + /// Return the key id (issuer) of the signature as a hexadecimal string #[cfg(feature = "signature-pgp")] pub fn signature_key_id(&self) -> Result, RPMError> { let rsa_sig = &self @@ -264,41 +264,6 @@ impl RPMPackage { verifier.verify(header_bytes.as_slice(), signature_header_only)?; } - // match verifier.algorithm() { - // signature::AlgorithmType::RSA => { - // if let Ok(signature_header_and_content) = rpm_v3_sig { - // signature::echo_signature( - // "signature_header(header and content)", - // signature_header_and_content, - // ); - // let header_and_content_cursor = - // io::Cursor::new(&header_bytes).chain(io::Cursor::new(&self.content)); - // verifier.verify(header_and_content_cursor, signature_header_and_content)?; - // } - - // if let Ok(signature_header_only) = rsa_sig { - // signature::echo_signature( - // "signature_header(header only)", - // signature_header_only, - // ); - // verifier.verify(header_bytes.as_slice(), signature_header_only)?; - // } else { - // return Err(RPMError::VerificationError { source: (), key_ref: () }) - // } - // } - // signature::AlgorithmType::EdDSA => { - // if let Ok(signature_header_only) = eddsa_sig { - // signature::echo_signature( - // "signature_header(header only)", - // signature_header_only, - // ); - // verifier.verify(header_bytes.as_slice(), signature_header_only)?; - // } else { - // return Err(RPMError::VerificationError { source: (), key_ref: () }) - // } - // } - //} - Ok(()) } diff --git a/src/rpm/signature/pgp.rs b/src/rpm/signature/pgp.rs index 7b7ee88c..3aa9e42c 100644 --- a/src/rpm/signature/pgp.rs +++ b/src/rpm/signature/pgp.rs @@ -310,9 +310,9 @@ pub(crate) mod test { #[test] fn verify_pgp_crate() { + use chrono::{TimeZone, Utc}; use pgp::types::{PublicKeyTrait, SecretKeyTrait}; use pgp::Signature; - use chrono::{TimeZone, Utc}; const RPM_SHA2_256: [u8; 32] = hex!("d92bfe276e311a67fe128768c5df4d06fd461e043afdf872ba4c679d860db81e"); diff --git a/src/rpm/signature/traits.rs b/src/rpm/signature/traits.rs index 266f8056..77e774bf 100644 --- a/src/rpm/signature/traits.rs +++ b/src/rpm/signature/traits.rs @@ -3,10 +3,10 @@ //! Does not contain hashing! Hashes are fixed by the rpm //! "spec" to sha1, md5 (yes, that is correct), sha2_256. -use std::io; -use std::fmt::Debug; use crate::errors::*; use crate::Timestamp; +use std::fmt::Debug; +use std::io; #[derive(Clone, Copy, Debug)] pub enum AlgorithmType { diff --git a/tests/common.rs b/tests/common.rs index 1f27e2ae..dcc02abd 100644 --- a/tests/common.rs +++ b/tests/common.rs @@ -39,15 +39,19 @@ pub fn load_asc_keys() -> (Vec, Vec) { } pub fn load_rsa_keys() -> (Vec, Vec) { - let signing_key_path = + (rsa_private_key(), rsa_public_key()) +} + +pub fn rsa_private_key() -> Vec { + let private_key = cargo_manifest_dir().join("test_assets/fixture_packages/signing_keys/secret_rsa4096.asc"); - let signing_key = std::fs::read(signing_key_path).unwrap(); + std::fs::read(private_key).unwrap() +} - let verification_key_path = +pub fn rsa_public_key() -> Vec { + let public_key = cargo_manifest_dir().join("test_assets/fixture_packages/signing_keys/public_rsa4096.asc"); - let verification_key = std::fs::read(verification_key_path).unwrap(); - - (signing_key.to_vec(), verification_key.to_vec()) + std::fs::read(public_key).unwrap() } pub fn eddsa_private_key() -> Vec { diff --git a/tests/compat.rs b/tests/compat.rs index dd9ac6c5..32280745 100644 --- a/tests/compat.rs +++ b/tests/compat.rs @@ -2,6 +2,7 @@ use rpm::*; use std::fs::File; use std::io::prelude::*; use std::io::BufReader; +use std::path::Path; use std::process::Stdio; mod common; @@ -12,19 +13,31 @@ mod pgp { use super::*; use signature::pgp::{Signer, Verifier}; - #[serial_test::serial] - fn create_full_rpm() -> Result<(), Box> { - let _ = env_logger::try_init(); - let (signing_key, _) = common::load_asc_keys(); - - let signer = Signer::load_from_asc_bytes(signing_key.as_ref()) - .expect("Must load signer from signing key"); + /// Verify that the RPM is installable with valid signatures on the various supported distros + #[track_caller] + fn try_installation_and_verify_signatures( + path: impl AsRef, + ) -> Result<(), Box> { + let dnf_cmd = format!("dnf --disablerepo=updates,updates-testing,updates-modular,fedora-modular install -y {};", path.as_ref().display()); + let rpm_sig_check = format!("rpm -vv --checksig {} 2>&1;", path.as_ref().display()); + // TODO: check signatures on all distros? + [ + ("fedora:38", &rpm_sig_check), + ("fedora:38", &dnf_cmd), + ("centos:stream9", &dnf_cmd), + ("centos:stream8", &dnf_cmd), + ] + .iter() + .try_for_each(|(image, cmd)| { + podman_container_launcher(cmd, image, vec![])?; + Ok(()) + }) + } + fn build_full_rpm() -> Result> { let cargo_file = common::cargo_manifest_dir().join("Cargo.toml"); - let out_file = common::cargo_out_dir().join("test.rpm"); - let mut f = File::create(out_file)?; - let pkg = RPMBuilder::new("test", "1.0.0", "MIT", "x86_64", "some package") + let bldr = RPMBuilder::new("test", "1.0.0", "MIT", "x86_64", "some package") .compression(CompressionType::Gzip) .with_file( cargo_file.to_str().unwrap(), @@ -71,187 +84,80 @@ mod pgp { .requires(Dependency::any("rpm-sign".to_string())) .vendor("dummy vendor") .url("dummy url") - .vcs("dummy vcs") - .build_and_sign(signer)?; + .vcs("dummy vcs"); - pkg.write(&mut f)?; - f.flush()?; - let epoch = pkg.metadata.get_epoch()?; - assert_eq!(1, epoch); + Ok(bldr) + } - let dnf_cmd = "dnf --disablerepo=updates,updates-testing,updates-modular,fedora-modular install -y /out/test.rpm;"; - let rpm_sig_check = "rpm -vv --checksig /out/test.rpm 2>&1;".to_string(); + #[test] + #[serial_test::serial] + fn test_install_full_rpm() -> Result<(), Box> { + let _ = env_logger::try_init(); + let pkg = build_full_rpm()?.build()?; + let out_file = common::cargo_out_dir().join("full_rpm_nosig.rpm"); + pkg.write_file(&out_file)?; + assert_eq!(1, pkg.metadata.get_epoch()?); - [ - ("fedora:38", rpm_sig_check.as_str()), - ("fedora:38", dnf_cmd), - ("centos:stream9", dnf_cmd), - ("centos:stream8", dnf_cmd), - ] - .iter() - .try_for_each(|(image, cmd)| { - podman_container_launcher(cmd, image, vec![])?; - Ok(()) - }) + try_installation_and_verify_signatures("/out/full_rpm_nosig.rpm")?; + + Ok(()) } #[test] #[serial_test::serial] - fn create_empty_rpm() -> Result<(), Box> { + fn test_install_empty_rpm() -> Result<(), Box> { + let _ = env_logger::try_init(); let pkg = RPMBuilder::new("foo", "1.0.0", "MIT", "x86_64", "an empty package").build()?; - let out_file = common::cargo_out_dir().join("test.rpm"); + let out_file = common::cargo_out_dir().join("empty_rpm_nosig.rpm"); + pkg.write_file(&out_file)?; - let mut f = std::fs::File::create(out_file)?; - pkg.write(&mut f)?; - let dnf_cmd = "dnf --disablerepo=updates,updates-testing,updates-modular,fedora-modular install -y /out/test.rpm;"; + try_installation_and_verify_signatures("/out/empty_rpm_nosig.rpm")?; - [ - ("fedora:38", dnf_cmd), - ("centos:stream9", dnf_cmd), - ("centos:stream8", dnf_cmd), - ] - .iter() - .try_for_each(|(image, cmd)| { - podman_container_launcher(cmd, image, vec![])?; - Ok(()) - }) + Ok(()) } #[test] #[serial_test::serial] - fn create_full_rpm_with_signature_and_verify_externally( - ) -> Result<(), Box> { + fn test_install_full_rpm_with_signature() -> Result<(), Box> { let _ = env_logger::try_init(); let (signing_key, _) = common::load_asc_keys(); let signer = Signer::load_from_asc_bytes(signing_key.as_ref()) .expect("Must load signer from signing key"); - let cargo_file = common::cargo_manifest_dir().join("Cargo.toml"); - let out_file = common::cargo_out_dir().join("test.rpm"); - - let mut f = std::fs::File::create(out_file)?; - let pkg = RPMBuilder::new("test", "1.0.0", "MIT", "x86_64", "some package") - .compression(CompressionType::Gzip) - .with_file( - cargo_file.to_str().unwrap(), - RPMFileOptions::new("/etc/foobar/foo.toml"), - )? - .with_file( - cargo_file.to_str().unwrap(), - RPMFileOptions::new("/etc/foobar/zazz.toml"), - )? - .with_file( - cargo_file.to_str().unwrap(), - RPMFileOptions::new("/etc/foobar/hugo/bazz.toml") - .mode(0o100_777) - .is_config(), - )? - .with_file( - cargo_file.to_str().unwrap(), - RPMFileOptions::new("/etc/foobar/bazz.toml"), - )? - .with_file( - cargo_file.to_str().unwrap(), - RPMFileOptions::new("/etc/foobar/hugo/aa.toml"), - )? - .with_file( - cargo_file.to_str().unwrap(), - RPMFileOptions::new("/var/honollulu/bazz.toml"), - )? - .with_file( - cargo_file.to_str().unwrap(), - RPMFileOptions::new("/etc/Cargo.toml"), - )? - .epoch(1) - .pre_install_script("echo preinst") - .add_changelog_entry("me", "was awesome, eh?", 1681411811) - .add_changelog_entry("you", "yeah, it was", 1681411991) - .requires(Dependency::any("rpm-sign".to_string())) - .vendor("dummy vendor") - .url("dummy repo") - .vcs("git:repo=example_repo:branch=example_branch:sha=example_sha") - .build_and_sign(signer)?; - - pkg.write(&mut f)?; - let epoch = pkg.metadata.get_epoch()?; - assert_eq!(1, epoch); + let pkg = build_full_rpm()?.build_and_sign(signer)?; + let out_file = common::cargo_out_dir().join("full_rpm_sig.rpm"); + pkg.write_file(&out_file)?; + assert_eq!(1, pkg.metadata.get_epoch()?); - let dnf_cmd = "dnf --disablerepo=updates,updates-testing,updates-modular,fedora-modular install -y /out/test.rpm;"; - let rpm_sig_check = "rpm -vv --checksig /out/test.rpm 2>&1;".to_string(); + try_installation_and_verify_signatures("/out/full_rpm_sig.rpm")?; - [ - ("fedora:38", rpm_sig_check.as_str()), - ("fedora:38", dnf_cmd), - ("centos:stream9", dnf_cmd), - ("centos:stream8", dnf_cmd), - ] - .iter() - .try_for_each(|(image, cmd)| { - podman_container_launcher(cmd, image, vec![])?; - Ok(()) - }) + Ok(()) } #[test] #[serial_test::serial] - fn parse_externally_signed_rpm_and_verify() -> Result<(), Box> { + fn test_install_empty_rpm_with_signature() -> Result<(), Box> { let _ = env_logger::try_init(); - let (signing_key, verification_key) = common::load_asc_keys(); - - let cargo_file = common::cargo_manifest_dir().join("Cargo.toml"); - let out_file = common::cargo_out_dir().join("roundtrip.rpm"); - - { - let signer = Signer::load_from_asc_bytes(signing_key.as_ref())?; - - let mut f = std::fs::File::create(&out_file)?; - let pkg = RPMBuilder::new( - "roundtrip", - "1.0.0", - "MIT", - "x86_64", - "spins round and round", - ) - .compression(CompressionType::Zstd) - .with_file( - cargo_file.to_str().unwrap(), - RPMFileOptions::new("/etc/foobar/hugo/bazz.toml") - .mode(FileMode::regular(0o777)) - .is_config(), - )? - .with_file( - cargo_file.to_str().unwrap(), - RPMFileOptions::new("/etc/Cargo.toml"), - )? - .epoch(3) - .pre_install_script("echo preinst") - .add_changelog_entry("you", "yada yada", 1681801261) - .requires(Dependency::any("rpm-sign".to_string())) - .build_and_sign(&signer)?; - - pkg.write(&mut f)?; - let epoch = pkg.metadata.get_epoch()?; - assert_eq!(3, epoch); - } + let (signing_key, _) = common::load_asc_keys(); - // verify - { - let out_file = std::fs::File::open(&out_file).expect("should be able to open rpm file"); - let mut buf_reader = std::io::BufReader::new(out_file); - let package = RPMPackage::parse(&mut buf_reader)?; + let signer = Signer::load_from_asc_bytes(signing_key.as_ref()) + .expect("Must load signer from signing key"); - let verifier = Verifier::load_from_asc_bytes(verification_key.as_ref())?; + let pkg = RPMBuilder::new("foo", "1.0.0", "MIT", "x86_64", "an empty package") + .build_and_sign(&signer)?; + let out_file = common::cargo_out_dir().join("empty_rpm_nosig.rpm"); + pkg.write_file(&out_file)?; - package.verify_signature(verifier)?; - } + try_installation_and_verify_signatures("/out/empty_rpm_nosig.rpm")?; Ok(()) } + // @todo: we don't really need to sign the RPMs as part of the test. Can use fixture. #[test] #[serial_test::serial] - fn create_signed_rpm_and_verify() -> Result<(), Box> { + fn test_verify_externally_signed_rpm() -> Result<(), Box> { let _ = env_logger::try_init(); let (_, verification_key) = common::load_asc_keys(); @@ -291,7 +197,7 @@ rpm -vv --checksig /out/{rpm_file} 2>&1 #[test] #[serial_test::serial] - fn create_signature_with_gpg_and_verify() -> Result<(), Box> { + fn test_verify_raw_gpg_signature() -> Result<(), Box> { let _ = env_logger::try_init(); let (_signing_key, verification_key) = common::load_asc_keys(); diff --git a/tests/signatures.rs b/tests/signatures.rs index ef711f03..a1a70cc9 100644 --- a/tests/signatures.rs +++ b/tests/signatures.rs @@ -7,18 +7,97 @@ use rpm::*; mod common; +/// Resign an already-signed package with new keys, and verify it with the new keys #[test] -fn test_rpm_file_signatures_resign_rsa() -> Result<(), Box> { +fn test_rpm_file_signatures_resign() -> Result<(), Box> { let pkg_path = common::rpm_ima_signed_file_path(); + + // test RSA let (signing_key, verification_key) = common::load_rsa_keys(); - resign_and_verify_with_keys(pkg_path.as_ref(), &signing_key, &verification_key) + resign_and_verify_with_keys( + pkg_path.as_ref(), + &signing_key, + &verification_key, + "rsa_resigned_pkg.rpm", + )?; + + // test EdDSA + let (signing_key, verification_key) = common::load_eddsa_keys(); + resign_and_verify_with_keys( + pkg_path.as_ref(), + &signing_key, + &verification_key, + "eddsa_resigned_pkg.rpm", + ) } +// @todo: we could really just use a fixture for this, better than rebuilding? +/// Test verifying the signature of a package that has been signed #[test] -fn test_rpm_file_signatures_resign_eddsa() -> Result<(), Box> { - let pkg_path = common::rpm_ima_signed_file_path(); +fn parse_externally_signed_rpm_and_verify() -> Result<(), Box> { + let _ = env_logger::try_init(); + + // test RSA + let (signing_key, verification_key) = common::load_rsa_keys(); + build_parse_sign_and_verify(&signing_key, &verification_key, "rsa_signed_pkg.rpm")?; + + // test EdDSA let (signing_key, verification_key) = common::load_eddsa_keys(); - resign_and_verify_with_keys(pkg_path.as_ref(), &signing_key, &verification_key) + build_parse_sign_and_verify(&signing_key, &verification_key, "eddsa_signed_pkg.rpm")?; + + Ok(()) +} + +/// Test an attempt to verify the signature of a package that is not signed +#[test] +fn test_verify_unsigned_package() -> Result<(), Box> { + let pkg = RPMPackage::open(&common::rpm_empty_path())?; + + // test RSA + let verification_key = common::rsa_public_key(); + let verifier = Verifier::load_from_asc_bytes(verification_key.as_ref())?; + assert!(matches!( + pkg.verify_signature(verifier), + Err(RPMError::NoSignatureFound) + )); + + // test EdDSA + let verification_key = common::eddsa_public_key(); + let verifier = Verifier::load_from_asc_bytes(verification_key.as_ref())?; + assert!(matches!( + pkg.verify_signature(verifier), + Err(RPMError::NoSignatureFound) + )); + + Ok(()) +} + +/// Test an attempt to verify the signature of a package using the wrong key type +#[test] +fn test_verify_package_with_wrong_key_type() -> Result<(), Box> { + let rsa_signer = Signer::load_from_asc_bytes(&common::rsa_private_key())?; + let rsa_verifier = Verifier::load_from_asc_bytes(&common::rsa_public_key())?; + let rsa_pkg = RPMBuilder::new("foo", "1.0.0", "MIT", "x86_64", "an empty package") + .build_and_sign(&rsa_signer)?; + + let eddsa_signer = Signer::load_from_asc_bytes(&common::eddsa_private_key())?; + let eddsa_verifier = Verifier::load_from_asc_bytes(&common::eddsa_public_key())?; + let eddsa_pkg = RPMBuilder::new("foo", "1.0.0", "MIT", "x86_64", "an empty package") + .build_and_sign(&eddsa_signer)?; + + // test EdDSA key with RSA-signed package + assert!(matches!( + rsa_pkg.verify_signature(&eddsa_verifier), + Err(RPMError::KeyNotFoundError { key_ref: _ }) + )); + + // test RSA key with EdDSA-signed package + assert!(matches!( + eddsa_pkg.verify_signature(&rsa_verifier), + Err(RPMError::KeyNotFoundError { key_ref: _ }) + )); + + Ok(()) } #[track_caller] @@ -26,11 +105,16 @@ fn resign_and_verify_with_keys( pkg_path: &Path, signing_key: &[u8], verification_key: &[u8], + pkg_out_path: impl AsRef, ) -> Result<(), Box> { let mut package = RPMPackage::open(pkg_path)?; let signer = Signer::load_from_asc_bytes(signing_key)?; package.sign_with_timestamp(&signer, 1_600_000_000)?; + let out_file = common::cargo_out_dir().join(pkg_out_path.as_ref()); + package.write_file(&out_file)?; + + let package = RPMPackage::open(&out_file)?; let verifier = Verifier::load_from_asc_bytes(verification_key).unwrap(); package .verify_signature(&verifier) @@ -42,6 +126,7 @@ fn resign_and_verify_with_keys( fn build_parse_sign_and_verify( signing_key: &[u8], verification_key: &[u8], + pkg_out_path: impl AsRef, ) -> Result<(), Box> { let _ = env_logger::try_init(); @@ -71,14 +156,13 @@ fn build_parse_sign_and_verify( .requires(Dependency::any("rpm-sign".to_string())) .build()?; - let epoch = pkg.metadata.get_epoch()?; - assert_eq!(3, epoch); + assert_eq!(3, pkg.metadata.get_epoch()?); // sign - let signer = Signer::load_from_asc_bytes(signing_key.as_ref())?; + let signer: Signer = Signer::load_from_asc_bytes(signing_key.as_ref())?; pkg.sign(signer)?; - let out_file = common::cargo_out_dir().join("roundtrip.rpm"); + let out_file = common::cargo_out_dir().join(pkg_out_path.as_ref()); pkg.write_file(&out_file)?; // verify @@ -88,19 +172,3 @@ fn build_parse_sign_and_verify( Ok(()) } - -#[test] -fn parse_externally_signed_rpm_and_verify_rsa() -> Result<(), Box> { - let _ = env_logger::try_init(); - let (signing_key, verification_key) = common::load_rsa_keys(); - - build_parse_sign_and_verify(&signing_key, &verification_key) -} - -#[test] -fn parse_externally_signed_rpm_and_verify_eddsa() -> Result<(), Box> { - let _ = env_logger::try_init(); - let (signing_key, verification_key) = common::load_eddsa_keys(); - - build_parse_sign_and_verify(&signing_key, &verification_key) -}