20260324 完成和routinator对齐snapshot delta replay correctness一致

This commit is contained in:
2026-03-24 10:10:04 +08:00
parent 557a69cbd2
commit d6d44669b4
23 changed files with 2043 additions and 223 deletions

View File

@@ -1,6 +1,8 @@
use std::process::Command;
use rpki::validation::cert_path::{CertPathError, validate_ee_cert_path};
use rpki::validation::cert_path::{
CertPathError, validate_ee_cert_path, validate_ee_cert_path_with_predecoded_ee,
};
fn openssl_available() -> bool {
Command::new("openssl")
@@ -295,6 +297,57 @@ fn validate_ee_cert_path_with_prevalidated_issuer_covers_success_and_error_paths
assert!(matches!(err, CertPathError::EeRevoked), "{err}");
}
#[test]
fn validate_ee_cert_path_with_predecoded_ee_matches_prevalidated_path_rules() {
use rpki::data_model::common::BigUnsigned;
use rpki::data_model::crl::RpkixCrl;
use rpki::data_model::rc::ResourceCertificate;
use std::collections::HashSet;
use x509_parser::prelude::FromDer;
use x509_parser::x509::SubjectPublicKeyInfo;
let g = generate_issuer_ca_ee_and_crl(
"keyUsage = critical, digitalSignature
",
);
let issuer = ResourceCertificate::decode_der(&g.issuer_ca_der).expect("decode issuer");
let ee = ResourceCertificate::decode_der(&g.ee_der).expect("decode ee");
let issuer_crl = RpkixCrl::decode_der(&g.issuer_crl_der).expect("decode crl");
let (rem, issuer_spki) = SubjectPublicKeyInfo::from_der(&issuer.tbs.subject_public_key_info)
.expect("parse issuer spki");
assert!(rem.is_empty());
let now = time::OffsetDateTime::now_utc();
validate_ee_cert_path_with_predecoded_ee(
&ee,
&g.ee_der,
&issuer,
&issuer_spki,
&issuer_crl,
&HashSet::new(),
Some("rsync://example.test/repo/issuer/issuer.cer"),
Some("rsync://example.test/repo/issuer/issuer.crl"),
now,
)
.expect("predecoded ee path ok");
let mut revoked = HashSet::new();
revoked.insert(BigUnsigned::from_biguint(&ee.tbs.serial_number).bytes_be);
let err = validate_ee_cert_path_with_predecoded_ee(
&ee,
&g.ee_der,
&issuer,
&issuer_spki,
&issuer_crl,
&revoked,
Some("rsync://example.test/repo/issuer/issuer.cer"),
Some("rsync://example.test/repo/issuer/issuer.crl"),
now,
)
.unwrap_err();
assert!(matches!(err, CertPathError::EeRevoked), "{err}");
}
#[test]
fn validate_ee_cert_path_with_prevalidated_issuer_rejects_non_ee_and_non_ca_issuer() {
use rpki::data_model::crl::RpkixCrl;

View File

@@ -1,3 +1,4 @@
use std::fs;
use std::process::Command;
fn multi_rir_bundle_root() -> std::path::PathBuf {
@@ -71,6 +72,94 @@ fn multi_rir_case_info_resolves_all_five_rirs_and_timings() {
}
}
#[test]
fn multi_rir_case_info_prefers_lock_validation_time_over_replay_started_at() {
let td = tempfile::tempdir().expect("tempdir");
let bundle_root = td.path();
let rir_root = bundle_root.join("apnic");
fs::create_dir_all(rir_root.join("base-payload-archive")).expect("base archive dir");
fs::create_dir_all(rir_root.join("payload-delta-archive")).expect("delta archive dir");
fs::create_dir_all(rir_root.join("timings")).expect("timings dir");
fs::write(
bundle_root.join("timing-summary.json"),
r#"{"apnic":{"durations":{"base-replay":1.5,"delta-replay":2.5}}}"#,
)
.expect("write timing summary");
fs::write(
rir_root.join("base-locks.json"),
r#"{"version":1,"capture":"base-cap","validationTime":"2026-03-16T11:49:15+08:00","rrdp":{},"rsync":{}}"#,
)
.expect("write base locks");
fs::write(
rir_root.join("locks-delta.json"),
r#"{"version":1,"capture":"delta-cap","baseCapture":"base-cap","baseLocksSha256":"deadbeef","validationTime":"2026-03-16T12:14:10+08:00","rrdp":{},"rsync":{}}"#,
)
.expect("write delta locks");
fs::write(
rir_root.join("timings/base-replay.json"),
r#"{"startedAt":"2099-01-01T00:00:00Z","durationSeconds":1.5}"#,
)
.expect("write base timing");
fs::write(
rir_root.join("timings/delta-replay.json"),
r#"{"startedAt":"2099-01-02T00:00:00Z","durationSeconds":2.5}"#,
)
.expect("write delta timing");
for rel in [
"base-vrps.csv",
"record-delta.csv",
"replay-delta.csv",
"verification.json",
"README.md",
] {
fs::write(rir_root.join(rel), "placeholder
").expect("write required file");
}
let repo_root = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"));
let out = Command::new("python3")
.arg(helper_script())
.args([
"--bundle-root",
bundle_root.to_string_lossy().as_ref(),
"--repo-root",
repo_root.to_string_lossy().as_ref(),
"--rir",
"apnic",
])
.output()
.expect("run helper script");
assert!(
out.status.success(),
"helper failed: status={}
stdout={}
stderr={}",
out.status,
String::from_utf8_lossy(&out.stdout),
String::from_utf8_lossy(&out.stderr)
);
let json: serde_json::Value = serde_json::from_slice(&out.stdout).expect("parse helper json");
assert_eq!(
json["validation_times"]["snapshot"].as_str(),
Some("2026-03-16T11:49:15+08:00")
);
assert_eq!(
json["validation_times"]["delta"].as_str(),
Some("2026-03-16T12:14:10+08:00")
);
assert_eq!(
json["timing_started_at"]["snapshot_replay"].as_str(),
Some("2099-01-01T00:00:00Z")
);
assert_eq!(
json["timing_started_at"]["delta_replay"].as_str(),
Some("2099-01-02T00:00:00Z")
);
}
#[test]
fn multi_rir_wrapper_describe_mode_works_for_ripe() {
let bundle_root = multi_rir_bundle_root();

View File

@@ -1,6 +1,6 @@
use rpki::fetch::rsync::LocalDirRsyncFetcher;
use rpki::policy::{Policy, SignedObjectFailurePolicy, SyncPreference};
use rpki::storage::{PackFile, PackTime, RocksStore};
use rpki::storage::{PackFile, PackTime, RocksStore, VcirOutputType};
use rpki::sync::repo::sync_publication_point;
use rpki::sync::rrdp::Fetcher;
use rpki::validation::manifest::process_manifest_publication_point;
@@ -403,4 +403,45 @@ fn process_snapshot_for_issuer_drop_publication_point_on_invalid_aspa_bytes() {
assert!(!out.warnings.is_empty());
}
#[test]
fn process_snapshot_for_issuer_populates_local_outputs_cache_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_publication_point_snapshot_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 out = process_publication_point_snapshot_for_issuer(
&pack,
&Policy::default(),
&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,
None,
);
assert!(!out.local_outputs_cache.is_empty(), "expected cached VCIR local outputs");
assert_eq!(out.local_outputs_cache.len(), out.vrps.len());
assert!(out
.local_outputs_cache
.iter()
.all(|entry| entry.output_type == VcirOutputType::Vrp));
assert!(out
.local_outputs_cache
.iter()
.all(|entry| entry.source_object_type == "roa"));
}
// NOTE: DN-based issuer resolution and pack-local CA indexing have been removed for determinism.

View File

@@ -284,3 +284,28 @@ fn write_multi_rir_summary_aggregates_case_reports() {
assert!(md.contains("Multi-RIR Replay Summary"), "{md}");
assert!(md.contains("| afrinic | true | 10.000 | 5.000 | 2.000 | true | 12.000 | 6.000 | 3.000 |"), "{md}");
}
#[test]
fn apnic_snapshot_profile_script_dry_run_builds_command() {
let script = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.join("scripts/payload_replay/run_apnic_snapshot_replay_profile.sh");
let out = Command::new("bash")
.env("DRY_RUN", "1")
.arg(&script)
.output()
.expect("run profile script dry-run");
assert!(
out.status.success(),
"script failed: status={}\nstdout={}\nstderr={}",
out.status,
String::from_utf8_lossy(&out.stdout),
String::from_utf8_lossy(&out.stderr)
);
let stdout = String::from_utf8_lossy(&out.stdout);
assert!(stdout.contains("--payload-replay-archive"), "{stdout}");
assert!(stdout.contains("--payload-replay-locks"), "{stdout}");
assert!(stdout.contains("--analyze"), "{stdout}");
assert!(stdout.contains("--profile-cpu"), "{stdout}");
}

View File

@@ -112,6 +112,7 @@ fn tree_continues_when_a_publication_point_fails() {
objects: ObjectsOutput {
vrps: Vec::new(),
aspas: Vec::new(),
local_outputs_cache: Vec::new(),
warnings: Vec::new(),
stats: ObjectsStats::default(),
audit: Vec::new(),
@@ -136,6 +137,7 @@ fn tree_continues_when_a_publication_point_fails() {
objects: ObjectsOutput {
vrps: Vec::new(),
aspas: Vec::new(),
local_outputs_cache: Vec::new(),
warnings: Vec::new(),
stats: ObjectsStats::default(),
audit: Vec::new(),

View File

@@ -122,6 +122,7 @@ fn tree_enqueues_children_for_fresh_and_current_instance_vcir_results() {
objects: ObjectsOutput {
vrps: Vec::new(),
aspas: Vec::new(),
local_outputs_cache: Vec::new(),
warnings: Vec::new(),
stats: ObjectsStats::default(),
audit: Vec::new(),
@@ -142,6 +143,7 @@ fn tree_enqueues_children_for_fresh_and_current_instance_vcir_results() {
objects: ObjectsOutput {
vrps: Vec::new(),
aspas: Vec::new(),
local_outputs_cache: Vec::new(),
warnings: Vec::new(),
stats: ObjectsStats::default(),
audit: Vec::new(),
@@ -162,6 +164,7 @@ fn tree_enqueues_children_for_fresh_and_current_instance_vcir_results() {
objects: ObjectsOutput {
vrps: Vec::new(),
aspas: Vec::new(),
local_outputs_cache: Vec::new(),
warnings: Vec::new(),
stats: ObjectsStats::default(),
audit: Vec::new(),
@@ -182,6 +185,7 @@ fn tree_enqueues_children_for_fresh_and_current_instance_vcir_results() {
objects: ObjectsOutput {
vrps: Vec::new(),
aspas: Vec::new(),
local_outputs_cache: Vec::new(),
warnings: Vec::new(),
stats: ObjectsStats::default(),
audit: Vec::new(),
@@ -239,6 +243,7 @@ fn tree_respects_max_depth_and_max_instances() {
objects: ObjectsOutput {
vrps: Vec::new(),
aspas: Vec::new(),
local_outputs_cache: Vec::new(),
warnings: Vec::new(),
stats: ObjectsStats::default(),
audit: Vec::new(),
@@ -259,6 +264,7 @@ fn tree_respects_max_depth_and_max_instances() {
objects: ObjectsOutput {
vrps: Vec::new(),
aspas: Vec::new(),
local_outputs_cache: Vec::new(),
warnings: Vec::new(),
stats: ObjectsStats::default(),
audit: Vec::new(),
@@ -308,6 +314,7 @@ fn tree_audit_includes_parent_and_discovered_from_for_non_root_nodes() {
objects: ObjectsOutput {
vrps: Vec::new(),
aspas: Vec::new(),
local_outputs_cache: Vec::new(),
warnings: Vec::new(),
stats: ObjectsStats::default(),
audit: Vec::new(),
@@ -328,6 +335,7 @@ fn tree_audit_includes_parent_and_discovered_from_for_non_root_nodes() {
objects: ObjectsOutput {
vrps: Vec::new(),
aspas: Vec::new(),
local_outputs_cache: Vec::new(),
warnings: Vec::new(),
stats: ObjectsStats::default(),
audit: Vec::new(),
@@ -380,6 +388,7 @@ fn tree_prefers_lexicographically_first_discovery_when_duplicate_manifest_is_que
objects: ObjectsOutput {
vrps: Vec::new(),
aspas: Vec::new(),
local_outputs_cache: Vec::new(),
warnings: Vec::new(),
stats: ObjectsStats::default(),
audit: Vec::new(),
@@ -400,6 +409,7 @@ fn tree_prefers_lexicographically_first_discovery_when_duplicate_manifest_is_que
objects: ObjectsOutput {
vrps: Vec::new(),
aspas: Vec::new(),
local_outputs_cache: Vec::new(),
warnings: Vec::new(),
stats: ObjectsStats::default(),
audit: Vec::new(),