diff --git a/scripts/compare/run_perf_compare_quick_remote.sh b/scripts/compare/run_perf_compare_quick_remote.sh index 51e8b10..50df656 100755 --- a/scripts/compare/run_perf_compare_quick_remote.sh +++ b/scripts/compare/run_perf_compare_quick_remote.sh @@ -242,6 +242,30 @@ ta_file_for_rir() { 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=() CLIENT_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 chmod 0777 state/ours/work-db state/ours/raw-store.db state/ours/repo-bytes.db 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' import time @@ -333,7 +359,7 @@ PY set +e LD_LIBRARY_PATH="$REMOTE_ROOT/lib${LD_LIBRARY_PATH:+:$LD_LIBRARY_PATH}" "$REMOTE_ROOT/bin/rpki-client" \ -vv \ - -S "$REMOTE_ROOT/rpki-client-skiplist" \ + -S rpki-client-skiplist \ "${CLIENT_TAL_ARGS[@]}" \ -d cache out \ > "$REMOTE_ROOT/steps/$STEP_ID/rpki-client/run.log" 2>&1 diff --git a/scripts/periodic/compare_ccr_cir_round.sh b/scripts/periodic/compare_ccr_cir_round.sh index 49870ff..75b3889 100755 --- a/scripts/periodic/compare_ccr_cir_round.sh +++ b/scripts/periodic/compare_ccr_cir_round.sh @@ -114,8 +114,8 @@ else: diagnosis = "ccr_mismatch_cir_not_available" elif not cir_compared: diagnosis = "ccr_mismatch_cir_compare_skipped" - elif cir.get("tals", {}).get("match") is not True: - diagnosis = "tal_input_difference" + elif cir.get("trustAnchors", {}).get("match") is not True: + diagnosis = "trust_anchor_input_difference" elif cir.get("objects", {}).get("match") is not True: diagnosis = "input_object_or_manifest_accepted_set_difference" elif ( @@ -154,10 +154,13 @@ combined = { "allMatch": cir.get("allMatch") if cir else None, "objectsMatch": cir.get("objects", {}).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, "oursObjectCount": cir.get("ours", {}).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, "rpkiClientRejectCount": cir.get("rpkiClient", {}).get("rejectCount") if cir else None, }, @@ -178,8 +181,8 @@ if cir: lines.extend([ f"- `cirObjectsMatch`: `{str(combined['cir']['objectsMatch']).lower()}`", f"- `cirRejectsMatch`: `{str(combined['cir']['rejectsMatch']).lower()}`", - f"- `cirTalsMatch`: `{str(combined['cir']['talsMatch']).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"- `cirTrustAnchorsMatch`: `{str(combined['cir']['trustAnchorsMatch']).lower()}`", + 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: lines.append(f"- `cirSkippedReason`: `{'ccr matched' if ccr_match and not always_compare_cir else 'CIR inputs unavailable'}`") diff --git a/src/bin/cir_dump_reject_list.rs b/src/bin/cir_dump_reject_list.rs index 9e73a11..b02fe90 100644 --- a/src/bin/cir_dump_reject_list.rs +++ b/src/bin/cir_dump_reject_list.rs @@ -57,7 +57,7 @@ fn real_main() -> Result<(), String> { let cir = rpki::cir::decode_cir(&bytes).map_err(|e| format!("decode cir failed: {e}"))?; 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() { println!( "{:04} object={} sha256={}", diff --git a/src/bin/cir_extract_inputs.rs b/src/bin/cir_extract_inputs.rs index 0218678..74c24cc 100644 --- a/src/bin/cir_extract_inputs.rs +++ b/src/bin/cir_extract_inputs.rs @@ -49,17 +49,23 @@ fn run(argv: Vec) -> Result<(), String> { .map_err(|e| format!("read CIR failed: {}: {e}", cir_path.display()))?; let cir = rpki::cir::decode_cir(&bytes).map_err(|e| e.to_string())?; - std::fs::create_dir_all(&tals_dir) - .map_err(|e| format!("create tals dir failed: {}: {e}", tals_dir.display()))?; + std::fs::create_dir_all(&tals_dir).map_err(|e| { + format!( + "create trust_anchors dir failed: {}: {e}", + tals_dir.display() + ) + })?; 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 path = tals_dir.join(filename); std::fs::write(&path, &tal.tal_bytes) .map_err(|e| format!("write TAL failed: {}: {e}", path.display()))?; tal_files.push(serde_json::json!({ "talUri": tal.tal_uri, + "taRsyncUri": tal.ta_rsync_uri, + "taCertificateSha256": hex::encode(&tal.ta_certificate_sha256), "path": path, })); } @@ -70,6 +76,7 @@ fn run(argv: Vec) -> Result<(), String> { .map_err(|e| format!("format validationTime failed: {e}"))?; let meta = serde_json::json!({ "validationTime": validation_time, + "trustAnchorCount": cir.trust_anchors.len(), "talFiles": tal_files, }); if let Some(parent) = meta_json.parent() { diff --git a/src/bin/cir_materialize.rs b/src/bin/cir_materialize.rs index ef83148..6122f69 100644 --- a/src/bin/cir_materialize.rs +++ b/src/bin/cir_materialize.rs @@ -59,9 +59,11 @@ fn run(argv: Vec) -> Result<(), String> { match result { Ok(summary) => { eprintln!( - "materialized CIR: mirror={} objects={} linked={} copied={} keep_db={}", + "materialized CIR: mirror={} objects={} trust_anchors={} materialized_files={} linked={} copied={} keep_db={}", mirror_root.display(), summary.object_count, + summary.trust_anchor_count, + summary.materialized_file_count, summary.linked_files, summary.copied_files, keep_db diff --git a/src/bin/cir_probe_rpki_client_cache.rs b/src/bin/cir_probe_rpki_client_cache.rs index 553514b..f42a64d 100644 --- a/src/bin/cir_probe_rpki_client_cache.rs +++ b/src/bin/cir_probe_rpki_client_cache.rs @@ -443,8 +443,8 @@ fn uri_extension(uri: &str) -> String { mod tests { use super::*; use rpki::cir::{ - CIR_VERSION_V2, CanonicalInputRepresentation, CirHashAlgorithm, CirObject, - CirRejectedObject, CirTal, compute_reject_list_sha256, encode_cir, + CIR_VERSION_V3, CanonicalInputRepresentation, CirHashAlgorithm, CirObject, + CirRejectedObject, CirTrustAnchor, compute_reject_list_sha256, encode_cir, }; #[test] @@ -664,20 +664,28 @@ mod tests { .collect::>(); objects.sort_by(|left, right| left.rsync_uri.cmp(&right.rsync_uri)); CanonicalInputRepresentation { - version: CIR_VERSION_V2, + version: CIR_VERSION_V3, hash_alg: CirHashAlgorithm::Sha256, validation_time: time::OffsetDateTime::UNIX_EPOCH, objects, - tals: vec![CirTal { - 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(), - }], + trust_anchors: vec![sample_trust_anchor()], reject_list_sha256: compute_reject_list_sha256(std::iter::empty::<&str>()), rejected_objects: Vec::::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) { std::fs::write(path, encode_cir(cir).expect("encode cir")).expect("write cir"); } diff --git a/src/bin/cir_state_compare.rs b/src/bin/cir_state_compare.rs index 11c6be0..0e11225 100644 --- a/src/bin/cir_state_compare.rs +++ b/src/bin/cir_state_compare.rs @@ -114,40 +114,56 @@ fn run(args: Args) -> Result<(), String> { .iter() .map(|item| item.object_uri.clone()) .collect::>(); - let ours_tals = ours - .tals + let ours_trust_anchors = ours + .trust_anchors .iter() - .map(|item| item.tal_uri.clone()) - .collect::>(); - let peer_tals = peer - .tals + .map(|item| { + ( + item.ta_rsync_uri.clone(), + hex::encode(&item.ta_certificate_sha256), + ) + }) + .collect::>(); + let peer_trust_anchors = peer + .trust_anchors .iter() - .map(|item| item.tal_uri.clone()) - .collect::>(); + .map(|item| { + ( + item.ta_rsync_uri.clone(), + hex::encode(&item.ta_certificate_sha256), + ) + }) + .collect::>(); 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 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 all_match = - object_summary.match_ && reject_summary.match_ && tal_summary.match_ && reject_hash_match; + let all_match = object_summary.match_ + && reject_summary.match_ + && trust_anchor_summary.match_ + && reject_hash_match; let summary = json!({ "allMatch": all_match, "objects": object_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, "ours": { "objectCount": ours.objects.len(), - "talCount": ours.tals.len(), + "talCount": ours.trust_anchors.len(), + "trustAnchorCount": ours.trust_anchors.len(), "rejectCount": ours.rejected_objects.len(), "rejectListSha256": hex::encode(&ours.reject_list_sha256), "validationTime": ours.validation_time.to_string(), }, "rpkiClient": { "objectCount": peer.objects.len(), - "talCount": peer.tals.len(), + "talCount": peer.trust_anchors.len(), + "trustAnchorCount": peer.trust_anchors.len(), "rejectCount": peer.rejected_objects.len(), "rejectListSha256": hex::encode(&peer.reject_list_sha256), "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) ), format!( - "- `talsMatch`: `{}`", - summary["tals"]["match"].as_bool().unwrap_or(false) + "- `trustAnchorsMatch`: `{}`", + summary["trustAnchors"]["match"].as_bool().unwrap_or(false) ), format!( "- `rejectListSha256Match`: `{}`", summary["rejectListSha256Match"].as_bool().unwrap_or(false) ), format!( - "- `ours`: objects `{}`, tals `{}`, rejects `{}`", + "- `ours`: objects `{}`, trustAnchors `{}`, rejects `{}`", 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) ), format!( - "- `rpkiClient`: objects `{}`, tals `{}`, rejects `{}`", + "- `rpkiClient`: objects `{}`, trustAnchors `{}`, rejects `{}`", 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) ), ]; @@ -410,8 +428,8 @@ fn uri_extension(uri: &str) -> String { mod tests { use super::*; use rpki::cir::{ - CIR_VERSION_V2, CanonicalInputRepresentation, CirHashAlgorithm, CirObject, - CirRejectedObject, CirTal, compute_reject_list_sha256, encode_cir, + CIR_VERSION_V3, CanonicalInputRepresentation, CirHashAlgorithm, CirObject, + CirRejectedObject, CirTrustAnchor, compute_reject_list_sha256, encode_cir, sha256, }; #[test] @@ -513,7 +531,7 @@ mod tests { assert_eq!(summary["allMatch"], true); assert_eq!(summary["objects"]["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["ours"]["objectCount"], 2); assert!( @@ -524,7 +542,7 @@ mod tests { } #[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 ours_cir = sample_cir( &[ @@ -581,7 +599,7 @@ mod tests { 1 ); assert_eq!(summary["rejects"]["match"], false); - assert_eq!(summary["tals"]["match"], false); + assert_eq!(summary["trustAnchors"]["match"], false); assert_eq!(summary["rejectListSha256Match"], false); } @@ -700,7 +718,7 @@ mod tests { fn sample_cir( objects: &[(&str, u8)], - tals: &[&str], + trust_anchors: &[&str], rejected_objects: &[(&str, Option<&str>)], ) -> CanonicalInputRepresentation { let rejected_objects = rejected_objects @@ -711,7 +729,7 @@ mod tests { }) .collect::>(); CanonicalInputRepresentation { - version: CIR_VERSION_V2, + version: CIR_VERSION_V3, hash_alg: CirHashAlgorithm::Sha256, validation_time: sample_time(), objects: objects @@ -721,12 +739,23 @@ mod tests { sha256: vec![*fill; 32], }) .collect(), - tals: tals + trust_anchors: trust_anchors .iter() - .map(|tal_uri| CirTal { - tal_uri: (*tal_uri).to_string(), - tal_bytes: format!("{tal_uri}\nrsync://example.net/repo/ta.cer\nMIIB") - .into_bytes(), + .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_bytes: format!("{ta_rsync_uri}\n\nAQID\n").into_bytes(), + ta_certificate_sha256: sha256(&ta_certificate_der), + ta_certificate_der, + } }) .collect(), reject_list_sha256: compute_reject_list_sha256( diff --git a/src/bin/cir_ta_only_fixture.rs b/src/bin/cir_ta_only_fixture.rs index c51d5ca..322facd 100644 --- a/src/bin/cir_ta_only_fixture.rs +++ b/src/bin/cir_ta_only_fixture.rs @@ -1,11 +1,9 @@ use std::path::PathBuf; -use rpki::blob_store::ExternalRepoBytesDb; use rpki::cir::{ - CIR_VERSION_V2, CanonicalInputRepresentation, CirHashAlgorithm, CirObject, CirTal, - compute_reject_list_sha256, encode_cir, + CIR_VERSION_V3, CanonicalInputRepresentation, CirHashAlgorithm, CirTrustAnchor, + compute_reject_list_sha256, encode_cir, sha256, }; -use sha2::Digest; const USAGE: &str = "Usage: cir_ta_only_fixture --tal-path --ta-path --tal-uri --validation-time --cir-out --repo-bytes-db "; @@ -88,6 +86,7 @@ fn parse_args( fn main() -> Result<(), String> { let argv: Vec = std::env::args().collect(); 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 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() .to_string(); - let sha = sha2::Sha256::digest(&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 ta_certificate_sha256 = sha256(&ta_bytes); let cir = CanonicalInputRepresentation { - version: CIR_VERSION_V2, + version: CIR_VERSION_V3, hash_alg: CirHashAlgorithm::Sha256, validation_time, - objects: vec![CirObject { - rsync_uri: ta_rsync_uri, - sha256: sha.to_vec(), + objects: Vec::new(), + trust_anchors: vec![CirTrustAnchor { + 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>()), rejected_objects: Vec::new(), }; diff --git a/src/cir/decode.rs b/src/cir/decode.rs index 47bd5d2..cc27001 100644 --- a/src/cir/decode.rs +++ b/src/cir/decode.rs @@ -1,6 +1,6 @@ use crate::cir::model::{ - CIR_VERSION_V2, CanonicalInputRepresentation, CirHashAlgorithm, CirObject, CirRejectedObject, - CirTal, + CIR_VERSION_V3, CanonicalInputRepresentation, CirHashAlgorithm, CirObject, CirRejectedObject, + CirTrustAnchor, }; use crate::data_model::common::DerReader; use crate::data_model::oid::{OID_SHA256, OID_SHA256_RAW}; @@ -32,9 +32,9 @@ pub fn decode_cir(der: &[u8]) -> Result Result Result Result { Ok(CirObject { rsync_uri, sha256 }) } -fn decode_tal(der: &[u8]) -> Result { +fn decode_trust_anchor(der: &[u8]) -> Result { let mut top = DerReader::new(der); let mut seq = top.take_sequence().map_err(CirDecodeError::Parse)?; 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)?) .map_err(|e| CirDecodeError::Parse(e.to_string()))? .to_string(); @@ -136,10 +143,26 @@ fn decode_tal(der: &[u8]) -> Result { .take_octet_string() .map_err(CirDecodeError::Parse)? .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() { - 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 { diff --git a/src/cir/encode.rs b/src/cir/encode.rs index 5fefd46..6208d94 100644 --- a/src/cir/encode.rs +++ b/src/cir/encode.rs @@ -1,6 +1,6 @@ use crate::cir::model::{ - CIR_VERSION_V2, CanonicalInputRepresentation, CirHashAlgorithm, CirObject, CirRejectedObject, - CirTal, + CIR_VERSION_V3, CanonicalInputRepresentation, CirHashAlgorithm, CirObject, CirRejectedObject, + CirTrustAnchor, }; use crate::data_model::oid::OID_SHA256_RAW; @@ -25,9 +25,9 @@ pub fn encode_cir(cir: &CanonicalInputRepresentation) -> Result, CirEnco .collect::, _>>()?, ), encode_sequence( - &cir.tals + &cir.trust_anchors .iter() - .map(encode_tal) + .map(encode_trust_anchor) .collect::, _>>()?, ), encode_octet_string(&cir.reject_list_sha256), @@ -48,11 +48,14 @@ fn encode_object(object: &CirObject) -> Result, CirEncodeError> { ])) } -fn encode_tal(tal: &CirTal) -> Result, CirEncodeError> { - tal.validate().map_err(CirEncodeError::Validate)?; +fn encode_trust_anchor(trust_anchor: &CirTrustAnchor) -> Result, CirEncodeError> { + trust_anchor.validate().map_err(CirEncodeError::Validate)?; Ok(encode_sequence(&[ - encode_ia5_string(tal.tal_uri.as_bytes()), - encode_octet_string(&tal.tal_bytes), + encode_ia5_string(trust_anchor.ta_rsync_uri.as_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) { #[allow(dead_code)] const _: () = { - let _ = CIR_VERSION_V2; + let _ = CIR_VERSION_V3; }; diff --git a/src/cir/export.rs b/src/cir/export.rs index d85017b..a077501 100644 --- a/src/cir/export.rs +++ b/src/cir/export.rs @@ -5,12 +5,11 @@ use std::path::Path; use crate::audit::{AuditObjectResult, PublicationPointAudit}; use crate::cir::encode::{CirEncodeError, encode_cir}; use crate::cir::model::{ - CIR_VERSION_V2, CanonicalInputRepresentation, CirHashAlgorithm, CirObject, CirRejectedObject, - CirTal, compute_reject_list_sha256, + CIR_VERSION_V3, CanonicalInputRepresentation, CirHashAlgorithm, CirObject, CirRejectedObject, + CirTrustAnchor, compute_reject_list_sha256, }; use crate::cir::static_pool::{ CirStaticPoolError, CirStaticPoolExportSummary, export_hashes_from_store, - write_bytes_to_static_pool, }; use crate::current_repo_index::CurrentRepoObject; use crate::data_model::ta::TrustAnchor; @@ -49,9 +48,6 @@ pub enum CirExportError { #[error("write CIR file failed: {0}: {1}")] Write(String, String), - - #[error("write CIR trust anchor bytes to repo store failed: {0}")] - WriteRepoBytes(String), } #[derive(Clone, Debug, PartialEq, Eq)] @@ -64,12 +60,12 @@ pub struct CirRawStoreExportSummary { #[derive(Clone, Debug, PartialEq, Eq)] pub struct CirExportSummary { pub object_count: usize, - pub tal_count: usize, + pub trust_anchor_count: usize, pub timing: CirExportTiming, } #[derive(Clone, Copy, Debug)] -pub struct CirTalBinding<'a> { +pub struct CirTrustAnchorBinding<'a> { pub trust_anchor: &'a TrustAnchor, pub tal_uri: &'a str, } @@ -118,6 +114,22 @@ fn collect_cir_objects_from_validation_audit( Ok(objects) } +fn canonical_ta_rsync_uri(trust_anchor: &TrustAnchor) -> Result { + 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( store: &RocksStore, trust_anchor: &TrustAnchor, @@ -127,7 +139,7 @@ pub fn build_cir_from_run( ) -> Result { build_cir_from_run_multi( store, - &[CirTalBinding { + &[CirTrustAnchorBinding { trust_anchor, tal_uri, }], @@ -139,7 +151,7 @@ pub fn build_cir_from_run( pub fn build_cir_from_run_multi( _store: &RocksStore, - tal_bindings: &[CirTalBinding<'_>], + tal_bindings: &[CirTrustAnchorBinding<'_>], validation_time: time::OffsetDateTime, publication_points: &[PublicationPointAudit], _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 { - let ta_hash = ta_sha256_hex(&binding.trust_anchor.ta_certificate.raw_der); - let mut saw_rsync_uri = false; - for uri in &binding.trust_anchor.tal.ta_uris { - if uri.scheme() == "rsync" { - 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 { + let ta_rsync_uri = canonical_ta_rsync_uri(binding.trust_anchor)?; + let ta_certificate_der = binding.trust_anchor.ta_certificate.raw_der.clone(); + trust_anchors.push(CirTrustAnchor { + ta_rsync_uri, tal_uri: binding.tal_uri.to_string(), 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 { - version: CIR_VERSION_V2, + version: CIR_VERSION_V3, hash_alg: CirHashAlgorithm::Sha256, validation_time: validation_time.to_offset(time::UtcOffset::UTC), objects: objects @@ -183,7 +189,7 @@ pub fn build_cir_from_run_multi( sha256: hex::decode(sha256_hex).expect("validated hex"), }) .collect(), - tals, + trust_anchors, reject_list_sha256: Vec::new(), rejected_objects: Vec::new(), }; @@ -238,36 +244,13 @@ pub fn export_cir_static_pool( cir: &CanonicalInputRepresentation, trust_anchors: &[&TrustAnchor], ) -> Result { - let ta_hashes = trust_anchors - .iter() - .map(|ta| ta_sha256_hex(&ta.ta_certificate.raw_der)) - .collect::>(); + let _ = trust_anchors; let hashes = cir .objects .iter() .map(|item| hex::encode(&item.sha256)) - .filter(|hash| !ta_hashes.contains(hash)) .collect::>(); - let mut summary = export_hashes_from_store(store, static_root, capture_date_utc, &hashes)?; - - let mut unique = hashes.iter().cloned().collect::>(); - 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) + export_hashes_from_store(store, static_root, capture_date_utc, &hashes).map_err(Into::into) } pub fn export_cir_raw_store( @@ -276,17 +259,14 @@ pub fn export_cir_raw_store( cir: &CanonicalInputRepresentation, trust_anchors: &[&TrustAnchor], ) -> Result { - let ta_by_hash = trust_anchors - .iter() - .map(|ta| (ta_sha256_hex(&ta.ta_certificate.raw_der), *ta)) - .collect::>(); + let _ = trust_anchors; let unique: BTreeSet = cir .objects .iter() .map(|item| hex::encode(&item.sha256)) .collect(); - let mut written_entries = 0usize; + let written_entries = 0usize; let mut reused_entries = 0usize; for sha256_hex in &unique { if store @@ -299,23 +279,6 @@ pub fn export_cir_raw_store( reused_entries += 1; 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( raw_store_path.display().to_string(), format!("raw store missing object for sha256={sha256_hex}"), @@ -340,7 +303,7 @@ pub fn export_cir_from_run( ) -> Result { export_cir_from_run_multi( store, - &[CirTalBinding { + &[CirTrustAnchorBinding { trust_anchor, tal_uri, }], @@ -354,7 +317,7 @@ pub fn export_cir_from_run( pub fn export_cir_from_run_multi( store: &RocksStore, - tal_bindings: &[CirTalBinding<'_>], + tal_bindings: &[CirTrustAnchorBinding<'_>], validation_time: time::OffsetDateTime, publication_points: &[PublicationPointAudit], 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 ta_blobs = tal_bindings - .iter() - .map(|binding| { - ( - ta_sha256_hex(&binding.trust_anchor.ta_certificate.raw_der), - binding.trust_anchor.ta_certificate.raw_der.clone(), - ) - }) - .collect::>(); - store - .put_blob_bytes_batch(&ta_blobs) - .map_err(|e| CirExportError::WriteRepoBytes(e.to_string()))?; + let _ = store; let started = std::time::Instant::now(); write_cir_file(cir_out, &cir)?; @@ -393,7 +345,7 @@ pub fn export_cir_from_run_multi( Ok(CirExportSummary { object_count: cir.objects.len(), - tal_count: cir.tals.len(), + trust_anchor_count: cir.trust_anchors.len(), timing: CirExportTiming { build_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)] mod tests { use super::*; @@ -497,19 +444,23 @@ mod tests { &publication_points, ) .expect("build cir"); - assert_eq!(cir.version, CIR_VERSION_V2); - assert_eq!(cir.tals.len(), 1); - assert_eq!(cir.tals[0].tal_uri, "https://example.test/root.tal"); + assert_eq!(cir.version, CIR_VERSION_V3); + assert_eq!(cir.trust_anchors.len(), 1); + assert_eq!( + cir.trust_anchors[0].tal_uri, + "https://example.test/root.tal" + ); assert!( cir.objects .iter() .any(|item| item.rsync_uri == "rsync://example.test/repo/a.cer") ); assert!( - cir.objects + !cir.objects .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] @@ -549,13 +500,16 @@ mod tests { sample_date(), ) .expect("export cir"); - assert_eq!(summary.tal_count, 1); - assert!(summary.object_count >= 2); + assert_eq!(summary.trust_anchor_count, 1); + assert_eq!(summary.object_count, 1); assert!(summary.timing.total_ms >= summary.timing.build_cir_ms); let der = std::fs::read(&cir_path).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] @@ -595,13 +549,13 @@ mod tests { sample_date(), ) .expect("export cir"); - assert!(summary.object_count >= 2); + assert_eq!(summary.object_count, 1); assert!(raw_store.exists()); assert!(cir_path.exists()); } #[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 store = RocksStore::open_with_external_repo_bytes( &td.path().join("db"), @@ -623,10 +577,7 @@ mod tests { ) .expect("export cir"); - assert_eq!( - store.get_blob_bytes(&ta_hash).unwrap(), - Some(ta.ta_certificate.raw_der.clone()) - ); + assert_eq!(store.get_blob_bytes(&ta_hash).unwrap(), None); } #[test] @@ -703,11 +654,11 @@ mod tests { let cir = build_cir_from_run_multi( &store, &[ - CirTalBinding { + CirTrustAnchorBinding { trust_anchor: &ta1, tal_uri: "https://example.test/apnic.tal", }, - CirTalBinding { + CirTrustAnchorBinding { trust_anchor: &ta2, tal_uri: "https://example.test/arin.tal", }, @@ -718,7 +669,7 @@ mod tests { ) .expect("build cir from consumed audit objects"); - assert_eq!(cir.tals.len(), 2); + assert_eq!(cir.trust_anchors.len(), 2); assert!( cir.objects .iter() @@ -730,17 +681,18 @@ mod tests { .any(|item| item.rsync_uri == "rsync://example.test/repo/superfluous.roa"), "current repo objects must not be included unless validation consumed them", ); - assert!( - cir.objects.iter().any(|item| { - item.rsync_uri.contains("apnic-rpki-root-iana-origin.cer") - || item.rsync_uri.contains("arin-rpki-ta.cer") - }), - "trust anchor rsync objects must be included", - ); + for trust_anchor in &cir.trust_anchors { + assert!( + !cir.objects + .iter() + .any(|item| item.rsync_uri == trust_anchor.ta_rsync_uri), + "trust anchor rsync objects must not be included in CIR.objects", + ); + } } #[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 store = RocksStore::open(td.path()).unwrap(); let apnic = sample_trust_anchor(); @@ -749,11 +701,11 @@ mod tests { let cir = build_cir_from_run_multi( &store, &[ - CirTalBinding { + CirTrustAnchorBinding { trust_anchor: &apnic, tal_uri: "https://rpki.apnic.net/repository/apnic-rpki-root-iana-origin.cer", }, - CirTalBinding { + CirTrustAnchorBinding { trust_anchor: &arin, tal_uri: "https://rrdp.arin.net/arin-rpki-ta.cer", }, @@ -765,13 +717,13 @@ mod tests { .expect("build cir with unsorted input bindings"); assert_eq!( - cir.tals + cir.trust_anchors .iter() - .map(|tal| tal.tal_uri.as_str()) + .map(|trust_anchor| trust_anchor.ta_rsync_uri.as_str()) .collect::>(), vec![ - "https://rpki.apnic.net/repository/apnic-rpki-root-iana-origin.cer", - "https://rrdp.arin.net/arin-rpki-ta.cer", + "rsync://rpki.apnic.net/repository/apnic-rpki-root-iana-origin.cer", + "rsync://rpki.arin.net/repository/arin-rpki-ta.cer", ] ); } @@ -810,7 +762,7 @@ mod tests { let cir = build_cir_from_run_multi( &store, - &[CirTalBinding { + &[CirTrustAnchorBinding { trust_anchor: &ta, tal_uri: "https://example.test/root.tal", }], @@ -860,7 +812,7 @@ mod tests { let cir_a = build_cir_from_run_multi( &store, - &[CirTalBinding { + &[CirTrustAnchorBinding { trust_anchor: &ta, tal_uri: "https://example.test/root.tal", }], @@ -871,7 +823,7 @@ mod tests { .expect("build cir a"); let cir_b = build_cir_from_run_multi( &store, - &[CirTalBinding { + &[CirTrustAnchorBinding { trust_anchor: &ta, tal_uri: "https://example.test/root.tal", }], @@ -895,7 +847,7 @@ mod tests { let err = build_cir_from_run_multi( &store, - &[CirTalBinding { + &[CirTrustAnchorBinding { trust_anchor: &sample_trust_anchor(), tal_uri: "file:///not-supported.tal", }], @@ -908,7 +860,7 @@ mod tests { let err = build_cir_from_run_multi( &store, - &[CirTalBinding { + &[CirTrustAnchorBinding { trust_anchor: &sample_trust_anchor_without_rsync_uri(), tal_uri: "https://example.test/root.tal", }], @@ -921,7 +873,7 @@ mod tests { } #[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 store = RocksStore::open(&td.path().join("db")).unwrap(); let static_root = td.path().join("static"); @@ -948,11 +900,11 @@ mod tests { let cir = build_cir_from_run_multi( &store, &[ - CirTalBinding { + CirTrustAnchorBinding { trust_anchor: &ta1, tal_uri: "https://example.test/apnic.tal", }, - CirTalBinding { + CirTrustAnchorBinding { trust_anchor: &ta2, tal_uri: "https://example.test/arin.tal", }, @@ -966,12 +918,20 @@ mod tests { let summary = export_cir_static_pool(&store, &static_root, sample_date(), &cir, &[&ta1, &ta2]) .expect("export static pool"); - assert!(summary.unique_hashes >= 3); - assert!(summary.written_files >= 3); + assert_eq!(summary.unique_hashes, 1); + 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] - 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 raw_store_path = td.path().join("raw-store.db"); let store = @@ -983,11 +943,11 @@ mod tests { let cir_only_tas = build_cir_from_run_multi( &store, &[ - CirTalBinding { + CirTrustAnchorBinding { trust_anchor: &ta1, tal_uri: "https://example.test/apnic.tal", }, - CirTalBinding { + CirTrustAnchorBinding { trust_anchor: &ta2, 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]) .expect("export raw store"); - assert!(summary.unique_hashes >= 2); - assert!(summary.written_entries >= 2 || summary.reused_entries >= 2); + assert_eq!(summary.unique_hashes, 0); + assert_eq!(summary.written_entries, 0); + assert_eq!(summary.reused_entries, 0); let mut cir_missing_object = cir_only_tas.clone(); cir_missing_object.objects.push(CirObject { diff --git a/src/cir/materialize.rs b/src/cir/materialize.rs index 02d4aac..9c06952 100644 --- a/src/cir/materialize.rs +++ b/src/cir/materialize.rs @@ -1,3 +1,4 @@ +use std::collections::BTreeSet; use std::fs; use std::path::{Path, PathBuf}; @@ -66,6 +67,8 @@ pub enum CirMaterializeError { #[derive(Clone, Debug, PartialEq, Eq)] pub struct CirMaterializeSummary { pub object_count: usize, + pub trust_anchor_count: usize, + pub materialized_file_count: usize, pub linked_files: usize, pub copied_files: usize, } @@ -78,16 +81,7 @@ pub fn materialize_cir( ) -> Result { cir.validate().map_err(CirMaterializeError::TreeMismatch)?; - 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(), - })?; + prepare_mirror_root(mirror_root, clean_rebuild)?; let mut linked_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 expected = cir - .objects - .iter() - .map(|item| item.rsync_uri.clone()) - .collect::>(); + let expected = expected_materialized_uris(cir); if actual != expected { return Err(CirMaterializeError::TreeMismatch(format!( "expected {} files, got {} files", @@ -141,6 +141,8 @@ pub fn materialize_cir( Ok(CirMaterializeSummary { object_count: cir.objects.len(), + trust_anchor_count: cir.trust_anchors.len(), + materialized_file_count: expected.len(), linked_files, copied_files, }) @@ -154,16 +156,7 @@ pub fn materialize_cir_from_raw_store( ) -> Result { cir.validate().map_err(CirMaterializeError::TreeMismatch)?; - 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(), - })?; + prepare_mirror_root(mirror_root, clean_rebuild)?; let raw_store = 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 { sha256_hex: sha256_hex.clone(), })?; - let relative = mirror_relative_path_for_rsync_uri(&object.rsync_uri)?; - let target = mirror_root.join(&relative); + write_bytes_to_mirror_uri( + mirror_root, + &object.rsync_uri, + &bytes, + &raw_store_db.display().to_string(), + )?; + copied_files += 1; + } - if let Some(parent) = target.parent() { - fs::create_dir_all(parent).map_err(|e| CirMaterializeError::CreateParent { - path: parent.display().to_string(), - detail: e.to_string(), - })?; - } - - if target.exists() { - fs::remove_file(&target).map_err(|e| CirMaterializeError::RemoveExistingTarget { - path: target.display().to_string(), - detail: e.to_string(), - })?; - } - - 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(), - })?; + 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 = cir - .objects - .iter() - .map(|item| item.rsync_uri.clone()) - .collect::>(); + let expected = expected_materialized_uris(cir); if actual != expected { return Err(CirMaterializeError::TreeMismatch(format!( "expected {} files, got {} files", @@ -224,6 +207,8 @@ pub fn materialize_cir_from_raw_store( Ok(CirMaterializeSummary { object_count: cir.objects.len(), + trust_anchor_count: cir.trust_anchors.len(), + materialized_file_count: expected.len(), linked_files: 0, copied_files, }) @@ -237,16 +222,7 @@ pub fn materialize_cir_from_repo_bytes( ) -> Result { cir.validate().map_err(CirMaterializeError::TreeMismatch)?; - 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(), - })?; + prepare_mirror_root(mirror_root, clean_rebuild)?; let repo_bytes = ExternalRepoBytesDb::open(repo_bytes_db).map_err(|e| { CirMaterializeError::OpenRepoBytesStore { @@ -267,37 +243,27 @@ pub fn materialize_cir_from_repo_bytes( .ok_or_else(|| CirMaterializeError::MissingRepoBytesObject { sha256_hex: sha256_hex.clone(), })?; - let relative = mirror_relative_path_for_rsync_uri(&object.rsync_uri)?; - let target = mirror_root.join(&relative); + write_bytes_to_mirror_uri( + mirror_root, + &object.rsync_uri, + &bytes, + &repo_bytes_db.display().to_string(), + )?; + copied_files += 1; + } - if let Some(parent) = target.parent() { - fs::create_dir_all(parent).map_err(|e| CirMaterializeError::CreateParent { - path: parent.display().to_string(), - detail: e.to_string(), - })?; - } - - if target.exists() { - fs::remove_file(&target).map_err(|e| CirMaterializeError::RemoveExistingTarget { - path: target.display().to_string(), - detail: e.to_string(), - })?; - } - - fs::write(&target, &bytes).map_err(|e| CirMaterializeError::Copy { - src: repo_bytes_db.display().to_string(), - dst: target.display().to_string(), - detail: e.to_string(), - })?; + 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 = cir - .objects - .iter() - .map(|item| item.rsync_uri.clone()) - .collect::>(); + let expected = expected_materialized_uris(cir); if actual != expected { return Err(CirMaterializeError::TreeMismatch(format!( "expected {} files, got {} files", @@ -308,11 +274,68 @@ pub fn materialize_cir_from_repo_bytes( 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); + + if let Some(parent) = target.parent() { + fs::create_dir_all(parent).map_err(|e| CirMaterializeError::CreateParent { + path: parent.display().to_string(), + detail: e.to_string(), + })?; + } + + if target.exists() { + fs::remove_file(&target).map_err(|e| CirMaterializeError::RemoveExistingTarget { + path: target.display().to_string(), + detail: e.to_string(), + })?; + } + + fs::write(&target, bytes).map_err(|e| CirMaterializeError::Copy { + src: src_label.to_string(), + dst: target.display().to_string(), + detail: e.to_string(), + }) +} + +fn expected_materialized_uris(cir: &CanonicalInputRepresentation) -> BTreeSet { + cir.objects + .iter() + .map(|item| item.rsync_uri.clone()) + .chain( + cir.trust_anchors + .iter() + .map(|item| item.ta_rsync_uri.clone()), + ) + .collect() +} + pub fn mirror_relative_path_for_rsync_uri(rsync_uri: &str) -> Result { let url = url::Url::parse(rsync_uri) .map_err(|_| CirMaterializeError::InvalidRsyncUri(rsync_uri.to_string()))?; @@ -375,10 +398,8 @@ pub fn resolve_static_pool_file( }) } -fn collect_materialized_uris( - mirror_root: &Path, -) -> Result, CirMaterializeError> { - let mut out = std::collections::BTreeSet::new(); +fn collect_materialized_uris(mirror_root: &Path) -> Result, CirMaterializeError> { + let mut out = BTreeSet::new(); let mut stack = vec![mirror_root.to_path_buf()]; while let Some(path) = stack.pop() { 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::cir::model::{ - CIR_VERSION_V2, CanonicalInputRepresentation, CirHashAlgorithm, CirObject, - CirRejectedObject, CirTal, compute_reject_list_sha256, + CIR_VERSION_V3, CanonicalInputRepresentation, CirHashAlgorithm, CirObject, + CirRejectedObject, CirTrustAnchor, compute_reject_list_sha256, sha256, }; use sha2::Digest; use std::path::{Path, PathBuf}; @@ -429,13 +450,25 @@ mod tests { .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 { let rejected_objects = vec![CirRejectedObject { object_uri: "rsync://example.net/repo/rejected-a.roa".to_string(), reason: Some("invalid roa".to_string()), }]; CanonicalInputRepresentation { - version: CIR_VERSION_V2, + version: CIR_VERSION_V3, hash_alg: CirHashAlgorithm::Sha256, validation_time: sample_time(), objects: vec![ @@ -454,10 +487,7 @@ mod tests { .unwrap(), }, ], - tals: vec![CirTal { - tal_uri: "https://tal.example.net/root.tal".to_string(), - tal_bytes: b"x".to_vec(), - }], + trust_anchors: vec![sample_trust_anchor()], reject_list_sha256: compute_reject_list_sha256( 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 { CanonicalInputRepresentation { - version: CIR_VERSION_V2, + version: CIR_VERSION_V3, hash_alg: CirHashAlgorithm::Sha256, validation_time: sample_time(), objects: vec![ @@ -480,10 +510,7 @@ mod tests { sha256: sha2::Sha256::digest(b).to_vec(), }, ], - tals: vec![CirTal { - tal_uri: "https://tal.example.net/root.tal".to_string(), - tal_bytes: b"x".to_vec(), - }], + trust_anchors: vec![sample_trust_anchor()], reject_list_sha256: compute_reject_list_sha256(std::iter::empty::<&str>()), rejected_objects: Vec::new(), } @@ -573,6 +600,8 @@ mod tests { let summary = materialize_cir(&sample_cir(), &static_root, &mirror_root, true).unwrap(); assert_eq!(summary.object_count, 2); + assert_eq!(summary.trust_anchor_count, 1); + assert_eq!(summary.materialized_file_count, 3); assert_eq!( std::fs::read(mirror_root.join("example.net/repo/a.cer")).unwrap(), b"a" @@ -581,6 +610,10 @@ mod tests { std::fs::read(mirror_root.join("example.net/repo/nested/b.roa")).unwrap(), 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()); } @@ -629,7 +662,7 @@ mod tests { let a = b"a".to_vec(); let b = b"b".to_vec(); let cir = CanonicalInputRepresentation { - version: CIR_VERSION_V2, + version: CIR_VERSION_V3, hash_alg: CirHashAlgorithm::Sha256, validation_time: sample_time(), objects: vec![ @@ -642,10 +675,7 @@ mod tests { sha256: sha2::Sha256::digest(&b).to_vec(), }, ], - tals: vec![CirTal { - tal_uri: "https://tal.example.net/root.tal".to_string(), - tal_bytes: b"x".to_vec(), - }], + trust_anchors: vec![sample_trust_anchor()], reject_list_sha256: compute_reject_list_sha256(std::iter::empty::<&str>()), rejected_objects: Vec::new(), }; @@ -663,8 +693,10 @@ mod tests { let summary = materialize_cir_from_raw_store(&cir, &raw_store_path, &mirror_root, true).unwrap(); 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.copied_files, 2); + assert_eq!(summary.copied_files, 3); assert_eq!( std::fs::read(mirror_root.join("example.net/repo/a.cer")).unwrap(), b"a" @@ -673,6 +705,10 @@ mod tests { std::fs::read(mirror_root.join("example.net/repo/nested/b.roa")).unwrap(), b"b" ); + assert_eq!( + std::fs::read(mirror_root.join("example.net/repo/ta.cer")).unwrap(), + b"ta-der" + ); } #[test] @@ -742,7 +778,7 @@ mod tests { let summary = 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); } @@ -752,7 +788,7 @@ mod tests { let raw_store_path = td.path().join("raw-store.db"); let mirror_root = td.path().join("mirror"); let cir = CanonicalInputRepresentation { - version: CIR_VERSION_V2, + version: CIR_VERSION_V3, hash_alg: CirHashAlgorithm::Sha256, validation_time: sample_time(), objects: vec![CirObject { @@ -762,10 +798,7 @@ mod tests { ) .unwrap(), }], - tals: vec![CirTal { - tal_uri: "https://tal.example.net/root.tal".to_string(), - tal_bytes: b"x".to_vec(), - }], + trust_anchors: vec![sample_trust_anchor()], reject_list_sha256: compute_reject_list_sha256(std::iter::empty::<&str>()), rejected_objects: Vec::new(), }; @@ -790,10 +823,15 @@ mod tests { let summary = materialize_cir_from_raw_store(&cir, &raw_store_path, &mirror_root, true).unwrap(); assert_eq!(summary.object_count, 1); + assert_eq!(summary.materialized_file_count, 2); assert_eq!( std::fs::read(mirror_root.join("example.net/repo/a.cer")).unwrap(), b"blob-a" ); + assert_eq!( + std::fs::read(mirror_root.join("example.net/repo/ta.cer")).unwrap(), + b"ta-der" + ); } #[test] @@ -804,7 +842,7 @@ mod tests { let a = b"a".to_vec(); let b = b"b".to_vec(); let cir = CanonicalInputRepresentation { - version: CIR_VERSION_V2, + version: CIR_VERSION_V3, hash_alg: CirHashAlgorithm::Sha256, validation_time: sample_time(), objects: vec![ @@ -817,10 +855,7 @@ mod tests { sha256: sha2::Sha256::digest(&b).to_vec(), }, ], - tals: vec![CirTal { - tal_uri: "https://tal.example.net/root.tal".to_string(), - tal_bytes: b"x".to_vec(), - }], + trust_anchors: vec![sample_trust_anchor()], reject_list_sha256: compute_reject_list_sha256(std::iter::empty::<&str>()), rejected_objects: Vec::new(), }; @@ -838,8 +873,10 @@ mod tests { let summary = materialize_cir_from_repo_bytes(&cir, &repo_bytes_db, &mirror_root, true).unwrap(); 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.copied_files, 2); + assert_eq!(summary.copied_files, 3); assert_eq!( std::fs::read(mirror_root.join("example.net/repo/a.cer")).unwrap(), b"a" @@ -848,6 +885,10 @@ mod tests { std::fs::read(mirror_root.join("example.net/repo/nested/b.roa")).unwrap(), 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]) { diff --git a/src/cir/mod.rs b/src/cir/mod.rs index 4e7b51e..b5addb5 100644 --- a/src/cir/mod.rs +++ b/src/cir/mod.rs @@ -12,16 +12,16 @@ pub use decode::{CirDecodeError, decode_cir}; pub use encode::{CirEncodeError, encode_cir}; #[cfg(feature = "full")] pub use export::{ - CirExportError, CirExportSummary, CirTalBinding, build_cir_from_run, build_cir_from_run_multi, - export_cir_from_run, export_cir_from_run_multi, write_cir_file, + CirExportError, CirExportSummary, CirTrustAnchorBinding, build_cir_from_run, + build_cir_from_run_multi, export_cir_from_run, export_cir_from_run_multi, write_cir_file, }; pub use materialize::{ CirMaterializeError, CirMaterializeSummary, materialize_cir, materialize_cir_from_raw_store, materialize_cir_from_repo_bytes, mirror_relative_path_for_rsync_uri, resolve_static_pool_file, }; pub use model::{ - CIR_VERSION_V1, CIR_VERSION_V2, CanonicalInputRepresentation, CirHashAlgorithm, CirObject, - CirRejectedObject, CirTal, compute_reject_list_sha256, + CIR_VERSION_V1, CIR_VERSION_V3, CanonicalInputRepresentation, CirHashAlgorithm, CirObject, + CirRejectedObject, CirTrustAnchor, compute_reject_list_sha256, sha256, }; pub use sequence::{CirSequenceManifest, CirSequenceStep, CirSequenceStepKind}; #[cfg(feature = "full")] @@ -34,8 +34,8 @@ pub use static_pool::{ #[cfg(test)] mod tests { use super::{ - CIR_VERSION_V2, CanonicalInputRepresentation, CirHashAlgorithm, CirObject, - CirRejectedObject, CirTal, compute_reject_list_sha256, decode_cir, encode_cir, + CIR_VERSION_V3, CanonicalInputRepresentation, CirHashAlgorithm, CirObject, + CirRejectedObject, CirTrustAnchor, compute_reject_list_sha256, decode_cir, encode_cir, }; fn sample_time() -> time::OffsetDateTime { @@ -46,6 +46,16 @@ mod tests { .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 { let rejected_objects = vec![ CirRejectedObject { @@ -58,7 +68,7 @@ mod tests { }, ]; CanonicalInputRepresentation { - version: CIR_VERSION_V2, + version: CIR_VERSION_V3, hash_alg: CirHashAlgorithm::Sha256, validation_time: sample_time(), objects: vec![ @@ -71,11 +81,11 @@ mod tests { sha256: vec![0x22; 32], }, ], - tals: vec![CirTal { - tal_uri: "https://tal.example.net/root.tal".to_string(), - tal_bytes: b"https://tal.example.net/ta.cer\nrsync://example.net/repo/ta.cer\nMIIB" - .to_vec(), - }], + trust_anchors: vec![sample_trust_anchor( + "rsync://example.net/repo/ta.cer", + "https://tal.example.net/root.tal", + b"ta-der", + )], reject_list_sha256: compute_reject_list_sha256( rejected_objects.iter().map(|item| item.object_uri.as_str()), ), @@ -114,14 +124,15 @@ mod tests { #[test] fn cir_roundtrip_minimal_succeeds() { let cir = CanonicalInputRepresentation { - version: CIR_VERSION_V2, + version: CIR_VERSION_V3, hash_alg: CirHashAlgorithm::Sha256, validation_time: sample_time(), objects: Vec::new(), - tals: vec![CirTal { - tal_uri: "https://tal.example.net/minimal.tal".to_string(), - tal_bytes: b"rsync://example.net/repo/ta.cer\nMIIB".to_vec(), - }], + trust_anchors: vec![sample_trust_anchor( + "rsync://example.net/repo/minimal-ta.cer", + "https://tal.example.net/minimal.tal", + b"minimal-ta-der", + )], reject_list_sha256: compute_reject_list_sha256(std::iter::empty::<&str>()), rejected_objects: Vec::new(), }; @@ -133,7 +144,7 @@ mod tests { #[test] fn cir_model_rejects_unsorted_duplicate_objects() { let cir = CanonicalInputRepresentation { - version: CIR_VERSION_V2, + version: CIR_VERSION_V3, hash_alg: CirHashAlgorithm::Sha256, validation_time: sample_time(), objects: vec![ @@ -146,10 +157,11 @@ mod tests { sha256: vec![0x22; 32], }, ], - tals: vec![CirTal { - tal_uri: "https://tal.example.net/root.tal".to_string(), - tal_bytes: b"x".to_vec(), - }], + trust_anchors: vec![sample_trust_anchor( + "rsync://example.net/repo/ta.cer", + "https://tal.example.net/root.tal", + b"ta-der", + )], reject_list_sha256: compute_reject_list_sha256(std::iter::empty::<&str>()), rejected_objects: Vec::new(), }; @@ -160,25 +172,27 @@ mod tests { #[test] fn cir_model_rejects_duplicate_tals() { let cir = CanonicalInputRepresentation { - version: CIR_VERSION_V2, + version: CIR_VERSION_V3, hash_alg: CirHashAlgorithm::Sha256, validation_time: sample_time(), objects: Vec::new(), - tals: vec![ - CirTal { - tal_uri: "https://tal.example.net/root.tal".to_string(), - tal_bytes: b"a".to_vec(), - }, - CirTal { - tal_uri: "https://tal.example.net/root.tal".to_string(), - tal_bytes: b"b".to_vec(), - }, + trust_anchors: vec![ + sample_trust_anchor( + "rsync://example.net/repo/ta.cer", + "https://tal.example.net/root.tal", + b"ta-der-a", + ), + sample_trust_anchor( + "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>()), rejected_objects: Vec::new(), }; - let err = encode_cir(&cir).expect_err("duplicate tals must fail"); - assert!(err.to_string().contains("CIR.tals"), "{err}"); + let err = encode_cir(&cir).expect_err("duplicate trust_anchors must fail"); + assert!(err.to_string().contains("CIR.trustAnchors"), "{err}"); } #[test] @@ -186,13 +200,13 @@ mod tests { let mut der = encode_cir(&sample_cir()).expect("encode cir"); let pos = der .windows(3) - .position(|window| window == [0x02, 0x01, CIR_VERSION_V2 as u8]) + .position(|window| window == [0x02, 0x01, CIR_VERSION_V3 as u8]) .or_else(|| { 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"); - der[pos + 2] = 3; + der[pos + 2] = 2; let err = decode_cir(&der).expect_err("wrong version must fail"); assert!(err.to_string().contains("unexpected CIR version"), "{err}"); } @@ -228,17 +242,18 @@ mod tests { #[test] fn cir_model_rejects_non_rsync_object_uri_and_empty_tals() { let bad_object = CanonicalInputRepresentation { - version: CIR_VERSION_V2, + version: CIR_VERSION_V3, hash_alg: CirHashAlgorithm::Sha256, validation_time: sample_time(), objects: vec![CirObject { rsync_uri: "https://example.net/repo/a.roa".to_string(), sha256: vec![0x11; 32], }], - tals: vec![CirTal { - tal_uri: "https://tal.example.net/root.tal".to_string(), - tal_bytes: b"x".to_vec(), - }], + trust_anchors: vec![sample_trust_anchor( + "rsync://example.net/repo/ta.cer", + "https://tal.example.net/root.tal", + b"ta-der", + )], reject_list_sha256: compute_reject_list_sha256(std::iter::empty::<&str>()), rejected_objects: Vec::new(), }; @@ -246,17 +261,18 @@ mod tests { assert!(err.to_string().contains("rsync://"), "{err}"); let no_tals = CanonicalInputRepresentation { - version: CIR_VERSION_V2, + version: CIR_VERSION_V3, hash_alg: CirHashAlgorithm::Sha256, validation_time: sample_time(), objects: Vec::new(), - tals: Vec::new(), + trust_anchors: Vec::new(), reject_list_sha256: compute_reject_list_sha256(std::iter::empty::<&str>()), 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!( - err.to_string().contains("CIR.tals must be non-empty"), + err.to_string() + .contains("CIR.trustAnchors must be non-empty"), "{err}" ); } @@ -264,14 +280,15 @@ mod tests { #[test] fn cir_model_rejects_non_utc_time_bad_hash_len_and_non_http_tal_uri() { let bad_time = CanonicalInputRepresentation { - version: CIR_VERSION_V2, + version: CIR_VERSION_V3, hash_alg: CirHashAlgorithm::Sha256, validation_time: sample_time().to_offset(time::UtcOffset::from_hms(8, 0, 0).unwrap()), objects: Vec::new(), - tals: vec![CirTal { - tal_uri: "https://tal.example.net/root.tal".to_string(), - tal_bytes: b"x".to_vec(), - }], + trust_anchors: vec![sample_trust_anchor( + "rsync://example.net/repo/ta.cer", + "https://tal.example.net/root.tal", + b"ta-der", + )], reject_list_sha256: compute_reject_list_sha256(std::iter::empty::<&str>()), rejected_objects: Vec::new(), }; @@ -279,17 +296,18 @@ mod tests { assert!(err.to_string().contains("UTC"), "{err}"); let bad_hash = CanonicalInputRepresentation { - version: CIR_VERSION_V2, + version: CIR_VERSION_V3, hash_alg: CirHashAlgorithm::Sha256, validation_time: sample_time(), objects: vec![CirObject { rsync_uri: "rsync://example.net/repo/a.roa".to_string(), sha256: vec![0x11; 31], }], - tals: vec![CirTal { - tal_uri: "https://tal.example.net/root.tal".to_string(), - tal_bytes: b"x".to_vec(), - }], + trust_anchors: vec![sample_trust_anchor( + "rsync://example.net/repo/ta.cer", + "https://tal.example.net/root.tal", + b"ta-der", + )], reject_list_sha256: compute_reject_list_sha256(std::iter::empty::<&str>()), rejected_objects: Vec::new(), }; @@ -297,14 +315,15 @@ mod tests { assert!(err.to_string().contains("32 bytes"), "{err}"); let bad_tal_uri = CanonicalInputRepresentation { - version: CIR_VERSION_V2, + version: CIR_VERSION_V3, hash_alg: CirHashAlgorithm::Sha256, validation_time: sample_time(), objects: Vec::new(), - tals: vec![CirTal { - tal_uri: "ftp://tal.example.net/root.tal".to_string(), - tal_bytes: b"x".to_vec(), - }], + trust_anchors: vec![sample_trust_anchor( + "rsync://example.net/repo/ta.cer", + "ftp://tal.example.net/root.tal", + b"ta-der", + )], reject_list_sha256: compute_reject_list_sha256(std::iter::empty::<&str>()), rejected_objects: Vec::new(), }; @@ -332,22 +351,25 @@ mod tests { ] .concat(), ); - let tal = test_encode_tlv( + let trust_anchor = test_encode_tlv( 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(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(), ); let bad = test_encode_tlv( 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(0x18, b"20260407123456Z"), 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(0x30, &[]), ] diff --git a/src/cir/model.rs b/src/cir/model.rs index 47ba11e..362be54 100644 --- a/src/cir/model.rs +++ b/src/cir/model.rs @@ -2,6 +2,7 @@ use crate::data_model::oid::OID_SHA256; pub const CIR_VERSION_V1: u32 = 1; pub const CIR_VERSION_V2: u32 = 2; +pub const CIR_VERSION_V3: u32 = 3; pub const DIGEST_LEN_SHA256: usize = 32; #[derive(Clone, Debug, PartialEq, Eq)] @@ -23,16 +24,16 @@ pub struct CanonicalInputRepresentation { pub hash_alg: CirHashAlgorithm, pub validation_time: time::OffsetDateTime, pub objects: Vec, - pub tals: Vec, + pub trust_anchors: Vec, pub reject_list_sha256: Vec, pub rejected_objects: Vec, } impl CanonicalInputRepresentation { pub fn validate(&self) -> Result<(), String> { - if self.version != CIR_VERSION_V2 { + if self.version != CIR_VERSION_V3 { return Err(format!( - "CIR version must be {CIR_VERSION_V2}, got {}", + "CIR version must be {CIR_VERSION_V3}, got {}", self.version )); } @@ -47,8 +48,10 @@ impl CanonicalInputRepresentation { "CIR.objects must be sorted by rsyncUri and unique", )?; validate_sorted_unique_strings( - self.tals.iter().map(|item| item.tal_uri.as_str()), - "CIR.tals must be sorted by talUri and unique", + self.trust_anchors + .iter() + .map(|item| item.ta_rsync_uri.as_str()), + "CIR.trustAnchors must be sorted by taRsyncUri and unique", )?; validate_sorted_unique_strings( self.rejected_objects @@ -56,8 +59,21 @@ impl CanonicalInputRepresentation { .map(|item| item.object_uri.as_str()), "CIR.rejectedObjects must be sorted by objectUri and unique", )?; - if self.tals.is_empty() { - return Err("CIR.tals must be non-empty".into()); + let object_uris = self + .objects + .iter() + .map(|item| item.rsync_uri.as_str()) + .collect::>(); + 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 { return Err(format!( @@ -68,8 +84,8 @@ impl CanonicalInputRepresentation { for object in &self.objects { object.validate()?; } - for tal in &self.tals { - tal.validate()?; + for trust_anchor in &self.trust_anchors { + trust_anchor.validate()?; } for item in &self.rejected_objects { item.validate()?; @@ -111,21 +127,55 @@ impl CirObject { } #[derive(Clone, Debug, PartialEq, Eq)] -pub struct CirTal { +pub struct CirTrustAnchor { + pub ta_rsync_uri: String, pub tal_uri: String, pub tal_bytes: Vec, + pub ta_certificate_der: Vec, + pub ta_certificate_sha256: Vec, } -impl CirTal { +impl CirTrustAnchor { 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://")) { 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 )); } 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(()) } @@ -150,15 +200,19 @@ impl CirRejectedObject { } pub fn compute_reject_list_sha256<'a>(uris: impl IntoIterator) -> Vec { - use sha2::Digest; - let mut body = Vec::new(); for uri in uris { let bytes = uri.as_bytes(); body.extend_from_slice(&(bytes.len() as u32).to_be_bytes()); body.extend_from_slice(bytes); } - sha2::Sha256::digest(body).to_vec() + sha256(&body) +} + +pub fn sha256(bytes: &[u8]) -> Vec { + use sha2::Digest; + + sha2::Sha256::digest(bytes).to_vec() } fn validate_sorted_unique_strings<'a>( diff --git a/src/cli.rs b/src/cli.rs index 85e69fa..0a8944d 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -1,7 +1,7 @@ use crate::ccr::{ 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::path::{Path, PathBuf}; @@ -1870,7 +1870,7 @@ pub fn run(argv: &[String]) -> Result<(), String> { .discoveries .iter() .zip(cir_tal_uris.iter()) - .map(|(discovery, tal_uri)| CirTalBinding { + .map(|(discovery, tal_uri)| CirTrustAnchorBinding { trust_anchor: &discovery.trust_anchor, 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_total_ms = Some(summary.timing.total_ms); 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(), summary.object_count, - summary.tal_count, + summary.trust_anchor_count, summary.timing.build_cir_ms, summary.timing.write_cir_ms, summary.timing.total_ms diff --git a/src/validation/from_tal.rs b/src/validation/from_tal.rs index a41ae3d..a549c94 100644 --- a/src/validation/from_tal.rs +++ b/src/validation/from_tal.rs @@ -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 { + 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)] mod tests { use super::*; use crate::fetch::rsync::LocalDirRsyncFetcher; + struct FailingHttpFetcher; + + impl Fetcher for FailingHttpFetcher { + fn fetch(&self, uri: &str) -> Result, String> { + Err(format!("blocked test HTTP fetch: {uri}")) + } + } + #[test] fn discover_root_ca_instance_from_tal_with_fetchers_supports_rsync_ta_uri() { let tal_bytes = std::fs::read( @@ -352,17 +371,18 @@ mod tests { std::fs::create_dir_all(&mirror_root).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( td.path() .join(rsync_uri.host_str().unwrap()) .join("repository"), ); - let discovery = discover_root_ca_instance_from_tal_with_fetchers(&http, &rsync, tal, None) - .expect("discover via rsync TA"); + let discovery = discover_root_ca_instance_from_tal_with_fetchers( + &FailingHttpFetcher, + &rsync, + tal, + None, + ) + .expect("discover via rsync TA fallback"); assert!( discovery .trust_anchor diff --git a/src/validation/run_tree_from_tal.rs b/src/validation/run_tree_from_tal.rs index 54b9d5f..ea4ccf9 100644 --- a/src/validation/run_tree_from_tal.rs +++ b/src/validation/run_tree_from_tal.rs @@ -23,7 +23,8 @@ use crate::replay::fetch_http::PayloadReplayHttpFetcher; use crate::replay::fetch_rsync::PayloadReplayRsyncFetcher; use crate::sync::rrdp::Fetcher; 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_url, 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| { 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 { 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 { - 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), + ) } } } diff --git a/tests/test_cir_delta_export_m1.rs b/tests/test_cir_delta_export_m1.rs index 1b70980..1005870 100644 --- a/tests/test_cir_delta_export_m1.rs +++ b/tests/test_cir_delta_export_m1.rs @@ -6,8 +6,8 @@ use rpki::ccr::{ encode_content_info, }; use rpki::cir::{ - CIR_VERSION_V2, CanonicalInputRepresentation, CirHashAlgorithm, CirObject, CirTal, - compute_reject_list_sha256, encode_cir, + CIR_VERSION_V3, CanonicalInputRepresentation, CirHashAlgorithm, CirObject, CirTrustAnchor, + compute_reject_list_sha256, encode_cir, sha256, }; 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}; hex::encode(Sha256::digest(b"delta-object")) }; + let trust_anchors = vec![test_trust_anchor()]; let full_cir = CanonicalInputRepresentation { - version: CIR_VERSION_V2, + version: CIR_VERSION_V3, hash_alg: CirHashAlgorithm::Sha256, validation_time: time::OffsetDateTime::parse( "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(), sha256: hex::decode(&full_obj_hash).unwrap(), }], - tals: vec![CirTal { - 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(), - }], + trust_anchors: trust_anchors.clone(), reject_list_sha256: compute_reject_list_sha256(std::iter::empty::<&str>()), rejected_objects: Vec::new(), }; let delta_cir = CanonicalInputRepresentation { - version: CIR_VERSION_V2, + version: CIR_VERSION_V3, hash_alg: CirHashAlgorithm::Sha256, validation_time: time::OffsetDateTime::parse( "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 }, - tals: full_cir.tals.clone(), + trust_anchors, reject_list_sha256: compute_reject_list_sha256(std::iter::empty::<&str>()), rejected_objects: Vec::new(), }; @@ -226,3 +224,15 @@ fi assert!(out.join("full").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, + } +} diff --git a/tests/test_cir_drop_report_m5.rs b/tests/test_cir_drop_report_m5.rs index e18913b..a32e834 100644 --- a/tests/test_cir_drop_report_m5.rs +++ b/tests/test_cir_drop_report_m5.rs @@ -7,8 +7,8 @@ use rpki::ccr::{ encode_content_info, }; use rpki::cir::{ - CIR_VERSION_V2, CanonicalInputRepresentation, CirHashAlgorithm, CirObject, CirTal, - compute_reject_list_sha256, encode_cir, + CIR_VERSION_V3, CanonicalInputRepresentation, CirHashAlgorithm, CirObject, CirTrustAnchor, + compute_reject_list_sha256, encode_cir, sha256, }; #[test] @@ -34,7 +34,7 @@ fn cir_drop_report_counts_dropped_roa_objects_and_vrps() { .expect("write repo bytes"); let cir = CanonicalInputRepresentation { - version: CIR_VERSION_V2, + version: CIR_VERSION_V3, hash_alg: CirHashAlgorithm::Sha256, validation_time: time::OffsetDateTime::parse( "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(), sha256: hex::decode(&hash).unwrap(), }], - tals: vec![CirTal { - tal_uri: "https://example.test/root.tal".to_string(), - tal_bytes: b"rsync://example.net/repo/root.cer\nMIIB".to_vec(), - }], + trust_anchors: vec![test_trust_anchor()], reject_list_sha256: compute_reject_list_sha256(std::iter::empty::<&str>()), rejected_objects: Vec::new(), }; @@ -124,3 +121,15 @@ fn cir_drop_report_counts_dropped_roa_objects_and_vrps() { .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, + } +} diff --git a/tests/test_cir_matrix_m9.rs b/tests/test_cir_matrix_m9.rs index dc4a0ae..c82ad78 100644 --- a/tests/test_cir_matrix_m9.rs +++ b/tests/test_cir_matrix_m9.rs @@ -3,8 +3,8 @@ use std::process::Command; use rpki::blob_store::ExternalRepoBytesDb; use rpki::cir::{ - CIR_VERSION_V2, CanonicalInputRepresentation, CirHashAlgorithm, CirObject, CirTal, - compute_reject_list_sha256, encode_cir, materialize_cir_from_repo_bytes, + CIR_VERSION_V3, CanonicalInputRepresentation, CirHashAlgorithm, CirTrustAnchor, + compute_reject_list_sha256, encode_cir, materialize_cir_from_repo_bytes, sha256, }; fn skip_heavy_script_replay_test() -> bool { @@ -30,26 +30,22 @@ fn build_ta_only_cir() -> (CanonicalInputRepresentation, Vec) { .expect("tal has rsync uri") .as_str() .to_string(); - let ta_hash = { - use sha2::{Digest, Sha256}; - Sha256::digest(&ta_bytes).to_vec() - }; ( CanonicalInputRepresentation { - version: CIR_VERSION_V2, + version: CIR_VERSION_V3, hash_alg: CirHashAlgorithm::Sha256, validation_time: time::OffsetDateTime::parse( "2026-04-07T00:00:00Z", &time::format_description::well_known::Rfc3339, ) .unwrap(), - objects: vec![CirObject { - rsync_uri: ta_rsync_uri, - sha256: ta_hash, - }], - tals: vec![CirTal { + objects: Vec::new(), + trust_anchors: vec![CirTrustAnchor { + ta_rsync_uri, tal_uri: "https://example.test/root.tal".to_string(), 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>()), rejected_objects: Vec::new(), diff --git a/tests/test_cir_peer_replay_m8.rs b/tests/test_cir_peer_replay_m8.rs index 5ce7ef4..85e343c 100644 --- a/tests/test_cir_peer_replay_m8.rs +++ b/tests/test_cir_peer_replay_m8.rs @@ -3,8 +3,8 @@ use std::process::Command; use rpki::blob_store::ExternalRepoBytesDb; use rpki::cir::{ - CIR_VERSION_V2, CanonicalInputRepresentation, CirHashAlgorithm, CirObject, CirTal, - compute_reject_list_sha256, encode_cir, materialize_cir_from_repo_bytes, + CIR_VERSION_V3, CanonicalInputRepresentation, CirHashAlgorithm, CirTrustAnchor, + compute_reject_list_sha256, encode_cir, materialize_cir_from_repo_bytes, sha256, }; fn skip_heavy_script_replay_test() -> bool { @@ -30,26 +30,22 @@ fn build_ta_only_cir() -> (CanonicalInputRepresentation, Vec) { .expect("tal has rsync uri") .as_str() .to_string(); - let ta_hash = { - use sha2::{Digest, Sha256}; - Sha256::digest(&ta_bytes).to_vec() - }; ( CanonicalInputRepresentation { - version: CIR_VERSION_V2, + version: CIR_VERSION_V3, hash_alg: CirHashAlgorithm::Sha256, validation_time: time::OffsetDateTime::parse( "2026-04-07T00:00:00Z", &time::format_description::well_known::Rfc3339, ) .unwrap(), - objects: vec![CirObject { - rsync_uri: ta_rsync_uri, - sha256: ta_hash, - }], - tals: vec![CirTal { + objects: Vec::new(), + trust_anchors: vec![CirTrustAnchor { + ta_rsync_uri, tal_uri: "https://example.test/root.tal".to_string(), 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>()), rejected_objects: Vec::new(), diff --git a/tests/test_cir_sequence_m2.rs b/tests/test_cir_sequence_m2.rs index f591c20..b8fbea7 100644 --- a/tests/test_cir_sequence_m2.rs +++ b/tests/test_cir_sequence_m2.rs @@ -6,8 +6,8 @@ use rpki::ccr::{ encode_content_info, }; use rpki::cir::{ - CIR_VERSION_V2, CanonicalInputRepresentation, CirHashAlgorithm, CirObject, CirTal, - compute_reject_list_sha256, encode_cir, + CIR_VERSION_V3, CanonicalInputRepresentation, CirHashAlgorithm, CirObject, CirTrustAnchor, + compute_reject_list_sha256, encode_cir, sha256, }; fn skip_heavy_blackbox_test() -> bool { @@ -36,7 +36,7 @@ fn cir_offline_sequence_writes_parseable_sequence_json_and_steps() { .unwrap(); let mk_cir = |uri: &str, hash_hex: &str, vt: &str| CanonicalInputRepresentation { - version: CIR_VERSION_V2, + version: CIR_VERSION_V3, hash_alg: CirHashAlgorithm::Sha256, validation_time: time::OffsetDateTime::parse( vt, @@ -47,10 +47,7 @@ fn cir_offline_sequence_writes_parseable_sequence_json_and_steps() { rsync_uri: uri.to_string(), sha256: hex::decode(hash_hex).unwrap(), }], - tals: vec![CirTal { - 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(), - }], + trust_anchors: vec![test_trust_anchor()], reject_list_sha256: compute_reject_list_sha256(std::iter::empty::<&str>()), rejected_objects: Vec::new(), }; @@ -68,7 +65,7 @@ fn cir_offline_sequence_writes_parseable_sequence_json_and_steps() { "2026-03-16T11:49:15Z", ); let delta_cir = CanonicalInputRepresentation { - version: CIR_VERSION_V2, + version: CIR_VERSION_V3, hash_alg: CirHashAlgorithm::Sha256, validation_time: time::OffsetDateTime::parse( "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 }, - tals: full_cir.tals.clone(), + trust_anchors: full_cir.trust_anchors.clone(), reject_list_sha256: compute_reject_list_sha256(std::iter::empty::<&str>()), rejected_objects: Vec::new(), }; @@ -244,3 +241,15 @@ fi 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, + } +} diff --git a/tests/test_cir_sequence_peer_replay_m4.rs b/tests/test_cir_sequence_peer_replay_m4.rs index e3f5f58..0f7df15 100644 --- a/tests/test_cir_sequence_peer_replay_m4.rs +++ b/tests/test_cir_sequence_peer_replay_m4.rs @@ -3,8 +3,8 @@ use std::process::Command; use rpki::blob_store::ExternalRepoBytesDb; use rpki::cir::{ - CIR_VERSION_V2, CanonicalInputRepresentation, CirHashAlgorithm, CirObject, CirTal, - compute_reject_list_sha256, encode_cir, materialize_cir_from_repo_bytes, + CIR_VERSION_V3, CanonicalInputRepresentation, CirHashAlgorithm, CirTrustAnchor, + compute_reject_list_sha256, encode_cir, materialize_cir_from_repo_bytes, sha256, }; fn skip_heavy_script_replay_test() -> bool { @@ -30,26 +30,22 @@ fn build_ta_only_cir() -> (CanonicalInputRepresentation, Vec) { .expect("tal has rsync uri") .as_str() .to_string(); - let ta_hash = { - use sha2::{Digest, Sha256}; - Sha256::digest(&ta_bytes).to_vec() - }; ( CanonicalInputRepresentation { - version: CIR_VERSION_V2, + version: CIR_VERSION_V3, hash_alg: CirHashAlgorithm::Sha256, validation_time: time::OffsetDateTime::parse( "2026-04-07T00:00:00Z", &time::format_description::well_known::Rfc3339, ) .unwrap(), - objects: vec![CirObject { - rsync_uri: ta_rsync_uri, - sha256: ta_hash, - }], - tals: vec![CirTal { + objects: Vec::new(), + trust_anchors: vec![CirTrustAnchor { + ta_rsync_uri, tal_uri: "https://example.test/root.tal".to_string(), 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>()), rejected_objects: Vec::new(), diff --git a/tests/test_cir_sequence_replay_m3.rs b/tests/test_cir_sequence_replay_m3.rs index f3d08ee..5e75abe 100644 --- a/tests/test_cir_sequence_replay_m3.rs +++ b/tests/test_cir_sequence_replay_m3.rs @@ -3,8 +3,8 @@ use std::process::Command; use rpki::blob_store::ExternalRepoBytesDb; use rpki::cir::{ - CIR_VERSION_V2, CanonicalInputRepresentation, CirHashAlgorithm, CirObject, CirTal, - compute_reject_list_sha256, encode_cir, materialize_cir_from_repo_bytes, + CIR_VERSION_V3, CanonicalInputRepresentation, CirHashAlgorithm, CirTrustAnchor, + compute_reject_list_sha256, encode_cir, materialize_cir_from_repo_bytes, sha256, }; fn skip_heavy_script_replay_test() -> bool { @@ -30,26 +30,22 @@ fn build_ta_only_cir() -> (CanonicalInputRepresentation, Vec) { .expect("tal has rsync uri") .as_str() .to_string(); - let ta_hash = { - use sha2::{Digest, Sha256}; - Sha256::digest(&ta_bytes).to_vec() - }; ( CanonicalInputRepresentation { - version: CIR_VERSION_V2, + version: CIR_VERSION_V3, hash_alg: CirHashAlgorithm::Sha256, validation_time: time::OffsetDateTime::parse( "2026-04-07T00:00:00Z", &time::format_description::well_known::Rfc3339, ) .unwrap(), - objects: vec![CirObject { - rsync_uri: ta_rsync_uri, - sha256: ta_hash, - }], - tals: vec![CirTal { + objects: Vec::new(), + trust_anchors: vec![CirTrustAnchor { + ta_rsync_uri, tal_uri: "https://example.test/root.tal".to_string(), 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>()), rejected_objects: Vec::new(), diff --git a/tests/test_cli_run_offline_m18.rs b/tests/test_cli_run_offline_m18.rs index 424bb9c..33d1947 100644 --- a/tests/test_cli_run_offline_m18.rs +++ b/tests/test_cli_run_offline_m18.rs @@ -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 cir = rpki::cir::decode_cir(&bytes).expect("decode cir"); - assert_eq!(cir.tals.len(), 1); - assert_eq!(cir.tals[0].tal_uri, "https://example.test/root.tal"); + assert_eq!(cir.trust_anchors.len(), 1); + assert_eq!( + cir.trust_anchors[0].tal_uri, + "https://example.test/root.tal" + ); + assert!(!cir.trust_anchors[0].ta_certificate_der.is_empty()); assert!( - cir.objects + !cir.objects .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) ); }