20261015_4 修复Manifest编号与改名抗重放校验
This commit is contained in:
@@ -11,6 +11,8 @@ pub struct CaInstanceUris {
|
||||
pub rsync_base_uri: String,
|
||||
/// rsync URI for the manifest object (`.mft`).
|
||||
pub manifest_rsync_uri: String,
|
||||
/// All current id-ad-rpkiManifest locations, including alternative names.
|
||||
pub manifest_sia_uris: Vec<String>,
|
||||
/// Publication point rsync URI (RFC 9286 terminology).
|
||||
pub publication_point_rsync_uri: String,
|
||||
/// Optional RRDP notification URI (https).
|
||||
@@ -78,6 +80,7 @@ pub fn ca_instance_uris_from_ca_certificate(
|
||||
|
||||
let mut ca_repo: Option<String> = None;
|
||||
let mut manifest: Option<String> = None;
|
||||
let mut manifest_sia_uris = Vec::new();
|
||||
let mut notify: Option<String> = None;
|
||||
|
||||
for ad in access_descriptions {
|
||||
@@ -93,6 +96,7 @@ pub fn ca_instance_uris_from_ca_certificate(
|
||||
return Err(CaInstanceUrisError::RpkiManifestNotRsync(u.to_string()));
|
||||
}
|
||||
manifest.get_or_insert(u.to_string());
|
||||
manifest_sia_uris.push(u.to_string());
|
||||
} else if ad.access_method_oid == OID_AD_RPKI_NOTIFY {
|
||||
let u = ad.access_location.as_str();
|
||||
if !u.starts_with("https://") {
|
||||
@@ -126,6 +130,7 @@ pub fn ca_instance_uris_from_ca_certificate(
|
||||
Ok(CaInstanceUris {
|
||||
rsync_base_uri: manifest_parent,
|
||||
manifest_rsync_uri,
|
||||
manifest_sia_uris,
|
||||
publication_point_rsync_uri,
|
||||
rrdp_notification_uri: notify,
|
||||
})
|
||||
|
||||
@@ -368,6 +368,10 @@ pub fn run_root_from_tal_url_once(
|
||||
|
||||
let run = run_publication_point_once(
|
||||
store,
|
||||
&<sha2::Sha256 as sha2::Digest>::digest(
|
||||
&discovery.trust_anchor.tal.subject_public_key_info_der,
|
||||
)
|
||||
.into(),
|
||||
policy,
|
||||
discovery.ca_instance.rrdp_notification_uri.as_deref(),
|
||||
&discovery.ca_instance.rsync_base_uri,
|
||||
|
||||
@@ -4,7 +4,9 @@ use crate::model::manifest::{ManifestDecodeError, ManifestObject, ManifestValida
|
||||
use crate::model::signed_object::SignedObjectVerifyError;
|
||||
use crate::output::report::Warning;
|
||||
use crate::repository::current_repo_index::CurrentRepoIndexHandle;
|
||||
use crate::repository::storage::{PackFile, PackTime, RocksStore, StorageError};
|
||||
use crate::repository::storage::{
|
||||
CaIdentity, CaManifestHistory, PackFile, PackTime, RocksStore, StorageError,
|
||||
};
|
||||
use crate::validation::cert_path::{CertPathError, validate_signed_object_ee_cert_path_fast};
|
||||
use crate::validation::policy::Policy;
|
||||
use crate::validation::publication_point::PublicationPointSnapshot;
|
||||
@@ -14,6 +16,7 @@ use x509_parser::prelude::FromDer;
|
||||
|
||||
include!("manifest/models_and_process.rs");
|
||||
include!("manifest/helpers.rs");
|
||||
include!("manifest/history.rs");
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests;
|
||||
|
||||
160
src/validation/manifest/history.rs
Normal file
160
src/validation/manifest/history.rs
Normal file
@@ -0,0 +1,160 @@
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct ManifestHistoryCandidate {
|
||||
pub identity: CaIdentity,
|
||||
pub manifest_sia_uris: Vec<String>,
|
||||
pub history: CaManifestHistory,
|
||||
}
|
||||
|
||||
fn manifest_filename(uri: &str) -> &str {
|
||||
uri.split(['?', '#'])
|
||||
.next()
|
||||
.unwrap_or("")
|
||||
.rsplit('/')
|
||||
.next()
|
||||
.unwrap_or("")
|
||||
}
|
||||
|
||||
fn manifest_filename_changed(old: &CaManifestHistory, current_sia: &[String]) -> bool {
|
||||
!current_sia
|
||||
.iter()
|
||||
.any(|uri| manifest_filename(uri) == old.reference_filename)
|
||||
}
|
||||
|
||||
/// Pure policy shared by staging and the locked commit. No state is reset
|
||||
/// until the whole publication point has passed validation.
|
||||
fn checked_manifest_history(
|
||||
old: Option<&CaManifestHistory>,
|
||||
candidate: &ManifestHistoryCandidate,
|
||||
) -> Result<CaManifestHistory, ManifestFreshError> {
|
||||
let mut next = candidate.history.clone();
|
||||
if !candidate
|
||||
.manifest_sia_uris
|
||||
.iter()
|
||||
.any(|uri| uri == &next.manifest_rsync_uri)
|
||||
{
|
||||
return Err(ManifestFreshError::IssuerCaLoadFailed {
|
||||
detail: "selected manifest URI absent from verified CA SIA".into(),
|
||||
});
|
||||
}
|
||||
next.validate_internal()?;
|
||||
next.reference_filename = manifest_filename(&next.manifest_rsync_uri).into();
|
||||
next.generation = 1;
|
||||
if let Some(old) = old {
|
||||
old.validate_internal()?;
|
||||
next.generation =
|
||||
old.generation
|
||||
.checked_add(1)
|
||||
.ok_or_else(|| StorageError::InvalidData {
|
||||
entity: "ca_manifest",
|
||||
detail: "history generation exhausted".into(),
|
||||
})?;
|
||||
let renamed = manifest_filename_changed(old, &candidate.manifest_sia_uris);
|
||||
if !renamed {
|
||||
next.reference_filename = old.reference_filename.clone();
|
||||
if next.manifest_sha256 == old.manifest_sha256
|
||||
&& next.manifest_number_be == old.manifest_number_be
|
||||
&& next.manifest_this_update == old.manifest_this_update
|
||||
{
|
||||
// The same signed file can be revalidated, but never reset a
|
||||
// reference name or bypass current validity / location checks.
|
||||
next.validate_internal()?;
|
||||
return Ok(next);
|
||||
}
|
||||
if cmp_minimal_be_unsigned(&next.manifest_number_be, &old.manifest_number_be)
|
||||
!= Ordering::Greater
|
||||
{
|
||||
return Err(ManifestFreshError::ManifestNumberNotIncreasing {
|
||||
old_hex: hex::encode_upper(&old.manifest_number_be),
|
||||
new_hex: hex::encode_upper(&next.manifest_number_be),
|
||||
});
|
||||
}
|
||||
}
|
||||
if next
|
||||
.manifest_this_update
|
||||
.parse()
|
||||
.expect("validated candidate time")
|
||||
<= old
|
||||
.manifest_this_update
|
||||
.parse()
|
||||
.expect("validated history time")
|
||||
{
|
||||
return Err(ManifestFreshError::ThisUpdateNotIncreasing {
|
||||
old_rfc3339_utc: old.manifest_this_update.rfc3339_utc.clone(),
|
||||
new_rfc3339_utc: next.manifest_this_update.rfc3339_utc.clone(),
|
||||
});
|
||||
}
|
||||
}
|
||||
next.validate_internal()?;
|
||||
Ok(next)
|
||||
}
|
||||
|
||||
pub(crate) fn commit_manifest_history(
|
||||
store: &RocksStore,
|
||||
candidate: &ManifestHistoryCandidate,
|
||||
) -> Result<(CaManifestHistory, Option<Warning>), ManifestFreshError> {
|
||||
let mut warning = None;
|
||||
let history = store.update_ca_manifest_history(&candidate.identity, |old| {
|
||||
let next = checked_manifest_history(old, candidate)?;
|
||||
warning = old.and_then(|old| {
|
||||
rename_warning_for_history(
|
||||
old,
|
||||
&candidate.identity,
|
||||
&candidate.manifest_sia_uris,
|
||||
&candidate.history.manifest_rsync_uri,
|
||||
)
|
||||
});
|
||||
Ok::<_, ManifestFreshError>(next)
|
||||
})?;
|
||||
Ok((history, warning))
|
||||
}
|
||||
|
||||
pub(crate) fn manifest_rename_warning(
|
||||
store: &RocksStore,
|
||||
identity: &CaIdentity,
|
||||
current_sia: &[String],
|
||||
selected_uri: &str,
|
||||
) -> Result<Option<Warning>, ManifestFreshError> {
|
||||
let Some(old) = store.get_ca_manifest_history(identity)? else {
|
||||
return Ok(None);
|
||||
};
|
||||
Ok(rename_warning_for_history(
|
||||
&old,
|
||||
identity,
|
||||
current_sia,
|
||||
selected_uri,
|
||||
))
|
||||
}
|
||||
|
||||
fn rename_warning_for_history(
|
||||
old: &CaManifestHistory,
|
||||
identity: &CaIdentity,
|
||||
current_sia: &[String],
|
||||
selected_uri: &str,
|
||||
) -> Option<Warning> {
|
||||
if !manifest_filename_changed(old, current_sia) {
|
||||
return None;
|
||||
}
|
||||
Some(Warning::new(format!(
|
||||
"manifest filename changed: ca={}:{} old={} selected={} current_sia={:?} old_number={} old_this_update={} (RFC 9981 §2–3)",
|
||||
hex::encode(identity.anchor_namespace), hex::encode(identity.spki_sha256), old.reference_filename,
|
||||
selected_uri, current_sia, hex::encode_upper(&old.manifest_number_be), old.manifest_this_update.rfc3339_utc
|
||||
)).with_category(crate::output::report::WarningCategory::SecurityPolicy).with_context(selected_uri))
|
||||
}
|
||||
|
||||
fn validate_manifest_sia_location(
|
||||
manifest: &ManifestObject,
|
||||
actual_uri: &str,
|
||||
) -> Result<(), ManifestFreshError> {
|
||||
let ee = &manifest.signed_object.signed_data.certificates[0];
|
||||
if ee
|
||||
.sia_signed_object_uris
|
||||
.iter()
|
||||
.any(|uri| uri == actual_uri)
|
||||
{
|
||||
return Ok(());
|
||||
}
|
||||
Err(ManifestFreshError::ManifestSiaUriMismatch {
|
||||
actual_uri: actual_uri.into(),
|
||||
signed_object_uris: ee.sia_signed_object_uris.clone(),
|
||||
})
|
||||
}
|
||||
325
src/validation/manifest/history_tests.rs
Normal file
325
src/validation/manifest/history_tests.rs
Normal file
@@ -0,0 +1,325 @@
|
||||
fn history_candidate(name: &str, number: Vec<u8>, seconds: i64) -> ManifestHistoryCandidate {
|
||||
let now = time::OffsetDateTime::UNIX_EPOCH + time::Duration::seconds(seconds);
|
||||
let uri = format!("rsync://example.net/repo/{name}");
|
||||
ManifestHistoryCandidate {
|
||||
identity: CaIdentity::from_spki([1; 32], b"verified CA SPKI"),
|
||||
manifest_sia_uris: vec![uri.clone()],
|
||||
history: CaManifestHistory {
|
||||
reference_filename: name.into(),
|
||||
manifest_rsync_uri: uri,
|
||||
manifest_number_be: number,
|
||||
manifest_this_update: PackTime::from_utc_offset_datetime(now),
|
||||
manifest_sha256: sha2::Sha256::digest(seconds.to_be_bytes()).to_vec(),
|
||||
updated_at_validation_time: PackTime::from_utc_offset_datetime(now),
|
||||
generation: 1,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn filename_reset_keeps_time_guard_and_latest_history_across_a_b_a() {
|
||||
let td = tempfile::tempdir().unwrap();
|
||||
let store = RocksStore::open(td.path()).unwrap();
|
||||
let first = history_candidate("a.mft", vec![0x7f; 20], 100);
|
||||
let (h1, _) = commit_manifest_history(&store, &first).unwrap();
|
||||
let second = history_candidate("b.mft", vec![0], 101);
|
||||
assert!(
|
||||
manifest_rename_warning(
|
||||
&store,
|
||||
&second.identity,
|
||||
&second.manifest_sia_uris,
|
||||
&second.history.manifest_rsync_uri
|
||||
)
|
||||
.unwrap()
|
||||
.is_some()
|
||||
);
|
||||
let (h2, _) = commit_manifest_history(&store, &second).unwrap();
|
||||
assert_eq!(h2.reference_filename, "b.mft");
|
||||
assert_eq!(h2.generation, h1.generation + 1);
|
||||
assert!(
|
||||
manifest_rename_warning(
|
||||
&store,
|
||||
&second.identity,
|
||||
&second.manifest_sia_uris,
|
||||
&second.history.manifest_rsync_uri
|
||||
)
|
||||
.unwrap()
|
||||
.is_none()
|
||||
);
|
||||
assert!(matches!(
|
||||
commit_manifest_history(&store, &first),
|
||||
Err(ManifestFreshError::ThisUpdateNotIncreasing { .. })
|
||||
));
|
||||
assert_eq!(
|
||||
store.get_ca_manifest_history(&first.identity).unwrap(),
|
||||
Some(h2.clone())
|
||||
);
|
||||
let third = history_candidate("a.mft", vec![0], 102);
|
||||
assert_eq!(
|
||||
commit_manifest_history(&store, &third)
|
||||
.unwrap()
|
||||
.0
|
||||
.reference_filename,
|
||||
"a.mft"
|
||||
);
|
||||
assert_eq!(
|
||||
checked_manifest_history(Some(&h2), &history_candidate("b.mft", vec![0], 103))
|
||||
.unwrap_err()
|
||||
.warning_category(),
|
||||
crate::output::report::WarningCategory::SecurityPolicy
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn multi_sia_reordering_and_directory_move_do_not_reset_number() {
|
||||
let first = history_candidate("a.mft", vec![10], 100).history;
|
||||
let mut reordered = history_candidate("b.mft", vec![1], 101);
|
||||
reordered
|
||||
.manifest_sia_uris
|
||||
.push("rsync://other.example/new/a.mft".into());
|
||||
assert!(!manifest_filename_changed(
|
||||
&first,
|
||||
&reordered.manifest_sia_uris
|
||||
));
|
||||
assert!(matches!(
|
||||
checked_manifest_history(Some(&first), &reordered),
|
||||
Err(ManifestFreshError::ManifestNumberNotIncreasing { .. })
|
||||
));
|
||||
reordered.history.manifest_number_be = vec![11];
|
||||
assert_eq!(
|
||||
checked_manifest_history(Some(&first), &reordered)
|
||||
.unwrap()
|
||||
.reference_filename,
|
||||
"a.mft"
|
||||
);
|
||||
let mut moved = history_candidate("a.mft", vec![1], 101);
|
||||
moved.history.manifest_rsync_uri = "rsync://new.example/another/a.mft".into();
|
||||
moved.manifest_sia_uris = vec![moved.history.manifest_rsync_uri.clone()];
|
||||
assert!(checked_manifest_history(Some(&first), &moved).is_err());
|
||||
moved.history.manifest_number_be = vec![11];
|
||||
moved.history.manifest_this_update = first.manifest_this_update.clone();
|
||||
assert!(matches!(
|
||||
checked_manifest_history(Some(&first), &moved),
|
||||
Err(ManifestFreshError::ThisUpdateNotIncreasing { .. })
|
||||
));
|
||||
moved.history.manifest_this_update = PackTime::from_utc_offset_datetime(
|
||||
time::OffsetDateTime::UNIX_EPOCH + time::Duration::seconds(101),
|
||||
);
|
||||
assert!(checked_manifest_history(Some(&first), &moved).is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unchanged_bytes_never_bypass_filename_time_or_selected_sia() {
|
||||
let old = history_candidate("a.mft", vec![10], 100).history;
|
||||
let same = history_candidate("a.mft", vec![10], 100);
|
||||
assert!(checked_manifest_history(Some(&old), &same).is_ok());
|
||||
let mut renamed = same.clone();
|
||||
renamed.history.manifest_rsync_uri = "rsync://example.net/repo/b.mft".into();
|
||||
renamed.manifest_sia_uris = vec![renamed.history.manifest_rsync_uri.clone()];
|
||||
assert!(matches!(
|
||||
checked_manifest_history(Some(&old), &renamed),
|
||||
Err(ManifestFreshError::ThisUpdateNotIncreasing { .. })
|
||||
));
|
||||
renamed.manifest_sia_uris.clear();
|
||||
assert!(checked_manifest_history(Some(&old), &renamed).is_err());
|
||||
let mut equal_changed = same;
|
||||
equal_changed.history.manifest_sha256[0] ^= 1;
|
||||
assert!(matches!(
|
||||
checked_manifest_history(Some(&old), &equal_changed),
|
||||
Err(ManifestFreshError::ManifestNumberNotIncreasing { .. })
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ca_history_isolates_keys_and_tal_namespaces_and_survives_restart() {
|
||||
let td = tempfile::tempdir().unwrap();
|
||||
let first = history_candidate("a.mft", vec![10], 100);
|
||||
{
|
||||
let store = RocksStore::open(td.path()).unwrap();
|
||||
commit_manifest_history(&store, &first).unwrap();
|
||||
}
|
||||
let store = RocksStore::open(td.path()).unwrap();
|
||||
let same_key = CaIdentity::from_spki([1; 32], b"verified CA SPKI");
|
||||
assert!(store.get_ca_manifest_history(&same_key).unwrap().is_some());
|
||||
let mut changed = history_candidate("a.mft", vec![0], 1);
|
||||
changed.identity = CaIdentity::from_spki([1; 32], b"rolled CA key");
|
||||
assert!(commit_manifest_history(&store, &changed).is_ok());
|
||||
changed.identity = CaIdentity::from_spki([2; 32], b"verified CA SPKI");
|
||||
assert!(commit_manifest_history(&store, &changed).is_ok());
|
||||
assert_eq!(
|
||||
store
|
||||
.get_ca_manifest_history(&same_key)
|
||||
.unwrap()
|
||||
.unwrap()
|
||||
.manifest_number_be,
|
||||
vec![10]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn concurrent_candidates_recheck_latest_history_before_commit() {
|
||||
let td = tempfile::tempdir().unwrap();
|
||||
let store = std::sync::Arc::new(RocksStore::open(td.path()).unwrap());
|
||||
let first = history_candidate("a.mft", vec![1], 100);
|
||||
commit_manifest_history(&store, &first).unwrap();
|
||||
let next = history_candidate("a.mft", vec![2], 101);
|
||||
let latest = history_candidate("a.mft", vec![3], 102);
|
||||
// Both candidates pass against generation 1 before either commits.
|
||||
let baseline = store
|
||||
.get_ca_manifest_history(&first.identity)
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
assert!(checked_manifest_history(Some(&baseline), &next).is_ok());
|
||||
assert!(checked_manifest_history(Some(&baseline), &latest).is_ok());
|
||||
let barrier = std::sync::Arc::new(std::sync::Barrier::new(2));
|
||||
std::thread::scope(|scope| {
|
||||
let newer = scope.spawn(|| {
|
||||
commit_manifest_history(&store, &latest).unwrap();
|
||||
barrier.wait();
|
||||
});
|
||||
let older = scope.spawn(|| {
|
||||
barrier.wait();
|
||||
assert!(commit_manifest_history(&store, &next).is_err());
|
||||
});
|
||||
newer.join().unwrap();
|
||||
older.join().unwrap();
|
||||
});
|
||||
let saved = store
|
||||
.get_ca_manifest_history(&first.identity)
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
assert_eq!(saved.manifest_number_be, vec![3]);
|
||||
assert_eq!(saved.generation, 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn signed_object_sia_requires_exact_location_and_accepts_any_matching_entry() {
|
||||
let (mut manifest, _, uri, _, _) = load_manifest_fixture();
|
||||
assert!(validate_manifest_sia_location(&manifest, &uri).is_ok());
|
||||
let alternate = uri.replace("child.mft", "another.mft");
|
||||
assert!(matches!(
|
||||
validate_manifest_sia_location(&manifest, &alternate),
|
||||
Err(ManifestFreshError::ManifestSiaUriMismatch { .. })
|
||||
));
|
||||
manifest.signed_object.signed_data.certificates[0]
|
||||
.sia_signed_object_uris
|
||||
.insert(0, alternate);
|
||||
assert!(validate_manifest_sia_location(&manifest, &uri).is_ok());
|
||||
assert!(
|
||||
validate_manifest_sia_location(&manifest, &uri.replace("rsync://", "RSYNC://")).is_err()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ca_sia_preserves_all_names_and_identity_ignores_certificate_reissuance() {
|
||||
use crate::model::rc::{AccessDescription, SubjectInfoAccess};
|
||||
let mut cert =
|
||||
crate::model::rc::ResourceCertificate::decode_der(&issuer_ca_fixture_der()).unwrap();
|
||||
let old_identity = CaIdentity::from_spki([1; 32], &cert.tbs.subject_public_key_info);
|
||||
let original =
|
||||
crate::validation::ca_instance::ca_instance_uris_from_ca_certificate(&cert).unwrap();
|
||||
let SubjectInfoAccess::Ca(sia) = cert.tbs.extensions.subject_info_access.as_mut().unwrap()
|
||||
else {
|
||||
panic!("CA SIA")
|
||||
};
|
||||
let alternative = "rsync://rpki-local-rsync:873/custom/child/alternative.mft";
|
||||
sia.access_descriptions.insert(
|
||||
0,
|
||||
AccessDescription {
|
||||
access_method_oid: crate::model::oid::OID_AD_RPKI_MANIFEST.into(),
|
||||
access_location: alternative.into(),
|
||||
},
|
||||
);
|
||||
let updated =
|
||||
crate::validation::ca_instance::ca_instance_uris_from_ca_certificate(&cert).unwrap();
|
||||
assert_eq!(updated.manifest_rsync_uri, alternative);
|
||||
assert_eq!(
|
||||
updated.manifest_sia_uris,
|
||||
vec![alternative.to_string(), original.manifest_rsync_uri]
|
||||
);
|
||||
assert_eq!(
|
||||
old_identity,
|
||||
CaIdentity::from_spki([1; 32], &cert.tbs.subject_public_key_info)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn expired_and_early_manifest_revalidation_cannot_bypass_validity() {
|
||||
let (manifest, bytes, _, _, _) = load_manifest_fixture();
|
||||
for now in [
|
||||
manifest.manifest.this_update - time::Duration::seconds(1),
|
||||
manifest.manifest.next_update + time::Duration::seconds(1),
|
||||
] {
|
||||
assert!(matches!(
|
||||
decode_and_validate_manifest_with_current_time(&bytes, now),
|
||||
Err(ManifestFreshError::StaleOrEarly { .. })
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn maximum_number_cannot_wrap_without_rename_and_generation_cannot_wrap() {
|
||||
let old = history_candidate("a.mft", vec![0x7f; 20], 100).history;
|
||||
let same_name = history_candidate("a.mft", vec![0], 101);
|
||||
assert!(matches!(
|
||||
checked_manifest_history(Some(&old), &same_name),
|
||||
Err(ManifestFreshError::ManifestNumberNotIncreasing { .. })
|
||||
));
|
||||
let exhausted = CaManifestHistory {
|
||||
generation: u64::MAX,
|
||||
..old
|
||||
};
|
||||
let renamed = history_candidate("b.mft", vec![0], 101);
|
||||
assert!(matches!(
|
||||
checked_manifest_history(Some(&exhausted), &renamed),
|
||||
Err(ManifestFreshError::Storage(_))
|
||||
));
|
||||
let invalid = ManifestHistoryCandidate {
|
||||
history: CaManifestHistory {
|
||||
manifest_number_be: vec![0x80; 20],
|
||||
..renamed.history.clone()
|
||||
},
|
||||
..renamed
|
||||
};
|
||||
assert!(matches!(
|
||||
checked_manifest_history(None, &invalid),
|
||||
Err(ManifestFreshError::Storage(_))
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn interleaved_rename_is_reported_against_the_latest_committed_reference() {
|
||||
let td = tempfile::tempdir().unwrap();
|
||||
let store = RocksStore::open(td.path()).unwrap();
|
||||
let original = history_candidate("a.mft", vec![10], 100);
|
||||
commit_manifest_history(&store, &original).unwrap();
|
||||
let candidate = history_candidate("a.mft", vec![11], 103);
|
||||
assert!(
|
||||
manifest_rename_warning(
|
||||
&store,
|
||||
&candidate.identity,
|
||||
&candidate.manifest_sia_uris,
|
||||
&candidate.history.manifest_rsync_uri
|
||||
)
|
||||
.unwrap()
|
||||
.is_none()
|
||||
);
|
||||
commit_manifest_history(&store, &history_candidate("b.mft", vec![0], 102)).unwrap();
|
||||
let (history, warning) = commit_manifest_history(&store, &candidate).unwrap();
|
||||
assert_eq!(history.reference_filename, "a.mft");
|
||||
assert!(warning.unwrap().message.contains("old=b.mft"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn filenames_compare_uri_path_segments_without_query_or_fragment() {
|
||||
assert_eq!(
|
||||
manifest_filename("rsync://host/r/a.mft?key=1#fragment"),
|
||||
"a.mft"
|
||||
);
|
||||
let old = history_candidate("a.mft", vec![10], 100).history;
|
||||
assert!(!manifest_filename_changed(
|
||||
&old,
|
||||
&["rsync://new-host/new/a.mft?key=2".into()]
|
||||
));
|
||||
assert_eq!(manifest_filename("rsync://host/r/a%3Fmft"), "a%3Fmft");
|
||||
}
|
||||
@@ -15,6 +15,7 @@ pub struct PublicationPointResult {
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct FreshValidatedPublicationPoint {
|
||||
pub history_candidate: ManifestHistoryCandidate,
|
||||
pub manifest_rsync_uri: String,
|
||||
pub publication_point_rsync_uri: String,
|
||||
pub manifest_number_be: Vec<u8>,
|
||||
@@ -203,6 +204,14 @@ pub enum ManifestFreshError {
|
||||
#[error("manifest file hash mismatch: {rsync_uri} (RFC 9286 §6.5; RFC 9286 §6.6)")]
|
||||
HashMismatch { rsync_uri: String },
|
||||
|
||||
#[error(
|
||||
"ManifestSiaUriMismatch: actual={actual_uri} signedObject={signed_object_uris:?} (RFC 9981 §4; RFC 9286 §6.6)"
|
||||
)]
|
||||
ManifestSiaUriMismatch {
|
||||
actual_uri: String,
|
||||
signed_object_uris: Vec<String>,
|
||||
},
|
||||
|
||||
#[error("issuer CA certificate bytes unavailable: {detail} (RFC 6487 §4; RFC 9286 §6.2)")]
|
||||
IssuerCaLoadFailed { detail: String },
|
||||
}
|
||||
@@ -211,11 +220,13 @@ impl ManifestFreshError {
|
||||
pub(crate) fn warning_category(&self) -> crate::output::report::WarningCategory {
|
||||
use crate::output::report::WarningCategory;
|
||||
match self {
|
||||
Self::Storage(_) | Self::RepoSyncFailed { .. } => WarningCategory::Unclassified,
|
||||
Self::RepoSyncFailed { .. } => WarningCategory::Unclassified,
|
||||
Self::IssuerCaLoadFailed { .. } | Self::EeCertPath(_) | Self::EeResources(_) => {
|
||||
WarningCategory::Certificate
|
||||
}
|
||||
Self::ManifestOutsidePublicationPoint { .. }
|
||||
Self::Storage(_)
|
||||
| Self::ManifestSiaUriMismatch { .. }
|
||||
| Self::ManifestOutsidePublicationPoint { .. }
|
||||
| Self::ManifestNumberNotIncreasing { .. }
|
||||
| Self::ThisUpdateNotIncreasing { .. }
|
||||
| Self::HashMismatch { .. } => WarningCategory::SecurityPolicy,
|
||||
@@ -235,6 +246,7 @@ pub enum ManifestProcessError {
|
||||
|
||||
pub fn process_manifest_publication_point(
|
||||
store: &RocksStore,
|
||||
anchor_namespace: &[u8; 32],
|
||||
policy: &Policy,
|
||||
manifest_rsync_uri: &str,
|
||||
publication_point_rsync_uri: &str,
|
||||
@@ -244,6 +256,7 @@ pub fn process_manifest_publication_point(
|
||||
) -> Result<PublicationPointResult, ManifestProcessError> {
|
||||
process_manifest_publication_point_after_repo_sync(
|
||||
store,
|
||||
anchor_namespace,
|
||||
policy,
|
||||
manifest_rsync_uri,
|
||||
publication_point_rsync_uri,
|
||||
@@ -257,6 +270,7 @@ pub fn process_manifest_publication_point(
|
||||
|
||||
pub fn process_manifest_publication_point_fresh_after_repo_sync(
|
||||
store: &RocksStore,
|
||||
anchor_namespace: &[u8; 32],
|
||||
manifest_rsync_uri: &str,
|
||||
publication_point_rsync_uri: &str,
|
||||
issuer_ca_der: &[u8],
|
||||
@@ -267,6 +281,7 @@ pub fn process_manifest_publication_point_fresh_after_repo_sync(
|
||||
) -> Result<FreshValidatedPublicationPoint, ManifestFreshError> {
|
||||
process_manifest_publication_point_fresh_after_repo_sync_with_timing(
|
||||
store,
|
||||
anchor_namespace,
|
||||
manifest_rsync_uri,
|
||||
publication_point_rsync_uri,
|
||||
None,
|
||||
@@ -299,6 +314,7 @@ pub struct FreshPublicationPointTimingBreakdown {
|
||||
|
||||
pub fn process_manifest_publication_point_fresh_after_repo_sync_with_timing(
|
||||
store: &RocksStore,
|
||||
anchor_namespace: &[u8; 32],
|
||||
manifest_rsync_uri: &str,
|
||||
publication_point_rsync_uri: &str,
|
||||
current_repo_index: Option<&CurrentRepoIndexHandle>,
|
||||
@@ -317,6 +333,7 @@ pub fn process_manifest_publication_point_fresh_after_repo_sync_with_timing(
|
||||
if repo_sync_ok {
|
||||
try_build_fresh_publication_point_with_timing(
|
||||
store,
|
||||
anchor_namespace,
|
||||
manifest_rsync_uri,
|
||||
publication_point_rsync_uri,
|
||||
current_repo_index,
|
||||
@@ -333,6 +350,7 @@ pub fn process_manifest_publication_point_fresh_after_repo_sync_with_timing(
|
||||
|
||||
pub fn process_manifest_publication_point_after_repo_sync(
|
||||
store: &RocksStore,
|
||||
anchor_namespace: &[u8; 32],
|
||||
policy: &Policy,
|
||||
manifest_rsync_uri: &str,
|
||||
publication_point_rsync_uri: &str,
|
||||
@@ -345,6 +363,7 @@ pub fn process_manifest_publication_point_after_repo_sync(
|
||||
let fresh = if repo_sync_ok {
|
||||
try_build_fresh_publication_point(
|
||||
store,
|
||||
anchor_namespace,
|
||||
manifest_rsync_uri,
|
||||
publication_point_rsync_uri,
|
||||
issuer_ca_der,
|
||||
@@ -406,6 +425,7 @@ fn decode_and_validate_manifest_with_current_time(
|
||||
|
||||
pub(crate) fn try_build_fresh_publication_point(
|
||||
store: &RocksStore,
|
||||
anchor_namespace: &[u8; 32],
|
||||
manifest_rsync_uri: &str,
|
||||
publication_point_rsync_uri: &str,
|
||||
issuer_ca_der: &[u8],
|
||||
@@ -414,6 +434,7 @@ pub(crate) fn try_build_fresh_publication_point(
|
||||
) -> Result<FreshValidatedPublicationPoint, ManifestFreshError> {
|
||||
try_build_fresh_publication_point_with_timing(
|
||||
store,
|
||||
anchor_namespace,
|
||||
manifest_rsync_uri,
|
||||
publication_point_rsync_uri,
|
||||
None,
|
||||
@@ -426,6 +447,7 @@ pub(crate) fn try_build_fresh_publication_point(
|
||||
|
||||
pub(crate) fn try_build_fresh_publication_point_with_timing(
|
||||
store: &RocksStore,
|
||||
anchor_namespace: &[u8; 32],
|
||||
manifest_rsync_uri: &str,
|
||||
publication_point_rsync_uri: &str,
|
||||
current_repo_index: Option<&CurrentRepoIndexHandle>,
|
||||
@@ -505,64 +527,34 @@ pub(crate) fn try_build_fresh_publication_point_with_timing(
|
||||
.to_offset(time::UtcOffset::UTC);
|
||||
let now = validation_time.to_offset(time::UtcOffset::UTC);
|
||||
|
||||
// RFC 9286 §4.2.1: 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 continue the current validation run with a clear failure if the
|
||||
// repository cannot provide a valid manifest.
|
||||
// - 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 anti_rollback_started = std::time::Instant::now();
|
||||
if let Some(old_meta) = store.get_manifest_anti_rollback_meta(manifest_rsync_uri)? {
|
||||
timing.anti_rollback_meta_hit = true;
|
||||
if old_meta.manifest_rsync_uri == manifest_rsync_uri {
|
||||
let new_num = manifest.manifest.manifest_number.bytes_be.as_slice();
|
||||
let old_num = old_meta.manifest_number_be.as_slice();
|
||||
match cmp_minimal_be_unsigned(new_num, old_num) {
|
||||
Ordering::Greater => {
|
||||
let old_this_update = old_meta
|
||||
.manifest_this_update
|
||||
.parse()
|
||||
.expect("manifest anti-rollback metadata 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 new_manifest_hash = sha2::Sha256::digest(&manifest_bytes);
|
||||
if old_meta.manifest_sha256.as_slice() != new_manifest_hash.as_slice() {
|
||||
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),
|
||||
});
|
||||
}
|
||||
}
|
||||
validate_manifest_sia_location(&manifest, manifest_rsync_uri)?;
|
||||
let issuer = crate::model::rc::ResourceCertificate::decode_der(issuer_ca_der).map_err(|e| {
|
||||
ManifestFreshError::IssuerCaLoadFailed {
|
||||
detail: e.to_string(),
|
||||
}
|
||||
} else {
|
||||
timing.anti_rollback_meta_miss = true;
|
||||
}
|
||||
})?;
|
||||
let issuer_uris = crate::validation::ca_instance::ca_instance_uris_from_ca_certificate(&issuer)
|
||||
.map_err(|e| ManifestFreshError::IssuerCaLoadFailed {
|
||||
detail: e.to_string(),
|
||||
})?;
|
||||
let history_candidate = ManifestHistoryCandidate {
|
||||
identity: CaIdentity::from_spki(*anchor_namespace, &issuer.tbs.subject_public_key_info),
|
||||
manifest_sia_uris: issuer_uris.manifest_sia_uris,
|
||||
history: CaManifestHistory {
|
||||
reference_filename: manifest_filename(manifest_rsync_uri).into(),
|
||||
manifest_rsync_uri: manifest_rsync_uri.into(),
|
||||
manifest_number_be: manifest.manifest.manifest_number.bytes_be.clone(),
|
||||
manifest_this_update: PackTime::from_utc_offset_datetime(this_update),
|
||||
manifest_sha256: sha2::Sha256::digest(&manifest_bytes).to_vec(),
|
||||
updated_at_validation_time: PackTime::from_utc_offset_datetime(now),
|
||||
generation: 1,
|
||||
},
|
||||
};
|
||||
let anti_rollback_started = std::time::Instant::now();
|
||||
let old = store.get_ca_manifest_history(&history_candidate.identity)?;
|
||||
timing.anti_rollback_meta_hit = old.is_some();
|
||||
timing.anti_rollback_meta_miss = old.is_none();
|
||||
checked_manifest_history(old.as_ref(), &history_candidate)?;
|
||||
timing.anti_rollback_guard_ms = anti_rollback_started.elapsed().as_millis() as u64;
|
||||
|
||||
let manifest_entries_started = std::time::Instant::now();
|
||||
@@ -680,6 +672,7 @@ pub(crate) fn try_build_fresh_publication_point_with_timing(
|
||||
|
||||
Ok((
|
||||
FreshValidatedPublicationPoint {
|
||||
history_candidate,
|
||||
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(),
|
||||
|
||||
@@ -125,6 +125,7 @@ fn try_build_fresh_publication_point_rejects_manifest_outside_publication_point(
|
||||
let (_, _, manifest_rsync_uri, _, validation_time) = load_manifest_fixture();
|
||||
let err = try_build_fresh_publication_point(
|
||||
&store,
|
||||
&[0; 32],
|
||||
&manifest_rsync_uri,
|
||||
"rsync://example.test/other/",
|
||||
&issuer_ca_fixture_der(),
|
||||
@@ -149,6 +150,7 @@ fn try_build_fresh_publication_point_reports_missing_manifest_when_raw_store_is_
|
||||
load_manifest_fixture();
|
||||
let err = try_build_fresh_publication_point(
|
||||
&store,
|
||||
&[0; 32],
|
||||
&manifest_rsync_uri,
|
||||
&publication_point_rsync_uri,
|
||||
&issuer_ca_fixture_der(),
|
||||
@@ -192,6 +194,7 @@ fn try_build_fresh_publication_point_reports_missing_locked_file() {
|
||||
|
||||
let err = try_build_fresh_publication_point(
|
||||
&store,
|
||||
&[0; 32],
|
||||
&manifest_rsync_uri,
|
||||
&publication_point_rsync_uri,
|
||||
&issuer_ca_fixture_der(),
|
||||
@@ -254,6 +257,7 @@ fn try_build_fresh_publication_point_detects_hash_mismatch_via_repository_view_h
|
||||
|
||||
let err = try_build_fresh_publication_point(
|
||||
&store,
|
||||
&[0; 32],
|
||||
&manifest_rsync_uri,
|
||||
&publication_point_rsync_uri,
|
||||
&issuer_ca_fixture_der(),
|
||||
@@ -322,6 +326,7 @@ fn try_build_fresh_publication_point_uses_current_repo_index_without_repository_
|
||||
|
||||
let (fresh, _timing) = try_build_fresh_publication_point_with_timing(
|
||||
&store,
|
||||
&[0; 32],
|
||||
&manifest_rsync_uri,
|
||||
&publication_point_rsync_uri,
|
||||
Some(¤t_index),
|
||||
@@ -356,6 +361,7 @@ fn try_build_fresh_publication_point_records_anti_rollback_meta_miss() {
|
||||
|
||||
let (_fresh, timing) = try_build_fresh_publication_point_with_timing(
|
||||
&store,
|
||||
&[0; 32],
|
||||
&manifest_rsync_uri,
|
||||
&publication_point_rsync_uri,
|
||||
None,
|
||||
@@ -388,7 +394,9 @@ fn try_build_fresh_publication_point_uses_anti_rollback_meta_hit_for_same_manife
|
||||
validation_time,
|
||||
);
|
||||
store
|
||||
.put_manifest_anti_rollback_meta(&previous_meta)
|
||||
.update_ca_manifest_history::<StorageError>(&test_ca_identity(), |_| {
|
||||
Ok(previous_meta.clone())
|
||||
})
|
||||
.expect("persist freshness");
|
||||
put_complete_publication_point_current_objects(
|
||||
&store,
|
||||
@@ -400,6 +408,7 @@ fn try_build_fresh_publication_point_uses_anti_rollback_meta_hit_for_same_manife
|
||||
|
||||
let (_fresh, timing) = try_build_fresh_publication_point_with_timing(
|
||||
&store,
|
||||
&[0; 32],
|
||||
&manifest_rsync_uri,
|
||||
&publication_point_rsync_uri,
|
||||
None,
|
||||
@@ -527,8 +536,10 @@ fn sample_manifest_meta(
|
||||
_base: &str,
|
||||
bytes: &[u8],
|
||||
now: time::OffsetDateTime,
|
||||
) -> crate::repository::storage::ManifestAntiRollbackMeta {
|
||||
crate::repository::storage::ManifestAntiRollbackMeta {
|
||||
) -> CaManifestHistory {
|
||||
CaManifestHistory {
|
||||
reference_filename: manifest_filename(uri).into(),
|
||||
generation: 1,
|
||||
manifest_rsync_uri: uri.into(),
|
||||
manifest_number_be: manifest.manifest.manifest_number.bytes_be.clone(),
|
||||
manifest_this_update: PackTime::from_utc_offset_datetime(manifest.manifest.this_update),
|
||||
@@ -557,7 +568,11 @@ fn history_survives_restart_and_rejects_rollback_without_advancing() {
|
||||
}
|
||||
{
|
||||
let store = RocksStore::open(dir.path()).unwrap();
|
||||
store.put_manifest_anti_rollback_meta(&meta).unwrap();
|
||||
store
|
||||
.update_ca_manifest_history::<StorageError>(&test_ca_identity(), |_| {
|
||||
Ok(meta.clone())
|
||||
})
|
||||
.unwrap();
|
||||
put_complete_publication_point_current_objects(
|
||||
&store,
|
||||
&manifest,
|
||||
@@ -568,11 +583,12 @@ fn history_survives_restart_and_rejects_rollback_without_advancing() {
|
||||
}
|
||||
let store = RocksStore::open(dir.path()).unwrap();
|
||||
assert_eq!(
|
||||
store.get_manifest_anti_rollback_meta(&uri).unwrap(),
|
||||
store.get_ca_manifest_history(&test_ca_identity()).unwrap(),
|
||||
Some(meta.clone())
|
||||
);
|
||||
let result = try_build_fresh_publication_point_with_timing(
|
||||
&store,
|
||||
&[0; 32],
|
||||
&uri,
|
||||
&base,
|
||||
None,
|
||||
@@ -599,8 +615,15 @@ fn history_survives_restart_and_rejects_rollback_without_advancing() {
|
||||
_ => unreachable!(),
|
||||
}
|
||||
assert_eq!(
|
||||
store.get_manifest_anti_rollback_meta(&uri).unwrap(),
|
||||
store.get_ca_manifest_history(&test_ca_identity()).unwrap(),
|
||||
Some(meta)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
fn test_ca_identity() -> CaIdentity {
|
||||
let cert = crate::model::rc::ResourceCertificate::decode_der(&issuer_ca_fixture_der()).unwrap();
|
||||
CaIdentity::from_spki([0; 32], &cert.tbs.subject_public_key_info)
|
||||
}
|
||||
|
||||
include!("history_tests.rs");
|
||||
|
||||
@@ -32,6 +32,7 @@ pub enum RunError {
|
||||
/// 3) signed object processing (ROA/ASPA) and public report/CCR output
|
||||
pub fn run_publication_point_once(
|
||||
store: &RocksStore,
|
||||
anchor_namespace: &[u8; 32],
|
||||
policy: &Policy,
|
||||
rrdp_notification_uri: Option<&str>,
|
||||
rsync_base_uri: &str,
|
||||
@@ -46,6 +47,7 @@ pub fn run_publication_point_once(
|
||||
validation_time: time::OffsetDateTime,
|
||||
) -> Result<RunOutput, RunError> {
|
||||
let handle = CaInstanceHandle {
|
||||
anchor_namespace: *anchor_namespace,
|
||||
depth: 0,
|
||||
tal_id: "single-publication-point".to_string(),
|
||||
parent_manifest_rsync_uri: None,
|
||||
@@ -55,6 +57,12 @@ pub fn run_publication_point_once(
|
||||
effective_as_resources: issuer_effective_as.cloned(),
|
||||
rsync_base_uri: rsync_base_uri.to_string(),
|
||||
manifest_rsync_uri: manifest_rsync_uri.to_string(),
|
||||
manifest_sia_uris: crate::validation::ca_instance::ca_instance_uris_from_ca_certificate(
|
||||
&crate::model::rc::ResourceCertificate::decode_der(issuer_ca_der)
|
||||
.map_err(|e| RunError::Runner(e.to_string()))?,
|
||||
)
|
||||
.map_err(|e| RunError::Runner(e.to_string()))?
|
||||
.manifest_sia_uris,
|
||||
publication_point_rsync_uri: publication_point_rsync_uri.to_string(),
|
||||
rrdp_notification_uri: rrdp_notification_uri.map(str::to_string),
|
||||
};
|
||||
|
||||
@@ -364,6 +364,10 @@ pub fn root_handle_from_trust_anchor(
|
||||
) -> CaInstanceHandle {
|
||||
let ta_rc = trust_anchor.ta_certificate.rc_ca.clone();
|
||||
CaInstanceHandle {
|
||||
anchor_namespace: <sha2::Sha256 as sha2::Digest>::digest(
|
||||
&trust_anchor.tal.subject_public_key_info_der,
|
||||
)
|
||||
.into(),
|
||||
depth: 0,
|
||||
tal_id,
|
||||
parent_manifest_rsync_uri: None,
|
||||
@@ -373,6 +377,7 @@ pub fn root_handle_from_trust_anchor(
|
||||
effective_as_resources: ta_rc.tbs.extensions.as_resources.clone(),
|
||||
rsync_base_uri: ca_instance.rsync_base_uri.clone(),
|
||||
manifest_rsync_uri: ca_instance.manifest_rsync_uri.clone(),
|
||||
manifest_sia_uris: ca_instance.manifest_sia_uris.clone(),
|
||||
publication_point_rsync_uri: ca_instance.publication_point_rsync_uri.clone(),
|
||||
rrdp_notification_uri: ca_instance.rrdp_notification_uri.clone(),
|
||||
}
|
||||
|
||||
@@ -118,6 +118,7 @@ impl CaCertificateRef {
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct CaInstanceHandle {
|
||||
pub anchor_namespace: [u8; 32],
|
||||
pub depth: usize,
|
||||
pub tal_id: String,
|
||||
pub parent_manifest_rsync_uri: Option<String>,
|
||||
@@ -135,6 +136,7 @@ pub struct CaInstanceHandle {
|
||||
pub effective_as_resources: Option<AsResourceSet>,
|
||||
pub rsync_base_uri: String,
|
||||
pub manifest_rsync_uri: String,
|
||||
pub manifest_sia_uris: Vec<String>,
|
||||
pub publication_point_rsync_uri: String,
|
||||
pub rrdp_notification_uri: Option<String>,
|
||||
}
|
||||
|
||||
@@ -308,6 +308,7 @@ fn discover_children_from_fresh_snapshot_with_audit_with_issuer_der<P: Publicati
|
||||
let child_hash = sha256_hex_from_32(&file.sha256);
|
||||
children.push(DiscoveredChildCaInstance {
|
||||
handle: CaInstanceHandle {
|
||||
anchor_namespace: issuer.anchor_namespace,
|
||||
depth: 0,
|
||||
tal_id: issuer.tal_id.clone(),
|
||||
parent_manifest_rsync_uri: Some(issuer.manifest_rsync_uri.clone()),
|
||||
@@ -317,6 +318,7 @@ fn discover_children_from_fresh_snapshot_with_audit_with_issuer_der<P: Publicati
|
||||
effective_as_resources: validated.effective_as_resources.clone(),
|
||||
rsync_base_uri: uris.rsync_base_uri.clone(),
|
||||
manifest_rsync_uri: uris.manifest_rsync_uri.clone(),
|
||||
manifest_sia_uris: uris.manifest_sia_uris.clone(),
|
||||
publication_point_rsync_uri: uris.publication_point_rsync_uri.clone(),
|
||||
rrdp_notification_uri: uris.rrdp_notification_uri.clone(),
|
||||
},
|
||||
|
||||
@@ -27,10 +27,45 @@ impl<'a> Rpkiv1PublicationPointRunner<'a> {
|
||||
let snapshot_prepare_started = std::time::Instant::now();
|
||||
let issuer_ca_der = ca_certificate_der_for_validation(ca, self.store, self.timing.as_ref())
|
||||
.map_err(|detail| FreshPublicationPointStageError {
|
||||
warnings: Vec::new(),
|
||||
error: ManifestFreshError::IssuerCaLoadFailed { detail },
|
||||
snapshot_prepare_ms: snapshot_prepare_started.elapsed().as_millis() as u64,
|
||||
})?;
|
||||
let issuer_ca_der: Arc<[u8]> = Arc::from(issuer_ca_der.as_ref());
|
||||
let identity = ResourceCertificate::decode_der(&issuer_ca_der)
|
||||
.map(|cert| {
|
||||
crate::repository::storage::CaIdentity::from_spki(
|
||||
ca.anchor_namespace,
|
||||
&cert.tbs.subject_public_key_info,
|
||||
)
|
||||
})
|
||||
.map_err(|e| FreshPublicationPointStageError {
|
||||
warnings: Vec::new(),
|
||||
error: ManifestFreshError::IssuerCaLoadFailed {
|
||||
detail: e.to_string(),
|
||||
},
|
||||
snapshot_prepare_ms: snapshot_prepare_started.elapsed().as_millis() as u64,
|
||||
})?;
|
||||
let rename_warnings: Vec<Warning> = crate::validation::manifest::manifest_rename_warning(
|
||||
self.store,
|
||||
&identity,
|
||||
&ca.manifest_sia_uris,
|
||||
&ca.manifest_rsync_uri,
|
||||
)
|
||||
.map_err(|error| FreshPublicationPointStageError {
|
||||
warnings: Vec::new(),
|
||||
error,
|
||||
snapshot_prepare_ms: snapshot_prepare_started.elapsed().as_millis() as u64,
|
||||
})?
|
||||
.into_iter()
|
||||
.collect();
|
||||
for warning in &rename_warnings {
|
||||
crate::logging::emit(
|
||||
crate::logging::Level::Warn,
|
||||
"manifest_filename_changed",
|
||||
|| serde_json::json!({"manifest_rsync_uri": ca.manifest_rsync_uri, "message": warning.message, "candidate_accepted": null}),
|
||||
);
|
||||
}
|
||||
let fresh_publication_point = {
|
||||
let _manifest_total = self
|
||||
.timing
|
||||
@@ -38,6 +73,7 @@ impl<'a> Rpkiv1PublicationPointRunner<'a> {
|
||||
.map(|t| t.span_phase("manifest_processing_total"));
|
||||
process_manifest_publication_point_fresh_after_repo_sync_with_timing(
|
||||
self.store,
|
||||
&ca.anchor_namespace,
|
||||
&ca.manifest_rsync_uri,
|
||||
&ca.publication_point_rsync_uri,
|
||||
self.current_repo_index.as_ref(),
|
||||
@@ -51,6 +87,16 @@ impl<'a> Rpkiv1PublicationPointRunner<'a> {
|
||||
let snapshot_prepare_ms = snapshot_prepare_started.elapsed().as_millis() as u64;
|
||||
let (fresh_point, snapshot_prepare_timing) =
|
||||
fresh_publication_point.map_err(|error| FreshPublicationPointStageError {
|
||||
warnings: rename_warnings
|
||||
.iter()
|
||||
.cloned()
|
||||
.map(|mut warning| {
|
||||
warning
|
||||
.message
|
||||
.push_str(&format!(" candidate_accepted=false reason={error}"));
|
||||
warning
|
||||
})
|
||||
.collect(),
|
||||
error,
|
||||
snapshot_prepare_ms,
|
||||
})?;
|
||||
@@ -152,7 +198,7 @@ impl<'a> Rpkiv1PublicationPointRunner<'a> {
|
||||
self.policy,
|
||||
)
|
||||
};
|
||||
let (discovered_children, child_audits, discovered_router_keys, warnings) = match out {
|
||||
let (discovered_children, child_audits, discovered_router_keys, mut warnings) = match out {
|
||||
Ok(out) => (out.children, out.audits, out.router_keys, Vec::new()),
|
||||
Err(e) => (
|
||||
Vec::new(),
|
||||
@@ -188,6 +234,7 @@ impl<'a> Rpkiv1PublicationPointRunner<'a> {
|
||||
child_discovery_ms,
|
||||
);
|
||||
|
||||
warnings.extend(rename_warnings);
|
||||
Ok(FreshPublicationPointStage {
|
||||
fresh_point,
|
||||
issuer_ca_der,
|
||||
@@ -205,7 +252,7 @@ impl<'a> Rpkiv1PublicationPointRunner<'a> {
|
||||
&self,
|
||||
ca: &CaInstanceHandle,
|
||||
fresh_point: &FreshValidatedPublicationPoint,
|
||||
warnings: Vec<Warning>,
|
||||
mut warnings: Vec<Warning>,
|
||||
objects: crate::validation::objects::ObjectsOutput,
|
||||
child_audits: Vec<ObjectAuditEntry>,
|
||||
discovered_children: Vec<DiscoveredChildCaInstance>,
|
||||
@@ -229,6 +276,7 @@ impl<'a> Rpkiv1PublicationPointRunner<'a> {
|
||||
snapshot_pack_ms,
|
||||
);
|
||||
|
||||
let mut ccr_projection = None;
|
||||
let mut ccr_projection_build_ms = 0;
|
||||
let mut ccr_append_ms = 0;
|
||||
if self.ccr_accumulator.is_some() {
|
||||
@@ -254,9 +302,30 @@ impl<'a> Rpkiv1PublicationPointRunner<'a> {
|
||||
let ccr_manifest_projection =
|
||||
crate::ccr::projection::from_snapshot(ca, &pack, subordinate_skis)?;
|
||||
ccr_projection_build_ms = ccr_projection_build_started.elapsed().as_millis() as u64;
|
||||
let ccr_append_started = std::time::Instant::now();
|
||||
self.append_ccr_manifest_projection(&ccr_manifest_projection)?;
|
||||
ccr_append_ms = ccr_append_started.elapsed().as_millis() as u64;
|
||||
ccr_projection = Some(ccr_manifest_projection);
|
||||
}
|
||||
let (_, commit_warning) = crate::validation::manifest::commit_manifest_history(
|
||||
self.store,
|
||||
&fresh_point.history_candidate,
|
||||
)
|
||||
.map_err(|error| format!("persist manifest freshness failed: {error}"))?;
|
||||
if let Some(warning) = commit_warning {
|
||||
warnings.retain(|item| !item.message.starts_with("manifest filename changed:"));
|
||||
warnings.push(warning);
|
||||
}
|
||||
for warning in &mut warnings {
|
||||
if warning.message.starts_with("manifest filename changed:") {
|
||||
warning.message.push_str(&format!(
|
||||
" new_number={} new_this_update={} candidate_accepted=true",
|
||||
hex::encode_upper(&pack.manifest_number_be),
|
||||
pack.this_update.rfc3339_utc
|
||||
));
|
||||
}
|
||||
}
|
||||
if let Some(projection) = ccr_projection {
|
||||
let started = std::time::Instant::now();
|
||||
self.append_ccr_manifest_projection(&projection)?;
|
||||
ccr_append_ms = started.elapsed().as_millis() as u64;
|
||||
}
|
||||
if let Some(timing) = self.timing.as_ref() {
|
||||
timing.record_phase_nanos(
|
||||
@@ -305,21 +374,6 @@ impl<'a> Rpkiv1PublicationPointRunner<'a> {
|
||||
audit_build_ms,
|
||||
);
|
||||
|
||||
// Commit protocol freshness only after successful publication-point
|
||||
// validation/finalization, independently of optional CCR output.
|
||||
self.store
|
||||
.put_manifest_anti_rollback_meta(
|
||||
&crate::repository::storage::ManifestAntiRollbackMeta {
|
||||
manifest_rsync_uri: pack.manifest_rsync_uri.clone(),
|
||||
manifest_number_be: pack.manifest_number_be.clone(),
|
||||
manifest_this_update: pack.this_update.clone(),
|
||||
manifest_sha256: <sha2::Sha256 as sha2::Digest>::digest(&pack.manifest_bytes)
|
||||
.to_vec(),
|
||||
updated_at_validation_time: pack.verified_at.clone(),
|
||||
},
|
||||
)
|
||||
.map_err(|error| format!("persist manifest freshness failed: {error}"))?;
|
||||
|
||||
Ok(FreshPublicationPointFinalizeOutput {
|
||||
result: PublicationPointRunResult {
|
||||
source: PublicationPointSource::Fresh,
|
||||
|
||||
@@ -97,7 +97,7 @@ impl<'a> PublicationPointRunner for Rpkiv1PublicationPointRunner<'a> {
|
||||
let stage = match stage {
|
||||
Ok(stage) => stage,
|
||||
Err(stage_error) => {
|
||||
let mut warnings = Vec::new();
|
||||
let mut warnings = stage_error.warnings;
|
||||
warnings.push(
|
||||
Warning::new(format!(
|
||||
"publication point processing failed: {}",
|
||||
|
||||
@@ -13,6 +13,7 @@ pub(crate) struct FreshPublicationPointStage {
|
||||
|
||||
#[derive(Debug)]
|
||||
pub(crate) struct FreshPublicationPointStageError {
|
||||
pub(crate) warnings: Vec<Warning>,
|
||||
pub(crate) error: ManifestFreshError,
|
||||
pub(crate) snapshot_prepare_ms: u64,
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user