优化数据对象decode + profile validate;benchmark对比routinator geomean 0.8

This commit is contained in:
yuyr 2026-02-26 17:01:51 +08:00
parent 1cc3351bef
commit 68cbd3c500
82 changed files with 5869 additions and 609 deletions

View File

@ -9,6 +9,7 @@ default = ["full"]
full = ["dep:rocksdb"] full = ["dep:rocksdb"]
[dependencies] [dependencies]
asn1-rs = "0.7.1"
der-parser = { version = "10.0.0", features = ["serialize"] } der-parser = { version = "10.0.0", features = ["serialize"] }
hex = "0.4.3" hex = "0.4.3"
base64 = "0.22.1" base64 = "0.22.1"

View File

@ -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"] }

View File

@ -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<Self, String> {
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<ObjType>,
sample_filter: Option<String>,
fixed_iters: Option<u64>,
warmup_iters: u64,
rounds: u64,
min_round_ms: u64,
max_adaptive_iters: u64,
strict: bool,
cert_inspect: bool,
out_csv: Option<PathBuf>,
out_md: Option<PathBuf>,
}
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 <PATH> Fixtures root dir (default: ../../tests/benchmark/selected_der_v2)\n\
--type <cer|crl|manifest|roa|aspa> Filter by type\n\
--sample <NAME> Filter by sample name (e.g. p50)\n\
--iters <N> Fixed iterations per round (optional; otherwise adaptive)\n\
--warmup-iters <N> Warmup iterations (default: 50)\n\
--rounds <N> Rounds (default: 5)\n\
--min-round-ms <MS> Adaptive: minimum round time (default: 200)\n\
--max-iters <N> Adaptive: maximum iters (default: 1_000_000)\n\
--strict <true|false> Strict DER where applicable (default: true)\n\
--cert-inspect Also run Cert::inspect_ca/inspect_ee where applicable (default: false)\n\
--out-csv <PATH> Write CSV output\n\
--out-md <PATH> 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::<u64>()
.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<ObjType> = None;
let mut sample_filter: Option<String> = None;
let mut fixed_iters: Option<u64> = 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<PathBuf> = None;
let mut out_md: Option<PathBuf> = 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<Sample> {
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<F: FnMut()>(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<ResultRow> = 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::<f64>() / (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());
}
}

123
scripts/stage2_perf_compare_m4.sh Executable file
View File

@ -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

View File

