diff --git a/Cargo.toml b/Cargo.toml index e67c8c9..f237a5c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,6 +9,7 @@ default = ["full"] full = ["dep:rocksdb"] [dependencies] +asn1-rs = "0.7.1" der-parser = { version = "10.0.0", features = ["serialize"] } hex = "0.4.3" base64 = "0.22.1" diff --git a/benchmark/routinator_object_bench/Cargo.toml b/benchmark/routinator_object_bench/Cargo.toml new file mode 100644 index 0000000..25986ca --- /dev/null +++ b/benchmark/routinator_object_bench/Cargo.toml @@ -0,0 +1,8 @@ +[package] +name = "routinator-object-bench" +version = "0.1.0" +edition = "2024" +publish = false + +[dependencies] +rpki = { version = "=0.19.1", features = ["repository"] } diff --git a/benchmark/routinator_object_bench/src/main.rs b/benchmark/routinator_object_bench/src/main.rs new file mode 100644 index 0000000..21f1c50 --- /dev/null +++ b/benchmark/routinator_object_bench/src/main.rs @@ -0,0 +1,552 @@ +use rpki::repository::cert::Cert; +use rpki::repository::crl::Crl; +use rpki::repository::manifest::Manifest; +use rpki::repository::roa::Roa; +use rpki::repository::aspa::Aspa; +use rpki::repository::resources::{AsResources, IpResources}; + +use std::hint::black_box; +use std::path::{Path, PathBuf}; +use std::time::Instant; + +#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)] +enum ObjType { + Cer, + Crl, + Manifest, + Roa, + Aspa, +} + +impl ObjType { + fn parse(s: &str) -> Result { + match s { + "cer" => Ok(Self::Cer), + "crl" => Ok(Self::Crl), + "manifest" => Ok(Self::Manifest), + "roa" => Ok(Self::Roa), + "aspa" => Ok(Self::Aspa), + _ => Err("type must be one of: cer, crl, manifest, roa, aspa".into()), + } + } + + fn as_str(self) -> &'static str { + match self { + ObjType::Cer => "cer", + ObjType::Crl => "crl", + ObjType::Manifest => "manifest", + ObjType::Roa => "roa", + ObjType::Aspa => "aspa", + } + } + + fn ext(self) -> &'static str { + match self { + ObjType::Cer => "cer", + ObjType::Crl => "crl", + ObjType::Manifest => "mft", + ObjType::Roa => "roa", + ObjType::Aspa => "asa", + } + } +} + +#[derive(Clone, Debug)] +struct Sample { + obj_type: ObjType, + name: String, + path: PathBuf, +} + +#[derive(Clone, Debug)] +struct Config { + dir: PathBuf, + type_filter: Option, + sample_filter: Option, + fixed_iters: Option, + warmup_iters: u64, + rounds: u64, + min_round_ms: u64, + max_adaptive_iters: u64, + strict: bool, + cert_inspect: bool, + out_csv: Option, + out_md: Option, +} + +fn usage_and_exit(err: Option<&str>) -> ! { + if let Some(err) = err { + eprintln!("error: {err}"); + eprintln!(); + } + eprintln!( + "Usage:\n\ + cargo run --release --manifest-path rpki/benchmark/routinator_object_bench/Cargo.toml -- [OPTIONS]\n\ +\n\ +Options:\n\ + --dir Fixtures root dir (default: ../../tests/benchmark/selected_der_v2)\n\ + --type Filter by type\n\ + --sample Filter by sample name (e.g. p50)\n\ + --iters Fixed iterations per round (optional; otherwise adaptive)\n\ + --warmup-iters Warmup iterations (default: 50)\n\ + --rounds Rounds (default: 5)\n\ + --min-round-ms Adaptive: minimum round time (default: 200)\n\ + --max-iters Adaptive: maximum iters (default: 1_000_000)\n\ + --strict Strict DER where applicable (default: true)\n\ + --cert-inspect Also run Cert::inspect_ca/inspect_ee where applicable (default: false)\n\ + --out-csv Write CSV output\n\ + --out-md Write Markdown output\n\ +" + ); + std::process::exit(2); +} + +fn parse_bool(s: &str, name: &str) -> bool { + match s { + "1" | "true" | "TRUE" | "yes" | "YES" => true, + "0" | "false" | "FALSE" | "no" | "NO" => false, + _ => usage_and_exit(Some(&format!("{name} must be true/false"))), + } +} + +fn parse_u64(s: &str, name: &str) -> u64 { + s.parse::() + .unwrap_or_else(|_| usage_and_exit(Some(&format!("{name} must be an integer")))) +} + +fn default_samples_dir() -> PathBuf { + PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../../tests/benchmark/selected_der_v2") +} + +fn parse_args() -> Config { + let mut dir: PathBuf = default_samples_dir(); + let mut type_filter: Option = None; + let mut sample_filter: Option = None; + let mut fixed_iters: Option = None; + let mut warmup_iters: u64 = 50; + let mut rounds: u64 = 5; + let mut min_round_ms: u64 = 200; + let mut max_adaptive_iters: u64 = 1_000_000; + let mut strict: bool = true; + let mut cert_inspect: bool = false; + let mut out_csv: Option = None; + let mut out_md: Option = None; + + let mut args = std::env::args().skip(1); + while let Some(arg) = args.next() { + match arg.as_str() { + "--dir" => dir = PathBuf::from(args.next().unwrap_or_else(|| usage_and_exit(None))), + "--type" => { + type_filter = Some(ObjType::parse( + &args.next().unwrap_or_else(|| usage_and_exit(None)), + ) + .unwrap_or_else(|e| usage_and_exit(Some(&e)))) + } + "--sample" => { + sample_filter = Some(args.next().unwrap_or_else(|| usage_and_exit(None))) + } + "--iters" => { + fixed_iters = Some(parse_u64( + &args.next().unwrap_or_else(|| usage_and_exit(None)), + "--iters", + )) + } + "--warmup-iters" => { + warmup_iters = parse_u64( + &args.next().unwrap_or_else(|| usage_and_exit(None)), + "--warmup-iters", + ) + } + "--rounds" => { + rounds = parse_u64(&args.next().unwrap_or_else(|| usage_and_exit(None)), "--rounds") + } + "--min-round-ms" => { + min_round_ms = parse_u64( + &args.next().unwrap_or_else(|| usage_and_exit(None)), + "--min-round-ms", + ) + } + "--max-iters" => { + max_adaptive_iters = parse_u64( + &args.next().unwrap_or_else(|| usage_and_exit(None)), + "--max-iters", + ) + } + "--strict" => { + strict = parse_bool( + &args.next().unwrap_or_else(|| usage_and_exit(None)), + "--strict", + ) + } + "--cert-inspect" => cert_inspect = true, + "--out-csv" => out_csv = Some(PathBuf::from(args.next().unwrap_or_else(|| usage_and_exit(None)))), + "--out-md" => out_md = Some(PathBuf::from(args.next().unwrap_or_else(|| usage_and_exit(None)))), + "-h" | "--help" => usage_and_exit(None), + _ => usage_and_exit(Some(&format!("unknown argument: {arg}"))), + } + } + + if warmup_iters == 0 { + usage_and_exit(Some("--warmup-iters must be > 0")); + } + if rounds == 0 { + usage_and_exit(Some("--rounds must be > 0")); + } + if min_round_ms == 0 { + usage_and_exit(Some("--min-round-ms must be > 0")); + } + if max_adaptive_iters == 0 { + usage_and_exit(Some("--max-iters must be > 0")); + } + if let Some(n) = fixed_iters { + if n == 0 { + usage_and_exit(Some("--iters must be > 0")); + } + } + + Config { + dir, + type_filter, + sample_filter, + fixed_iters, + warmup_iters, + rounds, + min_round_ms, + max_adaptive_iters, + strict, + cert_inspect, + out_csv, + out_md, + } +} + +fn read_samples(root: &Path) -> Vec { + let mut out = Vec::new(); + for obj_type in [ + ObjType::Cer, + ObjType::Crl, + ObjType::Manifest, + ObjType::Roa, + ObjType::Aspa, + ] { + let dir = root.join(obj_type.as_str()); + let rd = match std::fs::read_dir(&dir) { + Ok(rd) => rd, + Err(_) => continue, + }; + for ent in rd.flatten() { + let path = ent.path(); + if path.extension().and_then(|s| s.to_str()) != Some(obj_type.ext()) { + continue; + } + let name = path + .file_stem() + .and_then(|s| s.to_str()) + .unwrap_or("unknown") + .to_string(); + out.push(Sample { obj_type, name, path }); + } + } + out.sort_by(|a, b| a.obj_type.cmp(&b.obj_type).then_with(|| a.name.cmp(&b.name))); + out +} + +fn choose_iters_adaptive(mut op: F, min_round_ms: u64, max_iters: u64) -> u64 { + let min_secs = (min_round_ms as f64) / 1e3; + let mut iters: u64 = 1; + loop { + let start = Instant::now(); + for _ in 0..iters { + op(); + } + let elapsed = start.elapsed().as_secs_f64(); + if elapsed >= min_secs { + return iters; + } + if iters >= max_iters { + return iters; + } + iters = (iters.saturating_mul(2)).min(max_iters); + } +} + +fn count_ip(res: &IpResources) -> u64 { + if res.is_inherited() { + return 1; + } + let Ok(blocks) = res.to_blocks() else { + return 0; + }; + blocks.iter().count() as u64 +} + +fn count_as(res: &AsResources) -> u64 { + if res.is_inherited() { + return 1; + } + let Ok(blocks) = res.to_blocks() else { + return 0; + }; + blocks.iter().count() as u64 +} + +fn complexity(obj_type: ObjType, bytes: &[u8], strict: bool, cert_inspect: bool) -> u64 { + match obj_type { + ObjType::Cer => { + let cert = Cert::decode(bytes).expect("decode cert"); + if cert_inspect { + if cert.is_ca() { + cert.inspect_ca(strict).expect("inspect ca"); + } else { + cert.inspect_ee(strict).expect("inspect ee"); + } + } + count_ip(cert.v4_resources()) + .saturating_add(count_ip(cert.v6_resources())) + .saturating_add(count_as(cert.as_resources())) + } + ObjType::Crl => { + let crl = Crl::decode(bytes).expect("decode crl"); + crl.revoked_certs().iter().count() as u64 + } + ObjType::Manifest => { + let mft = Manifest::decode(bytes, strict).expect("decode manifest"); + if cert_inspect { + mft.cert().inspect_ee(strict).expect("inspect ee"); + } + mft.content().len() as u64 + } + ObjType::Roa => { + let roa = Roa::decode(bytes, strict).expect("decode roa"); + if cert_inspect { + roa.cert().inspect_ee(strict).expect("inspect ee"); + } + roa.content().iter().count() as u64 + } + ObjType::Aspa => { + let asa = Aspa::decode(bytes, strict).expect("decode aspa"); + if cert_inspect { + asa.cert().inspect_ee(strict).expect("inspect ee"); + } + asa.content().provider_as_set().len() as u64 + } + } +} + +fn decode_profile(obj_type: ObjType, bytes: &[u8], strict: bool, cert_inspect: bool) { + match obj_type { + ObjType::Cer => { + let cert = Cert::decode(black_box(bytes)).expect("decode cert"); + if cert_inspect { + if cert.is_ca() { + cert.inspect_ca(strict).expect("inspect ca"); + } else { + cert.inspect_ee(strict).expect("inspect ee"); + } + } + black_box(cert); + } + ObjType::Crl => { + let crl = Crl::decode(black_box(bytes)).expect("decode crl"); + black_box(crl); + } + ObjType::Manifest => { + let mft = Manifest::decode(black_box(bytes), strict).expect("decode manifest"); + if cert_inspect { + mft.cert().inspect_ee(strict).expect("inspect ee"); + } + black_box(mft); + } + ObjType::Roa => { + let roa = Roa::decode(black_box(bytes), strict).expect("decode roa"); + if cert_inspect { + roa.cert().inspect_ee(strict).expect("inspect ee"); + } + black_box(roa); + } + ObjType::Aspa => { + let asa = Aspa::decode(black_box(bytes), strict).expect("decode aspa"); + if cert_inspect { + asa.cert().inspect_ee(strict).expect("inspect ee"); + } + black_box(asa); + } + } +} + +#[derive(Clone, Debug)] +struct ResultRow { + obj_type: String, + sample: String, + size_bytes: usize, + complexity: u64, + avg_ns_per_op: f64, + ops_per_sec: f64, +} + +fn render_markdown(title: &str, rows: &[ResultRow]) -> String { + let mut out = String::new(); + out.push_str(&format!("# {title}\n\n")); + out.push_str("| type | sample | size_bytes | complexity | avg ns/op | ops/s |\n"); + out.push_str("|---|---|---:|---:|---:|---:|\n"); + for r in rows { + out.push_str(&format!( + "| {} | {} | {} | {} | {:.2} | {:.2} |\n", + r.obj_type, r.sample, r.size_bytes, r.complexity, r.avg_ns_per_op, r.ops_per_sec + )); + } + out +} + +fn render_csv(rows: &[ResultRow]) -> String { + let mut out = String::new(); + out.push_str("type,sample,size_bytes,complexity,avg_ns_per_op,ops_per_sec\n"); + for r in rows { + let sample = r.sample.replace('"', "\"\""); + out.push_str(&format!( + "{},{},{},{},{:.6},{:.6}\n", + r.obj_type, + format!("\"{}\"", sample), + r.size_bytes, + r.complexity, + r.avg_ns_per_op, + r.ops_per_sec + )); + } + out +} + +fn create_parent_dirs(path: &Path) { + if let Some(parent) = path.parent() { + std::fs::create_dir_all(parent).unwrap_or_else(|e| { + panic!("create_dir_all {}: {e}", parent.display()); + }); + } +} + +fn write_text_file(path: &Path, content: &str) { + create_parent_dirs(path); + std::fs::write(path, content).unwrap_or_else(|e| panic!("write {}: {e}", path.display())); +} + +fn main() { + let cfg = parse_args(); + let mut samples = read_samples(&cfg.dir); + if samples.is_empty() { + usage_and_exit(Some(&format!( + "no samples found under: {}", + cfg.dir.display() + ))); + } + + if let Some(t) = cfg.type_filter { + samples.retain(|s| s.obj_type == t); + if samples.is_empty() { + usage_and_exit(Some(&format!("no sample matched --type {}", t.as_str()))); + } + } + if let Some(filter) = cfg.sample_filter.as_deref() { + samples.retain(|s| s.name == filter); + if samples.is_empty() { + usage_and_exit(Some(&format!("no sample matched --sample {filter}"))); + } + } + + println!("# Routinator baseline (rpki crate) decode benchmark (selected_der_v2)"); + println!(); + println!("- dir: {}", cfg.dir.display()); + println!("- strict: {}", cfg.strict); + println!("- cert_inspect: {}", cfg.cert_inspect); + if let Some(t) = cfg.type_filter { + println!("- type: {}", t.as_str()); + } + if let Some(s) = cfg.sample_filter.as_deref() { + println!("- sample: {}", s); + } + if let Some(n) = cfg.fixed_iters { + println!("- iters: {} (fixed)", n); + } else { + println!( + "- warmup: {} iters, rounds: {}, min_round: {}ms (adaptive iters, max {})", + cfg.warmup_iters, cfg.rounds, cfg.min_round_ms, cfg.max_adaptive_iters + ); + } + if let Some(p) = cfg.out_csv.as_ref() { + println!("- out_csv: {}", p.display()); + } + if let Some(p) = cfg.out_md.as_ref() { + println!("- out_md: {}", p.display()); + } + println!(); + + println!("| type | sample | size_bytes | complexity | avg ns/op | ops/s |"); + println!("|---|---|---:|---:|---:|---:|"); + + let mut rows: Vec = Vec::with_capacity(samples.len()); + for sample in &samples { + let bytes = std::fs::read(&sample.path) + .unwrap_or_else(|e| panic!("read {}: {e}", sample.path.display())); + let size_bytes = bytes.len(); + let complexity = complexity(sample.obj_type, bytes.as_slice(), cfg.strict, cfg.cert_inspect); + + for _ in 0..cfg.warmup_iters { + decode_profile(sample.obj_type, bytes.as_slice(), cfg.strict, cfg.cert_inspect); + } + + let mut per_round_ns_per_op = Vec::with_capacity(cfg.rounds as usize); + for _round in 0..cfg.rounds { + let iters = if let Some(n) = cfg.fixed_iters { + n + } else { + choose_iters_adaptive( + || decode_profile(sample.obj_type, bytes.as_slice(), cfg.strict, cfg.cert_inspect), + cfg.min_round_ms, + cfg.max_adaptive_iters, + ) + }; + let start = Instant::now(); + for _ in 0..iters { + decode_profile(sample.obj_type, bytes.as_slice(), cfg.strict, cfg.cert_inspect); + } + let elapsed = start.elapsed(); + let total_ns = elapsed.as_secs_f64() * 1e9; + per_round_ns_per_op.push(total_ns / (iters as f64)); + } + + let avg_ns = per_round_ns_per_op.iter().sum::() / (per_round_ns_per_op.len() as f64); + let ops_per_sec = 1e9_f64 / avg_ns; + + println!( + "| {} | {} | {} | {} | {:.2} | {:.2} |", + sample.obj_type.as_str(), + sample.name, + size_bytes, + complexity, + avg_ns, + ops_per_sec + ); + + rows.push(ResultRow { + obj_type: sample.obj_type.as_str().to_string(), + sample: sample.name.clone(), + size_bytes, + complexity, + avg_ns_per_op: avg_ns, + ops_per_sec, + }); + } + + if let Some(path) = cfg.out_md.as_ref() { + let md = render_markdown( + "Routinator baseline (rpki crate) decode+inspect (selected_der_v2)", + &rows, + ); + write_text_file(path, &md); + eprintln!("Wrote {}", path.display()); + } + if let Some(path) = cfg.out_csv.as_ref() { + let csv = render_csv(&rows); + write_text_file(path, &csv); + eprintln!("Wrote {}", path.display()); + } +} diff --git a/scripts/stage2_perf_compare_m4.sh b/scripts/stage2_perf_compare_m4.sh new file mode 100755 index 0000000..21d34de --- /dev/null +++ b/scripts/stage2_perf_compare_m4.sh @@ -0,0 +1,123 @@ +#!/usr/bin/env bash +set -euo pipefail + +# M4: Compare decode+profile (OURS) vs routinator baseline (rpki crate 0.19.1) +# on selected_der_v2 fixtures (cer/crl/manifest/roa/aspa). +# +# Outputs under: +# - rpki/target/bench/stage2_selected_der_v2_routinator_decode_release.{csv,md} +# - rpki/target/bench/stage2_selected_der_v2_compare_ours_vs_routinator_decode_release.{csv,md} +# +# Notes: +# - OURS decode benchmark is produced by: +# `cargo test --release --test bench_stage2_decode_profile_selected_der_v2 -- --ignored --nocapture` +# and writes `stage2_selected_der_v2_decode_release.csv` when BENCH_OUT_CSV is set. + +ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +RPKI_DIR="$ROOT_DIR" + +OUT_DIR="$RPKI_DIR/target/bench" +mkdir -p "$OUT_DIR" + +OURS_CSV="${OURS_CSV:-$OUT_DIR/stage2_selected_der_v2_decode_release.csv}" + +ROUT_CSV="${ROUT_CSV:-$OUT_DIR/stage2_selected_der_v2_routinator_decode_release.csv}" +ROUT_MD="${ROUT_MD:-$OUT_DIR/stage2_selected_der_v2_routinator_decode_release.md}" + +COMPARE_CSV="${COMPARE_CSV:-$OUT_DIR/stage2_selected_der_v2_compare_ours_vs_routinator_decode_release.csv}" +COMPARE_MD="${COMPARE_MD:-$OUT_DIR/stage2_selected_der_v2_compare_ours_vs_routinator_decode_release.md}" + +WARMUP_ITERS="${WARMUP_ITERS:-10}" +ROUNDS="${ROUNDS:-3}" +MIN_ROUND_MS="${MIN_ROUND_MS:-200}" + +if [[ ! -f "$OURS_CSV" ]]; then + echo "ERROR: missing OURS CSV: $OURS_CSV" >&2 + echo "Hint: run:" >&2 + echo " cd rpki && BENCH_WARMUP_ITERS=$WARMUP_ITERS BENCH_ROUNDS=$ROUNDS BENCH_MIN_ROUND_MS=$MIN_ROUND_MS \\" >&2 + echo " BENCH_OUT_CSV=target/bench/stage2_selected_der_v2_decode_release.csv \\" >&2 + echo " cargo test --release --test bench_stage2_decode_profile_selected_der_v2 -- --ignored --nocapture" >&2 + exit 2 +fi + +echo "[1/2] Run routinator baseline bench (release)..." >&2 +(cd "$RPKI_DIR/benchmark/routinator_object_bench" && cargo run --release -q -- \ + --dir "$RPKI_DIR/tests/benchmark/selected_der_v2" \ + --warmup-iters "$WARMUP_ITERS" \ + --rounds "$ROUNDS" \ + --min-round-ms "$MIN_ROUND_MS" \ + --out-csv "$ROUT_CSV" \ + --out-md "$ROUT_MD") + +echo "[2/2] Join CSVs + compute ratios..." >&2 +python3 - "$OURS_CSV" "$ROUT_CSV" "$COMPARE_CSV" "$COMPARE_MD" <<'PY' +import csv +import sys +from pathlib import Path + +ours_path = Path(sys.argv[1]) +rout_path = Path(sys.argv[2]) +out_csv_path = Path(sys.argv[3]) +out_md_path = Path(sys.argv[4]) + +def read_csv(path: Path): + with path.open(newline="") as f: + return list(csv.DictReader(f)) + +ours_rows = read_csv(ours_path) +rout_rows = read_csv(rout_path) + +rout_by_key = {(r["type"], r["sample"]): r for r in rout_rows} + +out_rows = [] +for r in ours_rows: + key = (r["type"], r["sample"]) + rr = rout_by_key.get(key) + if rr is None: + raise SystemExit(f"missing routinator row for {key}") + + ours_ns = float(r["avg_ns_per_op"]) + rout_ns = float(rr["avg_ns_per_op"]) + ratio = (ours_ns / rout_ns) if rout_ns != 0.0 else float("inf") + + out_rows.append({ + "type": r["type"], + "sample": r["sample"], + "size_bytes": r["size_bytes"], + "complexity": r["complexity"], + "ours_avg_ns_per_op": f"{ours_ns:.2f}", + "ours_ops_per_sec": f"{float(r['ops_per_sec']):.2f}", + "rout_avg_ns_per_op": f"{rout_ns:.2f}", + "rout_ops_per_sec": f"{float(rr['ops_per_sec']):.2f}", + "ratio_ours_over_rout": f"{ratio:.4f}", + }) + +out_rows.sort(key=lambda x: (x["type"], x["sample"])) + +out_csv_path.parent.mkdir(parents=True, exist_ok=True) +with out_csv_path.open("w", newline="") as f: + w = csv.DictWriter(f, fieldnames=list(out_rows[0].keys())) + w.writeheader() + w.writerows(out_rows) + +lines = [] +lines.append("# Stage2 ours vs routinator (decode+profile, selected_der_v2)\n") +lines.append(f"- ours_csv: `{ours_path}`\n") +lines.append(f"- rout_csv: `{rout_path}`\n") +lines.append("\n") +lines.append("| type | sample | size_bytes | complexity | ours ns/op | rout ns/op | ratio |\n") +lines.append("|---|---|---:|---:|---:|---:|---:|\n") +for r in out_rows: + lines.append( + f"| {r['type']} | {r['sample']} | {r['size_bytes']} | {r['complexity']} | " + f"{r['ours_avg_ns_per_op']} | {r['rout_avg_ns_per_op']} | {r['ratio_ours_over_rout']} |\n" + ) + +out_md_path.parent.mkdir(parents=True, exist_ok=True) +out_md_path.write_text("".join(lines), encoding="utf-8") +PY + +echo "Done." >&2 +echo "- routinator CSV: $ROUT_CSV" >&2 +echo "- compare CSV: $COMPARE_CSV" >&2 +echo "- compare MD: $COMPARE_MD" >&2 diff --git a/specs/arch.excalidraw b/specs/arch.excalidraw index 6dbed3f..5aa602c 100644 --- a/specs/arch.excalidraw +++ b/specs/arch.excalidraw @@ -4601,6 +4601,925 @@ "startArrowhead": null, "endArrowhead": "arrow", "elbowed": false + }, + { + "id": "nz1R30GbU3V27e8siGpj0", + "type": "text", + "x": 163.01948306749506, + "y": 1851.404535362782, + "width": 474.99176025390625, + "height": 35, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "b17", + "roundness": null, + "seed": 1508242701, + "version": 456, + "versionNonce": 167514925, + "isDeleted": false, + "boundElements": [], + "updated": 1772001228159, + "link": null, + "locked": false, + "text": "V2 Parallel processing, multi worker", + "fontSize": 28, + "fontFamily": 5, + "textAlign": "center", + "verticalAlign": "top", + "containerId": null, + "originalText": "V2 Parallel processing, multi worker", + "autoResize": true, + "lineHeight": 1.25 + }, + { + "id": "NIt4aSy4mw5_vhn-d1TAT", + "type": "rectangle", + "x": 355.40690519895884, + "y": 2085.1589227446907, + "width": 273.0000305175781, + "height": 73.3333740234375, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "b18", + "roundness": null, + "seed": 1210072707, + "version": 273, + "versionNonce": 330766435, + "isDeleted": false, + "boundElements": [], + "updated": 1772001252588, + "link": null, + "locked": false + }, + { + "id": "a_hHz-mHKABQQ_yB-RBIj", + "type": "rectangle", + "x": 367.0735922106776, + "y": 2092.4923272857063, + "width": 34, + "height": 57.33331298828125, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "b19", + "roundness": null, + "seed": 658208291, + "version": 200, + "versionNonce": 1922831363, + "isDeleted": false, + "boundElements": [], + "updated": 1772001252588, + "link": null, + "locked": false + }, + { + "id": "eSio7xtVYJ4ar8wKPeH8d", + "type": "rectangle", + "x": 582.0735616930995, + "y": 2092.492296768128, + "width": 34, + "height": 57.33331298828125, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "b1A", + "roundness": null, + "seed": 1498331587, + "version": 270, + "versionNonce": 245406627, + "isDeleted": false, + "boundElements": [], + "updated": 1772001252588, + "link": null, + "locked": false + }, + { + "id": "rf9t-wepa665vlJ95Mk85", + "type": "text", + "x": 469.7402487048182, + "y": 2107.158953262269, + "width": 27.399978637695312, + "height": 25, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "b1B", + "roundness": null, + "seed": 1534626147, + "version": 189, + "versionNonce": 871575363, + "isDeleted": false, + "boundElements": [], + "updated": 1772001252588, + "link": null, + "locked": false, + "text": ".....", + "fontSize": 20, + "fontFamily": 5, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": ".....", + "autoResize": true, + "lineHeight": 1.25 + }, + { + "id": "DQYVJDhTuFzzErQYWLhaI", + "type": "rectangle", + "x": 357.53235615026176, + "y": 2195.678869684095, + "width": 273.0000305175781, + "height": 73.3333740234375, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "b1C", + "roundness": null, + "seed": 1540194797, + "version": 336, + "versionNonce": 379213443, + "isDeleted": false, + "boundElements": [ + { + "id": "cx18t9XNHa3SxhzWynvq8", + "type": "arrow" + } + ], + "updated": 1772001381161, + "link": null, + "locked": false + }, + { + "id": "F8lD1sLFlDaKou9hjWJYH", + "type": "rectangle", + "x": 369.1990431619805, + "y": 2203.0122742251106, + "width": 34, + "height": 57.33331298828125, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "b1D", + "roundness": null, + "seed": 2080422989, + "version": 262, + "versionNonce": 1666833389, + "isDeleted": false, + "boundElements": [], + "updated": 1772001314228, + "link": null, + "locked": false + }, + { + "id": "J-xIvXgkMjPMHt6t6QsU5", + "type": "rectangle", + "x": 584.1990126444024, + "y": 2203.0122437075324, + "width": 34, + "height": 57.33331298828125, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "b1E", + "roundness": null, + "seed": 2057153197, + "version": 332, + "versionNonce": 1156699725, + "isDeleted": false, + "boundElements": [], + "updated": 1772001314228, + "link": null, + "locked": false + }, + { + "id": "RCjALdkMhYpWm5oLR6Fhg", + "type": "text", + "x": 471.86569965612114, + "y": 2217.678900201673, + "width": 27.399978637695312, + "height": 25, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "b1F", + "roundness": null, + "seed": 1213058317, + "version": 251, + "versionNonce": 2052818093, + "isDeleted": false, + "boundElements": [], + "updated": 1772001314228, + "link": null, + "locked": false, + "text": ".....", + "fontSize": 20, + "fontFamily": 5, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": ".....", + "autoResize": true, + "lineHeight": 1.25 + }, + { + "id": "aaiPz7Zgg6qrhotinSLMg", + "type": "rectangle", + "x": 357.5324047964888, + "y": 2307.2616393916046, + "width": 273.0000305175781, + "height": 73.3333740234375, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "b1G", + "roundness": null, + "seed": 949960483, + "version": 326, + "versionNonce": 1452020173, + "isDeleted": false, + "boundElements": [ + { + "id": "1y4vVTwY-t4qgzTFGRHkw", + "type": "arrow" + } + ], + "updated": 1772001385208, + "link": null, + "locked": false + }, + { + "id": "tQQePVBD3t4lhQvSw-a3x", + "type": "rectangle", + "x": 369.1990918082075, + "y": 2314.5950439326202, + "width": 34, + "height": 57.33331298828125, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "b1H", + "roundness": null, + "seed": 2102485699, + "version": 252, + "versionNonce": 1087497251, + "isDeleted": false, + "boundElements": [], + "updated": 1772001320310, + "link": null, + "locked": false + }, + { + "id": "czuCaayGMFyZ8Tx-nkBi4", + "type": "rectangle", + "x": 584.1990612906294, + "y": 2314.595013415042, + "width": 34, + "height": 57.33331298828125, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "b1I", + "roundness": null, + "seed": 457052771, + "version": 322, + "versionNonce": 300378051, + "isDeleted": false, + "boundElements": [], + "updated": 1772001320310, + "link": null, + "locked": false + }, + { + "id": "LYQgQkkRnMtDIMw-XVjGv", + "type": "text", + "x": 471.86574830234815, + "y": 2329.2616699091827, + "width": 27.399978637695312, + "height": 25, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "b1J", + "roundness": null, + "seed": 590597635, + "version": 241, + "versionNonce": 415987555, + "isDeleted": false, + "boundElements": [], + "updated": 1772001320310, + "link": null, + "locked": false, + "text": ".....", + "fontSize": 20, + "fontFamily": 5, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": ".....", + "autoResize": true, + "lineHeight": 1.25 + }, + { + "id": "VXaedscRg23gtj4VAfx3n", + "type": "ellipse", + "x": 757.5801654506954, + "y": 2205.7782954366144, + "width": 156.21587759051351, + "height": 91.39166671731073, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "#ffc9c9", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "b1K", + "roundness": { + "type": 2 + }, + "seed": 71609603, + "version": 93, + "versionNonce": 180497411, + "isDeleted": false, + "boundElements": [ + { + "type": "text", + "id": "UdgzV1ZK-Eu4Ic_-Dzbun" + }, + { + "id": "u5himsZ3owA4D1i_lwkWo", + "type": "arrow" + }, + { + "id": "GVzVTq13ZZTqxbwQA1LJt", + "type": "arrow" + } + ], + "updated": 1772001524347, + "link": null, + "locked": false + }, + { + "id": "UdgzV1ZK-Eu4Ic_-Dzbun", + "type": "text", + "x": 798.1074831027793, + "y": 2239.162295155394, + "width": 74.69993591308594, + "height": 25, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "#ffc9c9", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "b1L", + "roundness": null, + "seed": 1863312717, + "version": 12, + "versionNonce": 1551635843, + "isDeleted": false, + "boundElements": null, + "updated": 1772001370485, + "link": null, + "locked": false, + "text": "shuffler", + "fontSize": 20, + "fontFamily": 5, + "textAlign": "center", + "verticalAlign": "middle", + "containerId": "VXaedscRg23gtj4VAfx3n", + "originalText": "shuffler", + "autoResize": true, + "lineHeight": 1.25 + }, + { + "id": "Y2RlD_fFLoEw5QJ5QcMR0", + "type": "arrow", + "x": 650.2482003533373, + "y": 2131.3897498674564, + "width": 94.57974585181091, + "height": 74.38854556915794, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "#ffc9c9", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "b1M", + "roundness": { + "type": 2 + }, + "seed": 849259149, + "version": 40, + "versionNonce": 1370636813, + "isDeleted": false, + "boundElements": null, + "updated": 1772001376501, + "link": null, + "locked": false, + "points": [ + [ + 0, + 0 + ], + [ + 94.57974585181091, + 74.38854556915794 + ] + ], + "lastCommittedPoint": null, + "startBinding": null, + "endBinding": null, + "startArrowhead": null, + "endArrowhead": "arrow", + "elbowed": false + }, + { + "id": "cx18t9XNHa3SxhzWynvq8", + "type": "arrow", + "x": 648.1228466944885, + "y": 2231.2829285126163, + "width": 85.01541115585599, + "height": 13.814944721198572, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "#ffc9c9", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "b1N", + "roundness": { + "type": 2 + }, + "seed": 865630317, + "version": 33, + "versionNonce": 921414371, + "isDeleted": false, + "boundElements": null, + "updated": 1772001381161, + "link": null, + "locked": false, + "points": [ + [ + 0, + 0 + ], + [ + 85.01541115585599, + 13.814944721198572 + ] + ], + "lastCommittedPoint": null, + "startBinding": { + "elementId": "DQYVJDhTuFzzErQYWLhaI", + "focus": -0.4435544400037994, + "gap": 17.590460026648657 + }, + "endBinding": null, + "startArrowhead": null, + "endArrowhead": "arrow", + "elbowed": false + }, + { + "id": "1y4vVTwY-t4qgzTFGRHkw", + "type": "arrow", + "x": 649.1855235239129, + "y": 2337.552168134323, + "width": 85.01545980208311, + "height": 40.38220598039834, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "#ffc9c9", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "b1O", + "roundness": { + "type": 2 + }, + "seed": 371136909, + "version": 42, + "versionNonce": 1853196141, + "isDeleted": false, + "boundElements": null, + "updated": 1772001385208, + "link": null, + "locked": false, + "points": [ + [ + 0, + 0 + ], + [ + 85.01545980208311, + -40.38220598039834 + ] + ], + "lastCommittedPoint": null, + "startBinding": { + "elementId": "aaiPz7Zgg6qrhotinSLMg", + "focus": 0.6632382229237758, + "gap": 14 + }, + "endBinding": null, + "startArrowhead": null, + "endArrowhead": "arrow", + "elbowed": false + }, + { + "id": "4BHU-kOejBKiEg7WbwFrO", + "type": "text", + "x": 700.9183968092904, + "y": 2105.0747078538043, + "width": 130.55987548828125, + "height": 50, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "b1P", + "roundness": null, + "seed": 1849944301, + "version": 257, + "versionNonce": 1581124931, + "isDeleted": false, + "boundElements": [], + "updated": 1772001406467, + "link": null, + "locked": false, + "text": "child\nCA Instances", + "fontSize": 20, + "fontFamily": 5, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "child\nCA Instances", + "autoResize": true, + "lineHeight": 1.25 + }, + { + "id": "u5himsZ3owA4D1i_lwkWo", + "type": "arrow", + "x": 892.5422145753583, + "y": 2296.107139385819, + "width": 639.7412048820175, + "height": 215.7267529628216, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "#ffc9c9", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "b1Q", + "roundness": { + "type": 2 + }, + "seed": 1943525507, + "version": 158, + "versionNonce": 1536842819, + "isDeleted": false, + "boundElements": [], + "updated": 1772001434749, + "link": null, + "locked": false, + "points": [ + [ + 0, + 0 + ], + [ + -78.63925288685539, + 215.7267529628216 + ], + [ + -639.7412048820175, + 189.15949170362182 + ], + [ + -560.0392021963971, + 59.510875372307964 + ] + ], + "lastCommittedPoint": null, + "startBinding": { + "elementId": "VXaedscRg23gtj4VAfx3n", + "focus": -0.7716335982433741, + "gap": 11.587857989220094 + }, + "endBinding": null, + "startArrowhead": null, + "endArrowhead": "arrow", + "elbowed": false + }, + { + "id": "H9brdXKovhIkuVtj3tFS1", + "type": "arrow", + "x": 226.23367546480065, + "y": 2442.758779572832, + "width": 108.39469057300934, + "height": 199.78625999786573, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "#ffc9c9", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "b1S", + "roundness": { + "type": 2 + }, + "seed": 1940678531, + "version": 160, + "versionNonce": 655212835, + "isDeleted": false, + "boundElements": null, + "updated": 1772001444761, + "link": null, + "locked": false, + "points": [ + [ + 0, + 0 + ], + [ + 108.39469057300934, + -199.78625999786573 + ] + ], + "lastCommittedPoint": null, + "startBinding": null, + "endBinding": null, + "startArrowhead": null, + "endArrowhead": "arrow", + "elbowed": false + }, + { + "id": "k_HeiQKWqjcVaepIiUy53", + "type": "arrow", + "x": 224.1082974828383, + "y": 2424.6931275339357, + "width": 106.269312591047, + "height": 295.42873132532804, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "#ffc9c9", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "b1T", + "roundness": { + "type": 2 + }, + "seed": 2104759395, + "version": 41, + "versionNonce": 213595907, + "isDeleted": false, + "boundElements": [], + "updated": 1772001452767, + "link": null, + "locked": false, + "points": [ + [ + 0, + 0 + ], + [ + 106.269312591047, + -295.42873132532804 + ] + ], + "lastCommittedPoint": null, + "startBinding": null, + "endBinding": null, + "startArrowhead": null, + "endArrowhead": "arrow", + "elbowed": false + }, + { + "id": "_JLEThxgSltZ1tj_es8Pd", + "type": "text", + "x": 962.6484905842728, + "y": 2092.3223913158026, + "width": 187.73983764648438, + "height": 25, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "b1V", + "roundness": null, + "seed": 1067471331, + "version": 121, + "versionNonce": 8657635, + "isDeleted": false, + "boundElements": [ + { + "id": "GVzVTq13ZZTqxbwQA1LJt", + "type": "arrow" + } + ], + "updated": 1772001539469, + "link": null, + "locked": false, + "text": "TALs CA Instances", + "fontSize": 20, + "fontFamily": 5, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "TALs CA Instances", + "autoResize": true, + "lineHeight": 1.25 + }, + { + "id": "GVzVTq13ZZTqxbwQA1LJt", + "type": "arrow", + "x": 1076.8824351390615, + "y": 2131.3223913158026, + "width": 158.6413207145456, + "height": 83.92501467310194, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "#ffc9c9", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "b1W", + "roundness": { + "type": 2 + }, + "seed": 2140227085, + "version": 39, + "versionNonce": 1940178563, + "isDeleted": false, + "boundElements": [], + "updated": 1772001539470, + "link": null, + "locked": false, + "points": [ + [ + 0, + 0 + ], + [ + -158.6413207145456, + 83.92501467310194 + ] + ], + "lastCommittedPoint": null, + "startBinding": { + "elementId": "_JLEThxgSltZ1tj_es8Pd", + "focus": -0.604699684061838, + "gap": 14 + }, + "endBinding": { + "elementId": "VXaedscRg23gtj4VAfx3n", + "focus": 0.10814148120478088, + "gap": 20.516226547403985 + }, + "startArrowhead": null, + "endArrowhead": "arrow", + "elbowed": false } ], "appState": { diff --git a/src/audit.rs b/src/audit.rs index bfd9f98..e041cf4 100644 --- a/src/audit.rs +++ b/src/audit.rs @@ -133,28 +133,23 @@ pub fn sha256_hex(bytes: &[u8]) -> String { } pub fn format_roa_ip_prefix(p: &crate::data_model::roa::IpPrefix) -> String { + let addr = p.addr_bytes(); match p.afi { crate::data_model::roa::RoaAfi::Ipv4 => { - if p.addr.len() != 4 { - return format!("ipv4:{:02X?}/{}", p.addr, p.prefix_len); - } format!( "{}.{}.{}.{}{}", - p.addr[0], - p.addr[1], - p.addr[2], - p.addr[3], + addr[0], + addr[1], + addr[2], + addr[3], format!("/{}", p.prefix_len) ) } crate::data_model::roa::RoaAfi::Ipv6 => { - if p.addr.len() != 16 { - return format!("ipv6:{:02X?}/{}", p.addr, p.prefix_len); - } let mut parts = Vec::with_capacity(8); for i in 0..8 { - let hi = p.addr[i * 2] as u16; - let lo = p.addr[i * 2 + 1] as u16; + let hi = addr[i * 2] as u16; + let lo = addr[i * 2 + 1] as u16; parts.push(format!("{:x}", (hi << 8) | lo)); } format!("{}{}", parts.join(":"), format!("/{}", p.prefix_len)) diff --git a/src/cli.rs b/src/cli.rs index fef4621..eccb9ab 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -612,7 +612,7 @@ mod tests { prefix: crate::data_model::roa::IpPrefix { afi: crate::data_model::roa::RoaAfi::Ipv4, prefix_len: 24, - addr: vec![192, 0, 2, 0], + addr: [192, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], }, max_length: 24, }], diff --git a/src/data_model/aspa.rs b/src/data_model/aspa.rs index 0d25c90..c7af962 100644 --- a/src/data_model/aspa.rs +++ b/src/data_model/aspa.rs @@ -1,10 +1,9 @@ use crate::data_model::oid::OID_CT_ASPA; +use crate::data_model::common::{DerReader, der_take_tlv}; use crate::data_model::rc::ResourceCertificate; use crate::data_model::signed_object::{ RpkiSignedObject, RpkiSignedObjectParsed, SignedObjectParseError, SignedObjectValidateError, }; -use der_parser::ber::Class; -use der_parser::der::{DerObject, Tag, parse_der}; #[derive(Clone, Debug, PartialEq, Eq)] pub struct AspaObject { @@ -203,7 +202,7 @@ impl AspaObject { impl AspaEContent { /// Parse step of scheme A (`parse → validate → verify`). pub fn parse_der(der: &[u8]) -> Result { - let (rem, _obj) = parse_der(der).map_err(|e| AspaParseError::Parse(e.to_string()))?; + let (_tag, _value, rem) = der_take_tlv(der).map_err(AspaParseError::Parse)?; if !rem.is_empty() { return Err(AspaParseError::TrailingBytes(rem.len())); } @@ -292,47 +291,66 @@ impl AspaObjectParsed { impl AspaEContentParsed { pub fn validate_profile(self) -> Result { - let (_rem, obj) = - parse_der(&self.der).map_err(|e| AspaProfileError::ProfileDecode(e.to_string()))?; + fn count_elements(mut r: DerReader<'_>) -> Result { + let mut n = 0usize; + while !r.is_empty() { + r.skip_any()?; + n += 1; + } + Ok(n) + } - let seq = obj - .as_sequence() - .map_err(|e| AspaProfileError::ProfileDecode(e.to_string()))?; - if seq.len() != 3 { + let mut r = DerReader::new(&self.der); + let mut seq = r + .take_sequence() + .map_err(|e| AspaProfileError::ProfileDecode(e))?; + if !r.is_empty() { + return Err(AspaProfileError::ProfileDecode( + "trailing bytes after ASProviderAttestation".into(), + )); + } + + let elem_count = + count_elements(seq).map_err(|e| AspaProfileError::ProfileDecode(e.to_string()))?; + if elem_count != 3 { return Err(AspaProfileError::InvalidAttestationSequence); } // version [0] EXPLICIT INTEGER MUST be present and MUST be 1. - let v_obj = &seq[0]; - if v_obj.class() != Class::ContextSpecific || v_obj.tag() != Tag(0) { + if seq + .peek_tag() + .map_err(|e| AspaProfileError::ProfileDecode(e.to_string()))? + != 0xA0 + { return Err(AspaProfileError::VersionMustBeExplicitOne); } - let inner_der = v_obj - .as_slice() + let (inner_tag, inner_val) = seq + .take_explicit(0xA0) .map_err(|e| AspaProfileError::ProfileDecode(e.to_string()))?; - let (rem, inner) = - parse_der(inner_der).map_err(|e| AspaProfileError::ProfileDecode(e.to_string()))?; - if !rem.is_empty() { - return Err(AspaProfileError::ProfileDecode( - "trailing bytes inside ASProviderAttestation.version".into(), - )); + if inner_tag != 0x02 { + return Err(AspaProfileError::VersionMustBeExplicitOne); } - let v = inner - .as_u64() + let v = crate::data_model::common::der_uint_from_bytes(inner_val) .map_err(|e| AspaProfileError::ProfileDecode(e.to_string()))?; if v != 1 { return Err(AspaProfileError::VersionMustBeExplicitOne); } - let customer_u64 = seq[1] - .as_u64() + let customer_u64 = seq + .take_uint_u64() .map_err(|e| AspaProfileError::ProfileDecode(e.to_string()))?; if customer_u64 > u32::MAX as u64 { return Err(AspaProfileError::CustomerAsIdOutOfRange(customer_u64)); } let customer_as_id = customer_u64 as u32; - let providers = parse_providers(&seq[2], customer_as_id)?; + let providers_seq = seq + .take_sequence() + .map_err(|e| AspaProfileError::ProfileDecode(e.to_string()))?; + if !seq.is_empty() { + return Err(AspaProfileError::InvalidAttestationSequence); + } + let providers = parse_providers_cursor(providers_seq, customer_as_id)?; Ok(AspaEContent { version: 1, @@ -342,19 +360,19 @@ impl AspaEContentParsed { } } -fn parse_providers(obj: &DerObject<'_>, customer_as_id: u32) -> Result, AspaProfileError> { - let seq = obj - .as_sequence() - .map_err(|e| AspaProfileError::ProfileDecode(e.to_string()))?; +fn parse_providers_cursor( + mut seq: DerReader<'_>, + customer_as_id: u32, +) -> Result, AspaProfileError> { if seq.is_empty() { return Err(AspaProfileError::EmptyProviders); } - let mut out: Vec = Vec::with_capacity(seq.len()); + let mut out: Vec = Vec::new(); let mut prev: Option = None; - for item in seq { - let v = item - .as_u64() + while !seq.is_empty() { + let v = seq + .take_uint_u64() .map_err(|e| AspaProfileError::ProfileDecode(e.to_string()))?; if v > u32::MAX as u64 { return Err(AspaProfileError::ProviderAsIdOutOfRange(v)); diff --git a/src/data_model/common.rs b/src/data_model/common.rs index 3cc185c..d21a84f 100644 --- a/src/data_model/common.rs +++ b/src/data_model/common.rs @@ -1,5 +1,6 @@ use x509_parser::asn1_rs::Tag; use x509_parser::x509::AlgorithmIdentifier; +use x509_parser::prelude::FromDer; pub type UtcTime = time::OffsetDateTime; @@ -100,6 +101,190 @@ pub fn algorithm_params_absent_or_null(sig: &AlgorithmIdentifier<'_>) -> bool { } } +/// Take a single DER TLV (Tag-Length-Value) from the start of `input`. +/// +/// This helper supports: +/// - short- and long-form lengths (up to 8 length bytes) +/// - only low-tag-number form tags (no high-tag-number form) +/// - definite length only (DER forbids indefinite length) +/// +/// Returns: `(tag_byte, value_bytes, remaining_bytes)`. +pub(crate) fn der_take_tlv(input: &[u8]) -> Result<(u8, &[u8], &[u8]), String> { + if input.len() < 2 { + return Err("truncated DER (need tag+len)".into()); + } + let tag = input[0]; + if (tag & 0x1F) == 0x1F { + return Err("high-tag-number form not supported".into()); + } + let len0 = input[1]; + if len0 == 0x80 { + return Err("indefinite length not allowed in DER".into()); + } + let (len, hdr_len) = if len0 & 0x80 == 0 { + (len0 as usize, 2usize) + } else { + let n = (len0 & 0x7F) as usize; + if n == 0 || n > 8 { + return Err("invalid DER length".into()); + } + if input.len() < 2 + n { + return Err("truncated DER (length bytes)".into()); + } + let mut l: usize = 0; + for &b in &input[2..2 + n] { + l = (l << 8) | (b as usize); + } + (l, 2 + n) + }; + if input.len() < hdr_len + len { + return Err("truncated DER (value bytes)".into()); + } + let value = &input[hdr_len..hdr_len + len]; + let rem = &input[hdr_len + len..]; + Ok((tag, value, rem)) +} + +/// Minimal streaming DER reader built on `der_take_tlv`. +/// +/// This is intentionally small and only supports the subset of DER needed by +/// RPKI object eContent decoders (ROA/ASPA), to avoid constructing a generic AST +/// (which is expensive on large objects such as ROAs with thousands of prefixes). +#[derive(Clone, Copy)] +pub(crate) struct DerReader<'a> { + buf: &'a [u8], +} + +impl<'a> DerReader<'a> { + pub(crate) fn new(buf: &'a [u8]) -> Self { + Self { buf } + } + + pub(crate) fn is_empty(&self) -> bool { + self.buf.is_empty() + } + + pub(crate) fn remaining_len(&self) -> usize { + self.buf.len() + } + + pub(crate) fn peek_tag(&self) -> Result { + self.buf.first().copied().ok_or_else(|| "truncated DER".into()) + } + + pub(crate) fn take_any(&mut self) -> Result<(u8, &'a [u8]), String> { + let (tag, value, rem) = der_take_tlv(self.buf)?; + self.buf = rem; + Ok((tag, value)) + } + + pub(crate) fn take_any_full(&mut self) -> Result<(u8, &'a [u8], &'a [u8]), String> { + let (tag, value, rem) = der_take_tlv(self.buf)?; + let consumed = self.buf.len() - rem.len(); + let full = &self.buf[..consumed]; + self.buf = rem; + Ok((tag, full, value)) + } + + pub(crate) fn skip_any(&mut self) -> Result<(), String> { + let _ = self.take_any()?; + Ok(()) + } + + pub(crate) fn take_tag(&mut self, expected_tag: u8) -> Result<&'a [u8], String> { + let (tag, value) = self.take_any()?; + if tag != expected_tag { + return Err(format!( + "unexpected tag: got 0x{tag:02X}, expected 0x{expected_tag:02X}" + )); + } + Ok(value) + } + + pub(crate) fn take_sequence(&mut self) -> Result, String> { + let value = self.take_tag(0x30)?; + Ok(DerReader::new(value)) + } + + pub(crate) fn take_octet_string(&mut self) -> Result<&'a [u8], String> { + self.take_tag(0x04) + } + + pub(crate) fn take_bit_string(&mut self) -> Result<(u8, &'a [u8]), String> { + let v = self.take_tag(0x03)?; + if v.is_empty() { + return Err("BIT STRING content is empty".into()); + } + Ok((v[0], &v[1..])) + } + + pub(crate) fn take_uint_u64(&mut self) -> Result { + let v = self.take_tag(0x02)?; + der_uint_from_bytes(v) + } + + pub(crate) fn take_explicit(&mut self, expected_outer_tag: u8) -> Result<(u8, &'a [u8]), String> { + let inner_der = self.take_tag(expected_outer_tag)?; + let (tag, value, rem) = der_take_tlv(inner_der)?; + if !rem.is_empty() { + return Err("trailing bytes inside EXPLICIT value".into()); + } + Ok((tag, value)) + } + + pub(crate) fn take_explicit_der(&mut self, expected_outer_tag: u8) -> Result<&'a [u8], String> { + let inner_der = self.take_tag(expected_outer_tag)?; + let (_tag, _value, rem) = der_take_tlv(inner_der)?; + if !rem.is_empty() { + return Err("trailing bytes inside EXPLICIT value".into()); + } + Ok(inner_der) + } +} + +pub(crate) fn der_uint_from_bytes(bytes: &[u8]) -> Result { + if bytes.is_empty() { + return Err("INTEGER has empty content".into()); + } + // Disallow negative values. + if (bytes[0] & 0x80) != 0 { + return Err("INTEGER is negative".into()); + } + // DER requires minimal encoding for INTEGER. + if bytes.len() > 1 && bytes[0] == 0x00 && (bytes[1] & 0x80) == 0 { + return Err("INTEGER not minimally encoded".into()); + } + if bytes.len() > 8 { + return Err("INTEGER does not fit u64".into()); + } + let mut v: u64 = 0; + for &b in bytes { + v = (v << 8) | (b as u64); + } + Ok(v) +} + +#[derive(Clone, Debug, PartialEq, Eq, Hash)] +pub struct X509NameDer(pub Vec); + +impl X509NameDer { + pub fn as_raw(&self) -> &[u8] { + &self.0 + } +} + +impl std::fmt::Display for X509NameDer { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let Ok((rem, name)) = x509_parser::x509::X509Name::from_der(&self.0) else { + return write!(f, ""); + }; + if !rem.is_empty() { + return write!(f, ""); + } + write!(f, "{name}") + } +} + /// Filename extensions registered in IANA "RPKI Repository Name Schemes". /// /// Source: diff --git a/src/data_model/crl.rs b/src/data_model/crl.rs index 6626542..9b5f986 100644 --- a/src/data_model/crl.rs +++ b/src/data_model/crl.rs @@ -1,7 +1,8 @@ pub use crate::data_model::common::{Asn1TimeEncoding, Asn1TimeUtc, BigUnsigned}; use crate::data_model::oid::{ - OID_AUTHORITY_KEY_IDENTIFIER, OID_CRL_NUMBER, OID_SHA256_WITH_RSA_ENCRYPTION, - OID_SUBJECT_KEY_IDENTIFIER, + OID_AUTHORITY_KEY_IDENTIFIER, OID_AUTHORITY_KEY_IDENTIFIER_RAW, OID_CRL_NUMBER, + OID_CRL_NUMBER_RAW, OID_SHA256_WITH_RSA_ENCRYPTION, OID_SHA256_WITH_RSA_ENCRYPTION_RAW, + OID_SUBJECT_KEY_IDENTIFIER_RAW, }; use x509_parser::certificate::X509Certificate; use x509_parser::extensions::{ParsedExtension, X509Extension}; @@ -425,8 +426,15 @@ fn algorithm_identifier_value(ai: &AlgorithmIdentifier<'_>) -> AlgorithmIdentifi tag: p.tag(), data: p.as_bytes().to_vec(), }); + // NOTE(perf): Avoid `to_id_string()` allocations for the signature algorithms we expect + // in RPKI CRLs. Fall back to `to_id_string()` for unexpected algorithms (mostly error paths). + let oid = if ai.algorithm.as_bytes() == OID_SHA256_WITH_RSA_ENCRYPTION_RAW { + OID_SHA256_WITH_RSA_ENCRYPTION.to_string() + } else { + ai.algorithm.to_id_string() + }; AlgorithmIdentifierValue { - oid: ai.algorithm.to_id_string(), + oid, parameters, } } @@ -434,9 +442,8 @@ fn algorithm_identifier_value(ai: &AlgorithmIdentifier<'_>) -> AlgorithmIdentifi fn parse_extensions_parse(exts: &[X509Extension<'_>]) -> Result, String> { let mut out = Vec::with_capacity(exts.len()); for ext in exts { - let oid = ext.oid.to_id_string(); - match oid.as_str() { - OID_AUTHORITY_KEY_IDENTIFIER => { + let oid = ext.oid.as_bytes(); + if oid == OID_AUTHORITY_KEY_IDENTIFIER_RAW { let ParsedExtension::AuthorityKeyIdentifier(aki) = ext.parsed_extension() else { return Err("AKI extension parse failed".to_string()); }; @@ -446,18 +453,19 @@ fn parse_extensions_parse(exts: &[X509Extension<'_>]) -> Result match ext.parsed_extension() { + } else if oid == OID_CRL_NUMBER_RAW { + match ext.parsed_extension() { ParsedExtension::CRLNumber(n) => out.push(CrlExtensionParsed::CrlNumber { number: n.clone(), critical: ext.critical, }), _ => return Err("CRLNumber extension parse failed".to_string()), - }, - _ => out.push(CrlExtensionParsed::Other { - oid, + } + } else { + out.push(CrlExtensionParsed::Other { + oid: ext.oid.to_id_string(), critical: ext.critical, - }), + }) } } Ok(out) @@ -525,7 +533,7 @@ fn validate_extensions_profile( fn get_subject_key_identifier(cert: &X509Certificate<'_>) -> Option> { cert.extensions() .iter() - .find(|ext| ext.oid.to_id_string() == OID_SUBJECT_KEY_IDENTIFIER) + .find(|ext| ext.oid.as_bytes() == OID_SUBJECT_KEY_IDENTIFIER_RAW) .and_then(|ext| match ext.parsed_extension() { ParsedExtension::SubjectKeyIdentifier(ki) => Some(ki.0.to_vec()), _ => None, diff --git a/src/data_model/manifest.rs b/src/data_model/manifest.rs index 12459d5..b486322 100644 --- a/src/data_model/manifest.rs +++ b/src/data_model/manifest.rs @@ -1,4 +1,5 @@ use crate::data_model::common::{BigUnsigned, UtcTime}; +use crate::data_model::common::der_take_tlv; use crate::data_model::oid::{OID_CT_RPKI_MANIFEST, OID_SHA256}; use crate::data_model::rc::ResourceCertificate; use crate::data_model::signed_object::{ @@ -821,42 +822,6 @@ fn oid_content_iter(bytes: &[u8]) -> impl Iterator + '_ { } } -fn der_take_tlv(input: &[u8]) -> Result<(u8, &[u8], &[u8]), String> { - if input.len() < 2 { - return Err("truncated DER (need tag+len)".into()); - } - let tag = input[0]; - if (tag & 0x1F) == 0x1F { - return Err("high-tag-number form not supported".into()); - } - let len0 = input[1]; - if len0 == 0x80 { - return Err("indefinite length not allowed in DER".into()); - } - let (len, hdr_len) = if len0 & 0x80 == 0 { - (len0 as usize, 2usize) - } else { - let n = (len0 & 0x7F) as usize; - if n == 0 || n > 8 { - return Err("invalid DER length".into()); - } - if input.len() < 2 + n { - return Err("truncated DER (length bytes)".into()); - } - let mut l: usize = 0; - for &b in &input[2..2 + n] { - l = (l << 8) | (b as usize); - } - (l, 2 + n) - }; - if input.len() < hdr_len + len { - return Err("truncated DER (value bytes)".into()); - } - let value = &input[hdr_len..hdr_len + len]; - let rem = &input[hdr_len + len..]; - Ok((tag, value, rem)) -} - #[cfg(test)] mod tests { use super::*; diff --git a/src/data_model/oid.rs b/src/data_model/oid.rs index aa84359..8ec4f3a 100644 --- a/src/data_model/oid.rs +++ b/src/data_model/oid.rs @@ -1,42 +1,72 @@ pub const OID_SHA256: &str = "2.16.840.1.101.3.4.2.1"; +pub const OID_SHA256_RAW: &[u8] = &asn1_rs::oid!(raw 2.16.840.1.101.3.4.2.1); pub const OID_SIGNED_DATA: &str = "1.2.840.113549.1.7.2"; +pub const OID_SIGNED_DATA_RAW: &[u8] = &asn1_rs::oid!(raw 1.2.840.113549.1.7.2); pub const OID_CMS_ATTR_CONTENT_TYPE: &str = "1.2.840.113549.1.9.3"; +pub const OID_CMS_ATTR_CONTENT_TYPE_RAW: &[u8] = &asn1_rs::oid!(raw 1.2.840.113549.1.9.3); pub const OID_CMS_ATTR_MESSAGE_DIGEST: &str = "1.2.840.113549.1.9.4"; +pub const OID_CMS_ATTR_MESSAGE_DIGEST_RAW: &[u8] = &asn1_rs::oid!(raw 1.2.840.113549.1.9.4); pub const OID_CMS_ATTR_SIGNING_TIME: &str = "1.2.840.113549.1.9.5"; +pub const OID_CMS_ATTR_SIGNING_TIME_RAW: &[u8] = &asn1_rs::oid!(raw 1.2.840.113549.1.9.5); pub const OID_RSA_ENCRYPTION: &str = "1.2.840.113549.1.1.1"; +pub const OID_RSA_ENCRYPTION_RAW: &[u8] = &asn1_rs::oid!(raw 1.2.840.113549.1.1.1); pub const OID_SHA256_WITH_RSA_ENCRYPTION: &str = "1.2.840.113549.1.1.11"; +pub const OID_SHA256_WITH_RSA_ENCRYPTION_RAW: &[u8] = &asn1_rs::oid!(raw 1.2.840.113549.1.1.11); // X.509 extensions (RFC 5280 / RFC 6487) pub const OID_BASIC_CONSTRAINTS: &str = "2.5.29.19"; +pub const OID_BASIC_CONSTRAINTS_RAW: &[u8] = &asn1_rs::oid!(raw 2.5.29.19); pub const OID_KEY_USAGE: &str = "2.5.29.15"; +pub const OID_KEY_USAGE_RAW: &[u8] = &asn1_rs::oid!(raw 2.5.29.15); pub const OID_EXTENDED_KEY_USAGE: &str = "2.5.29.37"; +pub const OID_EXTENDED_KEY_USAGE_RAW: &[u8] = &asn1_rs::oid!(raw 2.5.29.37); pub const OID_CRL_DISTRIBUTION_POINTS: &str = "2.5.29.31"; +pub const OID_CRL_DISTRIBUTION_POINTS_RAW: &[u8] = &asn1_rs::oid!(raw 2.5.29.31); pub const OID_AUTHORITY_INFO_ACCESS: &str = "1.3.6.1.5.5.7.1.1"; +pub const OID_AUTHORITY_INFO_ACCESS_RAW: &[u8] = &asn1_rs::oid!(raw 1.3.6.1.5.5.7.1.1); pub const OID_CERTIFICATE_POLICIES: &str = "2.5.29.32"; +pub const OID_CERTIFICATE_POLICIES_RAW: &[u8] = &asn1_rs::oid!(raw 2.5.29.32); pub const OID_AUTHORITY_KEY_IDENTIFIER: &str = "2.5.29.35"; +pub const OID_AUTHORITY_KEY_IDENTIFIER_RAW: &[u8] = &asn1_rs::oid!(raw 2.5.29.35); pub const OID_CRL_NUMBER: &str = "2.5.29.20"; +pub const OID_CRL_NUMBER_RAW: &[u8] = &asn1_rs::oid!(raw 2.5.29.20); pub const OID_SUBJECT_KEY_IDENTIFIER: &str = "2.5.29.14"; +pub const OID_SUBJECT_KEY_IDENTIFIER_RAW: &[u8] = &asn1_rs::oid!(raw 2.5.29.14); pub const OID_CT_RPKI_MANIFEST: &str = "1.2.840.113549.1.9.16.1.26"; +pub const OID_CT_RPKI_MANIFEST_RAW: &[u8] = + &asn1_rs::oid!(raw 1.2.840.113549.1.9.16.1.26); pub const OID_CT_ROUTE_ORIGIN_AUTHZ: &str = "1.2.840.113549.1.9.16.1.24"; +pub const OID_CT_ROUTE_ORIGIN_AUTHZ_RAW: &[u8] = + &asn1_rs::oid!(raw 1.2.840.113549.1.9.16.1.24); pub const OID_CT_ASPA: &str = "1.2.840.113549.1.9.16.1.49"; +pub const OID_CT_ASPA_RAW: &[u8] = &asn1_rs::oid!(raw 1.2.840.113549.1.9.16.1.49); // X.509 extensions / access methods (RFC 5280 / RFC 6487) pub const OID_SUBJECT_INFO_ACCESS: &str = "1.3.6.1.5.5.7.1.11"; +pub const OID_SUBJECT_INFO_ACCESS_RAW: &[u8] = &asn1_rs::oid!(raw 1.3.6.1.5.5.7.1.11); pub const OID_AD_SIGNED_OBJECT: &str = "1.3.6.1.5.5.7.48.11"; +pub const OID_AD_SIGNED_OBJECT_RAW: &[u8] = &asn1_rs::oid!(raw 1.3.6.1.5.5.7.48.11); pub const OID_AD_CA_ISSUERS: &str = "1.3.6.1.5.5.7.48.2"; +pub const OID_AD_CA_ISSUERS_RAW: &[u8] = &asn1_rs::oid!(raw 1.3.6.1.5.5.7.48.2); pub const OID_AD_CA_REPOSITORY: &str = "1.3.6.1.5.5.7.48.5"; +pub const OID_AD_CA_REPOSITORY_RAW: &[u8] = &asn1_rs::oid!(raw 1.3.6.1.5.5.7.48.5); pub const OID_AD_RPKI_MANIFEST: &str = "1.3.6.1.5.5.7.48.10"; +pub const OID_AD_RPKI_MANIFEST_RAW: &[u8] = &asn1_rs::oid!(raw 1.3.6.1.5.5.7.48.10); pub const OID_AD_RPKI_NOTIFY: &str = "1.3.6.1.5.5.7.48.13"; +pub const OID_AD_RPKI_NOTIFY_RAW: &[u8] = &asn1_rs::oid!(raw 1.3.6.1.5.5.7.48.13); // RFC 3779 resource extensions (RFC 6487 profile) pub const OID_IP_ADDR_BLOCKS: &str = "1.3.6.1.5.5.7.1.7"; +pub const OID_IP_ADDR_BLOCKS_RAW: &[u8] = &asn1_rs::oid!(raw 1.3.6.1.5.5.7.1.7); pub const OID_AUTONOMOUS_SYS_IDS: &str = "1.3.6.1.5.5.7.1.8"; +pub const OID_AUTONOMOUS_SYS_IDS_RAW: &[u8] = &asn1_rs::oid!(raw 1.3.6.1.5.5.7.1.8); // RPKI CP (RFC 6484 / RFC 6487) pub const OID_CP_IPADDR_ASNUMBER: &str = "1.3.6.1.5.5.7.14.2"; +pub const OID_CP_IPADDR_ASNUMBER_RAW: &[u8] = &asn1_rs::oid!(raw 1.3.6.1.5.5.7.14.2); diff --git a/src/data_model/rc.rs b/src/data_model/rc.rs index 5212f25..8192e2d 100644 --- a/src/data_model/rc.rs +++ b/src/data_model/rc.rs @@ -1,19 +1,21 @@ use der_parser::ber::{BerObjectContent, Class}; use der_parser::der::{DerObject, Tag, parse_der}; use der_parser::num_bigint::BigUint; -use url::Url; use x509_parser::asn1_rs::{Class as Asn1Class, Tag as Asn1Tag}; use x509_parser::extensions::ParsedExtension; use x509_parser::prelude::{FromDer, X509Certificate, X509Extension, X509Version}; use crate::data_model::common::{ - Asn1TimeUtc, InvalidTimeEncodingError, UtcTime, asn1_time_to_model, + Asn1TimeUtc, InvalidTimeEncodingError, UtcTime, X509NameDer, asn1_time_to_model, }; use crate::data_model::oid::{ - OID_AD_CA_ISSUERS, OID_AD_SIGNED_OBJECT, OID_AUTHORITY_INFO_ACCESS, - OID_AUTHORITY_KEY_IDENTIFIER, OID_AUTONOMOUS_SYS_IDS, OID_CP_IPADDR_ASNUMBER, - OID_CRL_DISTRIBUTION_POINTS, OID_IP_ADDR_BLOCKS, OID_SHA256_WITH_RSA_ENCRYPTION, - OID_SUBJECT_INFO_ACCESS, OID_SUBJECT_KEY_IDENTIFIER, + OID_AD_CA_ISSUERS_RAW, OID_AD_CA_REPOSITORY, OID_AD_CA_REPOSITORY_RAW, OID_AD_RPKI_MANIFEST, + OID_AD_RPKI_MANIFEST_RAW, OID_AD_RPKI_NOTIFY, OID_AD_RPKI_NOTIFY_RAW, OID_AD_SIGNED_OBJECT, + OID_AD_SIGNED_OBJECT_RAW, OID_AUTHORITY_INFO_ACCESS_RAW, OID_AUTHORITY_KEY_IDENTIFIER_RAW, + OID_AUTONOMOUS_SYS_IDS_RAW, OID_BASIC_CONSTRAINTS_RAW, OID_CERTIFICATE_POLICIES_RAW, + OID_CP_IPADDR_ASNUMBER, OID_CP_IPADDR_ASNUMBER_RAW, OID_CRL_DISTRIBUTION_POINTS_RAW, + OID_IP_ADDR_BLOCKS_RAW, OID_SHA256_WITH_RSA_ENCRYPTION, OID_SHA256_WITH_RSA_ENCRYPTION_RAW, + OID_SUBJECT_INFO_ACCESS_RAW, OID_SUBJECT_KEY_IDENTIFIER_RAW, }; /// Resource Certificate kind (semantic classification). @@ -43,8 +45,8 @@ pub struct RpkixTbsCertificate { pub version: u32, pub serial_number: BigUint, pub signature_algorithm: String, - pub issuer_dn: String, - pub subject_dn: String, + pub issuer_name: X509NameDer, + pub subject_name: X509NameDer, pub validity_not_before: UtcTime, pub validity_not_after: UtcTime, /// DER encoding of SubjectPublicKeyInfo. @@ -59,9 +61,9 @@ pub struct RcExtensions { /// Authority Key Identifier (AKI) keyIdentifier value. pub authority_key_identifier: Option>, /// CRL Distribution Points URIs (fullName). - pub crl_distribution_points_uris: Option>, + pub crl_distribution_points_uris: Option>, /// Authority Information Access (AIA) caIssuers URIs. - pub ca_issuers_uris: Option>, + pub ca_issuers_uris: Option>, pub subject_info_access: Option, pub certificate_policies_oid: Option, @@ -76,8 +78,8 @@ pub struct ResourceCertificateParsed { pub serial_number: BigUint, pub signature_algorithm: AlgorithmIdentifierValue, pub tbs_signature_algorithm: AlgorithmIdentifierValue, - pub issuer_dn: String, - pub subject_dn: String, + pub issuer_name: X509NameDer, + pub subject_name: X509NameDer, pub validity_not_before: Asn1TimeUtc, pub validity_not_after: Asn1TimeUtc, /// DER encoding of SubjectPublicKeyInfo. @@ -130,7 +132,7 @@ pub struct AuthorityKeyIdentifierParsed { #[derive(Clone, Debug, PartialEq, Eq)] pub struct AuthorityInfoAccessParsed { - pub ca_issuers_uris: Vec, + pub ca_issuers_uris: Vec, pub ca_issuers_access_location_not_uri: bool, } @@ -145,7 +147,7 @@ pub struct CrlDistributionPointParsed { pub reasons_present: bool, pub crl_issuer_present: bool, pub name_relative_to_crl_issuer_present: bool, - pub full_name_uris: Vec, + pub full_name_uris: Vec, pub full_name_not_uri: bool, pub full_name_present: bool, } @@ -153,7 +155,7 @@ pub struct CrlDistributionPointParsed { #[derive(Clone, Debug, PartialEq, Eq)] pub struct SubjectInfoAccessParsed { pub access_descriptions: Vec, - pub signed_object_uris: Vec, + pub signed_object_uris: Vec, pub signed_object_access_location_not_uri: bool, } @@ -170,7 +172,7 @@ pub struct SubjectInfoAccessCa { #[derive(Clone, Debug, PartialEq, Eq)] pub struct SubjectInfoAccessEe { - pub signed_object_uris: Vec, + pub signed_object_uris: Vec, /// The full list of access descriptions as carried in the SIA extension. pub access_descriptions: Vec, } @@ -178,7 +180,7 @@ pub struct SubjectInfoAccessEe { #[derive(Clone, Debug, PartialEq, Eq)] pub struct AccessDescription { pub access_method_oid: String, - pub access_location: Url, + pub access_location: String, } #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)] @@ -541,8 +543,8 @@ impl ResourceCertificate { serial_number: cert.tbs_certificate.serial.clone(), signature_algorithm, tbs_signature_algorithm, - issuer_dn: cert.issuer().to_string(), - subject_dn: cert.subject().to_string(), + issuer_name: X509NameDer(cert.issuer().as_raw().to_vec()), + subject_name: X509NameDer(cert.subject().as_raw().to_vec()), validity_not_before, validity_not_after, subject_public_key_info, @@ -591,7 +593,7 @@ impl ResourceCertificateParsed { return Err(ResourceCertificateProfileError::InvalidSignatureAlgorithmParameters); } - let is_self_signed = self.issuer_dn == self.subject_dn; + let is_self_signed = self.issuer_name == self.subject_name; let extensions = self.extensions.validate_profile(is_self_signed)?; let kind = if extensions.basic_constraints_ca { ResourceCertKind::Ca @@ -605,8 +607,8 @@ impl ResourceCertificateParsed { version, serial_number: self.serial_number, signature_algorithm: self.signature_algorithm.oid, - issuer_dn: self.issuer_dn, - subject_dn: self.subject_dn, + issuer_name: self.issuer_name, + subject_name: self.subject_name, validity_not_before: self.validity_not_before.utc, validity_not_after: self.validity_not_after.utc, subject_public_key_info: self.subject_public_key_info, @@ -622,20 +624,38 @@ impl RcExtensionsParsed { self, is_self_signed: bool, ) -> Result { - if self.basic_constraints_ca.len() > 1 { + // NOTE(perf): `self` is consumed. Prefer moving decoded fields out rather than cloning, + // especially for large resource sets and URI lists. + let RcExtensionsParsed { + basic_constraints_ca, + subject_key_identifier, + authority_key_identifier, + crl_distribution_points, + authority_info_access, + subject_info_access, + certificate_policies, + ip_resources, + as_resources, + } = self; + + if basic_constraints_ca.len() > 1 { return Err(ResourceCertificateProfileError::DuplicateExtension( "basicConstraints", )); } - let basic_constraints_ca = self.basic_constraints_ca.first().copied().unwrap_or(false); + let basic_constraints_ca = basic_constraints_ca.first().copied().unwrap_or(false); - let subject_key_identifier = match self.subject_key_identifier.as_slice() { - [] => None, - [(ski, critical)] => { - if *critical { + let subject_key_identifier = match subject_key_identifier.len() { + 0 => None, + 1 => { + let (ski, critical) = subject_key_identifier + .into_iter() + .next() + .expect("len==1"); + if critical { return Err(ResourceCertificateProfileError::SkiCriticality); } - Some(ski.clone()) + Some(ski) } _ => { return Err(ResourceCertificateProfileError::DuplicateExtension( @@ -644,16 +664,20 @@ impl RcExtensionsParsed { } }; - let authority_key_identifier = match self.authority_key_identifier.as_slice() { - [] => { + let authority_key_identifier = match authority_key_identifier.len() { + 0 => { if is_self_signed { None } else { return Err(ResourceCertificateProfileError::AkiMissing); } } - [(aki, critical)] => { - if *critical { + 1 => { + let (aki, critical) = authority_key_identifier + .into_iter() + .next() + .expect("len==1"); + if critical { return Err(ResourceCertificateProfileError::AkiCriticality); } if aki.has_authority_cert_issuer { @@ -662,7 +686,7 @@ impl RcExtensionsParsed { if aki.has_authority_cert_serial { return Err(ResourceCertificateProfileError::AkiAuthorityCertSerialPresent); } - let keyid = aki.key_identifier.clone(); + let keyid = aki.key_identifier; if is_self_signed { if let (Some(keyid), Some(ski)) = (keyid.as_ref(), subject_key_identifier.as_ref()) @@ -683,16 +707,20 @@ impl RcExtensionsParsed { } }; - let crl_distribution_points_uris = match self.crl_distribution_points.as_slice() { - [] => { + let crl_distribution_points_uris = match crl_distribution_points.len() { + 0 => { if is_self_signed { None } else { return Err(ResourceCertificateProfileError::CrlDistributionPointsMissing); } } - [(crldp, critical)] => { - if *critical { + 1 => { + let (crldp, critical) = crl_distribution_points + .into_iter() + .next() + .expect("len==1"); + if critical { return Err(ResourceCertificateProfileError::CrlDistributionPointsCriticality); } if is_self_signed { @@ -703,7 +731,11 @@ impl RcExtensionsParsed { if crldp.distribution_points.len() != 1 { return Err(ResourceCertificateProfileError::CrlDistributionPointsNotSingle); } - let dp = &crldp.distribution_points[0]; + let dp = crldp + .distribution_points + .into_iter() + .next() + .expect("len==1"); if dp.reasons_present { return Err(ResourceCertificateProfileError::CrlDistributionPointsHasReasons); } @@ -723,10 +755,10 @@ impl RcExtensionsParsed { ResourceCertificateProfileError::CrlDistributionPointsFullNameNotUri, ); } - if !dp.full_name_uris.iter().any(|u| u.scheme() == "rsync") { + if !dp.full_name_uris.iter().any(|u| u.starts_with("rsync://")) { return Err(ResourceCertificateProfileError::CrlDistributionPointsNoRsync); } - Some(dp.full_name_uris.clone()) + Some(dp.full_name_uris) } _ => { return Err(ResourceCertificateProfileError::DuplicateExtension( @@ -735,16 +767,17 @@ impl RcExtensionsParsed { } }; - let ca_issuers_uris = match self.authority_info_access.as_slice() { - [] => { + let ca_issuers_uris = match authority_info_access.len() { + 0 => { if is_self_signed { None } else { return Err(ResourceCertificateProfileError::AuthorityInfoAccessMissing); } } - [(aia, critical)] => { - if *critical { + 1 => { + let (aia, critical) = authority_info_access.into_iter().next().expect("len==1"); + if critical { return Err(ResourceCertificateProfileError::AuthorityInfoAccessCriticality); } if is_self_signed { @@ -762,10 +795,10 @@ impl RcExtensionsParsed { ResourceCertificateProfileError::AuthorityInfoAccessMissingCaIssuers, ); } - if !aia.ca_issuers_uris.iter().any(|u| u.scheme() == "rsync") { + if !aia.ca_issuers_uris.iter().any(|u| u.starts_with("rsync://")) { return Err(ResourceCertificateProfileError::AuthorityInfoAccessNoRsync); } - Some(aia.ca_issuers_uris.clone()) + Some(aia.ca_issuers_uris) } _ => { return Err(ResourceCertificateProfileError::DuplicateExtension( @@ -774,28 +807,32 @@ impl RcExtensionsParsed { } }; - let subject_info_access = match self.subject_info_access.as_slice() { - [] => None, - [(sia, critical)] => { - if *critical { + let subject_info_access = match subject_info_access.len() { + 0 => None, + 1 => { + let (sia, critical) = subject_info_access.into_iter().next().expect("len==1"); + if critical { return Err(ResourceCertificateProfileError::SiaCriticality); } if sia.signed_object_access_location_not_uri { return Err(ResourceCertificateProfileError::SignedObjectSiaNotUri); } if !sia.signed_object_uris.is_empty() - && !sia.signed_object_uris.iter().any(|u| u.scheme() == "rsync") + && !sia + .signed_object_uris + .iter() + .any(|u| u.starts_with("rsync://")) { return Err(ResourceCertificateProfileError::SignedObjectSiaNoRsync); } if sia.signed_object_uris.is_empty() { Some(SubjectInfoAccess::Ca(SubjectInfoAccessCa { - access_descriptions: sia.access_descriptions.clone(), + access_descriptions: sia.access_descriptions, })) } else { Some(SubjectInfoAccess::Ee(SubjectInfoAccessEe { - signed_object_uris: sia.signed_object_uris.clone(), - access_descriptions: sia.access_descriptions.clone(), + signed_object_uris: sia.signed_object_uris, + access_descriptions: sia.access_descriptions, })) } } @@ -806,10 +843,11 @@ impl RcExtensionsParsed { } }; - let certificate_policies_oid = match self.certificate_policies.as_slice() { - [] => None, - [(oids, critical)] => { - if !*critical { + let certificate_policies_oid = match certificate_policies.len() { + 0 => None, + 1 => { + let (oids, critical) = certificate_policies.into_iter().next().expect("len==1"); + if !critical { return Err(ResourceCertificateProfileError::CertificatePoliciesCriticality); } if oids.len() != 1 { @@ -817,7 +855,7 @@ impl RcExtensionsParsed { "expected exactly one policy".into(), )); } - let policy_oid = oids[0].clone(); + let policy_oid = oids.into_iter().next().expect("len==1"); if policy_oid != OID_CP_IPADDR_ASNUMBER { return Err(ResourceCertificateProfileError::InvalidCertificatePolicy( policy_oid, @@ -832,13 +870,14 @@ impl RcExtensionsParsed { } }; - let ip_resources = match self.ip_resources.as_slice() { - [] => None, - [(ip, critical)] => { - if !*critical { + let ip_resources = match ip_resources.len() { + 0 => None, + 1 => { + let (ip, critical) = ip_resources.into_iter().next().expect("len==1"); + if !critical { return Err(ResourceCertificateProfileError::IpResourcesCriticality); } - Some(ip.clone()) + Some(ip) } _ => { return Err(ResourceCertificateProfileError::DuplicateExtension( @@ -847,13 +886,14 @@ impl RcExtensionsParsed { } }; - let as_resources = match self.as_resources.as_slice() { - [] => None, - [(asn, critical)] => { - if !*critical { + let as_resources = match as_resources.len() { + 0 => None, + 1 => { + let (asn, critical) = as_resources.into_iter().next().expect("len==1"); + if !critical { return Err(ResourceCertificateProfileError::AsResourcesCriticality); } - Some(asn.clone()) + Some(asn) } _ => { return Err(ResourceCertificateProfileError::DuplicateExtension( @@ -884,8 +924,16 @@ fn algorithm_identifier_value( tag: p.tag(), data: p.as_bytes().to_vec(), }); + // NOTE(perf): Avoid `to_id_string()` allocations for the algorithms we expect + // in RPKI resource certificates. Fall back to `to_id_string()` for unexpected + // algorithms (mostly error paths). + let oid = if ai.algorithm.as_bytes() == OID_SHA256_WITH_RSA_ENCRYPTION_RAW { + OID_SHA256_WITH_RSA_ENCRYPTION.to_string() + } else { + ai.algorithm.to_id_string() + }; AlgorithmIdentifierValue { - oid: ai.algorithm.to_id_string(), + oid, parameters, } } @@ -905,25 +953,22 @@ fn parse_extensions_parse( let mut as_resources: Vec<(AsResourceSet, bool)> = Vec::new(); for ext in exts { - let oid = ext.oid.to_id_string(); - match oid.as_str() { - crate::data_model::oid::OID_BASIC_CONSTRAINTS => { + let oid = ext.oid.as_bytes(); + if oid == OID_BASIC_CONSTRAINTS_RAW { let ParsedExtension::BasicConstraints(bc) = ext.parsed_extension() else { return Err(ResourceCertificateParseError::Parse( "basicConstraints parse failed".into(), )); }; basic_constraints_ca.push(bc.ca); - } - OID_SUBJECT_KEY_IDENTIFIER => { + } else if oid == OID_SUBJECT_KEY_IDENTIFIER_RAW { let ParsedExtension::SubjectKeyIdentifier(s) = ext.parsed_extension() else { return Err(ResourceCertificateParseError::Parse( "subjectKeyIdentifier parse failed".into(), )); }; ski.push((s.0.to_vec(), ext.critical)); - } - OID_AUTHORITY_KEY_IDENTIFIER => { + } else if oid == OID_AUTHORITY_KEY_IDENTIFIER_RAW { let ParsedExtension::AuthorityKeyIdentifier(a) = ext.parsed_extension() else { return Err(ResourceCertificateParseError::Parse( "authorityKeyIdentifier parse failed".into(), @@ -937,51 +982,51 @@ fn parse_extensions_parse( }, ext.critical, )); - } - OID_CRL_DISTRIBUTION_POINTS => { + } else if oid == OID_CRL_DISTRIBUTION_POINTS_RAW { let ParsedExtension::CRLDistributionPoints(p) = ext.parsed_extension() else { return Err(ResourceCertificateParseError::Parse( "cRLDistributionPoints parse failed".into(), )); }; crldp.push((parse_crldp_parse(p)?, ext.critical)); - } - OID_AUTHORITY_INFO_ACCESS => { + } else if oid == OID_AUTHORITY_INFO_ACCESS_RAW { let ParsedExtension::AuthorityInfoAccess(p) = ext.parsed_extension() else { return Err(ResourceCertificateParseError::Parse( "authorityInfoAccess parse failed".into(), )); }; aia.push((parse_aia_parse(p.accessdescs.as_slice())?, ext.critical)); - } - OID_SUBJECT_INFO_ACCESS => { + } else if oid == OID_SUBJECT_INFO_ACCESS_RAW { let ParsedExtension::SubjectInfoAccess(s) = ext.parsed_extension() else { return Err(ResourceCertificateParseError::Parse( "subjectInfoAccess parse failed".into(), )); }; sia.push((parse_sia_parse(s.accessdescs.as_slice())?, ext.critical)); - } - crate::data_model::oid::OID_CERTIFICATE_POLICIES => { + } else if oid == OID_CERTIFICATE_POLICIES_RAW { let ParsedExtension::CertificatePolicies(cp) = ext.parsed_extension() else { return Err(ResourceCertificateParseError::Parse( "certificatePolicies parse failed".into(), )); }; - let oids: Vec = cp.iter().map(|p| p.policy_id.to_id_string()).collect(); + let mut oids: Vec = Vec::with_capacity(cp.len()); + for p in cp.iter() { + let b = p.policy_id.as_bytes(); + if b == OID_CP_IPADDR_ASNUMBER_RAW { + oids.push(OID_CP_IPADDR_ASNUMBER.to_string()); + } else { + oids.push(p.policy_id.to_id_string()); + } + } cert_policies.push((oids, ext.critical)); - } - OID_IP_ADDR_BLOCKS => { + } else if oid == OID_IP_ADDR_BLOCKS_RAW { let parsed = IpResourceSet::decode_extn_value(ext.value) .map_err(|_e| ResourceCertificateParseError::InvalidIpResourcesEncoding)?; ip_resources.push((parsed, ext.critical)); - } - OID_AUTONOMOUS_SYS_IDS => { + } else if oid == OID_AUTONOMOUS_SYS_IDS_RAW { let parsed = AsResourceSet::decode_extn_value(ext.value) .map_err(|_e| ResourceCertificateParseError::InvalidAsResourcesEncoding)?; as_resources.push((parsed, ext.critical)); - } - _ => {} } } @@ -1001,12 +1046,11 @@ fn parse_extensions_parse( fn parse_aia_parse( access: &[x509_parser::extensions::AccessDescription<'_>], ) -> Result { - let mut ca_issuers_uris: Vec = Vec::new(); + let mut ca_issuers_uris: Vec = Vec::new(); let mut ca_issuers_access_location_not_uri = false; for ad in access { - let access_method_oid = ad.access_method.to_id_string(); - if access_method_oid != OID_AD_CA_ISSUERS { + if ad.access_method.as_bytes() != OID_AD_CA_ISSUERS_RAW { continue; } let uri = match &ad.access_location { @@ -1016,9 +1060,7 @@ fn parse_aia_parse( continue; } }; - let url = Url::parse(uri) - .map_err(|_| ResourceCertificateParseError::Parse(format!("invalid URI: {uri}")))?; - ca_issuers_uris.push(url); + ca_issuers_uris.push(uri.to_string()); } Ok(AuthorityInfoAccessParsed { @@ -1032,7 +1074,7 @@ fn parse_crldp_parse( ) -> Result { let mut out: Vec = Vec::new(); for p in crldp.iter() { - let mut full_name_uris: Vec = Vec::new(); + let mut full_name_uris: Vec = Vec::new(); let mut full_name_not_uri = false; let mut full_name_present = false; let mut name_relative_to_crl_issuer_present = false; @@ -1046,12 +1088,7 @@ fn parse_crldp_parse( for n in names { match n { x509_parser::extensions::GeneralName::URI(u) => { - let url = Url::parse(u).map_err(|_| { - ResourceCertificateParseError::Parse(format!( - "invalid URI: {u}" - )) - })?; - full_name_uris.push(url); + full_name_uris.push(u.to_string()); } _ => { full_name_not_uri = true; @@ -1084,28 +1121,37 @@ fn parse_sia_parse( access: &[x509_parser::extensions::AccessDescription<'_>], ) -> Result { let mut all = Vec::with_capacity(access.len()); - let mut signed_object_uris: Vec = Vec::new(); + let mut signed_object_uris: Vec = Vec::new(); let mut signed_object_access_location_not_uri = false; for ad in access { - let access_method_oid = ad.access_method.to_id_string(); + let access_method_oid = if ad.access_method.as_bytes() == OID_AD_CA_REPOSITORY_RAW { + OID_AD_CA_REPOSITORY.to_string() + } else if ad.access_method.as_bytes() == OID_AD_RPKI_MANIFEST_RAW { + OID_AD_RPKI_MANIFEST.to_string() + } else if ad.access_method.as_bytes() == OID_AD_RPKI_NOTIFY_RAW { + OID_AD_RPKI_NOTIFY.to_string() + } else if ad.access_method.as_bytes() == OID_AD_SIGNED_OBJECT_RAW { + OID_AD_SIGNED_OBJECT.to_string() + } else { + ad.access_method.to_id_string() + }; + let is_signed_object = access_method_oid == OID_AD_SIGNED_OBJECT; let uri = match &ad.access_location { x509_parser::extensions::GeneralName::URI(u) => u, _ => { - if access_method_oid == OID_AD_SIGNED_OBJECT { + if is_signed_object { signed_object_access_location_not_uri = true; } continue; } }; - let url = Url::parse(uri) - .map_err(|_| ResourceCertificateParseError::Parse(format!("invalid URI: {uri}")))?; - if access_method_oid == OID_AD_SIGNED_OBJECT { - signed_object_uris.push(url.clone()); + if is_signed_object { + signed_object_uris.push(uri.to_string()); } all.push(AccessDescription { access_method_oid, - access_location: url, + access_location: uri.to_string(), }); } diff --git a/src/data_model/roa.rs b/src/data_model/roa.rs index c4b5dd2..06db55d 100644 --- a/src/data_model/roa.rs +++ b/src/data_model/roa.rs @@ -1,10 +1,9 @@ use crate::data_model::oid::OID_CT_ROUTE_ORIGIN_AUTHZ; +use crate::data_model::common::{DerReader, der_take_tlv}; use crate::data_model::rc::{Afi as RcAfi, IpPrefix as RcIpPrefix, ResourceCertificate}; use crate::data_model::signed_object::{ RpkiSignedObject, RpkiSignedObjectParsed, SignedObjectParseError, SignedObjectValidateError, }; -use der_parser::ber::{BerObjectContent, Class}; -use der_parser::der::{DerObject, Tag, parse_der}; #[derive(Clone, Debug, PartialEq, Eq)] pub struct RoaObject { @@ -229,14 +228,25 @@ pub struct IpPrefix { pub afi: RoaAfi, /// Prefix length in bits. pub prefix_len: u16, - /// Network order address bytes (IPv4 4 bytes / IPv6 16 bytes), with host bits cleared. - pub addr: Vec, + /// Network order address bytes (always 16 bytes), with host bits cleared. + /// + /// For IPv4 prefixes, only the first 4 bytes are used and the remaining 12 bytes are zero. + pub addr: [u8; 16], +} + +impl IpPrefix { + pub fn addr_bytes(&self) -> &[u8] { + match self.afi { + RoaAfi::Ipv4 => &self.addr[..4], + RoaAfi::Ipv6 => &self.addr[..16], + } + } } impl RoaEContent { /// Parse step of scheme A (`parse → validate → verify`). pub fn parse_der(der: &[u8]) -> Result { - let (rem, _obj) = parse_der(der).map_err(|e| RoaParseError::Parse(e.to_string()))?; + let (_tag, _value, rem) = der_take_tlv(der).map_err(RoaParseError::Parse)?; if !rem.is_empty() { return Err(RoaParseError::TrailingBytes(rem.len())); } @@ -293,7 +303,7 @@ impl RoaEContent { if !ip.contains_prefix(&rc_prefix) { return Err(RoaValidateError::PrefixNotInEeResources { afi: entry.prefix.afi, - addr: entry.prefix.addr.clone(), + addr: entry.prefix.addr_bytes().to_vec(), prefix_len: entry.prefix.prefix_len, }); } @@ -328,55 +338,75 @@ impl RoaObjectParsed { impl RoaEContentParsed { pub fn validate_profile(self) -> Result { - let (_rem, obj) = - parse_der(&self.der).map_err(|e| RoaProfileError::ProfileDecode(e.to_string()))?; - - let seq = obj - .as_sequence() - .map_err(|e| RoaProfileError::ProfileDecode(e.to_string()))?; - if seq.len() != 2 && seq.len() != 3 { - return Err(RoaProfileError::InvalidAttestationSequenceLen(seq.len())); + fn count_elements(mut r: DerReader<'_>) -> Result { + let mut n = 0usize; + while !r.is_empty() { + r.skip_any()?; + n += 1; + } + Ok(n) + } + + let mut r = DerReader::new(&self.der); + let mut seq = r + .take_sequence() + .map_err(|e| RoaProfileError::ProfileDecode(e))?; + if !r.is_empty() { + return Err(RoaProfileError::ProfileDecode( + "trailing bytes after RouteOriginAttestation".into(), + )); + } + + let elem_count = + count_elements(seq).map_err(|e| RoaProfileError::ProfileDecode(e.to_string()))?; + if elem_count != 2 && elem_count != 3 { + return Err(RoaProfileError::InvalidAttestationSequenceLen(elem_count)); } - let mut idx = 0; let mut version: u32 = 0; - if seq.len() == 3 { - let v_obj = &seq[0]; - if v_obj.class() != Class::ContextSpecific || v_obj.tag() != Tag(0) { + if elem_count == 3 { + if seq + .peek_tag() + .map_err(|e| RoaProfileError::ProfileDecode(e.to_string()))? + != 0xA0 + { return Err(RoaProfileError::ProfileDecode( "RouteOriginAttestation.version must be [0] EXPLICIT INTEGER".into(), )); } - let inner_der = v_obj - .as_slice() + let (inner_tag, inner_val) = seq + .take_explicit(0xA0) .map_err(|e| RoaProfileError::ProfileDecode(e.to_string()))?; - let (rem, inner) = - parse_der(inner_der).map_err(|e| RoaProfileError::ProfileDecode(e.to_string()))?; - if !rem.is_empty() { + if inner_tag != 0x02 { return Err(RoaProfileError::ProfileDecode( - "trailing bytes inside RouteOriginAttestation.version".into(), + "RouteOriginAttestation.version must be [0] EXPLICIT INTEGER".into(), )); } - let v = inner - .as_u64() + let v = crate::data_model::common::der_uint_from_bytes(inner_val) .map_err(|e| RoaProfileError::ProfileDecode(e.to_string()))?; if v != 0 { return Err(RoaProfileError::InvalidVersion(v)); } version = 0; - idx = 1; } - let as_id_u64 = seq[idx] - .as_u64() + let as_id_u64 = seq + .take_uint_u64() .map_err(|e| RoaProfileError::ProfileDecode(e.to_string()))?; if as_id_u64 > u32::MAX as u64 { return Err(RoaProfileError::AsIdOutOfRange(as_id_u64)); } let as_id = as_id_u64 as u32; - idx += 1; + let ip_addr_blocks = parse_ip_addr_blocks_cursor(seq.take_sequence().map_err(|e| { + RoaProfileError::ProfileDecode(format!("ipAddrBlocks: {e}")) + })?)?; - let ip_addr_blocks = parse_ip_addr_blocks(&seq[idx])?; + if !seq.is_empty() { + // Extra elements beyond the expected 2..3. + let extra = + count_elements(seq).map_err(|e| RoaProfileError::ProfileDecode(e.to_string()))?; + return Err(RoaProfileError::InvalidAttestationSequenceLen(elem_count + extra)); + } let mut out = RoaEContent { version, @@ -396,21 +426,34 @@ fn roa_prefix_to_rc(p: &IpPrefix) -> RcIpPrefix { RcIpPrefix { afi, prefix_len: p.prefix_len, - addr: p.addr.clone(), + addr: p.addr_bytes().to_vec(), } } -fn parse_ip_addr_blocks(obj: &DerObject<'_>) -> Result, RoaProfileError> { - let seq = obj - .as_sequence() - .map_err(|e| RoaProfileError::ProfileDecode(e.to_string()))?; - if seq.is_empty() || seq.len() > 2 { - return Err(RoaProfileError::InvalidIpAddrBlocksLen(seq.len())); +fn parse_ip_addr_blocks_cursor( + mut seq: DerReader<'_>, +) -> Result, RoaProfileError> { + fn count_elements(mut r: DerReader<'_>) -> Result { + let mut n = 0usize; + while !r.is_empty() { + r.skip_any()?; + n += 1; + } + Ok(n) } - let mut out: Vec = Vec::new(); - for fam in seq { - let family = parse_ip_address_family(fam)?; + let fam_count = + count_elements(seq).map_err(|e| RoaProfileError::ProfileDecode(e.to_string()))?; + if fam_count == 0 || fam_count > 2 { + return Err(RoaProfileError::InvalidIpAddrBlocksLen(fam_count)); + } + + let mut out: Vec = Vec::with_capacity(fam_count); + while !seq.is_empty() { + let family = parse_ip_address_family_cursor( + seq.take_sequence() + .map_err(|e| RoaProfileError::ProfileDecode(e.to_string()))?, + )?; if out.iter().any(|f| f.afi == family.afi) { return Err(RoaProfileError::DuplicateAfi(family.afi)); } @@ -419,77 +462,75 @@ fn parse_ip_addr_blocks(obj: &DerObject<'_>) -> Result, Ok(out) } -fn parse_ip_address_family(obj: &DerObject<'_>) -> Result { - let seq = obj - .as_sequence() - .map_err(|e| RoaProfileError::ProfileDecode(e.to_string()))?; - if seq.len() != 2 { +fn parse_ip_address_family_cursor( + mut fam: DerReader<'_>, +) -> Result { + let afi = { + let bytes = fam + .take_octet_string() + .map_err(|_e| RoaProfileError::InvalidAddressFamily)?; + if bytes.len() != 2 { + return Err(RoaProfileError::InvalidAddressFamily); + } + match bytes { + [0x00, 0x01] => RoaAfi::Ipv4, + [0x00, 0x02] => RoaAfi::Ipv6, + _ => return Err(RoaProfileError::UnsupportedAfi(bytes.to_vec())), + } + }; + + let mut addrs = fam + .take_sequence() + .map_err(|_e| RoaProfileError::InvalidIpAddressFamily)?; + if !fam.is_empty() { return Err(RoaProfileError::InvalidIpAddressFamily); } - let afi = parse_afi(&seq[0])?; - let addresses = parse_roa_addresses(afi, &seq[1])?; - if addresses.is_empty() { + if addrs.is_empty() { return Err(RoaProfileError::EmptyAddressList); } + let mut addresses: Vec = Vec::new(); + while !addrs.is_empty() { + let entry = addrs + .take_sequence() + .map_err(|e| RoaProfileError::ProfileDecode(e.to_string()))?; + addresses.push(parse_roa_ip_address_cursor(afi, entry)?); + } Ok(RoaIpAddressFamily { afi, addresses }) } -fn parse_afi(obj: &DerObject<'_>) -> Result { - let bytes = obj - .as_slice() - .map_err(|_e| RoaProfileError::InvalidAddressFamily)?; - if bytes.len() != 2 { - return Err(RoaProfileError::InvalidAddressFamily); - } - match bytes { - [0x00, 0x01] => Ok(RoaAfi::Ipv4), - [0x00, 0x02] => Ok(RoaAfi::Ipv6), - _ => Err(RoaProfileError::UnsupportedAfi(bytes.to_vec())), - } -} - -fn parse_roa_addresses( +fn parse_roa_ip_address_cursor( afi: RoaAfi, - obj: &DerObject<'_>, -) -> Result, RoaProfileError> { - let seq = obj - .as_sequence() - .map_err(|e| RoaProfileError::ProfileDecode(e.to_string()))?; - let mut out = Vec::with_capacity(seq.len()); - for entry in seq { - out.push(parse_roa_ip_address(afi, entry)?); - } - Ok(out) -} - -fn parse_roa_ip_address(afi: RoaAfi, obj: &DerObject<'_>) -> Result { - let seq = obj - .as_sequence() - .map_err(|e| RoaProfileError::ProfileDecode(e.to_string()))?; - if seq.is_empty() || seq.len() > 2 { + mut seq: DerReader<'_>, +) -> Result { + if seq.is_empty() { return Err(RoaProfileError::InvalidRoaIpAddress); } - let prefix = parse_prefix_bits(afi, &seq[0])?; - let max_length = match seq.get(1) { - None => None, - Some(m) => { - let v = m - .as_u64() - .map_err(|e| RoaProfileError::ProfileDecode(e.to_string()))?; - let max_len: u16 = v - .try_into() - .map_err(|_e| RoaProfileError::InvalidMaxLength { - afi, - prefix_len: prefix.prefix_len, - max_len: u16::MAX, - })?; - Some(max_len) - } + let (unused_bits, bytes) = seq + .take_bit_string() + .map_err(|_e| RoaProfileError::InvalidPrefixBitString)?; + let prefix = parse_prefix_bits_bytes(afi, unused_bits, bytes)?; + + let max_length = if !seq.is_empty() { + let v = seq + .take_uint_u64() + .map_err(|e| RoaProfileError::ProfileDecode(e.to_string()))?; + let max_len: u16 = v.try_into().map_err(|_e| RoaProfileError::InvalidMaxLength { + afi, + prefix_len: prefix.prefix_len, + max_len: u16::MAX, + })?; + Some(max_len) + } else { + None }; + if !seq.is_empty() { + return Err(RoaProfileError::InvalidRoaIpAddress); + } + if let Some(max_len) = max_length { let ub = afi.ub(); if max_len > ub || max_len < prefix.prefix_len { @@ -504,12 +545,11 @@ fn parse_roa_ip_address(afi: RoaAfi, obj: &DerObject<'_>) -> Result) -> Result { - let (unused_bits, bytes) = match &obj.content { - BerObjectContent::BitString(unused, bso) => (*unused, bso.data.to_vec()), - _ => return Err(RoaProfileError::InvalidPrefixBitString), - }; - +fn parse_prefix_bits_bytes( + afi: RoaAfi, + unused_bits: u8, + bytes: &[u8], +) -> Result { if unused_bits > 7 { return Err(RoaProfileError::InvalidPrefixUnusedBits); } @@ -531,7 +571,7 @@ fn parse_prefix_bits(afi: RoaAfi, obj: &DerObject<'_>) -> Result) -> Result Vec { +fn canonicalize_prefix_addr(afi: RoaAfi, prefix_len: u16, bytes: &[u8]) -> [u8; 16] { let full_len = match afi { RoaAfi::Ipv4 => 4, RoaAfi::Ipv6 => 16, }; - let mut addr = vec![0u8; full_len]; + let mut addr = [0u8; 16]; let copy_len = bytes.len().min(full_len); addr[..copy_len].copy_from_slice(&bytes[..copy_len]); @@ -557,7 +597,7 @@ fn canonicalize_prefix_addr(afi: RoaAfi, prefix_len: u16, bytes: &[u8]) -> Vec Result { - let (rem, obj) = - parse_der(der).map_err(|e| SignedObjectParseError::Parse(e.to_string()))?; - if !rem.is_empty() { - return Err(SignedObjectParseError::TrailingBytes(rem.len())); + let mut r = DerReader::new(der); + let mut content_info_seq = r + .take_sequence() + .map_err(|e| SignedObjectParseError::Parse(e.to_string()))?; + if !r.is_empty() { + return Err(SignedObjectParseError::TrailingBytes(r.remaining_len())); } - let content_info_seq = obj - .as_sequence() - .map_err(|e| SignedObjectParseError::Parse(e.to_string()))?; - if content_info_seq.len() != 2 { + let content_type = take_oid_string(&mut content_info_seq)?; + let signed_data = parse_signed_data_from_contentinfo_cursor(&mut content_info_seq)?; + if !content_info_seq.is_empty() { return Err(SignedObjectParseError::Parse( "ContentInfo must be a SEQUENCE of 2 elements".into(), )); } - let content_type = oid_to_string_parse(&content_info_seq[0])?; - let signed_data = parse_signed_data_from_contentinfo_parse(&content_info_seq[1])?; - Ok(RpkiSignedObjectParsed { raw_der: der.to_vec(), content_info_content_type: content_type, @@ -411,90 +411,93 @@ impl RpkiSignedObjectParsed { } } -fn parse_signed_data_from_contentinfo_parse( - obj: &DerObject<'_>, +fn parse_signed_data_from_contentinfo_cursor( + seq: &mut DerReader<'_>, ) -> Result { - // ContentInfo.content is `[0] EXPLICIT`, but `der-parser` will represent unknown tagged - // objects as `Unknown(Any)`. For EXPLICIT tags, the content octets are the full encoding of - // the inner object, so we parse it from the object's slice. - if obj.class() != Class::ContextSpecific || obj.tag() != Tag(0) { - return Err(SignedObjectParseError::Parse( - "ContentInfo.content must be [0] EXPLICIT".into(), - )); - } - let inner_der = obj - .as_slice() + let inner_der = seq.take_explicit_der(0xA0).map_err(|_e| { + SignedObjectParseError::Parse("ContentInfo.content must be [0] EXPLICIT".into()) + })?; + let mut r = DerReader::new(inner_der); + let signed_data_seq = r + .take_sequence() .map_err(|e| SignedObjectParseError::Parse(e.to_string()))?; - let (rem, inner_obj) = - parse_der(inner_der).map_err(|e| SignedObjectParseError::Parse(e.to_string()))?; - if !rem.is_empty() { + if !r.is_empty() { return Err(SignedObjectParseError::Parse( "trailing bytes inside ContentInfo.content".into(), )); } - parse_signed_data_parse(&inner_obj) + parse_signed_data_cursor(signed_data_seq) } -fn parse_signed_data_parse( - obj: &DerObject<'_>, -) -> Result { - let seq = obj - .as_sequence() - .map_err(|e| SignedObjectParseError::Parse(e.to_string()))?; - if seq.len() < 4 || seq.len() > 6 { - return Err(SignedObjectParseError::Parse( - "SignedData must be a SEQUENCE of 4..6 elements".into(), - )); - } - - let version = seq[0] - .as_u64() +fn parse_signed_data_cursor(mut seq: DerReader<'_>) -> Result { + let version = seq + .take_uint_u64() .map_err(|e| SignedObjectParseError::Parse(e.to_string()))?; - let digest_set = seq[1] - .as_set() + let digest_set_bytes = seq + .take_tag(0x31) .map_err(|e| SignedObjectParseError::Parse(e.to_string()))?; - let mut digest_algorithms: Vec = - Vec::with_capacity(digest_set.len()); - for item in digest_set { - let (oid, params_ok) = parse_algorithm_identifier_parse(item)?; + let mut digest_set = DerReader::new(digest_set_bytes); + let mut digest_algorithms: Vec = Vec::new(); + while !digest_set.is_empty() { + let alg = digest_set + .take_sequence() + .map_err(|e| SignedObjectParseError::Parse(e.to_string()))?; + let (oid, params_ok) = parse_algorithm_identifier_cursor(alg)?; digest_algorithms.push(AlgorithmIdentifierParsed { oid, params_ok }); } - let encap_content_info = parse_encapsulated_content_info_parse(&seq[2])?; + let encap_content_info = parse_encapsulated_content_info_cursor( + seq.take_sequence() + .map_err(|e| SignedObjectParseError::Parse(e.to_string()))?, + )?; let mut certificates: Option>> = None; let mut crls_present = false; - let mut signer_infos_obj: Option<&DerObject<'_>> = None; + let mut signer_infos: Option> = None; - for item in &seq[3..] { - if item.class() == Class::ContextSpecific && item.tag() == Tag(0) { - if certificates.is_some() { + while !seq.is_empty() { + let tag = seq + .peek_tag() + .map_err(|e| SignedObjectParseError::Parse(e.to_string()))?; + match tag { + 0xA0 => { + if certificates.is_some() { + return Err(SignedObjectParseError::Parse( + "SignedData.certificates appears more than once".into(), + )); + } + let content = seq + .take_tag(0xA0) + .map_err(|e| SignedObjectParseError::Parse(e.to_string()))?; + certificates = Some(split_der_objects(content)?); + } + 0xA1 => { + crls_present = true; + seq.skip_any() + .map_err(|e| SignedObjectParseError::Parse(e.to_string()))?; + } + 0x31 => { + if signer_infos.is_some() { + return Err(SignedObjectParseError::Parse( + "SignedData.signerInfos appears more than once".into(), + )); + } + let set_bytes = seq + .take_tag(0x31) + .map_err(|e| SignedObjectParseError::Parse(e.to_string()))?; + signer_infos = Some(parse_signer_infos_set_cursor(set_bytes)?); + } + _ => { return Err(SignedObjectParseError::Parse( - "SignedData.certificates appears more than once".into(), + "unexpected field in SignedData".into(), )); } - certificates = Some(parse_certificate_set_implicit_parse(item)?); - } else if item.class() == Class::ContextSpecific && item.tag() == Tag(1) { - crls_present = true; - } else if item.class() == Class::Universal && item.tag() == Tag::Set { - signer_infos_obj = Some(item); - } else { - return Err(SignedObjectParseError::Parse( - "unexpected field in SignedData".into(), - )); } } - let signer_infos_obj = signer_infos_obj - .ok_or_else(|| SignedObjectParseError::Parse("SignedData.signerInfos missing".into()))?; - let signer_infos_set = signer_infos_obj - .as_set() - .map_err(|e| SignedObjectParseError::Parse(e.to_string()))?; - let mut signer_infos: Vec = Vec::with_capacity(signer_infos_set.len()); - for si in signer_infos_set { - signer_infos.push(parse_signer_info_parse(si)?); - } + let signer_infos = + signer_infos.ok_or_else(|| SignedObjectParseError::Parse("SignedData.signerInfos missing".into()))?; Ok(SignedDataParsed { version, @@ -506,46 +509,39 @@ fn parse_signed_data_parse( }) } -fn parse_encapsulated_content_info_parse( - obj: &DerObject<'_>, +fn parse_encapsulated_content_info_cursor( + mut seq: DerReader<'_>, ) -> Result { - let seq = obj - .as_sequence() - .map_err(|e| SignedObjectParseError::Parse(e.to_string()))?; - if seq.is_empty() || seq.len() > 2 { + if seq.is_empty() { return Err(SignedObjectParseError::Parse( "EncapsulatedContentInfo must be SEQUENCE of 1..2".into(), )); } - let econtent_type = oid_to_string_parse(&seq[0])?; - let econtent = match seq.get(1) { - None => None, - Some(econtent_tagged) => { - if econtent_tagged.class() != Class::ContextSpecific || econtent_tagged.tag() != Tag(0) - { - return Err(SignedObjectParseError::Parse( - "EncapsulatedContentInfo.eContent must be [0] EXPLICIT".into(), - )); - } - let inner_der = econtent_tagged - .as_slice() - .map_err(|e| SignedObjectParseError::Parse(e.to_string()))?; - let (rem, inner_obj) = - parse_der(inner_der).map_err(|e| SignedObjectParseError::Parse(e.to_string()))?; - if !rem.is_empty() { - return Err(SignedObjectParseError::Parse( - "trailing bytes inside EncapsulatedContentInfo.eContent".into(), - )); - } - Some( - inner_obj - .as_slice() - .map_err(|e| SignedObjectParseError::Parse(e.to_string()))? - .to_vec(), - ) + let econtent_type = take_oid_string(&mut seq)?; + + let econtent = if seq.is_empty() { + None + } else { + let inner_der = seq.take_explicit_der(0xA0).map_err(|_e| { + SignedObjectParseError::Parse("EncapsulatedContentInfo.eContent must be [0] EXPLICIT".into()) + })?; + let mut inner = DerReader::new(inner_der); + let octets = inner + .take_octet_string() + .map_err(|e| SignedObjectParseError::Parse(e.to_string()))?; + if !inner.is_empty() { + return Err(SignedObjectParseError::Parse( + "trailing bytes inside EncapsulatedContentInfo.eContent".into(), + )); } + Some(octets.to_vec()) }; + if !seq.is_empty() { + return Err(SignedObjectParseError::Parse( + "EncapsulatedContentInfo must be SEQUENCE of 1..2".into(), + )); + } Ok(EncapsulatedContentInfoParsed { econtent_type, @@ -553,22 +549,28 @@ fn parse_encapsulated_content_info_parse( }) } -fn parse_certificate_set_implicit_parse( - obj: &DerObject<'_>, -) -> Result>, SignedObjectParseError> { - let content = obj - .as_slice() - .map_err(|e| SignedObjectParseError::Parse(e.to_string()))?; - let mut input = content; - let mut certs = Vec::new(); +fn split_der_objects(mut input: &[u8]) -> Result>, SignedObjectParseError> { + let mut out: Vec> = Vec::new(); while !input.is_empty() { - let (rem, _any_obj) = - parse_der(input).map_err(|e| SignedObjectParseError::Parse(e.to_string()))?; + let (_tag, _value, rem) = + der_take_tlv(input).map_err(|e| SignedObjectParseError::Parse(e.to_string()))?; let consumed = input.len() - rem.len(); - certs.push(input[..consumed].to_vec()); + out.push(input[..consumed].to_vec()); input = rem; } - Ok(certs) + Ok(out) +} + +fn parse_signer_infos_set_cursor(set_bytes: &[u8]) -> Result, SignedObjectParseError> { + let mut set = DerReader::new(set_bytes); + let mut out: Vec = Vec::new(); + while !set.is_empty() { + let si = set + .take_sequence() + .map_err(|e| SignedObjectParseError::Parse(e.to_string()))?; + out.push(parse_signer_info_cursor(si)?); + } + Ok(out) } fn validate_ee_certificate(der: &[u8]) -> Result { @@ -603,11 +605,7 @@ fn validate_ee_certificate(der: &[u8]) -> Result = match sia { - SubjectInfoAccess::Ee(ee) => ee - .signed_object_uris - .iter() - .map(|u| u.as_str().to_string()) - .collect(), + SubjectInfoAccess::Ee(ee) => ee.signed_object_uris.clone(), SubjectInfoAccess::Ca(_ca) => Vec::new(), }; if signed_object_uris.is_empty() { @@ -626,69 +624,64 @@ fn validate_ee_certificate(der: &[u8]) -> Result, -) -> Result { - let seq = obj - .as_sequence() - .map_err(|e| SignedObjectParseError::Parse(e.to_string()))?; - if seq.len() < 5 || seq.len() > 7 { - return Err(SignedObjectParseError::Parse( - "SignerInfo must be a SEQUENCE of 5..7 elements".into(), - )); - } - - let version = seq[0] - .as_u64() +fn parse_signer_info_cursor(mut seq: DerReader<'_>) -> Result { + let version = seq + .take_uint_u64() .map_err(|e| SignedObjectParseError::Parse(e.to_string()))?; - let sid = &seq[1]; - let sid = if sid.class() == Class::ContextSpecific && sid.tag() == Tag(0) { - let ski = sid - .as_slice() - .map_err(|e| SignedObjectParseError::Parse(e.to_string()))? - .to_vec(); - SignerIdentifierParsed::SubjectKeyIdentifier(ski) + let (sid_tag, sid_bytes) = seq + .take_any() + .map_err(|e| SignedObjectParseError::Parse(e.to_string()))?; + let sid = if (sid_tag & 0xC0) == 0x80 && (sid_tag & 0x1F) == 0 { + SignerIdentifierParsed::SubjectKeyIdentifier(sid_bytes.to_vec()) } else { SignerIdentifierParsed::Other }; - let (digest_oid, digest_params_ok) = parse_algorithm_identifier_parse(&seq[2])?; + let digest_alg_seq = seq + .take_sequence() + .map_err(|e| SignedObjectParseError::Parse(e.to_string()))?; + let (digest_oid, digest_params_ok) = parse_algorithm_identifier_cursor(digest_alg_seq)?; let digest_algorithm = AlgorithmIdentifierParsed { oid: digest_oid, params_ok: digest_params_ok, }; - let mut idx = 3; let mut signed_attrs_content: Option> = None; let mut signed_attrs_der_for_signature: Option> = None; - if seq[idx].class() == Class::ContextSpecific && seq[idx].tag() == Tag(0) { - let signed_attrs_obj = &seq[idx]; - let content = signed_attrs_obj - .as_slice() - .map_err(|e| SignedObjectParseError::Parse(e.to_string()))? - .to_vec(); - signed_attrs_content = Some(content); - signed_attrs_der_for_signature = - Some(make_signed_attrs_der_for_signature_parse(signed_attrs_obj)?); - idx += 1; + if seq + .peek_tag() + .map_err(|e| SignedObjectParseError::Parse(e.to_string()))? + == 0xA0 + { + let (tag, full_tlv, value) = seq + .take_any_full() + .map_err(|e| SignedObjectParseError::Parse(e.to_string()))?; + if tag != 0xA0 { + return Err(SignedObjectParseError::Parse( + "SignerInfo.signedAttrs must be [0] IMPLICIT".into(), + )); + } + signed_attrs_content = Some(value.to_vec()); + signed_attrs_der_for_signature = Some(make_signed_attrs_der_for_signature(full_tlv)?); } - let (signature_oid, signature_params_ok) = parse_algorithm_identifier_parse(&seq[idx])?; + let sig_alg_seq = seq + .take_sequence() + .map_err(|e| SignedObjectParseError::Parse(e.to_string()))?; + let (signature_oid, signature_params_ok) = parse_algorithm_identifier_cursor(sig_alg_seq)?; let signature_algorithm = AlgorithmIdentifierParsed { oid: signature_oid, params_ok: signature_params_ok, }; - idx += 1; - let signature = seq[idx] - .as_slice() + let signature = seq + .take_octet_string() .map_err(|e| SignedObjectParseError::Parse(e.to_string()))? .to_vec(); - idx += 1; - let unsigned_attrs_present = seq.get(idx).is_some(); + let unsigned_attrs_present = !seq.is_empty(); Ok(SignerInfoParsed { version, @@ -696,9 +689,9 @@ fn parse_signer_info_parse( digest_algorithm, signature_algorithm, signed_attrs_content, + signed_attrs_der_for_signature, unsigned_attrs_present, signature, - signed_attrs_der_for_signature, }) } @@ -843,58 +836,90 @@ fn parse_signed_attrs_implicit( let mut message_digest: Option> = None; let mut signing_time: Option = None; - let mut remaining = input; - while !remaining.is_empty() { - let (rem, attr_obj) = parse_der(remaining) - .map_err(|e| SignedObjectValidateError::SignedAttrsParse(e.to_string()))?; - remaining = rem; + fn count_elements(mut r: DerReader<'_>) -> Result { + let mut n = 0usize; + while !r.is_empty() { + r.skip_any()?; + n += 1; + } + Ok(n) + } - let attr_seq = attr_obj - .as_sequence() + let mut remaining = DerReader::new(input); + while !remaining.is_empty() { + let mut attr = remaining + .take_sequence() .map_err(|e| SignedObjectValidateError::SignedAttrsParse(e.to_string()))?; - if attr_seq.len() != 2 { + + let oid_bytes = attr + .take_tag(0x06) + .map_err(|e| SignedObjectValidateError::SignedAttrsParse(e.to_string()))?; + let oid = oid_value_bytes_to_string(oid_bytes); + + let values_bytes = attr + .take_tag(0x31) + .map_err(|e| SignedObjectValidateError::SignedAttrsParse(e.to_string()))?; + if !attr.is_empty() { return Err(SignedObjectValidateError::SignedAttrsParse( "Attribute must be SEQUENCE of 2".into(), )); } - let oid = oid_to_string_parse(&attr_seq[0]) - .map_err(|e| SignedObjectValidateError::SignedAttrsParse(e.to_string()))?; - let values_set = attr_seq[1] - .as_set() - .map_err(|e| SignedObjectValidateError::SignedAttrsParse(e.to_string()))?; - if values_set.len() != 1 { - return Err( - SignedObjectValidateError::InvalidSignedAttributeValuesCount { - oid, - count: values_set.len(), - }, - ); + + let mut values = DerReader::new(values_bytes); + let count = if values.is_empty() { + 0 + } else { + values + .skip_any() + .map_err(|e| SignedObjectValidateError::SignedAttrsParse(e.to_string()))?; + if values.is_empty() { + 1 + } else { + 1 + count_elements(values) + .map_err(|e| SignedObjectValidateError::SignedAttrsParse(e.to_string()))? + } + }; + if count != 1 { + return Err(SignedObjectValidateError::InvalidSignedAttributeValuesCount { + oid, + count, + }); } + // Re-parse the sole value. + let mut values = DerReader::new(values_bytes); + let (val_tag, val_bytes) = values + .take_any() + .map_err(|e| SignedObjectValidateError::SignedAttrsParse(e.to_string()))?; + match oid.as_str() { OID_CMS_ATTR_CONTENT_TYPE => { if content_type.is_some() { return Err(SignedObjectValidateError::DuplicateSignedAttribute(oid)); } - let v = oid_to_string_parse(&values_set[0]) - .map_err(|e| SignedObjectValidateError::SignedAttrsParse(e.to_string()))?; - content_type = Some(v); + if val_tag != 0x06 { + return Err(SignedObjectValidateError::SignedAttrsParse( + "content-type attr value must be OBJECT IDENTIFIER".into(), + )); + } + content_type = Some(oid_value_bytes_to_string(val_bytes)); } OID_CMS_ATTR_MESSAGE_DIGEST => { if message_digest.is_some() { return Err(SignedObjectValidateError::DuplicateSignedAttribute(oid)); } - let v = values_set[0] - .as_slice() - .map_err(|e| SignedObjectValidateError::SignedAttrsParse(e.to_string()))? - .to_vec(); - message_digest = Some(v); + if val_tag != 0x04 { + return Err(SignedObjectValidateError::SignedAttrsParse( + "message-digest attr value must be OCTET STRING".into(), + )); + } + message_digest = Some(val_bytes.to_vec()); } OID_CMS_ATTR_SIGNING_TIME => { if signing_time.is_some() { return Err(SignedObjectValidateError::DuplicateSignedAttribute(oid)); } - signing_time = Some(parse_signing_time_value(&values_set[0])?); + signing_time = Some(parse_signing_time_value_tlv(val_tag, val_bytes)?); } _ => { return Err(SignedObjectValidateError::UnsupportedSignedAttribute(oid)); @@ -913,35 +938,27 @@ fn parse_signed_attrs_implicit( }) } -fn parse_signing_time_value(obj: &DerObject<'_>) -> Result { - match &obj.content { - der_parser::ber::BerObjectContent::UTCTime(dt) => Ok(Asn1TimeUtc { - utc: dt - .to_datetime() - .map_err(|_| SignedObjectValidateError::InvalidSigningTimeValue)?, +fn parse_signing_time_value_tlv(tag: u8, value: &[u8]) -> Result { + match tag { + 0x17 => Ok(Asn1TimeUtc { + utc: parse_utctime(value)?, encoding: Asn1TimeEncoding::UtcTime, }), - der_parser::ber::BerObjectContent::GeneralizedTime(dt) => Ok(Asn1TimeUtc { - utc: dt - .to_datetime() - .map_err(|_| SignedObjectValidateError::InvalidSigningTimeValue)?, + 0x18 => Ok(Asn1TimeUtc { + utc: parse_generalized_time(value)?, encoding: Asn1TimeEncoding::GeneralizedTime, }), _ => Err(SignedObjectValidateError::InvalidSigningTimeValue), } } -fn make_signed_attrs_der_for_signature_parse( - obj: &DerObject<'_>, -) -> Result, SignedObjectParseError> { +fn make_signed_attrs_der_for_signature(full_tlv: &[u8]) -> Result, SignedObjectParseError> { // We need the DER encoding of SignedAttributes (SET OF Attribute) as signature input. // The SignedAttributes field in SignerInfo is `[0] IMPLICIT`, so the on-wire bytes start with // a context-specific constructed tag (0xA0 for tag 0). For signature verification, this tag // is replaced with the universal SET tag (0x31), leaving length+content unchanged. // - let mut cs_der = obj - .to_vec() - .map_err(|e| SignedObjectParseError::Parse(e.to_string()))?; + let mut cs_der = full_tlv.to_vec(); if cs_der.is_empty() { return Err(SignedObjectParseError::Parse( "signedAttrs encoding is empty".into(), @@ -953,32 +970,162 @@ fn make_signed_attrs_der_for_signature_parse( Ok(cs_der) } -fn oid_to_string_parse(obj: &DerObject<'_>) -> Result { - let oid = obj - .as_oid() +fn take_oid_string(seq: &mut DerReader<'_>) -> Result { + let oid = seq + .take_tag(0x06) .map_err(|e| SignedObjectParseError::Parse(e.to_string()))?; - Ok(oid.to_id_string()) + Ok(oid_value_bytes_to_string(oid)) } -fn parse_algorithm_identifier_parse( - obj: &DerObject<'_>, +fn oid_value_bytes_to_string(oid_value: &[u8]) -> String { + if oid_value == OID_SHA256_RAW { + return OID_SHA256.to_string(); + } + if oid_value == OID_SIGNED_DATA_RAW { + return OID_SIGNED_DATA.to_string(); + } + if oid_value == OID_CMS_ATTR_CONTENT_TYPE_RAW { + return OID_CMS_ATTR_CONTENT_TYPE.to_string(); + } + if oid_value == OID_CMS_ATTR_MESSAGE_DIGEST_RAW { + return OID_CMS_ATTR_MESSAGE_DIGEST.to_string(); + } + if oid_value == OID_CMS_ATTR_SIGNING_TIME_RAW { + return OID_CMS_ATTR_SIGNING_TIME.to_string(); + } + if oid_value == OID_RSA_ENCRYPTION_RAW { + return OID_RSA_ENCRYPTION.to_string(); + } + if oid_value == OID_SHA256_WITH_RSA_ENCRYPTION_RAW { + return OID_SHA256_WITH_RSA_ENCRYPTION.to_string(); + } + if oid_value == OID_CT_RPKI_MANIFEST_RAW { + return OID_CT_RPKI_MANIFEST.to_string(); + } + if oid_value == OID_CT_ROUTE_ORIGIN_AUTHZ_RAW { + return OID_CT_ROUTE_ORIGIN_AUTHZ.to_string(); + } + if oid_value == OID_CT_ASPA_RAW { + return OID_CT_ASPA.to_string(); + } + decode_oid_to_dotted_string(oid_value) +} + +fn decode_oid_to_dotted_string(value: &[u8]) -> String { + if value.is_empty() { + return "".into(); + } + let first = value[0]; + let a = (first / 40) as u32; + let b = (first % 40) as u32; + let mut out = String::new(); + out.push_str(&a.to_string()); + out.push('.'); + out.push_str(&b.to_string()); + + let mut idx = 1usize; + while idx < value.len() { + let mut v: u32 = 0; + loop { + if idx >= value.len() { + out.push_str("."); + return out; + } + let byte = value[idx]; + idx += 1; + v = (v << 7) | (byte as u32 & 0x7F); + if (byte & 0x80) == 0 { + break; + } + } + out.push('.'); + out.push_str(&v.to_string()); + } + out +} + +fn parse_algorithm_identifier_cursor( + mut seq: DerReader<'_>, ) -> Result<(String, bool), SignedObjectParseError> { - let seq = obj - .as_sequence() - .map_err(|e| SignedObjectParseError::Parse(e.to_string()))?; - if seq.is_empty() || seq.len() > 2 { + if seq.is_empty() { return Err(SignedObjectParseError::Parse( "AlgorithmIdentifier must be SEQUENCE of 1..2".into(), )); } - let oid = oid_to_string_parse(&seq[0])?; - let params_ok = match seq.get(1) { - None => true, - Some(p) => matches!(p.content, der_parser::ber::BerObjectContent::Null), + let oid = take_oid_string(&mut seq)?; + let params_ok = if seq.is_empty() { + true + } else { + let (tag, value) = seq + .take_any() + .map_err(|e| SignedObjectParseError::Parse(e.to_string()))?; + tag == 0x05 && value.is_empty() }; + if !seq.is_empty() { + return Err(SignedObjectParseError::Parse( + "AlgorithmIdentifier must be SEQUENCE of 1..2".into(), + )); + } Ok((oid, params_ok)) } +fn parse_utctime(value: &[u8]) -> Result { + let s = std::str::from_utf8(value).map_err(|_| SignedObjectValidateError::InvalidSigningTimeValue)?; + if !s.ends_with('Z') { + return Err(SignedObjectValidateError::InvalidSigningTimeValue); + } + let digits = &s[..s.len() - 1]; + if digits.len() != 10 && digits.len() != 12 { + return Err(SignedObjectValidateError::InvalidSigningTimeValue); + } + if !digits.as_bytes().iter().all(|b| b.is_ascii_digit()) { + return Err(SignedObjectValidateError::InvalidSigningTimeValue); + } + let yy: i32 = digits[0..2].parse().map_err(|_| SignedObjectValidateError::InvalidSigningTimeValue)?; + let year = if yy <= 49 { 2000 + yy } else { 1900 + yy }; + let mon: u8 = digits[2..4].parse().map_err(|_| SignedObjectValidateError::InvalidSigningTimeValue)?; + let day: u8 = digits[4..6].parse().map_err(|_| SignedObjectValidateError::InvalidSigningTimeValue)?; + let hour: u8 = digits[6..8].parse().map_err(|_| SignedObjectValidateError::InvalidSigningTimeValue)?; + let min: u8 = digits[8..10].parse().map_err(|_| SignedObjectValidateError::InvalidSigningTimeValue)?; + let sec: u8 = if digits.len() == 12 { + digits[10..12].parse().map_err(|_| SignedObjectValidateError::InvalidSigningTimeValue)? + } else { + 0 + }; + let month = time::Month::try_from(mon).map_err(|_| SignedObjectValidateError::InvalidSigningTimeValue)?; + let date = time::Date::from_calendar_date(year, month, day).map_err(|_| SignedObjectValidateError::InvalidSigningTimeValue)?; + let time = time::Time::from_hms(hour, min, sec).map_err(|_| SignedObjectValidateError::InvalidSigningTimeValue)?; + Ok(time::OffsetDateTime::new_utc(date, time)) +} + +fn parse_generalized_time(value: &[u8]) -> Result { + let s = std::str::from_utf8(value).map_err(|_| SignedObjectValidateError::InvalidSigningTimeValue)?; + if !s.ends_with('Z') { + return Err(SignedObjectValidateError::InvalidSigningTimeValue); + } + let digits = &s[..s.len() - 1]; + if digits.len() != 12 && digits.len() != 14 { + return Err(SignedObjectValidateError::InvalidSigningTimeValue); + } + if !digits.as_bytes().iter().all(|b| b.is_ascii_digit()) { + return Err(SignedObjectValidateError::InvalidSigningTimeValue); + } + let year: i32 = digits[0..4].parse().map_err(|_| SignedObjectValidateError::InvalidSigningTimeValue)?; + let mon: u8 = digits[4..6].parse().map_err(|_| SignedObjectValidateError::InvalidSigningTimeValue)?; + let day: u8 = digits[6..8].parse().map_err(|_| SignedObjectValidateError::InvalidSigningTimeValue)?; + let hour: u8 = digits[8..10].parse().map_err(|_| SignedObjectValidateError::InvalidSigningTimeValue)?; + let min: u8 = digits[10..12].parse().map_err(|_| SignedObjectValidateError::InvalidSigningTimeValue)?; + let sec: u8 = if digits.len() == 14 { + digits[12..14].parse().map_err(|_| SignedObjectValidateError::InvalidSigningTimeValue)? + } else { + 0 + }; + let month = time::Month::try_from(mon).map_err(|_| SignedObjectValidateError::InvalidSigningTimeValue)?; + let date = time::Date::from_calendar_date(year, month, day).map_err(|_| SignedObjectValidateError::InvalidSigningTimeValue)?; + let time = time::Time::from_hms(hour, min, sec).map_err(|_| SignedObjectValidateError::InvalidSigningTimeValue)?; + Ok(time::OffsetDateTime::new_utc(date, time)) +} + fn strip_leading_zeros(bytes: &[u8]) -> &[u8] { let mut idx = 0; while idx < bytes.len() && bytes[idx] == 0 { diff --git a/src/data_model/ta.rs b/src/data_model/ta.rs index e4a2d36..1ee7868 100644 --- a/src/data_model/ta.rs +++ b/src/data_model/ta.rs @@ -211,7 +211,7 @@ impl TaCertificateParsed { return Err(TaCertificateProfileError::NotCa); } - if rc_ca.tbs.issuer_dn != rc_ca.tbs.subject_dn { + if rc_ca.tbs.issuer_name != rc_ca.tbs.subject_name { return Err(TaCertificateProfileError::NotSelfSignedIssuerSubject); } diff --git a/src/validation/ca_instance.rs b/src/validation/ca_instance.rs index 23119ca..fbca776 100644 --- a/src/validation/ca_instance.rs +++ b/src/validation/ca_instance.rs @@ -78,23 +78,23 @@ pub fn ca_instance_uris_from_ca_certificate( for ad in access_descriptions { if ad.access_method_oid == OID_AD_CA_REPOSITORY { - let u = ad.access_location.to_string(); - if ad.access_location.scheme() != "rsync" { - return Err(CaInstanceUrisError::CaRepositoryNotRsync(u)); + let u = ad.access_location.as_str(); + if !u.starts_with("rsync://") { + return Err(CaInstanceUrisError::CaRepositoryNotRsync(u.to_string())); } - ca_repo.get_or_insert(u); + ca_repo.get_or_insert(u.to_string()); } else if ad.access_method_oid == OID_AD_RPKI_MANIFEST { - let u = ad.access_location.to_string(); - if ad.access_location.scheme() != "rsync" { - return Err(CaInstanceUrisError::RpkiManifestNotRsync(u)); + let u = ad.access_location.as_str(); + if !u.starts_with("rsync://") { + return Err(CaInstanceUrisError::RpkiManifestNotRsync(u.to_string())); } - manifest.get_or_insert(u); + manifest.get_or_insert(u.to_string()); } else if ad.access_method_oid == OID_AD_RPKI_NOTIFY { - let u = ad.access_location.to_string(); - if ad.access_location.scheme() != "https" { - return Err(CaInstanceUrisError::RpkiNotifyNotHttps(u)); + let u = ad.access_location.as_str(); + if !u.starts_with("https://") { + return Err(CaInstanceUrisError::RpkiNotifyNotHttps(u.to_string())); } - notify.get_or_insert(u); + notify.get_or_insert(u.to_string()); } } diff --git a/src/validation/ca_path.rs b/src/validation/ca_path.rs index 2af638a..0a397cd 100644 --- a/src/validation/ca_path.rs +++ b/src/validation/ca_path.rs @@ -1,6 +1,6 @@ use crate::data_model::common::BigUnsigned; use crate::data_model::crl::{CrlDecodeError, CrlVerifyError, RpkixCrl}; -use crate::data_model::oid::OID_KEY_USAGE; +use crate::data_model::oid::OID_KEY_USAGE_RAW; use crate::data_model::rc::{ AsIdentifierChoice, AsResourceSet, IpAddressChoice, IpResourceSet, ResourceCertKind, ResourceCertificate, ResourceCertificateDecodeError, @@ -135,10 +135,10 @@ pub fn validate_subordinate_ca_cert( return Err(CaPathError::IssuerNotCa); } - if child_ca.tbs.issuer_dn != issuer_ca.tbs.subject_dn { + if child_ca.tbs.issuer_name != issuer_ca.tbs.subject_name { return Err(CaPathError::IssuerSubjectMismatch { - child_issuer_dn: child_ca.tbs.issuer_dn.clone(), - issuer_subject_dn: issuer_ca.tbs.subject_dn.clone(), + child_issuer_dn: child_ca.tbs.issuer_name.to_string(), + issuer_subject_dn: issuer_ca.tbs.subject_name.to_string(), }); } @@ -247,7 +247,7 @@ fn validate_child_ca_key_usage(child_ca_der: &[u8]) -> Result<(), CaPathError> { let mut ku_critical: Option = None; for ext in cert.extensions() { - if ext.oid.to_id_string() == OID_KEY_USAGE { + if ext.oid.as_bytes() == OID_KEY_USAGE_RAW { ku_critical = Some(ext.critical); break; } @@ -695,9 +695,8 @@ mod tests { use crate::data_model::rc::{ RcExtensions, ResourceCertKind, ResourceCertificate, RpkixTbsCertificate, }; + use crate::data_model::common::X509NameDer; use der_parser::num_bigint::BigUint; - use url::Url; - fn dummy_cert( kind: ResourceCertKind, subject_dn: &str, @@ -707,16 +706,8 @@ mod tests { aia: Option>, crldp: Option>, ) -> ResourceCertificate { - let aia = aia.map(|v| { - v.into_iter() - .map(|s| Url::parse(s).expect("url")) - .collect::>() - }); - let crldp = crldp.map(|v| { - v.into_iter() - .map(|s| Url::parse(s).expect("url")) - .collect::>() - }); + let aia = aia.map(|v| v.into_iter().map(|s| s.to_string()).collect::>()); + let crldp = crldp.map(|v| v.into_iter().map(|s| s.to_string()).collect::>()); ResourceCertificate { raw_der: Vec::new(), @@ -725,8 +716,8 @@ mod tests { version: 2, serial_number: BigUint::from(1u8), signature_algorithm: "1.2.840.113549.1.1.11".to_string(), - issuer_dn: issuer_dn.to_string(), - subject_dn: subject_dn.to_string(), + issuer_name: X509NameDer(issuer_dn.as_bytes().to_vec()), + subject_name: X509NameDer(subject_dn.as_bytes().to_vec()), validity_not_before: time::OffsetDateTime::UNIX_EPOCH, validity_not_after: time::OffsetDateTime::UNIX_EPOCH, subject_public_key_info: Vec::new(), diff --git a/src/validation/cert_path.rs b/src/validation/cert_path.rs index a453b4b..8202a6a 100644 --- a/src/validation/cert_path.rs +++ b/src/validation/cert_path.rs @@ -112,10 +112,10 @@ pub fn validate_ee_cert_path( return Err(CertPathError::IssuerNotCa); } - if ee.tbs.issuer_dn != issuer_ca.tbs.subject_dn { + if ee.tbs.issuer_name != issuer_ca.tbs.subject_name { return Err(CertPathError::IssuerSubjectMismatch { - ee_issuer_dn: ee.tbs.issuer_dn.clone(), - issuer_subject_dn: issuer_ca.tbs.subject_dn.clone(), + ee_issuer_dn: ee.tbs.issuer_name.to_string(), + issuer_subject_dn: issuer_ca.tbs.subject_name.to_string(), }); } @@ -212,7 +212,7 @@ fn validate_ee_key_usage(ee_cert_der: &[u8]) -> Result<(), CertPathError> { let mut ku_critical: Option = None; for ext in cert.extensions() { - if ext.oid.to_id_string() == crate::data_model::oid::OID_KEY_USAGE { + if ext.oid.as_bytes() == crate::data_model::oid::OID_KEY_USAGE_RAW { ku_critical = Some(ext.critical); break; } @@ -302,8 +302,8 @@ mod tests { use crate::data_model::rc::{ RcExtensions, ResourceCertKind, ResourceCertificate, RpkixTbsCertificate, }; + use crate::data_model::common::X509NameDer; use der_parser::num_bigint::BigUint; - use url::Url; fn dummy_cert( kind: ResourceCertKind, @@ -314,16 +314,8 @@ mod tests { aia: Option>, crldp: Option>, ) -> ResourceCertificate { - let aia = aia.map(|v| { - v.into_iter() - .map(|s| Url::parse(s).expect("url")) - .collect::>() - }); - let crldp = crldp.map(|v| { - v.into_iter() - .map(|s| Url::parse(s).expect("url")) - .collect::>() - }); + let aia = aia.map(|v| v.into_iter().map(|s| s.to_string()).collect::>()); + let crldp = crldp.map(|v| v.into_iter().map(|s| s.to_string()).collect::>()); ResourceCertificate { raw_der: Vec::new(), kind, @@ -331,8 +323,8 @@ mod tests { version: 2, serial_number: BigUint::from(1u8), signature_algorithm: "1.2.840.113549.1.1.11".to_string(), - issuer_dn: issuer_dn.to_string(), - subject_dn: subject_dn.to_string(), + issuer_name: X509NameDer(issuer_dn.as_bytes().to_vec()), + subject_name: X509NameDer(subject_dn.as_bytes().to_vec()), validity_not_before: time::OffsetDateTime::UNIX_EPOCH, validity_not_after: time::OffsetDateTime::UNIX_EPOCH, subject_public_key_info: Vec::new(), diff --git a/src/validation/objects.rs b/src/validation/objects.rs index 1dfe2c8..62d5f4f 100644 --- a/src/validation/objects.rs +++ b/src/validation/objects.rs @@ -462,7 +462,7 @@ fn process_aspa_with_issuer( } fn choose_crl_for_certificate( - crldp_uris: Option<&Vec>, + crldp_uris: Option<&Vec>, crl_files: &[(String, Vec)], ) -> Result<(String, Vec), ObjectValidateError> { if crl_files.is_empty() { diff --git a/tests/bench_stage2_collect_selected_der_v2.rs b/tests/bench_stage2_collect_selected_der_v2.rs new file mode 100644 index 0000000..8d149a1 --- /dev/null +++ b/tests/bench_stage2_collect_selected_der_v2.rs @@ -0,0 +1,933 @@ +use serde::Serialize; +use sha2::Digest; +use std::collections::{BTreeMap, HashSet}; +use std::path::{Path, PathBuf}; + +use rpki::data_model::aspa::AspaObject; +use rpki::data_model::crl::RpkixCrl; +use rpki::data_model::manifest::ManifestObject; +use rpki::data_model::rc::{AsIdentifierChoice, AsResourceSet, IpAddressChoice, IpResourceSet, ResourceCertificate}; +use rpki::data_model::roa::RoaObject; + +#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize)] +#[serde(rename_all = "lowercase")] +enum ObjType { + Cer, + Crl, + Manifest, + Roa, + Aspa, +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] +enum Part { + PackManifest, + PackCrl, + StoredObject { index: u32 }, +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)] +struct CandidateRef { + pack_index: u32, + part: Part, + size_bytes: u32, +} + +#[derive(Clone, Debug, Serialize)] +struct SampleMeta { + obj_type: ObjType, + label: String, + file_rel_path: String, + size_bytes: u32, + sha256_hex: String, + + // Provenance (relative to BENCH_STORE_DIR) + pack_rel_path: String, + pack_update_time_rfc3339_utc: String, + manifest_uri: String, + object_uri: String, + + // From StoredManifest (if available) + manifest_this_update_rfc3339_utc: String, + manifest_not_after_rfc3339_utc: String, + + metrics: Metrics, +} + +#[derive(Clone, Debug, Serialize)] +#[serde(tag = "kind", rename_all = "snake_case")] +enum Metrics { + Cer { + spki_len: u32, + ext_count: u32, + ip_resource_count: u32, + as_resource_count: u32, + }, + Crl { + revoked_count: u32, + }, + Manifest { + file_count: u32, + }, + Roa { + addr_family_count: u32, + prefix_count: u32, + max_length_present: u32, + }, + Aspa { + provider_count: u32, + }, +} + +#[derive(Clone, Debug, Serialize)] +struct SamplesManifest { + created_at_rfc3339_utc: String, + store_dir_hint: String, + per_type: BTreeMap, + samples: Vec, +} + +fn env_string(name: &str) -> Option { + std::env::var(name).ok().filter(|s| !s.trim().is_empty()) +} + +fn env_u64_opt(name: &str) -> Option { + std::env::var(name).ok().and_then(|s| s.parse::().ok()) +} + +fn env_bool(name: &str) -> bool { + matches!( + std::env::var(name).as_deref(), + Ok("1") | Ok("true") | Ok("TRUE") | Ok("yes") | Ok("YES") + ) +} + +fn create_parent_dirs(path: &Path) { + if let Some(parent) = path.parent() { + std::fs::create_dir_all(parent).unwrap_or_else(|e| { + panic!("create_dir_all {}: {e}", parent.display()); + }); + } +} + +fn walk_collect_files(root: &Path, out: &mut Vec) -> std::io::Result<()> { + for entry in std::fs::read_dir(root)? { + let entry = entry?; + let path = entry.path(); + let meta = entry.metadata()?; + if meta.is_dir() { + walk_collect_files(&path, out)?; + continue; + } + if meta.is_file() { + out.push(path); + } + } + Ok(()) +} + +fn ext_from_uri_bytes(uri: &[u8]) -> Option<&[u8]> { + let mut last_dot = None; + for (i, &b) in uri.iter().enumerate() { + if b == b'.' { + last_dot = Some(i); + } + } + let dot = last_dot?; + if dot + 1 >= uri.len() { + return None; + } + Some(&uri[dot + 1..]) +} + +fn ext_matches_ascii_case_insensitive(ext: &[u8], needle_lower: &[u8]) -> bool { + if ext.len() != needle_lower.len() { + return false; + } + ext.iter() + .zip(needle_lower.iter()) + .all(|(&a, &b)| a.to_ascii_lowercase() == b) +} + +fn percentile_index(len: usize, q: f64) -> usize { + if len == 0 { + return 0; + } + if len == 1 { + return 0; + } + let q = q.clamp(0.0, 1.0); + (((len - 1) as f64) * q).round() as usize +} + +fn quantile_labels() -> Vec<(&'static str, f64)> { + vec![ + ("min", 0.0), + ("p01", 0.01), + ("p10", 0.10), + ("p25", 0.25), + ("p50", 0.50), + ("p75", 0.75), + ("p90", 0.90), + ("p95", 0.95), + ("p99", 0.99), + ("max", 1.0), + ] +} + +fn fmt_rfc3339_utc(t: time::OffsetDateTime) -> String { + use time::format_description::well_known::Rfc3339; + t.to_offset(time::UtcOffset::UTC) + .format(&Rfc3339) + .expect("format RFC3339") +} + +fn scan_pack_candidates( + pack_index: u32, + bytes: &[u8], + out_cer: &mut Vec, + out_crl: &mut Vec, + out_manifest: &mut Vec, + out_roa: &mut Vec, + out_aspa: &mut Vec, +) -> Result<(), String> { + let mut cur = ByteCursor::new(bytes); + + let _version = cur.read_u8()?; + cur.skip_string_u32()?; // manifest_uri + cur.skip_option_string_u32()?; // rpki_notify + + let update_kind = cur.read_u8()?; + cur.skip_i64()?; // update time + if update_kind != 0 { + // LastAttempt: no StoredManifest + objects + return Ok(()); + } + + cur.skip_i64()?; // not_after + cur.skip(20)?; // manifest_number + cur.skip_i64()?; // this_update + cur.skip_string_u32()?; // ca_repository + + let mft_len = cur.peek_u64_as_usize()?; + cur.skip_bytes_u64()?; // manifest DER + out_manifest.push(CandidateRef { + pack_index, + part: Part::PackManifest, + size_bytes: mft_len as u32, + }); + + cur.skip_string_u32()?; // crl_uri + let crl_len = cur.peek_u64_as_usize()?; + cur.skip_bytes_u64()?; + out_crl.push(CandidateRef { + pack_index, + part: Part::PackCrl, + size_bytes: crl_len as u32, + }); + + let mut object_index = 0u32; + while cur.remaining() > 0 { + let uri_bytes = cur.read_bytes_u32()?; + let ext = ext_from_uri_bytes(uri_bytes); + + let hash_type = cur.read_u8()?; + if hash_type == 1 { + cur.skip(32)?; + } else if hash_type != 0 { + return Err(format!("unsupported stored object hash_type {hash_type}")); + } + + let content_len = cur.peek_u64_as_usize()?; + cur.skip_bytes_u64()?; + + let cand = CandidateRef { + pack_index, + part: Part::StoredObject { index: object_index }, + size_bytes: content_len as u32, + }; + if let Some(ext) = ext { + if ext_matches_ascii_case_insensitive(ext, b"cer") { + out_cer.push(cand); + } else if ext_matches_ascii_case_insensitive(ext, b"roa") { + out_roa.push(cand); + } else if ext_matches_ascii_case_insensitive(ext, b"asa") { + out_aspa.push(cand); + } + } + + object_index += 1; + } + + Ok(()) +} + +struct Extracted { + pack_update_time_utc: time::OffsetDateTime, + manifest_uri: String, + manifest_this_update_utc: time::OffsetDateTime, + manifest_not_after_utc: time::OffsetDateTime, + object_uri: String, + der: Vec, +} + +fn extract_candidate(pack_path: &Path, cand: CandidateRef) -> Result { + let bytes = std::fs::read(pack_path).map_err(|e| format!("read {}: {e}", pack_path.display()))?; + let mut cur = ByteCursor::new(bytes.as_slice()); + + let _version = cur.read_u8()?; + let manifest_uri = cur.read_string_u32()?; + cur.skip_option_string_u32()?; // rpki_notify + + let update_kind = cur.read_u8()?; + let pack_update_time_utc = cur.read_time_utc_i64_be()?; + if update_kind != 0 { + return Err("pack is LastAttempt".to_string()); + } + + let manifest_not_after_utc = cur.read_time_utc_i64_be()?; + cur.skip(20)?; // manifest_number + let manifest_this_update_utc = cur.read_time_utc_i64_be()?; + cur.skip_string_u32()?; // ca_repository + + let manifest_der = cur.read_bytes_u64_vec()?; // manifest DER + + if cand.part == Part::PackManifest { + return Ok(Extracted { + pack_update_time_utc, + manifest_uri: manifest_uri.clone(), + manifest_this_update_utc, + manifest_not_after_utc, + object_uri: manifest_uri, + der: manifest_der, + }); + } + + let crl_uri = cur.read_string_u32()?; + let crl_der = cur.read_bytes_u64_vec()?; + + match cand.part { + Part::PackManifest => unreachable!("handled above"), + Part::PackCrl => Ok(Extracted { + pack_update_time_utc, + manifest_uri, + manifest_this_update_utc, + manifest_not_after_utc, + object_uri: crl_uri, + der: crl_der, + }), + Part::StoredObject { index } => { + let mut object_index = 0u32; + while cur.remaining() > 0 { + let uri = cur.read_string_u32()?; + let hash_type = cur.read_u8()?; + if hash_type == 1 { + cur.skip(32)?; + } else if hash_type != 0 { + return Err(format!("unsupported stored object hash_type {hash_type}")); + } + let content = cur.read_bytes_u64_vec()?; + if object_index == index { + return Ok(Extracted { + pack_update_time_utc, + manifest_uri, + manifest_this_update_utc, + manifest_not_after_utc, + object_uri: uri, + der: content, + }); + } + object_index += 1; + } + Err(format!("stored object index {index} out of range")) + } + } +} + +fn ip_resource_count(set: &IpResourceSet) -> u32 { + let mut n = 0u32; + for fam in &set.families { + match &fam.choice { + IpAddressChoice::Inherit => n = n.saturating_add(1), + IpAddressChoice::AddressesOrRanges(items) => { + n = n.saturating_add(items.len() as u32) + } + } + } + n +} + +fn as_choice_count(choice: &AsIdentifierChoice) -> u32 { + match choice { + AsIdentifierChoice::Inherit => 1, + AsIdentifierChoice::AsIdsOrRanges(items) => items.len() as u32, + } +} + +fn as_resource_count(set: &AsResourceSet) -> u32 { + let mut n = 0u32; + if let Some(c) = set.asnum.as_ref() { + n = n.saturating_add(as_choice_count(c)); + } + if let Some(c) = set.rdi.as_ref() { + n = n.saturating_add(as_choice_count(c)); + } + n +} + +fn metrics_for(obj_type: ObjType, der: &[u8]) -> Result { + match obj_type { + ObjType::Cer => { + let parsed = ResourceCertificate::parse_der(der).map_err(|e| e.to_string())?; + let spki_len = parsed.subject_public_key_info.len() as u32; + let ext_count = (parsed.extensions.basic_constraints_ca.len() + + parsed.extensions.subject_key_identifier.len() + + parsed.extensions.authority_key_identifier.len() + + parsed.extensions.crl_distribution_points.len() + + parsed.extensions.authority_info_access.len() + + parsed.extensions.subject_info_access.len() + + parsed.extensions.certificate_policies.len() + + parsed.extensions.ip_resources.len() + + parsed.extensions.as_resources.len()) as u32; + let cert = parsed.validate_profile().map_err(|e| e.to_string())?; + let ip_resource_count = cert + .tbs + .extensions + .ip_resources + .as_ref() + .map(ip_resource_count) + .unwrap_or(0); + let as_resource_count = cert + .tbs + .extensions + .as_resources + .as_ref() + .map(as_resource_count) + .unwrap_or(0); + Ok(Metrics::Cer { + spki_len, + ext_count, + ip_resource_count, + as_resource_count, + }) + } + ObjType::Crl => { + let parsed = RpkixCrl::parse_der(der).map_err(|e| e.to_string())?; + let crl = parsed.validate_profile().map_err(|e| e.to_string())?; + Ok(Metrics::Crl { + revoked_count: crl.revoked_certs.len() as u32, + }) + } + ObjType::Manifest => { + let mft = ManifestObject::decode_der(der).map_err(|e| e.to_string())?; + Ok(Metrics::Manifest { + file_count: mft.manifest.file_count() as u32, + }) + } + ObjType::Roa => { + let roa = RoaObject::decode_der(der).map_err(|e| e.to_string())?; + let addr_family_count = roa.roa.ip_addr_blocks.len() as u32; + let mut prefix_count = 0u32; + let mut max_length_present = 0u32; + for fam in &roa.roa.ip_addr_blocks { + for a in &fam.addresses { + prefix_count += 1; + if a.max_length.is_some() { + max_length_present += 1; + } + } + } + Ok(Metrics::Roa { + addr_family_count, + prefix_count, + max_length_present, + }) + } + ObjType::Aspa => { + let asa = AspaObject::decode_der(der).map_err(|e| e.to_string())?; + Ok(Metrics::Aspa { + provider_count: asa.aspa.provider_as_ids.len() as u32, + }) + } + } +} + +fn write_sample(out_dir: &Path, obj_type: ObjType, label: &str, der: &[u8]) -> PathBuf { + let (subdir, ext) = match obj_type { + ObjType::Cer => ("cer", "cer"), + ObjType::Crl => ("crl", "crl"), + ObjType::Manifest => ("manifest", "mft"), + ObjType::Roa => ("roa", "roa"), + ObjType::Aspa => ("aspa", "asa"), + }; + let path = out_dir.join(subdir).join(format!("{label}.{ext}")); + create_parent_dirs(&path); + std::fs::write(&path, der).unwrap_or_else(|e| panic!("write {}: {e}", path.display())); + path +} + +#[test] +#[ignore = "manual: collect representative CER/CRL/MFT/ROA/ASA DER fixtures from routinator stored/ SAP packs"] +fn stage2_collect_selected_der_v2_from_routinator_store() { + let store_dir = env_string("BENCH_STORE_DIR") + .map(PathBuf::from) + .unwrap_or_else(|| panic!("set BENCH_STORE_DIR to routinator stored/ dir")); + let out_dir = env_string("BENCH_OUT_DIR") + .map(PathBuf::from) + .unwrap_or_else(|| { + PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("tests/benchmark/selected_der_v2") + }); + let max_packs = env_u64_opt("BENCH_MAX_PACKS"); + let clean = env_bool("BENCH_CLEAN"); + let dry_run = env_bool("BENCH_DRY_RUN"); + let verbose = env_bool("BENCH_VERBOSE"); + + if clean && !dry_run && out_dir.exists() { + std::fs::remove_dir_all(&out_dir) + .unwrap_or_else(|e| panic!("remove_dir_all {}: {e}", out_dir.display())); + } + + let mut pack_paths = Vec::new(); + walk_collect_files(&store_dir, &mut pack_paths) + .unwrap_or_else(|e| panic!("walk {}: {e}", store_dir.display())); + pack_paths.retain(|p| p.extension().and_then(|s| s.to_str()) == Some("mft")); + pack_paths.sort(); + if let Some(max) = max_packs { + pack_paths.truncate(max as usize); + } + + println!("# Stage2 collect: selected_der_v2 (from routinator stored/ SAP packs)"); + println!(); + println!("- store_dir: {}", store_dir.display()); + println!("- out_dir: {}", out_dir.display()); + println!("- pack_files_found: {}", pack_paths.len()); + if let Some(max) = max_packs { + println!("- max_packs: {} (truncating input list)", max); + } + println!("- clean: {}", clean); + println!("- dry_run: {}", dry_run); + println!(); + + let mut cer = Vec::::new(); + let mut crl = Vec::::new(); + let mut mft = Vec::::new(); + let mut roa = Vec::::new(); + let mut asa = Vec::::new(); + + for (i, p) in pack_paths.iter().enumerate() { + let bytes = match std::fs::read(p) { + Ok(v) => v, + Err(_) => continue, + }; + if let Err(e) = scan_pack_candidates( + i as u32, + bytes.as_slice(), + &mut cer, + &mut crl, + &mut mft, + &mut roa, + &mut asa, + ) { + if verbose { + eprintln!("# scan error {}: {}", p.display(), e); + } + } + } + + let mut by_type: BTreeMap> = BTreeMap::new(); + by_type.insert(ObjType::Cer, cer); + by_type.insert(ObjType::Crl, crl); + by_type.insert(ObjType::Manifest, mft); + by_type.insert(ObjType::Roa, roa); + by_type.insert(ObjType::Aspa, asa); + + let mut samples = Vec::::new(); + let mut per_type_out = BTreeMap::::new(); + + for (obj_type, mut cands) in by_type { + cands.sort_by(|a, b| { + a.size_bytes + .cmp(&b.size_bytes) + .then_with(|| a.pack_index.cmp(&b.pack_index)) + .then_with(|| match (a.part, b.part) { + (Part::PackManifest, Part::PackManifest) => std::cmp::Ordering::Equal, + (Part::PackManifest, _) => std::cmp::Ordering::Less, + (_, Part::PackManifest) => std::cmp::Ordering::Greater, + (Part::PackCrl, Part::PackCrl) => std::cmp::Ordering::Equal, + (Part::PackCrl, Part::StoredObject { .. }) => std::cmp::Ordering::Less, + (Part::StoredObject { .. }, Part::PackCrl) => std::cmp::Ordering::Greater, + (Part::StoredObject { index: ai }, Part::StoredObject { index: bi }) => ai.cmp(&bi), + }) + }); + + println!("## {:?}", obj_type); + println!(); + println!("- candidates: {}", cands.len()); + if cands.is_empty() { + continue; + } + + let mut selected_sha = HashSet::::new(); + let mut selected_ptrs = HashSet::<(u32, Part)>::new(); + + for (label, q) in quantile_labels() { + let target = percentile_index(cands.len(), q); + + let mut found = None; + for off in 0.. { + let mut tried_any = false; + if off == 0 { + let j = target as i32; + if j < 0 || (j as usize) >= cands.len() { + tried_any = false; + } else { + tried_any = true; + let cand = cands[j as usize]; + if selected_ptrs.contains(&(cand.pack_index, cand.part)) { + continue; + } + + let pack_path = &pack_paths[cand.pack_index as usize]; + let extracted = match extract_candidate(pack_path.as_path(), cand) { + Ok(v) => v, + Err(_) => continue, + }; + + if extracted.der.len() != cand.size_bytes as usize { + continue; + } + + let sha256 = sha2::Sha256::digest(extracted.der.as_slice()); + let sha256_hex = hex::encode(sha256); + if selected_sha.contains(&sha256_hex) { + continue; + } + + let metrics = match metrics_for(obj_type, extracted.der.as_slice()) { + Ok(m) => m, + Err(_) => continue, + }; + + if !dry_run { + let p = write_sample( + out_dir.as_path(), + obj_type, + label, + extracted.der.as_slice(), + ); + if verbose { + println!( + "- wrote {} -> {}", + label, + p.strip_prefix(PathBuf::from(env!("CARGO_MANIFEST_DIR"))) + .unwrap_or(&p) + .display() + ); + } + } + + let pack_rel_path = pack_path + .strip_prefix(&store_dir) + .unwrap_or(pack_path.as_path()) + .display() + .to_string(); + + let file_rel_path = format!( + "tests/benchmark/selected_der_v2/{}/{}.{}", + match obj_type { + ObjType::Cer => "cer", + ObjType::Crl => "crl", + ObjType::Manifest => "manifest", + ObjType::Roa => "roa", + ObjType::Aspa => "aspa", + }, + label, + match obj_type { + ObjType::Cer => "cer", + ObjType::Crl => "crl", + ObjType::Manifest => "mft", + ObjType::Roa => "roa", + ObjType::Aspa => "asa", + } + ); + + let sha256_hex_for_set = sha256_hex.clone(); + samples.push(SampleMeta { + obj_type, + label: label.to_string(), + file_rel_path, + size_bytes: cand.size_bytes, + sha256_hex, + pack_rel_path, + pack_update_time_rfc3339_utc: fmt_rfc3339_utc( + extracted.pack_update_time_utc, + ), + manifest_uri: extracted.manifest_uri, + object_uri: extracted.object_uri, + manifest_this_update_rfc3339_utc: fmt_rfc3339_utc( + extracted.manifest_this_update_utc, + ), + manifest_not_after_rfc3339_utc: fmt_rfc3339_utc( + extracted.manifest_not_after_utc, + ), + metrics, + }); + + selected_ptrs.insert((cand.pack_index, cand.part)); + selected_sha.insert(sha256_hex_for_set); + found = Some(()); + break; + } + } else { + for delta in [off as i32, -(off as i32)] { + let j = target as i32 + delta; + if j < 0 || (j as usize) >= cands.len() { + continue; + } + tried_any = true; + let cand = cands[j as usize]; + if selected_ptrs.contains(&(cand.pack_index, cand.part)) { + continue; + } + + let pack_path = &pack_paths[cand.pack_index as usize]; + let extracted = match extract_candidate(pack_path.as_path(), cand) { + Ok(v) => v, + Err(_) => continue, + }; + + if extracted.der.len() != cand.size_bytes as usize { + continue; + } + + let sha256 = sha2::Sha256::digest(extracted.der.as_slice()); + let sha256_hex = hex::encode(sha256); + if selected_sha.contains(&sha256_hex) { + continue; + } + + let metrics = match metrics_for(obj_type, extracted.der.as_slice()) { + Ok(m) => m, + Err(_) => continue, + }; + + if !dry_run { + let p = write_sample( + out_dir.as_path(), + obj_type, + label, + extracted.der.as_slice(), + ); + if verbose { + println!( + "- wrote {} -> {}", + label, + p.strip_prefix(PathBuf::from(env!("CARGO_MANIFEST_DIR"))) + .unwrap_or(&p) + .display() + ); + } + } + + let pack_rel_path = pack_path + .strip_prefix(&store_dir) + .unwrap_or(pack_path.as_path()) + .display() + .to_string(); + + let file_rel_path = format!( + "tests/benchmark/selected_der_v2/{}/{}.{}", + match obj_type { + ObjType::Cer => "cer", + ObjType::Crl => "crl", + ObjType::Manifest => "manifest", + ObjType::Roa => "roa", + ObjType::Aspa => "aspa", + }, + label, + match obj_type { + ObjType::Cer => "cer", + ObjType::Crl => "crl", + ObjType::Manifest => "mft", + ObjType::Roa => "roa", + ObjType::Aspa => "asa", + } + ); + + let sha256_hex_for_set = sha256_hex.clone(); + samples.push(SampleMeta { + obj_type, + label: label.to_string(), + file_rel_path, + size_bytes: cand.size_bytes, + sha256_hex, + pack_rel_path, + pack_update_time_rfc3339_utc: fmt_rfc3339_utc( + extracted.pack_update_time_utc, + ), + manifest_uri: extracted.manifest_uri, + object_uri: extracted.object_uri, + manifest_this_update_rfc3339_utc: fmt_rfc3339_utc( + extracted.manifest_this_update_utc, + ), + manifest_not_after_rfc3339_utc: fmt_rfc3339_utc( + extracted.manifest_not_after_utc, + ), + metrics, + }); + + selected_ptrs.insert((cand.pack_index, cand.part)); + selected_sha.insert(sha256_hex_for_set); + found = Some(()); + break; + } + } + if found.is_some() { + break; + } + if !tried_any { + break; + } + } + } + + per_type_out.insert(format!("{:?}", obj_type).to_lowercase(), selected_sha.len() as u32); + println!(); + } + + samples.sort_by(|a, b| a.obj_type.cmp(&b.obj_type).then_with(|| a.label.cmp(&b.label))); + + let created_at_rfc3339_utc = fmt_rfc3339_utc(time::OffsetDateTime::now_utc()); + let manifest = SamplesManifest { + created_at_rfc3339_utc, + store_dir_hint: store_dir.display().to_string(), + per_type: per_type_out, + samples, + }; + + if !dry_run { + let meta_path = out_dir.join("meta").join("samples.json"); + create_parent_dirs(&meta_path); + let bytes = serde_json::to_vec_pretty(&manifest).expect("encode samples manifest"); + std::fs::write(&meta_path, bytes) + .unwrap_or_else(|e| panic!("write {}: {e}", meta_path.display())); + println!("- wrote meta: {}", meta_path.display()); + } +} + +struct ByteCursor<'a> { + bytes: &'a [u8], + pos: usize, +} + +impl<'a> ByteCursor<'a> { + fn new(bytes: &'a [u8]) -> Self { + Self { bytes, pos: 0 } + } + + fn remaining(&self) -> usize { + self.bytes.len().saturating_sub(self.pos) + } + + fn skip(&mut self, len: usize) -> Result<(), String> { + if len > self.remaining() { + return Err("truncated input".to_string()); + } + self.pos += len; + Ok(()) + } + + fn read_exact(&mut self, len: usize) -> Result<&'a [u8], String> { + if len > self.remaining() { + return Err(format!( + "truncated input: need {len} bytes, have {}", + self.remaining() + )); + } + let start = self.pos; + self.pos += len; + Ok(&self.bytes[start..start + len]) + } + + fn read_u8(&mut self) -> Result { + Ok(self.read_exact(1)?[0]) + } + + fn read_u32_be(&mut self) -> Result { + let b = self.read_exact(4)?; + Ok(u32::from_be_bytes([b[0], b[1], b[2], b[3]])) + } + + fn read_u64_be(&mut self) -> Result { + let b = self.read_exact(8)?; + Ok(u64::from_be_bytes([ + b[0], b[1], b[2], b[3], b[4], b[5], b[6], b[7], + ])) + } + + fn peek_u64_as_usize(&mut self) -> Result { + let start = self.pos; + let v = self.read_u64_be()?; + self.pos = start; + usize::try_from(v).map_err(|_| format!("length too large: {v}")) + } + + fn skip_i64(&mut self) -> Result<(), String> { + self.skip(8) + } + + fn read_i64_be(&mut self) -> Result { + let b = self.read_exact(8)?; + Ok(i64::from_be_bytes([ + b[0], b[1], b[2], b[3], b[4], b[5], b[6], b[7], + ])) + } + + fn read_time_utc_i64_be(&mut self) -> Result { + let ts = self.read_i64_be()?; + time::OffsetDateTime::from_unix_timestamp(ts).map_err(|e| { + format!("invalid unix timestamp {ts}: {e}") + }) + } + + fn read_bytes_u32(&mut self) -> Result<&'a [u8], String> { + let len = self.read_u32_be()? as usize; + self.read_exact(len) + } + + fn skip_string_u32(&mut self) -> Result<(), String> { + let len = self.read_u32_be()? as usize; + self.skip(len) + } + + fn read_string_u32(&mut self) -> Result { + let b = self.read_bytes_u32()?; + std::str::from_utf8(b) + .map(|s| s.to_string()) + .map_err(|e| format!("invalid UTF-8 string: {e}")) + } + + fn skip_option_string_u32(&mut self) -> Result<(), String> { + let len = self.read_u32_be()? as usize; + if len == 0 { + return Ok(()); + } + self.skip(len) + } + + fn skip_bytes_u64(&mut self) -> Result<(), String> { + let len_u64 = self.read_u64_be()?; + let len = usize::try_from(len_u64).map_err(|_| { + format!("data block too large for this system: {len_u64}") + })?; + self.skip(len) + } + + fn read_bytes_u64_vec(&mut self) -> Result, String> { + let len_u64 = self.read_u64_be()?; + let len = usize::try_from(len_u64).map_err(|_| { + format!("data block too large for this system: {len_u64}") + })?; + Ok(self.read_exact(len)?.to_vec()) + } +} diff --git a/tests/bench_stage2_decode_profile_selected_der_v2.rs b/tests/bench_stage2_decode_profile_selected_der_v2.rs new file mode 100644 index 0000000..52da3d3 --- /dev/null +++ b/tests/bench_stage2_decode_profile_selected_der_v2.rs @@ -0,0 +1,700 @@ +use rpki::data_model::aspa::AspaObject; +use rpki::data_model::crl::RpkixCrl; +use rpki::data_model::manifest::ManifestObject; +use rpki::data_model::rc::{AsIdentifierChoice, AsResourceSet, IpAddressChoice, IpResourceSet, ResourceCertificate}; +use rpki::data_model::roa::RoaObject; + +use rpki::storage::pack::PackFile; +use rpki::storage::RocksStore; + +use std::path::{Path, PathBuf}; +use std::time::Instant; + +#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)] +enum ObjType { + Cer, + Crl, + Manifest, + Roa, + Aspa, +} + +impl ObjType { + fn as_str(self) -> &'static str { + match self { + ObjType::Cer => "cer", + ObjType::Crl => "crl", + ObjType::Manifest => "manifest", + ObjType::Roa => "roa", + ObjType::Aspa => "aspa", + } + } + + fn ext(self) -> &'static str { + match self { + ObjType::Cer => "cer", + ObjType::Crl => "crl", + ObjType::Manifest => "mft", + ObjType::Roa => "roa", + ObjType::Aspa => "asa", + } + } +} + +#[derive(Clone, Debug)] +struct Sample { + obj_type: ObjType, + name: String, + path: PathBuf, +} + +fn default_samples_dir() -> PathBuf { + PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("tests/benchmark/selected_der_v2") +} + +fn read_samples(root: &Path) -> Vec { + let mut out = Vec::new(); + for obj_type in [ + ObjType::Cer, + ObjType::Crl, + ObjType::Manifest, + ObjType::Roa, + ObjType::Aspa, + ] { + let dir = root.join(obj_type.as_str()); + let rd = match std::fs::read_dir(&dir) { + Ok(rd) => rd, + Err(_) => continue, + }; + for ent in rd.flatten() { + let path = ent.path(); + if path.extension().and_then(|s| s.to_str()) != Some(obj_type.ext()) { + continue; + } + let name = path + .file_stem() + .and_then(|s| s.to_str()) + .unwrap_or("unknown") + .to_string(); + out.push(Sample { obj_type, name, path }); + } + } + out.sort_by(|a, b| a.obj_type.cmp(&b.obj_type).then_with(|| a.name.cmp(&b.name))); + out +} + +fn env_u64(name: &str, default: u64) -> u64 { + std::env::var(name) + .ok() + .and_then(|s| s.parse::().ok()) + .unwrap_or(default) +} + +fn env_u64_opt(name: &str) -> Option { + std::env::var(name).ok().and_then(|s| s.parse::().ok()) +} + +fn env_bool(name: &str) -> bool { + matches!( + std::env::var(name).as_deref(), + Ok("1") | Ok("true") | Ok("TRUE") | Ok("yes") | Ok("YES") + ) +} + +fn env_string(name: &str) -> Option { + std::env::var(name).ok().filter(|s| !s.trim().is_empty()) +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +enum BenchMode { + Both, + DecodeValidate, + Landing, +} + +impl BenchMode { + fn parse(s: &str) -> Result { + match s { + "" | "both" => Ok(Self::Both), + "decode" | "decode_validate" | "decode+validate" => Ok(Self::DecodeValidate), + "landing" => Ok(Self::Landing), + _ => Err(format!( + "invalid BENCH_MODE='{s}', expected one of: both, decode_validate, landing" + )), + } + } + + fn do_decode(self) -> bool { + matches!(self, BenchMode::Both | BenchMode::DecodeValidate) + } + + fn do_landing(self) -> bool { + matches!(self, BenchMode::Both | BenchMode::Landing) + } + + fn as_str(self) -> &'static str { + match self { + BenchMode::Both => "both", + BenchMode::DecodeValidate => "decode_validate", + BenchMode::Landing => "landing", + } + } +} + +fn escape_md(s: &str) -> String { + s.replace('|', "\\|").replace('\n', " ") +} + +fn create_parent_dirs(path: &Path) { + if let Some(parent) = path.parent() { + std::fs::create_dir_all(parent).unwrap_or_else(|e| { + panic!("create_dir_all {}: {e}", parent.display()); + }); + } +} + +fn write_text_file(path: &Path, content: &str) { + create_parent_dirs(path); + std::fs::write(path, content).unwrap_or_else(|e| panic!("write {}: {e}", path.display())); +} + +fn fmt_rfc3339_utc_now() -> String { + use time::format_description::well_known::Rfc3339; + time::OffsetDateTime::now_utc() + .to_offset(time::UtcOffset::UTC) + .format(&Rfc3339) + .unwrap_or_else(|_| "unknown".to_string()) +} + +fn choose_iters_adaptive(mut op: F, min_round_ms: u64, max_iters: u64) -> u64 { + let min_secs = (min_round_ms as f64) / 1e3; + let mut iters: u64 = 1; + loop { + let start = Instant::now(); + for _ in 0..iters { + op(); + } + let elapsed = start.elapsed().as_secs_f64(); + if elapsed >= min_secs { + return iters; + } + if iters >= max_iters { + return iters; + } + iters = (iters.saturating_mul(2)).min(max_iters); + } +} + +fn ip_resource_count(set: &IpResourceSet) -> u64 { + let mut n = 0u64; + for fam in &set.families { + match &fam.choice { + IpAddressChoice::Inherit => n = n.saturating_add(1), + IpAddressChoice::AddressesOrRanges(items) => n = n.saturating_add(items.len() as u64), + } + } + n +} + +fn as_choice_count(choice: &AsIdentifierChoice) -> u64 { + match choice { + AsIdentifierChoice::Inherit => 1, + AsIdentifierChoice::AsIdsOrRanges(items) => items.len() as u64, + } +} + +fn as_resource_count(set: &AsResourceSet) -> u64 { + let mut n = 0u64; + if let Some(c) = set.asnum.as_ref() { + n = n.saturating_add(as_choice_count(c)); + } + if let Some(c) = set.rdi.as_ref() { + n = n.saturating_add(as_choice_count(c)); + } + n +} + +fn complexity_main(obj_type: ObjType, bytes: &[u8]) -> u64 { + match obj_type { + ObjType::Cer => { + let cert = ResourceCertificate::decode_der(bytes).expect("decode cert"); + let ip = cert + .tbs + .extensions + .ip_resources + .as_ref() + .map(ip_resource_count) + .unwrap_or(0); + let asn = cert + .tbs + .extensions + .as_resources + .as_ref() + .map(as_resource_count) + .unwrap_or(0); + ip.saturating_add(asn) + } + ObjType::Crl => { + let crl = RpkixCrl::decode_der(bytes).expect("decode crl"); + crl.revoked_certs.len() as u64 + } + ObjType::Manifest => { + let mft = ManifestObject::decode_der(bytes).expect("decode manifest"); + mft.manifest.file_count() as u64 + } + ObjType::Roa => { + let roa = RoaObject::decode_der(bytes).expect("decode roa"); + let mut n = 0u64; + for fam in &roa.roa.ip_addr_blocks { + n = n.saturating_add(fam.addresses.len() as u64); + } + n + } + ObjType::Aspa => { + let asa = AspaObject::decode_der(bytes).expect("decode aspa"); + asa.aspa.provider_as_ids.len() as u64 + } + } +} + +fn decode_validate(obj_type: ObjType, bytes: &[u8]) { + match obj_type { + ObjType::Cer => { + let decoded = ResourceCertificate::decode_der(std::hint::black_box(bytes)) + .expect("decode cert"); + std::hint::black_box(decoded); + } + ObjType::Crl => { + let decoded = RpkixCrl::decode_der(std::hint::black_box(bytes)).expect("decode crl"); + std::hint::black_box(decoded); + } + ObjType::Manifest => { + let decoded = + ManifestObject::decode_der(std::hint::black_box(bytes)).expect("decode manifest"); + std::hint::black_box(decoded); + } + ObjType::Roa => { + let decoded = RoaObject::decode_der(std::hint::black_box(bytes)).expect("decode roa"); + std::hint::black_box(decoded); + } + ObjType::Aspa => { + let decoded = AspaObject::decode_der(std::hint::black_box(bytes)).expect("decode aspa"); + std::hint::black_box(decoded); + } + } +} + +fn landing_packfile_cbor_put( + store: &RocksStore, + obj_type: ObjType, + sample: &str, + bytes: &[u8], +) { + let rsync_uri = format!("rsync://bench.invalid/{}/{}.{}", obj_type.as_str(), sample, obj_type.ext()); + let pf = PackFile::from_bytes_compute_sha256(rsync_uri, bytes.to_vec()); + let encoded = serde_cbor::to_vec(std::hint::black_box(&pf)).expect("cbor encode packfile"); + let key = format!("bench:packfile:{}:{}", obj_type.as_str(), sample); + store.put_raw(&key, &encoded).expect("store put_raw"); +} + +#[derive(Clone, Debug, serde::Serialize)] +struct RunConfig { + dir: String, + mode: String, + obj_type: Option, + sample: Option, + fixed_iters: Option, + warmup_iters: u64, + rounds: u64, + min_round_ms: u64, + max_adaptive_iters: u64, + timestamp_utc: String, +} + +#[derive(Clone, Debug, serde::Serialize)] +struct ResultRow { + obj_type: String, + sample: String, + size_bytes: usize, + complexity: u64, + avg_ns_per_op: f64, + ops_per_sec: f64, +} + +#[derive(Clone, Debug, serde::Serialize)] +struct BenchmarkOutput { + config: RunConfig, + rows: Vec, +} + +fn render_markdown(title: &str, rows: &[ResultRow]) -> String { + let mut out = String::new(); + out.push_str(&format!("# {title}\n\n")); + out.push_str("| type | sample | size_bytes | complexity | avg ns/op | ops/s |\n"); + out.push_str("|---|---|---:|---:|---:|---:|\n"); + for r in rows { + out.push_str(&format!( + "| {} | {} | {} | {} | {:.2} | {:.2} |\n", + escape_md(&r.obj_type), + escape_md(&r.sample), + r.size_bytes, + r.complexity, + r.avg_ns_per_op, + r.ops_per_sec + )); + } + out +} + +fn render_csv(rows: &[ResultRow]) -> String { + let mut out = String::new(); + out.push_str("type,sample,size_bytes,complexity,avg_ns_per_op,ops_per_sec\n"); + for r in rows { + // sample names are controlled by our fixtures; keep it simple but safe for commas/quotes. + let sample = r.sample.replace('"', "\"\""); + out.push_str(&format!( + "{},{},{},{},{:.6},{:.6}\n", + r.obj_type, + format!("\"{}\"", sample), + r.size_bytes, + r.complexity, + r.avg_ns_per_op, + r.ops_per_sec + )); + } + out +} + +#[test] +#[ignore = "manual performance benchmark; decode+validate and landing for selected_der_v2"] +fn stage2_decode_validate_and_landing_benchmark_selected_der_v2() { + let dir = env_string("BENCH_DIR") + .map(PathBuf::from) + .unwrap_or_else(default_samples_dir); + + let mode = env_string("BENCH_MODE") + .as_deref() + .map(BenchMode::parse) + .transpose() + .unwrap_or_else(|e| panic!("{e}")) + .unwrap_or(BenchMode::Both); + + let type_filter = env_string("BENCH_TYPE"); + let sample_filter = env_string("BENCH_SAMPLE"); + let fixed_iters = env_u64_opt("BENCH_ITERS"); + let warmup_iters = env_u64("BENCH_WARMUP_ITERS", 50); + let rounds = env_u64("BENCH_ROUNDS", 5); + let min_round_ms = env_u64("BENCH_MIN_ROUND_MS", 200); + let max_adaptive_iters = env_u64("BENCH_MAX_ITERS", 1_000_000); + let verbose = env_bool("BENCH_VERBOSE"); + let out_md = env_string("BENCH_OUT_MD").map(PathBuf::from); + let out_json = env_string("BENCH_OUT_JSON").map(PathBuf::from); + let out_csv = env_string("BENCH_OUT_CSV").map(PathBuf::from); + let out_md_landing = env_string("BENCH_OUT_MD_LANDING").map(PathBuf::from); + let out_json_landing = env_string("BENCH_OUT_JSON_LANDING").map(PathBuf::from); + let out_csv_landing = env_string("BENCH_OUT_CSV_LANDING").map(PathBuf::from); + + if let Some(n) = fixed_iters { + assert!(n >= 1, "BENCH_ITERS must be >= 1"); + } + assert!(rounds >= 1, "BENCH_ROUNDS must be >= 1"); + assert!(min_round_ms >= 1, "BENCH_MIN_ROUND_MS must be >= 1"); + assert!(max_adaptive_iters >= 1, "BENCH_MAX_ITERS must be >= 1"); + + let mut samples = read_samples(&dir); + assert!( + !samples.is_empty(), + "no samples found under: {}", + dir.display() + ); + + if let Some(t) = type_filter.as_deref() { + samples.retain(|s| s.obj_type.as_str() == t); + assert!(!samples.is_empty(), "no sample matched BENCH_TYPE={t}"); + } + if let Some(filter) = sample_filter.as_deref() { + samples.retain(|s| s.name == filter); + assert!(!samples.is_empty(), "no sample matched BENCH_SAMPLE={filter}"); + } + + println!("# Stage2 decode+validate benchmark (selected_der_v2)"); + println!(); + println!("- dir: {}", dir.display()); + println!("- mode: {}", mode.as_str()); + if let Some(t) = type_filter.as_deref() { + println!("- type: {}", t); + } + if let Some(s) = sample_filter.as_deref() { + println!("- sample: {}", s); + } + if let Some(n) = fixed_iters { + println!("- iters: {} (fixed)", n); + } else { + println!( + "- warmup: {} iters, rounds: {}, min_round: {}ms (adaptive iters, max {})", + warmup_iters, rounds, min_round_ms, max_adaptive_iters + ); + } + if verbose { + println!("- verbose: true"); + } + if let Some(p) = out_md.as_ref() { + println!("- out_md: {}", p.display()); + } + if let Some(p) = out_json.as_ref() { + println!("- out_json: {}", p.display()); + } + if let Some(p) = out_csv.as_ref() { + println!("- out_csv: {}", p.display()); + } + if let Some(p) = out_md_landing.as_ref() { + println!("- out_md_landing: {}", p.display()); + } + if let Some(p) = out_json_landing.as_ref() { + println!("- out_json_landing: {}", p.display()); + } + if let Some(p) = out_csv_landing.as_ref() { + println!("- out_csv_landing: {}", p.display()); + } + println!(); + + if mode.do_decode() { + println!("## decode+validate"); + println!(); + println!("| type | sample | size_bytes | complexity | avg ns/op | ops/s |"); + println!("|---|---|---:|---:|---:|---:|"); + } + + let mut decode_rows: Vec = Vec::with_capacity(samples.len()); + + let temp = tempfile::tempdir().expect("tempdir"); + let store = RocksStore::open(temp.path()).expect("open rocksdb"); + + let mut landing_rows: Vec = Vec::with_capacity(samples.len()); + + for s in &samples { + let bytes = std::fs::read(&s.path).unwrap_or_else(|e| panic!("read {}: {e}", s.path.display())); + let size_bytes = bytes.len(); + let complexity = if mode.do_decode() { + complexity_main(s.obj_type, bytes.as_slice()) + } else { + 0 + }; + + if mode.do_decode() { + // Warm-up decode+validate. + for _ in 0..warmup_iters { + decode_validate(s.obj_type, bytes.as_slice()); + } + + let mut per_round_ns_per_op = Vec::with_capacity(rounds as usize); + for round in 0..rounds { + let iters = if let Some(n) = fixed_iters { + n + } else { + choose_iters_adaptive( + || decode_validate(s.obj_type, bytes.as_slice()), + min_round_ms, + max_adaptive_iters, + ) + }; + + let start = Instant::now(); + for _ in 0..iters { + decode_validate(s.obj_type, bytes.as_slice()); + } + let elapsed = start.elapsed(); + let total_ns = elapsed.as_secs_f64() * 1e9; + let ns_per_op = total_ns / (iters as f64); + per_round_ns_per_op.push(ns_per_op); + + if verbose { + println!( + "# {}.{} round {}: iters={} total_ms={:.2} ns/op={:.2}", + s.obj_type.as_str(), + s.name, + round + 1, + iters, + elapsed.as_secs_f64() * 1e3, + ns_per_op + ); + } + } + + let avg_ns = + per_round_ns_per_op.iter().sum::() / (per_round_ns_per_op.len() as f64); + let ops_per_sec = 1e9_f64 / avg_ns; + + println!( + "| {} | {} | {} | {} | {:.2} | {:.2} |", + s.obj_type.as_str(), + s.name, + size_bytes, + complexity, + avg_ns, + ops_per_sec + ); + + decode_rows.push(ResultRow { + obj_type: s.obj_type.as_str().to_string(), + sample: s.name.clone(), + size_bytes, + complexity, + avg_ns_per_op: avg_ns, + ops_per_sec, + }); + } + + if mode.do_landing() { + // Landing benchmark: PackFile(from_bytes_compute_sha256) + CBOR + RocksDB put_raw. + let mut per_round_ns_per_op = Vec::with_capacity(rounds as usize); + for _ in 0..warmup_iters { + landing_packfile_cbor_put(&store, s.obj_type, &s.name, bytes.as_slice()); + } + for round in 0..rounds { + let iters = if let Some(n) = fixed_iters { + n + } else { + choose_iters_adaptive( + || landing_packfile_cbor_put(&store, s.obj_type, &s.name, bytes.as_slice()), + min_round_ms, + max_adaptive_iters, + ) + }; + + let start = Instant::now(); + for _ in 0..iters { + landing_packfile_cbor_put(&store, s.obj_type, &s.name, bytes.as_slice()); + } + let elapsed = start.elapsed(); + let total_ns = elapsed.as_secs_f64() * 1e9; + let ns_per_op = total_ns / (iters as f64); + per_round_ns_per_op.push(ns_per_op); + + if verbose { + println!( + "# landing {}.{} round {}: iters={} total_ms={:.2} ns/op={:.2}", + s.obj_type.as_str(), + s.name, + round + 1, + iters, + elapsed.as_secs_f64() * 1e3, + ns_per_op + ); + } + } + + let avg_ns = + per_round_ns_per_op.iter().sum::() / (per_round_ns_per_op.len() as f64); + let ops_per_sec = 1e9_f64 / avg_ns; + + landing_rows.push(ResultRow { + obj_type: s.obj_type.as_str().to_string(), + sample: s.name.clone(), + size_bytes, + complexity, + avg_ns_per_op: avg_ns, + ops_per_sec, + }); + } + } + + if mode.do_decode() { + println!(); + } + if mode.do_landing() { + println!("## landing (PackFile::from_bytes_compute_sha256 + CBOR + RocksDB put_raw)"); + println!(); + println!("| type | sample | size_bytes | complexity | avg ns/op | ops/s |"); + println!("|---|---|---:|---:|---:|---:|"); + for r in &landing_rows { + println!( + "| {} | {} | {} | {} | {:.2} | {:.2} |", + escape_md(&r.obj_type), + escape_md(&r.sample), + r.size_bytes, + r.complexity, + r.avg_ns_per_op, + r.ops_per_sec + ); + } + println!(); + } + + if mode.do_decode() && (out_md.is_some() || out_json.is_some() || out_csv.is_some()) { + let timestamp_utc = fmt_rfc3339_utc_now(); + let cfg = RunConfig { + dir: dir.display().to_string(), + mode: "decode_validate".to_string(), + obj_type: type_filter.clone(), + sample: sample_filter.clone(), + fixed_iters, + warmup_iters, + rounds, + min_round_ms, + max_adaptive_iters, + timestamp_utc, + }; + + if let Some(path) = out_md { + let md = render_markdown("Stage2 decode+validate benchmark (selected_der_v2)", &decode_rows); + write_text_file(&path, &md); + eprintln!("Wrote {}", path.display()); + } + if let Some(path) = out_json { + let json = serde_json::to_string_pretty(&BenchmarkOutput { + config: cfg, + rows: decode_rows.clone(), + }) + .expect("serialize json"); + write_text_file(&path, &json); + eprintln!("Wrote {}", path.display()); + } + if let Some(path) = out_csv { + let csv = render_csv(&decode_rows); + write_text_file(&path, &csv); + eprintln!("Wrote {}", path.display()); + } + } + + if mode.do_landing() + && (out_md_landing.is_some() || out_json_landing.is_some() || out_csv_landing.is_some()) + { + let timestamp_utc = fmt_rfc3339_utc_now(); + let cfg = RunConfig { + dir: dir.display().to_string(), + mode: "landing_packfile_cbor_put".to_string(), + obj_type: type_filter, + sample: sample_filter, + fixed_iters, + warmup_iters, + rounds, + min_round_ms, + max_adaptive_iters, + timestamp_utc, + }; + + if let Some(path) = out_md_landing { + let md = render_markdown( + "Stage2 landing benchmark (PackFile CBOR + RocksDB put_raw)", + &landing_rows, + ); + write_text_file(&path, &md); + eprintln!("Wrote {}", path.display()); + } + if let Some(path) = out_json_landing { + let json = serde_json::to_string_pretty(&BenchmarkOutput { + config: cfg, + rows: landing_rows.clone(), + }) + .expect("serialize json"); + write_text_file(&path, &json); + eprintln!("Wrote {}", path.display()); + } + if let Some(path) = out_csv_landing { + let csv = render_csv(&landing_rows); + write_text_file(&path, &csv); + eprintln!("Wrote {}", path.display()); + } + } +} diff --git a/tests/bench_stage2_inventory_sap.rs b/tests/bench_stage2_inventory_sap.rs new file mode 100644 index 0000000..07cb2d4 --- /dev/null +++ b/tests/bench_stage2_inventory_sap.rs @@ -0,0 +1,235 @@ +#[path = "benchmark/sap.rs"] +mod sap; + +use serde::Serialize; +use std::collections::BTreeMap; +use std::path::{Path, PathBuf}; + +#[derive(Clone, Debug, Default)] +struct Sizes { + vals: Vec, + total: u64, +} + +impl Sizes { + fn push(&mut self, v: u64) { + self.vals.push(v); + self.total = self.total.saturating_add(v); + } + + fn summarize(&mut self) -> Option { + if self.vals.is_empty() { + return None; + } + self.vals.sort_unstable(); + let min = *self.vals.first().unwrap(); + let max = *self.vals.last().unwrap(); + let p50 = percentile(&self.vals, 0.50); + let p90 = percentile(&self.vals, 0.90); + let p99 = percentile(&self.vals, 0.99); + Some(SizesSummary { + count: self.vals.len() as u64, + total_bytes: self.total, + min, + p50, + p90, + p99, + max, + }) + } +} + +#[derive(Clone, Debug, Serialize)] +struct SizesSummary { + count: u64, + total_bytes: u64, + min: u64, + p50: u64, + p90: u64, + p99: u64, + max: u64, +} + +#[derive(Clone, Debug, Serialize)] +struct InventoryReport { + store_dir: String, + pack_files_found: u64, + packs_success: u64, + packs_last_attempt: u64, + packs_decode_error: u64, + objects_total: u64, + by_type: BTreeMap, +} + +fn percentile(sorted: &[u64], p: f64) -> u64 { + if sorted.is_empty() { + return 0; + } + if sorted.len() == 1 { + return sorted[0]; + } + let p = p.clamp(0.0, 1.0); + let idx = ((sorted.len() - 1) as f64 * p).round() as usize; + sorted[idx] +} + +fn env_string(name: &str) -> Option { + std::env::var(name).ok().filter(|s| !s.trim().is_empty()) +} + +fn env_u64_opt(name: &str) -> Option { + std::env::var(name).ok().and_then(|s| s.parse::().ok()) +} + +fn create_parent_dirs(path: &Path) { + if let Some(parent) = path.parent() { + std::fs::create_dir_all(parent).unwrap_or_else(|e| { + panic!("create_dir_all {}: {e}", parent.display()); + }); + } +} + +fn walk_collect_files(root: &Path, out: &mut Vec) -> std::io::Result<()> { + for entry in std::fs::read_dir(root)? { + let entry = entry?; + let path = entry.path(); + let meta = entry.metadata()?; + if meta.is_dir() { + walk_collect_files(&path, out)?; + continue; + } + if !meta.is_file() { + continue; + } + out.push(path); + } + Ok(()) +} + +fn object_type_from_rsync_uri(uri: &str) -> String { + let file = uri.rsplit('/').next().unwrap_or(uri); + if let Some((_, ext)) = file.rsplit_once('.') { + return ext.to_ascii_lowercase(); + } + "unknown".to_string() +} + +#[test] +#[ignore = "manual inventory of routinator stored publication point packs (SAP)"] +fn stage2_inventory_routinator_sap_store() { + let store_dir = env_string("BENCH_STORE_DIR") + .map(PathBuf::from) + .unwrap_or_else(|| { + panic!( + "set BENCH_STORE_DIR to routinator stored/ dir, e.g. /home/yuyr/dev/rust_playground/routinator/bench/full/cache/stored" + ) + }); + let max_packs = env_u64_opt("BENCH_MAX_PACKS"); + let out_json = env_string("BENCH_OUT_JSON").map(PathBuf::from); + + let mut pack_paths = Vec::new(); + walk_collect_files(&store_dir, &mut pack_paths) + .unwrap_or_else(|e| panic!("walk {}: {e}", store_dir.display())); + pack_paths.retain(|p| p.extension().and_then(|s| s.to_str()) == Some("mft")); + pack_paths.sort(); + + if let Some(max) = max_packs { + pack_paths.truncate(max as usize); + } + + println!("# Stage2 inventory: routinator SAP packs"); + println!(); + println!("- store_dir: {}", store_dir.display()); + println!("- pack_files_found: {}", pack_paths.len()); + if let Some(max) = max_packs { + println!("- max_packs: {} (truncating input list)", max); + } + if let Some(p) = out_json.as_ref() { + println!("- out_json: {}", p.display()); + } + println!(); + + let mut by_type: BTreeMap = BTreeMap::new(); + let mut packs_success = 0u64; + let mut packs_last_attempt = 0u64; + let mut packs_decode_error = 0u64; + let mut objects_total = 0u64; + + for p in &pack_paths { + let sap = match sap::SapPublicationPoint::decode_path(p.as_path()) { + Ok(v) => v, + Err(_e) => { + packs_decode_error += 1; + continue; + } + }; + + match sap.header.update_status { + sap::SapUpdateStatus::LastAttempt { .. } => { + packs_last_attempt += 1; + continue; + } + sap::SapUpdateStatus::Success { .. } => { + packs_success += 1; + } + } + + let Some(m) = sap.manifest.as_ref() else { + packs_decode_error += 1; + continue; + }; + + by_type + .entry("mft".to_string()) + .or_default() + .push(m.manifest_der.len() as u64); + by_type + .entry("crl".to_string()) + .or_default() + .push(m.crl_der.len() as u64); + objects_total += 2; + + for o in &sap.objects { + let typ = object_type_from_rsync_uri(&o.uri); + by_type + .entry(typ) + .or_default() + .push(o.content_der.len() as u64); + objects_total += 1; + } + } + + let mut summaries: BTreeMap = BTreeMap::new(); + for (typ, mut sizes) in by_type { + if let Some(summary) = sizes.summarize() { + summaries.insert(typ, summary); + } + } + + let report = InventoryReport { + store_dir: store_dir.display().to_string(), + pack_files_found: pack_paths.len() as u64, + packs_success, + packs_last_attempt, + packs_decode_error, + objects_total, + by_type: summaries.clone(), + }; + + println!("| type | count | total_bytes | p50 | p90 | p99 | max |"); + println!("|---|---:|---:|---:|---:|---:|---:|"); + for (typ, s) in &summaries { + println!( + "| {} | {} | {} | {} | {} | {} | {} |", + typ, s.count, s.total_bytes, s.p50, s.p90, s.p99, s.max + ); + } + + if let Some(path) = out_json.as_ref() { + create_parent_dirs(path); + let bytes = serde_json::to_vec_pretty(&report).expect("encode json"); + std::fs::write(path, bytes) + .unwrap_or_else(|e| panic!("write {}: {e}", path.display())); + } +} + diff --git a/tests/benchmark/profile_stage2_m5_hotspots.sh b/tests/benchmark/profile_stage2_m5_hotspots.sh new file mode 100755 index 0000000..45bd2eb --- /dev/null +++ b/tests/benchmark/profile_stage2_m5_hotspots.sh @@ -0,0 +1,191 @@ +#!/usr/bin/env bash +set -euo pipefail + +# M5: Generate perf profiles + flamegraphs + hotspots for Stage2 objects. +# Default target is ROA max sample since it is currently the largest gap in +# ours-vs-routinator decode comparison. +# +# Outputs (under rpki/target/bench): +# - perf_stage2_ours___decode.data +# - flamegraph_stage2_ours___decode_release.svg +# - hotspots_stage2_ours___decode_release.txt +# - perf_stage2_ours___landing.data +# - flamegraph_stage2_ours___landing_release.svg +# - hotspots_stage2_ours___landing_release.txt +# - perf_stage2_routinator___decode.data +# - flamegraph_stage2_routinator___decode_release.svg +# - hotspots_stage2_routinator___decode_release.txt + +ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" +OUT_DIR="$ROOT_DIR/target/bench" +mkdir -p "$OUT_DIR" + +TYPE="${BENCH_TYPE:-roa}" # cer|crl|roa +SAMPLE="${BENCH_SAMPLE:-max}" # e.g. max|p99|p50 + +ITERS="${BENCH_ITERS:-2000}" +WARMUP="${BENCH_WARMUP_ITERS:-50}" +FREQ="${FLAMEGRAPH_FREQ:-99}" + +PROFILE_LANDING="${PROFILE_LANDING:-1}" # 0|1 + +if ! command -v perf >/dev/null 2>&1; then + echo "ERROR: perf not found." >&2 + exit 2 +fi +if ! command -v flamegraph >/dev/null 2>&1; then + echo "ERROR: flamegraph not found. Install with: cargo install flamegraph" >&2 + exit 2 +fi + +# On WSL2, /usr/bin/perf is often a wrapper that errors because there is no +# kernel-matched perf binary. In that case, prefer a real perf binary under +# /usr/lib/linux-tools/*/perf by putting a shim earlier in PATH. +PERF_WRAPPER_OK=1 +PERF_VERSION_OUT="$(perf --version 2>&1 || true)" +if echo "${PERF_VERSION_OUT}" | grep -q "WARNING: perf not found for kernel"; then + PERF_WRAPPER_OK=0 +fi + +if [[ "${PERF_WRAPPER_OK}" == "0" ]]; then + PERF_REAL="$(ls -1 /usr/lib/linux-tools/*/perf 2>/dev/null | head -n 1 || true)" + if [[ -z "${PERF_REAL}" ]]; then + echo "ERROR: perf wrapper found, but no real perf binary under /usr/lib/linux-tools/*/perf" >&2 + exit 2 + fi + + SHIM_DIR="$OUT_DIR/tools" + mkdir -p "${SHIM_DIR}" + cat > "${SHIM_DIR}/perf" < ${PERF_REAL}" >&2 +fi + +echo "type=${TYPE} sample=${SAMPLE} iters=${ITERS} warmup=${WARMUP} freq=${FREQ}" >&2 + +build_ours_test_bin() { + (cd "$ROOT_DIR" && cargo test --release --test bench_stage2_decode_profile_selected_der_v2 --no-run -q) + local bin + bin="$(find "$ROOT_DIR/target/release/deps" -maxdepth 1 -type f -executable -name 'bench_stage2_decode_profile_selected_der_v2-*' | head -n 1 || true)" + if [[ -z "${bin}" ]]; then + echo "ERROR: failed to locate ours test binary under target/release/deps" >&2 + exit 2 + fi + echo "${bin}" +} + +build_routinator_bin() { + (cd "$ROOT_DIR/benchmark/routinator_object_bench" && cargo build --release -q) + local bin="$ROOT_DIR/benchmark/routinator_object_bench/target/release/routinator-object-bench" + if [[ ! -x "${bin}" ]]; then + echo "ERROR: failed to locate routinator-object-bench binary: ${bin}" >&2 + exit 2 + fi + echo "${bin}" +} + +run_perf_profile() { + local perfdata="$1" + shift + echo "[perf record] $perfdata" >&2 + perf record -o "$perfdata" -F "$FREQ" --call-graph dwarf -g -- "$@" +} + +gen_flamegraph() { + local perfdata="$1" + local out_svg="$2" + local title="$3" + local subtitle="$4" + echo "[flamegraph] $out_svg" >&2 + flamegraph --perfdata "$perfdata" --output "$out_svg" --title "$title" --subtitle "$subtitle" >/dev/null +} + +gen_hotspots() { + local perfdata="$1" + local out_txt="$2" + echo "[perf report] $out_txt" >&2 + # Keep it deterministic enough for diffing; avoid call-graph children view. + perf report --stdio -i "$perfdata" --no-children --sort overhead,symbol,dso > "$out_txt" || true +} + +OURS_BIN="$(build_ours_test_bin)" +ROUT_BIN="$(build_routinator_bin)" + +# OURS decode+validate +OURS_DECODE_PERF="$OUT_DIR/perf_stage2_ours_${TYPE}_${SAMPLE}_decode.data" +OURS_DECODE_SVG="$OUT_DIR/flamegraph_stage2_ours_${TYPE}_${SAMPLE}_decode_release.svg" +OURS_DECODE_HOT="$OUT_DIR/hotspots_stage2_ours_${TYPE}_${SAMPLE}_decode_release.txt" + +run_perf_profile "$OURS_DECODE_PERF" \ + env \ + BENCH_MODE=decode_validate \ + BENCH_TYPE="$TYPE" \ + BENCH_SAMPLE="$SAMPLE" \ + BENCH_ITERS="$ITERS" \ + BENCH_ROUNDS=1 \ + BENCH_WARMUP_ITERS="$WARMUP" \ + "$OURS_BIN" --ignored --nocapture >/dev/null + +gen_flamegraph "$OURS_DECODE_PERF" "$OURS_DECODE_SVG" \ + "stage2 ours decode_validate ${TYPE}.${SAMPLE}" "iters=${ITERS} warmup=${WARMUP} freq=${FREQ}" +gen_hotspots "$OURS_DECODE_PERF" "$OURS_DECODE_HOT" + +# OURS landing +if [[ "${PROFILE_LANDING}" == "1" ]]; then + OURS_LAND_PERF="$OUT_DIR/perf_stage2_ours_${TYPE}_${SAMPLE}_landing.data" + OURS_LAND_SVG="$OUT_DIR/flamegraph_stage2_ours_${TYPE}_${SAMPLE}_landing_release.svg" + OURS_LAND_HOT="$OUT_DIR/hotspots_stage2_ours_${TYPE}_${SAMPLE}_landing_release.txt" + + run_perf_profile "$OURS_LAND_PERF" \ + env \ + BENCH_MODE=landing \ + BENCH_TYPE="$TYPE" \ + BENCH_SAMPLE="$SAMPLE" \ + BENCH_ITERS="$ITERS" \ + BENCH_ROUNDS=1 \ + BENCH_WARMUP_ITERS="$WARMUP" \ + "$OURS_BIN" --ignored --nocapture >/dev/null + + gen_flamegraph "$OURS_LAND_PERF" "$OURS_LAND_SVG" \ + "stage2 ours landing ${TYPE}.${SAMPLE}" "iters=${ITERS} warmup=${WARMUP} freq=${FREQ}" + gen_hotspots "$OURS_LAND_PERF" "$OURS_LAND_HOT" +fi + +# Routinator baseline decode +ROUT_DECODE_PERF="$OUT_DIR/perf_stage2_routinator_${TYPE}_${SAMPLE}_decode.data" +ROUT_DECODE_SVG="$OUT_DIR/flamegraph_stage2_routinator_${TYPE}_${SAMPLE}_decode_release.svg" +ROUT_DECODE_HOT="$OUT_DIR/hotspots_stage2_routinator_${TYPE}_${SAMPLE}_decode_release.txt" + +run_perf_profile "$ROUT_DECODE_PERF" \ + "$ROUT_BIN" \ + --dir "$ROOT_DIR/tests/benchmark/selected_der_v2" \ + --type "$TYPE" \ + --sample "$SAMPLE" \ + --iters "$ITERS" \ + --rounds 1 \ + --warmup-iters "$WARMUP" \ + --min-round-ms 1 \ + --max-iters 1000000 \ + --strict true \ + --out-csv "$OUT_DIR/_tmp_routinator_profile.csv" \ + --out-md "$OUT_DIR/_tmp_routinator_profile.md" \ + >/dev/null + +gen_flamegraph "$ROUT_DECODE_PERF" "$ROUT_DECODE_SVG" \ + "stage2 routinator decode ${TYPE}.${SAMPLE}" "iters=${ITERS} warmup=${WARMUP} freq=${FREQ}" +gen_hotspots "$ROUT_DECODE_PERF" "$ROUT_DECODE_HOT" + +echo "Done." >&2 +echo "- ours decode flamegraph: $OURS_DECODE_SVG" >&2 +echo "- ours decode hotspots: $OURS_DECODE_HOT" >&2 +if [[ "${PROFILE_LANDING}" == "1" ]]; then + echo "- ours landing flamegraph: $OURS_LAND_SVG" >&2 + echo "- ours landing hotspots: $OURS_LAND_HOT" >&2 +fi +echo "- rout decode flamegraph: $ROUT_DECODE_SVG" >&2 +echo "- rout decode hotspots: $ROUT_DECODE_HOT" >&2 + diff --git a/tests/benchmark/sap.rs b/tests/benchmark/sap.rs new file mode 100644 index 0000000..9ac55ce --- /dev/null +++ b/tests/benchmark/sap.rs @@ -0,0 +1,255 @@ +use std::path::Path; + +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct SapPublicationPoint { + pub header: SapPointHeader, + pub manifest: Option, + pub objects: Vec, +} + +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct SapPointHeader { + pub version: u8, + pub manifest_uri: String, + pub rpki_notify: Option, + pub update_status: SapUpdateStatus, +} + +#[derive(Clone, Debug, PartialEq, Eq)] +pub enum SapUpdateStatus { + Success { time_utc: time::OffsetDateTime }, + LastAttempt { time_utc: time::OffsetDateTime }, +} + +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct SapStoredManifest { + pub not_after_utc: time::OffsetDateTime, + pub manifest_number: [u8; 20], + pub this_update_utc: time::OffsetDateTime, + pub ca_repository: String, + pub manifest_der: Vec, + pub crl_uri: String, + pub crl_der: Vec, +} + +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct SapStoredObject { + pub uri: String, + pub hash_sha256: Option<[u8; 32]>, + pub content_der: Vec, +} + +#[derive(Debug, thiserror::Error)] +pub enum SapDecodeError { + #[error("I/O error reading {path}: {err}")] + Io { path: String, err: String }, + + #[error("SAP decode error: {0}")] + Decode(String), +} + +impl SapPublicationPoint { + pub fn decode_path(path: &Path) -> Result { + let bytes = std::fs::read(path).map_err(|e| SapDecodeError::Io { + path: path.display().to_string(), + err: e.to_string(), + })?; + Self::decode_bytes(&bytes) + } + + pub fn decode_bytes(bytes: &[u8]) -> Result { + let mut cur = Cursor::new(bytes); + + let version = cur.read_u8()?; + let manifest_uri = cur.read_string_u32()?; + let rpki_notify = cur.read_option_string_u32()?; + + let update_kind = cur.read_u8()?; + let update_time_utc = cur.read_time_utc_i64_be()?; + let update_status = match update_kind { + 0 => SapUpdateStatus::Success { + time_utc: update_time_utc, + }, + 1 => SapUpdateStatus::LastAttempt { + time_utc: update_time_utc, + }, + _ => { + return Err(SapDecodeError::Decode(format!( + "invalid update_status kind: {update_kind}" + ))); + } + }; + + let header = SapPointHeader { + version, + manifest_uri, + rpki_notify, + update_status: update_status.clone(), + }; + + match update_status { + SapUpdateStatus::LastAttempt { .. } => Ok(SapPublicationPoint { + header, + manifest: None, + objects: Vec::new(), + }), + SapUpdateStatus::Success { .. } => { + let not_after_utc = cur.read_time_utc_i64_be()?; + let manifest_number = cur.read_array_20()?; + let this_update_utc = cur.read_time_utc_i64_be()?; + let ca_repository = cur.read_string_u32()?; + let manifest_der = cur.read_bytes_u64_vec()?; + let crl_uri = cur.read_string_u32()?; + let crl_der = cur.read_bytes_u64_vec()?; + + let manifest = SapStoredManifest { + not_after_utc, + manifest_number, + this_update_utc, + ca_repository, + manifest_der, + crl_uri, + crl_der, + }; + + let mut objects = Vec::new(); + while cur.remaining() > 0 { + let uri = match cur.try_read_string_u32() { + Ok(Some(v)) => v, + Ok(None) => break, + Err(e) => return Err(e), + }; + + let hash_type = cur.read_u8()?; + let hash_sha256 = match hash_type { + 0 => None, + 1 => Some(cur.read_array_32()?), + other => { + return Err(SapDecodeError::Decode(format!( + "unsupported hash_type {other} for stored object {uri}" + ))); + } + }; + let content_der = cur.read_bytes_u64_vec()?; + objects.push(SapStoredObject { + uri, + hash_sha256, + content_der, + }); + } + + Ok(SapPublicationPoint { + header, + manifest: Some(manifest), + objects, + }) + } + } + } +} + +struct Cursor<'a> { + bytes: &'a [u8], + pos: usize, +} + +impl<'a> Cursor<'a> { + fn new(bytes: &'a [u8]) -> Self { + Self { bytes, pos: 0 } + } + + fn remaining(&self) -> usize { + self.bytes.len().saturating_sub(self.pos) + } + + fn read_exact(&mut self, len: usize) -> Result<&'a [u8], SapDecodeError> { + if len > self.remaining() { + return Err(SapDecodeError::Decode(format!( + "truncated input: need {len} bytes, have {}", + self.remaining() + ))); + } + let start = self.pos; + self.pos += len; + Ok(&self.bytes[start..start + len]) + } + + fn read_u8(&mut self) -> Result { + Ok(self.read_exact(1)?[0]) + } + + fn read_u32_be(&mut self) -> Result { + let b = self.read_exact(4)?; + Ok(u32::from_be_bytes([b[0], b[1], b[2], b[3]])) + } + + fn read_u64_be(&mut self) -> Result { + let b = self.read_exact(8)?; + Ok(u64::from_be_bytes([ + b[0], b[1], b[2], b[3], b[4], b[5], b[6], b[7], + ])) + } + + fn read_i64_be(&mut self) -> Result { + let b = self.read_exact(8)?; + Ok(i64::from_be_bytes([ + b[0], b[1], b[2], b[3], b[4], b[5], b[6], b[7], + ])) + } + + fn read_time_utc_i64_be(&mut self) -> Result { + let ts = self.read_i64_be()?; + time::OffsetDateTime::from_unix_timestamp(ts).map_err(|e| { + SapDecodeError::Decode(format!("invalid unix timestamp {ts}: {e}")) + }) + } + + fn read_string_u32(&mut self) -> Result { + let len = self.read_u32_be()? as usize; + let b = self.read_exact(len)?; + std::str::from_utf8(b) + .map(|s| s.to_string()) + .map_err(|e| SapDecodeError::Decode(format!("invalid UTF-8 string: {e}"))) + } + + fn try_read_string_u32(&mut self) -> Result, SapDecodeError> { + if self.remaining() == 0 { + return Ok(None); + } + Ok(Some(self.read_string_u32()?)) + } + + fn read_option_string_u32(&mut self) -> Result, SapDecodeError> { + let len = self.read_u32_be()? as usize; + if len == 0 { + return Ok(None); + } + let b = self.read_exact(len)?; + let s = std::str::from_utf8(b) + .map_err(|e| SapDecodeError::Decode(format!("invalid UTF-8 string: {e}")))?; + Ok(Some(s.to_string())) + } + + fn read_bytes_u64_vec(&mut self) -> Result, SapDecodeError> { + let len_u64 = self.read_u64_be()?; + let len = usize::try_from(len_u64).map_err(|_| { + SapDecodeError::Decode(format!("data block too large for this system: {len_u64}")) + })?; + Ok(self.read_exact(len)?.to_vec()) + } + + fn read_array_20(&mut self) -> Result<[u8; 20], SapDecodeError> { + let b = self.read_exact(20)?; + let mut out = [0u8; 20]; + out.copy_from_slice(b); + Ok(out) + } + + fn read_array_32(&mut self) -> Result<[u8; 32], SapDecodeError> { + let b = self.read_exact(32)?; + let mut out = [0u8; 32]; + out.copy_from_slice(b); + Ok(out) + } +} + diff --git a/tests/benchmark/selected_der_v2/aspa/max.asa b/tests/benchmark/selected_der_v2/aspa/max.asa new file mode 100644 index 0000000..8f4713f Binary files /dev/null and b/tests/benchmark/selected_der_v2/aspa/max.asa differ diff --git a/tests/benchmark/selected_der_v2/aspa/min.asa b/tests/benchmark/selected_der_v2/aspa/min.asa new file mode 100644 index 0000000..e4f34c9 Binary files /dev/null and b/tests/benchmark/selected_der_v2/aspa/min.asa differ diff --git a/tests/benchmark/selected_der_v2/aspa/p01.asa b/tests/benchmark/selected_der_v2/aspa/p01.asa new file mode 100644 index 0000000..079cf3b Binary files /dev/null and b/tests/benchmark/selected_der_v2/aspa/p01.asa differ diff --git a/tests/benchmark/selected_der_v2/aspa/p10.asa b/tests/benchmark/selected_der_v2/aspa/p10.asa new file mode 100644 index 0000000..74a5bfb Binary files /dev/null and b/tests/benchmark/selected_der_v2/aspa/p10.asa differ diff --git a/tests/benchmark/selected_der_v2/aspa/p25.asa b/tests/benchmark/selected_der_v2/aspa/p25.asa new file mode 100644 index 0000000..306299c Binary files /dev/null and b/tests/benchmark/selected_der_v2/aspa/p25.asa differ diff --git a/tests/benchmark/selected_der_v2/aspa/p50.asa b/tests/benchmark/selected_der_v2/aspa/p50.asa new file mode 100644 index 0000000..5466c1a Binary files /dev/null and b/tests/benchmark/selected_der_v2/aspa/p50.asa differ diff --git a/tests/benchmark/selected_der_v2/aspa/p75.asa b/tests/benchmark/selected_der_v2/aspa/p75.asa new file mode 100644 index 0000000..4c8c96f Binary files /dev/null and b/tests/benchmark/selected_der_v2/aspa/p75.asa differ diff --git a/tests/benchmark/selected_der_v2/aspa/p90.asa b/tests/benchmark/selected_der_v2/aspa/p90.asa new file mode 100644 index 0000000..9976438 Binary files /dev/null and b/tests/benchmark/selected_der_v2/aspa/p90.asa differ diff --git a/tests/benchmark/selected_der_v2/aspa/p95.asa b/tests/benchmark/selected_der_v2/aspa/p95.asa new file mode 100644 index 0000000..8e6ef4b Binary files /dev/null and b/tests/benchmark/selected_der_v2/aspa/p95.asa differ diff --git a/tests/benchmark/selected_der_v2/aspa/p99.asa b/tests/benchmark/selected_der_v2/aspa/p99.asa new file mode 100644 index 0000000..2db3838 Binary files /dev/null and b/tests/benchmark/selected_der_v2/aspa/p99.asa differ diff --git a/tests/benchmark/selected_der_v2/cer/max.cer b/tests/benchmark/selected_der_v2/cer/max.cer new file mode 100644 index 0000000..7e7fc1b Binary files /dev/null and b/tests/benchmark/selected_der_v2/cer/max.cer differ diff --git a/tests/benchmark/selected_der_v2/cer/min.cer b/tests/benchmark/selected_der_v2/cer/min.cer new file mode 100644 index 0000000..1b1b1cd Binary files /dev/null and b/tests/benchmark/selected_der_v2/cer/min.cer differ diff --git a/tests/benchmark/selected_der_v2/cer/p01.cer b/tests/benchmark/selected_der_v2/cer/p01.cer new file mode 100644 index 0000000..aa369fb Binary files /dev/null and b/tests/benchmark/selected_der_v2/cer/p01.cer differ diff --git a/tests/benchmark/selected_der_v2/cer/p10.cer b/tests/benchmark/selected_der_v2/cer/p10.cer new file mode 100644 index 0000000..0f8a9ff Binary files /dev/null and b/tests/benchmark/selected_der_v2/cer/p10.cer differ diff --git a/tests/benchmark/selected_der_v2/cer/p25.cer b/tests/benchmark/selected_der_v2/cer/p25.cer new file mode 100644 index 0000000..871d524 Binary files /dev/null and b/tests/benchmark/selected_der_v2/cer/p25.cer differ diff --git a/tests/benchmark/selected_der_v2/cer/p50.cer b/tests/benchmark/selected_der_v2/cer/p50.cer new file mode 100644 index 0000000..ce3a833 Binary files /dev/null and b/tests/benchmark/selected_der_v2/cer/p50.cer differ diff --git a/tests/benchmark/selected_der_v2/cer/p75.cer b/tests/benchmark/selected_der_v2/cer/p75.cer new file mode 100644 index 0000000..f1143e0 Binary files /dev/null and b/tests/benchmark/selected_der_v2/cer/p75.cer differ diff --git a/tests/benchmark/selected_der_v2/cer/p90.cer b/tests/benchmark/selected_der_v2/cer/p90.cer new file mode 100644 index 0000000..8069b9c Binary files /dev/null and b/tests/benchmark/selected_der_v2/cer/p90.cer differ diff --git a/tests/benchmark/selected_der_v2/cer/p95.cer b/tests/benchmark/selected_der_v2/cer/p95.cer new file mode 100644 index 0000000..8eb4b1e Binary files /dev/null and b/tests/benchmark/selected_der_v2/cer/p95.cer differ diff --git a/tests/benchmark/selected_der_v2/cer/p99.cer b/tests/benchmark/selected_der_v2/cer/p99.cer new file mode 100644 index 0000000..338489b Binary files /dev/null and b/tests/benchmark/selected_der_v2/cer/p99.cer differ diff --git a/tests/benchmark/selected_der_v2/crl/max.crl b/tests/benchmark/selected_der_v2/crl/max.crl new file mode 100644 index 0000000..d29886d Binary files /dev/null and b/tests/benchmark/selected_der_v2/crl/max.crl differ diff --git a/tests/benchmark/selected_der_v2/crl/min.crl b/tests/benchmark/selected_der_v2/crl/min.crl new file mode 100644 index 0000000..79f516a Binary files /dev/null and b/tests/benchmark/selected_der_v2/crl/min.crl differ diff --git a/tests/benchmark/selected_der_v2/crl/p01.crl b/tests/benchmark/selected_der_v2/crl/p01.crl new file mode 100644 index 0000000..d1c35af Binary files /dev/null and b/tests/benchmark/selected_der_v2/crl/p01.crl differ diff --git a/tests/benchmark/selected_der_v2/crl/p10.crl b/tests/benchmark/selected_der_v2/crl/p10.crl new file mode 100644 index 0000000..ed4cecf Binary files /dev/null and b/tests/benchmark/selected_der_v2/crl/p10.crl differ diff --git a/tests/benchmark/selected_der_v2/crl/p25.crl b/tests/benchmark/selected_der_v2/crl/p25.crl new file mode 100644 index 0000000..412d93a Binary files /dev/null and b/tests/benchmark/selected_der_v2/crl/p25.crl differ diff --git a/tests/benchmark/selected_der_v2/crl/p50.crl b/tests/benchmark/selected_der_v2/crl/p50.crl new file mode 100644 index 0000000..0a3efc8 Binary files /dev/null and b/tests/benchmark/selected_der_v2/crl/p50.crl differ diff --git a/tests/benchmark/selected_der_v2/crl/p75.crl b/tests/benchmark/selected_der_v2/crl/p75.crl new file mode 100644 index 0000000..7bee095 Binary files /dev/null and b/tests/benchmark/selected_der_v2/crl/p75.crl differ diff --git a/tests/benchmark/selected_der_v2/crl/p90.crl b/tests/benchmark/selected_der_v2/crl/p90.crl new file mode 100644 index 0000000..bf39ff9 Binary files /dev/null and b/tests/benchmark/selected_der_v2/crl/p90.crl differ diff --git a/tests/benchmark/selected_der_v2/crl/p95.crl b/tests/benchmark/selected_der_v2/crl/p95.crl new file mode 100644 index 0000000..49ec0aa Binary files /dev/null and b/tests/benchmark/selected_der_v2/crl/p95.crl differ diff --git a/tests/benchmark/selected_der_v2/crl/p99.crl b/tests/benchmark/selected_der_v2/crl/p99.crl new file mode 100644 index 0000000..0b38686 Binary files /dev/null and b/tests/benchmark/selected_der_v2/crl/p99.crl differ diff --git a/tests/benchmark/selected_der_v2/manifest/max.mft b/tests/benchmark/selected_der_v2/manifest/max.mft new file mode 100644 index 0000000..16dbdf4 Binary files /dev/null and b/tests/benchmark/selected_der_v2/manifest/max.mft differ diff --git a/tests/benchmark/selected_der_v2/manifest/min.mft b/tests/benchmark/selected_der_v2/manifest/min.mft new file mode 100644 index 0000000..0411852 Binary files /dev/null and b/tests/benchmark/selected_der_v2/manifest/min.mft differ diff --git a/tests/benchmark/selected_der_v2/manifest/p01.mft b/tests/benchmark/selected_der_v2/manifest/p01.mft new file mode 100644 index 0000000..44ac5a8 Binary files /dev/null and b/tests/benchmark/selected_der_v2/manifest/p01.mft differ diff --git a/tests/benchmark/selected_der_v2/manifest/p10.mft b/tests/benchmark/selected_der_v2/manifest/p10.mft new file mode 100644 index 0000000..912193b Binary files /dev/null and b/tests/benchmark/selected_der_v2/manifest/p10.mft differ diff --git a/tests/benchmark/selected_der_v2/manifest/p25.mft b/tests/benchmark/selected_der_v2/manifest/p25.mft new file mode 100644 index 0000000..fd8a0d2 Binary files /dev/null and b/tests/benchmark/selected_der_v2/manifest/p25.mft differ diff --git a/tests/benchmark/selected_der_v2/manifest/p50.mft b/tests/benchmark/selected_der_v2/manifest/p50.mft new file mode 100644 index 0000000..27a33ec Binary files /dev/null and b/tests/benchmark/selected_der_v2/manifest/p50.mft differ diff --git a/tests/benchmark/selected_der_v2/manifest/p75.mft b/tests/benchmark/selected_der_v2/manifest/p75.mft new file mode 100644 index 0000000..65a510e Binary files /dev/null and b/tests/benchmark/selected_der_v2/manifest/p75.mft differ diff --git a/tests/benchmark/selected_der_v2/manifest/p90.mft b/tests/benchmark/selected_der_v2/manifest/p90.mft new file mode 100644 index 0000000..1710ca0 Binary files /dev/null and b/tests/benchmark/selected_der_v2/manifest/p90.mft differ diff --git a/tests/benchmark/selected_der_v2/manifest/p95.mft b/tests/benchmark/selected_der_v2/manifest/p95.mft new file mode 100644 index 0000000..e1442fa Binary files /dev/null and b/tests/benchmark/selected_der_v2/manifest/p95.mft differ diff --git a/tests/benchmark/selected_der_v2/manifest/p99.mft b/tests/benchmark/selected_der_v2/manifest/p99.mft new file mode 100644 index 0000000..65b8c12 Binary files /dev/null and b/tests/benchmark/selected_der_v2/manifest/p99.mft differ diff --git a/tests/benchmark/selected_der_v2/meta/samples.json b/tests/benchmark/selected_der_v2/meta/samples.json new file mode 100644 index 0000000..5c311b1 --- /dev/null +++ b/tests/benchmark/selected_der_v2/meta/samples.json @@ -0,0 +1,913 @@ +{ + "created_at_rfc3339_utc": "2026-02-25T07:45:23.460363033Z", + "store_dir_hint": "/home/yuyr/dev/rust_playground/routinator/bench/full/cache/stored", + "per_type": { + "aspa": 10, + "cer": 10, + "crl": 10, + "manifest": 10, + "roa": 10 + }, + "samples": [ + { + "obj_type": "cer", + "label": "max", + "file_rel_path": "tests/benchmark/selected_der_v2/cer/max.cer", + "size_bytes": 101541, + "sha256_hex": "6eb39fd890725623c7fb02fd9eb7940118264c24bbd5a1cf5fe508375d94f2a6", + "pack_rel_path": "rrdp/rrdp.lacnic.net/b4460caf6e2ae89d1e7133d19861729c100528f5bf30cc8ffec1eabb2279b4bf/rsync/repository.lacnic.net/rpki/lacnic/A1531B24BF50C461C7F574CD65267A8B0DC325DAAA10075F67165B98C4F4EFC3/0/05BAF2939E37DDDE1793A803162A35594ACBB405.mft", + "pack_update_time_rfc3339_utc": "2026-02-03T02:24:35Z", + "manifest_uri": "rsync://repository.lacnic.net/rpki/lacnic/A1531B24BF50C461C7F574CD65267A8B0DC325DAAA10075F67165B98C4F4EFC3/0/05BAF2939E37DDDE1793A803162A35594ACBB405.mft", + "object_uri": "rsync://repository.lacnic.net/rpki/lacnic/A1531B24BF50C461C7F574CD65267A8B0DC325DAAA10075F67165B98C4F4EFC3/0/605432E9E1B05A7E6C208B2946FDC9C967CA8A4B.cer", + "manifest_this_update_rfc3339_utc": "2026-02-02T15:00:09Z", + "manifest_not_after_rfc3339_utc": "2026-02-07T21:50:09Z", + "metrics": { + "kind": "cer", + "spki_len": 294, + "ext_count": 9, + "ip_resource_count": 12560, + "as_resource_count": 387 + } + }, + { + "obj_type": "cer", + "label": "min", + "file_rel_path": "tests/benchmark/selected_der_v2/cer/min.cer", + "size_bytes": 898, + "sha256_hex": "0eb0d58bf262a6149f7ff2949581eb4a36eb5b5ce055d7ea51cb1ce9375ecfbe", + "pack_rel_path": "rrdp/chloe.sobornost.net/a4ae76c5702d98db9df10a8a83f3450d9d07f06afd431e60721be71a84cf15b4/rsync/chloe.sobornost.net/rpki/RIPE-nljobsnijders/yqgF26w2R0m5sRVZCrbvD5cM29g.mft", + "pack_update_time_rfc3339_utc": "2026-02-03T04:32:08Z", + "manifest_uri": "rsync://chloe.sobornost.net/rpki/RIPE-nljobsnijders/yqgF26w2R0m5sRVZCrbvD5cM29g.mft", + "object_uri": "rsync://chloe.sobornost.net/rpki/RIPE-nljobsnijders/XUJQ4tgdREjYop786R0p_wdeyeI.cer", + "manifest_this_update_rfc3339_utc": "2026-02-03T04:24:59Z", + "manifest_not_after_rfc3339_utc": "2027-07-01T00:00:00Z", + "metrics": { + "kind": "cer", + "spki_len": 91, + "ext_count": 6, + "ip_resource_count": 0, + "as_resource_count": 1 + } + }, + { + "obj_type": "cer", + "label": "p01", + "file_rel_path": "tests/benchmark/selected_der_v2/cer/p01.cer", + "size_bytes": 1378, + "sha256_hex": "b00ff8d2746d735f23592abdfb812a6d48d3f8ea3df3f25431711535fb6e1644", + "pack_rel_path": "rrdp/rrdp.twnic.tw/9e5ef8f1eb47c1bd46d2c578ebefa6a774fe140dbfbfeff1a018f1ebfc1de14b/rsync/rpkica.twnic.tw/rpki/TWNICCA/ojp8Y1RxGKrkl_A-ExIclqs0VH4.mft", + "pack_update_time_rfc3339_utc": "2026-02-03T03:25:43Z", + "manifest_uri": "rsync://rpkica.twnic.tw/rpki/TWNICCA/ojp8Y1RxGKrkl_A-ExIclqs0VH4.mft", + "object_uri": "rsync://rpkica.twnic.tw/rpki/TWNICCA/6Vlz64b1l8rmEsZ5Ke2TUucLVSg.cer", + "manifest_this_update_rfc3339_utc": "2026-02-03T03:18:26Z", + "manifest_not_after_rfc3339_utc": "2026-09-30T00:00:00Z", + "metrics": { + "kind": "cer", + "spki_len": 294, + "ext_count": 8, + "ip_resource_count": 2, + "as_resource_count": 0 + } + }, + { + "obj_type": "cer", + "label": "p10", + "file_rel_path": "tests/benchmark/selected_der_v2/cer/p10.cer", + "size_bytes": 1537, + "sha256_hex": "2ebde61f5f5587561b6d390db9a76bfa86cdfd1571e9147e956dc2288be7ec7d", + "pack_rel_path": "rrdp/rrdp.apnic.net/020675249928c63c162a11d00ab299c0b2d0e2c3ca4d27a8ebf933ace26a66a6/rsync/rpki.apnic.net/repository/B3A24F201D6611E28AC8837C72FD1FF2/dAFlqA0QcZcKvAnAK3HBrHwdbg4.mft", + "pack_update_time_rfc3339_utc": "2026-02-03T03:25:13Z", + "manifest_uri": "rsync://rpki.apnic.net/repository/B3A24F201D6611E28AC8837C72FD1FF2/dAFlqA0QcZcKvAnAK3HBrHwdbg4.mft", + "object_uri": "rsync://rpki.apnic.net/repository/B3A24F201D6611E28AC8837C72FD1FF2/gmqoO74CQNE5BqurRkMX6eercO8.cer", + "manifest_this_update_rfc3339_utc": "2026-02-03T02:50:14Z", + "manifest_not_after_rfc3339_utc": "2026-02-10T02:50:14Z", + "metrics": { + "kind": "cer", + "spki_len": 294, + "ext_count": 8, + "ip_resource_count": 1, + "as_resource_count": 0 + } + }, + { + "obj_type": "cer", + "label": "p25", + "file_rel_path": "tests/benchmark/selected_der_v2/cer/p25.cer", + "size_bytes": 1581, + "sha256_hex": "4b519d26e07bbce5407b09717a0102ff9361b80e3d18bc9ef3ff5764b87853ec", + "pack_rel_path": "rrdp/rrdp.apnic.net/020675249928c63c162a11d00ab299c0b2d0e2c3ca4d27a8ebf933ace26a66a6/rsync/rpki.apnic.net/repository/B527EF581D6611E2BB468F7C72FD1FF2/DmWk9f02tb1o6zySNAiXjJB6p58.mft", + "pack_update_time_rfc3339_utc": "2026-02-03T04:29:30Z", + "manifest_uri": "rsync://rpki.apnic.net/repository/B527EF581D6611E2BB468F7C72FD1FF2/DmWk9f02tb1o6zySNAiXjJB6p58.mft", + "object_uri": "rsync://rpki.apnic.net/repository/B527EF581D6611E2BB468F7C72FD1FF2/J39oYmhAcrYqFW3fdd13JcOPUdA.cer", + "manifest_this_update_rfc3339_utc": "2026-02-03T03:47:20Z", + "manifest_not_after_rfc3339_utc": "2026-02-10T03:47:20Z", + "metrics": { + "kind": "cer", + "spki_len": 294, + "ext_count": 9, + "ip_resource_count": 2, + "as_resource_count": 1 + } + }, + { + "obj_type": "cer", + "label": "p50", + "file_rel_path": "tests/benchmark/selected_der_v2/cer/p50.cer", + "size_bytes": 1594, + "sha256_hex": "818d24d7dd2fcafc97a8e3783f2b7bd2844096850e2fdea81f4d69d2b45764f4", + "pack_rel_path": "rrdp/rrdp.apnic.net/020675249928c63c162a11d00ab299c0b2d0e2c3ca4d27a8ebf933ace26a66a6/rsync/rpki.apnic.net/repository/B527EF581D6611E2BB468F7C72FD1FF2/DmWk9f02tb1o6zySNAiXjJB6p58.mft", + "pack_update_time_rfc3339_utc": "2026-02-03T04:29:30Z", + "manifest_uri": "rsync://rpki.apnic.net/repository/B527EF581D6611E2BB468F7C72FD1FF2/DmWk9f02tb1o6zySNAiXjJB6p58.mft", + "object_uri": "rsync://rpki.apnic.net/repository/B527EF581D6611E2BB468F7C72FD1FF2/TxeC6ZVkwoo27twZ-XPR2SdHgf4.cer", + "manifest_this_update_rfc3339_utc": "2026-02-03T03:47:20Z", + "manifest_not_after_rfc3339_utc": "2026-02-10T03:47:20Z", + "metrics": { + "kind": "cer", + "spki_len": 294, + "ext_count": 9, + "ip_resource_count": 3, + "as_resource_count": 2 + } + }, + { + "obj_type": "cer", + "label": "p75", + "file_rel_path": "tests/benchmark/selected_der_v2/cer/p75.cer", + "size_bytes": 1793, + "sha256_hex": "f59769ebe6b3114db70579fc8dc64c256dac4eff714a8e192a6f34ae5424445c", + "pack_rel_path": "rrdp/rrdp.arin.net/e2c8cb4b372d841061fc231dcdc28a679c244f4f630d41cfe01c8c3aaa3a36f7/rsync/rpki.arin.net/repository/arin-rpki-ta/5e4a23ea-e80a-403e-b08c-2171da2157d3/fde169ed-d0d2-4165-8308-df2597e343f8/fde169ed-d0d2-4165-8308-df2597e343f8.mft", + "pack_update_time_rfc3339_utc": "2026-02-04T03:22:56Z", + "manifest_uri": "rsync://rpki.arin.net/repository/arin-rpki-ta/5e4a23ea-e80a-403e-b08c-2171da2157d3/fde169ed-d0d2-4165-8308-df2597e343f8/fde169ed-d0d2-4165-8308-df2597e343f8.mft", + "object_uri": "rsync://rpki.arin.net/repository/arin-rpki-ta/5e4a23ea-e80a-403e-b08c-2171da2157d3/fde169ed-d0d2-4165-8308-df2597e343f8/be801a76-17f5-4096-843c-c6728fe01188.cer", + "manifest_this_update_rfc3339_utc": "2026-02-04T02:10:03Z", + "manifest_not_after_rfc3339_utc": "2026-02-06T18:00:00Z", + "metrics": { + "kind": "cer", + "spki_len": 294, + "ext_count": 8, + "ip_resource_count": 2, + "as_resource_count": 0 + } + }, + { + "obj_type": "cer", + "label": "p90", + "file_rel_path": "tests/benchmark/selected_der_v2/cer/p90.cer", + "size_bytes": 1825, + "sha256_hex": "6930e4f019b59d64a135514da8da6bff6a2725e03178ae4914c68c2bc1af8d78", + "pack_rel_path": "rrdp/rrdp.arin.net/e2c8cb4b372d841061fc231dcdc28a679c244f4f630d41cfe01c8c3aaa3a36f7/rsync/rpki.arin.net/repository/arin-rpki-ta/5e4a23ea-e80a-403e-b08c-2171da2157d3/fde169ed-d0d2-4165-8308-df2597e343f8/fde169ed-d0d2-4165-8308-df2597e343f8.mft", + "pack_update_time_rfc3339_utc": "2026-02-04T03:22:56Z", + "manifest_uri": "rsync://rpki.arin.net/repository/arin-rpki-ta/5e4a23ea-e80a-403e-b08c-2171da2157d3/fde169ed-d0d2-4165-8308-df2597e343f8/fde169ed-d0d2-4165-8308-df2597e343f8.mft", + "object_uri": "rsync://rpki.arin.net/repository/arin-rpki-ta/5e4a23ea-e80a-403e-b08c-2171da2157d3/fde169ed-d0d2-4165-8308-df2597e343f8/2ce98e57-da60-49a3-b167-e6998e0d2945.cer", + "manifest_this_update_rfc3339_utc": "2026-02-04T02:10:03Z", + "manifest_not_after_rfc3339_utc": "2026-02-06T18:00:00Z", + "metrics": { + "kind": "cer", + "spki_len": 294, + "ext_count": 9, + "ip_resource_count": 3, + "as_resource_count": 1 + } + }, + { + "obj_type": "cer", + "label": "p95", + "file_rel_path": "tests/benchmark/selected_der_v2/cer/p95.cer", + "size_bytes": 1839, + "sha256_hex": "de8a89f3089560b21d0bf2021d79e41d4d259b8efcfb5fd795b349b031b3d527", + "pack_rel_path": "rrdp/rrdp.arin.net/e2c8cb4b372d841061fc231dcdc28a679c244f4f630d41cfe01c8c3aaa3a36f7/rsync/rpki.arin.net/repository/arin-rpki-ta/5e4a23ea-e80a-403e-b08c-2171da2157d3/0357272c-a79a-45bf-9586-92dd49ef3223/0357272c-a79a-45bf-9586-92dd49ef3223.mft", + "pack_update_time_rfc3339_utc": "2026-02-04T03:24:34Z", + "manifest_uri": "rsync://rpki.arin.net/repository/arin-rpki-ta/5e4a23ea-e80a-403e-b08c-2171da2157d3/0357272c-a79a-45bf-9586-92dd49ef3223/0357272c-a79a-45bf-9586-92dd49ef3223.mft", + "object_uri": "rsync://rpki.arin.net/repository/arin-rpki-ta/5e4a23ea-e80a-403e-b08c-2171da2157d3/0357272c-a79a-45bf-9586-92dd49ef3223/24597156-139e-456b-b7b5-78502f61a0ca.cer", + "manifest_this_update_rfc3339_utc": "2026-02-04T01:00:09Z", + "manifest_not_after_rfc3339_utc": "2026-02-06T01:00:00Z", + "metrics": { + "kind": "cer", + "spki_len": 294, + "ext_count": 9, + "ip_resource_count": 4, + "as_resource_count": 1 + } + }, + { + "obj_type": "cer", + "label": "p99", + "file_rel_path": "tests/benchmark/selected_der_v2/cer/p99.cer", + "size_bytes": 1957, + "sha256_hex": "9bc72415e60011489d1d4e87e60e18511ee57997d5597805702306e83adc5c32", + "pack_rel_path": "rrdp/rrdp.arin.net/e2c8cb4b372d841061fc231dcdc28a679c244f4f630d41cfe01c8c3aaa3a36f7/rsync/rpki.arin.net/repository/arin-rpki-ta/5e4a23ea-e80a-403e-b08c-2171da2157d3/f60c9f32-a87c-4339-a2f3-6299a3b02e29/f60c9f32-a87c-4339-a2f3-6299a3b02e29.mft", + "pack_update_time_rfc3339_utc": "2026-02-04T03:24:59Z", + "manifest_uri": "rsync://rpki.arin.net/repository/arin-rpki-ta/5e4a23ea-e80a-403e-b08c-2171da2157d3/f60c9f32-a87c-4339-a2f3-6299a3b02e29/f60c9f32-a87c-4339-a2f3-6299a3b02e29.mft", + "object_uri": "rsync://rpki.arin.net/repository/arin-rpki-ta/5e4a23ea-e80a-403e-b08c-2171da2157d3/f60c9f32-a87c-4339-a2f3-6299a3b02e29/45de47ca-0b52-44ab-af26-767d8a671192.cer", + "manifest_this_update_rfc3339_utc": "2026-02-03T21:00:08Z", + "manifest_not_after_rfc3339_utc": "2026-02-05T21:00:00Z", + "metrics": { + "kind": "cer", + "spki_len": 294, + "ext_count": 9, + "ip_resource_count": 22, + "as_resource_count": 2 + } + }, + { + "obj_type": "crl", + "label": "max", + "file_rel_path": "tests/benchmark/selected_der_v2/crl/max.crl", + "size_bytes": 895053, + "sha256_hex": "87bb4d0cced69cf0be5dd94d0e7fe77dd6de4f2e6f5df71b42c78f47ad50b253", + "pack_rel_path": "rrdp/rpki.cnnic.cn/d91215e7f18122c7165f486789e5ec6859e60d4acc113741c389b13efb42c6bc/rsync/rpki.cnnic.cn/rpki/A9162E3D0000/1663/iuTPeLSd8LLB0p0y5IqUOuT0Gsw.mft", + "pack_update_time_rfc3339_utc": "2026-02-03T02:26:01Z", + "manifest_uri": "rsync://rpki.cnnic.cn/rpki/A9162E3D0000/1663/iuTPeLSd8LLB0p0y5IqUOuT0Gsw.mft", + "object_uri": "rsync://rpki.cnnic.cn/rpki/A9162E3D0000/1663/iuTPeLSd8LLB0p0y5IqUOuT0Gsw.crl", + "manifest_this_update_rfc3339_utc": "2026-02-02T23:28:58Z", + "manifest_not_after_rfc3339_utc": "2027-01-09T08:23:18Z", + "metrics": { + "kind": "crl", + "revoked_count": 41411 + } + }, + { + "obj_type": "crl", + "label": "min", + "file_rel_path": "tests/benchmark/selected_der_v2/crl/min.crl", + "size_bytes": 409, + "sha256_hex": "80919a481ad03799f996a7911635f43e2ce0709a940d95b84ea059dc5aa7cbea", + "pack_rel_path": "rrdp/rrdp.arin.net/e2c8cb4b372d841061fc231dcdc28a679c244f4f630d41cfe01c8c3aaa3a36f7/rsync/rpki.arin.net/repository/arin-rpki-ta/arin-rpki-ta.mft", + "pack_update_time_rfc3339_utc": "2026-02-03T02:28:48Z", + "manifest_uri": "rsync://rpki.arin.net/repository/arin-rpki-ta/arin-rpki-ta.mft", + "object_uri": "rsync://rpki.arin.net/repository/arin-rpki-ta/arin-rpki-ta.crl", + "manifest_this_update_rfc3339_utc": "2025-08-19T17:19:47Z", + "manifest_not_after_rfc3339_utc": "2026-04-19T17:19:47Z", + "metrics": { + "kind": "crl", + "revoked_count": 0 + } + }, + { + "obj_type": "crl", + "label": "p01", + "file_rel_path": "tests/benchmark/selected_der_v2/crl/p01.crl", + "size_bytes": 433, + "sha256_hex": "e570e2fb424749381c8b331f37504cfa7ee4ff2ae2438c92d972ccdef47eaaeb", + "pack_rel_path": "rrdp/rrdp.lacnic.net/b4460caf6e2ae89d1e7133d19861729c100528f5bf30cc8ffec1eabb2279b4bf/rsync/repository.lacnic.net/rpki/lacnic/91274329588638C015CCBA8F9D89E1FEDD9BFDA8B46FE1567059F96B0BA87379/0/5F5C0DFD76D6DC1D850EE2157BD0F4E5F6ED2EBB.mft", + "pack_update_time_rfc3339_utc": "2026-02-03T02:24:39Z", + "manifest_uri": "rsync://repository.lacnic.net/rpki/lacnic/91274329588638C015CCBA8F9D89E1FEDD9BFDA8B46FE1567059F96B0BA87379/0/5F5C0DFD76D6DC1D850EE2157BD0F4E5F6ED2EBB.mft", + "object_uri": "rsync://repository.lacnic.net/rpki/lacnic/91274329588638C015CCBA8F9D89E1FEDD9BFDA8B46FE1567059F96B0BA87379/0/5F5C0DFD76D6DC1D850EE2157BD0F4E5F6ED2EBB.crl", + "manifest_this_update_rfc3339_utc": "2026-02-01T23:22:13Z", + "manifest_not_after_rfc3339_utc": "2026-02-07T06:44:13Z", + "metrics": { + "kind": "crl", + "revoked_count": 0 + } + }, + { + "obj_type": "crl", + "label": "p10", + "file_rel_path": "tests/benchmark/selected_der_v2/crl/p10.crl", + "size_bytes": 475, + "sha256_hex": "62754c72974e31156253f95e30d07cd3072f184c9ae37df665b4ecbbe20b3d94", + "pack_rel_path": "rrdp/rrdp.arin.net/e2c8cb4b372d841061fc231dcdc28a679c244f4f630d41cfe01c8c3aaa3a36f7/rsync/rpki.arin.net/repository/arin-rpki-ta/5e4a23ea-e80a-403e-b08c-2171da2157d3/0357272c-a79a-45bf-9586-92dd49ef3223/93ec9d2b-8293-4a44-a996-77893f2d4008/93ec9d2b-8293-4a44-a996-77893f2d4008.mft", + "pack_update_time_rfc3339_utc": "2026-02-04T03:24:47Z", + "manifest_uri": "rsync://rpki.arin.net/repository/arin-rpki-ta/5e4a23ea-e80a-403e-b08c-2171da2157d3/0357272c-a79a-45bf-9586-92dd49ef3223/93ec9d2b-8293-4a44-a996-77893f2d4008/93ec9d2b-8293-4a44-a996-77893f2d4008.mft", + "object_uri": "rsync://rpki.arin.net/repository/arin-rpki-ta/5e4a23ea-e80a-403e-b08c-2171da2157d3/0357272c-a79a-45bf-9586-92dd49ef3223/93ec9d2b-8293-4a44-a996-77893f2d4008/93ec9d2b-8293-4a44-a996-77893f2d4008.crl", + "manifest_this_update_rfc3339_utc": "2026-02-03T19:00:04Z", + "manifest_not_after_rfc3339_utc": "2026-02-05T19:00:00Z", + "metrics": { + "kind": "crl", + "revoked_count": 1 + } + }, + { + "obj_type": "crl", + "label": "p25", + "file_rel_path": "tests/benchmark/selected_der_v2/crl/p25.crl", + "size_bytes": 514, + "sha256_hex": "5cb9aca6321edea55bcffa73faba4b128dfc6f065b44247496b5fc0b988e8589", + "pack_rel_path": "rrdp/rrdp.arin.net/e2c8cb4b372d841061fc231dcdc28a679c244f4f630d41cfe01c8c3aaa3a36f7/rsync/rpki.arin.net/repository/arin-rpki-ta/5e4a23ea-e80a-403e-b08c-2171da2157d3/0357272c-a79a-45bf-9586-92dd49ef3223/9b13d137-bc85-4935-b740-e59f0078def5/9b13d137-bc85-4935-b740-e59f0078def5.mft", + "pack_update_time_rfc3339_utc": "2026-02-04T03:24:42Z", + "manifest_uri": "rsync://rpki.arin.net/repository/arin-rpki-ta/5e4a23ea-e80a-403e-b08c-2171da2157d3/0357272c-a79a-45bf-9586-92dd49ef3223/9b13d137-bc85-4935-b740-e59f0078def5/9b13d137-bc85-4935-b740-e59f0078def5.mft", + "object_uri": "rsync://rpki.arin.net/repository/arin-rpki-ta/5e4a23ea-e80a-403e-b08c-2171da2157d3/0357272c-a79a-45bf-9586-92dd49ef3223/9b13d137-bc85-4935-b740-e59f0078def5/9b13d137-bc85-4935-b740-e59f0078def5.crl", + "manifest_this_update_rfc3339_utc": "2026-02-03T20:00:08Z", + "manifest_not_after_rfc3339_utc": "2026-02-06T20:00:00Z", + "metrics": { + "kind": "crl", + "revoked_count": 2 + } + }, + { + "obj_type": "crl", + "label": "p50", + "file_rel_path": "tests/benchmark/selected_der_v2/crl/p50.crl", + "size_bytes": 540, + "sha256_hex": "738ea6345fdda75fe00f10b66eec108502ce1495f0688e9f0ca69a441ed4a1ea", + "pack_rel_path": "rrdp/rrdp.apnic.net/020675249928c63c162a11d00ab299c0b2d0e2c3ca4d27a8ebf933ace26a66a6/rsync/rpki.apnic.net/member_repository/A913E23F/C64CAAA43E5211EDAAD4B64FC4F9AE02/AgtgbdnoiYp9XfXqHKPZlgKOddk.mft", + "pack_update_time_rfc3339_utc": "2026-02-03T02:25:14Z", + "manifest_uri": "rsync://rpki.apnic.net/member_repository/A913E23F/C64CAAA43E5211EDAAD4B64FC4F9AE02/AgtgbdnoiYp9XfXqHKPZlgKOddk.mft", + "object_uri": "rsync://rpki.apnic.net/member_repository/A913E23F/C64CAAA43E5211EDAAD4B64FC4F9AE02/AgtgbdnoiYp9XfXqHKPZlgKOddk.crl", + "manifest_this_update_rfc3339_utc": "2026-02-03T01:03:21Z", + "manifest_not_after_rfc3339_utc": "2026-02-10T01:03:21Z", + "metrics": { + "kind": "crl", + "revoked_count": 4 + } + }, + { + "obj_type": "crl", + "label": "p75", + "file_rel_path": "tests/benchmark/selected_der_v2/crl/p75.crl", + "size_bytes": 554, + "sha256_hex": "5eb9bbca1c8fb200fe68541671b62a3bd654cb50dc0946780d49c9a191f37f5a", + "pack_rel_path": "rrdp/rrdp.arin.net/e2c8cb4b372d841061fc231dcdc28a679c244f4f630d41cfe01c8c3aaa3a36f7/rsync/rpki.arin.net/repository/arin-rpki-ta/5e4a23ea-e80a-403e-b08c-2171da2157d3/69fd0156-bb1f-48b6-bf32-c9492286f195/44b921f7-65dd-4b33-b399-9d372fb4a9e5/44b921f7-65dd-4b33-b399-9d372fb4a9e5.mft", + "pack_update_time_rfc3339_utc": "2026-02-04T03:24:12Z", + "manifest_uri": "rsync://rpki.arin.net/repository/arin-rpki-ta/5e4a23ea-e80a-403e-b08c-2171da2157d3/69fd0156-bb1f-48b6-bf32-c9492286f195/44b921f7-65dd-4b33-b399-9d372fb4a9e5/44b921f7-65dd-4b33-b399-9d372fb4a9e5.mft", + "object_uri": "rsync://rpki.arin.net/repository/arin-rpki-ta/5e4a23ea-e80a-403e-b08c-2171da2157d3/69fd0156-bb1f-48b6-bf32-c9492286f195/44b921f7-65dd-4b33-b399-9d372fb4a9e5/44b921f7-65dd-4b33-b399-9d372fb4a9e5.crl", + "manifest_this_update_rfc3339_utc": "2026-02-03T19:00:04Z", + "manifest_not_after_rfc3339_utc": "2026-02-06T19:00:00Z", + "metrics": { + "kind": "crl", + "revoked_count": 3 + } + }, + { + "obj_type": "crl", + "label": "p90", + "file_rel_path": "tests/benchmark/selected_der_v2/crl/p90.crl", + "size_bytes": 633, + "sha256_hex": "a7aff3478dc5f602ee15aba331163a53f843a83837281544f19e2d6fa6987f2e", + "pack_rel_path": "rrdp/rrdp.lacnic.net/b4460caf6e2ae89d1e7133d19861729c100528f5bf30cc8ffec1eabb2279b4bf/rsync/repository.lacnic.net/rpki/lacnic/4343B2828241D18D49CED12666F5034D90C1A11663270CCFE62C8E67AE1937D7/0/7A06C1864524D4D6B3F3D941A758895DEA60A24F.mft", + "pack_update_time_rfc3339_utc": "2026-02-03T02:25:22Z", + "manifest_uri": "rsync://repository.lacnic.net/rpki/lacnic/4343B2828241D18D49CED12666F5034D90C1A11663270CCFE62C8E67AE1937D7/0/7A06C1864524D4D6B3F3D941A758895DEA60A24F.mft", + "object_uri": "rsync://repository.lacnic.net/rpki/lacnic/4343B2828241D18D49CED12666F5034D90C1A11663270CCFE62C8E67AE1937D7/0/7A06C1864524D4D6B3F3D941A758895DEA60A24F.crl", + "manifest_this_update_rfc3339_utc": "2026-02-02T13:33:21Z", + "manifest_not_after_rfc3339_utc": "2026-02-07T14:30:21Z", + "metrics": { + "kind": "crl", + "revoked_count": 5 + } + }, + { + "obj_type": "crl", + "label": "p95", + "file_rel_path": "tests/benchmark/selected_der_v2/crl/p95.crl", + "size_bytes": 772, + "sha256_hex": "a0fb838e8b072a928748972895d8f837427b1ba797ca6c901a9e009b652da88b", + "pack_rel_path": "rrdp/rrdp.ripe.net/2315d99a99627f34bc597569abc7c177ad45108a37f173f3d06dbabd64962f3c/rsync/rpki.ripe.net/repository/DEFAULT/61/17e450-8818-4a27-9f35-518cd14713eb/1/t3M-nelIW0RnHI5RHaIh5BU8cRs.mft", + "pack_update_time_rfc3339_utc": "2026-02-03T04:30:53Z", + "manifest_uri": "rsync://rpki.ripe.net/repository/DEFAULT/61/17e450-8818-4a27-9f35-518cd14713eb/1/t3M-nelIW0RnHI5RHaIh5BU8cRs.mft", + "object_uri": "rsync://rpki.ripe.net/repository/DEFAULT/61/17e450-8818-4a27-9f35-518cd14713eb/1/t3M-nelIW0RnHI5RHaIh5BU8cRs.crl", + "manifest_this_update_rfc3339_utc": "2026-02-03T04:01:11Z", + "manifest_not_after_rfc3339_utc": "2026-02-04T04:01:11Z", + "metrics": { + "kind": "crl", + "revoked_count": 9 + } + }, + { + "obj_type": "crl", + "label": "p99", + "file_rel_path": "tests/benchmark/selected_der_v2/crl/p99.crl", + "size_bytes": 1843, + "sha256_hex": "7a511d49d8f7972b4055d2da2fb213dceb1ae30a6fab896073fd9773f3264777", + "pack_rel_path": "rrdp/rrdp.arin.net/e2c8cb4b372d841061fc231dcdc28a679c244f4f630d41cfe01c8c3aaa3a36f7/rsync/rpki.arin.net/repository/arin-rpki-ta/5e4a23ea-e80a-403e-b08c-2171da2157d3/746e0111-fafb-430f-b778-d204cfcd99a8/5ef79eb4-544e-4504-9a6f-d1ddff5bcd45/5ef79eb4-544e-4504-9a6f-d1ddff5bcd45.mft", + "pack_update_time_rfc3339_utc": "2026-02-04T03:23:07Z", + "manifest_uri": "rsync://rpki.arin.net/repository/arin-rpki-ta/5e4a23ea-e80a-403e-b08c-2171da2157d3/746e0111-fafb-430f-b778-d204cfcd99a8/5ef79eb4-544e-4504-9a6f-d1ddff5bcd45/5ef79eb4-544e-4504-9a6f-d1ddff5bcd45.mft", + "object_uri": "rsync://rpki.arin.net/repository/arin-rpki-ta/5e4a23ea-e80a-403e-b08c-2171da2157d3/746e0111-fafb-430f-b778-d204cfcd99a8/5ef79eb4-544e-4504-9a6f-d1ddff5bcd45/5ef79eb4-544e-4504-9a6f-d1ddff5bcd45.crl", + "manifest_this_update_rfc3339_utc": "2026-02-03T14:00:03Z", + "manifest_not_after_rfc3339_utc": "2026-02-06T14:00:00Z", + "metrics": { + "kind": "crl", + "revoked_count": 36 + } + }, + { + "obj_type": "manifest", + "label": "max", + "file_rel_path": "tests/benchmark/selected_der_v2/manifest/max.mft", + "size_bytes": 3867132, + "sha256_hex": "64a9a90da435b53a45289d869e21b80daca2a242508f18dff371a546d414c792", + "pack_rel_path": "rrdp/rrdp.arin.net/e2c8cb4b372d841061fc231dcdc28a679c244f4f630d41cfe01c8c3aaa3a36f7/rsync/rpki.arin.net/repository/arin-rpki-ta/5e4a23ea-e80a-403e-b08c-2171da2157d3/2a246947-2d62-4a6c-ba05-87187f0099b2/4e95a28e-27fe-479a-b086-2cc9809d54f6/4e95a28e-27fe-479a-b086-2cc9809d54f6.mft", + "pack_update_time_rfc3339_utc": "2026-02-04T03:23:24Z", + "manifest_uri": "rsync://rpki.arin.net/repository/arin-rpki-ta/5e4a23ea-e80a-403e-b08c-2171da2157d3/2a246947-2d62-4a6c-ba05-87187f0099b2/4e95a28e-27fe-479a-b086-2cc9809d54f6/4e95a28e-27fe-479a-b086-2cc9809d54f6.mft", + "object_uri": "rsync://rpki.arin.net/repository/arin-rpki-ta/5e4a23ea-e80a-403e-b08c-2171da2157d3/2a246947-2d62-4a6c-ba05-87187f0099b2/4e95a28e-27fe-479a-b086-2cc9809d54f6/4e95a28e-27fe-479a-b086-2cc9809d54f6.mft", + "manifest_this_update_rfc3339_utc": "2026-02-04T02:02:08Z", + "manifest_not_after_rfc3339_utc": "2026-02-06T10:00:00Z", + "metrics": { + "kind": "manifest", + "file_count": 48923 + } + }, + { + "obj_type": "manifest", + "label": "min", + "file_rel_path": "tests/benchmark/selected_der_v2/manifest/min.mft", + "size_bytes": 1786, + "sha256_hex": "aa3a23ab546dece484fbb43d6ba1546a610f2317ef4e89e0b381e9b24538e7cd", + "pack_rel_path": "rrdp/rrdp.ripe.net/2315d99a99627f34bc597569abc7c177ad45108a37f173f3d06dbabd64962f3c/rsync/rpki.ripe.net/repository/ripe-ncc-ta.mft", + "pack_update_time_rfc3339_utc": "2026-02-03T02:24:12Z", + "manifest_uri": "rsync://rpki.ripe.net/repository/ripe-ncc-ta.mft", + "object_uri": "rsync://rpki.ripe.net/repository/ripe-ncc-ta.mft", + "manifest_this_update_rfc3339_utc": "2026-01-14T10:50:01Z", + "manifest_not_after_rfc3339_utc": "2026-04-14T10:50:01Z", + "metrics": { + "kind": "manifest", + "file_count": 2 + } + }, + { + "obj_type": "manifest", + "label": "p01", + "file_rel_path": "tests/benchmark/selected_der_v2/manifest/p01.mft", + "size_bytes": 1924, + "sha256_hex": "be00f9a0bc379fd15f89cb5ff0397629dd3bc8334cc0cddefd9fb54f39929cfb", + "pack_rel_path": "rrdp/rrdp.ripe.net/2315d99a99627f34bc597569abc7c177ad45108a37f173f3d06dbabd64962f3c/rsync/rpki.ripe.net/repository/DEFAULT/45/031138-86e1-4182-a7a6-0107ec33e5b7/1/1W_iwM_XaRBE7jeEXPz1ThqEmXQ.mft", + "pack_update_time_rfc3339_utc": "2026-02-03T04:30:08Z", + "manifest_uri": "rsync://rpki.ripe.net/repository/DEFAULT/45/031138-86e1-4182-a7a6-0107ec33e5b7/1/1W_iwM_XaRBE7jeEXPz1ThqEmXQ.mft", + "object_uri": "rsync://rpki.ripe.net/repository/DEFAULT/45/031138-86e1-4182-a7a6-0107ec33e5b7/1/1W_iwM_XaRBE7jeEXPz1ThqEmXQ.mft", + "manifest_this_update_rfc3339_utc": "2026-02-03T04:01:07Z", + "manifest_not_after_rfc3339_utc": "2026-02-04T04:01:07Z", + "metrics": { + "kind": "manifest", + "file_count": 1 + } + }, + { + "obj_type": "manifest", + "label": "p10", + "file_rel_path": "tests/benchmark/selected_der_v2/manifest/p10.mft", + "size_bytes": 2012, + "sha256_hex": "f9654d60136983a5c8fce1c7d46d1051d570e801bfd1e95b9486c4583c142e57", + "pack_rel_path": "rrdp/rpki-repository.nic.ad.jp/aa83379c70e8dfe1337af6d0ae686a02631ded2a4138538d1a34917c84c123f2/rsync/rpki-repository.nic.ad.jp/ap/A91A73810000/30366/GUkc8TjsVYI8fTvhsuweTIDV8uU.mft", + "pack_update_time_rfc3339_utc": "2026-02-03T02:25:35Z", + "manifest_uri": "rsync://rpki-repository.nic.ad.jp/ap/A91A73810000/30366/GUkc8TjsVYI8fTvhsuweTIDV8uU.mft", + "object_uri": "rsync://rpki-repository.nic.ad.jp/ap/A91A73810000/30366/GUkc8TjsVYI8fTvhsuweTIDV8uU.mft", + "manifest_this_update_rfc3339_utc": "2026-02-03T01:25:11Z", + "manifest_not_after_rfc3339_utc": "2027-01-15T01:30:02Z", + "metrics": { + "kind": "manifest", + "file_count": 2 + } + }, + { + "obj_type": "manifest", + "label": "p25", + "file_rel_path": "tests/benchmark/selected_der_v2/manifest/p25.mft", + "size_bytes": 2113, + "sha256_hex": "72981b97caffddf00fcf13d11bbd3df8d2a9276b37f6c7877bfa3e47c46f9b0a", + "pack_rel_path": "rrdp/rrdp.apnic.net/020675249928c63c162a11d00ab299c0b2d0e2c3ca4d27a8ebf933ace26a66a6/rsync/rpki.apnic.net/member_repository/A915343B/6A0383E650A511EA976CB77FC4F9AE02/1mAaggCAx5DwpRpsN2X1tLDQGzc.mft", + "pack_update_time_rfc3339_utc": "2026-02-03T02:24:06Z", + "manifest_uri": "rsync://rpki.apnic.net/member_repository/A915343B/6A0383E650A511EA976CB77FC4F9AE02/1mAaggCAx5DwpRpsN2X1tLDQGzc.mft", + "object_uri": "rsync://rpki.apnic.net/member_repository/A915343B/6A0383E650A511EA976CB77FC4F9AE02/1mAaggCAx5DwpRpsN2X1tLDQGzc.mft", + "manifest_this_update_rfc3339_utc": "2026-02-02T18:58:11Z", + "manifest_not_after_rfc3339_utc": "2026-02-09T18:58:11Z", + "metrics": { + "kind": "manifest", + "file_count": 2 + } + }, + { + "obj_type": "manifest", + "label": "p50", + "file_rel_path": "tests/benchmark/selected_der_v2/manifest/p50.mft", + "size_bytes": 2220, + "sha256_hex": "cb94f3b73bacfa2014461574e7f3afdd380bb53fa9efe6d90e074be97fbcb47f", + "pack_rel_path": "rrdp/rrdp.lacnic.net/b4460caf6e2ae89d1e7133d19861729c100528f5bf30cc8ffec1eabb2279b4bf/rsync/repository.lacnic.net/rpki/lacnic/F192D3E65B741BC96E61B6810943E6A8F832DFB178388046815B4288805D4D08/0/3F0974925580D1783B4D99DCE063F669C26152F5.mft", + "pack_update_time_rfc3339_utc": "2026-02-03T02:24:59Z", + "manifest_uri": "rsync://repository.lacnic.net/rpki/lacnic/F192D3E65B741BC96E61B6810943E6A8F832DFB178388046815B4288805D4D08/0/3F0974925580D1783B4D99DCE063F669C26152F5.mft", + "object_uri": "rsync://repository.lacnic.net/rpki/lacnic/F192D3E65B741BC96E61B6810943E6A8F832DFB178388046815B4288805D4D08/0/3F0974925580D1783B4D99DCE063F669C26152F5.mft", + "manifest_this_update_rfc3339_utc": "2026-01-31T13:10:28Z", + "manifest_not_after_rfc3339_utc": "2026-02-06T08:23:28Z", + "metrics": { + "kind": "manifest", + "file_count": 2 + } + }, + { + "obj_type": "manifest", + "label": "p75", + "file_rel_path": "tests/benchmark/selected_der_v2/manifest/p75.mft", + "size_bytes": 2424, + "sha256_hex": "30930241ccd9af792a212f4f9b1e8e01d306aae97fa3c3f1946592a6a1e6188a", + "pack_rel_path": "rrdp/rrdp.lacnic.net/b4460caf6e2ae89d1e7133d19861729c100528f5bf30cc8ffec1eabb2279b4bf/rsync/repository.lacnic.net/rpki/lacnic/C4D72FDA9D9D4649BACD178B2C0FCCBD56680A1D743605C0A204D1487A56E154/0/6F2E85684756BECBED992F0B17433B71F691DBB2.mft", + "pack_update_time_rfc3339_utc": "2026-02-03T02:24:47Z", + "manifest_uri": "rsync://repository.lacnic.net/rpki/lacnic/C4D72FDA9D9D4649BACD178B2C0FCCBD56680A1D743605C0A204D1487A56E154/0/6F2E85684756BECBED992F0B17433B71F691DBB2.mft", + "object_uri": "rsync://repository.lacnic.net/rpki/lacnic/C4D72FDA9D9D4649BACD178B2C0FCCBD56680A1D743605C0A204D1487A56E154/0/6F2E85684756BECBED992F0B17433B71F691DBB2.mft", + "manifest_this_update_rfc3339_utc": "2026-02-01T10:49:31Z", + "manifest_not_after_rfc3339_utc": "2026-02-06T21:25:31Z", + "metrics": { + "kind": "manifest", + "file_count": 4 + } + }, + { + "obj_type": "manifest", + "label": "p90", + "file_rel_path": "tests/benchmark/selected_der_v2/manifest/p90.mft", + "size_bytes": 2707, + "sha256_hex": "c5712b14980d481591699815a64be415df697d085220395e042418a3fb7269ed", + "pack_rel_path": "rrdp/rrdp.lacnic.net/b4460caf6e2ae89d1e7133d19861729c100528f5bf30cc8ffec1eabb2279b4bf/rsync/repository.lacnic.net/rpki/lacnic/760CC0DA4C90C1F0023C05DFEE0070E5744BD89B60F4CD9C02EF81695E71099D/0/17444832D4DFC5B5F0EDB45E0DD0380E36A684B7.mft", + "pack_update_time_rfc3339_utc": "2026-02-03T02:25:01Z", + "manifest_uri": "rsync://repository.lacnic.net/rpki/lacnic/760CC0DA4C90C1F0023C05DFEE0070E5744BD89B60F4CD9C02EF81695E71099D/0/17444832D4DFC5B5F0EDB45E0DD0380E36A684B7.mft", + "object_uri": "rsync://repository.lacnic.net/rpki/lacnic/760CC0DA4C90C1F0023C05DFEE0070E5744BD89B60F4CD9C02EF81695E71099D/0/17444832D4DFC5B5F0EDB45E0DD0380E36A684B7.mft", + "manifest_this_update_rfc3339_utc": "2026-02-01T22:41:49Z", + "manifest_not_after_rfc3339_utc": "2026-02-06T13:29:49Z", + "metrics": { + "kind": "manifest", + "file_count": 7 + } + }, + { + "obj_type": "manifest", + "label": "p95", + "file_rel_path": "tests/benchmark/selected_der_v2/manifest/p95.mft", + "size_bytes": 3189, + "sha256_hex": "748b46f40db2de36e38baf74f636bafbe65f87750cd11885fdfdfee595872ebb", + "pack_rel_path": "rrdp/rpki-repo.registro.br/026b1847a231e62a6bcf46a3794665f4dcbe0e609f90c0e54d7062dbf72114d9/rsync/rpki-repo.registro.br/repo/31ECeNa6JFrQNNySZguSi82Sz114gA5jRxQYM2Por1qQ/1/8BB23FB0F38F7CDE17852DC61BD234DCEBC3BB90.mft", + "pack_update_time_rfc3339_utc": "2026-02-03T02:27:22Z", + "manifest_uri": "rsync://rpki-repo.registro.br/repo/31ECeNa6JFrQNNySZguSi82Sz114gA5jRxQYM2Por1qQ/1/8BB23FB0F38F7CDE17852DC61BD234DCEBC3BB90.mft", + "object_uri": "rsync://rpki-repo.registro.br/repo/31ECeNa6JFrQNNySZguSi82Sz114gA5jRxQYM2Por1qQ/1/8BB23FB0F38F7CDE17852DC61BD234DCEBC3BB90.mft", + "manifest_this_update_rfc3339_utc": "2026-02-03T01:45:17Z", + "manifest_not_after_rfc3339_utc": "2026-02-04T05:48:17Z", + "metrics": { + "kind": "manifest", + "file_count": 13 + } + }, + { + "obj_type": "manifest", + "label": "p99", + "file_rel_path": "tests/benchmark/selected_der_v2/manifest/p99.mft", + "size_bytes": 7579, + "sha256_hex": "d70a0cb76cef6eae4222cf2e9c530fe76c4d0e1ca790d8f7a69435193d317f9c", + "pack_rel_path": "rrdp/rrdp.arin.net/e2c8cb4b372d841061fc231dcdc28a679c244f4f630d41cfe01c8c3aaa3a36f7/rsync/rpki.arin.net/repository/arin-rpki-ta/5e4a23ea-e80a-403e-b08c-2171da2157d3/521eb33f-9672-4cd9-acce-137227e971ac/dcf48eb5-c08a-4793-8dd8-a83f660c3f82/dcf48eb5-c08a-4793-8dd8-a83f660c3f82.mft", + "pack_update_time_rfc3339_utc": "2026-02-04T03:24:25Z", + "manifest_uri": "rsync://rpki.arin.net/repository/arin-rpki-ta/5e4a23ea-e80a-403e-b08c-2171da2157d3/521eb33f-9672-4cd9-acce-137227e971ac/dcf48eb5-c08a-4793-8dd8-a83f660c3f82/dcf48eb5-c08a-4793-8dd8-a83f660c3f82.mft", + "object_uri": "rsync://rpki.arin.net/repository/arin-rpki-ta/5e4a23ea-e80a-403e-b08c-2171da2157d3/521eb33f-9672-4cd9-acce-137227e971ac/dcf48eb5-c08a-4793-8dd8-a83f660c3f82/dcf48eb5-c08a-4793-8dd8-a83f660c3f82.mft", + "manifest_this_update_rfc3339_utc": "2026-02-03T17:00:03Z", + "manifest_not_after_rfc3339_utc": "2026-02-05T17:00:00Z", + "metrics": { + "kind": "manifest", + "file_count": 68 + } + }, + { + "obj_type": "roa", + "label": "max", + "file_rel_path": "tests/benchmark/selected_der_v2/roa/max.roa", + "size_bytes": 49026, + "sha256_hex": "92e302f8d81a2a339c592508ddd598577f4fbf54374cc359843e7ee0ebd3461f", + "pack_rel_path": "rrdp/rrdp.apnic.net/020675249928c63c162a11d00ab299c0b2d0e2c3ca4d27a8ebf933ace26a66a6/rsync/rpki.apnic.net/member_repository/A91DFB70/2983647C838F11E586FC5812C4F9AE02/XS3RVLXc4h-3hsUm297xsEWSirg.mft", + "pack_update_time_rfc3339_utc": "2026-02-03T02:24:53Z", + "manifest_uri": "rsync://rpki.apnic.net/member_repository/A91DFB70/2983647C838F11E586FC5812C4F9AE02/XS3RVLXc4h-3hsUm297xsEWSirg.mft", + "object_uri": "rsync://rpki.apnic.net/member_repository/A91DFB70/2983647C838F11E586FC5812C4F9AE02/9B22F928BFE311EE949C2918C4F9AE02.roa", + "manifest_this_update_rfc3339_utc": "2026-02-02T15:44:10Z", + "manifest_not_after_rfc3339_utc": "2026-02-09T15:44:10Z", + "metrics": { + "kind": "roa", + "addr_family_count": 2, + "prefix_count": 4268, + "max_length_present": 4268 + } + }, + { + "obj_type": "roa", + "label": "min", + "file_rel_path": "tests/benchmark/selected_der_v2/roa/min.roa", + "size_bytes": 1724, + "sha256_hex": "5e2b9467c52689d5cdf2104b49a68aff4cbf03fd2620f3c85b863245cb36a548", + "pack_rel_path": "rrdp/chloe.sobornost.net/a4ae76c5702d98db9df10a8a83f3450d9d07f06afd431e60721be71a84cf15b4/rsync/chloe.sobornost.net/rpki/RIPE-nljobsnijders/yqgF26w2R0m5sRVZCrbvD5cM29g.mft", + "pack_update_time_rfc3339_utc": "2026-02-03T04:32:08Z", + "manifest_uri": "rsync://chloe.sobornost.net/rpki/RIPE-nljobsnijders/yqgF26w2R0m5sRVZCrbvD5cM29g.mft", + "object_uri": "rsync://chloe.sobornost.net/rpki/RIPE-nljobsnijders/euv64En05073B5-r95s2Uu_UwJg.roa", + "manifest_this_update_rfc3339_utc": "2026-02-03T04:24:59Z", + "manifest_not_after_rfc3339_utc": "2027-07-01T00:00:00Z", + "metrics": { + "kind": "roa", + "addr_family_count": 1, + "prefix_count": 1, + "max_length_present": 0 + } + }, + { + "obj_type": "roa", + "label": "p01", + "file_rel_path": "tests/benchmark/selected_der_v2/roa/p01.roa", + "size_bytes": 1757, + "sha256_hex": "141cbcbd8482f8cc17d014ee6475ce725d5250c61c1d04be80a56cd238c45446", + "pack_rel_path": "rrdp/rrdp.twnic.tw/9e5ef8f1eb47c1bd46d2c578ebefa6a774fe140dbfbfeff1a018f1ebfc1de14b/rsync/rpkica.twnic.tw/rpki/TWNICCA/TUNGHO/62dfk4yFAhN0yrHhr1CZMZsRCwc.mft", + "pack_update_time_rfc3339_utc": "2026-02-03T02:27:51Z", + "manifest_uri": "rsync://rpkica.twnic.tw/rpki/TWNICCA/TUNGHO/62dfk4yFAhN0yrHhr1CZMZsRCwc.mft", + "object_uri": "rsync://rpkica.twnic.tw/rpki/TWNICCA/TUNGHO/cIJ9FaRp5lfrLAxp7ucVIU2RW-M.roa", + "manifest_this_update_rfc3339_utc": "2026-02-02T04:17:15Z", + "manifest_not_after_rfc3339_utc": "2026-08-22T08:14:28Z", + "metrics": { + "kind": "roa", + "addr_family_count": 1, + "prefix_count": 1, + "max_length_present": 0 + } + }, + { + "obj_type": "roa", + "label": "p10", + "file_rel_path": "tests/benchmark/selected_der_v2/roa/p10.roa", + "size_bytes": 1919, + "sha256_hex": "9a69ca473003754b53c38566b062e12154a53d65814d5dd6a48c23686cf4ef8a", + "pack_rel_path": "rrdp/rrdp.apnic.net/020675249928c63c162a11d00ab299c0b2d0e2c3ca4d27a8ebf933ace26a66a6/rsync/rpki.apnic.net/member_repository/A918ACB2/9FE760A6B83C11EE8741902EC4F9AE02/6Ihh_0pFOAJnqBtY-pnkia0AG70.mft", + "pack_update_time_rfc3339_utc": "2026-02-03T04:29:43Z", + "manifest_uri": "rsync://rpki.apnic.net/member_repository/A918ACB2/9FE760A6B83C11EE8741902EC4F9AE02/6Ihh_0pFOAJnqBtY-pnkia0AG70.mft", + "object_uri": "rsync://rpki.apnic.net/member_repository/A918ACB2/9FE760A6B83C11EE8741902EC4F9AE02/BD1A38D4984B11EFB342C220C4F9AE02.roa", + "manifest_this_update_rfc3339_utc": "2026-02-03T03:28:59Z", + "manifest_not_after_rfc3339_utc": "2026-02-10T03:28:59Z", + "metrics": { + "kind": "roa", + "addr_family_count": 1, + "prefix_count": 1, + "max_length_present": 1 + } + }, + { + "obj_type": "roa", + "label": "p25", + "file_rel_path": "tests/benchmark/selected_der_v2/roa/p25.roa", + "size_bytes": 2044, + "sha256_hex": "27473f640b696fd5701c0f17dc6e9192992ab7c8889d20e05336aa7b709480fe", + "pack_rel_path": "rrdp/rrdp.afrinic.net/6a5a58f9bfe26843ee174cd41e801eeecbfd15b8bb0dd7fa3952941d726730b6/rsync/rpki.afrinic.net/repository/member_repository/F368F2D0/92F86E1C6E0511E8A1B5854BF8AEA228/eX2I2BPiD_-YLMdBnpabrqa_1ps.mft", + "pack_update_time_rfc3339_utc": "2026-02-03T04:29:29Z", + "manifest_uri": "rsync://rpki.afrinic.net/repository/member_repository/F368F2D0/92F86E1C6E0511E8A1B5854BF8AEA228/eX2I2BPiD_-YLMdBnpabrqa_1ps.mft", + "object_uri": "rsync://rpki.afrinic.net/repository/member_repository/F368F2D0/92F86E1C6E0511E8A1B5854BF8AEA228/618ABDFCD9BF11F0B2E65198DAE4EC9C.roa", + "manifest_this_update_rfc3339_utc": "2026-02-03T03:40:48Z", + "manifest_not_after_rfc3339_utc": "2026-02-05T03:40:48Z", + "metrics": { + "kind": "roa", + "addr_family_count": 1, + "prefix_count": 10, + "max_length_present": 10 + } + }, + { + "obj_type": "roa", + "label": "p50", + "file_rel_path": "tests/benchmark/selected_der_v2/roa/p50.roa", + "size_bytes": 2125, + "sha256_hex": "57dc478a33980a05922d9789cac407295532b109d10e8a4b9e014dc8469b19e7", + "pack_rel_path": "rrdp/rrdp.arin.net/e2c8cb4b372d841061fc231dcdc28a679c244f4f630d41cfe01c8c3aaa3a36f7/rsync/rpki.arin.net/repository/arin-rpki-ta/5e4a23ea-e80a-403e-b08c-2171da2157d3/746e0111-fafb-430f-b778-d204cfcd99a8/18800324-5150-4981-a144-bdb80e6bcb7c/18800324-5150-4981-a144-bdb80e6bcb7c.mft", + "pack_update_time_rfc3339_utc": "2026-02-04T03:23:03Z", + "manifest_uri": "rsync://rpki.arin.net/repository/arin-rpki-ta/5e4a23ea-e80a-403e-b08c-2171da2157d3/746e0111-fafb-430f-b778-d204cfcd99a8/18800324-5150-4981-a144-bdb80e6bcb7c/18800324-5150-4981-a144-bdb80e6bcb7c.mft", + "object_uri": "rsync://rpki.arin.net/repository/arin-rpki-ta/5e4a23ea-e80a-403e-b08c-2171da2157d3/746e0111-fafb-430f-b778-d204cfcd99a8/18800324-5150-4981-a144-bdb80e6bcb7c/9293d640-8dc0-3613-862d-4dfc5d30586c.roa", + "manifest_this_update_rfc3339_utc": "2026-02-04T02:02:08Z", + "manifest_not_after_rfc3339_utc": "2026-02-05T19:00:00Z", + "metrics": { + "kind": "roa", + "addr_family_count": 1, + "prefix_count": 1, + "max_length_present": 0 + } + }, + { + "obj_type": "roa", + "label": "p75", + "file_rel_path": "tests/benchmark/selected_der_v2/roa/p75.roa", + "size_bytes": 2126, + "sha256_hex": "fa0855d366f4dc657d5911b82806370366c19b9fb7ca2b125e4776162b90cc64", + "pack_rel_path": "rrdp/rrdp.arin.net/e2c8cb4b372d841061fc231dcdc28a679c244f4f630d41cfe01c8c3aaa3a36f7/rsync/rpki.arin.net/repository/arin-rpki-ta/5e4a23ea-e80a-403e-b08c-2171da2157d3/746e0111-fafb-430f-b778-d204cfcd99a8/d7c5f272-a2ae-458a-a75f-3cf41f19b603/d7c5f272-a2ae-458a-a75f-3cf41f19b603.mft", + "pack_update_time_rfc3339_utc": "2026-02-04T03:23:05Z", + "manifest_uri": "rsync://rpki.arin.net/repository/arin-rpki-ta/5e4a23ea-e80a-403e-b08c-2171da2157d3/746e0111-fafb-430f-b778-d204cfcd99a8/d7c5f272-a2ae-458a-a75f-3cf41f19b603/d7c5f272-a2ae-458a-a75f-3cf41f19b603.mft", + "object_uri": "rsync://rpki.arin.net/repository/arin-rpki-ta/5e4a23ea-e80a-403e-b08c-2171da2157d3/746e0111-fafb-430f-b778-d204cfcd99a8/d7c5f272-a2ae-458a-a75f-3cf41f19b603/2b935f54-a6b7-3f61-8adb-bce43ad4b075.roa", + "manifest_this_update_rfc3339_utc": "2026-02-03T14:02:08Z", + "manifest_not_after_rfc3339_utc": "2026-02-06T13:00:00Z", + "metrics": { + "kind": "roa", + "addr_family_count": 1, + "prefix_count": 1, + "max_length_present": 0 + } + }, + { + "obj_type": "roa", + "label": "p90", + "file_rel_path": "tests/benchmark/selected_der_v2/roa/p90.roa", + "size_bytes": 2132, + "sha256_hex": "8ec9f949cf6f499672357ccaa2c560b12ef5b5f2acba3e627be97582dc01d73d", + "pack_rel_path": "rrdp/rrdp.arin.net/e2c8cb4b372d841061fc231dcdc28a679c244f4f630d41cfe01c8c3aaa3a36f7/rsync/rpki.arin.net/repository/arin-rpki-ta/5e4a23ea-e80a-403e-b08c-2171da2157d3/2a246947-2d62-4a6c-ba05-87187f0099b2/4e95a28e-27fe-479a-b086-2cc9809d54f6/4e95a28e-27fe-479a-b086-2cc9809d54f6.mft", + "pack_update_time_rfc3339_utc": "2026-02-04T03:23:24Z", + "manifest_uri": "rsync://rpki.arin.net/repository/arin-rpki-ta/5e4a23ea-e80a-403e-b08c-2171da2157d3/2a246947-2d62-4a6c-ba05-87187f0099b2/4e95a28e-27fe-479a-b086-2cc9809d54f6/4e95a28e-27fe-479a-b086-2cc9809d54f6.mft", + "object_uri": "rsync://rpki.arin.net/repository/arin-rpki-ta/5e4a23ea-e80a-403e-b08c-2171da2157d3/2a246947-2d62-4a6c-ba05-87187f0099b2/4e95a28e-27fe-479a-b086-2cc9809d54f6/300d5263-ba19-312e-9ba8-7d527b888d9e.roa", + "manifest_this_update_rfc3339_utc": "2026-02-04T02:02:08Z", + "manifest_not_after_rfc3339_utc": "2026-02-06T10:00:00Z", + "metrics": { + "kind": "roa", + "addr_family_count": 1, + "prefix_count": 1, + "max_length_present": 0 + } + }, + { + "obj_type": "roa", + "label": "p95", + "file_rel_path": "tests/benchmark/selected_der_v2/roa/p95.roa", + "size_bytes": 2136, + "sha256_hex": "97fcd42de1a2f346d17edc58b671e04aafabef818f3ef9498609aa1fc684eee7", + "pack_rel_path": "rrdp/rrdp.arin.net/e2c8cb4b372d841061fc231dcdc28a679c244f4f630d41cfe01c8c3aaa3a36f7/rsync/rpki.arin.net/repository/arin-rpki-ta/5e4a23ea-e80a-403e-b08c-2171da2157d3/521eb33f-9672-4cd9-acce-137227e971ac/90dcf2c0-6606-4bd9-b445-77b183ef2ff4/90dcf2c0-6606-4bd9-b445-77b183ef2ff4.mft", + "pack_update_time_rfc3339_utc": "2026-02-04T03:24:29Z", + "manifest_uri": "rsync://rpki.arin.net/repository/arin-rpki-ta/5e4a23ea-e80a-403e-b08c-2171da2157d3/521eb33f-9672-4cd9-acce-137227e971ac/90dcf2c0-6606-4bd9-b445-77b183ef2ff4/90dcf2c0-6606-4bd9-b445-77b183ef2ff4.mft", + "object_uri": "rsync://rpki.arin.net/repository/arin-rpki-ta/5e4a23ea-e80a-403e-b08c-2171da2157d3/521eb33f-9672-4cd9-acce-137227e971ac/90dcf2c0-6606-4bd9-b445-77b183ef2ff4/5fdeae31-baab-3255-a593-abfb66a6396f.roa", + "manifest_this_update_rfc3339_utc": "2026-02-04T02:02:08Z", + "manifest_not_after_rfc3339_utc": "2026-02-06T02:00:00Z", + "metrics": { + "kind": "roa", + "addr_family_count": 1, + "prefix_count": 1, + "max_length_present": 1 + } + }, + { + "obj_type": "roa", + "label": "p99", + "file_rel_path": "tests/benchmark/selected_der_v2/roa/p99.roa", + "size_bytes": 2209, + "sha256_hex": "7bce2c12746b2f665624aa3f192ebdb4fc8cbc4f3d5eed2fa8e2661ad7135f7c", + "pack_rel_path": "rrdp/rrdp.afrinic.net/6a5a58f9bfe26843ee174cd41e801eeecbfd15b8bb0dd7fa3952941d726730b6/rsync/rpki.afrinic.net/repository/member_repository/F368F2D0/7F4A98EA6E0511E89C0D6E4BF8AEA228/JdY-COq-fPpnhdTB1tNBFt4Vs9w.mft", + "pack_update_time_rfc3339_utc": "2026-02-03T03:24:56Z", + "manifest_uri": "rsync://rpki.afrinic.net/repository/member_repository/F368F2D0/7F4A98EA6E0511E89C0D6E4BF8AEA228/JdY-COq-fPpnhdTB1tNBFt4Vs9w.mft", + "object_uri": "rsync://rpki.afrinic.net/repository/member_repository/F368F2D0/7F4A98EA6E0511E89C0D6E4BF8AEA228/E77E611EE6E611F0825B6ED4DAE4EC9C.roa", + "manifest_this_update_rfc3339_utc": "2026-02-03T03:02:20Z", + "manifest_not_after_rfc3339_utc": "2026-02-05T03:02:20Z", + "metrics": { + "kind": "roa", + "addr_family_count": 1, + "prefix_count": 20, + "max_length_present": 20 + } + }, + { + "obj_type": "aspa", + "label": "max", + "file_rel_path": "tests/benchmark/selected_der_v2/aspa/max.asa", + "size_bytes": 2157, + "sha256_hex": "2e1daefad00193eb31dfa0e66706005e3c873fe4f079badf3b5b2c6d10462505", + "pack_rel_path": "rrdp/rrdp.arin.net/e2c8cb4b372d841061fc231dcdc28a679c244f4f630d41cfe01c8c3aaa3a36f7/rsync/rpki.arin.net/repository/arin-rpki-ta/5e4a23ea-e80a-403e-b08c-2171da2157d3/fde169ed-d0d2-4165-8308-df2597e343f8/c0efcdab-fc76-4717-8db9-db07467f9fe0/c0efcdab-fc76-4717-8db9-db07467f9fe0.mft", + "pack_update_time_rfc3339_utc": "2026-02-04T03:22:58Z", + "manifest_uri": "rsync://rpki.arin.net/repository/arin-rpki-ta/5e4a23ea-e80a-403e-b08c-2171da2157d3/fde169ed-d0d2-4165-8308-df2597e343f8/c0efcdab-fc76-4717-8db9-db07467f9fe0/c0efcdab-fc76-4717-8db9-db07467f9fe0.mft", + "object_uri": "rsync://rpki.arin.net/repository/arin-rpki-ta/5e4a23ea-e80a-403e-b08c-2171da2157d3/fde169ed-d0d2-4165-8308-df2597e343f8/c0efcdab-fc76-4717-8db9-db07467f9fe0/053373c0-f494-3063-a255-8d4430bf6133.asa", + "manifest_this_update_rfc3339_utc": "2026-02-03T13:00:09Z", + "manifest_not_after_rfc3339_utc": "2026-02-05T13:00:00Z", + "metrics": { + "kind": "aspa", + "provider_count": 12 + } + }, + { + "obj_type": "aspa", + "label": "min", + "file_rel_path": "tests/benchmark/selected_der_v2/aspa/min.asa", + "size_bytes": 1685, + "sha256_hex": "5a6451ce5349f7483a74d555c44c946c17db8314c6707fc95950bdac282eae58", + "pack_rel_path": "rrdp/rpki.pudu.be/8d06bbb84ab1cf6124ee8f0f6bbdd9c28fc22ede46a3b55e9744a28a80263fe1/rsync/rpki.pudu.be/repo/pudu/1/CF7DC5A4F702D3DC9D56EA35B9EE202EC549647E.mft", + "pack_update_time_rfc3339_utc": "2026-02-03T04:31:47Z", + "manifest_uri": "rsync://rpki.pudu.be/repo/pudu/1/CF7DC5A4F702D3DC9D56EA35B9EE202EC549647E.mft", + "object_uri": "rsync://rpki.pudu.be/repo/pudu/1/AS56762.asa", + "manifest_this_update_rfc3339_utc": "2026-02-03T04:07:32Z", + "manifest_not_after_rfc3339_utc": "2026-02-04T05:17:32Z", + "metrics": { + "kind": "aspa", + "provider_count": 1 + } + }, + { + "obj_type": "aspa", + "label": "p01", + "file_rel_path": "tests/benchmark/selected_der_v2/aspa/p01.asa", + "size_bytes": 1736, + "sha256_hex": "28a98f95d617b77cac316d739a40dcb871306558fef582eaa671cfa4d899eb02", + "pack_rel_path": "rrdp/rpki.roa.net/78e0c5f52b7514947e9ef499c811d15797db7c65ea9051c27b11555439a0c010/rsync/rpki.roa.net/rrdp/xTom/58/5B1AD82F0E7DC771819A9A26674992A3951B9373.mft", + "pack_update_time_rfc3339_utc": "2026-02-03T02:26:07Z", + "manifest_uri": "rsync://rpki.roa.net/rrdp/xTom/58/5B1AD82F0E7DC771819A9A26674992A3951B9373.mft", + "object_uri": "rsync://rpki.roa.net/rrdp/xTom/58/AS138038.asa", + "manifest_this_update_rfc3339_utc": "2026-02-03T02:13:19Z", + "manifest_not_after_rfc3339_utc": "2026-02-04T05:43:19Z", + "metrics": { + "kind": "aspa", + "provider_count": 1 + } + }, + { + "obj_type": "aspa", + "label": "p10", + "file_rel_path": "tests/benchmark/selected_der_v2/aspa/p10.asa", + "size_bytes": 1789, + "sha256_hex": "857a667b95f8962e94b19425d65e00fee0a2e2886b9408e94a644873f6bcb20c", + "pack_rel_path": "rrdp/rrdp.ripe.net/2315d99a99627f34bc597569abc7c177ad45108a37f173f3d06dbabd64962f3c/rsync/rpki.ripe.net/repository/DEFAULT/31/de9d72-fcd4-4c7c-89ca-de4ba786f872/1/3l5sIzCRG8HcjIPUEe9rEgizu9Y.mft", + "pack_update_time_rfc3339_utc": "2026-02-03T04:30:59Z", + "manifest_uri": "rsync://rpki.ripe.net/repository/DEFAULT/31/de9d72-fcd4-4c7c-89ca-de4ba786f872/1/3l5sIzCRG8HcjIPUEe9rEgizu9Y.mft", + "object_uri": "rsync://rpki.ripe.net/repository/DEFAULT/31/de9d72-fcd4-4c7c-89ca-de4ba786f872/1/G0H1NZGAbtsyoA2e--eIMeks25w.asa", + "manifest_this_update_rfc3339_utc": "2026-02-03T04:00:42Z", + "manifest_not_after_rfc3339_utc": "2026-02-04T04:00:42Z", + "metrics": { + "kind": "aspa", + "provider_count": 1 + } + }, + { + "obj_type": "aspa", + "label": "p25", + "file_rel_path": "tests/benchmark/selected_der_v2/aspa/p25.asa", + "size_bytes": 1796, + "sha256_hex": "512afb7f399ac0559b88bd6a0bc6f369f9b6ad3f3e46aa1cd9e067087cf10cc9", + "pack_rel_path": "rrdp/rrdp.ripe.net/2315d99a99627f34bc597569abc7c177ad45108a37f173f3d06dbabd64962f3c/rsync/rpki.ripe.net/repository/DEFAULT/46/cadd6e-65c6-44da-9ee9-efea65dc3020/1/1-EAlIIyFaM6OWN4a59g--NKydCQ.mft", + "pack_update_time_rfc3339_utc": "2026-02-03T04:30:18Z", + "manifest_uri": "rsync://rpki.ripe.net/repository/DEFAULT/46/cadd6e-65c6-44da-9ee9-efea65dc3020/1/1-EAlIIyFaM6OWN4a59g--NKydCQ.mft", + "object_uri": "rsync://rpki.ripe.net/repository/DEFAULT/46/cadd6e-65c6-44da-9ee9-efea65dc3020/1/BNbYCTwC4UEQcd5nV0lyvO8_FF4.asa", + "manifest_this_update_rfc3339_utc": "2026-02-03T04:00:31Z", + "manifest_not_after_rfc3339_utc": "2026-02-04T04:00:31Z", + "metrics": { + "kind": "aspa", + "provider_count": 2 + } + }, + { + "obj_type": "aspa", + "label": "p50", + "file_rel_path": "tests/benchmark/selected_der_v2/aspa/p50.asa", + "size_bytes": 1856, + "sha256_hex": "2895646ffad9bac7df603e6bee1f535a68154d0d39735819de8a1f2d9eea6d06", + "pack_rel_path": "rrdp/rrdp.paas.rpki.ripe.net/bcd30c8fe6a4120dcd88ad3eeb82a7ea26f8ecce57fe1d5cb45e5e34c3a94a9b/rsync/rsync.paas.rpki.ripe.net/repository/97b5f6f4-24be-4ae0-82a8-e5d82842e229/0/A0BBB954570CFA6E856937043DD2C1745A0A05C2.mft", + "pack_update_time_rfc3339_utc": "2026-02-03T03:27:39Z", + "manifest_uri": "rsync://rsync.paas.rpki.ripe.net/repository/97b5f6f4-24be-4ae0-82a8-e5d82842e229/0/A0BBB954570CFA6E856937043DD2C1745A0A05C2.mft", + "object_uri": "rsync://rsync.paas.rpki.ripe.net/repository/97b5f6f4-24be-4ae0-82a8-e5d82842e229/0/AS214757.asa", + "manifest_this_update_rfc3339_utc": "2026-02-03T03:09:43Z", + "manifest_not_after_rfc3339_utc": "2026-02-04T05:59:43Z", + "metrics": { + "kind": "aspa", + "provider_count": 14 + } + }, + { + "obj_type": "aspa", + "label": "p75", + "file_rel_path": "tests/benchmark/selected_der_v2/aspa/p75.asa", + "size_bytes": 2116, + "sha256_hex": "e9ea6e871e0e435a5a3b7a019fd9f8761628504668c9ba0a744ad7f337fe4825", + "pack_rel_path": "rrdp/rrdp.arin.net/e2c8cb4b372d841061fc231dcdc28a679c244f4f630d41cfe01c8c3aaa3a36f7/rsync/rpki.arin.net/repository/arin-rpki-ta/5e4a23ea-e80a-403e-b08c-2171da2157d3/0357272c-a79a-45bf-9586-92dd49ef3223/59adcb6d-cc06-4e2c-b5df-20bfcd83176e/59adcb6d-cc06-4e2c-b5df-20bfcd83176e.mft", + "pack_update_time_rfc3339_utc": "2026-02-04T03:24:35Z", + "manifest_uri": "rsync://rpki.arin.net/repository/arin-rpki-ta/5e4a23ea-e80a-403e-b08c-2171da2157d3/0357272c-a79a-45bf-9586-92dd49ef3223/59adcb6d-cc06-4e2c-b5df-20bfcd83176e/59adcb6d-cc06-4e2c-b5df-20bfcd83176e.mft", + "object_uri": "rsync://rpki.arin.net/repository/arin-rpki-ta/5e4a23ea-e80a-403e-b08c-2171da2157d3/0357272c-a79a-45bf-9586-92dd49ef3223/59adcb6d-cc06-4e2c-b5df-20bfcd83176e/8314341a-405c-33d6-ad94-3e8c6d770291.asa", + "manifest_this_update_rfc3339_utc": "2026-02-03T19:00:04Z", + "manifest_not_after_rfc3339_utc": "2026-02-06T19:00:00Z", + "metrics": { + "kind": "aspa", + "provider_count": 2 + } + }, + { + "obj_type": "aspa", + "label": "p90", + "file_rel_path": "tests/benchmark/selected_der_v2/aspa/p90.asa", + "size_bytes": 2124, + "sha256_hex": "e1c33906ef3bdbec47fe0cc7c9fc163d427234b4f5581895dc5279df7f278614", + "pack_rel_path": "rrdp/rrdp.arin.net/e2c8cb4b372d841061fc231dcdc28a679c244f4f630d41cfe01c8c3aaa3a36f7/rsync/rpki.arin.net/repository/arin-rpki-ta/5e4a23ea-e80a-403e-b08c-2171da2157d3/746e0111-fafb-430f-b778-d204cfcd99a8/e7060a03-da54-4766-9b77-667ebbb66ffe/e7060a03-da54-4766-9b77-667ebbb66ffe.mft", + "pack_update_time_rfc3339_utc": "2026-02-04T03:23:06Z", + "manifest_uri": "rsync://rpki.arin.net/repository/arin-rpki-ta/5e4a23ea-e80a-403e-b08c-2171da2157d3/746e0111-fafb-430f-b778-d204cfcd99a8/e7060a03-da54-4766-9b77-667ebbb66ffe/e7060a03-da54-4766-9b77-667ebbb66ffe.mft", + "object_uri": "rsync://rpki.arin.net/repository/arin-rpki-ta/5e4a23ea-e80a-403e-b08c-2171da2157d3/746e0111-fafb-430f-b778-d204cfcd99a8/e7060a03-da54-4766-9b77-667ebbb66ffe/4f4bee4f-0c5a-38c0-a421-5b81029d9452.asa", + "manifest_this_update_rfc3339_utc": "2026-02-03T19:00:04Z", + "manifest_not_after_rfc3339_utc": "2026-02-05T19:00:00Z", + "metrics": { + "kind": "aspa", + "provider_count": 3 + } + }, + { + "obj_type": "aspa", + "label": "p95", + "file_rel_path": "tests/benchmark/selected_der_v2/aspa/p95.asa", + "size_bytes": 2131, + "sha256_hex": "87f9d2611bda2851a348619505c8b872bd34c26ce1ddfc46cb5d21bc8d055e5d", + "pack_rel_path": "rrdp/rrdp.arin.net/e2c8cb4b372d841061fc231dcdc28a679c244f4f630d41cfe01c8c3aaa3a36f7/rsync/rpki.arin.net/repository/arin-rpki-ta/5e4a23ea-e80a-403e-b08c-2171da2157d3/f60c9f32-a87c-4339-a2f3-6299a3b02e29/f2f0585e-a28e-4ead-9b1c-defd291ce54d/f2f0585e-a28e-4ead-9b1c-defd291ce54d.mft", + "pack_update_time_rfc3339_utc": "2026-02-04T03:24:59Z", + "manifest_uri": "rsync://rpki.arin.net/repository/arin-rpki-ta/5e4a23ea-e80a-403e-b08c-2171da2157d3/f60c9f32-a87c-4339-a2f3-6299a3b02e29/f2f0585e-a28e-4ead-9b1c-defd291ce54d/f2f0585e-a28e-4ead-9b1c-defd291ce54d.mft", + "object_uri": "rsync://rpki.arin.net/repository/arin-rpki-ta/5e4a23ea-e80a-403e-b08c-2171da2157d3/f60c9f32-a87c-4339-a2f3-6299a3b02e29/f2f0585e-a28e-4ead-9b1c-defd291ce54d/3dfa4a5a-70d2-39a3-8e69-18afc586e9ec.asa", + "manifest_this_update_rfc3339_utc": "2026-02-03T13:00:09Z", + "manifest_not_after_rfc3339_utc": "2026-02-06T13:00:00Z", + "metrics": { + "kind": "aspa", + "provider_count": 5 + } + }, + { + "obj_type": "aspa", + "label": "p99", + "file_rel_path": "tests/benchmark/selected_der_v2/aspa/p99.asa", + "size_bytes": 2147, + "sha256_hex": "8a7e34f96cbb16a3dce913dbf38f1f3fa806130100f6d38cc0b7a399f1a77e83", + "pack_rel_path": "rrdp/rrdp.arin.net/e2c8cb4b372d841061fc231dcdc28a679c244f4f630d41cfe01c8c3aaa3a36f7/rsync/rpki.arin.net/repository/arin-rpki-ta/5e4a23ea-e80a-403e-b08c-2171da2157d3/d9d1572f-6cbb-4cf7-b599-e9d0e981d9bf/4ee37184-86de-48c9-af35-5d102d140fa6/4ee37184-86de-48c9-af35-5d102d140fa6.mft", + "pack_update_time_rfc3339_utc": "2026-02-04T03:24:17Z", + "manifest_uri": "rsync://rpki.arin.net/repository/arin-rpki-ta/5e4a23ea-e80a-403e-b08c-2171da2157d3/d9d1572f-6cbb-4cf7-b599-e9d0e981d9bf/4ee37184-86de-48c9-af35-5d102d140fa6/4ee37184-86de-48c9-af35-5d102d140fa6.mft", + "object_uri": "rsync://rpki.arin.net/repository/arin-rpki-ta/5e4a23ea-e80a-403e-b08c-2171da2157d3/d9d1572f-6cbb-4cf7-b599-e9d0e981d9bf/4ee37184-86de-48c9-af35-5d102d140fa6/c00a162c-b8fa-3da5-8257-7074076c1ed6.asa", + "manifest_this_update_rfc3339_utc": "2026-02-04T03:00:08Z", + "manifest_not_after_rfc3339_utc": "2026-02-07T03:00:00Z", + "metrics": { + "kind": "aspa", + "provider_count": 8 + } + } + ] +} \ No newline at end of file diff --git a/tests/benchmark/selected_der_v2/roa/max.roa b/tests/benchmark/selected_der_v2/roa/max.roa new file mode 100644 index 0000000..7261133 Binary files /dev/null and b/tests/benchmark/selected_der_v2/roa/max.roa differ diff --git a/tests/benchmark/selected_der_v2/roa/min.roa b/tests/benchmark/selected_der_v2/roa/min.roa new file mode 100644 index 0000000..bee24c3 Binary files /dev/null and b/tests/benchmark/selected_der_v2/roa/min.roa differ diff --git a/tests/benchmark/selected_der_v2/roa/p01.roa b/tests/benchmark/selected_der_v2/roa/p01.roa new file mode 100644 index 0000000..9711f74 Binary files /dev/null and b/tests/benchmark/selected_der_v2/roa/p01.roa differ diff --git a/tests/benchmark/selected_der_v2/roa/p10.roa b/tests/benchmark/selected_der_v2/roa/p10.roa new file mode 100644 index 0000000..9b752c7 Binary files /dev/null and b/tests/benchmark/selected_der_v2/roa/p10.roa differ diff --git a/tests/benchmark/selected_der_v2/roa/p25.roa b/tests/benchmark/selected_der_v2/roa/p25.roa new file mode 100644 index 0000000..008239a Binary files /dev/null and b/tests/benchmark/selected_der_v2/roa/p25.roa differ diff --git a/tests/benchmark/selected_der_v2/roa/p50.roa b/tests/benchmark/selected_der_v2/roa/p50.roa new file mode 100644 index 0000000..15f8781 Binary files /dev/null and b/tests/benchmark/selected_der_v2/roa/p50.roa differ diff --git a/tests/benchmark/selected_der_v2/roa/p75.roa b/tests/benchmark/selected_der_v2/roa/p75.roa new file mode 100644 index 0000000..1e71941 Binary files /dev/null and b/tests/benchmark/selected_der_v2/roa/p75.roa differ diff --git a/tests/benchmark/selected_der_v2/roa/p90.roa b/tests/benchmark/selected_der_v2/roa/p90.roa new file mode 100644 index 0000000..1d337e8 Binary files /dev/null and b/tests/benchmark/selected_der_v2/roa/p90.roa differ diff --git a/tests/benchmark/selected_der_v2/roa/p95.roa b/tests/benchmark/selected_der_v2/roa/p95.roa new file mode 100644 index 0000000..b8188b1 Binary files /dev/null and b/tests/benchmark/selected_der_v2/roa/p95.roa differ diff --git a/tests/benchmark/selected_der_v2/roa/p99.roa b/tests/benchmark/selected_der_v2/roa/p99.roa new file mode 100644 index 0000000..61eb523 Binary files /dev/null and b/tests/benchmark/selected_der_v2/roa/p99.roa differ diff --git a/tests/test_aspa_validate_ee_resources.rs b/tests/test_aspa_validate_ee_resources.rs index 0cf6607..3c8feb6 100644 --- a/tests/test_aspa_validate_ee_resources.rs +++ b/tests/test_aspa_validate_ee_resources.rs @@ -2,6 +2,7 @@ use der_parser::num_bigint::BigUint; use time::OffsetDateTime; use rpki::data_model::aspa::{AspaEContent, AspaValidateError}; +use rpki::data_model::common::X509NameDer; use rpki::data_model::rc::{ AsIdOrRange, AsIdentifierChoice, AsResourceSet, IpResourceSet, RcExtensions, ResourceCertKind, ResourceCertificate, RpkixTbsCertificate, SubjectInfoAccess, @@ -17,8 +18,8 @@ fn dummy_ee( version: 2, serial_number: BigUint::from(1u8), signature_algorithm: "1.2.840.113549.1.1.11".to_string(), - issuer_dn: "CN=issuer".to_string(), - subject_dn: "CN=subject".to_string(), + issuer_name: X509NameDer(b"CN=issuer".to_vec()), + subject_name: X509NameDer(b"CN=subject".to_vec()), validity_not_before: OffsetDateTime::UNIX_EPOCH, validity_not_after: OffsetDateTime::UNIX_EPOCH, subject_public_key_info: vec![], diff --git a/tests/test_model_print_real_fixtures.rs b/tests/test_model_print_real_fixtures.rs index ca19e3e..47aecc9 100644 --- a/tests/test_model_print_real_fixtures.rs +++ b/tests/test_model_print_real_fixtures.rs @@ -127,8 +127,8 @@ impl From<&RpkixTbsCertificate> for RpkixTbsCertificatePretty { version: v.version, serial_number: hex::encode(v.serial_number.to_bytes_be()), signature_algorithm: v.signature_algorithm.clone(), - issuer_dn: v.issuer_dn.clone(), - subject_dn: v.subject_dn.clone(), + issuer_dn: v.issuer_name.to_string(), + subject_dn: v.subject_name.to_string(), validity_not_before: v.validity_not_before, validity_not_after: v.validity_not_after, subject_public_key_info: bytes_fmt(&v.subject_public_key_info), diff --git a/tests/test_roa_canonicalize.rs b/tests/test_roa_canonicalize.rs index 147f653..18e0c2a 100644 --- a/tests/test_roa_canonicalize.rs +++ b/tests/test_roa_canonicalize.rs @@ -120,7 +120,7 @@ fn canonicalize_sorts_families_sorts_and_dedups_addresses() { IpPrefix { afi: RoaAfi::Ipv4, prefix_len: 24, - addr: vec![192, 0, 2, 0], + addr: [192, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], } ); } diff --git a/tests/test_roa_validate_ee_resources.rs b/tests/test_roa_validate_ee_resources.rs index 2941dfd..c6f9cd2 100644 --- a/tests/test_roa_validate_ee_resources.rs +++ b/tests/test_roa_validate_ee_resources.rs @@ -1,6 +1,7 @@ use der_parser::num_bigint::BigUint; use time::OffsetDateTime; +use rpki::data_model::common::X509NameDer; use rpki::data_model::rc::{ Afi, AsResourceSet, IpAddressChoice, IpAddressFamily, IpAddressOrRange, IpPrefix, IpResourceSet, RcExtensions, ResourceCertKind, ResourceCertificate, RpkixTbsCertificate, @@ -20,8 +21,8 @@ fn dummy_ee( version: 2, serial_number: BigUint::from(1u8), signature_algorithm: "1.2.840.113549.1.1.11".to_string(), - issuer_dn: "CN=issuer".to_string(), - subject_dn: "CN=subject".to_string(), + issuer_name: X509NameDer(b"CN=issuer".to_vec()), + subject_name: X509NameDer(b"CN=subject".to_vec()), validity_not_before: OffsetDateTime::UNIX_EPOCH, validity_not_after: OffsetDateTime::UNIX_EPOCH, subject_public_key_info: vec![], @@ -55,7 +56,7 @@ fn test_roa_single_v4_prefix() -> RoaEContent { prefix: rpki::data_model::roa::IpPrefix { afi: RoaAfi::Ipv4, prefix_len: 8, - addr: vec![10, 0, 0, 0], + addr: [10, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], }, max_length: Some(24), }], @@ -159,7 +160,24 @@ fn contains_prefix_handles_non_octet_boundary_prefix_len() { prefix: rpki::data_model::roa::IpPrefix { afi: RoaAfi::Ipv4, prefix_len: 16, - addr: vec![0b1010_0000, 0x12, 0, 0], // 160.18.0.0/16 + addr: [ + 0b1010_0000, + 0x12, + 0, + 0, // 160.18.0.0/16 + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + ], }, max_length: None, }], diff --git a/tests/test_ta_certificate.rs b/tests/test_ta_certificate.rs index 0d192f5..491b6b9 100644 --- a/tests/test_ta_certificate.rs +++ b/tests/test_ta_certificate.rs @@ -1,4 +1,5 @@ use der_parser::num_bigint::BigUint; +use rpki::data_model::common::X509NameDer; use rpki::data_model::oid::OID_CP_IPADDR_ASNUMBER; use rpki::data_model::rc::{ Afi, AsIdOrRange, AsIdentifierChoice, AsResourceSet, IpAddressChoice, IpAddressFamily, @@ -17,8 +18,8 @@ fn dummy_rc_ca(ext: RcExtensions) -> ResourceCertificate { version: 2, serial_number: BigUint::from(1u32), signature_algorithm: "1.2.840.113549.1.1.11".into(), - issuer_dn: "CN=TA".into(), - subject_dn: "CN=TA".into(), + issuer_name: X509NameDer(b"CN=TA".to_vec()), + subject_name: X509NameDer(b"CN=TA".to_vec()), validity_not_before: t, validity_not_after: t, subject_public_key_info: Vec::new(), diff --git a/tests/test_uncovered_lines_fillers.rs b/tests/test_uncovered_lines_fillers.rs index 1270c39..771d921 100644 --- a/tests/test_uncovered_lines_fillers.rs +++ b/tests/test_uncovered_lines_fillers.rs @@ -107,23 +107,16 @@ fn audit_helpers_format_roa_ip_prefix_smoke() { let v4 = IpPrefix { afi: RoaAfi::Ipv4, prefix_len: 24, - addr: vec![192, 0, 2, 0], + addr: [192, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 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], + addr: [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]