rpki/src/validation/manifest.rs

1601 lines
60 KiB
Rust

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<Warning>,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct FreshValidatedPublicationPoint {
pub manifest_rsync_uri: String,
pub publication_point_rsync_uri: String,
pub manifest_number_be: Vec<u8>,
pub this_update: PackTime,
pub next_update: PackTime,
pub verified_at: PackTime,
pub manifest_bytes: Vec<u8>,
pub files: Vec<PackFile>,
}
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<PublicationPointResult, ManifestProcessError> {
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<FreshValidatedPublicationPoint, ManifestFreshError> {
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<PublicationPointResult, ManifestProcessError> {
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<PublicationPointSnapshot, ManifestReuseError> {
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<ManifestObject, ManifestFreshError> {
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<FreshValidatedPublicationPoint, ManifestFreshError> {
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(&current.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(&current_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::<Vec<_>>();
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::<std::collections::HashSet<_>>();
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::<Vec<_>>()
.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<u8> {
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<u8>,
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<u8>, 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<u8>, 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<u8>, 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<PackFile> {
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::<Vec<_>>();
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(&current_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::<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::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::<Vec<_>>();
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}"
);
}
}