Files
panda-rpki-oss/src/validation/manifest/tests.rs
Panda RPKI OSS Local 277cbca878
Some checks failed
ci / rust (push) Has been cancelled
ci / docker (push) Has been cancelled
初始化 Panda RPKI v0.1.0 开源候选版本
2026-09-09 18:01:15 +08:00

607 lines
20 KiB
Rust

//! 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<u8> {
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<u8>,
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<u8>, 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<u8>, 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<u8>,
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<u8>, 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<PackFile> {
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::<Vec<_>>();
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(&current_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::<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::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::<Vec<_>>();
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)
);
}
}