20260609 CIR区分fresh和cached验证输入

This commit is contained in:
yuyr 2026-06-11 19:13:48 +08:00
parent 74012c686d
commit 4047214ddf
26 changed files with 1054 additions and 552 deletions

View File

@ -87,6 +87,10 @@ pub struct PublicationPointAudit {
pub warnings: Vec<AuditWarning>, pub warnings: Vec<AuditWarning>,
pub objects: Vec<ObjectAuditEntry>, pub objects: Vec<ObjectAuditEntry>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub cir_fresh_objects: Vec<ObjectAuditEntry>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub cir_cached_objects: Vec<ObjectAuditEntry>,
} }
#[derive(Clone, Debug, PartialEq, Eq, Serialize)] #[derive(Clone, Debug, PartialEq, Eq, Serialize)]

View File

@ -127,7 +127,7 @@ fn main() -> Result<(), String> {
.map_err(|e| format!("open repo bytes db failed: {e}"))?; .map_err(|e| format!("open repo bytes db failed: {e}"))?;
let mut object_hash_by_uri = BTreeMap::new(); let mut object_hash_by_uri = BTreeMap::new();
for object in &cir.objects { for object in cir.validated_objects() {
object_hash_by_uri.insert(object.rsync_uri.clone(), hex::encode(&object.sha256)); object_hash_by_uri.insert(object.rsync_uri.clone(), hex::encode(&object.sha256));
} }

View File

@ -56,9 +56,24 @@ fn real_main() -> Result<(), String> {
.map_err(|e| format!("read cir failed: {}: {e}", cir_path.display()))?; .map_err(|e| format!("read cir failed: {}: {e}", cir_path.display()))?;
let cir = rpki::cir::decode_cir(&bytes).map_err(|e| format!("decode cir failed: {e}"))?; let cir = rpki::cir::decode_cir(&bytes).map_err(|e| format!("decode cir failed: {e}"))?;
println!("object_count={}", cir.objects.len()); println!("version={}", cir.version);
println!("object_count={}", cir.validated_object_count());
println!("fresh_object_count={}", cir.fresh_validated_objects.len());
println!("cached_object_count={}", cir.cached_validated_objects.len());
println!(
"object_list_sha256={}",
hex::encode(&cir.object_list_sha256)
);
println!(
"fresh_object_list_sha256={}",
hex::encode(&cir.fresh_object_list_sha256)
);
println!(
"cached_object_list_sha256={}",
hex::encode(&cir.cached_object_list_sha256)
);
println!("trust_anchor_count={}", cir.trust_anchors.len()); println!("trust_anchor_count={}", cir.trust_anchors.len());
for (index, item) in cir.objects.iter().take(args.limit).enumerate() { for (index, item) in cir.validated_objects().take(args.limit).enumerate() {
println!( println!(
"{:04} object={} sha256={}", "{:04} object={} sha256={}",
index + 1, index + 1,
@ -70,8 +85,18 @@ fn real_main() -> Result<(), String> {
"reject_list_sha256={}", "reject_list_sha256={}",
hex::encode(&cir.reject_list_sha256) hex::encode(&cir.reject_list_sha256)
); );
println!("reject_count={}", cir.rejected_objects.len()); println!("reject_count={}", cir.rejected_object_count());
for (index, item) in cir.rejected_objects.iter().take(args.limit).enumerate() { println!("fresh_reject_count={}", cir.fresh_rejected_objects.len());
println!("cached_reject_count={}", cir.cached_rejected_objects.len());
println!(
"fresh_reject_list_sha256={}",
hex::encode(&cir.fresh_reject_list_sha256)
);
println!(
"cached_reject_list_sha256={}",
hex::encode(&cir.cached_reject_list_sha256)
);
for (index, item) in cir.rejected_objects().take(args.limit).enumerate() {
println!( println!(
"{:04} uri={} reason={}", "{:04} uri={} reason={}",
index + 1, index + 1,

View File

@ -108,13 +108,11 @@ fn run(args: Args) -> Result<(), String> {
let peer = decode_cir(&read_file(peer_cir_path)?) let peer = decode_cir(&read_file(peer_cir_path)?)
.map_err(|e| format!("decode rpki-client CIR failed: {e}"))?; .map_err(|e| format!("decode rpki-client CIR failed: {e}"))?;
let peer_objects = peer let peer_objects = peer
.objects .validated_objects()
.iter()
.map(|item| item.rsync_uri.as_str()) .map(|item| item.rsync_uri.as_str())
.collect::<BTreeSet<_>>(); .collect::<BTreeSet<_>>();
let only_in_ours = ours let only_in_ours = ours
.objects .validated_objects()
.iter()
.filter(|item| !peer_objects.contains(item.rsync_uri.as_str())) .filter(|item| !peer_objects.contains(item.rsync_uri.as_str()))
.map(|item| ProbeObject { .map(|item| ProbeObject {
uri: item.rsync_uri.clone(), uri: item.rsync_uri.clone(),
@ -442,10 +440,7 @@ fn uri_extension(uri: &str) -> String {
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;
use rpki::cir::{ use rpki::cir::{CanonicalInputRepresentation, CirObject, CirTrustAnchor, encode_cir};
CIR_VERSION_V3, CanonicalInputRepresentation, CirHashAlgorithm, CirObject,
CirRejectedObject, CirTrustAnchor, compute_reject_list_sha256, encode_cir,
};
#[test] #[test]
fn parse_args_accepts_required_flags() { fn parse_args_accepts_required_flags() {
@ -663,15 +658,14 @@ mod tests {
}) })
.collect::<Vec<_>>(); .collect::<Vec<_>>();
objects.sort_by(|left, right| left.rsync_uri.cmp(&right.rsync_uri)); objects.sort_by(|left, right| left.rsync_uri.cmp(&right.rsync_uri));
CanonicalInputRepresentation { CanonicalInputRepresentation::new_v4(
version: CIR_VERSION_V3, time::OffsetDateTime::UNIX_EPOCH,
hash_alg: CirHashAlgorithm::Sha256,
validation_time: time::OffsetDateTime::UNIX_EPOCH,
objects, objects,
trust_anchors: vec![sample_trust_anchor()], Vec::new(),
reject_list_sha256: compute_reject_list_sha256(std::iter::empty::<&str>()), vec![sample_trust_anchor()],
rejected_objects: Vec::<CirRejectedObject>::new(), Vec::new(),
} Vec::new(),
)
} }
fn sample_trust_anchor() -> CirTrustAnchor { fn sample_trust_anchor() -> CirTrustAnchor {

View File

@ -95,23 +95,19 @@ fn run(args: Args) -> Result<(), String> {
.map_err(|e| format!("decode rpki-client CIR failed: {e}"))?; .map_err(|e| format!("decode rpki-client CIR failed: {e}"))?;
let ours_objects = ours let ours_objects = ours
.objects .validated_objects()
.iter()
.map(|item| (item.rsync_uri.clone(), hex::encode(&item.sha256))) .map(|item| (item.rsync_uri.clone(), hex::encode(&item.sha256)))
.collect::<BTreeMap<_, _>>(); .collect::<BTreeMap<_, _>>();
let peer_objects = peer let peer_objects = peer
.objects .validated_objects()
.iter()
.map(|item| (item.rsync_uri.clone(), hex::encode(&item.sha256))) .map(|item| (item.rsync_uri.clone(), hex::encode(&item.sha256)))
.collect::<BTreeMap<_, _>>(); .collect::<BTreeMap<_, _>>();
let ours_rejects = ours let ours_rejects = ours
.rejected_objects .rejected_objects()
.iter()
.map(|item| item.object_uri.clone()) .map(|item| item.object_uri.clone())
.collect::<BTreeSet<_>>(); .collect::<BTreeSet<_>>();
let peer_rejects = peer let peer_rejects = peer
.rejected_objects .rejected_objects()
.iter()
.map(|item| item.object_uri.clone()) .map(|item| item.object_uri.clone())
.collect::<BTreeSet<_>>(); .collect::<BTreeSet<_>>();
let ours_trust_anchors = ours let ours_trust_anchors = ours
@ -153,18 +149,18 @@ fn run(args: Args) -> Result<(), String> {
"trust_anchors": trust_anchor_summary.to_json(), "trust_anchors": trust_anchor_summary.to_json(),
"rejectListSha256Match": reject_hash_match, "rejectListSha256Match": reject_hash_match,
"ours": { "ours": {
"objectCount": ours.objects.len(), "objectCount": ours.validated_object_count(),
"talCount": ours.trust_anchors.len(), "talCount": ours.trust_anchors.len(),
"trustAnchorCount": ours.trust_anchors.len(), "trustAnchorCount": ours.trust_anchors.len(),
"rejectCount": ours.rejected_objects.len(), "rejectCount": ours.rejected_object_count(),
"rejectListSha256": hex::encode(&ours.reject_list_sha256), "rejectListSha256": hex::encode(&ours.reject_list_sha256),
"validationTime": ours.validation_time.to_string(), "validationTime": ours.validation_time.to_string(),
}, },
"rpkiClient": { "rpkiClient": {
"objectCount": peer.objects.len(), "objectCount": peer.validated_object_count(),
"talCount": peer.trust_anchors.len(), "talCount": peer.trust_anchors.len(),
"trustAnchorCount": peer.trust_anchors.len(), "trustAnchorCount": peer.trust_anchors.len(),
"rejectCount": peer.rejected_objects.len(), "rejectCount": peer.rejected_object_count(),
"rejectListSha256": hex::encode(&peer.reject_list_sha256), "rejectListSha256": hex::encode(&peer.reject_list_sha256),
"validationTime": peer.validation_time.to_string(), "validationTime": peer.validation_time.to_string(),
} }
@ -428,8 +424,8 @@ fn uri_extension(uri: &str) -> String {
mod tests { mod tests {
use super::*; use super::*;
use rpki::cir::{ use rpki::cir::{
CIR_VERSION_V3, CanonicalInputRepresentation, CirHashAlgorithm, CirObject, CanonicalInputRepresentation, CirObject, CirRejectedObject, CirTrustAnchor, encode_cir,
CirRejectedObject, CirTrustAnchor, compute_reject_list_sha256, encode_cir, sha256, sha256,
}; };
#[test] #[test]
@ -533,7 +529,7 @@ mod tests {
assert_eq!(summary["rejects"]["match"], true); assert_eq!(summary["rejects"]["match"], true);
assert_eq!(summary["trustAnchors"]["match"], true); assert_eq!(summary["trustAnchors"]["match"], true);
assert_eq!(summary["rejectListSha256Match"], true); assert_eq!(summary["rejectListSha256Match"], true);
assert_eq!(summary["ours"]["objectCount"], 2); assert_eq!(summary["ours"]["objectCount"], 3);
assert!( assert!(
std::fs::read_to_string(out_md) std::fs::read_to_string(out_md)
.expect("read markdown") .expect("read markdown")
@ -721,6 +717,24 @@ mod tests {
trust_anchors: &[&str], trust_anchors: &[&str],
rejected_objects: &[(&str, Option<&str>)], rejected_objects: &[(&str, Option<&str>)],
) -> CanonicalInputRepresentation { ) -> CanonicalInputRepresentation {
let mut cir_objects = objects
.iter()
.map(|(rsync_uri, fill)| CirObject {
rsync_uri: (*rsync_uri).to_string(),
sha256: vec![*fill; 32],
})
.collect::<Vec<_>>();
for (object_uri, _) in rejected_objects {
if cir_objects
.iter()
.all(|object| object.rsync_uri != *object_uri)
{
cir_objects.push(CirObject {
rsync_uri: (*object_uri).to_string(),
sha256: vec![0xee; 32],
});
}
}
let rejected_objects = rejected_objects let rejected_objects = rejected_objects
.iter() .iter()
.map(|(object_uri, reason)| CirRejectedObject { .map(|(object_uri, reason)| CirRejectedObject {
@ -728,18 +742,11 @@ mod tests {
reason: reason.map(str::to_string), reason: reason.map(str::to_string),
}) })
.collect::<Vec<_>>(); .collect::<Vec<_>>();
CanonicalInputRepresentation { CanonicalInputRepresentation::new_v4(
version: CIR_VERSION_V3, sample_time(),
hash_alg: CirHashAlgorithm::Sha256, cir_objects,
validation_time: sample_time(), Vec::new(),
objects: objects trust_anchors
.iter()
.map(|(rsync_uri, fill)| CirObject {
rsync_uri: (*rsync_uri).to_string(),
sha256: vec![*fill; 32],
})
.collect(),
trust_anchors: trust_anchors
.iter() .iter()
.map(|tal_uri| { .map(|tal_uri| {
let name = tal_uri let name = tal_uri
@ -758,11 +765,9 @@ mod tests {
} }
}) })
.collect(), .collect(),
reject_list_sha256: compute_reject_list_sha256(
rejected_objects.iter().map(|item| item.object_uri.as_str()),
),
rejected_objects, rejected_objects,
} Vec::new(),
)
} }
fn write_cir(path: &Path, cir: &CanonicalInputRepresentation) { fn write_cir(path: &Path, cir: &CanonicalInputRepresentation) {

View File

@ -1,9 +1,6 @@
use std::path::PathBuf; use std::path::PathBuf;
use rpki::cir::{ use rpki::cir::{CanonicalInputRepresentation, CirTrustAnchor, encode_cir, sha256};
CIR_VERSION_V3, CanonicalInputRepresentation, CirHashAlgorithm, CirTrustAnchor,
compute_reject_list_sha256, encode_cir, sha256,
};
const USAGE: &str = "Usage: cir_ta_only_fixture --tal-path <path> --ta-path <path> --tal-uri <url> --validation-time <rfc3339> --cir-out <path> --repo-bytes-db <path>"; const USAGE: &str = "Usage: cir_ta_only_fixture --tal-path <path> --ta-path <path> --tal-uri <url> --validation-time <rfc3339> --cir-out <path> --repo-bytes-db <path>";
@ -102,21 +99,20 @@ fn main() -> Result<(), String> {
let ta_certificate_sha256 = sha256(&ta_bytes); let ta_certificate_sha256 = sha256(&ta_bytes);
let cir = CanonicalInputRepresentation { let cir = CanonicalInputRepresentation::new_v4(
version: CIR_VERSION_V3,
hash_alg: CirHashAlgorithm::Sha256,
validation_time, validation_time,
objects: Vec::new(), Vec::new(),
trust_anchors: vec![CirTrustAnchor { Vec::new(),
vec![CirTrustAnchor {
ta_rsync_uri, ta_rsync_uri,
tal_uri, tal_uri,
tal_bytes, tal_bytes,
ta_certificate_der: ta_bytes, ta_certificate_der: ta_bytes,
ta_certificate_sha256, ta_certificate_sha256,
}], }],
reject_list_sha256: compute_reject_list_sha256(std::iter::empty::<&str>()), Vec::new(),
rejected_objects: Vec::new(), Vec::new(),
}; );
let der = encode_cir(&cir).map_err(|e| format!("encode cir failed: {e}"))?; let der = encode_cir(&cir).map_err(|e| format!("encode cir failed: {e}"))?;
if let Some(parent) = cir_out.parent() { if let Some(parent) = cir_out.parent() {
std::fs::create_dir_all(parent).map_err(|e| format!("create cir parent failed: {e}"))?; std::fs::create_dir_all(parent).map_err(|e| format!("create cir parent failed: {e}"))?;

View File

@ -273,23 +273,19 @@ fn build_cir_summary(args: &Args) -> Result<Value, String> {
let left = decode_cir(&read_file(&args.left_cir)?).map_err(|e| e.to_string())?; let left = decode_cir(&read_file(&args.left_cir)?).map_err(|e| e.to_string())?;
let right = decode_cir(&read_file(&args.right_cir)?).map_err(|e| e.to_string())?; let right = decode_cir(&read_file(&args.right_cir)?).map_err(|e| e.to_string())?;
let left_objects = left let left_objects = left
.objects .validated_objects()
.iter()
.map(|item| (item.rsync_uri.clone(), hex::encode(&item.sha256))) .map(|item| (item.rsync_uri.clone(), hex::encode(&item.sha256)))
.collect::<BTreeMap<_, _>>(); .collect::<BTreeMap<_, _>>();
let right_objects = right let right_objects = right
.objects .validated_objects()
.iter()
.map(|item| (item.rsync_uri.clone(), hex::encode(&item.sha256))) .map(|item| (item.rsync_uri.clone(), hex::encode(&item.sha256)))
.collect::<BTreeMap<_, _>>(); .collect::<BTreeMap<_, _>>();
let left_rejects = left let left_rejects = left
.rejected_objects .rejected_objects()
.iter()
.map(|item| item.object_uri.clone()) .map(|item| item.object_uri.clone())
.collect::<BTreeSet<_>>(); .collect::<BTreeSet<_>>();
let right_rejects = right let right_rejects = right
.rejected_objects .rejected_objects()
.iter()
.map(|item| item.object_uri.clone()) .map(|item| item.object_uri.clone())
.collect::<BTreeSet<_>>(); .collect::<BTreeSet<_>>();
let left_trust_anchors = left let left_trust_anchors = left
@ -338,15 +334,15 @@ fn build_cir_summary(args: &Args) -> Result<Value, String> {
"rejectListSha256Match": reject_hash_match, "rejectListSha256Match": reject_hash_match,
"validationTimeMatch": validation_time_match, "validationTimeMatch": validation_time_match,
"left": { "left": {
"objectCount": left.objects.len(), "objectCount": left.validated_object_count(),
"rejectCount": left.rejected_objects.len(), "rejectCount": left.rejected_object_count(),
"trustAnchorCount": left.trust_anchors.len(), "trustAnchorCount": left.trust_anchors.len(),
"rejectListSha256": hex::encode(&left.reject_list_sha256), "rejectListSha256": hex::encode(&left.reject_list_sha256),
"validationTime": format_time(left.validation_time)?, "validationTime": format_time(left.validation_time)?,
}, },
"right": { "right": {
"objectCount": right.objects.len(), "objectCount": right.validated_object_count(),
"rejectCount": right.rejected_objects.len(), "rejectCount": right.rejected_object_count(),
"trustAnchorCount": right.trust_anchors.len(), "trustAnchorCount": right.trust_anchors.len(),
"rejectListSha256": hex::encode(&right.reject_list_sha256), "rejectListSha256": hex::encode(&right.reject_list_sha256),
"validationTime": format_time(right.validation_time)?, "validationTime": format_time(right.validation_time)?,
@ -977,8 +973,8 @@ mod tests {
build_aspa_payload_state, build_roa_payload_state, encode_content_info, build_aspa_payload_state, build_roa_payload_state, encode_content_info,
}; };
use rpki::cir::{ use rpki::cir::{
CIR_VERSION_V3, CanonicalInputRepresentation, CirHashAlgorithm, CirObject, CanonicalInputRepresentation, CirObject, CirRejectedObject, CirTrustAnchor, encode_cir,
CirRejectedObject, CirTrustAnchor, compute_reject_list_sha256, encode_cir, sha256, sha256,
}; };
use rpki::data_model::roa::{IpPrefix, RoaAfi}; use rpki::data_model::roa::{IpPrefix, RoaAfi};
use rpki::validation::objects::{AspaAttestation, Vrp}; use rpki::validation::objects::{AspaAttestation, Vrp};
@ -1193,6 +1189,7 @@ mod tests {
codes, codes,
vec![ vec![
"P1_TRUST_ANCHOR_DIFFERENCE", "P1_TRUST_ANCHOR_DIFFERENCE",
"P2_OBJECT_URI_SET_DIFFERENCE",
"P3_OBJECT_CONTENT_HASH_DIFFERENCE", "P3_OBJECT_CONTENT_HASH_DIFFERENCE",
"P4_REJECT_DECISION_DIFFERENCE", "P4_REJECT_DECISION_DIFFERENCE",
"P5_VALIDATION_TIME_DIFFERENCE", "P5_VALIDATION_TIME_DIFFERENCE",
@ -1392,20 +1389,30 @@ mod tests {
}] }]
}) })
.unwrap_or_default(); .unwrap_or_default();
CanonicalInputRepresentation { let mut objects = vec![CirObject {
version: CIR_VERSION_V3, rsync_uri: "rsync://example.net/repo/a.roa".to_string(),
hash_alg: CirHashAlgorithm::Sha256, sha256: vec![object_hash_fill; 32],
validation_time, }];
objects: vec![CirObject { for rejected in &rejected_objects {
rsync_uri: "rsync://example.net/repo/a.roa".to_string(), if !objects
sha256: vec![object_hash_fill; 32], .iter()
}], .any(|object| object.rsync_uri == rejected.object_uri)
trust_anchors: vec![trust_anchor], {
reject_list_sha256: compute_reject_list_sha256( objects.push(CirObject {
rejected_objects.iter().map(|item| item.object_uri.as_str()), rsync_uri: rejected.object_uri.clone(),
), sha256: vec![object_hash_fill; 32],
rejected_objects, });
}
} }
objects.sort_by(|a, b| a.rsync_uri.cmp(&b.rsync_uri));
CanonicalInputRepresentation::new_v4(
validation_time,
objects,
Vec::new(),
vec![trust_anchor],
rejected_objects,
Vec::new(),
)
} }
fn sample_trust_anchor(name: &str) -> CirTrustAnchor { fn sample_trust_anchor(name: &str) -> CirTrustAnchor {

View File

@ -1,5 +1,5 @@
use crate::cir::model::{ use crate::cir::model::{
CIR_VERSION_V3, CanonicalInputRepresentation, CirHashAlgorithm, CirObject, CirRejectedObject, CIR_VERSION_V4, CanonicalInputRepresentation, CirHashAlgorithm, CirObject, CirRejectedObject,
CirTrustAnchor, CirTrustAnchor,
}; };
use crate::data_model::common::DerReader; use crate::data_model::common::DerReader;
@ -32,9 +32,9 @@ pub fn decode_cir(der: &[u8]) -> Result<CanonicalInputRepresentation, CirDecodeE
} }
let version = seq.take_uint_u64().map_err(CirDecodeError::Parse)? as u32; let version = seq.take_uint_u64().map_err(CirDecodeError::Parse)? as u32;
if version != CIR_VERSION_V3 { if version != CIR_VERSION_V4 {
return Err(CirDecodeError::UnexpectedVersion { return Err(CirDecodeError::UnexpectedVersion {
expected: CIR_VERSION_V3, expected: CIR_VERSION_V4,
actual: version, actual: version,
}); });
} }
@ -42,14 +42,24 @@ pub fn decode_cir(der: &[u8]) -> Result<CanonicalInputRepresentation, CirDecodeE
let validation_time = let validation_time =
parse_generalized_time(seq.take_tag(0x18).map_err(CirDecodeError::Parse)?)?; parse_generalized_time(seq.take_tag(0x18).map_err(CirDecodeError::Parse)?)?;
let objects_der = seq.take_tag(0x30).map_err(CirDecodeError::Parse)?; let fresh_objects_der = seq.take_tag(0x30).map_err(CirDecodeError::Parse)?;
let mut objects_reader = DerReader::new(objects_der); let mut fresh_objects_reader = DerReader::new(fresh_objects_der);
let mut objects = Vec::new(); let mut fresh_validated_objects = Vec::new();
while !objects_reader.is_empty() { while !fresh_objects_reader.is_empty() {
let (_tag, full, _value) = objects_reader let (_tag, full, _value) = fresh_objects_reader
.take_any_full() .take_any_full()
.map_err(CirDecodeError::Parse)?; .map_err(CirDecodeError::Parse)?;
objects.push(decode_object(full)?); fresh_validated_objects.push(decode_object(full)?);
}
let cached_objects_der = seq.take_tag(0x30).map_err(CirDecodeError::Parse)?;
let mut cached_objects_reader = DerReader::new(cached_objects_der);
let mut cached_validated_objects = Vec::new();
while !cached_objects_reader.is_empty() {
let (_tag, full, _value) = cached_objects_reader
.take_any_full()
.map_err(CirDecodeError::Parse)?;
cached_validated_objects.push(decode_object(full)?);
} }
let trust_anchors_der = seq.take_tag(0x30).map_err(CirDecodeError::Parse)?; let trust_anchors_der = seq.take_tag(0x30).map_err(CirDecodeError::Parse)?;
@ -62,19 +72,49 @@ pub fn decode_cir(der: &[u8]) -> Result<CanonicalInputRepresentation, CirDecodeE
trust_anchors.push(decode_trust_anchor(full)?); trust_anchors.push(decode_trust_anchor(full)?);
} }
let object_list_sha256 = seq
.take_octet_string()
.map_err(CirDecodeError::Parse)?
.to_vec();
let fresh_object_list_sha256 = seq
.take_octet_string()
.map_err(CirDecodeError::Parse)?
.to_vec();
let cached_object_list_sha256 = seq
.take_octet_string()
.map_err(CirDecodeError::Parse)?
.to_vec();
let reject_list_sha256 = seq let reject_list_sha256 = seq
.take_octet_string() .take_octet_string()
.map_err(CirDecodeError::Parse)? .map_err(CirDecodeError::Parse)?
.to_vec(); .to_vec();
let fresh_reject_list_sha256 = seq
.take_octet_string()
.map_err(CirDecodeError::Parse)?
.to_vec();
let cached_reject_list_sha256 = seq
.take_octet_string()
.map_err(CirDecodeError::Parse)?
.to_vec();
let rejected_der = seq.take_tag(0x30).map_err(CirDecodeError::Parse)?; let fresh_rejected_der = seq.take_tag(0x30).map_err(CirDecodeError::Parse)?;
let mut rejected_reader = DerReader::new(rejected_der); let mut fresh_rejected_reader = DerReader::new(fresh_rejected_der);
let mut rejected_objects = Vec::new(); let mut fresh_rejected_objects = Vec::new();
while !rejected_reader.is_empty() { while !fresh_rejected_reader.is_empty() {
let (_tag, full, _value) = rejected_reader let (_tag, full, _value) = fresh_rejected_reader
.take_any_full() .take_any_full()
.map_err(CirDecodeError::Parse)?; .map_err(CirDecodeError::Parse)?;
rejected_objects.push(decode_rejected_object(full)?); fresh_rejected_objects.push(decode_rejected_object(full)?);
}
let cached_rejected_der = seq.take_tag(0x30).map_err(CirDecodeError::Parse)?;
let mut cached_rejected_reader = DerReader::new(cached_rejected_der);
let mut cached_rejected_objects = Vec::new();
while !cached_rejected_reader.is_empty() {
let (_tag, full, _value) = cached_rejected_reader
.take_any_full()
.map_err(CirDecodeError::Parse)?;
cached_rejected_objects.push(decode_rejected_object(full)?);
} }
if !seq.is_empty() { if !seq.is_empty() {
@ -85,10 +125,17 @@ pub fn decode_cir(der: &[u8]) -> Result<CanonicalInputRepresentation, CirDecodeE
version, version,
hash_alg, hash_alg,
validation_time, validation_time,
objects, fresh_validated_objects,
cached_validated_objects,
trust_anchors, trust_anchors,
object_list_sha256,
fresh_object_list_sha256,
cached_object_list_sha256,
reject_list_sha256, reject_list_sha256,
rejected_objects, fresh_reject_list_sha256,
cached_reject_list_sha256,
fresh_rejected_objects,
cached_rejected_objects,
}; };
cir.validate().map_err(CirDecodeError::Validate)?; cir.validate().map_err(CirDecodeError::Validate)?;
Ok(cir) Ok(cir)

View File

@ -1,5 +1,5 @@
use crate::cir::model::{ use crate::cir::model::{
CIR_VERSION_V3, CanonicalInputRepresentation, CirHashAlgorithm, CirObject, CirRejectedObject, CIR_VERSION_V4, CanonicalInputRepresentation, CirHashAlgorithm, CirObject, CirRejectedObject,
CirTrustAnchor, CirTrustAnchor,
}; };
use crate::data_model::oid::OID_SHA256_RAW; use crate::data_model::oid::OID_SHA256_RAW;
@ -13,13 +13,19 @@ pub enum CirEncodeError {
pub fn encode_cir(cir: &CanonicalInputRepresentation) -> Result<Vec<u8>, CirEncodeError> { pub fn encode_cir(cir: &CanonicalInputRepresentation) -> Result<Vec<u8>, CirEncodeError> {
cir.validate().map_err(CirEncodeError::Validate)?; cir.validate().map_err(CirEncodeError::Validate)?;
Ok(encode_sequence(&[ Ok(encode_sequence(&[
encode_integer_u32(cir.version), encode_integer_u32(CIR_VERSION_V4),
encode_oid(match cir.hash_alg { encode_oid(match cir.hash_alg {
CirHashAlgorithm::Sha256 => OID_SHA256_RAW, CirHashAlgorithm::Sha256 => OID_SHA256_RAW,
}), }),
encode_generalized_time(cir.validation_time), encode_generalized_time(cir.validation_time),
encode_sequence( encode_sequence(
&cir.objects &cir.fresh_validated_objects
.iter()
.map(encode_object)
.collect::<Result<Vec<_>, _>>()?,
),
encode_sequence(
&cir.cached_validated_objects
.iter() .iter()
.map(encode_object) .map(encode_object)
.collect::<Result<Vec<_>, _>>()?, .collect::<Result<Vec<_>, _>>()?,
@ -30,9 +36,20 @@ pub fn encode_cir(cir: &CanonicalInputRepresentation) -> Result<Vec<u8>, CirEnco
.map(encode_trust_anchor) .map(encode_trust_anchor)
.collect::<Result<Vec<_>, _>>()?, .collect::<Result<Vec<_>, _>>()?,
), ),
encode_octet_string(&cir.object_list_sha256),
encode_octet_string(&cir.fresh_object_list_sha256),
encode_octet_string(&cir.cached_object_list_sha256),
encode_octet_string(&cir.reject_list_sha256), encode_octet_string(&cir.reject_list_sha256),
encode_octet_string(&cir.fresh_reject_list_sha256),
encode_octet_string(&cir.cached_reject_list_sha256),
encode_sequence( encode_sequence(
&cir.rejected_objects &cir.fresh_rejected_objects
.iter()
.map(encode_rejected_object)
.collect::<Result<Vec<_>, _>>()?,
),
encode_sequence(
&cir.cached_rejected_objects
.iter() .iter()
.map(encode_rejected_object) .map(encode_rejected_object)
.collect::<Result<Vec<_>, _>>()?, .collect::<Result<Vec<_>, _>>()?,
@ -160,8 +177,3 @@ fn encode_len_into(len: usize, out: &mut Vec<u8>) {
out.push(0x80 | (len_bytes.len() as u8)); out.push(0x80 | (len_bytes.len() as u8));
out.extend_from_slice(len_bytes); out.extend_from_slice(len_bytes);
} }
#[allow(dead_code)]
const _: () = {
let _ = CIR_VERSION_V3;
};

View File

@ -2,11 +2,10 @@ use std::collections::BTreeMap;
use std::collections::BTreeSet; use std::collections::BTreeSet;
use std::path::Path; use std::path::Path;
use crate::audit::{AuditObjectResult, PublicationPointAudit}; use crate::audit::{AuditObjectResult, ObjectAuditEntry, PublicationPointAudit};
use crate::cir::encode::{CirEncodeError, encode_cir}; use crate::cir::encode::{CirEncodeError, encode_cir};
use crate::cir::model::{ use crate::cir::model::{
CIR_VERSION_V3, CanonicalInputRepresentation, CirHashAlgorithm, CirObject, CirRejectedObject, CanonicalInputRepresentation, CirObject, CirRejectedObject, CirTrustAnchor,
CirTrustAnchor, compute_reject_list_sha256,
}; };
use crate::cir::static_pool::{ use crate::cir::static_pool::{
CirStaticPoolError, CirStaticPoolExportSummary, export_hashes_from_store, CirStaticPoolError, CirStaticPoolExportSummary, export_hashes_from_store,
@ -99,12 +98,45 @@ fn insert_consumed_object_hash(
Ok(()) Ok(())
} }
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
enum CirObjectSection {
Fresh,
Cached,
}
fn publication_point_uses_explicit_cir_sections(pp: &PublicationPointAudit) -> bool {
!pp.cir_fresh_objects.is_empty() || !pp.cir_cached_objects.is_empty()
}
fn publication_point_is_cached_fallback(pp: &PublicationPointAudit) -> bool {
pp.source == "vcir_current_instance" || pp.repo_terminal_state == "fallback_current_instance"
}
fn publication_point_cir_entries<'a>(
pp: &'a PublicationPointAudit,
section: CirObjectSection,
) -> &'a [ObjectAuditEntry] {
if publication_point_uses_explicit_cir_sections(pp) {
return match section {
CirObjectSection::Fresh => &pp.cir_fresh_objects,
CirObjectSection::Cached => &pp.cir_cached_objects,
};
}
match (section, publication_point_is_cached_fallback(pp)) {
(CirObjectSection::Fresh, false) => &pp.objects,
(CirObjectSection::Cached, true) => &pp.objects,
_ => &[],
}
}
fn collect_cir_objects_from_validation_audit( fn collect_cir_objects_from_validation_audit(
publication_points: &[PublicationPointAudit], publication_points: &[PublicationPointAudit],
section: CirObjectSection,
) -> Result<BTreeMap<String, String>, CirExportError> { ) -> Result<BTreeMap<String, String>, CirExportError> {
let mut objects = BTreeMap::new(); let mut objects = BTreeMap::new();
for pp in publication_points { for pp in publication_points {
for obj in &pp.objects { for obj in publication_point_cir_entries(pp, section) {
if !matches!(obj.result, AuditObjectResult::Ok | AuditObjectResult::Error) { if !matches!(obj.result, AuditObjectResult::Ok | AuditObjectResult::Error) {
continue; continue;
} }
@ -114,6 +146,35 @@ fn collect_cir_objects_from_validation_audit(
Ok(objects) Ok(objects)
} }
fn collect_rejected_objects_from_validation_audit(
publication_points: &[PublicationPointAudit],
section: CirObjectSection,
) -> Vec<CirRejectedObject> {
let mut rejected_objects = publication_points
.iter()
.flat_map(|pp| publication_point_cir_entries(pp, section).iter())
.filter(|item| item.result == AuditObjectResult::Error)
.filter(|item| item.rsync_uri.starts_with("rsync://"))
.map(|item| CirRejectedObject {
object_uri: item.rsync_uri.clone(),
reason: item.detail.clone(),
})
.collect::<Vec<_>>();
rejected_objects.sort_by(|a, b| a.object_uri.cmp(&b.object_uri));
rejected_objects.dedup_by(|a, b| a.object_uri == b.object_uri);
rejected_objects
}
fn cir_objects_from_hash_map(objects: BTreeMap<String, String>) -> Vec<CirObject> {
objects
.into_iter()
.map(|(rsync_uri, sha256_hex)| CirObject {
rsync_uri,
sha256: hex::decode(sha256_hex).expect("validated hex"),
})
.collect()
}
fn canonical_ta_rsync_uri(trust_anchor: &TrustAnchor) -> Result<String, CirExportError> { fn canonical_ta_rsync_uri(trust_anchor: &TrustAnchor) -> Result<String, CirExportError> {
if let Some(uri) = &trust_anchor.resolved_ta_uri if let Some(uri) = &trust_anchor.resolved_ta_uri
&& uri.scheme() == "rsync" && uri.scheme() == "rsync"
@ -162,7 +223,10 @@ pub fn build_cir_from_run_multi(
} }
} }
let objects = collect_cir_objects_from_validation_audit(publication_points)?; let fresh_objects =
collect_cir_objects_from_validation_audit(publication_points, CirObjectSection::Fresh)?;
let cached_objects =
collect_cir_objects_from_validation_audit(publication_points, CirObjectSection::Cached)?;
let mut trust_anchors = Vec::with_capacity(tal_bindings.len()); let mut trust_anchors = Vec::with_capacity(tal_bindings.len());
for binding in tal_bindings { for binding in tal_bindings {
@ -178,41 +242,21 @@ pub fn build_cir_from_run_multi(
} }
trust_anchors.sort_by(|a, b| a.ta_rsync_uri.cmp(&b.ta_rsync_uri)); trust_anchors.sort_by(|a, b| a.ta_rsync_uri.cmp(&b.ta_rsync_uri));
let cir = CanonicalInputRepresentation { let fresh_rejected_objects =
version: CIR_VERSION_V3, collect_rejected_objects_from_validation_audit(publication_points, CirObjectSection::Fresh);
hash_alg: CirHashAlgorithm::Sha256, let cached_rejected_objects = collect_rejected_objects_from_validation_audit(
validation_time: validation_time.to_offset(time::UtcOffset::UTC), publication_points,
objects: objects CirObjectSection::Cached,
.into_iter() );
.map(|(rsync_uri, sha256_hex)| CirObject {
rsync_uri,
sha256: hex::decode(sha256_hex).expect("validated hex"),
})
.collect(),
trust_anchors,
reject_list_sha256: Vec::new(),
rejected_objects: Vec::new(),
};
let mut rejected_objects = publication_points
.iter()
.flat_map(|pp| pp.objects.iter())
.filter(|item| item.result == AuditObjectResult::Error)
.filter(|item| item.rsync_uri.starts_with("rsync://"))
.map(|item| CirRejectedObject {
object_uri: item.rsync_uri.clone(),
reason: item.detail.clone(),
})
.collect::<Vec<_>>();
rejected_objects.sort_by(|a, b| a.object_uri.cmp(&b.object_uri));
rejected_objects.dedup_by(|a, b| a.object_uri == b.object_uri);
let cir = CanonicalInputRepresentation { let cir = CanonicalInputRepresentation::new_v4(
reject_list_sha256: compute_reject_list_sha256( validation_time,
rejected_objects.iter().map(|item| item.object_uri.as_str()), cir_objects_from_hash_map(fresh_objects),
), cir_objects_from_hash_map(cached_objects),
rejected_objects, trust_anchors,
..cir fresh_rejected_objects,
}; cached_rejected_objects,
);
cir.validate().map_err(CirExportError::Validate)?; cir.validate().map_err(CirExportError::Validate)?;
Ok(cir) Ok(cir)
} }
@ -239,8 +283,7 @@ pub fn export_cir_static_pool(
) -> Result<CirStaticPoolExportSummary, CirExportError> { ) -> Result<CirStaticPoolExportSummary, CirExportError> {
let _ = trust_anchors; let _ = trust_anchors;
let hashes = cir let hashes = cir
.objects .validated_objects()
.iter()
.map(|item| hex::encode(&item.sha256)) .map(|item| hex::encode(&item.sha256))
.collect::<Vec<_>>(); .collect::<Vec<_>>();
export_hashes_from_store(store, static_root, capture_date_utc, &hashes).map_err(Into::into) export_hashes_from_store(store, static_root, capture_date_utc, &hashes).map_err(Into::into)
@ -254,8 +297,7 @@ pub fn export_cir_raw_store(
) -> Result<CirRawStoreExportSummary, CirExportError> { ) -> Result<CirRawStoreExportSummary, CirExportError> {
let _ = trust_anchors; let _ = trust_anchors;
let unique: BTreeSet<String> = cir let unique: BTreeSet<String> = cir
.objects .validated_objects()
.iter()
.map(|item| hex::encode(&item.sha256)) .map(|item| hex::encode(&item.sha256))
.collect(); .collect();
@ -337,7 +379,7 @@ pub fn export_cir_from_run_multi(
let write_cir_ms = started.elapsed().as_millis() as u64; let write_cir_ms = started.elapsed().as_millis() as u64;
Ok(CirExportSummary { Ok(CirExportSummary {
object_count: cir.objects.len(), object_count: cir.validated_object_count(),
trust_anchor_count: cir.trust_anchors.len(), trust_anchor_count: cir.trust_anchors.len(),
timing: CirExportTiming { timing: CirExportTiming {
build_cir_ms, build_cir_ms,
@ -437,19 +479,19 @@ mod tests {
&publication_points, &publication_points,
) )
.expect("build cir"); .expect("build cir");
assert_eq!(cir.version, CIR_VERSION_V3); assert_eq!(cir.version, crate::cir::model::CIR_VERSION_V4);
assert_eq!(cir.trust_anchors.len(), 1); assert_eq!(cir.trust_anchors.len(), 1);
assert_eq!( assert_eq!(
cir.trust_anchors[0].tal_uri, cir.trust_anchors[0].tal_uri,
"https://example.test/root.tal" "https://example.test/root.tal"
); );
assert!( assert!(
cir.objects cir.fresh_validated_objects
.iter() .iter()
.any(|item| item.rsync_uri == "rsync://example.test/repo/a.cer") .any(|item| item.rsync_uri == "rsync://example.test/repo/a.cer")
); );
assert!( assert!(
!cir.objects !cir.fresh_validated_objects
.iter() .iter()
.any(|item| item.rsync_uri == cir.trust_anchors[0].ta_rsync_uri) .any(|item| item.rsync_uri == cir.trust_anchors[0].ta_rsync_uri)
); );
@ -610,12 +652,12 @@ mod tests {
.expect("build cir"); .expect("build cir");
assert!( assert!(
cir.objects cir.cached_validated_objects
.iter() .iter()
.any(|item| item.rsync_uri == "rsync://example.test/repo/fallback.mft") .any(|item| item.rsync_uri == "rsync://example.test/repo/fallback.mft")
); );
assert!( assert!(
cir.objects cir.cached_validated_objects
.iter() .iter()
.any(|item| item.rsync_uri == "rsync://example.test/repo/fallback.roa") .any(|item| item.rsync_uri == "rsync://example.test/repo/fallback.roa")
); );
@ -664,19 +706,19 @@ mod tests {
assert_eq!(cir.trust_anchors.len(), 2); assert_eq!(cir.trust_anchors.len(), 2);
assert!( assert!(
cir.objects cir.fresh_validated_objects
.iter() .iter()
.any(|item| item.rsync_uri == "rsync://example.test/repo/consumed.roa") .any(|item| item.rsync_uri == "rsync://example.test/repo/consumed.roa")
); );
assert!( assert!(
!cir.objects !cir.fresh_validated_objects
.iter() .iter()
.any(|item| item.rsync_uri == "rsync://example.test/repo/superfluous.roa"), .any(|item| item.rsync_uri == "rsync://example.test/repo/superfluous.roa"),
"current repo objects must not be included unless validation consumed them", "current repo objects must not be included unless validation consumed them",
); );
for trust_anchor in &cir.trust_anchors { for trust_anchor in &cir.trust_anchors {
assert!( assert!(
!cir.objects !cir.fresh_validated_objects
.iter() .iter()
.any(|item| item.rsync_uri == trust_anchor.ta_rsync_uri), .any(|item| item.rsync_uri == trust_anchor.ta_rsync_uri),
"trust anchor rsync objects must not be included in CIR.objects", "trust anchor rsync objects must not be included in CIR.objects",
@ -765,33 +807,33 @@ mod tests {
) )
.expect("build cir"); .expect("build cir");
assert_eq!(cir.rejected_objects.len(), 2); assert_eq!(cir.rejected_object_count(), 2);
assert_eq!( assert_eq!(
cir.rejected_objects[0].object_uri, cir.fresh_rejected_objects[0].object_uri,
"rsync://example.test/repo/a.roa" "rsync://example.test/repo/a.roa"
); );
assert_eq!( assert_eq!(
cir.rejected_objects[0].reason.as_deref(), cir.fresh_rejected_objects[0].reason.as_deref(),
Some("invalid roa") Some("invalid roa")
); );
assert_eq!( assert_eq!(
cir.rejected_objects[1].object_uri, cir.fresh_rejected_objects[1].object_uri,
"rsync://example.test/repo/c.roa" "rsync://example.test/repo/c.roa"
); );
assert!( assert!(
cir.objects cir.fresh_validated_objects
.iter() .iter()
.any(|item| item.rsync_uri == "rsync://example.test/repo/a.roa"), .any(|item| item.rsync_uri == "rsync://example.test/repo/a.roa"),
"rejected audit objects were still consumed as validation input", "rejected audit objects were still consumed as validation input",
); );
assert!( assert!(
cir.objects cir.fresh_validated_objects
.iter() .iter()
.any(|item| item.rsync_uri == "rsync://example.test/repo/c.roa"), .any(|item| item.rsync_uri == "rsync://example.test/repo/c.roa"),
"rejected audit objects were still consumed as validation input", "rejected audit objects were still consumed as validation input",
); );
assert!( assert!(
!cir.objects !cir.fresh_validated_objects
.iter() .iter()
.any(|item| item.rsync_uri == "rsync://example.test/repo/b.asa"), .any(|item| item.rsync_uri == "rsync://example.test/repo/b.asa"),
"skipped audit objects are not considered consumed input", "skipped audit objects are not considered consumed input",
@ -829,20 +871,20 @@ mod tests {
.expect("build cir"); .expect("build cir");
assert!( assert!(
cir.objects cir.fresh_validated_objects
.iter() .iter()
.any(|item| item.rsync_uri == manifest_uri), .any(|item| item.rsync_uri == manifest_uri),
"manifest rejected because issuer CRL is expired was still read as validation input", "manifest rejected because issuer CRL is expired was still read as validation input",
); );
assert_eq!(cir.rejected_objects.len(), 1); assert_eq!(cir.rejected_object_count(), 1);
assert_eq!(cir.rejected_objects[0].object_uri, manifest_uri); assert_eq!(cir.fresh_rejected_objects[0].object_uri, manifest_uri);
assert_eq!( assert_eq!(
cir.rejected_objects[0].reason.as_deref(), cir.fresh_rejected_objects[0].reason.as_deref(),
Some(reject_reason) Some(reject_reason)
); );
assert_eq!( assert_eq!(
cir.reject_list_sha256, cir.reject_list_sha256,
compute_reject_list_sha256([manifest_uri].into_iter()) crate::cir::model::compute_reject_list_sha256([manifest_uri].into_iter())
); );
} }
@ -894,21 +936,25 @@ mod tests {
.expect("build cir"); .expect("build cir");
assert!( assert!(
cir.objects cir.fresh_validated_objects
.iter() .iter()
.any(|item| item.rsync_uri == manifest_uri), .any(|item| item.rsync_uri == manifest_uri),
"rejected manifest is still a current-run validation input", "rejected manifest is still a current-run validation input",
); );
assert!( assert!(
!cir.objects.iter().any(|item| item.rsync_uri == roa_uri), !cir.fresh_validated_objects
.iter()
.any(|item| item.rsync_uri == roa_uri),
"ROA listed by a rejected manifest must not enter CIR objects", "ROA listed by a rejected manifest must not enter CIR objects",
); );
assert!( assert!(
!cir.objects.iter().any(|item| item.rsync_uri == crl_uri), !cir.fresh_validated_objects
.iter()
.any(|item| item.rsync_uri == crl_uri),
"CRL listed by a rejected manifest must not enter CIR objects", "CRL listed by a rejected manifest must not enter CIR objects",
); );
assert_eq!(cir.rejected_objects.len(), 1); assert_eq!(cir.rejected_object_count(), 1);
assert_eq!(cir.rejected_objects[0].object_uri, manifest_uri); assert_eq!(cir.fresh_rejected_objects[0].object_uri, manifest_uri);
} }
#[test] #[test]
@ -953,8 +999,8 @@ mod tests {
assert_eq!(cir_a.reject_list_sha256, cir_b.reject_list_sha256); assert_eq!(cir_a.reject_list_sha256, cir_b.reject_list_sha256);
assert_ne!( assert_ne!(
cir_a.rejected_objects[0].reason, cir_a.fresh_rejected_objects[0].reason,
cir_b.rejected_objects[0].reason cir_b.fresh_rejected_objects[0].reason
); );
} }
@ -1083,7 +1129,7 @@ mod tests {
assert_eq!(summary.reused_entries, 0); assert_eq!(summary.reused_entries, 0);
let mut cir_missing_object = cir_only_tas.clone(); let mut cir_missing_object = cir_only_tas.clone();
cir_missing_object.objects.push(CirObject { cir_missing_object.fresh_validated_objects.push(CirObject {
rsync_uri: "rsync://example.test/repo/missing.roa".to_string(), rsync_uri: "rsync://example.test/repo/missing.roa".to_string(),
sha256: vec![0x44; 32], sha256: vec![0x44; 32],
}); });

View File

@ -86,7 +86,7 @@ pub fn materialize_cir(
let mut linked_files = 0usize; let mut linked_files = 0usize;
let mut copied_files = 0usize; let mut copied_files = 0usize;
for object in &cir.objects { for object in cir.validated_objects() {
let sha256_hex = hex::encode(&object.sha256); let sha256_hex = hex::encode(&object.sha256);
let source = resolve_static_pool_file(static_root, &sha256_hex)?; let source = resolve_static_pool_file(static_root, &sha256_hex)?;
let relative = mirror_relative_path_for_rsync_uri(&object.rsync_uri)?; let relative = mirror_relative_path_for_rsync_uri(&object.rsync_uri)?;
@ -140,7 +140,7 @@ pub fn materialize_cir(
} }
Ok(CirMaterializeSummary { Ok(CirMaterializeSummary {
object_count: cir.objects.len(), object_count: cir.validated_object_count(),
trust_anchor_count: cir.trust_anchors.len(), trust_anchor_count: cir.trust_anchors.len(),
materialized_file_count: expected.len(), materialized_file_count: expected.len(),
linked_files, linked_files,
@ -165,7 +165,7 @@ pub fn materialize_cir_from_raw_store(
})?; })?;
let mut copied_files = 0usize; let mut copied_files = 0usize;
for object in &cir.objects { for object in cir.validated_objects() {
let sha256_hex = hex::encode(&object.sha256); let sha256_hex = hex::encode(&object.sha256);
let bytes = raw_store let bytes = raw_store
.get_blob_bytes(&sha256_hex) .get_blob_bytes(&sha256_hex)
@ -206,7 +206,7 @@ pub fn materialize_cir_from_raw_store(
} }
Ok(CirMaterializeSummary { Ok(CirMaterializeSummary {
object_count: cir.objects.len(), object_count: cir.validated_object_count(),
trust_anchor_count: cir.trust_anchors.len(), trust_anchor_count: cir.trust_anchors.len(),
materialized_file_count: expected.len(), materialized_file_count: expected.len(),
linked_files: 0, linked_files: 0,
@ -232,7 +232,7 @@ pub fn materialize_cir_from_repo_bytes(
})?; })?;
let mut copied_files = 0usize; let mut copied_files = 0usize;
for object in &cir.objects { for object in cir.validated_objects() {
let sha256_hex = hex::encode(&object.sha256); let sha256_hex = hex::encode(&object.sha256);
let bytes = repo_bytes let bytes = repo_bytes
.get_blob_bytes(&sha256_hex) .get_blob_bytes(&sha256_hex)
@ -273,7 +273,7 @@ pub fn materialize_cir_from_repo_bytes(
} }
Ok(CirMaterializeSummary { Ok(CirMaterializeSummary {
object_count: cir.objects.len(), object_count: cir.validated_object_count(),
trust_anchor_count: cir.trust_anchors.len(), trust_anchor_count: cir.trust_anchors.len(),
materialized_file_count: expected.len(), materialized_file_count: expected.len(),
linked_files: 0, linked_files: 0,
@ -325,8 +325,7 @@ fn write_bytes_to_mirror_uri(
} }
fn expected_materialized_uris(cir: &CanonicalInputRepresentation) -> BTreeSet<String> { fn expected_materialized_uris(cir: &CanonicalInputRepresentation) -> BTreeSet<String> {
cir.objects cir.validated_objects()
.iter()
.map(|item| item.rsync_uri.clone()) .map(|item| item.rsync_uri.clone())
.chain( .chain(
cir.trust_anchors cir.trust_anchors
@ -435,10 +434,7 @@ mod tests {
resolve_static_pool_file, resolve_static_pool_file,
}; };
use crate::blob_store::{ExternalRawStoreDb, ExternalRepoBytesDb}; use crate::blob_store::{ExternalRawStoreDb, ExternalRepoBytesDb};
use crate::cir::model::{ use crate::cir::model::{CanonicalInputRepresentation, CirObject, CirTrustAnchor, sha256};
CIR_VERSION_V3, CanonicalInputRepresentation, CirHashAlgorithm, CirObject,
CirRejectedObject, CirTrustAnchor, compute_reject_list_sha256, sha256,
};
use sha2::Digest; use sha2::Digest;
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
@ -463,15 +459,9 @@ mod tests {
} }
fn sample_cir() -> CanonicalInputRepresentation { fn sample_cir() -> CanonicalInputRepresentation {
let rejected_objects = vec![CirRejectedObject { CanonicalInputRepresentation::new_v4(
object_uri: "rsync://example.net/repo/rejected-a.roa".to_string(), sample_time(),
reason: Some("invalid roa".to_string()), vec![
}];
CanonicalInputRepresentation {
version: CIR_VERSION_V3,
hash_alg: CirHashAlgorithm::Sha256,
validation_time: sample_time(),
objects: vec![
CirObject { CirObject {
rsync_uri: "rsync://example.net/repo/a.cer".to_string(), rsync_uri: "rsync://example.net/repo/a.cer".to_string(),
sha256: hex::decode( sha256: hex::decode(
@ -487,20 +477,17 @@ mod tests {
.unwrap(), .unwrap(),
}, },
], ],
trust_anchors: vec![sample_trust_anchor()], Vec::new(),
reject_list_sha256: compute_reject_list_sha256( vec![sample_trust_anchor()],
rejected_objects.iter().map(|item| item.object_uri.as_str()), Vec::new(),
), Vec::new(),
rejected_objects, )
}
} }
fn cir_with_real_hashes(a: &[u8], b: &[u8]) -> CanonicalInputRepresentation { fn cir_with_real_hashes(a: &[u8], b: &[u8]) -> CanonicalInputRepresentation {
CanonicalInputRepresentation { CanonicalInputRepresentation::new_v4(
version: CIR_VERSION_V3, sample_time(),
hash_alg: CirHashAlgorithm::Sha256, vec![
validation_time: sample_time(),
objects: vec![
CirObject { CirObject {
rsync_uri: "rsync://example.net/repo/a.cer".to_string(), rsync_uri: "rsync://example.net/repo/a.cer".to_string(),
sha256: sha2::Sha256::digest(a).to_vec(), sha256: sha2::Sha256::digest(a).to_vec(),
@ -510,10 +497,11 @@ mod tests {
sha256: sha2::Sha256::digest(b).to_vec(), sha256: sha2::Sha256::digest(b).to_vec(),
}, },
], ],
trust_anchors: vec![sample_trust_anchor()], Vec::new(),
reject_list_sha256: compute_reject_list_sha256(std::iter::empty::<&str>()), vec![sample_trust_anchor()],
rejected_objects: Vec::new(), Vec::new(),
} Vec::new(),
)
} }
#[test] #[test]
@ -661,31 +649,14 @@ mod tests {
let mirror_root = td.path().join("mirror"); let mirror_root = td.path().join("mirror");
let a = b"a".to_vec(); let a = b"a".to_vec();
let b = b"b".to_vec(); let b = b"b".to_vec();
let cir = CanonicalInputRepresentation { let cir = cir_with_real_hashes(&a, &b);
version: CIR_VERSION_V3,
hash_alg: CirHashAlgorithm::Sha256,
validation_time: sample_time(),
objects: vec![
CirObject {
rsync_uri: "rsync://example.net/repo/a.cer".to_string(),
sha256: sha2::Sha256::digest(&a).to_vec(),
},
CirObject {
rsync_uri: "rsync://example.net/repo/nested/b.roa".to_string(),
sha256: sha2::Sha256::digest(&b).to_vec(),
},
],
trust_anchors: vec![sample_trust_anchor()],
reject_list_sha256: compute_reject_list_sha256(std::iter::empty::<&str>()),
rejected_objects: Vec::new(),
};
{ {
let raw_store = ExternalRawStoreDb::open(&raw_store_path).unwrap(); let raw_store = ExternalRawStoreDb::open(&raw_store_path).unwrap();
raw_store raw_store
.put_blob_bytes_batch(&[ .put_blob_bytes_batch(&[
(hex::encode(&cir.objects[0].sha256), a), (hex::encode(&cir.fresh_validated_objects[0].sha256), a),
(hex::encode(&cir.objects[1].sha256), b), (hex::encode(&cir.fresh_validated_objects[1].sha256), b),
]) ])
.unwrap(); .unwrap();
} }
@ -720,7 +691,10 @@ mod tests {
{ {
let raw_store = ExternalRawStoreDb::open(&raw_store_path).unwrap(); let raw_store = ExternalRawStoreDb::open(&raw_store_path).unwrap();
raw_store raw_store
.put_blob_bytes_batch(&[(hex::encode(&cir.objects[0].sha256), b"a".to_vec())]) .put_blob_bytes_batch(&[(
hex::encode(&cir.fresh_validated_objects[0].sha256),
b"a".to_vec(),
)])
.unwrap(); .unwrap();
} }
@ -742,8 +716,14 @@ mod tests {
let raw_store = ExternalRawStoreDb::open(&raw_store_path).unwrap(); let raw_store = ExternalRawStoreDb::open(&raw_store_path).unwrap();
raw_store raw_store
.put_blob_bytes_batch(&[ .put_blob_bytes_batch(&[
(hex::encode(&cir.objects[0].sha256), b"a".to_vec()), (
(hex::encode(&cir.objects[1].sha256), b"b".to_vec()), hex::encode(&cir.fresh_validated_objects[0].sha256),
b"a".to_vec(),
),
(
hex::encode(&cir.fresh_validated_objects[1].sha256),
b"b".to_vec(),
),
]) ])
.unwrap(); .unwrap();
} }
@ -767,8 +747,14 @@ mod tests {
let raw_store = ExternalRawStoreDb::open(&raw_store_path).unwrap(); let raw_store = ExternalRawStoreDb::open(&raw_store_path).unwrap();
raw_store raw_store
.put_blob_bytes_batch(&[ .put_blob_bytes_batch(&[
(hex::encode(&cir.objects[0].sha256), a.clone()), (
(hex::encode(&cir.objects[1].sha256), b.clone()), hex::encode(&cir.fresh_validated_objects[0].sha256),
a.clone(),
),
(
hex::encode(&cir.fresh_validated_objects[1].sha256),
b.clone(),
),
]) ])
.unwrap(); .unwrap();
} }
@ -787,21 +773,20 @@ mod tests {
let td = tempfile::tempdir().unwrap(); let td = tempfile::tempdir().unwrap();
let raw_store_path = td.path().join("raw-store.db"); let raw_store_path = td.path().join("raw-store.db");
let mirror_root = td.path().join("mirror"); let mirror_root = td.path().join("mirror");
let cir = CanonicalInputRepresentation { let cir = CanonicalInputRepresentation::new_v4(
version: CIR_VERSION_V3, sample_time(),
hash_alg: CirHashAlgorithm::Sha256, vec![CirObject {
validation_time: sample_time(),
objects: vec![CirObject {
rsync_uri: "rsync://example.net/repo/a.cer".to_string(), rsync_uri: "rsync://example.net/repo/a.cer".to_string(),
sha256: hex::decode( sha256: hex::decode(
"1111111111111111111111111111111111111111111111111111111111111111", "1111111111111111111111111111111111111111111111111111111111111111",
) )
.unwrap(), .unwrap(),
}], }],
trust_anchors: vec![sample_trust_anchor()], Vec::new(),
reject_list_sha256: compute_reject_list_sha256(std::iter::empty::<&str>()), vec![sample_trust_anchor()],
rejected_objects: Vec::new(), Vec::new(),
}; Vec::new(),
);
{ {
let raw_store = ExternalRawStoreDb::open(&raw_store_path).unwrap(); let raw_store = ExternalRawStoreDb::open(&raw_store_path).unwrap();
raw_store raw_store
@ -841,31 +826,14 @@ mod tests {
let mirror_root = td.path().join("mirror"); let mirror_root = td.path().join("mirror");
let a = b"a".to_vec(); let a = b"a".to_vec();
let b = b"b".to_vec(); let b = b"b".to_vec();
let cir = CanonicalInputRepresentation { let cir = cir_with_real_hashes(&a, &b);
version: CIR_VERSION_V3,
hash_alg: CirHashAlgorithm::Sha256,
validation_time: sample_time(),
objects: vec![
CirObject {
rsync_uri: "rsync://example.net/repo/a.cer".to_string(),
sha256: sha2::Sha256::digest(&a).to_vec(),
},
CirObject {
rsync_uri: "rsync://example.net/repo/nested/b.roa".to_string(),
sha256: sha2::Sha256::digest(&b).to_vec(),
},
],
trust_anchors: vec![sample_trust_anchor()],
reject_list_sha256: compute_reject_list_sha256(std::iter::empty::<&str>()),
rejected_objects: Vec::new(),
};
{ {
let repo_bytes = ExternalRepoBytesDb::open(&repo_bytes_db).unwrap(); let repo_bytes = ExternalRepoBytesDb::open(&repo_bytes_db).unwrap();
repo_bytes repo_bytes
.put_blob_bytes_batch(&[ .put_blob_bytes_batch(&[
(hex::encode(&cir.objects[0].sha256), a), (hex::encode(&cir.fresh_validated_objects[0].sha256), a),
(hex::encode(&cir.objects[1].sha256), b), (hex::encode(&cir.fresh_validated_objects[1].sha256), b),
]) ])
.unwrap(); .unwrap();
} }

View File

@ -22,8 +22,8 @@ pub use materialize::{
materialize_cir_from_repo_bytes, mirror_relative_path_for_rsync_uri, resolve_static_pool_file, materialize_cir_from_repo_bytes, mirror_relative_path_for_rsync_uri, resolve_static_pool_file,
}; };
pub use model::{ pub use model::{
CIR_VERSION_V1, CIR_VERSION_V3, CanonicalInputRepresentation, CirHashAlgorithm, CirObject, CIR_VERSION_V1, CIR_VERSION_V3, CIR_VERSION_V4, CanonicalInputRepresentation, CirHashAlgorithm,
CirRejectedObject, CirTrustAnchor, compute_reject_list_sha256, sha256, CirObject, CirRejectedObject, CirTrustAnchor, compute_reject_list_sha256, sha256,
}; };
pub use sequence::{CirSequenceManifest, CirSequenceStep, CirSequenceStepKind}; pub use sequence::{CirSequenceManifest, CirSequenceStep, CirSequenceStepKind};
#[cfg(feature = "full")] #[cfg(feature = "full")]
@ -36,8 +36,8 @@ pub use static_pool::{
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::{ use super::{
CIR_VERSION_V3, CanonicalInputRepresentation, CirHashAlgorithm, CirObject, CIR_VERSION_V4, CanonicalInputRepresentation, CirObject, CirRejectedObject, CirTrustAnchor,
CirRejectedObject, CirTrustAnchor, compute_reject_list_sha256, decode_cir, encode_cir, decode_cir, encode_cir,
}; };
fn sample_time() -> time::OffsetDateTime { fn sample_time() -> time::OffsetDateTime {
@ -69,11 +69,9 @@ mod tests {
reason: None, reason: None,
}, },
]; ];
CanonicalInputRepresentation { CanonicalInputRepresentation::new_v4(
version: CIR_VERSION_V3, sample_time(),
hash_alg: CirHashAlgorithm::Sha256, vec![
validation_time: sample_time(),
objects: vec![
CirObject { CirObject {
rsync_uri: "rsync://example.net/repo/a.cer".to_string(), rsync_uri: "rsync://example.net/repo/a.cer".to_string(),
sha256: vec![0x11; 32], sha256: vec![0x11; 32],
@ -82,17 +80,24 @@ mod tests {
rsync_uri: "rsync://example.net/repo/b.roa".to_string(), rsync_uri: "rsync://example.net/repo/b.roa".to_string(),
sha256: vec![0x22; 32], sha256: vec![0x22; 32],
}, },
CirObject {
rsync_uri: "rsync://example.net/repo/rejected-a.roa".to_string(),
sha256: vec![0x33; 32],
},
CirObject {
rsync_uri: "rsync://example.net/repo/rejected-b.asa".to_string(),
sha256: vec![0x44; 32],
},
], ],
trust_anchors: vec![sample_trust_anchor( Vec::new(),
vec![sample_trust_anchor(
"rsync://example.net/repo/ta.cer", "rsync://example.net/repo/ta.cer",
"https://tal.example.net/root.tal", "https://tal.example.net/root.tal",
b"ta-der", b"ta-der",
)], )],
reject_list_sha256: compute_reject_list_sha256(
rejected_objects.iter().map(|item| item.object_uri.as_str()),
),
rejected_objects, rejected_objects,
} Vec::new(),
)
} }
fn test_encode_tlv(tag: u8, value: &[u8]) -> Vec<u8> { fn test_encode_tlv(tag: u8, value: &[u8]) -> Vec<u8> {
@ -125,19 +130,18 @@ mod tests {
#[test] #[test]
fn cir_roundtrip_minimal_succeeds() { fn cir_roundtrip_minimal_succeeds() {
let cir = CanonicalInputRepresentation { let cir = CanonicalInputRepresentation::new_v4(
version: CIR_VERSION_V3, sample_time(),
hash_alg: CirHashAlgorithm::Sha256, Vec::new(),
validation_time: sample_time(), Vec::new(),
objects: Vec::new(), vec![sample_trust_anchor(
trust_anchors: vec![sample_trust_anchor(
"rsync://example.net/repo/minimal-ta.cer", "rsync://example.net/repo/minimal-ta.cer",
"https://tal.example.net/minimal.tal", "https://tal.example.net/minimal.tal",
b"minimal-ta-der", b"minimal-ta-der",
)], )],
reject_list_sha256: compute_reject_list_sha256(std::iter::empty::<&str>()), Vec::new(),
rejected_objects: Vec::new(), Vec::new(),
}; );
let der = encode_cir(&cir).expect("encode minimal cir"); let der = encode_cir(&cir).expect("encode minimal cir");
let decoded = decode_cir(&der).expect("decode minimal cir"); let decoded = decode_cir(&der).expect("decode minimal cir");
assert_eq!(decoded, cir); assert_eq!(decoded, cir);
@ -145,11 +149,9 @@ mod tests {
#[test] #[test]
fn cir_model_rejects_unsorted_duplicate_objects() { fn cir_model_rejects_unsorted_duplicate_objects() {
let cir = CanonicalInputRepresentation { let cir = CanonicalInputRepresentation::new_v4(
version: CIR_VERSION_V3, sample_time(),
hash_alg: CirHashAlgorithm::Sha256, vec![
validation_time: sample_time(),
objects: vec![
CirObject { CirObject {
rsync_uri: "rsync://example.net/repo/z.roa".to_string(), rsync_uri: "rsync://example.net/repo/z.roa".to_string(),
sha256: vec![0x11; 32], sha256: vec![0x11; 32],
@ -159,26 +161,29 @@ mod tests {
sha256: vec![0x22; 32], sha256: vec![0x22; 32],
}, },
], ],
trust_anchors: vec![sample_trust_anchor( Vec::new(),
vec![sample_trust_anchor(
"rsync://example.net/repo/ta.cer", "rsync://example.net/repo/ta.cer",
"https://tal.example.net/root.tal", "https://tal.example.net/root.tal",
b"ta-der", b"ta-der",
)], )],
reject_list_sha256: compute_reject_list_sha256(std::iter::empty::<&str>()), Vec::new(),
rejected_objects: Vec::new(), Vec::new(),
}; );
let err = encode_cir(&cir).expect_err("unsorted objects must fail"); let err = encode_cir(&cir).expect_err("unsorted objects must fail");
assert!(err.to_string().contains("CIR.objects"), "{err}"); assert!(
err.to_string().contains("CIR.freshValidatedObjects"),
"{err}"
);
} }
#[test] #[test]
fn cir_model_rejects_duplicate_tals() { fn cir_model_rejects_duplicate_tals() {
let cir = CanonicalInputRepresentation { let cir = CanonicalInputRepresentation::new_v4(
version: CIR_VERSION_V3, sample_time(),
hash_alg: CirHashAlgorithm::Sha256, Vec::new(),
validation_time: sample_time(), Vec::new(),
objects: Vec::new(), vec![
trust_anchors: vec![
sample_trust_anchor( sample_trust_anchor(
"rsync://example.net/repo/ta.cer", "rsync://example.net/repo/ta.cer",
"https://tal.example.net/root.tal", "https://tal.example.net/root.tal",
@ -190,9 +195,9 @@ mod tests {
b"ta-der-b", b"ta-der-b",
), ),
], ],
reject_list_sha256: compute_reject_list_sha256(std::iter::empty::<&str>()), Vec::new(),
rejected_objects: Vec::new(), Vec::new(),
}; );
let err = encode_cir(&cir).expect_err("duplicate trust_anchors must fail"); let err = encode_cir(&cir).expect_err("duplicate trust_anchors must fail");
assert!(err.to_string().contains("CIR.trustAnchors"), "{err}"); assert!(err.to_string().contains("CIR.trustAnchors"), "{err}");
} }
@ -202,10 +207,10 @@ mod tests {
let mut der = encode_cir(&sample_cir()).expect("encode cir"); let mut der = encode_cir(&sample_cir()).expect("encode cir");
let pos = der let pos = der
.windows(3) .windows(3)
.position(|window| window == [0x02, 0x01, CIR_VERSION_V3 as u8]) .position(|window| window == [0x02, 0x01, CIR_VERSION_V4 as u8])
.or_else(|| { .or_else(|| {
der.windows(3) der.windows(3)
.position(|window| window == [0x02, 0x01, CIR_VERSION_V3 as u8]) .position(|window| window == [0x02, 0x01, CIR_VERSION_V4 as u8])
}) })
.expect("find version integer"); .expect("find version integer");
der[pos + 2] = 2; der[pos + 2] = 2;
@ -243,34 +248,32 @@ mod tests {
#[test] #[test]
fn cir_model_rejects_non_rsync_object_uri_and_empty_tals() { fn cir_model_rejects_non_rsync_object_uri_and_empty_tals() {
let bad_object = CanonicalInputRepresentation { let bad_object = CanonicalInputRepresentation::new_v4(
version: CIR_VERSION_V3, sample_time(),
hash_alg: CirHashAlgorithm::Sha256, vec![CirObject {
validation_time: sample_time(),
objects: vec![CirObject {
rsync_uri: "https://example.net/repo/a.roa".to_string(), rsync_uri: "https://example.net/repo/a.roa".to_string(),
sha256: vec![0x11; 32], sha256: vec![0x11; 32],
}], }],
trust_anchors: vec![sample_trust_anchor( Vec::new(),
vec![sample_trust_anchor(
"rsync://example.net/repo/ta.cer", "rsync://example.net/repo/ta.cer",
"https://tal.example.net/root.tal", "https://tal.example.net/root.tal",
b"ta-der", b"ta-der",
)], )],
reject_list_sha256: compute_reject_list_sha256(std::iter::empty::<&str>()), Vec::new(),
rejected_objects: Vec::new(), Vec::new(),
}; );
let err = encode_cir(&bad_object).expect_err("non-rsync object uri must fail"); let err = encode_cir(&bad_object).expect_err("non-rsync object uri must fail");
assert!(err.to_string().contains("rsync://"), "{err}"); assert!(err.to_string().contains("rsync://"), "{err}");
let no_tals = CanonicalInputRepresentation { let no_tals = CanonicalInputRepresentation::new_v4(
version: CIR_VERSION_V3, sample_time(),
hash_alg: CirHashAlgorithm::Sha256, Vec::new(),
validation_time: sample_time(), Vec::new(),
objects: Vec::new(), Vec::new(),
trust_anchors: Vec::new(), Vec::new(),
reject_list_sha256: compute_reject_list_sha256(std::iter::empty::<&str>()), Vec::new(),
rejected_objects: Vec::new(), );
};
let err = encode_cir(&no_tals).expect_err("empty trust_anchors must fail"); let err = encode_cir(&no_tals).expect_err("empty trust_anchors must fail");
assert!( assert!(
err.to_string() err.to_string()
@ -281,54 +284,55 @@ mod tests {
#[test] #[test]
fn cir_model_rejects_non_utc_time_bad_hash_len_and_non_http_tal_uri() { fn cir_model_rejects_non_utc_time_bad_hash_len_and_non_http_tal_uri() {
let bad_time = CanonicalInputRepresentation { let bad_time = CanonicalInputRepresentation::new_v4(
version: CIR_VERSION_V3, sample_time().to_offset(time::UtcOffset::from_hms(8, 0, 0).unwrap()),
hash_alg: CirHashAlgorithm::Sha256, Vec::new(),
validation_time: sample_time().to_offset(time::UtcOffset::from_hms(8, 0, 0).unwrap()), Vec::new(),
objects: Vec::new(), vec![sample_trust_anchor(
trust_anchors: vec![sample_trust_anchor(
"rsync://example.net/repo/ta.cer", "rsync://example.net/repo/ta.cer",
"https://tal.example.net/root.tal", "https://tal.example.net/root.tal",
b"ta-der", b"ta-der",
)], )],
reject_list_sha256: compute_reject_list_sha256(std::iter::empty::<&str>()), Vec::new(),
rejected_objects: Vec::new(), Vec::new(),
);
let bad_time = CanonicalInputRepresentation {
validation_time: sample_time().to_offset(time::UtcOffset::from_hms(8, 0, 0).unwrap()),
..bad_time
}; };
let err = encode_cir(&bad_time).expect_err("non-utc validation time must fail"); let err = encode_cir(&bad_time).expect_err("non-utc validation time must fail");
assert!(err.to_string().contains("UTC"), "{err}"); assert!(err.to_string().contains("UTC"), "{err}");
let bad_hash = CanonicalInputRepresentation { let bad_hash = CanonicalInputRepresentation::new_v4(
version: CIR_VERSION_V3, sample_time(),
hash_alg: CirHashAlgorithm::Sha256, vec![CirObject {
validation_time: sample_time(),
objects: vec![CirObject {
rsync_uri: "rsync://example.net/repo/a.roa".to_string(), rsync_uri: "rsync://example.net/repo/a.roa".to_string(),
sha256: vec![0x11; 31], sha256: vec![0x11; 31],
}], }],
trust_anchors: vec![sample_trust_anchor( Vec::new(),
vec![sample_trust_anchor(
"rsync://example.net/repo/ta.cer", "rsync://example.net/repo/ta.cer",
"https://tal.example.net/root.tal", "https://tal.example.net/root.tal",
b"ta-der", b"ta-der",
)], )],
reject_list_sha256: compute_reject_list_sha256(std::iter::empty::<&str>()), Vec::new(),
rejected_objects: Vec::new(), Vec::new(),
}; );
let err = encode_cir(&bad_hash).expect_err("bad digest len must fail"); let err = encode_cir(&bad_hash).expect_err("bad digest len must fail");
assert!(err.to_string().contains("32 bytes"), "{err}"); assert!(err.to_string().contains("32 bytes"), "{err}");
let bad_tal_uri = CanonicalInputRepresentation { let bad_tal_uri = CanonicalInputRepresentation::new_v4(
version: CIR_VERSION_V3, sample_time(),
hash_alg: CirHashAlgorithm::Sha256, Vec::new(),
validation_time: sample_time(), Vec::new(),
objects: Vec::new(), vec![sample_trust_anchor(
trust_anchors: vec![sample_trust_anchor(
"rsync://example.net/repo/ta.cer", "rsync://example.net/repo/ta.cer",
"ftp://tal.example.net/root.tal", "ftp://tal.example.net/root.tal",
b"ta-der", b"ta-der",
)], )],
reject_list_sha256: compute_reject_list_sha256(std::iter::empty::<&str>()), Vec::new(),
rejected_objects: Vec::new(), Vec::new(),
}; );
let err = encode_cir(&bad_tal_uri).expect_err("bad tal uri must fail"); let err = encode_cir(&bad_tal_uri).expect_err("bad tal uri must fail");
assert!(err.to_string().contains("http:// or https://"), "{err}"); assert!(err.to_string().contains("http:// or https://"), "{err}");
} }
@ -367,12 +371,19 @@ mod tests {
let bad = test_encode_tlv( let bad = test_encode_tlv(
0x30, 0x30,
&[ &[
test_encode_tlv(0x02, &[CIR_VERSION_V3 as u8]), test_encode_tlv(0x02, &[CIR_VERSION_V4 as u8]),
test_encode_tlv(0x06, crate::data_model::oid::OID_SHA256_RAW), test_encode_tlv(0x06, crate::data_model::oid::OID_SHA256_RAW),
test_encode_tlv(0x18, b"20260407123456Z"), test_encode_tlv(0x18, b"20260407123456Z"),
test_encode_tlv(0x30, &object), test_encode_tlv(0x30, &object),
test_encode_tlv(0x30, &[]),
test_encode_tlv(0x30, &trust_anchor), test_encode_tlv(0x30, &trust_anchor),
test_encode_tlv(0x04, &[0x33; 32]), test_encode_tlv(0x04, &[0x33; 32]),
test_encode_tlv(0x04, &[0x33; 32]),
test_encode_tlv(0x04, &[0x33; 32]),
test_encode_tlv(0x04, &[0x33; 32]),
test_encode_tlv(0x04, &[0x33; 32]),
test_encode_tlv(0x04, &[0x33; 32]),
test_encode_tlv(0x30, &[]),
test_encode_tlv(0x30, &[]), test_encode_tlv(0x30, &[]),
] ]
.concat(), .concat(),
@ -412,9 +423,12 @@ mod tests {
#[test] #[test]
fn cir_model_rejects_unsorted_rejected_objects_and_bad_digest() { fn cir_model_rejects_unsorted_rejected_objects_and_bad_digest() {
let mut cir = sample_cir(); let mut cir = sample_cir();
cir.rejected_objects.swap(0, 1); cir.fresh_rejected_objects.swap(0, 1);
let err = encode_cir(&cir).expect_err("unsorted rejected objects must fail"); let err = encode_cir(&cir).expect_err("unsorted rejected objects must fail");
assert!(err.to_string().contains("CIR.rejectedObjects"), "{err}"); assert!(
err.to_string().contains("CIR.freshRejectedObjects"),
"{err}"
);
let mut cir = sample_cir(); let mut cir = sample_cir();
cir.reject_list_sha256 = vec![0x55; 32]; cir.reject_list_sha256 = vec![0x55; 32];

View File

@ -3,6 +3,7 @@ use crate::data_model::oid::OID_SHA256;
pub const CIR_VERSION_V1: u32 = 1; pub const CIR_VERSION_V1: u32 = 1;
pub const CIR_VERSION_V2: u32 = 2; pub const CIR_VERSION_V2: u32 = 2;
pub const CIR_VERSION_V3: u32 = 3; pub const CIR_VERSION_V3: u32 = 3;
pub const CIR_VERSION_V4: u32 = 4;
pub const DIGEST_LEN_SHA256: usize = 32; pub const DIGEST_LEN_SHA256: usize = 32;
#[derive(Clone, Debug, PartialEq, Eq)] #[derive(Clone, Debug, PartialEq, Eq)]
@ -23,17 +24,81 @@ pub struct CanonicalInputRepresentation {
pub version: u32, pub version: u32,
pub hash_alg: CirHashAlgorithm, pub hash_alg: CirHashAlgorithm,
pub validation_time: time::OffsetDateTime, pub validation_time: time::OffsetDateTime,
pub objects: Vec<CirObject>, pub fresh_validated_objects: Vec<CirObject>,
pub cached_validated_objects: Vec<CirObject>,
pub trust_anchors: Vec<CirTrustAnchor>, pub trust_anchors: Vec<CirTrustAnchor>,
pub object_list_sha256: Vec<u8>,
pub fresh_object_list_sha256: Vec<u8>,
pub cached_object_list_sha256: Vec<u8>,
pub reject_list_sha256: Vec<u8>, pub reject_list_sha256: Vec<u8>,
pub rejected_objects: Vec<CirRejectedObject>, pub fresh_reject_list_sha256: Vec<u8>,
pub cached_reject_list_sha256: Vec<u8>,
pub fresh_rejected_objects: Vec<CirRejectedObject>,
pub cached_rejected_objects: Vec<CirRejectedObject>,
} }
impl CanonicalInputRepresentation { impl CanonicalInputRepresentation {
pub fn new_v4(
validation_time: time::OffsetDateTime,
fresh_validated_objects: Vec<CirObject>,
cached_validated_objects: Vec<CirObject>,
trust_anchors: Vec<CirTrustAnchor>,
fresh_rejected_objects: Vec<CirRejectedObject>,
cached_rejected_objects: Vec<CirRejectedObject>,
) -> Self {
let fresh_object_list_sha256 = compute_object_list_sha256(fresh_validated_objects.iter());
let cached_object_list_sha256 = compute_object_list_sha256(cached_validated_objects.iter());
let object_list_sha256 = compute_sectioned_object_list_sha256(
fresh_validated_objects
.iter()
.map(|object| ("fresh", object))
.chain(
cached_validated_objects
.iter()
.map(|object| ("cached", object)),
),
);
let fresh_reject_list_sha256 = compute_reject_list_sha256(
fresh_rejected_objects
.iter()
.map(|item| item.object_uri.as_str()),
);
let cached_reject_list_sha256 = compute_reject_list_sha256(
cached_rejected_objects
.iter()
.map(|item| item.object_uri.as_str()),
);
let mut all_rejected_uris = fresh_rejected_objects
.iter()
.chain(cached_rejected_objects.iter())
.map(|item| item.object_uri.as_str())
.collect::<Vec<_>>();
all_rejected_uris.sort_unstable();
all_rejected_uris.dedup();
let reject_list_sha256 = compute_reject_list_sha256(all_rejected_uris.iter().copied());
Self {
version: CIR_VERSION_V4,
hash_alg: CirHashAlgorithm::Sha256,
validation_time: validation_time.to_offset(time::UtcOffset::UTC),
fresh_validated_objects,
cached_validated_objects,
trust_anchors,
object_list_sha256,
fresh_object_list_sha256,
cached_object_list_sha256,
reject_list_sha256,
fresh_reject_list_sha256,
cached_reject_list_sha256,
fresh_rejected_objects,
cached_rejected_objects,
}
}
pub fn validate(&self) -> Result<(), String> { pub fn validate(&self) -> Result<(), String> {
if self.version != CIR_VERSION_V3 { if self.version != CIR_VERSION_V4 {
return Err(format!( return Err(format!(
"CIR version must be {CIR_VERSION_V3}, got {}", "CIR version must be {CIR_VERSION_V4}, got {}",
self.version self.version
)); ));
} }
@ -44,8 +109,16 @@ impl CanonicalInputRepresentation {
return Err("CIR validationTime must be UTC".into()); return Err("CIR validationTime must be UTC".into());
} }
validate_sorted_unique_strings( validate_sorted_unique_strings(
self.objects.iter().map(|item| item.rsync_uri.as_str()), self.fresh_validated_objects
"CIR.objects must be sorted by rsyncUri and unique", .iter()
.map(|item| item.rsync_uri.as_str()),
"CIR.freshValidatedObjects must be sorted by rsyncUri and unique",
)?;
validate_sorted_unique_strings(
self.cached_validated_objects
.iter()
.map(|item| item.rsync_uri.as_str()),
"CIR.cachedValidatedObjects must be sorted by rsyncUri and unique",
)?; )?;
validate_sorted_unique_strings( validate_sorted_unique_strings(
self.trust_anchors self.trust_anchors
@ -54,14 +127,21 @@ impl CanonicalInputRepresentation {
"CIR.trustAnchors must be sorted by taRsyncUri and unique", "CIR.trustAnchors must be sorted by taRsyncUri and unique",
)?; )?;
validate_sorted_unique_strings( validate_sorted_unique_strings(
self.rejected_objects self.fresh_rejected_objects
.iter() .iter()
.map(|item| item.object_uri.as_str()), .map(|item| item.object_uri.as_str()),
"CIR.rejectedObjects must be sorted by objectUri and unique", "CIR.freshRejectedObjects must be sorted by objectUri and unique",
)?;
validate_sorted_unique_strings(
self.cached_rejected_objects
.iter()
.map(|item| item.object_uri.as_str()),
"CIR.cachedRejectedObjects must be sorted by objectUri and unique",
)?; )?;
let object_uris = self let object_uris = self
.objects .fresh_validated_objects
.iter() .iter()
.chain(self.cached_validated_objects.iter())
.map(|item| item.rsync_uri.as_str()) .map(|item| item.rsync_uri.as_str())
.collect::<std::collections::BTreeSet<_>>(); .collect::<std::collections::BTreeSet<_>>();
for trust_anchor in &self.trust_anchors { for trust_anchor in &self.trust_anchors {
@ -72,34 +152,136 @@ impl CanonicalInputRepresentation {
)); ));
} }
} }
let fresh_object_uris = self
.fresh_validated_objects
.iter()
.map(|item| item.rsync_uri.as_str())
.collect::<std::collections::BTreeSet<_>>();
for item in &self.fresh_rejected_objects {
if !fresh_object_uris.contains(item.object_uri.as_str()) {
return Err(format!(
"CIR.freshRejectedObjects URI must exist in freshValidatedObjects: {}",
item.object_uri
));
}
}
let cached_object_uris = self
.cached_validated_objects
.iter()
.map(|item| item.rsync_uri.as_str())
.collect::<std::collections::BTreeSet<_>>();
for item in &self.cached_rejected_objects {
if !cached_object_uris.contains(item.object_uri.as_str()) {
return Err(format!(
"CIR.cachedRejectedObjects URI must exist in cachedValidatedObjects: {}",
item.object_uri
));
}
}
if self.trust_anchors.is_empty() { if self.trust_anchors.is_empty() {
return Err("CIR.trustAnchors must be non-empty".into()); return Err("CIR.trustAnchors must be non-empty".into());
} }
if self.reject_list_sha256.len() != DIGEST_LEN_SHA256 { for (label, digest) in [
return Err(format!( ("CIR.objectListSha256", &self.object_list_sha256),
"CIR.rejectListSha256 must be {DIGEST_LEN_SHA256} bytes, got {}", ("CIR.freshObjectListSha256", &self.fresh_object_list_sha256),
self.reject_list_sha256.len() (
)); "CIR.cachedObjectListSha256",
&self.cached_object_list_sha256,
),
("CIR.rejectListSha256", &self.reject_list_sha256),
("CIR.freshRejectListSha256", &self.fresh_reject_list_sha256),
(
"CIR.cachedRejectListSha256",
&self.cached_reject_list_sha256,
),
] {
if digest.len() != DIGEST_LEN_SHA256 {
return Err(format!(
"{label} must be {DIGEST_LEN_SHA256} bytes, got {}",
digest.len()
));
}
} }
for object in &self.objects { for object in self.validated_objects() {
object.validate()?; object.validate()?;
} }
for trust_anchor in &self.trust_anchors { for trust_anchor in &self.trust_anchors {
trust_anchor.validate()?; trust_anchor.validate()?;
} }
for item in &self.rejected_objects { for item in self.rejected_objects() {
item.validate()?; item.validate()?;
} }
let expected_digest = compute_reject_list_sha256( let expected_fresh_object_digest =
self.rejected_objects compute_object_list_sha256(self.fresh_validated_objects.iter());
if self.fresh_object_list_sha256 != expected_fresh_object_digest {
return Err("CIR.freshObjectListSha256 does not match freshValidatedObjects".into());
}
let expected_cached_object_digest =
compute_object_list_sha256(self.cached_validated_objects.iter());
if self.cached_object_list_sha256 != expected_cached_object_digest {
return Err("CIR.cachedObjectListSha256 does not match cachedValidatedObjects".into());
}
let expected_object_digest = compute_sectioned_object_list_sha256(
self.fresh_validated_objects
.iter()
.map(|object| ("fresh", object))
.chain(
self.cached_validated_objects
.iter()
.map(|object| ("cached", object)),
),
);
if self.object_list_sha256 != expected_object_digest {
return Err("CIR.objectListSha256 does not match fresh/cached objects".into());
}
let expected_fresh_reject_digest = compute_reject_list_sha256(
self.fresh_rejected_objects
.iter() .iter()
.map(|item| item.object_uri.as_str()), .map(|item| item.object_uri.as_str()),
); );
if self.fresh_reject_list_sha256 != expected_fresh_reject_digest {
return Err("CIR.freshRejectListSha256 does not match freshRejectedObjects".into());
}
let expected_cached_reject_digest = compute_reject_list_sha256(
self.cached_rejected_objects
.iter()
.map(|item| item.object_uri.as_str()),
);
if self.cached_reject_list_sha256 != expected_cached_reject_digest {
return Err("CIR.cachedRejectListSha256 does not match cachedRejectedObjects".into());
}
let mut all_rejected_uris = self
.rejected_objects()
.map(|item| item.object_uri.as_str())
.collect::<Vec<_>>();
all_rejected_uris.sort_unstable();
all_rejected_uris.dedup();
let expected_digest = compute_reject_list_sha256(all_rejected_uris.iter().copied());
if self.reject_list_sha256 != expected_digest { if self.reject_list_sha256 != expected_digest {
return Err("CIR.rejectListSha256 does not match rejectedObjects".into()); return Err("CIR.rejectListSha256 does not match fresh/cached rejectedObjects".into());
} }
Ok(()) Ok(())
} }
pub fn validated_objects(&self) -> impl Iterator<Item = &CirObject> {
self.fresh_validated_objects
.iter()
.chain(self.cached_validated_objects.iter())
}
pub fn rejected_objects(&self) -> impl Iterator<Item = &CirRejectedObject> {
self.fresh_rejected_objects
.iter()
.chain(self.cached_rejected_objects.iter())
}
pub fn validated_object_count(&self) -> usize {
self.fresh_validated_objects.len() + self.cached_validated_objects.len()
}
pub fn rejected_object_count(&self) -> usize {
self.fresh_rejected_objects.len() + self.cached_rejected_objects.len()
}
} }
#[derive(Clone, Debug, PartialEq, Eq)] #[derive(Clone, Debug, PartialEq, Eq)]
@ -209,12 +391,39 @@ pub fn compute_reject_list_sha256<'a>(uris: impl IntoIterator<Item = &'a str>) -
sha256(&body) sha256(&body)
} }
pub fn compute_object_list_sha256<'a>(objects: impl IntoIterator<Item = &'a CirObject>) -> Vec<u8> {
let mut body = Vec::new();
for object in objects {
append_digest_string(&mut body, &object.rsync_uri);
body.extend_from_slice(&object.sha256);
}
sha256(&body)
}
pub fn compute_sectioned_object_list_sha256<'a>(
objects: impl IntoIterator<Item = (&'a str, &'a CirObject)>,
) -> Vec<u8> {
let mut body = Vec::new();
for (section, object) in objects {
append_digest_string(&mut body, section);
append_digest_string(&mut body, &object.rsync_uri);
body.extend_from_slice(&object.sha256);
}
sha256(&body)
}
pub fn sha256(bytes: &[u8]) -> Vec<u8> { pub fn sha256(bytes: &[u8]) -> Vec<u8> {
use sha2::Digest; use sha2::Digest;
sha2::Sha256::digest(bytes).to_vec() sha2::Sha256::digest(bytes).to_vec()
} }
fn append_digest_string(body: &mut Vec<u8>, value: &str) {
let bytes = value.as_bytes();
body.extend_from_slice(&(bytes.len() as u32).to_be_bytes());
body.extend_from_slice(bytes);
}
fn validate_sorted_unique_strings<'a>( fn validate_sorted_unique_strings<'a>(
items: impl IntoIterator<Item = &'a str>, items: impl IntoIterator<Item = &'a str>,
message: &str, message: &str,

View File

@ -352,11 +352,24 @@ impl Histogram {
struct CirMetrics { struct CirMetrics {
version: u32, version: u32,
objects: u64, objects: u64,
fresh_objects: u64,
cached_objects: u64,
trust_anchors: u64, trust_anchors: u64,
rejected_objects: u64, rejected_objects: u64,
fresh_rejected_objects: u64,
cached_rejected_objects: u64,
object_list_sha256: String,
fresh_object_list_sha256: String,
cached_object_list_sha256: String,
reject_list_sha256: String, reject_list_sha256: String,
fresh_reject_list_sha256: String,
cached_reject_list_sha256: String,
objects_by_type: BTreeMap<String, u64>, objects_by_type: BTreeMap<String, u64>,
fresh_objects_by_type: BTreeMap<String, u64>,
cached_objects_by_type: BTreeMap<String, u64>,
rejected_objects_by_type: BTreeMap<String, u64>, rejected_objects_by_type: BTreeMap<String, u64>,
fresh_rejected_objects_by_type: BTreeMap<String, u64>,
cached_rejected_objects_by_type: BTreeMap<String, u64>,
} }
#[derive(Clone, Debug, Default, Serialize)] #[derive(Clone, Debug, Default, Serialize)]
@ -973,25 +986,62 @@ fn parse_cir(path: &Path, snapshot: &mut MetricsSnapshot) {
{ {
Ok(cir) => { Ok(cir) => {
let mut objects_by_type = BTreeMap::new(); let mut objects_by_type = BTreeMap::new();
for object in &cir.objects { for object in cir.validated_objects() {
*objects_by_type *objects_by_type
.entry(object_type_from_uri(&object.rsync_uri)) .entry(object_type_from_uri(&object.rsync_uri))
.or_default() += 1; .or_default() += 1;
} }
let mut fresh_objects_by_type = BTreeMap::new();
for object in &cir.fresh_validated_objects {
*fresh_objects_by_type
.entry(object_type_from_uri(&object.rsync_uri))
.or_default() += 1;
}
let mut cached_objects_by_type = BTreeMap::new();
for object in &cir.cached_validated_objects {
*cached_objects_by_type
.entry(object_type_from_uri(&object.rsync_uri))
.or_default() += 1;
}
let mut rejected_objects_by_type = BTreeMap::new(); let mut rejected_objects_by_type = BTreeMap::new();
for object in &cir.rejected_objects { for object in cir.rejected_objects() {
*rejected_objects_by_type *rejected_objects_by_type
.entry(object_type_from_uri(&object.object_uri)) .entry(object_type_from_uri(&object.object_uri))
.or_default() += 1; .or_default() += 1;
} }
let mut fresh_rejected_objects_by_type = BTreeMap::new();
for object in &cir.fresh_rejected_objects {
*fresh_rejected_objects_by_type
.entry(object_type_from_uri(&object.object_uri))
.or_default() += 1;
}
let mut cached_rejected_objects_by_type = BTreeMap::new();
for object in &cir.cached_rejected_objects {
*cached_rejected_objects_by_type
.entry(object_type_from_uri(&object.object_uri))
.or_default() += 1;
}
snapshot.cir = Some(CirMetrics { snapshot.cir = Some(CirMetrics {
version: cir.version, version: cir.version,
objects: cir.objects.len() as u64, objects: cir.validated_object_count() as u64,
fresh_objects: cir.fresh_validated_objects.len() as u64,
cached_objects: cir.cached_validated_objects.len() as u64,
trust_anchors: cir.trust_anchors.len() as u64, trust_anchors: cir.trust_anchors.len() as u64,
rejected_objects: cir.rejected_objects.len() as u64, rejected_objects: cir.rejected_object_count() as u64,
fresh_rejected_objects: cir.fresh_rejected_objects.len() as u64,
cached_rejected_objects: cir.cached_rejected_objects.len() as u64,
object_list_sha256: hex::encode(&cir.object_list_sha256),
fresh_object_list_sha256: hex::encode(&cir.fresh_object_list_sha256),
cached_object_list_sha256: hex::encode(&cir.cached_object_list_sha256),
reject_list_sha256: hex::encode(&cir.reject_list_sha256), reject_list_sha256: hex::encode(&cir.reject_list_sha256),
fresh_reject_list_sha256: hex::encode(&cir.fresh_reject_list_sha256),
cached_reject_list_sha256: hex::encode(&cir.cached_reject_list_sha256),
objects_by_type, objects_by_type,
fresh_objects_by_type,
cached_objects_by_type,
rejected_objects_by_type, rejected_objects_by_type,
fresh_rejected_objects_by_type,
cached_rejected_objects_by_type,
}); });
} }
Err(err) => snapshot Err(err) => snapshot
@ -1580,6 +1630,14 @@ fn render_cir_metrics(writer: &mut PromWriter<'_>, instance: &str, cir: &CirMetr
&[label("instance", instance)], &[label("instance", instance)],
cir.objects as f64, cir.objects as f64,
); );
for (source, count) in [("fresh", cir.fresh_objects), ("cached", cir.cached_objects)] {
writer.gauge(
"ours_rp_cir_objects_by_source",
"CIR object count by validation input source",
&[label("instance", instance), label("source", source)],
count as f64,
);
}
writer.gauge( writer.gauge(
"ours_rp_cir_trust_anchors", "ours_rp_cir_trust_anchors",
"CIR trust anchor count", "CIR trust anchor count",
@ -1592,6 +1650,29 @@ fn render_cir_metrics(writer: &mut PromWriter<'_>, instance: &str, cir: &CirMetr
&[label("instance", instance)], &[label("instance", instance)],
cir.rejected_objects as f64, cir.rejected_objects as f64,
); );
for (source, count) in [
("fresh", cir.fresh_rejected_objects),
("cached", cir.cached_rejected_objects),
] {
writer.gauge(
"ours_rp_cir_rejected_objects_by_source",
"CIR rejected object count by validation input source",
&[label("instance", instance), label("source", source)],
count as f64,
);
}
for (source, digest) in [
("merged", cir.object_list_sha256.as_str()),
("fresh", cir.fresh_object_list_sha256.as_str()),
("cached", cir.cached_object_list_sha256.as_str()),
] {
writer.gauge(
"ours_rp_cir_object_list_digest_present",
"CIR object list digest is present by validation input source",
&[label("instance", instance), label("source", source)],
if digest.len() == 64 { 1.0 } else { 0.0 },
);
}
writer.gauge( writer.gauge(
"ours_rp_cir_reject_list_digest_present", "ours_rp_cir_reject_list_digest_present",
"CIR reject list digest is present", "CIR reject list digest is present",
@ -1602,6 +1683,18 @@ fn render_cir_metrics(writer: &mut PromWriter<'_>, instance: &str, cir: &CirMetr
0.0 0.0
}, },
); );
for (source, digest) in [
("merged", cir.reject_list_sha256.as_str()),
("fresh", cir.fresh_reject_list_sha256.as_str()),
("cached", cir.cached_reject_list_sha256.as_str()),
] {
writer.gauge(
"ours_rp_cir_reject_list_digest_present_by_source",
"CIR reject list digest is present by validation input source",
&[label("instance", instance), label("source", source)],
if digest.len() == 64 { 1.0 } else { 0.0 },
);
}
for (object_type, count) in &cir.objects_by_type { for (object_type, count) in &cir.objects_by_type {
writer.gauge( writer.gauge(
"ours_rp_cir_objects_by_type", "ours_rp_cir_objects_by_type",
@ -1613,6 +1706,23 @@ fn render_cir_metrics(writer: &mut PromWriter<'_>, instance: &str, cir: &CirMetr
*count as f64, *count as f64,
); );
} }
for (source, counts) in [
("fresh", &cir.fresh_objects_by_type),
("cached", &cir.cached_objects_by_type),
] {
for (object_type, count) in counts {
writer.gauge(
"ours_rp_cir_objects_by_source_type",
"CIR object count by validation input source and file type",
&[
label("instance", instance),
label("source", source),
label("object_type", object_type),
],
*count as f64,
);
}
}
for (object_type, count) in &cir.rejected_objects_by_type { for (object_type, count) in &cir.rejected_objects_by_type {
writer.gauge( writer.gauge(
"ours_rp_cir_rejected_objects_by_type", "ours_rp_cir_rejected_objects_by_type",
@ -1624,6 +1734,23 @@ fn render_cir_metrics(writer: &mut PromWriter<'_>, instance: &str, cir: &CirMetr
*count as f64, *count as f64,
); );
} }
for (source, counts) in [
("fresh", &cir.fresh_rejected_objects_by_type),
("cached", &cir.cached_rejected_objects_by_type),
] {
for (object_type, count) in counts {
writer.gauge(
"ours_rp_cir_rejected_objects_by_source_type",
"CIR rejected object count by validation input source and file type",
&[
label("instance", instance),
label("source", source),
label("object_type", object_type),
],
*count as f64,
);
}
}
} }
fn render_ccr_metrics(writer: &mut PromWriter<'_>, instance: &str, ccr: &CcrMetrics) { fn render_ccr_metrics(writer: &mut PromWriter<'_>, instance: &str, ccr: &CcrMetrics) {
@ -2092,8 +2219,8 @@ mod tests {
encode_content_info, encode_content_info,
}; };
use crate::cir::{ use crate::cir::{
CanonicalInputRepresentation, CirHashAlgorithm, CirObject, CirRejectedObject, CanonicalInputRepresentation, CirObject, CirRejectedObject, CirTrustAnchor, encode_cir,
CirTrustAnchor, compute_reject_list_sha256, encode_cir, sha256, sha256,
}; };
use tempfile::TempDir; use tempfile::TempDir;
@ -2154,7 +2281,7 @@ mod tests {
assert!(snapshot.repo_stats[0].sync_success); assert!(snapshot.repo_stats[0].sync_success);
assert_eq!(snapshot.repo_stats[0].download_bytes, 333); assert_eq!(snapshot.repo_stats[0].download_bytes, 333);
assert_eq!(snapshot.top_pp_by_object_count[0].object_count, 2); assert_eq!(snapshot.top_pp_by_object_count[0].object_count, 2);
assert_eq!(snapshot.cir.as_ref().unwrap().objects, 1); assert_eq!(snapshot.cir.as_ref().unwrap().objects, 2);
assert_eq!(snapshot.ccr.as_ref().unwrap().state_items["tas"], 1); assert_eq!(snapshot.ccr.as_ref().unwrap().state_items["tas"], 1);
let metrics = render_metrics(&snapshot); let metrics = render_metrics(&snapshot);
assert!(metrics.contains("ours_rp_repository_info")); assert!(metrics.contains("ours_rp_repository_info"));
@ -2162,12 +2289,29 @@ mod tests {
assert!(metrics.contains("ours_rp_repository_download_bytes")); assert!(metrics.contains("ours_rp_repository_download_bytes"));
assert!(metrics.contains("ours_rp_large_publication_points")); assert!(metrics.contains("ours_rp_large_publication_points"));
assert!(metrics.contains("ours_rp_cir_objects")); assert!(metrics.contains("ours_rp_cir_objects"));
assert!(metrics.contains("ours_rp_cir_objects_by_source"));
assert!(metrics.contains("ours_rp_cir_rejected_objects_by_source"));
assert!(metrics.contains("ours_rp_cir_objects_by_source_type"));
assert!(metrics.contains("ours_rp_cir_rejected_objects_by_source_type"));
assert!(metrics.contains("ours_rp_cir_object_list_digest_present"));
assert!(metrics.contains("ours_rp_cir_reject_list_digest_present_by_source"));
assert!(metrics.contains("ours_rp_ccr_state_items")); assert!(metrics.contains("ours_rp_ccr_state_items"));
assert!(
metrics.contains(r#"ours_rp_cir_objects_by_source{instance="test",source="fresh"} 2"#)
);
assert!(
metrics.contains(r#"ours_rp_cir_objects_by_source{instance="test",source="cached"} 0"#)
);
assert!(metrics.contains(
r#"ours_rp_cir_rejected_objects_by_source{instance="test",source="fresh"} 1"#
));
assert!(metrics.contains(r#"ours_rp_vrps{instance="test",kind="total"} 3"#)); assert!(metrics.contains(r#"ours_rp_vrps{instance="test",kind="total"} 3"#));
assert!(metrics.contains(r#"ours_rp_vrps{instance="test",kind="unique"} 2"#)); assert!(metrics.contains(r#"ours_rp_vrps{instance="test",kind="unique"} 2"#));
let status = render_status_json(&snapshot).expect("status"); let status = render_status_json(&snapshot).expect("status");
assert!(status.contains("topPublicationPointsByObjectCount")); assert!(status.contains("topPublicationPointsByObjectCount"));
assert!(status.contains(r#""vrpsUnique": 2"#)); assert!(status.contains(r#""vrpsUnique": 2"#));
assert!(status.contains(r#""freshObjects": 2"#));
assert!(status.contains(r#""cachedObjects": 0"#));
} }
#[test] #[test]
@ -2186,30 +2330,33 @@ mod tests {
object_uri: "rsync://repo.example/a/bad.roa".to_string(), object_uri: "rsync://repo.example/a/bad.roa".to_string(),
reason: Some("bad".to_string()), reason: Some("bad".to_string()),
}]; }];
let cir = CanonicalInputRepresentation { let cir = CanonicalInputRepresentation::new_v4(
version: crate::cir::CIR_VERSION_V3, time::OffsetDateTime::parse(
hash_alg: CirHashAlgorithm::Sha256,
validation_time: time::OffsetDateTime::parse(
"2026-05-25T00:00:00Z", "2026-05-25T00:00:00Z",
&time::format_description::well_known::Rfc3339, &time::format_description::well_known::Rfc3339,
) )
.unwrap(), .unwrap(),
objects: vec![CirObject { vec![
rsync_uri: "rsync://repo.example/a/a.roa".to_string(), CirObject {
sha256: vec![1; 32], rsync_uri: "rsync://repo.example/a/a.roa".to_string(),
}], sha256: vec![1; 32],
trust_anchors: vec![CirTrustAnchor { },
CirObject {
rsync_uri: "rsync://repo.example/a/bad.roa".to_string(),
sha256: vec![2; 32],
},
],
Vec::new(),
vec![CirTrustAnchor {
ta_rsync_uri: "rsync://repo.example/ta.cer".to_string(), ta_rsync_uri: "rsync://repo.example/ta.cer".to_string(),
tal_uri: "https://tal.example/tal.tal".to_string(), tal_uri: "https://tal.example/tal.tal".to_string(),
tal_bytes: b"rsync://repo.example/ta.cer\n\nAQID\n".to_vec(), tal_bytes: b"rsync://repo.example/ta.cer\n\nAQID\n".to_vec(),
ta_certificate_der: b"ta".to_vec(), ta_certificate_der: b"ta".to_vec(),
ta_certificate_sha256: sha256(b"ta"), ta_certificate_sha256: sha256(b"ta"),
}], }],
reject_list_sha256: compute_reject_list_sha256( rejected,
rejected.iter().map(|item| item.object_uri.as_str()), Vec::new(),
), );
rejected_objects: rejected,
};
encode_cir(&cir).expect("encode cir") encode_cir(&cir).expect("encode cir")
} }

View File

@ -48,8 +48,8 @@ mod tests {
build_aspa_payload_state, build_roa_payload_state, encode_content_info, build_aspa_payload_state, build_roa_payload_state, encode_content_info,
}; };
use crate::cir::{ use crate::cir::{
CIR_VERSION_V3, CanonicalInputRepresentation, CirHashAlgorithm, CirObject, CanonicalInputRepresentation, CirObject, CirRejectedObject, CirTrustAnchor, encode_cir,
CirRejectedObject, CirTrustAnchor, compute_reject_list_sha256, encode_cir, sha256, sha256,
}; };
use crate::data_model::roa::{IpPrefix, RoaAfi}; use crate::data_model::roa::{IpPrefix, RoaAfi};
use crate::validation::objects::{AspaAttestation, Vrp}; use crate::validation::objects::{AspaAttestation, Vrp};
@ -100,6 +100,7 @@ mod tests {
&[ &[
object("rsync://example.net/a.roa", 0x11), object("rsync://example.net/a.roa", 0x11),
object("rsync://example.net/persistent.roa", 0x44), object("rsync://example.net/persistent.roa", 0x44),
object("rsync://example.net/reject-old.roa", 0xee),
], ],
&[], &[],
64496, 64496,
@ -485,7 +486,18 @@ mod tests {
9, 9,
64496, 64496,
); );
write_sample_with_ccr_seq(root, "right2", 2, &[peer_mismatch], &[], 10, 64497); write_sample_with_ccr_seq(
root,
"right2",
2,
&[
peer_mismatch,
object("rsync://example.net/pp/rejected.roa", 0xee),
],
&[],
10,
64497,
);
std::fs::write( std::fs::write(
root.join("left.jsonl"), root.join("left.jsonl"),
jsonl(&[ jsonl(&[
@ -845,7 +857,6 @@ mod tests {
rejected: &[&str], rejected: &[&str],
) -> CanonicalInputRepresentation { ) -> CanonicalInputRepresentation {
let mut objects = objects.to_vec(); let mut objects = objects.to_vec();
objects.sort_by(|a, b| a.rsync_uri.cmp(&b.rsync_uri));
let rejected_objects = rejected let rejected_objects = rejected
.iter() .iter()
.map(|uri| CirRejectedObject { .map(|uri| CirRejectedObject {
@ -853,15 +864,26 @@ mod tests {
reason: Some("test".to_string()), reason: Some("test".to_string()),
}) })
.collect::<Vec<_>>(); .collect::<Vec<_>>();
CanonicalInputRepresentation { for rejected_uri in rejected {
version: CIR_VERSION_V3, if !objects
hash_alg: CirHashAlgorithm::Sha256, .iter()
validation_time: sample_time(seq), .any(|object| object.rsync_uri == *rejected_uri)
objects, {
trust_anchors: vec![sample_trust_anchor()], objects.push(CirObject {
reject_list_sha256: compute_reject_list_sha256(rejected.iter().copied()), rsync_uri: (*rejected_uri).to_string(),
rejected_objects, sha256: vec![0xee; 32],
});
}
} }
objects.sort_by(|a, b| a.rsync_uri.cmp(&b.rsync_uri));
CanonicalInputRepresentation::new_v4(
sample_time(seq),
objects,
Vec::new(),
vec![sample_trust_anchor()],
rejected_objects,
Vec::new(),
)
} }
fn sample_trust_anchor() -> CirTrustAnchor { fn sample_trust_anchor() -> CirTrustAnchor {

View File

@ -42,10 +42,12 @@ pub(super) fn load_sequence_meta(path: &Path, side: Side) -> Result<Vec<Sequence
.transpose()?; .transpose()?;
let validation_time = cir.validation_time; let validation_time = cir.validation_time;
samples.push(SequenceMeta { samples.push(SequenceMeta {
cir_object_count: raw.cir_object_count.or(Some(cir.objects.len() as u64)), cir_object_count: raw
.cir_object_count
.or(Some(cir.validated_object_count() as u64)),
cir_reject_count: raw cir_reject_count: raw
.cir_reject_count .cir_reject_count
.or(Some(cir.rejected_objects.len() as u64)), .or(Some(cir.rejected_object_count() as u64)),
cir_trust_anchor_count: raw cir_trust_anchor_count: raw
.cir_trust_anchor_count .cir_trust_anchor_count
.or(Some(cir.trust_anchors.len() as u64)), .or(Some(cir.trust_anchors.len() as u64)),
@ -89,8 +91,7 @@ fn load_sample_parts(
) )
})?; })?;
objects = cir objects = cir
.objects .validated_objects()
.iter()
.map(|item| (item.rsync_uri.clone(), hex::encode(&item.sha256))) .map(|item| (item.rsync_uri.clone(), hex::encode(&item.sha256)))
.collect::<BTreeMap<_, _>>(); .collect::<BTreeMap<_, _>>();
object_hashes = objects object_hashes = objects
@ -98,8 +99,7 @@ fn load_sample_parts(
.map(|(uri, hash)| object_hash_key(uri, hash)) .map(|(uri, hash)| object_hash_key(uri, hash))
.collect::<BTreeSet<_>>(); .collect::<BTreeSet<_>>();
rejects = cir rejects = cir
.rejected_objects .rejected_objects()
.iter()
.map(|item| item.object_uri.clone()) .map(|item| item.object_uri.clone())
.collect::<BTreeSet<_>>(); .collect::<BTreeSet<_>>();
} }

View File

@ -281,6 +281,19 @@ impl<'a> Rpkiv1PublicationPointRunner<'a> {
}) })
} }
fn fresh_failure_audit_entries_for_cir(
&self,
ca: &CaInstanceHandle,
fresh_err: &ManifestFreshError,
) -> Vec<ObjectAuditEntry> {
if !fresh_err.should_warn_when_current_instance_reused() {
return Vec::new();
}
self.rejected_manifest_audit_entry_for_failed_fetch(ca, fresh_err)
.into_iter()
.collect()
}
pub(crate) fn stage_fresh_publication_point_after_repo_ready( pub(crate) fn stage_fresh_publication_point_after_repo_ready(
&self, &self,
ca: &CaInstanceHandle, ca: &CaInstanceHandle,
@ -955,21 +968,15 @@ impl<'a> PublicationPointRunner for Rpkiv1PublicationPointRunner<'a> {
} }
crate::policy::CaFailedFetchPolicy::ReuseCurrentInstanceVcir => { crate::policy::CaFailedFetchPolicy::ReuseCurrentInstanceVcir => {
let projection_started = std::time::Instant::now(); let projection_started = std::time::Instant::now();
let mut projection = project_current_instance_vcir_on_failed_fetch( let projection = project_current_instance_vcir_on_failed_fetch(
self.store, self.store,
ca, ca,
&fresh_err, &fresh_err,
self.validation_time, self.validation_time,
) )
.map_err(|e| format!("failed fetch VCIR projection failed: {e}"))?; .map_err(|e| format!("failed fetch VCIR projection failed: {e}"))?;
if matches!( let fresh_failure_audits =
projection.source, self.fresh_failure_audit_entries_for_cir(ca, &fresh_err);
PublicationPointSource::FailedFetchNoCache
) && let Some(rejected_manifest) =
self.rejected_manifest_audit_entry_for_failed_fetch(ca, &fresh_err)
{
projection.objects.audit.push(rejected_manifest);
}
self.append_ccr_manifest_projection_from_reuse(&projection)?; self.append_ccr_manifest_projection_from_reuse(&projection)?;
let projection_ms = projection_started.elapsed().as_millis() as u64; let projection_ms = projection_started.elapsed().as_millis() as u64;
warnings.extend(projection.warnings.clone()); warnings.extend(projection.warnings.clone());
@ -986,6 +993,7 @@ impl<'a> PublicationPointRunner for Rpkiv1PublicationPointRunner<'a> {
&warnings, &warnings,
&projection.objects, &projection.objects,
&projection.child_audits, &projection.child_audits,
&fresh_failure_audits,
); );
let audit_build_ms = audit_build_started.elapsed().as_millis() as u64; let audit_build_ms = audit_build_started.elapsed().as_millis() as u64;
let result = PublicationPointRunResult { let result = PublicationPointRunResult {
@ -1922,7 +1930,9 @@ fn build_publication_point_audit_from_snapshot(
next_update_rfc3339_utc: pack.next_update.rfc3339_utc.clone(), next_update_rfc3339_utc: pack.next_update.rfc3339_utc.clone(),
verified_at_rfc3339_utc: pack.verified_at.rfc3339_utc.clone(), verified_at_rfc3339_utc: pack.verified_at.rfc3339_utc.clone(),
warnings, warnings,
objects: objects_out, objects: objects_out.clone(),
cir_fresh_objects: objects_out,
cir_cached_objects: Vec::new(),
} }
} }
@ -1938,6 +1948,7 @@ fn build_publication_point_audit_from_vcir(
runner_warnings: &[Warning], runner_warnings: &[Warning],
objects: &crate::validation::objects::ObjectsOutput, objects: &crate::validation::objects::ObjectsOutput,
child_audits: &[ObjectAuditEntry], child_audits: &[ObjectAuditEntry],
fresh_failure_audits: &[ObjectAuditEntry],
) -> PublicationPointAudit { ) -> PublicationPointAudit {
if let Some(pack) = pack { if let Some(pack) = pack {
return build_publication_point_audit_from_snapshot( return build_publication_point_audit_from_snapshot(
@ -1977,14 +1988,19 @@ fn build_publication_point_audit_from_vcir(
next_update_rfc3339_utc: String::new(), next_update_rfc3339_utc: String::new(),
verified_at_rfc3339_utc: String::new(), verified_at_rfc3339_utc: String::new(),
warnings, warnings,
objects: Vec::new(), objects: fresh_failure_audits.to_vec(),
cir_fresh_objects: fresh_failure_audits.to_vec(),
cir_cached_objects: Vec::new(),
}; };
}; };
if source == PublicationPointSource::FailedFetchNoCache { if source == PublicationPointSource::FailedFetchNoCache {
let mut objects_out = Vec::with_capacity(objects.audit.len() + child_audits.len()); let mut objects_out = Vec::with_capacity(
objects.audit.len() + child_audits.len() + fresh_failure_audits.len(),
);
objects_out.extend(child_audits.iter().cloned()); objects_out.extend(child_audits.iter().cloned());
objects_out.extend(objects.audit.iter().cloned()); objects_out.extend(objects.audit.iter().cloned());
objects_out.extend(fresh_failure_audits.iter().cloned());
return PublicationPointAudit { return PublicationPointAudit {
node_id: None, node_id: None,
parent_node_id: None, parent_node_id: None,
@ -2011,7 +2027,9 @@ fn build_publication_point_audit_from_vcir(
.clone(), .clone(),
verified_at_rfc3339_utc: vcir.last_successful_validation_time.rfc3339_utc.clone(), verified_at_rfc3339_utc: vcir.last_successful_validation_time.rfc3339_utc.clone(),
warnings, warnings,
objects: objects_out, objects: objects_out.clone(),
cir_fresh_objects: objects_out,
cir_cached_objects: Vec::new(),
}; };
} }
@ -2076,6 +2094,9 @@ fn build_publication_point_audit_from_vcir(
} }
} }
let mut audit_objects = objects_out.clone();
audit_objects.extend(fresh_failure_audits.iter().cloned());
PublicationPointAudit { PublicationPointAudit {
node_id: None, node_id: None,
parent_node_id: None, parent_node_id: None,
@ -2102,7 +2123,9 @@ fn build_publication_point_audit_from_vcir(
.clone(), .clone(),
verified_at_rfc3339_utc: vcir.last_successful_validation_time.rfc3339_utc.clone(), verified_at_rfc3339_utc: vcir.last_successful_validation_time.rfc3339_utc.clone(),
warnings, warnings,
objects: objects_out, objects: audit_objects,
cir_fresh_objects: fresh_failure_audits.to_vec(),
cir_cached_objects: objects_out,
} }
} }

View File

@ -3324,6 +3324,7 @@ fn build_publication_point_audit_from_vcir_uses_vcir_metadata_and_overlays_child
&runner_warnings, &runner_warnings,
&objects, &objects,
&child_audits, &child_audits,
&[],
); );
assert_eq!(audit.source, "vcir_current_instance"); assert_eq!(audit.source, "vcir_current_instance");
@ -3413,6 +3414,7 @@ fn build_publication_point_audit_from_vcir_failed_no_cache_keeps_current_reject_
&[Warning::new("latest VCIR instance_gate expired")], &[Warning::new("latest VCIR instance_gate expired")],
&objects, &objects,
&[], &[],
&[],
); );
assert_eq!(audit.source, "failed_fetch_no_cache"); assert_eq!(audit.source, "failed_fetch_no_cache");
@ -3537,6 +3539,7 @@ fn build_publication_point_audit_from_vcir_without_cached_inputs_returns_empty_l
roa_cache_stats: crate::validation::objects::RoaValidationCacheStats::default(), roa_cache_stats: crate::validation::objects::RoaValidationCacheStats::default(),
}, },
&[], &[],
&[],
); );
assert_eq!(audit.source, "failed_fetch_no_cache"); assert_eq!(audit.source, "failed_fetch_no_cache");

View File

@ -5,10 +5,7 @@ use rpki::ccr::{
CcrContentInfo, CcrDigestAlgorithm, RpkiCanonicalCacheRepresentation, TrustAnchorState, CcrContentInfo, CcrDigestAlgorithm, RpkiCanonicalCacheRepresentation, TrustAnchorState,
encode_content_info, encode_content_info,
}; };
use rpki::cir::{ use rpki::cir::{CanonicalInputRepresentation, CirObject, CirTrustAnchor, encode_cir, sha256};
CIR_VERSION_V3, CanonicalInputRepresentation, CirHashAlgorithm, CirObject, CirTrustAnchor,
compute_reject_list_sha256, encode_cir, sha256,
};
fn skip_heavy_blackbox_test() -> bool { fn skip_heavy_blackbox_test() -> bool {
std::env::var_os("RPKI_SKIP_HEAVY_BLACKBOX_TESTS").is_some() std::env::var_os("RPKI_SKIP_HEAVY_BLACKBOX_TESTS").is_some()
@ -44,31 +41,28 @@ fn cir_full_and_delta_pair_reuses_shared_repo_bytes_db() {
}; };
let trust_anchors = vec![test_trust_anchor()]; let trust_anchors = vec![test_trust_anchor()];
let full_cir = CanonicalInputRepresentation { let full_cir = CanonicalInputRepresentation::new_v4(
version: CIR_VERSION_V3, time::OffsetDateTime::parse(
hash_alg: CirHashAlgorithm::Sha256,
validation_time: time::OffsetDateTime::parse(
"2026-03-16T11:49:15Z", "2026-03-16T11:49:15Z",
&time::format_description::well_known::Rfc3339, &time::format_description::well_known::Rfc3339,
) )
.unwrap(), .unwrap(),
objects: vec![CirObject { vec![CirObject {
rsync_uri: "rsync://example.net/repo/full.roa".to_string(), rsync_uri: "rsync://example.net/repo/full.roa".to_string(),
sha256: hex::decode(&full_obj_hash).unwrap(), sha256: hex::decode(&full_obj_hash).unwrap(),
}], }],
trust_anchors: trust_anchors.clone(), Vec::new(),
reject_list_sha256: compute_reject_list_sha256(std::iter::empty::<&str>()), trust_anchors.clone(),
rejected_objects: Vec::new(), Vec::new(),
}; Vec::new(),
let delta_cir = CanonicalInputRepresentation { );
version: CIR_VERSION_V3, let delta_cir = CanonicalInputRepresentation::new_v4(
hash_alg: CirHashAlgorithm::Sha256, time::OffsetDateTime::parse(
validation_time: time::OffsetDateTime::parse(
"2026-03-16T11:50:15Z", "2026-03-16T11:50:15Z",
&time::format_description::well_known::Rfc3339, &time::format_description::well_known::Rfc3339,
) )
.unwrap(), .unwrap(),
objects: { {
let mut objects = vec![ let mut objects = vec![
CirObject { CirObject {
rsync_uri: "rsync://example.net/repo/full.roa".to_string(), rsync_uri: "rsync://example.net/repo/full.roa".to_string(),
@ -82,10 +76,11 @@ fn cir_full_and_delta_pair_reuses_shared_repo_bytes_db() {
objects.sort_by(|a, b| a.rsync_uri.cmp(&b.rsync_uri)); objects.sort_by(|a, b| a.rsync_uri.cmp(&b.rsync_uri));
objects objects
}, },
Vec::new(),
trust_anchors, trust_anchors,
reject_list_sha256: compute_reject_list_sha256(std::iter::empty::<&str>()), Vec::new(),
rejected_objects: Vec::new(), Vec::new(),
}; );
let empty_ccr = CcrContentInfo::new(RpkiCanonicalCacheRepresentation { let empty_ccr = CcrContentInfo::new(RpkiCanonicalCacheRepresentation {
version: 0, version: 0,
hash_alg: CcrDigestAlgorithm::Sha256, hash_alg: CcrDigestAlgorithm::Sha256,
@ -214,8 +209,8 @@ fi
rpki::cir::decode_cir(&std::fs::read(out.join("delta-001").join("input.cir")).unwrap()) rpki::cir::decode_cir(&std::fs::read(out.join("delta-001").join("input.cir")).unwrap())
.expect("decode delta cir"); .expect("decode delta cir");
assert!(!full_cir.objects.is_empty()); assert!(full_cir.validated_object_count() > 0);
assert!(!delta_cir.objects.is_empty()); assert!(delta_cir.validated_object_count() > 0);
let summary: serde_json::Value = let summary: serde_json::Value =
serde_json::from_slice(&std::fs::read(out.join("summary.json")).unwrap()).unwrap(); serde_json::from_slice(&std::fs::read(out.join("summary.json")).unwrap()).unwrap();

View File

@ -6,10 +6,7 @@ use rpki::ccr::{
CcrContentInfo, CcrDigestAlgorithm, RpkiCanonicalCacheRepresentation, TrustAnchorState, CcrContentInfo, CcrDigestAlgorithm, RpkiCanonicalCacheRepresentation, TrustAnchorState,
encode_content_info, encode_content_info,
}; };
use rpki::cir::{ use rpki::cir::{CanonicalInputRepresentation, CirObject, CirTrustAnchor, encode_cir, sha256};
CIR_VERSION_V3, CanonicalInputRepresentation, CirHashAlgorithm, CirObject, CirTrustAnchor,
compute_reject_list_sha256, encode_cir, sha256,
};
#[test] #[test]
fn cir_drop_report_counts_dropped_roa_objects_and_vrps() { fn cir_drop_report_counts_dropped_roa_objects_and_vrps() {
@ -33,22 +30,21 @@ fn cir_drop_report_counts_dropped_roa_objects_and_vrps() {
.put_blob_bytes_batch(&[(hash.clone(), roa_bytes.clone())]) .put_blob_bytes_batch(&[(hash.clone(), roa_bytes.clone())])
.expect("write repo bytes"); .expect("write repo bytes");
let cir = CanonicalInputRepresentation { let cir = CanonicalInputRepresentation::new_v4(
version: CIR_VERSION_V3, time::OffsetDateTime::parse(
hash_alg: CirHashAlgorithm::Sha256,
validation_time: time::OffsetDateTime::parse(
"2026-04-09T00:00:00Z", "2026-04-09T00:00:00Z",
&time::format_description::well_known::Rfc3339, &time::format_description::well_known::Rfc3339,
) )
.unwrap(), .unwrap(),
objects: vec![CirObject { vec![CirObject {
rsync_uri: "rsync://example.net/repo/AS4538.roa".to_string(), rsync_uri: "rsync://example.net/repo/AS4538.roa".to_string(),
sha256: hex::decode(&hash).unwrap(), sha256: hex::decode(&hash).unwrap(),
}], }],
trust_anchors: vec![test_trust_anchor()], Vec::new(),
reject_list_sha256: compute_reject_list_sha256(std::iter::empty::<&str>()), vec![test_trust_anchor()],
rejected_objects: Vec::new(), Vec::new(),
}; Vec::new(),
);
std::fs::write(&cir_path, encode_cir(&cir).unwrap()).unwrap(); std::fs::write(&cir_path, encode_cir(&cir).unwrap()).unwrap();
let ccr = CcrContentInfo::new(RpkiCanonicalCacheRepresentation { let ccr = CcrContentInfo::new(RpkiCanonicalCacheRepresentation {

View File

@ -3,8 +3,8 @@ use std::process::Command;
use rpki::blob_store::ExternalRepoBytesDb; use rpki::blob_store::ExternalRepoBytesDb;
use rpki::cir::{ use rpki::cir::{
CIR_VERSION_V3, CanonicalInputRepresentation, CirHashAlgorithm, CirTrustAnchor, CanonicalInputRepresentation, CirTrustAnchor, encode_cir, materialize_cir_from_repo_bytes,
compute_reject_list_sha256, encode_cir, materialize_cir_from_repo_bytes, sha256, sha256,
}; };
fn skip_heavy_script_replay_test() -> bool { fn skip_heavy_script_replay_test() -> bool {
@ -31,25 +31,24 @@ fn build_ta_only_cir() -> (CanonicalInputRepresentation, Vec<u8>) {
.as_str() .as_str()
.to_string(); .to_string();
( (
CanonicalInputRepresentation { CanonicalInputRepresentation::new_v4(
version: CIR_VERSION_V3, time::OffsetDateTime::parse(
hash_alg: CirHashAlgorithm::Sha256,
validation_time: time::OffsetDateTime::parse(
"2026-04-07T00:00:00Z", "2026-04-07T00:00:00Z",
&time::format_description::well_known::Rfc3339, &time::format_description::well_known::Rfc3339,
) )
.unwrap(), .unwrap(),
objects: Vec::new(), Vec::new(),
trust_anchors: vec![CirTrustAnchor { Vec::new(),
vec![CirTrustAnchor {
ta_rsync_uri, ta_rsync_uri,
tal_uri: "https://example.test/root.tal".to_string(), tal_uri: "https://example.test/root.tal".to_string(),
tal_bytes, tal_bytes,
ta_certificate_sha256: sha256(&ta_bytes), ta_certificate_sha256: sha256(&ta_bytes),
ta_certificate_der: ta_bytes.clone(), ta_certificate_der: ta_bytes.clone(),
}], }],
reject_list_sha256: compute_reject_list_sha256(std::iter::empty::<&str>()), Vec::new(),
rejected_objects: Vec::new(), Vec::new(),
}, ),
ta_bytes, ta_bytes,
) )
} }

View File

@ -3,8 +3,8 @@ use std::process::Command;
use rpki::blob_store::ExternalRepoBytesDb; use rpki::blob_store::ExternalRepoBytesDb;
use rpki::cir::{ use rpki::cir::{
CIR_VERSION_V3, CanonicalInputRepresentation, CirHashAlgorithm, CirTrustAnchor, CanonicalInputRepresentation, CirTrustAnchor, encode_cir, materialize_cir_from_repo_bytes,
compute_reject_list_sha256, encode_cir, materialize_cir_from_repo_bytes, sha256, sha256,
}; };
fn skip_heavy_script_replay_test() -> bool { fn skip_heavy_script_replay_test() -> bool {
@ -31,25 +31,24 @@ fn build_ta_only_cir() -> (CanonicalInputRepresentation, Vec<u8>) {
.as_str() .as_str()
.to_string(); .to_string();
( (
CanonicalInputRepresentation { CanonicalInputRepresentation::new_v4(
version: CIR_VERSION_V3, time::OffsetDateTime::parse(
hash_alg: CirHashAlgorithm::Sha256,
validation_time: time::OffsetDateTime::parse(
"2026-04-07T00:00:00Z", "2026-04-07T00:00:00Z",
&time::format_description::well_known::Rfc3339, &time::format_description::well_known::Rfc3339,
) )
.unwrap(), .unwrap(),
objects: Vec::new(), Vec::new(),
trust_anchors: vec![CirTrustAnchor { Vec::new(),
vec![CirTrustAnchor {
ta_rsync_uri, ta_rsync_uri,
tal_uri: "https://example.test/root.tal".to_string(), tal_uri: "https://example.test/root.tal".to_string(),
tal_bytes, tal_bytes,
ta_certificate_sha256: sha256(&ta_bytes), ta_certificate_sha256: sha256(&ta_bytes),
ta_certificate_der: ta_bytes.clone(), ta_certificate_der: ta_bytes.clone(),
}], }],
reject_list_sha256: compute_reject_list_sha256(std::iter::empty::<&str>()), Vec::new(),
rejected_objects: Vec::new(), Vec::new(),
}, ),
ta_bytes, ta_bytes,
) )
} }

View File

@ -5,10 +5,7 @@ use rpki::ccr::{
CcrContentInfo, CcrDigestAlgorithm, RpkiCanonicalCacheRepresentation, TrustAnchorState, CcrContentInfo, CcrDigestAlgorithm, RpkiCanonicalCacheRepresentation, TrustAnchorState,
encode_content_info, encode_content_info,
}; };
use rpki::cir::{ use rpki::cir::{CanonicalInputRepresentation, CirObject, CirTrustAnchor, encode_cir, sha256};
CIR_VERSION_V3, CanonicalInputRepresentation, CirHashAlgorithm, CirObject, CirTrustAnchor,
compute_reject_list_sha256, encode_cir, sha256,
};
fn skip_heavy_blackbox_test() -> bool { fn skip_heavy_blackbox_test() -> bool {
std::env::var_os("RPKI_SKIP_HEAVY_BLACKBOX_TESTS").is_some() std::env::var_os("RPKI_SKIP_HEAVY_BLACKBOX_TESTS").is_some()
@ -35,21 +32,19 @@ fn cir_offline_sequence_writes_parseable_sequence_json_and_steps() {
) )
.unwrap(); .unwrap();
let mk_cir = |uri: &str, hash_hex: &str, vt: &str| CanonicalInputRepresentation { let mk_cir = |uri: &str, hash_hex: &str, vt: &str| {
version: CIR_VERSION_V3, CanonicalInputRepresentation::new_v4(
hash_alg: CirHashAlgorithm::Sha256, time::OffsetDateTime::parse(vt, &time::format_description::well_known::Rfc3339)
validation_time: time::OffsetDateTime::parse( .unwrap(),
vt, vec![CirObject {
&time::format_description::well_known::Rfc3339, rsync_uri: uri.to_string(),
sha256: hex::decode(hash_hex).unwrap(),
}],
Vec::new(),
vec![test_trust_anchor()],
Vec::new(),
Vec::new(),
) )
.unwrap(),
objects: vec![CirObject {
rsync_uri: uri.to_string(),
sha256: hex::decode(hash_hex).unwrap(),
}],
trust_anchors: vec![test_trust_anchor()],
reject_list_sha256: compute_reject_list_sha256(std::iter::empty::<&str>()),
rejected_objects: Vec::new(),
}; };
let full_hash = { let full_hash = {
use sha2::{Digest, Sha256}; use sha2::{Digest, Sha256};
@ -64,17 +59,15 @@ fn cir_offline_sequence_writes_parseable_sequence_json_and_steps() {
&full_hash, &full_hash,
"2026-03-16T11:49:15Z", "2026-03-16T11:49:15Z",
); );
let delta_cir = CanonicalInputRepresentation { let delta_cir = CanonicalInputRepresentation::new_v4(
version: CIR_VERSION_V3, time::OffsetDateTime::parse(
hash_alg: CirHashAlgorithm::Sha256,
validation_time: time::OffsetDateTime::parse(
"2026-03-16T11:50:15Z", "2026-03-16T11:50:15Z",
&time::format_description::well_known::Rfc3339, &time::format_description::well_known::Rfc3339,
) )
.unwrap(), .unwrap(),
objects: { {
let mut objects = vec![ let mut objects = vec![
full_cir.objects[0].clone(), full_cir.fresh_validated_objects[0].clone(),
CirObject { CirObject {
rsync_uri: "rsync://example.net/repo/delta.roa".to_string(), rsync_uri: "rsync://example.net/repo/delta.roa".to_string(),
sha256: hex::decode(&delta_hash).unwrap(), sha256: hex::decode(&delta_hash).unwrap(),
@ -83,10 +76,11 @@ fn cir_offline_sequence_writes_parseable_sequence_json_and_steps() {
objects.sort_by(|a, b| a.rsync_uri.cmp(&b.rsync_uri)); objects.sort_by(|a, b| a.rsync_uri.cmp(&b.rsync_uri));
objects objects
}, },
trust_anchors: full_cir.trust_anchors.clone(), Vec::new(),
reject_list_sha256: compute_reject_list_sha256(std::iter::empty::<&str>()), full_cir.trust_anchors.clone(),
rejected_objects: Vec::new(), Vec::new(),
}; Vec::new(),
);
let empty_ccr = CcrContentInfo::new(RpkiCanonicalCacheRepresentation { let empty_ccr = CcrContentInfo::new(RpkiCanonicalCacheRepresentation {
version: 0, version: 0,
hash_alg: CcrDigestAlgorithm::Sha256, hash_alg: CcrDigestAlgorithm::Sha256,

View File

@ -3,8 +3,8 @@ use std::process::Command;
use rpki::blob_store::ExternalRepoBytesDb; use rpki::blob_store::ExternalRepoBytesDb;
use rpki::cir::{ use rpki::cir::{
CIR_VERSION_V3, CanonicalInputRepresentation, CirHashAlgorithm, CirTrustAnchor, CanonicalInputRepresentation, CirTrustAnchor, encode_cir, materialize_cir_from_repo_bytes,
compute_reject_list_sha256, encode_cir, materialize_cir_from_repo_bytes, sha256, sha256,
}; };
fn skip_heavy_script_replay_test() -> bool { fn skip_heavy_script_replay_test() -> bool {
@ -31,25 +31,24 @@ fn build_ta_only_cir() -> (CanonicalInputRepresentation, Vec<u8>) {
.as_str() .as_str()
.to_string(); .to_string();
( (
CanonicalInputRepresentation { CanonicalInputRepresentation::new_v4(
version: CIR_VERSION_V3, time::OffsetDateTime::parse(
hash_alg: CirHashAlgorithm::Sha256,
validation_time: time::OffsetDateTime::parse(
"2026-04-07T00:00:00Z", "2026-04-07T00:00:00Z",
&time::format_description::well_known::Rfc3339, &time::format_description::well_known::Rfc3339,
) )
.unwrap(), .unwrap(),
objects: Vec::new(), Vec::new(),
trust_anchors: vec![CirTrustAnchor { Vec::new(),
vec![CirTrustAnchor {
ta_rsync_uri, ta_rsync_uri,
tal_uri: "https://example.test/root.tal".to_string(), tal_uri: "https://example.test/root.tal".to_string(),
tal_bytes, tal_bytes,
ta_certificate_sha256: sha256(&ta_bytes), ta_certificate_sha256: sha256(&ta_bytes),
ta_certificate_der: ta_bytes.clone(), ta_certificate_der: ta_bytes.clone(),
}], }],
reject_list_sha256: compute_reject_list_sha256(std::iter::empty::<&str>()), Vec::new(),
rejected_objects: Vec::new(), Vec::new(),
}, ),
ta_bytes, ta_bytes,
) )
} }

View File

@ -3,8 +3,8 @@ use std::process::Command;
use rpki::blob_store::ExternalRepoBytesDb; use rpki::blob_store::ExternalRepoBytesDb;
use rpki::cir::{ use rpki::cir::{
CIR_VERSION_V3, CanonicalInputRepresentation, CirHashAlgorithm, CirTrustAnchor, CanonicalInputRepresentation, CirTrustAnchor, encode_cir, materialize_cir_from_repo_bytes,
compute_reject_list_sha256, encode_cir, materialize_cir_from_repo_bytes, sha256, sha256,
}; };
fn skip_heavy_script_replay_test() -> bool { fn skip_heavy_script_replay_test() -> bool {
@ -31,25 +31,24 @@ fn build_ta_only_cir() -> (CanonicalInputRepresentation, Vec<u8>) {
.as_str() .as_str()
.to_string(); .to_string();
( (
CanonicalInputRepresentation { CanonicalInputRepresentation::new_v4(
version: CIR_VERSION_V3, time::OffsetDateTime::parse(
hash_alg: CirHashAlgorithm::Sha256,
validation_time: time::OffsetDateTime::parse(
"2026-04-07T00:00:00Z", "2026-04-07T00:00:00Z",
&time::format_description::well_known::Rfc3339, &time::format_description::well_known::Rfc3339,
) )
.unwrap(), .unwrap(),
objects: Vec::new(), Vec::new(),
trust_anchors: vec![CirTrustAnchor { Vec::new(),
vec![CirTrustAnchor {
ta_rsync_uri, ta_rsync_uri,
tal_uri: "https://example.test/root.tal".to_string(), tal_uri: "https://example.test/root.tal".to_string(),
tal_bytes, tal_bytes,
ta_certificate_sha256: sha256(&ta_bytes), ta_certificate_sha256: sha256(&ta_bytes),
ta_certificate_der: ta_bytes.clone(), ta_certificate_der: ta_bytes.clone(),
}], }],
reject_list_sha256: compute_reject_list_sha256(std::iter::empty::<&str>()), Vec::new(),
rejected_objects: Vec::new(), Vec::new(),
}, ),
ta_bytes, ta_bytes,
) )
} }

View File

@ -134,8 +134,7 @@ fn cli_run_offline_mode_writes_cir_and_static_pool() {
); );
assert!(!cir.trust_anchors[0].ta_certificate_der.is_empty()); assert!(!cir.trust_anchors[0].ta_certificate_der.is_empty());
assert!( assert!(
!cir.objects !cir.validated_objects()
.iter()
.any(|item| item.rsync_uri == cir.trust_anchors[0].ta_rsync_uri) .any(|item| item.rsync_uri == cir.trust_anchors[0].ta_rsync_uri)
); );
} }