1601 lines
60 KiB
Rust
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(¤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::<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(¤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::<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}"
|
|
);
|
|
}
|
|
}
|