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::{FetchCachePpKey, FetchCachePpPack, RocksStore, StorageError}; use crate::validation::cert_path::{CertPathError, validate_ee_cert_path}; use sha2::Digest; use std::cmp::Ordering; use std::collections::HashMap; #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub enum PublicationPointSource { Fresh, FetchCachePp, } #[derive(Clone, Debug, PartialEq, Eq)] pub struct PublicationPointResult { pub source: PublicationPointSource, pub pack: FetchCachePpPack, pub warnings: Vec, } #[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 raw_objects: {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 raw_objects: {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 ManifestCachedError { #[error("fetch_cache_pp entry missing: {0} (RFC 9286 §6.6)")] MissingFetchCachePp(String), #[error("fetch_cache_pp pack invalid: {0}")] InvalidPack(#[from] StorageError), #[error("cached manifest revalidation failed: {0}")] CachedManifestFresh(#[from] ManifestFreshError), #[error( "cached fetch_cache_pp missing file referenced by manifest: {rsync_uri} (RFC 9286 §6.4; RFC 9286 §6.6)" )] CachedMissingFile { rsync_uri: String }, #[error("cached fetch_cache_pp file hash mismatch: {rsync_uri} (RFC 9286 §6.5; RFC 9286 §6.6)")] CachedHashMismatch { rsync_uri: String }, } #[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 usable fetch_cache_pp is available: fresh={fresh}; cached={cached}" )] NoUsableCache { fresh: ManifestFreshError, cached: ManifestCachedError, }, #[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_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_pack( 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(pack) => { let key = FetchCachePpKey::from_manifest_rsync_uri(manifest_rsync_uri); let bytes = pack.encode()?; store.put_fetch_cache_pp(&key, &bytes)?; Ok(PublicationPointResult { source: PublicationPointSource::Fresh, pack, warnings: Vec::new(), }) } Err(fresh_err) => match policy.ca_failed_fetch_policy { CaFailedFetchPolicy::StopAllOutput => { Err(ManifestProcessError::StopAllOutput(fresh_err)) } CaFailedFetchPolicy::UseFetchCachePp => { 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_and_revalidate_cached_pack( store, manifest_rsync_uri, publication_point_rsync_uri, issuer_ca_der, issuer_ca_rsync_uri, validation_time, ) { Ok(pack) => { warnings.push( Warning::new("using fetch_cache_pp for publication point") .with_rfc_refs(&[RfcRef("RFC 9286 §6.6")]) .with_context(manifest_rsync_uri), ); Ok(PublicationPointResult { source: PublicationPointSource::FetchCachePp, pack, warnings, }) } Err(cached_err) => Err(ManifestProcessError::NoUsableCache { fresh: fresh_err, cached: cached_err, }), } } }, } } pub fn load_and_revalidate_cached_pack( 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 { let key = FetchCachePpKey::from_manifest_rsync_uri(manifest_rsync_uri); let bytes = store .get_fetch_cache_pp(&key)? .ok_or_else(|| ManifestCachedError::MissingFetchCachePp(key.as_str().to_string()))?; let pack = FetchCachePpPack::decode(&bytes)?; if pack.manifest_rsync_uri != manifest_rsync_uri { return Err(ManifestCachedError::InvalidPack(StorageError::RocksDb( "cached pack manifest_rsync_uri does not match key".to_string(), ))); } if pack.publication_point_rsync_uri != publication_point_rsync_uri { return Err(ManifestCachedError::InvalidPack(StorageError::RocksDb( "cached pack publication_point_rsync_uri does not match expected".to_string(), ))); } revalidate_cached_pack_with_current_time( &pack, issuer_ca_der, issuer_ca_rsync_uri, validation_time, )?; Ok(pack) } fn revalidate_cached_pack_with_current_time( pack: &FetchCachePpPack, issuer_ca_der: &[u8], issuer_ca_rsync_uri: Option<&str>, validation_time: time::OffsetDateTime, ) -> Result<(), ManifestCachedError> { // First, re-validate the cached manifest itself with the current time. let manifest = decode_and_validate_manifest_with_current_time(&pack.manifest_bytes, validation_time) .map_err(ManifestCachedError::from)?; // Then, re-bind the manifest fileList to the cached pack contents, as per RFC 9286 §6.4-§6.5. let by_uri: HashMap<&str, &crate::storage::PackFile> = pack .files .iter() .map(|f| (f.rsync_uri.as_str(), f)) .collect(); let entries = manifest .manifest .parse_files() .map_err(|e| ManifestFreshError::Decode(ManifestDecodeError::Validate(e)))?; for entry in &entries { let rsync_uri = join_rsync_dir_and_file(&pack.publication_point_rsync_uri, entry.file_name.as_str()); let Some(file) = by_uri.get(rsync_uri.as_str()) else { return Err(ManifestCachedError::CachedMissingFile { rsync_uri }); }; if file.sha256.as_slice() != entry.hash_bytes.as_ref() { return Err(ManifestCachedError::CachedHashMismatch { rsync_uri }); } } // Finally, validate the manifest's embedded EE certificate path against the issuer CA + CRL. // This enforces cert validity + CRL validity at `validation_time` for cached packs. validate_manifest_embedded_ee_cert_path( &manifest, &pack.files, issuer_ca_der, issuer_ca_rsync_uri, validation_time, ) .map_err(ManifestCachedError::from)?; Ok(()) } 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) } fn try_build_fresh_pack( 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 { 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_bytes = store .get_raw(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(), })?; let manifest = decode_and_validate_manifest_with_current_time(&manifest_bytes, validation_time)?; 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 fetch_cache_pp). // - 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 key = FetchCachePpKey::from_manifest_rsync_uri(manifest_rsync_uri); if let Some(old_bytes) = store.get_fetch_cache_pp(&key).ok().flatten() { if let Ok(old_pack) = FetchCachePpPack::decode(&old_bytes) { if old_pack.manifest_rsync_uri == manifest_rsync_uri && old_pack.publication_point_rsync_uri == publication_point_rsync_uri { let new_num = manifest.manifest.manifest_number.bytes_be.as_slice(); let old_num = old_pack.manifest_number_be.as_slice(); match cmp_minimal_be_unsigned(new_num, old_num) { Ordering::Greater => { let old_this_update = old_pack .this_update .parse() .expect("pack internal validation ensures this_update 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 => { if old_pack.manifest_bytes != manifest_bytes { 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), }); } } } } } let entries = manifest .manifest .parse_files() .map_err(ManifestDecodeError::Validate)?; let mut files = Vec::with_capacity(manifest.manifest.file_count()); for entry in &entries { let rsync_uri = join_rsync_dir_and_file(publication_point_rsync_uri, entry.file_name.as_str()); let bytes = store .get_raw(&rsync_uri) .map_err(|_e| ManifestFreshError::MissingFile { rsync_uri: rsync_uri.clone(), })? .ok_or_else(|| ManifestFreshError::MissingFile { rsync_uri: rsync_uri.clone(), })?; let computed = sha2::Sha256::digest(&bytes); if computed.as_slice() != entry.hash_bytes.as_ref() { return Err(ManifestFreshError::HashMismatch { rsync_uri }); } files.push(crate::storage::PackFile::from_bytes_compute_sha256( rsync_uri, bytes, )); } // 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. validate_manifest_embedded_ee_cert_path( &manifest, &files, issuer_ca_der, issuer_ca_rsync_uri, validation_time, )?; Ok(FetchCachePpPack { format_version: FetchCachePpPack::FORMAT_VERSION_V1, 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: crate::storage::PackTime::from_utc_offset_datetime(this_update), next_update: crate::storage::PackTime::from_utc_offset_datetime(next_update), verified_at: crate::storage::PackTime::from_utc_offset_datetime(now), manifest_bytes, files, }) } 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 ee_der = ee.raw_der.as_slice(); 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 _validated = validate_ee_cert_path( ee_der, issuer_ca_der, f.bytes.as_slice(), 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(", "), )) }