5909 lines
224 KiB
Rust

use super::*;
use crate::data_model::oid::OID_AD_SIGNED_OBJECT;
use crate::data_model::rc::{
AccessDescription, AsIdOrRange, AsIdentifierChoice, AsResourceSet, ResourceCertificate,
};
use crate::data_model::roa::RoaAfi;
use crate::fetch::rsync::LocalDirRsyncFetcher;
use crate::fetch::rsync::{RsyncFetchError, RsyncFetcher};
use crate::storage::{
PackFile, PackTime, PublicationPointCacheProjection,
PublicationPointCacheProjectionWriteAction, RawByHashEntry, RepositoryViewEntry,
RepositoryViewState, RocksStore, ValidatedCaInstanceResult, ValidatedManifestMeta,
VcirArtifactKind, VcirArtifactRole, VcirArtifactValidationStatus, VcirAuditSummary,
VcirChildEntry, VcirInstanceGate, VcirLocalOutput, VcirLocalOutputPayload, VcirOutputType,
VcirRelatedArtifact, VcirSourceObjectType, VcirSummary,
};
use crate::sync::rrdp::Fetcher;
use crate::validation::publication_point::PublicationPointSnapshot;
use crate::validation::tree::{DiscoveredChildEntryProjection, PublicationPointRunner};
use std::process::Command;
use std::sync::Arc;
use std::sync::atomic::{AtomicUsize, Ordering};
fn sha256_32(input: &[u8]) -> [u8; 32] {
sha256_hex_to_32(&sha256_hex(input))
}
fn ipv4_addr(octets: [u8; 4]) -> [u8; 16] {
let mut addr = [0u8; 16];
addr[..4].copy_from_slice(&octets);
addr
}
#[test]
fn publication_point_cache_policy_fingerprint_includes_resource_validation_mode() {
let mut strict_policy = Policy::default();
strict_policy.resource_validation_mode = ResourceValidationMode::Rfc6487;
let mut vrs_policy = Policy::default();
vrs_policy.resource_validation_mode = ResourceValidationMode::ValidationUpdate03;
assert_ne!(
publication_point_cache_policy_fingerprint(&strict_policy),
publication_point_cache_policy_fingerprint(&vrs_policy)
);
}
#[test]
fn router_asns_for_resource_mode_filters_vrs_and_rejects_strict_overclaim() {
let issuer_as = AsResourceSet {
asnum: Some(AsIdentifierChoice::AsIdsOrRanges(vec![
AsIdOrRange::Range {
min: 64500,
max: 64510,
},
])),
rdi: None,
};
let strict = router_asns_for_resource_mode(
&[64505, 64520],
Some(&issuer_as),
ResourceValidationMode::Rfc6487,
)
.unwrap_err();
assert!(strict.contains("not a subset"), "{strict}");
let vrs = router_asns_for_resource_mode(
&[64505, 64520],
Some(&issuer_as),
ResourceValidationMode::ValidationUpdate03,
)
.expect("vrs filters router asns");
assert_eq!(vrs, vec![64505]);
}
struct NeverHttpFetcher;
impl Fetcher for NeverHttpFetcher {
fn fetch(&self, _uri: &str) -> Result<Vec<u8>, String> {
Err("http fetch disabled in test".to_string())
}
}
struct FailingRsyncFetcher;
impl RsyncFetcher for FailingRsyncFetcher {
fn fetch_objects(
&self,
_rsync_base_uri: &str,
) -> Result<Vec<(String, Vec<u8>)>, RsyncFetchError> {
Err(RsyncFetchError::Fetch("rsync disabled in test".to_string()))
}
}
fn sample_runner_with_ccr_accumulator<'a>(
store: &'a RocksStore,
policy: &'a Policy,
) -> Rpkiv1PublicationPointRunner<'a> {
Rpkiv1PublicationPointRunner {
store,
policy,
http_fetcher: &NeverHttpFetcher,
rsync_fetcher: &FailingRsyncFetcher,
validation_time: time::OffsetDateTime::now_utc(),
timing: None,
download_log: None,
replay_archive_index: None,
replay_delta_index: None,
rrdp_dedup: false,
rrdp_repo_cache: Mutex::new(HashMap::new()),
rsync_dedup: false,
rsync_repo_cache: Mutex::new(HashMap::new()),
current_repo_index: None,
repo_sync_runtime: None,
parallel_phase2_config: None,
parallel_roa_worker_pool: None,
ccr_accumulator: Some(Mutex::new(CcrAccumulator::new(Vec::new()))),
persist_vcir: true,
enable_roa_validation_cache: false,
enable_child_certificate_validation_cache: false,
publication_point_cache_observe_only: false,
enable_publication_point_validation_cache: false,
}
}
fn openssl_available() -> bool {
Command::new("openssl")
.arg("version")
.output()
.map(|o| o.status.success())
.unwrap_or(false)
}
struct Generated {
issuer_ca_der: Vec<u8>,
child_ca_der: Vec<u8>,
issuer_crl_der: Vec<u8>,
issuer_crl_der_next: Vec<u8>,
}
fn run(cmd: &mut 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 generate_chain_and_crl() -> Generated {
assert!(openssl_available(), "openssl is required for this test");
let td = tempfile::tempdir().expect("tempdir");
let dir = td.path();
std::fs::create_dir_all(dir.join("newcerts")).expect("newcerts");
std::fs::write(dir.join("index.txt"), b"").expect("index");
std::fs::write(dir.join("serial"), b"1000\n").expect("serial");
std::fs::write(dir.join("crlnumber"), b"1000\n").expect("crlnumber");
let cnf = format!(
r#"
[ ca ]
default_ca = CA_default
[ CA_default ]
dir = {dir}
database = $dir/index.txt
new_certs_dir = $dir/newcerts
certificate = $dir/issuer.pem
private_key = $dir/issuer.key
serial = $dir/serial
crlnumber = $dir/crlnumber
default_md = sha256
default_days = 365
default_crl_days = 1
policy = policy_any
x509_extensions = v3_issuer_ca
crl_extensions = crl_ext
unique_subject = no
copy_extensions = none
[ policy_any ]
commonName = supplied
[ req ]
prompt = no
distinguished_name = dn
[ dn ]
CN = Test Issuer CA
[ v3_issuer_ca ]
basicConstraints = critical,CA:true
keyUsage = critical, keyCertSign, cRLSign
subjectKeyIdentifier = hash
authorityKeyIdentifier = keyid:always
certificatePolicies = critical, 1.3.6.1.5.5.7.14.2
subjectInfoAccess = caRepository;URI:rsync://example.test/repo/issuer/, rpkiManifest;URI:rsync://example.test/repo/issuer/issuer.mft, rpkiNotify;URI:https://example.test/notification.xml
sbgp-ipAddrBlock = critical, IPv4:10.0.0.0/8
sbgp-autonomousSysNum = critical, AS:64496-64511
[ v3_child_ca ]
basicConstraints = critical,CA:true
keyUsage = critical, keyCertSign, cRLSign
subjectKeyIdentifier = hash
authorityKeyIdentifier = keyid:always
crlDistributionPoints = URI:rsync://example.test/repo/issuer/issuer.crl
authorityInfoAccess = caIssuers;URI:rsync://example.test/repo/issuer/issuer.cer
certificatePolicies = critical, 1.3.6.1.5.5.7.14.2
subjectInfoAccess = caRepository;URI:rsync://example.test/repo/child/, rpkiManifest;URI:rsync://example.test/repo/child/child.mft, rpkiNotify;URI:https://example.test/notification.xml
sbgp-ipAddrBlock = critical, IPv4:10.0.0.0/16
sbgp-autonomousSysNum = critical, AS:64496
[ crl_ext ]
authorityKeyIdentifier = keyid:always
"#,
dir = dir.display()
);
std::fs::write(dir.join("openssl.cnf"), cnf.as_bytes()).expect("write cnf");
run(Command::new("openssl")
.arg("genrsa")
.arg("-out")
.arg(dir.join("issuer.key"))
.arg("2048"));
run(Command::new("openssl")
.arg("req")
.arg("-new")
.arg("-x509")
.arg("-sha256")
.arg("-days")
.arg("365")
.arg("-key")
.arg(dir.join("issuer.key"))
.arg("-config")
.arg(dir.join("openssl.cnf"))
.arg("-extensions")
.arg("v3_issuer_ca")
.arg("-out")
.arg(dir.join("issuer.pem")));
run(Command::new("openssl")
.arg("genrsa")
.arg("-out")
.arg(dir.join("child.key"))
.arg("2048"));
run(Command::new("openssl")
.arg("req")
.arg("-new")
.arg("-key")
.arg(dir.join("child.key"))
.arg("-subj")
.arg("/CN=Test Child CA")
.arg("-out")
.arg(dir.join("child.csr")));
run(Command::new("openssl")
.arg("ca")
.arg("-batch")
.arg("-config")
.arg(dir.join("openssl.cnf"))
.arg("-in")
.arg(dir.join("child.csr"))
.arg("-extensions")
.arg("v3_child_ca")
.arg("-out")
.arg(dir.join("child.pem")));
run(Command::new("openssl")
.arg("x509")
.arg("-in")
.arg(dir.join("issuer.pem"))
.arg("-outform")
.arg("DER")
.arg("-out")
.arg(dir.join("issuer.cer")));
run(Command::new("openssl")
.arg("x509")
.arg("-in")
.arg(dir.join("child.pem"))
.arg("-outform")
.arg("DER")
.arg("-out")
.arg(dir.join("child.cer")));
run(Command::new("openssl")
.arg("ca")
.arg("-gencrl")
.arg("-config")
.arg(dir.join("openssl.cnf"))
.arg("-out")
.arg(dir.join("issuer.crl.pem")));
run(Command::new("openssl")
.arg("crl")
.arg("-in")
.arg(dir.join("issuer.crl.pem"))
.arg("-outform")
.arg("DER")
.arg("-out")
.arg(dir.join("issuer.crl")));
run(Command::new("openssl")
.arg("ca")
.arg("-gencrl")
.arg("-config")
.arg(dir.join("openssl.cnf"))
.arg("-out")
.arg(dir.join("issuer-next.crl.pem")));
run(Command::new("openssl")
.arg("crl")
.arg("-in")
.arg(dir.join("issuer-next.crl.pem"))
.arg("-outform")
.arg("DER")
.arg("-out")
.arg(dir.join("issuer-next.crl")));
Generated {
issuer_ca_der: std::fs::read(dir.join("issuer.cer")).expect("read issuer der"),
child_ca_der: std::fs::read(dir.join("child.cer")).expect("read child der"),
issuer_crl_der: std::fs::read(dir.join("issuer.crl")).expect("read crl der"),
issuer_crl_der_next: std::fs::read(dir.join("issuer-next.crl")).expect("read next crl der"),
}
}
struct GeneratedRouter {
issuer_ca_der: Vec<u8>,
router_der: Vec<u8>,
issuer_crl_der: Vec<u8>,
}
fn generate_router_cert_with_variant(key_spec: &str, include_eku: bool) -> GeneratedRouter {
assert!(openssl_available(), "openssl is required for this test");
let td = tempfile::tempdir().expect("tempdir");
let dir = td.path();
std::fs::create_dir_all(dir.join("newcerts")).expect("newcerts");
std::fs::write(dir.join("index.txt"), b"").expect("index");
std::fs::write(dir.join("serial"), b"1000\n").expect("serial");
std::fs::write(dir.join("crlnumber"), b"1000\n").expect("crlnumber");
let eku_line = if include_eku {
"extendedKeyUsage = 1.3.6.1.5.5.7.3.30"
} else {
""
};
let cnf = format!(
r#"
[ ca ]
default_ca = CA_default
[ CA_default ]
dir = {dir}
database = $dir/index.txt
new_certs_dir = $dir/newcerts
certificate = $dir/issuer.pem
private_key = $dir/issuer.key
serial = $dir/serial
crlnumber = $dir/crlnumber
default_md = sha256
default_days = 365
default_crl_days = 1
policy = policy_any
x509_extensions = v3_issuer_ca
crl_extensions = crl_ext
unique_subject = no
copy_extensions = none
[ policy_any ]
commonName = supplied
[ req ]
prompt = no
distinguished_name = dn
[ dn ]
CN = Test Issuer CA
[ v3_issuer_ca ]
basicConstraints = critical,CA:true
keyUsage = critical, keyCertSign, cRLSign
subjectKeyIdentifier = hash
authorityKeyIdentifier = keyid:always
certificatePolicies = critical, 1.3.6.1.5.5.7.14.2
subjectInfoAccess = caRepository;URI:rsync://example.test/repo/issuer/, rpkiManifest;URI:rsync://example.test/repo/issuer/issuer.mft, rpkiNotify;URI:https://example.test/notification.xml
sbgp-ipAddrBlock = critical, IPv4:10.0.0.0/8
sbgp-autonomousSysNum = critical, AS:64496-64511
[ v3_router ]
keyUsage = critical, digitalSignature
{eku_line}
authorityKeyIdentifier = keyid:always
crlDistributionPoints = URI:rsync://example.test/repo/issuer/issuer.crl
authorityInfoAccess = caIssuers;URI:rsync://example.test/repo/issuer/issuer.cer
certificatePolicies = critical, 1.3.6.1.5.5.7.14.2
sbgp-autonomousSysNum = critical, AS:64496
[ crl_ext ]
authorityKeyIdentifier = keyid:always
"#,
dir = dir.display(),
eku_line = eku_line,
);
std::fs::write(dir.join("openssl.cnf"), cnf.as_bytes()).expect("write cnf");
run(Command::new("openssl")
.arg("genrsa")
.arg("-out")
.arg(dir.join("issuer.key"))
.arg("2048"));
run(Command::new("openssl")
.arg("req")
.arg("-new")
.arg("-x509")
.arg("-sha256")
.arg("-days")
.arg("365")
.arg("-key")
.arg(dir.join("issuer.key"))
.arg("-config")
.arg(dir.join("openssl.cnf"))
.arg("-extensions")
.arg("v3_issuer_ca")
.arg("-out")
.arg(dir.join("issuer.pem")));
match key_spec {
"ec-p256" => run(Command::new("openssl")
.arg("ecparam")
.arg("-name")
.arg("prime256v1")
.arg("-genkey")
.arg("-noout")
.arg("-out")
.arg(dir.join("router.key"))),
"ec-p384" => run(Command::new("openssl")
.arg("ecparam")
.arg("-name")
.arg("secp384r1")
.arg("-genkey")
.arg("-noout")
.arg("-out")
.arg(dir.join("router.key"))),
other => panic!("unsupported key_spec {other}"),
}
run(Command::new("openssl")
.arg("req")
.arg("-new")
.arg("-key")
.arg(dir.join("router.key"))
.arg("-subj")
.arg("/CN=ROUTER-0000FC10/serialNumber=01020304")
.arg("-out")
.arg(dir.join("router.csr")));
run(Command::new("openssl")
.arg("ca")
.arg("-batch")
.arg("-config")
.arg(dir.join("openssl.cnf"))
.arg("-in")
.arg(dir.join("router.csr"))
.arg("-extensions")
.arg("v3_router")
.arg("-out")
.arg(dir.join("router.pem")));
run(Command::new("openssl")
.arg("x509")
.arg("-in")
.arg(dir.join("issuer.pem"))
.arg("-outform")
.arg("DER")
.arg("-out")
.arg(dir.join("issuer.cer")));
run(Command::new("openssl")
.arg("x509")
.arg("-in")
.arg(dir.join("router.pem"))
.arg("-outform")
.arg("DER")
.arg("-out")
.arg(dir.join("router.cer")));
run(Command::new("openssl")
.arg("ca")
.arg("-gencrl")
.arg("-config")
.arg(dir.join("openssl.cnf"))
.arg("-out")
.arg(dir.join("issuer.crl.pem")));
run(Command::new("openssl")
.arg("crl")
.arg("-in")
.arg(dir.join("issuer.crl.pem"))
.arg("-outform")
.arg("DER")
.arg("-out")
.arg(dir.join("issuer.crl")));
GeneratedRouter {
issuer_ca_der: std::fs::read(dir.join("issuer.cer")).expect("read issuer der"),
router_der: std::fs::read(dir.join("router.cer")).expect("read router der"),
issuer_crl_der: std::fs::read(dir.join("issuer.crl")).expect("read crl der"),
}
}
fn dummy_pack_with_files(files: Vec<PackFile>) -> PublicationPointSnapshot {
let now = time::OffsetDateTime::now_utc();
PublicationPointSnapshot {
format_version: PublicationPointSnapshot::FORMAT_VERSION_V1,
manifest_rsync_uri: "rsync://example.test/repo/issuer/issuer.mft".to_string(),
publication_point_rsync_uri: "rsync://example.test/repo/issuer/".to_string(),
manifest_number_be: vec![1],
this_update: PackTime::from_utc_offset_datetime(now),
next_update: PackTime::from_utc_offset_datetime(now + time::Duration::hours(1)),
verified_at: PackTime::from_utc_offset_datetime(now),
manifest_bytes: vec![0x01],
files,
}
}
fn cernet_publication_point_snapshot_for_vcir_tests()
-> (PublicationPointSnapshot, Vec<u8>, time::OffsetDateTime) {
let dir = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.join("tests/fixtures/repository/rpki.cernet.net/repo/cernet/0");
let rsync_base_uri = "rsync://rpki.cernet.net/repo/cernet/0/";
let manifest_file = "05FC9C5B88506F7C0D3F862C8895BED67E9F8EBA.mft";
let manifest_rsync_uri = format!("{rsync_base_uri}{manifest_file}");
let manifest_bytes = std::fs::read(dir.join(manifest_file)).expect("read manifest fixture");
let manifest = ManifestObject::decode_der(&manifest_bytes).expect("decode manifest fixture");
let candidate = manifest.manifest.this_update + time::Duration::seconds(60);
let validation_time = if candidate < manifest.manifest.next_update {
candidate
} else {
manifest.manifest.this_update
};
let issuer_ca_der = std::fs::read(
std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR")).join(
"tests/fixtures/repository/rpki.apnic.net/repository/B527EF581D6611E2BB468F7C72FD1FF2/BfycW4hQb3wNP4YsiJW-1n6fjro.cer",
),
)
.expect("read issuer ca fixture");
let store_dir = tempfile::tempdir().expect("store dir");
let store = RocksStore::open(store_dir.path()).expect("open rocksdb");
let policy = Policy {
sync_preference: crate::policy::SyncPreference::RsyncOnly,
..Policy::default()
};
sync_publication_point(
&store,
&policy,
None,
rsync_base_uri,
&NeverHttpFetcher,
&LocalDirRsyncFetcher::new(&dir),
None,
None,
)
.expect("sync cernet fixture");
let pp = crate::validation::manifest::process_manifest_publication_point(
&store,
&policy,
&manifest_rsync_uri,
rsync_base_uri,
issuer_ca_der.as_slice(),
Some(
"rsync://rpki.apnic.net/repository/B527EF581D6611E2BB468F7C72FD1FF2/BfycW4hQb3wNP4YsiJW-1n6fjro.cer",
),
validation_time,
)
.expect("process manifest publication point");
(pp.snapshot, issuer_ca_der, validation_time)
}
fn sample_vcir_for_projection(
now: time::OffsetDateTime,
child_cert_hash: &str,
) -> ValidatedCaInstanceResult {
let manifest_uri = "rsync://example.test/repo/issuer/issuer.mft".to_string();
let current_crl_uri = "rsync://example.test/repo/issuer/issuer.crl".to_string();
let child_cert_uri = "rsync://example.test/repo/issuer/child.cer".to_string();
let child_manifest_uri = "rsync://example.test/repo/child/child.mft".to_string();
let roa_uri = "rsync://example.test/repo/issuer/a.roa".to_string();
let aspa_uri = "rsync://example.test/repo/issuer/a.asa".to_string();
let router_uri = "rsync://example.test/repo/issuer/router.cer".to_string();
let manifest_hash = sha256_hex(b"manifest-bytes");
let current_crl_hash = sha256_hex(b"current-crl-bytes");
let roa_hash = sha256_hex(b"roa-bytes");
let aspa_hash = sha256_hex(b"aspa-bytes");
let router_hash = sha256_hex(b"router-bytes");
let ee_hash = sha256_hex(b"ee-cert-bytes");
let gate_until = PackTime::from_utc_offset_datetime(now + time::Duration::hours(1));
let ccr_manifest_projection = VcirCcrManifestProjection {
manifest_rsync_uri: manifest_uri.clone(),
manifest_sha256: hex::decode(&manifest_hash).expect("decode manifest hash"),
manifest_size: 2048,
manifest_ee_aki: vec![0x11; 20],
manifest_number_be: vec![1],
manifest_this_update: PackTime::from_utc_offset_datetime(now),
manifest_sia_locations_der: vec![
crate::ccr::manifest_location::encode_access_description_der(&AccessDescription {
access_method_oid: OID_AD_SIGNED_OBJECT.to_string(),
access_location: manifest_uri.clone(),
})
.expect("encode signedObject"),
],
subordinate_skis: vec![vec![0x33; 20]],
};
ValidatedCaInstanceResult {
manifest_rsync_uri: manifest_uri.clone(),
parent_manifest_rsync_uri: None,
tal_id: "test-tal".to_string(),
ca_subject_name: "CN=Issuer".to_string(),
ca_ski: "11".repeat(20),
issuer_ski: "22".repeat(20),
last_successful_validation_time: PackTime::from_utc_offset_datetime(now),
current_manifest_rsync_uri: manifest_uri.clone(),
current_crl_rsync_uri: current_crl_uri.clone(),
validated_manifest_meta: ValidatedManifestMeta {
validated_manifest_number: vec![1],
validated_manifest_this_update: PackTime::from_utc_offset_datetime(now),
validated_manifest_next_update: gate_until.clone(),
},
ccr_manifest_projection,
instance_gate: VcirInstanceGate {
manifest_next_update: gate_until.clone(),
current_crl_next_update: gate_until.clone(),
self_ca_not_after: PackTime::from_utc_offset_datetime(now + time::Duration::hours(2)),
instance_effective_until: gate_until.clone(),
},
child_entries: vec![VcirChildEntry {
child_manifest_rsync_uri: child_manifest_uri,
child_cert_rsync_uri: child_cert_uri.clone(),
child_cert_hash: child_cert_hash.to_string(),
child_ski: "33".repeat(20),
child_rsync_base_uri: "rsync://example.test/repo/child/".to_string(),
child_publication_point_rsync_uri: "rsync://example.test/repo/child/".to_string(),
child_rrdp_notification_uri: Some("https://example.test/child-notify.xml".to_string()),
child_effective_ip_resources: None,
child_effective_as_resources: None,
accepted_at_validation_time: PackTime::from_utc_offset_datetime(now),
}],
local_outputs: vec![
VcirLocalOutput {
output_type: VcirOutputType::Vrp,
item_effective_until: PackTime::from_utc_offset_datetime(
now + time::Duration::minutes(30),
),
source_object_uri: roa_uri.clone(),
source_object_type: VcirSourceObjectType::Roa,
source_object_hash: sha256_hex_to_32(&roa_hash),
source_ee_cert_hash: sha256_hex_to_32(&ee_hash),
payload: VcirLocalOutputPayload::Vrp {
asn: 64496,
afi: RoaAfi::Ipv4,
prefix_len: 24,
addr: ipv4_addr([203, 0, 113, 0]),
max_length: 24,
},
rule_hash: sha256_32(b"roa-rule"),
},
VcirLocalOutput {
output_type: VcirOutputType::Aspa,
item_effective_until: PackTime::from_utc_offset_datetime(
now + time::Duration::minutes(30),
),
source_object_uri: aspa_uri.clone(),
source_object_type: VcirSourceObjectType::Aspa,
source_object_hash: sha256_hex_to_32(&aspa_hash),
source_ee_cert_hash: sha256_hex_to_32(&ee_hash),
payload: VcirLocalOutputPayload::Aspa {
customer_as_id: 64496,
provider_as_ids: vec![64497, 64498],
},
rule_hash: sha256_32(b"aspa-rule"),
},
VcirLocalOutput {
output_type: VcirOutputType::RouterKey,
item_effective_until: PackTime::from_utc_offset_datetime(
now + time::Duration::minutes(30),
),
source_object_uri: router_uri.clone(),
source_object_type: VcirSourceObjectType::RouterKey,
source_object_hash: sha256_hex_to_32(&router_hash),
source_ee_cert_hash: sha256_hex_to_32(&router_hash),
payload: VcirLocalOutputPayload::RouterKey {
as_id: 64496,
ski: vec![0x11; 20],
spki_der: vec![0x30, 0x00],
},
rule_hash: sha256_32(b"router-key-rule"),
},
],
related_artifacts: vec![
VcirRelatedArtifact {
artifact_role: VcirArtifactRole::Manifest,
artifact_kind: VcirArtifactKind::Mft,
uri: Some(manifest_uri.clone()),
sha256: manifest_hash,
object_type: Some("mft".to_string()),
validation_status: VcirArtifactValidationStatus::Accepted,
reject_reason: None,
},
VcirRelatedArtifact {
artifact_role: VcirArtifactRole::CurrentCrl,
artifact_kind: VcirArtifactKind::Crl,
uri: Some(current_crl_uri),
sha256: current_crl_hash,
object_type: Some("crl".to_string()),
validation_status: VcirArtifactValidationStatus::Accepted,
reject_reason: None,
},
VcirRelatedArtifact {
artifact_role: VcirArtifactRole::ChildCaCert,
artifact_kind: VcirArtifactKind::Cer,
uri: Some(child_cert_uri),
sha256: child_cert_hash.to_string(),
object_type: Some("cer".to_string()),
validation_status: VcirArtifactValidationStatus::Accepted,
reject_reason: None,
},
VcirRelatedArtifact {
artifact_role: VcirArtifactRole::SignedObject,
artifact_kind: VcirArtifactKind::Roa,
uri: Some(roa_uri),
sha256: roa_hash,
object_type: Some("roa".to_string()),
validation_status: VcirArtifactValidationStatus::Accepted,
reject_reason: None,
},
VcirRelatedArtifact {
artifact_role: VcirArtifactRole::SignedObject,
artifact_kind: VcirArtifactKind::Aspa,
uri: Some(aspa_uri),
sha256: aspa_hash,
object_type: Some("aspa".to_string()),
validation_status: VcirArtifactValidationStatus::Accepted,
reject_reason: None,
},
],
summary: VcirSummary {
local_vrp_count: 1,
local_aspa_count: 1,
local_router_key_count: 1,
child_count: 1,
accepted_object_count: 4,
rejected_object_count: 0,
},
audit_summary: VcirAuditSummary {
failed_fetch_eligible: true,
last_failed_fetch_reason: None,
warning_count: 0,
audit_flags: Vec::new(),
},
}
}
fn put_vcir_for_failed_fetch_reuse(
store: &RocksStore,
ca: &CaInstanceHandle,
policy: &Policy,
vcir: &ValidatedCaInstanceResult,
) {
let validation_time = vcir
.last_successful_validation_time
.parse()
.expect("parse VCIR validation time");
let identity = failed_fetch_reuse_identity_for_fresh_result(
ca,
policy,
validation_time,
vcir.instance_gate.instance_effective_until.clone(),
)
.expect("build VCIR failed-fetch reuse identity");
store
.put_vcir_with_failed_fetch_reuse_identity(vcir, &identity)
.expect("put reusable VCIR");
}
fn sample_ca_for_failed_fetch_reuse(vcir: &ValidatedCaInstanceResult) -> CaInstanceHandle {
CaInstanceHandle {
depth: 0,
tal_id: "test-tal".to_string(),
parent_manifest_rsync_uri: None,
ca_certificate: CaCertificateRef::inline_der(Vec::new()),
ca_certificate_rsync_uri: None,
effective_ip_resources: None,
effective_as_resources: None,
rsync_base_uri: "rsync://example.test/repo/issuer/".to_string(),
manifest_rsync_uri: vcir.manifest_rsync_uri.clone(),
publication_point_rsync_uri: "rsync://example.test/repo/issuer/".to_string(),
rrdp_notification_uri: Some("https://example.test/notify.xml".to_string()),
}
}
#[test]
fn never_http_fetcher_returns_error() {
let f = NeverHttpFetcher;
let err = f.fetch("https://example.test/").unwrap_err();
assert!(err.contains("disabled"), "{err}");
}
#[test]
fn kind_from_rsync_uri_classifies_known_extensions() {
assert_eq!(
kind_from_rsync_uri("rsync://example.test/x.crl"),
AuditObjectKind::Crl
);
assert_eq!(
kind_from_rsync_uri("rsync://example.test/x.cer"),
AuditObjectKind::Certificate
);
assert_eq!(
kind_from_rsync_uri("rsync://example.test/x.roa"),
AuditObjectKind::Roa
);
assert_eq!(
kind_from_rsync_uri("rsync://example.test/x.asa"),
AuditObjectKind::Aspa
);
assert_eq!(
kind_from_rsync_uri("rsync://example.test/x.bin"),
AuditObjectKind::Other
);
}
#[test]
fn build_vcir_local_outputs_prefers_cached_outputs() {
let pack = dummy_pack_with_files(vec![]);
let ca = CaInstanceHandle {
depth: 0,
tal_id: "test-tal".to_string(),
parent_manifest_rsync_uri: None,
ca_certificate: CaCertificateRef::inline_der(vec![1]),
ca_certificate_rsync_uri: None,
effective_ip_resources: None,
effective_as_resources: None,
rsync_base_uri: pack.publication_point_rsync_uri.clone(),
manifest_rsync_uri: pack.manifest_rsync_uri.clone(),
publication_point_rsync_uri: pack.publication_point_rsync_uri.clone(),
rrdp_notification_uri: None,
};
let cached = vec![VcirLocalOutput {
output_type: VcirOutputType::Vrp,
item_effective_until: pack.next_update.clone(),
source_object_uri: "rsync://example.test/repo/issuer/a.roa".to_string(),
source_object_type: VcirSourceObjectType::Roa,
source_object_hash: sha256_32(b"cached-roa"),
source_ee_cert_hash: sha256_32(b"cached-ee"),
payload: VcirLocalOutputPayload::Vrp {
asn: 64500,
afi: RoaAfi::Ipv4,
prefix_len: 24,
addr: ipv4_addr([203, 0, 113, 0]),
max_length: 24,
},
rule_hash: sha256_32(b"cached-rule"),
}];
let mut objects = crate::validation::objects::ObjectsOutput {
vrps: Vec::new(),
aspas: Vec::new(),
router_keys: Vec::new(),
local_outputs_cache: cached.clone(),
warnings: Vec::new(),
stats: crate::validation::objects::ObjectsStats::default(),
audit: Vec::new(),
roa_cache_stats: crate::validation::objects::RoaValidationCacheStats::default(),
roa_cache_object_meta: Vec::new(),
};
let outputs =
take_or_build_vcir_local_outputs(&ca, &pack, &mut objects).expect("reuse cached outputs");
assert_eq!(outputs, cached);
assert!(objects.local_outputs_cache.is_empty());
}
#[test]
fn persist_vcir_non_repository_evidence_stores_current_ca_cert_only() {
let (pack, issuer_ca_der, validation_time) = cernet_publication_point_snapshot_for_vcir_tests();
let issuer_ca = ResourceCertificate::decode_der(&issuer_ca_der).expect("decode issuer ca");
let objects = crate::validation::objects::process_publication_point_snapshot_for_issuer(
&pack,
&Policy::default(),
issuer_ca_der.as_slice(),
Some(
"rsync://rpki.apnic.net/repository/B527EF581D6611E2BB468F7C72FD1FF2/BfycW4hQb3wNP4YsiJW-1n6fjro.cer",
),
issuer_ca.tbs.extensions.ip_resources.as_ref(),
issuer_ca.tbs.extensions.as_resources.as_ref(),
validation_time,
None,
);
assert!(
!objects.local_outputs_cache.is_empty(),
"expected local outputs from signed objects"
);
let store_dir = tempfile::tempdir().expect("store dir");
let store = RocksStore::open(store_dir.path()).expect("open rocksdb");
let ca = CaInstanceHandle {
depth: 0,
tal_id: "test-tal".to_string(),
parent_manifest_rsync_uri: None,
ca_certificate: CaCertificateRef::inline_der(issuer_ca_der.clone()),
ca_certificate_rsync_uri: Some(
"rsync://rpki.apnic.net/repository/B527EF581D6611E2BB468F7C72FD1FF2/BfycW4hQb3wNP4YsiJW-1n6fjro.cer".to_string(),
),
effective_ip_resources: issuer_ca.tbs.extensions.ip_resources.clone(),
effective_as_resources: issuer_ca.tbs.extensions.as_resources.clone(),
rsync_base_uri: pack.publication_point_rsync_uri.clone(),
manifest_rsync_uri: pack.manifest_rsync_uri.clone(),
publication_point_rsync_uri: pack.publication_point_rsync_uri.clone(),
rrdp_notification_uri: None,
};
persist_vcir_non_repository_evidence(&store, &ca).expect("persist embedded evidence");
let issuer_hash = sha256_hex(&issuer_ca_der);
let issuer_entry = store
.get_raw_by_hash_entry(&issuer_hash)
.expect("load issuer raw entry")
.expect("issuer raw entry present");
assert!(
issuer_entry
.origin_uris
.iter()
.any(|uri| uri.ends_with("BfycW4hQb3wNP4YsiJW-1n6fjro.cer"))
);
let first_output = objects
.local_outputs_cache
.first()
.expect("first local output");
assert!(
store
.get_raw_by_hash_entry(&first_output.source_ee_cert_hash_hex())
.expect("load source ee raw")
.is_none()
);
}
#[test]
fn build_router_key_local_outputs_encodes_router_key_payloads() {
let ca = CaInstanceHandle {
depth: 0,
tal_id: "test-tal".to_string(),
parent_manifest_rsync_uri: None,
ca_certificate: CaCertificateRef::inline_der(Vec::new()),
ca_certificate_rsync_uri: None,
effective_ip_resources: None,
effective_as_resources: None,
rsync_base_uri: "rsync://example.test/repo/issuer/".to_string(),
manifest_rsync_uri: "rsync://example.test/repo/issuer/issuer.mft".to_string(),
publication_point_rsync_uri: "rsync://example.test/repo/issuer/".to_string(),
rrdp_notification_uri: None,
};
let outputs = build_router_key_local_outputs(
&ca,
&[RouterKeyPayload {
as_id: 64496,
ski: vec![0x11; 20],
spki_der: vec![0x30, 0x00],
source_object_uri: "rsync://example.test/repo/issuer/router.cer".to_string(),
source_object_hash: "11".repeat(32),
source_ee_cert_hash: "11".repeat(32),
item_effective_until: PackTime {
rfc3339_utc: "2026-12-31T00:00:00Z".to_string(),
},
}],
);
assert_eq!(outputs.len(), 1);
assert_eq!(outputs[0].output_type, VcirOutputType::RouterKey);
assert_eq!(
outputs[0].source_object_type,
VcirSourceObjectType::RouterKey
);
assert!(outputs[0].payload_json().contains("spki_der_base64"));
}
#[test]
fn build_vcir_local_outputs_falls_back_to_decoding_accepted_objects_when_cache_is_empty() {
let (pack, issuer_ca_der, validation_time) = cernet_publication_point_snapshot_for_vcir_tests();
let issuer_ca = ResourceCertificate::decode_der(&issuer_ca_der).expect("decode issuer ca");
let objects = crate::validation::objects::process_publication_point_snapshot_for_issuer(
&pack,
&Policy::default(),
issuer_ca_der.as_slice(),
Some(
"rsync://rpki.apnic.net/repository/B527EF581D6611E2BB468F7C72FD1FF2/BfycW4hQb3wNP4YsiJW-1n6fjro.cer",
),
issuer_ca.tbs.extensions.ip_resources.as_ref(),
issuer_ca.tbs.extensions.as_resources.as_ref(),
validation_time,
None,
);
let mut objects_without_cache = objects.clone();
objects_without_cache.local_outputs_cache.clear();
let ca = CaInstanceHandle {
depth: 0,
tal_id: "test-tal".to_string(),
parent_manifest_rsync_uri: None,
ca_certificate: CaCertificateRef::inline_der(issuer_ca_der),
ca_certificate_rsync_uri: Some(
"rsync://rpki.apnic.net/repository/B527EF581D6611E2BB468F7C72FD1FF2/BfycW4hQb3wNP4YsiJW-1n6fjro.cer".to_string(),
),
effective_ip_resources: issuer_ca.tbs.extensions.ip_resources.clone(),
effective_as_resources: issuer_ca.tbs.extensions.as_resources.clone(),
rsync_base_uri: pack.publication_point_rsync_uri.clone(),
manifest_rsync_uri: pack.manifest_rsync_uri.clone(),
publication_point_rsync_uri: pack.publication_point_rsync_uri.clone(),
rrdp_notification_uri: None,
};
let local_outputs = build_vcir_local_outputs(&ca, &pack, &objects_without_cache)
.expect("rebuild vcir local outputs");
assert!(!local_outputs.is_empty());
assert_eq!(local_outputs.len(), objects.vrps.len());
assert!(
local_outputs
.iter()
.all(|output| output.output_type == VcirOutputType::Vrp)
);
}
#[test]
fn finalize_fresh_publication_point_releases_local_outputs_cache_after_persist() {
let (pack, issuer_ca_der, validation_time) = cernet_publication_point_snapshot_for_vcir_tests();
let issuer_ca = ResourceCertificate::decode_der(&issuer_ca_der).expect("decode issuer ca");
let mut objects = crate::validation::objects::process_publication_point_snapshot_for_issuer(
&pack,
&Policy::default(),
issuer_ca_der.as_slice(),
Some(
"rsync://rpki.apnic.net/repository/B527EF581D6611E2BB468F7C72FD1FF2/BfycW4hQb3wNP4YsiJW-1n6fjro.cer",
),
issuer_ca.tbs.extensions.ip_resources.as_ref(),
issuer_ca.tbs.extensions.as_resources.as_ref(),
validation_time,
None,
);
assert!(
!objects.local_outputs_cache.is_empty(),
"expected local outputs from signed objects"
);
let store_dir = tempfile::tempdir().expect("store dir");
let store = RocksStore::open(store_dir.path()).expect("open rocksdb");
let policy = Policy::default();
let runner = Rpkiv1PublicationPointRunner {
store: &store,
policy: &policy,
http_fetcher: &NeverHttpFetcher,
rsync_fetcher: &FailingRsyncFetcher,
validation_time,
timing: None,
download_log: None,
replay_archive_index: None,
replay_delta_index: None,
rrdp_dedup: false,
rrdp_repo_cache: Mutex::new(HashMap::new()),
rsync_dedup: false,
rsync_repo_cache: Mutex::new(HashMap::new()),
current_repo_index: None,
repo_sync_runtime: None,
parallel_phase2_config: None,
parallel_roa_worker_pool: None,
ccr_accumulator: None,
persist_vcir: true,
enable_roa_validation_cache: false,
enable_child_certificate_validation_cache: false,
publication_point_cache_observe_only: false,
enable_publication_point_validation_cache: false,
};
let ca = CaInstanceHandle {
depth: 0,
tal_id: "test-tal".to_string(),
parent_manifest_rsync_uri: None,
ca_certificate: CaCertificateRef::inline_der(issuer_ca_der.clone()),
ca_certificate_rsync_uri: Some(
"rsync://rpki.apnic.net/repository/B527EF581D6611E2BB468F7C72FD1FF2/BfycW4hQb3wNP4YsiJW-1n6fjro.cer".to_string(),
),
effective_ip_resources: issuer_ca.tbs.extensions.ip_resources.clone(),
effective_as_resources: issuer_ca.tbs.extensions.as_resources.clone(),
rsync_base_uri: pack.publication_point_rsync_uri.clone(),
manifest_rsync_uri: pack.manifest_rsync_uri.clone(),
publication_point_rsync_uri: pack.publication_point_rsync_uri.clone(),
rrdp_notification_uri: None,
};
let fresh_point = FreshValidatedPublicationPoint {
manifest_rsync_uri: pack.manifest_rsync_uri.clone(),
publication_point_rsync_uri: pack.publication_point_rsync_uri.clone(),
manifest_number_be: pack.manifest_number_be.clone(),
this_update: pack.this_update.clone(),
next_update: pack.next_update.clone(),
verified_at: pack.verified_at.clone(),
manifest_bytes: pack.manifest_bytes.clone(),
files: pack.files.clone(),
};
objects.local_outputs_cache.shrink_to_fit();
let original_cache_capacity = objects.local_outputs_cache.capacity();
let finalized = runner
.finalize_fresh_publication_point_from_reducer(
&ca,
&fresh_point,
Vec::new(),
objects,
Vec::new(),
Vec::new(),
None,
None,
0,
None,
)
.expect("finalize fresh publication point");
assert!(
finalized.result.objects.local_outputs_cache.is_empty(),
"local outputs cache should be released after VCIR persistence"
);
assert_eq!(
finalized.result.objects.local_outputs_cache.capacity(),
0,
"released cache should not keep its backing allocation"
);
assert!(original_cache_capacity > 0);
let persisted = store
.get_vcir(&pack.manifest_rsync_uri)
.expect("load persisted vcir")
.expect("persisted vcir");
assert!(
!persisted.local_outputs.is_empty(),
"VCIR should still persist local outputs before cache release"
);
}
#[test]
fn persist_vcir_for_fresh_result_stores_vcir_and_replay_meta_for_real_snapshot() {
let (pack, issuer_ca_der, validation_time) = cernet_publication_point_snapshot_for_vcir_tests();
let issuer_ca = ResourceCertificate::decode_der(&issuer_ca_der).expect("decode issuer ca");
let objects = crate::validation::objects::process_publication_point_snapshot_for_issuer(
&pack,
&Policy::default(),
issuer_ca_der.as_slice(),
Some(
"rsync://rpki.apnic.net/repository/B527EF581D6611E2BB468F7C72FD1FF2/BfycW4hQb3wNP4YsiJW-1n6fjro.cer",
),
issuer_ca.tbs.extensions.ip_resources.as_ref(),
issuer_ca.tbs.extensions.as_resources.as_ref(),
validation_time,
None,
);
let store_dir = tempfile::tempdir().expect("store dir");
let store = RocksStore::open(store_dir.path()).expect("open rocksdb");
let ca = CaInstanceHandle {
depth: 0,
tal_id: "test-tal".to_string(),
parent_manifest_rsync_uri: None,
ca_certificate: CaCertificateRef::inline_der(issuer_ca_der.clone()),
ca_certificate_rsync_uri: Some(
"rsync://rpki.apnic.net/repository/B527EF581D6611E2BB468F7C72FD1FF2/BfycW4hQb3wNP4YsiJW-1n6fjro.cer".to_string(),
),
effective_ip_resources: issuer_ca.tbs.extensions.ip_resources.clone(),
effective_as_resources: issuer_ca.tbs.extensions.as_resources.clone(),
rsync_base_uri: pack.publication_point_rsync_uri.clone(),
manifest_rsync_uri: pack.manifest_rsync_uri.clone(),
publication_point_rsync_uri: pack.publication_point_rsync_uri.clone(),
rrdp_notification_uri: None,
};
let mut objects = objects;
persist_vcir_for_fresh_result_with_timing(
&store,
&Policy::default(),
&ca,
&pack,
&mut objects,
&[],
&[],
&[],
validation_time,
false,
)
.map(|_timing| ())
.expect("persist vcir for fresh result");
let vcir = store
.get_vcir(&pack.manifest_rsync_uri)
.expect("get vcir")
.expect("vcir exists");
assert_eq!(vcir.manifest_rsync_uri, pack.manifest_rsync_uri);
assert_eq!(vcir.summary.local_vrp_count as usize, objects.vrps.len());
assert_eq!(
vcir.ccr_manifest_projection.manifest_rsync_uri,
pack.manifest_rsync_uri
);
assert_eq!(
vcir.ccr_manifest_projection.manifest_number_be,
pack.manifest_number_be
);
assert_eq!(
vcir.ccr_manifest_projection.manifest_this_update,
pack.this_update
);
assert_eq!(
vcir.ccr_manifest_projection.manifest_size,
pack.manifest_bytes.len() as u64
);
assert!(vcir.local_outputs.first().is_some(), "local outputs stored");
let replay_meta = store
.get_manifest_replay_meta(&pack.manifest_rsync_uri)
.expect("get replay meta")
.expect("replay meta exists");
assert_eq!(replay_meta.manifest_rsync_uri, pack.manifest_rsync_uri);
assert_eq!(
replay_meta.manifest_sha256,
sha2::Sha256::digest(&pack.manifest_bytes).to_vec()
);
}
#[test]
fn build_vcir_ccr_manifest_projection_from_fresh_real_snapshot_matches_manifest_contents() {
let (pack, issuer_ca_der, validation_time) = cernet_publication_point_snapshot_for_vcir_tests();
let issuer_ca = ResourceCertificate::decode_der(&issuer_ca_der).expect("decode issuer ca");
let ca = CaInstanceHandle {
depth: 0,
tal_id: "test-tal".to_string(),
parent_manifest_rsync_uri: None,
ca_certificate: CaCertificateRef::inline_der(issuer_ca_der),
ca_certificate_rsync_uri: Some(
"rsync://rpki.apnic.net/repository/B527EF581D6611E2BB468F7C72FD1FF2/BfycW4hQb3wNP4YsiJW-1n6fjro.cer".to_string(),
),
effective_ip_resources: issuer_ca.tbs.extensions.ip_resources.clone(),
effective_as_resources: issuer_ca.tbs.extensions.as_resources.clone(),
rsync_base_uri: pack.publication_point_rsync_uri.clone(),
manifest_rsync_uri: pack.manifest_rsync_uri.clone(),
publication_point_rsync_uri: pack.publication_point_rsync_uri.clone(),
rrdp_notification_uri: None,
};
let child_discovery =
discover_children_from_fresh_snapshot_with_audit(&ca, &pack, validation_time, None)
.expect("discover children");
let store_dir = tempfile::tempdir().expect("store dir");
let store = RocksStore::open(store_dir.path()).expect("open rocksdb");
let child_entries =
build_vcir_child_entries(&store, &child_discovery.children, validation_time)
.expect("build child entries");
let projection = build_vcir_ccr_manifest_projection_from_fresh(&ca, &pack, &child_entries)
.expect("build ccr manifest projection");
let manifest = ManifestObject::decode_der(&pack.manifest_bytes).expect("decode manifest");
let expected_locations = match manifest.signed_object.signed_data.certificates[0]
.resource_cert
.tbs
.extensions
.subject_info_access
.as_ref()
.expect("manifest sia")
{
SubjectInfoAccess::Ee(ee_sia) => vec![
crate::ccr::manifest_location::select_manifest_signed_object_location(
&pack.manifest_rsync_uri,
&ee_sia.access_descriptions,
)
.expect("select manifest signedObject"),
],
SubjectInfoAccess::Ca(_) => panic!("manifest ee SIA should not be CA variant"),
};
assert_eq!(projection.manifest_rsync_uri, pack.manifest_rsync_uri);
assert_eq!(
projection.manifest_sha256,
sha2::Sha256::digest(&pack.manifest_bytes).to_vec()
);
assert_eq!(projection.manifest_size, pack.manifest_bytes.len() as u64);
assert_eq!(
projection.manifest_ee_aki,
manifest.signed_object.signed_data.certificates[0]
.resource_cert
.tbs
.extensions
.authority_key_identifier
.clone()
.expect("manifest aki")
);
assert_eq!(
projection.manifest_number_be,
manifest.manifest.manifest_number.bytes_be
);
assert_eq!(projection.manifest_this_update, pack.this_update);
assert_eq!(projection.manifest_sia_locations_der, expected_locations);
let expected_subordinate_skis = child_entries
.iter()
.map(|child| hex::decode(&child.child_ski).expect("decode child ski"))
.collect::<Vec<_>>();
assert_eq!(projection.subordinate_skis, expected_subordinate_skis);
}
#[test]
fn build_vcir_child_entries_uses_projection_without_repo_bytes() {
let store_dir = tempfile::tempdir().expect("store dir");
let store = RocksStore::open(store_dir.path()).expect("open rocksdb");
let validation_time = time::OffsetDateTime::parse(
"2026-06-26T00:00:00Z",
&time::format_description::well_known::Rfc3339,
)
.expect("parse time");
let child = DiscoveredChildCaInstance {
handle: CaInstanceHandle {
depth: 1,
tal_id: "test-tal".to_string(),
parent_manifest_rsync_uri: Some("rsync://example.test/repo/root.mft".to_string()),
ca_certificate: CaCertificateRef::repo_bytes("aa".repeat(32)),
ca_certificate_rsync_uri: Some("rsync://example.test/repo/child.cer".to_string()),
effective_ip_resources: None,
effective_as_resources: None,
rsync_base_uri: "rsync://example.test/repo/child/".to_string(),
manifest_rsync_uri: "rsync://example.test/repo/child/child.mft".to_string(),
publication_point_rsync_uri: "rsync://example.test/repo/child/".to_string(),
rrdp_notification_uri: Some("https://example.test/notify.xml".to_string()),
},
discovered_from: crate::audit::DiscoveredFrom {
parent_manifest_rsync_uri: "rsync://example.test/repo/root.mft".to_string(),
child_ca_certificate_rsync_uri: "rsync://example.test/repo/child.cer".to_string(),
child_ca_certificate_sha256_hex: "aa".repeat(32),
},
child_entry_projection: Some(DiscoveredChildEntryProjection {
child_ski: "11".repeat(20),
}),
};
let entries = build_vcir_child_entries(&store, &[child], validation_time)
.expect("projection should avoid repo-bytes load");
assert_eq!(entries.len(), 1);
assert_eq!(entries[0].child_ski, "11".repeat(20));
}
#[test]
fn build_vcir_related_artifacts_classifies_snapshot_files_and_audit_statuses() {
let manifest_bytes = std::fs::read(
std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR")).join(
"tests/fixtures/repository/rpki.cernet.net/repo/cernet/0/05FC9C5B88506F7C0D3F862C8895BED67E9F8EBA.mft",
),
)
.expect("read manifest fixture");
let crl_bytes = std::fs::read(
std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR")).join(
"tests/fixtures/repository/rpki.cernet.net/repo/cernet/0/05FC9C5B88506F7C0D3F862C8895BED67E9F8EBA.crl",
),
)
.expect("read crl fixture");
let pack = PublicationPointSnapshot {
format_version: PublicationPointSnapshot::FORMAT_VERSION_V1,
manifest_rsync_uri: "rsync://example.test/repo/issuer/issuer.mft".to_string(),
publication_point_rsync_uri: "rsync://example.test/repo/issuer/".to_string(),
manifest_number_be: vec![1],
this_update: PackTime::from_utc_offset_datetime(time::OffsetDateTime::now_utc()),
next_update: PackTime::from_utc_offset_datetime(
time::OffsetDateTime::now_utc() + time::Duration::hours(1),
),
verified_at: PackTime::from_utc_offset_datetime(time::OffsetDateTime::now_utc()),
manifest_bytes,
files: vec![
PackFile::from_bytes_compute_sha256(
"rsync://example.test/repo/issuer/issuer.crl",
crl_bytes,
),
PackFile::from_bytes_compute_sha256(
"rsync://example.test/repo/issuer/child.cer",
vec![1u8, 2],
),
PackFile::from_bytes_compute_sha256(
"rsync://example.test/repo/issuer/a.roa",
vec![3u8, 4],
),
PackFile::from_bytes_compute_sha256(
"rsync://example.test/repo/issuer/a.asa",
vec![5u8, 6],
),
PackFile::from_bytes_compute_sha256(
"rsync://example.test/repo/issuer/a.gbr",
vec![7u8, 8],
),
PackFile::from_bytes_compute_sha256(
"rsync://example.test/repo/issuer/extra.bin",
vec![9u8],
),
],
};
let ca = CaInstanceHandle {
depth: 0,
tal_id: "test-tal".to_string(),
parent_manifest_rsync_uri: None,
ca_certificate: CaCertificateRef::inline_der(vec![0x11, 0x22]),
ca_certificate_rsync_uri: Some("rsync://example.test/repo/issuer/issuer.cer".to_string()),
effective_ip_resources: None,
effective_as_resources: None,
rsync_base_uri: pack.publication_point_rsync_uri.clone(),
manifest_rsync_uri: pack.manifest_rsync_uri.clone(),
publication_point_rsync_uri: pack.publication_point_rsync_uri.clone(),
rrdp_notification_uri: None,
};
let store_dir = tempfile::tempdir().expect("store dir");
let store = RocksStore::open(store_dir.path()).expect("open rocksdb");
let objects = crate::validation::objects::ObjectsOutput {
vrps: Vec::new(),
aspas: Vec::new(),
router_keys: Vec::new(),
local_outputs_cache: Vec::new(),
warnings: Vec::new(),
stats: crate::validation::objects::ObjectsStats::default(),
audit: vec![
ObjectAuditEntry {
rsync_uri: "rsync://example.test/repo/issuer/a.roa".to_string(),
sha256_hex: sha256_hex_from_32(&pack.files[2].sha256),
kind: AuditObjectKind::Roa,
result: AuditObjectResult::Error,
detail: Some("bad roa".to_string()),
},
ObjectAuditEntry {
rsync_uri: "rsync://example.test/repo/issuer/a.asa".to_string(),
sha256_hex: sha256_hex_from_32(&pack.files[3].sha256),
kind: AuditObjectKind::Aspa,
result: AuditObjectResult::Skipped,
detail: Some("skipped aspa".to_string()),
},
],
roa_cache_stats: crate::validation::objects::RoaValidationCacheStats::default(),
roa_cache_object_meta: Vec::new(),
};
let artifacts = build_vcir_related_artifacts(
&store,
&ca,
&pack,
"rsync://example.test/repo/issuer/issuer.crl",
&objects,
&[],
);
assert!(
artifacts
.iter()
.any(|artifact| artifact.artifact_role == VcirArtifactRole::Manifest)
);
assert!(
artifacts
.iter()
.any(|artifact| artifact.artifact_role == VcirArtifactRole::TrustAnchorCert)
);
assert!(artifacts.iter().any(|artifact| artifact.uri.as_deref()
== Some("rsync://example.test/repo/issuer/issuer.crl")
&& artifact.artifact_role == VcirArtifactRole::CurrentCrl));
assert!(artifacts.iter().any(|artifact| artifact.uri.as_deref()
== Some("rsync://example.test/repo/issuer/child.cer")
&& artifact.artifact_role == VcirArtifactRole::ChildCaCert));
assert!(artifacts.iter().any(|artifact| artifact.uri.as_deref()
== Some("rsync://example.test/repo/issuer/a.roa")
&& artifact.validation_status == VcirArtifactValidationStatus::Rejected
&& artifact.reject_reason.as_deref() == Some("bad roa")));
assert!(artifacts.iter().any(|artifact| artifact.uri.as_deref()
== Some("rsync://example.test/repo/issuer/a.asa")
&& artifact.validation_status == VcirArtifactValidationStatus::WarningOnly
&& artifact.reject_reason.is_none()));
assert!(artifacts.iter().any(|artifact| artifact.uri.as_deref()
== Some("rsync://example.test/repo/issuer/a.gbr")
&& artifact.artifact_kind == VcirArtifactKind::Gbr));
assert!(artifacts.iter().any(|artifact| artifact.uri.as_deref()
== Some("rsync://example.test/repo/issuer/extra.bin")
&& artifact.artifact_kind == VcirArtifactKind::Other));
assert!(
!artifacts
.iter()
.any(|artifact| artifact.uri.is_none()
&& artifact.sha256 == sha256_hex(b"embedded-ee")),
"embedded EE cert artifacts should no longer be persisted separately"
);
}
#[test]
fn select_issuer_crl_from_snapshot_reports_missing_crldp_for_self_signed_cert() {
let ta_der = std::fs::read(
std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("tests/fixtures/ta/apnic-ta.cer"),
)
.expect("read TA fixture");
let pack = dummy_pack_with_files(vec![]);
let err = select_issuer_crl_from_snapshot(&ta_der, &pack).unwrap_err();
assert!(err.contains("CRLDistributionPoints missing"), "{err}");
}
#[test]
fn select_issuer_crl_from_snapshot_finds_matching_crl() {
// Use real fixtures to ensure child cert has CRLDP rsync URI and CRL exists.
let child_cert_der =
std::fs::read(std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR")).join(
"tests/fixtures/repository/ca.rg.net/rpki/RGnet-OU/R-lVU1XGsAeqzV1Fv0HjOD6ZFkE.cer",
))
.expect("read child cert fixture");
let crl_der =
std::fs::read(std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR")).join(
"tests/fixtures/repository/ca.rg.net/rpki/RGnet-OU/bW-_qXU9uNhGQz21NR2ansB8lr0.crl",
))
.expect("read crl fixture");
let pack = dummy_pack_with_files(vec![PackFile::from_bytes_compute_sha256(
"rsync://ca.rg.net/rpki/RGnet-OU/bW-_qXU9uNhGQz21NR2ansB8lr0.crl",
crl_der.clone(),
)]);
let (uri, found) =
select_issuer_crl_from_snapshot(child_cert_der.as_slice(), &pack).expect("find crl");
assert_eq!(
uri,
"rsync://ca.rg.net/rpki/RGnet-OU/bW-_qXU9uNhGQz21NR2ansB8lr0.crl"
);
assert_eq!(found, crl_der.as_slice());
}
#[test]
fn discover_children_from_fresh_pack_discovers_child_ca() {
let g = generate_chain_and_crl();
let pack = dummy_pack_with_files(vec![
PackFile::from_bytes_compute_sha256(
"rsync://example.test/repo/issuer/issuer.crl",
g.issuer_crl_der.clone(),
),
PackFile::from_bytes_compute_sha256(
"rsync://example.test/repo/issuer/child.cer",
g.child_ca_der.clone(),
),
]);
let issuer_ca = ResourceCertificate::decode_der(&g.issuer_ca_der).expect("decode issuer");
let issuer = CaInstanceHandle {
depth: 0,
tal_id: "test-tal".to_string(),
parent_manifest_rsync_uri: None,
ca_certificate: CaCertificateRef::inline_der(g.issuer_ca_der.clone()),
ca_certificate_rsync_uri: None,
effective_ip_resources: issuer_ca.tbs.extensions.ip_resources.clone(),
effective_as_resources: issuer_ca.tbs.extensions.as_resources.clone(),
rsync_base_uri: "rsync://example.test/repo/issuer/".to_string(),
manifest_rsync_uri: "rsync://example.test/repo/issuer/issuer.mft".to_string(),
publication_point_rsync_uri: "rsync://example.test/repo/issuer/".to_string(),
rrdp_notification_uri: None,
};
let now = time::OffsetDateTime::now_utc();
let children = discover_children_from_fresh_snapshot_with_audit(&issuer, &pack, now, None)
.expect("discover children")
.children;
assert_eq!(children.len(), 1);
assert_eq!(
children[0].discovered_from.parent_manifest_rsync_uri,
issuer.manifest_rsync_uri
);
assert_eq!(
children[0].discovered_from.child_ca_certificate_rsync_uri,
"rsync://example.test/repo/issuer/child.cer"
);
assert_eq!(
children[0].handle.rsync_base_uri,
"rsync://example.test/repo/child/".to_string()
);
assert_eq!(
children[0].handle.manifest_rsync_uri,
"rsync://example.test/repo/child/child.mft".to_string()
);
assert_eq!(
children[0].handle.publication_point_rsync_uri,
"rsync://example.test/repo/child/".to_string()
);
assert_eq!(
children[0].handle.rrdp_notification_uri.as_deref(),
Some("https://example.test/notification.xml")
);
}
#[test]
fn discover_children_child_certificate_cache_reuses_successful_child_ca() {
let g = generate_chain_and_crl();
let pack = dummy_pack_with_files(vec![
PackFile::from_bytes_compute_sha256(
"rsync://example.test/repo/issuer/issuer.crl",
g.issuer_crl_der.clone(),
),
PackFile::from_bytes_compute_sha256(
"rsync://example.test/repo/issuer/child.cer",
g.child_ca_der.clone(),
),
]);
let issuer_ca = ResourceCertificate::decode_der(&g.issuer_ca_der).expect("decode issuer");
let issuer = CaInstanceHandle {
depth: 0,
tal_id: "test-tal".to_string(),
parent_manifest_rsync_uri: None,
ca_certificate: CaCertificateRef::inline_der(g.issuer_ca_der.clone()),
ca_certificate_rsync_uri: None,
effective_ip_resources: issuer_ca.tbs.extensions.ip_resources.clone(),
effective_as_resources: issuer_ca.tbs.extensions.as_resources.clone(),
rsync_base_uri: "rsync://example.test/repo/issuer/".to_string(),
manifest_rsync_uri: "rsync://example.test/repo/issuer/issuer.mft".to_string(),
publication_point_rsync_uri: "rsync://example.test/repo/issuer/".to_string(),
rrdp_notification_uri: None,
};
let store_dir = tempfile::tempdir().expect("store dir");
let store = RocksStore::open(store_dir.path()).expect("open rocksdb");
let policy = Policy::default();
let cache_context = ChildCertificateValidationCacheContext {
store: &store,
issuer_ca_sha256: issuer.ca_certificate_sha256_32().unwrap(),
ca_validation_context_digest: ca_validation_context_digest_for_ca(&issuer),
policy_fingerprint: publication_point_cache_policy_fingerprint(&policy),
};
let validation_time = time::OffsetDateTime::now_utc();
let first = discover_children_from_fresh_snapshot_with_audit_cached(
&issuer,
&pack,
validation_time,
None,
Some(cache_context),
)
.expect("first discovery writes cache");
assert_eq!(first.children.len(), 1);
let child_file = pack
.files
.iter()
.find(|file| file.rsync_uri.ends_with("child.cer"))
.expect("child file");
let cache_key = child_certificate_cache_key_sha256_hex(
&child_file.rsync_uri,
&child_file.sha256,
&cache_context.issuer_ca_sha256,
&cache_context.ca_validation_context_digest,
&cache_context.policy_fingerprint,
);
assert!(
store
.get_child_certificate_cache_projection(&cache_key)
.expect("get projection")
.is_some()
);
let timing = TimingHandle::new(crate::analysis::timing::TimingMeta {
recorded_at_utc_rfc3339: "2026-06-24T00:00:00Z".to_string(),
validation_time_utc_rfc3339: "2026-06-24T00:00:00Z".to_string(),
tal_url: None,
db_path: None,
});
let second = discover_children_from_fresh_snapshot_with_audit_cached(
&issuer,
&pack,
validation_time,
Some(&timing),
Some(cache_context),
)
.expect("second discovery reuses cache");
assert_eq!(second.children.len(), 1);
assert_eq!(
second.children[0].handle.manifest_rsync_uri,
first.children[0].handle.manifest_rsync_uri
);
assert!(second.audits.iter().any(|audit| {
audit
.detail
.as_deref()
.unwrap_or("")
.contains("child certificate validation cache")
}));
let counts = timing.counts_snapshot();
assert_eq!(
counts.get("child_certificate_cache_hit_ca").copied(),
Some(1)
);
assert_eq!(
counts
.get("child_certificate_cache_batch_lookup_publication_points")
.copied(),
Some(1)
);
assert_eq!(
counts
.get("child_certificate_cache_batch_lookup_entries")
.copied(),
Some(1)
);
assert_eq!(
counts
.get("child_certificate_der_load_fresh_count")
.copied(),
Some(0)
);
}
#[test]
fn discover_children_child_certificate_cache_rechecks_changed_valid_crl() {
let g = generate_chain_and_crl();
let pack = dummy_pack_with_files(vec![
PackFile::from_bytes_compute_sha256(
"rsync://example.test/repo/issuer/issuer.crl",
g.issuer_crl_der.clone(),
),
PackFile::from_bytes_compute_sha256(
"rsync://example.test/repo/issuer/child.cer",
g.child_ca_der.clone(),
),
]);
let mut changed_pack = pack.clone();
changed_pack.files[0] = PackFile::from_bytes_compute_sha256(
"rsync://example.test/repo/issuer/issuer.crl",
g.issuer_crl_der_next.clone(),
);
let issuer_ca = ResourceCertificate::decode_der(&g.issuer_ca_der).expect("decode issuer");
let issuer = CaInstanceHandle {
depth: 0,
tal_id: "test-tal".to_string(),
parent_manifest_rsync_uri: None,
ca_certificate: CaCertificateRef::inline_der(g.issuer_ca_der.clone()),
ca_certificate_rsync_uri: None,
effective_ip_resources: issuer_ca.tbs.extensions.ip_resources.clone(),
effective_as_resources: issuer_ca.tbs.extensions.as_resources.clone(),
rsync_base_uri: "rsync://example.test/repo/issuer/".to_string(),
manifest_rsync_uri: "rsync://example.test/repo/issuer/issuer.mft".to_string(),
publication_point_rsync_uri: "rsync://example.test/repo/issuer/".to_string(),
rrdp_notification_uri: None,
};
let store_dir = tempfile::tempdir().expect("store dir");
let store = RocksStore::open(store_dir.path()).expect("open rocksdb");
let policy = Policy::default();
let cache_context = ChildCertificateValidationCacheContext {
store: &store,
issuer_ca_sha256: issuer.ca_certificate_sha256_32().unwrap(),
ca_validation_context_digest: ca_validation_context_digest_for_ca(&issuer),
policy_fingerprint: publication_point_cache_policy_fingerprint(&policy),
};
let validation_time = time::OffsetDateTime::now_utc();
discover_children_from_fresh_snapshot_with_audit_cached(
&issuer,
&pack,
validation_time,
None,
Some(cache_context),
)
.expect("populate cache");
let child_file = pack
.files
.iter()
.find(|file| file.rsync_uri.ends_with("child.cer"))
.expect("child file");
let cache_key = child_certificate_cache_key_sha256_hex(
&child_file.rsync_uri,
&child_file.sha256,
&cache_context.issuer_ca_sha256,
&cache_context.ca_validation_context_digest,
&cache_context.policy_fingerprint,
);
let projection = store
.get_child_certificate_cache_projection(&cache_key)
.expect("get child certificate cache projection")
.expect("child certificate cache projection present");
let projected_until =
parse_snapshot_time_value(&projection.effective_until).expect("parse projected until");
let initial_crl = crate::data_model::crl::RpkixCrl::decode_der(&g.issuer_crl_der)
.expect("decode initial crl");
assert!(
projected_until > initial_crl.next_update.utc,
"child certificate cache projection must not be hard-capped by the issuing CRL nextUpdate"
);
let timing = TimingHandle::new(crate::analysis::timing::TimingMeta {
recorded_at_utc_rfc3339: "2026-06-24T00:00:00Z".to_string(),
validation_time_utc_rfc3339: "2026-06-24T00:00:00Z".to_string(),
tal_url: None,
db_path: None,
});
let out = discover_children_from_fresh_snapshot_with_audit_cached(
&issuer,
&changed_pack,
validation_time,
Some(&timing),
Some(cache_context),
)
.expect("changed valid CRL should recheck revocation and reuse cache");
assert_eq!(out.children.len(), 1);
assert!(
out.audits
.iter()
.any(|audit| audit.detail.as_deref().unwrap_or("").contains("cache"))
);
let counts = timing.counts_snapshot();
assert_eq!(
counts
.get("child_certificate_cache_crl_recheck_hit")
.copied(),
Some(1)
);
assert_eq!(
counts.get("child_certificate_cache_hit_ca").copied(),
Some(1)
);
}
#[test]
fn discover_children_child_certificate_cache_misses_when_current_crl_invalid() {
let g = generate_chain_and_crl();
let pack = dummy_pack_with_files(vec![
PackFile::from_bytes_compute_sha256(
"rsync://example.test/repo/issuer/issuer.crl",
g.issuer_crl_der.clone(),
),
PackFile::from_bytes_compute_sha256(
"rsync://example.test/repo/issuer/child.cer",
g.child_ca_der.clone(),
),
]);
let mut changed_pack = pack.clone();
changed_pack.files[0] = PackFile::from_bytes_compute_sha256(
"rsync://example.test/repo/issuer/issuer.crl",
b"not-a-valid-crl".to_vec(),
);
let issuer_ca = ResourceCertificate::decode_der(&g.issuer_ca_der).expect("decode issuer");
let issuer = CaInstanceHandle {
depth: 0,
tal_id: "test-tal".to_string(),
parent_manifest_rsync_uri: None,
ca_certificate: CaCertificateRef::inline_der(g.issuer_ca_der.clone()),
ca_certificate_rsync_uri: None,
effective_ip_resources: issuer_ca.tbs.extensions.ip_resources.clone(),
effective_as_resources: issuer_ca.tbs.extensions.as_resources.clone(),
rsync_base_uri: "rsync://example.test/repo/issuer/".to_string(),
manifest_rsync_uri: "rsync://example.test/repo/issuer/issuer.mft".to_string(),
publication_point_rsync_uri: "rsync://example.test/repo/issuer/".to_string(),
rrdp_notification_uri: None,
};
let store_dir = tempfile::tempdir().expect("store dir");
let store = RocksStore::open(store_dir.path()).expect("open rocksdb");
let policy = Policy::default();
let cache_context = ChildCertificateValidationCacheContext {
store: &store,
issuer_ca_sha256: issuer.ca_certificate_sha256_32().unwrap(),
ca_validation_context_digest: ca_validation_context_digest_for_ca(&issuer),
policy_fingerprint: publication_point_cache_policy_fingerprint(&policy),
};
let validation_time = time::OffsetDateTime::now_utc();
discover_children_from_fresh_snapshot_with_audit_cached(
&issuer,
&pack,
validation_time,
None,
Some(cache_context),
)
.expect("populate cache");
let timing = TimingHandle::new(crate::analysis::timing::TimingMeta {
recorded_at_utc_rfc3339: "2026-06-24T00:00:00Z".to_string(),
validation_time_utc_rfc3339: "2026-06-24T00:00:00Z".to_string(),
tal_url: None,
db_path: None,
});
let out = discover_children_from_fresh_snapshot_with_audit_cached(
&issuer,
&changed_pack,
validation_time,
Some(&timing),
Some(cache_context),
)
.expect("invalid CRL should miss cache and continue with audit error");
assert!(out.children.is_empty());
assert!(
out.audits
.iter()
.all(|audit| !audit.detail.as_deref().unwrap_or("").contains("cache"))
);
let counts = timing.counts_snapshot();
assert_eq!(
counts
.get("child_certificate_cache_miss_crl_invalid")
.copied(),
Some(1)
);
}
#[test]
fn discover_children_child_certificate_cache_misses_when_unchanged_crl_expired() {
let g = generate_chain_and_crl();
let pack = dummy_pack_with_files(vec![
PackFile::from_bytes_compute_sha256(
"rsync://example.test/repo/issuer/issuer.crl",
g.issuer_crl_der.clone(),
),
PackFile::from_bytes_compute_sha256(
"rsync://example.test/repo/issuer/child.cer",
g.child_ca_der.clone(),
),
]);
let issuer_ca = ResourceCertificate::decode_der(&g.issuer_ca_der).expect("decode issuer");
let issuer = CaInstanceHandle {
depth: 0,
tal_id: "test-tal".to_string(),
parent_manifest_rsync_uri: None,
ca_certificate: CaCertificateRef::inline_der(g.issuer_ca_der.clone()),
ca_certificate_rsync_uri: None,
effective_ip_resources: issuer_ca.tbs.extensions.ip_resources.clone(),
effective_as_resources: issuer_ca.tbs.extensions.as_resources.clone(),
rsync_base_uri: "rsync://example.test/repo/issuer/".to_string(),
manifest_rsync_uri: "rsync://example.test/repo/issuer/issuer.mft".to_string(),
publication_point_rsync_uri: "rsync://example.test/repo/issuer/".to_string(),
rrdp_notification_uri: None,
};
let store_dir = tempfile::tempdir().expect("store dir");
let store = RocksStore::open(store_dir.path()).expect("open rocksdb");
let policy = Policy::default();
let cache_context = ChildCertificateValidationCacheContext {
store: &store,
issuer_ca_sha256: issuer.ca_certificate_sha256_32().unwrap(),
ca_validation_context_digest: ca_validation_context_digest_for_ca(&issuer),
policy_fingerprint: publication_point_cache_policy_fingerprint(&policy),
};
let validation_time = time::OffsetDateTime::now_utc();
discover_children_from_fresh_snapshot_with_audit_cached(
&issuer,
&pack,
validation_time,
None,
Some(cache_context),
)
.expect("populate cache");
let timing = TimingHandle::new(crate::analysis::timing::TimingMeta {
recorded_at_utc_rfc3339: "2026-06-24T00:00:00Z".to_string(),
validation_time_utc_rfc3339: "2026-06-24T00:00:00Z".to_string(),
tal_url: None,
db_path: None,
});
let out = discover_children_from_fresh_snapshot_with_audit_cached(
&issuer,
&pack,
validation_time + time::Duration::days(2),
Some(&timing),
Some(cache_context),
)
.expect("expired unchanged CRL should miss cache and continue with audit error");
assert!(out.children.is_empty());
assert!(
out.audits
.iter()
.all(|audit| !audit.detail.as_deref().unwrap_or("").contains("cache"))
);
let counts = timing.counts_snapshot();
assert_eq!(
counts
.get("child_certificate_cache_miss_crl_expired")
.copied(),
Some(1)
);
}
#[test]
fn discover_children_with_audit_records_missing_crl_for_child_certificate() {
let now = time::OffsetDateTime::now_utc();
let child_ca_der =
std::fs::read(std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR")).join(
"tests/fixtures/repository/ca.rg.net/rpki/RGnet-OU/R-lVU1XGsAeqzV1Fv0HjOD6ZFkE.cer",
))
.expect("read child ca fixture");
// Pack contains the child CA cert but does not contain the CRL referenced by the child
// certificate CRLDistributionPoints extension.
let pack = dummy_pack_with_files(vec![PackFile::from_bytes_compute_sha256(
"rsync://ca.rg.net/rpki/RGnet-OU/R-lVU1XGsAeqzV1Fv0HjOD6ZFkE.cer",
child_ca_der,
)]);
let issuer = CaInstanceHandle {
depth: 0,
tal_id: "test-tal".to_string(),
parent_manifest_rsync_uri: None,
ca_certificate: CaCertificateRef::inline_der(vec![1]),
ca_certificate_rsync_uri: None,
effective_ip_resources: None,
effective_as_resources: None,
rsync_base_uri: "rsync://example.test/repo/issuer/".to_string(),
manifest_rsync_uri: pack.manifest_rsync_uri.clone(),
publication_point_rsync_uri: pack.publication_point_rsync_uri.clone(),
rrdp_notification_uri: None,
};
let out = discover_children_from_fresh_snapshot_with_audit(&issuer, &pack, now, None)
.expect("discovery should succeed with audit error");
assert_eq!(out.children.len(), 0);
assert_eq!(out.audits.len(), 1);
assert_eq!(
out.audits[0].rsync_uri,
"rsync://ca.rg.net/rpki/RGnet-OU/R-lVU1XGsAeqzV1Fv0HjOD6ZFkE.cer"
);
assert_eq!(out.audits[0].result, AuditObjectResult::Error);
assert!(
out.audits[0]
.detail
.as_deref()
.unwrap_or("")
.contains("cannot select issuer CRL"),
"expected deterministic CRL selection failure to be recorded"
);
}
#[test]
fn runner_offline_rsync_fixture_produces_pack_and_warnings() {
let fixture_dir = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.join("tests/fixtures/repository/rpki.cernet.net/repo/cernet/0");
assert!(fixture_dir.is_dir(), "fixture directory must exist");
let rsync_base_uri = "rsync://rpki.cernet.net/repo/cernet/0/".to_string();
let manifest_file = "05FC9C5B88506F7C0D3F862C8895BED67E9F8EBA.mft";
let manifest_rsync_uri = format!("{rsync_base_uri}{manifest_file}");
// Pick a validation_time inside the fixture manifest's validity window to keep this
// test stable across wall-clock time.
let fixture_manifest_bytes =
std::fs::read(fixture_dir.join(manifest_file)).expect("read manifest fixture");
let fixture_manifest =
crate::data_model::manifest::ManifestObject::decode_der(&fixture_manifest_bytes)
.expect("decode manifest fixture");
let validation_time = {
let this_update = fixture_manifest.manifest.this_update;
let next_update = fixture_manifest.manifest.next_update;
let candidate = this_update + time::Duration::seconds(60);
if candidate < next_update {
candidate
} else {
this_update
}
};
let store_dir = tempfile::tempdir().expect("store dir");
let store = RocksStore::open(store_dir.path()).expect("open rocksdb");
let policy = Policy {
sync_preference: crate::policy::SyncPreference::RsyncOnly,
..Policy::default()
};
let runner = Rpkiv1PublicationPointRunner {
store: &store,
policy: &policy,
http_fetcher: &NeverHttpFetcher,
rsync_fetcher: &LocalDirRsyncFetcher::new(&fixture_dir),
validation_time,
timing: None,
download_log: None,
replay_archive_index: None,
replay_delta_index: None,
rrdp_dedup: false,
rrdp_repo_cache: Mutex::new(HashMap::new()),
rsync_dedup: false,
rsync_repo_cache: Mutex::new(HashMap::new()),
current_repo_index: None,
repo_sync_runtime: None,
parallel_phase2_config: None,
parallel_roa_worker_pool: None,
ccr_accumulator: None,
persist_vcir: true,
enable_roa_validation_cache: false,
enable_child_certificate_validation_cache: false,
publication_point_cache_observe_only: false,
enable_publication_point_validation_cache: false,
};
// For this fixture-driven smoke, we provide the correct issuer CA certificate (the CA for
// this publication point) so ROA EE certificate paths can validate.
let issuer_ca_der = std::fs::read(
std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR")).join(
"tests/fixtures/repository/rpki.apnic.net/repository/B527EF581D6611E2BB468F7C72FD1FF2/BfycW4hQb3wNP4YsiJW-1n6fjro.cer",
),
)
.expect("read issuer ca fixture");
let issuer_ca = ResourceCertificate::decode_der(&issuer_ca_der).expect("decode issuer ca");
let handle = CaInstanceHandle {
depth: 0,
tal_id: "test-tal".to_string(),
parent_manifest_rsync_uri: None,
ca_certificate: CaCertificateRef::inline_der(issuer_ca_der),
ca_certificate_rsync_uri: Some("rsync://rpki.apnic.net/repository/B527EF581D6611E2BB468F7C72FD1FF2/BfycW4hQb3wNP4YsiJW-1n6fjro.cer".to_string()),
effective_ip_resources: issuer_ca.tbs.extensions.ip_resources.clone(),
effective_as_resources: issuer_ca.tbs.extensions.as_resources.clone(),
rsync_base_uri: rsync_base_uri.clone(),
manifest_rsync_uri: manifest_rsync_uri.clone(),
publication_point_rsync_uri: rsync_base_uri.clone(),
rrdp_notification_uri: None,
};
let out = runner
.run_publication_point(&handle)
.expect("run publication point");
assert_eq!(out.source, PublicationPointSource::Fresh);
let pack = out.snapshot.expect("fresh run pack");
assert_eq!(pack.manifest_rsync_uri, manifest_rsync_uri);
assert!(pack.files.len() > 1);
assert!(
out.objects.vrps.len() > 1,
"expected to extract VRPs from ROAs"
);
let vcir = store
.get_vcir(&manifest_rsync_uri)
.expect("get vcir")
.expect("vcir exists after fresh run");
assert_eq!(vcir.manifest_rsync_uri, manifest_rsync_uri);
assert_eq!(vcir.tal_id, "test-tal");
assert!(
vcir.local_outputs
.iter()
.any(|output| output.output_type == crate::storage::VcirOutputType::Vrp),
"expected VCIR local_outputs to contain VRP entries"
);
let first_vrp = vcir
.local_outputs
.iter()
.find(|output| output.output_type == crate::storage::VcirOutputType::Vrp)
.expect("first VCIR VRP output");
assert!(!first_vrp.rule_hash_hex().is_empty());
assert!(!first_vrp.output_id().is_empty());
let replay_meta = store
.get_manifest_replay_meta(&manifest_rsync_uri)
.expect("get replay meta")
.expect("replay meta exists");
assert_eq!(replay_meta.manifest_rsync_uri, manifest_rsync_uri);
}
#[test]
fn runner_roa_validation_cache_reuses_vcir_outputs_on_second_fixture_run() {
let fixture_dir = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.join("tests/fixtures/repository/rpki.cernet.net/repo/cernet/0");
assert!(fixture_dir.is_dir(), "fixture directory must exist");
let rsync_base_uri = "rsync://rpki.cernet.net/repo/cernet/0/".to_string();
let manifest_file = "05FC9C5B88506F7C0D3F862C8895BED67E9F8EBA.mft";
let manifest_rsync_uri = format!("{rsync_base_uri}{manifest_file}");
let fixture_manifest_bytes =
std::fs::read(fixture_dir.join(manifest_file)).expect("read manifest fixture");
let fixture_manifest =
crate::data_model::manifest::ManifestObject::decode_der(&fixture_manifest_bytes)
.expect("decode manifest fixture");
let validation_time = fixture_manifest.manifest.this_update + time::Duration::seconds(60);
let store_dir = tempfile::tempdir().expect("store dir");
let store = RocksStore::open(store_dir.path()).expect("open rocksdb");
let policy = Policy {
sync_preference: crate::policy::SyncPreference::RsyncOnly,
..Policy::default()
};
let issuer_ca_der = std::fs::read(
std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR")).join(
"tests/fixtures/repository/rpki.apnic.net/repository/B527EF581D6611E2BB468F7C72FD1FF2/BfycW4hQb3wNP4YsiJW-1n6fjro.cer",
),
)
.expect("read issuer ca fixture");
let issuer_ca = ResourceCertificate::decode_der(&issuer_ca_der).expect("decode issuer ca");
let handle = CaInstanceHandle {
depth: 0,
tal_id: "test-tal".to_string(),
parent_manifest_rsync_uri: None,
ca_certificate: CaCertificateRef::inline_der(issuer_ca_der),
ca_certificate_rsync_uri: Some("rsync://rpki.apnic.net/repository/B527EF581D6611E2BB468F7C72FD1FF2/BfycW4hQb3wNP4YsiJW-1n6fjro.cer".to_string()),
effective_ip_resources: issuer_ca.tbs.extensions.ip_resources.clone(),
effective_as_resources: issuer_ca.tbs.extensions.as_resources.clone(),
rsync_base_uri: rsync_base_uri.clone(),
manifest_rsync_uri: manifest_rsync_uri.clone(),
publication_point_rsync_uri: rsync_base_uri,
rrdp_notification_uri: None,
};
let first_runner = Rpkiv1PublicationPointRunner {
store: &store,
policy: &policy,
http_fetcher: &NeverHttpFetcher,
rsync_fetcher: &LocalDirRsyncFetcher::new(&fixture_dir),
validation_time,
timing: None,
download_log: None,
replay_archive_index: None,
replay_delta_index: None,
rrdp_dedup: false,
rrdp_repo_cache: Mutex::new(HashMap::new()),
rsync_dedup: false,
rsync_repo_cache: Mutex::new(HashMap::new()),
current_repo_index: None,
repo_sync_runtime: None,
parallel_phase2_config: None,
parallel_roa_worker_pool: None,
ccr_accumulator: None,
persist_vcir: true,
enable_roa_validation_cache: false,
enable_child_certificate_validation_cache: false,
publication_point_cache_observe_only: false,
enable_publication_point_validation_cache: false,
};
let first = first_runner
.run_publication_point(&handle)
.expect("first fresh run");
assert!(first.objects.vrps.len() > 1);
assert_eq!(first.objects.roa_cache_stats.hit_roas, 0);
let second_runner = Rpkiv1PublicationPointRunner {
store: &store,
policy: &policy,
http_fetcher: &NeverHttpFetcher,
rsync_fetcher: &LocalDirRsyncFetcher::new(&fixture_dir),
validation_time,
timing: None,
download_log: None,
replay_archive_index: None,
replay_delta_index: None,
rrdp_dedup: false,
rrdp_repo_cache: Mutex::new(HashMap::new()),
rsync_dedup: false,
rsync_repo_cache: Mutex::new(HashMap::new()),
current_repo_index: None,
repo_sync_runtime: None,
parallel_phase2_config: None,
parallel_roa_worker_pool: None,
ccr_accumulator: None,
persist_vcir: true,
enable_roa_validation_cache: true,
enable_child_certificate_validation_cache: false,
publication_point_cache_observe_only: false,
enable_publication_point_validation_cache: false,
};
let second = second_runner
.run_publication_point(&handle)
.expect("second cache-enabled run");
assert_eq!(second.objects.vrps, first.objects.vrps);
assert_eq!(second.objects.roa_cache_stats.enabled_publication_points, 1);
assert_eq!(
second.objects.roa_cache_stats.vcir_hit_publication_points,
1
);
assert_eq!(
second.objects.roa_cache_stats.vcir_miss_publication_points,
0
);
assert!(second.objects.roa_cache_stats.hit_roas > 1);
assert_eq!(second.objects.roa_cache_stats.miss_roas, 0);
assert_eq!(second.objects.roa_cache_stats.blocked_roas, 0);
assert_eq!(second.objects.roa_cache_stats.fresh_roas, 0);
}
#[test]
fn runner_publication_point_cache_observe_and_reuse_path() {
let fixture_dir = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.join("tests/fixtures/repository/rpki.cernet.net/repo/cernet/0");
assert!(fixture_dir.is_dir(), "fixture directory must exist");
let rsync_base_uri = "rsync://rpki.cernet.net/repo/cernet/0/".to_string();
let manifest_file = "05FC9C5B88506F7C0D3F862C8895BED67E9F8EBA.mft";
let manifest_rsync_uri = format!("{rsync_base_uri}{manifest_file}");
let fixture_manifest_bytes =
std::fs::read(fixture_dir.join(manifest_file)).expect("read manifest fixture");
let fixture_manifest =
crate::data_model::manifest::ManifestObject::decode_der(&fixture_manifest_bytes)
.expect("decode manifest fixture");
let validation_time = fixture_manifest.manifest.this_update + time::Duration::seconds(60);
let store_dir = tempfile::tempdir().expect("store dir");
let store = RocksStore::open(store_dir.path()).expect("open rocksdb");
let policy = Policy {
sync_preference: crate::policy::SyncPreference::RsyncOnly,
..Policy::default()
};
let issuer_ca_der = std::fs::read(
std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR")).join(
"tests/fixtures/repository/rpki.apnic.net/repository/B527EF581D6611E2BB468F7C72FD1FF2/BfycW4hQb3wNP4YsiJW-1n6fjro.cer",
),
)
.expect("read issuer ca fixture");
let issuer_ca = ResourceCertificate::decode_der(&issuer_ca_der).expect("decode issuer ca");
let handle = CaInstanceHandle {
depth: 0,
tal_id: "test-tal".to_string(),
parent_manifest_rsync_uri: None,
ca_certificate: CaCertificateRef::inline_der(issuer_ca_der),
ca_certificate_rsync_uri: Some("rsync://rpki.apnic.net/repository/B527EF581D6611E2BB468F7C72FD1FF2/BfycW4hQb3wNP4YsiJW-1n6fjro.cer".to_string()),
effective_ip_resources: issuer_ca.tbs.extensions.ip_resources.clone(),
effective_as_resources: issuer_ca.tbs.extensions.as_resources.clone(),
rsync_base_uri: rsync_base_uri.clone(),
manifest_rsync_uri: manifest_rsync_uri.clone(),
publication_point_rsync_uri: rsync_base_uri,
rrdp_notification_uri: None,
};
let first_runner = Rpkiv1PublicationPointRunner {
store: &store,
policy: &policy,
http_fetcher: &NeverHttpFetcher,
rsync_fetcher: &LocalDirRsyncFetcher::new(&fixture_dir),
validation_time,
timing: None,
download_log: None,
replay_archive_index: None,
replay_delta_index: None,
rrdp_dedup: false,
rrdp_repo_cache: Mutex::new(HashMap::new()),
rsync_dedup: false,
rsync_repo_cache: Mutex::new(HashMap::new()),
current_repo_index: None,
repo_sync_runtime: None,
parallel_phase2_config: None,
parallel_roa_worker_pool: None,
ccr_accumulator: None,
persist_vcir: true,
enable_roa_validation_cache: false,
enable_child_certificate_validation_cache: false,
publication_point_cache_observe_only: true,
enable_publication_point_validation_cache: false,
};
let first = first_runner
.run_publication_point(&handle)
.expect("first fresh run");
assert_eq!(first.source, PublicationPointSource::Fresh);
assert!(
store
.get_publication_point_cache_projection(&manifest_rsync_uri)
.expect("load publication-point projection")
.is_some()
);
let timing = crate::analysis::timing::TimingHandle::new(crate::analysis::timing::TimingMeta {
recorded_at_utc_rfc3339: "2026-01-01T00:00:00Z".to_string(),
validation_time_utc_rfc3339: "2026-01-01T00:00:00Z".to_string(),
tal_url: None,
db_path: None,
});
let observe_runner = Rpkiv1PublicationPointRunner {
store: &store,
policy: &policy,
http_fetcher: &NeverHttpFetcher,
rsync_fetcher: &LocalDirRsyncFetcher::new(&fixture_dir),
validation_time,
timing: Some(timing.clone()),
download_log: None,
replay_archive_index: None,
replay_delta_index: None,
rrdp_dedup: false,
rrdp_repo_cache: Mutex::new(HashMap::new()),
rsync_dedup: false,
rsync_repo_cache: Mutex::new(HashMap::new()),
current_repo_index: None,
repo_sync_runtime: None,
parallel_phase2_config: None,
parallel_roa_worker_pool: None,
ccr_accumulator: None,
persist_vcir: true,
enable_roa_validation_cache: false,
enable_child_certificate_validation_cache: false,
publication_point_cache_observe_only: true,
enable_publication_point_validation_cache: false,
};
let observed = observe_runner
.run_publication_point(&handle)
.expect("observe-only run");
assert_eq!(observed.source, PublicationPointSource::Fresh);
assert_eq!(observed.objects.vrps, first.objects.vrps);
assert_eq!(
timing
.counts_snapshot()
.get("publication_point_cache_theoretical_hits")
.copied(),
Some(1)
);
let cache_runner = Rpkiv1PublicationPointRunner {
store: &store,
policy: &policy,
http_fetcher: &NeverHttpFetcher,
rsync_fetcher: &LocalDirRsyncFetcher::new(&fixture_dir),
validation_time,
timing: None,
download_log: None,
replay_archive_index: None,
replay_delta_index: None,
rrdp_dedup: false,
rrdp_repo_cache: Mutex::new(HashMap::new()),
rsync_dedup: false,
rsync_repo_cache: Mutex::new(HashMap::new()),
current_repo_index: None,
repo_sync_runtime: None,
parallel_phase2_config: None,
parallel_roa_worker_pool: None,
ccr_accumulator: None,
persist_vcir: true,
enable_roa_validation_cache: false,
enable_child_certificate_validation_cache: false,
publication_point_cache_observe_only: false,
enable_publication_point_validation_cache: true,
};
let cached = cache_runner
.run_publication_point(&handle)
.expect("publication-point cache run");
assert_eq!(cached.source, PublicationPointSource::PublicationPointCache);
assert_eq!(cached.objects.vrps, first.objects.vrps);
assert_eq!(cached.objects.aspas, first.objects.aspas);
assert_eq!(
cached.discovered_children.len(),
first.discovered_children.len()
);
assert!(!cached.cir_cached_objects.is_empty());
}
fn seed_publication_point_cache_projection(
store: &RocksStore,
policy: &Policy,
ca: &CaInstanceHandle,
validation_time: time::OffsetDateTime,
) -> ValidatedCaInstanceResult {
let child_bytes = b"child-cert".to_vec();
let child_hash = sha256_hex(&child_bytes);
let vcir = sample_vcir_for_projection(validation_time, &child_hash);
store
.put_blob_bytes_batch(&[
(child_hash, child_bytes),
(sha256_hex(b"manifest-bytes"), b"manifest-bytes".to_vec()),
])
.expect("put cache bytes");
store
.put_repository_view_entry(&RepositoryViewEntry {
rsync_uri: ca.manifest_rsync_uri.clone(),
current_hash: Some(sha256_hex(b"manifest-bytes")),
repository_source: Some(ca.publication_point_rsync_uri.clone()),
object_type: Some("mft".to_string()),
state: RepositoryViewState::Present,
})
.expect("put manifest current view");
let projection = PublicationPointCacheProjection::from_vcir_with_context(
&vcir,
ca.publication_point_rsync_uri.clone(),
ca.ca_certificate_rsync_uri.clone(),
ca.ca_certificate_sha256_32().unwrap(),
sha256_32(b"manifest-bytes"),
ta_context_digest_for_ca(ca),
ca_validation_context_digest_for_ca(ca),
publication_point_cache_policy_fingerprint(policy),
)
.expect("build publication point projection");
store
.put_vcir_with_publication_point_cache_projection(&vcir, Some(&projection))
.expect("put publication point projection");
vcir
}
#[test]
fn publication_point_cache_future_notbefore_guard_detects_future_roa_notbefore_only() {
let uri = "rsync://example.test/repo/issuer/future.roa";
let bytes = std::fs::read(
std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.join("tests/fixtures/repository/rpki.cernet.net/repo/cernet/0/AS4538.roa"),
)
.expect("read ROA fixture");
let roa = RoaObject::decode_der(&bytes).expect("decode ROA fixture");
let ee = &roa.signed_object.signed_data.certificates[0].resource_cert;
let file = PackFile::from_bytes_compute_sha256(uri, bytes);
let pack = dummy_pack_with_files(vec![file]);
let objects = crate::validation::objects::ObjectsOutput {
vrps: Vec::new(),
aspas: Vec::new(),
router_keys: Vec::new(),
local_outputs_cache: Vec::new(),
warnings: Vec::new(),
stats: Default::default(),
audit: vec![ObjectAuditEntry {
rsync_uri: uri.to_string(),
sha256_hex: sha256_hex_from_32(&pack.files[0].sha256),
kind: AuditObjectKind::Roa,
result: AuditObjectResult::Error,
detail: Some(
"EE certificate path validation failed: certificate not valid at validation_time"
.to_string(),
),
}],
roa_cache_stats: Default::default(),
roa_cache_object_meta: Vec::new(),
};
assert!(publication_point_cache_has_future_not_before_risk(
&pack,
&objects,
&[],
ee.tbs.validity_not_before - time::Duration::seconds(1),
&Policy::default(),
));
assert!(!publication_point_cache_has_future_not_before_risk(
&pack,
&objects,
&[],
ee.tbs.validity_not_after + time::Duration::seconds(1),
&Policy::default(),
));
}
#[test]
fn publication_point_cache_delete_action_removes_existing_projection() {
let store_dir = tempfile::tempdir().expect("store dir");
let store = RocksStore::open(store_dir.path()).expect("open rocksdb");
let policy = Policy::default();
let validation_time = time::OffsetDateTime::UNIX_EPOCH + time::Duration::minutes(1);
let ca = publication_point_cache_fixture_ca();
let vcir = seed_publication_point_cache_projection(&store, &policy, &ca, validation_time);
assert!(
store
.get_publication_point_cache_projection_cached(&ca.manifest_rsync_uri)
.expect("load projection")
.is_some()
);
store
.replace_vcir_manifest_replay_meta_and_projection_action(
&vcir,
None,
PublicationPointCacheProjectionWriteAction::Delete {
manifest_rsync_uri: &vcir.manifest_rsync_uri,
},
)
.expect("delete projection");
assert!(
store
.get_publication_point_cache_projection_cached(&ca.manifest_rsync_uri)
.expect("load projection after delete")
.is_none()
);
}
fn publication_point_cache_fixture_ca() -> CaInstanceHandle {
CaInstanceHandle {
depth: 1,
tal_id: "test-tal".to_string(),
parent_manifest_rsync_uri: None,
ca_certificate: CaCertificateRef::inline_der(b"ca-cert".to_vec()),
ca_certificate_rsync_uri: Some("rsync://example.test/repo/issuer/issuer.cer".to_string()),
effective_ip_resources: None,
effective_as_resources: None,
rsync_base_uri: "rsync://example.test/repo/issuer/".to_string(),
manifest_rsync_uri: "rsync://example.test/repo/issuer/issuer.mft".to_string(),
publication_point_rsync_uri: "rsync://example.test/repo/issuer/".to_string(),
rrdp_notification_uri: Some("https://example.test/notification.xml".to_string()),
}
}
#[test]
fn runner_publication_point_cache_reuses_projection_outputs_children_and_ccr() {
let store_dir = tempfile::tempdir().expect("store dir");
let store = RocksStore::open(store_dir.path()).expect("open rocksdb");
let policy = Policy::default();
let validation_time = time::OffsetDateTime::UNIX_EPOCH + time::Duration::minutes(1);
let ca = publication_point_cache_fixture_ca();
let vcir = seed_publication_point_cache_projection(&store, &policy, &ca, validation_time);
let timing = crate::analysis::timing::TimingHandle::new(crate::analysis::timing::TimingMeta {
recorded_at_utc_rfc3339: "2026-01-01T00:00:00Z".to_string(),
validation_time_utc_rfc3339: "2026-01-01T00:00:00Z".to_string(),
tal_url: None,
db_path: None,
});
let runner = Rpkiv1PublicationPointRunner {
store: &store,
policy: &policy,
http_fetcher: &NeverHttpFetcher,
rsync_fetcher: &FailingRsyncFetcher,
validation_time,
timing: Some(timing.clone()),
download_log: None,
replay_archive_index: None,
replay_delta_index: None,
rrdp_dedup: false,
rrdp_repo_cache: Mutex::new(HashMap::new()),
rsync_dedup: false,
rsync_repo_cache: Mutex::new(HashMap::new()),
current_repo_index: None,
repo_sync_runtime: None,
parallel_phase2_config: None,
parallel_roa_worker_pool: None,
ccr_accumulator: Some(Mutex::new(CcrAccumulator::new(Vec::new()))),
persist_vcir: true,
enable_roa_validation_cache: false,
enable_child_certificate_validation_cache: false,
publication_point_cache_observe_only: false,
enable_publication_point_validation_cache: true,
};
let result = runner
.observe_or_reuse_publication_point_cache(&ca, Some("rrdp"), Some("delta"), 7, None, &[])
.expect("cache result");
assert_eq!(result.source, PublicationPointSource::PublicationPointCache);
assert_eq!(result.objects.vrps.len(), 1);
assert_eq!(result.objects.aspas.len(), 1);
assert_eq!(result.objects.router_keys.len(), 1);
assert_eq!(result.discovered_children.len(), 1);
assert_eq!(
result.discovered_children[0].handle.manifest_rsync_uri,
vcir.child_entries[0].child_manifest_rsync_uri
);
assert!(result.cir_fresh_objects.is_empty());
assert!(!result.cir_cached_objects.is_empty());
assert_eq!(
runner
.ccr_accumulator_snapshot()
.expect("ccr accumulator")
.manifest_count(),
1
);
let counts = timing.counts_snapshot();
assert_eq!(
counts.get("publication_point_cache_reuse_hits").copied(),
Some(1)
);
assert_eq!(
counts
.get("publication_point_cache_outputs_reused")
.copied(),
Some(3)
);
assert_eq!(
counts
.get("publication_point_cache_children_reused")
.copied(),
Some(1)
);
assert_eq!(
counts
.get("publication_point_cache_related_objects_reused")
.copied(),
Some(vcir.related_artifacts.len() as u64)
);
assert_eq!(
counts
.get("publication_point_cache_audit_objects_reused")
.copied(),
Some(result.cir_cached_objects.len() as u64)
);
let timing_dir = tempfile::tempdir().expect("timing dir");
let timing_path = timing_dir.path().join("timing.json");
timing.write_json(&timing_path, 20).expect("write timing");
let timing_json: serde_json::Value =
serde_json::from_slice(&std::fs::read(&timing_path).expect("read timing"))
.expect("parse timing");
let phase_keys = timing_json["phases"]
.as_object()
.expect("phases")
.keys()
.map(|key| key.as_str())
.collect::<std::collections::HashSet<_>>();
assert!(phase_keys.contains("publication_point_cache_lookup_hit_total"));
assert!(phase_keys.contains("publication_point_cache_reuse_build_total"));
assert!(phase_keys.contains("publication_point_cache_build_objects_total"));
}
#[test]
fn publication_point_cache_restore_children_parallel_keeps_order_and_audit() {
let store_dir = tempfile::tempdir().expect("store dir");
let store = RocksStore::open(store_dir.path()).expect("open rocksdb");
let policy = Policy::default();
let validation_time = time::OffsetDateTime::UNIX_EPOCH + time::Duration::minutes(1);
let ca = publication_point_cache_fixture_ca();
seed_publication_point_cache_projection(&store, &policy, &ca, validation_time);
let mut projection = store
.get_publication_point_cache_projection(&ca.manifest_rsync_uri)
.expect("load projection")
.expect("projection");
let template = projection.children[0].clone();
let mut blobs = Vec::new();
let mut children = Vec::new();
for index in 0..300 {
let bytes = format!("child-cert-{index}").into_bytes();
let child_hash = sha256_hex(&bytes);
let mut child = template.clone();
child.child_cert_hash = child_hash.clone();
child.child_cert_rsync_uri = format!("rsync://example.test/repo/issuer/child-{index}.cer");
child.child_manifest_rsync_uri =
format!("rsync://example.test/repo/child-{index}/child.mft");
child.child_publication_point_rsync_uri =
format!("rsync://example.test/repo/child-{index}/");
child.child_rsync_base_uri = child.child_publication_point_rsync_uri.clone();
blobs.push((child_hash, bytes));
children.push(child);
}
projection.children = children;
store.put_blob_bytes_batch(&blobs).expect("put child blobs");
let timing = crate::analysis::timing::TimingHandle::new(crate::analysis::timing::TimingMeta {
recorded_at_utc_rfc3339: "2026-01-01T00:00:00Z".to_string(),
validation_time_utc_rfc3339: "2026-01-01T00:00:00Z".to_string(),
tal_url: None,
db_path: None,
});
let mut warnings = Vec::new();
let (restored_children, audits) = restore_children_from_publication_point_cache(
&store,
&ca,
&projection,
validation_time,
&mut warnings,
4,
Some(&timing),
);
assert!(warnings.is_empty());
assert_eq!(restored_children.len(), 300);
assert_eq!(audits.len(), 300);
assert_eq!(
restored_children[0].handle.ca_certificate_sha256_hex(),
Some(sha256_hex(b"child-cert-0").as_str())
);
assert_eq!(
restored_children[299]
.handle
.ca_certificate_der(&store)
.unwrap()
.as_ref(),
b"child-cert-299"
);
assert_eq!(
restored_children[0].handle.manifest_rsync_uri,
"rsync://example.test/repo/child-0/child.mft"
);
assert!(
audits
.iter()
.all(|audit| audit.result == AuditObjectResult::Ok)
);
let counts = timing.counts_snapshot();
assert_eq!(
counts
.get("publication_point_cache_restore_children_parallel_publication_points")
.copied(),
Some(1)
);
assert_eq!(
counts
.get("publication_point_cache_restore_children_parallel_children")
.copied(),
Some(300)
);
assert_eq!(
counts
.get("publication_point_cache_restore_children_workers_total")
.copied(),
Some(4)
);
}
#[test]
fn publication_point_cache_restore_children_does_not_require_child_der_bytes() {
let store_dir = tempfile::tempdir().expect("store dir");
let store = RocksStore::open(store_dir.path()).expect("open rocksdb");
let policy = Policy::default();
let validation_time = time::OffsetDateTime::UNIX_EPOCH + time::Duration::minutes(1);
let ca = publication_point_cache_fixture_ca();
seed_publication_point_cache_projection(&store, &policy, &ca, validation_time);
let mut projection = store
.get_publication_point_cache_projection(&ca.manifest_rsync_uri)
.expect("load projection")
.expect("projection");
projection.children[0].child_cert_hash = "22".repeat(32);
let mut warnings = Vec::new();
let (restored_children, audits) = restore_children_from_publication_point_cache(
&store,
&ca,
&projection,
validation_time,
&mut warnings,
1,
None,
);
assert!(warnings.is_empty());
assert!(!restored_children.is_empty());
assert_eq!(audits.len(), restored_children.len());
assert_eq!(
restored_children[0].handle.ca_certificate_sha256_hex(),
Some(projection.children[0].child_cert_hash.as_str())
);
assert!(
restored_children[0]
.handle
.ca_certificate_der(&store)
.is_err(),
"lazy child handle should not require repo bytes until a fresh path asks for DER"
);
}
#[test]
fn runner_publication_point_cache_blocks_parent_policy_and_output_time_mismatch() {
let store_dir = tempfile::tempdir().expect("store dir");
let store = RocksStore::open(store_dir.path()).expect("open rocksdb");
let policy = Policy::default();
let validation_time = time::OffsetDateTime::UNIX_EPOCH + time::Duration::minutes(1);
let ca = publication_point_cache_fixture_ca();
seed_publication_point_cache_projection(&store, &policy, &ca, validation_time);
let timing = crate::analysis::timing::TimingHandle::new(crate::analysis::timing::TimingMeta {
recorded_at_utc_rfc3339: "2026-01-01T00:00:00Z".to_string(),
validation_time_utc_rfc3339: "2026-01-01T00:00:00Z".to_string(),
tal_url: None,
db_path: None,
});
let mut parent_changed_ca = ca.clone();
parent_changed_ca.parent_manifest_rsync_uri =
Some("rsync://example.test/repo/other-parent.mft".to_string());
let parent_changed_runner = Rpkiv1PublicationPointRunner {
store: &store,
policy: &policy,
http_fetcher: &NeverHttpFetcher,
rsync_fetcher: &FailingRsyncFetcher,
validation_time,
timing: Some(timing.clone()),
download_log: None,
replay_archive_index: None,
replay_delta_index: None,
rrdp_dedup: false,
rrdp_repo_cache: Mutex::new(HashMap::new()),
rsync_dedup: false,
rsync_repo_cache: Mutex::new(HashMap::new()),
current_repo_index: None,
repo_sync_runtime: None,
parallel_phase2_config: None,
parallel_roa_worker_pool: None,
ccr_accumulator: None,
persist_vcir: true,
enable_roa_validation_cache: false,
enable_child_certificate_validation_cache: false,
publication_point_cache_observe_only: false,
enable_publication_point_validation_cache: true,
};
assert!(
parent_changed_runner
.observe_or_reuse_publication_point_cache(
&parent_changed_ca,
Some("rrdp"),
Some("delta"),
7,
None,
&[]
)
.is_none()
);
assert_eq!(
timing
.counts_snapshot()
.get("publication_point_cache_miss_parent_context_mismatch")
.copied(),
Some(1)
);
let strict_policy = Policy {
strict: crate::policy::StrictPolicy::all(),
..Policy::default()
};
let policy_changed_runner = Rpkiv1PublicationPointRunner {
store: &store,
policy: &strict_policy,
http_fetcher: &NeverHttpFetcher,
rsync_fetcher: &FailingRsyncFetcher,
validation_time,
timing: Some(timing.clone()),
download_log: None,
replay_archive_index: None,
replay_delta_index: None,
rrdp_dedup: false,
rrdp_repo_cache: Mutex::new(HashMap::new()),
rsync_dedup: false,
rsync_repo_cache: Mutex::new(HashMap::new()),
current_repo_index: None,
repo_sync_runtime: None,
parallel_phase2_config: None,
parallel_roa_worker_pool: None,
ccr_accumulator: None,
persist_vcir: true,
enable_roa_validation_cache: false,
enable_child_certificate_validation_cache: false,
publication_point_cache_observe_only: false,
enable_publication_point_validation_cache: true,
};
assert!(
policy_changed_runner
.observe_or_reuse_publication_point_cache(
&ca,
Some("rrdp"),
Some("delta"),
7,
None,
&[]
)
.is_none()
);
assert_eq!(
timing
.counts_snapshot()
.get("publication_point_cache_miss_policy_mismatch")
.copied(),
Some(1)
);
let output_expired_runner = Rpkiv1PublicationPointRunner {
store: &store,
policy: &policy,
http_fetcher: &NeverHttpFetcher,
rsync_fetcher: &FailingRsyncFetcher,
validation_time: validation_time + time::Duration::minutes(40),
timing: Some(timing.clone()),
download_log: None,
replay_archive_index: None,
replay_delta_index: None,
rrdp_dedup: false,
rrdp_repo_cache: Mutex::new(HashMap::new()),
rsync_dedup: false,
rsync_repo_cache: Mutex::new(HashMap::new()),
current_repo_index: None,
repo_sync_runtime: None,
parallel_phase2_config: None,
parallel_roa_worker_pool: None,
ccr_accumulator: None,
persist_vcir: true,
enable_roa_validation_cache: false,
enable_child_certificate_validation_cache: false,
publication_point_cache_observe_only: false,
enable_publication_point_validation_cache: true,
};
assert!(
output_expired_runner
.observe_or_reuse_publication_point_cache(
&ca,
Some("rrdp"),
Some("delta"),
7,
None,
&[]
)
.is_none()
);
assert_eq!(
timing
.counts_snapshot()
.get("publication_point_cache_miss_output_time_gate")
.copied(),
Some(1)
);
}
#[test]
fn runner_rsync_dedup_skips_second_sync_for_same_base() {
let fixture_dir = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.join("tests/fixtures/repository/rpki.cernet.net/repo/cernet/0");
assert!(fixture_dir.is_dir(), "fixture directory must exist");
let rsync_base_uri = "rsync://rpki.cernet.net/repo/cernet/0/".to_string();
let manifest_file = "05FC9C5B88506F7C0D3F862C8895BED67E9F8EBA.mft";
let manifest_rsync_uri = format!("{rsync_base_uri}{manifest_file}");
let fixture_manifest_bytes =
std::fs::read(fixture_dir.join(manifest_file)).expect("read manifest fixture");
let fixture_manifest =
crate::data_model::manifest::ManifestObject::decode_der(&fixture_manifest_bytes)
.expect("decode manifest fixture");
let validation_time = fixture_manifest.manifest.this_update + time::Duration::seconds(60);
let store_dir = tempfile::tempdir().expect("store dir");
let store = RocksStore::open(store_dir.path()).expect("open rocksdb");
let policy = Policy {
sync_preference: crate::policy::SyncPreference::RsyncOnly,
..Policy::default()
};
let issuer_ca_der = std::fs::read(
std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR")).join(
"tests/fixtures/repository/rpki.apnic.net/repository/B527EF581D6611E2BB468F7C72FD1FF2/BfycW4hQb3wNP4YsiJW-1n6fjro.cer",
),
)
.expect("read issuer ca fixture");
let issuer_ca = ResourceCertificate::decode_der(&issuer_ca_der).expect("decode issuer ca");
let handle = CaInstanceHandle {
depth: 0,
tal_id: "test-tal".to_string(),
parent_manifest_rsync_uri: None,
ca_certificate: CaCertificateRef::inline_der(issuer_ca_der),
ca_certificate_rsync_uri: Some("rsync://rpki.apnic.net/repository/B527EF581D6611E2BB468F7C72FD1FF2/BfycW4hQb3wNP4YsiJW-1n6fjro.cer".to_string()),
effective_ip_resources: issuer_ca.tbs.extensions.ip_resources.clone(),
effective_as_resources: issuer_ca.tbs.extensions.as_resources.clone(),
rsync_base_uri: rsync_base_uri.clone(),
manifest_rsync_uri: manifest_rsync_uri.clone(),
publication_point_rsync_uri: rsync_base_uri.clone(),
rrdp_notification_uri: None,
};
struct CountingRsyncFetcher {
inner: LocalDirRsyncFetcher,
calls: Arc<AtomicUsize>,
}
impl RsyncFetcher for CountingRsyncFetcher {
fn fetch_objects(
&self,
rsync_base_uri: &str,
) -> Result<Vec<(String, Vec<u8>)>, RsyncFetchError> {
self.calls.fetch_add(1, Ordering::SeqCst);
self.inner.fetch_objects(rsync_base_uri)
}
}
let calls = Arc::new(AtomicUsize::new(0));
let rsync = CountingRsyncFetcher {
inner: LocalDirRsyncFetcher::new(&fixture_dir),
calls: calls.clone(),
};
let runner = Rpkiv1PublicationPointRunner {
store: &store,
policy: &policy,
http_fetcher: &NeverHttpFetcher,
rsync_fetcher: &rsync,
validation_time,
timing: None,
download_log: None,
replay_archive_index: None,
replay_delta_index: None,
rrdp_dedup: false,
rrdp_repo_cache: Mutex::new(HashMap::new()),
rsync_dedup: true,
rsync_repo_cache: Mutex::new(HashMap::new()),
current_repo_index: None,
repo_sync_runtime: None,
parallel_phase2_config: None,
parallel_roa_worker_pool: None,
ccr_accumulator: None,
persist_vcir: true,
enable_roa_validation_cache: false,
enable_child_certificate_validation_cache: false,
publication_point_cache_observe_only: false,
enable_publication_point_validation_cache: false,
};
let first = runner.run_publication_point(&handle).expect("first run ok");
assert_eq!(first.source, PublicationPointSource::Fresh);
let second = runner
.run_publication_point(&handle)
.expect("second run ok");
assert_eq!(second.source, PublicationPointSource::Fresh);
assert_eq!(
calls.load(Ordering::SeqCst),
1,
"rsync should be called once"
);
}
#[test]
fn runner_rsync_dedup_skips_second_sync_for_same_module_scope() {
let fixture_dir = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.join("tests/fixtures/repository/rpki.cernet.net/repo/cernet/0");
assert!(fixture_dir.is_dir(), "fixture directory must exist");
let first_base_uri = "rsync://rpki.cernet.net/repo/cernet/0/".to_string();
let second_base_uri = "rsync://rpki.cernet.net/repo/cernet/0/sub/".to_string();
let manifest_file = "05FC9C5B88506F7C0D3F862C8895BED67E9F8EBA.mft";
let manifest_rsync_uri = format!("{first_base_uri}{manifest_file}");
let fixture_manifest_bytes =
std::fs::read(fixture_dir.join(manifest_file)).expect("read manifest fixture");
let fixture_manifest =
crate::data_model::manifest::ManifestObject::decode_der(&fixture_manifest_bytes)
.expect("decode manifest fixture");
let validation_time = fixture_manifest.manifest.this_update + time::Duration::seconds(60);
let store_dir = tempfile::tempdir().expect("store dir");
let store = RocksStore::open(store_dir.path()).expect("open rocksdb");
let policy = Policy {
sync_preference: crate::policy::SyncPreference::RsyncOnly,
..Policy::default()
};
let issuer_ca_der = std::fs::read(
std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR")).join(
"tests/fixtures/repository/rpki.apnic.net/repository/B527EF581D6611E2BB468F7C72FD1FF2/BfycW4hQb3wNP4YsiJW-1n6fjro.cer",
),
)
.expect("read issuer ca fixture");
let issuer_ca = ResourceCertificate::decode_der(&issuer_ca_der).expect("decode issuer ca");
let handle = CaInstanceHandle {
depth: 0,
tal_id: "test-tal".to_string(),
parent_manifest_rsync_uri: None,
ca_certificate: CaCertificateRef::inline_der(issuer_ca_der),
ca_certificate_rsync_uri: Some("rsync://rpki.apnic.net/repository/B527EF581D6611E2BB468F7C72FD1FF2/BfycW4hQb3wNP4YsiJW-1n6fjro.cer".to_string()),
effective_ip_resources: issuer_ca.tbs.extensions.ip_resources.clone(),
effective_as_resources: issuer_ca.tbs.extensions.as_resources.clone(),
rsync_base_uri: first_base_uri.clone(),
manifest_rsync_uri: manifest_rsync_uri.clone(),
publication_point_rsync_uri: first_base_uri.clone(),
rrdp_notification_uri: None,
};
let second_handle = CaInstanceHandle {
rsync_base_uri: second_base_uri.clone(),
publication_point_rsync_uri: second_base_uri.clone(),
..handle.clone()
};
struct ModuleScopeRsyncFetcher {
inner: LocalDirRsyncFetcher,
calls: Arc<AtomicUsize>,
}
impl RsyncFetcher for ModuleScopeRsyncFetcher {
fn fetch_objects(
&self,
rsync_base_uri: &str,
) -> Result<Vec<(String, Vec<u8>)>, RsyncFetchError> {
self.calls.fetch_add(1, Ordering::SeqCst);
self.inner.fetch_objects(rsync_base_uri)
}
fn dedup_key(&self, _rsync_base_uri: &str) -> String {
"rsync://rpki.cernet.net/repo/".to_string()
}
}
let calls = Arc::new(AtomicUsize::new(0));
let rsync = ModuleScopeRsyncFetcher {
inner: LocalDirRsyncFetcher::new(&fixture_dir),
calls: calls.clone(),
};
let runner = Rpkiv1PublicationPointRunner {
store: &store,
policy: &policy,
http_fetcher: &NeverHttpFetcher,
rsync_fetcher: &rsync,
validation_time,
timing: None,
download_log: None,
replay_archive_index: None,
replay_delta_index: None,
rrdp_dedup: false,
rrdp_repo_cache: Mutex::new(HashMap::new()),
rsync_dedup: true,
rsync_repo_cache: Mutex::new(HashMap::new()),
current_repo_index: None,
repo_sync_runtime: None,
parallel_phase2_config: None,
parallel_roa_worker_pool: None,
ccr_accumulator: None,
persist_vcir: true,
enable_roa_validation_cache: false,
enable_child_certificate_validation_cache: false,
publication_point_cache_observe_only: false,
enable_publication_point_validation_cache: false,
};
let first = runner.run_publication_point(&handle).expect("first run ok");
assert_eq!(first.source, PublicationPointSource::Fresh);
let second = runner
.run_publication_point(&second_handle)
.expect("second run ok");
assert!(matches!(
second.source,
PublicationPointSource::Fresh | PublicationPointSource::VcirCurrentInstance
));
assert_eq!(
calls.load(Ordering::SeqCst),
1,
"module-scope dedup should skip second sync"
);
}
#[test]
fn runner_rsync_dedup_works_in_rsync_only_mode_even_when_rrdp_notify_exists() {
let fixture_dir = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.join("tests/fixtures/repository/rpki.cernet.net/repo/cernet/0");
assert!(fixture_dir.is_dir(), "fixture directory must exist");
let first_base_uri = "rsync://rpki.cernet.net/repo/cernet/0/".to_string();
let second_base_uri = "rsync://rpki.cernet.net/repo/cernet/0/sub/".to_string();
let manifest_file = "05FC9C5B88506F7C0D3F862C8895BED67E9F8EBA.mft";
let manifest_rsync_uri = format!("{first_base_uri}{manifest_file}");
let fixture_manifest_bytes =
std::fs::read(fixture_dir.join(manifest_file)).expect("read manifest fixture");
let fixture_manifest =
crate::data_model::manifest::ManifestObject::decode_der(&fixture_manifest_bytes)
.expect("decode manifest fixture");
let validation_time = fixture_manifest.manifest.this_update + time::Duration::seconds(60);
let store_dir = tempfile::tempdir().expect("store dir");
let store = RocksStore::open(store_dir.path()).expect("open rocksdb");
let policy = Policy {
sync_preference: crate::policy::SyncPreference::RsyncOnly,
..Policy::default()
};
let issuer_ca_der = std::fs::read(
std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR")).join(
"tests/fixtures/repository/rpki.apnic.net/repository/B527EF581D6611E2BB468F7C72FD1FF2/BfycW4hQb3wNP4YsiJW-1n6fjro.cer",
),
)
.expect("read issuer ca fixture");
let issuer_ca = ResourceCertificate::decode_der(&issuer_ca_der).expect("decode issuer ca");
let handle = CaInstanceHandle {
depth: 0,
tal_id: "test-tal".to_string(),
parent_manifest_rsync_uri: None,
ca_certificate: CaCertificateRef::inline_der(issuer_ca_der),
ca_certificate_rsync_uri: Some("rsync://rpki.apnic.net/repository/B527EF581D6611E2BB468F7C72FD1FF2/BfycW4hQb3wNP4YsiJW-1n6fjro.cer".to_string()),
effective_ip_resources: issuer_ca.tbs.extensions.ip_resources.clone(),
effective_as_resources: issuer_ca.tbs.extensions.as_resources.clone(),
rsync_base_uri: first_base_uri.clone(),
manifest_rsync_uri: manifest_rsync_uri.clone(),
publication_point_rsync_uri: first_base_uri.clone(),
rrdp_notification_uri: Some("https://rrdp.example.test/notification.xml".to_string()),
};
let second_handle = CaInstanceHandle {
rsync_base_uri: second_base_uri.clone(),
publication_point_rsync_uri: second_base_uri.clone(),
..handle.clone()
};
struct ModuleScopeRsyncFetcher {
inner: LocalDirRsyncFetcher,
calls: Arc<AtomicUsize>,
}
impl RsyncFetcher for ModuleScopeRsyncFetcher {
fn fetch_objects(
&self,
rsync_base_uri: &str,
) -> Result<Vec<(String, Vec<u8>)>, RsyncFetchError> {
self.calls.fetch_add(1, Ordering::SeqCst);
self.inner.fetch_objects(rsync_base_uri)
}
fn dedup_key(&self, _rsync_base_uri: &str) -> String {
"rsync://rpki.cernet.net/repo/".to_string()
}
}
let calls = Arc::new(AtomicUsize::new(0));
let rsync = ModuleScopeRsyncFetcher {
inner: LocalDirRsyncFetcher::new(&fixture_dir),
calls: calls.clone(),
};
let runner = Rpkiv1PublicationPointRunner {
store: &store,
policy: &policy,
http_fetcher: &NeverHttpFetcher,
rsync_fetcher: &rsync,
validation_time,
timing: None,
download_log: None,
replay_archive_index: None,
replay_delta_index: None,
rrdp_dedup: true,
rrdp_repo_cache: Mutex::new(HashMap::new()),
rsync_dedup: true,
rsync_repo_cache: Mutex::new(HashMap::new()),
current_repo_index: None,
repo_sync_runtime: None,
parallel_phase2_config: None,
parallel_roa_worker_pool: None,
ccr_accumulator: None,
persist_vcir: true,
enable_roa_validation_cache: false,
enable_child_certificate_validation_cache: false,
publication_point_cache_observe_only: false,
enable_publication_point_validation_cache: false,
};
let first = runner.run_publication_point(&handle).expect("first run ok");
assert_eq!(first.source, PublicationPointSource::Fresh);
let second = runner
.run_publication_point(&second_handle)
.expect("second run ok");
assert!(matches!(
second.source,
PublicationPointSource::Fresh | PublicationPointSource::VcirCurrentInstance
));
assert_eq!(
calls.load(Ordering::SeqCst),
1,
"rsync-only mode must deduplicate by rsync scope even when RRDP notification is present"
);
}
#[test]
fn runner_when_repo_sync_fails_uses_current_instance_vcir_and_keeps_children_empty_for_fixture() {
let fixture_dir = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.join("tests/fixtures/repository/rpki.cernet.net/repo/cernet/0");
assert!(fixture_dir.is_dir(), "fixture directory must exist");
let rsync_base_uri = "rsync://rpki.cernet.net/repo/cernet/0/".to_string();
let manifest_file = "05FC9C5B88506F7C0D3F862C8895BED67E9F8EBA.mft";
let manifest_rsync_uri = format!("{rsync_base_uri}{manifest_file}");
let fixture_manifest_bytes =
std::fs::read(fixture_dir.join(manifest_file)).expect("read manifest fixture");
let fixture_manifest =
crate::data_model::manifest::ManifestObject::decode_der(&fixture_manifest_bytes)
.expect("decode manifest fixture");
let validation_time = fixture_manifest.manifest.this_update + time::Duration::seconds(60);
let store_dir = tempfile::tempdir().expect("store dir");
let store = RocksStore::open(store_dir.path()).expect("open rocksdb");
let policy = Policy {
sync_preference: crate::policy::SyncPreference::RsyncOnly,
..Policy::default()
};
let issuer_ca_der = std::fs::read(
std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR")).join(
"tests/fixtures/repository/rpki.apnic.net/repository/B527EF581D6611E2BB468F7C72FD1FF2/BfycW4hQb3wNP4YsiJW-1n6fjro.cer",
),
)
.expect("read issuer ca fixture");
let issuer_ca = ResourceCertificate::decode_der(&issuer_ca_der).expect("decode issuer ca");
let handle = CaInstanceHandle {
depth: 0,
tal_id: "test-tal".to_string(),
parent_manifest_rsync_uri: None,
ca_certificate: CaCertificateRef::inline_der(issuer_ca_der),
ca_certificate_rsync_uri: Some("rsync://rpki.apnic.net/repository/B527EF581D6611E2BB468F7C72FD1FF2/BfycW4hQb3wNP4YsiJW-1n6fjro.cer".to_string()),
effective_ip_resources: issuer_ca.tbs.extensions.ip_resources.clone(),
effective_as_resources: issuer_ca.tbs.extensions.as_resources.clone(),
rsync_base_uri: rsync_base_uri.clone(),
manifest_rsync_uri: manifest_rsync_uri.clone(),
publication_point_rsync_uri: rsync_base_uri.clone(),
rrdp_notification_uri: None,
};
// First: successful fresh run to populate the latest VCIR baseline.
let ok_runner = Rpkiv1PublicationPointRunner {
store: &store,
policy: &policy,
http_fetcher: &NeverHttpFetcher,
rsync_fetcher: &LocalDirRsyncFetcher::new(&fixture_dir),
validation_time,
timing: None,
download_log: None,
replay_archive_index: None,
replay_delta_index: None,
rrdp_dedup: false,
rrdp_repo_cache: Mutex::new(HashMap::new()),
rsync_dedup: false,
rsync_repo_cache: Mutex::new(HashMap::new()),
current_repo_index: None,
repo_sync_runtime: None,
parallel_phase2_config: None,
parallel_roa_worker_pool: None,
ccr_accumulator: None,
persist_vcir: true,
enable_roa_validation_cache: false,
enable_child_certificate_validation_cache: false,
publication_point_cache_observe_only: false,
enable_publication_point_validation_cache: false,
};
let first = ok_runner
.run_publication_point(&handle)
.expect("first run ok");
assert_eq!(first.source, PublicationPointSource::Fresh);
assert!(
first.discovered_children.is_empty(),
"fixture has no child .cer"
);
// Second: repo sync fails, but we can still reuse current-instance VCIR.
let bad_runner = Rpkiv1PublicationPointRunner {
store: &store,
policy: &policy,
http_fetcher: &NeverHttpFetcher,
rsync_fetcher: &FailingRsyncFetcher,
validation_time,
timing: None,
download_log: None,
replay_archive_index: None,
replay_delta_index: None,
rrdp_dedup: false,
rrdp_repo_cache: Mutex::new(HashMap::new()),
rsync_dedup: false,
rsync_repo_cache: Mutex::new(HashMap::new()),
current_repo_index: None,
repo_sync_runtime: None,
parallel_phase2_config: None,
parallel_roa_worker_pool: None,
ccr_accumulator: None,
persist_vcir: true,
enable_roa_validation_cache: false,
enable_child_certificate_validation_cache: false,
publication_point_cache_observe_only: false,
enable_publication_point_validation_cache: false,
};
let second = bad_runner
.run_publication_point(&handle)
.expect("should reuse current-instance VCIR");
assert_eq!(second.source, PublicationPointSource::VcirCurrentInstance);
assert!(second.discovered_children.is_empty());
assert!(
second
.warnings
.iter()
.any(|w| w.message.contains("repo sync failed")),
"expected warning about repo sync failure"
);
// Verification-only replays have the frozen repository bytes available,
// so a fresh read would succeed and never naturally enter the fallback
// branch. A contract-selected URI must therefore select the same VCIR
// projection without attempting fresh manifest/object validation.
let mut forced_policy = policy.clone();
forced_policy
.verification_forced_vcir_reuse_manifest_uris
.insert(manifest_rsync_uri.clone());
let forced_runner = Rpkiv1PublicationPointRunner {
store: &store,
policy: &forced_policy,
http_fetcher: &NeverHttpFetcher,
rsync_fetcher: &LocalDirRsyncFetcher::new(&fixture_dir),
validation_time,
timing: None,
download_log: None,
replay_archive_index: None,
replay_delta_index: None,
rrdp_dedup: false,
rrdp_repo_cache: Mutex::new(HashMap::new()),
rsync_dedup: false,
rsync_repo_cache: Mutex::new(HashMap::new()),
current_repo_index: None,
repo_sync_runtime: None,
parallel_phase2_config: None,
parallel_roa_worker_pool: None,
ccr_accumulator: None,
persist_vcir: true,
enable_roa_validation_cache: false,
enable_child_certificate_validation_cache: false,
publication_point_cache_observe_only: false,
enable_publication_point_validation_cache: false,
};
let forced = forced_runner
.run_publication_point(&handle)
.expect("contract-selected fallback should reuse current-instance VCIR");
assert_eq!(forced.source, PublicationPointSource::VcirCurrentInstance);
assert!(forced.discovered_children.is_empty());
}
#[test]
fn build_publication_point_audit_emits_no_audit_entry_for_duplicate_pack_uri() {
let pack = dummy_pack_with_files(vec![
PackFile::from_bytes_compute_sha256("rsync://example.test/repo/dup.roa", vec![1u8]),
PackFile::from_bytes_compute_sha256("rsync://example.test/repo/dup.roa", vec![2u8]),
]);
let pp = crate::validation::manifest::PublicationPointResult {
source: crate::validation::manifest::PublicationPointSource::VcirCurrentInstance,
snapshot: pack.clone(),
warnings: Vec::new(),
};
let ca = CaInstanceHandle {
depth: 0,
tal_id: "test-tal".to_string(),
parent_manifest_rsync_uri: None,
ca_certificate: CaCertificateRef::inline_der(vec![1]),
ca_certificate_rsync_uri: None,
effective_ip_resources: None,
effective_as_resources: None,
rsync_base_uri: pack.publication_point_rsync_uri.clone(),
manifest_rsync_uri: pack.manifest_rsync_uri.clone(),
publication_point_rsync_uri: pack.publication_point_rsync_uri.clone(),
rrdp_notification_uri: None,
};
let objects = crate::validation::objects::ObjectsOutput {
vrps: Vec::new(),
aspas: Vec::new(),
router_keys: Vec::new(),
local_outputs_cache: Vec::new(),
warnings: Vec::new(),
stats: crate::validation::objects::ObjectsStats::default(),
audit: Vec::new(),
roa_cache_stats: crate::validation::objects::RoaValidationCacheStats::default(),
roa_cache_object_meta: Vec::new(),
};
let audit = build_publication_point_audit_from_snapshot(
&ca,
pp.source,
None,
None,
None,
None,
&pp.snapshot,
&[],
&objects,
&[],
);
assert_eq!(audit.source, "vcir_current_instance");
assert_eq!(audit.repo_sync_phase, None);
assert_eq!(audit.repo_terminal_state, "fallback_current_instance");
assert!(
audit
.objects
.iter()
.any(|e| e.detail.as_deref() == Some("skipped: no audit entry")),
"expected a duplicate key to produce a 'no audit entry' placeholder"
);
}
#[test]
fn build_publication_point_audit_marks_invalid_crl_as_error_and_overlays_roa_audit() {
let now = time::OffsetDateTime::now_utc();
let pack = dummy_pack_with_files(vec![
PackFile::from_bytes_compute_sha256("rsync://example.test/repo/issuer/bad.crl", vec![0u8]),
PackFile::from_bytes_compute_sha256("rsync://example.test/repo/issuer/x.roa", vec![1u8]),
]);
let pp = crate::validation::manifest::PublicationPointResult {
source: crate::validation::manifest::PublicationPointSource::Fresh,
snapshot: pack.clone(),
warnings: Vec::new(),
};
let issuer = CaInstanceHandle {
depth: 0,
tal_id: "test-tal".to_string(),
parent_manifest_rsync_uri: None,
ca_certificate: CaCertificateRef::inline_der(vec![1]),
ca_certificate_rsync_uri: None,
effective_ip_resources: None,
effective_as_resources: None,
rsync_base_uri: "rsync://example.test/repo/issuer/".to_string(),
manifest_rsync_uri: pack.manifest_rsync_uri.clone(),
publication_point_rsync_uri: pack.publication_point_rsync_uri.clone(),
rrdp_notification_uri: None,
};
let objects = crate::validation::objects::ObjectsOutput {
vrps: Vec::new(),
aspas: Vec::new(),
router_keys: Vec::new(),
local_outputs_cache: Vec::new(),
warnings: Vec::new(),
stats: crate::validation::objects::ObjectsStats::default(),
audit: vec![ObjectAuditEntry {
rsync_uri: "rsync://example.test/repo/issuer/x.roa".to_string(),
sha256_hex: sha256_hex_from_32(&pack.files[1].sha256),
kind: AuditObjectKind::Roa,
result: AuditObjectResult::Ok,
detail: None,
}],
roa_cache_stats: crate::validation::objects::RoaValidationCacheStats::default(),
roa_cache_object_meta: Vec::new(),
};
let audit = build_publication_point_audit_from_snapshot(
&issuer,
pp.source,
Some("rsync"),
Some("rsync_only_ok"),
Some(123),
Some("none"),
&pp.snapshot,
&[],
&objects,
&[],
);
assert_eq!(audit.objects[0].kind, AuditObjectKind::Manifest);
assert_eq!(audit.repo_sync_source.as_deref(), Some("rsync"));
assert_eq!(audit.repo_sync_phase.as_deref(), Some("rsync_only_ok"));
assert_eq!(audit.repo_sync_duration_ms, Some(123));
assert_eq!(audit.repo_sync_error.as_deref(), Some("none"));
assert_eq!(audit.repo_terminal_state, "fresh");
let crl = audit
.objects
.iter()
.find(|e| e.rsync_uri.ends_with("bad.crl"))
.expect("crl entry");
assert!(matches!(crl.result, AuditObjectResult::Error));
let roa = audit
.objects
.iter()
.find(|e| e.rsync_uri.ends_with("x.roa"))
.expect("roa entry");
assert!(matches!(roa.result, AuditObjectResult::Ok));
// Smoke that time fields are populated from pack.
assert!(audit.verified_at_rfc3339_utc.contains('T'));
assert!(audit.this_update_rfc3339_utc.contains('T'));
assert!(audit.next_update_rfc3339_utc.contains('T'));
let _ = now;
}
#[test]
fn discover_children_with_router_certificate_records_ok_audit_and_no_child() {
let g = generate_router_cert_with_variant("ec-p256", true);
let pack = dummy_pack_with_files(vec![
PackFile::from_bytes_compute_sha256(
"rsync://example.test/repo/issuer/issuer.crl",
g.issuer_crl_der.clone(),
),
PackFile::from_bytes_compute_sha256(
"rsync://example.test/repo/issuer/router.cer",
g.router_der.clone(),
),
]);
let issuer_ca = ResourceCertificate::decode_der(&g.issuer_ca_der).expect("decode issuer");
let issuer = CaInstanceHandle {
depth: 0,
tal_id: "test-tal".to_string(),
parent_manifest_rsync_uri: None,
ca_certificate: CaCertificateRef::inline_der(g.issuer_ca_der.clone()),
ca_certificate_rsync_uri: Some("rsync://example.test/repo/issuer/issuer.cer".to_string()),
effective_ip_resources: issuer_ca.tbs.extensions.ip_resources.clone(),
effective_as_resources: issuer_ca.tbs.extensions.as_resources.clone(),
rsync_base_uri: "rsync://example.test/repo/issuer/".to_string(),
manifest_rsync_uri: "rsync://example.test/repo/issuer/issuer.mft".to_string(),
publication_point_rsync_uri: "rsync://example.test/repo/issuer/".to_string(),
rrdp_notification_uri: None,
};
let out = discover_children_from_fresh_snapshot_with_audit(
&issuer,
&pack,
time::OffsetDateTime::now_utc(),
None,
)
.expect("discover router cert");
assert!(out.children.is_empty());
assert_eq!(out.audits.len(), 1);
assert!(matches!(out.audits[0].result, AuditObjectResult::Ok));
assert!(
out.audits[0]
.detail
.as_deref()
.unwrap_or("")
.contains("validated BGPsec router certificate")
);
}
#[test]
fn discover_children_with_non_router_ee_certificate_records_skipped_audit() {
let g = generate_router_cert_with_variant("ec-p256", false);
let pack = dummy_pack_with_files(vec![
PackFile::from_bytes_compute_sha256(
"rsync://example.test/repo/issuer/issuer.crl",
g.issuer_crl_der.clone(),
),
PackFile::from_bytes_compute_sha256(
"rsync://example.test/repo/issuer/router-no-eku.cer",
g.router_der.clone(),
),
]);
let issuer_ca = ResourceCertificate::decode_der(&g.issuer_ca_der).expect("decode issuer");
let issuer = CaInstanceHandle {
depth: 0,
tal_id: "test-tal".to_string(),
parent_manifest_rsync_uri: None,
ca_certificate: CaCertificateRef::inline_der(g.issuer_ca_der.clone()),
ca_certificate_rsync_uri: Some("rsync://example.test/repo/issuer/issuer.cer".to_string()),
effective_ip_resources: issuer_ca.tbs.extensions.ip_resources.clone(),
effective_as_resources: issuer_ca.tbs.extensions.as_resources.clone(),
rsync_base_uri: "rsync://example.test/repo/issuer/".to_string(),
manifest_rsync_uri: "rsync://example.test/repo/issuer/issuer.mft".to_string(),
publication_point_rsync_uri: "rsync://example.test/repo/issuer/".to_string(),
rrdp_notification_uri: None,
};
let out = discover_children_from_fresh_snapshot_with_audit(
&issuer,
&pack,
time::OffsetDateTime::now_utc(),
None,
)
.expect("discover non-router cert");
assert!(out.children.is_empty());
assert_eq!(out.audits.len(), 1);
assert!(matches!(out.audits[0].result, AuditObjectResult::Skipped));
assert!(
out.audits[0]
.detail
.as_deref()
.unwrap_or("")
.contains("not a CA resource certificate or BGPsec router certificate")
);
}
#[test]
fn discover_children_with_invalid_router_certificate_records_error_audit() {
let g = generate_router_cert_with_variant("ec-p384", true);
let pack = dummy_pack_with_files(vec![
PackFile::from_bytes_compute_sha256(
"rsync://example.test/repo/issuer/issuer.crl",
g.issuer_crl_der.clone(),
),
PackFile::from_bytes_compute_sha256(
"rsync://example.test/repo/issuer/router-invalid.cer",
g.router_der.clone(),
),
]);
let issuer_ca = ResourceCertificate::decode_der(&g.issuer_ca_der).expect("decode issuer");
let issuer = CaInstanceHandle {
depth: 0,
tal_id: "test-tal".to_string(),
parent_manifest_rsync_uri: None,
ca_certificate: CaCertificateRef::inline_der(g.issuer_ca_der.clone()),
ca_certificate_rsync_uri: Some("rsync://example.test/repo/issuer/issuer.cer".to_string()),
effective_ip_resources: issuer_ca.tbs.extensions.ip_resources.clone(),
effective_as_resources: issuer_ca.tbs.extensions.as_resources.clone(),
rsync_base_uri: "rsync://example.test/repo/issuer/".to_string(),
manifest_rsync_uri: "rsync://example.test/repo/issuer/issuer.mft".to_string(),
publication_point_rsync_uri: "rsync://example.test/repo/issuer/".to_string(),
rrdp_notification_uri: None,
};
let out = discover_children_from_fresh_snapshot_with_audit(
&issuer,
&pack,
time::OffsetDateTime::now_utc(),
None,
)
.expect("discover invalid router cert");
assert!(out.children.is_empty());
assert_eq!(out.audits.len(), 1);
assert!(matches!(out.audits[0].result, AuditObjectResult::Error));
assert!(
out.audits[0]
.detail
.as_deref()
.unwrap_or("")
.contains("router certificate validation failed")
);
}
#[test]
fn discover_children_with_audit_records_decode_error_for_corrupt_cer() {
let g = generate_chain_and_crl();
let pack = dummy_pack_with_files(vec![
PackFile::from_bytes_compute_sha256(
"rsync://example.test/repo/issuer/issuer.crl",
g.issuer_crl_der.clone(),
),
PackFile::from_bytes_compute_sha256(
"rsync://example.test/repo/issuer/corrupt.cer",
vec![0u8],
),
]);
let issuer_ca = ResourceCertificate::decode_der(&g.issuer_ca_der).expect("decode issuer");
let issuer = CaInstanceHandle {
depth: 0,
tal_id: "test-tal".to_string(),
parent_manifest_rsync_uri: None,
ca_certificate: CaCertificateRef::inline_der(g.issuer_ca_der.clone()),
ca_certificate_rsync_uri: None,
effective_ip_resources: issuer_ca.tbs.extensions.ip_resources.clone(),
effective_as_resources: issuer_ca.tbs.extensions.as_resources.clone(),
rsync_base_uri: "rsync://example.test/repo/issuer/".to_string(),
manifest_rsync_uri: "rsync://example.test/repo/issuer/issuer.mft".to_string(),
publication_point_rsync_uri: "rsync://example.test/repo/issuer/".to_string(),
rrdp_notification_uri: None,
};
let now = time::OffsetDateTime::now_utc();
let out = discover_children_from_fresh_snapshot_with_audit(&issuer, &pack, now, None)
.expect("discover children");
assert!(out.children.is_empty());
assert_eq!(out.audits.len(), 1);
assert!(matches!(out.audits[0].result, AuditObjectResult::Error));
}
#[test]
fn select_issuer_crl_uri_for_child_covers_missing_and_not_found_paths() {
let g = generate_chain_and_crl();
let child = ResourceCertificate::decode_der(&g.child_ca_der).expect("decode child cert");
let empty: std::collections::HashMap<String, CachedIssuerCrl> =
std::collections::HashMap::new();
let err = select_issuer_crl_uri_for_child(&child, &empty).unwrap_err();
assert!(err.contains("no CRL available"), "{err}");
let ta_der = std::fs::read(
std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("tests/fixtures/ta/apnic-ta.cer"),
)
.expect("read TA fixture");
let ta = ResourceCertificate::decode_der(&ta_der).expect("decode TA fixture");
let mut cache = std::collections::HashMap::new();
cache.insert(
"rsync://example.test/repo/issuer/issuer.crl".to_string(),
CachedIssuerCrl::Pending {
bytes: g.issuer_crl_der.clone(),
sha256_hex: None,
},
);
let err = select_issuer_crl_uri_for_child(&ta, &cache).unwrap_err();
assert!(err.contains("CRLDistributionPoints missing"), "{err}");
let mut wrong = std::collections::HashMap::new();
wrong.insert(
"rsync://example.test/repo/issuer/other.crl".to_string(),
CachedIssuerCrl::Pending {
bytes: g.issuer_crl_der,
sha256_hex: None,
},
);
let err = select_issuer_crl_uri_for_child(&child, &wrong).unwrap_err();
assert!(
err.contains("not found in publication point snapshot"),
"{err}"
);
}
#[test]
fn ensure_issuer_crl_verified_promotes_pending_cache_entry() {
let g = generate_chain_and_crl();
let mut cache = std::collections::HashMap::new();
let crl_uri = "rsync://example.test/repo/issuer/issuer.crl".to_string();
cache.insert(
crl_uri.clone(),
CachedIssuerCrl::Pending {
bytes: g.issuer_crl_der.clone(),
sha256_hex: None,
},
);
let first = ensure_issuer_crl_verified(&crl_uri, &mut cache, &g.issuer_ca_der)
.expect("verify pending CRL");
assert!(first.revoked_serials.is_empty());
assert!(matches!(cache.get(&crl_uri), Some(CachedIssuerCrl::Ok(_))));
let second = ensure_issuer_crl_verified(&crl_uri, &mut cache, &g.issuer_ca_der)
.expect("reuse verified CRL");
assert!(second.revoked_serials.is_empty());
}
#[test]
fn discover_children_with_invalid_issuer_der_records_error_audit() {
let g = generate_chain_and_crl();
let pack = dummy_pack_with_files(vec![
PackFile::from_bytes_compute_sha256(
"rsync://example.test/repo/issuer/issuer.crl",
g.issuer_crl_der.clone(),
),
PackFile::from_bytes_compute_sha256(
"rsync://example.test/repo/issuer/child.cer",
g.child_ca_der.clone(),
),
]);
let issuer = CaInstanceHandle {
depth: 0,
tal_id: "test-tal".to_string(),
parent_manifest_rsync_uri: None,
ca_certificate: CaCertificateRef::inline_der(vec![0u8]),
ca_certificate_rsync_uri: None,
effective_ip_resources: None,
effective_as_resources: None,
rsync_base_uri: "rsync://example.test/repo/issuer/".to_string(),
manifest_rsync_uri: "rsync://example.test/repo/issuer/issuer.mft".to_string(),
publication_point_rsync_uri: "rsync://example.test/repo/issuer/".to_string(),
rrdp_notification_uri: None,
};
let out = discover_children_from_fresh_snapshot_with_audit(
&issuer,
&pack,
time::OffsetDateTime::now_utc(),
None,
)
.expect("discover children with invalid issuer der");
assert!(out.children.is_empty());
assert_eq!(out.audits.len(), 1);
assert!(matches!(out.audits[0].result, AuditObjectResult::Error));
assert!(
out.audits[0]
.detail
.as_deref()
.unwrap_or("")
.contains("issuer CA decode failed")
);
}
#[test]
fn project_current_instance_vcir_reuses_local_outputs_and_restores_children() {
let now = time::OffsetDateTime::now_utc();
let g = generate_chain_and_crl();
let child_cert_hash = sha256_hex(&g.child_ca_der);
let vcir = sample_vcir_for_projection(now, &child_cert_hash);
let store_dir = tempfile::tempdir().expect("store dir");
let main_db = store_dir.path().join("work-db");
let repo_bytes_db = store_dir.path().join("repo-bytes.db");
let store = RocksStore::open_with_external_repo_bytes(&main_db, &repo_bytes_db)
.expect("open rocksdb with external repo bytes");
store
.put_blob_bytes_batch(&[(child_cert_hash.clone(), g.child_ca_der.clone())])
.expect("put child cert repo bytes");
assert!(
store
.get_raw_by_hash_entry(&child_cert_hash)
.expect("lookup child raw_by_hash")
.is_none(),
"child cert restoration should not require raw_by_hash entries"
);
let ca = CaInstanceHandle {
depth: 0,
tal_id: "test-tal".to_string(),
parent_manifest_rsync_uri: None,
ca_certificate: CaCertificateRef::inline_der(Vec::new()),
ca_certificate_rsync_uri: None,
effective_ip_resources: None,
effective_as_resources: None,
rsync_base_uri: "rsync://example.test/repo/issuer/".to_string(),
manifest_rsync_uri: vcir.manifest_rsync_uri.clone(),
publication_point_rsync_uri: "rsync://example.test/repo/issuer/".to_string(),
rrdp_notification_uri: Some("https://example.test/notify.xml".to_string()),
};
let policy = Policy::default();
put_vcir_for_failed_fetch_reuse(&store, &ca, &policy, &vcir);
let projection = project_current_instance_vcir_on_failed_fetch(
&store,
&ca,
&ManifestFreshError::RepoSyncFailed {
detail: "synthetic".to_string(),
},
&policy,
now,
)
.expect("project vcir");
assert_eq!(
projection.source,
PublicationPointSource::VcirCurrentInstance
);
assert_eq!(projection.objects.vrps.len(), 1);
assert_eq!(projection.objects.aspas.len(), 1);
assert_eq!(projection.objects.router_keys.len(), 1);
assert_eq!(projection.discovered_children.len(), 1);
assert_eq!(
projection.discovered_children[0].handle.manifest_rsync_uri,
"rsync://example.test/repo/child/child.mft"
);
assert_eq!(
projection.ccr_manifest_projection.as_ref(),
Some(&vcir.ccr_manifest_projection)
);
assert!(
projection.snapshot.is_none(),
"current-instance reuse should not reconstruct a byte-backed snapshot"
);
assert!(
!projection
.warnings
.iter()
.any(|warning| warning.message.contains("manifest failed fetch")),
"successful current-instance reuse should not duplicate the fresh fetch error"
);
assert!(
!projection
.warnings
.iter()
.any(|warning| warning.message.contains("using latest validated result")),
"successful current-instance reuse should be tracked by source, not warning"
);
assert!(
!projection
.warnings
.iter()
.any(|warning| warning.message.contains("manifest raw bytes missing")),
"successful current-instance reuse should not load repo bytes for audit reconstruction"
);
assert!(
!projection
.warnings
.iter()
.any(|warning| warning.message.contains("child certificate bytes missing")),
"child discovery restoration should read child certs from repo bytes"
);
}
#[test]
fn project_current_instance_vcir_does_not_reuse_without_identity() {
let now = time::OffsetDateTime::now_utc();
let child_cert_hash = sha256_hex(b"child-cert");
let vcir = sample_vcir_for_projection(now, &child_cert_hash);
let store_dir = tempfile::tempdir().expect("store dir");
let store = RocksStore::open(store_dir.path()).expect("open rocksdb");
store.put_vcir(&vcir).expect("put legacy-style VCIR");
let ca = sample_ca_for_failed_fetch_reuse(&vcir);
let projection = project_current_instance_vcir_on_failed_fetch(
&store,
&ca,
&ManifestFreshError::RepoSyncFailed {
detail: "synthetic".to_string(),
},
&Policy::default(),
now,
)
.expect("project VCIR");
assert_eq!(
projection.source,
PublicationPointSource::FailedFetchNoCache
);
assert!(projection.objects.vrps.is_empty());
assert!(projection.discovered_children.is_empty());
assert!(
projection
.warnings
.iter()
.any(|warning| warning.message.contains("reuse identity is missing"))
);
}
#[test]
fn project_current_instance_vcir_does_not_reuse_when_ta_context_changes() {
let now = time::OffsetDateTime::now_utc();
let child_cert_hash = sha256_hex(b"child-cert");
let vcir = sample_vcir_for_projection(now, &child_cert_hash);
let store_dir = tempfile::tempdir().expect("store dir");
let store = RocksStore::open(store_dir.path()).expect("open rocksdb");
let policy = Policy::default();
let original_ca = sample_ca_for_failed_fetch_reuse(&vcir);
put_vcir_for_failed_fetch_reuse(&store, &original_ca, &policy, &vcir);
let mut changed_ca = original_ca;
changed_ca.tal_id = "different-tal".to_string();
let projection = project_current_instance_vcir_on_failed_fetch(
&store,
&changed_ca,
&ManifestFreshError::RepoSyncFailed {
detail: "synthetic".to_string(),
},
&policy,
now,
)
.expect("project VCIR");
assert_eq!(
projection.source,
PublicationPointSource::FailedFetchNoCache
);
assert!(projection.objects.vrps.is_empty());
assert!(projection.discovered_children.is_empty());
assert!(
projection
.warnings
.iter()
.any(|warning| warning.message.contains("identity does not match"))
);
}
#[test]
fn project_current_instance_vcir_returns_no_output_when_instance_gate_expired() {
let now = time::OffsetDateTime::now_utc();
let child_cert_hash = sha256_hex(b"child-cert");
let mut vcir = sample_vcir_for_projection(now, &child_cert_hash);
let this_update = PackTime::from_utc_offset_datetime(now - time::Duration::minutes(2));
let expired = PackTime::from_utc_offset_datetime(now - time::Duration::minutes(1));
vcir.validated_manifest_meta.validated_manifest_this_update = this_update;
vcir.validated_manifest_meta.validated_manifest_next_update = expired.clone();
vcir.instance_gate.manifest_next_update = expired.clone();
vcir.instance_gate.current_crl_next_update = expired.clone();
vcir.instance_gate.instance_effective_until = expired;
let store_dir = tempfile::tempdir().expect("store dir");
let store = RocksStore::open(store_dir.path()).expect("open rocksdb");
store.put_vcir(&vcir).expect("put vcir");
let ca = CaInstanceHandle {
depth: 0,
tal_id: "test-tal".to_string(),
parent_manifest_rsync_uri: None,
ca_certificate: CaCertificateRef::inline_der(Vec::new()),
ca_certificate_rsync_uri: None,
effective_ip_resources: None,
effective_as_resources: None,
rsync_base_uri: "rsync://example.test/repo/issuer/".to_string(),
manifest_rsync_uri: vcir.manifest_rsync_uri.clone(),
publication_point_rsync_uri: "rsync://example.test/repo/issuer/".to_string(),
rrdp_notification_uri: None,
};
let projection = project_current_instance_vcir_on_failed_fetch(
&store,
&ca,
&ManifestFreshError::RepoSyncFailed {
detail: "synthetic".to_string(),
},
&Policy::default(),
now,
)
.expect("project vcir");
assert_eq!(
projection.source,
PublicationPointSource::FailedFetchNoCache
);
assert!(projection.ccr_manifest_projection.is_none());
assert!(projection.objects.vrps.is_empty());
assert!(projection.objects.aspas.is_empty());
assert!(projection.discovered_children.is_empty());
}
#[test]
fn project_current_instance_vcir_keeps_real_fresh_validation_warning() {
let now = time::OffsetDateTime::now_utc();
let child_cert_hash = sha256_hex(b"child-cert");
let vcir = sample_vcir_for_projection(now, &child_cert_hash);
let store_dir = tempfile::tempdir().expect("store dir");
let main_db = store_dir.path().join("work-db");
let repo_bytes_db = store_dir.path().join("repo-bytes.db");
let store = RocksStore::open_with_external_repo_bytes(&main_db, &repo_bytes_db)
.expect("open rocksdb with external repo bytes");
store
.put_blob_bytes_batch(&[(child_cert_hash, b"child-cert".to_vec())])
.expect("put child cert repo bytes");
let ca = CaInstanceHandle {
depth: 0,
tal_id: "test-tal".to_string(),
parent_manifest_rsync_uri: None,
ca_certificate: CaCertificateRef::inline_der(Vec::new()),
ca_certificate_rsync_uri: None,
effective_ip_resources: None,
effective_as_resources: None,
rsync_base_uri: "rsync://example.test/repo/issuer/".to_string(),
manifest_rsync_uri: vcir.manifest_rsync_uri.clone(),
publication_point_rsync_uri: "rsync://example.test/repo/issuer/".to_string(),
rrdp_notification_uri: None,
};
let policy = Policy::default();
put_vcir_for_failed_fetch_reuse(&store, &ca, &policy, &vcir);
let projection = project_current_instance_vcir_on_failed_fetch(
&store,
&ca,
&ManifestFreshError::HashMismatch {
rsync_uri: "rsync://example.test/repo/issuer/a.roa".to_string(),
},
&policy,
now,
)
.expect("project vcir");
assert_eq!(
projection.source,
PublicationPointSource::VcirCurrentInstance
);
assert!(
projection
.warnings
.iter()
.any(|warning| { warning.message.contains("manifest file hash mismatch") })
);
assert!(
!projection
.warnings
.iter()
.any(|warning| warning.message.contains("using latest validated result")),
"successful current-instance reuse should not emit bookkeeping warnings"
);
}
#[test]
fn project_current_instance_vcir_returns_no_output_when_latest_result_missing() {
let now = time::OffsetDateTime::now_utc();
let store_dir = tempfile::tempdir().expect("store dir");
let store = RocksStore::open(store_dir.path()).expect("open rocksdb");
let ca = CaInstanceHandle {
depth: 0,
tal_id: "test-tal".to_string(),
parent_manifest_rsync_uri: None,
ca_certificate: CaCertificateRef::inline_der(Vec::new()),
ca_certificate_rsync_uri: None,
effective_ip_resources: None,
effective_as_resources: None,
rsync_base_uri: "rsync://example.test/repo/issuer/".to_string(),
manifest_rsync_uri: "rsync://example.test/repo/issuer/issuer.mft".to_string(),
publication_point_rsync_uri: "rsync://example.test/repo/issuer/".to_string(),
rrdp_notification_uri: None,
};
let projection = project_current_instance_vcir_on_failed_fetch(
&store,
&ca,
&ManifestFreshError::RepoSyncFailed {
detail: "synthetic".to_string(),
},
&Policy::default(),
now,
)
.expect("project without cached vcir");
assert_eq!(
projection.source,
PublicationPointSource::FailedFetchNoCache
);
assert!(projection.vcir.is_none());
assert!(projection.ccr_manifest_projection.is_none());
assert!(projection.snapshot.is_none());
assert!(projection.objects.audit.is_empty());
assert!(projection.discovered_children.is_empty());
assert!(projection.warnings.iter().any(|warning| {
warning
.message
.contains("no latest validated result for current CA instance")
}));
}
#[test]
fn project_current_instance_vcir_returns_no_output_when_latest_result_is_ineligible() {
let now = time::OffsetDateTime::now_utc();
let child_cert_hash = sha256_hex(b"child-cert");
let mut vcir = sample_vcir_for_projection(now, &child_cert_hash);
vcir.audit_summary.failed_fetch_eligible = false;
let store_dir = tempfile::tempdir().expect("store dir");
let store = RocksStore::open(store_dir.path()).expect("open rocksdb");
store.put_vcir(&vcir).expect("put vcir");
let ca = CaInstanceHandle {
depth: 0,
tal_id: "test-tal".to_string(),
parent_manifest_rsync_uri: None,
ca_certificate: CaCertificateRef::inline_der(Vec::new()),
ca_certificate_rsync_uri: None,
effective_ip_resources: None,
effective_as_resources: None,
rsync_base_uri: "rsync://example.test/repo/issuer/".to_string(),
manifest_rsync_uri: vcir.manifest_rsync_uri.clone(),
publication_point_rsync_uri: "rsync://example.test/repo/issuer/".to_string(),
rrdp_notification_uri: None,
};
let projection = project_current_instance_vcir_on_failed_fetch(
&store,
&ca,
&ManifestFreshError::RepoSyncFailed {
detail: "synthetic".to_string(),
},
&Policy::default(),
now,
)
.expect("project ineligible vcir");
assert_eq!(
projection.source,
PublicationPointSource::FailedFetchNoCache
);
assert!(projection.vcir.is_some());
assert!(projection.ccr_manifest_projection.is_none());
assert!(projection.snapshot.is_none());
assert!(projection.discovered_children.is_empty());
assert!(projection.warnings.iter().any(|warning| {
warning
.message
.contains("latest VCIR is not marked failed-fetch eligible")
}));
}
#[test]
fn project_current_instance_vcir_rejects_mismatched_ccr_projection_uri() {
let now = time::OffsetDateTime::now_utc();
let child_cert_hash = sha256_hex(b"child-cert");
let mut vcir = sample_vcir_for_projection(now, &child_cert_hash);
vcir.ccr_manifest_projection.manifest_rsync_uri =
"rsync://example.test/repo/issuer/other.mft".to_string();
let store_dir = tempfile::tempdir().expect("store dir");
let store = RocksStore::open(store_dir.path()).expect("open rocksdb");
let ca = CaInstanceHandle {
depth: 0,
tal_id: "test-tal".to_string(),
parent_manifest_rsync_uri: None,
ca_certificate: CaCertificateRef::inline_der(Vec::new()),
ca_certificate_rsync_uri: None,
effective_ip_resources: None,
effective_as_resources: None,
rsync_base_uri: "rsync://example.test/repo/issuer/".to_string(),
manifest_rsync_uri: vcir.manifest_rsync_uri.clone(),
publication_point_rsync_uri: "rsync://example.test/repo/issuer/".to_string(),
rrdp_notification_uri: None,
};
let policy = Policy::default();
put_vcir_for_failed_fetch_reuse(&store, &ca, &policy, &vcir);
let err = project_current_instance_vcir_on_failed_fetch(
&store,
&ca,
&ManifestFreshError::RepoSyncFailed {
detail: "synthetic".to_string(),
},
&policy,
now,
)
.unwrap_err();
assert!(
err.contains("vcir CCR manifest projection URI mismatch"),
"{err}"
);
}
#[test]
fn fresh_and_reuse_paths_produce_equivalent_ccr_manifest_projection() {
let (pack, issuer_ca_der, validation_time) = cernet_publication_point_snapshot_for_vcir_tests();
let ca = CaInstanceHandle {
depth: 0,
tal_id: "test-tal".to_string(),
parent_manifest_rsync_uri: None,
ca_certificate: CaCertificateRef::inline_der(issuer_ca_der),
ca_certificate_rsync_uri: Some("rsync://rpki.apnic.net/repository/B527EF581D6611E2BB468F7C72FD1FF2/BfycW4hQb3wNP4YsiJW-1n6fjro.cer".to_string()),
effective_ip_resources: None,
effective_as_resources: None,
rsync_base_uri: pack.publication_point_rsync_uri.clone(),
manifest_rsync_uri: pack.manifest_rsync_uri.clone(),
publication_point_rsync_uri: pack.publication_point_rsync_uri.clone(),
rrdp_notification_uri: None,
};
let child_discovery =
discover_children_from_fresh_snapshot_with_audit(&ca, &pack, validation_time, None)
.expect("discover children");
let mut objects = empty_objects_output();
let store_dir = tempfile::tempdir().expect("store dir");
let store = RocksStore::open(store_dir.path()).expect("open rocksdb");
let (fresh_vcir, _timing) = build_vcir_from_fresh_result_with_timing(
&store,
&ca,
&pack,
&mut objects,
&[],
&child_discovery.audits,
&child_discovery.children,
validation_time,
)
.expect("build fresh vcir");
let reuse_projection = reuse_ccr_manifest_projection_from_vcir(&ca, &fresh_vcir)
.expect("reuse projection from vcir");
assert_eq!(fresh_vcir.ccr_manifest_projection, reuse_projection);
}
#[test]
fn append_ccr_manifest_projection_from_reuse_requires_projection_for_current_instance() {
let store_dir = tempfile::tempdir().expect("store dir");
let store = RocksStore::open(store_dir.path()).expect("open rocksdb");
let policy = Policy::default();
let runner = sample_runner_with_ccr_accumulator(&store, &policy);
let err = runner
.append_ccr_manifest_projection_from_reuse(&VcirReuseProjection {
source: PublicationPointSource::VcirCurrentInstance,
vcir: None,
ccr_manifest_projection: None,
snapshot: None,
objects: empty_objects_output(),
child_audits: Vec::new(),
discovered_children: Vec::new(),
warnings: Vec::new(),
})
.unwrap_err();
assert!(err.contains("missing CCR manifest projection"), "{err}");
assert_eq!(
runner
.ccr_accumulator_snapshot()
.expect("ccr accumulator snapshot")
.manifest_count(),
0
);
}
#[test]
fn append_ccr_manifest_projection_from_reuse_skips_failed_fetch_no_cache() {
let store_dir = tempfile::tempdir().expect("store dir");
let store = RocksStore::open(store_dir.path()).expect("open rocksdb");
let policy = Policy::default();
let runner = sample_runner_with_ccr_accumulator(&store, &policy);
runner
.append_ccr_manifest_projection_from_reuse(&VcirReuseProjection {
source: PublicationPointSource::FailedFetchNoCache,
vcir: None,
ccr_manifest_projection: None,
snapshot: None,
objects: empty_objects_output(),
child_audits: Vec::new(),
discovered_children: Vec::new(),
warnings: Vec::new(),
})
.expect("failed-fetch no-cache should not append");
assert_eq!(
runner
.ccr_accumulator_snapshot()
.expect("ccr accumulator snapshot")
.manifest_count(),
0
);
}
#[test]
fn parse_snapshot_time_value_reports_invalid_timestamp() {
let err = parse_snapshot_time_value(&PackTime {
rfc3339_utc: "not-a-time".to_string(),
})
.unwrap_err();
assert!(err.contains("invalid RFC3339 time 'not-a-time'"), "{err}");
}
#[test]
fn runner_roa_validation_cache_uses_projection_not_full_vcir_fallback() {
let now = time::OffsetDateTime::now_utc();
let child_cert_hash = sha256_hex(b"child-cert");
let mut vcir = sample_vcir_for_projection(now, &child_cert_hash);
vcir.local_outputs
.retain(|output| output.source_object_type != VcirSourceObjectType::Roa);
vcir.summary.local_vrp_count = 0;
vcir.summary.local_aspa_count = 1;
vcir.summary.local_router_key_count = 1;
let store_dir = tempfile::tempdir().expect("store dir");
let store = RocksStore::open(store_dir.path()).expect("open rocksdb");
store.put_vcir(&vcir).expect("put vcir without projection");
assert!(
store
.get_vcir(&vcir.manifest_rsync_uri)
.expect("get vcir")
.is_some()
);
assert!(
store
.get_roa_cache_projection(&vcir.manifest_rsync_uri)
.expect("get projection")
.is_none()
);
let timing = crate::analysis::timing::TimingHandle::new(crate::analysis::timing::TimingMeta {
recorded_at_utc_rfc3339: "2026-06-07T00:00:00Z".to_string(),
validation_time_utc_rfc3339: "2026-06-07T00:00:00Z".to_string(),
tal_url: None,
db_path: None,
});
let policy = Policy::default();
let runner = Rpkiv1PublicationPointRunner {
store: &store,
policy: &policy,
http_fetcher: &NeverHttpFetcher,
rsync_fetcher: &FailingRsyncFetcher,
validation_time: now,
timing: Some(timing.clone()),
download_log: None,
replay_archive_index: None,
replay_delta_index: None,
rrdp_dedup: false,
rrdp_repo_cache: Mutex::new(HashMap::new()),
rsync_dedup: false,
rsync_repo_cache: Mutex::new(HashMap::new()),
current_repo_index: None,
repo_sync_runtime: None,
parallel_phase2_config: None,
parallel_roa_worker_pool: None,
ccr_accumulator: None,
persist_vcir: true,
enable_roa_validation_cache: true,
enable_child_certificate_validation_cache: false,
publication_point_cache_observe_only: false,
enable_publication_point_validation_cache: false,
};
assert!(
runner
.roa_validation_cache_view_for_fresh_point(&vcir.manifest_rsync_uri)
.is_none()
);
let dir = tempfile::tempdir().expect("timing dir");
let path = dir.path().join("timing.json");
timing.write_json(&path, 10).expect("write timing");
let report: serde_json::Value =
serde_json::from_slice(&std::fs::read(path).expect("read timing")).expect("parse timing");
assert_eq!(
report["counts"]["roa_validation_cache_projection_missing_publication_points"],
1
);
assert!(
report["phases"]["roa_validation_cache_projection_load_total"]["count"]
.as_u64()
.unwrap_or_default()
>= 1
);
}
#[test]
fn build_objects_output_from_vcir_tracks_expired_and_invalid_cached_outputs() {
let now = time::OffsetDateTime::now_utc();
let child_cert_hash = sha256_hex(b"child-cert");
let mut vcir = sample_vcir_for_projection(now, &child_cert_hash);
let bad_time_uri = "rsync://example.test/repo/issuer/bad-time.roa".to_string();
let expired_uri = "rsync://example.test/repo/issuer/expired.asa".to_string();
let bad_json_uri = "rsync://example.test/repo/issuer/bad-json.roa".to_string();
let bad_prefix_uri = "rsync://example.test/repo/issuer/bad-prefix.roa".to_string();
let bad_aspa_uri = "rsync://example.test/repo/issuer/bad-aspa.asa".to_string();
for (uri, kind) in [
(bad_time_uri.clone(), VcirArtifactKind::Roa),
(expired_uri.clone(), VcirArtifactKind::Aspa),
(bad_json_uri.clone(), VcirArtifactKind::Roa),
(bad_prefix_uri.clone(), VcirArtifactKind::Roa),
(bad_aspa_uri.clone(), VcirArtifactKind::Aspa),
] {
vcir.related_artifacts.push(VcirRelatedArtifact {
artifact_role: VcirArtifactRole::SignedObject,
artifact_kind: kind,
uri: Some(uri.clone()),
sha256: sha256_hex(uri.as_bytes()),
object_type: Some(
match kind {
VcirArtifactKind::Aspa => "aspa",
_ => "roa",
}
.to_string(),
),
validation_status: VcirArtifactValidationStatus::Accepted,
reject_reason: None,
});
}
vcir.local_outputs.push(VcirLocalOutput {
output_type: VcirOutputType::Vrp,
item_effective_until: PackTime {
rfc3339_utc: "bad-time-value".to_string(),
},
source_object_uri: bad_time_uri.clone(),
source_object_type: VcirSourceObjectType::Roa,
source_object_hash: sha256_32(b"bad-time-src"),
source_ee_cert_hash: sha256_32(b"bad-time-ee"),
payload: VcirLocalOutputPayload::Vrp {
asn: 64496,
afi: RoaAfi::Ipv4,
prefix_len: 24,
addr: ipv4_addr([203, 0, 113, 0]),
max_length: 24,
},
rule_hash: sha256_32(b"bad-time-rule"),
});
vcir.local_outputs.push(VcirLocalOutput {
output_type: VcirOutputType::Aspa,
item_effective_until: PackTime::from_utc_offset_datetime(now - time::Duration::minutes(1)),
source_object_uri: expired_uri.clone(),
source_object_type: VcirSourceObjectType::Aspa,
source_object_hash: sha256_32(b"expired-src"),
source_ee_cert_hash: sha256_32(b"expired-ee"),
payload: VcirLocalOutputPayload::Aspa {
customer_as_id: 64500,
provider_as_ids: vec![64501],
},
rule_hash: sha256_32(b"expired-rule"),
});
vcir.local_outputs.push(VcirLocalOutput {
output_type: VcirOutputType::Vrp,
item_effective_until: PackTime::from_utc_offset_datetime(now + time::Duration::minutes(5)),
source_object_uri: bad_json_uri.clone(),
source_object_type: VcirSourceObjectType::Roa,
source_object_hash: sha256_32(b"bad-json-src"),
source_ee_cert_hash: sha256_32(b"bad-json-ee"),
payload: VcirLocalOutputPayload::Aspa {
customer_as_id: 64510,
provider_as_ids: vec![64511],
},
rule_hash: sha256_32(b"bad-json-rule"),
});
vcir.local_outputs.push(VcirLocalOutput {
output_type: VcirOutputType::Vrp,
item_effective_until: PackTime::from_utc_offset_datetime(now + time::Duration::minutes(5)),
source_object_uri: bad_prefix_uri.clone(),
source_object_type: VcirSourceObjectType::Roa,
source_object_hash: sha256_32(b"bad-prefix-src"),
source_ee_cert_hash: sha256_32(b"bad-prefix-ee"),
payload: VcirLocalOutputPayload::Aspa {
customer_as_id: 64512,
provider_as_ids: vec![64513],
},
rule_hash: sha256_32(b"bad-prefix-rule"),
});
vcir.local_outputs.push(VcirLocalOutput {
output_type: VcirOutputType::Aspa,
item_effective_until: PackTime::from_utc_offset_datetime(now + time::Duration::minutes(5)),
source_object_uri: bad_aspa_uri.clone(),
source_object_type: VcirSourceObjectType::Aspa,
source_object_hash: sha256_32(b"bad-aspa-src"),
source_ee_cert_hash: sha256_32(b"bad-aspa-ee"),
payload: VcirLocalOutputPayload::Vrp {
asn: 64520,
afi: RoaAfi::Ipv4,
prefix_len: 24,
addr: ipv4_addr([198, 51, 100, 0]),
max_length: 24,
},
rule_hash: sha256_32(b"bad-aspa-rule"),
});
let mut warnings = Vec::new();
let output = build_objects_output_from_vcir(&vcir, now, &mut warnings);
assert_eq!(output.vrps.len(), 1);
assert_eq!(output.aspas.len(), 1);
assert_eq!(output.stats.roa_total, 4);
assert_eq!(output.stats.roa_ok, 1);
assert_eq!(output.stats.aspa_total, 3);
assert_eq!(output.stats.aspa_ok, 1);
assert!(warnings.iter().any(|warning| {
warning
.message
.contains("cached local output has invalid item_effective_until")
}));
assert!(warnings.iter().any(|warning| {
warning
.message
.contains("cached ROA local output parse failed")
}));
assert!(warnings.iter().any(|warning| {
warning
.message
.contains("cached ASPA local output parse failed")
}));
assert!(output.audit.iter().any(|entry| {
entry.rsync_uri == expired_uri
&& matches!(entry.result, AuditObjectResult::Skipped)
&& entry.detail.as_deref() == Some("skipped: cached local output expired")
}));
assert!(output.audit.iter().any(|entry| {
entry.rsync_uri == bad_time_uri && matches!(entry.result, AuditObjectResult::Error)
}));
assert!(output.audit.iter().any(|entry| {
entry.rsync_uri == bad_prefix_uri
&& matches!(entry.result, AuditObjectResult::Error)
&& entry
.detail
.as_deref()
.unwrap_or("")
.contains("cached ROA local output parse failed")
}));
}
#[test]
fn build_publication_point_audit_from_vcir_uses_vcir_metadata_and_overlays_child_and_object_audits()
{
let now = time::OffsetDateTime::now_utc();
let child_cert_hash = sha256_hex(b"child-cert");
let mut vcir = sample_vcir_for_projection(now, &child_cert_hash);
vcir.related_artifacts
.retain(|artifact| artifact.artifact_role != VcirArtifactRole::Manifest);
let ca = CaInstanceHandle {
depth: 0,
tal_id: "test-tal".to_string(),
parent_manifest_rsync_uri: None,
ca_certificate: CaCertificateRef::inline_der(Vec::new()),
ca_certificate_rsync_uri: None,
effective_ip_resources: None,
effective_as_resources: None,
rsync_base_uri: "rsync://example.test/repo/issuer/".to_string(),
manifest_rsync_uri: vcir.manifest_rsync_uri.clone(),
publication_point_rsync_uri: "rsync://example.test/repo/issuer/".to_string(),
rrdp_notification_uri: Some("https://example.test/notify.xml".to_string()),
};
let runner_warnings = vec![Warning::new("runner warning")];
let objects = crate::validation::objects::ObjectsOutput {
vrps: Vec::new(),
aspas: Vec::new(),
router_keys: Vec::new(),
local_outputs_cache: Vec::new(),
warnings: vec![Warning::new("objects warning")],
stats: crate::validation::objects::ObjectsStats::default(),
audit: vec![ObjectAuditEntry {
rsync_uri: "rsync://example.test/repo/issuer/a.roa".to_string(),
sha256_hex: sha256_hex(b"override-roa"),
kind: AuditObjectKind::Roa,
result: AuditObjectResult::Error,
detail: Some("overridden from object audit".to_string()),
}],
roa_cache_stats: crate::validation::objects::RoaValidationCacheStats::default(),
roa_cache_object_meta: Vec::new(),
};
let child_audits = vec![ObjectAuditEntry {
rsync_uri: vcir.child_entries[0].child_cert_rsync_uri.clone(),
sha256_hex: vcir.child_entries[0].child_cert_hash.clone(),
kind: AuditObjectKind::Certificate,
result: AuditObjectResult::Ok,
detail: Some("restored child CA instance from VCIR".to_string()),
}];
let audit = build_publication_point_audit_from_vcir(
&ca,
PublicationPointSource::VcirCurrentInstance,
Some("rsync"),
Some("rrdp_failed_rsync_failed"),
Some(456),
Some("rsync failed"),
Some(&vcir),
None,
&runner_warnings,
&objects,
&child_audits,
&[],
);
assert_eq!(audit.source, "vcir_current_instance");
assert_eq!(audit.repo_sync_source.as_deref(), Some("rsync"));
assert_eq!(
audit.repo_sync_phase.as_deref(),
Some("rrdp_failed_rsync_failed")
);
assert_eq!(audit.repo_sync_duration_ms, Some(456));
assert_eq!(audit.repo_sync_error.as_deref(), Some("rsync failed"));
assert_eq!(audit.repo_terminal_state, "fallback_current_instance");
assert_eq!(audit.objects[0].rsync_uri, vcir.current_manifest_rsync_uri);
assert_eq!(audit.objects[0].kind, AuditObjectKind::Manifest);
assert_eq!(
audit.this_update_rfc3339_utc,
vcir.validated_manifest_meta
.validated_manifest_this_update
.rfc3339_utc
);
assert_eq!(
audit.next_update_rfc3339_utc,
vcir.validated_manifest_meta
.validated_manifest_next_update
.rfc3339_utc
);
assert_eq!(
audit.verified_at_rfc3339_utc,
vcir.last_successful_validation_time.rfc3339_utc
);
assert_eq!(audit.warnings.len(), 2);
assert!(audit.objects.iter().any(|entry| {
entry.rsync_uri == "rsync://example.test/repo/issuer/a.roa"
&& matches!(entry.result, AuditObjectResult::Error)
&& entry.detail.as_deref() == Some("overridden from object audit")
}));
assert!(audit.objects.iter().any(|entry| {
entry.rsync_uri == vcir.child_entries[0].child_cert_rsync_uri
&& matches!(entry.result, AuditObjectResult::Ok)
}));
}
#[test]
fn build_publication_point_audit_from_vcir_restores_reject_reason_with_legacy_fallback() {
let now = time::OffsetDateTime::now_utc();
let child_cert_hash = sha256_hex(b"child-cert");
let mut vcir = sample_vcir_for_projection(now, &child_cert_hash);
vcir.related_artifacts.push(VcirRelatedArtifact {
artifact_role: VcirArtifactRole::SignedObject,
artifact_kind: VcirArtifactKind::Roa,
uri: Some("rsync://example.test/repo/issuer/rejected-with-reason.roa".to_string()),
sha256: sha256_hex(b"rejected-with-reason"),
object_type: Some("roa".to_string()),
validation_status: VcirArtifactValidationStatus::Rejected,
reject_reason: Some("EE certificate path validation failed: test".to_string()),
});
vcir.related_artifacts.push(VcirRelatedArtifact {
artifact_role: VcirArtifactRole::SignedObject,
artifact_kind: VcirArtifactKind::Roa,
uri: Some("rsync://example.test/repo/issuer/rejected-legacy.roa".to_string()),
sha256: sha256_hex(b"rejected-legacy"),
object_type: Some("roa".to_string()),
validation_status: VcirArtifactValidationStatus::Rejected,
reject_reason: None,
});
let ca = CaInstanceHandle {
depth: 0,
tal_id: "test-tal".to_string(),
parent_manifest_rsync_uri: None,
ca_certificate: CaCertificateRef::inline_der(Vec::new()),
ca_certificate_rsync_uri: None,
effective_ip_resources: None,
effective_as_resources: None,
rsync_base_uri: "rsync://example.test/repo/issuer/".to_string(),
manifest_rsync_uri: vcir.manifest_rsync_uri.clone(),
publication_point_rsync_uri: "rsync://example.test/repo/issuer/".to_string(),
rrdp_notification_uri: None,
};
let objects = crate::validation::objects::ObjectsOutput {
vrps: Vec::new(),
aspas: Vec::new(),
router_keys: Vec::new(),
local_outputs_cache: Vec::new(),
warnings: Vec::new(),
stats: crate::validation::objects::ObjectsStats::default(),
audit: Vec::new(),
roa_cache_stats: crate::validation::objects::RoaValidationCacheStats::default(),
roa_cache_object_meta: Vec::new(),
};
let audit = build_publication_point_audit_from_vcir(
&ca,
PublicationPointSource::VcirCurrentInstance,
None,
None,
None,
None,
Some(&vcir),
None,
&[],
&objects,
&[],
&[],
);
assert!(audit.objects.iter().any(|entry| {
entry.rsync_uri == "rsync://example.test/repo/issuer/rejected-with-reason.roa"
&& matches!(entry.result, AuditObjectResult::Error)
&& entry.detail.as_deref() == Some("EE certificate path validation failed: test")
}));
assert!(audit.objects.iter().any(|entry| {
entry.rsync_uri == "rsync://example.test/repo/issuer/rejected-legacy.roa"
&& matches!(entry.result, AuditObjectResult::Error)
&& entry.detail.as_deref() == Some(CACHED_REJECT_REASON_NOT_RECORDED)
}));
assert!(
audit.objects.iter().all(|entry| {
!matches!(entry.result, AuditObjectResult::Ok) || entry.detail.is_none()
})
);
}
#[test]
fn build_publication_point_audit_from_pp_cache_projection_restores_reject_reason() {
let now = time::OffsetDateTime::now_utc();
let child_cert_hash = sha256_hex(b"child-cert");
let mut vcir = sample_vcir_for_projection(now, &child_cert_hash);
vcir.related_artifacts.push(VcirRelatedArtifact {
artifact_role: VcirArtifactRole::SignedObject,
artifact_kind: VcirArtifactKind::Roa,
uri: Some("rsync://example.test/repo/issuer/rejected-with-reason.roa".to_string()),
sha256: sha256_hex(b"rejected-with-reason"),
object_type: Some("roa".to_string()),
validation_status: VcirArtifactValidationStatus::Rejected,
reject_reason: Some("EE certificate path validation failed: test".to_string()),
});
vcir.related_artifacts.push(VcirRelatedArtifact {
artifact_role: VcirArtifactRole::SignedObject,
artifact_kind: VcirArtifactKind::Roa,
uri: Some("rsync://example.test/repo/issuer/rejected-legacy.roa".to_string()),
sha256: sha256_hex(b"rejected-legacy"),
object_type: Some("roa".to_string()),
validation_status: VcirArtifactValidationStatus::Rejected,
reject_reason: None,
});
let projection = PublicationPointCacheProjection::from_vcir_with_context(
&vcir,
"rsync://example.test/repo/issuer/".to_string(),
None,
[0x11; 32],
[0x22; 32],
[0x33; 32],
[0x44; 32],
[0x55; 32],
)
.expect("build publication point projection");
let ca = CaInstanceHandle {
depth: 0,
tal_id: "test-tal".to_string(),
parent_manifest_rsync_uri: None,
ca_certificate: CaCertificateRef::inline_der(Vec::new()),
ca_certificate_rsync_uri: None,
effective_ip_resources: None,
effective_as_resources: None,
rsync_base_uri: "rsync://example.test/repo/issuer/".to_string(),
manifest_rsync_uri: vcir.manifest_rsync_uri.clone(),
publication_point_rsync_uri: "rsync://example.test/repo/issuer/".to_string(),
rrdp_notification_uri: None,
};
let objects = crate::validation::objects::ObjectsOutput {
vrps: Vec::new(),
aspas: Vec::new(),
router_keys: Vec::new(),
local_outputs_cache: Vec::new(),
warnings: Vec::new(),
stats: crate::validation::objects::ObjectsStats::default(),
audit: Vec::new(),
roa_cache_stats: crate::validation::objects::RoaValidationCacheStats::default(),
roa_cache_object_meta: Vec::new(),
};
let audit = build_publication_point_audit_from_publication_point_cache_projection(
&ca,
PublicationPointSource::PublicationPointCache,
None,
None,
None,
None,
&projection,
now,
&[],
&objects,
&[],
);
assert!(audit.objects.iter().any(|entry| {
entry.rsync_uri == "rsync://example.test/repo/issuer/rejected-with-reason.roa"
&& matches!(entry.result, AuditObjectResult::Error)
&& entry.detail.as_deref() == Some("EE certificate path validation failed: test")
}));
assert!(audit.objects.iter().any(|entry| {
entry.rsync_uri == "rsync://example.test/repo/issuer/rejected-legacy.roa"
&& matches!(entry.result, AuditObjectResult::Error)
&& entry.detail.as_deref() == Some(CACHED_REJECT_REASON_NOT_RECORDED)
}));
}
#[test]
fn build_publication_point_audit_from_vcir_failed_no_cache_keeps_current_reject_only() {
let now = time::OffsetDateTime::now_utc();
let child_cert_hash = sha256_hex(b"child-cert");
let vcir = sample_vcir_for_projection(now, &child_cert_hash);
let ca = CaInstanceHandle {
depth: 0,
tal_id: "test-tal".to_string(),
parent_manifest_rsync_uri: None,
ca_certificate: CaCertificateRef::inline_der(Vec::new()),
ca_certificate_rsync_uri: None,
effective_ip_resources: None,
effective_as_resources: None,
rsync_base_uri: "rsync://example.test/repo/issuer/".to_string(),
manifest_rsync_uri: vcir.manifest_rsync_uri.clone(),
publication_point_rsync_uri: "rsync://example.test/repo/issuer/".to_string(),
rrdp_notification_uri: Some("https://example.test/notify.xml".to_string()),
};
let objects = crate::validation::objects::ObjectsOutput {
vrps: Vec::new(),
aspas: Vec::new(),
router_keys: Vec::new(),
local_outputs_cache: Vec::new(),
warnings: Vec::new(),
stats: crate::validation::objects::ObjectsStats::default(),
audit: vec![ObjectAuditEntry {
rsync_uri: vcir.current_manifest_rsync_uri.clone(),
sha256_hex: sha256_hex(b"current-manifest"),
kind: AuditObjectKind::Manifest,
result: AuditObjectResult::Error,
detail: Some("manifest is not valid at validation_time".to_string()),
}],
roa_cache_stats: crate::validation::objects::RoaValidationCacheStats::default(),
roa_cache_object_meta: Vec::new(),
};
let audit = build_publication_point_audit_from_vcir(
&ca,
PublicationPointSource::FailedFetchNoCache,
Some("rsync"),
Some("rsync_only_ok"),
Some(123),
None,
Some(&vcir),
None,
&[Warning::new("latest VCIR instance_gate expired")],
&objects,
&[],
&[],
);
assert_eq!(audit.source, "failed_fetch_no_cache");
assert_eq!(audit.repo_terminal_state, "failed_no_cache");
assert_eq!(
audit.this_update_rfc3339_utc,
vcir.validated_manifest_meta
.validated_manifest_this_update
.rfc3339_utc
);
assert_eq!(audit.objects.len(), 1);
assert_eq!(audit.objects[0].rsync_uri, vcir.current_manifest_rsync_uri);
assert!(matches!(audit.objects[0].result, AuditObjectResult::Error));
assert!(
!audit
.objects
.iter()
.any(|entry| entry.rsync_uri == "rsync://example.test/repo/issuer/a.roa"),
"failed-no-cache must not expand old VCIR related artifacts into current-run audit",
);
assert!(
!audit
.objects
.iter()
.any(|entry| entry.rsync_uri == "rsync://example.test/repo/issuer/issuer.crl"),
"failed-no-cache must not expose old CRL as current-run CIR input",
);
}
#[test]
fn rejected_manifest_audit_entry_for_failed_fetch_uses_current_repo_hash() {
let store_dir = tempfile::tempdir().expect("store dir");
let store = RocksStore::open(store_dir.path()).expect("open rocksdb");
let policy = Policy::default();
let runner = sample_runner_with_ccr_accumulator(&store, &policy);
let manifest_uri = "rsync://example.test/repo/issuer/issuer.mft";
let manifest_hash = sha256_hex(b"manifest-bytes");
store
.put_blob_bytes_batch(&[(manifest_hash.clone(), b"manifest-bytes".to_vec())])
.expect("put manifest bytes");
store
.put_repository_view_entry(&crate::storage::RepositoryViewEntry {
rsync_uri: manifest_uri.to_string(),
current_hash: Some(manifest_hash.clone()),
repository_source: Some("rsync://example.test/repo/issuer/".to_string()),
object_type: Some("mft".to_string()),
state: crate::storage::RepositoryViewState::Present,
})
.expect("put repository view");
let ca = CaInstanceHandle {
depth: 0,
tal_id: "test-tal".to_string(),
parent_manifest_rsync_uri: None,
ca_certificate: CaCertificateRef::inline_der(Vec::new()),
ca_certificate_rsync_uri: None,
effective_ip_resources: None,
effective_as_resources: None,
rsync_base_uri: "rsync://example.test/repo/issuer/".to_string(),
manifest_rsync_uri: manifest_uri.to_string(),
publication_point_rsync_uri: "rsync://example.test/repo/issuer/".to_string(),
rrdp_notification_uri: None,
};
let entry = runner
.rejected_manifest_audit_entry_for_failed_fetch(
&ca,
&ManifestFreshError::StaleOrEarly {
this_update_rfc3339_utc: "2026-05-27T08:37:07Z".to_string(),
next_update_rfc3339_utc: "2026-05-28T10:01:07Z".to_string(),
validation_time_rfc3339_utc: "2026-05-28T10:11:00Z".to_string(),
},
)
.expect("rejected manifest audit entry");
assert_eq!(entry.rsync_uri, manifest_uri);
assert_eq!(entry.sha256_hex, manifest_hash);
assert_eq!(entry.kind, AuditObjectKind::Manifest);
assert_eq!(entry.result, AuditObjectResult::Error);
assert!(
entry
.detail
.as_deref()
.unwrap_or("")
.contains("manifest is not valid at validation_time")
);
}
#[test]
fn build_publication_point_audit_from_vcir_without_cached_inputs_returns_empty_listing() {
let ca = CaInstanceHandle {
depth: 0,
tal_id: "test-tal".to_string(),
parent_manifest_rsync_uri: None,
ca_certificate: CaCertificateRef::inline_der(Vec::new()),
ca_certificate_rsync_uri: None,
effective_ip_resources: None,
effective_as_resources: None,
rsync_base_uri: "rsync://example.test/repo/issuer/".to_string(),
manifest_rsync_uri: "rsync://example.test/repo/issuer/issuer.mft".to_string(),
publication_point_rsync_uri: "rsync://example.test/repo/issuer/".to_string(),
rrdp_notification_uri: None,
};
let audit = build_publication_point_audit_from_vcir(
&ca,
PublicationPointSource::FailedFetchNoCache,
Some("rsync"),
Some("rsync_only_failed"),
Some(789),
Some("load from network failed, fallback to cache"),
None,
None,
&[Warning::new("runner warning")],
&crate::validation::objects::ObjectsOutput {
vrps: Vec::new(),
aspas: Vec::new(),
router_keys: Vec::new(),
local_outputs_cache: Vec::new(),
warnings: vec![Warning::new("object warning")],
stats: crate::validation::objects::ObjectsStats::default(),
audit: Vec::new(),
roa_cache_stats: crate::validation::objects::RoaValidationCacheStats::default(),
roa_cache_object_meta: Vec::new(),
},
&[],
&[],
);
assert_eq!(audit.source, "failed_fetch_no_cache");
assert_eq!(audit.repo_sync_source.as_deref(), Some("rsync"));
assert_eq!(audit.repo_sync_phase.as_deref(), Some("rsync_only_failed"));
assert_eq!(audit.repo_sync_duration_ms, Some(789));
assert_eq!(
audit.repo_sync_error.as_deref(),
Some("load from network failed, fallback to cache")
);
assert_eq!(audit.repo_terminal_state, "failed_no_cache");
assert!(audit.this_update_rfc3339_utc.is_empty());
assert!(audit.next_update_rfc3339_utc.is_empty());
assert!(audit.verified_at_rfc3339_utc.is_empty());
assert_eq!(audit.warnings.len(), 2);
assert!(audit.objects.is_empty());
}
#[test]
fn effective_repo_sync_duration_uses_runtime_duration_for_failures() {
assert_eq!(effective_repo_sync_duration_ms(0, Some(12), false), 12);
assert_eq!(effective_repo_sync_duration_ms(5, Some(12), false), 12);
assert_eq!(effective_repo_sync_duration_ms(20, Some(12), false), 20);
assert_eq!(effective_repo_sync_duration_ms(5, None, false), 5);
assert_eq!(effective_repo_sync_duration_ms(5, Some(12), true), 5);
}
#[test]
fn reconstruct_snapshot_from_vcir_reports_missing_manifest_and_related_raw_bytes() {
let now = time::OffsetDateTime::now_utc();
let child_cert_hash = sha256_hex(b"child-cert");
let mut vcir = sample_vcir_for_projection(now, &child_cert_hash);
let dup_uri = "rsync://example.test/repo/issuer/dup.roa".to_string();
vcir.related_artifacts.push(VcirRelatedArtifact {
artifact_role: VcirArtifactRole::SignedObject,
artifact_kind: VcirArtifactKind::Roa,
uri: Some(dup_uri.clone()),
sha256: sha256_hex(b"dup-roa-1"),
object_type: Some("roa".to_string()),
validation_status: VcirArtifactValidationStatus::Accepted,
reject_reason: None,
});
vcir.related_artifacts.push(VcirRelatedArtifact {
artifact_role: VcirArtifactRole::SignedObject,
artifact_kind: VcirArtifactKind::Roa,
uri: Some(dup_uri.clone()),
sha256: sha256_hex(b"dup-roa-2"),
object_type: Some("roa".to_string()),
validation_status: VcirArtifactValidationStatus::Accepted,
reject_reason: None,
});
vcir.related_artifacts.push(VcirRelatedArtifact {
artifact_role: VcirArtifactRole::IssuerCert,
artifact_kind: VcirArtifactKind::Cer,
uri: Some("rsync://example.test/repo/issuer/issuer.cer".to_string()),
sha256: sha256_hex(b"issuer-cert"),
object_type: Some("cer".to_string()),
validation_status: VcirArtifactValidationStatus::Accepted,
reject_reason: None,
});
let ca = CaInstanceHandle {
depth: 0,
tal_id: "test-tal".to_string(),
parent_manifest_rsync_uri: None,
ca_certificate: CaCertificateRef::inline_der(Vec::new()),
ca_certificate_rsync_uri: None,
effective_ip_resources: None,
effective_as_resources: None,
rsync_base_uri: "rsync://example.test/repo/issuer/".to_string(),
manifest_rsync_uri: vcir.manifest_rsync_uri.clone(),
publication_point_rsync_uri: "rsync://example.test/repo/issuer/".to_string(),
rrdp_notification_uri: None,
};
let store_dir = tempfile::tempdir().expect("store dir");
let store = RocksStore::open(store_dir.path()).expect("open rocksdb");
let mut warnings = Vec::new();
assert!(reconstruct_snapshot_from_vcir(&store, &ca, &vcir, &mut warnings).is_none());
assert!(warnings.iter().any(|warning| {
warning
.message
.contains("manifest raw bytes missing for VCIR audit reconstruction")
}));
let manifest_bytes = b"manifest-bytes".to_vec();
let current_crl_bytes = b"current-crl-bytes".to_vec();
let child_bytes = b"child-cert".to_vec();
let roa_bytes = b"roa-bytes".to_vec();
for (bytes, uri, object_type) in [
(
manifest_bytes.clone(),
Some(vcir.manifest_rsync_uri.clone()),
Some("mft".to_string()),
),
(
current_crl_bytes,
Some(vcir.current_crl_rsync_uri.clone()),
Some("crl".to_string()),
),
(
child_bytes,
Some(vcir.child_entries[0].child_cert_rsync_uri.clone()),
Some("cer".to_string()),
),
(
roa_bytes,
Some("rsync://example.test/repo/issuer/a.roa".to_string()),
Some("roa".to_string()),
),
] {
let mut entry = RawByHashEntry::from_bytes(sha256_hex(&bytes), bytes);
if let Some(uri) = uri {
entry.origin_uris.push(uri);
}
entry.object_type = object_type;
entry.encoding = Some("der".to_string());
store.put_raw_by_hash_entry(&entry).expect("put raw entry");
}
warnings.clear();
let pack = reconstruct_snapshot_from_vcir(&store, &ca, &vcir, &mut warnings)
.expect("reconstruct pack with partial related artifacts");
assert_eq!(pack.manifest_bytes, manifest_bytes);
assert_eq!(pack.files.len(), 3, "crl + child cert + roa only");
assert!(
pack.files
.iter()
.any(|file| file.rsync_uri.ends_with("issuer.crl"))
);
assert!(
pack.files
.iter()
.any(|file| file.rsync_uri.ends_with("child.cer"))
);
assert!(
pack.files
.iter()
.any(|file| file.rsync_uri.ends_with("a.roa"))
);
assert!(
!pack
.files
.iter()
.any(|file| file.rsync_uri.ends_with("issuer.cer"))
);
assert!(warnings.iter().any(|warning| {
warning
.message
.contains("related artifact raw bytes missing for VCIR audit reconstruction")
}));
}
#[test]
fn reconstruct_snapshot_from_vcir_reads_repo_bytes_without_raw_entries() {
let now = time::OffsetDateTime::now_utc();
let child_cert_hash = sha256_hex(b"child-cert");
let vcir = sample_vcir_for_projection(now, &child_cert_hash);
let ca = CaInstanceHandle {
depth: 0,
tal_id: "test-tal".to_string(),
parent_manifest_rsync_uri: None,
ca_certificate: CaCertificateRef::inline_der(Vec::new()),
ca_certificate_rsync_uri: None,
effective_ip_resources: None,
effective_as_resources: None,
rsync_base_uri: "rsync://example.test/repo/issuer/".to_string(),
manifest_rsync_uri: vcir.manifest_rsync_uri.clone(),
publication_point_rsync_uri: "rsync://example.test/repo/issuer/".to_string(),
rrdp_notification_uri: None,
};
let store_dir = tempfile::tempdir().expect("store dir");
let main_db = store_dir.path().join("work-db");
let repo_bytes_db = store_dir.path().join("repo-bytes.db");
let store = RocksStore::open_with_external_repo_bytes(&main_db, &repo_bytes_db)
.expect("open rocksdb with external repo bytes");
let repo_blobs = [
b"manifest-bytes".to_vec(),
b"current-crl-bytes".to_vec(),
b"child-cert".to_vec(),
b"roa-bytes".to_vec(),
b"aspa-bytes".to_vec(),
]
.into_iter()
.map(|bytes| (sha256_hex(&bytes), bytes))
.collect::<Vec<_>>();
store
.put_blob_bytes_batch(&repo_blobs)
.expect("put external repo bytes");
let manifest_hash = vcir
.related_artifacts
.iter()
.find(|artifact| artifact.artifact_role == VcirArtifactRole::Manifest)
.expect("manifest artifact")
.sha256
.clone();
assert!(
store
.get_raw_by_hash_entry(&manifest_hash)
.expect("raw manifest lookup")
.is_none(),
"repo object bytes must not require raw_by_hash entries"
);
let mut warnings = Vec::new();
let pack = reconstruct_snapshot_from_vcir(&store, &ca, &vcir, &mut warnings)
.expect("reconstruct pack from external repo bytes");
assert_eq!(pack.manifest_bytes, b"manifest-bytes".to_vec());
assert_eq!(pack.files.len(), 4, "crl + child cert + roa + aspa");
assert!(
warnings.iter().all(|warning| {
!warning
.message
.contains("raw bytes missing for VCIR audit reconstruction")
}),
"external repo bytes should satisfy VCIR audit reconstruction without raw warnings"
);
}
#[test]
fn runner_dedup_paths_execute_with_timing_enabled() {
let fixture_dir = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.join("tests/fixtures/repository/rpki.cernet.net/repo/cernet/0");
let rsync_base_uri = "rsync://rpki.cernet.net/repo/cernet/0/".to_string();
let manifest_file = "05FC9C5B88506F7C0D3F862C8895BED67E9F8EBA.mft";
let manifest_rsync_uri = format!("{rsync_base_uri}{manifest_file}");
let fixture_manifest_bytes =
std::fs::read(fixture_dir.join(manifest_file)).expect("read manifest fixture");
let fixture_manifest =
crate::data_model::manifest::ManifestObject::decode_der(&fixture_manifest_bytes)
.expect("decode manifest fixture");
let validation_time = fixture_manifest.manifest.this_update + time::Duration::seconds(60);
let store_dir = tempfile::tempdir().expect("store dir");
let store = RocksStore::open(store_dir.path()).expect("open rocksdb");
let issuer_ca_der = std::fs::read(
std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR")).join(
"tests/fixtures/repository/rpki.apnic.net/repository/B527EF581D6611E2BB468F7C72FD1FF2/BfycW4hQb3wNP4YsiJW-1n6fjro.cer",
),
)
.expect("read issuer ca fixture");
let issuer_ca = ResourceCertificate::decode_der(&issuer_ca_der).expect("decode issuer ca");
let handle = CaInstanceHandle {
depth: 0,
tal_id: "test-tal".to_string(),
parent_manifest_rsync_uri: None,
ca_certificate: CaCertificateRef::inline_der(issuer_ca_der),
ca_certificate_rsync_uri: Some("rsync://rpki.apnic.net/repository/B527EF581D6611E2BB468F7C72FD1FF2/BfycW4hQb3wNP4YsiJW-1n6fjro.cer".to_string()),
effective_ip_resources: issuer_ca.tbs.extensions.ip_resources.clone(),
effective_as_resources: issuer_ca.tbs.extensions.as_resources.clone(),
rsync_base_uri: rsync_base_uri.clone(),
manifest_rsync_uri: manifest_rsync_uri.clone(),
publication_point_rsync_uri: rsync_base_uri.clone(),
rrdp_notification_uri: Some("https://example.test/notification.xml".to_string()),
};
let timing = crate::analysis::timing::TimingHandle::new(crate::analysis::timing::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: None,
db_path: None,
});
let policy_rrdp = Policy::default();
let runner_rrdp = Rpkiv1PublicationPointRunner {
store: &store,
policy: &policy_rrdp,
http_fetcher: &NeverHttpFetcher,
rsync_fetcher: &LocalDirRsyncFetcher::new(&fixture_dir),
validation_time,
timing: Some(timing.clone()),
download_log: None,
replay_archive_index: None,
replay_delta_index: None,
rrdp_dedup: true,
rrdp_repo_cache: Mutex::new(HashMap::new()),
rsync_dedup: false,
rsync_repo_cache: Mutex::new(HashMap::new()),
current_repo_index: None,
repo_sync_runtime: None,
parallel_phase2_config: None,
parallel_roa_worker_pool: None,
ccr_accumulator: None,
persist_vcir: true,
enable_roa_validation_cache: false,
enable_child_certificate_validation_cache: false,
publication_point_cache_observe_only: false,
enable_publication_point_validation_cache: false,
};
let first = runner_rrdp
.run_publication_point(&handle)
.expect("rrdp fallback to rsync");
assert_eq!(first.source, PublicationPointSource::Fresh);
let second = runner_rrdp
.run_publication_point(&handle)
.expect("rrdp dedup skip");
assert_eq!(second.source, PublicationPointSource::Fresh);
let policy_rsync = Policy {
sync_preference: crate::policy::SyncPreference::RsyncOnly,
..Policy::default()
};
let runner_rsync = Rpkiv1PublicationPointRunner {
store: &store,
policy: &policy_rsync,
http_fetcher: &NeverHttpFetcher,
rsync_fetcher: &LocalDirRsyncFetcher::new(&fixture_dir),
validation_time,
timing: Some(timing),
download_log: None,
replay_archive_index: None,
replay_delta_index: None,
rrdp_dedup: false,
rrdp_repo_cache: Mutex::new(HashMap::new()),
rsync_dedup: true,
rsync_repo_cache: Mutex::new(HashMap::new()),
current_repo_index: None,
repo_sync_runtime: None,
parallel_phase2_config: None,
parallel_roa_worker_pool: None,
ccr_accumulator: None,
persist_vcir: true,
enable_roa_validation_cache: false,
enable_child_certificate_validation_cache: false,
publication_point_cache_observe_only: false,
enable_publication_point_validation_cache: false,
};
let third = runner_rsync
.run_publication_point(&handle)
.expect("rsync first run");
assert_eq!(third.source, PublicationPointSource::Fresh);
let fourth = runner_rsync
.run_publication_point(&handle)
.expect("rsync dedup run");
assert_eq!(fourth.source, PublicationPointSource::Fresh);
assert_eq!(
crate::fetch::rsync::normalize_rsync_base_uri("rsync://example.test/repo"),
"rsync://example.test/repo/"
);
}
#[derive(Debug, serde::Deserialize)]
#[serde(rename_all = "camelCase")]
struct RipeRootFinalizeFixtureEvent {
event_type: String,
validation_time: String,
pp_manifest_uri: Option<String>,
object_uri: Option<String>,
sha256: Option<String>,
object_type: Option<String>,
result: Option<String>,
reason: Option<String>,
}
#[derive(Debug)]
struct RipeRootFinalizeObject {
uri: String,
sha256_hex: String,
object_type: String,
result: String,
reason: Option<String>,
}
struct RipeRootFinalizeFixture {
manifest_uri: String,
publication_point_uri: String,
validation_time: time::OffsetDateTime,
manifest_sha256_hex: String,
objects: Vec<RipeRootFinalizeObject>,
}
fn load_ripe_root_finalize_fixture(
fixture_root: &std::path::Path,
run_id: &str,
) -> RipeRootFinalizeFixture {
let events_path = fixture_root
.join(format!("run_{run_id}"))
.join("ripe-root-events.jsonl");
let file = std::fs::File::open(&events_path)
.unwrap_or_else(|e| panic!("open fixture events {} failed: {e}", events_path.display()));
let reader = std::io::BufReader::new(file);
let mut manifest_uri = None;
let mut validation_time = None;
let mut manifest_sha256_hex = None;
let mut objects = Vec::new();
for line in std::io::BufRead::lines(reader) {
let line = line.expect("read fixture event line");
let event: RipeRootFinalizeFixtureEvent =
serde_json::from_str(&line).expect("decode fixture event");
if event.event_type == "publication_point" {
manifest_uri = event.pp_manifest_uri;
validation_time = Some(
time::OffsetDateTime::parse(
&event.validation_time,
&time::format_description::well_known::Rfc3339,
)
.expect("parse validation_time"),
);
continue;
}
if event.event_type != "object" {
continue;
}
let uri = event.object_uri.expect("fixture object uri");
let sha256_hex = event.sha256.expect("fixture object sha256");
let object_type = event.object_type.expect("fixture object type");
let result = event.result.expect("fixture object result");
if object_type == "manifest" {
manifest_sha256_hex = Some(sha256_hex.clone());
}
objects.push(RipeRootFinalizeObject {
uri,
sha256_hex,
object_type,
result,
reason: event.reason,
});
}
let manifest_uri = manifest_uri.expect("fixture publication_point event");
let publication_point_uri = manifest_uri
.rsplit_once('/')
.map(|(parent, _)| format!("{parent}/"))
.expect("manifest uri parent");
RipeRootFinalizeFixture {
manifest_uri,
publication_point_uri,
validation_time: validation_time.expect("fixture validation_time"),
manifest_sha256_hex: manifest_sha256_hex.expect("fixture manifest object"),
objects,
}
}
fn pack_file_from_fixture_object(
object: &RipeRootFinalizeObject,
repo_bytes: &Arc<crate::blob_store::ExternalRepoBytesDb>,
) -> PackFile {
PackFile::from_lazy_repo_bytes(
object.uri.clone(),
object.sha256_hex.clone(),
sha256_hex_to_32(&object.sha256_hex),
repo_bytes.clone(),
)
}
fn child_audit_from_fixture_object(object: &RipeRootFinalizeObject) -> ObjectAuditEntry {
ObjectAuditEntry {
rsync_uri: object.uri.clone(),
sha256_hex: object.sha256_hex.clone(),
kind: AuditObjectKind::Certificate,
result: match object.result.as_str() {
"ok" => AuditObjectResult::Ok,
"skipped" => AuditObjectResult::Skipped,
_ => AuditObjectResult::Error,
},
detail: object.reason.clone(),
}
}
fn discovered_child_from_fixture_object(
issuer: &CaInstanceHandle,
object: &RipeRootFinalizeObject,
child_entry_projection: Option<DiscoveredChildEntryProjection>,
) -> DiscoveredChildCaInstance {
let stem = object
.uri
.rsplit_once('/')
.map(|(_, file)| file.trim_end_matches(".cer"))
.unwrap_or("child");
let child_publication_point = format!("{}synthetic-child-{stem}/", issuer.rsync_base_uri);
let child_manifest = format!("{child_publication_point}child.mft");
DiscoveredChildCaInstance {
handle: CaInstanceHandle {
depth: issuer.depth + 1,
tal_id: issuer.tal_id.clone(),
parent_manifest_rsync_uri: Some(issuer.manifest_rsync_uri.clone()),
ca_certificate: CaCertificateRef::repo_bytes(object.sha256_hex.clone()),
ca_certificate_rsync_uri: Some(object.uri.clone()),
effective_ip_resources: None,
effective_as_resources: None,
rsync_base_uri: child_publication_point.clone(),
manifest_rsync_uri: child_manifest,
publication_point_rsync_uri: child_publication_point,
rrdp_notification_uri: issuer.rrdp_notification_uri.clone(),
},
discovered_from: crate::audit::DiscoveredFrom {
parent_manifest_rsync_uri: issuer.manifest_rsync_uri.clone(),
child_ca_certificate_rsync_uri: object.uri.clone(),
child_ca_certificate_sha256_hex: object.sha256_hex.clone(),
},
child_entry_projection,
}
}
fn child_entry_projection_from_fixture_object(
store: &RocksStore,
object: &RipeRootFinalizeObject,
) -> DiscoveredChildEntryProjection {
let child_der = store
.get_blob_bytes(&object.sha256_hex)
.expect("load child certificate bytes for projection")
.expect("child certificate bytes exist for projection");
let child_cert =
ResourceCertificate::decode_der(&child_der).expect("decode child certificate projection");
let child_ski = child_cert
.tbs
.extensions
.subject_key_identifier
.as_ref()
.expect("child certificate projection SKI");
DiscoveredChildEntryProjection {
child_ski: hex::encode(child_ski),
}
}
struct RipeRootChildEntryProfile {
count: usize,
load_der_nanos: u128,
decode_cert_nanos: u128,
build_entry_nanos: u128,
}
fn profile_ripe_root_child_entry_build(
store: &RocksStore,
discovered_children: &[DiscoveredChildCaInstance],
validation_time: time::OffsetDateTime,
) -> Result<RipeRootChildEntryProfile, String> {
let mut out = Vec::with_capacity(discovered_children.len());
let mut load_der_nanos = 0;
let mut decode_cert_nanos = 0;
let mut build_entry_nanos = 0;
for child in discovered_children {
let load_started = std::time::Instant::now();
let child_der = child.handle.ca_certificate_der(store)?;
load_der_nanos += load_started.elapsed().as_nanos();
let decode_started = std::time::Instant::now();
let child_cert = ResourceCertificate::decode_der(child_der.as_ref())
.map_err(|e| format!("decode child certificate for VCIR failed: {e}"))?;
decode_cert_nanos += decode_started.elapsed().as_nanos();
let build_started = std::time::Instant::now();
let child_ski = child_cert
.tbs
.extensions
.subject_key_identifier
.as_ref()
.ok_or_else(|| "child certificate missing SubjectKeyIdentifier".to_string())?;
out.push(VcirChildEntry {
child_manifest_rsync_uri: child.handle.manifest_rsync_uri.clone(),
child_cert_rsync_uri: child.discovered_from.child_ca_certificate_rsync_uri.clone(),
child_cert_hash: child
.discovered_from
.child_ca_certificate_sha256_hex
.clone(),
child_ski: hex::encode(child_ski),
child_rsync_base_uri: child.handle.rsync_base_uri.clone(),
child_publication_point_rsync_uri: child.handle.publication_point_rsync_uri.clone(),
child_rrdp_notification_uri: child.handle.rrdp_notification_uri.clone(),
child_effective_ip_resources: child.handle.effective_ip_resources.clone(),
child_effective_as_resources: child.handle.effective_as_resources.clone(),
accepted_at_validation_time: PackTime::from_utc_offset_datetime(validation_time),
});
build_entry_nanos += build_started.elapsed().as_nanos();
}
Ok(RipeRootChildEntryProfile {
count: out.len(),
load_der_nanos,
decode_cert_nanos,
build_entry_nanos,
})
}
#[test]
#[ignore = "manual performance repro: requires target/ripe-root-finalize-repro repo-bytes fixture"]
fn ripe_root_finalize_repro_from_remote_fixture() {
let fixture_root = std::env::var("RPKI_RIPE_ROOT_FINALIZE_FIXTURE")
.map(std::path::PathBuf::from)
.unwrap_or_else(|_| {
std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.join("target/ripe-root-finalize-repro")
});
let run_id = std::env::var("RPKI_RIPE_ROOT_FINALIZE_RUN").unwrap_or_else(|_| "0262".into());
assert!(
fixture_root.exists(),
"fixture root missing: {}; expected copied remote fixture",
fixture_root.display()
);
let fixture = load_ripe_root_finalize_fixture(&fixture_root, &run_id);
let repo_bytes_db = fixture_root.join("db/repo-bytes.db");
assert!(
repo_bytes_db.exists(),
"repo-bytes fixture missing: {}",
repo_bytes_db.display()
);
let store_dir = tempfile::tempdir().expect("store dir");
let work_db = store_dir.path().join("work-db");
let store = RocksStore::open_with_external_repo_bytes(&work_db, &repo_bytes_db)
.expect("open work-db with external repo-bytes");
let repo_bytes = Arc::new(
store
.external_repo_bytes_ref()
.expect("external repo bytes")
.clone(),
);
let manifest_bytes = store
.get_blob_bytes(&fixture.manifest_sha256_hex)
.expect("load fixture manifest bytes")
.expect("fixture manifest bytes exist");
let manifest = ManifestObject::decode_der(&manifest_bytes).expect("decode fixture manifest");
let mut files = Vec::with_capacity(fixture.objects.len().saturating_sub(1));
let mut child_audits = Vec::new();
let current_ca_hash = "007ad0c291b01ede4bd60e1204074ce3f7192186a022c9577cef5d8e91d5171a";
let current_ca_uri = "rsync://rpki.ripe.net/repository/aca/KpSo3VVK5wEHIJnHC2QHVV3d5mk.cer";
let parent_manifest_uri =
"rsync://rpki.ripe.net/repository/aca/7DNNDzoYvgAht7joQih2Qayxcxo.mft";
let ca = CaInstanceHandle {
depth: 1,
tal_id: "ripe-ncc".to_string(),
parent_manifest_rsync_uri: Some(parent_manifest_uri.to_string()),
ca_certificate: CaCertificateRef::repo_bytes(current_ca_hash.to_string()),
ca_certificate_rsync_uri: Some(current_ca_uri.to_string()),
effective_ip_resources: None,
effective_as_resources: None,
rsync_base_uri: fixture.publication_point_uri.clone(),
manifest_rsync_uri: fixture.manifest_uri.clone(),
publication_point_rsync_uri: fixture.publication_point_uri.clone(),
rrdp_notification_uri: Some("https://rrdp.ripe.net/notification.xml".to_string()),
};
let mut discovered_children = Vec::new();
let use_child_projection = std::env::var("RPKI_RIPE_ROOT_FINALIZE_USE_CHILD_PROJECTION")
.map(|value| value != "0" && value.to_ascii_lowercase() != "false")
.unwrap_or(false);
for object in &fixture.objects {
if object.uri == fixture.manifest_uri {
continue;
}
files.push(pack_file_from_fixture_object(object, &repo_bytes));
if object.object_type == "certificate" {
child_audits.push(child_audit_from_fixture_object(object));
if object.result == "ok" {
let child_entry_projection = use_child_projection
.then(|| child_entry_projection_from_fixture_object(&store, object));
discovered_children.push(discovered_child_from_fixture_object(
&ca,
object,
child_entry_projection,
));
}
}
}
if std::env::var("RPKI_RIPE_ROOT_FINALIZE_PROFILE_CHILD").is_ok() {
let profile_children = fixture
.objects
.iter()
.filter(|object| object.object_type == "certificate" && object.result == "ok")
.map(|object| discovered_child_from_fixture_object(&ca, object, None))
.collect::<Vec<_>>();
let profile_started = std::time::Instant::now();
let profile =
profile_ripe_root_child_entry_build(&store, &profile_children, fixture.validation_time)
.expect("profile child entry build");
eprintln!(
"ripe root child-entry profile: run={} count={} total_ms={} load_der_ms={:.3} decode_cert_ms={:.3} build_entry_ms={:.3}",
run_id,
profile.count,
profile_started.elapsed().as_millis(),
profile.load_der_nanos as f64 / 1_000_000.0,
profile.decode_cert_nanos as f64 / 1_000_000.0,
profile.build_entry_nanos as f64 / 1_000_000.0,
);
if std::env::var("RPKI_RIPE_ROOT_FINALIZE_ONLY_CHILD_PROFILE").is_ok() {
assert!(profile.count > 22_000);
return;
}
}
let fresh_point = FreshValidatedPublicationPoint {
manifest_rsync_uri: fixture.manifest_uri.clone(),
publication_point_rsync_uri: fixture.publication_point_uri.clone(),
manifest_number_be: manifest.manifest.manifest_number.bytes_be.clone(),
this_update: PackTime::from_utc_offset_datetime(manifest.manifest.this_update),
next_update: PackTime::from_utc_offset_datetime(manifest.manifest.next_update),
verified_at: PackTime::from_utc_offset_datetime(fixture.validation_time),
manifest_bytes,
files,
};
let policy = Policy::default();
let enable_ccr_accumulator = std::env::var("RPKI_RIPE_ROOT_FINALIZE_CCR")
.map(|value| value != "0" && value.to_ascii_lowercase() != "false")
.unwrap_or(true);
let runner = Rpkiv1PublicationPointRunner {
store: &store,
policy: &policy,
http_fetcher: &NeverHttpFetcher,
rsync_fetcher: &FailingRsyncFetcher,
validation_time: fixture.validation_time,
timing: None,
download_log: None,
replay_archive_index: None,
replay_delta_index: None,
rrdp_dedup: false,
rrdp_repo_cache: Mutex::new(HashMap::new()),
rsync_dedup: false,
rsync_repo_cache: Mutex::new(HashMap::new()),
current_repo_index: None,
repo_sync_runtime: None,
parallel_phase2_config: None,
parallel_roa_worker_pool: None,
ccr_accumulator: enable_ccr_accumulator
.then(|| Mutex::new(CcrAccumulator::new(Vec::new()))),
persist_vcir: true,
enable_roa_validation_cache: false,
enable_child_certificate_validation_cache: true,
publication_point_cache_observe_only: false,
enable_publication_point_validation_cache: true,
};
eprintln!(
"ripe root finalize repro setup: run={} ccr_accumulator={} child_projection={} objects={} files={} child_audits={} discovered_children={}",
run_id,
enable_ccr_accumulator,
use_child_projection,
fixture.objects.len(),
fresh_point.files.len(),
child_audits.len(),
discovered_children.len()
);
let started = std::time::Instant::now();
let output = runner
.finalize_fresh_publication_point_from_reducer(
&ca,
&fresh_point,
Vec::new(),
empty_objects_output(),
child_audits,
discovered_children,
Some("rrdp"),
Some("rrdp_ok"),
0,
None,
)
.expect("finalize fixture publication point");
let finalize_ms = started.elapsed().as_millis();
eprintln!(
"ripe root finalize repro timing: run={} finalize_ms={} snapshot_pack_ms={} persist_vcir_ms={} build_vcir_ms={} child_entries_ms={} related_artifacts_ms={} replace_vcir_ms={} replace_vcir_encode_ms={} replace_vcir_write_batch_ms={} ccr_projection_build_ms={} audit_build_ms={}",
run_id,
finalize_ms,
output.snapshot_pack_ms,
output.persist_vcir_ms,
output.persist_vcir_timing.build_vcir_ms,
output.persist_vcir_timing.build_vcir.child_entries_ms,
output.persist_vcir_timing.build_vcir.related_artifacts_ms,
output.persist_vcir_timing.replace_vcir_ms,
output.persist_vcir_timing.replace_vcir.vcir_encode_ms,
output.persist_vcir_timing.replace_vcir.write_batch_ms,
output.ccr_projection_build_ms,
output.audit_build_ms,
);
eprintln!(
"ripe root finalize repro result: audit_objects={} discovered_children={} ccr_manifest_count={}",
output.result.audit.objects.len(),
output.result.discovered_children.len(),
runner
.ccr_accumulator_snapshot()
.map(|snapshot| snapshot.manifest_count())
.unwrap_or(0),
);
assert!(fresh_point.files.len() > 22_000);
assert!(output.result.discovered_children.len() > 22_000);
if enable_ccr_accumulator {
assert_eq!(
runner
.ccr_accumulator_snapshot()
.expect("ccr snapshot")
.manifest_count(),
1
);
}
}