20260511 CIR V3 trust anchors self-contained

This commit is contained in:
yuyr 2026-05-11 18:12:41 +08:00
parent 51e483d924
commit f2fbb20a29
25 changed files with 742 additions and 522 deletions

View File

@ -242,6 +242,30 @@ ta_file_for_rir() {
esac esac
} }
refresh_ta_file_for_rir() {
local rir="$1"
local uri
local file
uri="$(tal_uri_for_rir "$rir")"
file="$(ta_file_for_rir "$rir")"
python3 - <<'PY' "$uri" "$file"
import sys
import urllib.request
uri, path = sys.argv[1:]
request = urllib.request.Request(uri, headers={"User-Agent": "rpki-dev/compare-fast-path"})
with urllib.request.urlopen(request, timeout=30) as response:
data = response.read()
if not data:
raise SystemExit(f"empty TA certificate response: {uri}")
with open(path, "wb") as output:
output.write(data)
PY
}
for rir in "${RIRS[@]}"; do
refresh_ta_file_for_rir "$rir"
done
OURS_TAL_ARGS=() OURS_TAL_ARGS=()
CLIENT_TAL_ARGS=() CLIENT_TAL_ARGS=()
OURS_CIR_TAL_ARGS=() OURS_CIR_TAL_ARGS=()
@ -260,6 +284,8 @@ fi
mkdir -p state/ours/work-db state/ours/raw-store.db state/ours/repo-bytes.db state/rpki-client/cache state/rpki-client/out state/rpki-client/ta state/rpki-client/.ta mkdir -p state/ours/work-db state/ours/raw-store.db state/ours/repo-bytes.db state/rpki-client/cache state/rpki-client/out state/rpki-client/ta state/rpki-client/.ta
chmod 0777 state/ours/work-db state/ours/raw-store.db state/ours/repo-bytes.db chmod 0777 state/ours/work-db state/ours/raw-store.db state/ours/repo-bytes.db
chmod -R 0777 state/rpki-client chmod -R 0777 state/rpki-client
touch state/rpki-client/rpki-client-skiplist
chmod 0644 state/rpki-client/rpki-client-skiplist
START_EPOCH="$(python3 - <<'PY' START_EPOCH="$(python3 - <<'PY'
import time import time
@ -333,7 +359,7 @@ PY
set +e set +e
LD_LIBRARY_PATH="$REMOTE_ROOT/lib${LD_LIBRARY_PATH:+:$LD_LIBRARY_PATH}" "$REMOTE_ROOT/bin/rpki-client" \ LD_LIBRARY_PATH="$REMOTE_ROOT/lib${LD_LIBRARY_PATH:+:$LD_LIBRARY_PATH}" "$REMOTE_ROOT/bin/rpki-client" \
-vv \ -vv \
-S "$REMOTE_ROOT/rpki-client-skiplist" \ -S rpki-client-skiplist \
"${CLIENT_TAL_ARGS[@]}" \ "${CLIENT_TAL_ARGS[@]}" \
-d cache out \ -d cache out \
> "$REMOTE_ROOT/steps/$STEP_ID/rpki-client/run.log" 2>&1 > "$REMOTE_ROOT/steps/$STEP_ID/rpki-client/run.log" 2>&1

View File

@ -114,8 +114,8 @@ else:
diagnosis = "ccr_mismatch_cir_not_available" diagnosis = "ccr_mismatch_cir_not_available"
elif not cir_compared: elif not cir_compared:
diagnosis = "ccr_mismatch_cir_compare_skipped" diagnosis = "ccr_mismatch_cir_compare_skipped"
elif cir.get("tals", {}).get("match") is not True: elif cir.get("trustAnchors", {}).get("match") is not True:
diagnosis = "tal_input_difference" diagnosis = "trust_anchor_input_difference"
elif cir.get("objects", {}).get("match") is not True: elif cir.get("objects", {}).get("match") is not True:
diagnosis = "input_object_or_manifest_accepted_set_difference" diagnosis = "input_object_or_manifest_accepted_set_difference"
elif ( elif (
@ -154,10 +154,13 @@ combined = {
"allMatch": cir.get("allMatch") if cir else None, "allMatch": cir.get("allMatch") if cir else None,
"objectsMatch": cir.get("objects", {}).get("match") if cir else None, "objectsMatch": cir.get("objects", {}).get("match") if cir else None,
"rejectsMatch": cir.get("rejects", {}).get("match") if cir else None, "rejectsMatch": cir.get("rejects", {}).get("match") if cir else None,
"talsMatch": cir.get("tals", {}).get("match") if cir else None, "trustAnchorsMatch": cir.get("trustAnchors", {}).get("match") if cir else None,
"talsMatch": cir.get("trustAnchors", {}).get("match") if cir else None,
"rejectListSha256Match": cir.get("rejectListSha256Match") if cir else None, "rejectListSha256Match": cir.get("rejectListSha256Match") if cir else None,
"oursObjectCount": cir.get("ours", {}).get("objectCount") if cir else None, "oursObjectCount": cir.get("ours", {}).get("objectCount") if cir else None,
"rpkiClientObjectCount": cir.get("rpkiClient", {}).get("objectCount") if cir else None, "rpkiClientObjectCount": cir.get("rpkiClient", {}).get("objectCount") if cir else None,
"oursTrustAnchorCount": cir.get("ours", {}).get("trustAnchorCount") if cir else None,
"rpkiClientTrustAnchorCount": cir.get("rpkiClient", {}).get("trustAnchorCount") if cir else None,
"oursRejectCount": cir.get("ours", {}).get("rejectCount") if cir else None, "oursRejectCount": cir.get("ours", {}).get("rejectCount") if cir else None,
"rpkiClientRejectCount": cir.get("rpkiClient", {}).get("rejectCount") if cir else None, "rpkiClientRejectCount": cir.get("rpkiClient", {}).get("rejectCount") if cir else None,
}, },
@ -178,8 +181,8 @@ if cir:
lines.extend([ lines.extend([
f"- `cirObjectsMatch`: `{str(combined['cir']['objectsMatch']).lower()}`", f"- `cirObjectsMatch`: `{str(combined['cir']['objectsMatch']).lower()}`",
f"- `cirRejectsMatch`: `{str(combined['cir']['rejectsMatch']).lower()}`", f"- `cirRejectsMatch`: `{str(combined['cir']['rejectsMatch']).lower()}`",
f"- `cirTalsMatch`: `{str(combined['cir']['talsMatch']).lower()}`", f"- `cirTrustAnchorsMatch`: `{str(combined['cir']['trustAnchorsMatch']).lower()}`",
f"- `cirCounts`: ours objects `{combined['cir']['oursObjectCount']}`, rpki-client objects `{combined['cir']['rpkiClientObjectCount']}`, ours rejects `{combined['cir']['oursRejectCount']}`, rpki-client rejects `{combined['cir']['rpkiClientRejectCount']}`", f"- `cirCounts`: ours objects `{combined['cir']['oursObjectCount']}`, rpki-client objects `{combined['cir']['rpkiClientObjectCount']}`, ours trustAnchors `{combined['cir']['oursTrustAnchorCount']}`, rpki-client trustAnchors `{combined['cir']['rpkiClientTrustAnchorCount']}`, ours rejects `{combined['cir']['oursRejectCount']}`, rpki-client rejects `{combined['cir']['rpkiClientRejectCount']}`",
]) ])
else: else:
lines.append(f"- `cirSkippedReason`: `{'ccr matched' if ccr_match and not always_compare_cir else 'CIR inputs unavailable'}`") lines.append(f"- `cirSkippedReason`: `{'ccr matched' if ccr_match and not always_compare_cir else 'CIR inputs unavailable'}`")

View File

@ -57,7 +57,7 @@ fn real_main() -> Result<(), String> {
let cir = rpki::cir::decode_cir(&bytes).map_err(|e| format!("decode cir failed: {e}"))?; let cir = rpki::cir::decode_cir(&bytes).map_err(|e| format!("decode cir failed: {e}"))?;
println!("object_count={}", cir.objects.len()); println!("object_count={}", cir.objects.len());
println!("tal_count={}", cir.tals.len()); println!("trust_anchor_count={}", cir.trust_anchors.len());
for (index, item) in cir.objects.iter().take(args.limit).enumerate() { for (index, item) in cir.objects.iter().take(args.limit).enumerate() {
println!( println!(
"{:04} object={} sha256={}", "{:04} object={} sha256={}",

View File

@ -49,17 +49,23 @@ fn run(argv: Vec<String>) -> Result<(), String> {
.map_err(|e| format!("read CIR failed: {}: {e}", cir_path.display()))?; .map_err(|e| format!("read CIR failed: {}: {e}", cir_path.display()))?;
let cir = rpki::cir::decode_cir(&bytes).map_err(|e| e.to_string())?; let cir = rpki::cir::decode_cir(&bytes).map_err(|e| e.to_string())?;
std::fs::create_dir_all(&tals_dir) std::fs::create_dir_all(&tals_dir).map_err(|e| {
.map_err(|e| format!("create tals dir failed: {}: {e}", tals_dir.display()))?; format!(
"create trust_anchors dir failed: {}: {e}",
tals_dir.display()
)
})?;
let mut tal_files = Vec::new(); let mut tal_files = Vec::new();
for (idx, tal) in cir.tals.iter().enumerate() { for (idx, tal) in cir.trust_anchors.iter().enumerate() {
let filename = format!("tal-{:03}.tal", idx + 1); let filename = format!("tal-{:03}.tal", idx + 1);
let path = tals_dir.join(filename); let path = tals_dir.join(filename);
std::fs::write(&path, &tal.tal_bytes) std::fs::write(&path, &tal.tal_bytes)
.map_err(|e| format!("write TAL failed: {}: {e}", path.display()))?; .map_err(|e| format!("write TAL failed: {}: {e}", path.display()))?;
tal_files.push(serde_json::json!({ tal_files.push(serde_json::json!({
"talUri": tal.tal_uri, "talUri": tal.tal_uri,
"taRsyncUri": tal.ta_rsync_uri,
"taCertificateSha256": hex::encode(&tal.ta_certificate_sha256),
"path": path, "path": path,
})); }));
} }
@ -70,6 +76,7 @@ fn run(argv: Vec<String>) -> Result<(), String> {
.map_err(|e| format!("format validationTime failed: {e}"))?; .map_err(|e| format!("format validationTime failed: {e}"))?;
let meta = serde_json::json!({ let meta = serde_json::json!({
"validationTime": validation_time, "validationTime": validation_time,
"trustAnchorCount": cir.trust_anchors.len(),
"talFiles": tal_files, "talFiles": tal_files,
}); });
if let Some(parent) = meta_json.parent() { if let Some(parent) = meta_json.parent() {

View File

@ -59,9 +59,11 @@ fn run(argv: Vec<String>) -> Result<(), String> {
match result { match result {
Ok(summary) => { Ok(summary) => {
eprintln!( eprintln!(
"materialized CIR: mirror={} objects={} linked={} copied={} keep_db={}", "materialized CIR: mirror={} objects={} trust_anchors={} materialized_files={} linked={} copied={} keep_db={}",
mirror_root.display(), mirror_root.display(),
summary.object_count, summary.object_count,
summary.trust_anchor_count,
summary.materialized_file_count,
summary.linked_files, summary.linked_files,
summary.copied_files, summary.copied_files,
keep_db keep_db

View File

@ -443,8 +443,8 @@ fn uri_extension(uri: &str) -> String {
mod tests { mod tests {
use super::*; use super::*;
use rpki::cir::{ use rpki::cir::{
CIR_VERSION_V2, CanonicalInputRepresentation, CirHashAlgorithm, CirObject, CIR_VERSION_V3, CanonicalInputRepresentation, CirHashAlgorithm, CirObject,
CirRejectedObject, CirTal, compute_reject_list_sha256, encode_cir, CirRejectedObject, CirTrustAnchor, compute_reject_list_sha256, encode_cir,
}; };
#[test] #[test]
@ -664,20 +664,28 @@ mod tests {
.collect::<Vec<_>>(); .collect::<Vec<_>>();
objects.sort_by(|left, right| left.rsync_uri.cmp(&right.rsync_uri)); objects.sort_by(|left, right| left.rsync_uri.cmp(&right.rsync_uri));
CanonicalInputRepresentation { CanonicalInputRepresentation {
version: CIR_VERSION_V2, version: CIR_VERSION_V3,
hash_alg: CirHashAlgorithm::Sha256, hash_alg: CirHashAlgorithm::Sha256,
validation_time: time::OffsetDateTime::UNIX_EPOCH, validation_time: time::OffsetDateTime::UNIX_EPOCH,
objects, objects,
tals: vec![CirTal { trust_anchors: vec![sample_trust_anchor()],
tal_uri: "https://tal.example.test/apnic.tal".to_string(),
tal_bytes: b"https://tal.example.test/apnic.tal\nrsync://example.test/ta.cer\nMIIB"
.to_vec(),
}],
reject_list_sha256: compute_reject_list_sha256(std::iter::empty::<&str>()), reject_list_sha256: compute_reject_list_sha256(std::iter::empty::<&str>()),
rejected_objects: Vec::<CirRejectedObject>::new(), rejected_objects: Vec::<CirRejectedObject>::new(),
} }
} }
fn sample_trust_anchor() -> CirTrustAnchor {
let ta_rsync_uri = "rsync://example.test/ta.cer";
let ta_certificate_der = b"ta-der".to_vec();
CirTrustAnchor {
ta_rsync_uri: ta_rsync_uri.to_string(),
tal_uri: "https://tal.example.test/apnic.tal".to_string(),
tal_bytes: format!("{ta_rsync_uri}\n\nAQID\n").into_bytes(),
ta_certificate_sha256: Sha256::digest(&ta_certificate_der).to_vec(),
ta_certificate_der,
}
}
fn write_cir(path: &Path, cir: &CanonicalInputRepresentation) { fn write_cir(path: &Path, cir: &CanonicalInputRepresentation) {
std::fs::write(path, encode_cir(cir).expect("encode cir")).expect("write cir"); std::fs::write(path, encode_cir(cir).expect("encode cir")).expect("write cir");
} }

View File

@ -114,40 +114,56 @@ fn run(args: Args) -> Result<(), String> {
.iter() .iter()
.map(|item| item.object_uri.clone()) .map(|item| item.object_uri.clone())
.collect::<BTreeSet<_>>(); .collect::<BTreeSet<_>>();
let ours_tals = ours let ours_trust_anchors = ours
.tals .trust_anchors
.iter() .iter()
.map(|item| item.tal_uri.clone()) .map(|item| {
.collect::<BTreeSet<_>>(); (
let peer_tals = peer item.ta_rsync_uri.clone(),
.tals hex::encode(&item.ta_certificate_sha256),
)
})
.collect::<BTreeMap<_, _>>();
let peer_trust_anchors = peer
.trust_anchors
.iter() .iter()
.map(|item| item.tal_uri.clone()) .map(|item| {
.collect::<BTreeSet<_>>(); (
item.ta_rsync_uri.clone(),
hex::encode(&item.ta_certificate_sha256),
)
})
.collect::<BTreeMap<_, _>>();
let object_summary = compare_object_maps(&ours_objects, &peer_objects, args.sample_limit); let object_summary = compare_object_maps(&ours_objects, &peer_objects, args.sample_limit);
let reject_summary = compare_sets(&ours_rejects, &peer_rejects, args.sample_limit); let reject_summary = compare_sets(&ours_rejects, &peer_rejects, args.sample_limit);
let tal_summary = compare_sets(&ours_tals, &peer_tals, args.sample_limit); let trust_anchor_summary =
compare_object_maps(&ours_trust_anchors, &peer_trust_anchors, args.sample_limit);
let reject_hash_match = ours.reject_list_sha256 == peer.reject_list_sha256; let reject_hash_match = ours.reject_list_sha256 == peer.reject_list_sha256;
let all_match = let all_match = object_summary.match_
object_summary.match_ && reject_summary.match_ && tal_summary.match_ && reject_hash_match; && reject_summary.match_
&& trust_anchor_summary.match_
&& reject_hash_match;
let summary = json!({ let summary = json!({
"allMatch": all_match, "allMatch": all_match,
"objects": object_summary.to_json(), "objects": object_summary.to_json(),
"rejects": reject_summary.to_json(), "rejects": reject_summary.to_json(),
"tals": tal_summary.to_json(), "trustAnchors": trust_anchor_summary.to_json(),
"trust_anchors": trust_anchor_summary.to_json(),
"rejectListSha256Match": reject_hash_match, "rejectListSha256Match": reject_hash_match,
"ours": { "ours": {
"objectCount": ours.objects.len(), "objectCount": ours.objects.len(),
"talCount": ours.tals.len(), "talCount": ours.trust_anchors.len(),
"trustAnchorCount": ours.trust_anchors.len(),
"rejectCount": ours.rejected_objects.len(), "rejectCount": ours.rejected_objects.len(),
"rejectListSha256": hex::encode(&ours.reject_list_sha256), "rejectListSha256": hex::encode(&ours.reject_list_sha256),
"validationTime": ours.validation_time.to_string(), "validationTime": ours.validation_time.to_string(),
}, },
"rpkiClient": { "rpkiClient": {
"objectCount": peer.objects.len(), "objectCount": peer.objects.len(),
"talCount": peer.tals.len(), "talCount": peer.trust_anchors.len(),
"trustAnchorCount": peer.trust_anchors.len(),
"rejectCount": peer.rejected_objects.len(), "rejectCount": peer.rejected_objects.len(),
"rejectListSha256": hex::encode(&peer.reject_list_sha256), "rejectListSha256": hex::encode(&peer.reject_list_sha256),
"validationTime": peer.validation_time.to_string(), "validationTime": peer.validation_time.to_string(),
@ -200,23 +216,25 @@ fn write_markdown(path: &Path, summary: &serde_json::Value) -> Result<(), String
summary["rejects"]["match"].as_bool().unwrap_or(false) summary["rejects"]["match"].as_bool().unwrap_or(false)
), ),
format!( format!(
"- `talsMatch`: `{}`", "- `trustAnchorsMatch`: `{}`",
summary["tals"]["match"].as_bool().unwrap_or(false) summary["trustAnchors"]["match"].as_bool().unwrap_or(false)
), ),
format!( format!(
"- `rejectListSha256Match`: `{}`", "- `rejectListSha256Match`: `{}`",
summary["rejectListSha256Match"].as_bool().unwrap_or(false) summary["rejectListSha256Match"].as_bool().unwrap_or(false)
), ),
format!( format!(
"- `ours`: objects `{}`, tals `{}`, rejects `{}`", "- `ours`: objects `{}`, trustAnchors `{}`, rejects `{}`",
summary["ours"]["objectCount"].as_u64().unwrap_or(0), summary["ours"]["objectCount"].as_u64().unwrap_or(0),
summary["ours"]["talCount"].as_u64().unwrap_or(0), summary["ours"]["trustAnchorCount"].as_u64().unwrap_or(0),
summary["ours"]["rejectCount"].as_u64().unwrap_or(0) summary["ours"]["rejectCount"].as_u64().unwrap_or(0)
), ),
format!( format!(
"- `rpkiClient`: objects `{}`, tals `{}`, rejects `{}`", "- `rpkiClient`: objects `{}`, trustAnchors `{}`, rejects `{}`",
summary["rpkiClient"]["objectCount"].as_u64().unwrap_or(0), summary["rpkiClient"]["objectCount"].as_u64().unwrap_or(0),
summary["rpkiClient"]["talCount"].as_u64().unwrap_or(0), summary["rpkiClient"]["trustAnchorCount"]
.as_u64()
.unwrap_or(0),
summary["rpkiClient"]["rejectCount"].as_u64().unwrap_or(0) summary["rpkiClient"]["rejectCount"].as_u64().unwrap_or(0)
), ),
]; ];
@ -410,8 +428,8 @@ fn uri_extension(uri: &str) -> String {
mod tests { mod tests {
use super::*; use super::*;
use rpki::cir::{ use rpki::cir::{
CIR_VERSION_V2, CanonicalInputRepresentation, CirHashAlgorithm, CirObject, CIR_VERSION_V3, CanonicalInputRepresentation, CirHashAlgorithm, CirObject,
CirRejectedObject, CirTal, compute_reject_list_sha256, encode_cir, CirRejectedObject, CirTrustAnchor, compute_reject_list_sha256, encode_cir, sha256,
}; };
#[test] #[test]
@ -513,7 +531,7 @@ mod tests {
assert_eq!(summary["allMatch"], true); assert_eq!(summary["allMatch"], true);
assert_eq!(summary["objects"]["match"], true); assert_eq!(summary["objects"]["match"], true);
assert_eq!(summary["rejects"]["match"], true); assert_eq!(summary["rejects"]["match"], true);
assert_eq!(summary["tals"]["match"], true); assert_eq!(summary["trustAnchors"]["match"], true);
assert_eq!(summary["rejectListSha256Match"], true); assert_eq!(summary["rejectListSha256Match"], true);
assert_eq!(summary["ours"]["objectCount"], 2); assert_eq!(summary["ours"]["objectCount"], 2);
assert!( assert!(
@ -524,7 +542,7 @@ mod tests {
} }
#[test] #[test]
fn run_reports_object_reject_tal_differences_with_samples() { fn run_reports_object_reject_trust_anchor_differences_with_samples() {
let temp = tempfile::tempdir().expect("tempdir"); let temp = tempfile::tempdir().expect("tempdir");
let ours_cir = sample_cir( let ours_cir = sample_cir(
&[ &[
@ -581,7 +599,7 @@ mod tests {
1 1
); );
assert_eq!(summary["rejects"]["match"], false); assert_eq!(summary["rejects"]["match"], false);
assert_eq!(summary["tals"]["match"], false); assert_eq!(summary["trustAnchors"]["match"], false);
assert_eq!(summary["rejectListSha256Match"], false); assert_eq!(summary["rejectListSha256Match"], false);
} }
@ -700,7 +718,7 @@ mod tests {
fn sample_cir( fn sample_cir(
objects: &[(&str, u8)], objects: &[(&str, u8)],
tals: &[&str], trust_anchors: &[&str],
rejected_objects: &[(&str, Option<&str>)], rejected_objects: &[(&str, Option<&str>)],
) -> CanonicalInputRepresentation { ) -> CanonicalInputRepresentation {
let rejected_objects = rejected_objects let rejected_objects = rejected_objects
@ -711,7 +729,7 @@ mod tests {
}) })
.collect::<Vec<_>>(); .collect::<Vec<_>>();
CanonicalInputRepresentation { CanonicalInputRepresentation {
version: CIR_VERSION_V2, version: CIR_VERSION_V3,
hash_alg: CirHashAlgorithm::Sha256, hash_alg: CirHashAlgorithm::Sha256,
validation_time: sample_time(), validation_time: sample_time(),
objects: objects objects: objects
@ -721,12 +739,23 @@ mod tests {
sha256: vec![*fill; 32], sha256: vec![*fill; 32],
}) })
.collect(), .collect(),
tals: tals trust_anchors: trust_anchors
.iter() .iter()
.map(|tal_uri| CirTal { .map(|tal_uri| {
let name = tal_uri
.rsplit('/')
.next()
.unwrap_or("root.tal")
.trim_end_matches(".tal");
let ta_rsync_uri = format!("rsync://example.net/repo/{name}.cer");
let ta_certificate_der = format!("ta-der-{name}").into_bytes();
CirTrustAnchor {
ta_rsync_uri: ta_rsync_uri.clone(),
tal_uri: (*tal_uri).to_string(), tal_uri: (*tal_uri).to_string(),
tal_bytes: format!("{tal_uri}\nrsync://example.net/repo/ta.cer\nMIIB") tal_bytes: format!("{ta_rsync_uri}\n\nAQID\n").into_bytes(),
.into_bytes(), ta_certificate_sha256: sha256(&ta_certificate_der),
ta_certificate_der,
}
}) })
.collect(), .collect(),
reject_list_sha256: compute_reject_list_sha256( reject_list_sha256: compute_reject_list_sha256(

View File

@ -1,11 +1,9 @@
use std::path::PathBuf; use std::path::PathBuf;
use rpki::blob_store::ExternalRepoBytesDb;
use rpki::cir::{ use rpki::cir::{
CIR_VERSION_V2, CanonicalInputRepresentation, CirHashAlgorithm, CirObject, CirTal, CIR_VERSION_V3, CanonicalInputRepresentation, CirHashAlgorithm, CirTrustAnchor,
compute_reject_list_sha256, encode_cir, compute_reject_list_sha256, encode_cir, sha256,
}; };
use sha2::Digest;
const USAGE: &str = "Usage: cir_ta_only_fixture --tal-path <path> --ta-path <path> --tal-uri <url> --validation-time <rfc3339> --cir-out <path> --repo-bytes-db <path>"; const USAGE: &str = "Usage: cir_ta_only_fixture --tal-path <path> --ta-path <path> --tal-uri <url> --validation-time <rfc3339> --cir-out <path> --repo-bytes-db <path>";
@ -88,6 +86,7 @@ fn parse_args(
fn main() -> Result<(), String> { fn main() -> Result<(), String> {
let argv: Vec<String> = std::env::args().collect(); let argv: Vec<String> = std::env::args().collect();
let (tal_path, ta_path, tal_uri, validation_time, cir_out, repo_bytes_db) = parse_args(&argv)?; let (tal_path, ta_path, tal_uri, validation_time, cir_out, repo_bytes_db) = parse_args(&argv)?;
let _ = repo_bytes_db;
let tal_bytes = std::fs::read(&tal_path).map_err(|e| format!("read tal failed: {e}"))?; let tal_bytes = std::fs::read(&tal_path).map_err(|e| format!("read tal failed: {e}"))?;
let ta_bytes = std::fs::read(&ta_path).map_err(|e| format!("read ta failed: {e}"))?; let ta_bytes = std::fs::read(&ta_path).map_err(|e| format!("read ta failed: {e}"))?;
@ -101,22 +100,20 @@ fn main() -> Result<(), String> {
.as_str() .as_str()
.to_string(); .to_string();
let sha = sha2::Sha256::digest(&ta_bytes); let ta_certificate_sha256 = sha256(&ta_bytes);
let hash_hex = hex::encode(sha);
ExternalRepoBytesDb::open(&repo_bytes_db)
.map_err(|e| format!("open repo bytes db failed: {e}"))?
.put_blob_bytes_batch(&[(hash_hex, ta_bytes.clone())])
.map_err(|e| format!("write repo bytes db failed: {e}"))?;
let cir = CanonicalInputRepresentation { let cir = CanonicalInputRepresentation {
version: CIR_VERSION_V2, version: CIR_VERSION_V3,
hash_alg: CirHashAlgorithm::Sha256, hash_alg: CirHashAlgorithm::Sha256,
validation_time, validation_time,
objects: vec![CirObject { objects: Vec::new(),
rsync_uri: ta_rsync_uri, trust_anchors: vec![CirTrustAnchor {
sha256: sha.to_vec(), ta_rsync_uri,
tal_uri,
tal_bytes,
ta_certificate_der: ta_bytes,
ta_certificate_sha256,
}], }],
tals: vec![CirTal { tal_uri, tal_bytes }],
reject_list_sha256: compute_reject_list_sha256(std::iter::empty::<&str>()), reject_list_sha256: compute_reject_list_sha256(std::iter::empty::<&str>()),
rejected_objects: Vec::new(), rejected_objects: Vec::new(),
}; };

View File

@ -1,6 +1,6 @@
use crate::cir::model::{ use crate::cir::model::{
CIR_VERSION_V2, CanonicalInputRepresentation, CirHashAlgorithm, CirObject, CirRejectedObject, CIR_VERSION_V3, CanonicalInputRepresentation, CirHashAlgorithm, CirObject, CirRejectedObject,
CirTal, CirTrustAnchor,
}; };
use crate::data_model::common::DerReader; use crate::data_model::common::DerReader;
use crate::data_model::oid::{OID_SHA256, OID_SHA256_RAW}; use crate::data_model::oid::{OID_SHA256, OID_SHA256_RAW};
@ -32,9 +32,9 @@ pub fn decode_cir(der: &[u8]) -> Result<CanonicalInputRepresentation, CirDecodeE
} }
let version = seq.take_uint_u64().map_err(CirDecodeError::Parse)? as u32; let version = seq.take_uint_u64().map_err(CirDecodeError::Parse)? as u32;
if version != CIR_VERSION_V2 { if version != CIR_VERSION_V3 {
return Err(CirDecodeError::UnexpectedVersion { return Err(CirDecodeError::UnexpectedVersion {
expected: CIR_VERSION_V2, expected: CIR_VERSION_V3,
actual: version, actual: version,
}); });
} }
@ -52,12 +52,14 @@ pub fn decode_cir(der: &[u8]) -> Result<CanonicalInputRepresentation, CirDecodeE
objects.push(decode_object(full)?); objects.push(decode_object(full)?);
} }
let tals_der = seq.take_tag(0x30).map_err(CirDecodeError::Parse)?; let trust_anchors_der = seq.take_tag(0x30).map_err(CirDecodeError::Parse)?;
let mut tals_reader = DerReader::new(tals_der); let mut trust_anchors_reader = DerReader::new(trust_anchors_der);
let mut tals = Vec::new(); let mut trust_anchors = Vec::new();
while !tals_reader.is_empty() { while !trust_anchors_reader.is_empty() {
let (_tag, full, _value) = tals_reader.take_any_full().map_err(CirDecodeError::Parse)?; let (_tag, full, _value) = trust_anchors_reader
tals.push(decode_tal(full)?); .take_any_full()
.map_err(CirDecodeError::Parse)?;
trust_anchors.push(decode_trust_anchor(full)?);
} }
let reject_list_sha256 = seq let reject_list_sha256 = seq
@ -84,7 +86,7 @@ pub fn decode_cir(der: &[u8]) -> Result<CanonicalInputRepresentation, CirDecodeE
hash_alg, hash_alg,
validation_time, validation_time,
objects, objects,
tals, trust_anchors,
reject_list_sha256, reject_list_sha256,
rejected_objects, rejected_objects,
}; };
@ -123,12 +125,17 @@ fn decode_object(der: &[u8]) -> Result<CirObject, CirDecodeError> {
Ok(CirObject { rsync_uri, sha256 }) Ok(CirObject { rsync_uri, sha256 })
} }
fn decode_tal(der: &[u8]) -> Result<CirTal, CirDecodeError> { fn decode_trust_anchor(der: &[u8]) -> Result<CirTrustAnchor, CirDecodeError> {
let mut top = DerReader::new(der); let mut top = DerReader::new(der);
let mut seq = top.take_sequence().map_err(CirDecodeError::Parse)?; let mut seq = top.take_sequence().map_err(CirDecodeError::Parse)?;
if !top.is_empty() { if !top.is_empty() {
return Err(CirDecodeError::Parse("trailing bytes after CirTal".into())); return Err(CirDecodeError::Parse(
"trailing bytes after CirTrustAnchor".into(),
));
} }
let ta_rsync_uri = std::str::from_utf8(seq.take_tag(0x16).map_err(CirDecodeError::Parse)?)
.map_err(|e| CirDecodeError::Parse(e.to_string()))?
.to_string();
let tal_uri = std::str::from_utf8(seq.take_tag(0x16).map_err(CirDecodeError::Parse)?) let tal_uri = std::str::from_utf8(seq.take_tag(0x16).map_err(CirDecodeError::Parse)?)
.map_err(|e| CirDecodeError::Parse(e.to_string()))? .map_err(|e| CirDecodeError::Parse(e.to_string()))?
.to_string(); .to_string();
@ -136,10 +143,26 @@ fn decode_tal(der: &[u8]) -> Result<CirTal, CirDecodeError> {
.take_octet_string() .take_octet_string()
.map_err(CirDecodeError::Parse)? .map_err(CirDecodeError::Parse)?
.to_vec(); .to_vec();
let ta_certificate_der = seq
.take_octet_string()
.map_err(CirDecodeError::Parse)?
.to_vec();
let ta_certificate_sha256 = seq
.take_octet_string()
.map_err(CirDecodeError::Parse)?
.to_vec();
if !seq.is_empty() { if !seq.is_empty() {
return Err(CirDecodeError::Parse("trailing fields in CirTal".into())); return Err(CirDecodeError::Parse(
"trailing fields in CirTrustAnchor".into(),
));
} }
Ok(CirTal { tal_uri, tal_bytes }) Ok(CirTrustAnchor {
ta_rsync_uri,
tal_uri,
tal_bytes,
ta_certificate_der,
ta_certificate_sha256,
})
} }
fn decode_rejected_object(der: &[u8]) -> Result<CirRejectedObject, CirDecodeError> { fn decode_rejected_object(der: &[u8]) -> Result<CirRejectedObject, CirDecodeError> {

View File

@ -1,6 +1,6 @@
use crate::cir::model::{ use crate::cir::model::{
CIR_VERSION_V2, CanonicalInputRepresentation, CirHashAlgorithm, CirObject, CirRejectedObject, CIR_VERSION_V3, CanonicalInputRepresentation, CirHashAlgorithm, CirObject, CirRejectedObject,
CirTal, CirTrustAnchor,
}; };
use crate::data_model::oid::OID_SHA256_RAW; use crate::data_model::oid::OID_SHA256_RAW;
@ -25,9 +25,9 @@ pub fn encode_cir(cir: &CanonicalInputRepresentation) -> Result<Vec<u8>, CirEnco
.collect::<Result<Vec<_>, _>>()?, .collect::<Result<Vec<_>, _>>()?,
), ),
encode_sequence( encode_sequence(
&cir.tals &cir.trust_anchors
.iter() .iter()
.map(encode_tal) .map(encode_trust_anchor)
.collect::<Result<Vec<_>, _>>()?, .collect::<Result<Vec<_>, _>>()?,
), ),
encode_octet_string(&cir.reject_list_sha256), encode_octet_string(&cir.reject_list_sha256),
@ -48,11 +48,14 @@ fn encode_object(object: &CirObject) -> Result<Vec<u8>, CirEncodeError> {
])) ]))
} }
fn encode_tal(tal: &CirTal) -> Result<Vec<u8>, CirEncodeError> { fn encode_trust_anchor(trust_anchor: &CirTrustAnchor) -> Result<Vec<u8>, CirEncodeError> {
tal.validate().map_err(CirEncodeError::Validate)?; trust_anchor.validate().map_err(CirEncodeError::Validate)?;
Ok(encode_sequence(&[ Ok(encode_sequence(&[
encode_ia5_string(tal.tal_uri.as_bytes()), encode_ia5_string(trust_anchor.ta_rsync_uri.as_bytes()),
encode_octet_string(&tal.tal_bytes), encode_ia5_string(trust_anchor.tal_uri.as_bytes()),
encode_octet_string(&trust_anchor.tal_bytes),
encode_octet_string(&trust_anchor.ta_certificate_der),
encode_octet_string(&trust_anchor.ta_certificate_sha256),
])) ]))
} }
@ -160,5 +163,5 @@ fn encode_len_into(len: usize, out: &mut Vec<u8>) {
#[allow(dead_code)] #[allow(dead_code)]
const _: () = { const _: () = {
let _ = CIR_VERSION_V2; let _ = CIR_VERSION_V3;
}; };

View File

@ -5,12 +5,11 @@ use std::path::Path;
use crate::audit::{AuditObjectResult, PublicationPointAudit}; use crate::audit::{AuditObjectResult, PublicationPointAudit};
use crate::cir::encode::{CirEncodeError, encode_cir}; use crate::cir::encode::{CirEncodeError, encode_cir};
use crate::cir::model::{ use crate::cir::model::{
CIR_VERSION_V2, CanonicalInputRepresentation, CirHashAlgorithm, CirObject, CirRejectedObject, CIR_VERSION_V3, CanonicalInputRepresentation, CirHashAlgorithm, CirObject, CirRejectedObject,
CirTal, compute_reject_list_sha256, CirTrustAnchor, compute_reject_list_sha256,
}; };
use crate::cir::static_pool::{ use crate::cir::static_pool::{
CirStaticPoolError, CirStaticPoolExportSummary, export_hashes_from_store, CirStaticPoolError, CirStaticPoolExportSummary, export_hashes_from_store,
write_bytes_to_static_pool,
}; };
use crate::current_repo_index::CurrentRepoObject; use crate::current_repo_index::CurrentRepoObject;
use crate::data_model::ta::TrustAnchor; use crate::data_model::ta::TrustAnchor;
@ -49,9 +48,6 @@ pub enum CirExportError {
#[error("write CIR file failed: {0}: {1}")] #[error("write CIR file failed: {0}: {1}")]
Write(String, String), Write(String, String),
#[error("write CIR trust anchor bytes to repo store failed: {0}")]
WriteRepoBytes(String),
} }
#[derive(Clone, Debug, PartialEq, Eq)] #[derive(Clone, Debug, PartialEq, Eq)]
@ -64,12 +60,12 @@ pub struct CirRawStoreExportSummary {
#[derive(Clone, Debug, PartialEq, Eq)] #[derive(Clone, Debug, PartialEq, Eq)]
pub struct CirExportSummary { pub struct CirExportSummary {
pub object_count: usize, pub object_count: usize,
pub tal_count: usize, pub trust_anchor_count: usize,
pub timing: CirExportTiming, pub timing: CirExportTiming,
} }
#[derive(Clone, Copy, Debug)] #[derive(Clone, Copy, Debug)]
pub struct CirTalBinding<'a> { pub struct CirTrustAnchorBinding<'a> {
pub trust_anchor: &'a TrustAnchor, pub trust_anchor: &'a TrustAnchor,
pub tal_uri: &'a str, pub tal_uri: &'a str,
} }
@ -118,6 +114,22 @@ fn collect_cir_objects_from_validation_audit(
Ok(objects) Ok(objects)
} }
fn canonical_ta_rsync_uri(trust_anchor: &TrustAnchor) -> Result<String, CirExportError> {
if let Some(uri) = &trust_anchor.resolved_ta_uri
&& uri.scheme() == "rsync"
{
return Ok(uri.as_str().to_string());
}
trust_anchor
.tal
.ta_uris
.iter()
.filter(|uri| uri.scheme() == "rsync")
.map(|uri| uri.as_str().to_string())
.min()
.ok_or(CirExportError::MissingTaRsyncUri)
}
pub fn build_cir_from_run( pub fn build_cir_from_run(
store: &RocksStore, store: &RocksStore,
trust_anchor: &TrustAnchor, trust_anchor: &TrustAnchor,
@ -127,7 +139,7 @@ pub fn build_cir_from_run(
) -> Result<CanonicalInputRepresentation, CirExportError> { ) -> Result<CanonicalInputRepresentation, CirExportError> {
build_cir_from_run_multi( build_cir_from_run_multi(
store, store,
&[CirTalBinding { &[CirTrustAnchorBinding {
trust_anchor, trust_anchor,
tal_uri, tal_uri,
}], }],
@ -139,7 +151,7 @@ pub fn build_cir_from_run(
pub fn build_cir_from_run_multi( pub fn build_cir_from_run_multi(
_store: &RocksStore, _store: &RocksStore,
tal_bindings: &[CirTalBinding<'_>], tal_bindings: &[CirTrustAnchorBinding<'_>],
validation_time: time::OffsetDateTime, validation_time: time::OffsetDateTime,
publication_points: &[PublicationPointAudit], publication_points: &[PublicationPointAudit],
_current_repo_objects: Option<&[CurrentRepoObject]>, _current_repo_objects: Option<&[CurrentRepoObject]>,
@ -150,30 +162,24 @@ pub fn build_cir_from_run_multi(
} }
} }
let mut objects = collect_cir_objects_from_validation_audit(publication_points)?; let objects = collect_cir_objects_from_validation_audit(publication_points)?;
let mut tals = Vec::with_capacity(tal_bindings.len()); let mut trust_anchors = Vec::with_capacity(tal_bindings.len());
for binding in tal_bindings { for binding in tal_bindings {
let ta_hash = ta_sha256_hex(&binding.trust_anchor.ta_certificate.raw_der); let ta_rsync_uri = canonical_ta_rsync_uri(binding.trust_anchor)?;
let mut saw_rsync_uri = false; let ta_certificate_der = binding.trust_anchor.ta_certificate.raw_der.clone();
for uri in &binding.trust_anchor.tal.ta_uris { trust_anchors.push(CirTrustAnchor {
if uri.scheme() == "rsync" { ta_rsync_uri,
saw_rsync_uri = true;
objects.insert(uri.as_str().to_string(), ta_hash.clone());
}
}
if !saw_rsync_uri {
return Err(CirExportError::MissingTaRsyncUri);
}
tals.push(CirTal {
tal_uri: binding.tal_uri.to_string(), tal_uri: binding.tal_uri.to_string(),
tal_bytes: binding.trust_anchor.tal.raw.clone(), tal_bytes: binding.trust_anchor.tal.raw.clone(),
ta_certificate_sha256: crate::cir::model::sha256(&ta_certificate_der),
ta_certificate_der,
}); });
} }
tals.sort_by(|a, b| a.tal_uri.cmp(&b.tal_uri)); trust_anchors.sort_by(|a, b| a.ta_rsync_uri.cmp(&b.ta_rsync_uri));
let cir = CanonicalInputRepresentation { let cir = CanonicalInputRepresentation {
version: CIR_VERSION_V2, version: CIR_VERSION_V3,
hash_alg: CirHashAlgorithm::Sha256, hash_alg: CirHashAlgorithm::Sha256,
validation_time: validation_time.to_offset(time::UtcOffset::UTC), validation_time: validation_time.to_offset(time::UtcOffset::UTC),
objects: objects objects: objects
@ -183,7 +189,7 @@ pub fn build_cir_from_run_multi(
sha256: hex::decode(sha256_hex).expect("validated hex"), sha256: hex::decode(sha256_hex).expect("validated hex"),
}) })
.collect(), .collect(),
tals, trust_anchors,
reject_list_sha256: Vec::new(), reject_list_sha256: Vec::new(),
rejected_objects: Vec::new(), rejected_objects: Vec::new(),
}; };
@ -238,36 +244,13 @@ pub fn export_cir_static_pool(
cir: &CanonicalInputRepresentation, cir: &CanonicalInputRepresentation,
trust_anchors: &[&TrustAnchor], trust_anchors: &[&TrustAnchor],
) -> Result<CirStaticPoolExportSummary, CirExportError> { ) -> Result<CirStaticPoolExportSummary, CirExportError> {
let ta_hashes = trust_anchors let _ = trust_anchors;
.iter()
.map(|ta| ta_sha256_hex(&ta.ta_certificate.raw_der))
.collect::<BTreeSet<_>>();
let hashes = cir let hashes = cir
.objects .objects
.iter() .iter()
.map(|item| hex::encode(&item.sha256)) .map(|item| hex::encode(&item.sha256))
.filter(|hash| !ta_hashes.contains(hash))
.collect::<Vec<_>>(); .collect::<Vec<_>>();
let mut summary = export_hashes_from_store(store, static_root, capture_date_utc, &hashes)?; export_hashes_from_store(store, static_root, capture_date_utc, &hashes).map_err(Into::into)
let mut unique = hashes.iter().cloned().collect::<BTreeSet<_>>();
for trust_anchor in trust_anchors {
let ta_hash = ta_sha256_hex(&trust_anchor.ta_certificate.raw_der);
let ta_result = write_bytes_to_static_pool(
static_root,
capture_date_utc,
&ta_hash,
&trust_anchor.ta_certificate.raw_der,
)?;
unique.insert(ta_hash);
if ta_result.written {
summary.written_files += 1;
} else {
summary.reused_files += 1;
}
}
summary.unique_hashes = unique.len();
Ok(summary)
} }
pub fn export_cir_raw_store( pub fn export_cir_raw_store(
@ -276,17 +259,14 @@ pub fn export_cir_raw_store(
cir: &CanonicalInputRepresentation, cir: &CanonicalInputRepresentation,
trust_anchors: &[&TrustAnchor], trust_anchors: &[&TrustAnchor],
) -> Result<CirRawStoreExportSummary, CirExportError> { ) -> Result<CirRawStoreExportSummary, CirExportError> {
let ta_by_hash = trust_anchors let _ = trust_anchors;
.iter()
.map(|ta| (ta_sha256_hex(&ta.ta_certificate.raw_der), *ta))
.collect::<BTreeMap<_, _>>();
let unique: BTreeSet<String> = cir let unique: BTreeSet<String> = cir
.objects .objects
.iter() .iter()
.map(|item| hex::encode(&item.sha256)) .map(|item| hex::encode(&item.sha256))
.collect(); .collect();
let mut written_entries = 0usize; let written_entries = 0usize;
let mut reused_entries = 0usize; let mut reused_entries = 0usize;
for sha256_hex in &unique { for sha256_hex in &unique {
if store if store
@ -299,23 +279,6 @@ pub fn export_cir_raw_store(
reused_entries += 1; reused_entries += 1;
continue; continue;
} }
if let Some(trust_anchor) = ta_by_hash.get(sha256_hex) {
let mut entry = crate::storage::RawByHashEntry::from_bytes(
sha256_hex.clone(),
trust_anchor.ta_certificate.raw_der.clone(),
);
entry.object_type = Some("cer".to_string());
for object in &cir.objects {
if hex::encode(&object.sha256) == *sha256_hex {
entry.origin_uris.push(object.rsync_uri.clone());
}
}
store.put_raw_by_hash_entry(&entry).map_err(|e| {
CirExportError::Write(raw_store_path.display().to_string(), e.to_string())
})?;
written_entries += 1;
continue;
}
return Err(CirExportError::Write( return Err(CirExportError::Write(
raw_store_path.display().to_string(), raw_store_path.display().to_string(),
format!("raw store missing object for sha256={sha256_hex}"), format!("raw store missing object for sha256={sha256_hex}"),
@ -340,7 +303,7 @@ pub fn export_cir_from_run(
) -> Result<CirExportSummary, CirExportError> { ) -> Result<CirExportSummary, CirExportError> {
export_cir_from_run_multi( export_cir_from_run_multi(
store, store,
&[CirTalBinding { &[CirTrustAnchorBinding {
trust_anchor, trust_anchor,
tal_uri, tal_uri,
}], }],
@ -354,7 +317,7 @@ pub fn export_cir_from_run(
pub fn export_cir_from_run_multi( pub fn export_cir_from_run_multi(
store: &RocksStore, store: &RocksStore,
tal_bindings: &[CirTalBinding<'_>], tal_bindings: &[CirTrustAnchorBinding<'_>],
validation_time: time::OffsetDateTime, validation_time: time::OffsetDateTime,
publication_points: &[PublicationPointAudit], publication_points: &[PublicationPointAudit],
cir_out: &Path, cir_out: &Path,
@ -374,18 +337,7 @@ pub fn export_cir_from_run_multi(
)?; )?;
let build_cir_ms = started.elapsed().as_millis() as u64; let build_cir_ms = started.elapsed().as_millis() as u64;
let ta_blobs = tal_bindings let _ = store;
.iter()
.map(|binding| {
(
ta_sha256_hex(&binding.trust_anchor.ta_certificate.raw_der),
binding.trust_anchor.ta_certificate.raw_der.clone(),
)
})
.collect::<Vec<_>>();
store
.put_blob_bytes_batch(&ta_blobs)
.map_err(|e| CirExportError::WriteRepoBytes(e.to_string()))?;
let started = std::time::Instant::now(); let started = std::time::Instant::now();
write_cir_file(cir_out, &cir)?; write_cir_file(cir_out, &cir)?;
@ -393,7 +345,7 @@ pub fn export_cir_from_run_multi(
Ok(CirExportSummary { Ok(CirExportSummary {
object_count: cir.objects.len(), object_count: cir.objects.len(),
tal_count: cir.tals.len(), trust_anchor_count: cir.trust_anchors.len(),
timing: CirExportTiming { timing: CirExportTiming {
build_cir_ms, build_cir_ms,
write_cir_ms, write_cir_ms,
@ -402,11 +354,6 @@ pub fn export_cir_from_run_multi(
}) })
} }
fn ta_sha256_hex(bytes: &[u8]) -> String {
use sha2::{Digest, Sha256};
hex::encode(Sha256::digest(bytes))
}
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;
@ -497,19 +444,23 @@ mod tests {
&publication_points, &publication_points,
) )
.expect("build cir"); .expect("build cir");
assert_eq!(cir.version, CIR_VERSION_V2); assert_eq!(cir.version, CIR_VERSION_V3);
assert_eq!(cir.tals.len(), 1); assert_eq!(cir.trust_anchors.len(), 1);
assert_eq!(cir.tals[0].tal_uri, "https://example.test/root.tal"); assert_eq!(
cir.trust_anchors[0].tal_uri,
"https://example.test/root.tal"
);
assert!( assert!(
cir.objects cir.objects
.iter() .iter()
.any(|item| item.rsync_uri == "rsync://example.test/repo/a.cer") .any(|item| item.rsync_uri == "rsync://example.test/repo/a.cer")
); );
assert!( assert!(
cir.objects !cir.objects
.iter() .iter()
.any(|item| item.rsync_uri.contains("apnic-rpki-root-iana-origin.cer")) .any(|item| item.rsync_uri == cir.trust_anchors[0].ta_rsync_uri)
); );
assert!(!cir.trust_anchors[0].ta_certificate_der.is_empty());
} }
#[test] #[test]
@ -549,13 +500,16 @@ mod tests {
sample_date(), sample_date(),
) )
.expect("export cir"); .expect("export cir");
assert_eq!(summary.tal_count, 1); assert_eq!(summary.trust_anchor_count, 1);
assert!(summary.object_count >= 2); assert_eq!(summary.object_count, 1);
assert!(summary.timing.total_ms >= summary.timing.build_cir_ms); assert!(summary.timing.total_ms >= summary.timing.build_cir_ms);
let der = std::fs::read(&cir_path).unwrap(); let der = std::fs::read(&cir_path).unwrap();
let cir = decode_cir(&der).unwrap(); let cir = decode_cir(&der).unwrap();
assert_eq!(cir.tals[0].tal_uri, "https://example.test/root.tal"); assert_eq!(
cir.trust_anchors[0].tal_uri,
"https://example.test/root.tal"
);
} }
#[test] #[test]
@ -595,13 +549,13 @@ mod tests {
sample_date(), sample_date(),
) )
.expect("export cir"); .expect("export cir");
assert!(summary.object_count >= 2); assert_eq!(summary.object_count, 1);
assert!(raw_store.exists()); assert!(raw_store.exists());
assert!(cir_path.exists()); assert!(cir_path.exists());
} }
#[test] #[test]
fn export_cir_from_run_writes_ta_bytes_to_repo_bytes_store() { fn export_cir_from_run_does_not_write_ta_bytes_to_repo_bytes_store() {
let td = tempfile::tempdir().unwrap(); let td = tempfile::tempdir().unwrap();
let store = RocksStore::open_with_external_repo_bytes( let store = RocksStore::open_with_external_repo_bytes(
&td.path().join("db"), &td.path().join("db"),
@ -623,10 +577,7 @@ mod tests {
) )
.expect("export cir"); .expect("export cir");
assert_eq!( assert_eq!(store.get_blob_bytes(&ta_hash).unwrap(), None);
store.get_blob_bytes(&ta_hash).unwrap(),
Some(ta.ta_certificate.raw_der.clone())
);
} }
#[test] #[test]
@ -703,11 +654,11 @@ mod tests {
let cir = build_cir_from_run_multi( let cir = build_cir_from_run_multi(
&store, &store,
&[ &[
CirTalBinding { CirTrustAnchorBinding {
trust_anchor: &ta1, trust_anchor: &ta1,
tal_uri: "https://example.test/apnic.tal", tal_uri: "https://example.test/apnic.tal",
}, },
CirTalBinding { CirTrustAnchorBinding {
trust_anchor: &ta2, trust_anchor: &ta2,
tal_uri: "https://example.test/arin.tal", tal_uri: "https://example.test/arin.tal",
}, },
@ -718,7 +669,7 @@ mod tests {
) )
.expect("build cir from consumed audit objects"); .expect("build cir from consumed audit objects");
assert_eq!(cir.tals.len(), 2); assert_eq!(cir.trust_anchors.len(), 2);
assert!( assert!(
cir.objects cir.objects
.iter() .iter()
@ -730,17 +681,18 @@ mod tests {
.any(|item| item.rsync_uri == "rsync://example.test/repo/superfluous.roa"), .any(|item| item.rsync_uri == "rsync://example.test/repo/superfluous.roa"),
"current repo objects must not be included unless validation consumed them", "current repo objects must not be included unless validation consumed them",
); );
for trust_anchor in &cir.trust_anchors {
assert!( assert!(
cir.objects.iter().any(|item| { !cir.objects
item.rsync_uri.contains("apnic-rpki-root-iana-origin.cer") .iter()
|| item.rsync_uri.contains("arin-rpki-ta.cer") .any(|item| item.rsync_uri == trust_anchor.ta_rsync_uri),
}), "trust anchor rsync objects must not be included in CIR.objects",
"trust anchor rsync objects must be included",
); );
} }
}
#[test] #[test]
fn build_cir_from_run_multi_sorts_tals_by_tal_uri() { fn build_cir_from_run_multi_sorts_trust_anchors_by_ta_rsync_uri() {
let td = tempfile::tempdir().unwrap(); let td = tempfile::tempdir().unwrap();
let store = RocksStore::open(td.path()).unwrap(); let store = RocksStore::open(td.path()).unwrap();
let apnic = sample_trust_anchor(); let apnic = sample_trust_anchor();
@ -749,11 +701,11 @@ mod tests {
let cir = build_cir_from_run_multi( let cir = build_cir_from_run_multi(
&store, &store,
&[ &[
CirTalBinding { CirTrustAnchorBinding {
trust_anchor: &apnic, trust_anchor: &apnic,
tal_uri: "https://rpki.apnic.net/repository/apnic-rpki-root-iana-origin.cer", tal_uri: "https://rpki.apnic.net/repository/apnic-rpki-root-iana-origin.cer",
}, },
CirTalBinding { CirTrustAnchorBinding {
trust_anchor: &arin, trust_anchor: &arin,
tal_uri: "https://rrdp.arin.net/arin-rpki-ta.cer", tal_uri: "https://rrdp.arin.net/arin-rpki-ta.cer",
}, },
@ -765,13 +717,13 @@ mod tests {
.expect("build cir with unsorted input bindings"); .expect("build cir with unsorted input bindings");
assert_eq!( assert_eq!(
cir.tals cir.trust_anchors
.iter() .iter()
.map(|tal| tal.tal_uri.as_str()) .map(|trust_anchor| trust_anchor.ta_rsync_uri.as_str())
.collect::<Vec<_>>(), .collect::<Vec<_>>(),
vec![ vec![
"https://rpki.apnic.net/repository/apnic-rpki-root-iana-origin.cer", "rsync://rpki.apnic.net/repository/apnic-rpki-root-iana-origin.cer",
"https://rrdp.arin.net/arin-rpki-ta.cer", "rsync://rpki.arin.net/repository/arin-rpki-ta.cer",
] ]
); );
} }
@ -810,7 +762,7 @@ mod tests {
let cir = build_cir_from_run_multi( let cir = build_cir_from_run_multi(
&store, &store,
&[CirTalBinding { &[CirTrustAnchorBinding {
trust_anchor: &ta, trust_anchor: &ta,
tal_uri: "https://example.test/root.tal", tal_uri: "https://example.test/root.tal",
}], }],
@ -860,7 +812,7 @@ mod tests {
let cir_a = build_cir_from_run_multi( let cir_a = build_cir_from_run_multi(
&store, &store,
&[CirTalBinding { &[CirTrustAnchorBinding {
trust_anchor: &ta, trust_anchor: &ta,
tal_uri: "https://example.test/root.tal", tal_uri: "https://example.test/root.tal",
}], }],
@ -871,7 +823,7 @@ mod tests {
.expect("build cir a"); .expect("build cir a");
let cir_b = build_cir_from_run_multi( let cir_b = build_cir_from_run_multi(
&store, &store,
&[CirTalBinding { &[CirTrustAnchorBinding {
trust_anchor: &ta, trust_anchor: &ta,
tal_uri: "https://example.test/root.tal", tal_uri: "https://example.test/root.tal",
}], }],
@ -895,7 +847,7 @@ mod tests {
let err = build_cir_from_run_multi( let err = build_cir_from_run_multi(
&store, &store,
&[CirTalBinding { &[CirTrustAnchorBinding {
trust_anchor: &sample_trust_anchor(), trust_anchor: &sample_trust_anchor(),
tal_uri: "file:///not-supported.tal", tal_uri: "file:///not-supported.tal",
}], }],
@ -908,7 +860,7 @@ mod tests {
let err = build_cir_from_run_multi( let err = build_cir_from_run_multi(
&store, &store,
&[CirTalBinding { &[CirTrustAnchorBinding {
trust_anchor: &sample_trust_anchor_without_rsync_uri(), trust_anchor: &sample_trust_anchor_without_rsync_uri(),
tal_uri: "https://example.test/root.tal", tal_uri: "https://example.test/root.tal",
}], }],
@ -921,7 +873,7 @@ mod tests {
} }
#[test] #[test]
fn export_cir_static_pool_writes_objects_and_multiple_tas() { fn export_cir_static_pool_writes_repository_objects_only() {
let td = tempfile::tempdir().unwrap(); let td = tempfile::tempdir().unwrap();
let store = RocksStore::open(&td.path().join("db")).unwrap(); let store = RocksStore::open(&td.path().join("db")).unwrap();
let static_root = td.path().join("static"); let static_root = td.path().join("static");
@ -948,11 +900,11 @@ mod tests {
let cir = build_cir_from_run_multi( let cir = build_cir_from_run_multi(
&store, &store,
&[ &[
CirTalBinding { CirTrustAnchorBinding {
trust_anchor: &ta1, trust_anchor: &ta1,
tal_uri: "https://example.test/apnic.tal", tal_uri: "https://example.test/apnic.tal",
}, },
CirTalBinding { CirTrustAnchorBinding {
trust_anchor: &ta2, trust_anchor: &ta2,
tal_uri: "https://example.test/arin.tal", tal_uri: "https://example.test/arin.tal",
}, },
@ -966,12 +918,20 @@ mod tests {
let summary = let summary =
export_cir_static_pool(&store, &static_root, sample_date(), &cir, &[&ta1, &ta2]) export_cir_static_pool(&store, &static_root, sample_date(), &cir, &[&ta1, &ta2])
.expect("export static pool"); .expect("export static pool");
assert!(summary.unique_hashes >= 3); assert_eq!(summary.unique_hashes, 1);
assert!(summary.written_files >= 3); assert_eq!(summary.written_files, 1);
for trust_anchor in &cir.trust_anchors {
let ta_hash = hex::encode(&trust_anchor.ta_certificate_sha256);
assert!(
!crate::cir::static_pool::static_pool_path(&static_root, sample_date(), &ta_hash)
.expect("static pool ta path")
.exists()
);
}
} }
#[test] #[test]
fn export_cir_raw_store_reports_missing_non_ta_object_and_writes_ta_entries() { fn export_cir_raw_store_reports_missing_non_ta_object_only() {
let td = tempfile::tempdir().unwrap(); let td = tempfile::tempdir().unwrap();
let raw_store_path = td.path().join("raw-store.db"); let raw_store_path = td.path().join("raw-store.db");
let store = let store =
@ -983,11 +943,11 @@ mod tests {
let cir_only_tas = build_cir_from_run_multi( let cir_only_tas = build_cir_from_run_multi(
&store, &store,
&[ &[
CirTalBinding { CirTrustAnchorBinding {
trust_anchor: &ta1, trust_anchor: &ta1,
tal_uri: "https://example.test/apnic.tal", tal_uri: "https://example.test/apnic.tal",
}, },
CirTalBinding { CirTrustAnchorBinding {
trust_anchor: &ta2, trust_anchor: &ta2,
tal_uri: "https://example.test/arin.tal", tal_uri: "https://example.test/arin.tal",
}, },
@ -1000,8 +960,9 @@ mod tests {
let summary = export_cir_raw_store(&store, &raw_store_path, &cir_only_tas, &[&ta1, &ta2]) let summary = export_cir_raw_store(&store, &raw_store_path, &cir_only_tas, &[&ta1, &ta2])
.expect("export raw store"); .expect("export raw store");
assert!(summary.unique_hashes >= 2); assert_eq!(summary.unique_hashes, 0);
assert!(summary.written_entries >= 2 || summary.reused_entries >= 2); assert_eq!(summary.written_entries, 0);
assert_eq!(summary.reused_entries, 0);
let mut cir_missing_object = cir_only_tas.clone(); let mut cir_missing_object = cir_only_tas.clone();
cir_missing_object.objects.push(CirObject { cir_missing_object.objects.push(CirObject {

View File

@ -1,3 +1,4 @@
use std::collections::BTreeSet;
use std::fs; use std::fs;
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
@ -66,6 +67,8 @@ pub enum CirMaterializeError {
#[derive(Clone, Debug, PartialEq, Eq)] #[derive(Clone, Debug, PartialEq, Eq)]
pub struct CirMaterializeSummary { pub struct CirMaterializeSummary {
pub object_count: usize, pub object_count: usize,
pub trust_anchor_count: usize,
pub materialized_file_count: usize,
pub linked_files: usize, pub linked_files: usize,
pub copied_files: usize, pub copied_files: usize,
} }
@ -78,16 +81,7 @@ pub fn materialize_cir(
) -> Result<CirMaterializeSummary, CirMaterializeError> { ) -> Result<CirMaterializeSummary, CirMaterializeError> {
cir.validate().map_err(CirMaterializeError::TreeMismatch)?; cir.validate().map_err(CirMaterializeError::TreeMismatch)?;
if clean_rebuild && mirror_root.exists() { prepare_mirror_root(mirror_root, clean_rebuild)?;
fs::remove_dir_all(mirror_root).map_err(|e| CirMaterializeError::RemoveMirrorRoot {
path: mirror_root.display().to_string(),
detail: e.to_string(),
})?;
}
fs::create_dir_all(mirror_root).map_err(|e| CirMaterializeError::CreateMirrorRoot {
path: mirror_root.display().to_string(),
detail: e.to_string(),
})?;
let mut linked_files = 0usize; let mut linked_files = 0usize;
let mut copied_files = 0usize; let mut copied_files = 0usize;
@ -125,12 +119,18 @@ pub fn materialize_cir(
} }
} }
for trust_anchor in &cir.trust_anchors {
write_bytes_to_mirror_uri(
mirror_root,
&trust_anchor.ta_rsync_uri,
&trust_anchor.ta_certificate_der,
"cir trust anchor",
)?;
copied_files += 1;
}
let actual = collect_materialized_uris(mirror_root)?; let actual = collect_materialized_uris(mirror_root)?;
let expected = cir let expected = expected_materialized_uris(cir);
.objects
.iter()
.map(|item| item.rsync_uri.clone())
.collect::<std::collections::BTreeSet<_>>();
if actual != expected { if actual != expected {
return Err(CirMaterializeError::TreeMismatch(format!( return Err(CirMaterializeError::TreeMismatch(format!(
"expected {} files, got {} files", "expected {} files, got {} files",
@ -141,6 +141,8 @@ pub fn materialize_cir(
Ok(CirMaterializeSummary { Ok(CirMaterializeSummary {
object_count: cir.objects.len(), object_count: cir.objects.len(),
trust_anchor_count: cir.trust_anchors.len(),
materialized_file_count: expected.len(),
linked_files, linked_files,
copied_files, copied_files,
}) })
@ -154,16 +156,7 @@ pub fn materialize_cir_from_raw_store(
) -> Result<CirMaterializeSummary, CirMaterializeError> { ) -> Result<CirMaterializeSummary, CirMaterializeError> {
cir.validate().map_err(CirMaterializeError::TreeMismatch)?; cir.validate().map_err(CirMaterializeError::TreeMismatch)?;
if clean_rebuild && mirror_root.exists() { prepare_mirror_root(mirror_root, clean_rebuild)?;
fs::remove_dir_all(mirror_root).map_err(|e| CirMaterializeError::RemoveMirrorRoot {
path: mirror_root.display().to_string(),
detail: e.to_string(),
})?;
}
fs::create_dir_all(mirror_root).map_err(|e| CirMaterializeError::CreateMirrorRoot {
path: mirror_root.display().to_string(),
detail: e.to_string(),
})?;
let raw_store = let raw_store =
ExternalRawStoreDb::open(raw_store_db).map_err(|e| CirMaterializeError::OpenRawStore { ExternalRawStoreDb::open(raw_store_db).map_err(|e| CirMaterializeError::OpenRawStore {
@ -183,37 +176,27 @@ pub fn materialize_cir_from_raw_store(
.ok_or_else(|| CirMaterializeError::MissingRawStoreObject { .ok_or_else(|| CirMaterializeError::MissingRawStoreObject {
sha256_hex: sha256_hex.clone(), sha256_hex: sha256_hex.clone(),
})?; })?;
let relative = mirror_relative_path_for_rsync_uri(&object.rsync_uri)?; write_bytes_to_mirror_uri(
let target = mirror_root.join(&relative); mirror_root,
&object.rsync_uri,
if let Some(parent) = target.parent() { &bytes,
fs::create_dir_all(parent).map_err(|e| CirMaterializeError::CreateParent { &raw_store_db.display().to_string(),
path: parent.display().to_string(), )?;
detail: e.to_string(), copied_files += 1;
})?;
} }
if target.exists() { for trust_anchor in &cir.trust_anchors {
fs::remove_file(&target).map_err(|e| CirMaterializeError::RemoveExistingTarget { write_bytes_to_mirror_uri(
path: target.display().to_string(), mirror_root,
detail: e.to_string(), &trust_anchor.ta_rsync_uri,
})?; &trust_anchor.ta_certificate_der,
} "cir trust anchor",
)?;
fs::write(&target, &bytes).map_err(|e| CirMaterializeError::Copy {
src: raw_store_db.display().to_string(),
dst: target.display().to_string(),
detail: e.to_string(),
})?;
copied_files += 1; copied_files += 1;
} }
let actual = collect_materialized_uris(mirror_root)?; let actual = collect_materialized_uris(mirror_root)?;
let expected = cir let expected = expected_materialized_uris(cir);
.objects
.iter()
.map(|item| item.rsync_uri.clone())
.collect::<std::collections::BTreeSet<_>>();
if actual != expected { if actual != expected {
return Err(CirMaterializeError::TreeMismatch(format!( return Err(CirMaterializeError::TreeMismatch(format!(
"expected {} files, got {} files", "expected {} files, got {} files",
@ -224,6 +207,8 @@ pub fn materialize_cir_from_raw_store(
Ok(CirMaterializeSummary { Ok(CirMaterializeSummary {
object_count: cir.objects.len(), object_count: cir.objects.len(),
trust_anchor_count: cir.trust_anchors.len(),
materialized_file_count: expected.len(),
linked_files: 0, linked_files: 0,
copied_files, copied_files,
}) })
@ -237,16 +222,7 @@ pub fn materialize_cir_from_repo_bytes(
) -> Result<CirMaterializeSummary, CirMaterializeError> { ) -> Result<CirMaterializeSummary, CirMaterializeError> {
cir.validate().map_err(CirMaterializeError::TreeMismatch)?; cir.validate().map_err(CirMaterializeError::TreeMismatch)?;
if clean_rebuild && mirror_root.exists() { prepare_mirror_root(mirror_root, clean_rebuild)?;
fs::remove_dir_all(mirror_root).map_err(|e| CirMaterializeError::RemoveMirrorRoot {
path: mirror_root.display().to_string(),
detail: e.to_string(),
})?;
}
fs::create_dir_all(mirror_root).map_err(|e| CirMaterializeError::CreateMirrorRoot {
path: mirror_root.display().to_string(),
detail: e.to_string(),
})?;
let repo_bytes = ExternalRepoBytesDb::open(repo_bytes_db).map_err(|e| { let repo_bytes = ExternalRepoBytesDb::open(repo_bytes_db).map_err(|e| {
CirMaterializeError::OpenRepoBytesStore { CirMaterializeError::OpenRepoBytesStore {
@ -267,7 +243,64 @@ pub fn materialize_cir_from_repo_bytes(
.ok_or_else(|| CirMaterializeError::MissingRepoBytesObject { .ok_or_else(|| CirMaterializeError::MissingRepoBytesObject {
sha256_hex: sha256_hex.clone(), sha256_hex: sha256_hex.clone(),
})?; })?;
let relative = mirror_relative_path_for_rsync_uri(&object.rsync_uri)?; write_bytes_to_mirror_uri(
mirror_root,
&object.rsync_uri,
&bytes,
&repo_bytes_db.display().to_string(),
)?;
copied_files += 1;
}
for trust_anchor in &cir.trust_anchors {
write_bytes_to_mirror_uri(
mirror_root,
&trust_anchor.ta_rsync_uri,
&trust_anchor.ta_certificate_der,
"cir trust anchor",
)?;
copied_files += 1;
}
let actual = collect_materialized_uris(mirror_root)?;
let expected = expected_materialized_uris(cir);
if actual != expected {
return Err(CirMaterializeError::TreeMismatch(format!(
"expected {} files, got {} files",
expected.len(),
actual.len()
)));
}
Ok(CirMaterializeSummary {
object_count: cir.objects.len(),
trust_anchor_count: cir.trust_anchors.len(),
materialized_file_count: expected.len(),
linked_files: 0,
copied_files,
})
}
fn prepare_mirror_root(mirror_root: &Path, clean_rebuild: bool) -> Result<(), CirMaterializeError> {
if clean_rebuild && mirror_root.exists() {
fs::remove_dir_all(mirror_root).map_err(|e| CirMaterializeError::RemoveMirrorRoot {
path: mirror_root.display().to_string(),
detail: e.to_string(),
})?;
}
fs::create_dir_all(mirror_root).map_err(|e| CirMaterializeError::CreateMirrorRoot {
path: mirror_root.display().to_string(),
detail: e.to_string(),
})
}
fn write_bytes_to_mirror_uri(
mirror_root: &Path,
rsync_uri: &str,
bytes: &[u8],
src_label: &str,
) -> Result<(), CirMaterializeError> {
let relative = mirror_relative_path_for_rsync_uri(rsync_uri)?;
let target = mirror_root.join(&relative); let target = mirror_root.join(&relative);
if let Some(parent) = target.parent() { if let Some(parent) = target.parent() {
@ -284,33 +317,23 @@ pub fn materialize_cir_from_repo_bytes(
})?; })?;
} }
fs::write(&target, &bytes).map_err(|e| CirMaterializeError::Copy { fs::write(&target, bytes).map_err(|e| CirMaterializeError::Copy {
src: repo_bytes_db.display().to_string(), src: src_label.to_string(),
dst: target.display().to_string(), dst: target.display().to_string(),
detail: e.to_string(), detail: e.to_string(),
})?; })
copied_files += 1; }
}
let actual = collect_materialized_uris(mirror_root)?; fn expected_materialized_uris(cir: &CanonicalInputRepresentation) -> BTreeSet<String> {
let expected = cir cir.objects
.objects
.iter() .iter()
.map(|item| item.rsync_uri.clone()) .map(|item| item.rsync_uri.clone())
.collect::<std::collections::BTreeSet<_>>(); .chain(
if actual != expected { cir.trust_anchors
return Err(CirMaterializeError::TreeMismatch(format!( .iter()
"expected {} files, got {} files", .map(|item| item.ta_rsync_uri.clone()),
expected.len(), )
actual.len() .collect()
)));
}
Ok(CirMaterializeSummary {
object_count: cir.objects.len(),
linked_files: 0,
copied_files,
})
} }
pub fn mirror_relative_path_for_rsync_uri(rsync_uri: &str) -> Result<PathBuf, CirMaterializeError> { pub fn mirror_relative_path_for_rsync_uri(rsync_uri: &str) -> Result<PathBuf, CirMaterializeError> {
@ -375,10 +398,8 @@ pub fn resolve_static_pool_file(
}) })
} }
fn collect_materialized_uris( fn collect_materialized_uris(mirror_root: &Path) -> Result<BTreeSet<String>, CirMaterializeError> {
mirror_root: &Path, let mut out = BTreeSet::new();
) -> Result<std::collections::BTreeSet<String>, CirMaterializeError> {
let mut out = std::collections::BTreeSet::new();
let mut stack = vec![mirror_root.to_path_buf()]; let mut stack = vec![mirror_root.to_path_buf()];
while let Some(path) = stack.pop() { while let Some(path) = stack.pop() {
for entry in fs::read_dir(&path).map_err(|e| CirMaterializeError::CreateMirrorRoot { for entry in fs::read_dir(&path).map_err(|e| CirMaterializeError::CreateMirrorRoot {
@ -415,8 +436,8 @@ mod tests {
}; };
use crate::blob_store::{ExternalRawStoreDb, ExternalRepoBytesDb}; use crate::blob_store::{ExternalRawStoreDb, ExternalRepoBytesDb};
use crate::cir::model::{ use crate::cir::model::{
CIR_VERSION_V2, CanonicalInputRepresentation, CirHashAlgorithm, CirObject, CIR_VERSION_V3, CanonicalInputRepresentation, CirHashAlgorithm, CirObject,
CirRejectedObject, CirTal, compute_reject_list_sha256, CirRejectedObject, CirTrustAnchor, compute_reject_list_sha256, sha256,
}; };
use sha2::Digest; use sha2::Digest;
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
@ -429,13 +450,25 @@ mod tests {
.unwrap() .unwrap()
} }
fn sample_trust_anchor() -> CirTrustAnchor {
let ta_rsync_uri = "rsync://example.net/repo/ta.cer";
let ta_certificate_der = b"ta-der".to_vec();
CirTrustAnchor {
ta_rsync_uri: ta_rsync_uri.to_string(),
tal_uri: "https://tal.example.net/root.tal".to_string(),
tal_bytes: format!("{ta_rsync_uri}\n\nAQID\n").into_bytes(),
ta_certificate_sha256: sha256(&ta_certificate_der),
ta_certificate_der,
}
}
fn sample_cir() -> CanonicalInputRepresentation { fn sample_cir() -> CanonicalInputRepresentation {
let rejected_objects = vec![CirRejectedObject { let rejected_objects = vec![CirRejectedObject {
object_uri: "rsync://example.net/repo/rejected-a.roa".to_string(), object_uri: "rsync://example.net/repo/rejected-a.roa".to_string(),
reason: Some("invalid roa".to_string()), reason: Some("invalid roa".to_string()),
}]; }];
CanonicalInputRepresentation { CanonicalInputRepresentation {
version: CIR_VERSION_V2, version: CIR_VERSION_V3,
hash_alg: CirHashAlgorithm::Sha256, hash_alg: CirHashAlgorithm::Sha256,
validation_time: sample_time(), validation_time: sample_time(),
objects: vec![ objects: vec![
@ -454,10 +487,7 @@ mod tests {
.unwrap(), .unwrap(),
}, },
], ],
tals: vec![CirTal { trust_anchors: vec![sample_trust_anchor()],
tal_uri: "https://tal.example.net/root.tal".to_string(),
tal_bytes: b"x".to_vec(),
}],
reject_list_sha256: compute_reject_list_sha256( reject_list_sha256: compute_reject_list_sha256(
rejected_objects.iter().map(|item| item.object_uri.as_str()), rejected_objects.iter().map(|item| item.object_uri.as_str()),
), ),
@ -467,7 +497,7 @@ mod tests {
fn cir_with_real_hashes(a: &[u8], b: &[u8]) -> CanonicalInputRepresentation { fn cir_with_real_hashes(a: &[u8], b: &[u8]) -> CanonicalInputRepresentation {
CanonicalInputRepresentation { CanonicalInputRepresentation {
version: CIR_VERSION_V2, version: CIR_VERSION_V3,
hash_alg: CirHashAlgorithm::Sha256, hash_alg: CirHashAlgorithm::Sha256,
validation_time: sample_time(), validation_time: sample_time(),
objects: vec![ objects: vec![
@ -480,10 +510,7 @@ mod tests {
sha256: sha2::Sha256::digest(b).to_vec(), sha256: sha2::Sha256::digest(b).to_vec(),
}, },
], ],
tals: vec![CirTal { trust_anchors: vec![sample_trust_anchor()],
tal_uri: "https://tal.example.net/root.tal".to_string(),
tal_bytes: b"x".to_vec(),
}],
reject_list_sha256: compute_reject_list_sha256(std::iter::empty::<&str>()), reject_list_sha256: compute_reject_list_sha256(std::iter::empty::<&str>()),
rejected_objects: Vec::new(), rejected_objects: Vec::new(),
} }
@ -573,6 +600,8 @@ mod tests {
let summary = materialize_cir(&sample_cir(), &static_root, &mirror_root, true).unwrap(); let summary = materialize_cir(&sample_cir(), &static_root, &mirror_root, true).unwrap();
assert_eq!(summary.object_count, 2); assert_eq!(summary.object_count, 2);
assert_eq!(summary.trust_anchor_count, 1);
assert_eq!(summary.materialized_file_count, 3);
assert_eq!( assert_eq!(
std::fs::read(mirror_root.join("example.net/repo/a.cer")).unwrap(), std::fs::read(mirror_root.join("example.net/repo/a.cer")).unwrap(),
b"a" b"a"
@ -581,6 +610,10 @@ mod tests {
std::fs::read(mirror_root.join("example.net/repo/nested/b.roa")).unwrap(), std::fs::read(mirror_root.join("example.net/repo/nested/b.roa")).unwrap(),
b"b" b"b"
); );
assert_eq!(
std::fs::read(mirror_root.join("example.net/repo/ta.cer")).unwrap(),
b"ta-der"
);
assert!(!mirror_root.join("stale/old.txt").exists()); assert!(!mirror_root.join("stale/old.txt").exists());
} }
@ -629,7 +662,7 @@ mod tests {
let a = b"a".to_vec(); let a = b"a".to_vec();
let b = b"b".to_vec(); let b = b"b".to_vec();
let cir = CanonicalInputRepresentation { let cir = CanonicalInputRepresentation {
version: CIR_VERSION_V2, version: CIR_VERSION_V3,
hash_alg: CirHashAlgorithm::Sha256, hash_alg: CirHashAlgorithm::Sha256,
validation_time: sample_time(), validation_time: sample_time(),
objects: vec![ objects: vec![
@ -642,10 +675,7 @@ mod tests {
sha256: sha2::Sha256::digest(&b).to_vec(), sha256: sha2::Sha256::digest(&b).to_vec(),
}, },
], ],
tals: vec![CirTal { trust_anchors: vec![sample_trust_anchor()],
tal_uri: "https://tal.example.net/root.tal".to_string(),
tal_bytes: b"x".to_vec(),
}],
reject_list_sha256: compute_reject_list_sha256(std::iter::empty::<&str>()), reject_list_sha256: compute_reject_list_sha256(std::iter::empty::<&str>()),
rejected_objects: Vec::new(), rejected_objects: Vec::new(),
}; };
@ -663,8 +693,10 @@ mod tests {
let summary = let summary =
materialize_cir_from_raw_store(&cir, &raw_store_path, &mirror_root, true).unwrap(); materialize_cir_from_raw_store(&cir, &raw_store_path, &mirror_root, true).unwrap();
assert_eq!(summary.object_count, 2); assert_eq!(summary.object_count, 2);
assert_eq!(summary.trust_anchor_count, 1);
assert_eq!(summary.materialized_file_count, 3);
assert_eq!(summary.linked_files, 0); assert_eq!(summary.linked_files, 0);
assert_eq!(summary.copied_files, 2); assert_eq!(summary.copied_files, 3);
assert_eq!( assert_eq!(
std::fs::read(mirror_root.join("example.net/repo/a.cer")).unwrap(), std::fs::read(mirror_root.join("example.net/repo/a.cer")).unwrap(),
b"a" b"a"
@ -673,6 +705,10 @@ mod tests {
std::fs::read(mirror_root.join("example.net/repo/nested/b.roa")).unwrap(), std::fs::read(mirror_root.join("example.net/repo/nested/b.roa")).unwrap(),
b"b" b"b"
); );
assert_eq!(
std::fs::read(mirror_root.join("example.net/repo/ta.cer")).unwrap(),
b"ta-der"
);
} }
#[test] #[test]
@ -742,7 +778,7 @@ mod tests {
let summary = let summary =
materialize_cir_from_raw_store(&cir, &raw_store_path, &mirror_root, false).unwrap(); materialize_cir_from_raw_store(&cir, &raw_store_path, &mirror_root, false).unwrap();
assert_eq!(summary.copied_files, 2); assert_eq!(summary.copied_files, 3);
assert_eq!(std::fs::read(&target).unwrap(), a); assert_eq!(std::fs::read(&target).unwrap(), a);
} }
@ -752,7 +788,7 @@ mod tests {
let raw_store_path = td.path().join("raw-store.db"); let raw_store_path = td.path().join("raw-store.db");
let mirror_root = td.path().join("mirror"); let mirror_root = td.path().join("mirror");
let cir = CanonicalInputRepresentation { let cir = CanonicalInputRepresentation {
version: CIR_VERSION_V2, version: CIR_VERSION_V3,
hash_alg: CirHashAlgorithm::Sha256, hash_alg: CirHashAlgorithm::Sha256,
validation_time: sample_time(), validation_time: sample_time(),
objects: vec![CirObject { objects: vec![CirObject {
@ -762,10 +798,7 @@ mod tests {
) )
.unwrap(), .unwrap(),
}], }],
tals: vec![CirTal { trust_anchors: vec![sample_trust_anchor()],
tal_uri: "https://tal.example.net/root.tal".to_string(),
tal_bytes: b"x".to_vec(),
}],
reject_list_sha256: compute_reject_list_sha256(std::iter::empty::<&str>()), reject_list_sha256: compute_reject_list_sha256(std::iter::empty::<&str>()),
rejected_objects: Vec::new(), rejected_objects: Vec::new(),
}; };
@ -790,10 +823,15 @@ mod tests {
let summary = let summary =
materialize_cir_from_raw_store(&cir, &raw_store_path, &mirror_root, true).unwrap(); materialize_cir_from_raw_store(&cir, &raw_store_path, &mirror_root, true).unwrap();
assert_eq!(summary.object_count, 1); assert_eq!(summary.object_count, 1);
assert_eq!(summary.materialized_file_count, 2);
assert_eq!( assert_eq!(
std::fs::read(mirror_root.join("example.net/repo/a.cer")).unwrap(), std::fs::read(mirror_root.join("example.net/repo/a.cer")).unwrap(),
b"blob-a" b"blob-a"
); );
assert_eq!(
std::fs::read(mirror_root.join("example.net/repo/ta.cer")).unwrap(),
b"ta-der"
);
} }
#[test] #[test]
@ -804,7 +842,7 @@ mod tests {
let a = b"a".to_vec(); let a = b"a".to_vec();
let b = b"b".to_vec(); let b = b"b".to_vec();
let cir = CanonicalInputRepresentation { let cir = CanonicalInputRepresentation {
version: CIR_VERSION_V2, version: CIR_VERSION_V3,
hash_alg: CirHashAlgorithm::Sha256, hash_alg: CirHashAlgorithm::Sha256,
validation_time: sample_time(), validation_time: sample_time(),
objects: vec![ objects: vec![
@ -817,10 +855,7 @@ mod tests {
sha256: sha2::Sha256::digest(&b).to_vec(), sha256: sha2::Sha256::digest(&b).to_vec(),
}, },
], ],
tals: vec![CirTal { trust_anchors: vec![sample_trust_anchor()],
tal_uri: "https://tal.example.net/root.tal".to_string(),
tal_bytes: b"x".to_vec(),
}],
reject_list_sha256: compute_reject_list_sha256(std::iter::empty::<&str>()), reject_list_sha256: compute_reject_list_sha256(std::iter::empty::<&str>()),
rejected_objects: Vec::new(), rejected_objects: Vec::new(),
}; };
@ -838,8 +873,10 @@ mod tests {
let summary = let summary =
materialize_cir_from_repo_bytes(&cir, &repo_bytes_db, &mirror_root, true).unwrap(); materialize_cir_from_repo_bytes(&cir, &repo_bytes_db, &mirror_root, true).unwrap();
assert_eq!(summary.object_count, 2); assert_eq!(summary.object_count, 2);
assert_eq!(summary.trust_anchor_count, 1);
assert_eq!(summary.materialized_file_count, 3);
assert_eq!(summary.linked_files, 0); assert_eq!(summary.linked_files, 0);
assert_eq!(summary.copied_files, 2); assert_eq!(summary.copied_files, 3);
assert_eq!( assert_eq!(
std::fs::read(mirror_root.join("example.net/repo/a.cer")).unwrap(), std::fs::read(mirror_root.join("example.net/repo/a.cer")).unwrap(),
b"a" b"a"
@ -848,6 +885,10 @@ mod tests {
std::fs::read(mirror_root.join("example.net/repo/nested/b.roa")).unwrap(), std::fs::read(mirror_root.join("example.net/repo/nested/b.roa")).unwrap(),
b"b" b"b"
); );
assert_eq!(
std::fs::read(mirror_root.join("example.net/repo/ta.cer")).unwrap(),
b"ta-der"
);
} }
fn write_static(root: &Path, date: &str, hash: &str, bytes: &[u8]) { fn write_static(root: &Path, date: &str, hash: &str, bytes: &[u8]) {

View File

@ -12,16 +12,16 @@ pub use decode::{CirDecodeError, decode_cir};
pub use encode::{CirEncodeError, encode_cir}; pub use encode::{CirEncodeError, encode_cir};
#[cfg(feature = "full")] #[cfg(feature = "full")]
pub use export::{ pub use export::{
CirExportError, CirExportSummary, CirTalBinding, build_cir_from_run, build_cir_from_run_multi, CirExportError, CirExportSummary, CirTrustAnchorBinding, build_cir_from_run,
export_cir_from_run, export_cir_from_run_multi, write_cir_file, build_cir_from_run_multi, export_cir_from_run, export_cir_from_run_multi, write_cir_file,
}; };
pub use materialize::{ pub use materialize::{
CirMaterializeError, CirMaterializeSummary, materialize_cir, materialize_cir_from_raw_store, CirMaterializeError, CirMaterializeSummary, materialize_cir, materialize_cir_from_raw_store,
materialize_cir_from_repo_bytes, mirror_relative_path_for_rsync_uri, resolve_static_pool_file, materialize_cir_from_repo_bytes, mirror_relative_path_for_rsync_uri, resolve_static_pool_file,
}; };
pub use model::{ pub use model::{
CIR_VERSION_V1, CIR_VERSION_V2, CanonicalInputRepresentation, CirHashAlgorithm, CirObject, CIR_VERSION_V1, CIR_VERSION_V3, CanonicalInputRepresentation, CirHashAlgorithm, CirObject,
CirRejectedObject, CirTal, compute_reject_list_sha256, CirRejectedObject, CirTrustAnchor, compute_reject_list_sha256, sha256,
}; };
pub use sequence::{CirSequenceManifest, CirSequenceStep, CirSequenceStepKind}; pub use sequence::{CirSequenceManifest, CirSequenceStep, CirSequenceStepKind};
#[cfg(feature = "full")] #[cfg(feature = "full")]
@ -34,8 +34,8 @@ pub use static_pool::{
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::{ use super::{
CIR_VERSION_V2, CanonicalInputRepresentation, CirHashAlgorithm, CirObject, CIR_VERSION_V3, CanonicalInputRepresentation, CirHashAlgorithm, CirObject,
CirRejectedObject, CirTal, compute_reject_list_sha256, decode_cir, encode_cir, CirRejectedObject, CirTrustAnchor, compute_reject_list_sha256, decode_cir, encode_cir,
}; };
fn sample_time() -> time::OffsetDateTime { fn sample_time() -> time::OffsetDateTime {
@ -46,6 +46,16 @@ mod tests {
.expect("valid rfc3339") .expect("valid rfc3339")
} }
fn sample_trust_anchor(ta_rsync_uri: &str, tal_uri: &str, ta_der: &[u8]) -> CirTrustAnchor {
CirTrustAnchor {
ta_rsync_uri: ta_rsync_uri.to_string(),
tal_uri: tal_uri.to_string(),
tal_bytes: format!("{ta_rsync_uri}\n\nAQID\n").into_bytes(),
ta_certificate_der: ta_der.to_vec(),
ta_certificate_sha256: super::sha256(ta_der),
}
}
fn sample_cir() -> CanonicalInputRepresentation { fn sample_cir() -> CanonicalInputRepresentation {
let rejected_objects = vec![ let rejected_objects = vec![
CirRejectedObject { CirRejectedObject {
@ -58,7 +68,7 @@ mod tests {
}, },
]; ];
CanonicalInputRepresentation { CanonicalInputRepresentation {
version: CIR_VERSION_V2, version: CIR_VERSION_V3,
hash_alg: CirHashAlgorithm::Sha256, hash_alg: CirHashAlgorithm::Sha256,
validation_time: sample_time(), validation_time: sample_time(),
objects: vec![ objects: vec![
@ -71,11 +81,11 @@ mod tests {
sha256: vec![0x22; 32], sha256: vec![0x22; 32],
}, },
], ],
tals: vec![CirTal { trust_anchors: vec![sample_trust_anchor(
tal_uri: "https://tal.example.net/root.tal".to_string(), "rsync://example.net/repo/ta.cer",
tal_bytes: b"https://tal.example.net/ta.cer\nrsync://example.net/repo/ta.cer\nMIIB" "https://tal.example.net/root.tal",
.to_vec(), b"ta-der",
}], )],
reject_list_sha256: compute_reject_list_sha256( reject_list_sha256: compute_reject_list_sha256(
rejected_objects.iter().map(|item| item.object_uri.as_str()), rejected_objects.iter().map(|item| item.object_uri.as_str()),
), ),
@ -114,14 +124,15 @@ mod tests {
#[test] #[test]
fn cir_roundtrip_minimal_succeeds() { fn cir_roundtrip_minimal_succeeds() {
let cir = CanonicalInputRepresentation { let cir = CanonicalInputRepresentation {
version: CIR_VERSION_V2, version: CIR_VERSION_V3,
hash_alg: CirHashAlgorithm::Sha256, hash_alg: CirHashAlgorithm::Sha256,
validation_time: sample_time(), validation_time: sample_time(),
objects: Vec::new(), objects: Vec::new(),
tals: vec![CirTal { trust_anchors: vec![sample_trust_anchor(
tal_uri: "https://tal.example.net/minimal.tal".to_string(), "rsync://example.net/repo/minimal-ta.cer",
tal_bytes: b"rsync://example.net/repo/ta.cer\nMIIB".to_vec(), "https://tal.example.net/minimal.tal",
}], b"minimal-ta-der",
)],
reject_list_sha256: compute_reject_list_sha256(std::iter::empty::<&str>()), reject_list_sha256: compute_reject_list_sha256(std::iter::empty::<&str>()),
rejected_objects: Vec::new(), rejected_objects: Vec::new(),
}; };
@ -133,7 +144,7 @@ mod tests {
#[test] #[test]
fn cir_model_rejects_unsorted_duplicate_objects() { fn cir_model_rejects_unsorted_duplicate_objects() {
let cir = CanonicalInputRepresentation { let cir = CanonicalInputRepresentation {
version: CIR_VERSION_V2, version: CIR_VERSION_V3,
hash_alg: CirHashAlgorithm::Sha256, hash_alg: CirHashAlgorithm::Sha256,
validation_time: sample_time(), validation_time: sample_time(),
objects: vec![ objects: vec![
@ -146,10 +157,11 @@ mod tests {
sha256: vec![0x22; 32], sha256: vec![0x22; 32],
}, },
], ],
tals: vec![CirTal { trust_anchors: vec![sample_trust_anchor(
tal_uri: "https://tal.example.net/root.tal".to_string(), "rsync://example.net/repo/ta.cer",
tal_bytes: b"x".to_vec(), "https://tal.example.net/root.tal",
}], b"ta-der",
)],
reject_list_sha256: compute_reject_list_sha256(std::iter::empty::<&str>()), reject_list_sha256: compute_reject_list_sha256(std::iter::empty::<&str>()),
rejected_objects: Vec::new(), rejected_objects: Vec::new(),
}; };
@ -160,25 +172,27 @@ mod tests {
#[test] #[test]
fn cir_model_rejects_duplicate_tals() { fn cir_model_rejects_duplicate_tals() {
let cir = CanonicalInputRepresentation { let cir = CanonicalInputRepresentation {
version: CIR_VERSION_V2, version: CIR_VERSION_V3,
hash_alg: CirHashAlgorithm::Sha256, hash_alg: CirHashAlgorithm::Sha256,
validation_time: sample_time(), validation_time: sample_time(),
objects: Vec::new(), objects: Vec::new(),
tals: vec![ trust_anchors: vec![
CirTal { sample_trust_anchor(
tal_uri: "https://tal.example.net/root.tal".to_string(), "rsync://example.net/repo/ta.cer",
tal_bytes: b"a".to_vec(), "https://tal.example.net/root.tal",
}, b"ta-der-a",
CirTal { ),
tal_uri: "https://tal.example.net/root.tal".to_string(), sample_trust_anchor(
tal_bytes: b"b".to_vec(), "rsync://example.net/repo/ta.cer",
}, "https://tal.example.net/root.tal",
b"ta-der-b",
),
], ],
reject_list_sha256: compute_reject_list_sha256(std::iter::empty::<&str>()), reject_list_sha256: compute_reject_list_sha256(std::iter::empty::<&str>()),
rejected_objects: Vec::new(), rejected_objects: Vec::new(),
}; };
let err = encode_cir(&cir).expect_err("duplicate tals must fail"); let err = encode_cir(&cir).expect_err("duplicate trust_anchors must fail");
assert!(err.to_string().contains("CIR.tals"), "{err}"); assert!(err.to_string().contains("CIR.trustAnchors"), "{err}");
} }
#[test] #[test]
@ -186,13 +200,13 @@ mod tests {
let mut der = encode_cir(&sample_cir()).expect("encode cir"); let mut der = encode_cir(&sample_cir()).expect("encode cir");
let pos = der let pos = der
.windows(3) .windows(3)
.position(|window| window == [0x02, 0x01, CIR_VERSION_V2 as u8]) .position(|window| window == [0x02, 0x01, CIR_VERSION_V3 as u8])
.or_else(|| { .or_else(|| {
der.windows(3) der.windows(3)
.position(|window| window == [0x02, 0x01, CIR_VERSION_V2 as u8]) .position(|window| window == [0x02, 0x01, CIR_VERSION_V3 as u8])
}) })
.expect("find version integer"); .expect("find version integer");
der[pos + 2] = 3; der[pos + 2] = 2;
let err = decode_cir(&der).expect_err("wrong version must fail"); let err = decode_cir(&der).expect_err("wrong version must fail");
assert!(err.to_string().contains("unexpected CIR version"), "{err}"); assert!(err.to_string().contains("unexpected CIR version"), "{err}");
} }
@ -228,17 +242,18 @@ mod tests {
#[test] #[test]
fn cir_model_rejects_non_rsync_object_uri_and_empty_tals() { fn cir_model_rejects_non_rsync_object_uri_and_empty_tals() {
let bad_object = CanonicalInputRepresentation { let bad_object = CanonicalInputRepresentation {
version: CIR_VERSION_V2, version: CIR_VERSION_V3,
hash_alg: CirHashAlgorithm::Sha256, hash_alg: CirHashAlgorithm::Sha256,
validation_time: sample_time(), validation_time: sample_time(),
objects: vec![CirObject { objects: vec![CirObject {
rsync_uri: "https://example.net/repo/a.roa".to_string(), rsync_uri: "https://example.net/repo/a.roa".to_string(),
sha256: vec![0x11; 32], sha256: vec![0x11; 32],
}], }],
tals: vec![CirTal { trust_anchors: vec![sample_trust_anchor(
tal_uri: "https://tal.example.net/root.tal".to_string(), "rsync://example.net/repo/ta.cer",
tal_bytes: b"x".to_vec(), "https://tal.example.net/root.tal",
}], b"ta-der",
)],
reject_list_sha256: compute_reject_list_sha256(std::iter::empty::<&str>()), reject_list_sha256: compute_reject_list_sha256(std::iter::empty::<&str>()),
rejected_objects: Vec::new(), rejected_objects: Vec::new(),
}; };
@ -246,17 +261,18 @@ mod tests {
assert!(err.to_string().contains("rsync://"), "{err}"); assert!(err.to_string().contains("rsync://"), "{err}");
let no_tals = CanonicalInputRepresentation { let no_tals = CanonicalInputRepresentation {
version: CIR_VERSION_V2, version: CIR_VERSION_V3,
hash_alg: CirHashAlgorithm::Sha256, hash_alg: CirHashAlgorithm::Sha256,
validation_time: sample_time(), validation_time: sample_time(),
objects: Vec::new(), objects: Vec::new(),
tals: Vec::new(), trust_anchors: Vec::new(),
reject_list_sha256: compute_reject_list_sha256(std::iter::empty::<&str>()), reject_list_sha256: compute_reject_list_sha256(std::iter::empty::<&str>()),
rejected_objects: Vec::new(), rejected_objects: Vec::new(),
}; };
let err = encode_cir(&no_tals).expect_err("empty tals must fail"); let err = encode_cir(&no_tals).expect_err("empty trust_anchors must fail");
assert!( assert!(
err.to_string().contains("CIR.tals must be non-empty"), err.to_string()
.contains("CIR.trustAnchors must be non-empty"),
"{err}" "{err}"
); );
} }
@ -264,14 +280,15 @@ mod tests {
#[test] #[test]
fn cir_model_rejects_non_utc_time_bad_hash_len_and_non_http_tal_uri() { fn cir_model_rejects_non_utc_time_bad_hash_len_and_non_http_tal_uri() {
let bad_time = CanonicalInputRepresentation { let bad_time = CanonicalInputRepresentation {
version: CIR_VERSION_V2, version: CIR_VERSION_V3,
hash_alg: CirHashAlgorithm::Sha256, hash_alg: CirHashAlgorithm::Sha256,
validation_time: sample_time().to_offset(time::UtcOffset::from_hms(8, 0, 0).unwrap()), validation_time: sample_time().to_offset(time::UtcOffset::from_hms(8, 0, 0).unwrap()),
objects: Vec::new(), objects: Vec::new(),
tals: vec![CirTal { trust_anchors: vec![sample_trust_anchor(
tal_uri: "https://tal.example.net/root.tal".to_string(), "rsync://example.net/repo/ta.cer",
tal_bytes: b"x".to_vec(), "https://tal.example.net/root.tal",
}], b"ta-der",
)],
reject_list_sha256: compute_reject_list_sha256(std::iter::empty::<&str>()), reject_list_sha256: compute_reject_list_sha256(std::iter::empty::<&str>()),
rejected_objects: Vec::new(), rejected_objects: Vec::new(),
}; };
@ -279,17 +296,18 @@ mod tests {
assert!(err.to_string().contains("UTC"), "{err}"); assert!(err.to_string().contains("UTC"), "{err}");
let bad_hash = CanonicalInputRepresentation { let bad_hash = CanonicalInputRepresentation {
version: CIR_VERSION_V2, version: CIR_VERSION_V3,
hash_alg: CirHashAlgorithm::Sha256, hash_alg: CirHashAlgorithm::Sha256,
validation_time: sample_time(), validation_time: sample_time(),
objects: vec![CirObject { objects: vec![CirObject {
rsync_uri: "rsync://example.net/repo/a.roa".to_string(), rsync_uri: "rsync://example.net/repo/a.roa".to_string(),
sha256: vec![0x11; 31], sha256: vec![0x11; 31],
}], }],
tals: vec![CirTal { trust_anchors: vec![sample_trust_anchor(
tal_uri: "https://tal.example.net/root.tal".to_string(), "rsync://example.net/repo/ta.cer",
tal_bytes: b"x".to_vec(), "https://tal.example.net/root.tal",
}], b"ta-der",
)],
reject_list_sha256: compute_reject_list_sha256(std::iter::empty::<&str>()), reject_list_sha256: compute_reject_list_sha256(std::iter::empty::<&str>()),
rejected_objects: Vec::new(), rejected_objects: Vec::new(),
}; };
@ -297,14 +315,15 @@ mod tests {
assert!(err.to_string().contains("32 bytes"), "{err}"); assert!(err.to_string().contains("32 bytes"), "{err}");
let bad_tal_uri = CanonicalInputRepresentation { let bad_tal_uri = CanonicalInputRepresentation {
version: CIR_VERSION_V2, version: CIR_VERSION_V3,
hash_alg: CirHashAlgorithm::Sha256, hash_alg: CirHashAlgorithm::Sha256,
validation_time: sample_time(), validation_time: sample_time(),
objects: Vec::new(), objects: Vec::new(),
tals: vec![CirTal { trust_anchors: vec![sample_trust_anchor(
tal_uri: "ftp://tal.example.net/root.tal".to_string(), "rsync://example.net/repo/ta.cer",
tal_bytes: b"x".to_vec(), "ftp://tal.example.net/root.tal",
}], b"ta-der",
)],
reject_list_sha256: compute_reject_list_sha256(std::iter::empty::<&str>()), reject_list_sha256: compute_reject_list_sha256(std::iter::empty::<&str>()),
rejected_objects: Vec::new(), rejected_objects: Vec::new(),
}; };
@ -332,22 +351,25 @@ mod tests {
] ]
.concat(), .concat(),
); );
let tal = test_encode_tlv( let trust_anchor = test_encode_tlv(
0x30, 0x30,
&[ &[
test_encode_tlv(0x16, b"rsync://example.net/repo/ta.cer"),
test_encode_tlv(0x16, b"https://tal.example.net/root.tal"), test_encode_tlv(0x16, b"https://tal.example.net/root.tal"),
test_encode_tlv(0x04, b"x"), test_encode_tlv(0x04, b"rsync://example.net/repo/ta.cer\n\nAQID\n"),
test_encode_tlv(0x04, b"ta-der"),
test_encode_tlv(0x04, &super::sha256(b"ta-der")),
] ]
.concat(), .concat(),
); );
let bad = test_encode_tlv( let bad = test_encode_tlv(
0x30, 0x30,
&[ &[
test_encode_tlv(0x02, &[CIR_VERSION_V2 as u8]), test_encode_tlv(0x02, &[CIR_VERSION_V3 as u8]),
test_encode_tlv(0x06, crate::data_model::oid::OID_SHA256_RAW), test_encode_tlv(0x06, crate::data_model::oid::OID_SHA256_RAW),
test_encode_tlv(0x18, b"20260407123456Z"), test_encode_tlv(0x18, b"20260407123456Z"),
test_encode_tlv(0x30, &object), test_encode_tlv(0x30, &object),
test_encode_tlv(0x30, &tal), test_encode_tlv(0x30, &trust_anchor),
test_encode_tlv(0x04, &[0x33; 32]), test_encode_tlv(0x04, &[0x33; 32]),
test_encode_tlv(0x30, &[]), test_encode_tlv(0x30, &[]),
] ]

View File

@ -2,6 +2,7 @@ use crate::data_model::oid::OID_SHA256;
pub const CIR_VERSION_V1: u32 = 1; pub const CIR_VERSION_V1: u32 = 1;
pub const CIR_VERSION_V2: u32 = 2; pub const CIR_VERSION_V2: u32 = 2;
pub const CIR_VERSION_V3: u32 = 3;
pub const DIGEST_LEN_SHA256: usize = 32; pub const DIGEST_LEN_SHA256: usize = 32;
#[derive(Clone, Debug, PartialEq, Eq)] #[derive(Clone, Debug, PartialEq, Eq)]
@ -23,16 +24,16 @@ pub struct CanonicalInputRepresentation {
pub hash_alg: CirHashAlgorithm, pub hash_alg: CirHashAlgorithm,
pub validation_time: time::OffsetDateTime, pub validation_time: time::OffsetDateTime,
pub objects: Vec<CirObject>, pub objects: Vec<CirObject>,
pub tals: Vec<CirTal>, pub trust_anchors: Vec<CirTrustAnchor>,
pub reject_list_sha256: Vec<u8>, pub reject_list_sha256: Vec<u8>,
pub rejected_objects: Vec<CirRejectedObject>, pub rejected_objects: Vec<CirRejectedObject>,
} }
impl CanonicalInputRepresentation { impl CanonicalInputRepresentation {
pub fn validate(&self) -> Result<(), String> { pub fn validate(&self) -> Result<(), String> {
if self.version != CIR_VERSION_V2 { if self.version != CIR_VERSION_V3 {
return Err(format!( return Err(format!(
"CIR version must be {CIR_VERSION_V2}, got {}", "CIR version must be {CIR_VERSION_V3}, got {}",
self.version self.version
)); ));
} }
@ -47,8 +48,10 @@ impl CanonicalInputRepresentation {
"CIR.objects must be sorted by rsyncUri and unique", "CIR.objects must be sorted by rsyncUri and unique",
)?; )?;
validate_sorted_unique_strings( validate_sorted_unique_strings(
self.tals.iter().map(|item| item.tal_uri.as_str()), self.trust_anchors
"CIR.tals must be sorted by talUri and unique", .iter()
.map(|item| item.ta_rsync_uri.as_str()),
"CIR.trustAnchors must be sorted by taRsyncUri and unique",
)?; )?;
validate_sorted_unique_strings( validate_sorted_unique_strings(
self.rejected_objects self.rejected_objects
@ -56,8 +59,21 @@ impl CanonicalInputRepresentation {
.map(|item| item.object_uri.as_str()), .map(|item| item.object_uri.as_str()),
"CIR.rejectedObjects must be sorted by objectUri and unique", "CIR.rejectedObjects must be sorted by objectUri and unique",
)?; )?;
if self.tals.is_empty() { let object_uris = self
return Err("CIR.tals must be non-empty".into()); .objects
.iter()
.map(|item| item.rsync_uri.as_str())
.collect::<std::collections::BTreeSet<_>>();
for trust_anchor in &self.trust_anchors {
if object_uris.contains(trust_anchor.ta_rsync_uri.as_str()) {
return Err(format!(
"CIR.objects must not include trust anchor URI {}",
trust_anchor.ta_rsync_uri
));
}
}
if self.trust_anchors.is_empty() {
return Err("CIR.trustAnchors must be non-empty".into());
} }
if self.reject_list_sha256.len() != DIGEST_LEN_SHA256 { if self.reject_list_sha256.len() != DIGEST_LEN_SHA256 {
return Err(format!( return Err(format!(
@ -68,8 +84,8 @@ impl CanonicalInputRepresentation {
for object in &self.objects { for object in &self.objects {
object.validate()?; object.validate()?;
} }
for tal in &self.tals { for trust_anchor in &self.trust_anchors {
tal.validate()?; trust_anchor.validate()?;
} }
for item in &self.rejected_objects { for item in &self.rejected_objects {
item.validate()?; item.validate()?;
@ -111,21 +127,55 @@ impl CirObject {
} }
#[derive(Clone, Debug, PartialEq, Eq)] #[derive(Clone, Debug, PartialEq, Eq)]
pub struct CirTal { pub struct CirTrustAnchor {
pub ta_rsync_uri: String,
pub tal_uri: String, pub tal_uri: String,
pub tal_bytes: Vec<u8>, pub tal_bytes: Vec<u8>,
pub ta_certificate_der: Vec<u8>,
pub ta_certificate_sha256: Vec<u8>,
} }
impl CirTal { impl CirTrustAnchor {
pub fn validate(&self) -> Result<(), String> { pub fn validate(&self) -> Result<(), String> {
if !self.ta_rsync_uri.starts_with("rsync://") {
return Err(format!(
"CirTrustAnchor.ta_rsync_uri must start with rsync://, got {}",
self.ta_rsync_uri
));
}
if !(self.tal_uri.starts_with("https://") || self.tal_uri.starts_with("http://")) { if !(self.tal_uri.starts_with("https://") || self.tal_uri.starts_with("http://")) {
return Err(format!( return Err(format!(
"CirTal.tal_uri must start with http:// or https://, got {}", "CirTrustAnchor.tal_uri must start with http:// or https://, got {}",
self.tal_uri self.tal_uri
)); ));
} }
if self.tal_bytes.is_empty() { if self.tal_bytes.is_empty() {
return Err("CirTal.tal_bytes must be non-empty".into()); return Err("CirTrustAnchor.tal_bytes must be non-empty".into());
}
let tal = crate::data_model::tal::Tal::decode_bytes(&self.tal_bytes)
.map_err(|e| format!("CirTrustAnchor.tal_bytes must decode as TAL: {e}"))?;
if !tal
.ta_uris
.iter()
.any(|uri| uri.as_str() == self.ta_rsync_uri)
{
return Err(format!(
"CirTrustAnchor.ta_rsync_uri must be listed in TAL bytes: {}",
self.ta_rsync_uri
));
}
if self.ta_certificate_der.is_empty() {
return Err("CirTrustAnchor.ta_certificate_der must be non-empty".into());
}
if self.ta_certificate_sha256.len() != DIGEST_LEN_SHA256 {
return Err(format!(
"CirTrustAnchor.ta_certificate_sha256 must be {DIGEST_LEN_SHA256} bytes, got {}",
self.ta_certificate_sha256.len()
));
}
let expected = sha256(&self.ta_certificate_der);
if self.ta_certificate_sha256 != expected {
return Err("CirTrustAnchor.ta_certificate_sha256 does not match DER bytes".into());
} }
Ok(()) Ok(())
} }
@ -150,15 +200,19 @@ impl CirRejectedObject {
} }
pub fn compute_reject_list_sha256<'a>(uris: impl IntoIterator<Item = &'a str>) -> Vec<u8> { pub fn compute_reject_list_sha256<'a>(uris: impl IntoIterator<Item = &'a str>) -> Vec<u8> {
use sha2::Digest;
let mut body = Vec::new(); let mut body = Vec::new();
for uri in uris { for uri in uris {
let bytes = uri.as_bytes(); let bytes = uri.as_bytes();
body.extend_from_slice(&(bytes.len() as u32).to_be_bytes()); body.extend_from_slice(&(bytes.len() as u32).to_be_bytes());
body.extend_from_slice(bytes); body.extend_from_slice(bytes);
} }
sha2::Sha256::digest(body).to_vec() sha256(&body)
}
pub fn sha256(bytes: &[u8]) -> Vec<u8> {
use sha2::Digest;
sha2::Sha256::digest(bytes).to_vec()
} }
fn validate_sorted_unique_strings<'a>( fn validate_sorted_unique_strings<'a>(

View File

@ -1,7 +1,7 @@
use crate::ccr::{ use crate::ccr::{
CcrAccumulator, CcrBuildBreakdown, build_ccr_from_run_with_breakdown, write_ccr_file, CcrAccumulator, CcrBuildBreakdown, build_ccr_from_run_with_breakdown, write_ccr_file,
}; };
use crate::cir::{CirTalBinding, export_cir_from_run_multi}; use crate::cir::{CirTrustAnchorBinding, export_cir_from_run_multi};
use std::io::BufWriter; use std::io::BufWriter;
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
@ -1870,7 +1870,7 @@ pub fn run(argv: &[String]) -> Result<(), String> {
.discoveries .discoveries
.iter() .iter()
.zip(cir_tal_uris.iter()) .zip(cir_tal_uris.iter())
.map(|(discovery, tal_uri)| CirTalBinding { .map(|(discovery, tal_uri)| CirTrustAnchorBinding {
trust_anchor: &discovery.trust_anchor, trust_anchor: &discovery.trust_anchor,
tal_uri: tal_uri.as_str(), tal_uri: tal_uri.as_str(),
}) })
@ -1889,10 +1889,10 @@ pub fn run(argv: &[String]) -> Result<(), String> {
cir_write_cir_ms = Some(summary.timing.write_cir_ms); cir_write_cir_ms = Some(summary.timing.write_cir_ms);
cir_total_ms = Some(summary.timing.total_ms); cir_total_ms = Some(summary.timing.total_ms);
eprintln!( eprintln!(
"wrote CIR: {} (objects={}, tals={}, build_cir_ms={}, write_cir_ms={}, total_ms={})", "wrote CIR: {} (objects={}, trust_anchors={}, build_cir_ms={}, write_cir_ms={}, total_ms={})",
cir_out_path.display(), cir_out_path.display(),
summary.object_count, summary.object_count,
summary.tal_count, summary.trust_anchor_count,
summary.timing.build_cir_ms, summary.timing.build_cir_ms,
summary.timing.write_cir_ms, summary.timing.write_cir_ms,
summary.timing.total_ms summary.timing.total_ms

View File

@ -319,11 +319,30 @@ fn discover_root_ca_instance_from_tal_and_ta_der_impl(
}) })
} }
pub fn canonical_tal_rsync_uri_from_bytes(tal_bytes: &[u8]) -> Result<Url, FromTalError> {
let tal = Tal::decode_bytes(tal_bytes)?;
tal.ta_uris
.iter()
.find(|uri| uri.scheme() == "rsync")
.cloned()
.ok_or_else(|| {
FromTalError::TaFetch("TAL contains no rsync TA URI for offline TA binding".to_string())
})
}
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;
use crate::fetch::rsync::LocalDirRsyncFetcher; use crate::fetch::rsync::LocalDirRsyncFetcher;
struct FailingHttpFetcher;
impl Fetcher for FailingHttpFetcher {
fn fetch(&self, uri: &str) -> Result<Vec<u8>, String> {
Err(format!("blocked test HTTP fetch: {uri}"))
}
}
#[test] #[test]
fn discover_root_ca_instance_from_tal_with_fetchers_supports_rsync_ta_uri() { fn discover_root_ca_instance_from_tal_with_fetchers_supports_rsync_ta_uri() {
let tal_bytes = std::fs::read( let tal_bytes = std::fs::read(
@ -352,17 +371,18 @@ mod tests {
std::fs::create_dir_all(&mirror_root).unwrap(); std::fs::create_dir_all(&mirror_root).unwrap();
std::fs::write(mirror_root.join("apnic-rpki-root-iana-origin.cer"), ta_der).unwrap(); std::fs::write(mirror_root.join("apnic-rpki-root-iana-origin.cer"), ta_der).unwrap();
let http = crate::fetch::http::BlockingHttpFetcher::new(
crate::fetch::http::HttpFetcherConfig::default(),
)
.unwrap();
let rsync = LocalDirRsyncFetcher::new( let rsync = LocalDirRsyncFetcher::new(
td.path() td.path()
.join(rsync_uri.host_str().unwrap()) .join(rsync_uri.host_str().unwrap())
.join("repository"), .join("repository"),
); );
let discovery = discover_root_ca_instance_from_tal_with_fetchers(&http, &rsync, tal, None) let discovery = discover_root_ca_instance_from_tal_with_fetchers(
.expect("discover via rsync TA"); &FailingHttpFetcher,
&rsync,
tal,
None,
)
.expect("discover via rsync TA fallback");
assert!( assert!(
discovery discovery
.trust_anchor .trust_anchor

View File

@ -23,7 +23,8 @@ use crate::replay::fetch_http::PayloadReplayHttpFetcher;
use crate::replay::fetch_rsync::PayloadReplayRsyncFetcher; use crate::replay::fetch_rsync::PayloadReplayRsyncFetcher;
use crate::sync::rrdp::Fetcher; use crate::sync::rrdp::Fetcher;
use crate::validation::from_tal::{ use crate::validation::from_tal::{
DiscoveredRootCaInstance, FromTalError, discover_root_ca_instance_from_tal_and_ta_der, DiscoveredRootCaInstance, FromTalError, canonical_tal_rsync_uri_from_bytes,
discover_root_ca_instance_from_tal_and_ta_der,
discover_root_ca_instance_from_tal_and_ta_der_with_strict_name, discover_root_ca_instance_from_tal_and_ta_der_with_strict_name,
discover_root_ca_instance_from_tal_url, discover_root_ca_instance_from_tal_url,
discover_root_ca_instance_from_tal_url_with_strict_name, discover_root_ca_instance_from_tal_url_with_strict_name,
@ -251,12 +252,19 @@ fn root_discovery_from_tal_input(
let ta_der = std::fs::read(ta_path).map_err(|e| { let ta_der = std::fs::read(ta_path).map_err(|e| {
FromTalError::TaFetch(format!("read TA file failed: {}: {e}", ta_path.display())) FromTalError::TaFetch(format!("read TA file failed: {}: {e}", ta_path.display()))
})?; })?;
let resolved_ta_uri = canonical_tal_rsync_uri_from_bytes(&tal_bytes)?;
if strict_name { if strict_name {
discover_root_ca_instance_from_tal_and_ta_der_with_strict_name( discover_root_ca_instance_from_tal_and_ta_der_with_strict_name(
&tal_bytes, &ta_der, None, &tal_bytes,
&ta_der,
Some(&resolved_ta_uri),
) )
} else { } else {
discover_root_ca_instance_from_tal_and_ta_der(&tal_bytes, &ta_der, None) discover_root_ca_instance_from_tal_and_ta_der(
&tal_bytes,
&ta_der,
Some(&resolved_ta_uri),
)
} }
} }
} }

View File

@ -6,8 +6,8 @@ use rpki::ccr::{
encode_content_info, encode_content_info,
}; };
use rpki::cir::{ use rpki::cir::{
CIR_VERSION_V2, CanonicalInputRepresentation, CirHashAlgorithm, CirObject, CirTal, CIR_VERSION_V3, CanonicalInputRepresentation, CirHashAlgorithm, CirObject, CirTrustAnchor,
compute_reject_list_sha256, encode_cir, compute_reject_list_sha256, encode_cir, sha256,
}; };
fn skip_heavy_blackbox_test() -> bool { fn skip_heavy_blackbox_test() -> bool {
@ -42,9 +42,10 @@ fn cir_full_and_delta_pair_reuses_shared_repo_bytes_db() {
use sha2::{Digest, Sha256}; use sha2::{Digest, Sha256};
hex::encode(Sha256::digest(b"delta-object")) hex::encode(Sha256::digest(b"delta-object"))
}; };
let trust_anchors = vec![test_trust_anchor()];
let full_cir = CanonicalInputRepresentation { let full_cir = CanonicalInputRepresentation {
version: CIR_VERSION_V2, version: CIR_VERSION_V3,
hash_alg: CirHashAlgorithm::Sha256, hash_alg: CirHashAlgorithm::Sha256,
validation_time: time::OffsetDateTime::parse( validation_time: time::OffsetDateTime::parse(
"2026-03-16T11:49:15Z", "2026-03-16T11:49:15Z",
@ -55,15 +56,12 @@ fn cir_full_and_delta_pair_reuses_shared_repo_bytes_db() {
rsync_uri: "rsync://example.net/repo/full.roa".to_string(), rsync_uri: "rsync://example.net/repo/full.roa".to_string(),
sha256: hex::decode(&full_obj_hash).unwrap(), sha256: hex::decode(&full_obj_hash).unwrap(),
}], }],
tals: vec![CirTal { trust_anchors: trust_anchors.clone(),
tal_uri: "https://rpki.apnic.net/tal/apnic-rfc7730-https.tal".to_string(),
tal_bytes: b"rsync://example.net/repo/root.cer\nMIIB".to_vec(),
}],
reject_list_sha256: compute_reject_list_sha256(std::iter::empty::<&str>()), reject_list_sha256: compute_reject_list_sha256(std::iter::empty::<&str>()),
rejected_objects: Vec::new(), rejected_objects: Vec::new(),
}; };
let delta_cir = CanonicalInputRepresentation { let delta_cir = CanonicalInputRepresentation {
version: CIR_VERSION_V2, version: CIR_VERSION_V3,
hash_alg: CirHashAlgorithm::Sha256, hash_alg: CirHashAlgorithm::Sha256,
validation_time: time::OffsetDateTime::parse( validation_time: time::OffsetDateTime::parse(
"2026-03-16T11:50:15Z", "2026-03-16T11:50:15Z",
@ -84,7 +82,7 @@ fn cir_full_and_delta_pair_reuses_shared_repo_bytes_db() {
objects.sort_by(|a, b| a.rsync_uri.cmp(&b.rsync_uri)); objects.sort_by(|a, b| a.rsync_uri.cmp(&b.rsync_uri));
objects objects
}, },
tals: full_cir.tals.clone(), trust_anchors,
reject_list_sha256: compute_reject_list_sha256(std::iter::empty::<&str>()), reject_list_sha256: compute_reject_list_sha256(std::iter::empty::<&str>()),
rejected_objects: Vec::new(), rejected_objects: Vec::new(),
}; };
@ -226,3 +224,15 @@ fi
assert!(out.join("full").join("result.ccr").is_file()); assert!(out.join("full").join("result.ccr").is_file());
assert!(out.join("delta-001").join("result.ccr").is_file()); assert!(out.join("delta-001").join("result.ccr").is_file());
} }
fn test_trust_anchor() -> CirTrustAnchor {
let ta_rsync_uri = "rsync://example.net/repo/root.cer";
let ta_certificate_der = b"ta-der".to_vec();
CirTrustAnchor {
ta_rsync_uri: ta_rsync_uri.to_string(),
tal_uri: "https://rpki.apnic.net/tal/apnic-rfc7730-https.tal".to_string(),
tal_bytes: format!("{ta_rsync_uri}\n\nAQID\n").into_bytes(),
ta_certificate_sha256: sha256(&ta_certificate_der),
ta_certificate_der,
}
}

View File

@ -7,8 +7,8 @@ use rpki::ccr::{
encode_content_info, encode_content_info,
}; };
use rpki::cir::{ use rpki::cir::{
CIR_VERSION_V2, CanonicalInputRepresentation, CirHashAlgorithm, CirObject, CirTal, CIR_VERSION_V3, CanonicalInputRepresentation, CirHashAlgorithm, CirObject, CirTrustAnchor,
compute_reject_list_sha256, encode_cir, compute_reject_list_sha256, encode_cir, sha256,
}; };
#[test] #[test]
@ -34,7 +34,7 @@ fn cir_drop_report_counts_dropped_roa_objects_and_vrps() {
.expect("write repo bytes"); .expect("write repo bytes");
let cir = CanonicalInputRepresentation { let cir = CanonicalInputRepresentation {
version: CIR_VERSION_V2, version: CIR_VERSION_V3,
hash_alg: CirHashAlgorithm::Sha256, hash_alg: CirHashAlgorithm::Sha256,
validation_time: time::OffsetDateTime::parse( validation_time: time::OffsetDateTime::parse(
"2026-04-09T00:00:00Z", "2026-04-09T00:00:00Z",
@ -45,10 +45,7 @@ fn cir_drop_report_counts_dropped_roa_objects_and_vrps() {
rsync_uri: "rsync://example.net/repo/AS4538.roa".to_string(), rsync_uri: "rsync://example.net/repo/AS4538.roa".to_string(),
sha256: hex::decode(&hash).unwrap(), sha256: hex::decode(&hash).unwrap(),
}], }],
tals: vec![CirTal { trust_anchors: vec![test_trust_anchor()],
tal_uri: "https://example.test/root.tal".to_string(),
tal_bytes: b"rsync://example.net/repo/root.cer\nMIIB".to_vec(),
}],
reject_list_sha256: compute_reject_list_sha256(std::iter::empty::<&str>()), reject_list_sha256: compute_reject_list_sha256(std::iter::empty::<&str>()),
rejected_objects: Vec::new(), rejected_objects: Vec::new(),
}; };
@ -124,3 +121,15 @@ fn cir_drop_report_counts_dropped_roa_objects_and_vrps() {
.contains("Dropped By Reason") .contains("Dropped By Reason")
); );
} }
fn test_trust_anchor() -> CirTrustAnchor {
let ta_rsync_uri = "rsync://example.net/repo/root.cer";
let ta_certificate_der = b"ta-der".to_vec();
CirTrustAnchor {
ta_rsync_uri: ta_rsync_uri.to_string(),
tal_uri: "https://example.test/root.tal".to_string(),
tal_bytes: format!("{ta_rsync_uri}\n\nAQID\n").into_bytes(),
ta_certificate_sha256: sha256(&ta_certificate_der),
ta_certificate_der,
}
}

View File

@ -3,8 +3,8 @@ use std::process::Command;
use rpki::blob_store::ExternalRepoBytesDb; use rpki::blob_store::ExternalRepoBytesDb;
use rpki::cir::{ use rpki::cir::{
CIR_VERSION_V2, CanonicalInputRepresentation, CirHashAlgorithm, CirObject, CirTal, CIR_VERSION_V3, CanonicalInputRepresentation, CirHashAlgorithm, CirTrustAnchor,
compute_reject_list_sha256, encode_cir, materialize_cir_from_repo_bytes, compute_reject_list_sha256, encode_cir, materialize_cir_from_repo_bytes, sha256,
}; };
fn skip_heavy_script_replay_test() -> bool { fn skip_heavy_script_replay_test() -> bool {
@ -30,26 +30,22 @@ fn build_ta_only_cir() -> (CanonicalInputRepresentation, Vec<u8>) {
.expect("tal has rsync uri") .expect("tal has rsync uri")
.as_str() .as_str()
.to_string(); .to_string();
let ta_hash = {
use sha2::{Digest, Sha256};
Sha256::digest(&ta_bytes).to_vec()
};
( (
CanonicalInputRepresentation { CanonicalInputRepresentation {
version: CIR_VERSION_V2, version: CIR_VERSION_V3,
hash_alg: CirHashAlgorithm::Sha256, hash_alg: CirHashAlgorithm::Sha256,
validation_time: time::OffsetDateTime::parse( validation_time: time::OffsetDateTime::parse(
"2026-04-07T00:00:00Z", "2026-04-07T00:00:00Z",
&time::format_description::well_known::Rfc3339, &time::format_description::well_known::Rfc3339,
) )
.unwrap(), .unwrap(),
objects: vec![CirObject { objects: Vec::new(),
rsync_uri: ta_rsync_uri, trust_anchors: vec![CirTrustAnchor {
sha256: ta_hash, ta_rsync_uri,
}],
tals: vec![CirTal {
tal_uri: "https://example.test/root.tal".to_string(), tal_uri: "https://example.test/root.tal".to_string(),
tal_bytes, tal_bytes,
ta_certificate_sha256: sha256(&ta_bytes),
ta_certificate_der: ta_bytes.clone(),
}], }],
reject_list_sha256: compute_reject_list_sha256(std::iter::empty::<&str>()), reject_list_sha256: compute_reject_list_sha256(std::iter::empty::<&str>()),
rejected_objects: Vec::new(), rejected_objects: Vec::new(),

View File

@ -3,8 +3,8 @@ use std::process::Command;
use rpki::blob_store::ExternalRepoBytesDb; use rpki::blob_store::ExternalRepoBytesDb;
use rpki::cir::{ use rpki::cir::{
CIR_VERSION_V2, CanonicalInputRepresentation, CirHashAlgorithm, CirObject, CirTal, CIR_VERSION_V3, CanonicalInputRepresentation, CirHashAlgorithm, CirTrustAnchor,
compute_reject_list_sha256, encode_cir, materialize_cir_from_repo_bytes, compute_reject_list_sha256, encode_cir, materialize_cir_from_repo_bytes, sha256,
}; };
fn skip_heavy_script_replay_test() -> bool { fn skip_heavy_script_replay_test() -> bool {
@ -30,26 +30,22 @@ fn build_ta_only_cir() -> (CanonicalInputRepresentation, Vec<u8>) {
.expect("tal has rsync uri") .expect("tal has rsync uri")
.as_str() .as_str()
.to_string(); .to_string();
let ta_hash = {
use sha2::{Digest, Sha256};
Sha256::digest(&ta_bytes).to_vec()
};
( (
CanonicalInputRepresentation { CanonicalInputRepresentation {
version: CIR_VERSION_V2, version: CIR_VERSION_V3,
hash_alg: CirHashAlgorithm::Sha256, hash_alg: CirHashAlgorithm::Sha256,
validation_time: time::OffsetDateTime::parse( validation_time: time::OffsetDateTime::parse(
"2026-04-07T00:00:00Z", "2026-04-07T00:00:00Z",
&time::format_description::well_known::Rfc3339, &time::format_description::well_known::Rfc3339,
) )
.unwrap(), .unwrap(),
objects: vec![CirObject { objects: Vec::new(),
rsync_uri: ta_rsync_uri, trust_anchors: vec![CirTrustAnchor {
sha256: ta_hash, ta_rsync_uri,
}],
tals: vec![CirTal {
tal_uri: "https://example.test/root.tal".to_string(), tal_uri: "https://example.test/root.tal".to_string(),
tal_bytes, tal_bytes,
ta_certificate_sha256: sha256(&ta_bytes),
ta_certificate_der: ta_bytes.clone(),
}], }],
reject_list_sha256: compute_reject_list_sha256(std::iter::empty::<&str>()), reject_list_sha256: compute_reject_list_sha256(std::iter::empty::<&str>()),
rejected_objects: Vec::new(), rejected_objects: Vec::new(),

View File

@ -6,8 +6,8 @@ use rpki::ccr::{
encode_content_info, encode_content_info,
}; };
use rpki::cir::{ use rpki::cir::{
CIR_VERSION_V2, CanonicalInputRepresentation, CirHashAlgorithm, CirObject, CirTal, CIR_VERSION_V3, CanonicalInputRepresentation, CirHashAlgorithm, CirObject, CirTrustAnchor,
compute_reject_list_sha256, encode_cir, compute_reject_list_sha256, encode_cir, sha256,
}; };
fn skip_heavy_blackbox_test() -> bool { fn skip_heavy_blackbox_test() -> bool {
@ -36,7 +36,7 @@ fn cir_offline_sequence_writes_parseable_sequence_json_and_steps() {
.unwrap(); .unwrap();
let mk_cir = |uri: &str, hash_hex: &str, vt: &str| CanonicalInputRepresentation { let mk_cir = |uri: &str, hash_hex: &str, vt: &str| CanonicalInputRepresentation {
version: CIR_VERSION_V2, version: CIR_VERSION_V3,
hash_alg: CirHashAlgorithm::Sha256, hash_alg: CirHashAlgorithm::Sha256,
validation_time: time::OffsetDateTime::parse( validation_time: time::OffsetDateTime::parse(
vt, vt,
@ -47,10 +47,7 @@ fn cir_offline_sequence_writes_parseable_sequence_json_and_steps() {
rsync_uri: uri.to_string(), rsync_uri: uri.to_string(),
sha256: hex::decode(hash_hex).unwrap(), sha256: hex::decode(hash_hex).unwrap(),
}], }],
tals: vec![CirTal { trust_anchors: vec![test_trust_anchor()],
tal_uri: "https://rpki.apnic.net/tal/apnic-rfc7730-https.tal".to_string(),
tal_bytes: b"rsync://example.net/repo/root.cer\nMIIB".to_vec(),
}],
reject_list_sha256: compute_reject_list_sha256(std::iter::empty::<&str>()), reject_list_sha256: compute_reject_list_sha256(std::iter::empty::<&str>()),
rejected_objects: Vec::new(), rejected_objects: Vec::new(),
}; };
@ -68,7 +65,7 @@ fn cir_offline_sequence_writes_parseable_sequence_json_and_steps() {
"2026-03-16T11:49:15Z", "2026-03-16T11:49:15Z",
); );
let delta_cir = CanonicalInputRepresentation { let delta_cir = CanonicalInputRepresentation {
version: CIR_VERSION_V2, version: CIR_VERSION_V3,
hash_alg: CirHashAlgorithm::Sha256, hash_alg: CirHashAlgorithm::Sha256,
validation_time: time::OffsetDateTime::parse( validation_time: time::OffsetDateTime::parse(
"2026-03-16T11:50:15Z", "2026-03-16T11:50:15Z",
@ -86,7 +83,7 @@ fn cir_offline_sequence_writes_parseable_sequence_json_and_steps() {
objects.sort_by(|a, b| a.rsync_uri.cmp(&b.rsync_uri)); objects.sort_by(|a, b| a.rsync_uri.cmp(&b.rsync_uri));
objects objects
}, },
tals: full_cir.tals.clone(), trust_anchors: full_cir.trust_anchors.clone(),
reject_list_sha256: compute_reject_list_sha256(std::iter::empty::<&str>()), reject_list_sha256: compute_reject_list_sha256(std::iter::empty::<&str>()),
rejected_objects: Vec::new(), rejected_objects: Vec::new(),
}; };
@ -244,3 +241,15 @@ fi
assert!(out.join(rel).is_file(), "missing {}", rel); assert!(out.join(rel).is_file(), "missing {}", rel);
} }
} }
fn test_trust_anchor() -> CirTrustAnchor {
let ta_rsync_uri = "rsync://example.net/repo/root.cer";
let ta_certificate_der = b"ta-der".to_vec();
CirTrustAnchor {
ta_rsync_uri: ta_rsync_uri.to_string(),
tal_uri: "https://rpki.apnic.net/tal/apnic-rfc7730-https.tal".to_string(),
tal_bytes: format!("{ta_rsync_uri}\n\nAQID\n").into_bytes(),
ta_certificate_sha256: sha256(&ta_certificate_der),
ta_certificate_der,
}
}

View File

@ -3,8 +3,8 @@ use std::process::Command;
use rpki::blob_store::ExternalRepoBytesDb; use rpki::blob_store::ExternalRepoBytesDb;
use rpki::cir::{ use rpki::cir::{
CIR_VERSION_V2, CanonicalInputRepresentation, CirHashAlgorithm, CirObject, CirTal, CIR_VERSION_V3, CanonicalInputRepresentation, CirHashAlgorithm, CirTrustAnchor,
compute_reject_list_sha256, encode_cir, materialize_cir_from_repo_bytes, compute_reject_list_sha256, encode_cir, materialize_cir_from_repo_bytes, sha256,
}; };
fn skip_heavy_script_replay_test() -> bool { fn skip_heavy_script_replay_test() -> bool {
@ -30,26 +30,22 @@ fn build_ta_only_cir() -> (CanonicalInputRepresentation, Vec<u8>) {
.expect("tal has rsync uri") .expect("tal has rsync uri")
.as_str() .as_str()
.to_string(); .to_string();
let ta_hash = {
use sha2::{Digest, Sha256};
Sha256::digest(&ta_bytes).to_vec()
};
( (
CanonicalInputRepresentation { CanonicalInputRepresentation {
version: CIR_VERSION_V2, version: CIR_VERSION_V3,
hash_alg: CirHashAlgorithm::Sha256, hash_alg: CirHashAlgorithm::Sha256,
validation_time: time::OffsetDateTime::parse( validation_time: time::OffsetDateTime::parse(
"2026-04-07T00:00:00Z", "2026-04-07T00:00:00Z",
&time::format_description::well_known::Rfc3339, &time::format_description::well_known::Rfc3339,
) )
.unwrap(), .unwrap(),
objects: vec![CirObject { objects: Vec::new(),
rsync_uri: ta_rsync_uri, trust_anchors: vec![CirTrustAnchor {
sha256: ta_hash, ta_rsync_uri,
}],
tals: vec![CirTal {
tal_uri: "https://example.test/root.tal".to_string(), tal_uri: "https://example.test/root.tal".to_string(),
tal_bytes, tal_bytes,
ta_certificate_sha256: sha256(&ta_bytes),
ta_certificate_der: ta_bytes.clone(),
}], }],
reject_list_sha256: compute_reject_list_sha256(std::iter::empty::<&str>()), reject_list_sha256: compute_reject_list_sha256(std::iter::empty::<&str>()),
rejected_objects: Vec::new(), rejected_objects: Vec::new(),

View File

@ -3,8 +3,8 @@ use std::process::Command;
use rpki::blob_store::ExternalRepoBytesDb; use rpki::blob_store::ExternalRepoBytesDb;
use rpki::cir::{ use rpki::cir::{
CIR_VERSION_V2, CanonicalInputRepresentation, CirHashAlgorithm, CirObject, CirTal, CIR_VERSION_V3, CanonicalInputRepresentation, CirHashAlgorithm, CirTrustAnchor,
compute_reject_list_sha256, encode_cir, materialize_cir_from_repo_bytes, compute_reject_list_sha256, encode_cir, materialize_cir_from_repo_bytes, sha256,
}; };
fn skip_heavy_script_replay_test() -> bool { fn skip_heavy_script_replay_test() -> bool {
@ -30,26 +30,22 @@ fn build_ta_only_cir() -> (CanonicalInputRepresentation, Vec<u8>) {
.expect("tal has rsync uri") .expect("tal has rsync uri")
.as_str() .as_str()
.to_string(); .to_string();
let ta_hash = {
use sha2::{Digest, Sha256};
Sha256::digest(&ta_bytes).to_vec()
};
( (
CanonicalInputRepresentation { CanonicalInputRepresentation {
version: CIR_VERSION_V2, version: CIR_VERSION_V3,
hash_alg: CirHashAlgorithm::Sha256, hash_alg: CirHashAlgorithm::Sha256,
validation_time: time::OffsetDateTime::parse( validation_time: time::OffsetDateTime::parse(
"2026-04-07T00:00:00Z", "2026-04-07T00:00:00Z",
&time::format_description::well_known::Rfc3339, &time::format_description::well_known::Rfc3339,
) )
.unwrap(), .unwrap(),
objects: vec![CirObject { objects: Vec::new(),
rsync_uri: ta_rsync_uri, trust_anchors: vec![CirTrustAnchor {
sha256: ta_hash, ta_rsync_uri,
}],
tals: vec![CirTal {
tal_uri: "https://example.test/root.tal".to_string(), tal_uri: "https://example.test/root.tal".to_string(),
tal_bytes, tal_bytes,
ta_certificate_sha256: sha256(&ta_bytes),
ta_certificate_der: ta_bytes.clone(),
}], }],
reject_list_sha256: compute_reject_list_sha256(std::iter::empty::<&str>()), reject_list_sha256: compute_reject_list_sha256(std::iter::empty::<&str>()),
rejected_objects: Vec::new(), rejected_objects: Vec::new(),

View File

@ -127,12 +127,16 @@ fn cli_run_offline_mode_writes_cir_and_static_pool() {
let bytes = std::fs::read(&cir_path).expect("read cir"); let bytes = std::fs::read(&cir_path).expect("read cir");
let cir = rpki::cir::decode_cir(&bytes).expect("decode cir"); let cir = rpki::cir::decode_cir(&bytes).expect("decode cir");
assert_eq!(cir.tals.len(), 1); assert_eq!(cir.trust_anchors.len(), 1);
assert_eq!(cir.tals[0].tal_uri, "https://example.test/root.tal"); assert_eq!(
cir.trust_anchors[0].tal_uri,
"https://example.test/root.tal"
);
assert!(!cir.trust_anchors[0].ta_certificate_der.is_empty());
assert!( assert!(
cir.objects !cir.objects
.iter() .iter()
.any(|item| item.rsync_uri.contains("apnic-rpki-root-iana-origin.cer")) .any(|item| item.rsync_uri == cir.trust_anchors[0].ta_rsync_uri)
); );
} }