将fetch pp cache改成使用vcir结构,跑通apnic全量同步

This commit is contained in:
2026-03-13 14:45:41 +08:00
parent e3339533b8
commit cf764c35bb
53 changed files with 8931 additions and 2201 deletions

View File

@@ -6,8 +6,8 @@ use rpki::data_model::rc::{
};
use rpki::data_model::roa::RoaObject;
use rpki::storage::PackFile;
use rpki::storage::RocksStore;
use rpki::storage::pack::PackFile;
use std::path::{Path, PathBuf};
use std::time::Instant;

View File

@@ -6,7 +6,7 @@ use std::time::{Duration, Instant};
use rpki::fetch::http::{BlockingHttpFetcher, HttpFetcherConfig};
use rpki::fetch::rsync::{RsyncFetchError, RsyncFetcher};
use rpki::policy::{CaFailedFetchPolicy, Policy, SyncPreference};
use rpki::storage::{FetchCachePpKey, RocksStore};
use rpki::storage::RocksStore;
use rpki::sync::repo::{RepoSyncSource, sync_publication_point};
use rpki::sync::rrdp::{Fetcher, parse_notification, sync_from_notification};
use rpki::validation::from_tal::discover_root_ca_instance_from_tal_url;
@@ -80,13 +80,13 @@ impl Fetcher for CountingDenyUriFetcher {
fn live_policy() -> Policy {
let mut p = Policy::default();
p.sync_preference = SyncPreference::RrdpThenRsync;
p.ca_failed_fetch_policy = CaFailedFetchPolicy::UseFetchCachePp;
p.ca_failed_fetch_policy = CaFailedFetchPolicy::ReuseCurrentInstanceVcir;
p
}
#[test]
#[ignore = "live network: APNIC RRDP snapshot bootstrap into persistent RocksDB"]
fn apnic_live_bootstrap_snapshot_and_fetch_cache_pp_pack_to_persistent_db() {
fn apnic_live_bootstrap_snapshot_and_persist_root_vcir_to_persistent_db() {
let http = live_http_fetcher();
let rsync = AlwaysFailRsyncFetcher;
@@ -120,8 +120,8 @@ fn apnic_live_bootstrap_snapshot_and_fetch_cache_pp_pack_to_persistent_db() {
assert_eq!(sync.source, RepoSyncSource::Rrdp);
// Build + persist a fetch_cache_pp pack for the root publication point so later runs can
// validate behavior under failed fetch conditions (RFC 9286 §6.6).
// Build the root publication point and persist the latest VCIR so later runs can
// validate current-instance failed-fetch reuse behavior (RFC 9286 §6.6).
let ta_der = discovery.trust_anchor.ta_certificate.raw_der;
let pp = process_manifest_publication_point(
&store,
@@ -136,9 +136,10 @@ fn apnic_live_bootstrap_snapshot_and_fetch_cache_pp_pack_to_persistent_db() {
assert_eq!(pp.source, PublicationPointSource::Fresh);
let key = FetchCachePpKey::from_manifest_rsync_uri(&ca_instance.manifest_rsync_uri);
let cached = store.get_fetch_cache_pp(&key).expect("get fetch_cache_pp");
assert!(cached.is_some(), "expected fetch_cache_pp to be stored");
let cached = store
.get_vcir(&ca_instance.manifest_rsync_uri)
.expect("get vcir");
assert!(cached.is_some(), "expected VCIR to be stored");
eprintln!(
"OK: bootstrap complete; persistent db at: {}",
@@ -262,15 +263,15 @@ fn apnic_live_delta_only_from_persistent_db() {
}
#[test]
#[ignore = "offline/synthetic: after bootstrap, force repo sync failure and assert fetch_cache_pp is used (RFC 9286 §6.6)"]
fn apnic_root_repo_sync_failure_uses_fetch_cache_pp_pack() {
#[ignore = "offline/synthetic: after bootstrap, force repo sync failure and assert current-instance VCIR is reused (RFC 9286 §6.6)"]
fn apnic_root_repo_sync_failure_reuses_current_instance_vcir() {
let http = live_http_fetcher();
let db_dir = persistent_db_dir();
let store = RocksStore::open(&db_dir).expect("open rocksdb (must have been bootstrapped)");
let mut policy = live_policy();
policy.sync_preference = SyncPreference::RrdpThenRsync;
policy.ca_failed_fetch_policy = CaFailedFetchPolicy::UseFetchCachePp;
policy.ca_failed_fetch_policy = CaFailedFetchPolicy::ReuseCurrentInstanceVcir;
let validation_time = time::OffsetDateTime::now_utc();
@@ -278,12 +279,13 @@ fn apnic_root_repo_sync_failure_uses_fetch_cache_pp_pack() {
.expect("discover root CA instance from APNIC TAL");
let ca_instance = discovery.ca_instance;
// Ensure cache exists (created by bootstrap).
let key = FetchCachePpKey::from_manifest_rsync_uri(&ca_instance.manifest_rsync_uri);
let cached = store.get_fetch_cache_pp(&key).expect("get fetch_cache_pp");
// Ensure current-instance VCIR exists (created by bootstrap).
let cached = store
.get_vcir(&ca_instance.manifest_rsync_uri)
.expect("get vcir");
assert!(
cached.is_some(),
"missing fetch_cache_pp; run bootstrap test first. db_dir={}",
"missing VCIR; run bootstrap test first. db_dir={}",
db_dir.display()
);
@@ -301,13 +303,13 @@ fn apnic_root_repo_sync_failure_uses_fetch_cache_pp_pack() {
false,
Some("synthetic repo sync failure"),
)
.expect("must fall back to fetch_cache_pp");
.expect("must reuse current-instance VCIR");
assert_eq!(pp.source, PublicationPointSource::FetchCachePp);
assert_eq!(pp.source, PublicationPointSource::VcirCurrentInstance);
assert!(
pp.warnings
.iter()
.any(|w| w.message.contains("using fetch_cache_pp")),
"expected cache-use warning"
pp.warnings.iter().any(|w| w
.message
.contains("using latest validated result for current CA instance")),
"expected current-instance VCIR reuse warning"
);
}

View File

@@ -63,37 +63,39 @@ impl LiveStats {
rpki::validation::manifest::PublicationPointSource::Fresh => {
self.publication_points_fresh += 1
}
rpki::validation::manifest::PublicationPointSource::FetchCachePp => {
rpki::validation::manifest::PublicationPointSource::VcirCurrentInstance => {
self.publication_points_cached += 1
}
rpki::validation::manifest::PublicationPointSource::FailedFetchNoCache => {}
}
// Include manifest object URI itself.
self.pack_uris_total += 1;
self.pack_file_uris_unique
.insert(res.pack.manifest_rsync_uri.clone());
*self
.pack_uris_by_ext_total
.entry(ext_of_uri(&res.pack.manifest_rsync_uri))
.or_insert(0) += 1;
for f in &res.pack.files {
if let Some(pack) = res.snapshot.as_ref() {
self.pack_uris_total += 1;
self.pack_file_uris_unique.insert(f.rsync_uri.clone());
self.pack_file_uris_unique
.insert(pack.manifest_rsync_uri.clone());
*self
.pack_uris_by_ext_total
.entry(ext_of_uri(&f.rsync_uri))
.entry(ext_of_uri(&pack.manifest_rsync_uri))
.or_insert(0) += 1;
if f.rsync_uri.ends_with(".crl") {
self.crl_total += 1;
if RpkixCrl::decode_der(&f.bytes).is_ok() {
self.crl_decode_ok += 1;
}
}
for f in &pack.files {
self.pack_uris_total += 1;
self.pack_file_uris_unique.insert(f.rsync_uri.clone());
*self
.pack_uris_by_ext_total
.entry(ext_of_uri(&f.rsync_uri))
.or_insert(0) += 1;
if f.rsync_uri.ends_with(".cer") {
self.child_ca_cert_candidates_total += 1;
if f.rsync_uri.ends_with(".crl") {
self.crl_total += 1;
if RpkixCrl::decode_der(&f.bytes).is_ok() {
self.crl_decode_ok += 1;
}
}
if f.rsync_uri.ends_with(".cer") {
self.child_ca_cert_candidates_total += 1;
}
}
}
@@ -168,7 +170,6 @@ fn apnic_tree_full_stats_serial() {
validation_time,
timing: None,
download_log: None,
revalidate_only: false,
rrdp_dedup: true,
rrdp_repo_cache: std::sync::Mutex::new(std::collections::HashMap::new()),
rsync_dedup: true,
@@ -181,8 +182,12 @@ fn apnic_tree_full_stats_serial() {
stats: &stats,
};
let root: CaInstanceHandle =
root_handle_from_trust_anchor(&discovery.trust_anchor, None, &discovery.ca_instance);
let root: CaInstanceHandle = root_handle_from_trust_anchor(
&discovery.trust_anchor,
"test-tal".to_string(),
None,
&discovery.ca_instance,
);
let max_depth = std::env::var("RPKI_APNIC_MAX_DEPTH")
.ok()
@@ -197,7 +202,6 @@ fn apnic_tree_full_stats_serial() {
&TreeRunConfig {
max_depth,
max_instances,
revalidate_only: false,
},
)
.expect("run tree");
@@ -213,11 +217,6 @@ fn apnic_tree_full_stats_serial() {
}
}
let fetch_cache_pp_total = store
.fetch_cache_pp_iter_all()
.expect("fetch_cache_pp_iter_all")
.count();
println!("APNIC Stage2 full-tree serial stats");
println!("tal_url={APNIC_TAL_URL}");
println!(
@@ -246,7 +245,6 @@ fn apnic_tree_full_stats_serial() {
stats.pack_file_uris_unique.len()
);
println!("pack_uris_by_ext_total={:?}", stats.pack_uris_by_ext_total);
println!("fetch_cache_pp_total={fetch_cache_pp_total}");
println!();
println!(
"crl_total={} crl_decode_ok={}",
@@ -273,7 +271,6 @@ fn apnic_tree_full_stats_serial() {
"rocksdb_raw_objects_total={} raw_by_ext={:?}",
raw_total, raw_by_ext
);
println!("rocksdb_fetch_cache_pp_total={fetch_cache_pp_total}");
// Loose sanity assertions (avoid flakiness due to repository churn).
//

View File

@@ -36,7 +36,6 @@ fn apnic_tree_depth1_processes_more_than_root() {
&TreeRunConfig {
max_depth: Some(1),
max_instances: Some(2),
revalidate_only: false,
},
)
.expect("run tree from tal");
@@ -75,7 +74,6 @@ fn apnic_tree_root_only_processes_root_with_long_timeouts() {
&TreeRunConfig {
max_depth: Some(0),
max_instances: Some(1),
revalidate_only: false,
},
)
.expect("run APNIC root-only");

View File

@@ -467,3 +467,337 @@ fn validate_subordinate_ca_rejects_tampered_crl_signature() {
.unwrap_err();
assert!(matches!(err, CaPathError::CrlVerify(_)));
}
#[test]
fn validate_subordinate_ca_rejects_non_ca_child_and_non_ca_issuer() {
let generated = generate_chain_and_crl(
"keyUsage = critical, keyCertSign, cRLSign
sbgp-ipAddrBlock = critical, IPv4:10.0.0.0/16
",
false,
);
let issuer = ResourceCertificate::decode_der(&generated.issuer_ca_der).expect("decode issuer");
let ee_child_der =
std::fs::read("tests/fixtures/repository/rpki.cernet.net/repo/cernet/0/AS4538.roa")
.expect("read roa fixture");
let roa = rpki::data_model::roa::RoaObject::decode_der(&ee_child_der).expect("decode roa");
let ee_der = roa.signed_object.signed_data.certificates[0]
.raw_der
.clone();
let err = validate_subordinate_ca_cert(
&ee_der,
&generated.issuer_ca_der,
&generated.issuer_crl_der,
Some("rsync://example.test/repo/issuer/issuer.cer"),
"rsync://example.test/repo/issuer/issuer.crl",
issuer.tbs.extensions.ip_resources.as_ref(),
issuer.tbs.extensions.as_resources.as_ref(),
time::OffsetDateTime::now_utc(),
)
.unwrap_err();
assert!(matches!(err, CaPathError::ChildNotCa), "{err}");
let err = validate_subordinate_ca_cert(
&generated.child_ca_der,
&ee_der,
&generated.issuer_crl_der,
Some("rsync://example.test/repo/issuer/issuer.cer"),
"rsync://example.test/repo/issuer/issuer.crl",
issuer.tbs.extensions.ip_resources.as_ref(),
issuer.tbs.extensions.as_resources.as_ref(),
time::OffsetDateTime::now_utc(),
)
.unwrap_err();
assert!(matches!(err, CaPathError::IssuerNotCa), "{err}");
}
#[test]
fn validate_subordinate_ca_rejects_mismatched_issuer_subject_and_missing_resources() {
let with_resources = generate_chain_and_crl(
"keyUsage = critical, keyCertSign, cRLSign
sbgp-ipAddrBlock = critical, IPv4:10.0.0.0/16
sbgp-autonomousSysNum = critical, AS:64496
",
false,
);
let issuer =
ResourceCertificate::decode_der(&with_resources.issuer_ca_der).expect("decode issuer");
let wrong_issuer_der = std::fs::read(
"tests/fixtures/repository/ca.rg.net/rpki/RGnet-OU/R-lVU1XGsAeqzV1Fv0HjOD6ZFkE.cer",
)
.expect("read wrong issuer fixture");
let wrong_issuer =
ResourceCertificate::decode_der(&wrong_issuer_der).expect("decode wrong issuer");
let err = validate_subordinate_ca_cert(
&with_resources.child_ca_der,
&wrong_issuer_der,
&with_resources.issuer_crl_der,
Some("rsync://example.test/repo/issuer/issuer.cer"),
"rsync://example.test/repo/issuer/issuer.crl",
wrong_issuer.tbs.extensions.ip_resources.as_ref(),
wrong_issuer.tbs.extensions.as_resources.as_ref(),
time::OffsetDateTime::now_utc(),
)
.unwrap_err();
assert!(
matches!(err, CaPathError::IssuerSubjectMismatch { .. }),
"{err}"
);
let no_resources = generate_chain_and_crl(
"keyUsage = critical, keyCertSign, cRLSign
",
false,
);
let err = validate_subordinate_ca_cert(
&no_resources.child_ca_der,
&no_resources.issuer_ca_der,
&no_resources.issuer_crl_der,
Some("rsync://example.test/repo/issuer/issuer.cer"),
"rsync://example.test/repo/issuer/issuer.crl",
issuer.tbs.extensions.ip_resources.as_ref(),
issuer.tbs.extensions.as_resources.as_ref(),
time::OffsetDateTime::now_utc(),
)
.unwrap_err();
assert!(matches!(err, CaPathError::ResourcesMissing), "{err}");
}
#[test]
fn validate_subordinate_ca_with_prevalidated_issuer_covers_success_and_error_paths() {
use rpki::data_model::common::BigUnsigned;
use rpki::data_model::crl::RpkixCrl;
use rpki::validation::ca_path::validate_subordinate_ca_cert_with_prevalidated_issuer;
use std::collections::HashSet;
use x509_parser::prelude::FromDer;
use x509_parser::x509::SubjectPublicKeyInfo;
let generated = generate_chain_and_crl(
"keyUsage = critical, keyCertSign, cRLSign
sbgp-ipAddrBlock = critical, IPv4:10.0.0.0/16
sbgp-autonomousSysNum = critical, AS:64496
",
false,
);
let issuer = ResourceCertificate::decode_der(&generated.issuer_ca_der).expect("decode issuer");
let child = ResourceCertificate::decode_der(&generated.child_ca_der).expect("decode child");
let issuer_crl = RpkixCrl::decode_der(&generated.issuer_crl_der).expect("decode crl");
let (rem, issuer_spki) = SubjectPublicKeyInfo::from_der(&issuer.tbs.subject_public_key_info)
.expect("parse issuer spki");
assert!(rem.is_empty());
let now = time::OffsetDateTime::now_utc();
let validated = validate_subordinate_ca_cert_with_prevalidated_issuer(
&generated.child_ca_der,
child.clone(),
&issuer,
&issuer_spki,
&issuer_crl,
&HashSet::new(),
Some("rsync://example.test/repo/issuer/issuer.cer"),
"rsync://example.test/repo/issuer/issuer.crl",
issuer.tbs.extensions.ip_resources.as_ref(),
issuer.tbs.extensions.as_resources.as_ref(),
now,
)
.expect("validate subordinate with prevalidated issuer");
assert!(validated.effective_ip_resources.is_some());
assert!(validated.effective_as_resources.is_some());
let mut revoked = HashSet::new();
revoked.insert(BigUnsigned::from_biguint(&child.tbs.serial_number).bytes_be);
let err = validate_subordinate_ca_cert_with_prevalidated_issuer(
&generated.child_ca_der,
child,
&issuer,
&issuer_spki,
&issuer_crl,
&revoked,
Some("rsync://example.test/repo/issuer/issuer.cer"),
"rsync://example.test/repo/issuer/issuer.crl",
issuer.tbs.extensions.ip_resources.as_ref(),
issuer.tbs.extensions.as_resources.as_ref(),
now,
)
.unwrap_err();
assert!(matches!(err, CaPathError::ChildRevoked), "{err}");
}
#[test]
fn validate_subordinate_ca_with_prevalidated_issuer_rejects_non_ca_inputs_and_invalid_times() {
use rpki::data_model::crl::RpkixCrl;
use rpki::validation::ca_path::validate_subordinate_ca_cert_with_prevalidated_issuer;
use std::collections::HashSet;
use x509_parser::prelude::FromDer;
use x509_parser::x509::SubjectPublicKeyInfo;
let generated = generate_chain_and_crl(
"keyUsage = critical, keyCertSign, cRLSign
sbgp-ipAddrBlock = critical, IPv4:10.0.0.0/16
sbgp-autonomousSysNum = critical, AS:64496
",
false,
);
let issuer = ResourceCertificate::decode_der(&generated.issuer_ca_der).expect("decode issuer");
let child = ResourceCertificate::decode_der(&generated.child_ca_der).expect("decode child");
let issuer_crl = RpkixCrl::decode_der(&generated.issuer_crl_der).expect("decode crl");
let (rem, issuer_spki) = SubjectPublicKeyInfo::from_der(&issuer.tbs.subject_public_key_info)
.expect("parse issuer spki");
assert!(rem.is_empty());
let roa_der =
std::fs::read("tests/fixtures/repository/rpki.cernet.net/repo/cernet/0/AS4538.roa")
.expect("read roa fixture");
let roa = rpki::data_model::roa::RoaObject::decode_der(&roa_der).expect("decode roa");
let ee_rc =
ResourceCertificate::decode_der(&roa.signed_object.signed_data.certificates[0].raw_der)
.expect("decode ee rc");
let (rem, ee_spki) =
SubjectPublicKeyInfo::from_der(&ee_rc.tbs.subject_public_key_info).expect("parse ee spki");
assert!(rem.is_empty());
let err = validate_subordinate_ca_cert_with_prevalidated_issuer(
&generated.child_ca_der,
child.clone(),
&ee_rc,
&ee_spki,
&issuer_crl,
&HashSet::new(),
Some("rsync://example.test/repo/issuer/issuer.cer"),
"rsync://example.test/repo/issuer/issuer.crl",
issuer.tbs.extensions.ip_resources.as_ref(),
issuer.tbs.extensions.as_resources.as_ref(),
time::OffsetDateTime::now_utc(),
)
.unwrap_err();
assert!(matches!(err, CaPathError::IssuerNotCa), "{err}");
let err = validate_subordinate_ca_cert_with_prevalidated_issuer(
&generated.child_ca_der,
ee_rc,
&issuer,
&issuer_spki,
&issuer_crl,
&HashSet::new(),
Some("rsync://example.test/repo/issuer/issuer.cer"),
"rsync://example.test/repo/issuer/issuer.crl",
issuer.tbs.extensions.ip_resources.as_ref(),
issuer.tbs.extensions.as_resources.as_ref(),
time::OffsetDateTime::now_utc(),
)
.unwrap_err();
assert!(matches!(err, CaPathError::ChildNotCa), "{err}");
let err = validate_subordinate_ca_cert_with_prevalidated_issuer(
&generated.child_ca_der,
child.clone(),
&issuer,
&issuer_spki,
&issuer_crl,
&HashSet::new(),
Some("rsync://example.test/repo/issuer/issuer.cer"),
"rsync://example.test/repo/issuer/issuer.crl",
issuer.tbs.extensions.ip_resources.as_ref(),
issuer.tbs.extensions.as_resources.as_ref(),
child.tbs.validity_not_before - time::Duration::seconds(1),
)
.unwrap_err();
assert!(
matches!(err, CaPathError::CertificateNotValidAtTime),
"{err}"
);
let err = validate_subordinate_ca_cert_with_prevalidated_issuer(
&generated.child_ca_der,
child,
&issuer,
&issuer_spki,
&issuer_crl,
&HashSet::new(),
Some("rsync://example.test/repo/issuer/issuer.cer"),
"rsync://example.test/repo/issuer/issuer.crl",
issuer.tbs.extensions.ip_resources.as_ref(),
issuer.tbs.extensions.as_resources.as_ref(),
issuer_crl.next_update.utc + time::Duration::seconds(1),
)
.unwrap_err();
assert!(matches!(err, CaPathError::CrlNotValidAtTime), "{err}");
}
#[test]
fn validate_subordinate_ca_with_prevalidated_issuer_rejects_mismatch_and_missing_resources() {
use rpki::data_model::crl::RpkixCrl;
use rpki::validation::ca_path::validate_subordinate_ca_cert_with_prevalidated_issuer;
use std::collections::HashSet;
use x509_parser::prelude::FromDer;
use x509_parser::x509::SubjectPublicKeyInfo;
let generated = generate_chain_and_crl(
"keyUsage = critical, keyCertSign, cRLSign
sbgp-ipAddrBlock = critical, IPv4:10.0.0.0/16
sbgp-autonomousSysNum = critical, AS:64496
",
false,
);
let issuer = ResourceCertificate::decode_der(&generated.issuer_ca_der).expect("decode issuer");
let child = ResourceCertificate::decode_der(&generated.child_ca_der).expect("decode child");
let issuer_crl = RpkixCrl::decode_der(&generated.issuer_crl_der).expect("decode crl");
let (rem, issuer_spki) = SubjectPublicKeyInfo::from_der(&issuer.tbs.subject_public_key_info)
.expect("parse issuer spki");
assert!(rem.is_empty());
let mut mismatched_child = child.clone();
mismatched_child.tbs.issuer_name = mismatched_child.tbs.subject_name.clone();
let err = validate_subordinate_ca_cert_with_prevalidated_issuer(
&generated.child_ca_der,
mismatched_child,
&issuer,
&issuer_spki,
&issuer_crl,
&HashSet::new(),
Some("rsync://example.test/repo/issuer/issuer.cer"),
"rsync://example.test/repo/issuer/issuer.crl",
issuer.tbs.extensions.ip_resources.as_ref(),
issuer.tbs.extensions.as_resources.as_ref(),
time::OffsetDateTime::now_utc(),
)
.unwrap_err();
assert!(
matches!(err, CaPathError::IssuerSubjectMismatch { .. }),
"{err}"
);
let no_resources = generate_chain_and_crl(
"keyUsage = critical, keyCertSign, cRLSign
",
false,
);
let no_resources_child = ResourceCertificate::decode_der(&no_resources.child_ca_der)
.expect("decode no resources child");
let no_resources_issuer = ResourceCertificate::decode_der(&no_resources.issuer_ca_der)
.expect("decode no resources issuer");
let no_resources_crl =
RpkixCrl::decode_der(&no_resources.issuer_crl_der).expect("decode no resources crl");
let (rem, no_resources_spki) =
SubjectPublicKeyInfo::from_der(&no_resources_issuer.tbs.subject_public_key_info)
.expect("parse no resources issuer spki");
assert!(rem.is_empty());
let err = validate_subordinate_ca_cert_with_prevalidated_issuer(
&no_resources.child_ca_der,
no_resources_child,
&no_resources_issuer,
&no_resources_spki,
&no_resources_crl,
&HashSet::new(),
Some("rsync://example.test/repo/issuer/issuer.cer"),
"rsync://example.test/repo/issuer/issuer.crl",
no_resources_issuer.tbs.extensions.ip_resources.as_ref(),
no_resources_issuer.tbs.extensions.as_resources.as_ref(),
time::OffsetDateTime::now_utc(),
)
.unwrap_err();
assert!(matches!(err, CaPathError::ResourcesMissing), "{err}");
}

View File

@@ -244,3 +244,250 @@ fn ee_key_usage_wrong_bits_is_rejected() {
.unwrap_err();
assert!(matches!(err, CertPathError::KeyUsageInvalidBits), "{err}");
}
#[test]
fn validate_ee_cert_path_with_prevalidated_issuer_covers_success_and_error_paths() {
use rpki::data_model::common::BigUnsigned;
use rpki::data_model::crl::RpkixCrl;
use rpki::data_model::rc::ResourceCertificate;
use rpki::validation::cert_path::validate_ee_cert_path_with_prevalidated_issuer;
use std::collections::HashSet;
use x509_parser::prelude::FromDer;
use x509_parser::x509::SubjectPublicKeyInfo;
let g = generate_issuer_ca_ee_and_crl(
"keyUsage = critical, digitalSignature
",
);
let issuer = ResourceCertificate::decode_der(&g.issuer_ca_der).expect("decode issuer");
let ee = ResourceCertificate::decode_der(&g.ee_der).expect("decode ee");
let issuer_crl = RpkixCrl::decode_der(&g.issuer_crl_der).expect("decode crl");
let (rem, issuer_spki) = SubjectPublicKeyInfo::from_der(&issuer.tbs.subject_public_key_info)
.expect("parse issuer spki");
assert!(rem.is_empty());
let now = time::OffsetDateTime::now_utc();
validate_ee_cert_path_with_prevalidated_issuer(
&g.ee_der,
&issuer,
&issuer_spki,
&issuer_crl,
&HashSet::new(),
Some("rsync://example.test/repo/issuer/issuer.cer"),
Some("rsync://example.test/repo/issuer/issuer.crl"),
now,
)
.expect("prevalidated ee path ok");
let mut revoked = HashSet::new();
revoked.insert(BigUnsigned::from_biguint(&ee.tbs.serial_number).bytes_be);
let err = validate_ee_cert_path_with_prevalidated_issuer(
&g.ee_der,
&issuer,
&issuer_spki,
&issuer_crl,
&revoked,
Some("rsync://example.test/repo/issuer/issuer.cer"),
Some("rsync://example.test/repo/issuer/issuer.crl"),
now,
)
.unwrap_err();
assert!(matches!(err, CertPathError::EeRevoked), "{err}");
}
#[test]
fn validate_ee_cert_path_with_prevalidated_issuer_rejects_non_ee_and_non_ca_issuer() {
use rpki::data_model::crl::RpkixCrl;
use rpki::data_model::rc::ResourceCertificate;
use rpki::validation::cert_path::validate_ee_cert_path_with_prevalidated_issuer;
use std::collections::HashSet;
use x509_parser::prelude::FromDer;
use x509_parser::x509::SubjectPublicKeyInfo;
let g = generate_issuer_ca_ee_and_crl(
"keyUsage = critical, digitalSignature
",
);
let issuer = ResourceCertificate::decode_der(&g.issuer_ca_der).expect("decode issuer");
let ee = ResourceCertificate::decode_der(&g.ee_der).expect("decode ee");
let issuer_crl = RpkixCrl::decode_der(&g.issuer_crl_der).expect("decode crl");
let (rem, issuer_spki) = SubjectPublicKeyInfo::from_der(&issuer.tbs.subject_public_key_info)
.expect("parse issuer spki");
assert!(rem.is_empty());
let now = time::OffsetDateTime::now_utc();
let err = validate_ee_cert_path_with_prevalidated_issuer(
&g.issuer_ca_der,
&issuer,
&issuer_spki,
&issuer_crl,
&HashSet::new(),
None,
None,
now,
)
.unwrap_err();
assert!(matches!(err, CertPathError::EeNotEe), "{err}");
let (rem, bad_spki) =
SubjectPublicKeyInfo::from_der(&ee.tbs.subject_public_key_info).expect("parse ee spki");
assert!(rem.is_empty());
let err = validate_ee_cert_path_with_prevalidated_issuer(
&g.ee_der,
&ee,
&bad_spki,
&issuer_crl,
&HashSet::new(),
None,
None,
now,
)
.unwrap_err();
assert!(matches!(err, CertPathError::IssuerNotCa), "{err}");
}
#[test]
fn validate_ee_cert_path_with_prevalidated_issuer_rejects_mismatched_issuer_subject() {
use rpki::data_model::crl::RpkixCrl;
use rpki::data_model::rc::ResourceCertificate;
use rpki::validation::cert_path::validate_ee_cert_path_with_prevalidated_issuer;
use std::collections::HashSet;
use x509_parser::prelude::FromDer;
use x509_parser::x509::SubjectPublicKeyInfo;
let g = generate_issuer_ca_ee_and_crl(
"keyUsage = critical, digitalSignature
",
);
let wrong_issuer_der = std::fs::read(
"tests/fixtures/repository/ca.rg.net/rpki/RGnet-OU/R-lVU1XGsAeqzV1Fv0HjOD6ZFkE.cer",
)
.expect("read wrong issuer fixture");
let wrong_issuer =
ResourceCertificate::decode_der(&wrong_issuer_der).expect("decode wrong issuer");
let issuer_crl = RpkixCrl::decode_der(&g.issuer_crl_der).expect("decode crl");
let (rem, wrong_spki) =
SubjectPublicKeyInfo::from_der(&wrong_issuer.tbs.subject_public_key_info)
.expect("parse wrong issuer spki");
assert!(rem.is_empty());
let err = validate_ee_cert_path_with_prevalidated_issuer(
&g.ee_der,
&wrong_issuer,
&wrong_spki,
&issuer_crl,
&HashSet::new(),
None,
None,
time::OffsetDateTime::now_utc(),
)
.unwrap_err();
assert!(
matches!(err, CertPathError::IssuerSubjectMismatch { .. }),
"{err}"
);
}
#[test]
fn validate_ee_cert_path_rejects_non_ee_and_non_ca_issuer() {
use rpki::data_model::roa::RoaObject;
let g = generate_issuer_ca_ee_and_crl(
"keyUsage = critical, digitalSignature
",
);
let roa_der =
std::fs::read("tests/fixtures/repository/rpki.cernet.net/repo/cernet/0/AS4538.roa")
.expect("read roa fixture");
let roa = RoaObject::decode_der(&roa_der).expect("decode roa");
let ee_from_roa = roa.signed_object.signed_data.certificates[0]
.raw_der
.clone();
let now = time::OffsetDateTime::now_utc();
let err = validate_ee_cert_path(
&g.issuer_ca_der,
&g.issuer_ca_der,
&g.issuer_crl_der,
None,
None,
now,
)
.unwrap_err();
assert!(matches!(err, CertPathError::EeNotEe), "{err}");
let err = validate_ee_cert_path(&g.ee_der, &ee_from_roa, &g.issuer_crl_der, None, None, now)
.unwrap_err();
assert!(matches!(err, CertPathError::IssuerNotCa), "{err}");
}
#[test]
fn validate_ee_cert_path_rejects_stale_crl() {
use rpki::data_model::crl::RpkixCrl;
let g = generate_issuer_ca_ee_and_crl(
"keyUsage = critical, digitalSignature
",
);
let crl = RpkixCrl::decode_der(&g.issuer_crl_der).expect("decode crl");
let err = validate_ee_cert_path(
&g.ee_der,
&g.issuer_ca_der,
&g.issuer_crl_der,
None,
None,
crl.next_update.utc + time::Duration::seconds(1),
)
.unwrap_err();
assert!(matches!(err, CertPathError::CrlNotValidAtTime), "{err}");
}
#[test]
fn validate_ee_cert_path_with_prevalidated_issuer_rejects_invalid_times() {
use rpki::data_model::crl::RpkixCrl;
use rpki::data_model::rc::ResourceCertificate;
use rpki::validation::cert_path::validate_ee_cert_path_with_prevalidated_issuer;
use std::collections::HashSet;
use x509_parser::prelude::FromDer;
use x509_parser::x509::SubjectPublicKeyInfo;
let g = generate_issuer_ca_ee_and_crl(
"keyUsage = critical, digitalSignature
",
);
let issuer = ResourceCertificate::decode_der(&g.issuer_ca_der).expect("decode issuer");
let ee = ResourceCertificate::decode_der(&g.ee_der).expect("decode ee");
let issuer_crl = RpkixCrl::decode_der(&g.issuer_crl_der).expect("decode crl");
let (rem, issuer_spki) = SubjectPublicKeyInfo::from_der(&issuer.tbs.subject_public_key_info)
.expect("parse issuer spki");
assert!(rem.is_empty());
let err = validate_ee_cert_path_with_prevalidated_issuer(
&g.ee_der,
&issuer,
&issuer_spki,
&issuer_crl,
&HashSet::new(),
Some("rsync://example.test/repo/issuer/issuer.cer"),
Some("rsync://example.test/repo/issuer/issuer.crl"),
ee.tbs.validity_not_before - time::Duration::seconds(1),
)
.unwrap_err();
assert!(
matches!(err, CertPathError::CertificateNotValidAtTime),
"{err}"
);
let err = validate_ee_cert_path_with_prevalidated_issuer(
&g.ee_der,
&issuer,
&issuer_spki,
&issuer_crl,
&HashSet::new(),
Some("rsync://example.test/repo/issuer/issuer.cer"),
Some("rsync://example.test/repo/issuer/issuer.crl"),
issuer_crl.next_update.utc + time::Duration::seconds(1),
)
.unwrap_err();
assert!(matches!(err, CertPathError::CrlNotValidAtTime), "{err}");
}

View File

@@ -1,8 +1,9 @@
use rpki::audit::PublicationPointAudit;
use rpki::policy::{Policy, SignedObjectFailurePolicy};
use rpki::storage::{FetchCachePpPack, PackFile, PackTime};
use rpki::storage::{PackFile, PackTime};
use rpki::validation::manifest::PublicationPointSource;
use rpki::validation::objects::process_fetch_cache_pp_pack_for_issuer;
use rpki::validation::objects::process_publication_point_snapshot_for_issuer;
use rpki::validation::publication_point::PublicationPointSnapshot;
use rpki::validation::tree::{
CaInstanceHandle, PublicationPointRunResult, PublicationPointRunner, TreeRunConfig,
run_tree_serial_audit,
@@ -13,12 +14,12 @@ fn fixture_bytes(path: &str) -> Vec<u8> {
.unwrap_or_else(|e| panic!("read fixture {path}: {e}"))
}
fn dummy_pack(files: Vec<PackFile>) -> FetchCachePpPack {
fn dummy_snapshot(files: Vec<PackFile>) -> PublicationPointSnapshot {
let now = time::OffsetDateTime::now_utc();
let manifest_rsync_uri =
"rsync://rpki.cernet.net/repo/cernet/0/05FC9C5B88506F7C0D3F862C8895BED67E9F8EBA.mft";
FetchCachePpPack {
format_version: FetchCachePpPack::FORMAT_VERSION_V1,
PublicationPointSnapshot {
format_version: PublicationPointSnapshot::FORMAT_VERSION_V1,
manifest_rsync_uri: manifest_rsync_uri.to_string(),
publication_point_rsync_uri: "rsync://rpki.cernet.net/repo/cernet/0/".to_string(),
manifest_number_be: vec![1],
@@ -34,7 +35,7 @@ fn dummy_pack(files: Vec<PackFile>) -> FetchCachePpPack {
struct SinglePackRunner {
policy: Policy,
pack: FetchCachePpPack,
snapshot: PublicationPointSnapshot,
}
impl PublicationPointRunner for SinglePackRunner {
@@ -42,8 +43,8 @@ impl PublicationPointRunner for SinglePackRunner {
&self,
ca: &CaInstanceHandle,
) -> Result<PublicationPointRunResult, String> {
let objects = process_fetch_cache_pp_pack_for_issuer(
&self.pack,
let objects = process_publication_point_snapshot_for_issuer(
&self.snapshot,
&self.policy,
&ca.ca_certificate_der,
ca.ca_certificate_rsync_uri.as_deref(),
@@ -55,7 +56,7 @@ impl PublicationPointRunner for SinglePackRunner {
Ok(PublicationPointRunResult {
source: PublicationPointSource::Fresh,
pack: self.pack.clone(),
snapshot: Some(self.snapshot.clone()),
warnings: Vec::new(),
objects,
audit: PublicationPointAudit::default(),
@@ -70,7 +71,7 @@ fn crl_mismatch_drops_publication_point_and_cites_rfc_sections() {
fixture_bytes("tests/fixtures/repository/rpki.cernet.net/repo/cernet/0/AS4538.roa");
// Include at least one CRL file but with a URI that does NOT match the EE certificate's CRLDP.
let pack = dummy_pack(vec![
let pack = dummy_snapshot(vec![
PackFile::from_bytes_compute_sha256("rsync://example.test/repo/not-it.crl", vec![0x01]),
PackFile::from_bytes_compute_sha256("rsync://example.test/repo/a.roa", roa_bytes),
]);
@@ -78,10 +79,15 @@ fn crl_mismatch_drops_publication_point_and_cites_rfc_sections() {
let mut policy = Policy::default();
policy.signed_object_failure_policy = SignedObjectFailurePolicy::DropPublicationPoint;
let runner = SinglePackRunner { policy, pack };
let runner = SinglePackRunner {
policy,
snapshot: pack,
};
let root = CaInstanceHandle {
depth: 0,
tal_id: "test-tal".to_string(),
parent_manifest_rsync_uri: None,
// Use a real, parseable CA certificate DER so objects processing can reach CRL selection.
// The test only asserts CRLDP/locked-pack error handling, not signature chaining.
ca_certificate_der: fixture_bytes("tests/fixtures/ta/apnic-ta.cer"),
@@ -102,7 +108,6 @@ fn crl_mismatch_drops_publication_point_and_cites_rfc_sections() {
&TreeRunConfig {
max_depth: Some(0),
max_instances: Some(1),
revalidate_only: false,
},
)
.expect("run tree audit");

View File

@@ -1,280 +0,0 @@
use std::path::Path;
use rpki::data_model::manifest::ManifestObject;
use rpki::policy::{CaFailedFetchPolicy, Policy};
use rpki::storage::{FetchCachePpKey, FetchCachePpPack, RocksStore};
use rpki::validation::manifest::{
PublicationPointSource, process_manifest_publication_point,
process_manifest_publication_point_after_repo_sync,
};
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 load_cernet_manifest_fixture() -> (std::path::PathBuf, Vec<u8>, ManifestObject) {
let manifest_path = Path::new(
"tests/fixtures/repository/rpki.cernet.net/repo/cernet/0/05FC9C5B88506F7C0D3F862C8895BED67E9F8EBA.mft",
)
.to_path_buf();
let bytes = std::fs::read(&manifest_path).expect("read manifest fixture");
let obj = ManifestObject::decode_der(&bytes).expect("decode manifest fixture");
(manifest_path, bytes, obj)
}
fn store_raw_publication_point_files(
store: &RocksStore,
manifest_path: &Path,
manifest_rsync_uri: &str,
manifest_bytes: &[u8],
manifest: &ManifestObject,
publication_point_rsync_uri: &str,
) {
store
.put_raw(manifest_rsync_uri, manifest_bytes)
.expect("store manifest raw");
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 raw");
}
}
#[test]
fn cached_pack_revalidation_rejects_missing_file_referenced_by_manifest() {
let (manifest_path, manifest_bytes, manifest) = load_cernet_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_raw_publication_point_files(
&store,
&manifest_path,
&manifest_rsync_uri,
&manifest_bytes,
&manifest,
&publication_point_rsync_uri,
);
let mut policy = Policy::default();
policy.ca_failed_fetch_policy = CaFailedFetchPolicy::UseFetchCachePp;
let issuer_ca_der = issuer_ca_fixture();
let _fresh = process_manifest_publication_point(
&store,
&policy,
&manifest_rsync_uri,
&publication_point_rsync_uri,
&issuer_ca_der,
Some(issuer_ca_rsync_uri()),
validation_time,
)
.expect("fresh run stores fetch_cache_pp");
let key = FetchCachePpKey::from_manifest_rsync_uri(&manifest_rsync_uri);
let cached_bytes = store
.get_fetch_cache_pp(&key)
.expect("get fetch_cache_pp")
.expect("fetch_cache_pp exists");
let mut pack = FetchCachePpPack::decode(&cached_bytes).expect("decode pack");
// Remove one file from the pack: pack stays internally consistent, but no longer satisfies
// RFC 9286 §6.4 when revalidated against the manifest fileList.
pack.files.pop().expect("non-empty pack");
let bytes = pack.encode().expect("encode pack");
store
.put_fetch_cache_pp(&key, &bytes)
.expect("overwrite fetch_cache_pp");
// Force cache path: remove raw manifest so fresh processing fails at §6.2.
store
.delete_raw(&manifest_rsync_uri)
.expect("delete raw manifest");
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("cache pack missing file must be rejected");
let msg = err.to_string();
assert!(msg.contains("cached fetch_cache_pp missing file"), "{msg}");
assert!(msg.contains("RFC 9286 §6.4"), "{msg}");
}
#[test]
fn cached_pack_revalidation_rejects_hash_mismatch_against_manifest_filelist() {
let (manifest_path, manifest_bytes, manifest) = load_cernet_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_raw_publication_point_files(
&store,
&manifest_path,
&manifest_rsync_uri,
&manifest_bytes,
&manifest,
&publication_point_rsync_uri,
);
let mut policy = Policy::default();
policy.ca_failed_fetch_policy = CaFailedFetchPolicy::UseFetchCachePp;
let issuer_ca_der = issuer_ca_fixture();
let _fresh = process_manifest_publication_point(
&store,
&policy,
&manifest_rsync_uri,
&publication_point_rsync_uri,
&issuer_ca_der,
Some(issuer_ca_rsync_uri()),
validation_time,
)
.expect("fresh run stores fetch_cache_pp");
let key = FetchCachePpKey::from_manifest_rsync_uri(&manifest_rsync_uri);
let cached_bytes = store
.get_fetch_cache_pp(&key)
.expect("get fetch_cache_pp")
.expect("fetch_cache_pp exists");
let mut pack = FetchCachePpPack::decode(&cached_bytes).expect("decode pack");
// Mutate one file but keep pack internally consistent by recomputing its sha256 field.
let victim = pack.files.first_mut().expect("non-empty pack");
victim.bytes[0] ^= 0xFF;
victim.sha256 = victim.compute_sha256();
let bytes = pack.encode().expect("encode pack");
store
.put_fetch_cache_pp(&key, &bytes)
.expect("overwrite fetch_cache_pp");
// Force cache path.
store
.delete_raw(&manifest_rsync_uri)
.expect("delete raw manifest");
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("cache pack hash mismatch must be rejected");
let msg = err.to_string();
assert!(
msg.contains("cached fetch_cache_pp file hash mismatch"),
"{msg}"
);
assert!(msg.contains("RFC 9286 §6.5"), "{msg}");
}
#[test]
fn repo_sync_failure_forces_fetch_cache_pp_even_if_raw_objects_are_present() {
let (manifest_path, manifest_bytes, manifest) = load_cernet_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_raw_publication_point_files(
&store,
&manifest_path,
&manifest_rsync_uri,
&manifest_bytes,
&manifest,
&publication_point_rsync_uri,
);
let mut policy = Policy::default();
policy.ca_failed_fetch_policy = CaFailedFetchPolicy::UseFetchCachePp;
let issuer_ca_der = issuer_ca_fixture();
// First run: fresh processing stores fetch_cache_pp.
let _fresh = process_manifest_publication_point(
&store,
&policy,
&manifest_rsync_uri,
&publication_point_rsync_uri,
&issuer_ca_der,
Some(issuer_ca_rsync_uri()),
validation_time,
)
.expect("fresh run stores fetch_cache_pp");
// Second run: simulate repo sync failure. Even though raw_objects still contain everything
// needed for a fresh pack, failed fetch semantics require using cached objects only.
let res = process_manifest_publication_point_after_repo_sync(
&store,
&policy,
&manifest_rsync_uri,
&publication_point_rsync_uri,
&issuer_ca_der,
Some(issuer_ca_rsync_uri()),
validation_time,
false,
Some("synthetic repo sync failure"),
)
.expect("must fall back to fetch_cache_pp");
assert_eq!(res.source, PublicationPointSource::FetchCachePp);
assert!(
res.warnings
.iter()
.any(|w| w.message.contains("using fetch_cache_pp")),
"expected fetch_cache_pp warning"
);
}

View File

@@ -10,6 +10,7 @@ use rpki::validation::from_tal::{
discover_root_ca_instance_from_tal_and_ta_der, discover_root_ca_instance_from_tal_url,
run_root_from_tal_url_once,
};
use rpki::validation::manifest::PublicationPointSource;
use url::Url;
struct MapFetcher {
@@ -42,6 +43,26 @@ impl RsyncFetcher for EmptyRsync {
}
}
fn openssl_available() -> bool {
std::process::Command::new("openssl")
.arg("version")
.output()
.map(|o| o.status.success())
.unwrap_or(false)
}
fn run(cmd: &mut std::process::Command) {
let out = cmd.output().expect("run command");
if !out.status.success() {
panic!(
"command failed: {:?}\nstdout={}\nstderr={}",
cmd,
String::from_utf8_lossy(&out.stdout),
String::from_utf8_lossy(&out.stderr)
);
}
}
fn apnic_tal_bytes() -> Vec<u8> {
std::fs::read("tests/fixtures/tal/apnic-rfc7730-https.tal").expect("read apnic TAL fixture")
}
@@ -145,7 +166,7 @@ fn discover_root_from_tal_url_errors_when_tal_fetch_fails() {
}
#[test]
fn run_root_from_tal_url_once_propagates_run_error_when_repo_is_empty() {
fn run_root_from_tal_url_once_returns_failed_fetch_no_cache_when_repo_is_empty() {
let tal_bytes = apnic_tal_bytes();
let tal = Tal::decode_bytes(&tal_bytes).expect("decode TAL");
let ta_uri = tal.ta_uris[0].as_str().to_string();
@@ -161,7 +182,7 @@ fn run_root_from_tal_url_once_propagates_run_error_when_repo_is_empty() {
let mut policy = Policy::default();
policy.sync_preference = SyncPreference::RsyncOnly;
let err = run_root_from_tal_url_once(
let out = run_root_from_tal_url_once(
&store,
&policy,
"https://example.test/apnic.tal",
@@ -169,7 +190,105 @@ fn run_root_from_tal_url_once_propagates_run_error_when_repo_is_empty() {
&EmptyRsync,
time::OffsetDateTime::now_utc(),
)
.unwrap_err();
.expect("run should return failed-fetch-no-cache output");
assert!(matches!(err, FromTalError::Run(_)));
assert_eq!(
out.run.publication_point_source,
PublicationPointSource::FailedFetchNoCache
);
assert!(out.run.objects.vrps.is_empty());
assert!(out.run.objects.aspas.is_empty());
assert!(
out.run
.publication_point_warnings
.iter()
.any(|warning| warning.message.contains("no latest validated result"))
);
}
#[test]
fn discover_root_records_ca_instance_discovery_failure_when_ta_lacks_sia() {
use base64::Engine;
assert!(openssl_available(), "openssl is required for this test");
let temp = tempfile::tempdir().expect("tempdir");
let dir = temp.path();
std::fs::write(
dir.join("openssl.cnf"),
r#"
[ req ]
prompt = no
distinguished_name = dn
x509_extensions = v3_ta
[ dn ]
CN = Test TA Without SIA
[ v3_ta ]
basicConstraints = critical,CA:true
keyUsage = critical, keyCertSign, cRLSign
subjectKeyIdentifier = hash
certificatePolicies = critical, 1.3.6.1.5.5.7.14.2
sbgp-ipAddrBlock = critical, IPv4:10.0.0.0/8
sbgp-autonomousSysNum = critical, AS:64496-64511
"#,
)
.expect("write openssl cnf");
run(std::process::Command::new("openssl")
.arg("genrsa")
.arg("-out")
.arg(dir.join("ta.key"))
.arg("2048"));
run(std::process::Command::new("openssl")
.arg("req")
.arg("-new")
.arg("-x509")
.arg("-sha256")
.arg("-days")
.arg("365")
.arg("-key")
.arg(dir.join("ta.key"))
.arg("-config")
.arg(dir.join("openssl.cnf"))
.arg("-extensions")
.arg("v3_ta")
.arg("-out")
.arg(dir.join("ta.pem")));
run(std::process::Command::new("openssl")
.arg("x509")
.arg("-in")
.arg(dir.join("ta.pem"))
.arg("-outform")
.arg("DER")
.arg("-out")
.arg(dir.join("ta.cer")));
run(std::process::Command::new("sh").arg("-c").arg(format!(
"openssl x509 -in {} -pubkey -noout | openssl pkey -pubin -outform DER > {}",
dir.join("ta.pem").display(),
dir.join("spki.der").display(),
)));
let ta_uri = "https://example.test/no-sia-ta.cer";
let tal_text = format!(
"{ta_uri}\n\n{}\n",
base64::engine::general_purpose::STANDARD
.encode(std::fs::read(dir.join("spki.der")).expect("read spki der"))
);
let tal = Tal::decode_bytes(tal_text.as_bytes()).expect("decode generated tal");
let mut map = HashMap::new();
map.insert(
ta_uri.to_string(),
std::fs::read(dir.join("ta.cer")).expect("read ta der"),
);
let fetcher = MapFetcher::new(map);
let err = discover_root_ca_instance_from_tal(&fetcher, tal, None).unwrap_err();
assert!(matches!(err, FromTalError::TaFetch(_)), "{err}");
assert!(
err.to_string().contains("CA instance discovery failed"),
"{err}"
);
}

View File

@@ -1,8 +1,5 @@
use std::path::Path;
use rpki::data_model::manifest::ManifestObject;
use rpki::policy::{CaFailedFetchPolicy, Policy};
use rpki::storage::{FetchCachePpKey, FetchCachePpPack, RocksStore};
use rpki::storage::RocksStore;
use rpki::validation::manifest::process_manifest_publication_point;
fn issuer_ca_fixture() -> Vec<u8> {
@@ -16,35 +13,13 @@ 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
}
#[test]
fn cache_is_not_used_when_missing_and_fresh_manifest_is_missing() {
let temp = tempfile::tempdir().expect("tempdir");
let store = RocksStore::open(temp.path()).expect("open rocksdb");
let mut policy = Policy::default();
policy.ca_failed_fetch_policy = CaFailedFetchPolicy::UseFetchCachePp;
policy.ca_failed_fetch_policy = CaFailedFetchPolicy::ReuseCurrentInstanceVcir;
let issuer_ca_der = issuer_ca_fixture();
let err = process_manifest_publication_point(
@@ -56,89 +31,15 @@ fn cache_is_not_used_when_missing_and_fresh_manifest_is_missing() {
Some(issuer_ca_rsync_uri()),
time::OffsetDateTime::from_unix_timestamp(0).unwrap(),
)
.expect_err("no raw and no fetch_cache_pp should fail");
assert!(err.to_string().contains("fetch_cache_pp entry missing"));
}
#[test]
fn cache_pack_publication_point_mismatch_is_rejected() {
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 _ = 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 stores fetch_cache_pp pack");
// Corrupt the cached pack by changing the publication point.
let key = FetchCachePpKey::from_manifest_rsync_uri(&manifest_rsync_uri);
let bytes = store
.get_fetch_cache_pp(&key)
.expect("get fetch_cache_pp")
.expect("fetch_cache_pp exists");
let mut pack = FetchCachePpPack::decode(&bytes).expect("decode pack");
pack.publication_point_rsync_uri = "rsync://evil.invalid/repo/".to_string();
let bytes = pack.encode().expect("re-encode pack");
store
.put_fetch_cache_pp(&key, &bytes)
.expect("overwrite fetch_cache_pp");
// Remove raw manifest to force cache path.
store
.delete_raw(&manifest_rsync_uri)
.expect("delete raw manifest");
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("cache pack mismatch should fail");
.expect_err("no raw and no current-instance VCIR should fail");
let msg = err.to_string();
assert!(
err.to_string()
.contains("publication_point_rsync_uri does not match expected")
msg.contains("no reusable current-instance validated result is available"),
"{msg}"
);
assert!(
msg.contains("latest current-instance VCIR missing"),
"{msg}"
);
}

View File

@@ -1,8 +1,14 @@
use std::path::Path;
use sha2::Digest;
use rpki::data_model::manifest::ManifestObject;
use rpki::policy::{CaFailedFetchPolicy, Policy};
use rpki::storage::{FetchCachePpKey, FetchCachePpPack, RocksStore};
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> {
@@ -38,8 +44,78 @@ fn fixture_dir_to_rsync_uri(dir: &Path) -> String {
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_writes_fetch_cache_pp_pack() {
fn manifest_success_returns_validated_publication_point_data() {
let manifest_path = Path::new(
"tests/fixtures/repository/rpki.cernet.net/repo/cernet/0/05FC9C5B88506F7C0D3F862C8895BED67E9F8EBA.mft",
);
@@ -87,21 +163,15 @@ fn manifest_success_writes_fetch_cache_pp_pack() {
assert_eq!(out.source, PublicationPointSource::Fresh);
assert!(out.warnings.is_empty());
let key = FetchCachePpKey::from_manifest_rsync_uri(&manifest_rsync_uri);
let stored = store
.get_fetch_cache_pp(&key)
.expect("get fetch_cache_pp")
.expect("fetch_cache_pp pack exists");
let decoded = FetchCachePpPack::decode(&stored).expect("decode stored pack");
assert_eq!(decoded.manifest_rsync_uri, manifest_rsync_uri);
assert_eq!(out.snapshot.manifest_rsync_uri, manifest_rsync_uri);
assert_eq!(
decoded.publication_point_rsync_uri,
out.snapshot.publication_point_rsync_uri,
publication_point_rsync_uri
);
}
#[test]
fn manifest_hash_mismatch_falls_back_to_fetch_cache_pp_when_enabled() {
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",
);
@@ -144,15 +214,17 @@ fn manifest_hash_mismatch_falls_back_to_fetch_cache_pp_when_enabled() {
Some(issuer_ca_rsync_uri()),
validation_time,
)
.expect("first run stores fetch_cache_pp pack");
.expect("first run returns validated publication point");
assert_eq!(first.source, PublicationPointSource::Fresh);
let key = FetchCachePpKey::from_manifest_rsync_uri(&manifest_rsync_uri);
let cached_bytes = store
.get_fetch_cache_pp(&key)
.expect("get fetch_cache_pp")
.expect("fetch_cache_pp pack exists");
let cached_pack = FetchCachePpPack::decode(&cached_bytes).expect("decode cached");
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
@@ -176,10 +248,14 @@ fn manifest_hash_mismatch_falls_back_to_fetch_cache_pp_when_enabled() {
Some(issuer_ca_rsync_uri()),
validation_time,
)
.expect("second run falls back to fetch_cache_pp");
assert_eq!(second.source, PublicationPointSource::FetchCachePp);
assert!(!second.warnings.is_empty());
assert_eq!(second.pack, cached_pack);
.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]
@@ -216,7 +292,7 @@ fn manifest_failed_fetch_stop_all_output() {
}
let mut policy = Policy::default();
policy.ca_failed_fetch_policy = CaFailedFetchPolicy::UseFetchCachePp;
policy.ca_failed_fetch_policy = CaFailedFetchPolicy::ReuseCurrentInstanceVcir;
let issuer_ca_der = issuer_ca_fixture();
let _ = process_manifest_publication_point(
&store,
@@ -227,7 +303,7 @@ fn manifest_failed_fetch_stop_all_output() {
Some(issuer_ca_rsync_uri()),
validation_time,
)
.expect("first run stores fetch_cache_pp pack");
.expect("first run returns validated publication point");
let entries = manifest
.manifest
@@ -252,13 +328,13 @@ fn manifest_failed_fetch_stop_all_output() {
Some(issuer_ca_rsync_uri()),
validation_time,
)
.expect_err("stop_all_output should not use fetch_cache_pp");
.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_fallback_pack_is_revalidated_and_rejected_if_stale() {
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",
);
@@ -302,7 +378,16 @@ fn manifest_fallback_pack_is_revalidated_and_rejected_if_stale() {
Some(issuer_ca_rsync_uri()),
ok_time,
)
.expect("first run stores fetch_cache_pp pack");
.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)
@@ -317,9 +402,9 @@ fn manifest_fallback_pack_is_revalidated_and_rejected_if_stale() {
Some(issuer_ca_rsync_uri()),
stale_time,
)
.expect_err("stale validation_time must reject fetch_cache_pp pack");
.expect_err("stale validation_time must reject current-instance VCIR reuse");
let msg = err.to_string();
assert!(msg.contains("not valid at validation_time"));
assert!(msg.contains("instance_gate expired"), "{msg}");
}
#[test]
@@ -369,9 +454,18 @@ fn manifest_revalidation_with_unchanged_manifest_is_fresh() {
Some(issuer_ca_rsync_uri()),
t1,
)
.expect("first run builds and stores fetch_cache_pp pack");
.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,
@@ -384,16 +478,19 @@ fn manifest_revalidation_with_unchanged_manifest_is_fresh() {
.expect("second run should accept revalidation of the same manifest");
assert_eq!(second.source, PublicationPointSource::Fresh);
assert!(second.warnings.is_empty());
assert_eq!(second.pack.manifest_bytes, first.pack.manifest_bytes);
assert_eq!(
second.pack.manifest_number_be,
first.pack.manifest_number_be
second.snapshot.manifest_bytes,
first.snapshot.manifest_bytes
);
assert_eq!(second.pack.files, first.pack.files);
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_uses_fetch_cache_pp() {
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",
);
@@ -439,16 +536,11 @@ fn manifest_rollback_is_treated_as_failed_fetch_and_uses_fetch_cache_pp() {
Some(issuer_ca_rsync_uri()),
t1,
)
.expect("first run builds and stores fetch_cache_pp pack");
.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 key = FetchCachePpKey::from_manifest_rsync_uri(&manifest_rsync_uri);
let stored = store
.get_fetch_cache_pp(&key)
.expect("get fetch_cache_pp")
.expect("fetch_cache_pp pack exists");
let mut bumped = FetchCachePpPack::decode(&stored).expect("decode stored pack");
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);
@@ -461,10 +553,14 @@ fn manifest_rollback_is_treated_as_failed_fetch_and_uses_fetch_cache_pp() {
break;
}
}
let bumped_bytes = bumped.encode().expect("encode bumped pack");
store
.put_fetch_cache_pp(&key, &bumped_bytes)
.expect("store bumped pack");
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,
@@ -475,9 +571,8 @@ fn manifest_rollback_is_treated_as_failed_fetch_and_uses_fetch_cache_pp() {
Some(issuer_ca_rsync_uri()),
t2,
)
.expect("second run should treat rollback as failed fetch and use cache");
assert_eq!(second.source, PublicationPointSource::FetchCachePp);
assert_eq!(second.pack, bumped);
.expect("second run should treat rollback as failed fetch and reuse current-instance VCIR");
assert_eq!(second.source, PublicationPointSource::VcirCurrentInstance);
assert!(
second
.warnings

View File

@@ -90,7 +90,7 @@ fn manifest_outside_publication_point_yields_no_usable_cache() {
)
.unwrap_err();
// With no cached pack available for this wrong publication point, we get NoUsableCache.
// With no reusable current-instance snapshot/VCIR for this wrong publication point, we get NoUsableCache.
assert!(
matches!(err, ManifestProcessError::NoUsableCache { .. }),
"{err}"
@@ -98,7 +98,7 @@ fn manifest_outside_publication_point_yields_no_usable_cache() {
}
#[test]
fn manifest_outside_publication_point_detects_cached_pack_pp_mismatch() {
fn manifest_outside_publication_point_detects_current_instance_snapshot_pp_mismatch() {
let manifest_path = Path::new(
"tests/fixtures/repository/rpki.cernet.net/repo/cernet/0/05FC9C5B88506F7C0D3F862C8895BED67E9F8EBA.mft",
);
@@ -133,7 +133,7 @@ fn manifest_outside_publication_point_detects_cached_pack_pp_mismatch() {
let policy = Policy::default();
let issuer_ca_der = issuer_ca_fixture_der();
// First run creates and stores fetch_cache_pp pack (Fresh).
// First run validates a fresh publication point snapshot and stores the latest VCIR.
let first = process_manifest_publication_point(
&store,
&policy,
@@ -147,7 +147,7 @@ fn manifest_outside_publication_point_detects_cached_pack_pp_mismatch() {
assert_eq!(first.source, PublicationPointSource::Fresh);
// Second run with wrong publication point: fresh fails outside PP; cache load also fails
// because the cached pack's publication_point_rsync_uri doesn't match the expected one.
// because the reusable current-instance snapshot carries a different publication_point_rsync_uri.
let wrong_pp = "rsync://example.test/not-the-pp/";
let err = process_manifest_publication_point(
&store,

View File

@@ -1,8 +1,14 @@
use std::path::Path;
use sha2::Digest;
use rpki::data_model::manifest::ManifestObject;
use rpki::policy::{CaFailedFetchPolicy, Policy};
use rpki::storage::{FetchCachePpKey, FetchCachePpPack, RocksStore};
use rpki::storage::{
PackTime, RawByHashEntry, RocksStore, ValidatedCaInstanceResult, ValidatedManifestMeta,
VcirArtifactKind, VcirArtifactRole, VcirArtifactValidationStatus, VcirAuditSummary,
VcirInstanceGate, VcirRelatedArtifact, VcirSummary,
};
use rpki::validation::manifest::{
ManifestProcessError, PublicationPointSource, process_manifest_publication_point,
process_manifest_publication_point_after_repo_sync,
@@ -41,6 +47,76 @@ fn fixture_dir_to_rsync_uri(dir: &Path) -> String {
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");
}
fn store_manifest_and_locked_files(
store: &RocksStore,
manifest_path: &Path,
@@ -70,7 +146,7 @@ fn store_manifest_and_locked_files(
}
#[test]
fn repo_sync_failed_can_fall_back_to_fetch_cache_pp_when_present() {
fn repo_sync_failed_can_reuse_current_instance_vcir_when_present() {
let manifest_path = Path::new(
"tests/fixtures/repository/rpki.cernet.net/repo/cernet/0/05FC9C5B88506F7C0D3F862C8895BED67E9F8EBA.mft",
);
@@ -94,7 +170,7 @@ fn repo_sync_failed_can_fall_back_to_fetch_cache_pp_when_present() {
let issuer_ca_der = issuer_ca_fixture_der();
// First run: build and store a valid fetch_cache_pp pack (Fresh).
// First run: build a fresh publication point result, then seed a current-instance VCIR baseline.
let policy = Policy::default();
let first = process_manifest_publication_point(
&store,
@@ -108,7 +184,16 @@ fn repo_sync_failed_can_fall_back_to_fetch_cache_pp_when_present() {
.expect("first run ok");
assert_eq!(first.source, PublicationPointSource::Fresh);
// Second run: simulate RRDP/rsync repo sync failure and ensure we still accept the cached pack.
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,
);
// Seed a latest current-instance VCIR baseline, then simulate RRDP/rsync repo sync failure.
let second = process_manifest_publication_point_after_repo_sync(
&store,
&policy,
@@ -120,83 +205,13 @@ fn repo_sync_failed_can_fall_back_to_fetch_cache_pp_when_present() {
false,
Some("repo sync failed in test"),
)
.expect("repo sync failure should fall back to fetch_cache_pp");
assert_eq!(second.source, PublicationPointSource::FetchCachePp);
assert_eq!(second.pack, first.pack);
assert!(!second.warnings.is_empty());
}
#[test]
fn cached_pack_manifest_rsync_uri_mismatch_is_rejected_as_invalid_pack() {
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_manifest_and_locked_files(
&store,
manifest_path,
&manifest_rsync_uri,
&publication_point_rsync_uri,
&manifest,
&manifest_bytes,
);
let issuer_ca_der = issuer_ca_fixture_der();
let policy = Policy::default();
// First run stores a valid pack.
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 stores pack");
// Corrupt cached pack metadata: manifest_rsync_uri doesn't match the key.
let key = FetchCachePpKey::from_manifest_rsync_uri(&manifest_rsync_uri);
let cached_bytes = store
.get_fetch_cache_pp(&key)
.expect("get fetch_cache_pp")
.expect("fetch_cache_pp exists");
let mut pack = FetchCachePpPack::decode(&cached_bytes).expect("decode pack");
pack.manifest_rsync_uri = "rsync://example.test/wrong.mft".to_string();
store
.put_fetch_cache_pp(&key, &pack.encode().expect("encode pack"))
.expect("store corrupted pack");
// Force fresh failure and trigger cache load.
let err = process_manifest_publication_point_after_repo_sync(
&store,
&policy,
&manifest_rsync_uri,
&publication_point_rsync_uri,
&issuer_ca_der,
Some(issuer_ca_rsync_uri()),
validation_time,
false,
Some("repo sync failed in test"),
)
.unwrap_err();
.expect("repo sync failure should reuse current-instance VCIR");
assert_eq!(second.source, PublicationPointSource::VcirCurrentInstance);
assert!(
matches!(err, ManifestProcessError::NoUsableCache { .. }),
"{err}"
);
assert!(
err.to_string()
.contains("cached pack manifest_rsync_uri does not match key"),
"unexpected error: {err}"
second.warnings.iter().any(|w| w
.message
.contains("using latest validated result for current CA instance")),
"expected current-instance VCIR reuse warning"
);
}
@@ -269,7 +284,7 @@ fn manifest_missing_locked_file_is_treated_as_failed_fetch() {
}
#[test]
fn manifest_number_increases_but_this_update_not_increasing_is_failed_fetch() {
fn manifest_number_increases_but_this_update_not_increasing_reuses_current_instance_vcir() {
let manifest_path = Path::new(
"tests/fixtures/repository/rpki.cernet.net/repo/cernet/0/05FC9C5B88506F7C0D3F862C8895BED67E9F8EBA.mft",
);
@@ -294,7 +309,7 @@ fn manifest_number_increases_but_this_update_not_increasing_is_failed_fetch() {
let issuer_ca_der = issuer_ca_fixture_der();
let policy = Policy::default();
// Build and store a valid pack first.
// Build and store a valid publication point snapshot first.
let _ = process_manifest_publication_point(
&store,
&policy,
@@ -304,23 +319,18 @@ fn manifest_number_increases_but_this_update_not_increasing_is_failed_fetch() {
Some(issuer_ca_rsync_uri()),
validation_time,
)
.expect("first run stores pack");
.expect("first run returns validated publication point");
// Replace the cached pack with an "older" manifestNumber but a newer thisUpdate to trigger
// Seed VCIR with an older manifestNumber but a newer thisUpdate to trigger
// RFC 9286 §4.2.1 thisUpdate monotonicity failure on the fresh path.
let key = FetchCachePpKey::from_manifest_rsync_uri(&manifest_rsync_uri);
let cached_bytes = store
.get_fetch_cache_pp(&key)
.expect("get fetch_cache_pp")
.expect("fetch_cache_pp exists");
let mut old_pack = FetchCachePpPack::decode(&cached_bytes).expect("decode pack");
old_pack.manifest_number_be = vec![0];
old_pack.this_update = rpki::storage::PackTime::from_utc_offset_datetime(
store_validated_manifest_baseline(
&store,
&manifest_rsync_uri,
&manifest_bytes,
vec![0],
manifest.manifest.this_update + time::Duration::hours(24),
manifest.manifest.next_update,
);
store
.put_fetch_cache_pp(&key, &old_pack.encode().expect("encode pack"))
.expect("store adjusted pack");
let out = process_manifest_publication_point(
&store,
@@ -331,8 +341,8 @@ fn manifest_number_increases_but_this_update_not_increasing_is_failed_fetch() {
Some(issuer_ca_rsync_uri()),
validation_time,
)
.expect("should fall back to fetch_cache_pp");
assert_eq!(out.source, PublicationPointSource::FetchCachePp);
.expect("should reuse current-instance VCIR");
assert_eq!(out.source, PublicationPointSource::VcirCurrentInstance);
assert!(
out.warnings
.iter()
@@ -368,7 +378,7 @@ fn manifest_number_equal_but_bytes_differ_is_rejected_without_cache() {
let mut policy = Policy::default();
policy.ca_failed_fetch_policy = CaFailedFetchPolicy::StopAllOutput;
// Store a cached pack that has the same manifestNumber but different manifest bytes.
// Store a cached snapshot that has the same manifestNumber but different manifest bytes.
let _ = process_manifest_publication_point(
&store,
&policy,
@@ -378,18 +388,18 @@ fn manifest_number_equal_but_bytes_differ_is_rejected_without_cache() {
Some(issuer_ca_rsync_uri()),
validation_time,
)
.expect("first run stores pack");
.expect("first run returns validated publication point");
let key = FetchCachePpKey::from_manifest_rsync_uri(&manifest_rsync_uri);
let cached_bytes = store
.get_fetch_cache_pp(&key)
.expect("get fetch_cache_pp")
.expect("fetch_cache_pp exists");
let mut pack = FetchCachePpPack::decode(&cached_bytes).expect("decode pack");
pack.manifest_bytes[0] ^= 0xFF;
store
.put_fetch_cache_pp(&key, &pack.encode().expect("encode pack"))
.expect("store adjusted pack");
let mut mutated_manifest_bytes = manifest_bytes.clone();
mutated_manifest_bytes[0] ^= 0xFF;
store_validated_manifest_baseline(
&store,
&manifest_rsync_uri,
&mutated_manifest_bytes,
manifest.manifest.manifest_number.bytes_be.clone(),
manifest.manifest.this_update,
manifest.manifest.next_update,
);
let err = process_manifest_publication_point(
&store,
@@ -455,154 +465,3 @@ fn manifest_embedded_ee_cert_path_validation_fails_with_wrong_issuer_ca() {
"unexpected error: {err}"
);
}
#[test]
fn cached_pack_missing_file_is_rejected_during_revalidation() {
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_manifest_and_locked_files(
&store,
manifest_path,
&manifest_rsync_uri,
&publication_point_rsync_uri,
&manifest,
&manifest_bytes,
);
let issuer_ca_der = issuer_ca_fixture_der();
let policy = Policy::default();
// Store a valid pack first.
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 stores pack");
// Corrupt cached pack by removing one referenced file.
let key = FetchCachePpKey::from_manifest_rsync_uri(&manifest_rsync_uri);
let cached_bytes = store
.get_fetch_cache_pp(&key)
.expect("get fetch_cache_pp")
.expect("fetch_cache_pp exists");
let mut pack = FetchCachePpPack::decode(&cached_bytes).expect("decode pack");
assert!(!pack.files.is_empty(), "fixture should lock some files");
pack.files.pop();
store
.put_fetch_cache_pp(&key, &pack.encode().expect("encode pack"))
.expect("store corrupted pack");
// Force the fresh path to fail and trigger cache revalidation.
let err = process_manifest_publication_point_after_repo_sync(
&store,
&policy,
&manifest_rsync_uri,
&publication_point_rsync_uri,
&issuer_ca_der,
Some(issuer_ca_rsync_uri()),
validation_time,
false,
Some("repo sync failed in test"),
)
.unwrap_err();
assert!(
matches!(err, ManifestProcessError::NoUsableCache { .. }),
"{err}"
);
assert!(
err.to_string()
.contains("cached fetch_cache_pp missing file"),
"unexpected error: {err}"
);
}
#[test]
fn cached_pack_hash_mismatch_is_rejected_during_revalidation() {
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_manifest_and_locked_files(
&store,
manifest_path,
&manifest_rsync_uri,
&publication_point_rsync_uri,
&manifest,
&manifest_bytes,
);
let issuer_ca_der = issuer_ca_fixture_der();
let policy = Policy::default();
// Store a valid pack first.
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 stores pack");
// Corrupt cached pack by changing one file's bytes+sha256 so internal validation passes,
// but the manifest fileList binding check fails (RFC 9286 §6.5).
let key = FetchCachePpKey::from_manifest_rsync_uri(&manifest_rsync_uri);
let cached_bytes = store
.get_fetch_cache_pp(&key)
.expect("get fetch_cache_pp")
.expect("fetch_cache_pp exists");
let mut pack = FetchCachePpPack::decode(&cached_bytes).expect("decode pack");
let victim = pack.files.first_mut().expect("non-empty file list");
victim.bytes[0] ^= 0xFF;
victim.sha256 = victim.compute_sha256();
store
.put_fetch_cache_pp(&key, &pack.encode().expect("encode pack"))
.expect("store corrupted pack");
let err = process_manifest_publication_point_after_repo_sync(
&store,
&policy,
&manifest_rsync_uri,
&publication_point_rsync_uri,
&issuer_ca_der,
Some(issuer_ca_rsync_uri()),
validation_time,
false,
Some("repo sync failed in test"),
)
.unwrap_err();
assert!(
matches!(err, ManifestProcessError::NoUsableCache { .. }),
"{err}"
);
assert!(
err.to_string()
.contains("cached fetch_cache_pp file hash mismatch"),
"unexpected error: {err}"
);
}

View File

@@ -2,7 +2,7 @@ use std::path::Path;
use rpki::data_model::manifest::ManifestObject;
use rpki::policy::{CaFailedFetchPolicy, Policy};
use rpki::storage::{FetchCachePpKey, RocksStore};
use rpki::storage::RocksStore;
use rpki::validation::manifest::process_manifest_publication_point;
fn issuer_ca_fixture() -> Vec<u8> {
@@ -70,12 +70,11 @@ fn manifest_outside_publication_point_is_failed_fetch_rfc9286_section6_1() {
let msg = err.to_string();
assert!(msg.contains("RFC 9286 §6.1"), "{msg}");
let key = FetchCachePpKey::from_manifest_rsync_uri(manifest_rsync_uri);
assert!(
store
.get_fetch_cache_pp(&key)
.expect("get fetch_cache_pp")
.get_vcir(manifest_rsync_uri)
.expect("get vcir")
.is_none(),
"must not write fetch_cache_pp on failed fetch"
"must not write VCIR on failed fetch"
);
}

View File

@@ -6,7 +6,7 @@ use rpki::data_model::rc::ResourceCertificate;
use rpki::policy::{Policy, SignedObjectFailurePolicy};
use rpki::storage::{PackFile, RocksStore};
use rpki::validation::manifest::process_manifest_publication_point;
use rpki::validation::objects::process_fetch_cache_pp_pack_for_issuer;
use rpki::validation::objects::process_publication_point_snapshot_for_issuer;
fn fixture_to_rsync_uri(path: &Path) -> String {
let rel = path
@@ -31,7 +31,7 @@ fn fixture_dir_to_rsync_uri(dir: &Path) -> String {
}
fn build_cernet_pack_and_validation_time() -> (
rpki::storage::FetchCachePpPack,
rpki::validation::publication_point::PublicationPointSnapshot,
time::OffsetDateTime,
Vec<u8>,
ResourceCertificate,
@@ -85,11 +85,11 @@ fn build_cernet_pack_and_validation_time() -> (
let issuer_ca = ResourceCertificate::decode_der(&issuer_ca_der).expect("decode issuer CA cert");
let crl_file = out
.pack
.snapshot
.files
.iter()
.find(|f| f.rsync_uri.ends_with(".crl"))
.expect("crl present in pack");
.expect("crl present in snapshot");
let crl = RpkixCrl::decode_der(&crl_file.bytes).expect("decode crl");
let mut t = manifest.manifest.this_update;
@@ -101,7 +101,7 @@ fn build_cernet_pack_and_validation_time() -> (
}
t += time::Duration::seconds(1);
(out.pack, t, issuer_ca_der, issuer_ca)
(out.snapshot, t, issuer_ca_der, issuer_ca)
}
#[test]
@@ -113,7 +113,7 @@ fn missing_crl_causes_roas_to_be_dropped_under_drop_object_policy() {
let mut policy = Policy::default();
policy.signed_object_failure_policy = SignedObjectFailurePolicy::DropObject;
let out = process_fetch_cache_pp_pack_for_issuer(
let out = process_publication_point_snapshot_for_issuer(
&pack,
&policy,
&issuer_ca_der,
@@ -139,7 +139,7 @@ fn wrong_issuer_ca_cert_causes_roas_to_be_dropped_under_drop_object_policy() {
// Use an unrelated trust anchor certificate as the issuer to force EE cert path validation to fail.
let wrong_issuer_ca_der =
std::fs::read("tests/fixtures/ta/arin-ta.cer").expect("read wrong issuer ca");
let out = process_fetch_cache_pp_pack_for_issuer(
let out = process_publication_point_snapshot_for_issuer(
&pack,
&policy,
&wrong_issuer_ca_der,
@@ -167,7 +167,7 @@ fn invalid_aspa_object_is_reported_as_warning_under_drop_object_policy() {
let mut policy = Policy::default();
policy.signed_object_failure_policy = SignedObjectFailurePolicy::DropObject;
let out = process_fetch_cache_pp_pack_for_issuer(
let out = process_publication_point_snapshot_for_issuer(
&pack,
&policy,
&issuer_ca_der,

View File

@@ -6,7 +6,7 @@ use rpki::data_model::rc::ResourceCertificate;
use rpki::policy::{Policy, SignedObjectFailurePolicy};
use rpki::storage::{PackFile, RocksStore};
use rpki::validation::manifest::process_manifest_publication_point;
use rpki::validation::objects::process_fetch_cache_pp_pack_for_issuer;
use rpki::validation::objects::process_publication_point_snapshot_for_issuer;
fn fixture_to_rsync_uri(path: &Path) -> String {
let rel = path
@@ -31,7 +31,7 @@ fn fixture_dir_to_rsync_uri(dir: &Path) -> String {
}
fn build_cernet_pack_and_validation_time() -> (
rpki::storage::FetchCachePpPack,
rpki::validation::publication_point::PublicationPointSnapshot,
time::OffsetDateTime,
Vec<u8>,
ResourceCertificate,
@@ -85,11 +85,11 @@ fn build_cernet_pack_and_validation_time() -> (
let issuer_ca = ResourceCertificate::decode_der(&issuer_ca_der).expect("decode issuer CA cert");
let crl_file = out
.pack
.snapshot
.files
.iter()
.find(|f| f.rsync_uri.ends_with(".crl"))
.expect("crl present in pack");
.expect("crl present in snapshot");
let crl = RpkixCrl::decode_der(&crl_file.bytes).expect("decode crl");
// Choose a validation_time that is within:
@@ -106,7 +106,7 @@ fn build_cernet_pack_and_validation_time() -> (
}
t += time::Duration::seconds(1);
(out.pack, t, issuer_ca_der, issuer_ca)
(out.snapshot, t, issuer_ca_der, issuer_ca)
}
#[test]
@@ -119,13 +119,13 @@ fn drop_object_policy_drops_only_failing_object() {
.iter()
.find(|f| f.rsync_uri.ends_with("AS4538.roa"))
.map(|f| f.rsync_uri.clone())
.expect("AS4538.roa present in pack");
.expect("AS4538.roa present in snapshot");
let tamper_idx = pack
.files
.iter()
.position(|f| f.rsync_uri.ends_with(".roa") && f.rsync_uri != valid_roa_uri)
.expect("another ROA present in pack");
.expect("another ROA present in snapshot");
let victim_uri = pack.files[tamper_idx].rsync_uri.clone();
let mut tampered = pack.files[tamper_idx].bytes.clone();
@@ -136,7 +136,7 @@ fn drop_object_policy_drops_only_failing_object() {
let mut policy = Policy::default();
policy.signed_object_failure_policy = SignedObjectFailurePolicy::DropObject;
let out = process_fetch_cache_pp_pack_for_issuer(
let out = process_publication_point_snapshot_for_issuer(
&pack,
&policy,
&issuer_ca_der,
@@ -168,7 +168,7 @@ fn drop_publication_point_policy_drops_the_publication_point() {
.files
.iter()
.position(|f| f.rsync_uri.ends_with(".roa"))
.expect("a ROA present in pack");
.expect("a ROA present in snapshot");
let victim_uri = pack.files[tamper_idx].rsync_uri.clone();
let mut tampered = pack.files[tamper_idx].bytes.clone();
@@ -179,7 +179,7 @@ fn drop_publication_point_policy_drops_the_publication_point() {
let mut policy = Policy::default();
policy.signed_object_failure_policy = SignedObjectFailurePolicy::DropPublicationPoint;
let out = process_fetch_cache_pp_pack_for_issuer(
let out = process_publication_point_snapshot_for_issuer(
&pack,
&policy,
&issuer_ca_der,

View File

@@ -1,10 +1,11 @@
use rpki::fetch::rsync::LocalDirRsyncFetcher;
use rpki::policy::{Policy, SignedObjectFailurePolicy, SyncPreference};
use rpki::storage::{FetchCachePpPack, PackFile, PackTime, RocksStore};
use rpki::storage::{PackFile, PackTime, RocksStore};
use rpki::sync::repo::sync_publication_point;
use rpki::sync::rrdp::Fetcher;
use rpki::validation::manifest::process_manifest_publication_point;
use rpki::validation::objects::process_fetch_cache_pp_pack_for_issuer;
use rpki::validation::objects::process_publication_point_snapshot_for_issuer;
use rpki::validation::publication_point::PublicationPointSnapshot;
struct NoopHttpFetcher;
impl Fetcher for NoopHttpFetcher {
@@ -50,16 +51,16 @@ fn issuer_ca_rsync_uri() -> &'static str {
"rsync://rpki.apnic.net/repository/B527EF581D6611E2BB468F7C72FD1FF2/BfycW4hQb3wNP4YsiJW-1n6fjro.cer"
}
fn minimal_pack(
fn minimal_snapshot(
manifest_rsync_uri: &str,
publication_point_rsync_uri: &str,
manifest_bytes: Vec<u8>,
files: Vec<PackFile>,
validation_time: time::OffsetDateTime,
) -> FetchCachePpPack {
// Keep times consistent enough to pass internal pack validation.
FetchCachePpPack {
format_version: FetchCachePpPack::FORMAT_VERSION_V1,
) -> PublicationPointSnapshot {
// Keep times consistent enough for a valid snapshot.
PublicationPointSnapshot {
format_version: PublicationPointSnapshot::FORMAT_VERSION_V1,
manifest_rsync_uri: manifest_rsync_uri.to_string(),
publication_point_rsync_uri: publication_point_rsync_uri.to_string(),
manifest_number_be: vec![1],
@@ -71,12 +72,12 @@ fn minimal_pack(
}
}
fn build_fetch_cache_pp_from_local_rsync_fixture(
fn build_publication_point_snapshot_from_local_rsync_fixture(
dir: &std::path::Path,
rsync_base_uri: &str,
manifest_rsync_uri: &str,
validation_time: time::OffsetDateTime,
) -> rpki::storage::FetchCachePpPack {
) -> rpki::validation::publication_point::PublicationPointSnapshot {
let store_dir = tempfile::tempdir().expect("store dir");
let store = RocksStore::open(store_dir.path()).expect("open rocksdb");
let policy = Policy {
@@ -107,16 +108,16 @@ fn build_fetch_cache_pp_from_local_rsync_fixture(
)
.expect("process manifest");
pp.pack
pp.snapshot
}
#[test]
fn process_pack_for_issuer_extracts_vrps_from_real_cernet_fixture() {
fn process_snapshot_for_issuer_extracts_vrps_from_real_cernet_fixture() {
let (dir, rsync_base_uri, manifest_file) = cernet_fixture();
let manifest_rsync_uri = format!("{rsync_base_uri}{manifest_file}");
let validation_time = validation_time_from_manifest_fixture(&dir, &manifest_file);
let pack = build_fetch_cache_pp_from_local_rsync_fixture(
let pack = build_publication_point_snapshot_from_local_rsync_fixture(
&dir,
&rsync_base_uri,
&manifest_rsync_uri,
@@ -128,7 +129,7 @@ fn process_pack_for_issuer_extracts_vrps_from_real_cernet_fixture() {
.expect("decode issuer ca");
let policy = Policy::default();
let out = process_fetch_cache_pp_pack_for_issuer(
let out = process_publication_point_snapshot_for_issuer(
&pack,
&policy,
&issuer_ca_der,
@@ -153,7 +154,7 @@ fn signed_object_failure_policy_drop_object_drops_only_bad_object() {
let manifest_rsync_uri = format!("{rsync_base_uri}{manifest_file}");
let validation_time = validation_time_from_manifest_fixture(&dir, &manifest_file);
let mut pack = build_fetch_cache_pp_from_local_rsync_fixture(
let mut pack = build_publication_point_snapshot_from_local_rsync_fixture(
&dir,
&rsync_base_uri,
&manifest_rsync_uri,
@@ -171,7 +172,7 @@ fn signed_object_failure_policy_drop_object_drops_only_bad_object() {
.files
.iter()
.position(|f| f.rsync_uri.ends_with(".roa"))
.expect("pack contains roa");
.expect("snapshot contains roa");
let bad_uri = pack.files[bad_idx].rsync_uri.clone();
pack.files[bad_idx] = PackFile::from_bytes_compute_sha256(bad_uri, vec![0u8]);
@@ -183,7 +184,7 @@ fn signed_object_failure_policy_drop_object_drops_only_bad_object() {
signed_object_failure_policy: SignedObjectFailurePolicy::DropObject,
..Policy::default()
};
let out = process_fetch_cache_pp_pack_for_issuer(
let out = process_publication_point_snapshot_for_issuer(
&pack,
&policy,
&issuer_ca_der,
@@ -216,7 +217,7 @@ fn signed_object_failure_policy_drop_publication_point_drops_all_output() {
let manifest_rsync_uri = format!("{rsync_base_uri}{manifest_file}");
let validation_time = validation_time_from_manifest_fixture(&dir, &manifest_file);
let mut pack = build_fetch_cache_pp_from_local_rsync_fixture(
let mut pack = build_publication_point_snapshot_from_local_rsync_fixture(
&dir,
&rsync_base_uri,
&manifest_rsync_uri,
@@ -234,7 +235,7 @@ fn signed_object_failure_policy_drop_publication_point_drops_all_output() {
.files
.iter()
.position(|f| f.rsync_uri.ends_with(".roa"))
.expect("pack contains roa");
.expect("snapshot contains roa");
let bad_uri = pack.files[bad_idx].rsync_uri.clone();
pack.files[bad_idx] = PackFile::from_bytes_compute_sha256(bad_uri, vec![0u8]);
@@ -246,7 +247,7 @@ fn signed_object_failure_policy_drop_publication_point_drops_all_output() {
signed_object_failure_policy: SignedObjectFailurePolicy::DropPublicationPoint,
..Policy::default()
};
let out = process_fetch_cache_pp_pack_for_issuer(
let out = process_publication_point_snapshot_for_issuer(
&pack,
&policy,
&issuer_ca_der,
@@ -283,13 +284,13 @@ fn signed_object_failure_policy_drop_publication_point_drops_all_output() {
}
#[test]
fn process_pack_for_issuer_without_crl_drops_publication_point() {
fn process_snapshot_for_issuer_without_crl_drops_publication_point() {
let (dir, rsync_base_uri, manifest_file) = cernet_fixture();
let manifest_rsync_uri = format!("{rsync_base_uri}{manifest_file}");
let validation_time = validation_time_from_manifest_fixture(&dir, &manifest_file);
let manifest_bytes = std::fs::read(dir.join(&manifest_file)).expect("read mft");
let pack = minimal_pack(
let pack = minimal_snapshot(
&manifest_rsync_uri,
&rsync_base_uri,
manifest_bytes,
@@ -301,7 +302,7 @@ fn process_pack_for_issuer_without_crl_drops_publication_point() {
);
let policy = Policy::default();
let out = process_fetch_cache_pp_pack_for_issuer(
let out = process_publication_point_snapshot_for_issuer(
&pack,
&policy,
&[],
@@ -318,7 +319,7 @@ fn process_pack_for_issuer_without_crl_drops_publication_point() {
}
#[test]
fn process_pack_for_issuer_handles_invalid_aspa_bytes() {
fn process_snapshot_for_issuer_handles_invalid_aspa_bytes() {
let (dir, rsync_base_uri, manifest_file) = cernet_fixture();
let manifest_rsync_uri = format!("{rsync_base_uri}{manifest_file}");
let validation_time = validation_time_from_manifest_fixture(&dir, &manifest_file);
@@ -326,7 +327,7 @@ fn process_pack_for_issuer_handles_invalid_aspa_bytes() {
let crl_bytes =
std::fs::read(dir.join("05FC9C5B88506F7C0D3F862C8895BED67E9F8EBA.crl")).expect("read crl");
let pack = minimal_pack(
let pack = minimal_snapshot(
&manifest_rsync_uri,
&rsync_base_uri,
manifest_bytes,
@@ -344,7 +345,7 @@ fn process_pack_for_issuer_handles_invalid_aspa_bytes() {
signed_object_failure_policy: SignedObjectFailurePolicy::DropObject,
..Policy::default()
};
let out = process_fetch_cache_pp_pack_for_issuer(
let out = process_publication_point_snapshot_for_issuer(
&pack,
&policy,
&[],
@@ -359,7 +360,7 @@ fn process_pack_for_issuer_handles_invalid_aspa_bytes() {
}
#[test]
fn process_pack_for_issuer_drop_publication_point_on_invalid_aspa_bytes() {
fn process_snapshot_for_issuer_drop_publication_point_on_invalid_aspa_bytes() {
let (dir, rsync_base_uri, manifest_file) = cernet_fixture();
let manifest_rsync_uri = format!("{rsync_base_uri}{manifest_file}");
let validation_time = validation_time_from_manifest_fixture(&dir, &manifest_file);
@@ -367,7 +368,7 @@ fn process_pack_for_issuer_drop_publication_point_on_invalid_aspa_bytes() {
let crl_bytes =
std::fs::read(dir.join("05FC9C5B88506F7C0D3F862C8895BED67E9F8EBA.crl")).expect("read crl");
let pack = minimal_pack(
let pack = minimal_snapshot(
&manifest_rsync_uri,
&rsync_base_uri,
manifest_bytes,
@@ -387,7 +388,7 @@ fn process_pack_for_issuer_drop_publication_point_on_invalid_aspa_bytes() {
signed_object_failure_policy: SignedObjectFailurePolicy::DropPublicationPoint,
..Policy::default()
};
let out = process_fetch_cache_pp_pack_for_issuer(
let out = process_publication_point_snapshot_for_issuer(
&pack,
&policy,
&[],

View File

@@ -1,16 +1,17 @@
use rpki::policy::{Policy, SignedObjectFailurePolicy};
use rpki::storage::{FetchCachePpPack, PackFile, PackTime};
use rpki::validation::objects::process_fetch_cache_pp_pack_for_issuer;
use rpki::storage::{PackFile, PackTime};
use rpki::validation::objects::process_publication_point_snapshot_for_issuer;
use rpki::validation::publication_point::PublicationPointSnapshot;
fn fixture_bytes(path: &str) -> Vec<u8> {
std::fs::read(std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR")).join(path))
.unwrap_or_else(|e| panic!("read fixture {path}: {e}"))
}
fn dummy_pack(manifest_bytes: Vec<u8>, files: Vec<PackFile>) -> FetchCachePpPack {
fn dummy_snapshot(manifest_bytes: Vec<u8>, files: Vec<PackFile>) -> PublicationPointSnapshot {
let now = time::OffsetDateTime::now_utc();
FetchCachePpPack {
format_version: FetchCachePpPack::FORMAT_VERSION_V1,
PublicationPointSnapshot {
format_version: PublicationPointSnapshot::FORMAT_VERSION_V1,
manifest_rsync_uri: "rsync://example.test/repo/pp/manifest.mft".to_string(),
publication_point_rsync_uri: "rsync://example.test/repo/pp/".to_string(),
manifest_number_be: vec![1],
@@ -23,7 +24,7 @@ fn dummy_pack(manifest_bytes: Vec<u8>, files: Vec<PackFile>) -> FetchCachePpPack
}
#[test]
fn process_pack_drop_object_on_wrong_issuer_ca_for_roa() {
fn process_snapshot_drop_object_on_wrong_issuer_ca_for_roa() {
let manifest_bytes = fixture_bytes(
"tests/fixtures/repository/rpki.cernet.net/repo/cernet/0/05FC9C5B88506F7C0D3F862C8895BED67E9F8EBA.mft",
);
@@ -43,7 +44,7 @@ fn process_pack_drop_object_on_wrong_issuer_ca_for_roa() {
"tests/fixtures/repository/rpki.cernet.net/repo/cernet/0/05FC9C5B88506F7C0D3F862C8895BED67E9F8EBA.crl",
);
let pack = dummy_pack(
let pack = dummy_snapshot(
manifest_bytes,
vec![
PackFile::from_bytes_compute_sha256(ee_crldp, crl_bytes),
@@ -60,7 +61,7 @@ fn process_pack_drop_object_on_wrong_issuer_ca_for_roa() {
};
let wrong_issuer_ca_der = fixture_bytes("tests/fixtures/ta/arin-ta.cer");
let out = process_fetch_cache_pp_pack_for_issuer(
let out = process_publication_point_snapshot_for_issuer(
&pack,
&policy,
&wrong_issuer_ca_der,
@@ -78,7 +79,7 @@ fn process_pack_drop_object_on_wrong_issuer_ca_for_roa() {
}
#[test]
fn process_pack_drop_publication_point_on_wrong_issuer_ca_for_roa_skips_rest() {
fn process_snapshot_drop_publication_point_on_wrong_issuer_ca_for_roa_skips_rest() {
let manifest_bytes = fixture_bytes(
"tests/fixtures/repository/rpki.cernet.net/repo/cernet/0/05FC9C5B88506F7C0D3F862C8895BED67E9F8EBA.mft",
);
@@ -101,7 +102,7 @@ fn process_pack_drop_publication_point_on_wrong_issuer_ca_for_roa_skips_rest() {
"tests/fixtures/repository/chloe.sobornost.net/rpki/RIPE-nljobsnijders/5m80fwYws_3FiFD7JiQjAqZ1RYQ.asa",
);
let pack = dummy_pack(
let pack = dummy_snapshot(
manifest_bytes,
vec![
PackFile::from_bytes_compute_sha256(ee_crldp, crl_bytes),
@@ -123,7 +124,7 @@ fn process_pack_drop_publication_point_on_wrong_issuer_ca_for_roa_skips_rest() {
};
let wrong_issuer_ca_der = fixture_bytes("tests/fixtures/ta/arin-ta.cer");
let out = process_fetch_cache_pp_pack_for_issuer(
let out = process_publication_point_snapshot_for_issuer(
&pack,
&policy,
&wrong_issuer_ca_der,
@@ -138,7 +139,7 @@ fn process_pack_drop_publication_point_on_wrong_issuer_ca_for_roa_skips_rest() {
}
#[test]
fn process_pack_drop_object_on_wrong_issuer_ca_for_aspa() {
fn process_snapshot_drop_object_on_wrong_issuer_ca_for_aspa() {
let manifest_bytes = fixture_bytes(
"tests/fixtures/repository/rpki.cernet.net/repo/cernet/0/05FC9C5B88506F7C0D3F862C8895BED67E9F8EBA.mft",
);
@@ -159,7 +160,7 @@ fn process_pack_drop_object_on_wrong_issuer_ca_for_aspa() {
"tests/fixtures/repository/rpki.cernet.net/repo/cernet/0/05FC9C5B88506F7C0D3F862C8895BED67E9F8EBA.crl",
);
let pack = dummy_pack(
let pack = dummy_snapshot(
manifest_bytes,
vec![
PackFile::from_bytes_compute_sha256(ee_crldp, crl_bytes),
@@ -173,7 +174,7 @@ fn process_pack_drop_object_on_wrong_issuer_ca_for_aspa() {
};
let wrong_issuer_ca_der = fixture_bytes("tests/fixtures/ta/arin-ta.cer");
let out = process_fetch_cache_pp_pack_for_issuer(
let out = process_publication_point_snapshot_for_issuer(
&pack,
&policy,
&wrong_issuer_ca_der,
@@ -191,7 +192,7 @@ fn process_pack_drop_object_on_wrong_issuer_ca_for_aspa() {
}
#[test]
fn process_pack_drop_publication_point_on_wrong_issuer_ca_for_aspa_skips_rest() {
fn process_snapshot_drop_publication_point_on_wrong_issuer_ca_for_aspa_skips_rest() {
let manifest_bytes = fixture_bytes(
"tests/fixtures/repository/rpki.cernet.net/repo/cernet/0/05FC9C5B88506F7C0D3F862C8895BED67E9F8EBA.mft",
);
@@ -214,7 +215,7 @@ fn process_pack_drop_publication_point_on_wrong_issuer_ca_for_aspa_skips_rest()
"tests/fixtures/repository/rpki.cernet.net/repo/cernet/0/05FC9C5B88506F7C0D3F862C8895BED67E9F8EBA.crl",
);
let pack = dummy_pack(
let pack = dummy_snapshot(
manifest_bytes,
vec![
PackFile::from_bytes_compute_sha256(ee_crldp, crl_bytes),
@@ -229,7 +230,7 @@ fn process_pack_drop_publication_point_on_wrong_issuer_ca_for_aspa_skips_rest()
};
let wrong_issuer_ca_der = fixture_bytes("tests/fixtures/ta/arin-ta.cer");
let out = process_fetch_cache_pp_pack_for_issuer(
let out = process_publication_point_snapshot_for_issuer(
&pack,
&policy,
&wrong_issuer_ca_der,
@@ -243,7 +244,7 @@ fn process_pack_drop_publication_point_on_wrong_issuer_ca_for_aspa_skips_rest()
}
#[test]
fn process_pack_for_issuer_marks_objects_skipped_when_missing_issuer_crl() {
fn process_snapshot_for_issuer_marks_objects_skipped_when_missing_issuer_crl() {
let manifest_bytes = fixture_bytes(
"tests/fixtures/repository/rpki.cernet.net/repo/cernet/0/05FC9C5B88506F7C0D3F862C8895BED67E9F8EBA.mft",
);
@@ -253,7 +254,7 @@ fn process_pack_for_issuer_marks_objects_skipped_when_missing_issuer_crl() {
"tests/fixtures/repository/chloe.sobornost.net/rpki/RIPE-nljobsnijders/5m80fwYws_3FiFD7JiQjAqZ1RYQ.asa",
);
let pack = dummy_pack(
let pack = dummy_snapshot(
manifest_bytes,
vec![
PackFile::from_bytes_compute_sha256("rsync://example.test/repo/pp/a.roa", roa_bytes),
@@ -266,7 +267,7 @@ fn process_pack_for_issuer_marks_objects_skipped_when_missing_issuer_crl() {
..Policy::default()
};
let out = process_fetch_cache_pp_pack_for_issuer(
let out = process_publication_point_snapshot_for_issuer(
&pack,
&policy,
&fixture_bytes("tests/fixtures/ta/apnic-ta.cer"),
@@ -287,12 +288,12 @@ fn process_pack_for_issuer_marks_objects_skipped_when_missing_issuer_crl() {
}
#[test]
fn process_pack_for_issuer_drop_object_records_errors_and_continues() {
fn process_snapshot_for_issuer_drop_object_records_errors_and_continues() {
let manifest_bytes = fixture_bytes(
"tests/fixtures/repository/rpki.cernet.net/repo/cernet/0/05FC9C5B88506F7C0D3F862C8895BED67E9F8EBA.mft",
);
let pack = dummy_pack(
let pack = dummy_snapshot(
manifest_bytes,
vec![
PackFile::from_bytes_compute_sha256(
@@ -309,7 +310,7 @@ fn process_pack_for_issuer_drop_object_records_errors_and_continues() {
..Policy::default()
};
let out = process_fetch_cache_pp_pack_for_issuer(
let out = process_publication_point_snapshot_for_issuer(
&pack,
&policy,
&fixture_bytes("tests/fixtures/ta/apnic-ta.cer"),
@@ -328,12 +329,12 @@ fn process_pack_for_issuer_drop_object_records_errors_and_continues() {
}
#[test]
fn process_pack_for_issuer_drop_publication_point_records_skips_for_rest() {
fn process_snapshot_for_issuer_drop_publication_point_records_skips_for_rest() {
let manifest_bytes = fixture_bytes(
"tests/fixtures/repository/rpki.cernet.net/repo/cernet/0/05FC9C5B88506F7C0D3F862C8895BED67E9F8EBA.mft",
);
let pack = dummy_pack(
let pack = dummy_snapshot(
manifest_bytes,
vec![
PackFile::from_bytes_compute_sha256(
@@ -351,7 +352,7 @@ fn process_pack_for_issuer_drop_publication_point_records_skips_for_rest() {
..Policy::default()
};
let out = process_fetch_cache_pp_pack_for_issuer(
let out = process_publication_point_snapshot_for_issuer(
&pack,
&policy,
&fixture_bytes("tests/fixtures/ta/apnic-ta.cer"),
@@ -370,7 +371,7 @@ fn process_pack_for_issuer_drop_publication_point_records_skips_for_rest() {
}
#[test]
fn process_pack_for_issuer_selects_crl_by_ee_crldp_uri_roa() {
fn process_snapshot_for_issuer_selects_crl_by_ee_crldp_uri_roa() {
let manifest_bytes = fixture_bytes(
"tests/fixtures/repository/rpki.cernet.net/repo/cernet/0/05FC9C5B88506F7C0D3F862C8895BED67E9F8EBA.mft",
);
@@ -389,7 +390,7 @@ fn process_pack_for_issuer_selects_crl_by_ee_crldp_uri_roa() {
// Provide a CRL file with the *exact* rsync URI referenced by the embedded EE certificate.
// Bytes need not be valid for this test: we just want to cover deterministic selection.
let pack = dummy_pack(
let pack = dummy_snapshot(
manifest_bytes,
vec![
PackFile::from_bytes_compute_sha256(ee_crldp, vec![0x01]),
@@ -402,7 +403,7 @@ fn process_pack_for_issuer_selects_crl_by_ee_crldp_uri_roa() {
..Policy::default()
};
let out = process_fetch_cache_pp_pack_for_issuer(
let out = process_publication_point_snapshot_for_issuer(
&pack,
&policy,
&fixture_bytes("tests/fixtures/ta/apnic-ta.cer"),
@@ -420,7 +421,7 @@ fn process_pack_for_issuer_selects_crl_by_ee_crldp_uri_roa() {
}
#[test]
fn process_pack_for_issuer_rejects_roa_when_crldp_crl_missing() {
fn process_snapshot_for_issuer_rejects_roa_when_crldp_crl_missing() {
let manifest_bytes = fixture_bytes(
"tests/fixtures/repository/rpki.cernet.net/repo/cernet/0/05FC9C5B88506F7C0D3F862C8895BED67E9F8EBA.mft",
);
@@ -428,7 +429,7 @@ fn process_pack_for_issuer_rejects_roa_when_crldp_crl_missing() {
fixture_bytes("tests/fixtures/repository/rpki.cernet.net/repo/cernet/0/AS4538.roa");
// Pack has a CRL, but its URI does not match the embedded EE certificate CRLDP.
let pack = dummy_pack(
let pack = dummy_snapshot(
manifest_bytes,
vec![
PackFile::from_bytes_compute_sha256(
@@ -444,7 +445,7 @@ fn process_pack_for_issuer_rejects_roa_when_crldp_crl_missing() {
..Policy::default()
};
let out = process_fetch_cache_pp_pack_for_issuer(
let out = process_publication_point_snapshot_for_issuer(
&pack,
&policy,
&fixture_bytes("tests/fixtures/ta/apnic-ta.cer"),
@@ -469,7 +470,7 @@ fn process_pack_for_issuer_rejects_roa_when_crldp_crl_missing() {
}
#[test]
fn process_pack_for_issuer_selects_crl_by_ee_crldp_uri_aspa() {
fn process_snapshot_for_issuer_selects_crl_by_ee_crldp_uri_aspa() {
let manifest_bytes = fixture_bytes(
"tests/fixtures/repository/rpki.cernet.net/repo/cernet/0/05FC9C5B88506F7C0D3F862C8895BED67E9F8EBA.mft",
);
@@ -487,7 +488,7 @@ fn process_pack_for_issuer_selects_crl_by_ee_crldp_uri_aspa() {
.as_str()
.to_string();
let pack = dummy_pack(
let pack = dummy_snapshot(
manifest_bytes,
vec![
PackFile::from_bytes_compute_sha256(ee_crldp, vec![0x01]),
@@ -500,7 +501,7 @@ fn process_pack_for_issuer_selects_crl_by_ee_crldp_uri_aspa() {
..Policy::default()
};
let out = process_fetch_cache_pp_pack_for_issuer(
let out = process_publication_point_snapshot_for_issuer(
&pack,
&policy,
&fixture_bytes("tests/fixtures/ta/apnic-ta.cer"),

View File

@@ -6,7 +6,7 @@ fn policy_defaults_are_correct() {
assert_eq!(p.sync_preference, SyncPreference::RrdpThenRsync);
assert_eq!(
p.ca_failed_fetch_policy,
CaFailedFetchPolicy::UseFetchCachePp
CaFailedFetchPolicy::ReuseCurrentInstanceVcir
);
assert_eq!(
p.signed_object_failure_policy,
@@ -30,6 +30,17 @@ signed_object_failure_policy = "drop_publication_point"
);
}
#[test]
fn policy_toml_parsing_accepts_reuse_current_instance_vcir() {
let toml = r#"ca_failed_fetch_policy = "reuse_current_instance_vcir"
"#;
let p = Policy::from_toml_str(toml).expect("parse TOML policy");
assert_eq!(
p.ca_failed_fetch_policy,
CaFailedFetchPolicy::ReuseCurrentInstanceVcir
);
}
#[test]
fn policy_toml_parsing_uses_defaults_when_missing() {
let p = Policy::from_toml_str("").expect("parse empty TOML policy");

View File

@@ -7,7 +7,8 @@ use rpki::fetch::rsync::LocalDirRsyncFetcher;
use rpki::policy::{Policy, SyncPreference};
use rpki::storage::RocksStore;
use rpki::sync::rrdp::Fetcher;
use rpki::validation::run::{fetch_cache_pp_exists, run_publication_point_once};
use rpki::validation::manifest::PublicationPointSource;
use rpki::validation::run::run_publication_point_once;
fn fixture_to_rsync_uri(path: &Path) -> String {
let rel = path
@@ -40,7 +41,7 @@ impl Fetcher for NeverHttpFetcher {
}
#[test]
fn e2e_offline_uses_rsync_then_writes_fetch_cache_pp_then_outputs_vrps() {
fn e2e_offline_uses_rsync_then_writes_latest_vcir_then_outputs_vrps() {
let fixture_dir = Path::new("tests/fixtures/repository/rpki.cernet.net/repo/cernet/0");
let rsync_base_uri = "rsync://rpki.cernet.net/repo/cernet/0/";
let manifest_path = fixture_dir.join("05FC9C5B88506F7C0D3F862C8895BED67E9F8EBA.mft");
@@ -80,14 +81,6 @@ fn e2e_offline_uses_rsync_then_writes_fetch_cache_pp_then_outputs_vrps() {
let temp = tempfile::tempdir().expect("tempdir");
let store = RocksStore::open(temp.path()).expect("open rocksdb");
let expected_files = std::fs::read_dir(fixture_dir)
.expect("read fixture dir")
.filter_map(|e| e.ok())
.filter_map(|e| e.metadata().ok().map(|m| (e, m)))
.filter(|(_e, m)| m.is_file())
.count();
assert!(expected_files >= 3, "fixture dir seems incomplete");
let out = run_publication_point_once(
&store,
&policy,
@@ -105,9 +98,14 @@ fn e2e_offline_uses_rsync_then_writes_fetch_cache_pp_then_outputs_vrps() {
)
.expect("run publication point once");
assert!(fetch_cache_pp_exists(&store, &manifest_rsync_uri).expect("exists check"));
assert_eq!(out.repo_sync.objects_written, expected_files);
assert!(
store
.get_vcir(&manifest_rsync_uri)
.expect("get vcir")
.is_some()
);
assert_eq!(out.publication_point_source, PublicationPointSource::Fresh);
assert!(out.publication_point_warnings.is_empty());
assert!(
out.objects.vrps.iter().any(|v| v.asn == 4538),
"expected VRPs for AS4538"

View File

@@ -1,8 +1,10 @@
use rpki::analysis::timing::{TimingHandle, TimingMeta};
use rpki::validation::from_tal::discover_root_ca_instance_from_tal_and_ta_der;
use rpki::validation::run_tree_from_tal::root_handle_from_trust_anchor;
use rpki::validation::run_tree_from_tal::{
run_tree_from_tal_and_ta_der_serial, run_tree_from_tal_and_ta_der_serial_audit,
run_tree_from_tal_url_serial, run_tree_from_tal_url_serial_audit,
run_tree_from_tal_and_ta_der_serial_audit_with_timing, run_tree_from_tal_url_serial,
run_tree_from_tal_url_serial_audit, run_tree_from_tal_url_serial_audit_with_timing,
};
use rpki::validation::tree::TreeRunConfig;
@@ -31,6 +33,22 @@ impl rpki::fetch::rsync::RsyncFetcher for EmptyRsyncFetcher {
}
}
fn test_timing_handle(temp: &tempfile::TempDir) -> TimingHandle {
TimingHandle::new(TimingMeta {
recorded_at_utc_rfc3339: "2026-03-11T00:00:00Z".to_string(),
validation_time_utc_rfc3339: "2026-03-11T00:00:00Z".to_string(),
tal_url: Some("mock:apnic.tal".to_string()),
db_path: Some(temp.path().display().to_string()),
})
}
fn read_timing_json(temp: &tempfile::TempDir, timing: &TimingHandle) -> serde_json::Value {
let path = temp.path().join("timing.json");
timing.write_json(&path, 20).expect("write timing json");
serde_json::from_slice(&std::fs::read(&path).expect("read timing json"))
.expect("parse timing json")
}
#[test]
fn root_handle_is_constructible_from_fixture_tal_and_ta() {
let tal_bytes = std::fs::read("tests/fixtures/tal/apnic-rfc7730-https.tal")
@@ -40,7 +58,12 @@ fn root_handle_is_constructible_from_fixture_tal_and_ta() {
let discovery =
discover_root_ca_instance_from_tal_and_ta_der(&tal_bytes, &ta_der, None).expect("discover");
let root = root_handle_from_trust_anchor(&discovery.trust_anchor, None, &discovery.ca_instance);
let root = root_handle_from_trust_anchor(
&discovery.trust_anchor,
"test-tal".to_string(),
None,
&discovery.ca_instance,
);
assert_eq!(root.depth, 0);
assert_eq!(
@@ -91,18 +114,18 @@ fn run_tree_from_tal_url_entry_executes_and_records_failure_when_repo_empty() {
&TreeRunConfig {
max_depth: Some(0),
max_instances: Some(1),
revalidate_only: false,
},
)
.expect("run tree");
assert_eq!(out.tree.instances_processed, 0);
assert_eq!(out.tree.instances_failed, 1);
assert_eq!(out.tree.instances_processed, 1);
assert_eq!(out.tree.instances_failed, 0);
assert!(
out.tree
.warnings
.iter()
.any(|w| w.message.contains("publication point failed")),
"expected failure warning"
.any(|w| w.message.contains("manifest failed fetch")
|| w.message.contains("no latest validated result")),
"expected failed-fetch warning"
);
}
@@ -135,18 +158,18 @@ fn run_tree_from_tal_and_ta_der_entry_executes_and_records_failure_when_repo_emp
&TreeRunConfig {
max_depth: Some(0),
max_instances: Some(1),
revalidate_only: false,
},
)
.expect("run tree");
assert_eq!(out.tree.instances_processed, 0);
assert_eq!(out.tree.instances_failed, 1);
assert_eq!(out.tree.instances_processed, 1);
assert_eq!(out.tree.instances_failed, 0);
assert!(
out.tree
.warnings
.iter()
.any(|w| w.message.contains("publication point failed")),
"expected failure warning"
.any(|w| w.message.contains("manifest failed fetch")
|| w.message.contains("no latest validated result")),
"expected failed-fetch warning"
);
}
@@ -187,14 +210,14 @@ fn run_tree_from_tal_url_audit_entry_collects_no_publication_points_when_repo_em
&TreeRunConfig {
max_depth: Some(0),
max_instances: Some(1),
revalidate_only: false,
},
)
.expect("run tree audit");
assert_eq!(out.tree.instances_processed, 0);
assert_eq!(out.tree.instances_failed, 1);
assert!(out.publication_points.is_empty());
assert_eq!(out.tree.instances_processed, 1);
assert_eq!(out.tree.instances_failed, 0);
assert_eq!(out.publication_points.len(), 1);
assert_eq!(out.publication_points[0].source, "failed_fetch_no_cache");
}
#[test]
@@ -227,12 +250,113 @@ fn run_tree_from_tal_and_ta_der_audit_entry_collects_no_publication_points_when_
&TreeRunConfig {
max_depth: Some(0),
max_instances: Some(1),
revalidate_only: false,
},
)
.expect("run tree audit");
assert_eq!(out.tree.instances_processed, 0);
assert_eq!(out.tree.instances_failed, 1);
assert!(out.publication_points.is_empty());
assert_eq!(out.tree.instances_processed, 1);
assert_eq!(out.tree.instances_failed, 0);
assert_eq!(out.publication_points.len(), 1);
assert_eq!(out.publication_points[0].source, "failed_fetch_no_cache");
}
#[test]
fn run_tree_from_tal_url_audit_with_timing_records_phases_when_repo_empty() {
let tal_url = "mock:apnic.tal";
let tal_bytes = std::fs::read("tests/fixtures/tal/apnic-rfc7730-https.tal")
.expect("read apnic tal fixture");
let ta_der = std::fs::read("tests/fixtures/ta/apnic-ta.cer").expect("read apnic ta fixture");
let mut map = HashMap::new();
map.insert(tal_url.to_string(), tal_bytes);
map.insert(
"https://rpki.apnic.net/repository/apnic-rpki-root-iana-origin.cer".to_string(),
ta_der.clone(),
);
map.insert(
"rsync://rpki.apnic.net/repository/apnic-rpki-root-iana-origin.cer".to_string(),
ta_der.clone(),
);
let http = MapHttpFetcher { map };
let rsync = EmptyRsyncFetcher;
let temp = tempfile::tempdir().expect("tempdir");
let store = rpki::storage::RocksStore::open(temp.path()).expect("open rocksdb");
let policy = rpki::policy::Policy {
sync_preference: rpki::policy::SyncPreference::RsyncOnly,
..rpki::policy::Policy::default()
};
let timing = test_timing_handle(&temp);
let out = run_tree_from_tal_url_serial_audit_with_timing(
&store,
&policy,
tal_url,
&http,
&rsync,
time::OffsetDateTime::now_utc(),
&TreeRunConfig {
max_depth: Some(0),
max_instances: Some(1),
},
&timing,
)
.expect("run tree audit with timing");
assert_eq!(out.tree.instances_processed, 1);
assert_eq!(out.tree.instances_failed, 0);
let report = read_timing_json(&temp, &timing);
assert_eq!(report["phases"]["tal_bootstrap"]["count"].as_u64(), Some(1));
assert_eq!(
report["phases"]["tree_run_total"]["count"].as_u64(),
Some(1)
);
}
#[test]
fn run_tree_from_tal_and_ta_der_audit_with_timing_records_phases_when_repo_empty() {
let tal_bytes = std::fs::read("tests/fixtures/tal/apnic-rfc7730-https.tal")
.expect("read apnic tal fixture");
let ta_der = std::fs::read("tests/fixtures/ta/apnic-ta.cer").expect("read apnic ta fixture");
let http = MapHttpFetcher {
map: HashMap::new(),
};
let rsync = EmptyRsyncFetcher;
let temp = tempfile::tempdir().expect("tempdir");
let store = rpki::storage::RocksStore::open(temp.path()).expect("open rocksdb");
let policy = rpki::policy::Policy {
sync_preference: rpki::policy::SyncPreference::RsyncOnly,
..rpki::policy::Policy::default()
};
let timing = test_timing_handle(&temp);
let out = run_tree_from_tal_and_ta_der_serial_audit_with_timing(
&store,
&policy,
&tal_bytes,
&ta_der,
None,
&http,
&rsync,
time::OffsetDateTime::now_utc(),
&TreeRunConfig {
max_depth: Some(0),
max_instances: Some(1),
},
&timing,
)
.expect("run tree audit with timing");
assert_eq!(out.tree.instances_processed, 1);
assert_eq!(out.tree.instances_failed, 0);
let report = read_timing_json(&temp, &timing);
assert_eq!(report["phases"]["tal_bootstrap"]["count"].as_u64(), Some(1));
assert_eq!(
report["phases"]["tree_run_total"]["count"].as_u64(),
Some(1)
);
}

View File

@@ -1,7 +1,7 @@
use rpki::storage::{FetchCachePpKey, RocksStore};
use rpki::storage::RocksStore;
#[test]
fn storage_iter_all_lists_raw_and_fetch_cache_pp_entries() {
fn storage_iter_all_lists_raw_entries() {
let temp = tempfile::tempdir().expect("tempdir");
let store = RocksStore::open(temp.path()).expect("open rocksdb");
@@ -12,11 +12,6 @@ fn storage_iter_all_lists_raw_and_fetch_cache_pp_entries() {
.put_raw("rsync://example.test/repo/b.roa", b"b")
.expect("put_raw b");
let key = FetchCachePpKey::from_manifest_rsync_uri("rsync://example.test/repo/m.mft");
store
.put_fetch_cache_pp(&key, b"x")
.expect("put_fetch_cache_pp");
let raw_keys = store
.raw_iter_all()
.expect("raw_iter_all")
@@ -25,11 +20,4 @@ fn storage_iter_all_lists_raw_and_fetch_cache_pp_entries() {
assert_eq!(raw_keys.len(), 2);
assert!(raw_keys.contains(&"rsync://example.test/repo/a.cer".to_string()));
assert!(raw_keys.contains(&"rsync://example.test/repo/b.roa".to_string()));
let keys = store
.fetch_cache_pp_iter_all()
.expect("fetch_cache_pp_iter_all")
.map(|(k, _v)| String::from_utf8(k.to_vec()).expect("utf8 key"))
.collect::<Vec<_>>();
assert_eq!(keys, vec![key.as_str().to_string()]);
}

View File

@@ -1,6 +1,6 @@
use rocksdb::WriteBatch;
use rpki::storage::{FetchCachePpKey, RocksStore};
use rpki::storage::RocksStore;
#[test]
fn storage_delete_rrdp_state_works() {
@@ -57,15 +57,6 @@ fn storage_raw_iter_prefix_filters_by_prefix() {
}
}
#[test]
fn storage_fetch_cache_pp_key_format_is_stable() {
let k = FetchCachePpKey::from_manifest_rsync_uri("rsync://example.net/repo/manifest.mft");
assert_eq!(
k.as_str(),
"fetch_cache_pp:rsync://example.net/repo/manifest.mft"
);
}
#[test]
fn storage_write_batch_accepts_empty_batch() {
let temp = tempfile::tempdir().expect("tempdir");

View File

@@ -1,6 +1,6 @@
use std::path::Path;
use rpki::storage::{FetchCachePpKey, RocksStore};
use rpki::storage::RocksStore;
#[test]
fn storage_opens_and_creates_column_families() {
@@ -24,26 +24,6 @@ fn raw_objects_roundtrip_by_rsync_uri() {
assert!(got.is_none());
}
#[test]
fn fetch_cache_pp_roundtrip_by_manifest_uri() {
let dir = tempfile::tempdir().expect("tempdir");
let store = RocksStore::open(dir.path()).expect("open rocksdb");
let manifest_uri = "rsync://example.invalid/repo/manifest.mft";
let key = FetchCachePpKey::from_manifest_rsync_uri(manifest_uri);
assert_eq!(
key.as_str(),
"fetch_cache_pp:rsync://example.invalid/repo/manifest.mft"
);
let bytes = b"pack";
store
.put_fetch_cache_pp(&key, bytes)
.expect("put fetch_cache_pp");
let got = store.get_fetch_cache_pp(&key).expect("get fetch_cache_pp");
assert_eq!(got.as_deref(), Some(bytes.as_slice()));
}
#[test]
fn rrdp_state_roundtrip_by_notification_uri() {
let dir = tempfile::tempdir().expect("tempdir");

View File

@@ -2,17 +2,18 @@ use std::collections::HashMap;
use rpki::audit::{DiscoveredFrom, PublicationPointAudit};
use rpki::report::Warning;
use rpki::storage::{FetchCachePpPack, PackTime};
use rpki::storage::PackTime;
use rpki::validation::manifest::PublicationPointSource;
use rpki::validation::objects::{ObjectsOutput, ObjectsStats};
use rpki::validation::publication_point::PublicationPointSnapshot;
use rpki::validation::tree::{
CaInstanceHandle, DiscoveredChildCaInstance, PublicationPointRunResult, PublicationPointRunner,
TreeRunConfig, run_tree_serial,
};
fn empty_pack(manifest_uri: &str, pp_uri: &str) -> FetchCachePpPack {
FetchCachePpPack {
format_version: FetchCachePpPack::FORMAT_VERSION_V1,
fn empty_snapshot(manifest_uri: &str, pp_uri: &str) -> PublicationPointSnapshot {
PublicationPointSnapshot {
format_version: PublicationPointSnapshot::FORMAT_VERSION_V1,
publication_point_rsync_uri: pp_uri.to_string(),
manifest_rsync_uri: manifest_uri.to_string(),
manifest_number_be: vec![1],
@@ -33,6 +34,8 @@ fn empty_pack(manifest_uri: &str, pp_uri: &str) -> FetchCachePpPack {
fn ca_handle(manifest_uri: &str) -> CaInstanceHandle {
CaInstanceHandle {
depth: 0,
tal_id: "test-tal".to_string(),
parent_manifest_rsync_uri: None,
ca_certificate_der: Vec::new(),
ca_certificate_rsync_uri: None,
effective_ip_resources: None,
@@ -104,7 +107,7 @@ fn tree_continues_when_a_publication_point_fails() {
root_manifest,
PublicationPointRunResult {
source: PublicationPointSource::Fresh,
pack: empty_pack(root_manifest, "rsync://example.test/repo/"),
snapshot: Some(empty_snapshot(root_manifest, "rsync://example.test/repo/")),
warnings: Vec::new(),
objects: ObjectsOutput {
vrps: Vec::new(),
@@ -125,7 +128,10 @@ fn tree_continues_when_a_publication_point_fails() {
ok_child_manifest,
PublicationPointRunResult {
source: PublicationPointSource::Fresh,
pack: empty_pack(ok_child_manifest, "rsync://example.test/repo/ok-child/"),
snapshot: Some(empty_snapshot(
ok_child_manifest,
"rsync://example.test/repo/ok-child/",
)),
warnings: vec![Warning::new("ok child warning")],
objects: ObjectsOutput {
vrps: Vec::new(),

View File

@@ -2,9 +2,10 @@ use std::collections::HashMap;
use rpki::audit::{DiscoveredFrom, PublicationPointAudit};
use rpki::report::Warning;
use rpki::storage::{FetchCachePpPack, PackFile, PackTime};
use rpki::storage::{PackFile, PackTime};
use rpki::validation::manifest::PublicationPointSource;
use rpki::validation::objects::{ObjectsOutput, ObjectsStats};
use rpki::validation::publication_point::PublicationPointSnapshot;
use rpki::validation::tree::{
CaInstanceHandle, DiscoveredChildCaInstance, PublicationPointRunResult, PublicationPointRunner,
TreeRunConfig, run_tree_serial, run_tree_serial_audit,
@@ -43,8 +44,8 @@ impl PublicationPointRunner for MockRunner {
}
}
fn empty_pack(manifest_uri: &str, pp_uri: &str) -> FetchCachePpPack {
FetchCachePpPack {
fn empty_snapshot(manifest_uri: &str, pp_uri: &str) -> PublicationPointSnapshot {
PublicationPointSnapshot {
format_version: 1,
publication_point_rsync_uri: pp_uri.to_string(),
manifest_rsync_uri: manifest_uri.to_string(),
@@ -66,6 +67,8 @@ fn empty_pack(manifest_uri: &str, pp_uri: &str) -> FetchCachePpPack {
fn ca_handle(manifest_uri: &str) -> CaInstanceHandle {
CaInstanceHandle {
depth: 0,
tal_id: "test-tal".to_string(),
parent_manifest_rsync_uri: None,
ca_certificate_der: Vec::new(),
ca_certificate_rsync_uri: None,
effective_ip_resources: None,
@@ -97,7 +100,7 @@ fn discovered_child(
}
#[test]
fn tree_enqueues_children_only_for_fresh_publication_points() {
fn tree_enqueues_children_for_fresh_and_current_instance_vcir_results() {
let root_manifest = "rsync://example.test/repo/root.mft";
let child1_manifest = "rsync://example.test/repo/child1.mft";
let child2_manifest = "rsync://example.test/repo/child2.mft";
@@ -114,7 +117,7 @@ fn tree_enqueues_children_only_for_fresh_publication_points() {
root_manifest,
PublicationPointRunResult {
source: PublicationPointSource::Fresh,
pack: empty_pack(root_manifest, "rsync://example.test/repo/"),
snapshot: Some(empty_snapshot(root_manifest, "rsync://example.test/repo/")),
warnings: Vec::new(),
objects: ObjectsOutput {
vrps: Vec::new(),
@@ -130,8 +133,11 @@ fn tree_enqueues_children_only_for_fresh_publication_points() {
.with(
child1_manifest,
PublicationPointRunResult {
source: PublicationPointSource::FetchCachePp,
pack: empty_pack(child1_manifest, "rsync://example.test/repo/child1/"),
source: PublicationPointSource::VcirCurrentInstance,
snapshot: Some(empty_snapshot(
child1_manifest,
"rsync://example.test/repo/child1/",
)),
warnings: vec![Warning::new("child1 warning")],
objects: ObjectsOutput {
vrps: Vec::new(),
@@ -148,7 +154,30 @@ fn tree_enqueues_children_only_for_fresh_publication_points() {
child2_manifest,
PublicationPointRunResult {
source: PublicationPointSource::Fresh,
pack: empty_pack(child2_manifest, "rsync://example.test/repo/child2/"),
snapshot: Some(empty_snapshot(
child2_manifest,
"rsync://example.test/repo/child2/",
)),
warnings: Vec::new(),
objects: ObjectsOutput {
vrps: Vec::new(),
aspas: Vec::new(),
warnings: Vec::new(),
stats: ObjectsStats::default(),
audit: Vec::new(),
},
audit: PublicationPointAudit::default(),
discovered_children: Vec::new(),
},
)
.with(
grandchild_manifest,
PublicationPointRunResult {
source: PublicationPointSource::Fresh,
snapshot: Some(empty_snapshot(
grandchild_manifest,
"rsync://example.test/repo/grandchild/",
)),
warnings: Vec::new(),
objects: ObjectsOutput {
vrps: Vec::new(),
@@ -165,14 +194,20 @@ fn tree_enqueues_children_only_for_fresh_publication_points() {
let out = run_tree_serial(ca_handle(root_manifest), &runner, &TreeRunConfig::default())
.expect("run tree");
// root + child1 + child2. grandchild must NOT be processed because child1 used cache.
assert_eq!(out.instances_processed, 3);
// root + child1 + child2 + grandchild. child1 uses current-instance VCIR, but its child
// entry is still restored and processed fresh-first later in the traversal.
assert_eq!(out.instances_processed, 4);
assert_eq!(out.instances_failed, 0);
let called = runner.called();
assert_eq!(
called,
vec![root_manifest, child1_manifest, child2_manifest]
vec![
root_manifest,
child1_manifest,
child2_manifest,
grandchild_manifest
]
);
assert!(
@@ -184,8 +219,8 @@ fn tree_enqueues_children_only_for_fresh_publication_points() {
assert!(
out.warnings
.iter()
.any(|w| w.message.contains("skipping child CA discovery")),
"expected RFC 9286 §6.6 enforcement warning"
.all(|w| !w.message.contains("skipping child CA discovery")),
"did not expect old skip-child warning"
);
}
@@ -199,7 +234,7 @@ fn tree_respects_max_depth_and_max_instances() {
root_manifest,
PublicationPointRunResult {
source: PublicationPointSource::Fresh,
pack: empty_pack(root_manifest, "rsync://example.test/repo/"),
snapshot: Some(empty_snapshot(root_manifest, "rsync://example.test/repo/")),
warnings: Vec::new(),
objects: ObjectsOutput {
vrps: Vec::new(),
@@ -216,7 +251,10 @@ fn tree_respects_max_depth_and_max_instances() {
child_manifest,
PublicationPointRunResult {
source: PublicationPointSource::Fresh,
pack: empty_pack(child_manifest, "rsync://example.test/repo/child/"),
snapshot: Some(empty_snapshot(
child_manifest,
"rsync://example.test/repo/child/",
)),
warnings: Vec::new(),
objects: ObjectsOutput {
vrps: Vec::new(),
@@ -236,7 +274,6 @@ fn tree_respects_max_depth_and_max_instances() {
&TreeRunConfig {
max_depth: Some(0),
max_instances: None,
revalidate_only: false,
},
)
.expect("run tree depth-limited");
@@ -249,7 +286,6 @@ fn tree_respects_max_depth_and_max_instances() {
&TreeRunConfig {
max_depth: None,
max_instances: Some(1),
revalidate_only: false,
},
)
.expect("run tree instance-limited");
@@ -267,7 +303,7 @@ fn tree_audit_includes_parent_and_discovered_from_for_non_root_nodes() {
root_manifest,
PublicationPointRunResult {
source: PublicationPointSource::Fresh,
pack: empty_pack(root_manifest, "rsync://example.test/repo/"),
snapshot: Some(empty_snapshot(root_manifest, "rsync://example.test/repo/")),
warnings: Vec::new(),
objects: ObjectsOutput {
vrps: Vec::new(),
@@ -284,7 +320,10 @@ fn tree_audit_includes_parent_and_discovered_from_for_non_root_nodes() {
child_manifest,
PublicationPointRunResult {
source: PublicationPointSource::Fresh,
pack: empty_pack(child_manifest, "rsync://example.test/repo/child/"),
snapshot: Some(empty_snapshot(
child_manifest,
"rsync://example.test/repo/child/",
)),
warnings: Vec::new(),
objects: ObjectsOutput {
vrps: Vec::new(),
@@ -320,16 +359,23 @@ fn tree_audit_includes_parent_and_discovered_from_for_non_root_nodes() {
}
#[test]
fn tree_revalidate_only_enqueues_children_from_fetch_cache_pp() {
fn tree_prefers_lexicographically_first_discovery_when_duplicate_manifest_is_queued() {
let root_manifest = "rsync://example.test/repo/root.mft";
let child_manifest = "rsync://example.test/repo/child.mft";
let duplicate_manifest = "rsync://example.test/repo/child.mft";
let mut first = discovered_child(root_manifest, duplicate_manifest);
first.discovered_from.child_ca_certificate_rsync_uri =
"rsync://example.test/repo/z-child.cer".to_string();
let mut second = discovered_child(root_manifest, duplicate_manifest);
second.discovered_from.child_ca_certificate_rsync_uri =
"rsync://example.test/repo/a-child.cer".to_string();
let runner = MockRunner::default()
.with(
root_manifest,
PublicationPointRunResult {
source: PublicationPointSource::FetchCachePp,
pack: empty_pack(root_manifest, "rsync://example.test/repo/"),
source: PublicationPointSource::Fresh,
snapshot: Some(empty_snapshot(root_manifest, "rsync://example.test/repo/")),
warnings: Vec::new(),
objects: ObjectsOutput {
vrps: Vec::new(),
@@ -339,14 +385,17 @@ fn tree_revalidate_only_enqueues_children_from_fetch_cache_pp() {
audit: Vec::new(),
},
audit: PublicationPointAudit::default(),
discovered_children: vec![discovered_child(root_manifest, child_manifest)],
discovered_children: vec![first, second],
},
)
.with(
child_manifest,
duplicate_manifest,
PublicationPointRunResult {
source: PublicationPointSource::FetchCachePp,
pack: empty_pack(child_manifest, "rsync://example.test/repo/child/"),
source: PublicationPointSource::Fresh,
snapshot: Some(empty_snapshot(
duplicate_manifest,
"rsync://example.test/repo/child/",
)),
warnings: Vec::new(),
objects: ObjectsOutput {
vrps: Vec::new(),
@@ -360,27 +409,21 @@ fn tree_revalidate_only_enqueues_children_from_fetch_cache_pp() {
},
);
let out = run_tree_serial(
ca_handle(root_manifest),
&runner,
&TreeRunConfig {
max_depth: None,
max_instances: None,
revalidate_only: false,
},
)
.expect("run tree");
assert_eq!(out.instances_processed, 1);
let out = run_tree_serial_audit(ca_handle(root_manifest), &runner, &TreeRunConfig::default())
.expect("run tree audit");
let out = run_tree_serial(
ca_handle(root_manifest),
&runner,
&TreeRunConfig {
max_depth: None,
max_instances: None,
revalidate_only: true,
},
)
.expect("run tree");
assert_eq!(out.instances_processed, 2);
assert_eq!(out.tree.instances_processed, 2);
assert_eq!(
runner.called(),
vec![root_manifest.to_string(), duplicate_manifest.to_string()]
);
let child_audit = &out.publication_points[1];
assert_eq!(
child_audit
.discovered_from
.as_ref()
.expect("child discovered_from")
.child_ca_certificate_rsync_uri,
"rsync://example.test/repo/a-child.cer"
);
}

View File

@@ -1,69 +0,0 @@
use rpki::storage::{FetchCachePpPack, PackFile, PackTime};
fn sample_pack() -> FetchCachePpPack {
let this_update =
PackTime::from_utc_offset_datetime(time::OffsetDateTime::from_unix_timestamp(0).unwrap());
let next_update = PackTime::from_utc_offset_datetime(
time::OffsetDateTime::from_unix_timestamp(3600).unwrap(),
);
let verified_at =
PackTime::from_utc_offset_datetime(time::OffsetDateTime::from_unix_timestamp(10).unwrap());
let file1 = PackFile::from_bytes_compute_sha256(
"rsync://example.net/repo/CA/1.crl",
b"crl-bytes".to_vec(),
);
let file2 = PackFile::from_bytes_compute_sha256(
"rsync://example.net/repo/CA/2.cer",
b"cer-bytes".to_vec(),
);
FetchCachePpPack {
format_version: FetchCachePpPack::FORMAT_VERSION_V1,
manifest_rsync_uri: "rsync://example.net/repo/CA/manifest.mft".to_string(),
publication_point_rsync_uri: "rsync://example.net/repo/CA/".to_string(),
manifest_number_be: vec![1],
this_update,
next_update,
verified_at,
manifest_bytes: b"manifest-bytes".to_vec(),
files: vec![file1, file2],
}
}
#[test]
fn pack_encode_decode_roundtrip() {
let pack = sample_pack();
let bytes = pack.encode().expect("encode");
let decoded = FetchCachePpPack::decode(&bytes).expect("decode");
assert_eq!(decoded, pack);
}
#[test]
fn pack_rejects_missing_manifest() {
let mut pack = sample_pack();
pack.manifest_bytes.clear();
let bytes = pack.encode().expect("encode");
assert!(FetchCachePpPack::decode(&bytes).is_err());
}
#[test]
fn pack_rejects_duplicate_rsync_uri_entries() {
let mut pack = sample_pack();
let dup =
PackFile::from_bytes_compute_sha256("rsync://example.net/repo/CA/1.crl", b"other".to_vec());
pack.files.push(dup);
let bytes = pack.encode().expect("encode");
assert!(FetchCachePpPack::decode(&bytes).is_err());
}
#[test]
fn pack_includes_this_update_next_update() {
let pack = sample_pack();
let bytes = pack.encode().expect("encode");
let decoded = FetchCachePpPack::decode(&bytes).expect("decode");
let this_update = decoded.this_update.parse().expect("parse this_update");
let next_update = decoded.next_update.parse().expect("parse next_update");
assert!(next_update > this_update);
}

View File

@@ -1,129 +0,0 @@
use rpki::storage::{FetchCachePpPack, PackFile, PackTime};
fn base_pack() -> FetchCachePpPack {
let this_update =
PackTime::from_utc_offset_datetime(time::OffsetDateTime::from_unix_timestamp(0).unwrap());
let next_update = PackTime::from_utc_offset_datetime(
time::OffsetDateTime::from_unix_timestamp(3600).unwrap(),
);
let verified_at =
PackTime::from_utc_offset_datetime(time::OffsetDateTime::from_unix_timestamp(10).unwrap());
let file =
PackFile::from_bytes_compute_sha256("rsync://example.net/repo/obj.cer", b"x".to_vec());
FetchCachePpPack {
format_version: FetchCachePpPack::FORMAT_VERSION_V1,
manifest_rsync_uri: "rsync://example.net/repo/manifest.mft".to_string(),
publication_point_rsync_uri: "rsync://example.net/repo/".to_string(),
manifest_number_be: vec![1],
this_update,
next_update,
verified_at,
manifest_bytes: b"manifest".to_vec(),
files: vec![file],
}
}
#[test]
fn pack_rejects_unsupported_format_version() {
let mut pack = base_pack();
pack.format_version = 999;
let bytes = pack.encode().expect("encode");
assert!(
FetchCachePpPack::decode(&bytes)
.unwrap_err()
.to_string()
.contains("unsupported pack format_version")
);
}
#[test]
fn pack_rejects_missing_manifest_rsync_uri() {
let mut pack = base_pack();
pack.manifest_rsync_uri.clear();
let bytes = pack.encode().expect("encode");
assert!(FetchCachePpPack::decode(&bytes).is_err());
}
#[test]
fn pack_rejects_missing_publication_point_rsync_uri() {
let mut pack = base_pack();
pack.publication_point_rsync_uri.clear();
let bytes = pack.encode().expect("encode");
assert!(FetchCachePpPack::decode(&bytes).is_err());
}
#[test]
fn pack_rejects_missing_manifest_number() {
let mut pack = base_pack();
pack.manifest_number_be.clear();
let bytes = pack.encode().expect("encode");
let err = FetchCachePpPack::decode(&bytes).unwrap_err();
assert!(err.to_string().contains("missing required field"));
}
#[test]
fn pack_rejects_manifest_number_too_long() {
let mut pack = base_pack();
pack.manifest_number_be = vec![1u8; 21];
let bytes = pack.encode().expect("encode");
let err = FetchCachePpPack::decode(&bytes).unwrap_err();
assert!(err.to_string().contains("at most 20 octets"));
}
#[test]
fn pack_rejects_manifest_number_with_leading_zeros() {
let mut pack = base_pack();
pack.manifest_number_be = vec![0u8, 1u8];
let bytes = pack.encode().expect("encode");
let err = FetchCachePpPack::decode(&bytes).unwrap_err();
assert!(err.to_string().contains("leading zeros"));
}
#[test]
fn pack_rejects_invalid_time_fields() {
let mut pack = base_pack();
pack.this_update = PackTime {
rfc3339_utc: "not-a-time".to_string(),
};
let bytes = pack.encode().expect("encode");
assert!(FetchCachePpPack::decode(&bytes).is_err());
}
#[test]
fn pack_rejects_empty_file_bytes() {
let mut pack = base_pack();
let mut sha = [0u8; 32];
sha[0] = 1;
pack.files = vec![PackFile::new(
"rsync://example.net/repo/empty.cer",
Vec::new(),
sha,
)];
let bytes = pack.encode().expect("encode");
assert!(FetchCachePpPack::decode(&bytes).is_err());
}
#[test]
fn pack_rejects_file_hash_mismatch() {
let mut pack = base_pack();
pack.files = vec![PackFile::new(
"rsync://example.net/repo/bad.cer",
b"abc".to_vec(),
[0u8; 32],
)];
let bytes = pack.encode().expect("encode");
let err = FetchCachePpPack::decode(&bytes).unwrap_err();
assert!(err.to_string().contains("file hash mismatch"));
}
#[test]
fn pack_rejects_missing_file_rsync_uri() {
let mut pack = base_pack();
let file = PackFile::from_bytes_compute_sha256("", b"x".to_vec());
pack.files = vec![file];
let bytes = pack.encode().expect("encode");
let err = FetchCachePpPack::decode(&bytes).unwrap_err();
assert!(err.to_string().contains("missing required field"));
}