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

View File

@ -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'}`")

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}"))?;
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={}",

View File

@ -49,17 +49,23 @@ fn run(argv: Vec<String>) -> 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<String>) -> 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() {

View File

@ -59,9 +59,11 @@ fn run(argv: Vec<String>) -> 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

View File

@ -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::<Vec<_>>();
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::<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) {
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()
.map(|item| item.object_uri.clone())
.collect::<BTreeSet<_>>();
let ours_tals = ours
.tals
let ours_trust_anchors = ours
.trust_anchors
.iter()
.map(|item| item.tal_uri.clone())
.collect::<BTreeSet<_>>();
let peer_tals = peer
.tals
.map(|item| {
(
item.ta_rsync_uri.clone(),
hex::encode(&item.ta_certificate_sha256),
)
})
.collect::<BTreeMap<_, _>>();
let peer_trust_anchors = peer
.trust_anchors
.iter()
.map(|item| item.tal_uri.clone())
.collect::<BTreeSet<_>>();
.map(|item| {
(
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 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::<Vec<_>>();
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 {
.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!("{tal_uri}\nrsync://example.net/repo/ta.cer\nMIIB")
.into_bytes(),
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(

View File

@ -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 <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> {
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 _ = 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(),
};

View File

@ -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<CanonicalInputRepresentation, CirDecodeE
}
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 {
expected: CIR_VERSION_V2,
expected: CIR_VERSION_V3,
actual: version,
});
}
@ -52,12 +52,14 @@ pub fn decode_cir(der: &[u8]) -> Result<CanonicalInputRepresentation, CirDecodeE
objects.push(decode_object(full)?);
}
let tals_der = seq.take_tag(0x30).map_err(CirDecodeError::Parse)?;
let mut tals_reader = DerReader::new(tals_der);
let mut tals = Vec::new();
while !tals_reader.is_empty() {
let (_tag, full, _value) = tals_reader.take_any_full().map_err(CirDecodeError::Parse)?;
tals.push(decode_tal(full)?);
let trust_anchors_der = seq.take_tag(0x30).map_err(CirDecodeError::Parse)?;
let mut trust_anchors_reader = DerReader::new(trust_anchors_der);
let mut trust_anchors = Vec::new();
while !trust_anchors_reader.is_empty() {
let (_tag, full, _value) = trust_anchors_reader
.take_any_full()
.map_err(CirDecodeError::Parse)?;
trust_anchors.push(decode_trust_anchor(full)?);
}
let reject_list_sha256 = seq
@ -84,7 +86,7 @@ pub fn decode_cir(der: &[u8]) -> Result<CanonicalInputRepresentation, CirDecodeE
hash_alg,
validation_time,
objects,
tals,
trust_anchors,
reject_list_sha256,
rejected_objects,
};
@ -123,12 +125,17 @@ fn decode_object(der: &[u8]) -> Result<CirObject, CirDecodeError> {
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 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<CirTal, CirDecodeError> {
.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<CirRejectedObject, CirDecodeError> {

View File

@ -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<Vec<u8>, CirEnco
.collect::<Result<Vec<_>, _>>()?,
),
encode_sequence(
&cir.tals
&cir.trust_anchors
.iter()
.map(encode_tal)
.map(encode_trust_anchor)
.collect::<Result<Vec<_>, _>>()?,
),
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> {
tal.validate().map_err(CirEncodeError::Validate)?;
fn encode_trust_anchor(trust_anchor: &CirTrustAnchor) -> Result<Vec<u8>, 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<u8>) {
#[allow(dead_code)]
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::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<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(
store: &RocksStore,
trust_anchor: &TrustAnchor,
@ -127,7 +139,7 @@ pub fn build_cir_from_run(
) -> Result<CanonicalInputRepresentation, CirExportError> {
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<CirStaticPoolExportSummary, CirExportError> {
let ta_hashes = trust_anchors
.iter()
.map(|ta| ta_sha256_hex(&ta.ta_certificate.raw_der))
.collect::<BTreeSet<_>>();
let _ = trust_anchors;
let hashes = cir
.objects
.iter()
.map(|item| hex::encode(&item.sha256))
.filter(|hash| !ta_hashes.contains(hash))
.collect::<Vec<_>>();
let mut summary = export_hashes_from_store(store, static_root, capture_date_utc, &hashes)?;
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)
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<CirRawStoreExportSummary, CirExportError> {
let ta_by_hash = trust_anchors
.iter()
.map(|ta| (ta_sha256_hex(&ta.ta_certificate.raw_der), *ta))
.collect::<BTreeMap<_, _>>();
let _ = trust_anchors;
let unique: BTreeSet<String> = 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<CirExportSummary, CirExportError> {
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::<Vec<_>>();
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",
);
for trust_anchor in &cir.trust_anchors {
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",
!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<_>>(),
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 {

View File

@ -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<CirMaterializeSummary, CirMaterializeError> {
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::<std::collections::BTreeSet<_>>();
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<CirMaterializeSummary, CirMaterializeError> {
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);
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(),
})?;
write_bytes_to_mirror_uri(
mirror_root,
&object.rsync_uri,
&bytes,
&raw_store_db.display().to_string(),
)?;
copied_files += 1;
}
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::<std::collections::BTreeSet<_>>();
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<CirMaterializeSummary, CirMaterializeError> {
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,7 +243,64 @@ 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)?;
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);
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 {
src: repo_bytes_db.display().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(),
})?;
copied_files += 1;
}
})
}
let actual = collect_materialized_uris(mirror_root)?;
let expected = cir
.objects
fn expected_materialized_uris(cir: &CanonicalInputRepresentation) -> BTreeSet<String> {
cir.objects
.iter()
.map(|item| item.rsync_uri.clone())
.collect::<std::collections::BTreeSet<_>>();
if actual != expected {
return Err(CirMaterializeError::TreeMismatch(format!(
"expected {} files, got {} files",
expected.len(),
actual.len()
)));
}
Ok(CirMaterializeSummary {
object_count: cir.objects.len(),
linked_files: 0,
copied_files,
})
.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<PathBuf, CirMaterializeError> {
@ -375,10 +398,8 @@ pub fn resolve_static_pool_file(
})
}
fn collect_materialized_uris(
mirror_root: &Path,
) -> Result<std::collections::BTreeSet<String>, CirMaterializeError> {
let mut out = std::collections::BTreeSet::new();
fn collect_materialized_uris(mirror_root: &Path) -> Result<BTreeSet<String>, 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]) {

View File

@ -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, &[]),
]

View File

@ -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<CirObject>,
pub tals: Vec<CirTal>,
pub trust_anchors: Vec<CirTrustAnchor>,
pub reject_list_sha256: Vec<u8>,
pub rejected_objects: Vec<CirRejectedObject>,
}
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::<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 {
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<u8>,
pub ta_certificate_der: Vec<u8>,
pub ta_certificate_sha256: Vec<u8>,
}
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<Item = &'a str>) -> Vec<u8> {
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<u8> {
use sha2::Digest;
sha2::Sha256::digest(bytes).to_vec()
}
fn validate_sorted_unique_strings<'a>(

View File

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

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)]
mod tests {
use super::*;
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]
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

View File

@ -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),
)
}
}
}

View File

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

View File

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

View File

@ -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<u8>) {
.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(),

View File

@ -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<u8>) {
.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(),

View File

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

View File

@ -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<u8>) {
.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(),

View File

@ -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<u8>) {
.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(),

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 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)
);
}