5909 lines
224 KiB
Rust
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
|
|
);
|
|
}
|
|
}
|