串行验证通过
This commit is contained in:
7
tests/fixtures/rrdp/notification.xml
vendored
Normal file
7
tests/fixtures/rrdp/notification.xml
vendored
Normal file
@@ -0,0 +1,7 @@
|
||||
<notification xmlns="http://www.ripe.net/rpki/rrdp"
|
||||
version="1"
|
||||
session_id="9df4b597-af9e-4dca-bdda-719cce2c4e28"
|
||||
serial="1">
|
||||
<snapshot uri="https://example.net/rrdp/snapshot.xml"
|
||||
hash="dcb1ce91401d568d7ddf7a4c9f70c65d8428c3a5e7135f82db99c4de30413551"/>
|
||||
</notification>
|
||||
12
tests/fixtures/rrdp/snapshot.xml
vendored
Normal file
12
tests/fixtures/rrdp/snapshot.xml
vendored
Normal file
@@ -0,0 +1,12 @@
|
||||
<snapshot xmlns="http://www.ripe.net/rpki/rrdp"
|
||||
version="1"
|
||||
session_id="9df4b597-af9e-4dca-bdda-719cce2c4e28"
|
||||
serial="1">
|
||||
<publish uri="rsync://example.net/repo/obj1.cer">
|
||||
YWJj
|
||||
</publish>
|
||||
<publish uri="rsync://example.net/repo/obj2.crl">
|
||||
ZGVm
|
||||
</publish>
|
||||
</snapshot>
|
||||
|
||||
37
tests/test_apnic_live_smoke.rs
Normal file
37
tests/test_apnic_live_smoke.rs
Normal file
@@ -0,0 +1,37 @@
|
||||
use rpki::data_model::ta::TrustAnchor;
|
||||
use rpki::data_model::tal::Tal;
|
||||
use rpki::fetch::http::{BlockingHttpFetcher, HttpFetcherConfig};
|
||||
use rpki::validation::ca_instance::ca_instance_uris_from_ca_certificate;
|
||||
use rpki::validation::from_tal::discover_root_ca_instance_from_tal_url;
|
||||
|
||||
const APNIC_TAL_URL: &str = "https://tal.apnic.net/tal-archive/apnic-rfc7730-https.tal";
|
||||
|
||||
#[test]
|
||||
#[ignore = "live network smoke test (APNIC TAL)"]
|
||||
fn apnic_tal_downloads_and_binds_to_ta_certificate() {
|
||||
let fetcher = BlockingHttpFetcher::new(HttpFetcherConfig::default()).expect("build fetcher");
|
||||
|
||||
let tal_bytes = fetcher.fetch_bytes(APNIC_TAL_URL).expect("download TAL");
|
||||
let tal = Tal::decode_bytes(&tal_bytes).expect("decode TAL");
|
||||
|
||||
let ta_uri = tal.ta_uris.first().expect("TAL has TA URIs").clone();
|
||||
let ta_bytes = fetcher
|
||||
.fetch_bytes(ta_uri.as_str())
|
||||
.expect("download TA cert");
|
||||
|
||||
let trust_anchor = TrustAnchor::bind_der(tal, &ta_bytes, Some(&ta_uri)).expect("bind");
|
||||
|
||||
let ca_uris = ca_instance_uris_from_ca_certificate(&trust_anchor.ta_certificate.rc_ca)
|
||||
.expect("extract CA instance URIs");
|
||||
assert!(ca_uris.rsync_base_uri.starts_with("rsync://"));
|
||||
assert!(ca_uris.manifest_rsync_uri.starts_with("rsync://"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[ignore = "live network smoke test (APNIC TAL)"]
|
||||
fn apnic_tal_discovery_api_smoke() {
|
||||
let fetcher = BlockingHttpFetcher::new(HttpFetcherConfig::default()).expect("build fetcher");
|
||||
let d = discover_root_ca_instance_from_tal_url(&fetcher, APNIC_TAL_URL).expect("discover");
|
||||
assert!(d.ca_instance.rsync_base_uri.starts_with("rsync://"));
|
||||
assert!(d.ca_instance.manifest_rsync_uri.starts_with("rsync://"));
|
||||
}
|
||||
253
tests/test_apnic_stats_live_stage2.rs
Normal file
253
tests/test_apnic_stats_live_stage2.rs
Normal file
@@ -0,0 +1,253 @@
|
||||
use std::cell::RefCell;
|
||||
use std::collections::{BTreeMap, HashSet};
|
||||
|
||||
use rpki::data_model::crl::RpkixCrl;
|
||||
use rpki::fetch::http::{BlockingHttpFetcher, HttpFetcherConfig};
|
||||
use rpki::fetch::rsync_system::{SystemRsyncConfig, SystemRsyncFetcher};
|
||||
use rpki::policy::Policy;
|
||||
use rpki::storage::RocksStore;
|
||||
use rpki::validation::from_tal::discover_root_ca_instance_from_tal_url;
|
||||
use rpki::validation::run_tree_from_tal::root_handle_from_trust_anchor;
|
||||
use rpki::validation::tree::{
|
||||
CaInstanceHandle, PublicationPointRunResult, PublicationPointRunner, TreeRunConfig,
|
||||
run_tree_serial,
|
||||
};
|
||||
use rpki::validation::tree_runner::Rpkiv1PublicationPointRunner;
|
||||
|
||||
const APNIC_TAL_URL: &str = "https://tal.apnic.net/tal-archive/apnic-rfc7730-https.tal";
|
||||
|
||||
#[derive(Default, Debug)]
|
||||
struct LiveStats {
|
||||
publication_points_processed: usize,
|
||||
publication_points_fresh: usize,
|
||||
publication_points_cached: usize,
|
||||
rrdp_repos_unique: HashSet<String>,
|
||||
|
||||
pack_file_uris_unique: HashSet<String>,
|
||||
pack_uris_total: usize,
|
||||
pack_uris_by_ext_total: BTreeMap<String, usize>,
|
||||
|
||||
crl_total: usize,
|
||||
crl_decode_ok: usize,
|
||||
|
||||
child_ca_cert_candidates_total: usize,
|
||||
child_ca_cert_validated_ok: usize,
|
||||
|
||||
roa_total: usize,
|
||||
roa_ok: usize,
|
||||
aspa_total: usize,
|
||||
aspa_ok: usize,
|
||||
|
||||
objects_dropped_publication_points: usize,
|
||||
}
|
||||
|
||||
fn ext_of_uri(uri: &str) -> String {
|
||||
let last = uri.rsplit('/').next().unwrap_or(uri);
|
||||
match last.rsplit_once('.') {
|
||||
Some((_base, ext)) if !ext.is_empty() => ext.to_ascii_lowercase(),
|
||||
_ => "(none)".to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
impl LiveStats {
|
||||
fn record_ca_inputs(&mut self, ca: &CaInstanceHandle) {
|
||||
if let Some(u) = ca.rrdp_notification_uri.as_ref() {
|
||||
self.rrdp_repos_unique.insert(u.clone());
|
||||
}
|
||||
}
|
||||
|
||||
fn record(&mut self, res: &PublicationPointRunResult) {
|
||||
self.publication_points_processed += 1;
|
||||
match res.source {
|
||||
rpki::validation::manifest::PublicationPointSource::Fresh => self.publication_points_fresh += 1,
|
||||
rpki::validation::manifest::PublicationPointSource::VerifiedCache => {
|
||||
self.publication_points_cached += 1
|
||||
}
|
||||
}
|
||||
|
||||
// Include manifest object URI itself.
|
||||
self.pack_uris_total += 1;
|
||||
self.pack_file_uris_unique
|
||||
.insert(res.pack.manifest_rsync_uri.clone());
|
||||
*self
|
||||
.pack_uris_by_ext_total
|
||||
.entry(ext_of_uri(&res.pack.manifest_rsync_uri))
|
||||
.or_insert(0) += 1;
|
||||
|
||||
for f in &res.pack.files {
|
||||
self.pack_uris_total += 1;
|
||||
self.pack_file_uris_unique.insert(f.rsync_uri.clone());
|
||||
*self
|
||||
.pack_uris_by_ext_total
|
||||
.entry(ext_of_uri(&f.rsync_uri))
|
||||
.or_insert(0) += 1;
|
||||
|
||||
if f.rsync_uri.ends_with(".crl") {
|
||||
self.crl_total += 1;
|
||||
if RpkixCrl::decode_der(&f.bytes).is_ok() {
|
||||
self.crl_decode_ok += 1;
|
||||
}
|
||||
}
|
||||
|
||||
if f.rsync_uri.ends_with(".cer") {
|
||||
self.child_ca_cert_candidates_total += 1;
|
||||
}
|
||||
}
|
||||
|
||||
self.child_ca_cert_validated_ok += res.discovered_children.len();
|
||||
|
||||
self.roa_total += res.objects.stats.roa_total;
|
||||
self.roa_ok += res.objects.stats.roa_ok;
|
||||
self.aspa_total += res.objects.stats.aspa_total;
|
||||
self.aspa_ok += res.objects.stats.aspa_ok;
|
||||
|
||||
if res.objects.stats.publication_point_dropped {
|
||||
self.objects_dropped_publication_points += 1;
|
||||
}
|
||||
|
||||
if self.publication_points_processed % 25 == 0 {
|
||||
println!(
|
||||
"progress: processed={} fresh={} cached={} failed_objects_pps={} vrps={} aspas={}",
|
||||
self.publication_points_processed,
|
||||
self.publication_points_fresh,
|
||||
self.publication_points_cached,
|
||||
self.objects_dropped_publication_points,
|
||||
self.roa_ok,
|
||||
self.aspa_ok,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct CountingRunner<'a> {
|
||||
inner: &'a dyn PublicationPointRunner,
|
||||
stats: &'a RefCell<LiveStats>,
|
||||
}
|
||||
|
||||
impl<'a> PublicationPointRunner for CountingRunner<'a> {
|
||||
fn run_publication_point(
|
||||
&self,
|
||||
ca: &CaInstanceHandle,
|
||||
) -> Result<PublicationPointRunResult, String> {
|
||||
self.stats.borrow_mut().record_ca_inputs(ca);
|
||||
let res = self.inner.run_publication_point(ca)?;
|
||||
self.stats.borrow_mut().record(&res);
|
||||
Ok(res)
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[ignore = "live network + rsync full-tree stats (APNIC TAL); may take minutes"]
|
||||
fn apnic_tree_full_stats_serial() {
|
||||
let http = BlockingHttpFetcher::new(HttpFetcherConfig::default()).expect("http fetcher");
|
||||
let rsync = SystemRsyncFetcher::new(SystemRsyncConfig::default());
|
||||
let validation_time = time::OffsetDateTime::now_utc();
|
||||
|
||||
let temp = tempfile::tempdir().expect("tempdir");
|
||||
let store = RocksStore::open(temp.path()).expect("open rocksdb");
|
||||
let policy = Policy::default();
|
||||
|
||||
let discovery =
|
||||
discover_root_ca_instance_from_tal_url(&http, APNIC_TAL_URL).expect("discover APNIC TAL");
|
||||
|
||||
let runner = Rpkiv1PublicationPointRunner {
|
||||
store: &store,
|
||||
policy: &policy,
|
||||
http_fetcher: &http,
|
||||
rsync_fetcher: &rsync,
|
||||
validation_time,
|
||||
};
|
||||
|
||||
let stats = RefCell::new(LiveStats::default());
|
||||
let counting = CountingRunner {
|
||||
inner: &runner,
|
||||
stats: &stats,
|
||||
};
|
||||
|
||||
let root: CaInstanceHandle =
|
||||
root_handle_from_trust_anchor(&discovery.trust_anchor, None, &discovery.ca_instance);
|
||||
|
||||
let max_depth = std::env::var("RPKI_APNIC_MAX_DEPTH")
|
||||
.ok()
|
||||
.and_then(|s| s.parse::<usize>().ok());
|
||||
let max_instances = std::env::var("RPKI_APNIC_MAX_INSTANCES")
|
||||
.ok()
|
||||
.and_then(|s| s.parse::<usize>().ok());
|
||||
|
||||
let out = run_tree_serial(
|
||||
root,
|
||||
&counting,
|
||||
&TreeRunConfig {
|
||||
max_depth,
|
||||
max_instances,
|
||||
},
|
||||
)
|
||||
.expect("run tree");
|
||||
|
||||
let stats = stats.into_inner();
|
||||
|
||||
let mut raw_total = 0usize;
|
||||
let mut raw_by_ext: BTreeMap<String, usize> = BTreeMap::new();
|
||||
for (k, _v) in store.raw_iter_all().expect("raw_iter_all") {
|
||||
raw_total += 1;
|
||||
if let Ok(uri) = std::str::from_utf8(&k) {
|
||||
*raw_by_ext.entry(ext_of_uri(uri)).or_insert(0) += 1;
|
||||
}
|
||||
}
|
||||
|
||||
let verified_total = store
|
||||
.verified_iter_all()
|
||||
.expect("verified_iter_all")
|
||||
.count();
|
||||
|
||||
println!("APNIC Stage2 full-tree serial stats");
|
||||
println!("tal_url={APNIC_TAL_URL}");
|
||||
println!("validation_time={}", validation_time.format(&time::format_description::well_known::Rfc3339).unwrap());
|
||||
println!();
|
||||
println!(
|
||||
"publication_points_processed={} publication_points_failed={} fresh={} cached={}",
|
||||
stats.publication_points_processed,
|
||||
out.instances_failed,
|
||||
stats.publication_points_fresh,
|
||||
stats.publication_points_cached
|
||||
);
|
||||
println!("rrdp_repos_unique={}", stats.rrdp_repos_unique.len());
|
||||
println!("objects_dropped_publication_points={}", stats.objects_dropped_publication_points);
|
||||
println!();
|
||||
println!(
|
||||
"pack_uris_total={} pack_uris_unique={}",
|
||||
stats.pack_uris_total,
|
||||
stats.pack_file_uris_unique.len()
|
||||
);
|
||||
println!("pack_uris_by_ext_total={:?}", stats.pack_uris_by_ext_total);
|
||||
println!();
|
||||
println!(
|
||||
"crl_total={} crl_decode_ok={}",
|
||||
stats.crl_total, stats.crl_decode_ok
|
||||
);
|
||||
println!(
|
||||
"child_ca_cert_candidates_total={} child_ca_cert_validated_ok={}",
|
||||
stats.child_ca_cert_candidates_total, stats.child_ca_cert_validated_ok
|
||||
);
|
||||
println!(
|
||||
"roa_total={} roa_ok={} vrps_out={}",
|
||||
stats.roa_total,
|
||||
stats.roa_ok,
|
||||
out.vrps.len()
|
||||
);
|
||||
println!(
|
||||
"aspa_total={} aspa_ok={} aspas_out={}",
|
||||
stats.aspa_total,
|
||||
stats.aspa_ok,
|
||||
out.aspas.len()
|
||||
);
|
||||
println!();
|
||||
println!("rocksdb_raw_objects_total={} raw_by_ext={:?}", raw_total, raw_by_ext);
|
||||
println!("rocksdb_verified_packs_total={}", verified_total);
|
||||
|
||||
// Loose sanity assertions (avoid flakiness due to repository churn).
|
||||
assert!(
|
||||
out.instances_processed >= 2,
|
||||
"expected to process root + at least one child"
|
||||
);
|
||||
}
|
||||
37
tests/test_apnic_tree_live_m15.rs
Normal file
37
tests/test_apnic_tree_live_m15.rs
Normal file
@@ -0,0 +1,37 @@
|
||||
use rpki::fetch::http::{BlockingHttpFetcher, HttpFetcherConfig};
|
||||
use rpki::fetch::rsync_system::{SystemRsyncConfig, SystemRsyncFetcher};
|
||||
use rpki::policy::Policy;
|
||||
use rpki::storage::RocksStore;
|
||||
use rpki::validation::run_tree_from_tal::run_tree_from_tal_url_serial;
|
||||
use rpki::validation::tree::TreeRunConfig;
|
||||
|
||||
const APNIC_TAL_URL: &str = "https://tal.apnic.net/tal-archive/apnic-rfc7730-https.tal";
|
||||
|
||||
#[test]
|
||||
#[ignore = "live network + rsync smoke test (APNIC TAL)"]
|
||||
fn apnic_tree_depth1_processes_more_than_root() {
|
||||
let http = BlockingHttpFetcher::new(HttpFetcherConfig::default()).expect("http fetcher");
|
||||
let rsync = SystemRsyncFetcher::new(SystemRsyncConfig::default());
|
||||
|
||||
let temp = tempfile::tempdir().expect("tempdir");
|
||||
let store = RocksStore::open(temp.path()).expect("open rocksdb");
|
||||
let policy = Policy::default();
|
||||
let out = run_tree_from_tal_url_serial(
|
||||
&store,
|
||||
&policy,
|
||||
APNIC_TAL_URL,
|
||||
&http,
|
||||
&rsync,
|
||||
time::OffsetDateTime::now_utc(),
|
||||
&TreeRunConfig {
|
||||
max_depth: Some(1),
|
||||
max_instances: Some(2),
|
||||
},
|
||||
)
|
||||
.expect("run tree from tal");
|
||||
|
||||
assert!(
|
||||
out.tree.instances_processed >= 2,
|
||||
"expected to process root + at least one child"
|
||||
);
|
||||
}
|
||||
@@ -25,6 +25,9 @@ fn dummy_ee(
|
||||
extensions: RcExtensions {
|
||||
basic_constraints_ca: false,
|
||||
subject_key_identifier: Some(vec![0x01]),
|
||||
authority_key_identifier: None,
|
||||
crl_distribution_points_uris: None,
|
||||
ca_issuers_uris: None,
|
||||
subject_info_access: Some(SubjectInfoAccess::Ca(
|
||||
rpki::data_model::rc::SubjectInfoAccessCa {
|
||||
access_descriptions: vec![],
|
||||
|
||||
65
tests/test_ca_instance_discovery.rs
Normal file
65
tests/test_ca_instance_discovery.rs
Normal file
@@ -0,0 +1,65 @@
|
||||
use rpki::data_model::manifest::ManifestObject;
|
||||
use rpki::data_model::ta::TrustAnchor;
|
||||
use rpki::data_model::tal::Tal;
|
||||
use rpki::validation::ca_instance::{CaInstanceUrisError, ca_instance_uris_from_ca_certificate};
|
||||
use url::Url;
|
||||
|
||||
fn load_tal_and_ta_fixture(tal_name: &str, ta_name: &str) -> TrustAnchor {
|
||||
let tal_bytes =
|
||||
std::fs::read(format!("tests/fixtures/tal/{tal_name}")).expect("read TAL fixture");
|
||||
let tal = Tal::decode_bytes(&tal_bytes).expect("decode TAL");
|
||||
|
||||
let ta_der =
|
||||
std::fs::read(format!("tests/fixtures/ta/{ta_name}")).expect("read TA fixture");
|
||||
let resolved = tal.ta_uris[0].clone();
|
||||
|
||||
TrustAnchor::bind_der(tal, &ta_der, Some(&resolved)).expect("bind TAL and TA")
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ca_instance_uris_are_discoverable_from_trust_anchor_certificates() {
|
||||
let cases = [
|
||||
("afrinic.tal", "afrinic-ta.cer"),
|
||||
("apnic-rfc7730-https.tal", "apnic-ta.cer"),
|
||||
("arin.tal", "arin-ta.cer"),
|
||||
("lacnic.tal", "lacnic-ta.cer"),
|
||||
("ripe-ncc.tal", "ripe-ncc-ta.cer"),
|
||||
];
|
||||
|
||||
for (tal, ta) in cases {
|
||||
let trust_anchor = load_tal_and_ta_fixture(tal, ta);
|
||||
let uris = ca_instance_uris_from_ca_certificate(&trust_anchor.ta_certificate.rc_ca)
|
||||
.expect("extract CA instance URIs");
|
||||
|
||||
assert!(uris.rsync_base_uri.starts_with("rsync://"));
|
||||
assert!(uris.rsync_base_uri.ends_with('/'));
|
||||
assert!(uris.publication_point_rsync_uri.starts_with("rsync://"));
|
||||
assert!(uris.publication_point_rsync_uri.ends_with('/'));
|
||||
assert!(uris.manifest_rsync_uri.starts_with("rsync://"));
|
||||
assert!(
|
||||
uris.manifest_rsync_uri.ends_with(".mft"),
|
||||
"manifest URI should look like an mft: {}",
|
||||
uris.manifest_rsync_uri
|
||||
);
|
||||
|
||||
if let Some(n) = &uris.rrdp_notification_uri {
|
||||
assert_eq!(Url::parse(n).unwrap().scheme(), "https");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ca_instance_uris_rejects_ee_certificate() {
|
||||
let mft_der = std::fs::read(
|
||||
"tests/fixtures/repository/rpki.cernet.net/repo/cernet/0/05FC9C5B88506F7C0D3F862C8895BED67E9F8EBA.mft",
|
||||
)
|
||||
.expect("read manifest fixture");
|
||||
let mft = ManifestObject::decode_der(&mft_der).expect("decode manifest");
|
||||
let ee = &mft.signed_object.signed_data.certificates[0].resource_cert;
|
||||
|
||||
let err = ca_instance_uris_from_ca_certificate(ee).unwrap_err();
|
||||
assert!(
|
||||
matches!(err, CaInstanceUrisError::NotCa),
|
||||
"expected NotCa, got: {err}"
|
||||
);
|
||||
}
|
||||
489
tests/test_ca_path_m15.rs
Normal file
489
tests/test_ca_path_m15.rs
Normal file
@@ -0,0 +1,489 @@
|
||||
use std::process::Command;
|
||||
|
||||
use rpki::data_model::rc::ResourceCertificate;
|
||||
use rpki::validation::ca_path::{CaPathError, validate_subordinate_ca_cert};
|
||||
|
||||
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>,
|
||||
}
|
||||
|
||||
fn write(path: &std::path::Path, s: &str) {
|
||||
std::fs::write(path, s.as_bytes()).expect("write file");
|
||||
}
|
||||
|
||||
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(child_ext: &str, revoke_child: bool) -> Generated {
|
||||
assert!(openssl_available(), "openssl is required for this test");
|
||||
|
||||
let td = tempfile::tempdir().expect("tempdir");
|
||||
let dir = td.path();
|
||||
|
||||
// Minimal CA database layout required by `openssl ca`.
|
||||
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
|
||||
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
|
||||
{child_ext}
|
||||
|
||||
[ crl_ext ]
|
||||
authorityKeyIdentifier = keyid:always
|
||||
"#,
|
||||
dir = dir.display(),
|
||||
child_ext = child_ext
|
||||
);
|
||||
write(&dir.join("openssl.cnf"), &cnf);
|
||||
|
||||
// Issuer CA key + self-signed CA cert (DER later).
|
||||
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("-out")
|
||||
.arg(dir.join("issuer.pem"))
|
||||
.arg("-config")
|
||||
.arg(dir.join("openssl.cnf"))
|
||||
.arg("-extensions")
|
||||
.arg("v3_issuer_ca"),
|
||||
);
|
||||
|
||||
// Child CA key + CSR.
|
||||
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=Child CA")
|
||||
.arg("-out")
|
||||
.arg(dir.join("child.csr"))
|
||||
.arg("-config")
|
||||
.arg(dir.join("openssl.cnf")),
|
||||
);
|
||||
|
||||
// Issue child CA cert using openssl ca (so it appears in the CA database for CRL).
|
||||
run(
|
||||
Command::new("openssl")
|
||||
.arg("ca")
|
||||
.arg("-batch")
|
||||
.arg("-config")
|
||||
.arg(dir.join("openssl.cnf"))
|
||||
.arg("-extensions")
|
||||
.arg("v3_child_ca")
|
||||
.arg("-in")
|
||||
.arg(dir.join("child.csr"))
|
||||
.arg("-out")
|
||||
.arg(dir.join("child.pem"))
|
||||
.arg("-notext"),
|
||||
);
|
||||
|
||||
if revoke_child {
|
||||
run(
|
||||
Command::new("openssl")
|
||||
.arg("ca")
|
||||
.arg("-config")
|
||||
.arg(dir.join("openssl.cnf"))
|
||||
.arg("-revoke")
|
||||
.arg(dir.join("child.pem")),
|
||||
);
|
||||
}
|
||||
|
||||
// Generate CRL.
|
||||
run(
|
||||
Command::new("openssl")
|
||||
.arg("ca")
|
||||
.arg("-gencrl")
|
||||
.arg("-config")
|
||||
.arg(dir.join("openssl.cnf"))
|
||||
.arg("-out")
|
||||
.arg(dir.join("issuer.crl.pem")),
|
||||
);
|
||||
|
||||
// Convert to DER.
|
||||
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("crl")
|
||||
.arg("-in")
|
||||
.arg(dir.join("issuer.crl.pem"))
|
||||
.arg("-outform")
|
||||
.arg("DER")
|
||||
.arg("-out")
|
||||
.arg(dir.join("issuer.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"),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn validate_subordinate_ca_succeeds_for_valid_child_and_subset_resources() {
|
||||
let generated = generate_chain_and_crl(
|
||||
"keyUsage = critical, keyCertSign, cRLSign\nsbgp-ipAddrBlock = critical, IPv4:10.0.0.0/16\nsbgp-autonomousSysNum = critical, AS:64496\n",
|
||||
false,
|
||||
);
|
||||
|
||||
let issuer = ResourceCertificate::decode_der(&generated.issuer_ca_der).expect("decode issuer");
|
||||
|
||||
let now = time::OffsetDateTime::now_utc();
|
||||
let validated = validate_subordinate_ca_cert(
|
||||
&generated.child_ca_der,
|
||||
&generated.issuer_ca_der,
|
||||
&generated.issuer_crl_der,
|
||||
Some("rsync://example.test/repo/issuer/issuer.cer"),
|
||||
"rsync://example.test/repo/issuer/issuer.crl",
|
||||
issuer.tbs.extensions.ip_resources.as_ref(),
|
||||
issuer.tbs.extensions.as_resources.as_ref(),
|
||||
now,
|
||||
)
|
||||
.expect("validate subordinate");
|
||||
|
||||
assert!(validated.effective_ip_resources.is_some());
|
||||
assert!(validated.effective_as_resources.is_some());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn validate_subordinate_ca_rejects_wrong_key_usage_bits() {
|
||||
let generated = generate_chain_and_crl(
|
||||
"keyUsage = critical, digitalSignature\nsbgp-ipAddrBlock = critical, IPv4:10.0.0.0/16\n",
|
||||
false,
|
||||
);
|
||||
|
||||
let issuer = ResourceCertificate::decode_der(&generated.issuer_ca_der).expect("decode issuer");
|
||||
let now = time::OffsetDateTime::now_utc();
|
||||
let err = validate_subordinate_ca_cert(
|
||||
&generated.child_ca_der,
|
||||
&generated.issuer_ca_der,
|
||||
&generated.issuer_crl_der,
|
||||
Some("rsync://example.test/repo/issuer/issuer.cer"),
|
||||
"rsync://example.test/repo/issuer/issuer.crl",
|
||||
issuer.tbs.extensions.ip_resources.as_ref(),
|
||||
issuer.tbs.extensions.as_resources.as_ref(),
|
||||
now,
|
||||
)
|
||||
.unwrap_err();
|
||||
assert!(matches!(err, CaPathError::KeyUsageInvalidBits));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn validate_subordinate_ca_rejects_out_of_scope_resources() {
|
||||
let generated = generate_chain_and_crl(
|
||||
"keyUsage = critical, keyCertSign, cRLSign\nsbgp-ipAddrBlock = critical, IPv4:11.0.0.0/8\n",
|
||||
false,
|
||||
);
|
||||
|
||||
let issuer = ResourceCertificate::decode_der(&generated.issuer_ca_der).expect("decode issuer");
|
||||
let now = time::OffsetDateTime::now_utc();
|
||||
let err = validate_subordinate_ca_cert(
|
||||
&generated.child_ca_der,
|
||||
&generated.issuer_ca_der,
|
||||
&generated.issuer_crl_der,
|
||||
Some("rsync://example.test/repo/issuer/issuer.cer"),
|
||||
"rsync://example.test/repo/issuer/issuer.crl",
|
||||
issuer.tbs.extensions.ip_resources.as_ref(),
|
||||
issuer.tbs.extensions.as_resources.as_ref(),
|
||||
now,
|
||||
)
|
||||
.unwrap_err();
|
||||
assert!(matches!(err, CaPathError::ResourcesNotSubset));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn validate_subordinate_ca_rejects_revoked_child() {
|
||||
let generated = generate_chain_and_crl(
|
||||
"keyUsage = critical, keyCertSign, cRLSign\nsbgp-ipAddrBlock = critical, IPv4:10.0.0.0/16\n",
|
||||
true,
|
||||
);
|
||||
|
||||
let issuer = ResourceCertificate::decode_der(&generated.issuer_ca_der).expect("decode issuer");
|
||||
let now = time::OffsetDateTime::now_utc();
|
||||
let err = validate_subordinate_ca_cert(
|
||||
&generated.child_ca_der,
|
||||
&generated.issuer_ca_der,
|
||||
&generated.issuer_crl_der,
|
||||
Some("rsync://example.test/repo/issuer/issuer.cer"),
|
||||
"rsync://example.test/repo/issuer/issuer.crl",
|
||||
issuer.tbs.extensions.ip_resources.as_ref(),
|
||||
issuer.tbs.extensions.as_resources.as_ref(),
|
||||
now,
|
||||
)
|
||||
.unwrap_err();
|
||||
assert!(matches!(err, CaPathError::ChildRevoked));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn validate_subordinate_ca_rejects_missing_key_usage_extension() {
|
||||
let generated = generate_chain_and_crl(
|
||||
"sbgp-ipAddrBlock = critical, IPv4:10.0.0.0/16\nsbgp-autonomousSysNum = critical, AS:64496\n",
|
||||
false,
|
||||
);
|
||||
let issuer = ResourceCertificate::decode_der(&generated.issuer_ca_der).expect("decode issuer");
|
||||
let now = time::OffsetDateTime::now_utc();
|
||||
let err = validate_subordinate_ca_cert(
|
||||
&generated.child_ca_der,
|
||||
&generated.issuer_ca_der,
|
||||
&generated.issuer_crl_der,
|
||||
Some("rsync://example.test/repo/issuer/issuer.cer"),
|
||||
"rsync://example.test/repo/issuer/issuer.crl",
|
||||
issuer.tbs.extensions.ip_resources.as_ref(),
|
||||
issuer.tbs.extensions.as_resources.as_ref(),
|
||||
now,
|
||||
)
|
||||
.unwrap_err();
|
||||
assert!(matches!(err, CaPathError::KeyUsageMissing));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn validate_subordinate_ca_rejects_non_critical_key_usage() {
|
||||
let generated = generate_chain_and_crl(
|
||||
"keyUsage = keyCertSign, cRLSign\nsbgp-ipAddrBlock = critical, IPv4:10.0.0.0/16\n",
|
||||
false,
|
||||
);
|
||||
let issuer = ResourceCertificate::decode_der(&generated.issuer_ca_der).expect("decode issuer");
|
||||
let now = time::OffsetDateTime::now_utc();
|
||||
let err = validate_subordinate_ca_cert(
|
||||
&generated.child_ca_der,
|
||||
&generated.issuer_ca_der,
|
||||
&generated.issuer_crl_der,
|
||||
Some("rsync://example.test/repo/issuer/issuer.cer"),
|
||||
"rsync://example.test/repo/issuer/issuer.crl",
|
||||
issuer.tbs.extensions.ip_resources.as_ref(),
|
||||
issuer.tbs.extensions.as_resources.as_ref(),
|
||||
now,
|
||||
)
|
||||
.unwrap_err();
|
||||
assert!(matches!(err, CaPathError::KeyUsageNotCritical));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn validate_subordinate_ca_rejects_when_child_has_no_resources() {
|
||||
let generated = generate_chain_and_crl("keyUsage = critical, keyCertSign, cRLSign\n", false);
|
||||
let issuer = ResourceCertificate::decode_der(&generated.issuer_ca_der).expect("decode issuer");
|
||||
let now = time::OffsetDateTime::now_utc();
|
||||
let err = validate_subordinate_ca_cert(
|
||||
&generated.child_ca_der,
|
||||
&generated.issuer_ca_der,
|
||||
&generated.issuer_crl_der,
|
||||
Some("rsync://example.test/repo/issuer/issuer.cer"),
|
||||
"rsync://example.test/repo/issuer/issuer.crl",
|
||||
issuer.tbs.extensions.ip_resources.as_ref(),
|
||||
issuer.tbs.extensions.as_resources.as_ref(),
|
||||
now,
|
||||
)
|
||||
.unwrap_err();
|
||||
assert!(matches!(err, CaPathError::ResourcesMissing));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn validate_subordinate_ca_rejects_when_cert_not_valid_at_validation_time() {
|
||||
let generated = generate_chain_and_crl(
|
||||
"keyUsage = critical, keyCertSign, cRLSign\nsbgp-ipAddrBlock = critical, IPv4:10.0.0.0/16\n",
|
||||
false,
|
||||
);
|
||||
let issuer = ResourceCertificate::decode_der(&generated.issuer_ca_der).expect("decode issuer");
|
||||
let validation_time = time::OffsetDateTime::now_utc() + time::Duration::days(400);
|
||||
let err = validate_subordinate_ca_cert(
|
||||
&generated.child_ca_der,
|
||||
&generated.issuer_ca_der,
|
||||
&generated.issuer_crl_der,
|
||||
Some("rsync://example.test/repo/issuer/issuer.cer"),
|
||||
"rsync://example.test/repo/issuer/issuer.crl",
|
||||
issuer.tbs.extensions.ip_resources.as_ref(),
|
||||
issuer.tbs.extensions.as_resources.as_ref(),
|
||||
validation_time,
|
||||
)
|
||||
.unwrap_err();
|
||||
assert!(matches!(err, CaPathError::CertificateNotValidAtTime));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn validate_subordinate_ca_rejects_when_crl_not_valid_at_validation_time() {
|
||||
let generated = generate_chain_and_crl(
|
||||
"keyUsage = critical, keyCertSign, cRLSign\nsbgp-ipAddrBlock = critical, IPv4:10.0.0.0/16\n",
|
||||
false,
|
||||
);
|
||||
let issuer = ResourceCertificate::decode_der(&generated.issuer_ca_der).expect("decode issuer");
|
||||
let validation_time = time::OffsetDateTime::now_utc() + time::Duration::days(2);
|
||||
let err = validate_subordinate_ca_cert(
|
||||
&generated.child_ca_der,
|
||||
&generated.issuer_ca_der,
|
||||
&generated.issuer_crl_der,
|
||||
Some("rsync://example.test/repo/issuer/issuer.cer"),
|
||||
"rsync://example.test/repo/issuer/issuer.crl",
|
||||
issuer.tbs.extensions.ip_resources.as_ref(),
|
||||
issuer.tbs.extensions.as_resources.as_ref(),
|
||||
validation_time,
|
||||
)
|
||||
.unwrap_err();
|
||||
assert!(matches!(err, CaPathError::CrlNotValidAtTime));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn validate_subordinate_ca_rejects_tampered_child_signature() {
|
||||
let generated = generate_chain_and_crl(
|
||||
"keyUsage = critical, keyCertSign, cRLSign\nsbgp-ipAddrBlock = critical, IPv4:10.0.0.0/16\n",
|
||||
false,
|
||||
);
|
||||
let issuer = ResourceCertificate::decode_der(&generated.issuer_ca_der).expect("decode issuer");
|
||||
let now = time::OffsetDateTime::now_utc();
|
||||
|
||||
let mut tampered = generated.child_ca_der.clone();
|
||||
if let Some(last) = tampered.last_mut() {
|
||||
*last ^= 0x01;
|
||||
}
|
||||
|
||||
let err = validate_subordinate_ca_cert(
|
||||
&tampered,
|
||||
&generated.issuer_ca_der,
|
||||
&generated.issuer_crl_der,
|
||||
Some("rsync://example.test/repo/issuer/issuer.cer"),
|
||||
"rsync://example.test/repo/issuer/issuer.crl",
|
||||
issuer.tbs.extensions.ip_resources.as_ref(),
|
||||
issuer.tbs.extensions.as_resources.as_ref(),
|
||||
now,
|
||||
)
|
||||
.unwrap_err();
|
||||
assert!(matches!(err, CaPathError::ChildSignatureInvalid(_)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn validate_subordinate_ca_rejects_tampered_crl_signature() {
|
||||
let generated = generate_chain_and_crl(
|
||||
"keyUsage = critical, keyCertSign, cRLSign\nsbgp-ipAddrBlock = critical, IPv4:10.0.0.0/16\n",
|
||||
false,
|
||||
);
|
||||
let issuer = ResourceCertificate::decode_der(&generated.issuer_ca_der).expect("decode issuer");
|
||||
let now = time::OffsetDateTime::now_utc();
|
||||
|
||||
let mut tampered = generated.issuer_crl_der.clone();
|
||||
if let Some(last) = tampered.last_mut() {
|
||||
*last ^= 0x01;
|
||||
}
|
||||
|
||||
let err = validate_subordinate_ca_cert(
|
||||
&generated.child_ca_der,
|
||||
&generated.issuer_ca_der,
|
||||
&tampered,
|
||||
Some("rsync://example.test/repo/issuer/issuer.cer"),
|
||||
"rsync://example.test/repo/issuer/issuer.crl",
|
||||
issuer.tbs.extensions.ip_resources.as_ref(),
|
||||
issuer.tbs.extensions.as_resources.as_ref(),
|
||||
now,
|
||||
)
|
||||
.unwrap_err();
|
||||
assert!(matches!(err, CaPathError::CrlVerify(_)));
|
||||
}
|
||||
235
tests/test_cert_path_key_usage.rs
Normal file
235
tests/test_cert_path_key_usage.rs
Normal file
@@ -0,0 +1,235 @@
|
||||
use std::process::Command;
|
||||
|
||||
use rpki::validation::cert_path::{CertPathError, validate_ee_cert_path};
|
||||
|
||||
fn openssl_available() -> bool {
|
||||
Command::new("openssl")
|
||||
.arg("version")
|
||||
.output()
|
||||
.map(|o| o.status.success())
|
||||
.unwrap_or(false)
|
||||
}
|
||||
|
||||
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)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
struct Generated {
|
||||
issuer_ca_der: Vec<u8>,
|
||||
ee_der: Vec<u8>,
|
||||
issuer_crl_der: Vec<u8>,
|
||||
}
|
||||
|
||||
fn generate_issuer_ca_ee_and_crl(ee_ext: &str) -> 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
|
||||
|
||||
[ v3_ee ]
|
||||
basicConstraints = critical,CA:false
|
||||
subjectKeyIdentifier = hash
|
||||
authorityKeyIdentifier = keyid:always
|
||||
crlDistributionPoints = URI:rsync://example.test/repo/issuer/issuer.crl
|
||||
authorityInfoAccess = caIssuers;URI:rsync://example.test/repo/issuer/issuer.cer
|
||||
{ee_ext}
|
||||
|
||||
[ crl_ext ]
|
||||
authorityKeyIdentifier = keyid:always
|
||||
"#,
|
||||
dir = dir.display(),
|
||||
ee_ext = ee_ext
|
||||
);
|
||||
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("ee.key"))
|
||||
.arg("2048"),
|
||||
);
|
||||
run(
|
||||
Command::new("openssl")
|
||||
.arg("req")
|
||||
.arg("-new")
|
||||
.arg("-key")
|
||||
.arg(dir.join("ee.key"))
|
||||
.arg("-subj")
|
||||
.arg("/CN=Test EE")
|
||||
.arg("-out")
|
||||
.arg(dir.join("ee.csr")),
|
||||
);
|
||||
|
||||
run(
|
||||
Command::new("openssl")
|
||||
.arg("ca")
|
||||
.arg("-batch")
|
||||
.arg("-config")
|
||||
.arg(dir.join("openssl.cnf"))
|
||||
.arg("-in")
|
||||
.arg(dir.join("ee.csr"))
|
||||
.arg("-extensions")
|
||||
.arg("v3_ee")
|
||||
.arg("-out")
|
||||
.arg(dir.join("ee.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("ee.pem"))
|
||||
.arg("-outform")
|
||||
.arg("DER")
|
||||
.arg("-out")
|
||||
.arg(dir.join("ee.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")),
|
||||
);
|
||||
|
||||
Generated {
|
||||
issuer_ca_der: std::fs::read(dir.join("issuer.cer")).expect("read issuer der"),
|
||||
ee_der: std::fs::read(dir.join("ee.cer")).expect("read ee der"),
|
||||
issuer_crl_der: std::fs::read(dir.join("issuer.crl")).expect("read crl der"),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ee_key_usage_digital_signature_only_is_accepted() {
|
||||
let g = generate_issuer_ca_ee_and_crl("keyUsage = critical, digitalSignature\n");
|
||||
let now = time::OffsetDateTime::now_utc();
|
||||
validate_ee_cert_path(&g.ee_der, &g.issuer_ca_der, &g.issuer_crl_der, None, None, now)
|
||||
.expect("valid EE path");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ee_key_usage_missing_is_rejected() {
|
||||
let g = generate_issuer_ca_ee_and_crl("");
|
||||
let now = time::OffsetDateTime::now_utc();
|
||||
let err = validate_ee_cert_path(&g.ee_der, &g.issuer_ca_der, &g.issuer_crl_der, None, None, now)
|
||||
.unwrap_err();
|
||||
assert!(matches!(err, CertPathError::KeyUsageMissing), "{err}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ee_key_usage_not_critical_is_rejected() {
|
||||
let g = generate_issuer_ca_ee_and_crl("keyUsage = digitalSignature\n");
|
||||
let now = time::OffsetDateTime::now_utc();
|
||||
let err = validate_ee_cert_path(&g.ee_der, &g.issuer_ca_der, &g.issuer_crl_der, None, None, now)
|
||||
.unwrap_err();
|
||||
assert!(matches!(err, CertPathError::KeyUsageNotCritical), "{err}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ee_key_usage_wrong_bits_is_rejected() {
|
||||
let g = generate_issuer_ca_ee_and_crl("keyUsage = critical, digitalSignature, keyEncipherment\n");
|
||||
let now = time::OffsetDateTime::now_utc();
|
||||
let err = validate_ee_cert_path(&g.ee_der, &g.issuer_ca_der, &g.issuer_crl_der, None, None, now)
|
||||
.unwrap_err();
|
||||
assert!(matches!(err, CertPathError::KeyUsageInvalidBits), "{err}");
|
||||
}
|
||||
120
tests/test_cert_path_m7.rs
Normal file
120
tests/test_cert_path_m7.rs
Normal file
@@ -0,0 +1,120 @@
|
||||
use rpki::data_model::crl::RpkixCrl;
|
||||
use rpki::data_model::manifest::ManifestObject;
|
||||
use rpki::data_model::rc::ResourceCertificate;
|
||||
use rpki::validation::cert_path::{CertPathError, validate_ee_cert_path};
|
||||
|
||||
fn max_time(mut t: time::OffsetDateTime, other: time::OffsetDateTime) -> time::OffsetDateTime {
|
||||
if other > t {
|
||||
t = other;
|
||||
}
|
||||
t
|
||||
}
|
||||
|
||||
fn min_time(mut t: time::OffsetDateTime, other: time::OffsetDateTime) -> time::OffsetDateTime {
|
||||
if other < t {
|
||||
t = other;
|
||||
}
|
||||
t
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ee_cert_must_be_issued_by_ca_and_not_revoked() {
|
||||
let manifest_der = std::fs::read(
|
||||
"tests/fixtures/repository/rpki.cernet.net/repo/cernet/0/05FC9C5B88506F7C0D3F862C8895BED67E9F8EBA.mft",
|
||||
)
|
||||
.expect("read manifest fixture");
|
||||
let crl_der = std::fs::read(
|
||||
"tests/fixtures/repository/rpki.cernet.net/repo/cernet/0/05FC9C5B88506F7C0D3F862C8895BED67E9F8EBA.crl",
|
||||
)
|
||||
.expect("read CRL fixture");
|
||||
let issuer_ca_der = std::fs::read(
|
||||
"tests/fixtures/repository/rpki.apnic.net/repository/B527EF581D6611E2BB468F7C72FD1FF2/BfycW4hQb3wNP4YsiJW-1n6fjro.cer",
|
||||
)
|
||||
.expect("read issuer CA cert fixture");
|
||||
|
||||
let manifest = ManifestObject::decode_der(&manifest_der).expect("decode manifest");
|
||||
let ee_der = &manifest.signed_object.signed_data.certificates[0].raw_der;
|
||||
|
||||
let ee = ResourceCertificate::decode_der(ee_der).expect("decode EE cert");
|
||||
let issuer = ResourceCertificate::decode_der(&issuer_ca_der).expect("decode issuer CA cert");
|
||||
let crl = RpkixCrl::decode_der(&crl_der).expect("decode CRL");
|
||||
|
||||
let mut t = ee.tbs.validity_not_before;
|
||||
t = max_time(t, issuer.tbs.validity_not_before);
|
||||
t = max_time(t, crl.this_update.utc);
|
||||
t = max_time(t, manifest.manifest.this_update);
|
||||
t += time::Duration::seconds(1);
|
||||
|
||||
let mut upper = ee.tbs.validity_not_after;
|
||||
upper = min_time(upper, issuer.tbs.validity_not_after);
|
||||
upper = min_time(upper, crl.next_update.utc);
|
||||
upper = min_time(upper, manifest.manifest.next_update);
|
||||
|
||||
assert!(t < upper);
|
||||
|
||||
validate_ee_cert_path(ee_der, &issuer_ca_der, &crl_der, None, None, t)
|
||||
.expect("cert path validates");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn wrong_issuer_ca_is_rejected() {
|
||||
let manifest_der = std::fs::read(
|
||||
"tests/fixtures/repository/rpki.cernet.net/repo/cernet/0/05FC9C5B88506F7C0D3F862C8895BED67E9F8EBA.mft",
|
||||
)
|
||||
.expect("read manifest fixture");
|
||||
let crl_der = std::fs::read(
|
||||
"tests/fixtures/repository/rpki.cernet.net/repo/cernet/0/05FC9C5B88506F7C0D3F862C8895BED67E9F8EBA.crl",
|
||||
)
|
||||
.expect("read CRL fixture");
|
||||
let wrong_issuer_ca_der = std::fs::read(
|
||||
"tests/fixtures/repository/ca.rg.net/rpki/RGnet-OU/R-lVU1XGsAeqzV1Fv0HjOD6ZFkE.cer",
|
||||
)
|
||||
.expect("read wrong issuer CA cert fixture");
|
||||
|
||||
let manifest = ManifestObject::decode_der(&manifest_der).expect("decode manifest");
|
||||
let ee_der = &manifest.signed_object.signed_data.certificates[0].raw_der;
|
||||
let ee = ResourceCertificate::decode_der(ee_der).expect("decode EE cert");
|
||||
let crl = RpkixCrl::decode_der(&crl_der).expect("decode CRL");
|
||||
|
||||
let t = max_time(ee.tbs.validity_not_before, crl.this_update.utc) + time::Duration::seconds(1);
|
||||
|
||||
let err = validate_ee_cert_path(ee_der, &wrong_issuer_ca_der, &crl_der, None, None, t)
|
||||
.expect_err("wrong issuer must be rejected");
|
||||
assert!(
|
||||
matches!(
|
||||
err,
|
||||
CertPathError::IssuerSubjectMismatch { .. }
|
||||
| CertPathError::EeSignatureInvalid(_)
|
||||
| CertPathError::IssuerNotCa
|
||||
),
|
||||
"{err}"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ee_not_valid_after_not_after_is_rejected() {
|
||||
let manifest_der = std::fs::read(
|
||||
"tests/fixtures/repository/rpki.cernet.net/repo/cernet/0/05FC9C5B88506F7C0D3F862C8895BED67E9F8EBA.mft",
|
||||
)
|
||||
.expect("read manifest fixture");
|
||||
let crl_der = std::fs::read(
|
||||
"tests/fixtures/repository/rpki.cernet.net/repo/cernet/0/05FC9C5B88506F7C0D3F862C8895BED67E9F8EBA.crl",
|
||||
)
|
||||
.expect("read CRL fixture");
|
||||
let issuer_ca_der = std::fs::read(
|
||||
"tests/fixtures/repository/rpki.apnic.net/repository/B527EF581D6611E2BB468F7C72FD1FF2/BfycW4hQb3wNP4YsiJW-1n6fjro.cer",
|
||||
)
|
||||
.expect("read issuer CA cert fixture");
|
||||
|
||||
let manifest = ManifestObject::decode_der(&manifest_der).expect("decode manifest");
|
||||
let ee_der = &manifest.signed_object.signed_data.certificates[0].raw_der;
|
||||
let ee = ResourceCertificate::decode_der(ee_der).expect("decode EE cert");
|
||||
|
||||
let too_late = ee.tbs.validity_not_after + time::Duration::seconds(1);
|
||||
let err = validate_ee_cert_path(ee_der, &issuer_ca_der, &crl_der, None, None, too_late)
|
||||
.expect_err("expired ee rejected");
|
||||
assert!(
|
||||
matches!(err, CertPathError::CertificateNotValidAtTime),
|
||||
"{err}"
|
||||
);
|
||||
}
|
||||
42
tests/test_cli_run_offline_m18.rs
Normal file
42
tests/test_cli_run_offline_m18.rs
Normal file
@@ -0,0 +1,42 @@
|
||||
#[test]
|
||||
fn cli_run_offline_mode_executes_and_writes_json() {
|
||||
let db_dir = tempfile::tempdir().expect("db tempdir");
|
||||
let repo_dir = tempfile::tempdir().expect("repo tempdir");
|
||||
let out_dir = tempfile::tempdir().expect("out tempdir");
|
||||
let report_path = out_dir.path().join("report.json");
|
||||
|
||||
let policy_path = out_dir.path().join("policy.toml");
|
||||
std::fs::write(&policy_path, "sync_preference = \"rsync_only\"\n").expect("write policy");
|
||||
|
||||
let tal_path = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"))
|
||||
.join("tests/fixtures/tal/apnic-rfc7730-https.tal");
|
||||
let ta_path = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"))
|
||||
.join("tests/fixtures/ta/apnic-ta.cer");
|
||||
|
||||
let argv = vec![
|
||||
"rpki".to_string(),
|
||||
"--db".to_string(),
|
||||
db_dir.path().to_string_lossy().to_string(),
|
||||
"--policy".to_string(),
|
||||
policy_path.to_string_lossy().to_string(),
|
||||
"--tal-path".to_string(),
|
||||
tal_path.to_string_lossy().to_string(),
|
||||
"--ta-path".to_string(),
|
||||
ta_path.to_string_lossy().to_string(),
|
||||
"--rsync-local-dir".to_string(),
|
||||
repo_dir.path().to_string_lossy().to_string(),
|
||||
"--max-depth".to_string(),
|
||||
"0".to_string(),
|
||||
"--max-instances".to_string(),
|
||||
"1".to_string(),
|
||||
"--report-json".to_string(),
|
||||
report_path.to_string_lossy().to_string(),
|
||||
];
|
||||
|
||||
rpki::cli::run(&argv).expect("cli run");
|
||||
|
||||
let bytes = std::fs::read(&report_path).expect("read report json");
|
||||
let v: serde_json::Value = serde_json::from_slice(&bytes).expect("parse report json");
|
||||
assert_eq!(v["format_version"], 1);
|
||||
}
|
||||
|
||||
54
tests/test_cli_smoke_m18.rs
Normal file
54
tests/test_cli_smoke_m18.rs
Normal file
@@ -0,0 +1,54 @@
|
||||
use std::process::Command;
|
||||
|
||||
#[test]
|
||||
fn cli_offline_smoke_writes_report_json() {
|
||||
let bin = env!("CARGO_BIN_EXE_rpki");
|
||||
|
||||
let db_dir = tempfile::tempdir().expect("db tempdir");
|
||||
let repo_dir = tempfile::tempdir().expect("repo tempdir");
|
||||
let out_dir = tempfile::tempdir().expect("out tempdir");
|
||||
let report_path = out_dir.path().join("report.json");
|
||||
|
||||
let tal_path = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"))
|
||||
.join("tests/fixtures/tal/apnic-rfc7730-https.tal");
|
||||
let ta_path = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"))
|
||||
.join("tests/fixtures/ta/apnic-ta.cer");
|
||||
|
||||
let out = Command::new(bin)
|
||||
.args([
|
||||
"--db",
|
||||
db_dir.path().to_string_lossy().as_ref(),
|
||||
"--tal-path",
|
||||
tal_path.to_string_lossy().as_ref(),
|
||||
"--ta-path",
|
||||
ta_path.to_string_lossy().as_ref(),
|
||||
"--rsync-local-dir",
|
||||
repo_dir.path().to_string_lossy().as_ref(),
|
||||
"--max-depth",
|
||||
"0",
|
||||
"--max-instances",
|
||||
"1",
|
||||
"--report-json",
|
||||
report_path.to_string_lossy().as_ref(),
|
||||
])
|
||||
.output()
|
||||
.expect("run cli");
|
||||
|
||||
assert!(
|
||||
out.status.success(),
|
||||
"cli failed: status={}\nstdout={}\nstderr={}",
|
||||
out.status,
|
||||
String::from_utf8_lossy(&out.stdout),
|
||||
String::from_utf8_lossy(&out.stderr)
|
||||
);
|
||||
|
||||
let bytes = std::fs::read(&report_path).expect("read report json");
|
||||
let v: serde_json::Value = serde_json::from_slice(&bytes).expect("parse report json");
|
||||
assert_eq!(v["format_version"], 1);
|
||||
assert!(v.get("policy").is_some());
|
||||
assert!(v.get("tree").is_some());
|
||||
assert!(v.get("publication_points").is_some());
|
||||
assert!(v.get("vrps").is_some());
|
||||
assert!(v.get("aspas").is_some());
|
||||
}
|
||||
|
||||
64
tests/test_fetch_http.rs
Normal file
64
tests/test_fetch_http.rs
Normal file
@@ -0,0 +1,64 @@
|
||||
use std::io::{Read, Write};
|
||||
use std::net::TcpListener;
|
||||
use std::thread;
|
||||
|
||||
use rpki::fetch::http::{BlockingHttpFetcher, HttpFetcherConfig};
|
||||
use rpki::sync::rrdp::Fetcher;
|
||||
|
||||
fn start_one_shot_http_server() -> (String, thread::JoinHandle<()>) {
|
||||
let listener = TcpListener::bind("127.0.0.1:0").expect("bind");
|
||||
let addr = listener.local_addr().expect("local addr");
|
||||
let addr_s = addr.to_string();
|
||||
|
||||
let h = thread::spawn(move || {
|
||||
for _ in 0..3 {
|
||||
let (mut sock, _peer) = listener.accept().expect("accept");
|
||||
let mut buf = [0u8; 4096];
|
||||
let n = sock.read(&mut buf).expect("read request");
|
||||
let req = String::from_utf8_lossy(&buf[..n]);
|
||||
let path = req
|
||||
.lines()
|
||||
.next()
|
||||
.and_then(|l| l.split_whitespace().nth(1))
|
||||
.unwrap_or("/");
|
||||
|
||||
let (status, body) = if path == "/ok" {
|
||||
("200 OK", b"ok".as_slice())
|
||||
} else {
|
||||
("404 Not Found", b"nope".as_slice())
|
||||
};
|
||||
|
||||
let resp = format!(
|
||||
"HTTP/1.1 {status}\r\nContent-Length: {}\r\nConnection: close\r\n\r\n",
|
||||
body.len()
|
||||
);
|
||||
sock.write_all(resp.as_bytes()).expect("write headers");
|
||||
sock.write_all(body).expect("write body");
|
||||
}
|
||||
});
|
||||
|
||||
(addr_s, h)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn http_fetcher_handles_success_and_status_errors() {
|
||||
let (addr, h) = start_one_shot_http_server();
|
||||
|
||||
let fetcher = BlockingHttpFetcher::new(HttpFetcherConfig::default()).expect("build fetcher");
|
||||
|
||||
let ok = fetcher
|
||||
.fetch_bytes(&format!("http://{addr}/ok"))
|
||||
.expect("fetch ok");
|
||||
assert_eq!(ok, b"ok");
|
||||
|
||||
// Also exercise the trait method wrapper.
|
||||
let ok2 = Fetcher::fetch(&fetcher, &format!("http://{addr}/ok")).expect("fetch via trait");
|
||||
assert_eq!(ok2, b"ok");
|
||||
|
||||
let err = fetcher
|
||||
.fetch_bytes(&format!("http://{addr}/missing"))
|
||||
.unwrap_err();
|
||||
assert!(err.contains("404"), "error should mention 404, got: {err}");
|
||||
|
||||
h.join().expect("server thread join");
|
||||
}
|
||||
25
tests/test_fetch_rsync_localdir.rs
Normal file
25
tests/test_fetch_rsync_localdir.rs
Normal file
@@ -0,0 +1,25 @@
|
||||
use std::os::unix::net::UnixListener;
|
||||
|
||||
use rpki::fetch::rsync::{LocalDirRsyncFetcher, RsyncFetcher};
|
||||
|
||||
#[test]
|
||||
fn local_dir_rsync_fetcher_normalizes_base_and_skips_non_files() {
|
||||
let td = tempfile::tempdir().expect("tempdir");
|
||||
let dir = td.path();
|
||||
|
||||
std::fs::write(dir.join("a.txt"), b"a").expect("write file");
|
||||
|
||||
// Create a Unix domain socket entry; metadata().is_file() should be false.
|
||||
let _sock = UnixListener::bind(dir.join("sock")).expect("bind unix socket");
|
||||
|
||||
let fetcher = LocalDirRsyncFetcher::new(dir);
|
||||
|
||||
let out = fetcher
|
||||
.fetch_objects("rsync://example.test/repo") // intentionally no trailing slash
|
||||
.expect("fetch objects");
|
||||
|
||||
let uris: Vec<_> = out.iter().map(|(u, _)| u.as_str()).collect();
|
||||
assert!(uris.contains(&"rsync://example.test/repo/a.txt"));
|
||||
assert!(!uris.iter().any(|u| u.ends_with("/sock")));
|
||||
}
|
||||
|
||||
38
tests/test_fetch_rsync_system.rs
Normal file
38
tests/test_fetch_rsync_system.rs
Normal file
@@ -0,0 +1,38 @@
|
||||
use std::fs;
|
||||
|
||||
use rpki::fetch::rsync::RsyncFetcher;
|
||||
use rpki::fetch::rsync_system::{SystemRsyncConfig, SystemRsyncFetcher};
|
||||
|
||||
#[test]
|
||||
fn system_rsync_fetcher_can_sync_from_local_directory_for_tests() {
|
||||
let tmp = tempfile::tempdir().expect("tempdir");
|
||||
let src = tmp.path().join("src");
|
||||
let nested = src.join("sub");
|
||||
fs::create_dir_all(&nested).expect("mkdir");
|
||||
fs::write(src.join("a.txt"), b"aaa").expect("write a");
|
||||
fs::write(nested.join("b.txt"), b"bbb").expect("write b");
|
||||
|
||||
let config = SystemRsyncConfig {
|
||||
rsync_bin: "rsync".into(),
|
||||
..Default::default()
|
||||
};
|
||||
let fetcher = SystemRsyncFetcher::new(config);
|
||||
|
||||
let base = src.to_string_lossy().to_string();
|
||||
let out = fetcher.fetch_objects(&base).expect("fetch objects");
|
||||
|
||||
let mut saw_a = false;
|
||||
let mut saw_b = false;
|
||||
for (uri, bytes) in out {
|
||||
if uri.ends_with("a.txt") {
|
||||
assert_eq!(bytes, b"aaa");
|
||||
saw_a = true;
|
||||
}
|
||||
if uri.ends_with("sub/b.txt") {
|
||||
assert_eq!(bytes, b"bbb");
|
||||
saw_b = true;
|
||||
}
|
||||
}
|
||||
assert!(saw_a, "expected a.txt");
|
||||
assert!(saw_b, "expected sub/b.txt");
|
||||
}
|
||||
181
tests/test_from_tal_offline.rs
Normal file
181
tests/test_from_tal_offline.rs
Normal file
@@ -0,0 +1,181 @@
|
||||
use std::collections::HashMap;
|
||||
|
||||
use rpki::data_model::tal::Tal;
|
||||
use rpki::fetch::rsync::{RsyncFetchError, RsyncFetcher};
|
||||
use rpki::policy::{Policy, SyncPreference};
|
||||
use rpki::storage::RocksStore;
|
||||
use rpki::sync::rrdp::Fetcher;
|
||||
use rpki::validation::from_tal::{
|
||||
FromTalError, discover_root_ca_instance_from_tal, discover_root_ca_instance_from_tal_and_ta_der,
|
||||
discover_root_ca_instance_from_tal_url, run_root_from_tal_url_once,
|
||||
};
|
||||
use rpki::validation::objects::IssuerCaCertificateResolver;
|
||||
use url::Url;
|
||||
|
||||
struct MapFetcher {
|
||||
by_uri: HashMap<String, Vec<u8>>,
|
||||
}
|
||||
|
||||
impl MapFetcher {
|
||||
fn new(by_uri: HashMap<String, Vec<u8>>) -> Self {
|
||||
Self { by_uri }
|
||||
}
|
||||
}
|
||||
|
||||
impl Fetcher for MapFetcher {
|
||||
fn fetch(&self, uri: &str) -> Result<Vec<u8>, String> {
|
||||
self.by_uri
|
||||
.get(uri)
|
||||
.cloned()
|
||||
.ok_or_else(|| format!("missing mapping for {uri}"))
|
||||
}
|
||||
}
|
||||
|
||||
struct EmptyRsync;
|
||||
|
||||
impl RsyncFetcher for EmptyRsync {
|
||||
fn fetch_objects(&self, _rsync_base_uri: &str) -> Result<Vec<(String, Vec<u8>)>, RsyncFetchError> {
|
||||
Ok(Vec::new())
|
||||
}
|
||||
}
|
||||
|
||||
struct NullResolver;
|
||||
|
||||
impl IssuerCaCertificateResolver for NullResolver {
|
||||
fn resolve_by_subject_dn(&self, _subject_dn: &str) -> Option<Vec<u8>> {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
fn apnic_tal_bytes() -> Vec<u8> {
|
||||
std::fs::read("tests/fixtures/tal/apnic-rfc7730-https.tal").expect("read apnic TAL fixture")
|
||||
}
|
||||
|
||||
fn apnic_ta_der() -> Vec<u8> {
|
||||
std::fs::read("tests/fixtures/ta/apnic-ta.cer").expect("read apnic TA fixture")
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn offline_discovery_from_apnic_tal_and_ta_der_fixture_works() {
|
||||
let tal_bytes = apnic_tal_bytes();
|
||||
let ta_der = apnic_ta_der();
|
||||
|
||||
let d = discover_root_ca_instance_from_tal_and_ta_der(&tal_bytes, &ta_der, None)
|
||||
.expect("discover root from fixtures");
|
||||
|
||||
assert!(d.ca_instance.rsync_base_uri.starts_with("rsync://"));
|
||||
assert!(d.ca_instance.rsync_base_uri.ends_with('/'));
|
||||
assert!(d.ca_instance.manifest_rsync_uri.starts_with("rsync://"));
|
||||
assert!(d.ca_instance.manifest_rsync_uri.ends_with(".mft"));
|
||||
if let Some(n) = &d.ca_instance.rrdp_notification_uri {
|
||||
assert!(n.starts_with("https://"));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn discover_root_from_tal_url_works_with_mock_fetcher() {
|
||||
let tal_bytes = apnic_tal_bytes();
|
||||
let tal = Tal::decode_bytes(&tal_bytes).expect("decode TAL");
|
||||
let ta_uri = tal.ta_uris[0].as_str().to_string();
|
||||
|
||||
let mut map = HashMap::new();
|
||||
map.insert("https://example.test/apnic.tal".to_string(), tal_bytes);
|
||||
map.insert(ta_uri, apnic_ta_der());
|
||||
|
||||
let fetcher = MapFetcher::new(map);
|
||||
|
||||
let d = discover_root_ca_instance_from_tal_url(&fetcher, "https://example.test/apnic.tal")
|
||||
.expect("discover");
|
||||
assert!(d.ca_instance.rsync_base_uri.starts_with("rsync://"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn discover_root_tries_multiple_ta_uris_until_one_succeeds() {
|
||||
let tal_bytes = apnic_tal_bytes();
|
||||
let mut tal = Tal::decode_bytes(&tal_bytes).expect("decode TAL");
|
||||
let good_uri = tal.ta_uris[0].clone();
|
||||
tal.ta_uris.insert(0, Url::parse("https://example.invalid/bad.cer").unwrap());
|
||||
|
||||
let mut map = HashMap::new();
|
||||
map.insert(good_uri.as_str().to_string(), apnic_ta_der());
|
||||
let fetcher = MapFetcher::new(map);
|
||||
|
||||
let d = discover_root_ca_instance_from_tal(&fetcher, tal, None).expect("discover");
|
||||
assert_eq!(d.trust_anchor.resolved_ta_uri.as_ref(), Some(&good_uri));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn discover_root_errors_when_no_ta_uris_present() {
|
||||
let tal_bytes = apnic_tal_bytes();
|
||||
let mut tal = Tal::decode_bytes(&tal_bytes).expect("decode TAL");
|
||||
tal.ta_uris.clear();
|
||||
|
||||
let fetcher = MapFetcher::new(HashMap::new());
|
||||
let err = discover_root_ca_instance_from_tal(&fetcher, tal, None).unwrap_err();
|
||||
assert!(matches!(err, FromTalError::NoTaUris));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn discover_root_errors_when_all_ta_fetches_fail() {
|
||||
let tal_bytes = apnic_tal_bytes();
|
||||
let tal = Tal::decode_bytes(&tal_bytes).expect("decode TAL");
|
||||
let fetcher = MapFetcher::new(HashMap::new());
|
||||
let err = discover_root_ca_instance_from_tal(&fetcher, tal, None).unwrap_err();
|
||||
assert!(matches!(err, FromTalError::TaFetch(_)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn discover_root_errors_when_ta_does_not_bind_to_tal_spki() {
|
||||
let tal_bytes = apnic_tal_bytes();
|
||||
let tal = Tal::decode_bytes(&tal_bytes).expect("decode TAL");
|
||||
|
||||
// Use a different TA cert fixture to trigger SPKI mismatch.
|
||||
let wrong_ta = std::fs::read("tests/fixtures/ta/arin-ta.cer").expect("read arin ta");
|
||||
|
||||
let mut map = HashMap::new();
|
||||
map.insert(tal.ta_uris[0].as_str().to_string(), wrong_ta);
|
||||
let fetcher = MapFetcher::new(map);
|
||||
|
||||
let err = discover_root_ca_instance_from_tal(&fetcher, tal, None).unwrap_err();
|
||||
assert!(matches!(err, FromTalError::TaFetch(_)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn discover_root_from_tal_url_errors_when_tal_fetch_fails() {
|
||||
let fetcher = MapFetcher::new(HashMap::new());
|
||||
let err = discover_root_ca_instance_from_tal_url(&fetcher, "https://example.test/missing.tal")
|
||||
.unwrap_err();
|
||||
assert!(matches!(err, FromTalError::TalFetch(_)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn run_root_from_tal_url_once_propagates_run_error_when_repo_is_empty() {
|
||||
let tal_bytes = apnic_tal_bytes();
|
||||
let tal = Tal::decode_bytes(&tal_bytes).expect("decode TAL");
|
||||
let ta_uri = tal.ta_uris[0].as_str().to_string();
|
||||
|
||||
let mut map = HashMap::new();
|
||||
map.insert("https://example.test/apnic.tal".to_string(), tal_bytes);
|
||||
map.insert(ta_uri, apnic_ta_der());
|
||||
let fetcher = MapFetcher::new(map);
|
||||
|
||||
let temp = tempfile::tempdir().expect("tempdir");
|
||||
let store = RocksStore::open(temp.path()).expect("open rocksdb");
|
||||
|
||||
let mut policy = Policy::default();
|
||||
policy.sync_preference = SyncPreference::RsyncOnly;
|
||||
|
||||
let err = run_root_from_tal_url_once(
|
||||
&store,
|
||||
&policy,
|
||||
"https://example.test/apnic.tal",
|
||||
&fetcher,
|
||||
&EmptyRsync,
|
||||
&NullResolver,
|
||||
time::OffsetDateTime::now_utc(),
|
||||
)
|
||||
.unwrap_err();
|
||||
|
||||
assert!(matches!(err, FromTalError::Run(_)));
|
||||
}
|
||||
|
||||
118
tests/test_manifest_cache_errors_more.rs
Normal file
118
tests/test_manifest_cache_errors_more.rs
Normal file
@@ -0,0 +1,118 @@
|
||||
use std::path::Path;
|
||||
|
||||
use rpki::data_model::manifest::ManifestObject;
|
||||
use rpki::policy::{CaFailedFetchPolicy, Policy};
|
||||
use rpki::storage::{RocksStore, VerifiedKey, VerifiedPublicationPointPack};
|
||||
use rpki::validation::manifest::process_manifest_publication_point;
|
||||
|
||||
fn fixture_to_rsync_uri(path: &Path) -> String {
|
||||
let rel = path
|
||||
.strip_prefix("tests/fixtures/repository")
|
||||
.expect("path under tests/fixtures/repository");
|
||||
let mut it = rel.components();
|
||||
let host = it
|
||||
.next()
|
||||
.expect("host component")
|
||||
.as_os_str()
|
||||
.to_string_lossy();
|
||||
let rest = it.as_path().to_string_lossy();
|
||||
format!("rsync://{host}/{rest}")
|
||||
}
|
||||
|
||||
fn fixture_dir_to_rsync_uri(dir: &Path) -> String {
|
||||
let mut s = fixture_to_rsync_uri(dir);
|
||||
if !s.ends_with('/') {
|
||||
s.push('/');
|
||||
}
|
||||
s
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cache_is_not_used_when_missing_and_fresh_manifest_is_missing() {
|
||||
let temp = tempfile::tempdir().expect("tempdir");
|
||||
let store = RocksStore::open(temp.path()).expect("open rocksdb");
|
||||
|
||||
let mut policy = Policy::default();
|
||||
policy.ca_failed_fetch_policy = CaFailedFetchPolicy::UseVerifiedCache;
|
||||
|
||||
let err = process_manifest_publication_point(
|
||||
&store,
|
||||
&policy,
|
||||
"rsync://example.net/repo/manifest.mft",
|
||||
"rsync://example.net/repo/",
|
||||
time::OffsetDateTime::from_unix_timestamp(0).unwrap(),
|
||||
)
|
||||
.expect_err("no raw and no verified cache should fail");
|
||||
|
||||
assert!(err.to_string().contains("verified cache entry missing"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cache_pack_publication_point_mismatch_is_rejected() {
|
||||
let manifest_path = Path::new(
|
||||
"tests/fixtures/repository/rpki.cernet.net/repo/cernet/0/05FC9C5B88506F7C0D3F862C8895BED67E9F8EBA.mft",
|
||||
);
|
||||
let manifest_bytes = std::fs::read(manifest_path).expect("read manifest fixture");
|
||||
let manifest = ManifestObject::decode_der(&manifest_bytes).expect("decode manifest fixture");
|
||||
|
||||
let validation_time = manifest.manifest.this_update + time::Duration::seconds(1);
|
||||
|
||||
let manifest_rsync_uri = fixture_to_rsync_uri(manifest_path);
|
||||
let publication_point_rsync_uri = fixture_dir_to_rsync_uri(manifest_path.parent().unwrap());
|
||||
|
||||
let temp = tempfile::tempdir().expect("tempdir");
|
||||
let store = RocksStore::open(temp.path()).expect("open rocksdb");
|
||||
|
||||
store
|
||||
.put_raw(&manifest_rsync_uri, &manifest_bytes)
|
||||
.expect("store manifest");
|
||||
for entry in &manifest.manifest.files {
|
||||
let file_path = manifest_path.parent().unwrap().join(&entry.file_name);
|
||||
let bytes = std::fs::read(&file_path)
|
||||
.unwrap_or_else(|_| panic!("read fixture file referenced by manifest: {file_path:?}"));
|
||||
let rsync_uri = format!("{publication_point_rsync_uri}{}", entry.file_name);
|
||||
store.put_raw(&rsync_uri, &bytes).expect("store file");
|
||||
}
|
||||
|
||||
let policy = Policy::default();
|
||||
let _ = process_manifest_publication_point(
|
||||
&store,
|
||||
&policy,
|
||||
&manifest_rsync_uri,
|
||||
&publication_point_rsync_uri,
|
||||
validation_time,
|
||||
)
|
||||
.expect("first run stores verified pack");
|
||||
|
||||
// Corrupt the cached pack by changing the publication point.
|
||||
let key = VerifiedKey::from_manifest_rsync_uri(&manifest_rsync_uri);
|
||||
let bytes = store
|
||||
.get_verified(&key)
|
||||
.expect("get verified")
|
||||
.expect("verified exists");
|
||||
let mut pack = VerifiedPublicationPointPack::decode(&bytes).expect("decode pack");
|
||||
pack.publication_point_rsync_uri = "rsync://evil.invalid/repo/".to_string();
|
||||
let bytes = pack.encode().expect("re-encode pack");
|
||||
store
|
||||
.put_verified(&key, &bytes)
|
||||
.expect("overwrite verified");
|
||||
|
||||
// Remove raw manifest to force cache path.
|
||||
store
|
||||
.delete_raw(&manifest_rsync_uri)
|
||||
.expect("delete raw manifest");
|
||||
|
||||
let err = process_manifest_publication_point(
|
||||
&store,
|
||||
&policy,
|
||||
&manifest_rsync_uri,
|
||||
&publication_point_rsync_uri,
|
||||
validation_time,
|
||||
)
|
||||
.expect_err("cache pack mismatch should fail");
|
||||
|
||||
assert!(
|
||||
err.to_string()
|
||||
.contains("publication_point_rsync_uri does not match expected")
|
||||
);
|
||||
}
|
||||
266
tests/test_manifest_processor_m4.rs
Normal file
266
tests/test_manifest_processor_m4.rs
Normal file
@@ -0,0 +1,266 @@
|
||||
use std::path::Path;
|
||||
|
||||
use rpki::data_model::manifest::ManifestObject;
|
||||
use rpki::policy::{CaFailedFetchPolicy, Policy};
|
||||
use rpki::storage::{RocksStore, VerifiedKey, VerifiedPublicationPointPack};
|
||||
use rpki::validation::manifest::{PublicationPointSource, process_manifest_publication_point};
|
||||
|
||||
fn fixture_to_rsync_uri(path: &Path) -> String {
|
||||
let rel = path
|
||||
.strip_prefix("tests/fixtures/repository")
|
||||
.expect("path under tests/fixtures/repository");
|
||||
let mut it = rel.components();
|
||||
let host = it
|
||||
.next()
|
||||
.expect("host component")
|
||||
.as_os_str()
|
||||
.to_string_lossy();
|
||||
let rest = it.as_path().to_string_lossy();
|
||||
format!("rsync://{host}/{rest}")
|
||||
}
|
||||
|
||||
fn fixture_dir_to_rsync_uri(dir: &Path) -> String {
|
||||
let mut s = fixture_to_rsync_uri(dir);
|
||||
if !s.ends_with('/') {
|
||||
s.push('/');
|
||||
}
|
||||
s
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn manifest_success_writes_verified_pack() {
|
||||
let manifest_path = Path::new(
|
||||
"tests/fixtures/repository/rpki.cernet.net/repo/cernet/0/05FC9C5B88506F7C0D3F862C8895BED67E9F8EBA.mft",
|
||||
);
|
||||
let manifest_bytes = std::fs::read(manifest_path).expect("read manifest fixture");
|
||||
let manifest = ManifestObject::decode_der(&manifest_bytes).expect("decode manifest fixture");
|
||||
|
||||
let validation_time = manifest.manifest.this_update + time::Duration::seconds(1);
|
||||
|
||||
let manifest_rsync_uri = fixture_to_rsync_uri(manifest_path);
|
||||
let publication_point_rsync_uri = fixture_dir_to_rsync_uri(manifest_path.parent().unwrap());
|
||||
|
||||
let temp = tempfile::tempdir().expect("tempdir");
|
||||
let store = RocksStore::open(temp.path()).expect("open rocksdb");
|
||||
|
||||
store
|
||||
.put_raw(&manifest_rsync_uri, &manifest_bytes)
|
||||
.expect("store manifest");
|
||||
for entry in &manifest.manifest.files {
|
||||
let file_path = manifest_path.parent().unwrap().join(&entry.file_name);
|
||||
let bytes = std::fs::read(&file_path)
|
||||
.unwrap_or_else(|_| panic!("read fixture file referenced by manifest: {file_path:?}"));
|
||||
let rsync_uri = format!("{publication_point_rsync_uri}{}", entry.file_name);
|
||||
store.put_raw(&rsync_uri, &bytes).expect("store file");
|
||||
}
|
||||
|
||||
let policy = Policy::default();
|
||||
let out = process_manifest_publication_point(
|
||||
&store,
|
||||
&policy,
|
||||
&manifest_rsync_uri,
|
||||
&publication_point_rsync_uri,
|
||||
validation_time,
|
||||
)
|
||||
.expect("process manifest publication point");
|
||||
assert_eq!(out.source, PublicationPointSource::Fresh);
|
||||
assert!(out.warnings.is_empty());
|
||||
|
||||
let key = VerifiedKey::from_manifest_rsync_uri(&manifest_rsync_uri);
|
||||
let stored = store
|
||||
.get_verified(&key)
|
||||
.expect("get verified")
|
||||
.expect("verified pack exists");
|
||||
let decoded = VerifiedPublicationPointPack::decode(&stored).expect("decode stored pack");
|
||||
assert_eq!(decoded.manifest_rsync_uri, manifest_rsync_uri);
|
||||
assert_eq!(
|
||||
decoded.publication_point_rsync_uri,
|
||||
publication_point_rsync_uri
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn manifest_hash_mismatch_falls_back_to_verified_cache_when_enabled() {
|
||||
let manifest_path = Path::new(
|
||||
"tests/fixtures/repository/rpki.cernet.net/repo/cernet/0/05FC9C5B88506F7C0D3F862C8895BED67E9F8EBA.mft",
|
||||
);
|
||||
let manifest_bytes = std::fs::read(manifest_path).expect("read manifest fixture");
|
||||
let manifest = ManifestObject::decode_der(&manifest_bytes).expect("decode manifest fixture");
|
||||
|
||||
let validation_time = manifest.manifest.this_update + time::Duration::seconds(1);
|
||||
|
||||
let manifest_rsync_uri = fixture_to_rsync_uri(manifest_path);
|
||||
let publication_point_rsync_uri = fixture_dir_to_rsync_uri(manifest_path.parent().unwrap());
|
||||
|
||||
let temp = tempfile::tempdir().expect("tempdir");
|
||||
let store = RocksStore::open(temp.path()).expect("open rocksdb");
|
||||
store
|
||||
.put_raw(&manifest_rsync_uri, &manifest_bytes)
|
||||
.expect("store manifest");
|
||||
for entry in &manifest.manifest.files {
|
||||
let file_path = manifest_path.parent().unwrap().join(&entry.file_name);
|
||||
let bytes = std::fs::read(&file_path)
|
||||
.unwrap_or_else(|_| panic!("read fixture file referenced by manifest: {file_path:?}"));
|
||||
let rsync_uri = format!("{publication_point_rsync_uri}{}", entry.file_name);
|
||||
store.put_raw(&rsync_uri, &bytes).expect("store file");
|
||||
}
|
||||
|
||||
let policy = Policy::default();
|
||||
let first = process_manifest_publication_point(
|
||||
&store,
|
||||
&policy,
|
||||
&manifest_rsync_uri,
|
||||
&publication_point_rsync_uri,
|
||||
validation_time,
|
||||
)
|
||||
.expect("first run stores verified pack");
|
||||
assert_eq!(first.source, PublicationPointSource::Fresh);
|
||||
|
||||
let key = VerifiedKey::from_manifest_rsync_uri(&manifest_rsync_uri);
|
||||
let cached_bytes = store
|
||||
.get_verified(&key)
|
||||
.expect("get verified")
|
||||
.expect("verified pack exists");
|
||||
let cached_pack = VerifiedPublicationPointPack::decode(&cached_bytes).expect("decode cached");
|
||||
|
||||
let victim = manifest
|
||||
.manifest
|
||||
.files
|
||||
.first()
|
||||
.expect("non-empty file list");
|
||||
let victim_uri = format!("{publication_point_rsync_uri}{}", victim.file_name);
|
||||
let mut tampered = store
|
||||
.get_raw(&victim_uri)
|
||||
.expect("get victim raw")
|
||||
.expect("victim raw exists");
|
||||
tampered[0] ^= 0xFF;
|
||||
store.put_raw(&victim_uri, &tampered).expect("tamper raw");
|
||||
|
||||
let second = process_manifest_publication_point(
|
||||
&store,
|
||||
&policy,
|
||||
&manifest_rsync_uri,
|
||||
&publication_point_rsync_uri,
|
||||
validation_time,
|
||||
)
|
||||
.expect("second run falls back to verified cache");
|
||||
assert_eq!(second.source, PublicationPointSource::VerifiedCache);
|
||||
assert!(!second.warnings.is_empty());
|
||||
assert_eq!(second.pack, cached_pack);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn manifest_failed_fetch_stop_all_output() {
|
||||
let manifest_path = Path::new(
|
||||
"tests/fixtures/repository/rpki.cernet.net/repo/cernet/0/05FC9C5B88506F7C0D3F862C8895BED67E9F8EBA.mft",
|
||||
);
|
||||
let manifest_bytes = std::fs::read(manifest_path).expect("read manifest fixture");
|
||||
let manifest = ManifestObject::decode_der(&manifest_bytes).expect("decode manifest fixture");
|
||||
|
||||
let validation_time = manifest.manifest.this_update + time::Duration::seconds(1);
|
||||
|
||||
let manifest_rsync_uri = fixture_to_rsync_uri(manifest_path);
|
||||
let publication_point_rsync_uri = fixture_dir_to_rsync_uri(manifest_path.parent().unwrap());
|
||||
|
||||
let temp = tempfile::tempdir().expect("tempdir");
|
||||
let store = RocksStore::open(temp.path()).expect("open rocksdb");
|
||||
store
|
||||
.put_raw(&manifest_rsync_uri, &manifest_bytes)
|
||||
.expect("store manifest");
|
||||
for entry in &manifest.manifest.files {
|
||||
let file_path = manifest_path.parent().unwrap().join(&entry.file_name);
|
||||
let bytes = std::fs::read(&file_path)
|
||||
.unwrap_or_else(|_| panic!("read fixture file referenced by manifest: {file_path:?}"));
|
||||
let rsync_uri = format!("{publication_point_rsync_uri}{}", entry.file_name);
|
||||
store.put_raw(&rsync_uri, &bytes).expect("store file");
|
||||
}
|
||||
|
||||
let mut policy = Policy::default();
|
||||
policy.ca_failed_fetch_policy = CaFailedFetchPolicy::UseVerifiedCache;
|
||||
let _ = process_manifest_publication_point(
|
||||
&store,
|
||||
&policy,
|
||||
&manifest_rsync_uri,
|
||||
&publication_point_rsync_uri,
|
||||
validation_time,
|
||||
)
|
||||
.expect("first run stores verified pack");
|
||||
|
||||
let victim = manifest
|
||||
.manifest
|
||||
.files
|
||||
.first()
|
||||
.expect("non-empty file list");
|
||||
let victim_uri = format!("{publication_point_rsync_uri}{}", victim.file_name);
|
||||
let mut tampered = store
|
||||
.get_raw(&victim_uri)
|
||||
.expect("get victim raw")
|
||||
.expect("victim raw exists");
|
||||
tampered[0] ^= 0xFF;
|
||||
store.put_raw(&victim_uri, &tampered).expect("tamper raw");
|
||||
|
||||
policy.ca_failed_fetch_policy = CaFailedFetchPolicy::StopAllOutput;
|
||||
let err = process_manifest_publication_point(
|
||||
&store,
|
||||
&policy,
|
||||
&manifest_rsync_uri,
|
||||
&publication_point_rsync_uri,
|
||||
validation_time,
|
||||
)
|
||||
.expect_err("stop_all_output should not use verified cache");
|
||||
let msg = err.to_string();
|
||||
assert!(msg.contains("cache use is disabled"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn manifest_fallback_pack_is_revalidated_and_rejected_if_stale() {
|
||||
let manifest_path = Path::new(
|
||||
"tests/fixtures/repository/rpki.cernet.net/repo/cernet/0/05FC9C5B88506F7C0D3F862C8895BED67E9F8EBA.mft",
|
||||
);
|
||||
let manifest_bytes = std::fs::read(manifest_path).expect("read manifest fixture");
|
||||
let manifest = ManifestObject::decode_der(&manifest_bytes).expect("decode manifest fixture");
|
||||
|
||||
let ok_time = manifest.manifest.this_update + time::Duration::seconds(1);
|
||||
let stale_time = manifest.manifest.next_update + time::Duration::seconds(1);
|
||||
|
||||
let manifest_rsync_uri = fixture_to_rsync_uri(manifest_path);
|
||||
let publication_point_rsync_uri = fixture_dir_to_rsync_uri(manifest_path.parent().unwrap());
|
||||
|
||||
let temp = tempfile::tempdir().expect("tempdir");
|
||||
let store = RocksStore::open(temp.path()).expect("open rocksdb");
|
||||
store
|
||||
.put_raw(&manifest_rsync_uri, &manifest_bytes)
|
||||
.expect("store manifest");
|
||||
for entry in &manifest.manifest.files {
|
||||
let file_path = manifest_path.parent().unwrap().join(&entry.file_name);
|
||||
let bytes = std::fs::read(&file_path)
|
||||
.unwrap_or_else(|_| panic!("read fixture file referenced by manifest: {file_path:?}"));
|
||||
let rsync_uri = format!("{publication_point_rsync_uri}{}", entry.file_name);
|
||||
store.put_raw(&rsync_uri, &bytes).expect("store file");
|
||||
}
|
||||
|
||||
let policy = Policy::default();
|
||||
let _ = process_manifest_publication_point(
|
||||
&store,
|
||||
&policy,
|
||||
&manifest_rsync_uri,
|
||||
&publication_point_rsync_uri,
|
||||
ok_time,
|
||||
)
|
||||
.expect("first run stores verified pack");
|
||||
|
||||
store
|
||||
.delete_raw(&manifest_rsync_uri)
|
||||
.expect("delete manifest raw to force fallback");
|
||||
|
||||
let err = process_manifest_publication_point(
|
||||
&store,
|
||||
&policy,
|
||||
&manifest_rsync_uri,
|
||||
&publication_point_rsync_uri,
|
||||
stale_time,
|
||||
)
|
||||
.expect_err("stale validation_time must reject verified cache pack");
|
||||
let msg = err.to_string();
|
||||
assert!(msg.contains("not valid at validation_time"));
|
||||
}
|
||||
172
tests/test_objects_errors_more.rs
Normal file
172
tests/test_objects_errors_more.rs
Normal file
@@ -0,0 +1,172 @@
|
||||
use std::collections::HashMap;
|
||||
use std::path::Path;
|
||||
|
||||
use rpki::data_model::crl::RpkixCrl;
|
||||
use rpki::data_model::manifest::ManifestObject;
|
||||
use rpki::data_model::rc::ResourceCertificate;
|
||||
use rpki::policy::{Policy, SignedObjectFailurePolicy};
|
||||
use rpki::storage::{PackFile, RocksStore};
|
||||
use rpki::validation::manifest::process_manifest_publication_point;
|
||||
use rpki::validation::objects::{
|
||||
IssuerCaCertificateResolver, process_verified_publication_point_pack,
|
||||
};
|
||||
|
||||
fn fixture_to_rsync_uri(path: &Path) -> String {
|
||||
let rel = path
|
||||
.strip_prefix("tests/fixtures/repository")
|
||||
.expect("path under tests/fixtures/repository");
|
||||
let mut it = rel.components();
|
||||
let host = it
|
||||
.next()
|
||||
.expect("host component")
|
||||
.as_os_str()
|
||||
.to_string_lossy();
|
||||
let rest = it.as_path().to_string_lossy();
|
||||
format!("rsync://{host}/{rest}")
|
||||
}
|
||||
|
||||
fn fixture_dir_to_rsync_uri(dir: &Path) -> String {
|
||||
let mut s = fixture_to_rsync_uri(dir);
|
||||
if !s.ends_with('/') {
|
||||
s.push('/');
|
||||
}
|
||||
s
|
||||
}
|
||||
|
||||
struct EmptyResolver;
|
||||
|
||||
impl IssuerCaCertificateResolver for EmptyResolver {
|
||||
fn resolve_by_subject_dn(&self, _subject_dn: &str) -> Option<Vec<u8>> {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
struct MapResolver {
|
||||
by_subject_dn: HashMap<String, Vec<u8>>,
|
||||
}
|
||||
|
||||
impl IssuerCaCertificateResolver for MapResolver {
|
||||
fn resolve_by_subject_dn(&self, subject_dn: &str) -> Option<Vec<u8>> {
|
||||
self.by_subject_dn.get(subject_dn).cloned()
|
||||
}
|
||||
}
|
||||
|
||||
fn build_cernet_pack_and_validation_time() -> (
|
||||
rpki::storage::VerifiedPublicationPointPack,
|
||||
time::OffsetDateTime,
|
||||
MapResolver,
|
||||
) {
|
||||
let manifest_path = Path::new(
|
||||
"tests/fixtures/repository/rpki.cernet.net/repo/cernet/0/05FC9C5B88506F7C0D3F862C8895BED67E9F8EBA.mft",
|
||||
);
|
||||
let manifest_bytes = std::fs::read(manifest_path).expect("read manifest fixture");
|
||||
let manifest = ManifestObject::decode_der(&manifest_bytes).expect("decode manifest fixture");
|
||||
|
||||
let manifest_rsync_uri = fixture_to_rsync_uri(manifest_path);
|
||||
let publication_point_rsync_uri = fixture_dir_to_rsync_uri(manifest_path.parent().unwrap());
|
||||
|
||||
let temp = tempfile::tempdir().expect("tempdir");
|
||||
let store = RocksStore::open(temp.path()).expect("open rocksdb");
|
||||
|
||||
store
|
||||
.put_raw(&manifest_rsync_uri, &manifest_bytes)
|
||||
.expect("store manifest");
|
||||
for entry in &manifest.manifest.files {
|
||||
let file_path = manifest_path.parent().unwrap().join(&entry.file_name);
|
||||
let bytes = std::fs::read(&file_path)
|
||||
.unwrap_or_else(|_| panic!("read fixture file referenced by manifest: {file_path:?}"));
|
||||
let rsync_uri = format!("{publication_point_rsync_uri}{}", entry.file_name);
|
||||
store.put_raw(&rsync_uri, &bytes).expect("store file");
|
||||
}
|
||||
|
||||
let policy = Policy::default();
|
||||
let out = process_manifest_publication_point(
|
||||
&store,
|
||||
&policy,
|
||||
&manifest_rsync_uri,
|
||||
&publication_point_rsync_uri,
|
||||
manifest.manifest.this_update + time::Duration::seconds(1),
|
||||
)
|
||||
.expect("process manifest publication point");
|
||||
|
||||
let issuer_ca_der = std::fs::read(
|
||||
"tests/fixtures/repository/rpki.apnic.net/repository/B527EF581D6611E2BB468F7C72FD1FF2/BfycW4hQb3wNP4YsiJW-1n6fjro.cer",
|
||||
)
|
||||
.expect("read issuer CA cert fixture");
|
||||
let issuer_ca = ResourceCertificate::decode_der(&issuer_ca_der).expect("decode issuer CA cert");
|
||||
|
||||
let crl_file = out
|
||||
.pack
|
||||
.files
|
||||
.iter()
|
||||
.find(|f| f.rsync_uri.ends_with(".crl"))
|
||||
.expect("crl present in pack");
|
||||
let crl = RpkixCrl::decode_der(&crl_file.bytes).expect("decode crl");
|
||||
|
||||
let mut t = manifest.manifest.this_update;
|
||||
if issuer_ca.tbs.validity_not_before > t {
|
||||
t = issuer_ca.tbs.validity_not_before;
|
||||
}
|
||||
if crl.this_update.utc > t {
|
||||
t = crl.this_update.utc;
|
||||
}
|
||||
t += time::Duration::seconds(1);
|
||||
|
||||
let resolver = MapResolver {
|
||||
by_subject_dn: HashMap::from([(issuer_ca.tbs.subject_dn, issuer_ca_der)]),
|
||||
};
|
||||
|
||||
(out.pack, t, resolver)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn missing_crl_causes_roas_to_be_dropped_under_drop_object_policy() {
|
||||
let (mut pack, validation_time, resolver) = build_cernet_pack_and_validation_time();
|
||||
pack.files.retain(|f| !f.rsync_uri.ends_with(".crl"));
|
||||
|
||||
let mut policy = Policy::default();
|
||||
policy.signed_object_failure_policy = SignedObjectFailurePolicy::DropObject;
|
||||
|
||||
let out = process_verified_publication_point_pack(&pack, &policy, &resolver, validation_time)
|
||||
.expect("drop_object should not fail the publication point");
|
||||
assert!(out.vrps.is_empty());
|
||||
assert!(!out.warnings.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn missing_issuer_ca_cert_causes_roas_to_be_dropped_under_drop_object_policy() {
|
||||
let (pack, validation_time, _resolver) = build_cernet_pack_and_validation_time();
|
||||
|
||||
let mut policy = Policy::default();
|
||||
policy.signed_object_failure_policy = SignedObjectFailurePolicy::DropObject;
|
||||
|
||||
let out =
|
||||
process_verified_publication_point_pack(&pack, &policy, &EmptyResolver, validation_time)
|
||||
.expect("drop_object should not fail the publication point");
|
||||
assert!(out.vrps.is_empty());
|
||||
assert!(!out.warnings.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn invalid_aspa_object_is_reported_as_warning_under_drop_object_policy() {
|
||||
let (mut pack, validation_time, resolver) = build_cernet_pack_and_validation_time();
|
||||
|
||||
let uri = "rsync://rpki.cernet.net/repo/cernet/0/INVALID.asa".to_string();
|
||||
pack.files.push(PackFile::from_bytes_compute_sha256(
|
||||
uri.clone(),
|
||||
b"\0\0".to_vec(),
|
||||
));
|
||||
|
||||
let mut policy = Policy::default();
|
||||
policy.signed_object_failure_policy = SignedObjectFailurePolicy::DropObject;
|
||||
|
||||
let out = process_verified_publication_point_pack(&pack, &policy, &resolver, validation_time)
|
||||
.expect("drop_object should not fail");
|
||||
|
||||
assert!(
|
||||
out.warnings
|
||||
.iter()
|
||||
.any(|w| w.context.as_deref() == Some(&uri)),
|
||||
"expected warning for invalid ASPA"
|
||||
);
|
||||
}
|
||||
189
tests/test_objects_policy_m8.rs
Normal file
189
tests/test_objects_policy_m8.rs
Normal file
@@ -0,0 +1,189 @@
|
||||
use std::collections::HashMap;
|
||||
use std::path::Path;
|
||||
|
||||
use rpki::data_model::crl::RpkixCrl;
|
||||
use rpki::data_model::manifest::ManifestObject;
|
||||
use rpki::data_model::rc::ResourceCertificate;
|
||||
use rpki::policy::{Policy, SignedObjectFailurePolicy};
|
||||
use rpki::storage::{PackFile, RocksStore};
|
||||
use rpki::validation::manifest::process_manifest_publication_point;
|
||||
use rpki::validation::objects::{
|
||||
IssuerCaCertificateResolver, ObjectsProcessError, process_verified_publication_point_pack,
|
||||
};
|
||||
|
||||
fn fixture_to_rsync_uri(path: &Path) -> String {
|
||||
let rel = path
|
||||
.strip_prefix("tests/fixtures/repository")
|
||||
.expect("path under tests/fixtures/repository");
|
||||
let mut it = rel.components();
|
||||
let host = it
|
||||
.next()
|
||||
.expect("host component")
|
||||
.as_os_str()
|
||||
.to_string_lossy();
|
||||
let rest = it.as_path().to_string_lossy();
|
||||
format!("rsync://{host}/{rest}")
|
||||
}
|
||||
|
||||
fn fixture_dir_to_rsync_uri(dir: &Path) -> String {
|
||||
let mut s = fixture_to_rsync_uri(dir);
|
||||
if !s.ends_with('/') {
|
||||
s.push('/');
|
||||
}
|
||||
s
|
||||
}
|
||||
|
||||
struct MapResolver {
|
||||
by_subject_dn: HashMap<String, Vec<u8>>,
|
||||
}
|
||||
|
||||
impl IssuerCaCertificateResolver for MapResolver {
|
||||
fn resolve_by_subject_dn(&self, subject_dn: &str) -> Option<Vec<u8>> {
|
||||
self.by_subject_dn.get(subject_dn).cloned()
|
||||
}
|
||||
}
|
||||
|
||||
fn build_cernet_pack_and_validation_time() -> (
|
||||
rpki::storage::VerifiedPublicationPointPack,
|
||||
time::OffsetDateTime,
|
||||
MapResolver,
|
||||
) {
|
||||
let manifest_path = Path::new(
|
||||
"tests/fixtures/repository/rpki.cernet.net/repo/cernet/0/05FC9C5B88506F7C0D3F862C8895BED67E9F8EBA.mft",
|
||||
);
|
||||
let manifest_bytes = std::fs::read(manifest_path).expect("read manifest fixture");
|
||||
let manifest = ManifestObject::decode_der(&manifest_bytes).expect("decode manifest fixture");
|
||||
|
||||
let manifest_rsync_uri = fixture_to_rsync_uri(manifest_path);
|
||||
let publication_point_rsync_uri = fixture_dir_to_rsync_uri(manifest_path.parent().unwrap());
|
||||
|
||||
let temp = tempfile::tempdir().expect("tempdir");
|
||||
let store = RocksStore::open(temp.path()).expect("open rocksdb");
|
||||
|
||||
store
|
||||
.put_raw(&manifest_rsync_uri, &manifest_bytes)
|
||||
.expect("store manifest");
|
||||
for entry in &manifest.manifest.files {
|
||||
let file_path = manifest_path.parent().unwrap().join(&entry.file_name);
|
||||
let bytes = std::fs::read(&file_path)
|
||||
.unwrap_or_else(|_| panic!("read fixture file referenced by manifest: {file_path:?}"));
|
||||
let rsync_uri = format!("{publication_point_rsync_uri}{}", entry.file_name);
|
||||
store.put_raw(&rsync_uri, &bytes).expect("store file");
|
||||
}
|
||||
|
||||
let policy = Policy::default();
|
||||
let out = process_manifest_publication_point(
|
||||
&store,
|
||||
&policy,
|
||||
&manifest_rsync_uri,
|
||||
&publication_point_rsync_uri,
|
||||
manifest.manifest.this_update + time::Duration::seconds(1),
|
||||
)
|
||||
.expect("process manifest publication point");
|
||||
|
||||
let issuer_ca_der = std::fs::read(
|
||||
"tests/fixtures/repository/rpki.apnic.net/repository/B527EF581D6611E2BB468F7C72FD1FF2/BfycW4hQb3wNP4YsiJW-1n6fjro.cer",
|
||||
)
|
||||
.expect("read issuer CA cert fixture");
|
||||
let issuer_ca = ResourceCertificate::decode_der(&issuer_ca_der).expect("decode issuer CA cert");
|
||||
|
||||
let crl_file = out
|
||||
.pack
|
||||
.files
|
||||
.iter()
|
||||
.find(|f| f.rsync_uri.ends_with(".crl"))
|
||||
.expect("crl present in pack");
|
||||
let crl = RpkixCrl::decode_der(&crl_file.bytes).expect("decode crl");
|
||||
|
||||
// Choose a validation_time that is within:
|
||||
// - manifest thisUpdate..nextUpdate (RFC 9286 §6.3)
|
||||
// - issuer CA validity
|
||||
// - CRL thisUpdate..nextUpdate
|
||||
// - and should be within EE validity for most objects.
|
||||
let mut t = manifest.manifest.this_update;
|
||||
if issuer_ca.tbs.validity_not_before > t {
|
||||
t = issuer_ca.tbs.validity_not_before;
|
||||
}
|
||||
if crl.this_update.utc > t {
|
||||
t = crl.this_update.utc;
|
||||
}
|
||||
t += time::Duration::seconds(1);
|
||||
|
||||
let mut resolver = MapResolver {
|
||||
by_subject_dn: HashMap::new(),
|
||||
};
|
||||
resolver
|
||||
.by_subject_dn
|
||||
.insert(issuer_ca.tbs.subject_dn, issuer_ca_der);
|
||||
|
||||
(out.pack, t, resolver)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn drop_object_policy_drops_only_failing_object() {
|
||||
let (mut pack, validation_time, resolver) = build_cernet_pack_and_validation_time();
|
||||
|
||||
let valid_roa_uri = pack
|
||||
.files
|
||||
.iter()
|
||||
.find(|f| f.rsync_uri.ends_with("AS4538.roa"))
|
||||
.map(|f| f.rsync_uri.clone())
|
||||
.expect("AS4538.roa present in pack");
|
||||
|
||||
let tamper_idx = pack
|
||||
.files
|
||||
.iter()
|
||||
.position(|f| f.rsync_uri.ends_with(".roa") && f.rsync_uri != valid_roa_uri)
|
||||
.expect("another ROA present in pack");
|
||||
let victim_uri = pack.files[tamper_idx].rsync_uri.clone();
|
||||
|
||||
let mut tampered = pack.files[tamper_idx].bytes.clone();
|
||||
let last = tampered.len() - 1;
|
||||
tampered[last] ^= 0xFF;
|
||||
pack.files[tamper_idx] = PackFile::from_bytes_compute_sha256(victim_uri.clone(), tampered);
|
||||
|
||||
let mut policy = Policy::default();
|
||||
policy.signed_object_failure_policy = SignedObjectFailurePolicy::DropObject;
|
||||
|
||||
let out = process_verified_publication_point_pack(&pack, &policy, &resolver, validation_time)
|
||||
.expect("drop_object should succeed");
|
||||
|
||||
assert!(
|
||||
out.vrps.iter().any(|v| v.asn == 4538),
|
||||
"expected at least one VRP for AS4538"
|
||||
);
|
||||
assert!(
|
||||
out.warnings
|
||||
.iter()
|
||||
.any(|w| w.context.as_deref() == Some(&victim_uri)),
|
||||
"expected a warning for the tampered object"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn drop_publication_point_policy_fails_the_publication_point() {
|
||||
let (mut pack, validation_time, resolver) = build_cernet_pack_and_validation_time();
|
||||
|
||||
let tamper_idx = pack
|
||||
.files
|
||||
.iter()
|
||||
.position(|f| f.rsync_uri.ends_with(".roa"))
|
||||
.expect("a ROA present in pack");
|
||||
let victim_uri = pack.files[tamper_idx].rsync_uri.clone();
|
||||
|
||||
let mut tampered = pack.files[tamper_idx].bytes.clone();
|
||||
let last = tampered.len() - 1;
|
||||
tampered[last] ^= 0xFF;
|
||||
pack.files[tamper_idx] = PackFile::from_bytes_compute_sha256(victim_uri.clone(), tampered);
|
||||
|
||||
let mut policy = Policy::default();
|
||||
policy.signed_object_failure_policy = SignedObjectFailurePolicy::DropPublicationPoint;
|
||||
|
||||
let err = process_verified_publication_point_pack(&pack, &policy, &resolver, validation_time)
|
||||
.expect_err("drop_publication_point should fail");
|
||||
match err {
|
||||
ObjectsProcessError::PublicationPointDropped { rsync_uri, .. } => {
|
||||
assert_eq!(rsync_uri, victim_uri);
|
||||
}
|
||||
}
|
||||
}
|
||||
463
tests/test_objects_process_pack_for_issuer.rs
Normal file
463
tests/test_objects_process_pack_for_issuer.rs
Normal file
@@ -0,0 +1,463 @@
|
||||
use rpki::fetch::rsync::LocalDirRsyncFetcher;
|
||||
use rpki::policy::{Policy, SignedObjectFailurePolicy, SyncPreference};
|
||||
use rpki::storage::{PackFile, PackTime, RocksStore, VerifiedPublicationPointPack};
|
||||
use rpki::sync::repo::sync_publication_point;
|
||||
use rpki::sync::rrdp::Fetcher;
|
||||
use rpki::validation::manifest::process_manifest_publication_point;
|
||||
use rpki::validation::objects::{
|
||||
IssuerCaCertificateResolver, process_verified_publication_point_pack,
|
||||
process_verified_publication_point_pack_for_issuer,
|
||||
};
|
||||
|
||||
struct NoopHttpFetcher;
|
||||
impl Fetcher for NoopHttpFetcher {
|
||||
fn fetch(&self, _uri: &str) -> Result<Vec<u8>, String> {
|
||||
Err("http disabled in test".to_string())
|
||||
}
|
||||
}
|
||||
|
||||
fn cernet_fixture() -> (std::path::PathBuf, String, String) {
|
||||
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/".to_string();
|
||||
let manifest_file = "05FC9C5B88506F7C0D3F862C8895BED67E9F8EBA.mft".to_string();
|
||||
(dir, rsync_base_uri, manifest_file)
|
||||
}
|
||||
|
||||
fn validation_time_from_manifest_fixture(dir: &std::path::Path, manifest_file: &str) -> time::OffsetDateTime {
|
||||
let bytes = std::fs::read(dir.join(manifest_file)).expect("read manifest fixture");
|
||||
let mft = rpki::data_model::manifest::ManifestObject::decode_der(&bytes).expect("decode mft");
|
||||
let this_update = mft.manifest.this_update;
|
||||
let next_update = mft.manifest.next_update;
|
||||
let candidate = this_update + time::Duration::seconds(60);
|
||||
if candidate < next_update { candidate } else { this_update }
|
||||
}
|
||||
|
||||
fn issuer_ca_fixture() -> Vec<u8> {
|
||||
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")
|
||||
}
|
||||
|
||||
fn issuer_ca_rsync_uri() -> &'static str {
|
||||
"rsync://rpki.apnic.net/repository/B527EF581D6611E2BB468F7C72FD1FF2/BfycW4hQb3wNP4YsiJW-1n6fjro.cer"
|
||||
}
|
||||
|
||||
fn minimal_pack(
|
||||
manifest_rsync_uri: &str,
|
||||
publication_point_rsync_uri: &str,
|
||||
manifest_bytes: Vec<u8>,
|
||||
files: Vec<PackFile>,
|
||||
validation_time: time::OffsetDateTime,
|
||||
) -> VerifiedPublicationPointPack {
|
||||
// Keep times consistent enough to pass internal pack validation.
|
||||
VerifiedPublicationPointPack {
|
||||
format_version: VerifiedPublicationPointPack::FORMAT_VERSION_V1,
|
||||
manifest_rsync_uri: manifest_rsync_uri.to_string(),
|
||||
publication_point_rsync_uri: publication_point_rsync_uri.to_string(),
|
||||
this_update: PackTime::from_utc_offset_datetime(validation_time),
|
||||
next_update: PackTime::from_utc_offset_datetime(validation_time + time::Duration::hours(1)),
|
||||
verified_at: PackTime::from_utc_offset_datetime(validation_time),
|
||||
manifest_bytes,
|
||||
files,
|
||||
}
|
||||
}
|
||||
|
||||
fn build_verified_pack_from_local_rsync_fixture(
|
||||
dir: &std::path::Path,
|
||||
rsync_base_uri: &str,
|
||||
manifest_rsync_uri: &str,
|
||||
validation_time: time::OffsetDateTime,
|
||||
) -> rpki::storage::VerifiedPublicationPointPack {
|
||||
let store_dir = tempfile::tempdir().expect("store dir");
|
||||
let store = RocksStore::open(store_dir.path()).expect("open rocksdb");
|
||||
let policy = Policy {
|
||||
sync_preference: SyncPreference::RsyncOnly,
|
||||
..Policy::default()
|
||||
};
|
||||
|
||||
sync_publication_point(
|
||||
&store,
|
||||
&policy,
|
||||
None,
|
||||
rsync_base_uri,
|
||||
&NoopHttpFetcher,
|
||||
&LocalDirRsyncFetcher::new(dir),
|
||||
)
|
||||
.expect("sync into raw_objects");
|
||||
|
||||
let pp = process_manifest_publication_point(
|
||||
&store,
|
||||
&policy,
|
||||
manifest_rsync_uri,
|
||||
rsync_base_uri,
|
||||
validation_time,
|
||||
)
|
||||
.expect("process manifest");
|
||||
|
||||
pp.pack
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn process_pack_for_issuer_extracts_vrps_from_real_cernet_fixture() {
|
||||
let (dir, rsync_base_uri, manifest_file) = cernet_fixture();
|
||||
let manifest_rsync_uri = format!("{rsync_base_uri}{manifest_file}");
|
||||
let validation_time = validation_time_from_manifest_fixture(&dir, &manifest_file);
|
||||
|
||||
let pack = build_verified_pack_from_local_rsync_fixture(
|
||||
&dir,
|
||||
&rsync_base_uri,
|
||||
&manifest_rsync_uri,
|
||||
validation_time,
|
||||
);
|
||||
|
||||
let issuer_ca_der = issuer_ca_fixture();
|
||||
let issuer_ca = rpki::data_model::rc::ResourceCertificate::decode_der(&issuer_ca_der)
|
||||
.expect("decode issuer ca");
|
||||
|
||||
let policy = Policy::default();
|
||||
let out = process_verified_publication_point_pack_for_issuer(
|
||||
&pack,
|
||||
&policy,
|
||||
&issuer_ca_der,
|
||||
Some(issuer_ca_rsync_uri()),
|
||||
issuer_ca.tbs.extensions.ip_resources.as_ref(),
|
||||
issuer_ca.tbs.extensions.as_resources.as_ref(),
|
||||
validation_time,
|
||||
);
|
||||
|
||||
assert!(out.vrps.len() > 10, "expected many VRPs, got {}", out.vrps.len());
|
||||
assert!(out.aspas.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn signed_object_failure_policy_drop_object_drops_only_bad_object() {
|
||||
let (dir, rsync_base_uri, manifest_file) = cernet_fixture();
|
||||
let manifest_rsync_uri = format!("{rsync_base_uri}{manifest_file}");
|
||||
let validation_time = validation_time_from_manifest_fixture(&dir, &manifest_file);
|
||||
|
||||
let mut pack = build_verified_pack_from_local_rsync_fixture(
|
||||
&dir,
|
||||
&rsync_base_uri,
|
||||
&manifest_rsync_uri,
|
||||
validation_time,
|
||||
);
|
||||
|
||||
let roa_uris = pack
|
||||
.files
|
||||
.iter()
|
||||
.filter(|f| f.rsync_uri.ends_with(".roa"))
|
||||
.map(|f| f.rsync_uri.clone())
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let bad_idx = pack
|
||||
.files
|
||||
.iter()
|
||||
.position(|f| f.rsync_uri.ends_with(".roa"))
|
||||
.expect("pack contains roa");
|
||||
let bad_uri = pack.files[bad_idx].rsync_uri.clone();
|
||||
pack.files[bad_idx] = PackFile::from_bytes_compute_sha256(bad_uri, vec![0u8]);
|
||||
|
||||
let issuer_ca_der = issuer_ca_fixture();
|
||||
let issuer_ca = rpki::data_model::rc::ResourceCertificate::decode_der(&issuer_ca_der)
|
||||
.expect("decode issuer ca");
|
||||
|
||||
let policy = Policy {
|
||||
signed_object_failure_policy: SignedObjectFailurePolicy::DropObject,
|
||||
..Policy::default()
|
||||
};
|
||||
let out = process_verified_publication_point_pack_for_issuer(
|
||||
&pack,
|
||||
&policy,
|
||||
&issuer_ca_der,
|
||||
Some(issuer_ca_rsync_uri()),
|
||||
issuer_ca.tbs.extensions.ip_resources.as_ref(),
|
||||
issuer_ca.tbs.extensions.as_resources.as_ref(),
|
||||
validation_time,
|
||||
);
|
||||
|
||||
assert!(out.vrps.len() > 0);
|
||||
assert!(!out.warnings.is_empty());
|
||||
assert_eq!(
|
||||
out.audit.len(),
|
||||
roa_uris.len(),
|
||||
"expected one audit entry per ROA"
|
||||
);
|
||||
assert!(
|
||||
out.audit.iter().any(|e| e.rsync_uri == pack.files[bad_idx].rsync_uri && matches!(e.result, rpki::audit::AuditObjectResult::Error)),
|
||||
"expected audit error for the corrupted ROA"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn signed_object_failure_policy_drop_publication_point_drops_all_output() {
|
||||
let (dir, rsync_base_uri, manifest_file) = cernet_fixture();
|
||||
let manifest_rsync_uri = format!("{rsync_base_uri}{manifest_file}");
|
||||
let validation_time = validation_time_from_manifest_fixture(&dir, &manifest_file);
|
||||
|
||||
let mut pack = build_verified_pack_from_local_rsync_fixture(
|
||||
&dir,
|
||||
&rsync_base_uri,
|
||||
&manifest_rsync_uri,
|
||||
validation_time,
|
||||
);
|
||||
|
||||
let roa_uris = pack
|
||||
.files
|
||||
.iter()
|
||||
.filter(|f| f.rsync_uri.ends_with(".roa"))
|
||||
.map(|f| f.rsync_uri.clone())
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let bad_idx = pack
|
||||
.files
|
||||
.iter()
|
||||
.position(|f| f.rsync_uri.ends_with(".roa"))
|
||||
.expect("pack contains roa");
|
||||
let bad_uri = pack.files[bad_idx].rsync_uri.clone();
|
||||
pack.files[bad_idx] = PackFile::from_bytes_compute_sha256(bad_uri, vec![0u8]);
|
||||
|
||||
let issuer_ca_der = issuer_ca_fixture();
|
||||
let issuer_ca = rpki::data_model::rc::ResourceCertificate::decode_der(&issuer_ca_der)
|
||||
.expect("decode issuer ca");
|
||||
|
||||
let policy = Policy {
|
||||
signed_object_failure_policy: SignedObjectFailurePolicy::DropPublicationPoint,
|
||||
..Policy::default()
|
||||
};
|
||||
let out = process_verified_publication_point_pack_for_issuer(
|
||||
&pack,
|
||||
&policy,
|
||||
&issuer_ca_der,
|
||||
Some(issuer_ca_rsync_uri()),
|
||||
issuer_ca.tbs.extensions.ip_resources.as_ref(),
|
||||
issuer_ca.tbs.extensions.as_resources.as_ref(),
|
||||
validation_time,
|
||||
);
|
||||
|
||||
assert!(out.vrps.is_empty());
|
||||
assert!(out.aspas.is_empty());
|
||||
assert!(!out.warnings.is_empty());
|
||||
assert_eq!(
|
||||
out.audit.len(),
|
||||
roa_uris.len(),
|
||||
"expected audit entries for all ROAs (error + skipped due to policy)"
|
||||
);
|
||||
let bad_entry = out
|
||||
.audit
|
||||
.iter()
|
||||
.find(|e| e.rsync_uri == pack.files[bad_idx].rsync_uri)
|
||||
.expect("bad roa audit entry");
|
||||
assert!(
|
||||
matches!(bad_entry.result, rpki::audit::AuditObjectResult::Error),
|
||||
"expected error for corrupted ROA"
|
||||
);
|
||||
assert!(
|
||||
out.audit
|
||||
.iter()
|
||||
.any(|e| matches!(e.result, rpki::audit::AuditObjectResult::Skipped)),
|
||||
"expected at least one skipped ROA due to drop_publication_point policy"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn process_pack_for_issuer_without_crl_drops_publication_point() {
|
||||
let (dir, rsync_base_uri, manifest_file) = cernet_fixture();
|
||||
let manifest_rsync_uri = format!("{rsync_base_uri}{manifest_file}");
|
||||
let validation_time = validation_time_from_manifest_fixture(&dir, &manifest_file);
|
||||
let manifest_bytes = std::fs::read(dir.join(&manifest_file)).expect("read mft");
|
||||
|
||||
let pack = minimal_pack(
|
||||
&manifest_rsync_uri,
|
||||
&rsync_base_uri,
|
||||
manifest_bytes,
|
||||
vec![
|
||||
// include a ROA-looking file; it won't be reached because CRL selection fails first
|
||||
PackFile::from_bytes_compute_sha256(format!("{rsync_base_uri}dummy.roa"), vec![1]),
|
||||
],
|
||||
validation_time,
|
||||
);
|
||||
|
||||
let policy = Policy::default();
|
||||
let out = process_verified_publication_point_pack_for_issuer(
|
||||
&pack,
|
||||
&policy,
|
||||
&[],
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
validation_time,
|
||||
);
|
||||
|
||||
assert!(out.vrps.is_empty());
|
||||
assert!(out.aspas.is_empty());
|
||||
assert!(!out.warnings.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn process_pack_for_issuer_handles_invalid_aspa_bytes() {
|
||||
let (dir, rsync_base_uri, manifest_file) = cernet_fixture();
|
||||
let manifest_rsync_uri = format!("{rsync_base_uri}{manifest_file}");
|
||||
let validation_time = validation_time_from_manifest_fixture(&dir, &manifest_file);
|
||||
let manifest_bytes = std::fs::read(dir.join(&manifest_file)).expect("read mft");
|
||||
let crl_bytes = std::fs::read(dir.join(
|
||||
"05FC9C5B88506F7C0D3F862C8895BED67E9F8EBA.crl",
|
||||
))
|
||||
.expect("read crl");
|
||||
|
||||
let pack = minimal_pack(
|
||||
&manifest_rsync_uri,
|
||||
&rsync_base_uri,
|
||||
manifest_bytes,
|
||||
vec![
|
||||
PackFile::from_bytes_compute_sha256(
|
||||
format!("{rsync_base_uri}05FC9C5B88506F7C0D3F862C8895BED67E9F8EBA.crl"),
|
||||
crl_bytes,
|
||||
),
|
||||
PackFile::from_bytes_compute_sha256(format!("{rsync_base_uri}bad.asa"), vec![0u8]),
|
||||
],
|
||||
validation_time,
|
||||
);
|
||||
|
||||
let policy = Policy {
|
||||
signed_object_failure_policy: SignedObjectFailurePolicy::DropObject,
|
||||
..Policy::default()
|
||||
};
|
||||
let out = process_verified_publication_point_pack_for_issuer(
|
||||
&pack,
|
||||
&policy,
|
||||
&[],
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
validation_time,
|
||||
);
|
||||
assert!(out.aspas.is_empty());
|
||||
assert!(!out.warnings.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn process_pack_for_issuer_drop_publication_point_on_invalid_aspa_bytes() {
|
||||
let (dir, rsync_base_uri, manifest_file) = cernet_fixture();
|
||||
let manifest_rsync_uri = format!("{rsync_base_uri}{manifest_file}");
|
||||
let validation_time = validation_time_from_manifest_fixture(&dir, &manifest_file);
|
||||
let manifest_bytes = std::fs::read(dir.join(&manifest_file)).expect("read mft");
|
||||
let crl_bytes = std::fs::read(dir.join(
|
||||
"05FC9C5B88506F7C0D3F862C8895BED67E9F8EBA.crl",
|
||||
))
|
||||
.expect("read crl");
|
||||
|
||||
let pack = minimal_pack(
|
||||
&manifest_rsync_uri,
|
||||
&rsync_base_uri,
|
||||
manifest_bytes,
|
||||
vec![
|
||||
PackFile::from_bytes_compute_sha256(
|
||||
format!("{rsync_base_uri}05FC9C5B88506F7C0D3F862C8895BED67E9F8EBA.crl"),
|
||||
crl_bytes,
|
||||
),
|
||||
PackFile::from_bytes_compute_sha256(format!("{rsync_base_uri}bad.asa"), vec![0u8]),
|
||||
// A ROA-like file: we should not reach it due to policy.
|
||||
PackFile::from_bytes_compute_sha256(format!("{rsync_base_uri}dummy.roa"), vec![1u8]),
|
||||
],
|
||||
validation_time,
|
||||
);
|
||||
|
||||
let policy = Policy {
|
||||
signed_object_failure_policy: SignedObjectFailurePolicy::DropPublicationPoint,
|
||||
..Policy::default()
|
||||
};
|
||||
let out = process_verified_publication_point_pack_for_issuer(
|
||||
&pack,
|
||||
&policy,
|
||||
&[],
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
validation_time,
|
||||
);
|
||||
assert!(out.vrps.is_empty());
|
||||
assert!(out.aspas.is_empty());
|
||||
assert!(!out.warnings.is_empty());
|
||||
}
|
||||
|
||||
struct NoIssuerResolver;
|
||||
impl IssuerCaCertificateResolver for NoIssuerResolver {
|
||||
fn resolve_by_subject_dn(&self, _subject_dn: &str) -> Option<Vec<u8>> {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
struct AlwaysIssuerResolver {
|
||||
issuer_ca_der: Vec<u8>,
|
||||
}
|
||||
|
||||
impl IssuerCaCertificateResolver for AlwaysIssuerResolver {
|
||||
fn resolve_by_subject_dn(&self, _subject_dn: &str) -> Option<Vec<u8>> {
|
||||
Some(self.issuer_ca_der.clone())
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn process_verified_pack_indexes_ca_certs_by_subject() {
|
||||
let (dir, rsync_base_uri, manifest_file) = cernet_fixture();
|
||||
let manifest_rsync_uri = format!("{rsync_base_uri}{manifest_file}");
|
||||
let validation_time = validation_time_from_manifest_fixture(&dir, &manifest_file);
|
||||
let manifest_bytes = std::fs::read(dir.join(&manifest_file)).expect("read mft");
|
||||
|
||||
// Add a real CA certificate to exercise CA indexing logic.
|
||||
let ca_der = issuer_ca_fixture();
|
||||
|
||||
let pack = minimal_pack(
|
||||
&manifest_rsync_uri,
|
||||
&rsync_base_uri,
|
||||
manifest_bytes,
|
||||
vec![PackFile::from_bytes_compute_sha256(
|
||||
format!("{rsync_base_uri}some-ca.cer"),
|
||||
ca_der,
|
||||
)],
|
||||
validation_time,
|
||||
);
|
||||
|
||||
let policy = Policy::default();
|
||||
let out = process_verified_publication_point_pack(
|
||||
&pack,
|
||||
&policy,
|
||||
&NoIssuerResolver,
|
||||
validation_time,
|
||||
)
|
||||
.expect("process pack");
|
||||
assert!(out.vrps.is_empty());
|
||||
assert!(out.aspas.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn process_pack_with_resolver_extracts_vrps_from_real_cernet_fixture() {
|
||||
let (dir, rsync_base_uri, manifest_file) = cernet_fixture();
|
||||
let manifest_rsync_uri = format!("{rsync_base_uri}{manifest_file}");
|
||||
let validation_time = validation_time_from_manifest_fixture(&dir, &manifest_file);
|
||||
|
||||
let pack = build_verified_pack_from_local_rsync_fixture(
|
||||
&dir,
|
||||
&rsync_base_uri,
|
||||
&manifest_rsync_uri,
|
||||
validation_time,
|
||||
);
|
||||
|
||||
let policy = Policy::default();
|
||||
let issuer_ca_der = issuer_ca_fixture();
|
||||
let resolver = AlwaysIssuerResolver {
|
||||
issuer_ca_der: issuer_ca_der.clone(),
|
||||
};
|
||||
|
||||
let out = process_verified_publication_point_pack(&pack, &policy, &resolver, validation_time)
|
||||
.expect("process pack");
|
||||
assert!(out.vrps.len() > 10, "expected many VRPs, got {}", out.vrps.len());
|
||||
assert!(
|
||||
out.audit.len() >= pack.files.iter().filter(|f| f.rsync_uri.ends_with(".roa")).count(),
|
||||
"expected ROA audit entries"
|
||||
);
|
||||
}
|
||||
304
tests/test_objects_processing_coverage_m18.rs
Normal file
304
tests/test_objects_processing_coverage_m18.rs
Normal file
@@ -0,0 +1,304 @@
|
||||
use rpki::policy::{Policy, SignedObjectFailurePolicy};
|
||||
use rpki::storage::{PackFile, PackTime, VerifiedPublicationPointPack};
|
||||
use rpki::validation::objects::{
|
||||
IssuerCaCertificateResolver, ObjectsProcessError, process_verified_publication_point_pack,
|
||||
process_verified_publication_point_pack_for_issuer,
|
||||
};
|
||||
|
||||
fn fixture_bytes(path: &str) -> Vec<u8> {
|
||||
std::fs::read(std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR")).join(path))
|
||||
.unwrap_or_else(|e| panic!("read fixture {path}: {e}"))
|
||||
}
|
||||
|
||||
fn dummy_pack(manifest_bytes: Vec<u8>, files: Vec<PackFile>) -> VerifiedPublicationPointPack {
|
||||
let now = time::OffsetDateTime::now_utc();
|
||||
VerifiedPublicationPointPack {
|
||||
format_version: VerifiedPublicationPointPack::FORMAT_VERSION_V1,
|
||||
manifest_rsync_uri: "rsync://example.test/repo/pp/manifest.mft".to_string(),
|
||||
publication_point_rsync_uri: "rsync://example.test/repo/pp/".to_string(),
|
||||
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,
|
||||
files,
|
||||
}
|
||||
}
|
||||
|
||||
struct NoneResolver;
|
||||
|
||||
impl IssuerCaCertificateResolver for NoneResolver {
|
||||
fn resolve_by_subject_dn(&self, _subject_dn: &str) -> Option<Vec<u8>> {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn process_pack_drop_object_on_missing_issuer_ca_for_roa() {
|
||||
let manifest_bytes = fixture_bytes(
|
||||
"tests/fixtures/repository/rpki.cernet.net/repo/cernet/0/05FC9C5B88506F7C0D3F862C8895BED67E9F8EBA.mft",
|
||||
);
|
||||
let roa_bytes =
|
||||
fixture_bytes("tests/fixtures/repository/rpki.cernet.net/repo/cernet/0/AS4538.roa");
|
||||
|
||||
let pack = dummy_pack(
|
||||
manifest_bytes,
|
||||
vec![PackFile::from_bytes_compute_sha256(
|
||||
"rsync://rpki.cernet.net/repo/cernet/0/AS4538.roa",
|
||||
roa_bytes,
|
||||
)],
|
||||
);
|
||||
|
||||
let policy = Policy {
|
||||
signed_object_failure_policy: SignedObjectFailurePolicy::DropObject,
|
||||
..Policy::default()
|
||||
};
|
||||
|
||||
let out =
|
||||
process_verified_publication_point_pack(&pack, &policy, &NoneResolver, time::OffsetDateTime::now_utc())
|
||||
.expect("drop_object should not error");
|
||||
|
||||
assert_eq!(out.stats.roa_total, 1);
|
||||
assert_eq!(out.stats.roa_ok, 0);
|
||||
assert_eq!(out.audit.len(), 1);
|
||||
assert_eq!(out.warnings.len(), 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn process_pack_drop_publication_point_on_missing_issuer_ca_for_roa_skips_rest() {
|
||||
let manifest_bytes = fixture_bytes(
|
||||
"tests/fixtures/repository/rpki.cernet.net/repo/cernet/0/05FC9C5B88506F7C0D3F862C8895BED67E9F8EBA.mft",
|
||||
);
|
||||
let roa_bytes =
|
||||
fixture_bytes("tests/fixtures/repository/rpki.cernet.net/repo/cernet/0/AS4538.roa");
|
||||
let aspa_bytes = fixture_bytes(
|
||||
"tests/fixtures/repository/chloe.sobornost.net/rpki/RIPE-nljobsnijders/5m80fwYws_3FiFD7JiQjAqZ1RYQ.asa",
|
||||
);
|
||||
|
||||
let pack = dummy_pack(
|
||||
manifest_bytes,
|
||||
vec![
|
||||
PackFile::from_bytes_compute_sha256(
|
||||
"rsync://example.test/repo/pp/first.roa",
|
||||
roa_bytes.clone(),
|
||||
),
|
||||
PackFile::from_bytes_compute_sha256(
|
||||
"rsync://example.test/repo/pp/second.roa",
|
||||
roa_bytes,
|
||||
),
|
||||
PackFile::from_bytes_compute_sha256(
|
||||
"rsync://example.test/repo/pp/x.asa",
|
||||
aspa_bytes,
|
||||
),
|
||||
],
|
||||
);
|
||||
|
||||
let policy = Policy {
|
||||
signed_object_failure_policy: SignedObjectFailurePolicy::DropPublicationPoint,
|
||||
..Policy::default()
|
||||
};
|
||||
|
||||
let err =
|
||||
process_verified_publication_point_pack(&pack, &policy, &NoneResolver, time::OffsetDateTime::now_utc())
|
||||
.unwrap_err();
|
||||
assert!(matches!(err, ObjectsProcessError::PublicationPointDropped { .. }));
|
||||
assert!(err.to_string().contains("drop_publication_point"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn process_pack_drop_object_on_missing_issuer_ca_for_aspa() {
|
||||
let manifest_bytes = fixture_bytes(
|
||||
"tests/fixtures/repository/rpki.cernet.net/repo/cernet/0/05FC9C5B88506F7C0D3F862C8895BED67E9F8EBA.mft",
|
||||
);
|
||||
let aspa_bytes = fixture_bytes(
|
||||
"tests/fixtures/repository/chloe.sobornost.net/rpki/RIPE-nljobsnijders/5m80fwYws_3FiFD7JiQjAqZ1RYQ.asa",
|
||||
);
|
||||
|
||||
let pack = dummy_pack(
|
||||
manifest_bytes,
|
||||
vec![PackFile::from_bytes_compute_sha256(
|
||||
"rsync://example.test/repo/pp/x.asa",
|
||||
aspa_bytes,
|
||||
)],
|
||||
);
|
||||
|
||||
let policy = Policy {
|
||||
signed_object_failure_policy: SignedObjectFailurePolicy::DropObject,
|
||||
..Policy::default()
|
||||
};
|
||||
|
||||
let out =
|
||||
process_verified_publication_point_pack(&pack, &policy, &NoneResolver, time::OffsetDateTime::now_utc())
|
||||
.expect("drop_object should not error");
|
||||
|
||||
assert_eq!(out.stats.aspa_total, 1);
|
||||
assert_eq!(out.stats.aspa_ok, 0);
|
||||
assert_eq!(out.audit.len(), 1);
|
||||
assert_eq!(out.warnings.len(), 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn process_pack_drop_publication_point_on_missing_issuer_ca_for_aspa_skips_rest() {
|
||||
let manifest_bytes = fixture_bytes(
|
||||
"tests/fixtures/repository/rpki.cernet.net/repo/cernet/0/05FC9C5B88506F7C0D3F862C8895BED67E9F8EBA.mft",
|
||||
);
|
||||
let roa_bytes =
|
||||
fixture_bytes("tests/fixtures/repository/rpki.cernet.net/repo/cernet/0/AS4538.roa");
|
||||
let aspa_bytes = fixture_bytes(
|
||||
"tests/fixtures/repository/chloe.sobornost.net/rpki/RIPE-nljobsnijders/5m80fwYws_3FiFD7JiQjAqZ1RYQ.asa",
|
||||
);
|
||||
|
||||
let pack = dummy_pack(
|
||||
manifest_bytes,
|
||||
vec![
|
||||
PackFile::from_bytes_compute_sha256(
|
||||
"rsync://example.test/repo/pp/x.asa",
|
||||
aspa_bytes,
|
||||
),
|
||||
PackFile::from_bytes_compute_sha256(
|
||||
"rsync://example.test/repo/pp/y.roa",
|
||||
roa_bytes,
|
||||
),
|
||||
],
|
||||
);
|
||||
|
||||
let policy = Policy {
|
||||
signed_object_failure_policy: SignedObjectFailurePolicy::DropPublicationPoint,
|
||||
..Policy::default()
|
||||
};
|
||||
|
||||
let err =
|
||||
process_verified_publication_point_pack(&pack, &policy, &NoneResolver, time::OffsetDateTime::now_utc())
|
||||
.unwrap_err();
|
||||
assert!(matches!(err, ObjectsProcessError::PublicationPointDropped { .. }));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn process_pack_for_issuer_marks_objects_skipped_when_missing_issuer_crl() {
|
||||
let manifest_bytes = fixture_bytes(
|
||||
"tests/fixtures/repository/rpki.cernet.net/repo/cernet/0/05FC9C5B88506F7C0D3F862C8895BED67E9F8EBA.mft",
|
||||
);
|
||||
let roa_bytes =
|
||||
fixture_bytes("tests/fixtures/repository/rpki.cernet.net/repo/cernet/0/AS4538.roa");
|
||||
let aspa_bytes = fixture_bytes(
|
||||
"tests/fixtures/repository/chloe.sobornost.net/rpki/RIPE-nljobsnijders/5m80fwYws_3FiFD7JiQjAqZ1RYQ.asa",
|
||||
);
|
||||
|
||||
let pack = dummy_pack(
|
||||
manifest_bytes,
|
||||
vec![
|
||||
PackFile::from_bytes_compute_sha256(
|
||||
"rsync://example.test/repo/pp/a.roa",
|
||||
roa_bytes,
|
||||
),
|
||||
PackFile::from_bytes_compute_sha256(
|
||||
"rsync://example.test/repo/pp/a.asa",
|
||||
aspa_bytes,
|
||||
),
|
||||
],
|
||||
);
|
||||
|
||||
let policy = Policy {
|
||||
signed_object_failure_policy: SignedObjectFailurePolicy::DropObject,
|
||||
..Policy::default()
|
||||
};
|
||||
|
||||
let out = process_verified_publication_point_pack_for_issuer(
|
||||
&pack,
|
||||
&policy,
|
||||
&[0x01, 0x02, 0x03],
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
time::OffsetDateTime::now_utc(),
|
||||
);
|
||||
|
||||
assert!(out.stats.publication_point_dropped);
|
||||
assert_eq!(out.stats.roa_total, 1);
|
||||
assert_eq!(out.stats.aspa_total, 1);
|
||||
assert_eq!(out.vrps.len(), 0);
|
||||
assert_eq!(out.aspas.len(), 0);
|
||||
assert_eq!(out.audit.len(), 2);
|
||||
assert_eq!(out.warnings.len(), 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn process_pack_for_issuer_drop_object_records_errors_and_continues() {
|
||||
let manifest_bytes = fixture_bytes(
|
||||
"tests/fixtures/repository/rpki.cernet.net/repo/cernet/0/05FC9C5B88506F7C0D3F862C8895BED67E9F8EBA.mft",
|
||||
);
|
||||
|
||||
let pack = dummy_pack(
|
||||
manifest_bytes,
|
||||
vec![
|
||||
PackFile::from_bytes_compute_sha256(
|
||||
"rsync://example.test/repo/pp/issuer.crl",
|
||||
vec![0x01],
|
||||
),
|
||||
PackFile::from_bytes_compute_sha256("rsync://example.test/repo/pp/a.roa", vec![0x00]),
|
||||
PackFile::from_bytes_compute_sha256("rsync://example.test/repo/pp/b.asa", vec![0x00]),
|
||||
],
|
||||
);
|
||||
|
||||
let policy = Policy {
|
||||
signed_object_failure_policy: SignedObjectFailurePolicy::DropObject,
|
||||
..Policy::default()
|
||||
};
|
||||
|
||||
let out = process_verified_publication_point_pack_for_issuer(
|
||||
&pack,
|
||||
&policy,
|
||||
&[0x01, 0x02, 0x03],
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
time::OffsetDateTime::now_utc(),
|
||||
);
|
||||
|
||||
assert!(!out.stats.publication_point_dropped);
|
||||
assert_eq!(out.stats.roa_total, 1);
|
||||
assert_eq!(out.stats.aspa_total, 1);
|
||||
assert_eq!(out.audit.len(), 2);
|
||||
assert_eq!(out.warnings.len(), 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn process_pack_for_issuer_drop_publication_point_records_skips_for_rest() {
|
||||
let manifest_bytes = fixture_bytes(
|
||||
"tests/fixtures/repository/rpki.cernet.net/repo/cernet/0/05FC9C5B88506F7C0D3F862C8895BED67E9F8EBA.mft",
|
||||
);
|
||||
|
||||
let pack = dummy_pack(
|
||||
manifest_bytes,
|
||||
vec![
|
||||
PackFile::from_bytes_compute_sha256(
|
||||
"rsync://example.test/repo/pp/issuer.crl",
|
||||
vec![0x01],
|
||||
),
|
||||
PackFile::from_bytes_compute_sha256("rsync://example.test/repo/pp/a.roa", vec![0x00]),
|
||||
PackFile::from_bytes_compute_sha256("rsync://example.test/repo/pp/b.roa", vec![0x00]),
|
||||
PackFile::from_bytes_compute_sha256("rsync://example.test/repo/pp/c.asa", vec![0x00]),
|
||||
],
|
||||
);
|
||||
|
||||
let policy = Policy {
|
||||
signed_object_failure_policy: SignedObjectFailurePolicy::DropPublicationPoint,
|
||||
..Policy::default()
|
||||
};
|
||||
|
||||
let out = process_verified_publication_point_pack_for_issuer(
|
||||
&pack,
|
||||
&policy,
|
||||
&[0x01, 0x02, 0x03],
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
time::OffsetDateTime::now_utc(),
|
||||
);
|
||||
|
||||
assert!(out.stats.publication_point_dropped);
|
||||
assert_eq!(out.stats.roa_total, 2);
|
||||
assert_eq!(out.stats.aspa_total, 1);
|
||||
assert!(out.audit.len() >= 3, "expected error + skipped entries");
|
||||
assert_eq!(out.warnings.len(), 1);
|
||||
}
|
||||
|
||||
37
tests/test_policy.rs
Normal file
37
tests/test_policy.rs
Normal file
@@ -0,0 +1,37 @@
|
||||
use rpki::policy::{CaFailedFetchPolicy, Policy, SignedObjectFailurePolicy, SyncPreference};
|
||||
|
||||
#[test]
|
||||
fn policy_defaults_are_correct() {
|
||||
let p = Policy::default();
|
||||
assert_eq!(p.sync_preference, SyncPreference::RrdpThenRsync);
|
||||
assert_eq!(
|
||||
p.ca_failed_fetch_policy,
|
||||
CaFailedFetchPolicy::UseVerifiedCache
|
||||
);
|
||||
assert_eq!(
|
||||
p.signed_object_failure_policy,
|
||||
SignedObjectFailurePolicy::DropObject
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn policy_toml_parsing_supports_overrides() {
|
||||
let toml = r#"
|
||||
sync_preference = "rsync_only"
|
||||
ca_failed_fetch_policy = "stop_all_output"
|
||||
signed_object_failure_policy = "drop_publication_point"
|
||||
"#;
|
||||
let p = Policy::from_toml_str(toml).expect("parse TOML policy");
|
||||
assert_eq!(p.sync_preference, SyncPreference::RsyncOnly);
|
||||
assert_eq!(p.ca_failed_fetch_policy, CaFailedFetchPolicy::StopAllOutput);
|
||||
assert_eq!(
|
||||
p.signed_object_failure_policy,
|
||||
SignedObjectFailurePolicy::DropPublicationPoint
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn policy_toml_parsing_uses_defaults_when_missing() {
|
||||
let p = Policy::from_toml_str("").expect("parse empty TOML policy");
|
||||
assert_eq!(p, Policy::default());
|
||||
}
|
||||
270
tests/test_repo_sync_m6.rs
Normal file
270
tests/test_repo_sync_m6.rs
Normal file
@@ -0,0 +1,270 @@
|
||||
use std::collections::HashMap;
|
||||
use std::sync::{Arc, Mutex};
|
||||
|
||||
use rpki::fetch::rsync::{LocalDirRsyncFetcher, RsyncFetchError, RsyncFetcher};
|
||||
use rpki::policy::{Policy, SyncPreference};
|
||||
use rpki::storage::RocksStore;
|
||||
use rpki::sync::repo::{RepoSyncSource, sync_publication_point};
|
||||
use rpki::sync::rrdp::Fetcher;
|
||||
|
||||
struct MapFetcher {
|
||||
by_uri: HashMap<String, Vec<u8>>,
|
||||
}
|
||||
|
||||
impl Fetcher for MapFetcher {
|
||||
fn fetch(&self, uri: &str) -> Result<Vec<u8>, String> {
|
||||
self.by_uri
|
||||
.get(uri)
|
||||
.cloned()
|
||||
.ok_or_else(|| format!("not found: {uri}"))
|
||||
}
|
||||
}
|
||||
|
||||
struct CountingRsyncFetcher<F> {
|
||||
inner: F,
|
||||
calls: Arc<Mutex<usize>>,
|
||||
}
|
||||
|
||||
impl<F> CountingRsyncFetcher<F> {
|
||||
fn new(inner: F) -> (Self, Arc<Mutex<usize>>) {
|
||||
let calls = Arc::new(Mutex::new(0usize));
|
||||
(
|
||||
Self {
|
||||
inner,
|
||||
calls: calls.clone(),
|
||||
},
|
||||
calls,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl<F: RsyncFetcher> RsyncFetcher for CountingRsyncFetcher<F> {
|
||||
fn fetch_objects(
|
||||
&self,
|
||||
rsync_base_uri: &str,
|
||||
) -> Result<Vec<(String, Vec<u8>)>, RsyncFetchError> {
|
||||
*self.calls.lock().unwrap() += 1;
|
||||
self.inner.fetch_objects(rsync_base_uri)
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn repo_sync_uses_rrdp_when_available() {
|
||||
let notification_xml =
|
||||
std::fs::read("tests/fixtures/rrdp/notification.xml").expect("read notification");
|
||||
let snapshot_xml = std::fs::read("tests/fixtures/rrdp/snapshot.xml").expect("read snapshot");
|
||||
|
||||
let http_fetcher = MapFetcher {
|
||||
by_uri: HashMap::from([
|
||||
(
|
||||
"https://example.net/rrdp/notification.xml".to_string(),
|
||||
notification_xml,
|
||||
),
|
||||
(
|
||||
"https://example.net/rrdp/snapshot.xml".to_string(),
|
||||
snapshot_xml,
|
||||
),
|
||||
]),
|
||||
};
|
||||
|
||||
let temp = tempfile::tempdir().expect("tempdir");
|
||||
let store = RocksStore::open(temp.path()).expect("open rocksdb");
|
||||
|
||||
let local_repo = tempfile::tempdir().expect("local repo dir");
|
||||
std::fs::write(local_repo.path().join("x.cer"), b"x").unwrap();
|
||||
let (rsync_fetcher, calls) =
|
||||
CountingRsyncFetcher::new(LocalDirRsyncFetcher::new(local_repo.path()));
|
||||
|
||||
let policy = Policy::default();
|
||||
let out = sync_publication_point(
|
||||
&store,
|
||||
&policy,
|
||||
Some("https://example.net/rrdp/notification.xml"),
|
||||
"rsync://example.net/repo/",
|
||||
&http_fetcher,
|
||||
&rsync_fetcher,
|
||||
)
|
||||
.expect("sync");
|
||||
|
||||
assert_eq!(out.source, RepoSyncSource::Rrdp);
|
||||
assert_eq!(out.objects_written, 2);
|
||||
assert_eq!(*calls.lock().unwrap(), 0);
|
||||
|
||||
assert_eq!(
|
||||
store
|
||||
.get_raw("rsync://example.net/repo/obj1.cer")
|
||||
.unwrap()
|
||||
.unwrap(),
|
||||
b"abc"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn repo_sync_skips_snapshot_when_state_unchanged() {
|
||||
let notification_xml =
|
||||
std::fs::read("tests/fixtures/rrdp/notification.xml").expect("read notification");
|
||||
let snapshot_xml = std::fs::read("tests/fixtures/rrdp/snapshot.xml").expect("read snapshot");
|
||||
|
||||
let http_fetcher = MapFetcher {
|
||||
by_uri: HashMap::from([
|
||||
(
|
||||
"https://example.net/rrdp/notification.xml".to_string(),
|
||||
notification_xml,
|
||||
),
|
||||
(
|
||||
"https://example.net/rrdp/snapshot.xml".to_string(),
|
||||
snapshot_xml,
|
||||
),
|
||||
]),
|
||||
};
|
||||
|
||||
let temp = tempfile::tempdir().expect("tempdir");
|
||||
let store = RocksStore::open(temp.path()).expect("open rocksdb");
|
||||
|
||||
let local_repo = tempfile::tempdir().expect("local repo dir");
|
||||
std::fs::write(local_repo.path().join("x.cer"), b"x").unwrap();
|
||||
let (rsync_fetcher, calls) =
|
||||
CountingRsyncFetcher::new(LocalDirRsyncFetcher::new(local_repo.path()));
|
||||
|
||||
let policy = Policy::default();
|
||||
|
||||
let out1 = sync_publication_point(
|
||||
&store,
|
||||
&policy,
|
||||
Some("https://example.net/rrdp/notification.xml"),
|
||||
"rsync://example.net/repo/",
|
||||
&http_fetcher,
|
||||
&rsync_fetcher,
|
||||
)
|
||||
.expect("sync 1");
|
||||
assert_eq!(out1.source, RepoSyncSource::Rrdp);
|
||||
assert_eq!(out1.objects_written, 2);
|
||||
|
||||
let out2 = sync_publication_point(
|
||||
&store,
|
||||
&policy,
|
||||
Some("https://example.net/rrdp/notification.xml"),
|
||||
"rsync://example.net/repo/",
|
||||
&http_fetcher,
|
||||
&rsync_fetcher,
|
||||
)
|
||||
.expect("sync 2");
|
||||
assert_eq!(out2.source, RepoSyncSource::Rrdp);
|
||||
assert_eq!(
|
||||
out2.objects_written, 0,
|
||||
"expected to skip snapshot apply when state unchanged"
|
||||
);
|
||||
assert_eq!(
|
||||
*calls.lock().unwrap(),
|
||||
0,
|
||||
"expected no rsync fallback calls"
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
store
|
||||
.get_raw("rsync://example.net/repo/obj1.cer")
|
||||
.unwrap()
|
||||
.unwrap(),
|
||||
b"abc"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn repo_sync_falls_back_to_rsync_on_rrdp_failure() {
|
||||
// Provide notification, but omit snapshot, causing RRDP fetch failure.
|
||||
let notification_xml =
|
||||
std::fs::read("tests/fixtures/rrdp/notification.xml").expect("read notification");
|
||||
let http_fetcher = MapFetcher {
|
||||
by_uri: HashMap::from([(
|
||||
"https://example.net/rrdp/notification.xml".to_string(),
|
||||
notification_xml,
|
||||
)]),
|
||||
};
|
||||
|
||||
let temp = tempfile::tempdir().expect("tempdir");
|
||||
let store = RocksStore::open(temp.path()).expect("open rocksdb");
|
||||
|
||||
let local_repo = tempfile::tempdir().expect("local repo dir");
|
||||
std::fs::create_dir_all(local_repo.path().join("sub")).unwrap();
|
||||
std::fs::write(local_repo.path().join("sub/obj.cer"), b"hello").unwrap();
|
||||
|
||||
let (rsync_fetcher, calls) =
|
||||
CountingRsyncFetcher::new(LocalDirRsyncFetcher::new(local_repo.path()));
|
||||
|
||||
let policy = Policy::default();
|
||||
let out = sync_publication_point(
|
||||
&store,
|
||||
&policy,
|
||||
Some("https://example.net/rrdp/notification.xml"),
|
||||
"rsync://example.net/repo/",
|
||||
&http_fetcher,
|
||||
&rsync_fetcher,
|
||||
)
|
||||
.expect("fallback sync");
|
||||
|
||||
assert_eq!(out.source, RepoSyncSource::Rsync);
|
||||
assert_eq!(out.objects_written, 1);
|
||||
assert_eq!(*calls.lock().unwrap(), 1);
|
||||
assert!(!out.warnings.is_empty());
|
||||
assert!(
|
||||
out.warnings[0]
|
||||
.rfc_refs
|
||||
.iter()
|
||||
.any(|r| r.0 == "RFC 8182 §3.4.5")
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
store
|
||||
.get_raw("rsync://example.net/repo/sub/obj.cer")
|
||||
.unwrap()
|
||||
.unwrap(),
|
||||
b"hello"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn repo_sync_rsync_populates_raw_objects() {
|
||||
let temp = tempfile::tempdir().expect("tempdir");
|
||||
let store = RocksStore::open(temp.path()).expect("open rocksdb");
|
||||
|
||||
let local_repo = tempfile::tempdir().expect("local repo dir");
|
||||
std::fs::create_dir_all(local_repo.path().join("a/b")).unwrap();
|
||||
std::fs::write(local_repo.path().join("a/one.cer"), b"1").unwrap();
|
||||
std::fs::write(local_repo.path().join("a/b/two.crl"), b"2").unwrap();
|
||||
|
||||
let http_fetcher = MapFetcher {
|
||||
by_uri: HashMap::new(),
|
||||
};
|
||||
let rsync_fetcher = LocalDirRsyncFetcher::new(local_repo.path());
|
||||
|
||||
let mut policy = Policy::default();
|
||||
policy.sync_preference = SyncPreference::RsyncOnly;
|
||||
|
||||
let out = sync_publication_point(
|
||||
&store,
|
||||
&policy,
|
||||
None,
|
||||
"rsync://example.net/repo/",
|
||||
&http_fetcher,
|
||||
&rsync_fetcher,
|
||||
)
|
||||
.expect("rsync-only sync");
|
||||
|
||||
assert_eq!(out.source, RepoSyncSource::Rsync);
|
||||
assert_eq!(out.objects_written, 2);
|
||||
|
||||
assert_eq!(
|
||||
store
|
||||
.get_raw("rsync://example.net/repo/a/one.cer")
|
||||
.unwrap()
|
||||
.unwrap(),
|
||||
b"1"
|
||||
);
|
||||
assert_eq!(
|
||||
store
|
||||
.get_raw("rsync://example.net/repo/a/b/two.crl")
|
||||
.unwrap()
|
||||
.unwrap(),
|
||||
b"2"
|
||||
);
|
||||
}
|
||||
@@ -28,6 +28,9 @@ fn dummy_ee(
|
||||
extensions: RcExtensions {
|
||||
basic_constraints_ca: false,
|
||||
subject_key_identifier: Some(vec![0x01]),
|
||||
authority_key_identifier: None,
|
||||
crl_distribution_points_uris: None,
|
||||
ca_issuers_uris: None,
|
||||
subject_info_access: Some(SubjectInfoAccess::Ca(
|
||||
rpki::data_model::rc::SubjectInfoAccessCa {
|
||||
access_descriptions: vec![],
|
||||
|
||||
169
tests/test_rrdp_sync_m5.rs
Normal file
169
tests/test_rrdp_sync_m5.rs
Normal file
@@ -0,0 +1,169 @@
|
||||
use std::collections::HashMap;
|
||||
|
||||
use rpki::storage::RocksStore;
|
||||
use rpki::sync::rrdp::{Fetcher, parse_notification_snapshot, sync_from_notification_snapshot};
|
||||
use sha2::Digest;
|
||||
|
||||
struct MapFetcher {
|
||||
by_uri: HashMap<String, Vec<u8>>,
|
||||
}
|
||||
|
||||
impl Fetcher for MapFetcher {
|
||||
fn fetch(&self, uri: &str) -> Result<Vec<u8>, String> {
|
||||
self.by_uri
|
||||
.get(uri)
|
||||
.cloned()
|
||||
.ok_or_else(|| format!("not found: {uri}"))
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn notification_parses_and_snapshot_is_applied_and_state_written() {
|
||||
let notification_xml =
|
||||
std::fs::read("tests/fixtures/rrdp/notification.xml").expect("read notification");
|
||||
let snapshot_xml = std::fs::read("tests/fixtures/rrdp/snapshot.xml").expect("read snapshot");
|
||||
|
||||
let notif = parse_notification_snapshot(¬ification_xml).expect("parse notification");
|
||||
assert_eq!(notif.serial, 1);
|
||||
assert_eq!(notif.snapshot_uri, "https://example.net/rrdp/snapshot.xml");
|
||||
|
||||
let mut fetcher = MapFetcher {
|
||||
by_uri: HashMap::new(),
|
||||
};
|
||||
fetcher.by_uri.insert(
|
||||
"https://example.net/rrdp/snapshot.xml".to_string(),
|
||||
snapshot_xml,
|
||||
);
|
||||
|
||||
let temp = tempfile::tempdir().expect("tempdir");
|
||||
let store = RocksStore::open(temp.path()).expect("open rocksdb");
|
||||
|
||||
let published = sync_from_notification_snapshot(
|
||||
&store,
|
||||
"https://example.net/rrdp/notification.xml",
|
||||
¬ification_xml,
|
||||
&fetcher,
|
||||
)
|
||||
.expect("sync");
|
||||
assert_eq!(published, 2);
|
||||
|
||||
let obj1 = store
|
||||
.get_raw("rsync://example.net/repo/obj1.cer")
|
||||
.expect("get obj1")
|
||||
.expect("obj1 exists");
|
||||
assert_eq!(obj1, b"abc");
|
||||
|
||||
let obj2 = store
|
||||
.get_raw("rsync://example.net/repo/obj2.crl")
|
||||
.expect("get obj2")
|
||||
.expect("obj2 exists");
|
||||
assert_eq!(obj2, b"def");
|
||||
|
||||
let state_bytes = store
|
||||
.get_rrdp_state("https://example.net/rrdp/notification.xml")
|
||||
.expect("get state")
|
||||
.expect("state exists");
|
||||
let state = rpki::sync::rrdp::RrdpState::decode(&state_bytes).expect("decode state");
|
||||
assert_eq!(
|
||||
state.session_id,
|
||||
"9df4b597-af9e-4dca-bdda-719cce2c4e28".to_string()
|
||||
);
|
||||
assert_eq!(state.serial, 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn snapshot_hash_mismatch_is_rejected() {
|
||||
let mut notification_xml =
|
||||
String::from_utf8(std::fs::read("tests/fixtures/rrdp/notification.xml").unwrap()).unwrap();
|
||||
notification_xml = notification_xml.replace(
|
||||
"dcb1ce91401d568d7ddf7a4c9f70c65d8428c3a5e7135f82db99c4de30413551",
|
||||
"0000000000000000000000000000000000000000000000000000000000000000",
|
||||
);
|
||||
|
||||
let snapshot_xml = std::fs::read("tests/fixtures/rrdp/snapshot.xml").expect("read snapshot");
|
||||
let fetcher = MapFetcher {
|
||||
by_uri: HashMap::from([(
|
||||
"https://example.net/rrdp/snapshot.xml".to_string(),
|
||||
snapshot_xml,
|
||||
)]),
|
||||
};
|
||||
|
||||
let temp = tempfile::tempdir().expect("tempdir");
|
||||
let store = RocksStore::open(temp.path()).expect("open rocksdb");
|
||||
let err = sync_from_notification_snapshot(
|
||||
&store,
|
||||
"https://example.net/rrdp/notification.xml",
|
||||
notification_xml.as_bytes(),
|
||||
&fetcher,
|
||||
)
|
||||
.expect_err("hash mismatch rejected");
|
||||
assert!(err.to_string().contains("hash mismatch"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn session_id_mismatch_is_rejected() {
|
||||
let notification_xml =
|
||||
std::fs::read("tests/fixtures/rrdp/notification.xml").expect("read notification");
|
||||
let mut snapshot_xml =
|
||||
String::from_utf8(std::fs::read("tests/fixtures/rrdp/snapshot.xml").unwrap()).unwrap();
|
||||
snapshot_xml = snapshot_xml.replace(
|
||||
"9df4b597-af9e-4dca-bdda-719cce2c4e28",
|
||||
"00000000-0000-4000-8000-000000000000",
|
||||
);
|
||||
let snapshot_xml = snapshot_xml.into_bytes();
|
||||
|
||||
// Recompute snapshot hash and patch notification to keep hash correct, so we test
|
||||
// the session_id mismatch check.
|
||||
let mut notif_str = String::from_utf8(notification_xml).unwrap();
|
||||
let digest = sha2::Sha256::digest(&snapshot_xml);
|
||||
let hex_hash = hex::encode(digest);
|
||||
notif_str = notif_str.replace(
|
||||
"dcb1ce91401d568d7ddf7a4c9f70c65d8428c3a5e7135f82db99c4de30413551",
|
||||
&hex_hash,
|
||||
);
|
||||
|
||||
let fetcher = MapFetcher {
|
||||
by_uri: HashMap::from([(
|
||||
"https://example.net/rrdp/snapshot.xml".to_string(),
|
||||
snapshot_xml,
|
||||
)]),
|
||||
};
|
||||
|
||||
let temp = tempfile::tempdir().expect("tempdir");
|
||||
let store = RocksStore::open(temp.path()).expect("open rocksdb");
|
||||
let err = sync_from_notification_snapshot(
|
||||
&store,
|
||||
"https://example.net/rrdp/notification.xml",
|
||||
notif_str.as_bytes(),
|
||||
&fetcher,
|
||||
)
|
||||
.expect_err("session_id mismatch rejected");
|
||||
|
||||
assert!(err.to_string().contains("session_id mismatch"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn serial_zero_is_rejected() {
|
||||
let notification_xml =
|
||||
String::from_utf8(std::fs::read("tests/fixtures/rrdp/notification.xml").unwrap()).unwrap();
|
||||
let notification_xml = notification_xml.replace("serial=\"1\"", "serial=\"0\"");
|
||||
|
||||
let snapshot_xml = std::fs::read("tests/fixtures/rrdp/snapshot.xml").expect("read snapshot");
|
||||
let fetcher = MapFetcher {
|
||||
by_uri: HashMap::from([(
|
||||
"https://example.net/rrdp/snapshot.xml".to_string(),
|
||||
snapshot_xml,
|
||||
)]),
|
||||
};
|
||||
|
||||
let temp = tempfile::tempdir().expect("tempdir");
|
||||
let store = RocksStore::open(temp.path()).expect("open rocksdb");
|
||||
let err = sync_from_notification_snapshot(
|
||||
&store,
|
||||
"https://example.net/rrdp/notification.xml",
|
||||
notification_xml.as_bytes(),
|
||||
&fetcher,
|
||||
)
|
||||
.expect_err("serial=0 rejected");
|
||||
assert!(err.to_string().contains("serial invalid"));
|
||||
}
|
||||
128
tests/test_run_m9.rs
Normal file
128
tests/test_run_m9.rs
Normal file
@@ -0,0 +1,128 @@
|
||||
use std::collections::HashMap;
|
||||
use std::path::Path;
|
||||
|
||||
use rpki::data_model::crl::RpkixCrl;
|
||||
use rpki::data_model::manifest::ManifestObject;
|
||||
use rpki::data_model::rc::ResourceCertificate;
|
||||
use rpki::fetch::rsync::LocalDirRsyncFetcher;
|
||||
use rpki::policy::{Policy, SyncPreference};
|
||||
use rpki::storage::RocksStore;
|
||||
use rpki::sync::rrdp::Fetcher;
|
||||
use rpki::validation::objects::IssuerCaCertificateResolver;
|
||||
use rpki::validation::run::{run_publication_point_once, verified_pack_exists};
|
||||
|
||||
fn fixture_to_rsync_uri(path: &Path) -> String {
|
||||
let rel = path
|
||||
.strip_prefix("tests/fixtures/repository")
|
||||
.expect("path under tests/fixtures/repository");
|
||||
let mut it = rel.components();
|
||||
let host = it
|
||||
.next()
|
||||
.expect("host component")
|
||||
.as_os_str()
|
||||
.to_string_lossy();
|
||||
let rest = it.as_path().to_string_lossy();
|
||||
format!("rsync://{host}/{rest}")
|
||||
}
|
||||
|
||||
fn fixture_dir_to_rsync_uri(dir: &Path) -> String {
|
||||
let mut s = fixture_to_rsync_uri(dir);
|
||||
if !s.ends_with('/') {
|
||||
s.push('/');
|
||||
}
|
||||
s
|
||||
}
|
||||
|
||||
struct NeverHttpFetcher;
|
||||
|
||||
impl Fetcher for NeverHttpFetcher {
|
||||
fn fetch(&self, _uri: &str) -> Result<Vec<u8>, String> {
|
||||
Err("http fetch disabled in offline test".to_string())
|
||||
}
|
||||
}
|
||||
|
||||
struct MapResolver {
|
||||
by_subject_dn: HashMap<String, Vec<u8>>,
|
||||
}
|
||||
|
||||
impl IssuerCaCertificateResolver for MapResolver {
|
||||
fn resolve_by_subject_dn(&self, subject_dn: &str) -> Option<Vec<u8>> {
|
||||
self.by_subject_dn.get(subject_dn).cloned()
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn e2e_offline_uses_rsync_then_writes_verified_pack_then_outputs_vrps() {
|
||||
let fixture_dir = Path::new("tests/fixtures/repository/rpki.cernet.net/repo/cernet/0");
|
||||
let rsync_base_uri = "rsync://rpki.cernet.net/repo/cernet/0/";
|
||||
let manifest_path = fixture_dir.join("05FC9C5B88506F7C0D3F862C8895BED67E9F8EBA.mft");
|
||||
let manifest_rsync_uri = fixture_to_rsync_uri(&manifest_path);
|
||||
let publication_point_rsync_uri = fixture_dir_to_rsync_uri(fixture_dir);
|
||||
|
||||
let issuer_ca_der = std::fs::read("tests/fixtures/repository/rpki.apnic.net/repository/B527EF581D6611E2BB468F7C72FD1FF2/BfycW4hQb3wNP4YsiJW-1n6fjro.cer")
|
||||
.expect("read issuer ca");
|
||||
let issuer_ca = ResourceCertificate::decode_der(&issuer_ca_der).expect("decode issuer ca");
|
||||
|
||||
// Choose a validation_time that is safely inside:
|
||||
// - manifest thisUpdate..nextUpdate
|
||||
// - issuer CA validity
|
||||
// - CRL thisUpdate..nextUpdate
|
||||
let manifest_der = std::fs::read(&manifest_path).expect("read manifest");
|
||||
let manifest = ManifestObject::decode_der(&manifest_der).expect("decode manifest");
|
||||
|
||||
let crl_der = std::fs::read(fixture_dir.join("05FC9C5B88506F7C0D3F862C8895BED67E9F8EBA.crl"))
|
||||
.expect("read crl");
|
||||
let crl = RpkixCrl::decode_der(&crl_der).expect("decode crl");
|
||||
|
||||
let mut t = manifest.manifest.this_update;
|
||||
if issuer_ca.tbs.validity_not_before > t {
|
||||
t = issuer_ca.tbs.validity_not_before;
|
||||
}
|
||||
if crl.this_update.utc > t {
|
||||
t = crl.this_update.utc;
|
||||
}
|
||||
t += time::Duration::seconds(1);
|
||||
|
||||
let mut policy = Policy::default();
|
||||
policy.sync_preference = SyncPreference::RsyncOnly;
|
||||
|
||||
let rsync_fetcher = LocalDirRsyncFetcher::new(fixture_dir);
|
||||
let http_fetcher = NeverHttpFetcher;
|
||||
|
||||
let resolver = MapResolver {
|
||||
by_subject_dn: HashMap::from([(issuer_ca.tbs.subject_dn, issuer_ca_der)]),
|
||||
};
|
||||
|
||||
let temp = tempfile::tempdir().expect("tempdir");
|
||||
let store = RocksStore::open(temp.path()).expect("open rocksdb");
|
||||
|
||||
let expected_files = std::fs::read_dir(fixture_dir)
|
||||
.expect("read fixture dir")
|
||||
.filter_map(|e| e.ok())
|
||||
.filter_map(|e| e.metadata().ok().map(|m| (e, m)))
|
||||
.filter(|(_e, m)| m.is_file())
|
||||
.count();
|
||||
assert!(expected_files >= 3, "fixture dir seems incomplete");
|
||||
|
||||
let out = run_publication_point_once(
|
||||
&store,
|
||||
&policy,
|
||||
None,
|
||||
rsync_base_uri,
|
||||
&manifest_rsync_uri,
|
||||
&publication_point_rsync_uri,
|
||||
&http_fetcher,
|
||||
&rsync_fetcher,
|
||||
&resolver,
|
||||
t,
|
||||
)
|
||||
.expect("run publication point once");
|
||||
|
||||
assert!(verified_pack_exists(&store, &manifest_rsync_uri).expect("exists check"));
|
||||
assert_eq!(out.repo_sync.objects_written, expected_files);
|
||||
|
||||
assert!(
|
||||
out.objects.vrps.iter().any(|v| v.asn == 4538),
|
||||
"expected VRPs for AS4538"
|
||||
);
|
||||
}
|
||||
222
tests/test_run_tree_from_tal_offline_m17.rs
Normal file
222
tests/test_run_tree_from_tal_offline_m17.rs
Normal file
@@ -0,0 +1,222 @@
|
||||
use rpki::validation::from_tal::discover_root_ca_instance_from_tal_and_ta_der;
|
||||
use rpki::validation::run_tree_from_tal::root_handle_from_trust_anchor;
|
||||
use rpki::validation::run_tree_from_tal::{
|
||||
run_tree_from_tal_and_ta_der_serial, run_tree_from_tal_url_serial,
|
||||
run_tree_from_tal_and_ta_der_serial_audit, run_tree_from_tal_url_serial_audit,
|
||||
};
|
||||
use rpki::validation::tree::TreeRunConfig;
|
||||
|
||||
use std::collections::HashMap;
|
||||
|
||||
struct MapHttpFetcher {
|
||||
map: HashMap<String, Vec<u8>>,
|
||||
}
|
||||
|
||||
impl rpki::sync::rrdp::Fetcher for MapHttpFetcher {
|
||||
fn fetch(&self, uri: &str) -> Result<Vec<u8>, String> {
|
||||
self.map
|
||||
.get(uri)
|
||||
.cloned()
|
||||
.ok_or_else(|| format!("no fixture mapped for {uri}"))
|
||||
}
|
||||
}
|
||||
|
||||
struct EmptyRsyncFetcher;
|
||||
impl rpki::fetch::rsync::RsyncFetcher for EmptyRsyncFetcher {
|
||||
fn fetch_objects(&self, _rsync_base_uri: &str) -> Result<Vec<(String, Vec<u8>)>, rpki::fetch::rsync::RsyncFetchError> {
|
||||
Ok(Vec::new())
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn root_handle_is_constructible_from_fixture_tal_and_ta() {
|
||||
let tal_bytes = std::fs::read("tests/fixtures/tal/apnic-rfc7730-https.tal")
|
||||
.expect("read apnic tal fixture");
|
||||
let ta_der = std::fs::read("tests/fixtures/ta/apnic-ta.cer").expect("read apnic ta fixture");
|
||||
|
||||
let discovery =
|
||||
discover_root_ca_instance_from_tal_and_ta_der(&tal_bytes, &ta_der, None).expect("discover");
|
||||
|
||||
let root =
|
||||
root_handle_from_trust_anchor(&discovery.trust_anchor, None, &discovery.ca_instance);
|
||||
|
||||
assert_eq!(root.depth, 0);
|
||||
assert_eq!(root.manifest_rsync_uri, discovery.ca_instance.manifest_rsync_uri);
|
||||
assert_eq!(root.rsync_base_uri, discovery.ca_instance.rsync_base_uri);
|
||||
assert!(root.ca_certificate_der.len() > 100, "TA der should be non-empty");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn run_tree_from_tal_url_entry_executes_and_records_failure_when_repo_empty() {
|
||||
let tal_url = "mock:apnic.tal";
|
||||
let tal_bytes = std::fs::read("tests/fixtures/tal/apnic-rfc7730-https.tal")
|
||||
.expect("read apnic tal fixture");
|
||||
let ta_der = std::fs::read("tests/fixtures/ta/apnic-ta.cer").expect("read apnic ta fixture");
|
||||
|
||||
let mut map = HashMap::new();
|
||||
map.insert(tal_url.to_string(), tal_bytes);
|
||||
map.insert(
|
||||
"https://rpki.apnic.net/repository/apnic-rpki-root-iana-origin.cer".to_string(),
|
||||
ta_der.clone(),
|
||||
);
|
||||
map.insert(
|
||||
"rsync://rpki.apnic.net/repository/apnic-rpki-root-iana-origin.cer".to_string(),
|
||||
ta_der.clone(),
|
||||
);
|
||||
let http = MapHttpFetcher { map };
|
||||
|
||||
let rsync = EmptyRsyncFetcher;
|
||||
let temp = tempfile::tempdir().expect("tempdir");
|
||||
let store = rpki::storage::RocksStore::open(temp.path()).expect("open rocksdb");
|
||||
let policy = rpki::policy::Policy {
|
||||
sync_preference: rpki::policy::SyncPreference::RsyncOnly,
|
||||
..rpki::policy::Policy::default()
|
||||
};
|
||||
|
||||
let out = run_tree_from_tal_url_serial(
|
||||
&store,
|
||||
&policy,
|
||||
tal_url,
|
||||
&http,
|
||||
&rsync,
|
||||
time::OffsetDateTime::now_utc(),
|
||||
&TreeRunConfig {
|
||||
max_depth: Some(0),
|
||||
max_instances: Some(1),
|
||||
},
|
||||
)
|
||||
.expect("run tree");
|
||||
assert_eq!(out.tree.instances_processed, 0);
|
||||
assert_eq!(out.tree.instances_failed, 1);
|
||||
assert!(
|
||||
out.tree
|
||||
.warnings
|
||||
.iter()
|
||||
.any(|w| w.message.contains("publication point failed")),
|
||||
"expected failure warning"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn run_tree_from_tal_and_ta_der_entry_executes_and_records_failure_when_repo_empty() {
|
||||
let tal_bytes = std::fs::read("tests/fixtures/tal/apnic-rfc7730-https.tal")
|
||||
.expect("read apnic tal fixture");
|
||||
let ta_der = std::fs::read("tests/fixtures/ta/apnic-ta.cer").expect("read apnic ta fixture");
|
||||
|
||||
let http = MapHttpFetcher { map: HashMap::new() };
|
||||
let rsync = EmptyRsyncFetcher;
|
||||
let temp = tempfile::tempdir().expect("tempdir");
|
||||
let store = rpki::storage::RocksStore::open(temp.path()).expect("open rocksdb");
|
||||
let policy = rpki::policy::Policy {
|
||||
sync_preference: rpki::policy::SyncPreference::RsyncOnly,
|
||||
..rpki::policy::Policy::default()
|
||||
};
|
||||
|
||||
let out = run_tree_from_tal_and_ta_der_serial(
|
||||
&store,
|
||||
&policy,
|
||||
&tal_bytes,
|
||||
&ta_der,
|
||||
None,
|
||||
&http,
|
||||
&rsync,
|
||||
time::OffsetDateTime::now_utc(),
|
||||
&TreeRunConfig {
|
||||
max_depth: Some(0),
|
||||
max_instances: Some(1),
|
||||
},
|
||||
)
|
||||
.expect("run tree");
|
||||
assert_eq!(out.tree.instances_processed, 0);
|
||||
assert_eq!(out.tree.instances_failed, 1);
|
||||
assert!(
|
||||
out.tree
|
||||
.warnings
|
||||
.iter()
|
||||
.any(|w| w.message.contains("publication point failed")),
|
||||
"expected failure warning"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn run_tree_from_tal_url_audit_entry_collects_no_publication_points_when_repo_empty() {
|
||||
let tal_url = "mock:apnic.tal";
|
||||
let tal_bytes = std::fs::read("tests/fixtures/tal/apnic-rfc7730-https.tal")
|
||||
.expect("read apnic tal fixture");
|
||||
let ta_der = std::fs::read("tests/fixtures/ta/apnic-ta.cer").expect("read apnic ta fixture");
|
||||
|
||||
let mut map = HashMap::new();
|
||||
map.insert(tal_url.to_string(), tal_bytes);
|
||||
map.insert(
|
||||
"https://rpki.apnic.net/repository/apnic-rpki-root-iana-origin.cer".to_string(),
|
||||
ta_der.clone(),
|
||||
);
|
||||
map.insert(
|
||||
"rsync://rpki.apnic.net/repository/apnic-rpki-root-iana-origin.cer".to_string(),
|
||||
ta_der.clone(),
|
||||
);
|
||||
let http = MapHttpFetcher { map };
|
||||
let rsync = EmptyRsyncFetcher;
|
||||
|
||||
let temp = tempfile::tempdir().expect("tempdir");
|
||||
let store = rpki::storage::RocksStore::open(temp.path()).expect("open rocksdb");
|
||||
let policy = rpki::policy::Policy {
|
||||
sync_preference: rpki::policy::SyncPreference::RsyncOnly,
|
||||
..rpki::policy::Policy::default()
|
||||
};
|
||||
|
||||
let out = run_tree_from_tal_url_serial_audit(
|
||||
&store,
|
||||
&policy,
|
||||
tal_url,
|
||||
&http,
|
||||
&rsync,
|
||||
time::OffsetDateTime::now_utc(),
|
||||
&TreeRunConfig {
|
||||
max_depth: Some(0),
|
||||
max_instances: Some(1),
|
||||
},
|
||||
)
|
||||
.expect("run tree audit");
|
||||
|
||||
assert_eq!(out.tree.instances_processed, 0);
|
||||
assert_eq!(out.tree.instances_failed, 1);
|
||||
assert!(out.publication_points.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn run_tree_from_tal_and_ta_der_audit_entry_collects_no_publication_points_when_repo_empty() {
|
||||
let tal_bytes = std::fs::read("tests/fixtures/tal/apnic-rfc7730-https.tal")
|
||||
.expect("read apnic tal fixture");
|
||||
let ta_der = std::fs::read("tests/fixtures/ta/apnic-ta.cer").expect("read apnic ta fixture");
|
||||
|
||||
let http = MapHttpFetcher { map: HashMap::new() };
|
||||
let rsync = EmptyRsyncFetcher;
|
||||
|
||||
let temp = tempfile::tempdir().expect("tempdir");
|
||||
let store = rpki::storage::RocksStore::open(temp.path()).expect("open rocksdb");
|
||||
let policy = rpki::policy::Policy {
|
||||
sync_preference: rpki::policy::SyncPreference::RsyncOnly,
|
||||
..rpki::policy::Policy::default()
|
||||
};
|
||||
|
||||
let out = run_tree_from_tal_and_ta_der_serial_audit(
|
||||
&store,
|
||||
&policy,
|
||||
&tal_bytes,
|
||||
&ta_der,
|
||||
None,
|
||||
&http,
|
||||
&rsync,
|
||||
time::OffsetDateTime::now_utc(),
|
||||
&TreeRunConfig {
|
||||
max_depth: Some(0),
|
||||
max_instances: Some(1),
|
||||
},
|
||||
)
|
||||
.expect("run tree audit");
|
||||
|
||||
assert_eq!(out.tree.instances_processed, 0);
|
||||
assert_eq!(out.tree.instances_failed, 1);
|
||||
assert!(out.publication_points.is_empty());
|
||||
}
|
||||
34
tests/test_storage_iter_all.rs
Normal file
34
tests/test_storage_iter_all.rs
Normal file
@@ -0,0 +1,34 @@
|
||||
use rpki::storage::{RocksStore, VerifiedKey};
|
||||
|
||||
#[test]
|
||||
fn storage_iter_all_lists_raw_and_verified_entries() {
|
||||
let temp = tempfile::tempdir().expect("tempdir");
|
||||
let store = RocksStore::open(temp.path()).expect("open rocksdb");
|
||||
|
||||
store
|
||||
.put_raw("rsync://example.test/repo/a.cer", b"a")
|
||||
.expect("put_raw a");
|
||||
store
|
||||
.put_raw("rsync://example.test/repo/b.roa", b"b")
|
||||
.expect("put_raw b");
|
||||
|
||||
let key = VerifiedKey::from_manifest_rsync_uri("rsync://example.test/repo/m.mft");
|
||||
store.put_verified(&key, b"x").expect("put_verified");
|
||||
|
||||
let raw_keys = store
|
||||
.raw_iter_all()
|
||||
.expect("raw_iter_all")
|
||||
.map(|(k, _v)| String::from_utf8(k.to_vec()).expect("utf8 key"))
|
||||
.collect::<Vec<_>>();
|
||||
assert_eq!(raw_keys.len(), 2);
|
||||
assert!(raw_keys.contains(&"rsync://example.test/repo/a.cer".to_string()));
|
||||
assert!(raw_keys.contains(&"rsync://example.test/repo/b.roa".to_string()));
|
||||
|
||||
let verified_keys = store
|
||||
.verified_iter_all()
|
||||
.expect("verified_iter_all")
|
||||
.map(|(k, _v)| String::from_utf8(k.to_vec()).expect("utf8 key"))
|
||||
.collect::<Vec<_>>();
|
||||
assert_eq!(verified_keys, vec![key.as_str().to_string()]);
|
||||
}
|
||||
|
||||
73
tests/test_storage_misc_coverage_more.rs
Normal file
73
tests/test_storage_misc_coverage_more.rs
Normal file
@@ -0,0 +1,73 @@
|
||||
use rocksdb::WriteBatch;
|
||||
|
||||
use rpki::storage::{RocksStore, VerifiedKey};
|
||||
|
||||
#[test]
|
||||
fn storage_delete_rrdp_state_works() {
|
||||
let temp = tempfile::tempdir().expect("tempdir");
|
||||
let store = RocksStore::open(temp.path()).expect("open rocksdb");
|
||||
|
||||
store
|
||||
.put_rrdp_state("https://example.net/rrdp/notification.xml", b"state")
|
||||
.expect("put state");
|
||||
assert_eq!(
|
||||
store
|
||||
.get_rrdp_state("https://example.net/rrdp/notification.xml")
|
||||
.unwrap()
|
||||
.unwrap(),
|
||||
b"state"
|
||||
);
|
||||
|
||||
store
|
||||
.delete_rrdp_state("https://example.net/rrdp/notification.xml")
|
||||
.expect("delete state");
|
||||
assert!(
|
||||
store
|
||||
.get_rrdp_state("https://example.net/rrdp/notification.xml")
|
||||
.unwrap()
|
||||
.is_none()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn storage_raw_iter_prefix_filters_by_prefix() {
|
||||
let temp = tempfile::tempdir().expect("tempdir");
|
||||
let store = RocksStore::open(temp.path()).expect("open rocksdb");
|
||||
|
||||
store
|
||||
.put_raw("rsync://example.net/repo/a/1.cer", b"1")
|
||||
.unwrap();
|
||||
store
|
||||
.put_raw("rsync://example.net/repo/a/2.cer", b"2")
|
||||
.unwrap();
|
||||
store
|
||||
.put_raw("rsync://example.net/repo/b/1.cer", b"3")
|
||||
.unwrap();
|
||||
|
||||
let prefix = b"rsync://example.net/repo/a/";
|
||||
let items = store
|
||||
.raw_iter_prefix(prefix)
|
||||
.expect("iter")
|
||||
.map(|(k, v)| (String::from_utf8_lossy(&k).to_string(), v.to_vec()))
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
assert_eq!(items.len(), 2);
|
||||
for (k, _v) in &items {
|
||||
assert!(k.starts_with("rsync://example.net/repo/a/"));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn storage_verified_key_format_is_stable() {
|
||||
let k = VerifiedKey::from_manifest_rsync_uri("rsync://example.net/repo/manifest.mft");
|
||||
assert_eq!(k.as_str(), "verified:rsync://example.net/repo/manifest.mft");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn storage_write_batch_accepts_empty_batch() {
|
||||
let temp = tempfile::tempdir().expect("tempdir");
|
||||
let store = RocksStore::open(temp.path()).expect("open rocksdb");
|
||||
store
|
||||
.write_batch(WriteBatch::default())
|
||||
.expect("write batch");
|
||||
}
|
||||
76
tests/test_storage_rocksdb.rs
Normal file
76
tests/test_storage_rocksdb.rs
Normal file
@@ -0,0 +1,76 @@
|
||||
use std::path::Path;
|
||||
|
||||
use rpki::storage::{RocksStore, VerifiedKey};
|
||||
|
||||
#[test]
|
||||
fn storage_opens_and_creates_column_families() {
|
||||
let dir = tempfile::tempdir().expect("tempdir");
|
||||
let _store = RocksStore::open(dir.path()).expect("open rocksdb");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn raw_objects_roundtrip_by_rsync_uri() {
|
||||
let dir = tempfile::tempdir().expect("tempdir");
|
||||
let store = RocksStore::open(dir.path()).expect("open rocksdb");
|
||||
|
||||
let key = "rsync://example.invalid/repo/a.cer";
|
||||
let value = b"hello";
|
||||
store.put_raw(key, value).expect("put raw");
|
||||
let got = store.get_raw(key).expect("get raw");
|
||||
assert_eq!(got.as_deref(), Some(value.as_slice()));
|
||||
|
||||
store.delete_raw(key).expect("delete raw");
|
||||
let got = store.get_raw(key).expect("get raw after delete");
|
||||
assert!(got.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn verified_pack_roundtrip_by_manifest_uri() {
|
||||
let dir = tempfile::tempdir().expect("tempdir");
|
||||
let store = RocksStore::open(dir.path()).expect("open rocksdb");
|
||||
|
||||
let manifest_uri = "rsync://example.invalid/repo/manifest.mft";
|
||||
let verified_key = VerifiedKey::from_manifest_rsync_uri(manifest_uri);
|
||||
assert_eq!(
|
||||
verified_key.as_str(),
|
||||
"verified:rsync://example.invalid/repo/manifest.mft"
|
||||
);
|
||||
|
||||
let bytes = b"pack";
|
||||
store
|
||||
.put_verified(&verified_key, bytes)
|
||||
.expect("put verified");
|
||||
let got = store.get_verified(&verified_key).expect("get verified");
|
||||
assert_eq!(got.as_deref(), Some(bytes.as_slice()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rrdp_state_roundtrip_by_notification_uri() {
|
||||
let dir = tempfile::tempdir().expect("tempdir");
|
||||
let store = RocksStore::open(dir.path()).expect("open rocksdb");
|
||||
|
||||
let notif = "https://example.invalid/rrdp/notification.xml";
|
||||
let state = b"{\"session_id\":\"00000000-0000-0000-0000-000000000000\",\"last_serial\":1}";
|
||||
store.put_rrdp_state(notif, state).expect("put rrdp_state");
|
||||
|
||||
let got = store.get_rrdp_state(notif).expect("get rrdp_state");
|
||||
assert_eq!(got.as_deref(), Some(state.as_slice()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn store_is_reopenable() {
|
||||
let dir = tempfile::tempdir().expect("tempdir");
|
||||
let path: &Path = dir.path();
|
||||
|
||||
let store = RocksStore::open(path).expect("open rocksdb");
|
||||
store
|
||||
.put_raw("rsync://example.invalid/repo/x", b"x")
|
||||
.expect("put");
|
||||
drop(store);
|
||||
|
||||
let store = RocksStore::open(path).expect("reopen rocksdb");
|
||||
let got = store
|
||||
.get_raw("rsync://example.invalid/repo/x")
|
||||
.expect("get after reopen");
|
||||
assert_eq!(got.as_deref(), Some(b"x".as_slice()));
|
||||
}
|
||||
@@ -68,6 +68,9 @@ fn ta_constraints_require_policies_and_ski() {
|
||||
let rc = dummy_rc_ca(RcExtensions {
|
||||
basic_constraints_ca: true,
|
||||
subject_key_identifier: None,
|
||||
authority_key_identifier: None,
|
||||
crl_distribution_points_uris: None,
|
||||
ca_issuers_uris: None,
|
||||
subject_info_access: None,
|
||||
certificate_policies_oid: None,
|
||||
ip_resources: Some(rpki::data_model::rc::IpResourceSet { families: vec![] }),
|
||||
@@ -94,6 +97,9 @@ fn ta_constraints_require_non_empty_resources_and_no_inherit() {
|
||||
let rc = dummy_rc_ca(RcExtensions {
|
||||
basic_constraints_ca: true,
|
||||
subject_key_identifier: Some(vec![1]),
|
||||
authority_key_identifier: None,
|
||||
crl_distribution_points_uris: None,
|
||||
ca_issuers_uris: None,
|
||||
subject_info_access: None,
|
||||
certificate_policies_oid: Some(OID_CP_IPADDR_ASNUMBER.to_string()),
|
||||
ip_resources: None,
|
||||
|
||||
103
tests/test_ta_validate_rc_constraints.rs
Normal file
103
tests/test_ta_validate_rc_constraints.rs
Normal file
@@ -0,0 +1,103 @@
|
||||
use rpki::data_model::rc::{
|
||||
Afi, AsIdentifierChoice, AsResourceSet, IpAddressChoice, IpAddressFamily, IpResourceSet,
|
||||
ResourceCertKind,
|
||||
};
|
||||
use rpki::data_model::ta::{TaCertificate, TaCertificateProfileError};
|
||||
|
||||
fn apnic_ta() -> TaCertificate {
|
||||
let der = std::fs::read("tests/fixtures/ta/apnic-ta.cer").expect("read apnic ta");
|
||||
TaCertificate::decode_der(&der).expect("decode apnic ta")
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ta_rc_constraints_reject_wrong_kind() {
|
||||
let ta = apnic_ta();
|
||||
let mut rc = ta.rc_ca.clone();
|
||||
rc.kind = ResourceCertKind::Ee;
|
||||
assert!(matches!(
|
||||
TaCertificate::validate_rc_constraints(&rc),
|
||||
Err(TaCertificateProfileError::NotCa)
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ta_rc_constraints_reject_missing_policies_oid() {
|
||||
let ta = apnic_ta();
|
||||
let mut rc = ta.rc_ca.clone();
|
||||
rc.tbs.extensions.certificate_policies_oid = None;
|
||||
assert!(matches!(
|
||||
TaCertificate::validate_rc_constraints(&rc),
|
||||
Err(TaCertificateProfileError::MissingOrInvalidCertificatePolicies)
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ta_rc_constraints_reject_missing_subject_key_identifier() {
|
||||
let ta = apnic_ta();
|
||||
let mut rc = ta.rc_ca.clone();
|
||||
rc.tbs.extensions.subject_key_identifier = None;
|
||||
assert!(matches!(
|
||||
TaCertificate::validate_rc_constraints(&rc),
|
||||
Err(TaCertificateProfileError::MissingSubjectKeyIdentifier)
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ta_rc_constraints_reject_missing_resources() {
|
||||
let ta = apnic_ta();
|
||||
let mut rc = ta.rc_ca.clone();
|
||||
rc.tbs.extensions.ip_resources = None;
|
||||
rc.tbs.extensions.as_resources = None;
|
||||
assert!(matches!(
|
||||
TaCertificate::validate_rc_constraints(&rc),
|
||||
Err(TaCertificateProfileError::ResourcesMissing)
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ta_rc_constraints_reject_empty_resources() {
|
||||
let ta = apnic_ta();
|
||||
let mut rc = ta.rc_ca.clone();
|
||||
rc.tbs.extensions.ip_resources = Some(IpResourceSet {
|
||||
families: vec![IpAddressFamily {
|
||||
afi: Afi::Ipv4,
|
||||
choice: IpAddressChoice::AddressesOrRanges(vec![]),
|
||||
}],
|
||||
});
|
||||
rc.tbs.extensions.as_resources = None;
|
||||
assert!(matches!(
|
||||
TaCertificate::validate_rc_constraints(&rc),
|
||||
Err(TaCertificateProfileError::ResourcesEmpty)
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ta_rc_constraints_reject_ip_inherit() {
|
||||
let ta = apnic_ta();
|
||||
let mut rc = ta.rc_ca.clone();
|
||||
rc.tbs.extensions.ip_resources = Some(IpResourceSet {
|
||||
families: vec![IpAddressFamily {
|
||||
afi: Afi::Ipv6,
|
||||
choice: IpAddressChoice::Inherit,
|
||||
}],
|
||||
});
|
||||
assert!(matches!(
|
||||
TaCertificate::validate_rc_constraints(&rc),
|
||||
Err(TaCertificateProfileError::IpResourcesInherit)
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ta_rc_constraints_reject_as_inherit() {
|
||||
let ta = apnic_ta();
|
||||
let mut rc = ta.rc_ca.clone();
|
||||
rc.tbs.extensions.as_resources = Some(AsResourceSet {
|
||||
asnum: Some(AsIdentifierChoice::Inherit),
|
||||
rdi: None,
|
||||
});
|
||||
assert!(matches!(
|
||||
TaCertificate::validate_rc_constraints(&rc),
|
||||
Err(TaCertificateProfileError::AsResourcesInherit)
|
||||
));
|
||||
}
|
||||
|
||||
47
tests/test_ta_verify_self_signature.rs
Normal file
47
tests/test_ta_verify_self_signature.rs
Normal file
@@ -0,0 +1,47 @@
|
||||
use rpki::data_model::rc::ResourceCertificate;
|
||||
use rpki::data_model::ta::{TaCertificate, TaCertificateVerifyError};
|
||||
|
||||
#[test]
|
||||
fn ta_verify_self_signature_succeeds_for_fixture() {
|
||||
let der = std::fs::read("tests/fixtures/ta/apnic-ta.cer").expect("read apnic ta");
|
||||
let ta = TaCertificate::decode_der(&der).expect("decode TA");
|
||||
ta.verify_self_signature().expect("verify self signature");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ta_verify_self_signature_rejects_trailing_bytes() {
|
||||
let der = std::fs::read("tests/fixtures/ta/apnic-ta.cer").expect("read apnic ta");
|
||||
let rc_ca = ResourceCertificate::decode_der(&der).expect("decode rc");
|
||||
|
||||
let mut raw_with_trailing = der.clone();
|
||||
raw_with_trailing.extend_from_slice(&[0u8, 1u8, 2u8]);
|
||||
|
||||
let ta = TaCertificate {
|
||||
raw_der: raw_with_trailing,
|
||||
rc_ca,
|
||||
};
|
||||
let err = ta.verify_self_signature().unwrap_err();
|
||||
assert!(matches!(err, TaCertificateVerifyError::TrailingBytes(3)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ta_verify_self_signature_rejects_tampered_signature() {
|
||||
let der = std::fs::read("tests/fixtures/ta/apnic-ta.cer").expect("read apnic ta");
|
||||
let rc_ca = ResourceCertificate::decode_der(&der).expect("decode rc");
|
||||
|
||||
let mut tampered = der.clone();
|
||||
if let Some(last) = tampered.last_mut() {
|
||||
*last ^= 0x01;
|
||||
}
|
||||
let ta = TaCertificate {
|
||||
raw_der: tampered,
|
||||
rc_ca,
|
||||
};
|
||||
|
||||
let err = ta.verify_self_signature().unwrap_err();
|
||||
assert!(matches!(
|
||||
err,
|
||||
TaCertificateVerifyError::InvalidSelfSignature(_) | TaCertificateVerifyError::Parse(_)
|
||||
));
|
||||
}
|
||||
|
||||
136
tests/test_tree_failure_handling.rs
Normal file
136
tests/test_tree_failure_handling.rs
Normal file
@@ -0,0 +1,136 @@
|
||||
use std::collections::HashMap;
|
||||
|
||||
use rpki::report::Warning;
|
||||
use rpki::storage::{PackTime, VerifiedPublicationPointPack};
|
||||
use rpki::validation::manifest::PublicationPointSource;
|
||||
use rpki::validation::objects::{ObjectsOutput, ObjectsStats};
|
||||
use rpki::validation::tree::{
|
||||
CaInstanceHandle, PublicationPointRunResult, PublicationPointRunner, TreeRunConfig,
|
||||
run_tree_serial,
|
||||
};
|
||||
use rpki::audit::PublicationPointAudit;
|
||||
|
||||
fn empty_pack(manifest_uri: &str, pp_uri: &str) -> VerifiedPublicationPointPack {
|
||||
VerifiedPublicationPointPack {
|
||||
format_version: VerifiedPublicationPointPack::FORMAT_VERSION_V1,
|
||||
publication_point_rsync_uri: pp_uri.to_string(),
|
||||
manifest_rsync_uri: manifest_uri.to_string(),
|
||||
this_update: PackTime {
|
||||
rfc3339_utc: "2026-01-01T00:00:00Z".to_string(),
|
||||
},
|
||||
next_update: PackTime {
|
||||
rfc3339_utc: "2026-12-31T00:00:00Z".to_string(),
|
||||
},
|
||||
verified_at: PackTime {
|
||||
rfc3339_utc: "2026-02-06T00:00:00Z".to_string(),
|
||||
},
|
||||
manifest_bytes: vec![1, 2, 3],
|
||||
files: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
fn ca_handle(manifest_uri: &str) -> CaInstanceHandle {
|
||||
CaInstanceHandle {
|
||||
depth: 0,
|
||||
ca_certificate_der: Vec::new(),
|
||||
ca_certificate_rsync_uri: None,
|
||||
effective_ip_resources: None,
|
||||
effective_as_resources: None,
|
||||
rsync_base_uri: "rsync://example.test/repo/".to_string(),
|
||||
manifest_rsync_uri: manifest_uri.to_string(),
|
||||
publication_point_rsync_uri: "rsync://example.test/repo/".to_string(),
|
||||
rrdp_notification_uri: None,
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
struct ResultRunner {
|
||||
by_manifest: HashMap<String, Result<PublicationPointRunResult, String>>,
|
||||
}
|
||||
|
||||
impl ResultRunner {
|
||||
fn with_ok(mut self, manifest: &str, res: PublicationPointRunResult) -> Self {
|
||||
self.by_manifest.insert(manifest.to_string(), Ok(res));
|
||||
self
|
||||
}
|
||||
|
||||
fn with_err(mut self, manifest: &str, err: &str) -> Self {
|
||||
self.by_manifest
|
||||
.insert(manifest.to_string(), Err(err.to_string()));
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl PublicationPointRunner for ResultRunner {
|
||||
fn run_publication_point(
|
||||
&self,
|
||||
ca: &CaInstanceHandle,
|
||||
) -> Result<PublicationPointRunResult, String> {
|
||||
self.by_manifest
|
||||
.get(&ca.manifest_rsync_uri)
|
||||
.cloned()
|
||||
.unwrap_or_else(|| Err(format!("no mock for {}", ca.manifest_rsync_uri)))
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tree_continues_when_a_publication_point_fails() {
|
||||
let root_manifest = "rsync://example.test/repo/root.mft";
|
||||
let bad_child_manifest = "rsync://example.test/repo/bad-child.mft";
|
||||
let ok_child_manifest = "rsync://example.test/repo/ok-child.mft";
|
||||
|
||||
let runner = ResultRunner::default()
|
||||
.with_ok(
|
||||
root_manifest,
|
||||
PublicationPointRunResult {
|
||||
source: PublicationPointSource::Fresh,
|
||||
pack: empty_pack(root_manifest, "rsync://example.test/repo/"),
|
||||
warnings: Vec::new(),
|
||||
objects: ObjectsOutput {
|
||||
vrps: Vec::new(),
|
||||
aspas: Vec::new(),
|
||||
warnings: Vec::new(),
|
||||
stats: ObjectsStats::default(),
|
||||
audit: Vec::new(),
|
||||
},
|
||||
audit: PublicationPointAudit::default(),
|
||||
discovered_children: vec![ca_handle(bad_child_manifest), ca_handle(ok_child_manifest)],
|
||||
},
|
||||
)
|
||||
.with_err(bad_child_manifest, "synthetic failure")
|
||||
.with_ok(
|
||||
ok_child_manifest,
|
||||
PublicationPointRunResult {
|
||||
source: PublicationPointSource::Fresh,
|
||||
pack: empty_pack(ok_child_manifest, "rsync://example.test/repo/ok-child/"),
|
||||
warnings: vec![Warning::new("ok child warning")],
|
||||
objects: ObjectsOutput {
|
||||
vrps: Vec::new(),
|
||||
aspas: Vec::new(),
|
||||
warnings: Vec::new(),
|
||||
stats: ObjectsStats::default(),
|
||||
audit: Vec::new(),
|
||||
},
|
||||
audit: PublicationPointAudit::default(),
|
||||
discovered_children: Vec::new(),
|
||||
},
|
||||
);
|
||||
|
||||
let out = run_tree_serial(ca_handle(root_manifest), &runner, &TreeRunConfig::default())
|
||||
.expect("run tree");
|
||||
|
||||
assert_eq!(out.instances_processed, 2);
|
||||
assert_eq!(out.instances_failed, 1);
|
||||
assert!(
|
||||
out.warnings
|
||||
.iter()
|
||||
.any(|w| w.message.contains("publication point failed")),
|
||||
"expected failure warning"
|
||||
);
|
||||
assert!(
|
||||
out.warnings
|
||||
.iter()
|
||||
.any(|w| w.message.contains("ok child warning")),
|
||||
"expected ok child warning propagated"
|
||||
);
|
||||
}
|
||||
231
tests/test_tree_traversal_m14.rs
Normal file
231
tests/test_tree_traversal_m14.rs
Normal file
@@ -0,0 +1,231 @@
|
||||
use std::collections::HashMap;
|
||||
|
||||
use rpki::report::Warning;
|
||||
use rpki::storage::{PackFile, PackTime, VerifiedPublicationPointPack};
|
||||
use rpki::validation::manifest::PublicationPointSource;
|
||||
use rpki::validation::tree::{
|
||||
CaInstanceHandle, PublicationPointRunResult, PublicationPointRunner, TreeRunConfig,
|
||||
run_tree_serial,
|
||||
};
|
||||
use rpki::validation::objects::{ObjectsOutput, ObjectsStats};
|
||||
use rpki::audit::PublicationPointAudit;
|
||||
|
||||
#[derive(Default)]
|
||||
struct MockRunner {
|
||||
by_manifest: HashMap<String, PublicationPointRunResult>,
|
||||
calls: std::sync::Mutex<Vec<String>>,
|
||||
}
|
||||
|
||||
impl MockRunner {
|
||||
fn with(mut self, manifest: &str, res: PublicationPointRunResult) -> Self {
|
||||
self.by_manifest.insert(manifest.to_string(), res);
|
||||
self
|
||||
}
|
||||
|
||||
fn called(&self) -> Vec<String> {
|
||||
self.calls.lock().unwrap().clone()
|
||||
}
|
||||
}
|
||||
|
||||
impl PublicationPointRunner for MockRunner {
|
||||
fn run_publication_point(
|
||||
&self,
|
||||
ca: &CaInstanceHandle,
|
||||
) -> Result<PublicationPointRunResult, String> {
|
||||
self.calls
|
||||
.lock()
|
||||
.unwrap()
|
||||
.push(ca.manifest_rsync_uri.clone());
|
||||
self.by_manifest
|
||||
.get(&ca.manifest_rsync_uri)
|
||||
.cloned()
|
||||
.ok_or_else(|| format!("no mock for {}", ca.manifest_rsync_uri))
|
||||
}
|
||||
}
|
||||
|
||||
fn empty_pack(manifest_uri: &str, pp_uri: &str) -> VerifiedPublicationPointPack {
|
||||
VerifiedPublicationPointPack {
|
||||
format_version: 1,
|
||||
publication_point_rsync_uri: pp_uri.to_string(),
|
||||
manifest_rsync_uri: manifest_uri.to_string(),
|
||||
this_update: PackTime {
|
||||
rfc3339_utc: "2026-01-01T00:00:00Z".to_string(),
|
||||
},
|
||||
next_update: PackTime {
|
||||
rfc3339_utc: "2026-12-31T00:00:00Z".to_string(),
|
||||
},
|
||||
verified_at: PackTime {
|
||||
rfc3339_utc: "2026-02-06T00:00:00Z".to_string(),
|
||||
},
|
||||
manifest_bytes: vec![1, 2, 3],
|
||||
files: vec![PackFile::from_bytes_compute_sha256(
|
||||
manifest_uri,
|
||||
vec![1],
|
||||
)],
|
||||
}
|
||||
}
|
||||
|
||||
fn ca_handle(manifest_uri: &str) -> CaInstanceHandle {
|
||||
CaInstanceHandle {
|
||||
depth: 0,
|
||||
ca_certificate_der: Vec::new(),
|
||||
ca_certificate_rsync_uri: None,
|
||||
effective_ip_resources: None,
|
||||
effective_as_resources: None,
|
||||
rsync_base_uri: "rsync://example.test/repo/".to_string(),
|
||||
manifest_rsync_uri: manifest_uri.to_string(),
|
||||
publication_point_rsync_uri: "rsync://example.test/repo/".to_string(),
|
||||
rrdp_notification_uri: None,
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tree_enqueues_children_only_for_fresh_publication_points() {
|
||||
let root_manifest = "rsync://example.test/repo/root.mft";
|
||||
let child1_manifest = "rsync://example.test/repo/child1.mft";
|
||||
let child2_manifest = "rsync://example.test/repo/child2.mft";
|
||||
let grandchild_manifest = "rsync://example.test/repo/grandchild.mft";
|
||||
|
||||
let root_children = vec![ca_handle(child1_manifest), ca_handle(child2_manifest)];
|
||||
let child1_children = vec![ca_handle(grandchild_manifest)];
|
||||
|
||||
let runner = MockRunner::default()
|
||||
.with(
|
||||
root_manifest,
|
||||
PublicationPointRunResult {
|
||||
source: PublicationPointSource::Fresh,
|
||||
pack: empty_pack(root_manifest, "rsync://example.test/repo/"),
|
||||
warnings: Vec::new(),
|
||||
objects: ObjectsOutput {
|
||||
vrps: Vec::new(),
|
||||
aspas: Vec::new(),
|
||||
warnings: Vec::new(),
|
||||
stats: ObjectsStats::default(),
|
||||
audit: Vec::new(),
|
||||
},
|
||||
audit: PublicationPointAudit::default(),
|
||||
discovered_children: root_children,
|
||||
},
|
||||
)
|
||||
.with(
|
||||
child1_manifest,
|
||||
PublicationPointRunResult {
|
||||
source: PublicationPointSource::VerifiedCache,
|
||||
pack: empty_pack(child1_manifest, "rsync://example.test/repo/child1/"),
|
||||
warnings: vec![Warning::new("child1 warning")],
|
||||
objects: ObjectsOutput {
|
||||
vrps: Vec::new(),
|
||||
aspas: Vec::new(),
|
||||
warnings: Vec::new(),
|
||||
stats: ObjectsStats::default(),
|
||||
audit: Vec::new(),
|
||||
},
|
||||
audit: PublicationPointAudit::default(),
|
||||
discovered_children: child1_children,
|
||||
},
|
||||
)
|
||||
.with(
|
||||
child2_manifest,
|
||||
PublicationPointRunResult {
|
||||
source: PublicationPointSource::Fresh,
|
||||
pack: empty_pack(child2_manifest, "rsync://example.test/repo/child2/"),
|
||||
warnings: Vec::new(),
|
||||
objects: ObjectsOutput {
|
||||
vrps: Vec::new(),
|
||||
aspas: Vec::new(),
|
||||
warnings: Vec::new(),
|
||||
stats: ObjectsStats::default(),
|
||||
audit: Vec::new(),
|
||||
},
|
||||
audit: PublicationPointAudit::default(),
|
||||
discovered_children: Vec::new(),
|
||||
},
|
||||
);
|
||||
|
||||
let out = run_tree_serial(ca_handle(root_manifest), &runner, &TreeRunConfig::default())
|
||||
.expect("run tree");
|
||||
|
||||
// root + child1 + child2. grandchild must NOT be processed because child1 used cache.
|
||||
assert_eq!(out.instances_processed, 3);
|
||||
assert_eq!(out.instances_failed, 0);
|
||||
|
||||
let called = runner.called();
|
||||
assert_eq!(called, vec![root_manifest, child1_manifest, child2_manifest]);
|
||||
|
||||
assert!(
|
||||
out.warnings.iter().any(|w| w.message.contains("child1 warning")),
|
||||
"expected child1 warning propagated"
|
||||
);
|
||||
assert!(
|
||||
out.warnings
|
||||
.iter()
|
||||
.any(|w| w.message.contains("skipping child CA discovery")),
|
||||
"expected RFC 9286 §6.6 enforcement warning"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tree_respects_max_depth_and_max_instances() {
|
||||
let root_manifest = "rsync://example.test/repo/root.mft";
|
||||
let child_manifest = "rsync://example.test/repo/child.mft";
|
||||
|
||||
let runner = MockRunner::default()
|
||||
.with(
|
||||
root_manifest,
|
||||
PublicationPointRunResult {
|
||||
source: PublicationPointSource::Fresh,
|
||||
pack: empty_pack(root_manifest, "rsync://example.test/repo/"),
|
||||
warnings: Vec::new(),
|
||||
objects: ObjectsOutput {
|
||||
vrps: Vec::new(),
|
||||
aspas: Vec::new(),
|
||||
warnings: Vec::new(),
|
||||
stats: ObjectsStats::default(),
|
||||
audit: Vec::new(),
|
||||
},
|
||||
audit: PublicationPointAudit::default(),
|
||||
discovered_children: vec![ca_handle(child_manifest)],
|
||||
},
|
||||
)
|
||||
.with(
|
||||
child_manifest,
|
||||
PublicationPointRunResult {
|
||||
source: PublicationPointSource::Fresh,
|
||||
pack: empty_pack(child_manifest, "rsync://example.test/repo/child/"),
|
||||
warnings: Vec::new(),
|
||||
objects: ObjectsOutput {
|
||||
vrps: Vec::new(),
|
||||
aspas: Vec::new(),
|
||||
warnings: Vec::new(),
|
||||
stats: ObjectsStats::default(),
|
||||
audit: Vec::new(),
|
||||
},
|
||||
audit: PublicationPointAudit::default(),
|
||||
discovered_children: Vec::new(),
|
||||
},
|
||||
);
|
||||
|
||||
let out = run_tree_serial(
|
||||
ca_handle(root_manifest),
|
||||
&runner,
|
||||
&TreeRunConfig {
|
||||
max_depth: Some(0),
|
||||
max_instances: None,
|
||||
},
|
||||
)
|
||||
.expect("run tree depth-limited");
|
||||
assert_eq!(out.instances_processed, 1);
|
||||
assert_eq!(out.instances_failed, 0);
|
||||
|
||||
let out = run_tree_serial(
|
||||
ca_handle(root_manifest),
|
||||
&runner,
|
||||
&TreeRunConfig {
|
||||
max_depth: None,
|
||||
max_instances: Some(1),
|
||||
},
|
||||
)
|
||||
.expect("run tree instance-limited");
|
||||
assert_eq!(out.instances_processed, 1);
|
||||
assert_eq!(out.instances_failed, 0);
|
||||
}
|
||||
137
tests/test_uncovered_lines_fillers.rs
Normal file
137
tests/test_uncovered_lines_fillers.rs
Normal file
@@ -0,0 +1,137 @@
|
||||
use rpki::data_model::manifest::ManifestObject;
|
||||
use rpki::data_model::rc::{AsIdOrRange, AsIdentifierChoice, AsResourceSet, ResourceCertificate};
|
||||
use rpki::data_model::ta::{TaCertificate, TaCertificateParsed, TaCertificateProfileError};
|
||||
use rpki::data_model::tal::{Tal, TalDecodeError, TalProfileError};
|
||||
use rpki::data_model::roa::{IpPrefix, RoaAfi};
|
||||
|
||||
#[test]
|
||||
fn tal_validate_profile_noop_is_callable() {
|
||||
let raw = std::fs::read("tests/fixtures/tal/apnic-rfc7730-https.tal").expect("read tal");
|
||||
let tal = Tal::decode_bytes(&raw).expect("decode tal");
|
||||
tal.validate_profile().expect("validate_profile is no-op");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tal_invalid_uri_after_valid_uri_is_reported_as_missing_separator() {
|
||||
let s = "https://example.invalid/ta.cer\nnot a url\n\nAQ==\n";
|
||||
assert!(matches!(
|
||||
Tal::decode_bytes(s.as_bytes()),
|
||||
Err(TalDecodeError::Validate(
|
||||
TalProfileError::MissingSeparatorEmptyLine
|
||||
))
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tal_missing_separator_empty_line_is_rejected() {
|
||||
let s = "https://example.invalid/ta.cer\nAQ==\n";
|
||||
assert!(matches!(
|
||||
Tal::decode_bytes(s.as_bytes()),
|
||||
Err(TalDecodeError::Validate(
|
||||
TalProfileError::MissingSeparatorEmptyLine
|
||||
))
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tal_missing_spki_is_rejected() {
|
||||
let s = "https://example.invalid/ta.cer\n\n\n";
|
||||
assert!(matches!(
|
||||
Tal::decode_bytes(s.as_bytes()),
|
||||
Err(TalDecodeError::Validate(TalProfileError::MissingSpki))
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tal_missing_separator_due_to_eof_is_rejected() {
|
||||
// One URI and then EOF: should fail at the mandatory empty-line separator check.
|
||||
let s = "https://example.invalid/ta.cer";
|
||||
assert!(matches!(
|
||||
Tal::decode_bytes(s.as_bytes()),
|
||||
Err(TalDecodeError::Validate(
|
||||
TalProfileError::MissingSeparatorEmptyLine
|
||||
))
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ta_validate_profile_noop_is_callable() {
|
||||
let der = std::fs::read("tests/fixtures/ta/apnic-ta.cer").expect("read ta");
|
||||
let ta = TaCertificate::decode_der(&der).expect("decode ta");
|
||||
ta.validate_profile().expect("validate_profile is no-op");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ta_parsed_validate_profile_rejects_ee_resource_certificate() {
|
||||
// Extract an embedded EE certificate from a real manifest fixture and feed it into the TA
|
||||
// profile validator to hit the `NotCa` branch in `TaCertificateParsed::validate_profile`.
|
||||
let mft_der = std::fs::read(
|
||||
"tests/fixtures/repository/ca.rg.net/rpki/RGnet-OU/bW-_qXU9uNhGQz21NR2ansB8lr0.mft",
|
||||
)
|
||||
.expect("read manifest fixture");
|
||||
let mft = ManifestObject::decode_der(&mft_der).expect("decode manifest");
|
||||
let ee_der = mft.signed_object.signed_data.certificates[0]
|
||||
.resource_cert
|
||||
.raw_der
|
||||
.clone();
|
||||
|
||||
let rc_parsed = ResourceCertificate::parse_der(&ee_der).expect("parse embedded EE cert");
|
||||
let ta_parsed = TaCertificateParsed { rc_parsed };
|
||||
let err = ta_parsed.validate_profile().unwrap_err();
|
||||
assert!(matches!(err, TaCertificateProfileError::NotCa));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ta_rc_constraints_exercise_rdi_non_empty_branch() {
|
||||
let der = std::fs::read("tests/fixtures/ta/apnic-ta.cer").expect("read ta");
|
||||
let ta = TaCertificate::decode_der(&der).expect("decode ta");
|
||||
|
||||
let mut rc = ta.rc_ca.clone();
|
||||
rc.tbs.extensions.as_resources = Some(AsResourceSet {
|
||||
asnum: rc
|
||||
.tbs
|
||||
.extensions
|
||||
.as_resources
|
||||
.as_ref()
|
||||
.and_then(|v| v.asnum.clone()),
|
||||
rdi: Some(AsIdentifierChoice::AsIdsOrRanges(vec![AsIdOrRange::Id(
|
||||
64496,
|
||||
)])),
|
||||
});
|
||||
|
||||
TaCertificate::validate_rc_constraints(&rc).expect("constraints should still pass");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn audit_helpers_format_roa_ip_prefix_smoke() {
|
||||
let v4 = IpPrefix {
|
||||
afi: RoaAfi::Ipv4,
|
||||
prefix_len: 24,
|
||||
addr: vec![192, 0, 2, 0],
|
||||
};
|
||||
assert_eq!(rpki::audit::format_roa_ip_prefix(&v4), "192.0.2.0/24");
|
||||
|
||||
let v6 = IpPrefix {
|
||||
afi: RoaAfi::Ipv6,
|
||||
prefix_len: 32,
|
||||
addr: vec![0x20, 0x01, 0x0d, 0xb8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
|
||||
};
|
||||
assert!(rpki::audit::format_roa_ip_prefix(&v6).ends_with("/32"));
|
||||
|
||||
let bad = IpPrefix {
|
||||
afi: RoaAfi::Ipv4,
|
||||
prefix_len: 8,
|
||||
addr: vec![1, 2, 3],
|
||||
};
|
||||
assert!(rpki::audit::format_roa_ip_prefix(&bad).starts_with("ipv4:"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn audit_helpers_sha256_hex_smoke() {
|
||||
let h = rpki::audit::sha256_hex(b"abc");
|
||||
assert_eq!(h.len(), 64);
|
||||
|
||||
let bytes = [0x11u8; 32];
|
||||
let h2 = rpki::audit::sha256_hex_from_32(&bytes);
|
||||
assert_eq!(h2, "11".repeat(32));
|
||||
}
|
||||
68
tests/test_verified_pack.rs
Normal file
68
tests/test_verified_pack.rs
Normal file
@@ -0,0 +1,68 @@
|
||||
use rpki::storage::{PackFile, PackTime, VerifiedPublicationPointPack};
|
||||
|
||||
fn sample_pack() -> VerifiedPublicationPointPack {
|
||||
let this_update =
|
||||
PackTime::from_utc_offset_datetime(time::OffsetDateTime::from_unix_timestamp(0).unwrap());
|
||||
let next_update = PackTime::from_utc_offset_datetime(
|
||||
time::OffsetDateTime::from_unix_timestamp(3600).unwrap(),
|
||||
);
|
||||
let verified_at =
|
||||
PackTime::from_utc_offset_datetime(time::OffsetDateTime::from_unix_timestamp(10).unwrap());
|
||||
|
||||
let file1 = PackFile::from_bytes_compute_sha256(
|
||||
"rsync://example.net/repo/CA/1.crl",
|
||||
b"crl-bytes".to_vec(),
|
||||
);
|
||||
let file2 = PackFile::from_bytes_compute_sha256(
|
||||
"rsync://example.net/repo/CA/2.cer",
|
||||
b"cer-bytes".to_vec(),
|
||||
);
|
||||
|
||||
VerifiedPublicationPointPack {
|
||||
format_version: VerifiedPublicationPointPack::FORMAT_VERSION_V1,
|
||||
manifest_rsync_uri: "rsync://example.net/repo/CA/manifest.mft".to_string(),
|
||||
publication_point_rsync_uri: "rsync://example.net/repo/CA/".to_string(),
|
||||
this_update,
|
||||
next_update,
|
||||
verified_at,
|
||||
manifest_bytes: b"manifest-bytes".to_vec(),
|
||||
files: vec![file1, file2],
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pack_encode_decode_roundtrip() {
|
||||
let pack = sample_pack();
|
||||
let bytes = pack.encode().expect("encode");
|
||||
let decoded = VerifiedPublicationPointPack::decode(&bytes).expect("decode");
|
||||
assert_eq!(decoded, pack);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pack_rejects_missing_manifest() {
|
||||
let mut pack = sample_pack();
|
||||
pack.manifest_bytes.clear();
|
||||
let bytes = pack.encode().expect("encode");
|
||||
assert!(VerifiedPublicationPointPack::decode(&bytes).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pack_rejects_duplicate_rsync_uri_entries() {
|
||||
let mut pack = sample_pack();
|
||||
let dup =
|
||||
PackFile::from_bytes_compute_sha256("rsync://example.net/repo/CA/1.crl", b"other".to_vec());
|
||||
pack.files.push(dup);
|
||||
let bytes = pack.encode().expect("encode");
|
||||
assert!(VerifiedPublicationPointPack::decode(&bytes).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pack_includes_this_update_next_update() {
|
||||
let pack = sample_pack();
|
||||
let bytes = pack.encode().expect("encode");
|
||||
let decoded = VerifiedPublicationPointPack::decode(&bytes).expect("decode");
|
||||
|
||||
let this_update = decoded.this_update.parse().expect("parse this_update");
|
||||
let next_update = decoded.next_update.parse().expect("parse next_update");
|
||||
assert!(next_update > this_update);
|
||||
}
|
||||
101
tests/test_verified_pack_decode_errors_more.rs
Normal file
101
tests/test_verified_pack_decode_errors_more.rs
Normal file
@@ -0,0 +1,101 @@
|
||||
use rpki::storage::{PackFile, PackTime, VerifiedPublicationPointPack};
|
||||
|
||||
fn base_pack() -> VerifiedPublicationPointPack {
|
||||
let this_update =
|
||||
PackTime::from_utc_offset_datetime(time::OffsetDateTime::from_unix_timestamp(0).unwrap());
|
||||
let next_update = PackTime::from_utc_offset_datetime(
|
||||
time::OffsetDateTime::from_unix_timestamp(3600).unwrap(),
|
||||
);
|
||||
let verified_at =
|
||||
PackTime::from_utc_offset_datetime(time::OffsetDateTime::from_unix_timestamp(10).unwrap());
|
||||
|
||||
let file =
|
||||
PackFile::from_bytes_compute_sha256("rsync://example.net/repo/obj.cer", b"x".to_vec());
|
||||
|
||||
VerifiedPublicationPointPack {
|
||||
format_version: VerifiedPublicationPointPack::FORMAT_VERSION_V1,
|
||||
manifest_rsync_uri: "rsync://example.net/repo/manifest.mft".to_string(),
|
||||
publication_point_rsync_uri: "rsync://example.net/repo/".to_string(),
|
||||
this_update,
|
||||
next_update,
|
||||
verified_at,
|
||||
manifest_bytes: b"manifest".to_vec(),
|
||||
files: vec![file],
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pack_rejects_unsupported_format_version() {
|
||||
let mut pack = base_pack();
|
||||
pack.format_version = 999;
|
||||
let bytes = pack.encode().expect("encode");
|
||||
assert!(
|
||||
VerifiedPublicationPointPack::decode(&bytes)
|
||||
.unwrap_err()
|
||||
.to_string()
|
||||
.contains("unsupported pack format_version")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pack_rejects_missing_manifest_rsync_uri() {
|
||||
let mut pack = base_pack();
|
||||
pack.manifest_rsync_uri.clear();
|
||||
let bytes = pack.encode().expect("encode");
|
||||
assert!(VerifiedPublicationPointPack::decode(&bytes).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pack_rejects_missing_publication_point_rsync_uri() {
|
||||
let mut pack = base_pack();
|
||||
pack.publication_point_rsync_uri.clear();
|
||||
let bytes = pack.encode().expect("encode");
|
||||
assert!(VerifiedPublicationPointPack::decode(&bytes).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pack_rejects_invalid_time_fields() {
|
||||
let mut pack = base_pack();
|
||||
pack.this_update = PackTime {
|
||||
rfc3339_utc: "not-a-time".to_string(),
|
||||
};
|
||||
let bytes = pack.encode().expect("encode");
|
||||
assert!(VerifiedPublicationPointPack::decode(&bytes).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pack_rejects_empty_file_bytes() {
|
||||
let mut pack = base_pack();
|
||||
let mut sha = [0u8; 32];
|
||||
sha[0] = 1;
|
||||
pack.files = vec![PackFile::new(
|
||||
"rsync://example.net/repo/empty.cer",
|
||||
Vec::new(),
|
||||
sha,
|
||||
)];
|
||||
let bytes = pack.encode().expect("encode");
|
||||
assert!(VerifiedPublicationPointPack::decode(&bytes).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pack_rejects_file_hash_mismatch() {
|
||||
let mut pack = base_pack();
|
||||
pack.files = vec![PackFile::new(
|
||||
"rsync://example.net/repo/bad.cer",
|
||||
b"abc".to_vec(),
|
||||
[0u8; 32],
|
||||
)];
|
||||
let bytes = pack.encode().expect("encode");
|
||||
let err = VerifiedPublicationPointPack::decode(&bytes).unwrap_err();
|
||||
assert!(err.to_string().contains("file hash mismatch"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pack_rejects_missing_file_rsync_uri() {
|
||||
let mut pack = base_pack();
|
||||
let file = PackFile::from_bytes_compute_sha256("", b"x".to_vec());
|
||||
pack.files = vec![file];
|
||||
let bytes = pack.encode().expect("encode");
|
||||
let err = VerifiedPublicationPointPack::decode(&bytes).unwrap_err();
|
||||
assert!(err.to_string().contains("missing required field"));
|
||||
}
|
||||
Reference in New Issue
Block a user