rpki/tests/test_manifest_processor_m4.rs

584 lines
21 KiB
Rust

use std::path::Path;
use sha2::Digest;
use rpki::data_model::manifest::ManifestObject;
use rpki::policy::{CaFailedFetchPolicy, Policy};
use rpki::storage::{
PackTime, RawByHashEntry, RocksStore, ValidatedCaInstanceResult, ValidatedManifestMeta,
VcirArtifactKind, VcirArtifactRole, VcirArtifactValidationStatus, VcirAuditSummary,
VcirInstanceGate, VcirRelatedArtifact, VcirSummary,
};
use rpki::validation::manifest::{PublicationPointSource, process_manifest_publication_point};
fn issuer_ca_fixture() -> Vec<u8> {
std::fs::read(
"tests/fixtures/repository/rpki.apnic.net/repository/B527EF581D6611E2BB468F7C72FD1FF2/BfycW4hQb3wNP4YsiJW-1n6fjro.cer",
)
.expect("read issuer ca fixture")
}
fn issuer_ca_rsync_uri() -> &'static str {
"rsync://rpki.apnic.net/repository/B527EF581D6611E2BB468F7C72FD1FF2/BfycW4hQb3wNP4YsiJW-1n6fjro.cer"
}
fn fixture_to_rsync_uri(path: &Path) -> String {
let rel = path
.strip_prefix("tests/fixtures/repository")
.expect("path under tests/fixtures/repository");
let mut it = rel.components();
let host = it
.next()
.expect("host component")
.as_os_str()
.to_string_lossy();
let rest = it.as_path().to_string_lossy();
format!("rsync://{host}/{rest}")
}
fn fixture_dir_to_rsync_uri(dir: &Path) -> String {
let mut s = fixture_to_rsync_uri(dir);
if !s.ends_with('/') {
s.push('/');
}
s
}
fn store_validated_manifest_baseline(
store: &RocksStore,
manifest_rsync_uri: &str,
manifest_bytes: &[u8],
manifest_number_be: Vec<u8>,
this_update: time::OffsetDateTime,
next_update: time::OffsetDateTime,
) {
let manifest_sha256 = hex::encode(sha2::Sha256::digest(manifest_bytes));
let mut manifest_raw =
RawByHashEntry::from_bytes(manifest_sha256.clone(), manifest_bytes.to_vec());
manifest_raw
.origin_uris
.push(manifest_rsync_uri.to_string());
manifest_raw.object_type = Some("mft".to_string());
manifest_raw.encoding = Some("der".to_string());
store
.put_raw_by_hash_entry(&manifest_raw)
.expect("store VCIR manifest raw_by_hash");
let vcir = ValidatedCaInstanceResult {
manifest_rsync_uri: manifest_rsync_uri.to_string(),
parent_manifest_rsync_uri: None,
tal_id: "test-tal".to_string(),
ca_subject_name: "CN=test".to_string(),
ca_ski: "aa".to_string(),
issuer_ski: "aa".to_string(),
last_successful_validation_time: PackTime::from_utc_offset_datetime(this_update),
current_manifest_rsync_uri: manifest_rsync_uri.to_string(),
current_crl_rsync_uri: format!("{manifest_rsync_uri}.crl"),
validated_manifest_meta: ValidatedManifestMeta {
validated_manifest_number: manifest_number_be,
validated_manifest_this_update: PackTime::from_utc_offset_datetime(this_update),
validated_manifest_next_update: PackTime::from_utc_offset_datetime(next_update),
},
instance_gate: VcirInstanceGate {
manifest_next_update: PackTime::from_utc_offset_datetime(next_update),
current_crl_next_update: PackTime::from_utc_offset_datetime(next_update),
self_ca_not_after: PackTime::from_utc_offset_datetime(next_update),
instance_effective_until: PackTime::from_utc_offset_datetime(next_update),
},
child_entries: Vec::new(),
local_outputs: Vec::new(),
related_artifacts: vec![VcirRelatedArtifact {
artifact_role: VcirArtifactRole::Manifest,
artifact_kind: VcirArtifactKind::Mft,
uri: Some(manifest_rsync_uri.to_string()),
sha256: manifest_sha256,
object_type: Some("mft".to_string()),
validation_status: VcirArtifactValidationStatus::Accepted,
}],
summary: VcirSummary {
local_vrp_count: 0,
local_aspa_count: 0,
child_count: 0,
accepted_object_count: 1,
rejected_object_count: 0,
},
audit_summary: VcirAuditSummary {
failed_fetch_eligible: true,
last_failed_fetch_reason: None,
warning_count: 0,
audit_flags: Vec::new(),
},
};
store
.put_vcir(&vcir)
.expect("store validated manifest baseline");
}
#[test]
fn manifest_success_returns_validated_publication_point_data() {
let manifest_path = Path::new(
"tests/fixtures/repository/rpki.cernet.net/repo/cernet/0/05FC9C5B88506F7C0D3F862C8895BED67E9F8EBA.mft",
);
let manifest_bytes = std::fs::read(manifest_path).expect("read manifest fixture");
let manifest = ManifestObject::decode_der(&manifest_bytes).expect("decode manifest fixture");
let validation_time = manifest.manifest.this_update + time::Duration::seconds(1);
let manifest_rsync_uri = fixture_to_rsync_uri(manifest_path);
let publication_point_rsync_uri = fixture_dir_to_rsync_uri(manifest_path.parent().unwrap());
let temp = tempfile::tempdir().expect("tempdir");
let store = RocksStore::open(temp.path()).expect("open rocksdb");
store
.put_raw(&manifest_rsync_uri, &manifest_bytes)
.expect("store manifest");
let entries = manifest
.manifest
.parse_files()
.expect("parse validated manifest fileList");
for entry in &entries {
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:?}"));
let rsync_uri = format!("{publication_point_rsync_uri}{}", entry.file_name);
store.put_raw(&rsync_uri, &bytes).expect("store file");
}
let policy = Policy::default();
let issuer_ca_der = issuer_ca_fixture();
let out = process_manifest_publication_point(
&store,
&policy,
&manifest_rsync_uri,
&publication_point_rsync_uri,
&issuer_ca_der,
Some(issuer_ca_rsync_uri()),
validation_time,
)
.expect("process manifest publication point");
assert_eq!(out.source, PublicationPointSource::Fresh);
assert!(out.warnings.is_empty());
assert_eq!(out.snapshot.manifest_rsync_uri, manifest_rsync_uri);
assert_eq!(
out.snapshot.publication_point_rsync_uri,
publication_point_rsync_uri
);
}
#[test]
fn manifest_hash_mismatch_reuses_current_instance_vcir_when_enabled() {
let manifest_path = Path::new(
"tests/fixtures/repository/rpki.cernet.net/repo/cernet/0/05FC9C5B88506F7C0D3F862C8895BED67E9F8EBA.mft",
);
let manifest_bytes = std::fs::read(manifest_path).expect("read manifest fixture");
let manifest = ManifestObject::decode_der(&manifest_bytes).expect("decode manifest fixture");
let validation_time = manifest.manifest.this_update + time::Duration::seconds(1);
let manifest_rsync_uri = fixture_to_rsync_uri(manifest_path);
let publication_point_rsync_uri = fixture_dir_to_rsync_uri(manifest_path.parent().unwrap());
let temp = tempfile::tempdir().expect("tempdir");
let store = RocksStore::open(temp.path()).expect("open rocksdb");
store
.put_raw(&manifest_rsync_uri, &manifest_bytes)
.expect("store manifest");
let entries = manifest
.manifest
.parse_files()
.expect("parse validated manifest fileList");
for entry in &entries {
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:?}"));
let rsync_uri = format!("{publication_point_rsync_uri}{}", entry.file_name);
store.put_raw(&rsync_uri, &bytes).expect("store file");
}
let policy = Policy::default();
let issuer_ca_der = issuer_ca_fixture();
let first = process_manifest_publication_point(
&store,
&policy,
&manifest_rsync_uri,
&publication_point_rsync_uri,
&issuer_ca_der,
Some(issuer_ca_rsync_uri()),
validation_time,
)
.expect("first run returns validated publication point");
assert_eq!(first.source, PublicationPointSource::Fresh);
store_validated_manifest_baseline(
&store,
&manifest_rsync_uri,
&manifest_bytes,
manifest.manifest.manifest_number.bytes_be.clone(),
manifest.manifest.this_update,
manifest.manifest.next_update,
);
let entries = manifest
.manifest
.parse_files()
.expect("parse validated manifest fileList");
let victim = entries.first().expect("non-empty file list");
let victim_uri = format!("{publication_point_rsync_uri}{}", victim.file_name);
let mut tampered = store
.get_raw(&victim_uri)
.expect("get victim raw")
.expect("victim raw exists");
tampered[0] ^= 0xFF;
store.put_raw(&victim_uri, &tampered).expect("tamper raw");
let second = process_manifest_publication_point(
&store,
&policy,
&manifest_rsync_uri,
&publication_point_rsync_uri,
&issuer_ca_der,
Some(issuer_ca_rsync_uri()),
validation_time,
)
.expect("second run reuses current-instance VCIR");
assert_eq!(second.source, PublicationPointSource::VcirCurrentInstance);
assert!(
second.warnings.iter().any(|w| w
.message
.contains("using latest validated result for current CA instance")),
"expected current-instance VCIR reuse warning"
);
}
#[test]
fn manifest_failed_fetch_stop_all_output() {
let manifest_path = Path::new(
"tests/fixtures/repository/rpki.cernet.net/repo/cernet/0/05FC9C5B88506F7C0D3F862C8895BED67E9F8EBA.mft",
);
let manifest_bytes = std::fs::read(manifest_path).expect("read manifest fixture");
let manifest = ManifestObject::decode_der(&manifest_bytes).expect("decode manifest fixture");
let validation_time = manifest.manifest.this_update + time::Duration::seconds(1);
let manifest_rsync_uri = fixture_to_rsync_uri(manifest_path);
let publication_point_rsync_uri = fixture_dir_to_rsync_uri(manifest_path.parent().unwrap());
let temp = tempfile::tempdir().expect("tempdir");
let store = RocksStore::open(temp.path()).expect("open rocksdb");
store
.put_raw(&manifest_rsync_uri, &manifest_bytes)
.expect("store manifest");
let entries = manifest
.manifest
.parse_files()
.expect("parse validated manifest fileList");
for entry in &entries {
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:?}"));
let rsync_uri = format!("{publication_point_rsync_uri}{}", entry.file_name);
store.put_raw(&rsync_uri, &bytes).expect("store file");
}
let mut policy = Policy::default();
policy.ca_failed_fetch_policy = CaFailedFetchPolicy::ReuseCurrentInstanceVcir;
let issuer_ca_der = issuer_ca_fixture();
let _ = process_manifest_publication_point(
&store,
&policy,
&manifest_rsync_uri,
&publication_point_rsync_uri,
&issuer_ca_der,
Some(issuer_ca_rsync_uri()),
validation_time,
)
.expect("first run returns validated publication point");
let entries = manifest
.manifest
.parse_files()
.expect("parse validated manifest fileList");
let victim = entries.first().expect("non-empty file list");
let victim_uri = format!("{publication_point_rsync_uri}{}", victim.file_name);
let mut tampered = store
.get_raw(&victim_uri)
.expect("get victim raw")
.expect("victim raw exists");
tampered[0] ^= 0xFF;
store.put_raw(&victim_uri, &tampered).expect("tamper raw");
policy.ca_failed_fetch_policy = CaFailedFetchPolicy::StopAllOutput;
let err = process_manifest_publication_point(
&store,
&policy,
&manifest_rsync_uri,
&publication_point_rsync_uri,
&issuer_ca_der,
Some(issuer_ca_rsync_uri()),
validation_time,
)
.expect_err("stop_all_output should not reuse current-instance VCIR");
let msg = err.to_string();
assert!(msg.contains("cache use is disabled"));
}
#[test]
fn manifest_failed_fetch_rejects_stale_current_instance_vcir() {
let manifest_path = Path::new(
"tests/fixtures/repository/rpki.cernet.net/repo/cernet/0/05FC9C5B88506F7C0D3F862C8895BED67E9F8EBA.mft",
);
let manifest_bytes = std::fs::read(manifest_path).expect("read manifest fixture");
let manifest = ManifestObject::decode_der(&manifest_bytes).expect("decode manifest fixture");
let ok_time = manifest.manifest.this_update + time::Duration::seconds(1);
let stale_time = manifest.manifest.next_update + time::Duration::seconds(1);
let manifest_rsync_uri = fixture_to_rsync_uri(manifest_path);
let publication_point_rsync_uri = fixture_dir_to_rsync_uri(manifest_path.parent().unwrap());
let temp = tempfile::tempdir().expect("tempdir");
let store = RocksStore::open(temp.path()).expect("open rocksdb");
store
.put_raw(&manifest_rsync_uri, &manifest_bytes)
.expect("store manifest");
let entries = manifest
.manifest
.parse_files()
.expect("parse validated manifest fileList");
for entry in &entries {
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:?}"));
let rsync_uri = format!("{publication_point_rsync_uri}{}", entry.file_name);
store.put_raw(&rsync_uri, &bytes).expect("store file");
}
let policy = Policy::default();
let issuer_ca_der = issuer_ca_fixture();
let _ = process_manifest_publication_point(
&store,
&policy,
&manifest_rsync_uri,
&publication_point_rsync_uri,
&issuer_ca_der,
Some(issuer_ca_rsync_uri()),
ok_time,
)
.expect("first run returns validated publication point");
store_validated_manifest_baseline(
&store,
&manifest_rsync_uri,
&manifest_bytes,
manifest.manifest.manifest_number.bytes_be.clone(),
manifest.manifest.this_update,
manifest.manifest.next_update,
);
store
.delete_raw(&manifest_rsync_uri)
.expect("delete manifest raw to force fallback");
let err = process_manifest_publication_point(
&store,
&policy,
&manifest_rsync_uri,
&publication_point_rsync_uri,
&issuer_ca_der,
Some(issuer_ca_rsync_uri()),
stale_time,
)
.expect_err("stale validation_time must reject current-instance VCIR reuse");
let msg = err.to_string();
assert!(msg.contains("instance_gate expired"), "{msg}");
}
#[test]
fn manifest_revalidation_with_unchanged_manifest_is_fresh() {
let manifest_path = Path::new(
"tests/fixtures/repository/rpki.cernet.net/repo/cernet/0/05FC9C5B88506F7C0D3F862C8895BED67E9F8EBA.mft",
);
let manifest_bytes = std::fs::read(manifest_path).expect("read manifest fixture");
let manifest = ManifestObject::decode_der(&manifest_bytes).expect("decode manifest fixture");
let t1 = manifest.manifest.this_update + time::Duration::seconds(1);
let t2 = manifest.manifest.this_update + time::Duration::seconds(2);
let manifest_rsync_uri = fixture_to_rsync_uri(manifest_path);
let publication_point_rsync_uri = fixture_dir_to_rsync_uri(manifest_path.parent().unwrap());
let temp = tempfile::tempdir().expect("tempdir");
let store = RocksStore::open(temp.path()).expect("open rocksdb");
store
.put_raw(&manifest_rsync_uri, &manifest_bytes)
.expect("store manifest");
let entries = manifest
.manifest
.parse_files()
.expect("parse validated manifest fileList");
for entry in &entries {
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:?}"));
let rsync_uri = format!("{publication_point_rsync_uri}{}", entry.file_name);
store.put_raw(&rsync_uri, &bytes).expect("store file");
}
let policy = Policy::default();
let issuer_ca_der = issuer_ca_fixture();
let first = process_manifest_publication_point(
&store,
&policy,
&manifest_rsync_uri,
&publication_point_rsync_uri,
&issuer_ca_der,
Some(issuer_ca_rsync_uri()),
t1,
)
.expect("first run returns validated publication point");
assert_eq!(first.source, PublicationPointSource::Fresh);
store_validated_manifest_baseline(
&store,
&manifest_rsync_uri,
&manifest_bytes,
first.snapshot.manifest_number_be.clone(),
manifest.manifest.this_update,
manifest.manifest.next_update,
);
let second = process_manifest_publication_point(
&store,
&policy,
&manifest_rsync_uri,
&publication_point_rsync_uri,
&issuer_ca_der,
Some(issuer_ca_rsync_uri()),
t2,
)
.expect("second run should accept revalidation of the same manifest");
assert_eq!(second.source, PublicationPointSource::Fresh);
assert!(second.warnings.is_empty());
assert_eq!(
second.snapshot.manifest_bytes,
first.snapshot.manifest_bytes
);
assert_eq!(
second.snapshot.manifest_number_be,
first.snapshot.manifest_number_be
);
assert_eq!(second.snapshot.files, first.snapshot.files);
}
#[test]
fn manifest_rollback_is_treated_as_failed_fetch_and_reuses_current_instance_vcir() {
let manifest_path = Path::new(
"tests/fixtures/repository/rpki.cernet.net/repo/cernet/0/05FC9C5B88506F7C0D3F862C8895BED67E9F8EBA.mft",
);
let manifest_bytes = std::fs::read(manifest_path).expect("read manifest fixture");
let manifest = ManifestObject::decode_der(&manifest_bytes).expect("decode manifest fixture");
let t1 = manifest.manifest.this_update + time::Duration::seconds(1);
let t2 = manifest.manifest.this_update + time::Duration::seconds(2);
let manifest_rsync_uri = fixture_to_rsync_uri(manifest_path);
let publication_point_rsync_uri = fixture_dir_to_rsync_uri(manifest_path.parent().unwrap());
let temp = tempfile::tempdir().expect("tempdir");
let store = RocksStore::open(temp.path()).expect("open rocksdb");
store
.put_raw(&manifest_rsync_uri, &manifest_bytes)
.expect("store manifest");
let entries = manifest
.manifest
.parse_files()
.expect("parse validated manifest fileList");
for entry in &entries {
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:?}"));
let rsync_uri = format!("{publication_point_rsync_uri}{}", entry.file_name);
store.put_raw(&rsync_uri, &bytes).expect("store file");
}
let policy = Policy::default();
let issuer_ca_der = issuer_ca_fixture();
let first = process_manifest_publication_point(
&store,
&policy,
&manifest_rsync_uri,
&publication_point_rsync_uri,
&issuer_ca_der,
Some(issuer_ca_rsync_uri()),
t1,
)
.expect("first run returns validated publication point");
assert_eq!(first.source, PublicationPointSource::Fresh);
// Simulate a previously validated manifest with a higher manifestNumber (rollback detection).
let mut bumped = first.snapshot.clone();
// Deterministically bump the cached manifestNumber to be strictly greater than the current one.
for i in (0..bumped.manifest_number_be.len()).rev() {
let (v, carry) = bumped.manifest_number_be[i].overflowing_add(1);
bumped.manifest_number_be[i] = v;
if !carry {
break;
}
if i == 0 {
bumped.manifest_number_be.insert(0, 1);
break;
}
}
store_validated_manifest_baseline(
&store,
&manifest_rsync_uri,
&manifest_bytes,
bumped.manifest_number_be.clone(),
manifest.manifest.this_update,
manifest.manifest.next_update,
);
let second = process_manifest_publication_point(
&store,
&policy,
&manifest_rsync_uri,
&publication_point_rsync_uri,
&issuer_ca_der,
Some(issuer_ca_rsync_uri()),
t2,
)
.expect("second run should treat rollback as failed fetch and reuse current-instance VCIR");
assert_eq!(second.source, PublicationPointSource::VcirCurrentInstance);
assert!(
second
.warnings
.iter()
.any(|w| w.message.contains("manifestNumber not higher")),
"expected warning mentioning manifestNumber monotonicity"
);
}