@ -4601,6 +4601,925 @@
"startArrowhead": null, "startArrowhead": null,
"endArrowhead": "arrow", "endArrowhead": "arrow",
"elbowed": false "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": { "appState": {

View File

@ -133,28 +133,23 @@ pub fn sha256_hex(bytes: &[u8]) -> String {
} }
pub fn format_roa_ip_prefix(p: &crate::data_model::roa::IpPrefix) -> String { pub fn format_roa_ip_prefix(p: &crate::data_model::roa::IpPrefix) -> String {
let addr = p.addr_bytes();
match p.afi { match p.afi {
crate::data_model::roa::RoaAfi::Ipv4 => { crate::data_model::roa::RoaAfi::Ipv4 => {
if p.addr.len() != 4 {
return format!("ipv4:{:02X?}/{}", p.addr, p.prefix_len);
}
format!( format!(
"{}.{}.{}.{}{}", "{}.{}.{}.{}{}",
p.addr[0], addr[0],
p.addr[1], addr[1],
p.addr[2], addr[2],
p.addr[3], addr[3],
format!("/{}", p.prefix_len) format!("/{}", p.prefix_len)
) )
} }
crate::data_model::roa::RoaAfi::Ipv6 => { 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); let mut parts = Vec::with_capacity(8);
for i in 0..8 { for i in 0..8 {
let hi = p.addr[i * 2] as u16; let hi = addr[i * 2] as u16;
let lo = p.addr[i * 2 + 1] as u16; let lo = addr[i * 2 + 1] as u16;
parts.push(format!("{:x}", (hi << 8) | lo)); parts.push(format!("{:x}", (hi << 8) | lo));
} }
format!("{}{}", parts.join(":"), format!("/{}", p.prefix_len)) format!("{}{}", parts.join(":"), format!("/{}", p.prefix_len))

View File

@ -612,7 +612,7 @@ mod tests {
prefix: crate::data_model::roa::IpPrefix { prefix: crate::data_model::roa::IpPrefix {
afi: crate::data_model::roa::RoaAfi::Ipv4, afi: crate::data_model::roa::RoaAfi::Ipv4,
prefix_len: 24, 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, max_length: 24,
}], }],

View File

@ -1,10 +1,9 @@
use crate::data_model::oid::OID_CT_ASPA; 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::rc::ResourceCertificate;
use crate::data_model::signed_object::{ use crate::data_model::signed_object::{
RpkiSignedObject, RpkiSignedObjectParsed, SignedObjectParseError, SignedObjectValidateError, RpkiSignedObject, RpkiSignedObjectParsed, SignedObjectParseError, SignedObjectValidateError,
}; };
use der_parser::ber::Class;
use der_parser::der::{DerObject, Tag, parse_der};
#[derive(Clone, Debug, PartialEq, Eq)] #[derive(Clone, Debug, PartialEq, Eq)]
pub struct AspaObject { pub struct AspaObject {
@ -203,7 +202,7 @@ impl AspaObject {
impl AspaEContent { impl AspaEContent {
/// Parse step of scheme A (`parse → validate → verify`). /// Parse step of scheme A (`parse → validate → verify`).
pub fn parse_der(der: &[u8]) -> Result<AspaEContentParsed, AspaParseError> { pub fn parse_der(der: &[u8]) -> Result<AspaEContentParsed, AspaParseError> {
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() { if !rem.is_empty() {
return Err(AspaParseError::TrailingBytes(rem.len())); return Err(AspaParseError::TrailingBytes(rem.len()));
} }
@ -292,47 +291,66 @@ impl AspaObjectParsed {
impl AspaEContentParsed { impl AspaEContentParsed {
pub fn validate_profile(self) -> Result<AspaEContent, AspaProfileError> { pub fn validate_profile(self) -> Result<AspaEContent, AspaProfileError> {
let (_rem, obj) = fn count_elements(mut r: DerReader<'_>) -> Result<usize, String> {
parse_der(&self.der).map_err(|e| AspaProfileError::ProfileDecode(e.to_string()))?; let mut n = 0usize;
while !r.is_empty() {
r.skip_any()?;
n += 1;
}
Ok(n)
}
let seq = obj let mut r = DerReader::new(&self.der);
.as_sequence() let mut seq = r
.map_err(|e| AspaProfileError::ProfileDecode(e.to_string()))?; .take_sequence()
if seq.len() != 3 { .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); return Err(AspaProfileError::InvalidAttestationSequence);
} }
// version [0] EXPLICIT INTEGER MUST be present and MUST be 1. // version [0] EXPLICIT INTEGER MUST be present and MUST be 1.
let v_obj = &seq[0]; if seq
if v_obj.class() != Class::ContextSpecific || v_obj.tag() != Tag(0) { .peek_tag()
.map_err(|e| AspaProfileError::ProfileDecode(e.to_string()))?
!= 0xA0
{
return Err(AspaProfileError::VersionMustBeExplicitOne); return Err(AspaProfileError::VersionMustBeExplicitOne);
} }
let inner_der = v_obj let (inner_tag, inner_val) = seq
.as_slice() .take_explicit(0xA0)
.map_err(|e| AspaProfileError::ProfileDecode(e.to_string()))?; .map_err(|e| AspaProfileError::ProfileDecode(e.to_string()))?;
let (rem, inner) = if inner_tag != 0x02 {
parse_der(inner_der).map_err(|e| AspaProfileError::ProfileDecode(e.to_string()))?; return Err(AspaProfileError::VersionMustBeExplicitOne);
if !rem.is_empty() {
return Err(AspaProfileError::ProfileDecode(
"trailing bytes inside ASProviderAttestation.version".into(),
));
} }
let v = inner let v = crate::data_model::common::der_uint_from_bytes(inner_val)
.as_u64()
.map_err(|e| AspaProfileError::ProfileDecode(e.to_string()))?; .map_err(|e| AspaProfileError::ProfileDecode(e.to_string()))?;
if v != 1 { if v != 1 {
return Err(AspaProfileError::VersionMustBeExplicitOne); return Err(AspaProfileError::VersionMustBeExplicitOne);
} }
let customer_u64 = seq[1] let customer_u64 = seq
.as_u64() .take_uint_u64()
.map_err(|e| AspaProfileError::ProfileDecode(e.to_string()))?; .map_err(|e| AspaProfileError::ProfileDecode(e.to_string()))?;
if customer_u64 > u32::MAX as u64 { if customer_u64 > u32::MAX as u64 {
return Err(AspaProfileError::CustomerAsIdOutOfRange(customer_u64)); return Err(AspaProfileError::CustomerAsIdOutOfRange(customer_u64));
} }
let customer_as_id = customer_u64 as u32; 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 { Ok(AspaEContent {
version: 1, version: 1,
@ -342,19 +360,19 @@ impl AspaEContentParsed {
} }
} }
fn parse_providers(obj: &DerObject<'_>, customer_as_id: u32) -> Result<Vec<u32>, AspaProfileError> { fn parse_providers_cursor(
let seq = obj mut seq: DerReader<'_>,
.as_sequence() customer_as_id: u32,
.map_err(|e| AspaProfileError::ProfileDecode(e.to_string()))?; ) -> Result<Vec<u32>, AspaProfileError> {
if seq.is_empty() { if seq.is_empty() {
return Err(AspaProfileError::EmptyProviders); return Err(AspaProfileError::EmptyProviders);
} }
let mut out: Vec<u32> = Vec::with_capacity(seq.len()); let mut out: Vec<u32> = Vec::new();
let mut prev: Option<u32> = None; let mut prev: Option<u32> = None;
for item in seq { while !seq.is_empty() {
let v = item let v = seq
.as_u64() .take_uint_u64()
.map_err(|e| AspaProfileError::ProfileDecode(e.to_string()))?; .map_err(|e| AspaProfileError::ProfileDecode(e.to_string()))?;
if v > u32::MAX as u64 { if v > u32::MAX as u64 {
return Err(AspaProfileError::ProviderAsIdOutOfRange(v)); return Err(AspaProfileError::ProviderAsIdOutOfRange(v));

View File

@ -1,5 +1,6 @@
use x509_parser::asn1_rs::Tag; use x509_parser::asn1_rs::Tag;
use x509_parser::x509::AlgorithmIdentifier; use x509_parser::x509::AlgorithmIdentifier;
use x509_parser::prelude::FromDer;
pub type UtcTime = time::OffsetDateTime; 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<u8, String> {
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<DerReader<'a>, 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<u64, String> {
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<u64, String> {
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<u8>);
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, "<invalid X.509 Name DER>");
};
if !rem.is_empty() {
return write!(f, "<invalid X.509 Name DER (trailing bytes)>");
}
write!(f, "{name}")
}
}
/// Filename extensions registered in IANA "RPKI Repository Name Schemes". /// Filename extensions registered in IANA "RPKI Repository Name Schemes".
/// ///
/// Source: <https://www.iana.org/assignments/rpki/rpki.xhtml> /// Source: <https://www.iana.org/assignments/rpki/rpki.xhtml>

View File

@ -1,7 +1,8 @@
pub use crate::data_model::common::{Asn1TimeEncoding, Asn1TimeUtc, BigUnsigned}; pub use crate::data_model::common::{Asn1TimeEncoding, Asn1TimeUtc, BigUnsigned};
use crate::data_model::oid::{ use crate::data_model::oid::{
OID_AUTHORITY_KEY_IDENTIFIER, OID_CRL_NUMBER, OID_SHA256_WITH_RSA_ENCRYPTION, OID_AUTHORITY_KEY_IDENTIFIER, OID_AUTHORITY_KEY_IDENTIFIER_RAW, OID_CRL_NUMBER,
OID_SUBJECT_KEY_IDENTIFIER, 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::certificate::X509Certificate;
use x509_parser::extensions::{ParsedExtension, X509Extension}; use x509_parser::extensions::{ParsedExtension, X509Extension};
@ -425,8 +426,15 @@ fn algorithm_identifier_value(ai: &AlgorithmIdentifier<'_>) -> AlgorithmIdentifi
tag: p.tag(), tag: p.tag(),
data: p.as_bytes().to_vec(), 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 { AlgorithmIdentifierValue {
oid: ai.algorithm.to_id_string(), oid,
parameters, parameters,
} }
} }
@ -434,9 +442,8 @@ fn algorithm_identifier_value(ai: &AlgorithmIdentifier<'_>) -> AlgorithmIdentifi
fn parse_extensions_parse(exts: &[X509Extension<'_>]) -> Result<Vec<CrlExtensionParsed>, String> { fn parse_extensions_parse(exts: &[X509Extension<'_>]) -> Result<Vec<CrlExtensionParsed>, String> {
let mut out = Vec::with_capacity(exts.len()); let mut out = Vec::with_capacity(exts.len());
for ext in exts { for ext in exts {
let oid = ext.oid.to_id_string(); let oid = ext.oid.as_bytes();
match oid.as_str() { if oid == OID_AUTHORITY_KEY_IDENTIFIER_RAW {
OID_AUTHORITY_KEY_IDENTIFIER => {
let ParsedExtension::AuthorityKeyIdentifier(aki) = ext.parsed_extension() else { let ParsedExtension::AuthorityKeyIdentifier(aki) = ext.parsed_extension() else {
return Err("AKI extension parse failed".to_string()); return Err("AKI extension parse failed".to_string());
}; };
@ -446,18 +453,19 @@ fn parse_extensions_parse(exts: &[X509Extension<'_>]) -> Result<Vec<CrlExtension
|| aki.authority_cert_serial.is_some(), || aki.authority_cert_serial.is_some(),
critical: ext.critical, critical: ext.critical,
}); });
} } else if oid == OID_CRL_NUMBER_RAW {
OID_CRL_NUMBER => match ext.parsed_extension() { match ext.parsed_extension() {
ParsedExtension::CRLNumber(n) => out.push(CrlExtensionParsed::CrlNumber { ParsedExtension::CRLNumber(n) => out.push(CrlExtensionParsed::CrlNumber {
number: n.clone(), number: n.clone(),
critical: ext.critical, critical: ext.critical,
}), }),
_ => return Err("CRLNumber extension parse failed".to_string()), _ => return Err("CRLNumber extension parse failed".to_string()),
}, }
_ => out.push(CrlExtensionParsed::Other { } else {
oid, out.push(CrlExtensionParsed::Other {
oid: ext.oid.to_id_string(),
critical: ext.critical, critical: ext.critical,
}), })
} }
} }
Ok(out) Ok(out)
@ -525,7 +533,7 @@ fn validate_extensions_profile(
fn get_subject_key_identifier(cert: &X509Certificate<'_>) -> Option<Vec<u8>> { fn get_subject_key_identifier(cert: &X509Certificate<'_>) -> Option<Vec<u8>> {
cert.extensions() cert.extensions()
.iter() .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() { .and_then(|ext| match ext.parsed_extension() {
ParsedExtension::SubjectKeyIdentifier(ki) => Some(ki.0.to_vec()), ParsedExtension::SubjectKeyIdentifier(ki) => Some(ki.0.to_vec()),
_ => None, _ => None,

View File

@ -1,4 +1,5 @@
use crate::data_model::common::{BigUnsigned, UtcTime}; 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::oid::{OID_CT_RPKI_MANIFEST, OID_SHA256};
use crate::data_model::rc::ResourceCertificate; use crate::data_model::rc::ResourceCertificate;
use crate::data_model::signed_object::{ use crate::data_model::signed_object::{
@ -821,42 +822,6 @@ fn oid_content_iter(bytes: &[u8]) -> impl Iterator<Item = u64> + '_ {
} }
} }
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)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;

View File

@ -1,42 +1,72 @@
pub const OID_SHA256: &str = "2.16.840.1.101.3.4.2.1"; 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: &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: &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: &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: &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: &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: &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) // X.509 extensions (RFC 5280 / RFC 6487)
pub const OID_BASIC_CONSTRAINTS: &str = "2.5.29.19"; 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: &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: &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: &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: &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: &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: &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: &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: &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: &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: &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: &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) // 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: &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: &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: &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: &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: &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: &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) // 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: &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: &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) // 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: &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);

View File

@ -1,19 +1,21 @@
use der_parser::ber::{BerObjectContent, Class}; use der_parser::ber::{BerObjectContent, Class};
use der_parser::der::{DerObject, Tag, parse_der}; use der_parser::der::{DerObject, Tag, parse_der};
use der_parser::num_bigint::BigUint; use der_parser::num_bigint::BigUint;
use url::Url;
use x509_parser::asn1_rs::{Class as Asn1Class, Tag as Asn1Tag}; use x509_parser::asn1_rs::{Class as Asn1Class, Tag as Asn1Tag};
use x509_parser::extensions::ParsedExtension; use x509_parser::extensions::ParsedExtension;
use x509_parser::prelude::{FromDer, X509Certificate, X509Extension, X509Version}; use x509_parser::prelude::{FromDer, X509Certificate, X509Extension, X509Version};
use crate::data_model::common::{ 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::{ use crate::data_model::oid::{
OID_AD_CA_ISSUERS, OID_AD_SIGNED_OBJECT, OID_AUTHORITY_INFO_ACCESS, OID_AD_CA_ISSUERS_RAW, OID_AD_CA_REPOSITORY, OID_AD_CA_REPOSITORY_RAW, OID_AD_RPKI_MANIFEST,
OID_AUTHORITY_KEY_IDENTIFIER, OID_AUTONOMOUS_SYS_IDS, OID_CP_IPADDR_ASNUMBER, OID_AD_RPKI_MANIFEST_RAW, OID_AD_RPKI_NOTIFY, OID_AD_RPKI_NOTIFY_RAW, OID_AD_SIGNED_OBJECT,
OID_CRL_DISTRIBUTION_POINTS, OID_IP_ADDR_BLOCKS, OID_SHA256_WITH_RSA_ENCRYPTION, OID_AD_SIGNED_OBJECT_RAW, OID_AUTHORITY_INFO_ACCESS_RAW, OID_AUTHORITY_KEY_IDENTIFIER_RAW,
OID_SUBJECT_INFO_ACCESS, OID_SUBJECT_KEY_IDENTIFIER, 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). /// Resource Certificate kind (semantic classification).
@ -43,8 +45,8 @@ pub struct RpkixTbsCertificate {
pub version: u32, pub version: u32,
pub serial_number: BigUint, pub serial_number: BigUint,
pub signature_algorithm: String, pub signature_algorithm: String,
pub issuer_dn: String, pub issuer_name: X509NameDer,
pub subject_dn: String, pub subject_name: X509NameDer,
pub validity_not_before: UtcTime, pub validity_not_before: UtcTime,
pub validity_not_after: UtcTime, pub validity_not_after: UtcTime,
/// DER encoding of SubjectPublicKeyInfo. /// DER encoding of SubjectPublicKeyInfo.
@ -59,9 +61,9 @@ pub struct RcExtensions {
/// Authority Key Identifier (AKI) keyIdentifier value. /// Authority Key Identifier (AKI) keyIdentifier value.
pub authority_key_identifier: Option<Vec<u8>>, pub authority_key_identifier: Option<Vec<u8>>,
/// CRL Distribution Points URIs (fullName). /// CRL Distribution Points URIs (fullName).
pub crl_distribution_points_uris: Option<Vec<Url>>, pub crl_distribution_points_uris: Option<Vec<String>>,
/// Authority Information Access (AIA) caIssuers URIs. /// Authority Information Access (AIA) caIssuers URIs.
pub ca_issuers_uris: Option<Vec<Url>>, pub ca_issuers_uris: Option<Vec<String>>,
pub subject_info_access: Option<SubjectInfoAccess>, pub subject_info_access: Option<SubjectInfoAccess>,
pub certificate_policies_oid: Option<String>, pub certificate_policies_oid: Option<String>,
@ -76,8 +78,8 @@ pub struct ResourceCertificateParsed {
pub serial_number: BigUint, pub serial_number: BigUint,
pub signature_algorithm: AlgorithmIdentifierValue, pub signature_algorithm: AlgorithmIdentifierValue,
pub tbs_signature_algorithm: AlgorithmIdentifierValue, pub tbs_signature_algorithm: AlgorithmIdentifierValue,
pub issuer_dn: String, pub issuer_name: X509NameDer,
pub subject_dn: String, pub subject_name: X509NameDer,
pub validity_not_before: Asn1TimeUtc, pub validity_not_before: Asn1TimeUtc,
pub validity_not_after: Asn1TimeUtc, pub validity_not_after: Asn1TimeUtc,
/// DER encoding of SubjectPublicKeyInfo. /// DER encoding of SubjectPublicKeyInfo.
@ -130,7 +132,7 @@ pub struct AuthorityKeyIdentifierParsed {
#[derive(Clone, Debug, PartialEq, Eq)] #[derive(Clone, Debug, PartialEq, Eq)]
pub struct AuthorityInfoAccessParsed { pub struct AuthorityInfoAccessParsed {
pub ca_issuers_uris: Vec<Url>, pub ca_issuers_uris: Vec<String>,
pub ca_issuers_access_location_not_uri: bool, pub ca_issuers_access_location_not_uri: bool,
} }
@ -145,7 +147,7 @@ pub struct CrlDistributionPointParsed {
pub reasons_present: bool, pub reasons_present: bool,
pub crl_issuer_present: bool, pub crl_issuer_present: bool,
pub name_relative_to_crl_issuer_present: bool, pub name_relative_to_crl_issuer_present: bool,
pub full_name_uris: Vec<Url>, pub full_name_uris: Vec<String>,
pub full_name_not_uri: bool, pub full_name_not_uri: bool,
pub full_name_present: bool, pub full_name_present: bool,
} }
@ -153,7 +155,7 @@ pub struct CrlDistributionPointParsed {
#[derive(Clone, Debug, PartialEq, Eq)] #[derive(Clone, Debug, PartialEq, Eq)]
pub struct SubjectInfoAccessParsed { pub struct SubjectInfoAccessParsed {
pub access_descriptions: Vec<AccessDescription>, pub access_descriptions: Vec<AccessDescription>,
pub signed_object_uris: Vec<Url>, pub signed_object_uris: Vec<String>,
pub signed_object_access_location_not_uri: bool, pub signed_object_access_location_not_uri: bool,
} }
@ -170,7 +172,7 @@ pub struct SubjectInfoAccessCa {
#[derive(Clone, Debug, PartialEq, Eq)] #[derive(Clone, Debug, PartialEq, Eq)]
pub struct SubjectInfoAccessEe { pub struct SubjectInfoAccessEe {
pub signed_object_uris: Vec<Url>, pub signed_object_uris: Vec<String>,
/// The full list of access descriptions as carried in the SIA extension. /// The full list of access descriptions as carried in the SIA extension.
pub access_descriptions: Vec<AccessDescription>, pub access_descriptions: Vec<AccessDescription>,
} }
@ -178,7 +180,7 @@ pub struct SubjectInfoAccessEe {
#[derive(Clone, Debug, PartialEq, Eq)] #[derive(Clone, Debug, PartialEq, Eq)]
pub struct AccessDescription { pub struct AccessDescription {
pub access_method_oid: String, pub access_method_oid: String,
pub access_location: Url, pub access_location: String,
} }
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)] #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
@ -541,8 +543,8 @@ impl ResourceCertificate {
serial_number: cert.tbs_certificate.serial.clone(), serial_number: cert.tbs_certificate.serial.clone(),
signature_algorithm, signature_algorithm,
tbs_signature_algorithm, tbs_signature_algorithm,
issuer_dn: cert.issuer().to_string(), issuer_name: X509NameDer(cert.issuer().as_raw().to_vec()),
subject_dn: cert.subject().to_string(), subject_name: X509NameDer(cert.subject().as_raw().to_vec()),
validity_not_before, validity_not_before,
validity_not_after, validity_not_after,
subject_public_key_info, subject_public_key_info,
@ -591,7 +593,7 @@ impl ResourceCertificateParsed {
return Err(ResourceCertificateProfileError::InvalidSignatureAlgorithmParameters); 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 extensions = self.extensions.validate_profile(is_self_signed)?;
let kind = if extensions.basic_constraints_ca { let kind = if extensions.basic_constraints_ca {
ResourceCertKind::Ca ResourceCertKind::Ca
@ -605,8 +607,8 @@ impl ResourceCertificateParsed {
version, version,
serial_number: self.serial_number, serial_number: self.serial_number,
signature_algorithm: self.signature_algorithm.oid, signature_algorithm: self.signature_algorithm.oid,
issuer_dn: self.issuer_dn, issuer_name: self.issuer_name,
subject_dn: self.subject_dn, subject_name: self.subject_name,
validity_not_before: self.validity_not_before.utc, validity_not_before: self.validity_not_before.utc,
validity_not_after: self.validity_not_after.utc, validity_not_after: self.validity_not_after.utc,
subject_public_key_info: self.subject_public_key_info, subject_public_key_info: self.subject_public_key_info,
@ -622,20 +624,38 @@ impl RcExtensionsParsed {
self, self,
is_self_signed: bool, is_self_signed: bool,
) -> Result<RcExtensions, ResourceCertificateProfileError> { ) -> Result<RcExtensions, ResourceCertificateProfileError> {
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( return Err(ResourceCertificateProfileError::DuplicateExtension(
"basicConstraints", "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() { let subject_key_identifier = match subject_key_identifier.len() {
[] => None, 0 => None,
[(ski, critical)] => { 1 => {
if *critical { let (ski, critical) = subject_key_identifier
.into_iter()
.next()
.expect("len==1");
if critical {
return Err(ResourceCertificateProfileError::SkiCriticality); return Err(ResourceCertificateProfileError::SkiCriticality);
} }
Some(ski.clone()) Some(ski)
} }
_ => { _ => {
return Err(ResourceCertificateProfileError::DuplicateExtension( 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 { if is_self_signed {
None None
} else { } else {
return Err(ResourceCertificateProfileError::AkiMissing); return Err(ResourceCertificateProfileError::AkiMissing);
} }
} }
[(aki, critical)] => { 1 => {
if *critical { let (aki, critical) = authority_key_identifier
.into_iter()
.next()
.expect("len==1");
if critical {
return Err(ResourceCertificateProfileError::AkiCriticality); return Err(ResourceCertificateProfileError::AkiCriticality);
} }
if aki.has_authority_cert_issuer { if aki.has_authority_cert_issuer {
@ -662,7 +686,7 @@ impl RcExtensionsParsed {
if aki.has_authority_cert_serial { if aki.has_authority_cert_serial {
return Err(ResourceCertificateProfileError::AkiAuthorityCertSerialPresent); return Err(ResourceCertificateProfileError::AkiAuthorityCertSerialPresent);
} }
let keyid = aki.key_identifier.clone(); let keyid = aki.key_identifier;
if is_self_signed { if is_self_signed {
if let (Some(keyid), Some(ski)) = if let (Some(keyid), Some(ski)) =
(keyid.as_ref(), subject_key_identifier.as_ref()) (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 { if is_self_signed {
None None
} else { } else {
return Err(ResourceCertificateProfileError::CrlDistributionPointsMissing); return Err(ResourceCertificateProfileError::CrlDistributionPointsMissing);
} }
} }
[(crldp, critical)] => { 1 => {
if *critical { let (crldp, critical) = crl_distribution_points
.into_iter()
.next()
.expect("len==1");
if critical {
return Err(ResourceCertificateProfileError::CrlDistributionPointsCriticality); return Err(ResourceCertificateProfileError::CrlDistributionPointsCriticality);
} }
if is_self_signed { if is_self_signed {
@ -703,7 +731,11 @@ impl RcExtensionsParsed {
if crldp.distribution_points.len() != 1 { if crldp.distribution_points.len() != 1 {
return Err(ResourceCertificateProfileError::CrlDistributionPointsNotSingle); 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 { if dp.reasons_present {
return Err(ResourceCertificateProfileError::CrlDistributionPointsHasReasons); return Err(ResourceCertificateProfileError::CrlDistributionPointsHasReasons);
} }
@ -723,10 +755,10 @@ impl RcExtensionsParsed {
ResourceCertificateProfileError::CrlDistributionPointsFullNameNotUri, 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); return Err(ResourceCertificateProfileError::CrlDistributionPointsNoRsync);
} }
Some(dp.full_name_uris.clone()) Some(dp.full_name_uris)
} }
_ => { _ => {
return Err(ResourceCertificateProfileError::DuplicateExtension( 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 { if is_self_signed {
None None
} else { } else {
return Err(ResourceCertificateProfileError::AuthorityInfoAccessMissing); return Err(ResourceCertificateProfileError::AuthorityInfoAccessMissing);
} }
} }
[(aia, critical)] => { 1 => {
if *critical { let (aia, critical) = authority_info_access.into_iter().next().expect("len==1");
if critical {
return Err(ResourceCertificateProfileError::AuthorityInfoAccessCriticality); return Err(ResourceCertificateProfileError::AuthorityInfoAccessCriticality);
} }
if is_self_signed { if is_self_signed {
@ -762,10 +795,10 @@ impl RcExtensionsParsed {
ResourceCertificateProfileError::AuthorityInfoAccessMissingCaIssuers, 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); return Err(ResourceCertificateProfileError::AuthorityInfoAccessNoRsync);
} }
Some(aia.ca_issuers_uris.clone()) Some(aia.ca_issuers_uris)
} }
_ => { _ => {
return Err(ResourceCertificateProfileError::DuplicateExtension( return Err(ResourceCertificateProfileError::DuplicateExtension(
@ -774,28 +807,32 @@ impl RcExtensionsParsed {
} }
}; };
let subject_info_access = match self.subject_info_access.as_slice() { let subject_info_access = match subject_info_access.len() {
[] => None, 0 => None,
[(sia, critical)] => { 1 => {
if *critical { let (sia, critical) = subject_info_access.into_iter().next().expect("len==1");
if critical {
return Err(ResourceCertificateProfileError::SiaCriticality); return Err(ResourceCertificateProfileError::SiaCriticality);
} }
if sia.signed_object_access_location_not_uri { if sia.signed_object_access_location_not_uri {
return Err(ResourceCertificateProfileError::SignedObjectSiaNotUri); return Err(ResourceCertificateProfileError::SignedObjectSiaNotUri);
} }
if !sia.signed_object_uris.is_empty() 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); return Err(ResourceCertificateProfileError::SignedObjectSiaNoRsync);
} }
if sia.signed_object_uris.is_empty() { if sia.signed_object_uris.is_empty() {
Some(SubjectInfoAccess::Ca(SubjectInfoAccessCa { Some(SubjectInfoAccess::Ca(SubjectInfoAccessCa {
access_descriptions: sia.access_descriptions.clone(), access_descriptions: sia.access_descriptions,
})) }))
} else { } else {
Some(SubjectInfoAccess::Ee(SubjectInfoAccessEe { Some(SubjectInfoAccess::Ee(SubjectInfoAccessEe {
signed_object_uris: sia.signed_object_uris.clone(), signed_object_uris: sia.signed_object_uris,
access_descriptions: sia.access_descriptions.clone(), access_descriptions: sia.access_descriptions,
})) }))
} }
} }
@ -806,10 +843,11 @@ impl RcExtensionsParsed {
} }
}; };
let certificate_policies_oid = match self.certificate_policies.as_slice() { let certificate_policies_oid = match certificate_policies.len() {
[] => None, 0 => None,
[(oids, critical)] => { 1 => {
if !*critical { let (oids, critical) = certificate_policies.into_iter().next().expect("len==1");
if !critical {
return Err(ResourceCertificateProfileError::CertificatePoliciesCriticality); return Err(ResourceCertificateProfileError::CertificatePoliciesCriticality);
} }
if oids.len() != 1 { if oids.len() != 1 {
@ -817,7 +855,7 @@ impl RcExtensionsParsed {
"expected exactly one policy".into(), "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 { if policy_oid != OID_CP_IPADDR_ASNUMBER {
return Err(ResourceCertificateProfileError::InvalidCertificatePolicy( return Err(ResourceCertificateProfileError::InvalidCertificatePolicy(
policy_oid, policy_oid,
@ -832,13 +870,14 @@ impl RcExtensionsParsed {
} }
}; };
let ip_resources = match self.ip_resources.as_slice() { let ip_resources = match ip_resources.len() {
[] => None, 0 => None,
[(ip, critical)] => { 1 => {
if !*critical { let (ip, critical) = ip_resources.into_iter().next().expect("len==1");
if !critical {
return Err(ResourceCertificateProfileError::IpResourcesCriticality); return Err(ResourceCertificateProfileError::IpResourcesCriticality);
} }
Some(ip.clone()) Some(ip)
} }
_ => { _ => {
return Err(ResourceCertificateProfileError::DuplicateExtension( return Err(ResourceCertificateProfileError::DuplicateExtension(
@ -847,13 +886,14 @@ impl RcExtensionsParsed {
} }
}; };
let as_resources = match self.as_resources.as_slice() { let as_resources = match as_resources.len() {
[] => None, 0 => None,
[(asn, critical)] => { 1 => {
if !*critical { let (asn, critical) = as_resources.into_iter().next().expect("len==1");
if !critical {
return Err(ResourceCertificateProfileError::AsResourcesCriticality); return Err(ResourceCertificateProfileError::AsResourcesCriticality);
} }
Some(asn.clone()) Some(asn)
} }
_ => { _ => {
return Err(ResourceCertificateProfileError::DuplicateExtension( return Err(ResourceCertificateProfileError::DuplicateExtension(
@ -884,8 +924,16 @@ fn algorithm_identifier_value(
tag: p.tag(), tag: p.tag(),
data: p.as_bytes().to_vec(), 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 { AlgorithmIdentifierValue {
oid: ai.algorithm.to_id_string(), oid,
parameters, parameters,
} }
} }
@ -905,25 +953,22 @@ fn parse_extensions_parse(
let mut as_resources: Vec<(AsResourceSet, bool)> = Vec::new(); let mut as_resources: Vec<(AsResourceSet, bool)> = Vec::new();
for ext in exts { for ext in exts {
let oid = ext.oid.to_id_string(); let oid = ext.oid.as_bytes();
match oid.as_str() { if oid == OID_BASIC_CONSTRAINTS_RAW {
crate::data_model::oid::OID_BASIC_CONSTRAINTS => {
let ParsedExtension::BasicConstraints(bc) = ext.parsed_extension() else { let ParsedExtension::BasicConstraints(bc) = ext.parsed_extension() else {
return Err(ResourceCertificateParseError::Parse( return Err(ResourceCertificateParseError::Parse(
"basicConstraints parse failed".into(), "basicConstraints parse failed".into(),
)); ));
}; };
basic_constraints_ca.push(bc.ca); basic_constraints_ca.push(bc.ca);
} } else if oid == OID_SUBJECT_KEY_IDENTIFIER_RAW {
OID_SUBJECT_KEY_IDENTIFIER => {
let ParsedExtension::SubjectKeyIdentifier(s) = ext.parsed_extension() else { let ParsedExtension::SubjectKeyIdentifier(s) = ext.parsed_extension() else {
return Err(ResourceCertificateParseError::Parse( return Err(ResourceCertificateParseError::Parse(
"subjectKeyIdentifier parse failed".into(), "subjectKeyIdentifier parse failed".into(),
)); ));
}; };
ski.push((s.0.to_vec(), ext.critical)); ski.push((s.0.to_vec(), ext.critical));
} } else if oid == OID_AUTHORITY_KEY_IDENTIFIER_RAW {
OID_AUTHORITY_KEY_IDENTIFIER => {
let ParsedExtension::AuthorityKeyIdentifier(a) = ext.parsed_extension() else { let ParsedExtension::AuthorityKeyIdentifier(a) = ext.parsed_extension() else {
return Err(ResourceCertificateParseError::Parse( return Err(ResourceCertificateParseError::Parse(
"authorityKeyIdentifier parse failed".into(), "authorityKeyIdentifier parse failed".into(),
@ -937,52 +982,52 @@ fn parse_extensions_parse(
}, },
ext.critical, ext.critical,
)); ));
} } else if oid == OID_CRL_DISTRIBUTION_POINTS_RAW {
OID_CRL_DISTRIBUTION_POINTS => {
let ParsedExtension::CRLDistributionPoints(p) = ext.parsed_extension() else { let ParsedExtension::CRLDistributionPoints(p) = ext.parsed_extension() else {
return Err(ResourceCertificateParseError::Parse( return Err(ResourceCertificateParseError::Parse(
"cRLDistributionPoints parse failed".into(), "cRLDistributionPoints parse failed".into(),
)); ));
}; };
crldp.push((parse_crldp_parse(p)?, ext.critical)); crldp.push((parse_crldp_parse(p)?, ext.critical));
} } else if oid == OID_AUTHORITY_INFO_ACCESS_RAW {
OID_AUTHORITY_INFO_ACCESS => {
let ParsedExtension::AuthorityInfoAccess(p) = ext.parsed_extension() else { let ParsedExtension::AuthorityInfoAccess(p) = ext.parsed_extension() else {
return Err(ResourceCertificateParseError::Parse( return Err(ResourceCertificateParseError::Parse(
"authorityInfoAccess parse failed".into(), "authorityInfoAccess parse failed".into(),
)); ));
}; };
aia.push((parse_aia_parse(p.accessdescs.as_slice())?, ext.critical)); aia.push((parse_aia_parse(p.accessdescs.as_slice())?, ext.critical));
} } else if oid == OID_SUBJECT_INFO_ACCESS_RAW {
OID_SUBJECT_INFO_ACCESS => {
let ParsedExtension::SubjectInfoAccess(s) = ext.parsed_extension() else { let ParsedExtension::SubjectInfoAccess(s) = ext.parsed_extension() else {
return Err(ResourceCertificateParseError::Parse( return Err(ResourceCertificateParseError::Parse(
"subjectInfoAccess parse failed".into(), "subjectInfoAccess parse failed".into(),
)); ));
}; };
sia.push((parse_sia_parse(s.accessdescs.as_slice())?, ext.critical)); sia.push((parse_sia_parse(s.accessdescs.as_slice())?, ext.critical));
} } else if oid == OID_CERTIFICATE_POLICIES_RAW {
crate::data_model::oid::OID_CERTIFICATE_POLICIES => {
let ParsedExtension::CertificatePolicies(cp) = ext.parsed_extension() else { let ParsedExtension::CertificatePolicies(cp) = ext.parsed_extension() else {
return Err(ResourceCertificateParseError::Parse( return Err(ResourceCertificateParseError::Parse(
"certificatePolicies parse failed".into(), "certificatePolicies parse failed".into(),
)); ));
}; };
let oids: Vec<String> = cp.iter().map(|p| p.policy_id.to_id_string()).collect(); let mut oids: Vec<String> = Vec::with_capacity(cp.len());
cert_policies.push((oids, ext.critical)); 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());
} }
OID_IP_ADDR_BLOCKS => { }
cert_policies.push((oids, ext.critical));
} else if oid == OID_IP_ADDR_BLOCKS_RAW {
let parsed = IpResourceSet::decode_extn_value(ext.value) let parsed = IpResourceSet::decode_extn_value(ext.value)
.map_err(|_e| ResourceCertificateParseError::InvalidIpResourcesEncoding)?; .map_err(|_e| ResourceCertificateParseError::InvalidIpResourcesEncoding)?;
ip_resources.push((parsed, ext.critical)); ip_resources.push((parsed, ext.critical));
} } else if oid == OID_AUTONOMOUS_SYS_IDS_RAW {
OID_AUTONOMOUS_SYS_IDS => {
let parsed = AsResourceSet::decode_extn_value(ext.value) let parsed = AsResourceSet::decode_extn_value(ext.value)
.map_err(|_e| ResourceCertificateParseError::InvalidAsResourcesEncoding)?; .map_err(|_e| ResourceCertificateParseError::InvalidAsResourcesEncoding)?;
as_resources.push((parsed, ext.critical)); as_resources.push((parsed, ext.critical));
} }
_ => {}
}
} }
Ok(RcExtensionsParsed { Ok(RcExtensionsParsed {
@ -1001,12 +1046,11 @@ fn parse_extensions_parse(
fn parse_aia_parse( fn parse_aia_parse(
access: &[x509_parser::extensions::AccessDescription<'_>], access: &[x509_parser::extensions::AccessDescription<'_>],
) -> Result<AuthorityInfoAccessParsed, ResourceCertificateParseError> { ) -> Result<AuthorityInfoAccessParsed, ResourceCertificateParseError> {
let mut ca_issuers_uris: Vec<Url> = Vec::new(); let mut ca_issuers_uris: Vec<String> = Vec::new();
let mut ca_issuers_access_location_not_uri = false; let mut ca_issuers_access_location_not_uri = false;
for ad in access { for ad in access {
let access_method_oid = ad.access_method.to_id_string(); if ad.access_method.as_bytes() != OID_AD_CA_ISSUERS_RAW {
if access_method_oid != OID_AD_CA_ISSUERS {
continue; continue;
} }
let uri = match &ad.access_location { let uri = match &ad.access_location {
@ -1016,9 +1060,7 @@ fn parse_aia_parse(
continue; continue;
} }
}; };
let url = Url::parse(uri) ca_issuers_uris.push(uri.to_string());
.map_err(|_| ResourceCertificateParseError::Parse(format!("invalid URI: {uri}")))?;
ca_issuers_uris.push(url);
} }
Ok(AuthorityInfoAccessParsed { Ok(AuthorityInfoAccessParsed {
@ -1032,7 +1074,7 @@ fn parse_crldp_parse(
) -> Result<CrlDistributionPointsParsed, ResourceCertificateParseError> { ) -> Result<CrlDistributionPointsParsed, ResourceCertificateParseError> {
let mut out: Vec<CrlDistributionPointParsed> = Vec::new(); let mut out: Vec<CrlDistributionPointParsed> = Vec::new();
for p in crldp.iter() { for p in crldp.iter() {
let mut full_name_uris: Vec<Url> = Vec::new(); let mut full_name_uris: Vec<String> = Vec::new();
let mut full_name_not_uri = false; let mut full_name_not_uri = false;
let mut full_name_present = false; let mut full_name_present = false;
let mut name_relative_to_crl_issuer_present = false; let mut name_relative_to_crl_issuer_present = false;
@ -1046,12 +1088,7 @@ fn parse_crldp_parse(
for n in names { for n in names {
match n { match n {
x509_parser::extensions::GeneralName::URI(u) => { x509_parser::extensions::GeneralName::URI(u) => {
let url = Url::parse(u).map_err(|_| { full_name_uris.push(u.to_string());
ResourceCertificateParseError::Parse(format!(
"invalid URI: {u}"
))
})?;
full_name_uris.push(url);
} }
_ => { _ => {
full_name_not_uri = true; full_name_not_uri = true;
@ -1084,28 +1121,37 @@ fn parse_sia_parse(
access: &[x509_parser::extensions::AccessDescription<'_>], access: &[x509_parser::extensions::AccessDescription<'_>],
) -> Result<SubjectInfoAccessParsed, ResourceCertificateParseError> { ) -> Result<SubjectInfoAccessParsed, ResourceCertificateParseError> {
let mut all = Vec::with_capacity(access.len()); let mut all = Vec::with_capacity(access.len());
let mut signed_object_uris: Vec<Url> = Vec::new(); let mut signed_object_uris: Vec<String> = Vec::new();
let mut signed_object_access_location_not_uri = false; let mut signed_object_access_location_not_uri = false;
for ad in access { 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 { let uri = match &ad.access_location {
x509_parser::extensions::GeneralName::URI(u) => u, 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; signed_object_access_location_not_uri = true;
} }
continue; continue;
} }
}; };
let url = Url::parse(uri) if is_signed_object {
.map_err(|_| ResourceCertificateParseError::Parse(format!("invalid URI: {uri}")))?; signed_object_uris.push(uri.to_string());
if access_method_oid == OID_AD_SIGNED_OBJECT {
signed_object_uris.push(url.clone());
} }
all.push(AccessDescription { all.push(AccessDescription {
access_method_oid, access_method_oid,
access_location: url, access_location: uri.to_string(),
}); });
} }

View File

@ -1,10 +1,9 @@
use crate::data_model::oid::OID_CT_ROUTE_ORIGIN_AUTHZ; 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::rc::{Afi as RcAfi, IpPrefix as RcIpPrefix, ResourceCertificate};
use crate::data_model::signed_object::{ use crate::data_model::signed_object::{
RpkiSignedObject, RpkiSignedObjectParsed, SignedObjectParseError, SignedObjectValidateError, RpkiSignedObject, RpkiSignedObjectParsed, SignedObjectParseError, SignedObjectValidateError,
}; };
use der_parser::ber::{BerObjectContent, Class};
use der_parser::der::{DerObject, Tag, parse_der};
#[derive(Clone, Debug, PartialEq, Eq)] #[derive(Clone, Debug, PartialEq, Eq)]
pub struct RoaObject { pub struct RoaObject {
@ -229,14 +228,25 @@ pub struct IpPrefix {
pub afi: RoaAfi, pub afi: RoaAfi,
/// Prefix length in bits. /// Prefix length in bits.
pub prefix_len: u16, pub prefix_len: u16,
/// Network order address bytes (IPv4 4 bytes / IPv6 16 bytes), with host bits cleared. /// Network order address bytes (always 16 bytes), with host bits cleared.
pub addr: Vec<u8>, ///
/// 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 { impl RoaEContent {
/// Parse step of scheme A (`parse → validate → verify`). /// Parse step of scheme A (`parse → validate → verify`).
pub fn parse_der(der: &[u8]) -> Result<RoaEContentParsed, RoaParseError> { pub fn parse_der(der: &[u8]) -> Result<RoaEContentParsed, RoaParseError> {
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() { if !rem.is_empty() {
return Err(RoaParseError::TrailingBytes(rem.len())); return Err(RoaParseError::TrailingBytes(rem.len()));
} }
@ -293,7 +303,7 @@ impl RoaEContent {
if !ip.contains_prefix(&rc_prefix) { if !ip.contains_prefix(&rc_prefix) {
return Err(RoaValidateError::PrefixNotInEeResources { return Err(RoaValidateError::PrefixNotInEeResources {
afi: entry.prefix.afi, afi: entry.prefix.afi,
addr: entry.prefix.addr.clone(), addr: entry.prefix.addr_bytes().to_vec(),
prefix_len: entry.prefix.prefix_len, prefix_len: entry.prefix.prefix_len,
}); });
} }
@ -328,55 +338,75 @@ impl RoaObjectParsed {
impl RoaEContentParsed { impl RoaEContentParsed {
pub fn validate_profile(self) -> Result<RoaEContent, RoaProfileError> { pub fn validate_profile(self) -> Result<RoaEContent, RoaProfileError> {
let (_rem, obj) = fn count_elements(mut r: DerReader<'_>) -> Result<usize, String> {
parse_der(&self.der).map_err(|e| RoaProfileError::ProfileDecode(e.to_string()))?; let mut n = 0usize;
while !r.is_empty() {
let seq = obj r.skip_any()?;
.as_sequence() n += 1;
.map_err(|e| RoaProfileError::ProfileDecode(e.to_string()))?; }
if seq.len() != 2 && seq.len() != 3 { Ok(n)
return Err(RoaProfileError::InvalidAttestationSequenceLen(seq.len())); }
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; let mut version: u32 = 0;
if seq.len() == 3 { if elem_count == 3 {
let v_obj = &seq[0]; if seq
if v_obj.class() != Class::ContextSpecific || v_obj.tag() != Tag(0) { .peek_tag()
.map_err(|e| RoaProfileError::ProfileDecode(e.to_string()))?
!= 0xA0
{
return Err(RoaProfileError::ProfileDecode( return Err(RoaProfileError::ProfileDecode(
"RouteOriginAttestation.version must be [0] EXPLICIT INTEGER".into(), "RouteOriginAttestation.version must be [0] EXPLICIT INTEGER".into(),
)); ));
} }
let inner_der = v_obj let (inner_tag, inner_val) = seq
.as_slice() .take_explicit(0xA0)
.map_err(|e| RoaProfileError::ProfileDecode(e.to_string()))?; .map_err(|e| RoaProfileError::ProfileDecode(e.to_string()))?;
let (rem, inner) = if inner_tag != 0x02 {
parse_der(inner_der).map_err(|e| RoaProfileError::ProfileDecode(e.to_string()))?;
if !rem.is_empty() {
return Err(RoaProfileError::ProfileDecode( return Err(RoaProfileError::ProfileDecode(
"trailing bytes inside RouteOriginAttestation.version".into(), "RouteOriginAttestation.version must be [0] EXPLICIT INTEGER".into(),
)); ));
} }
let v = inner let v = crate::data_model::common::der_uint_from_bytes(inner_val)
.as_u64()
.map_err(|e| RoaProfileError::ProfileDecode(e.to_string()))?; .map_err(|e| RoaProfileError::ProfileDecode(e.to_string()))?;
if v != 0 { if v != 0 {
return Err(RoaProfileError::InvalidVersion(v)); return Err(RoaProfileError::InvalidVersion(v));
} }
version = 0; version = 0;
idx = 1;
} }
let as_id_u64 = seq[idx] let as_id_u64 = seq
.as_u64() .take_uint_u64()
.map_err(|e| RoaProfileError::ProfileDecode(e.to_string()))?; .map_err(|e| RoaProfileError::ProfileDecode(e.to_string()))?;
if as_id_u64 > u32::MAX as u64 { if as_id_u64 > u32::MAX as u64 {
return Err(RoaProfileError::AsIdOutOfRange(as_id_u64)); return Err(RoaProfileError::AsIdOutOfRange(as_id_u64));
} }
let as_id = as_id_u64 as u32; 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 { let mut out = RoaEContent {
version, version,
@ -396,21 +426,34 @@ fn roa_prefix_to_rc(p: &IpPrefix) -> RcIpPrefix {
RcIpPrefix { RcIpPrefix {
afi, afi,
prefix_len: p.prefix_len, prefix_len: p.prefix_len,
addr: p.addr.clone(), addr: p.addr_bytes().to_vec(),
} }
} }
fn parse_ip_addr_blocks(obj: &DerObject<'_>) -> Result<Vec<RoaIpAddressFamily>, RoaProfileError> { fn parse_ip_addr_blocks_cursor(
let seq = obj mut seq: DerReader<'_>,
.as_sequence() ) -> Result<Vec<RoaIpAddressFamily>, RoaProfileError> {
.map_err(|e| RoaProfileError::ProfileDecode(e.to_string()))?; fn count_elements(mut r: DerReader<'_>) -> Result<usize, String> {
if seq.is_empty() || seq.len() > 2 { let mut n = 0usize;
return Err(RoaProfileError::InvalidIpAddrBlocksLen(seq.len())); while !r.is_empty() {
r.skip_any()?;
n += 1;
}
Ok(n)
} }
let mut out: Vec<RoaIpAddressFamily> = Vec::new(); let fam_count =
for fam in seq { count_elements(seq).map_err(|e| RoaProfileError::ProfileDecode(e.to_string()))?;
let family = parse_ip_address_family(fam)?; if fam_count == 0 || fam_count > 2 {
return Err(RoaProfileError::InvalidIpAddrBlocksLen(fam_count));
}
let mut out: Vec<RoaIpAddressFamily> = 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) { if out.iter().any(|f| f.afi == family.afi) {
return Err(RoaProfileError::DuplicateAfi(family.afi)); return Err(RoaProfileError::DuplicateAfi(family.afi));
} }
@ -419,77 +462,75 @@ fn parse_ip_addr_blocks(obj: &DerObject<'_>) -> Result<Vec<RoaIpAddressFamily>,
Ok(out) Ok(out)
} }
fn parse_ip_address_family(obj: &DerObject<'_>) -> Result<RoaIpAddressFamily, RoaProfileError> { fn parse_ip_address_family_cursor(
let seq = obj mut fam: DerReader<'_>,
.as_sequence() ) -> Result<RoaIpAddressFamily, RoaProfileError> {
.map_err(|e| RoaProfileError::ProfileDecode(e.to_string()))?; let afi = {
if seq.len() != 2 { let bytes = fam
return Err(RoaProfileError::InvalidIpAddressFamily); .take_octet_string()
}
let afi = parse_afi(&seq[0])?;
let addresses = parse_roa_addresses(afi, &seq[1])?;
if addresses.is_empty() {
return Err(RoaProfileError::EmptyAddressList);
}
Ok(RoaIpAddressFamily { afi, addresses })
}
fn parse_afi(obj: &DerObject<'_>) -> Result<RoaAfi, RoaProfileError> {
let bytes = obj
.as_slice()
.map_err(|_e| RoaProfileError::InvalidAddressFamily)?; .map_err(|_e| RoaProfileError::InvalidAddressFamily)?;
if bytes.len() != 2 { if bytes.len() != 2 {
return Err(RoaProfileError::InvalidAddressFamily); return Err(RoaProfileError::InvalidAddressFamily);
} }
match bytes { match bytes {
[0x00, 0x01] => Ok(RoaAfi::Ipv4), [0x00, 0x01] => RoaAfi::Ipv4,
[0x00, 0x02] => Ok(RoaAfi::Ipv6), [0x00, 0x02] => RoaAfi::Ipv6,
_ => Err(RoaProfileError::UnsupportedAfi(bytes.to_vec())), _ => 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);
} }
fn parse_roa_addresses( if addrs.is_empty() {
return Err(RoaProfileError::EmptyAddressList);
}
let mut addresses: Vec<RoaIpAddress> = 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_roa_ip_address_cursor(
afi: RoaAfi, afi: RoaAfi,
obj: &DerObject<'_>, mut seq: DerReader<'_>,
) -> Result<Vec<RoaIpAddress>, RoaProfileError> { ) -> Result<RoaIpAddress, RoaProfileError> {
let seq = obj if seq.is_empty() {
.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<RoaIpAddress, RoaProfileError> {
let seq = obj
.as_sequence()
.map_err(|e| RoaProfileError::ProfileDecode(e.to_string()))?;
if seq.is_empty() || seq.len() > 2 {
return Err(RoaProfileError::InvalidRoaIpAddress); return Err(RoaProfileError::InvalidRoaIpAddress);
} }
let prefix = parse_prefix_bits(afi, &seq[0])?; let (unused_bits, bytes) = seq
let max_length = match seq.get(1) { .take_bit_string()
None => None, .map_err(|_e| RoaProfileError::InvalidPrefixBitString)?;
Some(m) => { let prefix = parse_prefix_bits_bytes(afi, unused_bits, bytes)?;
let v = m
.as_u64() let max_length = if !seq.is_empty() {
let v = seq
.take_uint_u64()
.map_err(|e| RoaProfileError::ProfileDecode(e.to_string()))?; .map_err(|e| RoaProfileError::ProfileDecode(e.to_string()))?;
let max_len: u16 = v let max_len: u16 = v.try_into().map_err(|_e| RoaProfileError::InvalidMaxLength {
.try_into()
.map_err(|_e| RoaProfileError::InvalidMaxLength {
afi, afi,
prefix_len: prefix.prefix_len, prefix_len: prefix.prefix_len,
max_len: u16::MAX, max_len: u16::MAX,
})?; })?;
Some(max_len) Some(max_len)
} } else {
None
}; };
if !seq.is_empty() {
return Err(RoaProfileError::InvalidRoaIpAddress);
}
if let Some(max_len) = max_length { if let Some(max_len) = max_length {
let ub = afi.ub(); let ub = afi.ub();
if max_len > ub || max_len < prefix.prefix_len { if max_len > ub || max_len < prefix.prefix_len {
@ -504,12 +545,11 @@ fn parse_roa_ip_address(afi: RoaAfi, obj: &DerObject<'_>) -> Result<RoaIpAddress
Ok(RoaIpAddress { prefix, max_length }) Ok(RoaIpAddress { prefix, max_length })
} }
fn parse_prefix_bits(afi: RoaAfi, obj: &DerObject<'_>) -> Result<IpPrefix, RoaProfileError> { fn parse_prefix_bits_bytes(
let (unused_bits, bytes) = match &obj.content { afi: RoaAfi,
BerObjectContent::BitString(unused, bso) => (*unused, bso.data.to_vec()), unused_bits: u8,
_ => return Err(RoaProfileError::InvalidPrefixBitString), bytes: &[u8],
}; ) -> Result<IpPrefix, RoaProfileError> {
if unused_bits > 7 { if unused_bits > 7 {
return Err(RoaProfileError::InvalidPrefixUnusedBits); return Err(RoaProfileError::InvalidPrefixUnusedBits);
} }
@ -531,7 +571,7 @@ fn parse_prefix_bits(afi: RoaAfi, obj: &DerObject<'_>) -> Result<IpPrefix, RoaPr
return Err(RoaProfileError::PrefixLenOutOfRange { afi, prefix_len }); return Err(RoaProfileError::PrefixLenOutOfRange { afi, prefix_len });
} }
let addr = canonicalize_prefix_addr(afi, prefix_len, &bytes); let addr = canonicalize_prefix_addr(afi, prefix_len, bytes);
Ok(IpPrefix { Ok(IpPrefix {
afi, afi,
prefix_len, prefix_len,
@ -539,12 +579,12 @@ fn parse_prefix_bits(afi: RoaAfi, obj: &DerObject<'_>) -> Result<IpPrefix, RoaPr
}) })
} }
fn canonicalize_prefix_addr(afi: RoaAfi, prefix_len: u16, bytes: &[u8]) -> Vec<u8> { fn canonicalize_prefix_addr(afi: RoaAfi, prefix_len: u16, bytes: &[u8]) -> [u8; 16] {
let full_len = match afi { let full_len = match afi {
RoaAfi::Ipv4 => 4, RoaAfi::Ipv4 => 4,
RoaAfi::Ipv6 => 16, RoaAfi::Ipv6 => 16,
}; };
let mut addr = vec![0u8; full_len]; let mut addr = [0u8; 16];
let copy_len = bytes.len().min(full_len); let copy_len = bytes.len().min(full_len);
addr[..copy_len].copy_from_slice(&bytes[..copy_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<u
let rem = (prefix_len % 8) as u8; let rem = (prefix_len % 8) as u8;
if rem != 0 { if rem != 0 {
let mask: u8 = 0xFF << (8 - rem); let mask: u8 = 0xFF << (8 - rem);
if last_prefix_byte < addr.len() { if last_prefix_byte < full_len {
addr[last_prefix_byte] &= mask; addr[last_prefix_byte] &= mask;
} }
} }

View File

@ -1,12 +1,14 @@
use crate::data_model::common::{Asn1TimeEncoding, Asn1TimeUtc}; use crate::data_model::common::{der_take_tlv, Asn1TimeEncoding, Asn1TimeUtc, DerReader};
use crate::data_model::oid::{ use crate::data_model::oid::{
OID_AD_SIGNED_OBJECT, OID_CMS_ATTR_CONTENT_TYPE, OID_CMS_ATTR_MESSAGE_DIGEST, OID_AD_SIGNED_OBJECT, OID_CMS_ATTR_CONTENT_TYPE, OID_CMS_ATTR_MESSAGE_DIGEST,
OID_CMS_ATTR_SIGNING_TIME, OID_RSA_ENCRYPTION, OID_SHA256, OID_SHA256_WITH_RSA_ENCRYPTION, OID_CMS_ATTR_CONTENT_TYPE_RAW, OID_CMS_ATTR_MESSAGE_DIGEST_RAW, OID_CMS_ATTR_SIGNING_TIME,
OID_SIGNED_DATA, OID_SUBJECT_INFO_ACCESS, OID_CMS_ATTR_SIGNING_TIME_RAW, OID_CT_ASPA, OID_CT_ASPA_RAW, OID_CT_RPKI_MANIFEST,
OID_CT_RPKI_MANIFEST_RAW, OID_CT_ROUTE_ORIGIN_AUTHZ, OID_CT_ROUTE_ORIGIN_AUTHZ_RAW,
OID_RSA_ENCRYPTION, OID_RSA_ENCRYPTION_RAW, OID_SHA256, OID_SHA256_RAW,
OID_SHA256_WITH_RSA_ENCRYPTION, OID_SHA256_WITH_RSA_ENCRYPTION_RAW, OID_SIGNED_DATA,
OID_SIGNED_DATA_RAW, OID_SUBJECT_INFO_ACCESS,
}; };
use crate::data_model::rc::{ResourceCertificate, SubjectInfoAccess}; use crate::data_model::rc::{ResourceCertificate, SubjectInfoAccess};
use der_parser::ber::Class;
use der_parser::der::{DerObject, Tag, parse_der};
use ring::digest; use ring::digest;
use x509_parser::prelude::FromDer; use x509_parser::prelude::FromDer;
use x509_parser::public_key::PublicKey; use x509_parser::public_key::PublicKey;
@ -303,24 +305,22 @@ impl RpkiSignedObject {
/// This performs encoding/structure parsing only. Profile constraints are enforced by /// This performs encoding/structure parsing only. Profile constraints are enforced by
/// `RpkiSignedObjectParsed::validate_profile`. /// `RpkiSignedObjectParsed::validate_profile`.
pub fn parse_der(der: &[u8]) -> Result<RpkiSignedObjectParsed, SignedObjectParseError> { pub fn parse_der(der: &[u8]) -> Result<RpkiSignedObjectParsed, SignedObjectParseError> {
let (rem, obj) = let mut r = DerReader::new(der);
parse_der(der).map_err(|e| SignedObjectParseError::Parse(e.to_string()))?; let mut content_info_seq = r
if !rem.is_empty() { .take_sequence()
return Err(SignedObjectParseError::TrailingBytes(rem.len())); .map_err(|e| SignedObjectParseError::Parse(e.to_string()))?;
if !r.is_empty() {
return Err(SignedObjectParseError::TrailingBytes(r.remaining_len()));
} }
let content_info_seq = obj let content_type = take_oid_string(&mut content_info_seq)?;
.as_sequence() let signed_data = parse_signed_data_from_contentinfo_cursor(&mut content_info_seq)?;
.map_err(|e| SignedObjectParseError::Parse(e.to_string()))?; if !content_info_seq.is_empty() {
if content_info_seq.len() != 2 {
return Err(SignedObjectParseError::Parse( return Err(SignedObjectParseError::Parse(
"ContentInfo must be a SEQUENCE of 2 elements".into(), "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 { Ok(RpkiSignedObjectParsed {
raw_der: der.to_vec(), raw_der: der.to_vec(),
content_info_content_type: content_type, content_info_content_type: content_type,
@ -411,91 +411,94 @@ impl RpkiSignedObjectParsed {
} }
} }
fn parse_signed_data_from_contentinfo_parse( fn parse_signed_data_from_contentinfo_cursor(
obj: &DerObject<'_>, seq: &mut DerReader<'_>,
) -> Result<SignedDataParsed, SignedObjectParseError> { ) -> Result<SignedDataParsed, SignedObjectParseError> {
// ContentInfo.content is `[0] EXPLICIT`, but `der-parser` will represent unknown tagged let inner_der = seq.take_explicit_der(0xA0).map_err(|_e| {
// objects as `Unknown(Any)`. For EXPLICIT tags, the content octets are the full encoding of SignedObjectParseError::Parse("ContentInfo.content must be [0] EXPLICIT".into())
// the inner object, so we parse it from the object's slice. })?;
if obj.class() != Class::ContextSpecific || obj.tag() != Tag(0) { let mut r = DerReader::new(inner_der);
return Err(SignedObjectParseError::Parse( let signed_data_seq = r
"ContentInfo.content must be [0] EXPLICIT".into(), .take_sequence()
));
}
let inner_der = obj
.as_slice()
.map_err(|e| SignedObjectParseError::Parse(e.to_string()))?; .map_err(|e| SignedObjectParseError::Parse(e.to_string()))?;
let (rem, inner_obj) = if !r.is_empty() {
parse_der(inner_der).map_err(|e| SignedObjectParseError::Parse(e.to_string()))?;
if !rem.is_empty() {
return Err(SignedObjectParseError::Parse( return Err(SignedObjectParseError::Parse(
"trailing bytes inside ContentInfo.content".into(), "trailing bytes inside ContentInfo.content".into(),
)); ));
} }
parse_signed_data_parse(&inner_obj) parse_signed_data_cursor(signed_data_seq)
} }
fn parse_signed_data_parse( fn parse_signed_data_cursor(mut seq: DerReader<'_>) -> Result<SignedDataParsed, SignedObjectParseError> {
obj: &DerObject<'_>, let version = seq
) -> Result<SignedDataParsed, SignedObjectParseError> { .take_uint_u64()
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()
.map_err(|e| SignedObjectParseError::Parse(e.to_string()))?; .map_err(|e| SignedObjectParseError::Parse(e.to_string()))?;
let digest_set = seq[1] let digest_set_bytes = seq
.as_set() .take_tag(0x31)
.map_err(|e| SignedObjectParseError::Parse(e.to_string()))?; .map_err(|e| SignedObjectParseError::Parse(e.to_string()))?;
let mut digest_algorithms: Vec<AlgorithmIdentifierParsed> = let mut digest_set = DerReader::new(digest_set_bytes);
Vec::with_capacity(digest_set.len()); let mut digest_algorithms: Vec<AlgorithmIdentifierParsed> = Vec::new();
for item in digest_set { while !digest_set.is_empty() {
let (oid, params_ok) = parse_algorithm_identifier_parse(item)?; 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 }); 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<Vec<Vec<u8>>> = None; let mut certificates: Option<Vec<Vec<u8>>> = None;
let mut crls_present = false; let mut crls_present = false;
let mut signer_infos_obj: Option<&DerObject<'_>> = None; let mut signer_infos: Option<Vec<SignerInfoParsed>> = None;
for item in &seq[3..] { while !seq.is_empty() {
if item.class() == Class::ContextSpecific && item.tag() == Tag(0) { let tag = seq
.peek_tag()
.map_err(|e| SignedObjectParseError::Parse(e.to_string()))?;
match tag {
0xA0 => {
if certificates.is_some() { if certificates.is_some() {
return Err(SignedObjectParseError::Parse( return Err(SignedObjectParseError::Parse(
"SignedData.certificates appears more than once".into(), "SignedData.certificates appears more than once".into(),
)); ));
} }
certificates = Some(parse_certificate_set_implicit_parse(item)?); let content = seq
} else if item.class() == Class::ContextSpecific && item.tag() == Tag(1) { .take_tag(0xA0)
.map_err(|e| SignedObjectParseError::Parse(e.to_string()))?;
certificates = Some(split_der_objects(content)?);
}
0xA1 => {
crls_present = true; crls_present = true;
} else if item.class() == Class::Universal && item.tag() == Tag::Set { seq.skip_any()
signer_infos_obj = Some(item); .map_err(|e| SignedObjectParseError::Parse(e.to_string()))?;
} else { }
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( return Err(SignedObjectParseError::Parse(
"unexpected field in SignedData".into(), "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<SignerInfoParsed> = 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 { Ok(SignedDataParsed {
version, version,
digest_algorithms, digest_algorithms,
@ -506,46 +509,39 @@ fn parse_signed_data_parse(
}) })
} }
fn parse_encapsulated_content_info_parse( fn parse_encapsulated_content_info_cursor(
obj: &DerObject<'_>, mut seq: DerReader<'_>,
) -> Result<EncapsulatedContentInfoParsed, SignedObjectParseError> { ) -> Result<EncapsulatedContentInfoParsed, SignedObjectParseError> {
let seq = obj if seq.is_empty() {
.as_sequence()
.map_err(|e| SignedObjectParseError::Parse(e.to_string()))?;
if seq.is_empty() || seq.len() > 2 {
return Err(SignedObjectParseError::Parse( return Err(SignedObjectParseError::Parse(
"EncapsulatedContentInfo must be SEQUENCE of 1..2".into(), "EncapsulatedContentInfo must be SEQUENCE of 1..2".into(),
)); ));
} }
let econtent_type = oid_to_string_parse(&seq[0])?;
let econtent = match seq.get(1) { let econtent_type = take_oid_string(&mut seq)?;
None => None,
Some(econtent_tagged) => { let econtent = if seq.is_empty() {
if econtent_tagged.class() != Class::ContextSpecific || econtent_tagged.tag() != Tag(0) None
{ } else {
return Err(SignedObjectParseError::Parse( let inner_der = seq.take_explicit_der(0xA0).map_err(|_e| {
"EncapsulatedContentInfo.eContent must be [0] EXPLICIT".into(), SignedObjectParseError::Parse("EncapsulatedContentInfo.eContent must be [0] EXPLICIT".into())
)); })?;
} let mut inner = DerReader::new(inner_der);
let inner_der = econtent_tagged let octets = inner
.as_slice() .take_octet_string()
.map_err(|e| SignedObjectParseError::Parse(e.to_string()))?; .map_err(|e| SignedObjectParseError::Parse(e.to_string()))?;
let (rem, inner_obj) = if !inner.is_empty() {
parse_der(inner_der).map_err(|e| SignedObjectParseError::Parse(e.to_string()))?;
if !rem.is_empty() {
return Err(SignedObjectParseError::Parse( return Err(SignedObjectParseError::Parse(
"trailing bytes inside EncapsulatedContentInfo.eContent".into(), "trailing bytes inside EncapsulatedContentInfo.eContent".into(),
)); ));
} }
Some( Some(octets.to_vec())
inner_obj
.as_slice()
.map_err(|e| SignedObjectParseError::Parse(e.to_string()))?
.to_vec(),
)
}
}; };
if !seq.is_empty() {
return Err(SignedObjectParseError::Parse(
"EncapsulatedContentInfo must be SEQUENCE of 1..2".into(),
));
}
Ok(EncapsulatedContentInfoParsed { Ok(EncapsulatedContentInfoParsed {
econtent_type, econtent_type,
@ -553,22 +549,28 @@ fn parse_encapsulated_content_info_parse(
}) })
} }
fn parse_certificate_set_implicit_parse( fn split_der_objects(mut input: &[u8]) -> Result<Vec<Vec<u8>>, SignedObjectParseError> {
obj: &DerObject<'_>, let mut out: Vec<Vec<u8>> = Vec::new();
) -> Result<Vec<Vec<u8>>, SignedObjectParseError> {
let content = obj
.as_slice()
.map_err(|e| SignedObjectParseError::Parse(e.to_string()))?;
let mut input = content;
let mut certs = Vec::new();
while !input.is_empty() { while !input.is_empty() {
let (rem, _any_obj) = let (_tag, _value, rem) =
parse_der(input).map_err(|e| SignedObjectParseError::Parse(e.to_string()))?; der_take_tlv(input).map_err(|e| SignedObjectParseError::Parse(e.to_string()))?;
let consumed = input.len() - rem.len(); let consumed = input.len() - rem.len();
certs.push(input[..consumed].to_vec()); out.push(input[..consumed].to_vec());
input = rem; input = rem;
} }
Ok(certs) Ok(out)
}
fn parse_signer_infos_set_cursor(set_bytes: &[u8]) -> Result<Vec<SignerInfoParsed>, SignedObjectParseError> {
let mut set = DerReader::new(set_bytes);
let mut out: Vec<SignerInfoParsed> = 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<ResourceEeCertificate, SignedObjectValidateError> { fn validate_ee_certificate(der: &[u8]) -> Result<ResourceEeCertificate, SignedObjectValidateError> {
@ -603,11 +605,7 @@ fn validate_ee_certificate(der: &[u8]) -> Result<ResourceEeCertificate, SignedOb
.as_ref() .as_ref()
.ok_or(SignedObjectValidateError::EeCertificateMissingSia)?; .ok_or(SignedObjectValidateError::EeCertificateMissingSia)?;
let signed_object_uris: Vec<String> = match sia { let signed_object_uris: Vec<String> = match sia {
SubjectInfoAccess::Ee(ee) => ee SubjectInfoAccess::Ee(ee) => ee.signed_object_uris.clone(),
.signed_object_uris
.iter()
.map(|u| u.as_str().to_string())
.collect(),
SubjectInfoAccess::Ca(_ca) => Vec::new(), SubjectInfoAccess::Ca(_ca) => Vec::new(),
}; };
if signed_object_uris.is_empty() { if signed_object_uris.is_empty() {
@ -626,69 +624,64 @@ fn validate_ee_certificate(der: &[u8]) -> Result<ResourceEeCertificate, SignedOb
}) })
} }
fn parse_signer_info_parse( fn parse_signer_info_cursor(mut seq: DerReader<'_>) -> Result<SignerInfoParsed, SignedObjectParseError> {
obj: &DerObject<'_>, let version = seq
) -> Result<SignerInfoParsed, SignedObjectParseError> { .take_uint_u64()
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()
.map_err(|e| SignedObjectParseError::Parse(e.to_string()))?; .map_err(|e| SignedObjectParseError::Parse(e.to_string()))?;
let sid = &seq[1]; let (sid_tag, sid_bytes) = seq
let sid = if sid.class() == Class::ContextSpecific && sid.tag() == Tag(0) { .take_any()
let ski = sid .map_err(|e| SignedObjectParseError::Parse(e.to_string()))?;
.as_slice() let sid = if (sid_tag & 0xC0) == 0x80 && (sid_tag & 0x1F) == 0 {
.map_err(|e| SignedObjectParseError::Parse(e.to_string()))? SignerIdentifierParsed::SubjectKeyIdentifier(sid_bytes.to_vec())
.to_vec();
SignerIdentifierParsed::SubjectKeyIdentifier(ski)
} else { } else {
SignerIdentifierParsed::Other 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 { let digest_algorithm = AlgorithmIdentifierParsed {
oid: digest_oid, oid: digest_oid,
params_ok: digest_params_ok, params_ok: digest_params_ok,
}; };
let mut idx = 3;
let mut signed_attrs_content: Option<Vec<u8>> = None; let mut signed_attrs_content: Option<Vec<u8>> = None;
let mut signed_attrs_der_for_signature: Option<Vec<u8>> = None; let mut signed_attrs_der_for_signature: Option<Vec<u8>> = None;
if seq[idx].class() == Class::ContextSpecific && seq[idx].tag() == Tag(0) { if seq
let signed_attrs_obj = &seq[idx]; .peek_tag()
let content = signed_attrs_obj
.as_slice()
.map_err(|e| SignedObjectParseError::Parse(e.to_string()))? .map_err(|e| SignedObjectParseError::Parse(e.to_string()))?
.to_vec(); == 0xA0
signed_attrs_content = Some(content); {
signed_attrs_der_for_signature = let (tag, full_tlv, value) = seq
Some(make_signed_attrs_der_for_signature_parse(signed_attrs_obj)?); .take_any_full()
idx += 1; .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 { let signature_algorithm = AlgorithmIdentifierParsed {
oid: signature_oid, oid: signature_oid,
params_ok: signature_params_ok, params_ok: signature_params_ok,
}; };
idx += 1;
let signature = seq[idx] let signature = seq
.as_slice() .take_octet_string()
.map_err(|e| SignedObjectParseError::Parse(e.to_string()))? .map_err(|e| SignedObjectParseError::Parse(e.to_string()))?
.to_vec(); .to_vec();
idx += 1;
let unsigned_attrs_present = seq.get(idx).is_some(); let unsigned_attrs_present = !seq.is_empty();
Ok(SignerInfoParsed { Ok(SignerInfoParsed {
version, version,
@ -696,9 +689,9 @@ fn parse_signer_info_parse(
digest_algorithm, digest_algorithm,
signature_algorithm, signature_algorithm,
signed_attrs_content, signed_attrs_content,
signed_attrs_der_for_signature,
unsigned_attrs_present, unsigned_attrs_present,
signature, signature,
signed_attrs_der_for_signature,
}) })
} }
@ -843,58 +836,90 @@ fn parse_signed_attrs_implicit(
let mut message_digest: Option<Vec<u8>> = None; let mut message_digest: Option<Vec<u8>> = None;
let mut signing_time: Option<Asn1TimeUtc> = None; let mut signing_time: Option<Asn1TimeUtc> = None;
let mut remaining = input; fn count_elements(mut r: DerReader<'_>) -> Result<usize, String> {
while !remaining.is_empty() { let mut n = 0usize;
let (rem, attr_obj) = parse_der(remaining) while !r.is_empty() {
.map_err(|e| SignedObjectValidateError::SignedAttrsParse(e.to_string()))?; r.skip_any()?;
remaining = rem; n += 1;
}
Ok(n)
}
let attr_seq = attr_obj let mut remaining = DerReader::new(input);
.as_sequence() while !remaining.is_empty() {
let mut attr = remaining
.take_sequence()
.map_err(|e| SignedObjectValidateError::SignedAttrsParse(e.to_string()))?; .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( return Err(SignedObjectValidateError::SignedAttrsParse(
"Attribute must be SEQUENCE of 2".into(), "Attribute must be SEQUENCE of 2".into(),
)); ));
} }
let oid = oid_to_string_parse(&attr_seq[0])
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()))?; .map_err(|e| SignedObjectValidateError::SignedAttrsParse(e.to_string()))?;
let values_set = attr_seq[1] if values.is_empty() {
.as_set() 1
.map_err(|e| SignedObjectValidateError::SignedAttrsParse(e.to_string()))?; } else {
if values_set.len() != 1 { 1 + count_elements(values)
return Err( .map_err(|e| SignedObjectValidateError::SignedAttrsParse(e.to_string()))?
SignedObjectValidateError::InvalidSignedAttributeValuesCount {
oid,
count: values_set.len(),
},
);
} }
};
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() { match oid.as_str() {
OID_CMS_ATTR_CONTENT_TYPE => { OID_CMS_ATTR_CONTENT_TYPE => {
if content_type.is_some() { if content_type.is_some() {
return Err(SignedObjectValidateError::DuplicateSignedAttribute(oid)); return Err(SignedObjectValidateError::DuplicateSignedAttribute(oid));
} }
let v = oid_to_string_parse(&values_set[0]) if val_tag != 0x06 {
.map_err(|e| SignedObjectValidateError::SignedAttrsParse(e.to_string()))?; return Err(SignedObjectValidateError::SignedAttrsParse(
content_type = Some(v); "content-type attr value must be OBJECT IDENTIFIER".into(),
));
}
content_type = Some(oid_value_bytes_to_string(val_bytes));
} }
OID_CMS_ATTR_MESSAGE_DIGEST => { OID_CMS_ATTR_MESSAGE_DIGEST => {
if message_digest.is_some() { if message_digest.is_some() {
return Err(SignedObjectValidateError::DuplicateSignedAttribute(oid)); return Err(SignedObjectValidateError::DuplicateSignedAttribute(oid));
} }
let v = values_set[0] if val_tag != 0x04 {
.as_slice() return Err(SignedObjectValidateError::SignedAttrsParse(
.map_err(|e| SignedObjectValidateError::SignedAttrsParse(e.to_string()))? "message-digest attr value must be OCTET STRING".into(),
.to_vec(); ));
message_digest = Some(v); }
message_digest = Some(val_bytes.to_vec());
} }
OID_CMS_ATTR_SIGNING_TIME => { OID_CMS_ATTR_SIGNING_TIME => {
if signing_time.is_some() { if signing_time.is_some() {
return Err(SignedObjectValidateError::DuplicateSignedAttribute(oid)); 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)); return Err(SignedObjectValidateError::UnsupportedSignedAttribute(oid));
@ -913,35 +938,27 @@ fn parse_signed_attrs_implicit(
}) })
} }
fn parse_signing_time_value(obj: &DerObject<'_>) -> Result<Asn1TimeUtc, SignedObjectValidateError> { fn parse_signing_time_value_tlv(tag: u8, value: &[u8]) -> Result<Asn1TimeUtc, SignedObjectValidateError> {
match &obj.content { match tag {
der_parser::ber::BerObjectContent::UTCTime(dt) => Ok(Asn1TimeUtc { 0x17 => Ok(Asn1TimeUtc {
utc: dt utc: parse_utctime(value)?,
.to_datetime()
.map_err(|_| SignedObjectValidateError::InvalidSigningTimeValue)?,
encoding: Asn1TimeEncoding::UtcTime, encoding: Asn1TimeEncoding::UtcTime,
}), }),
der_parser::ber::BerObjectContent::GeneralizedTime(dt) => Ok(Asn1TimeUtc { 0x18 => Ok(Asn1TimeUtc {
utc: dt utc: parse_generalized_time(value)?,
.to_datetime()
.map_err(|_| SignedObjectValidateError::InvalidSigningTimeValue)?,
encoding: Asn1TimeEncoding::GeneralizedTime, encoding: Asn1TimeEncoding::GeneralizedTime,
}), }),
_ => Err(SignedObjectValidateError::InvalidSigningTimeValue), _ => Err(SignedObjectValidateError::InvalidSigningTimeValue),
} }
} }
fn make_signed_attrs_der_for_signature_parse( fn make_signed_attrs_der_for_signature(full_tlv: &[u8]) -> Result<Vec<u8>, SignedObjectParseError> {
obj: &DerObject<'_>,
) -> Result<Vec<u8>, SignedObjectParseError> {
// We need the DER encoding of SignedAttributes (SET OF Attribute) as signature input. // 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 // 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 // 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. // is replaced with the universal SET tag (0x31), leaving length+content unchanged.
// //
let mut cs_der = obj let mut cs_der = full_tlv.to_vec();
.to_vec()
.map_err(|e| SignedObjectParseError::Parse(e.to_string()))?;
if cs_der.is_empty() { if cs_der.is_empty() {
return Err(SignedObjectParseError::Parse( return Err(SignedObjectParseError::Parse(
"signedAttrs encoding is empty".into(), "signedAttrs encoding is empty".into(),
@ -953,32 +970,162 @@ fn make_signed_attrs_der_for_signature_parse(
Ok(cs_der) Ok(cs_der)
} }
fn oid_to_string_parse(obj: &DerObject<'_>) -> Result<String, SignedObjectParseError> { fn take_oid_string(seq: &mut DerReader<'_>) -> Result<String, SignedObjectParseError> {
let oid = obj let oid = seq
.as_oid() .take_tag(0x06)
.map_err(|e| SignedObjectParseError::Parse(e.to_string()))?; .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( fn oid_value_bytes_to_string(oid_value: &[u8]) -> String {
obj: &DerObject<'_>, 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 "<empty-oid>".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(".<truncated>");
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> { ) -> Result<(String, bool), SignedObjectParseError> {
let seq = obj if seq.is_empty() {
.as_sequence()
.map_err(|e| SignedObjectParseError::Parse(e.to_string()))?;
if seq.is_empty() || seq.len() > 2 {
return Err(SignedObjectParseError::Parse( return Err(SignedObjectParseError::Parse(
"AlgorithmIdentifier must be SEQUENCE of 1..2".into(), "AlgorithmIdentifier must be SEQUENCE of 1..2".into(),
)); ));
} }
let oid = oid_to_string_parse(&seq[0])?; let oid = take_oid_string(&mut seq)?;
let params_ok = match seq.get(1) { let params_ok = if seq.is_empty() {
None => true, true
Some(p) => matches!(p.content, der_parser::ber::BerObjectContent::Null), } 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)) Ok((oid, params_ok))
} }
fn parse_utctime(value: &[u8]) -> Result<time::OffsetDateTime, SignedObjectValidateError> {
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<time::OffsetDateTime, SignedObjectValidateError> {
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] { fn strip_leading_zeros(bytes: &[u8]) -> &[u8] {
let mut idx = 0; let mut idx = 0;
while idx < bytes.len() && bytes[idx] == 0 { while idx < bytes.len() && bytes[idx] == 0 {

View File

@ -211,7 +211,7 @@ impl TaCertificateParsed {
return Err(TaCertificateProfileError::NotCa); 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); return Err(TaCertificateProfileError::NotSelfSignedIssuerSubject);
} }

View File

@ -78,23 +78,23 @@ pub fn ca_instance_uris_from_ca_certificate(
for ad in access_descriptions { for ad in access_descriptions {
if ad.access_method_oid == OID_AD_CA_REPOSITORY { if ad.access_method_oid == OID_AD_CA_REPOSITORY {
let u = ad.access_location.to_string(); let u = ad.access_location.as_str();
if ad.access_location.scheme() != "rsync" { if !u.starts_with("rsync://") {
return Err(CaInstanceUrisError::CaRepositoryNotRsync(u)); 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 { } else if ad.access_method_oid == OID_AD_RPKI_MANIFEST {
let u = ad.access_location.to_string(); let u = ad.access_location.as_str();
if ad.access_location.scheme() != "rsync" { if !u.starts_with("rsync://") {
return Err(CaInstanceUrisError::RpkiManifestNotRsync(u)); 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 { } else if ad.access_method_oid == OID_AD_RPKI_NOTIFY {
let u = ad.access_location.to_string(); let u = ad.access_location.as_str();
if ad.access_location.scheme() != "https" { if !u.starts_with("https://") {
return Err(CaInstanceUrisError::RpkiNotifyNotHttps(u)); return Err(CaInstanceUrisError::RpkiNotifyNotHttps(u.to_string()));
} }
notify.get_or_insert(u); notify.get_or_insert(u.to_string());
} }
} }

View File

@ -1,6 +1,6 @@
use crate::data_model::common::BigUnsigned; use crate::data_model::common::BigUnsigned;
use crate::data_model::crl::{CrlDecodeError, CrlVerifyError, RpkixCrl}; 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::{ use crate::data_model::rc::{
AsIdentifierChoice, AsResourceSet, IpAddressChoice, IpResourceSet, ResourceCertKind, AsIdentifierChoice, AsResourceSet, IpAddressChoice, IpResourceSet, ResourceCertKind,
ResourceCertificate, ResourceCertificateDecodeError, ResourceCertificate, ResourceCertificateDecodeError,
@ -135,10 +135,10 @@ pub fn validate_subordinate_ca_cert(
return Err(CaPathError::IssuerNotCa); 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 { return Err(CaPathError::IssuerSubjectMismatch {
child_issuer_dn: child_ca.tbs.issuer_dn.clone(), child_issuer_dn: child_ca.tbs.issuer_name.to_string(),
issuer_subject_dn: issuer_ca.tbs.subject_dn.clone(), 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<bool> = None; let mut ku_critical: Option<bool> = None;
for ext in cert.extensions() { 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); ku_critical = Some(ext.critical);
break; break;
} }
@ -695,9 +695,8 @@ mod tests {
use crate::data_model::rc::{ use crate::data_model::rc::{
RcExtensions, ResourceCertKind, ResourceCertificate, RpkixTbsCertificate, RcExtensions, ResourceCertKind, ResourceCertificate, RpkixTbsCertificate,
}; };
use crate::data_model::common::X509NameDer;
use der_parser::num_bigint::BigUint; use der_parser::num_bigint::BigUint;
use url::Url;
fn dummy_cert( fn dummy_cert(
kind: ResourceCertKind, kind: ResourceCertKind,
subject_dn: &str, subject_dn: &str,
@ -707,16 +706,8 @@ mod tests {
aia: Option<Vec<&str>>, aia: Option<Vec<&str>>,
crldp: Option<Vec<&str>>, crldp: Option<Vec<&str>>,
) -> ResourceCertificate { ) -> ResourceCertificate {
let aia = aia.map(|v| { let aia = aia.map(|v| v.into_iter().map(|s| s.to_string()).collect::<Vec<_>>());
v.into_iter() let crldp = crldp.map(|v| v.into_iter().map(|s| s.to_string()).collect::<Vec<_>>());
.map(|s| Url::parse(s).expect("url"))
.collect::<Vec<_>>()
});
let crldp = crldp.map(|v| {
v.into_iter()
.map(|s| Url::parse(s).expect("url"))
.collect::<Vec<_>>()
});
ResourceCertificate { ResourceCertificate {
raw_der: Vec::new(), raw_der: Vec::new(),
@ -725,8 +716,8 @@ mod tests {
version: 2, version: 2,
serial_number: BigUint::from(1u8), serial_number: BigUint::from(1u8),
signature_algorithm: "1.2.840.113549.1.1.11".to_string(), signature_algorithm: "1.2.840.113549.1.1.11".to_string(),
issuer_dn: issuer_dn.to_string(), issuer_name: X509NameDer(issuer_dn.as_bytes().to_vec()),
subject_dn: subject_dn.to_string(), subject_name: X509NameDer(subject_dn.as_bytes().to_vec()),
validity_not_before: time::OffsetDateTime::UNIX_EPOCH, validity_not_before: time::OffsetDateTime::UNIX_EPOCH,
validity_not_after: time::OffsetDateTime::UNIX_EPOCH, validity_not_after: time::OffsetDateTime::UNIX_EPOCH,
subject_public_key_info: Vec::new(), subject_public_key_info: Vec::new(),

View File

@ -112,10 +112,10 @@ pub fn validate_ee_cert_path(
return Err(CertPathError::IssuerNotCa); 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 { return Err(CertPathError::IssuerSubjectMismatch {
ee_issuer_dn: ee.tbs.issuer_dn.clone(), ee_issuer_dn: ee.tbs.issuer_name.to_string(),
issuer_subject_dn: issuer_ca.tbs.subject_dn.clone(), 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<bool> = None; let mut ku_critical: Option<bool> = None;
for ext in cert.extensions() { 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); ku_critical = Some(ext.critical);
break; break;
} }
@ -302,8 +302,8 @@ mod tests {
use crate::data_model::rc::{ use crate::data_model::rc::{
RcExtensions, ResourceCertKind, ResourceCertificate, RpkixTbsCertificate, RcExtensions, ResourceCertKind, ResourceCertificate, RpkixTbsCertificate,
}; };
use crate::data_model::common::X509NameDer;
use der_parser::num_bigint::BigUint; use der_parser::num_bigint::BigUint;
use url::Url;
fn dummy_cert( fn dummy_cert(
kind: ResourceCertKind, kind: ResourceCertKind,
@ -314,16 +314,8 @@ mod tests {
aia: Option<Vec<&str>>, aia: Option<Vec<&str>>,
crldp: Option<Vec<&str>>, crldp: Option<Vec<&str>>,
) -> ResourceCertificate { ) -> ResourceCertificate {
let aia = aia.map(|v| { let aia = aia.map(|v| v.into_iter().map(|s| s.to_string()).collect::<Vec<_>>());
v.into_iter() let crldp = crldp.map(|v| v.into_iter().map(|s| s.to_string()).collect::<Vec<_>>());
.map(|s| Url::parse(s).expect("url"))
.collect::<Vec<_>>()
});
let crldp = crldp.map(|v| {
v.into_iter()
.map(|s| Url::parse(s).expect("url"))
.collect::<Vec<_>>()
});
ResourceCertificate { ResourceCertificate {
raw_der: Vec::new(), raw_der: Vec::new(),
kind, kind,
@ -331,8 +323,8 @@ mod tests {
version: 2, version: 2,
serial_number: BigUint::from(1u8), serial_number: BigUint::from(1u8),
signature_algorithm: "1.2.840.113549.1.1.11".to_string(), signature_algorithm: "1.2.840.113549.1.1.11".to_string(),
issuer_dn: issuer_dn.to_string(), issuer_name: X509NameDer(issuer_dn.as_bytes().to_vec()),
subject_dn: subject_dn.to_string(), subject_name: X509NameDer(subject_dn.as_bytes().to_vec()),
validity_not_before: time::OffsetDateTime::UNIX_EPOCH, validity_not_before: time::OffsetDateTime::UNIX_EPOCH,
validity_not_after: time::OffsetDateTime::UNIX_EPOCH, validity_not_after: time::OffsetDateTime::UNIX_EPOCH,
subject_public_key_info: Vec::new(), subject_public_key_info: Vec::new(),

View File

@ -462,7 +462,7 @@ fn process_aspa_with_issuer(
} }
fn choose_crl_for_certificate( fn choose_crl_for_certificate(
crldp_uris: Option<&Vec<url::Url>>, crldp_uris: Option<&Vec<String>>,
crl_files: &[(String, Vec<u8>)], crl_files: &[(String, Vec<u8>)],
) -> Result<(String, Vec<u8>), ObjectValidateError> { ) -> Result<(String, Vec<u8>), ObjectValidateError> {
if crl_files.is_empty() { if crl_files.is_empty() {

View File

@ -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<String, u32>,
samples: Vec<SampleMeta>,
}
fn env_string(name: &str) -> Option<String> {
std::env::var(name).ok().filter(|s| !s.trim().is_empty())
}
fn env_u64_opt(name: &str) -> Option<u64> {
std::env::var(name).ok().and_then(|s| s.parse::<u64>().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<PathBuf>) -> 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<CandidateRef>,
out_crl: &mut Vec<CandidateRef>,
out_manifest: &mut Vec<CandidateRef>,
out_roa: &mut Vec<CandidateRef>,
out_aspa: &mut Vec<CandidateRef>,
) -> 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<u8>,
}
fn extract_candidate(pack_path: &Path, cand: CandidateRef) -> Result<Extracted, String> {
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<Metrics, String> {
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::<CandidateRef>::new();
let mut crl = Vec::<CandidateRef>::new();
let mut mft = Vec::<CandidateRef>::new();
let mut roa = Vec::<CandidateRef>::new();
let mut asa = Vec::<CandidateRef>::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<ObjType, Vec<CandidateRef>> = 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::<SampleMeta>::new();
let mut per_type_out = BTreeMap::<String, u32>::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::<String>::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<u8, String> {
Ok(self.read_exact(1)?[0])
}
fn read_u32_be(&mut self) -> Result<u32, String> {
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<u64, String> {
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<usize, String> {
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<i64, String> {
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<time::OffsetDateTime, String> {
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<String, String> {
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<Vec<u8>, 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())
}
}

View File

@ -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<Sample> {
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::<u64>().ok())
.unwrap_or(default)
}
fn env_u64_opt(name: &str) -> Option<u64> {
std::env::var(name).ok().and_then(|s| s.parse::<u64>().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<String> {
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<Self, String> {
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<F: FnMut()>(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<String>,
sample: Option<String>,
fixed_iters: Option<u64>,
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<ResultRow>,
}
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<ResultRow> = 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<ResultRow> = 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::<f64>() / (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::<f64>() / (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());
}
}
}

View File

@ -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<u64>,
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<SizesSummary> {
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<String, SizesSummary>,
}
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<String> {
std::env::var(name).ok().filter(|s| !s.trim().is_empty())
}
fn env_u64_opt(name: &str) -> Option<u64> {
std::env::var(name).ok().and_then(|s| s.parse::<u64>().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<PathBuf>) -> 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<String, Sizes> = 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<String, SizesSummary> = 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()));
}
}

View File

@ -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_<type>_<sample>_decode.data
# - flamegraph_stage2_ours_<type>_<sample>_decode_release.svg
# - hotspots_stage2_ours_<type>_<sample>_decode_release.txt
# - perf_stage2_ours_<type>_<sample>_landing.data
# - flamegraph_stage2_ours_<type>_<sample>_landing_release.svg
# - hotspots_stage2_ours_<type>_<sample>_landing_release.txt
# - perf_stage2_routinator_<type>_<sample>_decode.data
# - flamegraph_stage2_routinator_<type>_<sample>_decode_release.svg
# - hotspots_stage2_routinator_<type>_<sample>_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" <<EOF
#!/usr/bin/env bash
exec "${PERF_REAL}" "\$@"
EOF
chmod +x "${SHIM_DIR}/perf"
export PATH="${SHIM_DIR}:${PATH}"
echo "Using perf shim -> ${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

255
tests/benchmark/sap.rs Normal file
View File

@ -0,0 +1,255 @@
use std::path::Path;
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct SapPublicationPoint {
pub header: SapPointHeader,
pub manifest: Option<SapStoredManifest>,
pub objects: Vec<SapStoredObject>,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct SapPointHeader {
pub version: u8,
pub manifest_uri: String,
pub rpki_notify: Option<String>,
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<u8>,
pub crl_uri: String,
pub crl_der: Vec<u8>,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct SapStoredObject {
pub uri: String,
pub hash_sha256: Option<[u8; 32]>,
pub content_der: Vec<u8>,
}
#[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<Self, SapDecodeError> {
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<Self, SapDecodeError> {
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<u8, SapDecodeError> {
Ok(self.read_exact(1)?[0])
}
fn read_u32_be(&mut self) -> Result<u32, SapDecodeError> {
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<u64, SapDecodeError> {
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<i64, SapDecodeError> {
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<time::OffsetDateTime, SapDecodeError> {
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<String, SapDecodeError> {
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<Option<String>, SapDecodeError> {
if self.remaining() == 0 {
return Ok(None);
}
Ok(Some(self.read_string_u32()?))
}
fn read_option_string_u32(&mut self) -> Result<Option<String>, 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<Vec<u8>, 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)
}
}

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -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
}
}
]
}

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -2,6 +2,7 @@ use der_parser::num_bigint::BigUint;
use time::OffsetDateTime; use time::OffsetDateTime;
use rpki::data_model::aspa::{AspaEContent, AspaValidateError}; use rpki::data_model::aspa::{AspaEContent, AspaValidateError};
use rpki::data_model::common::X509NameDer;
use rpki::data_model::rc::{ use rpki::data_model::rc::{
AsIdOrRange, AsIdentifierChoice, AsResourceSet, IpResourceSet, RcExtensions, ResourceCertKind, AsIdOrRange, AsIdentifierChoice, AsResourceSet, IpResourceSet, RcExtensions, ResourceCertKind,
ResourceCertificate, RpkixTbsCertificate, SubjectInfoAccess, ResourceCertificate, RpkixTbsCertificate, SubjectInfoAccess,
@ -17,8 +18,8 @@ fn dummy_ee(
version: 2, version: 2,
serial_number: BigUint::from(1u8), serial_number: BigUint::from(1u8),
signature_algorithm: "1.2.840.113549.1.1.11".to_string(), signature_algorithm: "1.2.840.113549.1.1.11".to_string(),
issuer_dn: "CN=issuer".to_string(), issuer_name: X509NameDer(b"CN=issuer".to_vec()),
subject_dn: "CN=subject".to_string(), subject_name: X509NameDer(b"CN=subject".to_vec()),
validity_not_before: OffsetDateTime::UNIX_EPOCH, validity_not_before: OffsetDateTime::UNIX_EPOCH,
validity_not_after: OffsetDateTime::UNIX_EPOCH, validity_not_after: OffsetDateTime::UNIX_EPOCH,
subject_public_key_info: vec![], subject_public_key_info: vec![],

View File

@ -127,8 +127,8 @@ impl From<&RpkixTbsCertificate> for RpkixTbsCertificatePretty {
version: v.version, version: v.version,
serial_number: hex::encode(v.serial_number.to_bytes_be()), serial_number: hex::encode(v.serial_number.to_bytes_be()),
signature_algorithm: v.signature_algorithm.clone(), signature_algorithm: v.signature_algorithm.clone(),
issuer_dn: v.issuer_dn.clone(), issuer_dn: v.issuer_name.to_string(),
subject_dn: v.subject_dn.clone(), subject_dn: v.subject_name.to_string(),
validity_not_before: v.validity_not_before, validity_not_before: v.validity_not_before,
validity_not_after: v.validity_not_after, validity_not_after: v.validity_not_after,
subject_public_key_info: bytes_fmt(&v.subject_public_key_info), subject_public_key_info: bytes_fmt(&v.subject_public_key_info),

View File

@ -120,7 +120,7 @@ fn canonicalize_sorts_families_sorts_and_dedups_addresses() {
IpPrefix { IpPrefix {
afi: RoaAfi::Ipv4, afi: RoaAfi::Ipv4,
prefix_len: 24, 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],
} }
); );
} }

View File

@ -1,6 +1,7 @@
use der_parser::num_bigint::BigUint; use der_parser::num_bigint::BigUint;
use time::OffsetDateTime; use time::OffsetDateTime;
use rpki::data_model::common::X509NameDer;
use rpki::data_model::rc::{ use rpki::data_model::rc::{
Afi, AsResourceSet, IpAddressChoice, IpAddressFamily, IpAddressOrRange, IpPrefix, Afi, AsResourceSet, IpAddressChoice, IpAddressFamily, IpAddressOrRange, IpPrefix,
IpResourceSet, RcExtensions, ResourceCertKind, ResourceCertificate, RpkixTbsCertificate, IpResourceSet, RcExtensions, ResourceCertKind, ResourceCertificate, RpkixTbsCertificate,
@ -20,8 +21,8 @@ fn dummy_ee(
version: 2, version: 2,
serial_number: BigUint::from(1u8), serial_number: BigUint::from(1u8),
signature_algorithm: "1.2.840.113549.1.1.11".to_string(), signature_algorithm: "1.2.840.113549.1.1.11".to_string(),
issuer_dn: "CN=issuer".to_string(), issuer_name: X509NameDer(b"CN=issuer".to_vec()),
subject_dn: "CN=subject".to_string(), subject_name: X509NameDer(b"CN=subject".to_vec()),
validity_not_before: OffsetDateTime::UNIX_EPOCH, validity_not_before: OffsetDateTime::UNIX_EPOCH,
validity_not_after: OffsetDateTime::UNIX_EPOCH, validity_not_after: OffsetDateTime::UNIX_EPOCH,
subject_public_key_info: vec![], subject_public_key_info: vec![],
@ -55,7 +56,7 @@ fn test_roa_single_v4_prefix() -> RoaEContent {
prefix: rpki::data_model::roa::IpPrefix { prefix: rpki::data_model::roa::IpPrefix {
afi: RoaAfi::Ipv4, afi: RoaAfi::Ipv4,
prefix_len: 8, 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), max_length: Some(24),
}], }],
@ -159,7 +160,24 @@ fn contains_prefix_handles_non_octet_boundary_prefix_len() {
prefix: rpki::data_model::roa::IpPrefix { prefix: rpki::data_model::roa::IpPrefix {
afi: RoaAfi::Ipv4, afi: RoaAfi::Ipv4,
prefix_len: 16, 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, max_length: None,
}], }],

View File

@ -1,4 +1,5 @@
use der_parser::num_bigint::BigUint; 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::oid::OID_CP_IPADDR_ASNUMBER;
use rpki::data_model::rc::{ use rpki::data_model::rc::{
Afi, AsIdOrRange, AsIdentifierChoice, AsResourceSet, IpAddressChoice, IpAddressFamily, Afi, AsIdOrRange, AsIdentifierChoice, AsResourceSet, IpAddressChoice, IpAddressFamily,
@ -17,8 +18,8 @@ fn dummy_rc_ca(ext: RcExtensions) -> ResourceCertificate {
version: 2, version: 2,
serial_number: BigUint::from(1u32), serial_number: BigUint::from(1u32),
signature_algorithm: "1.2.840.113549.1.1.11".into(), signature_algorithm: "1.2.840.113549.1.1.11".into(),
issuer_dn: "CN=TA".into(), issuer_name: X509NameDer(b"CN=TA".to_vec()),
subject_dn: "CN=TA".into(), subject_name: X509NameDer(b"CN=TA".to_vec()),
validity_not_before: t, validity_not_before: t,
validity_not_after: t, validity_not_after: t,
subject_public_key_info: Vec::new(), subject_public_key_info: Vec::new(),

View File

@ -107,23 +107,16 @@ fn audit_helpers_format_roa_ip_prefix_smoke() {
let v4 = IpPrefix { let v4 = IpPrefix {
afi: RoaAfi::Ipv4, afi: RoaAfi::Ipv4,
prefix_len: 24, 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"); assert_eq!(rpki::audit::format_roa_ip_prefix(&v4), "192.0.2.0/24");
let v6 = IpPrefix { let v6 = IpPrefix {
afi: RoaAfi::Ipv6, afi: RoaAfi::Ipv6,
prefix_len: 32, 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")); 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] #[test]