//! Manifest processing regression tests. use super::*; use crate::repository::current_repo_index::CurrentRepoIndex; use crate::repository::storage::{RawByHashEntry, RocksStore}; use std::path::PathBuf; fn fixture_repository() -> PathBuf { crate::test_support::synthetic_repository().case_repository("baseline-v1") } fn manifest_fixture_path() -> PathBuf { fixture_repository().join("child/child.mft") } fn issuer_ca_fixture_der() -> Vec { std::fs::read(fixture_repository().join("root/child.cer")).expect("read synthetic issuer CA") } fn issuer_ca_rsync_uri() -> &'static str { "rsync://rpki-local-rsync:873/custom/root/child.cer" } fn load_manifest_fixture() -> ( ManifestObject, Vec, String, String, time::OffsetDateTime, ) { let manifest_path = manifest_fixture_path(); let manifest_bytes = std::fs::read(&manifest_path).expect("read synthetic manifest"); let manifest = ManifestObject::decode_der(&manifest_bytes).expect("decode synthetic manifest"); let manifest_rsync_uri = "rsync://rpki-local-rsync:873/custom/child/child.mft".to_string(); let publication_point_rsync_uri = "rsync://rpki-local-rsync:873/custom/child/".to_string(); let validation_time = manifest.manifest.this_update + time::Duration::seconds(1); ( manifest, manifest_bytes, manifest_rsync_uri, publication_point_rsync_uri, validation_time, ) } fn raw_by_hash_entry(uri: &str, bytes: Vec, object_type: &str) -> RawByHashEntry { let mut entry = RawByHashEntry::from_bytes(hex::encode(sha2::Sha256::digest(&bytes)), bytes); entry.origin_uris.push(uri.to_string()); entry.object_type = Some(object_type.to_string()); entry.encoding = Some("der".to_string()); entry } fn put_current_object(store: &RocksStore, rsync_uri: &str, bytes: Vec, object_type: &str) { let hash = hex::encode(sha2::Sha256::digest(&bytes)); store .put_raw_by_hash_entry(&raw_by_hash_entry(rsync_uri, bytes, object_type)) .expect("put raw_by_hash entry"); store .put_repository_view_entry(&crate::repository::storage::RepositoryViewEntry { rsync_uri: rsync_uri.to_string(), current_hash: Some(hash), repository_source: Some("https://example.test/notification.xml".to_string()), object_type: Some(object_type.to_string()), state: crate::repository::storage::RepositoryViewState::Present, }) .expect("put repository view entry"); } fn put_complete_publication_point_current_objects( store: &RocksStore, manifest: &ManifestObject, manifest_rsync_uri: &str, manifest_bytes: Vec, publication_point_rsync_uri: &str, ) { put_current_object(store, manifest_rsync_uri, manifest_bytes, "mft"); for entry in manifest.manifest.parse_files().expect("parse files") { let file_path = manifest_fixture_path() .parent() .unwrap() .join(entry.file_name.as_str()); let bytes = std::fs::read(&file_path).expect("read fixture file"); let rsync_uri = format!("{publication_point_rsync_uri}{}", entry.file_name); let object_type = rsync_uri.rsplit('.').next().unwrap_or("bin"); put_current_object(store, &rsync_uri, bytes, object_type); } } fn put_raw_only(store: &RocksStore, rsync_uri: &str, bytes: Vec, object_type: &str) { store .put_raw_by_hash_entry(&raw_by_hash_entry(rsync_uri, bytes, object_type)) .expect("put raw_by_hash entry"); } fn locked_files_for_manifest( manifest: &ManifestObject, publication_point_rsync_uri: &str, ) -> Vec { let manifest_path = manifest_fixture_path(); manifest .manifest .parse_files() .expect("parse files") .into_iter() .map(|entry| { let file_path = manifest_path .parent() .unwrap() .join(entry.file_name.as_str()); let bytes = std::fs::read(&file_path).unwrap_or_else(|_| { panic!("read fixture file referenced by manifest: {file_path:?}") }); PackFile::from_bytes_compute_sha256( format!("{publication_point_rsync_uri}{}", entry.file_name), bytes, ) }) .collect() } #[test] fn try_build_fresh_publication_point_rejects_manifest_outside_publication_point() { let temp = tempfile::tempdir().expect("tempdir"); let store = RocksStore::open(temp.path()).expect("open rocksdb"); let (_, _, manifest_rsync_uri, _, validation_time) = load_manifest_fixture(); let err = try_build_fresh_publication_point( &store, &manifest_rsync_uri, "rsync://example.test/other/", &issuer_ca_fixture_der(), Some(issuer_ca_rsync_uri()), validation_time, ) .unwrap_err(); assert!( matches!( err, ManifestFreshError::ManifestOutsidePublicationPoint { .. } ), "{err}" ); } #[test] fn try_build_fresh_publication_point_reports_missing_manifest_when_raw_store_is_empty() { let temp = tempfile::tempdir().expect("tempdir"); let store = RocksStore::open(temp.path()).expect("open rocksdb"); let (_, _, manifest_rsync_uri, publication_point_rsync_uri, validation_time) = load_manifest_fixture(); let err = try_build_fresh_publication_point( &store, &manifest_rsync_uri, &publication_point_rsync_uri, &issuer_ca_fixture_der(), Some(issuer_ca_rsync_uri()), validation_time, ) .unwrap_err(); assert!( matches!(err, ManifestFreshError::MissingManifest { .. }), "{err}" ); } #[test] fn try_build_fresh_publication_point_reports_missing_locked_file() { let temp = tempfile::tempdir().expect("tempdir"); let store = RocksStore::open(temp.path()).expect("open rocksdb"); let ( manifest, manifest_bytes, manifest_rsync_uri, publication_point_rsync_uri, validation_time, ) = load_manifest_fixture(); put_current_object(&store, &manifest_rsync_uri, manifest_bytes, "mft"); let first_non_crl = manifest .manifest .parse_files() .expect("parse files") .into_iter() .find(|entry| !entry.file_name.ends_with(".crl")) .expect("fixture non-crl entry"); let file_path = manifest_fixture_path() .parent() .unwrap() .join(first_non_crl.file_name.as_str()); let bytes = std::fs::read(&file_path).expect("read fixture file"); let rsync_uri = format!("{publication_point_rsync_uri}{}", first_non_crl.file_name); let object_type = rsync_uri.rsplit('.').next().unwrap_or("bin"); put_current_object(&store, &rsync_uri, bytes, object_type); let err = try_build_fresh_publication_point( &store, &manifest_rsync_uri, &publication_point_rsync_uri, &issuer_ca_fixture_der(), Some(issuer_ca_rsync_uri()), validation_time, ) .unwrap_err(); assert!( matches!(err, ManifestFreshError::MissingFile { .. }), "{err}" ); } #[test] fn try_build_fresh_publication_point_detects_hash_mismatch_via_repository_view_hash() { let temp = tempfile::tempdir().expect("tempdir"); let store = RocksStore::open(temp.path()).expect("open rocksdb"); let ( manifest, manifest_bytes, manifest_rsync_uri, publication_point_rsync_uri, validation_time, ) = load_manifest_fixture(); put_current_object(&store, &manifest_rsync_uri, manifest_bytes, "mft"); let entries = manifest .manifest .parse_files() .expect("parse files") .into_iter() .collect::>(); let first = entries .iter() .find(|entry| !entry.file_name.ends_with(".crl")) .expect("synthetic manifest non-CRL entry"); let first_uri = format!("{publication_point_rsync_uri}{}", first.file_name); let object_type = first_uri.rsplit('.').next().unwrap_or("bin"); put_current_object( &store, &first_uri, b"wrong object bytes".to_vec(), object_type, ); for entry in entries .iter() .filter(|entry| entry.file_name != first.file_name) { let file_path = manifest_fixture_path() .parent() .unwrap() .join(entry.file_name.as_str()); let bytes = std::fs::read(&file_path).expect("read fixture file"); let rsync_uri = format!("{publication_point_rsync_uri}{}", entry.file_name); let object_type = rsync_uri.rsplit('.').next().unwrap_or("bin"); put_current_object(&store, &rsync_uri, bytes, object_type); } let err = try_build_fresh_publication_point( &store, &manifest_rsync_uri, &publication_point_rsync_uri, &issuer_ca_fixture_der(), Some(issuer_ca_rsync_uri()), validation_time, ) .unwrap_err(); assert!( matches!(err, ManifestFreshError::HashMismatch { .. }), "{err}" ); } #[test] fn try_build_fresh_publication_point_uses_current_repo_index_without_repository_view() { let temp = tempfile::tempdir().expect("tempdir"); let store = RocksStore::open(temp.path()).expect("open rocksdb"); let ( manifest, manifest_bytes, manifest_rsync_uri, publication_point_rsync_uri, validation_time, ) = load_manifest_fixture(); put_raw_only(&store, &manifest_rsync_uri, manifest_bytes.clone(), "mft"); let current_index = CurrentRepoIndex::shared(); let mut entries = vec![crate::repository::storage::RepositoryViewEntry { rsync_uri: manifest_rsync_uri.clone(), current_hash: Some(hex::encode(sha2::Sha256::digest(&manifest_bytes))), repository_source: Some("https://example.test/notification.xml".to_string()), object_type: Some("mft".to_string()), state: crate::repository::storage::RepositoryViewState::Present, }]; for entry in manifest.manifest.parse_files().expect("parse files") { let file_path = manifest_fixture_path() .parent() .unwrap() .join(entry.file_name.as_str()); let bytes = std::fs::read(&file_path).expect("read fixture file"); let rsync_uri = format!("{publication_point_rsync_uri}{}", entry.file_name); let object_type = rsync_uri.rsplit('.').next().unwrap_or("bin").to_string(); put_raw_only(&store, &rsync_uri, bytes.clone(), &object_type); entries.push(crate::repository::storage::RepositoryViewEntry { rsync_uri, current_hash: Some(hex::encode(sha2::Sha256::digest(&bytes))), repository_source: Some("https://example.test/notification.xml".to_string()), object_type: Some(object_type), state: crate::repository::storage::RepositoryViewState::Present, }); } current_index .write() .expect("index write lock") .apply_repository_view_entries(&entries) .expect("apply current index"); assert!( store .get_repository_view_entry(&manifest_rsync_uri) .expect("get repository view") .is_none() ); let (fresh, _timing) = try_build_fresh_publication_point_with_timing( &store, &manifest_rsync_uri, &publication_point_rsync_uri, Some(¤t_index), &issuer_ca_fixture_der(), Some(issuer_ca_rsync_uri()), validation_time, ) .expect("fresh publication point via current index"); assert_eq!(fresh.manifest_rsync_uri, manifest_rsync_uri); assert_eq!(fresh.files.len(), manifest.manifest.file_count()); } #[test] fn try_build_fresh_publication_point_records_anti_rollback_meta_miss() { let temp = tempfile::tempdir().expect("tempdir"); let store = RocksStore::open(temp.path()).expect("open rocksdb"); let ( manifest, manifest_bytes, manifest_rsync_uri, publication_point_rsync_uri, validation_time, ) = load_manifest_fixture(); put_complete_publication_point_current_objects( &store, &manifest, &manifest_rsync_uri, manifest_bytes, &publication_point_rsync_uri, ); let (_fresh, timing) = try_build_fresh_publication_point_with_timing( &store, &manifest_rsync_uri, &publication_point_rsync_uri, None, &issuer_ca_fixture_der(), Some(issuer_ca_rsync_uri()), validation_time, ) .expect("fresh publication point without freshness metadata"); assert!(timing.anti_rollback_meta_miss); assert!(!timing.anti_rollback_meta_hit); } #[test] fn try_build_fresh_publication_point_uses_anti_rollback_meta_hit_for_same_manifest() { let temp = tempfile::tempdir().expect("tempdir"); let store = RocksStore::open(temp.path()).expect("open rocksdb"); let ( manifest, manifest_bytes, manifest_rsync_uri, publication_point_rsync_uri, validation_time, ) = load_manifest_fixture(); let previous_meta = sample_manifest_meta( &manifest, &manifest_rsync_uri, &publication_point_rsync_uri, &manifest_bytes, validation_time, ); store .put_manifest_anti_rollback_meta(&previous_meta) .expect("persist freshness"); put_complete_publication_point_current_objects( &store, &manifest, &manifest_rsync_uri, manifest_bytes, &publication_point_rsync_uri, ); let (_fresh, timing) = try_build_fresh_publication_point_with_timing( &store, &manifest_rsync_uri, &publication_point_rsync_uri, None, &issuer_ca_fixture_der(), Some(issuer_ca_rsync_uri()), validation_time, ) .expect("fresh publication point with matching freshness metadata"); assert!(timing.anti_rollback_meta_hit); assert!(!timing.anti_rollback_meta_miss); } #[test] fn validate_manifest_embedded_ee_cert_path_rejects_missing_crl_files() { let (manifest, _, _, publication_point_rsync_uri, validation_time) = load_manifest_fixture(); let files = locked_files_for_manifest(&manifest, &publication_point_rsync_uri) .into_iter() .filter(|f| !f.rsync_uri.ends_with(".crl")) .collect::>(); let err = validate_manifest_embedded_ee_cert_path( &manifest, &files, &issuer_ca_fixture_der(), Some(issuer_ca_rsync_uri()), validation_time, ) .unwrap_err(); assert!(matches!(err, ManifestFreshError::NoCrlFiles), "{err}"); } #[test] fn validate_manifest_embedded_ee_cert_path_rejects_missing_ee_crldp() { let (mut manifest, _, _, publication_point_rsync_uri, validation_time) = load_manifest_fixture(); manifest.signed_object.signed_data.certificates[0] .resource_cert .tbs .extensions .crl_distribution_points_uris = None; let files = locked_files_for_manifest(&manifest, &publication_point_rsync_uri); let err = validate_manifest_embedded_ee_cert_path( &manifest, &files, &issuer_ca_fixture_der(), Some(issuer_ca_rsync_uri()), validation_time, ) .unwrap_err(); assert!(matches!(err, ManifestFreshError::EeCrlDpMissing), "{err}"); } #[test] fn validate_manifest_embedded_ee_cert_path_rejects_unlisted_crldp_uri() { let (manifest, _, _, publication_point_rsync_uri, validation_time) = load_manifest_fixture(); let mut files = locked_files_for_manifest(&manifest, &publication_point_rsync_uri) .into_iter() .filter(|f| !f.rsync_uri.ends_with(".crl")) .collect::>(); files.push(PackFile::from_bytes_compute_sha256( "rsync://example.test/repo/unrelated.crl", b"dummy".to_vec(), )); let err = validate_manifest_embedded_ee_cert_path( &manifest, &files, &issuer_ca_fixture_der(), Some(issuer_ca_rsync_uri()), validation_time, ) .unwrap_err(); assert!(matches!(err, ManifestFreshError::EeCrlNotFound(_)), "{err}"); } #[test] fn validate_manifest_embedded_ee_cert_path_rejects_expired_crl() { let (manifest, _, _, publication_point_rsync_uri, _) = load_manifest_fixture(); let files = locked_files_for_manifest(&manifest, &publication_point_rsync_uri); let ee = &manifest.signed_object.signed_data.certificates[0]; let crldp_uri = ee .resource_cert .tbs .extensions .crl_distribution_points_uris .as_ref() .and_then(|uris| uris.first()) .expect("fixture manifest EE CRLDP") .as_str() .to_string(); let crl_file = files .iter() .find(|file| file.rsync_uri == crldp_uri) .expect("fixture CRL referenced by manifest EE"); let crl = crate::model::crl::RpkixCrl::decode_der(crl_file.bytes().expect("read fixture crl bytes")) .expect("decode fixture crl"); let validation_time = crl.next_update.utc; let err = validate_manifest_embedded_ee_cert_path( &manifest, &files, &issuer_ca_fixture_der(), Some(issuer_ca_rsync_uri()), validation_time, ) .unwrap_err(); assert!( matches!( err, ManifestFreshError::EeCertPath( crate::validation::cert_path::CertPathError::CrlNotValidAtTime ) ), "{err}" ); } fn sample_manifest_meta( manifest: &ManifestObject, uri: &str, _base: &str, bytes: &[u8], now: time::OffsetDateTime, ) -> crate::repository::storage::ManifestAntiRollbackMeta { crate::repository::storage::ManifestAntiRollbackMeta { manifest_rsync_uri: uri.into(), manifest_number_be: manifest.manifest.manifest_number.bytes_be.clone(), manifest_this_update: PackTime::from_utc_offset_datetime(manifest.manifest.this_update), manifest_sha256: sha2::Sha256::digest(bytes).to_vec(), updated_at_validation_time: PackTime::from_utc_offset_datetime(now), } } #[test] fn history_survives_restart_and_rejects_rollback_without_advancing() { for case in 0..4 { let dir = tempfile::tempdir().unwrap(); let (manifest, bytes, uri, base, now) = load_manifest_fixture(); let mut meta = sample_manifest_meta(&manifest, &uri, &base, &bytes, now); match case { 0 => {} 1 => { meta.manifest_number_be = vec![0x7f; 20]; } 2 => { meta.manifest_sha256 = vec![0x55; 32]; } 3 => { meta.manifest_number_be = vec![0]; } _ => unreachable!(), } { let store = RocksStore::open(dir.path()).unwrap(); store.put_manifest_anti_rollback_meta(&meta).unwrap(); put_complete_publication_point_current_objects( &store, &manifest, &uri, bytes.clone(), &base, ); } let store = RocksStore::open(dir.path()).unwrap(); assert_eq!( store.get_manifest_anti_rollback_meta(&uri).unwrap(), Some(meta.clone()) ); let result = try_build_fresh_publication_point_with_timing( &store, &uri, &base, None, &issuer_ca_fixture_der(), Some(issuer_ca_rsync_uri()), now, ); match case { 0 => { assert!(result.unwrap().1.anti_rollback_meta_hit); } 1 | 2 => { assert!(matches!( result, Err(ManifestFreshError::ManifestNumberNotIncreasing { .. }) )); } 3 => { assert!(matches!( result, Err(ManifestFreshError::ThisUpdateNotIncreasing { .. }) )); } _ => unreachable!(), } assert_eq!( store.get_manifest_anti_rollback_meta(&uri).unwrap(), Some(meta) ); } }