From adb68fd469313033f89bf3dc5a95a152b7e0acd3 Mon Sep 17 00:00:00 2001 From: yuyr Date: Wed, 24 Jun 2026 09:50:23 +0800 Subject: [PATCH] =?UTF-8?q?20260624=20=E4=BC=98=E5=8C=96ROA=20cache=20look?= =?UTF-8?q?up=E9=95=BF=E5=B0=BE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/storage.rs | 37 ++++ src/storage/tests.rs | 8 + src/validation/objects.rs | 430 ++++++++++++++++++++++++++++++++------ 3 files changed, 416 insertions(+), 59 deletions(-) diff --git a/src/storage.rs b/src/storage.rs index 4cd71b7..9473b2d 100644 --- a/src/storage.rs +++ b/src/storage.rs @@ -1093,6 +1093,8 @@ pub struct RoaCacheObjectProjection { pub ee_serial: Option>, #[serde(rename = "c", default, skip_serializing_if = "Option::is_none")] pub crl_uri: Option, + #[serde(rename = "x")] + pub outputs_effective_until_unix: i64, #[serde(rename = "o")] pub outputs: Vec, } @@ -1123,6 +1125,29 @@ impl RoaCacheObjectProjection { for output in &self.outputs { output.validate_internal()?; } + let expected_effective_until = self + .outputs + .iter() + .map(|output| { + output + .item_effective_until + .parse() + .map(|time| time.unix_timestamp()) + .map_err(|detail| StorageError::InvalidData { + entity: "roa_cache_projection.entries[].outputs_effective_until_unix", + detail, + }) + }) + .collect::>>()? + .into_iter() + .min() + .expect("outputs must not be empty"); + if self.outputs_effective_until_unix != expected_effective_until { + return Err(StorageError::InvalidData { + entity: "roa_cache_projection.entries[].outputs_effective_until_unix", + detail: "must equal the earliest output item_effective_until".to_string(), + }); + } Ok(()) } } @@ -1638,6 +1663,14 @@ impl RoaCacheProjection { else { continue; }; + let projected_output_effective_until = projected_output + .item_effective_until + .parse() + .map(|time| time.unix_timestamp()) + .map_err(|detail| StorageError::InvalidData { + entity: "roa_cache_projection.entries[].outputs_effective_until_unix", + detail, + })?; let meta = meta_by_uri .as_ref() .and_then(|meta| meta.get(output.source_object_uri.as_str()).copied()); @@ -1666,6 +1699,9 @@ impl RoaCacheProjection { ), }); } + entry.outputs_effective_until_unix = entry + .outputs_effective_until_unix + .min(projected_output_effective_until); entry.outputs.push(projected_output); } else { entry_index_by_uri.insert(output.source_object_uri.clone(), entries.len()); @@ -1674,6 +1710,7 @@ impl RoaCacheProjection { source_object_hash: output.source_object_hash, ee_serial: meta.map(|meta| meta.ee_serial.clone()), crl_uri: meta.map(|meta| meta.crl_uri.clone()), + outputs_effective_until_unix: projected_output_effective_until, outputs: vec![projected_output], }); } diff --git a/src/storage/tests.rs b/src/storage/tests.rs index ab4e906..86e9d77 100644 --- a/src/storage/tests.rs +++ b/src/storage/tests.rs @@ -313,6 +313,10 @@ fn roa_cache_projection_from_vcir_keeps_only_roa_vrp_outputs() { projection.entries[0].source_object_uri, "rsync://example.test/repo/object.roa" ); + assert_eq!( + projection.entries[0].outputs_effective_until_unix, + 12 * 3600 + ); assert_eq!(projection.entries[0].outputs.len(), 1); assert!(matches!( projection.entries[0].outputs[0].payload, @@ -337,6 +341,10 @@ fn roa_cache_projection_groups_multiple_outputs_by_roa_uri() { assert_eq!(projection.entries.len(), 1); assert_eq!(projection.entries[0].outputs.len(), 2); + assert_eq!( + projection.entries[0].outputs_effective_until_unix, + 12 * 3600 + ); } #[test] diff --git a/src/validation/objects.rs b/src/validation/objects.rs index 269b3a0..e3e3b95 100644 --- a/src/validation/objects.rs +++ b/src/validation/objects.rs @@ -16,8 +16,7 @@ use crate::parallel::object_worker::{ use crate::policy::{Policy, SignedObjectFailurePolicy}; use crate::report::{RfcRef, Warning}; use crate::storage::{ - PackFile, PackTime, RoaCacheObjectMeta, RoaCacheProjection, ValidatedCaInstanceResult, - VcirArtifactKind, VcirArtifactRole, VcirArtifactValidationStatus, VcirLocalOutput, + PackFile, PackTime, RoaCacheObjectMeta, RoaCacheProjection, VcirLocalOutput, VcirLocalOutputPayload, VcirOutputType, VcirSourceObjectType, }; use crate::validation::cert_path::{CertPathError, validate_signed_object_ee_cert_path_fast}; @@ -170,6 +169,11 @@ pub struct RoaValidationCacheStats { pub metadata_blocked_roas: usize, pub context_gate_nanos: u64, pub lookup_nanos: u64, + pub lookup_entry_gate_nanos: u64, + pub lookup_crl_gate_nanos: u64, + pub lookup_materialize_nanos: u64, + pub lookup_crl_gate_verified_crls: usize, + pub lookup_crl_gate_reused_crls: usize, } #[derive(Clone, Copy, Debug)] @@ -232,6 +236,17 @@ impl RoaValidationCacheStats { .context_gate_nanos .saturating_add(other.context_gate_nanos); self.lookup_nanos = self.lookup_nanos.saturating_add(other.lookup_nanos); + self.lookup_entry_gate_nanos = self + .lookup_entry_gate_nanos + .saturating_add(other.lookup_entry_gate_nanos); + self.lookup_crl_gate_nanos = self + .lookup_crl_gate_nanos + .saturating_add(other.lookup_crl_gate_nanos); + self.lookup_materialize_nanos = self + .lookup_materialize_nanos + .saturating_add(other.lookup_materialize_nanos); + self.lookup_crl_gate_verified_crls += other.lookup_crl_gate_verified_crls; + self.lookup_crl_gate_reused_crls += other.lookup_crl_gate_reused_crls; } fn for_input(input: RoaValidationCacheInput<'_>, roa_total: usize) -> Self { @@ -251,6 +266,21 @@ impl RoaValidationCacheStats { stats } + fn record_lookup(&mut self, total_nanos: u64, metrics: RoaCacheLookupMetrics) { + self.lookup_nanos = self.lookup_nanos.saturating_add(total_nanos); + self.lookup_entry_gate_nanos = self + .lookup_entry_gate_nanos + .saturating_add(metrics.entry_gate_nanos); + self.lookup_crl_gate_nanos = self + .lookup_crl_gate_nanos + .saturating_add(metrics.crl_gate_nanos); + self.lookup_materialize_nanos = self + .lookup_materialize_nanos + .saturating_add(metrics.materialize_nanos); + self.lookup_crl_gate_verified_crls += metrics.crl_gate_verified_crls; + self.lookup_crl_gate_reused_crls += metrics.crl_gate_reused_crls; + } + fn record_to_timing(&self, timing: Option<&TimingHandle>) { let Some(timing) = timing else { return; @@ -318,11 +348,48 @@ impl RoaValidationCacheStats { "roa_validation_cache_lookup_nanos", self.lookup_nanos as usize, ); + record_non_zero( + timing, + "roa_validation_cache_lookup_entry_gate_nanos", + self.lookup_entry_gate_nanos as usize, + ); + record_non_zero( + timing, + "roa_validation_cache_lookup_crl_gate_nanos", + self.lookup_crl_gate_nanos as usize, + ); + record_non_zero( + timing, + "roa_validation_cache_lookup_materialize_nanos", + self.lookup_materialize_nanos as usize, + ); + record_non_zero( + timing, + "roa_validation_cache_lookup_crl_gate_verified_crls", + self.lookup_crl_gate_verified_crls, + ); + record_non_zero( + timing, + "roa_validation_cache_lookup_crl_gate_reused_crls", + self.lookup_crl_gate_reused_crls, + ); timing.record_phase_nanos( "roa_validation_cache_context_gate_total", self.context_gate_nanos, ); timing.record_phase_nanos("roa_validation_cache_lookup_total", self.lookup_nanos); + timing.record_phase_nanos( + "roa_validation_cache_lookup_entry_gate_total", + self.lookup_entry_gate_nanos, + ); + timing.record_phase_nanos( + "roa_validation_cache_lookup_crl_gate_total", + self.lookup_crl_gate_nanos, + ); + timing.record_phase_nanos( + "roa_validation_cache_lookup_materialize_total", + self.lookup_materialize_nanos, + ); } } @@ -332,6 +399,98 @@ fn record_non_zero(timing: &TimingHandle, key: &'static str, value: usize) { } } +fn elapsed_nanos_u64(started: Instant) -> u64 { + started.elapsed().as_nanos().min(u128::from(u64::MAX)) as u64 +} + +#[derive(Clone, Debug, Default, PartialEq, Eq)] +struct RoaCacheLookupMetrics { + entry_gate_nanos: u64, + crl_gate_nanos: u64, + materialize_nanos: u64, + crl_gate_verified_crls: usize, + crl_gate_reused_crls: usize, +} + +#[derive(Clone, Debug)] +enum RoaCacheCrlGate { + Unchanged, + ChangedValid(Arc), + Expired, + Invalid, + Missing, +} + +#[derive(Debug, Default)] +struct RoaCacheCrlGateSet { + gates_by_uri: HashMap, +} + +impl RoaCacheCrlGateSet { + fn evaluate( + &mut self, + crl_uri: &str, + expected_crl_sha256_by_uri: &HashMap, + crl_cache: &mut std::collections::HashMap, + issuer_ca_der: &[u8], + validation_time: time::OffsetDateTime, + metrics: &mut RoaCacheLookupMetrics, + ) -> RoaCacheCrlGate { + if let Some(gate) = self.gates_by_uri.get(crl_uri) { + metrics.crl_gate_reused_crls += 1; + return gate.clone(); + } + + let gate = evaluate_roa_cache_crl_gate( + crl_uri, + expected_crl_sha256_by_uri, + crl_cache, + issuer_ca_der, + validation_time, + metrics, + ); + self.gates_by_uri.insert(crl_uri.to_string(), gate.clone()); + gate + } +} + +fn evaluate_roa_cache_crl_gate( + crl_uri: &str, + expected_crl_sha256_by_uri: &HashMap, + crl_cache: &mut std::collections::HashMap, + issuer_ca_der: &[u8], + validation_time: time::OffsetDateTime, + metrics: &mut RoaCacheLookupMetrics, +) -> RoaCacheCrlGate { + let crl_unchanged = { + let Some(current_crl_hash) = crl_cache + .get_mut(crl_uri) + .map(CachedIssuerCrl::current_sha256_hex) + else { + return RoaCacheCrlGate::Missing; + }; + expected_crl_sha256_by_uri + .get(crl_uri) + .map(|expected| expected == current_crl_hash) + .unwrap_or(false) + }; + if crl_unchanged { + return RoaCacheCrlGate::Unchanged; + } + + let verified_crl = match ensure_issuer_crl_verified(crl_uri, crl_cache, issuer_ca_der) { + Ok(verified_crl) => { + metrics.crl_gate_verified_crls += 1; + verified_crl + } + Err(_) => return RoaCacheCrlGate::Invalid, + }; + if !crl_valid_at_time(&verified_crl.crl, validation_time) { + return RoaCacheCrlGate::Expired; + } + RoaCacheCrlGate::ChangedValid(verified_crl) +} + #[derive(Clone, Debug)] pub struct RoaValidationCacheView { entries_by_uri: HashMap, @@ -347,6 +506,7 @@ pub struct CachedRoaValidationResult { source_object_hash: [u8; 32], ee_serial: Option>, crl_uri: Option, + outputs_effective_until_unix: i64, outputs: Vec, } @@ -403,6 +563,7 @@ impl RoaValidationCacheView { source_object_hash: entry.source_object_hash, ee_serial: entry.ee_serial.clone(), crl_uri: entry.crl_uri.clone(), + outputs_effective_until_unix: entry.outputs_effective_until_unix, outputs, }, ); @@ -461,59 +622,112 @@ impl RoaValidationCacheView { issuer_ca_der: &[u8], validation_time: time::OffsetDateTime, ) -> RoaCacheLookupResult { + self.lookup_with_metrics(file, crl_cache, issuer_ca_der, validation_time, None) + .0 + } + + fn lookup_with_metrics( + &self, + file: &PackFile, + crl_cache: &mut std::collections::HashMap, + issuer_ca_der: &[u8], + validation_time: time::OffsetDateTime, + crl_gate_set: Option<&mut RoaCacheCrlGateSet>, + ) -> (RoaCacheLookupResult, RoaCacheLookupMetrics) { + let mut metrics = RoaCacheLookupMetrics::default(); + let entry_gate_started = Instant::now(); + macro_rules! return_entry_gate { + ($result:expr) => {{ + metrics.entry_gate_nanos = metrics + .entry_gate_nanos + .saturating_add(elapsed_nanos_u64(entry_gate_started)); + return ($result, metrics); + }}; + } + macro_rules! return_crl_gate { + ($started:expr, $result:expr) => {{ + metrics.crl_gate_nanos = metrics + .crl_gate_nanos + .saturating_add(elapsed_nanos_u64($started)); + return ($result, metrics); + }}; + } + macro_rules! return_materialize { + ($started:expr, $result:expr) => {{ + metrics.materialize_nanos = metrics + .materialize_nanos + .saturating_add(elapsed_nanos_u64($started)); + return ($result, metrics); + }}; + } + if self.blocked { - return RoaCacheLookupResult::ExpiredBlocked; + return_entry_gate!(RoaCacheLookupResult::ExpiredBlocked); } let Some(cached) = self.entries_by_uri.get(file.rsync_uri.as_str()) else { - return RoaCacheLookupResult::Miss; + return_entry_gate!(RoaCacheLookupResult::Miss); }; if cached.source_object_hash != file.sha256 { - return RoaCacheLookupResult::HashBlocked; + return_entry_gate!(RoaCacheLookupResult::HashBlocked); } if cached.outputs.is_empty() { - return RoaCacheLookupResult::Miss; + return_entry_gate!(RoaCacheLookupResult::Miss); } - if cached.outputs.iter().any(|output| { - output - .item_effective_until - .parse() - .map_or(true, |t| t <= validation_time) - }) { - return RoaCacheLookupResult::ExpiredBlocked; + if cached.outputs_effective_until_unix <= validation_time.unix_timestamp() { + return_entry_gate!(RoaCacheLookupResult::ExpiredBlocked); } let Some(crl_uri) = cached.crl_uri.as_deref() else { - return RoaCacheLookupResult::MetadataBlocked; + return_entry_gate!(RoaCacheLookupResult::MetadataBlocked); }; let Some(ee_serial) = cached.ee_serial.as_ref() else { - return RoaCacheLookupResult::MetadataBlocked; + return_entry_gate!(RoaCacheLookupResult::MetadataBlocked); }; - let crl_unchanged = { - let Some(current_crl_hash) = crl_cache - .get_mut(crl_uri) - .map(CachedIssuerCrl::current_sha256_hex) - else { - return RoaCacheLookupResult::MetadataBlocked; - }; - self.crl_sha256_by_uri - .get(crl_uri) - .map(|expected| expected == current_crl_hash) - .unwrap_or(false) - }; - if !crl_unchanged { - let verified_crl = match ensure_issuer_crl_verified(crl_uri, crl_cache, issuer_ca_der) { - Ok(verified_crl) => verified_crl, - Err(_) => return RoaCacheLookupResult::MetadataBlocked, - }; - if !crl_valid_at_time(&verified_crl.crl, validation_time) { - return RoaCacheLookupResult::ExpiredBlocked; - } - if verified_crl.revoked_serials.contains(ee_serial) { - return RoaCacheLookupResult::RevokedBlocked; - } - } + metrics.entry_gate_nanos = metrics + .entry_gate_nanos + .saturating_add(elapsed_nanos_u64(entry_gate_started)); + let crl_gate_started = Instant::now(); + let crl_gate = if let Some(crl_gate_set) = crl_gate_set { + crl_gate_set.evaluate( + crl_uri, + &self.crl_sha256_by_uri, + crl_cache, + issuer_ca_der, + validation_time, + &mut metrics, + ) + } else { + evaluate_roa_cache_crl_gate( + crl_uri, + &self.crl_sha256_by_uri, + crl_cache, + issuer_ca_der, + validation_time, + &mut metrics, + ) + }; + let crl_rechecked = match crl_gate { + RoaCacheCrlGate::Unchanged => false, + RoaCacheCrlGate::ChangedValid(verified_crl) => { + if verified_crl.revoked_serials.contains(ee_serial) { + return_crl_gate!(crl_gate_started, RoaCacheLookupResult::RevokedBlocked); + } + true + } + RoaCacheCrlGate::Expired => { + return_crl_gate!(crl_gate_started, RoaCacheLookupResult::ExpiredBlocked); + } + RoaCacheCrlGate::Invalid | RoaCacheCrlGate::Missing => { + return_crl_gate!(crl_gate_started, RoaCacheLookupResult::MetadataBlocked); + } + }; + metrics.crl_gate_nanos = metrics + .crl_gate_nanos + .saturating_add(elapsed_nanos_u64(crl_gate_started)); + + let materialize_started = Instant::now(); let mut vrps = Vec::with_capacity(cached.outputs.len()); for output in &cached.outputs { let VcirLocalOutputPayload::Vrp { @@ -524,7 +738,7 @@ impl RoaValidationCacheView { max_length, } = &output.payload else { - return RoaCacheLookupResult::MetadataBlocked; + return_materialize!(materialize_started, RoaCacheLookupResult::MetadataBlocked); }; vrps.push(Vrp { asn: *asn, @@ -548,10 +762,13 @@ impl RoaValidationCacheView { crl_uri: crl_uri.to_string(), }), }; - if crl_unchanged { - RoaCacheLookupResult::Hit(ok) + metrics.materialize_nanos = metrics + .materialize_nanos + .saturating_add(elapsed_nanos_u64(materialize_started)); + if crl_rechecked { + (RoaCacheLookupResult::CrlRecheckHit(ok), metrics) } else { - RoaCacheLookupResult::CrlRecheckHit(ok) + (RoaCacheLookupResult::Hit(ok), metrics) } } } @@ -946,19 +1163,24 @@ pub fn process_publication_point_for_issuer_with_cache_options { roa_cache_stats.hit_roas += 1; @@ -1956,6 +2178,11 @@ pub(crate) fn prepare_publication_point_for_parallel_roa_with_cache { roa_cache_stats.hit_roas += 1; @@ -3330,8 +3557,9 @@ mod tests { use crate::policy::Policy; use crate::storage::{ PackTime, RoaCacheObjectMeta, RoaCacheProjection, RoaCacheProjectionContext, - ValidatedManifestMeta, VcirAuditSummary, VcirCcrManifestProjection, VcirInstanceGate, - VcirRelatedArtifact, VcirSummary, + ValidatedCaInstanceResult, ValidatedManifestMeta, VcirArtifactKind, VcirArtifactRole, + VcirArtifactValidationStatus, VcirAuditSummary, VcirCcrManifestProjection, + VcirInstanceGate, VcirRelatedArtifact, VcirSummary, }; use crate::validation::publication_point::PublicationPointSnapshot; use std::collections::HashMap; @@ -3551,6 +3779,50 @@ mod tests { assert!(matches!(second, RoaCacheLookupResult::Hit(_))); } + #[test] + fn roa_validation_cache_lookup_reuses_publication_point_crl_gate() { + let validation_time = fixed_time("2026-06-05T00:00:00Z"); + let issuer_der = b"issuer-ca"; + let crl_bytes = b"current-crl".to_vec(); + let crl_hash = sha256_32(&crl_bytes); + let roa_hash = [0x11; 32]; + let vcir = sample_roa_cache_vcir( + issuer_der, + crl_hash, + roa_hash, + fixed_time("2026-06-07T00:00:00Z"), + fixed_time("2026-06-08T00:00:00Z"), + ); + let projection = sample_roa_cache_projection(&vcir, roa_hash); + let view = RoaValidationCacheView::from_projection(&projection, validation_time); + let file = PackFile::from_bytes_with_sha256(TEST_ROA_URI, vec![0x01], roa_hash); + let mut crl_cache = sample_crl_cache(crl_bytes); + let mut gate_set = RoaCacheCrlGateSet::default(); + + let (first, first_metrics) = view.lookup_with_metrics( + &file, + &mut crl_cache, + issuer_der, + validation_time, + Some(&mut gate_set), + ); + assert!(matches!(first, RoaCacheLookupResult::Hit(_))); + assert_eq!(first_metrics.crl_gate_reused_crls, 0); + assert_eq!(gate_set.gates_by_uri.len(), 1); + + let (second, second_metrics) = view.lookup_with_metrics( + &file, + &mut crl_cache, + issuer_der, + validation_time, + Some(&mut gate_set), + ); + assert!(matches!(second, RoaCacheLookupResult::Hit(_))); + assert_eq!(second_metrics.crl_gate_reused_crls, 1); + assert_eq!(second_metrics.crl_gate_verified_crls, 0); + assert_eq!(gate_set.gates_by_uri.len(), 1); + } + #[test] fn cached_verified_crl_reports_hash_and_validity_window() { let crl_bytes = fixture_bytes( @@ -3592,6 +3864,10 @@ mod tests { let projection = sample_roa_cache_projection(&vcir, roa_hash); assert_eq!(projection.parent_context_digest, Some(TEST_PARENT_CONTEXT)); assert_eq!(projection.policy_fingerprint, Some(TEST_POLICY_FINGERPRINT)); + assert_eq!( + projection.entries[0].outputs_effective_until_unix, + fixed_time("2026-06-07T00:00:00Z").unix_timestamp() + ); assert_eq!( projection.entries[0].ee_serial.as_deref(), Some(&[0x01][..]) @@ -3775,6 +4051,11 @@ mod tests { stats.context_blocked_roas = 3; stats.context_gate_nanos = 4; stats.lookup_nanos = 5; + stats.lookup_entry_gate_nanos = 6; + stats.lookup_crl_gate_nanos = 7; + stats.lookup_materialize_nanos = 8; + stats.lookup_crl_gate_verified_crls = 9; + stats.lookup_crl_gate_reused_crls = 10; let timing = TimingHandle::new(TimingMeta { recorded_at_utc_rfc3339: "2026-06-05T00:00:00Z".to_string(), @@ -3822,6 +4103,26 @@ mod tests { 4 ); assert_eq!(report["counts"]["roa_validation_cache_lookup_nanos"], 5); + assert_eq!( + report["counts"]["roa_validation_cache_lookup_entry_gate_nanos"], + 6 + ); + assert_eq!( + report["counts"]["roa_validation_cache_lookup_crl_gate_nanos"], + 7 + ); + assert_eq!( + report["counts"]["roa_validation_cache_lookup_materialize_nanos"], + 8 + ); + assert_eq!( + report["counts"]["roa_validation_cache_lookup_crl_gate_verified_crls"], + 9 + ); + assert_eq!( + report["counts"]["roa_validation_cache_lookup_crl_gate_reused_crls"], + 10 + ); } #[test] @@ -3960,6 +4261,11 @@ mod tests { ) .local_outputs .remove(0); + let outputs_effective_until_unix = output + .item_effective_until + .parse() + .expect("parse output effective until") + .unix_timestamp(); let mut view = RoaValidationCacheView { entries_by_uri: HashMap::new(), issuer_ca_sha256_hex: Some(sha256_hex(issuer_der)), @@ -3981,6 +4287,7 @@ mod tests { source_object_hash: roa_hash, ee_serial: Some(vec![0x01]), crl_uri: None, + outputs_effective_until_unix, outputs: vec![output.clone()], }, ); @@ -3996,6 +4303,7 @@ mod tests { source_object_hash: roa_hash, ee_serial: None, crl_uri: Some(TEST_CRL_URI.to_string()), + outputs_effective_until_unix, outputs: vec![output.clone()], }, ); @@ -4011,6 +4319,7 @@ mod tests { source_object_hash: roa_hash, ee_serial: Some(vec![0x01]), crl_uri: Some(TEST_CRL_URI.to_string()), + outputs_effective_until_unix, outputs: vec![output.clone()], }, ); @@ -4036,6 +4345,7 @@ mod tests { source_object_hash: [0xff; 32], ee_serial: Some(vec![0x01]), crl_uri: Some(TEST_CRL_URI.to_string()), + outputs_effective_until_unix, outputs: vec![output.clone()], }, ); @@ -4051,6 +4361,7 @@ mod tests { source_object_hash: roa_hash, ee_serial: Some(vec![0x01]), crl_uri: Some(TEST_CRL_URI.to_string()), + outputs_effective_until_unix, outputs: Vec::new(), }, ); @@ -4070,6 +4381,7 @@ mod tests { source_object_hash: roa_hash, ee_serial: Some(vec![0x01]), crl_uri: Some(TEST_CRL_URI.to_string()), + outputs_effective_until_unix, outputs: vec![output], }, );