20260529 修复CIR输入与拒绝审计语义
This commit is contained in:
parent
57c23f19aa
commit
9f7981e117
@ -193,18 +193,11 @@ pub fn build_cir_from_run_multi(
|
||||
reject_list_sha256: Vec::new(),
|
||||
rejected_objects: Vec::new(),
|
||||
};
|
||||
let object_uri_set = cir
|
||||
.objects
|
||||
.iter()
|
||||
.map(|item| item.rsync_uri.as_str())
|
||||
.collect::<BTreeSet<_>>();
|
||||
let mut rejected_objects = publication_points
|
||||
.iter()
|
||||
.flat_map(|pp| pp.objects.iter())
|
||||
.filter(|item| {
|
||||
item.result == AuditObjectResult::Error
|
||||
&& object_uri_set.contains(item.rsync_uri.as_str())
|
||||
})
|
||||
.filter(|item| item.result == AuditObjectResult::Error)
|
||||
.filter(|item| item.rsync_uri.starts_with("rsync://"))
|
||||
.map(|item| CirRejectedObject {
|
||||
object_uri: item.rsync_uri.clone(),
|
||||
reason: item.detail.clone(),
|
||||
@ -754,7 +747,7 @@ mod tests {
|
||||
sha256_hex: "33".repeat(32),
|
||||
kind: crate::audit::AuditObjectKind::Roa,
|
||||
result: crate::audit::AuditObjectResult::Error,
|
||||
detail: Some("not in cir object set".to_string()),
|
||||
detail: Some("second rejected roa".to_string()),
|
||||
},
|
||||
],
|
||||
..PublicationPointAudit::default()
|
||||
@ -785,6 +778,18 @@ mod tests {
|
||||
cir.rejected_objects[1].object_uri,
|
||||
"rsync://example.test/repo/c.roa"
|
||||
);
|
||||
assert!(
|
||||
cir.objects
|
||||
.iter()
|
||||
.any(|item| item.rsync_uri == "rsync://example.test/repo/a.roa"),
|
||||
"rejected audit objects were still consumed as validation input",
|
||||
);
|
||||
assert!(
|
||||
cir.objects
|
||||
.iter()
|
||||
.any(|item| item.rsync_uri == "rsync://example.test/repo/c.roa"),
|
||||
"rejected audit objects were still consumed as validation input",
|
||||
);
|
||||
assert!(
|
||||
!cir.objects
|
||||
.iter()
|
||||
@ -793,6 +798,119 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn build_cir_from_run_multi_records_crl_expired_manifest_in_objects_and_rejects() {
|
||||
let td = tempfile::tempdir().unwrap();
|
||||
let store = RocksStore::open(td.path()).unwrap();
|
||||
let ta = sample_trust_anchor();
|
||||
let manifest_uri = "rsync://example.test/repo/expired-crl.mft";
|
||||
let reject_reason = "manifest embedded EE certificate path validation failed: CRL not valid at validation_time (RFC 5280 §6.3.3(g); RFC 5280 §5.1.2.4-§5.1.2.5; RFC 6487 §5)";
|
||||
let publication_points = vec![PublicationPointAudit {
|
||||
objects: vec![crate::audit::ObjectAuditEntry {
|
||||
rsync_uri: manifest_uri.to_string(),
|
||||
sha256_hex: "44".repeat(32),
|
||||
kind: crate::audit::AuditObjectKind::Manifest,
|
||||
result: crate::audit::AuditObjectResult::Error,
|
||||
detail: Some(reject_reason.to_string()),
|
||||
}],
|
||||
..PublicationPointAudit::default()
|
||||
}];
|
||||
|
||||
let cir = build_cir_from_run_multi(
|
||||
&store,
|
||||
&[CirTrustAnchorBinding {
|
||||
trust_anchor: &ta,
|
||||
tal_uri: "https://example.test/root.tal",
|
||||
}],
|
||||
sample_time(),
|
||||
&publication_points,
|
||||
None,
|
||||
)
|
||||
.expect("build cir");
|
||||
|
||||
assert!(
|
||||
cir.objects
|
||||
.iter()
|
||||
.any(|item| item.rsync_uri == manifest_uri),
|
||||
"manifest rejected because issuer CRL is expired was still read as validation input",
|
||||
);
|
||||
assert_eq!(cir.rejected_objects.len(), 1);
|
||||
assert_eq!(cir.rejected_objects[0].object_uri, manifest_uri);
|
||||
assert_eq!(
|
||||
cir.rejected_objects[0].reason.as_deref(),
|
||||
Some(reject_reason)
|
||||
);
|
||||
assert_eq!(
|
||||
cir.reject_list_sha256,
|
||||
compute_reject_list_sha256([manifest_uri].into_iter())
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn build_cir_from_run_multi_excludes_manifest_locked_files_when_manifest_is_rejected() {
|
||||
let td = tempfile::tempdir().unwrap();
|
||||
let store = RocksStore::open(td.path()).unwrap();
|
||||
let ta = sample_trust_anchor();
|
||||
let manifest_uri = "rsync://example.test/repo/rejected.mft";
|
||||
let roa_uri = "rsync://example.test/repo/listed.roa";
|
||||
let crl_uri = "rsync://example.test/repo/listed.crl";
|
||||
let publication_points = vec![PublicationPointAudit {
|
||||
objects: vec![
|
||||
crate::audit::ObjectAuditEntry {
|
||||
rsync_uri: manifest_uri.to_string(),
|
||||
sha256_hex: "44".repeat(32),
|
||||
kind: crate::audit::AuditObjectKind::Manifest,
|
||||
result: crate::audit::AuditObjectResult::Error,
|
||||
detail: Some("manifest EE cert path rejected".to_string()),
|
||||
},
|
||||
crate::audit::ObjectAuditEntry {
|
||||
rsync_uri: roa_uri.to_string(),
|
||||
sha256_hex: "55".repeat(32),
|
||||
kind: crate::audit::AuditObjectKind::Roa,
|
||||
result: crate::audit::AuditObjectResult::Skipped,
|
||||
detail: Some("manifest rejected before locked object validation".to_string()),
|
||||
},
|
||||
crate::audit::ObjectAuditEntry {
|
||||
rsync_uri: crl_uri.to_string(),
|
||||
sha256_hex: "66".repeat(32),
|
||||
kind: crate::audit::AuditObjectKind::Crl,
|
||||
result: crate::audit::AuditObjectResult::Skipped,
|
||||
detail: Some("manifest rejected before locked object validation".to_string()),
|
||||
},
|
||||
],
|
||||
..PublicationPointAudit::default()
|
||||
}];
|
||||
|
||||
let cir = build_cir_from_run_multi(
|
||||
&store,
|
||||
&[CirTrustAnchorBinding {
|
||||
trust_anchor: &ta,
|
||||
tal_uri: "https://example.test/root.tal",
|
||||
}],
|
||||
sample_time(),
|
||||
&publication_points,
|
||||
None,
|
||||
)
|
||||
.expect("build cir");
|
||||
|
||||
assert!(
|
||||
cir.objects
|
||||
.iter()
|
||||
.any(|item| item.rsync_uri == manifest_uri),
|
||||
"rejected manifest is still a current-run validation input",
|
||||
);
|
||||
assert!(
|
||||
!cir.objects.iter().any(|item| item.rsync_uri == roa_uri),
|
||||
"ROA listed by a rejected manifest must not enter CIR objects",
|
||||
);
|
||||
assert!(
|
||||
!cir.objects.iter().any(|item| item.rsync_uri == crl_uri),
|
||||
"CRL listed by a rejected manifest must not enter CIR objects",
|
||||
);
|
||||
assert_eq!(cir.rejected_objects.len(), 1);
|
||||
assert_eq!(cir.rejected_objects[0].object_uri, manifest_uri);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn build_cir_from_run_multi_reject_digest_ignores_reason_text() {
|
||||
let td = tempfile::tempdir().unwrap();
|
||||
|
||||
@ -1602,6 +1602,51 @@ mod tests {
|
||||
assert!(matches!(err, ManifestFreshError::EeCrlNotFound(_)), "{err}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn validate_manifest_embedded_ee_cert_path_rejects_expired_crl() {
|
||||
let (manifest, _, _, publication_point_rsync_uri, _) = load_manifest_fixture();
|
||||
let files = locked_files_for_manifest(&manifest, &publication_point_rsync_uri);
|
||||
let ee = &manifest.signed_object.signed_data.certificates[0];
|
||||
let crldp_uri = ee
|
||||
.resource_cert
|
||||
.tbs
|
||||
.extensions
|
||||
.crl_distribution_points_uris
|
||||
.as_ref()
|
||||
.and_then(|uris| uris.first())
|
||||
.expect("fixture manifest EE CRLDP")
|
||||
.as_str()
|
||||
.to_string();
|
||||
let crl_file = files
|
||||
.iter()
|
||||
.find(|file| file.rsync_uri == crldp_uri)
|
||||
.expect("fixture CRL referenced by manifest EE");
|
||||
let crl = crate::data_model::crl::RpkixCrl::decode_der(
|
||||
crl_file.bytes().expect("read fixture crl bytes"),
|
||||
)
|
||||
.expect("decode fixture crl");
|
||||
let validation_time = crl.next_update.utc;
|
||||
|
||||
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::EeCertPath(
|
||||
crate::validation::cert_path::CertPathError::CrlNotValidAtTime
|
||||
)
|
||||
),
|
||||
"{err}"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn load_current_instance_vcir_publication_point_returns_manifest_and_locked_files() {
|
||||
let temp = tempfile::tempdir().expect("tempdir");
|
||||
|
||||
@ -183,6 +183,36 @@ impl<'a> Rpkiv1PublicationPointRunner<'a> {
|
||||
}
|
||||
}
|
||||
|
||||
fn current_manifest_hash_hex_for_audit(&self, ca: &CaInstanceHandle) -> Option<String> {
|
||||
if let Some(index_handle) = self.current_repo_index.as_ref()
|
||||
&& let Ok(index) = index_handle.lock()
|
||||
&& let Some(entry) = index.get_by_uri(&ca.manifest_rsync_uri)
|
||||
{
|
||||
return Some(entry.current_hash_hex.clone());
|
||||
}
|
||||
|
||||
self.store
|
||||
.load_current_object_with_hash_by_uri(&ca.manifest_rsync_uri)
|
||||
.ok()
|
||||
.flatten()
|
||||
.map(|current| current.current_hash_hex)
|
||||
}
|
||||
|
||||
fn rejected_manifest_audit_entry_for_failed_fetch(
|
||||
&self,
|
||||
ca: &CaInstanceHandle,
|
||||
fresh_err: &ManifestFreshError,
|
||||
) -> Option<ObjectAuditEntry> {
|
||||
let sha256_hex = self.current_manifest_hash_hex_for_audit(ca)?;
|
||||
Some(ObjectAuditEntry {
|
||||
rsync_uri: ca.manifest_rsync_uri.clone(),
|
||||
sha256_hex,
|
||||
kind: AuditObjectKind::Manifest,
|
||||
result: AuditObjectResult::Error,
|
||||
detail: Some(fresh_err.to_string()),
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) fn stage_fresh_publication_point_after_repo_ready(
|
||||
&self,
|
||||
ca: &CaInstanceHandle,
|
||||
@ -811,13 +841,21 @@ impl<'a> PublicationPointRunner for Rpkiv1PublicationPointRunner<'a> {
|
||||
}
|
||||
crate::policy::CaFailedFetchPolicy::ReuseCurrentInstanceVcir => {
|
||||
let projection_started = std::time::Instant::now();
|
||||
let projection = project_current_instance_vcir_on_failed_fetch(
|
||||
let mut projection = project_current_instance_vcir_on_failed_fetch(
|
||||
self.store,
|
||||
ca,
|
||||
&fresh_err,
|
||||
self.validation_time,
|
||||
)
|
||||
.map_err(|e| format!("failed fetch VCIR projection failed: {e}"))?;
|
||||
if matches!(
|
||||
projection.source,
|
||||
PublicationPointSource::FailedFetchNoCache
|
||||
) && let Some(rejected_manifest) =
|
||||
self.rejected_manifest_audit_entry_for_failed_fetch(ca, &fresh_err)
|
||||
{
|
||||
projection.objects.audit.push(rejected_manifest);
|
||||
}
|
||||
self.append_ccr_manifest_projection_from_reuse(&projection)?;
|
||||
let projection_ms = projection_started.elapsed().as_millis() as u64;
|
||||
warnings.extend(projection.warnings.clone());
|
||||
@ -1849,6 +1887,40 @@ fn build_publication_point_audit_from_vcir(
|
||||
};
|
||||
};
|
||||
|
||||
if source == PublicationPointSource::FailedFetchNoCache {
|
||||
let mut objects_out = Vec::with_capacity(objects.audit.len() + child_audits.len());
|
||||
objects_out.extend(child_audits.iter().cloned());
|
||||
objects_out.extend(objects.audit.iter().cloned());
|
||||
return PublicationPointAudit {
|
||||
node_id: None,
|
||||
parent_node_id: None,
|
||||
discovered_from: None,
|
||||
rsync_base_uri: ca.rsync_base_uri.clone(),
|
||||
manifest_rsync_uri: ca.manifest_rsync_uri.clone(),
|
||||
publication_point_rsync_uri: ca.publication_point_rsync_uri.clone(),
|
||||
rrdp_notification_uri: ca.rrdp_notification_uri.clone(),
|
||||
source: source_label(source),
|
||||
repo_sync_source: repo_sync_source.map(ToString::to_string),
|
||||
repo_sync_phase: repo_sync_phase.map(ToString::to_string),
|
||||
repo_sync_duration_ms,
|
||||
repo_sync_error: repo_sync_error.map(ToString::to_string),
|
||||
repo_terminal_state: terminal_state_label(source).to_string(),
|
||||
this_update_rfc3339_utc: vcir
|
||||
.validated_manifest_meta
|
||||
.validated_manifest_this_update
|
||||
.rfc3339_utc
|
||||
.clone(),
|
||||
next_update_rfc3339_utc: vcir
|
||||
.validated_manifest_meta
|
||||
.validated_manifest_next_update
|
||||
.rfc3339_utc
|
||||
.clone(),
|
||||
verified_at_rfc3339_utc: vcir.last_successful_validation_time.rfc3339_utc.clone(),
|
||||
warnings,
|
||||
objects: objects_out,
|
||||
};
|
||||
}
|
||||
|
||||
let mut audit_by_uri: HashMap<String, ObjectAuditEntry> = HashMap::new();
|
||||
for artifact in &vcir.related_artifacts {
|
||||
let Some(uri) = artifact.uri.as_ref() else {
|
||||
@ -6428,6 +6500,140 @@ authorityKeyIdentifier = keyid:always
|
||||
}));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn build_publication_point_audit_from_vcir_failed_no_cache_keeps_current_reject_only() {
|
||||
let now = time::OffsetDateTime::now_utc();
|
||||
let child_cert_hash = sha256_hex(b"child-cert");
|
||||
let vcir = sample_vcir_for_projection(now, &child_cert_hash);
|
||||
|
||||
let ca = CaInstanceHandle {
|
||||
depth: 0,
|
||||
tal_id: "test-tal".to_string(),
|
||||
parent_manifest_rsync_uri: None,
|
||||
ca_certificate_der: Vec::new(),
|
||||
ca_certificate_rsync_uri: None,
|
||||
effective_ip_resources: None,
|
||||
effective_as_resources: None,
|
||||
rsync_base_uri: "rsync://example.test/repo/issuer/".to_string(),
|
||||
manifest_rsync_uri: vcir.manifest_rsync_uri.clone(),
|
||||
publication_point_rsync_uri: "rsync://example.test/repo/issuer/".to_string(),
|
||||
rrdp_notification_uri: Some("https://example.test/notify.xml".to_string()),
|
||||
};
|
||||
let objects = crate::validation::objects::ObjectsOutput {
|
||||
vrps: Vec::new(),
|
||||
aspas: Vec::new(),
|
||||
router_keys: Vec::new(),
|
||||
local_outputs_cache: Vec::new(),
|
||||
warnings: Vec::new(),
|
||||
stats: crate::validation::objects::ObjectsStats::default(),
|
||||
audit: vec![ObjectAuditEntry {
|
||||
rsync_uri: vcir.current_manifest_rsync_uri.clone(),
|
||||
sha256_hex: sha256_hex(b"current-manifest"),
|
||||
kind: AuditObjectKind::Manifest,
|
||||
result: AuditObjectResult::Error,
|
||||
detail: Some("manifest is not valid at validation_time".to_string()),
|
||||
}],
|
||||
};
|
||||
|
||||
let audit = build_publication_point_audit_from_vcir(
|
||||
&ca,
|
||||
PublicationPointSource::FailedFetchNoCache,
|
||||
Some("rsync"),
|
||||
Some("rsync_only_ok"),
|
||||
Some(123),
|
||||
None,
|
||||
Some(&vcir),
|
||||
None,
|
||||
&[Warning::new("latest VCIR instance_gate expired")],
|
||||
&objects,
|
||||
&[],
|
||||
);
|
||||
|
||||
assert_eq!(audit.source, "failed_fetch_no_cache");
|
||||
assert_eq!(audit.repo_terminal_state, "failed_no_cache");
|
||||
assert_eq!(
|
||||
audit.this_update_rfc3339_utc,
|
||||
vcir.validated_manifest_meta
|
||||
.validated_manifest_this_update
|
||||
.rfc3339_utc
|
||||
);
|
||||
assert_eq!(audit.objects.len(), 1);
|
||||
assert_eq!(audit.objects[0].rsync_uri, vcir.current_manifest_rsync_uri);
|
||||
assert!(matches!(audit.objects[0].result, AuditObjectResult::Error));
|
||||
assert!(
|
||||
!audit
|
||||
.objects
|
||||
.iter()
|
||||
.any(|entry| entry.rsync_uri == "rsync://example.test/repo/issuer/a.roa"),
|
||||
"failed-no-cache must not expand old VCIR related artifacts into current-run audit",
|
||||
);
|
||||
assert!(
|
||||
!audit
|
||||
.objects
|
||||
.iter()
|
||||
.any(|entry| entry.rsync_uri == "rsync://example.test/repo/issuer/issuer.crl"),
|
||||
"failed-no-cache must not expose old CRL as current-run CIR input",
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rejected_manifest_audit_entry_for_failed_fetch_uses_current_repo_hash() {
|
||||
let store_dir = tempfile::tempdir().expect("store dir");
|
||||
let store = RocksStore::open(store_dir.path()).expect("open rocksdb");
|
||||
let policy = Policy::default();
|
||||
let runner = sample_runner_with_ccr_accumulator(&store, &policy);
|
||||
let manifest_uri = "rsync://example.test/repo/issuer/issuer.mft";
|
||||
let manifest_hash = sha256_hex(b"manifest-bytes");
|
||||
store
|
||||
.put_blob_bytes_batch(&[(manifest_hash.clone(), b"manifest-bytes".to_vec())])
|
||||
.expect("put manifest bytes");
|
||||
store
|
||||
.put_repository_view_entry(&crate::storage::RepositoryViewEntry {
|
||||
rsync_uri: manifest_uri.to_string(),
|
||||
current_hash: Some(manifest_hash.clone()),
|
||||
repository_source: Some("rsync://example.test/repo/issuer/".to_string()),
|
||||
object_type: Some("mft".to_string()),
|
||||
state: crate::storage::RepositoryViewState::Present,
|
||||
})
|
||||
.expect("put repository view");
|
||||
let ca = CaInstanceHandle {
|
||||
depth: 0,
|
||||
tal_id: "test-tal".to_string(),
|
||||
parent_manifest_rsync_uri: None,
|
||||
ca_certificate_der: Vec::new(),
|
||||
ca_certificate_rsync_uri: None,
|
||||
effective_ip_resources: None,
|
||||
effective_as_resources: None,
|
||||
rsync_base_uri: "rsync://example.test/repo/issuer/".to_string(),
|
||||
manifest_rsync_uri: manifest_uri.to_string(),
|
||||
publication_point_rsync_uri: "rsync://example.test/repo/issuer/".to_string(),
|
||||
rrdp_notification_uri: None,
|
||||
};
|
||||
|
||||
let entry = runner
|
||||
.rejected_manifest_audit_entry_for_failed_fetch(
|
||||
&ca,
|
||||
&ManifestFreshError::StaleOrEarly {
|
||||
this_update_rfc3339_utc: "2026-05-27T08:37:07Z".to_string(),
|
||||
next_update_rfc3339_utc: "2026-05-28T10:01:07Z".to_string(),
|
||||
validation_time_rfc3339_utc: "2026-05-28T10:11:00Z".to_string(),
|
||||
},
|
||||
)
|
||||
.expect("rejected manifest audit entry");
|
||||
|
||||
assert_eq!(entry.rsync_uri, manifest_uri);
|
||||
assert_eq!(entry.sha256_hex, manifest_hash);
|
||||
assert_eq!(entry.kind, AuditObjectKind::Manifest);
|
||||
assert_eq!(entry.result, AuditObjectResult::Error);
|
||||
assert!(
|
||||
entry
|
||||
.detail
|
||||
.as_deref()
|
||||
.unwrap_or("")
|
||||
.contains("manifest is not valid at validation_time")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn build_publication_point_audit_from_vcir_without_cached_inputs_returns_empty_listing() {
|
||||
let ca = CaInstanceHandle {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user