use crate::current_repo_index::CurrentRepoIndexHandle; use crate::data_model::manifest::{ManifestDecodeError, ManifestObject, ManifestValidateError}; use crate::data_model::signed_object::SignedObjectVerifyError; use crate::policy::{CaFailedFetchPolicy, Policy}; use crate::report::{RfcRef, Warning}; use crate::storage::{PackFile, PackTime, RocksStore, StorageError, VcirArtifactRole}; use crate::validation::cert_path::{CertPathError, validate_signed_object_ee_cert_path_fast}; use crate::validation::publication_point::PublicationPointSnapshot; use sha2::Digest; use std::cmp::Ordering; use std::collections::HashSet; use x509_parser::prelude::FromDer; #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub enum PublicationPointSource { Fresh, VcirCurrentInstance, FailedFetchNoCache, } #[derive(Clone, Debug, PartialEq, Eq)] pub struct PublicationPointResult { pub source: PublicationPointSource, pub snapshot: PublicationPointSnapshot, pub warnings: Vec, } #[derive(Clone, Debug, PartialEq, Eq)] pub struct FreshValidatedPublicationPoint { pub manifest_rsync_uri: String, pub publication_point_rsync_uri: String, pub manifest_number_be: Vec, pub this_update: PackTime, pub next_update: PackTime, pub verified_at: PackTime, pub manifest_bytes: Vec, pub files: Vec, } pub trait PublicationPointData { fn manifest_rsync_uri(&self) -> &str; fn publication_point_rsync_uri(&self) -> &str; fn manifest_number_be(&self) -> &[u8]; fn this_update(&self) -> &PackTime; fn next_update(&self) -> &PackTime; fn verified_at(&self) -> &PackTime; fn manifest_bytes(&self) -> &[u8]; fn files(&self) -> &[PackFile]; } impl FreshValidatedPublicationPoint { pub fn to_publication_point_snapshot(&self) -> PublicationPointSnapshot { PublicationPointSnapshot { format_version: PublicationPointSnapshot::FORMAT_VERSION_V1, manifest_rsync_uri: self.manifest_rsync_uri.clone(), publication_point_rsync_uri: self.publication_point_rsync_uri.clone(), manifest_number_be: self.manifest_number_be.clone(), this_update: self.this_update.clone(), next_update: self.next_update.clone(), verified_at: self.verified_at.clone(), manifest_bytes: self.manifest_bytes.clone(), files: self.files.clone(), } } } impl PublicationPointData for FreshValidatedPublicationPoint { fn manifest_rsync_uri(&self) -> &str { &self.manifest_rsync_uri } fn publication_point_rsync_uri(&self) -> &str { &self.publication_point_rsync_uri } fn manifest_number_be(&self) -> &[u8] { self.manifest_number_be.as_slice() } fn this_update(&self) -> &PackTime { &self.this_update } fn next_update(&self) -> &PackTime { &self.next_update } fn verified_at(&self) -> &PackTime { &self.verified_at } fn manifest_bytes(&self) -> &[u8] { self.manifest_bytes.as_slice() } fn files(&self) -> &[PackFile] { self.files.as_slice() } } impl PublicationPointData for PublicationPointSnapshot { fn manifest_rsync_uri(&self) -> &str { &self.manifest_rsync_uri } fn publication_point_rsync_uri(&self) -> &str { &self.publication_point_rsync_uri } fn manifest_number_be(&self) -> &[u8] { self.manifest_number_be.as_slice() } fn this_update(&self) -> &PackTime { &self.this_update } fn next_update(&self) -> &PackTime { &self.next_update } fn verified_at(&self) -> &PackTime { &self.verified_at } fn manifest_bytes(&self) -> &[u8] { self.manifest_bytes.as_slice() } fn files(&self) -> &[PackFile] { self.files.as_slice() } } #[derive(Debug, thiserror::Error)] pub enum ManifestFreshError { #[error("repo sync failed: {detail} (RFC 8182 §3.4.5; RFC 9286 §6.6)")] RepoSyncFailed { detail: String }, #[error( "manifest not found in current repository view: {manifest_rsync_uri} (RFC 9286 §6.2; RFC 9286 §6.6)" )] MissingManifest { manifest_rsync_uri: String }, #[error("manifest decode failed: {0} (RFC 9286 §4; RFC 9286 §6.2; RFC 9286 §6.6)")] Decode(#[from] ManifestDecodeError), #[error( "manifest embedded EE certificate resources invalid: {0} (RFC 9286 §5.1; RFC 9286 §6.2; RFC 9286 §6.6)" )] EeResources(#[from] ManifestValidateError), #[error( "manifest CMS signature verification failed: {0} (RFC 6488 §3; RFC 9589 §4; RFC 9286 §6.2; RFC 9286 §6.6)" )] Signature(#[from] SignedObjectVerifyError), #[error( "manifest embedded EE certificate path validation failed: {0} (RFC 6488 §3; RFC 9286 §6.2; RFC 9286 §6.6)" )] EeCertPath(#[from] CertPathError), #[error( "manifest embedded EE certificate CRLDistributionPoints missing (cannot validate EE certificate) (RFC 6487 §4.8.6; RFC 6488 §3; RFC 9286 §6.2; RFC 9286 §6.6)" )] EeCrlDpMissing, #[error( "publication point contains no CRL files (cannot validate manifest EE certificate) (RFC 9286 §7; RFC 6487 §4.8.6; RFC 6488 §3; RFC 9286 §6.2; RFC 9286 §6.6)" )] NoCrlFiles, #[error( "CRL referenced by manifest embedded EE certificate CRLDistributionPoints not found at publication point: {0} (RFC 6487 §4.8.6; RFC 9286 §4.2.1; RFC 9286 §6.2; RFC 9286 §6.6)" )] EeCrlNotFound(String), #[error( "manifest is not valid at validation_time: this_update={this_update_rfc3339_utc} next_update={next_update_rfc3339_utc} validation_time={validation_time_rfc3339_utc} (RFC 9286 §6.3; RFC 9286 §6.6)" )] StaleOrEarly { this_update_rfc3339_utc: String, next_update_rfc3339_utc: String, validation_time_rfc3339_utc: String, }, #[error( "manifest must reside at the same publication point as id-ad-caRepository: manifest={manifest_rsync_uri} publication_point={publication_point_rsync_uri} (RFC 9286 §6.1; RFC 9286 §6.6)" )] ManifestOutsidePublicationPoint { manifest_rsync_uri: String, publication_point_rsync_uri: String, }, #[error( "manifestNumber not higher than previously validated manifest: old={old_hex} new={new_hex} (RFC 9286 §4.2.1; RFC 9286 §6.6)" )] ManifestNumberNotIncreasing { old_hex: String, new_hex: String }, #[error( "thisUpdate not more recent than previously validated manifest: old={old_rfc3339_utc} new={new_rfc3339_utc} (RFC 9286 §4.2.1; RFC 9286 §6.6)" )] ThisUpdateNotIncreasing { old_rfc3339_utc: String, new_rfc3339_utc: String, }, #[error( "manifest referenced file missing in current repository view: {rsync_uri} (RFC 9286 §6.4; RFC 9286 §6.6)" )] MissingFile { rsync_uri: String }, #[error("manifest file hash mismatch: {rsync_uri} (RFC 9286 §6.5; RFC 9286 §6.6)")] HashMismatch { rsync_uri: String }, } #[derive(Debug, thiserror::Error)] pub enum ManifestReuseError { #[error("latest current-instance VCIR missing: {0} (RFC 9286 §6.6)")] MissingCurrentInstanceVcir(String), #[error( "latest current-instance VCIR is not marked failed-fetch eligible: {0} (RFC 9286 §6.6)" )] IneligibleCurrentInstanceVcir(String), #[error( "latest current-instance VCIR instance_gate expired: manifest={manifest_rsync_uri} effective_until={effective_until_rfc3339_utc} validation_time={validation_time_rfc3339_utc} (RFC 9286 §6.6)" )] CurrentInstanceVcirExpired { manifest_rsync_uri: String, effective_until_rfc3339_utc: String, validation_time_rfc3339_utc: String, }, #[error( "current-instance VCIR current_manifest_rsync_uri does not match requested manifest URI: expected={expected} actual={actual}" )] ManifestUriMismatch { expected: String, actual: String }, #[error("manifest raw bytes missing for current-instance VCIR reconstruction: {0}")] MissingManifestRaw(String), #[error("artifact raw bytes missing for current-instance VCIR reconstruction: {rsync_uri}")] MissingArtifactRaw { rsync_uri: String }, #[error("invalid current-instance VCIR: {0}")] InvalidCurrentInstanceVcir(String), #[error("storage error during current-instance VCIR reuse: {0}")] Storage(#[from] StorageError), } #[derive(Debug, thiserror::Error)] pub enum ManifestProcessError { #[error("manifest processing failed and cache use is disabled: {0}")] StopAllOutput(#[from] ManifestFreshError), #[error( "manifest processing failed and no reusable current-instance validated result is available: fresh={fresh}; reused={reused}" )] NoUsableCache { fresh: ManifestFreshError, reused: ManifestReuseError, }, #[error("storage error during manifest processing: {0}")] Storage(#[from] StorageError), } pub fn process_manifest_publication_point( store: &RocksStore, policy: &Policy, manifest_rsync_uri: &str, publication_point_rsync_uri: &str, issuer_ca_der: &[u8], issuer_ca_rsync_uri: Option<&str>, validation_time: time::OffsetDateTime, ) -> Result { process_manifest_publication_point_after_repo_sync( store, policy, manifest_rsync_uri, publication_point_rsync_uri, issuer_ca_der, issuer_ca_rsync_uri, validation_time, true, None, ) } pub fn process_manifest_publication_point_fresh_after_repo_sync( store: &RocksStore, manifest_rsync_uri: &str, publication_point_rsync_uri: &str, issuer_ca_der: &[u8], issuer_ca_rsync_uri: Option<&str>, validation_time: time::OffsetDateTime, repo_sync_ok: bool, repo_sync_error: Option<&str>, ) -> Result { process_manifest_publication_point_fresh_after_repo_sync_with_timing( store, manifest_rsync_uri, publication_point_rsync_uri, None, issuer_ca_der, issuer_ca_rsync_uri, validation_time, repo_sync_ok, repo_sync_error, ) .map(|(fresh, _timing)| fresh) } #[derive(Clone, Debug, Default)] pub struct FreshPublicationPointTimingBreakdown { pub manifest_load_ms: u64, pub manifest_decode_ms: u64, pub replay_guard_ms: u64, pub manifest_entries_ms: u64, pub pack_files_ms: u64, pub ee_path_validate_ms: u64, } pub fn process_manifest_publication_point_fresh_after_repo_sync_with_timing( store: &RocksStore, manifest_rsync_uri: &str, publication_point_rsync_uri: &str, current_repo_index: Option<&CurrentRepoIndexHandle>, issuer_ca_der: &[u8], issuer_ca_rsync_uri: Option<&str>, validation_time: time::OffsetDateTime, repo_sync_ok: bool, repo_sync_error: Option<&str>, ) -> Result< ( FreshValidatedPublicationPoint, FreshPublicationPointTimingBreakdown, ), ManifestFreshError, > { if repo_sync_ok { try_build_fresh_publication_point_with_timing( store, manifest_rsync_uri, publication_point_rsync_uri, current_repo_index, issuer_ca_der, issuer_ca_rsync_uri, validation_time, ) } else { Err(ManifestFreshError::RepoSyncFailed { detail: repo_sync_error.unwrap_or("repo sync failed").to_string(), }) } } pub fn process_manifest_publication_point_after_repo_sync( store: &RocksStore, policy: &Policy, manifest_rsync_uri: &str, publication_point_rsync_uri: &str, issuer_ca_der: &[u8], issuer_ca_rsync_uri: Option<&str>, validation_time: time::OffsetDateTime, repo_sync_ok: bool, repo_sync_error: Option<&str>, ) -> Result { let fresh = if repo_sync_ok { try_build_fresh_publication_point( store, manifest_rsync_uri, publication_point_rsync_uri, issuer_ca_der, issuer_ca_rsync_uri, validation_time, ) } else { Err(ManifestFreshError::RepoSyncFailed { detail: repo_sync_error.unwrap_or("repo sync failed").to_string(), }) }; match fresh { Ok(fresh_point) => { let snapshot = fresh_point.to_publication_point_snapshot(); Ok(PublicationPointResult { source: PublicationPointSource::Fresh, snapshot, warnings: Vec::new(), }) } Err(fresh_err) => match policy.ca_failed_fetch_policy { CaFailedFetchPolicy::StopAllOutput => { Err(ManifestProcessError::StopAllOutput(fresh_err)) } CaFailedFetchPolicy::ReuseCurrentInstanceVcir => { let mut warnings = vec![ Warning::new(format!("manifest failed fetch: {fresh_err}")) .with_rfc_refs(&[RfcRef("RFC 9286 §6.6")]) .with_context(manifest_rsync_uri), ]; match load_current_instance_vcir_publication_point( store, manifest_rsync_uri, publication_point_rsync_uri, validation_time, ) { Ok(snapshot) => { warnings.push( Warning::new("using latest validated result for current CA instance") .with_rfc_refs(&[RfcRef("RFC 9286 §6.6")]) .with_context(manifest_rsync_uri), ); Ok(PublicationPointResult { source: PublicationPointSource::VcirCurrentInstance, snapshot, warnings, }) } Err(reused) => Err(ManifestProcessError::NoUsableCache { fresh: fresh_err, reused, }), } } }, } } pub fn load_current_instance_vcir_publication_point( store: &RocksStore, manifest_rsync_uri: &str, publication_point_rsync_uri: &str, validation_time: time::OffsetDateTime, ) -> Result { let vcir = store.get_vcir(manifest_rsync_uri)?.ok_or_else(|| { ManifestReuseError::MissingCurrentInstanceVcir(manifest_rsync_uri.to_string()) })?; if vcir.current_manifest_rsync_uri != manifest_rsync_uri { return Err(ManifestReuseError::ManifestUriMismatch { expected: manifest_rsync_uri.to_string(), actual: vcir.current_manifest_rsync_uri.clone(), }); } if !vcir.audit_summary.failed_fetch_eligible { return Err(ManifestReuseError::IneligibleCurrentInstanceVcir( manifest_rsync_uri.to_string(), )); } let instance_effective_until = vcir .instance_gate .instance_effective_until .parse() .map_err(|e| { ManifestReuseError::InvalidCurrentInstanceVcir(format!( "instance_gate.instance_effective_until parse failed: {e}" )) })?; if validation_time > instance_effective_until { use time::format_description::well_known::Rfc3339; return Err(ManifestReuseError::CurrentInstanceVcirExpired { manifest_rsync_uri: manifest_rsync_uri.to_string(), effective_until_rfc3339_utc: instance_effective_until .to_offset(time::UtcOffset::UTC) .format(&Rfc3339) .expect("format VCIR instance_effective_until"), validation_time_rfc3339_utc: validation_time .to_offset(time::UtcOffset::UTC) .format(&Rfc3339) .expect("format validation_time"), }); } let manifest_artifact = vcir .related_artifacts .iter() .find(|artifact| { artifact.artifact_role == VcirArtifactRole::Manifest && artifact.uri.as_deref() == Some(manifest_rsync_uri) }) .ok_or_else(|| { ManifestReuseError::InvalidCurrentInstanceVcir( "missing manifest artifact matching manifest_rsync_uri".to_string(), ) })?; let manifest_bytes = store .get_blob_bytes(&manifest_artifact.sha256)? .ok_or_else(|| ManifestReuseError::MissingManifestRaw(manifest_artifact.sha256.clone()))?; let mut seen = HashSet::new(); let mut files = Vec::new(); for artifact in &vcir.related_artifacts { let Some(uri) = artifact.uri.as_ref() else { continue; }; if artifact.artifact_role == VcirArtifactRole::Manifest || artifact.artifact_role == VcirArtifactRole::IssuerCert || artifact.artifact_role == VcirArtifactRole::Tal || artifact.artifact_role == VcirArtifactRole::TrustAnchorCert { continue; } if !seen.insert(uri.clone()) { continue; } let entry_bytes = store.get_blob_bytes(&artifact.sha256)?.ok_or_else(|| { ManifestReuseError::MissingArtifactRaw { rsync_uri: uri.clone(), } })?; files.push(PackFile::from_bytes_compute_sha256(uri, entry_bytes)); } Ok(PublicationPointSnapshot { format_version: PublicationPointSnapshot::FORMAT_VERSION_V1, manifest_rsync_uri: manifest_rsync_uri.to_string(), publication_point_rsync_uri: publication_point_rsync_uri.to_string(), manifest_number_be: vcir .validated_manifest_meta .validated_manifest_number .clone(), this_update: vcir .validated_manifest_meta .validated_manifest_this_update .clone(), next_update: vcir .validated_manifest_meta .validated_manifest_next_update .clone(), verified_at: vcir.last_successful_validation_time.clone(), manifest_bytes, files, }) } fn decode_and_validate_manifest_with_current_time( manifest_bytes: &[u8], validation_time: time::OffsetDateTime, ) -> Result { let manifest = ManifestObject::decode_der(manifest_bytes)?; manifest.validate_embedded_ee_cert()?; manifest.signed_object.verify()?; let this_update = manifest .manifest .this_update .to_offset(time::UtcOffset::UTC); let next_update = manifest .manifest .next_update .to_offset(time::UtcOffset::UTC); let now = validation_time.to_offset(time::UtcOffset::UTC); if now < this_update || now > next_update { return Err(ManifestFreshError::StaleOrEarly { this_update_rfc3339_utc: this_update .format(&time::format_description::well_known::Rfc3339) .expect("format thisUpdate"), next_update_rfc3339_utc: next_update .format(&time::format_description::well_known::Rfc3339) .expect("format nextUpdate"), validation_time_rfc3339_utc: now .format(&time::format_description::well_known::Rfc3339) .expect("format validation_time"), }); } Ok(manifest) } pub(crate) fn try_build_fresh_publication_point( store: &RocksStore, manifest_rsync_uri: &str, publication_point_rsync_uri: &str, issuer_ca_der: &[u8], issuer_ca_rsync_uri: Option<&str>, validation_time: time::OffsetDateTime, ) -> Result { try_build_fresh_publication_point_with_timing( store, manifest_rsync_uri, publication_point_rsync_uri, None, issuer_ca_der, issuer_ca_rsync_uri, validation_time, ) .map(|(fresh, _timing)| fresh) } pub(crate) fn try_build_fresh_publication_point_with_timing( store: &RocksStore, manifest_rsync_uri: &str, publication_point_rsync_uri: &str, current_repo_index: Option<&CurrentRepoIndexHandle>, issuer_ca_der: &[u8], issuer_ca_rsync_uri: Option<&str>, validation_time: time::OffsetDateTime, ) -> Result< ( FreshValidatedPublicationPoint, FreshPublicationPointTimingBreakdown, ), ManifestFreshError, > { let mut timing = FreshPublicationPointTimingBreakdown::default(); let current_index_guard = current_repo_index.and_then(|handle| handle.lock().ok()); if !rsync_uri_is_under_publication_point(manifest_rsync_uri, publication_point_rsync_uri) { return Err(ManifestFreshError::ManifestOutsidePublicationPoint { manifest_rsync_uri: manifest_rsync_uri.to_string(), publication_point_rsync_uri: publication_point_rsync_uri.to_string(), }); } let manifest_load_started = std::time::Instant::now(); let manifest_bytes = if let Some(index) = current_index_guard.as_ref() { let current = index.get_by_uri(manifest_rsync_uri).ok_or_else(|| { ManifestFreshError::MissingManifest { manifest_rsync_uri: manifest_rsync_uri.to_string(), } })?; store .get_blob_bytes(¤t.current_hash_hex) .map_err(|e| ManifestFreshError::MissingManifest { manifest_rsync_uri: format!("{manifest_rsync_uri} ({e})"), })? .ok_or_else(|| ManifestFreshError::MissingManifest { manifest_rsync_uri: manifest_rsync_uri.to_string(), })? } else { store .load_current_object_bytes_by_uri(manifest_rsync_uri) .map_err(|e| ManifestFreshError::MissingManifest { manifest_rsync_uri: format!("{manifest_rsync_uri} ({e})"), })? .ok_or_else(|| ManifestFreshError::MissingManifest { manifest_rsync_uri: manifest_rsync_uri.to_string(), })? }; timing.manifest_load_ms = manifest_load_started.elapsed().as_millis() as u64; let manifest_decode_started = std::time::Instant::now(); let manifest = decode_and_validate_manifest_with_current_time(&manifest_bytes, validation_time)?; timing.manifest_decode_ms = manifest_decode_started.elapsed().as_millis() as u64; let this_update = manifest .manifest .this_update .to_offset(time::UtcOffset::UTC); let next_update = manifest .manifest .next_update .to_offset(time::UtcOffset::UTC); let now = validation_time.to_offset(time::UtcOffset::UTC); // RFC 9286 §4.2.1: replay/rollback detection for manifestNumber and thisUpdate. // // Important nuance for revalidation across runs: // - If the manifestNumber is equal to the previously validated manifestNumber *and* the // manifest bytes are identical, then this is the same manifest being revalidated and MUST // be accepted (otherwise, RPs would incorrectly treat stable repositories as "failed fetch" // and fall back to the current-instance VCIR snapshot). // - If manifestNumber is equal but the manifest bytes differ, treat this as invalid (a // repository is not allowed to change the manifest while keeping the manifestNumber). // - If manifestNumber is lower, treat as rollback and reject. // - If manifestNumber is higher, require thisUpdate to be more recent than the previously // validated thisUpdate. let replay_guard_started = std::time::Instant::now(); if let Some(old_vcir) = store.get_vcir(manifest_rsync_uri).ok().flatten() { if old_vcir.manifest_rsync_uri == manifest_rsync_uri { let new_num = manifest.manifest.manifest_number.bytes_be.as_slice(); let old_num = old_vcir .validated_manifest_meta .validated_manifest_number .as_slice(); match cmp_minimal_be_unsigned(new_num, old_num) { Ordering::Greater => { let old_this_update = old_vcir .validated_manifest_meta .validated_manifest_this_update .parse() .expect("vcir internal validation ensures thisUpdate parses"); if this_update <= old_this_update { use time::format_description::well_known::Rfc3339; return Err(ManifestFreshError::ThisUpdateNotIncreasing { old_rfc3339_utc: old_this_update .to_offset(time::UtcOffset::UTC) .format(&Rfc3339) .expect("format old thisUpdate"), new_rfc3339_utc: this_update .format(&Rfc3339) .expect("format new thisUpdate"), }); } } Ordering::Equal => { let old_manifest_hash = old_vcir.related_artifacts.iter().find_map(|artifact| { (artifact.artifact_role == VcirArtifactRole::Manifest && artifact.uri.as_deref() == Some(manifest_rsync_uri)) .then_some(artifact.sha256.as_str()) }); let new_manifest_hash = hex::encode(sha2::Sha256::digest(&manifest_bytes)); if old_manifest_hash != Some(new_manifest_hash.as_str()) { return Err(ManifestFreshError::ManifestNumberNotIncreasing { old_hex: hex::encode_upper(old_num), new_hex: hex::encode_upper(new_num), }); } } Ordering::Less => { return Err(ManifestFreshError::ManifestNumberNotIncreasing { old_hex: hex::encode_upper(old_num), new_hex: hex::encode_upper(new_num), }); } } } } timing.replay_guard_ms = replay_guard_started.elapsed().as_millis() as u64; let manifest_entries_started = std::time::Instant::now(); let entries = manifest .manifest .parse_files() .map_err(ManifestDecodeError::Validate)?; timing.manifest_entries_ms = manifest_entries_started.elapsed().as_millis() as u64; let mut files = Vec::with_capacity(manifest.manifest.file_count()); let pack_files_started = std::time::Instant::now(); let external_raw_store = store .external_raw_store_ref() .cloned() .map(std::sync::Arc::new); let external_repo_bytes = store .external_repo_bytes_ref() .cloned() .map(std::sync::Arc::new); for entry in &entries { let rsync_uri = join_rsync_dir_and_file(publication_point_rsync_uri, entry.file_name.as_str()); let current_object = if let Some(index) = current_index_guard.as_ref() { let current = index .get_by_uri(&rsync_uri) .ok_or_else(|| ManifestFreshError::MissingFile { rsync_uri: rsync_uri.clone(), })?; crate::storage::CurrentObjectWithHash { current_hash_hex: current.current_hash_hex.clone(), current_hash: current.current_hash, bytes: Vec::new(), } } else { store .load_current_object_with_hash_by_uri(&rsync_uri) .map_err(|_e| ManifestFreshError::MissingFile { rsync_uri: rsync_uri.clone(), })? .ok_or_else(|| ManifestFreshError::MissingFile { rsync_uri: rsync_uri.clone(), })? }; if current_object.current_hash != entry.hash_bytes { return Err(ManifestFreshError::HashMismatch { rsync_uri }); } if let (Some(_), Some(repo_bytes)) = (current_index_guard.as_ref(), external_repo_bytes.as_ref()) { files.push(PackFile::from_lazy_repo_bytes( rsync_uri, current_object.current_hash_hex, current_object.current_hash, repo_bytes.clone(), )); } else if let (Some(_), Some(raw_store)) = (current_index_guard.as_ref(), external_raw_store.as_ref()) { files.push(PackFile::from_lazy_external_raw_store( rsync_uri, current_object.current_hash_hex, current_object.current_hash, raw_store.clone(), )); } else { let bytes = if current_object.bytes.is_empty() { store .get_blob_bytes(¤t_object.current_hash_hex) .map_err(|_e| ManifestFreshError::MissingFile { rsync_uri: rsync_uri.clone(), })? .ok_or_else(|| ManifestFreshError::MissingFile { rsync_uri: rsync_uri.clone(), })? } else { current_object.bytes }; files.push(PackFile::from_bytes_with_sha256( rsync_uri, bytes, current_object.current_hash, )); } } timing.pack_files_ms = pack_files_started.elapsed().as_millis() as u64; // RFC 6488 §3: manifest (signed object) validity includes a valid EE cert path. // We validate this after §6.4/§6.5 so the issuer CRL can be selected from the publication point. let ee_path_validate_started = std::time::Instant::now(); validate_manifest_embedded_ee_cert_path( &manifest, &files, issuer_ca_der, issuer_ca_rsync_uri, validation_time, )?; timing.ee_path_validate_ms = ee_path_validate_started.elapsed().as_millis() as u64; Ok(( FreshValidatedPublicationPoint { manifest_rsync_uri: manifest_rsync_uri.to_string(), publication_point_rsync_uri: publication_point_rsync_uri.to_string(), manifest_number_be: manifest.manifest.manifest_number.bytes_be.clone(), this_update: PackTime::from_utc_offset_datetime(this_update), next_update: PackTime::from_utc_offset_datetime(next_update), verified_at: PackTime::from_utc_offset_datetime(now), manifest_bytes, files, }, timing, )) } fn cmp_minimal_be_unsigned(a: &[u8], b: &[u8]) -> Ordering { // Compare two minimal big-endian byte strings as unsigned integers. // (Leading zeros are not expected; callers store minimal big-endian.) a.len().cmp(&b.len()).then_with(|| a.cmp(b)) } fn join_rsync_dir_and_file(base: &str, file_name: &str) -> String { if base.ends_with('/') { format!("{base}{file_name}") } else { format!("{base}/{file_name}") } } fn rsync_uri_is_under_publication_point(uri: &str, publication_point_rsync_uri: &str) -> bool { let pp = if publication_point_rsync_uri.ends_with('/') { publication_point_rsync_uri.to_string() } else { format!("{publication_point_rsync_uri}/") }; uri.starts_with(&pp) } fn validate_manifest_embedded_ee_cert_path( manifest: &ManifestObject, files: &[crate::storage::PackFile], issuer_ca_der: &[u8], issuer_ca_rsync_uri: Option<&str>, validation_time: time::OffsetDateTime, ) -> Result<(), ManifestFreshError> { let ee = &manifest.signed_object.signed_data.certificates[0]; let crl_files = files .iter() .filter(|f| f.rsync_uri.ends_with(".crl")) .collect::>(); if crl_files.is_empty() { return Err(ManifestFreshError::NoCrlFiles); } let Some(crldp_uris) = ee .resource_cert .tbs .extensions .crl_distribution_points_uris .as_ref() else { return Err(ManifestFreshError::EeCrlDpMissing); }; for u in crldp_uris { let s = u.as_str(); if let Some(f) = crl_files.iter().find(|f| f.rsync_uri == s) { let crl_bytes = f.bytes().map_err(|e| ManifestFreshError::MissingFile { rsync_uri: format!("{s} ({e})"), })?; let issuer_ca = crate::data_model::rc::ResourceCertificate::decode_der(issuer_ca_der) .map_err(CertPathError::IssuerDecode)?; let (rem, issuer_spki) = x509_parser::x509::SubjectPublicKeyInfo::from_der( &issuer_ca.tbs.subject_public_key_info, ) .map_err(|e| CertPathError::IssuerSpkiParse(e.to_string()))?; if !rem.is_empty() { return Err(CertPathError::IssuerSpkiTrailingBytes(rem.len()).into()); } let issuer_crl = crate::data_model::crl::RpkixCrl::decode_der(crl_bytes) .map_err(CertPathError::from)?; let revoked_serials = issuer_crl .revoked_certs .iter() .map(|rc| rc.serial_number.bytes_be.clone()) .collect::>(); validate_signed_object_ee_cert_path_fast( ee, &issuer_ca, &issuer_spki, &issuer_crl, &revoked_serials, issuer_ca_rsync_uri, Some(f.rsync_uri.as_str()), validation_time, )?; return Ok(()); } } Err(ManifestFreshError::EeCrlNotFound( crldp_uris .iter() .map(|u| u.as_str()) .collect::>() .join(", "), )) } #[cfg(test)] mod tests { use super::*; use crate::current_repo_index::CurrentRepoIndex; use crate::data_model::manifest::ManifestObject; use crate::storage::{ PackFile, PackTime, RawByHashEntry, RocksStore, ValidatedCaInstanceResult, ValidatedManifestMeta, VcirArtifactKind, VcirArtifactRole, VcirArtifactValidationStatus, VcirAuditSummary, VcirCcrManifestProjection, VcirInstanceGate, VcirRelatedArtifact, VcirSummary, }; use std::path::Path; fn manifest_fixture_path() -> &'static Path { Path::new( "tests/fixtures/repository/rpki.cernet.net/repo/cernet/0/05FC9C5B88506F7C0D3F862C8895BED67E9F8EBA.mft", ) } fn issuer_ca_fixture_der() -> Vec { std::fs::read( "tests/fixtures/repository/rpki.apnic.net/repository/B527EF581D6611E2BB468F7C72FD1FF2/BfycW4hQb3wNP4YsiJW-1n6fjro.cer", ) .expect("read issuer ca fixture") } fn issuer_ca_rsync_uri() -> &'static str { "rsync://rpki.apnic.net/repository/B527EF581D6611E2BB468F7C72FD1FF2/BfycW4hQb3wNP4YsiJW-1n6fjro.cer" } fn fixture_to_rsync_uri(path: &Path) -> String { let rel = path .strip_prefix("tests/fixtures/repository") .expect("path under fixture repository"); let mut it = rel.components(); let host = it .next() .expect("host component") .as_os_str() .to_string_lossy(); let rest = it.as_path().to_string_lossy(); format!("rsync://{host}/{rest}") } fn fixture_dir_to_rsync_uri(dir: &Path) -> String { let mut s = fixture_to_rsync_uri(dir); if !s.ends_with('/') { s.push('/'); } s } fn load_manifest_fixture() -> ( ManifestObject, Vec, String, String, time::OffsetDateTime, ) { let manifest_path = manifest_fixture_path(); let manifest_bytes = std::fs::read(manifest_path).expect("read manifest fixture"); let manifest = ManifestObject::decode_der(&manifest_bytes).expect("decode manifest"); let manifest_rsync_uri = fixture_to_rsync_uri(manifest_path); let publication_point_rsync_uri = fixture_dir_to_rsync_uri(manifest_path.parent().unwrap()); let validation_time = manifest.manifest.this_update + time::Duration::seconds(1); ( manifest, manifest_bytes, manifest_rsync_uri, publication_point_rsync_uri, validation_time, ) } fn raw_by_hash_entry(uri: &str, bytes: Vec, object_type: &str) -> RawByHashEntry { let mut entry = RawByHashEntry::from_bytes(hex::encode(sha2::Sha256::digest(&bytes)), bytes); entry.origin_uris.push(uri.to_string()); entry.object_type = Some(object_type.to_string()); entry.encoding = Some("der".to_string()); entry } fn put_current_object(store: &RocksStore, rsync_uri: &str, bytes: Vec, object_type: &str) { let hash = hex::encode(sha2::Sha256::digest(&bytes)); store .put_raw_by_hash_entry(&raw_by_hash_entry(rsync_uri, bytes, object_type)) .expect("put raw_by_hash entry"); store .put_repository_view_entry(&crate::storage::RepositoryViewEntry { rsync_uri: rsync_uri.to_string(), current_hash: Some(hash), repository_source: Some("https://example.test/notification.xml".to_string()), object_type: Some(object_type.to_string()), state: crate::storage::RepositoryViewState::Present, }) .expect("put repository view entry"); } fn put_raw_only(store: &RocksStore, rsync_uri: &str, bytes: Vec, object_type: &str) { store .put_raw_by_hash_entry(&raw_by_hash_entry(rsync_uri, bytes, object_type)) .expect("put raw_by_hash entry"); } fn sample_current_instance_vcir( manifest_rsync_uri: &str, publication_point_rsync_uri: &str, manifest_sha256: &str, locked_object_uri: &str, locked_object_sha256: &str, validation_time: time::OffsetDateTime, failed_fetch_eligible: bool, ) -> ValidatedCaInstanceResult { let gate_time = PackTime::from_utc_offset_datetime(validation_time + time::Duration::hours(1)); let ccr_manifest_projection = VcirCcrManifestProjection { manifest_rsync_uri: manifest_rsync_uri.to_string(), manifest_sha256: hex::decode(manifest_sha256).expect("decode manifest sha256"), manifest_size: 2048, manifest_ee_aki: vec![0x11; 20], manifest_number_be: vec![1], manifest_this_update: PackTime::from_utc_offset_datetime(validation_time), manifest_sia_locations_der: vec![vec![ 0x30, 0x11, 0x06, 0x08, 0x2b, 0x06, 0x01, 0x05, 0x05, 0x07, 0x30, 0x05, 0x86, 0x05, b'r', b's', b'y', b'n', b'c', ]], subordinate_skis: Vec::new(), }; ValidatedCaInstanceResult { manifest_rsync_uri: manifest_rsync_uri.to_string(), parent_manifest_rsync_uri: None, tal_id: "test-tal".to_string(), ca_subject_name: "CN=test".to_string(), ca_ski: "00112233445566778899aabbccddeeff00112233".to_string(), issuer_ski: "00112233445566778899aabbccddeeff00112233".to_string(), last_successful_validation_time: PackTime::from_utc_offset_datetime(validation_time), current_manifest_rsync_uri: manifest_rsync_uri.to_string(), current_crl_rsync_uri: format!("{publication_point_rsync_uri}current.crl"), validated_manifest_meta: ValidatedManifestMeta { validated_manifest_number: vec![1], validated_manifest_this_update: PackTime::from_utc_offset_datetime(validation_time), validated_manifest_next_update: gate_time.clone(), }, ccr_manifest_projection, instance_gate: VcirInstanceGate { manifest_next_update: gate_time.clone(), current_crl_next_update: gate_time.clone(), self_ca_not_after: gate_time.clone(), instance_effective_until: gate_time, }, child_entries: Vec::new(), local_outputs: Vec::new(), related_artifacts: vec![ VcirRelatedArtifact { artifact_role: VcirArtifactRole::Manifest, artifact_kind: VcirArtifactKind::Mft, uri: Some(manifest_rsync_uri.to_string()), sha256: manifest_sha256.to_string(), object_type: Some("mft".to_string()), validation_status: VcirArtifactValidationStatus::Accepted, }, VcirRelatedArtifact { artifact_role: VcirArtifactRole::SignedObject, artifact_kind: VcirArtifactKind::Roa, uri: Some(locked_object_uri.to_string()), sha256: locked_object_sha256.to_string(), object_type: Some("roa".to_string()), validation_status: VcirArtifactValidationStatus::Accepted, }, ], summary: VcirSummary { local_vrp_count: 0, local_aspa_count: 0, local_router_key_count: 0, child_count: 0, accepted_object_count: 2, rejected_object_count: 0, }, audit_summary: VcirAuditSummary { failed_fetch_eligible, last_failed_fetch_reason: None, warning_count: 0, audit_flags: Vec::new(), }, } } fn locked_files_for_manifest( manifest: &ManifestObject, publication_point_rsync_uri: &str, ) -> Vec { let manifest_path = manifest_fixture_path(); manifest .manifest .parse_files() .expect("parse files") .into_iter() .map(|entry| { let file_path = manifest_path .parent() .unwrap() .join(entry.file_name.as_str()); let bytes = std::fs::read(&file_path).unwrap_or_else(|_| { panic!("read fixture file referenced by manifest: {file_path:?}") }); PackFile::from_bytes_compute_sha256( format!("{publication_point_rsync_uri}{}", entry.file_name), bytes, ) }) .collect() } #[test] fn try_build_fresh_publication_point_rejects_manifest_outside_publication_point() { let temp = tempfile::tempdir().expect("tempdir"); let store = RocksStore::open(temp.path()).expect("open rocksdb"); let (_, _, manifest_rsync_uri, _, validation_time) = load_manifest_fixture(); let err = try_build_fresh_publication_point( &store, &manifest_rsync_uri, "rsync://example.test/other/", &issuer_ca_fixture_der(), Some(issuer_ca_rsync_uri()), validation_time, ) .unwrap_err(); assert!( matches!( err, ManifestFreshError::ManifestOutsidePublicationPoint { .. } ), "{err}" ); } #[test] fn try_build_fresh_publication_point_reports_missing_manifest_when_raw_store_is_empty() { let temp = tempfile::tempdir().expect("tempdir"); let store = RocksStore::open(temp.path()).expect("open rocksdb"); let (_, _, manifest_rsync_uri, publication_point_rsync_uri, validation_time) = load_manifest_fixture(); let err = try_build_fresh_publication_point( &store, &manifest_rsync_uri, &publication_point_rsync_uri, &issuer_ca_fixture_der(), Some(issuer_ca_rsync_uri()), validation_time, ) .unwrap_err(); assert!( matches!(err, ManifestFreshError::MissingManifest { .. }), "{err}" ); } #[test] fn try_build_fresh_publication_point_reports_missing_locked_file() { let temp = tempfile::tempdir().expect("tempdir"); let store = RocksStore::open(temp.path()).expect("open rocksdb"); let ( manifest, manifest_bytes, manifest_rsync_uri, publication_point_rsync_uri, validation_time, ) = load_manifest_fixture(); put_current_object(&store, &manifest_rsync_uri, manifest_bytes, "mft"); let first_non_crl = manifest .manifest .parse_files() .expect("parse files") .into_iter() .find(|entry| !entry.file_name.ends_with(".crl")) .expect("fixture non-crl entry"); let file_path = manifest_fixture_path() .parent() .unwrap() .join(first_non_crl.file_name.as_str()); let bytes = std::fs::read(&file_path).expect("read fixture file"); let rsync_uri = format!("{publication_point_rsync_uri}{}", first_non_crl.file_name); let object_type = rsync_uri.rsplit('.').next().unwrap_or("bin"); put_current_object(&store, &rsync_uri, bytes, object_type); let err = try_build_fresh_publication_point( &store, &manifest_rsync_uri, &publication_point_rsync_uri, &issuer_ca_fixture_der(), Some(issuer_ca_rsync_uri()), validation_time, ) .unwrap_err(); assert!( matches!(err, ManifestFreshError::MissingFile { .. }), "{err}" ); } #[test] fn try_build_fresh_publication_point_detects_hash_mismatch_via_repository_view_hash() { let temp = tempfile::tempdir().expect("tempdir"); let store = RocksStore::open(temp.path()).expect("open rocksdb"); let ( manifest, manifest_bytes, manifest_rsync_uri, publication_point_rsync_uri, validation_time, ) = load_manifest_fixture(); put_current_object(&store, &manifest_rsync_uri, manifest_bytes, "mft"); let non_crl_entries = manifest .manifest .parse_files() .expect("parse files") .into_iter() .filter(|entry| !entry.file_name.ends_with(".crl")) .collect::>(); let first = &non_crl_entries[0]; let second = &non_crl_entries[1]; let first_uri = format!("{publication_point_rsync_uri}{}", first.file_name); let second_path = manifest_fixture_path() .parent() .unwrap() .join(second.file_name.as_str()); let wrong_bytes = std::fs::read(&second_path).expect("read wrong fixture file"); let object_type = first_uri.rsplit('.').next().unwrap_or("bin"); put_current_object(&store, &first_uri, wrong_bytes, object_type); for entry in non_crl_entries.iter().skip(1) { let file_path = manifest_fixture_path() .parent() .unwrap() .join(entry.file_name.as_str()); let bytes = std::fs::read(&file_path).expect("read fixture file"); let rsync_uri = format!("{publication_point_rsync_uri}{}", entry.file_name); let object_type = rsync_uri.rsplit('.').next().unwrap_or("bin"); put_current_object(&store, &rsync_uri, bytes, object_type); } let err = try_build_fresh_publication_point( &store, &manifest_rsync_uri, &publication_point_rsync_uri, &issuer_ca_fixture_der(), Some(issuer_ca_rsync_uri()), validation_time, ) .unwrap_err(); assert!( matches!(err, ManifestFreshError::HashMismatch { .. }), "{err}" ); } #[test] fn try_build_fresh_publication_point_uses_current_repo_index_without_repository_view() { let temp = tempfile::tempdir().expect("tempdir"); let store = RocksStore::open(temp.path()).expect("open rocksdb"); let ( manifest, manifest_bytes, manifest_rsync_uri, publication_point_rsync_uri, validation_time, ) = load_manifest_fixture(); put_raw_only(&store, &manifest_rsync_uri, manifest_bytes.clone(), "mft"); let current_index = CurrentRepoIndex::shared(); let mut entries = vec![crate::storage::RepositoryViewEntry { rsync_uri: manifest_rsync_uri.clone(), current_hash: Some(hex::encode(sha2::Sha256::digest(&manifest_bytes))), repository_source: Some("https://example.test/notification.xml".to_string()), object_type: Some("mft".to_string()), state: crate::storage::RepositoryViewState::Present, }]; for entry in manifest.manifest.parse_files().expect("parse files") { let file_path = manifest_fixture_path() .parent() .unwrap() .join(entry.file_name.as_str()); let bytes = std::fs::read(&file_path).expect("read fixture file"); let rsync_uri = format!("{publication_point_rsync_uri}{}", entry.file_name); let object_type = rsync_uri.rsplit('.').next().unwrap_or("bin").to_string(); put_raw_only(&store, &rsync_uri, bytes.clone(), &object_type); entries.push(crate::storage::RepositoryViewEntry { rsync_uri, current_hash: Some(hex::encode(sha2::Sha256::digest(&bytes))), repository_source: Some("https://example.test/notification.xml".to_string()), object_type: Some(object_type), state: crate::storage::RepositoryViewState::Present, }); } current_index .lock() .expect("index lock") .apply_repository_view_entries(&entries) .expect("apply current index"); assert!( store .get_repository_view_entry(&manifest_rsync_uri) .expect("get repository view") .is_none() ); let (fresh, _timing) = try_build_fresh_publication_point_with_timing( &store, &manifest_rsync_uri, &publication_point_rsync_uri, Some(¤t_index), &issuer_ca_fixture_der(), Some(issuer_ca_rsync_uri()), validation_time, ) .expect("fresh publication point via current index"); assert_eq!(fresh.manifest_rsync_uri, manifest_rsync_uri); assert_eq!(fresh.files.len(), manifest.manifest.file_count()); } #[test] fn validate_manifest_embedded_ee_cert_path_rejects_missing_crl_files() { let (manifest, _, _, publication_point_rsync_uri, validation_time) = load_manifest_fixture(); let files = locked_files_for_manifest(&manifest, &publication_point_rsync_uri) .into_iter() .filter(|f| !f.rsync_uri.ends_with(".crl")) .collect::>(); let err = validate_manifest_embedded_ee_cert_path( &manifest, &files, &issuer_ca_fixture_der(), Some(issuer_ca_rsync_uri()), validation_time, ) .unwrap_err(); assert!(matches!(err, ManifestFreshError::NoCrlFiles), "{err}"); } #[test] fn validate_manifest_embedded_ee_cert_path_rejects_missing_ee_crldp() { let (mut manifest, _, _, publication_point_rsync_uri, validation_time) = load_manifest_fixture(); manifest.signed_object.signed_data.certificates[0] .resource_cert .tbs .extensions .crl_distribution_points_uris = None; let files = locked_files_for_manifest(&manifest, &publication_point_rsync_uri); let err = validate_manifest_embedded_ee_cert_path( &manifest, &files, &issuer_ca_fixture_der(), Some(issuer_ca_rsync_uri()), validation_time, ) .unwrap_err(); assert!(matches!(err, ManifestFreshError::EeCrlDpMissing), "{err}"); } #[test] fn validate_manifest_embedded_ee_cert_path_rejects_unlisted_crldp_uri() { let (manifest, _, _, publication_point_rsync_uri, validation_time) = load_manifest_fixture(); let mut files = locked_files_for_manifest(&manifest, &publication_point_rsync_uri) .into_iter() .filter(|f| !f.rsync_uri.ends_with(".crl")) .collect::>(); files.push(PackFile::from_bytes_compute_sha256( "rsync://example.test/repo/unrelated.crl", b"dummy".to_vec(), )); let err = validate_manifest_embedded_ee_cert_path( &manifest, &files, &issuer_ca_fixture_der(), Some(issuer_ca_rsync_uri()), validation_time, ) .unwrap_err(); assert!(matches!(err, ManifestFreshError::EeCrlNotFound(_)), "{err}"); } #[test] fn load_current_instance_vcir_publication_point_returns_manifest_and_locked_files() { let temp = tempfile::tempdir().expect("tempdir"); let store = RocksStore::open(temp.path()).expect("open rocksdb"); let validation_time = time::OffsetDateTime::UNIX_EPOCH + time::Duration::hours(1); let manifest_uri = "rsync://example.test/repo/current.mft"; let publication_point_uri = "rsync://example.test/repo/"; let locked_uri = "rsync://example.test/repo/object.roa"; let manifest_bytes = vec![0x30, 0x31, 0x32]; let locked_bytes = vec![0x01, 0x02, 0x03, 0x04]; let manifest_entry = raw_by_hash_entry(manifest_uri, manifest_bytes.clone(), "mft"); let locked_entry = raw_by_hash_entry(locked_uri, locked_bytes.clone(), "roa"); store .put_raw_by_hash_entry(&manifest_entry) .expect("put manifest raw_by_hash"); store .put_raw_by_hash_entry(&locked_entry) .expect("put locked raw_by_hash"); let vcir = sample_current_instance_vcir( manifest_uri, publication_point_uri, &manifest_entry.sha256_hex, locked_uri, &locked_entry.sha256_hex, validation_time, true, ); store.put_vcir(&vcir).expect("put vcir"); let point = load_current_instance_vcir_publication_point( &store, manifest_uri, publication_point_uri, validation_time, ) .expect("load current-instance vcir publication point"); assert_eq!(point.manifest_bytes, manifest_bytes); assert_eq!(point.files.len(), 1); assert_eq!(point.files[0].rsync_uri, locked_uri); assert_eq!( point.files[0].bytes_cloned().expect("locked bytes"), locked_bytes ); } #[test] fn load_current_instance_vcir_publication_point_rejects_ineligible_vcir() { let temp = tempfile::tempdir().expect("tempdir"); let store = RocksStore::open(temp.path()).expect("open rocksdb"); let validation_time = time::OffsetDateTime::UNIX_EPOCH + time::Duration::hours(1); let manifest_uri = "rsync://example.test/repo/current.mft"; let publication_point_uri = "rsync://example.test/repo/"; let locked_uri = "rsync://example.test/repo/object.roa"; let manifest_entry = raw_by_hash_entry(manifest_uri, vec![0x30], "mft"); let locked_entry = raw_by_hash_entry(locked_uri, vec![0x01], "roa"); store .put_raw_by_hash_entry(&manifest_entry) .expect("put manifest raw_by_hash"); store .put_raw_by_hash_entry(&locked_entry) .expect("put locked raw_by_hash"); let vcir = sample_current_instance_vcir( manifest_uri, publication_point_uri, &manifest_entry.sha256_hex, locked_uri, &locked_entry.sha256_hex, validation_time, false, ); store.put_vcir(&vcir).expect("put vcir"); let err = load_current_instance_vcir_publication_point( &store, manifest_uri, publication_point_uri, validation_time, ) .unwrap_err(); assert!( matches!(err, ManifestReuseError::IneligibleCurrentInstanceVcir(_)), "{err}" ); } #[test] fn load_current_instance_vcir_publication_point_rejects_expired_vcir() { let temp = tempfile::tempdir().expect("tempdir"); let store = RocksStore::open(temp.path()).expect("open rocksdb"); let validation_time = time::OffsetDateTime::UNIX_EPOCH + time::Duration::hours(2); let manifest_uri = "rsync://example.test/repo/current.mft"; let publication_point_uri = "rsync://example.test/repo/"; let locked_uri = "rsync://example.test/repo/object.roa"; let manifest_entry = raw_by_hash_entry(manifest_uri, vec![0x30], "mft"); let locked_entry = raw_by_hash_entry(locked_uri, vec![0x01], "roa"); store .put_raw_by_hash_entry(&manifest_entry) .expect("put manifest raw_by_hash"); store .put_raw_by_hash_entry(&locked_entry) .expect("put locked raw_by_hash"); let mut vcir = sample_current_instance_vcir( manifest_uri, publication_point_uri, &manifest_entry.sha256_hex, locked_uri, &locked_entry.sha256_hex, validation_time - time::Duration::hours(2), true, ); let expired = PackTime::from_utc_offset_datetime(validation_time - time::Duration::minutes(1)); vcir.instance_gate.manifest_next_update = expired.clone(); vcir.instance_gate.current_crl_next_update = expired.clone(); vcir.instance_gate.self_ca_not_after = expired.clone(); vcir.instance_gate.instance_effective_until = expired; vcir.validated_manifest_meta.validated_manifest_next_update = vcir.instance_gate.instance_effective_until.clone(); store.put_vcir(&vcir).expect("put expired vcir"); let err = load_current_instance_vcir_publication_point( &store, manifest_uri, publication_point_uri, validation_time, ) .unwrap_err(); assert!( matches!(err, ManifestReuseError::CurrentInstanceVcirExpired { .. }), "{err}" ); } #[test] fn load_current_instance_vcir_publication_point_rejects_missing_locked_artifact_raw() { let temp = tempfile::tempdir().expect("tempdir"); let store = RocksStore::open(temp.path()).expect("open rocksdb"); let validation_time = time::OffsetDateTime::UNIX_EPOCH + time::Duration::hours(1); let manifest_uri = "rsync://example.test/repo/current.mft"; let publication_point_uri = "rsync://example.test/repo/"; let locked_uri = "rsync://example.test/repo/object.roa"; let manifest_entry = raw_by_hash_entry(manifest_uri, vec![0x30], "mft"); store .put_raw_by_hash_entry(&manifest_entry) .expect("put manifest raw_by_hash"); let vcir = sample_current_instance_vcir( manifest_uri, publication_point_uri, &manifest_entry.sha256_hex, locked_uri, &hex::encode(sha2::Sha256::digest([0x01, 0x02])), validation_time, true, ); store.put_vcir(&vcir).expect("put vcir"); let err = load_current_instance_vcir_publication_point( &store, manifest_uri, publication_point_uri, validation_time, ) .unwrap_err(); assert!( matches!(err, ManifestReuseError::MissingArtifactRaw { .. }), "{err}" ); } }